"use strict";
/*
 * Copyright 2017 balena.io
 *
 * 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.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.emitSourceMetadata = exports.emitDrives = exports.emitFail = exports.emitState = exports.emitLog = void 0;
const ws_1 = require("ws");
const lodash_1 = require("lodash");
const errors_1 = require("../shared/errors");
const exit_codes_1 = require("../shared/exit-codes");
const child_writer_1 = require("./child-writer");
const scanner_1 = require("./scanner");
const source_metadata_1 = require("./source-metadata");
// Utility to parse --key=value arguments into process.env if not already set
function injectEnvFromArgs() {
    for (const arg of process.argv.slice(2)) {
        const match = arg.match(/^--([^=]+)=(.*)$/);
        if (match) {
            const key = match[1];
            const value = match[2];
            if (process.env[key] === undefined) {
                process.env[key] = value;
            }
        }
    }
}
// Inject env vars from arguments if not already present
injectEnvFromArgs();
console.log('Etcher child process started with the following environment variables:');
console.log(JSON.stringify(process.env, null, 2));
const ETCHER_SERVER_ADDRESS = process.env.ETCHER_SERVER_ADDRESS;
const ETCHER_SERVER_PORT = process.env.ETCHER_SERVER_PORT;
// const ETCHER_SERVER_ID = process.env.ETCHER_SERVER_ID as string;
const ETCHER_TERMINATE_TIMEOUT = parseInt(process.env.ETCHER_TERMINATE_TIMEOUT ?? '10000', 10);
const host = ETCHER_SERVER_ADDRESS ?? '127.0.0.1';
const port = parseInt(ETCHER_SERVER_PORT || '3434', 10);
// const path = ETCHER_SERVER_ID || "etcher";
// TODO: use the path as cheap authentication
const wss = new ws_1.WebSocketServer({ host, port });
// hold emit functions
let emitLog;
let emitState;
let emitFail;
let emitDrives;
let emitSourceMetadata; // Record<string, never> means an empty object
// Terminate the child process
async function terminate(exitCode) {
    await (0, child_writer_1.cleanup)(Date.now());
    process.nextTick(() => {
        process.exit(exitCode || exit_codes_1.SUCCESS);
    });
}
// kill the process if no initila connections or heartbeat for X sec (default 10)
function setTerminateTimeout() {
    if (ETCHER_TERMINATE_TIMEOUT > 0) {
        return setTimeout(() => {
            console.log(`no connections or heartbeat for ${ETCHER_TERMINATE_TIMEOUT} ms, terminating`);
            terminate();
        }, ETCHER_TERMINATE_TIMEOUT);
    }
    else {
        return null;
    }
}
// terminate the process cleanly on SIGINT
process.once('SIGINT', async () => {
    await terminate(exit_codes_1.SUCCESS);
});
// terminate the process cleanly on SIGTERM
process.once('SIGTERM', async () => {
    await terminate(exit_codes_1.SUCCESS);
});
let terminateInterval = setTerminateTimeout();
function setup() {
    return new Promise((resolve, reject) => {
        wss.on('connection', (ws) => {
            console.log('connection established... setting up');
            /**
             * @summary Send a message to the IPC server
             */
            function emit(type, payload) {
                ws.send(JSON.stringify({ type, payload }));
                // ipc.of[IPC_SERVER_ID].emit("message", { type, payload });
            }
            /**
             * @summary Print logs and send them back to client
             */
            function log(message) {
                console.log(message);
                emit('log', message);
            }
            /**
             * @summary Handle `errors`
             */
            async function handleError(error) {
                emit('error', (0, errors_1.toJSON)(error));
                await terminate(exit_codes_1.GENERAL_ERROR);
            }
            /**
             * @summary Handle `abort` from client
             */
            const onAbort = async (exitCode) => {
                log('Abort');
                emit('abort');
                await terminate(exitCode);
            };
            /**
             * @summary Handle `skip` from client; skip validation
             */
            const onSkip = async (exitCode) => {
                log('Skip validation');
                emit('skip');
                await terminate(exitCode);
            };
            /**
             * @summary Handle `write` from client; start writing to the drives
             */
            const onWrite = async (options) => {
                log('write requested');
                // Remove leftover tmp files older than 1 hour
                (0, child_writer_1.cleanup)(Date.now() - 60 * 60 * 1000);
                let exitCode = exit_codes_1.SUCCESS;
                // Write to the drives
                const results = await (0, child_writer_1.write)(options);
                // handle potential errors from the write process
                if (results.errors.length > 0) {
                    results.errors = results.errors.map(errors_1.toJSON);
                    exitCode = exit_codes_1.GENERAL_ERROR;
                }
                // send the results back to the client
                emit('done', { results });
                // terminate this process
                await terminate(exitCode);
            };
            /**
             * @summary Handle `sourceMetadata` from client; get source metadata
             */
            const onSourceMetadata = async (params) => {
                log('sourceMetadata requested');
                const { selected, SourceType, auth } = JSON.parse(params);
                try {
                    const sourceMatadata = await (0, source_metadata_1.getSourceMetadata)(selected, SourceType, auth);
                    emitSourceMetadata(sourceMatadata);
                }
                catch (error) {
                    emitFail(error);
                }
            };
            // handle uncaught exceptions
            process.once('uncaughtException', handleError);
            // terminate the process if the connection is closed
            ws.on('error', async () => {
                await terminate(exit_codes_1.SUCCESS);
            });
            // route messages from the client by `type`
            const messagesHandler = {
                // terminate the process
                terminate: () => terminate(exit_codes_1.SUCCESS),
                /*
                 receive a `heartbeat`, reset the terminate timeout
                 this mechanism ensure the process will be terminated if the client is disconnected
                */
                heartbeat: () => {
                    if (terminateInterval) {
                        clearTimeout(terminateInterval);
                    }
                    terminateInterval = setTerminateTimeout();
                },
                // resolve the setup promise when the client is ready
                ready: () => {
                    log('Ready ...');
                    resolve({ emit, log });
                },
                // start scanning for drives
                scan: () => {
                    log('Scan requested');
                    (0, scanner_1.startScanning)();
                },
                // route `cancel` from client
                cancel: () => onAbort(exit_codes_1.GENERAL_ERROR),
                // route `skip` from client
                skip: () => onSkip(exit_codes_1.GENERAL_ERROR),
                // route `write` from client
                write: async (options) => onWrite(options),
                // route `sourceMetadata` from client
                sourceMetadata: async (params) => onSourceMetadata(params),
            };
            // message handler, parse and route messages coming on WS
            ws.on('message', async (jsonData) => {
                const data = JSON.parse(jsonData);
                const message = messagesHandler[data.type];
                if (message) {
                    await message(data.payload);
                }
                else {
                    throw new Error(`Unknown message type: ${data.type}`);
                }
            });
            // inform the client that the server is ready to receive messages
            emit('ready', {});
            ws.on('error', (error) => {
                reject(error);
            });
        });
    });
}
// setTimeout(() => console.log('wss', wss.address()), 1000);
console.log('waiting for connection...');
setup().then(({ emit, log }) => {
    // connection is established, clear initial terminate timeout
    if (terminateInterval) {
        clearInterval(terminateInterval);
    }
    console.log('waiting for instruction...');
    // set the exportable emit functions
    exports.emitLog = emitLog = (message) => {
        log(message);
    };
    exports.emitState = emitState = (state) => {
        emit('state', state);
    };
    exports.emitFail = emitFail = (data) => {
        emit('fail', data);
    };
    exports.emitDrives = emitDrives = (drives) => {
        emit('drives', JSON.stringify((0, lodash_1.values)(drives)));
    };
    exports.emitSourceMetadata = emitSourceMetadata = (sourceMetadata) => {
        emit('sourceMetadata', JSON.stringify(sourceMetadata));
    };
});
