Make everything use active evals (#30)
* Add trace log level * Use active eval to implement spdlog * Split server/client active eval interfaces Since all properties are *not* valid on both sides * +200% fire resistance * Implement exec using active evaluations * Fully implement child process streams * Watch impl, move child_process back to explicitly adding events Automatically forwarding all events might be the right move, but wanna think/discuss it a bit more because it didn't come out very cleanly. * Would you like some args with that callback? * Implement the rest of child_process using active evals * Rampant memory leaks Emit "kill" to active evaluations when client disconnects in order to kill processes. Most likely won't be the final solution. * Resolve some minor issues with output panel * Implement node-pty with active evals * Provide clearTimeout to vm sandbox * Implement socket with active evals * Extract some callback logic Also remove some eval interfaces, need to re-think those. * Implement net.Server and remainder of net.Socket using active evals * Implement dispose for active evaluations * Use trace for express requests * Handle sending buffers through evaluation events * Make event logging a bit more clear * Fix some errors due to us not actually instantiating until connect/listen * is this a commit message? * We can just create the evaluator in the ctor Not sure what I was thinking. * memory leak for you, memory leak for everyone * it's a ternary now * Don't dispose automatically on close or error The code may or may not be disposable at that point. * Handle parsing buffers on the client side as well * Remove unused protobuf * Remove TypedValue * Remove unused forkProvider and test * Improve dispose pattern for active evals * Socket calls close after error; no need to bind both * Improve comment * Comment is no longer wishy washy due to explicit boolean * Simplify check for sendHandle and options * Replace _require with __non_webpack_require__ Webpack will then replace this with `require` which we then provide to the vm sandbox. * Provide path.parse * Prevent original-fs from loading * Start with a pid of -1 vscode immediately checks the PID to see if the debug process launch correctly, but of course we don't get the pid synchronously. * Pass arguments to bootstrap-fork * Fully implement streams Was causing errors because internally the stream would set this.writing to true and it would never become false, so subsequent messages would never send. * Fix serializing errors and streams emitting errors multiple times * Was emitting close to data * Fix missing path for spawned processes * Move evaluation onDispose call Now it's accurate and runs when the active evaluation has actually disposed. * Fix promisifying fs.exists * Fix some active eval callback issues * Patch existsSync in debug adapter
This commit is contained in:
@@ -1,85 +1,194 @@
|
||||
import * as cp from "child_process";
|
||||
import { Client, useBuffer } from "@coder/protocol";
|
||||
import * as net from "net";
|
||||
import * as stream from "stream";
|
||||
import { CallbackEmitter, ActiveEvalReadable, ActiveEvalWritable, createUniqueEval } from "./evaluation";
|
||||
import { client } from "./client";
|
||||
import { promisify } from "./util";
|
||||
import { promisify } from "util";
|
||||
|
||||
class CP {
|
||||
public constructor(
|
||||
private readonly client: Client,
|
||||
) { }
|
||||
declare var __non_webpack_require__: typeof require;
|
||||
|
||||
public exec = (
|
||||
command: string,
|
||||
options?: { encoding?: BufferEncoding | string | "buffer" | null } & cp.ExecOptions | null | ((error: Error | null, stdout: string, stderr: string) => void) | ((error: Error | null, stdout: Buffer, stderr: Buffer) => void),
|
||||
callback?: ((error: Error | null, stdout: string, stderr: string) => void) | ((error: Error | null, stdout: Buffer, stderr: Buffer) => void),
|
||||
): cp.ChildProcess => {
|
||||
// TODO: Probably should add an `exec` instead of using `spawn`, especially
|
||||
// since bash might not be available.
|
||||
const childProcess = this.client.spawn("bash", ["-c", command.replace(/"/g, "\\\"")]);
|
||||
class ChildProcess extends CallbackEmitter implements cp.ChildProcess {
|
||||
private _connected: boolean = false;
|
||||
private _killed: boolean = false;
|
||||
private _pid = -1;
|
||||
public readonly stdin: stream.Writable;
|
||||
public readonly stdout: stream.Readable;
|
||||
public readonly stderr: stream.Readable;
|
||||
// We need the explicit type otherwise TypeScript thinks it is (Writable | Readable)[].
|
||||
public readonly stdio: [stream.Writable, stream.Readable, stream.Readable] = [this.stdin, this.stdout, this.stderr];
|
||||
|
||||
let stdout = "";
|
||||
childProcess.stdout.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
// tslint:disable no-any
|
||||
public constructor(method: "exec", command: string, options?: { encoding?: string | null } & cp.ExecOptions | null, callback?: (...args: any[]) => void);
|
||||
public constructor(method: "fork", modulePath: string, options?: cp.ForkOptions, args?: string[]);
|
||||
public constructor(method: "spawn", command: string, options?: cp.SpawnOptions, args?: string[]);
|
||||
public constructor(method: "exec" | "spawn" | "fork", command: string, options: object = {}, callback?: string[] | ((...args: any[]) => void)) {
|
||||
// tslint:enable no-any
|
||||
super();
|
||||
|
||||
let stderr = "";
|
||||
childProcess.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
childProcess.on("exit", (exitCode) => {
|
||||
const error = exitCode !== 0 ? new Error(stderr) : null;
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
}
|
||||
if (callback) {
|
||||
// @ts-ignore not sure how to make this work.
|
||||
callback(
|
||||
error,
|
||||
useBuffer(options) ? Buffer.from(stdout) : stdout,
|
||||
useBuffer(options) ? Buffer.from(stderr) : stderr,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// @ts-ignore TODO: not fully implemented
|
||||
return childProcess;
|
||||
}
|
||||
|
||||
public fork = (modulePath: string, args?: string[] | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => {
|
||||
if (options && options.env && options.env.AMD_ENTRYPOINT) {
|
||||
// @ts-ignore TODO: not fully implemented
|
||||
return this.client.bootstrapFork(
|
||||
options.env.AMD_ENTRYPOINT,
|
||||
Array.isArray(args) ? args : [],
|
||||
// @ts-ignore TODO: env is a different type
|
||||
Array.isArray(args) || !args ? options : args,
|
||||
);
|
||||
let args: string[] = [];
|
||||
if (Array.isArray(callback)) {
|
||||
args = callback;
|
||||
callback = undefined;
|
||||
}
|
||||
|
||||
// @ts-ignore TODO: not fully implemented
|
||||
return this.client.fork(
|
||||
modulePath,
|
||||
Array.isArray(args) ? args : [],
|
||||
// @ts-ignore TODO: env is a different type
|
||||
Array.isArray(args) || !args ? options : args,
|
||||
);
|
||||
this.ae = client.run((ae, command, method, args, options, callbackId) => {
|
||||
const cp = __non_webpack_require__("child_process") as typeof import("child_process");
|
||||
const { maybeCallback, createUniqueEval, bindWritable, bindReadable, preserveEnv } = __non_webpack_require__("@coder/ide/src/fill/evaluation") as typeof import("@coder/ide/src/fill/evaluation");
|
||||
|
||||
preserveEnv(options);
|
||||
|
||||
let childProcess: cp.ChildProcess;
|
||||
switch (method) {
|
||||
case "exec":
|
||||
childProcess = cp.exec(command, options, maybeCallback(ae, callbackId));
|
||||
break;
|
||||
case "spawn":
|
||||
childProcess = cp.spawn(command, args, options);
|
||||
break;
|
||||
case "fork":
|
||||
const forkOptions = options as cp.ForkOptions;
|
||||
if (forkOptions && forkOptions.env && forkOptions.env.AMD_ENTRYPOINT) {
|
||||
// TODO: This is vscode-specific and should be abstracted.
|
||||
const { forkModule } = __non_webpack_require__("@coder/server/src/vscode/bootstrapFork") as typeof import ("@coder/server/src/vscode/bootstrapFork");
|
||||
childProcess = forkModule(forkOptions.env.AMD_ENTRYPOINT, args, forkOptions);
|
||||
} else {
|
||||
childProcess = cp.fork(command, args, options);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(`invalid method ${method}`);
|
||||
}
|
||||
|
||||
ae.on("disconnect", () => childProcess.disconnect());
|
||||
ae.on("kill", (signal) => childProcess.kill(signal));
|
||||
ae.on("ref", () => childProcess.ref());
|
||||
ae.on("send", (message, callbackId) => childProcess.send(message, maybeCallback(ae, callbackId)));
|
||||
ae.on("unref", () => childProcess.unref());
|
||||
|
||||
ae.emit("pid", childProcess.pid);
|
||||
childProcess.on("close", (code, signal) => ae.emit("close", code, signal));
|
||||
childProcess.on("disconnect", () => ae.emit("disconnect"));
|
||||
childProcess.on("error", (error) => ae.emit("error", error));
|
||||
childProcess.on("exit", (code, signal) => ae.emit("exit", code, signal));
|
||||
childProcess.on("message", (message) => ae.emit("message", message));
|
||||
|
||||
bindWritable(createUniqueEval(ae, "stdin"), childProcess.stdin);
|
||||
bindReadable(createUniqueEval(ae, "stdout"), childProcess.stdout);
|
||||
bindReadable(createUniqueEval(ae, "stderr"), childProcess.stderr);
|
||||
|
||||
return {
|
||||
onDidDispose: (cb): cp.ChildProcess => childProcess.on("close", cb),
|
||||
dispose: (): void => {
|
||||
childProcess.kill();
|
||||
setTimeout(() => childProcess.kill("SIGKILL"), 5000); // Double tap.
|
||||
},
|
||||
};
|
||||
}, command, method, args, options, this.storeCallback(callback));
|
||||
|
||||
this.ae.on("pid", (pid) => {
|
||||
this._pid = pid;
|
||||
this._connected = true;
|
||||
});
|
||||
|
||||
this.stdin = new ActiveEvalWritable(createUniqueEval(this.ae, "stdin"));
|
||||
this.stdout = new ActiveEvalReadable(createUniqueEval(this.ae, "stdout"));
|
||||
this.stderr = new ActiveEvalReadable(createUniqueEval(this.ae, "stderr"));
|
||||
|
||||
this.ae.on("close", (code, signal) => this.emit("close", code, signal));
|
||||
this.ae.on("disconnect", () => this.emit("disconnect"));
|
||||
this.ae.on("error", (error) => this.emit("error", error));
|
||||
this.ae.on("exit", (code, signal) => {
|
||||
this._connected = false;
|
||||
this._killed = true;
|
||||
this.emit("exit", code, signal);
|
||||
});
|
||||
this.ae.on("message", (message) => this.emit("message", message));
|
||||
}
|
||||
|
||||
public spawn = (command: string, args?: string[] | cp.SpawnOptions, options?: cp.SpawnOptions): cp.ChildProcess => {
|
||||
// @ts-ignore TODO: not fully implemented
|
||||
return this.client.spawn(
|
||||
command,
|
||||
Array.isArray(args) ? args : [],
|
||||
// @ts-ignore TODO: env is a different type
|
||||
Array.isArray(args) || !args ? options : args,
|
||||
);
|
||||
public get pid(): number { return this._pid; }
|
||||
public get connected(): boolean { return this._connected; }
|
||||
public get killed(): boolean { return this._killed; }
|
||||
|
||||
public kill(): void { this.ae.emit("kill"); }
|
||||
public disconnect(): void { this.ae.emit("disconnect"); }
|
||||
public ref(): void { this.ae.emit("ref"); }
|
||||
public unref(): void { this.ae.emit("unref"); }
|
||||
|
||||
public send(
|
||||
message: any, // tslint:disable-line no-any to match spec
|
||||
sendHandle?: net.Socket | net.Server | ((error: Error) => void),
|
||||
options?: cp.MessageOptions | ((error: Error) => void),
|
||||
callback?: (error: Error) => void): boolean {
|
||||
if (typeof sendHandle === "function") {
|
||||
callback = sendHandle;
|
||||
sendHandle = undefined;
|
||||
} else if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
if (sendHandle || options) {
|
||||
throw new Error("sendHandle and options are not supported");
|
||||
}
|
||||
this.ae.emit("send", message, this.storeCallback(callback));
|
||||
|
||||
// Unfortunately this will always have to be true since we can't retrieve
|
||||
// the actual response synchronously.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const fillCp = new CP(client);
|
||||
class CP {
|
||||
public readonly ChildProcess = ChildProcess;
|
||||
|
||||
// tslint:disable-next-line no-any makes util.promisify return an object
|
||||
(fillCp as any).exec[promisify.customPromisifyArgs] = ["stdout", "stderr"];
|
||||
public exec = (
|
||||
command: string,
|
||||
options?: { encoding?: string | null } & cp.ExecOptions | null | ((error: cp.ExecException | null, stdout: string, stderr: string) => void) | ((error: cp.ExecException | null, stdout: Buffer, stderr: Buffer) => void),
|
||||
callback?: ((error: cp.ExecException | null, stdout: string, stderr: string) => void) | ((error: cp.ExecException | null, stdout: Buffer, stderr: Buffer) => void),
|
||||
): cp.ChildProcess => {
|
||||
if (typeof options === "function") {
|
||||
callback = options;
|
||||
options = undefined;
|
||||
}
|
||||
|
||||
return new ChildProcess("exec", command, options, callback);
|
||||
}
|
||||
|
||||
public fork = (modulePath: string, args?: string[] | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => {
|
||||
if (args && !Array.isArray(args)) {
|
||||
options = args;
|
||||
args = undefined;
|
||||
}
|
||||
|
||||
return new ChildProcess("fork", modulePath, options, args);
|
||||
}
|
||||
|
||||
public spawn = (command: string, args?: string[] | cp.SpawnOptions, options?: cp.SpawnOptions): cp.ChildProcess => {
|
||||
if (args && !Array.isArray(args)) {
|
||||
options = args;
|
||||
args = undefined;
|
||||
}
|
||||
|
||||
return new ChildProcess("spawn", command, options, args);
|
||||
}
|
||||
}
|
||||
|
||||
const fillCp = new CP();
|
||||
// Methods that don't follow the standard callback pattern (an error followed
|
||||
// by a single result) need to provide a custom promisify function.
|
||||
Object.defineProperty(fillCp.exec, promisify.custom, {
|
||||
value: (
|
||||
command: string,
|
||||
options?: { encoding?: string | null } & cp.ExecOptions | null,
|
||||
): Promise<{ stdout: string | Buffer, stderr: string | Buffer }> => {
|
||||
return new Promise((resolve, reject): void => {
|
||||
fillCp.exec(command, options, (error: cp.ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve({ stdout, stderr });
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
export = fillCp;
|
||||
|
||||
Reference in New Issue
Block a user