/*
 * forever.js: Top level include for the forever module
 *
 * (C) 2010 Charlie Robbins & the Contributors
 * MIT LICENCE
 *
 */

var fs = require('fs'),
    path = require('path'),
    events = require('events'),
    exec = require('child_process').exec,
    spawn = require('child_process').spawn,
    cliff = require('cliff'),
    nconf = require('nconf'),
    nssocket = require('nssocket'),
    timespan = require('timespan'),
    utile = require('utile'),
    winston = require('winston'),
    mkdirp = utile.mkdirp,
    async = utile.async;

var forever = exports;

//
// Setup `forever.log` to be a custom `winston` logger.
//
forever.log = new (winston.Logger)({
  transports: [
    new (winston.transports.Console)()
  ]
});

forever.log.cli();

//
// Setup `forever out` for logEvents with `winston` custom logger.
//
forever.out = new (winston.Logger)({
  transports: [
    new (winston.transports.Console)()
  ]
});

//
// ### Export Components / Settings
// Export `version` and important Prototypes from `lib/forever/*`
//
forever.initialized  = false;
forever.kill         = require('forever-monitor').kill;
forever.checkProcess = require('forever-monitor').checkProcess;
forever.root         = process.env.FOREVER_ROOT || path.join(process.env.HOME || process.env.USERPROFILE || '/root', '.forever');
forever.config       = new nconf.File({ file: path.join(forever.root, 'config.json') });
forever.Forever      = forever.Monitor = require('forever-monitor').Monitor;
forever.Worker       = require('./forever/worker').Worker;
forever.cli          = require('./forever/cli');

//
// Expose version through `pkginfo`
//
exports.version = require('../package').version;

//
// ### function getSockets (sockPath, callback)
// #### @sockPath {string} Path in which to look for UNIX domain sockets
// #### @callback {function} Continuation to pass control to when complete
// Attempts to read the files from `sockPath` if the directory does not exist,
// then it is created using `mkdirp`.
//
function getSockets(sockPath, callback) {
  var sockets;

  try {
    sockets = fs.readdirSync(sockPath);
  }
  catch (ex) {
    if (ex.code !== 'ENOENT') {
      return callback(ex);
    }

    return mkdirp(sockPath, '0755', function (err) {
      return err ? callback(err) : callback(null, []);
    });
  }

  callback(null, sockets);
}

//
// ### function getAllProcess (callback)
// #### @callback {function} Continuation to respond to when complete.
// Returns all data for processes managed by forever.
//
function getAllProcesses(callback) {
  var sockPath = forever.config.get('sockPath');

  function getProcess(name, next) {
    var fullPath = path.join(sockPath, name),
        socket = new nssocket.NsSocket();

    if (process.platform === 'win32') {
      // it needs the prefix
      fullPath = '\\\\.\\pipe\\' + fullPath;
    }

    socket.connect(fullPath, function (err) {
      if (err) {
        next(err);
      }

      socket.dataOnce(['data'], function (data) {
        data.socket = fullPath;
        next(null, data);
        socket.end();
      });

      socket.send(['data']);
    });

    socket.on('error', function (err) {
      if (err.code === 'ECONNREFUSED') {
        fs.unlink(fullPath, function () {
          next();
        });
      }
      else {
        next();
      }
    });
  }

  getSockets(sockPath, function (err, sockets) {
    if (err || (sockets && sockets.length === 0)) {
      return callback(err);
    }

    async.map(sockets, getProcess, function (err, processes) {
      callback(err, processes.filter(Boolean));
    });
  });
}

//
// ### function getAllPids ()
// Returns the set of all pids managed by forever.
// e.x. [{ pid: 12345, foreverPid: 12346 }, ...]
//
function getAllPids(processes) {
  return !processes ? null : processes.map(function (proc) {
    return {
      pid: proc.pid,
      foreverPid: proc.foreverPid
    };
  });
}

