aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThijs Schreijer <thijs@thijsschreijer.nl>2025-04-04 14:32:17 +0200
committerThijs Schreijer <thijs@thijsschreijer.nl>2025-04-07 10:44:01 +0200
commitefb8ca600413c4cc70985e9a93aa55ac581134a7 (patch)
treee83a615a17f12a894d310020365beb13c65a491c
parent49e7dac558178e6200bc5886db3ef28c73d5edd9 (diff)
downloadluasystem-efb8ca600413c4cc70985e9a93aa55ac581134a7.tar.gz
luasystem-efb8ca600413c4cc70985e9a93aa55ac581134a7.tar.bz2
luasystem-efb8ca600413c4cc70985e9a93aa55ac581134a7.zip
feat(term): detach fd of stderr+stdout to be independent
If not independent, then setting non-block on one may affect the others.
-rw-r--r--doc_topics/03-terminal.md4
-rw-r--r--src/term.c101
2 files changed, 105 insertions, 0 deletions
diff --git a/doc_topics/03-terminal.md b/doc_topics/03-terminal.md
index 5bdf543..3705cce 100644
--- a/doc_topics/03-terminal.md
+++ b/doc_topics/03-terminal.md
@@ -16,6 +16,7 @@ Since there are a myriad of settings available;
16- `system.setconsoleflags` (Windows) 16- `system.setconsoleflags` (Windows)
17- `system.setconsolecp` (Windows) 17- `system.setconsolecp` (Windows)
18- `system.setconsoleoutputcp` (Windows) 18- `system.setconsoleoutputcp` (Windows)
19- `system.detachfds` (Posix)
19- `system.setnonblock` (Posix) 20- `system.setnonblock` (Posix)
20- `system.tcsetattr` (Posix) 21- `system.tcsetattr` (Posix)
21 22
@@ -101,6 +102,9 @@ On Posix the traditional file approach is used, which:
101 102
102To use non-blocking input here's how to set it up: 103To use non-blocking input here's how to set it up:
103 104
105 -- Detach stdin/out/err; to get their own independent file descriptions
106 sys.detachfds()
107
104 -- setup Windows console to disable echo and line input (not required since _getwchar is used, just for consistency) 108 -- setup Windows console to disable echo and line input (not required since _getwchar is used, just for consistency)
105 sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) - sys.CIF_ECHO_INPUT - sys.CIF_LINE_INPUT) 109 sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) - sys.CIF_ECHO_INPUT - sys.CIF_LINE_INPUT)
106 110
diff --git a/src/term.c b/src/term.c
index 4cfce95..caca4e1 100644
--- a/src/term.c
+++ b/src/term.c
@@ -649,8 +649,105 @@ static int lst_tcsetattr(lua_State *L)
649 649
650 650
651 651
652#ifndef _WIN32
653/*
654reopen FDs for independent file descriptions.
655fd should be either 1 or 2 (stdout or stderr)
656*/
657static int reopen_fd(lua_State *L, int fd, int flags) {
658 char path[64];
659 int newfd = -1;
660
661 // If fd is a terminal, reopen its actual device (e.g. /dev/ttys003)
662 // Works on all POSIX platforms that have terminals (macOS, Linux, BSD, etc.)
663 if (isatty(fd)) {
664 const char *tty = ttyname(fd);
665 if (tty) {
666 newfd = open(tty, flags);
667 if (newfd >= 0) return newfd;
668 }
669 }
670
671 // For non-tty: try /dev/fd/N — POSIX-compliant and standard on macOS, Linux, BSD.
672 // This gives a new file description even if the target is a file or pipe.
673 snprintf(path, sizeof(path), "/dev/fd/%d", fd);
674 newfd = open(path, flags);
675 if (newfd >= 0) return newfd;
676
677 // Fallback: for platforms/environments where /dev/fd/N doesn't exist.
678 // /dev/stdout and /dev/stderr are standard on Linux, but may not create new descriptions.
679 const char *fallback_path = (fd == 1) ? "/dev/stdout" :
680 (fd == 2) ? "/dev/stderr" : NULL;
681
682 if (fallback_path) {
683 newfd = open(fallback_path, flags);
684 if (newfd >= 0) return newfd;
685 }
686
687 // All attempts failed — raise error with detailed info
688 return luaL_error(L, "Failed to reopen fd %d: tried ttyname(), /dev/fd/%d, and fallback %s: %s",
689 fd, fd, fallback_path ? fallback_path : "(none)", strerror(errno));
690}
691#endif
692
693
694
695/***
696Creates new file descriptions for `stdout` and `stderr`.
697Even if the file descriptors are unique, they still might point to the same
698file description, and hence share settings like `O_NONBLOCK`. This means that
699if one of them is set to non-blocking, the other will be as well. This can
700lead to unexpected behavior.
701
702This function is used to detach `stdout` and `stderr` from the original
703file descriptions, and create new file descriptions for them. This allows
704independent control of flags (e.g., `O_NONBLOCK`) on `stdout` and `stderr`,
705avoiding shared side effects.
706
707Does not modify `stdin` (fd 0), and does nothing on Windows.
708@function detachfds
709@return boolean `true` on success, or throws an error on failure.
710@see setnonblock
711*/
712static int lst_detachfds(lua_State *L) {
713 static int already_detached = 0; // detaching is once per process(not per thread or Lua state)
714 if (already_detached) {
715 lua_pushnil(L);
716 lua_pushliteral(L, "stdout and stderr already detached");
717 return 1;
718 }
719 already_detached = 1;
720
721#ifndef _WIN32
722 // Reopen stdout and stderr with new file descriptions
723 int fd_out = reopen_fd(L, 1, O_WRONLY);
724 int fd_err = reopen_fd(L, 2, O_WRONLY);
725
726 // Replace fd 1 and 2 in-place using dup2
727 if (dup2(fd_out, 1) < 0) {
728 close(fd_out);
729 return luaL_error(L, "dup2 failed for stdout: %s", strerror(errno));
730 }
731 if (dup2(fd_err, 2) < 0) {
732 close(fd_err);
733 return luaL_error(L, "dup2 failed for stderr: %s", strerror(errno));
734 }
735
736 // Clean up temporary file descriptors — fd 1 and 2 now own them
737 close(fd_out);
738 close(fd_err);
739
740#endif
741
742 lua_pushboolean(L, 1);
743 return 1;
744}
745
746
747
652/*** 748/***
653Enables or disables non-blocking mode for a file (Posix). 749Enables or disables non-blocking mode for a file (Posix).
750Check `detachfds` in case there are shared file descriptions.
654@function setnonblock 751@function setnonblock
655@tparam file fd file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr` 752@tparam file fd file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr`
656@tparam boolean make_non_block a truthy value will enable non-blocking mode, a falsy value will disable it. 753@tparam boolean make_non_block a truthy value will enable non-blocking mode, a falsy value will disable it.
@@ -659,8 +756,10 @@ Enables or disables non-blocking mode for a file (Posix).
659@treturn[2] string error message 756@treturn[2] string error message
660@treturn[2] int errnum 757@treturn[2] int errnum
661@see getnonblock 758@see getnonblock
759@see detachfds
662@usage 760@usage
663local sys = require('system') 761local sys = require('system')
762sys.detachfds() -- detach stdout and stderr, so only stdin becomes non-blocking
664 763
665-- set io.stdin to non-blocking mode 764-- set io.stdin to non-blocking mode
666local old_setting = sys.getnonblock(io.stdin) 765local old_setting = sys.getnonblock(io.stdin)
@@ -717,6 +816,7 @@ Gets non-blocking mode status for a file (Posix).
717@treturn[2] nil 816@treturn[2] nil
718@treturn[2] string error message 817@treturn[2] string error message
719@treturn[2] int errnum 818@treturn[2] int errnum
819@see setnonblock
720*/ 820*/
721static int lst_getnonblock(lua_State *L) 821static int lst_getnonblock(lua_State *L)
722{ 822{
@@ -1157,6 +1257,7 @@ static luaL_Reg func[] = {
1157 { "setconsoleflags", lst_setconsoleflags }, 1257 { "setconsoleflags", lst_setconsoleflags },
1158 { "tcgetattr", lst_tcgetattr }, 1258 { "tcgetattr", lst_tcgetattr },
1159 { "tcsetattr", lst_tcsetattr }, 1259 { "tcsetattr", lst_tcsetattr },
1260 { "detachfds", lst_detachfds },
1160 { "getnonblock", lst_getnonblock }, 1261 { "getnonblock", lst_getnonblock },
1161 { "setnonblock", lst_setnonblock }, 1262 { "setnonblock", lst_setnonblock },
1162 { "_readkey", lst_readkey }, 1263 { "_readkey", lst_readkey },