/**
 * Copyright 2013-2022 the PM2 project authors. All rights reserved.
 * Use of this source code is governed by a license that
 * can be found in the LICENSE file.
 */
'use strict';

const commander   = require('commander');
const fs          = require('fs');
const path        = require('path');
const eachLimit   = require('async/eachLimit');
const series      = require('async/series');
const debug       = require('debug')('pm2:cli');
const util        = require('util');
const chalk       = require('chalk');
const fclone      = require('fclone');

var DockerMgmt  = require('./API/ExtraMgmt/Docker.js')
var conf        = require('../constants.js');
var Client      = require('./Client');
var Common      = require('./Common');
var KMDaemon    = require('@pm2/agent/src/InteractorClient');
var Config      = require('./tools/Config');
var Modularizer = require('./API/Modules/Modularizer.js');
var path_structure = require('../paths.js');
var UX          = require('./API/UX');
var pkg         = require('../package.json');
var hf = require('./API/Modules/flagExt.js');
var Configuration = require('./Configuration.js');
const sexec = require('./tools/sexec.js')

var IMMUTABLE_MSG = chalk.bold.blue('Use --update-env to update environment variables');

/**
 * Main Function to be imported
 * can be aliased to PM2
 *
 * To use it when PM2 is installed as a module:
 *
 * var PM2 = require('pm2');
 *
 * var pm2 = PM2(<opts>);
 *
 *
 * @param {Object}  opts
 * @param {String}  [opts.cwd=<current>]         override pm2 cwd for starting scripts
 * @param {String}  [opts.pm2_home=[<paths.js>]] pm2 directory for log, pids, socket files
 * @param {Boolean} [opts.independent=false]     unique PM2 instance (random pm2_home)
 * @param {Boolean} [opts.daemon_mode=true]      should be called in the same process or not
 * @param {String}  [opts.public_key=null]       pm2 plus bucket public key
 * @param {String}  [opts.secret_key=null]       pm2 plus bucket secret key
 * @param {String}  [opts.machine_name=null]     pm2 plus instance name
 */
class API {

  constructor (opts) {
    if (!opts) opts = {};
    var that = this;

    this.daemon_mode = typeof(opts.daemon_mode) == 'undefined' ? true : opts.daemon_mode;
    this.pm2_home = conf.PM2_ROOT_PATH;
    this.public_key = conf.PUBLIC_KEY || opts.public_key || null;
    this.secret_key = conf.SECRET_KEY || opts.secret_key || null;
    this.machine_name = conf.MACHINE_NAME || opts.machine_name || null

    /**
     * CWD resolution
     */
    this.cwd = process.cwd();
    if (opts.cwd) {
      this.cwd = path.resolve(opts.cwd);
    }

    /**
     * PM2 HOME resolution
     */
    if (opts.pm2_home && opts.independent == true)
      throw new Error('You cannot set a pm2_home and independent instance in same time');

    if (opts.pm2_home) {
      // Override default conf file
      this.pm2_home = opts.pm2_home;
      conf = Object.assign(conf, path_structure(this.pm2_home));
    }
    else if (opts.independent == true && conf.IS_WINDOWS === false) {
      // Create an unique pm2 instance
      const crypto = require('crypto');
      var random_file = crypto.randomBytes(8).toString('hex');
      this.pm2_home = path.join('/tmp', random_file);

      // If we dont explicitly tell to have a daemon
      // It will go as in proc
      if (typeof(opts.daemon_mode) == 'undefined')
        this.daemon_mode = false;
      conf = Object.assign(conf, path_structure(this.pm2_home));
    }

    this._conf = conf;

    if (conf.IS_WINDOWS) {
      // Weird fix, may need to be dropped
      // @todo windows connoisseur double check
      if (process.stdout._handle && process.stdout._handle.setBlocking)
        process.stdout._handle.setBlocking(true);
    }

    this.Client = new Client({
      pm2_home: that.pm2_home,
      conf: this._conf,
      secret_key: this.secret_key,
      public_key: this.public_key,
      daemon_mode: this.daemon_mode,
      machine_name: this.machine_name
    });

    this.pm2_configuration = Configuration.getSync('pm2') || {}

    this.gl_interact_infos = null;
    this.gl_is_km_linked = false;

    try {
      var pid = fs.readFileSync(conf.INTERACTOR_PID_PATH);
      pid = parseInt(pid.toString().trim());
      process.kill(pid, 0);
      that.gl_is_km_linked = true;
    } catch (e) {
      that.gl_is_km_linked = false;
    }

    // For testing purposes
    if (this.secret_key && process.env.NODE_ENV == 'local_test')
      that.gl_is_km_linked = true;

    KMDaemon.ping(this._conf, function(err, result) {
      if (!err && result === true) {
        fs.readFile(conf.INTERACTION_CONF, (err, _conf) => {
          if (!err) {
            try {
              that.gl_interact_infos = JSON.parse(_conf.toString())
            } catch(e) {
              var json5 = require('./tools/json5.js')
              try {
                that.gl_interact_infos = json5.parse(_conf.toString())
              } catch(e) {
                console.error(e)
                that.gl_interact_infos = null
              }
            }
          }
        })
      }
    })

    this.gl_retry = 0;
  }

  /**
   * Connect to PM2
   * Calling this command is now optional
   *
   * @param {Function} cb callback once pm2 is ready for commands
   */
  connect (noDaemon, cb) {
    var that = this;
    this.start_timer = new Date();

    if (typeof(cb) == 'undefined') {
      cb = noDaemon;
      noDaemon = false;
    } else if (noDaemon === true) {
      // Backward compatibility with PM2 1.x
      this.Client.daemon_mode = false;
      this.daemon_mode = false;
    }

    this.Client.start(function(err, meta) {
      if (err)
        return cb(err);

      if (meta.new_pm2_instance == false && that.daemon_mode === true)
        return cb(err, meta);

      that.launchSysMonitoring(() => {})
      // If new pm2 instance has been popped
      // Launch all modules
      that.launchAll(that, function(err_mod) {
        return cb(err, meta);
      });
    });
  }

  /**
   * Usefull when custom PM2 created with independent flag set to true
   * This will cleanup the newly created instance
   * by removing folder, killing PM2 and so on
   *
   * @param {Function} cb callback once cleanup is successfull
   */
  destroy (cb) {
    var that = this;

    debug('Killing and deleting current deamon');

    this.killDaemon(function() {
      var cmd = 'rm -rf ' + that.pm2_home;
      var test_path = path.join(that.pm2_home, 'module_conf.json');
      var test_path_2 = path.join(that.pm2_home, 'pm2.pid');

      if (that.pm2_home.indexOf('.pm2') > -1)
        return cb(new Error('Destroy is not a allowed method on .pm2'));

      fs.access(test_path, fs.R_OK, function(err) {
        if (err) return cb(err);
        debug('Deleting temporary folder %s', that.pm2_home);
        sexec(cmd, cb);
      });
    });
  }

  /**
   * Disconnect from PM2 instance
   * This will allow your software to exit by itself
   *
   * @param {Function} [cb] optional callback once connection closed
   */
  disconnect (cb) {
    var that = this;

    if (!cb) cb = function() {};

    this.Client.close(function(err, data) {
      debug('The session lasted %ds', (new Date() - that.start_timer) / 1000);
      return cb(err, data);
    });
  };

  /**
   * Alias on disconnect
   * @param cb
   */
  close (cb) {
    this.disconnect(cb);
  }

