2018-05-16 08:22:34 +00:00
|
|
|
#
|
2019-02-06 14:49:11 +00:00
|
|
|
# Chronos Handles
|
|
|
|
# (c) Copyright 2018-Present
|
2018-05-16 08:22:34 +00:00
|
|
|
# Status Research & Development GmbH
|
|
|
|
#
|
|
|
|
# Licensed under either of
|
|
|
|
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
|
|
|
# MIT license (LICENSE-MIT)
|
|
|
|
|
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
|
|
|
{.push raises: [Defect].}
|
|
|
|
|
2021-05-07 20:52:24 +00:00
|
|
|
import std/[net, nativesockets]
|
|
|
|
import ./asyncloop
|
2018-05-16 08:22:34 +00:00
|
|
|
|
|
|
|
when defined(windows):
|
2019-10-24 13:21:31 +00:00
|
|
|
import os, winlean
|
2018-05-16 08:22:34 +00:00
|
|
|
const
|
2018-05-21 21:52:57 +00:00
|
|
|
asyncInvalidSocket* = AsyncFD(-1)
|
2018-08-27 18:41:11 +00:00
|
|
|
TCP_NODELAY* = 1
|
|
|
|
IPPROTO_TCP* = 6
|
2019-07-15 09:59:42 +00:00
|
|
|
PIPE_TYPE_BYTE = 0x00000000'i32
|
|
|
|
PIPE_READMODE_BYTE = 0x00000000'i32
|
|
|
|
PIPE_WAIT = 0x00000000'i32
|
|
|
|
DEFAULT_PIPE_SIZE = 65536'i32
|
|
|
|
ERROR_PIPE_CONNECTED = 535
|
|
|
|
ERROR_PIPE_BUSY = 231
|
|
|
|
pipeHeaderName = r"\\.\pipe\chronos\"
|
|
|
|
|
|
|
|
proc connectNamedPipe(hNamedPipe: Handle, lpOverlapped: pointer): WINBOOL
|
|
|
|
{.importc: "ConnectNamedPipe", stdcall, dynlib: "kernel32".}
|
2018-05-16 08:22:34 +00:00
|
|
|
else:
|
2020-06-24 08:21:52 +00:00
|
|
|
import os, posix
|
2018-05-16 08:22:34 +00:00
|
|
|
const
|
|
|
|
asyncInvalidSocket* = AsyncFD(posix.INVALID_SOCKET)
|
2018-08-27 18:41:11 +00:00
|
|
|
TCP_NODELAY* = 1
|
|
|
|
IPPROTO_TCP* = 6
|
2018-05-16 08:22:34 +00:00
|
|
|
|
2019-07-15 09:59:42 +00:00
|
|
|
const
|
|
|
|
asyncInvalidPipe* = asyncInvalidSocket
|
|
|
|
|
2018-05-16 08:22:34 +00:00
|
|
|
proc setSocketBlocking*(s: SocketHandle, blocking: bool): bool =
|
|
|
|
## Sets blocking mode on socket.
|
|
|
|
when defined(windows):
|
|
|
|
var mode = clong(ord(not blocking))
|
|
|
|
if ioctlsocket(s, FIONBIO, addr(mode)) == -1:
|
2021-05-07 20:52:24 +00:00
|
|
|
false
|
|
|
|
else:
|
|
|
|
true
|
2018-05-16 08:22:34 +00:00
|
|
|
else:
|
2021-05-07 20:52:24 +00:00
|
|
|
let x: int = fcntl(s, F_GETFL, 0)
|
2018-05-16 08:22:34 +00:00
|
|
|
if x == -1:
|
2021-05-07 20:52:24 +00:00
|
|
|
false
|
2018-05-16 08:22:34 +00:00
|
|
|
else:
|
2021-05-07 20:52:24 +00:00
|
|
|
let mode = if blocking: x and not O_NONBLOCK else: x or O_NONBLOCK
|
2018-05-16 08:22:34 +00:00
|
|
|
if fcntl(s, F_SETFL, mode) == -1:
|
2021-05-07 20:52:24 +00:00
|
|
|
false
|
|
|
|
else:
|
|
|
|
true
|
2018-05-16 08:22:34 +00:00
|
|
|
|
2018-05-21 21:52:57 +00:00
|
|
|
proc setSockOpt*(socket: AsyncFD, level, optname, optval: int): bool =
|
2018-05-16 08:22:34 +00:00
|
|
|
## `setsockopt()` for integer options.
|
|
|
|
## Returns ``true`` on success, ``false`` on error.
|
|
|
|
var value = cint(optval)
|
2021-05-07 20:52:24 +00:00
|
|
|
setsockopt(SocketHandle(socket), cint(level), cint(optname),
|
|
|
|
addr(value), SockLen(sizeof(value))) >= cint(0)
|
2018-05-29 09:59:39 +00:00
|
|
|
|
2019-05-07 20:11:40 +00:00
|
|
|
proc setSockOpt*(socket: AsyncFD, level, optname: int, value: pointer,
|
|
|
|
valuelen: int): bool =
|
|
|
|
## `setsockopt()` for custom options (pointer and length).
|
|
|
|
## Returns ``true`` on success, ``false`` on error.
|
2021-05-07 20:52:24 +00:00
|
|
|
setsockopt(SocketHandle(socket), cint(level), cint(optname), value,
|
|
|
|
SockLen(valuelen)) >= cint(0)
|
2019-05-07 20:11:40 +00:00
|
|
|
|
2018-05-21 21:52:57 +00:00
|
|
|
proc getSockOpt*(socket: AsyncFD, level, optname: int, value: var int): bool =
|
2018-05-16 08:22:34 +00:00
|
|
|
## `getsockopt()` for integer options.
|
2019-05-07 20:11:40 +00:00
|
|
|
## Returns ``true`` on success, ``false`` on error.
|
2018-05-16 08:22:34 +00:00
|
|
|
var res: cint
|
2019-05-10 05:27:05 +00:00
|
|
|
var size = SockLen(sizeof(res))
|
2018-05-16 08:22:34 +00:00
|
|
|
if getsockopt(SocketHandle(socket), cint(level), cint(optname),
|
2019-05-10 05:27:05 +00:00
|
|
|
addr(res), addr(size)) >= cint(0):
|
|
|
|
value = int(res)
|
2021-05-07 20:52:24 +00:00
|
|
|
true
|
|
|
|
else:
|
|
|
|
false
|
2018-05-16 08:22:34 +00:00
|
|
|
|
2019-05-07 20:11:40 +00:00
|
|
|
proc getSockOpt*(socket: AsyncFD, level, optname: int, value: pointer,
|
|
|
|
valuelen: var int): bool =
|
|
|
|
## `getsockopt()` for custom options (pointer and length).
|
|
|
|
## Returns ``true`` on success, ``false`` on error.
|
2021-05-07 20:52:24 +00:00
|
|
|
getsockopt(SocketHandle(socket), cint(level), cint(optname),
|
2021-12-08 14:58:24 +00:00
|
|
|
value, cast[ptr SockLen](addr valuelen)) >= cint(0)
|
2019-05-07 20:11:40 +00:00
|
|
|
|
2018-05-21 21:52:57 +00:00
|
|
|
proc getSocketError*(socket: AsyncFD, err: var int): bool =
|
|
|
|
## Recover error code associated with socket handle ``socket``.
|
2021-05-07 20:52:24 +00:00
|
|
|
getSockOpt(socket, cint(SOL_SOCKET), cint(SO_ERROR), err)
|
2018-05-16 08:22:34 +00:00
|
|
|
|
|
|
|
proc createAsyncSocket*(domain: Domain, sockType: SockType,
|
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
|
|
|
protocol: Protocol): AsyncFD {.
|
|
|
|
raises: [Defect, CatchableError].} =
|
2018-05-16 08:22:34 +00:00
|
|
|
## Creates new asynchronous socket.
|
|
|
|
## Returns ``asyncInvalidSocket`` on error.
|
|
|
|
let handle = createNativeSocket(domain, sockType, protocol)
|
|
|
|
if handle == osInvalidSocket:
|
|
|
|
return asyncInvalidSocket
|
|
|
|
if not setSocketBlocking(handle, false):
|
|
|
|
close(handle)
|
|
|
|
return asyncInvalidSocket
|
2021-05-07 20:52:24 +00:00
|
|
|
register(AsyncFD(handle))
|
|
|
|
AsyncFD(handle)
|
2018-05-16 08:22:34 +00:00
|
|
|
|
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
|
|
|
proc wrapAsyncSocket*(sock: SocketHandle): AsyncFD {.
|
|
|
|
raises: [Defect, CatchableError].} =
|
2018-05-21 21:52:57 +00:00
|
|
|
## Wraps socket to asynchronous socket handle.
|
2018-05-16 08:22:34 +00:00
|
|
|
## Return ``asyncInvalidSocket`` on error.
|
|
|
|
if not setSocketBlocking(sock, false):
|
|
|
|
close(sock)
|
|
|
|
return asyncInvalidSocket
|
2021-05-07 20:52:24 +00:00
|
|
|
register(AsyncFD(sock))
|
|
|
|
AsyncFD(sock)
|
2019-07-15 09:59:42 +00:00
|
|
|
|
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
|
|
|
proc getMaxOpenFiles*(): int {.raises: [Defect, OSError].} =
|
2020-06-24 08:21:52 +00:00
|
|
|
## Returns maximum file descriptor number that can be opened by this process.
|
|
|
|
##
|
|
|
|
## Note: On Windows its impossible to obtain such number, so getMaxOpenFiles()
|
|
|
|
## will return constant value of 16384. You can get more information on this
|
|
|
|
## link https://docs.microsoft.com/en-us/archive/blogs/markrussinovich/pushing-the-limits-of-windows-handles
|
|
|
|
when defined(windows):
|
2021-05-07 20:52:24 +00:00
|
|
|
16384
|
2020-06-24 08:21:52 +00:00
|
|
|
else:
|
|
|
|
var limits: RLimit
|
|
|
|
if getrlimit(posix.RLIMIT_NOFILE, limits) != 0:
|
|
|
|
raiseOSError(osLastError())
|
2021-05-07 20:52:24 +00:00
|
|
|
int(limits.rlim_cur)
|
2020-06-24 08:21:52 +00:00
|
|
|
|
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
|
|
|
proc setMaxOpenFiles*(count: int) {.raises: [Defect, OSError].} =
|
2020-06-24 08:21:52 +00:00
|
|
|
## Set maximum file descriptor number that can be opened by this process.
|
|
|
|
##
|
|
|
|
## Note: On Windows its impossible to set this value, so it just a nop call.
|
|
|
|
when defined(windows):
|
|
|
|
discard
|
|
|
|
else:
|
|
|
|
var limits: RLimit
|
|
|
|
if getrlimit(posix.RLIMIT_NOFILE, limits) != 0:
|
|
|
|
raiseOSError(osLastError())
|
|
|
|
limits.rlim_cur = count
|
|
|
|
if setrlimit(posix.RLIMIT_NOFILE, limits) != 0:
|
|
|
|
raiseOSError(osLastError())
|
|
|
|
|
2019-07-15 09:59:42 +00:00
|
|
|
proc createAsyncPipe*(): tuple[read: AsyncFD, write: AsyncFD] =
|
|
|
|
## Create new asynchronouse pipe.
|
2021-05-07 20:52:24 +00:00
|
|
|
## Returns tuple of read pipe handle and write pipe handle``asyncInvalidPipe``
|
|
|
|
## on error.
|
2019-07-15 09:59:42 +00:00
|
|
|
when defined(windows):
|
|
|
|
var pipeIn, pipeOut: Handle
|
|
|
|
var pipeName: WideCString
|
|
|
|
var uniq = 0'u64
|
|
|
|
var sa = SECURITY_ATTRIBUTES(nLength: sizeof(SECURITY_ATTRIBUTES).cint,
|
|
|
|
lpSecurityDescriptor: nil, bInheritHandle: 0)
|
|
|
|
while true:
|
|
|
|
QueryPerformanceCounter(uniq)
|
|
|
|
pipeName = newWideCString(pipeHeaderName & $uniq)
|
|
|
|
var openMode = FILE_FLAG_FIRST_PIPE_INSTANCE or FILE_FLAG_OVERLAPPED or
|
|
|
|
PIPE_ACCESS_INBOUND
|
|
|
|
var pipeMode = PIPE_TYPE_BYTE or PIPE_READMODE_BYTE or PIPE_WAIT
|
|
|
|
pipeIn = createNamedPipe(pipeName, openMode, pipeMode, 1'i32,
|
|
|
|
DEFAULT_PIPE_SIZE, DEFAULT_PIPE_SIZE,
|
|
|
|
0'i32, addr sa)
|
|
|
|
if pipeIn == INVALID_HANDLE_VALUE:
|
|
|
|
let err = osLastError()
|
|
|
|
# If error in {ERROR_ACCESS_DENIED, ERROR_PIPE_BUSY}, then named pipe
|
|
|
|
# with such name already exists.
|
|
|
|
if int32(err) != ERROR_ACCESS_DENIED and int32(err) != ERROR_PIPE_BUSY:
|
2021-05-07 20:52:24 +00:00
|
|
|
return (read: asyncInvalidPipe, write: asyncInvalidPipe)
|
2019-07-15 09:59:42 +00:00
|
|
|
continue
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
|
|
|
|
var openMode = (GENERIC_WRITE or FILE_WRITE_DATA or SYNCHRONIZE)
|
|
|
|
pipeOut = createFileW(pipeName, openMode, 0, addr(sa), OPEN_EXISTING,
|
|
|
|
FILE_FLAG_OVERLAPPED, 0)
|
|
|
|
if pipeOut == INVALID_HANDLE_VALUE:
|
|
|
|
discard closeHandle(pipeIn)
|
2021-05-07 20:52:24 +00:00
|
|
|
return (read: asyncInvalidPipe, write: asyncInvalidPipe)
|
2019-07-15 09:59:42 +00:00
|
|
|
|
|
|
|
var ovl = OVERLAPPED()
|
|
|
|
let res = connectNamedPipe(pipeIn, cast[pointer](addr ovl))
|
|
|
|
if res == 0:
|
|
|
|
let err = osLastError()
|
2021-05-07 20:52:24 +00:00
|
|
|
case int32(err)
|
|
|
|
of ERROR_PIPE_CONNECTED:
|
2019-07-15 09:59:42 +00:00
|
|
|
discard
|
2021-05-07 20:52:24 +00:00
|
|
|
of ERROR_IO_PENDING:
|
2019-07-15 09:59:42 +00:00
|
|
|
var bytesRead = 0.Dword
|
|
|
|
if getOverlappedResult(pipeIn, addr ovl, bytesRead, 1) == 0:
|
|
|
|
discard closeHandle(pipeIn)
|
|
|
|
discard closeHandle(pipeOut)
|
2021-05-07 20:52:24 +00:00
|
|
|
return (read: asyncInvalidPipe, write: asyncInvalidPipe)
|
2019-07-15 09:59:42 +00:00
|
|
|
else:
|
|
|
|
discard closeHandle(pipeIn)
|
|
|
|
discard closeHandle(pipeOut)
|
2021-05-07 20:52:24 +00:00
|
|
|
return (read: asyncInvalidPipe, write: asyncInvalidPipe)
|
2019-07-15 09:59:42 +00:00
|
|
|
|
2021-05-07 20:52:24 +00:00
|
|
|
(read: AsyncFD(pipeIn), write: AsyncFD(pipeOut))
|
2019-07-15 09:59:42 +00:00
|
|
|
else:
|
|
|
|
var fds: array[2, cint]
|
|
|
|
|
|
|
|
if posix.pipe(fds) == -1:
|
2021-05-07 20:52:24 +00:00
|
|
|
return (read: asyncInvalidPipe, write: asyncInvalidPipe)
|
2019-07-15 09:59:42 +00:00
|
|
|
|
|
|
|
if not(setSocketBlocking(SocketHandle(fds[0]), false)) or
|
|
|
|
not(setSocketBlocking(SocketHandle(fds[1]), false)):
|
2021-05-07 20:52:24 +00:00
|
|
|
return (read: asyncInvalidPipe, write: asyncInvalidPipe)
|
2019-07-15 09:59:42 +00:00
|
|
|
|
2021-05-07 20:52:24 +00:00
|
|
|
(read: AsyncFD(fds[0]), write: AsyncFD(fds[1]))
|