Refactor evaluations (#285)

* Replace evaluations with proxies and messages

* Return proxies synchronously

Otherwise events can be lost.

* Ensure events cannot be missed

* Refactor remaining fills

* Use more up-to-date version of util

For callbackify.

* Wait for dispose to come back before removing

This prevents issues with the "done" event not always being the last
event fired. For example a socket might close and then end, but only
if the caller called end.

* Remove old node-pty tests

* Fix emitting events twice on duplex streams

* Preserve environment when spawning processes

* Throw a better error if the proxy doesn't exist

* Remove rimraf dependency from ide

* Update net.Server.listening

* Use exit event instead of killed

Doesn't look like killed is even a thing.

* Add response timeout to server

* Fix trash

* Require node-pty & spdlog after they get unpackaged

This fixes an error when running in the binary.

* Fix errors in down emitter preventing reconnecting

* Fix disposing proxies when nothing listens to "error" event

* Refactor event tests to use jest.fn()

* Reject proxy call when disconnected

Otherwise it'll wait for the timeout which is a waste of time since we
already know the connection is dead.

* Use nbin for binary packaging

* Remove additional module requires

* Attempt to remove require for local bootstrap-fork

* Externalize fsevents
This commit is contained in:
Asher 2019-03-26 13:01:25 -05:00 committed by Kyle Carberry
parent d16c6aeb30
commit dc2253e718
75 changed files with 5866 additions and 6181 deletions

View File

@ -33,50 +33,12 @@ const buildServerBinaryPackage = register("build:server:binary:package", async (
throw new Error("Cannot build binary without server bundle built");
}
await buildServerBinaryCopy();
await dependencyNexeBinary();
const resp = await runner.execute(isWin ? "npm.cmd" : "npm", ["run", "build:nexe"]);
const resp = await runner.execute(isWin ? "npm.cmd" : "npm", ["run", "build:binary"]);
if (resp.exitCode !== 0) {
throw new Error(`Failed to package binary: ${resp.stderr}`);
}
});
const dependencyNexeBinary = register("dependency:nexe", async (runner) => {
if (os.platform() === "linux" && process.env.COMPRESS === "true") {
// Download the nexe binary so we can compress it before nexe runs. If we
// don't want compression we don't need to do anything since nexe will take
// care of getting the binary.
const nexeDir = path.join(os.homedir(), ".nexe");
const targetBinaryName = `${os.platform()}-${os.arch()}-${process.version.substr(1)}`;
const targetBinaryPath = path.join(nexeDir, targetBinaryName);
if (!fs.existsSync(targetBinaryPath)) {
fse.mkdirpSync(nexeDir);
runner.cwd = nexeDir;
await runner.execute("wget", [`https://github.com/nexe/nexe/releases/download/v3.0.0-beta.15/${targetBinaryName}`]);
await runner.execute("chmod", ["+x", targetBinaryPath]);
}
// Compress with upx if it doesn't already look compressed.
if (fs.statSync(targetBinaryPath).size >= 20000000) {
// It needs to be executable for upx to work, which it might not be if
// nexe downloaded it.
fs.chmodSync(targetBinaryPath, "755");
const upxFolder = path.join(os.tmpdir(), "upx");
const upxBinary = path.join(upxFolder, "upx");
if (!fs.existsSync(upxBinary)) {
fse.mkdirpSync(upxFolder);
runner.cwd = upxFolder;
const upxExtract = await runner.execute("bash", ["-c", "curl -L https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz | tar xJ --strip-components=1"]);
if (upxExtract.exitCode !== 0) {
throw new Error(`Failed to extract upx: ${upxExtract.stderr}`);
}
}
if (!fs.existsSync(upxBinary)) {
throw new Error("Not sure how, but the UPX binary does not exist");
}
await runner.execute(upxBinary, [targetBinaryPath]);
}
}
});
const buildServerBinaryCopy = register("build:server:binary:copy", async (runner) => {
const cliPath = path.join(pkgsPath, "server");
const cliBuildPath = path.join(cliPath, "build");

View File

@ -37,10 +37,12 @@
"ts-loader": "^5.3.3",
"ts-node": "^7.0.1",
"tsconfig-paths": "^3.8.0",
"tslib": "^1.9.3",
"tslint": "^5.12.1",
"typescript": "^3.2.2",
"typescript-tslint-plugin": "^0.2.1",
"uglifyjs-webpack-plugin": "^2.1.1",
"util": "^0.11.1",
"webpack": "^4.28.4",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.2.1",

View File

@ -1,28 +1,51 @@
import { IDisposable } from "@coder/disposable";
export interface Event<T> {
(listener: (e: T) => void): IDisposable;
(listener: (value: T) => void): IDisposable;
(id: number | string, listener: (value: T) => void): IDisposable;
}
/**
* Emitter typecasts for a single event type.
* Emitter typecasts for a single event type. You can optionally use IDs, but
* using undefined with IDs will not work. If you emit without an ID, *all*
* listeners regardless of their ID (or lack thereof) will receive the event.
* Similarly, if you listen without an ID you will get *all* events for any or
* no ID.
*/
export class Emitter<T> {
private listeners = <Array<(e: T) => void>>[];
private listeners = <Array<(value: T) => void>>[];
private readonly idListeners = new Map<number | string, Array<(value: T) => void>>();
public get event(): Event<T> {
return (cb: (e: T) => void): IDisposable => {
if (this.listeners) {
this.listeners.push(cb);
return (id: number | string | ((value: T) => void), cb?: (value: T) => void): IDisposable => {
if (typeof id !== "function") {
if (this.idListeners.has(id)) {
this.idListeners.get(id)!.push(cb!);
} else {
this.idListeners.set(id, [cb!]);
}
return {
dispose: (): void => {
if (this.idListeners.has(id)) {
const cbs = this.idListeners.get(id)!;
const i = cbs.indexOf(cb!);
if (i !== -1) {
cbs.splice(i, 1);
}
}
},
};
}
cb = id;
this.listeners.push(cb);
return {
dispose: (): void => {
if (this.listeners) {
const i = this.listeners.indexOf(cb);
if (i !== -1) {
this.listeners.splice(i, 1);
}
const i = this.listeners.indexOf(cb!);
if (i !== -1) {
this.listeners.splice(i, 1);
}
},
};
@ -32,16 +55,45 @@ export class Emitter<T> {
/**
* Emit an event with a value.
*/
public emit(value: T): void {
if (this.listeners) {
this.listeners.forEach((t) => t(value));
public emit(value: T): void;
public emit(id: number | string, value: T): void;
public emit(id: number | string | T, value?: T): void {
if ((typeof id === "number" || typeof id === "string") && typeof value !== "undefined") {
if (this.idListeners.has(id)) {
this.idListeners.get(id)!.forEach((cb) => cb(value!));
}
this.listeners.forEach((cb) => cb(value!));
} else {
this.idListeners.forEach((cbs) => cbs.forEach((cb) => cb((id as T)!)));
this.listeners.forEach((cb) => cb((id as T)!));
}
}
/**
* Dispose the current events.
*/
public dispose(): void {
this.listeners = [];
public dispose(): void;
public dispose(id: number | string): void;
public dispose(id?: number | string): void {
if (typeof id !== "undefined") {
this.idListeners.delete(id);
} else {
this.listeners = [];
this.idListeners.clear();
}
}
public get counts(): { [key: string]: number } {
const counts = <{ [key: string]: number }>{};
if (this.listeners.length > 0) {
counts["n/a"] = this.listeners.length;
}
this.idListeners.forEach((cbs, id) => {
if (cbs.length > 0) {
counts[`${id}`] = cbs.length;
}
});
return counts;
}
}

View File

@ -0,0 +1,122 @@
import { Emitter } from "../src/events";
describe("Event", () => {
const emitter = new Emitter<number>();
it("should listen to global event", () => {
const fn = jest.fn();
const d = emitter.event(fn);
emitter.emit(10);
expect(fn).toHaveBeenCalledWith(10);
d.dispose();
});
it("should listen to id event", () => {
const fn = jest.fn();
const d = emitter.event(0, fn);
emitter.emit(0, 5);
expect(fn).toHaveBeenCalledWith(5);
d.dispose();
});
it("should listen to string id event", () => {
const fn = jest.fn();
const d = emitter.event("string", fn);
emitter.emit("string", 55);
expect(fn).toHaveBeenCalledWith(55);
d.dispose();
});
it("should not listen wrong id event", () => {
const fn = jest.fn();
const d = emitter.event(1, fn);
emitter.emit(0, 5);
emitter.emit(1, 6);
expect(fn).toHaveBeenCalledWith(6);
expect(fn).toHaveBeenCalledTimes(1);
d.dispose();
});
it("should listen to id event globally", () => {
const fn = jest.fn();
const d = emitter.event(fn);
emitter.emit(1, 11);
expect(fn).toHaveBeenCalledWith(11);
d.dispose();
});
it("should listen to global event", () => {
const fn = jest.fn();
const d = emitter.event(3, fn);
emitter.emit(14);
expect(fn).toHaveBeenCalledWith(14);
d.dispose();
});
it("should listen to id event multiple times", () => {
const fn = jest.fn();
const disposers = [
emitter.event(934, fn),
emitter.event(934, fn),
emitter.event(934, fn),
emitter.event(934, fn),
];
emitter.emit(934, 324);
expect(fn).toHaveBeenCalledTimes(4);
expect(fn).toHaveBeenCalledWith(324);
disposers.forEach((d) => d.dispose());
});
it("should dispose individually", () => {
const fn = jest.fn();
const d = emitter.event(fn);
const fn2 = jest.fn();
const d2 = emitter.event(1, fn2);
d.dispose();
emitter.emit(12);
emitter.emit(1, 12);
expect(fn).not.toBeCalled();
expect(fn2).toBeCalledTimes(2);
d2.dispose();
emitter.emit(12);
emitter.emit(1, 12);
expect(fn).not.toBeCalled();
expect(fn2).toBeCalledTimes(2);
});
it("should dispose by id", () => {
const fn = jest.fn();
emitter.event(fn);
const fn2 = jest.fn();
emitter.event(1, fn2);
emitter.dispose(1);
emitter.emit(12);
emitter.emit(1, 12);
expect(fn).toBeCalledTimes(2);
expect(fn2).not.toBeCalled();
});
it("should dispose all", () => {
const fn = jest.fn();
emitter.event(fn);
emitter.event(1, fn);
emitter.dispose();
emitter.emit(12);
emitter.emit(1, 12);
expect(fn).not.toBeCalled();
});
});

View File

@ -1,3 +1,5 @@
// tslint:disable no-any
export interface EvalHelper { }
interface ActiveEvalEmitter {
removeAllListeners(event?: string): void;
@ -106,7 +108,7 @@ interface IMenuItem {
command: ICommandAction;
alt?: ICommandAction;
// when?: ContextKeyExpr;
group?: 'navigation' | string;
group?: "navigation" | string;
order?: number;
}
@ -135,23 +137,7 @@ interface ICommandRegistry {
}
declare namespace ide {
export const client: {
run(func: (helper: ActiveEvalEmitter) => Disposer): ActiveEvalEmitter;
run<T1>(func: (helper: ActiveEvalEmitter, a1: T1) => Disposer, a1: T1): ActiveEvalEmitter;
run<T1, T2>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2) => Disposer, a1: T1, a2: T2): ActiveEvalEmitter;
run<T1, T2, T3>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2, a3: T3) => Disposer, a1: T1, a2: T2, a3: T3): ActiveEvalEmitter;
run<T1, T2, T3, T4>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2, a3: T3, a4: T4) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4): ActiveEvalEmitter;
run<T1, T2, T3, T4, T5>(func: (helper: ActiveEvalEmitter, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): ActiveEvalEmitter;
run<T1, T2, T3, T4, T5, T6>(func: (helper: ActiveEvalEmitter, 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): ActiveEvalEmitter;
evaluate<R>(func: (helper: EvalHelper) => R | Promise<R>): Promise<R>;
evaluate<R, T1>(func: (helper: EvalHelper, a1: T1) => R | Promise<R>, a1: T1): Promise<R>;
evaluate<R, T1, T2>(func: (helper: EvalHelper, a1: T1, a2: T2) => R | Promise<R>, a1: T1, a2: T2): Promise<R>;
evaluate<R, T1, T2, T3>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3) => R | Promise<R>, a1: T1, a2: T2, a3: T3): Promise<R>;
evaluate<R, T1, T2, T3, T4>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4): Promise<R>;
evaluate<R, T1, T2, T3, T4, T5>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): Promise<R>;
evaluate<R, T1, T2, T3, T4, T5, T6>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6): Promise<R>;
};
export const client: {};
export const workbench: {
readonly statusbarService: IStatusbarService;
@ -177,8 +163,8 @@ declare namespace ide {
Ignore = 0,
Info = 1,
Warning = 2,
Error = 3
}
Error = 3,
}
export enum StatusbarAlignment {
LEFT = 0,
@ -229,7 +215,7 @@ declare namespace ide {
declare global {
interface Window {
ide?: typeof ide;
addEventListener(event: "ide-ready", callback: (ide: CustomEvent & { readonly ide: typeof ide }) => void): void;
}
}

View File

@ -1,10 +1,5 @@
{
"name": "@coder/ide",
"description": "Browser-based IDE client abstraction.",
"main": "src/index.ts",
"dependencies": {},
"devDependencies": {
"@types/rimraf": "^2.0.2",
"rimraf": "^2.6.3"
}
"main": "src/index.ts"
}

View File

@ -1,195 +1,4 @@
import * as cp from "child_process";
import * as net from "net";
import * as stream from "stream";
import { CallbackEmitter, ActiveEvalReadable, ActiveEvalWritable } from "@coder/protocol";
import { Module } 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;
export = client.modules[Module.ChildProcess];

View File

@ -61,7 +61,12 @@ class WebsocketConnection implements ReadWriteConnection {
socket.addEventListener("close", (event) => {
if (this.isUp) {
this.isUp = false;
this.downEmitter.emit(undefined);
try {
this.downEmitter.emit(undefined);
} catch (error) {
// Don't let errors here prevent restarting.
logger.error(error.message);
}
}
logger.warn(
"Web socket closed",

View File

@ -1,12 +1,10 @@
/// <reference path="../../../../lib/vscode/src/typings/electron.d.ts" />
import { EventEmitter } from "events";
import * as fs from "fs";
import * as trash from "trash";
import { logger, field } from "@coder/logger";
import { IKey, Dialog as DialogBox } from "./dialog";
import { clipboard } from "./clipboard";
import { client } from "./client";
declare var __non_webpack_require__: typeof require;
// tslint:disable-next-line no-any
(global as any).getOpenUrls = (): string[] => {
@ -184,9 +182,7 @@ class Clipboard {
class Shell {
public async moveItemToTrash(path: string): Promise<void> {
await client.evaluate((helper, path) => {
return helper.modules.trash(path);
}, path);
await trash(path);
}
}

View File

@ -1,763 +1,4 @@
import { EventEmitter } from "events";
import * as fs from "fs";
import * as stream from "stream";
import { Client, IEncodingOptions, IEncodingOptionsCallback } from "@coder/protocol";
import { Module } from "@coder/protocol";
import { client } from "./client";
import { promisify } from "util";
declare var __non_webpack_require__: typeof require;
declare var _Buffer: typeof Buffer;
/**
* Implements the native fs module
* Doesn't use `implements typeof import("fs")` to remove need for __promisify__ impls
*
* TODO: For now we can't use async in the evaluate calls because they get
* transpiled to TypeScript's helpers. tslib is included but we also need to set
* _this somehow which the __awaiter helper uses.
*/
class FS {
public constructor(
private readonly client: Client,
) { }
public access = (path: fs.PathLike, mode: number | undefined | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof mode === "function") {
callback = mode;
mode = undefined;
}
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");
return util.promisify(fs.access)(path, mode);
}, path, mode).then(() => {
callback!(undefined!);
}).catch((ex) => {
callback!(ex);
});
}
// tslint:disable-next-line no-any
public appendFile = (file: fs.PathLike | number, data: any, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
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");
return util.promisify(fs.appendFile)(path, data, options);
}, file, data, options).then(() => {
callback!(undefined!);
}).catch((ex) => {
callback!(ex);
});
}
public chmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
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");
return util.promisify(fs.chmod)(path, mode);
}, path, mode).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public chown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
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");
return util.promisify(fs.chown)(path, uid, gid);
}, path, uid, gid).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public close = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
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");
return util.promisify(fs.close)(fd);
}, fd).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public copyFile = (src: fs.PathLike, dest: fs.PathLike, flags: number | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof flags === "function") {
callback = 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");
return util.promisify(fs.copyFile)(src, dest, flags);
}, src, dest, typeof flags !== "function" ? flags : undefined).then(() => {
callback!(undefined!);
}).catch((ex) => {
callback!(ex);
});
}
// tslint:disable-next-line no-any
public createWriteStream = (path: fs.PathLike, options?: any): fs.WriteStream => {
const ae = this.client.run((ae, path, options) => {
const fs = __non_webpack_require__("fs") as typeof import("fs");
const str = fs.createWriteStream(path, options);
ae.on("write", (d: string) => str.write(_Buffer.from(d, "utf8")));
ae.on("close", () => str.close());
ae.on("destroy", () => str.destroy());
str.on("close", () => ae.emit("close"));
str.on("open", (fd) => ae.emit("open", fd));
str.on("error", (err) => ae.emit(err));
return {
onDidDispose: (cb): fs.WriteStream => str.on("close", cb),
dispose: (): void => str.close(),
};
}, path, options);
return new (class WriteStream extends stream.Writable implements fs.WriteStream {
private _bytesWritten: number = 0;
public constructor() {
super({
write: (data, encoding, cb): void => {
this._bytesWritten += data.length;
ae.emit("write", Buffer.from(data, encoding), encoding);
cb();
},
});
ae.on("open", (fd: number) => this.emit("open", fd));
ae.on("close", () => this.emit("close"));
}
public get bytesWritten(): number {
return this._bytesWritten;
}
public get path(): string | Buffer {
return "";
}
public close(): void {
ae.emit("close");
}
public destroy(): void {
ae.emit("destroy");
}
}) as fs.WriteStream;
}
public exists = (path: fs.PathLike, callback: (exists: boolean) => void): void => {
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");
return util.promisify(fs.exists)(path);
}, path).then((r) => {
callback(r);
}).catch(() => {
callback(false);
});
}
public fchmod = (fd: number, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
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");
return util.promisify(fs.fchmod)(fd, mode);
}, fd, mode).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public fchown = (fd: number, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
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");
return util.promisify(fs.fchown)(fd, uid, gid);
}, fd, uid, gid).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public fdatasync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
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");
return util.promisify(fs.fdatasync)(fd);
}, fd).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public fstat = (fd: number, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
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");
return util.promisify(fs.fstat)(fd).then((stats) => {
return tslib.__assign(stats, {
_isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false,
_isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false,
_isDirectory: stats.isDirectory(),
_isFIFO: stats.isFIFO ? stats.isFIFO() : false,
_isFile: stats.isFile(),
_isSocket: stats.isSocket ? stats.isSocket() : false,
_isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false,
});
});
}, fd).then((stats) => {
callback(undefined!, new Stats(stats));
}).catch((ex) => {
callback(ex, undefined!);
});
}
public fsync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
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");
return util.promisify(fs.fsync)(fd);
}, fd).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public ftruncate = (fd: number, len: number | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof len === "function") {
callback = len;
len = undefined;
}
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");
return util.promisify(fs.ftruncate)(fd, len);
}, fd, len).then(() => {
callback!(undefined!);
}).catch((ex) => {
callback!(ex);
});
}
public futimes = (fd: number, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => {
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");
return util.promisify(fs.futimes)(fd, atime, mtime);
}, fd, atime, mtime).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public lchmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
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");
return util.promisify(fs.lchmod)(path, mode);
}, path, mode).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public lchown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
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");
return util.promisify(fs.lchown)(path, uid, gid);
}, path, uid, gid).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public link = (existingPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
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");
return util.promisify(fs.link)(existingPath, newPath);
}, existingPath, newPath).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public lstat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
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");
return util.promisify(fs.lstat)(path).then((stats) => {
return tslib.__assign(stats, {
_isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false,
_isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false,
_isDirectory: stats.isDirectory(),
_isFIFO: stats.isFIFO ? stats.isFIFO() : false,
_isFile: stats.isFile(),
_isSocket: stats.isSocket ? stats.isSocket() : false,
_isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false,
});
});
}, path).then((stats) => {
callback(undefined!, new Stats(stats));
}).catch((ex) => {
callback(ex, undefined!);
});
}
public mkdir = (path: fs.PathLike, mode: number | string | fs.MakeDirectoryOptions | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof mode === "function") {
callback = mode;
mode = undefined;
}
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");
return util.promisify(fs.mkdir)(path, mode);
}, path, mode).then(() => {
callback!(undefined!);
}).catch((ex) => {
callback!(ex);
});
}
public mkdtemp = (prefix: string, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, folder: string | Buffer) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
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");
return util.promisify(fs.mkdtemp)(prefix, options);
}, prefix, options).then((folder) => {
callback!(undefined!, folder);
}).catch((ex) => {
callback!(ex, undefined!);
});
}
public open = (path: fs.PathLike, flags: string | number, mode: string | number | undefined | null | ((err: NodeJS.ErrnoException, fd: number) => void), callback?: (err: NodeJS.ErrnoException, fd: number) => void): void => {
if (typeof mode === "function") {
callback = mode;
mode = undefined;
}
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");
return util.promisify(fs.open)(path, flags, mode);
}, path, flags, mode).then((fd) => {
callback!(undefined!, fd);
}).catch((ex) => {
callback!(ex, undefined!);
});
}
public read = <TBuffer extends Buffer | Uint8Array>(fd: number, buffer: TBuffer, offset: number, length: number, position: number | null, callback: (err: NodeJS.ErrnoException, bytesRead: number, buffer: TBuffer) => void): void => {
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);
return util.promisify(fs.read)(fd, buffer, 0, length, position).then((resp) => {
return {
bytesRead: resp.bytesRead,
content: resp.bytesRead < buffer.length ? buffer.slice(0, resp.bytesRead) : buffer,
};
});
}, fd, length, position).then((resp) => {
buffer.set(resp.content, offset);
callback(undefined!, resp.bytesRead, resp.content as TBuffer);
}).catch((ex) => {
callback(ex, undefined!, undefined!);
});
}
public readFile = (path: fs.PathLike | number, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, data: string | Buffer) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
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");
return util.promisify(fs.readFile)(path, options).then((value) => value.toString());
}, path, options).then((buffer) => {
callback!(undefined!, buffer);
}).catch((ex) => {
callback!(ex, undefined!);
});
}
public readdir = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, files: Buffer[] | fs.Dirent[] | string[]) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
// TODO: options can also take `withFileTypes` but the types aren't working.
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");
return util.promisify(fs.readdir)(path, options);
}, path, options).then((files) => {
callback!(undefined!, files);
}).catch((ex) => {
callback!(ex, undefined!);
});
}
public readlink = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, linkString: string | Buffer) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
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");
return util.promisify(fs.readlink)(path, options);
}, path, options).then((linkString) => {
callback!(undefined!, linkString);
}).catch((ex) => {
callback!(ex, undefined!);
});
}
public realpath = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, resolvedPath: string | Buffer) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
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");
return util.promisify(fs.realpath)(path, options);
}, path, options).then((resolvedPath) => {
callback!(undefined!, resolvedPath);
}).catch((ex) => {
callback!(ex, undefined!);
});
}
public rename = (oldPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
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");
return util.promisify(fs.rename)(oldPath, newPath);
}, oldPath, newPath).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public rmdir = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
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");
return util.promisify(fs.rmdir)(path);
}, path).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public stat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
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");
return util.promisify(fs.stat)(path).then((stats) => {
return tslib.__assign(stats, {
/**
* We need to check if functions exist because nexe's implemented FS
* lib doesnt implement fs.stats properly
*/
_isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false,
_isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false,
_isDirectory: stats.isDirectory(),
_isFIFO: stats.isFIFO ? stats.isFIFO() : false,
_isFile: stats.isFile(),
_isSocket: stats.isSocket ? stats.isSocket() : false,
_isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false,
});
});
}, path).then((stats) => {
callback(undefined!, new Stats(stats));
}).catch((ex) => {
callback(ex, undefined!);
});
}
public symlink = (target: fs.PathLike, path: fs.PathLike, type: fs.symlink.Type | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof type === "function") {
callback = type;
type = undefined;
}
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");
return util.promisify(fs.symlink)(target, path, type);
}, target, path, type).then(() => {
callback!(undefined!);
}).catch((ex) => {
callback!(ex);
});
}
public truncate = (path: fs.PathLike, len: number | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof len === "function") {
callback = len;
len = undefined;
}
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");
return util.promisify(fs.truncate)(path, len);
}, path, len).then(() => {
callback!(undefined!);
}).catch((ex) => {
callback!(ex);
});
}
public unlink = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
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");
return util.promisify(fs.unlink)(path);
}, path).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public utimes = (path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => {
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");
return util.promisify(fs.utimes)(path, atime, mtime);
}, path, atime, mtime).then(() => {
callback(undefined!);
}).catch((ex) => {
callback(ex);
});
}
public write = <TBuffer extends Buffer | Uint8Array>(fd: number, buffer: TBuffer, offset: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void), length: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void), position: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void), callback?: (err: NodeJS.ErrnoException, written: number, buffer: TBuffer) => void): void => {
if (typeof offset === "function") {
callback = offset;
offset = undefined;
}
if (typeof length === "function") {
callback = length;
length = undefined;
}
if (typeof position === "function") {
callback = position;
position = undefined;
}
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");
return util.promisify(fs.write)(fd, _Buffer.from(buffer, "utf8"), offset, length, position).then((resp) => {
return {
bytesWritten: resp.bytesWritten,
content: resp.buffer.toString("utf8"),
};
});
}, fd, buffer.toString(), offset, length, position).then((r) => {
callback!(undefined!, r.bytesWritten, Buffer.from(r.content, "utf8") as TBuffer);
}).catch((ex) => {
callback!(ex, undefined!, undefined!);
});
}
// tslint:disable-next-line no-any
public writeFile = (path: fs.PathLike | number, data: any, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
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");
return util.promisify(fs.writeFile)(path, data, options);
}, path, data, options).then(() => {
callback!(undefined!);
}).catch((ex) => {
callback!(ex);
});
}
public watch = (filename: fs.PathLike, options?: IEncodingOptions | ((event: string, filename: string | Buffer) => void), listener?: ((event: string, filename: string | Buffer) => void)): fs.FSWatcher => {
if (typeof options === "function") {
listener = options;
options = undefined;
}
const ae = this.client.run((ae, filename, hasListener, options) => {
const fs = __non_webpack_require__("fs") as typeof import ("fs");
// tslint:disable-next-line no-any
const watcher = fs.watch(filename, options as any, hasListener ? (event, filename): void => {
ae.emit("listener", event, filename);
} : undefined);
watcher.on("change", (event, filename) => ae.emit("change", event, filename));
watcher.on("error", (error) => ae.emit("error", error));
ae.on("close", () => watcher.close());
return {
onDidDispose: (cb): void => ae.on("close", cb),
dispose: (): void => watcher.close(),
};
}, filename.toString(), !!listener, options);
return new class Watcher extends EventEmitter implements fs.FSWatcher {
public constructor() {
super();
ae.on("change", (event: string, filename: string) => this.emit("change", event, filename));
ae.on("error", (error: Error) => this.emit("error", error));
ae.on("listener", (event: string, filename: string) => listener && listener(event, filename));
}
public close(): void {
ae.emit("close");
}
};
}
}
interface IStats {
dev: number;
ino: number;
mode: number;
nlink: number;
uid: number;
gid: number;
rdev: number;
size: number;
blksize: number;
blocks: number;
atimeMs: number;
mtimeMs: number;
ctimeMs: number;
birthtimeMs: number;
atime: Date | string;
mtime: Date | string;
ctime: Date | string;
birthtime: Date | string;
_isFile: boolean;
_isDirectory: boolean;
_isBlockDevice: boolean;
_isCharacterDevice: boolean;
_isSymbolicLink: boolean;
_isFIFO: boolean;
_isSocket: boolean;
}
class Stats implements fs.Stats {
public readonly atime: Date;
public readonly mtime: Date;
public readonly ctime: Date;
public readonly birthtime: Date;
public constructor(private readonly stats: IStats) {
this.atime = new Date(stats.atime);
this.mtime = new Date(stats.mtime);
this.ctime = new Date(stats.ctime);
this.birthtime = new Date(stats.birthtime);
}
public get dev(): number { return this.stats.dev; }
public get ino(): number { return this.stats.ino; }
public get mode(): number { return this.stats.mode; }
public get nlink(): number { return this.stats.nlink; }
public get uid(): number { return this.stats.uid; }
public get gid(): number { return this.stats.gid; }
public get rdev(): number { return this.stats.rdev; }
public get size(): number { return this.stats.size; }
public get blksize(): number { return this.stats.blksize; }
public get blocks(): number { return this.stats.blocks; }
public get atimeMs(): number { return this.stats.atimeMs; }
public get mtimeMs(): number { return this.stats.mtimeMs; }
public get ctimeMs(): number { return this.stats.ctimeMs; }
public get birthtimeMs(): number { return this.stats.birthtimeMs; }
public isFile(): boolean { return this.stats._isFile; }
public isDirectory(): boolean { return this.stats._isDirectory; }
public isBlockDevice(): boolean { return this.stats._isBlockDevice; }
public isCharacterDevice(): boolean { return this.stats._isCharacterDevice; }
public isSymbolicLink(): boolean { return this.stats._isSymbolicLink; }
public isFIFO(): boolean { return this.stats._isFIFO; }
public isSocket(): boolean { return this.stats._isSocket; }
public toObject(): object {
return JSON.parse(JSON.stringify(this));
}
}
const fillFs = new FS(client);
// 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(fillFs.exists, promisify.custom, {
value: (path: fs.PathLike): Promise<boolean> => new Promise((resolve): void => fillFs.exists(path, resolve)),
});
export = fillFs;
export = client.modules[Module.Fs];

View File

