code/main.js

'use strict';

/**
 * MainService
 * @namespace MainService
 * @description Manages main init function and commands dispatching.
 */

const hooks = require('./hooks');
const telegram = require('./telegram');
const express = require('./express');
const monitor = require('./monitor');
const local = require('./local');
const logger = require('./logger');
const rpc = require('./rpc');
const commandline = require('./commandline');
const path = require('path');
const Promise = require('promise');
const commandLineCommands = require('command-line-commands');
const getUsage = require('command-line-usage');
const ansi = require('ansi-escape-sequences');
const package_desc = require('../package.json');

const _ = require('underscore');
const s = require('underscore.string');
_.mixin(s.exports());

let tcid = null;
let pid_path = path.resolve(__dirname, '..', '.pid');

const cli_common_conf = [{
  name: 'verbose',
  alias: 'V',
  type: Boolean
}, {
  name: 'telegramid',
  alias: 'T',
  type: String
}, {
  name: 'token',
  alias: 'K',
  type: String
}, {
  name: 'config-dir',
  type: String
}, {
  name: 'config-file',
  type: String
}, {
  name: 'hooks-dir',
  type: String
}, {
  name: 'log-file',
  type: String
}];

let monitor_control_def = _.union([], cli_common_conf, [{
  name: 'hook',
  alias: 'H',
  type: String,
  defaultOption: true
}]);

let help_def = _.union([], cli_common_conf, [{
  name: 'hook',
  alias: 'H',
  type: String,
  defaultOption: true
}]);

/**
 * @class
 * @classdesc Manages main init function and commands dispatching.
 */

