123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841 |
- /* 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, Cc, Cr} = require("chrome");
- const {OS} = require("resource://gre/modules/osfile.jsm");
- const {FileUtils} = require("resource://gre/modules/FileUtils.jsm");
- const {NetUtil} = require("resource://gre/modules/NetUtil.jsm");
- const promise = require("promise");
- const defer = require("devtools/shared/defer");
- const DevToolsUtils = require("devtools/shared/DevToolsUtils");
- const EventEmitter = require("devtools/shared/event-emitter");
- // Bug 1188401: When loaded from xpcshell tests, we do not have browser/ files
- // and can't load target.js. Should be fixed by bug 912121.
- loader.lazyRequireGetter(this, "TargetFactory", "devtools/client/framework/target", true);
- // XXX: bug 912476 make this module a real protocol.js front
- // by converting webapps actor to protocol.js
- const PR_USEC_PER_MSEC = 1000;
- const PR_RDWR = 0x04;
- const PR_CREATE_FILE = 0x08;
- const PR_TRUNCATE = 0x20;
- const CHUNK_SIZE = 10000;
- const appTargets = new Map();
- function addDirToZip(writer, dir, basePath) {
- let files = dir.directoryEntries;
- while (files.hasMoreElements()) {
- let file = files.getNext().QueryInterface(Ci.nsIFile);
- if (file.isHidden() ||
- file.isSpecial() ||
- file.equals(writer.file))
- {
- continue;
- }
- if (file.isDirectory()) {
- writer.addEntryDirectory(basePath + file.leafName + "/",
- file.lastModifiedTime * PR_USEC_PER_MSEC,
- true);
- addDirToZip(writer, file, basePath + file.leafName + "/");
- } else {
- writer.addEntryFile(basePath + file.leafName,
- Ci.nsIZipWriter.COMPRESSION_DEFAULT,
- file,
- true);
- }
- }
- }
- /**
- * Convert an XPConnect result code to its name and message.
- * We have to extract them from an exception per bug 637307 comment 5.
- */
- function getResultText(code) {
- let regexp =
- /^\[Exception... "(.*)" nsresult: "0x[0-9a-fA-F]* \((.*)\)" location: ".*" data: .*\]$/;
- let ex = Cc["@mozilla.org/js/xpc/Exception;1"].
- createInstance(Ci.nsIXPCException);
- ex.initialize(null, code, null, null, null, null);
- let [, message, name] = regexp.exec(ex.toString());
- return { name: name, message: message };
- }
- function zipDirectory(zipFile, dirToArchive) {
- let deferred = defer();
- let writer = Cc["@mozilla.org/zipwriter;1"].createInstance(Ci.nsIZipWriter);
- writer.open(zipFile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE);
- this.addDirToZip(writer, dirToArchive, "");
- writer.processQueue({
- onStartRequest: function onStartRequest(request, context) {},
- onStopRequest: (request, context, status) => {
- if (status == Cr.NS_OK) {
- writer.close();
- deferred.resolve(zipFile);
- }
- else {
- let { name, message } = getResultText(status);
- deferred.reject(name + ": " + message);
- }
- }
- }, null);
- return deferred.promise;
- }
- function uploadPackage(client, webappsActor, packageFile, progressCallback) {
- if (client.traits.bulk) {
- return uploadPackageBulk(client, webappsActor, packageFile, progressCallback);
- } else {
- return uploadPackageJSON(client, webappsActor, packageFile, progressCallback);
- }
- }
- function uploadPackageJSON(client, webappsActor, packageFile, progressCallback) {
- let deferred = defer();
- let request = {
- to: webappsActor,
- type: "uploadPackage"
- };
- client.request(request, (res) => {
- openFile(res.actor);
- });
- let fileSize;
- let bytesRead = 0;
- function emitProgress() {
- progressCallback({
- bytesSent: bytesRead,
- totalBytes: fileSize
- });
- }
- function openFile(actor) {
- let openedFile;
- OS.File.open(packageFile.path)
- .then(file => {
- openedFile = file;
- return openedFile.stat();
- })
- .then(fileInfo => {
- fileSize = fileInfo.size;
- emitProgress();
- uploadChunk(actor, openedFile);
- });
- }
- function uploadChunk(actor, file) {
- file.read(CHUNK_SIZE)
- .then(function (bytes) {
- bytesRead += bytes.length;
- emitProgress();
- // To work around the fact that JSON.stringify translates the typed
- // array to object, we are encoding the typed array here into a string
- let chunk = String.fromCharCode.apply(null, bytes);
- let request = {
- to: actor,
- type: "chunk",
- chunk: chunk
- };
- client.request(request, (res) => {
- if (bytes.length == CHUNK_SIZE) {
- uploadChunk(actor, file);
- } else {
- file.close().then(function () {
- endsUpload(actor);
- });
- }
- });
- });
- }
- function endsUpload(actor) {
- let request = {
- to: actor,
- type: "done"
- };
- client.request(request, (res) => {
- deferred.resolve(actor);
- });
- }
- return deferred.promise;
- }
- function uploadPackageBulk(client, webappsActor, packageFile, progressCallback) {
- let deferred = defer();
- let request = {
- to: webappsActor,
- type: "uploadPackage",
- bulk: true
- };
- client.request(request, (res) => {
- startBulkUpload(res.actor);
- });
- function startBulkUpload(actor) {
- console.log("Starting bulk upload");
- let fileSize = packageFile.fileSize;
- console.log("File size: " + fileSize);
- let request = client.startBulkRequest({
- actor: actor,
- type: "stream",
- length: fileSize
- });
- request.on("bulk-send-ready", ({copyFrom}) => {
- NetUtil.asyncFetch({
- uri: NetUtil.newURI(packageFile),
- loadUsingSystemPrincipal: true
- }, function (inputStream) {
- let copying = copyFrom(inputStream);
- copying.on("progress", (e, progress) => {
- progressCallback(progress);
- });
- copying.then(() => {
- console.log("Bulk upload done");
- inputStream.close();
- deferred.resolve(actor);
- });
- });
- });
- }
- return deferred.promise;
- }
- function removeServerTemporaryFile(client, fileActor) {
- let request = {
- to: fileActor,
- type: "remove"
- };
- client.request(request);
- }
- /**
- * progressCallback argument:
- * Function called as packaged app installation proceeds.
- * The progress object passed to this function contains:
- * * bytesSent: The number of bytes sent so far
- * * totalBytes: The total number of bytes to send
- */
- function installPackaged(client, webappsActor, packagePath, appId, progressCallback) {
- let deferred = defer();
- let file = FileUtils.File(packagePath);
- let packagePromise;
- if (file.isDirectory()) {
- let tmpZipFile = FileUtils.getDir("TmpD", [], true);
- tmpZipFile.append("application.zip");
- tmpZipFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
- packagePromise = zipDirectory(tmpZipFile, file);
- } else {
- packagePromise = promise.resolve(file);
- }
- packagePromise.then((zipFile) => {
- uploadPackage(client, webappsActor, zipFile, progressCallback)
- .then((fileActor) => {
- let request = {
- to: webappsActor,
- type: "install",
- appId: appId,
- upload: fileActor
- };
- client.request(request, (res) => {
- // If the install method immediatly fails,
- // reject immediatly the installPackaged promise.
- // Otherwise, wait for webappsEvent for completion
- if (res.error) {
- deferred.reject(res);
- }
- if ("error" in res)
- deferred.reject({error: res.error, message: res.message});
- else
- deferred.resolve({appId: res.appId});
- });
- // Ensure deleting the temporary package file, but only if that a temporary
- // package created when we pass a directory as `packagePath`
- if (zipFile != file)
- zipFile.remove(false);
- // In case of success or error, ensure deleting the temporary package file
- // also created on the device, but only once install request is done
- deferred.promise.then(
- () => removeServerTemporaryFile(client, fileActor),
- () => removeServerTemporaryFile(client, fileActor));
- });
- });
- return deferred.promise;
- }
- exports.installPackaged = installPackaged;
- function installHosted(client, webappsActor, appId, metadata, manifest) {
- let deferred = defer();
- let request = {
- to: webappsActor,
- type: "install",
- appId: appId,
- metadata: metadata,
- manifest: manifest
- };
- client.request(request, (res) => {
- if (res.error) {
- deferred.reject(res);
- }
- if ("error" in res)
- deferred.reject({error: res.error, message: res.message});
- else
- deferred.resolve({appId: res.appId});
- });
- return deferred.promise;
- }
- exports.installHosted = installHosted;
- function getTargetForApp(client, webappsActor, manifestURL) {
- // Ensure always returning the exact same JS object for a target
- // of the same app in order to show only one toolbox per app and
- // avoid re-creating lot of objects twice.
- let existingTarget = appTargets.get(manifestURL);
- if (existingTarget)
- return promise.resolve(existingTarget);
- let deferred = defer();
- let request = {
- to: webappsActor,
- type: "getAppActor",
- manifestURL: manifestURL,
- };
- client.request(request, (res) => {
- if (res.error) {
- deferred.reject(res.error);
- } else {
- let options = {
- form: res.actor,
- client: client,
- chrome: false
- };
- TargetFactory.forRemoteTab(options).then((target) => {
- target.isApp = true;
- appTargets.set(manifestURL, target);
- target.on("close", () => {
- appTargets.delete(manifestURL);
- });
- deferred.resolve(target);
- }, (error) => {
- deferred.reject(error);
- });
- }
- });
- return deferred.promise;
- }
- exports.getTargetForApp = getTargetForApp;
- function reloadApp(client, webappsActor, manifestURL) {
- return getTargetForApp(client,
- webappsActor,
- manifestURL).
- then((target) => {
- // Request the ContentActor to reload the app
- let request = {
- to: target.form.actor,
- type: "reload",
- options: {
- force: true
- },
- manifestURL: manifestURL
- };
- return client.request(request);
- }, () => {
- throw new Error("Not running");
- });
- }
- exports.reloadApp = reloadApp;
- function launchApp(client, webappsActor, manifestURL) {
- return client.request({
- to: webappsActor,
- type: "launch",
- manifestURL: manifestURL
- });
- }
- exports.launchApp = launchApp;
- function closeApp(client, webappsActor, manifestURL) {
- return client.request({
- to: webappsActor,
- type: "close",
- manifestURL: manifestURL
- });
- }
- exports.closeApp = closeApp;
- function getTarget(client, form) {
- let deferred = defer();
- let options = {
- form: form,
- client: client,
- chrome: false
- };
- TargetFactory.forRemoteTab(options).then((target) => {
- target.isApp = true;
- deferred.resolve(target);
- }, (error) => {
- deferred.reject(error);
- });
- return deferred.promise;
- }
- /**
- * `App` instances are client helpers to manage a given app
- * and its the tab actors
- */
- function App(client, webappsActor, manifest) {
- this.client = client;
- this.webappsActor = webappsActor;
- this.manifest = manifest;
- // This attribute is managed by the AppActorFront
- this.running = false;
- this.iconURL = null;
- }
- App.prototype = {
- getForm: function () {
- if (this._form) {
- return promise.resolve(this._form);
- }
- let request = {
- to: this.webappsActor,
- type: "getAppActor",
- manifestURL: this.manifest.manifestURL
- };
- return this.client.request(request)
- .then(res => {
- return this._form = res.actor;
- });
- },
- getTarget: function () {
- if (this._target) {
- return promise.resolve(this._target);
- }
- return this.getForm().
- then((form) => getTarget(this.client, form)).
- then((target) => {
- target.on("close", () => {
- delete this._form;
- delete this._target;
- });
- return this._target = target;
- });
- },
- launch: function () {
- return launchApp(this.client, this.webappsActor,
- this.manifest.manifestURL);
- },
- reload: function () {
- return reloadApp(this.client, this.webappsActor,
- this.manifest.manifestURL);
- },
- close: function () {
- return closeApp(this.client, this.webappsActor,
- this.manifest.manifestURL);
- },
- getIcon: function () {
- if (this.iconURL) {
- return promise.resolve(this.iconURL);
- }
- let deferred = defer();
- let request = {
- to: this.webappsActor,
- type: "getIconAsDataURL",
- manifestURL: this.manifest.manifestURL
- };
- this.client.request(request, res => {
- if (res.error) {
- deferred.reject(res.message || res.error);
- } else if (res.url) {
- this.iconURL = res.url;
- deferred.resolve(res.url);
- } else {
- deferred.reject("Unable to fetch app icon");
- }
- });
- return deferred.promise;
- }
- };
- /**
- * `AppActorFront` is a client for the webapps actor.
- */
- function AppActorFront(client, form) {
- this.client = client;
- this.actor = form.webappsActor;
- this._clientListener = this._clientListener.bind(this);
- this._onInstallProgress = this._onInstallProgress.bind(this);
- this._listeners = [];
- EventEmitter.decorate(this);
- }
- AppActorFront.prototype = {
- /**
- * List `App` instances for all currently running apps.
- */
- get runningApps() {
- if (!this._apps) {
- throw new Error("Can't get running apps before calling watchApps.");
- }
- let r = new Map();
- for (let [manifestURL, app] of this._apps) {
- if (app.running) {
- r.set(manifestURL, app);
- }
- }
- return r;
- },
- /**
- * List `App` instances for all installed apps.
- */
- get apps() {
- if (!this._apps) {
- throw new Error("Can't get apps before calling watchApps.");
- }
- return this._apps;
- },
- /**
- * Returns a `App` object instance for the given manifest URL
- * (and cache it per AppActorFront object)
- */
- _getApp: function (manifestURL) {
- let app = this._apps ? this._apps.get(manifestURL) : null;
- if (app) {
- return promise.resolve(app);
- } else {
- let request = {
- to: this.actor,
- type: "getApp",
- manifestURL: manifestURL
- };
- return this.client.request(request)
- .then(res => {
- let app = new App(this.client, this.actor, res.app);
- if (this._apps) {
- this._apps.set(manifestURL, app);
- }
- return app;
- }, e => {
- console.error("Unable to retrieve app", manifestURL, e);
- });
- }
- },
- /**
- * Starts watching for app opening/closing installing/uninstalling.
- * Needs to be called before using `apps` or `runningApps` attributes.
- */
- watchApps: function (listener) {
- // Fixes race between two references to the same front
- // calling watchApps at the same time
- if (this._loadingPromise) {
- return this._loadingPromise;
- }
- // Only call watchApps for the first listener being register,
- // for all next ones, just send fake appOpen events for already
- // opened apps
- if (this._apps) {
- this.runningApps.forEach((app, manifestURL) => {
- listener("appOpen", app);
- });
- return promise.resolve();
- }
- // First retrieve all installed apps and create
- // related `App` object for each
- let request = {
- to: this.actor,
- type: "getAll"
- };
- return this._loadingPromise = this.client.request(request)
- .then(res => {
- delete this._loadingPromise;
- this._apps = new Map();
- for (let a of res.apps) {
- let app = new App(this.client, this.actor, a);
- this._apps.set(a.manifestURL, app);
- }
- })
- .then(() => {
- // Then retrieve all running apps in order to flag them as running
- let request = {
- to: this.actor,
- type: "listRunningApps"
- };
- return this.client.request(request)
- .then(res => res.apps);
- })
- .then(apps => {
- let promises = apps.map(manifestURL => {
- // _getApp creates `App` instance and register it to AppActorFront
- return this._getApp(manifestURL)
- .then(app => {
- app.running = true;
- // Fake appOpen event for all already opened
- this._notifyListeners("appOpen", app);
- });
- });
- return promise.all(promises);
- })
- .then(() => {
- // Finally ask to receive all app events
- return this._listenAppEvents(listener);
- });
- },
- fetchIcons: function () {
- // On demand, retrieve apps icons in order to be able
- // to synchronously retrieve it on `App` objects
- let promises = [];
- for (let [manifestURL, app] of this._apps) {
- promises.push(app.getIcon());
- }
- return DevToolsUtils.settleAll(promises)
- .then(null, () => {});
- },
- _listenAppEvents: function (listener) {
- this._listeners.push(listener);
- if (this._listeners.length > 1) {
- return promise.resolve();
- }
- let client = this.client;
- let f = this._clientListener;
- client.addListener("appOpen", f);
- client.addListener("appClose", f);
- client.addListener("appInstall", f);
- client.addListener("appUninstall", f);
- let request = {
- to: this.actor,
- type: "watchApps"
- };
- return this.client.request(request);
- },
- _unlistenAppEvents: function (listener) {
- let idx = this._listeners.indexOf(listener);
- if (idx != -1) {
- this._listeners.splice(idx, 1);
- }
- // Until we released all listener, we don't ask to stop sending events
- if (this._listeners.length != 0) {
- return promise.resolve();
- }
- let client = this.client;
- let f = this._clientListener;
- client.removeListener("appOpen", f);
- client.removeListener("appClose", f);
- client.removeListener("appInstall", f);
- client.removeListener("appUninstall", f);
- // Remove `_apps` in order to allow calling watchApps again
- // and repopulate the apps Map.
- delete this._apps;
- let request = {
- to: this.actor,
- type: "unwatchApps"
- };
- return this.client.request(request);
- },
- _clientListener: function (type, message) {
- let { manifestURL } = message;
- // Reset the app object to get a fresh copy when we (re)install the app.
- if (type == "appInstall" && this._apps && this._apps.has(manifestURL)) {
- this._apps.delete(manifestURL);
- }
- this._getApp(manifestURL).then((app) => {
- switch (type) {
- case "appOpen":
- app.running = true;
- this._notifyListeners("appOpen", app);
- break;
- case "appClose":
- app.running = false;
- this._notifyListeners("appClose", app);
- break;
- case "appInstall":
- // The call to _getApp is going to create App object
- // This app may have been running while being installed, so check the list
- // of running apps again to get the right answer.
- let request = {
- to: this.actor,
- type: "listRunningApps"
- };
- this.client.request(request)
- .then(res => {
- if (res.apps.indexOf(manifestURL) !== -1) {
- app.running = true;
- this._notifyListeners("appInstall", app);
- this._notifyListeners("appOpen", app);
- } else {
- this._notifyListeners("appInstall", app);
- }
- });
- break;
- case "appUninstall":
- // Fake a appClose event if we didn't got one before uninstall
- if (app.running) {
- app.running = false;
- this._notifyListeners("appClose", app);
- }
- this._apps.delete(manifestURL);
- this._notifyListeners("appUninstall", app);
- break;
- default:
- return;
- }
- });
- },
- _notifyListeners: function (type, app) {
- this._listeners.forEach(f => {
- f(type, app);
- });
- },
- unwatchApps: function (listener) {
- return this._unlistenAppEvents(listener);
- },
- /*
- * Install a packaged app.
- *
- * Events are going to be emitted on the front
- * as install progresses. Events will have the following fields:
- * * bytesSent: The number of bytes sent so far
- * * totalBytes: The total number of bytes to send
- */
- installPackaged: function (packagePath, appId) {
- let request = () => {
- return installPackaged(this.client, this.actor, packagePath, appId,
- this._onInstallProgress)
- .then(response => ({
- appId: response.appId,
- manifestURL: "app://" + response.appId + "/manifest.webapp"
- }));
- };
- return this._install(request);
- },
- _onInstallProgress: function (progress) {
- this.emit("install-progress", progress);
- },
- _install: function (request) {
- let deferred = defer();
- let finalAppId = null, manifestURL = null;
- let installs = {};
- // We need to resolve only once the request is done *AND*
- // once we receive the related appInstall message for
- // the same manifestURL
- let resolve = app => {
- this._unlistenAppEvents(listener);
- installs = null;
- deferred.resolve({ app: app, appId: finalAppId });
- };
- // Listen for appInstall event, in order to resolve with
- // the matching app object.
- let listener = (type, app) => {
- if (type == "appInstall") {
- // Resolves immediately if the request has already resolved
- // or just flag the installed app to eventually resolve
- // when the request gets its response.
- if (app.manifest.manifestURL === manifestURL) {
- resolve(app);
- } else {
- installs[app.manifest.manifestURL] = app;
- }
- }
- };
- this._listenAppEvents(listener)
- // Execute the request
- .then(request)
- .then(response => {
- finalAppId = response.appId;
- manifestURL = response.manifestURL;
- // Resolves immediately if the appInstall event
- // was dispatched during the request.
- if (manifestURL in installs) {
- resolve(installs[manifestURL]);
- }
- }, deferred.reject);
- return deferred.promise;
- },
- /*
- * Install a hosted app.
- *
- * Events are going to be emitted on the front
- * as install progresses. Events will have the following fields:
- * * bytesSent: The number of bytes sent so far
- * * totalBytes: The total number of bytes to send
- */
- installHosted: function (appId, metadata, manifest) {
- let manifestURL = metadata.manifestURL ||
- metadata.origin + "/manifest.webapp";
- let request = () => {
- return installHosted(this.client, this.actor, appId, metadata,
- manifest)
- .then(response => ({
- appId: response.appId,
- manifestURL: manifestURL
- }));
- };
- return this._install(request);
- }
- };
- exports.AppActorFront = AppActorFront;
|