diff --git a/chronos/asyncsync.nim b/chronos/asyncsync.nim index c0f51b4d3..66cc42d0d 100644 --- a/chronos/asyncsync.nim +++ b/chronos/asyncsync.nim @@ -13,7 +13,7 @@ {.push raises: [].} import std/[sequtils, math, deques, tables, typetraits] -import ./asyncloop +import ./asyncloop, ./shutdown export asyncloop type @@ -247,12 +247,14 @@ proc empty*[T](aq: AsyncQueue[T]): bool {.inline.} = (len(aq.queue) == 0) proc addFirstImpl[T](aq: AsyncQueue[T], item: T) = - aq.queue.addFirst(item) - aq.getters.wakeupNext() + if not isShutdownInProgress(): + aq.queue.addFirst(item) + aq.getters.wakeupNext() proc addLastImpl[T](aq: AsyncQueue[T], item: T) = - aq.queue.addLast(item) - aq.getters.wakeupNext() + if not isShutdownInProgress(): + aq.queue.addLast(item) + aq.getters.wakeupNext() proc popFirstImpl[T](aq: AsyncQueue[T]): T = let res = aq.queue.popFirst() diff --git a/chronos/handles.nim b/chronos/handles.nim index 72b0751e8..e5ead064b 100644 --- a/chronos/handles.nim +++ b/chronos/handles.nim @@ -9,7 +9,7 @@ {.push raises: [].} -import "."/[asyncloop, osdefs, osutils] +import "."/[asyncloop, osdefs, osutils, shutdown] import results from nativesockets import Domain, Protocol, SockType, toInt export Domain, Protocol, SockType, results @@ -129,6 +129,9 @@ proc createAsyncSocket2*(domain: Domain, sockType: SockType, protocol: Protocol, inherit = true): Result[AsyncFD, OSErrorCode] = ## Creates new asynchronous socket. + + checkShutdownInProgress() + when defined(windows): let flags = if inherit: diff --git a/chronos/internal/asyncengine.nim b/chronos/internal/asyncengine.nim index 45197a038..17eba61a8 100644 --- a/chronos/internal/asyncengine.nim +++ b/chronos/internal/asyncengine.nim @@ -17,7 +17,7 @@ from nativesockets import Port import std/[tables, heapqueue, deques] import results -import ".."/[config, futures, osdefs, oserrno, osutils, timer] +import ".."/[config, futures, osdefs, oserrno, osutils, timer, shutdown] import ./[asyncmacro, errors] @@ -64,6 +64,7 @@ type ticks*: Deque[AsyncCallback] trackers*: Table[string, TrackerBase] counters*: Table[string, TrackerCounter] + networkEventsCount*: int proc sentinelCallbackImpl(arg: pointer) {.gcsafe, noreturn.} = raiseAssert "Sentinel callback MUST not be scheduled" @@ -185,7 +186,7 @@ when defined(nimdoc): ## Perform single asynchronous step, processing timers and completing ## tasks. Blocks until at least one event has completed. ## - ## Exceptions raised during `async` task exection are stored as outcome + ## Exceptions raised during `async` task exception are stored as outcome ## in the corresponding `Future` - `poll` itself does not raise. proc register2*(fd: AsyncFD): Result[void, OSErrorCode] = discard @@ -327,6 +328,9 @@ elif defined(windows): if port == osdefs.INVALID_HANDLE_VALUE: raiseOsDefect(osLastError(), "newDispatcher(): Unable to create " & "IOCP port") + + initShutdownInProgressToFalse() ## defined in shutdown.nim + var res = PDispatcher( ioPort: port, handles: initHashSet[AsyncFD](), @@ -353,6 +357,8 @@ elif defined(windows): proc register2*(fd: AsyncFD): Result[void, OSErrorCode] = ## Register file descriptor ``fd`` in thread's dispatcher. + checkShutdownInProgress() + let loop = getThreadDispatcher() if createIoCompletionPort(HANDLE(fd), loop.ioPort, cast[CompletionKey](fd), 1) == osdefs.INVALID_HANDLE_VALUE: @@ -404,6 +410,9 @@ elif defined(windows): ## ## NOTE: This is private procedure, not supposed to be publicly available, ## please use ``waitForSingleObject()``. + + checkShutdownInProgress() + let loop = getThreadDispatcher() var ovl = RefCustomOverlapped(data: CompletionData(cb: cb)) @@ -457,7 +466,11 @@ elif defined(windows): ## Registers callback ``cb`` to be called when process with process ## identifier ``pid`` exited. Returns process identifier, which can be ## used to clear process callback via ``removeProcess``. + + checkShutdownInProgress() + doAssert(pid > 0, "Process identifier must be positive integer") + let hProcess = openProcess(SYNCHRONIZE, WINBOOL(0), DWORD(pid)) flags = WT_EXECUTEINWAITTHREAD or WT_EXECUTEONLYONCE @@ -533,6 +546,9 @@ elif defined(windows): ## ## NOTE: On Windows only subset of signals are supported: SIGINT, SIGTERM, ## SIGQUIT + + checkShutdownInProgress() + const supportedSignals = [SIGINT, SIGTERM, SIGQUIT] doAssert(cint(signal) in supportedSignals, "Signal is not supported") let loop = getThreadDispatcher() @@ -598,7 +614,7 @@ elif defined(windows): # Moving expired timers to `loop.callbacks` and calculate timeout loop.processTimersGetTimeout(curTimeout) - let networkEventsCount = + loop.networkEventsCount = if isNil(loop.getQueuedCompletionStatusEx): let res = getQueuedCompletionStatus( loop.ioPort, @@ -635,7 +651,7 @@ elif defined(windows): else: int(eventsReceived) - for i in 0 ..< networkEventsCount: + for i in 0 ..< loop.networkEventsCount: var customOverlapped = PtrCustomOverlapped(events[i].lpOverlapped) customOverlapped.data.errCode = block: @@ -654,7 +670,7 @@ elif defined(windows): # We move idle callbacks to `loop.callbacks` only if there no pending # network events. - if networkEventsCount == 0: + if loop.networkEventsCount == 0: loop.processIdlers() # We move tick callbacks to `loop.callbacks` always. @@ -697,6 +713,19 @@ elif defined(windows): if not(isNil(aftercb)): loop.callbacks.addLast(AsyncCallback(function: aftercb, udata: param)) + proc safeCloseHandle(h: HANDLE): Result[void, string] = + let res = closeHandle(h) + if res == 0: # WINBOOL FALSE + return err("Failed to close handle error code: " & osErrorMsg(osLastError())) + ok() + + proc closeDispatcher*(loop: PDispatcher): Result[void, string] = + ? safeCloseHandle(loop.ioPort) + for i in loop.handles.items: + closeHandle(i) + loop.handles.clear() + ok() + proc unregisterAndCloseFd*(fd: AsyncFD): Result[void, OSErrorCode] = ## Unregister from system queue and close asynchronous socket. ## @@ -749,6 +778,8 @@ elif defined(macosx) or defined(freebsd) or defined(netbsd) or "Could not initialize selector") res.get() + initShutdownInProgressToFalse() ## defined in shutdown.nim + var res = PDispatcher( selector: selector, timers: initHeapQueue[TimerCallback](), @@ -777,6 +808,7 @@ elif defined(macosx) or defined(freebsd) or defined(netbsd) or proc register2*(fd: AsyncFD): Result[void, OSErrorCode] = ## Register file descriptor ``fd`` in thread's dispatcher. + checkShutdownInProgress() var data: SelectorData getThreadDispatcher().selector.registerHandle2(cint(fd), {}, data) @@ -788,6 +820,8 @@ elif defined(macosx) or defined(freebsd) or defined(netbsd) or udata: pointer = nil): Result[void, OSErrorCode] = ## Start watching the file descriptor ``fd`` for read availability and then ## call the callback ``cb`` with specified argument ``udata``. + checkShutdownInProgress() + let loop = getThreadDispatcher() var newEvents = {Event.Read} withData(loop.selector, cint(fd), adata) do: @@ -816,6 +850,7 @@ elif defined(macosx) or defined(freebsd) or defined(netbsd) or udata: pointer = nil): Result[void, OSErrorCode] = ## Start watching the file descriptor ``fd`` for write availability and then ## call the callback ``cb`` with specified argument ``udata``. + checkShutdownInProgress() let loop = getThreadDispatcher() var newEvents = {Event.Write} withData(loop.selector, cint(fd), adata) do: @@ -936,6 +971,14 @@ elif defined(macosx) or defined(freebsd) or defined(netbsd) or ## You can execute ``aftercb`` before actual socket close operation. closeSocket(fd, aftercb) + proc closeDispatcher*(loop: PDispatcher): Result[void, string] = + ## Close selector associated with current thread's dispatcher. + try: + loop.selector.close() + except IOSelectorsException as e: + return err("Exception in closeDispatcher: " & e.msg) + ok() + when chronosEventEngine in ["epoll", "kqueue"]: type ProcessHandle* = distinct int @@ -950,6 +993,8 @@ elif defined(macosx) or defined(freebsd) or defined(netbsd) or ## callback ``cb`` with specified argument ``udata``. Returns signal ## identifier code, which can be used to remove signal callback ## via ``removeSignal``. + checkShutdownInProgress() + let loop = getThreadDispatcher() var data: SelectorData let sigfd = ? loop.selector.registerSignal(signal, data) @@ -967,6 +1012,8 @@ elif defined(macosx) or defined(freebsd) or defined(netbsd) or ## Registers callback ``cb`` to be called when process with process ## identifier ``pid`` exited. Returns process' descriptor, which can be ## used to clear process callback via ``removeProcess``. + checkShutdownInProgress() + let loop = getThreadDispatcher() var data: SelectorData let procfd = ? loop.selector.registerProcess(pid, data) @@ -1026,14 +1073,14 @@ elif defined(macosx) or defined(freebsd) or defined(netbsd) or loop.processTimersGetTimeout(curTimeout) # Processing IO descriptors and all hardware events. - let count = + loop.networkEventsCount = block: let res = loop.selector.selectInto2(curTimeout, loop.keys) if res.isErr(): raiseOsDefect(res.error(), "poll(): Unable to get OS events") res.get() - for i in 0 ..< count: + for i in 0 ..< loop.networkEventsCount: let fd = loop.keys[i].fd let events = loop.keys[i].events @@ -1062,7 +1109,7 @@ elif defined(macosx) or defined(freebsd) or defined(netbsd) or # We move idle callbacks to `loop.callbacks` only if there no pending # network events. - if count == 0: + if loop.networkEventsCount == 0: loop.processIdlers() # We move tick callbacks to `loop.callbacks` always. @@ -1104,8 +1151,13 @@ proc setTimer*(at: Moment, cb: CallbackFunc, udata: pointer = nil): TimerCallback = ## Arrange for the callback ``cb`` to be called at the given absolute ## timestamp ``at``. You can also pass ``udata`` to callback. + let finishAt = if isShutdownInProgress(): + ## Schedule timer to now so it will be executed ASAP. + Moment.now() + else: + at let loop = getThreadDispatcher() - result = TimerCallback(finishAt: at, + result = TimerCallback(finishAt: finishAt, function: AsyncCallback(function: cb, udata: udata)) loop.timers.push(result) @@ -1206,6 +1258,7 @@ proc internalCallTick*(cbproc: CallbackFunc) = proc runForever*() = ## Begins a never ending global dispatcher poll loop. ## Raises different exceptions depending on the platform. + ## It is expected to run for the entire lifetime of the process. while true: poll() diff --git a/chronos/internal/asyncfutures.nim b/chronos/internal/asyncfutures.nim index 51622f14d..7e5f3489c 100644 --- a/chronos/internal/asyncfutures.nim +++ b/chronos/internal/asyncfutures.nim @@ -17,7 +17,7 @@ import std/[sequtils, macros] import stew/base10 import ./[asyncengine, raisesfutures] -import ../[config, futures] +import ../[config, futures, shutdown] export raisesfutures.Raising, raisesfutures.InternalRaisesFuture, @@ -629,7 +629,10 @@ proc pollFor[F: Future | InternalRaisesFuture](fut: F): F {.raises: [].} = # # Must not be called recursively (from inside `async` procedures). # - # See alse `awaitne`. + # The process may eventually start another thread after this loop's completion. + # Therefore, the dispatcher's resources are cleaned up in the end. + # + # See also `awaitne`. if not(fut.finished()): var finished = false # Ensure that callbacks currently scheduled on the future run before returning @@ -641,6 +644,20 @@ proc pollFor[F: Future | InternalRaisesFuture](fut: F): F {.raises: [].} = fut +proc gracefulShutdown*(): Result[void, string] {.raises: [].} = + ## Continues polling the dispatcher until shutdown completion, then + ## performs final cleanup of all dispatcher resources. + ## + ## This routine shall be called only after `pollFor` has completed. Upon + ## invocation, all streams are assumed to have been closed. + + setShutdownInProgress() + let disp = getThreadDispatcher() + while disp.networkEventsCount > 0: + poll() + + ? disp.closeDispatcher() + proc waitFor*[T: not void](fut: Future[T]): lent T {.raises: [CatchableError].} = ## Blocks the current thread of execution until `fut` has finished, returning ## its value. diff --git a/chronos/ioselects/ioselectors_epoll.nim b/chronos/ioselects/ioselectors_epoll.nim index 2156a390c..ea3527732 100644 --- a/chronos/ioselects/ioselectors_epoll.nim +++ b/chronos/ioselects/ioselectors_epoll.nim @@ -10,6 +10,7 @@ # This module implements Linux epoll(). import std/[deques, tables] import stew/base10 +import ../shutdown {.push raises: [].} @@ -94,6 +95,7 @@ proc freeProcess[T](s: Selector[T], ident: int32) = s.processes.del(ident) proc new*(t: typedesc[Selector], T: typedesc): SelectResult[Selector[T]] = + checkShutdownInProgress() var nmask: Sigset if sigemptyset(nmask) < 0: return err(osLastError()) @@ -123,6 +125,7 @@ proc close2*[T](s: Selector[T]): SelectResult[void] = ok() proc new*(t: typedesc[SelectEvent]): SelectResult[SelectEvent] = + checkShutdownInProgress() let eFd = eventfd(0, EFD_CLOEXEC or EFD_NONBLOCK) if eFd == -1: return err(osLastError()) @@ -131,6 +134,7 @@ proc new*(t: typedesc[SelectEvent]): SelectResult[SelectEvent] = ok(res) proc trigger2*(event: SelectEvent): SelectResult[void] = + checkShutdownInProgress() var data: uint64 = 1 let res = handleEintr(osdefs.write(event.efd, addr data, sizeof(uint64))) if res == -1: @@ -160,6 +164,7 @@ proc init(t: typedesc[EpollEvent], fdi: cint, events: set[Event]): EpollEvent = proc registerHandle2*[T](s: Selector[T], fd: cint, events: set[Event], data: T): SelectResult[void] = + checkShutdownInProgress() let skey = SelectorKey[T](ident: fd, events: events, param: 0, data: data) s.addKey(fd, skey) @@ -316,6 +321,7 @@ proc registerSignal*[T](s: Selector[T], signal: int, proc registerTimer2*[T](s: Selector[T], timeout: int, oneshot: bool, data: T): SelectResult[cint] = + checkShutdownInProgress() let timerFd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC or TFD_NONBLOCK) if timerFd == -1: return err(osLastError()) @@ -369,6 +375,7 @@ proc registerTimer2*[T](s: Selector[T], timeout: int, oneshot: bool, proc registerEvent2*[T](s: Selector[T], ev: SelectEvent, data: T): SelectResult[cint] = + checkShutdownInProgress() doAssert(not(isNil(ev))) let key = SelectorKey[T](ident: ev.efd, events: {Event.User}, @@ -392,6 +399,7 @@ template checkPid(pid: int) = "Invalid process idientified (pid) value") proc registerProcess*[T](s: Selector, pid: int, data: T): SelectResult[cint] = + checkShutdownInProgress() checkPid(pid) let @@ -626,6 +634,7 @@ proc checkProcesses[T](s: Selector[T]) = proc selectInto2*[T](s: Selector[T], timeout: int, readyKeys: var openArray[ReadyKey] ): SelectResult[int] = + checkShutdownInProgress() var queueEvents: array[chronosEventsCount, EpollEvent] k: int = 0 @@ -668,6 +677,7 @@ proc selectInto2*[T](s: Selector[T], timeout: int, ok(k) proc select2*[T](s: Selector[T], timeout: int): SelectResult[seq[ReadyKey]] = + checkShutdownInProgress() var res = newSeq[ReadyKey](chronosEventsCount) let count = ? selectInto2(s, timeout, res) res.setLen(count) diff --git a/chronos/ioselects/ioselectors_kqueue.nim b/chronos/ioselects/ioselectors_kqueue.nim index e39f96892..d45a853bf 100644 --- a/chronos/ioselects/ioselectors_kqueue.nim +++ b/chronos/ioselects/ioselectors_kqueue.nim @@ -12,6 +12,7 @@ import std/[kqueue, deques, tables] import stew/base10 +import ../shutdown const # SIG_IGN and SIG_DFL declared in posix.nim as variables, but we need them @@ -101,6 +102,7 @@ template getUdata(event: KEvent): int32 = cast[int32](uint32(udata)) proc new*(t: typedesc[Selector], T: typedesc): SelectResult[Selector[T]] = + checkShutdownInProgress() let kqFd = block: let res = handleEintr(kqueue()) @@ -126,6 +128,7 @@ proc close2*[T](s: Selector[T]): SelectResult[void] = ok() proc new*(t: typedesc[SelectEvent]): SelectResult[SelectEvent] = + checkShutdownInProgress() var fds: array[2, cint] when declared(pipe2): if osdefs.pipe2(fds, osdefs.O_NONBLOCK or osdefs.O_CLOEXEC) == -1: @@ -156,6 +159,7 @@ proc new*(t: typedesc[SelectEvent]): SelectResult[SelectEvent] = ok(res) proc trigger2*(event: SelectEvent): SelectResult[void] = + checkShutdownInProgress() var data: uint64 = 1 let res = handleEintr(osdefs.write(event.wfd, addr data, sizeof(uint64))) if res == -1: @@ -190,6 +194,7 @@ template modifyKQueue(changes: var openArray[KEvent], index: int, nident: uint, proc registerHandle2*[T](s: Selector[T], fd: cint, events: set[Event], data: T): SelectResult[void] = + checkShutdownInProgress() let selectorKey = SelectorKey[T](ident: fd, events: events, param: 0, data: data) s.addKey(fd, selectorKey) @@ -298,6 +303,7 @@ template checkSignal(signal: int) = proc registerSignal*[T](s: Selector[T], signal: int, data: T): SelectResult[cint] = + checkShutdownInProgress() checkSignal(signal) let @@ -336,6 +342,7 @@ template checkPid(pid: int) = proc registerProcess*[T](s: Selector[T], pid: int, data: T): SelectResult[cint] = + checkShutdownInProgress() checkPid(pid) let @@ -357,6 +364,7 @@ proc registerProcess*[T](s: Selector[T], pid: int, proc registerEvent2*[T](s: Selector[T], ev: SelectEvent, data: T): SelectResult[cint] = + checkShutdownInProgress() doAssert(not(isNil(ev))) let selectorKey = SelectorKey[T](ident: ev.rfd, events: {Event.User}, @@ -391,6 +399,7 @@ template processVnodeEvents(events: set[Event]): cuint = proc registerVnode2*[T](s: Selector[T], fd: cint, events: set[Event], data: T): SelectResult[cint] = + checkShutdownInProgress() let events = {Event.Vnode} + events fflags = processVnodeEvents(events) @@ -557,6 +566,7 @@ proc prepareKey[T](s: Selector[T], event: KEvent): Opt[ReadyKey] = proc selectInto2*[T](s: Selector[T], timeout: int, readyKeys: var openArray[ReadyKey] ): SelectResult[int] = + checkShutdownInProgress() var tv: Timespec queueEvents: array[chronosEventsCount, KEvent] @@ -601,6 +611,7 @@ proc selectInto2*[T](s: Selector[T], timeout: int, proc select2*[T](s: Selector[T], timeout: int): Result[seq[ReadyKey], OSErrorCode] = + checkShutdownInProgress() var res = newSeq[ReadyKey](chronosEventsCount) let count = ? selectInto2(s, timeout, res) res.setLen(count) diff --git a/chronos/ioselects/ioselectors_poll.nim b/chronos/ioselects/ioselectors_poll.nim index 51f21bbfd..6abcc422e 100644 --- a/chronos/ioselects/ioselectors_poll.nim +++ b/chronos/ioselects/ioselectors_poll.nim @@ -10,6 +10,7 @@ # This module implements Posix poll(). import std/tables import stew/base10 +import ../shutdown {.push raises: [].} @@ -49,6 +50,7 @@ proc freeKey[T](s: Selector[T], key: int32) = s.fds.del(key) proc new*(t: typedesc[Selector], T: typedesc): SelectResult[Selector[T]] = + checkShutdownInProgress() let selector = Selector[T]( fds: initTable[int32, SelectorKey[T]](chronosInitialSize) ) @@ -56,9 +58,10 @@ proc new*(t: typedesc[Selector], T: typedesc): SelectResult[Selector[T]] = proc close2*[T](s: Selector[T]): SelectResult[void] = s.fds.clear() - s.pollfds.clear() + s.pollfds.setLen(0) proc new*(t: typedesc[SelectEvent]): SelectResult[SelectEvent] = + checkShutdownInProgress() let flags = {DescriptorFlag.NonBlock, DescriptorFlag.CloseOnExec} let pipes = ? createOsPipe(flags, flags) var res = cast[SelectEvent](allocShared0(sizeof(SelectEventImpl))) @@ -67,6 +70,7 @@ proc new*(t: typedesc[SelectEvent]): SelectResult[SelectEvent] = ok(res) proc trigger2*(event: SelectEvent): SelectResult[void] = + checkShutdownInProgress() var data: uint64 = 1 let res = handleEintr(osdefs.write(event.wfd, addr data, sizeof(uint64))) if res == -1: @@ -126,6 +130,7 @@ template pollRemove[T](s: Selector[T], sock: cint) = proc registerHandle2*[T](s: Selector[T], fd: cint, events: set[Event], data: T): SelectResult[void] = + checkShutdownInProgress() let skey = SelectorKey[T](ident: fd, events: events, param: 0, data: data) s.addKey(fd, skey) @@ -156,6 +161,7 @@ proc updateHandle2*[T](s: Selector[T], fd: cint, proc registerEvent2*[T](s: Selector[T], ev: SelectEvent, data: T): SelectResult[cint] = + checkShutdownInProgress() doAssert(not(isNil(ev))) let key = SelectorKey[T](ident: ev.rfd, events: {Event.User}, @@ -215,6 +221,7 @@ proc prepareKey[T](s: Selector[T], event: var TPollfd): Opt[ReadyKey] = proc selectInto2*[T](s: Selector[T], timeout: int, readyKeys: var openArray[ReadyKey]): SelectResult[int] = + checkShutdownInProgress() var k = 0 verifySelectParams(timeout, -1, int(high(cint))) @@ -244,6 +251,7 @@ proc selectInto2*[T](s: Selector[T], timeout: int, ok(k) proc select2*[T](s: Selector[T], timeout: int): SelectResult[seq[ReadyKey]] = + checkShutdownInProgress() var res = newSeq[ReadyKey](chronosEventsCount) let count = ? selectInto2(s, timeout, res) res.setLen(count) diff --git a/chronos/oserrno.nim b/chronos/oserrno.nim index 2a9f82ce5..45609ac52 100644 --- a/chronos/oserrno.nim +++ b/chronos/oserrno.nim @@ -1334,6 +1334,7 @@ elif defined(windows): WSAECONNABORTED* = OSErrorCode(10053) WSAECONNRESET* = OSErrorCode(10054) WSAENOBUFS* = OSErrorCode(10055) + WSAESHUTDOWN* = OSErrorCode(10058) WSAETIMEDOUT* = OSErrorCode(10060) WSAEADDRINUSE* = OSErrorCode(10048) WSAEDISCON* = OSErrorCode(10101) diff --git a/chronos/shutdown.nim b/chronos/shutdown.nim new file mode 100644 index 000000000..4129f0115 --- /dev/null +++ b/chronos/shutdown.nim @@ -0,0 +1,31 @@ +## Manages nim-chronos graceful shutdown procedures. +## +## Phases: +## 1. Block new work requests. +## 2. Signal all current work to start ending. +## 3. Wait until all tasks are completed. +## 4. Close resources. +## + +import ./oserrno + +## Flag aimed to control whether shutdown is in progress. +## When set to true, new work requests should be rejected. +var shutdownInProgress{.threadvar.}: bool + +proc initShutdownInProgressToFalse*() = + shutdownInProgress = false + +proc setShutdownInProgress*() = + shutdownInProgress = true + +proc isShutdownInProgress*(): bool = + return shutdownInProgress + +template checkShutdownInProgress*(): untyped = + if isShutdownInProgress(): + when defined(windows): + return err(WSAESHUTDOWN) + else: + return err(ESHUTDOWN) +