@ -1,258 +1,4 @@
import * as net from "net";
import { CallbackEmitter, ActiveEvalDuplex, ActiveEvalHelper } from "@coder/protocol";
import { Module } from "@coder/protocol";
import { client } from "./client";
declare var __non_webpack_require__: typeof require;
class Socket extends ActiveEvalDuplex implements net.Socket {
private _connecting: boolean = false;
private _destroyed: boolean = false;
public constructor(options?: net.SocketConstructorOpts, ae?: ActiveEvalHelper) {
super(ae || client.run((ae, options) => {
const net = __non_webpack_require__("net") as typeof import("net");
return ae.bindSocket(new net.Socket(options));
}, options));
this.ae.on("connect", () => {
this._connecting = false;
this.emit("connect");
});
this.ae.on("error", () => {
this._connecting = false;
this._destroyed = true;
});
this.ae.on("lookup", (error, address, family, host) => this.emit("lookup", error, address, family, host));
this.ae.on("timeout", () => this.emit("timeout"));
}
public connect(options: net.SocketConnectOpts | number | string, host?: string | Function, connectionListener?: Function): this {
// This is to get around type issues with socket.connect as well as extract
// the function wherever it might be.
switch (typeof options) {
case "string": options = { path: options }; break;
case "number": options = { port: options }; break;
}
switch (typeof host) {
case "function": connectionListener = host; break;
case "string": (options as net.TcpSocketConnectOpts).host = host; break;
}
this._connecting = true;
this.ae.emit("connect", options, this.storeCallback(connectionListener));
return this;
}
// tslint:disable-next-line no-any
public write(data: any, encoding?: string | Function, fd?: string | Function): boolean {
let callback: Function | undefined;
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
if (typeof fd === "function") {
callback = fd;
fd = undefined;
}
this.ae.emit("write", data, encoding, fd, this.storeCallback(callback));
return true; // Always true since we can't get this synchronously.
}
public get connecting(): boolean { return this._connecting; }
public get destroyed(): boolean { return this._destroyed; }
public get bufferSize(): number { throw new Error("not implemented"); }
public get bytesRead(): number { throw new Error("not implemented"); }
public get bytesWritten(): number { throw new Error("not implemented"); }
public get localAddress(): string { throw new Error("not implemented"); }
public get localPort(): number { throw new Error("not implemented"); }
public address(): net.AddressInfo | string { throw new Error("not implemented"); }
public setTimeout(timeout: number, callback?: Function): this { return this.emitReturnThis("setTimeout", timeout, this.storeCallback(callback)); }
public setNoDelay(noDelay?: boolean): this { return this.emitReturnThis("setNoDelay", noDelay); }
public setKeepAlive(enable?: boolean, initialDelay?: number): this { return this.emitReturnThis("setKeepAlive", enable, initialDelay); }
public unref(): void { this.ae.emit("unref"); }
public ref(): void { this.ae.emit("ref"); }
}
class Server extends CallbackEmitter implements net.Server {
private readonly sockets = new Map<number, Socket>();
private _listening: boolean = false;
public constructor(options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean } | ((socket: Socket) => void), connectionListener?: (socket: Socket) => void) {
super();
if (typeof options === "function") {
connectionListener = options;
options = undefined;
}
this.ae = client.run((ae, options, callbackId) => {
const net = __non_webpack_require__("net") as typeof import("net");
let connectionId = 0;
const sockets = new Map<number, net.Socket>();
const storeSocket = (socket: net.Socket): number => {
const socketId = connectionId++;
sockets.set(socketId, socket);
const socketAe = ae.createUnique(socketId);
const disposer = socketAe.bindSocket(socket);
socket.on("close", () => {
disposer.dispose();
sockets.delete(socketId);
});
return socketId;
};
const callback = ae.maybeCallback(callbackId);
let server = new net.Server(options, typeof callback !== "undefined" ? (socket): void => {
callback(storeSocket(socket));
} : undefined);
server.on("close", () => ae.emit("close"));
server.on("connection", (socket) => ae.emit("connection", storeSocket(socket)));
server.on("error", (error) => ae.emit("error", error));
server.on("listening", () => ae.emit("listening"));
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());
return {
onDidDispose: (cb): net.Server => server.on("close", cb),
dispose: (): void => {
server.removeAllListeners();
server.close();
sockets.forEach((socket) => {
socket.removeAllListeners();
socket.end();
socket.destroy();
socket.unref();
});
sockets.clear();
},
};
}, options || {}, this.storeCallback(connectionListener));
this.ae.on("close", () => {
this._listening = false;
this.emit("close");
});
this.ae.on("connection", (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) {
connectionListener(socket);
}
this.emit("connection", socket);
});
this.ae.on("error", (error) => {
this._listening = false;
this.emit("error", error);
});
this.ae.on("listening", () => {
this._listening = true;
this.emit("listening");
});
}
public listen(handle?: net.ListenOptions | number | string, hostname?: string | number | Function, backlog?: number | Function, listeningListener?: Function): this {
if (typeof handle === "undefined") {
throw new Error("no handle");
}
switch (typeof handle) {
case "number": handle = { port: handle }; break;
case "string": handle = { path: handle }; break;
}
switch (typeof hostname) {
case "function": listeningListener = hostname; break;
case "string": handle.host = hostname; break;
case "number": handle.backlog = hostname; break;
}
switch (typeof backlog) {
case "function": listeningListener = backlog; break;
case "number": handle.backlog = backlog; break;
}
if (listeningListener) {
this.ae.on("listening", () => {
listeningListener!();
});
}
this.ae.emit("listen", handle);
return this;
}
public close(callback?: Function): this {
// close() doesn't fire the close event until all connections are also
// closed, but it does prevent new connections.
this._listening = false;
this.ae.emit("close", this.storeCallback(callback));
return this;
}
public get connections(): number { return this.sockets.size; }
public get listening(): boolean { return this._listening; }
public get maxConnections(): number { throw new Error("not implemented"); }
public address(): net.AddressInfo | string { throw new Error("not implemented"); }
public ref(): this { return this.emitReturnThis("ref"); }
public unref(): this { return this.emitReturnThis("unref"); }
public getConnections(cb: (error: Error | null, count: number) => void): void { cb(null, this.sockets.size); }
// tslint:disable-next-line no-any
private emitReturnThis(event: string, ...args: any[]): this {
this.ae.emit(event, ...args);
return this;
}
}
type NodeNet = typeof net;
/**
* Implementation of net for the browser.
*/
class Net implements NodeNet {
// @ts-ignore this is because Socket is missing things from the Stream
// namespace but I'm unsure how best to provide them (finished,
// finished.__promisify__, pipeline, and some others) or if it even matters.
public readonly Socket = Socket;
public readonly Server = Server;
public createConnection(target: string | number | net.NetConnectOpts, host?: string | Function, callback?: Function): net.Socket {
const socket = new Socket();
socket.connect(target, host, callback);
return socket;
}
public createServer(
options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean } | ((socket: net.Socket) => void),
connectionListener?: (socket: net.Socket) => void,
): net.Server {
return new Server(options, connectionListener);
}
public connect(): net.Socket { throw new Error("not implemented"); }
public isIP(_input: string): number { throw new Error("not implemented"); }
public isIPv4(_input: string): boolean { throw new Error("not implemented"); }
public isIPv6(_input: string): boolean { throw new Error("not implemented"); }
}
export = new Net();
export = client.modules[Module.Net];

View File

@ -0,0 +1,4 @@
import { Module } from "@coder/protocol";
import { client } from "./client";
export = client.modules[Module.Trash].trash;

View File

@ -2,113 +2,3 @@
# yarn lockfile v1
"@types/events@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
"@types/glob@*":
version "7.1.1"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
dependencies:
"@types/events" "*"
"@types/minimatch" "*"
"@types/node" "*"
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/node@*":
version "11.9.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.9.4.tgz#ceb0048a546db453f6248f2d1d95e937a6f00a14"
integrity sha512-Zl8dGvAcEmadgs1tmSPcvwzO1YRsz38bVJQvH1RvRqSR9/5n61Q1ktcDL0ht3FXWR+ZpVmXVwN1LuH4Ax23NsA==
"@types/rimraf@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.2.tgz#7f0fc3cf0ff0ad2a99bb723ae1764f30acaf8b6e"
integrity sha512-Hm/bnWq0TCy7jmjeN5bKYij9vw5GrDFWME4IuxV08278NtU/VdGbzsBohcCUJ7+QMqmUq5hpRKB39HeQWJjztQ==
dependencies:
"@types/glob" "*"
"@types/node" "*"
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
brace-expansion@^1.1.7:
version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==
dependencies:
balanced-match "^1.0.0"
concat-map "0.0.1"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8=
glob@^7.1.3:
version "7.1.3"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1"
integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
inflight@^1.0.4:
version "1.0.6"
resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9"
integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=
dependencies:
once "^1.3.0"
wrappy "1"
inherits@2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
minimatch@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==
dependencies:
brace-expansion "^1.1.7"
once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
dependencies:
wrappy "1"
path-is-absolute@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
rimraf@^2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
dependencies:
glob "^7.1.3"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=

View File

@ -33,7 +33,8 @@
"@coder/(.*)/test": "<rootDir>/$1/test",
"@coder/(.*)": "<rootDir>/$1/src",
"vs/(.*)": "<rootDir>/../lib/vscode/src/vs/$1",
"vszip": "<rootDir>/../lib/vscode/src/vs/base/node/zip.ts"
"vszip": "<rootDir>/../lib/vscode/src/vs/base/node/zip.ts",
"^node-pty": "node-pty-prebuilt"
},
"transform": {
"^.+\\.tsx?$": "ts-jest"
@ -44,4 +45,4 @@
],
"testRegex": ".*\\.test\\.tsx?"
}
}
}

View File

@ -7,12 +7,13 @@
"node-pty-prebuilt": "^0.7.6",
"spdlog": "^0.7.2",
"trash": "^4.3.0",
"tslib": "^1.9.3",
"ws": "^6.1.2"
},
"devDependencies": {
"@types/google-protobuf": "^3.2.7",
"@types/rimraf": "^2.0.2",
"@types/text-encoding": "^0.0.35",
"rimraf": "^2.6.3",
"text-encoding": "^0.7.0",
"ts-protoc-gen": "^0.8.0"
}

View File

@ -1,19 +1,32 @@
import { EventEmitter } from "events";
import { PathLike } from "fs";
import { ExecException, ExecOptions } from "child_process";
import { promisify } from "util";
import { Emitter } from "@coder/events";
import { logger, field } from "@coder/logger";
import { Ping, NewEvalMessage, ServerMessage, EvalDoneMessage, EvalFailedMessage, ClientMessage, WorkingInitMessage, EvalEventMessage } from "../proto";
import { ReadWriteConnection, InitData, OperatingSystem, SharedProcessData } from "../common/connection";
import { ActiveEvalHelper, EvalHelper, Disposer, ServerActiveEvalHelper } from "../common/helpers";
import { stringify, parse } from "../common/util";
import { ReadWriteConnection, InitData, SharedProcessData } from "../common/connection";
import { Module, ServerProxy } from "../common/proxy";
import { stringify, parse, moduleToProto, protoToModule, protoToOperatingSystem } from "../common/util";
import { Ping, ServerMessage, ClientMessage, MethodMessage, NamedProxyMessage, NumberedProxyMessage, SuccessMessage, FailMessage, EventMessage, CallbackMessage } from "../proto";
import { FsModule, ChildProcessModule, NetModule, NodePtyModule, SpdlogModule, TrashModule } from "./modules";
// tslint:disable no-any
interface ProxyData {
promise: Promise<void>;
instance: any;
callbacks: Map<number, (...args: any[]) => void>;
}
/**
* Client accepts an arbitrary connection intended to communicate with the Server.
* Client accepts a connection to communicate with the server.
*/
export class Client {
private evalId = 0;
private readonly evalDoneEmitter = new Emitter<EvalDoneMessage>();
private readonly evalFailedEmitter = new Emitter<EvalFailedMessage>();
private readonly evalEventEmitter = new Emitter<EvalEventMessage>();
private messageId = 0;
private callbackId = 0;
private readonly proxies = new Map<number | Module, ProxyData>();
private readonly successEmitter = new Emitter<SuccessMessage>();
private readonly failEmitter = new Emitter<FailMessage>();
private readonly eventEmitter = new Emitter<{ event: string; args: any[]; }>();
private _initData: InitData | undefined;
private readonly initDataEmitter = new Emitter<InitData>();
@ -22,37 +35,123 @@ export class Client {
private readonly sharedProcessActiveEmitter = new Emitter<SharedProcessData>();
public readonly onSharedProcessActive = this.sharedProcessActiveEmitter.event;
private disconnected: boolean = false;
// The socket timeout is 60s, so we need to send a ping periodically to
// prevent it from closing.
private pingTimeout: NodeJS.Timer | number | undefined;
private readonly pingTimeoutDelay = 30000;
private readonly responseTimeout = 10000;
public readonly modules: {
[Module.ChildProcess]: ChildProcessModule,
[Module.Fs]: FsModule,
[Module.Net]: NetModule,
[Module.NodePty]: NodePtyModule,
[Module.Spdlog]: SpdlogModule,
[Module.Trash]: TrashModule,
};
/**
* @param connection Established connection to the server
*/
public constructor(
private readonly connection: ReadWriteConnection,
) {
connection.onMessage((data) => {
public constructor(private readonly connection: ReadWriteConnection) {
connection.onMessage(async (data) => {
let message: ServerMessage | undefined;
try {
message = ServerMessage.deserializeBinary(data);
this.handleMessage(message);
await this.handleMessage(message);
} catch (error) {
logger.error(
"Failed to handle server message",
field("id", message && message.hasEvalEvent() ? message.getEvalEvent()!.getId() : undefined),
field("id", message && this.getMessageId(message)),
field("length", data.byteLength),
field("error", error.message),
);
}
});
connection.onClose(() => {
clearTimeout(this.pingTimeout as any); // tslint:disable-line no-any
this.pingTimeout = undefined;
this.createProxy(Module.ChildProcess);
this.createProxy(Module.Fs);
this.createProxy(Module.Net);
this.createProxy(Module.NodePty);
this.createProxy(Module.Spdlog);
this.createProxy(Module.Trash);
this.modules = {
[Module.ChildProcess]: new ChildProcessModule(this.getProxy(Module.ChildProcess).instance),
[Module.Fs]: new FsModule(this.getProxy(Module.Fs).instance),
[Module.Net]: new NetModule(this.getProxy(Module.Net).instance),
[Module.NodePty]: new NodePtyModule(this.getProxy(Module.NodePty).instance),
[Module.Spdlog]: new SpdlogModule(this.getProxy(Module.Spdlog).instance),
[Module.Trash]: new TrashModule(this.getProxy(Module.Trash).instance),
};
// 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(this.modules[Module.Fs].exists, promisify.custom, {
value: (path: PathLike): Promise<boolean> => {
return new Promise((resolve): void => this.modules[Module.Fs].exists(path, resolve));
},
});
Object.defineProperty(this.modules[Module.ChildProcess].exec, promisify.custom, {
value: (
command: string,
options?: { encoding?: string | null } & ExecOptions | null,
): Promise<{ stdout: string | Buffer, stderr: string | Buffer }> => {
return new Promise((resolve, reject): void => {
this.modules[Module.ChildProcess].exec(command, options, (error: ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => {
if (error) {
reject(error);
} else {
resolve({ stdout, stderr });
}
});
});
},
});
/**
* If the connection is interrupted, the calls will neither succeed nor fail
* nor exit so we need to send a failure on all of them as well as trigger
* events so things like child processes can clean up and possibly restart.
*/
const handleDisconnect = (): void => {
this.disconnected = true;
logger.trace(() => [
"disconnected from server",
field("proxies", this.proxies.size),
field("callbacks", Array.from(this.proxies.values()).reduce((count, p) => count + p.callbacks.size, 0)),
field("success listeners", this.successEmitter.counts),
field("fail listeners", this.failEmitter.counts),
field("event listeners", this.eventEmitter.counts),
]);
const message = new FailMessage();
const error = new Error("disconnected");
message.setResponse(stringify(error));
this.failEmitter.emit(message);
this.eventEmitter.emit({ event: "exit", args: [1] });
this.eventEmitter.emit({ event: "close", args: [] });
try {
this.eventEmitter.emit({ event: "error", args: [error] });
} catch (error) {
// If nothing is listening, EventEmitter will throw an error.
}
this.eventEmitter.emit({ event: "done", args: [true] });
};
connection.onDown(() => handleDisconnect());
connection.onClose(() => {
clearTimeout(this.pingTimeout as any);
this.pingTimeout = undefined;
handleDisconnect();
});
connection.onUp(() => this.disconnected = false);
this.initDataPromise = new Promise((resolve): void => {
this.initDataEmitter.event(resolve);
});
@ -60,6 +159,9 @@ export class Client {
this.startPinging();
}
/**
* Close the connection.
*/
public dispose(): void {
this.connection.close();
}
@ -68,158 +170,109 @@ export class Client {
return this.initDataPromise;
}
public run(func: (helper: ServerActiveEvalHelper) => Disposer): ActiveEvalHelper;
public run<T1>(func: (helper: ServerActiveEvalHelper, a1: T1) => Disposer, a1: T1): ActiveEvalHelper;
public run<T1, T2>(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2) => Disposer, a1: T1, a2: T2): ActiveEvalHelper;
public run<T1, T2, T3>(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2, a3: T3) => Disposer, a1: T1, a2: T2, a3: T3): ActiveEvalHelper;
public run<T1, T2, T3, T4>(func: (helper: ServerActiveEvalHelper, a1: T1, a2: T2, a3: T3, a4: T4) => Disposer, a1: T1, a2: T2, a3: T3, a4: T4): ActiveEvalHelper;
public run<T1, T2, T3, T4, T5>(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<T1, T2, T3, T4, T5, T6>(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
* function should return a disposer for cleaning up when the client
* disconnects and for notifying when disposal has happened outside manual
* activation.
* Make a remote call for a proxy's method using proto.
*/
public run<T1, T2, T3, T4, T5, T6>(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);
private remoteCall(proxyId: number | Module, method: string, args: any[]): Promise<any> {
if (this.disconnected) {
return Promise.reject(new Error("disconnected"));
}
// This takes server events and emits them to the client's emitter.
const eventEmitter = new EventEmitter();
const d1 = this.evalEventEmitter.event((msg) => {
if (msg.getId() === doEval.id) {
eventEmitter.emit(msg.getEvent(), ...msg.getArgsList().map(parse));
}
});
const message = new MethodMessage();
const id = this.messageId++;
let proxyMessage: NamedProxyMessage | NumberedProxyMessage;
if (typeof proxyId === "string") {
proxyMessage = new NamedProxyMessage();
proxyMessage.setModule(moduleToProto(proxyId));
message.setNamedProxy(proxyMessage);
} else {
proxyMessage = new NumberedProxyMessage();
proxyMessage.setProxyId(proxyId);
message.setNumberedProxy(proxyMessage);
}
proxyMessage.setId(id);
proxyMessage.setMethod(method);
doEval.completed.then(() => {
d1.dispose();
}).catch((ex) => {
d1.dispose();
// This error event is only received by the client.
eventEmitter.emit("error", ex);
});
const storeCallback = (cb: (...args: any[]) => void): number => {
const callbackId = this.callbackId++;
logger.trace(() => [
"storing callback",
field("proxyId", proxyId),
field("callbackId", callbackId),
]);
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 => {
const eventsMsg = new EvalEventMessage();
eventsMsg.setId(doEval.id);
eventsMsg.setEvent(event);
eventsMsg.setArgsList(args.map((a) => stringify(a)));
const clientMsg = new ClientMessage();
clientMsg.setEvalEvent(eventsMsg);
this.connection.send(clientMsg.serializeBinary());
},
removeAllListeners: (event: string): EventEmitter => eventEmitter.removeAllListeners(event),
// tslint:enable no-any
});
}
this.getProxy(proxyId).callbacks.set(callbackId, cb);
public evaluate<R>(func: (helper: EvalHelper) => R | Promise<R>): Promise<R>;
public evaluate<R, T1>(func: (helper: EvalHelper, a1: T1) => R | Promise<R>, a1: T1): Promise<R>;
public evaluate<R, T1, T2>(func: (helper: EvalHelper, a1: T1, a2: T2) => R | Promise<R>, a1: T1, a2: T2): Promise<R>;
public evaluate<R, T1, T2, T3>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3) => R | Promise<R>, a1: T1, a2: T2, a3: T3): Promise<R>;
public evaluate<R, T1, T2, T3, T4>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4): Promise<R>;
public evaluate<R, T1, T2, T3, T4, T5>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5): Promise<R>;
public evaluate<R, T1, T2, T3, T4, T5, T6>(func: (helper: EvalHelper, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6) => R | Promise<R>, a1: T1, a2: T2, a3: T3, a4: T4, a5: T5, a6: T6): Promise<R>;
/**
* 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((helper, value) => {
* return value;
* }, "hi");
* console.log(returned);
* // output: "hi"
* @param func Function to evaluate
* @returns Promise rejected or resolved from the evaluated function
*/
public evaluate<R, T1, T2, T3, T4, T5, T6>(func: (helper: EvalHelper, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6) => R | Promise<R>, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6): Promise<R> {
return this.doEvaluate(func, a1, a2, a3, a4, a5, a6, false).completed;
}
return callbackId;
};
// tslint:disable-next-line no-any
private doEvaluate<R, T1, T2, T3, T4, T5, T6>(func: (...args: any[]) => void | Promise<void> | R | Promise<R>, a1?: T1, a2?: T2, a3?: T3, a4?: T4, a5?: T5, a6?: T6, active: boolean = false): {
readonly completed: Promise<R>;
readonly id: number;
} {
const newEval = new NewEvalMessage();
const id = this.evalId++;
newEval.setId(id);
newEval.setActive(active);
newEval.setArgsList([a1, a2, a3, a4, a5, a6].map((a) => stringify(a)));
newEval.setFunction(func.toString());
const stringifiedArgs = args.map((a) => stringify(a, storeCallback));
logger.trace(() => [
"sending",
field("id", id),
field("proxyId", proxyId),
field("method", method),
field("args", stringifiedArgs),
]);
const clientMsg = new ClientMessage();
clientMsg.setNewEval(newEval);
this.connection.send(clientMsg.serializeBinary());
proxyMessage.setArgsList(stringifiedArgs);
const completed = new Promise<R>((resolve, reject): void => {
const clientMessage = new ClientMessage();
clientMessage.setMethod(message);
this.connection.send(clientMessage.serializeBinary());
// The server will send back a fail or success message when the method
// has completed, so we listen for that based on the message's unique ID.
const promise = new Promise((resolve, reject): void => {
const dispose = (): void => {
d1.dispose();
d2.dispose();
clearTimeout(timeout as any);
};
const d1 = this.evalDoneEmitter.event((doneMsg) => {
if (doneMsg.getId() === id) {
dispose();
resolve(parse(doneMsg.getResponse()));
}
const timeout = setTimeout(() => {
dispose();
reject(new Error("timed out"));
}, this.responseTimeout);
const d1 = this.successEmitter.event(id, (message) => {
dispose();
resolve(this.parse(message.getResponse()));
});
const d2 = this.evalFailedEmitter.event((failedMsg) => {
if (failedMsg.getId() === id) {
dispose();
reject(parse(failedMsg.getResponse()));
}
const d2 = this.failEmitter.event(id, (message) => {
dispose();
reject(parse(message.getResponse()));
});
});
return { completed, id };
return promise;
}
/**
* Handles a message from the server. All incoming server messages should be
* routed through here.
* Handle all messages from the server.
*/
private handleMessage(message: ServerMessage): void {
private async handleMessage(message: ServerMessage): Promise<void> {
if (message.hasInit()) {
const init = message.getInit()!;
let opSys: OperatingSystem;
switch (init.getOperatingSystem()) {
case WorkingInitMessage.OperatingSystem.WINDOWS:
opSys = OperatingSystem.Windows;
break;
case WorkingInitMessage.OperatingSystem.LINUX:
opSys = OperatingSystem.Linux;
break;
case WorkingInitMessage.OperatingSystem.MAC:
opSys = OperatingSystem.Mac;
break;
default:
throw new Error(`unsupported operating system ${init.getOperatingSystem()}`);
}
this._initData = {
dataDirectory: init.getDataDirectory(),
homeDirectory: init.getHomeDirectory(),
tmpDirectory: init.getTmpDirectory(),
workingDirectory: init.getWorkingDirectory(),
os: opSys,
os: protoToOperatingSystem(init.getOperatingSystem()),
shell: init.getShell(),
builtInExtensionsDirectory: init.getBuiltinExtensionsDir(),
};
this.initDataEmitter.emit(this._initData);
} else if (message.hasEvalDone()) {
this.evalDoneEmitter.emit(message.getEvalDone()!);
} else if (message.hasEvalFailed()) {
this.evalFailedEmitter.emit(message.getEvalFailed()!);
} else if (message.hasEvalEvent()) {
this.evalEventEmitter.emit(message.getEvalEvent()!);
} else if (message.hasSuccess()) {
this.emitSuccess(message.getSuccess()!);
} else if (message.hasFail()) {
this.emitFail(message.getFail()!);
} else if (message.hasEvent()) {
await this.emitEvent(message.getEvent()!);
} else if (message.hasCallback()) {
await this.runCallback(message.getCallback()!);
} else if (message.hasSharedProcessActive()) {
const sharedProcessActiveMessage = message.getSharedProcessActive()!;
this.sharedProcessActiveEmitter.emit({
@ -227,13 +280,85 @@ export class Client {
logPath: sharedProcessActiveMessage.getLogPath(),
});
} else if (message.hasPong()) {
// Nothing to do since we run the pings on a timer, in case either message
// is dropped which would break the ping cycle.
// Nothing to do since pings are on a timer rather than waiting for the
// next pong in case a message from either the client or server is dropped
// which would break the ping cycle.
} else {
throw new Error("unknown message type");
}
}
private emitSuccess(message: SuccessMessage): void {
logger.trace(() => [
"received resolve",
field("id", message.getId()),
]);
this.successEmitter.emit(message.getId(), message);
}
private emitFail(message: FailMessage): void {
logger.trace(() => [
"received reject",
field("id", message.getId()),
]);
this.failEmitter.emit(message.getId(), message);
}
/**
* Emit an event received from the server. We could send requests for "on" to
* the server and serialize functions using IDs, but doing it that way makes
* it possible to miss events depending on whether the server receives the
* request before it emits. Instead, emit all events from the server so all
* events are always caught on the client.
*/
private async emitEvent(message: EventMessage): Promise<void> {
const eventMessage = message.getNamedEvent()! || message.getNumberedEvent()!;
const proxyId = message.getNamedEvent()
? protoToModule(message.getNamedEvent()!.getModule())
: message.getNumberedEvent()!.getProxyId();
const event = eventMessage.getEvent();
await this.ensureResolved(proxyId);
logger.trace(() => [
"received event",
field("proxyId", proxyId),
field("event", event),
field("args", eventMessage.getArgsList()),
]);
const args = eventMessage.getArgsList().map((a) => this.parse(a));
this.eventEmitter.emit(proxyId, { event, args });
}
/**
* Run a callback as requested by the server. Since we don't know when
* callbacks get garbage collected we dispose them only when the proxy
* disposes. That means they should only be used if they run for the lifetime
* of the proxy (like child_process.exec), otherwise we'll leak. They should
* also only be used when passed together with the method. If they are sent
* afterward, they may never be called due to timing issues.
*/
private async runCallback(message: CallbackMessage): Promise<void> {
const callbackMessage = message.getNamedCallback()! || message.getNumberedCallback()!;
const proxyId = message.getNamedCallback()
? protoToModule(message.getNamedCallback()!.getModule())
: message.getNumberedCallback()!.getProxyId();
const callbackId = callbackMessage.getCallbackId();
await this.ensureResolved(proxyId);
logger.trace(() => [
"running callback",
field("proxyId", proxyId),
field("callbackId", callbackId),
field("args", callbackMessage.getArgsList()),
]);
const args = callbackMessage.getArgsList().map((a) => this.parse(a));
this.getProxy(proxyId).callbacks.get(callbackId)!(...args);
}
/**
* Start the ping loop. Does nothing if already pinging.
*/
private startPinging = (): void => {
if (typeof this.pingTimeout !== "undefined") {
return;
@ -250,4 +375,136 @@ export class Client {
schedulePing();
}
/**
* Return the message's ID if it has one or a string identifier. For logging
* errors with an ID to make the error more useful.
*/
private getMessageId(message: ServerMessage): number | string | undefined {
if (message.hasInit()) {
return "init";
} else if (message.hasSuccess()) {
return message.getSuccess()!.getId();
} else if (message.hasFail()) {
return message.getFail()!.getId();
} else if (message.hasEvent()) {
const eventMessage = message.getEvent()!.getNamedEvent()!
|| message.getEvent()!.getNumberedEvent()!;
return `event: ${eventMessage.getEvent()}`;
} else if (message.hasCallback()) {
const callbackMessage = message.getCallback()!.getNamedCallback()!
|| message.getCallback()!.getNumberedCallback()!;
return `callback: ${callbackMessage.getCallbackId()}`;
} else if (message.hasSharedProcessActive()) {
return "shared";
} else if (message.hasPong()) {
return "pong";
}
}
/**
* Return a proxy that makes remote calls.
*/
private createProxy<T>(proxyId: number | Module, promise: Promise<any> = Promise.resolve()): T {
logger.trace(() => [
"creating proxy",
field("proxyId", proxyId),
]);
const instance = new Proxy({
proxyId,
onDone: (cb: (...args: any[]) => void): void => {
this.eventEmitter.event(proxyId, (event) => {
if (event.event === "done") {
cb(...event.args);
}
});
},
onEvent: (cb: (event: string, ...args: any[]) => void): void => {
this.eventEmitter.event(proxyId, (event) => {
cb(event.event, ...event.args);
});
},
}, {
get: (target: any, name: string): any => {
// When resolving a promise with a proxy, it will check for "then".
if (name === "then") {
return;
}
if (typeof target[name] === "undefined") {
target[name] = (...args: any[]): Promise<any> | ServerProxy => {
return this.remoteCall(proxyId, name, args);
};
}
return target[name];
},
});
this.proxies.set(proxyId, {
promise,
instance,
callbacks: new Map(),
});
instance.onDone((disconnected: boolean) => {
const log = (): void => {
logger.trace(() => [
typeof proxyId === "number" ? "disposed proxy" : "disposed proxy callbacks",
field("proxyId", proxyId),
field("disconnected", disconnected),
field("callbacks", Array.from(this.proxies.values()).reduce((count, proxy) => count + proxy.callbacks.size, 0)),
field("success listeners", this.successEmitter.counts),
field("fail listeners", this.failEmitter.counts),
field("event listeners", this.eventEmitter.counts),
]);
};
// Uniquely identified items (top-level module proxies) can continue to
// be used so we don't need to delete them.
if (typeof proxyId === "number") {
const dispose = (): void => {
this.proxies.delete(proxyId);
this.eventEmitter.dispose(proxyId);
log();
};
if (!disconnected) {
instance.dispose().then(dispose).catch(dispose);
} else {
dispose();
}
} else {
// The callbacks will still be unusable though.
this.getProxy(proxyId).callbacks.clear();
log();
}
});
return instance;
}
/**
* We aren't guaranteed the promise will call all the `then` callbacks
* synchronously once it resolves, so the event message can come in and fire
* before a caller has been able to attach an event. Waiting for the promise
* ensures it runs after everything else.
*/
private async ensureResolved(proxyId: number | Module): Promise<void> {
await this.getProxy(proxyId).promise;
}
private parse(value?: string, promise?: Promise<any>): any {
return parse(value, undefined, (id) => this.createProxy(id, promise));
}
private getProxy(proxyId: number | Module): ProxyData {
if (!this.proxies.has(proxyId)) {
throw new Error(`proxy ${proxyId} disposed too early`);
}
return this.proxies.get(proxyId)!;
}
}

View File

@ -0,0 +1,126 @@
import * as cp from "child_process";
import * as net from "net";
import * as stream from "stream";
import { callbackify } from "util";
import { ClientProxy } from "../../common/proxy";
import { ChildProcessModuleProxy, ChildProcessProxy, ChildProcessProxies } from "../../node/modules/child_process";
import { Readable, Writable } from "./stream";
export class ChildProcess extends ClientProxy<ChildProcessProxy> implements cp.ChildProcess {
public readonly stdin: stream.Writable;
public readonly stdout: stream.Readable;
public readonly stderr: stream.Readable;
public readonly stdio: [stream.Writable, stream.Readable, stream.Readable];
private _connected: boolean = false;
private _killed: boolean = false;
private _pid = -1;
public constructor(proxyPromises: Promise<ChildProcessProxies>) {
super(proxyPromises.then((p) => p.childProcess));
this.stdin = new Writable(proxyPromises.then((p) => p.stdin!));
this.stdout = new Readable(proxyPromises.then((p) => p.stdout!));
this.stderr = new Readable(proxyPromises.then((p) => p.stderr!));
this.stdio = [this.stdin, this.stdout, this.stderr];
this.proxy.getPid().then((pid) => {
this._pid = pid;
this._connected = true;
});
this.on("disconnect", () => this._connected = false);
this.on("exit", () => {
this._connected = false;
this._killed = true;
});
}
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._killed = true;
this.proxy.kill();
}
public disconnect(): void {
this.proxy.disconnect();
}
public ref(): void {
this.proxy.ref();
}
public unref(): void {
this.proxy.unref();
}
public send(
message: any, // tslint:disable-line no-any
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");
}
callbackify(this.proxy.send)(message, (error) => {
if (callback) {
callback(error);
}
});
return true; // Always true since we can't get this synchronously.
}
}
export class ChildProcessModule {
public constructor(private readonly proxy: ChildProcessModuleProxy) {}
public exec = (
command: string,
options?: { encoding?: string | null } & cp.ExecOptions | null
| ((error: cp.ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => void),
callback?: ((error: cp.ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => void),
): cp.ChildProcess => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
return new ChildProcess(this.proxy.exec(command, options, callback));
}
public fork = (modulePath: string, args?: string[] | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => {
if (!Array.isArray(args)) {
options = args;
args = undefined;
}
return new ChildProcess(this.proxy.fork(modulePath, args, options));
}
public spawn = (command: string, args?: string[] | cp.SpawnOptions, options?: cp.SpawnOptions): cp.ChildProcess => {
if (!Array.isArray(args)) {
options = args;
args = undefined;
}
return new ChildProcess(this.proxy.spawn(command, args, options));
}
}

View File

@ -0,0 +1,316 @@
import * as fs from "fs";
import { callbackify } from "util";
import { ClientProxy } from "../../common/proxy";
import { IEncodingOptions, IEncodingOptionsCallback } from "../../common/util";
import { FsModuleProxy, Stats as IStats, WatcherProxy, WriteStreamProxy } from "../../node/modules/fs";
import { Writable } from "./stream";
// tslint:disable no-any
class Watcher extends ClientProxy<WatcherProxy> implements fs.FSWatcher {
public close(): void {
this.proxy.close();
}
}
class WriteStream extends Writable<WriteStreamProxy> implements fs.WriteStream {
public get bytesWritten(): number {
throw new Error("not implemented");
}
public get path(): string | Buffer {
throw new Error("not implemented");
}
public close(): void {
this.proxy.close();
}
}
export class FsModule {
public constructor(private readonly proxy: FsModuleProxy) {}
public access = (path: fs.PathLike, mode: number | undefined | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof mode === "function") {
callback = mode;
mode = undefined;
}
callbackify(this.proxy.access)(path, mode, callback!);
}
public appendFile = (path: fs.PathLike | number, data: any, options?: fs.WriteFileOptions | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
callbackify(this.proxy.appendFile)(path, data, options, callback!);
}
public chmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.chmod)(path, mode, callback!);
}
public chown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.chown)(path, uid, gid, callback!);
}
public close = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.close)(fd, callback!);
}
public copyFile = (src: fs.PathLike, dest: fs.PathLike, flags: number | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof flags === "function") {
callback = flags;
}
callbackify(this.proxy.copyFile)(
src, dest, typeof flags !== "function" ? flags : undefined, callback!,
);
}
public createWriteStream = (path: fs.PathLike, options?: any): fs.WriteStream => {
return new WriteStream(this.proxy.createWriteStream(path, options));
}
public exists = (path: fs.PathLike, callback: (exists: boolean) => void): void => {
callbackify(this.proxy.exists)(path, (exists) => {
callback!(exists as any);
});
}
public fchmod = (fd: number, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.fchmod)(fd, mode, callback!);
}
public fchown = (fd: number, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.fchown)(fd, uid, gid, callback!);
}
public fdatasync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.fdatasync)(fd, callback!);
}
public fstat = (fd: number, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
callbackify(this.proxy.fstat)(fd, (error, stats) => {
callback(error, stats && new Stats(stats));
});
}
public fsync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.fsync)(fd, callback!);
}
public ftruncate = (fd: number, len: number | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof len === "function") {
callback = len;
len = undefined;
}
callbackify(this.proxy.ftruncate)(fd, len, callback!);
}
public futimes = (fd: number, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.futimes)(fd, atime, mtime, callback!);
}
public lchmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.lchmod)(path, mode, callback!);
}
public lchown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.lchown)(path, uid, gid, callback!);
}
public link = (existingPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.link)(existingPath, newPath, callback!);
}
public lstat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
callbackify(this.proxy.lstat)(path, (error, stats) => {
callback(error, stats && new Stats(stats));
});
}
public mkdir = (path: fs.PathLike, mode: number | string | fs.MakeDirectoryOptions | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof mode === "function") {
callback = mode;
mode = undefined;
}
callbackify(this.proxy.mkdir)(path, mode, callback!);
}
public mkdtemp = (prefix: string, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, folder: string | Buffer) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
callbackify(this.proxy.mkdtemp)(prefix, options, callback!);
}
public open = (path: fs.PathLike, flags: string | number, mode: string | number | undefined | null | ((err: NodeJS.ErrnoException, fd: number) => void), callback?: (err: NodeJS.ErrnoException, fd: number) => void): void => {
if (typeof mode === "function") {
callback = mode;
mode = undefined;
}
callbackify(this.proxy.open)(path, flags, mode, callback!);
}
public read = (fd: number, buffer: Buffer, offset: number, length: number, position: number | null, callback: (err: NodeJS.ErrnoException, bytesRead: number, buffer: Buffer) => void): void => {
this.proxy.read(fd, length, position).then((response) => {
buffer.set(response.buffer, offset);
callback(undefined!, response.bytesRead, response.buffer);
}).catch((error) => {
callback(error, undefined!, undefined!);
});
}
public readFile = (path: fs.PathLike | number, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, data: string | Buffer) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
callbackify(this.proxy.readFile)(path, options, callback!);
}
public readdir = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, files: Buffer[] | fs.Dirent[] | string[]) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
callbackify(this.proxy.readdir)(path, options, callback!);
}
public readlink = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, linkString: string | Buffer) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
callbackify(this.proxy.readlink)(path, options, callback!);
}
public realpath = (path: fs.PathLike, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException, resolvedPath: string | Buffer) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
callbackify(this.proxy.realpath)(path, options, callback!);
}
public rename = (oldPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.rename)(oldPath, newPath, callback!);
}
public rmdir = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.rmdir)(path, callback!);
}
public stat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
callbackify(this.proxy.stat)(path, (error, stats) => {
callback(error, stats && new Stats(stats));
});
}
public symlink = (target: fs.PathLike, path: fs.PathLike, type: fs.symlink.Type | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof type === "function") {
callback = type;
type = undefined;
}
callbackify(this.proxy.symlink)(target, path, type, callback!);
}
public truncate = (path: fs.PathLike, len: number | undefined | null | ((err: NodeJS.ErrnoException) => void), callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof len === "function") {
callback = len;
len = undefined;
}
callbackify(this.proxy.truncate)(path, len, callback!);
}
public unlink = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.unlink)(path, callback!);
}
public utimes = (path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => {
callbackify(this.proxy.utimes)(path, atime, mtime, callback!);
}
public write = (fd: number, buffer: Buffer, offset: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: Buffer) => void), length: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: Buffer) => void), position: number | undefined | ((err: NodeJS.ErrnoException, written: number, buffer: Buffer) => void), callback?: (err: NodeJS.ErrnoException, written: number, buffer: Buffer) => void): void => {
if (typeof offset === "function") {
callback = offset;
offset = undefined;
}
if (typeof length === "function") {
callback = length;
length = undefined;
}
if (typeof position === "function") {
callback = position;
position = undefined;
}
this.proxy.write(fd, buffer, offset, length, position).then((r) => {
callback!(undefined!, r.bytesWritten, r.buffer);
}).catch((error) => {
callback!(error, undefined!, undefined!);
});
}
public writeFile = (path: fs.PathLike | number, data: any, options: IEncodingOptionsCallback, callback?: (err: NodeJS.ErrnoException) => void): void => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
callbackify(this.proxy.writeFile)(path, data, options, callback!);
}
public watch = (filename: fs.PathLike, options?: IEncodingOptions | ((event: string, filename: string | Buffer) => void), listener?: ((event: string, filename: string | Buffer) => void)): fs.FSWatcher => {
if (typeof options === "function") {
listener = options;
options = undefined;
}
const watcher = new Watcher(this.proxy.watch(filename, options));
if (listener) {
watcher.on("change", listener);
}
return watcher;
}
}
class Stats implements fs.Stats {
public readonly atime: Date;
public readonly mtime: Date;
public readonly ctime: Date;
public readonly birthtime: Date;
public constructor(private readonly stats: IStats) {
this.atime = new Date(stats.atime);
this.mtime = new Date(stats.mtime);
this.ctime = new Date(stats.ctime);
this.birthtime = new Date(stats.birthtime);
}
public get dev(): number { return this.stats.dev; }
public get ino(): number { return this.stats.ino; }
public get mode(): number { return this.stats.mode; }
public get nlink(): number { return this.stats.nlink; }
public get uid(): number { return this.stats.uid; }
public get gid(): number { return this.stats.gid; }
public get rdev(): number { return this.stats.rdev; }
public get size(): number { return this.stats.size; }
public get blksize(): number { return this.stats.blksize; }
public get blocks(): number { return this.stats.blocks; }
public get atimeMs(): number { return this.stats.atimeMs; }
public get mtimeMs(): number { return this.stats.mtimeMs; }
public get ctimeMs(): number { return this.stats.ctimeMs; }
public get birthtimeMs(): number { return this.stats.birthtimeMs; }
public isFile(): boolean { return this.stats._isFile; }
public isDirectory(): boolean { return this.stats._isDirectory; }
public isBlockDevice(): boolean { return this.stats._isBlockDevice; }
public isCharacterDevice(): boolean { return this.stats._isCharacterDevice; }
public isSymbolicLink(): boolean { return this.stats._isSymbolicLink; }
public isFIFO(): boolean { return this.stats._isFIFO; }
public isSocket(): boolean { return this.stats._isSocket; }
public toObject(): object {
return JSON.parse(JSON.stringify(this));
}
}

