Remove socket prewarming

The potential for breakage is too high, and I am working on an
alternative solution that will be better long term.

Prewarming is still used for kittens launched via keybindings
This commit is contained in:
Kovid Goyal 2022-08-20 13:38:33 +05:30
parent 4cc0138a28
commit 44ccdd36d6
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
11 changed files with 13 additions and 1278 deletions

View File

@ -23,10 +23,6 @@ following dependencies are installed first.
should use a version of the source that matches the binary version as closely should use a version of the source that matches the binary version as closely
as possible, since the two are tightly coupled. as possible, since the two are tightly coupled.
You may need to unset the :envvar:`KITTY_PREWARM_SOCKET` environment variable,
otherwise your modified code will not be executed, when running some of the
``kitty +`` and ``kitty @`` commands.
Dependencies Dependencies
---------------- ----------------

View File

@ -40,7 +40,7 @@ Detailed list of changes
- A new option :opt:`remote_control_password` to use fine grained permissions for what can be remote controlled (:disc:`5320`) - A new option :opt:`remote_control_password` to use fine grained permissions for what can be remote controlled (:disc:`5320`)
- Reduce startup latency by ~30 milliseconds when running kittens and remote control commands inside kitty (:iss:`5159`) - Reduce startup latency by ~30 milliseconds when running kittens via key bindings inside kitty (:iss:`5159`)
- A new option :opt:`modify_font` to adjust various font metrics like underlines, cell sizes etc. (:pull:`5265`) - A new option :opt:`modify_font` to adjust various font metrics like underlines, cell sizes etc. (:pull:`5265`)

View File

@ -142,17 +142,6 @@ Variables that kitty sets when running child programs
A public key that programs can use to communicate securely with kitty using A public key that programs can use to communicate securely with kitty using
the remote control protocol. The format is: :code:`protocol:key data`. the remote control protocol. The format is: :code:`protocol:key data`.
.. envvar:: KITTY_PREWARM_SOCKET
Path to a UNIX domain socket used to avoid Python interpreter startup
latency when running kittens, or remote control or using ``kitty +launch`` or
``kitty +runpy``.
.. envvar:: KITTY_PREWARM_SOCKET_REAL_TTY
Path to the the PTY used to run the prewarmed process in when using
:envvar:`KITTY_PREWARM_SOCKET`.
.. envvar:: WINDOWID .. envvar:: WINDOWID
The id for the :term:`OS Window <os_window>` the program is running in. Only available The id for the :term:`OS Window <os_window>` the program is running in. Only available

View File

@ -2,9 +2,7 @@
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net> # License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
import os
import re import re
import sys
from typing import List from typing import List
from kitty.conf.generate import write_output from kitty.conf.generate import write_output
@ -24,9 +22,6 @@ def patch_color_list(path: str, colors: List[str], name: str, spc: str = ' ')
def main() -> None: def main() -> None:
if 'prewarmed' in getattr(sys, 'kitty_run_data'):
os.environ.pop('KITTY_PREWARM_SOCKET')
os.execlp(sys.executable, sys.executable, '+launch', __file__, *sys.argv[1:])
from kitty.options.definition import definition from kitty.options.definition import definition
write_output('kitty', definition) write_output('kitty', definition)
nullable_colors = [] nullable_colors = []

View File

@ -148,7 +148,6 @@ def process_env() -> Dict[str, str]:
ssl_env_var = getattr(sys, 'kitty_ssl_env_var', None) ssl_env_var = getattr(sys, 'kitty_ssl_env_var', None)
if ssl_env_var is not None: if ssl_env_var is not None:
ans.pop(ssl_env_var, None) ans.pop(ssl_env_var, None)
ans.pop('KITTY_PREWARM_SOCKET', None)
return ans return ans
@ -256,9 +255,6 @@ class Child:
env['KITTY_LISTEN_ON'] = boss.listening_on env['KITTY_LISTEN_ON'] = boss.listening_on
else: else:
env.pop('KITTY_LISTEN_ON', None) env.pop('KITTY_LISTEN_ON', None)
if not self.is_prewarmed:
env['KITTY_PREWARM_SOCKET'] = boss.prewarm.socket_env_var()
env['KITTY_PREWARM_SOCKET_REAL_TTY'] = ' ' * 32
if self.cwd: if self.cwd:
# needed in case cwd is a symlink, in which case shells # needed in case cwd is a symlink, in which case shells
# can use it to display the current directory name rather # can use it to display the current directory name rather
@ -464,11 +460,6 @@ class Child:
import termios import termios
if self.child_fd is None: if self.child_fd is None:
return False return False
tty_name = self.foreground_environ.get('KITTY_PREWARM_SOCKET_REAL_TTY')
if tty_name and tty_name.startswith('/'):
with open(os.open(tty_name, os.O_RDWR | os.O_CLOEXEC | os.O_NOCTTY, 0)) as real_tty:
t = termios.tcgetattr(real_tty.fileno())
else:
t = termios.tcgetattr(self.child_fd) t = termios.tcgetattr(self.child_fd)
if not t[3] & termios.ISIG: if not t[3] & termios.ISIG:
return False return False

View File

