@typescript/server-harness
Version:
Communicate with a tsserver process
213 lines • 8.29 kB
JavaScript
;
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