//
// ### function stopOrRestart (action, event, format, target)
// #### @action {string} Action that is to be sent to target(s).
// #### @event {string} Event that will be emitted on success.
// #### @format {boolean} Indicated if we should CLI format the returned output.
// #### @target {string} Index or script name to stop. Optional.
// ####                  If not provided -> action will be sent to all targets.
// Returns emitter that you can use to handle events on failure or success (i.e 'error' or <event>)
//
function stopOrRestart(action, event, format, target) {
  var emitter = new events.EventEmitter();

  function sendAction(proc, next) {
    var socket = new nssocket.NsSocket();

    function onMessage(data) {
      //
      // Cleanup the socket.
      //
      socket.undata([action, 'ok'],    onMessage);
      socket.undata([action, 'error'], onMessage);
      socket.end();

      //
      // Messages are only sent back from error cases. The event
      // calling context is available from `nssocket`.
      //
      var message = data && data.message,
          type    = this.event.slice().pop();

      //
      // Remark (Tjatse): This message comes from `forever-monitor`, the process is marked
      // as `STOPPED`: message: Cannot stop process that is not running.
      //
      // Remark (indexzero): We should probably warn instead of emitting an error in `forever-monitor`,
      // OR handle that error in `bin/worker` for better RPC.
      //
      return type === 'error' && /is not running/.test(message)
        ? next(new Error(message))
        : next(null, data);
    }

    socket.connect(proc.socket, function (err) {
      if (err) {
        next(err);
      }

      socket.dataOnce([action, 'ok'],    onMessage);
      socket.dataOnce([action, 'error'], onMessage);
      socket.send([action]);
    });

    //
    // Remark (indexzero): This is a race condition, but unlikely to ever hit since
    // if the socket errors we will never get any data in the first place...
    //
    socket.on('error', function (err) {
      next(err);
    });
  }


  getAllProcesses(function (err, processes) {
    if (err) {
      return process.nextTick(function () {
        emitter.emit('error', err);
      });
    }

    var procs;
    if (target !== undefined && target !== null) {
      if (isNaN(target)) {
        procs = forever.findByScript(target, processes);
      }
      procs = procs
        || forever.findById(target, processes)
        || forever.findByIndex(target, processes)
        || forever.findByUid(target, processes)
        || forever.findByPid(target, processes);
    }
    else {
      procs = processes;
    }

    if (procs && procs.length > 0) {
      async.map(procs, sendAction, function (err, results) {
        if (err) {
          emitter.emit('error', err);
        }

        //
        // Remark (indexzero): we should do something with the results.
        //
        emitter.emit(event, forever.format(format, procs));
      });
    }
    else {
      process.nextTick(function () {
        emitter.emit('error', new Error('Cannot find forever process: ' + target));
      });
    }
  });

  return emitter;
}

//
// ### function load (options, [callback])
// #### @options {Object} Options to load into the forever module
// Initializes configuration for forever module
//
forever.load = function (options) {
  // memorize current options.
  this._loadedOptions = options;

  //
  // Setup the incoming options with default options.
  //
  options           = options           || {};
  options.loglength = options.loglength || 100;
  options.logstream = options.logstream || false;
  options.root      = options.root      || forever.root;
  options.pidPath   = options.pidPath   || path.join(options.root, 'pids');
  options.sockPath  = options.sockPath  || path.join(options.root, 'sock');

  //
  // If forever is initalized and the config directories are identical
  // simply return without creating directories
  //
  if (forever.initialized && forever.config.get('root') === options.root &&
    forever.config.get('pidPath') === options.pidPath) {
    return;
  }

  forever.config = new nconf.File({ file: path.join(options.root, 'config.json') });

  //
  // Try to load the forever `config.json` from
  // the specified location.
  //
  try {
    forever.config.loadSync();
  }
  catch (ex) { }

  //
  // Setup the columns for `forever list`.
  //
  options.columns  = options.columns  || forever.config.get('columns');
  if (!options.columns) {
    options.columns = [
      'uid', 'command', 'script', 'forever', 'pid', 'id', 'logfile', 'uptime'
    ];
  }

  forever.config.set('root', options.root);
  forever.config.set('pidPath', options.pidPath);
  forever.config.set('sockPath', options.sockPath);
  forever.config.set('loglength', options.loglength);
  forever.config.set('logstream', options.logstream);
  forever.config.set('columns', options.columns);

  //
  // Setup timestamp to event logger
  //
  forever.out.transports.console.timestamp = forever.config.get('timestamp') === 'true';

  //
  // Attempt to see if `forever` has been configured to
  // run in debug mode.
  //
  options.debug = options.debug || forever.config.get('debug') || false;

  if (options.debug) {
    //
    // If we have been indicated to debug this forever process
    // then setup `forever._debug` to be an instance of `winston.Logger`.
    //
    forever._debug();
  }

  //
  // Syncronously create the `root` directory
  // and the `pid` directory for forever. Although there is
  // an additional overhead here of the sync action. It simplifies
  // the setup of forever dramatically.
  //
  function tryCreate(dir) {
    try {
      fs.mkdirSync(dir, '0755');
    }
    catch (ex) { }
  }

  tryCreate(forever.config.get('root'));
  tryCreate(forever.config.get('pidPath'));
  tryCreate(forever.config.get('sockPath'));

  //
  // Attempt to save the new `config.json` for forever
  //
  try {
    forever.config.saveSync();
  }
  catch (ex) { }

  forever.initialized = true;
};

