Bug 1415318 - Optionally send web console output to logcat in GeckoView r=jchen,rbarker draft
authorJames Willcox <snorp@snorp.net>
Fri, 01 Jun 2018 08:53:48 -0500
changeset 806449 b6e43e5fa7c24182ee2c41cfb5eeb3f44d38cbea
parent 806111 2b323e5c1f5e0b28bc16bd26cdc70d7454f836a2
child 806450 b5e84d2e12ce310f20315dc14f658fe94a76e162
push id112891
push userbmo:snorp@snorp.net
push dateSat, 09 Jun 2018 20:21:42 +0000
reviewersjchen, rbarker
bugs1415318
milestone62.0a1
Bug 1415318 - Optionally send web console output to logcat in GeckoView r=jchen,rbarker This adds a GeckoRuntimeSetting that allows apps to direct the web console to logcat. MozReview-Commit-ID: 7KgX5Ol6D3E
mobile/android/app/geckoview-prefs.js
mobile/android/components/geckoview/GeckoViewStartup.js
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntimeSettings.java
mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
mobile/android/modules/geckoview/GeckoViewConsole.jsm
mobile/android/modules/geckoview/moz.build
--- a/mobile/android/app/geckoview-prefs.js
+++ b/mobile/android/app/geckoview-prefs.js
@@ -8,8 +8,10 @@
 
 pref("privacy.trackingprotection.pbmode.enabled", false);
 pref("dom.ipc.processCount", 1);
 pref("dom.ipc.keepProcessesAlive.web", 1);
 pref("dom.ipc.processPrelaunch.enabled", false);
 
 // Tell Telemetry that we're in GeckoView mode.
 pref("toolkit.telemetry.isGeckoViewMode", true);
+
+pref("geckoview.console.enabled", false);
--- a/mobile/android/components/geckoview/GeckoViewStartup.js
+++ b/mobile/android/components/geckoview/GeckoViewStartup.js
@@ -5,16 +5,18 @@
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   GeckoViewTelemetryController: "resource://gre/modules/GeckoViewTelemetryController.jsm",
   GeckoViewUtils: "resource://gre/modules/GeckoViewUtils.jsm",
   Services: "resource://gre/modules/Services.jsm",
 });
 
