From 75047f52be07e4d617a800fe5a101636625caa49 Mon Sep 17 00:00:00 2001 From: Eric <5089238+emizzle@users.noreply.github.com> Date: Mon, 24 Mar 2025 19:39:38 +1100 Subject: [PATCH] force kill windows test processes In situations like timeouts, windows will hang when attempting to close the test process streams. In this case, we have to force kill the test process externally. This is the same process as force killing hardhat nodes after they are already terminated, but windows refuses hangs when closing their process streams. This commit creates a forceKillProcess utility that allows a process to be killed by its process name and matching commandline criteria, like TestId (for test process) or --port (for hardhat) --- tests/integration/hardhatprocess.nim | 17 +-- tests/integration/scripts/winkillhardhat.sh | 110 -------------------- tests/integration/scripts/winkillprocess.sh | 97 +++++++++++++++++ tests/integration/testmanager.nim | 37 ++++++- tests/integration/utils.nim | 16 +++ 5 files changed, 149 insertions(+), 128 deletions(-) delete mode 100644 tests/integration/scripts/winkillhardhat.sh create mode 100644 tests/integration/scripts/winkillprocess.sh diff --git a/tests/integration/hardhatprocess.nim b/tests/integration/hardhatprocess.nim index 82d34fce..b00b0400 100644 --- a/tests/integration/hardhatprocess.nim +++ b/tests/integration/hardhatprocess.nim @@ -14,6 +14,7 @@ import pkg/codex/conf import pkg/codex/utils/trackedfutures import ./codexclient import ./nodeprocess +import ./utils export codexclient export chronicles @@ -159,20 +160,6 @@ method onOutputLineCaptured(node: HardhatProcess, line: string) = discard logFile.closeFile() node.logFile = none IoHandle -proc killHardhatByPort( - port: int -): Future[CommandExResponse] {. - async: ( - raises: [ - AsyncProcessError, AsyncProcessTimeoutError, CancelledError, ValueError, OSError - ] - ) -.} = - let path = splitFile(currentSourcePath()).dir / "scripts" / "winkillhardhat.sh" - let cmd = &"{absolutePath(path)} killvendorport {port}" - trace "Forcefully killing windows hardhat process", port, cmd - return await execCommandEx(cmd, timeout = 5.seconds) - proc closeProcessStreams(node: HardhatProcess) {.async: (raises: []).} = when not defined(windows): if not node.process.isNil: @@ -186,7 +173,7 @@ proc closeProcessStreams(node: HardhatProcess) {.async: (raises: []).} = error "Failed to get port from Hardhat args" return try: - let cmdResult = await killHardhatByPort(port) + let cmdResult = await forceKillProcess("node.exe", &"--port {port}") if cmdResult.status > 0: error "Failed to forcefully kill windows hardhat process", port, exitCode = cmdResult.status, stderr = cmdResult.stdError diff --git a/tests/integration/scripts/winkillhardhat.sh b/tests/integration/scripts/winkillhardhat.sh deleted file mode 100644 index 32730d7d..00000000 --- a/tests/integration/scripts/winkillhardhat.sh +++ /dev/null @@ -1,110 +0,0 @@ -#!/bin/bash - -# List all node.exe processes with command line arguments -list_node_processes() { - powershell.exe -Command "Get-WmiObject Win32_Process -Filter \"name = 'node.exe'\" | Select-Object ProcessId, Name, CommandLine | Format-Table -AutoSize" -} - -# Find node processes containing vendor\codex-contracts-eth in command line -find_vendor_node_processes() { - echo "Looking for node processes with 'vendor\\codex-contracts-eth' in command line..." - powershell.exe -Command "Get-WmiObject Win32_Process -Filter \"name = 'node.exe'\" | Where-Object { \$_.CommandLine -match 'vendor\\\\codex-contracts-eth' } | Select-Object ProcessId, Name, CommandLine | Format-Table -AutoSize" -} - -# Find node processes running on a specific port -find_node_by_port() { - local port=$1 - echo "Looking for node processes running on port $port..." - powershell.exe -Command "Get-WmiObject Win32_Process -Filter \"name = 'node.exe'\" | Where-Object { \$_.CommandLine -match '--port $port' } | Select-Object ProcessId, Name, CommandLine | Format-Table -AutoSize" -} - -# Kill all node.exe processes containing vendor\codex-contracts-eth -kill_vendor_node_processes() { - echo "Finding and killing node.exe processes containing 'vendor\\codex-contracts-eth'..." - powershell.exe -Command " - \$processes = Get-WmiObject Win32_Process -Filter \"name = 'node.exe'\" | Where-Object { \$_.CommandLine -match 'vendor\\\\codex-contracts-eth' }; - if (\$processes) { - foreach (\$process in \$processes) { - Stop-Process -Id \$process.ProcessId -Force; - Write-Host \"Killed process \$(\$process.ProcessId)\"; - } - } else { - Write-Host \"No matching node.exe processes found\"; - } - " -} - -# Kill node.exe process running on a specific port -kill_node_by_port() { - local port=$1 - echo "Finding and killing node.exe process running on port $port..." - powershell.exe -Command " - \$processes = Get-WmiObject Win32_Process -Filter \"name = 'node.exe'\" | Where-Object { \$_.CommandLine -match '--port $port' }; - if (\$processes) { - foreach (\$process in \$processes) { - Stop-Process -Id \$process.ProcessId -Force; - Write-Host \"Killed process \$(\$process.ProcessId) running on port $port\"; - } - } else { - Write-Host \"No node.exe process found running on port $port\"; - } - " -} - -# Kill node.exe process with both vendor string and specific port -kill_vendor_node_by_port() { - local port=$1 - echo "Finding and killing node.exe process with 'vendor\\codex-contracts-eth' running on port $port..." - powershell.exe -Command " - \$processes = Get-WmiObject Win32_Process -Filter \"name = 'node.exe'\" | Where-Object { \$_.CommandLine -match 'vendor\\\\codex-contracts-eth' -and \$_.CommandLine -match '--port $port' }; - if (\$processes) { - foreach (\$process in \$processes) { - Stop-Process -Id \$process.ProcessId -Force; - Write-Host \"Killed process \$(\$process.ProcessId) running on port $port\"; - } - } else { - Write-Host \"No matching node.exe process found running on port $port\"; - } - " -} - -# Check if being run directly or sourced -if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then - # If run directly (not sourced), provide command line interface - case "$1" in - list) - list_node_processes - ;; - find) - find_vendor_node_processes - ;; - findport) - if [ -z "$2" ]; then - echo "Usage: $0 findport PORT_NUMBER" - exit 1 - fi - find_node_by_port "$2" - ;; - killall) - kill_vendor_node_processes - ;; - killport) - if [ -z "$2" ]; then - echo "Usage: $0 killport PORT_NUMBER" - exit 1 - fi - kill_node_by_port "$2" - ;; - killvendorport) - if [ -z "$2" ]; then - echo "Usage: $0 killvendorport PORT_NUMBER" - exit 1 - fi - kill_vendor_node_by_port "$2" - ;; - *) - echo "Usage: $0 {list|find|findport PORT|killall|killport PORT|killvendorport PORT}" - exit 1 - ;; - esac -fi \ No newline at end of file diff --git a/tests/integration/scripts/winkillprocess.sh b/tests/integration/scripts/winkillprocess.sh new file mode 100644 index 00000000..b5e58ab4 --- /dev/null +++ b/tests/integration/scripts/winkillprocess.sh @@ -0,0 +1,97 @@ +#!/bin/bash + +# List all processes with a specific name +list() { + local name=$1 + echo "Listing all processes named '$name'..." + powershell.exe -Command "Get-CimInstance Win32_Process -Filter \"name = '$name'\" | Select-Object ProcessId, Name, CommandLine | Format-Table -AutoSize" +} + +# Search for processes with a specific name and command line pattern +search() { + local name=$1 + local pattern=$2 + echo "Searching for '$name' processes with command line matching '$pattern'..." + powershell.exe -Command " + \$processes = Get-CimInstance Win32_Process -Filter \"name = '$name'\" | Where-Object { \$_.CommandLine -match '$pattern' }; + if (\$processes) { + \$processes | Select-Object ProcessId, Name, CommandLine | Format-Table -AutoSize; + } else { + Write-Host \"No matching '$name' processes found\"; + } + " +} + +# Kill all processes with a specific name +killall() { + local name=$1 + echo "Finding and killing all '$name' processes..." + powershell.exe -Command " + \$processes = Get-CimInstance Win32_Process -Filter \"name = '$name'\"; + if (\$processes) { + foreach (\$process in \$processes) { + Stop-Process -Id \$process.ProcessId -Force; + Write-Host \"Killed process \$(\$process.ProcessId)\"; + } + } else { + Write-Host \"No '$name' processes found\"; + } + " +} + +# Kill processes with a specific name and command line pattern +kill() { + local name=$1 + local pattern=$2 + echo "Finding and killing '$name' processes with command line matching '$pattern'..." + powershell.exe -Command " + \$processes = Get-CimInstance Win32_Process -Filter \"name = '$name'\" | Where-Object { \$_.CommandLine -match '$pattern' }; + if (\$processes) { + foreach (\$process in \$processes) { + Stop-Process -Id \$process.ProcessId -Force; + Write-Host \"Killed process \$(\$process.ProcessId)\"; + } + } else { + Write-Host \"No matching '$name' processes found\"; + } + " +} + +# Check if being run directly or sourced +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + # If run directly (not sourced), provide command line interface + case "$1" in + list) + if [ -z "$2" ]; then + echo "Usage: $0 list PROCESS_NAME" + exit 1 + fi + list "$2" + ;; + search) + if [ -z "$2" ] || [ -z "$3" ]; then + echo "Usage: $0 search PROCESS_NAME COMMANDLINE_PATTERN" + exit 1 + fi + search "$2" "$3" + ;; + killall) + if [ -z "$2" ]; then + echo "Usage: $0 killall PROCESS_NAME" + exit 1 + fi + killall "$2" + ;; + kill) + if [ -z "$2" ] || [ -z "$3" ]; then + echo "Usage: $0 kill PROCESS_NAME COMMANDLINE_PATTERN" + exit 1 + fi + kill "$2" "$3" + ;; + *) + echo "Usage: $0 {list PROCESS_NAME|search PROCESS_NAME COMMANDLINE_PATTERN|killall PROCESS_NAME|kill PROCESS_NAME COMMANDLINE_PATTERN}" + exit 1 + ;; + esac +fi diff --git a/tests/integration/testmanager.nim b/tests/integration/testmanager.nim index 6b10e94a..a41935a2 100644 --- a/tests/integration/testmanager.nim +++ b/tests/integration/testmanager.nim @@ -3,7 +3,6 @@ import std/strformat import std/terminal from std/times import fromUnix, format, now from std/unicode import toUpper -import std/unittest import pkg/chronos import pkg/chronos/asyncproc import pkg/codex/conf @@ -389,6 +388,38 @@ proc teardownHardhat(test: IntegrationTest, hardhat: Hardhat) {.async: (raises: test.manager.hardhats.keepItIf(it != hardhat) +proc closeProcessStreams(test: IntegrationTest) {.async: (raises: []).} = + logScope: + name = test.config.name + + when not defined(windows): + if not test.process.isNil: + trace "Closing test process' streams" + await test.process.closeWait() + trace "Test process' streams closed" + else: + # Windows hangs when attempting to close the test's process streams, so try + # to kill the process externally. + try: + let cmdResult = await test.forceKillProcess("nim.exe", &"-d:TestId {test.testId}") + if cmdResult.status > 0: + error "Failed to forcefully kill windows test process", + port, exitCode = cmdResult.status, stderr = cmdResult.stdError + else: + trace "Successfully killed windows test process by force", + port, exitCode = cmdResult.status, stdout = cmdResult.stdOutput + except ValueError, OSError: + let eMsg = getCurrentExceptionMsg() + error "Failed to forcefully kill windows test process, bad path to command", + error = eMsg + except CancelledError as e: + discard + except AsyncProcessError as e: + error "Failed to forcefully kill windows test process", port, error = e.msg + except AsyncProcessTimeoutError as e: + error "Timeout while forcefully killing windows test process", + port, error = e.msg + proc teardownTest(test: IntegrationTest) {.async: (raises: []).} = logScope: test = test.config.name @@ -409,8 +440,8 @@ proc teardownTest(test: IntegrationTest) {.async: (raises: []).} = let e = getCurrentException() warn "Test process failed to terminate, check for zombies", error = e.msg - await test.process.closeWait() - trace "Test process output streams closed" + await test.closeProcessStreams() + test.process = nil proc teardown(test: IntegrationTest, hardhat: ?Hardhat) {.async: (raises: []).} = if test.config.startHardhat and hardhat =? hardhat and not hardhat.process.isNil: diff --git a/tests/integration/utils.nim b/tests/integration/utils.nim index 4ce8fcda..974ec7d6 100644 --- a/tests/integration/utils.nim +++ b/tests/integration/utils.nim @@ -1,5 +1,6 @@ import std/os import pkg/chronos +import pkg/chronos/asyncproc import pkg/codex/logutils {.push raises: [].} @@ -64,3 +65,18 @@ proc appendFile*(filename: string, content: string) {.raises: [IOError].} = raise newException(IOError, "cannot open and write " & filename & ": " & e.msg) finally: close(f) + +when defined(windows): + proc forceKillProcess( + processName, matchingCriteria: string + ): Future[CommandExResponse] {. + async: ( + raises: [ + AsyncProcessError, AsyncProcessTimeoutError, CancelledError, ValueError, OSError + ] + ) + .} = + let path = splitFile(currentSourcePath()).dir / "scripts" / "winkillprocess.sh" + let cmd = &"{absolutePath(path)} kill {processName} \"{matchingCriteria}\"" + trace "Forcefully killing windows process", processName, matchingCriteria, cmd + return await execCommandEx(cmd, timeout = 5.seconds)