//
// ### @private function _debug ()
// Sets up debugging for this forever process
//
forever._debug = function () {
  var debug = forever.config.get('debug');

  if (!debug) {
    forever.config.set('debug', true);
    forever.log.add(winston.transports.File, {
      level: 'silly',
      filename: path.join(forever.config.get('root'), 'forever.debug.log')
    });
  }
};

//
// Ensure forever will always be loaded the first time it is required.
//
forever.load();

//
// ### function stat (logFile, script, callback)
// #### @logFile {string} Path to the log file for this script
// #### @logAppend {boolean} Optional. True Prevent failure if the log file exists.
// #### @script {string} Path to the target script.
// #### @callback {function} Continuation to pass control back to
// Ensures that the logFile doesn't exist and that
// the target script does exist before executing callback.
//
forever.stat = function (logFile, script, callback) {
  var logAppend;

  if (arguments.length === 4) {
    logAppend = callback;
    callback = arguments[3];
  }

  fs.stat(script, function (err, stats) {
    if (err) {
      return callback(new Error('script ' + script + ' does not exist.'));
    }

    return logAppend ? callback(null) : fs.stat(logFile, function (err, stats) {
      return !err
        ? callback(new Error('log file ' + logFile + ' exists. Use the -a or --append option to append log.'))
        : callback(null);
    });
  });
};

//
// ### function start (script, options)
// #### @script {string} Location of the script to run.
// #### @options {Object} Configuration for forever instance.
// Starts a script with forever
//
forever.start = function (script, options) {
  if (!options.uid) {
    options.uid = utile.randomString(4).replace(/^\-/, '_');
  }

  if (!options.logFile) {
    options.logFile = forever.logFilePath(options.uid + '.log');
  }

  //
  // Create the monitor, log events, and start.
  //
  var monitor = new forever.Monitor(script, options);
  forever.logEvents(monitor);
  return monitor.start();
};

//
// ### function startDaemon (script, options)
// #### @script {string} Location of the script to run.
// #### @options {Object} Configuration for forever instance.
// Starts a script with forever as a daemon
//
forever.startDaemon = function (script, options) {
  options         = options || {};
  options.uid     = options.uid || utile.randomString(4).replace(/^\-/, '_');
  options.logFile = forever.logFilePath(options.logFile || forever.config.get('logFile') || options.uid + '.log');
  options.pidFile = forever.pidFilePath(options.pidFile || forever.config.get('pidFile') || options.uid + '.pid');

  var monitor, outFD, errFD, monitorPath;

  //
  // This log file is forever's log file - the user's outFile and errFile
  // options are not taken into account here.  This will be an aggregate of all
  // the app's output, as well as messages from the monitor process, where
  // applicable.
  //
  outFD = fs.openSync(options.logFile, 'a');
  errFD = fs.openSync(options.logFile, 'a');
  monitorPath = path.resolve(__dirname, '..', 'bin', 'monitor');

  monitor = spawn(process.execPath, [monitorPath, script], {
    stdio: ['ipc', outFD, errFD],
    detached: true
  });

  monitor.on('exit', function (code) {
    console.error('Monitor died unexpectedly with exit code %d', code);
  });

  // transmit options to daemonic(child) process, keep configuration lineage.
  options._loadedOptions = this._loadedOptions;

  monitor.send(JSON.stringify(options));

  // close the ipc communication channel with the monitor
  // otherwise the corresponding events listeners will prevent
  // the exit of the current process (observed with node 0.11.9)
  monitor.disconnect();

  // make sure the monitor is unref() and does not prevent the
  // exit of the current process
  monitor.unref();

  return monitor;
};

//
// ### function startServer ()
// #### @arguments {forever.Monitor...} A list of forever.Monitor instances
// Starts the `forever` HTTP server for communication with the forever CLI.
// **NOTE:** This will change your `process.title`.
//
forever.startServer = function () {
  var args = Array.prototype.slice.call(arguments),
      monitors = [],
      callback;

  args.forEach(function (a) {
    if (Array.isArray(a)) {
      monitors = monitors.concat(a.filter(function (m) {
        return m instanceof forever.Monitor;
      }));
    }
    else if (a instanceof forever.Monitor) {
      monitors.push(a);
    }
    else if (typeof a === 'function') {
      callback = a;
    }
  });

  async.map(monitors, function (monitor, next) {
    var worker = new forever.Worker({
      monitor: monitor,
      sockPath: forever.config.get('sockPath'),
      exitOnStop: true
    });

    worker.start(function (err) {
      return err ? next(err) : next(null, worker);
    });
  }, callback || function () {});
};