  /**
   * Launch modules
   *
   * @param {Function} cb callback once pm2 has launched modules
   */
  launchModules (cb) {
    this.launchAll(this, cb);
  }

  /**
   * Enable bus allowing to retrieve various process event
   * like logs, restarts, reloads
   *
   * @param {Function} cb callback called with 1st param err and 2nb param the bus
   */
  launchBus (cb) {
    this.Client.launchBus(cb);
  }

  /**
   * Exit methods for API
   * @param {Integer} code exit code for terminal
   */
  exitCli (code) {
    var that = this;

    // Do nothing if PM2 called programmatically (also in speedlist)
    if (conf.PM2_PROGRAMMATIC && process.env.PM2_USAGE != 'CLI') return false;

    KMDaemon.disconnectRPC(function() {
      that.Client.close(function() {
        code = code || 0;
        // Safe exits process after all streams are drained.
        // file descriptor flag.
        var fds = 0;
        // exits process when stdout (1) and sdterr(2) are both drained.
        function tryToExit() {
          if ((fds & 1) && (fds & 2)) {
            debug('This command took %ds to execute', (new Date() - that.start_timer) / 1000);
            process.exit(code);
          }
        }

        [process.stdout, process.stderr].forEach(function(std) {
          var fd = std.fd;
          if (!std.bufferSize) {
            // bufferSize equals 0 means current stream is drained.
            fds = fds | fd;
          } else {
            // Appends nothing to the std queue, but will trigger `tryToExit` event on `drain`.
            std.write && std.write('', function() {
              fds = fds | fd;
              tryToExit();
            });
          }
          // Does not write anything more.
          delete std.write;
        });
        tryToExit();
      });
    });
  }

////////////////////////////
// Application management //
////////////////////////////

  /**
   * Start a file or json with configuration
   * @param {Object||String} cmd script to start or json
   * @param {Function} cb called when application has been started
   */
  start (cmd, opts, cb) {
    if (typeof(opts) == "function") {
      cb = opts;
      opts = {};
    }
    if (!opts) opts = {};

    var that = this;
    if (Array.isArray(opts.watch) && opts.watch.length === 0)
      opts.watch = (opts.rawArgs ? !!~opts.rawArgs.indexOf('--watch') : !!~process.argv.indexOf('--watch')) || false;

    if (Common.isConfigFile(cmd) || (typeof(cmd) === 'object')) {
      that._startJson(cmd, opts, 'restartProcessId', (err, procs) => {
        return cb ? cb(err, procs) : this.speedList()
      })
    }
    else {
      that._startScript(cmd, opts, (err, procs) => {
        return cb ? cb(err, procs) : this.speedList(0)
      })
    }
  }

  /**
   * Reset process counters
   *
   * @method resetMetaProcess
   */
  reset (process_name, cb) {
    var that = this;

    function processIds(ids, cb) {
      eachLimit(ids, conf.CONCURRENT_ACTIONS, function(id, next) {
        that.Client.executeRemote('resetMetaProcessId', id, function(err, res) {
          if (err) console.error(err);
          Common.printOut(conf.PREFIX_MSG + 'Resetting meta for process id %d', id);
          return next();
        });
      }, function(err) {
        if (err) return cb(Common.retErr(err));
        return cb ? cb(null, {success:true}) : that.speedList();
      });
    }

    if (process_name == 'all') {
      that.Client.getAllProcessId(function(err, ids) {
        if (err) {
          Common.printError(err);
          return cb ? cb(Common.retErr(err)) : that.exitCli(conf.ERROR_EXIT);
        }
        return processIds(ids, cb);
      });
    }
    else if (isNaN(process_name)) {
      that.Client.getProcessIdByName(process_name, function(err, ids) {
        if (err) {
          Common.printError(err);
          return cb ? cb(Common.retErr(err)) : that.exitCli(conf.ERROR_EXIT);
        }
        if (ids.length === 0) {
          Common.printError('Unknown process name');
          return cb ? cb(new Error('Unknown process name')) : that.exitCli(conf.ERROR_EXIT);
        }
        return processIds(ids, cb);
      });
    } else {
      processIds([process_name], cb);
    }
  }

  /**
   * Update daemonized PM2 Daemon
   *
   * @param {Function} cb callback when pm2 has been upgraded
   */
  update (cb) {
    var that = this;

    Common.printOut('Be sure to have the latest version by doing `npm install pm2@latest -g` before doing this procedure.');

    // Dump PM2 processes
    that.Client.executeRemote('notifyKillPM2', {}, function() {});

    that.getVersion(function(err, new_version) {
      // If not linked to PM2 plus, and update PM2 to latest, display motd.update
      if (!that.gl_is_km_linked && !err && (pkg.version != new_version)) {
        var dt = fs.readFileSync(path.join(__dirname, that._conf.PM2_UPDATE));
        console.log(dt.toString());
      }

      that.dump(function(err) {
        that.killDaemon(function() {
          that.Client.launchDaemon({interactor:false}, function(err, child) {
            that.Client.launchRPC(function() {
              that.resurrect(function() {
                Common.printOut(chalk.blue.bold('>>>>>>>>>> PM2 updated'));
                that.launchSysMonitoring(() => {})
                that.launchAll(that, function() {
                  KMDaemon.launchAndInteract(that._conf, {
                    pm2_version: pkg.version
                  }, function(err, data, interactor_proc) {
                  })
                  setTimeout(() => {
                    return cb ? cb(null, {success:true}) : that.speedList();
                  }, 250)
                });
              });
            });
          });
        });
      });
    });

    return false;
  }

  /**
   * Reload an application
   *
   * @param {String} process_name Application Name or All
   * @param {Object} opts         Options
   * @param {Function} cb         Callback
   */
  reload (process_name, opts, cb) {
    var that = this;

    if (typeof(opts) == "function") {
      cb = opts;
      opts = {};
    }

    var delay = Common.lockReload();
    if (delay > 0 && opts.force != true) {
      Common.printError(conf.PREFIX_MSG_ERR + 'Reload already in progress, please try again in ' + Math.floor((conf.RELOAD_LOCK_TIMEOUT - delay) / 1000) + ' seconds or use --force');
      return cb ? cb(new Error('Reload in progress')) : that.exitCli(conf.ERROR_EXIT);
    }

    if (Common.isConfigFile(process_name))
      that._startJson(process_name, opts, 'reloadProcessId', function(err, apps) {
        Common.unlockReload();
        if (err)
          return cb ? cb(err) : that.exitCli(conf.ERROR_EXIT);
        return cb ? cb(null, apps) : that.exitCli(conf.SUCCESS_EXIT);
      });
    else {
      if (opts && opts.env) {
        var err = 'Using --env [env] without passing the ecosystem.config.js does not work'
        Common.err(err);
        Common.unlockReload();
        return cb ? cb(Common.retErr(err)) : that.exitCli(conf.ERROR_EXIT);
      }

      if (opts && !opts.updateEnv)
        Common.printOut(IMMUTABLE_MSG);

      that._operate('reloadProcessId', process_name, opts, function(err, apps) {
        Common.unlockReload();

        if (err)
          return cb ? cb(err) : that.exitCli(conf.ERROR_EXIT);
        return cb ? cb(null, apps) : that.exitCli(conf.SUCCESS_EXIT);
      });
    }
  }

