From 8b8bef015e2fc582880a3dcff7487edf04027574 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 22 Feb 2019 15:56:29 -0600 Subject: [PATCH] Add evaluation helpers (#33) * Add evaluation helpers * Make some helpers only available server-side They don't make any sense on the client side. * Fork the right thing --- package.json | 4 +- packages/ide/src/fill/child_process.ts | 33 +- packages/ide/src/fill/electron.ts | 2 +- packages/ide/src/fill/fs.ts | 70 ++-- packages/ide/src/fill/net.ts | 19 +- packages/protocol/src/browser/client.ts | 48 +-- packages/protocol/src/browser/evaluate.ts | 8 - .../src/common/helpers.ts} | 303 +++++++++++------- packages/protocol/src/common/util.ts | 6 - packages/protocol/src/index.ts | 2 +- packages/protocol/src/node/evaluate.ts | 13 +- packages/protocol/src/node/server.ts | 4 +- packages/protocol/test/evaluate.test.ts | 4 +- packages/server/src/cli.ts | 24 +- packages/tunnel/yarn.lock | 4 + packages/vscode/src/dialog.ts | 2 +- packages/vscode/src/fill/node-pty.ts | 7 +- 17 files changed, 306 insertions(+), 247 deletions(-) delete mode 100644 packages/protocol/src/browser/evaluate.ts rename packages/{ide/src/fill/evaluation.ts => protocol/src/common/helpers.ts} (52%) create mode 100644 packages/tunnel/yarn.lock diff --git a/package.json b/package.json index 2e0397b87..f196b2280 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "build:rules": "cd ./rules && tsc -p .", "packages:install": "cd ./packages && yarn", "postinstall": "npm-run-all --parallel packages:install build:rules", - "start": "cd ./packages/server && yarn start", - "task": "ts-node -r tsconfig-paths/register build/tasks.ts", + "start": "cd ./packages/server && yarn start", + "task": "ts-node -r tsconfig-paths/register build/tasks.ts", "test": "cd ./packages && yarn test" }, "devDependencies": { diff --git a/packages/ide/src/fill/child_process.ts b/packages/ide/src/fill/child_process.ts index c11cc18ad..92a2a7e02 100644 --- a/packages/ide/src/fill/child_process.ts +++ b/packages/ide/src/fill/child_process.ts @@ -1,7 +1,7 @@ import * as cp from "child_process"; import * as net from "net"; import * as stream from "stream"; -import { CallbackEmitter, ActiveEvalReadable, ActiveEvalWritable, createUniqueEval } from "./evaluation"; +import { CallbackEmitter, ActiveEvalReadable, ActiveEvalWritable } from "@coder/protocol"; import { client } from "./client"; import { promisify } from "util"; @@ -33,27 +33,19 @@ class ChildProcess extends CallbackEmitter implements cp.ChildProcess { 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); + ae.preserveEnv(options); let childProcess: cp.ChildProcess; switch (method) { case "exec": - childProcess = cp.exec(command, options, maybeCallback(ae, callbackId)); + childProcess = cp.exec(command, options, ae.maybeCallback(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); - } + childProcess = ae.fork(command, args, options); break; default: throw new Error(`invalid method ${method}`); @@ -62,7 +54,7 @@ class ChildProcess extends CallbackEmitter implements cp.ChildProcess { 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, maybeCallback(ae, callbackId))); + ae.on("send", (message: string, callbackId: number) => childProcess.send(message, ae.maybeCallback(callbackId))); ae.on("unref", () => childProcess.unref()); ae.emit("pid", childProcess.pid); @@ -73,13 +65,16 @@ class ChildProcess extends CallbackEmitter implements cp.ChildProcess { childProcess.on("message", (message) => ae.emit("message", message)); if (childProcess.stdin) { - bindWritable(createUniqueEval(ae, "stdin"), childProcess.stdin); + const stdinAe = ae.createUnique("stdin"); + stdinAe.bindWritable(childProcess.stdin); } if (childProcess.stdout) { - bindReadable(createUniqueEval(ae, "stdout"), childProcess.stdout); + const stdoutAe = ae.createUnique("stdout"); + stdoutAe.bindReadable(childProcess.stdout); } if (childProcess.stderr) { - bindReadable(createUniqueEval(ae, "stderr"), childProcess.stderr); + const stderrAe = ae.createUnique("stderr"); + stderrAe.bindReadable(childProcess.stderr); } return { @@ -96,9 +91,9 @@ class ChildProcess extends CallbackEmitter implements cp.ChildProcess { 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.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")); diff --git a/packages/ide/src/fill/electron.ts b/packages/ide/src/fill/electron.ts index 6cac1e5bb..c537f6c8b 100644 --- a/packages/ide/src/fill/electron.ts +++ b/packages/ide/src/fill/electron.ts @@ -152,7 +152,7 @@ class Clipboard { class Shell { public async moveItemToTrash(path: string): Promise { - await client.evaluate((path) => { + await client.evaluate((_helper, path) => { const trash = __non_webpack_require__("trash") as typeof import("trash"); return trash(path); diff --git a/packages/ide/src/fill/fs.ts b/packages/ide/src/fill/fs.ts index 558352236..e191422fa 100644 --- a/packages/ide/src/fill/fs.ts +++ b/packages/ide/src/fill/fs.ts @@ -26,7 +26,7 @@ class FS { callback = mode; mode = undefined; } - this.client.evaluate((path, mode) => { + this.client.evaluate((_helper, path, mode) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -44,7 +44,7 @@ class FS { callback = options; options = undefined; } - this.client.evaluate((path, data, options) => { + this.client.evaluate((_helper, path, data, options) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -57,7 +57,7 @@ class FS { } public chmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((path, mode) => { + this.client.evaluate((_helper, path, mode) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -70,7 +70,7 @@ class FS { } public chown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((path, uid, gid) => { + this.client.evaluate((_helper, path, uid, gid) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -83,7 +83,7 @@ class FS { } public close = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((fd) => { + this.client.evaluate((_helper, fd) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -99,7 +99,7 @@ class FS { if (typeof flags === "function") { callback = flags; } - this.client.evaluate((src, dest, flags) => { + this.client.evaluate((_helper, src, dest, flags) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -166,7 +166,7 @@ class FS { } public exists = (path: fs.PathLike, callback: (exists: boolean) => void): void => { - this.client.evaluate((path) => { + this.client.evaluate((_helper, path) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -179,7 +179,7 @@ class FS { } public fchmod = (fd: number, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((fd, mode) => { + this.client.evaluate((_helper, fd, mode) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -192,7 +192,7 @@ class FS { } public fchown = (fd: number, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((fd, uid, gid) => { + this.client.evaluate((_helper, fd, uid, gid) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -205,7 +205,7 @@ class FS { } public fdatasync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((fd) => { + this.client.evaluate((_helper, fd) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -218,7 +218,7 @@ class FS { } public fstat = (fd: number, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => { - this.client.evaluate((fd) => { + this.client.evaluate((_helper, fd) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); const tslib = __non_webpack_require__("tslib") as typeof import("tslib"); @@ -242,7 +242,7 @@ class FS { } public fsync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((fd) => { + this.client.evaluate((_helper, fd) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -259,7 +259,7 @@ class FS { callback = len; len = undefined; } - this.client.evaluate((fd, len) => { + this.client.evaluate((_helper, fd, len) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -272,7 +272,7 @@ class FS { } public futimes = (fd: number, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((fd, atime, mtime) => { + this.client.evaluate((_helper, fd, atime, mtime) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -285,7 +285,7 @@ class FS { } public lchmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((path, mode) => { + this.client.evaluate((_helper, path, mode) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -298,7 +298,7 @@ class FS { } public lchown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((path, uid, gid) => { + this.client.evaluate((_helper, path, uid, gid) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -311,7 +311,7 @@ class FS { } public link = (existingPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((existingPath, newPath) => { + this.client.evaluate((_helper, existingPath, newPath) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -324,7 +324,7 @@ class FS { } public lstat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => { - this.client.evaluate((path) => { + this.client.evaluate((_helper, path) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); const tslib = __non_webpack_require__("tslib") as typeof import("tslib"); @@ -352,7 +352,7 @@ class FS { callback = mode; mode = undefined; } - this.client.evaluate((path, mode) => { + this.client.evaluate((_helper, path, mode) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -369,7 +369,7 @@ class FS { callback = options; options = undefined; } - this.client.evaluate((prefix, options) => { + this.client.evaluate((_helper, prefix, options) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -386,7 +386,7 @@ class FS { callback = mode; mode = undefined; } - this.client.evaluate((path, flags, mode) => { + this.client.evaluate((_helper, path, flags, mode) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -399,7 +399,7 @@ class FS { } public read = (fd: number, buffer: TBuffer, offset: number, length: number, position: number | null, callback: (err: NodeJS.ErrnoException, bytesRead: number, buffer: TBuffer) => void): void => { - this.client.evaluate((fd, length, position) => { + this.client.evaluate((_helper, fd, length, position) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); const buffer = new _Buffer(length); @@ -424,7 +424,7 @@ class FS { callback = options; options = undefined; } - this.client.evaluate((path, options) => { + this.client.evaluate((_helper, path, options) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -442,7 +442,7 @@ class FS { options = undefined; } // TODO: options can also take `withFileTypes` but the types aren't working. - this.client.evaluate((path, options) => { + this.client.evaluate((_helper, path, options) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -459,7 +459,7 @@ class FS { callback = options; options = undefined; } - this.client.evaluate((path, options) => { + this.client.evaluate((_helper, path, options) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -476,7 +476,7 @@ class FS { callback = options; options = undefined; } - this.client.evaluate((path, options) => { + this.client.evaluate((_helper, path, options) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -489,7 +489,7 @@ class FS { } public rename = (oldPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((oldPath, newPath) => { + this.client.evaluate((_helper, oldPath, newPath) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -502,7 +502,7 @@ class FS { } public rmdir = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((path) => { + this.client.evaluate((_helper, path) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -515,7 +515,7 @@ class FS { } public stat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => { - this.client.evaluate((path) => { + this.client.evaluate((_helper, path) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); const tslib = __non_webpack_require__("tslib") as typeof import("tslib"); @@ -547,7 +547,7 @@ class FS { callback = type; type = undefined; } - this.client.evaluate((target, path, type) => { + this.client.evaluate((_helper, target, path, type) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -564,7 +564,7 @@ class FS { callback = len; len = undefined; } - this.client.evaluate((path, len) => { + this.client.evaluate((_helper, path, len) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -577,7 +577,7 @@ class FS { } public unlink = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((path) => { + this.client.evaluate((_helper, path) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -590,7 +590,7 @@ class FS { } public utimes = (path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => { - this.client.evaluate((path, atime, mtime) => { + this.client.evaluate((_helper, path, atime, mtime) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -615,7 +615,7 @@ class FS { callback = position; position = undefined; } - this.client.evaluate((fd, buffer, offset, length, position) => { + this.client.evaluate((_helper, fd, buffer, offset, length, position) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); @@ -638,7 +638,7 @@ class FS { callback = options; options = undefined; } - this.client.evaluate((path, data, options) => { + this.client.evaluate((_helper, path, data, options) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); diff --git a/packages/ide/src/fill/net.ts b/packages/ide/src/fill/net.ts index 4f3a41b42..cd44deefd 100644 --- a/packages/ide/src/fill/net.ts +++ b/packages/ide/src/fill/net.ts @@ -1,6 +1,5 @@ import * as net from "net"; -import { ActiveEval } from "@coder/protocol"; -import { CallbackEmitter, ActiveEvalDuplex, createUniqueEval } from "./evaluation"; +import { CallbackEmitter, ActiveEvalDuplex, ActiveEvalHelper } from "@coder/protocol"; import { client } from "./client"; declare var __non_webpack_require__: typeof require; @@ -9,12 +8,11 @@ class Socket extends ActiveEvalDuplex implements net.Socket { private _connecting: boolean = false; private _destroyed: boolean = false; - public constructor(options?: net.SocketConstructorOpts, ae?: ActiveEval) { + public constructor(options?: net.SocketConstructorOpts, ae?: ActiveEvalHelper) { super(ae || client.run((ae, options) => { const net = __non_webpack_require__("net") as typeof import("net"); - const { bindSocket } = __non_webpack_require__("@coder/ide/src/fill/evaluation") as typeof import("@coder/ide/src/fill/evaluation"); - return bindSocket(ae, new net.Socket(options)); + return ae.bindSocket(new net.Socket(options)); }, options)); this.ae.on("connect", () => { @@ -94,14 +92,14 @@ class Server extends CallbackEmitter implements net.Server { this.ae = client.run((ae, options, callbackId) => { const net = __non_webpack_require__("net") as typeof import("net"); - const { maybeCallback, bindSocket, createUniqueEval } = __non_webpack_require__("@coder/ide/src/fill/evaluation") as typeof import("@coder/ide/src/fill/evaluation"); let connectionId = 0; const sockets = new Map(); const storeSocket = (socket: net.Socket): number => { const socketId = connectionId++; sockets.set(socketId, socket); - const disposer = bindSocket(createUniqueEval(ae, socketId), socket); + const socketAe = ae.createUnique(socketId); + const disposer = socketAe.bindSocket(socket); socket.on("close", () => { disposer.dispose(); sockets.delete(socketId); @@ -110,7 +108,7 @@ class Server extends CallbackEmitter implements net.Server { return socketId; }; - const callback = maybeCallback(ae, callbackId); + const callback = ae.maybeCallback(callbackId); let server = new net.Server(options, typeof callback !== "undefined" ? (socket): void => { callback(storeSocket(socket)); } : undefined); @@ -120,7 +118,7 @@ class Server extends CallbackEmitter implements net.Server { server.on("error", (error) => ae.emit("error", error)); server.on("listening", () => ae.emit("listening")); - ae.on("close", (callbackId: number) => server.close(maybeCallback(ae, callbackId))); + ae.on("close", (callbackId: number) => server.close(ae.maybeCallback(callbackId))); ae.on("listen", (handle?: net.ListenOptions | number | string) => server.listen(handle)); ae.on("ref", () => server.ref()); ae.on("unref", () => server.unref()); @@ -147,7 +145,8 @@ class Server extends CallbackEmitter implements net.Server { }); this.ae.on("connection", (socketId) => { - const socket = new Socket(undefined, createUniqueEval(this.ae, socketId)); + const socketAe = this.ae.createUnique(socketId); + const socket = new Socket(undefined, socketAe); this.sockets.set(socketId, socket); socket.on("close", () => this.sockets.delete(socketId)); if (connectionListener) { diff --git a/packages/protocol/src/browser/client.ts b/packages/protocol/src/browser/client.ts index ff621a748..0e0960bd0 100644 --- a/packages/protocol/src/browser/client.ts +++ b/packages/protocol/src/browser/client.ts @@ -1,10 +1,10 @@ import { EventEmitter } from "events"; import { Emitter } from "@coder/events"; import { logger, field } from "@coder/logger"; -import { ReadWriteConnection, InitData, OperatingSystem, SharedProcessData } from "../common/connection"; -import { Disposer, stringify, parse } from "../common/util"; import { NewEvalMessage, ServerMessage, EvalDoneMessage, EvalFailedMessage, ClientMessage, WorkingInitMessage, EvalEventMessage } from "../proto"; -import { ActiveEval } from "./evaluate"; +import { ReadWriteConnection, InitData, OperatingSystem, SharedProcessData } from "../common/connection"; +import { ActiveEvalHelper, EvalHelper, Disposer, ServerActiveEvalHelper } from "../common/helpers"; +import { stringify, parse } from "../common/util"; /** * Client accepts an arbitrary connection intended to communicate with the Server. @@ -56,13 +56,13 @@ export class Client { return this.initDataPromise; } - public run(func: (ae: ActiveEval) => Disposer): ActiveEval; - public run(func: (ae: ActiveEval, a1: T1) => Disposer, a1: T1): ActiveEval; - public run(func: (ae: ActiveEval, a1: T1, a2: T2) => Disposer, a1: T1, a2: T2): ActiveEval; - public run(func: (ae: ActiveEval, a1: T1, a2: T2, a3: T3) => Disposer, a1: T1, a2: T2, a3: T3): ActiveEval; - public run(func: (ae: ActiveEval, a1: T1, a2: T2, a3: T3, a4: T4) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4): ActiveEval; - public run(func: (ae: ActiveEval, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): ActiveEval; - public run(func: (ae: ActiveEval, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6): ActiveEval; + public run(func: (helper: ServerActiveEvalHelper) => Disposer): ActiveEvalHelper; + public run(func: (helper: ServerActiveEvalHelper, a1: T1) => Disposer, a1: T1): ActiveEvalHelper; + public run(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2) => Disposer, a1: T1, a2: T2): ActiveEvalHelper; + public run(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2, a3: T3) => Disposer, a1: T1, a2: T2, a3: T3): ActiveEvalHelper; + public run(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2, a3: T3, a4: T4) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4): ActiveEvalHelper; + public run(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): ActiveEvalHelper; + public run(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6): ActiveEvalHelper; /** * Run a function on the server and provide an event emitter which allows * listening and emitting to the emitter provided to that function. The @@ -70,7 +70,7 @@ export class Client { * disconnects and for notifying when disposal has happened outside manual * activation. */ - public run(func: (ae: ActiveEval, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6) => Disposer, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6): ActiveEval { + public run(func: (helper: ServerActiveEvalHelper, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6) => Disposer, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6): ActiveEvalHelper { const doEval = this.doEvaluate(func, a1, a2, a3, a4, a5, a6, true); // This takes server events and emits them to the client's emitter. @@ -89,9 +89,9 @@ export class Client { eventEmitter.emit("error", ex); }); - // This takes client events and emits them to the server's emitter and - // listens to events received from the server (via the event hook above). - return { + return new ActiveEvalHelper({ + // This takes client events and emits them to the server's emitter and + // listens to events received from the server (via the event hook above). // tslint:disable no-any on: (event: string, cb: (...args: any[]) => void): EventEmitter => eventEmitter.on(event, cb), emit: (event: string, ...args: any[]): void => { @@ -105,21 +105,21 @@ export class Client { }, removeAllListeners: (event: string): EventEmitter => eventEmitter.removeAllListeners(event), // tslint:enable no-any - }; + }); } - public evaluate(func: () => R | Promise): Promise; - public evaluate(func: (a1: T1) => R | Promise, a1: T1): Promise; - public evaluate(func: (a1: T1, a2: T2) => R | Promise, a1: T1, a2: T2): Promise; - public evaluate(func: (a1: T1, a2: T2, a3: T3) => R | Promise, a1: T1, a2: T2, a3: T3): Promise; - public evaluate(func: (a1: T1, a2: T2, a3: T3, a4: T4) => R | Promise, a1: T1, a2: T2, a3: T3, a4: T4): Promise; - public evaluate(func: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => R | Promise, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): Promise; - public evaluate(func: (a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => R | Promise, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6): Promise; + public evaluate(func: (helper: EvalHelper) => R | Promise): Promise; + public evaluate(func: (helper: EvalHelper, a1: T1) => R | Promise, a1: T1): Promise; + public evaluate(func: (helper: EvalHelper, a1: T1, a2: T2) => R | Promise, a1: T1, a2: T2): Promise; + public evaluate(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3) => R | Promise, a1: T1, a2: T2, a3: T3): Promise; + public evaluate(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4) => R | Promise, a1: T1, a2: T2, a3: T3, a4: T4): Promise; + public evaluate(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => R | Promise, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): Promise; + public evaluate(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => R | Promise, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6): Promise; /** * Evaluates a function on the server. * To pass variables, ensure they are serializable and passed through the included function. * @example - * const returned = await this.client.evaluate((value) => { + * const returned = await this.client.evaluate((helper, value) => { * return value; * }, "hi"); * console.log(returned); @@ -127,7 +127,7 @@ export class Client { * @param func Function to evaluate * @returns Promise rejected or resolved from the evaluated function */ - public evaluate(func: (a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6) => R | Promise, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6): Promise { + public evaluate(func: (helper: EvalHelper, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6) => R | Promise, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6): Promise { return this.doEvaluate(func, a1, a2, a3, a4, a5, a6, false).completed; } diff --git a/packages/protocol/src/browser/evaluate.ts b/packages/protocol/src/browser/evaluate.ts deleted file mode 100644 index 780d20b86..000000000 --- a/packages/protocol/src/browser/evaluate.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface ActiveEval { - removeAllListeners(event?: string): void; - - // tslint:disable no-any - emit(event: string, ...args: any[]): void; - on(event: string, cb: (...args: any[]) => void): void; - // tslint:disable no-any -} diff --git a/packages/ide/src/fill/evaluation.ts b/packages/protocol/src/common/helpers.ts similarity index 52% rename from packages/ide/src/fill/evaluation.ts rename to packages/protocol/src/common/helpers.ts index eeea73568..b3c3e79b2 100644 --- a/packages/ide/src/fill/evaluation.ts +++ b/packages/protocol/src/common/helpers.ts @@ -1,154 +1,215 @@ -import { SpawnOptions, ForkOptions } from "child_process"; +import { ChildProcess, SpawnOptions, ForkOptions } from "child_process"; import { EventEmitter } from "events"; import { Socket } from "net"; import { Duplex, Readable, Writable } from "stream"; +import { IDisposable } from "@coder/disposable"; import { logger } from "@coder/logger"; -import { ActiveEval, Disposer } from "@coder/protocol"; // tslint:disable no-any -/** - * If there is a callback ID, return a function that emits the callback event on - * the active evaluation with that ID and all arguments passed to it. Otherwise, - * return undefined. - */ -export const maybeCallback = (ae: ActiveEval, callbackId?: number): ((...args: any[]) => void) | undefined => { - return typeof callbackId !== "undefined" ? (...args: any[]): void => { - ae.emit("callback", callbackId, ...args); - } : undefined; -}; -// Some spawn code tries to preserve the env (the debug adapter for -// instance) but the env is mostly blank (since we're in the browser), so -// we'll just always preserve the main process.env here, otherwise it -// won't have access to PATH, etc. -// TODO: An alternative solution would be to send the env to the browser? -export const preserveEnv = (options: SpawnOptions | ForkOptions): void => { - if (options && options.env) { - options.env = { ...process.env, ...options.env }; +export type ForkProvider = (modulePath: string, args: string[], options: ForkOptions, dataDir?: string) => ChildProcess; + +export interface Disposer extends IDisposable { + onDidDispose: (cb: () => void) => void; +} + +interface ActiveEvalEmitter { + removeAllListeners(event?: string): void; + emit(event: string, ...args: any[]): void; + on(event: string, cb: (...args: any[]) => void): void; +} + +/** + * Helper class for evaluations. + */ +export class EvalHelper { + /** + * Some spawn code tries to preserve the env (the debug adapter for instance) + * but the env is mostly blank (since we're in the browser), so we'll just + * always preserve the main process.env here, otherwise it won't have access + * to PATH, etc. + * TODO: An alternative solution would be to send the env to the browser? + */ + public preserveEnv(options: SpawnOptions | ForkOptions): void { + if (options && options.env) { + options.env = { ...process.env, ...options.env }; + } } -}; +} /** - * Bind a socket to an active evaluation. + * Helper class for active evaluations. */ -export const bindSocket = (ae: ActiveEval, socket: Socket): Disposer => { - socket.on("connect", () => ae.emit("connect")); - socket.on("lookup", (error, address, family, host) => ae.emit("lookup", error, address, family, host)); - socket.on("timeout", () => ae.emit("timeout")); - - ae.on("connect", (options, callbackId) => socket.connect(options, maybeCallback(ae, callbackId))); - ae.on("ref", () => socket.ref()); - ae.on("setKeepAlive", (enable, initialDelay) => socket.setKeepAlive(enable, initialDelay)); - ae.on("setNoDelay", (noDelay) => socket.setNoDelay(noDelay)); - ae.on("setTimeout", (timeout, callbackId) => socket.setTimeout(timeout, maybeCallback(ae, callbackId))); - ae.on("unref", () => socket.unref()); - - bindReadable(ae, socket); - bindWritable(ae, socket); - - return { - onDidDispose: (cb): Socket => socket.on("close", cb), - dispose: (): void => { - socket.removeAllListeners(); - socket.end(); - socket.destroy(); - socket.unref(); - }, - }; -}; - -/** - * Bind a writable stream to an active evaluation. - */ -export const bindWritable = (ae: ActiveEval, writable: Writable | Duplex): void => { - if (!((writable as Readable).read)) { // To avoid binding twice. - writable.on("close", () => ae.emit("close")); - writable.on("error", (error) => ae.emit("error", error)); - - ae.on("destroy", () => writable.destroy()); +export class ActiveEvalHelper extends EvalHelper implements ActiveEvalEmitter { + public constructor(private readonly emitter: ActiveEvalEmitter) { + super(); } - writable.on("drain", () => ae.emit("drain")); - writable.on("finish", () => ae.emit("finish")); - writable.on("pipe", () => ae.emit("pipe")); - writable.on("unpipe", () => ae.emit("unpipe")); + public removeAllListeners(event?: string): void { + this.emitter.removeAllListeners(event); + } - ae.on("cork", () => writable.cork()); - ae.on("end", (chunk, encoding, callbackId) => writable.end(chunk, encoding, maybeCallback(ae, callbackId))); - ae.on("setDefaultEncoding", (encoding) => writable.setDefaultEncoding(encoding)); - ae.on("uncork", () => writable.uncork()); - // Sockets can pass an fd instead of a callback but streams cannot. - ae.on("write", (chunk, encoding, fd, callbackId) => writable.write(chunk, encoding, maybeCallback(ae, callbackId) || fd)); -}; + public emit(event: string, ...args: any[]): void { + this.emitter.emit(event, ...args); + } -/** - * Bind a readable stream to an active evaluation. - */ -export const bindReadable = (ae: ActiveEval, readable: Readable): void => { - // Streams don't have an argument on close but sockets do. - readable.on("close", (...args: any[]) => ae.emit("close", ...args)); - readable.on("data", (data) => ae.emit("data", data)); - readable.on("end", () => ae.emit("end")); - readable.on("error", (error) => ae.emit("error", error)); - readable.on("readable", () => ae.emit("readable")); + public on(event: string, cb: (...args: any[]) => void): void { + this.emitter.on(event, cb); + } - ae.on("destroy", () => readable.destroy()); - ae.on("pause", () => readable.pause()); - ae.on("push", (chunk, encoding) => readable.push(chunk, encoding)); - ae.on("resume", () => readable.resume()); - ae.on("setEncoding", (encoding) => readable.setEncoding(encoding)); - ae.on("unshift", (chunk) => readable.unshift(chunk)); -}; + /** + * Create a new helper to make unique events for an item. + */ + public createUnique(id: number | "stdout" | "stderr" | "stdin"): ActiveEvalHelper { + return new ActiveEvalHelper(this.createUniqueEmitter(id)); + } -/** - * Wrap an evaluation emitter to make unique events for an item to prevent - * conflicts when it shares that emitter with other items. - */ -export const createUniqueEval = (ae: ActiveEval, id: number | "stdout" | "stderr" | "stdin"): ActiveEval => { - let events = []; + /** + * Wrap the evaluation emitter to make unique events for an item to prevent + * conflicts when it shares that emitter with other items. + */ + protected createUniqueEmitter(id: number | "stdout" | "stderr" | "stdin"): ActiveEvalEmitter { + let events = []; - return { - removeAllListeners: (event?: string): void => { - if (!event) { - events.forEach((e) => ae.removeAllListeners(e)); - events = []; - } else { - const index = events.indexOf(event); - if (index !== -1) { - events.splice(index, 1); - ae.removeAllListeners(`${event}:${id}`); + return { + removeAllListeners: (event?: string): void => { + if (!event) { + events.forEach((e) => this.removeAllListeners(e)); + events = []; + } else { + const index = events.indexOf(event); + if (index !== -1) { + events.splice(index, 1); + this.removeAllListeners(`${event}:${id}`); + } } - } - }, - emit: (event: string, ...args: any[]): void => { - ae.emit(`${event}:${id}`, ...args); - }, - on: (event: string, cb: (...args: any[]) => void): void => { - if (!events.includes(event)) { - events.push(event); - } - ae.on(`${event}:${id}`, cb); - }, - }; -}; + }, + emit: (event: string, ...args: any[]): void => { + this.emit(`${event}:${id}`, ...args); + }, + on: (event: string, cb: (...args: any[]) => void): void => { + if (!events.includes(event)) { + events.push(event); + } + this.on(`${event}:${id}`, cb); + }, + }; + } +} + +/** + * Helper class for server-side active evaluations. + */ +export class ServerActiveEvalHelper extends ActiveEvalHelper { + public constructor(emitter: ActiveEvalEmitter, public readonly fork: ForkProvider) { + super(emitter); + } + + /** + * If there is a callback ID, return a function that emits the callback event + * on the active evaluation with that ID and all arguments passed to it. + * Otherwise, return undefined. + */ + public maybeCallback(callbackId?: number): ((...args: any[]) => void) | undefined { + return typeof callbackId !== "undefined" ? (...args: any[]): void => { + this.emit("callback", callbackId, ...args); + } : undefined; + } + + /** + * Bind a socket to an active evaluation and returns a disposer. + */ + public bindSocket(socket: Socket): Disposer { + socket.on("connect", () => this.emit("connect")); + socket.on("lookup", (error, address, family, host) => this.emit("lookup", error, address, family, host)); + socket.on("timeout", () => this.emit("timeout")); + + this.on("connect", (options, callbackId) => socket.connect(options, this.maybeCallback(callbackId))); + this.on("ref", () => socket.ref()); + this.on("setKeepAlive", (enable, initialDelay) => socket.setKeepAlive(enable, initialDelay)); + this.on("setNoDelay", (noDelay) => socket.setNoDelay(noDelay)); + this.on("setTimeout", (timeout, callbackId) => socket.setTimeout(timeout, this.maybeCallback(callbackId))); + this.on("unref", () => socket.unref()); + + this.bindReadable(socket); + this.bindWritable(socket); + + return { + onDidDispose: (cb): Socket => socket.on("close", cb), + dispose: (): void => { + socket.removeAllListeners(); + socket.end(); + socket.destroy(); + socket.unref(); + }, + }; + } + + /** + * Bind a writable stream to the active evaluation. + */ + public bindWritable(writable: Writable | Duplex): void { + if (!((writable as Readable).read)) { // To avoid binding twice. + writable.on("close", () => this.emit("close")); + writable.on("error", (error) => this.emit("error", error)); + + this.on("destroy", () => writable.destroy()); + } + + writable.on("drain", () => this.emit("drain")); + writable.on("finish", () => this.emit("finish")); + writable.on("pipe", () => this.emit("pipe")); + writable.on("unpipe", () => this.emit("unpipe")); + + this.on("cork", () => writable.cork()); + this.on("end", (chunk, encoding, callbackId) => writable.end(chunk, encoding, this.maybeCallback(callbackId))); + this.on("setDefaultEncoding", (encoding) => writable.setDefaultEncoding(encoding)); + this.on("uncork", () => writable.uncork()); + // Sockets can pass an fd instead of a callback but streams cannot. + this.on("write", (chunk, encoding, fd, callbackId) => writable.write(chunk, encoding, this.maybeCallback(callbackId) || fd)); + } + + /** + * Bind a readable stream to the active evaluation. + */ + public bindReadable(readable: Readable): void { + // Streams don't have an argument on close but sockets do. + readable.on("close", (...args: any[]) => this.emit("close", ...args)); + readable.on("data", (data) => this.emit("data", data)); + readable.on("end", () => this.emit("end")); + readable.on("error", (error) => this.emit("error", error)); + readable.on("readable", () => this.emit("readable")); + + this.on("destroy", () => readable.destroy()); + this.on("pause", () => readable.pause()); + this.on("push", (chunk, encoding) => readable.push(chunk, encoding)); + this.on("resume", () => readable.resume()); + this.on("setEncoding", (encoding) => readable.setEncoding(encoding)); + this.on("unshift", (chunk) => readable.unshift(chunk)); + } + + public createUnique(id: number | "stdout" | "stderr" | "stdin"): ServerActiveEvalHelper { + return new ServerActiveEvalHelper(this.createUniqueEmitter(id), this.fork); + } +} /** * An event emitter that can store callbacks with IDs in a map so we can pass * them back and forth through an active evaluation using those IDs. */ export class CallbackEmitter extends EventEmitter { - private _ae: ActiveEval | undefined; + private _ae: ActiveEvalHelper | undefined; private callbackId = 0; private readonly callbacks = new Map(); - public constructor(ae?: ActiveEval) { + public constructor(ae?: ActiveEvalHelper) { super(); if (ae) { this.ae = ae; } } - protected get ae(): ActiveEval { + protected get ae(): ActiveEvalHelper { if (!this._ae) { throw new Error("trying to access active evaluation before it has been set"); } @@ -156,7 +217,7 @@ export class CallbackEmitter extends EventEmitter { return this._ae; } - protected set ae(ae: ActiveEval) { + protected set ae(ae: ActiveEvalHelper) { if (this._ae) { throw new Error("cannot override active evaluation"); } @@ -195,7 +256,7 @@ export class CallbackEmitter extends EventEmitter { * A writable stream over an active evaluation. */ export class ActiveEvalWritable extends CallbackEmitter implements Writable { - public constructor(ae: ActiveEval) { + public constructor(ae: ActiveEvalHelper) { super(ae); // Streams don't have an argument on close but sockets do. this.ae.on("close", (...args: any[]) => this.emit("close", ...args)); @@ -249,7 +310,7 @@ export class ActiveEvalWritable extends CallbackEmitter implements Writable { * A readable stream over an active evaluation. */ export class ActiveEvalReadable extends CallbackEmitter implements Readable { - public constructor(ae: ActiveEval) { + public constructor(ae: ActiveEvalHelper) { super(ae); this.ae.on("close", () => this.emit("close")); this.ae.on("data", (data) => this.emit("data", data)); @@ -290,7 +351,7 @@ export class ActiveEvalReadable extends CallbackEmitter implements Readable { */ export class ActiveEvalDuplex extends ActiveEvalReadable implements Duplex { // Some unfortunate duplication here since we can't have multiple extends. - public constructor(ae: ActiveEval) { + public constructor(ae: ActiveEvalHelper) { super(ae); this.ae.on("drain", () => this.emit("drain")); this.ae.on("finish", () => this.emit("finish")); diff --git a/packages/protocol/src/common/util.ts b/packages/protocol/src/common/util.ts index 5cb9b5cc5..bc96ba9c9 100644 --- a/packages/protocol/src/common/util.ts +++ b/packages/protocol/src/common/util.ts @@ -1,5 +1,3 @@ -import { IDisposable } from "@coder/disposable"; - /** * Return true if we're in a browser environment (including web workers). */ @@ -84,7 +82,3 @@ export const parse = (arg: string): any => { // tslint:disable-line no-any return result; }; - -export interface Disposer extends IDisposable { - onDidDispose: (cb: () => void) => void; -} diff --git a/packages/protocol/src/index.ts b/packages/protocol/src/index.ts index 81904fa96..6d293bd28 100644 --- a/packages/protocol/src/index.ts +++ b/packages/protocol/src/index.ts @@ -1,4 +1,4 @@ export * from "./browser/client"; -export * from "./browser/evaluate"; export * from "./common/connection"; +export * from "./common/helpers"; export * from "./common/util"; diff --git a/packages/protocol/src/node/evaluate.ts b/packages/protocol/src/node/evaluate.ts index 4e9088e2e..3d66ccd90 100644 --- a/packages/protocol/src/node/evaluate.ts +++ b/packages/protocol/src/node/evaluate.ts @@ -1,8 +1,10 @@ +import { fork as cpFork } from "child_process"; import { EventEmitter } from "events"; import * as vm from "vm"; import { logger, field } from "@coder/logger"; import { NewEvalMessage, EvalFailedMessage, EvalDoneMessage, ServerMessage, EvalEventMessage } from "../proto"; import { SendableConnection } from "../common/connection"; +import { ServerActiveEvalHelper, EvalHelper, ForkProvider } from "../common/helpers"; import { stringify, parse } from "../common/util"; export interface ActiveEvaluation { @@ -11,7 +13,7 @@ export interface ActiveEvaluation { } declare var __non_webpack_require__: typeof require; -export const evaluate = (connection: SendableConnection, message: NewEvalMessage, onDispose: () => void): ActiveEvaluation | void => { +export const evaluate = (connection: SendableConnection, message: NewEvalMessage, onDispose: () => void, fork?: ForkProvider): ActiveEvaluation | void => { /** * Send the response and call onDispose. */ @@ -46,7 +48,10 @@ export const evaluate = (connection: SendableConnection, message: NewEvalMessage let eventEmitter = message.getActive() ? new EventEmitter(): undefined; const sandbox = { - eventEmitter: eventEmitter ? { + helper: eventEmitter ? new ServerActiveEvalHelper({ + removeAllListeners: (event?: string): void => { + eventEmitter!.removeAllListeners(event); + }, // tslint:disable no-any on: (event: string, cb: (...args: any[]) => void): void => { eventEmitter!.on(event, (...args: any[]) => { @@ -73,7 +78,7 @@ export const evaluate = (connection: SendableConnection, message: NewEvalMessage connection.send(serverMsg.serializeBinary()); }, // tslint:enable no-any - } : undefined, + }, fork || cpFork) : new EvalHelper(), _Buffer: Buffer, // When the client is ran from Webpack, it will replace // __non_webpack_require__ with require, which we then need to provide to @@ -94,7 +99,7 @@ export const evaluate = (connection: SendableConnection, message: NewEvalMessage let value: any; // tslint:disable-line no-any try { - const code = `(${message.getFunction()})(${eventEmitter ? "eventEmitter, " : ""}...args);`; + const code = `(${message.getFunction()})(helper, ...args);`; value = vm.runInNewContext(code, sandbox, { // If the code takes longer than this to return, it is killed and throws. timeout: message.getTimeout() || 15000, diff --git a/packages/protocol/src/node/server.ts b/packages/protocol/src/node/server.ts index 448390f25..8464a6ac4 100644 --- a/packages/protocol/src/node/server.ts +++ b/packages/protocol/src/node/server.ts @@ -5,12 +5,14 @@ import { promisify } from "util"; import { logger, field } from "@coder/logger"; import { ClientMessage, WorkingInitMessage, ServerMessage } from "../proto"; import { evaluate, ActiveEvaluation } from "./evaluate"; +import { ForkProvider } from "../common/helpers"; import { ReadWriteConnection } from "../common/connection"; export interface ServerOptions { readonly workingDirectory: string; readonly dataDirectory: string; readonly builtInExtensionsDirectory: string; + readonly fork?: ForkProvider; } export class Server { @@ -105,7 +107,7 @@ export class Server { logger.trace(() => [ `dispose ${evalMessage.getId()}, ${this.evals.size} left`, ]); - }); + }, this.options ? this.options.fork : undefined); if (resp) { this.evals.set(evalMessage.getId(), resp); } diff --git a/packages/protocol/test/evaluate.test.ts b/packages/protocol/test/evaluate.test.ts index db3fe67de..00f00106e 100644 --- a/packages/protocol/test/evaluate.test.ts +++ b/packages/protocol/test/evaluate.test.ts @@ -13,7 +13,7 @@ describe("Evaluate", () => { it("should compute from string", async () => { const start = "ban\%\$\"``a,,,,asdasd"; - const value = await client.evaluate((a) => { + const value = await client.evaluate((_helper, a) => { return a; }, start); @@ -21,7 +21,7 @@ describe("Evaluate", () => { }, 100); it("should compute from object", async () => { - const value = await client.evaluate((arg) => { + const value = await client.evaluate((_helper, arg) => { return arg.bananas * 2; }, { bananas: 1 }); diff --git a/packages/server/src/cli.ts b/packages/server/src/cli.ts index 039516417..cf58c2554 100644 --- a/packages/server/src/cli.ts +++ b/packages/server/src/cli.ts @@ -1,12 +1,13 @@ import { field, logger } from "@coder/logger"; import { ServerMessage, SharedProcessActiveMessage } from "@coder/protocol/src/proto"; import { Command, flags } from "@oclif/command"; +import { fork, ForkOptions, ChildProcess } from "child_process"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import * as WebSocket from "ws"; import { createApp } from "./server"; -import { requireModule, requireFork } from "./vscode/bootstrapFork"; +import { requireModule, requireFork, forkModule } from "./vscode/bootstrapFork"; import { SharedProcess, SharedProcessState } from "./vscode/sharedProcess"; import { setup as setupNativeModules } from "./modules"; import { fillFs } from "./fill"; @@ -158,13 +159,20 @@ export class Entry extends Command { app.use(require("webpack-hot-middleware")(compiler)); } }, { - builtInExtensionsDirectory: builtInExtensionsDir, - dataDirectory: dataDir, - workingDirectory: workingDir, - }, password, hasCustomHttps ? { - key: certKeyData, - cert: certData, - } : undefined); + builtInExtensionsDirectory: builtInExtensionsDir, + dataDirectory: dataDir, + workingDirectory: workingDir, + fork: (modulePath: string, args: string[], options: ForkOptions, dataDir?: string): ChildProcess => { + if (options && options.env && options.env.AMD_ENTRYPOINT) { + return forkModule(options.env.AMD_ENTRYPOINT, args, options, dataDir); + } + + return fork(modulePath, args, options); + }, + }, password, hasCustomHttps ? { + key: certKeyData, + cert: certData, + } : undefined); logger.info("Starting webserver...", field("host", flags.host), field("port", flags.port)); app.server.listen(flags.port, flags.host); diff --git a/packages/tunnel/yarn.lock b/packages/tunnel/yarn.lock new file mode 100644 index 000000000..fb57ccd13 --- /dev/null +++ b/packages/tunnel/yarn.lock @@ -0,0 +1,4 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + diff --git a/packages/vscode/src/dialog.ts b/packages/vscode/src/dialog.ts index ec43851e5..f368920b4 100644 --- a/packages/vscode/src/dialog.ts +++ b/packages/vscode/src/dialog.ts @@ -380,7 +380,7 @@ class Dialog { } private async list(directory: string): Promise> { - return ideClient.evaluate((directory) => { + return ideClient.evaluate((_helper, directory) => { const fs = __non_webpack_require__("fs") as typeof import("fs"); const util = __non_webpack_require__("util") as typeof import("util"); const path = __non_webpack_require__("path") as typeof import("path"); diff --git a/packages/vscode/src/fill/node-pty.ts b/packages/vscode/src/fill/node-pty.ts index 2b294adb3..ae2e1dbfb 100644 --- a/packages/vscode/src/fill/node-pty.ts +++ b/packages/vscode/src/fill/node-pty.ts @@ -1,7 +1,7 @@ import { client } from "@coder/ide/src/fill/client"; import { EventEmitter } from "events"; import * as nodePty from "node-pty"; -import { ActiveEval } from "@coder/protocol"; +import { ActiveEvalHelper } from "@coder/protocol"; // Use this to prevent Webpack from hijacking require. declare var __non_webpack_require__: typeof require; @@ -11,16 +11,15 @@ declare var __non_webpack_require__: typeof require; */ class Pty implements nodePty.IPty { private readonly emitter = new EventEmitter(); - private readonly ae: ActiveEval; + private readonly ae: ActiveEvalHelper; private _pid = -1; private _process = ""; public constructor(file: string, args: string[] | string, options: nodePty.IPtyForkOptions) { this.ae = client.run((ae, file, args, options) => { const nodePty = __non_webpack_require__("node-pty") as typeof import("node-pty"); - const { preserveEnv } = __non_webpack_require__("@coder/ide/src/fill/evaluation") as typeof import("@coder/ide/src/fill/evaluation"); - preserveEnv(options); + ae.preserveEnv(options); const ptyProc = nodePty.spawn(file, args, options);