|
1 | 1 | import * as cp from "child_process"; |
2 | | -import { Client, useBuffer } from "@coder/protocol"; |
| 2 | +import * as net from "net"; |
| 3 | +import * as stream from "stream"; |
| 4 | +import { CallbackEmitter, ActiveEvalReadable, ActiveEvalWritable, createUniqueEval } from "./evaluation"; |
3 | 5 | import { client } from "./client"; |
4 | | -import { promisify } from "./util"; |
| 6 | +import { promisify } from "util"; |
5 | 7 |
|
6 | | -class CP { |
7 | | - public constructor( |
8 | | - private readonly client: Client, |
9 | | - ) { } |
| 8 | +declare var __non_webpack_require__: typeof require; |
10 | 9 |
|
11 | | - public exec = ( |
12 | | - command: string, |
13 | | - 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), |
14 | | - callback?: ((error: Error | null, stdout: string, stderr: string) => void) | ((error: Error | null, stdout: Buffer, stderr: Buffer) => void), |
15 | | - ): cp.ChildProcess => { |
16 | | - // TODO: Probably should add an `exec` instead of using `spawn`, especially |
17 | | - // since bash might not be available. |
18 | | - const childProcess = this.client.spawn("bash", ["-c", command.replace(/"/g, "\\\"")]); |
| 10 | +class ChildProcess extends CallbackEmitter implements cp.ChildProcess { |
| 11 | + private _connected: boolean = false; |
| 12 | + private _killed: boolean = false; |
| 13 | + private _pid = -1; |
| 14 | + public readonly stdin: stream.Writable; |
| 15 | + public readonly stdout: stream.Readable; |
| 16 | + public readonly stderr: stream.Readable; |
| 17 | + // We need the explicit type otherwise TypeScript thinks it is (Writable | Readable)[]. |
| 18 | + public readonly stdio: [stream.Writable, stream.Readable, stream.Readable] = [this.stdin, this.stdout, this.stderr]; |
19 | 19 |
|
20 | | - let stdout = ""; |
21 | | - childProcess.stdout.on("data", (data) => { |
22 | | - stdout += data.toString(); |
23 | | - }); |
| 20 | + // tslint:disable no-any |
| 21 | + public constructor(method: "exec", command: string, options?: { encoding?: string | null } & cp.ExecOptions | null, callback?: (...args: any[]) => void); |
| 22 | + public constructor(method: "fork", modulePath: string, options?: cp.ForkOptions, args?: string[]); |
| 23 | + public constructor(method: "spawn", command: string, options?: cp.SpawnOptions, args?: string[]); |
| 24 | + public constructor(method: "exec" | "spawn" | "fork", command: string, options: object = {}, callback?: string[] | ((...args: any[]) => void)) { |
| 25 | + // tslint:enable no-any |
| 26 | + super(); |
24 | 27 |
|
25 | | - let stderr = ""; |
26 | | - childProcess.stderr.on("data", (data) => { |
27 | | - stderr += data.toString(); |
28 | | - }); |
| 28 | + let args: string[] = []; |
| 29 | + if (Array.isArray(callback)) { |
| 30 | + args = callback; |
| 31 | + callback = undefined; |
| 32 | + } |
29 | 33 |
|
30 | | - childProcess.on("exit", (exitCode) => { |
31 | | - const error = exitCode !== 0 ? new Error(stderr) : null; |
32 | | - if (typeof options === "function") { |
33 | | - callback = options; |
34 | | - } |
35 | | - if (callback) { |
36 | | - // @ts-ignore not sure how to make this work. |
37 | | - callback( |
38 | | - error, |
39 | | - useBuffer(options) ? Buffer.from(stdout) : stdout, |
40 | | - useBuffer(options) ? Buffer.from(stderr) : stderr, |
41 | | - ); |
| 34 | + this.ae = client.run((ae, command, method, args, options, callbackId) => { |
| 35 | + const cp = __non_webpack_require__("child_process") as typeof import("child_process"); |
| 36 | + const { maybeCallback, createUniqueEval, bindWritable, bindReadable, preserveEnv } = __non_webpack_require__("@coder/ide/src/fill/evaluation") as typeof import("@coder/ide/src/fill/evaluation"); |
| 37 | + |
| 38 | + preserveEnv(options); |
| 39 | + |
| 40 | + let childProcess: cp.ChildProcess; |
| 41 | + switch (method) { |
| 42 | + case "exec": |
| 43 | + childProcess = cp.exec(command, options, maybeCallback(ae, callbackId)); |
| 44 | + break; |
| 45 | + case "spawn": |
| 46 | + childProcess = cp.spawn(command, args, options); |
| 47 | + break; |
| 48 | + case "fork": |
| 49 | + const forkOptions = options as cp.ForkOptions; |
| 50 | + if (forkOptions && forkOptions.env && forkOptions.env.AMD_ENTRYPOINT) { |
| 51 | + // TODO: This is vscode-specific and should be abstracted. |
| 52 | + const { forkModule } = __non_webpack_require__("@coder/server/src/vscode/bootstrapFork") as typeof import ("@coder/server/src/vscode/bootstrapFork"); |
| 53 | + childProcess = forkModule(forkOptions.env.AMD_ENTRYPOINT, args, forkOptions); |
| 54 | + } else { |
| 55 | + childProcess = cp.fork(command, args, options); |
| 56 | + } |
| 57 | + break; |
| 58 | + default: |
| 59 | + throw new Error(`invalid method ${method}`); |
42 | 60 | } |
| 61 | + |
| 62 | + ae.on("disconnect", () => childProcess.disconnect()); |
| 63 | + ae.on("kill", (signal) => childProcess.kill(signal)); |
| 64 | + ae.on("ref", () => childProcess.ref()); |
| 65 | + ae.on("send", (message, callbackId) => childProcess.send(message, maybeCallback(ae, callbackId))); |
| 66 | + ae.on("unref", () => childProcess.unref()); |
| 67 | + |
| 68 | + ae.emit("pid", childProcess.pid); |
| 69 | + childProcess.on("close", (code, signal) => ae.emit("close", code, signal)); |
| 70 | + childProcess.on("disconnect", () => ae.emit("disconnect")); |
| 71 | + childProcess.on("error", (error) => ae.emit("error", error)); |
| 72 | + childProcess.on("exit", (code, signal) => ae.emit("exit", code, signal)); |
| 73 | + childProcess.on("message", (message) => ae.emit("message", message)); |
| 74 | + |
| 75 | + bindWritable(createUniqueEval(ae, "stdin"), childProcess.stdin); |
| 76 | + bindReadable(createUniqueEval(ae, "stdout"), childProcess.stdout); |
| 77 | + bindReadable(createUniqueEval(ae, "stderr"), childProcess.stderr); |
| 78 | + |
| 79 | + return { |
| 80 | + onDidDispose: (cb): cp.ChildProcess => childProcess.on("close", cb), |
| 81 | + dispose: (): void => { |
| 82 | + childProcess.kill(); |
| 83 | + setTimeout(() => childProcess.kill("SIGKILL"), 5000); // Double tap. |
| 84 | + }, |
| 85 | + }; |
| 86 | + }, command, method, args, options, this.storeCallback(callback)); |
| 87 | + |
| 88 | + this.ae.on("pid", (pid) => { |
| 89 | + this._pid = pid; |
| 90 | + this._connected = true; |
43 | 91 | }); |
44 | 92 |
|
45 | | - // @ts-ignore TODO: not fully implemented |
46 | | - return childProcess; |
| 93 | + this.stdin = new ActiveEvalWritable(createUniqueEval(this.ae, "stdin")); |
| 94 | + this.stdout = new ActiveEvalReadable(createUniqueEval(this.ae, "stdout")); |
| 95 | + this.stderr = new ActiveEvalReadable(createUniqueEval(this.ae, "stderr")); |
| 96 | + |
| 97 | + this.ae.on("close", (code, signal) => this.emit("close", code, signal)); |
| 98 | + this.ae.on("disconnect", () => this.emit("disconnect")); |
| 99 | + this.ae.on("error", (error) => this.emit("error", error)); |
| 100 | + this.ae.on("exit", (code, signal) => { |
| 101 | + this._connected = false; |
| 102 | + this._killed = true; |
| 103 | + this.emit("exit", code, signal); |
| 104 | + }); |
| 105 | + this.ae.on("message", (message) => this.emit("message", message)); |
| 106 | + } |
| 107 | + |
| 108 | + public get pid(): number { return this._pid; } |
| 109 | + public get connected(): boolean { return this._connected; } |
| 110 | + public get killed(): boolean { return this._killed; } |
| 111 | + |
| 112 | + public kill(): void { this.ae.emit("kill"); } |
| 113 | + public disconnect(): void { this.ae.emit("disconnect"); } |
| 114 | + public ref(): void { this.ae.emit("ref"); } |
| 115 | + public unref(): void { this.ae.emit("unref"); } |
| 116 | + |
| 117 | + public send( |
| 118 | + message: any, // tslint:disable-line no-any to match spec |
| 119 | + sendHandle?: net.Socket | net.Server | ((error: Error) => void), |
| 120 | + options?: cp.MessageOptions | ((error: Error) => void), |
| 121 | + callback?: (error: Error) => void): boolean { |
| 122 | + if (typeof sendHandle === "function") { |
| 123 | + callback = sendHandle; |
| 124 | + sendHandle = undefined; |
| 125 | + } else if (typeof options === "function") { |
| 126 | + callback = options; |
| 127 | + options = undefined; |
| 128 | + } |
| 129 | + if (sendHandle || options) { |
| 130 | + throw new Error("sendHandle and options are not supported"); |
| 131 | + } |
| 132 | + this.ae.emit("send", message, this.storeCallback(callback)); |
| 133 | + |
| 134 | + // Unfortunately this will always have to be true since we can't retrieve |
| 135 | + // the actual response synchronously. |
| 136 | + return true; |
| 137 | + } |
| 138 | +} |
| 139 | + |
| 140 | +class CP { |
| 141 | + public readonly ChildProcess = ChildProcess; |
| 142 | + |
| 143 | + public exec = ( |
| 144 | + command: string, |
| 145 | + 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), |
| 146 | + callback?: ((error: cp.ExecException | null, stdout: string, stderr: string) => void) | ((error: cp.ExecException | null, stdout: Buffer, stderr: Buffer) => void), |
| 147 | + ): cp.ChildProcess => { |
| 148 | + if (typeof options === "function") { |
| 149 | + callback = options; |
| 150 | + options = undefined; |
| 151 | + } |
| 152 | + |
| 153 | + return new ChildProcess("exec", command, options, callback); |
47 | 154 | } |
48 | 155 |
|
49 | 156 | public fork = (modulePath: string, args?: string[] | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => { |
50 | | - if (options && options.env && options.env.AMD_ENTRYPOINT) { |
51 | | - // @ts-ignore TODO: not fully implemented |
52 | | - return this.client.bootstrapFork( |
53 | | - options.env.AMD_ENTRYPOINT, |
54 | | - Array.isArray(args) ? args : [], |
55 | | - // @ts-ignore TODO: env is a different type |
56 | | - Array.isArray(args) || !args ? options : args, |
57 | | - ); |
| 157 | + if (args && !Array.isArray(args)) { |
| 158 | + options = args; |
| 159 | + args = undefined; |
58 | 160 | } |
59 | 161 |
|
60 | | - // @ts-ignore TODO: not fully implemented |
61 | | - return this.client.fork( |
62 | | - modulePath, |
63 | | - Array.isArray(args) ? args : [], |
64 | | - // @ts-ignore TODO: env is a different type |
65 | | - Array.isArray(args) || !args ? options : args, |
66 | | - ); |
| 162 | + return new ChildProcess("fork", modulePath, options, args); |
67 | 163 | } |
68 | 164 |
|
69 | 165 | public spawn = (command: string, args?: string[] | cp.SpawnOptions, options?: cp.SpawnOptions): cp.ChildProcess => { |
70 | | - // @ts-ignore TODO: not fully implemented |
71 | | - return this.client.spawn( |
72 | | - command, |
73 | | - Array.isArray(args) ? args : [], |
74 | | - // @ts-ignore TODO: env is a different type |
75 | | - Array.isArray(args) || !args ? options : args, |
76 | | - ); |
| 166 | + if (args && !Array.isArray(args)) { |
| 167 | + options = args; |
| 168 | + args = undefined; |
| 169 | + } |
| 170 | + |
| 171 | + return new ChildProcess("spawn", command, options, args); |
77 | 172 | } |
78 | 173 | } |
79 | 174 |
|
80 | | -const fillCp = new CP(client); |
81 | | - |
82 | | -// tslint:disable-next-line no-any makes util.promisify return an object |
83 | | -(fillCp as any).exec[promisify.customPromisifyArgs] = ["stdout", "stderr"]; |
84 | | - |
| 175 | +const fillCp = new CP(); |
| 176 | +// Methods that don't follow the standard callback pattern (an error followed |
| 177 | +// by a single result) need to provide a custom promisify function. |
| 178 | +Object.defineProperty(fillCp.exec, promisify.custom, { |
| 179 | + value: ( |
| 180 | + command: string, |
| 181 | + options?: { encoding?: string | null } & cp.ExecOptions | null, |
| 182 | + ): Promise<{ stdout: string | Buffer, stderr: string | Buffer }> => { |
| 183 | + return new Promise((resolve, reject): void => { |
| 184 | + fillCp.exec(command, options, (error: cp.ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => { |
| 185 | + if (error) { |
| 186 | + reject(error); |
| 187 | + } else { |
| 188 | + resolve({ stdout, stderr }); |
| 189 | + } |
| 190 | + }); |
| 191 | + }); |
| 192 | + }, |
| 193 | +}); |
85 | 194 | export = fillCp; |
0 commit comments