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:
@ -8,7 +8,6 @@
|
||||
"dependencies": {
|
||||
"iconv-lite": "^0.4.24",
|
||||
"onigasm": "^2.2.1",
|
||||
"spdlog": "^0.7.2",
|
||||
"string-replace-loader": "^2.1.1",
|
||||
"tar-stream": "^2.0.1"
|
||||
},
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as util from "util";
|
||||
import { Emitter, Event } from "@coder/events";
|
||||
import { client as ideClient } from "@coder/ide/src/fill/client";
|
||||
import { $, addClass, append } from "vs/base/browser/dom";
|
||||
import { HighlightedLabel } from "vs/base/browser/ui/highlightedlabel/highlightedLabel";
|
||||
import { ObjectTree } from "vs/base/browser/ui/tree/objectTree";
|
||||
@ -16,8 +16,6 @@ import { IThemeService } from "vs/platform/theme/common/themeService";
|
||||
import { workbench } from "./workbench";
|
||||
import "./dialog.scss";
|
||||
|
||||
declare var __non_webpack_require__: typeof require;
|
||||
|
||||
export enum DialogType {
|
||||
NewFolder,
|
||||
Save,
|
||||
@ -183,15 +181,15 @@ class Dialog {
|
||||
this.filesNode = document.createElement("div");
|
||||
this.filesNode.className = "files-list";
|
||||
this.entryList = new ObjectTree<DialogEntry, string>(this.filesNode, {
|
||||
getHeight: (entry: DialogEntry): number => {
|
||||
getHeight: (_entry: DialogEntry): number => {
|
||||
return 20;
|
||||
},
|
||||
getTemplateId: (entry: DialogEntry): string => {
|
||||
getTemplateId: (_entry: DialogEntry): string => {
|
||||
return "dialog-entry";
|
||||
},
|
||||
}, [new DialogEntryRenderer()], {
|
||||
openController: {
|
||||
shouldOpen: (event): boolean => {
|
||||
shouldOpen: (_event): boolean => {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
@ -341,7 +339,6 @@ class Dialog {
|
||||
}
|
||||
|
||||
private set path(directory: string) {
|
||||
const ts = Date.now();
|
||||
this.list(directory).then((value) => {
|
||||
this._path = directory;
|
||||
this.buildPath();
|
||||
@ -380,32 +377,16 @@ class Dialog {
|
||||
}
|
||||
|
||||
private async list(directory: string): Promise<ReadonlyArray<DialogEntry>> {
|
||||
return ideClient.evaluate((_helper, directory) => {
|
||||
const fs = __non_webpack_require__("fs") as typeof import("fs");
|
||||
const util = __non_webpack_require__("util") as typeof import("util");
|
||||
const path = __non_webpack_require__("path") as typeof import("path");
|
||||
const paths = (await util.promisify(fs.readdir)(directory)).sort();
|
||||
const stats = await Promise.all(paths.map(p => util.promisify(fs.stat)(path.join(directory, p))));
|
||||
|
||||
return util.promisify(fs.readdir)(directory).then((paths) => {
|
||||
paths = paths.sort();
|
||||
|
||||
return Promise.all(paths.map(p => util.promisify(fs.stat)(path.join(directory, p)))).then((stats) => {
|
||||
return {
|
||||
paths,
|
||||
stats,
|
||||
};
|
||||
});
|
||||
}).then(({ paths, stats }) => {
|
||||
return stats.map((stat, index): DialogEntry => {
|
||||
return {
|
||||
fullPath: path.join(directory, paths[index]),
|
||||
name: paths[index],
|
||||
isDirectory: stat.isDirectory(),
|
||||
lastModified: stat.mtime.toDateString(),
|
||||
size: stat.size,
|
||||
};
|
||||
});
|
||||
});
|
||||
}, directory);
|
||||
return stats.map((stat, index): DialogEntry => ({
|
||||
fullPath: path.join(directory, paths[index]),
|
||||
name: paths[index],
|
||||
isDirectory: stat.isDirectory(),
|
||||
lastModified: stat.mtime.toDateString(),
|
||||
size: stat.size,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@ -441,7 +422,7 @@ class DialogEntryRenderer implements ITreeRenderer<DialogEntry, string, DialogEn
|
||||
};
|
||||
}
|
||||
|
||||
public renderElement(node: ITreeNode<DialogEntry, string>, index: number, templateData: DialogEntryData): void {
|
||||
public renderElement(node: ITreeNode<DialogEntry, string>, _index: number, templateData: DialogEntryData): void {
|
||||
templateData.icon.className = "dialog-entry-icon monaco-icon-label";
|
||||
const classes = getIconClasses(
|
||||
workbench.serviceCollection.get<IModelService>(IModelService) as IModelService,
|
||||
@ -465,7 +446,7 @@ class DialogEntryRenderer implements ITreeRenderer<DialogEntry, string, DialogEn
|
||||
templateData.lastModified.innerText = node.element.lastModified;
|
||||
}
|
||||
|
||||
public disposeTemplate(templateData: DialogEntryData): void {
|
||||
public disposeTemplate(_templateData: DialogEntryData): void {
|
||||
// throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
|
@ -1,94 +1,4 @@
|
||||
import { Module } from "@coder/protocol";
|
||||
import { client } from "@coder/ide/src/fill/client";
|
||||
import { EventEmitter } from "events";
|
||||
import * as nodePty from "node-pty";
|
||||
import { ActiveEvalHelper } from "@coder/protocol";
|
||||
import { logger } from "@coder/logger";
|
||||
|
||||
/**
|
||||
* Implementation of nodePty for the browser.
|
||||
*/
|
||||
class Pty implements nodePty.IPty {
|
||||
private readonly emitter = new EventEmitter();
|
||||
private readonly ae: ActiveEvalHelper;
|
||||
private _pid = -1;
|
||||
private _process = "";
|
||||
|
||||
public constructor(file: string, args: string[] | string, options: nodePty.IPtyForkOptions) {
|
||||
this.ae = client.run((ae, file, args, options) => {
|
||||
ae.preserveEnv(options);
|
||||
|
||||
const ptyProc = ae.modules.pty.spawn(file, args, options);
|
||||
|
||||
let process = ptyProc.process;
|
||||
ae.emit("process", process);
|
||||
ae.emit("pid", ptyProc.pid);
|
||||
|
||||
const timer = setInterval(() => {
|
||||
if (ptyProc.process !== process) {
|
||||
process = ptyProc.process;
|
||||
ae.emit("process", process);
|
||||
}
|
||||
}, 200);
|
||||
|
||||
ptyProc.on("exit", (code, signal) => {
|
||||
clearTimeout(timer);
|
||||
ae.emit("exit", code, signal);
|
||||
});
|
||||
|
||||
ptyProc.on("data", (data) => ae.emit("data", data));
|
||||
|
||||
ae.on("resize", (cols: number, rows: number) => ptyProc.resize(cols, rows));
|
||||
ae.on("write", (data: string) => ptyProc.write(data));
|
||||
ae.on("kill", (signal: string) => ptyProc.kill(signal));
|
||||
|
||||
return {
|
||||
onDidDispose: (cb): void => ptyProc.on("exit", cb),
|
||||
dispose: (): void => {
|
||||
ptyProc.kill();
|
||||
setTimeout(() => ptyProc.kill("SIGKILL"), 5000); // Double tap.
|
||||
},
|
||||
};
|
||||
}, file, args, options);
|
||||
|
||||
this.ae.on("error", (error) => logger.error(error.message));
|
||||
|
||||
this.ae.on("pid", (pid) => this._pid = pid);
|
||||
this.ae.on("process", (process) => this._process = process);
|
||||
|
||||
this.ae.on("exit", (code, signal) => this.emitter.emit("exit", code, signal));
|
||||
this.ae.on("data", (data) => this.emitter.emit("data", data));
|
||||
}
|
||||
|
||||
public get pid(): number {
|
||||
return this._pid;
|
||||
}
|
||||
|
||||
public get process(): string {
|
||||
return this._process;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line no-any
|
||||
public on(event: string, listener: (...args: any[]) => void): void {
|
||||
this.emitter.on(event, listener);
|
||||
}
|
||||
|
||||
public resize(columns: number, rows: number): void {
|
||||
this.ae.emit("resize", columns, rows);
|
||||
}
|
||||
|
||||
public write(data: string): void {
|
||||
this.ae.emit("write", data);
|
||||
}
|
||||
|
||||
public kill(signal?: string): void {
|
||||
this.ae.emit("kill", signal);
|
||||
}
|
||||
}
|
||||
|
||||
const ptyType: typeof nodePty = {
|
||||
spawn: (file: string, args: string[] | string, options: nodePty.IPtyForkOptions): nodePty.IPty => {
|
||||
return new Pty(file, args, options);
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = ptyType;
|
||||
export = client.modules[Module.NodePty];
|
||||
|
@ -1,63 +1,4 @@
|
||||
import { RotatingLogger as NodeRotatingLogger } from "spdlog";
|
||||
import { logger } from "@coder/logger";
|
||||
import { Module } from "@coder/protocol";
|
||||
import { client } from "@coder/ide/src/fill/client";
|
||||
|
||||
const ae = client.run((ae) => {
|
||||
const loggers = new Map<number, NodeRotatingLogger>();
|
||||
|
||||
ae.on("new", (id: number, name: string, filePath: string, fileSize: number, fileCount: number) => {
|
||||
const logger = new ae.modules.spdlog.RotatingLogger(name, filePath, fileSize, fileCount);
|
||||
loggers.set(id, logger);
|
||||
});
|
||||
|
||||
ae.on("clearFormatters", (id: number) => loggers.get(id)!.clearFormatters());
|
||||
ae.on("critical", (id: number, message: string) => loggers.get(id)!.critical(message));
|
||||
ae.on("debug", (id: number, message: string) => loggers.get(id)!.debug(message));
|
||||
ae.on("drop", (id: number) => loggers.get(id)!.drop());
|
||||
ae.on("errorLog", (id: number, message: string) => loggers.get(id)!.error(message));
|
||||
ae.on("flush", (id: number) => loggers.get(id)!.flush());
|
||||
ae.on("info", (id: number, message: string) => loggers.get(id)!.info(message));
|
||||
ae.on("setAsyncMode", (bufferSize: number, flushInterval: number) => ae.modules.spdlog.setAsyncMode(bufferSize, flushInterval));
|
||||
ae.on("setLevel", (id: number, level: number) => loggers.get(id)!.setLevel(level));
|
||||
ae.on("trace", (id: number, message: string) => loggers.get(id)!.trace(message));
|
||||
ae.on("warn", (id: number, message: string) => loggers.get(id)!.warn(message));
|
||||
|
||||
const disposeCallbacks = <Array<() => void>>[];
|
||||
|
||||
return {
|
||||
onDidDispose: (cb): number => disposeCallbacks.push(cb),
|
||||
dispose: (): void => {
|
||||
loggers.forEach((logger) => logger.flush());
|
||||
loggers.clear();
|
||||
disposeCallbacks.forEach((cb) => cb());
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const spdLogger = logger.named("spdlog");
|
||||
ae.on("close", () => spdLogger.error("session closed prematurely"));
|
||||
ae.on("error", (error: Error) => spdLogger.error(error.message));
|
||||
|
||||
let id = 0;
|
||||
export class RotatingLogger implements NodeRotatingLogger {
|
||||
private readonly id = id++;
|
||||
|
||||
public constructor(name: string, filePath: string, fileSize: number, fileCount: number) {
|
||||
ae.emit("new", this.id, name, filePath, fileSize, fileCount);
|
||||
}
|
||||
|
||||
public trace(message: string): void { ae.emit("trace", this.id, message); }
|
||||
public debug(message: string): void { ae.emit("debug", this.id, message); }
|
||||
public info(message: string): void { ae.emit("info", this.id, message); }
|
||||
public warn(message: string): void { ae.emit("warn", this.id, message); }
|
||||
public error(message: string): void { ae.emit("errorLog", this.id, message); }
|
||||
public critical(message: string): void { ae.emit("critical", this.id, message); }
|
||||
public setLevel(level: number): void { ae.emit("setLevel", this.id, level); }
|
||||
public clearFormatters(): void { ae.emit("clearFormatters", this.id); }
|
||||
public flush(): void { ae.emit("flush", this.id); }
|
||||
public drop(): void { ae.emit("drop", this.id); }
|
||||
}
|
||||
|
||||
export const setAsyncMode = (bufferSize: number, flushInterval: number): void => {
|
||||
ae.emit("setAsyncMode", bufferSize, flushInterval);
|
||||
};
|
||||
export = client.modules[Module.Spdlog];
|
||||
|
@ -202,6 +202,8 @@ export class Workbench {
|
||||
/**
|
||||
* Resolves the error of the workspace identifier being invalid.
|
||||
*/
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(ex);
|
||||
this.workspace = undefined;
|
||||
location.reload();
|
||||
|
||||
|
@ -1,99 +0,0 @@
|
||||
import { IPty } from "node-pty";
|
||||
import { createClient } from "@coder/protocol/test";
|
||||
|
||||
const client = createClient();
|
||||
jest.mock("../../ide/src/fill/client", () => ({ client }));
|
||||
const pty = require("../src/fill/node-pty") as typeof import("node-pty");
|
||||
|
||||
describe("node-pty", () => {
|
||||
/**
|
||||
* Returns a function that when called returns a promise that resolves with
|
||||
* the next chunk of data from the process.
|
||||
*/
|
||||
const promisifyData = (proc: IPty): (() => Promise<string>) => {
|
||||
// Use a persistent callback instead of creating it in the promise since
|
||||
// otherwise we could lose data that comes in while no promise is listening.
|
||||
let onData: (() => void) | undefined;
|
||||
let buffer: string | undefined;
|
||||
proc.on("data", (data) => {
|
||||
// Remove everything that isn't a letter, number, or $ to avoid issues
|
||||
// with ANSI escape codes printing inside the test output.
|
||||
buffer = (buffer || "") + data.toString().replace(/[^a-zA-Z0-9$]/g, "");
|
||||
if (onData) {
|
||||
onData();
|
||||
}
|
||||
});
|
||||
|
||||
return (): Promise<string> => new Promise((resolve): void => {
|
||||
onData = (): void => {
|
||||
if (typeof buffer !== "undefined") {
|
||||
const data = buffer;
|
||||
buffer = undefined;
|
||||
onData = undefined;
|
||||
resolve(data);
|
||||
}
|
||||
};
|
||||
onData();
|
||||
});
|
||||
};
|
||||
|
||||
it("should create shell", async () => {
|
||||
// Setting the config file to something that shouldn't exist so the test
|
||||
// isn't affected by custom configuration.
|
||||
const proc = pty.spawn("/bin/bash", ["--rcfile", "/tmp/test/nope/should/not/exist"], {
|
||||
cols: 100,
|
||||
rows: 10,
|
||||
});
|
||||
|
||||
const getData = promisifyData(proc);
|
||||
|
||||
// First it outputs @hostname:cwd
|
||||
expect((await getData()).length).toBeGreaterThan(1);
|
||||
|
||||
// Then it seems to overwrite that with a shorter prompt in the format of
|
||||
// [hostname@user]$
|
||||
expect((await getData())).toContain("$");
|
||||
|
||||
proc.kill();
|
||||
|
||||
await new Promise((resolve): void => {
|
||||
proc.on("exit", resolve);
|
||||
});
|
||||
});
|
||||
|
||||
it("should resize", async () => {
|
||||
// Requires the `tput lines` cmd to be available.
|
||||
// Setting the config file to something that shouldn't exist so the test
|
||||
// isn't affected by custom configuration.
|
||||
const proc = pty.spawn("/bin/bash", ["--rcfile", "/tmp/test/nope/should/not/exist"], {
|
||||
cols: 10,
|
||||
rows: 10,
|
||||
});
|
||||
|
||||
const getData = promisifyData(proc);
|
||||
|
||||
// We've already tested these first two bits of output; see shell test.
|
||||
await getData();
|
||||
await getData();
|
||||
|
||||
proc.write("tput lines\n");
|
||||
expect(await getData()).toContain("tput");
|
||||
|
||||
expect((await getData()).trim()).toContain("10");
|
||||
proc.resize(10, 50);
|
||||
|
||||
// The prompt again.
|
||||
await getData();
|
||||
await getData();
|
||||
|
||||
proc.write("tput lines\n");
|
||||
expect(await getData()).toContain("tput");
|
||||
|
||||
expect((await getData())).toContain("50");
|
||||
|
||||
proc.kill();
|
||||
await new Promise((resolve): void => {
|
||||
proc.on("exit", resolve);
|
||||
});
|
||||
});
|
||||
});
|
@ -34,11 +34,6 @@ big.js@^5.2.2:
|
||||
resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328"
|
||||
integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==
|
||||
|
||||
bindings@^1.3.0:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.1.tgz#21fc7c6d67c18516ec5aaa2815b145ff77b26ea5"
|
||||
integrity sha512-i47mqjF9UbjxJhxGf+pZ6kSxrnI3wBLlnGI2ArWJ4r0VrvDS7ZYXkprq/pLaBWYq4GM0r4zdHY+NNRqEMU7uew==
|
||||
|
||||
bl@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/bl/-/bl-3.0.0.tgz#3611ec00579fd18561754360b21e9f784500ff88"
|
||||
@ -114,24 +109,12 @@ lru-cache@^4.1.1:
|
||||
pseudomap "^1.0.2"
|
||||
yallist "^2.1.2"
|
||||
|
||||
minimist@0.0.8:
|
||||
version "0.0.8"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
|
||||
integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=
|
||||
|
||||
minimist@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
|
||||
integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
|
||||
|
||||
mkdirp@^0.5.1:
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
|
||||
integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
|
||||
dependencies:
|
||||
minimist "0.0.8"
|
||||
|
||||
nan@^2.10.0, nan@^2.8.0:
|
||||
nan@^2.10.0:
|
||||
version "2.12.1"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
|
||||
integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==
|
||||
@ -194,15 +177,6 @@ schema-utils@^0.4.5:
|
||||
ajv "^6.1.0"
|
||||
ajv-keywords "^3.1.0"
|
||||
|
||||
spdlog@^0.7.2:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/spdlog/-/spdlog-0.7.2.tgz#9298753d7694b9ee9bbfd7e01ea1e4c6ace1e64d"
|
||||
integrity sha512-rHfWCaWMD4NindDnql6rc6kn7Bs8JR92jhiUpCl3D6v+jYcQ6GozMLig0RliOOR8st5mU+IHLZnr15fBys5x/Q==
|
||||
dependencies:
|
||||
bindings "^1.3.0"
|
||||
mkdirp "^0.5.1"
|
||||
nan "^2.8.0"
|
||||
|
||||
string-replace-loader@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/string-replace-loader/-/string-replace-loader-2.1.1.tgz#b72e7b57b6ef04efe615aff0ad989b5c14ca63d1"
|
||||
|
Reference in New Issue
Block a user