/* 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";

const Cu = Components.utils;
const Ci = Components.interfaces;

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
                                   "@mozilla.org/childprocessmessagemanager;1",
                                   "nsIMessageSender");

XPCOMUtils.defineLazyModuleGetter(this, "TelemetryController",
                                  "resource://gre/modules/TelemetryController.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "E10SUtils",
                                  "resource:///modules/E10SUtils.jsm");

/*
 * The message manager has an upper limit on message sizes that it can
 * reliably forward to the parent so we limit the size of console log event
 * messages that we forward here. The web console is local and receives the
 * full console message, but addons subscribed to console event messages
 * in the parent receive the truncated version. Due to fragmentation,
 * messages as small as 1MB have resulted in IPC allocation failures on
 * 32-bit platforms. To limit IPC allocation sizes, console.log messages
 * with arguments with total size > MSG_MGR_CONSOLE_MAX_SIZE (bytes) have
 * their arguments completely truncated. MSG_MGR_CONSOLE_VAR_SIZE is an
 * approximation of how much space (in bytes) a JS non-string variable will
 * require in the manager's implementation. For strings, we use 2 bytes per
 * char. The console message URI and function name are limited to
 * MSG_MGR_CONSOLE_INFO_MAX characters. We don't attempt to calculate
 * the exact amount of space the message manager implementation will require
 * for a given message so this is imperfect.
 */
const MSG_MGR_CONSOLE_MAX_SIZE = 1024 * 1024; // 1MB
const MSG_MGR_CONSOLE_VAR_SIZE = 8;
const MSG_MGR_CONSOLE_INFO_MAX = 1024;

function ContentProcessSingleton() {}
ContentProcessSingleton.prototype = {
  classID: Components.ID("{ca2a8470-45c7-11e4-916c-0800200c9a66}"),
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                         Ci.nsISupportsWeakReference]),

  observe(subject, topic, data) {
    switch (topic) {
    case "app-startup": {
      Services.obs.addObserver(this, "console-api-log-event");
      Services.obs.addObserver(this, "xpcom-shutdown");
      TelemetryController.observe(null, topic, null);
      break;
    }
    case "console-api-log-event": {
      let consoleMsg = subject.wrappedJSObject;

      let msgData = {
        level: consoleMsg.level,
        filename: consoleMsg.filename.substring(0, MSG_MGR_CONSOLE_INFO_MAX),
        lineNumber: consoleMsg.lineNumber,
        functionName: consoleMsg.functionName &&
          consoleMsg.functionName.substring(0, MSG_MGR_CONSOLE_INFO_MAX),
        timeStamp: consoleMsg.timeStamp,
        addonId: consoleMsg.addonId,
        arguments: [],
      };

      // We can't send objects over the message manager, so we sanitize
      // them out, replacing those arguments with "<unavailable>".
      let unavailString = "<unavailable>";
      let unavailStringLength = unavailString.length * 2; // 2-bytes per char

      // When the sum of argument sizes reaches MSG_MGR_CONSOLE_MAX_SIZE,
      // replace all arguments with "<truncated>".
      let totalArgLength = 0;

      // Walk through the arguments, checking the type and size.
      for (let arg of consoleMsg.arguments) {
        if ((typeof arg == "object" || typeof arg == "function") &&
            arg !== null) {
          if (Services.appinfo.remoteType === E10SUtils.EXTENSION_REMOTE_TYPE) {
            // For OOP extensions: we want the developer to be able to see the
            // logs in the Browser Console. When the Addon Toolbox will be more
            // prominent we can revisit.
            try {
              // If the argument is clonable, then send it as-is. If
              // cloning fails, fall back to the unavailable string.
              arg = Cu.cloneInto(arg, {});
            } catch (e) {
              arg = unavailString;
            }
          } else {
            arg = unavailString;
          }
          totalArgLength += unavailStringLength;
        } else if (typeof arg == "string") {
          totalArgLength += arg.length * 2; // 2-bytes per char
        } else {
          totalArgLength += MSG_MGR_CONSOLE_VAR_SIZE;
        }

        if (totalArgLength <= MSG_MGR_CONSOLE_MAX_SIZE) {
          msgData.arguments.push(arg);
        } else {
          // arguments take up too much space
          msgData.arguments = ["<truncated>"];
          break;
        }
      }

      cpmm.sendAsyncMessage("Console:Log", msgData);
      break;
    }

    case "xpcom-shutdown":
      Services.obs.removeObserver(this, "console-api-log-event");
      Services.obs.removeObserver(this, "xpcom-shutdown");
      break;
    }
  },
};

this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentProcessSingleton]);