//
// ### function stop (target, [format])
// #### @target {string} Index or script name to stop
// #### @format {boolean} Indicated if we should CLI format the returned output.
// Stops the process(es) with the specified index or script name
// in the list of all processes
//
forever.stop = function (target, format) {
  return stopOrRestart('stop', 'stop', format, target);
};

//
// ### function restart (target, format)
// #### @target {string} Index or script name to restart
// #### @format {boolean} Indicated if we should CLI format the returned output.
// Restarts the process(es) with the specified index or script name
// in the list of all processes
//
forever.restart = function (target, format) {
  return stopOrRestart('restart', 'restart', format, target);
};

//
// ### function stopbypid (target, format)
// #### @pid {string} Pid of process to stop.
// #### @format {boolean} Indicated if we should CLI format the returned output.
// Stops the process with specified pid
//
forever.stopbypid = function (pid, format) {
  // stopByPid only capable of stopping, but can't restart
  return stopOrRestart('stop', 'stopByPid', format, pid);
};

//
// ### function restartAll (format)
// #### @format {boolean} Value indicating if we should format output
// Restarts all processes managed by forever.
//
forever.restartAll = function (format) {
  return stopOrRestart('restart', 'restartAll', format);
};

//
// ### function stopAll (format)
// #### @format {boolean} Value indicating if we should format output
// Stops all processes managed by forever.
//
forever.stopAll = function (format) {
  return stopOrRestart('stop', 'stopAll', format);
};

//
// ### function list (format, procs, callback)
// #### @format {boolean} If set, will return a formatted string of data
// #### @callback {function} Continuation to respond to when complete.
// Returns the list of all process data managed by forever.
//
forever.list = function (format, callback) {
  getAllProcesses(function (err, processes) {
    callback(err, forever.format(format, processes));
  });
};

//
// ### function tail (target, length, callback)
// #### @target {string} Target script to list logs for
// #### @options {length|stream} **Optional** Length of the logs to tail, boolean stream
// #### @callback {function} Continuation to respond to when complete.
// Responds with the latest `length` logs for the specified `target` process
// managed by forever. If no `length` is supplied then `forever.config.get('loglength`)`
// is used.
//
forever.tail = function (target, options, callback) {
  if (!callback && typeof options === 'function') {
    callback = options;
    options.length = 0;
    options.stream = false;
  }

  var that   = this,
      length = options.length || forever.config.get('loglength'),
      stream = options.stream || forever.config.get('logstream'),
      blanks = function (e, i, a) { return e !== ''; },
      title  = function (e, i, a) { return e.match(/^==>/); },
      args   = ['-n', length],
      logs;

  if (stream) { args.unshift('-f'); }

  function tailProcess(procs, next) {
    var count = 0,
        map   = {},
        tail;

    procs.forEach(function (proc) {
      args.push(proc.logFile);
      map[proc.logFile] = { pid: proc.pid, file: proc.file };
      count++;
    });

    tail = spawn('tail', args, {
      stdio: [null, 'pipe', 'pipe'],
    });

    tail.stdio[1].setEncoding('utf8');
    tail.stdio[2].setEncoding('utf8');

    tail.stdio[1].on('data', function (data) {
      var chunk = data.split('\n\n');
      chunk.forEach(function (logs) {
        var logs = logs.split('\n').filter(blanks),
            file = logs.filter(title),
            lines,
            proc;

        proc = file.length
          ? map[file[0].split(' ')[1]]
          : map[procs[0].logFile];

        lines = count !== 1
          ? logs.slice(1)
          : logs;

        lines.forEach(function (line) {
          callback(null, { file: proc.file, pid: proc.pid, line: line });
        });
      });
    });

    tail.stdio[2].on('data', function (err) {
      return callback(err);
    });
  }

  getAllProcesses(function (err, processes) {
    if (err) {
      return callback(err);
    }
    else if (!processes) {
      return callback(new Error('Cannot find forever process: ' + target));
    }

    var procs = forever.findByIndex(target, processes)
      || forever.findByScript(target, processes);

    if (!procs) {
      return callback(new Error('No logs available for process: ' + target));
    }

    tailProcess(procs, callback);
  });
};

