"use strict";
var fs = require("fs");
var http = require("http");
var minimist = require("minimist");
var path = require("path");
var q = require("q");
var binaries_1 = require("../binaries");
var gecko_driver_1 = require("../binaries/gecko_driver");
var cli_1 = require("../cli");
var config_1 = require("../config");
var files_1 = require("../files");
var utils_1 = require("../utils");
var Opt = require("./");
var opts_1 = require("./opts");
var commandName = 'start';
var logger = new cli_1.Logger('start');
var prog = new cli_1.Program()
    .command(commandName, 'start up the selenium server')
    .action(start)
    .addOption(opts_1.Opts[Opt.OUT_DIR])
    .addOption(opts_1.Opts[Opt.SELENIUM_PORT])
    .addOption(opts_1.Opts[Opt.APPIUM_PORT])
    .addOption(opts_1.Opts[Opt.AVD_PORT])
    .addOption(opts_1.Opts[Opt.VERSIONS_STANDALONE])
    .addOption(opts_1.Opts[Opt.VERSIONS_CHROME])
    .addOption(opts_1.Opts[Opt.VERSIONS_ANDROID])
    .addOption(opts_1.Opts[Opt.VERSIONS_APPIUM])
    .addOption(opts_1.Opts[Opt.CHROME_LOGS])
    .addOption(opts_1.Opts[Opt.LOGGING])
    .addOption(opts_1.Opts[Opt.ANDROID])
    .addOption(opts_1.Opts[Opt.AVDS])
    .addOption(opts_1.Opts[Opt.AVD_USE_SNAPSHOTS])
    .addOption(opts_1.Opts[Opt.STARTED_SIGNIFIER])
    .addOption(opts_1.Opts[Opt.SIGNAL_VIA_IPC])
    .addOption(opts_1.Opts[Opt.QUIET])
    .addOption(opts_1.Opts[Opt.DETACH]);
if (config_1.Config.osType() === 'Darwin') {
    prog.addOption(opts_1.Opts[Opt.IOS]);
}
if (config_1.Config.osType() === 'Windows_NT') {
    prog.addOption(opts_1.Opts[Opt.VERSIONS_IE])
        .addOption(opts_1.Opts[Opt.IE32])
        .addOption(opts_1.Opts[Opt.IE])
        .addOption(opts_1.Opts[Opt.EDGE]);
}
exports.program = prog;
// stand alone runner
var argv = minimist(process.argv.slice(2), prog.getMinimistOptions());
if (argv._[0] === 'start-run') {
    prog.run(JSON.parse(JSON.stringify(argv)));
}
else if (argv._[0] === 'start-help') {
    prog.printHelp();
}
// Manage processes used in android emulation
var androidProcesses = [];
var androidActiveAVDs = [];
/**
 * Parses the options and starts the selenium standalone server.
 * @param options
 */