View File

@ -0,0 +1,6 @@
export * from "./child_process";
export * from "./fs";
export * from "./net";
export * from "./node-pty";
export * from "./spdlog";
export * from "./trash";

View File

@ -0,0 +1,280 @@
import * as net from "net";
import { callbackify } from "util";
import { ClientProxy } from "../../common/proxy";
import { NetModuleProxy, NetServerProxy, NetSocketProxy } from "../../node/modules/net";
import { Duplex } from "./stream";
export class Socket extends Duplex<NetSocketProxy> implements net.Socket {
private _connecting: boolean = false;
private _destroyed: boolean = false;
public constructor(proxyPromise: Promise<NetSocketProxy> | NetSocketProxy, connecting?: boolean) {
super(proxyPromise);
if (connecting) {
this._connecting = connecting;
}
this.on("close", () => {
this._destroyed = true;
this._connecting = false;
});
this.on("connect", () => this._connecting = false);
}
public connect(options: number | string | net.SocketConnectOpts, host?: string | Function, callback?: Function): this {
if (typeof host === "function") {
callback = host;
host = undefined;
}
this._connecting = true;
if (callback) {
this.on("connect", callback as () => void);
}
this.proxy.connect(options, host);
return this;
}
// tslint:disable-next-line no-any
public end(data?: any, encoding?: string | Function, callback?: Function): void {
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
callbackify(this.proxy.end)(data, encoding, () => {
if (callback) {
callback();
}
});
}
// tslint:disable-next-line no-any
public write(data: any, encoding?: string | Function, fd?: string | Function): boolean {
let callback: undefined | Function;
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
if (typeof fd === "function") {
callback = fd;
fd = undefined;
}
if (typeof fd !== "undefined") {
throw new Error("fd argument not supported");
}
callbackify(this.proxy.write)(data, encoding, () => {
if (callback) {
callback();
}
});
return true; // Always true since we can't get this synchronously.
}
public get connecting(): boolean {
return this._connecting;
}
public get destroyed(): boolean {
return this._destroyed;
}
public get bufferSize(): number {
throw new Error("not implemented");
}
public get bytesRead(): number {
throw new Error("not implemented");
}
public get bytesWritten(): number {
throw new Error("not implemented");
}
public get localAddress(): string {
throw new Error("not implemented");
}
public get localPort(): number {
throw new Error("not implemented");
}
public address(): net.AddressInfo | string {
throw new Error("not implemented");
}
public setTimeout(): this {
throw new Error("not implemented");
}
public setNoDelay(): this {
throw new Error("not implemented");
}
public setKeepAlive(): this {
throw new Error("not implemented");
}
public unref(): void {
this.proxy.unref();
}
public ref(): void {
this.proxy.ref();
}
}
export class Server extends ClientProxy<NetServerProxy> implements net.Server {
private readonly sockets = new Map<number, net.Socket>();
private _listening: boolean = false;
public constructor(proxyPromise: Promise<NetServerProxy> | NetServerProxy) {
super(proxyPromise);
this.proxy.onConnection((socketProxy) => {
this.emit("connection", new Socket(socketProxy));
});
this.on("listening", () => this._listening = true);
this.on("error", () => this._listening = false);
this.on("close", () => this._listening = false);
}
public listen(handle?: net.ListenOptions | number | string, hostname?: string | number | Function, backlog?: number | Function, callback?: Function): this {
if (typeof hostname === "function") {
callback = hostname;
hostname = undefined;
}
if (typeof backlog === "function") {
callback = backlog;
backlog = undefined;
}
if (callback) {
this.on("listening", callback as () => void);
}
this.proxy.listen(handle, hostname, backlog);
return this;
}
public get connections(): number {
return this.sockets.size;
}
public get listening(): boolean {
return this._listening;
}
public get maxConnections(): number {
throw new Error("not implemented");
}
public address(): net.AddressInfo | string {
throw new Error("not implemented");
}
public close(callback?: () => void): this {
this._listening = false;
if (callback) {
this.on("close", callback);
}
this.proxy.close();
return this;
}
public ref(): this {
this.proxy.ref();
return this;
}
public unref(): this {
this.proxy.unref();
return this;
}
public getConnections(cb: (error: Error | null, count: number) => void): void {
cb(null, this.sockets.size);
}
}
type NodeNet = typeof net;
export class NetModule implements NodeNet {
public readonly Socket: typeof net.Socket;
public readonly Server: typeof net.Server;
public constructor(private readonly proxy: NetModuleProxy) {
// @ts-ignore this is because Socket is missing things from the Stream
// namespace but I'm unsure how best to provide them (finished,
// finished.__promisify__, pipeline, and some others) or if it even matters.
this.Socket = class extends Socket {
public constructor(options?: net.SocketConstructorOpts) {
super(proxy.createSocket(options));
}
};
this.Server = class extends Server {
public constructor(options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean } | ((socket: Socket) => void), listener?: (socket: Socket) => void) {
super(proxy.createServer(typeof options !== "function" ? options : undefined));
if (typeof options === "function") {
listener = options;
}
if (listener) {
this.on("connection", listener);
}
}
};
}
public createConnection = (target: string | number | net.NetConnectOpts, host?: string | Function, callback?: Function): net.Socket => {
if (typeof host === "function") {
callback = host;
host = undefined;
}
const socket = new Socket(this.proxy.createConnection(target, host), true);
if (callback) {
socket.on("connect", callback as () => void);
}
return socket;
}
public createServer = (
options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean } | ((socket: net.Socket) => void),
callback?: (socket: net.Socket) => void,
): net.Server => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
const server = new Server(this.proxy.createServer(options));
if (callback) {
server.on("connection", callback);
}
return server;
}
public connect = (): net.Socket => {
throw new Error("not implemented");
}
public isIP = (_input: string): number => {
throw new Error("not implemented");
}
public isIPv4 = (_input: string): boolean => {
throw new Error("not implemented");
}
public isIPv6 = (_input: string): boolean => {
throw new Error("not implemented");
}
}

View File

@ -0,0 +1,45 @@
import * as pty from "node-pty";
import { ClientProxy } from "../../common/proxy";
import { NodePtyModuleProxy, NodePtyProcessProxy } from "../../node/modules/node-pty";
export class NodePtyProcess extends ClientProxy<NodePtyProcessProxy> implements pty.IPty {
private _pid = -1;
private _process = "";
public constructor(proxyPromise: Promise<NodePtyProcessProxy>) {
super(proxyPromise);
this.proxy.getPid().then((pid) => this._pid = pid);
this.proxy.getProcess().then((process) => this._process = process);
this.on("process", (process) => this._process = process);
}
public get pid(): number {
return this._pid;
}
public get process(): string {
return this._process;
}
public resize(columns: number, rows: number): void {
this.proxy.resize(columns, rows);
}
public write(data: string): void {
this.proxy.write(data);
}
public kill(signal?: string): void {
this.proxy.kill(signal);
}
}
type NodePty = typeof pty;
export class NodePtyModule implements NodePty {
public constructor(private readonly proxy: NodePtyModuleProxy) {}
public spawn = (file: string, args: string[] | string, options: pty.IPtyForkOptions): pty.IPty => {
return new NodePtyProcess(this.proxy.spawn(file, args, options));
}
}

View File

@ -0,0 +1,32 @@
import * as spdlog from "spdlog";
import { ClientProxy } from "../../common/proxy";
import { RotatingLoggerProxy, SpdlogModuleProxy } from "../../node/modules/spdlog";
class RotatingLogger extends ClientProxy<RotatingLoggerProxy> implements spdlog.RotatingLogger {
public async trace (message: string): Promise<void> { this.proxy.trace(message); }
public async debug (message: string): Promise<void> { this.proxy.debug(message); }
public async info (message: string): Promise<void> { this.proxy.info(message); }
public async warn (message: string): Promise<void> { this.proxy.warn(message); }
public async error (message: string): Promise<void> { this.proxy.error(message); }
public async critical (message: string): Promise<void> { this.proxy.critical(message); }
public async setLevel (level: number): Promise<void> { this.proxy.setLevel(level); }
public async clearFormatters (): Promise<void> { this.proxy.clearFormatters(); }
public async flush (): Promise<void> { this.proxy.flush(); }
public async drop (): Promise<void> { this.proxy.drop(); }
}
export class SpdlogModule {
public readonly RotatingLogger: typeof spdlog.RotatingLogger;
public constructor(private readonly proxy: SpdlogModuleProxy) {
this.RotatingLogger = class extends RotatingLogger {
public constructor(name: string, filename: string, filesize: number, filecount: number) {
super(proxy.createLogger(name, filename, filesize, filecount));
}
};
}
public setAsyncMode = (bufferSize: number, flushInterval: number): void => {
this.proxy.setAsyncMode(bufferSize, flushInterval);
}
}

View File

@ -0,0 +1,233 @@
import * as stream from "stream";
import { callbackify } from "util";
import { ClientProxy } from "../../common/proxy";
import { DuplexProxy, IReadableProxy, WritableProxy } from "../../node/modules/stream";
export class Writable<T extends WritableProxy = WritableProxy> extends ClientProxy<T> implements stream.Writable {
public get writable(): boolean {
throw new Error("not implemented");
}
public get writableHighWaterMark(): number {
throw new Error("not implemented");
}
public get writableLength(): number {
throw new Error("not implemented");
}
public _write(): void {
throw new Error("not implemented");
}
public _destroy(): void {
throw new Error("not implemented");
}
public _final(): void {
throw new Error("not implemented");
}
public pipe<T>(): T {
throw new Error("not implemented");
}
public cork(): void {
throw new Error("not implemented");
}
public uncork(): void {
throw new Error("not implemented");
}
public destroy(): void {
this.proxy.destroy();
}
public setDefaultEncoding(encoding: string): this {
this.proxy.setDefaultEncoding(encoding);
return this;
}
// tslint:disable-next-line no-any
public write(chunk: any, encoding?: string | ((error?: Error | null) => void), callback?: (error?: Error | null) => void): boolean {
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
callbackify(this.proxy.write)(chunk, encoding, (error) => {
if (callback) {
callback(error);
}
});
return true; // Always true since we can't get this synchronously.
}
// tslint:disable-next-line no-any
public end(data?: any | (() => void), encoding?: string | (() => void), callback?: (() => void)): void {
if (typeof data === "function") {
callback = data;
data = undefined;
}
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
callbackify(this.proxy.end)(data, encoding, () => {
if (callback) {
callback();
}
});
}
}
export class Readable<T extends IReadableProxy = IReadableProxy> extends ClientProxy<T> implements stream.Readable {
public get readable(): boolean {
throw new Error("not implemented");
}
public get readableHighWaterMark(): number {
throw new Error("not implemented");
}
public get readableLength(): number {
throw new Error("not implemented");
}
public _read(): void {
throw new Error("not implemented");
}
public read(): void {
throw new Error("not implemented");
}
public _destroy(): void {
throw new Error("not implemented");
}
public unpipe(): this {
throw new Error("not implemented");
}
public pause(): this {
throw new Error("not implemented");
}
public resume(): this {
throw new Error("not implemented");
}
public isPaused(): boolean {
throw new Error("not implemented");
}
public wrap(): this {
throw new Error("not implemented");
}
public push(): boolean {
throw new Error("not implemented");
}
public unshift(): void {
throw new Error("not implemented");
}
public pipe<T>(): T {
throw new Error("not implemented");
}
// tslint:disable-next-line no-any
public [Symbol.asyncIterator](): AsyncIterableIterator<any> {
throw new Error("not implemented");
}
public destroy(): void {
this.proxy.destroy();
}
public setEncoding(encoding: string): this {
this.proxy.setEncoding(encoding);
return this;
}
}
export class Duplex<T extends DuplexProxy = DuplexProxy> extends Writable<T> implements stream.Duplex, stream.Readable {
private readonly _readable: Readable;
public constructor(proxyPromise: Promise<T> | T) {
super(proxyPromise);
this._readable = new Readable(proxyPromise, false);
}
public get readable(): boolean {
return this._readable.readable;
}
public get readableHighWaterMark(): number {
return this._readable.readableHighWaterMark;
}
public get readableLength(): number {
return this._readable.readableLength;
}
public _read(): void {
this._readable._read();
}
public read(): void {
this._readable.read();
}
public unpipe(): this {
this._readable.unpipe();
return this;
}
public pause(): this {
this._readable.unpipe();
return this;
}
public resume(): this {
this._readable.resume();
return this;
}
public isPaused(): boolean {
return this._readable.isPaused();
}
public wrap(): this {
this._readable.wrap();
return this;
}
public push(): boolean {
return this._readable.push();
}
public unshift(): void {
this._readable.unshift();
}
// tslint:disable-next-line no-any
public [Symbol.asyncIterator](): AsyncIterableIterator<any> {
return this._readable[Symbol.asyncIterator]();
}
public setEncoding(encoding: string): this {
this.proxy.setEncoding(encoding);
return this;
}
}

View File

@ -0,0 +1,10 @@
import * as trash from "trash";
import { TrashModuleProxy } from "../../node/modules/trash";
export class TrashModule {
public constructor(private readonly proxy: TrashModuleProxy) {}
public trash = (path: string, options?: trash.Options): Promise<void> => {
return this.proxy.trash(path, options);
}
}

View File

@ -5,6 +5,8 @@ export interface SendableConnection {
export interface ReadWriteConnection extends SendableConnection {
onMessage(cb: (data: Uint8Array | Buffer) => void): void;
onClose(cb: () => void): void;
onDown(cb: () => void): void;
onUp(cb: () => void): void;
close(): void;
}

View File

@ -1,422 +0,0 @@
/// <reference path="../../../../lib/vscode/src/typings/spdlog.d.ts" />
/// <reference path="../../node_modules/node-pty-prebuilt/typings/node-pty.d.ts" />
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";
// tslint:disable no-any
export type ForkProvider = (modulePath: string, args: string[], options: ForkOptions) => 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;
}
/**
* For any non-external modules that are not built in, we need to require and
* access them server-side. A require on the client-side won't work since that
* code won't exist on the server (and bloat the client with an unused import),
* and we can't manually import on the server-side and then call
* `__webpack_require__` on the client-side because Webpack stores modules by
* their paths which would require us to hard-code the path.
*/
export interface Modules {
pty: typeof import("node-pty");
spdlog: typeof import("spdlog");
trash: typeof import("trash");
}
/**
* Helper class for server-side evaluations.
*/
export class EvalHelper {
public constructor(public modules: Modules) {}
/**
* 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 };
}
}
}
/**
* Helper class for client-side active evaluations.
*/
export class ActiveEvalHelper implements ActiveEvalEmitter {
public constructor(private readonly emitter: ActiveEvalEmitter) {}
public removeAllListeners(event?: string): void {
this.emitter.removeAllListeners(event);
}
public emit(event: string, ...args: any[]): void {
this.emitter.emit(event, ...args);
}
public on(event: string, cb: (...args: any[]) => void): void {
this.emitter.on(event, cb);
}
/**
* 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 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 = <string[]>[];
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 => {
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 implements EvalHelper {
private readonly evalHelper: EvalHelper;
public constructor(public modules: Modules, emitter: ActiveEvalEmitter, public readonly fork: ForkProvider) {
super(emitter);
this.evalHelper = new EvalHelper(modules);
}
public preserveEnv(options: SpawnOptions | ForkOptions): void {
this.evalHelper.preserveEnv(options);
}
/**
* 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.modules, 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: ActiveEvalHelper | undefined;
private callbackId = 0;
private readonly callbacks = new Map<number, Function>();
public constructor(ae?: ActiveEvalHelper) {
super();
if (ae) {
this.ae = ae;
}
}
protected get ae(): ActiveEvalHelper {
if (!this._ae) {
throw new Error("trying to access active evaluation before it has been set");
}
return this._ae;
}
protected set ae(ae: ActiveEvalHelper) {
if (this._ae) {
throw new Error("cannot override active evaluation");
}
this._ae = ae;
this.ae.on("callback", (callbackId, ...args: any[]) => this.runCallback(callbackId, ...args));
}
/**
* Store the callback and return and ID referencing its location in the map.
*/
protected storeCallback(callback?: Function): number | undefined {
if (!callback) {
return undefined;
}
const callbackId = this.callbackId++;
this.callbacks.set(callbackId, callback);
return callbackId;
}
/**
* Call the function with the specified ID and delete it from the map.
* If the ID is undefined or doesn't exist, nothing happens.
*/
private runCallback(callbackId?: number, ...args: any[]): void {
const callback = typeof callbackId !== "undefined" && this.callbacks.get(callbackId);
if (callback && typeof callbackId !== "undefined") {
this.callbacks.delete(callbackId);
callback(...args);
}
}
}
/**
* A writable stream over an active evaluation.
*/
export class ActiveEvalWritable extends CallbackEmitter implements Writable {
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));
this.ae.on("drain", () => this.emit("drain"));
this.ae.on("error", (error) => this.emit("error", error));
this.ae.on("finish", () => this.emit("finish"));
this.ae.on("pipe", () => logger.warn("pipe is not supported"));
this.ae.on("unpipe", () => logger.warn("unpipe is not supported"));
}
public get writable(): boolean { throw new Error("not implemented"); }
public get writableHighWaterMark(): number { throw new Error("not implemented"); }
public get writableLength(): number { throw new Error("not implemented"); }
public _write(): void { throw new Error("not implemented"); }
public _destroy(): void { throw new Error("not implemented"); }
public _final(): void { throw new Error("not implemented"); }
public pipe<T>(): T { throw new Error("not implemented"); }
public cork(): void { this.ae.emit("cork"); }
public destroy(): void { this.ae.emit("destroy"); }
public setDefaultEncoding(encoding: string): this {
this.ae.emit("setDefaultEncoding", encoding);
return this;
}
public uncork(): void { this.ae.emit("uncork"); }
public write(chunk: any, encoding?: string | ((error?: Error | null) => void), callback?: (error?: Error | null) => void): boolean {
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
// Sockets can pass an fd instead of a callback but streams cannot..
this.ae.emit("write", chunk, encoding, undefined, this.storeCallback(callback));
// Always true since we can't get this synchronously.
return true;
}
public end(data?: any, encoding?: string | Function, callback?: Function): void {
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
this.ae.emit("end", data, encoding, this.storeCallback(callback));
}
}
/**
* A readable stream over an active evaluation.
*/
export class ActiveEvalReadable extends CallbackEmitter implements Readable {
public constructor(ae: ActiveEvalHelper) {
super(ae);
this.ae.on("close", () => this.emit("close"));
this.ae.on("data", (data) => this.emit("data", data));
this.ae.on("end", () => this.emit("end"));
this.ae.on("error", (error) => this.emit("error", error));
this.ae.on("readable", () => this.emit("readable"));
}
public get readable(): boolean { throw new Error("not implemented"); }
public get readableHighWaterMark(): number { throw new Error("not implemented"); }
public get readableLength(): number { throw new Error("not implemented"); }
public _read(): void { throw new Error("not implemented"); }
public read(): any { throw new Error("not implemented"); }
public isPaused(): boolean { throw new Error("not implemented"); }
public pipe<T>(): T { throw new Error("not implemented"); }
public unpipe(): this { throw new Error("not implemented"); }
public unshift(): this { throw new Error("not implemented"); }
public wrap(): this { throw new Error("not implemented"); }
public push(): boolean { throw new Error("not implemented"); }
public _destroy(): void { throw new Error("not implemented"); }
public [Symbol.asyncIterator](): AsyncIterableIterator<any> { throw new Error("not implemented"); }
public destroy(): void { this.ae.emit("destroy"); }
public pause(): this { return this.emitReturnThis("pause"); }
public resume(): this { return this.emitReturnThis("resume"); }
public setEncoding(encoding?: string): this { return this.emitReturnThis("setEncoding", encoding); }
// tslint:disable-next-line no-any
protected emitReturnThis(event: string, ...args: any[]): this {
this.ae.emit(event, ...args);
return this;
}
}
/**
* An duplex stream over an active evaluation.
*/
export class ActiveEvalDuplex extends ActiveEvalReadable implements Duplex {
// Some unfortunate duplication here since we can't have multiple extends.
public constructor(ae: ActiveEvalHelper) {
super(ae);
this.ae.on("drain", () => this.emit("drain"));
this.ae.on("finish", () => this.emit("finish"));
this.ae.on("pipe", () => logger.warn("pipe is not supported"));
this.ae.on("unpipe", () => logger.warn("unpipe is not supported"));
}
public get writable(): boolean { throw new Error("not implemented"); }
public get writableHighWaterMark(): number { throw new Error("not implemented"); }
public get writableLength(): number { throw new Error("not implemented"); }
public _write(): void { throw new Error("not implemented"); }
public _destroy(): void { throw new Error("not implemented"); }
public _final(): void { throw new Error("not implemented"); }
public pipe<T>(): T { throw new Error("not implemented"); }
public cork(): void { this.ae.emit("cork"); }
public destroy(): void { this.ae.emit("destroy"); }
public setDefaultEncoding(encoding: string): this {
this.ae.emit("setDefaultEncoding", encoding);
return this;
}
public uncork(): void { this.ae.emit("uncork"); }
public write(chunk: any, encoding?: string | ((error?: Error | null) => void), callback?: (error?: Error | null) => void): boolean {
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
// Sockets can pass an fd instead of a callback but streams cannot..
this.ae.emit("write", chunk, encoding, undefined, this.storeCallback(callback));
// Always true since we can't get this synchronously.
return true;
}
public end(data?: any, encoding?: string | Function, callback?: Function): void {
if (typeof encoding === "function") {
callback = encoding;
encoding = undefined;
}
this.ae.emit("end", data, encoding, this.storeCallback(callback));
}
}