  /**
   * Restart process
   *
   * @param {String} cmd   Application Name / Process id / JSON application file / 'all'
   * @param {Object} opts  Extra options to be updated
   * @param {Function} cb  Callback
   */
  restart (cmd, opts, cb) {
    if (typeof(opts) == "function") {
      cb = opts;
      opts = {};
    }
    var that = this;

    if (typeof(cmd) === 'number')
      cmd = cmd.toString();

    if (cmd == "-") {
      // Restart from PIPED JSON
      process.stdin.resume();
      process.stdin.setEncoding('utf8');
      process.stdin.on('data', function (param) {
        process.stdin.pause();
        that.actionFromJson('restartProcessId', param, opts, 'pipe', cb);
      });
    }
    else if (Common.isConfigFile(cmd) || typeof(cmd) === 'object')
      that._startJson(cmd, opts, 'restartProcessId', cb);
    else {
      if (opts && opts.env) {
        var err = 'Using --env [env] without passing the ecosystem.config.js does not work'
        Common.err(err);
        return cb ? cb(Common.retErr(err)) : that.exitCli(conf.ERROR_EXIT);
      }
      if (opts && !opts.updateEnv)
        Common.printOut(IMMUTABLE_MSG);
      that._operate('restartProcessId', cmd, opts, cb);
    }
  }

  /**
   * Delete process
   *
   * @param {String} process_name Application Name / Process id / Application file / 'all'
   * @param {Function} cb Callback
   */
  delete (process_name, jsonVia, cb) {
    var that = this;

    if (typeof(jsonVia) === "function") {
      cb = jsonVia;
      jsonVia = null;
    }

    if (typeof(process_name) === "number") {
      process_name = process_name.toString();
    }

    if (jsonVia == 'pipe')
      return that.actionFromJson('deleteProcessId', process_name, commander, 'pipe', (err, procs) => {
        return cb ? cb(err, procs) : this.speedList()
      });
    if (Common.isConfigFile(process_name))
      return that.actionFromJson('deleteProcessId', process_name, commander, 'file', (err, procs) => {
        return cb ? cb(err, procs) : this.speedList()
      });
    else {
      that._operate('deleteProcessId', process_name, (err, procs) => {
        return cb ? cb(err, procs) : this.speedList()
      });
    }
  }

  /**
   * Stop process
   *
   * @param {String} process_name Application Name / Process id / Application file / 'all'
   * @param {Function} cb Callback
   */
  stop (process_name, cb) {
    var that = this;

    if (typeof(process_name) === 'number')
      process_name = process_name.toString();

    if (process_name == "-") {
      process.stdin.resume();
      process.stdin.setEncoding('utf8');
      process.stdin.on('data', function (param) {
        process.stdin.pause();
        that.actionFromJson('stopProcessId', param, commander, 'pipe', (err, procs) => {
          return cb ? cb(err, procs) : this.speedList()
        })
      });
    }
    else if (Common.isConfigFile(process_name))
      that.actionFromJson('stopProcessId', process_name, commander, 'file', (err, procs) => {
        return cb ? cb(err, procs) : this.speedList()
      });
    else
      that._operate('stopProcessId', process_name, (err, procs) => {
        return cb ? cb(err, procs) : this.speedList()
      });
  }

  /**
   * Get list of all processes managed
   *
   * @param {Function} cb Callback
   */
  list (opts, cb) {
    var that = this;

    if (typeof(opts) == 'function') {
      cb = opts;
      opts = null;
    }

    that.Client.executeRemote('getMonitorData', {}, function(err, list) {
      if (err) {
        Common.printError(err);
        return cb ? cb(Common.retErr(err)) : that.exitCli(conf.ERROR_EXIT);
      }

      if (opts && opts.rawArgs && opts.rawArgs.indexOf('--watch') > -1) {
        var dayjs = require('dayjs');
        function show() {
          process.stdout.write('\x1b[2J');
          process.stdout.write('\x1b[0f');
          console.log('Last refresh: ', dayjs().format());
          that.Client.executeRemote('getMonitorData', {}, function(err, list) {
            UX.list(list, null);
          });
        }

        show();
        setInterval(show, 900);
        return false;
      }

      return cb ? cb(null, list) : that.speedList(null);
    });
  }

  /**
   * Kill Daemon
   *
   * @param {Function} cb Callback
   */
  killDaemon (cb) {
    process.env.PM2_STATUS = 'stopping'

    var that = this;

    that.Client.executeRemote('notifyKillPM2', {}, function() {});

    that._operate('deleteProcessId', 'all', function(err, list) {
      Common.printOut(conf.PREFIX_MSG + '[v] All Applications Stopped');
      process.env.PM2_SILENT = 'false';

      that.killAgent(function(err, data) {
        if (!err) {
          Common.printOut(conf.PREFIX_MSG + '[v] Agent Stopped');
        }

        that.Client.killDaemon(function(err, res) {
          if (err) Common.printError(err);
          Common.printOut(conf.PREFIX_MSG + '[v] PM2 Daemon Stopped');
          return cb ? cb(err, res) : that.exitCli(conf.SUCCESS_EXIT);
        });

      });
    })
  }

  kill (cb) {
    this.killDaemon(cb);
  }

  /////////////////////
  // Private methods //
  /////////////////////

