diff options
Diffstat (limited to 'devtools/server/actors/worker.js')
-rw-r--r-- | devtools/server/actors/worker.js | 611 |
1 files changed, 611 insertions, 0 deletions
diff --git a/devtools/server/actors/worker.js b/devtools/server/actors/worker.js new file mode 100644 index 000000000..1937229d5 --- /dev/null +++ b/devtools/server/actors/worker.js @@ -0,0 +1,611 @@ +/* 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 { Ci } = require("chrome"); +const { DebuggerServer } = require("devtools/server/main"); +const Services = require("Services"); +const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm"); +const protocol = require("devtools/shared/protocol"); +const { Arg, method, RetVal } = protocol; +const { + workerSpec, + pushSubscriptionSpec, + serviceWorkerRegistrationSpec, + serviceWorkerSpec, +} = require("devtools/shared/specs/worker"); + +loader.lazyRequireGetter(this, "ChromeUtils"); +loader.lazyRequireGetter(this, "events", "sdk/event/core"); + +XPCOMUtils.defineLazyServiceGetter( + this, "wdm", + "@mozilla.org/dom/workers/workerdebuggermanager;1", + "nsIWorkerDebuggerManager" +); + +XPCOMUtils.defineLazyServiceGetter( + this, "swm", + "@mozilla.org/serviceworkers/manager;1", + "nsIServiceWorkerManager" +); + +XPCOMUtils.defineLazyServiceGetter( + this, "PushService", + "@mozilla.org/push/Service;1", + "nsIPushService" +); + +function matchWorkerDebugger(dbg, options) { + if ("type" in options && dbg.type !== options.type) { + return false; + } + if ("window" in options) { + let window = dbg.window; + while (window !== null && window.parent !== window) { + window = window.parent; + } + + if (window !== options.window) { + return false; + } + } + + return true; +} + +let WorkerActor = protocol.ActorClassWithSpec(workerSpec, { + initialize(conn, dbg) { + protocol.Actor.prototype.initialize.call(this, conn); + this._dbg = dbg; + this._attached = false; + this._threadActor = null; + this._transport = null; + }, + + form(detail) { + if (detail === "actorid") { + return this.actorID; + } + let form = { + actor: this.actorID, + consoleActor: this._consoleActor, + url: this._dbg.url, + type: this._dbg.type + }; + if (this._dbg.type === Ci.nsIWorkerDebugger.TYPE_SERVICE) { + let registration = this._getServiceWorkerRegistrationInfo(); + form.scope = registration.scope; + } + return form; + }, + + attach() { + if (this._dbg.isClosed) { + return { error: "closed" }; + } + + if (!this._attached) { + // Automatically disable their internal timeout that shut them down + // Should be refactored by having actors specific to service workers + if (this._dbg.type == Ci.nsIWorkerDebugger.TYPE_SERVICE) { + let worker = this._getServiceWorkerInfo(); + if (worker) { + worker.attachDebugger(); + } + } + this._dbg.addListener(this); + this._attached = true; + } + + return { + type: "attached", + url: this._dbg.url + }; + }, + + detach() { + if (!this._attached) { + return { error: "wrongState" }; + } + + this._detach(); + + return { type: "detached" }; + }, + + destroy() { + protocol.Actor.prototype.destroy.call(this); + if (this._attached) { + this._detach(); + } + }, + + disconnect() { + this.destroy(); + }, + + connect(options) { + if (!this._attached) { + return { error: "wrongState" }; + } + + if (this._threadActor !== null) { + return { + type: "connected", + threadActor: this._threadActor + }; + } + + return DebuggerServer.connectToWorker( + this.conn, this._dbg, this.actorID, options + ).then(({ threadActor, transport, consoleActor }) => { + this._threadActor = threadActor; + this._transport = transport; + this._consoleActor = consoleActor; + + return { + type: "connected", + threadActor: this._threadActor, + consoleActor: this._consoleActor + }; + }, (error) => { + return { error: error.toString() }; + }); + }, + + push() { + if (this._dbg.type !== Ci.nsIWorkerDebugger.TYPE_SERVICE) { + return { error: "wrongType" }; + } + let registration = this._getServiceWorkerRegistrationInfo(); + let originAttributes = ChromeUtils.originAttributesToSuffix( + this._dbg.principal.originAttributes); + swm.sendPushEvent(originAttributes, registration.scope); + return { type: "pushed" }; + }, + + onClose() { + if (this._attached) { + this._detach(); + } + + this.conn.sendActorEvent(this.actorID, "close"); + }, + + onError(filename, lineno, message) { + reportError("ERROR:" + filename + ":" + lineno + ":" + message + "\n"); + }, + + _getServiceWorkerRegistrationInfo() { + return swm.getRegistrationByPrincipal(this._dbg.principal, this._dbg.url); + }, + + _getServiceWorkerInfo() { + let registration = this._getServiceWorkerRegistrationInfo(); + return registration.getWorkerByID(this._dbg.serviceWorkerID); + }, + + _detach() { + if (this._threadActor !== null) { + this._transport.close(); + this._transport = null; + this._threadActor = null; + } + + // If the worker is already destroyed, nsIWorkerDebugger.type throws + // (_dbg.closed appears to be false when it throws) + let type; + try { + type = this._dbg.type; + } catch (e) {} + + if (type == Ci.nsIWorkerDebugger.TYPE_SERVICE) { + let worker = this._getServiceWorkerInfo(); + if (worker) { + worker.detachDebugger(); + } + } + + this._dbg.removeListener(this); + this._attached = false; + } +}); + +exports.WorkerActor = WorkerActor; + +function WorkerActorList(conn, options) { + this._conn = conn; + this._options = options; + this._actors = new Map(); + this._onListChanged = null; + this._mustNotify = false; + this.onRegister = this.onRegister.bind(this); + this.onUnregister = this.onUnregister.bind(this); +} + +WorkerActorList.prototype = { + getList() { + // Create a set of debuggers. + let dbgs = new Set(); + let e = wdm.getWorkerDebuggerEnumerator(); + while (e.hasMoreElements()) { + let dbg = e.getNext().QueryInterface(Ci.nsIWorkerDebugger); + if (matchWorkerDebugger(dbg, this._options)) { + dbgs.add(dbg); + } + } + + // Delete each actor for which we don't have a debugger. + for (let [dbg, ] of this._actors) { + if (!dbgs.has(dbg)) { + this._actors.delete(dbg); + } + } + + // Create an actor for each debugger for which we don't have one. + for (let dbg of dbgs) { + if (!this._actors.has(dbg)) { + this._actors.set(dbg, new WorkerActor(this._conn, dbg)); + } + } + + let actors = []; + for (let [, actor] of this._actors) { + actors.push(actor); + } + + if (!this._mustNotify) { + if (this._onListChanged !== null) { + wdm.addListener(this); + } + this._mustNotify = true; + } + + return Promise.resolve(actors); + }, + + get onListChanged() { + return this._onListChanged; + }, + + set onListChanged(onListChanged) { + if (typeof onListChanged !== "function" && onListChanged !== null) { + throw new Error("onListChanged must be either a function or null."); + } + if (onListChanged === this._onListChanged) { + return; + } + + if (this._mustNotify) { + if (this._onListChanged === null && onListChanged !== null) { + wdm.addListener(this); + } + if (this._onListChanged !== null && onListChanged === null) { + wdm.removeListener(this); + } + } + this._onListChanged = onListChanged; + }, + + _notifyListChanged() { + this._onListChanged(); + + if (this._onListChanged !== null) { + wdm.removeListener(this); + } + this._mustNotify = false; + }, + + onRegister(dbg) { + if (matchWorkerDebugger(dbg, this._options)) { + this._notifyListChanged(); + } + }, + + onUnregister(dbg) { + if (matchWorkerDebugger(dbg, this._options)) { + this._notifyListChanged(); + } + } +}; + +exports.WorkerActorList = WorkerActorList; + +let PushSubscriptionActor = protocol.ActorClassWithSpec(pushSubscriptionSpec, { + initialize(conn, subscription) { + protocol.Actor.prototype.initialize.call(this, conn); + this._subscription = subscription; + }, + + form(detail) { + if (detail === "actorid") { + return this.actorID; + } + let subscription = this._subscription; + return { + actor: this.actorID, + endpoint: subscription.endpoint, + pushCount: subscription.pushCount, + lastPush: subscription.lastPush, + quota: subscription.quota + }; + }, + + destroy() { + protocol.Actor.prototype.destroy.call(this); + this._subscription = null; + }, +}); + +let ServiceWorkerActor = protocol.ActorClassWithSpec(serviceWorkerSpec, { + initialize(conn, worker) { + protocol.Actor.prototype.initialize.call(this, conn); + this._worker = worker; + }, + + form() { + if (!this._worker) { + return null; + } + + return { + url: this._worker.scriptSpec, + state: this._worker.state, + }; + }, + + destroy() { + protocol.Actor.prototype.destroy.call(this); + this._worker = null; + }, +}); + +// Lazily load the service-worker-child.js process script only once. +let _serviceWorkerProcessScriptLoaded = false; + +let ServiceWorkerRegistrationActor = +protocol.ActorClassWithSpec(serviceWorkerRegistrationSpec, { + /** + * Create the ServiceWorkerRegistrationActor + * @param DebuggerServerConnection conn + * The server connection. + * @param ServiceWorkerRegistrationInfo registration + * The registration's information. + */ + initialize(conn, registration) { + protocol.Actor.prototype.initialize.call(this, conn); + this._conn = conn; + this._registration = registration; + this._pushSubscriptionActor = null; + this._registration.addListener(this); + + let {installingWorker, waitingWorker, activeWorker} = registration; + this._installingWorker = new ServiceWorkerActor(conn, installingWorker); + this._waitingWorker = new ServiceWorkerActor(conn, waitingWorker); + this._activeWorker = new ServiceWorkerActor(conn, activeWorker); + + Services.obs.addObserver(this, PushService.subscriptionModifiedTopic, false); + }, + + onChange() { + this._installingWorker.destroy(); + this._waitingWorker.destroy(); + this._activeWorker.destroy(); + + let {installingWorker, waitingWorker, activeWorker} = this._registration; + this._installingWorker = new ServiceWorkerActor(this._conn, installingWorker); + this._waitingWorker = new ServiceWorkerActor(this._conn, waitingWorker); + this._activeWorker = new ServiceWorkerActor(this._conn, activeWorker); + + events.emit(this, "registration-changed"); + }, + + form(detail) { + if (detail === "actorid") { + return this.actorID; + } + let registration = this._registration; + let installingWorker = this._installingWorker.form(); + let waitingWorker = this._waitingWorker.form(); + let activeWorker = this._activeWorker.form(); + + let isE10s = Services.appinfo.browserTabsRemoteAutostart; + return { + actor: this.actorID, + scope: registration.scope, + url: registration.scriptSpec, + installingWorker, + waitingWorker, + activeWorker, + // - In e10s: only active registrations are available. + // - In non-e10s: registrations always have at least one worker, if the worker is + // active, the registration is active. + active: isE10s ? true : !!activeWorker + }; + }, + + destroy() { + protocol.Actor.prototype.destroy.call(this); + Services.obs.removeObserver(this, PushService.subscriptionModifiedTopic, false); + this._registration.removeListener(this); + this._registration = null; + if (this._pushSubscriptionActor) { + this._pushSubscriptionActor.destroy(); + } + this._pushSubscriptionActor = null; + + this._installingWorker.destroy(); + this._waitingWorker.destroy(); + this._activeWorker.destroy(); + + this._installingWorker = null; + this._waitingWorker = null; + this._activeWorker = null; + }, + + disconnect() { + this.destroy(); + }, + + /** + * Standard observer interface to listen to push messages and changes. + */ + observe(subject, topic, data) { + let scope = this._registration.scope; + if (data !== scope) { + // This event doesn't concern us, pretend nothing happened. + return; + } + switch (topic) { + case PushService.subscriptionModifiedTopic: + if (this._pushSubscriptionActor) { + this._pushSubscriptionActor.destroy(); + this._pushSubscriptionActor = null; + } + events.emit(this, "push-subscription-modified"); + break; + } + }, + + start() { + if (!_serviceWorkerProcessScriptLoaded) { + Services.ppmm.loadProcessScript( + "resource://devtools/server/service-worker-child.js", true); + _serviceWorkerProcessScriptLoaded = true; + } + Services.ppmm.broadcastAsyncMessage("serviceWorkerRegistration:start", { + scope: this._registration.scope + }); + return { type: "started" }; + }, + + unregister() { + let { principal, scope } = this._registration; + let unregisterCallback = { + unregisterSucceeded: function () {}, + unregisterFailed: function () { + console.error("Failed to unregister the service worker for " + scope); + }, + QueryInterface: XPCOMUtils.generateQI( + [Ci.nsIServiceWorkerUnregisterCallback]) + }; + swm.propagateUnregister(principal, unregisterCallback, scope); + + return { type: "unregistered" }; + }, + + getPushSubscription() { + let registration = this._registration; + let pushSubscriptionActor = this._pushSubscriptionActor; + if (pushSubscriptionActor) { + return Promise.resolve(pushSubscriptionActor); + } + return new Promise((resolve, reject) => { + PushService.getSubscription( + registration.scope, + registration.principal, + (result, subscription) => { + if (!subscription) { + resolve(null); + return; + } + pushSubscriptionActor = new PushSubscriptionActor(this._conn, subscription); + this._pushSubscriptionActor = pushSubscriptionActor; + resolve(pushSubscriptionActor); + } + ); + }); + }, +}); + +function ServiceWorkerRegistrationActorList(conn) { + this._conn = conn; + this._actors = new Map(); + this._onListChanged = null; + this._mustNotify = false; + this.onRegister = this.onRegister.bind(this); + this.onUnregister = this.onUnregister.bind(this); +} + +ServiceWorkerRegistrationActorList.prototype = { + getList() { + // Create a set of registrations. + let registrations = new Set(); + let array = swm.getAllRegistrations(); + for (let index = 0; index < array.length; ++index) { + registrations.add( + array.queryElementAt(index, Ci.nsIServiceWorkerRegistrationInfo)); + } + + // Delete each actor for which we don't have a registration. + for (let [registration, ] of this._actors) { + if (!registrations.has(registration)) { + this._actors.delete(registration); + } + } + + // Create an actor for each registration for which we don't have one. + for (let registration of registrations) { + if (!this._actors.has(registration)) { + this._actors.set(registration, + new ServiceWorkerRegistrationActor(this._conn, registration)); + } + } + + if (!this._mustNotify) { + if (this._onListChanged !== null) { + swm.addListener(this); + } + this._mustNotify = true; + } + + let actors = []; + for (let [, actor] of this._actors) { + actors.push(actor); + } + + return Promise.resolve(actors); + }, + + get onListchanged() { + return this._onListchanged; + }, + + set onListChanged(onListChanged) { + if (typeof onListChanged !== "function" && onListChanged !== null) { + throw new Error("onListChanged must be either a function or null."); + } + + if (this._mustNotify) { + if (this._onListChanged === null && onListChanged !== null) { + swm.addListener(this); + } + if (this._onListChanged !== null && onListChanged === null) { + swm.removeListener(this); + } + } + this._onListChanged = onListChanged; + }, + + _notifyListChanged() { + this._onListChanged(); + + if (this._onListChanged !== null) { + swm.removeListener(this); + } + this._mustNotify = false; + }, + + onRegister(registration) { + this._notifyListChanged(); + }, + + onUnregister(registration) { + this._notifyListChanged(); + } +}; + +exports.ServiceWorkerRegistrationActorList = ServiceWorkerRegistrationActorList; |