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
|
## AsyncProcess leaks tracker name
|
||||||
|
|
||||||
type
|
type
|
||||||
AsyncProcessError* = object of CatchableError
|
AsyncProcessError* = object of AsyncError
|
||||||
|
AsyncProcessTimeoutError* = object of AsyncProcessError
|
||||||
|
|
||||||
AsyncProcessResult*[T] = Result[T, OSErrorCode]
|
AsyncProcessResult*[T] = Result[T, OSErrorCode]
|
||||||
|
|
||||||
|
@ -107,6 +108,9 @@ type
|
||||||
stdError*: string
|
stdError*: string
|
||||||
status*: int
|
status*: int
|
||||||
|
|
||||||
|
WaitOperation {.pure.} = enum
|
||||||
|
Kill, Terminate
|
||||||
|
|
||||||
template Pipe*(t: typedesc[AsyncProcess]): ProcessStreamHandle =
|
template Pipe*(t: typedesc[AsyncProcess]): ProcessStreamHandle =
|
||||||
ProcessStreamHandle(kind: ProcessStreamHandleKind.Auto)
|
ProcessStreamHandle(kind: ProcessStreamHandleKind.Auto)
|
||||||
|
|
||||||
|
@ -294,6 +298,11 @@ proc raiseAsyncProcessError(msg: string, exc: ref CatchableError = nil) {.
|
||||||
msg & " ([" & $exc.name & "]: " & $exc.msg & ")"
|
msg & " ([" & $exc.name & "]: " & $exc.msg & ")"
|
||||||
raise newException(AsyncProcessError, message)
|
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) {.
|
proc raiseAsyncProcessError(msg: string, error: OSErrorCode|cint) {.
|
||||||
noreturn, noinit, noinline, raises: [AsyncProcessError].} =
|
noreturn, noinit, noinline, raises: [AsyncProcessError].} =
|
||||||
when error is OSErrorCode:
|
when error is OSErrorCode:
|
||||||
|
@ -1189,6 +1198,45 @@ proc closeProcessStreams(pipes: AsyncProcessPipes,
|
||||||
res
|
res
|
||||||
allFutures(pending)
|
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.} =
|
proc closeWait*(p: AsyncProcessRef) {.async.} =
|
||||||
# Here we ignore all possible errrors, because we do not want to raise
|
# Here we ignore all possible errrors, because we do not want to raise
|
||||||
# exceptions.
|
# exceptions.
|
||||||
|
@ -1216,14 +1264,15 @@ proc execCommand*(command: string,
|
||||||
options = {AsyncProcessOption.EvalCommand},
|
options = {AsyncProcessOption.EvalCommand},
|
||||||
timeout = InfiniteDuration
|
timeout = InfiniteDuration
|
||||||
): Future[int] {.async.} =
|
): Future[int] {.async.} =
|
||||||
let poptions = options + {AsyncProcessOption.EvalCommand}
|
let
|
||||||
let process = await startProcess(command, options = poptions)
|
poptions = options + {AsyncProcessOption.EvalCommand}
|
||||||
let res =
|
process = await startProcess(command, options = poptions)
|
||||||
try:
|
res =
|
||||||
await process.waitForExit(timeout)
|
try:
|
||||||
finally:
|
await process.waitForExit(timeout)
|
||||||
await process.closeWait()
|
finally:
|
||||||
return res
|
await process.closeWait()
|
||||||
|
res
|
||||||
|
|
||||||
proc execCommandEx*(command: string,
|
proc execCommandEx*(command: string,
|
||||||
options = {AsyncProcessOption.EvalCommand},
|
options = {AsyncProcessOption.EvalCommand},
|
||||||
|
@ -1256,10 +1305,43 @@ proc execCommandEx*(command: string,
|
||||||
finally:
|
finally:
|
||||||
await process.closeWait()
|
await process.closeWait()
|
||||||
|
|
||||||
return res
|
res
|
||||||
|
|
||||||
proc pid*(p: AsyncProcessRef): int =
|
proc pid*(p: AsyncProcessRef): int =
|
||||||
## Returns process ``p`` identifier.
|
## Returns process ``p`` identifier.
|
||||||
int(p.processId)
|
int(p.processId)
|
||||||
|
|
||||||
template processId*(p: AsyncProcessRef): int = pid(p)
|
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" (
|
IF /I "%1" == "STDIN" (
|
||||||
GOTO :STDINTEST
|
GOTO :STDINTEST
|
||||||
|
) ELSE IF /I "%1" == "TIMEOUT1" (
|
||||||
|
GOTO :TIMEOUTTEST1
|
||||||
) ELSE IF /I "%1" == "TIMEOUT2" (
|
) ELSE IF /I "%1" == "TIMEOUT2" (
|
||||||
GOTO :TIMEOUTTEST2
|
GOTO :TIMEOUTTEST2
|
||||||
) ELSE IF /I "%1" == "TIMEOUT10" (
|
) ELSE IF /I "%1" == "TIMEOUT10" (
|
||||||
|
@ -19,6 +21,10 @@ SET /P "INPUTDATA="
|
||||||
ECHO STDIN DATA: %INPUTDATA%
|
ECHO STDIN DATA: %INPUTDATA%
|
||||||
EXIT 0
|
EXIT 0
|
||||||
|
|
||||||
|
:TIMEOUTTEST1
|
||||||
|
ping -n 1 127.0.0.1 > NUL
|
||||||
|
EXIT 1
|
||||||
|
|
||||||
:TIMEOUTTEST2
|
:TIMEOUTTEST2
|
||||||
ping -n 2 127.0.0.1 > NUL
|
ping -n 2 127.0.0.1 > NUL
|
||||||
EXIT 2
|
EXIT 2
|
||||||
|
|
|
@ -96,7 +96,11 @@ suite "Asynchronous process management test suite":
|
||||||
|
|
||||||
let
|
let
|
||||||
options = {AsyncProcessOption.EvalCommand}
|
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)
|
process = await startProcess(command, options = options)
|
||||||
|
|
||||||
|
@ -407,6 +411,52 @@ suite "Asynchronous process management test suite":
|
||||||
finally:
|
finally:
|
||||||
await process.closeWait()
|
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":
|
test "File descriptors leaks test":
|
||||||
when defined(windows):
|
when defined(windows):
|
||||||
skip()
|
skip()
|
||||||
|
|
|
@ -3,6 +3,9 @@
|
||||||
if [ "$1" == "stdin" ]; then
|
if [ "$1" == "stdin" ]; then
|
||||||
read -r inputdata
|
read -r inputdata
|
||||||
echo "STDIN DATA: $inputdata"
|
echo "STDIN DATA: $inputdata"
|
||||||
|
elif [ "$1" == "timeout1" ]; then
|
||||||
|
sleep 1
|
||||||
|
exit 1
|
||||||
elif [ "$1" == "timeout2" ]; then
|
elif [ "$1" == "timeout2" ]; then
|
||||||
sleep 2
|
sleep 2
|
||||||
exit 2
|
exit 2
|
||||||
|
@ -15,6 +18,11 @@ elif [ "$1" == "bigdata" ]; then
|
||||||
done
|
done
|
||||||
elif [ "$1" == "envtest" ]; then
|
elif [ "$1" == "envtest" ]; then
|
||||||
echo "$CHRONOSASYNC"
|
echo "$CHRONOSASYNC"
|
||||||
|
elif [ "$1" == "noterm" ]; then
|
||||||
|
trap -- '' SIGTERM
|
||||||
|
while true; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
else
|
else
|
||||||
echo "arguments missing"
|
echo "arguments missing"
|
||||||
fi
|
fi
|
||||||
|
|
Loading…
Reference in New Issue