From 692032a125c578c2963553be525c8d30ce83c42e Mon Sep 17 00:00:00 2001 From: Or Geva Date: Wed, 29 Nov 2023 16:34:26 +0200 Subject: [PATCH] Remove timeout from advanced security scan (#444) * Refactor execute command cancellation * Handle errors better when killing child processes --- .../scanLogic/scanRunners/analyzerManager.ts | 24 ++--- .../scanRunners/applicabilityScan.ts | 2 +- src/main/scanLogic/scanRunners/iacScan.ts | 2 +- src/main/scanLogic/scanRunners/jasRunner.ts | 11 +-- src/main/scanLogic/scanRunners/sastScan.ts | 2 +- src/main/scanLogic/scanRunners/secretsScan.ts | 2 +- src/main/utils/resource.ts | 12 ++- src/main/utils/runUtils.ts | 49 ---------- src/main/utils/scanUtils.ts | 69 +++++++++++--- src/test/tests/runUtils.test.ts | 89 ------------------- src/test/tests/scanAnlayzerRunner.test.ts | 27 ++---- src/test/tests/scanUtils.test.ts | 88 ++++++++++++++++++ 12 files changed, 178 insertions(+), 199 deletions(-) delete mode 100644 src/test/tests/runUtils.test.ts create mode 100644 src/test/tests/scanUtils.test.ts diff --git a/src/main/scanLogic/scanRunners/analyzerManager.ts b/src/main/scanLogic/scanRunners/analyzerManager.ts index 001668899..e5cffbe71 100644 --- a/src/main/scanLogic/scanRunners/analyzerManager.ts +++ b/src/main/scanLogic/scanRunners/analyzerManager.ts @@ -8,7 +8,6 @@ import { IProxyConfig, JfrogClient } from 'jfrog-client-js'; import { ConnectionUtils } from '../../connect/connectionUtils'; import { Configuration } from '../../utils/configuration'; import { Translators } from '../../utils/translators'; -import { RunUtils } from '../../utils/runUtils'; /** * Analyzer manager is responsible for running the analyzer on the workspace. @@ -30,7 +29,6 @@ export class AnalyzerManager { AnalyzerManager.BINARY_NAME ); private static readonly JFROG_RELEASES_URL: string = 'https://releases.jfrog.io'; - public static readonly TIMEOUT_MILLISECS: number = 1000 * 60 * 5; public static readonly ENV_PLATFORM_URL: string = 'JF_PLATFORM_URL'; public static readonly ENV_TOKEN: string = 'JF_TOKEN'; public static readonly ENV_USER: string = 'JF_USER'; @@ -93,27 +91,15 @@ export class AnalyzerManager { return false; } - public async runWithTimeout(checkCancel: () => void, args: string[], executionLogDirectory?: string): Promise { - await AnalyzerManager.FINISH_UPDATE_PROMISE; - await RunUtils.runWithTimeout(AnalyzerManager.TIMEOUT_MILLISECS, checkCancel, { - title: this._binary.name, - task: this.run(args, executionLogDirectory) - }); - } - /** * Execute the cmd command to run the binary with given arguments - * @param args - the arguments for the command + * @param args - the arguments for the command + * @param checkCancel - A function that throws ScanCancellationError if the user chose to stop the scan * @param executionLogDirectory - the directory to save the execution log in */ - private async run(args: string[], executionLogDirectory?: string): Promise { - let std: any = await this._binary.run(args, this.createEnvForRun(executionLogDirectory)); - if (std.stdout && std.stdout.length > 0) { - this._logManager.logMessage('Done executing with log, log:\n' + std.stdout, 'DEBUG'); - } - if (std.stderr && std.stderr.length > 0) { - this._logManager.logMessage('Done executing with log, log:\n' + std.stderr, 'ERR'); - } + public async run(args: string[], checkCancel: () => void, executionLogDirectory?: string): Promise { + await AnalyzerManager.FINISH_UPDATE_PROMISE; + await this._binary.run(args, checkCancel, this.createEnvForRun(executionLogDirectory)); } /** diff --git a/src/main/scanLogic/scanRunners/applicabilityScan.ts b/src/main/scanLogic/scanRunners/applicabilityScan.ts index 4b80527d5..6959e2920 100644 --- a/src/main/scanLogic/scanRunners/applicabilityScan.ts +++ b/src/main/scanLogic/scanRunners/applicabilityScan.ts @@ -61,7 +61,7 @@ export class ApplicabilityRunner extends JasRunner { /** @override */ protected async runBinary(yamlConfigPath: string, executionLogDirectory: string | undefined, checkCancel: () => void): Promise { - await this.executeBinary(checkCancel, ['ca', yamlConfigPath], executionLogDirectory); + await this.runAnalyzerManager(checkCancel, ['ca', yamlConfigPath], executionLogDirectory); } /** @override */ diff --git a/src/main/scanLogic/scanRunners/iacScan.ts b/src/main/scanLogic/scanRunners/iacScan.ts index d9089d6b0..8c1296216 100644 --- a/src/main/scanLogic/scanRunners/iacScan.ts +++ b/src/main/scanLogic/scanRunners/iacScan.ts @@ -31,7 +31,7 @@ export class IacRunner extends JasRunner { /** @override */ protected async runBinary(yamlConfigPath: string, executionLogDirectory: string | undefined, checkCancel: () => void): Promise { - await this.executeBinary(checkCancel, ['iac', yamlConfigPath], executionLogDirectory); + await this.runAnalyzerManager(checkCancel, ['iac', yamlConfigPath], executionLogDirectory); } /** diff --git a/src/main/scanLogic/scanRunners/jasRunner.ts b/src/main/scanLogic/scanRunners/jasRunner.ts index e8dd9ed84..eeeac167e 100644 --- a/src/main/scanLogic/scanRunners/jasRunner.ts +++ b/src/main/scanLogic/scanRunners/jasRunner.ts @@ -87,13 +87,14 @@ export abstract class JasRunner { } /** - * Execute the cmd command to run the binary with given arguments and an option to abort the operation. - * @param checkCancel - Check if should cancel - * @param args - Arguments for the command + * Run Analyzer Manager with given arguments and an option to abort the operation. + * @param checkCancel - A function that throws ScanCancellationError if the user chose to stop the scan + * @param args - Arguments for the command * @param executionLogDirectory - Directory to save the execution log in */ - protected async executeBinary(checkCancel: () => void, args: string[], executionLogDirectory?: string): Promise { - await this._analyzerManager.runWithTimeout(checkCancel, args, executionLogDirectory); + protected async runAnalyzerManager(checkCancel: () => void, args: string[], executionLogDirectory?: string): Promise { + checkCancel(); + await this._analyzerManager.run(args, checkCancel, executionLogDirectory); } protected logStartScanning(request: AnalyzeScanRequest): void { diff --git a/src/main/scanLogic/scanRunners/sastScan.ts b/src/main/scanLogic/scanRunners/sastScan.ts index c39cd2dd3..28501dbdc 100644 --- a/src/main/scanLogic/scanRunners/sastScan.ts +++ b/src/main/scanLogic/scanRunners/sastScan.ts @@ -74,7 +74,7 @@ export class SastRunner extends JasRunner { checkCancel: () => void, responsePath: string ): Promise { - await this.executeBinary(checkCancel, ['zd', yamlConfigPath, responsePath], executionLogDirectory); + await this.runAnalyzerManager(checkCancel, ['zd', yamlConfigPath, responsePath], executionLogDirectory); } /** @override */ diff --git a/src/main/scanLogic/scanRunners/secretsScan.ts b/src/main/scanLogic/scanRunners/secretsScan.ts index a707c156c..69bf58062 100644 --- a/src/main/scanLogic/scanRunners/secretsScan.ts +++ b/src/main/scanLogic/scanRunners/secretsScan.ts @@ -31,7 +31,7 @@ export class SecretsRunner extends JasRunner { /** @override */ protected async runBinary(yamlConfigPath: string, executionLogDirectory: string | undefined, checkCancel: () => void): Promise { - await this.executeBinary(checkCancel, ['sec', yamlConfigPath], executionLogDirectory); + await this.runAnalyzerManager(checkCancel, ['sec', yamlConfigPath], executionLogDirectory); } /** diff --git a/src/main/utils/resource.ts b/src/main/utils/resource.ts index b9910274f..b8483b292 100644 --- a/src/main/utils/resource.ts +++ b/src/main/utils/resource.ts @@ -1,6 +1,5 @@ import * as fs from 'fs'; import * as path from 'path'; - import { IChecksumResult, JfrogClient } from 'jfrog-client-js'; import { LogManager } from '../log/logManager'; import { Utils } from './utils'; @@ -155,10 +154,17 @@ export class Resource { return ScanUtils.Hash('SHA256', fileBuffer); } - public async run(args: string[], env?: NodeJS.ProcessEnv | undefined): Promise { + public async run(args: string[], checkCancel: () => void, env?: NodeJS.ProcessEnv | undefined): Promise { let command: string = '"' + this.fullPath + '" ' + args.join(' '); this._logManager.debug("Executing '" + command + "' in directory '" + this._targetDir + "'"); - return await ScanUtils.executeCmdAsync(command, this._targetDir, env); + try { + const output: string = await ScanUtils.executeCmdAsync(command, checkCancel, this._targetDir, env); + if (output.length > 0) { + this._logManager.logMessage('Done executing "' + command + '" with output:\n' + output, 'DEBUG'); + } + } catch (error) { + throw new Error('Failed to execute "' + command + '" err: ' + error); + } } public get fullPath(): string { diff --git a/src/main/utils/runUtils.ts b/src/main/utils/runUtils.ts index 7b087dccb..a2717451a 100644 --- a/src/main/utils/runUtils.ts +++ b/src/main/utils/runUtils.ts @@ -1,53 +1,4 @@ -import { ScanTimeoutError } from './scanUtils'; - -export interface Task { - title: string; - task: Promise; -} - export class RunUtils { - // every 0.1 sec - private static readonly CHECK_INTERVAL_MILLISECS: number = 100; - - /** - * Creates a Promise that is resolved with an array of results when all of the provided Promises resolve, or rejected when: - * 1. Any Promise is rejected. - * 2. Cancel was requested. - * 3. Timeout reached. - * @param timeout - time in millisecs until execution timeout - * @param checkCancel - check if cancel was requested - * @param tasks - the promises that the new promise will wrap - * @returns Promise that wrap the given promises - */ - static async runWithTimeout(timeout: number, checkCancel: () => void, ...tasks: (Promise | Task)[]): Promise { - let results: T[] = []; - const wrappedTasks: Promise[] = []>tasks.map(async (task, index) => { - let result: T = await Promise.race([ - // Add task from argument - !(task instanceof Promise) && task.task ? task.task : task, - // Add task to check if cancel was requested from the user or reached timeout - this.checkCancelAndTimeoutTask(!(task instanceof Promise) && task.title ? task.title : '' + (index + 1), timeout, checkCancel) - ]); - results.push(result); - }); - await Promise.all(wrappedTasks); - return results; - } - - /** - * Async task that checks if an abort signal was given. - * If the active task is <= 0 the task is completed - * @param tasksBundle - an object that holds the information about the active async tasks count and the abort signal for them - */ - private static async checkCancelAndTimeoutTask(title: string, timeout: number, checkCancel: () => void): Promise { - let checkInterval: number = timeout < RunUtils.CHECK_INTERVAL_MILLISECS ? timeout : RunUtils.CHECK_INTERVAL_MILLISECS; - for (let elapsed: number = 0; elapsed < timeout; elapsed += checkInterval) { - checkCancel(); - await this.delay(checkInterval); - } - throw new ScanTimeoutError(title, timeout); - } - /** * Sleep and delay task for sleepIntervalMilliseconds * @param sleepIntervalMilliseconds - the amount of time in milliseconds to wait diff --git a/src/main/utils/scanUtils.ts b/src/main/utils/scanUtils.ts index 28be806a6..39a0c758b 100644 --- a/src/main/utils/scanUtils.ts +++ b/src/main/utils/scanUtils.ts @@ -5,7 +5,6 @@ import * as fse from 'fs-extra'; import * as os from 'os'; import * as path from 'path'; import * as tmp from 'tmp'; -import * as util from 'util'; import * as vscode from 'vscode'; import { ContextKeys } from '../constants/contextKeys'; import { LogManager } from '../log/logManager'; @@ -21,6 +20,8 @@ export class ScanUtils { public static readonly RESOURCES_DIR: string = ScanUtils.getResourcesDir(); public static readonly SPAWN_PROCESS_BUFFER_SIZE: number = 104857600; + // every 0.1 sec + private static readonly CANCELLATION_CHECK_INTERVAL_MS: number = 100; public static async scanWithProgress( scanCbk: (progress: vscode.Progress<{ message?: string; increment?: number }>, checkCanceled: () => void) => Promise, @@ -158,8 +159,62 @@ export class ScanUtils { return exec.execSync(command, { cwd: cwd, maxBuffer: ScanUtils.SPAWN_PROCESS_BUFFER_SIZE, env: env }); } - public static async executeCmdAsync(command: string, cwd?: string, env?: NodeJS.ProcessEnv | undefined): Promise { - return await util.promisify(exec.exec)(command, { cwd: cwd, maxBuffer: ScanUtils.SPAWN_PROCESS_BUFFER_SIZE, env: env }); + /** + * Executes a command asynchronously and returns the output. + * @param command - The command to execute. + * @param checkCancel - A function that throws ScanCancellationError if the user chose to stop the scan + * @param cwd - The current working directory for the command execution. + * @param env - Optional environment variables for the command execution. + * @returns Command output or rejects with an error. + */ + public static async executeCmdAsync( + command: string, + checkCancel: () => void, + cwd?: string, + env?: NodeJS.ProcessEnv | undefined + ): Promise { + return new Promise((resolve, reject) => { + try { + const childProcess: exec.ChildProcess = exec.exec( + command, + { cwd: cwd, maxBuffer: ScanUtils.SPAWN_PROCESS_BUFFER_SIZE, env: env } as exec.ExecOptions, + (error: exec.ExecException | null, stdout: string, stderr: string) => { + clearInterval(checkCancellationInterval); + if (error) { + reject(error); + } else { + stderr.trim() ? reject(new Error(stderr.trim())) : resolve(stdout.trim()); + } + } + ); + + const checkCancellationInterval: NodeJS.Timer = setInterval(() => { + ScanUtils.cancelIfRequested(childProcess, checkCancel, reject); + }, ScanUtils.CANCELLATION_CHECK_INTERVAL_MS); + } catch (error) { + reject(error); + } + }); + } + + /** + * Cancels the child process if cancellation is requested. + * @param childProcess - The child process to be cancelled. + * @param checkCancel - A function that throws ScanCancellationError if the user chose to stop the scan + * @param reject - A function to reject the promise. + */ + private static cancelIfRequested(childProcess: exec.ChildProcess, checkCancel: () => void, reject: (reason?: any) => void): void { + try { + checkCancel(); + } catch (error) { + const killSuccess: boolean = childProcess.kill('SIGTERM'); + if (!killSuccess) { + const originalError: string = error instanceof Error ? error.message : String(error); + reject(`${originalError}\nFailed to kill the process.`); + return; + } + reject(error); + } } public static setScanInProgress(state: boolean) { @@ -253,7 +308,7 @@ export class ScanUtils { logger.logMessage(error.message, 'DEBUG'); return undefined; } - if (handle || error instanceof ScanTimeoutError) { + if (handle) { logger.logError(error, true); return undefined; } @@ -301,9 +356,3 @@ export class FileScanError extends Error { export class ScanCancellationError extends Error { message: string = 'Scan was cancelled'; } - -export class ScanTimeoutError extends Error { - constructor(scan: string, public time_millisecs: number) { - super(`Task ${scan} timed out after ${time_millisecs}ms`); - } -} diff --git a/src/test/tests/runUtils.test.ts b/src/test/tests/runUtils.test.ts deleted file mode 100644 index 400bf0925..000000000 --- a/src/test/tests/runUtils.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { assert } from 'chai'; -import { RunUtils, Task } from '../../main/utils/runUtils'; -import { ScanCancellationError, ScanTimeoutError } from '../../main/utils/scanUtils'; - -describe('Run Utils tests', async () => { - [ - { - name: 'Task ended', - timeout: false, - shouldAbort: false, - addTitle: undefined, - expectedErr: undefined - }, - { - name: 'Task aborted', - timeout: false, - shouldAbort: true, - addTitle: undefined, - expectedErr: new ScanCancellationError() - }, - { - name: 'Task timeout with title', - timeout: true, - shouldAbort: false, - addTitle: "'TitledTask'", - expectedErr: new ScanTimeoutError("'TitledTask'", 100) - }, - { - name: 'Task timeout no title', - timeout: true, - shouldAbort: false, - addTitle: undefined, - expectedErr: new Error() - }, - { - name: 'Task throws error', - timeout: false, - shouldAbort: false, - addTitle: undefined, - expectedErr: new Error('general error') - } - ].forEach(async test => { - it('Run with abort controller - ' + test.name, async () => { - let tasks: (Promise | Task)[] = []; - let expectedResults: number[] = [0, 1, 2, 3]; - - for (let i: number = 0; i < expectedResults.length; i++) { - let task: Promise | Task = - test.name.includes('Task throws error') && i == expectedResults.length - 1 - ? RunUtils.delay(200).then(() => { - throw new Error('general error'); - }) - : RunUtils.delay((expectedResults.length - i) * 200).then(() => { - return i; - }); - tasks.push(test.addTitle ? ({ title: test.addTitle, task: task } as Task) : task); - } - - try { - let activeTasks: Promise = RunUtils.runWithTimeout( - test.timeout ? 100 : 1000, - () => { - if (test.shouldAbort) { - throw new ScanCancellationError(); - } - }, - ...tasks - ); - let actualResults: number[] = await activeTasks; - if (test.expectedErr) { - assert.fail('Expected run to throw error'); - } - // Make sure all tasks ended - assert.sameMembers(actualResults, expectedResults); - } catch (err) { - if (!test.expectedErr) { - assert.fail('Expected run not to throw error but got ' + err); - } - if (err instanceof Error) { - if (test.timeout && !test.addTitle) { - assert.include(err.message, 'timed out after'); - } else { - assert.equal(err.message, test.expectedErr.message); - } - } - } - }); - }); -}); diff --git a/src/test/tests/scanAnlayzerRunner.test.ts b/src/test/tests/scanAnlayzerRunner.test.ts index f76c95a7b..cad2f113f 100644 --- a/src/test/tests/scanAnlayzerRunner.test.ts +++ b/src/test/tests/scanAnlayzerRunner.test.ts @@ -8,8 +8,7 @@ import { LogManager } from '../../main/log/logManager'; import { AnalyzeScanRequest, AnalyzerScanRun, ScanType } from '../../main/scanLogic/scanRunners/analyzerModels'; import { JasRunner } from '../../main/scanLogic/scanRunners/jasRunner'; import { AppsConfigModule } from '../../main/utils/jfrogAppsConfig/jfrogAppsConfig'; -import { RunUtils } from '../../main/utils/runUtils'; -import { NotEntitledError, ScanCancellationError, ScanTimeoutError, ScanUtils } from '../../main/utils/scanUtils'; +import { NotEntitledError, ScanCancellationError, ScanUtils } from '../../main/utils/scanUtils'; import { Translators } from '../../main/utils/translators'; import { AnalyzerManager } from '../../main/scanLogic/scanRunners/analyzerManager'; @@ -41,7 +40,6 @@ describe('Analyzer BinaryRunner tests', async () => { function createDummyBinaryRunner( connection: ConnectionManager = connectionManager, - timeout: number = AnalyzerManager.TIMEOUT_MILLISECS, dummyAction: () => Promise = () => Promise.resolve() ): JasRunner { return new (class extends JasRunner { @@ -57,14 +55,14 @@ describe('Analyzer BinaryRunner tests', async () => { _executionLogDirectory: string | undefined, checkCancel: () => void ): Promise { - await RunUtils.runWithTimeout(timeout, checkCancel, dummyAction()); + checkCancel(); + await dummyAction(); } })(connection, dummyName, logManager, new AppsConfigModule(''), {} as AnalyzerManager); } function createDummyAnalyzerManager( connection: ConnectionManager = connectionManager, - timeout: number = AnalyzerManager.TIMEOUT_MILLISECS, dummyAction: () => Promise = () => Promise.resolve() ): AnalyzerManager { return new (class extends AnalyzerManager { @@ -80,7 +78,8 @@ describe('Analyzer BinaryRunner tests', async () => { _executionLogDirectory: string | undefined, checkCancel: () => void ): Promise { - await RunUtils.runWithTimeout(timeout, checkCancel, dummyAction()); + checkCancel(); + await dummyAction(); } })(connection, logManager); } @@ -196,35 +195,25 @@ describe('Analyzer BinaryRunner tests', async () => { [ { name: 'Run valid request', - timeout: AnalyzerManager.TIMEOUT_MILLISECS, createDummyResponse: true, shouldAbort: false, expectedErr: undefined }, { name: 'Not entitled', - timeout: AnalyzerManager.TIMEOUT_MILLISECS, createDummyResponse: true, shouldAbort: false, expectedErr: new NotEntitledError() }, { name: 'Cancel requested', - timeout: AnalyzerManager.TIMEOUT_MILLISECS, createDummyResponse: true, shouldAbort: true, expectedErr: new ScanCancellationError() }, - { - name: 'Timeout', - timeout: 1, - createDummyResponse: true, - shouldAbort: false, - expectedErr: new ScanTimeoutError('' + 1, 1) - }, + { name: 'Response not created', - timeout: AnalyzerManager.TIMEOUT_MILLISECS, createDummyResponse: false, shouldAbort: false, expectedErr: new Error( @@ -237,13 +226,11 @@ describe('Analyzer BinaryRunner tests', async () => { let requestPath: string = path.join(tempFolder, 'request'); let responsePath: string = path.join(tempFolder, 'response'); - let runner: JasRunner = createDummyBinaryRunner(connectionManager, test.timeout, async () => { + let runner: JasRunner = createDummyBinaryRunner(connectionManager, async () => { if (test.shouldAbort) { throw new ScanCancellationError(); } else if (test.name === 'Not entitled') { throw new DummyRunnerError(); - } else if (test.name === 'Timeout') { - await RunUtils.delay(AnalyzerManager.TIMEOUT_MILLISECS); } if (test.createDummyResponse) { fs.writeFileSync(responsePath, JSON.stringify({} as AnalyzerScanRun)); diff --git a/src/test/tests/scanUtils.test.ts b/src/test/tests/scanUtils.test.ts new file mode 100644 index 000000000..a37e3c7e5 --- /dev/null +++ b/src/test/tests/scanUtils.test.ts @@ -0,0 +1,88 @@ +import { assert, expect } from 'chai'; +import { ScanUtils } from '../../main/utils/scanUtils'; +import sinon from 'sinon'; +import * as fs from 'fs'; +import * as path from 'path'; +import { isWindows } from './utils/utils.test'; + +describe('ScanUtils', () => { + describe('executeCmdAsync', () => { + let clearIntervalSpy: sinon.SinonSpy; + beforeEach(() => { + clearIntervalSpy = sinon.spy(global, 'clearInterval'); + }); + + afterEach(() => { + clearIntervalSpy.restore(); + }); + + const dummyCheckError: () => void = () => { + return; + }; + + it('execute a command successfully', async () => { + const command: string = 'echo Hello, World!'; + const output: string = await ScanUtils.executeCmdAsync(command, () => dummyCheckError, undefined, undefined); + assert.isTrue(clearIntervalSpy.calledOnce); + assert.equal(output, 'Hello, World!'); + }); + + it('reject with an error if the command fails', async () => { + const command: string = 'invalid_command'; + try { + await ScanUtils.executeCmdAsync(command, dummyCheckError, undefined, undefined); + // If the above line doesn't throw an error, the test should fail + expect.fail('The command should have failed.'); + } catch (error) { + assert.instanceOf(error, Error); + assert.isTrue(clearIntervalSpy.calledOnce); + } + }); + + it('reject with a cancellation error if canceled', async () => { + const cancelSignal: () => void = () => { + throw new Error('Cancellation requested.'); + }; + + try { + await ScanUtils.executeCmdAsync('sleep 2', cancelSignal, undefined, undefined); + // If the above line doesn't throw an error, the test should fail + expect.fail('The command should have been canceled.'); + } catch (error) { + if (error instanceof Error) { + assert.equal(error.message, 'Cancellation requested.'); + } else { + assert.fail('The error should have been an instance of Error.'); + } + } + }); + + it('call childProcess.kill when cancellation is requested', async () => { + const cancelSignal: () => void = () => { + throw new Error('Cancellation requested.'); + }; + const randomFileName: string = `file_${Date.now()}.txt`; + + // Define the command that waits for 2 seconds and writes a file + const command: string = isWindows() ? `ping 127.0.0.1 -n 2 & echo > ${randomFileName}` : `sleep 2 && touch ${randomFileName}`; + + try { + await ScanUtils.executeCmdAsync(command, cancelSignal, __dirname, undefined); + expect.fail('The command should have been canceled.'); + } catch (error) { + if (error instanceof Error) { + assert.equal(error.message, 'Cancellation requested.'); + + // Wait for 3 seconds to ensure the file is not created + await new Promise(resolve => setTimeout(resolve, 3000)); + + // Check if the file 'output.txt' does not exist (indicating it wasn't written due to process kill) + assert.isFalse(fs.existsSync(path.join(__dirname, randomFileName))); + } else { + // Fail the test if the error is not an instance of Error + expect.fail('The error should have been an instance of Error.'); + } + } + }); + }); +});