Make everything use active evals (#30)
* Add trace log level * Use active eval to implement spdlog * Split server/client active eval interfaces Since all properties are *not* valid on both sides * +200% fire resistance * Implement exec using active evaluations * Fully implement child process streams * Watch impl, move child_process back to explicitly adding events Automatically forwarding all events might be the right move, but wanna think/discuss it a bit more because it didn't come out very cleanly. * Would you like some args with that callback? * Implement the rest of child_process using active evals * Rampant memory leaks Emit "kill" to active evaluations when client disconnects in order to kill processes. Most likely won't be the final solution. * Resolve some minor issues with output panel * Implement node-pty with active evals * Provide clearTimeout to vm sandbox * Implement socket with active evals * Extract some callback logic Also remove some eval interfaces, need to re-think those. * Implement net.Server and remainder of net.Socket using active evals * Implement dispose for active evaluations * Use trace for express requests * Handle sending buffers through evaluation events * Make event logging a bit more clear * Fix some errors due to us not actually instantiating until connect/listen * is this a commit message? * We can just create the evaluator in the ctor Not sure what I was thinking. * memory leak for you, memory leak for everyone * it's a ternary now * Don't dispose automatically on close or error The code may or may not be disposable at that point. * Handle parsing buffers on the client side as well * Remove unused protobuf * Remove TypedValue * Remove unused forkProvider and test * Improve dispose pattern for active evals * Socket calls close after error; no need to bind both * Improve comment * Comment is no longer wishy washy due to explicit boolean * Simplify check for sendHandle and options * Replace _require with __non_webpack_require__ Webpack will then replace this with `require` which we then provide to the vm sandbox. * Provide path.parse * Prevent original-fs from loading * Start with a pid of -1 vscode immediately checks the PID to see if the debug process launch correctly, but of course we don't get the pid synchronously. * Pass arguments to bootstrap-fork * Fully implement streams Was causing errors because internally the stream would set this.writing to true and it would never become false, so subsequent messages would never send. * Fix serializing errors and streams emitting errors multiple times * Was emitting close to data * Fix missing path for spawned processes * Move evaluation onDispose call Now it's accurate and runs when the active evaluation has actually disposed. * Fix promisifying fs.exists * Fix some active eval callback issues * Patch existsSync in debug adapter
This commit is contained in:
@ -1,268 +0,0 @@
|
||||
import * as cp from "child_process";
|
||||
import * as net from "net";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import { TextEncoder, TextDecoder } from "text-encoding";
|
||||
import { createClient } from "./helpers";
|
||||
import { ChildProcess } from "../src/browser/command";
|
||||
|
||||
(global as any).TextDecoder = TextDecoder; // tslint:disable-line no-any
|
||||
(global as any).TextEncoder = TextEncoder; // tslint:disable-line no-any
|
||||
|
||||
describe("spawn", () => {
|
||||
const client = createClient({
|
||||
dataDirectory: "",
|
||||
workingDirectory: "",
|
||||
builtInExtensionsDirectory: "",
|
||||
forkProvider: (msg): cp.ChildProcess => {
|
||||
return cp.spawn(msg.getCommand(), msg.getArgsList(), {
|
||||
stdio: [null, null, null, "ipc"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a function that when called returns a promise that resolves with
|
||||
* the next chunk of data from the process.
|
||||
*/
|
||||
const promisifyData = (proc: ChildProcess): (() => 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.stdout.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 execute command and return output", (done) => {
|
||||
const proc = client.spawn("echo", ["test"]);
|
||||
proc.stdout.on("data", (data) => {
|
||||
expect(data).toEqual("test\n");
|
||||
});
|
||||
proc.on("exit", (): void => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
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 = client.spawn("/bin/bash", ["--rcfile", "/tmp/test/nope/should/not/exist"], {
|
||||
tty: {
|
||||
columns: 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 cat", (done) => {
|
||||
const proc = client.spawn("cat", []);
|
||||
expect(proc.pid).toBeUndefined();
|
||||
proc.stdout.on("data", (data) => {
|
||||
expect(data).toEqual("banana");
|
||||
expect(proc.pid).toBeDefined();
|
||||
proc.kill();
|
||||
});
|
||||
proc.on("exit", () => done());
|
||||
proc.send("banana");
|
||||
proc.stdin.end();
|
||||
});
|
||||
|
||||
it("should print env variable", (done) => {
|
||||
const proc = client.spawn("env", [], {
|
||||
env: { hi: "donkey" },
|
||||
});
|
||||
proc.stdout.on("data", (data) => {
|
||||
expect(data).toEqual("hi=donkey\n");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
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 = client.spawn("/bin/bash", ["--rcfile", "/tmp/test/nope/should/not/exist"], {
|
||||
tty: {
|
||||
columns: 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.send("tput lines\n");
|
||||
expect(await getData()).toContain("tput");
|
||||
|
||||
expect((await getData()).trim()).toContain("10");
|
||||
proc.resize!({
|
||||
columns: 10,
|
||||
rows: 50,
|
||||
});
|
||||
|
||||
// The prompt again.
|
||||
await getData();
|
||||
await getData();
|
||||
|
||||
proc.send("tput lines\n");
|
||||
expect(await getData()).toContain("tput");
|
||||
|
||||
expect((await getData())).toContain("50");
|
||||
|
||||
proc.kill();
|
||||
expect(proc.killed).toBeTruthy();
|
||||
await new Promise((resolve): void => {
|
||||
proc.on("exit", resolve);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fork and echo messages", (done) => {
|
||||
const proc = client.fork(path.join(__dirname, "forker.js"));
|
||||
proc.on("message", (msg) => {
|
||||
expect(msg.bananas).toBeTruthy();
|
||||
proc.kill();
|
||||
});
|
||||
proc.send({ bananas: true }, undefined, true);
|
||||
proc.on("exit", () => done());
|
||||
});
|
||||
});
|
||||
|
||||
describe("createConnection", () => {
|
||||
const client = createClient();
|
||||
const tmpPath = path.join(os.tmpdir(), Math.random().toString());
|
||||
let server: net.Server;
|
||||
beforeAll(async () => {
|
||||
await new Promise((r): void => {
|
||||
server = net.createServer().listen(tmpPath, () => {
|
||||
r();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
it("should connect to socket", async () => {
|
||||
await new Promise((resolve): void => {
|
||||
const socket = client.createConnection(tmpPath, () => {
|
||||
socket.end();
|
||||
socket.addListener("close", () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise((resolve): void => {
|
||||
const socket = new client.Socket();
|
||||
socket.connect(tmpPath, () => {
|
||||
socket.end();
|
||||
socket.addListener("close", () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should get data from server", (done) => {
|
||||
server.once("connection", (socket: net.Socket) => {
|
||||
socket.write("hi how r u");
|
||||
});
|
||||
|
||||
const socket = client.createConnection(tmpPath);
|
||||
|
||||
socket.addListener("data", (data) => {
|
||||
expect(data.toString()).toEqual("hi how r u");
|
||||
socket.end();
|
||||
socket.addListener("close", () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should send data to server", (done) => {
|
||||
const clientSocket = client.createConnection(tmpPath);
|
||||
clientSocket.write(Buffer.from("bananas"));
|
||||
server.once("connection", (socket: net.Socket) => {
|
||||
socket.addListener("data", (data) => {
|
||||
expect(data.toString()).toEqual("bananas");
|
||||
socket.end();
|
||||
clientSocket.addListener("end", () => {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("createServer", () => {
|
||||
const client = createClient();
|
||||
const tmpPath = path.join(os.tmpdir(), Math.random().toString());
|
||||
|
||||
it("should connect to server", (done) => {
|
||||
const s = client.createServer(() => {
|
||||
s.close();
|
||||
});
|
||||
s.on("close", () => {
|
||||
done();
|
||||
});
|
||||
s.listen(tmpPath);
|
||||
});
|
||||
|
||||
it("should connect to server and get socket connection", (done) => {
|
||||
const s = client.createServer();
|
||||
s.listen(tmpPath, () => {
|
||||
net.createConnection(tmpPath, () => {
|
||||
checks++;
|
||||
s.close();
|
||||
});
|
||||
});
|
||||
let checks = 0;
|
||||
s.on("connection", (con) => {
|
||||
expect(checks).toEqual(1);
|
||||
con.end();
|
||||
checks++;
|
||||
});
|
||||
s.on("close", () => {
|
||||
expect(checks).toEqual(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
@ -48,7 +48,7 @@ describe("Evaluate", () => {
|
||||
|
||||
it("should resolve with promise", async () => {
|
||||
const value = await client.evaluate(async () => {
|
||||
await new Promise((r) => setTimeout(r, 100));
|
||||
await new Promise((r): number => setTimeout(r, 100));
|
||||
|
||||
return "donkey";
|
||||
});
|
||||
@ -64,6 +64,11 @@ describe("Evaluate", () => {
|
||||
ae.emit("close");
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
onDidDispose: (): void => undefined,
|
||||
dispose: (): void => undefined,
|
||||
};
|
||||
});
|
||||
runner.emit("1");
|
||||
runner.on("2", () => runner.emit("3"));
|
||||
|
Reference in New Issue
Block a user