Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add non-blocking io mode setter for POSIX #19120

Closed
wants to merge 16 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/pure/nativesockets.nim
Original file line number Diff line number Diff line change
Expand Up @@ -724,14 +724,16 @@ proc setBlocking*(s: SocketHandle, blocking: bool) =
var mode = clong(ord(not blocking)) # 1 for non-blocking, 0 for blocking
if ioctlsocket(s, FIONBIO, addr(mode)) == -1:
raiseOSError(osLastError())
else: # BSD sockets
elif defined(freertos) or defined(lwip):
var x: int = fcntl(s, F_GETFL, 0)
if x == -1:
raiseOSError(osLastError())
else:
var mode = if blocking: x and not O_NONBLOCK else: x or O_NONBLOCK
if fcntl(s, F_SETFL, mode) == -1:
raiseOSError(osLastError())
else:
setNonBlocking(FileHandle(s), not blocking)

proc timeValFromMilliseconds(timeout = 500): Timeval =
if timeout != -1:
Expand Down
57 changes: 56 additions & 1 deletion lib/system/io.nim
Original file line number Diff line number Diff line change
Expand Up @@ -270,16 +270,20 @@ const SupportIoctlInheritCtl = (defined(linux) or defined(bsd)) and
not defined(nimscript)
when SupportIoctlInheritCtl:
var
FIONBIO {.importc, header: "<sys/ioctl.h>".}: cint
FIOCLEX {.importc, header: "<sys/ioctl.h>".}: cint
FIONCLEX {.importc, header: "<sys/ioctl.h>".}: cint

proc c_ioctl(fd: cint, request: cint): cint {.
importc: "ioctl", header: "<sys/ioctl.h>", varargs.}
elif defined(posix) and not defined(lwip) and not defined(nimscript):
elif defined(posix) and not (defined(nimscript) or defined(freertos)):
var
F_GETFD {.importc, header: "<fcntl.h>".}: cint
F_SETFD {.importc, header: "<fcntl.h>".}: cint
F_GETFL {.importc, header: "<fcntl.h>".}: cint
F_SETFL {.importc, header: "<fcntl.h>".}: cint
FD_CLOEXEC {.importc, header: "<fcntl.h>".}: cint
O_NONBLOCK {.importc, header: "<fcntl.h>".}: cint