  /**
   * Method to START / RESTART a script
   *
   * @private
   * @param {string} script script name (will be resolved according to location)
   */
  _startScript (script, opts, cb) {
    if (typeof opts == "function") {
      cb = opts;
      opts = {};
    }
    var that = this;

    /**
     * Commander.js tricks
     */
    var app_conf = Config.filterOptions(opts);
    var appConf = {};

    if (typeof app_conf.name == 'function')
      delete app_conf.name;

    delete app_conf.args;

    // Retrieve arguments via -- <args>
    var argsIndex;

    if (opts.rawArgs && (argsIndex = opts.rawArgs.indexOf('--')) >= 0)
      app_conf.args = opts.rawArgs.slice(argsIndex + 1);
    else if (opts.scriptArgs)
      app_conf.args = opts.scriptArgs;

    app_conf.script = script;
    if(!app_conf.namespace)
      app_conf.namespace = 'default';

    if ((appConf = Common.verifyConfs(app_conf)) instanceof Error) {
      Common.err(appConf)
      return cb ? cb(Common.retErr(appConf)) : that.exitCli(conf.ERROR_EXIT);
    }

    app_conf = appConf[0];

    if (opts.watchDelay) {
      if (typeof opts.watchDelay === "string" && opts.watchDelay.indexOf("ms") !== -1)
        app_conf.watch_delay = parseInt(opts.watchDelay);
      else {
        app_conf.watch_delay = parseFloat(opts.watchDelay) * 1000;
      }
    }

    var mas = [];
    if(typeof opts.ext != 'undefined')
      hf.make_available_extension(opts, mas); // for -e flag
    mas.length > 0 ? app_conf.ignore_watch = mas : 0;

    /**
     * If -w option, write configuration to configuration.json file
     */
    if (app_conf.write) {
      var dst_path = path.join(process.env.PWD || process.cwd(), app_conf.name + '-pm2.json');
      Common.printOut(conf.PREFIX_MSG + 'Writing configuration to', chalk.blue(dst_path));
      // pretty JSON
      try {
        fs.writeFileSync(dst_path, JSON.stringify(app_conf, null, 2));
      } catch (e) {
        console.error(e.stack || e);
      }
    }

    series([
      restartExistingProcessName,
      restartExistingNameSpace,
      restartExistingProcessId,
      restartExistingProcessPathOrStartNew
    ], function(err, data) {
      if (err instanceof Error)
        return cb ? cb(err) : that.exitCli(conf.ERROR_EXIT);

      var ret = {};

      data.forEach(function(_dt) {
        if (_dt !== undefined)
          ret = _dt;
      });

      return cb ? cb(null, ret) : that.speedList();
    });

    /**
     * If start <app_name> start/restart application
     */
    function restartExistingProcessName(cb) {
      if (!isNaN(script) ||
        (typeof script === 'string' && script.indexOf('/') != -1) ||
        (typeof script === 'string' && path.extname(script) !== ''))
        return cb(null);

        that.Client.getProcessIdByName(script, function(err, ids) {
          if (err && cb) return cb(err);
          if (ids.length > 0) {
            that._operate('restartProcessId', script, opts, function(err, list) {
              if (err) return cb(err);
              Common.printOut(conf.PREFIX_MSG + 'Process successfully started');
              return cb(true, list);
            });
          }
          else return cb(null);
        });
    }

    /**
     * If start <namespace> start/restart namespace
     */
    function restartExistingNameSpace(cb) {
      if (!isNaN(script) ||
        (typeof script === 'string' && script.indexOf('/') != -1) ||
        (typeof script === 'string' && path.extname(script) !== ''))
        return cb(null);

      if (script !== 'all') {
        that.Client.getProcessIdsByNamespace(script, function (err, ids) {
          if (err && cb) return cb(err);
          if (ids.length > 0) {
            that._operate('restartProcessId', script, opts, function (err, list) {
              if (err) return cb(err);
              Common.printOut(conf.PREFIX_MSG + 'Process successfully started');
              return cb(true, list);
            });
          }
          else return cb(null);
        });
      }
      else {
        that._operate('restartProcessId', 'all', function(err, list) {
          if (err) return cb(err);
          Common.printOut(conf.PREFIX_MSG + 'Process successfully started');
          return cb(true, list);
        });
      }
    }

    function restartExistingProcessId(cb) {
      if (isNaN(script)) return cb(null);

      that._operate('restartProcessId', script, opts, function(err, list) {
        if (err) return cb(err);
        Common.printOut(conf.PREFIX_MSG + 'Process successfully started');
        return cb(true, list);
      });
    }

    /**
     * Restart a process with the same full path
     * Or start it
     */
    function restartExistingProcessPathOrStartNew(cb) {
      that.Client.executeRemote('getMonitorData', {}, function(err, procs) {
        if (err) return cb ? cb(new Error(err)) : that.exitCli(conf.ERROR_EXIT);

        var full_path = path.resolve(that.cwd, script);
        var managed_script = null;

        procs.forEach(function(proc) {
          if (proc.pm2_env.pm_exec_path == full_path &&
              proc.pm2_env.name == app_conf.name)
            managed_script = proc;
        });

        if (managed_script &&
          (managed_script.pm2_env.status == conf.STOPPED_STATUS ||
            managed_script.pm2_env.status == conf.STOPPING_STATUS ||
            managed_script.pm2_env.status == conf.ERRORED_STATUS)) {
          // Restart process if stopped
          var app_name = managed_script.pm2_env.name;

          that._operate('restartProcessId', app_name, opts, function(err, list) {
            if (err) return cb ? cb(new Error(err)) : that.exitCli(conf.ERROR_EXIT);
            Common.printOut(conf.PREFIX_MSG + 'Process successfully started');
            return cb(true, list);
          });
          return false;
        }
        else if (managed_script && !opts.force) {
          Common.err('Script already launched, add -f option to force re-execution');
          return cb(new Error('Script already launched'));
        }

        var resolved_paths = null;

        try {
          resolved_paths = Common.resolveAppAttributes({
            cwd      : that.cwd,
            pm2_home : that.pm2_home
          }, app_conf);
        } catch(e) {
          Common.err(e.message);
          return cb(Common.retErr(e));
        }

        Common.printOut(conf.PREFIX_MSG + 'Starting %s in %s (%d instance' + (resolved_paths.instances > 1 ? 's' : '') + ')',
          resolved_paths.pm_exec_path, resolved_paths.exec_mode, resolved_paths.instances);

        if (!resolved_paths.env) resolved_paths.env = {};

        // Set PM2 HOME in case of child process using PM2 API
        resolved_paths.env['PM2_HOME'] = that.pm2_home;

        var additional_env = Modularizer.getAdditionalConf(resolved_paths.name);
        Object.assign(resolved_paths.env, additional_env);

        // Is KM linked?
        resolved_paths.km_link = that.gl_is_km_linked;

        that.Client.executeRemote('prepare', resolved_paths, function(err, data) {
          if (err) {
            Common.printError(conf.PREFIX_MSG_ERR + 'Error while launching application', err.stack || err);
            return cb(Common.retErr(err));
          }

          Common.printOut(conf.PREFIX_MSG + 'Done.');
          return cb(true, data);
        });
        return false;
      });
    }
  }

