/**
 * exception-handler.js: Object for handling uncaughtException events.
 *
 * (C) 2010 Charlie Robbins
 * MIT LICENCE
 */

'use strict';

function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return _typeof(key) === "symbol" ? key : String(key); }
function _toPrimitive(input, hint) { if (_typeof(input) !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (_typeof(res) !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
var os = require('os');
var asyncForEach = require('async/forEach');
var debug = require('@dabh/diagnostics')('winston:exception');
var once = require('one-time');
var stackTrace = require('stack-trace');
var ExceptionStream = require('./exception-stream');

/**
 * Object for handling uncaughtException events.
 * @type {ExceptionHandler}
 */
module.exports = /*#__PURE__*/function () {
  /**
   * TODO: add contructor description
   * @param {!Logger} logger - TODO: add param description
   */
  function ExceptionHandler(logger) {
    _classCallCheck(this, ExceptionHandler);
    if (!logger) {
      throw new Error('Logger is required to handle exceptions');
    }
    this.logger = logger;
    this.handlers = new Map();
  }

  /**
   * Handles `uncaughtException` events for the current process by adding any
   * handlers passed in.
   * @returns {undefined}
   */
  _createClass(ExceptionHandler, [{
    key: "handle",
    value: function handle() {
      var _this = this;
      for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
        args[_key] = arguments[_key];
      }
      args.forEach(function (arg) {
        if (Array.isArray(arg)) {
          return arg.forEach(function (handler) {
            return _this._addHandler(handler);
          });
        }
        _this._addHandler(arg);
      });
      if (!this.catcher) {
        this.catcher = this._uncaughtException.bind(this);
        process.on('uncaughtException', this.catcher);
      }
    }

    /**
     * Removes any handlers to `uncaughtException` events for the current
     * process. This does not modify the state of the `this.handlers` set.
     * @returns {undefined}
     */
  }, {
    key: "unhandle",
    value: function unhandle() {
      var _this2 = this;
      if (this.catcher) {
        process.removeListener('uncaughtException', this.catcher);
        this.catcher = false;
        Array.from(this.handlers.values()).forEach(function (wrapper) {
          return _this2.logger.unpipe(wrapper);
        });
      }
    }

    /**
     * TODO: add method description
     * @param {Error} err - Error to get information about.
     * @returns {mixed} - TODO: add return description.
     */
  }, {
    key: "getAllInfo",
    value: function getAllInfo(err) {
      var message = null;
      if (err) {
        message = typeof err === 'string' ? err : err.message;
      }
      return {
        error: err,
        // TODO (indexzero): how do we configure this?
        level: 'error',
        message: ["uncaughtException: ".concat(message || '(no error message)'), err && err.stack || '  No stack trace'].join('\n'),
        stack: err && err.stack,
        exception: true,
        date: new Date().toString(),
        process: this.getProcessInfo(),
        os: this.getOsInfo(),
        trace: this.getTrace(err)
      };
    }

    /**
     * Gets all relevant process information for the currently running process.
     * @returns {mixed} - TODO: add return description.
     */
  }, {
    key: "getProcessInfo",
    value: function getProcessInfo() {
      return {
        pid: process.pid,
        uid: process.getuid ? process.getuid() : null,
        gid: process.getgid ? process.getgid() : null,
        cwd: process.cwd(),
        execPath: process.execPath,
        version: process.version,
        argv: process.argv,
        memoryUsage: process.memoryUsage()
      };
    }

    /**
     * Gets all relevant OS information for the currently running process.
     * @returns {mixed} - TODO: add return description.
     */
  }, {
    key: "getOsInfo",
    value: function getOsInfo() {
      return {
        loadavg: os.loadavg(),
        uptime: os.uptime()
      };
    }

    /**
     * Gets a stack trace for the specified error.
     * @param {mixed} err - TODO: add param description.
     * @returns {mixed} - TODO: add return description.
     */
  }, {
    key: "getTrace",
    value: function getTrace(err) {
      var trace = err ? stackTrace.parse(err) : stackTrace.get();
      return trace.map(function (site) {
        return {
          column: site.getColumnNumber(),
          file: site.getFileName(),
          "function": site.getFunctionName(),
          line: site.getLineNumber(),
          method: site.getMethodName(),
          "native": site.isNative()
        };
      });
    }

    /**
     * Helper method to add a transport as an exception handler.
     * @param {Transport} handler - The transport to add as an exception handler.
     * @returns {void}
     */
  }, {
    key: "_addHandler",
    value: function _addHandler(handler) {
      if (!this.handlers.has(handler)) {
        handler.handleExceptions = true;
        var wrapper = new ExceptionStream(handler);
        this.handlers.set(handler, wrapper);
        this.logger.pipe(wrapper);
      }
    }

    /**
     * Logs all relevant information around the `err` and exits the current
     * process.
     * @param {Error} err - Error to handle
     * @returns {mixed} - TODO: add return description.
     * @private
     */
  }, {
    key: "_uncaughtException",
    value: function _uncaughtException(err) {
      var info = this.getAllInfo(err);
      var handlers = this._getExceptionHandlers();
      // Calculate if we should exit on this error
      var doExit = typeof this.logger.exitOnError === 'function' ? this.logger.exitOnError(err) : this.logger.exitOnError;
      var timeout;
      if (!handlers.length && doExit) {
        // eslint-disable-next-line no-console
        console.warn('winston: exitOnError cannot be true with no exception handlers.');
        // eslint-disable-next-line no-console
        console.warn('winston: not exiting process.');
        doExit = false;
      }
      function gracefulExit() {
        debug('doExit', doExit);
        debug('process._exiting', process._exiting);
        if (doExit && !process._exiting) {
          // Remark: Currently ignoring any exceptions from transports when
          // catching uncaught exceptions.
          if (timeout) {
            clearTimeout(timeout);
          }
          // eslint-disable-next-line no-process-exit
          process.exit(1);
        }
      }
      if (!handlers || handlers.length === 0) {
        return process.nextTick(gracefulExit);
      }

      // Log to all transports attempting to listen for when they are completed.
      asyncForEach(handlers, function (handler, next) {
        var done = once(next);
        var transport = handler.transport || handler;

        // Debug wrapping so that we can inspect what's going on under the covers.
        function onDone(event) {
          return function () {
            debug(event);
            done();
          };
        }
        transport._ending = true;
        transport.once('finish', onDone('finished'));
        transport.once('error', onDone('error'));
      }, function () {
        return doExit && gracefulExit();
      });
      this.logger.log(info);

      // If exitOnError is true, then only allow the logging of exceptions to
      // take up to `3000ms`.
      if (doExit) {
        timeout = setTimeout(gracefulExit, 3000);
      }
    }

    /**
     * Returns the list of transports and exceptionHandlers for this instance.
     * @returns {Array} - List of transports and exceptionHandlers for this
     * instance.
     * @private
     */
  }, {
    key: "_getExceptionHandlers",
    value: function _getExceptionHandlers() {
      // Remark (indexzero): since `logger.transports` returns all of the pipes
      // from the _readableState of the stream we actually get the join of the
      // explicit handlers and the implicit transports with
      // `handleExceptions: true`
      return this.logger.transports.filter(function (wrap) {
        var transport = wrap.transport || wrap;
        return transport.handleExceptions;
      });
    }
  }]);
  return ExceptionHandler;
}();