From 98ca844bc6170ee332353abdeaac0be0e4a43af0 Mon Sep 17 00:00:00 2001 From: Thijs Schreijer Date: Thu, 10 Apr 2025 10:04:19 +0200 Subject: feat(term): detach fd of stderr+stdout to be independent (#59) --- src/term.c | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) (limited to 'src') diff --git a/src/term.c b/src/term.c index 4cfce95..792832d 100644 --- a/src/term.c +++ b/src/term.c @@ -649,8 +649,104 @@ static int lst_tcsetattr(lua_State *L) +#ifndef _WIN32 +/* +reopen FDs for independent file descriptions. +*/ +static void reopen_fd(lua_State *L, int fd, int flags) { + char path[64]; + int newfd = -1; + + if (fd != STDOUT_FILENO && fd != STDERR_FILENO) { + luaL_error(L, "Invalid file descriptor: %d. Only stdout (1) and stderr (2) are supported.", fd); + } + + const char *fallback_path = (fd == STDOUT_FILENO) ? "/dev/stdout" : + (fd == STDERR_FILENO) ? "/dev/stderr" : NULL; + + // If fd is a terminal, reopen its actual device (e.g. /dev/ttys003) + // Works on all POSIX platforms that have terminals (macOS, Linux, BSD, etc.) + if (isatty(fd)) { + const char *tty = ttyname(fd); + if (tty) { + newfd = open(tty, flags); + } + } + + if (newfd < 0) { + // For non-tty: try /dev/fd/N — POSIX-compliant and standard on macOS, Linux, BSD. + // This gives a new file description even if the target is a file or pipe. + snprintf(path, sizeof(path), "/dev/fd/%d", fd); + newfd = open(path, flags); + } + + if (newfd < 0) { + // Fallback: for platforms/environments where /dev/fd/N doesn't exist. + // /dev/stdout and /dev/stderr are standard on Linux, but may not create new descriptions. + if (fallback_path) { + newfd = open(fallback_path, flags); + } + } + + if (newfd < 0) { + // All attempts failed — raise error with detailed info (call will not return) + luaL_error(L, "Failed to reopen fd %d: tried ttyname(), /dev/fd/%d, and fallback %s: %s", + fd, fd, fallback_path ? fallback_path : "(none)", strerror(errno)); + } + + // Replace the original fd with the new one + if (dup2(newfd, fd) < 0) { + close(newfd); + luaL_error(L, "dup2 failed for fd %d: %s", fd, strerror(errno)); + } + + close(newfd); // Close the new fd, as dup2 has replaced the original fd with it +} +#endif + + + +/*** +Creates new file descriptions for `stdout` and `stderr`. +Even if the file descriptors are unique, they still might point to the same +file description, and hence share settings like `O_NONBLOCK`. This means that +if one of them is set to non-blocking, the other will be as well. This can +lead to unexpected behavior. + +This function is used to detach `stdout` and `stderr` from the original +file descriptions, and create new file descriptions for them. This allows +independent control of flags (e.g., `O_NONBLOCK`) on `stdout` and `stderr`, +avoiding shared side effects. + +Does not modify `stdin` (fd 0), and does nothing on Windows. +@function detachfds +@return boolean `true` on success, or throws an error on failure. +@see setnonblock +*/ +static int lst_detachfds(lua_State *L) { + static int already_detached = 0; // detaching is once per process(not per thread or Lua state) + if (already_detached) { + lua_pushnil(L); + lua_pushliteral(L, "stdout and stderr already detached"); + return 1; + } + already_detached = 1; + +#ifndef _WIN32 + // Reopen stdout and stderr with new file descriptions + reopen_fd(L, STDOUT_FILENO, O_WRONLY); + reopen_fd(L, STDERR_FILENO, O_WRONLY); +#endif + + lua_pushboolean(L, 1); + return 1; +} + + + /*** Enables or disables non-blocking mode for a file (Posix). +Check `detachfds` in case there are shared file descriptions. @function setnonblock @tparam file fd file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr` @tparam boolean make_non_block a truthy value will enable non-blocking mode, a falsy value will disable it. @@ -659,8 +755,10 @@ Enables or disables non-blocking mode for a file (Posix). @treturn[2] string error message @treturn[2] int errnum @see getnonblock +@see detachfds @usage local sys = require('system') +sys.detachfds() -- detach stdout and stderr, so only stdin becomes non-blocking -- set io.stdin to non-blocking mode local old_setting = sys.getnonblock(io.stdin) @@ -717,6 +815,7 @@ Gets non-blocking mode status for a file (Posix). @treturn[2] nil @treturn[2] string error message @treturn[2] int errnum +@see setnonblock */ static int lst_getnonblock(lua_State *L) { @@ -1157,6 +1256,7 @@ static luaL_Reg func[] = { { "setconsoleflags", lst_setconsoleflags }, { "tcgetattr", lst_tcgetattr }, { "tcsetattr", lst_tcsetattr }, + { "detachfds", lst_detachfds }, { "getnonblock", lst_getnonblock }, { "setnonblock", lst_setnonblock }, { "_readkey", lst_readkey }, -- cgit v1.2.3-55-g6feb