"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const commander = require("commander");
const fs = require("fs-extra");
const path = require("path");
const tmp = require("tmp");
const resolve_1 = require("resolve");
const resolve_bin_1 = require("resolve-bin");
const child_process_1 = require("child_process");
const services = require("./services");
const Logger_1 = require("./shared/Logger");
const Server_1 = require("./Server");
const project_1 = require("./project");
const checkEngine_1 = require("./shared/checkEngine");
class BasicServer extends Server_1.Server {
    constructor() {
        super();
        this.commander = new commander.Command();
        this.commander.version(require('../package.json').version);
        this.commander.on('--help', () => {
            console.log('');
            console.log('  Help for individual commands:');
            console.log('');
            console.log('    ros init --help');
            console.log('    ros start --help');
            console.log('    ros backup --help');
            console.log('');
        });
        this.commander.command('start')
            .description('Start Realm Object Server')
            .option("-d, --data <path>", "Specify the data path to use (default ./data)", "./data")
            .option("-a, --address <address>", "Specify the address to listen on (default 0.0.0.0)", "0.0.0.0")
            .option("-p, --port <port>", "Specify the port to listen on (default 9080)", "9080")
            .option("-l, --loglevel <level>", "The log level (all | trace | debug | detail | info | warn | error | fatal | off) (default info)", "info")
            .option("-f, --logfile <filename>", "The file to log to. If the option is absent, logging will be directed to stdout")
            .option("--no-auto-keygen", "Do not autogenerate keys on startup")
            .option("--private-key <privateKeyPath>", "Alternative path to the private key (default <data>/keys/auth.key)")
            .option("--public-key <publicKeyPath>", "Alternative path to the public key (default <data>/keys/auth.pub)")
            .option("--refresh-token-ttl <refreshTokenTtl>", "TTL of issued refresh tokens in seconds", String(10 * 60 * 60 * 24 * 365 * 10))
            .option("--access-token-ttl <accessTokenTtl>", "TTL of issued access tokens in seconds", String(10 * 60))
            .option('--auth <provider1[,provider2 ...]>', "A list of auth providers to load", "password,anonymous,nickname")
            .option('--https', "Enable HTTPS", false)
            .option('--https-key <keyPath>', "Path to the HTTPS private key")
            .option('--https-cert <certPath>', "Path to the HTTPS certificate chain")
            .option('--https-address <port>', "HTTPS listen address", "0.0.0.0")
            .option('--https-port <port>', "HTTPS listen port", 9443)
            .action((command) => {
            command.parent.handler = this.runStart(command);
        });
        this.commander.command('backup')
            .description('Create a backup of the Realm Object Server.')
            .option("-f, --from <path>", "The source data path")
            .option("-t, --to <path>", "The destination data path")
            .option("--overwrite", "Overwrite the destination", false)
            .action((command) => {
            command.parent.handler = this.runBackup(command);
            command.parent.terminate = true;
        });
        this.commander.command('init <projectName>')
            .description('Create a new ROS project')
            .option('-t --template <template>', 'template type either js or ts. the default is ts', /^(ts|js)$/i, 'ts')
            .action((projectName, command) => {
            command.parent.handler = this.runInit(projectName, command);
            command.parent.terminate = true;
        });
    }
    runInit(projectName, options) {
        return __awaiter(this, void 0, void 0, function* () {
            yield project_1.initProject(projectName, options.template);
        });
    }
    run(argv) {
        return __awaiter(this, void 0, void 0, function* () {
            checkEngine_1.checkEngine();
            const command = yield this.commander.parse(argv);
            if (command.handler) {
                try {
                    yield Promise.resolve(command.handler);
                    if (command.terminate) {
                        process.exit(0);
                    }
                }
                catch (err) {
                    console.error(err.message);
                    process.exit(1);
                }
            }
            else {
                this.commander.outputHelp();
                process.exit(1);
            }
        });
    }
    start(params) {
        const _super = name => super[name];
        return __awaiter(this, void 0, void 0, function* () {
            if (!params.services) {
                this.addService(new services.SyncProxyService());
                const syncConfig = {
                    dataPath: path.join(params.dataPath, "sync"),
                    publicKeyPath: params.publicKeyPath || path.join(params.dataPath, "keys", "auth.pub"),
                    logLevel: params.logLevel,
                    enableDownloadLogCompaction: params.enableDownloadLogCompaction,
                    maxDownloadSize: params.maxDownloadSize,
                    featureToken: params.featureToken
                };
                this.addService(new services.SyncService(syncConfig));
                const authService = new services.AuthService({
                    refreshTokenTtl: params.refreshTokenTtl,
                    accessTokenTtl: params.accessTokenTtl
                });
                if (params.authProviders && params.authProviders.length > 0) {
                    for (const provider of params.authProviders) {
                        authService.addProvider(provider);
                    }
                }
                else {
                    const defaultProviders = [
                        { type: 'PasswordAuthProvider', config: { autoCreateAdminUser: true } },
                        { type: 'AnonymousAuthProvider' },
                        { type: 'NicknameAuthProvider' }
                    ];
                    authService.setDefaultProviders(defaultProviders);
                }
                this.addService(authService);
                this.addService(new services.RealmDirectoryService());
                this.addService(new services.LogService());
                this.addService(new services.PermissionsService());
                this.addService(new services.HealthService());
                this.addService(new services.WelcomeService());
                this.addService(new services.PasswordAuthUIService());
            }
            else {
                this.addServices(...params.services);
            }
            this.addService(new services.MixpanelService({
                serverVersion: this.version
            }));
            process.removeAllListeners("SIGINT");
            process.on('SIGINT', () => __awaiter(this, void 0, void 0, function* () {
                this.logger.info("Got signal SIGINT. Shutting down");
                this.shutdown()
                    .then(() => process.exit(0))
                    .catch((err) => {
                    console.error(err);
                    process.exit(1);
                });
            }));
            process.removeAllListeners("SIGTERM");
            process.on('SIGTERM', () => __awaiter(this, void 0, void 0, function* () {
                this.logger.info("Got signal SIGTERM. Shutting down.");
                this.shutdown()
                    .then(() => process.exit(0))
                    .catch((err) => {
                    console.error(err);
                    process.exit(1);
                });
            }));
            this.unhandledRejectionCallback = (reason, promise) => {
                console.error("Detected an unhandled promise rejection at: ", promise, ", reason: ", reason);
            };
            process.on("unhandledRejection", this.unhandledRejectionCallback);
            const startupDeadline = params.startupTimeout || 30000;
            const startupTimeout = setTimeout(() => {
                this.logger.fatal(`BasicServer didn't come up in ${startupDeadline}ms. Bailing.`);
                process.exit(1);
            }, startupDeadline);
            try {
                yield _super("start").call(this, params);
            }
            finally {
                clearTimeout(startupTimeout);
            }
        });
    }
    shutdown() {
        const _super = name => super[name];
        return __awaiter(this, void 0, void 0, function* () {
            yield _super("shutdown").call(this);
            process.removeListener('unhandledRejection', this.unhandledRejectionCallback);
            process.removeAllListeners('SIGTERM');
            process.removeAllListeners('SIGINT');
        });
    }
    runStart(command) {
        return __awaiter(this, void 0, void 0, function* () {
            const authProviders = [];
            const providerNames = command.auth.split(',');
            for (const providerName of providerNames) {
                let providerClass;
                try {
                    providerClass = require(`./auth/${providerName}`).default;
                }
                catch (e) {
                    console.error("The auth provider '%s' cannot be found or does not have a default export", providerName);
                    process.exit(1);
                }
                const options = {};
                if (providerName === "password") {
                    options.autoCreateAdminUser = true;
                }
                authProviders.push(new providerClass(options));
            }
            const logLevel = Logger_1.SyncLogLevel[command.loglevel];
            if (logLevel === undefined) {
                throw new Server_1.ServerValidationError("log-level must be one of: all, trace, debug, detail, info, warn, error, fatal, off");
            }
            if (command.logfile) {
                try {
                    fs.openSync(command.logfile, 'a');
                }
                catch (e) {
                    console.error("The log file '%s' cannot be opened for writing. Aborting", command.logfile);
                    process.exit(1);
                }
            }
            const refreshTokenTtl = parseInt(command.refreshTokenTtl, 10);
            if (String(refreshTokenTtl) !== command.refreshTokenTtl || refreshTokenTtl <= 0) {
                throw new Server_1.ServerValidationError("refresh-token-ttl must be a positive number.");
            }
            const accessTokenTtl = parseInt(command.accessTokenTtl, 10);
            if (String(accessTokenTtl) !== command.accessTokenTtl || accessTokenTtl <= 0) {
                throw new Server_1.ServerValidationError("access-token-ttl must be a positive number.");
            }
            const port = parseInt(command.port, 10);
            if (!(port > 0 && port <= 65535)) {
                throw new Server_1.ServerValidationError(`Invalid port number (1-65535): '${command.port}'`);
            }
            const params = {
                publicKeyPath: command.publicKey,
                privateKeyPath: command.privateKey,
                accessTokenTtl: command.accessTokenTtl,
                refreshTokenTtl: command.refreshTokenTtl,
                dataPath: path.resolve(command.data),
                address: command.address,
                port: command.port,
                logLevel: command.loglevel,
                authProviders,
            };
            if (command.https) {
                params.https = true;
                params.httpsPort = parseInt(command.httpsPort, 10);
                if (!(params.httpsPort > 0 && params.httpsPort <= 65535)) {
                    throw new Server_1.ServerValidationError(`Invalid HTTPS port number (1-65535): '${command.httpsPort}'`);
                }
                params.httpsAddress = command.httpsAddress;
                params.httpsKeyPath = command.httpsKey;
                params.httpsCertChainPath = command.httpsCert;
            }
            if (command.logfile) {
                params.logger = new Logger_1.FileConsoleLogger(command.logfile, logLevel);
            }
            yield this.start(params);
        });
    }
    runBackup(args, logger) {
        return __awaiter(this, void 0, void 0, function* () {
            logger = logger || new Logger_1.FileConsoleLogger();
            if (!args.from || !args.to) {
                throw new Error("Please specify both --from and --to. Aborting.");
            }
            const syncServerPath = resolve_1.sync("realm-sync-server");
            const cmd = resolve_bin_1.sync(path.resolve(syncServerPath, "..", ".."), { executable: "realm-backup" });
            const tmpdir = tmp.dirSync({ unsafeCleanup: true });
            try {
                if (args.overwrite) {
                    fs.emptyDirSync(path.resolve(args.to));
                }
                const child = child_process_1.spawn(cmd, [path.resolve(args.from), path.resolve(args.to)], {
                    cwd: tmpdir.name,
                });
                child.stdout.on('data', (data) => logger.info(data.toString().trim()));
                child.stderr.on('data', (data) => logger.error(data.toString().trim()));
                yield new Promise((resolve, reject) => {
                    child.on('exit', (code) => {
                        if (code) {
                            reject(new Error(`command failed: ${cmd}`));
                        }
                        resolve();
                    });
                });
            }
            finally {
                tmpdir.removeCallback();
            }
        });
    }
}
exports.BasicServer = BasicServer;
//# sourceMappingURL=BasicServer.js.map