Skip to content

Commit

Permalink
Added events to spawn()
Browse files Browse the repository at this point in the history
For exemple:

```ts
const process = spawn(command,args)
process.on("stdOut",console.log)
```
  • Loading branch information
OrigamingWasTaken committed Oct 8, 2024
1 parent 529bae8 commit 8324254
Showing 1 changed file with 104 additions and 93 deletions.
197 changes: 104 additions & 93 deletions frontend/src/windows/main/ts/tools/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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<SpawnResult> {
return new Promise<SpawnResult>((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<number, SpawnedProcess>();

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<number> {
const fullCommand = buildCommand(command, args);
const spawnedProcess = new SpawnedProcess();

const promise = new Promise<number>((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);
}

0 comments on commit 8324254

Please sign in to comment.