mirror of
https://github.com/classilla/tenfourfox.git
synced 2024-10-22 02:25:05 +00:00
852 lines
28 KiB
JavaScript
852 lines
28 KiB
JavaScript
/* 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/. */
|
|
|
|
const {Cu} = require("chrome");
|
|
|
|
const promise = require("promise");
|
|
const {TargetFactory} = require("devtools/client/framework/target");
|
|
const Services = require("Services");
|
|
const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
|
|
const EventEmitter = require("devtools/shared/event-emitter");
|
|
const {TextEncoder, OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
|
|
const {AppProjects} = require("devtools/client/webide/modules/app-projects");
|
|
const TabStore = require("devtools/client/webide/modules/tab-store");
|
|
const {AppValidator} = require("devtools/client/webide/modules/app-validator");
|
|
const {ConnectionManager, Connection} = require("devtools/shared/client/connection-manager");
|
|
const {AppActorFront} = require("devtools/shared/apps/app-actor-front");
|
|
const {getDeviceFront} = require("devtools/server/actors/device");
|
|
const {getPreferenceFront} = require("devtools/server/actors/preference");
|
|
const {getSettingsFront} = require("devtools/server/actors/settings");
|
|
const {setTimeout} = require("sdk/timers");
|
|
const {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
|
|
const {RuntimeScanners, RuntimeTypes} = require("devtools/client/webide/modules/runtimes");
|
|
const {NetUtil} = Cu.import("resource://gre/modules/NetUtil.jsm", {});
|
|
const Telemetry = require("devtools/client/shared/telemetry");
|
|
const {ProjectBuilding} = require("./build");
|
|
|
|
const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
|
|
|
|
var AppManager = exports.AppManager = {
|
|
|
|
DEFAULT_PROJECT_ICON: "chrome://webide/skin/default-app-icon.png",
|
|
DEFAULT_PROJECT_NAME: "--",
|
|
|
|
_initialized: false,
|
|
|
|
init: function() {
|
|
if (this._initialized) {
|
|
return;
|
|
}
|
|
this._initialized = true;
|
|
|
|
let port = Services.prefs.getIntPref("devtools.debugger.remote-port");
|
|
this.connection = ConnectionManager.createConnection("localhost", port);
|
|
this.onConnectionChanged = this.onConnectionChanged.bind(this);
|
|
this.connection.on(Connection.Events.STATUS_CHANGED, this.onConnectionChanged);
|
|
|
|
this.tabStore = new TabStore(this.connection);
|
|
this.onTabList = this.onTabList.bind(this);
|
|
this.onTabNavigate = this.onTabNavigate.bind(this);
|
|
this.onTabClosed = this.onTabClosed.bind(this);
|
|
this.tabStore.on("tab-list", this.onTabList);
|
|
this.tabStore.on("navigate", this.onTabNavigate);
|
|
this.tabStore.on("closed", this.onTabClosed);
|
|
|
|
this._clearRuntimeList();
|
|
this._rebuildRuntimeList = this._rebuildRuntimeList.bind(this);
|
|
RuntimeScanners.on("runtime-list-updated", this._rebuildRuntimeList);
|
|
RuntimeScanners.enable();
|
|
this._rebuildRuntimeList();
|
|
|
|
this.onInstallProgress = this.onInstallProgress.bind(this);
|
|
|
|
this._telemetry = new Telemetry();
|
|
},
|
|
|
|
destroy: function() {
|
|
if (!this._initialized) {
|
|
return;
|
|
}
|
|
this._initialized = false;
|
|
|
|
this.selectedProject = null;
|
|
this.selectedRuntime = null;
|
|
RuntimeScanners.off("runtime-list-updated", this._rebuildRuntimeList);
|
|
RuntimeScanners.disable();
|
|
this.runtimeList = null;
|
|
this.tabStore.off("tab-list", this.onTabList);
|
|
this.tabStore.off("navigate", this.onTabNavigate);
|
|
this.tabStore.off("closed", this.onTabClosed);
|
|
this.tabStore.destroy();
|
|
this.tabStore = null;
|
|
this.connection.off(Connection.Events.STATUS_CHANGED, this.onConnectionChanged);
|
|
this._listTabsResponse = null;
|
|
this.connection.disconnect();
|
|
this.connection = null;
|
|
},
|
|
|
|
/**
|
|
* This module emits various events when state changes occur. The basic event
|
|
* naming scheme is that event "X" means "X has changed" or "X is available".
|
|
* Some names are more detailed to clarify their precise meaning.
|
|
*
|
|
* The events this module may emit include:
|
|
* before-project:
|
|
* The selected project is about to change. The event includes a special
|
|
* |cancel| callback that will abort the project change if desired.
|
|
* connection:
|
|
* The connection status has changed (connected, disconnected, etc.)
|
|
* install-progress:
|
|
* A project being installed to a runtime has made further progress. This
|
|
* event contains additional details about exactly how far the process is
|
|
* when such information is available.
|
|
* project:
|
|
* The selected project has changed.
|
|
* project-started:
|
|
* The selected project started running on the connected runtime.
|
|
* project-stopped:
|
|
* The selected project stopped running on the connected runtime.
|
|
* project-removed:
|
|
* The selected project was removed from the project list.
|
|
* project-validated:
|
|
* The selected project just completed validation. As part of validation,
|
|
* many pieces of metadata about the project are refreshed, including its
|
|
* name, manifest details, etc.
|
|
* runtime:
|
|
* The selected runtime has changed.
|
|
* runtime-apps-icons:
|
|
* The list of URLs for the runtime app icons are available.
|
|
* runtime-global-actors:
|
|
* The list of global actors for the entire runtime (but not actors for a
|
|
* specific tab or app) are now available, so we can test for features
|
|
* like preferences and settings.
|
|
* runtime-details:
|
|
* The selected runtime's details have changed, such as its user-visible
|
|
* name.
|
|
* runtime-list:
|
|
* The list of available runtimes has changed, or any of the user-visible
|
|
* details (like names) for the non-selected runtimes has changed.
|
|
* runtime-telemetry:
|
|
* Detailed runtime telemetry has been recorded. Used by tests.
|
|
* runtime-targets:
|
|
* The list of remote runtime targets available from the currently
|
|
* connected runtime (such as tabs or apps) has changed, or any of the
|
|
* user-visible details (like names) for the non-selected runtime targets
|
|
* has changed. This event includes |type| in the details, to distinguish
|
|
* "apps" and "tabs".
|
|
*/
|
|
update: function(what, details) {
|
|
// Anything we want to forward to the UI
|
|
this.emit("app-manager-update", what, details);
|
|
},
|
|
|
|
reportError: function(l10nProperty, ...l10nArgs) {
|
|
let win = Services.wm.getMostRecentWindow("devtools:webide");
|
|
if (win) {
|
|
win.UI.reportError(l10nProperty, ...l10nArgs);
|
|
} else {
|
|
let text;
|
|
if (l10nArgs.length > 0) {
|
|
text = Strings.formatStringFromName(l10nProperty, l10nArgs, l10nArgs.length);
|
|
} else {
|
|
text = Strings.GetStringFromName(l10nProperty);
|
|
}
|
|
console.error(text);
|
|
}
|
|
},
|
|
|
|
onConnectionChanged: function() {
|
|
console.log("Connection status changed: " + this.connection.status);
|
|
|
|
if (this.connection.status == Connection.Status.DISCONNECTED) {
|
|
this.selectedRuntime = null;
|
|
}
|
|
|
|
if (!this.connected) {
|
|
if (this._appsFront) {
|
|
this._appsFront.off("install-progress", this.onInstallProgress);
|
|
this._appsFront.unwatchApps();
|
|
this._appsFront = null;
|
|
}
|
|
this._listTabsResponse = null;
|
|
} else {
|
|
this.connection.client.listTabs((response) => {
|
|
if (response.webappsActor) {
|
|
let front = new AppActorFront(this.connection.client,
|
|
response);
|
|
front.on("install-progress", this.onInstallProgress);
|
|
front.watchApps(() => this.checkIfProjectIsRunning())
|
|
.then(() => {
|
|
// This can't be done earlier as many operations
|
|
// in the apps actor require watchApps to be called
|
|
// first.
|
|
this._appsFront = front;
|
|
this._listTabsResponse = response;
|
|
this._recordRuntimeInfo();
|
|
this.update("runtime-global-actors");
|
|
})
|
|
.then(() => {
|
|
this.checkIfProjectIsRunning();
|
|
this.update("runtime-targets", { type: "apps" });
|
|
front.fetchIcons().then(() => this.update("runtime-apps-icons"));
|
|
});
|
|
} else {
|
|
this._listTabsResponse = response;
|
|
this._recordRuntimeInfo();
|
|
this.update("runtime-global-actors");
|
|
}
|
|
});
|
|
}
|
|
|
|
this.update("connection");
|
|
},
|
|
|
|
get connected() {
|
|
return this.connection &&
|
|
this.connection.status == Connection.Status.CONNECTED;
|
|
},
|
|
|
|
get apps() {
|
|
if (this._appsFront) {
|
|
return this._appsFront.apps;
|
|
} else {
|
|
return new Map();
|
|
}
|
|
},
|
|
|
|
onInstallProgress: function(event, details) {
|
|
this.update("install-progress", details);
|
|
},
|
|
|
|
isProjectRunning: function() {
|
|
if (this.selectedProject.type == "mainProcess" ||
|
|
this.selectedProject.type == "tab") {
|
|
return true;
|
|
}
|
|
|
|
let app = this._getProjectFront(this.selectedProject);
|
|
return app && app.running;
|
|
},
|
|
|
|
checkIfProjectIsRunning: function() {
|
|
if (this.selectedProject) {
|
|
if (this.isProjectRunning()) {
|
|
this.update("project-started");
|
|
} else {
|
|
this.update("project-stopped");
|
|
}
|
|
}
|
|
},
|
|
|
|
listTabs: function() {
|
|
return this.tabStore.listTabs();
|
|
},
|
|
|
|
onTabList: function() {
|
|
this.update("runtime-targets", { type: "tabs" });
|
|
},
|
|
|
|
// TODO: Merge this into TabProject as part of project-agnostic work
|
|
onTabNavigate: function() {
|
|
this.update("runtime-targets", { type: "tabs" });
|
|
if (this.selectedProject.type !== "tab") {
|
|
return;
|
|
}
|
|
let tab = this.selectedProject.app = this.tabStore.selectedTab;
|
|
let uri = NetUtil.newURI(tab.url);
|
|
// Wanted to use nsIFaviconService here, but it only works for visited
|
|
// tabs, so that's no help for any remote tabs. Maybe some favicon wizard
|
|
// knows how to get high-res favicons easily, or we could offer actor
|
|
// support for this (bug 1061654).
|
|
tab.favicon = uri.prePath + "/favicon.ico";
|
|
tab.name = tab.title || Strings.GetStringFromName("project_tab_loading");
|
|
if (uri.scheme.startsWith("http")) {
|
|
tab.name = uri.host + ": " + tab.name;
|
|
}
|
|
this.selectedProject.location = tab.url;
|
|
this.selectedProject.name = tab.name;
|
|
this.selectedProject.icon = tab.favicon;
|
|
this.update("project-validated");
|
|
},
|
|
|
|
onTabClosed: function() {
|
|
if (this.selectedProject.type !== "tab") {
|
|
return;
|
|
}
|
|
this.selectedProject = null;
|
|
},
|
|
|
|
reloadTab: function() {
|
|
if (this.selectedProject && this.selectedProject.type != "tab") {
|
|
return promise.reject("tried to reload non-tab project");
|
|
}
|
|
return this.getTarget().then(target => {
|
|
target.activeTab.reload();
|
|
}, console.error.bind(console));
|
|
},
|
|
|
|
getTarget: function() {
|
|
if (this.selectedProject.type == "mainProcess") {
|
|
// Fx >=39 exposes a ChromeActor to debug the main process
|
|
if (this.connection.client.mainRoot.traits.allowChromeProcess) {
|
|
return this.connection.client.getProcess()
|
|
.then(aResponse => {
|
|
return TargetFactory.forRemoteTab({
|
|
form: aResponse.form,
|
|
client: this.connection.client,
|
|
chrome: true
|
|
});
|
|
});
|
|
} else {
|
|
// Fx <39 exposes tab actors on the root actor
|
|
return TargetFactory.forRemoteTab({
|
|
form: this._listTabsResponse,
|
|
client: this.connection.client,
|
|
chrome: true,
|
|
isTabActor: false
|
|
});
|
|
}
|
|
}
|
|
|
|
if (this.selectedProject.type == "tab") {
|
|
return this.tabStore.getTargetForTab();
|
|
}
|
|
|
|
let app = this._getProjectFront(this.selectedProject);
|
|
if (!app) {
|
|
return promise.reject("Can't find app front for selected project");
|
|
}
|
|
|
|
return Task.spawn(function* () {
|
|
// Once we asked the app to launch, the app isn't necessary completely loaded.
|
|
// launch request only ask the app to launch and immediatly returns.
|
|
// We have to keep trying to get app tab actors required to create its target.
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
try {
|
|
return yield app.getTarget();
|
|
} catch(e) {}
|
|
let deferred = promise.defer();
|
|
setTimeout(deferred.resolve, 500);
|
|
yield deferred.promise;
|
|
}
|
|
|
|
AppManager.reportError("error_cantConnectToApp", app.manifest.manifestURL);
|
|
throw new Error("can't connect to app");
|
|
});
|
|
},
|
|
|
|
getProjectManifestURL: function(project) {
|
|
let manifest = null;
|
|
if (project.type == "runtimeApp") {
|
|
manifest = project.app.manifestURL;
|
|
}
|
|
|
|
if (project.type == "hosted") {
|
|
manifest = project.location;
|
|
}
|
|
|
|
if (project.type == "packaged" && project.packagedAppOrigin) {
|
|
manifest = "app://" + project.packagedAppOrigin + "/manifest.webapp";
|
|
}
|
|
|
|
return manifest;
|
|
},
|
|
|
|
_getProjectFront: function(project) {
|
|
let manifest = this.getProjectManifestURL(project);
|
|
if (manifest && this._appsFront) {
|
|
return this._appsFront.apps.get(manifest);
|
|
}
|
|
return null;
|
|
},
|
|
|
|
_selectedProject: null,
|
|
set selectedProject(project) {
|
|
// A regular comparison doesn't work as we recreate a new object every time
|
|
let prev = this._selectedProject;
|
|
if (!prev && !project) {
|
|
return;
|
|
} else if (prev && project && prev.type === project.type) {
|
|
let type = project.type;
|
|
if (type === "runtimeApp") {
|
|
if (prev.app.manifestURL === project.app.manifestURL) {
|
|
return;
|
|
}
|
|
} else if (type === "tab") {
|
|
if (prev.app.actor === project.app.actor) {
|
|
return;
|
|
}
|
|
} else if (type === "packaged" || type === "hosted") {
|
|
if (prev.location === project.location) {
|
|
return;
|
|
}
|
|
} else if (type === "mainProcess") {
|
|
return;
|
|
} else {
|
|
throw new Error("Unsupported project type: " + type);
|
|
}
|
|
}
|
|
|
|
let cancelled = false;
|
|
this.update("before-project", { cancel: () => { cancelled = true; } });
|
|
if (cancelled) {
|
|
return;
|
|
}
|
|
|
|
this._selectedProject = project;
|
|
|
|
// Clear out tab store's selected state, if any
|
|
this.tabStore.selectedTab = null;
|
|
|
|
if (project) {
|
|
if (project.type == "packaged" ||
|
|
project.type == "hosted") {
|
|
this.validateAndUpdateProject(project);
|
|
}
|
|
if (project.type == "tab") {
|
|
this.tabStore.selectedTab = project.app;
|
|
}
|
|
}
|
|
|
|
this.update("project");
|
|
this.checkIfProjectIsRunning();
|
|
},
|
|
get selectedProject() {
|
|
return this._selectedProject;
|
|
},
|
|
|
|
removeSelectedProject: Task.async(function*() {
|
|
let location = this.selectedProject.location;
|
|
AppManager.selectedProject = null;
|
|
// If the user cancels the removeProject operation, don't remove the project
|
|
if (AppManager.selectedProject != null) {
|
|
return;
|
|
}
|
|
|
|
yield AppProjects.remove(location);
|
|
AppManager.update("project-removed");
|
|
}),
|
|
|
|
packageProject: Task.async(function*(project) {
|
|
if (!project) {
|
|
return;
|
|
}
|
|
if (project.type == "packaged" ||
|
|
project.type == "hosted") {
|
|
yield ProjectBuilding.build({
|
|
project: project,
|
|
logger: this.update.bind(this, "pre-package")
|
|
});
|
|
}
|
|
}),
|
|
|
|
_selectedRuntime: null,
|
|
set selectedRuntime(value) {
|
|
this._selectedRuntime = value;
|
|
if (!value && this.selectedProject &&
|
|
(this.selectedProject.type == "mainProcess" ||
|
|
this.selectedProject.type == "runtimeApp" ||
|
|
this.selectedProject.type == "tab")) {
|
|
this.selectedProject = null;
|
|
}
|
|
this.update("runtime");
|
|
},
|
|
|
|
get selectedRuntime() {
|
|
return this._selectedRuntime;
|
|
},
|
|
|
|
connectToRuntime: function(runtime) {
|
|
|
|
if (this.connected && this.selectedRuntime === runtime) {
|
|
// Already connected
|
|
return promise.resolve();
|
|
}
|
|
|
|
let deferred = promise.defer();
|
|
|
|
this.disconnectRuntime().then(() => {
|
|
this.selectedRuntime = runtime;
|
|
|
|
let onConnectedOrDisconnected = () => {
|
|
this.connection.off(Connection.Events.CONNECTED, onConnectedOrDisconnected);
|
|
this.connection.off(Connection.Events.DISCONNECTED, onConnectedOrDisconnected);
|
|
if (this.connected) {
|
|
deferred.resolve();
|
|
} else {
|
|
deferred.reject();
|
|
}
|
|
};
|
|
this.connection.on(Connection.Events.CONNECTED, onConnectedOrDisconnected);
|
|
this.connection.on(Connection.Events.DISCONNECTED, onConnectedOrDisconnected);
|
|
try {
|
|
// Reset the connection's state to defaults
|
|
this.connection.resetOptions();
|
|
// Only watch for errors here. Final resolution occurs above, once
|
|
// we've reached the CONNECTED state.
|
|
this.selectedRuntime.connect(this.connection)
|
|
.then(null, e => deferred.reject(e));
|
|
} catch(e) {
|
|
deferred.reject(e);
|
|
}
|
|
}, deferred.reject);
|
|
|
|
// Record connection result in telemetry
|
|
let logResult = result => {
|
|
this._telemetry.log("DEVTOOLS_WEBIDE_CONNECTION_RESULT", result);
|
|
if (runtime.type) {
|
|
this._telemetry.log("DEVTOOLS_WEBIDE_" + runtime.type +
|
|
"_CONNECTION_RESULT", result);
|
|
}
|
|
};
|
|
deferred.promise.then(() => logResult(true), () => logResult(false));
|
|
|
|
// If successful, record connection time in telemetry
|
|
deferred.promise.then(() => {
|
|
const timerId = "DEVTOOLS_WEBIDE_CONNECTION_TIME_SECONDS";
|
|
this._telemetry.startTimer(timerId);
|
|
this.connection.once(Connection.Events.STATUS_CHANGED, () => {
|
|
this._telemetry.stopTimer(timerId);
|
|
});
|
|
}).catch(() => {
|
|
// Empty rejection handler to silence uncaught rejection warnings
|
|
// |connectToRuntime| caller should listen for rejections.
|
|
// Bug 1121100 may find a better way to silence these.
|
|
});
|
|
|
|
return deferred.promise;
|
|
},
|
|
|
|
_recordRuntimeInfo: Task.async(function*() {
|
|
if (!this.connected) {
|
|
return;
|
|
}
|
|
let runtime = this.selectedRuntime;
|
|
this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE",
|
|
runtime.type || "UNKNOWN", true);
|
|
this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID",
|
|
runtime.id || "unknown", true);
|
|
if (!this.deviceFront) {
|
|
this.update("runtime-telemetry");
|
|
return;
|
|
}
|
|
let d = yield this.deviceFront.getDescription();
|
|
this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PROCESSOR",
|
|
d.processor, true);
|
|
this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_OS",
|
|
d.os, true);
|
|
this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PLATFORM_VERSION",
|
|
d.platformversion, true);
|
|
this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_APP_TYPE",
|
|
d.apptype, true);
|
|
this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_VERSION",
|
|
d.version, true);
|
|
this.update("runtime-telemetry");
|
|
}),
|
|
|
|
isMainProcessDebuggable: function() {
|
|
// Fx <39 exposes chrome tab actors on RootActor
|
|
// Fx >=39 exposes a dedicated actor via getProcess request
|
|
return this.connection.client &&
|
|
this.connection.client.mainRoot &&
|
|
this.connection.client.mainRoot.traits.allowChromeProcess ||
|
|
(this._listTabsResponse &&
|
|
this._listTabsResponse.consoleActor);
|
|
},
|
|
|
|
get deviceFront() {
|
|
if (!this._listTabsResponse) {
|
|
return null;
|
|
}
|
|
return getDeviceFront(this.connection.client, this._listTabsResponse);
|
|
},
|
|
|
|
get preferenceFront() {
|
|
if (!this._listTabsResponse) {
|
|
return null;
|
|
}
|
|
return getPreferenceFront(this.connection.client, this._listTabsResponse);
|
|
},
|
|
|
|
get settingsFront() {
|
|
if (!this._listTabsResponse) {
|
|
return null;
|
|
}
|
|
return getSettingsFront(this.connection.client, this._listTabsResponse);
|
|
},
|
|
|
|
disconnectRuntime: function() {
|
|
if (!this.connected) {
|
|
return promise.resolve();
|
|
}
|
|
let deferred = promise.defer();
|
|
this.connection.once(Connection.Events.DISCONNECTED, () => deferred.resolve());
|
|
this.connection.disconnect();
|
|
return deferred.promise;
|
|
},
|
|
|
|
launchRuntimeApp: function() {
|
|
if (this.selectedProject && this.selectedProject.type != "runtimeApp") {
|
|
return promise.reject("attempting to launch a non-runtime app");
|
|
}
|
|
let app = this._getProjectFront(this.selectedProject);
|
|
return app.launch();
|
|
},
|
|
|
|
launchOrReloadRuntimeApp: function() {
|
|
if (this.selectedProject && this.selectedProject.type != "runtimeApp") {
|
|
return promise.reject("attempting to launch / reload a non-runtime app");
|
|
}
|
|
let app = this._getProjectFront(this.selectedProject);
|
|
if (!app.running) {
|
|
return app.launch();
|
|
} else {
|
|
return app.reload();
|
|
}
|
|
},
|
|
|
|
runtimeCanHandleApps: function() {
|
|
return !!this._appsFront;
|
|
},
|
|
|
|
installAndRunProject: function() {
|
|
let project = this.selectedProject;
|
|
|
|
if (!project || (project.type != "packaged" && project.type != "hosted")) {
|
|
console.error("Can't install project. Unknown type of project.");
|
|
return promise.reject("Can't install");
|
|
}
|
|
|
|
if (!this._listTabsResponse) {
|
|
this.reportError("error_cantInstallNotFullyConnected");
|
|
return promise.reject("Can't install");
|
|
}
|
|
|
|
if (!this._appsFront) {
|
|
console.error("Runtime doesn't have a webappsActor");
|
|
return promise.reject("Can't install");
|
|
}
|
|
|
|
return Task.spawn(function* () {
|
|
let self = AppManager;
|
|
|
|
// Package and validate project
|
|
yield self.packageProject(project);
|
|
yield self.validateAndUpdateProject(project);
|
|
|
|
if (project.errorsCount > 0) {
|
|
self.reportError("error_cantInstallValidationErrors");
|
|
return;
|
|
}
|
|
|
|
let installPromise;
|
|
|
|
if (project.type != "packaged" && project.type != "hosted") {
|
|
return promise.reject("Don't know how to install project");
|
|
}
|
|
|
|
let response;
|
|
if (project.type == "packaged") {
|
|
let packageDir = yield ProjectBuilding.getPackageDir(project);
|
|
console.log("Installing app from " + packageDir);
|
|
|
|
response = yield self._appsFront.installPackaged(packageDir,
|
|
project.packagedAppOrigin);
|
|
|
|
// If the packaged app specified a custom origin override,
|
|
// we need to update the local project origin
|
|
project.packagedAppOrigin = response.appId;
|
|
// And ensure the indexed db on disk is also updated
|
|
AppProjects.update(project);
|
|
}
|
|
|
|
if (project.type == "hosted") {
|
|
let manifestURLObject = Services.io.newURI(project.location, null, null);
|
|
let origin = Services.io.newURI(manifestURLObject.prePath, null, null);
|
|
let appId = origin.host;
|
|
let metadata = {
|
|
origin: origin.spec,
|
|
manifestURL: project.location
|
|
};
|
|
response = yield self._appsFront.installHosted(appId,
|
|
metadata,
|
|
project.manifest);
|
|
}
|
|
|
|
// Addons don't have any document to load (yet?)
|
|
// So that there is no need to run them, installing is enough
|
|
if (project.manifest.manifest_version || project.manifest.role === "addon") {
|
|
return;
|
|
}
|
|
|
|
let {app} = response;
|
|
if (!app.running) {
|
|
let deferred = promise.defer();
|
|
self.on("app-manager-update", function onUpdate(event, what) {
|
|
if (what == "project-started") {
|
|
self.off("app-manager-update", onUpdate);
|
|
deferred.resolve();
|
|
}
|
|
});
|
|
yield app.launch();
|
|
yield deferred.promise;
|
|
} else {
|
|
yield app.reload();
|
|
}
|
|
});
|
|
},
|
|
|
|
stopRunningApp: function() {
|
|
let app = this._getProjectFront(this.selectedProject);
|
|
return app.close();
|
|
},
|
|
|
|
/* PROJECT VALIDATION */
|
|
|
|
validateAndUpdateProject: function(project) {
|
|
if (!project) {
|
|
return promise.reject();
|
|
}
|
|
|
|
return Task.spawn(function* () {
|
|
|
|
let packageDir = yield ProjectBuilding.getPackageDir(project);
|
|
let validation = new AppValidator({
|
|
type: project.type,
|
|
// Build process may place the manifest in a non-root directory
|
|
location: packageDir
|
|
});
|
|
|
|
yield validation.validate();
|
|
|
|
if (validation.manifest) {
|
|
let manifest = validation.manifest;
|
|
let iconPath;
|
|
if (manifest.icons) {
|
|
let size = Object.keys(manifest.icons).sort((a, b) => b - a)[0];
|
|
if (size) {
|
|
iconPath = manifest.icons[size];
|
|
}
|
|
}
|
|
if (!iconPath) {
|
|
project.icon = AppManager.DEFAULT_PROJECT_ICON;
|
|
} else {
|
|
if (project.type == "hosted") {
|
|
let manifestURL = Services.io.newURI(project.location, null, null);
|
|
let origin = Services.io.newURI(manifestURL.prePath, null, null);
|
|
project.icon = Services.io.newURI(iconPath, null, origin).spec;
|
|
} else if (project.type == "packaged") {
|
|
let projectFolder = FileUtils.File(packageDir);
|
|
let folderURI = Services.io.newFileURI(projectFolder).spec;
|
|
project.icon = folderURI + iconPath.replace(/^\/|\\/, "");
|
|
}
|
|
}
|
|
project.manifest = validation.manifest;
|
|
|
|
if ("name" in project.manifest) {
|
|
project.name = project.manifest.name;
|
|
} else {
|
|
project.name = AppManager.DEFAULT_PROJECT_NAME;
|
|
}
|
|
} else {
|
|
project.manifest = null;
|
|
project.icon = AppManager.DEFAULT_PROJECT_ICON;
|
|
project.name = AppManager.DEFAULT_PROJECT_NAME;
|
|
}
|
|
|
|
project.validationStatus = "valid";
|
|
|
|
if (validation.warnings.length > 0) {
|
|
project.warningsCount = validation.warnings.length;
|
|
project.warnings = validation.warnings;
|
|
project.validationStatus = "warning";
|
|
} else {
|
|
project.warnings = "";
|
|
project.warningsCount = 0;
|
|
}
|
|
|
|
if (validation.errors.length > 0) {
|
|
project.errorsCount = validation.errors.length;
|
|
project.errors = validation.errors;
|
|
project.validationStatus = "error";
|
|
} else {
|
|
project.errors = "";
|
|
project.errorsCount = 0;
|
|
}
|
|
|
|
if (project.warningsCount && project.errorsCount) {
|
|
project.validationStatus = "error warning";
|
|
}
|
|
|
|
if (project.type === "hosted" && project.location !== validation.manifestURL) {
|
|
yield AppProjects.updateLocation(project, validation.manifestURL);
|
|
} else if (AppProjects.get(project.location)) {
|
|
yield AppProjects.update(project);
|
|
}
|
|
|
|
if (AppManager.selectedProject === project) {
|
|
AppManager.update("project-validated");
|
|
}
|
|
});
|
|
},
|
|
|
|
/* RUNTIME LIST */
|
|
|
|
_clearRuntimeList: function() {
|
|
this.runtimeList = {
|
|
usb: [],
|
|
wifi: [],
|
|
simulator: [],
|
|
other: []
|
|
};
|
|
},
|
|
|
|
_rebuildRuntimeList: function() {
|
|
let runtimes = RuntimeScanners.listRuntimes();
|
|
this._clearRuntimeList();
|
|
|
|
// Reorganize runtimes by type
|
|
for (let runtime of runtimes) {
|
|
switch (runtime.type) {
|
|
case RuntimeTypes.USB:
|
|
this.runtimeList.usb.push(runtime);
|
|
break;
|
|
case RuntimeTypes.WIFI:
|
|
this.runtimeList.wifi.push(runtime);
|
|
break;
|
|
case RuntimeTypes.SIMULATOR:
|
|
this.runtimeList.simulator.push(runtime);
|
|
break;
|
|
default:
|
|
this.runtimeList.other.push(runtime);
|
|
}
|
|
}
|
|
|
|
this.update("runtime-details");
|
|
this.update("runtime-list");
|
|
},
|
|
|
|
/* MANIFEST UTILS */
|
|
|
|
writeManifest: function(project) {
|
|
if (project.type != "packaged") {
|
|
return promise.reject("Not a packaged app");
|
|
}
|
|
|
|
if (!project.manifest) {
|
|
project.manifest = {};
|
|
}
|
|
|
|
let folder = project.location;
|
|
let manifestPath = OS.Path.join(folder, "manifest.webapp");
|
|
let text = JSON.stringify(project.manifest, null, 2);
|
|
let encoder = new TextEncoder();
|
|
let array = encoder.encode(text);
|
|
return OS.File.writeAtomic(manifestPath, array, {tmpPath: manifestPath + ".tmp"});
|
|
},
|
|
};
|
|
|
|
EventEmitter.decorate(AppManager);
|