| 
									
										
										
										
											2024-12-09 02:39:31 +01:00
										 |  |  | import * as plugins from './plugins.js'; | 
					
						
							|  |  |  | import { ShellEnv } from './classes.shellenv.js'; | 
					
						
							|  |  |  | import type { IShellEnvContructorOptions, TExecutor } from './classes.shellenv.js'; | 
					
						
							|  |  |  | import { ShellLog } from './classes.shelllog.js'; | 
					
						
							| 
									
										
										
										
											2018-07-30 16:08:14 +02:00
										 |  |  | import * as cp from 'child_process'; | 
					
						
							| 
									
										
										
										
											2017-03-08 16:51:02 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  | // -- interfaces --
 | 
					
						
							|  |  |  | export interface IExecResult { | 
					
						
							|  |  |  |   exitCode: number; | 
					
						
							|  |  |  |   stdout: string; | 
					
						
							| 
									
										
										
										
											2017-03-08 16:51:02 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  | export interface IExecResultStreaming { | 
					
						
							|  |  |  |   childProcess: cp.ChildProcess; | 
					
						
							|  |  |  |   finalPromise: Promise<IExecResult>; | 
					
						
							| 
									
										
										
										
											2024-04-18 13:42:51 +02:00
										 |  |  |   kill: () => Promise<void>; | 
					
						
							|  |  |  |   terminate: () => Promise<void>; | 
					
						
							|  |  |  |   keyboardInterrupt: () => Promise<void>; | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |   customSignal: (signal: plugins.smartexit.TProcessSignal) => Promise<void>; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | interface IExecOptions { | 
					
						
							|  |  |  |   commandString: string; | 
					
						
							|  |  |  |   silent?: boolean; | 
					
						
							|  |  |  |   strict?: boolean; | 
					
						
							|  |  |  |   streaming?: boolean; | 
					
						
							|  |  |  |   interactive?: boolean; | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-03-08 16:51:02 +01:00
										 |  |  | export class Smartshell { | 
					
						
							| 
									
										
										
										
											2019-05-19 22:41:20 +02:00
										 |  |  |   public shellEnv: ShellEnv; | 
					
						
							|  |  |  |   public smartexit = new plugins.smartexit.SmartExit(); | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |   constructor(optionsArg: IShellEnvContructorOptions) { | 
					
						
							|  |  |  |     this.shellEnv = new ShellEnv(optionsArg); | 
					
						
							| 
									
										
										
										
											2018-11-26 17:55:15 +01:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2017-03-10 20:14:40 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  |   /** | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |    * Executes a given command asynchronously. | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  |    */ | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |   private async _exec(options: IExecOptions): Promise<IExecResult | IExecResultStreaming | void> { | 
					
						
							| 
									
										
										
										
											2023-06-22 11:51:44 +02:00
										 |  |  |     if (options.interactive) { | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |       return await this._execInteractive({ commandString: options.commandString }); | 
					
						
							| 
									
										
										
										
											2024-09-17 17:02:42 +02:00
										 |  |  |     } | 
					
						
							|  |  |  |     return await this._execCommand(options); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-06-22 14:16:16 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |   /** | 
					
						
							|  |  |  |    * Executes an interactive command. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private async _execInteractive(options: Pick<IExecOptions, 'commandString'>): Promise<void> { | 
					
						
							|  |  |  |     // Skip interactive execution in CI environments.
 | 
					
						
							| 
									
										
										
										
											2024-09-17 17:02:42 +02:00
										 |  |  |     if (process.env.CI) { | 
					
						
							|  |  |  |       return; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-06-22 14:16:16 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |     return new Promise<void>((resolve) => { | 
					
						
							|  |  |  |       const shell = cp.spawn(options.commandString, { | 
					
						
							|  |  |  |         stdio: 'inherit', | 
					
						
							|  |  |  |         shell: true, | 
					
						
							|  |  |  |         detached: true, | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2023-06-22 14:16:16 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |       this.smartexit.addProcess(shell); | 
					
						
							| 
									
										
										
										
											2024-09-17 17:02:42 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |       shell.on('close', (code) => { | 
					
						
							|  |  |  |         console.log(`Interactive shell terminated with code ${code}`); | 
					
						
							|  |  |  |         this.smartexit.removeProcess(shell); | 
					
						
							|  |  |  |         resolve(); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2024-09-17 17:02:42 +02:00
										 |  |  |     }); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2023-06-22 11:51:44 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |   /** | 
					
						
							|  |  |  |    * Executes a command and returns either a non-streaming result or a streaming interface. | 
					
						
							|  |  |  |    */ | 
					
						
							|  |  |  |   private async _execCommand(options: IExecOptions): Promise<IExecResult | IExecResultStreaming> { | 
					
						
							| 
									
										
										
										
											2024-09-17 17:02:42 +02:00
										 |  |  |     const commandToExecute = this.shellEnv.createEnvExecString(options.commandString); | 
					
						
							|  |  |  |     const shellLogInstance = new ShellLog(); | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  |     const execChildProcess = cp.spawn(commandToExecute, [], { | 
					
						
							|  |  |  |       shell: true, | 
					
						
							| 
									
										
										
										
											2021-11-26 15:17:52 +01:00
										 |  |  |       cwd: process.cwd(), | 
					
						
							| 
									
										
										
										
											2018-10-28 19:12:15 +01:00
										 |  |  |       env: process.env, | 
					
						
							| 
									
										
										
										
											2020-05-22 01:23:27 +00:00
										 |  |  |       detached: false, | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2019-05-19 22:41:20 +02:00
										 |  |  |     this.smartexit.addProcess(execChildProcess); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |     // Capture stdout and stderr output.
 | 
					
						
							| 
									
										
										
										
											2020-05-22 01:23:27 +00:00
										 |  |  |     execChildProcess.stdout.on('data', (data) => { | 
					
						
							| 
									
										
										
										
											2023-06-22 11:51:44 +02:00
										 |  |  |       if (!options.silent) { | 
					
						
							| 
									
										
										
										
											2024-09-17 17:02:42 +02:00
										 |  |  |         shellLogInstance.writeToConsole(data); | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2024-09-17 17:02:42 +02:00
										 |  |  |       shellLogInstance.addToBuffer(data); | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  |     }); | 
					
						
							| 
									
										
										
										
											2023-06-22 11:51:44 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-22 01:23:27 +00:00
										 |  |  |     execChildProcess.stderr.on('data', (data) => { | 
					
						
							| 
									
										
										
										
											2023-06-22 11:51:44 +02:00
										 |  |  |       if (!options.silent) { | 
					
						
							| 
									
										
										
										
											2024-09-17 17:02:42 +02:00
										 |  |  |         shellLogInstance.writeToConsole(data); | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  |       } | 
					
						
							| 
									
										
										
										
											2024-09-17 17:02:42 +02:00
										 |  |  |       shellLogInstance.addToBuffer(data); | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |     // Wrap child process termination into a Promise.
 | 
					
						
							|  |  |  |     const childProcessEnded: Promise<IExecResult> = new Promise((resolve, reject) => { | 
					
						
							|  |  |  |       execChildProcess.on('exit', (code, signal) => { | 
					
						
							|  |  |  |         this.smartexit.removeProcess(execChildProcess); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         const execResult: IExecResult = { | 
					
						
							|  |  |  |           exitCode: typeof code === 'number' ? code : (signal ? 1 : 0), | 
					
						
							|  |  |  |           stdout: shellLogInstance.logStore.toString(), | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         if (options.strict && code !== 0) { | 
					
						
							|  |  |  |           reject(new Error(`Command "${options.commandString}" exited with code ${code}`)); | 
					
						
							|  |  |  |         } else { | 
					
						
							|  |  |  |           resolve(execResult); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |       execChildProcess.on('error', (error) => { | 
					
						
							|  |  |  |         this.smartexit.removeProcess(execChildProcess); | 
					
						
							|  |  |  |         reject(error); | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  |     }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |     // If streaming mode is enabled, return a streaming interface immediately.
 | 
					
						
							| 
									
										
										
										
											2023-06-22 11:51:44 +02:00
										 |  |  |     if (options.streaming) { | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |       return { | 
					
						
							| 
									
										
										
										
											2021-07-26 21:24:13 +02:00
										 |  |  |         childProcess: execChildProcess, | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |         finalPromise: childProcessEnded, | 
					
						
							| 
									
										
										
										
											2024-04-18 13:42:51 +02:00
										 |  |  |         kill: async () => { | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |           console.log(`Running tree kill with SIGKILL on process ${execChildProcess.pid}`); | 
					
						
							| 
									
										
										
										
											2024-04-18 13:42:51 +02:00
										 |  |  |           await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGKILL'); | 
					
						
							| 
									
										
										
										
											2021-08-17 18:19:52 +02:00
										 |  |  |         }, | 
					
						
							| 
									
										
										
										
											2024-04-18 13:42:51 +02:00
										 |  |  |         terminate: async () => { | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |           console.log(`Running tree kill with SIGTERM on process ${execChildProcess.pid}`); | 
					
						
							| 
									
										
										
										
											2024-04-18 13:42:51 +02:00
										 |  |  |           await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGTERM'); | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         keyboardInterrupt: async () => { | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |           console.log(`Running tree kill with SIGINT on process ${execChildProcess.pid}`); | 
					
						
							| 
									
										
										
										
											2024-04-18 13:42:51 +02:00
										 |  |  |           await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, 'SIGINT'); | 
					
						
							|  |  |  |         }, | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |         customSignal: async (signal: plugins.smartexit.TProcessSignal) => { | 
					
						
							|  |  |  |           console.log(`Running tree kill with custom signal ${signal} on process ${execChildProcess.pid}`); | 
					
						
							|  |  |  |           await plugins.smartexit.SmartExit.killTreeByPid(execChildProcess.pid, signal); | 
					
						
							| 
									
										
										
										
											2021-07-26 21:24:13 +02:00
										 |  |  |         }, | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |       } as IExecResultStreaming; | 
					
						
							| 
									
										
										
										
											2021-07-26 21:24:13 +02:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |     // For non-streaming mode, wait for the process to complete.
 | 
					
						
							|  |  |  |     return await childProcessEnded; | 
					
						
							| 
									
										
										
										
											2018-07-30 16:08:14 +02:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-22 11:51:44 +02:00
										 |  |  |   public async exec(commandString: string): Promise<IExecResult> { | 
					
						
							|  |  |  |     return (await this._exec({ commandString })) as IExecResult; | 
					
						
							| 
									
										
										
										
											2018-07-30 16:08:14 +02:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-22 11:51:44 +02:00
										 |  |  |   public async execSilent(commandString: string): Promise<IExecResult> { | 
					
						
							|  |  |  |     return (await this._exec({ commandString, silent: true })) as IExecResult; | 
					
						
							| 
									
										
										
										
											2018-07-30 16:08:14 +02:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2017-03-10 22:08:04 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-22 11:51:44 +02:00
										 |  |  |   public async execStrict(commandString: string): Promise<IExecResult> { | 
					
						
							|  |  |  |     return (await this._exec({ commandString, strict: true })) as IExecResult; | 
					
						
							| 
									
										
										
										
											2019-05-29 10:56:45 +02:00
										 |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-22 11:51:44 +02:00
										 |  |  |   public async execStrictSilent(commandString: string): Promise<IExecResult> { | 
					
						
							|  |  |  |     return (await this._exec({ commandString, silent: true, strict: true })) as IExecResult; | 
					
						
							| 
									
										
										
										
											2018-07-30 16:08:14 +02:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2017-03-11 02:36:27 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |   public async execStreaming(commandString: string, silent: boolean = false): Promise<IExecResultStreaming> { | 
					
						
							| 
									
										
										
										
											2023-06-22 11:51:44 +02:00
										 |  |  |     return (await this._exec({ commandString, silent, streaming: true })) as IExecResultStreaming; | 
					
						
							| 
									
										
										
										
											2018-07-30 16:08:14 +02:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-22 11:51:44 +02:00
										 |  |  |   public async execStreamingSilent(commandString: string): Promise<IExecResultStreaming> { | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |     return (await this._exec({ commandString, silent: true, streaming: true })) as IExecResultStreaming; | 
					
						
							| 
									
										
										
										
											2018-07-30 16:08:14 +02:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |   public async execInteractive(commandString: string): Promise<void> { | 
					
						
							| 
									
										
										
										
											2023-06-22 11:51:44 +02:00
										 |  |  |     await this._exec({ commandString, interactive: true }); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-22 14:16:16 +02:00
										 |  |  |   public async execAndWaitForLine( | 
					
						
							|  |  |  |     commandString: string, | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |     regex: RegExp, | 
					
						
							|  |  |  |     silent: boolean = false | 
					
						
							|  |  |  |   ): Promise<void> { | 
					
						
							|  |  |  |     const execStreamingResult = await this.execStreaming(commandString, silent); | 
					
						
							|  |  |  |     return new Promise<void>((resolve) => { | 
					
						
							|  |  |  |       execStreamingResult.childProcess.stdout.on('data', (chunk: Buffer | string) => { | 
					
						
							|  |  |  |         const data = typeof chunk === 'string' ? chunk : chunk.toString(); | 
					
						
							|  |  |  |         if (regex.test(data)) { | 
					
						
							|  |  |  |           resolve(); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |       }); | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  |     }); | 
					
						
							| 
									
										
										
										
											2018-07-30 16:08:14 +02:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2018-07-30 16:03:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-02-20 12:37:10 +01:00
										 |  |  |   public async execAndWaitForLineSilent(commandString: string, regex: RegExp): Promise<void> { | 
					
						
							|  |  |  |     return this.execAndWaitForLine(commandString, regex, true); | 
					
						
							| 
									
										
										
										
											2019-05-28 10:43:54 +02:00
										 |  |  |   } | 
					
						
							| 
									
										
										
										
											2024-09-17 17:02:42 +02:00
										 |  |  | } |