function start(options) {
    if (options[Opt.DETACH].getBoolean()) {
        return detachedRun(options);
    }
    var osType = config_1.Config.osType();
    var stdio = options[Opt.QUIET].getBoolean() ? 'pipe' : 'inherit';
    var binaries = files_1.FileManager.setupBinaries();
    var seleniumPort = options[Opt.SELENIUM_PORT].getString();
    var appiumPort = options[Opt.APPIUM_PORT].getString();
    var avdPort = options[Opt.AVD_PORT].getNumber();
    var android = options[Opt.ANDROID].getBoolean();
    var outputDir = config_1.Config.getSeleniumDir();
    if (options[Opt.OUT_DIR].getString()) {
        if (path.isAbsolute(options[Opt.OUT_DIR].getString())) {
            outputDir = options[Opt.OUT_DIR].getString();
        }
        else {
            outputDir = path.resolve(config_1.Config.getBaseDir(), options[Opt.OUT_DIR].getString());
        }
    }
    try {
        // check if folder exists
        fs.statSync(outputDir).isDirectory();
    }
    catch (e) {
        // if the folder does not exist, quit early.
        logger.warn('the out_dir path ' + outputDir + ' does not exist, run webdriver-manager update');
        return;
    }
    var chromeLogs = null;
    var loggingFile = null;
    if (options[Opt.CHROME_LOGS].getString()) {
        if (path.isAbsolute(options[Opt.CHROME_LOGS].getString())) {
            chromeLogs = options[Opt.CHROME_LOGS].getString();
        }
        else {
            chromeLogs = path.resolve(config_1.Config.getBaseDir(), options[Opt.CHROME_LOGS].getString());
        }
    }
    binaries[binaries_1.StandAlone.id].versionCustom = options[Opt.VERSIONS_STANDALONE].getString();
    binaries[binaries_1.ChromeDriver.id].versionCustom = options[Opt.VERSIONS_CHROME].getString();
    if (options[Opt.VERSIONS_IE]) {
        binaries[binaries_1.IEDriver.id].versionCustom = options[Opt.VERSIONS_IE].getString();
    }
    binaries[binaries_1.AndroidSDK.id].versionCustom = options[Opt.VERSIONS_ANDROID].getString();
    binaries[binaries_1.Appium.id].versionCustom = options[Opt.VERSIONS_APPIUM].getString();
    var downloadedBinaries = files_1.FileManager.downloadedBinaries(outputDir);
    if (downloadedBinaries[binaries_1.StandAlone.id] == null) {
        logger.error('Selenium Standalone is not present. Install with ' +
            'webdriver-manager update --standalone');
        process.exit(1);
    }
    var args = [];
    if (osType === 'Linux') {
        // selenium server may take a long time to start because /dev/random is BLOCKING if there is not
        // enough entropy the solution is to use /dev/urandom, which is NON-BLOCKING (use /dev/./urandom
        // because of a java bug)
        // https://github.com/seleniumhq/selenium-google-code-issue-archive/issues/1301
        // https://bugs.openjdk.java.net/browse/JDK-6202721
        args.push('-Djava.security.egd=file:///dev/./urandom');
    }
    if (options[Opt.LOGGING].getString()) {
        if (path.isAbsolute(options[Opt.LOGGING].getString())) {
            loggingFile = options[Opt.LOGGING].getString();
        }
        else {
            loggingFile = path.resolve(config_1.Config.getBaseDir(), options[Opt.LOGGING].getString());
        }
        args.push('-Djava.util.logging.config.file=' + loggingFile);
    }
    if (downloadedBinaries[binaries_1.ChromeDriver.id] != null) {
        args.push('-Dwebdriver.chrome.driver=' +
            path.resolve(outputDir, binaries[binaries_1.ChromeDriver.id].executableFilename(osType)));
        if (chromeLogs != null) {
            args.push('-Dwebdriver.chrome.logfile=' + chromeLogs);
        }
    }
    if (downloadedBinaries[gecko_driver_1.GeckoDriver.id] != null) {
        args.push('-Dwebdriver.gecko.driver=' +
            path.resolve(outputDir, binaries[gecko_driver_1.GeckoDriver.id].executableFilename(osType)));
    }
    if (downloadedBinaries[binaries_1.IEDriver.id] != null) {
        if (options[Opt.IE32].getBoolean()) {
            binaries[binaries_1.IEDriver.id].arch = 'Win32';
        }
        args.push('-Dwebdriver.ie.driver=' +
            path.resolve(outputDir, binaries[binaries_1.IEDriver.id].executableFilename(osType)));
    }
    if (options[Opt.EDGE] && options[Opt.EDGE].getString()) {
        // validate that the file exists prior to adding it to args
        try {
            var edgeFile = options[Opt.EDGE].getString();
            if (fs.statSync(edgeFile).isFile()) {
                args.push('-Dwebdriver.edge.driver=' + options[Opt.EDGE].getString());
            }
        }
        catch (err) {
        }
    }
    if (android) {
        if (downloadedBinaries[binaries_1.AndroidSDK.id] != null) {
            var avds = options[Opt.AVDS].getString();
            startAndroid(outputDir, binaries[binaries_1.AndroidSDK.id], avds.split(','), options[Opt.AVD_USE_SNAPSHOTS].getBoolean(), avdPort, stdio);
        }
        else {
            logger.warn('Not starting android because it is not installed');
        }
    }
    if (downloadedBinaries[binaries_1.Appium.id] != null) {
        startAppium(outputDir, binaries[binaries_1.Appium.id], binaries[binaries_1.AndroidSDK.id], appiumPort, stdio);
    }
    // log the command to launch selenium server
    args.push('-jar');
    args.push(path.resolve(outputDir, binaries[binaries_1.StandAlone.id].filename()));
    // Add the port parameter, has to declared after the jar file
    if (seleniumPort) {
        args.push('-port', seleniumPort);
    }
    var argsToString = '';
    for (var arg in args) {
        argsToString += ' ' + args[arg];
    }
    logger.info('java' + argsToString);
    var seleniumProcess = utils_1.spawn('java', args, stdio);
    if (options[Opt.STARTED_SIGNIFIER].getString()) {
        signalWhenReady(options[Opt.STARTED_SIGNIFIER].getString(), options[Opt.SIGNAL_VIA_IPC].getBoolean(), outputDir, seleniumPort, downloadedBinaries[binaries_1.Appium.id] ? appiumPort : '', binaries[binaries_1.AndroidSDK.id], avdPort, androidActiveAVDs);
    }
    logger.info('seleniumProcess.pid: ' + seleniumProcess.pid);
    seleniumProcess.on('exit', function (code) {
        logger.info('Selenium Standalone has exited with code ' + code);
        shutdownEverything();
        process.exit(process.exitCode || code);
    });
    seleniumProcess.on('error', function (error) {
        logger.warn('Selenium Standalone server encountered an error: ' + error);
    });
    process.stdin.resume();
    process.stdin.on('data', function (chunk) {
        logger.info('Attempting to shut down selenium nicely');
        shutdownEverything(seleniumPort);
    });
    process.on('SIGINT', function () {
        logger.info('Staying alive until the Selenium Standalone process exits');
        shutdownEverything(seleniumPort);
    });
}
function startAndroid(outputDir, sdk, avds, useSnapshots, port, stdio) {
    var sdkPath = path.resolve(outputDir, sdk.executableFilename(config_1.Config.osType()));
    if (avds[0] == 'all') {
        avds = require(path.resolve(sdkPath, 'available_avds.json'));
    }
    else if (avds[0] == 'none') {
        avds.length = 0;
    }
    var minAVDPort = 5554;
    var maxAVDPort = 5586 - 2 * avds.length;
    if (avds.length && ((port < minAVDPort) || (port > maxAVDPort))) {
        throw new RangeError('AVD Port must be between ' + minAVDPort + ' and ' + maxAVDPort + ' to emulate ' +
            avds.length + ' android devices');
    }
    avds.forEach(function (avd, i) {
        // Credit to appium-ci, which this code was adapted from
        var emuBin = 'emulator'; // TODO(sjelin): get the 64bit linux version working
        var emuArgs = [
            '-avd',
            avd + '-v' + sdk.versionCustom + '-wd-manager',
            '-netfast',
        ];
        var portArg = null;
        if (!useSnapshots) {
            emuArgs = emuArgs.concat(['-no-snapshot-load', '-no-snapshot-save']);
        }
        if (port) {
            portArg = port + i * 2;
            emuArgs = emuArgs.concat(['-port', '' + portArg]);
        }
        if (emuBin !== 'emulator') {
            emuArgs = emuArgs.concat(['-qemu', '-enable-kvm']);
        }
        logger.info('Starting ' + avd + ' on ' + (portArg == null ? 'default port' : 'port ' + portArg));
        var child = utils_1.spawn(path.resolve(sdkPath, 'tools', emuBin), emuArgs, stdio);
        child.on('error', function (error) {
            logger.warn(avd + ' encountered an error: ' + error);
        });
        androidProcesses.push(child);
        androidActiveAVDs.push(avd);
    });
}
function killAndroid() {
    for (var i = 0; i < androidProcesses.length; i++) {
        logger.info('Shutting down ' + androidActiveAVDs[i]);
        androidProcesses[i].kill();
    }
    androidProcesses.length = androidActiveAVDs.length = 0;
}
// Manage appium process
var appiumProcess;
function startAppium(outputDir, binary, androidSDK, port, stdio) {
    logger.info('Starting appium server');
    if (androidSDK) {
        process.env.ANDROID_HOME =
            path.resolve(outputDir, androidSDK.executableFilename(config_1.Config.osType()));
    }
    appiumProcess = utils_1.spawn('npm', ['run', 'appium'].concat(port ? ['--', '--port', port] : []), stdio, { cwd: path.resolve(outputDir, binary.filename()) });
}
function killAppium() {
    if (appiumProcess != null) {
        appiumProcess.kill();
        appiumProcess = null;
    }
}
function signalWhenReady(signal, viaIPC, outputDir, seleniumPort, appiumPort, androidSDK, avdPort, avdNames) {
    var maxWait = 60 * 1000;
    function waitFor(getStatus, testStatus, desc) {
        var checkInterval = 100;
        return q.Promise(function (resolve, reject) {
            var waited = 0;
            (function recursiveCheck() {
                setTimeout(function () {
                    getStatus()
                        .then(function (status) {
                        if (!testStatus(status)) {
                            return q.reject('Invalid status' + (desc ? ' for ' + desc : '') + ': ' + status);
                        }
                    })
                        .then(function () {
                        resolve(undefined);
                    }, function (error) {
                        waited += checkInterval;
                        if (waited < maxWait) {
                            recursiveCheck();
                        }
                        else {
                            reject('Timed out' + (desc ? ' wating for' + desc : '') +
                                '.  Final rejection reason: ' + JSON.stringify(error));
                        }
                    });
                }, checkInterval);
            })();
        });
    }
    ;
    function waitForAndroid(avdPort, avdName, appiumPort) {
        var sdkPath = path.resolve(outputDir, androidSDK.executableFilename(config_1.Config.osType()));
        logger.info('Waiting for ' + avdName + '\'s emulator to start');
        return utils_1.adb(sdkPath, avdPort, 'wait-for-device', maxWait)
            .then(function () {
            logger.info('Waiting for ' + avdName + '\'s OS to boot up');
            return waitFor(function () {
                return utils_1.adb(sdkPath, avdPort, 'shell', maxWait, ['getprop', 'sys.boot_completed']);
            }, function (status) {
                return status.trim() == '1';
            }, avdName + '\'s OS');
        }, function (error) {
            return q.reject('Failed to wait for ' + avdName + '\'s emulator to start (' + error.code + ': ' +
                error.message + ')');
        })
            .then(function () {
            logger.info('Waiting for ' + avdName + ' to be ready to launch chrome');
            var version = binaries_1.AndroidSDK.VERSIONS[parseInt(avdName.slice('android-'.length))];
            return utils_1.request('POST', appiumPort, '/wd/hub/session', maxWait, {
                desiredCapabilities: {
                    browserName: 'chrome',
                    platformName: 'Android',
                    platformVersion: version,
                    deviceName: 'Android Emulator'
                }
            })
                .then(function (data) {
                return JSON.parse(data)['sessionId'];
            }, function (error) {
                return q.reject('Could not start chrome on ' + avdName + ' (' + error.code + ': ' +
                    error.message + ')');
            });
        })
            .then(function (sessionId) {
            logger.info('Shutting down dummy chrome instance for ' + avdName);
            return utils_1.request('DELETE', appiumPort, '/wd/hub/session/' + sessionId)
                .then(function () { }, function (error) {
                return q.reject('Could not close chrome on ' + avdName + ' (' + error.code + ': ' +
                    error.message + ')');
            });
        });
    }
    var pending = [waitFor(function () {
            return utils_1.request('GET', seleniumPort, '/selenium-server/driver/?cmd=getLogMessages', maxWait);
        }, function (logs) {
            return logs.toUpperCase().indexOf('OK') != -1;
        }, 'selenium server')];
    if (appiumPort) {
        pending.push(waitFor(function () {
            return utils_1.request('GET', appiumPort, '/wd/hub/status', maxWait);
        }, function (status) {
            return JSON.parse(status).status == 0;
        }, 'appium server'));
    }
    if (androidSDK && avdPort) {
        for (var i = 0; i < avdNames.length; i++) {
            pending.push(waitForAndroid(avdPort + 2 * i, avdNames[i], appiumPort));
        }
    }
    q.all(pending).then(function () {
        logger.info('Everything started');
        sendStartedSignal(signal, viaIPC);
    }, function (error) {
        logger.error(error);
        shutdownEverything(seleniumPort);
        process.exitCode = 1;
    });
}
function sendStartedSignal(signal, viaIPC) {
    if (viaIPC) {
        if (process.send) {
            return process.send(signal);
        }
        else {
            logger.warn('No IPC channel, sending signal via stdout');
        }
    }
    console.log(signal);
}
function shutdownEverything(seleniumPort) {
    if (seleniumPort) {
        http.get('http://localhost:' + seleniumPort + '/selenium-server/driver/?cmd=shutDownSeleniumServer');
    }
    killAndroid();
    killAppium();
}
function detachedRun(options) {
    var file = path.resolve(__dirname, '..', 'webdriver.js');
    var oldSignal = options[Opt.STARTED_SIGNIFIER].getString();
    var oldViaIPC = options[Opt.SIGNAL_VIA_IPC].getBoolean();
    options[Opt.DETACH].value = false;
    options[Opt.STARTED_SIGNIFIER].value = 'server started';
    options[Opt.SIGNAL_VIA_IPC].value = true;
    var args = [file, commandName].concat(cli_1.unparseOptions(options));
    var unreffed = false;
    var child = utils_1.spawn(process.execPath, args, ['ignore', 1, 2, 'ipc']);
    child.on('message', function (message) {
        if (message == options[Opt.STARTED_SIGNIFIER].getString()) {
            if (oldSignal) {
                sendStartedSignal(oldSignal, oldViaIPC);
            }
            logger.info('Detached pid: ' + child.pid);
            child.disconnect();
            child.unref();
            unreffed = true;
        }
    });
    child.on('exit', function (code) {
        if (!unreffed) {
            if (code == 0) {
                logger.warn('Server never seemed to start, and has now exited');
            }
            else {
                logger.error('Server never seemed to start, and has probably crashed');
            }
            process.exit(code);
        }
    });
}
//# sourceMappingURL=start.js.map