logos-storage-nim/tests/integration/hardhatprocess.nim
Eric e3d0304ef7
fix: forcefully kill windows hardhat processes after termination
On windows, termination of hardhat processes would not actually kill the process, and then closing the process' streams would then hang the calling nim process. To get around this, the process is now killed externally using a script, winkillhardhat.sh. This script first queries open processes by inspecting the command line value of all "node.exe" processes, searching for "vendor/codex-contracts-eth" and for the port parameter it was started with. After querying, the process is killed using the `Stop-Process` powershell command (passing the pid of the windows process).
2025-06-04 16:00:51 +10:00

222 lines
6.5 KiB
Nim

import pkg/questionable
import pkg/questionable/results
import pkg/confutils
import pkg/chronicles
import pkg/chronos
import pkg/chronos/asyncproc
import pkg/stew/io2
import std/os
import std/sets
import std/sequtils
import std/strformat
import std/strutils
import pkg/codex/conf
import pkg/codex/utils/trackedfutures
import ./codexclient
import ./nodeprocess
export codexclient
export chronicles
export nodeprocess
{.push raises: [].}
logScope:
topics = "integration testing hardhat process"
type
OnOutputLineCaptured = proc(line: string) {.gcsafe, raises: [].}
HardhatProcess* = ref object of NodeProcess
logFile: ?IoHandle
onOutputLine: OnOutputLineCaptured
HardhatProcessError* = object of NodeProcessError
method workingDir(node: HardhatProcess): string =
return currentSourcePath() / ".." / ".." / ".." / "vendor" / "codex-contracts-eth"
method executable(node: HardhatProcess): string =
return
"node_modules" / ".bin" / (when defined(windows): "hardhat.cmd" else: "hardhat")
method startedOutput(node: HardhatProcess): string =
return "Started HTTP and WebSocket JSON-RPC server at"
method processOptions(node: HardhatProcess): set[AsyncProcessOption] =
return {}
method outputLineEndings(node: HardhatProcess): string =
return "\n"
proc openLogFile(node: HardhatProcess, logFilePath: string): IoHandle =
let logFileHandle =
openFile(logFilePath, {OpenFlags.Write, OpenFlags.Create, OpenFlags.Truncate})
without fileHandle =? logFileHandle:
fatal "failed to open log file",
path = logFilePath, errorCode = $logFileHandle.error
raiseAssert "failed to open log file, aborting"
return fileHandle
method start*(
node: HardhatProcess
) {.async: (raises: [CancelledError, NodeProcessError]).} =
logScope:
nodeName = node.name
var executable = ""
try:
executable = absolutePath(node.workingDir / node.executable)
if not fileExists(executable):
raiseAssert "cannot start hardhat, executable doesn't exist (looking for " &
&"{executable}). Try running `npm install` in {node.workingDir}."
except CatchableError as parent:
raiseAssert "failed build path to hardhat executable: " & parent.msg
let poptions = node.processOptions + {AsyncProcessOption.StdErrToStdOut}
let args = @["node", "--export", "deployment-localhost.json"].concat(node.arguments)
trace "starting node", args, executable, workingDir = node.workingDir
try:
node.process = await startProcess(
executable,
node.workingDir,
args,
options = poptions,
stdoutHandle = AsyncProcess.Pipe,
)
except CancelledError as error:
raise error
except CatchableError as parent:
raise newException(
HardhatProcessError, "failed to start hardhat process: " & parent.msg, parent
)
proc port(node: HardhatProcess): ?int =
var next = false
for arg in node.arguments:
# TODO: move to constructor
if next:
return parseInt(arg).catch.option
if arg.contains "--port":
next = true
return none int
proc startNode*(
_: type HardhatProcess,
args: seq[string],
debug: string | bool = false,
name: string,
onOutputLineCaptured: OnOutputLineCaptured = nil,
): Future[HardhatProcess] {.async: (raises: [CancelledError, NodeProcessError]).} =
logScope:
nodeName = name
var logFilePath = ""
var arguments = newSeq[string]()
for arg in args:
# TODO: move to constructor
if arg.contains "--log-file=":
logFilePath = arg.split("=")[1]
else:
arguments.add arg
trace "starting hardhat node", arguments
## Starts a Hardhat Node with the specified arguments.
## Set debug to 'true' to see output of the node.
let hardhat = HardhatProcess(
arguments: arguments,
debug: ($debug != "false"),
trackedFutures: TrackedFutures.new(),
name: name,
onOutputLine: onOutputLineCaptured,
)
await hardhat.start()
# TODO: move to constructor
if logFilePath != "":
hardhat.logFile = some hardhat.openLogFile(logFilePath)
return hardhat
method onOutputLineCaptured(node: HardhatProcess, line: string) =
logScope:
nodeName = node.name
if not node.onOutputLine.isNil:
node.onOutputLine(line)
without logFile =? node.logFile:
return
if error =? logFile.writeFile(line & "\n").errorOption:
error "failed to write to hardhat file", errorCode = $error
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:
trace "closing node process' streams"
await node.process.closeWait()
trace "node process' streams closed"
else:
# Windows hangs when attempting to close hardhat's process streams, so try
# to kill the process externally.
without port =? node.port:
error "Failed to get port from Hardhat args"
return
try:
let cmdResult = await killHardhatByPort(port)
if cmdResult.status > 0:
error "Failed to forcefully kill windows hardhat process",
port, exitCode = cmdResult.status, stderr = cmdResult.stdError
else:
trace "Successfully killed windows hardhat process by force",
port, exitCode = cmdResult.status, stdout = cmdResult.stdOutput
except ValueError, OSError:
let eMsg = getCurrentExceptionMsg()
error "Failed to forcefully kill windows hardhat process, bad path to command",
error = eMsg
except CancelledError as e:
discard
except AsyncProcessError as e:
error "Failed to forcefully kill windows hardhat process", port, error = e.msg
except AsyncProcessTimeoutError as e:
error "Timeout while forcefully killing windows hardhat process",
port, error = e.msg
method stop*(node: HardhatProcess) {.async: (raises: []).} =
# terminate the process
await procCall NodeProcess(node).stop()
await node.closeProcessStreams()
if logFile =? node.logFile:
trace "closing hardhat log file"
discard logFile.closeFile()
node.process = nil
method removeDataDir*(node: HardhatProcess) =
discard