From 83242540b5beab4a2b71eddd2269b321027817f5 Mon Sep 17 00:00:00 2001 From: OrigamingWasTaken <74014262+OrigamingWasTaken@users.noreply.github.com> Date: Tue, 8 Oct 2024 22:00:58 +0200 Subject: [PATCH] Added events to spawn() For exemple: ```ts const process = spawn(command,args) process.on("stdOut",console.log) ``` --- frontend/src/windows/main/ts/tools/shell.ts | 197 +++++++++++--------- 1 file changed, 104 insertions(+), 93 deletions(-) diff --git a/frontend/src/windows/main/ts/tools/shell.ts b/frontend/src/windows/main/ts/tools/shell.ts index e445373..abe1ea4 100644 --- a/frontend/src/windows/main/ts/tools/shell.ts +++ b/frontend/src/windows/main/ts/tools/shell.ts @@ -5,28 +5,28 @@ import { events, os } from '@neutralinojs/lib'; * Represents the result of a shell command execution. */ interface ExecutionResult { - /** The standard output of the command. */ - stdOut: string; - /** The standard error output of the command. */ - stdErr: string; - /** The exit code of the command. */ - exitCode: number; + /** The standard output of the command. */ + stdOut: string; + /** The standard error output of the command. */ + stdErr: string; + /** The exit code of the command. */ + exitCode: number; } /** * Options for executing shell commands. */ export interface ExecuteOptions { - /** If true, don't throw an error when stderr is not empty. Default is false. */ - skipStderrCheck?: boolean; - /** The timeout in milliseconds. If provided, the command will be terminated after this time. */ - timeoutMs?: number; - /** If true, the command will be treated as a "whole" command and args will be ignored */ - completeCommand?: boolean; - /** Pass this argument to neutralino's os.execCommand function */ - background?: boolean; - /** Directory from which to execute the command */ - cwd?: string; + /** If true, don't throw an error when stderr is not empty. Default is false. */ + skipStderrCheck?: boolean; + /** The timeout in milliseconds. If provided, the command will be terminated after this time. */ + timeoutMs?: number; + /** If true, the command will be treated as a "whole" command and args will be ignored */ + completeCommand?: boolean; + /** Pass this argument to neutralino's os.execCommand function */ + background?: boolean; + /** Directory from which to execute the command */ + cwd?: string; } /** @@ -103,91 +103,102 @@ export async function shell( } /** - * Represents the result of a process spawn. + * Options for spawning processes. */ -interface SpawnResult { - /** The standard output of the process. */ - stdout: string; - /** The standard error output of the process. */ - stderr: string; - /** The exit code of the process. */ - exitCode: number | null; +export interface SpawnOptions { + /** If true, don't throw an error when stderr is not empty. Default is false. */ + skipStderrCheck?: boolean; + /** The timeout in milliseconds. If provided, the process will be terminated after this time. */ + timeoutMs?: number; + /** The current working directory for the spawned process. */ + cwd?: string; } /** - * Options for spawning processes. + * Interface for the event emitter functionality of spawned processes. */ -export interface SpawnOptions { - /** If true, don't throw an error when stderr is not empty. Default is false. */ - skipStderrCheck?: boolean; - /** The timeout in milliseconds. If provided, the process will be terminated after this time. */ - timeoutMs?: number; - /** The current working directory for the spawned process. */ - cwd?: string; +export interface SpawnEventEmitter { + on(event: 'stdOut' | 'stdErr' | 'exit', listener: (data: string) => void): void; + off(event: 'stdOut' | 'stdErr' | 'exit', listener: (data: string) => void): void; } /** - * Spawns a process using os.spawnProcess. - * @param command - The command to execute. - * @param options - Spawn options. - * @returns A promise that resolves with the SpawnResult. - * @throws Will throw an error if the process spawn fails, times out, or if stderr is not empty (unless skipStderrCheck is true). + * Represents a spawned process with event emitter functionality. */ -export function spawn(command: string, args: string[] = [], options: SpawnOptions = {}): Promise { - return new Promise((resolve, reject) => { - const fullCommand = buildCommand(command, args); - let stdout = ''; - let stderr = ''; - let exitCode: number | null = null; - let timeoutId: Timer | null = null; - - const cleanup = (processHandler: (evt: any) => void) => { - if (timeoutId) { - clearTimeout(timeoutId); - } - events.off('spawnedProcess', processHandler); - }; - - const handleProcessEnd = (processHandler: (evt: any) => void) => { - cleanup(processHandler); - if (!options.skipStderrCheck && stderr.trim().length > 0) { - reject(new Error(`Process produced stderr output: ${stderr}`)); - } else { - resolve({ stdout, stderr, exitCode }); - } - }; - - os.spawnProcess(fullCommand, options.cwd) - .then((process) => { - const processHandler = (evt: any) => { - if (process.id === evt.detail.id) { - switch (evt.detail.action) { - case 'stdOut': - stdout += evt.detail.data; - break; - case 'stdErr': - stderr += evt.detail.data; - break; - case 'exit': - exitCode = parseInt(evt.detail.data, 10); - handleProcessEnd(processHandler); - break; - } - } - }; +class SpawnedProcess implements SpawnEventEmitter { + private listeners: { [key: string]: ((data: string) => void)[] } = { + stdOut: [], + stdErr: [], + exit: [] + }; + + on(event: 'stdOut' | 'stdErr' | 'exit', listener: (data: string) => void): void { + this.listeners[event].push(listener); + } + + off(event: 'stdOut' | 'stdErr' | 'exit', listener: (data: string) => void): void { + const index = this.listeners[event].indexOf(listener); + if (index !== -1) { + this.listeners[event].splice(index, 1); + } + } + + emit(event: 'stdOut' | 'stdErr' | 'exit', data: string): void { + this.listeners[event].forEach(listener => listener(data)); + } +} - events.on('spawnedProcess', processHandler); +// Map to store all active spawned processes +const spawnedProcesses = new Map(); - if (options.timeoutMs) { - timeoutId = setTimeout(() => { - os.updateSpawnedProcess(process.id, 'terminate'); - cleanup(processHandler); - reject(new Error(`Process execution timed out after ${options.timeoutMs}ms`)); - }, options.timeoutMs); - } - }) - .catch((error) => { - reject(new Error(`Failed to spawn process: ${error.message}`)); - }); - }); -} +// Global event handler for all spawned processes +events.on('spawnedProcess', (evt: any) => { + const { id, action, data } = evt.detail; + const process = spawnedProcesses.get(id); + if (process) { + process.emit(action as 'stdOut' | 'stdErr' | 'exit', data); + } +}); + +/** + * Spawns a process using os.spawnProcess with event emitter functionality. + * @param command - The command to execute. + * @param args - An array of arguments for the command. + * @param options - Spawn options. + * @returns A promise that resolves with the exit code, combined with an event emitter interface. + * @throws Will throw an error if the process spawn fails or times out. + */ +export function spawn(command: string, args: (string | number)[] = [], options: SpawnOptions = {}): SpawnEventEmitter & Promise { + const fullCommand = buildCommand(command, args); + const spawnedProcess = new SpawnedProcess(); + + const promise = new Promise((resolve, reject) => { + let timeoutId: Timer | null = null; + + const handleExit = (exitCode: string) => { + if (timeoutId) clearTimeout(timeoutId); + resolve(parseInt(exitCode, 10)); + }; + + spawnedProcess.on('exit', handleExit); + + os.spawnProcess(fullCommand, options.cwd) + .then((process) => { + spawnedProcesses.set(process.id, spawnedProcess); + + if (options.timeoutMs) { + timeoutId = setTimeout(() => { + os.updateSpawnedProcess(process.id, 'terminate'); + spawnedProcesses.delete(process.id); + reject(new Error(`Process execution timed out after ${options.timeoutMs}ms`)); + }, options.timeoutMs); + } + }) + .catch((error) => { + reject(new Error(`Failed to spawn process: ${error.message}`)); + }); + }); + + // Combine the SpawnedProcess instance with the Promise + return Object.assign(spawnedProcess, promise); +} \ No newline at end of file