"use strict";
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());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Shell = void 0;
const os_1 = require("os");
const util_1 = require("util");
const child_process_1 = require("child_process");
const events_1 = require("events");
const debug_1 = __importDefault(require("debug"));
const p_timeout_1 = __importDefault(require("p-timeout"));
const p_queue_1 = __importDefault(require("p-queue"));
const kind_of_1 = __importDefault(require("kind-of"));
const nanoid_1 = require("nanoid");
const accumulate_stream_1 = require("accumulate-stream");
const trim_buffer_1 = require("trim-buffer");
const errors_1 = require("./errors");
const converters_1 = require("./converters");
const DELIMITER_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
const DELIMITER_LENGTH = 16;
class Shell {
    constructor(options = {}) {
        var _a, _b, _c;
        this.isExited = false;
        this.history = [];
        this.executable = this.setExecutable(options);
        this.executableOptions = this.setExecutableOptions(options);
        this.spawnOptions = Object.assign(Object.assign({}, options.spawnOptions), { stdio: 'pipe', detached: false, shell: false });
        this.invocationQueue = this.setInvocationQueue();
        this.process = (0, child_process_1.spawn)(this.executable, this.executableOptions, this.spawnOptions);
        this.streams = {
            stdin: this.process.stdin,
            stdout: this.process.stdout,
            stderr: this.process.stderr,
        };
        this.debuggers = this.setDebuggers(options);
        this.debuggers.comment(`shell process started: "${this.executable} ${this.executableOptions.join(' ')}"`);
        this.exitPromise = this.setProcessExitListeners();
        this.resultsEncoding = (_a = options.outputEncoding) !== null && _a !== void 0 ? _a : 'utf-8';
        this.setProcessStdioEncoding(options);
        this.setProcessKillOptions(options);
        this.invocationTimeout = (_b = options.invocationTimeout) !== null && _b !== void 0 ? _b : 0;
        this.throwOnInvocationError = (_c = options.throwOnInvocationError) !== null && _c !== void 0 ? _c : true;
    }
    detachStreamFromOutput(stream) {
        this.streams.stdout.unpipe(stream);
        this.streams.stderr.unpipe(stream);
        stream.end();
    }
    readBulkFromOutput(stream, delimiter) {
        return __awaiter(this, void 0, void 0, function* () {
            let firstDelimiterOccurrence = -1;
            let secondDelimiterOccurrence = -1;
            const accumulator = new accumulate_stream_1.AccumulateStream({
                custom: {
                    event: delimiter,
                    isDone(chunk) {
                        const buffer = this.getBuffer();
                        if (firstDelimiterOccurrence === -1) {
                            firstDelimiterOccurrence = buffer.indexOf(delimiter);
                        }
                        const secondDelimiterByteOffset = chunk.length + delimiter.length < buffer.length ? chunk.length + delimiter.length : delimiter.length + 1;
                        secondDelimiterOccurrence = buffer.indexOf(delimiter, -secondDelimiterByteOffset);
                        return firstDelimiterOccurrence !== secondDelimiterOccurrence && secondDelimiterOccurrence !== -1;
                    },
                },
            });
            accumulator.on('chunk', ({ chunk }) => this.debuggers.output((0, trim_buffer_1.trimBuffer)(chunk).toString(this.resultsEncoding)));
            stream.pipe(accumulator);
            const [accumulatedOutput] = yield (0, events_1.once)(accumulator, 'data');
            this.detachStreamFromOutput(accumulator);
            return (0, trim_buffer_1.trimBuffer)(accumulatedOutput.slice(firstDelimiterOccurrence + delimiter.length, secondDelimiterOccurrence));
        });
    }
    setExecutable({ executable }) {
        if (!executable) {
            throw new Error(`unable to determine shell executable
${(0, util_1.inspect)(Object.assign({ platform: process.platform, SHELL: process.env.SHELL, PATH: process.env.PATH }, process.versions), { compact: true, depth: 5, breakLength: 80 })}`);
        }
        return executable;
    }
    setExecutableOptions({ executableOptions = {} }) {
        let options = [];
        Object.entries(executableOptions).forEach(([name, value]) => {
            if (!name.startsWith('-') && name.startsWith('--')) {
                throw new Error(`option ${name} must starts with a dash "-" | "--"`);
            }
            switch (typeof value) {
                case 'string':
                    options = [...options, name, value];
                    break;
                case 'boolean':
                    options = !value ? [...options] : [...options, name];
                    break;
                default:
                    throw new Error(`option ${name} value must be string or boolean`);
            }
        });
        return options;
    }
    setInvocationQueue() {
        return new p_queue_1.default({
            concurrency: 1,
            autoStart: true,
        });
    }
    clearInvocationQueue() {
        this.invocationQueue.pause();
        this.invocationQueue.removeAllListeners();
        this.invocationQueue.clear();
    }
    setDebuggers({ debug }) {
        var _a, _b;
        const namespace = `${(_b = (_a = this.executable.split('/')) === null || _a === void 0 ? void 0 : _a.pop()) === null || _b === void 0 ? void 0 : _b.toUpperCase()}:${this.process.pid}`;
        const _debugger = (0, debug_1.default)(namespace);
        _debugger.log = console.log.bind(console);
        if (debug) {
            debug_1.default.enable(`${process.env.DEBUG},${namespace}*`);
        }
        return {
            comment: _debugger.extend(' #'),
            command: _debugger.extend(' $'),
            output: _debugger.extend(' >'),
        };
    }
    setProcessExitListeners() {
        let useStderrOnExit = false;
        const stderr = new accumulate_stream_1.AccumulateStream({ count: 3 });
        this.streams.stderr.pipe(stderr);
        (0, events_1.once)(stderr, 'data').then(() => {
            this.detachStreamFromOutput(stderr);
        });
        const processErrorPromise = (0, events_1.once)(this.process, 'error').then(([error]) => {
            throw new errors_1.ProcessError(this.process, error);
        });
        const processExitPromise = (0, events_1.once)(this.process, 'exit').then(([code, signal]) => {
            const error = !useStderrOnExit ? undefined : new Error(stderr.getBuffer().toString(this.resultsEncoding));
            if (signal && signal !== 'SIGTERM') {
                throw new errors_1.ProcessError(this.process, error);
            }
            if (code && code !== 0 && code !== 143) {
                throw new errors_1.ProcessError(this.process, error);
            }
        });
        (0, events_1.once)(this.streams.stdin, 'error').then(() => {
            useStderrOnExit = true;
        });
        return Promise.race([processErrorPromise, processExitPromise]).finally(() => {
            this.isExited = true;
            this.clearInvocationQueue();
            this.streams.stdin.end();
            this.detachStreamFromOutput(stderr);
            this.debuggers.comment(`shell process exited`);
        });
    }
    setProcessStdioEncoding({ inputEncoding = 'utf-8', outputEncoding = 'utf-8', }) {
        this.streams.stdin.setDefaultEncoding(inputEncoding);
        this.streams.stdout.setEncoding(outputEncoding);
        this.streams.stderr.setEncoding(outputEncoding);
    }
    setProcessKillOptions({ disposeTimeout }) {
        if (disposeTimeout) {
            setTimeout(() => __awaiter(this, void 0, void 0, function* () {
                yield this.dispose();
            }), disposeTimeout);
        }
    }
    invokeCommand(command) {
        return __awaiter(this, void 0, void 0, function* () {
            this.debuggers.comment('starting command invocation');
            this.debuggers.command(command);
            this.history.unshift({
                command,
                raw: '',
                hadErrors: false,
                startTime: Date.now(),
            });
            const currentHistoryRecord = this.history[0];
            const bulkDelimiter = (0, nanoid_1.customAlphabet)(DELIMITER_ALPHABET, DELIMITER_LENGTH)();
            const outputPromise = Promise.all([
                this.readBulkFromOutput(this.streams.stdout, bulkDelimiter),
                this.readBulkFromOutput(this.streams.stderr, bulkDelimiter),
            ]);
            let isCommandFailed = false;
            let isCommandTimedOut = false;
            const isCommandDone = Promise.race([this.exitPromise, outputPromise]);
            this.streams.stdin.write(this.writeToError(bulkDelimiter));
            this.streams.stdin.write(os_1.EOL);
            this.streams.stdin.write(this.writeToOutput(bulkDelimiter));
            this.streams.stdin.write(os_1.EOL);
            this.streams.stdin.write(command);
            this.streams.stdin.write(os_1.EOL);
            this.streams.stdin.write(this.writeToError(bulkDelimiter));
            this.streams.stdin.write(os_1.EOL);
            this.streams.stdin.write(this.writeToOutput(bulkDelimiter));
            this.streams.stdin.write(os_1.EOL);
            try {
                if (!this.invocationTimeout) {
                    yield isCommandDone;
                }
                else {
                    yield (0, p_timeout_1.default)(isCommandDone, this.invocationTimeout, () => {
                        isCommandTimedOut = true;
                    });
                }
            }
            finally {
                currentHistoryRecord.duration = Date.now() - currentHistoryRecord.startTime;
                if (!this.isExited) {
                    if (!isCommandTimedOut) {
                        const [stdout, stderr] = yield outputPromise;
                        currentHistoryRecord.stdout = stdout;
                        currentHistoryRecord.stderr = stderr;
                        isCommandFailed = stderr.length !== 0;
                        if (!isCommandFailed) {
                            currentHistoryRecord.raw = currentHistoryRecord.stdout.toString(this.resultsEncoding);
                            this.debuggers.comment('command invocation succeeded');
                        }
                        else {
                            currentHistoryRecord.hadErrors = true;
                            currentHistoryRecord.raw = currentHistoryRecord.stderr.toString(this.resultsEncoding);
                            this.debuggers.comment('command invocation failed');
                        }
                    }
                    else {
                        currentHistoryRecord.hadErrors = true;
                        this.debuggers.comment('command invocation timed out');
                    }
                }
                else {
                    this.debuggers.comment('command invocation stopped');
                }
            }
            if (this.throwOnInvocationError && currentHistoryRecord.hadErrors) {
                throw new errors_1.InvocationError(currentHistoryRecord.raw);
            }
            return currentHistoryRecord;
        });
    }
    invoke(command) {
        return __awaiter(this, void 0, void 0, function* () {
            if (this.isExited) {
                throw new errors_1.InvocationError('invoke called after process exited');
            }
            return this.invocationQueue.add(() => this.invokeCommand(command));
        });
    }
    dispose(signal = 'SIGTERM') {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.isExited) {
                this.clearInvocationQueue();
                this.process.kill(signal);
            }
            return this.exitPromise;
        });
    }
    static convertToShell(jsObject) {
        const objectType = (0, kind_of_1.default)(jsObject);
        const hasConverter = this.converters.has(objectType);
        if (!hasConverter) {
            throw new Error(`cannot convert ${objectType} object to its shell representation`);
        }
        return this.converters.get(objectType).call(undefined, jsObject, this.convertToShell.bind(this));
    }
    static command(literals, ...expressions) {
        return literals
            .map((literal, index) => {
            return `${literal}${this.convertToShell(expressions === null || expressions === void 0 ? void 0 : expressions[index])}`;
        })
            .join('');
    }
    static invoke(command, options) {
        return __awaiter(this, void 0, void 0, function* () {
            const shell = new this(options);
            try {
                return yield shell.invoke(command);
            }
            finally {
                yield shell.dispose();
            }
        });
    }
    static $(literals, ...expressions) {
        return __awaiter(this, void 0, void 0, function* () {
            return this.invoke(this.command(literals, ...expressions));
        });
    }
    static $$(literals, ...expressions) {
        return (options) => this.invoke(this.command(literals, ...expressions), options);
    }
}
exports.Shell = Shell;
Shell.converters = converters_1.SHELL_CONVERTERS;
//# sourceMappingURL=Shell.js.map