Fix coping and moving files around using the file tree (#568)

* Implement write/read buffers in electron fill

This makes cutting and copy files from the file tree work.

* Implement fs.createReadStream

This is used by the file tree to copy files.

* Allow passing proxies back from client to server

This makes things like piping streams possible.

* Synchronously bind to proxy events

This eliminates any chance whatsoever of missing events due to binding
too late.

* Make it possible to bind some events on demand

* Add some protocol documentation
This commit is contained in:
Asher
2019-04-24 10:38:21 -05:00
committed by GitHub
parent 30b8565e2d
commit c9f91e77cd
21 changed files with 546 additions and 278 deletions

View File

@@ -7,29 +7,35 @@ 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) {}
export class ChildProcessProxy extends ServerProxy<cp.ChildProcess> {
public constructor(instance: cp.ChildProcess) {
super({
bindEvents: ["close", "disconnect", "error", "exit", "message"],
doneEvents: ["close"],
instance,
});
}
public async kill(signal?: string): Promise<void> {
this.process.kill(signal);
this.instance.kill(signal);
}
public async disconnect(): Promise<void> {
this.process.disconnect();
this.instance.disconnect();
}
public async ref(): Promise<void> {
this.process.ref();
this.instance.ref();
}
public async unref(): Promise<void> {
this.process.unref();
this.instance.unref();
}
// tslint:disable-next-line no-any
public async send(message: any): Promise<void> {
return new Promise((resolve, reject): void => {
this.process.send(message, (error) => {
this.instance.send(message, (error) => {
if (error) {
reject(error);
} else {
@@ -40,25 +46,13 @@ export class ChildProcessProxy implements ServerProxy {
}
public async getPid(): Promise<number> {
return this.process.pid;
}
public async onDone(cb: () => void): Promise<void> {
this.process.on("close", cb);
return this.instance.pid;
}
public async dispose(): Promise<void> {
this.process.kill();
setTimeout(() => this.process.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));
this.instance.kill();
setTimeout(() => this.instance.kill("SIGKILL"), 5000); // Double tap.
await super.dispose();
}
}
@@ -98,8 +92,10 @@ export class ChildProcessModuleProxy {
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),
// Child processes streams appear to immediately flow so we need to bind
// to the data event right away.
stdout: process.stdout && new ReadableProxy(process.stdout, ["data"]),
stderr: process.stderr && new ReadableProxy(process.stderr, ["data"]),
};
}
}

View File

@@ -2,9 +2,9 @@ import * as fs from "fs";
import { promisify } from "util";
import { ServerProxy } from "../../common/proxy";
import { IEncodingOptions } from "../../common/util";
import { WritableProxy } from "./stream";
import { ReadableProxy, WritableProxy } from "./stream";
// tslint:disable completed-docs
// tslint:disable completed-docs no-any
/**
* A serializable version of fs.Stats.
@@ -37,45 +37,52 @@ export interface Stats {
_isSocket: boolean;
}
export class WriteStreamProxy extends WritableProxy<fs.WriteStream> {
export class ReadStreamProxy extends ReadableProxy<fs.ReadStream> {
public constructor(stream: fs.ReadStream) {
super(stream, ["open"]);
}
public async close(): Promise<void> {
this.stream.close();
this.instance.close();
}
public async dispose(): Promise<void> {
this.instance.close();
await super.dispose();
this.stream.close();
}
// tslint:disable-next-line no-any
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
await super.onEvent(cb);
this.stream.on("open", (fd) => cb("open", fd));
}
}
export class WatcherProxy implements ServerProxy {
public constructor(private readonly watcher: fs.FSWatcher) {}
export class WriteStreamProxy extends WritableProxy<fs.WriteStream> {
public constructor(stream: fs.WriteStream) {
super(stream, ["open"]);
}
public async close(): Promise<void> {
this.watcher.close();
this.instance.close();
}
public async dispose(): Promise<void> {
this.watcher.close();
this.watcher.removeAllListeners();
this.instance.close();
await super.dispose();
}
}
export class WatcherProxy extends ServerProxy<fs.FSWatcher> {
public constructor(watcher: fs.FSWatcher) {
super({
bindEvents: ["change", "close", "error"],
doneEvents: ["close", "error"],
instance: watcher,
});
}
public async onDone(cb: () => void): Promise<void> {
this.watcher.on("close", cb);
this.watcher.on("error", cb);
public async close(): Promise<void> {
this.instance.close();
}
// 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));
public async dispose(): Promise<void> {
this.instance.close();
await super.dispose();
}
}
@@ -84,7 +91,6 @@ export class FsModuleProxy {
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);
}
@@ -105,7 +111,10 @@ export class FsModuleProxy {
return promisify(fs.copyFile)(src, dest, flags);
}
// tslint:disable-next-line no-any
public async createReadStream(path: fs.PathLike, options?: any): Promise<ReadStreamProxy> {
return new ReadStreamProxy(fs.createReadStream(path, options));
}
public async createWriteStream(path: fs.PathLike, options?: any): Promise<WriteStreamProxy> {
return new WriteStreamProxy(fs.createWriteStream(path, options));
}
@@ -236,7 +245,6 @@ export class FsModuleProxy {
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);
}

View File

@@ -2,78 +2,65 @@ import * as net from "net";
import { ServerProxy } from "../../common/proxy";
import { DuplexProxy } from "./stream";
// tslint:disable completed-docs
// tslint:disable completed-docs no-any
export class NetSocketProxy extends DuplexProxy<net.Socket> {
public constructor(socket: net.Socket) {
super(socket, ["connect", "lookup", "timeout"]);
}
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
this.instance.connect(options as any, host as any);
}
public async unref(): Promise<void> {
this.stream.unref();
this.instance.unref();
}
public async ref(): Promise<void> {
this.stream.ref();
this.instance.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> {
await 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"));
this.instance.end();
this.instance.destroy();
this.instance.unref();
await super.dispose();
}
}
export class NetServerProxy implements ServerProxy {
public constructor(private readonly server: net.Server) {}
export class NetServerProxy extends ServerProxy<net.Server> {
public constructor(instance: net.Server) {
super({
bindEvents: ["close", "error", "listening"],
doneEvents: ["close"],
instance,
});
}
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
this.instance.listen(handle, hostname as any, backlog as any);
}
public async ref(): Promise<void> {
this.server.ref();
this.instance.ref();
}
public async unref(): Promise<void> {
this.server.unref();
this.instance.unref();
}
public async close(): Promise<void> {
this.server.close();
this.instance.close();
}
public async onConnection(cb: (proxy: NetSocketProxy) => void): Promise<void> {
this.server.on("connection", (socket) => cb(new NetSocketProxy(socket)));
this.instance.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"));
this.instance.close();
this.instance.removeAllListeners();
}
}
@@ -83,7 +70,7 @@ export class NetModuleProxy {
}
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
return new NetSocketProxy(net.createConnection(target as any, host));
}
public async createServer(options?: { allowHalfOpen?: boolean, pauseOnConnect?: boolean }): Promise<NetServerProxy> {

View File

@@ -9,18 +9,25 @@ import { preserveEnv } from "../../common/util";
/**
* Server-side IPty proxy.
*/
export class NodePtyProcessProxy implements ServerProxy {
private readonly emitter = new EventEmitter();
export class NodePtyProcessProxy extends ServerProxy {
public constructor(private readonly process: pty.IPty) {
super({
bindEvents: ["process", "data", "exit"],
doneEvents: ["exit"],
instance: new EventEmitter(),
});
this.process.on("data", (data) => this.instance.emit("data", data));
this.process.on("exit", (exitCode, signal) => this.instance.emit("exit", exitCode, signal));
let name = process.process;
setTimeout(() => { // Need to wait for the caller to listen to the event.
this.emitter.emit("process", name);
this.instance.emit("process", name);
}, 1);
const timer = setInterval(() => {
if (process.process !== name) {
name = process.process;
this.emitter.emit("process", name);
this.instance.emit("process", name);
}
}, 200);
@@ -47,21 +54,10 @@ export class NodePtyProcessProxy implements ServerProxy {
this.process.write(data);
}
public async onDone(cb: () => void): Promise<void> {
this.process.on("exit", cb);
}
public async dispose(): Promise<void> {
this.process.kill();
setTimeout(() => this.process.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));
await super.dispose();
}
}

View File

@@ -5,10 +5,14 @@ import { ServerProxy } from "../../common/proxy";
// tslint:disable completed-docs
export class RotatingLoggerProxy implements ServerProxy {
private readonly emitter = new EventEmitter();
public constructor(private readonly logger: spdlog.RotatingLogger) {}
export class RotatingLoggerProxy extends ServerProxy<EventEmitter> {
public constructor(private readonly logger: spdlog.RotatingLogger) {
super({
bindEvents: [],
doneEvents: ["dispose"],
instance: new EventEmitter(),
});
}
public async trace (message: string): Promise<void> { this.logger.trace(message); }
public async debug (message: string): Promise<void> { this.logger.debug(message); }
@@ -21,19 +25,10 @@ export class RotatingLoggerProxy implements ServerProxy {
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> {
await 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.
this.instance.emit("dispose");
await super.dispose();
}
}

View File

@@ -1,32 +1,38 @@
import { EventEmitter } from "events";
import * as stream from "stream";
import { ServerProxy } from "../../common/proxy";
// tslint:disable completed-docs
// tslint:disable completed-docs no-any
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();
export class WritableProxy<T extends stream.Writable = stream.Writable> extends ServerProxy<T> {
public constructor(instance: T, bindEvents: string[] = [], delayedEvents?: string[]) {
super({
bindEvents: ["close", "drain", "error", "finish"].concat(bindEvents),
doneEvents: ["close"],
delayedEvents,
instance,
});
}
public async destroy(): Promise<void> {
this.instance.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, () => {
this.instance.end(data, encoding, () => {
resolve();
});
});
}
public async setDefaultEncoding(encoding: string): Promise<void> {
this.stream.setDefaultEncoding(encoding);
this.instance.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) => {
this.instance.write(data, encoding, (error) => {
if (error) {
reject(error);
} else {
@@ -37,22 +43,8 @@ export class WritableProxy<T extends stream.Writable = stream.Writable> implemen
}
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.instance.end();
await super.dispose();
}
}
@@ -60,50 +52,58 @@ export class WritableProxy<T extends stream.Writable = stream.Writable> implemen
* 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>;
export interface IReadableProxy<T extends EventEmitter> extends ServerProxy<T> {
pipe<P extends WritableProxy>(destination: P, options?: { end?: boolean; }): 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) {}
export class ReadableProxy<T extends stream.Readable = stream.Readable> extends ServerProxy<T> implements IReadableProxy<T> {
public constructor(instance: T, bindEvents: string[] = []) {
super({
bindEvents: ["close", "end", "error"].concat(bindEvents),
doneEvents: ["close"],
delayedEvents: ["data"],
instance,
});
}
public async pipe<P extends WritableProxy>(destination: P, options?: { end?: boolean; }): Promise<void> {
this.instance.pipe(destination.instance, options);
// `pipe` switches the stream to flowing mode and makes data start emitting.
await this.bindDelayedEvent("data");
}
public async destroy(): Promise<void> {
this.stream.destroy();
this.instance.destroy();
}
public async setEncoding(encoding: string): Promise<void> {
this.stream.setEncoding(encoding);
this.instance.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));
this.instance.destroy();
await super.dispose();
}
}
export class DuplexProxy<T extends stream.Duplex = stream.Duplex> extends WritableProxy<T> implements IReadableProxy {
export class DuplexProxy<T extends stream.Duplex = stream.Duplex> extends WritableProxy<T> implements IReadableProxy<T> {
public constructor(stream: T, bindEvents: string[] = []) {
super(stream, ["end"].concat(bindEvents), ["data"]);
}
public async pipe<P extends WritableProxy>(destination: P, options?: { end?: boolean; }): Promise<void> {
this.instance.pipe(destination.instance, options);
// `pipe` switches the stream to flowing mode and makes data start emitting.
await this.bindDelayedEvent("data");
}
public async setEncoding(encoding: string): Promise<void> {
this.stream.setEncoding(encoding);
this.instance.setEncoding(encoding);
}
// tslint:disable-next-line no-any
public async onEvent(cb: (event: string, ...args: any[]) => void): Promise<void> {
await super.onEvent(cb);
this.stream.on("data", (chunk) => cb("data", chunk));
this.stream.on("end", () => cb("end"));
public async dispose(): Promise<void> {
this.instance.destroy();
await super.dispose();
}
}

View File

@@ -136,6 +136,7 @@ export class Server {
const args = proxyMessage.getArgsList().map((a) => protoToArgument(
a,
(id, args) => this.sendCallback(proxyId, id, args),
(id) => this.getProxy(id).instance,
));
logger.trace(() => [
@@ -241,9 +242,7 @@ export class Server {
this.proxies.set(proxyId, { instance });
if (isProxy(instance)) {
instance.onEvent((event, ...args) => this.sendEvent(proxyId, event, ...args)).catch((error) => {
logger.error(error.message);
});
instance.onEvent((event, ...args) => this.sendEvent(proxyId, event, ...args));
instance.onDone(() => {
// It might have finished because we disposed it due to a disconnect.
if (!this.disconnected) {
@@ -255,8 +254,6 @@ export class Server {
this.removeProxy(proxyId);
}, this.responseTimeout);
}
}).catch((error) => {
logger.error(error.message);
});
}