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

@@ -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);
}
}