diff --git a/chrome/content/zotero/debugViewer.html b/chrome/content/zotero/debugViewer.html new file mode 100644 index 0000000000..35f9d04619 --- /dev/null +++ b/chrome/content/zotero/debugViewer.html @@ -0,0 +1,139 @@ + + + + + Debug Output + + + + + + +
+ + + + + +
+
+
+
+
+ + diff --git a/chrome/content/zotero/debugViewer.js b/chrome/content/zotero/debugViewer.js new file mode 100644 index 0000000000..7672ec7b01 --- /dev/null +++ b/chrome/content/zotero/debugViewer.js @@ -0,0 +1,189 @@ +"use strict"; + +var interval = 1000; +var intervalID; +var stopping = false; + +function start() { + updateErrors().then(function () { + if (stopping) return; + + addInitialOutput(); + Zotero.Debug.addConsoleViewerListener(addLine) + intervalID = setInterval(() => updateErrors(), interval); + }); +} + +function stop() { + stopping = true; + if (intervalID) { + clearInterval(intervalID); + intervalID = null; + } + Zotero.Debug.removeConsoleViewerListener() +} + +function updateErrors() { + return Zotero.getSystemInfo() + .then(function (sysInfo) { + if (stopping) return; + + var errors = Zotero.getErrors(true); + var errorStr = errors.length ? errors.join('\n\n') + '\n\n' : ''; + + var scroll = atPageBottom(); + + document.getElementById('errors').textContent = errorStr + sysInfo; + + // TODO: This doesn't seem to work for some reason -- when errors are logged, it doesn't stay + // at the bottom + if (scroll) { + scrollToPageBottom(); + } + }); +} + +function addInitialOutput() { + Zotero.Debug.getConsoleViewerOutput().forEach(function (line) { + addLine(line); + }); +} + +function addLine(line) { + var scroll = atPageBottom() + + var p = document.createElement('p'); + p.textContent = line; + var output = document.getElementById('output'); + output.appendChild(p); + + // If scrolled to the bottom of the page, stay there + if (scroll) { + scrollToPageBottom(); + } + + document.getElementById('submit-button').removeAttribute('disabled'); + document.getElementById('clear-button').removeAttribute('disabled'); +} + +function atPageBottom() { + return (window.innerHeight + window.scrollY) >= document.body.offsetHeight - 100; +} + +function scrollToPageBottom() { + window.scrollTo(0, document.body.scrollHeight); +} + +function submit(button) { + button.setAttribute('disabled', ''); + clearSubmitStatus(); + + Components.utils.import("resource://zotero/config.js"); + var url = ZOTERO_CONFIG.REPOSITORY_URL + "report?debug=1"; + var output = document.getElementById('errors').textContent + + "\n\n" + "=========================================================\n\n" + + Array.from(document.getElementById('output').childNodes).map(p => p.textContent).join("\n\n"); + var pm = document.getElementById('submit-progress'); + pm.removeAttribute('hidden'); + + Zotero.HTTP.request( + "POST", + url, + { + compressBody: true, + body: output, + logBodyLength: 30, + timeout: 30000, + // Update progress meter + requestObserver: function (req) { + req.channel.notificationCallbacks = { + onProgress: function (request, context, progress, progressMax) { + if (!pm.value || progress > pm.value) { + pm.value = progress; + } + if (!pm.max || progressMax > pm.max) { + pm.max = progressMax; + } + }, + + // nsIInterfaceRequestor + getInterface: function (iid) { + try { + return this.QueryInterface(iid); + } + catch (e) { + throw Components.results.NS_NOINTERFACE; + } + }, + + QueryInterface: function(iid) { + if (iid.equals(Components.interfaces.nsISupports) || + iid.equals(Components.interfaces.nsIInterfaceRequestor) || + iid.equals(Components.interfaces.nsIProgressEventSink)) { + return this; + } + throw Components.results.NS_NOINTERFACE; + }, + + } + } + } + ) + .then(function (xmlhttp) { + var reported = xmlhttp.responseXML.getElementsByTagName('reported'); + if (reported.length != 1) { + showSubmitError(e); + return false; + } + + showSubmitResult(reported[0].getAttribute('reportID')); + }) + .catch(function (e) { + showSubmitError(e); + return false; + }) + .finally(function () { + pm.setAttribute('hidden', ''); + button.removeAttribute('disabled'); + }); +} + +function showSubmitResult(id) { + var elem = document.getElementById('submit-result'); + elem.removeAttribute('hidden'); + document.getElementById('debug-id').textContent = "D" + id; + var copyID = document.getElementById('submit-result-copy-id'); + copyID.style.visibility = 'visible'; + copyID.setAttribute('data-tooltip', 'Copy ID to Clipboard'); +} + +function copyIDToClipboard(elem) { + var id = document.getElementById('debug-id').textContent; + Components.classes["@mozilla.org/widget/clipboardhelper;1"] + .getService(Components.interfaces.nsIClipboardHelper) + .copyString(id); + elem.setAttribute('data-tooltip', 'Copied'); + setTimeout(() => elem.style.visibility = 'hidden', 750); +} + +function showSubmitError(e) { + var elem = document.getElementById('submit-error'); + elem.removeAttribute('hidden'); + elem.textContent = "Error submitting output"; + Components.utils.reportError(e); + Zotero.debug(e, 1); +} + +function clearSubmitStatus() { + document.getElementById('submit-result').setAttribute('hidden', ''); + document.getElementById('submit-error').setAttribute('hidden', ''); +} + +function clearOutput(button) { + button.setAttribute('disabled', ''); + document.getElementById('output').textContent = ''; + clearSubmitStatus(); +} + +window.addEventListener('load', start); +window.addEventListener("unload", stop); diff --git a/chrome/content/zotero/xpcom/debug.js b/chrome/content/zotero/xpcom/debug.js index 3c4e91d2a0..e068992e1f 100644 --- a/chrome/content/zotero/xpcom/debug.js +++ b/chrome/content/zotero/xpcom/debug.js @@ -25,19 +25,38 @@ Zotero.Debug = new function () { - var _console, _consolePref, _stackTrace, _store, _level, _lastTime, _output = []; + var _console, _stackTrace, _store, _level, _lastTime, _output = []; var _slowTime = false; var _colorOutput = false; + var _consoleViewer = false; + var _consoleViewerQueue = []; + var _consoleViewerListener; - this.init = function (forceDebugLog) { - _consolePref = Zotero.Prefs.get('debug.log'); - _console = _consolePref || forceDebugLog; - if (_console && Zotero.isFx && !Zotero.isBookmarklet && (!Zotero.isWin || _consolePref)) { + /** + * Initialize debug logging + * + * Debug logging can be set in several different ways: + * + * - via the debug.log pref in the client or connector + * - by enabling debug output logging in the Advanced prefs in the client + * - by passing -ZoteroDebug or -ZoteroDebugText on the command line + * + * In the client, debug.log and -ZoteroDebugText enable logging via the terminal, while -ZoteroDebug + * enables logging via an in-app HTML-based window. + * + * @param {Integer} [forceDebugLog = 0] - Force output even if pref disabled + * 2: window (-ZoteroDebug) + * 1: text console (-ZoteroDebugText) + * 0: disabled + */ + this.init = function (forceDebugLog = 0) { + _console = Zotero.Prefs.get('debug.log') || forceDebugLog == 1; + _consoleViewer = forceDebugLog == 2; + // When logging to the text console from the client on Mac/Linux, colorize output + if (_console && Zotero.isFx && !Zotero.isBookmarklet) { _colorOutput = true; - } - if (_colorOutput) { - // Time threshold in milliseconds above which intervals - // should be colored red in terminal output + + // Time threshold in ms above which intervals should be colored red in terminal output _slowTime = Zotero.Prefs.get('debug.log.slowTime'); } _store = Zotero.Prefs.get('debug.store'); @@ -48,15 +67,23 @@ Zotero.Debug = new function () { _stackTrace = Zotero.Prefs.get('debug.stackTrace'); this.storing = _store; - this.enabled = _console || _store; + this.updateEnabled(); if (Zotero.isStandalone) { - Zotero.Prefs.set('browser.dom.window.dump.enabled', _console, true); + // Enable dump() from window (non-XPCOM) scopes when terminal or viewer logging is enabled. + // (These will always go to the terminal, even in viewer mode.) + Zotero.Prefs.set('browser.dom.window.dump.enabled', _console || _consoleViewer, true); + + if (_consoleViewer) { + setTimeout(function () { + Zotero.openInViewer("chrome://zotero/content/debugViewer.html"); + }, 1000); + } } } this.log = function (message, level, stack) { - if (!_console && !_store) { + if (!this.enabled) { return; } @@ -118,21 +145,23 @@ Zotero.Debug = new function () { message += '\n' + Zotero.Debug.stackToString(stack); } - if (_console) { - var output = 'zotero(' + level + ')' + deltaStr + ': ' + message; - if(Zotero.isFx && !Zotero.isBookmarklet) { - // On Windows, where the text console (-console) is inexplicably glacial, - // log to the Browser Console instead if only the -ZoteroDebug flag is used. - // Developers can use the debug.log/debug.time prefs and the Cygwin text console. - // - // TODO: Get rid of the filename and line number - if (!_consolePref && Zotero.isWin && !Zotero.isStandalone) { - var console = Components.utils.import("resource://gre/modules/Console.jsm", {}).console; - console.log(output); + if (_console || _consoleViewer) { + var output = '(' + level + ')' + deltaStr + ': ' + message; + if (Zotero.isFx && !Zotero.isBookmarklet) { + // Text console + if (_console) { + dump("zotero" + output + "\n\n"); } - // Otherwise dump to the text console - else { - dump(output + "\n\n"); + // Console window + if (_consoleViewer) { + // If there's a listener, pass line immediately + if (_consoleViewerListener) { + _consoleViewerListener(output); + } + // Otherwise add to queue + else { + _consoleViewerQueue.push(output); + } } } else if(window.console) { window.console.log(output); @@ -193,6 +222,26 @@ Zotero.Debug = new function () { }); + this.getConsoleViewerOutput = function () { + var queue = _consoleViewerQueue; + _consoleViewerQueue = []; + return queue; + } + + + this.addConsoleViewerListener = function (listener) { + _consoleViewerListener = listener; + }; + + + this.removeConsoleViewerListener = function () { + _consoleViewerListener = null; + // At least for now, stop logging once console viewer is closed + _consoleViewer = false; + this.updateEnabled(); + }; + + this.setStore = function (enable) { if (enable) { this.clear(); @@ -200,10 +249,14 @@ Zotero.Debug = new function () { _store = enable; this.storing = _store; - this.enabled = _console || _store; } + this.updateEnabled = function () { + this.enabled = _console || _consoleViewer || _store; + }; + + this.count = function () { return _output.length; } diff --git a/components/zotero-service.js b/components/zotero-service.js index 6362317e9b..315f215dc5 100644 --- a/components/zotero-service.js +++ b/components/zotero-service.js @@ -485,9 +485,13 @@ function ZoteroCommandLineHandler() {} ZoteroCommandLineHandler.prototype = { /* nsICommandLineHandler */ handle : function(cmdLine) { - // Force debug output + // Force debug output to window if (cmdLine.handleFlag("ZoteroDebug", false)) { - zInitOptions.forceDebugLog = true; + zInitOptions.forceDebugLog = 2; + } + // Force debug output to text console + else if (cmdLine.handleFlag("ZoteroDebugText", false)) { + zInitOptions.forceDebugLog = 1; } // handler to open Zotero pane at startup in Zotero for Firefox