henry pushed to branch tor-browser-150.0a1-16.0-2 at The Tor Project / Applications / Tor Browser Commits: b61277be by Henry Wilkes at 2026-04-28T12:46:37+01:00 fixup! TB 40933: Add tor-launcher functionality TB 44796: Convert `TorProviders` types to strings. This allows them to be used in log messages. - - - - - 6a41ffd8 by Henry Wilkes at 2026-04-28T12:53:36+01:00 fixup! TB 40933: Add tor-launcher functionality TB 44796: Add `TorProviderState`, which tracks the state of a provider. We create a new `TorProviderBase` class which tracks this state, and the existing `TorProvider` now extends. `TorProviderBuilder.sys.mjs`: + We provide a callback to let the current provider instance let `TorProviderBuilder` know when this state has changed. + This will emit `TorProviderSateChanged` as a replacement for `TorProcessExited`. The `Stopped` state has the same role as the previous event, except it is more generic. This event also lets any consumers know when a provider has been just replaced (`Starting`) and successfully initialised (`Running`). `TorControlPort.sys.mjs`: + Add `onClosed` callback to signal to the `TorProvider` that the control port has closed, which will trigger `_stoppedInternal`. + Delay adding the event handler so the `TorProvider` only listens to a control port after it is successfully set up and adopted by the `TorProvider`. `TorProvider.sys.mjs`: + `TorProvider` no longer emits `TorProviderTopics.ProcessExited`. + Instead, `TorProvider` calls `_stoppedInternal` whenever the provider stops running. I.e. whenever the control port is closed, which includes when the process exits but also other scenarios where the control port might close unexpectedly. - - - - - 7167229f by Henry Wilkes at 2026-04-28T12:53:37+01:00 fixup! TB 40933: Add tor-launcher functionality TB 44796: Convert the "none" provider into a `TorProviderNone` class, so it can be handled by the same logic as `TorProvider`. - - - - - adba934e by Henry Wilkes at 2026-04-28T12:53:39+01:00 fixup! TB 40933: Add tor-launcher functionality TB 44796: Add the `settledState` method to allow `TorConnect` to wait for the first provider's state after it has settled. This is a way to get the current providers state prior to having listened to `TorProviderStateChange`. Add the `replace` method to allow `TorConnect` to request a new provider on the user's behalf. - - - - - e74c8d84 by Henry Wilkes at 2026-04-28T12:53:40+01:00 fixup! TB 40933: Add tor-launcher functionality TB 44796: Drop `#torExited` and internals of `firstWindowLoaded` and instead use promptProviderState which handles both. These are only temporary changes to be dropped in tor-browser#43570. - - - - - 615707ff by Henry Wilkes at 2026-04-28T12:53:41+01:00 fixup! TB 40597: Implement TorSettings module TB 44796: Listen for ProviderStateChanged rather than ProcessStopped. - - - - - 48f5bfb2 by Henry Wilkes at 2026-04-28T12:53:42+01:00 fixup! TB 40933: Add tor-launcher functionality TB 44796: Drop `TorProvider.isRunning`. - - - - - 424f8e1a by Henry Wilkes at 2026-04-28T12:53:43+01:00 fixup! TB 40597: Implement TorSettings module TB 44796: Use `TorProvider.state` instead of `isRunning`. - - - - - 8 changed files: - toolkit/components/tor-launcher/TorControlPort.sys.mjs - toolkit/components/tor-launcher/TorProvider.sys.mjs - + toolkit/components/tor-launcher/TorProviderBase.sys.mjs - toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs - + toolkit/components/tor-launcher/TorProviderNone.sys.mjs - toolkit/components/tor-launcher/moz.build - toolkit/modules/TorConnect.sys.mjs - toolkit/modules/TorSettings.sys.mjs Changes: ===================================== toolkit/components/tor-launcher/TorControlPort.sys.mjs ===================================== @@ -384,21 +384,19 @@ export class TorController { /** * The event handler. * - * @type {TorEventHandler} + * @type {?TorEventHandler} */ - #eventHandler; + #eventHandler = null; /** * Connect to a control port over a Unix socket. * Not available on Windows. * * @param {nsIFile} ipcFile The path to the Unix socket to connect to - * @param {TorEventHandler} eventHandler The event handler to use for - * asynchronous notifications * @returns {TorController} */ - static fromIpcFile(ipcFile, eventHandler) { - return new TorController(AsyncSocket.fromIpcFile(ipcFile), eventHandler); + static fromIpcFile(ipcFile) { + return new TorController(AsyncSocket.fromIpcFile(ipcFile)); } /** @@ -406,15 +404,10 @@ export class TorController { * * @param {string} host The hostname to connect to * @param {number} port The port to connect the to - * @param {TorEventHandler} eventHandler The event handler to use for - * asynchronous notifications * @returns {TorController} */ - static fromSocketAddress(host, port, eventHandler) { - return new TorController( - AsyncSocket.fromSocketAddress(host, port), - eventHandler - ); + static fromSocketAddress(host, port) { + return new TorController(AsyncSocket.fromSocketAddress(host, port)); } /** @@ -425,15 +418,22 @@ export class TorController { * * @private * @param {AsyncSocket} socket The socket to use - * @param {TorEventHandler} eventHandler The event handler to use for - * asynchronous notifications */ - constructor(socket, eventHandler) { + constructor(socket) { this.#socket = socket; - this.#eventHandler = eventHandler; this.#startMessagePump(); } + /** + * Set an event handler for this instance. + * + * @param {TorEventHandler} eventHandler The event handler to use for + * asynchronous notifications + */ + setEventHandler(eventHandler) { + this.#eventHandler = eventHandler; + } + // Socket and communication handling /** @@ -649,6 +649,7 @@ export class TorController { this.#socket?.close(); } finally { this.#socket = null; + this.#eventHandler?.onClosed(); } } @@ -1324,6 +1325,7 @@ export class TorController { * The controller owner can implement this methods to receive asynchronous * notifications from the controller. * + * @property {OnClosed} onClosed Called when the socket is closed. * @property {OnBootstrapStatus} onBootstrapStatus Called when a bootstrap * status is received (i.e., a STATUS_CLIENT event with a BOOTSTRAP action) * @property {OnLogMessage} onLogMessage Called when a log message is received @@ -1336,6 +1338,9 @@ export class TorController { * a connect cell along a circuit (i.e., a STREAM event with a SENTCONNECT * status) */ +/** + * @callback OnClosed + */ /** * @callback OnBootstrapStatus * ===================================== toolkit/components/tor-launcher/TorProvider.sys.mjs ===================================== @@ -6,6 +6,7 @@ import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs"; import { TorLauncherUtil } from "resource://gre/modules/TorLauncherUtil.sys.mjs"; import { TorParsers } from "resource://gre/modules/TorParsers.sys.mjs"; +import { TorProviderBase } from "resource://gre/modules/TorProviderBase.sys.mjs"; import { TorBootstrapError, TorProviderTopics, @@ -88,7 +89,7 @@ const TorConfigKeys = Object.freeze({ * It can start a new tor instance, or connect to an existing one. * In the former case, it also takes its ownership by default. */ -export class TorProvider { +export class TorProvider extends TorProviderBase { /** * The control port settings. * @@ -184,7 +185,7 @@ export class TorProvider { * Starts a new tor process and connect to its control port, or connect to the * control port of an existing tor daemon. */ - async init() { + async _initInternal() { logger.debug("Initializing the Tor provider."); // These settings might be customized in the following steps. @@ -259,7 +260,7 @@ export class TorProvider { * control connection is closed. Therefore, as a matter of facts, calling this * function also makes the child Tor instance stop. */ - uninit() { + async _uninitInternal() { logger.debug("Uninitializing the Tor provider."); if (this.#torProcess) { @@ -509,17 +510,6 @@ export class TorProvider { return TorLauncherUtil.shouldStartAndOwnTor; } - /** - * TODO: Rename to isReady once we remove finish the migration. - * - * @returns {boolean} true if we currently have a connection to the control - * port. We take for granted that if we have one, we authenticated to it, and - * so we have already verified we can send and receive data. - */ - get isRunning() { - return this.#controlConnection?.isOpen ?? false; - } - /** * Return the data about the current bridge, if any, or null. * We can detect bridge only when the configured bridge lines include the @@ -740,6 +730,11 @@ export class TorProvider { }; tryConnect(); }); + // The previous TorControlPort instances may have failed, in which case we + // want to ignore their events (such as `onClose`). This instance has just + // succeeded and will be owned by this TorProvider instance, therefore we + // want to start listening to its events now. + this.#controlConnection.setEventHandler(this); // The following code will never throw, but we still want to wait for it // before marking the provider as initialized. @@ -751,7 +746,6 @@ export class TorProvider { this.#torProcess.onExit = exitCode => { logger.info(`The tor process exited with code ${exitCode}`); this.#closeConnection("The tor process exited suddenly"); - Services.obs.notifyObservers(null, TorProviderTopics.ProcessExited); }; if (!TorLauncherUtil.shouldOnlyConfigureTor) { await this.#takeOwnership(); @@ -823,14 +817,12 @@ export class TorProvider { let controlPort; if (this.#controlPortSettings.ipcFile) { controlPort = lazy.TorController.fromIpcFile( - this.#controlPortSettings.ipcFile, - this + this.#controlPortSettings.ipcFile ); } else { controlPort = lazy.TorController.fromSocketAddress( this.#controlPortSettings.host, - this.#controlPortSettings.port, - this + this.#controlPortSettings.port ); } try { @@ -864,21 +856,27 @@ export class TorProvider { * attempt) */ #closeConnection(reason) { - this.#cancelConnection(reason); - if (this.#controlConnection) { - logger.info("Closing the control connection", reason); - try { - this.#controlConnection.close(); - } catch (e) { - logger.error("Failed to close the control port connection", e); + try { + this.#cancelConnection(reason); + if (this.#controlConnection) { + logger.info("Closing the control connection", reason); + try { + this.#controlConnection.close(); + } catch (e) { + logger.error("Failed to close the control port connection", e); + } + this.#controlConnection = null; + } else { + logger.trace( + "Requested to close an already closed control port connection" + ); } - this.#controlConnection = null; - } else { - logger.trace( - "Requested to close an already closed control port connection" - ); + } finally { + this.#lastWarning = {}; + // #controlConnection.close() may already trigger onClosed, but we + // call it once more for error cases as well. + this._stoppedInternal(); } - this.#lastWarning = {}; } // Authentication @@ -975,6 +973,13 @@ export class TorProvider { // Notification handlers + /** + * Notification that the control port has closed. + */ + onClosed() { + this._stoppedInternal(); + } + /** * Receive and process a notification with the bootstrap status. * ===================================== toolkit/components/tor-launcher/TorProviderBase.sys.mjs ===================================== @@ -0,0 +1,200 @@ +import { + TorProviderState, + TorProviderInitError, +} from "resource://gre/modules/TorProviderBuilder.sys.mjs"; + +/** + * @callback StateChangedCallback + */ + +/** + * The base class for tor providers. + */ +export class TorProviderBase { + /** + * The current provider state. + * + * @type {string} + */ + #state = TorProviderState.Starting; + + /** + * The callback for when our state changes. `null` once the `uninit` method is + * called. + * + * @type {?StateChangedCallback} + */ + #stateChangedCallback; + + /** + * The promise to return from the `init` method. + * + * @type {?Promise<undefined>} + */ + #initPromise = null; + + /** + * The promise to return from the `uninit` method. + * + * @type {?Promise<undefined>} + */ + #uninitPromise = null; + + /** + * Create a new provider. + * + * @param {StateChangedCallback} stateChangedCallback - A callback to let an + * owner know that a provider's state has changed. + */ + // NOTE: This should *not* be overridden by implementations. + constructor(stateChangedCallback) { + this.#stateChangedCallback = stateChangedCallback; + } + + /** + * Initialize the provider and wait for it to be `Running`. + * + * This is safe to call multiple times, with each call waiting for the + * provider to be ready. + */ + // NOTE: This should *not* be overridden by implementations. + async init() { + if (!this.#initPromise) { + this.#initPromise = this._initInternal().then( + () => { + this.#setState(TorProviderState.Running); + }, + error => { + this.#setState(TorProviderState.Stopped); + // Wrap the error in `TorProviderInitError` to let callers know that + // this error is an initialization error. + throw new TorProviderInitError(error); + } + ); + } + return this.#initPromise; + } + + /** + * Uninitialize the provider and wait for it to be cleaned up. + * + * This is safe to call multiple times, with each call waiting for the + * clean up. + */ + // NOTE: This should *not* be overridden by implementations. + async uninit() { + if (!this.#uninitPromise) { + this.#setState(TorProviderState.Stopped); + this.#stateChangedCallback = null; + this.#uninitPromise = this._uninitInternal(); + } + return this.#uninitPromise; + } + + /** + * The current `TorProviderState` state of the provider. + * + * @type {string} + */ + // NOTE: This should *not* be overridden by implementations. + get state() { + return this.#state; + } + + /** + * Set the state of the provider. Announcing this via a callback. + * + * @param {string} state - The new `TorProviderState`. + */ + #setState(state) { + if (state === this.#state) { + return; + } + if (this.#state === TorProviderState.Stopped) { + // Ignore any changes away from the `Stopped` state, which is unexpected + // since implementations can only trigger the `Stopped` state, and + // everything else is handled by `TorProviderBase`. + return; + } + this.#state = state; + this.#stateChangedCallback?.(); + } + + /** + * An internal method to be called by implementations when they stop working. + * + * Optional and safe to call as part of `_uninitInternal`. + */ + // NOTE: This should *not* be overridden by implementations. + _stoppedInternal() { + this.#setState(TorProviderState.Stopped); + } + + // Implementation methods. + + /** + * An internal initialization method for provider instances to implement. + */ + async _initInternal() { + throw new Error("_initInternal not implemented."); + } + + /** + * An internal uninitialization method for provider instances to implement. + */ + async _uninitInternal() { + throw new Error("_uninitInternal not implemented."); + } + + async writeBridgeSettings(_bridges) { + throw new Error("writeBridgeSettings not implemented."); + } + + async writeProxySettings(_proxy) { + throw new Error("writeProxySettings not implemented."); + } + + async writeFirewallSettings(_firewall) { + throw new Error("writeFirewallSettings not implemented."); + } + + async flushSettings() { + throw new Error("flushSettings not implemented."); + } + + async connect() { + throw new Error("connect not implemented."); + } + + async stopBootstrap() { + throw new Error("stopBootstrap not implemented."); + } + + async newnym() { + throw new Error("newnym not implemented."); + } + + async getBridges() { + throw new Error("getBridges not implemented."); + } + + async getPluggableTransports() { + throw new Error("getPluggableTransports not implemented."); + } + + async onionAuthAdd(_address, _b64PrivateKey, _isPermanent) { + throw new Error("onionAuthAdd not implemented."); + } + + async onionAuthRemove(_address) { + throw new Error("onionAuthRemove not implemented."); + } + + async onionAuthViewKeys() { + throw new Error("onionAuthViewKeys not implemented."); + } + + get currentBridge() { + throw new Error("currentBridge not implemented."); + } +} ===================================== toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs ===================================== @@ -6,10 +6,19 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs", TorProvider: "resource://gre/modules/TorProvider.sys.mjs", + TorProviderNone: "resource://gre/modules/TorProviderNone.sys.mjs", +}); + +ChromeUtils.defineLazyGetter(lazy, "logger", () => { + return console.createInstance({ + // Share preference with TorProvider. + maxLogLevelPref: "browser.tor_provider.log_level", + prefix: "TorProviderBuilder", + }); }); export const TorProviderTopics = Object.freeze({ - ProcessExited: "TorProcessExited", + ProviderStateChanged: "TorProviderStateChanged", BootstrapStatus: "TorBootstrapStatus", BootstrapError: "TorBootstrapError", TorLog: "TorLog", @@ -18,6 +27,15 @@ export const TorProviderTopics = Object.freeze({ CircuitCredentialsMatched: "TorCircuitCredentialsMatched", }); +/** + * The tracked state a provider might be in. + */ +export const TorProviderState = Object.freeze({ + Starting: "Starting", + Running: "Running", + Stopped: "Stopped", +}); + /** * Wrapper error class for errors raised during TorProvider.init. */ @@ -54,8 +72,8 @@ export class TorBootstrapError extends Error { } export const TorProviders = Object.freeze({ - none: 0, - tor: 1, + none: "none", + tor: "tor", }); /** @@ -65,6 +83,17 @@ export const TorProviders = Object.freeze({ * @property {string} msg The message */ +/** + * @typedef {object} TorProviderData + * + * The data associated with a tor provider. + * + * @property {TorProviderBase} provider - The provider instance. + * @property {Promise<undefined>} initPromise - A promise that fulfils after the + * previous provider is cleaned up and the new provider's initialization + * completes or throws an error. + */ + /** * The factory to get a Tor provider. * Currently we support only TorProvider, i.e., the one that interacts with @@ -72,14 +101,35 @@ export const TorProviders = Object.freeze({ */ export class TorProviderBuilder { /** - * A promise with the instance of the provider that we are using. + * Data about the current provider instance. * - * @type {Promise<TorProvider>?} + * @type {?TorProviderData} */ - static #provider = null; + static #providerData = null; /** - * A record of the log messages from all TorProvider instances. + * Whether the `uninit` method has been called. + * + * @type {boolean} + */ + static #uninitialized = false; + + /** + * Check that we are active before a public call. + * + * @throws {Error} Throws if we are not active. + */ + static #checkActive() { + if (this.#uninitialized) { + throw new Error("TorProviderBuilder has already been uninitialized."); + } + if (!this.#providerData) { + throw new Error("TorProviderBuilder has not been initialized."); + } + } + + /** + * A record of the log messages from all provider instances. * * @type {LogEntry[]} */ @@ -120,22 +170,6 @@ export class TorProviderBuilder { this.#log.push(logEntry); } - /** - * The observer that checks when the tor process exits, and reinitializes the - * provider. - * - * @type {Function} - */ - static #exitObserver = null; - - /** - * Tell whether the browser UI is ready. - * We ignore any errors until it is because we cannot show them. - * - * @type {boolean} - */ - static #uiReady = false; - /** * Initialize the provider of choice. */ @@ -149,115 +183,214 @@ export class TorProviderBuilder { }; Services.obs.addObserver(this.#logObserver, TorProviderTopics.TorLog); + // Even though initialization of the initial provider is asynchronous, we do + // not expect the caller to await it. The reason is that any call to build() + // will wait the initialization anyway (and re-throw any initialization + // error). + this.#replaceProvider(); + } + + /** + * Replace the provider with a new instance. + */ + static #replaceProvider() { + // NOTE: We need to ensure that the #providerData is set as soon as + // TorProviderBuilder.init is called. + // I.e. it should be safe to call + // TorProviderBuilder.init(); + // TorProviderBuilder.build(); + // TorProviderBuilder.settledState(); + // // etc + // without any await. + // + // In particular, this is needed by `TorConnect.init`, which will call + // `settledState`. It will also call `build` immediately if quickstart is + // set. See tor-browser#41921. + if (this.#providerData) { + lazy.logger.info( + `Replacing the provider with a "${this.providerType}" provider.` + ); + } else { + lazy.logger.info(`Creating the initial "${this.providerType}" provider.`); + } + + let providerClass; switch (this.providerType) { case TorProviders.tor: - // Even though initialization of the initial TorProvider is - // asynchronous, we do not expect the caller to await it. The reason is - // that any call to build() will wait the initialization anyway (and - // re-throw any initialization error). - this.#initTorProvider(); + providerClass = lazy.TorProvider; break; case TorProviders.none: - lazy.TorLauncherUtil.setProxyConfiguration( - lazy.TorLauncherUtil.getPreferredSocksConfiguration() - ); + providerClass = lazy.TorProviderNone; break; default: - console.error(`Unknown tor provider ${this.providerType}.`); + lazy.logger.error(`Unknown tor provider ${this.providerType}.`); break; } + // NOTE: It should be safe to create another provider instance whilst the + // existing one is still active. However, we will wait until the other is + // uninitialized before we initialize the new one. + const provider = new providerClass(() => { + this.#notifyStateChanged(provider); + }); + const prevProviderData = this.#providerData; + // NOTE: We want `#providerData` to be set prior to our call to + // `provider.init`, so we create the `initPromise` prior to setting it. + const { promise: initPromise, resolve, reject } = Promise.withResolvers(); + this.#providerData = { provider, initPromise }; + // Let observers know we are restarting the provider. + this.#notifyStateChanged(provider); + + // Run the rest of the init in an async operation that will cause + // `initPromise` to settle. + // NOTE: `#cleanupProviderData` should not throw, unlike `provider.init()`, + // which may throw. + // NOTE: We wait for `#cleanupProviderData` to complete before calling + // `provider.init()` in case the implementation relies on this. + this.#cleanupProviderData(prevProviderData).finally(() => { + provider.init().then(resolve, reject); + }); } /** - * Replace #provider with a new instance. + * Notify any listeners that the state of the current provider has changed. * - * @returns {Promise<TorProvider>} The new instance. + * @param {TorProviderBase} provider - The provider who's state has changed. */ - static #initTorProvider() { - if (!this.#exitObserver) { - this.#exitObserver = this.#torExited.bind(this); - Services.obs.addObserver( - this.#exitObserver, - TorProviderTopics.ProcessExited - ); + static #notifyStateChanged(provider) { + if (this.#uninitialized) { + // Do not signal the final state changes when we uninitialize. + return; + } + if (provider !== this.#providerData.provider) { + // Delayed call from an old provider. Ignore. + return; } - // NOTE: We need to ensure that the #provider is set as soon - // TorProviderBuilder.init is called. - // I.e. it should be safe to call - // TorProviderBuilder.init(); - // TorProviderBuilder.build(); - // without any await. - // - // Therefore, we await the oldProvider within the Promise rather than make - // #initTorProvider async. - // - // In particular, this is needed by TorConnect when the user has selected - // quickstart, in which case `TorConnect.init` will immediately request the - // provider. See tor-browser#41921. - this.#provider = this.#replaceTorProvider(this.#provider); - return this.#provider; + Services.obs.notifyObservers( + null, + TorProviderTopics.ProviderStateChanged, + provider.state + ); + + this.#promptProviderState(false); } /** - * Replace a TorProvider instance. Resolves once the TorProvider is - * initialised. + * Check the given provider's state. + * + * If the provider is no longer the current one, it is considered to be + * "Stopped". + * + * If it is still the current provider, the state will be updated and + * returned. * - * @param {Promise<TorProvider>?} oldProvider - The previous's provider's - * promise, if any. - * @returns {TorProvider} The new TorProvider instance. + * @param {TorProviderBase} provider - The provider to check. + * @returns {string} - The `TorProviderState` state for the provider. */ - static async #replaceTorProvider(oldProvider) { + static #checkProviderState(provider) { + if (this.#providerData?.provider !== provider) { + // Replaced. + lazy.logger.debug("The checked provider has been replaced."); + return TorProviderState.Stopped; + } + return this.#providerData.provider.state; + } + + /** + * Cleanup the given provider data. + * + * @param {?TorProviderData} providerData - The data to clean up. + */ + static async #cleanupProviderData(providerData) { + if (!providerData) { + return; + } try { - // Uninitialise the old TorProvider, if there is any. - (await oldProvider)?.uninit(); + await providerData.initPromise; } catch {} - const provider = new lazy.TorProvider(); + + // Call `uninit` to clean up, even if `init` threw. + // Should be safe to call more than once (via `uninit`). try { - await provider.init(); + await providerData.provider.uninit(); } catch (error) { - // Wrap in an error type for callers to know whether the error comes from - // initialisation or something else. - throw new TorProviderInitError(error); + lazy.logger.error("Error in uninitializing provider", error); } - return provider; } static uninit() { - this.#provider?.then(provider => { - provider.uninit(); - this.#provider = null; - }); - if (this.#exitObserver) { - Services.obs.removeObserver( - this.#exitObserver, - TorProviderTopics.ProcessExited - ); - this.#exitObserver = null; - } + this.#uninitialized = true; + + // NOTE: `uninit` should not be followed by any further calls to public + // methods. So we can clear the `#providerData` without keeping it for any + // future provider instances to wait on. + const providerData = this.#providerData; + this.#providerData = null; + this.#cleanupProviderData(providerData); + Services.obs.removeObserver(this.#logObserver, TorProviderTopics.TorLog); } /** - * Build a provider. - * This method will wait for the system to be initialized, and allows you to - * catch also any initialization errors. + * Request the current instance of the Tor provider. + * + * This method will wait for the system to be initialized before returning the + * provider. + * + * This will throw any initialization errors of the provider, if it had any. + * This will also throw if the provider is no longer active. * * @returns {TorProvider} A TorProvider instance */ static async build() { - if (!this.#provider && this.providerType === TorProviders.none) { + this.#checkActive(); + if (this.#providerData.provider instanceof lazy.TorProviderNone) { throw new Error( "Tor Browser has been configured to use only the proxy functionalities." ); - } else if (!this.#provider) { - throw new Error( - "The provider has not been initialized or already uninitialized." + } + + const { provider, initPromise } = this.#providerData; + // initPromise may throw. + await initPromise; + if (this.#checkProviderState(provider) !== TorProviderState.Running) { + lazy.logger.warn("Request was made for a provider that has stopped."); + // TODO: Wait for the new instance instead? + throw new TorProviderInitError( + new Error("Provider is no longer active.") ); } - return this.#provider; + return provider; + } + + /** + * Get the state of the current provider instance. Waits until the provider + * has finished initialisation first. + * + * If the provider has been replaced, the Stopped state will be returned. + * + * @returns {string} - The `TorProviderState` state for the provider that + * existed when this method was called. + */ + static async settledState() { + this.#checkActive(); + const { provider, initPromise } = this.#providerData; + try { + await initPromise; + } catch {} + return this.#checkProviderState(provider); } + /** + * Replace the current provider instance with a new provider. + */ + static replace() { + this.#checkActive(); + this.#replaceProvider(); + } + + // TODO: Remove firstWindowLoaded, #uiReady, #prompting, #promptProviderState + // and use TorConnect instead. tor-browser#43570. /** * Check if the provider has been succesfully initialized when the first * browser window is shown. @@ -266,50 +399,72 @@ export class TorProviderBuilder { * but we should modify TorConnect and about:torconnect to handle this case * there with a better UX. */ - static async firstWindowLoaded() { - // FIXME: Just integrate this with the about:torconnect or about:tor UI. - if ( - !lazy.TorLauncherUtil.shouldStartAndOwnTor || - this.providerType !== TorProviders.tor - ) { - // If we are not managing the Tor daemon we cannot restart it, so just - // early return. - return; - } - let running = false; - try { - const provider = await this.#provider; - // The initialization might have succeeded, but so far we have ignored any - // error notification. So, check that the process has not exited after the - // provider has been initialized successfully, but the UI was not ready - // yet. - running = provider.isRunning; - } catch { - // Not even initialized, running is already false. - } - while (!running && lazy.TorLauncherUtil.showRestartPrompt(true)) { - try { - await this.#initTorProvider(); - running = true; - } catch {} - } - // The user might have canceled the restart, but at this point the UI is - // ready in any case. - this.#uiReady = true; + static firstWindowLoaded() { + this.#promptProviderState(true); } - static async #torExited() { + /** + * Tell whether the browser UI is ready. + * We ignore any errors until it is because we cannot show them. + * + * @type {boolean} + */ + static #uiReady = false; + + /** + * Whether we are prompting the user for a restart of the provider. + * + * @type {boolean} + */ + static #prompting = false; + + /** + * Prompt the user to restart the provider, if this is necessary. + * + * @param {boolean} uiReady - Whether this is being called for the first time + * when the UI is ready. + */ + static async #promptProviderState(uiReady) { + if (uiReady) { + this.#uiReady = true; + } + if (this.#providerData.provider.state === TorProviderState.Running) { + // Nothing to wait for. + return; + } if (!this.#uiReady) { - console.warn( - `Seen ${TorProviderTopics.ProcessExited}, but not doing anything because the UI is not ready yet.` + lazy.logger.warn( + "Seen exit, but not doing anything because the UI is not ready yet." ); return; } - while (lazy.TorLauncherUtil.showRestartPrompt(false)) { - try { - await this.#initTorProvider(); - break; - } catch {} + if (this.#prompting) { + // Already prompting, so don't duplicate. + return; + } + + this.#prompting = true; + let waitForInit = uiReady; + let retry = true; + try { + while (retry) { + if (waitForInit) { + try { + await this.#providerData.initPromise; + } catch {} + } + if ( + this.#providerData.provider.state === TorProviderState.Stopped && + lazy.TorLauncherUtil.showRestartPrompt(uiReady) + ) { + waitForInit = true; + this.replace(); + } else { + retry = false; + } + } + } finally { + this.#prompting = false; } } @@ -320,7 +475,7 @@ export class TorProviderBuilder { * Otherwise, if it is not valid, the C tor implementation is chosen as the * default one. * - * @returns {number} An entry from TorProviders + * @returns {string} An entry from TorProviders */ static get providerType() { // TODO: Add a preference to permanently save this without and avoid always ===================================== toolkit/components/tor-launcher/TorProviderNone.sys.mjs ===================================== @@ -0,0 +1,19 @@ +import { TorProviderBase } from "resource://gre/modules/TorProviderBase.sys.mjs"; + +const lazy = {}; +ChromeUtils.defineESModuleGetters(lazy, { + TorLauncherUtil: "resource://gre/modules/TorLauncherUtil.sys.mjs", +}); + +/** + * A provider that only sets the proxy settings. + */ +export class TorProviderNone extends TorProviderBase { + async _initInternal() { + lazy.TorLauncherUtil.setProxyConfiguration( + lazy.TorLauncherUtil.getPreferredSocksConfiguration() + ); + } + + async _uninitInternal() {} +} ===================================== toolkit/components/tor-launcher/moz.build ===================================== @@ -7,7 +7,9 @@ EXTRA_JS_MODULES += [ "TorProcess.sys.mjs", "TorProcessAndroid.sys.mjs", "TorProvider.sys.mjs", + "TorProviderBase.sys.mjs", "TorProviderBuilder.sys.mjs", + "TorProviderNone.sys.mjs", "TorStartupService.sys.mjs", ] ===================================== toolkit/modules/TorConnect.sys.mjs ===================================== @@ -9,6 +9,7 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { MoatRPC: "resource://gre/modules/Moat.sys.mjs", TorBootstrapRequest: "resource://gre/modules/TorBootstrapRequest.sys.mjs", + TorProviderState: "resource://gre/modules/TorProviderBuilder.sys.mjs", TorProviderTopics: "resource://gre/modules/TorProviderBuilder.sys.mjs", TorBootstrapError: "resource://gre/modules/TorProviderBuilder.sys.mjs", TorProviderInitError: "resource://gre/modules/TorProviderBuilder.sys.mjs", @@ -913,7 +914,7 @@ export const TorConnect = { }; // register the Tor topics we always care about - observeTopic(lazy.TorProviderTopics.ProcessExited); + observeTopic(lazy.TorProviderTopics.ProviderStateChanged); observeTopic(lazy.TorProviderTopics.HasWarnOrErr); observeTopic(lazy.TorSettingsTopics.SettingsChanged); observeTopic(NETWORK_LINK_TOPIC); @@ -933,7 +934,7 @@ export const TorConnect = { } }, - async observe(subject, topic) { + async observe(subject, topic, data) { lazy.logger.debug(`Observed ${topic}`); switch (topic) { @@ -947,7 +948,10 @@ export const TorConnect = { this._notifyBootstrapProgress(); } break; - case lazy.TorProviderTopics.ProcessExited: + case lazy.TorProviderTopics.ProviderStateChanged: + if (data !== lazy.TorProviderState.Stopped) { + break; + } lazy.logger.info("Starting again since the tor process exited"); // Treat a failure as a possibly broken configuration. // So, prevent quickstart at the next start. @@ -1341,7 +1345,7 @@ export const TorConnect = { this._tryAgain = true; if (error instanceof lazy.TorProviderInitError) { - // Treat like TorProviderTopics.ProcessExited. We expect a user + // Treat like TorProviderTopics.ProviderStateChanged. We expect a user // notification when this happens. // Treat a failure as a possibly broken configuration. // So, prevent quickstart at the next start. ===================================== toolkit/modules/TorSettings.sys.mjs ===================================== @@ -9,6 +9,7 @@ ChromeUtils.defineESModuleGetters(lazy, { Lox: "resource://gre/modules/Lox.sys.mjs", LoxTopics: "resource://gre/modules/Lox.sys.mjs", TorParsers: "resource://gre/modules/TorParsers.sys.mjs", + TorProviderState: "resource://gre/modules/TorProviderBuilder.sys.mjs", }); ChromeUtils.defineLazyGetter(lazy, "logger", () => { @@ -940,7 +941,10 @@ class TorSettingsImpl { // Test whether the provider is no longer running or has been replaced. const providerRunning = () => { - return providerRef === this.#providerRef && provider.isRunning; + return ( + providerRef === this.#providerRef && + provider.state !== lazy.TorProviderState.Stopped + ); }; lazy.logger.debug("Passing on settings to the provider", apply, details); View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/6e25550... -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/6e25550... You're receiving this email because of your account on gitlab.torproject.org. Manage all notifications: https://gitlab.torproject.org/-/profile/notifications | Help: https://gitlab.torproject.org/help
participants (1)
-
henry (@henry)