Add some helpers for asyncproc. (#424)
* Initial commit. * Adjust posix tests. * Fix compilation issue. * Attempt to fix flaky addProcess() test.
This commit is contained in:
parent
f91ac169dc
commit
53e9f75735
|
@ -24,7 +24,8 @@ const
|
|||
## AsyncProcess leaks tracker name
|
||||
|
||||
type
|
||||
AsyncProcessError* = object of CatchableError
|
||||
AsyncProcessError* = object of AsyncError
|
||||
AsyncProcessTimeoutError* = object of AsyncProcessError
|
||||
|
||||
AsyncProcessResult*[T] = Result[T, OSErrorCode]
|
||||
|
||||
|
@ -107,6 +108,9 @@ type
|
|||
stdError*: string
|
||||
status*: int
|
||||
|
||||
WaitOperation {.pure.} = enum
|
||||
Kill, Terminate
|
||||
|
||||
template Pipe*(t: typedesc[AsyncProcess]): ProcessStreamHandle =
|
||||
ProcessStreamHandle(kind: ProcessStreamHandleKind.Auto)
|
||||
|
||||
|
@ -294,6 +298,11 @@ proc raiseAsyncProcessError(msg: string, exc: ref CatchableError = nil) {.
|
|||
msg & " ([" & $exc.name & "]: " & $exc.msg & ")"
|
||||
raise newException(AsyncProcessError, message)
|
||||
|
||||
proc raiseAsyncProcessTimeoutError() {.
|
||||
noreturn, noinit, noinline, raises: [AsyncProcessTimeoutError].} =
|
||||
let message = "Operation timed out"
|
||||
raise newException(AsyncProcessTimeoutError, message)
|
||||
|
||||
proc raiseAsyncProcessError(msg: string, error: OSErrorCode|cint) {.
|
||||
noreturn, noinit, noinline, raises: [AsyncProcessError].} =
|
||||
when error is OSErrorCode:
|
||||
|
@ -1189,6 +1198,45 @@ proc closeProcessStreams(pipes: AsyncProcessPipes,
|
|||
res
|
||||
allFutures(pending)
|
||||
|
||||
proc opAndWaitForExit(p: AsyncProcessRef, op: WaitOperation,
|
||||
timeout = InfiniteDuration): Future[int] {.async.} =
|
||||
let timerFut =
|
||||
if timeout == InfiniteDuration:
|
||||
newFuture[void]("chronos.killAndwaitForExit")
|
||||
else:
|
||||
sleepAsync(timeout)
|
||||
|
||||
while true:
|
||||
if p.running().get(true):
|
||||
# We ignore operation errors because we going to repeat calling
|
||||
# operation until process will not exit.
|
||||
case op
|
||||
of WaitOperation.Kill:
|
||||
discard p.kill()
|
||||
of WaitOperation.Terminate:
|
||||
discard p.terminate()
|
||||
else:
|
||||
let exitCode = p.peekExitCode().valueOr:
|
||||
raiseAsyncProcessError("Unable to peek process exit code", error)
|
||||
if not(timerFut.finished()):
|
||||
await cancelAndWait(timerFut)
|
||||
return exitCode
|
||||
|
||||
let waitFut = p.waitForExit().wait(100.milliseconds)
|
||||
discard await race(FutureBase(waitFut), FutureBase(timerFut))
|
||||
|
||||
if waitFut.finished() and not(waitFut.failed()):
|
||||
let res = p.peekExitCode()
|
||||
if res.isOk():
|
||||
if not(timerFut.finished()):
|
||||
await cancelAndWait(timerFut)
|
||||
return res.get()
|
||||
|
||||
if timerFut.finished():
|
||||
if not(waitFut.finished()):
|
||||
await waitFut.cancelAndWait()
|
||||
raiseAsyncProcessTimeoutError()
|
||||
|
||||
proc closeWait*(p: AsyncProcessRef) {.async.} =
|
||||
# Here we ignore all possible errrors, because we do not want to raise
|
||||
# exceptions.
|
||||
|
@ -1216,14 +1264,15 @@ proc execCommand*(command: string,
|
|||
options = {AsyncProcessOption.EvalCommand},
|
||||
timeout = InfiniteDuration
|
||||
): Future[int] {.async.} =
|
||||
let poptions = options + {AsyncProcessOption.EvalCommand}
|
||||
let process = await startProcess(command, options = poptions)
|
||||
let res =
|
||||
try:
|
||||
await process.waitForExit(timeout)
|
||||
finally:
|
||||
await process.closeWait()
|
||||
return res
|
||||
let
|
||||
poptions = options + {AsyncProcessOption.EvalCommand}
|
||||
process = await startProcess(command, options = poptions)
|
||||
res =
|
||||
try:
|
||||
await process.waitForExit(timeout)
|
||||
finally:
|
||||
await process.closeWait()
|
||||
res
|
||||
|
||||
proc execCommandEx*(command: string,
|
||||
options = {AsyncProcessOption.EvalCommand},
|
||||
|
@ -1256,10 +1305,43 @@ proc execCommandEx*(command: string,
|
|||
finally:
|
||||
await process.closeWait()
|
||||
|
||||
return res
|
||||
res
|
||||
|
||||
proc pid*(p: AsyncProcessRef): int =
|
||||
## Returns process ``p`` identifier.
|
||||
int(p.processId)
|
||||
|
||||
template processId*(p: AsyncProcessRef): int = pid(p)
|
||||
|
||||
proc killAndWaitForExit*(p: AsyncProcessRef,
|
||||
timeout = InfiniteDuration): Future[int] =
|
||||
## Perform continuous attempts to kill the ``p`` process for specified period
|
||||
## of time ``timeout``.
|
||||
##
|
||||
## On Posix systems, killing means sending ``SIGKILL`` to the process ``p``,
|
||||
## On Windows, it uses ``TerminateProcess`` to kill the process ``p``.
|
||||
##
|
||||
## If the process ``p`` fails to be killed within the ``timeout`` time, it
|
||||
## will raise ``AsyncProcessTimeoutError``.
|
||||
##
|
||||
## In case of error this it will raise ``AsyncProcessError``.
|
||||
##
|
||||
## Returns process ``p`` exit code.
|
||||
opAndWaitForExit(p, WaitOperation.Kill, timeout)
|
||||
|
||||
proc terminateAndWaitForExit*(p: AsyncProcessRef,
|
||||
timeout = InfiniteDuration): Future[int] =
|
||||
## Perform continuous attempts to terminate the ``p`` process for specified
|
||||
## period of time ``timeout``.
|
||||
##
|
||||
## On Posix systems, terminating means sending ``SIGTERM`` to the process
|
||||
## ``p``, on Windows, it uses ``TerminateProcess`` to terminate the process
|
||||
## ``p``.
|
||||
##
|
||||
## If the process ``p`` fails to be terminated within the ``timeout`` time, it
|
||||
## will raise ``AsyncProcessTimeoutError``.
|
||||
##
|
||||
## In case of error this it will raise ``AsyncProcessError``.
|
||||
##
|
||||
## Returns process ``p`` exit code.
|
||||
opAndWaitForExit(p, WaitOperation.Terminate, timeout)
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
IF /I "%1" == "STDIN" (
|
||||
GOTO :STDINTEST
|
||||
) ELSE IF /I "%1" == "TIMEOUT1" (
|
||||
GOTO :TIMEOUTTEST1
|
||||
) ELSE IF /I "%1" == "TIMEOUT2" (
|
||||
GOTO :TIMEOUTTEST2
|
||||
) ELSE IF /I "%1" == "TIMEOUT10" (
|
||||
|
@ -19,6 +21,10 @@ SET /P "INPUTDATA="
|
|||
ECHO STDIN DATA: %INPUTDATA%
|
||||
EXIT 0
|
||||
|
||||
:TIMEOUTTEST1
|
||||
ping -n 1 127.0.0.1 > NUL
|
||||
EXIT 1
|
||||
|
||||
:TIMEOUTTEST2
|
||||
ping -n 2 127.0.0.1 > NUL
|
||||
EXIT 2
|
||||
|
|
|
@ -96,7 +96,11 @@ suite "Asynchronous process management test suite":
|
|||
|
||||
let
|
||||
options = {AsyncProcessOption.EvalCommand}
|
||||
command = "exit 1"
|
||||
command =
|
||||
when defined(windows):
|
||||
"tests\\testproc.bat timeout1"
|
||||
else:
|
||||
"tests/testproc.sh timeout1"
|
||||
|
||||
process = await startProcess(command, options = options)
|
||||
|
||||
|
@ -407,6 +411,52 @@ suite "Asynchronous process management test suite":
|
|||
finally:
|
||||
await process.closeWait()
|
||||
|
||||
asyncTest "killAndWaitForExit() test":
|
||||
let command =
|
||||
when defined(windows):
|
||||
("tests\\testproc.bat", "timeout10", 0)
|
||||
else:
|
||||
("tests/testproc.sh", "timeout10", 128 + int(SIGKILL))
|
||||
let process = await startProcess(command[0], arguments = @[command[1]])
|
||||
try:
|
||||
let exitCode = await process.killAndWaitForExit(10.seconds)
|
||||
check exitCode == command[2]
|
||||
finally:
|
||||
await process.closeWait()
|
||||
|
||||
asyncTest "terminateAndWaitForExit() test":
|
||||
let command =
|
||||
when defined(windows):
|
||||
("tests\\testproc.bat", "timeout10", 0)
|
||||
else:
|
||||
("tests/testproc.sh", "timeout10", 128 + int(SIGTERM))
|
||||
let process = await startProcess(command[0], arguments = @[command[1]])
|
||||
try:
|
||||
let exitCode = await process.terminateAndWaitForExit(10.seconds)
|
||||
check exitCode == command[2]
|
||||
finally:
|
||||
await process.closeWait()
|
||||
|
||||
asyncTest "terminateAndWaitForExit() timeout test":
|
||||
when defined(windows):
|
||||
skip()
|
||||
else:
|
||||
let
|
||||
command = ("tests/testproc.sh", "noterm", 128 + int(SIGKILL))
|
||||
process = await startProcess(command[0], arguments = @[command[1]])
|
||||
# We should wait here to allow `bash` execute `trap` command, otherwise
|
||||
# our test script will be killed with SIGTERM. Increase this timeout
|
||||
# if test become flaky.
|
||||
await sleepAsync(1.seconds)
|
||||
try:
|
||||
expect AsyncProcessTimeoutError:
|
||||
let exitCode {.used.} =
|
||||
await process.terminateAndWaitForExit(1.seconds)
|
||||
let exitCode = await process.killAndWaitForExit(10.seconds)
|
||||
check exitCode == command[2]
|
||||
finally:
|
||||
await process.closeWait()
|
||||
|
||||
test "File descriptors leaks test":
|
||||
when defined(windows):
|
||||
skip()
|
||||
|
|
|
@ -3,6 +3,9 @@
|
|||
if [ "$1" == "stdin" ]; then
|
||||
read -r inputdata
|
||||
echo "STDIN DATA: $inputdata"
|
||||
elif [ "$1" == "timeout1" ]; then
|
||||
sleep 1
|
||||
exit 1
|
||||
elif [ "$1" == "timeout2" ]; then
|
||||
sleep 2
|
||||
exit 2
|
||||
|
@ -15,6 +18,11 @@ elif [ "$1" == "bigdata" ]; then
|
|||
done
|
||||
elif [ "$1" == "envtest" ]; then
|
||||
echo "$CHRONOSASYNC"
|
||||
elif [ "$1" == "noterm" ]; then
|
||||
trap -- '' SIGTERM
|
||||
while true; do
|
||||
sleep 1
|
||||
done
|
||||
else
|
||||
echo "arguments missing"
|
||||
fi
|
||||
|
|
Loading…
Reference in New Issue