diff --git a/kitty/child.c b/kitty/child.c index f3b6ee339..9ddb4bc2a 100644 --- a/kitty/child.c +++ b/kitty/child.c @@ -80,10 +80,10 @@ wait_for_terminal_ready(int fd) { static PyObject* spawn(PyObject *self UNUSED, PyObject *args) { PyObject *argv_p, *env_p, *handled_signals_p; - int master, slave, stdin_read_fd, stdin_write_fd, ready_read_fd, ready_write_fd; + int master, slave, stdin_read_fd, stdin_write_fd, ready_read_fd, ready_write_fd, forward_stdio; const char *kitten_exe; char *cwd, *exe; - if (!PyArg_ParseTuple(args, "ssO!O!iiiiiiO!s", &exe, &cwd, &PyTuple_Type, &argv_p, &PyTuple_Type, &env_p, &master, &slave, &stdin_read_fd, &stdin_write_fd, &ready_read_fd, &ready_write_fd, &PyTuple_Type, &handled_signals_p, &kitten_exe)) return NULL; + if (!PyArg_ParseTuple(args, "ssO!O!iiiiiiO!sp", &exe, &cwd, &PyTuple_Type, &argv_p, &PyTuple_Type, &env_p, &master, &slave, &stdin_read_fd, &stdin_write_fd, &ready_read_fd, &ready_write_fd, &PyTuple_Type, &handled_signals_p, &kitten_exe, &forward_stdio)) return NULL; char name[2048] = {0}; if (ttyname_r(slave, name, sizeof(name) - 1) != 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; } char **argv = serialize_string_tuple(argv_p); @@ -129,15 +129,20 @@ spawn(PyObject *self UNUSED, PyObject *args) { if (ioctl(tfd, TIOCSCTTY, 0) == -1) exit_on_err("Failed to set controlling terminal with TIOCSCTTY"); safe_close(tfd, __FILE__, __LINE__); + int min_closed_fd = 3; + if (forward_stdio) { + if (safe_dup2(STDOUT_FILENO, min_closed_fd++) == -1) exit_on_err("dup2() failed for forwarded fd 1"); + if (safe_dup2(STDERR_FILENO, min_closed_fd++) == -1) exit_on_err("dup2() failed for forwarded fd 2"); + } // Redirect stdin/stdout/stderr to the pty - if (safe_dup2(slave, 1) == -1) exit_on_err("dup2() failed for fd number 1"); - if (safe_dup2(slave, 2) == -1) exit_on_err("dup2() failed for fd number 2"); + if (safe_dup2(slave, STDOUT_FILENO) == -1) exit_on_err("dup2() failed for fd number 1"); + if (safe_dup2(slave, STDERR_FILENO) == -1) exit_on_err("dup2() failed for fd number 2"); if (stdin_read_fd > -1) { - if (safe_dup2(stdin_read_fd, 0) == -1) exit_on_err("dup2() failed for fd number 0"); + if (safe_dup2(stdin_read_fd, STDIN_FILENO) == -1) exit_on_err("dup2() failed for fd number 0"); safe_close(stdin_read_fd, __FILE__, __LINE__); safe_close(stdin_write_fd, __FILE__, __LINE__); } else { - if (safe_dup2(slave, 0) == -1) exit_on_err("dup2() failed for fd number 0"); + if (safe_dup2(slave, STDIN_FILENO) == -1) exit_on_err("dup2() failed for fd number 0"); } safe_close(slave, __FILE__, __LINE__); safe_close(master, __FILE__, __LINE__); @@ -148,7 +153,7 @@ spawn(PyObject *self UNUSED, PyObject *args) { safe_close(ready_read_fd, __FILE__, __LINE__); // Close any extra fds inherited from parent - for (int c = 3; c < 201; c++) safe_close(c, __FILE__, __LINE__); + for (int c = min_closed_fd; c < 201; c++) safe_close(c, __FILE__, __LINE__); environ = env; execvp(exe, argv); diff --git a/kitty/child.py b/kitty/child.py index 0996f5658..c52184f99 100644 --- a/kitty/child.py +++ b/kitty/child.py @@ -218,12 +218,13 @@ class Child: def final_env(self) -> Dict[str, str]: from kitty.options.utils import DELETE_ENV_VAR env = default_env().copy() + opts = fast_data_types.get_options() boss = fast_data_types.get_boss() if is_macos and env.get('LC_CTYPE') == 'UTF-8' and not getattr(sys, 'kitty_run_data').get( 'lc_ctype_before_python') and not getattr(default_env, 'lc_ctype_set_by_user', False): del env['LC_CTYPE'] env.update(self.env) - env['TERM'] = fast_data_types.get_options().term + env['TERM'] = opts.term env['COLORTERM'] = 'truecolor' env['KITTY_PID'] = getpid() env['KITTY_PUBLIC_KEY'] = boss.encryption_public_key @@ -240,7 +241,8 @@ class Child: if tdir: env['TERMINFO'] = tdir env['KITTY_INSTALLATION_DIR'] = kitty_base_dir - opts = fast_data_types.get_options() + if opts.forward_stdio: + env['KITTY_STDIO_FORWARDED'] = '3' self.unmodified_argv = list(self.argv) if 'disabled' not in opts.shell_integration: from .shell_integration import modify_shell_environ @@ -256,6 +258,7 @@ class Child: def fork(self) -> Optional[int]: if self.forked: return None + opts = fast_data_types.get_options() self.forked = True master, slave = openpty() stdin, self.stdin = self.stdin, None @@ -291,7 +294,7 @@ class Child: self.final_argv0 = argv[0] pid = fast_data_types.spawn( self.final_exe, self.cwd, tuple(argv), env, master, slave, stdin_read_fd, stdin_write_fd, - ready_read_fd, ready_write_fd, tuple(handled_signals), kitten_exe()) + ready_read_fd, ready_write_fd, tuple(handled_signals), kitten_exe(), opts.forward_stdio) os.close(slave) self.pid = pid self.child_fd = master diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index c26c3b67e..f2f775eb1 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1322,6 +1322,7 @@ def spawn( ready_write_fd: int, handled_signals: Tuple[int, ...], kitten_exe: str, + forward_stdio: bool, ) -> int: pass diff --git a/kitty/options/definition.py b/kitty/options/definition.py index e5221d38d..13c1b3db3 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -3082,6 +3082,16 @@ work. Changing this option by reloading the config will only affect newly created windows. ''' ) + +opt('forward_stdio', 'no', option_type='to_bool', long_text=''' +Forward STDOUT and STDERR of the kitty process to child processes +as file descriptors 3 and 4. This is useful for debugging as it +allows child processes to print to kitty's STDOUT directly. For example, +:code:`echo hello world >&3` in a shell will print to the parent kitty's +STDOUT. When enabled, this also sets the :code:`KITTY_STDIO_FORWARDED=3` +environment variable so child processes know about the forwarding. +''') + egr() # }}} diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 814880a96..8a9be9f18 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -990,6 +990,9 @@ class Parser: def foreground(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['foreground'] = to_color(val) + def forward_stdio(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: + ans['forward_stdio'] = to_bool(val) + def hide_window_decorations(self, val: str, ans: typing.Dict[str, typing.Any]) -> None: ans['hide_window_decorations'] = hide_window_decorations(val) diff --git a/kitty/options/types.py b/kitty/options/types.py index 8e185cbdf..4c476cf74 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -365,6 +365,7 @@ option_names = ( # {{{ 'font_size', 'force_ltr', 'foreground', + 'forward_stdio', 'hide_window_decorations', 'inactive_border_color', 'inactive_tab_background', @@ -526,6 +527,7 @@ class Options: font_size: float = 11.0 force_ltr: bool = False foreground: Color = Color(221, 221, 221) + forward_stdio: bool = False hide_window_decorations: int = 0 inactive_border_color: Color = Color(204, 204, 204) inactive_tab_background: Color = Color(153, 153, 153) diff --git a/kitty/window.py b/kitty/window.py index e9c924409..210c91e9f 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -4,7 +4,6 @@ import json import os import re -import sys import weakref from collections import deque from contextlib import contextmanager, suppress @@ -1210,8 +1209,7 @@ class Window: def handle_remote_print(self, msg: str) -> None: text = process_remote_print(msg) - print(text, end='', file=sys.stderr) - sys.stderr.flush() + print(text, end='', flush=True) def send_cmd_response(self, response: Any) -> None: self.screen.send_escape_code_to_child(DCS, '@kitty-cmd' + json.dumps(response)) diff --git a/tools/tty/tty.go b/tools/tty/tty.go index 55b2afe80..16d0c3271 100644 --- a/tools/tty/tty.go +++ b/tools/tty/tty.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "os" + "strconv" "time" "golang.org/x/sys/unix" @@ -340,6 +341,14 @@ func (self *Term) GetSize() (*unix.Winsize, error) { func Ctermid() string { return "/dev/tty" } func DebugPrintln(a ...any) { + if fds := os.Getenv(`KITTY_STDIO_FORWARDED`); fds != "" { + if fd, err := strconv.Atoi(fds); err == nil && fd > -1 { + if f := os.NewFile(uintptr(fd), ""); f != nil { + fmt.Fprintln(f, a...) + return + } + } + } term, err := OpenControllingTerm() if err == nil { defer term.Close()