app-actor-front.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841
  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, Cc, Cr} = require("chrome");
  6. const {OS} = require("resource://gre/modules/osfile.jsm");
  7. const {FileUtils} = require("resource://gre/modules/FileUtils.jsm");
  8. const {NetUtil} = require("resource://gre/modules/NetUtil.jsm");
  9. const promise = require("promise");
  10. const defer = require("devtools/shared/defer");
  11. const DevToolsUtils = require("devtools/shared/DevToolsUtils");
  12. const EventEmitter = require("devtools/shared/event-emitter");
  13. // Bug 1188401: When loaded from xpcshell tests, we do not have browser/ files
  14. // and can't load target.js. Should be fixed by bug 912121.
  15. loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
  16. // XXX: bug 912476 make this module a real protocol.js front
  17. // by converting webapps actor to protocol.js
  18. const PR_USEC_PER_MSEC = 1000;
  19. const PR_RDWR = 0x04;
  20. const PR_CREATE_FILE = 0x08;
  21. const PR_TRUNCATE = 0x20;
  22. const CHUNK_SIZE = 10000;
  23. const appTargets = new Map();
  24. function addDirToZip(writer, dir, basePath) {
  25. let files = dir.directoryEntries;
  26. while (files.hasMoreElements()) {
  27. let file = files.getNext().QueryInterface(Ci.nsIFile);
  28. if (file.isHidden() ||
  29. file.isSpecial() ||
  30. file.equals(writer.file))
  31. {
  32. continue;
  33. }
  34. if (file.isDirectory()) {
  35. writer.addEntryDirectory(basePath + file.leafName + "/",
  36. file.lastModifiedTime * PR_USEC_PER_MSEC,
  37. true);
  38. addDirToZip(writer, file, basePath + file.leafName + "/");
  39. } else {
  40. writer.addEntryFile(basePath + file.leafName,
  41. Ci.nsIZipWriter.COMPRESSION_DEFAULT,
  42. file,
  43. true);
  44. }
  45. }
  46. }
  47. /**
  48. * Convert an XPConnect result code to its name and message.
  49. * We have to extract them from an exception per bug 637307 comment 5.
  50. */
  51. function getResultText(code) {
  52. let regexp =
  53. /^\[Exception... "(.*)" nsresult: "0x[0-9a-fA-F]* \((.*)\)" location: ".*" data: .*\]$/;
  54. let ex = Cc["@mozilla.org/js/xpc/Exception;1"].
  55. createInstance(Ci.nsIXPCException);
  56. ex.initialize(null, code, null, null, null, null);
  57. let [, message, name] = regexp.exec(ex.toString());
  58. return { name: name, message: message };
  59. }
  60. function zipDirectory(zipFile, dirToArchive) {
  61. let deferred = defer();
  62. let writer = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
  63. writer.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
  64. this.addDirToZip(writer, dirToArchive, "");
  65. writer.processQueue({
  66. onStartRequest: function onStartRequest(request, context) {},
  67. onStopRequest: (request, context, status) => {
  68. if (status == Cr.NS_OK) {
  69. writer.close();
  70. deferred.resolve(zipFile);
  71. }
  72. else {
  73. let { name, message } = getResultText(status);
  74. deferred.reject(name + ": " + message);
  75. }
  76. }
  77. }, null);
  78. return deferred.promise;
  79. }
  80. function uploadPackage(client, webappsActor, packageFile, progressCallback) {
  81. if (client.traits.bulk) {
  82. return uploadPackageBulk(client, webappsActor, packageFile, progressCallback);
  83. } else {
  84. return uploadPackageJSON(client, webappsActor, packageFile, progressCallback);
  85. }
  86. }
  87. function uploadPackageJSON(client, webappsActor, packageFile, progressCallback) {
  88. let deferred = defer();
  89. let request = {
  90. to: webappsActor,
  91. type: "uploadPackage"
  92. };
  93. client.request(request, (res) => {
  94. openFile(res.actor);
  95. });
  96. let fileSize;
  97. let bytesRead = 0;
  98. function emitProgress() {
  99. progressCallback({
  100. bytesSent: bytesRead,
  101. totalBytes: fileSize
  102. });
  103. }
  104. function openFile(actor) {
  105. let openedFile;
  106. OS.File.open(packageFile.path)
  107. .then(file => {
  108. openedFile = file;
  109. return openedFile.stat();
  110. })
  111. .then(fileInfo => {
  112. fileSize = fileInfo.size;
  113. emitProgress();
  114. uploadChunk(actor, openedFile);
  115. });
  116. }
  117. function uploadChunk(actor, file) {
  118. file.read(CHUNK_SIZE)
  119. .then(function (bytes) {
  120. bytesRead += bytes.length;
  121. emitProgress();
  122. // To work around the fact that JSON.stringify translates the typed
  123. // array to object, we are encoding the typed array here into a string
  124. let chunk = String.fromCharCode.apply(null, bytes);
  125. let request = {
  126. to: actor,
  127. type: "chunk",
  128. chunk: chunk
  129. };
  130. client.request(request, (res) => {
  131. if (bytes.length == CHUNK_SIZE) {
  132. uploadChunk(actor, file);
  133. } else {
  134. file.close().then(function () {
  135. endsUpload(actor);
  136. });
  137. }
  138. });
  139. });
  140. }
  141. function endsUpload(actor) {
  142. let request = {
  143. to: actor,
  144. type: "done"
  145. };
  146. client.request(request, (res) => {
  147. deferred.resolve(actor);
  148. });
  149. }
  150. return deferred.promise;
  151. }
  152. function uploadPackageBulk(client, webappsActor, packageFile, progressCallback) {
  153. let deferred = defer();
  154. let request = {
  155. to: webappsActor,
  156. type: "uploadPackage",
  157. bulk: true
  158. };
  159. client.request(request, (res) => {
  160. startBulkUpload(res.actor);
  161. });
  162. function startBulkUpload(actor) {
  163. console.log("Starting bulk upload");
  164. let fileSize = packageFile.fileSize;
  165. console.log("File size: " + fileSize);
  166. let request = client.startBulkRequest({
  167. actor: actor,
  168. type: "stream",
  169. length: fileSize
  170. });
  171. request.on("bulk-send-ready", ({copyFrom}) => {
  172. NetUtil.asyncFetch({
  173. uri: NetUtil.newURI(packageFile),
  174. loadUsingSystemPrincipal: true
  175. }, function (inputStream) {
  176. let copying = copyFrom(inputStream);
  177. copying.on("progress", (e, progress) => {
  178. progressCallback(progress);
  179. });
  180. copying.then(() => {
  181. console.log("Bulk upload done");
  182. inputStream.close();
  183. deferred.resolve(actor);
  184. });
  185. });
  186. });
  187. }
  188. return deferred.promise;
  189. }
  190. function removeServerTemporaryFile(client, fileActor) {
  191. let request = {
  192. to: fileActor,
  193. type: "remove"
  194. };
  195. client.request(request);
  196. }
  197. /**
  198. * progressCallback argument:
  199. * Function called as packaged app installation proceeds.
  200. * The progress object passed to this function contains:
  201. * * bytesSent: The number of bytes sent so far
  202. * * totalBytes: The total number of bytes to send
  203. */
  204. function installPackaged(client, webappsActor, packagePath, appId, progressCallback) {
  205. let deferred = defer();
  206. let file = FileUtils.File(packagePath);
  207. let packagePromise;
  208. if (file.isDirectory()) {
  209. let tmpZipFile = FileUtils.getDir("TmpD", [], true);
  210. tmpZipFile.append("application.zip");
  211. tmpZipFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
  212. packagePromise = zipDirectory(tmpZipFile, file);
  213. } else {
  214. packagePromise = promise.resolve(file);
  215. }
  216. packagePromise.then((zipFile) => {
  217. uploadPackage(client, webappsActor, zipFile, progressCallback)
  218. .then((fileActor) => {
  219. let request = {
  220. to: webappsActor,
  221. type: "install",
  222. appId: appId,
  223. upload: fileActor
  224. };
  225. client.request(request, (res) => {
  226. // If the install method immediatly fails,
  227. // reject immediatly the installPackaged promise.
  228. // Otherwise, wait for webappsEvent for completion
  229. if (res.error) {
  230. deferred.reject(res);
  231. }
  232. if ("error" in res)
  233. deferred.reject({error: res.error, message: res.message});
  234. else
  235. deferred.resolve({appId: res.appId});
  236. });
  237. // Ensure deleting the temporary package file, but only if that a temporary
  238. // package created when we pass a directory as `packagePath`
  239. if (zipFile != file)
  240. zipFile.remove(false);
  241. // In case of success or error, ensure deleting the temporary package file
  242. // also created on the device, but only once install request is done
  243. deferred.promise.then(
  244. () => removeServerTemporaryFile(client, fileActor),
  245. () => removeServerTemporaryFile(client, fileActor));
  246. });
  247. });
  248. return deferred.promise;
  249. }
  250. exports.installPackaged = installPackaged;
  251. function installHosted(client, webappsActor, appId, metadata, manifest) {
  252. let deferred = defer();
  253. let request = {
  254. to: webappsActor,
  255. type: "install",
  256. appId: appId,
  257. metadata: metadata,
  258. manifest: manifest
  259. };
  260. client.request(request, (res) => {
  261. if (res.error) {
  262. deferred.reject(res);
  263. }
  264. if ("error" in res)
  265. deferred.reject({error: res.error, message: res.message});
  266. else
  267. deferred.resolve({appId: res.appId});
  268. });
  269. return deferred.promise;
  270. }
  271. exports.installHosted = installHosted;
  272. function getTargetForApp(client, webappsActor, manifestURL) {
  273. // Ensure always returning the exact same JS object for a target
  274. // of the same app in order to show only one toolbox per app and
  275. // avoid re-creating lot of objects twice.
  276. let existingTarget = appTargets.get(manifestURL);
  277. if (existingTarget)
  278. return promise.resolve(existingTarget);
  279. let deferred = defer();
  280. let request = {
  281. to: webappsActor,
  282. type: "getAppActor",
  283. manifestURL: manifestURL,
  284. };
  285. client.request(request, (res) => {
  286. if (res.error) {
  287. deferred.reject(res.error);
  288. } else {
  289. let options = {
  290. form: res.actor,
  291. client: client,
  292. chrome: false
  293. };
  294. TargetFactory.forRemoteTab(options).then((target) => {
  295. target.isApp = true;
  296. appTargets.set(manifestURL, target);
  297. target.on("close", () => {
  298. appTargets.delete(manifestURL);
  299. });
  300. deferred.resolve(target);
  301. }, (error) => {
  302. deferred.reject(error);
  303. });
  304. }
  305. });
  306. return deferred.promise;
  307. }
  308. exports.getTargetForApp = getTargetForApp;
  309. function reloadApp(client, webappsActor, manifestURL) {
  310. return getTargetForApp(client,
  311. webappsActor,
  312. manifestURL).
  313. then((target) => {
  314. // Request the ContentActor to reload the app
  315. let request = {
  316. to: target.form.actor,
  317. type: "reload",
  318. options: {
  319. force: true
  320. },
  321. manifestURL: manifestURL
  322. };
  323. return client.request(request);
  324. }, () => {
  325. throw new Error("Not running");
  326. });
  327. }
  328. exports.reloadApp = reloadApp;
  329. function launchApp(client, webappsActor, manifestURL) {
  330. return client.request({
  331. to: webappsActor,
  332. type: "launch",
  333. manifestURL: manifestURL
  334. });
  335. }
  336. exports.launchApp = launchApp;
  337. function closeApp(client, webappsActor, manifestURL) {
  338. return client.request({
  339. to: webappsActor,
  340. type: "close",
  341. manifestURL: manifestURL
  342. });
  343. }
  344. exports.closeApp = closeApp;
  345. function getTarget(client, form) {
  346. let deferred = defer();
  347. let options = {
  348. form: form,
  349. client: client,
  350. chrome: false
  351. };
  352. TargetFactory.forRemoteTab(options).then((target) => {
  353. target.isApp = true;
  354. deferred.resolve(target);
  355. }, (error) => {
  356. deferred.reject(error);
  357. });
  358. return deferred.promise;
  359. }
  360. /**
  361. * `App` instances are client helpers to manage a given app
  362. * and its the tab actors
  363. */
  364. function App(client, webappsActor, manifest) {
  365. this.client = client;
  366. this.webappsActor = webappsActor;
  367. this.manifest = manifest;
  368. // This attribute is managed by the AppActorFront
  369. this.running = false;
  370. this.iconURL = null;
  371. }
  372. App.prototype = {
  373. getForm: function () {
  374. if (this._form) {
  375. return promise.resolve(this._form);
  376. }
  377. let request = {
  378. to: this.webappsActor,
  379. type: "getAppActor",
  380. manifestURL: this.manifest.manifestURL
  381. };
  382. return this.client.request(request)
  383. .then(res => {
  384. return this._form = res.actor;
  385. });
  386. },
  387. getTarget: function () {
  388. if (this._target) {
  389. return promise.resolve(this._target);
  390. }
  391. return this.getForm().
  392. then((form) => getTarget(this.client, form)).
  393. then((target) => {
  394. target.on("close", () => {
  395. delete this._form;
  396. delete this._target;
  397. });
  398. return this._target = target;
  399. });
  400. },
  401. launch: function () {
  402. return launchApp(this.client, this.webappsActor,
  403. this.manifest.manifestURL);
  404. },
  405. reload: function () {
  406. return reloadApp(this.client, this.webappsActor,
  407. this.manifest.manifestURL);
  408. },
  409. close: function () {
  410. return closeApp(this.client, this.webappsActor,
  411. this.manifest.manifestURL);
  412. },
  413. getIcon: function () {
  414. if (this.iconURL) {
  415. return promise.resolve(this.iconURL);
  416. }
  417. let deferred = defer();
  418. let request = {
  419. to: this.webappsActor,
  420. type: "getIconAsDataURL",
  421. manifestURL: this.manifest.manifestURL
  422. };
  423. this.client.request(request, res => {
  424. if (res.error) {
  425. deferred.reject(res.message || res.error);
  426. } else if (res.url) {
  427. this.iconURL = res.url;
  428. deferred.resolve(res.url);
  429. } else {
  430. deferred.reject("Unable to fetch app icon");
  431. }
  432. });
  433. return deferred.promise;
  434. }
  435. };
  436. /**
  437. * `AppActorFront` is a client for the webapps actor.
  438. */
  439. function AppActorFront(client, form) {
  440. this.client = client;
  441. this.actor = form.webappsActor;
  442. this._clientListener = this._clientListener.bind(this);
  443. this._onInstallProgress = this._onInstallProgress.bind(this);
  444. this._listeners = [];
  445. EventEmitter.decorate(this);
  446. }
  447. AppActorFront.prototype = {
  448. /**
  449. * List `App` instances for all currently running apps.
  450. */
  451. get runningApps() {
  452. if (!this._apps) {
  453. throw new Error("Can't get running apps before calling watchApps.");
  454. }
  455. let r = new Map();
  456. for (let [manifestURL, app] of this._apps) {
  457. if (app.running) {
  458. r.set(manifestURL, app);
  459. }
  460. }
  461. return r;
  462. },
  463. /**
  464. * List `App` instances for all installed apps.
  465. */
  466. get apps() {
  467. if (!this._apps) {
  468. throw new Error("Can't get apps before calling watchApps.");
  469. }
  470. return this._apps;
  471. },
  472. /**
  473. * Returns a `App` object instance for the given manifest URL
  474. * (and cache it per AppActorFront object)
  475. */
  476. _getApp: function (manifestURL) {
  477. let app = this._apps ? this._apps.get(manifestURL) : null;
  478. if (app) {
  479. return promise.resolve(app);
  480. } else {
  481. let request = {
  482. to: this.actor,
  483. type: "getApp",
  484. manifestURL: manifestURL
  485. };
  486. return this.client.request(request)
  487. .then(res => {
  488. let app = new App(this.client, this.actor, res.app);
  489. if (this._apps) {
  490. this._apps.set(manifestURL, app);
  491. }
  492. return app;
  493. }, e => {
  494. console.error("Unable to retrieve app", manifestURL, e);
  495. });
  496. }
  497. },
  498. /**
  499. * Starts watching for app opening/closing installing/uninstalling.
  500. * Needs to be called before using `apps` or `runningApps` attributes.
  501. */
  502. watchApps: function (listener) {
  503. // Fixes race between two references to the same front
  504. // calling watchApps at the same time
  505. if (this._loadingPromise) {
  506. return this._loadingPromise;
  507. }
  508. // Only call watchApps for the first listener being register,
  509. // for all next ones, just send fake appOpen events for already
  510. // opened apps
  511. if (this._apps) {
  512. this.runningApps.forEach((app, manifestURL) => {
  513. listener("appOpen", app);
  514. });
  515. return promise.resolve();
  516. }
  517. // First retrieve all installed apps and create
  518. // related `App` object for each
  519. let request = {
  520. to: this.actor,
  521. type: "getAll"
  522. };
  523. return this._loadingPromise = this.client.request(request)
  524. .then(res => {
  525. delete this._loadingPromise;
  526. this._apps = new Map();
  527. for (let a of res.apps) {
  528. let app = new App(this.client, this.actor, a);
  529. this._apps.set(a.manifestURL, app);
  530. }
  531. })
  532. .then(() => {
  533. // Then retrieve all running apps in order to flag them as running
  534. let request = {
  535. to: this.actor,
  536. type: "listRunningApps"
  537. };
  538. return this.client.request(request)
  539. .then(res => res.apps);
  540. })
  541. .then(apps => {
  542. let promises = apps.map(manifestURL => {
  543. // _getApp creates `App` instance and register it to AppActorFront
  544. return this._getApp(manifestURL)
  545. .then(app => {
  546. app.running = true;
  547. // Fake appOpen event for all already opened
  548. this._notifyListeners("appOpen", app);
  549. });
  550. });
  551. return promise.all(promises);
  552. })
  553. .then(() => {
  554. // Finally ask to receive all app events
  555. return this._listenAppEvents(listener);
  556. });
  557. },
  558. fetchIcons: function () {
  559. // On demand, retrieve apps icons in order to be able
  560. // to synchronously retrieve it on `App` objects
  561. let promises = [];
  562. for (let [manifestURL, app] of this._apps) {
  563. promises.push(app.getIcon());
  564. }
  565. return DevToolsUtils.settleAll(promises)
  566. .then(null, () => {});
  567. },
  568. _listenAppEvents: function (listener) {
  569. this._listeners.push(listener);
  570. if (this._listeners.length > 1) {
  571. return promise.resolve();
  572. }
  573. let client = this.client;
  574. let f = this._clientListener;
  575. client.addListener("appOpen", f);
  576. client.addListener("appClose", f);
  577. client.addListener("appInstall", f);
  578. client.addListener("appUninstall", f);
  579. let request = {
  580. to: this.actor,
  581. type: "watchApps"
  582. };
  583. return this.client.request(request);
  584. },
  585. _unlistenAppEvents: function (listener) {
  586. let idx = this._listeners.indexOf(listener);
  587. if (idx != -1) {
  588. this._listeners.splice(idx, 1);
  589. }
  590. // Until we released all listener, we don't ask to stop sending events
  591. if (this._listeners.length != 0) {
  592. return promise.resolve();
  593. }
  594. let client = this.client;
  595. let f = this._clientListener;
  596. client.removeListener("appOpen", f);
  597. client.removeListener("appClose", f);
  598. client.removeListener("appInstall", f);
  599. client.removeListener("appUninstall", f);
  600. // Remove `_apps` in order to allow calling watchApps again
  601. // and repopulate the apps Map.
  602. delete this._apps;
  603. let request = {
  604. to: this.actor,
  605. type: "unwatchApps"
  606. };
  607. return this.client.request(request);
  608. },
  609. _clientListener: function (type, message) {
  610. let { manifestURL } = message;
  611. // Reset the app object to get a fresh copy when we (re)install the app.
  612. if (type == "appInstall" && this._apps && this._apps.has(manifestURL)) {
  613. this._apps.delete(manifestURL);
  614. }
  615. this._getApp(manifestURL).then((app) => {
  616. switch (type) {
  617. case "appOpen":
  618. app.running = true;
  619. this._notifyListeners("appOpen", app);
  620. break;
  621. case "appClose":
  622. app.running = false;
  623. this._notifyListeners("appClose", app);
  624. break;
  625. case "appInstall":
  626. // The call to _getApp is going to create App object
  627. // This app may have been running while being installed, so check the list
  628. // of running apps again to get the right answer.
  629. let request = {
  630. to: this.actor,
  631. type: "listRunningApps"
  632. };
  633. this.client.request(request)
  634. .then(res => {
  635. if (res.apps.indexOf(manifestURL) !== -1) {
  636. app.running = true;
  637. this._notifyListeners("appInstall", app);
  638. this._notifyListeners("appOpen", app);
  639. } else {
  640. this._notifyListeners("appInstall", app);
  641. }
  642. });
  643. break;
  644. case "appUninstall":
  645. // Fake a appClose event if we didn't got one before uninstall
  646. if (app.running) {
  647. app.running = false;
  648. this._notifyListeners("appClose", app);
  649. }
  650. this._apps.delete(manifestURL);
  651. this._notifyListeners("appUninstall", app);
  652. break;
  653. default:
  654. return;
  655. }
  656. });
  657. },
  658. _notifyListeners: function (type, app) {
  659. this._listeners.forEach(f => {
  660. f(type, app);
  661. });
  662. },
  663. unwatchApps: function (listener) {
  664. return this._unlistenAppEvents(listener);
  665. },
  666. /*
  667. * Install a packaged app.
  668. *
  669. * Events are going to be emitted on the front
  670. * as install progresses. Events will have the following fields:
  671. * * bytesSent: The number of bytes sent so far
  672. * * totalBytes: The total number of bytes to send
  673. */
  674. installPackaged: function (packagePath, appId) {
  675. let request = () => {
  676. return installPackaged(this.client, this.actor, packagePath, appId,
  677. this._onInstallProgress)
  678. .then(response => ({
  679. appId: response.appId,
  680. manifestURL: "app://" + response.appId + "/manifest.webapp"
  681. }));
  682. };
  683. return this._install(request);
  684. },
  685. _onInstallProgress: function (progress) {
  686. this.emit("install-progress", progress);
  687. },
  688. _install: function (request) {
  689. let deferred = defer();
  690. let finalAppId = null, manifestURL = null;
  691. let installs = {};
  692. // We need to resolve only once the request is done *AND*
  693. // once we receive the related appInstall message for
  694. // the same manifestURL
  695. let resolve = app => {
  696. this._unlistenAppEvents(listener);
  697. installs = null;
  698. deferred.resolve({ app: app, appId: finalAppId });
  699. };
  700. // Listen for appInstall event, in order to resolve with
  701. // the matching app object.
  702. let listener = (type, app) => {
  703. if (type == "appInstall") {
  704. // Resolves immediately if the request has already resolved
  705. // or just flag the installed app to eventually resolve
  706. // when the request gets its response.
  707. if (app.manifest.manifestURL === manifestURL) {
  708. resolve(app);
  709. } else {
  710. installs[app.manifest.manifestURL] = app;
  711. }
  712. }
  713. };
  714. this._listenAppEvents(listener)
  715. // Execute the request
  716. .then(request)
  717. .then(response => {
  718. finalAppId = response.appId;
  719. manifestURL = response.manifestURL;
  720. // Resolves immediately if the appInstall event
  721. // was dispatched during the request.
  722. if (manifestURL in installs) {
  723. resolve(installs[manifestURL]);
  724. }
  725. }, deferred.reject);
  726. return deferred.promise;
  727. },
  728. /*
  729. * Install a hosted app.
  730. *
  731. * Events are going to be emitted on the front
  732. * as install progresses. Events will have the following fields:
  733. * * bytesSent: The number of bytes sent so far
  734. * * totalBytes: The total number of bytes to send
  735. */
  736. installHosted: function (appId, metadata, manifest) {
  737. let manifestURL = metadata.manifestURL ||
  738. metadata.origin + "/manifest.webapp";
  739. let request = () => {
  740. return installHosted(this.client, this.actor, appId, metadata,
  741. manifest)
  742. .then(response => ({
  743. appId: response.appId,
  744. manifestURL: manifestURL
  745. }));
  746. };
  747. return this._install(request);
  748. }
  749. };
  750. exports.AppActorFront = AppActorFront;