diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 34d6b5aa9ede..296fac9ec881 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -306,6 +306,9 @@ pref("browser.urlbar.trimURLs", true); pref("browser.altClickSave", false); +// Enable logging downloads operations to the Error Console. +pref("browser.download.debug", false); + // Number of milliseconds to wait for the http headers (and thus // the Content-Disposition filename) before giving up and falling back to // picking a filename without that info in hand so that the user sees some diff --git a/browser/components/downloads/content/downloads.js b/browser/components/downloads/content/downloads.js index bdb10a93f13d..9260af8f3ee9 100644 --- a/browser/components/downloads/content/downloads.js +++ b/browser/components/downloads/content/downloads.js @@ -121,7 +121,9 @@ const DownloadsPanel = { */ initialize: function DP_initialize(aCallback) { + DownloadsCommon.log("Attempting to initialize DownloadsPanel for a window."); if (this._state != this.kStateUninitialized) { + DownloadsCommon.log("DownloadsPanel is already initialized."); DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay, aCallback); return; @@ -137,11 +139,16 @@ const DownloadsPanel = { // Now that data loading has eventually started, load the required XUL // elements and initialize our views. + DownloadsCommon.log("Ensuring DownloadsPanel overlay loaded."); DownloadsOverlayLoader.ensureOverlayLoaded(this.kDownloadsOverlay, function DP_I_callback() { DownloadsViewController.initialize(); + DownloadsCommon.log("Attaching DownloadsView..."); DownloadsCommon.getData(window).addView(DownloadsView); + DownloadsCommon.log("DownloadsView attached - the panel for this window", + "should now see download items come in."); DownloadsPanel._attachEventListeners(); + DownloadsCommon.log("DownloadsPanel initialized."); aCallback(); }); }, @@ -153,7 +160,9 @@ const DownloadsPanel = { */ terminate: function DP_terminate() { + DownloadsCommon.log("Attempting to terminate DownloadsPanel for a window."); if (this._state == this.kStateUninitialized) { + DownloadsCommon.log("DownloadsPanel was never initialized. Nothing to do."); return; } @@ -169,6 +178,7 @@ const DownloadsPanel = { this._state = this.kStateUninitialized; DownloadsSummary.active = false; + DownloadsCommon.log("DownloadsPanel terminated."); }, ////////////////////////////////////////////////////////////////////////////// @@ -191,7 +201,10 @@ const DownloadsPanel = { */ showPanel: function DP_showPanel() { + DownloadsCommon.log("Opening the downloads panel."); + if (this.isPanelShowing) { + DownloadsCommon.log("Panel is already showing - focusing instead."); this._focusPanel(); return; } @@ -204,6 +217,7 @@ const DownloadsPanel = { setTimeout(function () DownloadsPanel._openPopupIfDataReady(), 0); }.bind(this)); + DownloadsCommon.log("Waiting for the downloads panel to appear."); this._state = this.kStateWaitingData; }, @@ -213,7 +227,10 @@ const DownloadsPanel = { */ hidePanel: function DP_hidePanel() { + DownloadsCommon.log("Closing the downloads panel."); + if (!this.isPanelShowing) { + DownloadsCommon.log("Downloads panel is not showing - nothing to do."); return; } @@ -223,6 +240,7 @@ const DownloadsPanel = { // was open, then the onPopupHidden event handler has already updated the // current state, otherwise we must update the state ourselves. this._state = this.kStateHidden; + DownloadsCommon.log("Downloads panel is now closed."); }, /** @@ -298,6 +316,7 @@ const DownloadsPanel = { return; } + DownloadsCommon.log("Downloads panel has shown."); this._state = this.kStateShown; // Since at most one popup is open at any given time, we can set globally. @@ -319,6 +338,8 @@ const DownloadsPanel = { return; } + DownloadsCommon.log("Downloads panel has hidden."); + // Removes the keyfocus attribute so that we stop handling keyboard // navigation. this.keyFocusing = false; @@ -341,6 +362,7 @@ const DownloadsPanel = { */ showDownloadsHistory: function DP_showDownloadsHistory() { + DownloadsCommon.log("Showing download history."); // Hide the panel before showing another window, otherwise focus will return // to the browser window when the panel closes automatically. this.hidePanel(); @@ -445,6 +467,8 @@ const DownloadsPanel = { return; } + DownloadsCommon.log("Received a paste event."); + let trans = Cc["@mozilla.org/widget/transferable;1"] .createInstance(Ci.nsITransferable); trans.init(null); @@ -464,6 +488,7 @@ const DownloadsPanel = { } let uri = NetUtil.newURI(url); + DownloadsCommon.log("Pasted URL seems valid. Starting download."); saveURL(uri.spec, name || uri.spec, null, true, true, undefined, document); } catch (ex) {} @@ -525,9 +550,13 @@ const DownloadsPanel = { } if (aAnchor) { + DownloadsCommon.log("Opening downloads panel popup."); this.panel.openPopup(aAnchor, "bottomcenter topright", 0, 0, false, null); } else { + DownloadsCommon.error("We can't find the anchor! Failure case - opening", + "downloads panel on TabsToolbar. We should never", + "get here!"); Components.utils.reportError( "Downloads button cannot be found"); } @@ -597,6 +626,7 @@ const DownloadsOverlayLoader = { } this._overlayLoading = true; + DownloadsCommon.log("Loading overlay ", aOverlay); document.loadOverlay(aOverlay, DOL_EOL_loadCallback.bind(this)); }, @@ -663,12 +693,16 @@ const DownloadsView = { */ _itemCountChanged: function DV_itemCountChanged() { + DownloadsCommon.log("The downloads item count has changed - we are tracking", + this._dataItems.length, "downloads in total."); let count = this._dataItems.length; let hiddenCount = count - this.kItemCountLimit; if (count > 0) { + DownloadsCommon.log("Setting the panel's hasdownloads attribute to true."); DownloadsPanel.panel.setAttribute("hasdownloads", "true"); } else { + DownloadsCommon.log("Removing the panel's hasdownloads attribute."); DownloadsPanel.panel.removeAttribute("hasdownloads"); } @@ -704,6 +738,7 @@ const DownloadsView = { */ onDataLoadStarting: function DV_onDataLoadStarting() { + DownloadsCommon.log("onDataLoadStarting called for DownloadsView."); this.loading = true; }, @@ -712,6 +747,8 @@ const DownloadsView = { */ onDataLoadCompleted: function DV_onDataLoadCompleted() { + DownloadsCommon.log("onDataLoadCompleted called for DownloadsView."); + this.loading = false; // We suppressed item count change notifications during the batch load, at @@ -730,6 +767,9 @@ const DownloadsView = { */ onDataInvalidated: function DV_onDataInvalidated() { + DownloadsCommon.log("Downloads data has been invalidated. Cleaning up", + "DownloadsView."); + DownloadsPanel.terminate(); // Clear the list by replacing with a shallow copy. @@ -755,6 +795,9 @@ const DownloadsView = { */ onDataItemAdded: function DV_onDataItemAdded(aDataItem, aNewest) { + DownloadsCommon.log("A new download data item was added - aNewest =", + aNewest); + if (aNewest) { this._dataItems.unshift(aDataItem); } else { @@ -790,6 +833,8 @@ const DownloadsView = { */ onDataItemRemoved: function DV_onDataItemRemoved(aDataItem) { + DownloadsCommon.log("A download data item was removed."); + let itemIndex = this._dataItems.indexOf(aDataItem); this._dataItems.splice(itemIndex, 1); @@ -837,6 +882,9 @@ const DownloadsView = { */ _addViewItem: function DV_addViewItem(aDataItem, aNewest) { + DownloadsCommon.log("Adding a new DownloadsViewItem to the downloads list.", + "aNewest =", aNewest); + let element = document.createElement("richlistitem"); let viewItem = new DownloadsViewItem(aDataItem, element); this._viewItems[aDataItem.downloadGuid] = viewItem; @@ -852,6 +900,7 @@ const DownloadsView = { */ _removeViewItem: function DV_removeViewItem(aDataItem) { + DownloadsCommon.log("Removing a DownloadsViewItem from the downloads list."); let element = this.getViewItem(aDataItem)._element; let previousSelectedIndex = this.richListBox.selectedIndex; this.richListBox.removeChild(element); diff --git a/browser/components/downloads/src/DownloadsCommon.jsm b/browser/components/downloads/src/DownloadsCommon.jsm index dd64cff492bd..c0952e7096cc 100644 --- a/browser/components/downloads/src/DownloadsCommon.jsm +++ b/browser/components/downloads/src/DownloadsCommon.jsm @@ -59,6 +59,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow", "resource:///modules/RecentWindow.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.jsm"); +XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger", + "resource:///modules/DownloadsLogger.jsm"); const nsIDM = Ci.nsIDownloadManager; @@ -90,6 +92,22 @@ XPCOMUtils.defineLazyGetter(this, "DownloadsLocalFileCtor", function () { const kPartialDownloadSuffix = ".part"; +const kPrefDebug = "browser.download.debug"; + +let DebugPrefObserver = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference]), + observe: function PDO_observe(aSubject, aTopic, aData) { + this.debugEnabled = Services.prefs.getBoolPref(kPrefDebug); + } +} + +XPCOMUtils.defineLazyGetter(DebugPrefObserver, "debugEnabled", function () { + Services.prefs.addObserver(kPrefDebug, DebugPrefObserver, true); + return Services.prefs.getBoolPref(kPrefDebug); +}); + + //////////////////////////////////////////////////////////////////////////////// //// DownloadsCommon @@ -98,6 +116,27 @@ const kPartialDownloadSuffix = ".part"; * and provides shared methods for all the instances of the user interface. */ this.DownloadsCommon = { + log: function DC_log(...aMessageArgs) { + delete this.log; + this.log = function DC_log(...aMessageArgs) { + if (!DebugPrefObserver.debugEnabled) { + return; + } + DownloadsLogger.log.apply(DownloadsLogger, aMessageArgs); + } + this.log.apply(this, aMessageArgs); + }, + + error: function DC_error(...aMessageArgs) { + delete this.error; + this.error = function DC_error(...aMessageArgs) { + if (!DebugPrefObserver.debugEnabled) { + return; + } + DownloadsLogger.reportError.apply(DownloadsLogger, aMessageArgs); + } + this.error.apply(this, aMessageArgs); + }, /** * Returns an object whose keys are the string names from the downloads string * bundle, and whose values are either the translated strings or functions @@ -687,7 +726,8 @@ DownloadsDataCtor.prototype = { return existingItem; } } - + DownloadsCommon.log("Creating a new DownloadsDataItem with downloadGuid =", + downloadGuid); let dataItem = new DownloadsDataItem(aSource); this.dataItems[downloadGuid] = dataItem; @@ -759,6 +799,7 @@ DownloadsDataCtor.prototype = { if (aActiveOnly) { if (this._loadState == this.kLoadNone) { + DownloadsCommon.log("Loading only active downloads from the persistence database"); // Indicate to the views that a batch loading operation is in progress. this._views.forEach( function (view) view.onDataLoadStarting() @@ -777,6 +818,7 @@ DownloadsDataCtor.prototype = { this._views.forEach( function (view) view.onDataLoadCompleted() ); + DownloadsCommon.log("Active downloads done loading."); } } else { if (this._loadState != this.kLoadAll) { @@ -784,6 +826,7 @@ DownloadsDataCtor.prototype = { // columns are read in the _initFromDataRow method of DownloadsDataItem. // Order by descending download identifier so that the most recent // downloads are notified first to the listening views. + DownloadsCommon.log("Loading all downloads from the persistence database."); let dbConnection = Services.downloads.DBConnection; let statement = dbConnection.createAsyncStatement( "SELECT guid, target, name, source, referrer, state, " @@ -834,12 +877,14 @@ DownloadsDataCtor.prototype = { handleError: function DD_handleError(aError) { - Cu.reportError("Database statement execution error (" + aError.result + - "): " + aError.message); + DownloadsCommon.error("Database statement execution error (", + aError.result, "): ", aError.message); }, handleCompletion: function DD_handleCompletion(aReason) { + DownloadsCommon.log("Loading all downloads from database completed with reason:", + aReason); this._pendingStatement = null; // To ensure that we don't inadvertently delete more downloads from the @@ -868,13 +913,16 @@ DownloadsDataCtor.prototype = { case "download-manager-remove-download-guid": // If a single download was removed, remove the corresponding data item. if (aSubject) { - this._removeDataItem(aSubject.QueryInterface(Ci.nsISupportsCString) - .data); + let downloadGuid = aSubject.data.QueryInterface(Ci.nsISupportsCString); + DownloadsCommon.log("A single download with id", + downloadGuid, "was removed."); + this._removeDataItem(downloadGuid); break; } // Multiple downloads have been removed. Iterate over known downloads // and remove those that don't exist anymore. + DownloadsCommon.log("Multiple downloads were removed."); for each (let dataItem in this.dataItems) { if (dataItem) { // Bug 449811 - We have to bind to the dataItem because Javascript @@ -883,6 +931,8 @@ DownloadsDataCtor.prototype = { Services.downloads.getDownloadByGUID(dataItemBinding.downloadGuid, function(aStatus, aResult) { if (aStatus == Components.results.NS_ERROR_NOT_AVAILABLE) { + DownloadsCommon.log("Removing download with id", + dataItemBinding.downloadGuid); this._removeDataItem(dataItemBinding.downloadGuid); } }.bind(this)); @@ -916,6 +966,7 @@ DownloadsDataCtor.prototype = { let wasInProgress = dataItem.inProgress; + DownloadsCommon.log("A download changed its state to:", aDownload.state); dataItem.state = aDownload.state; dataItem.referrer = aDownload.referrer && aDownload.referrer.spec; dataItem.resumable = aDownload.resumable; @@ -1034,7 +1085,9 @@ DownloadsDataCtor.prototype = { */ _notifyDownloadEvent: function DD_notifyDownloadEvent(aType) { + DownloadsCommon.log("Attempting to notify that a new download has started or finished."); if (DownloadsCommon.useToolkitUI) { + DownloadsCommon.log("Cancelling notification - we're using the toolkit downloads manager."); return; } @@ -1048,6 +1101,7 @@ DownloadsDataCtor.prototype = { // For new downloads after the first one, don't show the panel // automatically, but provide a visible notification in the topmost // browser window, if the status indicator is already visible. + DownloadsCommon.log("Showing new download notification."); browserWin.DownloadsIndicatorView.showEventNotification(aType); return; } diff --git a/browser/components/downloads/src/DownloadsLogger.jsm b/browser/components/downloads/src/DownloadsLogger.jsm new file mode 100644 index 000000000000..1218539c98b2 --- /dev/null +++ b/browser/components/downloads/src/DownloadsLogger.jsm @@ -0,0 +1,76 @@ +/* -*- Mode: js2; js2-basic-offset: 2; indent-tabs-mode: nil; -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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/. */ + +/** + * The contents of this file were copied almost entirely from + * toolkit/identity/LogUtils.jsm. Until we've got a more generalized logging + * mechanism for toolkit, I think this is going to be how we roll. + */ + +"use strict"; + +this.EXPORTED_SYMBOLS = ["DownloadsLogger"]; +const PREF_DEBUG = "browser.download.debug"; + +const Cu = Components.utils; +const Ci = Components.interfaces; +const Cc = Components.classes; +const Cr = Components.results; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +this.DownloadsLogger = { + _generateLogMessage: function _generateLogMessage(args) { + // create a string representation of a list of arbitrary things + let strings = []; + + for (let arg of args) { + if (typeof arg === 'string') { + strings.push(arg); + } else if (arg === undefined) { + strings.push('undefined'); + } else if (arg === null) { + strings.push('null'); + } else { + try { + strings.push(JSON.stringify(arg, null, 2)); + } catch(err) { + strings.push("<>"); + } + } + }; + return 'Downloads: ' + strings.join(' '); + }, + + /** + * log() - utility function to print a list of arbitrary things + * + * Enable with about:config pref browser.download.debug + */ + log: function DL_log(...args) { + let output = this._generateLogMessage(args); + dump(output + "\n"); + + // Additionally, make the output visible in the Error Console + Services.console.logStringMessage(output); + }, + + /** + * reportError() - report an error through component utils as well as + * our log function + */ + reportError: function DL_reportError(...aArgs) { + // Report the error in the browser + let output = this._generateLogMessage(aArgs); + Cu.reportError(output); + dump("ERROR:" + output + "\n"); + for (let frame = Components.stack.caller; frame; frame = frame.caller) { + dump("\t" + frame + "\n"); + } + } + +}; diff --git a/browser/components/downloads/src/Makefile.in b/browser/components/downloads/src/Makefile.in index 4a03cad9baf8..edbef7a93c3a 100644 --- a/browser/components/downloads/src/Makefile.in +++ b/browser/components/downloads/src/Makefile.in @@ -18,8 +18,9 @@ EXTRA_PP_COMPONENTS = \ DownloadsStartup.js \ $(NULL) -EXTRA_PP_JS_MODULES = \ +EXTRA_JS_MODULES = \ DownloadsCommon.jsm \ + DownloadsLogger.jsm \ $(NULL) include $(topsrcdir)/config/rules.mk