diff options
author | Thijs Schreijer <thijs@thijsschreijer.nl> | 2025-04-10 10:04:19 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-04-10 10:04:19 +0200 |
commit | 98ca844bc6170ee332353abdeaac0be0e4a43af0 (patch) | |
tree | 88b0e2119a2c4b67c126349214a53b3ef8782c2b | |
parent | 49e7dac558178e6200bc5886db3ef28c73d5edd9 (diff) | |
download | luasystem-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.md | 7 | ||||
-rw-r--r-- | doc_topics/03-terminal.md | 2 | ||||
-rw-r--r-- | src/term.c | 100 |
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, |
@@ -649,8 +649,104 @@ static int lst_tcsetattr(lua_State *L) | |||
649 | 649 | ||
650 | 650 | ||
651 | 651 | ||
652 | #ifndef _WIN32 | ||
653 | /* | ||
654 | reopen FDs for independent file descriptions. | ||
655 | */ | ||
656 | static 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 | /*** | ||
710 | Creates new file descriptions for `stdout` and `stderr`. | ||
711 | Even if the file descriptors are unique, they still might point to the same | ||
712 | file description, and hence share settings like `O_NONBLOCK`. This means that | ||
713 | if one of them is set to non-blocking, the other will be as well. This can | ||
714 | lead to unexpected behavior. | ||
715 | |||
716 | This function is used to detach `stdout` and `stderr` from the original | ||
717 | file descriptions, and create new file descriptions for them. This allows | ||
718 | independent control of flags (e.g., `O_NONBLOCK`) on `stdout` and `stderr`, | ||
719 | avoiding shared side effects. | ||
720 | |||
721 | Does 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 | */ | ||
726 | static 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 | /*** |
653 | Enables or disables non-blocking mode for a file (Posix). | 748 | Enables or disables non-blocking mode for a file (Posix). |
749 | Check `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 |
663 | local sys = require('system') | 760 | local sys = require('system') |
761 | sys.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 |
666 | local old_setting = sys.getnonblock(io.stdin) | 764 | local 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 | */ |
721 | static int lst_getnonblock(lua_State *L) | 820 | static 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 }, |