From 02fda01bf260a16d70e5b827819b5314542d5ecd Mon Sep 17 00:00:00 2001 From: Eugene Kabanov Date: Fri, 2 Jun 2023 01:53:20 +0300 Subject: [PATCH] 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. --- chronos/asyncloop.nim | 154 ++++++++++++++++++++++++++-------- chronos/handles.nim | 2 +- chronos/osdefs.nim | 46 +++++++++- chronos/oserrno.nim | 2 + chronos/osutils.nim | 117 +++++++++++++++++++++++++- chronos/transports/stream.nim | 6 +- tests/testsignal.nim | 95 ++++++++++++--------- 7 files changed, 342 insertions(+), 80 deletions(-) diff --git a/chronos/asyncloop.nim b/chronos/asyncloop.nim index ff2f0794..8c9b6261 100644 --- a/chronos/asyncloop.nim +++ b/chronos/asyncloop.nim @@ -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 diff --git a/chronos/handles.nim b/chronos/handles.nim index ee12ab1f..65b31a43 100644 --- a/chronos/handles.nim +++ b/chronos/handles.nim @@ -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) diff --git a/chronos/osdefs.nim b/chronos/osdefs.nim index 39bd947d..92773d07 100644 --- a/chronos/osdefs.nim +++ b/chronos/osdefs.nim @@ -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: "", 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)), diff --git a/chronos/oserrno.nim b/chronos/oserrno.nim index 5cacb223..4f1c7658 100644 --- a/chronos/oserrno.nim +++ b/chronos/oserrno.nim @@ -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) diff --git a/chronos/osutils.nim b/chronos/osutils.nim index 2ff1072a..40f76a35 100644 --- a/chronos/osutils.nim +++ b/chronos/osutils.nim @@ -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--``. + ## + ## 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 = diff --git a/chronos/transports/stream.nim b/chronos/transports/stream.nim index 1b28b1de..260d21ee 100644 --- a/chronos/transports/stream.nim +++ b/chronos/transports/stream.nim @@ -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) = diff --git a/tests/testsignal.nim b/tests/testsignal.nim index 0bcf7933..4114809f 100644 --- a/tests/testsignal.nim +++ b/tests/testsignal.nim @@ -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()