diff --git a/src/plugins/aws/ssm/index.ts b/src/plugins/aws/ssm/index.ts index 37f8392..1b1b885 100644 --- a/src/plugins/aws/ssm/index.ts +++ b/src/plugins/aws/ssm/index.ts @@ -158,21 +158,17 @@ const createSsmCommands = ( ]; }; -type SpawnCommandOptions = { - credentials: AwsCredentials; - command: string[]; - attemptsRemaining?: number; - controller: AbortController; - detached?: boolean; -}; - /** Starts an SSM session in the terminal by spawning `aws ssm` as a subprocess * * Requires `aws ssm` to be installed on the client machine. */ -const spawnSsmNode = async ( - options: SpawnCommandOptions -): Promise => +const spawnSsmNode = async (options: { + credentials: AwsCredentials; + command: string[]; + attemptsRemaining?: number; + abortController: AbortController; + detached?: boolean; +}): Promise => new Promise((resolve, reject) => { const child = spawn("/usr/bin/env", options.command, { env: { @@ -187,7 +183,10 @@ const spawnSsmNode = async ( const { isAccessPropagated } = accessPropagationGuard(child); const abortListener = (code: any) => { - options.controller.signal.removeEventListener("abort", abortListener); + options.abortController.signal.removeEventListener( + "abort", + abortListener + ); if (options.detached) { // child.kill() does not kill the entire process group, only the child process. If the child process has descendants, they will not be relinquished properly. The solution to this is to use a detached process and kill the entire process group when the parent process is killed. Consequently, we also need to intercept the SIGINT signal and send a kill signal using an AbortController to ensure all detached processes are killed when the parent process is killed. process.kill(-child.pid!); @@ -196,7 +195,7 @@ const spawnSsmNode = async ( }; child.on("spawn", () => { - options.controller.signal.addEventListener("abort", abortListener); + options.abortController.signal.addEventListener("abort", abortListener); }); const exitListener = child.on("exit", (code) => { @@ -205,7 +204,10 @@ const spawnSsmNode = async ( // In the case of ephemeral AccessDenied exceptions due to unpropagated // permissions, continually retry access until success if (!isAccessPropagated()) { - options.controller.signal.removeEventListener("abort", abortListener); + options.abortController.signal.removeEventListener( + "abort", + abortListener + ); const attemptsRemaining = options?.attemptsRemaining ?? MAX_SSM_RETRIES; if (attemptsRemaining <= 0) { @@ -224,7 +226,7 @@ const spawnSsmNode = async ( return; } - options.controller.abort(code); + options.abortController.abort(code); }); }); @@ -272,18 +274,16 @@ export const ssm = async ( }; /** The AbortController is responsible for sending a shared signal to all spawned processes {@link spawnSsmNode} when the parent process is terminated unexpectedly. This is necessary because the spawned processes are detached and would otherwise continue running after the parent process is terminated. */ - const controller = new AbortController(); + const abortController = new AbortController(); process.on("SIGINT", () => { - controller.abort("SIGINT"); + abortController.abort("SIGINT"); }); - const commands = createSsmCommands(ssmArgs); - await Promise.all( - commands.map(({ command, detached }) => + createSsmCommands(ssmArgs).map(({ command, detached }) => spawnSsmNode({ credentials, - controller, + abortController, detached, command, })