A facility to easily have child programs print to kitty stdout

Works better than the kitty-print DCS escape code as that tends to get
interleaved with other writes to the tty since the loop infrastructure
writes in a separate I/O thread.
This commit is contained in:
Kovid Goyal
2023-07-21 13:02:14 +05:30
parent eecf24b986
commit 59b47a3a8f
8 changed files with 44 additions and 13 deletions

View File

@@ -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);

View File

@@ -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

View File

@@ -1322,6 +1322,7 @@ def spawn(
ready_write_fd: int,
handled_signals: Tuple[int, ...],
kitten_exe: str,
forward_stdio: bool,
) -> int:
pass

View File

@@ -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() # }}}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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))

View File

@@ -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), "<kitty_stdout>"); f != nil {
fmt.Fprintln(f, a...)
return
}
}
}
term, err := OpenControllingTerm()
if err == nil {
defer term.Close()