tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 116b1706e6db02978586c01be0285e85f71f83d2
parent a9d97f006fbcde69fc94b7e1aab2b0998504e675
Author: Henrik Skupin <mail@hskupin.info>
Date:   Tue, 23 Dec 2025 11:36:51 +0000

Bug 2002949 - [remote] Add ChromeWindowListener to listen for "opened" and "closed" events of Chrome windows. r=Sasha

Differential Revision: https://phabricator.services.mozilla.com/D277073

Diffstat:
Mremote/jar.mn | 1+
Aremote/shared/listeners/ChromeWindowListener.sys.mjs | 119+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mremote/shared/listeners/test/browser/browser.toml | 2++
Aremote/shared/listeners/test/browser/browser_ChromeWindowListener.js | 175+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 297 insertions(+), 0 deletions(-)

diff --git a/remote/jar.mn b/remote/jar.mn @@ -48,6 +48,7 @@ remote.jar: content/shared/listeners/BeforeStopRequestListener.sys.mjs (shared/listeners/BeforeStopRequestListener.sys.mjs) content/shared/listeners/BrowsingContextListener.sys.mjs (shared/listeners/BrowsingContextListener.sys.mjs) content/shared/listeners/CachedResourceListener.sys.mjs (shared/listeners/CachedResourceListener.sys.mjs) + content/shared/listeners/ChromeWindowListener.sys.mjs (shared/listeners/ChromeWindowListener.sys.mjs) content/shared/listeners/ConsoleAPIListener.sys.mjs (shared/listeners/ConsoleAPIListener.sys.mjs) content/shared/listeners/ConsoleListener.sys.mjs (shared/listeners/ConsoleListener.sys.mjs) content/shared/listeners/ContextualIdentityListener.sys.mjs (shared/listeners/ContextualIdentityListener.sys.mjs) diff --git a/remote/shared/listeners/ChromeWindowListener.sys.mjs b/remote/shared/listeners/ChromeWindowListener.sys.mjs @@ -0,0 +1,119 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs", + + BrowsingContextListener: + "chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs", +}); + +const OBSERVER_TOPIC_CLOSED = "domwindowclosed"; +const OBSERVER_TOPIC_OPENED = "domwindowopened"; + +/** + * The ChromeWindowListener can be used to listen for notifications + * coming from Chrome windows that get opened or closed. + * + * Example: + * ``` + * const listener = new ChromeWindowListener(); + * listener.on("opened", onOpened); + * listener.startListening(); + * + * const onOpened = (eventName, data = {}) => { + * const { window, why } = data; + * ... + * }; + * ``` + * + * @fires message + * The ChromeWindowListener emits "opened" and "closed" events, + * with the following object as payload: + * - {ChromeWindow} window + * Chrome window the notification relates to. + */ +export class ChromeWindowListener { + #closingWindows; + #contextListener; + #listening; + + /** + * Create a new ChromeWindowListener instance. + */ + constructor() { + lazy.EventEmitter.decorate(this); + + // When the `domwindowclosed` notification is sent, the + // containing browsing contexts still exist. We must delay + // emitting the `closed` event until the corresponding + // top-level chrome browsing context has closed as well. + this.#closingWindows = new WeakSet(); + + this.#contextListener = new lazy.BrowsingContextListener(); + this.#listening = false; + } + + destroy() { + this.stopListening(); + } + + observe(subject, topic) { + switch (topic) { + case OBSERVER_TOPIC_OPENED: { + this.emit("opened", { window: subject }); + break; + } + case OBSERVER_TOPIC_CLOSED: { + this.#closingWindows.add(subject); + break; + } + } + } + + startListening() { + if (this.#listening) { + return; + } + + Services.ww.registerNotification(this); + + this.#contextListener.on("discarded", this.#onContextDiscarded); + this.#contextListener.startListening(); + + this.#listening = true; + } + + stopListening() { + if (!this.#listening) { + return; + } + + this.#contextListener.off("discarded", this.#onContextDiscarded); + this.#contextListener.stopListening(); + + Services.ww.unregisterNotification(this); + + this.#closingWindows = new WeakSet(); + this.#listening = false; + } + + #onContextDiscarded = (_, data = {}) => { + const { browsingContext } = data; + + if (browsingContext.isContent || browsingContext.parent) { + // We only care about top-level chrome browsing contexts + return; + } + + const window = browsingContext.topChromeWindow; + if (this.#closingWindows.has(window)) { + this.#closingWindows.delete(window); + + this.emit("closed", { window }); + } + }; +} diff --git a/remote/shared/listeners/test/browser/browser.toml b/remote/shared/listeners/test/browser/browser.toml @@ -12,6 +12,8 @@ prefs = ["remote.messagehandler.modulecache.useBrowserTestRoot=true"] ["browser_CachedResourceListener.js"] +["browser_ChromeWindowListener.js"] + ["browser_ConsoleAPIListener.js"] ["browser_ConsoleAPIListener_cached_messages.js"] diff --git a/remote/shared/listeners/test/browser/browser_ChromeWindowListener.js b/remote/shared/listeners/test/browser/browser_ChromeWindowListener.js @@ -0,0 +1,175 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const { ChromeWindowListener } = ChromeUtils.importESModule( + "chrome://remote/content/shared/listeners/ChromeWindowListener.sys.mjs" +); + +add_task(async function test_openedOnNewWindow() { + const listener = new ChromeWindowListener(); + const opened = listener.once("opened"); + + listener.startListening(); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + const { window } = await opened; + is(window, win, "Received expected window"); + + listener.stopListening(); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_closedOnWindowClose() { + const listener = new ChromeWindowListener(); + const closed = listener.once("closed"); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + listener.startListening(); + + await BrowserTestUtils.closeWindow(win); + + const { window } = await closed; + is(window, win, "Received expected window"); + + listener.stopListening(); +}); + +add_task(async function test_multipleWindows() { + const listener = new ChromeWindowListener(); + const openedEvents = []; + + listener.on("opened", (_eventName, data) => { + openedEvents.push(data); + }); + + listener.startListening(); + + const win1 = await BrowserTestUtils.openNewBrowserWindow(); + const win2 = await BrowserTestUtils.openNewBrowserWindow(); + + await TestUtils.waitForCondition(() => openedEvents.length === 2); + + is(openedEvents.length, 2, "Received two opened events"); + is(openedEvents[0].window, win1, "First event has correct window"); + is(openedEvents[1].window, win2, "Second event has correct window"); + + listener.stopListening(); + await BrowserTestUtils.closeWindow(win1); + await BrowserTestUtils.closeWindow(win2); +}); + +add_task(async function test_noEventsBeforeStartListening() { + const listener = new ChromeWindowListener(); + let eventReceived = false; + + listener.on("opened", () => { + eventReceived = true; + }); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + await TestUtils.waitForTick(); + + ok(!eventReceived, "No event received when not listening"); + + listener.startListening(); + + await TestUtils.waitForTick(); + + ok( + !eventReceived, + "No event received for windows opened before startListening" + ); + + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_noEventsAfterStopListening() { + const listener = new ChromeWindowListener(); + let eventCount = 0; + + listener.on("opened", () => { + eventCount++; + }); + + listener.startListening(); + + const win1 = await BrowserTestUtils.openNewBrowserWindow(); + + await TestUtils.waitForCondition(() => eventCount === 1); + + listener.stopListening(); + + const win2 = await BrowserTestUtils.openNewBrowserWindow(); + + await TestUtils.waitForTick(); + + is(eventCount, 1, "Only one event received before stopListening"); + + await BrowserTestUtils.closeWindow(win1); + await BrowserTestUtils.closeWindow(win2); +}); + +add_task(async function test_startListeningIdempotent() { + const listener = new ChromeWindowListener(); + let eventCount = 0; + + listener.on("opened", () => { + eventCount++; + }); + + listener.startListening(); + listener.startListening(); + + const win = await BrowserTestUtils.openNewBrowserWindow(); + + await TestUtils.waitForCondition(() => eventCount === 1); + + is( + eventCount, + 1, + "Only one event received despite multiple startListening calls" + ); + + listener.stopListening(); + await BrowserTestUtils.closeWindow(win); +}); + +add_task(async function test_stopListeningIdempotent() { + const listener = new ChromeWindowListener(); + + listener.startListening(); + listener.stopListening(); + listener.stopListening(); + + ok(true, "Multiple stopListening calls do not throw"); +}); + +add_task(async function test_destroyStopsListening() { + const listener = new ChromeWindowListener(); + let eventCount = 0; + + listener.on("opened", () => { + eventCount++; + }); + + listener.startListening(); + + const win1 = await BrowserTestUtils.openNewBrowserWindow(); + + await TestUtils.waitForCondition(() => eventCount === 1); + + listener.destroy(); + + const win2 = await BrowserTestUtils.openNewBrowserWindow(); + + await TestUtils.waitForTick(); + + is(eventCount, 1, "No events received after destroy"); + + await BrowserTestUtils.closeWindow(win1); + await BrowserTestUtils.closeWindow(win2); +});