Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/test/simulation/workbench/utils/simulationExec.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
import * as cp from 'child_process';
6
import { ipcRenderer } from 'electron';
7
import { IDisposable } from 'monaco-editor';
8
import * as path from 'path';
9
import { AsyncIterableEmitter, AsyncIterableObject } from '../../../../src/util/vs/base/common/async';
10
import { CancellationToken } from '../../../../src/util/vs/base/common/cancellation';
11
import { REPO_ROOT } from './utils';
12
13
export const SIMULATION_MAIN_PATH = path.join(REPO_ROOT, './dist/simulationMain.js');
14
15
export interface ISpawnSimulationOptions {
16
args: string[];
17
ignoreNonJSONLines?: boolean;
18
}
19
20
export function spawnSimulation<T>(options: ISpawnSimulationOptions, token: CancellationToken = CancellationToken.None): AsyncIterableObject<T> {
21
return extractJSONL<T>(forkSimulationMain(options.args, token), options);
22
}
23
24
/** spawn `npm run simulate` from Electron main process */
25
export function spawnSimulationFromMainProcess<T>(options: ISpawnSimulationOptions, token: CancellationToken = CancellationToken.None): AsyncIterableObject<T> {
26
return extractJSONL<T>(forkSimulationMainFromMainProcess(options.args, token), options);
27
}
28
29
let mainRendererEventProcessor: MainProcessEventHandler | undefined;
30
31
function forkSimulationMainFromMainProcess(args: string[], token: CancellationToken): AsyncIterableObject<string> {
32
if (!mainRendererEventProcessor) {
33
mainRendererEventProcessor = new MainProcessEventHandler();
34
}
35
return mainRendererEventProcessor.spawn(args, token);
36
}
37
38
export function extractJSONL<T>(source: AsyncIterableObject<string>, options?: ISpawnSimulationOptions): AsyncIterableObject<T> {
39
return splitToLines(source).map((line): T | null => {
40
if (line.length === 0) {
41
// always ignore empty lines
42
return null;
43
}
44
45
if (!line.startsWith('{') || !line.endsWith('}')) {
46
if (!options?.ignoreNonJSONLines) {
47
console.warn(line);
48
}
49
return null;
50
}
51
52
try {
53
const obj = JSON.parse(line);
54
return obj as T;
55
} catch (err) {
56
if (!options?.ignoreNonJSONLines) {
57
console.error(`ignoring invalid line: ${line}`);
58
}
59
return null;
60
}
61
}).coalesce();
62
}
63
64
/**
65
* Split an incoming stream of text to a stream of lines.
66
*/
67
function splitToLines(source: AsyncIterable<string>): AsyncIterableObject<string> {
68
return new AsyncIterableObject<string>(async (emitter) => {
69
let buffer = '';
70
for await (const str of source) {
71
buffer += str;
72
do {
73
const newlineIndex = buffer.indexOf('\n');
74
if (newlineIndex === -1) {
75
break;
76
}
77
78
// take the first line
79
const line = buffer.substring(0, newlineIndex);
80
buffer = buffer.substring(newlineIndex + 1);
81
82
emitter.emitOne(line);
83
} while (true);
84
}
85
86
if (buffer.length > 0) {
87
// last line which doesn't end with \n
88
emitter.emitOne(buffer);
89
}
90
});
91
}
92
93
function forkSimulationMain(args: string[], token: CancellationToken): AsyncIterableObject<string> {
94
return new AsyncIterableObject<string>((emitter) => {
95
return new Promise<void>((resolve, reject) => {
96
const proc = cp.spawn('node', [SIMULATION_MAIN_PATH, ...args], { stdio: 'pipe' });
97
const listener = token.onCancellationRequested(() => {
98
proc.kill('SIGTERM');
99
// FIXME@ulugbekna: let's not reject the promise for now -- otherwise, stdout.json.txt isn't written
100
// reject(new CancellationError());
101
});
102
proc.on('error', (err) => {
103
listener.dispose();
104
reject(err);
105
});
106
proc.on('exit', (code, signal) => {
107
listener.dispose();
108
if (code !== 0) {
109
reject(new Error(`Process exited with code ${code}`));
110
return;
111
}
112
resolve();
113
});
114
proc.stdout?.setEncoding('utf8');
115
proc.stdout?.on('data', (data) => {
116
emitter?.emitOne(data);
117
});
118
119
proc.stderr?.setEncoding('utf8');
120
proc.stderr?.on('data', (data) => {
121
console.error(data);
122
});
123
});
124
});
125
}
126
127
type MainProcessEventHandle = {
128
emitter: AsyncIterableEmitter<string>;
129
cancellationListener: IDisposable;
130
resolve: () => void;
131
reject: (reason?: string) => void;
132
stderrChunks: string[];
133
};
134
135
// change to configure logging, e.g., to `console.debug`
136
const log = {
137
debug: (...args: any) => { }
138
};
139
140
class MainProcessEventHandler {
141
142
private i: number;
143
private idMap: Map<number, MainProcessEventHandle>;
144
145
constructor() {
146
this.i = 0;
147
this.idMap = new Map<number, MainProcessEventHandle>();
148
149
ipcRenderer.on('stdout-data', (_event, { id, data }) => {
150
log.debug(`stdout-data (ID ${id}): ${data.toString()}`);
151
const handle = this.getHandleOrThrow(id);
152
handle.emitter.emitOne(data);
153
});
154
155
ipcRenderer.on('stderr-data', (_event, { id, data }) => {
156
console.warn(`stderr-data (ID ${id}): ${data.toString()}`);
157
const handle = this.idMap.get(id);
158
if (!handle) {
159
return;
160
}
161
handle.stderrChunks.push(data.toString());
162
});
163
164
ipcRenderer.on('process-exit', (_event, { id, code }) => {
165
log.debug(`process exit (ID ${id}) with code ${code}`);
166
const handle = this.getHandleOrThrow(id);
167
this.idMap.delete(id);
168
handle.cancellationListener.dispose();
169
if (code === 0) {
170
handle.resolve();
171
} else {
172
const stderr = handle.stderrChunks.join('');
173
handle.reject(stderr || `Process exited with code ${code}`);
174
}
175
});
176
}
177
178
spawn(processArgs: string[], token: CancellationToken) {
179
const id = this.i++;
180
const idMap = this.idMap;
181
182
return new AsyncIterableObject<string>((emitter) => {
183
return new Promise<void>((resolve, reject) => {
184
const cancellationListener = token.onCancellationRequested(() => {
185
ipcRenderer.send('kill-process', { id });
186
});
187
188
idMap.set(id, { emitter, cancellationListener, resolve, reject, stderrChunks: [] });
189
ipcRenderer.send('spawn-process', { id, processArgs });
190
});
191
});
192
}
193
194
private getHandleOrThrow(id: number) {
195
const handle = this.idMap.get(id);
196
if (!handle) {
197
throw new Error(`[MainProcessEventHandler] No handle found for ID ${id}`);
198
}
199
return handle;
200
}
201
}
202
203