  /**
   * Method to start/restart/reload processes from a JSON file
   * It will start app not started
   * Can receive only option to skip applications
   *
   * @private
   */
  _startJson (file, opts, action, pipe, cb) {
    var config     = {};
    var appConf    = {};
    var staticConf = [];
    var deployConf = {};
    var apps_info  = [];
    var that = this;

    /**
     * Get File configuration
     */
    if (typeof(cb) === 'undefined' && typeof(pipe) === 'function') {
      cb = pipe;
    }
    if (typeof(file) === 'object') {
      config = file;
    } else if (pipe === 'pipe') {
      config = Common.parseConfig(file, 'pipe');
    } else {
      var data = null;

      var isAbsolute = path.isAbsolute(file)
      var file_path = isAbsolute ? file : path.join(that.cwd, file);

      debug('Resolved filepath %s', file_path);

      try {
        data = fs.readFileSync(file_path);
      } catch(e) {
        Common.printError(conf.PREFIX_MSG_ERR + 'File ' + file +' not found');
        return cb ? cb(Common.retErr(e)) : that.exitCli(conf.ERROR_EXIT);
      }

      try {
        config = Common.parseConfig(data, file);
      } catch(e) {
        Common.printError(conf.PREFIX_MSG_ERR + 'File ' + file + ' malformated');
        console.error(e);
        return cb ? cb(Common.retErr(e)) : that.exitCli(conf.ERROR_EXIT);
      }
    }

    /**
     * Alias some optional fields
     */
    if (config.deploy)
      deployConf = config.deploy;
    if (config.static)
      staticConf = config.static;
    if (config.apps)
      appConf = config.apps;
    else if (config.pm2)
      appConf = config.pm2;
    else
      appConf = config;
    if (!Array.isArray(appConf))
      appConf = [appConf];

    if ((appConf = Common.verifyConfs(appConf)) instanceof Error)
      return cb ? cb(appConf) : that.exitCli(conf.ERROR_EXIT);

    process.env.PM2_JSON_PROCESSING = true;

    // Get App list
    var apps_name = [];
    var proc_list = {};

    // Add statics to apps
    staticConf.forEach(function(serve) {
      appConf.push({
        name: serve.name ? serve.name : `static-page-server-${serve.port}`,
        script: path.resolve(__dirname, 'API', 'Serve.js'),
        env: {
          PM2_SERVE_PORT: serve.port,
          PM2_SERVE_HOST: serve.host,
          PM2_SERVE_PATH: serve.path,
          PM2_SERVE_SPA: serve.spa,
          PM2_SERVE_DIRECTORY: serve.directory,
          PM2_SERVE_BASIC_AUTH: serve.basic_auth !== undefined,
          PM2_SERVE_BASIC_AUTH_USERNAME: serve.basic_auth ? serve.basic_auth.username : null,
          PM2_SERVE_BASIC_AUTH_PASSWORD: serve.basic_auth ? serve.basic_auth.password : null,
          PM2_SERVE_MONITOR: serve.monitor
        }
      });
    });

    // Here we pick only the field we want from the CLI when starting a JSON
    appConf.forEach(function(app) {
      if (!app.env) { app.env = {}; }
      app.env.io = app.io;
      // --only <app>
      if (opts.only) {
        var apps = opts.only.split(/,| /)
        if (apps.indexOf(app.name) == -1)
          return false
      }
      // Namespace
      if (!app.namespace) {
        if (opts.namespace)
          app.namespace = opts.namespace;
        else
          app.namespace = 'default';
      }
      // --watch
      if (!app.watch && opts.watch && opts.watch === true)
        app.watch = true;
      // --ignore-watch
      if (!app.ignore_watch && opts.ignore_watch)
        app.ignore_watch = opts.ignore_watch;
      if (opts.install_url)
        app.install_url = opts.install_url;
      // --instances <nb>
      if (opts.instances && typeof(opts.instances) === 'number')
        app.instances = opts.instances;
      // --uid <user>
      if (opts.uid)
        app.uid = opts.uid;
      // --gid <user>
      if (opts.gid)
        app.gid = opts.gid;
      // Specific
      if (app.append_env_to_name && opts.env)
        app.name += ('-' + opts.env);
      if (opts.name_prefix && app.name.indexOf(opts.name_prefix) == -1)
        app.name = `${opts.name_prefix}:${app.name}`

      app.username = Common.getCurrentUsername();
      apps_name.push(app.name);
    });

    that.Client.executeRemote('getMonitorData', {}, function(err, raw_proc_list) {
      if (err) {
        Common.printError(err);
        return cb ? cb(Common.retErr(err)) : that.exitCli(conf.ERROR_EXIT);
      }

      /**
       * Uniquify in memory process list
       */
      raw_proc_list.forEach(function(proc) {
        proc_list[proc.name] = proc;
      });

      /**
       * Auto detect application already started
       * and act on them depending on action
       */
      eachLimit(Object.keys(proc_list), conf.CONCURRENT_ACTIONS, function(proc_name, next) {
        // Skip app name (--only option)
        if (apps_name.indexOf(proc_name) == -1)
          return next();

        if (!(action == 'reloadProcessId' ||
            action == 'softReloadProcessId' ||
            action == 'restartProcessId'))
          throw new Error('Wrong action called');

        var apps = appConf.filter(function(app) {
          return app.name == proc_name;
        });

        var envs = apps.map(function(app){
          // Binds env_diff to env and returns it.
          return Common.mergeEnvironmentVariables(app, opts.env, deployConf);
        });

        // Assigns own enumerable properties of all
        // Notice: if people use the same name in different apps,
        //         duplicated envs will be overrode by the last one
        var env = envs.reduce(function(e1, e2){
          return Object.assign(e1, e2);
        });

        // When we are processing JSON, allow to keep the new env by default
        env.updateEnv = true;

        // Pass `env` option
        that._operate(action, proc_name, env, function(err, ret) {
          if (err) Common.printError(err);

          // For return
          apps_info = apps_info.concat(ret);

          that.Client.notifyGod(action, proc_name);
          // And Remove from array to spy
          apps_name.splice(apps_name.indexOf(proc_name), 1);
          return next();
        });

      }, function(err) {
        if (err) return cb ? cb(Common.retErr(err)) : that.exitCli(conf.ERROR_EXIT);
        if (apps_name.length > 0 && action != 'start')
          Common.printOut(conf.PREFIX_MSG_WARNING + 'Applications %s not running, starting...', apps_name.join(', '));
        // Start missing apps
        return startApps(apps_name, function(err, apps) {
          apps_info = apps_info.concat(apps);
          return cb ? cb(err, apps_info) : that.speedList(err ? 1 : 0);
        });
      });
      return false;
    });

    function startApps(app_name_to_start, cb) {
      var apps_to_start = [];
      var apps_started = [];
      var apps_errored = [];

      appConf.forEach(function(app, i) {
        if (app_name_to_start.indexOf(app.name) != -1) {
          apps_to_start.push(appConf[i]);
        }
      });

      eachLimit(apps_to_start, conf.CONCURRENT_ACTIONS, function(app, next) {
        if (opts.cwd)
          app.cwd = opts.cwd;
        if (opts.force_name)
          app.name = opts.force_name;
        if (opts.started_as_module)
          app.pmx_module = true;

        var resolved_paths = null;

        // hardcode script name to use `serve` feature inside a process file
        if (app.script === 'serve') {
          app.script = path.resolve(__dirname, 'API', 'Serve.js')
        }

        try {
          resolved_paths = Common.resolveAppAttributes({
            cwd      : that.cwd,
            pm2_home : that.pm2_home
          }, app);
        } catch (e) {
          apps_errored.push(e)
          Common.err(`Error: ${e.message}`)
          return next();
        }

        if (!resolved_paths.env) resolved_paths.env = {};

        // Set PM2 HOME in case of child process using PM2 API
        resolved_paths.env['PM2_HOME'] = that.pm2_home;

        var additional_env = Modularizer.getAdditionalConf(resolved_paths.name);
        Object.assign(resolved_paths.env, additional_env);

        resolved_paths.env = Common.mergeEnvironmentVariables(resolved_paths, opts.env, deployConf);

        delete resolved_paths.env.current_conf;

        // Is KM linked?
        resolved_paths.km_link = that.gl_is_km_linked;

        if (resolved_paths.wait_ready) {
          Common.warn(`App ${resolved_paths.name} has option 'wait_ready' set, waiting for app to be ready...`)
        }
        that.Client.executeRemote('prepare', resolved_paths, function(err, data) {
          if (err) {
            Common.printError(conf.PREFIX_MSG_ERR + 'Process failed to launch %s', err.message ? err.message : err);
            return next();
          }
          if (data.length === 0) {
            Common.printError(conf.PREFIX_MSG_ERR + 'Process config loading failed', data);
            return next();
          }

          Common.printOut(conf.PREFIX_MSG + 'App [%s] launched (%d instances)', data[0].pm2_env.name, data.length);
          apps_started = apps_started.concat(data);
          next();
        });

      }, function(err) {
        var final_error = err || apps_errored.length > 0 ? apps_errored : null
        return cb ? cb(final_error, apps_started) : that.speedList();
      });
      return false;
    }
  }