View File

@ -0,0 +1,83 @@
import { EventEmitter } from "events";
import { isPromise } from "./util";
// tslint:disable no-any
/**
* Allow using a proxy like it's returned synchronously. This only works because
* all proxy methods return promises.
*/
const unpromisify = <T extends ServerProxy>(proxyPromise: Promise<T>): T => {
return new Proxy({}, {
get: (target: any, name: string): any => {
if (typeof target[name] === "undefined") {
target[name] = async (...args: any[]): Promise<any> => {
const proxy = await proxyPromise;
return proxy ? (proxy as any)[name](...args) : undefined;
};
}
return target[name];
},
});
};
/**
* Client-side emitter that just forwards proxy events to its own emitter.
* It also turns a promisified proxy into a non-promisified proxy so we don't
* need a bunch of `then` calls everywhere.
*/
export abstract class ClientProxy<T extends ServerProxy> extends EventEmitter {
protected readonly proxy: T;
/**
* You can specify not to bind events in order to avoid emitting twice for
* duplex streams.
*/
public constructor(proxyPromise: Promise<T> | T, bindEvents: boolean = true) {
super();
this.proxy = isPromise(proxyPromise) ? unpromisify(proxyPromise) : proxyPromise;
if (bindEvents) {
this.proxy.onEvent((event, ...args): void => {
this.emit(event, ...args);
});
}
}
}
/**
* Proxy to the actual instance on the server. Every method must only accept
* serializable arguments and must return promises with serializable values. If
* a proxy itself has proxies on creation (like how ChildProcess has stdin),
* then it should return all of those at once, otherwise you will miss events
* from those child proxies and fail to dispose them properly.
*/
export interface ServerProxy {
dispose(): Promise<void>;
/**
* This is used instead of an event to force it to be implemented since there
* would be no guarantee the implementation would remember to emit the event.
*/
onDone(cb: () => void): Promise<void>;
/**
* Listen to all possible events. On the client, this is to reduce boilerplate
* that would just be a bunch of error-prone forwarding of each individual
* event from the proxy to its own emitter. It also fixes a timing issue
* because we just always send all events from the server, so we never miss
* any due to listening too late.
*/
// tslint:disable-next-line no-any
onEvent(cb: (event: string, ...args: any[]) => void): Promise<void>;
}
export enum Module {
Fs = "fs",
ChildProcess = "child_process",
Net = "net",
Spdlog = "spdlog",
NodePty = "node-pty",
Trash = "trash",
}

View File

@ -1,3 +1,9 @@
import { Module as ProtoModule, WorkingInitMessage } from "../proto";
import { OperatingSystem } from "../common/connection";
import { Module, ServerProxy } from "./proxy";
// tslint:disable no-any
/**
* Return true if we're in a browser environment (including web workers).
*/
@ -14,86 +20,294 @@ export const escapePath = (path: string): string => {
};
export type IEncodingOptions = {
encoding?: string | null;
encoding?: BufferEncoding | null;
flag?: string;
mode?: string;
persistent?: boolean;
recursive?: boolean;
} | string | undefined | null;
} | BufferEncoding | undefined | null;
// tslint:disable-next-line no-any
export type IEncodingOptionsCallback = IEncodingOptions | ((err: NodeJS.ErrnoException, ...args: any[]) => void);
/**
* Stringify an event argument. isError is because although methods like
* `fs.stat` are supposed to throw Error objects, they currently throw regular
* objects when running tests through Jest.
*/
export const stringify = (arg: any, isError?: boolean): string => { // tslint:disable-line no-any
if (arg instanceof Error || isError) {
// Errors don't stringify at all. They just become "{}".
return JSON.stringify({
type: "Error",
data: {
message: arg.message,
stack: arg.stack,
code: (arg as NodeJS.ErrnoException).code,
},
});
} else if (arg instanceof Uint8Array) {
// With stringify, these get turned into objects with each index becoming a
// key for some reason. Then trying to do something like write that data
// results in [object Object] being written. Stringify them like a Buffer
// instead.
return JSON.stringify({
type: "Buffer",
data: Array.from(arg),
});
}
interface StringifiedError {
type: "error";
data: {
message: string;
stack?: string;
code?: string;
};
}
return JSON.stringify(arg);
interface StringifiedBuffer {
type: "buffer";
data: number[];
}
interface StringifiedObject {
type: "object";
data: { [key: string]: StringifiedValue };
}
interface StringifiedArray {
type: "array";
data: StringifiedValue[];
}
interface StringifiedProxy {
type: "proxy";
data: {
id: number;
};
}
interface StringifiedFunction {
type: "function";
data: {
id: number;
};
}
interface StringifiedUndefined {
type: "undefined";
}
type StringifiedValue = StringifiedFunction | StringifiedProxy
| StringifiedUndefined | StringifiedObject | StringifiedArray
| StringifiedBuffer | StringifiedError | number | string | boolean | null;
const isPrimitive = (value: any): value is number | string | boolean | null => {
return typeof value === "number"
|| typeof value === "string"
|| typeof value === "boolean"
|| value === null;
};
/**
* Parse an event argument.
*/
export const parse = (arg: string): any => { // tslint:disable-line no-any
const convert = (value: any): any => { // tslint:disable-line no-any
if (value && value.data && value.type) {
switch (value.type) {
// JSON.stringify turns a Buffer into an object but JSON.parse doesn't
// turn it back, it just remains an object.
case "Buffer":
if (Array.isArray(value.data)) {
return Buffer.from(value);
}
break;
// Errors apparently can't be stringified, so we do something similar to
// what happens to buffers and stringify them as regular objects.
case "Error":
if (value.data.message) {
const error = new Error(value.data.message);
// TODO: Can we set the stack? Doing so seems to make it into an
// "invalid object".
if (typeof value.data.code !== "undefined") {
(error as NodeJS.ErrnoException).code = value.data.code;
}
// tslint:disable-next-line no-any
(error as any).originalStack = value.data.stack;
return error;
}
break;
/**
* Stringify an argument or a return value.
* If sending a function is possible, provide `storeFunction`.
* If sending a proxy is possible, provide `storeProxy`.
*/
export const stringify = (
value: any,
storeFunction?: (fn: () => void) => number,
storeProxy?: (proxy: ServerProxy) => number,
): string => {
const convert = (currentValue: any): StringifiedValue => {
// Errors don't stringify at all. They just become "{}".
// For some reason when running in Jest errors aren't instances of Error,
// so also check against the values.
if (currentValue instanceof Error
|| (currentValue && typeof currentValue.message !== "undefined"
&& typeof currentValue.stack !== "undefined")) {
return {
type: "error",
data: {
message: currentValue.message,
stack: currentValue.stack,
code: (currentValue as NodeJS.ErrnoException).code,
},
};
}
// With stringify, Uint8Array gets turned into objects with each index
// becoming a key for some reason. Then trying to do something like write
// that data results in [object Object] being written. Stringify them like
// a Buffer instead. Also handle Buffer so it doesn't get caught by the
// object check and to get the same type.
if (currentValue instanceof Uint8Array || currentValue instanceof Buffer) {
return {
type: "buffer",
data: Array.from(currentValue),
};
}
if (Array.isArray(currentValue)) {
return {
type: "array",
data: currentValue.map((a) => convert(a)),
};
}
if (isProxy(currentValue)) {
if (!storeProxy) {
throw new Error("no way to serialize proxy");
}
return {
type: "proxy",
data: {
id: storeProxy(currentValue),
},
};
}
if (currentValue !== null && typeof currentValue === "object") {
const converted: { [key: string]: StringifiedValue } = {};
Object.keys(currentValue).forEach((key) => {
converted[key] = convert(currentValue[key]);
});
return {
type: "object",
data: converted,
};
}
// `undefined` can't be stringified.
if (typeof currentValue === "undefined") {
return {
type: "undefined",
};
}
if (typeof currentValue === "function") {
if (!storeFunction) {
throw new Error("no way to serialize function");
}
return {
type: "function",
data: {
id: storeFunction(currentValue),
},
};
}
if (!isPrimitive(currentValue)) {
throw new Error(`cannot stringify ${typeof currentValue}`);
}
return currentValue;
};
return JSON.stringify(convert(value));
};
/**
* Parse an argument.
* If running a remote callback is supported, provide `runCallback`.
* If using a remote proxy is supported, provide `createProxy`.
*/
export const parse = (
value?: string,
runCallback?: (id: number, args: any[]) => void,
createProxy?: (id: number) => ServerProxy,
): any => {
const convert = (currentValue: StringifiedValue): any => {
if (currentValue && !isPrimitive(currentValue)) {
// Would prefer a switch but the types don't seem to work.
if (currentValue.type === "buffer") {
return Buffer.from(currentValue.data);
}
if (currentValue.type === "error") {
const error = new Error(currentValue.data.message);
if (typeof currentValue.data.code !== "undefined") {
(error as NodeJS.ErrnoException).code = currentValue.data.code;
}
(error as any).originalStack = currentValue.data.stack;
return error;
}
if (currentValue.type === "object") {
const converted: { [key: string]: any } = {};
Object.keys(currentValue.data).forEach((key) => {
converted[key] = convert(currentValue.data[key]);
});
return converted;
}
if (currentValue.type === "array") {
return currentValue.data.map(convert);
}
if (currentValue.type === "undefined") {
return undefined;
}
if (currentValue.type === "function") {
if (!runCallback) {
throw new Error("no way to run remote callback");
}
return (...args: any[]): void => {
return runCallback(currentValue.data.id, args);
};
}
if (currentValue.type === "proxy") {
if (!createProxy) {
throw new Error("no way to create proxy");
}
return createProxy(currentValue.data.id);
}
}
if (value && typeof value === "object") {
Object.keys(value).forEach((key) => {
value[key] = convert(value[key]);
});
}
return value;
return currentValue;
};
return arg ? convert(JSON.parse(arg)) : arg;
return value && convert(JSON.parse(value));
};
export const protoToModule = (protoModule: ProtoModule): Module => {
switch (protoModule) {
case ProtoModule.CHILDPROCESS: return Module.ChildProcess;
case ProtoModule.FS: return Module.Fs;
case ProtoModule.NET: return Module.Net;
case ProtoModule.NODEPTY: return Module.NodePty;
case ProtoModule.SPDLOG: return Module.Spdlog;
case ProtoModule.TRASH: return Module.Trash;
default: throw new Error(`invalid module ${protoModule}`);
}
};
export const moduleToProto = (moduleName: Module): ProtoModule => {
switch (moduleName) {
case Module.ChildProcess: return ProtoModule.CHILDPROCESS;
case Module.Fs: return ProtoModule.FS;
case Module.Net: return ProtoModule.NET;
case Module.NodePty: return ProtoModule.NODEPTY;
case Module.Spdlog: return ProtoModule.SPDLOG;
case Module.Trash: return ProtoModule.TRASH;
default: throw new Error(`invalid module "${moduleName}"`);
}
};
export const protoToOperatingSystem = (protoOp: WorkingInitMessage.OperatingSystem): OperatingSystem => {
switch (protoOp) {
case WorkingInitMessage.OperatingSystem.WINDOWS: return OperatingSystem.Windows;
case WorkingInitMessage.OperatingSystem.LINUX: return OperatingSystem.Linux;
case WorkingInitMessage.OperatingSystem.MAC: return OperatingSystem.Mac;
default: throw new Error(`unsupported operating system ${protoOp}`);
}
};
export const platformToProto = (platform: NodeJS.Platform): WorkingInitMessage.OperatingSystem => {
switch (platform) {
case "win32": return WorkingInitMessage.OperatingSystem.WINDOWS;
case "linux": return WorkingInitMessage.OperatingSystem.LINUX;
case "darwin": return WorkingInitMessage.OperatingSystem.MAC;
default: throw new Error(`unrecognized platform "${platform}"`);
}
};
export const isProxy = (value: any): value is ServerProxy => {
return value && typeof value === "object" && typeof value.onEvent === "function";
};
export const isPromise = (value: any): value is Promise<any> => {
return typeof value.then === "function" && typeof value.catch === "function";
};
/**
* When spawning VS Code tries to preserve the environment but since it's in
* the browser, it doesn't work.
*/
export const preserveEnv = (options?: { env?: NodeJS.ProcessEnv } | null): void => {
if (options && options.env) {
options.env = { ...process.env, ...options.env };
}
};

View File

@ -1,4 +1,4 @@
export * from "./browser/client";
export * from "./common/connection";
export * from "./common/helpers";
export * from "./common/proxy";
export * from "./common/util";

View File

@ -1,157 +0,0 @@
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, Modules } from "../common/helpers";
import { stringify, parse } from "../common/util";
export interface ActiveEvaluation {
onEvent(msg: EvalEventMessage): void;
dispose(): void;
}
declare var __non_webpack_require__: typeof require;
export const evaluate = (connection: SendableConnection, message: NewEvalMessage, onDispose: () => void, fork?: ForkProvider): ActiveEvaluation | void => {
/**
* Send the response and call onDispose.
*/
// tslint:disable-next-line no-any
const sendResp = (resp: any): void => {
logger.trace(() => [
"resolve",
field("id", message.getId()),
field("response", stringify(resp)),
]);
const evalDone = new EvalDoneMessage();
evalDone.setId(message.getId());
evalDone.setResponse(stringify(resp));
const serverMsg = new ServerMessage();
serverMsg.setEvalDone(evalDone);
connection.send(serverMsg.serializeBinary());
onDispose();
};
/**
* Send an exception and call onDispose.
*/
const sendException = (error: Error): void => {
logger.trace(() => [
"reject",
field("id", message.getId()),
field("response", stringify(error, true)),
]);
const evalFailed = new EvalFailedMessage();
evalFailed.setId(message.getId());
evalFailed.setResponse(stringify(error, true));
const serverMsg = new ServerMessage();
serverMsg.setEvalFailed(evalFailed);
connection.send(serverMsg.serializeBinary());
onDispose();
};
const modules: Modules = {
spdlog: require("spdlog"),
pty: require("node-pty-prebuilt"),
trash: require("trash"),
};
let eventEmitter = message.getActive() ? new EventEmitter(): undefined;
const sandbox = {
helper: eventEmitter ? new ServerActiveEvalHelper(modules, {
removeAllListeners: (event?: string): void => {
eventEmitter!.removeAllListeners(event);
},
// tslint:disable no-any
on: (event: string, cb: (...args: any[]) => void): void => {
eventEmitter!.on(event, (...args: any[]) => {
logger.trace(() => [
`${event}`,
field("id", message.getId()),
field("args", args.map((a) => stringify(a))),
]);
cb(...args);
});
},
emit: (event: string, ...args: any[]): void => {
logger.trace(() => [
`emit ${event}`,
field("id", message.getId()),
field("args", args.map((a) => stringify(a))),
]);
const eventMsg = new EvalEventMessage();
eventMsg.setEvent(event);
eventMsg.setArgsList(args.map((a) => stringify(a)));
eventMsg.setId(message.getId());
const serverMsg = new ServerMessage();
serverMsg.setEvalEvent(eventMsg);
connection.send(serverMsg.serializeBinary());
},
// tslint:enable no-any
}, fork || cpFork) : new EvalHelper(modules),
_Buffer: Buffer,
// When the client is ran from Webpack, it will replace
// __non_webpack_require__ with require, which we then need to provide to
// the sandbox. Since the server might also be using Webpack, we need to set
// it to the non-Webpack version when that's the case. Then we need to also
// provide __non_webpack_require__ for when the client doesn't run through
// Webpack meaning it doesn't get replaced with require (Jest for example).
require: typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require,
__non_webpack_require__: typeof __non_webpack_require__ !== "undefined" ? __non_webpack_require__ : require,
setTimeout,
setInterval,
clearTimeout,
process: {
env: process.env,
},
args: message.getArgsList().map(parse),
};
let value: any; // tslint:disable-line no-any
try {
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,
});
} catch (ex) {
sendException(ex);
}
// An evaluation completes when the value it returns resolves. An active
// evaluation completes when it is disposed. Active evaluations are required
// to return disposers so we can know both when it has ended (so we can clean
// up on our end) and how to force end it (for example when the client
// disconnects).
// tslint:disable-next-line no-any
const promise = !eventEmitter ? value as Promise<any> : new Promise((resolve): void => {
value.onDidDispose(resolve);
});
if (promise && promise.then) {
promise.then(sendResp).catch(sendException);
} else {
sendResp(value);
}
return eventEmitter ? {
onEvent: (eventMsg: EvalEventMessage): void => {
eventEmitter!.emit(eventMsg.getEvent(), ...eventMsg.getArgsList().map(parse));
},
dispose: (): void => {
if (eventEmitter) {
if (value && value.dispose) {
value.dispose();
}
eventEmitter.removeAllListeners();
eventEmitter = undefined;
}
},
} : undefined;
};

View File

@ -0,0 +1,103 @@
import * as cp from "child_process";
import { ServerProxy } from "../../common/proxy";
import { preserveEnv } from "../../common/util";
import { WritableProxy, ReadableProxy } from "./stream";
export type ForkProvider = (modulePath: string, args?: string[], options?: cp.ForkOptions) => cp.ChildProcess;
export class ChildProcessProxy implements ServerProxy {
public constructor(private readonly process: cp.ChildProcess) {}
public async kill(signal?: string): Promise<void> {
this.process.kill(signal);
}
public async disconnect(): Promise<void> {
this.process.disconnect();
}
public async ref(): Promise<void> {
this.process.ref();
}
public async unref(): Promise<void> {
this.process.unref();
}
// tslint:disable-next-line no-any
public async send(message: any): Promise<void> {
return new Promise((resolve, reject) => {
this.process.send(message, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
public async getPid(): Promise<number> {
return this.process.pid;
}
public async onDone(cb: () => void): Promise<void> {
this.process.on("close", cb);
}
public async dispose(): Promise<void> {
this.kill();
setTimeout(() => this.kill("SIGKILL"), 5000); // Double tap.
}
// tslint:disable-next-line no-any
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
this.process.on("close", (code, signal) => cb("close", code, signal));
this.process.on("disconnect", () => cb("disconnect"));
this.process.on("error", (error) => cb("error", error));
this.process.on("exit", (exitCode, signal) => cb("exit", exitCode, signal));
this.process.on("message", (message) => cb("message", message));
}
}
export interface ChildProcessProxies {
childProcess: ChildProcessProxy;
stdin?: WritableProxy;
stdout?: ReadableProxy;
stderr?: ReadableProxy;
}
export class ChildProcessModuleProxy {
public constructor(private readonly forkProvider?: ForkProvider) {}
public async exec(
command: string,
options?: { encoding?: string | null } & cp.ExecOptions | null,
callback?: ((error: cp.ExecException | null, stdin: string | Buffer, stdout: string | Buffer) => void),
): Promise<ChildProcessProxies> {
preserveEnv(options);
return this.returnProxies(cp.exec(command, options, callback));
}
public async fork(modulePath: string, args?: string[], options?: cp.ForkOptions): Promise<ChildProcessProxies> {
preserveEnv(options);
return this.returnProxies((this.forkProvider || cp.fork)(modulePath, args, options));
}
public async spawn(command: string, args?: string[], options?: cp.SpawnOptions): Promise<ChildProcessProxies> {
preserveEnv(options);
return this.returnProxies(cp.spawn(command, args, options));
}
private returnProxies(process: cp.ChildProcess): ChildProcessProxies {
return {
childProcess: new ChildProcessProxy(process),
stdin: process.stdin && new WritableProxy(process.stdin),
stdout: process.stdout && new ReadableProxy(process.stdout),
stderr: process.stderr && new ReadableProxy(process.stderr),
};
}
}

View File

@ -0,0 +1,250 @@
import * as fs from "fs";
import { promisify } from "util";
import { ServerProxy } from "../../common/proxy";
import { IEncodingOptions } from "../../common/util";
import { WritableProxy } from "./stream";
/**
* A serializable version of fs.Stats.
*/
export interface Stats {
dev: number;
ino: number;
mode: number;
nlink: number;
uid: number;
gid: number;
rdev: number;
size: number;
blksize: number;
blocks: number;
atimeMs: number;
mtimeMs: number;
ctimeMs: number;
birthtimeMs: number;
atime: Date | string;
mtime: Date | string;
ctime: Date | string;
birthtime: Date | string;
_isFile: boolean;
_isDirectory: boolean;
_isBlockDevice: boolean;
_isCharacterDevice: boolean;
_isSymbolicLink: boolean;
_isFIFO: boolean;
_isSocket: boolean;
}
export class WriteStreamProxy extends WritableProxy<fs.WriteStream> {
public async close(): Promise<void> {
this.stream.close();
}
public async dispose(): Promise<void> {
super.dispose();
this.stream.close();
}
// tslint:disable-next-line no-any
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
super.onEvent(cb);
this.stream.on("open", (fd) => cb("open", fd));
}
}
export class WatcherProxy implements ServerProxy {
public constructor(private readonly watcher: fs.FSWatcher) {}
public async close(): Promise<void> {
this.watcher.close();
}
public async dispose(): Promise<void> {
this.watcher.close();
this.watcher.removeAllListeners();
}
public async onDone(cb: () => void): Promise<void> {
this.watcher.on("close", cb);
this.watcher.on("error", cb);
}
// tslint:disable-next-line no-any
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
this.watcher.on("change", (event, filename) => cb("change", event, filename));
this.watcher.on("close", () => cb("close"));
this.watcher.on("error", (error) => cb("error", error));
}
}
export class FsModuleProxy {
public access(path: fs.PathLike, mode?: number): Promise<void> {
return promisify(fs.access)(path, mode);
}
// tslint:disable-next-line no-any
public appendFile(file: fs.PathLike | number, data: any, options?: fs.WriteFileOptions): Promise<void> {
return promisify(fs.appendFile)(file, data, options);
}
public chmod(path: fs.PathLike, mode: string | number): Promise<void> {
return promisify(fs.chmod)(path, mode);
}
public chown(path: fs.PathLike, uid: number, gid: number): Promise<void> {
return promisify(fs.chown)(path, uid, gid);
}
public close(fd: number): Promise<void> {
return promisify(fs.close)(fd);
}
public copyFile(src: fs.PathLike, dest: fs.PathLike, flags?: number): Promise<void> {
return promisify(fs.copyFile)(src, dest, flags);
}
// tslint:disable-next-line no-any
public async createWriteStream(path: fs.PathLike, options?: any): Promise<WriteStreamProxy> {
return new WriteStreamProxy(fs.createWriteStream(path, options));
}
public exists(path: fs.PathLike): Promise<boolean> {
return promisify(fs.exists)(path);
}
public fchmod(fd: number, mode: string | number): Promise<void> {
return promisify(fs.fchmod)(fd, mode);
}
public fchown(fd: number, uid: number, gid: number): Promise<void> {
return promisify(fs.fchown)(fd, uid, gid);
}
public fdatasync(fd: number): Promise<void> {
return promisify(fs.fdatasync)(fd);
}
public async fstat(fd: number): Promise<Stats> {
return this.makeStatsSerializable(await promisify(fs.fstat)(fd));
}
public fsync(fd: number): Promise<void> {
return promisify(fs.fsync)(fd);
}
public ftruncate(fd: number, len?: number | null): Promise<void> {
return promisify(fs.ftruncate)(fd, len);
}
public futimes(fd: number, atime: string | number | Date, mtime: string | number | Date): Promise<void> {
return promisify(fs.futimes)(fd, atime, mtime);
}
public lchmod(path: fs.PathLike, mode: string | number): Promise<void> {
return promisify(fs.lchmod)(path, mode);
}
public lchown(path: fs.PathLike, uid: number, gid: number): Promise<void> {
return promisify(fs.lchown)(path, uid, gid);
}
public link(existingPath: fs.PathLike, newPath: fs.PathLike): Promise<void> {
return promisify(fs.link)(existingPath, newPath);
}
public async lstat(path: fs.PathLike): Promise<Stats> {
return this.makeStatsSerializable(await promisify(fs.lstat)(path));
}
public mkdir(path: fs.PathLike, mode: number | string | fs.MakeDirectoryOptions | undefined | null): Promise<void> {
return promisify(fs.mkdir)(path, mode);
}
public mkdtemp(prefix: string, options: IEncodingOptions): Promise<string | Buffer> {
return promisify(fs.mkdtemp)(prefix, options);
}
public open(path: fs.PathLike, flags: string | number, mode: string | number | undefined | null): Promise<number> {
return promisify(fs.open)(path, flags, mode);
}
public read(fd: number, length: number, position: number | null): Promise<{ bytesRead: number, buffer: Buffer }> {
const buffer = new Buffer(length);
return promisify(fs.read)(fd, buffer, 0, length, position);
}
public readFile(path: fs.PathLike | number, options: IEncodingOptions): Promise<string | Buffer> {
return promisify(fs.readFile)(path, options);
}
public readdir(path: fs.PathLike, options: IEncodingOptions): Promise<Buffer[] | fs.Dirent[] | string[]> {
return promisify(fs.readdir)(path, options);
}
public readlink(path: fs.PathLike, options: IEncodingOptions): Promise<string | Buffer> {
return promisify(fs.readlink)(path, options);
}
public realpath(path: fs.PathLike, options: IEncodingOptions): Promise<string | Buffer> {
return promisify(fs.realpath)(path, options);
}
public rename(oldPath: fs.PathLike, newPath: fs.PathLike): Promise<void> {
return promisify(fs.rename)(oldPath, newPath);
}
public rmdir(path: fs.PathLike): Promise<void> {
return promisify(fs.rmdir)(path);
}
public async stat(path: fs.PathLike): Promise<Stats> {
return this.makeStatsSerializable(await promisify(fs.stat)(path));
}
public symlink(target: fs.PathLike, path: fs.PathLike, type?: fs.symlink.Type | null): Promise<void> {
return promisify(fs.symlink)(target, path, type);
}
public truncate(path: fs.PathLike, len?: number | null): Promise<void> {
return promisify(fs.truncate)(path, len);
}
public unlink(path: fs.PathLike): Promise<void> {
return promisify(fs.unlink)(path);
}
public utimes(path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date): Promise<void> {
return promisify(fs.utimes)(path, atime, mtime);
}
public async write(fd: number, buffer: Buffer, offset?: number, length?: number, position?: number): Promise<{ bytesWritten: number, buffer: Buffer }> {
return promisify(fs.write)(fd, buffer, offset, length, position);
}
// tslint:disable-next-line no-any
public writeFile (path: fs.PathLike | number, data: any, options: IEncodingOptions): Promise<void> {
return promisify(fs.writeFile)(path, data, options);
}
public async watch(filename: fs.PathLike, options?: IEncodingOptions): Promise<WatcherProxy> {
return new WatcherProxy(fs.watch(filename, options));
}
private makeStatsSerializable(stats: fs.Stats): Stats {
return {
...stats,
/**
* We need to check if functions exist because nexe's implemented FS
* lib doesnt implement fs.stats properly.
*/
_isBlockDevice: stats.isBlockDevice ? stats.isBlockDevice() : false,
_isCharacterDevice: stats.isCharacterDevice ? stats.isCharacterDevice() : false,
_isDirectory: stats.isDirectory(),
_isFIFO: stats.isFIFO ? stats.isFIFO() : false,
_isFile: stats.isFile(),
_isSocket: stats.isSocket ? stats.isSocket() : false,
_isSymbolicLink: stats.isSymbolicLink ? stats.isSymbolicLink() : false,
};
}
}

View File

@ -0,0 +1,6 @@
export * from "./child_process";
export * from "./fs";
export * from "./net";
export * from "./node-pty";
export * from "./spdlog";
export * from "./trash";

View File

@ -0,0 +1,90 @@
import * as net from "net";
import { ServerProxy } from "../../common/proxy";
import { DuplexProxy } from "./stream";
export class NetSocketProxy extends DuplexProxy<net.Socket> {
public async connect(options: number | string | net.SocketConnectOpts, host?: string): Promise<void> {
this.stream.connect(options as any, host as any); // tslint:disable-line no-any this works fine
}
public async unref(): Promise<void> {
this.stream.unref();
}
public async ref(): Promise<void> {
this.stream.ref();
}
public async dispose(): Promise<void> {
this.stream.removeAllListeners();
this.stream.end();
this.stream.destroy();
this.stream.unref();
}
public async onDone(cb: () => void): Promise<void> {
this.stream.on("close", cb);
}
// tslint:disable-next-line no-any
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
super.onEvent(cb);
this.stream.on("connect", () => cb("connect"));
this.stream.on("lookup", (error, address, family, host) => cb("lookup", error, address, family, host));
this.stream.on("timeout", () => cb("timeout"));
}
}
export class NetServerProxy implements ServerProxy {
public constructor(private readonly server: net.Server) {}
public async listen(handle?: net.ListenOptions | number | string, hostname?: string | number, backlog?: number): Promise<void> {
this.server.listen(handle, hostname as any, backlog as any); // tslint:disable-line no-any this is fine
}
public async ref(): Promise<void> {
this.server.ref();
}
public async unref(): Promise<void> {
this.server.unref();
}
public async close(): Promise<void> {
this.server.close();
}
public async onConnection(cb: (proxy: NetSocketProxy) => void): Promise<void> {
this.server.on("connection", (socket) => cb(new NetSocketProxy(socket)));
}
public async dispose(): Promise<void> {
this.server.close();
this.server.removeAllListeners();
}
public async onDone(cb: () => void): Promise<void> {
this.server.on("close", cb);
}
// tslint:disable-next-line no-any
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
this.server.on("close", () => cb("close"));
this.server.on("error", (error) => cb("error", error));
this.server.on("listening", () => cb("listening"));
}
}
export class NetModuleProxy {
public async createSocket(options?: net.SocketConstructorOpts): Promise<NetSocketProxy> {
return new NetSocketProxy(new net.Socket(options));
}
public async createConnection(target: string | number | net.NetConnectOpts, host?: string): Promise<NetSocketProxy> {
return new NetSocketProxy(net.createConnection(target as any, host)); // tslint:disable-line no-any defeat stubborness
}
public async createServer(options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean }): Promise<NetServerProxy> {
return new NetServerProxy(net.createServer(options));
}
}