//
// ### function findById (id, processes)
// #### @id {string} id of the process to find.
// #### @processes {Array} Set of processes to find in.
// Finds the process with the specified index.
//
forever.findById = function (id, processes) {
  if (!processes) { return null; }

  var procs = processes.filter(function (p) {
    return p.id === id;
  });

  if (procs.length === 0) { procs = null; }
  return procs;
};

//
// ### function findByIndex (index, processes)
// #### @index {string} Index of the process to find.
// #### @processes {Array} Set of processes to find in.
// Finds the process with the specified index.
//
forever.findByIndex = function (index, processes) {
  var indexAsNum = parseInt(index, 10),
      proc;

  if (indexAsNum == index) {
    proc = processes && processes[indexAsNum];
  }
  return proc ? [proc] : null;
};

//
// ### function findByScript (script, processes)
// #### @script {string} The name of the script to find.
// #### @processes {Array} Set of processes to find in.
// Finds the process with the specified script name.
//
forever.findByScript = function (script, processes) {
  if (!processes) { return null; }

  // make script absolute.
  if (script.indexOf('/') != 0) {
    script = path.resolve(process.cwd(), script);
  }

  var procs = processes.filter(function (p) {
    return p.file === script || path.join(p.spawnWith.cwd, p.file) === script;
  });

  if (procs.length === 0) { procs = null; }
  return procs;
};

//
// ### function findByUid (uid, processes)
// #### @uid {string} The uid of the process to find.
// #### @processes {Array} Set of processes to find in.
// Finds the process with the specified uid.
//
forever.findByUid = function (script, processes) {
  var procs = !processes
    ? null
    : processes.filter(function (p) {
      return p.uid === script;
    });

  if (procs && procs.length === 0) { procs = null; }
  return procs;
};

//
// ### function findByPid (pid, processes)
// #### @pid {string} The pid of the process to find.
// #### @processes {Array} Set of processes to find in.
// Finds the process with the specified pid.
//
forever.findByPid = function (pid, processes) {
  pid = typeof pid === 'string'
    ? parseInt(pid, 10)
    : pid;

  var procs = processes && processes.filter(function (p) {
    return p.pid === pid;
  });

  if (procs && procs.length === 0) { procs = null; }
  return procs || null;
};

//
// ### function format (format, procs)
// #### @format {Boolean} Value indicating if processes should be formatted
// #### @procs {Array} Processes to format
// Returns a formatted version of the `procs` supplied based on the column
// configuration in `forever.config`.
//
forever.format = function (format, procs) {
  if (!procs || procs.length === 0) {
    return null;
  }

  var index = 0,
      columns = forever.config.get('columns'),
      rows = [['   '].concat(columns)],
      formatted;

  function mapColumns(prefix, mapFn) {
    return [prefix].concat(columns.map(mapFn));
  }

  if (format) {
    //
    // Iterate over the procs to see which has the
    // longest options string
    //
    procs.forEach(function (proc) {
      rows.push(mapColumns('[' + index + ']', function (column) {
        return forever.columns[column]
          ? forever.columns[column].get(proc)
          : 'MISSING';
      }));

      index++;
    });

    formatted = cliff.stringifyRows(rows, mapColumns('white', function (column) {
      return forever.columns[column]
        ? forever.columns[column].color
        : 'white';
    }));
  }

  return format ? formatted : procs;
};

//
// ### function cleanUp ()
// Utility function for removing excess pid and
// config, and log files used by forever.
//
forever.cleanUp = function (cleanLogs, allowManager) {
  var emitter = new events.EventEmitter(),
      pidPath = forever.config.get('pidPath');

  getAllProcesses(function (err, processes) {
    if (err) {
      return process.nextTick(function () {
        emitter.emit('error', err);
      });
    }
    else if (cleanLogs) {
      forever.cleanLogsSync(processes);
    }

    function unlinkProcess(proc, done) {
      fs.unlink(path.join(pidPath, proc.uid + '.pid'), function () {
        //
        // Ignore errors (in case the file doesnt exist).
        //

        if (cleanLogs && proc.logFile) {
          //
          // If we are cleaning logs then do so if the process
          // has a logfile.
          //
          return fs.unlink(proc.logFile, function () {
            done();
          });
        }

        done();
      });
    }

    function cleanProcess(proc, done) {
      if (proc.child && proc.manager) {
        return done();
      }
      else if (!proc.child && !proc.manager
        || (!proc.child && proc.manager && allowManager)
        || proc.dead) {
        return unlinkProcess(proc, done);
      }

      //
      // If we have a manager but no child, wait a moment
      // in-case the child is currently restarting, but **only**
      // if we have not already waited for this process
      //
      if (!proc.waited) {
        proc.waited = true;
        return setTimeout(function () {
          checkProcess(proc, done);
        }, 500);
      }

      done();
    }

    function checkProcess(proc, next) {
      proc.child = forever.checkProcess(proc.pid);
      proc.manager = forever.checkProcess(proc.foreverPid);
      cleanProcess(proc, next);
    }

    if (processes && processes.length > 0) {
      (function cleanBatch(batch) {
        async.forEach(batch, checkProcess, function () {
          return processes.length > 0
            ? cleanBatch(processes.splice(0, 10))
            : emitter.emit('cleanUp');
        });
      })(processes.splice(0, 10));
    }
    else {
      process.nextTick(function () {
        emitter.emit('cleanUp');
      });
    }
  });

  return emitter;
};