  /**
   * Apply a RPC method on the json file
   * @private
   * @method actionFromJson
   * @param {string} action RPC Method
   * @param {object} options
   * @param {string|object} file file
   * @param {string} jsonVia action type (=only 'pipe' ?)
   * @param {Function}
   */
  actionFromJson (action, file, opts, jsonVia, cb) {
    var appConf = {};
    var ret_processes = [];
    var that = this;

    //accept programmatic calls
    if (typeof file == 'object') {
      cb = typeof jsonVia == 'function' ? jsonVia : cb;
      appConf = file;
    }
    else if (jsonVia == 'file') {
      var data = null;

      try {
        data = fs.readFileSync(file);
      } catch(e) {
        Common.printError(conf.PREFIX_MSG_ERR + 'File ' + file +' not found');
        return cb ? cb(Common.retErr(e)) : that.exitCli(conf.ERROR_EXIT);
      }

      try {
        appConf = Common.parseConfig(data, file);
      } catch(e) {
        Common.printError(conf.PREFIX_MSG_ERR + 'File ' + file + ' malformated');
        console.error(e);
        return cb ? cb(Common.retErr(e)) : that.exitCli(conf.ERROR_EXIT);
      }
    } else if (jsonVia == 'pipe') {
      appConf = Common.parseConfig(file, 'pipe');
    } else {
      Common.printError('Bad call to actionFromJson, jsonVia should be one of file, pipe');
      return that.exitCli(conf.ERROR_EXIT);
    }

    // Backward compatibility
    if (appConf.apps)
      appConf = appConf.apps;

    if (!Array.isArray(appConf))
      appConf = [appConf];

    if ((appConf = Common.verifyConfs(appConf)) instanceof Error)
      return cb ? cb(appConf) : that.exitCli(conf.ERROR_EXIT);

    eachLimit(appConf, conf.CONCURRENT_ACTIONS, function(proc, next1) {
      var name = '';
      var new_env;

      if (!proc.name)
        name = path.basename(proc.script);
      else
        name = proc.name;

      if (opts.only && opts.only != name)
        return process.nextTick(next1);

      if (opts && opts.env)
        new_env = Common.mergeEnvironmentVariables(proc, opts.env);
      else
        new_env = Common.mergeEnvironmentVariables(proc);

      that.Client.getProcessIdByName(name, function(err, ids) {
        if (err) {
          Common.printError(err);
          return next1();
        }
        if (!ids) return next1();

        eachLimit(ids, conf.CONCURRENT_ACTIONS, function(id, next2) {
          var opts = {};

          //stopProcessId could accept options to?
          if (action == 'restartProcessId') {
            opts = {id : id, env : new_env};
          } else {
            opts = id;
          }

          that.Client.executeRemote(action, opts, function(err, res) {
            ret_processes.push(res);
            if (err) {
              Common.printError(err);
              return next2();
            }

            if (action == 'restartProcessId') {
              that.Client.notifyGod('restart', id);
            } else if (action == 'deleteProcessId') {
              that.Client.notifyGod('delete', id);
            } else if (action == 'stopProcessId') {
              that.Client.notifyGod('stop', id);
            }

            Common.printOut(conf.PREFIX_MSG + '[%s](%d) \u2713', name, id);
            return next2();
          });
        }, function(err) {
          return next1(null, ret_processes);
        });
      });
    }, function(err) {
      if (cb) return cb(null, ret_processes);
      else return that.speedList();
    });
  }