View File

@ -0,0 +1,75 @@
/// <reference path="../../../../../lib/vscode/src/typings/node-pty.d.ts" />
import { EventEmitter } from "events";
import * as pty from "node-pty";
import { ServerProxy } from "../../common/proxy";
import { preserveEnv } from "../../common/util";
/**
* Server-side IPty proxy.
*/
export class NodePtyProcessProxy implements ServerProxy {
private readonly emitter = new EventEmitter();
public constructor(private readonly process: pty.IPty) {
let name = process.process;
setTimeout(() => { // Need to wait for the caller to listen to the event.
this.emitter.emit("process", name);
}, 1);
const timer = setInterval(() => {
if (process.process !== name) {
name = process.process;
this.emitter.emit("process", name);
}
}, 200);
this.onDone(() => clearInterval(timer));
}
public async getPid(): Promise<number> {
return this.process.pid;
}
public async getProcess(): Promise<string> {
return this.process.process;
}
public async kill(signal?: string): Promise<void> {
this.process.kill(signal);
}
public async resize(columns: number, rows: number): Promise<void> {
this.process.resize(columns, rows);
}
public async write(data: string): Promise<void> {
this.process.write(data);
}
public async onDone(cb: () => void): Promise<void> {
this.process.on("exit", cb);
}
public async dispose(): Promise<void> {
this.kill();
setTimeout(() => this.kill("SIGKILL"), 5000); // Double tap.
this.emitter.removeAllListeners();
}
// tslint:disable-next-line no-any
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
this.emitter.on("process", (process) => cb("process", process));
this.process.on("data", (data) => cb("data", data));
this.process.on("exit", (exitCode, signal) => cb("exit", exitCode, signal));
}
}
/**
* Server-side node-pty proxy.
*/
export class NodePtyModuleProxy {
public async spawn(file: string, args: string[] | string, options: pty.IPtyForkOptions): Promise<NodePtyProcessProxy> {
preserveEnv(options);
return new NodePtyProcessProxy(require("node-pty").spawn(file, args, options));
}
}

View File

@ -0,0 +1,46 @@
/// <reference path="../../../../../lib/vscode/src/typings/spdlog.d.ts" />
import { EventEmitter } from "events";
import * as spdlog from "spdlog";
import { ServerProxy } from "../../common/proxy";
export class RotatingLoggerProxy implements ServerProxy {
private readonly emitter = new EventEmitter();
public constructor(private readonly logger: spdlog.RotatingLogger) {}
public async trace (message: string): Promise<void> { this.logger.trace(message); }
public async debug (message: string): Promise<void> { this.logger.debug(message); }
public async info (message: string): Promise<void> { this.logger.info(message); }
public async warn (message: string): Promise<void> { this.logger.warn(message); }
public async error (message: string): Promise<void> { this.logger.error(message); }
public async critical (message: string): Promise<void> { this.logger.critical(message); }
public async setLevel (level: number): Promise<void> { this.logger.setLevel(level); }
public async clearFormatters (): Promise<void> { this.logger.clearFormatters(); }
public async flush (): Promise<void> { this.logger.flush(); }
public async drop (): Promise<void> { this.logger.drop(); }
public async onDone(cb: () => void): Promise<void> {
this.emitter.on("dispose", cb);
}
public async dispose(): Promise<void> {
this.flush();
this.emitter.emit("dispose");
this.emitter.removeAllListeners();
}
// tslint:disable-next-line no-any
public async onEvent(_cb: (event: string, ...args: any[]) => void): Promise<void> {
// No events.
}
}
export class SpdlogModuleProxy {
public async createLogger(name: string, filePath: string, fileSize: number, fileCount: number): Promise<RotatingLoggerProxy> {
return new RotatingLoggerProxy(new (require("spdlog") as typeof import("spdlog")).RotatingLogger(name, filePath, fileSize, fileCount));
}
public async setAsyncMode(bufferSize: number, flushInterval: number): Promise<void> {
require("spdlog").setAsyncMode(bufferSize, flushInterval);
}
}

View File

@ -0,0 +1,107 @@
import * as stream from "stream";
import { ServerProxy } from "../../common/proxy";
export class WritableProxy<T extends stream.Writable = stream.Writable> implements ServerProxy {
public constructor(protected readonly stream: T) {}
public async destroy(): Promise<void> {
this.stream.destroy();
}
// tslint:disable-next-line no-any
public async end(data?: any, encoding?: string): Promise<void> {
return new Promise((resolve): void => {
this.stream.end(data, encoding, () => {
resolve();
});
});
}
public async setDefaultEncoding(encoding: string): Promise<void> {
this.stream.setDefaultEncoding(encoding);
}
// tslint:disable-next-line no-any
public async write(data: any, encoding?: string): Promise<void> {
return new Promise((resolve, reject): void => {
this.stream.write(data, encoding, (error) => {
if (error) {
reject(error);
} else {
resolve();
}
});
});
}
public async dispose(): Promise<void> {
this.stream.end();
this.stream.removeAllListeners();
}
public async onDone(cb: () => void): Promise<void> {
this.stream.on("close", cb);
}
// tslint:disable-next-line no-any
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
// Sockets have an extra argument on "close".
// tslint:disable-next-line no-any
this.stream.on("close", (...args: any[]) => cb("close", ...args));
this.stream.on("drain", () => cb("drain"));
this.stream.on("error", (error) => cb("error", error));
this.stream.on("finish", () => cb("finish"));
}
}
/**
* This noise is because we can't do multiple extends and we also can't seem to
* do `extends WritableProxy<T> implement ReadableProxy<T>` (for `DuplexProxy`).
*/
export interface IReadableProxy extends ServerProxy {
destroy(): Promise<void>;
setEncoding(encoding: string): Promise<void>;
dispose(): Promise<void>;
onDone(cb: () => void): Promise<void>;
}
export class ReadableProxy<T extends stream.Readable = stream.Readable> implements IReadableProxy {
public constructor(protected readonly stream: T) {}
public async destroy(): Promise<void> {
this.stream.destroy();
}
public async setEncoding(encoding: string): Promise<void> {
this.stream.setEncoding(encoding);
}
public async dispose(): Promise<void> {
this.stream.destroy();
}
public async onDone(cb: () => void): Promise<void> {
this.stream.on("close", cb);
}
// tslint:disable-next-line no-any
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
this.stream.on("close", () => cb("close"));
this.stream.on("data", (chunk) => cb("data", chunk));
this.stream.on("end", () => cb("end"));
this.stream.on("error", (error) => cb("error", error));
}
}
export class DuplexProxy<T extends stream.Duplex = stream.Duplex> extends WritableProxy<T> implements IReadableProxy {
public async setEncoding(encoding: string): Promise<void> {
this.stream.setEncoding(encoding);
}
// tslint:disable-next-line no-any
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
super.onEvent(cb);
this.stream.on("data", (chunk) => cb("data", chunk));
this.stream.on("end", () => cb("end"));
}
}

View File

@ -0,0 +1,7 @@
import * as trash from "trash";
export class TrashModuleProxy {
public async trash(path: string, options?: trash.Options): Promise<void> {
return trash(path, options);
}
}

View File

