diff options
Diffstat (limited to 'devtools/client/webide/modules/app-manager.js')
-rw-r--r-- | devtools/client/webide/modules/app-manager.js | 850 |
1 files changed, 850 insertions, 0 deletions
diff --git a/devtools/client/webide/modules/app-manager.js b/devtools/client/webide/modules/app-manager.js new file mode 100644 index 000000000..88dfcdd44 --- /dev/null +++ b/devtools/client/webide/modules/app-manager.js @@ -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); |