//
// ### function cleanLogsSync (processes)
// #### @processes {Array} The set of all forever processes
// Removes all log files from the root forever directory
// that do not belong to current running forever processes.
//
forever.cleanLogsSync = function (processes) {
  var root = forever.config.get('root'),
      files = fs.readdirSync(root),
      running,
      runningLogs;

  running = processes && processes.filter(function (p) {
    return p && p.logFile;
  });

  runningLogs = running && running.map(function (p) {
    return p.logFile.split('/').pop();
  });

  files.forEach(function (file) {
    if (/\.log$/.test(file) && (!runningLogs || runningLogs.indexOf(file) === -1)) {
      fs.unlinkSync(path.join(root, file));
    }
  });
};

//
// ### function logFilePath (logFile)
// #### @logFile {string} Log file path
// Determines the full logfile path name
//
forever.logFilePath = function (logFile, uid) {
  return logFile && (logFile[0] === '/' || logFile[1] === ':')
    ? logFile
    : path.join(forever.config.get('root'), logFile || (uid || 'forever') + '.log');
};

//
// ### function pidFilePath (pidFile)
// #### @logFile {string} Pid file path
// Determines the full pid file path name
//
forever.pidFilePath = function (pidFile) {
  return pidFile && (pidFile[0] === '/' || pidFile[1] === ':')
    ? pidFile
    : path.join(forever.config.get('pidPath'), pidFile);
};

//
// ### @function logEvents (monitor)
// #### @monitor {forever.Monitor} Monitor to log events for
// Logs important restart and error events to `console.error`
//
forever.logEvents = function (monitor) {
  monitor.on('watch:error', function (info) {
    forever.out.error(info.message);
    forever.out.error(info.error);
  });

  monitor.on('watch:restart', function (info) {
    forever.out.error('restarting script because ' + info.file + ' changed');
  });

  monitor.on('restart', function () {
    forever.out.error('Script restart attempt #' + monitor.times);
  });

  monitor.on('exit:code', function (code, signal) {
    forever.out.error((code !== null && code !== undefined)
      ? 'Forever detected script exited with code: ' + code
      : 'Forever detected script was killed by signal: ' + signal);
  });
};

//
// ### @columns {Object}
// Property descriptors for accessing forever column information
// through `forever list` and `forever.list()`
//
forever.columns = {
  uid: {
    color: 'white',
    get: function (proc) {
      return proc.uid;
    }
  },
  id: {
    color: 'white',
    get: function (proc) {
      return proc.id ? proc.id : '';
    }
  },
  command: {
    color: 'grey',
    get: function (proc) {
      return (proc.command || 'node').grey;
    }
  },
  script: {
    color: 'grey',
    get: function (proc) {
      return [proc.file].concat(proc.args).join(' ').grey;
    }
  },
  forever: {
    color: 'white',
    get: function (proc) {
      return proc.foreverPid;
    }
  },
  pid: {
    color: 'white',
    get: function (proc) {
      return proc.pid;
    }
  },
  logfile: {
    color: 'magenta',
    get: function (proc) {
      return proc.logFile ? proc.logFile.magenta : '';
    }
  },
  dir: {
    color: 'grey',
    get: function (proc) {
      return proc.sourceDir.grey;
    }
  },
  uptime: {
    color: 'yellow',
    get: function (proc) {
      return proc.running ? timespan.fromDates(new Date(proc.ctime), new Date()).toString().yellow : "STOPPED".red;
    }
  }
};
