Add facility to emulate signals on Windows. (#357)

* Initial Windows asyncproc part.
Deprecate usage of net/nativesockets in handles, asyncloop.
Introduce primitives with inheritable flag.

* Some posix fixes.

* More duplicate fixes.

* Fix AsyncProcessOption.EchoCommand option.
Remove isMainModule code.
Fix execCommand to use AsyncProcessOption.EvalCommand.

* Initial asyncproc tests commit.

* Some Posix fixes.

* Fix Posix crash.

* Add testsuite.
Fix osnet.nim to use proper declarations.
Fix timer.nim to use proper declarations.
Add Windows environment handling procedures.
Fix createAsyncPipe.
Add leaks tracking for AsyncProcessRef.

* Fix O_CLOEXEC constant value.

* Add file descriptors leak test.

* Remove commented code.
Refactor exceptions.
Fix compilation warnings.

* No exception ioselectors_kqueue initial commit.

* Some BSD fixes.
Linux refactoring of selectors.nim.

* Some fixes to move further.

* Last Linux fixes.

* Fixes for asyncloop to use 2nd version of selectors api.

* Add osutils.nim.

* Some fixes.

* Hardening resumeRead(), resumeWrite() and consumers.
Add ESRCH handling.
Introduce no-exception fromPipe2.

* Make Windows part exception-free and fix zombie race issue.

* createStreamServer() fixes.

* Upgrade asyncproc to use non-exception primitives.
Fix ioselectors_kqueue to use closeFd().

* Refactor accept() and acceptLoop() to be exception free.

* Deprecated some `result` usage.
Last fixes to make stream.nim exception free.
Use closeFd().
Refactor some loops to use handleEintr().

* Fix connect() forgot to unregister socket on error.

* All createAsyncSocket() sockets should be closed with unregisterAndCloseFd().

* Attempt to fix posix bug with incomplete output.

* Change number of runs in big test.

* Refactoring pipes creation. Attempt to fix "write error: Resource temporarily unavailable".

* Fix waitForExit(duration) code.
Fix test exit code.

* Fix Windows missing SIGTERM.

* Fix mistype.

* Fix compilation error.

* Attempt to fix Nim 1.6 issue.

* Eliminate Nim's WideCString usage to avoid any problems in future.

* Deprecate posix usage in osnet.

* Eliminate unused imports.

* Some debugging statements for investigation.

* Remove all the debugging statements.

* Remove testhelpers in favor of unittest2/asynctests.

* Fix flaky test.

* Make test more resilient to timings.

* Add memory size dump to CI.

* Refactor some blocks to use valueOr.
Make waitForExit to use kill() instead of terminate().

* Remove memory dumps.

* Fix peekProcessExitCode() blocks issue.

* Fix Windows issue.

* Add some debugging echoes.

* Fix compilation error.

* Add some debugging echoes.

* Add more helpers.

* Fix compilation error.

* waitForExit() is primary suspect.

* Remove fast-path for waitForExit.
Remove loop for reading signal events.

* Remove all debugging echoes.

* Return one debugging echo.

* Fix futures tests.

* Add test for multiple processes waiting to attempt stress test.

* Refactor ioselectors_epoll for better signalfd and process monitoring.
Add more race condition fixes to waitForExit.
Fix some Nim 1.6 warnings.

* Fix after rebase issues and warnings.

* Fix style issues.
Fix different Nim versions issues.
Workaround `signalfd` style issues.

* Add one more Linux signals workaround.
Add one more multiple processes test.

* Windows fixes.

* Remove unixPlatform define.
Fix WSAECONNABORTED for devel.

* Temporarily disable rate limit tests.
Fix more devel issues.

* Deprecate `hasThreadSupport` for ioselectors_kqueue.
Fix verifySelectParams issue.
Add exit codes test for multiple processes.
Fix osnet PtrToCstringConv warning.

* ioselectors_kqueue refactoring.

* Initial commit.

* Fix 1.2-1.4 compilation issue.

* Fix unused warning for testCtrlC() test.

* Post-rebase fixes.

* Restore master files.

* More fixes.

* Remove duplicates.

* Fix style mistake.

* Add more flexible pragmas.
This commit is contained in:
Eugene Kabanov 2023-06-02 01:53:20 +03:00 committed by GitHub
parent 315a27236c
commit 02fda01bf2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 342 additions and 80 deletions

View File

@ -10,8 +10,10 @@
when (NimMajor, NimMinor) < (1, 4):
{.push raises: [Defect].}
{.pragma: callbackFunc, stdcall, gcsafe, raises: [Defect].}
else:
{.push raises: [].}
{.pragma: callbackFunc, stdcall, gcsafe, raises: [].}
from nativesockets import Port
import std/[tables, strutils, heapqueue, deques]
@ -289,6 +291,7 @@ func toException*(v: OSErrorCode): ref OSError = newOSError(v)
# Result[T, OSErrorCode] values.
when defined(windows):
export SIGINT, SIGQUIT, SIGTERM
type
CompletionKey = ULONG_PTR
@ -301,11 +304,8 @@ when defined(windows):
CustomOverlapped* = object of OVERLAPPED
data*: CompletionData
OVERLAPPED_ENTRY* = object
lpCompletionKey*: ULONG_PTR
lpOverlapped*: ptr CustomOverlapped
internal: ULONG_PTR
dwNumberOfBytesTransferred: DWORD
DispatcherFlag* = enum
SignalHandlerInstalled
PDispatcher* = ref object of PDispatcherBase
ioPort: HANDLE
@ -315,6 +315,7 @@ when defined(windows):
getAcceptExSockAddrs*: WSAPROC_GETACCEPTEXSOCKADDRS
transmitFile*: WSAPROC_TRANSMITFILE
getQueuedCompletionStatusEx*: LPFN_GETQUEUEDCOMPLETIONSTATUSEX
flags: set[DispatcherFlag]
PtrCustomOverlapped* = ptr CustomOverlapped
@ -330,6 +331,7 @@ when defined(windows):
WaitableHandle* = ref PostCallbackData
ProcessHandle* = distinct WaitableHandle
SignalHandle* = distinct WaitableHandle
WaitableResult* {.pure.} = enum
Ok, Timeout
@ -444,7 +446,7 @@ when defined(windows):
{.push stackTrace: off.}
proc waitableCallback(param: pointer, timerOrWaitFired: WINBOOL) {.
stdcall, gcsafe.} =
callbackFunc.} =
# This procedure will be executed in `wait thread`, so it must not use
# GC related objects.
# We going to ignore callbacks which was spawned when `isNil(param) == true`
@ -577,6 +579,86 @@ when defined(windows):
## Remove process' watching using process' descriptor ``procHandle``.
removeProcess2(procHandle).tryGet()
{.push stackTrace: off.}
proc consoleCtrlEventHandler(dwCtrlType: DWORD): uint32 {.callbackFunc.} =
## This procedure will be executed in different thread, so it MUST not use
## any GC related features (strings, seqs, echo etc.).
case dwCtrlType
of CTRL_C_EVENT:
return
(if raiseSignal(SIGINT).valueOr(false): TRUE else: FALSE)
of CTRL_BREAK_EVENT:
return
(if raiseSignal(SIGINT).valueOr(false): TRUE else: FALSE)
of CTRL_CLOSE_EVENT:
return
(if raiseSignal(SIGTERM).valueOr(false): TRUE else: FALSE)
of CTRL_LOGOFF_EVENT:
return
(if raiseSignal(SIGQUIT).valueOr(false): TRUE else: FALSE)
else:
FALSE
{.pop.}
proc addSignal2*(signal: int, cb: CallbackFunc,
udata: pointer = nil): Result[SignalHandle, OSErrorCode] =
## Start watching signal ``signal``, and when signal appears, call the
## callback ``cb`` with specified argument ``udata``. Returns signal
## identifier code, which can be used to remove signal callback
## via ``removeSignal``.
##
## NOTE: On Windows only subset of signals are supported: SIGINT, SIGTERM,
## SIGQUIT
const supportedSignals = [SIGINT, SIGTERM, SIGQUIT]
doAssert(cint(signal) in supportedSignals, "Signal is not supported")
let loop = getThreadDispatcher()
var hWait: WaitableHandle = nil
proc continuation(ucdata: pointer) {.gcsafe.} =
doAssert(not(isNil(ucdata)))
doAssert(not(isNil(hWait)))
cb(hWait[].udata)
if SignalHandlerInstalled notin loop.flags:
if getConsoleCP() != 0'u32:
# Console application, we going to cleanup Nim default signal handlers.
if setConsoleCtrlHandler(consoleCtrlEventHandler, TRUE) == FALSE:
return err(osLastError())
loop.flags.incl(SignalHandlerInstalled)
else:
return err(ERROR_NOT_SUPPORTED)
let
flags = WT_EXECUTEINWAITTHREAD
hEvent = ? openEvent($getSignalName(signal))
hWait = registerWaitable(hEvent, flags, InfiniteDuration,
continuation, udata).valueOr:
discard closeFd(hEvent)
return err(error)
ok(SignalHandle(hWait))
proc removeSignal2*(signalHandle: SignalHandle): Result[void, OSErrorCode] =
## Remove watching signal ``signal``.
? closeWaitable(WaitableHandle(signalHandle))
ok()
proc addSignal*(signal: int, cb: CallbackFunc,
udata: pointer = nil): SignalHandle {.
raises: [Defect, ValueError].} =
## Registers callback ``cb`` to be called when signal ``signal`` will be
## raised. Returns signal identifier, which can be used to clear signal
## callback via ``removeSignal``.
addSignal2(signal, cb, udata).valueOr:
raise newException(ValueError, osErrorMsg(error))
proc removeSignal*(signalHandle: SignalHandle) {.
raises: [Defect, ValueError].} =
## Remove signal's watching using signal descriptor ``signalfd``.
let res = removeSignal2(signalHandle)
if res.isErr():
raise newException(ValueError, osErrorMsg(res.error()))
proc poll*() =
## Perform single asynchronous step, processing timers and completing
## tasks. Blocks until at least one event has completed.
@ -870,7 +952,6 @@ elif defined(macosx) or defined(freebsd) or defined(netbsd) or
## Please note, that socket is not closed immediately. To avoid bugs with
## closing socket, while operation pending, socket will be closed as
## soon as all pending operations will be notified.
## You can execute ``aftercb`` before actual socket close operation.
let loop = getThreadDispatcher()
proc continuation(udata: pointer) =
@ -1168,41 +1249,44 @@ proc callIdle*(cbproc: CallbackFunc) =
include asyncfutures2
when not(defined(windows)):
when asyncEventEngine in ["epoll", "kqueue"]:
proc waitSignal*(signal: int): Future[void] {.raises: [Defect].} =
var retFuture = newFuture[void]("chronos.waitSignal()")
var signalHandle: Opt[SignalHandle]
template getSignalException(e: OSErrorCode): untyped =
newException(AsyncError, "Could not manipulate signal handler, " &
"reason [" & $int(e) & "]: " & osErrorMsg(e))
when defined(macosx) or defined(macos) or defined(freebsd) or
defined(netbsd) or defined(openbsd) or defined(dragonfly) or
defined(linux) or defined(windows):
proc continuation(udata: pointer) {.gcsafe.} =
if not(retFuture.finished()):
if signalHandle.isSome():
let res = removeSignal2(signalHandle.get())
if res.isErr():
retFuture.fail(getSignalException(res.error()))
else:
retFuture.complete()
proc waitSignal*(signal: int): Future[void] {.raises: [Defect].} =
var retFuture = newFuture[void]("chronos.waitSignal()")
var signalHandle: Opt[SignalHandle]
proc cancellation(udata: pointer) {.gcsafe.} =
if not(retFuture.finished()):
if signalHandle.isSome():
let res = removeSignal2(signalHandle.get())
if res.isErr():
retFuture.fail(getSignalException(res.error()))
template getSignalException(e: OSErrorCode): untyped =
newException(AsyncError, "Could not manipulate signal handler, " &
"reason [" & $int(e) & "]: " & osErrorMsg(e))
signalHandle =
block:
let res = addSignal2(signal, continuation)
proc continuation(udata: pointer) {.gcsafe.} =
if not(retFuture.finished()):
if signalHandle.isSome():
let res = removeSignal2(signalHandle.get())
if res.isErr():
retFuture.fail(getSignalException(res.error()))
Opt.some(res.get())
else:
retFuture.complete()
retFuture.cancelCallback = cancellation
retFuture
proc cancellation(udata: pointer) {.gcsafe.} =
if not(retFuture.finished()):
if signalHandle.isSome():
let res = removeSignal2(signalHandle.get())
if res.isErr():
retFuture.fail(getSignalException(res.error()))
signalHandle =
block:
let res = addSignal2(signal, continuation)
if res.isErr():
retFuture.fail(getSignalException(res.error()))
Opt.some(res.get())
retFuture.cancelCallback = cancellation
retFuture
proc sleepAsync*(duration: Duration): Future[void] =
## Suspends the execution of the current async procedure for the next

