Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
"use strict";
const {
WorkersListener,
// eslint-disable-next-line mozilla/reject-some-requires
class LegacyServiceWorkersWatcher extends LegacyWorkersWatcher {
// Holds the current target URL object
#currentTargetURL;
constructor(targetCommand, onTargetAvailable, onTargetDestroyed, commands) {
super(targetCommand, onTargetAvailable, onTargetDestroyed);
this._registrations = [];
this._processTargets = new Set();
this.commands = commands;
// We need to listen for registration changes at least in order to properly
// filter service workers by domain when debugging a local tab.
//
// A WorkerTarget instance has a url property, but it points to the url of
// the script, whereas the url property of the ServiceWorkerRegistration
// points to the URL controlled by the service worker.
//
// Historically we have been matching the service worker registration URL
// to match service workers for local tab tools (app panel & debugger).
// Maybe here we could have some more info on the actual worker.
this._workersListener = new WorkersListener(this.rootFront, {
registrationsOnly: true,
});
// Note that this is called much more often than when a registration
// is created or destroyed. WorkersListener notifies of anything that
// potentially impacted workers.
// I use it as a shortcut in this first patch. Listening to rootFront's
// "serviceWorkerRegistrationListChanged" should be enough to be notified
// about registrations. And if we need to also update the
// "debuggerServiceWorkerStatus" from here, then we would have to
// also listen to "registration-changed" one each registration.
this._onRegistrationListChanged =
this._onRegistrationListChanged.bind(this);
this._onDocumentEvent = this._onDocumentEvent.bind(this);
// Flag used from the parent class to listen to process targets.
// Decision tree is complicated, keep all logic in the parent methods.
this._isServiceWorkerWatcher = true;
}
/**
* Override from LegacyWorkersWatcher.
*
* We record all valid service worker targets (ie workers that match a service
* worker registration), but we will only notify about the ones which match
* the current domain.
*/
_recordWorkerTarget(workerTarget) {
return !!this._getRegistrationForWorkerTarget(workerTarget);
}
// Override from LegacyWorkersWatcher.
_supportWorkerTarget(workerTarget) {
if (!workerTarget.isServiceWorker) {
return false;
}
const registration = this._getRegistrationForWorkerTarget(workerTarget);
return registration && this._isRegistrationValidForTarget(registration);
}
// Override from LegacyWorkersWatcher.
async listen() {
// Listen to the current target front.
this.target = this.targetCommand.targetFront;
if (this.targetCommand.descriptorFront.isTabDescriptor) {
this.#currentTargetURL = new URL(this.targetCommand.targetFront.url);
}
this._workersListener.addListener(this._onRegistrationListChanged);
// Fetch the registrations before calling listen, since service workers
// might already be available and will need to be compared with the existing
// registrations.
await this._onRegistrationListChanged();
if (this.targetCommand.descriptorFront.isTabDescriptor) {
await this.commands.resourceCommand.watchResources(
[this.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
{
onAvailable: this._onDocumentEvent,
ignoreExistingResources: true,
}
);
}
await super.listen();
}
// Override from LegacyWorkersWatcher.
unlisten(...args) {
this._workersListener.removeListener(this._onRegistrationListChanged);
if (this.targetCommand.descriptorFront.isTabDescriptor) {
this.commands.resourceCommand.unwatchResources(
[this.commands.resourceCommand.TYPES.DOCUMENT_EVENT],
{
onAvailable: this._onDocumentEvent,
}
);
}
super.unlisten(...args);
}
// Override from LegacyWorkersWatcher.
async _onProcessAvailable({ targetFront }) {
if (this.targetCommand.descriptorFront.isTabDescriptor) {
// XXX: This has been ported straight from the current debugger
// implementation. Since pauseMatchingServiceWorkers expects an origin
// to filter matching workers, it only makes sense when we are debugging
// a tab. However in theory, parent process debugging could pause all
// service workers without matching anything.
try {
// To support early breakpoint we need to setup the
// `pauseMatchingServiceWorkers` mechanism in each process.
await targetFront.pauseMatchingServiceWorkers({
origin: this.#currentTargetURL.origin,
});
} catch (e) {
if (targetFront.actorID) {
throw e;
} else {
console.warn(
"Process target destroyed while calling pauseMatchingServiceWorkers"
);
}
}
}
this._processTargets.add(targetFront);
return super._onProcessAvailable({ targetFront });
}
_onProcessDestroyed({ targetFront }) {
this._processTargets.delete(targetFront);
return super._onProcessDestroyed({ targetFront });
}
_onDocumentEvent(resources) {
for (const resource of resources) {
if (
resource.resourceType !==
this.commands.resourceCommand.TYPES.DOCUMENT_EVENT
) {
continue;
}
if (resource.name === "will-navigate") {
// We rely on will-navigate as the onTargetAvailable for the top-level frame can
// happen after the onTargetAvailable for processes (handled in _onProcessAvailable),
// where we need the origin we navigate to.
this.#currentTargetURL = new URL(resource.newURI);
continue;
}
// Note that we rely on "dom-loading" rather than "will-navigate" because the
// destroyed/available callbacks should be triggered after the Debugger
// has cleaned up its reducers, which happens on "will-navigate".
// On the other end, "dom-complete", which is a better mapping of "navigate", is
// happening too late (because of resources being throttled), and would cause failures
// in test (like browser_target_command_service_workers_navigation.js), as the new worker
// target would already be registered at this point, and seen as something that would
// need to be destroyed.
if (resource.name === "dom-loading") {
const allServiceWorkerTargets = this._getAllServiceWorkerTargets();
for (const target of allServiceWorkerTargets) {
// Note: we call isTargetRegistered again because calls to
// onTargetDestroyed might have modified the list of registered targets.
const isRegisteredAfter =
this.targetCommand.isTargetRegistered(target);
const isValidTarget = this._supportWorkerTarget(target);
if (isValidTarget && !isRegisteredAfter) {
// If the target is still valid for the current top target, call
// onTargetAvailable as well.
this.onTargetAvailable(target);
}
}
}
}
}
async _onRegistrationListChanged() {
if (this.targetCommand.isDestroyed()) {
return;
}
await this._updateRegistrations();
// Everything after this point is not strictly necessary for sw support
// in the target list, but it makes the behavior closer to the previous
// listAllWorkers/WorkersListener pair.
const allServiceWorkerTargets = this._getAllServiceWorkerTargets();
for (const target of allServiceWorkerTargets) {
const hasRegistration = this._getRegistrationForWorkerTarget(target);
if (!hasRegistration) {
// XXX: At this point the worker target is not really destroyed, but
// historically, listAllWorkers* APIs stopped returning worker targets
// if worker registrations are no longer available.
if (this.targetCommand.isTargetRegistered(target)) {
// Only emit onTargetDestroyed if it wasn't already done by
// onNavigate (ie the target is still tracked by TargetCommand)
this.onTargetDestroyed(target);
}
// Here we only care about service workers which no longer match *any*
// registration. The worker will be completely destroyed soon, remove
// it from the legacy worker watcher internal targetsByProcess Maps.
this._removeTargetReferences(target);
}
}
}
// Delete the provided worker target from the internal targetsByProcess Maps.
_removeTargetReferences(target) {
const allProcessTargets = this._getProcessTargets().filter(t =>
this.targetsByProcess.get(t)
);
for (const processTarget of allProcessTargets) {
this.targetsByProcess.get(processTarget).delete(target);
}
}
async _updateRegistrations() {
const { registrations } =
await this.rootFront.listServiceWorkerRegistrations();
this._registrations = registrations;
}
_getRegistrationForWorkerTarget(workerTarget) {
return this._registrations.find(r => {
return (
r.evaluatingWorker?.id === workerTarget.id ||
r.activeWorker?.id === workerTarget.id ||
r.installingWorker?.id === workerTarget.id ||
r.waitingWorker?.id === workerTarget.id
);
});
}
_getProcessTargets() {
return [...this._processTargets];
}
// Flatten all service worker targets in all processes.
_getAllServiceWorkerTargets() {
const allProcessTargets = this._getProcessTargets().filter(target =>
this.targetsByProcess.get(target)
);
const serviceWorkerTargets = [];
for (const target of allProcessTargets) {
serviceWorkerTargets.push(...this.targetsByProcess.get(target));
}
return serviceWorkerTargets;
}
// Check if the registration is relevant for the current target, ie
// corresponds to the same domain.
_isRegistrationValidForTarget(registration) {
if (this.targetCommand.descriptorFront.isBrowserProcessDescriptor) {
// All registrations are valid for main process debugging.
return true;
}
if (!this.targetCommand.descriptorFront.isTabDescriptor) {
// No support for service worker targets outside of main process &
// tab debugging.
return false;
}
// For local tabs, we match ServiceWorkerRegistrations and the target
// if they share the same hostname for their "url" properties.
const targetDomain = this.#currentTargetURL.hostname;
try {
const registrationDomain = new URL(registration.url).hostname;
return registrationDomain === targetDomain;
} catch (e) {
// XXX: Some registrations have an empty URL.
return false;
}
}
}
module.exports = LegacyServiceWorkersWatcher;