Path: blob/main/extensions/copilot/test/simulation/workbench/utils/simulationExec.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/4import * as cp from 'child_process';5import { ipcRenderer } from 'electron';6import { IDisposable } from 'monaco-editor';7import * as path from 'path';8import { AsyncIterableEmitter, AsyncIterableObject } from '../../../../src/util/vs/base/common/async';9import { CancellationToken } from '../../../../src/util/vs/base/common/cancellation';10import { REPO_ROOT } from './utils';1112export const SIMULATION_MAIN_PATH = path.join(REPO_ROOT, './dist/simulationMain.js');1314export interface ISpawnSimulationOptions {15args: string[];16ignoreNonJSONLines?: boolean;17}1819export function spawnSimulation<T>(options: ISpawnSimulationOptions, token: CancellationToken = CancellationToken.None): AsyncIterableObject<T> {20return extractJSONL<T>(forkSimulationMain(options.args, token), options);21}2223/** spawn `npm run simulate` from Electron main process */24export function spawnSimulationFromMainProcess<T>(options: ISpawnSimulationOptions, token: CancellationToken = CancellationToken.None): AsyncIterableObject<T> {25return extractJSONL<T>(forkSimulationMainFromMainProcess(options.args, token), options);26}2728let mainRendererEventProcessor: MainProcessEventHandler | undefined;2930function forkSimulationMainFromMainProcess(args: string[], token: CancellationToken): AsyncIterableObject<string> {31if (!mainRendererEventProcessor) {32mainRendererEventProcessor = new MainProcessEventHandler();33}34return mainRendererEventProcessor.spawn(args, token);35}3637export function extractJSONL<T>(source: AsyncIterableObject<string>, options?: ISpawnSimulationOptions): AsyncIterableObject<T> {38return splitToLines(source).map((line): T | null => {39if (line.length === 0) {40// always ignore empty lines41return null;42}4344if (!line.startsWith('{') || !line.endsWith('}')) {45if (!options?.ignoreNonJSONLines) {46console.warn(line);47}48return null;49}5051try {52const obj = JSON.parse(line);53return obj as T;54} catch (err) {55if (!options?.ignoreNonJSONLines) {56console.error(`ignoring invalid line: ${line}`);57}58return null;59}60}).coalesce();61}6263/**64* Split an incoming stream of text to a stream of lines.65*/66function splitToLines(source: AsyncIterable<string>): AsyncIterableObject<string> {67return new AsyncIterableObject<string>(async (emitter) => {68let buffer = '';69for await (const str of source) {70buffer += str;71do {72const newlineIndex = buffer.indexOf('\n');73if (newlineIndex === -1) {74break;75}7677// take the first line78const line = buffer.substring(0, newlineIndex);79buffer = buffer.substring(newlineIndex + 1);8081emitter.emitOne(line);82} while (true);83}8485if (buffer.length > 0) {86// last line which doesn't end with \n87emitter.emitOne(buffer);88}89});90}9192function forkSimulationMain(args: string[], token: CancellationToken): AsyncIterableObject<string> {93return new AsyncIterableObject<string>((emitter) => {94return new Promise<void>((resolve, reject) => {95const proc = cp.spawn('node', [SIMULATION_MAIN_PATH, ...args], { stdio: 'pipe' });96const listener = token.onCancellationRequested(() => {97proc.kill('SIGTERM');98// FIXME@ulugbekna: let's not reject the promise for now -- otherwise, stdout.json.txt isn't written99// reject(new CancellationError());100});101proc.on('error', (err) => {102listener.dispose();103reject(err);104});105proc.on('exit', (code, signal) => {106listener.dispose();107if (code !== 0) {108reject(new Error(`Process exited with code ${code}`));109return;110}111resolve();112});113proc.stdout?.setEncoding('utf8');114proc.stdout?.on('data', (data) => {115emitter?.emitOne(data);116});117118proc.stderr?.setEncoding('utf8');119proc.stderr?.on('data', (data) => {120console.error(data);121});122});123});124}125126type MainProcessEventHandle = {127emitter: AsyncIterableEmitter<string>;128cancellationListener: IDisposable;129resolve: () => void;130reject: (reason?: string) => void;131stderrChunks: string[];132};133134// change to configure logging, e.g., to `console.debug`135const log = {136debug: (...args: any) => { }137};138139class MainProcessEventHandler {140141private i: number;142private idMap: Map<number, MainProcessEventHandle>;143144constructor() {145this.i = 0;146this.idMap = new Map<number, MainProcessEventHandle>();147148ipcRenderer.on('stdout-data', (_event, { id, data }) => {149log.debug(`stdout-data (ID ${id}): ${data.toString()}`);150const handle = this.getHandleOrThrow(id);151handle.emitter.emitOne(data);152});153154ipcRenderer.on('stderr-data', (_event, { id, data }) => {155console.warn(`stderr-data (ID ${id}): ${data.toString()}`);156const handle = this.idMap.get(id);157if (!handle) {158return;159}160handle.stderrChunks.push(data.toString());161});162163ipcRenderer.on('process-exit', (_event, { id, code }) => {164log.debug(`process exit (ID ${id}) with code ${code}`);165const handle = this.getHandleOrThrow(id);166this.idMap.delete(id);167handle.cancellationListener.dispose();168if (code === 0) {169handle.resolve();170} else {171const stderr = handle.stderrChunks.join('');172handle.reject(stderr || `Process exited with code ${code}`);173}174});175}176177spawn(processArgs: string[], token: CancellationToken) {178const id = this.i++;179const idMap = this.idMap;180181return new AsyncIterableObject<string>((emitter) => {182return new Promise<void>((resolve, reject) => {183const cancellationListener = token.onCancellationRequested(() => {184ipcRenderer.send('kill-process', { id });185});186187idMap.set(id, { emitter, cancellationListener, resolve, reject, stderrChunks: [] });188ipcRenderer.send('spawn-process', { id, processArgs });189});190});191}192193private getHandleOrThrow(id: number) {194const handle = this.idMap.get(id);195if (!handle) {196throw new Error(`[MainProcessEventHandler] No handle found for ID ${id}`);197}198return handle;199}200}201202203