@ -268,11 +268,9 @@ read_exe_path(char *exe, size_t buf_sz) {
} }
#endif // }}} #endif // }}}
extern void use_prewarmed_process(int argc, char *argv[], char *envp[]);
int main(int argc, char *argv[], char* envp[]) { int main(int argc, char *argv[], char* envp[]) {
if (argc < 1 || !argv) { fprintf(stderr, "Invalid argc/argv\n"); return 1; } if (argc < 1 || !argv) { fprintf(stderr, "Invalid argc/argv\n"); return 1; }
use_prewarmed_process(argc, argv, envp);
char exe[PATH_MAX+1] = {0}; char exe[PATH_MAX+1] = {0};
char exe_dir_buf[PATH_MAX+1] = {0}; char exe_dir_buf[PATH_MAX+1] = {0};
FREE_AFTER_FUNCTION const char *lc_ctype = NULL; FREE_AFTER_FUNCTION const char *lc_ctype = NULL;

View File

@ -1,827 +0,0 @@
/*
* Copyright (C) 2022 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
// for SA_RESTART
#define _XOPEN_SOURCE 700
// for cfmakeraw
#ifdef __APPLE__
#define _DARWIN_C_SOURCE 1
const static int has_splice = 0;
#else
#define _DEFAULT_SOURCE
// for splice()
#define _GNU_SOURCE
#ifdef __linux__
#define HAS_SPLICE
const static int has_splice = 0; // sadly the kernel doesnt support splice between tty fds anymore
// https://www.spinics.net/lists/kernel/msg3799785.html
#else
const static int has_splice = 0;
#endif
#endif
// Includes {{{
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <poll.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#ifdef __APPLE__
#include <util.h>
#include <mach-o/dyld.h>
#include <sys/syslimits.h>
#include <sys/stat.h>
#else
#include <pty.h>
#endif
#include <limits.h>
#define arraysz(x) (sizeof(x)/sizeof(x[0]))
#define MAX(x, y) __extension__ ({ \
__typeof__ (x) a = (x); __typeof__ (y) b = (y); \
a > b ? a : b;})
#define MIN(x, y) __extension__ ({ \
__typeof__ (x) a = (x); __typeof__ (y) b = (y); \
a < b ? a : b;})
// }}}
#define IO_BUZ_SZ 8192
#define remove_i_from_array(array, i, count) { \
(count)--; \
if ((i) < (count)) { \
memmove((array) + (i), (array) + (i) + 1, sizeof((array)[0]) * ((count) - (i))); \
}}
// ancient macs (10.15) have no MSG_NOSIGNAL
#ifndef MSG_NOSIGNAL
# define MSG_NOSIGNAL 0
# ifdef SO_NOSIGPIPE
# define KITTY_USE_SO_NOSIGPIPE
# else
# error "Cannot block SIGPIPE!"
# endif
#endif
typedef struct transfer_buf {
char *buf;
size_t sz;
} transfer_buf;
static transfer_buf from_child_tty = {0};
static transfer_buf to_child_tty = {0};
static char child_tty_name[256];
static int child_master_fd = -1, child_slave_fd = -1;
static struct winsize self_winsize = {0};
static struct termios self_termios = {0}, restore_termios = {0};
static bool termios_needs_restore = false;
static int self_ttyfd = -1, socket_fd = -1, signal_read_fd = -1, signal_write_fd = -1;
static int stdin_pos = -1, stdout_pos = -1, stderr_pos = -1;
static char fd_send_buf[256];
struct iovec launch_msg = {0};
struct msghdr launch_msg_container = {.msg_control = fd_send_buf, .msg_controllen = sizeof(fd_send_buf), .msg_iov = &launch_msg, .msg_iovlen = 1 };
static size_t launch_msg_cap = 0;
char *launch_msg_buf = NULL;
static pid_t child_pid = 0;
static void
left_shift_buffer(transfer_buf *t, size_t n) {
if (t->sz > n) {
t->sz -= n;
memmove(t->buf, t->buf + n, t->sz);
} else t->sz = 0;
}
#define err_prefix "prewarm wrapper process error: "
static inline void
print_error(const char *s, int errnum) {
if (errnum != 0) fprintf(stderr, "%s%s: %s\n\r", err_prefix, s, strerror(errnum));
else fprintf(stderr, "%s%s\n\r", err_prefix, s);
}
#define pe(fmt, ...) { fprintf(stderr, err_prefix); fprintf(stderr, fmt, __VA_ARGS__); fprintf(stderr, "\n\r"); }
static bool
parse_long(const char *str, long *val) {
char *temp;
bool rc = true;
errno = 0;
const long t = strtol(str, &temp, 0);
if (temp == str || *temp != '\0' || ((*val == LONG_MIN || *val == LONG_MAX) && errno == ERANGE)) rc = false;
*val = t;
return rc;
}
static inline int
safe_open(const char *path, int flags, mode_t mode) {
while (true) {
int fd = open(path, flags, mode);
if (fd == -1 && errno == EINTR) continue;
return fd;
}
}
static inline void
safe_close(int fd) {
while(close(fd) != 0 && errno == EINTR);
}
static inline int
safe_dup2(int a, int b) {
int ret;
while((ret = dup2(a, b)) < 0 && errno == EINTR);
return ret;
}
static inline bool
safe_tcsetattr(int fd, int actions, const struct termios *tp) {
int ret = 0;
while((ret = tcsetattr(fd, actions, tp)) != 0 && errno == EINTR);
return ret == 0;
}
static ssize_t
safe_read(int fd, void *buf, size_t n) {
ssize_t ret = 0;
while((ret = read(fd, buf, n)) ==-1 && errno == EINTR);
return ret;
}
static ssize_t
safe_send(int fd, void *buf, size_t n, int flags) {
ssize_t ret = 0;
while((ret = send(fd, buf, n, flags)) ==-1 && errno == EINTR);
return ret;
}
static ssize_t
safe_write(int fd, void *buf, size_t n) {
ssize_t ret = 0;
while((ret = write(fd, buf, n)) ==-1 && errno == EINTR);
return ret;
}
static bool
set_blocking(int fd, bool blocking) {
if (fd < 0) return false;
int flags = fcntl(fd, F_GETFL, 0);
if (flags == -1) return false;
flags = blocking ? (flags & ~O_NONBLOCK) : (flags | O_NONBLOCK);
return (fcntl(fd, F_SETFL, flags) == 0) ? true : false;
}
static int
connect_to_socket_synchronously(const char *addr) {
struct sockaddr_un sock_addr = {.sun_family=AF_UNIX};
strncpy(sock_addr.sun_path, addr, sizeof(sock_addr.sun_path) - 1);
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
#ifdef __linux__
const size_t addrlen = strnlen(sock_addr.sun_path, sizeof(sock_addr.sun_path)) + sizeof(sock_addr.sun_family);
if (sock_addr.sun_path[0] == '@') sock_addr.sun_path[0] = 0;
#else
const size_t addrlen = sizeof(sock_addr);
#endif
if (connect(fd, (struct sockaddr*)&sock_addr, addrlen) != 0) {
if (errno != EINTR && errno != EINPROGRESS) return -1;
struct pollfd poll_data = {.fd=fd, .events=POLLOUT};
while (poll (&poll_data, 1, -1) == -1) { if (errno != EINTR) return -1; }
int socket_error_code = 0;
socklen_t sizeof_socket_error_code = sizeof(socket_error_code);
if (getsockopt (fd, SOL_SOCKET, SO_ERROR, &socket_error_code, &sizeof_socket_error_code) == -1) return -1;
if (socket_error_code != 0) return -1;
}
return fd;
}
static bool
is_prewarmable(int argc, char *argv[]) {
if (argc < 2) return false;
switch (argv[1][0]) {
case '+':
if (argv[1][1] != 0) return strcmp(argv[1], "+open") != 0;
if (argc < 3) return false;
return strcmp(argv[2], "open") != 0;
break;
case '@':
return true;
break;
default:
return false;
break;
}
return false;
}
static bool
get_termios_state(struct termios *t) {
while (tcgetattr(self_ttyfd, t) != 0) {
if (errno != EINTR) return false;
}
return true;
}
static void
restore_termios_settings(void) {
if (self_ttyfd > -1 && termios_needs_restore) {
// we only restore termios if its current state is the same as the result of cfmakeraw() on the startup termios state
// this is to try to detect if something like less changed the termios state. This allows, for instance kitty @ ls | less to work
struct termios current_state;
if (get_termios_state(&current_state) && memcmp(&self_termios, &current_state, sizeof(current_state)) == 0) {
safe_tcsetattr(self_ttyfd, TCSAFLUSH, &restore_termios);
termios_needs_restore = false;
}
}
}
static void
reapply_termios_settings(void) {
safe_tcsetattr(self_ttyfd, TCSANOW, &self_termios);
termios_needs_restore = true;
}
static void
cleanup(void) {
child_pid = 0;
restore_termios_settings();
#define cfd(fd) if (fd > -1) { safe_close(fd); fd = -1; }
cfd(child_master_fd); cfd(child_slave_fd);
cfd(self_ttyfd); cfd(socket_fd); cfd(signal_read_fd); cfd(signal_write_fd);
#undef cfd
if (launch_msg_buf) { free(launch_msg_buf); launch_msg.iov_len = 0; launch_msg_buf = NULL; }
if (from_child_tty.buf) { free(from_child_tty.buf); from_child_tty.buf = NULL; }
}
static bool
safe_winsz(int fd, int action, struct winsize *ws) {
int ret;
while ((ret = ioctl(fd, action, ws)) == -1 && errno == EINTR);
return ret != -1;
}
static bool
get_window_size(void) {
return safe_winsz(self_ttyfd, TIOCGWINSZ, &self_winsize);
}
bool
set_iutf8(int fd, bool on) {
(void)fd; (void)on;
#ifdef IUTF8
struct termios attrs;
if (tcgetattr(fd, &attrs) != 0) return false;
if (on) attrs.c_iflag |= IUTF8;
else attrs.c_iflag &= ~IUTF8;
if (tcsetattr(fd, TCSANOW, &attrs) != 0) return false;
#endif
return true;
}
static bool
open_pty(void) {
while (openpty(&child_master_fd, &child_slave_fd, child_tty_name, &self_termios, &self_winsize) == -1) {
if (errno != EINTR) return false;
}
return set_iutf8(child_master_fd, true);
}
static void
handle_signal(int sig_num, siginfo_t *si, void *ucontext) {
(void)sig_num; (void)ucontext;
int save_err = errno;
char *buf = (char*)si;
size_t sz = sizeof(siginfo_t);
while (signal_write_fd != -1 && sz) {
// as long as sz is less than PIPE_BUF write will either write all or return -1 with EAGAIN
// so we are guaranteed atomic writes, barring implementation bugs
ssize_t ret = safe_write(signal_write_fd, buf, sz);
if (ret <= 0) break;
sz -= ret;
buf += ret;
}
errno = save_err;
}
static bool
setup_signal_handler(void) {
int fds[2];
if (pipe(fds) != 0) return false;
signal_read_fd = fds[0]; signal_write_fd = fds[1];
set_blocking(signal_write_fd, false);
sigset_t masked_signals;
sigemptyset(&masked_signals);
sigaddset(&masked_signals, SIGWINCH);
sigaddset(&masked_signals, SIGINT);
sigaddset(&masked_signals, SIGTERM);
sigaddset(&masked_signals, SIGQUIT);
sigaddset(&masked_signals, SIGHUP);
sigaddset(&masked_signals, SIGTSTP);
struct sigaction act = {.sa_sigaction=handle_signal, .sa_flags=SA_SIGINFO | SA_RESTART, .sa_mask = masked_signals};
#define a(which) if (sigaction(which, &act, NULL) != 0) return false;
a(SIGWINCH); a(SIGINT); a(SIGTERM); a(SIGQUIT); a(SIGHUP); a(SIGTSTP);
#undef a
return true;
}
static void
setup_stdio_handles(void) {
int pos = 0;
if (!isatty(STDIN_FILENO)) stdin_pos = pos++;
if (!isatty(STDOUT_FILENO)) {
stdout_pos = pos++;
}
if (!isatty(STDERR_FILENO)) stderr_pos = pos++;
}
static bool
ensure_launch_msg_space(size_t sz) {
if (launch_msg_cap > launch_msg.iov_len + sz) return true;
const size_t c = MAX(2 * launch_msg_cap, launch_msg_cap + launch_msg.iov_len + sz + 8);
launch_msg_cap = MAX(c, 64 * 1024);
launch_msg_buf = realloc(launch_msg_buf, launch_msg_cap);
return launch_msg_buf != NULL;
}
static bool
write_item_to_launch_msg(const char *prefix, const char *data) {
size_t prefixlen = strlen(prefix), datalen = strlen(data), msg_len = 8 + prefixlen + datalen;
if (!ensure_launch_msg_space(msg_len)) return false;
memcpy(launch_msg_buf + launch_msg.iov_len, prefix, prefixlen);
launch_msg.iov_len += prefixlen;
launch_msg_buf[launch_msg.iov_len++] = ':';
memcpy(launch_msg_buf + launch_msg.iov_len, data, datalen);
launch_msg.iov_len += datalen;
launch_msg_buf[launch_msg.iov_len++] = 0;
launch_msg.iov_base = launch_msg_buf;
return true;
}
extern char **environ;
static bool
create_launch_msg(int argc, char *argv[]) {
#define w(prefix, data) { if (!write_item_to_launch_msg(prefix, data)) return false; }
static char buf[4*PATH_MAX];
w("tty_name", child_tty_name);
snprintf(buf, sizeof(buf), "%zu", sizeof(self_winsize));
w("winsize", buf);
if (getcwd(buf, sizeof(buf))) { w("cwd", buf); }
for (int i = 0; i < argc; i++) w("argv", argv[i]);
char **s = environ;
for (; *s; s++) w("env", *s);
int num_fds = 0, fds[4];
#define sio(which, x) if (which##_pos > -1) { snprintf(buf, sizeof(buf), "%d", which##_pos); w(#which, buf); fds[num_fds++] = x; }
sio(stdin, STDIN_FILENO); sio(stdout, STDOUT_FILENO); sio(stderr, STDERR_FILENO);
#undef sio
w("finish", "");
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&launch_msg_container);
cmsg->cmsg_len = CMSG_LEN(sizeof(fds[0]) * num_fds);
memcpy(CMSG_DATA(cmsg), fds, num_fds * sizeof(fds[0]));
launch_msg_container.msg_controllen = cmsg->cmsg_len;
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
return true;
#undef w
}
static int exit_status = EXIT_FAILURE;
static int pending_signals[32] = {0};
enum ChildState { CHILD_NOT_STARTED, CHILD_STARTED, CHILD_STOPPED, CHILD_EXITED };
static enum ChildState child_state = CHILD_NOT_STARTED;
static void flush_data(void);
static bool
read_from_zygote(void) {
ssize_t n;
static char buf[64];
static transfer_buf t = {.buf=buf};
n = safe_read(socket_fd, t.buf + t.sz, sizeof(buf) - t.sz);
if (n < 0) {
if (errno == EIO || errno == EPIPE) { socket_fd = -1; return true; }
return false;
}
if (n) {
t.sz += n;
while (t.sz >= sizeof(long long)) {
long long val = *((long long*)t.buf);
left_shift_buffer(&t, sizeof(long long));
if (child_state == CHILD_NOT_STARTED) {
if (val == 0) { print_error("Got zero child pid from prewarm socket", 0); return false; }
child_pid = val;
child_state = CHILD_STARTED;
if (child_slave_fd > -1) { safe_close(child_slave_fd); child_slave_fd = -1; }
for (size_t i = 0; i < arraysz(pending_signals) && pending_signals[i]; i++) {
kill(child_pid, pending_signals[i]);
}
memset(pending_signals, 0, sizeof(pending_signals));
} else {
int child_exit_status = val;
if (WIFEXITED(child_exit_status)) {
exit_status = WEXITSTATUS(child_exit_status);
child_pid = 0;
child_state = CHILD_EXITED;
} else if (WIFSIGNALED(child_exit_status)) {
int signum = WTERMSIG(child_exit_status);
if (signum > 0) {
flush_data();
cleanup();
signal(signum, SIG_DFL);
raise(signum);
_exit(1);
}
} else if (WIFSTOPPED(child_exit_status)) {
child_state = CHILD_STOPPED;
int signum = WSTOPSIG(child_exit_status);
restore_termios_settings();
struct sigaction defval = {.sa_handler = SIG_DFL}, original;
sigaction(signum, &defval, &original);
raise(signum);
sigaction(signum, &original, NULL);
reapply_termios_settings();
if (child_pid > 0) {
kill(child_pid, SIGCONT);
}
child_state = CHILD_STARTED;
}
}
}
} else { socket_fd = -1; return true; }
return true;
}
static void
close_sent_fds(void) {
#define redirect(which, mode) { int fd = safe_open("/dev/null", mode | O_CLOEXEC, 0); if (fd > -1) { safe_dup2(fd, which); safe_close(fd); } }
if (stdin_pos > -1) redirect(STDIN_FILENO, O_RDONLY);
if (stdout_pos > -1) redirect(STDOUT_FILENO, O_WRONLY);
if (stderr_pos > -1) redirect(STDERR_FILENO, O_WRONLY);
#undef redirect
}
static bool
send_launch_msg(void) {
ssize_t n;
while ((n = sendmsg(socket_fd, &launch_msg_container, MSG_NOSIGNAL)) < 0 && errno == EINTR);
if (n < 0) return false;
if (n == 0) { errno = EPIPE; return false; }
// some bytes sent, null out the control msg data as it is already sent
launch_msg_container.msg_controllen = 0;
launch_msg_container.msg_control = NULL;
if ((size_t)n > launch_msg.iov_len) {
launch_msg.iov_len = 0;
close_sent_fds();
}
else launch_msg.iov_len -= n;
launch_msg.iov_base = (char*)launch_msg.iov_base + n;
return true;
}
struct fd_to_watch {
bool want_read, want_write, want_error;
};
struct watched_fds {
struct fd_to_watch self_ttyfd, signal_read_fd, socket_fd, child_master_fd;
};
static struct watched_fds wf = {0};
static bool
read_from_tty(int *fd, transfer_buf *t) {
if (*fd < 0) return true;
if (t->sz < IO_BUZ_SZ) {
ssize_t n = safe_read(*fd, t->buf + t->sz, IO_BUZ_SZ - t->sz);
if (n < 0) {
if (errno == EPIPE || errno == EIO) { *fd = -1; return true; }
if (errno == EAGAIN) return true;
return false;
}
if (n == 0) *fd = -1; // hangup
t->sz += n;
}
return true;
}
static bool
read_from_child_tty(void) {
return read_from_tty(&child_master_fd, &from_child_tty);
}
static bool
write_to_tty(transfer_buf *src, int *dest_fd) {
if (*dest_fd < 0) return true;
if (src->sz) {
ssize_t n = safe_write(*dest_fd, src->buf, src->sz);
if (n < 0) {
if (errno == EPIPE || errno == EIO) { *dest_fd = -1; return true; }
return false;
}
if (n > 0) {
left_shift_buffer(src, n);
} else *dest_fd = -1;
}
return true;
}
static bool
from_child_to_self(void) {
return write_to_tty(&from_child_tty, &self_ttyfd);
}
static bool
from_self_to_child(void) {
return write_to_tty(&to_child_tty, &child_master_fd);
}
static bool
read_from_self_tty(void) {
return read_from_tty(&self_ttyfd, &to_child_tty);
}
static bool window_size_dirty = false;
static bool
read_signals(void) {
static char buf[sizeof(siginfo_t) * 8];
static transfer_buf b = {.buf=buf};
ssize_t len = safe_read(signal_read_fd, buf + b.sz, sizeof(buf) - b.sz);
if (len < 0) return false;
if (len == 0) return true;
b.sz += len;
while (b.sz >= sizeof(siginfo_t)) {
siginfo_t *sig = (siginfo_t*)buf;
switch(sig->si_signo) {
case SIGWINCH:
window_size_dirty = true; break;
case SIGINT: case SIGTERM: case SIGHUP: case SIGQUIT: case SIGTSTP:
if (child_pid > 0) kill(child_pid, sig->si_signo);
else {
for (size_t i = 0; i < arraysz(pending_signals); i++) {
if (!pending_signals[i]) {
pending_signals[i] = sig->si_signo;
break;
}
}
}
break;
}
left_shift_buffer(&b, sizeof(siginfo_t));
}
return true;
}
static bool
keep_going(void) {
switch(child_state) {
case CHILD_NOT_STARTED:
return self_ttyfd > -1 && signal_read_fd > -1 && socket_fd > -1 && child_master_fd > -1;
case CHILD_STARTED:
case CHILD_STOPPED:
return self_ttyfd > -1 && signal_read_fd > -1 && socket_fd > -1;
case CHILD_EXITED:
return self_ttyfd > -1 && signal_read_fd > -1 && child_master_fd > -1;
}
return false;
}
static void
flush_data(void) {
if (self_ttyfd > -1 && from_child_tty.sz > 0) {
set_blocking(self_ttyfd, false);
from_child_to_self();
}
if (child_master_fd > -1 && from_child_tty.sz < IO_BUZ_SZ) {
set_blocking(child_master_fd, false);
read_from_child_tty();
}
if (self_ttyfd > -1 && from_child_tty.sz > 0) {
from_child_to_self();
}
}
static char sosbuf[2 * sizeof(self_winsize)] = {0};
static transfer_buf send_on_socket = {.buf=sosbuf};
static void
add_window_size_to_buffer(void) {
char *p;
if (send_on_socket.sz % sizeof(self_winsize)) {
// partial send
if (send_on_socket.sz > sizeof(self_winsize)) send_on_socket.sz -= sizeof(self_winsize); // replace second size
p = send_on_socket.buf + send_on_socket.sz;
send_on_socket.sz += sizeof(self_winsize);
} else {
// replace all sizes
p = send_on_socket.buf;
send_on_socket.sz = sizeof(self_winsize);
}
memcpy(p, &self_winsize, sizeof(self_winsize));
}
static bool
send_over_socket(void) {
if (!send_on_socket.sz || socket_fd < 0) return true;
ssize_t n = safe_send(socket_fd, send_on_socket.buf, send_on_socket.sz, MSG_NOSIGNAL);
if (n < 0) return false;
if (n) {
if (n >= send_on_socket.sz) send_on_socket.sz = 0;
else {
left_shift_buffer(&send_on_socket, n);
}
}
return true;
}
static void
set_append_mode(int fd, bool yes) {
int val = fcntl(fd, F_GETFL);
if (yes) val |= O_APPEND; else val &= ~O_APPEND;
fcntl(fd, F_SETFL, val);
}
#ifndef HAS_SPLICE
#define splice(...) -1
#endif
static bool
transfer_in_kernel(int from, int to, bool *read_failed) {
if (has_splice) {
ssize_t n = 0;
while ((n = splice(from, NULL, to, NULL, PIPE_BUF, 0)) == -1 && errno == EINTR);
*read_failed = n == 0;
return n > 0;
}
return false;
}
static void
loop(void) {
#define fail(s) { print_error(s, errno); return; }
int ret, nfds = 0;
#define init(which) wf.which.want_read = true; nfds = MAX(which, nfds);
init(self_ttyfd); init(signal_read_fd); init(socket_fd); init(child_master_fd);
#undef init
fd_set readable, writable, errorable;
nfds++;
struct { bool data_available_from_child, data_available_from_self; } sd;
if (has_splice) { set_append_mode(child_master_fd, false); set_append_mode(self_ttyfd, false); }
while (keep_going()) {
if (window_size_dirty) {
if (!get_window_size()) fail("getting window size for self tty failed");
// macOS barfs with ENOTTY if we try to use TIOCSWINSZ from this process, so send it to the zygote
/* if (!safe_winsz(child_master_fd, TIOCSWINSZ, &self_winsize)) fail("setting window size on child pty failed"); */
add_window_size_to_buffer();
window_size_dirty = false;
}
const bool can_splice = has_splice && child_master_fd > -1 && self_ttyfd > -1;
static bool splice_ok = true;
if (can_splice && splice_ok) {
wf.self_ttyfd.want_read = !sd.data_available_from_self;
wf.self_ttyfd.want_write = sd.data_available_from_child;
wf.child_master_fd.want_read = !sd.data_available_from_child;
wf.child_master_fd.want_write = sd.data_available_from_self;
if (!splice_ok) { set_append_mode(self_ttyfd, true); set_append_mode(child_master_fd, true); }
} else {
wf.self_ttyfd.want_read = to_child_tty.sz < IO_BUZ_SZ; wf.self_ttyfd.want_write = from_child_tty.sz > 0;
wf.child_master_fd.want_read = from_child_tty.sz < IO_BUZ_SZ; wf.child_master_fd.want_write = to_child_tty.sz > 0;
}
wf.socket_fd.want_write = launch_msg.iov_len > 0 || send_on_socket.sz > 0;
FD_ZERO(&readable); FD_ZERO(&writable); FD_ZERO(&errorable);
#define set(which) if (which > -1) { if (wf.which.want_read) { FD_SET(which, &readable); } if (wf.which.want_write) { FD_SET(which, &writable); } if (wf.which.want_error) { FD_SET(which, &errorable); } }
set(self_ttyfd); set(child_master_fd); set(socket_fd); set(signal_read_fd);
#undef set
while ((ret = select(nfds, &readable, &writable, &errorable, NULL)) == -1) { if (errno != EINTR) fail("select() failed"); }
if (!ret) continue;
if (can_splice && splice_ok) {
bool read_failed = false;
if (FD_ISSET(self_ttyfd, &readable)) sd.data_available_from_self = true;
if (sd.data_available_from_self && FD_ISSET(child_master_fd, &writable)) {
splice_ok = transfer_in_kernel(self_ttyfd, child_master_fd, &read_failed);
sd.data_available_from_self = false;
if (read_failed) fail("reading in kernel from self tty failed");
}
if (FD_ISSET(child_master_fd, &readable)) sd.data_available_from_child = true;
if (sd.data_available_from_child && FD_ISSET(self_ttyfd, &writable) && splice_ok) {
splice_ok = transfer_in_kernel(child_master_fd, self_ttyfd, &read_failed);
sd.data_available_from_child = false;
if (read_failed) fail("reading in kernel from child tty failed");
}
} else {
if (child_master_fd > -1) {
if (FD_ISSET(child_master_fd, &writable)) if (!from_self_to_child()) fail("writing to child tty failed");
if (FD_ISSET(child_master_fd, &readable)) {
if (!read_from_child_tty()) fail("reading from child tty failed");
}
}
if (self_ttyfd > -1) {
if (FD_ISSET(self_ttyfd, &readable)) if (!read_from_self_tty()) fail("reading from self tty failed");
if (FD_ISSET(self_ttyfd, &writable)) if (!from_child_to_self()) fail("writing to self tty failed");
}
}
if (signal_read_fd > -1 && FD_ISSET(signal_read_fd, &readable)) if (!read_signals()) fail("reading from signal fd failed");
if (socket_fd > -1) {
if (FD_ISSET(socket_fd, &writable)) {
if (launch_msg.iov_len > 0) { if (!send_launch_msg()) fail("sending launch message failed"); }
else if (send_on_socket.sz > 0) { if (!send_over_socket()) fail("sending on socket failed"); }
}
if (FD_ISSET(socket_fd, &readable)) {
if (!read_from_zygote()) fail("reading information about child failed");
if (socket_fd < 0) { // hangup
child_pid = 0;
child_state = CHILD_EXITED;
}
}
}
}
#undef fail
}
static char*
check_socket_addr(char *addr) {
char *p = strchr(addr, ':');
if (!p) return NULL;
*p = 0;
long val = -1;
bool ok = parse_long(addr, &val);
*p = ':';
if (!ok || val != geteuid()) return NULL;
addr = p + 1;
p = strchr(addr, ':');
if (!p) return NULL;
*p = 0;
ok = parse_long(addr, &val);
*p = ':';
if (!ok || val != getegid()) return NULL;
return p + 1;
}
void
use_prewarmed_process(int argc, char *argv[], char *envp[]) {
char *env_addr = getenv("KITTY_PREWARM_SOCKET");
if (!env_addr || !*env_addr || !is_prewarmable(argc, argv)) return;
env_addr = check_socket_addr(env_addr);
if (!env_addr) return;
self_ttyfd = safe_open(ctermid(NULL), O_RDWR | O_NONBLOCK, 0);
if (self_ttyfd < 0) return;
setup_stdio_handles();
#define fail(s) { print_error(s, errno); cleanup(); return; }
if (!setup_signal_handler()) fail("Failed to setup signal handling");
if (!get_window_size()) fail("Failed to get window size of controlling terminal");
if (!get_termios_state(&self_termios)) fail("Failed to get termios state of controlling terminal");
if (!open_pty()) fail("Failed to open slave pty");
memcpy(&restore_termios, &self_termios, sizeof(restore_termios));
termios_needs_restore = true;
cfmakeraw(&self_termios);
if (!safe_tcsetattr(self_ttyfd, TCSANOW, &self_termios)) fail("Failed to put tty into raw mode");
if (!create_launch_msg(argc, argv)) fail("Failed to create launch message");
socket_fd = connect_to_socket_synchronously(env_addr);
if (socket_fd < 0) fail("Failed to connect to prewarm socket");
#ifdef KITTY_USE_SO_NOSIGPIPE
{
int val = 1;
if (setsockopt(socket_fd, SOL_SOCKET, SO_NOSIGPIPE, (void*)&val, sizeof(val)) != 0) fail("Failed to set SO_NOSIGPIPE");
}
#endif
from_child_tty.buf = malloc(IO_BUZ_SZ * 2);
if (!from_child_tty.buf) fail("Out of memory allocating IO buffer");
to_child_tty.buf = from_child_tty.buf + IO_BUZ_SZ;
#undef fail
while (*envp) {
char *p = *envp;
const char *q = "KITTY_PREWARM_SOCKET_REAL_TTY=";
while (*p && *q && *p == *q) {
if (*p == '=') {
p++;
snprintf(p, strlen(p) + 1, "%s", child_tty_name);
break;
}
p++; q++;
}
envp++;
}
loop();
flush_data();
cleanup();
exit(exit_status);
}