+const {debug, warn} = GeckoViewUtils.initLogging("GeckoViewStartup", this);
+
 function GeckoViewStartup() {
 }
 
 GeckoViewStartup.prototype = {
   classID: Components.ID("{8e993c34-fdd6-432c-967e-f995d888777f}"),
 
   QueryInterface: ChromeUtils.generateQI([Ci.nsIObserver]),
 
@@ -31,31 +33,43 @@ GeckoViewStartup.prototype = {
     url = url.substring(4, url.indexOf("!/") + 2);
 
     let protocolHandler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
     protocolHandler.setSubstitution("android", Services.io.newURI(url));
   },
 
   /* ----------  nsIObserver  ---------- */
   observe: function(aSubject, aTopic, aData) {
+    debug `observe: ${aTopic}`;
     switch (aTopic) {
       case "app-startup": {
         // Parent and content process.
         GeckoViewUtils.addLazyGetter(this, "GeckoViewPermission", {
           service: "@mozilla.org/content-permission/prompt;1",
           observers: [
             "getUserMedia:ask-device-permission",
             "getUserMedia:request",
             "PeerConnection:request",
           ],
           ppmm: [
             "GeckoView:AddCameraPermission",
           ],
         });
 
+        GeckoViewUtils.addLazyGetter(this, "GeckoViewConsole", {
+          module: "resource://gre/modules/GeckoViewConsole.jsm",
+        });
+
+        GeckoViewUtils.addLazyPrefObserver({
+          name: "geckoview.console.enabled",
+          default: false,
+        }, {
+          handler: _ => this.GeckoViewConsole,
+        });
+
         if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
           // Parent process only.
           this.setResourceSubstitutions();
 
           Services.mm.loadFrameScript(
               "chrome://geckoview/content/GeckoViewPromptContent.js", true);
 
           GeckoViewUtils.addLazyGetter(this, "ContentCrashHandler", {
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/TestRunnerActivity.java
@@ -117,17 +117,18 @@ public class TestRunnerActivity extends 
             runtimeSettingsBuilder.arguments(new String[] { "-purgecaches" });
             final Bundle extras = intent.getExtras();
             if (extras != null) {
                 runtimeSettingsBuilder.extras(extras);
             }
 
             runtimeSettingsBuilder
                     .nativeCrashReportingEnabled(true)
-                    .javaCrashReportingEnabled(true);
+                    .javaCrashReportingEnabled(true)
+                    .consoleOutput(true);
 
             sRuntime = GeckoRuntime.create(this, runtimeSettingsBuilder.build());
             sRuntime.setDelegate(new GeckoRuntime.Delegate() {
                 @Override
                 public void onShutdown() {
                     mKillProcessOnDestroy = true;
                     finish();
                 }
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/rule/GeckoSessionTestRule.java
@@ -1247,17 +1247,18 @@ public class GeckoSessionTestRule extend
                                                 classes, recorder);
         mAllDelegates = new HashSet<>(DEFAULT_DELEGATES);
 
         if (sRuntime == null) {
             final GeckoRuntimeSettings.Builder runtimeSettingsBuilder =
                 new GeckoRuntimeSettings.Builder();
             runtimeSettingsBuilder.arguments(new String[] { "-purgecaches" })
                     .extras(InstrumentationRegistry.getArguments())
-                    .remoteDebuggingEnabled(true);
+                    .remoteDebuggingEnabled(true)
+                    .consoleOutput(true);
 
             if (env.isAutomation()) {
                 runtimeSettingsBuilder
                         .nativeCrashReportingEnabled(true)
                         .javaCrashReportingEnabled(true);
             }
 
             sRuntime = GeckoRuntime.create(
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntimeSettings.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/GeckoRuntimeSettings.java
@@ -209,16 +209,31 @@ public final class GeckoRuntimeSettings 
          * @return This Builder instance.
          **/
         public @NonNull Builder trackingProtectionCategories(
                 @TrackingProtectionDelegate.Category int categories) {
             mSettings.mTrackingProtection
                      .set(TrackingProtection.buildPrefValue(categories));
             return this;
         }
+
+        /**
+         * Set whether or not web console messages should go to logcat.
+         *
+         * Note: If enabled, Gecko performance may be negatively impacted if
+         * content makes heavy use of the console API.
+         *
+         * @param enabled A flag determining whether or not web console messages should be
+         *                printed to logcat.
+         * @return The builder instance.
+         */
+        public @NonNull Builder consoleOutput(boolean enabled) {
+            mSettings.mConsoleOutput.set(enabled);
+            return this;
+        }
     }
 
     /* package */ GeckoRuntime runtime;
     /* package */ boolean mUseContentProcess;
     /* package */ String[] mArgs;
     /* package */ Bundle mExtras;
     /* package */ int prefCount;
 
@@ -260,24 +275,26 @@ public final class GeckoRuntimeSettings 
     /* package */ Pref<Integer> mCookieBehavior = new Pref<Integer>(
         "network.cookie.cookieBehavior", COOKIE_ACCEPT_ALL);
     /* package */ Pref<Integer> mCookieLifetime = new Pref<Integer>(
         "network.cookie.lifetimePolicy", COOKIE_LIFETIME_NORMAL);
     /* package */ Pref<Integer> mCookieLifetimeDays = new Pref<Integer>(
         "network.cookie.lifetime.days", 90);
     /* package */ Pref<String> mTrackingProtection = new Pref<String>(
         "urlclassifier.trackingTable", "");
+    /* package */ Pref<Boolean> mConsoleOutput = new Pref<Boolean>(
+            "geckoview.console.enabled", false);
 
     /* package */ boolean mNativeCrashReporting;
     /* package */ boolean mJavaCrashReporting;
     /* package */ boolean mDebugPause;
 
     private final Pref<?>[] mPrefs = new Pref<?>[] {
         mCookieBehavior, mCookieLifetime, mCookieLifetimeDays, mJavaScript,
-        mRemoteDebugging, mTrackingProtection, mWebFonts
+        mRemoteDebugging, mTrackingProtection, mWebFonts, mConsoleOutput
     };
 
     /* package */ GeckoRuntimeSettings() {
         this(null);
     }
 
     /* package */ GeckoRuntimeSettings(final @Nullable GeckoRuntimeSettings settings) {
         if (BuildConfig.DEBUG && prefCount != mPrefs.length) {
@@ -559,16 +576,41 @@ public final class GeckoRuntimeSettings 
      * @return This GeckoRuntimeSettings instance.
      **/
     public @NonNull GeckoRuntimeSettings setTrackingProtectionCategories(
             @TrackingProtectionDelegate.Category int categories) {
         mTrackingProtection.set(TrackingProtection.buildPrefValue(categories));
         return this;
     }
 
+    /**
+     * Set whether or not web console messages should go to logcat.
+     *
+     * Note: If enabled, Gecko performance may be negatively impacted if
+     * content makes heavy use of the console API.
+     *
+     * @param enabled A flag determining whether or not web console messages should be
+     *                printed to logcat.
+     * @return This GeckoRuntimeSettings instance.
+     */
+
+    public @NonNull GeckoRuntimeSettings setConsoleOutputEnabled(boolean enabled) {
+        mConsoleOutput.set(enabled);
+        return this;
+    }
+
+    /**
+     * Get whether or not web console messages are sent to logcat.
+     *
+     * @return This GeckoRuntimeSettings instance.
+     */
+    public @NonNull boolean getConsoleOutputEnabled() {
+        return mConsoleOutput.get();
+    }
+
     @Override // Parcelable
     public int describeContents() {
         return 0;
     }
 
     @Override // Parcelable
     public void writeToParcel(Parcel out, int flags) {
         ParcelableUtils.writeBoolean(out, mUseContentProcess);
--- a/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
+++ b/mobile/android/geckoview_example/src/main/java/org/mozilla/geckoview_example/GeckoViewActivity.java
@@ -116,16 +116,17 @@ public class GeckoViewActivity extends A
             if (extras != null) {
                 runtimeSettingsBuilder.extras(extras);
             }
             runtimeSettingsBuilder
                     .useContentProcessHint(mUseMultiprocess)
                     .remoteDebuggingEnabled(true)
                     .nativeCrashReportingEnabled(true)
                     .javaCrashReportingEnabled(true)
+                    .consoleOutput(true)
                     .trackingProtectionCategories(TrackingProtectionDelegate.CATEGORY_ALL);
 
             sGeckoRuntime = GeckoRuntime.create(this, runtimeSettingsBuilder.build());
         }
 
         mGeckoSession = (GeckoSession)getIntent().getParcelableExtra("session");
         if (mGeckoSession != null) {
             connectSession(mGeckoSession);
new file mode 100644
--- /dev/null
+++ b/mobile/android/modules/geckoview/GeckoViewConsole.jsm
@@ -0,0 +1,140 @@
+/* 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/. */
+"use strict";
+
+var EXPORTED_SYMBOLS = ["GeckoViewConsole"];
+
+ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+ChromeUtils.import("resource://gre/modules/GeckoViewUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  Services: "resource://gre/modules/Services.jsm"
+});
+
+GeckoViewUtils.initLogging("GeckoViewConsole", this);
+
+const LOG_EVENT_TOPIC = "console-api-log-event";
+
+var GeckoViewConsole = {
+  _isEnabled: false,
+
+  get enabled() {
+    return this._isEnabled;
+  },
+
+  set enabled(val) {
+    debug `enabled = ${val}`;
+    if (!!val === this._isEnabled) {
+      return;
+    }
+
+    this._isEnabled = !!val;
+    if (this._isEnabled) {
+      Services.obs.addObserver(this, LOG_EVENT_TOPIC);
+    } else {
+      Services.obs.removeObserver(this, LOG_EVENT_TOPIC);
+    }
+  },
+
+  observe(aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case "nsPref:changed":
+        this.enabled = Services.prefs.getBoolPref(aData, false);
+        break;
+      case LOG_EVENT_TOPIC:
+        this._handleConsoleMessage(aSubject);
+        break;
+    }
+  },
+
+  _handleConsoleMessage(aMessage) {
+    aMessage = aMessage.wrappedJSObject;
+
+    let mappedArguments = Array.map(aMessage.arguments, this.formatResult, this);
+    let joinedArguments = Array.join(mappedArguments, " ");
+
+    if (aMessage.level == "error" || aMessage.level == "warn") {
+      let flag = (aMessage.level == "error" ? Ci.nsIScriptError.errorFlag : Ci.nsIScriptError.warningFlag);
+      let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
+      consoleMsg.init(joinedArguments, null, null, 0, 0, flag, "content javascript");
+      Services.console.logMessage(consoleMsg);
+    } else if (aMessage.level == "trace") {
+      let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+      let args = aMessage.arguments;
+      let filename = this.abbreviateSourceURL(args[0].filename);
+      let functionName = args[0].functionName || bundle.GetStringFromName("stacktrace.anonymousFunction");
+      let lineNumber = args[0].lineNumber;
+
+      let body = bundle.formatStringFromName("stacktrace.outputMessage", [filename, functionName, lineNumber], 3);
+      body += "\n";
+      args.forEach(function(aFrame) {
+        let functionName = aFrame.functionName || bundle.GetStringFromName("stacktrace.anonymousFunction");
+        body += "  " + aFrame.filename + " :: " + functionName + " :: " + aFrame.lineNumber + "\n";
+      });
+
+      Services.console.logStringMessage(body);
+    } else if (aMessage.level == "time" && aMessage.arguments) {
+      let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+      let body = bundle.formatStringFromName("timer.start", [aMessage.arguments.name], 1);
+      Services.console.logStringMessage(body);
+    } else if (aMessage.level == "timeEnd" && aMessage.arguments) {
+      let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+      let body = bundle.formatStringFromName("timer.end", [aMessage.arguments.name, aMessage.arguments.duration], 2);
+      Services.console.logStringMessage(body);
+    } else if (["group", "groupCollapsed", "groupEnd"].includes(aMessage.level)) {
+      // Do nothing yet
+    } else {
+      Services.console.logStringMessage(joinedArguments);
+    }
+  },
+
+  getResultType(aResult) {
+    let type = aResult === null ? "null" : typeof aResult;
+    if (type == "object" && aResult.constructor && aResult.constructor.name)
+      type = aResult.constructor.name;
+    return type.toLowerCase();
+  },
+
+  formatResult(aResult) {
+    let output = "";
+    let type = this.getResultType(aResult);
+    switch (type) {
+      case "string":
+      case "boolean":
+      case "date":
+      case "error":
+      case "number":
+      case "regexp":
+        output = aResult.toString();
+        break;
+      case "null":
+      case "undefined":
+        output = type;
+        break;
+      default:
+        output = aResult.toString();
+        break;
+    }
+
+    return output;
+  },
+
+  abbreviateSourceURL(aSourceURL) {
+    // Remove any query parameters.
+    let hookIndex = aSourceURL.indexOf("?");
+    if (hookIndex > -1)
+      aSourceURL = aSourceURL.substring(0, hookIndex);
+
+    // Remove a trailing "/".
+    if (aSourceURL[aSourceURL.length - 1] == "/")
+      aSourceURL = aSourceURL.substring(0, aSourceURL.length - 1);
+
+    // Remove all but the last path component.
+    let slashIndex = aSourceURL.lastIndexOf("/");
+    if (slashIndex > -1)
+      aSourceURL = aSourceURL.substring(slashIndex + 1);
+
+    return aSourceURL;
+  }
+};
--- a/mobile/android/modules/geckoview/moz.build
+++ b/mobile/android/modules/geckoview/moz.build
@@ -3,16 +3,17 @@
 # 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/.
 
 EXTRA_JS_MODULES += [
     'AndroidLog.jsm',
     'ContentCrashHandler.jsm',
     'GeckoViewAccessibility.jsm',
+    'GeckoViewConsole.jsm',
     'GeckoViewContent.jsm',
     'GeckoViewContentModule.jsm',
     'GeckoViewModule.jsm',
     'GeckoViewNavigation.jsm',
     'GeckoViewProgress.jsm',
     'GeckoViewRemoteDebugger.jsm',
     'GeckoViewSettings.jsm',
     'GeckoViewTab.jsm',