henry pushed to branch tor-browser-153.0esr-16.0-1 at The Tor Project / Applications / Tor Browser Commits: e6adf55b by Henry Wilkes at 2026-07-29T11:02:33+01:00 fixup! TB 40933: Add tor-launcher functionality TB 43186: Stop using `TorProviderBuilder` to prompt users on desktop. Also replace `settledState` with `currentState` and `settled`. - - - - - dda5c0f2 by Henry Wilkes at 2026-07-29T11:05:04+01:00 fixup! TB 40597: Implement TorSettings module TB 43186: TorConnect handles TorProvider initialisation errors. + Add the `"ProviderStopped"` stage, which we enter whenever the current `TorProvider` needs replacing. + In the `init` method we check `TorProvider.settledState` to see if the current provider is still running at startup. After this, we listen for the `ProviderStateChanged` event to see if it stops running later on. + We expose a `restartProvider` method to request a new provider. The `ProviderStatusChange` event allow consumers to update the UI. - - - - - 3d9e8cbc by Henry Wilkes at 2026-07-29T11:05:05+01:00 fixup! TB 27476: Implement about:torconnect captive portal within Tor Browser TB 43186: Add a `"ProviderStopped"` page to about:torconnect. - - - - - 3dbd891f by Henry Wilkes at 2026-07-29T11:05:06+01:00 fixup! Tor Browser strings TB 43186 - Add strings for the new `"ProviderStopped"` stage. - - - - - 9 changed files: - browser/components/torconnect/TorConnectParent.sys.mjs - browser/components/torconnect/content/aboutTorConnect.css - browser/components/torconnect/content/aboutTorConnect.html - browser/components/torconnect/content/aboutTorConnect.js - toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs - toolkit/components/tor-launcher/tor-launcher.manifest - toolkit/locales/en-US/toolkit/global/tor-browser.ftl - toolkit/modules/RemotePageAccessManager.sys.mjs - toolkit/modules/TorConnect.sys.mjs Changes: ===================================== browser/components/torconnect/TorConnectParent.sys.mjs ===================================== @@ -3,6 +3,7 @@ import { TorStrings } from "moz-src:///toolkit/modules/TorStrings.sys.mjs"; import { TorConnect, + TorConnectStage, TorConnectTopics, } from "moz-src:///toolkit/modules/TorConnect.sys.mjs"; @@ -26,9 +27,7 @@ and a particular about:torconnect page * It adapts and relays the messages from and to the TorConnect module. */ export class TorConnectParent extends JSWindowActorParent { - constructor(...args) { - super(...args); - + actorCreated() { const self = this; // JSWindowActiveParent derived objects cannot observe directly, so create a @@ -44,6 +43,9 @@ export class TorConnectParent extends JSWindowActorParent { case TorConnectTopics.StageChange: self.sendAsyncMessage("torconnect:stage-change", obj); break; + case TorConnectTopics.ProviderStatusChange: + self.sendAsyncMessage("torconnect:provider-status-change", obj); + break; case TorConnectTopics.BootstrapProgress: self.sendAsyncMessage("torconnect:bootstrap-progress", obj); break; @@ -64,6 +66,10 @@ export class TorConnectParent extends JSWindowActorParent { this.torConnectObserver, TorConnectTopics.StageChange ); + Services.obs.addObserver( + this.torConnectObserver, + TorConnectTopics.ProviderStatusChange + ); Services.obs.addObserver( this.torConnectObserver, TorConnectTopics.BootstrapProgress @@ -124,6 +130,9 @@ export class TorConnectParent extends JSWindowActorParent { Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit ); break; + case "torconnect:restart-provider": + TorConnect.restartProvider(); + break; case "torconnect:start-again": TorConnect.startAgain(); break; @@ -267,3 +276,13 @@ export class TorConnectParent extends JSWindowActorParent { ); } } + +Services.obs.addObserver((_subject, topic) => { + if (topic !== TorConnectTopics.StageChange) { + return; + } + if (TorConnect.stageName === TorConnectStage.ProviderStopped) { + // Make sure we have an open tab to show the ProviderStopped stage. + TorConnectParent.open(); + } +}, TorConnectTopics.StageChange); ===================================== browser/components/torconnect/content/aboutTorConnect.css ===================================== @@ -235,40 +235,57 @@ form#locationDropdown select { stroke: var(--icon-color); /* Make non-interactive (non-draggable). */ pointer-events: none; -} -.torconnect-icon.offline-icon { - content: url("chrome://browser/content/torconnect/network-broken.svg"); -} + &.offline-icon { + content: url("chrome://browser/content/torconnect/network-broken.svg"); + } -.torconnect-icon.assist-icon { - content: url("chrome://browser/content/torconnect/tor-connect-broken.svg"); -} + &.assist-icon { + content: url("chrome://browser/content/torconnect/tor-connect-broken.svg"); + } -.torconnect-icon.location-icon { - content: url("chrome://browser/content/torconnect/connection-location.svg"); - stroke: var(--icon-color-warning); + &.location-icon { + content: url("chrome://browser/content/torconnect/connection-location.svg"); + stroke: var(--icon-color-warning); + } } .torconnect-heading { grid-area: heading; /* Do not show the focus outline. */ outline: none; - font-size: var(--font-size-xxxlarge); + font-size: var(--font-size-xxlarge); margin: 0; margin-block-end: var(--space-xxlarge); } .torconnect-text-container { grid-area: text-container; + + & > *:not(:last-child) { + margin-block-end: var(--space-large); + } + + & :is(p, ul) { + margin: 0; + } + + & ul { + padding-inline-start: var(--space-xxlarge); + } + + & li:not(:last-child) { + margin-block-end: var(--space-medium); + } } -.torconnect-text-container p { - margin: 0; +.torconnect-list-intro { + font-weight: var(--font-weight-semibold); } -.torconnect-text-container > *:not(:last-child) { - margin-block-end: var(--space-large); +body.hide-provider-config-debugging .provider-config-debugging, +body:not(.hide-provider-config-debugging) .provider-generic-debugging { + display: none; } .torconnect-controls-container { @@ -281,11 +298,33 @@ form#locationDropdown select { align-items: center; /* Gap between children and rows. */ gap: var(--space-small) var(--space-small); + + & > * { + flex: 0 0 max-content; + margin: 0; + } } -.torconnect-controls-container > * { - flex: 0 0 max-content; - margin: 0; +#restart-provider-failed { + color: var(--text-color-error); + align-items: center; + gap: var(--space-xxsmall); + + &:not([hidden]) { + display: flex; + } + + & > * { + flex: 0 0 max-content; + } +} + +#restart-provider-failed-icon { + width: var(--icon-size); + height: var(--icon-size); + -moz-context-properties: fill; + fill: var(--icon-color-critical); + content: url("chrome://global/skin/icons/error.svg"); } /* Temporary hacks for the connectPageContainer. */ ===================================== browser/components/torconnect/content/aboutTorConnect.html ===================================== @@ -15,12 +15,22 @@ href="chrome://browser/content/torconnect/aboutTorConnect.css" /> + <link rel="localization" href="branding/brand.ftl" /> <link rel="localization" href="toolkit/global/tor-browser.ftl" /> + <link rel="localization" href="toolkit/global/mozSupportLink.ftl" /> + <script + type="module" + src="chrome://global/content/elements/moz-button.mjs" + ></script> <script type="module" src="chrome://global/content/elements/moz-toggle.mjs" ></script> + <script + type="module" + src="chrome://global/content/elements/moz-support-link.mjs" + ></script> </head> <body class="onion-pattern-background"> <div id="progressBar" hidden="hidden"> @@ -50,6 +60,86 @@ <span class="breadcrumb-label"></span> </span> </div> + <div class="torconnect-stage-content" data-stage-name="ProviderStopped"> + <img class="torconnect-icon assist-icon" alt="" /> + <h1 + class="torconnect-heading" + tabindex="-1" + data-l10n-id="tor-connect-tor-not-working-heading" + ></h1> + <div class="torconnect-text-container"> + <p data-l10n-id="tor-connect-tor-not-working-intro"></p> + <p + class="torconnect-list-intro" + data-l10n-id="tor-connect-tor-not-working-cause-intro" + ></p> + <ul> + <li + class="provider-config-debugging" + data-l10n-id="tor-connect-tor-not-working-cause-applications" + ></li> + <li + class="provider-config-debugging" + data-l10n-id="tor-connect-tor-not-working-cause-files" + ></li> + <li + class="provider-config-debugging" + data-l10n-id="tor-connect-tor-not-working-cause-other" + ></li> + <li + class="provider-generic-debugging" + data-l10n-id="tor-connect-tor-not-working-cause-generic" + ></li> + </ul> + <p + class="torconnect-list-intro" + data-l10n-id="tor-connect-tor-not-working-resolve-intro" + ></p> + <ul> + <li + class="provider-config-debugging" + data-l10n-id="tor-connect-tor-not-working-resolve-applications" + ></li> + <li + class="provider-config-debugging" + data-l10n-id="tor-connect-tor-not-working-resolve-files" + ></li> + <li + class="provider-config-debugging" + data-l10n-id="tor-connect-tor-not-working-resolve-restart-long" + ></li> + <li + class="provider-generic-debugging" + data-l10n-id="tor-connect-tor-not-working-resolve-restart-short" + ></li> + </ul> + <p data-l10n-id="tor-connect-tor-not-working-support"> + <a + is="moz-support-link" + data-l10n-name="support-link" + support-page="tor-manual:get-in-touch__bug-or-feedback" + > + </a> + </p> + <a + is="moz-support-link" + support-page="tor-manual:encountering-issues__troubleshooting" + ></a> + </div> + <div class="torconnect-controls-container"> + <div id="restart-provider-failed" role="alert"> + <img id="restart-provider-failed-icon" alt="" /> + <span + data-l10n-id="tor-connect-tor-not-working-restarting-failed" + ></span> + </div> + <moz-button + id="restart-provider-button" + type="primary" + data-l10n-id="tor-connect-tor-not-working-restart-button" + ></moz-button> + </div> + </div> <div id="connectPageContainer" class="torconnect-stage-content"> <img id="tor-connect-icon" class="torconnect-icon" alt="" /> <h1 ===================================== browser/components/torconnect/content/aboutTorConnect.js ===================================== @@ -134,6 +134,8 @@ class AboutTorConnect { "full-regions-option-group" ), tryBridgeButton: document.querySelector(this.selectors.buttons.tryBridge), + restartProviderButton: document.getElementById("restart-provider-button"), + restartProviderFailed: document.getElementById("restart-provider-failed"), }); /** @@ -418,6 +420,10 @@ class AboutTorConnect { console.warn("Page opened whilst loading"); isLoaded = false; break; + case "ProviderStopped": + this.hideBreadcrumbs(); + this.updateProviderStatus(stage.providerStatus, true); + break; case "Start": this.showStart(stage.tryAgain, stage.potentiallyBlocked); if (focusConnect) { @@ -479,6 +485,157 @@ class AboutTorConnect { moveFocus.focus(); } + /** + * The stored data for the `ProviderStopped` page content. `null` whilst + * uninitialized. + * + * @type {object} + */ + _providerStoppedData = null; + + /** + * Called whenever the tor provider status may have changed. Only relevant in + * the `ProviderStopped` stage. + * + * @param {ProviderStatus} providerStatus - The latest provider status. + * @param {boolean} enteringStage - Whether this is being called because we + * have just entered the `ProviderStopped` stage as the initial stage of the + * page, or transitioning from another stage. + */ + updateProviderStatus(providerStatus, enteringStage) { + if (this.shownStage !== "ProviderStopped") { + // Ignore. + return; + } + if (enteringStage) { + // Reset the data: + // + When entering the stage. + // + When opening the page in this initial stage. + this._providerStoppedData = { + // Set to `null` to make sure we don't early exit below. + isRestarting: null, + forceShowRestarting: false, + canShowFailedAlert: false, + // We set the maybeConfigIssue when entering the stage, but do not attempt + // to update it between failed attempts. + maybeConfigIssue: providerStatus.maybeConfigIssue, + }; + } + + // We show the UI as restarting if the state is not "Stopped", which + // includes both "Starting" and "Running". In the latter case (which is + // unexpected) we still want to show the state as loading before we enter a + // different shownStage. + const restarting = providerStatus.state !== "Stopped"; + + const data = this._providerStoppedData; + + if (restarting === data.isRestarting) { + // No change in the recorded state. Don't change the UI or adjust any + // timeouts. + return; + } + + data.isRestarting = restarting; + if (restarting) { + // Whilst we remain in this `ProviderStopped` stage, we will now show the + // failed alert when we re-enter the "Stopped" state. + data.canShowFailedAlert = true; + + if (!data.forceShowRestarting) { + // Force the restarting UI to show for at least a certain amount of + // time. Even if we fail early, we continue to show the restarting UI + // for a short time to make sure the user has enough time to visually + // see the restarting UI before we return to the error state. + data.forceShowRestarting = true; + setTimeout(() => { + if ( + data !== this._providerStoppedData || + this.shownStage !== "ProviderStopped" + ) { + // We left or re-entered this stage whilst waiting. Do nothing. + return; + } + data.forceShowRestarting = false; + this.updateProviderStoppedContent(); + }, 1000); + } + // Else, we already have a pending timeout. + // E.g., consider the following timeline, where "R" represents entering + // the "Starting" state, "S" represents entering the "Stopped" state, + // "T0" represents the start of the forcedShowRestarting timeout, and "T1" + // represents the completion: + // + // |--------------+-----+-----+-----+----+------------------> + // S0 R0 S1 R1 S2 + // T0 T1 + // |--stopped UI--|--restarting UI-------|--stopped UI------- + // + // + // Similarly with S2 delayed: + // + // |--------------+-----+-----+----------+----+-------------> + // S0 R0 S1 R1 S2 + // T0 T1 + // |--stopped UI--|--restarting UI------------|--stopped UI-- + // + // + // I.e. we restart once (R0) which starts the timeout. Then we stop (S1) and + // restart (R1) again (though unlikely, this could happen if the user + // opens another `about:torconnect` tab after S1 and presses the restart + // button). Rather than create a new timeout, which would delay the + // restart display for even longer, we maintain the current one. + } + + this.updateProviderStoppedContent(); + } + + /** + * Whether we are currently *showing* the tor provider as restarting in the + * UI. + * + * @type {boolean} + */ + restartingProvider = false; + + /** + * Called to update the page content for the `ProviderStopped` stage. + */ + updateProviderStoppedContent() { + const { + isRestarting, + forceShowRestarting, + canShowFailedAlert, + maybeConfigIssue, + } = this._providerStoppedData; + + // Hide any of the debugging information related to configuration issues if + // we do not suspect that this could be the cause. + document.body.classList.toggle( + "hide-provider-config-debugging", + !maybeConfigIssue + ); + + const showRestarting = isRestarting || forceShowRestarting; + this.elements.restartProviderFailed.hidden = + showRestarting || !canShowFailedAlert; + + this.restartingProvider = showRestarting; + // The button is functionally disabled, but can still keep focus. + // TODO: tor-browser#45050. Disabled attribute is waiting on bugzilla bug + // 1927727. + // this.elements.restartProviderButton.disabled = showRestarting; + this.elements.restartProviderButton.iconSrc = showRestarting + ? "chrome://global/skin/icons/loading.svg" + : null; + document.l10n.setAttributes( + this.elements.restartProviderButton, + showRestarting + ? "tor-connect-tor-not-working-restarting-button" + : "tor-connect-tor-not-working-restart-button" + ); + } + updateBootstrappingStatus(data) { this.elements.progressMeter.style.setProperty( "--progress-percent", @@ -816,6 +973,14 @@ class AboutTorConnect { RPMSendAsyncMessage("torconnect:restart"); }); + this.elements.restartProviderButton.addEventListener("click", () => { + if (this.restartingProvider) { + // Ignore clicks whilst the button is shown as "Restarting". + return; + } + RPMSendAsyncMessage("torconnect:restart-provider"); + }); + this.elements.configureButton.textContent = TorStrings.torConnect.torConfigure; this.elements.configureButton.addEventListener("click", () => { @@ -896,6 +1061,9 @@ class AboutTorConnect { RPMAddMessageListener("torconnect:stage-change", ({ data }) => { this.updateStage(data); }); + RPMAddMessageListener("torconnect:provider-status-change", ({ data }) => { + this.updateProviderStatus(data, false); + }); RPMAddMessageListener("torconnect:bootstrap-progress", ({ data }) => { this.updateBootstrappingStatus(data); }); ===================================== toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs ===================================== @@ -204,13 +204,13 @@ export class TorProviderBuilder { // I.e. it should be safe to call // TorProviderBuilder.init(); // TorProviderBuilder.build(); - // TorProviderBuilder.settledState(); + // TorProviderBuilder.settled(); // // 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. + // `settled`. 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.` @@ -372,21 +372,24 @@ export class TorProviderBuilder { } /** - * Get the state of the current provider instance. Waits until the provider - * has finished initialisation first. + * Get the state of the current provider instance. * - * 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. + * @returns {string} - The `TorProviderState` state for the current provider. */ - static async settledState() { + static currentState() { + this.#checkActive(); + return this.#providerData.provider.state; + } + + /** + * Wait until the current provider instance has finished initializing, so it + * is no longer in the `Starting` state. + */ + static async settled() { this.#checkActive(); - const { provider, initPromise } = this.#providerData; try { - await initPromise; + await this.#providerData.initPromise; } catch {} - return this.#checkProviderState(provider); } /** ===================================== toolkit/components/tor-launcher/tor-launcher.manifest ===================================== @@ -1,2 +1 @@ category profile-after-change TorStartupService @torproject.org/tor-startup-service;1 -category browser-first-window-ready moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs TorProviderBuilder.firstWindowLoaded ===================================== toolkit/locales/en-US/toolkit/global/tor-browser.ftl ===================================== @@ -15,6 +15,37 @@ appmenu-open-tor-manual = # Here "Tor" refers to the Tor network. tor-connect-page-title = Connect to Tor +# Here "process" is a noun, referring to the execution of a computer program. "Tor process" specifically refers to the execution of the tor program, which is the program that handles the connection to the "Tor network". +tor-connect-tor-not-working-heading = The Tor process has stopped working +# Here "process" is a noun, referring to the execution of a computer program. +tor-connect-tor-not-working-intro = The underlying process that controls your connection to the Tor network has stopped working. +tor-connect-tor-not-working-cause-intro = What could be causing this? +# "-brand-short-name" will be substituted with the localized name for "Tor Browser". +tor-connect-tor-not-working-cause-applications = Another application that uses the Tor network is conflicting with { -brand-short-name }. +# Here "process" is a noun, referring to the execution of a computer program. "Tor process" specifically refers to the execution of the tor program, which is the program that handles the connection to the "Tor network". +tor-connect-tor-not-working-cause-files = The files used by the Tor process have been modified or removed. +# Here "process" is a noun, referring to the execution of a computer program. "Tor process" specifically refers to the execution of the tor program, which is the program that handles the connection to the "Tor network". +tor-connect-tor-not-working-cause-other = The Tor process has stopped working due to a different technical issue. +# Here "process" is a noun, referring to the execution of a computer program. "Tor process" specifically refers to the execution of the tor program, which is the program that handles the connection to the "Tor network". +tor-connect-tor-not-working-cause-generic = The Tor process has stopped working due to a technical issue. +tor-connect-tor-not-working-resolve-intro = What can you do about it? +tor-connect-tor-not-working-resolve-applications = Quit any other applications that use the Tor network, or reconfigure them. +# Here "process" is a noun, referring to the execution of a computer program. "Tor process" specifically refers to the execution of the tor program, which is the program that handles the connection to the "Tor network". +tor-connect-tor-not-working-resolve-files = If you have modified the files used by the Tor process, undo these changes. +# Here "process" is a noun, referring to the execution of a computer program. "Tor process" specifically refers to the execution of the tor program, which is the program that handles the connection to the "Tor network". +tor-connect-tor-not-working-resolve-restart-long = Once you have followed these steps, or made any other change, try restarting the Tor process (this won’t close your browser tabs). +# Here "process" is a noun, referring to the execution of a computer program. "Tor process" specifically refers to the execution of the tor program, which is the program that handles the connection to the "Tor network". +tor-connect-tor-not-working-resolve-restart-short = Try restarting the Tor process (this won’t close your browser tabs). +# The translation of "support channels" should also be wrapped in '<a data-l10n-name="support-link">' and '</a>', which will link to the corresponding support page. +tor-connect-tor-not-working-support = If the problem remains, get in contact with us through one of our <a data-l10n-name="support-link">support channels</a>. +# Here "process" is a noun, referring to the execution of a computer program. "Tor process" specifically refers to the execution of the tor program, which is the program that handles the connection to the "Tor network". +tor-connect-tor-not-working-restart-button = + .label = Restart Tor process +# Here "process" is a noun, referring to the execution of a computer program. "Tor process" specifically refers to the execution of the tor program, which is the program that handles the connection to the "Tor network". +tor-connect-tor-not-working-restarting-button = + .label = Restarting Tor process… +tor-connect-tor-not-working-restarting-failed = Restarting failed + ## Tor Browser home page. tor-browser-home-heading-stable = Explore. Privately. ===================================== toolkit/modules/RemotePageAccessManager.sys.mjs ===================================== @@ -296,6 +296,7 @@ export let RemotePageAccessManager = { "about:torconnect": { RPMAddMessageListener: [ "torconnect:stage-change", + "torconnect:provider-status-change", "torconnect:bootstrap-progress", "torconnect:quickstart-change", "torconnect:region-names-change", @@ -307,10 +308,12 @@ export let RemotePageAccessManager = { "torconnect:set-quickstart", "torconnect:view-tor-logs", "torconnect:restart", + "torconnect:restart-provider", "torconnect:start-again", "torconnect:choose-region", ], RPMSendQuery: ["torconnect:get-init-args", "torconnect:get-regions"], + RPMGetFormatURLPref: ["app.support.baseURL"], }, "about:welcome": { RPMSendAsyncMessage: ["ActivityStream:ContentToMain"], ===================================== toolkit/modules/TorConnect.sys.mjs ===================================== @@ -10,6 +10,8 @@ ChromeUtils.defineESModuleGetters(lazy, { MoatRPC: "moz-src:///toolkit/modules/Moat.sys.mjs", TorBootstrapRequest: "moz-src:///toolkit/components/tor-launcher/TorBootstrapRequest.sys.mjs", + TorProviderBuilder: + "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs", TorProviderState: "moz-src:///toolkit/components/tor-launcher/TorProviderBuilder.sys.mjs", TorProviderTopics: @@ -81,11 +83,28 @@ ChromeUtils.defineLazyGetter(lazy, "logger", () => /* Topics Notified by the TorConnect module */ export const TorConnectTopics = Object.freeze({ + // Fired when the stage changes. + // The stage name from `TorConnectStage` is passed as the event subject. StageChange: "torconnect:stage-change", + // Fired when the `TorConnect.quickstart` value changes. QuickstartChange: "torconnect:quickstart-change", + // Fired when the `TorConnect.internetStatus` changes. InternetStatusChange: "torconnect:internet-status-change", + // Fired when the `TorConnect.stage.providerStatus` changes. + // NOTE: The state of the provider may switch to `TorProviderState.Stopped` + // prior to `TorConnect` entering the stage `TorConnectStage.ProviderStopped` + // because a stage switch can be delayed. A consumer should always show the + // current `TorConnect` stage as the highest priority, and only react to this + // event when we are already in the `TorConnectStage.ProviderStopped` stage. + // The `ProviderStatus` object is passed as the event subject. + ProviderStatusChange: "torconnect:provider-status-change", + // Fired when the list of region names returned by `TorConnect.getRegionNames` + // may have changed. RegionNamesChange: "torconnect:region-names-change", + // Fired when the `TorConnect.stage.bootstrapStatus` may have changed. + // The `BootstrapStatus` object is passed as the event subject. BootstrapProgress: "torconnect:bootstrap-progress", + // Fired when we enter the `TorConnectStage.Bootstrapped` stage. BootstrapComplete: "torconnect:bootstrap-complete", }); @@ -655,6 +674,7 @@ export const InternetStatus = Object.freeze({ export const TorConnectStage = Object.freeze({ Disabled: "Disabled", Loading: "Loading", + ProviderStopped: "ProviderStopped", Start: "Start", Bootstrapping: "Bootstrapping", Offline: "Offline", @@ -689,6 +709,8 @@ export const TorConnectStage = Object.freeze({ * have reached an error stage at some point before being bootstrapped. * @property {BootstrappingStatus} bootstrappingStatus - The current * bootstrapping status. + * @property {ProviderStatus} providerStatus - The current status of the Tor + * provider. */ /** @@ -714,6 +736,19 @@ export const TorConnectStage = Object.freeze({ * @property {?string} reason - The bootstrapping failure reason. */ +/** + * @typedef {object} ProviderStatus + * + * The status of the current Tor provider. + * + * @property {string} state - The current `TorProviderState` state of the + * provider. + * @property {boolean} maybeConfigIssue - Whether we suspect that a + * configuration issue (conflicting tor process or a mis-config) *may* have + * caused the provider to be stopped. This is a *soft* indicator for UI + * purposes to show additional debugging advice that might resolve the issue. + */ + export const TorConnect = { /** * Default bootstrap options for simulation. @@ -800,6 +835,52 @@ export const TorConnect = { */ _potentiallyBlocked: false, + /** + * The current `TorProviderState` state. `null` whilst we wait for + * `TorProviderBuilder.settled` to return. + * + * @type {?string} + */ + _providerKnownState: null, + + /** + * The current `TorProviderState` state. Whilst we wait for + * `TorProviderBuilder.settled` to return with the true state, we will assume + * that the provider is in the `TorProviderState.Running` state, as would be + * expected under normal circumstances. + */ + get _providerState() { + return this._providerKnownState ?? lazy.TorProviderState.Running; + }, + + /** + * Convenience property for detecting when a provider is running. + * + * @type {boolean} + */ + get _providerRunning() { + return this._providerState === lazy.TorProviderState.Running; + }, + + /** + * Whether we suspect a config issue cause the provider to fail. + * + * @type {boolean} + */ + _maybeConfigIssue: false, + + /** + * The latest provider status information for consumers. + * + * @returns {ProviderStatus} - The provider status. + */ + _providerStatus() { + return { + state: this._providerState, + maybeConfigIssue: this._maybeConfigIssue, + }; + }, + /** * Get a summary of the current user stage. * @@ -822,6 +903,7 @@ export const TorConnect = { tryAgain: this._tryAgain, potentiallyBlocked: this._potentiallyBlocked, bootstrappingStatus: structuredClone(this._bootstrappingStatus), + providerStatus: this._providerStatus(), }; }, @@ -928,6 +1010,23 @@ export const TorConnect = { this._updateInternetStatus(); + // Do not await the `TorProviderBuilder` initialisation before we move to + // the Start stage so that the UI can load quickly. We presume it will not + // fail, but will switch to `ProviderStopped` if necessary. + lazy.TorProviderBuilder.settled().then(() => { + // NOTE: Assuming `TorConnect` is the only consumer of + // `TorProviderBuilder` that will replace the provider, then the current + // provider should be the *first* provider. + const state = lazy.TorProviderBuilder.currentState(); + lazy.logger.debug(`Init state: ${state}`); + this._setProviderState(state, true); + }); + + if (!this._providerRunning) { + // Promise resolved immediately, and failed. Should have already requested + // the ProviderStopped stage. + return; + } // NOTE: At this point, _requestedStage should still be `null`. this._setStage(TorConnectStage.Start); if ( @@ -955,19 +1054,13 @@ export const TorConnect = { } break; 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. - Services.prefs.setBoolPref(TorConnectPrefs.prompt_at_startup, true); - this._makeStageRequest(TorConnectStage.Start, true); + this._setProviderState(data, false); break; case lazy.TorSettingsTopics.SettingsChanged: if ( this._stageName !== TorConnectStage.Bootstrapped && this._stageName !== TorConnectStage.Loading && + this._stageName !== TorConnectStage.ProviderStopped && this._stageName !== TorConnectStage.Start && subject.wrappedJSObject.changes.some(propertyName => propertyName.startsWith("bridges.") @@ -1009,9 +1102,20 @@ export const TorConnect = { * @param {string} name - The name of the stage to move to. */ _setStage(name) { + if (this._stageName === TorConnectStage.Disabled) { + throw new Error(`Trying to set the stage to ${name} when disabled`); + } if (this._bootstrapAttempt) { throw new Error(`Trying to set the stage to ${name} during a bootstrap`); } + if (!this._providerRunning && name !== TorConnectStage.ProviderStopped) { + if (!lazy.TorLauncherUtil.isAndroid) { + // TODO: Remove Android exception. + throw new Error( + `Trying to set the stage to ${name} when provider is not running` + ); + } + } lazy.logger.info(`Entering stage ${name}`); this._stageName = name; @@ -1211,6 +1315,13 @@ export const TorConnect = { return false; } + if (!this._providerRunning) { + lazy.logger.warn( + `Tor provider is not running. Ignoring request with ${regionCode}.` + ); + return false; + } + const currentStage = this._stageName; if (regionCode) { @@ -1315,7 +1426,7 @@ export const TorConnect = { this._defaultRegion = bootstrapAttempt.detectedRegion; } - if (result === "complete") { + if (result === "complete" && this._providerRunning) { // Reset tryAgain, potentiallyBlocked and errorDetails in case the tor // process exists later on. this._tryAgain = false; @@ -1351,15 +1462,10 @@ export const TorConnect = { this._tryAgain = true; if (error instanceof lazy.TorProviderInitError) { - // 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. - Services.prefs.setBoolPref(TorConnectPrefs.prompt_at_startup, true); - lazy.logger.info( - "Starting again since the tor provider failed to initialise" - ); - this._setStage(TorConnectStage.Start); + // Unexpected. We expect TorProviderTopics.ProviderStateChanged to have + // already been called. I.e. the requestStage should be set. + lazy.logger.error("Unexpected TorProviderInitError"); + this._setProviderState(lazy.TorProviderBuilder.currentState(), false); return; } @@ -1489,14 +1595,27 @@ export const TorConnect = { lazy.logger.warn(`Cannot move to ${stage} when bootstrapped`); return; } + if (!this._providerRunning && stage !== TorConnectStage.ProviderStopped) { + if (!lazy.TorLauncherUtil.isAndroid) { + // TODO: Remove Android exception. + lazy.logger.warn( + `Cannot move to ${stage} when provider is not running` + ); + return; + } + } if (this._stageName === TorConnectStage.Loading) { - if (stage === TorConnectStage.Start) { - // Will transition to "Start" stage when loading completes. - lazy.logger.info("Still in the Loading stage"); + if (stage === TorConnectStage.ProviderStopped) { + lazy.logger.info("Skipping straight from Loading to ProviderStopped"); } else { - lazy.logger.warn(`Cannot move to ${stage} when Loading`); + if (stage === TorConnectStage.Start) { + // Will transition to "Start" stage when loading completes. + lazy.logger.info("Still in the Loading stage"); + } else { + lazy.logger.warn(`Cannot move to ${stage} when Loading`); + } + return; } - return; } if (!this._bootstrapAttempt) { @@ -1519,6 +1638,116 @@ export const TorConnect = { this._bootstrapAttempt?.cancel(); }, + /** + * Called when the current Tor provider's state changes. + * + * @param {string} state - The `TorProviderState` we are now in. + * @param {boolean} initialState - Whether this is the initial provider's + * state. + */ + _setProviderState(state, initialState) { + if (this._providerKnownState === null && !initialState) { + // Wait for `TorProviderBuilder.settled` to return first. + // We stick to the assumed status for the time being. + return; + } + + const wasRunning = this._providerRunning; + const prevState = this._providerState; + this._providerKnownState = state; + + if (prevState === state) { + // No change in state. + // NOTE: If this is the initial state, then `prevState` will be + // `TorProviderState.Running`, in which case this will *not* notify + // consumers if the initial state is also `Running`. + return; + } + + if (this._providerRunning) { + // As soon as we get a provider that is running (has not immediately + // exited), from now on we will no longer *suspect* that a configuration + // issue (conflicting tor process, or a mis-config) *could* cause a tor + // provider to stop. I.e. if we ever re-enter the "Stopped" or "Starting" + // state, `maybeConfigIssue` will remain `false`. + // + // This is because we assume that the browser's established tor process + // can not be killed by the spawning of a new conflicting non-browser tor + // process, and any changes in the configuration (via files or otherwise) + // mid session won't be read by the existing process. + // + // NOTE: In principle, if the browser tor process is killed for some other + // reason, in the interim there is a time period where the configuration + // could change or a new non-browser tor process is spawned that could + // conflict, which may prevent any *new* browser tor process from + // starting. I.e. the cause of failure switches from non-config issue to + // a config issue. However, such a scenario is deemed unlikely, and the + // consequences of not accounting for it are very mild since + // `maybeConfigIssue` is only soft hint for the debugging UI. + this._maybeConfigIssue = false; + if (!wasRunning) { + // Return to the Start stage to re-try a bootstrap. + this._makeStageRequest(TorConnectStage.Start); + } + } else { + if (initialState) { + // This is the first time we learn the provider state. If the first + // provider is not running after initialisation, then we *suspect* that + // there *could* be a conflict with another tor process. E.g. an + // existing tor process is already using the ports we want to use, so + // the browser's tor provider will fail at initialisation. + this._maybeConfigIssue = true; + } + if (wasRunning) { + // Transitioning from the Running state to a non-Running state. + + // Prevent quickstart firing at the next startup until we have another + // successful bootstrap. + Services.prefs.setBoolPref(TorConnectPrefs.prompt_at_startup, true); + // We request the ProviderStopped stage, which may take some time to + // reach if we need to cancel the current bootstrap first. + // But other methods should take into account that _providerRunning is now + // `false` to early return and guarantee that we enter this + // ProviderStopped stage. + if (lazy.TorLauncherUtil.isAndroid) { + // TODO: Remove this Android path when android supports the + // `ProviderStopped` stage. + this._makeStageRequest(TorConnectStage.Start, true); + } else { + this._makeStageRequest(TorConnectStage.ProviderStopped, true); + } + } + } + + const providerStatus = this._providerStatus(); + lazy.logger.debug("New provider status", providerStatus); + Services.obs.notifyObservers( + providerStatus, + TorConnectTopics.ProviderStatusChange + ); + }, + + /** + * Restart the Tor provider after it has stopped. + */ + restartProvider() { + lazy.logger.debug("Request to restart Tor provider"); + this._ensureEnabled(); + + if (this._stageName !== TorConnectStage.ProviderStopped) { + lazy.logger.warn( + `Cannot restart the provider in stage ${this._stageName}` + ); + return; + } + if (this._providerState === lazy.TorProviderState.Starting) { + lazy.logger.warn("Already restarting the Tor provider"); + return; + } + + lazy.TorProviderBuilder.replace(); + }, + /** * Restart the TorConnect stage to the start. */ View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/3867e54... -- View it on GitLab: https://gitlab.torproject.org/tpo/applications/tor-browser/-/compare/3867e54... 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)