View File

@ -1,15 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python
# License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net> # License: GPLv3 Copyright: 2022, Kovid Goyal <kovid at kovidgoyal.net>
import errno
import fcntl import fcntl
import io import io
import json import json
import os import os
import select import select
import signal import signal
import socket
import struct
import sys import sys
import termios import termios
import time import time
@ -27,14 +24,14 @@ from typing import (
from kitty.constants import kitty_exe, running_in_kitty from kitty.constants import kitty_exe, running_in_kitty
from kitty.entry_points import main as main_entry_point from kitty.entry_points import main as main_entry_point
from kitty.fast_data_types import ( from kitty.fast_data_types import (
CLD_EXITED, CLD_KILLED, CLD_STOPPED, get_options, getpeereid, CLD_EXITED, CLD_KILLED, CLD_STOPPED, get_options, install_signal_handlers,
install_signal_handlers, read_signals, remove_signal_handlers, safe_pipe, read_signals, remove_signal_handlers, safe_pipe, set_options,
set_options, set_use_os_log set_use_os_log
) )
from kitty.options.types import Options from kitty.options.types import Options
from kitty.shm import SharedMemory from kitty.shm import SharedMemory
from kitty.types import SignalInfo from kitty.types import SignalInfo
from kitty.utils import log_error, random_unix_socket, safer_fork from kitty.utils import log_error, safer_fork
if TYPE_CHECKING: if TYPE_CHECKING:
from _typeshed import ReadableBuffer, WriteableBuffer from _typeshed import ReadableBuffer, WriteableBuffer
@ -90,7 +87,6 @@ class PrewarmProcess:
to_prewarm_stdin: int, to_prewarm_stdin: int,
from_prewarm_stdout: int, from_prewarm_stdout: int,
from_prewarm_death_notify: int, from_prewarm_death_notify: int,
unix_socket_name: str,
) -> None: ) -> None:
self.children: Dict[int, Child] = {} self.children: Dict[int, Child] = {}
self.worker_pid = prewarm_process_pid self.worker_pid = prewarm_process_pid
@ -99,10 +95,6 @@ class PrewarmProcess:
self.read_from_process_fd = from_prewarm_stdout self.read_from_process_fd = from_prewarm_stdout
self.poll = select.poll() self.poll = select.poll()
self.poll.register(self.read_from_process_fd, select.POLLIN) self.poll.register(self.read_from_process_fd, select.POLLIN)
self.unix_socket_name = unix_socket_name
def socket_env_var(self) -> str:
return f'{os.geteuid()}:{os.getegid()}:{self.unix_socket_name}'
def take_from_worker_fd(self, create_file: bool = False) -> int: def take_from_worker_fd(self, create_file: bool = False) -> int:
if create_file: if create_file:
@ -375,20 +367,6 @@ def fork(shm_address: str, free_non_child_resources: Callable[[], None]) -> Tupl
return 0, -1 # type: ignore return 0, -1 # type: ignore
def verify_socket_creds(conn: socket.socket) -> bool:
# needed as abstract unix sockets used on Linux have no permissions and
# older BSDs ignore socket file permissions
uid, gid = getpeereid(conn.fileno())
return uid == os.geteuid() and gid == os.getegid()
class SocketChildData:
def __init__(self) -> None:
self.cwd = self.tty_name = ''
self.argv: List[str] = []
self.env: Dict[str, str] = {}
Funtion = TypeVar('Funtion', bound=Callable[..., Any]) Funtion = TypeVar('Funtion', bound=Callable[..., Any])
@ -421,218 +399,7 @@ interactive_and_job_control_signals = (
) )
def fork_socket_child(child_data: SocketChildData, tty_fd: int, stdio_fds: Dict[str, int], free_non_child_resources: Callable[[], None]) -> int: def main(stdin_fd: int, stdout_fd: int, notify_child_death_fd: int) -> None:
# see https://www.gnu.org/software/libc/manual/html_node/Launching-Jobs.html
child_pid = safer_fork()
if child_pid:
return child_pid
# child process
eintr_retry(os.setpgid)(0, 0)
eintr_retry(os.tcsetpgrp)(tty_fd, eintr_retry(os.getpgid)(0))
for x in interactive_and_job_control_signals:
signal.signal(x, signal.SIG_DFL)
restore_python_signal_handlers()
# the std streams fds are closed in free_non_child_resources()
for which in ('stdin', 'stdout', 'stderr'):
fd = stdio_fds[which] if stdio_fds[which] > -1 else tty_fd
safe_dup2(fd, getattr(sys, which).fileno())
free_non_child_resources()
child_main({'cwd': child_data.cwd, 'env': child_data.env, 'argv': child_data.argv}, prewarm_type='socket')
def fork_socket_child_supervisor(conn: socket.socket, free_non_child_resources: Callable[[], None]) -> None:
import array
global is_zygote
if safer_fork():
conn.close()
return
is_zygote = False
os.setsid()
restore_python_signal_handlers()
free_non_child_resources()
signal_read_fd = install_signal_handlers(signal.SIGCHLD, signal.SIGUSR1)[0]
# See https://www.gnu.org/software/libc/manual/html_node/Initializing-the-Shell.html
for x in interactive_and_job_control_signals:
signal.signal(x, signal.SIG_IGN)
poll = select.poll()
poll.register(signal_read_fd, select.POLLIN)
from_socket_buf = b''
to_socket_buf = b''
keep_going = True
child_pid = -1
socket_fd = conn.fileno()
launch_msg_read = False
os.set_blocking(socket_fd, False)
received_fds: List[int] = []
stdio_positions = dict.fromkeys(('stdin', 'stdout', 'stderr'), -1)
stdio_fds = dict.fromkeys(('stdin', 'stdout', 'stderr'), -1)
winsize = 8
exit_after_write = False
child_data = SocketChildData()
def handle_signal(siginfo: SignalInfo) -> None:
nonlocal to_socket_buf, exit_after_write, child_pid
if siginfo.si_signo != signal.SIGCHLD or siginfo.si_code not in (CLD_KILLED, CLD_EXITED, CLD_STOPPED):
return
while True:
try:
pid, status = os.waitpid(-1, os.WNOHANG | os.WUNTRACED)
except ChildProcessError:
pid = 0
if not pid:
break
if pid != child_pid:
continue
to_socket_buf += struct.pack('q', status)
if not os.WIFSTOPPED(status):
exit_after_write = True
child_pid = -1
def write_to_socket() -> None:
nonlocal keep_going, to_socket_buf, keep_going
buf = memoryview(to_socket_buf)
while buf:
try:
n = os.write(socket_fd, buf)
except OSError:
n = 0
if n == 0:
keep_going = False
return
buf = buf[n:]
to_socket_buf = bytes(buf)
if exit_after_write and not to_socket_buf:
keep_going = False
def read_winsize() -> None:
nonlocal from_socket_buf
msg = conn.recv(io.DEFAULT_BUFFER_SIZE)
if not msg:
return
from_socket_buf += msg
data = memoryview(from_socket_buf)
record = memoryview(b'')
while len(data) >= winsize:
record, data = data[:winsize], data[winsize:]
if record:
try:
with open(safe_open(os.ctermid(), os.O_RDWR | os.O_CLOEXEC), 'w') as f:
safe_ioctl(f.fileno(), termios.TIOCSWINSZ, record)
except OSError:
traceback.print_exc()
from_socket_buf = bytes(data)
def read_launch_msg() -> bool:
nonlocal keep_going, from_socket_buf, launch_msg_read, winsize
try:
msg, ancdata, flags, addr = conn.recvmsg(io.DEFAULT_BUFFER_SIZE, 1024)
except OSError as e:
if e.errno == errno.ENOMEM:
# macOS does this when no ancilliary data is present
msg, ancdata, flags, addr = conn.recvmsg(io.DEFAULT_BUFFER_SIZE)
else:
raise
for cmsg_level, cmsg_type, cmsg_data in ancdata:
if cmsg_level == socket.SOL_SOCKET and cmsg_type == socket.SCM_RIGHTS:
fds = array.array("i") # Array of ints
# Append data, ignoring any truncated integers at the end.
fds.frombytes(cmsg_data[:len(cmsg_data) - (len(cmsg_data) % fds.itemsize)])
received_fds.extend(fds)
if not msg:
return False
from_socket_buf += msg
while (idx := from_socket_buf.find(b'\0')) > -1:
line = from_socket_buf[:idx].decode('utf-8')
from_socket_buf = from_socket_buf[idx+1:]
cmd, _, payload = line.partition(':')
if cmd == 'finish':
for x in received_fds:
os.set_inheritable(x, True)
os.set_blocking(x, True)
for k, pos in stdio_positions.items():
if pos > -1:
stdio_fds[k] = received_fds[pos]
del received_fds[:]
return True
elif cmd == 'cwd':
child_data.cwd = payload
elif cmd == 'env':
k, _, v = payload.partition('=')
child_data.env[k] = v
elif cmd == 'argv':
child_data.argv.append(payload)
elif cmd in stdio_positions:
stdio_positions[cmd] = int(payload)
elif cmd == 'tty_name':
child_data.tty_name = payload
elif cmd == 'winsize':
winsize = int(payload)
return False
def free_non_child_resources2() -> None:
for fd in received_fds:
safe_close(fd)
for k, v in tuple(stdio_fds.items()):
if v > -1:
safe_close(v)
stdio_fds[k] = -1
conn.close()
def launch_child() -> None:
nonlocal to_socket_buf, child_pid
sys.__stdout__.flush()
sys.__stderr__.flush()
tty_fd = establish_controlling_tty(child_data.tty_name, closefd=False)
child_pid = fork_socket_child(child_data, tty_fd, stdio_fds, free_non_child_resources2)
if child_pid:
# this is also done in the child process, but we dont
# know when, so do it here as well
eintr_retry(os.setpgid)(child_pid, child_pid)
eintr_retry(os.tcsetpgrp)(tty_fd, child_pid)
for fd in stdio_fds.values():
if fd > -1:
safe_close(fd)
safe_close(tty_fd)
else:
raise SystemExit('fork_socket_child() returned in the child process')
to_socket_buf += struct.pack('q', child_pid)
def read_from_socket() -> None:
nonlocal launch_msg_read
if launch_msg_read:
read_winsize()
else:
if read_launch_msg():
launch_msg_read = True
launch_child()
try:
while keep_going:
poll.register(socket_fd, select.POLLIN | (select.POLLOUT if to_socket_buf else 0))
for fd, event in poll.poll():
if event & error_events:
keep_going = False
break
if fd == socket_fd:
if event & select.POLLOUT:
write_to_socket()
if event & select.POLLIN:
read_from_socket()
elif fd == signal_read_fd and event & select.POLLIN:
read_signals(signal_read_fd, handle_signal)
finally:
if child_pid: # supervisor process
with suppress(OSError):
conn.shutdown(socket.SHUT_RDWR)
with suppress(OSError):
conn.close()
raise SystemExit(0)
def main(stdin_fd: int, stdout_fd: int, notify_child_death_fd: int, unix_socket: socket.socket) -> None:
global parent_tty_name global parent_tty_name
with suppress(OSError): with suppress(OSError):
parent_tty_name = os.ttyname(sys.stdout.fileno()) parent_tty_name = os.ttyname(sys.stdout.fileno())
@ -640,12 +407,9 @@ def main(stdin_fd: int, stdout_fd: int, notify_child_death_fd: int, unix_socket:
os.set_blocking(stdin_fd, False) os.set_blocking(stdin_fd, False)
os.set_blocking(stdout_fd, False) os.set_blocking(stdout_fd, False)
signal_read_fd = install_signal_handlers(signal.SIGCHLD, signal.SIGUSR1)[0] signal_read_fd = install_signal_handlers(signal.SIGCHLD, signal.SIGUSR1)[0]
os.set_blocking(unix_socket.fileno(), False)
unix_socket.listen(5)
poll = select.poll() poll = select.poll()
poll.register(stdin_fd, select.POLLIN) poll.register(stdin_fd, select.POLLIN)
poll.register(signal_read_fd, select.POLLIN) poll.register(signal_read_fd, select.POLLIN)
poll.register(unix_socket.fileno(), select.POLLIN)
input_buf = output_buf = child_death_buf = b'' input_buf = output_buf = child_death_buf = b''
child_ready_fds: Dict[int, int] = {} child_ready_fds: Dict[int, int] = {}
child_pid_map: Dict[int, int] = {} child_pid_map: Dict[int, int] = {}
@ -666,7 +430,6 @@ def main(stdin_fd: int, stdout_fd: int, notify_child_death_fd: int, unix_socket:
for fd in get_all_non_child_fds(): for fd in get_all_non_child_fds():
if fd > -1: if fd > -1:
safe_close(fd) safe_close(fd)
unix_socket.close()
def check_event(event: int, err_msg: str) -> None: def check_event(event: int, err_msg: str) -> None:
if event & select.POLLHUP: if event & select.POLLHUP:
@ -766,15 +529,6 @@ def main(stdin_fd: int, stdout_fd: int, notify_child_death_fd: int, unix_socket:
read_signals(signal_read_fd, handle_signal) read_signals(signal_read_fd, handle_signal)
def handle_socket_client(event: int) -> None:
check_event(event, 'UNIX socket fd listener failed')
conn, addr = unix_socket.accept()
if not verify_socket_creds(conn):
print_error('Connection attempted with invalid credentials ignoring')
conn.close()
return
fork_socket_child_supervisor(conn, free_non_child_resources)
keep_type_checker_happy = True keep_type_checker_happy = True
try: try:
while is_zygote and keep_type_checker_happy: while is_zygote and keep_type_checker_happy:
@ -791,8 +545,6 @@ def main(stdin_fd: int, stdout_fd: int, notify_child_death_fd: int, unix_socket:
handle_signals(event) handle_signals(event)
elif q == notify_child_death_fd: elif q == notify_child_death_fd:
handle_notify_child_death(event) handle_notify_child_death(event)
elif q == unix_socket.fileno():
handle_socket_client(event)
except (KeyboardInterrupt, EOFError, BrokenPipeError): except (KeyboardInterrupt, EOFError, BrokenPipeError):
if is_zygote: if is_zygote:
raise SystemExit(1) raise SystemExit(1)
@ -809,33 +561,18 @@ def main(stdin_fd: int, stdout_fd: int, notify_child_death_fd: int, unix_socket:
safe_close(fmd) safe_close(fmd)
def get_socket_name(unix_socket: socket.socket) -> str: def exec_main(stdin_read: int, stdout_write: int, death_notify_write: int) -> None:
sname = unix_socket.getsockname()
if isinstance(sname, bytes):
sname = sname.decode('utf-8')
assert isinstance(sname, str)
if sname.startswith('\0'):
sname = '@' + sname[1:]
return sname
def exec_main(stdin_read: int, stdout_write: int, death_notify_write: int, unix_socket: Optional[socket.socket] = None) -> None:
os.setsid() os.setsid()
os.set_inheritable(stdin_read, False) os.set_inheritable(stdin_read, False)
os.set_inheritable(stdout_write, False) os.set_inheritable(stdout_write, False)
os.set_inheritable(death_notify_write, False) os.set_inheritable(death_notify_write, False)
running_in_kitty(False) running_in_kitty(False)
if unix_socket is None:
unix_socket = random_unix_socket()
os.write(stdout_write, f'{get_socket_name(unix_socket)}\n'.encode('utf-8'))
if not sys.stdout.line_buffering: # happens if the parent kitty instance has stdout not pointing to a terminal if not sys.stdout.line_buffering: # happens if the parent kitty instance has stdout not pointing to a terminal
sys.stdout.reconfigure(line_buffering=True) # type: ignore sys.stdout.reconfigure(line_buffering=True) # type: ignore
try: try:
main(stdin_read, stdout_write, death_notify_write, unix_socket) main(stdin_read, stdout_write, death_notify_write)
finally: finally:
set_options(None) set_options(None)
if is_zygote:
unix_socket.close()
def fork_prewarm_process(opts: Options, use_exec: bool = False) -> Optional[PrewarmProcess]: def fork_prewarm_process(opts: Options, use_exec: bool = False) -> Optional[PrewarmProcess]:
@ -850,21 +587,15 @@ def fork_prewarm_process(opts: Options, use_exec: bool = False) -> Optional[Prew
child_pid = tp.pid child_pid = tp.pid
tp.returncode = 0 # prevent a warning when the popen object is deleted with the process still running tp.returncode = 0 # prevent a warning when the popen object is deleted with the process still running
os.set_blocking(stdout_read, True) os.set_blocking(stdout_read, True)
with open(stdout_read, 'rb', closefd=False) as f:
socket_name = f.readline().decode('utf-8').rstrip()
os.set_blocking(stdout_read, False) os.set_blocking(stdout_read, False)
else: else:
unix_socket = random_unix_socket()
socket_name = get_socket_name(unix_socket)
child_pid = safer_fork() child_pid = safer_fork()
if child_pid: if child_pid:
# master # master
if not use_exec:
unix_socket.close()
safe_close(stdin_read) safe_close(stdin_read)
safe_close(stdout_write) safe_close(stdout_write)
safe_close(death_notify_write) safe_close(death_notify_write)
p = PrewarmProcess(child_pid, stdin_write, stdout_read, death_notify_read, socket_name) p = PrewarmProcess(child_pid, stdin_write, stdout_read, death_notify_read)
if use_exec: if use_exec:
p.reload_kitty_config() p.reload_kitty_config()
return p return p
@ -874,5 +605,5 @@ def fork_prewarm_process(opts: Options, use_exec: bool = False) -> Optional[Prew
safe_close(stdout_read) safe_close(stdout_read)
safe_close(death_notify_read) safe_close(death_notify_read)
set_options(opts) set_options(opts)
exec_main(stdin_read, stdout_write, death_notify_write, unix_socket) exec_main(stdin_read, stdout_write, death_notify_write)
raise SystemExit(0) raise SystemExit(0)

View File

@ -9,9 +9,8 @@ import signal
import subprocess import subprocess
import tempfile import tempfile
import time import time
from contextlib import suppress
from kitty.constants import kitty_exe, terminfo_dir from kitty.constants import kitty_exe
from kitty.fast_data_types import ( from kitty.fast_data_types import (
CLD_EXITED, CLD_KILLED, CLD_STOPPED, get_options, has_sigqueue, install_signal_handlers, CLD_EXITED, CLD_KILLED, CLD_STOPPED, get_options, has_sigqueue, install_signal_handlers,
read_signals, sigqueue read_signals, sigqueue
@ -24,139 +23,6 @@ class Prewarm(BaseTest):
maxDiff = None maxDiff = None
def test_socket_prewarming(self):
from kitty.prewarm import fork_prewarm_process, wait_for_child_death
exit_code = 17
src = '''\
def socket_child_main(exit_code=0, initial_print=''):
import os, sys, json, signal
from kitty.fast_data_types import get_options
from kitty.utils import read_screen_size
def report_screen_size_change(*a):
print("Screen size changed:", read_screen_size(fd=sys.stderr.fileno()).cols, file=sys.stderr, flush=True)
def report_tstp(*a):
print("SIGTSTP received", file=sys.stderr, flush=True)
raise SystemExit(19)
signal.signal(signal.SIGWINCH, report_screen_size_change)
signal.signal(signal.SIGTSTP, report_tstp)
if initial_print:
print(initial_print, flush=True, file=sys.stderr)
output = {
'test_env': os.environ.get('TEST_ENV_PASS', ''),
'cwd': os.path.realpath(os.getcwd()),
'font_family': get_options().font_family,
'cols': read_screen_size(fd=sys.stderr.fileno()).cols,
'stdin_data': sys.stdin.read(),
'done': 'hello',
}
print(json.dumps(output, indent=2), file=sys.stderr, flush=True)
print('testing stdout', end='')
raise SystemExit(exit_code)
''' + '\n\n'
cwd = os.path.realpath(tempfile.gettempdir())
opts = self.set_options()
opts.config_overrides = 'font_family prewarm',
p = fork_prewarm_process(opts, use_exec=True)
if p is None:
return
env = os.environ.copy()
env.update({
'TEST_ENV_PASS': 'xyz',
'KITTY_PREWARM_SOCKET': p.socket_env_var(),
'KITTY_PREWARM_SOCKET_REAL_TTY': ' ' * 32,
'TERM': 'xterm-kitty',
'TERMINFO': terminfo_dir
})
cols = 117
def wait_for_death(exit_code, timeout=5):
status = wait_for_child_death(pty.child_pid, timeout=timeout)
if status is None:
os.kill(pty.child_pid, signal.SIGKILL)
if status is None:
pty.process_input_from_child(0)
self.assertIsNotNone(status, f'prewarm wrapper process did not exit. Screen contents: {pty.screen_contents()}')
if isinstance(exit_code, signal.Signals):
self.assertTrue(os.WIFSIGNALED(status), 'prewarm wrapper did not die with a signal')
self.assertEqual(os.WTERMSIG(status), exit_code.value)
else:
with suppress(AttributeError):
self.assertEqual(os.waitstatus_to_exitcode(status), exit_code, pty.screen_contents())
if not self.is_ci: # signal delivery tests are pretty flakey on CI so give up on them
with self.subTest(msg='test SIGINT via signal to wrapper'):
pty = self.create_pty(
argv=[kitty_exe(), '+runpy', src + 'socket_child_main(initial_print="child ready:")'], cols=cols, env=env, cwd=cwd)
pty.wait_till(lambda: 'child ready:' in pty.screen_contents())
os.kill(pty.child_pid, signal.SIGINT)
pty.wait_till(lambda: 'KeyboardInterrupt' in pty.screen_contents())
wait_for_death(signal.SIGINT)
with self.subTest(msg='test SIGINT via Ctrl-c'):
pty = self.create_pty(
argv=[kitty_exe(), '+runpy', src + 'socket_child_main(initial_print="child ready:")'], cols=cols, env=env, cwd=cwd)
pty.wait_till(lambda: 'child ready:' in pty.screen_contents())
pty.write_to_child('\x03', flush=True)
pty.wait_till(lambda: 'KeyboardInterrupt' in pty.screen_contents())
wait_for_death(signal.SIGINT)
with self.subTest(msg='test SIGTSTP via Ctrl-z'):
pty = self.create_pty(
argv=[kitty_exe(), '+runpy', src + 'socket_child_main(initial_print="child ready:")'], cols=cols, env=env, cwd=cwd)
pty.wait_till(lambda: 'child ready:' in pty.screen_contents())
pty.write_to_child('\x1a', flush=True)
pty.wait_till(lambda: 'SIGTSTP received' in pty.screen_contents())
wait_for_death(19)
with self.subTest(msg='test SIGWINCH handling'):
pty = self.create_pty(
argv=[kitty_exe(), '+runpy', src + 'socket_child_main(initial_print="child ready:")'], cols=cols, env=env, cwd=cwd)
pty.wait_till(lambda: 'child ready:' in pty.screen_contents())
pty.set_window_size(columns=cols + 3)
pty.wait_till(lambda: f'Screen size changed: {cols + 3}' in pty.screen_contents())
os.close(pty.master_fd)
with self.subTest(msg='test env rewrite'):
pty = self.create_pty(
argv=[kitty_exe(), '+runpy', src + 'socket_child_main(initial_print="child ready:")'], cols=cols, env=env, cwd=cwd)
pty.wait_till(lambda: 'child ready:' in pty.screen_contents())
from kitty.child import environ_of_process
self.assertIn('/', environ_of_process(pty.child_pid).get('KITTY_PREWARM_SOCKET_REAL_TTY', ''))
os.close(pty.master_fd)
with self.subTest(msg='test passing of data via cwd, env vars and stdin/stdout redirection'):
stdin_r, stdin_w = os.pipe()
os.set_inheritable(stdin_w, False)
stdout_r, stdout_w = os.pipe()
os.set_inheritable(stdout_r, False)
pty = self.create_pty(
argv=[kitty_exe(), '+runpy', src + f'socket_child_main({exit_code})'], cols=cols, env=env, cwd=cwd,
stdin_fd=stdin_r, stdout_fd=stdout_w)
stdin_data = 'testing--stdin-read'
with open(stdin_w, 'w') as f:
f.write(stdin_data)
def has_json():
s = pty.screen_contents().strip()
return 'hello' in s and s.endswith('}')
pty.wait_till(has_json)
wait_for_death(exit_code)
output = json.loads(pty.screen_contents().strip())
self.assertEqual(output['test_env'], env['TEST_ENV_PASS'])
self.assertEqual(output['cwd'], cwd)
self.assertEqual(output['font_family'], 'prewarm')
self.assertEqual(output['cols'], cols)
self.assertEqual(output['stdin_data'], stdin_data)
with open(stdout_r) as f:
stdout_data = f.read()
self.assertEqual(stdout_data, 'testing stdout')
def test_prewarming(self): def test_prewarming(self):
from kitty.prewarm import fork_prewarm_process from kitty.prewarm import fork_prewarm_process

View File

@ -920,7 +920,7 @@ def build_launcher(args: Options, launcher_dir: str = '.', bundle_type: str = 's
os.makedirs(launcher_dir, exist_ok=True) os.makedirs(launcher_dir, exist_ok=True)
os.makedirs(build_dir, exist_ok=True) os.makedirs(build_dir, exist_ok=True)
objects = [] objects = []
for src in ('kitty/launcher/main.c', 'kitty/launcher/prewarm.c'): for src in ('kitty/launcher/main.c',):
obj = os.path.join(build_dir, src.replace('/', '-').replace('.c', '.o')) obj = os.path.join(build_dir, src.replace('/', '-').replace('.c', '.o'))
objects.append(obj) objects.append(obj)
cmd = env.cc + cppflags + cflags + ['-c', src, '-o', obj] cmd = env.cc + cppflags + cflags + ['-c', src, '-o', obj]

View File

@ -3,7 +3,6 @@
import importlib import importlib
import os import os
import sys
import warnings import warnings
from contextlib import contextmanager from contextlib import contextmanager
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
@ -25,9 +24,6 @@ def env_vars(**kw: str) -> Iterator[None]:
def main() -> None: def main() -> None:
if 'prewarmed' in getattr(sys, 'kitty_run_data'):
os.environ.pop('KITTY_PREWARM_SOCKET')
os.execlp(sys.executable, sys.executable, '+launch', __file__, *sys.argv[1:])
warnings.simplefilter('error') warnings.simplefilter('error')
current_home = os.path.expanduser('~') + os.sep current_home = os.path.expanduser('~') + os.sep
paths = os.environ.get('PATH', '/usr/local/sbin:/usr/local/bin:/usr/bin').split(os.pathsep) paths = os.environ.get('PATH', '/usr/local/sbin:/usr/local/bin:/usr/bin').split(os.pathsep)