director-manager.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  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 events = require("sdk/event/core");
  6. const protocol = require("devtools/shared/protocol");
  7. const { Cu, Ci } = require("chrome");
  8. const { on, once, off, emit } = events;
  9. const { method, Arg, Option, RetVal, types } = protocol;
  10. const { sandbox, evaluate } = require("sdk/loader/sandbox");
  11. const { Class } = require("sdk/core/heritage");
  12. const { PlainTextConsole } = require("sdk/console/plain-text");
  13. const { DirectorRegistry } = require("./director-registry");
  14. const {
  15. messagePortSpec,
  16. directorManagerSpec,
  17. directorScriptSpec,
  18. } = require("devtools/shared/specs/director-manager");
  19. /**
  20. * Error Messages
  21. */
  22. const ERR_MESSAGEPORT_FINALIZED = "message port finalized";
  23. const ERR_DIRECTOR_UNKNOWN_SCRIPTID = "unkown director-script id";
  24. const ERR_DIRECTOR_UNINSTALLED_SCRIPTID = "uninstalled director-script id";
  25. /**
  26. * A MessagePort Actor allowing communication through messageport events
  27. * over the remote debugging protocol.
  28. */
  29. var MessagePortActor = exports.MessagePortActor = protocol.ActorClassWithSpec(messagePortSpec, {
  30. /**
  31. * Create a MessagePort actor.
  32. *
  33. * @param DebuggerServerConnection conn
  34. * The server connection.
  35. * @param MessagePort port
  36. * The wrapped MessagePort.
  37. */
  38. initialize: function (conn, port) {
  39. protocol.Actor.prototype.initialize.call(this, conn);
  40. // NOTE: can't get a weak reference because we need to subscribe events
  41. // using port.onmessage or addEventListener
  42. this.port = port;
  43. },
  44. destroy: function (conn) {
  45. protocol.Actor.prototype.destroy.call(this, conn);
  46. this.finalize();
  47. },
  48. /**
  49. * Sends a message on the wrapped message port.
  50. *
  51. * @param Object msg
  52. * The JSON serializable message event payload
  53. */
  54. postMessage: function (msg) {
  55. if (!this.port) {
  56. console.error(ERR_MESSAGEPORT_FINALIZED);
  57. return;
  58. }
  59. this.port.postMessage(msg);
  60. },
  61. /**
  62. * Starts to receive and send queued messages on this message port.
  63. */
  64. start: function () {
  65. if (!this.port) {
  66. console.error(ERR_MESSAGEPORT_FINALIZED);
  67. return;
  68. }
  69. // NOTE: set port.onmessage to a function is an implicit start
  70. // and starts to send queued messages.
  71. // On the client side we should set MessagePortClient.onmessage
  72. // to a setter which register an handler to the message event
  73. // and call the actor start method to start receiving messages
  74. // from the MessagePort's queue.
  75. this.port.onmessage = (evt) => {
  76. var ports;
  77. // TODO: test these wrapped ports
  78. if (Array.isArray(evt.ports)) {
  79. ports = evt.ports.map((port) => {
  80. let actor = new MessagePortActor(this.conn, port);
  81. this.manage(actor);
  82. return actor;
  83. });
  84. }
  85. emit(this, "message", {
  86. isTrusted: evt.isTrusted,
  87. data: evt.data,
  88. origin: evt.origin,
  89. lastEventId: evt.lastEventId,
  90. source: this,
  91. ports: ports
  92. });
  93. };
  94. },
  95. /**
  96. * Starts to receive and send queued messages on this message port, or
  97. * raise an exception if the port is null
  98. */
  99. close: function () {
  100. if (!this.port) {
  101. console.error(ERR_MESSAGEPORT_FINALIZED);
  102. return;
  103. }
  104. try {
  105. this.port.onmessage = null;
  106. this.port.close();
  107. } catch (e) {
  108. // The port might be a dead object
  109. console.error(e);
  110. }
  111. },
  112. finalize: function () {
  113. this.close();
  114. this.port = null;
  115. },
  116. });
  117. /**
  118. * The Director Script Actor manage javascript code running in a non-privileged sandbox with the same
  119. * privileges of the target global (browser tab or a firefox os app).
  120. *
  121. * After retrieving an instance of this actor (from the tab director actor), you'll need to set it up
  122. * by calling setup().
  123. *
  124. * After the setup, this actor will automatically attach/detach the content script (and optionally a
  125. * directly connect the debugger client and the content script using a MessageChannel) on tab
  126. * navigation.
  127. */
  128. var DirectorScriptActor = exports.DirectorScriptActor = protocol.ActorClassWithSpec(directorScriptSpec, {
  129. /**
  130. * Creates the director script actor
  131. *
  132. * @param DebuggerServerConnection conn
  133. * The server connection.
  134. * @param Actor tabActor
  135. * The tab (or root) actor.
  136. * @param String scriptId
  137. * The director-script id.
  138. * @param String scriptCode
  139. * The director-script javascript source.
  140. * @param Object scriptOptions
  141. * The director-script options object.
  142. */
  143. initialize: function (conn, tabActor, { scriptId, scriptCode, scriptOptions }) {
  144. protocol.Actor.prototype.initialize.call(this, conn, tabActor);
  145. this.tabActor = tabActor;
  146. this._scriptId = scriptId;
  147. this._scriptCode = scriptCode;
  148. this._scriptOptions = scriptOptions;
  149. this._setupCalled = false;
  150. this._onGlobalCreated = this._onGlobalCreated.bind(this);
  151. this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this);
  152. },
  153. destroy: function (conn) {
  154. protocol.Actor.prototype.destroy.call(this, conn);
  155. this.finalize();
  156. },
  157. /**
  158. * Starts listening to the tab global created, in order to create the director-script sandbox
  159. * using the configured scriptCode, attached/detached automatically to the tab
  160. * window on tab navigation.
  161. *
  162. * @param Boolean reload
  163. * attach the page immediately or reload it first.
  164. * @param Boolean skipAttach
  165. * skip the attach
  166. */
  167. setup: function ({ reload, skipAttach }) {
  168. if (this._setupCalled) {
  169. // do nothing
  170. return;
  171. }
  172. this._setupCalled = true;
  173. on(this.tabActor, "window-ready", this._onGlobalCreated);
  174. on(this.tabActor, "window-destroyed", this._onGlobalDestroyed);
  175. // optional skip attach (needed by director-manager for director scripts bulk activation)
  176. if (skipAttach) {
  177. return;
  178. }
  179. if (reload) {
  180. this.window.location.reload();
  181. } else {
  182. // fake a global created event to attach without reload
  183. this._onGlobalCreated({ id: getWindowID(this.window), window: this.window, isTopLevel: true });
  184. }
  185. },
  186. /**
  187. * Get the attached MessagePort actor if any
  188. */
  189. getMessagePort: function () {
  190. return this._messagePortActor;
  191. },
  192. /**
  193. * Stop listening for document global changes, destroy the content worker and puts
  194. * this actor to hibernation.
  195. */
  196. finalize: function () {
  197. if (!this._setupCalled) {
  198. return;
  199. }
  200. off(this.tabActor, "window-ready", this._onGlobalCreated);
  201. off(this.tabActor, "window-destroyed", this._onGlobalDestroyed);
  202. this._onGlobalDestroyed({ id: this._lastAttachedWinId });
  203. this._setupCalled = false;
  204. },
  205. // local helpers
  206. get window() {
  207. return this.tabActor.window;
  208. },
  209. /* event handlers */
  210. _onGlobalCreated: function ({ id, window, isTopLevel }) {
  211. if (!isTopLevel) {
  212. // filter iframes
  213. return;
  214. }
  215. try {
  216. if (this._lastAttachedWinId) {
  217. // if we have received a global created without a previous global destroyed,
  218. // it's time to cleanup the previous state
  219. this._onGlobalDestroyed(this._lastAttachedWinId);
  220. }
  221. // TODO: check if we want to share a single sandbox per global
  222. // for multiple debugger clients
  223. // create & attach the new sandbox
  224. this._scriptSandbox = new DirectorScriptSandbox({
  225. scriptId: this._scriptId,
  226. scriptCode: this._scriptCode,
  227. scriptOptions: this._scriptOptions
  228. });
  229. // attach the global window
  230. this._lastAttachedWinId = id;
  231. var port = this._scriptSandbox.attach(window, id);
  232. this._onDirectorScriptAttach(window, port);
  233. } catch (e) {
  234. this._onDirectorScriptError(e);
  235. }
  236. },
  237. _onGlobalDestroyed: function ({ id }) {
  238. if (id !== this._lastAttachedWinId) {
  239. // filter destroyed globals
  240. return;
  241. }
  242. // unmanage and cleanup the messageport actor
  243. if (this._messagePortActor) {
  244. this.unmanage(this._messagePortActor);
  245. this._messagePortActor = null;
  246. }
  247. // NOTE: destroy here the old worker
  248. if (this._scriptSandbox) {
  249. this._scriptSandbox.destroy(this._onDirectorScriptError.bind(this));
  250. // send a detach event to the debugger client
  251. emit(this, "detach", {
  252. directorScriptId: this._scriptId,
  253. innerId: this._lastAttachedWinId
  254. });
  255. this._lastAttachedWinId = null;
  256. this._scriptSandbox = null;
  257. }
  258. },
  259. _onDirectorScriptError: function (error) {
  260. // route the content script error to the debugger client
  261. if (error) {
  262. // prevents silent director-script-errors
  263. console.error("director-script-error", error);
  264. // route errors to debugger server clients
  265. emit(this, "error", {
  266. directorScriptId: this._scriptId,
  267. message: error.toString(),
  268. stack: error.stack,
  269. fileName: error.fileName,
  270. lineNumber: error.lineNumber,
  271. columnNumber: error.columnNumber
  272. });
  273. }
  274. },
  275. _onDirectorScriptAttach: function (window, port) {
  276. let portActor = new MessagePortActor(this.conn, port);
  277. this.manage(portActor);
  278. this._messagePortActor = portActor;
  279. emit(this, "attach", {
  280. directorScriptId: this._scriptId,
  281. url: (window && window.location) ? window.location.toString() : "",
  282. innerId: this._lastAttachedWinId,
  283. port: this._messagePortActor
  284. });
  285. }
  286. });
  287. /**
  288. * The DirectorManager Actor is a tab actor which manages enabling/disabling director scripts.
  289. */
  290. const DirectorManagerActor = exports.DirectorManagerActor = protocol.ActorClassWithSpec(directorManagerSpec, {
  291. /* init & destroy methods */
  292. initialize: function (conn, tabActor) {
  293. protocol.Actor.prototype.initialize.call(this, conn);
  294. this.tabActor = tabActor;
  295. this._directorScriptActorsMap = new Map();
  296. },
  297. destroy: function (conn) {
  298. protocol.Actor.prototype.destroy.call(this, conn);
  299. this.finalize();
  300. },
  301. /**
  302. * Retrieves the list of installed director-scripts.
  303. */
  304. list: function () {
  305. let enabledScriptIds = [...this._directorScriptActorsMap.keys()];
  306. return {
  307. installed: DirectorRegistry.list(),
  308. enabled: enabledScriptIds
  309. };
  310. },
  311. /**
  312. * Bulk enabling director-scripts.
  313. *
  314. * @param Array[String] selectedIds
  315. * The list of director-script ids to be enabled,
  316. * ["*"] will activate all the installed director-scripts
  317. * @param Boolean reload
  318. * optionally reload the target window
  319. */
  320. enableByScriptIds: function (selectedIds, { reload }) {
  321. if (selectedIds && selectedIds.length === 0) {
  322. // filtered all director scripts ids
  323. return;
  324. }
  325. for (let scriptId of DirectorRegistry.list()) {
  326. // filter director script ids
  327. if (selectedIds.indexOf("*") < 0 &&
  328. selectedIds.indexOf(scriptId) < 0) {
  329. continue;
  330. }
  331. let actor = this.getByScriptId(scriptId);
  332. // skip attach if reload is true (activated director scripts
  333. // will be automatically attached on the final reload)
  334. actor.setup({ reload: false, skipAttach: reload });
  335. }
  336. if (reload) {
  337. this.tabActor.window.location.reload();
  338. }
  339. },
  340. /**
  341. * Bulk disabling director-scripts.
  342. *
  343. * @param Array[String] selectedIds
  344. * The list of director-script ids to be disable,
  345. * ["*"] will de-activate all the enable director-scripts
  346. * @param Boolean reload
  347. * optionally reload the target window
  348. */
  349. disableByScriptIds: function (selectedIds, { reload }) {
  350. if (selectedIds && selectedIds.length === 0) {
  351. // filtered all director scripts ids
  352. return;
  353. }
  354. for (let scriptId of this._directorScriptActorsMap.keys()) {
  355. // filter director script ids
  356. if (selectedIds.indexOf("*") < 0 &&
  357. selectedIds.indexOf(scriptId) < 0) {
  358. continue;
  359. }
  360. let actor = this._directorScriptActorsMap.get(scriptId);
  361. this._directorScriptActorsMap.delete(scriptId);
  362. // finalize the actor (which will produce director-script-detach event)
  363. actor.finalize();
  364. // unsubscribe event handlers on the disabled actor
  365. off(actor);
  366. this.unmanage(actor);
  367. }
  368. if (reload) {
  369. this.tabActor.window.location.reload();
  370. }
  371. },
  372. /**
  373. * Retrieves the actor instance of an installed director-script
  374. * (and create the actor instance if it doesn't exists yet).
  375. */
  376. getByScriptId: function (scriptId) {
  377. var id = scriptId;
  378. // raise an unknown director-script id exception
  379. if (!DirectorRegistry.checkInstalled(id)) {
  380. console.error(ERR_DIRECTOR_UNKNOWN_SCRIPTID, id);
  381. throw Error(ERR_DIRECTOR_UNKNOWN_SCRIPTID);
  382. }
  383. // get a previous created actor instance
  384. let actor = this._directorScriptActorsMap.get(id);
  385. // create a new actor instance
  386. if (!actor) {
  387. let directorScriptDefinition = DirectorRegistry.get(id);
  388. // test lazy director-script (e.g. uninstalled in the parent process)
  389. if (!directorScriptDefinition) {
  390. console.error(ERR_DIRECTOR_UNINSTALLED_SCRIPTID, id);
  391. throw Error(ERR_DIRECTOR_UNINSTALLED_SCRIPTID);
  392. }
  393. actor = new DirectorScriptActor(this.conn, this.tabActor, directorScriptDefinition);
  394. this._directorScriptActorsMap.set(id, actor);
  395. on(actor, "error", emit.bind(null, this, "director-script-error"));
  396. on(actor, "attach", emit.bind(null, this, "director-script-attach"));
  397. on(actor, "detach", emit.bind(null, this, "director-script-detach"));
  398. this.manage(actor);
  399. }
  400. return actor;
  401. },
  402. finalize: function () {
  403. this.disableByScriptIds(["*"], false);
  404. }
  405. });
  406. /* private helpers */
  407. /**
  408. * DirectorScriptSandbox is a private utility class, which attach a non-priviliged sandbox
  409. * to a target window.
  410. */
  411. const DirectorScriptSandbox = Class({
  412. initialize: function ({scriptId, scriptCode, scriptOptions}) {
  413. this._scriptId = scriptId;
  414. this._scriptCode = scriptCode;
  415. this._scriptOptions = scriptOptions;
  416. },
  417. attach: function (window, innerId) {
  418. this._innerId = innerId,
  419. this._window = window;
  420. this._proto = Cu.createObjectIn(this._window);
  421. var id = this._scriptId;
  422. var uri = this._scriptCode;
  423. this._sandbox = sandbox(window, {
  424. sandboxName: uri,
  425. sandboxPrototype: this._proto,
  426. sameZoneAs: window,
  427. wantXrays: true,
  428. wantComponents: false,
  429. wantExportHelpers: false,
  430. metadata: {
  431. URI: uri,
  432. addonID: id,
  433. SDKDirectorScript: true,
  434. "inner-window-id": innerId
  435. }
  436. });
  437. // create a CommonJS module object which match the interface from addon-sdk
  438. // (addon-sdk/sources/lib/toolkit/loader.js#L678-L686)
  439. var module = Cu.cloneInto(Object.create(null, {
  440. id: { enumerable: true, value: id },
  441. uri: { enumerable: true, value: uri },
  442. exports: { enumerable: true, value: Cu.createObjectIn(this._sandbox) }
  443. }), this._sandbox);
  444. // create a console API object
  445. let directorScriptConsole = new PlainTextConsole(null, this._innerId);
  446. // inject CommonJS module globals into the sandbox prototype
  447. Object.defineProperties(this._proto, {
  448. module: { enumerable: true, value: module },
  449. exports: { enumerable: true, value: module.exports },
  450. console: {
  451. enumerable: true,
  452. value: Cu.cloneInto(directorScriptConsole, this._sandbox, { cloneFunctions: true })
  453. }
  454. });
  455. Object.defineProperties(this._sandbox, {
  456. require: {
  457. enumerable: true,
  458. value: Cu.cloneInto(function () {
  459. throw Error("NOT IMPLEMENTED");
  460. }, this._sandbox, { cloneFunctions: true })
  461. }
  462. });
  463. // TODO: if the debugger target is local, the debugger client could pass
  464. // to the director actor the resource url instead of the entire javascript source code.
  465. // evaluate the director script source in the sandbox
  466. evaluate(this._sandbox, this._scriptCode, "javascript:" + this._scriptCode);
  467. // prepare the messageport connected to the debugger client
  468. let { port1, port2 } = new this._window.MessageChannel();
  469. // prepare the unload callbacks queue
  470. var sandboxOnUnloadQueue = this._sandboxOnUnloadQueue = [];
  471. // create the attach options
  472. var attachOptions = this._attachOptions = Cu.createObjectIn(this._sandbox);
  473. Object.defineProperties(attachOptions, {
  474. port: { enumerable: true, value: port1 },
  475. window: { enumerable: true, value: window },
  476. scriptOptions: { enumerable: true, value: Cu.cloneInto(this._scriptOptions, this._sandbox) },
  477. onUnload: {
  478. enumerable: true,
  479. value: Cu.cloneInto(function (cb) {
  480. // collect unload callbacks
  481. if (typeof cb == "function") {
  482. sandboxOnUnloadQueue.push(cb);
  483. }
  484. }, this._sandbox, { cloneFunctions: true })
  485. }
  486. });
  487. // select the attach method
  488. var exports = this._proto.module.exports;
  489. if (this._scriptOptions && "attachMethod" in this._scriptOptions) {
  490. this._sandboxOnAttach = exports[this._scriptOptions.attachMethod];
  491. } else {
  492. this._sandboxOnAttach = exports;
  493. }
  494. if (typeof this._sandboxOnAttach !== "function") {
  495. throw Error("the configured attachMethod '" +
  496. (this._scriptOptions.attachMethod || "module.exports") +
  497. "' is not exported by the directorScript");
  498. }
  499. // call the attach method
  500. this._sandboxOnAttach.call(this._sandbox, attachOptions);
  501. return port2;
  502. },
  503. destroy: function (onError) {
  504. // evaluate queue unload methods if any
  505. while (this._sandboxOnUnloadQueue && this._sandboxOnUnloadQueue.length > 0) {
  506. let cb = this._sandboxOnUnloadQueue.pop();
  507. try {
  508. cb();
  509. } catch (e) {
  510. console.error("Exception on DirectorScript Sandbox destroy", e);
  511. onError(e);
  512. }
  513. }
  514. Cu.nukeSandbox(this._sandbox);
  515. }
  516. });
  517. function getWindowID(window) {
  518. return window.QueryInterface(Ci.nsIInterfaceRequestor)
  519. .getInterface(Ci.nsIDOMWindowUtils)
  520. .currentInnerWindowID;
  521. }