@ -1,10 +1,13 @@
import { mkdirp } from "fs-extra";
import * as os from "os";
import { logger, field } from "@coder/logger";
import { Pong, ClientMessage, WorkingInitMessage, ServerMessage } from "../proto";
import { evaluate, ActiveEvaluation } from "./evaluate";
import { ForkProvider } from "../common/helpers";
import { field, logger} from "@coder/logger";
import { ReadWriteConnection } from "../common/connection";
import { Module, ServerProxy } from "../common/proxy";
import { isPromise, isProxy, moduleToProto, parse, platformToProto, protoToModule, stringify } from "../common/util";
import { CallbackMessage, ClientMessage, EventMessage, FailMessage, MethodMessage, NamedCallbackMessage, NamedEventMessage, NumberedCallbackMessage, NumberedEventMessage, Pong, ServerMessage, SuccessMessage, WorkingInitMessage } from "../proto";
import { ChildProcessModuleProxy, ForkProvider, FsModuleProxy, NetModuleProxy, NodePtyModuleProxy, SpdlogModuleProxy, TrashModuleProxy } from "./modules";
// tslint:disable no-any
export interface ServerOptions {
readonly workingDirectory: string;
@ -14,27 +17,59 @@ export interface ServerOptions {
readonly fork?: ForkProvider;
}
interface ProxyData {
disposeTimeout?: number | NodeJS.Timer;
instance: any;
}
export class Server {
private readonly evals = new Map<number, ActiveEvaluation>();
private proxyId = 0;
private readonly proxies = new Map<number | Module, ProxyData>();
private disconnected: boolean = false;
private responseTimeout = 10000;
public constructor(
private readonly connection: ReadWriteConnection,
private readonly options?: ServerOptions,
) {
connection.onMessage((data) => {
connection.onMessage(async (data) => {
try {
this.handleMessage(ClientMessage.deserializeBinary(data));
await this.handleMessage(ClientMessage.deserializeBinary(data));
} catch (ex) {
logger.error("Failed to handle client message", field("length", data.byteLength), field("exception", {
message: ex.message,
stack: ex.stack,
}));
logger.error(
"Failed to handle client message",
field("length", data.byteLength),
field("exception", {
message: ex.message,
stack: ex.stack,
}),
);
}
});
connection.onClose(() => {
this.evals.forEach((e) => e.dispose());
this.disconnected = true;
logger.trace(() => [
"disconnected from client",
field("proxies", this.proxies.size),
]);
this.proxies.forEach((proxy, proxyId) => {
if (isProxy(proxy.instance)) {
proxy.instance.dispose();
}
this.removeProxy(proxyId);
});
});
this.storeProxy(new ChildProcessModuleProxy(this.options ? this.options.fork : undefined), Module.ChildProcess);
this.storeProxy(new FsModuleProxy(), Module.Fs);
this.storeProxy(new NetModuleProxy(), Module.Net);
this.storeProxy(new NodePtyModuleProxy(), Module.NodePty);
this.storeProxy(new SpdlogModuleProxy(), Module.Spdlog);
this.storeProxy(new TrashModuleProxy(), Module.Trash);
if (!this.options) {
logger.warn("No server options provided. InitMessage will not be sent.");
@ -55,53 +90,19 @@ export class Server {
initMsg.setBuiltinExtensionsDir(this.options.builtInExtensionsDirectory);
initMsg.setHomeDirectory(os.homedir());
initMsg.setTmpDirectory(os.tmpdir());
const platform = os.platform();
let operatingSystem: WorkingInitMessage.OperatingSystem;
switch (platform) {
case "win32":
operatingSystem = WorkingInitMessage.OperatingSystem.WINDOWS;
break;
case "linux":
operatingSystem = WorkingInitMessage.OperatingSystem.LINUX;
break;
case "darwin":
operatingSystem = WorkingInitMessage.OperatingSystem.MAC;
break;
default:
throw new Error(`unrecognized platform "${platform}"`);
}
initMsg.setOperatingSystem(operatingSystem);
initMsg.setOperatingSystem(platformToProto(os.platform()));
initMsg.setShell(os.userInfo().shell || global.process.env.SHELL);
const srvMsg = new ServerMessage();
srvMsg.setInit(initMsg);
connection.send(srvMsg.serializeBinary());
}
private handleMessage(message: ClientMessage): void {
if (message.hasNewEval()) {
const evalMessage = message.getNewEval()!;
logger.trace(() => [
"EvalMessage",
field("id", evalMessage.getId()),
field("args", evalMessage.getArgsList()),
field("function", evalMessage.getFunction()),
]);
const resp = evaluate(this.connection, evalMessage, () => {
this.evals.delete(evalMessage.getId());
logger.trace(() => [
`dispose ${evalMessage.getId()}, ${this.evals.size} left`,
]);
}, this.options ? this.options.fork : undefined);
if (resp) {
this.evals.set(evalMessage.getId(), resp);
}
} else if (message.hasEvalEvent()) {
const evalEventMessage = message.getEvalEvent()!;
const e = this.evals.get(evalEventMessage.getId());
if (!e) {
return;
}
e.onEvent(evalEventMessage);
/**
* Handle all messages from the client.
*/
private async handleMessage(message: ClientMessage): Promise<void> {
if (message.hasMethod()) {
await this.runMethod(message.getMethod()!);
} else if (message.hasPing()) {
logger.trace("ping");
const srvMsg = new ServerMessage();
@ -111,4 +112,230 @@ export class Server {
throw new Error("unknown message type");
}
}
/**
* Run a method on a proxy.
*/
private async runMethod(message: MethodMessage): Promise<void> {
const proxyMessage = message.getNamedProxy()! || message.getNumberedProxy()!;
const id = proxyMessage.getId();
const proxyId = message.hasNamedProxy()
? protoToModule(message.getNamedProxy()!.getModule())
: message.getNumberedProxy()!.getProxyId();
const method = proxyMessage.getMethod();
const args = proxyMessage.getArgsList().map((a) => parse(
a,
(id, args) => this.sendCallback(proxyId, id, args),
));
logger.trace(() => [
"received",
field("id", id),
field("proxyId", proxyId),
field("method", method),
field("args", proxyMessage.getArgsList()),
]);
let response: any;
try {
const proxy = this.getProxy(proxyId);
if (typeof proxy.instance[method] !== "function") {
throw new Error(`"${method}" is not a function`);
}
response = proxy.instance[method](...args);
// We wait for the client to call "dispose" instead of doing it onDone to
// ensure all the messages it sent get processed before we get rid of it.
if (method === "dispose") {
this.removeProxy(proxyId);
}
// Proxies must always return promises.
if (!isPromise(response)) {
throw new Error('"${method}" must return a promise');
}
} catch (error) {
logger.error(
error.message,
field("type", typeof response),
field("proxyId", proxyId),
);
this.sendException(id, error);
}
try {
this.sendResponse(id, await response);
} catch (error) {
this.sendException(id, error);
}
}
/**
* Send a callback to the client.
*/
private sendCallback(proxyId: number | Module, callbackId: number, args: any[]): void {
const stringifiedArgs = args.map((a) => this.stringify(a));
logger.trace(() => [
"sending callback",
field("proxyId", proxyId),
field("callbackId", callbackId),
field("args", stringifiedArgs),
]);
const message = new CallbackMessage();
let callbackMessage: NamedCallbackMessage | NumberedCallbackMessage;
if (typeof proxyId === "string") {
callbackMessage = new NamedCallbackMessage();
callbackMessage.setModule(moduleToProto(proxyId));
message.setNamedCallback(callbackMessage);
} else {
callbackMessage = new NumberedCallbackMessage();
callbackMessage.setProxyId(proxyId);
message.setNumberedCallback(callbackMessage);
}
callbackMessage.setCallbackId(callbackId);
callbackMessage.setArgsList(stringifiedArgs);
const serverMessage = new ServerMessage();
serverMessage.setCallback(message);
this.connection.send(serverMessage.serializeBinary());
}
/**
* Store a proxy and bind events to send them back to the client.
*/
private storeProxy(instance: ServerProxy): number;
private storeProxy(instance: any, moduleProxyId: Module): Module;
private storeProxy(instance: ServerProxy | any, moduleProxyId?: Module): number | Module {
// In case we disposed while waiting for a function to return.
if (this.disconnected) {
if (isProxy(instance)) {
instance.dispose();
}
throw new Error("disposed");
}
const proxyId = moduleProxyId || this.proxyId++;
logger.trace(() => [
"storing proxy",
field("proxyId", proxyId),
]);
this.proxies.set(proxyId, { instance });
if (isProxy(instance)) {
instance.onEvent((event, ...args) => this.sendEvent(proxyId, event, ...args));
instance.onDone(() => {
// It might have finished because we disposed it due to a disconnect.
if (!this.disconnected) {
this.sendEvent(proxyId, "done");
this.getProxy(proxyId).disposeTimeout = setTimeout(() => {
instance.dispose();
this.removeProxy(proxyId);
}, this.responseTimeout);
}
});
}
return proxyId;
}
/**
* Send an event to the client.
*/
private sendEvent(proxyId: number | Module, event: string, ...args: any[]): void {
const stringifiedArgs = args.map((a) => this.stringify(a));
logger.trace(() => [
"sending event",
field("proxyId", proxyId),
field("event", event),
field("args", stringifiedArgs),
]);
const message = new EventMessage();
let eventMessage: NamedEventMessage | NumberedEventMessage;
if (typeof proxyId === "string") {
eventMessage = new NamedEventMessage();
eventMessage.setModule(moduleToProto(proxyId));
message.setNamedEvent(eventMessage);
} else {
eventMessage = new NumberedEventMessage();
eventMessage.setProxyId(proxyId);
message.setNumberedEvent(eventMessage);
}
eventMessage.setEvent(event);
eventMessage.setArgsList(stringifiedArgs);
const serverMessage = new ServerMessage();
serverMessage.setEvent(message);
this.connection.send(serverMessage.serializeBinary());
}
/**
* Send a response back to the client.
*/
private sendResponse(id: number, response: any): void {
const stringifiedResponse = this.stringify(response);
logger.trace(() => [
"sending resolve",
field("id", id),
field("response", stringifiedResponse),
]);
const successMessage = new SuccessMessage();
successMessage.setId(id);
successMessage.setResponse(stringifiedResponse);
const serverMessage = new ServerMessage();
serverMessage.setSuccess(successMessage);
this.connection.send(serverMessage.serializeBinary());
}
/**
* Send an exception back to the client.
*/
private sendException(id: number, error: Error): void {
const stringifiedError = stringify(error);
logger.trace(() => [
"sending reject",
field("id", id) ,
field("response", stringifiedError),
]);
const failedMessage = new FailMessage();
failedMessage.setId(id);
failedMessage.setResponse(stringifiedError);
const serverMessage = new ServerMessage();
serverMessage.setFail(failedMessage);
this.connection.send(serverMessage.serializeBinary());
}
/**
* Call after disposing a proxy.
*/
private removeProxy(proxyId: number | Module): void {
clearTimeout(this.getProxy(proxyId).disposeTimeout as any);
this.proxies.delete(proxyId);
logger.trace(() => [
"disposed and removed proxy",
field("proxyId", proxyId),
field("proxies", this.proxies.size),
]);
}
private stringify(value: any): string {
return stringify(value, undefined, (p) => this.storeProxy(p));
}
private getProxy(proxyId: number | Module): ProxyData {
if (!this.proxies.has(proxyId)) {
throw new Error(`proxy ${proxyId} disposed too early`);
}
return this.proxies.get(proxyId)!;
}
}

View File

@ -2,29 +2,29 @@ syntax = "proto3";
import "node.proto";
import "vscode.proto";
// Messages that the client can send to the server.
message ClientMessage {
oneof msg {
// node.proto
NewEvalMessage new_eval = 11;
EvalEventMessage eval_event = 12;
Ping ping = 13;
MethodMessage method = 20;
Ping ping = 21;
}
}
// Messages that the server can send to the client.
message ServerMessage {
oneof msg {
// node.proto
EvalFailedMessage eval_failed = 13;
EvalDoneMessage eval_done = 14;
EvalEventMessage eval_event = 15;
FailMessage fail = 13;
SuccessMessage success = 14;
EventMessage event = 19;
CallbackMessage callback = 22;
Pong pong = 18;
WorkingInitMessage init = 16;
// vscode.proto
SharedProcessActiveMessage shared_process_active = 17;
Pong pong = 18;
}
}

View File

@ -6,15 +6,10 @@ import * as node_pb from "./node_pb";
import * as vscode_pb from "./vscode_pb";
export class ClientMessage extends jspb.Message {
hasNewEval(): boolean;
clearNewEval(): void;
getNewEval(): node_pb.NewEvalMessage | undefined;
setNewEval(value?: node_pb.NewEvalMessage): void;
hasEvalEvent(): boolean;
clearEvalEvent(): void;
getEvalEvent(): node_pb.EvalEventMessage | undefined;
setEvalEvent(value?: node_pb.EvalEventMessage): void;
hasMethod(): boolean;
clearMethod(): void;
getMethod(): node_pb.MethodMessage | undefined;
setMethod(value?: node_pb.MethodMessage): void;
hasPing(): boolean;
clearPing(): void;
@ -34,34 +29,42 @@ export class ClientMessage extends jspb.Message {
export namespace ClientMessage {
export type AsObject = {
newEval?: node_pb.NewEvalMessage.AsObject,
evalEvent?: node_pb.EvalEventMessage.AsObject,
method?: node_pb.MethodMessage.AsObject,
ping?: node_pb.Ping.AsObject,
}
export enum MsgCase {
MSG_NOT_SET = 0,
NEW_EVAL = 11,
EVAL_EVENT = 12,
PING = 13,
METHOD = 20,
PING = 21,
}
}
export class ServerMessage extends jspb.Message {
hasEvalFailed(): boolean;
clearEvalFailed(): void;
getEvalFailed(): node_pb.EvalFailedMessage | undefined;
setEvalFailed(value?: node_pb.EvalFailedMessage): void;
hasFail(): boolean;
clearFail(): void;
getFail(): node_pb.FailMessage | undefined;
setFail(value?: node_pb.FailMessage): void;
hasEvalDone(): boolean;
clearEvalDone(): void;
getEvalDone(): node_pb.EvalDoneMessage | undefined;
setEvalDone(value?: node_pb.EvalDoneMessage): void;
hasSuccess(): boolean;
clearSuccess(): void;
getSuccess(): node_pb.SuccessMessage | undefined;
setSuccess(value?: node_pb.SuccessMessage): void;
hasEvalEvent(): boolean;
clearEvalEvent(): void;
getEvalEvent(): node_pb.EvalEventMessage | undefined;
setEvalEvent(value?: node_pb.EvalEventMessage): void;
hasEvent(): boolean;
clearEvent(): void;
getEvent(): node_pb.EventMessage | undefined;
setEvent(value?: node_pb.EventMessage): void;
hasCallback(): boolean;
clearCallback(): void;
getCallback(): node_pb.CallbackMessage | undefined;
setCallback(value?: node_pb.CallbackMessage): void;
hasPong(): boolean;
clearPong(): void;
getPong(): node_pb.Pong | undefined;
setPong(value?: node_pb.Pong): void;
hasInit(): boolean;
clearInit(): void;
@ -73,11 +76,6 @@ export class ServerMessage extends jspb.Message {
getSharedProcessActive(): vscode_pb.SharedProcessActiveMessage | undefined;
setSharedProcessActive(value?: vscode_pb.SharedProcessActiveMessage): void;
hasPong(): boolean;
clearPong(): void;
getPong(): node_pb.Pong | undefined;
setPong(value?: node_pb.Pong): void;
getMsgCase(): ServerMessage.MsgCase;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): ServerMessage.AsObject;
@ -91,22 +89,24 @@ export class ServerMessage extends jspb.Message {
export namespace ServerMessage {
export type AsObject = {
evalFailed?: node_pb.EvalFailedMessage.AsObject,
evalDone?: node_pb.EvalDoneMessage.AsObject,
evalEvent?: node_pb.EvalEventMessage.AsObject,
fail?: node_pb.FailMessage.AsObject,
success?: node_pb.SuccessMessage.AsObject,
event?: node_pb.EventMessage.AsObject,
callback?: node_pb.CallbackMessage.AsObject,
pong?: node_pb.Pong.AsObject,
init?: WorkingInitMessage.AsObject,
sharedProcessActive?: vscode_pb.SharedProcessActiveMessage.AsObject,
pong?: node_pb.Pong.AsObject,
}
export enum MsgCase {
MSG_NOT_SET = 0,
EVAL_FAILED = 13,
EVAL_DONE = 14,
EVAL_EVENT = 15,
FAIL = 13,
SUCCESS = 14,
EVENT = 19,
CALLBACK = 22,
PONG = 18,
INIT = 16,
SHARED_PROCESS_ACTIVE = 17,
PONG = 18,
}
}

View File

@ -43,16 +43,15 @@ if (goog.DEBUG && !COMPILED) {
* @private {!Array<!Array<number>>}
* @const
*/
proto.ClientMessage.oneofGroups_ = [[11,12,13]];
proto.ClientMessage.oneofGroups_ = [[20,21]];
/**
* @enum {number}
*/
proto.ClientMessage.MsgCase = {
MSG_NOT_SET: 0,
NEW_EVAL: 11,
EVAL_EVENT: 12,
PING: 13
METHOD: 20,
PING: 21
};
/**
@ -91,8 +90,7 @@ proto.ClientMessage.prototype.toObject = function(opt_includeInstance) {
*/
proto.ClientMessage.toObject = function(includeInstance, msg) {
var f, obj = {
newEval: (f = msg.getNewEval()) && node_pb.NewEvalMessage.toObject(includeInstance, f),
evalEvent: (f = msg.getEvalEvent()) && node_pb.EvalEventMessage.toObject(includeInstance, f),
method: (f = msg.getMethod()) && node_pb.MethodMessage.toObject(includeInstance, f),
ping: (f = msg.getPing()) && node_pb.Ping.toObject(includeInstance, f)
};
@ -130,17 +128,12 @@ proto.ClientMessage.deserializeBinaryFromReader = function(msg, reader) {
}
var field = reader.getFieldNumber();
switch (field) {
case 11:
var value = new node_pb.NewEvalMessage;
reader.readMessage(value,node_pb.NewEvalMessage.deserializeBinaryFromReader);
msg.setNewEval(value);
case 20:
var value = new node_pb.MethodMessage;
reader.readMessage(value,node_pb.MethodMessage.deserializeBinaryFromReader);
msg.setMethod(value);
break;
case 12:
var value = new node_pb.EvalEventMessage;
reader.readMessage(value,node_pb.EvalEventMessage.deserializeBinaryFromReader);
msg.setEvalEvent(value);
break;
case 13:
case 21:
var value = new node_pb.Ping;
reader.readMessage(value,node_pb.Ping.deserializeBinaryFromReader);
msg.setPing(value);
@ -174,26 +167,18 @@ proto.ClientMessage.prototype.serializeBinary = function() {
*/
proto.ClientMessage.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getNewEval();
f = message.getMethod();
if (f != null) {
writer.writeMessage(
11,
20,
f,
node_pb.NewEvalMessage.serializeBinaryToWriter
);
}
f = message.getEvalEvent();
if (f != null) {
writer.writeMessage(
12,
f,
node_pb.EvalEventMessage.serializeBinaryToWriter
node_pb.MethodMessage.serializeBinaryToWriter
);
}
f = message.getPing();
if (f != null) {
writer.writeMessage(
13,
21,
f,
node_pb.Ping.serializeBinaryToWriter
);
@ -202,23 +187,23 @@ proto.ClientMessage.serializeBinaryToWriter = function(message, writer) {
/**
* optional NewEvalMessage new_eval = 11;
* @return {?proto.NewEvalMessage}
* optional MethodMessage method = 20;
* @return {?proto.MethodMessage}
*/
proto.ClientMessage.prototype.getNewEval = function() {
return /** @type{?proto.NewEvalMessage} */ (
jspb.Message.getWrapperField(this, node_pb.NewEvalMessage, 11));
proto.ClientMessage.prototype.getMethod = function() {
return /** @type{?proto.MethodMessage} */ (
jspb.Message.getWrapperField(this, node_pb.MethodMessage, 20));
};
/** @param {?proto.NewEvalMessage|undefined} value */
proto.ClientMessage.prototype.setNewEval = function(value) {
jspb.Message.setOneofWrapperField(this, 11, proto.ClientMessage.oneofGroups_[0], value);
/** @param {?proto.MethodMessage|undefined} value */
proto.ClientMessage.prototype.setMethod = function(value) {
jspb.Message.setOneofWrapperField(this, 20, proto.ClientMessage.oneofGroups_[0], value);
};
proto.ClientMessage.prototype.clearNewEval = function() {
this.setNewEval(undefined);
proto.ClientMessage.prototype.clearMethod = function() {
this.setMethod(undefined);
};
@ -226,54 +211,24 @@ proto.ClientMessage.prototype.clearNewEval = function() {
* Returns whether this field is set.
* @return {!boolean}
*/
proto.ClientMessage.prototype.hasNewEval = function() {
return jspb.Message.getField(this, 11) != null;
proto.ClientMessage.prototype.hasMethod = function() {
return jspb.Message.getField(this, 20) != null;
};
/**
* optional EvalEventMessage eval_event = 12;
* @return {?proto.EvalEventMessage}
*/
proto.ClientMessage.prototype.getEvalEvent = function() {
return /** @type{?proto.EvalEventMessage} */ (
jspb.Message.getWrapperField(this, node_pb.EvalEventMessage, 12));
};
/** @param {?proto.EvalEventMessage|undefined} value */
proto.ClientMessage.prototype.setEvalEvent = function(value) {
jspb.Message.setOneofWrapperField(this, 12, proto.ClientMessage.oneofGroups_[0], value);
};
proto.ClientMessage.prototype.clearEvalEvent = function() {
this.setEvalEvent(undefined);
};
/**
* Returns whether this field is set.
* @return {!boolean}
*/
proto.ClientMessage.prototype.hasEvalEvent = function() {
return jspb.Message.getField(this, 12) != null;
};
/**
* optional Ping ping = 13;
* optional Ping ping = 21;
* @return {?proto.Ping}
*/
proto.ClientMessage.prototype.getPing = function() {
return /** @type{?proto.Ping} */ (
jspb.Message.getWrapperField(this, node_pb.Ping, 13));
jspb.Message.getWrapperField(this, node_pb.Ping, 21));
};
/** @param {?proto.Ping|undefined} value */
proto.ClientMessage.prototype.setPing = function(value) {
jspb.Message.setOneofWrapperField(this, 13, proto.ClientMessage.oneofGroups_[0], value);
jspb.Message.setOneofWrapperField(this, 21, proto.ClientMessage.oneofGroups_[0], value);
};
@ -287,7 +242,7 @@ proto.ClientMessage.prototype.clearPing = function() {
* @return {!boolean}
*/
proto.ClientMessage.prototype.hasPing = function() {
return jspb.Message.getField(this, 13) != null;
return jspb.Message.getField(this, 21) != null;
};
@ -317,19 +272,20 @@ if (goog.DEBUG && !COMPILED) {
* @private {!Array<!Array<number>>}
* @const
*/
proto.ServerMessage.oneofGroups_ = [[13,14,15,16,17,18]];
proto.ServerMessage.oneofGroups_ = [[13,14,19,22,18,16,17]];
/**
* @enum {number}
*/
proto.ServerMessage.MsgCase = {
MSG_NOT_SET: 0,
EVAL_FAILED: 13,
EVAL_DONE: 14,
EVAL_EVENT: 15,
FAIL: 13,
SUCCESS: 14,
EVENT: 19,
CALLBACK: 22,
PONG: 18,
INIT: 16,
SHARED_PROCESS_ACTIVE: 17,
PONG: 18
SHARED_PROCESS_ACTIVE: 17
};
/**
@ -368,12 +324,13 @@ proto.ServerMessage.prototype.toObject = function(opt_includeInstance) {
*/
proto.ServerMessage.toObject = function(includeInstance, msg) {
var f, obj = {
evalFailed: (f = msg.getEvalFailed()) && node_pb.EvalFailedMessage.toObject(includeInstance, f),
evalDone: (f = msg.getEvalDone()) && node_pb.EvalDoneMessage.toObject(includeInstance, f),
evalEvent: (f = msg.getEvalEvent()) && node_pb.EvalEventMessage.toObject(includeInstance, f),
fail: (f = msg.getFail()) && node_pb.FailMessage.toObject(includeInstance, f),
success: (f = msg.getSuccess()) && node_pb.SuccessMessage.toObject(includeInstance, f),
event: (f = msg.getEvent()) && node_pb.EventMessage.toObject(includeInstance, f),
callback: (f = msg.getCallback()) && node_pb.CallbackMessage.toObject(includeInstance, f),
pong: (f = msg.getPong()) && node_pb.Pong.toObject(includeInstance, f),
init: (f = msg.getInit()) && proto.WorkingInitMessage.toObject(includeInstance, f),
sharedProcessActive: (f = msg.getSharedProcessActive()) && vscode_pb.SharedProcessActiveMessage.toObject(includeInstance, f),
pong: (f = msg.getPong()) && node_pb.Pong.toObject(includeInstance, f)
sharedProcessActive: (f = msg.getSharedProcessActive()) && vscode_pb.SharedProcessActiveMessage.toObject(includeInstance, f)
};
if (includeInstance) {
@ -411,19 +368,29 @@ proto.ServerMessage.deserializeBinaryFromReader = function(msg, reader) {
var field = reader.getFieldNumber();
switch (field) {
case 13:
var value = new node_pb.EvalFailedMessage;
reader.readMessage(value,node_pb.EvalFailedMessage.deserializeBinaryFromReader);
msg.setEvalFailed(value);
var value = new node_pb.FailMessage;
reader.readMessage(value,node_pb.FailMessage.deserializeBinaryFromReader);
msg.setFail(value);
break;
case 14:
var value = new node_pb.EvalDoneMessage;
reader.readMessage(value,node_pb.EvalDoneMessage.deserializeBinaryFromReader);
msg.setEvalDone(value);
var value = new node_pb.SuccessMessage;
reader.readMessage(value,node_pb.SuccessMessage.deserializeBinaryFromReader);
msg.setSuccess(value);
break;
case 15:
var value = new node_pb.EvalEventMessage;
reader.readMessage(value,node_pb.EvalEventMessage.deserializeBinaryFromReader);
msg.setEvalEvent(value);
case 19:
var value = new node_pb.EventMessage;
reader.readMessage(value,node_pb.EventMessage.deserializeBinaryFromReader);
msg.setEvent(value);
break;
case 22:
var value = new node_pb.CallbackMessage;
reader.readMessage(value,node_pb.CallbackMessage.deserializeBinaryFromReader);
msg.setCallback(value);
break;
case 18:
var value = new node_pb.Pong;
reader.readMessage(value,node_pb.Pong.deserializeBinaryFromReader);
msg.setPong(value);
break;
case 16:
var value = new proto.WorkingInitMessage;
@ -435,11 +402,6 @@ proto.ServerMessage.deserializeBinaryFromReader = function(msg, reader) {
reader.readMessage(value,vscode_pb.SharedProcessActiveMessage.deserializeBinaryFromReader);
msg.setSharedProcessActive(value);
break;
case 18:
var value = new node_pb.Pong;
reader.readMessage(value,node_pb.Pong.deserializeBinaryFromReader);
msg.setPong(value);
break;
default:
reader.skipField();
break;
@ -469,28 +431,44 @@ proto.ServerMessage.prototype.serializeBinary = function() {
*/
proto.ServerMessage.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
f = message.getEvalFailed();
f = message.getFail();
if (f != null) {
writer.writeMessage(
13,
f,
node_pb.EvalFailedMessage.serializeBinaryToWriter
node_pb.FailMessage.serializeBinaryToWriter
);
}
f = message.getEvalDone();
f = message.getSuccess();
if (f != null) {
writer.writeMessage(
14,
f,
node_pb.EvalDoneMessage.serializeBinaryToWriter
node_pb.SuccessMessage.serializeBinaryToWriter
);
}
f = message.getEvalEvent();
f = message.getEvent();
if (f != null) {
writer.writeMessage(
15,
19,
f,
node_pb.EvalEventMessage.serializeBinaryToWriter
node_pb.EventMessage.serializeBinaryToWriter
);
}
f = message.getCallback();
if (f != null) {
writer.writeMessage(
22,
f,
node_pb.CallbackMessage.serializeBinaryToWriter
);
}
f = message.getPong();
if (f != null) {
writer.writeMessage(
18,
f,
node_pb.Pong.serializeBinaryToWriter
);
}
f = message.getInit();
@ -509,35 +487,27 @@ proto.ServerMessage.serializeBinaryToWriter = function(message, writer) {
vscode_pb.SharedProcessActiveMessage.serializeBinaryToWriter
);
}
f = message.getPong();
if (f != null) {
writer.writeMessage(
18,
f,
node_pb.Pong.serializeBinaryToWriter
);
}
};
/**
* optional EvalFailedMessage eval_failed = 13;
* @return {?proto.EvalFailedMessage}
* optional FailMessage fail = 13;
* @return {?proto.FailMessage}
*/
proto.ServerMessage.prototype.getEvalFailed = function() {
return /** @type{?proto.EvalFailedMessage} */ (
jspb.Message.getWrapperField(this, node_pb.EvalFailedMessage, 13));
proto.ServerMessage.prototype.getFail = function() {
return /** @type{?proto.FailMessage} */ (
jspb.Message.getWrapperField(this, node_pb.FailMessage, 13));
};
/** @param {?proto.EvalFailedMessage|undefined} value */
proto.ServerMessage.prototype.setEvalFailed = function(value) {
/** @param {?proto.FailMessage|undefined} value */
proto.ServerMessage.prototype.setFail = function(value) {
jspb.Message.setOneofWrapperField(this, 13, proto.ServerMessage.oneofGroups_[0], value);
};
proto.ServerMessage.prototype.clearEvalFailed = function() {
this.setEvalFailed(undefined);
proto.ServerMessage.prototype.clearFail = function() {
this.setFail(undefined);
};
@ -545,29 +515,29 @@ proto.ServerMessage.prototype.clearEvalFailed = function() {
* Returns whether this field is set.
* @return {!boolean}
*/
proto.ServerMessage.prototype.hasEvalFailed = function() {
proto.ServerMessage.prototype.hasFail = function() {
return jspb.Message.getField(this, 13) != null;
};
/**
* optional EvalDoneMessage eval_done = 14;
* @return {?proto.EvalDoneMessage}
* optional SuccessMessage success = 14;
* @return {?proto.SuccessMessage}
*/
proto.ServerMessage.prototype.getEvalDone = function() {
return /** @type{?proto.EvalDoneMessage} */ (
jspb.Message.getWrapperField(this, node_pb.EvalDoneMessage, 14));
proto.ServerMessage.prototype.getSuccess = function() {
return /** @type{?proto.SuccessMessage} */ (
jspb.Message.getWrapperField(this, node_pb.SuccessMessage, 14));
};
/** @param {?proto.EvalDoneMessage|undefined} value */
proto.ServerMessage.prototype.setEvalDone = function(value) {
/** @param {?proto.SuccessMessage|undefined} value */
proto.ServerMessage.prototype.setSuccess = function(value) {
jspb.Message.setOneofWrapperField(this, 14, proto.ServerMessage.oneofGroups_[0], value);
};
proto.ServerMessage.prototype.clearEvalDone = function() {
this.setEvalDone(undefined);
proto.ServerMessage.prototype.clearSuccess = function() {
this.setSuccess(undefined);
};
@ -575,29 +545,29 @@ proto.ServerMessage.prototype.clearEvalDone = function() {
* Returns whether this field is set.
* @return {!boolean}
*/
proto.ServerMessage.prototype.hasEvalDone = function() {
proto.ServerMessage.prototype.hasSuccess = function() {
return jspb.Message.getField(this, 14) != null;
};
/**
* optional EvalEventMessage eval_event = 15;
* @return {?proto.EvalEventMessage}
* optional EventMessage event = 19;
* @return {?proto.EventMessage}
*/
proto.ServerMessage.prototype.getEvalEvent = function() {
return /** @type{?proto.EvalEventMessage} */ (
jspb.Message.getWrapperField(this, node_pb.EvalEventMessage, 15));
proto.ServerMessage.prototype.getEvent = function() {
return /** @type{?proto.EventMessage} */ (
jspb.Message.getWrapperField(this, node_pb.EventMessage, 19));
};
/** @param {?proto.EvalEventMessage|undefined} value */
proto.ServerMessage.prototype.setEvalEvent = function(value) {
jspb.Message.setOneofWrapperField(this, 15, proto.ServerMessage.oneofGroups_[0], value);
/** @param {?proto.EventMessage|undefined} value */
proto.ServerMessage.prototype.setEvent = function(value) {
jspb.Message.setOneofWrapperField(this, 19, proto.ServerMessage.oneofGroups_[0], value);
};
proto.ServerMessage.prototype.clearEvalEvent = function() {
this.setEvalEvent(undefined);
proto.ServerMessage.prototype.clearEvent = function() {
this.setEvent(undefined);
};
@ -605,8 +575,68 @@ proto.ServerMessage.prototype.clearEvalEvent = function() {
* Returns whether this field is set.
* @return {!boolean}
*/
proto.ServerMessage.prototype.hasEvalEvent = function() {
return jspb.Message.getField(this, 15) != null;
proto.ServerMessage.prototype.hasEvent = function() {
return jspb.Message.getField(this, 19) != null;
};
/**
* optional CallbackMessage callback = 22;
* @return {?proto.CallbackMessage}
*/
proto.ServerMessage.prototype.getCallback = function() {
return /** @type{?proto.CallbackMessage} */ (
jspb.Message.getWrapperField(this, node_pb.CallbackMessage, 22));
};
/** @param {?proto.CallbackMessage|undefined} value */
proto.ServerMessage.prototype.setCallback = function(value) {
jspb.Message.setOneofWrapperField(this, 22, proto.ServerMessage.oneofGroups_[0], value);
};
proto.ServerMessage.prototype.clearCallback = function() {
this.setCallback(undefined);
};
/**
* Returns whether this field is set.
* @return {!boolean}
*/
proto.ServerMessage.prototype.hasCallback = function() {
return jspb.Message.getField(this, 22) != null;
};
/**
* optional Pong pong = 18;
* @return {?proto.Pong}
*/
proto.ServerMessage.prototype.getPong = function() {
return /** @type{?proto.Pong} */ (
jspb.Message.getWrapperField(this, node_pb.Pong, 18));
};
/** @param {?proto.Pong|undefined} value */
proto.ServerMessage.prototype.setPong = function(value) {
jspb.Message.setOneofWrapperField(this, 18, proto.ServerMessage.oneofGroups_[0], value);
};
proto.ServerMessage.prototype.clearPong = function() {
this.setPong(undefined);
};
/**
* Returns whether this field is set.
* @return {!boolean}
*/
proto.ServerMessage.prototype.hasPong = function() {
return jspb.Message.getField(this, 18) != null;
};
@ -670,36 +700,6 @@ proto.ServerMessage.prototype.hasSharedProcessActive = function() {
};
/**
* optional Pong pong = 18;
* @return {?proto.Pong}
*/
proto.ServerMessage.prototype.getPong = function() {
return /** @type{?proto.Pong} */ (
jspb.Message.getWrapperField(this, node_pb.Pong, 18));
};
/** @param {?proto.Pong|undefined} value */
proto.ServerMessage.prototype.setPong = function(value) {
jspb.Message.setOneofWrapperField(this, 18, proto.ServerMessage.oneofGroups_[0], value);
};
proto.ServerMessage.prototype.clearPong = function() {
this.setPong(undefined);
};
/**
* Returns whether this field is set.
* @return {!boolean}
*/
proto.ServerMessage.prototype.hasPong = function() {
return jspb.Message.getField(this, 18) != null;
};
/**
* Generated by JsPbCodeGenerator.

View File

@ -1,28 +1,90 @@
syntax = "proto3";
message NewEvalMessage {
uint64 id = 1;
string function = 2;
repeated string args = 3;
// Timeout in ms
uint32 timeout = 4;
// Create active eval message.
// Allows for dynamic communication for an eval
bool active = 5;
enum Module {
ChildProcess = 0;
Fs = 1;
Net = 2;
NodePty = 3;
Spdlog = 4;
Trash = 5;
}
message EvalEventMessage {
// A proxy identified by a unique name like "fs".
message NamedProxyMessage {
uint64 id = 1;
Module module = 2;
string method = 3;
repeated string args = 4;
}
// A general proxy identified by an ID like WriteStream.
message NumberedProxyMessage {
uint64 id = 1;
uint64 proxy_id = 2;
string method = 3;
repeated string args = 4;
}
// Call a remote method.
message MethodMessage {
oneof msg {
NamedProxyMessage named_proxy = 1;
NumberedProxyMessage numbered_proxy = 2;
}
}
// Call a remote callback.
message CallbackMessage {
oneof msg {
NamedCallbackMessage named_callback = 1;
NumberedCallbackMessage numbered_callback = 2;
}
}
// A remote callback for uniquely named proxy.
message NamedCallbackMessage {
Module module = 1;
uint64 callback_id = 2;
repeated string args = 3;
}
// A remote callback for a numbered proxy.
message NumberedCallbackMessage {
uint64 proxy_id = 1;
uint64 callback_id = 2;
repeated string args = 3;
}
// Emit an event.
message EventMessage {
oneof msg {
NamedEventMessage named_event = 1;
NumberedEventMessage numbered_event = 2;
}
}
// Emit an event on a uniquely named proxy.
message NamedEventMessage {
Module module = 1;
string event = 2;
repeated string args = 3;
}
message EvalFailedMessage {
// Emit an event on a numbered proxy.
message NumberedEventMessage {
uint64 proxy_id = 1;
string event = 2;
repeated string args = 3;
}
// Remote method failed.
message FailMessage {
uint64 id = 1;
string response = 2;
}
message EvalDoneMessage {
// Remote method succeeded.
message SuccessMessage {
uint64 id = 1;
string response = 2;
}

View File

@ -3,48 +3,243 @@
import * as jspb from "google-protobuf";
export class NewEvalMessage extends jspb.Message {
export class NamedProxyMessage extends jspb.Message {
getId(): number;
setId(value: number): void;
getFunction(): string;
setFunction(value: string): void;
getModule(): Module;
setModule(value: Module): void;
getMethod(): string;
setMethod(value: string): void;
clearArgsList(): void;
getArgsList(): Array<string>;
setArgsList(value: Array<string>): void;
addArgs(value: string, index?: number): string;
getTimeout(): number;
setTimeout(value: number): void;
getActive(): boolean;
setActive(value: boolean): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): NewEvalMessage.AsObject;
static toObject(includeInstance: boolean, msg: NewEvalMessage): NewEvalMessage.AsObject;
toObject(includeInstance?: boolean): NamedProxyMessage.AsObject;
static toObject(includeInstance: boolean, msg: NamedProxyMessage): NamedProxyMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: NewEvalMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): NewEvalMessage;
static deserializeBinaryFromReader(message: NewEvalMessage, reader: jspb.BinaryReader): NewEvalMessage;
static serializeBinaryToWriter(message: NamedProxyMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): NamedProxyMessage;
static deserializeBinaryFromReader(message: NamedProxyMessage, reader: jspb.BinaryReader): NamedProxyMessage;
}
export namespace NewEvalMessage {
export namespace NamedProxyMessage {
export type AsObject = {
id: number,
pb_function: string,
module: Module,
method: string,
argsList: Array<string>,
timeout: number,
active: boolean,
}
}
export class EvalEventMessage extends jspb.Message {
export class NumberedProxyMessage extends jspb.Message {
getId(): number;
setId(value: number): void;
getProxyId(): number;
setProxyId(value: number): void;
getMethod(): string;
setMethod(value: string): void;
clearArgsList(): void;
getArgsList(): Array<string>;
setArgsList(value: Array<string>): void;
addArgs(value: string, index?: number): string;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): NumberedProxyMessage.AsObject;
static toObject(includeInstance: boolean, msg: NumberedProxyMessage): NumberedProxyMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: NumberedProxyMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): NumberedProxyMessage;
static deserializeBinaryFromReader(message: NumberedProxyMessage, reader: jspb.BinaryReader): NumberedProxyMessage;
}
export namespace NumberedProxyMessage {
export type AsObject = {
id: number,
proxyId: number,
method: string,
argsList: Array<string>,
}
}
export class MethodMessage extends jspb.Message {
hasNamedProxy(): boolean;
clearNamedProxy(): void;
getNamedProxy(): NamedProxyMessage | undefined;
setNamedProxy(value?: NamedProxyMessage): void;
hasNumberedProxy(): boolean;
clearNumberedProxy(): void;
getNumberedProxy(): NumberedProxyMessage | undefined;
setNumberedProxy(value?: NumberedProxyMessage): void;
getMsgCase(): MethodMessage.MsgCase;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): MethodMessage.AsObject;
static toObject(includeInstance: boolean, msg: MethodMessage): MethodMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: MethodMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): MethodMessage;
static deserializeBinaryFromReader(message: MethodMessage, reader: jspb.BinaryReader): MethodMessage;
}
export namespace MethodMessage {
export type AsObject = {
namedProxy?: NamedProxyMessage.AsObject,
numberedProxy?: NumberedProxyMessage.AsObject,
}
export enum MsgCase {
MSG_NOT_SET = 0,
NAMED_PROXY = 1,
NUMBERED_PROXY = 2,
}
}
export class CallbackMessage extends jspb.Message {
hasNamedCallback(): boolean;
clearNamedCallback(): void;
getNamedCallback(): NamedCallbackMessage | undefined;
setNamedCallback(value?: NamedCallbackMessage): void;
hasNumberedCallback(): boolean;
clearNumberedCallback(): void;
getNumberedCallback(): NumberedCallbackMessage | undefined;
setNumberedCallback(value?: NumberedCallbackMessage): void;
getMsgCase(): CallbackMessage.MsgCase;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): CallbackMessage.AsObject;
static toObject(includeInstance: boolean, msg: CallbackMessage): CallbackMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: CallbackMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): CallbackMessage;
static deserializeBinaryFromReader(message: CallbackMessage, reader: jspb.BinaryReader): CallbackMessage;
}
export namespace CallbackMessage {
export type AsObject = {
namedCallback?: NamedCallbackMessage.AsObject,
numberedCallback?: NumberedCallbackMessage.AsObject,
}
export enum MsgCase {
MSG_NOT_SET = 0,
NAMED_CALLBACK = 1,
NUMBERED_CALLBACK = 2,
}
}
export class NamedCallbackMessage extends jspb.Message {
getModule(): Module;
setModule(value: Module): void;
getCallbackId(): number;
setCallbackId(value: number): void;
clearArgsList(): void;
getArgsList(): Array<string>;
setArgsList(value: Array<string>): void;
addArgs(value: string, index?: number): string;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): NamedCallbackMessage.AsObject;
static toObject(includeInstance: boolean, msg: NamedCallbackMessage): NamedCallbackMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: NamedCallbackMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): NamedCallbackMessage;
static deserializeBinaryFromReader(message: NamedCallbackMessage, reader: jspb.BinaryReader): NamedCallbackMessage;
}
export namespace NamedCallbackMessage {
export type AsObject = {
module: Module,
callbackId: number,
argsList: Array<string>,
}
}
export class NumberedCallbackMessage extends jspb.Message {
getProxyId(): number;
setProxyId(value: number): void;
getCallbackId(): number;
setCallbackId(value: number): void;
clearArgsList(): void;
getArgsList(): Array<string>;
setArgsList(value: Array<string>): void;
addArgs(value: string, index?: number): string;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): NumberedCallbackMessage.AsObject;
static toObject(includeInstance: boolean, msg: NumberedCallbackMessage): NumberedCallbackMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: NumberedCallbackMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): NumberedCallbackMessage;
static deserializeBinaryFromReader(message: NumberedCallbackMessage, reader: jspb.BinaryReader): NumberedCallbackMessage;
}
export namespace NumberedCallbackMessage {
export type AsObject = {
proxyId: number,
callbackId: number,
argsList: Array<string>,
}
}
export class EventMessage extends jspb.Message {
hasNamedEvent(): boolean;
clearNamedEvent(): void;
getNamedEvent(): NamedEventMessage | undefined;
setNamedEvent(value?: NamedEventMessage): void;
hasNumberedEvent(): boolean;
clearNumberedEvent(): void;
getNumberedEvent(): NumberedEventMessage | undefined;
setNumberedEvent(value?: NumberedEventMessage): void;
getMsgCase(): EventMessage.MsgCase;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): EventMessage.AsObject;
static toObject(includeInstance: boolean, msg: EventMessage): EventMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: EventMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): EventMessage;
static deserializeBinaryFromReader(message: EventMessage, reader: jspb.BinaryReader): EventMessage;
}
export namespace EventMessage {
export type AsObject = {
namedEvent?: NamedEventMessage.AsObject,
numberedEvent?: NumberedEventMessage.AsObject,
}
export enum MsgCase {
MSG_NOT_SET = 0,
NAMED_EVENT = 1,
NUMBERED_EVENT = 2,
}
}
export class NamedEventMessage extends jspb.Message {
getModule(): Module;
setModule(value: Module): void;
getEvent(): string;
setEvent(value: string): void;
@ -54,24 +249,54 @@ export class EvalEventMessage extends jspb.Message {
addArgs(value: string, index?: number): string;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): EvalEventMessage.AsObject;
static toObject(includeInstance: boolean, msg: EvalEventMessage): EvalEventMessage.AsObject;
toObject(includeInstance?: boolean): NamedEventMessage.AsObject;
static toObject(includeInstance: boolean, msg: NamedEventMessage): NamedEventMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: EvalEventMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): EvalEventMessage;
static deserializeBinaryFromReader(message: EvalEventMessage, reader: jspb.BinaryReader): EvalEventMessage;
static serializeBinaryToWriter(message: NamedEventMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): NamedEventMessage;
static deserializeBinaryFromReader(message: NamedEventMessage, reader: jspb.BinaryReader): NamedEventMessage;
}
export namespace EvalEventMessage {
export namespace NamedEventMessage {
export type AsObject = {
id: number,
module: Module,
event: string,
argsList: Array<string>,
}
}
export class EvalFailedMessage extends jspb.Message {
export class NumberedEventMessage extends jspb.Message {
getProxyId(): number;
setProxyId(value: number): void;
getEvent(): string;
setEvent(value: string): void;
clearArgsList(): void;
getArgsList(): Array<string>;
setArgsList(value: Array<string>): void;
addArgs(value: string, index?: number): string;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): NumberedEventMessage.AsObject;
static toObject(includeInstance: boolean, msg: NumberedEventMessage): NumberedEventMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: NumberedEventMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): NumberedEventMessage;
static deserializeBinaryFromReader(message: NumberedEventMessage, reader: jspb.BinaryReader): NumberedEventMessage;
}
export namespace NumberedEventMessage {
export type AsObject = {
proxyId: number,
event: string,
argsList: Array<string>,
}
}
export class FailMessage extends jspb.Message {
getId(): number;
setId(value: number): void;
@ -79,23 +304,23 @@ export class EvalFailedMessage extends jspb.Message {
setResponse(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): EvalFailedMessage.AsObject;
static toObject(includeInstance: boolean, msg: EvalFailedMessage): EvalFailedMessage.AsObject;
toObject(includeInstance?: boolean): FailMessage.AsObject;
static toObject(includeInstance: boolean, msg: FailMessage): FailMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: EvalFailedMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): EvalFailedMessage;
static deserializeBinaryFromReader(message: EvalFailedMessage, reader: jspb.BinaryReader): EvalFailedMessage;
static serializeBinaryToWriter(message: FailMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): FailMessage;
static deserializeBinaryFromReader(message: FailMessage, reader: jspb.BinaryReader): FailMessage;
}
export namespace EvalFailedMessage {
export namespace FailMessage {
export type AsObject = {
id: number,
response: string,
}
}
export class EvalDoneMessage extends jspb.Message {
export class SuccessMessage extends jspb.Message {
getId(): number;
setId(value: number): void;
@ -103,16 +328,16 @@ export class EvalDoneMessage extends jspb.Message {
setResponse(value: string): void;
serializeBinary(): Uint8Array;
toObject(includeInstance?: boolean): EvalDoneMessage.AsObject;
static toObject(includeInstance: boolean, msg: EvalDoneMessage): EvalDoneMessage.AsObject;
toObject(includeInstance?: boolean): SuccessMessage.AsObject;
static toObject(includeInstance: boolean, msg: SuccessMessage): SuccessMessage.AsObject;
static extensions: {[key: number]: jspb.ExtensionFieldInfo<jspb.Message>};
static extensionsBinary: {[key: number]: jspb.ExtensionFieldBinaryInfo<jspb.Message>};
static serializeBinaryToWriter(message: EvalDoneMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): EvalDoneMessage;
static deserializeBinaryFromReader(message: EvalDoneMessage, reader: jspb.BinaryReader): EvalDoneMessage;
static serializeBinaryToWriter(message: SuccessMessage, writer: jspb.BinaryWriter): void;
static deserializeBinary(bytes: Uint8Array): SuccessMessage;
static deserializeBinaryFromReader(message: SuccessMessage, reader: jspb.BinaryReader): SuccessMessage;
}
export namespace EvalDoneMessage {
export namespace SuccessMessage {
export type AsObject = {
id: number,
response: string,
@ -151,3 +376,12 @@ export namespace Pong {
}
}
export enum Module {
CHILDPROCESS = 0,
FS = 1,
NET = 2,
NODEPTY = 3,
SPDLOG = 4,
TRASH = 5,
}

File diff suppressed because it is too large Load Diff

View File

@ -3,12 +3,12 @@ import * as path from "path";
import { Readable } from "stream";
import * as util from "util";
import { createClient } from "@coder/protocol/test";
const client = createClient();
jest.mock("../src/fill/client", () => ({ client }));
const cp = require("../src/fill/child_process") as typeof import("child_process");
import { Module } from "../src/common/proxy";
describe("child_process", () => {
const client = createClient();
const cp = client.modules[Module.ChildProcess];
const getStdout = async (proc: ChildProcess): Promise<string> => {
return new Promise((r): Readable => proc.stdout.on("data", r))
.then((s) => s.toString());
@ -71,4 +71,28 @@ describe("child_process", () => {
await new Promise((r): ChildProcess => proc.on("exit", r));
});
});
it("should dispose", (done) => {
setTimeout(() => {
client.dispose();
done();
}, 100);
});
it("should disconnect", async () => {
const client = createClient();
const cp = client.modules[Module.ChildProcess];
const proc = cp.fork(path.join(__dirname, "forker.js"));
const fn = jest.fn();
proc.on("error", fn);
proc.send({ bananas: true });
await expect(new Promise((r): ChildProcess => proc.on("message", r)))
.resolves.toMatchObject({
bananas: true,
});
client.dispose();
expect(fn).toHaveBeenCalledWith(new Error("disconnected"));
});
});

View File

@ -1,84 +0,0 @@
import { createClient } from "./helpers";
describe("Evaluate", () => {
const client = createClient();
it("should transfer string", async () => {
const value = await client.evaluate(() => {
return "hi";
});
expect(value).toEqual("hi");
}, 100);
it("should compute from string", async () => {
const start = "ban\%\$\"``a,,,,asdasd";
const value = await client.evaluate((_helper, a) => {
return a;
}, start);
expect(value).toEqual(start);
}, 100);
it("should compute from object", async () => {
const value = await client.evaluate((_helper, arg) => {
return arg.bananas * 2;
}, { bananas: 1 });
expect(value).toEqual(2);
}, 100);
it("should transfer object", async () => {
const value = await client.evaluate(() => {
return { alpha: "beta" };
});
expect(value.alpha).toEqual("beta");
}, 100);
it("should require", async () => {
const value = await client.evaluate(() => {
const fs = require("fs") as typeof import("fs");
return Object.keys(fs).filter((f) => f === "readFileSync");
});
expect(value[0]).toEqual("readFileSync");
}, 100);
it("should resolve with promise", async () => {
const value = await client.evaluate(async () => {
await new Promise((r): number => setTimeout(r, 100));
return "donkey";
});
expect(value).toEqual("donkey");
}, 250);
it("should do active process", (done) => {
const runner = client.run((ae) => {
ae.on("first", () => {
ae.emit("first:response");
ae.on("second", () => ae.emit("second:response"));
});
const disposeCallbacks = <Array<() => void>>[];
const dispose = (): void => {
disposeCallbacks.forEach((cb) => cb());
ae.emit("disposed");
};
return {
onDidDispose: (cb: () => void): number => disposeCallbacks.push(cb),
dispose,
};
});
runner.emit("first");
runner.on("first:response", () => runner.emit("second"));
runner.on("second:response", () => client.dispose());
runner.on("disposed", () => done());
});
});

View File

@ -1,53 +1,34 @@
import * as nativeFs from "fs";
import * as os from "os";
import * as path from "path";
import * as util from "util";
import * as rimraf from "rimraf";
import { createClient } from "@coder/protocol/test";
const client = createClient();
jest.mock("../src/fill/client", () => ({ client }));
const fs = require("../src/fill/fs") as typeof import("fs");
import { Module } from "../src/common/proxy";
import { createClient, Helper } from "./helpers";
describe("fs", () => {
let i = 0;
const coderDir = path.join(os.tmpdir(), "coder", "fs");
const testFile = path.join(__dirname, "fs.test.ts");
const tmpFile = (): string => path.join(coderDir, `${i++}`);
const createTmpFile = async (): Promise<string> => {
const tf = tmpFile();
await util.promisify(nativeFs.writeFile)(tf, "");
return tf;
};
const client = createClient();
// tslint:disable-next-line no-any
const fs = client.modules[Module.Fs] as any as typeof import("fs");
const helper = new Helper("fs");
beforeAll(async () => {
try {
await util.promisify(nativeFs.mkdir)(path.dirname(coderDir));
} catch (error) {
if (error.code !== "EEXIST" && error.code !== "EISDIR") {
throw error;
}
}
await util.promisify(rimraf)(coderDir);
await util.promisify(nativeFs.mkdir)(coderDir);
await helper.prepare();
});
describe("access", () => {
it("should access existing file", async () => {
await expect(util.promisify(fs.access)(testFile))
await expect(util.promisify(fs.access)(__filename))
.resolves.toBeUndefined();
});
it("should fail to access nonexistent file", async () => {
await expect(util.promisify(fs.access)(tmpFile()))
await expect(util.promisify(fs.access)(helper.tmpFile()))
.rejects.toThrow("ENOENT");
});
});
describe("append", () => {
it("should append to existing file", async () => {
const file = await createTmpFile();
const file = await helper.createTmpFile();
await expect(util.promisify(fs.appendFile)(file, "howdy"))
.resolves.toBeUndefined();
expect(await util.promisify(nativeFs.readFile)(file, "utf8"))
@ -55,7 +36,7 @@ describe("fs", () => {
});
it("should create then append to nonexistent file", async () => {
const file = tmpFile();
const file = helper.tmpFile();
await expect(util.promisify(fs.appendFile)(file, "howdy"))
.resolves.toBeUndefined();
expect(await util.promisify(nativeFs.readFile)(file, "utf8"))
@ -63,7 +44,7 @@ describe("fs", () => {
});
it("should fail to append to file in nonexistent directory", async () => {
const file = path.join(tmpFile(), "nope");
const file = path.join(helper.tmpFile(), "nope");
await expect(util.promisify(fs.appendFile)(file, "howdy"))
.rejects.toThrow("ENOENT");
expect(await util.promisify(nativeFs.exists)(file))
@ -73,33 +54,33 @@ describe("fs", () => {
describe("chmod", () => {
it("should chmod existing file", async () => {
const file = await createTmpFile();
const file = await helper.createTmpFile();
await expect(util.promisify(fs.chmod)(file, "755"))
.resolves.toBeUndefined();
});
it("should fail to chmod nonexistent file", async () => {
await expect(util.promisify(fs.chmod)(tmpFile(), "755"))
await expect(util.promisify(fs.chmod)(helper.tmpFile(), "755"))
.rejects.toThrow("ENOENT");
});
});
describe("chown", () => {
it("should chown existing file", async () => {
const file = await createTmpFile();
const file = await helper.createTmpFile();
await expect(util.promisify(fs.chown)(file, 1, 1))
.resolves.toBeUndefined();
});
it("should fail to chown nonexistent file", async () => {
await expect(util.promisify(fs.chown)(tmpFile(), 1, 1))
await expect(util.promisify(fs.chown)(helper.tmpFile(), 1, 1))
.rejects.toThrow("ENOENT");
});
});
describe("close", () => {
it("should close opened file", async () => {
const file = await createTmpFile();
const file = await helper.createTmpFile();
const fd = await util.promisify(nativeFs.open)(file, "r");
await expect(util.promisify(fs.close)(fd))
.resolves.toBeUndefined();
@ -113,8 +94,8 @@ describe("fs", () => {
describe("copyFile", () => {
it("should copy existing file", async () => {
const source = await createTmpFile();
const destination = tmpFile();
const source = await helper.createTmpFile();
const destination = helper.tmpFile();
await expect(util.promisify(fs.copyFile)(source, destination))
.resolves.toBeUndefined();
await expect(util.promisify(fs.exists)(destination))
@ -122,44 +103,47 @@ describe("fs", () => {
});
it("should fail to copy nonexistent file", async () => {
await expect(util.promisify(fs.copyFile)(tmpFile(), tmpFile()))
await expect(util.promisify(fs.copyFile)(helper.tmpFile(), helper.tmpFile()))
.rejects.toThrow("ENOENT");
});
});
describe("createWriteStream", () => {
it("should write to file", async () => {
const file = tmpFile();
const file = helper.tmpFile();
const content = "howdy\nhow\nr\nu";
const stream = fs.createWriteStream(file);
stream.on("open", (fd) => {
expect(fd).toBeDefined();
stream.write(content);
stream.close();
stream.end();
});
await expect(new Promise((resolve): void => {
stream.on("close", async () => {
resolve(await util.promisify(nativeFs.readFile)(file, "utf8"));
});
})).resolves.toBe(content);
await Promise.all([
new Promise((resolve): nativeFs.WriteStream => stream.on("close", resolve)),
new Promise((resolve): nativeFs.WriteStream => stream.on("finish", resolve)),
]);
await expect(util.promisify(nativeFs.readFile)(file, "utf8")).resolves.toBe(content);
});
});
describe("exists", () => {
it("should output file exists", async () => {
await expect(util.promisify(fs.exists)(testFile))
await expect(util.promisify(fs.exists)(__filename))
.resolves.toBe(true);
});
it("should output file does not exist", async () => {
await expect(util.promisify(fs.exists)(tmpFile()))
await expect(util.promisify(fs.exists)(helper.tmpFile()))
.resolves.toBe(false);
});
});
describe("fchmod", () => {
it("should fchmod existing file", async () => {
const file = await createTmpFile();
const file = await helper.createTmpFile();
const fd = await util.promisify(nativeFs.open)(file, "r");
await expect(util.promisify(fs.fchmod)(fd, "755"))
.resolves.toBeUndefined();
@ -174,7 +158,7 @@ describe("fs", () => {
describe("fchown", () => {
it("should fchown existing file", async () => {
const file = await createTmpFile();
const file = await helper.createTmpFile();
const fd = await util.promisify(nativeFs.open)(file, "r");
await expect(util.promisify(fs.fchown)(fd, 1, 1))
.resolves.toBeUndefined();
@ -189,7 +173,7 @@ describe("fs", () => {
describe("fdatasync", () => {
it("should fdatasync existing file", async () => {
const file = await createTmpFile();
const file = await helper.createTmpFile();
const fd = await util.promisify(nativeFs.open)(file, "r");
await expect(util.promisify(fs.fdatasync)(fd))
.resolves.toBeUndefined();
@ -204,7 +188,7 @@ describe("fs", () => {
describe("fstat", () => {
it("should fstat existing file", async () => {
const fd = await util.promisify(nativeFs.open)(testFile, "r");
const fd = await util.promisify(nativeFs.open)(__filename, "r");
const stat = await util.promisify(nativeFs.fstat)(fd);
await expect(util.promisify(fs.fstat)(fd))
.resolves.toMatchObject({
@ -221,7 +205,7 @@ describe("fs", () => {
describe("fsync", () => {
it("should fsync existing file", async () => {
const file = await createTmpFile();
const file = await helper.createTmpFile();
const fd = await util.promisify(nativeFs.open)(file, "r");
await expect(util.promisify(fs.fsync)(fd))
.resolves.toBeUndefined();
@ -236,7 +220,7 @@ describe("fs", () => {
describe("ftruncate", () => {
it("should ftruncate existing file", async () => {
const file = await createTmpFile();
const file = await helper.createTmpFile();
const fd = await util.promisify(nativeFs.open)(file, "w");
await expect(util.promisify(fs.ftruncate)(fd, 1))
.resolves.toBeUndefined();
@ -251,7 +235,7 @@ describe("fs", () => {
describe("futimes", () => {
it("should futimes existing file", async () => {
const file = await createTmpFile();
const file = await helper.createTmpFile();
const fd = await util.promisify(nativeFs.open)(file, "w");
await expect(util.promisify(fs.futimes)(fd, 1, 1))
.resolves.toBeUndefined();
@ -266,36 +250,36 @@ describe("fs", () => {
describe("lchmod", () => {
it("should lchmod existing file", async () => {
const file = await createTmpFile();
const file = await helper.createTmpFile();
await expect(util.promisify(fs.lchmod)(file, "755"))
.resolves.toBeUndefined();
});
// TODO: Doesn't fail on my system?
it("should fail to lchmod nonexistent file", async () => {
await expect(util.promisify(fs.lchmod)(tmpFile(), "755"))
await expect(util.promisify(fs.lchmod)(helper.tmpFile(), "755"))
.resolves.toBeUndefined();
});
});
describe("lchown", () => {
it("should lchown existing file", async () => {
const file = await createTmpFile();
const file = await helper.createTmpFile();
await expect(util.promisify(fs.lchown)(file, 1, 1))
.resolves.toBeUndefined();
});
// TODO: Doesn't fail on my system?
it("should fail to lchown nonexistent file", async () => {
await expect(util.promisify(fs.lchown)(tmpFile(), 1, 1))
await expect(util.promisify(fs.lchown)(helper.tmpFile(), 1, 1))
.resolves.toBeUndefined();
});
});
describe("link", () => {
it("should link existing file", async () => {
const source = await createTmpFile();
const destination = tmpFile();
const source = await helper.createTmpFile();
const destination = helper.tmpFile();
await expect(util.promisify(fs.link)(source, destination))
.resolves.toBeUndefined();
await expect(util.promisify(fs.exists)(destination))
@ -303,29 +287,30 @@ describe("fs", () => {
});
it("should fail to link nonexistent file", async () => {
await expect(util.promisify(fs.link)(tmpFile(), tmpFile()))
await expect(util.promisify(fs.link)(helper.tmpFile(), helper.tmpFile()))
.rejects.toThrow("ENOENT");
});
});
describe("lstat", () => {
it("should lstat existing file", async () => {
const stat = await util.promisify(nativeFs.lstat)(testFile);
await expect(util.promisify(fs.lstat)(testFile))
const stat = await util.promisify(nativeFs.lstat)(__filename);
await expect(util.promisify(fs.lstat)(__filename))
.resolves.toMatchObject({
size: stat.size,
});
});
it("should fail to lstat non-existent file", async () => {
await expect(util.promisify(fs.lstat)(tmpFile()))
await expect(util.promisify(fs.lstat)(helper.tmpFile()))
.rejects.toThrow("ENOENT");
});
});
describe("mkdir", () => {
const target = tmpFile();
let target: string;
it("should create nonexistent directory", async () => {
target = helper.tmpFile();
await expect(util.promisify(fs.mkdir)(target))
.resolves.toBeUndefined();
});
@ -338,28 +323,28 @@ describe("fs", () => {
describe("mkdtemp", () => {
it("should create temp dir", async () => {
await expect(util.promisify(fs.mkdtemp)(coderDir + "/"))
await expect(util.promisify(fs.mkdtemp)(helper.coderDir + "/"))
.resolves.toMatch(/^\/tmp\/coder\/fs\/[a-zA-Z0-9]{6}/);
});
});
describe("open", () => {
it("should open existing file", async () => {
const fd = await util.promisify(fs.open)(testFile, "r");
const fd = await util.promisify(fs.open)(__filename, "r");
expect(fd).not.toBeNaN();
await expect(util.promisify(fs.close)(fd))
.resolves.toBeUndefined();
});
it("should fail to open nonexistent file", async () => {
await expect(util.promisify(fs.open)(tmpFile(), "r"))
await expect(util.promisify(fs.open)(helper.tmpFile(), "r"))
.rejects.toThrow("ENOENT");
});
});
describe("read", () => {
it("should read existing file", async () => {
const fd = await util.promisify(nativeFs.open)(testFile, "r");
const fd = await util.promisify(nativeFs.open)(__filename, "r");
const stat = await util.promisify(nativeFs.fstat)(fd);
const buffer = new Buffer(stat.size);
let bytesRead = 0;
@ -373,7 +358,7 @@ describe("fs", () => {
bytesRead += chunkSize;
}
const content = await util.promisify(nativeFs.readFile)(testFile, "utf8");
const content = await util.promisify(nativeFs.readFile)(__filename, "utf8");
expect(buffer.toString()).toEqual(content);
await util.promisify(nativeFs.close)(fd);
});
@ -386,64 +371,64 @@ describe("fs", () => {
describe("readFile", () => {
it("should read existing file", async () => {
const content = await util.promisify(nativeFs.readFile)(testFile, "utf8");
await expect(util.promisify(fs.readFile)(testFile, "utf8"))
const content = await util.promisify(nativeFs.readFile)(__filename, "utf8");
await expect(util.promisify(fs.readFile)(__filename, "utf8"))
.resolves.toEqual(content);
});
it("should fail to read nonexistent file", async () => {
await expect(util.promisify(fs.readFile)(tmpFile()))
await expect(util.promisify(fs.readFile)(helper.tmpFile()))
.rejects.toThrow("ENOENT");
});
});
describe("readdir", () => {
it("should read existing directory", async () => {
const paths = await util.promisify(nativeFs.readdir)(coderDir);
await expect(util.promisify(fs.readdir)(coderDir))
const paths = await util.promisify(nativeFs.readdir)(helper.coderDir);
await expect(util.promisify(fs.readdir)(helper.coderDir))
.resolves.toEqual(paths);
});
it("should fail to read nonexistent directory", async () => {
await expect(util.promisify(fs.readdir)(tmpFile()))
await expect(util.promisify(fs.readdir)(helper.tmpFile()))
.rejects.toThrow("ENOENT");
});
});
describe("readlink", () => {
it("should read existing link", async () => {
const source = await createTmpFile();
const destination = tmpFile();
const source = await helper.createTmpFile();
const destination = helper.tmpFile();
await util.promisify(nativeFs.symlink)(source, destination);
await expect(util.promisify(fs.readlink)(destination))
.resolves.toBe(source);
});
it("should fail to read nonexistent link", async () => {
await expect(util.promisify(fs.readlink)(tmpFile()))
await expect(util.promisify(fs.readlink)(helper.tmpFile()))
.rejects.toThrow("ENOENT");
});
});
describe("realpath", () => {
it("should read real path of existing file", async () => {
const source = await createTmpFile();
const destination = tmpFile();
const source = await helper.createTmpFile();
const destination = helper.tmpFile();
nativeFs.symlinkSync(source, destination);
await expect(util.promisify(fs.realpath)(destination))
.resolves.toBe(source);
});
it("should fail to read real path of nonexistent file", async () => {
await expect(util.promisify(fs.realpath)(tmpFile()))
await expect(util.promisify(fs.realpath)(helper.tmpFile()))
.rejects.toThrow("ENOENT");
});
});
describe("rename", () => {
it("should rename existing file", async () => {
const source = await createTmpFile();
const destination = tmpFile();
const source = await helper.createTmpFile();
const destination = helper.tmpFile();
await expect(util.promisify(fs.rename)(source, destination))
.resolves.toBeUndefined();
await expect(util.promisify(nativeFs.exists)(source))
@ -453,14 +438,14 @@ describe("fs", () => {
});
it("should fail to rename nonexistent file", async () => {
await expect(util.promisify(fs.rename)(tmpFile(), tmpFile()))
await expect(util.promisify(fs.rename)(helper.tmpFile(), helper.tmpFile()))
.rejects.toThrow("ENOENT");
});
});
describe("rmdir", () => {
it("should rmdir existing directory", async () => {
const dir = tmpFile();
const dir = helper.tmpFile();
await util.promisify(nativeFs.mkdir)(dir);
await expect(util.promisify(fs.rmdir)(dir))
.resolves.toBeUndefined();
@ -469,15 +454,15 @@ describe("fs", () => {
});
it("should fail to rmdir nonexistent directory", async () => {
await expect(util.promisify(fs.rmdir)(tmpFile()))
await expect(util.promisify(fs.rmdir)(helper.tmpFile()))
.rejects.toThrow("ENOENT");
});
});
describe("stat", () => {
it("should stat existing file", async () => {
const nativeStat = await util.promisify(nativeFs.stat)(testFile);
const stat = await util.promisify(fs.stat)(testFile);
const nativeStat = await util.promisify(nativeFs.stat)(__filename);
const stat = await util.promisify(fs.stat)(__filename);
expect(stat).toMatchObject({
size: nativeStat.size,
});
@ -485,7 +470,7 @@ describe("fs", () => {
});
it("should stat existing folder", async () => {
const dir = tmpFile();
const dir = helper.tmpFile();
await util.promisify(nativeFs.mkdir)(dir);
const nativeStat = await util.promisify(nativeFs.stat)(dir);
const stat = await util.promisify(fs.stat)(dir);
@ -496,7 +481,7 @@ describe("fs", () => {
});
it("should fail to stat nonexistent file", async () => {
const error = await util.promisify(fs.stat)(tmpFile()).catch((e) => e);
const error = await util.promisify(fs.stat)(helper.tmpFile()).catch((e) => e);
expect(error.message).toContain("ENOENT");
expect(error.code).toBe("ENOENT");
});
@ -504,8 +489,8 @@ describe("fs", () => {
describe("symlink", () => {
it("should symlink existing file", async () => {
const source = await createTmpFile();
const destination = tmpFile();
const source = await helper.createTmpFile();
const destination = helper.tmpFile();
await expect(util.promisify(fs.symlink)(source, destination))
.resolves.toBeUndefined();
expect(util.promisify(nativeFs.exists)(source))
@ -514,14 +499,14 @@ describe("fs", () => {
// TODO: Seems to be happy to do this on my system?
it("should fail to symlink nonexistent file", async () => {
await expect(util.promisify(fs.symlink)(tmpFile(), tmpFile()))
await expect(util.promisify(fs.symlink)(helper.tmpFile(), helper.tmpFile()))
.resolves.toBeUndefined();
});
});
describe("truncate", () => {
it("should truncate existing file", async () => {
const file = tmpFile();
const file = helper.tmpFile();
await util.promisify(nativeFs.writeFile)(file, "hiiiiii");
await expect(util.promisify(fs.truncate)(file, 2))
.resolves.toBeUndefined();
@ -530,14 +515,14 @@ describe("fs", () => {
});
it("should fail to truncate nonexistent file", async () => {
await expect(util.promisify(fs.truncate)(tmpFile(), 0))
await expect(util.promisify(fs.truncate)(helper.tmpFile(), 0))
.rejects.toThrow("ENOENT");
});
});
describe("unlink", () => {
it("should unlink existing file", async () => {
const file = await createTmpFile();
const file = await helper.createTmpFile();
await expect(util.promisify(fs.unlink)(file))
.resolves.toBeUndefined();
expect(util.promisify(nativeFs.exists)(file))
@ -545,27 +530,27 @@ describe("fs", () => {
});
it("should fail to unlink nonexistent file", async () => {
await expect(util.promisify(fs.unlink)(tmpFile()))
await expect(util.promisify(fs.unlink)(helper.tmpFile()))
.rejects.toThrow("ENOENT");
});
});
describe("utimes", () => {
it("should update times on existing file", async () => {
const file = await createTmpFile();
const file = await helper.createTmpFile();
await expect(util.promisify(fs.utimes)(file, 100, 100))
.resolves.toBeUndefined();
});
it("should fail to update times on nonexistent file", async () => {
await expect(util.promisify(fs.utimes)(tmpFile(), 100, 100))
await expect(util.promisify(fs.utimes)(helper.tmpFile(), 100, 100))
.rejects.toThrow("ENOENT");
});
});
describe("write", () => {
it("should write to existing file", async () => {
const file = await createTmpFile();
const file = await helper.createTmpFile();
const fd = await util.promisify(nativeFs.open)(file, "w");
await expect(util.promisify(fs.write)(fd, Buffer.from("hi")))
.resolves.toBe(2);
@ -582,11 +567,15 @@ describe("fs", () => {
describe("writeFile", () => {
it("should write file", async () => {
const file = await createTmpFile();
const file = await helper.createTmpFile();
await expect(util.promisify(fs.writeFile)(file, "howdy"))
.resolves.toBeUndefined();
await expect(util.promisify(nativeFs.readFile)(file, "utf8"))
.resolves.toBe("howdy");
});
});
it("should dispose", () => {
client.dispose();
});
});

View File

@ -1,7 +1,54 @@
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import * as rimraf from "rimraf";
import * as util from "util";
import { IDisposable } from "@coder/disposable";
import { Emitter } from "@coder/events";
import { Client } from "../src/browser/client";
import { Server, ServerOptions } from "../src/node/server";
// So we only make the directory once when running multiple tests.
let mkdirPromise: Promise<void> | undefined;
export class Helper {
private i = 0;
public coderDir: string;
private baseDir = path.join(os.tmpdir(), "coder");
public constructor(directoryName: string) {
if (!directoryName.trim()) {
throw new Error("no directory name");
}
this.coderDir = path.join(this.baseDir, directoryName);
}
public tmpFile(): string {
return path.join(this.coderDir, `${this.i++}`);
}
public async createTmpFile(): Promise<string> {
const tf = this.tmpFile();
await util.promisify(fs.writeFile)(tf, "");
return tf;
}
public async prepare(): Promise<void> {
if (!mkdirPromise) {
mkdirPromise = util.promisify(fs.mkdir)(this.baseDir).catch((error) => {
if (error.code !== "EEXIST" && error.code !== "EISDIR") {
throw error;
}
});
}
await mkdirPromise;
await util.promisify(rimraf)(this.coderDir);
await util.promisify(fs.mkdir)(this.coderDir);
}
}
export const createClient = (serverOptions?: ServerOptions): Client => {
const s2c = new Emitter<Uint8Array | Buffer>();
const c2s = new Emitter<Uint8Array | Buffer>();
@ -10,19 +57,19 @@ export const createClient = (serverOptions?: ServerOptions): Client => {
// tslint:disable-next-line no-unused-expression
new Server({
close: (): void => closeCallbacks.forEach((cb) => cb()),
onDown: (_cb: () => void): void => undefined,
onUp: (_cb: () => void): void => undefined,
onClose: (cb: () => void): number => closeCallbacks.push(cb),
onMessage: (cb): void => {
c2s.event((d) => cb(d));
},
onMessage: (cb): IDisposable => c2s.event((d) => cb(d)),
send: (data): NodeJS.Timer => setTimeout(() => s2c.emit(data), 0),
}, serverOptions);
const client = new Client({
close: (): void => closeCallbacks.forEach((cb) => cb()),
onDown: (_cb: () => void): void => undefined,
onUp: (_cb: () => void): void => undefined,
onClose: (cb: () => void): number => closeCallbacks.push(cb),
onMessage: (cb): void => {
s2c.event((d) => cb(d));
},
onMessage: (cb): IDisposable => s2c.event((d) => cb(d)),
send: (data): NodeJS.Timer => setTimeout(() => c2s.emit(data), 0),
});

View File

@ -1,34 +1,18 @@
import * as fs from "fs";
import * as nativeNet from "net";
import * as os from "os";
import * as path from "path";
import * as util from "util";
import * as rimraf from "rimraf";
import { createClient } from "@coder/protocol/test";
const client = createClient();
jest.mock("../src/fill/client", () => ({ client }));
const net = require("../src/fill/net") as typeof import("net");
import { Module } from "../src/common/proxy";
import { createClient, Helper } from "./helpers";
describe("net", () => {
let i = 0;
const coderDir = path.join(os.tmpdir(), "coder", "net");
const tmpFile = (): string => path.join(coderDir, `socket.${i++}`);
const client = createClient();
const net = client.modules[Module.Net];
const helper = new Helper("net");
beforeAll(async () => {
try {
await util.promisify(fs.mkdir)(path.dirname(coderDir));
} catch (error) {
if (error.code !== "EEXIST" && error.code !== "EISDIR") {
throw error;
}
}
await util.promisify(rimraf)(coderDir);
await util.promisify(fs.mkdir)(coderDir);
await helper.prepare();
});
describe("Socket", () => {
const socketPath = tmpFile();
const socketPath = helper.tmpFile();
let server: nativeNet.Server;
beforeAll(async () => {
@ -41,6 +25,19 @@ describe("net", () => {
server.close();
});
it("should fail to connect", async () => {
const socket = new net.Socket();
const fn = jest.fn();
socket.on("error", fn);
socket.connect("/tmp/t/e/s/t/d/o/e/s/n/o/t/e/x/i/s/t");
await new Promise((r): nativeNet.Socket => socket.on("close", r));
expect(fn).toHaveBeenCalledTimes(1);
});
it("should connect", async () => {
await new Promise((resolve): void => {
const socket = net.createConnection(socketPath, () => {
@ -98,7 +95,7 @@ describe("net", () => {
const s = net.createServer();
s.on("listening", () => s.close());
s.on("close", () => done());
s.listen(tmpFile());
s.listen(helper.tmpFile());
});
it("should get connection", async () => {
@ -109,9 +106,13 @@ describe("net", () => {
}
});
const socketPath = tmpFile();
const socketPath = helper.tmpFile();
s.listen(socketPath);
await new Promise((resolve): void => {
s.on("listening", resolve);
});
const makeConnection = async (): Promise<void> => {
net.createConnection(socketPath);
await Promise.all([
@ -134,4 +135,11 @@ describe("net", () => {
await new Promise((r): nativeNet.Server => s.on("close", r));
});
});
it("should dispose", (done) => {
setTimeout(() => {
client.dispose();
done();
}, 100);
});
});

View File

@ -1,11 +1,11 @@
import { IPty } from "node-pty";
import { createClient } from "@coder/protocol/test";
const client = createClient();
jest.mock("../../ide/src/fill/client", () => ({ client }));
const pty = require("../src/fill/node-pty") as typeof import("node-pty");
import { Module } from "../src/common/proxy";
import { createClient } from "./helpers";
describe("node-pty", () => {
const client = createClient();
const pty = client.modules[Module.NodePty];
/**
* Returns a function that when called returns a promise that resolves with
* the next chunk of data from the process.
@ -47,12 +47,11 @@ describe("node-pty", () => {
const getData = promisifyData(proc);
// First it outputs @hostname:cwd
expect((await getData()).length).toBeGreaterThan(1);
// Then it seems to overwrite that with a shorter prompt in the format of
// [hostname@user]$
expect((await getData())).toContain("$");
// Wait for [hostname@user]$
let data = "";
while (!data.includes("$")) {
data = await getData();
}
proc.kill();
@ -67,33 +66,34 @@ describe("node-pty", () => {
// isn't affected by custom configuration.
const proc = pty.spawn("/bin/bash", ["--rcfile", "/tmp/test/nope/should/not/exist"], {
cols: 10,
rows: 10,
rows: 912,
});
const getData = promisifyData(proc);
// We've already tested these first two bits of output; see shell test.
await getData();
await getData();
proc.write("tput lines\n");
expect(await getData()).toContain("tput");
expect((await getData()).trim()).toContain("10");
proc.resize(10, 50);
// The prompt again.
await getData();
await getData();
let data = "";
while (!data.includes("912")) {
data = await getData();
}
proc.resize(10, 219);
proc.write("tput lines\n");
expect(await getData()).toContain("tput");
expect((await getData())).toContain("50");
while (!data.includes("219")) {
data = await getData();
}
proc.kill();
await new Promise((resolve): void => {
proc.on("exit", resolve);
});
});
it("should dispose", (done) => {
setTimeout(() => {
client.dispose();
done();
}, 100);
});
});

View File

@ -0,0 +1,35 @@
import * as fs from "fs";
import * as util from "util";
import { Module } from "../src/common/proxy";
import { createClient, Helper } from "./helpers";
describe("spdlog", () => {
const client = createClient();
const spdlog = client.modules[Module.Spdlog];
const helper = new Helper("spdlog");
beforeAll(async () => {
await helper.prepare();
});
it("should log to a file", async () => {
const file = await helper.createTmpFile();
const logger = new spdlog.RotatingLogger("test logger", file, 10000, 10);
logger.trace("trace");
logger.debug("debug");
logger.info("info");
logger.warn("warn");
logger.error("error");
logger.critical("critical");
logger.flush();
await new Promise((resolve): number | NodeJS.Timer => setTimeout(resolve, 1000));
expect(await util.promisify(fs.readFile)(file, "utf8"))
.toContain("critical");
});
it("should dispose", () => {
setTimeout(() => {
client.dispose();
}, 100);
});
});

View File

@ -0,0 +1,26 @@
import * as fs from "fs";
import * as util from "util";
import { Module } from "../src/common/proxy";
import { createClient, Helper } from "./helpers";
describe("trash", () => {
const client = createClient();
const trash = client.modules[Module.Trash];
const helper = new Helper("trash");
beforeAll(async () => {
await helper.prepare();
});
it("should trash a file", async () => {
const file = await helper.createTmpFile();
await trash.trash(file);
expect(await util.promisify(fs.exists)(file)).toBeFalsy();
});
it("should dispose", () => {
setTimeout(() => {
client.dispose();
}, 100);
});
});

View File

@ -14,11 +14,43 @@
dependencies:
execa "^0.2.2"
"@types/events@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
"@types/glob@*":
version "7.1.1"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
integrity sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==
dependencies:
"@types/events" "*"
"@types/minimatch" "*"
"@types/node" "*"
"@types/google-protobuf@^3.2.7":
version "3.2.7"
resolved "https://registry.yarnpkg.com/@types/google-protobuf/-/google-protobuf-3.2.7.tgz#9576ed5dd62cdb1c9f952522028a03b7cb2b69b5"
integrity sha512-Pb9wl5qDEwfnJeeu6Zpn5Y+waLrKETStqLZXHMGCTbkNuBBudPy4qOGN6veamyeoUBwTm2knOVeP/FlHHhhmzA==
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==
"@types/node@*":
version "11.11.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-11.11.3.tgz#7c6b0f8eaf16ae530795de2ad1b85d34bf2f5c58"
integrity sha512-wp6IOGu1lxsfnrD+5mX6qwSwWuqsdkKKxTN4aQc4wByHAKZJf9/D4KXPQ1POUjEbnCP5LMggB0OEFNY9OTsMqg==
"@types/rimraf@^2.0.2":
version "2.0.2"
resolved "https://registry.yarnpkg.com/@types/rimraf/-/rimraf-2.0.2.tgz#7f0fc3cf0ff0ad2a99bb723ae1764f30acaf8b6e"
integrity sha512-Hm/bnWq0TCy7jmjeN5bKYij9vw5GrDFWME4IuxV08278NtU/VdGbzsBohcCUJ7+QMqmUq5hpRKB39HeQWJjztQ==
dependencies:
"@types/glob" "*"
"@types/node" "*"
"@types/text-encoding@^0.0.35":
version "0.0.35"
resolved "https://registry.yarnpkg.com/@types/text-encoding/-/text-encoding-0.0.35.tgz#6f14474e0b232bc70c59677aadc65dcc5a99c3a9"
@ -898,7 +930,7 @@ readable-stream@^2.0.6, readable-stream@^2.3.0, readable-stream@^2.3.5:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
rimraf@^2.2.8:
rimraf@^2.2.8, rimraf@^2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab"
integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==
@ -1124,11 +1156,6 @@ ts-protoc-gen@^0.8.0:
dependencies:
google-protobuf "^3.6.1"
tslib@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"

View File

@ -6,9 +6,10 @@
"scripts": {
"start": "node --max-old-space-size=32384 --require ts-node/register --require tsconfig-paths/register src/cli.ts",
"build": "rm -rf ./out && ../../node_modules/.bin/cross-env CLI=true UV_THREADPOOL_SIZE=100 node --max-old-space-size=32384 ../../node_modules/webpack/bin/webpack.js --config ./webpack.config.js",
"build:nexe": "node scripts/nexe.js"
"build:binary": "ts-node scripts/nbin.ts"
},
"dependencies": {
"@coder/nbin": "^1.0.0",
"@oclif/config": "^1.10.4",
"@oclif/errors": "^1.2.2",
"@oclif/plugin-help": "^2.1.4",
@ -32,7 +33,6 @@
"@types/safe-compare": "^1.1.0",
"@types/ws": "^6.0.1",
"fs-extra": "^7.0.1",
"nexe": "^2.0.0-rc.34",
"opn": "^5.4.0",
"string-replace-webpack-plugin": "^0.1.3",
"ts-node": "^7.0.1",

View File

@ -0,0 +1,21 @@
import { Binary } from "@coder/nbin";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
const target = `${os.platform()}-${os.arch()}`;
const rootDir = path.join(__dirname, "..");
const bin = new Binary({
mainFile: path.join(rootDir, "out", "cli.js"),
});
bin.writeFiles(path.join(rootDir, "build", "**"));
bin.writeFiles(path.join(rootDir, "out", "**"));
bin.build().then((binaryData) => {
const outputPath = path.join(__dirname, "..", `cli-${target}`);
fs.writeFileSync(outputPath, binaryData);
fs.chmodSync(outputPath, "755");
}).catch((ex) => {
// tslint:disable-next-line:no-console
console.error(ex);
process.exit(1);
});

View File

@ -1,31 +0,0 @@
const fs = require("fs");
const fse = require("fs-extra");
const os = require("os");
const path = require("path");
const nexePath = require.resolve("nexe");
const shimPath = path.join(path.dirname(nexePath), "lib/steps/shim.js");
let shimContent = fs.readFileSync(shimPath).toString();
const replaceString = `global.nativeFs = { existsSync: originalExistsSync, readFile: originalReadFile, readFileSync: originalReadFileSync, createReadStream: originalCreateReadStream, readdir: originalReaddir, readdirSync: originalReaddirSync, statSync: originalStatSync, stat: originalStat, realpath: originalRealpath, realpathSync: originalRealpathSync };`;
shimContent = shimContent.replace(/compiler\.options\.resources\.length[\s\S]*wrap\("(.*\\n)"/g, (om, a) => {
return om.replace(a, `${a}${replaceString}`);
});
fs.writeFileSync(shimPath, shimContent);
const nexe = require("nexe");
const target = `${os.platform()}-${os.arch()}`;
nexe.compile({
debugBundle: true,
input: path.join(__dirname, "../out/cli.js"),
output: `cli-${target}`,
targets: [target],
/**
* To include native extensions, do NOT install node_modules for each one. They
* are not required as each extension is built using webpack.
*/
resources: [
path.join(__dirname, "../package.json"),
path.join(__dirname, "../build/**/*"),
],
});

View File

@ -9,7 +9,6 @@ import * as os from "os";
import * as path from "path";
import * as WebSocket from "ws";
import { buildDir, cacheHome, dataHome, isCli, serveStatic } from "./constants";
import { fillFs } from "./fill";
import { setup as setupNativeModules } from "./modules";
import { createApp } from "./server";
import { forkModule, requireFork, requireModule } from "./vscode/bootstrapFork";
@ -44,8 +43,10 @@ export class Entry extends Command {
}];
public async run(): Promise<void> {
Error.stackTraceLimit = Infinity;
if (isCli) {
fillFs();
require("nbin").shimNativeFs("/home/kyle/node/coder/code-server/packages/server");
}
const { args, flags } = this.parse(Entry);
@ -182,13 +183,13 @@ export class Entry extends Command {
dataDirectory: dataDir,
workingDirectory: workingDir,
cacheDirectory: cacheHome,
fork: (modulePath: string, args: string[], options: ForkOptions): ChildProcess => {
fork: (modulePath: string, args?: string[], options?: ForkOptions): ChildProcess => {
if (options && options.env && options.env.AMD_ENTRYPOINT) {
return forkModule(options.env.AMD_ENTRYPOINT, args, options, dataDir);
}
if (isCli) {
return spawn(process.execPath, ["--fork", modulePath, "--args", JSON.stringify(args), "--data-dir", dataDir], {
return spawn(process.execPath, [path.join(buildDir, "out", "cli.js"), "--fork", modulePath, "--args", JSON.stringify(args), "--data-dir", dataDir], {
...options,
stdio: [null, null, null, "ipc"],
});

View File

@ -1,195 +0,0 @@
import * as fs from "fs";
import * as path from "path";
import * as util from "util";
import { isCli, buildDir } from "./constants";
// tslint:disable:no-any
const nativeFs = (<any>global).nativeFs as typeof fs || {};
const oldAccess = fs.access;
const existsWithinBinary = (path: fs.PathLike): Promise<boolean> => {
return new Promise<boolean>((resolve): void => {
if (typeof path === "number") {
if (path < 0) {
return resolve(true);
}
}
oldAccess(path, fs.constants.F_OK, (err) => {
const exists = !err;
const es = fs.existsSync(path);
const res = !exists && es;
resolve(res);
});
});
};
export const fillFs = (): void => {
/**
* Refer to https://github.com/nexe/nexe/blob/master/src/fs/patch.ts
* For impls
*/
if (!isCli) {
throw new Error("Should not fill FS when not in CLI");
}
interface FD {
readonly path: string;
position: number;
}
let lastFd = Number.MIN_SAFE_INTEGER;
const fds = new Map<number, FD>();
const replaceNative = <T extends keyof typeof fs>(propertyName: T, func: (callOld: () => void, ...args: any[]) => any, customPromisify?: (...args: any[]) => Promise<any>): void => {
const oldFunc = (<any>fs)[propertyName];
fs[propertyName] = (...args: any[]): any => {
try {
return func(() => {
return oldFunc(...args);
}, ...args);
} catch (ex) {
return oldFunc(...args);
}
};
if (customPromisify) {
(<any>fs[propertyName])[util.promisify.custom] = (...args: any[]): any => {
return customPromisify(...args).catch((ex) => {
throw ex;
});
};
}
};
replaceNative("access", (callNative, path, mode, callback) => {
existsWithinBinary(path).then((exists) => {
if (!exists) {
return callNative();
}
return callback();
});
});
replaceNative("exists", (callOld, path, callback) => {
existsWithinBinary(path).then((exists) => {
if (exists) {
return callback(true);
}
return callOld();
});
}, (path) => new Promise((res): void => fs.exists(path, res)));
replaceNative("open", (callOld, path: fs.PathLike, flags: string | Number, mode: any, callback: any) => {
existsWithinBinary(path).then((exists) => {
if (!exists) {
return callOld();
}
if (typeof mode === "function") {
callback = mode;
mode = undefined;
}
if (path === process.execPath) {
return callOld();
}
const fd = lastFd++;
fds.set(fd, {
path: path.toString(),
position: 0,
});
callback(undefined, fd);
});
});
replaceNative("close", (callOld, fd: number, callback) => {
if (!fds.has(fd)) {
return callOld();
}
fds.delete(fd);
callback();
});
replaceNative("read", (callOld, fd: number, buffer: Buffer, offset: number, length: number, position: number | null, callback?: (err: NodeJS.ErrnoException, bytesRead: number, buffer: Buffer) => void) => {
if (!fds.has(fd)) {
return callOld();
}
const fileDesc = fds.get(fd)!;
return fs.readFile(fileDesc.path, (err, rb) => {
if (err) {
return callOld();
}
rb = rb.slice(position || fileDesc.position);
const sliced = rb.slice(0, length);
if (position === null) {
fileDesc.position += sliced.byteLength;
}
buffer.set(sliced, offset);
if (callback) {
callback(undefined!, sliced.byteLength, buffer);
}
});
}, (fd: number, buffer: Buffer, offset: number, length: number, position: number | null): Promise<{
bytesRead: number;
buffer: Buffer;
}> => {
return new Promise((res, rej): void => {
fs.read(fd, buffer, offset, length, position, (err, bytesRead, buffer) => {
if (err) {
return rej(err);
}
res({
bytesRead,
buffer,
});
});
});
});
replaceNative("readdir", (callOld, directory: string, callback: (err: NodeJS.ErrnoException, paths: string[]) => void) => {
const relative = path.relative(directory, buildDir!);
if (relative.startsWith("..")) {
return callOld();
}
return nativeFs.readdir(directory, callback);
});
const fillNativeFunc = <T extends keyof typeof fs>(propertyName: T): void => {
replaceNative(propertyName, (callOld, newPath, ...args) => {
if (typeof newPath !== "string") {
return callOld();
}
const rel = path.relative(newPath, buildDir!);
if (rel.startsWith("..")) {
return callOld();
}
const func = nativeFs[propertyName] as any;
return func(newPath, ...args);
});
};
const properties: Array<keyof typeof fs> = [
"existsSync",
"readFile",
"readFileSync",
"createReadStream",
"readdir",
"readdirSync",
"statSync",
"stat",
"realpath",
"realpathSync",
];
properties.forEach((p) => fillNativeFunc(p));
};

View File

@ -1,11 +1,13 @@
import { mkdirp } from "fs-extra";
import { logger, field } from "@coder/logger";
import { field, logger } from "@coder/logger";
import { ReadWriteConnection } from "@coder/protocol";
import { Server, ServerOptions } from "@coder/protocol/src/node/server";
import { TunnelCloseCode } from "@coder/tunnel/src/common";
import { handle as handleTunnel } from "@coder/tunnel/src/server";
import * as express from "express";
//@ts-ignore
import * as expressStaticGzip from "express-static-gzip";
import * as fs from "fs";
import { mkdirp } from "fs-extra";
import * as http from "http";
//@ts-ignore
import * as httpolyglot from "httpolyglot";
@ -17,11 +19,9 @@ import * as path from "path";
import * as pem from "pem";
import * as util from "util";
import * as ws from "ws";
import safeCompare = require("safe-compare");
import { TunnelCloseCode } from "@coder/tunnel/src/common";
import { handle as handleTunnel } from "@coder/tunnel/src/server";
import { createPortScanner } from "./portScanner";
import { buildDir } from "./constants";
import { createPortScanner } from "./portScanner";
import safeCompare = require("safe-compare");
interface CreateAppOptions {
registerMiddleware?: (app: express.Application) => void;
@ -180,10 +180,13 @@ export const createApp = async (options: CreateAppOptions): Promise<{
logger.error(error.message);
}
},
onUp: (): void => undefined, // This can't come back up.
onDown: (cb): void => ws.addEventListener("close", () => cb()),
onClose: (cb): void => ws.addEventListener("close", () => cb()),
};
const server = new Server(connection, options.serverOptions);
// tslint:disable-next-line no-unused-expression
new Server(connection, options.serverOptions);
});
const baseDir = buildDir || path.join(__dirname, "..");
@ -202,6 +205,10 @@ export const createApp = async (options: CreateAppOptions): Promise<{
unauthStaticFunc(req, res, next);
}
});
// @ts-ignore
app.use((err, req, res, next) => {
next();
});
app.get("/ping", (req, res) => {
res.json({
hostname: os.hostname(),
@ -235,9 +242,11 @@ export const createApp = async (options: CreateAppOptions): Promise<{
}
const content = await util.promisify(fs.readFile)(fullPath);
res.header("Content-Type", mimeType as string);
res.writeHead(200, {
"Content-Type": mimeType,
"Content-Length": content.byteLength,
});
res.write(content);
res.status(200);
res.end();
} catch (ex) {
res.write(ex.toString());

View File

@ -1,9 +1,8 @@
import * as cp from "child_process";
import * as fs from "fs";
import * as path from "path";
import * as zlib from "zlib";
import * as vm from "vm";
import { isCli } from "../constants";
import { buildDir, isCli } from "../constants";
let ipcMsgBuffer: Buffer[] | undefined = [];
let ipcMsgListener = process.send ? (d: Buffer): number => ipcMsgBuffer!.push(d) : undefined;
@ -11,6 +10,8 @@ if (ipcMsgListener) {
process.on("message", ipcMsgListener);
}
declare var __non_webpack_require__: typeof require;
/**
* Requires a module from the filesystem.
*
@ -29,7 +30,7 @@ const requireFilesystemModule = (id: string, builtInExtensionsDir: string): any
const fileName = id.endsWith(".js") ? id : `${id}.js`;
const req = vm.runInThisContext(mod.wrap(fs.readFileSync(fileName).toString()), {
displayErrors: true,
filename: id + fileName,
filename: fileName,
});
req(customMod.exports, customMod.require.bind(customMod), customMod, fileName, path.dirname(id));
@ -46,6 +47,14 @@ export const requireFork = (modulePath: string, args: string[], builtInExtension
const Module = require("module") as typeof import("module");
const oldRequire = Module.prototype.require;
// tslint:disable-next-line:no-any
const oldLoad = (Module as any)._findPath;
// @ts-ignore
(Module as any)._findPath = function (request, parent, isMain): any {
const lookupPaths = oldLoad.call(this, request, parent, isMain);
return lookupPaths;
};
// tslint:disable-next-line:no-any
Module.prototype.require = function (id: string): any {
if (id === "typescript") {
return require("typescript");
@ -96,23 +105,20 @@ export const requireModule = (modulePath: string, dataDir: string, builtInExtens
*/
// tslint:disable-next-line:no-any
(<any>cp).fork = (modulePath: string, args: ReadonlyArray<string> = [], options?: cp.ForkOptions): cp.ChildProcess => {
return cp.spawn(process.execPath, ["--fork", modulePath, "--args", JSON.stringify(args), "--data-dir", dataDir], {
return cp.spawn(process.execPath, [path.join(buildDir, "out", "cli.js"), "--fork", modulePath, "--args", JSON.stringify(args), "--data-dir", dataDir], {
...options,
stdio: [null, null, null, "ipc"],
});
};
}
let content: Buffer | undefined;
const readFile = (name: string): Buffer => {
return fs.readFileSync(path.join(process.env.BUILD_DIR as string || path.join(__dirname, "../.."), "./build", name));
};
const baseDir = path.join(buildDir, "build");
if (isCli) {
content = zlib.gunzipSync(readFile("bootstrap-fork.js.gz"));
__non_webpack_require__(path.join(baseDir, "bootstrap-fork.js.gz"));
} else {
content = readFile("../../vscode/out/bootstrap-fork.js");
// We need to check `isCli` here to confuse webpack.
require(path.join(__dirname, isCli ? "" : "../../../vscode/out/bootstrap-fork.js"));
}
eval(content.toString());
};
/**
@ -123,12 +129,12 @@ export const requireModule = (modulePath: string, dataDir: string, builtInExtens
* cp.stderr.on("data", (data) => console.log(data.toString("utf8")));
* @param modulePath Path of the VS Code module to load.
*/
export const forkModule = (modulePath: string, args: string[], options: cp.ForkOptions, dataDir?: string): cp.ChildProcess => {
export const forkModule = (modulePath: string, args?: string[], options?: cp.ForkOptions, dataDir?: string): cp.ChildProcess => {
let proc: cp.ChildProcess;
const forkOptions: cp.ForkOptions = {
stdio: [null, null, null, "ipc"],
};
if (options.env) {
if (options && options.env) {
// This prevents vscode from trying to load original-fs from electron.
delete options.env.ELECTRON_RUN_AS_NODE;
forkOptions.env = options.env;
@ -141,7 +147,7 @@ export const forkModule = (modulePath: string, args: string[], options: cp.ForkO
forkArgs.push("--data-dir", dataDir);
}
if (isCli) {
proc = cp.spawn(process.execPath, forkArgs, forkOptions);
proc = cp.spawn(process.execPath, [path.join(buildDir, "out", "cli.js"), ...forkArgs], forkOptions);
} else {
proc = cp.spawn(process.execPath, ["--require", "ts-node/register", "--require", "tsconfig-paths/register", process.argv[1], ...forkArgs], forkOptions);
}

View File

@ -13,6 +13,7 @@ module.exports = merge(
path: path.join(__dirname, "out"),
libraryTarget: "commonjs",
},
mode: "production",
node: {
console: false,
global: false,
@ -27,7 +28,11 @@ module.exports = merge(
"node-pty": "node-pty-prebuilt",
},
},
externals: ["tslib"],
externals: {
"tslib": "commonjs tslib",
"nbin": "commonjs nbin",
"fsevents": "fsevents",
},
entry: "./packages/server/src/cli.ts",
plugins: [
new webpack.DefinePlugin({

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,6 @@
"dependencies": {
"iconv-lite": "^0.4.24",
"onigasm": "^2.2.1",
"spdlog": "^0.7.2",
"string-replace-loader": "^2.1.1",
"tar-stream": "^2.0.1"
},

View File

@ -1,7 +1,7 @@
import * as fs from "fs";
import * as path from "path";
import * as util from "util";
import { Emitter, Event } from "@coder/events";
import { client as ideClient } from "@coder/ide/src/fill/client";
import { $, addClass, append } from "vs/base/browser/dom";
import { HighlightedLabel } from "vs/base/browser/ui/highlightedlabel/highlightedLabel";
import { ObjectTree } from "vs/base/browser/ui/tree/objectTree";
@ -16,8 +16,6 @@ import { IThemeService } from "vs/platform/theme/common/themeService";
import { workbench } from "./workbench";
import "./dialog.scss";
declare var __non_webpack_require__: typeof require;
export enum DialogType {
NewFolder,
Save,
@ -183,15 +181,15 @@ class Dialog {
this.filesNode = document.createElement("div");
this.filesNode.className = "files-list";
this.entryList = new ObjectTree<DialogEntry, string>(this.filesNode, {
getHeight: (entry: DialogEntry): number => {
getHeight: (_entry: DialogEntry): number => {
return 20;
},
getTemplateId: (entry: DialogEntry): string => {
getTemplateId: (_entry: DialogEntry): string => {
return "dialog-entry";
},
}, [new DialogEntryRenderer()], {
openController: {
shouldOpen: (event): boolean => {
shouldOpen: (_event): boolean => {
return true;
},
},
@ -341,7 +339,6 @@ class Dialog {
}
private set path(directory: string) {
const ts = Date.now();
this.list(directory).then((value) => {
this._path = directory;
this.buildPath();
@ -380,32 +377,16 @@ class Dialog {
}
private async list(directory: string): Promise<ReadonlyArray<DialogEntry>> {
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");
const paths = (await util.promisify(fs.readdir)(directory)).sort();
const stats = await Promise.all(paths.map(p => util.promisify(fs.stat)(path.join(directory, p))));
return util.promisify(fs.readdir)(directory).then((paths) => {
paths = paths.sort();
return Promise.all(paths.map(p => util.promisify(fs.stat)(path.join(directory, p)))).then((stats) => {
return {
paths,
stats,
};
});
}).then(({ paths, stats }) => {
return stats.map((stat, index): DialogEntry => {
return {
fullPath: path.join(directory, paths[index]),
name: paths[index],
isDirectory: stat.isDirectory(),
lastModified: stat.mtime.toDateString(),
size: stat.size,
};
});
});
}, directory);
return stats.map((stat, index): DialogEntry => ({
fullPath: path.join(directory, paths[index]),
name: paths[index],
isDirectory: stat.isDirectory(),
lastModified: stat.mtime.toDateString(),
size: stat.size,
}));
}
}
@ -441,7 +422,7 @@ class DialogEntryRenderer implements ITreeRenderer<DialogEntry, string, DialogEn
};
}
public renderElement(node: ITreeNode<DialogEntry, string>, index: number, templateData: DialogEntryData): void {
public renderElement(node: ITreeNode<DialogEntry, string>, _index: number, templateData: DialogEntryData): void {
templateData.icon.className = "dialog-entry-icon monaco-icon-label";
const classes = getIconClasses(
workbench.serviceCollection.get<IModelService>(IModelService) as IModelService,
@ -465,7 +446,7 @@ class DialogEntryRenderer implements ITreeRenderer<DialogEntry, string, DialogEn
templateData.lastModified.innerText = node.element.lastModified;
}
public disposeTemplate(templateData: DialogEntryData): void {
public disposeTemplate(_templateData: DialogEntryData): void {
// throw new Error("Method not implemented.");
}
}

View File

@ -1,94 +1,4 @@
import { Module } from "@coder/protocol";
import { client } from "@coder/ide/src/fill/client";
import { EventEmitter } from "events";
import * as nodePty from "node-pty";
import { ActiveEvalHelper } from "@coder/protocol";
import { logger } from "@coder/logger";
/**
* Implementation of nodePty for the browser.
*/
class Pty implements nodePty.IPty {
private readonly emitter = new EventEmitter();
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) => {
ae.preserveEnv(options);
const ptyProc = ae.modules.pty.spawn(file, args, options);
let process = ptyProc.process;
ae.emit("process", process);
ae.emit("pid", ptyProc.pid);
const timer = setInterval(() => {
if (ptyProc.process !== process) {
process = ptyProc.process;
ae.emit("process", process);
}
}, 200);
ptyProc.on("exit", (code, signal) => {
clearTimeout(timer);
ae.emit("exit", code, signal);
});
ptyProc.on("data", (data) => ae.emit("data", data));
ae.on("resize", (cols: number, rows: number) => ptyProc.resize(cols, rows));
ae.on("write", (data: string) => ptyProc.write(data));
ae.on("kill", (signal: string) => ptyProc.kill(signal));
return {
onDidDispose: (cb): void => ptyProc.on("exit", cb),
dispose: (): void => {
ptyProc.kill();
setTimeout(() => ptyProc.kill("SIGKILL"), 5000); // Double tap.
},
};
}, file, args, options);
this.ae.on("error", (error) => logger.error(error.message));
this.ae.on("pid", (pid) => this._pid = pid);
this.ae.on("process", (process) => this._process = process);
this.ae.on("exit", (code, signal) => this.emitter.emit("exit", code, signal));
this.ae.on("data", (data) => this.emitter.emit("data", data));
}
public get pid(): number {
return this._pid;
}
public get process(): string {
return this._process;
}
// tslint:disable-next-line no-any
public on(event: string, listener: (...args: any[]) => void): void {
this.emitter.on(event, listener);
}
public resize(columns: number, rows: number): void {
this.ae.emit("resize", columns, rows);
}
public write(data: string): void {
this.ae.emit("write", data);
}
public kill(signal?: string): void {
this.ae.emit("kill", signal);
}
}
const ptyType: typeof nodePty = {
spawn: (file: string, args: string[] | string, options: nodePty.IPtyForkOptions): nodePty.IPty => {
return new Pty(file, args, options);
},
};
module.exports = ptyType;
export = client.modules[Module.NodePty];

View File

@ -1,63 +1,4 @@
import { RotatingLogger as NodeRotatingLogger } from "spdlog";
import { logger } from "@coder/logger";
import { Module } from "@coder/protocol";
import { client } from "@coder/ide/src/fill/client";
const ae = client.run((ae) => {
const loggers = new Map<number, NodeRotatingLogger>();
ae.on("new", (id: number, name: string, filePath: string, fileSize: number, fileCount: number) => {
const logger = new ae.modules.spdlog.RotatingLogger(name, filePath, fileSize, fileCount);
loggers.set(id, logger);
});
ae.on("clearFormatters", (id: number) => loggers.get(id)!.clearFormatters());
ae.on("critical", (id: number, message: string) => loggers.get(id)!.critical(message));
ae.on("debug", (id: number, message: string) => loggers.get(id)!.debug(message));
ae.on("drop", (id: number) => loggers.get(id)!.drop());
ae.on("errorLog", (id: number, message: string) => loggers.get(id)!.error(message));
ae.on("flush", (id: number) => loggers.get(id)!.flush());
ae.on("info", (id: number, message: string) => loggers.get(id)!.info(message));
ae.on("setAsyncMode", (bufferSize: number, flushInterval: number) => ae.modules.spdlog.setAsyncMode(bufferSize, flushInterval));
ae.on("setLevel", (id: number, level: number) => loggers.get(id)!.setLevel(level));
ae.on("trace", (id: number, message: string) => loggers.get(id)!.trace(message));
ae.on("warn", (id: number, message: string) => loggers.get(id)!.warn(message));
const disposeCallbacks = <Array<() => void>>[];
return {
onDidDispose: (cb): number => disposeCallbacks.push(cb),
dispose: (): void => {
loggers.forEach((logger) => logger.flush());
loggers.clear();
disposeCallbacks.forEach((cb) => cb());
},
};
});
const spdLogger = logger.named("spdlog");
ae.on("close", () => spdLogger.error("session closed prematurely"));
ae.on("error", (error: Error) => spdLogger.error(error.message));
let id = 0;
export class RotatingLogger implements NodeRotatingLogger {
private readonly id = id++;
public constructor(name: string, filePath: string, fileSize: number, fileCount: number) {
ae.emit("new", this.id, name, filePath, fileSize, fileCount);
}
public trace(message: string): void { ae.emit("trace", this.id, message); }
public debug(message: string): void { ae.emit("debug", this.id, message); }
public info(message: string): void { ae.emit("info", this.id, message); }
public warn(message: string): void { ae.emit("warn", this.id, message); }
public error(message: string): void { ae.emit("errorLog", this.id, message); }
public critical(message: string): void { ae.emit("critical", this.id, message); }
public setLevel(level: number): void { ae.emit("setLevel", this.id, level); }
public clearFormatters(): void { ae.emit("clearFormatters", this.id); }
public flush(): void { ae.emit("flush", this.id); }
public drop(): void { ae.emit("drop", this.id); }
}
export const setAsyncMode = (bufferSize: number, flushInterval: number): void => {
ae.emit("setAsyncMode", bufferSize, flushInterval);
};
export = client.modules[Module.Spdlog];

View File

@ -202,6 +202,8 @@ export class Workbench {
/**
* Resolves the error of the workspace identifier being invalid.
*/
// tslint:disable-next-line:no-console
console.error(ex);
this.workspace = undefined;
location.reload();

View File

@ -34,11 +34,6 @@ big.js@^5.2.2:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
bindings@^1.3.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.1.tgz#21fc7c6d67c18516ec5aaa2815b145ff77b26ea5"
integrity sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==
bl@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88"
@ -114,24 +109,12 @@ lru-cache@^4.1.1:
pseudomap "^1.0.2"
yallist "^2.1.2"
minimist@0.0.8:
version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
mkdirp@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
dependencies:
minimist "0.0.8"
nan@^2.10.0, nan@^2.8.0:
nan@^2.10.0:
version "2.12.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==
@ -194,15 +177,6 @@ schema-utils@^0.4.5:
ajv "^6.1.0"
ajv-keywords "^3.1.0"
spdlog@^0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.7.2.tgz#9298753d7694b9ee9bbfd7e01ea1e4c6ace1e64d"
integrity sha512-rHfWCaWMD4NindDnql6rc6kn7Bs8JR92jhiUpCl3D6v+jYcQ6GozMLig0RliOOR8st5mU+IHLZnr15fBys5x/Q==
dependencies:
bindings "^1.3.0"
mkdirp "^0.5.1"
nan "^2.8.0"
string-replace-loader@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-replace-loader/-/string-replace-loader-2.1.1.tgz#b72e7b57b6ef04efe615aff0ad989b5c14ca63d1"

View File

@ -12,7 +12,6 @@ module.exports = merge(
typescriptCompilerOptions: {
"target": "es5",
"lib": ["dom", "esnext"],
"importHelpers": true,
},
},
), {
@ -58,6 +57,7 @@ module.exports = merge(
"fs": path.join(fills, "fs.ts"),
"net": path.join(fills, "net.ts"),
"util": path.join(fills, "util.ts"),
"trash": path.join(fills, "trash.ts"),
"electron": path.join(fills, "electron.ts"),
"native-keymap": path.join(vsFills, "native-keymap.ts"),

View File

@ -1,3 +1,23 @@
const fs = require("fs");
const util = require("util");
// This isn't properly promisified in Jest.
Object.defineProperty(fs.read, util.promisify.custom, {
configurable: true,
value: (...args) => {
return new Promise((resolve, reject) => {
args.push((error, bytesRead, buffer) => {
if (error) {
reject(error);
} else {
resolve({ bytesRead, buffer });
}
});
fs.read(...args);
});
},
});
global.requestAnimationFrame = (cb) => {
setTimeout(cb, 0);
};

View File

@ -154,12 +154,21 @@ index 25de96d..66e732e 100644
+
+startup({ machineId: "1" });
diff --git a/src/vs/editor/browser/config/configuration.ts b/src/vs/editor/browser/config/configuration.ts
index f4a579b..7f57220 100644
index f4a579b..aa3c6d9 100644
--- a/src/vs/editor/browser/config/configuration.ts
+++ b/src/vs/editor/browser/config/configuration.ts
@@ -353 +353 @@ export class Configuration extends CommonEditorConfiguration {
@@ -10 +9,0 @@ import { Disposable } from 'vs/base/common/lifecycle';
-import * as platform from 'vs/base/common/platform';
@@ -17 +15,0 @@ import { IDimension } from 'vs/editor/common/editorCommon';
-import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
@@ -18,0 +17 @@ import { IAccessibilityService } from 'vs/platform/accessibility/common/accessib
+import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
@@ -353 +352 @@ export class Configuration extends CommonEditorConfiguration {
- if (platform.isMacintosh) {
+ if (browser.isMacintosh) {
@@ -364 +363 @@ export class Configuration extends CommonEditorConfiguration {
- emptySelectionClipboard: browser.isWebKit || browser.isFirefox,
+ emptySelectionClipboard: false, // browser.isWebKit || browser.isFirefox,
diff --git a/src/vs/editor/browser/controller/mouseHandler.ts b/src/vs/editor/browser/controller/mouseHandler.ts
index 1a8af35..fe56af9 100644
--- a/src/vs/editor/browser/controller/mouseHandler.ts
@ -189,11 +198,14 @@ index fa004fb..6420078 100644
- 'included': platform.isMacintosh
+ 'included': platform.isNative && platform.isMacintosh
diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts
index 934e908..b404444 100644
index 934e908..bd6fd88 100644
--- a/src/vs/editor/common/config/editorOptions.ts
+++ b/src/vs/editor/common/config/editorOptions.ts
@@ -9,0 +10 @@ import * as platform from 'vs/base/common/platform';
@@ -6 +6 @@
-import * as nls from 'vs/nls';
+import * as browser from 'vs/base/browser/browser';
@@ -13,0 +14 @@ import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/model/wordHelper';
+import * as nls from 'vs/nls';
@@ -1765 +1766 @@ export class EditorOptionsValidator {
- configuredMulticursorModifier = platform.isMacintosh ? 'metaKey' : 'ctrlKey';
+ configuredMulticursorModifier = browser.isMacintosh ? 'metaKey' : 'ctrlKey';
@ -203,6 +215,9 @@ index 934e908..b404444 100644
@@ -2534 +2535 @@ export const EDITOR_FONT_DEFAULTS = {
- platform.isMacintosh ? 12 : 14
+ browser.isMacintosh ? 12 : 14
@@ -2640 +2641 @@ export const EDITOR_DEFAULTS: IValidatedEditorOptions = {
- selectionClipboard: true,
+ selectionClipboard: false,
diff --git a/src/vs/editor/common/config/fontInfo.ts b/src/vs/editor/common/config/fontInfo.ts
index 88cb52a..c4a1be9 100644
--- a/src/vs/editor/common/config/fontInfo.ts

View File

@ -11,6 +11,7 @@
"strict": true,
"resolveJsonModule": true,
"experimentalDecorators": true,
"importHelpers": true,
"plugins": [
{
"name": "tslint-language-service"
@ -22,6 +23,9 @@
],
"vs/*": [
"./lib/vscode/src/vs/*"
],
"node-pty": [
"./packages/protocol/node_modules/node-pty-prebuilt"
]
}
}

View File

@ -5076,7 +5076,7 @@ tsconfig-paths@^3.8.0:
minimist "^1.2.0"
strip-bom "^3.0.0"
tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0:
tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"
integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==
@ -5284,6 +5284,13 @@ util@^0.10.3:
dependencies:
inherits "2.0.3"
util@^0.11.1:
version "0.11.1"
resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61"
integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==
dependencies:
inherits "2.0.3"
utila@^0.4.0, utila@~0.4:
version "0.4.0"
resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"