const MainService = {
  /**
   * @function help
   * @description Returns command line help interface.<br/>Will include commandline hooks' help.
   * @static
   * @param {Config} config Link to config manager
   * @param {String} hook_name You can pass a commandline hook name or command to have a detailed help here.
   * @memberof MainService
   * @public
   * @returns {Promise}
   */
  help: function (config, hook_name) {
    return new Promise(function (resolve, reject) {
      hooks.load().then(function () {
        let cm_hooks = hooks.get_hooks('has_command_line_hook');

        let help = '';
        let footer = '-----------------------------';
        hook_name = (hook_name || '');

        let header = [
          '',
          ansi.format(ansi.format(require('../assets/ansi-header'), 'cyan')),
          '',
          ansi.format(`${package_desc.name} v${package_desc.version}`, 'bold'),
          `${package_desc.description}`,
          ''
        ];

        if (hook_name) {
          switch (hook_name) {
          case 'help':
            help = getUsage(help_def, {
              header: header,
              description: 'This Help',
              title: 'Command: help',
              synopsis: 'The output of this help will change according to installed hooks'
            });
            break;
          case 'start':
            help = getUsage(cli_common_conf, {
              header: header,
              description: 'Start the server',
              title: 'Command: start',
              synopsis: 'Will start the main receiving server'
            });
            break;
          case 'reload':
            help = getUsage(cli_common_conf, {
              header: header,
              description: 'Reload hooks',
              title: 'Command: reload',
              synopsis: 'Will reload hook in the main server'
            });
            break;
          case 'stop':
            help = getUsage(cli_common_conf, {
              header: header,
              description: 'Stop the server',
              title: 'Command: stop',
              synopsis: 'Will stop the main receiving server'
            });
            break;
          case 'monitor:start':
            help = getUsage(monitor_control_def, {
              header: header,
              description: 'Start a monitor',
              title: 'Command: monitor:start',
              synopsis: 'Will start a monitoring hook'
            });
            break;
          case 'monitor:stop':
            help = getUsage(monitor_control_def, {
              header: header,
              description: 'Stop a monitor',
              title: 'Command: monitor:stop',
              synopsis: 'Will stop a monitoring hook'
            });
            break;
          case 'monitor:restart':
            help = getUsage(monitor_control_def, {
              header: header,
              description: 'Restart a monitor',
              title: 'Command: monitor:restart',
              synopsis: 'Will restart a monitoring hook'
            });
            break;
          default:
            if (config.get('commandline:active') !== false) {
              for (let i = 0; i < cm_hooks.length; i++) {
                let cml = cm_hooks[i];
                if (_.isArray(cml.params)) {
                  help = getUsage(_.union([], cli_common_conf, cml.params), {
                    header: header,
                    description: cml.description,
                    synopsis: cml.help,
                    title: `Command: ${cml.cmd_name}`
                  });
                  break;
                } else {
                  help = getUsage(cli_common_conf, {
                    header: header,
                    description: cml.description,
                    synopsis: cml.help,
                    title: `Command: ${cml.cmd_name}`
                  });
                  break;
                }
              }
            }
            break;
          }
        } else {
          let commands = ['help', 'start', 'stop', 'reload', 'monitor:start', 'monitor:stop', 'monitor:restart'];
          if (config.get('commandline:active') !== false) {
            for (let i = 0; i < cm_hooks.length; i++) {
              let cml = cm_hooks[i];
              commands.push(cml.cmd_name);
            }
          }
          help = getUsage([], {
            header: header,
            synopsis: [
              ansi.format(`${commands.length} commands defined:`, 'bold'),
              '',
              commands.join(', '),
              '',
              footer,
              '',
              'use command ' + ansi.format('help', 'cyan') + ' followed by ' + ansi.format('command name', 'cyan') + ' to print command specific help'
            ]
          });
        }
        return resolve(help);
      }).catch(reject);
    });
  },
  /**
   * @function parse_commands
   * @description Parse command line arguments and commands
   * @static
   * @param {Config} config Link to config manager
   * @param {Object} cmd_arguments Command arguments override
   * @memberof MainService
   * @public
   * @returns {Promise}
   */
  parse_commands: function (config, cmd_arguments) {
    return new Promise(function (resolve, reject) {
      hooks.load().then(function () {
        let cm_hooks = hooks.get_hooks('has_command_line_hook');

        let cla = [{
          name: 'help',
          definitions: help_def
        }, {
          name: 'start',
          definitions: cli_common_conf
        }, {
          name: 'stop',
          definitions: cli_common_conf
        }, {
          name: 'reload',
          definitions: cli_common_conf
        }, {
          name: 'monitor:start',
          definitions: monitor_control_def
        }, {
          name: 'monitor:stop',
          definitions: monitor_control_def
        }, {
          name: 'monitor:restart',
          definitions: monitor_control_def
        }];

        if (config.get('commandline:active') !== false) {
          for (let i = 0; i < cm_hooks.length; i++) {
            let cml = cm_hooks[i];
            if (_.isArray(cml.params)) {
              cla.push({
                name: cml.cmd_name,
                definitions: _.union([], cli_common_conf, cml.params)
              });
            } else {
              cla.push({
                name: cml.cmd_name,
                definitions: cli_common_conf
              });
            }
            logger.notify(`Registered '${cml.full_name}' command line hook with command ${cml.cmd_name}`);
          }
        }

        const cli = commandLineCommands(cla);

        let command_line_res = null;

        try {
          if (cmd_arguments) {
            command_line_res = cli.parse(cmd_arguments);
          } else {
            command_line_res = cli.parse();
          }
        } catch (e) {
          logger.error(e);
        }

        const command = _.extend({}, command_line_res);

        tcid = command.telegramid;

        if (command.token) {
          config.set('telegram:token', command.token);
        }

        if (command['config-dir'] || command['config-file']) {
          config.load_from(command['config-file'] || command['config-dir']);
        }

        if (command['hooks-dir'] && config.get('hooks:folder') != command['hooks-dir']) {
          config.set('hooks:folder', command['hooks-dir']);
          return hooks.reload(command['hooks-dir']).then(function () {
            return parse_command(config, cmd_arguments).then(resolve).catch(reject);
          }).catch(reject);
        }

        command.name = (command.name || '');
        command.options = (command.options || {});

        resolve(command);

      }).catch(reject);

    });
  },
  /**
   * @function start_server
   * @description Starts the service
   * @static
   * @memberof MainService
   * @public
   * @returns {Promise}
   */
  start_server: function () {
    return new Promise(function (resolve, reject) {

      hooks.load().then(function () {
        let cm_hooks = hooks.get_hooks('has_command_line_hook');

        const npid = require('npid');
        const pid = npid.create(pid_path, true);
        pid.removeOnExit();
        process.on('uncaughtException', function (err) {
          pid.remove();
          return reject(err);
        });
        process.on('SIGINT', function () {
          pid.remove();
          resolve(`${package_desc.name} v${package_desc.version} stopped.`);
        });
        logger.log(`${package_desc.name} v${package_desc.version} starting...`);

        return telegram.init(tcid).then(local.init).then(monitor.init).then(express.init).then(rpc.init).then(function () {
          logger.log(`${package_desc.name} v${package_desc.version} started.`);
        }).catch(function (error) {
          reject(error);
        });
      }).catch(reject);
    });
  },

  /**
   * @function stop_server
   * @description Stops the service
   * @static
   * @memberof MainService
   * @public
   * @returns {Promise}
   */
  stop_server: function () {
    return new Promise(function (resolve, reject) {
      let fs = require('fs');
      let terminate = Promise.denodeify(require('terminate'));
      const read = Promise.denodeify(fs.readFile);
      let mainpid = '';
      return read(pid_path, 'utf8').then(function (running_pid) {
        mainpid = running_pid;
        return Promise.resolve(mainpid);
      }).then(terminate).then(function (done) {
        return new Promise(function (resolve, reject) {
          if (done) {
            const exec = require('child_process').exec;
            exec(`kill -2 ${mainpid}`, function (error, stdout, stderr) {
              if (error) {
                return reject(error);
              }
              if (stderr.length > 0) {
                return reject(stderr.toString('utf8'));
              } else {
                logger.log(`${package_desc.name} v${package_desc.version} has been stopped.`);
              }
              resolve(true);
            });
          } else {
            resolve(false);
          }
        });
      }).catch(function (error) {
        if (error.code != 'ENOENT') {
          logger.error(error);
        } else {
          logger.log(`${package_desc.name} v${package_desc.version} is not running.`);
        }
      }).finally(function () {
        resolve();
      });
    });
  },
  /**
   * @function main
   * @description Main app
   * @static
   * @param {Config} config Link to config manager
   * @param {Object} cmd_arguments Command arguments override
   * @memberof MainService
   * @public
   * @returns {Promise}
   */

  main: function (config, cmd_arguments) {
    return new Promise(function (resolve, reject) {
      return MainService.parse_commands(config, cmd_arguments).then(function (command) {
        if (command.options.verbose) {
          config.set('verbose', true);
        }
        if (command.options['log-file']) {
          logger.set_log_file(command.options['log-file']);
        }
        switch (command.name) {
        case 'help':
          MainService.help(config, command.options.hook).then(resolve).catch(reject);
          break;
        case '':
          return reject('No command specified');
        case 'monitor:start':
          rpc.send('start_monitor', command.options.hook).then(resolve).catch(reject);
          break;
        case 'monitor:stop':
          rpc.send('stop_monitor', command.options.hook).then(resolve).catch(reject);
          break;
        case 'monitor:restart':
          rpc.send('restart_monitor', command.options.hook).then(resolve).catch(reject);
          break;
        case 'stop':
          return MainService.stop_server().then(resolve).catch(reject);
          break;
        case 'start':
          return MainService.start_server().then(resolve).catch(reject);
          break;
        case 'reload':
          rpc.send('reload_hooks').then(resolve).catch(reject);
          break;
        default:
          if (config.get('commandline:active') !== false) {
            return telegram.init(tcid, true).then(commandline.init).then(function () {
              commandline.execute(command.name, command.options).then(resolve).catch(reject);
            }).catch(function (error) {
              logger.error(error);
            });
          } else {
            reject('Command line not active');
          }
          break;
        }
      }).catch(reject);
    });
  }
};

module.exports = MainService;