/**
 * Copyright Microsoft Corporation. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import child_process from 'child_process';
import { EventEmitter } from 'events';
import debug from 'debug';
import { assert } from '@isomorphic/assert';
import { timeOrigin } from '@isomorphic/time';
export class ProcessHost extends EventEmitter {
    process;
    _didSendStop = false;
    _processDidExit = false;
    _didExitAndRanOnExit = false;
    _entryScript;
    _lastMessageId = 0;
    _callbacks = new Map();
    _processName;
    _producedEnv = {};
    _extraEnv;
    _requestHandlers = new Map();
    constructor(entryScript, processName, env) {
        super();
        this._entryScript = entryScript;
        this._processName = processName;
        this._extraEnv = env;
    }
    async startRunner(runnerParams, options = {}) {
        assert(!this.process, 'Internal error: starting the same process twice');
        this.process = child_process.fork(this._entryScript, {
            // Note: we pass detached:false, so that workers are in the same process group.
            // This way Ctrl+C or a kill command can shutdown all workers in case they misbehave.
            // Otherwise user can end up with a bunch of workers stuck in a busy loop without self-destructing.
            detached: false,
            env: {
                ...process.env,
                ...this._extraEnv,
            },
            stdio: [
                'ignore',
                options.onStdOut ? 'pipe' : 'inherit',
                (options.onStdErr && !process.env.PW_RUNNER_DEBUG) ? 'pipe' : 'inherit',
                'ipc',
            ],
        });
        this.process.on('exit', async (code, signal) => {
            this._processDidExit = true;
            await this.onExit();
            this._didExitAndRanOnExit = true;
            this.emit('exit', { unexpectedly: !this._didSendStop, code, signal });
        });
        this.process.on('error', e => { }); // do not yell at a send to dead process.
        this.process.on('message', (message) => {
            if (debug.enabled('pw:test:protocol'))
                debug('pw:test:protocol')('◀ RECV ' + JSON.stringify(message));
            if (message.method === '__env_produced__') {
                const producedEnv = message.params;
                this._producedEnv = Object.fromEntries(producedEnv.map(e => [e[0], e[1] ?? undefined]));
            }
            else if (message.method === '__dispatch__') {
                const { id, error, method, params, result } = message.params;
                if (id && this._callbacks.has(id)) {
                    const { resolve, reject } = this._callbacks.get(id);
                    this._callbacks.delete(id);
                    if (error) {
                        const errorObject = new Error(error.message);
                        errorObject.stack = error.stack;
                        reject(errorObject);
                    }
                    else {
                        resolve(result);
                    }
                }
                else {
                    this.emit(method, params);
                }
            }
            else if (message.method === '__request__') {
                const { id, method, params } = message.params;
                const handler = this._requestHandlers.get(method);
                if (!handler) {
                    this.send({ method: '__response__', params: { id, error: { message: 'Unknown method' } } });
                }
                else {
                    handler(params).then(result => {
                        this.send({ method: '__response__', params: { id, result } });
                    }).catch(error => {
                        this.send({ method: '__response__', params: { id, error: { message: error.message } } });
                    });
                }
            }
            else {
                this.emit(message.method, message.params);
            }
        });
        if (options.onStdOut)
            this.process.stdout?.on('data', options.onStdOut);
        if (options.onStdErr)
            this.process.stderr?.on('data', options.onStdErr);
        const error = await new Promise(resolve => {
            this.process.once('exit', (code, signal) => resolve({ unexpectedly: true, code, signal }));
            this.once('ready', () => resolve(undefined));
        });
        if (error)
            return error;
        const processParams = {
            processName: this._processName,
            timeOrigin: timeOrigin(),
        };
        this.send({
            method: '__init__', params: {
                processParams,
                runnerParams
            }
        });
    }
    sendMessage(message) {
        const id = ++this._lastMessageId;
        this.send({
            method: '__dispatch__',
            params: { id, ...message }
        });
        return new Promise((resolve, reject) => {
            this._callbacks.set(id, { resolve, reject });
        });
    }
    sendMessageNoReply(message) {
        this.sendMessage(message).catch(() => { });
    }
    async onExit() {
    }
    onRequest(method, handler) {
        this._requestHandlers.set(method, handler);
    }
    async stop() {
        if (!this._processDidExit && !this._didSendStop) {
            this.send({ method: '__stop__' });
            this._didSendStop = true;
        }
        if (!this._didExitAndRanOnExit)
            await new Promise(f => this.once('exit', f));
    }
    didSendStop() {
        return this._didSendStop;
    }
    producedEnv() {
        return this._producedEnv;
    }
    send(message) {
        if (debug.enabled('pw:test:protocol'))
            debug('pw:test:protocol')('SEND ► ' + JSON.stringify(message));
        this.process?.send(message);
    }
}
//# sourceMappingURL=processHost.js.map