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 }, |
