diff --git a/browser/base/content/test/Makefile.in b/browser/base/content/test/Makefile.in index 1dc05eaed6d..cbed37e6921 100644 --- a/browser/base/content/test/Makefile.in +++ b/browser/base/content/test/Makefile.in @@ -51,6 +51,7 @@ endif # The following tests are disabled because they are unreliable: # browser_bug423833.js is bug 428712 # browser_sanitize-download-history.js is bug 432425 +# browser_aboutHome.js is bug 890409 # # browser_sanitizeDialog_treeView.js is disabled until the tree view is added # back to the clear recent history dialog (sanitize.xul), if it ever is (bug @@ -71,7 +72,6 @@ MOCHITEST_BROWSER_FILES = \ blockPluginVulnerableNoUpdate.xml \ blockPluginVulnerableUpdatable.xml \ browser_aboutHealthReport.js \ - browser_aboutHome.js \ browser_aboutSyncProgress.js \ browser_addKeywordSearch.js \ browser_addon_bar_aomlistener.js \ diff --git a/browser/base/content/test/browser_aboutHome.js b/browser/base/content/test/browser_aboutHome.js index deb229be479..12c3080a6b3 100644 --- a/browser/base/content/test/browser_aboutHome.js +++ b/browser/base/content/test/browser_aboutHome.js @@ -89,27 +89,8 @@ let gTests = [ }, { - desc: "Check that performing a search fires a search event.", - setup: function () { }, - run: function () { - let deferred = Promise.defer(); - let doc = gBrowser.contentDocument; - - doc.addEventListener("AboutHomeSearchEvent", function onSearch(e) { - is(e.detail, doc.documentElement.getAttribute("searchEngineName"), "Detail is search engine name"); - - gBrowser.stop(); - deferred.resolve(); - }, true, true); - - doc.getElementById("searchText").value = "it works"; - doc.getElementById("searchSubmit").click(); - return deferred.promise; - } -}, - -{ - desc: "Check that performing a search records to Firefox Health Report.", + desc: "Check that performing a search fires a search event and records to " + + "Firefox Health Report.", setup: function () { }, run: function () { try { @@ -120,46 +101,33 @@ let gTests = [ return Promise.resolve(); } + let numSearchesBefore = 0; let deferred = Promise.defer(); let doc = gBrowser.contentDocument; // We rely on the listener in browser.js being installed and fired before // this one. If this ever changes, we should add an executeSoon() or similar. doc.addEventListener("AboutHomeSearchEvent", function onSearch(e) { - executeSoon(gBrowser.stop.bind(gBrowser)); - let reporter = Components.classes["@mozilla.org/datareporting/service;1"] - .getService() - .wrappedJSObject - .healthReporter; - ok(reporter, "Health Reporter instance available."); + let engineName = doc.documentElement.getAttribute("searchEngineName"); + is(e.detail, engineName, "Detail is search engine name"); - reporter.onInit().then(function onInit() { - let provider = reporter.getProvider("org.mozilla.searches"); - ok(provider, "Searches provider is available."); - - let engineName = doc.documentElement.getAttribute("searchEngineName"); - let id = Services.search.getEngineByName(engineName).identifier; - - let m = provider.getMeasurement("counts", 2); - m.getValues().then(function onValues(data) { - let now = new Date(); - ok(data.days.hasDay(now), "Have data for today."); - - let day = data.days.getDay(now); - let field = id + ".abouthome"; - ok(day.has(field), "Have data for about home on this engine."); - - // Note the search from the previous test. - is(day.get(field), 2, "Have searches recorded."); - - deferred.resolve(); - }); + gBrowser.stop(); + getNumberOfSearches().then(num => { + is(num, numSearchesBefore + 1, "One more search recorded."); + deferred.resolve(); }); }, true, true); - doc.getElementById("searchText").value = "a search"; - doc.getElementById("searchSubmit").click(); + // Get the current number of recorded searches. + getNumberOfSearches().then(num => { + numSearchesBefore = num; + + info("Perform a search."); + doc.getElementById("searchText").value = "a search"; + doc.getElementById("searchSubmit").click(); + }); + return deferred.promise; } }, @@ -422,3 +390,54 @@ function promiseBrowserAttributes(aTab) return deferred.promise; } + +/** + * Retrieves the number of about:home searches recorded for the current day. + * + * @return {Promise} Returns a promise resolving to the number of searches. + */ +function getNumberOfSearches() { + let reporter = Components.classes["@mozilla.org/datareporting/service;1"] + .getService() + .wrappedJSObject + .healthReporter; + ok(reporter, "Health Reporter instance available."); + + return reporter.onInit().then(function onInit() { + let provider = reporter.getProvider("org.mozilla.searches"); + ok(provider, "Searches provider is available."); + + let m = provider.getMeasurement("counts", 2); + return m.getValues().then(data => { + let now = new Date(); + let yday = new Date(now); + yday.setDate(yday.getDate() - 1); + + // Add the number of searches recorded yesterday to the number of searches + // recorded today. This makes the test not fail intermittently when it is + // run at midnight and we accidentally compare the number of searches from + // different days. Tests are always run with an empty profile so there + // are no searches from yesterday, normally. Should the test happen to run + // past midnight we make sure to count them in as well. + return getNumberOfSearchesByDate(data, now) + + getNumberOfSearchesByDate(data, yday); + }); + }); +} + +function getNumberOfSearchesByDate(aData, aDate) { + if (aData.days.hasDay(aDate)) { + let doc = gBrowser.contentDocument; + let engineName = doc.documentElement.getAttribute("searchEngineName"); + let id = Services.search.getEngineByName(engineName).identifier; + + let day = aData.days.getDay(aDate); + let field = id + ".abouthome"; + + if (day.has(field)) { + return day.get(field) || 0; + } + } + + return 0; // No records found. +} diff --git a/browser/components/sessionstore/src/SessionStore.jsm b/browser/components/sessionstore/src/SessionStore.jsm index 11932edd82a..8e92478278b 100644 --- a/browser/components/sessionstore/src/SessionStore.jsm +++ b/browser/components/sessionstore/src/SessionStore.jsm @@ -460,7 +460,11 @@ let SessionStoreInternal = { // A Lazy getter for the sessionstore.js backup promise. XPCOMUtils.defineLazyGetter(this, "_backupSessionFileOnce", function () { - return _SessionFile.createBackupCopy(); + // We're creating a backup of sessionstore.js by moving it to .bak + // because that's a lot faster than creating a copy. sessionstore.js + // would be overwritten shortly afterwards anyway so we can save time + // and just move instead of copy. + return _SessionFile.moveToBackupPath(); }); // at this point, we've as good as resumed the session, so we can @@ -504,12 +508,12 @@ let SessionStoreInternal = { return Task.spawn(function task() { try { // Perform background backup - yield _SessionFile.createUpgradeBackupCopy("-" + buildID); + yield _SessionFile.createBackupCopy("-" + buildID); this._prefBranch.setCharPref(PREF_UPGRADE, buildID); // In case of success, remove previous backup. - yield _SessionFile.removeUpgradeBackup("-" + latestBackup); + yield _SessionFile.removeBackupCopy("-" + latestBackup); } catch (ex) { debug("Could not perform upgrade backup " + ex); debug(ex.stack); @@ -772,12 +776,12 @@ let SessionStoreInternal = { this._restoreCount = this._initialState.windows ? this._initialState.windows.length : 0; this.restoreWindow(aWindow, this._initialState, this._isCmdLineEmpty(aWindow, this._initialState)); - - // _loadState changed from "stopped" to "running" - // force a save operation so that crashes happening during startup are correctly counted - this._initialState.session.state = STATE_RUNNING_STR; - this._saveStateObject(this._initialState); this._initialState = null; + + // _loadState changed from "stopped" to "running". Save the session's + // load state immediately so that crashes happening during startup + // are correctly counted. + _SessionFile.writeLoadStateOnceAfterStartup(STATE_RUNNING_STR); } } else { diff --git a/browser/components/sessionstore/src/SessionWorker.js b/browser/components/sessionstore/src/SessionWorker.js new file mode 100644 index 00000000000..a4d17813ce9 --- /dev/null +++ b/browser/components/sessionstore/src/SessionWorker.js @@ -0,0 +1,221 @@ +/* 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/. */ + +/** + * A worker dedicated to handle I/O for Session Store. + */ + +"use strict"; + +importScripts("resource://gre/modules/osfile.jsm"); + +let File = OS.File; +let Encoder = new TextEncoder(); +let Decoder = new TextDecoder(); + +/** + * Communications with the controller. + * + * Accepts messages: + * {fun:function_name, args:array_of_arguments_or_null, id: custom_id} + * + * Sends messages: + * {ok: result, id: custom_id} / {fail: serialized_form_of_OS.File.Error, + * id: custom_id} + */ +self.onmessage = function (msg) { + let data = msg.data; + if (!(data.fun in Agent)) { + throw new Error("Cannot find method " + data.fun); + } + + let result; + let id = data.id; + + try { + result = Agent[data.fun].apply(Agent, data.args); + } catch (ex if ex instanceof OS.File.Error) { + // Instances of OS.File.Error know how to serialize themselves + // (deserialization ensures that we end up with OS-specific + // instances of |OS.File.Error|) + self.postMessage({fail: OS.File.Error.toMsg(ex), id: id}); + return; + } + + // Other exceptions do not, and should be propagated through DOM's + // built-in mechanism for uncaught errors, although this mechanism + // may lose interesting information. + self.postMessage({ok: result, id: id}); +}; + +let Agent = { + // The initial session string as read from disk. + initialState: null, + + // Boolean that tells whether we already wrote + // the loadState to disk once after startup. + hasWrittenLoadStateOnce: false, + + // The path to sessionstore.js + path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"), + + // The path to sessionstore.bak + backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"), + + /** + * This method is only intended to be called by _SessionFile.syncRead() and + * can be removed when we're not supporting synchronous SessionStore + * initialization anymore. When sessionstore.js is read from disk + * synchronously the state string must be supplied to the worker manually by + * calling this method. + */ + setInitialState: function (aState) { + // _SessionFile.syncRead() should not be called after startup has finished. + // Thus we also don't support any setInitialState() calls after we already + // wrote the loadState to disk. + if (this.hasWrittenLoadStateOnce) { + throw new Error("writeLoadStateOnceAfterStartup() must only be called once."); + } + + // Initial state might have been filled by read() already but yet we might + // be called by _SessionFile.syncRead() before SessionStore.jsm had a chance + // to call writeLoadStateOnceAfterStartup(). It's safe to ignore + // setInitialState() calls if this happens. + if (!this.initialState) { + this.initialState = aState; + } + }, + + /** + * Read the session from disk. + * In case sessionstore.js does not exist, attempt to read sessionstore.bak. + */ + read: function () { + for (let path of [this.path, this.backupPath]) { + try { + return this.initialState = Decoder.decode(File.read(path)); + } catch (ex if isNoSuchFileEx(ex)) { + // Ignore exceptions about non-existent files. + } + } + + // No sessionstore data files found. Return an empty string. + return ""; + }, + + /** + * Write the session to disk. + */ + write: function (stateString) { + let bytes = Encoder.encode(stateString); + return File.writeAtomic(this.path, bytes, {tmpPath: this.path + ".tmp"}); + }, + + /** + * Writes the session state to disk again but changes session.state to + * 'running' before doing so. This is intended to be called only once, shortly + * after startup so that we detect crashes on startup correctly. + */ + writeLoadStateOnceAfterStartup: function (loadState) { + if (this.hasWrittenLoadStateOnce) { + throw new Error("writeLoadStateOnceAfterStartup() must only be called once."); + } + + if (!this.initialState) { + throw new Error("writeLoadStateOnceAfterStartup() must not be called " + + "without a valid session state or before it has been " + + "read from disk."); + } + + // Make sure we can't call this function twice. + this.hasWrittenLoadStateOnce = true; + + let state; + try { + state = JSON.parse(this.initialState); + } finally { + this.initialState = null; + } + + state.session = state.session || {}; + state.session.state = loadState; + return this.write(JSON.stringify(state)); + }, + + /** + * Moves sessionstore.js to sessionstore.bak. + */ + moveToBackupPath: function () { + try { + return File.move(this.path, this.backupPath); + } catch (ex if isNoSuchFileEx(ex)) { + // Ignore exceptions about non-existent files. + return true; + } + }, + + /** + * Creates a copy of sessionstore.js. + */ + createBackupCopy: function (ext) { + try { + return File.copy(this.path, this.backupPath + ext); + } catch (ex if isNoSuchFileEx(ex)) { + // Ignore exceptions about non-existent files. + return true; + } + }, + + /** + * Removes a backup copy. + */ + removeBackupCopy: function (ext) { + try { + return File.remove(this.backupPath + ext); + } catch (ex if isNoSuchFileEx(ex)) { + // Ignore exceptions about non-existent files. + return true; + } + }, + + /** + * Wipes all files holding session data from disk. + */ + wipe: function () { + let exn; + + // Erase session state file + try { + File.remove(this.path); + } catch (ex if isNoSuchFileEx(ex)) { + // Ignore exceptions about non-existent files. + } catch (ex) { + // Don't stop immediately. + exn = ex; + } + + // Erase any backup, any file named "sessionstore.bak[-buildID]". + let iter = new File.DirectoryIterator(OS.Constants.Path.profileDir); + for (let entry in iter) { + if (!entry.isDir && entry.path.startsWith(this.backupPath)) { + try { + File.remove(entry.path); + } catch (ex) { + // Don't stop immediately. + exn = exn || ex; + } + } + } + + if (exn) { + throw exn; + } + + return true; + } +}; + +function isNoSuchFileEx(aReason) { + return aReason instanceof OS.File.Error && aReason.becauseNoSuchFile; +} diff --git a/browser/components/sessionstore/src/_SessionFile.jsm b/browser/components/sessionstore/src/_SessionFile.jsm index 83afb92d4f4..4a69b36be47 100644 --- a/browser/components/sessionstore/src/_SessionFile.jsm +++ b/browser/components/sessionstore/src/_SessionFile.jsm @@ -32,6 +32,7 @@ const Ci = Components.interfaces; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/osfile.jsm"); +Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this); Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js"); XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch", @@ -44,66 +45,62 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "Telemetry", "@mozilla.org/base/telemetry;1", "nsITelemetry"); - -// An encoder to UTF-8. -XPCOMUtils.defineLazyGetter(this, "gEncoder", function () { - return new TextEncoder(); -}); -// A decoder. -XPCOMUtils.defineLazyGetter(this, "gDecoder", function () { - return new TextDecoder(); -}); +XPCOMUtils.defineLazyModuleGetter(this, "Deprecated", + "resource://gre/modules/Deprecated.jsm"); this._SessionFile = { - /** - * A promise fulfilled once initialization (either synchronous or - * asynchronous) is complete. - */ - promiseInitialized: function SessionFile_initialized() { - return SessionFileInternal.promiseInitialized; - }, /** * Read the contents of the session file, asynchronously. */ - read: function SessionFile_read() { + read: function () { return SessionFileInternal.read(); }, /** * Read the contents of the session file, synchronously. */ - syncRead: function SessionFile_syncRead() { + syncRead: function () { + Deprecated.warning( + "syncRead is deprecated and will be removed in a future version", + "https://bugzilla.mozilla.org/show_bug.cgi?id=532150") return SessionFileInternal.syncRead(); }, /** * Write the contents of the session file, asynchronously. */ - write: function SessionFile_write(aData) { + write: function (aData) { return SessionFileInternal.write(aData); }, + /** + * Writes the initial state to disk again only to change the session's load + * state. This must only be called once, it will throw an error otherwise. + */ + writeLoadStateOnceAfterStartup: function (aLoadState) { + return SessionFileInternal.writeLoadStateOnceAfterStartup(aLoadState); + }, /** * Create a backup copy, asynchronously. */ - createBackupCopy: function SessionFile_createBackupCopy() { - return SessionFileInternal.createBackupCopy(); + moveToBackupPath: function () { + return SessionFileInternal.moveToBackupPath(); }, /** * Create a backup copy, asynchronously. * This is designed to perform backup on upgrade. */ - createUpgradeBackupCopy: function(ext) { - return SessionFileInternal.createUpgradeBackupCopy(ext); + createBackupCopy: function (ext) { + return SessionFileInternal.createBackupCopy(ext); }, /** * Remove a backup copy, asynchronously. * This is designed to clean up a backup on upgrade. */ - removeUpgradeBackup: function(ext) { - return SessionFileInternal.removeUpgradeBackup(ext); + removeBackupCopy: function (ext) { + return SessionFileInternal.removeBackupCopy(ext); }, /** * Wipe the contents of the session file, asynchronously. */ - wipe: function SessionFile_wipe() { + wipe: function () { return SessionFileInternal.wipe(); } }; @@ -147,11 +144,6 @@ const TaskUtils = { }; let SessionFileInternal = { - /** - * A promise fulfilled once initialization is complete - */ - promiseInitialized: Promise.defer(), - /** * The path to sessionstore.js */ @@ -168,7 +160,7 @@ let SessionFileInternal = { * A path to read the file from. * @returns string if successful, undefined otherwise. */ - readAuxSync: function ssfi_readAuxSync(aPath) { + readAuxSync: function (aPath) { let text; try { let file = new FileUtils.File(aPath); @@ -197,7 +189,7 @@ let SessionFileInternal = { * happened between backup and write), attempt to read the sessionstore.bak * instead. */ - syncRead: function ssfi_syncRead() { + syncRead: function () { // Start measuring the duration of the synchronous read. TelemetryStopwatch.start("FX_SESSION_RESTORE_SYNC_READ_FILE_MS"); // First read the sessionstore.js. @@ -208,83 +200,26 @@ let SessionFileInternal = { } // Finish the telemetry probe and return an empty string. TelemetryStopwatch.finish("FX_SESSION_RESTORE_SYNC_READ_FILE_MS"); - return text || ""; + text = text || ""; + + // The worker needs to know the initial state read from + // disk so that writeLoadStateOnceAfterStartup() works. + SessionWorker.post("setInitialState", [text]); + return text; }, - /** - * Utility function to safely read a file asynchronously. - * @param aPath - * A path to read the file from. - * @param aReadOptions - * Read operation options. - * |outExecutionDuration| option will be reused and can be - * incrementally updated by the worker process. - * @returns string if successful, undefined otherwise. - */ - readAux: function ssfi_readAux(aPath, aReadOptions) { - let self = this; - return TaskUtils.spawn(function () { - let text; - try { - let bytes = yield OS.File.read(aPath, undefined, aReadOptions); - text = gDecoder.decode(bytes); - // If the file is read successfully, add a telemetry probe based on - // the updated duration value of the |outExecutionDuration| option. - let histogram = Telemetry.getHistogramById( - "FX_SESSION_RESTORE_READ_FILE_MS"); - histogram.add(aReadOptions.outExecutionDuration); - } catch (ex if self._isNoSuchFile(ex)) { - // Ignore exceptions about non-existent files. - } catch (ex) { - Cu.reportError(ex); - } - throw new Task.Result(text); - }); + read: function () { + return SessionWorker.post("read").then(msg => msg.ok); }, - /** - * Read the sessionstore file asynchronously. - * - * In case sessionstore.js file does not exist or is corrupted (something - * happened between backup and write), attempt to read the sessionstore.bak - * instead. - */ - read: function ssfi_read() { - let self = this; - return TaskUtils.spawn(function task() { - // Specify |outExecutionDuration| option to hold the combined duration of - // the asynchronous reads off the main thread (of both sessionstore.js and - // sessionstore.bak, if necessary). If sessionstore.js does not exist or - // is corrupted, |outExecutionDuration| will register the time it took to - // attempt to read the file. It will then be subsequently incremented by - // the read time of sessionsore.bak. - let readOptions = { - outExecutionDuration: null - }; - // First read the sessionstore.js. - let text = yield self.readAux(self.path, readOptions); - if (typeof text === "undefined") { - // If sessionstore.js does not exist or is corrupted, read the - // sessionstore.bak. - text = yield self.readAux(self.backupPath, readOptions); - } - // Return either the content of the sessionstore.bak if it was read - // successfully or an empty string otherwise. - throw new Task.Result(text || ""); - }); - }, - - write: function ssfi_write(aData) { + write: function (aData) { let refObj = {}; - let self = this; return TaskUtils.spawn(function task() { TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj); TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj); - let bytes = gEncoder.encode(aData); - try { - let promise = OS.File.writeAtomic(self.path, bytes, {tmpPath: self.path + ".tmp"}); + let promise = SessionWorker.post("write", [aData]); // At this point, we measure how long we stop the main thread TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj); @@ -294,93 +229,51 @@ let SessionFileInternal = { } catch (ex) { TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj); TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_MS", refObj); - Cu.reportError("Could not write session state file " + self.path - + ": " + aReason); - } - }); - }, - - createBackupCopy: function ssfi_createBackupCopy() { - let backupCopyOptions = { - outExecutionDuration: null - }; - let self = this; - return TaskUtils.spawn(function task() { - try { - yield OS.File.move(self.path, self.backupPath, backupCopyOptions); - Telemetry.getHistogramById("FX_SESSION_RESTORE_BACKUP_FILE_MS").add( - backupCopyOptions.outExecutionDuration); - } catch (ex if self._isNoSuchFile(ex)) { - // Ignore exceptions about non-existent files. - } catch (ex) { - Cu.reportError("Could not backup session state file: " + ex); - throw ex; - } - }); - }, - - createUpgradeBackupCopy: function(ext) { - return TaskUtils.spawn(function task() { - try { - yield OS.File.copy(this.path, this.backupPath + ext); - } catch (ex if this._isNoSuchFile(ex)) { - // Ignore exceptions about non-existent files. - } catch (ex) { - Cu.reportError("Could not backup session state file to " + - dest + ": " + ex); - throw ex; + Cu.reportError("Could not write session state file " + this.path + + ": " + ex); } }.bind(this)); }, - removeUpgradeBackup: function(ext) { - return TaskUtils.spawn(function task() { - try { - yield OS.File.remove(this.backupPath + ext); - } catch (ex if this._isNoSuchFile(ex)) { - // Ignore exceptions about non-existent files. - } - }.bind(this)); + writeLoadStateOnceAfterStartup: function (aLoadState) { + return SessionWorker.post("writeLoadStateOnceAfterStartup", [aLoadState]); }, - wipe: function ssfi_wipe() { - let self = this; - return TaskUtils.spawn(function task() { - let exn; - // Erase session state file - try { - yield OS.File.remove(self.path); - } catch (ex if self._isNoSuchFile(ex)) { - // Ignore exceptions about non-existent files. - } catch (ex) { - // Report error, don't stop immediately - Cu.reportError("Could not remove session state file: " + ex); - exn = ex; - } - - // Erase any backup, any file named "sessionstore.bak[-buildID]". - let iterator = new OS.File.DirectoryIterator(OS.Constants.Path.profileDir); - for (let promise of iterator) { - let entry = yield promise; - if (!entry.isDir && entry.path.startsWith(self.backupPath)) { - try { - yield OS.File.remove(entry.path); - } catch (ex) { - // Report error, don't stop immediately - Cu.reportError("Could not remove backup file " + entry.path + " : " + ex); - exn = exn || ex; - } - } - } - - if (exn) { - throw exn; - } - }); - + moveToBackupPath: function () { + return SessionWorker.post("moveToBackupPath"); }, - _isNoSuchFile: function ssfi_isNoSuchFile(aReason) { - return aReason instanceof OS.File.Error && aReason.becauseNoSuchFile; + createBackupCopy: function (ext) { + return SessionWorker.post("createBackupCopy", [ext]); + }, + + removeBackupCopy: function (ext) { + return SessionWorker.post("removeBackupCopy", [ext]); + }, + + wipe: function () { + return SessionWorker.post("wipe"); } }; + +// Interface to a dedicated thread handling I/O +let SessionWorker = (function () { + let worker = new PromiseWorker("resource:///modules/sessionstore/SessionWorker.js", + OS.Shared.LOG.bind("SessionWorker")); + return { + post: function post(...args) { + let promise = worker.post.apply(worker, args); + return promise.then( + null, + function onError(error) { + // Decode any serialized error + if (error instanceof PromiseWorker.WorkerError) { + throw OS.File.Error.fromMsg(error.data); + } else { + throw error; + } + } + ); + } + }; +})(); diff --git a/browser/components/sessionstore/src/moz.build b/browser/components/sessionstore/src/moz.build index bdd2ee4c104..41e67e1b765 100644 --- a/browser/components/sessionstore/src/moz.build +++ b/browser/components/sessionstore/src/moz.build @@ -16,6 +16,7 @@ EXTRA_JS_MODULES = [ 'DocumentUtils.jsm', 'SessionMigration.jsm', 'SessionStorage.jsm', + 'SessionWorker.js', 'XPathGenerator.jsm', '_SessionFile.jsm', ] diff --git a/browser/components/sessionstore/test/browser_354894_perwindowpb.js b/browser/components/sessionstore/test/browser_354894_perwindowpb.js index 78e861ba08f..d2814dfe721 100644 --- a/browser/components/sessionstore/test/browser_354894_perwindowpb.js +++ b/browser/components/sessionstore/test/browser_354894_perwindowpb.js @@ -196,18 +196,13 @@ function test() { options = {private: true}; } - let newWin = OpenBrowserWindow(options); - newWin.addEventListener("load", function(aEvent) { - newWin.removeEventListener("load", arguments.callee, false); - newWin.gBrowser.addEventListener("load", function(aEvent) { - newWin.gBrowser.removeEventListener("load", arguments.callee, true); - TEST_URLS.forEach(function (url) { - newWin.gBrowser.addTab(url); - }); + whenNewWindowLoaded(options, function (newWin) { + TEST_URLS.forEach(function (url) { + newWin.gBrowser.addTab(url); + }); - executeSoon(function() testFn(newWin)); - }, true); - }, false); + executeSoon(() => testFn(newWin)); + }); } /** @@ -230,20 +225,16 @@ function test() { // Open a new window // The previously closed window should be restored - newWin = OpenBrowserWindow({}); - newWin.addEventListener("load", function() { - this.removeEventListener("load", arguments.callee, true); - executeSoon(function() { - is(newWin.gBrowser.browsers.length, TEST_URLS.length + 1, - "Restored window in-session with otherpopup windows around"); + whenNewWindowLoaded({}, function (newWin) { + is(newWin.gBrowser.browsers.length, TEST_URLS.length + 1, + "Restored window in-session with otherpopup windows around"); - // Cleanup - newWin.close(); + // Cleanup + newWin.close(); - // Next please - executeSoon(nextFn); - }); - }, true); + // Next please + executeSoon(nextFn); + }); }); } @@ -259,32 +250,24 @@ function test() { // Enter private browsing mode // Open a new window. // The previously closed window should NOT be restored - newWin = OpenBrowserWindow({private: true}); - newWin.addEventListener("load", function() { - this.removeEventListener("load", arguments.callee, true); - executeSoon(function() { - is(newWin.gBrowser.browsers.length, 1, - "Did not restore in private browing mode"); + whenNewWindowLoaded({private: true}, function (newWin) { + is(newWin.gBrowser.browsers.length, 1, + "Did not restore in private browing mode"); - // Cleanup - newWin.BrowserTryToCloseWindow(); + // Cleanup + newWin.BrowserTryToCloseWindow(); - // Exit private browsing mode again - newWin = OpenBrowserWindow({}); - newWin.addEventListener("load", function() { - this.removeEventListener("load", arguments.callee, true); - executeSoon(function() { - is(newWin.gBrowser.browsers.length, TEST_URLS.length + 1, - "Restored after leaving private browsing again"); + // Exit private browsing mode again + whenNewWindowLoaded({}, function (newWin) { + is(newWin.gBrowser.browsers.length, TEST_URLS.length + 1, + "Restored after leaving private browsing again"); - newWin.close(); + newWin.close(); - // Next please - executeSoon(nextFn); - }); - }, true); + // Next please + executeSoon(nextFn); }); - }, true); + }); }); } @@ -312,21 +295,17 @@ function test() { popup2.close(); // open a new window the previously closed window should be restored to - newWin = OpenBrowserWindow({}); - newWin.addEventListener("load", function() { - this.removeEventListener("load", arguments.callee, true); - executeSoon(function() { - is(newWin.gBrowser.browsers.length, TEST_URLS.length + 1, - "Restored window and associated tabs in session"); + whenNewWindowLoaded({}, function (newWin) { + is(newWin.gBrowser.browsers.length, TEST_URLS.length + 1, + "Restored window and associated tabs in session"); - // Cleanup - newWin.close(); - popup.close(); + // Cleanup + newWin.close(); + popup.close(); - // Next please - executeSoon(nextFn); - }); - }, true); + // Next please + executeSoon(nextFn); + }); }, true); }, false); }); @@ -366,22 +345,18 @@ function test() { // but instead a new window is opened without restoring anything popup.close(); - let newWin = OpenBrowserWindow({}); - newWin.addEventListener("load", function() { - newWin.removeEventListener("load", arguments.callee, true); - executeSoon(function() { - isnot(newWin.gBrowser.browsers.length, 2, - "Did not restore the popup window"); - is(TEST_URLS.indexOf(newWin.gBrowser.browsers[0].currentURI.spec), -1, - "Did not restore the popup window (2)"); + whenNewWindowLoaded({}, function (newWin) { + isnot(newWin.gBrowser.browsers.length, 2, + "Did not restore the popup window"); + is(TEST_URLS.indexOf(newWin.gBrowser.browsers[0].currentURI.spec), -1, + "Did not restore the popup window (2)"); - // Cleanup - newWin.close(); + // Cleanup + newWin.close(); - // Next please - executeSoon(nextFn); - }); - }, true); + // Next please + executeSoon(nextFn); + }); }, true); }, false); }, true); @@ -402,27 +377,23 @@ function test() { newWin = undoCloseWindow(0); - newWin2 = OpenBrowserWindow({}); - newWin2.addEventListener("load", function() { - newWin2.removeEventListener("load", arguments.callee, true); - executeSoon(function() { - is(newWin2.gBrowser.browsers.length, 1, - "Did not restore, as undoCloseWindow() was last called"); - is(TEST_URLS.indexOf(newWin2.gBrowser.browsers[0].currentURI.spec), -1, - "Did not restore, as undoCloseWindow() was last called (2)"); + whenNewWindowLoaded({}, function (newWin2) { + is(newWin2.gBrowser.browsers.length, 1, + "Did not restore, as undoCloseWindow() was last called"); + is(TEST_URLS.indexOf(newWin2.gBrowser.browsers[0].currentURI.spec), -1, + "Did not restore, as undoCloseWindow() was last called (2)"); - browserWindowsCount([2, 3], "browser windows while running testOpenCloseRestoreFromPopup"); + browserWindowsCount([2, 3], "browser windows while running testOpenCloseRestoreFromPopup"); - // Cleanup - newWin.close(); - newWin2.close(); + // Cleanup + newWin.close(); + newWin2.close(); - browserWindowsCount([0, 1], "browser windows while running testOpenCloseRestoreFromPopup"); + browserWindowsCount([0, 1], "browser windows while running testOpenCloseRestoreFromPopup"); - // Next please - executeSoon(nextFn); - }); - }, true); + // Next please + executeSoon(nextFn); + }); }); }); } diff --git a/browser/components/sessionstore/test/browser_524745.js b/browser/components/sessionstore/test/browser_524745.js index b531f4c8d2f..d984ec38b8c 100644 --- a/browser/components/sessionstore/test/browser_524745.js +++ b/browser/components/sessionstore/test/browser_524745.js @@ -10,37 +10,34 @@ function test() { waitForExplicitFinish(); - let window_B = openDialog(location, "_blank", "chrome,all,dialog=no"); - window_B.addEventListener("load", function(aEvent) { - window_B.removeEventListener("load", arguments.callee, false); + whenNewWindowLoaded({ private: false }, function (window_B) { + waitForFocus(function() { + // Add identifying information to window_B + ss.setWindowValue(window_B, uniqKey, uniqVal); + let state = JSON.parse(ss.getBrowserState()); + let selectedWindow = state.windows[state.selectedWindow - 1]; + is(selectedWindow.extData && selectedWindow.extData[uniqKey], uniqVal, + "selectedWindow is window_B"); + // Now minimize window_B. The selected window shouldn't have the secret data + window_B.minimize(); waitForFocus(function() { - // Add identifying information to window_B - ss.setWindowValue(window_B, uniqKey, uniqVal); - let state = JSON.parse(ss.getBrowserState()); - let selectedWindow = state.windows[state.selectedWindow - 1]; - is(selectedWindow.extData && selectedWindow.extData[uniqKey], uniqVal, - "selectedWindow is window_B"); + state = JSON.parse(ss.getBrowserState()); + selectedWindow = state.windows[state.selectedWindow - 1]; + ok(!selectedWindow.extData || !selectedWindow.extData[uniqKey], + "selectedWindow is not window_B after minimizing it"); - // Now minimize window_B. The selected window shouldn't have the secret data - window_B.minimize(); - waitForFocus(function() { - state = JSON.parse(ss.getBrowserState()); - selectedWindow = state.windows[state.selectedWindow - 1]; - ok(!selectedWindow.extData || !selectedWindow.extData[uniqKey], - "selectedWindow is not window_B after minimizing it"); + // Now minimize the last open window (assumes no other tests left windows open) + window.minimize(); + state = JSON.parse(ss.getBrowserState()); + is(state.selectedWindow, 0, + "selectedWindow should be 0 when all windows are minimized"); - // Now minimize the last open window (assumes no other tests left windows open) - window.minimize(); - state = JSON.parse(ss.getBrowserState()); - is(state.selectedWindow, 0, - "selectedWindow should be 0 when all windows are minimized"); - - // Cleanup - window.restore(); - window_B.close(); - finish(); - }); - }, window_B); - }, false); + // Cleanup + window.restore(); + window_B.close(); + finish(); + }); + }, window_B); + }); } diff --git a/browser/components/sessionstore/test/browser_819510_perwindowpb.js b/browser/components/sessionstore/test/browser_819510_perwindowpb.js index 26937134c68..087ea4f4a29 100644 --- a/browser/components/sessionstore/test/browser_819510_perwindowpb.js +++ b/browser/components/sessionstore/test/browser_819510_perwindowpb.js @@ -174,23 +174,7 @@ function forceWriteState(aCallback) { } function testOnWindow(aIsPrivate, aCallback) { - let win = OpenBrowserWindow({private: aIsPrivate}); - let gotLoad = false; - let gotActivate = false; - win.addEventListener("activate", function onActivate() { - win.removeEventListener("activate", onActivate, false); - gotActivate = true; - if (gotLoad) { - executeSoon(function() { aCallback(win) }); - } - }, false); - win.addEventListener("load", function onLoad() { - win.removeEventListener("load", onLoad, false); - gotLoad = true; - if (gotActivate) { - executeSoon(function() { aCallback(win) }); - } - }, false); + whenNewWindowLoaded({private: aIsPrivate}, aCallback); } function waitForTabLoad(aWin, aURL, aCallback) { diff --git a/browser/components/sessionstore/test/browser_833286_atomic_backup.js b/browser/components/sessionstore/test/browser_833286_atomic_backup.js index bc21ae39983..a04a6ed8ce5 100644 --- a/browser/components/sessionstore/test/browser_833286_atomic_backup.js +++ b/browser/components/sessionstore/test/browser_833286_atomic_backup.js @@ -78,9 +78,9 @@ function testWriteNoBackup() { let array = yield OS.File.read(path); gSSData = gDecoder.decode(array); - // Manually trigger _SessionFile.createBackupCopy since the backup once + // Manually trigger _SessionFile.moveToBackupPath since the backup once // promise is already resolved and backup would not be triggered again. - yield _SessionFile.createBackupCopy(); + yield _SessionFile.moveToBackupPath(); nextTest(testWriteBackup); } @@ -140,4 +140,4 @@ function testNoWriteBackup() { is(ssBakData, gSSBakData, "sessionstore.bak is unchanged."); executeSoon(finish); -} \ No newline at end of file +} diff --git a/browser/components/sessionstore/test/browser_dying_cache.js b/browser/components/sessionstore/test/browser_dying_cache.js index 41e22d864d3..169a07539ab 100644 --- a/browser/components/sessionstore/test/browser_dying_cache.js +++ b/browser/components/sessionstore/test/browser_dying_cache.js @@ -14,10 +14,11 @@ function test() { function runTests() { // Open a new window. let win = OpenBrowserWindow(); - yield whenWindowLoaded(win); + yield whenDelayedStartupFinished(win, next); // Load some URL in the current tab. - win.gBrowser.selectedBrowser.loadURI("about:robots"); + let flags = Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY; + win.gBrowser.selectedBrowser.loadURIWithFlags("about:robots", flags); yield whenBrowserLoaded(win.gBrowser.selectedBrowser); // Open a second tab and close the first one. diff --git a/browser/components/sessionstore/test/browser_input.js b/browser/components/sessionstore/test/browser_input.js index 56681ca4fe9..3c135eecec0 100644 --- a/browser/components/sessionstore/test/browser_input.js +++ b/browser/components/sessionstore/test/browser_input.js @@ -19,7 +19,7 @@ function runTests() { // because we always collect data for tabs of active windows no matter if // the window is dirty or not. let win = OpenBrowserWindow(); - yield waitForLoad(win); + yield whenDelayedStartupFinished(win, next); // Create a tab with some form fields. let tab = gBrowser.selectedTab = gBrowser.addTab(URL); diff --git a/browser/components/sessionstore/test/browser_pageshow.js b/browser/components/sessionstore/test/browser_pageshow.js index 0130288e368..3560b61af59 100644 --- a/browser/components/sessionstore/test/browser_pageshow.js +++ b/browser/components/sessionstore/test/browser_pageshow.js @@ -23,7 +23,7 @@ function runTests() { // because we always collect data for tabs of active windows no matter if // the window is dirty or not. let win = OpenBrowserWindow(); - yield waitForLoad(win); + yield whenDelayedStartupFinished(win, next); // Create a tab with two history entries. let tab = gBrowser.selectedTab = gBrowser.addTab("about:blank"); diff --git a/browser/components/sessionstore/test/browser_windowRestore_perwindowpb.js b/browser/components/sessionstore/test/browser_windowRestore_perwindowpb.js index 3ee34513816..ebe774b5b16 100644 --- a/browser/components/sessionstore/test/browser_windowRestore_perwindowpb.js +++ b/browser/components/sessionstore/test/browser_windowRestore_perwindowpb.js @@ -13,9 +13,7 @@ function test() { // Load a private window, then close it // and verify it doesn't get remembered for restoring - var win = OpenBrowserWindow({private: true}); - - whenWindowLoaded(win, function onload() { + whenNewWindowLoaded({private: true}, function (win) { info("The private window got loaded"); win.addEventListener("SSWindowClosing", function onclosing() { win.removeEventListener("SSWindowClosing", onclosing, false); diff --git a/browser/components/sessionstore/test/head.js b/browser/components/sessionstore/test/head.js index 85c62a9a172..ac2849b581b 100644 --- a/browser/components/sessionstore/test/head.js +++ b/browser/components/sessionstore/test/head.js @@ -289,39 +289,34 @@ function closeAllButPrimaryWindow() { } } +/** + * When opening a new window it is not sufficient to wait for its load event. + * We need to use whenDelayedStartupFinshed() here as the browser window's + * delayedStartup() routine is executed one tick after the window's load event + * has been dispatched. browser-delayed-startup-finished might be deferred even + * further if parts of the window's initialization process take more time than + * expected (e.g. reading a big session state from disk). + */ function whenNewWindowLoaded(aOptions, aCallback) { let win = OpenBrowserWindow(aOptions); - let gotLoad = false; - let gotActivate = (Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager).activeWindow == win); - - function maybeRunCallback() { - if (gotLoad && gotActivate) { - win.BrowserChromeTest.runWhenReady(function() { - executeSoon(function() { aCallback(win); }); - }); - } - } - - if (!gotActivate) { - win.addEventListener("activate", function onActivate() { - info("Got activate."); - win.removeEventListener("activate", onActivate, false); - gotActivate = true; - maybeRunCallback(); - }, false); - } else { - info("Was activated."); - } - - win.addEventListener("load", function onLoad() { - info("Got load"); - win.removeEventListener("load", onLoad, false); - gotLoad = true; - maybeRunCallback(); - }, false); + whenDelayedStartupFinished(win, () => aCallback(win)); return win; } +/** + * This waits for the browser-delayed-startup-finished notification of a given + * window. It indicates that the windows has loaded completely and is ready to + * be used for testing. + */ +function whenDelayedStartupFinished(aWindow, aCallback) { + Services.obs.addObserver(function observer(aSubject, aTopic) { + if (aWindow == aSubject) { + Services.obs.removeObserver(observer, aTopic); + executeSoon(aCallback); + } + }, "browser-delayed-startup-finished", false); +} + /** * The test runner that controls the execution flow of our tests. */ diff --git a/browser/components/sessionstore/test/unit/test_backup.js b/browser/components/sessionstore/test/unit/test_backup.js index 5efa4358c72..9e25b966cf9 100644 --- a/browser/components/sessionstore/test/unit/test_backup.js +++ b/browser/components/sessionstore/test/unit/test_backup.js @@ -19,7 +19,7 @@ function pathBackup(ext) { // Ensure that things proceed smoothly if there is no file to back up add_task(function test_nothing_to_backup() { - yield _SessionFile.createUpgradeBackupCopy(""); + yield _SessionFile.createBackupCopy(""); }); // Create a file, back it up, remove it @@ -29,14 +29,14 @@ add_task(function test_do_backup() { yield OS.File.writeAtomic(pathStore, content, {tmpPath: pathStore + ".tmp"}); do_print("Ensuring that the backup is created"); - yield _SessionFile.createUpgradeBackupCopy(ext); + yield _SessionFile.createBackupCopy(ext); do_check_true((yield OS.File.exists(pathBackup(ext)))); let data = yield OS.File.read(pathBackup(ext)); do_check_eq((new TextDecoder()).decode(data), content); do_print("Ensuring that we can remove the backup"); - yield _SessionFile.removeUpgradeBackup(ext); + yield _SessionFile.removeBackupCopy(ext); do_check_false((yield OS.File.exists(pathBackup(ext)))); }); diff --git a/browser/devtools/profiler/commands.js b/browser/devtools/profiler/commands.js index cbbb9282ffe..e94414bb8c9 100644 --- a/browser/devtools/profiler/commands.js +++ b/browser/devtools/profiler/commands.js @@ -69,7 +69,7 @@ gcli.addCommand({ throw gcli.lookup("profilerAlreadyStarted2"); panel.toggleRecording(); - return gcli.lookup("profilerStarted"); + return gcli.lookup("profilerStarted2"); } return gDevTools.showToolbox(context.environment.target, "jsprofiler") diff --git a/browser/devtools/profiler/test/browser_profiler_cmd.js b/browser/devtools/profiler/test/browser_profiler_cmd.js index 42e00ae3092..db0af1dc1bb 100644 --- a/browser/devtools/profiler/test/browser_profiler_cmd.js +++ b/browser/devtools/profiler/test/browser_profiler_cmd.js @@ -48,7 +48,7 @@ function testProfilerStart() { deferred.resolve(); }); - cmd("profiler start", gcli.lookup("profilerStarted")); + cmd("profiler start", gcli.lookup("profilerStarted2")); return deferred.promise; } diff --git a/browser/devtools/shared/Parser.jsm b/browser/devtools/shared/Parser.jsm index 42b32c07cb2..ef302a83d58 100644 --- a/browser/devtools/shared/Parser.jsm +++ b/browser/devtools/shared/Parser.jsm @@ -1474,7 +1474,9 @@ let SyntaxTreeVisitor = { aCallbacks.onArrayExpression(aNode); } for (let element of aNode.elements) { - if (element) { + // TODO: remove the typeof check when support for SpreadExpression is + // added (bug 890913). + if (element && typeof this[element.type] == "function") { this[element.type](element, aNode, aCallbacks); } } diff --git a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties index 5b1a9ce47e0..ceda6a70e6b 100644 --- a/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties +++ b/browser/locales/en-US/chrome/browser/devtools/gclicommands.properties @@ -1251,9 +1251,9 @@ profilerNotFound=Profile not found # start the profiler. profilerNotStarted3=Profiler has not been started yet. Use 'profile start' to start profiling -# LOCALIZATION NOTE (profilerStarted) A very short string that indicates that +# LOCALIZATION NOTE (profilerStarted2) A very short string that indicates that # we have started recording. -profilerStarted=Recording... +profilerStarted2=Recording… # LOCALIZATION NOTE (profilerNotReady) A message that is displayed whenever # an operation cannot be completed because the profiler has not been opened yet. diff --git a/toolkit/devtools/server/actors/script.js b/toolkit/devtools/server/actors/script.js index e2460515e9f..fa64ab587f0 100644 --- a/toolkit/devtools/server/actors/script.js +++ b/toolkit/devtools/server/actors/script.js @@ -2849,7 +2849,7 @@ function fetch(aURL, aOptions={ loadFromCache: true }) { try { NetUtil.asyncFetch(url, function onFetch(aStream, aStatus) { if (!Components.isSuccessCode(aStatus)) { - deferred.reject("Request failed: " + url); + deferred.reject(new Error("Request failed: " + url)); return; } @@ -2858,7 +2858,7 @@ function fetch(aURL, aOptions={ loadFromCache: true }) { aStream.close(); }); } catch (ex) { - deferred.reject("Request failed: " + url); + deferred.reject(new Error("Request failed: " + url)); } break; @@ -2876,7 +2876,7 @@ function fetch(aURL, aOptions={ loadFromCache: true }) { let streamListener = { onStartRequest: function(aRequest, aContext, aStatusCode) { if (!Components.isSuccessCode(aStatusCode)) { - deferred.reject("Request failed: " + url); + deferred.reject(new Error("Request failed: " + url)); } }, onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) { @@ -2884,7 +2884,7 @@ function fetch(aURL, aOptions={ loadFromCache: true }) { }, onStopRequest: function(aRequest, aContext, aStatusCode) { if (!Components.isSuccessCode(aStatusCode)) { - deferred.reject("Request failed: " + url); + deferred.reject(new Error("Request failed: " + url)); return; } @@ -2934,6 +2934,7 @@ function convertToUnicode(aString, aCharset=null) { * An optional prefix for the reported error message. */ function reportError(aError, aPrefix="") { + dbg_assert(aError instanceof Error, "Must pass Error objects to reportError"); let msg = aPrefix + aError.message + ":\n" + aError.stack; Cu.reportError(msg); dumpn(msg); diff --git a/toolkit/devtools/server/main.js b/toolkit/devtools/server/main.js index 9544bc858b9..66ec873da06 100644 --- a/toolkit/devtools/server/main.js +++ b/toolkit/devtools/server/main.js @@ -24,8 +24,7 @@ let wantLogging = Services.prefs.getBoolPref("devtools.debugger.log"); Cu.import("resource://gre/modules/jsdebugger.jsm"); addDebuggerToGlobal(this); -let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise; -const { defer, resolve, reject, all } = promise; +loadSubScript.call(this, "resource://gre/modules/commonjs/sdk/core/promise.js"); Cu.import("resource://gre/modules/devtools/SourceMap.jsm"); @@ -198,11 +197,11 @@ var DebuggerServer = { get initialized() this._initialized, /** - * Performs cleanup tasks before shutting down the debugger server, if no - * connections are currently open. Such tasks include clearing any actor - * constructors added at runtime. This method should be called whenever a - * debugger server is no longer useful, to avoid memory leaks. After this - * method returns, the debugger server must be initialized again before use. + * Performs cleanup tasks before shutting down the debugger server. Such tasks + * include clearing any actor constructors added at runtime. This method + * should be called whenever a debugger server is no longer useful, to avoid + * memory leaks. After this method returns, the debugger server must be + * initialized again before use. */ destroy: function DS_destroy() { if (!this._initialized) {