UNPKG

@typescript/server-harness

Version:
213 lines 8.29 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.launchServer = void 0; const cp = require("child_process"); const os = require("os"); /** * Forks a new server process. By default, the server will not have ATA or produce diagnostic events. */ function launchServer(tsserverPath, args, execArgv, env) { var _a; const eventListeners = []; const errorListeners = []; const exitListeners = []; const closeListeners = []; const serverProc = cp.fork(tsserverPath, args !== null && args !== void 0 ? args : ["--disableAutomaticTypingAcquisition"], { execArgv: execArgv !== null && execArgv !== void 0 ? execArgv : (_a = process.execArgv) === null || _a === void 0 ? void 0 : _a.map(arg => bumpDebugPort(arg)), env, stdio: ["pipe", "pipe", "ignore", "ipc"] }); const useNodeIpc = !!args && !!args.filter(a => a.toLocaleLowerCase() === "--useNodeIpc".toLocaleLowerCase()).length; const getNext = makeListeners(serverProc, useNodeIpc, eventListeners, errorListeners); serverProc.on("exit", code => { for (const listener of exitListeners) { listener(code); } }); serverProc.on("close", (code, signal) => { for (const listener of closeListeners) { listener(code, signal); } }); function on(event, listener) { switch (event) { case "event": eventListeners.push(listener); break; case "communicationError": errorListeners.push(listener); break; case "exit": exitListeners.push(listener); break; case "close": closeListeners.push(listener); break; } } return { message: request => message(serverProc, useNodeIpc, getNext, request), exitOrKill: timeoutMs => exitOrKill(serverProc, useNodeIpc, timeoutMs), kill: () => kill(serverProc), on, pid: serverProc.pid, }; } exports.launchServer = launchServer; function bumpDebugPort(arg) { const match = /^(--inspect(?:-brk)?)(?:=(\d+))?$/.exec(arg); return match ? `${match[1]}=${match[2] ? (+match[2] + 1) : 9230}` : arg; } function makeListeners(serverProc, useNodeIpc, eventListeners, errorListeners) { const waiters = new Map(); const objects = new Map(); // Once we get out of sync with the incoming stream, we can't recover. // Ignore further data, rather than producing more errors. let hadCommunicationError = false; if (useNodeIpc) { serverProc.on('message', obj => { if (hadCommunicationError) return; try { handleMessage(obj); } catch (e) { hadCommunicationError = true; for (const listener of errorListeners) { listener(e); } } }); } else { let unconsumedChunks = []; let unconsumedByteLength = 0; let headerByteLength = -1; let currentByteLength = -1; serverProc.stdout.on('data', buffer => { if (hadCommunicationError) return; try { unconsumedChunks.push(buffer); unconsumedByteLength += buffer.byteLength; while (true) { if (headerByteLength < 0) { // This could be done directly in the buffer, but strings are much simpler const text = Buffer.concat(unconsumedChunks, unconsumedByteLength).toString("utf8"); const headerMatch = text.match(/Content-Length: (\d+)/); // Receiving a chunk shorter than this is very unlikely if (!headerMatch) break; headerByteLength = text.indexOf("{", headerMatch.index + headerMatch[0].length); // All single-byte characters if (headerByteLength < 0) return; // Don't have the body yet const bodyByteLength = +headerMatch[1]; currentByteLength = headerByteLength + bodyByteLength + (os.EOL.length - 1); // tsserver assumes the final newline has length one on every OS } if (unconsumedByteLength < currentByteLength) return; const combined = Buffer.concat(unconsumedChunks, unconsumedByteLength); const jsonText = combined.toString("utf8", headerByteLength, currentByteLength); const obj = JSON.parse(jsonText); unconsumedByteLength -= currentByteLength; unconsumedChunks = unconsumedByteLength > 0 ? [combined.subarray(currentByteLength)] : []; headerByteLength = -1; currentByteLength = -1; handleMessage(obj); } } catch (e) { hadCommunicationError = true; for (const listener of errorListeners) { listener(e); } } }); } function handleMessage(obj) { if (obj.type === "event" && obj.event !== "requestCompleted") { for (const listener of eventListeners) { listener(obj); } return; } const requestSeq = obj.type === "event" ? obj.body.request_seq : obj.request_seq; const w = waiters.get(requestSeq); if (w) { waiters.delete(requestSeq); w(obj); } else { objects.set(requestSeq, obj); } } const getResponse = (seq) => new Promise(resolve => { const obj = objects.get(seq); if (obj) { objects.delete(seq); resolve(obj); } else { waiters.set(seq, resolve); } }); return getResponse; } async function message(serverProc, useNodeIpc, getResponse, request) { // TODO: It would be more robust to handle write/send failures if (!serverProc.connected || serverProc.killed || serverProc.exitCode !== null || serverProc.signalCode !== null) { throw new Error("Server has exited"); } const seq = request.seq; if (useNodeIpc) { serverProc.send(request); } else { serverProc.stdin.write(JSON.stringify(request) + "\n"); } // Several commands, such as `configure`, are flagged as not requiring a response. In practice, however, // they do return trivial responses. The only command that definitely won't return a response is `exit`. // If it eventually becomes necessary to handle to handle another non-responding command, we'll probably // need to make message take an optional parameter to that effect. if (request.command === "exit") { return Promise.resolve(undefined); } return await getResponse(seq); } async function exitOrKill(serverProc, useNodeIpc, timeoutMs) { return new Promise((resolve, reject) => { let timedOut = false; serverProc.once("close", () => { if (!timedOut) { clearTimeout(timeout); resolve(true); } }); const timeout = setTimeout(async () => { timedOut = true; await kill(serverProc); resolve(false); }, timeoutMs); // No response, so nothing to await message(serverProc, useNodeIpc, /*getResponse*/ undefined, { "command": "exit" }).catch(err => reject(err)); ; }); } function kill(serverProc) { return new Promise((resolve, reject) => { serverProc.once("close", () => { resolve(); }); // If the server has already exited, there won't be a close event if (serverProc.exitCode !== null || serverProc.signalCode !== null) { resolve(); } if (!serverProc.kill("SIGKILL")) { reject(new Error("Failed to send kill signal to server")); } }); } //# sourceMappingURL=index.js.map