mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-03 22:38:41 +09:00
Revert "Remove WebIDE devtools component."
This reverts commit 8c1500707f.
# Conflicts:
# application/basilisk/components/customizableui/CustomizableUI.jsm
This commit is contained in:
parent
4ea7de218b
commit
df89b48e30
163 changed files with 13812 additions and 0 deletions
197
devtools/client/webide/modules/addons.js
Normal file
197
devtools/client/webide/modules/addons.js
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
/* 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 promise = require("promise");
|
||||
const {AddonManager} = require("resource://gre/modules/AddonManager.jsm");
|
||||
const Services = require("Services");
|
||||
const {getJSON} = require("devtools/client/shared/getjson");
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
|
||||
const ADDONS_URL = "devtools.webide.addonsURL";
|
||||
|
||||
var SIMULATOR_LINK = Services.prefs.getCharPref("devtools.webide.simulatorAddonsURL");
|
||||
var ADB_LINK = Services.prefs.getCharPref("devtools.webide.adbAddonURL");
|
||||
var ADAPTERS_LINK = Services.prefs.getCharPref("devtools.webide.adaptersAddonURL");
|
||||
var SIMULATOR_ADDON_ID = Services.prefs.getCharPref("devtools.webide.simulatorAddonID");
|
||||
var ADB_ADDON_ID = Services.prefs.getCharPref("devtools.webide.adbAddonID");
|
||||
var ADAPTERS_ADDON_ID = Services.prefs.getCharPref("devtools.webide.adaptersAddonID");
|
||||
|
||||
var platform = Services.appShell.hiddenDOMWindow.navigator.platform;
|
||||
var OS = "";
|
||||
if (platform.indexOf("Win") != -1) {
|
||||
OS = "win32";
|
||||
} else if (platform.indexOf("Mac") != -1) {
|
||||
OS = "mac64";
|
||||
} else if (platform.indexOf("Linux") != -1) {
|
||||
if (platform.indexOf("x86_64") != -1) {
|
||||
OS = "linux64";
|
||||
} else {
|
||||
OS = "linux32";
|
||||
}
|
||||
}
|
||||
|
||||
var addonsListener = {};
|
||||
addonsListener.onEnabled =
|
||||
addonsListener.onDisabled =
|
||||
addonsListener.onInstalled =
|
||||
addonsListener.onUninstalled = (updatedAddon) => {
|
||||
GetAvailableAddons().then(addons => {
|
||||
for (let a of [...addons.simulators, addons.adb, addons.adapters]) {
|
||||
if (a.addonID == updatedAddon.id) {
|
||||
a.updateInstallStatus();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
AddonManager.addAddonListener(addonsListener);
|
||||
|
||||
var GetAvailableAddons_promise = null;
|
||||
var GetAvailableAddons = exports.GetAvailableAddons = function () {
|
||||
if (!GetAvailableAddons_promise) {
|
||||
let deferred = promise.defer();
|
||||
GetAvailableAddons_promise = deferred.promise;
|
||||
let addons = {
|
||||
simulators: [],
|
||||
adb: null
|
||||
};
|
||||
getJSON(ADDONS_URL).then(json => {
|
||||
for (let stability in json) {
|
||||
for (let version of json[stability]) {
|
||||
addons.simulators.push(new SimulatorAddon(stability, version));
|
||||
}
|
||||
}
|
||||
addons.adb = new ADBAddon();
|
||||
addons.adapters = new AdaptersAddon();
|
||||
deferred.resolve(addons);
|
||||
}, e => {
|
||||
GetAvailableAddons_promise = null;
|
||||
deferred.reject(e);
|
||||
});
|
||||
}
|
||||
return GetAvailableAddons_promise;
|
||||
};
|
||||
|
||||
exports.ForgetAddonsList = function () {
|
||||
GetAvailableAddons_promise = null;
|
||||
};
|
||||
|
||||
function Addon() {}
|
||||
Addon.prototype = {
|
||||
_status: "unknown",
|
||||
set status(value) {
|
||||
if (this._status != value) {
|
||||
this._status = value;
|
||||
this.emit("update");
|
||||
}
|
||||
},
|
||||
get status() {
|
||||
return this._status;
|
||||
},
|
||||
|
||||
updateInstallStatus: function () {
|
||||
AddonManager.getAddonByID(this.addonID, (addon) => {
|
||||
if (addon && !addon.userDisabled) {
|
||||
this.status = "installed";
|
||||
} else {
|
||||
this.status = "uninstalled";
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
install: function () {
|
||||
AddonManager.getAddonByID(this.addonID, (addon) => {
|
||||
if (addon && !addon.userDisabled) {
|
||||
this.status = "installed";
|
||||
return;
|
||||
}
|
||||
this.status = "preparing";
|
||||
if (addon && addon.userDisabled) {
|
||||
addon.userDisabled = false;
|
||||
} else {
|
||||
AddonManager.getInstallForURL(this.xpiLink, (install) => {
|
||||
install.addListener(this);
|
||||
install.install();
|
||||
}, "application/x-xpinstall");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
uninstall: function () {
|
||||
AddonManager.getAddonByID(this.addonID, (addon) => {
|
||||
addon.uninstall();
|
||||
});
|
||||
},
|
||||
|
||||
installFailureHandler: function (install, message) {
|
||||
this.status = "uninstalled";
|
||||
this.emit("failure", message);
|
||||
},
|
||||
|
||||
onDownloadStarted: function () {
|
||||
this.status = "downloading";
|
||||
},
|
||||
|
||||
onInstallStarted: function () {
|
||||
this.status = "installing";
|
||||
},
|
||||
|
||||
onDownloadProgress: function (install) {
|
||||
if (install.maxProgress == -1) {
|
||||
this.emit("progress", -1);
|
||||
} else {
|
||||
this.emit("progress", install.progress / install.maxProgress);
|
||||
}
|
||||
},
|
||||
|
||||
onInstallEnded: function ({addon}) {
|
||||
addon.userDisabled = false;
|
||||
},
|
||||
|
||||
onDownloadCancelled: function (install) {
|
||||
this.installFailureHandler(install, "Download cancelled");
|
||||
},
|
||||
onDownloadFailed: function (install) {
|
||||
this.installFailureHandler(install, "Download failed");
|
||||
},
|
||||
onInstallCancelled: function (install) {
|
||||
this.installFailureHandler(install, "Install cancelled");
|
||||
},
|
||||
onInstallFailed: function (install) {
|
||||
this.installFailureHandler(install, "Install failed");
|
||||
},
|
||||
};
|
||||
|
||||
function SimulatorAddon(stability, version) {
|
||||
EventEmitter.decorate(this);
|
||||
this.stability = stability;
|
||||
this.version = version;
|
||||
// This addon uses the string "linux" for "linux32"
|
||||
let fixedOS = OS == "linux32" ? "linux" : OS;
|
||||
this.xpiLink = SIMULATOR_LINK.replace(/#OS#/g, fixedOS)
|
||||
.replace(/#VERSION#/g, version)
|
||||
.replace(/#SLASHED_VERSION#/g, version.replace(/\./g, "_"));
|
||||
this.addonID = SIMULATOR_ADDON_ID.replace(/#SLASHED_VERSION#/g, version.replace(/\./g, "_"));
|
||||
this.updateInstallStatus();
|
||||
}
|
||||
SimulatorAddon.prototype = Object.create(Addon.prototype);
|
||||
|
||||
function ADBAddon() {
|
||||
EventEmitter.decorate(this);
|
||||
// This addon uses the string "linux" for "linux32"
|
||||
let fixedOS = OS == "linux32" ? "linux" : OS;
|
||||
this.xpiLink = ADB_LINK.replace(/#OS#/g, fixedOS);
|
||||
this.addonID = ADB_ADDON_ID;
|
||||
this.updateInstallStatus();
|
||||
}
|
||||
ADBAddon.prototype = Object.create(Addon.prototype);
|
||||
|
||||
function AdaptersAddon() {
|
||||
EventEmitter.decorate(this);
|
||||
this.xpiLink = ADAPTERS_LINK.replace(/#OS#/g, OS);
|
||||
this.addonID = ADAPTERS_ADDON_ID;
|
||||
this.updateInstallStatus();
|
||||
}
|
||||
AdaptersAddon.prototype = Object.create(Addon.prototype);
|
||||
850
devtools/client/webide/modules/app-manager.js
Normal file
850
devtools/client/webide/modules/app-manager.js
Normal file
|
|
@ -0,0 +1,850 @@
|
|||
/* 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/shared/fronts/device");
|
||||
const {getPreferenceFront} = require("devtools/shared/fronts/preference");
|
||||
const {getSettingsFront} = require("devtools/shared/fronts/settings");
|
||||
const {Task} = require("devtools/shared/task");
|
||||
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);
|
||||
235
devtools/client/webide/modules/app-projects.js
Normal file
235
devtools/client/webide/modules/app-projects.js
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
/* 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 {Cc, Ci, Cu, Cr} = require("chrome");
|
||||
const promise = require("promise");
|
||||
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const {generateUUID} = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
|
||||
const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
|
||||
|
||||
/**
|
||||
* IndexedDB wrapper that just save project objects
|
||||
*
|
||||
* The only constraint is that project objects have to have
|
||||
* a unique `location` object.
|
||||
*/
|
||||
|
||||
const IDB = {
|
||||
_db: null,
|
||||
databaseName: "AppProjects",
|
||||
|
||||
open: function () {
|
||||
let deferred = promise.defer();
|
||||
|
||||
let request = indexedDB.open(IDB.databaseName, 5);
|
||||
request.onerror = function (event) {
|
||||
deferred.reject("Unable to open AppProjects indexedDB: " +
|
||||
this.error.name + " - " + this.error.message);
|
||||
};
|
||||
request.onupgradeneeded = function (event) {
|
||||
let db = event.target.result;
|
||||
db.createObjectStore("projects", { keyPath: "location" });
|
||||
};
|
||||
|
||||
request.onsuccess = function () {
|
||||
let db = IDB._db = request.result;
|
||||
let objectStore = db.transaction("projects").objectStore("projects");
|
||||
let projects = [];
|
||||
let toRemove = [];
|
||||
objectStore.openCursor().onsuccess = function (event) {
|
||||
let cursor = event.target.result;
|
||||
if (cursor) {
|
||||
if (cursor.value.location) {
|
||||
|
||||
// We need to make sure this object has a `.location` property.
|
||||
// The UI depends on this property.
|
||||
// This should not be needed as we make sure to register valid
|
||||
// projects, but in the past (before bug 924568), we might have
|
||||
// registered invalid objects.
|
||||
|
||||
|
||||
// We also want to make sure the location is valid.
|
||||
// If the location doesn't exist, we remove the project.
|
||||
|
||||
try {
|
||||
let file = FileUtils.File(cursor.value.location);
|
||||
if (file.exists()) {
|
||||
projects.push(cursor.value);
|
||||
} else {
|
||||
toRemove.push(cursor.value.location);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.result == Cr.NS_ERROR_FILE_UNRECOGNIZED_PATH) {
|
||||
// A URL
|
||||
projects.push(cursor.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
cursor.continue();
|
||||
} else {
|
||||
let removePromises = [];
|
||||
for (let location of toRemove) {
|
||||
removePromises.push(IDB.remove(location));
|
||||
}
|
||||
promise.all(removePromises).then(() => {
|
||||
deferred.resolve(projects);
|
||||
});
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
add: function (project) {
|
||||
let deferred = promise.defer();
|
||||
|
||||
if (!project.location) {
|
||||
// We need to make sure this object has a `.location` property.
|
||||
deferred.reject("Missing location property on project object.");
|
||||
} else {
|
||||
let transaction = IDB._db.transaction(["projects"], "readwrite");
|
||||
let objectStore = transaction.objectStore("projects");
|
||||
let request = objectStore.add(project);
|
||||
request.onerror = function (event) {
|
||||
deferred.reject("Unable to add project to the AppProjects indexedDB: " +
|
||||
this.error.name + " - " + this.error.message);
|
||||
};
|
||||
request.onsuccess = function () {
|
||||
deferred.resolve();
|
||||
};
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
update: function (project) {
|
||||
let deferred = promise.defer();
|
||||
|
||||
var transaction = IDB._db.transaction(["projects"], "readwrite");
|
||||
var objectStore = transaction.objectStore("projects");
|
||||
var request = objectStore.put(project);
|
||||
request.onerror = function (event) {
|
||||
deferred.reject("Unable to update project to the AppProjects indexedDB: " +
|
||||
this.error.name + " - " + this.error.message);
|
||||
};
|
||||
request.onsuccess = function () {
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
remove: function (location) {
|
||||
let deferred = promise.defer();
|
||||
|
||||
let request = IDB._db.transaction(["projects"], "readwrite")
|
||||
.objectStore("projects")
|
||||
.delete(location);
|
||||
request.onsuccess = function (event) {
|
||||
deferred.resolve();
|
||||
};
|
||||
request.onerror = function () {
|
||||
deferred.reject("Unable to delete project to the AppProjects indexedDB: " +
|
||||
this.error.name + " - " + this.error.message);
|
||||
};
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
};
|
||||
|
||||
var loadDeferred = promise.defer();
|
||||
|
||||
loadDeferred.resolve(IDB.open().then(function (projects) {
|
||||
AppProjects.projects = projects;
|
||||
AppProjects.emit("ready", projects);
|
||||
}));
|
||||
|
||||
const AppProjects = {
|
||||
load: function () {
|
||||
return loadDeferred.promise;
|
||||
},
|
||||
|
||||
addPackaged: function (folder) {
|
||||
let file = FileUtils.File(folder.path);
|
||||
if (!file.exists()) {
|
||||
return promise.reject("path doesn't exist");
|
||||
}
|
||||
let existingProject = this.get(folder.path);
|
||||
if (existingProject) {
|
||||
return promise.reject("Already added");
|
||||
}
|
||||
let project = {
|
||||
type: "packaged",
|
||||
location: folder.path,
|
||||
// We need a unique id, that is the app origin,
|
||||
// in order to identify the app when being installed on the device.
|
||||
// The packaged app local path is a valid id, but only on the client.
|
||||
// This origin will be used to generate the true id of an app:
|
||||
// its manifest URL.
|
||||
// If the app ends up specifying an explicit origin in its manifest,
|
||||
// we will override this random UUID on app install.
|
||||
packagedAppOrigin: generateUUID().toString().slice(1, -1)
|
||||
};
|
||||
return IDB.add(project).then(() => {
|
||||
this.projects.push(project);
|
||||
return project;
|
||||
});
|
||||
},
|
||||
|
||||
addHosted: function (manifestURL) {
|
||||
let existingProject = this.get(manifestURL);
|
||||
if (existingProject) {
|
||||
return promise.reject("Already added");
|
||||
}
|
||||
let project = {
|
||||
type: "hosted",
|
||||
location: manifestURL
|
||||
};
|
||||
return IDB.add(project).then(() => {
|
||||
this.projects.push(project);
|
||||
return project;
|
||||
});
|
||||
},
|
||||
|
||||
update: function (project) {
|
||||
return IDB.update(project);
|
||||
},
|
||||
|
||||
updateLocation: function (project, newLocation) {
|
||||
return IDB.remove(project.location)
|
||||
.then(() => {
|
||||
project.location = newLocation;
|
||||
return IDB.add(project);
|
||||
});
|
||||
},
|
||||
|
||||
remove: function (location) {
|
||||
return IDB.remove(location).then(() => {
|
||||
for (let i = 0; i < this.projects.length; i++) {
|
||||
if (this.projects[i].location == location) {
|
||||
this.projects.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error("Unable to find project in AppProjects store");
|
||||
});
|
||||
},
|
||||
|
||||
get: function (location) {
|
||||
for (let i = 0; i < this.projects.length; i++) {
|
||||
if (this.projects[i].location == location) {
|
||||
return this.projects[i];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
projects: []
|
||||
};
|
||||
|
||||
EventEmitter.decorate(AppProjects);
|
||||
|
||||
exports.AppProjects = AppProjects;
|
||||
292
devtools/client/webide/modules/app-validator.js
Normal file
292
devtools/client/webide/modules/app-validator.js
Normal file
|
|
@ -0,0 +1,292 @@
|
|||
/* 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";
|
||||
|
||||
var {Ci, Cu, CC} = require("chrome");
|
||||
const promise = require("promise");
|
||||
|
||||
const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm", {});
|
||||
const Services = require("Services");
|
||||
const {Task} = require("devtools/shared/task");
|
||||
var XMLHttpRequest = CC("@mozilla.org/xmlextras/xmlhttprequest;1");
|
||||
var strings = Services.strings.createBundle("chrome://devtools/locale/app-manager.properties");
|
||||
|
||||
function AppValidator({ type, location }) {
|
||||
this.type = type;
|
||||
this.location = location;
|
||||
this.errors = [];
|
||||
this.warnings = [];
|
||||
}
|
||||
|
||||
AppValidator.prototype.error = function (message) {
|
||||
this.errors.push(message);
|
||||
};
|
||||
|
||||
AppValidator.prototype.warning = function (message) {
|
||||
this.warnings.push(message);
|
||||
};
|
||||
|
||||
AppValidator.prototype._getPackagedManifestFile = function () {
|
||||
let manifestFile = FileUtils.File(this.location);
|
||||
if (!manifestFile.exists()) {
|
||||
this.error(strings.GetStringFromName("validator.nonExistingFolder"));
|
||||
return null;
|
||||
}
|
||||
if (!manifestFile.isDirectory()) {
|
||||
this.error(strings.GetStringFromName("validator.expectProjectFolder"));
|
||||
return null;
|
||||
}
|
||||
|
||||
let appManifestFile = manifestFile.clone();
|
||||
appManifestFile.append("manifest.webapp");
|
||||
|
||||
let jsonManifestFile = manifestFile.clone();
|
||||
jsonManifestFile.append("manifest.json");
|
||||
|
||||
let hasAppManifest = appManifestFile.exists() && appManifestFile.isFile();
|
||||
let hasJsonManifest = jsonManifestFile.exists() && jsonManifestFile.isFile();
|
||||
|
||||
if (!hasAppManifest && !hasJsonManifest) {
|
||||
this.error(strings.GetStringFromName("validator.noManifestFile"));
|
||||
return null;
|
||||
}
|
||||
|
||||
return hasAppManifest ? appManifestFile : jsonManifestFile;
|
||||
};
|
||||
|
||||
AppValidator.prototype._getPackagedManifestURL = function () {
|
||||
let manifestFile = this._getPackagedManifestFile();
|
||||
if (!manifestFile) {
|
||||
return null;
|
||||
}
|
||||
return Services.io.newFileURI(manifestFile).spec;
|
||||
};
|
||||
|
||||
AppValidator.checkManifest = function (manifestURL) {
|
||||
let deferred = promise.defer();
|
||||
let error;
|
||||
|
||||
let req = new XMLHttpRequest();
|
||||
req.overrideMimeType("text/plain");
|
||||
|
||||
try {
|
||||
req.open("GET", manifestURL, true);
|
||||
req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING;
|
||||
} catch (e) {
|
||||
error = strings.formatStringFromName("validator.invalidManifestURL", [manifestURL], 1);
|
||||
deferred.reject(error);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
req.onload = function () {
|
||||
let manifest = null;
|
||||
try {
|
||||
manifest = JSON.parse(req.responseText);
|
||||
} catch (e) {
|
||||
error = strings.formatStringFromName("validator.invalidManifestJSON", [e, manifestURL], 2);
|
||||
deferred.reject(error);
|
||||
}
|
||||
|
||||
deferred.resolve({manifest, manifestURL});
|
||||
};
|
||||
|
||||
req.onerror = function () {
|
||||
error = strings.formatStringFromName("validator.noAccessManifestURL", [req.statusText, manifestURL], 2);
|
||||
deferred.reject(error);
|
||||
};
|
||||
|
||||
try {
|
||||
req.send(null);
|
||||
} catch (e) {
|
||||
error = strings.formatStringFromName("validator.noAccessManifestURL", [e, manifestURL], 2);
|
||||
deferred.reject(error);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
AppValidator.findManifestAtOrigin = function (manifestURL) {
|
||||
let fixedManifest = Services.io.newURI(manifestURL, null, null).prePath + "/manifest.webapp";
|
||||
return AppValidator.checkManifest(fixedManifest);
|
||||
};
|
||||
|
||||
AppValidator.findManifestPath = function (manifestURL) {
|
||||
let deferred = promise.defer();
|
||||
|
||||
if (manifestURL.endsWith("manifest.webapp")) {
|
||||
deferred.reject();
|
||||
} else {
|
||||
let fixedManifest = manifestURL + "/manifest.webapp";
|
||||
deferred.resolve(AppValidator.checkManifest(fixedManifest));
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
AppValidator.checkAlternateManifest = function (manifestURL) {
|
||||
return Task.spawn(function* () {
|
||||
let result;
|
||||
try {
|
||||
result = yield AppValidator.findManifestPath(manifestURL);
|
||||
} catch (e) {
|
||||
result = yield AppValidator.findManifestAtOrigin(manifestURL);
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
AppValidator.prototype._fetchManifest = function (manifestURL) {
|
||||
let deferred = promise.defer();
|
||||
this.manifestURL = manifestURL;
|
||||
|
||||
AppValidator.checkManifest(manifestURL)
|
||||
.then(({manifest, manifestURL}) => {
|
||||
deferred.resolve(manifest);
|
||||
}, error => {
|
||||
AppValidator.checkAlternateManifest(manifestURL)
|
||||
.then(({manifest, manifestURL}) => {
|
||||
this.manifestURL = manifestURL;
|
||||
deferred.resolve(manifest);
|
||||
}, () => {
|
||||
this.error(error);
|
||||
deferred.resolve(null);
|
||||
});
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
AppValidator.prototype._getManifest = function () {
|
||||
let manifestURL;
|
||||
if (this.type == "packaged") {
|
||||
manifestURL = this._getPackagedManifestURL();
|
||||
if (!manifestURL)
|
||||
return promise.resolve(null);
|
||||
} else if (this.type == "hosted") {
|
||||
manifestURL = this.location;
|
||||
try {
|
||||
Services.io.newURI(manifestURL, null, null);
|
||||
} catch (e) {
|
||||
this.error(strings.formatStringFromName("validator.invalidHostedManifestURL", [manifestURL, e.message], 2));
|
||||
return promise.resolve(null);
|
||||
}
|
||||
} else {
|
||||
this.error(strings.formatStringFromName("validator.invalidProjectType", [this.type], 1));
|
||||
return promise.resolve(null);
|
||||
}
|
||||
return this._fetchManifest(manifestURL);
|
||||
};
|
||||
|
||||
AppValidator.prototype.validateManifest = function (manifest) {
|
||||
if (!manifest.name) {
|
||||
this.error(strings.GetStringFromName("validator.missNameManifestProperty"));
|
||||
}
|
||||
|
||||
if (!manifest.icons || Object.keys(manifest.icons).length === 0) {
|
||||
this.warning(strings.GetStringFromName("validator.missIconsManifestProperty"));
|
||||
} else if (!manifest.icons["128"]) {
|
||||
this.warning(strings.GetStringFromName("validator.missIconMarketplace2"));
|
||||
}
|
||||
};
|
||||
|
||||
AppValidator.prototype._getOriginURL = function () {
|
||||
if (this.type == "packaged") {
|
||||
let manifestURL = Services.io.newURI(this.manifestURL, null, null);
|
||||
return Services.io.newURI(".", null, manifestURL).spec;
|
||||
} else if (this.type == "hosted") {
|
||||
return Services.io.newURI(this.location, null, null).prePath;
|
||||
}
|
||||
};
|
||||
|
||||
AppValidator.prototype.validateLaunchPath = function (manifest) {
|
||||
let deferred = promise.defer();
|
||||
// The launch_path field has to start with a `/`
|
||||
if (manifest.launch_path && manifest.launch_path[0] !== "/") {
|
||||
this.error(strings.formatStringFromName("validator.nonAbsoluteLaunchPath", [manifest.launch_path], 1));
|
||||
deferred.resolve();
|
||||
return deferred.promise;
|
||||
}
|
||||
let origin = this._getOriginURL();
|
||||
let path;
|
||||
if (this.type == "packaged") {
|
||||
path = "." + (manifest.launch_path || "/index.html");
|
||||
} else if (this.type == "hosted") {
|
||||
path = manifest.launch_path || "/";
|
||||
}
|
||||
let indexURL;
|
||||
try {
|
||||
indexURL = Services.io.newURI(path, null, Services.io.newURI(origin, null, null)).spec;
|
||||
} catch (e) {
|
||||
this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [origin + path], 1));
|
||||
deferred.resolve();
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
let req = new XMLHttpRequest();
|
||||
req.overrideMimeType("text/plain");
|
||||
try {
|
||||
req.open("HEAD", indexURL, true);
|
||||
req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING;
|
||||
} catch (e) {
|
||||
this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [indexURL], 1));
|
||||
deferred.resolve();
|
||||
return deferred.promise;
|
||||
}
|
||||
req.onload = () => {
|
||||
if (req.status >= 400)
|
||||
this.error(strings.formatStringFromName("validator.accessFailedLaunchPathBadHttpCode", [indexURL, req.status], 2));
|
||||
deferred.resolve();
|
||||
};
|
||||
req.onerror = () => {
|
||||
this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [indexURL], 1));
|
||||
deferred.resolve();
|
||||
};
|
||||
|
||||
try {
|
||||
req.send(null);
|
||||
} catch (e) {
|
||||
this.error(strings.formatStringFromName("validator.accessFailedLaunchPath", [indexURL], 1));
|
||||
deferred.resolve();
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
AppValidator.prototype.validateType = function (manifest) {
|
||||
let appType = manifest.type || "web";
|
||||
if (["web", "privileged", "certified"].indexOf(appType) === -1) {
|
||||
this.error(strings.formatStringFromName("validator.invalidAppType", [appType], 1));
|
||||
} else if (this.type == "hosted" &&
|
||||
["certified", "privileged"].indexOf(appType) !== -1) {
|
||||
this.error(strings.formatStringFromName("validator.invalidHostedPriviledges", [appType], 1));
|
||||
}
|
||||
|
||||
// certified app are not fully supported on the simulator
|
||||
if (appType === "certified") {
|
||||
this.warning(strings.GetStringFromName("validator.noCertifiedSupport"));
|
||||
}
|
||||
};
|
||||
|
||||
AppValidator.prototype.validate = function () {
|
||||
this.errors = [];
|
||||
this.warnings = [];
|
||||
return this._getManifest().
|
||||
then((manifest) => {
|
||||
if (manifest) {
|
||||
this.manifest = manifest;
|
||||
|
||||
// Skip validations for add-ons
|
||||
if (manifest.role === "addon" || manifest.manifest_version) {
|
||||
return promise.resolve();
|
||||
}
|
||||
|
||||
this.validateManifest(manifest);
|
||||
this.validateType(manifest);
|
||||
return this.validateLaunchPath(manifest);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
exports.AppValidator = AppValidator;
|
||||
199
devtools/client/webide/modules/build.js
Normal file
199
devtools/client/webide/modules/build.js
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
/* 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, Cc, Ci} = require("chrome");
|
||||
|
||||
const promise = require("promise");
|
||||
const { Task } = require("devtools/shared/task");
|
||||
const { TextDecoder, OS } = Cu.import("resource://gre/modules/osfile.jsm", {});
|
||||
const Subprocess = require("sdk/system/child_process/subprocess");
|
||||
|
||||
const ProjectBuilding = exports.ProjectBuilding = {
|
||||
fetchPackageManifest: Task.async(function* (project) {
|
||||
let manifestPath = OS.Path.join(project.location, "package.json");
|
||||
let exists = yield OS.File.exists(manifestPath);
|
||||
if (!exists) {
|
||||
// No explicit manifest, try to generate one if possible
|
||||
return this.generatePackageManifest(project);
|
||||
}
|
||||
|
||||
let data = yield OS.File.read(manifestPath);
|
||||
data = new TextDecoder().decode(data);
|
||||
let manifest;
|
||||
try {
|
||||
manifest = JSON.parse(data);
|
||||
} catch (e) {
|
||||
throw new Error("Error while reading WebIDE manifest at: '" + manifestPath +
|
||||
"', invalid JSON: " + e.message);
|
||||
}
|
||||
return manifest;
|
||||
}),
|
||||
|
||||
/**
|
||||
* For common frameworks in the community, attempt to detect the build
|
||||
* settings if none are defined. This makes it much easier to get started
|
||||
* with WebIDE. Later on, perhaps an add-on could define such things for
|
||||
* different frameworks.
|
||||
*/
|
||||
generatePackageManifest: Task.async(function* (project) {
|
||||
// Cordova
|
||||
let cordovaConfigPath = OS.Path.join(project.location, "config.xml");
|
||||
let exists = yield OS.File.exists(cordovaConfigPath);
|
||||
if (!exists) {
|
||||
return;
|
||||
}
|
||||
let data = yield OS.File.read(cordovaConfigPath);
|
||||
data = new TextDecoder().decode(data);
|
||||
if (data.contains("cordova.apache.org")) {
|
||||
return {
|
||||
"webide": {
|
||||
"prepackage": "cordova prepare",
|
||||
"packageDir": "./platforms/firefoxos/www"
|
||||
}
|
||||
};
|
||||
}
|
||||
}),
|
||||
|
||||
hasPrepackage: Task.async(function* (project) {
|
||||
let manifest = yield ProjectBuilding.fetchPackageManifest(project);
|
||||
return manifest && manifest.webide && "prepackage" in manifest.webide;
|
||||
}),
|
||||
|
||||
// If the app depends on some build step, run it before pushing the app
|
||||
build: Task.async(function* ({ project, logger }) {
|
||||
if (!(yield this.hasPrepackage(project))) {
|
||||
return;
|
||||
}
|
||||
|
||||
let manifest = yield ProjectBuilding.fetchPackageManifest(project);
|
||||
|
||||
logger("start");
|
||||
try {
|
||||
yield this._build(project, manifest, logger);
|
||||
logger("succeed");
|
||||
} catch (e) {
|
||||
logger("failed", e);
|
||||
}
|
||||
}),
|
||||
|
||||
_build: Task.async(function* (project, manifest, logger) {
|
||||
// Look for `webide` property
|
||||
manifest = manifest.webide;
|
||||
|
||||
let command, cwd, args = [], env = [];
|
||||
|
||||
// Copy frequently used env vars
|
||||
let envService = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
|
||||
["HOME", "PATH"].forEach(key => {
|
||||
let value = envService.get(key);
|
||||
if (value) {
|
||||
env.push(key + "=" + value);
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof (manifest.prepackage) === "string") {
|
||||
command = manifest.prepackage.replace(/%project%/g, project.location);
|
||||
} else if (manifest.prepackage.command) {
|
||||
command = manifest.prepackage.command;
|
||||
|
||||
args = manifest.prepackage.args || [];
|
||||
args = args.map(a => a.replace(/%project%/g, project.location));
|
||||
|
||||
env = env.concat(manifest.prepackage.env || []);
|
||||
env = env.map(a => a.replace(/%project%/g, project.location));
|
||||
|
||||
if (manifest.prepackage.cwd) {
|
||||
// Normalize path for Windows support (converts / to \)
|
||||
let path = OS.Path.normalize(manifest.prepackage.cwd);
|
||||
// Note that Path.join also support absolute path and argument.
|
||||
// So that if cwd is absolute, it will return cwd.
|
||||
let rel = OS.Path.join(project.location, path);
|
||||
let exists = yield OS.File.exists(rel);
|
||||
if (exists) {
|
||||
cwd = rel;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error("pre-package manifest is invalid, missing or invalid " +
|
||||
"`prepackage` attribute");
|
||||
}
|
||||
|
||||
if (!cwd) {
|
||||
cwd = project.location;
|
||||
}
|
||||
|
||||
logger("Running pre-package hook '" + command + "' " +
|
||||
args.join(" ") +
|
||||
" with ENV=[" + env.join(", ") + "]" +
|
||||
" at " + cwd);
|
||||
|
||||
// Run the command through a shell command in order to support non absolute
|
||||
// paths.
|
||||
// On Windows `ComSpec` env variable is going to refer to cmd.exe,
|
||||
// Otherwise, on Linux and Mac, SHELL env variable should refer to
|
||||
// the user chosen shell program.
|
||||
// (We do not check for OS, as on windows, with cygwin, ComSpec isn't set)
|
||||
let shell = envService.get("ComSpec") || envService.get("SHELL");
|
||||
args.unshift(command);
|
||||
|
||||
// For cmd.exe, we have to pass the `/C` option,
|
||||
// but for unix shells we need -c.
|
||||
// That to interpret next argument as a shell command.
|
||||
if (envService.exists("ComSpec")) {
|
||||
args.unshift("/C");
|
||||
} else {
|
||||
args.unshift("-c");
|
||||
}
|
||||
|
||||
// Subprocess changes CWD, we have to save and restore it.
|
||||
let originalCwd = yield OS.File.getCurrentDirectory();
|
||||
try {
|
||||
let defer = promise.defer();
|
||||
Subprocess.call({
|
||||
command: shell,
|
||||
arguments: args,
|
||||
environment: env,
|
||||
workdir: cwd,
|
||||
|
||||
stdout: data =>
|
||||
logger(data),
|
||||
stderr: data =>
|
||||
logger(data),
|
||||
|
||||
done: result => {
|
||||
logger("Terminated with error code: " + result.exitCode);
|
||||
if (result.exitCode == 0) {
|
||||
defer.resolve();
|
||||
} else {
|
||||
defer.reject("pre-package command failed with error code " + result.exitCode);
|
||||
}
|
||||
}
|
||||
});
|
||||
defer.promise.then(() => {
|
||||
OS.File.setCurrentDirectory(originalCwd);
|
||||
});
|
||||
yield defer.promise;
|
||||
} catch (e) {
|
||||
throw new Error("Unable to run pre-package command '" + command + "' " +
|
||||
args.join(" ") + ":\n" + (e.message || e));
|
||||
}
|
||||
}),
|
||||
|
||||
getPackageDir: Task.async(function* (project) {
|
||||
let manifest = yield ProjectBuilding.fetchPackageManifest(project);
|
||||
if (!manifest || !manifest.webide || !manifest.webide.packageDir) {
|
||||
return project.location;
|
||||
}
|
||||
manifest = manifest.webide;
|
||||
|
||||
let packageDir = OS.Path.join(project.location, manifest.packageDir);
|
||||
// On Windows, replace / by \\
|
||||
packageDir = OS.Path.normalize(packageDir);
|
||||
let exists = yield OS.File.exists(packageDir);
|
||||
if (exists) {
|
||||
return packageDir;
|
||||
}
|
||||
throw new Error("Unable to resolve application package directory: '" + manifest.packageDir + "'");
|
||||
})
|
||||
};
|
||||
373
devtools/client/webide/modules/config-view.js
Normal file
373
devtools/client/webide/modules/config-view.js
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
/* 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 EventEmitter = require("devtools/shared/event-emitter");
|
||||
const Services = require("Services");
|
||||
const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
|
||||
|
||||
var ConfigView;
|
||||
|
||||
module.exports = ConfigView = function (window) {
|
||||
EventEmitter.decorate(this);
|
||||
this._doc = window.document;
|
||||
this._keys = [];
|
||||
return this;
|
||||
};
|
||||
|
||||
ConfigView.prototype = {
|
||||
_renderByType: function (input, name, value, customType) {
|
||||
value = customType || typeof value;
|
||||
|
||||
switch (value) {
|
||||
case "boolean":
|
||||
input.setAttribute("data-type", "boolean");
|
||||
input.setAttribute("type", "checkbox");
|
||||
break;
|
||||
case "number":
|
||||
input.setAttribute("data-type", "number");
|
||||
input.setAttribute("type", "number");
|
||||
break;
|
||||
case "object":
|
||||
input.setAttribute("data-type", "object");
|
||||
input.setAttribute("type", "text");
|
||||
break;
|
||||
default:
|
||||
input.setAttribute("data-type", "string");
|
||||
input.setAttribute("type", "text");
|
||||
break;
|
||||
}
|
||||
return input;
|
||||
},
|
||||
|
||||
set front(front) {
|
||||
this._front = front;
|
||||
},
|
||||
|
||||
set keys(keys) {
|
||||
this._keys = keys;
|
||||
},
|
||||
|
||||
get keys() {
|
||||
return this._keys;
|
||||
},
|
||||
|
||||
set kind(kind) {
|
||||
this._kind = kind;
|
||||
},
|
||||
|
||||
set includeTypeName(include) {
|
||||
this._includeTypeName = include;
|
||||
},
|
||||
|
||||
search: function (event) {
|
||||
if (event.target.value.length) {
|
||||
let stringMatch = new RegExp(event.target.value, "i");
|
||||
|
||||
for (let i = 0; i < this._keys.length; i++) {
|
||||
let key = this._keys[i];
|
||||
let row = this._doc.getElementById("row-" + key);
|
||||
if (key.match(stringMatch)) {
|
||||
row.classList.remove("hide");
|
||||
} else if (row) {
|
||||
row.classList.add("hide");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var trs = this._doc.getElementById("device-fields").querySelectorAll("tr");
|
||||
|
||||
for (let i = 0; i < trs.length; i++) {
|
||||
trs[i].classList.remove("hide");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
generateDisplay: function (json) {
|
||||
let deviceItems = Object.keys(json);
|
||||
deviceItems.sort();
|
||||
this.keys = deviceItems;
|
||||
for (let i = 0; i < this.keys.length; i++) {
|
||||
let key = this.keys[i];
|
||||
this.generateField(key, json[key].value, json[key].hasUserValue);
|
||||
}
|
||||
},
|
||||
|
||||
generateField: function (name, value, hasUserValue, customType, newRow) {
|
||||
let table = this._doc.querySelector("table");
|
||||
let sResetDefault = Strings.GetStringFromName("device_reset_default");
|
||||
|
||||
if (this._keys.indexOf(name) === -1) {
|
||||
this._keys.push(name);
|
||||
}
|
||||
|
||||
let input = this._doc.createElement("input");
|
||||
let tr = this._doc.createElement("tr");
|
||||
tr.setAttribute("id", "row-" + name);
|
||||
tr.classList.add("edit-row");
|
||||
let td = this._doc.createElement("td");
|
||||
td.classList.add("field-name");
|
||||
td.textContent = name;
|
||||
tr.appendChild(td);
|
||||
td = this._doc.createElement("td");
|
||||
input.classList.add("editable");
|
||||
input.setAttribute("id", name);
|
||||
input = this._renderByType(input, name, value, customType);
|
||||
|
||||
if (customType === "boolean" || input.type === "checkbox") {
|
||||
input.checked = value;
|
||||
} else {
|
||||
if (typeof value === "object") {
|
||||
value = JSON.stringify(value);
|
||||
}
|
||||
input.value = value;
|
||||
}
|
||||
|
||||
if (!(this._includeTypeName || isNaN(parseInt(value, 10)))) {
|
||||
input.type = "number";
|
||||
}
|
||||
|
||||
td.appendChild(input);
|
||||
tr.appendChild(td);
|
||||
td = this._doc.createElement("td");
|
||||
td.setAttribute("id", "td-" + name);
|
||||
|
||||
let button = this._doc.createElement("button");
|
||||
button.setAttribute("data-id", name);
|
||||
button.setAttribute("id", "btn-" + name);
|
||||
button.classList.add("reset");
|
||||
button.textContent = sResetDefault;
|
||||
td.appendChild(button);
|
||||
|
||||
if (!hasUserValue) {
|
||||
button.classList.add("hide");
|
||||
}
|
||||
|
||||
tr.appendChild(td);
|
||||
|
||||
// If this is a new field, add it to the top of the table.
|
||||
if (newRow) {
|
||||
let existing = table.querySelector("#" + name);
|
||||
|
||||
if (!existing) {
|
||||
table.insertBefore(tr, newRow);
|
||||
} else {
|
||||
existing.value = value;
|
||||
}
|
||||
} else {
|
||||
table.appendChild(tr);
|
||||
}
|
||||
},
|
||||
|
||||
resetTable: function () {
|
||||
let table = this._doc.querySelector("table");
|
||||
let trs = table.querySelectorAll("tr:not(#add-custom-field)");
|
||||
|
||||
for (var i = 0; i < trs.length; i++) {
|
||||
table.removeChild(trs[i]);
|
||||
}
|
||||
|
||||
return table;
|
||||
},
|
||||
|
||||
_getCallType: function (type, name) {
|
||||
let frontName = "get";
|
||||
|
||||
if (this._includeTypeName) {
|
||||
frontName += type;
|
||||
}
|
||||
|
||||
return this._front[frontName + this._kind](name);
|
||||
},
|
||||
|
||||
_setCallType: function (type, name, value) {
|
||||
let frontName = "set";
|
||||
|
||||
if (this._includeTypeName) {
|
||||
frontName += type;
|
||||
}
|
||||
|
||||
return this._front[frontName + this._kind](name, value);
|
||||
},
|
||||
|
||||
_saveByType: function (options) {
|
||||
let fieldName = options.id;
|
||||
let inputType = options.type;
|
||||
let value = options.value;
|
||||
let input = this._doc.getElementById(fieldName);
|
||||
|
||||
switch (inputType) {
|
||||
case "boolean":
|
||||
this._setCallType("Bool", fieldName, input.checked);
|
||||
break;
|
||||
case "number":
|
||||
this._setCallType("Int", fieldName, value);
|
||||
break;
|
||||
case "object":
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
} catch (e) {}
|
||||
this._setCallType("Object", fieldName, value);
|
||||
break;
|
||||
default:
|
||||
this._setCallType("Char", fieldName, value);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
updateField: function (event) {
|
||||
if (event.target) {
|
||||
let inputType = event.target.getAttribute("data-type");
|
||||
let inputValue = event.target.checked || event.target.value;
|
||||
|
||||
if (event.target.nodeName == "input" &&
|
||||
event.target.validity.valid &&
|
||||
event.target.classList.contains("editable")) {
|
||||
let id = event.target.id;
|
||||
if (inputType === "boolean") {
|
||||
if (event.target.checked) {
|
||||
inputValue = true;
|
||||
} else {
|
||||
inputValue = false;
|
||||
}
|
||||
}
|
||||
|
||||
this._saveByType({
|
||||
id: id,
|
||||
type: inputType,
|
||||
value: inputValue
|
||||
});
|
||||
this._doc.getElementById("btn-" + id).classList.remove("hide");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_resetToDefault: function (name, input, button) {
|
||||
this._front["clearUser" + this._kind](name);
|
||||
let dataType = input.getAttribute("data-type");
|
||||
let tr = this._doc.getElementById("row-" + name);
|
||||
|
||||
switch (dataType) {
|
||||
case "boolean":
|
||||
this._defaultField = this._getCallType("Bool", name);
|
||||
this._defaultField.then(boolean => {
|
||||
input.checked = boolean;
|
||||
}, () => {
|
||||
input.checked = false;
|
||||
tr.parentNode.removeChild(tr);
|
||||
});
|
||||
break;
|
||||
case "number":
|
||||
this._defaultField = this._getCallType("Int", name);
|
||||
this._defaultField.then(number => {
|
||||
input.value = number;
|
||||
}, () => {
|
||||
tr.parentNode.removeChild(tr);
|
||||
});
|
||||
break;
|
||||
case "object":
|
||||
this._defaultField = this._getCallType("Object", name);
|
||||
this._defaultField.then(object => {
|
||||
input.value = JSON.stringify(object);
|
||||
}, () => {
|
||||
tr.parentNode.removeChild(tr);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
this._defaultField = this._getCallType("Char", name);
|
||||
this._defaultField.then(string => {
|
||||
input.value = string;
|
||||
}, () => {
|
||||
tr.parentNode.removeChild(tr);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
button.classList.add("hide");
|
||||
},
|
||||
|
||||
checkReset: function (event) {
|
||||
if (event.target.classList.contains("reset")) {
|
||||
let btnId = event.target.getAttribute("data-id");
|
||||
let input = this._doc.getElementById(btnId);
|
||||
this._resetToDefault(btnId, input, event.target);
|
||||
}
|
||||
},
|
||||
|
||||
updateFieldType: function () {
|
||||
let table = this._doc.querySelector("table");
|
||||
let customValueType = table.querySelector("#custom-value-type").value;
|
||||
let customTextEl = table.querySelector("#custom-value-text");
|
||||
let customText = customTextEl.value;
|
||||
|
||||
if (customValueType.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (customValueType) {
|
||||
case "boolean":
|
||||
customTextEl.type = "checkbox";
|
||||
customText = customTextEl.checked;
|
||||
break;
|
||||
case "number":
|
||||
customText = parseInt(customText, 10) || 0;
|
||||
customTextEl.type = "number";
|
||||
break;
|
||||
default:
|
||||
customTextEl.type = "text";
|
||||
break;
|
||||
}
|
||||
|
||||
return customValueType;
|
||||
},
|
||||
|
||||
clearNewFields: function () {
|
||||
let table = this._doc.querySelector("table");
|
||||
let customTextEl = table.querySelector("#custom-value-text");
|
||||
if (customTextEl.checked) {
|
||||
customTextEl.checked = false;
|
||||
} else {
|
||||
customTextEl.value = "";
|
||||
}
|
||||
|
||||
this.updateFieldType();
|
||||
},
|
||||
|
||||
updateNewField: function () {
|
||||
let table = this._doc.querySelector("table");
|
||||
let customValueType = this.updateFieldType();
|
||||
|
||||
if (!customValueType) {
|
||||
return;
|
||||
}
|
||||
|
||||
let customRow = table.querySelector("tr:nth-of-type(2)");
|
||||
let customTextEl = table.querySelector("#custom-value-text");
|
||||
let customTextNameEl = table.querySelector("#custom-value-name");
|
||||
|
||||
if (customTextEl.validity.valid) {
|
||||
let customText = customTextEl.value;
|
||||
|
||||
if (customValueType === "boolean") {
|
||||
customText = customTextEl.checked;
|
||||
}
|
||||
|
||||
let customTextName = customTextNameEl.value.replace(/[^A-Za-z0-9\.\-_]/gi, "");
|
||||
this.generateField(customTextName, customText, true, customValueType, customRow);
|
||||
this._saveByType({
|
||||
id: customTextName,
|
||||
type: customValueType,
|
||||
value: customText
|
||||
});
|
||||
customTextNameEl.value = "";
|
||||
this.clearNewFields();
|
||||
}
|
||||
},
|
||||
|
||||
checkNewFieldSubmit: function (event) {
|
||||
if (event.keyCode === 13) {
|
||||
this._doc.getElementById("custom-value").click();
|
||||
}
|
||||
}
|
||||
};
|
||||
21
devtools/client/webide/modules/moz.build
Normal file
21
devtools/client/webide/modules/moz.build
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# vim: set filetype=python:
|
||||
# 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/.
|
||||
|
||||
DevToolsModules(
|
||||
'addons.js',
|
||||
'app-manager.js',
|
||||
'app-projects.js',
|
||||
'app-validator.js',
|
||||
'build.js',
|
||||
'config-view.js',
|
||||
'project-list.js',
|
||||
'runtime-list.js',
|
||||
'runtimes.js',
|
||||
'simulator-process.js',
|
||||
'simulators.js',
|
||||
'tab-store.js',
|
||||
'utils.js'
|
||||
)
|
||||
375
devtools/client/webide/modules/project-list.js
Normal file
375
devtools/client/webide/modules/project-list.js
Normal file
|
|
@ -0,0 +1,375 @@
|
|||
/* 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 Services = require("Services");
|
||||
const {AppProjects} = require("devtools/client/webide/modules/app-projects");
|
||||
const {AppManager} = require("devtools/client/webide/modules/app-manager");
|
||||
const promise = require("promise");
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const {Task} = require("devtools/shared/task");
|
||||
const utils = require("devtools/client/webide/modules/utils");
|
||||
const Telemetry = require("devtools/client/shared/telemetry");
|
||||
|
||||
const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
|
||||
|
||||
var ProjectList;
|
||||
|
||||
module.exports = ProjectList = function (win, parentWindow) {
|
||||
EventEmitter.decorate(this);
|
||||
this._doc = win.document;
|
||||
this._UI = parentWindow.UI;
|
||||
this._parentWindow = parentWindow;
|
||||
this._telemetry = new Telemetry();
|
||||
this._panelNodeEl = "div";
|
||||
|
||||
this.onWebIDEUpdate = this.onWebIDEUpdate.bind(this);
|
||||
this._UI.on("webide-update", this.onWebIDEUpdate);
|
||||
|
||||
AppManager.init();
|
||||
this.appManagerUpdate = this.appManagerUpdate.bind(this);
|
||||
AppManager.on("app-manager-update", this.appManagerUpdate);
|
||||
};
|
||||
|
||||
ProjectList.prototype = {
|
||||
get doc() {
|
||||
return this._doc;
|
||||
},
|
||||
|
||||
appManagerUpdate: function (event, what, details) {
|
||||
// Got a message from app-manager.js
|
||||
// See AppManager.update() for descriptions of what these events mean.
|
||||
switch (what) {
|
||||
case "project-removed":
|
||||
case "runtime-apps-icons":
|
||||
case "runtime-targets":
|
||||
case "connection":
|
||||
this.update(details);
|
||||
break;
|
||||
case "project":
|
||||
this.updateCommands();
|
||||
this.update(details);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onWebIDEUpdate: function (event, what, details) {
|
||||
if (what == "busy" || what == "unbusy") {
|
||||
this.updateCommands();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* testOptions: { chrome mochitest support
|
||||
* folder: nsIFile, where to store the app
|
||||
* index: Number, index of the app in the template list
|
||||
* name: String name of the app
|
||||
* }
|
||||
*/
|
||||
newApp: function (testOptions) {
|
||||
let parentWindow = this._parentWindow;
|
||||
let self = this;
|
||||
return this._UI.busyUntil(Task.spawn(function* () {
|
||||
// Open newapp.xul, which will feed ret.location
|
||||
let ret = {location: null, testOptions: testOptions};
|
||||
parentWindow.openDialog("chrome://webide/content/newapp.xul", "newapp", "chrome,modal", ret);
|
||||
if (!ret.location)
|
||||
return;
|
||||
|
||||
// Retrieve added project
|
||||
let project = AppProjects.get(ret.location);
|
||||
|
||||
// Select project
|
||||
AppManager.selectedProject = project;
|
||||
|
||||
self._telemetry.actionOccurred("webideNewProject");
|
||||
}), "creating new app");
|
||||
},
|
||||
|
||||
importPackagedApp: function (location) {
|
||||
let parentWindow = this._parentWindow;
|
||||
let UI = this._UI;
|
||||
return UI.busyUntil(Task.spawn(function* () {
|
||||
let directory = utils.getPackagedDirectory(parentWindow, location);
|
||||
|
||||
if (!directory) {
|
||||
// User cancelled directory selection
|
||||
return;
|
||||
}
|
||||
|
||||
yield UI.importAndSelectApp(directory);
|
||||
}), "importing packaged app");
|
||||
},
|
||||
|
||||
importHostedApp: function (location) {
|
||||
let parentWindow = this._parentWindow;
|
||||
let UI = this._UI;
|
||||
return UI.busyUntil(Task.spawn(function* () {
|
||||
let url = utils.getHostedURL(parentWindow, location);
|
||||
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
yield UI.importAndSelectApp(url);
|
||||
}), "importing hosted app");
|
||||
},
|
||||
|
||||
/**
|
||||
* opts: {
|
||||
* panel: Object, currenl project panel node
|
||||
* name: String, name of the project
|
||||
* icon: String path of the project icon
|
||||
* }
|
||||
*/
|
||||
_renderProjectItem: function (opts) {
|
||||
let span = opts.panel.querySelector("span") || this._doc.createElement("span");
|
||||
span.textContent = opts.name;
|
||||
let icon = opts.panel.querySelector("img") || this._doc.createElement("img");
|
||||
icon.className = "project-image";
|
||||
icon.setAttribute("src", opts.icon);
|
||||
opts.panel.appendChild(icon);
|
||||
opts.panel.appendChild(span);
|
||||
opts.panel.setAttribute("title", opts.name);
|
||||
},
|
||||
|
||||
refreshTabs: function () {
|
||||
if (AppManager.connected) {
|
||||
return AppManager.listTabs().then(() => {
|
||||
this.updateTabs();
|
||||
}).catch(console.error);
|
||||
}
|
||||
},
|
||||
|
||||
updateTabs: function () {
|
||||
let tabsHeaderNode = this._doc.querySelector("#panel-header-tabs");
|
||||
let tabsNode = this._doc.querySelector("#project-panel-tabs");
|
||||
|
||||
while (tabsNode.hasChildNodes()) {
|
||||
tabsNode.firstChild.remove();
|
||||
}
|
||||
|
||||
if (!AppManager.connected) {
|
||||
tabsHeaderNode.setAttribute("hidden", "true");
|
||||
return;
|
||||
}
|
||||
|
||||
let tabs = AppManager.tabStore.tabs;
|
||||
|
||||
tabsHeaderNode.removeAttribute("hidden");
|
||||
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
let tab = tabs[i];
|
||||
let URL = this._parentWindow.URL;
|
||||
let url;
|
||||
try {
|
||||
url = new URL(tab.url);
|
||||
} catch (e) {
|
||||
// Don't try to handle invalid URLs, especially from Valence.
|
||||
continue;
|
||||
}
|
||||
// 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).
|
||||
if (url.origin) {
|
||||
tab.favicon = url.origin + "/favicon.ico";
|
||||
}
|
||||
tab.name = tab.title || Strings.GetStringFromName("project_tab_loading");
|
||||
if (url.protocol.startsWith("http")) {
|
||||
tab.name = url.hostname + ": " + tab.name;
|
||||
}
|
||||
let panelItemNode = this._doc.createElement(this._panelNodeEl);
|
||||
panelItemNode.className = "panel-item";
|
||||
tabsNode.appendChild(panelItemNode);
|
||||
this._renderProjectItem({
|
||||
panel: panelItemNode,
|
||||
name: tab.name,
|
||||
icon: tab.favicon || AppManager.DEFAULT_PROJECT_ICON
|
||||
});
|
||||
panelItemNode.addEventListener("click", () => {
|
||||
AppManager.selectedProject = {
|
||||
type: "tab",
|
||||
app: tab,
|
||||
icon: tab.favicon || AppManager.DEFAULT_PROJECT_ICON,
|
||||
location: tab.url,
|
||||
name: tab.name
|
||||
};
|
||||
}, true);
|
||||
}
|
||||
|
||||
return promise.resolve();
|
||||
},
|
||||
|
||||
updateApps: function () {
|
||||
let doc = this._doc;
|
||||
let runtimeappsHeaderNode = doc.querySelector("#panel-header-runtimeapps");
|
||||
let sortedApps = [];
|
||||
for (let [manifestURL, app] of AppManager.apps) {
|
||||
sortedApps.push(app);
|
||||
}
|
||||
sortedApps = sortedApps.sort((a, b) => {
|
||||
return a.manifest.name > b.manifest.name;
|
||||
});
|
||||
let mainProcess = AppManager.isMainProcessDebuggable();
|
||||
if (AppManager.connected && (sortedApps.length > 0 || mainProcess)) {
|
||||
runtimeappsHeaderNode.removeAttribute("hidden");
|
||||
} else {
|
||||
runtimeappsHeaderNode.setAttribute("hidden", "true");
|
||||
}
|
||||
|
||||
let runtimeAppsNode = doc.querySelector("#project-panel-runtimeapps");
|
||||
while (runtimeAppsNode.hasChildNodes()) {
|
||||
runtimeAppsNode.firstChild.remove();
|
||||
}
|
||||
|
||||
if (mainProcess) {
|
||||
let panelItemNode = doc.createElement(this._panelNodeEl);
|
||||
panelItemNode.className = "panel-item";
|
||||
this._renderProjectItem({
|
||||
panel: panelItemNode,
|
||||
name: Strings.GetStringFromName("mainProcess_label"),
|
||||
icon: AppManager.DEFAULT_PROJECT_ICON
|
||||
});
|
||||
runtimeAppsNode.appendChild(panelItemNode);
|
||||
panelItemNode.addEventListener("click", () => {
|
||||
AppManager.selectedProject = {
|
||||
type: "mainProcess",
|
||||
name: Strings.GetStringFromName("mainProcess_label"),
|
||||
icon: AppManager.DEFAULT_PROJECT_ICON
|
||||
};
|
||||
}, true);
|
||||
}
|
||||
|
||||
for (let i = 0; i < sortedApps.length; i++) {
|
||||
let app = sortedApps[i];
|
||||
let panelItemNode = doc.createElement(this._panelNodeEl);
|
||||
panelItemNode.className = "panel-item";
|
||||
this._renderProjectItem({
|
||||
panel: panelItemNode,
|
||||
name: app.manifest.name,
|
||||
icon: app.iconURL || AppManager.DEFAULT_PROJECT_ICON
|
||||
});
|
||||
runtimeAppsNode.appendChild(panelItemNode);
|
||||
panelItemNode.addEventListener("click", () => {
|
||||
AppManager.selectedProject = {
|
||||
type: "runtimeApp",
|
||||
app: app.manifest,
|
||||
icon: app.iconURL || AppManager.DEFAULT_PROJECT_ICON,
|
||||
name: app.manifest.name
|
||||
};
|
||||
}, true);
|
||||
}
|
||||
|
||||
return promise.resolve();
|
||||
},
|
||||
|
||||
updateCommands: function () {
|
||||
let doc = this._doc;
|
||||
let newAppCmd;
|
||||
let packagedAppCmd;
|
||||
let hostedAppCmd;
|
||||
|
||||
newAppCmd = doc.querySelector("#new-app");
|
||||
packagedAppCmd = doc.querySelector("#packaged-app");
|
||||
hostedAppCmd = doc.querySelector("#hosted-app");
|
||||
|
||||
if (!newAppCmd || !packagedAppCmd || !hostedAppCmd) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._parentWindow.document.querySelector("window").classList.contains("busy")) {
|
||||
newAppCmd.setAttribute("disabled", "true");
|
||||
packagedAppCmd.setAttribute("disabled", "true");
|
||||
hostedAppCmd.setAttribute("disabled", "true");
|
||||
return;
|
||||
}
|
||||
|
||||
newAppCmd.removeAttribute("disabled");
|
||||
packagedAppCmd.removeAttribute("disabled");
|
||||
hostedAppCmd.removeAttribute("disabled");
|
||||
},
|
||||
|
||||
/**
|
||||
* Trigger an update of the project and remote runtime list.
|
||||
* @param options object (optional)
|
||||
* An |options| object containing a type of |apps| or |tabs| will limit
|
||||
* what is updated to only those sections.
|
||||
*/
|
||||
update: function (options) {
|
||||
let deferred = promise.defer();
|
||||
|
||||
if (options && options.type === "apps") {
|
||||
return this.updateApps();
|
||||
} else if (options && options.type === "tabs") {
|
||||
return this.updateTabs();
|
||||
}
|
||||
|
||||
let doc = this._doc;
|
||||
let projectsNode = doc.querySelector("#project-panel-projects");
|
||||
|
||||
while (projectsNode.hasChildNodes()) {
|
||||
projectsNode.firstChild.remove();
|
||||
}
|
||||
|
||||
AppProjects.load().then(() => {
|
||||
let projects = AppProjects.projects;
|
||||
for (let i = 0; i < projects.length; i++) {
|
||||
let project = projects[i];
|
||||
let panelItemNode = doc.createElement(this._panelNodeEl);
|
||||
panelItemNode.className = "panel-item";
|
||||
projectsNode.appendChild(panelItemNode);
|
||||
if (!project.validationStatus) {
|
||||
// The result of the validation process (storing names, icons, …) is not stored in
|
||||
// the IndexedDB database when App Manager v1 is used.
|
||||
// We need to run the validation again and update the name and icon of the app.
|
||||
AppManager.validateAndUpdateProject(project).then(() => {
|
||||
this._renderProjectItem({
|
||||
panel: panelItemNode,
|
||||
name: project.name,
|
||||
icon: project.icon
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this._renderProjectItem({
|
||||
panel: panelItemNode,
|
||||
name: project.name || AppManager.DEFAULT_PROJECT_NAME,
|
||||
icon: project.icon || AppManager.DEFAULT_PROJECT_ICON
|
||||
});
|
||||
}
|
||||
panelItemNode.addEventListener("click", () => {
|
||||
AppManager.selectedProject = project;
|
||||
}, true);
|
||||
}
|
||||
|
||||
deferred.resolve();
|
||||
}, deferred.reject);
|
||||
|
||||
// List remote apps and the main process, if they exist
|
||||
this.updateApps();
|
||||
|
||||
// Build the tab list right now, so it's fast...
|
||||
this.updateTabs();
|
||||
|
||||
// But re-list them and rebuild, in case any tabs navigated since the last
|
||||
// time they were listed.
|
||||
if (AppManager.connected) {
|
||||
AppManager.listTabs().then(() => {
|
||||
this.updateTabs();
|
||||
}).catch(console.error);
|
||||
}
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
this._doc = null;
|
||||
AppManager.off("app-manager-update", this.appManagerUpdate);
|
||||
this._UI.off("webide-update", this.onWebIDEUpdate);
|
||||
this._UI = null;
|
||||
this._parentWindow = null;
|
||||
this._panelNodeEl = null;
|
||||
}
|
||||
};
|
||||
207
devtools/client/webide/modules/runtime-list.js
Normal file
207
devtools/client/webide/modules/runtime-list.js
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
/* 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 Services = require("Services");
|
||||
const {AppManager} = require("devtools/client/webide/modules/app-manager");
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const {RuntimeScanners, WiFiScanner} = require("devtools/client/webide/modules/runtimes");
|
||||
const {Devices} = require("resource://devtools/shared/apps/Devices.jsm");
|
||||
const {Task} = require("devtools/shared/task");
|
||||
const utils = require("devtools/client/webide/modules/utils");
|
||||
|
||||
const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
|
||||
|
||||
var RuntimeList;
|
||||
|
||||
module.exports = RuntimeList = function (window, parentWindow) {
|
||||
EventEmitter.decorate(this);
|
||||
this._doc = window.document;
|
||||
this._UI = parentWindow.UI;
|
||||
this._Cmds = parentWindow.Cmds;
|
||||
this._parentWindow = parentWindow;
|
||||
this._panelNodeEl = "button";
|
||||
this._panelBoxEl = "div";
|
||||
|
||||
this.onWebIDEUpdate = this.onWebIDEUpdate.bind(this);
|
||||
this._UI.on("webide-update", this.onWebIDEUpdate);
|
||||
|
||||
AppManager.init();
|
||||
this.appManagerUpdate = this.appManagerUpdate.bind(this);
|
||||
AppManager.on("app-manager-update", this.appManagerUpdate);
|
||||
};
|
||||
|
||||
RuntimeList.prototype = {
|
||||
get doc() {
|
||||
return this._doc;
|
||||
},
|
||||
|
||||
appManagerUpdate: function (event, what, details) {
|
||||
// Got a message from app-manager.js
|
||||
// See AppManager.update() for descriptions of what these events mean.
|
||||
switch (what) {
|
||||
case "runtime-list":
|
||||
this.update();
|
||||
break;
|
||||
case "connection":
|
||||
case "runtime-global-actors":
|
||||
this.updateCommands();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
onWebIDEUpdate: function (event, what, details) {
|
||||
if (what == "busy" || what == "unbusy") {
|
||||
this.updateCommands();
|
||||
}
|
||||
},
|
||||
|
||||
takeScreenshot: function () {
|
||||
this._Cmds.takeScreenshot();
|
||||
},
|
||||
|
||||
showRuntimeDetails: function () {
|
||||
this._Cmds.showRuntimeDetails();
|
||||
},
|
||||
|
||||
showPermissionsTable: function () {
|
||||
this._Cmds.showPermissionsTable();
|
||||
},
|
||||
|
||||
showDevicePreferences: function () {
|
||||
this._Cmds.showDevicePrefs();
|
||||
},
|
||||
|
||||
showSettings: function () {
|
||||
this._Cmds.showSettings();
|
||||
},
|
||||
|
||||
showTroubleShooting: function () {
|
||||
this._Cmds.showTroubleShooting();
|
||||
},
|
||||
|
||||
showAddons: function () {
|
||||
this._Cmds.showAddons();
|
||||
},
|
||||
|
||||
refreshScanners: function () {
|
||||
RuntimeScanners.scan();
|
||||
},
|
||||
|
||||
updateCommands: function () {
|
||||
let doc = this._doc;
|
||||
|
||||
// Runtime commands
|
||||
let screenshotCmd = doc.querySelector("#runtime-screenshot");
|
||||
let permissionsCmd = doc.querySelector("#runtime-permissions");
|
||||
let detailsCmd = doc.querySelector("#runtime-details");
|
||||
let disconnectCmd = doc.querySelector("#runtime-disconnect");
|
||||
let devicePrefsCmd = doc.querySelector("#runtime-preferences");
|
||||
let settingsCmd = doc.querySelector("#runtime-settings");
|
||||
|
||||
if (AppManager.connected) {
|
||||
if (AppManager.deviceFront) {
|
||||
detailsCmd.removeAttribute("disabled");
|
||||
permissionsCmd.removeAttribute("disabled");
|
||||
screenshotCmd.removeAttribute("disabled");
|
||||
}
|
||||
if (AppManager.preferenceFront) {
|
||||
devicePrefsCmd.removeAttribute("disabled");
|
||||
}
|
||||
if (AppManager.settingsFront) {
|
||||
settingsCmd.removeAttribute("disabled");
|
||||
}
|
||||
disconnectCmd.removeAttribute("disabled");
|
||||
} else {
|
||||
detailsCmd.setAttribute("disabled", "true");
|
||||
permissionsCmd.setAttribute("disabled", "true");
|
||||
screenshotCmd.setAttribute("disabled", "true");
|
||||
disconnectCmd.setAttribute("disabled", "true");
|
||||
devicePrefsCmd.setAttribute("disabled", "true");
|
||||
settingsCmd.setAttribute("disabled", "true");
|
||||
}
|
||||
},
|
||||
|
||||
update: function () {
|
||||
let doc = this._doc;
|
||||
let wifiHeaderNode = doc.querySelector("#runtime-header-wifi");
|
||||
|
||||
if (WiFiScanner.allowed) {
|
||||
wifiHeaderNode.removeAttribute("hidden");
|
||||
} else {
|
||||
wifiHeaderNode.setAttribute("hidden", "true");
|
||||
}
|
||||
|
||||
let usbListNode = doc.querySelector("#runtime-panel-usb");
|
||||
let wifiListNode = doc.querySelector("#runtime-panel-wifi");
|
||||
let simulatorListNode = doc.querySelector("#runtime-panel-simulator");
|
||||
let otherListNode = doc.querySelector("#runtime-panel-other");
|
||||
let noHelperNode = doc.querySelector("#runtime-panel-noadbhelper");
|
||||
let noUSBNode = doc.querySelector("#runtime-panel-nousbdevice");
|
||||
|
||||
if (Devices.helperAddonInstalled) {
|
||||
noHelperNode.setAttribute("hidden", "true");
|
||||
} else {
|
||||
noHelperNode.removeAttribute("hidden");
|
||||
}
|
||||
|
||||
let runtimeList = AppManager.runtimeList;
|
||||
|
||||
if (!runtimeList) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (runtimeList.usb.length === 0 && Devices.helperAddonInstalled) {
|
||||
noUSBNode.removeAttribute("hidden");
|
||||
} else {
|
||||
noUSBNode.setAttribute("hidden", "true");
|
||||
}
|
||||
|
||||
for (let [type, parent] of [
|
||||
["usb", usbListNode],
|
||||
["wifi", wifiListNode],
|
||||
["simulator", simulatorListNode],
|
||||
["other", otherListNode],
|
||||
]) {
|
||||
while (parent.hasChildNodes()) {
|
||||
parent.firstChild.remove();
|
||||
}
|
||||
for (let runtime of runtimeList[type]) {
|
||||
let r = runtime;
|
||||
let panelItemNode = doc.createElement(this._panelBoxEl);
|
||||
panelItemNode.className = "panel-item-complex";
|
||||
|
||||
let connectButton = doc.createElement(this._panelNodeEl);
|
||||
connectButton.className = "panel-item runtime-panel-item-" + type;
|
||||
connectButton.textContent = r.name;
|
||||
|
||||
connectButton.addEventListener("click", () => {
|
||||
this._UI.dismissErrorNotification();
|
||||
this._UI.connectToRuntime(r);
|
||||
}, true);
|
||||
panelItemNode.appendChild(connectButton);
|
||||
|
||||
if (r.configure) {
|
||||
let configButton = doc.createElement(this._panelNodeEl);
|
||||
configButton.className = "configure-button";
|
||||
configButton.addEventListener("click", r.configure.bind(r), true);
|
||||
panelItemNode.appendChild(configButton);
|
||||
}
|
||||
|
||||
parent.appendChild(panelItemNode);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
this._doc = null;
|
||||
AppManager.off("app-manager-update", this.appManagerUpdate);
|
||||
this._UI.off("webide-update", this.onWebIDEUpdate);
|
||||
this._UI = null;
|
||||
this._Cmds = null;
|
||||
this._parentWindow = null;
|
||||
this._panelNodeEl = null;
|
||||
}
|
||||
};
|
||||
673
devtools/client/webide/modules/runtimes.js
Normal file
673
devtools/client/webide/modules/runtimes.js
Normal file
|
|
@ -0,0 +1,673 @@
|
|||
/* 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} = require("chrome");
|
||||
const Services = require("Services");
|
||||
const {Devices} = require("resource://devtools/shared/apps/Devices.jsm");
|
||||
const {Connection} = require("devtools/shared/client/connection-manager");
|
||||
const {DebuggerServer} = require("devtools/server/main");
|
||||
const {Simulators} = require("devtools/client/webide/modules/simulators");
|
||||
const discovery = require("devtools/shared/discovery/discovery");
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const promise = require("promise");
|
||||
loader.lazyRequireGetter(this, "AuthenticationResult",
|
||||
"devtools/shared/security/auth", true);
|
||||
loader.lazyRequireGetter(this, "DevToolsUtils",
|
||||
"devtools/shared/DevToolsUtils");
|
||||
|
||||
const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
|
||||
|
||||
/**
|
||||
* Runtime and Scanner API
|
||||
*
|
||||
* |RuntimeScanners| maintains a set of |Scanner| objects that produce one or
|
||||
* more |Runtime|s to connect to. Add-ons can extend the set of known runtimes
|
||||
* by registering additional |Scanner|s that emit them.
|
||||
*
|
||||
* Each |Scanner| must support the following API:
|
||||
*
|
||||
* enable()
|
||||
* Bind any event handlers and start any background work the scanner needs to
|
||||
* maintain an updated set of |Runtime|s.
|
||||
* Called when the first consumer (such as WebIDE) actively interested in
|
||||
* maintaining the |Runtime| list enables the registry.
|
||||
* disable()
|
||||
* Unbind any event handlers and stop any background work the scanner needs to
|
||||
* maintain an updated set of |Runtime|s.
|
||||
* Called when the last consumer (such as WebIDE) actively interested in
|
||||
* maintaining the |Runtime| list disables the registry.
|
||||
* emits "runtime-list-updated"
|
||||
* If the set of runtimes a |Scanner| manages has changed, it must emit this
|
||||
* event to notify consumers of changes.
|
||||
* scan()
|
||||
* Actively refreshes the list of runtimes the scanner knows about. If your
|
||||
* scanner uses an active scanning approach (as opposed to listening for
|
||||
* events when changes occur), the bulk of the work would be done here.
|
||||
* @return Promise
|
||||
* Should be resolved when scanning is complete. If scanning has no
|
||||
* well-defined end point, you can resolve immediately, as long as
|
||||
* update event is emitted later when changes are noticed.
|
||||
* listRuntimes()
|
||||
* Return the current list of runtimes known to the |Scanner| instance.
|
||||
* @return Iterable
|
||||
*
|
||||
* Each |Runtime| must support the following API:
|
||||
*
|
||||
* |type| field
|
||||
* The |type| must be one of the values from the |RuntimeTypes| object. This
|
||||
* is used for Telemetry and to support displaying sets of |Runtime|s
|
||||
* categorized by type.
|
||||
* |id| field
|
||||
* An identifier that is unique in the set of all runtimes with the same
|
||||
* |type|. WebIDE tries to save the last used runtime via type + id, and
|
||||
* tries to locate it again in the next session, so this value should attempt
|
||||
* to be stable across Firefox sessions.
|
||||
* |name| field
|
||||
* A user-visible label to identify the runtime that will be displayed in a
|
||||
* runtime list.
|
||||
* |prolongedConnection| field
|
||||
* A boolean value which should be |true| if the connection process is
|
||||
* expected to take a unknown or large amount of time. A UI may use this as a
|
||||
* hint to skip timeouts or other time-based code paths.
|
||||
* connect()
|
||||
* Configure the passed |connection| object with any settings need to
|
||||
* successfully connect to the runtime, and call the |connection|'s connect()
|
||||
* method.
|
||||
* @param Connection connection
|
||||
* A |Connection| object from the DevTools |ConnectionManager|.
|
||||
* @return Promise
|
||||
* Resolved once you've called the |connection|'s connect() method.
|
||||
* configure() OPTIONAL
|
||||
* Show a configuration screen if the runtime is configurable.
|
||||
*/
|
||||
|
||||
/* SCANNER REGISTRY */
|
||||
|
||||
var RuntimeScanners = {
|
||||
|
||||
_enabledCount: 0,
|
||||
_scanners: new Set(),
|
||||
|
||||
get enabled() {
|
||||
return !!this._enabledCount;
|
||||
},
|
||||
|
||||
add(scanner) {
|
||||
if (this.enabled) {
|
||||
// Enable any scanner added while globally enabled
|
||||
this._enableScanner(scanner);
|
||||
}
|
||||
this._scanners.add(scanner);
|
||||
this._emitUpdated();
|
||||
},
|
||||
|
||||
remove(scanner) {
|
||||
this._scanners.delete(scanner);
|
||||
if (this.enabled) {
|
||||
// Disable any scanner removed while globally enabled
|
||||
this._disableScanner(scanner);
|
||||
}
|
||||
this._emitUpdated();
|
||||
},
|
||||
|
||||
has(scanner) {
|
||||
return this._scanners.has(scanner);
|
||||
},
|
||||
|
||||
scan() {
|
||||
if (!this.enabled) {
|
||||
return promise.resolve();
|
||||
}
|
||||
|
||||
if (this._scanPromise) {
|
||||
return this._scanPromise;
|
||||
}
|
||||
|
||||
let promises = [];
|
||||
|
||||
for (let scanner of this._scanners) {
|
||||
promises.push(scanner.scan());
|
||||
}
|
||||
|
||||
this._scanPromise = promise.all(promises);
|
||||
|
||||
// Reset pending promise
|
||||
this._scanPromise.then(() => {
|
||||
this._scanPromise = null;
|
||||
}, () => {
|
||||
this._scanPromise = null;
|
||||
});
|
||||
|
||||
return this._scanPromise;
|
||||
},
|
||||
|
||||
listRuntimes: function* () {
|
||||
for (let scanner of this._scanners) {
|
||||
for (let runtime of scanner.listRuntimes()) {
|
||||
yield runtime;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_emitUpdated() {
|
||||
this.emit("runtime-list-updated");
|
||||
},
|
||||
|
||||
enable() {
|
||||
if (this._enabledCount++ !== 0) {
|
||||
// Already enabled scanners during a previous call
|
||||
return;
|
||||
}
|
||||
this._emitUpdated = this._emitUpdated.bind(this);
|
||||
for (let scanner of this._scanners) {
|
||||
this._enableScanner(scanner);
|
||||
}
|
||||
},
|
||||
|
||||
_enableScanner(scanner) {
|
||||
scanner.enable();
|
||||
scanner.on("runtime-list-updated", this._emitUpdated);
|
||||
},
|
||||
|
||||
disable() {
|
||||
if (--this._enabledCount !== 0) {
|
||||
// Already disabled scanners during a previous call
|
||||
return;
|
||||
}
|
||||
for (let scanner of this._scanners) {
|
||||
this._disableScanner(scanner);
|
||||
}
|
||||
},
|
||||
|
||||
_disableScanner(scanner) {
|
||||
scanner.off("runtime-list-updated", this._emitUpdated);
|
||||
scanner.disable();
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
EventEmitter.decorate(RuntimeScanners);
|
||||
|
||||
exports.RuntimeScanners = RuntimeScanners;
|
||||
|
||||
/* SCANNERS */
|
||||
|
||||
var SimulatorScanner = {
|
||||
|
||||
_runtimes: [],
|
||||
|
||||
enable() {
|
||||
this._updateRuntimes = this._updateRuntimes.bind(this);
|
||||
Simulators.on("updated", this._updateRuntimes);
|
||||
this._updateRuntimes();
|
||||
},
|
||||
|
||||
disable() {
|
||||
Simulators.off("updated", this._updateRuntimes);
|
||||
},
|
||||
|
||||
_emitUpdated() {
|
||||
this.emit("runtime-list-updated");
|
||||
},
|
||||
|
||||
_updateRuntimes() {
|
||||
Simulators.findSimulators().then(simulators => {
|
||||
this._runtimes = [];
|
||||
for (let simulator of simulators) {
|
||||
this._runtimes.push(new SimulatorRuntime(simulator));
|
||||
}
|
||||
this._emitUpdated();
|
||||
});
|
||||
},
|
||||
|
||||
scan() {
|
||||
return promise.resolve();
|
||||
},
|
||||
|
||||
listRuntimes: function () {
|
||||
return this._runtimes;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
EventEmitter.decorate(SimulatorScanner);
|
||||
RuntimeScanners.add(SimulatorScanner);
|
||||
|
||||
/**
|
||||
* TODO: Remove this comaptibility layer in the future (bug 1085393)
|
||||
* This runtime exists to support the ADB Helper add-on below version 0.7.0.
|
||||
*
|
||||
* This scanner will list all ADB devices as runtimes, even if they may or may
|
||||
* not actually connect (since the |DeprecatedUSBRuntime| assumes a Firefox OS
|
||||
* device).
|
||||
*/
|
||||
var DeprecatedAdbScanner = {
|
||||
|
||||
_runtimes: [],
|
||||
|
||||
enable() {
|
||||
this._updateRuntimes = this._updateRuntimes.bind(this);
|
||||
Devices.on("register", this._updateRuntimes);
|
||||
Devices.on("unregister", this._updateRuntimes);
|
||||
Devices.on("addon-status-updated", this._updateRuntimes);
|
||||
this._updateRuntimes();
|
||||
},
|
||||
|
||||
disable() {
|
||||
Devices.off("register", this._updateRuntimes);
|
||||
Devices.off("unregister", this._updateRuntimes);
|
||||
Devices.off("addon-status-updated", this._updateRuntimes);
|
||||
},
|
||||
|
||||
_emitUpdated() {
|
||||
this.emit("runtime-list-updated");
|
||||
},
|
||||
|
||||
_updateRuntimes() {
|
||||
this._runtimes = [];
|
||||
for (let id of Devices.available()) {
|
||||
let runtime = new DeprecatedUSBRuntime(id);
|
||||
this._runtimes.push(runtime);
|
||||
runtime.updateNameFromADB().then(() => {
|
||||
this._emitUpdated();
|
||||
}, () => {});
|
||||
}
|
||||
this._emitUpdated();
|
||||
},
|
||||
|
||||
scan() {
|
||||
return promise.resolve();
|
||||
},
|
||||
|
||||
listRuntimes: function () {
|
||||
return this._runtimes;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
EventEmitter.decorate(DeprecatedAdbScanner);
|
||||
RuntimeScanners.add(DeprecatedAdbScanner);
|
||||
|
||||
// ADB Helper 0.7.0 and later will replace this scanner on startup
|
||||
exports.DeprecatedAdbScanner = DeprecatedAdbScanner;
|
||||
|
||||
/**
|
||||
* This is a lazy ADB scanner shim which only tells the ADB Helper to start and
|
||||
* stop as needed. The real scanner that lists devices lives in ADB Helper.
|
||||
* ADB Helper 0.8.0 and later wait until these signals are received before
|
||||
* starting ADB polling. For earlier versions, they have no effect.
|
||||
*/
|
||||
var LazyAdbScanner = {
|
||||
|
||||
enable() {
|
||||
Devices.emit("adb-start-polling");
|
||||
},
|
||||
|
||||
disable() {
|
||||
Devices.emit("adb-stop-polling");
|
||||
},
|
||||
|
||||
scan() {
|
||||
return promise.resolve();
|
||||
},
|
||||
|
||||
listRuntimes: function () {
|
||||
return [];
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
EventEmitter.decorate(LazyAdbScanner);
|
||||
RuntimeScanners.add(LazyAdbScanner);
|
||||
|
||||
var WiFiScanner = {
|
||||
|
||||
_runtimes: [],
|
||||
|
||||
init() {
|
||||
this.updateRegistration();
|
||||
Services.prefs.addObserver(this.ALLOWED_PREF, this, false);
|
||||
},
|
||||
|
||||
enable() {
|
||||
this._updateRuntimes = this._updateRuntimes.bind(this);
|
||||
discovery.on("devtools-device-added", this._updateRuntimes);
|
||||
discovery.on("devtools-device-updated", this._updateRuntimes);
|
||||
discovery.on("devtools-device-removed", this._updateRuntimes);
|
||||
this._updateRuntimes();
|
||||
},
|
||||
|
||||
disable() {
|
||||
discovery.off("devtools-device-added", this._updateRuntimes);
|
||||
discovery.off("devtools-device-updated", this._updateRuntimes);
|
||||
discovery.off("devtools-device-removed", this._updateRuntimes);
|
||||
},
|
||||
|
||||
_emitUpdated() {
|
||||
this.emit("runtime-list-updated");
|
||||
},
|
||||
|
||||
_updateRuntimes() {
|
||||
this._runtimes = [];
|
||||
for (let device of discovery.getRemoteDevicesWithService("devtools")) {
|
||||
this._runtimes.push(new WiFiRuntime(device));
|
||||
}
|
||||
this._emitUpdated();
|
||||
},
|
||||
|
||||
scan() {
|
||||
discovery.scan();
|
||||
return promise.resolve();
|
||||
},
|
||||
|
||||
listRuntimes: function () {
|
||||
return this._runtimes;
|
||||
},
|
||||
|
||||
ALLOWED_PREF: "devtools.remote.wifi.scan",
|
||||
|
||||
get allowed() {
|
||||
return Services.prefs.getBoolPref(this.ALLOWED_PREF);
|
||||
},
|
||||
|
||||
updateRegistration() {
|
||||
if (this.allowed) {
|
||||
RuntimeScanners.add(WiFiScanner);
|
||||
} else {
|
||||
RuntimeScanners.remove(WiFiScanner);
|
||||
}
|
||||
this._emitUpdated();
|
||||
},
|
||||
|
||||
observe(subject, topic, data) {
|
||||
if (data !== WiFiScanner.ALLOWED_PREF) {
|
||||
return;
|
||||
}
|
||||
WiFiScanner.updateRegistration();
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
EventEmitter.decorate(WiFiScanner);
|
||||
WiFiScanner.init();
|
||||
|
||||
exports.WiFiScanner = WiFiScanner;
|
||||
|
||||
var StaticScanner = {
|
||||
enable() {},
|
||||
disable() {},
|
||||
scan() { return promise.resolve(); },
|
||||
listRuntimes() {
|
||||
let runtimes = [gRemoteRuntime];
|
||||
if (Services.prefs.getBoolPref("devtools.webide.enableLocalRuntime")) {
|
||||
runtimes.push(gLocalRuntime);
|
||||
}
|
||||
return runtimes;
|
||||
}
|
||||
};
|
||||
|
||||
EventEmitter.decorate(StaticScanner);
|
||||
RuntimeScanners.add(StaticScanner);
|
||||
|
||||
/* RUNTIMES */
|
||||
|
||||
// These type strings are used for logging events to Telemetry.
|
||||
// You must update Histograms.json if new types are added.
|
||||
var RuntimeTypes = exports.RuntimeTypes = {
|
||||
USB: "USB",
|
||||
WIFI: "WIFI",
|
||||
SIMULATOR: "SIMULATOR",
|
||||
REMOTE: "REMOTE",
|
||||
LOCAL: "LOCAL",
|
||||
OTHER: "OTHER"
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: Remove this comaptibility layer in the future (bug 1085393)
|
||||
* This runtime exists to support the ADB Helper add-on below version 0.7.0.
|
||||
*
|
||||
* This runtime assumes it is connecting to a Firefox OS device.
|
||||
*/
|
||||
function DeprecatedUSBRuntime(id) {
|
||||
this._id = id;
|
||||
}
|
||||
|
||||
DeprecatedUSBRuntime.prototype = {
|
||||
type: RuntimeTypes.USB,
|
||||
get device() {
|
||||
return Devices.getByName(this._id);
|
||||
},
|
||||
connect: function (connection) {
|
||||
if (!this.device) {
|
||||
return promise.reject(new Error("Can't find device: " + this.name));
|
||||
}
|
||||
return this.device.connect().then((port) => {
|
||||
connection.host = "localhost";
|
||||
connection.port = port;
|
||||
connection.connect();
|
||||
});
|
||||
},
|
||||
get id() {
|
||||
return this._id;
|
||||
},
|
||||
get name() {
|
||||
return this._productModel || this._id;
|
||||
},
|
||||
updateNameFromADB: function () {
|
||||
if (this._productModel) {
|
||||
return promise.reject();
|
||||
}
|
||||
let deferred = promise.defer();
|
||||
if (this.device && this.device.shell) {
|
||||
this.device.shell("getprop ro.product.model").then(stdout => {
|
||||
this._productModel = stdout;
|
||||
deferred.resolve();
|
||||
}, () => {});
|
||||
} else {
|
||||
this._productModel = null;
|
||||
deferred.reject();
|
||||
}
|
||||
return deferred.promise;
|
||||
},
|
||||
};
|
||||
|
||||
// For testing use only
|
||||
exports._DeprecatedUSBRuntime = DeprecatedUSBRuntime;
|
||||
|
||||
function WiFiRuntime(deviceName) {
|
||||
this.deviceName = deviceName;
|
||||
}
|
||||
|
||||
WiFiRuntime.prototype = {
|
||||
type: RuntimeTypes.WIFI,
|
||||
// Mark runtime as taking a long time to connect
|
||||
prolongedConnection: true,
|
||||
connect: function (connection) {
|
||||
let service = discovery.getRemoteService("devtools", this.deviceName);
|
||||
if (!service) {
|
||||
return promise.reject(new Error("Can't find device: " + this.name));
|
||||
}
|
||||
connection.advertisement = service;
|
||||
connection.authenticator.sendOOB = this.sendOOB;
|
||||
// Disable the default connection timeout, since QR scanning can take an
|
||||
// unknown amount of time. This prevents spurious errors (even after
|
||||
// eventual success) from being shown.
|
||||
connection.timeoutDelay = 0;
|
||||
connection.connect();
|
||||
return promise.resolve();
|
||||
},
|
||||
get id() {
|
||||
return this.deviceName;
|
||||
},
|
||||
get name() {
|
||||
return this.deviceName;
|
||||
},
|
||||
|
||||
/**
|
||||
* During OOB_CERT authentication, a notification dialog like this is used to
|
||||
* to display a token which the user must transfer through some mechanism to the
|
||||
* server to authenticate the devices.
|
||||
*
|
||||
* This implementation presents the token as text for the user to transfer
|
||||
* manually. For a mobile device, you should override this implementation with
|
||||
* something more convenient, such as displaying a QR code.
|
||||
*
|
||||
* This method receives an object containing:
|
||||
* @param host string
|
||||
* The host name or IP address of the debugger server.
|
||||
* @param port number
|
||||
* The port number of the debugger server.
|
||||
* @param cert object (optional)
|
||||
* The server's cert details.
|
||||
* @param authResult AuthenticationResult
|
||||
* Authentication result sent from the server.
|
||||
* @param oob object (optional)
|
||||
* The token data to be transferred during OOB_CERT step 8:
|
||||
* * sha256: hash(ClientCert)
|
||||
* * k : K(random 128-bit number)
|
||||
* @return object containing:
|
||||
* * close: Function to hide the notification
|
||||
*/
|
||||
sendOOB(session) {
|
||||
const WINDOW_ID = "devtools:wifi-auth";
|
||||
let { authResult } = session;
|
||||
// Only show in the PENDING state
|
||||
if (authResult != AuthenticationResult.PENDING) {
|
||||
throw new Error("Expected PENDING result, got " + authResult);
|
||||
}
|
||||
|
||||
// Listen for the window our prompt opens, so we can close it programatically
|
||||
let promptWindow;
|
||||
let windowListener = {
|
||||
onOpenWindow(xulWindow) {
|
||||
let win = xulWindow.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow);
|
||||
win.addEventListener("load", function listener() {
|
||||
win.removeEventListener("load", listener, false);
|
||||
if (win.document.documentElement.getAttribute("id") != WINDOW_ID) {
|
||||
return;
|
||||
}
|
||||
// Found the window
|
||||
promptWindow = win;
|
||||
Services.wm.removeListener(windowListener);
|
||||
}, false);
|
||||
},
|
||||
onCloseWindow() {},
|
||||
onWindowTitleChange() {}
|
||||
};
|
||||
Services.wm.addListener(windowListener);
|
||||
|
||||
// |openDialog| is typically a blocking API, so |executeSoon| to get around this
|
||||
DevToolsUtils.executeSoon(() => {
|
||||
// Height determines the size of the QR code. Force a minimum size to
|
||||
// improve scanability.
|
||||
const MIN_HEIGHT = 600;
|
||||
let win = Services.wm.getMostRecentWindow("devtools:webide");
|
||||
let width = win.outerWidth * 0.8;
|
||||
let height = Math.max(win.outerHeight * 0.5, MIN_HEIGHT);
|
||||
win.openDialog("chrome://webide/content/wifi-auth.xhtml",
|
||||
WINDOW_ID,
|
||||
"modal=yes,width=" + width + ",height=" + height, session);
|
||||
});
|
||||
|
||||
return {
|
||||
close() {
|
||||
if (!promptWindow) {
|
||||
return;
|
||||
}
|
||||
promptWindow.close();
|
||||
promptWindow = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// For testing use only
|
||||
exports._WiFiRuntime = WiFiRuntime;
|
||||
|
||||
function SimulatorRuntime(simulator) {
|
||||
this.simulator = simulator;
|
||||
}
|
||||
|
||||
SimulatorRuntime.prototype = {
|
||||
type: RuntimeTypes.SIMULATOR,
|
||||
connect: function (connection) {
|
||||
return this.simulator.launch().then(port => {
|
||||
connection.host = "localhost";
|
||||
connection.port = port;
|
||||
connection.keepConnecting = true;
|
||||
connection.once(Connection.Events.DISCONNECTED, e => this.simulator.kill());
|
||||
connection.connect();
|
||||
});
|
||||
},
|
||||
configure() {
|
||||
Simulators.emit("configure", this.simulator);
|
||||
},
|
||||
get id() {
|
||||
return this.simulator.id;
|
||||
},
|
||||
get name() {
|
||||
return this.simulator.name;
|
||||
},
|
||||
};
|
||||
|
||||
// For testing use only
|
||||
exports._SimulatorRuntime = SimulatorRuntime;
|
||||
|
||||
var gLocalRuntime = {
|
||||
type: RuntimeTypes.LOCAL,
|
||||
connect: function (connection) {
|
||||
if (!DebuggerServer.initialized) {
|
||||
DebuggerServer.init();
|
||||
DebuggerServer.addBrowserActors();
|
||||
}
|
||||
DebuggerServer.allowChromeProcess = true;
|
||||
connection.host = null; // Force Pipe transport
|
||||
connection.port = null;
|
||||
connection.connect();
|
||||
return promise.resolve();
|
||||
},
|
||||
get id() {
|
||||
return "local";
|
||||
},
|
||||
get name() {
|
||||
return Strings.GetStringFromName("local_runtime");
|
||||
},
|
||||
};
|
||||
|
||||
// For testing use only
|
||||
exports._gLocalRuntime = gLocalRuntime;
|
||||
|
||||
var gRemoteRuntime = {
|
||||
type: RuntimeTypes.REMOTE,
|
||||
connect: function (connection) {
|
||||
let win = Services.wm.getMostRecentWindow("devtools:webide");
|
||||
if (!win) {
|
||||
return promise.reject(new Error("No WebIDE window found"));
|
||||
}
|
||||
let ret = {value: connection.host + ":" + connection.port};
|
||||
let title = Strings.GetStringFromName("remote_runtime_promptTitle");
|
||||
let message = Strings.GetStringFromName("remote_runtime_promptMessage");
|
||||
let ok = Services.prompt.prompt(win, title, message, ret, null, {});
|
||||
let [host, port] = ret.value.split(":");
|
||||
if (!ok) {
|
||||
return promise.reject({canceled: true});
|
||||
}
|
||||
if (!host || !port) {
|
||||
return promise.reject(new Error("Invalid host or port"));
|
||||
}
|
||||
connection.host = host;
|
||||
connection.port = port;
|
||||
connection.connect();
|
||||
return promise.resolve();
|
||||
},
|
||||
get name() {
|
||||
return Strings.GetStringFromName("remote_runtime");
|
||||
},
|
||||
};
|
||||
|
||||
// For testing use only
|
||||
exports._gRemoteRuntime = gRemoteRuntime;
|
||||
325
devtools/client/webide/modules/simulator-process.js
Normal file
325
devtools/client/webide/modules/simulator-process.js
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
/* 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 { Cc, Ci, Cu } = require("chrome");
|
||||
|
||||
const Environment = require("sdk/system/environment").env;
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const promise = require("promise");
|
||||
const Subprocess = require("sdk/system/child_process/subprocess");
|
||||
const Services = require("Services");
|
||||
|
||||
loader.lazyGetter(this, "OS", () => {
|
||||
const Runtime = require("sdk/system/runtime");
|
||||
switch (Runtime.OS) {
|
||||
case "Darwin":
|
||||
return "mac64";
|
||||
case "Linux":
|
||||
if (Runtime.XPCOMABI.indexOf("x86_64") === 0) {
|
||||
return "linux64";
|
||||
} else {
|
||||
return "linux32";
|
||||
}
|
||||
case "WINNT":
|
||||
return "win32";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
});
|
||||
|
||||
function SimulatorProcess() {}
|
||||
SimulatorProcess.prototype = {
|
||||
|
||||
// Check if B2G is running.
|
||||
get isRunning() {
|
||||
return !!this.process;
|
||||
},
|
||||
|
||||
// Start the process and connect the debugger client.
|
||||
run() {
|
||||
|
||||
// Resolve B2G binary.
|
||||
let b2g = this.b2gBinary;
|
||||
if (!b2g || !b2g.exists()) {
|
||||
throw Error("B2G executable not found.");
|
||||
}
|
||||
|
||||
// Ensure Gaia profile exists.
|
||||
let gaia = this.gaiaProfile;
|
||||
if (!gaia || !gaia.exists()) {
|
||||
throw Error("Gaia profile directory not found.");
|
||||
}
|
||||
|
||||
this.once("stdout", function () {
|
||||
if (OS == "mac64") {
|
||||
console.debug("WORKAROUND run osascript to show b2g-desktop window on OS=='mac64'");
|
||||
// Escape double quotes and escape characters for use in AppleScript.
|
||||
let path = b2g.path.replace(/\\/g, "\\\\").replace(/\"/g, '\\"');
|
||||
|
||||
Subprocess.call({
|
||||
command: "/usr/bin/osascript",
|
||||
arguments: ["-e", 'tell application "' + path + '" to activate'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
let logHandler = (e, data) => this.log(e, data.trim());
|
||||
this.on("stdout", logHandler);
|
||||
this.on("stderr", logHandler);
|
||||
this.once("exit", () => {
|
||||
this.off("stdout", logHandler);
|
||||
this.off("stderr", logHandler);
|
||||
});
|
||||
|
||||
let environment;
|
||||
if (OS.indexOf("linux") > -1) {
|
||||
environment = ["TMPDIR=" + Services.dirsvc.get("TmpD", Ci.nsIFile).path];
|
||||
["DISPLAY", "XAUTHORITY"].forEach(key => {
|
||||
if (key in Environment) {
|
||||
environment.push(key + "=" + Environment[key]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Spawn a B2G instance.
|
||||
this.process = Subprocess.call({
|
||||
command: b2g,
|
||||
arguments: this.args,
|
||||
environment: environment,
|
||||
stdout: data => this.emit("stdout", data),
|
||||
stderr: data => this.emit("stderr", data),
|
||||
// On B2G instance exit, reset tracked process, remote debugger port and
|
||||
// shuttingDown flag, then finally emit an exit event.
|
||||
done: result => {
|
||||
console.log("B2G terminated with " + result.exitCode);
|
||||
this.process = null;
|
||||
this.emit("exit", result.exitCode);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Request a B2G instance kill.
|
||||
kill() {
|
||||
let deferred = promise.defer();
|
||||
if (this.process) {
|
||||
this.once("exit", (e, exitCode) => {
|
||||
this.shuttingDown = false;
|
||||
deferred.resolve(exitCode);
|
||||
});
|
||||
if (!this.shuttingDown) {
|
||||
this.shuttingDown = true;
|
||||
this.emit("kill", null);
|
||||
this.process.kill();
|
||||
}
|
||||
return deferred.promise;
|
||||
} else {
|
||||
return promise.resolve(undefined);
|
||||
}
|
||||
},
|
||||
|
||||
// Maybe log output messages.
|
||||
log(level, message) {
|
||||
if (!Services.prefs.getBoolPref("devtools.webide.logSimulatorOutput")) {
|
||||
return;
|
||||
}
|
||||
if (level === "stderr" || level === "error") {
|
||||
console.error(message);
|
||||
return;
|
||||
}
|
||||
console.log(message);
|
||||
},
|
||||
|
||||
// Compute B2G CLI arguments.
|
||||
get args() {
|
||||
let args = [];
|
||||
|
||||
// Gaia profile.
|
||||
args.push("-profile", this.gaiaProfile.path);
|
||||
|
||||
// Debugger server port.
|
||||
let port = parseInt(this.options.port);
|
||||
args.push("-start-debugger-server", "" + port);
|
||||
|
||||
// Screen size.
|
||||
let width = parseInt(this.options.width);
|
||||
let height = parseInt(this.options.height);
|
||||
if (width && height) {
|
||||
args.push("-screen", width + "x" + height);
|
||||
}
|
||||
|
||||
// Ignore eventual zombie instances of b2g that are left over.
|
||||
args.push("-no-remote");
|
||||
|
||||
// If we are running a simulator based on Mulet,
|
||||
// we have to override the default chrome URL
|
||||
// in order to prevent the Browser UI to appear.
|
||||
if (this.b2gBinary.leafName.includes("firefox")) {
|
||||
args.push("-chrome", "chrome://b2g/content/shell.html");
|
||||
}
|
||||
|
||||
return args;
|
||||
},
|
||||
};
|
||||
|
||||
EventEmitter.decorate(SimulatorProcess.prototype);
|
||||
|
||||
|
||||
function CustomSimulatorProcess(options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
var CSPp = CustomSimulatorProcess.prototype = Object.create(SimulatorProcess.prototype);
|
||||
|
||||
// Compute B2G binary file handle.
|
||||
Object.defineProperty(CSPp, "b2gBinary", {
|
||||
get: function () {
|
||||
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
|
||||
file.initWithPath(this.options.b2gBinary);
|
||||
return file;
|
||||
}
|
||||
});
|
||||
|
||||
// Compute Gaia profile file handle.
|
||||
Object.defineProperty(CSPp, "gaiaProfile", {
|
||||
get: function () {
|
||||
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
|
||||
file.initWithPath(this.options.gaiaProfile);
|
||||
return file;
|
||||
}
|
||||
});
|
||||
|
||||
exports.CustomSimulatorProcess = CustomSimulatorProcess;
|
||||
|
||||
|
||||
function AddonSimulatorProcess(addon, options) {
|
||||
this.addon = addon;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
var ASPp = AddonSimulatorProcess.prototype = Object.create(SimulatorProcess.prototype);
|
||||
|
||||
// Compute B2G binary file handle.
|
||||
Object.defineProperty(ASPp, "b2gBinary", {
|
||||
get: function () {
|
||||
let file;
|
||||
try {
|
||||
let pref = "extensions." + this.addon.id + ".customRuntime";
|
||||
file = Services.prefs.getComplexValue(pref, Ci.nsIFile);
|
||||
} catch (e) {}
|
||||
|
||||
if (!file) {
|
||||
file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
|
||||
file.append("b2g");
|
||||
let binaries = {
|
||||
win32: "b2g-bin.exe",
|
||||
mac64: "B2G.app/Contents/MacOS/b2g-bin",
|
||||
linux32: "b2g-bin",
|
||||
linux64: "b2g-bin",
|
||||
};
|
||||
binaries[OS].split("/").forEach(node => file.append(node));
|
||||
}
|
||||
// If the binary doesn't exists, it may be because of a simulator
|
||||
// based on mulet, which has a different binary name.
|
||||
if (!file.exists()) {
|
||||
file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
|
||||
file.append("firefox");
|
||||
let binaries = {
|
||||
win32: "firefox.exe",
|
||||
mac64: "FirefoxNightly.app/Contents/MacOS/firefox-bin",
|
||||
linux32: "firefox-bin",
|
||||
linux64: "firefox-bin",
|
||||
};
|
||||
binaries[OS].split("/").forEach(node => file.append(node));
|
||||
}
|
||||
return file;
|
||||
}
|
||||
});
|
||||
|
||||
// Compute Gaia profile file handle.
|
||||
Object.defineProperty(ASPp, "gaiaProfile", {
|
||||
get: function () {
|
||||
let file;
|
||||
|
||||
// Custom profile from simulator configuration.
|
||||
if (this.options.gaiaProfile) {
|
||||
file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
|
||||
file.initWithPath(this.options.gaiaProfile);
|
||||
return file;
|
||||
}
|
||||
|
||||
// Custom profile from addon prefs.
|
||||
try {
|
||||
let pref = "extensions." + this.addon.id + ".gaiaProfile";
|
||||
file = Services.prefs.getComplexValue(pref, Ci.nsIFile);
|
||||
return file;
|
||||
} catch (e) {}
|
||||
|
||||
// Default profile from addon.
|
||||
file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
|
||||
file.append("profile");
|
||||
return file;
|
||||
}
|
||||
});
|
||||
|
||||
exports.AddonSimulatorProcess = AddonSimulatorProcess;
|
||||
|
||||
|
||||
function OldAddonSimulatorProcess(addon, options) {
|
||||
this.addon = addon;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
var OASPp = OldAddonSimulatorProcess.prototype = Object.create(AddonSimulatorProcess.prototype);
|
||||
|
||||
// Compute B2G binary file handle.
|
||||
Object.defineProperty(OASPp, "b2gBinary", {
|
||||
get: function () {
|
||||
let file;
|
||||
try {
|
||||
let pref = "extensions." + this.addon.id + ".customRuntime";
|
||||
file = Services.prefs.getComplexValue(pref, Ci.nsIFile);
|
||||
} catch (e) {}
|
||||
|
||||
if (!file) {
|
||||
file = this.addon.getResourceURI().QueryInterface(Ci.nsIFileURL).file;
|
||||
let version = this.addon.name.match(/\d+\.\d+/)[0].replace(/\./, "_");
|
||||
file.append("resources");
|
||||
file.append("fxos_" + version + "_simulator");
|
||||
file.append("data");
|
||||
file.append(OS == "linux32" ? "linux" : OS);
|
||||
let binaries = {
|
||||
win32: "b2g/b2g-bin.exe",
|
||||
mac64: "B2G.app/Contents/MacOS/b2g-bin",
|
||||
linux32: "b2g/b2g-bin",
|
||||
linux64: "b2g/b2g-bin",
|
||||
};
|
||||
binaries[OS].split("/").forEach(node => file.append(node));
|
||||
}
|
||||
return file;
|
||||
}
|
||||
});
|
||||
|
||||
// Compute B2G CLI arguments.
|
||||
Object.defineProperty(OASPp, "args", {
|
||||
get: function () {
|
||||
let args = [];
|
||||
|
||||
// Gaia profile.
|
||||
args.push("-profile", this.gaiaProfile.path);
|
||||
|
||||
// Debugger server port.
|
||||
let port = parseInt(this.options.port);
|
||||
args.push("-dbgport", "" + port);
|
||||
|
||||
// Ignore eventual zombie instances of b2g that are left over.
|
||||
args.push("-no-remote");
|
||||
|
||||
return args;
|
||||
}
|
||||
});
|
||||
|
||||
exports.OldAddonSimulatorProcess = OldAddonSimulatorProcess;
|
||||
368
devtools/client/webide/modules/simulators.js
Normal file
368
devtools/client/webide/modules/simulators.js
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
/* 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 { AddonManager } = require("resource://gre/modules/AddonManager.jsm");
|
||||
const { Task } = require("devtools/shared/task");
|
||||
loader.lazyRequireGetter(this, "ConnectionManager", "devtools/shared/client/connection-manager", true);
|
||||
loader.lazyRequireGetter(this, "AddonSimulatorProcess", "devtools/client/webide/modules/simulator-process", true);
|
||||
loader.lazyRequireGetter(this, "OldAddonSimulatorProcess", "devtools/client/webide/modules/simulator-process", true);
|
||||
loader.lazyRequireGetter(this, "CustomSimulatorProcess", "devtools/client/webide/modules/simulator-process", true);
|
||||
const asyncStorage = require("devtools/shared/async-storage");
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const promise = require("promise");
|
||||
const Services = require("Services");
|
||||
|
||||
const SimulatorRegExp = new RegExp(Services.prefs.getCharPref("devtools.webide.simulatorAddonRegExp"));
|
||||
const LocaleCompare = (a, b) => {
|
||||
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
||||
};
|
||||
|
||||
var Simulators = {
|
||||
|
||||
// The list of simulator configurations.
|
||||
_simulators: [],
|
||||
|
||||
/**
|
||||
* Load a previously saved list of configurations (only once).
|
||||
*
|
||||
* @return Promise.
|
||||
*/
|
||||
_load() {
|
||||
if (this._loadingPromise) {
|
||||
return this._loadingPromise;
|
||||
}
|
||||
|
||||
this._loadingPromise = Task.spawn(function* () {
|
||||
let jobs = [];
|
||||
|
||||
let value = yield asyncStorage.getItem("simulators");
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(options => {
|
||||
let simulator = new Simulator(options);
|
||||
Simulators.add(simulator, true);
|
||||
|
||||
// If the simulator had a reference to an addon, fix it.
|
||||
if (options.addonID) {
|
||||
let deferred = promise.defer();
|
||||
AddonManager.getAddonByID(options.addonID, addon => {
|
||||
simulator.addon = addon;
|
||||
delete simulator.options.addonID;
|
||||
deferred.resolve();
|
||||
});
|
||||
jobs.push(deferred.promise);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
yield promise.all(jobs);
|
||||
yield Simulators._addUnusedAddons();
|
||||
Simulators.emitUpdated();
|
||||
return Simulators._simulators;
|
||||
});
|
||||
|
||||
return this._loadingPromise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add default simulators to the list for each new (unused) addon.
|
||||
*
|
||||
* @return Promise.
|
||||
*/
|
||||
_addUnusedAddons: Task.async(function* () {
|
||||
let jobs = [];
|
||||
|
||||
let addons = yield Simulators.findSimulatorAddons();
|
||||
addons.forEach(addon => {
|
||||
jobs.push(Simulators.addIfUnusedAddon(addon, true));
|
||||
});
|
||||
|
||||
yield promise.all(jobs);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Save the current list of configurations.
|
||||
*
|
||||
* @return Promise.
|
||||
*/
|
||||
_save: Task.async(function* () {
|
||||
yield this._load();
|
||||
|
||||
let value = Simulators._simulators.map(simulator => {
|
||||
let options = JSON.parse(JSON.stringify(simulator.options));
|
||||
if (simulator.addon != null) {
|
||||
options.addonID = simulator.addon.id;
|
||||
}
|
||||
return options;
|
||||
});
|
||||
|
||||
yield asyncStorage.setItem("simulators", value);
|
||||
}),
|
||||
|
||||
/**
|
||||
* List all available simulators.
|
||||
*
|
||||
* @return Promised simulator list.
|
||||
*/
|
||||
findSimulators: Task.async(function* () {
|
||||
yield this._load();
|
||||
return Simulators._simulators;
|
||||
}),
|
||||
|
||||
/**
|
||||
* List all installed simulator addons.
|
||||
*
|
||||
* @return Promised addon list.
|
||||
*/
|
||||
findSimulatorAddons() {
|
||||
let deferred = promise.defer();
|
||||
AddonManager.getAllAddons(all => {
|
||||
let addons = [];
|
||||
for (let addon of all) {
|
||||
if (Simulators.isSimulatorAddon(addon)) {
|
||||
addons.push(addon);
|
||||
}
|
||||
}
|
||||
// Sort simulator addons by name.
|
||||
addons.sort(LocaleCompare);
|
||||
deferred.resolve(addons);
|
||||
});
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new simulator for `addon` if no other simulator uses it.
|
||||
*/
|
||||
addIfUnusedAddon(addon, silently = false) {
|
||||
let simulators = this._simulators;
|
||||
let matching = simulators.filter(s => s.addon && s.addon.id == addon.id);
|
||||
if (matching.length > 0) {
|
||||
return promise.resolve();
|
||||
}
|
||||
let options = {};
|
||||
options.name = addon.name.replace(" Simulator", "");
|
||||
// Some addons specify a simulator type at the end of their version string,
|
||||
// e.g. "2_5_tv".
|
||||
let type = this.simulatorAddonVersion(addon).split("_")[2];
|
||||
if (type) {
|
||||
// "tv" is shorthand for type "television".
|
||||
options.type = (type === "tv" ? "television" : type);
|
||||
}
|
||||
return this.add(new Simulator(options, addon), silently);
|
||||
},
|
||||
|
||||
// TODO (Bug 1146521) Maybe find a better way to deal with removed addons?
|
||||
removeIfUsingAddon(addon) {
|
||||
let simulators = this._simulators;
|
||||
let remaining = simulators.filter(s => !s.addon || s.addon.id != addon.id);
|
||||
this._simulators = remaining;
|
||||
if (remaining.length !== simulators.length) {
|
||||
this.emitUpdated();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new simulator to the list. Caution: `simulator.name` may be modified.
|
||||
*
|
||||
* @return Promise to added simulator.
|
||||
*/
|
||||
add(simulator, silently = false) {
|
||||
let simulators = this._simulators;
|
||||
let uniqueName = this.uniqueName(simulator.options.name);
|
||||
simulator.options.name = uniqueName;
|
||||
simulators.push(simulator);
|
||||
if (!silently) {
|
||||
this.emitUpdated();
|
||||
}
|
||||
return promise.resolve(simulator);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a simulator from the list.
|
||||
*/
|
||||
remove(simulator) {
|
||||
let simulators = this._simulators;
|
||||
let remaining = simulators.filter(s => s !== simulator);
|
||||
this._simulators = remaining;
|
||||
if (remaining.length !== simulators.length) {
|
||||
this.emitUpdated();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a unique name for a simulator (may add a suffix, e.g. "MyName (1)").
|
||||
*/
|
||||
uniqueName(name) {
|
||||
let simulators = this._simulators;
|
||||
|
||||
let names = {};
|
||||
simulators.forEach(simulator => names[simulator.name] = true);
|
||||
|
||||
// Strip any previous suffix, add a new suffix if necessary.
|
||||
let stripped = name.replace(/ \(\d+\)$/, "");
|
||||
let unique = stripped;
|
||||
for (let i = 1; names[unique]; i++) {
|
||||
unique = stripped + " (" + i + ")";
|
||||
}
|
||||
return unique;
|
||||
},
|
||||
|
||||
/**
|
||||
* Compare an addon's ID against the expected form of a simulator addon ID,
|
||||
* and try to extract its version if there is a match.
|
||||
*
|
||||
* Note: If a simulator addon is recognized, but no version can be extracted
|
||||
* (e.g. custom RegExp pref value), we return "Unknown" to keep the returned
|
||||
* value 'truthy'.
|
||||
*/
|
||||
simulatorAddonVersion(addon) {
|
||||
let match = SimulatorRegExp.exec(addon.id);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
let version = match[1];
|
||||
return version || "Unknown";
|
||||
},
|
||||
|
||||
/**
|
||||
* Detect simulator addons, including "unofficial" ones.
|
||||
*/
|
||||
isSimulatorAddon(addon) {
|
||||
return !!this.simulatorAddonVersion(addon);
|
||||
},
|
||||
|
||||
emitUpdated() {
|
||||
this.emit("updated", { length: this._simulators.length });
|
||||
this._simulators.sort(LocaleCompare);
|
||||
this._save();
|
||||
},
|
||||
|
||||
onConfigure(e, simulator) {
|
||||
this._lastConfiguredSimulator = simulator;
|
||||
},
|
||||
|
||||
onInstalled(addon) {
|
||||
if (this.isSimulatorAddon(addon)) {
|
||||
this.addIfUnusedAddon(addon);
|
||||
}
|
||||
},
|
||||
|
||||
onEnabled(addon) {
|
||||
if (this.isSimulatorAddon(addon)) {
|
||||
this.addIfUnusedAddon(addon);
|
||||
}
|
||||
},
|
||||
|
||||
onDisabled(addon) {
|
||||
if (this.isSimulatorAddon(addon)) {
|
||||
this.removeIfUsingAddon(addon);
|
||||
}
|
||||
},
|
||||
|
||||
onUninstalled(addon) {
|
||||
if (this.isSimulatorAddon(addon)) {
|
||||
this.removeIfUsingAddon(addon);
|
||||
}
|
||||
},
|
||||
};
|
||||
exports.Simulators = Simulators;
|
||||
AddonManager.addAddonListener(Simulators);
|
||||
EventEmitter.decorate(Simulators);
|
||||
Simulators.on("configure", Simulators.onConfigure.bind(Simulators));
|
||||
|
||||
function Simulator(options = {}, addon = null) {
|
||||
this.addon = addon;
|
||||
this.options = options;
|
||||
|
||||
// Fill `this.options` with default values where needed.
|
||||
let defaults = this.defaults;
|
||||
for (let option in defaults) {
|
||||
if (this.options[option] == null) {
|
||||
this.options[option] = defaults[option];
|
||||
}
|
||||
}
|
||||
}
|
||||
Simulator.prototype = {
|
||||
|
||||
// Default simulation options.
|
||||
_defaults: {
|
||||
// Based on the Firefox OS Flame.
|
||||
phone: {
|
||||
width: 320,
|
||||
height: 570,
|
||||
pixelRatio: 1.5
|
||||
},
|
||||
// Based on a 720p HD TV.
|
||||
television: {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
pixelRatio: 1,
|
||||
}
|
||||
},
|
||||
_defaultType: "phone",
|
||||
|
||||
restoreDefaults() {
|
||||
let defaults = this.defaults;
|
||||
let options = this.options;
|
||||
for (let option in defaults) {
|
||||
options[option] = defaults[option];
|
||||
}
|
||||
},
|
||||
|
||||
launch() {
|
||||
// Close already opened simulation.
|
||||
if (this.process) {
|
||||
return this.kill().then(this.launch.bind(this));
|
||||
}
|
||||
|
||||
this.options.port = ConnectionManager.getFreeTCPPort();
|
||||
|
||||
// Choose simulator process type.
|
||||
if (this.options.b2gBinary) {
|
||||
// Custom binary.
|
||||
this.process = new CustomSimulatorProcess(this.options);
|
||||
} else if (this.version > "1.3") {
|
||||
// Recent simulator addon.
|
||||
this.process = new AddonSimulatorProcess(this.addon, this.options);
|
||||
} else {
|
||||
// Old simulator addon.
|
||||
this.process = new OldAddonSimulatorProcess(this.addon, this.options);
|
||||
}
|
||||
this.process.run();
|
||||
|
||||
return promise.resolve(this.options.port);
|
||||
},
|
||||
|
||||
kill() {
|
||||
let process = this.process;
|
||||
if (!process) {
|
||||
return promise.resolve();
|
||||
}
|
||||
this.process = null;
|
||||
return process.kill();
|
||||
},
|
||||
|
||||
get defaults() {
|
||||
let defaults = this._defaults;
|
||||
return defaults[this.type] || defaults[this._defaultType];
|
||||
},
|
||||
|
||||
get id() {
|
||||
return this.name;
|
||||
},
|
||||
|
||||
get name() {
|
||||
return this.options.name;
|
||||
},
|
||||
|
||||
get type() {
|
||||
return this.options.type || this._defaultType;
|
||||
},
|
||||
|
||||
get version() {
|
||||
return this.options.b2gBinary ? "Custom" : this.addon.name.match(/\d+\.\d+/)[0];
|
||||
},
|
||||
};
|
||||
exports.Simulator = Simulator;
|
||||
178
devtools/client/webide/modules/tab-store.js
Normal file
178
devtools/client/webide/modules/tab-store.js
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
/* 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 { TargetFactory } = require("devtools/client/framework/target");
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const { Connection } = require("devtools/shared/client/connection-manager");
|
||||
const promise = require("promise");
|
||||
const { Task } = require("devtools/shared/task");
|
||||
|
||||
const _knownTabStores = new WeakMap();
|
||||
|
||||
var TabStore;
|
||||
|
||||
module.exports = TabStore = function (connection) {
|
||||
// If we already know about this connection,
|
||||
// let's re-use the existing store.
|
||||
if (_knownTabStores.has(connection)) {
|
||||
return _knownTabStores.get(connection);
|
||||
}
|
||||
|
||||
_knownTabStores.set(connection, this);
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
|
||||
this._resetStore();
|
||||
|
||||
this.destroy = this.destroy.bind(this);
|
||||
this._onStatusChanged = this._onStatusChanged.bind(this);
|
||||
|
||||
this._connection = connection;
|
||||
this._connection.once(Connection.Events.DESTROYED, this.destroy);
|
||||
this._connection.on(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
|
||||
this._onTabListChanged = this._onTabListChanged.bind(this);
|
||||
this._onTabNavigated = this._onTabNavigated.bind(this);
|
||||
this._onStatusChanged();
|
||||
return this;
|
||||
};
|
||||
|
||||
TabStore.prototype = {
|
||||
|
||||
destroy: function () {
|
||||
if (this._connection) {
|
||||
// While this.destroy is bound using .once() above, that event may not
|
||||
// have occurred when the TabStore client calls destroy, so we
|
||||
// manually remove it here.
|
||||
this._connection.off(Connection.Events.DESTROYED, this.destroy);
|
||||
this._connection.off(Connection.Events.STATUS_CHANGED, this._onStatusChanged);
|
||||
_knownTabStores.delete(this._connection);
|
||||
this._connection = null;
|
||||
}
|
||||
},
|
||||
|
||||
_resetStore: function () {
|
||||
this.response = null;
|
||||
this.tabs = [];
|
||||
this._selectedTab = null;
|
||||
this._selectedTabTargetPromise = null;
|
||||
},
|
||||
|
||||
_onStatusChanged: function () {
|
||||
if (this._connection.status == Connection.Status.CONNECTED) {
|
||||
// Watch for changes to remote browser tabs
|
||||
this._connection.client.addListener("tabListChanged",
|
||||
this._onTabListChanged);
|
||||
this._connection.client.addListener("tabNavigated",
|
||||
this._onTabNavigated);
|
||||
this.listTabs();
|
||||
} else {
|
||||
if (this._connection.client) {
|
||||
this._connection.client.removeListener("tabListChanged",
|
||||
this._onTabListChanged);
|
||||
this._connection.client.removeListener("tabNavigated",
|
||||
this._onTabNavigated);
|
||||
}
|
||||
this._resetStore();
|
||||
}
|
||||
},
|
||||
|
||||
_onTabListChanged: function () {
|
||||
this.listTabs().then(() => this.emit("tab-list"))
|
||||
.catch(console.error);
|
||||
},
|
||||
|
||||
_onTabNavigated: function (e, { from, title, url }) {
|
||||
if (!this._selectedTab || from !== this._selectedTab.actor) {
|
||||
return;
|
||||
}
|
||||
this._selectedTab.url = url;
|
||||
this._selectedTab.title = title;
|
||||
this.emit("navigate");
|
||||
},
|
||||
|
||||
listTabs: function () {
|
||||
if (!this._connection || !this._connection.client) {
|
||||
return promise.reject(new Error("Can't listTabs, not connected."));
|
||||
}
|
||||
let deferred = promise.defer();
|
||||
this._connection.client.listTabs(response => {
|
||||
if (response.error) {
|
||||
this._connection.disconnect();
|
||||
deferred.reject(response.error);
|
||||
return;
|
||||
}
|
||||
let tabsChanged = JSON.stringify(this.tabs) !== JSON.stringify(response.tabs);
|
||||
this.response = response;
|
||||
this.tabs = response.tabs;
|
||||
this._checkSelectedTab();
|
||||
if (tabsChanged) {
|
||||
this.emit("tab-list");
|
||||
}
|
||||
deferred.resolve(response);
|
||||
});
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
// TODO: Tab "selection" should really take place by creating a TabProject
|
||||
// which is the selected project. This should be done as part of the
|
||||
// project-agnostic work.
|
||||
_selectedTab: null,
|
||||
_selectedTabTargetPromise: null,
|
||||
get selectedTab() {
|
||||
return this._selectedTab;
|
||||
},
|
||||
set selectedTab(tab) {
|
||||
if (this._selectedTab === tab) {
|
||||
return;
|
||||
}
|
||||
this._selectedTab = tab;
|
||||
this._selectedTabTargetPromise = null;
|
||||
// Attach to the tab to follow navigation events
|
||||
if (this._selectedTab) {
|
||||
this.getTargetForTab();
|
||||
}
|
||||
},
|
||||
|
||||
_checkSelectedTab: function () {
|
||||
if (!this._selectedTab) {
|
||||
return;
|
||||
}
|
||||
let alive = this.tabs.some(tab => {
|
||||
return tab.actor === this._selectedTab.actor;
|
||||
});
|
||||
if (!alive) {
|
||||
this._selectedTab = null;
|
||||
this._selectedTabTargetPromise = null;
|
||||
this.emit("closed");
|
||||
}
|
||||
},
|
||||
|
||||
getTargetForTab: function () {
|
||||
if (this._selectedTabTargetPromise) {
|
||||
return this._selectedTabTargetPromise;
|
||||
}
|
||||
let store = this;
|
||||
this._selectedTabTargetPromise = Task.spawn(function* () {
|
||||
// If you connect to a tab, then detach from it, the root actor may have
|
||||
// de-listed the actors that belong to the tab. This breaks the toolbox
|
||||
// if you try to connect to the same tab again. To work around this
|
||||
// issue, we force a "listTabs" request before connecting to a tab.
|
||||
yield store.listTabs();
|
||||
return TargetFactory.forRemoteTab({
|
||||
form: store._selectedTab,
|
||||
client: store._connection.client,
|
||||
chrome: false
|
||||
});
|
||||
});
|
||||
this._selectedTabTargetPromise.then(target => {
|
||||
target.once("close", () => {
|
||||
this._selectedTabTargetPromise = null;
|
||||
});
|
||||
});
|
||||
return this._selectedTabTargetPromise;
|
||||
},
|
||||
|
||||
};
|
||||
68
devtools/client/webide/modules/utils.js
Normal file
68
devtools/client/webide/modules/utils.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/* 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 { Cc, Cu, Ci } = require("chrome");
|
||||
const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm", {});
|
||||
const Services = require("Services");
|
||||
const Strings = Services.strings.createBundle("chrome://devtools/locale/webide.properties");
|
||||
|
||||
function doesFileExist(location) {
|
||||
let file = new FileUtils.File(location);
|
||||
return file.exists();
|
||||
}
|
||||
exports.doesFileExist = doesFileExist;
|
||||
|
||||
function _getFile(location, ...pickerParams) {
|
||||
if (location) {
|
||||
return new FileUtils.File(location);
|
||||
}
|
||||
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
|
||||
fp.init(...pickerParams);
|
||||
let res = fp.show();
|
||||
if (res == Ci.nsIFilePicker.returnCancel) {
|
||||
return null;
|
||||
}
|
||||
return fp.file;
|
||||
}
|
||||
|
||||
function getCustomBinary(window, location) {
|
||||
return _getFile(location, window, Strings.GetStringFromName("selectCustomBinary_title"), Ci.nsIFilePicker.modeOpen);
|
||||
}
|
||||
exports.getCustomBinary = getCustomBinary;
|
||||
|
||||
function getCustomProfile(window, location) {
|
||||
return _getFile(location, window, Strings.GetStringFromName("selectCustomProfile_title"), Ci.nsIFilePicker.modeGetFolder);
|
||||
}
|
||||
exports.getCustomProfile = getCustomProfile;
|
||||
|
||||
function getPackagedDirectory(window, location) {
|
||||
return _getFile(location, window, Strings.GetStringFromName("importPackagedApp_title"), Ci.nsIFilePicker.modeGetFolder);
|
||||
}
|
||||
exports.getPackagedDirectory = getPackagedDirectory;
|
||||
|
||||
function getHostedURL(window, location) {
|
||||
let ret = { value: null };
|
||||
|
||||
if (!location) {
|
||||
Services.prompt.prompt(window,
|
||||
Strings.GetStringFromName("importHostedApp_title"),
|
||||
Strings.GetStringFromName("importHostedApp_header"),
|
||||
ret, null, {});
|
||||
location = ret.value;
|
||||
}
|
||||
|
||||
if (!location) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Clean location string and add "http://" if missing
|
||||
location = location.trim();
|
||||
try { // Will fail if no scheme
|
||||
Services.io.extractScheme(location);
|
||||
} catch (e) {
|
||||
location = "http://" + location;
|
||||
}
|
||||
return location;
|
||||
}
|
||||
exports.getHostedURL = getHostedURL;
|
||||
Loading…
Add table
Add a link
Reference in a new issue