aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorThijs Schreijer <thijs@thijsschreijer.nl>2025-04-10 10:04:19 +0200
committerGitHub <noreply@github.com>2025-04-10 10:04:19 +0200
commit98ca844bc6170ee332353abdeaac0be0e4a43af0 (patch)
tree88b0e2119a2c4b67c126349214a53b3ef8782c2b
parent49e7dac558178e6200bc5886db3ef28c73d5edd9 (diff)
downloadluasystem-98ca844bc6170ee332353abdeaac0be0e4a43af0.tar.gz
luasystem-98ca844bc6170ee332353abdeaac0be0e4a43af0.tar.bz2
luasystem-98ca844bc6170ee332353abdeaac0be0e4a43af0.zip
feat(term): detach fd of stderr+stdout to be independent (#59)
Diffstat (limited to '')
-rw-r--r--CHANGELOG.md7
-rw-r--r--doc_topics/03-terminal.md2
-rw-r--r--src/term.c100
3 files changed, 107 insertions, 2 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2fd09d1..c617db5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,9 @@ The scope of what is covered by the version number excludes:
30### version 0.6.0, unreleased 30### version 0.6.0, unreleased
31 31
32- Fix: when sleep returns an error, pass that on in `readkey`. 32- Fix: when sleep returns an error, pass that on in `readkey`.
33- Feat: added `detachfds` which will create separate file descriptions for `stdout`
34 and `stderr` to ensure that related settings (eg. non-blocking flag) will not be shared
35 amongst those streams and `stdin`.
33 36
34### version 0.5.1, released 12-Mar-2025 37### version 0.5.1, released 12-Mar-2025
35 38
@@ -42,12 +45,12 @@ The scope of what is covered by the version number excludes:
42- Feat: allow passing in a sleep function to `readkey` and `readansi` 45- Feat: allow passing in a sleep function to `readkey` and `readansi`
43- Fix: NetBSD fix compilation, undeclared directives 46- Fix: NetBSD fix compilation, undeclared directives
44- Refactor: random bytes; remove deprecated API usage on Windows, move to 47- Refactor: random bytes; remove deprecated API usage on Windows, move to
45 binary api instead of /dev/urandom file on linux and bsd 48 binary api instead of `/dev/urandom` file on linux and bsd
46 49
47### version 0.4.5, released 18-Dec-2024 50### version 0.4.5, released 18-Dec-2024
48 51
49- Fix: suppress a warning when building with clang 52- Fix: suppress a warning when building with clang
50- Fix: do not rely on luaconf.h to include limits.h, fixes builds with latest LuaJIT (#38). 53- Fix: do not rely on `luaconf.h` to include `limits.h`, fixes builds with latest LuaJIT (#38).
51 54
52### version 0.4.4, released 03-Sep-2024 55### version 0.4.4, released 03-Sep-2024
53 56
diff --git a/doc_topics/03-terminal.md b/doc_topics/03-terminal.md
index 5bdf543..a5341d6 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
@@ -105,6 +106,7 @@ To use non-blocking input here's how to set it up:
105 sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) - sys.CIF_ECHO_INPUT - sys.CIF_LINE_INPUT) 106 sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) - sys.CIF_ECHO_INPUT - sys.CIF_LINE_INPUT)
106 107
107 -- setup Posix by disabling echo, canonical mode, and making non-blocking 108 -- setup Posix by disabling echo, canonical mode, and making non-blocking
109 sys.detachfds() -- ensure stdin/out/err have their own file descriptions
108 local of_attr = sys.tcgetattr(io.stdin) 110 local of_attr = sys.tcgetattr(io.stdin)
109 sys.tcsetattr(io.stdin, sys.TCSANOW, { 111 sys.tcsetattr(io.stdin, sys.TCSANOW, {
110 lflag = of_attr.lflag - sys.L_ICANON - sys.L_ECHO, 112 lflag = of_attr.lflag - sys.L_ICANON - sys.L_ECHO,
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)
649 649
650 650
651 651
652#ifndef _WIN32
653/*
654reopen FDs for independent file descriptions.
655*/
656static void reopen_fd(lua_State *L, int fd, int flags) {
657 char path[64];
658 int newfd = -1;
659
660 if (fd != STDOUT_FILENO && fd != STDERR_FILENO) {
661 luaL_error(L, "Invalid file descriptor: %d. Only stdout (1) and stderr (2) are supported.", fd);
662 }
663
664 const char *fallback_path = (fd == STDOUT_FILENO) ? "/dev/stdout" :
665 (fd == STDERR_FILENO) ? "/dev/stderr" : NULL;
666
667 // If fd is a terminal, reopen its actual device (e.g. /dev/ttys003)
668 // Works on all POSIX platforms that have terminals (macOS, Linux, BSD, etc.)
669 if (isatty(fd)) {
670 const char *tty = ttyname(fd);
671 if (tty) {
672 newfd = open(tty, flags);
673 }
674 }
675
676 if (newfd < 0) {
677 // For non-tty: try /dev/fd/N — POSIX-compliant and standard on macOS, Linux, BSD.
678 // This gives a new file description even if the target is a file or pipe.
679 snprintf(path, sizeof(path), "/dev/fd/%d", fd);
680 newfd = open(path, flags);
681 }
682
683 if (newfd < 0) {
684 // Fallback: for platforms/environments where /dev/fd/N doesn't exist.
685 // /dev/stdout and /dev/stderr are standard on Linux, but may not create new descriptions.
686 if (fallback_path) {
687 newfd = open(fallback_path, flags);
688 }
689 }
690
691 if (newfd < 0) {
692 // All attempts failed — raise error with detailed info (call will not return)
693 luaL_error(L, "Failed to reopen fd %d: tried ttyname(), /dev/fd/%d, and fallback %s: %s",
694 fd, fd, fallback_path ? fallback_path : "(none)", strerror(errno));
695 }
696
697 // Replace the original fd with the new one
698 if (dup2(newfd, fd) < 0) {
699 close(newfd);
700 luaL_error(L, "dup2 failed for fd %d: %s", fd, strerror(errno));
701 }
702
703 close(newfd); // Close the new fd, as dup2 has replaced the original fd with it
704}
705#endif
706
707
708
709/***
710Creates new file descriptions for `stdout` and `stderr`.
711Even if the file descriptors are unique, they still might point to the same
712file description, and hence share settings like `O_NONBLOCK`. This means that
713if one of them is set to non-blocking, the other will be as well. This can
714lead to unexpected behavior.
715
716This function is used to detach `stdout` and `stderr` from the original
717file descriptions, and create new file descriptions for them. This allows
718independent control of flags (e.g., `O_NONBLOCK`) on `stdout` and `stderr`,
719avoiding shared side effects.
720
721Does not modify `stdin` (fd 0), and does nothing on Windows.
722@function detachfds
723@return boolean `true` on success, or throws an error on failure.
724@see setnonblock
725*/
726static int lst_detachfds(lua_State *L) {
727 static int already_detached = 0; // detaching is once per process(not per thread or Lua state)
728 if (already_detached) {
729 lua_pushnil(L);
730 lua_pushliteral(L, "stdout and stderr already detached");
731 return 1;
732 }
733 already_detached = 1;
734
735#ifndef _WIN32
736 // Reopen stdout and stderr with new file descriptions
737 reopen_fd(L, STDOUT_FILENO, O_WRONLY);
738 reopen_fd(L, STDERR_FILENO, O_WRONLY);
739#endif
740
741 lua_pushboolean(L, 1);
742 return 1;
743}
744
745
746
652/*** 747/***
653Enables or disables non-blocking mode for a file (Posix). 748Enables or disables non-blocking mode for a file (Posix).
749Check `detachfds` in case there are shared file descriptions.
654@function setnonblock 750@function setnonblock
655@tparam file fd file handle to operate on, one of `io.stdin`, `io.stdout`, `io.stderr` 751@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. 752@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).
659@treturn[2] string error message 755@treturn[2] string error message
660@treturn[2] int errnum 756@treturn[2] int errnum
661@see getnonblock 757@see getnonblock
758@see detachfds
662@usage 759@usage
663local sys = require('system') 760local sys = require('system')
761sys.detachfds() -- detach stdout and stderr, so only stdin becomes non-blocking
664 762
665-- set io.stdin to non-blocking mode 763-- set io.stdin to non-blocking mode
666local old_setting = sys.getnonblock(io.stdin) 764local old_setting = sys.getnonblock(io.stdin)
@@ -717,6 +815,7 @@ Gets non-blocking mode status for a file (Posix).
717@treturn[2] nil 815@treturn[2] nil
718@treturn[2] string error message 816@treturn[2] string error message
719@treturn[2] int errnum 817@treturn[2] int errnum
818@see setnonblock
720*/ 819*/
721static int lst_getnonblock(lua_State *L) 820static int lst_getnonblock(lua_State *L)
722{ 821{
@@ -1157,6 +1256,7 @@ static luaL_Reg func[] = {
1157 { "setconsoleflags", lst_setconsoleflags }, 1256 { "setconsoleflags", lst_setconsoleflags },
1158 { "tcgetattr", lst_tcgetattr }, 1257 { "tcgetattr", lst_tcgetattr },
1159 { "tcsetattr", lst_tcsetattr }, 1258 { "tcsetattr", lst_tcsetattr },
1259 { "detachfds", lst_detachfds },
1160 { "getnonblock", lst_getnonblock }, 1260 { "getnonblock", lst_getnonblock },
1161 { "setnonblock", lst_setnonblock }, 1261 { "setnonblock", lst_setnonblock },
1162 { "_readkey", lst_readkey }, 1262 { "_readkey", lst_readkey },