Bug 1415318 - Optionally send web console output to logcat in GeckoView r=jchen,rbarker
authorJames Willcox <snorp@snorp.net>
Fri, 01 Jun 2018 08:53:48 -0500
changeset 479128 8904c9386abf5316d72be0457d1651f7af9d0865
parent 479127 93b750a2d372d8361f5639cb5f8a92c8b177e4d6
child 479129 24fe98c45aae15108532297e7fceb70486888e24
push id9719
push userffxbld-merge
push dateFri, 24 Aug 2018 17:49:46 +0000
treeherdermozilla-beta@719ec98fba77 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjchen, rbarker
bugs1415318
milestone63.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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
@@ -1256,17 +1256,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;
 
@@ -261,24 +276,26 @@ public final class GeckoRuntimeSettings 
         "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",
         TrackingProtection.buildPrefValue(TrackingProtectionDelegate.CATEGORY_ALL));
+    /* 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
+        mCookieBehavior, mCookieLifetime, mCookieLifetimeDays, mConsoleOutput,
+        mJavaScript, mRemoteDebugging, mTrackingProtection, mWebFonts
     };
 
     /* package */ GeckoRuntimeSettings() {
         this(null);
     }
 
     /* package */ GeckoRuntimeSettings(final @Nullable GeckoRuntimeSettings settings) {
         if (BuildConfig.DEBUG && prefCount != mPrefs.length) {
@@ -560,16 +577,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 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
@@ -109,16 +109,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(aVal) {
+    debug `enabled = ${aVal}`;
+    if (!!aVal === this._isEnabled) {
+      return;
+    }
+
+    this._isEnabled = !!aVal;
+    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',