mirror of
https://github.com/coder/code-server.git
synced 2026-05-12 23:37:25 +02:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
83
packages/protocol/src/common/proxy.ts
Normal file
83
packages/protocol/src/common/proxy.ts
Normal 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",
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user