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