modules/cmd/argv.js

/**
 * Command line utils module. Contains service functions for working with the command line arguments
 * @example
     var argv = require('cmd/argv');
     // connect to server
     var session = argv.establishConnectionFromCmdLineAttributes();
     console.log('Session.uData: ', session.uData, typeof session.uData, session.uData.lang);

     userLang = session.uData.lang || 'en';
     conn = session.connection;
     // obtain domain information
     var domainInfo = conn.getDomainInfo();
 * @module cmd/argv
 */
var
    assert = require('assert'),
    ok = assert.ok,
    fs = require('fs'),
    http = require('http'),
    options = require('./options'),
    UBConnection = require('UBConnection');

/**
 * @deprecated Use `options.switchIndex' instead
 * @param switchName
 * @returns {Number} switch index if found or -1 otherwise
 */
exports.findCmdLineSwitch = options.switchIndex;
/**
 * @deprecated Use `options.switchValue' instead
 * @param switchName
 * @returns {String} switch value or `undefined` in case switch not found or switch not have value
 */
exports.findCmdLineSwitchValue = options.switchValue;

/**
 *	Get config file name. if -cfg switch passed then use this switch value, else use default
 *	@return {String}
 */
exports.getConfigFileName = function getConfigFileName() {
    var
        cfgFile;

    if (cfgFile = options.switchValue('cfg')) {
        cfgFile = relToAbs(process.cwd(), cfgFile);
        if (!fs.isFile(cfgFile)){
            console.warn('passed -cfg file not exist ' + cfgFile);
            cfgFile = '';
        }
    }
    if (!cfgFile){
        cfgFile = process.cwd() + 'ubConfig.json';
        cfgFile = fs.isFile(cfgFile) ? cfgFile: '';
    }
    if (!cfgFile){
        cfgFile = process.binPath + 'ubConfig.json';
        cfgFile = fs.isFile(cfgFile) ? cfgFile: '';
    }
    if (!cfgFile) throw new Error('Server configuration file not found');
    return cfgFile;
};


/**
 * Describe set of command line attributes taken by {@link cmd.argv#establishConnectionFromCmdLineAttributes} method.
 *
 * To be used during display command help message:
 *
        var argv = require('./argv');
        if (argv.findCmdLineSwitch('help') !== -1){
            console.info([
                'This command throw error in case server is started',
                'Usage: ',
                    '>UB -f cmd/checkServerNotStarted ' + argv.establishConnectionFromCmdLineAttributesUsageInfo
            ].join('\r\n'));
            return;
        }
 * @type {String}
 */
exports.establishConnectionFromCmdLineAttributesUsageInfo = [
    '[-host http(s)://hostName:port] [-u userName] [-p password] [-cfg localServerConfig]',
    'Where:',
    '  -host Server URL to connect, including protocol. If omitted UB_HOST environment variable used. Default is `http://localhost:888`',
    '  -u    User name (if omitted UB_USER environment variable used)',
    '  -p    User password (if omitted UB_PWD environment variable used)',
    '  -cfg  Path to server config'
].join('\r\n');

var verboseMode = options.switchIndex('noLogo') === -1 ;

/**
 * @class ServerSession
 */
function ServerSession(config){
    /**
     * @type {String}
     * @readonly
     */
    this.HOST = config.host;
    /**
     * @type {String}
     * @readonly
     */
    this.USER = config.user;
    /**
     * @type {String}
     * @readonly
     */
    this.PWD = config.pwd;
    /**
     * Custom user data returned by server login method
     * @type {String}
     * @readonly
     */
    this.uData =  null;
    this.__serverStartedByMe =  false;
    /**
     * @type {UBConnection}
     */
    this.connection = null;
    /**
     * Shut down server in case it started during connection establish or logout from remote server
     * @method
     */
    this.logout = function () {
        if (this.__serverStartedByMe) {
            if (verboseMode) console.info('Shut down local server');
            stopServer();
        } else {
            this.connection.logout();
        }
    };

    /**
     * Result of `getAppInfo` endpoint execution
     * @type {Object}
     */
    this.appInfo = {};
}

/**
 * Parse cmd line and environment variables for command line parameters expected by UnityBase `cmd` mode
 * @return {ServerSession}
 */
