exception tracking (#166)
* exception tracking
This PR adds minimal exception tracking to chronos, moving the goalpost
one step further.
In particular, it becomes invalid to raise exceptions from `callSoon`
callbacks: this is critical for writing correct error handling because
there's no reasonable way that a user of chronos can possibly _reason_
about exceptions coming out of there: the event loop will be in an
indeterminite state when the loop is executing an _random_ callback.
As expected, there are several issues in the error handling of chronos:
in particular, it will end up in an inconsistent internal state whenever
the selector loop operations fail, because the internal state update
functions are not written in an exception-safe way. This PR turns this
into a Defect, which probably is not the optimal way of handling things
- expect more work to be done here.
Some API have no way of reporting back errors to callers - for example,
when something fails in the accept loop, there's not much it can do, and
no way to report it back to the user of the API - this has been fixed
with the new accept flow - the old one should be deprecated.
Finally, there is information loss in the API: in composite operations
like `poll` and `waitFor` there's no way to differentiate internal
errors from user-level errors originating from callbacks.
* store `CatchableError` in future
* annotate proc's with correct raises information
* `selectors2` to avoid non-CatchableError IOSelectorsException
* `$` should never raise
* remove unnecessary gcsafe annotations
* fix exceptions leaking out of timer waits
* fix some imports
* functions must signal raising the union of all exceptions across all
platforms to enable cross-platform code
* switch to unittest2
* add `selectors2` which supercedes the std library version and fixes
several exception handling issues in there
* fixes
* docs, platform-independent eh specifiers for some functions
* add feature flag for strict exception mode
also bump version to 3.0.0 - _most_ existing code should be compatible
with this version of exception handling but some things might need
fixing - callbacks, existing raises specifications etc.
* fix AsyncCheck for non-void T
2021-03-24 09:08:33 +00:00
|
|
|
#
|
|
|
|
#
|
|
|
|
# Nim's Runtime Library
|
|
|
|
# (c) Copyright 2016 Eugene Kabanov
|
|
|
|
#
|
|
|
|
# See the file "copying.txt", included in this
|
|
|
|
# distribution, for details about the copyright.
|
|
|
|
#
|
|
|
|
|
|
|
|
## This module allows high-level and efficient I/O multiplexing.
|
|
|
|
##
|
|
|
|
## Supported OS primitives: ``epoll``, ``kqueue``, ``poll`` and
|
|
|
|
## Windows ``select``.
|
|
|
|
##
|
|
|
|
## To use threadsafe version of this module, it needs to be compiled
|
|
|
|
## with both ``-d:threadsafe`` and ``--threads:on`` options.
|
|
|
|
##
|
|
|
|
## Supported features: files, sockets, pipes, timers, processes, signals
|
|
|
|
## and user events.
|
|
|
|
##
|
|
|
|
## Fully supported OS: MacOSX, FreeBSD, OpenBSD, NetBSD, Linux (except
|
|
|
|
## for Android).
|
|
|
|
##
|
|
|
|
## Partially supported OS: Windows (only sockets and user events),
|
|
|
|
## Solaris (files, sockets, handles and user events).
|
|
|
|
## Android (files, sockets, handles and user events).
|
|
|
|
##
|
|
|
|
## TODO: ``/dev/poll``, ``event ports`` and filesystem events.
|
|
|
|
|
|
|
|
# Based on std/selectors, but with stricter exception handling and effect
|
|
|
|
# support - changes could potentially be backported to nim but are not
|
|
|
|
# backwards-compatible.
|
|
|
|
|
|
|
|
import os, nativesockets
|
|
|
|
|
|
|
|
const hasThreadSupport = compileOption("threads") and defined(threadsafe)
|
|
|
|
|
|
|
|
const ioselSupportedPlatform* = defined(macosx) or defined(freebsd) or
|
|
|
|
defined(netbsd) or defined(openbsd) or
|
|
|
|
defined(dragonfly) or
|
|
|
|
(defined(linux) and not defined(android))
|
|
|
|
## This constant is used to determine whether the destination platform is
|
|
|
|
## fully supported by ``ioselectors`` module.
|
|
|
|
|
|
|
|
const bsdPlatform = defined(macosx) or defined(freebsd) or
|
|
|
|
defined(netbsd) or defined(openbsd) or
|
|
|
|
defined(dragonfly)
|
|
|
|
|
|
|
|
when defined(nimdoc):
|
|
|
|
type
|
|
|
|
Selector*[T] = ref object
|
|
|
|
## An object which holds descriptors to be checked for read/write status
|
|
|
|
|
|
|
|
Event* {.pure.} = enum
|
|
|
|
## An enum which hold event types
|
|
|
|
Read, ## Descriptor is available for read
|
|
|
|
Write, ## Descriptor is available for write
|
|
|
|
Timer, ## Timer descriptor is completed
|
|
|
|
Signal, ## Signal is raised
|
|
|
|
Process, ## Process is finished
|
|
|
|
Vnode, ## BSD specific file change
|
|
|
|
User, ## User event is raised
|
|
|
|
Error, ## Error occurred while waiting for descriptor
|
|
|
|
VnodeWrite, ## NOTE_WRITE (BSD specific, write to file occurred)
|
|
|
|
VnodeDelete, ## NOTE_DELETE (BSD specific, unlink of file occurred)
|
|
|
|
VnodeExtend, ## NOTE_EXTEND (BSD specific, file extended)
|
|
|
|
VnodeAttrib, ## NOTE_ATTRIB (BSD specific, file attributes changed)
|
|
|
|
VnodeLink, ## NOTE_LINK (BSD specific, file link count changed)
|
|
|
|
VnodeRename, ## NOTE_RENAME (BSD specific, file renamed)
|
|
|
|
VnodeRevoke ## NOTE_REVOKE (BSD specific, file revoke occurred)
|
|
|
|
|
|
|
|
ReadyKey* = object
|
|
|
|
## An object which holds result for descriptor
|
|
|
|
fd* : int ## file/socket descriptor
|
|
|
|
events*: set[Event] ## set of events
|
|
|
|
errorCode*: OSErrorCode ## additional error code information for
|
|
|
|
## Error events
|
|
|
|
|
|
|
|
SelectEvent* = object
|
|
|
|
## An object which holds user defined event
|
|
|
|
|
|
|
|
proc newSelector*[T](): Selector[T] =
|
|
|
|
## Creates a new selector
|
|
|
|
|
|
|
|
proc close*[T](s: Selector[T]) =
|
|
|
|
## Closes the selector.
|
|
|
|
|
|
|
|
proc registerHandle*[T](s: Selector[T], fd: int | SocketHandle,
|
|
|
|
events: set[Event], data: T) =
|
|
|
|
## Registers file/socket descriptor ``fd`` to selector ``s``
|
|
|
|
## with events set in ``events``. The ``data`` is application-defined
|
|
|
|
## data, which will be passed when an event is triggered.
|
|
|
|
|
|
|
|
proc updateHandle*[T](s: Selector[T], fd: int | SocketHandle,
|
|
|
|
events: set[Event]) =
|
|
|
|
## Update file/socket descriptor ``fd``, registered in selector
|
|
|
|
## ``s`` with new events set ``event``.
|
|
|
|
|
|
|
|
proc registerTimer*[T](s: Selector[T], timeout: int, oneshot: bool,
|
|
|
|
data: T): int {.discardable.} =
|
|
|
|
## Registers timer notification with ``timeout`` (in milliseconds)
|
|
|
|
## to selector ``s``.
|
|
|
|
##
|
|
|
|
## If ``oneshot`` is ``true``, timer will be notified only once.
|
|
|
|
##
|
|
|
|
## Set ``oneshot`` to ``false`` if you want periodic notifications.
|
|
|
|
##
|
|
|
|
## The ``data`` is application-defined data, which will be passed, when
|
|
|
|
## the timer is triggered.
|
|
|
|
##
|
|
|
|
## Returns the file descriptor for the registered timer.
|
|
|
|
|
|
|
|
proc registerSignal*[T](s: Selector[T], signal: int,
|
|
|
|
data: T): int {.discardable.} =
|
|
|
|
## Registers Unix signal notification with ``signal`` to selector
|
|
|
|
## ``s``.
|
|
|
|
##
|
|
|
|
## The ``data`` is application-defined data, which will be
|
|
|
|
## passed when signal raises.
|
|
|
|
##
|
|
|
|
## Returns the file descriptor for the registered signal.
|
|
|
|
##
|
|
|
|
## **Note:** This function is not supported on ``Windows``.
|
|
|
|
|
|
|
|
proc registerProcess*[T](s: Selector[T], pid: int,
|
|
|
|
data: T): int {.discardable.} =
|
|
|
|
## Registers a process id (pid) notification (when process has
|
|
|
|
## exited) in selector ``s``.
|
|
|
|
##
|
|
|
|
## The ``data`` is application-defined data, which will be passed when
|
|
|
|
## process with ``pid`` has exited.
|
|
|
|
##
|
|
|
|
## Returns the file descriptor for the registered signal.
|
|
|
|
|
|
|
|
proc registerEvent*[T](s: Selector[T], ev: SelectEvent, data: T) =
|
|
|
|
## Registers selector event ``ev`` in selector ``s``.
|
|
|
|
##
|
|
|
|
## The ``data`` is application-defined data, which will be passed when
|
|
|
|
## ``ev`` happens.
|
|
|
|
|
|
|
|
proc registerVnode*[T](s: Selector[T], fd: cint, events: set[Event],
|
|
|
|
data: T) =
|
|
|
|
## Registers selector BSD/MacOSX specific vnode events for file
|
|
|
|
## descriptor ``fd`` and events ``events``.
|
|
|
|
## ``data`` application-defined data, which to be passed, when
|
|
|
|
## vnode event happens.
|
|
|
|
##
|
|
|
|
## **Note:** This function is supported only by BSD and MacOSX.
|
|
|
|
|
|
|
|
proc newSelectEvent*(): SelectEvent =
|
|
|
|
## Creates a new user-defined event.
|
|
|
|
|
|
|
|
proc trigger*(ev: SelectEvent) =
|
|
|
|
## Trigger event ``ev``.
|
|
|
|
|
|
|
|
proc close*(ev: SelectEvent) =
|
|
|
|
## Closes user-defined event ``ev``.
|
|
|
|
|
|
|
|
proc unregister*[T](s: Selector[T], ev: SelectEvent) =
|
|
|
|
## Unregisters user-defined event ``ev`` from selector ``s``.
|
|
|
|
|
|
|
|
proc unregister*[T](s: Selector[T], fd: int|SocketHandle|cint) =
|
|
|
|
## Unregisters file/socket descriptor ``fd`` from selector ``s``.
|
|
|
|
|
|
|
|
proc selectInto*[T](s: Selector[T], timeout: int,
|
2021-12-10 09:55:25 +00:00
|
|
|
results: var openArray[ReadyKey]): int =
|
exception tracking (#166)
* exception tracking
This PR adds minimal exception tracking to chronos, moving the goalpost
one step further.
In particular, it becomes invalid to raise exceptions from `callSoon`
callbacks: this is critical for writing correct error handling because
there's no reasonable way that a user of chronos can possibly _reason_
about exceptions coming out of there: the event loop will be in an
indeterminite state when the loop is executing an _random_ callback.
As expected, there are several issues in the error handling of chronos:
in particular, it will end up in an inconsistent internal state whenever
the selector loop operations fail, because the internal state update
functions are not written in an exception-safe way. This PR turns this
into a Defect, which probably is not the optimal way of handling things
- expect more work to be done here.
Some API have no way of reporting back errors to callers - for example,
when something fails in the accept loop, there's not much it can do, and
no way to report it back to the user of the API - this has been fixed
with the new accept flow - the old one should be deprecated.
Finally, there is information loss in the API: in composite operations
like `poll` and `waitFor` there's no way to differentiate internal
errors from user-level errors originating from callbacks.
* store `CatchableError` in future
* annotate proc's with correct raises information
* `selectors2` to avoid non-CatchableError IOSelectorsException
* `$` should never raise
* remove unnecessary gcsafe annotations
* fix exceptions leaking out of timer waits
* fix some imports
* functions must signal raising the union of all exceptions across all
platforms to enable cross-platform code
* switch to unittest2
* add `selectors2` which supercedes the std library version and fixes
several exception handling issues in there
* fixes
* docs, platform-independent eh specifiers for some functions
* add feature flag for strict exception mode
also bump version to 3.0.0 - _most_ existing code should be compatible
with this version of exception handling but some things might need
fixing - callbacks, existing raises specifications etc.
* fix AsyncCheck for non-void T
2021-03-24 09:08:33 +00:00
|
|
|
## Waits for events registered in selector ``s``.
|
|
|
|
##
|
|
|
|
## The ``timeout`` argument specifies the maximum number of milliseconds
|
|
|
|
## the function will be blocked for if no events are ready. Specifying a
|
|
|
|
## timeout of ``-1`` causes the function to block indefinitely.
|
|
|
|
## All available events will be stored in ``results`` array.
|
|
|
|
##
|
|
|
|
## Returns number of triggered events.
|
|
|
|
|
|
|
|
proc select*[T](s: Selector[T], timeout: int): seq[ReadyKey] =
|
|
|
|
## Waits for events registered in selector ``s``.
|
|
|
|
##
|
|
|
|
## The ``timeout`` argument specifies the maximum number of milliseconds
|
|
|
|
## the function will be blocked for if no events are ready. Specifying a
|
|
|
|
## timeout of ``-1`` causes the function to block indefinitely.
|
|
|
|
##
|
|
|
|
## Returns a list of triggered events.
|
|
|
|
|
|
|
|
proc getData*[T](s: Selector[T], fd: SocketHandle|int): var T =
|
|
|
|
## Retrieves application-defined ``data`` associated with descriptor ``fd``.
|
|
|
|
## If specified descriptor ``fd`` is not registered, empty/default value
|
|
|
|
## will be returned.
|
|
|
|
|
|
|
|
proc setData*[T](s: Selector[T], fd: SocketHandle|int, data: var T): bool =
|
|
|
|
## Associate application-defined ``data`` with descriptor ``fd``.
|
|
|
|
##
|
|
|
|
## Returns ``true``, if data was successfully updated, ``false`` otherwise.
|
|
|
|
|
|
|
|
template isEmpty*[T](s: Selector[T]): bool = # TODO: Why is this a template?
|
|
|
|
## Returns ``true``, if there are no registered events or descriptors
|
|
|
|
## in selector.
|
|
|
|
|
|
|
|
template withData*[T](s: Selector[T], fd: SocketHandle|int, value,
|
|
|
|
body: untyped) =
|
|
|
|
## Retrieves the application-data assigned with descriptor ``fd``
|
|
|
|
## to ``value``. This ``value`` can be modified in the scope of
|
|
|
|
## the ``withData`` call.
|
|
|
|
##
|
|
|
|
## .. code-block:: nim
|
|
|
|
##
|
|
|
|
## s.withData(fd, value) do:
|
|
|
|
## # block is executed only if ``fd`` registered in selector ``s``
|
|
|
|
## value.uid = 1000
|
|
|
|
##
|
|
|
|
|
|
|
|
template withData*[T](s: Selector[T], fd: SocketHandle|int, value,
|
|
|
|
body1, body2: untyped) =
|
|
|
|
## Retrieves the application-data assigned with descriptor ``fd``
|
|
|
|
## to ``value``. This ``value`` can be modified in the scope of
|
|
|
|
## the ``withData`` call.
|
|
|
|
##
|
|
|
|
## .. code-block:: nim
|
|
|
|
##
|
|
|
|
## s.withData(fd, value) do:
|
|
|
|
## # block is executed only if ``fd`` registered in selector ``s``.
|
|
|
|
## value.uid = 1000
|
|
|
|
## do:
|
|
|
|
## # block is executed if ``fd`` not registered in selector ``s``.
|
|
|
|
## raise
|
|
|
|
##
|
|
|
|
|
|
|
|
proc contains*[T](s: Selector[T], fd: SocketHandle|int): bool {.inline.} =
|
|
|
|
## Determines whether selector contains a file descriptor.
|
|
|
|
|
|
|
|
proc getFd*[T](s: Selector[T]): int =
|
|
|
|
## Retrieves the underlying selector's file descriptor.
|
|
|
|
##
|
|
|
|
## For *poll* and *select* selectors ``-1`` is returned.
|
|
|
|
|
|
|
|
else:
|
|
|
|
import strutils
|
|
|
|
when hasThreadSupport:
|
|
|
|
import locks
|
|
|
|
|
|
|
|
type
|
|
|
|
SharedArray[T] = UncheckedArray[T]
|
|
|
|
|
|
|
|
proc allocSharedArray[T](nsize: int): ptr SharedArray[T] =
|
|
|
|
result = cast[ptr SharedArray[T]](allocShared0(sizeof(T) * nsize))
|
|
|
|
|
|
|
|
proc reallocSharedArray[T](sa: ptr SharedArray[T], nsize: int): ptr SharedArray[T] =
|
|
|
|
result = cast[ptr SharedArray[T]](reallocShared(sa, sizeof(T) * nsize))
|
|
|
|
|
|
|
|
proc deallocSharedArray[T](sa: ptr SharedArray[T]) =
|
|
|
|
deallocShared(cast[pointer](sa))
|
|
|
|
type
|
|
|
|
Event* {.pure.} = enum
|
|
|
|
Read, Write, Timer, Signal, Process, Vnode, User, Error, Oneshot,
|
|
|
|
Finished, VnodeWrite, VnodeDelete, VnodeExtend, VnodeAttrib, VnodeLink,
|
|
|
|
VnodeRename, VnodeRevoke
|
|
|
|
|
|
|
|
type
|
|
|
|
IOSelectorsException* = object of CatchableError
|
|
|
|
|
|
|
|
ReadyKey* = object
|
|
|
|
fd* : int
|
|
|
|
events*: set[Event]
|
|
|
|
errorCode*: OSErrorCode
|
|
|
|
|
|
|
|
SelectorKey[T] = object
|
|
|
|
ident: int
|
|
|
|
events: set[Event]
|
|
|
|
param: int
|
|
|
|
data: T
|
|
|
|
|
|
|
|
const
|
|
|
|
InvalidIdent = -1
|
|
|
|
|
|
|
|
proc raiseIOSelectorsError[T](message: T) =
|
|
|
|
var msg = ""
|
|
|
|
when T is string:
|
|
|
|
msg.add(message)
|
|
|
|
elif T is OSErrorCode:
|
|
|
|
msg.add(osErrorMsg(message) & " (code: " & $int(message) & ")")
|
|
|
|
else:
|
|
|
|
msg.add("Internal Error\n")
|
|
|
|
var err = newException(IOSelectorsException, msg)
|
|
|
|
raise err
|
|
|
|
|
|
|
|
proc setNonBlocking(fd: cint) {.inline.} =
|
|
|
|
setBlocking(fd.SocketHandle, false)
|
|
|
|
|
|
|
|
when not defined(windows):
|
|
|
|
import posix
|
|
|
|
|
|
|
|
template setKey(s, pident, pevents, pparam, pdata: untyped) =
|
|
|
|
var skey = addr(s.fds[pident])
|
|
|
|
skey.ident = pident
|
|
|
|
skey.events = pevents
|
|
|
|
skey.param = pparam
|
|
|
|
skey.data = data
|
|
|
|
|
|
|
|
when ioselSupportedPlatform:
|
|
|
|
template blockSignals(newmask: var Sigset, oldmask: var Sigset) =
|
|
|
|
when hasThreadSupport:
|
|
|
|
if posix.pthread_sigmask(SIG_BLOCK, newmask, oldmask) == -1:
|
|
|
|
raiseIOSelectorsError(osLastError())
|
|
|
|
else:
|
|
|
|
if posix.sigprocmask(SIG_BLOCK, newmask, oldmask) == -1:
|
|
|
|
raiseIOSelectorsError(osLastError())
|
|
|
|
|
|
|
|
template unblockSignals(newmask: var Sigset, oldmask: var Sigset) =
|
|
|
|
when hasThreadSupport:
|
|
|
|
if posix.pthread_sigmask(SIG_UNBLOCK, newmask, oldmask) == -1:
|
|
|
|
raiseIOSelectorsError(osLastError())
|
|
|
|
else:
|
|
|
|
if posix.sigprocmask(SIG_UNBLOCK, newmask, oldmask) == -1:
|
|
|
|
raiseIOSelectorsError(osLastError())
|
|
|
|
|
|
|
|
template clearKey[T](key: ptr SelectorKey[T]) =
|
|
|
|
var empty: T
|
|
|
|
key.ident = InvalidIdent
|
|
|
|
key.events = {}
|
|
|
|
key.data = empty
|
|
|
|
|
|
|
|
proc verifySelectParams(timeout: int) =
|
|
|
|
# Timeout of -1 means: wait forever
|
|
|
|
# Anything higher is the time to wait in milliseconds.
|
|
|
|
doAssert(timeout >= -1, "Cannot select with a negative value, got " & $timeout)
|
|
|
|
|
|
|
|
when defined(linux):
|
|
|
|
include ./ioselects/ioselectors_epoll
|
|
|
|
elif bsdPlatform:
|
|
|
|
include ./ioselects/ioselectors_kqueue
|
|
|
|
elif defined(windows):
|
|
|
|
include ./ioselects/ioselectors_select
|
|
|
|
elif defined(solaris):
|
|
|
|
include ./ioselects/ioselectors_poll # need to replace it with event ports
|
|
|
|
elif defined(genode):
|
|
|
|
include ./ioselects/ioselectors_select # TODO: use the native VFS layer
|
|
|
|
elif defined(nintendoswitch):
|
|
|
|
include ./ioselects/ioselectors_select
|
|
|
|
else:
|
|
|
|
include ./ioselects/ioselectors_poll
|
|
|
|
|
|
|
|
proc register*[T](s: Selector[T], fd: int | SocketHandle,
|
|
|
|
events: set[Event], data: T) {.deprecated: "use registerHandle instead".} =
|
|
|
|
## **Deprecated since v0.18.0:** Use ``registerHandle`` instead.
|
|
|
|
s.registerHandle(fd, events, data)
|
|
|
|
|
|
|
|
proc setEvent*(ev: SelectEvent) {.deprecated: "use trigger instead",
|
|
|
|
raises: [Defect, IOSelectorsException].} =
|
|
|
|
## Trigger event ``ev``.
|
|
|
|
##
|
|
|
|
## **Deprecated since v0.18.0:** Use ``trigger`` instead.
|
|
|
|
ev.trigger()
|
|
|
|
|
|
|
|
proc update*[T](s: Selector[T], fd: int | SocketHandle,
|
|
|
|
events: set[Event]) {.deprecated: "use updateHandle instead".} =
|
|
|
|
## Update file/socket descriptor ``fd``, registered in selector
|
|
|
|
## ``s`` with new events set ``event``.
|
|
|
|
##
|
|
|
|
## **Deprecated since v0.18.0:** Use ``updateHandle`` instead.
|
|
|
|
s.updateHandle()
|