feat(smartshell): add cwd-aware execution options, structured strict-mode errors, and safer process tree termination
This commit is contained in:
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartshell',
|
||||
version: '3.3.8',
|
||||
version: '3.4.0',
|
||||
description: 'A library for executing shell commands using promises.'
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export class ShellEnv {
|
||||
private _setPath(commandStringArg: string): string {
|
||||
let commandResult = commandStringArg;
|
||||
let commandPaths: string[] = [];
|
||||
commandPaths = commandPaths.concat(process.env.PATH.split(':'));
|
||||
commandPaths = commandPaths.concat(process.env.PATH?.split(':') ?? []);
|
||||
if (process.env.SMARTSHELL_PATH) {
|
||||
commandPaths = commandPaths.concat(process.env.SMARTSHELL_PATH.split(':'));
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ export interface IDeferred<T> {
|
||||
}
|
||||
|
||||
export class SmartExecution {
|
||||
public smartshell: Smartshell;
|
||||
public currentStreamingExecution: IExecResultStreaming;
|
||||
public smartshell!: Smartshell;
|
||||
public currentStreamingExecution!: IExecResultStreaming;
|
||||
public commandString: string;
|
||||
|
||||
private isRestartInProgress = false;
|
||||
@@ -52,4 +52,4 @@ export class SmartExecution {
|
||||
await this.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+168
-72
@@ -8,10 +8,37 @@ import * as cp from 'child_process';
|
||||
export interface IExecResult {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
/** stdout currently preserves smartshell's legacy combined stdout/stderr buffer. */
|
||||
combinedOutput?: string;
|
||||
signal?: NodeJS.Signals;
|
||||
stderr?: string;
|
||||
}
|
||||
|
||||
export class SmartshellError extends Error {
|
||||
public command: string;
|
||||
public result: IExecResult;
|
||||
public exitCode: number;
|
||||
public stdout: string;
|
||||
public combinedOutput?: string;
|
||||
public stderr?: string;
|
||||
public signal?: NodeJS.Signals;
|
||||
|
||||
constructor(command: string, result: IExecResult) {
|
||||
const reason = result.signal
|
||||
? `terminated by signal ${result.signal}`
|
||||
: `exited with code ${result.exitCode}`;
|
||||
super(`Command "${command}" ${reason}`);
|
||||
this.name = 'SmartshellError';
|
||||
this.command = command;
|
||||
this.result = result;
|
||||
this.exitCode = result.exitCode;
|
||||
this.stdout = result.stdout;
|
||||
this.combinedOutput = result.combinedOutput;
|
||||
this.stderr = result.stderr;
|
||||
this.signal = result.signal;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IExecResultInteractive extends IExecResult {
|
||||
sendInput: (input: string) => Promise<void>;
|
||||
sendLine: (line: string) => Promise<void>;
|
||||
@@ -31,15 +58,7 @@ export interface IExecResultStreaming {
|
||||
endInput: () => void;
|
||||
}
|
||||
|
||||
interface IExecOptions {
|
||||
commandString: string;
|
||||
silent?: boolean;
|
||||
strict?: boolean;
|
||||
streaming?: boolean;
|
||||
interactive?: boolean;
|
||||
passthrough?: boolean;
|
||||
interactiveControl?: boolean;
|
||||
usePty?: boolean;
|
||||
export interface IExecRuntimeOptions {
|
||||
ptyCols?: number;
|
||||
ptyRows?: number;
|
||||
ptyTerm?: string;
|
||||
@@ -49,14 +68,35 @@ interface IExecOptions {
|
||||
timeout?: number;
|
||||
debug?: boolean;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
cwd?: string;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface ISpawnOptions extends Omit<IExecOptions, 'commandString'> {
|
||||
interface IExecOptions extends IExecRuntimeOptions {
|
||||
commandString: string;
|
||||
silent?: boolean;
|
||||
strict?: boolean;
|
||||
streaming?: boolean;
|
||||
interactive?: boolean;
|
||||
passthrough?: boolean;
|
||||
interactiveControl?: boolean;
|
||||
usePty?: boolean;
|
||||
}
|
||||
|
||||
export interface ISpawnOptions extends IExecRuntimeOptions {
|
||||
command: string;
|
||||
args?: string[];
|
||||
silent?: boolean;
|
||||
strict?: boolean;
|
||||
streaming?: boolean;
|
||||
interactive?: boolean;
|
||||
passthrough?: boolean;
|
||||
interactiveControl?: boolean;
|
||||
usePty?: boolean;
|
||||
}
|
||||
|
||||
export type TExecCommandOptions = IExecRuntimeOptions;
|
||||
|
||||
export class Smartshell {
|
||||
public shellEnv: ShellEnv;
|
||||
public smartexit = new plugins.smartexit.SmartExit();
|
||||
@@ -68,9 +108,13 @@ export class Smartshell {
|
||||
/**
|
||||
* Executes a given command asynchronously.
|
||||
*/
|
||||
private async _exec(options: IExecOptions): Promise<IExecResult | IExecResultStreaming | void> {
|
||||
private async _exec(options: IExecOptions): Promise<IExecResult | IExecResultStreaming | IExecResultInteractive | void> {
|
||||
if (options.interactive) {
|
||||
return await this._execInteractive({ commandString: options.commandString });
|
||||
return await this._execInteractive({
|
||||
commandString: options.commandString,
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
});
|
||||
}
|
||||
return await this._execCommand(options);
|
||||
}
|
||||
@@ -78,7 +122,7 @@ export class Smartshell {
|
||||
/**
|
||||
* Executes an interactive command.
|
||||
*/
|
||||
private async _execInteractive(options: Pick<IExecOptions, 'commandString'>): Promise<void> {
|
||||
private async _execInteractive(options: Pick<IExecOptions, 'commandString' | 'cwd' | 'env'>): Promise<void> {
|
||||
// Skip interactive execution in CI environments.
|
||||
if (process.env.CI) {
|
||||
return;
|
||||
@@ -89,6 +133,8 @@ export class Smartshell {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
detached: true,
|
||||
cwd: options.cwd || process.cwd(),
|
||||
env: options.env || process.env,
|
||||
});
|
||||
|
||||
this.smartexit.addProcess(shell);
|
||||
@@ -117,7 +163,7 @@ export class Smartshell {
|
||||
|
||||
const execChildProcess = cp.spawn(options.command, options.args || [], {
|
||||
shell: false, // SECURITY: Never use shell with untrusted input
|
||||
cwd: process.cwd(),
|
||||
cwd: options.cwd || process.cwd(),
|
||||
env: options.env || process.env,
|
||||
detached: true, // Own process group — immune to terminal SIGINT, managed by smartexit
|
||||
signal: options.signal,
|
||||
@@ -132,7 +178,7 @@ export class Smartshell {
|
||||
if (options.debug) {
|
||||
console.log(`[smartshell] Timeout reached for process ${execChildProcess.pid}, terminating...`);
|
||||
}
|
||||
execChildProcess.kill('SIGTERM');
|
||||
void this.killProcessTree(execChildProcess.pid, 'SIGTERM', options.debug);
|
||||
}, options.timeout);
|
||||
}
|
||||
|
||||
@@ -232,18 +278,17 @@ export class Smartshell {
|
||||
}
|
||||
|
||||
const exitCode = typeof code === 'number' ? code : (signal ? 1 : 0);
|
||||
const combinedOutput = shellLogInstance.logStore.toString();
|
||||
const execResult: IExecResult = {
|
||||
exitCode,
|
||||
stdout: shellLogInstance.logStore.toString(),
|
||||
stdout: combinedOutput,
|
||||
combinedOutput,
|
||||
signal: signal || undefined,
|
||||
stderr: stderrBuffer,
|
||||
};
|
||||
|
||||
if (options.strict && exitCode !== 0) {
|
||||
const errorMsg = signal
|
||||
? `Command "${options.command}" terminated by signal ${signal}`
|
||||
: `Command "${options.command}" exited with code ${exitCode}`;
|
||||
reject(new Error(errorMsg));
|
||||
reject(new SmartshellError(options.command, execResult));
|
||||
} else {
|
||||
resolve(execResult);
|
||||
}
|
||||
@@ -296,25 +341,25 @@ export class Smartshell {
|
||||
if (options.debug) {
|
||||
console.log(`[smartshell] Running tree kill with SIGKILL on process ${execChildProcess.pid}`);
|
||||
}
|
||||
await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGKILL');
|
||||
await this.killProcessTree(execChildProcess.pid, 'SIGKILL', options.debug);
|
||||
},
|
||||
terminate: async () => {
|
||||
if (options.debug) {
|
||||
console.log(`[smartshell] Running tree kill with SIGTERM on process ${execChildProcess.pid}`);
|
||||
}
|
||||
await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGTERM');
|
||||
await this.killProcessTree(execChildProcess.pid, 'SIGTERM', options.debug);
|
||||
},
|
||||
keyboardInterrupt: async () => {
|
||||
if (options.debug) {
|
||||
console.log(`[smartshell] Running tree kill with SIGINT on process ${execChildProcess.pid}`);
|
||||
}
|
||||
await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGINT');
|
||||
await this.killProcessTree(execChildProcess.pid, 'SIGINT', options.debug);
|
||||
},
|
||||
customSignal: async (signal: plugins.smartexit.TProcessSignal) => {
|
||||
if (options.debug) {
|
||||
console.log(`[smartshell] Running tree kill with custom signal ${signal} on process ${execChildProcess.pid}`);
|
||||
}
|
||||
await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, signal);
|
||||
await this.killProcessTree(execChildProcess.pid, signal, options.debug);
|
||||
},
|
||||
} as IExecResultStreaming;
|
||||
}
|
||||
@@ -344,7 +389,7 @@ export class Smartshell {
|
||||
const shellBinary = this.shellEnv.executor === 'bash' ? '/bin/bash' : true;
|
||||
const execChildProcess = cp.spawn(commandToExecute, [], {
|
||||
shell: shellBinary,
|
||||
cwd: process.cwd(),
|
||||
cwd: options.cwd || process.cwd(),
|
||||
env: options.env || process.env,
|
||||
detached: true, // Own process group — immune to terminal SIGINT, managed by smartexit
|
||||
signal: options.signal,
|
||||
@@ -359,7 +404,7 @@ export class Smartshell {
|
||||
if (options.debug) {
|
||||
console.log(`[smartshell] Timeout reached for process ${execChildProcess.pid}, terminating...`);
|
||||
}
|
||||
execChildProcess.kill('SIGTERM');
|
||||
void this.killProcessTree(execChildProcess.pid, 'SIGTERM', options.debug);
|
||||
}, options.timeout);
|
||||
}
|
||||
|
||||
@@ -459,18 +504,17 @@ export class Smartshell {
|
||||
}
|
||||
|
||||
const exitCode = typeof code === 'number' ? code : (signal ? 1 : 0);
|
||||
const combinedOutput = shellLogInstance.logStore.toString();
|
||||
const execResult: IExecResult = {
|
||||
exitCode,
|
||||
stdout: shellLogInstance.logStore.toString(),
|
||||
stdout: combinedOutput,
|
||||
combinedOutput,
|
||||
signal: signal || undefined,
|
||||
stderr: stderrBuffer,
|
||||
};
|
||||
|
||||
if (options.strict && exitCode !== 0) {
|
||||
const errorMsg = signal
|
||||
? `Command "${options.commandString}" terminated by signal ${signal}`
|
||||
: `Command "${options.commandString}" exited with code ${exitCode}`;
|
||||
reject(new Error(errorMsg));
|
||||
reject(new SmartshellError(options.commandString, execResult));
|
||||
} else {
|
||||
resolve(execResult);
|
||||
}
|
||||
@@ -523,25 +567,25 @@ export class Smartshell {
|
||||
if (options.debug) {
|
||||
console.log(`[smartshell] Running tree kill with SIGKILL on process ${execChildProcess.pid}`);
|
||||
}
|
||||
await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGKILL');
|
||||
await this.killProcessTree(execChildProcess.pid, 'SIGKILL', options.debug);
|
||||
},
|
||||
terminate: async () => {
|
||||
if (options.debug) {
|
||||
console.log(`[smartshell] Running tree kill with SIGTERM on process ${execChildProcess.pid}`);
|
||||
}
|
||||
await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGTERM');
|
||||
await this.killProcessTree(execChildProcess.pid, 'SIGTERM', options.debug);
|
||||
},
|
||||
keyboardInterrupt: async () => {
|
||||
if (options.debug) {
|
||||
console.log(`[smartshell] Running tree kill with SIGINT on process ${execChildProcess.pid}`);
|
||||
}
|
||||
await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGINT');
|
||||
await this.killProcessTree(execChildProcess.pid, 'SIGINT', options.debug);
|
||||
},
|
||||
customSignal: async (signal: plugins.smartexit.TProcessSignal) => {
|
||||
if (options.debug) {
|
||||
console.log(`[smartshell] Running tree kill with custom signal ${signal} on process ${execChildProcess.pid}`);
|
||||
}
|
||||
await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, signal);
|
||||
await this.killProcessTree(execChildProcess.pid, signal, options.debug);
|
||||
},
|
||||
} as IExecResultStreaming;
|
||||
}
|
||||
@@ -550,58 +594,87 @@ export class Smartshell {
|
||||
return await childProcessEnded;
|
||||
}
|
||||
|
||||
public async exec(commandString: string): Promise<IExecResult> {
|
||||
const result = await this._exec({ commandString });
|
||||
private async killProcessTree(
|
||||
pid: number | undefined,
|
||||
signal: plugins.smartexit.TProcessSignal,
|
||||
debug?: boolean,
|
||||
): Promise<void> {
|
||||
if (!pid) {
|
||||
return;
|
||||
}
|
||||
if (debug) {
|
||||
console.log(`[smartshell] Killing process tree ${pid} with ${signal}`);
|
||||
}
|
||||
try {
|
||||
await plugins.smartexit.SmartExit.killTreeByPid(pid, signal);
|
||||
} catch (error) {
|
||||
if (debug) {
|
||||
console.log(`[smartshell] Tree kill failed for ${pid}: ${error}`);
|
||||
}
|
||||
try {
|
||||
process.kill(pid, signal as NodeJS.Signals);
|
||||
} catch {
|
||||
// Process already exited or is not accessible.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async exec(commandString: string, options: TExecCommandOptions = {}): Promise<IExecResult> {
|
||||
const result = await this._exec({ commandString, ...options });
|
||||
// Type assertion is safe here because non-streaming, non-interactive exec always returns IExecResult
|
||||
return result as IExecResult;
|
||||
}
|
||||
|
||||
public async execSilent(commandString: string): Promise<IExecResult> {
|
||||
return (await this._exec({ commandString, silent: true })) as IExecResult;
|
||||
public async execSilent(commandString: string, options: TExecCommandOptions = {}): Promise<IExecResult> {
|
||||
return (await this._exec({ commandString, ...options, silent: true })) as IExecResult;
|
||||
}
|
||||
|
||||
public async execStrict(commandString: string): Promise<IExecResult> {
|
||||
return (await this._exec({ commandString, strict: true })) as IExecResult;
|
||||
public async execStrict(commandString: string, options: TExecCommandOptions = {}): Promise<IExecResult> {
|
||||
return (await this._exec({ commandString, ...options, strict: true })) as IExecResult;
|
||||
}
|
||||
|
||||
public async execStrictSilent(commandString: string): Promise<IExecResult> {
|
||||
return (await this._exec({ commandString, silent: true, strict: true })) as IExecResult;
|
||||
public async execStrictSilent(commandString: string, options: TExecCommandOptions = {}): Promise<IExecResult> {
|
||||
return (await this._exec({ commandString, ...options, silent: true, strict: true })) as IExecResult;
|
||||
}
|
||||
|
||||
public async execStreaming(commandString: string, silent: boolean = false): Promise<IExecResultStreaming> {
|
||||
return (await this._exec({ commandString, silent, streaming: true })) as IExecResultStreaming;
|
||||
public async execStreaming(
|
||||
commandString: string,
|
||||
silent: boolean = false,
|
||||
options: TExecCommandOptions = {},
|
||||
): Promise<IExecResultStreaming> {
|
||||
return (await this._exec({ commandString, ...options, silent, streaming: true })) as IExecResultStreaming;
|
||||
}
|
||||
|
||||
public async execStreamingSilent(commandString: string): Promise<IExecResultStreaming> {
|
||||
return (await this._exec({ commandString, silent: true, streaming: true })) as IExecResultStreaming;
|
||||
public async execStreamingSilent(commandString: string, options: TExecCommandOptions = {}): Promise<IExecResultStreaming> {
|
||||
return (await this._exec({ commandString, ...options, silent: true, streaming: true })) as IExecResultStreaming;
|
||||
}
|
||||
|
||||
public async execInteractive(commandString: string): Promise<void> {
|
||||
await this._exec({ commandString, interactive: true });
|
||||
public async execInteractive(commandString: string, options: TExecCommandOptions = {}): Promise<void> {
|
||||
await this._exec({ commandString, ...options, interactive: true });
|
||||
}
|
||||
|
||||
public async execPassthrough(commandString: string): Promise<IExecResult> {
|
||||
return await this._exec({ commandString, passthrough: true }) as IExecResult;
|
||||
public async execPassthrough(commandString: string, options: TExecCommandOptions = {}): Promise<IExecResult> {
|
||||
return await this._exec({ commandString, ...options, passthrough: true }) as IExecResult;
|
||||
}
|
||||
|
||||
public async execStreamingPassthrough(commandString: string): Promise<IExecResultStreaming> {
|
||||
return await this._exec({ commandString, streaming: true, passthrough: true }) as IExecResultStreaming;
|
||||
public async execStreamingPassthrough(commandString: string, options: TExecCommandOptions = {}): Promise<IExecResultStreaming> {
|
||||
return await this._exec({ commandString, ...options, streaming: true, passthrough: true }) as IExecResultStreaming;
|
||||
}
|
||||
|
||||
public async execInteractiveControl(commandString: string): Promise<IExecResultInteractive> {
|
||||
return await this._exec({ commandString, interactiveControl: true }) as IExecResultInteractive;
|
||||
public async execInteractiveControl(commandString: string, options: TExecCommandOptions = {}): Promise<IExecResultInteractive> {
|
||||
return await this._exec({ commandString, ...options, interactiveControl: true }) as IExecResultInteractive;
|
||||
}
|
||||
|
||||
public async execStreamingInteractiveControl(commandString: string): Promise<IExecResultStreaming> {
|
||||
return await this._exec({ commandString, streaming: true, interactiveControl: true }) as IExecResultStreaming;
|
||||
public async execStreamingInteractiveControl(commandString: string, options: TExecCommandOptions = {}): Promise<IExecResultStreaming> {
|
||||
return await this._exec({ commandString, ...options, streaming: true, interactiveControl: true }) as IExecResultStreaming;
|
||||
}
|
||||
|
||||
public async execInteractiveControlPty(commandString: string): Promise<IExecResultInteractive> {
|
||||
return await this._exec({ commandString, interactiveControl: true, usePty: true }) as IExecResultInteractive;
|
||||
public async execInteractiveControlPty(commandString: string, options: TExecCommandOptions = {}): Promise<IExecResultInteractive> {
|
||||
return await this._exec({ commandString, ...options, interactiveControl: true, usePty: true }) as IExecResultInteractive;
|
||||
}
|
||||
|
||||
public async execStreamingInteractiveControlPty(commandString: string): Promise<IExecResultStreaming> {
|
||||
return await this._exec({ commandString, streaming: true, interactiveControl: true, usePty: true }) as IExecResultStreaming;
|
||||
public async execStreamingInteractiveControlPty(commandString: string, options: TExecCommandOptions = {}): Promise<IExecResultStreaming> {
|
||||
return await this._exec({ commandString, ...options, streaming: true, interactiveControl: true, usePty: true }) as IExecResultStreaming;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -618,23 +691,31 @@ export class Smartshell {
|
||||
* Executes a command with args array in streaming mode
|
||||
*/
|
||||
public async execSpawnStreaming(command: string, args: string[] = [], options: Omit<ISpawnOptions, 'command' | 'args' | 'streaming'> = {}): Promise<IExecResultStreaming> {
|
||||
return await this._execSpawn({ command, args, streaming: true, ...options }) as IExecResultStreaming;
|
||||
return await this._execSpawn({ command, args, ...options, streaming: true }) as IExecResultStreaming;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a command with args array with interactive control
|
||||
*/
|
||||
public async execSpawnInteractiveControl(command: string, args: string[] = [], options: Omit<ISpawnOptions, 'command' | 'args' | 'interactiveControl'> = {}): Promise<IExecResultInteractive> {
|
||||
return await this._execSpawn({ command, args, interactiveControl: true, ...options }) as IExecResultInteractive;
|
||||
return await this._execSpawn({ command, args, ...options, interactiveControl: true }) as IExecResultInteractive;
|
||||
}
|
||||
|
||||
public async execAndWaitForLine(
|
||||
commandString: string,
|
||||
regex: RegExp,
|
||||
silent: boolean = false,
|
||||
options: { timeout?: number; terminateOnMatch?: boolean } = {}
|
||||
options: { timeout?: number; terminateOnMatch?: boolean; cwd?: string; env?: NodeJS.ProcessEnv } = {}
|
||||
): Promise<void> {
|
||||
const execStreamingResult = await this.execStreaming(commandString, silent);
|
||||
const execStreamingResult = await this.execStreaming(commandString, silent, {
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
});
|
||||
const stdout = execStreamingResult.childProcess.stdout;
|
||||
if (!stdout) {
|
||||
await execStreamingResult.terminate();
|
||||
throw new Error('stdout is not available for this process');
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
let matched = false;
|
||||
@@ -646,7 +727,7 @@ export class Smartshell {
|
||||
if (!matched) {
|
||||
matched = true;
|
||||
// Remove listener to prevent memory leak
|
||||
execStreamingResult.childProcess.stdout.removeAllListeners('data');
|
||||
stdout.removeAllListeners('data');
|
||||
await execStreamingResult.terminate();
|
||||
reject(new Error(`Timeout waiting for pattern after ${options.timeout}ms`));
|
||||
}
|
||||
@@ -664,7 +745,7 @@ export class Smartshell {
|
||||
}
|
||||
|
||||
// Remove listener to prevent memory leak
|
||||
execStreamingResult.childProcess.stdout.removeListener('data', dataHandler);
|
||||
stdout.removeListener('data', dataHandler);
|
||||
|
||||
// Terminate process if requested
|
||||
if (options.terminateOnMatch) {
|
||||
@@ -676,7 +757,7 @@ export class Smartshell {
|
||||
}
|
||||
};
|
||||
|
||||
execStreamingResult.childProcess.stdout.on('data', dataHandler);
|
||||
stdout.on('data', dataHandler);
|
||||
|
||||
// Also resolve/reject when process ends
|
||||
execStreamingResult.finalPromise.then(() => {
|
||||
@@ -699,7 +780,7 @@ export class Smartshell {
|
||||
});
|
||||
}
|
||||
|
||||
public async execAndWaitForLineSilent(commandString: string, regex: RegExp, options?: { timeout?: number; terminateOnMatch?: boolean }): Promise<void> {
|
||||
public async execAndWaitForLineSilent(commandString: string, regex: RegExp, options?: { timeout?: number; terminateOnMatch?: boolean; cwd?: string; env?: NodeJS.ProcessEnv }): Promise<void> {
|
||||
return this.execAndWaitForLine(commandString, regex, true, options);
|
||||
}
|
||||
|
||||
@@ -769,7 +850,7 @@ export class Smartshell {
|
||||
name: options.ptyTerm || 'xterm-256color',
|
||||
cols: options.ptyCols || 120,
|
||||
rows: options.ptyRows || 30,
|
||||
cwd: process.cwd(),
|
||||
cwd: options.cwd || process.cwd(),
|
||||
env: options.env || process.env,
|
||||
});
|
||||
|
||||
@@ -784,18 +865,33 @@ export class Smartshell {
|
||||
shellLogInstance.addToBuffer(data);
|
||||
});
|
||||
|
||||
let timeoutHandle: NodeJS.Timeout | null = null;
|
||||
if (options.timeout) {
|
||||
timeoutHandle = setTimeout(() => {
|
||||
if (options.debug) {
|
||||
console.log(`[smartshell] Timeout reached for PTY process ${ptyProcess.pid}, terminating...`);
|
||||
}
|
||||
ptyProcess.kill('SIGTERM');
|
||||
}, options.timeout);
|
||||
}
|
||||
|
||||
// Wrap PTY termination into a Promise
|
||||
const childProcessEnded: Promise<IExecResult> = new Promise((resolve, reject) => {
|
||||
ptyProcess.onExit(({ exitCode, signal }: { exitCode: number; signal?: number }) => {
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
this.smartexit.removeProcess({ pid: ptyProcess.pid } as any);
|
||||
|
||||
const combinedOutput = shellLogInstance.logStore.toString();
|
||||
const execResult: IExecResult = {
|
||||
exitCode: exitCode ?? (signal ? 1 : 0),
|
||||
stdout: shellLogInstance.logStore.toString(),
|
||||
stdout: combinedOutput,
|
||||
combinedOutput,
|
||||
};
|
||||
|
||||
if (options.strict && exitCode !== 0) {
|
||||
reject(new Error(`Command "${options.commandString}" exited with code ${exitCode}`));
|
||||
reject(new SmartshellError(options.commandString, execResult));
|
||||
} else {
|
||||
resolve(execResult);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user