exports.serverSessionFromCmdLineAttributes = function serverSessionFromCmdLineAttributes(config) {
    if (!config){
        config = options.describe('', '').add(exports.establishConnectionFromCmdLineAttributes._cmdLineParams).parse();
    }

    return new ServerSession(config);
};

/**
 * Service function for establish UnityBase server connection from client-side command line script.
 * Parse command line attributes for switches `host`, `u`, `p` and:
 *
 *  - Check specified server is started (simple request to `host`) and if not started then
 *      start server locally with local config
 *  - Establish connection to specified host
 *  - Retrieve application information and in case authorization is required then call login method using `u` and `p` params
 *  - Return serverSession object with connection in case of success or throw assertion error
 * @param {Object} [config]
 * @param {String} [config.host]
 * @param {String} [config.user]
 * @param {String} [config.pwd]
 * @param {Boolean} [config.forceStartServer=false} If we sure local server not started - start it without checking. Faster because check take near 2 sec.
 * @return {ServerSession}
 */
exports.establishConnectionFromCmdLineAttributes  = function establishConnectionFromCmdLineAttributes(config){
    var
        serverSession,conn;

    if (!config){ // for a backward compatibility with UB 1.11
        config = options.describe('', '').add(exports.establishConnectionFromCmdLineAttributes._cmdLineParams).parse();
    }
    serverSession = this.serverSessionFromCmdLineAttributes(config);
    // in case we operate from GUI server use existed connection
    if (global.conn){
        serverSession.connection = global.conn;
    } else {
        //if ((hostStart === 'localhost') || (hostStart === '127') || (hostStart === '10')) {
            var serverStarted = config.forceStartServer ? false : this.checkServerStarted(serverSession.HOST);
            if (serverStarted) {
                if (verboseMode) console.info('Server is running - use started server instance');
            } else {
                if (verboseMode) console.info('Server not started - start local server instance');
                ok(startServer(), 'Local server started');
                serverSession.__serverStartedByMe = true;
            }
        //}
        conn = serverSession.connection = new UBConnection({ URL: serverSession.HOST });
        conn.onRequestAuthParams = function () {
            return {login: serverSession.USER, password: serverSession.PWD};
        };
        ok(serverSession.appInfo = conn.getAppInfo(), 'Run server method getAppInfo fail');
        if (verboseMode) console.info('Connected to ', serverSession.HOST);
    }
    return serverSession;
};

exports.establishConnectionFromCmdLineAttributes._cmdLineParams = [
    {short: 'host',     long: 'host', param: 'fullServerURL', defaultValue: 'http://localhost:888',    searchInEnv: true, help: 'Server URL to connect, including protocol'},
    {short: 'u',     long: 'user', param: 'userName', searchInEnv: true, help: 'User name'},
    {short: 'p',     long: 'pwd', param: 'password', searchInEnv: true, help: 'User password'},
    {short: 'cfg',     long: 'cfg', param: 'localServerConfig', defaultValue: 'ubConfig.json', searchInEnv: false, help: 'Path to server config'}
];


/**
 * Perform check somebody listen on URL
 * @param {String} URL
 * @return {boolean}
 */
exports.checkServerStarted = function checkServerStarted(URL){
    if (verboseMode) console.info('Check server is running...');
    try {
        var resp = http.get({URL: URL, connectTimeout: 1000, receiveTimeout: 1000, sendTimeout: 1000}); //dummy
        if (verboseMode) console.debug('STATUS', resp.statusCode);
        return true;
    } catch(e) {}
    return false;
};

/**
 * Will replace placeholders %VAR_NAME% to environment variable value
 * @private
 * @param {String} content
 * @return {String}
 */
function replaceEnvironmentVariables(content){
    return content.replace(/%(.*?)%/gm, function replacer(match, p1){
        return  process.env[p1] ? process.env[p1].replace(/\\/g, '\\\\') : 'NOT_FOUND_ENV_VAR(' + match + ')';
    });
}

/**
 * Will replace placeholders "#include(pathToFile) to file content
 * @private
 * @param {String} content
 * @return {String}
 */
