8b8bef015e
* Add evaluation helpers * Make some helpers only available server-side They don't make any sense on the client side. * Fork the right thing
196 lines
7.0 KiB
TypeScript
196 lines
7.0 KiB
TypeScript
import * as cp from "child_process";
|
|
import * as net from "net";
|
|
import * as stream from "stream";
|
|
import { CallbackEmitter, ActiveEvalReadable, ActiveEvalWritable } from "@coder/protocol";
|
|
import { client } from "./client";
|
|
import { promisify } from "util";
|
|
|
|
declare var __non_webpack_require__: typeof require;
|
|
|
|
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];
|
|
|
|
// 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 args: string[] = [];
|
|
if (Array.isArray(callback)) {
|
|
args = callback;
|
|
callback = undefined;
|
|
}
|
|
|
|
this.ae = client.run((ae, command, method, args, options, callbackId) => {
|
|
const cp = __non_webpack_require__("child_process") as typeof import("child_process");
|
|
|
|
ae.preserveEnv(options);
|
|
|
|
let childProcess: cp.ChildProcess;
|
|
switch (method) {
|
|
case "exec":
|
|
childProcess = cp.exec(command, options, ae.maybeCallback(callbackId));
|
|
break;
|
|
case "spawn":
|
|
childProcess = cp.spawn(command, args, options);
|
|
break;
|
|
case "fork":
|
|
childProcess = ae.fork(command, args, options);
|
|
break;
|
|
default:
|
|
throw new Error(`invalid method ${method}`);
|
|
}
|
|
|
|
ae.on("disconnect", () => childProcess.disconnect());
|
|
ae.on("kill", (signal: string) => childProcess.kill(signal));
|
|
ae.on("ref", () => childProcess.ref());
|
|
ae.on("send", (message: string, callbackId: number) => childProcess.send(message, ae.maybeCallback(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));
|
|
|
|
if (childProcess.stdin) {
|
|
const stdinAe = ae.createUnique("stdin");
|
|
stdinAe.bindWritable(childProcess.stdin);
|
|
}
|
|
if (childProcess.stdout) {
|
|
const stdoutAe = ae.createUnique("stdout");
|
|
stdoutAe.bindReadable(childProcess.stdout);
|
|
}
|
|
if (childProcess.stderr) {
|
|
const stderrAe = ae.createUnique("stderr");
|
|
stderrAe.bindReadable(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(this.ae.createUnique("stdin"));
|
|
this.stdout = new ActiveEvalReadable(this.ae.createUnique("stdout"));
|
|
this.stderr = new ActiveEvalReadable(this.ae.createUnique("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 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;
|
|
}
|
|
}
|
|
|
|
class CP {
|
|
public readonly ChildProcess = ChildProcess;
|
|
|
|
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;
|