diff options
author | Thijs Schreijer <thijs@thijsschreijer.nl> | 2025-04-04 14:32:17 +0200 |
---|---|---|
committer | Thijs Schreijer <thijs@thijsschreijer.nl> | 2025-04-07 10:44:01 +0200 |
commit | efb8ca600413c4cc70985e9a93aa55ac581134a7 (patch) | |
tree | e83a615a17f12a894d310020365beb13c65a491c | |
parent | 49e7dac558178e6200bc5886db3ef28c73d5edd9 (diff) | |
download | luasystem-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.md | 4 | ||||
-rw-r--r-- | src/term.c | 101 |
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 | ||
102 | To use non-blocking input here's how to set it up: | 103 | To 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 | ||
@@ -649,8 +649,105 @@ 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 | fd should be either 1 or 2 (stdout or stderr) | ||
656 | */ | ||
657 | static 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 | /*** | ||
696 | Creates new file descriptions for `stdout` and `stderr`. | ||
697 | Even if the file descriptors are unique, they still might point to the same | ||
698 | file description, and hence share settings like `O_NONBLOCK`. This means that | ||
699 | if one of them is set to non-blocking, the other will be as well. This can | ||
700 | lead to unexpected behavior. | ||
701 | |||
702 | This function is used to detach `stdout` and `stderr` from the original | ||
703 | file descriptions, and create new file descriptions for them. This allows | ||
704 | independent control of flags (e.g., `O_NONBLOCK`) on `stdout` and `stderr`, | ||
705 | avoiding shared side effects. | ||
706 | |||
707 | Does 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 | */ | ||
712 | static 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 | /*** |
653 | Enables or disables non-blocking mode for a file (Posix). | 749 | Enables or disables non-blocking mode for a file (Posix). |
750 | Check `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 |
663 | local sys = require('system') | 761 | local sys = require('system') |
762 | sys.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 |
666 | local old_setting = sys.getnonblock(io.stdin) | 765 | local 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 | */ |
721 | static int lst_getnonblock(lua_State *L) | 821 | static 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 }, |