  /**
   * Main function to operate with PM2 daemon
   *
   * @param {String} action_name  Name of action (restartProcessId, deleteProcessId, stopProcessId)
   * @param {String} process_name can be 'all', a id integer or process name
   * @param {Object} envs         object with CLI options / environment
   */
  _operate (action_name, process_name, envs, cb) {
    var that = this;
    var update_env = false;
    var ret = [];

    // Make sure all options exist
    if (!envs)
      envs = {};

    if (typeof(envs) == 'function'){
      cb = envs;
      envs = {};
    }

    // Set via env.update (JSON processing)
    if (envs.updateEnv === true)
      update_env = true;

    var concurrent_actions = envs.parallel || conf.CONCURRENT_ACTIONS;

    if (!process.env.PM2_JSON_PROCESSING || envs.commands) {
      envs = that._handleAttributeUpdate(envs);
    }

    /**
     * Set current updated configuration if not passed
     */
    if (!envs.current_conf) {
      var _conf = fclone(envs);
      envs = {
        current_conf : _conf
      }

      // Is KM linked?
      envs.current_conf.km_link = that.gl_is_km_linked;
    }

    /**
     * Operate action on specific process id
     */
    function processIds(ids, cb) {
      Common.printOut(conf.PREFIX_MSG + 'Applying action %s on app [%s](ids: %s)', action_name, process_name, ids);

      if (ids.length <= 2)
        concurrent_actions = 1;

      if (action_name == 'deleteProcessId')
        concurrent_actions = 10;

      eachLimit(ids, concurrent_actions, function(id, next) {
        var opts;

        // These functions need extra param to be passed
        if (action_name == 'restartProcessId' ||
          action_name == 'reloadProcessId' ||
          action_name == 'softReloadProcessId') {
          var new_env = {};

          if (update_env === true) {
            if (conf.PM2_PROGRAMMATIC == true)
              new_env = Common.safeExtend({}, process.env);
            else
              new_env = Object.assign({}, process.env);

            Object.keys(envs).forEach(function(k) {
              new_env[k] = envs[k];
            });
          }
          else {
            new_env = envs;
          }

          opts = {
            id  : id,
            env : new_env
          };
        }
        else {
          opts = id;
        }

        that.Client.executeRemote(action_name, opts, function(err, res) {
          if (err) {
            Common.printError(conf.PREFIX_MSG_ERR + 'Process %s not found', id);
            return next(`Process ${id} not found`);
          }

          if (action_name == 'restartProcessId') {
            that.Client.notifyGod('restart', id);
          } else if (action_name == 'deleteProcessId') {
            that.Client.notifyGod('delete', id);
          } else if (action_name == 'stopProcessId') {
            that.Client.notifyGod('stop', id);
          } else if (action_name == 'reloadProcessId') {
            that.Client.notifyGod('reload', id);
          } else if (action_name == 'softReloadProcessId') {
            that.Client.notifyGod('graceful reload', id);
          }

          if (!Array.isArray(res))
            res = [res];

          // Filter return
          res.forEach(function(proc) {
            Common.printOut(conf.PREFIX_MSG + '[%s](%d) \u2713', proc.pm2_env ? proc.pm2_env.name : process_name, id);

            if (action_name == 'stopProcessId' && proc.pm2_env && proc.pm2_env.cron_restart) {
              Common.warn(`App ${chalk.bold(proc.pm2_env.name)} stopped but CRON RESTART is still UP ${proc.pm2_env.cron_restart}`)
            }

            if (!proc.pm2_env) return false;

            ret.push({
              name         : proc.pm2_env.name,
              namespace: proc.pm2_env.namespace,
              pm_id        : proc.pm2_env.pm_id,
              status       : proc.pm2_env.status,
              restart_time : proc.pm2_env.restart_time,
              pm2_env : {
                name         : proc.pm2_env.name,
                namespace: proc.pm2_env.namespace,
                pm_id        : proc.pm2_env.pm_id,
                status       : proc.pm2_env.status,
                restart_time : proc.pm2_env.restart_time,
                env          : proc.pm2_env.env
              }
            });
          });

          return next();
        });
      }, function(err) {
        if (err) return cb ? cb(Common.retErr(err)) : that.exitCli(conf.ERROR_EXIT);
        return cb ? cb(null, ret) : that.speedList();
      });
    }

    if (process_name == 'all') {
      // When using shortcuts like 'all', do not delete modules
      var fn

      if (process.env.PM2_STATUS == 'stopping')
        that.Client.getAllProcessId(function(err, ids) {
          reoperate(err, ids)
        });
      else
        that.Client.getAllProcessIdWithoutModules(function(err, ids) {
          reoperate(err, ids)
        });

      function reoperate(err, ids) {
        if (err) {
          Common.printError(err);
          return cb ? cb(Common.retErr(err)) : that.exitCli(conf.ERROR_EXIT);
        }
        if (!ids || ids.length === 0) {
          Common.printError(conf.PREFIX_MSG_WARNING + 'No process found');
          return cb ? cb(new Error('process name not found')) : that.exitCli(conf.ERROR_EXIT);
        }
        return processIds(ids, cb);
      }
    }
    // operate using regex
    else if (isNaN(process_name) && process_name[0] === '/' && process_name[process_name.length - 1] === '/') {
      var regex = new RegExp(process_name.replace(/\//g, ''));

      that.Client.executeRemote('getMonitorData', {}, function(err, list) {
        if (err) {
          Common.printError('Error retrieving process list: ' + err);
          return cb(err);
        }
        var found_proc = [];
        list.forEach(function(proc) {
          if (regex.test(proc.pm2_env.name)) {
            found_proc.push(proc.pm_id);
          }
        });

        if (found_proc.length === 0) {
          Common.printError(conf.PREFIX_MSG_WARNING + 'No process found');
          return cb ? cb(new Error('process name not found')) : that.exitCli(conf.ERROR_EXIT);
        }

        return processIds(found_proc, cb);
      });
    }
    else if (isNaN(process_name)) {
      /**
       * We can not stop or delete a module but we can restart it
       * to refresh configuration variable
       */
      var allow_module_restart = action_name == 'restartProcessId' ? true : false;

      that.Client.getProcessIdByName(process_name, allow_module_restart, function (err, ids) {
        if (err) {
          Common.printError(err);
          return cb ? cb(Common.retErr(err)) : that.exitCli(conf.ERROR_EXIT);
        }
        if (ids && ids.length > 0) {
          /**
         * Determine if the process to restart is a module
         * if yes load configuration variables and merge with the current environment
         */
          var additional_env = Modularizer.getAdditionalConf(process_name);
          Object.assign(envs, additional_env);
          return processIds(ids, cb);
        }

        that.Client.getProcessIdsByNamespace(process_name, allow_module_restart, function (err, ns_process_ids) {
          if (err) {
            Common.printError(err);
            return cb ? cb(Common.retErr(err)) : that.exitCli(conf.ERROR_EXIT);
          }
          if (!ns_process_ids || ns_process_ids.length === 0) {
            Common.printError(conf.PREFIX_MSG_ERR + 'Process or Namespace %s not found', process_name);
            return cb ? cb(new Error('process or namespace not found')) : that.exitCli(conf.ERROR_EXIT);
          }

          /**
           * Determine if the process to restart is a module
           * if yes load configuration variables and merge with the current environment
           */
          var ns_additional_env = Modularizer.getAdditionalConf(process_name);
          Object.assign(envs, ns_additional_env);
          return processIds(ns_process_ids, cb);
        });
      });
    } else {
      if (that.pm2_configuration.docker == "true" ||
          that.pm2_configuration.docker == true) {
        // Docker/Systemd process interaction detection
        that.Client.executeRemote('getMonitorData', {}, (err, proc_list) => {
          var higher_id = 0
          proc_list.forEach(p => { p.pm_id > higher_id ? higher_id = p.pm_id : null })

          // Is Docker/Systemd
          if (process_name > higher_id)
            return DockerMgmt.processCommand(that, higher_id, process_name, action_name, (err) => {
              if (err) {
                Common.printError(conf.PREFIX_MSG_ERR + (err.message ? err.message : err));
                return cb ? cb(Common.retErr(err)) : that.exitCli(conf.ERROR_EXIT);
              }

              return cb ? cb(null, ret) : that.speedList();
            })

          // Check if application name as number is an app name
          that.Client.getProcessIdByName(process_name, function(err, ids) {
            if (ids.length > 0)
              return processIds(ids, cb);

            // Check if application name as number is an namespace
            that.Client.getProcessIdsByNamespace(process_name, function(err, ns_process_ids) {
              if (ns_process_ids.length > 0)
                return processIds(ns_process_ids, cb);
              // Else operate on pm id
              return processIds([process_name], cb);
            });
          });
        })
      }
      else {
        // Check if application name as number is an app name
        that.Client.getProcessIdByName(process_name, function(err, ids) {
          if (ids.length > 0)
            return processIds(ids, cb);

          // Check if application name as number is an namespace
          that.Client.getProcessIdsByNamespace(process_name, function(err, ns_process_ids) {
            if (ns_process_ids.length > 0)
              return processIds(ns_process_ids, cb);
            // Else operate on pm id
            return processIds([process_name], cb);
          });
        });
      }
    }
  }

  /**
   * Converts CamelCase Commander.js arguments
   * to Underscore
   * (nodeArgs -> node_args)
   */
  _handleAttributeUpdate (opts) {
    var conf = Config.filterOptions(opts);
    var that = this;

    if (typeof(conf.name) != 'string')
      delete conf.name;

    var argsIndex = 0;
    if (opts.rawArgs && (argsIndex = opts.rawArgs.indexOf('--')) >= 0) {
      conf.args = opts.rawArgs.slice(argsIndex + 1);
    }

    var appConf = Common.verifyConfs(conf)[0];

    if (appConf instanceof Error) {
      Common.printError('Error while transforming CamelCase args to underscore');
      return appConf;
    }

    if (argsIndex == -1)
      delete appConf.args;
    if (appConf.name == 'undefined')
      delete appConf.name;

    delete appConf.exec_mode;

    if (Array.isArray(appConf.watch) && appConf.watch.length === 0) {
      if (!~opts.rawArgs.indexOf('--watch'))
        delete appConf.watch
    }

    // Options set via environment variables
    if (process.env.PM2_DEEP_MONITORING)
      appConf.deep_monitoring = true;

    // Force deletion of defaults values set by commander
    // to avoid overriding specified configuration by user
    if (appConf.treekill === true)
      delete appConf.treekill;
    if (appConf.pmx === true)
      delete appConf.pmx;
    if (appConf.vizion === true)
      delete appConf.vizion;
    if (appConf.automation === true)
      delete appConf.automation;
    if (appConf.autostart === true)
      delete appConf.autostart;
    if (appConf.autorestart === true)
      delete appConf.autorestart;

    return appConf;
  }

  getProcessIdByName (name, cb) {
    var that = this;

    this.Client.getProcessIdByName(name, function(err, id) {
      if (err) {
        Common.printError(err);
        return cb ? cb(Common.retErr(err)) : that.exitCli(conf.ERROR_EXIT);
      }
      console.log(id);
      return cb ? cb(null, id) : that.exitCli(conf.SUCCESS_EXIT);
    });
  }

  /**
   * Description
   * @method jlist
   * @param {} debug
   * @return
   */
  jlist (debug) {
    var that = this;

    that.Client.executeRemote('getMonitorData', {}, function(err, list) {
      if (err) {
        Common.printError(err);
        return that.exitCli(conf.ERROR_EXIT);
      }

      if (debug) {
        process.stdout.write(util.inspect(list, false, null, false));
      }
      else {
        process.stdout.write(JSON.stringify(list));
      }

      that.exitCli(conf.SUCCESS_EXIT);

    });
  }

  /**
   * Display system information
   * @method slist
   * @return
   */
  slist (tree) {
    this.Client.executeRemote('getSystemData', {}, (err, sys_infos) => {
      if (err) {
        Common.err(err)
        return this.exitCli(conf.ERROR_EXIT)
      }

      if (tree === true) {
        var treeify = require('./tools/treeify.js')
        console.log(treeify.asTree(sys_infos, true))
      }
      else
        process.stdout.write(util.inspect(sys_infos, false, null, false))
      this.exitCli(conf.SUCCESS_EXIT)
    })
  }

  /**
   * Description
   * @method speedList
   * @return
   */
  speedList (code, apps_acted) {
    var that = this;
    var systemdata = null
    var acted = []

    if ((code != 0 && code != null)) {
      return that.exitCli(code ? code : conf.SUCCESS_EXIT);
    }

    if (apps_acted && apps_acted.length > 0) {
      apps_acted.forEach(proc => {
        acted.push(proc.pm2_env ? proc.pm2_env.pm_id : proc.pm_id)
      })
    }

    // Do nothing if PM2 called programmatically and not called from CLI (also in exitCli)
    if ((conf.PM2_PROGRAMMATIC && process.env.PM2_USAGE != 'CLI'))
      return false;

    return that.Client.executeRemote('getMonitorData', {}, (err, proc_list) => {
      doList(err, proc_list)
    })

    function doList(err, list) {
      if (err) {
        if (that.gl_retry == 0) {
          that.gl_retry += 1;
          return setTimeout(that.speedList.bind(that), 1400);
        }
        console.error('Error retrieving process list: %s.\nA process seems to be on infinite loop, retry in 5 seconds',err);
        return that.exitCli(conf.ERROR_EXIT);
      }
      if (process.stdout.isTTY === false) {
        UX.list_min(list);
      }
      else if (commander.miniList && !commander.silent)
        UX.list_min(list);
      else if (!commander.silent) {
        if (that.gl_interact_infos) {
          var dashboard_url = `https://app.pm2.io/#/r/${that.gl_interact_infos.public_key}`

          if (that.gl_interact_infos.info_node != 'https://root.keymetrics.io') {
            dashboard_url = `${that.gl_interact_infos.info_node}/#/r/${that.gl_interact_infos.public_key}`
          }

          Common.printOut('%s PM2+ activated | Instance Name: %s | Dash: %s',
                          chalk.green.bold('⇆'),
                          chalk.bold(that.gl_interact_infos.machine_name),
                          chalk.bold(dashboard_url))
        }
        UX.list(list, commander);
        //Common.printOut(chalk.white.italic(' Use `pm2 show <id|name>` to get more details about an app'));
      }

      if (that.Client.daemon_mode == false) {
        Common.printOut('[--no-daemon] Continue to stream logs');
        Common.printOut('[--no-daemon] Exit on target PM2 exit pid=' + fs.readFileSync(conf.PM2_PID_FILE_PATH).toString());
        global._auto_exit = true;
        return that.streamLogs('all', 0, false, 'HH:mm:ss', false);
      }
      // if (process.stdout.isTTY) if looking for start logs
      else if (!process.env.TRAVIS && process.env.NODE_ENV != 'test' && acted.length > 0 && (commander.attach === true)) {
        Common.info(`Log streaming apps id: ${chalk.cyan(acted.join(' '))}, exit with Ctrl-C or will exit in 10secs`)

        // setTimeout(() => {
        //   Common.info(`Log streaming exited automatically, run 'pm2 logs' to continue watching logs`)
        //   return that.exitCli(code ? code : conf.SUCCESS_EXIT);
        // }, 10000)

        return acted.forEach((proc_name) => {
          that.streamLogs(proc_name, 0, false, null, false);
        })
      }
      else {
        return that.exitCli(code ? code : conf.SUCCESS_EXIT);
      }
    }
  }

  /**
   * Scale up/down a process
   * @method scale
   */
  scale (app_name, number, cb) {
    var that = this;

    function addProcs(proc, value, cb) {
      (function ex(proc, number) {
        if (number-- === 0) return cb();
        Common.printOut(conf.PREFIX_MSG + 'Scaling up application');
        that.Client.executeRemote('duplicateProcessId', proc.pm2_env.pm_id, ex.bind(this, proc, number));
      })(proc, number);
    }

    function rmProcs(procs, value, cb) {
      var i = 0;

      (function ex(procs, number) {
        if (number++ === 0) return cb();
        that._operate('deleteProcessId', procs[i++].pm2_env.pm_id, ex.bind(this, procs, number));
      })(procs, number);
    }

    function end() {
      return cb ? cb(null, {success:true}) : that.speedList();
    }

    this.Client.getProcessByName(app_name, function(err, procs) {
      if (err) {
        Common.printError(err);
        return cb ? cb(Common.retErr(err)) : that.exitCli(conf.ERROR_EXIT);
      }

      if (!procs || procs.length === 0) {
        Common.printError(conf.PREFIX_MSG_ERR + 'Application %s not found', app_name);
        return cb ? cb(new Error('App not found')) : that.exitCli(conf.ERROR_EXIT);
      }

      var proc_number = procs.length;

      if (typeof(number) === 'string' && number.indexOf('+') >= 0) {
        number = parseInt(number, 10);
        return addProcs(procs[0], number, end);
      }
      else if (typeof(number) === 'string' && number.indexOf('-') >= 0) {
        number = parseInt(number, 10);
        return rmProcs(procs[0], number, end);
      }
      else {
        number = parseInt(number, 10);
        number = number - proc_number;

        if (number < 0)
          return rmProcs(procs, number, end);
        else if (number > 0)
          return addProcs(procs[0], number, end);
        else {
          Common.printError(conf.PREFIX_MSG_ERR + 'Nothing to do');
          return cb ? cb(new Error('Same process number')) : that.exitCli(conf.ERROR_EXIT);
        }
      }
    });
  }

  /**
   * Description
   * @method describeProcess
   * @param {} pm2_id
   * @return
   */
  describe (pm2_id, cb) {
    var that = this;

    var found_proc = [];

    that.Client.executeRemote('getMonitorData', {}, function(err, list) {
      if (err) {
        Common.printError('Error retrieving process list: ' + err);
        that.exitCli(conf.ERROR_EXIT);
      }

      list.forEach(function(proc) {
        if ((!isNaN(pm2_id)    && proc.pm_id == pm2_id) ||
          (typeof(pm2_id) === 'string' && proc.name  == pm2_id)) {
          found_proc.push(proc);
        }
      });

      if (found_proc.length === 0) {
        Common.printError(conf.PREFIX_MSG_WARNING + '%s doesn\'t exist', pm2_id);
        return cb ? cb(null, []) : that.exitCli(conf.ERROR_EXIT);
      }

      if (!cb) {
        found_proc.forEach(function(proc) {
          UX.describe(proc);
        });
      }

      return cb ? cb(null, found_proc) : that.exitCli(conf.SUCCESS_EXIT);
    });
  }

  /**
   * API method to perform a deep update of PM2
   * @method deepUpdate
   */
  deepUpdate (cb) {
    var that = this;

    Common.printOut(conf.PREFIX_MSG + 'Updating PM2...');

    var child = sexec("npm i -g pm2@latest; pm2 update");

    child.stdout.on('end', function() {
      Common.printOut(conf.PREFIX_MSG + 'PM2 successfully updated');
      cb ? cb(null, {success:true}) : that.exitCli(conf.SUCCESS_EXIT);
    });
  }
};


//////////////////////////
// Load all API methods //
//////////////////////////

require('./API/Extra.js')(API);
require('./API/Deploy.js')(API);
require('./API/Modules/index.js')(API);

require('./API/pm2-plus/link.js')(API);
require('./API/pm2-plus/process-selector.js')(API);
require('./API/pm2-plus/helpers.js')(API);

require('./API/Configuration.js')(API);
require('./API/Version.js')(API);
require('./API/Startup.js')(API);
require('./API/LogManagement.js')(API);
require('./API/Containerizer.js')(API);


module.exports = API;