proc c_fcntl(fd: cint, cmd: cint): cint {.
importc: "fcntl", header: "<fcntl.h>", varargs.}
Expand Down Expand Up @@ -361,6 +365,57 @@ when defined(nimdoc) or (defined(posix) and not defined(nimscript)) or defined(w
result = setHandleInformation(cast[IoHandle](f), HANDLE_FLAG_INHERIT,
inheritable.WinDWORD) != 0

when defined(nimdoc) or not (defined(nimscript) or defined(windows) or defined(freertos)):
proc setNonBlocking*(f: FileHandle, nonBlocking = true) {.raises: [OSError].} =
## Control file handle blocking mode.
##
## Non-blocking IO `read`/`write` calls return immediately with whatever
## result is available, without putting the current thread to sleep. The
## call is expected to be tried again.
##
## Calling `read` on a non-blocking file handle will result in an `IOError`
## of `EAGAIN <https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/errno.h.html>`_
## whenever there is no data to read. The state can be checked beforehand
## with either `endOfFile <#endOfFile,File>`_ or `atEnd <streams.html#atEnd,Stream>`_.
##
## This requires the OS file handle, which can be
## retrieved via `getOsFileHandle <#getOsFileHandle,File>`_.
##
## This procedure is available for POSIX platforms. Test for
## availability with `declared() <system.html#declared,untyped>`_.
##
## There are separate APIs on Windows for using console handles,
## pipes and sockets in a non-blocking manner. Some of which aren't
## implemented in stdlib yet.
##
## See `setNonBlocking(File, bool) <#setNonBlocking,File>`_.
runnableExamples:
when not defined(windows):
setNonBlocking(getOsFileHandle(stdin))
doAssert(endOfFile(stdin))
when SupportIoctlInheritCtl:
let opt = if nonBlocking: 1 else: 0
if c_ioctl(f, FIONBIO, unsafeAddr(opt)) == -1:
raise newException(OSError, "failed to set file handle mode")
elif defined(posix):
var x: int = c_fcntl(f, F_GETFL, 0)
if x == -1:
raise newException(OSError, "failed to get file handle mode")
else:
var mode = if nonBlocking: x or O_NONBLOCK else: x and not O_NONBLOCK
if c_fcntl(f, F_SETFL, mode) == -1:
raise newException(OSError, "failed to set file handle mode")

proc setNonBlocking*(f: File, nonBlocking = true) {.raises: [OSError].} =
## Control file blocking mode.
##
## See `setNonBlocking(FileHandle, bool) <#setNonBlocking,FileHandle>`_.
runnableExamples:
when not defined(windows):
setNonBlocking(stdin)
doAssert(endOfFile(stdin))
setNonBlocking(getOsFileHandle(f), nonBlocking)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Define for Windows too, but with a {.error: ....} (less confusing than getting a "Undeclared identifier" error

proc readLine*(f: File, line: var string): bool {.tags: [ReadIOEffect],
benign.} =
## reads a line of text from the file `f` into `line`. May throw an IO
Expand Down
147 changes: 147 additions & 0 deletions tests/stdlib/tio_nonblocking.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
discard """
disabled: "win"
targets: "c"
matrix: "-d:threadsafe --threads:on"
timeout: 60
output: "started\nstopped\nquit\n"
"""
# PURPOSE
# Test setting and unsetting non-blocking IO mode on stdin, stdout, and stderr.
# Test the exception handing behavior.
# DESIGN
# Create a parent process.
# Parent tries setting an invalid file handle to non-blocking and then catches the error.
# Parent creates child process.
# Parent waits on child's non-blocking stdout and stderr.
# Child waits on non-blocking stdin.
# Parent sends `start` command to child on stdin.
# Child responds by writing `started` to its stdout and then waits on blocking stdin.
# Parent reads `started` on child's stdout and responds with `stop` on stdin and then closes stdin.
# Child catches IO exception from closed stdin and responds with `stopped` on stdout and quits.
# Parent gets signaled of quit, prints `quit` and quits.
when not (compileOption("threads") and defined(threadsafe)):
{.error: "-d:threadsafe --threads:on needed".}

import std/[selectors, osproc, streams, os, posix]

type
Handler = proc() {.closure.}
ErrorHandler = proc(code: OSErrorCode) {.closure.}
Handlers = tuple[process, stdout, stderr: tuple[handle: int, onEvent: Handler, onError: ErrorHandler]]
Monitor = enum
StdIn, StdOut, StdErr, Quit

const blockIndefinitely = -1

proc drain(f: File): string =
while not f.endOfFile:
result &= f.readChar

proc drain(f: Stream): string =
while not f.atEnd:
result &= f.readChar

proc monitor(arg: Handlers) {.thread.} =
var watcher = newSelector[Monitor]()
let processSignal = watcher.registerProcess(arg.process.handle, Quit)
watcher.registerHandle(arg.stdout.handle, {Event.Read}, StdOut)
watcher.registerHandle(arg.stderr.handle, {Event.Read}, StdErr)
{.gcsafe.}:
block running:
while true:
let events = watcher.select(blockIndefinitely)
for ready in events.items:
var kind: Monitor = watcher.getData(ready.fd)
case kind:
of StdIn: discard
of StdOut:
if Event.Read in ready.events:
arg.stdout.onEvent()
if Event.Error in ready.events:
if ready.errorCode.int == ECONNRESET:
watcher.unregister(ready.fd)
else:
arg.stderr.onError(ready.errorCode)
break running
of StdErr:
if Event.Read in ready.events:
arg.stderr.onEvent()
if Event.Error in ready.events:
if ready.errorCode.int == ECONNRESET:
watcher.unregister(ready.fd)
else:
arg.stderr.onError(ready.errorCode)
break running
of Quit:
arg.process.onEvent()
break running
watcher.unregister(processSignal)
watcher.close

proc parent =
try:
# test that exception is thrown
setNonBlocking(-1)
doAssert(false, "setNonBlocking should raise exception for invalid input")
except:
discard
var child = startProcess(
getAppFilename(),
args = ["child"],
options = {}
)
var thread: Thread[Handlers]
setNonBlocking(child.outputHandle)
setNonBlocking(child.errorHandle)
proc onEvent() =
let output = child.outputStream.drain
stdout.write output
if output == "started\n":
child.inputStream.write "stop"
child.inputStream.close
proc onError(code: OSErrorCode) {.closure.} =
doAssert(false, "error " & $code)
proc onQuit() {.closure.} =
echo "quit"
createThread(thread, monitor, (
(child.processID.int, onQuit, onError),
(child.outputHandle.int, onEvent, onError),
(child.errorHandle.int, onEvent, onError)))
child.inputStream.write "start"
child.inputStream.flush
doAssert(child.waitForExit == 0)
joinThread(thread)

proc child =
var watcher = newSelector[Monitor]()
watcher.registerHandle(stdin.getOsFileHandle.int, {Event.Read}, StdIn)
setNonBlocking(stdin)
block running:
while true:
let events = watcher.select(blockIndefinitely)
for ready in events.items:
var kind: Monitor = watcher.getData(ready.fd)
case kind:
of StdIn:
if stdin.drain == "start": # this would normally block
echo "started" # piped to parent
break running
else: discard
watcher.close
setNonBlocking(stdin, false)
try:
# this line ensures the above setNonBlocking call is distinguisable from
# a no-op; "stopped" would never be sent
if not stdin.endOfFile:
echo stdin.readAll
except: # this will raise when stdin is closed by the parent
doAssert(osLastError().int == EAGAIN)
echo "stopped" # piped to parent

proc main =
if paramCount() > 0:
child()
else:
parent()

main()