in server/aws-lsp-codewhisperer/src/language-server/agenticChat/tools/executeBash.ts [311:494]
public async invoke(
params: ExecuteBashParams,
cancellationToken?: CancellationToken,
updates?: WritableStream
): Promise<InvokeOutput> {
const { shellName, shellFlag } = IS_WINDOWS_PLATFORM
? { shellName: 'cmd.exe', shellFlag: '/c' }
: { shellName: 'bash', shellFlag: '-c' }
this.logging.info(`Invoking ${shellName} command: "${params.command}" in cwd: "${params.cwd}"`)
return new Promise(async (resolve, reject) => {
let finished = false
const abort = (err: Error) => {
if (!finished) {
finished = true
reject(err) // <─ propagate the error to caller
}
}
// Check if cancelled before starting
if (cancellationToken?.isCancellationRequested) {
this.logging.debug('Command execution cancelled before starting')
return abort(new CancellationError('user'))
}
this.logging.debug(
`Spawning process with command: ${shellName} ${shellFlag} "${params.command}" (cwd=${params.cwd})`
)
const stdoutBuffer: string[] = []
const stderrBuffer: string[] = []
// Use a closure boolean value firstChunk and a function to get and set its value
let isFirstChunk = true
const getAndSetFirstChunk = (newValue: boolean): boolean => {
const oldValue = isFirstChunk
isFirstChunk = newValue
return oldValue
}
// Use a queue to maintain chronological order of chunks
// This ensures that the output is processed in the exact order it was generated by the child process.
const outputQueue: TimestampedChunk[] = []
let processingQueue = false
const writer = updates?.getWriter()
// Process the queue in order
const processQueue = () => {
if (processingQueue || outputQueue.length === 0) {
return
}
processingQueue = true
try {
// Sort by timestamp to ensure chronological order
outputQueue.sort((a, b) => a.timestamp - b.timestamp)
while (outputQueue.length > 0) {
const chunk = outputQueue.shift()!
ExecuteBash.handleTimestampedChunk(chunk, stdoutBuffer, stderrBuffer, writer)
}
} finally {
processingQueue = false
}
}
const childProcessOptions: ChildProcessOptions = {
spawnOptions: {
cwd: params.cwd,
stdio: ['pipe', 'pipe', 'pipe'],
},
collect: false,
waitForStreams: true,
onStdout: async (chunk: string) => {
if (cancellationToken?.isCancellationRequested) {
this.logging.debug('Command execution cancelled during stdout processing')
return abort(new CancellationError('user'))
}
const isFirst = getAndSetFirstChunk(false)
const timestamp = Date.now()
outputQueue.push({
timestamp,
isStdout: true,
content: chunk,
isFirst,
})
processQueue()
},
onStderr: async (chunk: string) => {
if (cancellationToken?.isCancellationRequested) {
this.logging.debug('Command execution cancelled during stderr processing')
return abort(new CancellationError('user'))
}
const isFirst = getAndSetFirstChunk(false)
const timestamp = Date.now()
outputQueue.push({
timestamp,
isStdout: false,
content: chunk,
isFirst,
})
processQueue()
},
}
this.childProcess = new ChildProcess(
this.logging,
shellName,
[shellFlag, params.command],
childProcessOptions
)
// Set up cancellation listener
if (cancellationToken) {
cancellationToken.onCancellationRequested(() => {
this.logging.debug('cancellation detected, killing child process')
// Kill the process
this.childProcess?.stop(false, 'SIGTERM')
// After a short delay, force kill with SIGKILL if still running
setTimeout(() => {
if (this.childProcess && !this.childProcess.stopped) {
this.logging.debug('Process still running after SIGTERM, sending SIGKILL')
// Try to kill the process group with SIGKILL
this.childProcess.stop(true, 'SIGKILL')
}
}, 500)
// Return from the function after cancellation
return abort(new CancellationError('user'))
})
}
try {
const result = await this.childProcess.run()
// Check if cancelled after execution
if (cancellationToken?.isCancellationRequested) {
this.logging.debug('Command execution cancelled after completion')
return abort(new CancellationError('user'))
}
const exitStatus = result.exitCode ?? 0
const stdout = stdoutBuffer.join('\n')
const stderr = stderrBuffer.join('\n')
const success = exitStatus === 0 && !stderr
const [stdoutTrunc, stdoutSuffix] = ExecuteBash.truncateSafelyWithSuffix(
stdout,
maxToolResponseSize / 3
)
const [stderrTrunc, stderrSuffix] = ExecuteBash.truncateSafelyWithSuffix(
stderr,
maxToolResponseSize / 3
)
const outputJson: ExecuteBashOutput = {
exitStatus: exitStatus.toString(),
stdout: stdoutTrunc + (stdoutSuffix ? ' ... truncated' : ''),
stderr: stderrTrunc + (stderrSuffix ? ' ... truncated' : ''),
}
resolve({
output: {
kind: 'json',
content: outputJson,
success,
},
})
} catch (err: any) {
// Check if this was due to cancellation
if (cancellationToken?.isCancellationRequested) {
return abort(new CancellationError('user'))
} else {
this.logging.error(`Failed to execute ${shellName} command '${params.command}': ${err.message}`)
reject(new Error(`Failed to execute command: ${err.message}`))
}
} finally {
await writer?.close()
writer?.releaseLock()
}
})
}