Skip to content

Commit

Permalink
Implement rest of RPC stuff and proper client w/ handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
filiptibell committed Nov 22, 2023
1 parent 23f0edc commit 0931380
Show file tree
Hide file tree
Showing 9 changed files with 285 additions and 121 deletions.
11 changes: 11 additions & 0 deletions editors/vscode/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions editors/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@
},
"dependencies": {
"anymatch": "^3.1.3",
"linebyline": "^1.3.0",
"semver": "^7.5.0",
"tree-kill": "^1.2.2"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,67 +3,16 @@ import * as cp from "child_process";
import * as os from "os";
import * as fs from "fs";

const kill = require("tree-kill");
const treekill = require("tree-kill");
const readline = require("linebyline");

import { SettingsProvider } from "../providers/settings";
import { RpcMessage, validateRpcMessage } from "./message";

let outputChannel: vscode.OutputChannel;

const KILL_SIGNALS = ["SIGHUP", "SIGINT", "SIGKILL", "SIGTERM"];

export type RpcMessageKind = "Request" | "Response" | "Notification";
export type RpcMessageData = {
id: number;
method: string;
value?: any;
};
export type RpcMessage = {
kind: RpcMessageKind;
data: RpcMessageData;
};

const validateRpcMessage = (
messageString: string
): { valid: true; message: RpcMessage } | { valid: false; err: string } => {
const message = JSON.parse(messageString);
if (typeof message !== "object") {
return {
valid: false,
err: `message must be an object, got ${typeof message}`,
};
}
if (typeof message.kind !== "string") {
return {
valid: false,
err: `message.kind must be a string, got ${typeof message.kind}`,
};
}
if (typeof message.data !== "object") {
return {
valid: false,
err: `message.data must be a object, got ${typeof message.data}`,
};
}
if (typeof message.data.id !== "number") {
return {
valid: false,
err: `message.data.id must be a number, got ${typeof message.data
.id}`,
};
}
if (typeof message.data.method !== "string") {
return {
valid: false,
err: `message.data.method must be a string, got ${typeof message
.data.method}`,
};
}
return {
valid: true,
message,
};
};

const fileExistsSync = (path: vscode.Uri): boolean => {
try {
return fs.existsSync(path.fsPath);
Expand Down Expand Up @@ -101,14 +50,18 @@ const findServerExecutable = (context: vscode.ExtensionContext): string => {
return command;
};

export const startServer = (
export const log = (message: string) => {
if (outputChannel === undefined) {
outputChannel = vscode.window.createOutputChannel("Roblox UI");
}
outputChannel.append(message);
};

export const start = (
context: vscode.ExtensionContext,
workspacePath: string,
settings: SettingsProvider,
callback: (
child: cp.ChildProcessWithoutNullStreams,
message: RpcMessage
) => void
callback: (message: RpcMessage) => void
): cp.ChildProcessWithoutNullStreams => {
if (outputChannel === undefined) {
outputChannel = vscode.window.createOutputChannel("Roblox UI");
Expand All @@ -134,19 +87,17 @@ export const startServer = (
shell: true,
});

let stdout = "";
childProcess.stdout.on("data", (data: Buffer) => {
stdout += data.toString("utf8");
if (stdout.endsWith("\n")) {
const result = validateRpcMessage(stdout);
stdout = "";
if (result.valid) {
callback(childProcess, result.message);
} else {
outputChannel.appendLine(
"Failed to parse rpc message:\n" + result.err
);
}
readline(childProcess.stdout, {
maxLineLength: 1024 * 256, // 256 KiB should be enough for any message
retainBuffer: false,
}).on("line", (stdout: string) => {
const result = validateRpcMessage(stdout);
if (result.valid) {
callback(result.message);
} else {
outputChannel.appendLine(
"Failed to parse rpc message:\n" + result.err
);
}
});

Expand All @@ -157,11 +108,11 @@ export const startServer = (
return childProcess;
};

export const stopServer = (
server: cp.ChildProcessWithoutNullStreams
export const kill = (
childProcess: cp.ChildProcessWithoutNullStreams
): Promise<void> => {
return new Promise((resolve, reject) => {
if (server.pid === undefined) {
if (childProcess.pid === undefined) {
reject("Failed to superkill process: no pid");
return;
}
Expand All @@ -176,7 +127,7 @@ export const stopServer = (
let killErrorLines = "";

for (const signal of KILL_SIGNALS) {
kill(server.pid, signal, (err: Error | undefined) => {
treekill(childProcess.pid, signal, (err: Error | undefined) => {
if (err) {
killErrors += 1;
killErrorLines += "- ";
Expand Down
37 changes: 37 additions & 0 deletions editors/vscode/src/server/dom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
type DomNotificationAdded = {
parentId?: string;
childId: string;
};

type DomNotificationRemoved = {
parentId?: string;
childId: string;
};

type DomNotificationChanged = {
id: string;
className?: string;
name?: string;
};

export type DomNotification =
| null
| { kind: "Added"; data: DomNotificationAdded }
| { kind: "Removed"; data: DomNotificationRemoved }
| { kind: "Changed"; data: DomNotificationChanged };

export type DomInstance = {
id: string;
className: string;
name: string;
children?: string[];
};

export type DomRootRequest = void;
export type DomRootResponse = null | DomInstance;

export type DomGetRequest = { id: string };
export type DomGetResponse = null | DomInstance;

export type DomChildrenRequest = { id: string };
export type DomChildrenResponse = DomInstance[];
110 changes: 110 additions & 0 deletions editors/vscode/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import * as vscode from "vscode";
import * as cp from "child_process";

import { SettingsProvider } from "../providers/settings";
import { kill, log, start } from "./child";
import { RpcMessage, createRpcRequest, respondToRpcMessage } from "./message";

export * from "./dom";
export * from "./message";

type RpcCallback = (response: RpcMessage) => void;
export type RpcHandler = (data?: any) => any | undefined;

export class RpcServer {
private child: cp.ChildProcessWithoutNullStreams;
private handlers: Map<string, RpcHandler> = new Map();
private resolvers: Map<number, RpcCallback> = new Map();
private idCounter: number = 0;

constructor(
private readonly context: vscode.ExtensionContext,
private readonly workspacePath: string,
private readonly settingsProvider: SettingsProvider
) {
this.child = start(
this.context,
this.workspacePath,
this.settingsProvider,
(message) => {
this.onMessage(message);
}
);
}

public async stop() {
await kill(this.child);
this.idCounter = 0;
}

public async restart() {
await kill(this.child);
this.idCounter = 0;
this.child = start(
this.context,
this.workspacePath,
this.settingsProvider,
(message) => {
this.onMessage(message);
}
);
}

public async sendRequest(
method: string,
requestValue?: any
): Promise<any | undefined> {
this.idCounter += 1;
const id = this.idCounter;
return new Promise((resolve) => {
this.resolvers.set(id, resolve);
const requestRpc =
requestValue !== undefined
? createRpcRequest(method, id, requestValue)
: createRpcRequest(method, id, null);
const requestJson = JSON.stringify(requestRpc);
this.child.stdin.write(requestJson);
this.child.stdin.write("\n");
});
}

public onRequest(method: string, handler: RpcHandler) {
this.handlers.set(method, handler);
}

private onMessage(message: RpcMessage) {
if (message.kind === "Request") {
let handler = this.handlers.get(message.data.method);
if (handler !== undefined) {
const responseValue = handler(message.data.value);
const responseRpc =
responseValue !== undefined
? respondToRpcMessage(message, responseValue)
: respondToRpcMessage(message, null);
const responseJson = JSON.stringify(responseRpc);
this.child.stdin.write(responseJson);
this.child.stdin.write("\n");
} else {
log(
"Missing handler for request!" +
`\nMethod: "${message.data.method}"` +
`\nId: ${message.data.id}` +
`\nValue: ${JSON.stringify(message.data.value)}\n`
);
}
} else if (message.kind === "Response") {
let resolver = this.resolvers.get(message.data.id);
if (resolver !== undefined) {
this.resolvers.delete(message.data.id);
resolver(message.data.value);
} else {
log(
"Missing resolver for request!" +
`\nMethod: "${message.data.method}"` +
`\nId: ${message.data.id}` +
`\nValue: ${JSON.stringify(message.data.value)}\n`
);
}
}
}
}
Loading

0 comments on commit 0931380

Please sign in to comment.