function replaceIncludeVariables(content){

    return content.replace(/"#include\((.*)\)"/gm, function replacer(match, p1){
        var filePath, content, res;
        try {
            filePath = JSON.parse('{"f": "' + p1 + '"}');
        }catch(e){
            return 'INVALID INCLUDE ' + p1;
        }
        filePath = relToAbs(process.configPath, filePath.f);
        if (!fs.statSync(filePath)){
            return 'INVALID INCLUDE ' + filePath;
        }
        content = removeCommentsFromJSON(fs.readFileSync(filePath));
        if (!content){
            return 'EMPTY INCLUDE ' + filePath;
        }
        return replaceEnvironmentVariables(content);
    });
}

/**
 * Read server configuration using file, resolved by argv.getConfigFileName
 * parse it in safe mode, replace environment variables by it values and return parsed config
 *
 * @return {Object}
 */
exports.getServerConfiguration = function getServerConfiguration(){
    var cfgFileName = this.getConfigFileName();
    if (verboseMode) console.debug('Used config:', cfgFileName);

    var content = removeCommentsFromJSON(fs.readFileSync(cfgFileName));
    content = replaceIncludeVariables(replaceEnvironmentVariables(content));

    var result = safeParseJSON(content, cfgFileName);
    // add name attribute for applications
    if (!result.application){
        result.application = {};
    }
    result.application.name = result.httpServer.path ? result.httpServer.path : '/';
    return result;
};

/**
 * Return a URL server actually listen on
 * @param {Object} config Server configuration
 */
exports.serverURLFromConfig = function serverURlFromConfig(config){
    var httpCfg = config.httpServer || {};
    var rUrl = (httpCfg.protocol && httpCfg.protocol === 'https') ? 'https://' : 'http://';
    // in case of serverDomainNames in [+, *] replace it to localhost
    rUrl += httpCfg.host ? (httpCfg.host.length === 1 ? 'localhost' : httpCfg.host) : 'localhost';
    rUrl += httpCfg.port ? ':' + httpCfg.port : ':80';
    if (httpCfg.path) rUrl += '/' + httpCfg.path;
    return rUrl;
};

/**
 * Identify application, for which command is executed.
 * First check for command line parameter -app, if not defined - environment variable UB_APP, if not defined - first application in config
 *
 * @throws Error if application not found
 *
 * @deprecated  Starting from 1.11 config contain only one application, so use `config.application` directly
 * @param {Object} serverConfig
 * @return {Object} Application config
 */
exports.identifyApp = function identifyApp(serverConfig){
    var appName = options.switchValue('app') || process.env['UB_APP'];
    if (appName) console.warn('`-app` command line switch and UB_APP environment variable is deprecated. Starting from 1.11 config contain only one application');
    var result = serverConfig.application;
    if (!result) throw new Error('"application" section not found in server configuration');
    if (!result.domain || !result.domain.models){
        throw new Error('domain models not configured for application');
    }
    return result;
};

function safeParseJSON(content, fileName){
    var
        jsonlint = require('jsonlint');

    try{
        return JSON.parse(content);
    } catch(e) {
    }
    // we are here in case of wrong JSON - let's parse it slowly and retrieve error line using jsonlint
    try {
        jsonlint.parse(content); // MPV -  buggy implementation of jsonlint replace "\\f" -> "\f" in the string
    } catch(e) {
        throw new Error('In file: ' + fileName + '\n' + e.message);
    }
}
/**
 * JSON file parsing, allow to parse semi-JSON files with comments. In case of errors inside JSON show detailed error description
 * @todo - rewrite when switch to SpiderMonkey 33 in in which native JSON.parse show error line number
 * @param {String} fileName
 * @param {Boolean} [allowMultiLineString=false] Replace `\n` before parse (not compatible with JSON format, but multiline string is useful)
 * @return {Object}
 */
exports.safeParseJSONfile = function safeParseJSONfile(fileName, allowMultiLineString){
  var 
	content;
  content = removeCommentsFromJSON(fs.readFileSync(fileName));
  if (allowMultiLineString){
      content = content.replace('\n', ' ', 'gm').replace('\r', ' ', 'gm').replace('\t', ' ', 'gm')
  }
  return safeParseJSON(content, fileName);
};