diff --git a/lib/pure/nativesockets.nim b/lib/pure/nativesockets.nim index 4037d7181e86..711d65136429 100644 --- a/lib/pure/nativesockets.nim +++ b/lib/pure/nativesockets.nim @@ -724,7 +724,7 @@ 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()) @@ -732,6 +732,8 @@ proc setBlocking*(s: SocketHandle, blocking: bool) = 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: diff --git a/lib/system/io.nim b/lib/system/io.nim index 2ad43acdbb75..a0afdf0dcdbf 100644 --- a/lib/system/io.nim +++ b/lib/system/io.nim @@ -270,16 +270,20 @@ const SupportIoctlInheritCtl = (defined(linux) or defined(bsd)) and not defined(nimscript) when SupportIoctlInheritCtl: var + FIONBIO {.importc, header: "".}: cint FIOCLEX {.importc, header: "".}: cint FIONCLEX {.importc, header: "".}: cint proc c_ioctl(fd: cint, request: cint): cint {. importc: "ioctl", header: "", 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: "".}: cint F_SETFD {.importc, header: "".}: cint + F_GETFL {.importc, header: "".}: cint + F_SETFL {.importc, header: "".}: cint FD_CLOEXEC {.importc, header: "".}: cint + O_NONBLOCK {.importc, header: "".}: cint proc c_fcntl(fd: cint, cmd: cint): cint {. importc: "fcntl", header: "", varargs.} @@ -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 `_ + ## whenever there is no data to read. The state can be checked beforehand + ## with either `endOfFile <#endOfFile,File>`_ or `atEnd `_. + ## + ## 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() `_. + ## + ## 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) + 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 diff --git a/tests/stdlib/tio_nonblocking.nim b/tests/stdlib/tio_nonblocking.nim new file mode 100644 index 000000000000..034de9aff689 --- /dev/null +++ b/tests/stdlib/tio_nonblocking.nim @@ -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()