"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());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppDebugSocketProxyFactory = void 0;
const events_1 = require("events");
const constants_1 = require("../../constants");
const net = require("net");
const ws = require("ws");
const ios_device_lib_1 = require("ios-device-lib");
const yok_1 = require("../../common/yok");
class AppDebugSocketProxyFactory extends events_1.EventEmitter {
    constructor($logger, $errors, $lockService, $options, $tempService, $net) {
        super();
        this.$logger = $logger;
        this.$errors = $errors;
        this.$lockService = $lockService;
        this.$options = $options;
        this.$tempService = $tempService;
        this.$net = $net;
        this.deviceWebServers = {};
        this.deviceTcpServers = {};
    }
    getTCPSocketProxy(deviceIdentifier, appId) {
        return this.deviceTcpServers[`${deviceIdentifier}-${appId}`];
    }
    addTCPSocketProxy(device, appId, projectName, projectDir) {
        return __awaiter(this, void 0, void 0, function* () {
            const cacheKey = `${device.deviceInfo.identifier}-${appId}`;
            const existingServer = this.deviceTcpServers[cacheKey];
            if (existingServer) {
                this.$errors.fail(`TCP socket proxy is already running for device '${device.deviceInfo.identifier}' and app '${appId}'`);
            }
            this.$logger.info("\nSetting up proxy...\nPress Ctrl + C to terminate, or disconnect.\n");
            const server = net.createServer({
                allowHalfOpen: true,
            });
            this.deviceTcpServers[cacheKey] = server;
            server.on("connection", (frontendSocket) => __awaiter(this, void 0, void 0, function* () {
                this.$logger.info("Frontend client connected.");
                frontendSocket.on("end", () => {
                    this.$logger.info("Frontend socket closed!");
                    if (!this.$options.watch) {
                        process.exit(0);
                    }
                });
                const appDebugSocket = yield device.getDebugSocket(appId, projectName, projectDir);
                this.$logger.info("Backend socket created.");
                appDebugSocket.on("end", () => {
                    this.$logger.info("Backend socket closed!");
                    if (!this.$options.watch) {
                        process.exit(0);
                    }
                });
                frontendSocket.on("close", () => __awaiter(this, void 0, void 0, function* () {
                    this.$logger.info("Frontend socket closed");
                    yield device.destroyDebugSocket(appId);
                }));
                appDebugSocket.on("close", () => {
                    this.$logger.info("Backend socket closed");
                    frontendSocket.destroy();
                    server.close();
                    delete this.deviceTcpServers[cacheKey];
                });
                appDebugSocket.pipe(frontendSocket);
                frontendSocket.pipe(appDebugSocket);
                frontendSocket.resume();
            }));
            const socketFileLocation = yield this.$tempService.path({
                suffix: ".sock",
            });
            server.listen(socketFileLocation);
            if (!this.$options.client) {
                this.$logger.info("socket-file-location: " + socketFileLocation);
            }
            return server;
        });
    }
    ensureWebSocketProxy(device, appId, projectName, projectDir) {
        return __awaiter(this, void 0, void 0, function* () {
            const existingWebProxy = this.deviceWebServers[`${device.deviceInfo.identifier}-${appId}`];
            const result = existingWebProxy ||
                (yield this.addWebSocketProxy(device, appId, projectName, projectDir));
            // TODO: do not remove till VSCode waits for this message in order to reattach
            this.$logger.info("Opened localhost " + result.options.port);
            return result;
        });
    }
    addWebSocketProxy(device, appId, projectName, projectDir) {
        return __awaiter(this, void 0, void 0, function* () {
            let clientConnectionLockRelease;
            const cacheKey = `${device.deviceInfo.identifier}-${appId}`;
            const existingServer = this.deviceWebServers[cacheKey];
            if (existingServer) {
                this.$errors.fail(`Web socket proxy is already running for device '${device.deviceInfo.identifier}' and app '${appId}'`);
            }
            // NOTE: We will try to provide command line options to select ports, at least on the localhost.
            const localPort = yield this.$net.getAvailablePortInRange(41000);
            this.$logger.info("\nSetting up debugger proxy...\nPress Ctrl + C to terminate, or disconnect.\n");
            // NB: When the inspector frontend connects we might not have connected to the inspector backend yet.
            // That's why we use the verifyClient callback of the websocket server to stall the upgrade request until we connect.
            // We store the socket that connects us to the device in the upgrade request object itself and later on retrieve it
            // in the connection callback.
            let currentAppSocket = null;
            let currentWebSocket = null;
            const server = new ws.Server({
                port: localPort,
                verifyClient: (info, callback) => __awaiter(this, void 0, void 0, function* () {
                    let acceptHandshake = true;
                    clientConnectionLockRelease = null;
                    try {
                        clientConnectionLockRelease = yield this.$lockService.lock(`debug-connection-${device.deviceInfo.identifier}-${appId}.lock`);
                        this.$logger.info("Frontend client connected.");
                        let appDebugSocket;
                        if (currentAppSocket) {
                            currentAppSocket.removeAllListeners();
                            currentAppSocket = null;
                            if (currentWebSocket) {
                                currentWebSocket.removeAllListeners();
                                currentWebSocket.close();
                                currentWebSocket = null;
                            }
                            yield device.destroyDebugSocket(appId);
                        }
                        appDebugSocket = yield device.getDebugSocket(appId, projectName, projectDir);
                        currentAppSocket = appDebugSocket;
                        this.$logger.info("Backend socket created.");
                        info.req["__deviceSocket"] = appDebugSocket;
                    }
                    catch (err) {
                        if (clientConnectionLockRelease) {
                            clientConnectionLockRelease();
                        }
                        err.deviceIdentifier = device.deviceInfo.identifier;
                        this.$logger.trace(err);
                        this.emit(constants_1.CONNECTION_ERROR_EVENT_NAME, err);
                        acceptHandshake = false;
                        this.$logger.warn(`Cannot connect to device socket. The error message is '${err.message}'.`);
                    }
                    callback(acceptHandshake);
                }),
            });
            this.deviceWebServers[cacheKey] = server;
            server.on("connection", (webSocket, req) => {
                currentWebSocket = webSocket;
                const encoding = "utf16le";
                const appDebugSocket = req["__deviceSocket"];
                const packets = new ios_device_lib_1.MessageUnpackStream();
                appDebugSocket.pipe(packets);
                packets.on("data", (buffer) => {
                    const message = buffer.toString(encoding);
                    if (webSocket.readyState === webSocket.OPEN) {
                        if (process.env.DEBUG_DEVTOOLS_SOCKETS) {
                            console.log({
                                msgFromRuntime: JSON.parse(message),
                            });
                        }
                        webSocket.send(message);
                    }
                    else {
                        this.$logger.trace(`Received message ${message}, but unable to send it to webSocket as its state is: ${webSocket.readyState}`);
                    }
                });
                webSocket.on("error", (err) => {
                    this.$logger.trace("Error on debugger websocket", err);
                });
                appDebugSocket.on("error", (err) => {
                    this.$logger.trace("Error on debugger deviceSocket", err);
                });
                webSocket.on("message", (message) => {
                    const msg = message.toString();
                    if (process.env.DEBUG_DEVTOOLS_SOCKETS) {
                        console.log({
                            msgFromDevtools: JSON.parse(msg),
                        });
                    }
                    const length = Buffer.byteLength(msg, encoding);
                    const payload = Buffer.allocUnsafe(length + 4);
                    payload.writeInt32BE(length, 0);
                    payload.write(msg, 4, length, encoding);
                    appDebugSocket.write(payload);
                });
                appDebugSocket.on("close", () => {
                    currentAppSocket = null;
                    this.$logger.trace("Backend socket closed!");
                    webSocket.close();
                });
                webSocket.on("close", () => __awaiter(this, void 0, void 0, function* () {
                    currentWebSocket = null;
                    this.$logger.trace("Frontend socket closed!");
                    appDebugSocket.unpipe(packets);
                    packets.destroy();
                    yield device.destroyDebugSocket(appId);
                    if (!this.$options.watch) {
                        process.exit(0);
                    }
                }));
                clientConnectionLockRelease();
            });
            return server;
        });
    }
    removeAllProxies() {
        let deviceId;
        for (deviceId in this.deviceWebServers) {
            this.deviceWebServers[deviceId].close();
        }
        for (deviceId in this.deviceTcpServers) {
            this.deviceTcpServers[deviceId].close();
        }
        this.deviceWebServers = {};
        this.deviceTcpServers = {};
    }
}
exports.AppDebugSocketProxyFactory = AppDebugSocketProxyFactory;
yok_1.injector.register("appDebugSocketProxyFactory", AppDebugSocketProxyFactory);
//# sourceMappingURL=app-debug-socket-proxy-factory.js.map