Skip to content

Commit aabb3db

Browse files
committed
feat(vscode): communicate with worker using ipc instead of websocket
1 parent 4871b94 commit aabb3db

File tree

9 files changed

+217
-217
lines changed

9 files changed

+217
-217
lines changed

packages/vscode/package.json

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,12 @@
6868
"@types/mocha": "^10.0.10",
6969
"@types/node": "^22.16.5",
7070
"@types/vscode": "1.97.0",
71-
"@types/ws": "^8.18.1",
7271
"@vscode/test-cli": "^0.0.12",
7372
"@vscode/test-electron": "^2.5.2",
7473
"@vscode/vsce": "3.6.2",
75-
"get-port": "^7.1.0",
7674
"glob": "^7.2.3",
7775
"mocha": "^11.7.4",
7876
"ovsx": "^0.10.6",
79-
"typescript": "^5.9.3",
80-
"ws": "^8.18.3"
77+
"typescript": "^5.9.3"
8178
}
8279
}

packages/vscode/src/logger.ts

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,21 @@
1-
import { formatWithOptions } from 'node:util';
21
import vscode from 'vscode';
2+
import { BaseLogger, type LogLevel } from './shared/logger';
33

4-
function formatValues(values: unknown[]): string {
5-
return formatWithOptions({ depth: 4 }, ...values);
6-
}
7-
8-
export class Logger implements vscode.Disposable {
4+
export class MasterLogger extends BaseLogger implements vscode.Disposable {
95
readonly #channel: vscode.LogOutputChannel;
106

117
constructor(private readonly name = 'Rstest') {
8+
super();
129
this.#channel = vscode.window.createOutputChannel(this.name, { log: true });
1310
}
1411

15-
public trace(...values: unknown[]) {
16-
this.#channel.trace(formatValues(values));
17-
}
18-
19-
public debug(...values: unknown[]) {
20-
this.#channel.debug(formatValues(values));
21-
}
22-
23-
public info(...values: unknown[]) {
24-
this.#channel.info(formatValues(values));
25-
}
26-
27-
public warn(...values: unknown[]) {
28-
this.#channel.warn(formatValues(values));
29-
}
30-
31-
public error(...values: unknown[]) {
32-
this.#channel.error(formatValues(values));
12+
override log(level: LogLevel, message: string) {
13+
this.#channel[level](message);
3314
}
3415

3516
public dispose() {
3617
this.#channel.dispose();
3718
}
3819
}
3920

40-
export const logger = new Logger();
21+
export const logger = new MasterLogger();

packages/vscode/src/master.ts

Lines changed: 46 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,15 @@
11
import { spawn } from 'node:child_process';
2-
import { createServer } from 'node:http';
32
import path, { dirname } from 'node:path';
4-
import getPort from 'get-port';
53
import vscode from 'vscode';
6-
import type { WebSocket } from 'ws';
7-
import { WebSocketServer } from 'ws';
84
import { getConfigValue } from './config';
95
import { logger } from './logger';
10-
import type {
11-
WorkerEvent,
12-
WorkerEventFinish,
13-
WorkerRunTestData,
14-
} from './types';
6+
import type { LogLevel } from './shared/logger';
7+
import { expose, wrap } from './shared/rpc';
8+
import type { WorkerRunTestData } from './types';
9+
import type { Worker } from './worker';
1510

1611
export class RstestApi {
17-
public ws: WebSocket | null = null;
18-
private testPromises: Map<
19-
string,
20-
{ resolve: (value: any) => void; reject: (reason?: any) => void }
21-
> = new Map();
12+
public worker: Pick<Worker, 'initRstest' | 'runTest'> | null = null;
2213
private versionMismatchWarned = false;
2314

2415
public resolveRstestPath(): { cwd: string; rstestPath: string }[] {
@@ -116,81 +107,69 @@ export class RstestApi {
116107
}
117108

118109
public async runTest(item: vscode.TestItem) {
119-
if (this.ws) {
110+
if (this.worker) {
120111
const data: WorkerRunTestData = {
121-
type: 'runTest',
122112
id: item.id,
123113
fileFilters: [item.uri!.fsPath],
124114
testNamePattern: item.label,
125115
};
126116

127-
// Create a promise that will be resolved when we get a response with the matching ID
128-
const promise = new Promise<any>((resolve, reject) => {
129-
this.testPromises.set(item.id, { resolve, reject });
130-
131-
// Set a timeout to prevent hanging indefinitely
132-
setTimeout(() => {
133-
const promiseObj = this.testPromises.get(item.id);
134-
if (promiseObj) {
135-
this.testPromises.delete(item.id);
136-
reject(new Error(`Test execution timed out for ${item.label}`));
137-
}
138-
}, 10000); // 10 seconds timeout
139-
});
140-
141-
this.ws.send(JSON.stringify(data));
142-
return promise;
117+
return Promise.race([
118+
this.worker.runTest(data),
119+
new Promise((_, reject) =>
120+
setTimeout(
121+
reject,
122+
10_000,
123+
new Error(`Test execution timed out for ${item.label}`),
124+
),
125+
), // 10 seconds timeout
126+
]);
143127
}
144128
}
145129

146130
public async runFileTests(fileItem: vscode.TestItem) {
147-
if (this.ws) {
131+
if (this.worker) {
148132
const fileId = `file_${fileItem.id}`;
149133
const data: WorkerRunTestData = {
150-
type: 'runTest',
151134
id: fileId,
152135
fileFilters: [fileItem.uri!.fsPath],
153136
testNamePattern: '', // Empty pattern to run all tests in the file
154137
};
155138

156-
// Create a promise that will be resolved when we get a response with the matching ID
157-
const promise = new Promise<WorkerEventFinish>((resolve, reject) => {
158-
this.testPromises.set(fileId, { resolve, reject });
159-
160-
// Set a timeout to prevent hanging indefinitely
161-
setTimeout(() => {
162-
const promiseObj = this.testPromises.get(fileId);
163-
if (promiseObj) {
164-
this.testPromises.delete(fileId);
165-
reject(
166-
new Error(
167-
`File test execution timed out for ${fileItem.uri!.fsPath}`,
168-
),
169-
);
170-
}
171-
}, 30000); // 30 seconds timeout for file-level tests
172-
});
173-
174-
this.ws.send(JSON.stringify(data));
175-
return promise;
139+
return Promise.race([
140+
this.worker.runTest(data),
141+
new Promise((_, reject) =>
142+
setTimeout(
143+
reject,
144+
30_000,
145+
new Error(
146+
`File test execution timed out for ${fileItem.uri!.fsPath}`,
147+
),
148+
),
149+
), // 30 seconds timeout for file-level tests
150+
]);
176151
}
177152
}
178153

179154
public async createChildProcess() {
155+
const { cwd, rstestPath } = this.resolveRstestPath()[0];
156+
if (!cwd || !rstestPath) {
157+
logger.error('Failed to resolve rstest path or cwd');
158+
return;
159+
}
160+
180161
const execArgv: string[] = [];
181162
const workerPath = path.resolve(__dirname, 'worker.js');
182-
const port = await getPort();
183-
const wsAddress = `ws://localhost:${port}`;
184163
logger.debug('Spawning worker process', {
185164
workerPath,
186-
wsAddress,
187165
});
188166
const rstestProcess = spawn('node', [...execArgv, workerPath], {
189-
stdio: 'pipe',
167+
cwd,
168+
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
169+
serialization: 'advanced',
190170
env: {
191171
...process.env,
192172
TEST: 'true',
193-
RSTEST_WS_ADDRESS: wsAddress,
194173
},
195174
});
196175

@@ -204,52 +183,20 @@ export class RstestApi {
204183
logger.error('worker stderr', content.trimEnd());
205184
});
206185

207-
const server = createServer().listen(port).unref();
208-
const wss = new WebSocketServer({ server });
186+
this.worker = wrap<Worker>(rstestProcess);
187+
expose(rstestProcess, this);
209188

210-
wss.once('connection', (ws) => {
211-
this.ws = ws;
212-
logger.debug('Worker connected', { wsAddress });
213-
const { cwd, rstestPath } = this.resolveRstestPath()[0];
214-
if (!cwd || !rstestPath) {
215-
logger.error('Failed to resolve rstest path or cwd');
216-
return;
217-
}
218-
219-
ws.send(
220-
JSON.stringify({
221-
type: 'init',
222-
rstestPath,
223-
cwd,
224-
}),
225-
);
226-
logger.debug('Sent init payload to worker', { cwd, rstestPath });
227-
228-
ws.on('message', (_data) => {
229-
const _message = JSON.parse(_data.toString()) as WorkerEvent;
230-
if (_message.type === 'finish') {
231-
const message: WorkerEventFinish = _message;
232-
logger.debug('Received worker completion event', {
233-
id: message.id,
234-
testResult: message.testResults,
235-
testFileResult: message.testFileResults,
236-
});
237-
// Check if we have a pending promise for this test ID
238-
const promiseObj = this.testPromises.get(message.id);
239-
if (promiseObj) {
240-
// Resolve the promise with the message data
241-
promiseObj.resolve(message);
242-
// Remove the promise from the map
243-
this.testPromises.delete(message.id);
244-
}
245-
}
246-
});
247-
});
189+
this.worker.initRstest({ cwd, rstestPath });
190+
logger.debug('Sent init payload to worker', { cwd, rstestPath });
248191

249192
rstestProcess.on('exit', (code, signal) => {
250193
logger.debug('Worker process exited', { code, signal });
251194
});
252195
}
253196

254197
public async createRstestWorker() {}
198+
199+
async log(level: LogLevel, message: string) {
200+
logger[level](message);
201+
}
255202
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { formatWithOptions } from 'node:util';
2+
3+
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
4+
5+
export type Logger = {
6+
[K in LogLevel]: (...params: unknown[]) => void;
7+
};
8+
9+
export abstract class BaseLogger implements Logger {
10+
constructor(private prefix?: string) {}
11+
protected log(level: LogLevel, message: string): void {
12+
console[level](message);
13+
}
14+
private logWithFormat(level: LogLevel, params: unknown[]) {
15+
this.log(
16+
level,
17+
formatWithOptions(
18+
{ depth: 4 },
19+
...(this.prefix ? [`[${this.prefix}]`] : []),
20+
...params,
21+
),
22+
);
23+
}
24+
trace(...params: unknown[]) {
25+
this.logWithFormat('trace', params);
26+
}
27+
debug(...params: unknown[]) {
28+
this.logWithFormat('debug', params);
29+
}
30+
info(...params: unknown[]) {
31+
this.logWithFormat('info', params);
32+
}
33+
warn(...params: unknown[]) {
34+
this.logWithFormat('warn', params);
35+
}
36+
error(...params: unknown[]) {
37+
this.logWithFormat('error', params);
38+
}
39+
}

0 commit comments

Comments
 (0)