Archived
1
0

Make everything use active evals (#30)

* Add trace log level

* Use active eval to implement spdlog

* Split server/client active eval interfaces

Since all properties are *not* valid on both sides

* +200% fire resistance

* Implement exec using active evaluations

* Fully implement child process streams

* Watch impl, move child_process back to explicitly adding events

Automatically forwarding all events might be the right move, but wanna
think/discuss it a bit more because it didn't come out very cleanly.

* Would you like some args with that callback?

* Implement the rest of child_process using active evals

* Rampant memory leaks

Emit "kill" to active evaluations when client disconnects in order to
kill processes. Most likely won't be the final solution.

* Resolve some minor issues with output panel

* Implement node-pty with active evals

* Provide clearTimeout to vm sandbox

* Implement socket with active evals

* Extract some callback logic

Also remove some eval interfaces, need to re-think those.

* Implement net.Server and remainder of net.Socket using active evals

* Implement dispose for active evaluations

* Use trace for express requests

* Handle sending buffers through evaluation events

* Make event logging a bit more clear

* Fix some errors due to us not actually instantiating until connect/listen

* is this a commit message?

* We can just create the evaluator in the ctor

Not sure what I was thinking.

* memory leak for you, memory leak for everyone

* it's a ternary now

* Don't dispose automatically on close or error

The code may or may not be disposable at that point.

* Handle parsing buffers on the client side as well

* Remove unused protobuf

* Remove TypedValue

* Remove unused forkProvider and test

* Improve dispose pattern for active evals

* Socket calls close after error; no need to bind both

* Improve comment

* Comment is no longer wishy washy due to explicit boolean

* Simplify check for sendHandle and options

* Replace _require with __non_webpack_require__

Webpack will then replace this with `require` which we then provide to
the vm sandbox.

* Provide path.parse

* Prevent original-fs from loading

* Start with a pid of -1

vscode immediately checks the PID to see if the debug process launch
correctly, but of course we don't get the pid synchronously.

* Pass arguments to bootstrap-fork

* Fully implement streams

Was causing errors because internally the stream would set this.writing
to true and it would never become false, so subsequent messages would
never send.

* Fix serializing errors and streams emitting errors multiple times

* Was emitting close to data

* Fix missing path for spawned processes

* Move evaluation onDispose call

Now it's accurate and runs when the active evaluation has actually
disposed.

* Fix promisifying fs.exists

* Fix some active eval callback issues

* Patch existsSync in debug adapter
This commit is contained in:
Asher
2019-02-19 10:17:03 -06:00
committed by GitHub
parent 73762017c8
commit 4a80bcb42c
39 changed files with 1694 additions and 8731 deletions

View File

@ -1,85 +1,194 @@
import * as cp from "child_process";
import { Client, useBuffer } from "@coder/protocol";
import * as net from "net";
import * as stream from "stream";
import { CallbackEmitter, ActiveEvalReadable, ActiveEvalWritable, createUniqueEval } from "./evaluation";
import { client } from "./client";
import { promisify } from "./util";
import { promisify } from "util";
class CP {
public constructor(
private readonly client: Client,
) { }
declare var __non_webpack_require__: typeof require;
public exec = (
command: string,
options?: { encoding?: BufferEncoding | string | "buffer" | null } & cp.ExecOptions | null | ((error: Error | null, stdout: string, stderr: string) => void) | ((error: Error | null, stdout: Buffer, stderr: Buffer) => void),
callback?: ((error: Error | null, stdout: string, stderr: string) => void) | ((error: Error | null, stdout: Buffer, stderr: Buffer) => void),
): cp.ChildProcess => {
// TODO: Probably should add an `exec` instead of using `spawn`, especially
// since bash might not be available.
const childProcess = this.client.spawn("bash", ["-c", command.replace(/"/g, "\\\"")]);
class ChildProcess extends CallbackEmitter implements cp.ChildProcess {
private _connected: boolean = false;
private _killed: boolean = false;
private _pid = -1;
public readonly stdin: stream.Writable;
public readonly stdout: stream.Readable;
public readonly stderr: stream.Readable;
// We need the explicit type otherwise TypeScript thinks it is (Writable | Readable)[].
public readonly stdio: [stream.Writable, stream.Readable, stream.Readable] = [this.stdin, this.stdout, this.stderr];
let stdout = "";
childProcess.stdout.on("data", (data) => {
stdout += data.toString();
});
// tslint:disable no-any
public constructor(method: "exec", command: string, options?: { encoding?: string | null } & cp.ExecOptions | null, callback?: (...args: any[]) => void);
public constructor(method: "fork", modulePath: string, options?: cp.ForkOptions, args?: string[]);
public constructor(method: "spawn", command: string, options?: cp.SpawnOptions, args?: string[]);
public constructor(method: "exec" | "spawn" | "fork", command: string, options: object = {}, callback?: string[] | ((...args: any[]) => void)) {
// tslint:enable no-any
super();
let stderr = "";
childProcess.stderr.on("data", (data) => {
stderr += data.toString();
});
childProcess.on("exit", (exitCode) => {
const error = exitCode !== 0 ? new Error(stderr) : null;
if (typeof options === "function") {
callback = options;
}
if (callback) {
// @ts-ignore not sure how to make this work.
callback(
error,
useBuffer(options) ? Buffer.from(stdout) : stdout,
useBuffer(options) ? Buffer.from(stderr) : stderr,
);
}
});
// @ts-ignore TODO: not fully implemented
return childProcess;
}
public fork = (modulePath: string, args?: string[] | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => {
if (options && options.env && options.env.AMD_ENTRYPOINT) {
// @ts-ignore TODO: not fully implemented
return this.client.bootstrapFork(
options.env.AMD_ENTRYPOINT,
Array.isArray(args) ? args : [],
// @ts-ignore TODO: env is a different type
Array.isArray(args) || !args ? options : args,
);
let args: string[] = [];
if (Array.isArray(callback)) {
args = callback;
callback = undefined;
}
// @ts-ignore TODO: not fully implemented
return this.client.fork(
modulePath,
Array.isArray(args) ? args : [],
// @ts-ignore TODO: env is a different type
Array.isArray(args) || !args ? options : args,
);
this.ae = client.run((ae, command, method, args, options, callbackId) => {
const cp = __non_webpack_require__("child_process") as typeof import("child_process");
const { maybeCallback, createUniqueEval, bindWritable, bindReadable, preserveEnv } = __non_webpack_require__("@coder/ide/src/fill/evaluation") as typeof import("@coder/ide/src/fill/evaluation");
preserveEnv(options);
let childProcess: cp.ChildProcess;
switch (method) {
case "exec":
childProcess = cp.exec(command, options, maybeCallback(ae, callbackId));
break;
case "spawn":
childProcess = cp.spawn(command, args, options);
break;
case "fork":
const forkOptions = options as cp.ForkOptions;
if (forkOptions && forkOptions.env && forkOptions.env.AMD_ENTRYPOINT) {
// TODO: This is vscode-specific and should be abstracted.
const { forkModule } = __non_webpack_require__("@coder/server/src/vscode/bootstrapFork") as typeof import ("@coder/server/src/vscode/bootstrapFork");
childProcess = forkModule(forkOptions.env.AMD_ENTRYPOINT, args, forkOptions);
} else {
childProcess = cp.fork(command, args, options);
}
break;
default:
throw new Error(`invalid method ${method}`);
}
ae.on("disconnect", () => childProcess.disconnect());
ae.on("kill", (signal) => childProcess.kill(signal));
ae.on("ref", () => childProcess.ref());
ae.on("send", (message, callbackId) => childProcess.send(message, maybeCallback(ae, callbackId)));
ae.on("unref", () => childProcess.unref());
ae.emit("pid", childProcess.pid);
childProcess.on("close", (code, signal) => ae.emit("close", code, signal));
childProcess.on("disconnect", () => ae.emit("disconnect"));
childProcess.on("error", (error) => ae.emit("error", error));
childProcess.on("exit", (code, signal) => ae.emit("exit", code, signal));
childProcess.on("message", (message) => ae.emit("message", message));
bindWritable(createUniqueEval(ae, "stdin"), childProcess.stdin);
bindReadable(createUniqueEval(ae, "stdout"), childProcess.stdout);
bindReadable(createUniqueEval(ae, "stderr"), childProcess.stderr);
return {
onDidDispose: (cb): cp.ChildProcess => childProcess.on("close", cb),
dispose: (): void => {
childProcess.kill();
setTimeout(() => childProcess.kill("SIGKILL"), 5000); // Double tap.
},
};
}, command, method, args, options, this.storeCallback(callback));
this.ae.on("pid", (pid) => {
this._pid = pid;
this._connected = true;
});
this.stdin = new ActiveEvalWritable(createUniqueEval(this.ae, "stdin"));
this.stdout = new ActiveEvalReadable(createUniqueEval(this.ae, "stdout"));
this.stderr = new ActiveEvalReadable(createUniqueEval(this.ae, "stderr"));
this.ae.on("close", (code, signal) => this.emit("close", code, signal));
this.ae.on("disconnect", () => this.emit("disconnect"));
this.ae.on("error", (error) => this.emit("error", error));
this.ae.on("exit", (code, signal) => {
this._connected = false;
this._killed = true;
this.emit("exit", code, signal);
});
this.ae.on("message", (message) => this.emit("message", message));
}
public spawn = (command: string, args?: string[] | cp.SpawnOptions, options?: cp.SpawnOptions): cp.ChildProcess => {
// @ts-ignore TODO: not fully implemented
return this.client.spawn(
command,
Array.isArray(args) ? args : [],
// @ts-ignore TODO: env is a different type
Array.isArray(args) || !args ? options : args,
);
public get pid(): number { return this._pid; }
public get connected(): boolean { return this._connected; }
public get killed(): boolean { return this._killed; }
public kill(): void { this.ae.emit("kill"); }
public disconnect(): void { this.ae.emit("disconnect"); }
public ref(): void { this.ae.emit("ref"); }
public unref(): void { this.ae.emit("unref"); }
public send(
message: any, // tslint:disable-line no-any to match spec
sendHandle?: net.Socket | net.Server | ((error: Error) => void),
options?: cp.MessageOptions | ((error: Error) => void),
callback?: (error: Error) => void): boolean {
if (typeof sendHandle === "function") {
callback = sendHandle;
sendHandle = undefined;
} else if (typeof options === "function") {
callback = options;
options = undefined;
}
if (sendHandle || options) {
throw new Error("sendHandle and options are not supported");
}
this.ae.emit("send", message, this.storeCallback(callback));
// Unfortunately this will always have to be true since we can't retrieve
// the actual response synchronously.
return true;
}
}
const fillCp = new CP(client);
class CP {
public readonly ChildProcess = ChildProcess;
// tslint:disable-next-line no-any makes util.promisify return an object
(fillCp as any).exec[promisify.customPromisifyArgs] = ["stdout", "stderr"];
public exec = (
command: string,
options?: { encoding?: string | null } & cp.ExecOptions | null | ((error: cp.ExecException | null, stdout: string, stderr: string) => void) | ((error: cp.ExecException | null, stdout: Buffer, stderr: Buffer) => void),
callback?: ((error: cp.ExecException | null, stdout: string, stderr: string) => void) | ((error: cp.ExecException | null, stdout: Buffer, stderr: Buffer) => void),
): cp.ChildProcess => {
if (typeof options === "function") {
callback = options;
options = undefined;
}
return new ChildProcess("exec", command, options, callback);
}
public fork = (modulePath: string, args?: string[] | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => {
if (args && !Array.isArray(args)) {
options = args;
args = undefined;
}
return new ChildProcess("fork", modulePath, options, args);
}
public spawn = (command: string, args?: string[] | cp.SpawnOptions, options?: cp.SpawnOptions): cp.ChildProcess => {
if (args && !Array.isArray(args)) {
options = args;
args = undefined;
}
return new ChildProcess("spawn", command, options, args);
}
}
const fillCp = new CP();
// Methods that don't follow the standard callback pattern (an error followed
// by a single result) need to provide a custom promisify function.
Object.defineProperty(fillCp.exec, promisify.custom, {
value: (
command: string,
options?: { encoding?: string | null } & cp.ExecOptions | null,
): Promise<{ stdout: string | Buffer, stderr: string | Buffer }> => {
return new Promise((resolve, reject): void => {
fillCp.exec(command, options, (error: cp.ExecException | null, stdout: string | Buffer, stderr: string | Buffer) => {
if (error) {
reject(error);
} else {
resolve({ stdout, stderr });
}
});
});
},
});
export = fillCp;

View File

@ -6,8 +6,7 @@ import { IKey, Dialog as DialogBox } from "./dialog";
import { clipboard } from "./clipboard";
import { client } from "./client";
// Use this to get around Webpack inserting our fills.
declare var _require: typeof require;
declare var __non_webpack_require__: typeof require;
// tslint:disable-next-line no-any
(global as any).getOpenUrls = (): string[] => {
@ -99,7 +98,7 @@ class Clipboard {
class Shell {
public async moveItemToTrash(path: string): Promise<void> {
await client.evaluate((path) => {
const trash = _require("trash") as typeof import("trash");
const trash = __non_webpack_require__("trash") as typeof import("trash");
return trash(path);
}, path);

View File

@ -0,0 +1,338 @@
import { SpawnOptions, ForkOptions } from "child_process";
import { EventEmitter } from "events";
import { Socket } from "net";
import { Duplex, Readable, Writable } from "stream";
import { logger } from "@coder/logger";
import { ActiveEval, Disposer } from "@coder/protocol";
// tslint:disable no-any
/**
* If there is a callback ID, return a function that emits the callback event on
* the active evaluation with that ID and all arguments passed to it. Otherwise,
* return undefined.
*/
export const maybeCallback = (ae: ActiveEval, callbackId?: number): ((...args: any[]) => void) | undefined => {
return typeof callbackId !== "undefined" ? (...args: any[]): void => {
ae.emit("callback", callbackId, ...args);
} : undefined;
};
// Some spawn code tries to preserve the env (the debug adapter for
// instance) but the env is mostly blank (since we're in the browser), so
// we'll just always preserve the main process.env here, otherwise it
// won't have access to PATH, etc.
// TODO: An alternative solution would be to send the env to the browser?
export const preserveEnv = (options: SpawnOptions | ForkOptions): void => {
if (options && options.env) {
options.env = { ...process.env, ...options.env };
}
};
/**
* Bind a socket to an active evaluation.
*/
export const bindSocket = (ae: ActiveEval, socket: Socket): Disposer => {
socket.on("connect", () => ae.emit("connect"));
socket.on("lookup", (error, address, family, host) => ae.emit("lookup", error, address, family, host));
socket.on("timeout", () => ae.emit("timeout"));
ae.on("connect", (options, callbackId) => socket.connect(options, maybeCallback(ae, callbackId)));
ae.on("ref", () => socket.ref());
ae.on("setKeepAlive", (enable, initialDelay) => socket.setKeepAlive(enable, initialDelay));
ae.on("setNoDelay", (noDelay) => socket.setNoDelay(noDelay));
ae.on("setTimeout", (timeout, callbackId) => socket.setTimeout(timeout, maybeCallback(ae, callbackId)));
ae.on("unref", () => socket.unref());
bindReadable(ae, socket);
bindWritable(ae, socket);
return {
onDidDispose: (cb): Socket => socket.on("close", cb),
dispose: (): void => {
socket.removeAllListeners();
socket.end();
socket.destroy();
socket.unref();
},
};
};
/**
* Bind a writable stream to an active evaluation.
*/
export const bindWritable = (ae: ActiveEval, writable: Writable | Duplex): void => {
if (!((writable as Readable).read)) { // To avoid binding twice.
writable.on("close", () => ae.emit("close"));
writable.on("error", (error) => ae.emit("error", error));
ae.on("destroy", () => writable.destroy());
}
writable.on("drain", () => ae.emit("drain"));
writable.on("finish", () => ae.emit("finish"));
writable.on("pipe", () => ae.emit("pipe"));
writable.on("unpipe", () => ae.emit("unpipe"));
ae.on("cork", () => writable.cork());
ae.on("end", (chunk, encoding, callbackId) => writable.end(chunk, encoding, maybeCallback(ae, callbackId)));
ae.on("setDefaultEncoding", (encoding) => writable.setDefaultEncoding(encoding));
ae.on("uncork", () => writable.uncork());
// Sockets can pass an fd instead of a callback but streams cannot.
ae.on("write", (chunk, encoding, fd, callbackId) => writable.write(chunk, encoding, maybeCallback(ae, callbackId) || fd));
};
/**
* Bind a readable stream to an active evaluation.
*/
export const bindReadable = (ae: ActiveEval, readable: Readable): void => {
// Streams don't have an argument on close but sockets do.
readable.on("close", (...args: any[]) => ae.emit("close", ...args));
readable.on("data", (data) => ae.emit("data", data));
readable.on("end", () => ae.emit("end"));
readable.on("error", (error) => ae.emit("error", error));
readable.on("readable", () => ae.emit("readable"));
ae.on("destroy", () => readable.destroy());
ae.on("pause", () => readable.pause());
ae.on("push", (chunk, encoding) => readable.push(chunk, encoding));
ae.on("resume", () => readable.resume());
ae.on("setEncoding", (encoding) => readable.setEncoding(encoding));
ae.on("unshift", (chunk) => readable.unshift(chunk));
};
/**
* Wrap an evaluation emitter to make unique events for an item to prevent
* conflicts when it shares that emitter with other items.
*/
export const createUniqueEval = (ae: ActiveEval, id: number | "stdout" | "stderr" | "stdin"): ActiveEval => {
let events = <string[]>[];
return {
removeAllListeners: (event?: string): void => {
if (!event) {
events.forEach((e) => ae.removeAllListeners(e));
events = [];
} else {
const index = events.indexOf(event);
if (index !== -1) {
events.splice(index, 1);
ae.removeAllListeners(`${event}:${id}`);
}
}
},
emit: (event: string, ...args: any[]): void => {
ae.emit(`${event}:${id}`, ...args);
},
on: (event: string, cb: (...args: any[]) => void): void => {
if (!events.includes(event)) {
events.push(event);
}
ae.on(`${event}:${id}`, cb);
},
};
};
/**
* An event emitter that can store callbacks with IDs in a map so we can pass
* them back and forth through an active evaluation using those IDs.
*/
export class CallbackEmitter extends EventEmitter {
private _ae: ActiveEval | undefined;
private callbackId = 0;
private readonly callbacks = new Map<number, Function>();
public constructor(ae?: ActiveEval) {
super();
if (ae) {
this.ae = ae;
}
}
protected get ae(): ActiveEval {
if (!this._ae) {
throw new Error("trying to access active evaluation before it has been set");
}
return this._ae;
}
protected set ae(ae: ActiveEval) {
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: ActiveEval) {
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: ActiveEval) {
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: ActiveEval) {
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

@ -1,13 +1,11 @@
import { exec, ChildProcess } from "child_process";
import { EventEmitter } from "events";
import * as fs from "fs";
import * as stream from "stream";
import { Client, IEncodingOptions, IEncodingOptionsCallback, escapePath, useBuffer } from "@coder/protocol";
import { Client, IEncodingOptions, IEncodingOptionsCallback } from "@coder/protocol";
import { client } from "./client";
import { promisify } from "util";
// Use this to get around Webpack inserting our fills.
// TODO: is there a better way?
declare var _require: typeof require;
declare var __non_webpack_require__: typeof require;
declare var _Buffer: typeof Buffer;
/**
@ -29,8 +27,8 @@ class FS {
mode = undefined;
}
this.client.evaluate((path, mode) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -47,8 +45,8 @@ class FS {
options = undefined;
}
this.client.evaluate((path, data, options) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -60,8 +58,8 @@ class FS {
public chmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((path, mode) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -73,8 +71,8 @@ class FS {
public chown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((path, uid, gid) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -86,8 +84,8 @@ class FS {
public close = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((fd) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -102,8 +100,8 @@ class FS {
callback = flags;
}
this.client.evaluate((src, dest, flags) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -113,19 +111,21 @@ class FS {
});
}
/**
* This should NOT be used for long-term writes.
* The runnable will be killed after the timeout specified in evaluate.ts
*/
// 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 = _require("fs") as typeof import("fs");
const fs = __non_webpack_require__("fs") as typeof import("fs");
const str = fs.createWriteStream(path, options);
ae.on("write", (d, e) => str.write(_Buffer.from(d, "utf8")));
ae.on("write", (d) => str.write(_Buffer.from(d, "utf8")));
ae.on("close", () => str.close());
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 {
@ -134,7 +134,7 @@ class FS {
public constructor() {
super({
write: (data, encoding, cb) => {
write: (data, encoding, cb): void => {
this._bytesWritten += data.length;
ae.emit("write", Buffer.from(data, encoding), encoding);
cb();
@ -162,8 +162,8 @@ class FS {
public exists = (path: fs.PathLike, callback: (exists: boolean) => void): void => {
this.client.evaluate((path) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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) => {
@ -175,8 +175,8 @@ class FS {
public fchmod = (fd: number, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((fd, mode) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -188,8 +188,8 @@ class FS {
public fchown = (fd: number, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((fd, uid, gid) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -201,8 +201,8 @@ class FS {
public fdatasync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((fd) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -214,9 +214,9 @@ class FS {
public fstat = (fd: number, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
this.client.evaluate((fd) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
const tslib = _require("tslib") as typeof import("tslib");
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, {
@ -238,8 +238,8 @@ class FS {
public fsync = (fd: number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((fd) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -255,8 +255,8 @@ class FS {
len = undefined;
}
this.client.evaluate((fd, len) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -268,8 +268,8 @@ class FS {
public futimes = (fd: number, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((fd, atime, mtime) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -281,8 +281,8 @@ class FS {
public lchmod = (path: fs.PathLike, mode: string | number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((path, mode) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -294,8 +294,8 @@ class FS {
public lchown = (path: fs.PathLike, uid: number, gid: number, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((path, uid, gid) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -307,8 +307,8 @@ class FS {
public link = (existingPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((existingPath, newPath) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -320,9 +320,9 @@ class FS {
public lstat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
this.client.evaluate((path) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
const tslib = _require("tslib") as typeof import("tslib");
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, {
@ -348,8 +348,8 @@ class FS {
mode = undefined;
}
this.client.evaluate((path, mode) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -365,8 +365,8 @@ class FS {
options = undefined;
}
this.client.evaluate((prefix, options) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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) => {
@ -382,8 +382,8 @@ class FS {
mode = undefined;
}
this.client.evaluate((path, flags, mode) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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) => {
@ -395,8 +395,8 @@ class FS {
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((fd, length, position) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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) => {
@ -420,8 +420,8 @@ class FS {
options = undefined;
}
this.client.evaluate((path, options) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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) => {
@ -438,8 +438,8 @@ class FS {
}
// TODO: options can also take `withFileTypes` but the types aren't working.
this.client.evaluate((path, options) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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) => {
@ -455,8 +455,8 @@ class FS {
options = undefined;
}
this.client.evaluate((path, options) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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) => {
@ -472,8 +472,8 @@ class FS {
options = undefined;
}
this.client.evaluate((path, options) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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) => {
@ -485,8 +485,8 @@ class FS {
public rename = (oldPath: fs.PathLike, newPath: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((oldPath, newPath) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -498,8 +498,8 @@ class FS {
public rmdir = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((path) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -511,9 +511,9 @@ class FS {
public stat = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException, stats: fs.Stats) => void): void => {
this.client.evaluate((path) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
const tslib = _require("tslib") as typeof import("tslib");
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, {
@ -543,8 +543,8 @@ class FS {
type = undefined;
}
this.client.evaluate((target, path, type) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -560,8 +560,8 @@ class FS {
len = undefined;
}
this.client.evaluate((path, len) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -573,8 +573,8 @@ class FS {
public unlink = (path: fs.PathLike, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((path) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -586,8 +586,8 @@ class FS {
public utimes = (path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date, callback: (err: NodeJS.ErrnoException) => void): void => {
this.client.evaluate((path, atime, mtime) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -603,8 +603,8 @@ class FS {
position = undefined;
}
this.client.evaluate((fd, buffer, offset, length, position) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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 {
@ -626,8 +626,8 @@ class FS {
options = undefined;
}
this.client.evaluate((path, data, options) => {
const fs = _require("fs") as typeof import("fs");
const util = _require("util") as typeof import("util");
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(() => {
@ -637,63 +637,40 @@ class FS {
});
}
public watch = (filename: fs.PathLike, options: IEncodingOptions, listener?: ((event: string, filename: string) => void) | ((event: string, filename: Buffer) => void)): fs.FSWatcher => {
// TODO: can we modify `evaluate` for long-running processes like watch?
// Especially since inotifywait might not be available.
const buffer = new NewlineInputBuffer((msg): void => {
msg = msg.trim();
const index = msg.lastIndexOf(":");
const events = msg.substring(index + 1).split(",");
const baseFilename = msg.substring(0, index).split("/").pop();
events.forEach((event) => {
switch (event) {
// Rename is emitted when a file appears or disappears in the directory.
case "CREATE":
case "DELETE":
case "MOVED_FROM":
case "MOVED_TO":
watcher.emit("rename", baseFilename);
break;
case "CLOSE_WRITE":
watcher.emit("change", baseFilename);
break;
}
});
});
// TODO: `exec` is undefined for some reason.
const process = exec(`inotifywait ${escapePath(filename.toString())} -m --format "%w%f:%e"`);
process.on("exit", (exitCode) => {
watcher.emit("error", new Error(`process terminated unexpectedly with code ${exitCode}`));
});
process.stdout.on("data", (data) => {
buffer.push(data);
});
const watcher = new Watcher(process);
if (listener) {
const l = listener;
watcher.on("change", (filename) => {
// @ts-ignore not sure how to make this work.
l("change", useBuffer(options) ? Buffer.from(filename) : filename);
});
watcher.on("rename", (filename) => {
// @ts-ignore not sure how to make this work.
l("rename", useBuffer(options) ? Buffer.from(filename) : filename);
});
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;
}
return watcher;
}
}
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());
class Watcher extends EventEmitter implements fs.FSWatcher {
public constructor(private readonly process: ChildProcess) {
super();
}
return {
onDidDispose: (cb): void => ae.on("close", cb),
dispose: (): void => watcher.close(),
};
}, filename.toString(), !!listener, options);
public close(): void {
this.process.kill();
return new class Watcher extends EventEmitter implements fs.FSWatcher {
public constructor() {
super();
ae.on("change", (event, filename) => this.emit("change", event, filename));
ae.on("error", (error) => this.emit("error", error));
ae.on("listener", (event, filename) => listener && listener(event, filename));
}
public close(): void {
ae.emit("close");
}
};
}
}
@ -765,38 +742,10 @@ class Stats implements fs.Stats {
}
}
/**
* Class for safely taking input and turning it into separate messages.
* Assumes that messages are split by newlines.
*/
class NewlineInputBuffer {
private callback: (msg: string) => void;
private buffer: string | undefined;
public constructor(callback: (msg: string) => void) {
this.callback = callback;
}
/**
* Add data to be buffered.
*/
public push(data: string | Uint8Array): void {
let input = typeof data === "string" ? data : data.toString();
if (this.buffer) {
input = this.buffer + input;
this.buffer = undefined;
}
const lines = input.split("\n");
const length = lines.length - 1;
const lastLine = lines[length];
if (lastLine.length > 0) {
this.buffer = lastLine;
}
lines.pop(); // This is either the line we buffered or an empty string.
for (let i = 0; i < length; ++i) {
this.callback(lines[i]);
}
}
}
export = new FS(client);
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;

View File

@ -1,55 +1,256 @@
import * as net from "net";
import { Client } from "@coder/protocol";
import { ActiveEval } from "@coder/protocol";
import { CallbackEmitter, ActiveEvalDuplex, createUniqueEval } from "./evaluation";
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?: ActiveEval) {
super(ae || client.run((ae, options) => {
const net = __non_webpack_require__("net") as typeof import("net");
const { bindSocket } = __non_webpack_require__("@coder/ide/src/fill/evaluation") as typeof import("@coder/ide/src/fill/evaluation");
return bindSocket(ae, new net.Socket(options));
}, 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");
const { maybeCallback, bindSocket, createUniqueEval } = __non_webpack_require__("@coder/ide/src/fill/evaluation") as typeof import("@coder/ide/src/fill/evaluation");
let connectionId = 0;
let server = new net.Server(options, maybeCallback(ae, callbackId));
const sockets = new Map<number, net.Socket>();
const storeSocket = (socket: net.Socket): number => {
const socketId = connectionId++;
sockets.set(socketId, socket);
const disposer = bindSocket(createUniqueEval(ae, socketId), socket);
socket.on("close", () => {
disposer.dispose();
sockets.delete(socketId);
});
return socketId;
};
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) => server.close(maybeCallback(ae, callbackId)));
ae.on("listen", (handle) => 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 socket = new Socket(undefined, createUniqueEval(this.ae, socketId));
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 {
public constructor(
private readonly client: Client,
) {}
public get Socket(): typeof net.Socket {
return this.client.Socket;
}
public get Server(): typeof net.Server {
throw new Error("not implemented");
}
public connect(): net.Socket {
throw new Error("not implemented");
}
// @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 {
if (typeof target === "object") {
throw new Error("not implemented");
}
const socket = new Socket();
socket.connect(target, host, callback);
return this.client.createConnection(target, typeof host === "function" ? host : callback) as net.Socket;
}
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");
return socket;
}
public createServer(
_options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean } | ((socket: net.Socket) => void),
_connectionListener?: (socket: net.Socket) => void,
options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean } | ((socket: net.Socket) => void),
connectionListener?: (socket: net.Socket) => void,
): net.Server {
return this.client.createServer() as 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(client);
export = new Net();