commit c7dd1efaff8180014af79b6db9aaebc30ab2a7ec
parent e4049a13c32bfb3f3b22368969721ac216bc6f0b
Author: Andrea Marchesini <amarchesini@mozilla.com>
Date: Mon, 10 Nov 2025 14:06:22 +0000
Bug 1998795 - IPProtection: implement the ACTIVATING state - part 4 - Activating state, r=ip-protection-reviewers,sstreich
Differential Revision: https://phabricator.services.mozilla.com/D271669
Diffstat:
5 files changed, 154 insertions(+), 36 deletions(-)
diff --git a/browser/components/ipprotection/IPPProxyManager.sys.mjs b/browser/components/ipprotection/IPPProxyManager.sys.mjs
@@ -50,6 +50,7 @@ ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
export const IPPProxyStates = Object.freeze({
NOT_READY: "not-ready",
READY: "ready",
+ ACTIVATING: "activating",
ACTIVE: "active",
ERROR: "error",
});
@@ -60,6 +61,8 @@ export const IPPProxyStates = Object.freeze({
class IPPProxyManagerSingleton extends EventTarget {
#state = IPPProxyStates.NOT_READY;
+ #activatingPromise = null;
+
#pass = null;
/**@type {import("./IPPChannelFilter.sys.mjs").IPPChannelFilter | null} */
#connection = null;
@@ -96,7 +99,10 @@ class IPPProxyManagerSingleton extends EventTarget {
this.errors = [];
- if (this.#state === IPPProxyStates.ACTIVE) {
+ if (
+ this.#state === IPPProxyStates.ACTIVE ||
+ this.#state === IPPProxyStates.ACTIVATING
+ ) {
this.stop(false);
}
@@ -173,34 +179,50 @@ class IPPProxyManagerSingleton extends EventTarget {
throw new Error("This method should not be called when not ready");
}
- let started = false;
- try {
- started = await this.#startInternal();
- } catch (error) {
- this.#setErrorState(ERRORS.GENERIC, error);
- return;
- }
+ if (this.#state === IPPProxyStates.ACTIVATING) {
+ if (!this.#activatingPromise) {
+ throw new Error("Activating without a promise?!?");
+ }
- if (this.#state === IPPProxyStates.ERROR) {
- return;
+ return this.#activatingPromise;
}
- // Proxy failed to start but no error was given.
- if (!started) {
- this.#setState(IPPProxyStates.READY);
- return;
- }
-
- this.#setState(IPPProxyStates.ACTIVE);
-
- Glean.ipprotection.toggled.record({
- userAction,
- enabled: true,
- });
-
- if (userAction) {
- this.#reloadCurrentTab();
- }
+ const activating = async () => {
+ let started = false;
+ try {
+ started = await this.#startInternal();
+ } catch (error) {
+ this.#setErrorState(ERRORS.GENERIC, error);
+ return;
+ }
+
+ if (this.#state === IPPProxyStates.ERROR) {
+ return;
+ }
+
+ // Proxy failed to start but no error was given.
+ if (!started) {
+ this.#setState(IPPProxyStates.READY);
+ return;
+ }
+
+ this.#setState(IPPProxyStates.ACTIVE);
+
+ Glean.ipprotection.toggled.record({
+ userAction,
+ enabled: true,
+ });
+
+ if (userAction) {
+ this.#reloadCurrentTab();
+ }
+ };
+
+ this.#setState(IPPProxyStates.ACTIVATING);
+ this.#activatingPromise = activating().finally(
+ () => (this.#activatingPromise = null)
+ );
+ return this.#activatingPromise;
}
async #startInternal() {
@@ -271,6 +293,15 @@ class IPPProxyManagerSingleton extends EventTarget {
* True if started by user action, false if system action
*/
async stop(userAction = true) {
+ if (this.#state === IPPProxyStates.ACTIVATING) {
+ if (!this.#activatingPromise) {
+ throw new Error("Activating without a promise?!?");
+ }
+
+ await this.#activatingPromise.then(() => this.stop(userAction));
+ return;
+ }
+
if (this.#state !== IPPProxyStates.ACTIVE) {
return;
}
@@ -311,7 +342,10 @@ class IPPProxyManagerSingleton extends EventTarget {
*/
async reset() {
this.#pass = null;
- if (this.#state === IPPProxyStates.ACTIVE) {
+ if (
+ this.#state === IPPProxyStates.ACTIVE ||
+ this.#state === IPPProxyStates.ACTIVATING
+ ) {
await this.stop();
}
}
diff --git a/browser/components/ipprotection/tests/browser/browser_IPPProxyManager.js b/browser/components/ipprotection/tests/browser/browser_IPPProxyManager.js
@@ -97,7 +97,7 @@ add_task(async function test_IPPProxyManager_handleProxyErrorEvent() {
// Test inactive connection
const isolationKeyBeforeStop = IPPProxyManager.isolationKey;
- IPPProxyManager.stop();
+ await IPPProxyManager.stop();
const inactiveErrorEvent = new CustomEvent("proxy-http-error", {
detail: {
diff --git a/browser/components/ipprotection/tests/browser/browser_IPProtectionService.js b/browser/components/ipprotection/tests/browser/browser_IPProtectionService.js
@@ -346,7 +346,7 @@ add_task(async function test_IPProtectionService_retry_errors() {
Assert.equal(IPPProxyManager.state, IPPProxyStates.ACTIVE, "Proxy is active");
- IPPProxyManager.stop();
+ await IPPProxyManager.stop();
await closePanel();
await cleanupAlpha();
diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPProtectionPanel.js b/browser/components/ipprotection/tests/xpcshell/test_IPProtectionPanel.js
@@ -276,7 +276,7 @@ add_task(async function test_IPProtectionPanel_started_stopped() {
() => IPPProxyManager.state !== IPPProxyStates.ACTIVE
);
- IPPProxyManager.stop();
+ await IPPProxyManager.stop();
await stoppedEventPromise;
diff --git a/browser/components/ipprotection/tests/xpcshell/test_IPProxyManager.js b/browser/components/ipprotection/tests/xpcshell/test_IPProxyManager.js
@@ -40,6 +40,12 @@ add_task(async function test_IPPProxyManager_start() {
IPPProxyManager.start();
+ Assert.equal(
+ IPPProxyManager.state,
+ IPPProxyStates.ACTIVATING,
+ "Proxy activation"
+ );
+
await startedEventPromise;
Assert.equal(
@@ -81,14 +87,14 @@ add_task(async function test_IPPProxyManager_stop() {
let stoppedEventPromise = waitForEvent(
IPPProxyManager,
"IPPProxyManager:StateChanged",
- () => IPPProxyManager.state !== IPPProxyStates.ACTIVE
+ () => IPPProxyManager.state === IPPProxyStates.READY
);
- IPPProxyManager.stop();
+ await IPPProxyManager.stop();
await stoppedEventPromise;
- Assert.notEqual(
+ Assert.equal(
IPPProxyManager.state,
- IPPProxyStates.ACTIVE,
+ IPPProxyStates.READY,
"IP Protection service should not be active after stopping"
);
Assert.ok(
@@ -135,7 +141,7 @@ add_task(async function test_IPPProxyManager_start_stop_reset() {
"Should have a valid proxy pass after starting"
);
- IPPProxyManager.stop();
+ await IPPProxyManager.stop();
Assert.ok(!IPPProxyManager.active, "Should not be active after starting");
@@ -270,7 +276,15 @@ add_task(async function test_IPPProxytates_active() {
"IP Protection service should be ready"
);
- await IPPProxyManager.start(false);
+ const startPromise = IPPProxyManager.start(false);
+
+ Assert.equal(
+ IPPProxyManager.state,
+ IPPProxyStates.ACTIVATING,
+ "Proxy activation"
+ );
+
+ await startPromise;
Assert.equal(
IPProtectionService.state,
@@ -295,3 +309,73 @@ add_task(async function test_IPPProxytates_active() {
IPProtectionService.uninit();
sandbox.restore();
});
+
+/**
+ * Tests the quick start/stop calls.
+ */
+add_task(async function test_IPPProxytates_start_stop() {
+ let sandbox = sinon.createSandbox();
+ sandbox.stub(IPPSignInWatcher, "isSignedIn").get(() => true);
+ sandbox
+ .stub(IPProtectionService.guardian, "isLinkedToGuardian")
+ .resolves(true);
+ sandbox.stub(IPProtectionService.guardian, "fetchUserInfo").resolves({
+ status: 200,
+ error: undefined,
+ entitlement: { uid: 42 },
+ });
+ sandbox.stub(IPProtectionService.guardian, "fetchProxyPass").resolves({
+ status: 200,
+ error: undefined,
+ pass: {
+ isValid: () => options.validProxyPass,
+ asBearerToken: () => "Bearer helloworld",
+ },
+ });
+
+ const waitForReady = waitForEvent(
+ IPProtectionService,
+ "IPProtectionService:StateChanged",
+ () => IPProtectionService.state === IPProtectionStates.READY
+ );
+
+ IPProtectionService.init();
+
+ await waitForReady;
+
+ Assert.equal(
+ IPProtectionService.state,
+ IPProtectionStates.READY,
+ "IP Protection service should be ready"
+ );
+
+ IPPProxyManager.start(false);
+ IPPProxyManager.start(false);
+ IPPProxyManager.start(false);
+
+ IPPProxyManager.stop(false);
+ IPPProxyManager.stop(false);
+ IPPProxyManager.stop(false);
+ IPPProxyManager.stop(false);
+
+ Assert.equal(
+ IPPProxyManager.state,
+ IPPProxyStates.ACTIVATING,
+ "Proxy activation"
+ );
+
+ await waitForEvent(
+ IPPProxyManager,
+ "IPPProxyManager:StateChanged",
+ () => IPPProxyManager.state === IPPProxyStates.ACTIVE
+ );
+
+ await waitForEvent(
+ IPPProxyManager,
+ "IPPProxyManager:StateChanged",
+ () => IPPProxyManager.state === IPPProxyStates.READY
+ );
+
+ IPProtectionService.uninit();
+ sandbox.restore();
+});