worker.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this
  3. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. "use strict";
  5. const { Ci } = require("chrome");
  6. const { DebuggerServer } = require("devtools/server/main");
  7. const Services = require("Services");
  8. const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
  9. const protocol = require("devtools/shared/protocol");
  10. const { Arg, method, RetVal } = protocol;
  11. const {
  12. workerSpec,
  13. pushSubscriptionSpec,
  14. serviceWorkerRegistrationSpec,
  15. serviceWorkerSpec,
  16. } = require("devtools/shared/specs/worker");
  17. loader.lazyRequireGetter(this, "ChromeUtils");
  18. loader.lazyRequireGetter(this, "events", "sdk/event/core");
  19. XPCOMUtils.defineLazyServiceGetter(
  20. this, "wdm",
  21. "@mozilla.org/dom/workers/workerdebuggermanager;1",
  22. "nsIWorkerDebuggerManager"
  23. );
  24. XPCOMUtils.defineLazyServiceGetter(
  25. this, "swm",
  26. "@mozilla.org/serviceworkers/manager;1",
  27. "nsIServiceWorkerManager"
  28. );
  29. XPCOMUtils.defineLazyServiceGetter(
  30. this, "PushService",
  31. "@mozilla.org/push/Service;1",
  32. "nsIPushService"
  33. );
  34. function matchWorkerDebugger(dbg, options) {
  35. if ("type" in options && dbg.type !== options.type) {
  36. return false;
  37. }
  38. if ("window" in options) {
  39. let window = dbg.window;
  40. while (window !== null && window.parent !== window) {
  41. window = window.parent;
  42. }
  43. if (window !== options.window) {
  44. return false;
  45. }
  46. }
  47. return true;
  48. }
  49. let WorkerActor = protocol.ActorClassWithSpec(workerSpec, {
  50. initialize(conn, dbg) {
  51. protocol.Actor.prototype.initialize.call(this, conn);
  52. this._dbg = dbg;
  53. this._attached = false;
  54. this._threadActor = null;
  55. this._transport = null;
  56. },
  57. form(detail) {
  58. if (detail === "actorid") {
  59. return this.actorID;
  60. }
  61. let form = {
  62. actor: this.actorID,
  63. consoleActor: this._consoleActor,
  64. url: this._dbg.url,
  65. type: this._dbg.type
  66. };
  67. if (this._dbg.type === Ci.nsIWorkerDebugger.TYPE_SERVICE) {
  68. let registration = this._getServiceWorkerRegistrationInfo();
  69. form.scope = registration.scope;
  70. }
  71. return form;
  72. },
  73. attach() {
  74. if (this._dbg.isClosed) {
  75. return { error: "closed" };
  76. }
  77. if (!this._attached) {
  78. // Automatically disable their internal timeout that shut them down
  79. // Should be refactored by having actors specific to service workers
  80. if (this._dbg.type == Ci.nsIWorkerDebugger.TYPE_SERVICE) {
  81. let worker = this._getServiceWorkerInfo();
  82. if (worker) {
  83. worker.attachDebugger();
  84. }
  85. }
  86. this._dbg.addListener(this);
  87. this._attached = true;
  88. }
  89. return {
  90. type: "attached",
  91. url: this._dbg.url
  92. };
  93. },
  94. detach() {
  95. if (!this._attached) {
  96. return { error: "wrongState" };
  97. }
  98. this._detach();
  99. return { type: "detached" };
  100. },
  101. destroy() {
  102. protocol.Actor.prototype.destroy.call(this);
  103. if (this._attached) {
  104. this._detach();
  105. }
  106. },
  107. disconnect() {
  108. this.destroy();
  109. },
  110. connect(options) {
  111. if (!this._attached) {
  112. return { error: "wrongState" };
  113. }
  114. if (this._threadActor !== null) {
  115. return {
  116. type: "connected",
  117. threadActor: this._threadActor
  118. };
  119. }
  120. return DebuggerServer.connectToWorker(
  121. this.conn, this._dbg, this.actorID, options
  122. ).then(({ threadActor, transport, consoleActor }) => {
  123. this._threadActor = threadActor;
  124. this._transport = transport;
  125. this._consoleActor = consoleActor;
  126. return {
  127. type: "connected",
  128. threadActor: this._threadActor,
  129. consoleActor: this._consoleActor
  130. };
  131. }, (error) => {
  132. return { error: error.toString() };
  133. });
  134. },
  135. push() {
  136. if (this._dbg.type !== Ci.nsIWorkerDebugger.TYPE_SERVICE) {
  137. return { error: "wrongType" };
  138. }
  139. let registration = this._getServiceWorkerRegistrationInfo();
  140. let originAttributes = ChromeUtils.originAttributesToSuffix(
  141. this._dbg.principal.originAttributes);
  142. swm.sendPushEvent(originAttributes, registration.scope);
  143. return { type: "pushed" };
  144. },
  145. onClose() {
  146. if (this._attached) {
  147. this._detach();
  148. }
  149. this.conn.sendActorEvent(this.actorID, "close");
  150. },
  151. onError(filename, lineno, message) {
  152. reportError("ERROR:" + filename + ":" + lineno + ":" + message + "\n");
  153. },
  154. _getServiceWorkerRegistrationInfo() {
  155. return swm.getRegistrationByPrincipal(this._dbg.principal, this._dbg.url);
  156. },
  157. _getServiceWorkerInfo() {
  158. let registration = this._getServiceWorkerRegistrationInfo();
  159. return registration.getWorkerByID(this._dbg.serviceWorkerID);
  160. },
  161. _detach() {
  162. if (this._threadActor !== null) {
  163. this._transport.close();
  164. this._transport = null;
  165. this._threadActor = null;
  166. }
  167. // If the worker is already destroyed, nsIWorkerDebugger.type throws
  168. // (_dbg.closed appears to be false when it throws)
  169. let type;
  170. try {
  171. type = this._dbg.type;
  172. } catch (e) {}
  173. if (type == Ci.nsIWorkerDebugger.TYPE_SERVICE) {
  174. let worker = this._getServiceWorkerInfo();
  175. if (worker) {
  176. worker.detachDebugger();
  177. }
  178. }
  179. this._dbg.removeListener(this);
  180. this._attached = false;
  181. }
  182. });
  183. exports.WorkerActor = WorkerActor;
  184. function WorkerActorList(conn, options) {
  185. this._conn = conn;
  186. this._options = options;
  187. this._actors = new Map();
  188. this._onListChanged = null;
  189. this._mustNotify = false;
  190. this.onRegister = this.onRegister.bind(this);
  191. this.onUnregister = this.onUnregister.bind(this);
  192. }
  193. WorkerActorList.prototype = {
  194. getList() {
  195. // Create a set of debuggers.
  196. let dbgs = new Set();
  197. let e = wdm.getWorkerDebuggerEnumerator();
  198. while (e.hasMoreElements()) {
  199. let dbg = e.getNext().QueryInterface(Ci.nsIWorkerDebugger);
  200. if (matchWorkerDebugger(dbg, this._options)) {
  201. dbgs.add(dbg);
  202. }
  203. }
  204. // Delete each actor for which we don't have a debugger.
  205. for (let [dbg, ] of this._actors) {
  206. if (!dbgs.has(dbg)) {
  207. this._actors.delete(dbg);
  208. }
  209. }
  210. // Create an actor for each debugger for which we don't have one.
  211. for (let dbg of dbgs) {
  212. if (!this._actors.has(dbg)) {
  213. this._actors.set(dbg, new WorkerActor(this._conn, dbg));
  214. }
  215. }
  216. let actors = [];
  217. for (let [, actor] of this._actors) {
  218. actors.push(actor);
  219. }
  220. if (!this._mustNotify) {
  221. if (this._onListChanged !== null) {
  222. wdm.addListener(this);
  223. }
  224. this._mustNotify = true;
  225. }
  226. return Promise.resolve(actors);
  227. },
  228. get onListChanged() {
  229. return this._onListChanged;
  230. },
  231. set onListChanged(onListChanged) {
  232. if (typeof onListChanged !== "function" && onListChanged !== null) {
  233. throw new Error("onListChanged must be either a function or null.");
  234. }
  235. if (onListChanged === this._onListChanged) {
  236. return;
  237. }
  238. if (this._mustNotify) {
  239. if (this._onListChanged === null && onListChanged !== null) {
  240. wdm.addListener(this);
  241. }
  242. if (this._onListChanged !== null && onListChanged === null) {
  243. wdm.removeListener(this);
  244. }
  245. }
  246. this._onListChanged = onListChanged;
  247. },
  248. _notifyListChanged() {
  249. this._onListChanged();
  250. if (this._onListChanged !== null) {
  251. wdm.removeListener(this);
  252. }
  253. this._mustNotify = false;
  254. },
  255. onRegister(dbg) {
  256. if (matchWorkerDebugger(dbg, this._options)) {
  257. this._notifyListChanged();
  258. }
  259. },
  260. onUnregister(dbg) {
  261. if (matchWorkerDebugger(dbg, this._options)) {
  262. this._notifyListChanged();
  263. }
  264. }
  265. };
  266. exports.WorkerActorList = WorkerActorList;
  267. let PushSubscriptionActor = protocol.ActorClassWithSpec(pushSubscriptionSpec, {
  268. initialize(conn, subscription) {
  269. protocol.Actor.prototype.initialize.call(this, conn);
  270. this._subscription = subscription;
  271. },
  272. form(detail) {
  273. if (detail === "actorid") {
  274. return this.actorID;
  275. }
  276. let subscription = this._subscription;
  277. return {
  278. actor: this.actorID,
  279. endpoint: subscription.endpoint,
  280. pushCount: subscription.pushCount,
  281. lastPush: subscription.lastPush,
  282. quota: subscription.quota
  283. };
  284. },
  285. destroy() {
  286. protocol.Actor.prototype.destroy.call(this);
  287. this._subscription = null;
  288. },
  289. });
  290. let ServiceWorkerActor = protocol.ActorClassWithSpec(serviceWorkerSpec, {
  291. initialize(conn, worker) {
  292. protocol.Actor.prototype.initialize.call(this, conn);
  293. this._worker = worker;
  294. },
  295. form() {
  296. if (!this._worker) {
  297. return null;
  298. }
  299. return {
  300. url: this._worker.scriptSpec,
  301. state: this._worker.state,
  302. };
  303. },
  304. destroy() {
  305. protocol.Actor.prototype.destroy.call(this);
  306. this._worker = null;
  307. },
  308. });
  309. // Lazily load the service-worker-child.js process script only once.
  310. let _serviceWorkerProcessScriptLoaded = false;
  311. let ServiceWorkerRegistrationActor =
  312. protocol.ActorClassWithSpec(serviceWorkerRegistrationSpec, {
  313. /**
  314. * Create the ServiceWorkerRegistrationActor
  315. * @param DebuggerServerConnection conn
  316. * The server connection.
  317. * @param ServiceWorkerRegistrationInfo registration
  318. * The registration's information.
  319. */
  320. initialize(conn, registration) {
  321. protocol.Actor.prototype.initialize.call(this, conn);
  322. this._conn = conn;
  323. this._registration = registration;
  324. this._pushSubscriptionActor = null;
  325. this._registration.addListener(this);
  326. let {installingWorker, waitingWorker, activeWorker} = registration;
  327. this._installingWorker = new ServiceWorkerActor(conn, installingWorker);
  328. this._waitingWorker = new ServiceWorkerActor(conn, waitingWorker);
  329. this._activeWorker = new ServiceWorkerActor(conn, activeWorker);
  330. Services.obs.addObserver(this, PushService.subscriptionModifiedTopic, false);
  331. },
  332. onChange() {
  333. this._installingWorker.destroy();
  334. this._waitingWorker.destroy();
  335. this._activeWorker.destroy();
  336. let {installingWorker, waitingWorker, activeWorker} = this._registration;
  337. this._installingWorker = new ServiceWorkerActor(this._conn, installingWorker);
  338. this._waitingWorker = new ServiceWorkerActor(this._conn, waitingWorker);
  339. this._activeWorker = new ServiceWorkerActor(this._conn, activeWorker);
  340. events.emit(this, "registration-changed");
  341. },
  342. form(detail) {
  343. if (detail === "actorid") {
  344. return this.actorID;
  345. }
  346. let registration = this._registration;
  347. let installingWorker = this._installingWorker.form();
  348. let waitingWorker = this._waitingWorker.form();
  349. let activeWorker = this._activeWorker.form();
  350. let isE10s = Services.appinfo.browserTabsRemoteAutostart;
  351. return {
  352. actor: this.actorID,
  353. scope: registration.scope,
  354. url: registration.scriptSpec,
  355. installingWorker,
  356. waitingWorker,
  357. activeWorker,
  358. // - In e10s: only active registrations are available.
  359. // - In non-e10s: registrations always have at least one worker, if the worker is
  360. // active, the registration is active.
  361. active: isE10s ? true : !!activeWorker
  362. };
  363. },
  364. destroy() {
  365. protocol.Actor.prototype.destroy.call(this);
  366. Services.obs.removeObserver(this, PushService.subscriptionModifiedTopic, false);
  367. this._registration.removeListener(this);
  368. this._registration = null;
  369. if (this._pushSubscriptionActor) {
  370. this._pushSubscriptionActor.destroy();
  371. }
  372. this._pushSubscriptionActor = null;
  373. this._installingWorker.destroy();
  374. this._waitingWorker.destroy();
  375. this._activeWorker.destroy();
  376. this._installingWorker = null;
  377. this._waitingWorker = null;
  378. this._activeWorker = null;
  379. },
  380. disconnect() {
  381. this.destroy();
  382. },
  383. /**
  384. * Standard observer interface to listen to push messages and changes.
  385. */
  386. observe(subject, topic, data) {
  387. let scope = this._registration.scope;
  388. if (data !== scope) {
  389. // This event doesn't concern us, pretend nothing happened.
  390. return;
  391. }
  392. switch (topic) {
  393. case PushService.subscriptionModifiedTopic:
  394. if (this._pushSubscriptionActor) {
  395. this._pushSubscriptionActor.destroy();
  396. this._pushSubscriptionActor = null;
  397. }
  398. events.emit(this, "push-subscription-modified");
  399. break;
  400. }
  401. },
  402. start() {
  403. if (!_serviceWorkerProcessScriptLoaded) {
  404. Services.ppmm.loadProcessScript(
  405. "resource://devtools/server/service-worker-child.js", true);
  406. _serviceWorkerProcessScriptLoaded = true;
  407. }
  408. Services.ppmm.broadcastAsyncMessage("serviceWorkerRegistration:start", {
  409. scope: this._registration.scope
  410. });
  411. return { type: "started" };
  412. },
  413. unregister() {
  414. let { principal, scope } = this._registration;
  415. let unregisterCallback = {
  416. unregisterSucceeded: function () {},
  417. unregisterFailed: function () {
  418. console.error("Failed to unregister the service worker for " + scope);
  419. },
  420. QueryInterface: XPCOMUtils.generateQI(
  421. [Ci.nsIServiceWorkerUnregisterCallback])
  422. };
  423. swm.propagateUnregister(principal, unregisterCallback, scope);
  424. return { type: "unregistered" };
  425. },
  426. getPushSubscription() {
  427. let registration = this._registration;
  428. let pushSubscriptionActor = this._pushSubscriptionActor;
  429. if (pushSubscriptionActor) {
  430. return Promise.resolve(pushSubscriptionActor);
  431. }
  432. return new Promise((resolve, reject) => {
  433. PushService.getSubscription(
  434. registration.scope,
  435. registration.principal,
  436. (result, subscription) => {
  437. if (!subscription) {
  438. resolve(null);
  439. return;
  440. }
  441. pushSubscriptionActor = new PushSubscriptionActor(this._conn, subscription);
  442. this._pushSubscriptionActor = pushSubscriptionActor;
  443. resolve(pushSubscriptionActor);
  444. }
  445. );
  446. });
  447. },
  448. });
  449. function ServiceWorkerRegistrationActorList(conn) {
  450. this._conn = conn;
  451. this._actors = new Map();
  452. this._onListChanged = null;
  453. this._mustNotify = false;
  454. this.onRegister = this.onRegister.bind(this);
  455. this.onUnregister = this.onUnregister.bind(this);
  456. }
  457. ServiceWorkerRegistrationActorList.prototype = {
  458. getList() {
  459. // Create a set of registrations.
  460. let registrations = new Set();
  461. let array = swm.getAllRegistrations();
  462. for (let index = 0; index < array.length; ++index) {
  463. registrations.add(
  464. array.queryElementAt(index, Ci.nsIServiceWorkerRegistrationInfo));
  465. }
  466. // Delete each actor for which we don't have a registration.
  467. for (let [registration, ] of this._actors) {
  468. if (!registrations.has(registration)) {
  469. this._actors.delete(registration);
  470. }
  471. }
  472. // Create an actor for each registration for which we don't have one.
  473. for (let registration of registrations) {
  474. if (!this._actors.has(registration)) {
  475. this._actors.set(registration,
  476. new ServiceWorkerRegistrationActor(this._conn, registration));
  477. }
  478. }
  479. if (!this._mustNotify) {
  480. if (this._onListChanged !== null) {
  481. swm.addListener(this);
  482. }
  483. this._mustNotify = true;
  484. }
  485. let actors = [];
  486. for (let [, actor] of this._actors) {
  487. actors.push(actor);
  488. }
  489. return Promise.resolve(actors);
  490. },
  491. get onListchanged() {
  492. return this._onListchanged;
  493. },
  494. set onListChanged(onListChanged) {
  495. if (typeof onListChanged !== "function" && onListChanged !== null) {
  496. throw new Error("onListChanged must be either a function or null.");
  497. }
  498. if (this._mustNotify) {
  499. if (this._onListChanged === null && onListChanged !== null) {
  500. swm.addListener(this);
  501. }
  502. if (this._onListChanged !== null && onListChanged === null) {
  503. swm.removeListener(this);
  504. }
  505. }
  506. this._onListChanged = onListChanged;
  507. },
  508. _notifyListChanged() {
  509. this._onListChanged();
  510. if (this._onListChanged !== null) {
  511. swm.removeListener(this);
  512. }
  513. this._mustNotify = false;
  514. },
  515. onRegister(registration) {
  516. this._notifyListChanged();
  517. },
  518. onUnregister(registration) {
  519. this._notifyListChanged();
  520. }
  521. };
  522. exports.ServiceWorkerRegistrationActorList = ServiceWorkerRegistrationActorList;