var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    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) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import { exec } from 'child_process';
import crypto from 'crypto';
import axios from 'axios';
import getPort from 'get-port';
import Debug from 'debug';
import util from 'util';
const execAsync = util.promisify(exec);
const debug = Debug('deeplinks:container-controller');
const log = debug.extend('log');
const error = debug.extend('error');
const DOCKER = process.env.DOCKER || '0';
export const runnerControllerOptionsDefault = {
    gql_docker_domain: 'deep-links',
    network: 'network',
    handlersHash: {},
};
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
export class ContainerController {
    constructor(options) {
        this.runContainerHash = {};
        this.handlersHash = {};
        this.network = (options === null || options === void 0 ? void 0 : options.network) || runnerControllerOptionsDefault.network;
        this.gql_docker_domain = (options === null || options === void 0 ? void 0 : options.gql_docker_domain) || runnerControllerOptionsDefault.gql_docker_domain;
        this.handlersHash = (options === null || options === void 0 ? void 0 : options.handlersHash) || runnerControllerOptionsDefault.handlersHash;
    }
    ;
    _runContainer(containerName, dockerPort, options) {
        return __awaiter(this, void 0, void 0, function* () {
            const { handler, forcePort, forceRestart, publish } = options;
            const { network, gql_docker_domain } = this;
            let done = false;
            let count = 30;
            let dockerRunResult;
            while (!done) {
                log('_runContainer count', { count });
                if (count < 0)
                    return { error: 'timeout _runContainer' };
                count--;
                try {
                    const command = `docker pull ${handler}; docker run --add-host host.docker.internal:host-gateway --restart unless-stopped -e PORT=${dockerPort} -e GQL_URN=${gql_docker_domain}:3006/gql -e GQL_SSL=0 --name ${containerName} ${publish ? `-p ${dockerPort}:${dockerPort}` : `--expose ${dockerPort}`} --net deep-${network} -d ${handler}`;
                    log('_runContainer command', { command });
                    const dockerRunResultObject = yield execAsync(command);
                    log('_runContainer dockerRunResultObject', JSON.stringify(dockerRunResultObject, null, 2));
                    dockerRunResult = dockerRunResultObject === null || dockerRunResultObject === void 0 ? void 0 : dockerRunResultObject.stdout;
                    log('_runContainer dockerRunResultString', { dockerRunResult });
                }
                catch (e) {
                    dockerRunResult = e.stderr;
                }
                if (dockerRunResult.indexOf('port is already allocated') !== -1) {
                    error('port is already allocated');
                    if (forcePort)
                        return { error: 'port is already allocated' };
                    continue;
                }
                else if (dockerRunResult.indexOf('is already in use by container') !== -1) {
                    error('is already in use by container');
                    if (!forceRestart)
                        return { error: 'is already in use by container' };
                    yield this._dropContainer(containerName);
                }
                else {
                    done = true;
                    if (!publish && !+DOCKER)
                        throw new Error('Need proxy, dl issue https://github.com/deep-foundation/deeplinks/issues/45');
                    let host = publish ? +DOCKER ? 'host.docker.internal' : 'localhost' : +DOCKER ? containerName : '';
                    const container = { name: containerName, host, port: dockerPort, options };
                    log('_runContainer container', container);
                    try {
                        const waitResult = yield execAsync(`npx wait-on --timeout 5000 http-get://${host}:${dockerPort}/healthz`);
                        log('_runContainer npx done', { waitResult });
                    }
                    catch (e) {
                        error('_runContainer error', e);
                        return { error: 'wait-on healthz timeout _runContainer' };
                    }
                    return container;
                }
            }
        });
    }
    newContainer(options) {
        return __awaiter(this, void 0, void 0, function* () {
            const { handler, forcePort, forceName } = options;
            const { network, handlersHash, runContainerHash } = this;
            const containerName = forceName || `deep-${crypto.createHash('md5').update(handler).digest("hex")}`;
            log('newContainer options, network, handlersHash, containerName, forceName', { options, network, handlersHash, containerName, forceName });
            let container = yield this.findContainer(containerName);
            log('newContainer container', { container });
            if (container)
                return container;
            let dockerPort = forcePort || (yield getPort());
            if (runContainerHash[containerName] === undefined) {
                runContainerHash[containerName] = new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
                    let done = false;
                    let count = 10;
                    while (!done) {
                        log('newContainer while count', { count });
                        count--;
                        container = yield this._runContainer(containerName, dockerPort, options);
                        log('newContainer while container', { container });
                        if (container === null || container === void 0 ? void 0 : container.error) {
                            if (!forcePort)
                                dockerPort = yield getPort();
                            continue;
                        }
                        handlersHash[containerName] = container;
                        log('newContainer done true', { containerName, handlersHash, runContainerHash });
                        done = true;
                    }
                    resolve(undefined);
                }));
            }
            yield runContainerHash[containerName];
            return handlersHash[containerName];
        });
    }
    findContainer(containerName) {
        return __awaiter(this, void 0, void 0, function* () {
            const { handlersHash, runContainerHash } = this;
            yield runContainerHash[containerName];
            return handlersHash[containerName];
        });
    }
    _dropContainer(containerName) {
        var _a;
        return __awaiter(this, void 0, void 0, function* () {
            log('_dropContainer', { containerName });
            return (_a = (yield execAsync(`docker stop ${containerName} && docker rm ${containerName}`))) === null || _a === void 0 ? void 0 : _a.stdout;
        });
    }
    dropContainer(container) {
        return __awaiter(this, void 0, void 0, function* () {
            const { handlersHash, runContainerHash } = this;
            yield runContainerHash[container.name];
            if (!handlersHash[container.name])
                return;
            let dockerStopResult = yield this._dropContainer(container.name);
            handlersHash[container.name] = undefined;
            log('dropContainer dockerStopResult', { dockerStopResult });
        });
    }
    _checkAndRestart(container) {
        return __awaiter(this, void 0, void 0, function* () {
            const { handlersHash, runContainerHash } = this;
            try {
                const healthz = `http://${container.host}:${container.port}/healthz`;
                yield axios.get(healthz);
                return { error: 'healthz ok' };
            }
            catch (e) {
                error('heatlthz error code', e.code);
                runContainerHash[container.name] = new Promise((resolve) => __awaiter(this, void 0, void 0, function* () {
                    handlersHash[container.name] = undefined;
                    container.options.forcePort = container.port;
                    container.options.forceName = container.name;
                    const newContainer = yield this._runContainer(container.name, container.port, container.options);
                    handlersHash[container.name] = newContainer;
                    resolve(undefined);
                }));
                yield runContainerHash[container.name];
                log('_checkAndRestart handlersHash[container.name]', handlersHash[container.name]);
                return handlersHash[container.name];
            }
        });
    }
    initHandler(container) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            const { runContainerHash } = this;
            const { host, port } = container;
            let initResult;
            yield runContainerHash[container.name];
            try {
                const initRunner = `http://${host}:${port}/init`;
                log('initHandler initRunner', { initRunner });
                initResult = yield axios.post(initRunner);
                log('initHandler initResult status', { status: initResult.status });
            }
            catch (e) {
                error('init error code', e.code);
                const checkResult = yield this._checkAndRestart(container);
                if (checkResult === null || checkResult === void 0 ? void 0 : checkResult.error)
                    return Object.assign({}, checkResult);
                yield this.initHandler(container);
                return ({ error: e });
            }
            if ((_a = initResult === null || initResult === void 0 ? void 0 : initResult.data) === null || _a === void 0 ? void 0 : _a.error)
                return { error: (_b = initResult === null || initResult === void 0 ? void 0 : initResult.data) === null || _b === void 0 ? void 0 : _b.error };
            return {};
        });
    }
    callHandler(options) {
        var _a, _b, _c, _d, _e;
        return __awaiter(this, void 0, void 0, function* () {
            const { handlersHash, runContainerHash } = this;
            const { container } = options;
            yield runContainerHash[container.name];
            try {
                const callRunner = `http://${container.host}:${container.port}/call`;
                log('callHandler', { callRunner, params: options });
                const callResult = yield axios.post(callRunner, { params: options });
                log('callHandler callResult status', { status: callResult.status });
                if ((_a = callResult === null || callResult === void 0 ? void 0 : callResult.data) === null || _a === void 0 ? void 0 : _a.error)
                    return { error: (_b = callResult === null || callResult === void 0 ? void 0 : callResult.data) === null || _b === void 0 ? void 0 : _b.error };
                if ((_c = callResult === null || callResult === void 0 ? void 0 : callResult.data) === null || _c === void 0 ? void 0 : _c.rejected)
                    return Promise.reject((_d = callResult === null || callResult === void 0 ? void 0 : callResult.data) === null || _d === void 0 ? void 0 : _d.rejected);
                return (_e = callResult === null || callResult === void 0 ? void 0 : callResult.data) === null || _e === void 0 ? void 0 : _e.resolved;
            }
            catch (e) {
                error('call error', e);
                const checkResult = yield this._checkAndRestart(container);
                if (checkResult === null || checkResult === void 0 ? void 0 : checkResult.error)
                    return Object.assign({}, checkResult);
                yield this.callHandler(options);
                return ({ error: e });
            }
        });
    }
}
//# sourceMappingURL=container-controller.js.map