View File

@ -15,7 +15,7 @@ else:
import "."/[asyncloop, osdefs, osutils]
import stew/results
from nativesockets import Domain, Protocol, SockType, toInt
export Domain, Protocol, SockType, results
export Domain, Protocol, SockType, results, osutils
const
asyncInvalidSocket* = AsyncFD(osdefs.INVALID_SOCKET)

View File

@ -61,6 +61,12 @@ when defined(windows):
INADDR_BROADCAST* = 0xffff_ffff'u32
INADDR_NONE* = 0xffff_ffff'u32
CTRL_C_EVENT* = 0'u32
CTRL_BREAK_EVENT* = 1'u32
CTRL_CLOSE_EVENT* = 2'u32
CTRL_LOGOFF_EVENT* = 5'u32
CTRL_SHUTDOWN_EVENT* = 6'u32
WAIT_ABANDONED* = 0x80'u32
WAIT_OBJECT_0* = 0x00'u32
WAIT_TIMEOUT* = 0x102'u32
@ -299,7 +305,10 @@ when defined(windows):
POVERLAPPED_COMPLETION_ROUTINE* = proc (para1: DWORD, para2: DWORD,
para3: POVERLAPPED) {.
stdcall, gcsafe, raises: [].}
stdcall, gcsafe, raises: [].}
PHANDLER_ROUTINE* = proc (dwCtrlType: DWORD): WINBOOL {.
stdcall, gcsafe, raises: [Defect].}
OSVERSIONINFO* {.final, pure.} = object
dwOSVersionInfoSize*: DWORD
@ -494,6 +503,8 @@ when defined(windows):
dwMilliseconds: DWORD, fAlertable: WINBOOL): WINBOOL {.
stdcall, gcsafe, raises: [].}
WindowsSigHandler = proc (a: cint) {.noconv, raises: [], gcsafe.}
proc getVersionEx*(lpVersionInfo: ptr OSVERSIONINFO): WINBOOL {.
stdcall, dynlib: "kernel32", importc: "GetVersionExW", sideEffect.}
@ -593,6 +604,9 @@ when defined(windows):
proc getCurrentProcess*(): HANDLE {.
stdcall, dynlib: "kernel32", importc: "GetCurrentProcess", sideEffect.}
proc getCurrentProcessId*(): DWORD {.
stdcall, dynlib: "kernel32", importc: "GetCurrentProcessId", sideEffect.}
proc getSystemTimeAsFileTime*(lpSystemTimeAsFileTime: var FILETIME) {.
stdcall, dynlib: "kernel32", importc: "GetSystemTimeAsFileTime",
sideEffect.}
@ -710,7 +724,7 @@ when defined(windows):
proc createEvent*(lpEventAttributes: ptr SECURITY_ATTRIBUTES,
bManualReset: DWORD, bInitialState: DWORD,
lpName: LPWSTR): HANDLE {.
lpName: ptr WCHAR): HANDLE {.
stdcall, dynlib: "kernel32", importc: "CreateEventW", sideEffect.}
proc setEvent*(hEvent: HANDLE): WINBOOL {.
@ -811,9 +825,37 @@ when defined(windows):
proc rtlNtStatusToDosError*(code: uint64): ULONG {.
stdcall, dynlib: "ntdll", importc: "RtlNtStatusToDosError", sideEffect.}
proc getConsoleCP*(): UINT {.
stdcall, dynlib: "kernel32", importc: "GetConsoleCP", sideEffect.}
proc setConsoleCtrlHandler*(handleRoutine: PHANDLER_ROUTINE,
add: WINBOOL): WINBOOL {.
stdcall, dynlib: "kernel32", importc: "SetConsoleCtrlHandler",
sideEffect.}
proc generateConsoleCtrlEvent*(dwCtrlEvent: DWORD,
dwProcessGroupId: DWORD): WINBOOL {.
stdcall, dynlib: "kernel32", importc: "GenerateConsoleCtrlEvent",
sideEffect.}
proc `==`*(x, y: SocketHandle): bool {.borrow.}
proc `==`*(x, y: HANDLE): bool {.borrow.}
proc c_signal*(sign: cint, handler: WindowsSigHandler): WindowsSigHandler {.
importc: "signal", header: "<signal.h>", raises: [], sideEffect.}
const
SIGABRT* = cint(22)
SIGINT* = cint(2)
SIGQUIT* = cint(3)
SIGTERM* = cint(15)
SIGFPE* = cint(8)
SIGILL* = cint(4)
SIGSEGV* = cint(11)
SIG_DFL* = cast[WindowsSigHandler](0)
SIG_IGN* = cast[WindowsSigHandler](1)
SIG_ERR* = cast[WindowsSigHandler](-1)
proc getSecurityAttributes*(inheritHandle = false): SECURITY_ATTRIBUTES =
SECURITY_ATTRIBUTES(
nLength: DWORD(sizeof(SECURITY_ATTRIBUTES)),

View File

@ -1315,6 +1315,8 @@ elif defined(windows):
ERROR_FILE_NOT_FOUND* = OSErrorCode(2)
ERROR_TOO_MANY_OPEN_FILES* = OSErrorCode(4)
ERROR_ACCESS_DENIED* = OSErrorCode(5)
ERROR_ALREADY_EXISTS* = OSErrorCode(183)
ERROR_NOT_SUPPORTED* = OSErrorCode(50)
ERROR_BROKEN_PIPE* = OSErrorCode(109)
ERROR_BUFFER_OVERFLOW* = OSErrorCode(111)
ERROR_PIPE_BUSY* = OSErrorCode(231)

View File

@ -18,7 +18,12 @@ else:
when defined(windows) or defined(nimdoc):
import stew/base10
const PipeHeaderName* = r"\\.\pipe\LOCAL\chronos\"
const
PipeHeaderName* = r"\\.\pipe\LOCAL\chronos\"
SignalPrefixName* = cstring(r"Local\chronos-events-")
MaxSignalEventLength* = 64
MaxSignalSuffixLength* = MaxSignalEventLength -
(len(SignalPrefixName) + Base10.maxLen(uint64) + 2)
type
DescriptorFlag* {.pure.} = enum
@ -74,6 +79,26 @@ when defined(windows):
proc closeFd*(s: HANDLE): int =
if osdefs.closeHandle(s) == TRUE: 0 else: -1
proc toWideBuffer*(s: openArray[char],
d: var openArray[WCHAR]): Result[int, OSErrorCode] =
if len(s) == 0: return ok(0)
let res = multiByteToWideChar(CP_UTF8, 0'u32, unsafeAddr s[0], cint(-1),
addr d[0], cint(len(d)))
if res == 0:
err(osLastError())
else:
ok(res)
proc toMultibyteBuffer*(s: openArray[WCHAR],
d: var openArray[char]): Result[int, OSErrorCode] =
if len(s) == 0: return ok(0)
let res = wideCharToMultiByte(CP_UTF8, 0'u32, unsafeAddr s[0], cint(-1),
addr d[0], cint(len(d)), nil, nil)
if res == 0:
err(osLastError())
else:
ok(res)
proc toWideString*(s: string): Result[LPWSTR, OSErrorCode] =
if len(s) == 0:
ok(cast[LPWSTR](alloc0(sizeof(WCHAR))))
@ -209,6 +234,96 @@ when defined(windows):
return err(errorCode)
ok((read: pipeIn, write: pipeOut))
proc getSignalName*(signal: int): cstring =
## Convert Windows SIGNAL identifier to string representation.
##
## This procedure supports only SIGINT, SIGTERM and SIGQUIT values.
case signal
of SIGINT: cstring("sigint")
of SIGTERM: cstring("sigterm")
of SIGQUIT: cstring("sigquit")
else:
raiseAssert "Signal is not supported"
proc getEventPath*(suffix: cstring): array[MaxSignalEventLength, WCHAR] =
## Create Windows' Event object name suffixed by ``suffix``. This name
## is create in local session namespace with name like this:
## ``Local\chronos-events-<process id>-<suffix>``.
##
## This procedure is GC-free, so it could be used in other threads.
doAssert(len(suffix) < MaxSignalSuffixLength)
var
resMc: array[MaxSignalEventLength, char]
resWc: array[MaxSignalEventLength, WCHAR]
var offset = 0
let
pid = osdefs.getCurrentProcessId()
pid10 = Base10.toBytes(uint64(pid))
copyMem(addr resMc[offset], SignalPrefixName, len(SignalPrefixName))
offset += len(SignalPrefixName)
copyMem(addr resMc[offset], unsafeAddr pid10.data[0], pid10.len)
offset += pid10.len
resMc[offset] = '-'
offset += 1
copyMem(addr resMc[offset], suffix, len(suffix))
offset += len(suffix)
resMc[offset] = '\x00'
let res = toWideBuffer(resMc, resWc)
if res.isErr():
raiseAssert "Invalid suffix value, got " & osErrorMsg(res.error())
resWc
proc raiseEvent(suffix: cstring): Result[bool, OSErrorCode] =
var sa = getSecurityAttributes()
let
eventName = getEventPath(suffix)
# We going to fire event, so we can try to create it already signaled.
event = createEvent(addr sa, FALSE, TRUE, unsafeAddr eventName[0])
errorCode = osLastError()
if event == HANDLE(0):
err(errorCode)
else:
if errorCode == ERROR_ALREADY_EXISTS:
let res = setEvent(event)
if res == FALSE:
err(osLastError())
else:
ok(true)
else:
ok(false)
proc raiseSignal*(signal: cint): Result[bool, OSErrorCode] =
## This is helper procedure which could help to raise Unix signals in
## Windows GUI / Service application. Console applications are handled
## automatically.
##
## This procedure does not use Nim's GC, so it can be placed in any handler
## of your application even in code which is running in different thread.
raiseEvent(getSignalName(signal))
proc raiseConsoleCtrlSignal*(groupId = 0'u32): Result[void, OSErrorCode] =
## Raise CTRL+C event in current console.
if generateConsoleCtrlEvent(CTRL_C_EVENT, groupId) == FALSE:
err(osLastError())
else:
ok()
proc openEvent*(suffix: string): Result[HANDLE, OSErrorCode] =
## Open or create Windows event object with suffix ``suffix``.
var sa = getSecurityAttributes()
let
# We going to wait for created event, so we don't need to create it in
# signaled state.
eventName = getEventPath(suffix)
event = createEvent(addr sa, FALSE, FALSE, unsafeAddr eventName[0])
if event == HANDLE(0):
let errorCode = osLastError()
err(errorCode)
else:
ok(event)
else:
template handleEintr*(body: untyped): untyped =

View File

@ -1896,7 +1896,7 @@ proc close*(server: StreamServer) =
proc closeWait*(server: StreamServer): Future[void] =
## Close server ``server`` and release all resources.
server.close()
result = server.join()
server.join()
proc createStreamServer*(host: TransportAddress,
cbproc: StreamCallback,
@ -2055,7 +2055,7 @@ proc createStreamServer*(host: TransportAddress,
addr slen) != 0:
let err = osLastError()
if sock == asyncInvalidSocket:
serverSocket.closeSocket()
discard unregisterAndCloseFd(serverSocket)
raiseTransportOsError(err)
fromSAddr(addr saddr, slen, localAddress)
@ -2150,7 +2150,7 @@ proc createStreamServer*[T](host: TransportAddress,
proc getUserData*[T](server: StreamServer): T {.inline.} =
## Obtain user data stored in ``server`` object.
result = cast[T](server.udata)
cast[T](server.udata)
template fastWrite(transp: auto, pbytes: var ptr byte, rbytes: var int,
nbytes: int) =

View File

@ -6,7 +6,7 @@
# Apache License, version 2.0, (LICENSE-APACHEv2)
# MIT license (LICENSE-MIT)
import unittest2
import ../chronos
import ../chronos, ../chronos/oserrno
{.used.}
@ -14,57 +14,76 @@ when not defined(windows):
import posix
suite "Signal handling test suite":
when not defined(windows):
proc testSignal(signal, value: int): Future[bool] {.async.} =
var
signalCounter = 0
sigfd: SignalHandle
sigFd: SignalHandle
handlerFut = newFuture[void]("signal.handler")
proc signalProc(udata: pointer) =
proc signalHandler(udata: pointer) {.gcsafe.} =
signalCounter = cast[int](udata)
try:
removeSignal(sigfd)
except Exception as exc:
raiseAssert exc.msg
let res = removeSignal2(sigFd)
if res.isErr():
handlerFut.fail(newException(ValueError, osErrorMsg(res.error())))
else:
handlerFut.complete()
proc asyncProc() {.async.} =
await sleepAsync(500.milliseconds)
sigFd =
block:
let res = addSignal2(signal, signalHandler, cast[pointer](value))
if res.isErr():
raiseAssert osErrorMsg(res.error())
res.get()
proc test(signal, value: int): bool =
try:
sigfd = addSignal(signal, signalProc, cast[pointer](value))
except Exception as exc:
raiseAssert exc.msg
var fut = asyncProc()
when defined(windows):
discard raiseSignal(cint(signal))
else:
discard posix.kill(posix.getpid(), cint(signal))
waitFor(fut)
signalCounter == value
proc testWait(signal: int): bool =
var fut = waitSignal(signal)
await handlerFut.wait(5.seconds)
return signalCounter == value
proc testWait(signal: int): Future[bool] {.async.} =
var fut = waitSignal(signal)
when defined(windows):
discard raiseSignal(cint(signal))
else:
discard posix.kill(posix.getpid(), cint(signal))
waitFor(fut)
true
await fut.wait(5.seconds)
return true
when defined(windows):
proc testCtrlC(): Future[bool] {.async, used.} =
var fut = waitSignal(SIGINT)
let res = raiseConsoleCtrlSignal()
if res.isErr():
raiseAssert osErrorMsg(res.error())
await fut.wait(5.seconds)
return true
test "SIGINT test":
when not defined(windows):
check test(SIGINT, 31337) == true
else:
skip()
let res = waitFor testSignal(SIGINT, 31337)
check res == true
test "SIGTERM test":
when defined(windows):
skip()
else:
check test(SIGTERM, 65537) == true
let res = waitFor testSignal(SIGTERM, 65537)
check res == true
test "waitSignal(SIGINT) test":
when defined(windows):
skip()
else:
check testWait(SIGINT) == true
let res = waitFor testWait(SIGINT)
check res == true
test "waitSignal(SIGTERM) test":
when defined(windows):
skip()
else:
check testWait(SIGTERM) == true
let res = waitFor testWait(SIGTERM)
check res == true
# This test doesn't work well in test suite, because it generates CTRL+C
# event in Windows console, parent process receives this signal and stops
# test suite execution.
# test "Windows [CTRL+C] test":
# when defined(windows):
# let res = waitFor testCtrlC()
# check res == true
# else:
# skip()