Fix the problem caused by wrong cursor coordinates. No more messing with the main cursor, instead the cursor is saved when receiving a pre-edit text update and used for drawing later. Update the overlay to the last visible cursor position before rendering to ensure it always moves with the cursor. Finally, draw the overlay after line rendering is complete, and restore the line buffer after updating the rendered data to ensure that the line text being read is correct at all times. This also improves performance by only rendering once when changes are made, eliminating the need to repeatedly disable and draw after various commands and not even comprehensively.
1936 lines
72 KiB
C
1936 lines
72 KiB
C
/*
|
|
* child-monitor.c
|
|
* Copyright (C) 2017 Kovid Goyal <kovid at kovidgoyal.net>
|
|
*
|
|
* Distributed under terms of the GPL3 license.
|
|
*/
|
|
|
|
#include "loop-utils.h"
|
|
#include "safe-wrappers.h"
|
|
#include "state.h"
|
|
#include "threading.h"
|
|
#include "screen.h"
|
|
#include "fonts.h"
|
|
#include "charsets.h"
|
|
#include "monotonic.h"
|
|
#include <termios.h>
|
|
#include <unistd.h>
|
|
#include <float.h>
|
|
#include <fcntl.h>
|
|
#include <sys/ioctl.h>
|
|
#include <sys/wait.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <signal.h>
|
|
extern PyTypeObject Screen_Type;
|
|
|
|
#if defined(__APPLE__) || defined(__OpenBSD__)
|
|
#define NO_SIGQUEUE 1
|
|
#endif
|
|
|
|
#ifdef DEBUG_EVENT_LOOP
|
|
#define EVDBG(...) log_event(__VA_ARGS__)
|
|
#else
|
|
#define EVDBG(...)
|
|
#endif
|
|
|
|
#define EXTRA_FDS 3
|
|
#ifndef MSG_NOSIGNAL
|
|
// Apple does not implement MSG_NOSIGNAL
|
|
#define MSG_NOSIGNAL 0
|
|
#endif
|
|
#define USE_RENDER_FRAMES (global_state.has_render_frames && OPT(sync_to_monitor))
|
|
|
|
static void (*parse_func)(Screen*, PyObject*, monotonic_t);
|
|
|
|
typedef struct {
|
|
char *data;
|
|
size_t sz;
|
|
id_type peer_id;
|
|
} Message;
|
|
|
|
typedef struct {
|
|
PyObject_HEAD
|
|
|
|
PyObject *dump_callback, *update_screen, *death_notify;
|
|
unsigned int count;
|
|
bool shutting_down;
|
|
pthread_t io_thread, talk_thread;
|
|
|
|
int talk_fd, listen_fd, prewarm_fd;
|
|
Message *messages;
|
|
size_t messages_capacity, messages_count;
|
|
LoopData io_loop_data;
|
|
} ChildMonitor;
|
|
|
|
|
|
typedef struct {
|
|
Screen *screen;
|
|
bool needs_removal;
|
|
int fd;
|
|
unsigned long id;
|
|
pid_t pid;
|
|
} Child;
|
|
|
|
static const Child EMPTY_CHILD = {0};
|
|
#define screen_mutex(op, which) \
|
|
pthread_mutex_##op(&screen->which##_buf_lock);
|
|
#define children_mutex(op) \
|
|
pthread_mutex_##op(&children_lock);
|
|
#define talk_mutex(op) \
|
|
pthread_mutex_##op(&talk_lock);
|
|
|
|
|
|
static Child children[MAX_CHILDREN] = {{0}};
|
|
static Child scratch[MAX_CHILDREN] = {{0}};
|
|
static Child add_queue[MAX_CHILDREN] = {{0}}, remove_queue[MAX_CHILDREN] = {{0}}, remove_notify[MAX_CHILDREN] = {{0}};
|
|
static size_t add_queue_count = 0, remove_queue_count = 0;
|
|
static struct pollfd children_fds[MAX_CHILDREN + EXTRA_FDS] = {{0}};
|
|
static pthread_mutex_t children_lock, talk_lock;
|
|
static bool kill_signal_received = false, reload_config_signal_received = false;
|
|
static ChildMonitor *the_monitor = NULL;
|
|
|
|
typedef struct {
|
|
pid_t pid;
|
|
int status;
|
|
} ReapedPID;
|
|
|
|
static pid_t monitored_pids[256] = {0};
|
|
static size_t monitored_pids_count = 0;
|
|
static ReapedPID reaped_pids[arraysz(monitored_pids)] = {{0}};
|
|
static size_t reaped_pids_count = 0;
|
|
|
|
|
|
|
|
// Main thread functions {{{
|
|
|
|
#define FREE_CHILD(x) \
|
|
Py_CLEAR((x).screen); x = EMPTY_CHILD;
|
|
|
|
#define XREF_CHILD(x, OP) OP(x.screen);
|
|
#define INCREF_CHILD(x) XREF_CHILD(x, Py_INCREF)
|
|
#define DECREF_CHILD(x) XREF_CHILD(x, Py_DECREF)
|
|
|
|
// The max time to wait for events from the window system
|
|
// before ticking over the main loop. Negative values mean wait forever.
|
|
static monotonic_t maximum_wait = -1;
|
|
|
|
static void
|
|
set_maximum_wait(monotonic_t val) {
|
|
if (val >= 0 && (val < maximum_wait || maximum_wait < 0)) maximum_wait = val;
|
|
}
|
|
|
|
#define KITTY_HANDLED_SIGNALS SIGINT, SIGHUP, SIGTERM, SIGCHLD, SIGUSR1, SIGUSR2, 0
|
|
|
|
static void
|
|
mask_variadic_signals(int sentinel, ...) {
|
|
sigset_t signals;
|
|
sigemptyset(&signals);
|
|
va_list valist;
|
|
va_start(valist, sentinel);
|
|
while (true) {
|
|
int sig = va_arg(valist, int);
|
|
if (sig == sentinel) break;
|
|
sigaddset(&signals, sig);
|
|
}
|
|
va_end(valist);
|
|
#ifdef HAS_SIGNAL_FD
|
|
sigprocmask(SIG_BLOCK, &signals, NULL);
|
|
#else
|
|
struct sigaction act = {.sa_handler=SIG_IGN, .sa_flags=SA_RESTART, .sa_mask = signals};
|
|
va_start(valist, sentinel);
|
|
while (true) {
|
|
int sig = va_arg(valist, int);
|
|
if (sig == sentinel) break;
|
|
sigaction(sig, &act, NULL);
|
|
}
|
|
va_end(valist);
|
|
#endif
|
|
}
|
|
|
|
static PyObject*
|
|
mask_kitty_signals_process_wide(PyObject *self UNUSED, PyObject *a UNUSED) {
|
|
mask_variadic_signals(0, KITTY_HANDLED_SIGNALS);
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
static PyObject *
|
|
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
|
|
ChildMonitor *self;
|
|
PyObject *dump_callback, *death_notify;
|
|
int talk_fd = -1, listen_fd = -1, prewarm_fd = -1;
|
|
int ret;
|
|
|
|
if (the_monitor) { PyErr_SetString(PyExc_RuntimeError, "Can have only a single ChildMonitor instance"); return NULL; }
|
|
if (!PyArg_ParseTuple(args, "OO|iii", &death_notify, &dump_callback, &talk_fd, &listen_fd, &prewarm_fd)) return NULL;
|
|
if ((ret = pthread_mutex_init(&children_lock, NULL)) != 0) {
|
|
PyErr_Format(PyExc_RuntimeError, "Failed to create children_lock mutex: %s", strerror(ret));
|
|
return NULL;
|
|
}
|
|
if ((ret = pthread_mutex_init(&talk_lock, NULL)) != 0) {
|
|
PyErr_Format(PyExc_RuntimeError, "Failed to create talk_lock mutex: %s", strerror(ret));
|
|
return NULL;
|
|
}
|
|
self = (ChildMonitor *)type->tp_alloc(type, 0);
|
|
if (!init_loop_data(&self->io_loop_data, KITTY_HANDLED_SIGNALS)) return PyErr_SetFromErrno(PyExc_OSError);
|
|
self->talk_fd = talk_fd;
|
|
self->listen_fd = listen_fd;
|
|
self->prewarm_fd = prewarm_fd;
|
|
if (self == NULL) return PyErr_NoMemory();
|
|
self->death_notify = death_notify; Py_INCREF(death_notify);
|
|
if (dump_callback != Py_None) {
|
|
self->dump_callback = dump_callback; Py_INCREF(dump_callback);
|
|
parse_func = parse_worker_dump;
|
|
} else parse_func = parse_worker;
|
|
self->count = 0;
|
|
children_fds[0].fd = self->io_loop_data.wakeup_read_fd; children_fds[1].fd = self->io_loop_data.signal_read_fd;
|
|
children_fds[2].fd = self->prewarm_fd;
|
|
children_fds[0].events = POLLIN; children_fds[1].events = POLLIN; children_fds[2].events = POLLIN;
|
|
the_monitor = self;
|
|
|
|
return (PyObject*) self;
|
|
}
|
|
|
|
static void
|
|
dealloc(ChildMonitor* self) {
|
|
if (self->messages) {
|
|
for (size_t i = 0; i < self->messages_count; i++) free(self->messages[i].data);
|
|
free(self->messages); self->messages = NULL;
|
|
self->messages_count = 0; self->messages_capacity = 0;
|
|
}
|
|
pthread_mutex_destroy(&children_lock);
|
|
pthread_mutex_destroy(&talk_lock);
|
|
Py_CLEAR(self->dump_callback);
|
|
Py_CLEAR(self->death_notify);
|
|
while (remove_queue_count) {
|
|
remove_queue_count--;
|
|
FREE_CHILD(remove_queue[remove_queue_count]);
|
|
}
|
|
while (add_queue_count) {
|
|
add_queue_count--;
|
|
FREE_CHILD(add_queue[add_queue_count]);
|
|
}
|
|
free_loop_data(&self->io_loop_data);
|
|
safe_close(self->prewarm_fd, __FILE__, __LINE__); self->prewarm_fd = -1;
|
|
Py_TYPE(self)->tp_free((PyObject*)self);
|
|
}
|
|
|
|
static PyObject*
|
|
handled_signals(ChildMonitor *self, PyObject *args UNUSED) {
|
|
PyObject *ans = PyTuple_New(self->io_loop_data.num_handled_signals);
|
|
if (ans) {
|
|
for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(ans); i++) {
|
|
PyTuple_SET_ITEM(ans, i, PyLong_FromLong((long)self->io_loop_data.handled_signals[i]));
|
|
}
|
|
}
|
|
return ans;
|
|
}
|
|
|
|
static void
|
|
wakeup_io_loop(ChildMonitor *self, bool in_signal_handler) {
|
|
wakeup_loop(&self->io_loop_data, in_signal_handler, "io_loop");
|
|
}
|
|
|
|
static void* io_loop(void *data);
|
|
static void* talk_loop(void *data);
|
|
static void send_response_to_peer(id_type peer_id, const char *msg, size_t msg_sz);
|
|
static void wakeup_talk_loop(bool);
|
|
static bool talk_thread_started = false;
|
|
|
|
static PyObject *
|
|
start(PyObject *s, PyObject *a UNUSED) {
|
|
#define start_doc "start() -> Start the I/O thread"
|
|
ChildMonitor *self = (ChildMonitor*)s;
|
|
int ret;
|
|
if (self->talk_fd > -1 || self->listen_fd > -1) {
|
|
if ((ret = pthread_create(&self->talk_thread, NULL, talk_loop, self)) != 0) {
|
|
return PyErr_Format(PyExc_OSError, "Failed to start talk thread with error: %s", strerror(ret));
|
|
}
|
|
talk_thread_started = true;
|
|
}
|
|
ret = pthread_create(&self->io_thread, NULL, io_loop, self);
|
|
if (ret != 0) return PyErr_Format(PyExc_OSError, "Failed to start I/O thread with error: %s", strerror(ret));
|
|
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
static PyObject *
|
|
wakeup(ChildMonitor *self, PyObject *args UNUSED) {
|
|
#define wakeup_doc "wakeup() -> wakeup the ChildMonitor I/O thread, forcing it to exit from poll() if it is waiting there."
|
|
wakeup_io_loop(self, false);
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
static PyObject *
|
|
add_child(ChildMonitor *self, PyObject *args) {
|
|
#define add_child_doc "add_child(id, pid, fd, screen) -> Add a child."
|
|
children_mutex(lock);
|
|
if (self->count + add_queue_count >= MAX_CHILDREN) { PyErr_SetString(PyExc_ValueError, "Too many children"); children_mutex(unlock); return NULL; }
|
|
add_queue[add_queue_count] = EMPTY_CHILD;
|
|
#define A(attr) &add_queue[add_queue_count].attr
|
|
if (!PyArg_ParseTuple(args, "kiiO", A(id), A(pid), A(fd), A(screen))) {
|
|
children_mutex(unlock);
|
|
return NULL;
|
|
}
|
|
#undef A
|
|
INCREF_CHILD(add_queue[add_queue_count]);
|
|
add_queue_count++;
|
|
children_mutex(unlock);
|
|
wakeup_io_loop(self, false);
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
#define schedule_write_to_child_generic(id, num, va_start, get_next_arg, va_end) \
|
|
ChildMonitor *self = the_monitor; \
|
|
bool found = false; \
|
|
const char *data; \
|
|
size_t szval, sz = 0; \
|
|
va_start(ap, num); \
|
|
for (unsigned int i = 0; i < num; i++) { \
|
|
get_next_arg(ap); \
|
|
sz += szval; \
|
|
} \
|
|
va_end(ap); \
|
|
children_mutex(lock); \
|
|
for (size_t i = 0; i < self->count; i++) { \
|
|
if (children[i].id == id) { \
|
|
Screen *screen = children[i].screen; \
|
|
screen_mutex(lock, write); \
|
|
size_t space_left = screen->write_buf_sz - screen->write_buf_used; \
|
|
if (space_left < sz) { \
|
|
if (screen->write_buf_used + sz > 100 * 1024 * 1024) { \
|
|
log_error("Too much data being sent to child with id: %lu, ignoring it", id); \
|
|
screen_mutex(unlock, write); \
|
|
break; \
|
|
} \
|
|
screen->write_buf_sz = screen->write_buf_used + sz; \
|
|
screen->write_buf = PyMem_RawRealloc(screen->write_buf, screen->write_buf_sz); \
|
|
if (screen->write_buf == NULL) { fatal("Out of memory."); } \
|
|
} \
|
|
found = true; \
|
|
va_start(ap, num); \
|
|
for (unsigned int i = 0; i < num; i++) { \
|
|
get_next_arg(ap); \
|
|
memcpy(screen->write_buf + screen->write_buf_used, data, szval); \
|
|
screen->write_buf_used += szval; \
|
|
} \
|
|
va_end(ap); \
|
|
if (screen->write_buf_sz > BUFSIZ && screen->write_buf_used < BUFSIZ) { \
|
|
screen->write_buf_sz = BUFSIZ; \
|
|
screen->write_buf = PyMem_RawRealloc(screen->write_buf, screen->write_buf_sz); \
|
|
if (screen->write_buf == NULL) { fatal("Out of memory."); } \
|
|
} \
|
|
if (screen->write_buf_used) wakeup_io_loop(self, false); \
|
|
screen_mutex(unlock, write); \
|
|
break; \
|
|
} \
|
|
} \
|
|
children_mutex(unlock); \
|
|
return found;
|
|
|
|
bool
|
|
schedule_write_to_child(unsigned long id, unsigned int num, ...) {
|
|
va_list ap;
|
|
#define get_next_arg(ap) data = va_arg(ap, const char*); szval = va_arg(ap, size_t);
|
|
schedule_write_to_child_generic(id, num, va_start, get_next_arg, va_end);
|
|
#undef get_next_arg
|
|
}
|
|
|
|
bool
|
|
schedule_write_to_child_python(unsigned long id, const char *prefix, PyObject *ap, const char *suffix) {
|
|
if (!PyTuple_Check(ap)) return false;
|
|
bool has_prefix = prefix && prefix[0], has_suffix = suffix && suffix[0];
|
|
const size_t extra = (has_prefix ? 1 : 0) + (has_suffix ? 1 : 0);
|
|
size_t num = PyTuple_GET_SIZE(ap) + extra;
|
|
Py_ssize_t pidx;
|
|
#define py_start(ap, num) pidx = 0;
|
|
#define py_end(ap) pidx = 0;
|
|
#define get_next_arg(ap) { \
|
|
size_t pidxf = pidx++; \
|
|
if (pidxf == 0 && has_prefix) { data = prefix; szval = strlen(prefix); } \
|
|
else { \
|
|
if (has_prefix) pidxf--; \
|
|
if (has_suffix && pidxf >= (size_t)PyTuple_GET_SIZE(ap)) { data = suffix; szval = strlen(suffix); } \
|
|
else { \
|
|
PyObject *t = PyTuple_GET_ITEM(ap, pidxf); \
|
|
if (PyBytes_Check(t)) { data = PyBytes_AS_STRING(t); szval = PyBytes_GET_SIZE(t); } \
|
|
else { \
|
|
Py_ssize_t usz; \
|
|
data = PyUnicode_AsUTF8AndSize(t, &usz); szval = usz; \
|
|
if (!data) fatal("Failed to convert object to bytes in schedule_write_to_child_python"); \
|
|
} \
|
|
} \
|
|
} \
|
|
}
|
|
schedule_write_to_child_generic(id, num, py_start, get_next_arg, py_end);
|
|
#undef py_start
|
|
#undef py_end
|
|
#undef get_next_arg
|
|
}
|
|
|
|
static PyObject *
|
|
needs_write(ChildMonitor UNUSED *self, PyObject *args) {
|
|
#define needs_write_doc "needs_write(id, data) -> Queue data to be written to child."
|
|
unsigned long id;
|
|
Py_buffer buf;
|
|
if (!PyArg_ParseTuple(args, "ky*", &id, &buf)) return NULL;
|
|
if (schedule_write_to_child(id, 1, buf.buf, (size_t)buf.len)) { Py_RETURN_TRUE; }
|
|
Py_RETURN_FALSE;
|
|
}
|
|
|
|
static PyObject *
|
|
shutdown_monitor(ChildMonitor *self, PyObject *a UNUSED) {
|
|
#define shutdown_monitor_doc "shutdown_monitor() -> Shutdown the monitor loop."
|
|
self->shutting_down = true;
|
|
wakeup_talk_loop(false);
|
|
wakeup_io_loop(self, false);
|
|
int ret = pthread_join(self->io_thread, NULL);
|
|
if (ret != 0) return PyErr_Format(PyExc_OSError, "Failed to join() I/O thread with error: %s", strerror(ret));
|
|
if (talk_thread_started) {
|
|
ret = pthread_join(self->talk_thread, NULL);
|
|
if (ret != 0) return PyErr_Format(PyExc_OSError, "Failed to join() talk thread with error: %s", strerror(ret));
|
|
}
|
|
talk_thread_started = false;
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
static bool
|
|
do_parse(ChildMonitor *self, Screen *screen, monotonic_t now, bool flush) {
|
|
bool input_read = false;
|
|
screen_mutex(lock, read);
|
|
if (screen->read_buf_sz || screen->pending_mode.used) {
|
|
monotonic_t time_since_new_input = now - screen->new_input_at;
|
|
if (flush || time_since_new_input >= OPT(input_delay)) {
|
|
bool read_buf_full = screen->read_buf_sz >= READ_BUF_SZ;
|
|
input_read = true;
|
|
parse_func(screen, self->dump_callback, now);
|
|
if (read_buf_full) wakeup_io_loop(self, false); // Ensure the read fd has POLLIN set
|
|
screen->new_input_at = 0;
|
|
if (screen->pending_mode.activated_at) {
|
|
monotonic_t time_since_pending = MAX(0, now - screen->pending_mode.activated_at);
|
|
set_maximum_wait(screen->pending_mode.wait_time - time_since_pending);
|
|
}
|
|
} else set_maximum_wait(OPT(input_delay) - time_since_new_input);
|
|
}
|
|
screen_mutex(unlock, read);
|
|
return input_read;
|
|
}
|
|
|
|
static bool
|
|
parse_input(ChildMonitor *self) {
|
|
// Parse all available input that was read in the I/O thread.
|
|
size_t count = 0, remove_count = 0;
|
|
bool input_read = false, reload_config_called = false;
|
|
monotonic_t now = monotonic();
|
|
children_mutex(lock);
|
|
while (remove_queue_count) {
|
|
remove_queue_count--;
|
|
remove_notify[remove_count] = remove_queue[remove_queue_count];
|
|
INCREF_CHILD(remove_notify[remove_count]);
|
|
remove_count++;
|
|
FREE_CHILD(remove_queue[remove_queue_count]);
|
|
}
|
|
|
|
if (UNLIKELY(kill_signal_received || reload_config_signal_received)) {
|
|
if (kill_signal_received) {
|
|
global_state.quit_request = IMPERATIVE_CLOSE_REQUESTED;
|
|
global_state.has_pending_closes = true;
|
|
request_tick_callback();
|
|
kill_signal_received = false;
|
|
}
|
|
else if (reload_config_signal_received) {
|
|
reload_config_signal_received = false;
|
|
reload_config_called = true;
|
|
}
|
|
} else {
|
|
count = self->count;
|
|
for (size_t i = 0; i < count; i++) {
|
|
scratch[i] = children[i];
|
|
INCREF_CHILD(scratch[i]);
|
|
}
|
|
}
|
|
children_mutex(unlock);
|
|
|
|
Message *msgs = NULL;
|
|
size_t msgs_count = 0;
|
|
talk_mutex(lock);
|
|
if (UNLIKELY(self->messages_count)) {
|
|
msgs = malloc(sizeof(Message) * self->messages_count);
|
|
if (msgs) {
|
|
memcpy(msgs, self->messages, sizeof(Message) * self->messages_count);
|
|
msgs_count = self->messages_count;
|
|
}
|
|
memset(self->messages, 0, sizeof(Message) * self->messages_capacity);
|
|
self->messages_count = 0;
|
|
}
|
|
talk_mutex(unlock);
|
|
|
|
if (msgs_count) {
|
|
for (size_t i = 0; i < msgs_count; i++) {
|
|
Message *msg = msgs + i;
|
|
PyObject *resp = NULL;
|
|
if (msg->data) {
|
|
resp = PyObject_CallMethod(global_state.boss, "peer_message_received", "y#K", msg->data, (int)msg->sz, msg->peer_id);
|
|
free(msg->data);
|
|
if (!resp) PyErr_Print();
|
|
}
|
|
if (resp) {
|
|
if (PyBytes_Check(resp)) send_response_to_peer(msg->peer_id, PyBytes_AS_STRING(resp), PyBytes_GET_SIZE(resp));
|
|
else if (resp == Py_None) send_response_to_peer(msg->peer_id, NULL, 0);
|
|
Py_CLEAR(resp);
|
|
} else send_response_to_peer(msg->peer_id, NULL, 0);
|
|
}
|
|
free(msgs); msgs = NULL;
|
|
}
|
|
|
|
while(remove_count) {
|
|
// must be done while no locks are held, since the locks are non-recursive and
|
|
// the python function could call into other functions in this module
|
|
remove_count--;
|
|
if (remove_notify[remove_count].screen) do_parse(self, remove_notify[remove_count].screen, now, true);
|
|
PyObject *t = PyObject_CallFunction(self->death_notify, "k", remove_notify[remove_count].id);
|
|
if (t == NULL) PyErr_Print();
|
|
else Py_DECREF(t);
|
|
FREE_CHILD(remove_notify[remove_count]);
|
|
}
|
|
|
|
for (size_t i = 0; i < count; i++) {
|
|
if (!scratch[i].needs_removal) {
|
|
if (do_parse(self, scratch[i].screen, now, false)) input_read = true;
|
|
}
|
|
DECREF_CHILD(scratch[i]);
|
|
}
|
|
if (reload_config_called) {
|
|
call_boss(load_config_file, "");
|
|
}
|
|
return input_read;
|
|
}
|
|
|
|
static bool
|
|
mark_child_for_close(ChildMonitor *self, id_type window_id) {
|
|
bool found = false;
|
|
children_mutex(lock);
|
|
for (size_t i = 0; i < self->count; i++) {
|
|
if (children[i].id == window_id) {
|
|
children[i].needs_removal = true;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
for (size_t i = 0; i < add_queue_count; i++) {
|
|
if (add_queue[i].id == window_id) {
|
|
add_queue[i].needs_removal = true;
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
}
|
|
children_mutex(unlock);
|
|
wakeup_io_loop(self, false);
|
|
return found;
|
|
}
|
|
|
|
|
|
static PyObject *
|
|
mark_for_close(ChildMonitor *self, PyObject *args) {
|
|
#define mark_for_close_doc "Mark a child to be removed from the child monitor"
|
|
id_type window_id;
|
|
if (!PyArg_ParseTuple(args, "K", &window_id)) return NULL;
|
|
if (mark_child_for_close(self, window_id)) { Py_RETURN_TRUE; }
|
|
Py_RETURN_FALSE;
|
|
}
|
|
|
|
static bool
|
|
pty_resize(int fd, struct winsize *dim) {
|
|
while(true) {
|
|
if (ioctl(fd, TIOCSWINSZ, dim) == -1) {
|
|
if (errno == EINTR) continue;
|
|
if (errno != EBADF && errno != ENOTTY) {
|
|
log_error("Failed to resize tty associated with fd: %d with error: %s", fd, strerror(errno));
|
|
return false;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static PyObject *
|
|
resize_pty(ChildMonitor *self, PyObject *args) {
|
|
#define resize_pty_doc "Resize the pty associated with the specified child"
|
|
unsigned long window_id;
|
|
struct winsize dim;
|
|
int fd = -1;
|
|
if (!PyArg_ParseTuple(args, "kHHHH", &window_id, &dim.ws_row, &dim.ws_col, &dim.ws_xpixel, &dim.ws_ypixel)) return NULL;
|
|
children_mutex(lock);
|
|
#define FIND(queue, count) { \
|
|
for (size_t i = 0; i < count; i++) { \
|
|
if (queue[i].id == window_id) { \
|
|
fd = queue[i].fd; \
|
|
break; \
|
|
} \
|
|
}}
|
|
FIND(children, self->count);
|
|
if (fd == -1) FIND(add_queue, add_queue_count);
|
|
if (fd != -1) {
|
|
if (!pty_resize(fd, &dim)) PyErr_SetFromErrno(PyExc_OSError);
|
|
} else log_error("Failed to send resize signal to child with id: %lu (children count: %u) (add queue: %zu)", window_id, self->count, add_queue_count);
|
|
children_mutex(unlock);
|
|
if (PyErr_Occurred()) return NULL;
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
bool
|
|
set_iutf8(int UNUSED fd, bool UNUSED 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 PyObject*
|
|
pyset_iutf8(ChildMonitor *self, PyObject *args) {
|
|
id_type window_id;
|
|
int on;
|
|
PyObject *found = Py_False;
|
|
if (!PyArg_ParseTuple(args, "Kp", &window_id, &on)) return NULL;
|
|
children_mutex(lock);
|
|
for (size_t i = 0; i < self->count; i++) {
|
|
if (children[i].id == window_id) {
|
|
found = Py_True;
|
|
if (!set_iutf8(children_fds[EXTRA_FDS + i].fd, on & 1)) PyErr_SetFromErrno(PyExc_OSError);
|
|
break;
|
|
}
|
|
}
|
|
children_mutex(unlock);
|
|
if (PyErr_Occurred()) return NULL;
|
|
Py_INCREF(found);
|
|
return found;
|
|
}
|
|
|
|
#undef FREE_CHILD
|
|
#undef INCREF_CHILD
|
|
#undef DECREF_CHILD
|
|
|
|
static bool
|
|
cursor_needs_render(Window *w) {
|
|
return w->cursor_visible_at_last_render != w->render_data.screen->cursor_render_info.is_visible || w->last_cursor_x != w->render_data.screen->cursor_render_info.x || w->last_cursor_y != w->render_data.screen->cursor_render_info.y || w->last_cursor_shape != w->render_data.screen->cursor_render_info.shape;
|
|
}
|
|
|
|
static bool
|
|
collect_cursor_info(CursorRenderInfo *ans, Window *w, monotonic_t now, OSWindow *os_window) {
|
|
ScreenRenderData *rd = &w->render_data;
|
|
Cursor *cursor;
|
|
if (screen_is_overlay_active(rd->screen)) {
|
|
// Do not force the cursor to be visible here for the sake of some programs that prefer it hidden
|
|
cursor = &(rd->screen->overlay_line.original_line.cursor);
|
|
ans->x = rd->screen->overlay_line.cursor_x;
|
|
ans->y = rd->screen->overlay_line.ynum;
|
|
} else {
|
|
cursor = rd->screen->cursor;
|
|
ans->x = cursor->x; ans->y = cursor->y;
|
|
}
|
|
ans->is_visible = false;
|
|
if (rd->screen->scrolled_by || !screen_is_cursor_visible(rd->screen)) return cursor_needs_render(w);
|
|
monotonic_t time_since_start_blink = now - os_window->cursor_blink_zero_time;
|
|
bool cursor_blinking = OPT(cursor_blink_interval) > 0 && !cursor->non_blinking && os_window->is_focused && (OPT(cursor_stop_blinking_after) == 0 || time_since_start_blink <= OPT(cursor_stop_blinking_after));
|
|
bool do_draw_cursor = true;
|
|
if (cursor_blinking) {
|
|
int t = monotonic_t_to_ms(time_since_start_blink);
|
|
int d = monotonic_t_to_ms(OPT(cursor_blink_interval));
|
|
int n = t / d;
|
|
do_draw_cursor = n % 2 == 0 ? true : false;
|
|
monotonic_t bucket = ms_to_monotonic_t((monotonic_t)(n + 1) * d);
|
|
monotonic_t delay = bucket - time_since_start_blink;
|
|
set_maximum_wait(delay);
|
|
}
|
|
if (!do_draw_cursor) { ans->is_visible = false; return cursor_needs_render(w); }
|
|
ans->is_visible = true;
|
|
ans->shape = cursor->shape ? cursor->shape : OPT(cursor_shape);
|
|
ans->is_focused = os_window->is_focused;
|
|
return cursor_needs_render(w);
|
|
}
|
|
|
|
static void
|
|
change_menubar_title(PyObject *title UNUSED) {
|
|
#ifdef __APPLE__
|
|
static PyObject *current_title = NULL;
|
|
if (title != current_title) {
|
|
current_title = title;
|
|
if (title && OPT(macos_show_window_title_in) & MENUBAR) update_menu_bar_title(title);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static bool
|
|
prepare_to_render_os_window(OSWindow *os_window, monotonic_t now, unsigned int *active_window_id, color_type *active_window_bg, unsigned int *num_visible_windows, bool *all_windows_have_same_bg, bool scan_for_animated_images) {
|
|
#define TD os_window->tab_bar_render_data
|
|
bool needs_render = os_window->needs_render;
|
|
os_window->needs_render = false;
|
|
if (TD.screen && os_window->num_tabs >= OPT(tab_bar_min_tabs)) {
|
|
if (!os_window->tab_bar_data_updated) {
|
|
call_boss(update_tab_bar_data, "K", os_window->id);
|
|
os_window->tab_bar_data_updated = true;
|
|
}
|
|
if (send_cell_data_to_gpu(TD.vao_idx, 0, TD.xstart, TD.ystart, TD.dx, TD.dy, TD.screen, os_window)) needs_render = true;
|
|
}
|
|
if (OPT(mouse_hide_wait) > 0 && !is_mouse_hidden(os_window)) {
|
|
if (now - os_window->last_mouse_activity_at >= OPT(mouse_hide_wait)) hide_mouse(os_window);
|
|
else set_maximum_wait(OPT(mouse_hide_wait) - now + os_window->last_mouse_activity_at);
|
|
}
|
|
Tab *tab = os_window->tabs + os_window->active_tab;
|
|
*active_window_bg = OPT(background);
|
|
*all_windows_have_same_bg = true;
|
|
*num_visible_windows = 0;
|
|
color_type first_window_bg = 0;
|
|
for (unsigned int i = 0; i < tab->num_windows; i++) {
|
|
Window *w = tab->windows + i;
|
|
#define WD w->render_data
|
|
if (w->visible && WD.screen) {
|
|
*num_visible_windows += 1;
|
|
color_type window_bg = colorprofile_to_color(WD.screen->color_profile, WD.screen->color_profile->overridden.default_bg, WD.screen->color_profile->configured.default_bg).rgb;
|
|
if (*num_visible_windows == 1) first_window_bg = window_bg;
|
|
if (first_window_bg != window_bg) *all_windows_have_same_bg = false;
|
|
if (w->last_drag_scroll_at > 0) {
|
|
if (now - w->last_drag_scroll_at >= ms_to_monotonic_t(20ll)) {
|
|
if (drag_scroll(w, os_window)) {
|
|
w->last_drag_scroll_at = now;
|
|
set_maximum_wait(ms_to_monotonic_t(20ll));
|
|
needs_render = true;
|
|
} else w->last_drag_scroll_at = 0;
|
|
} else set_maximum_wait(now - w->last_drag_scroll_at);
|
|
}
|
|
bool is_active_window = i == tab->active_window;
|
|
if (is_active_window) {
|
|
*active_window_id = w->id;
|
|
if (collect_cursor_info(&WD.screen->cursor_render_info, w, now, os_window)) needs_render = true;
|
|
WD.screen->cursor_render_info.is_focused = os_window->is_focused;
|
|
set_os_window_title_from_window(w, os_window);
|
|
*active_window_bg = window_bg;
|
|
} else {
|
|
if (WD.screen->render_unfocused_cursor) {
|
|
if (collect_cursor_info(&WD.screen->cursor_render_info, w, now, os_window)) needs_render = true;
|
|
WD.screen->cursor_render_info.is_focused = false;
|
|
} else {
|
|
WD.screen->cursor_render_info.is_visible = false;
|
|
}
|
|
}
|
|
if (scan_for_animated_images) {
|
|
monotonic_t min_gap;
|
|
if (scan_active_animations(WD.screen->grman, now, &min_gap, true)) needs_render = true;
|
|
if (min_gap < MONOTONIC_T_MAX) {
|
|
global_state.check_for_active_animated_images = true;
|
|
set_maximum_wait(min_gap);
|
|
}
|
|
}
|
|
if (send_cell_data_to_gpu(WD.vao_idx, WD.gvao_idx, WD.xstart, WD.ystart, WD.dx, WD.dy, WD.screen, os_window)) needs_render = true;
|
|
if (WD.screen->start_visual_bell_at != 0) needs_render = true;
|
|
}
|
|
}
|
|
return needs_render;
|
|
}
|
|
|
|
static void
|
|
render_os_window(OSWindow *os_window, unsigned int active_window_id, color_type active_window_bg, unsigned int num_visible_windows, bool all_windows_have_same_bg) {
|
|
// ensure all pixels are cleared to background color at least once in every buffer
|
|
if (os_window->clear_count++ < 3) blank_os_window(os_window);
|
|
Tab *tab = os_window->tabs + os_window->active_tab;
|
|
BorderRects *br = &tab->border_rects;
|
|
float x_ratio = 1, y_ratio = 1;
|
|
draw_borders(br->vao_idx, br->num_border_rects, br->rect_buf, br->is_dirty, os_window->viewport_width, os_window->viewport_height, active_window_bg, num_visible_windows, all_windows_have_same_bg, os_window);
|
|
br->is_dirty = false;
|
|
bool static_live_resize_in_progress = os_window->live_resize.in_progress && OPT(resize_draw_strategy) == RESIZE_DRAW_STATIC;
|
|
if (static_live_resize_in_progress) {
|
|
x_ratio = (float) os_window->viewport_width / (float) os_window->live_resize.width;
|
|
y_ratio = (float) os_window->viewport_height / (float) os_window->live_resize.height;
|
|
}
|
|
if (TD.screen && os_window->num_tabs >= OPT(tab_bar_min_tabs)) draw_cells(TD.vao_idx, 0, &TD, x_ratio, y_ratio, os_window, true, false, NULL);
|
|
for (unsigned int i = 0; i < tab->num_windows; i++) {
|
|
Window *w = tab->windows + i;
|
|
if (w->visible && WD.screen) {
|
|
bool is_active_window = i == tab->active_window;
|
|
draw_cells(WD.vao_idx, WD.gvao_idx, &WD, x_ratio, y_ratio, os_window, is_active_window, true, w);
|
|
if (WD.screen->start_visual_bell_at != 0) {
|
|
set_maximum_wait(OPT(repaint_delay));
|
|
}
|
|
w->cursor_visible_at_last_render = WD.screen->cursor_render_info.is_visible; w->last_cursor_x = WD.screen->cursor_render_info.x; w->last_cursor_y = WD.screen->cursor_render_info.y; w->last_cursor_shape = WD.screen->cursor_render_info.shape;
|
|
}
|
|
}
|
|
swap_window_buffers(os_window);
|
|
os_window->last_active_tab = os_window->active_tab; os_window->last_num_tabs = os_window->num_tabs; os_window->last_active_window_id = active_window_id;
|
|
os_window->focused_at_last_render = os_window->is_focused;
|
|
os_window->is_damaged = false;
|
|
if (USE_RENDER_FRAMES) request_frame_render(os_window);
|
|
#undef WD
|
|
#undef TD
|
|
}
|
|
|
|
static void
|
|
draw_resizing_text(OSWindow *w) {
|
|
char text[32] = {0};
|
|
unsigned int width = w->live_resize.width, height = w->live_resize.height;
|
|
snprintf(text, sizeof(text), "%u x %u cells", width / w->fonts_data->cell_width, height / w->fonts_data->cell_height);
|
|
StringCanvas rendered = render_simple_text(w->fonts_data, text);
|
|
if (rendered.canvas) {
|
|
draw_centered_alpha_mask(w, width, height, rendered.width, rendered.height, rendered.canvas);
|
|
free(rendered.canvas);
|
|
}
|
|
}
|
|
|
|
static bool
|
|
no_render_frame_received_recently(OSWindow *w, monotonic_t now, monotonic_t max_wait) {
|
|
bool ans = now - w->last_render_frame_received_at > max_wait;
|
|
if (ans && global_state.debug_rendering) {
|
|
if (global_state.is_wayland) {
|
|
log_error("No render frame received in %.2f seconds", monotonic_t_to_s_double(max_wait));
|
|
} else {
|
|
log_error("No render frame received in %.2f seconds, re-requesting at: %f", monotonic_t_to_s_double(max_wait), monotonic_t_to_s_double(now));
|
|
}
|
|
}
|
|
return ans;
|
|
}
|
|
|
|
static void
|
|
render(monotonic_t now, bool input_read) {
|
|
EVDBG("input_read: %d, check_for_active_animated_images: %d", input_read, global_state.check_for_active_animated_images);
|
|
static monotonic_t last_render_at = MONOTONIC_T_MIN;
|
|
monotonic_t time_since_last_render = last_render_at == MONOTONIC_T_MIN ? OPT(repaint_delay) : now - last_render_at;
|
|
if (!input_read && time_since_last_render < OPT(repaint_delay)) {
|
|
set_maximum_wait(OPT(repaint_delay) - time_since_last_render);
|
|
return;
|
|
}
|
|
|
|
const bool scan_for_animated_images = global_state.check_for_active_animated_images;
|
|
global_state.check_for_active_animated_images = false;
|
|
|
|
for (size_t i = 0; i < global_state.num_os_windows; i++) {
|
|
OSWindow *w = global_state.os_windows + i;
|
|
if (!w->num_tabs) continue;
|
|
if (!should_os_window_be_rendered(w)) {
|
|
update_os_window_title(w);
|
|
if (w->is_focused) change_menubar_title(w->window_title);
|
|
continue;
|
|
}
|
|
if (USE_RENDER_FRAMES && w->render_state != RENDER_FRAME_READY) {
|
|
if (w->render_state == RENDER_FRAME_NOT_REQUESTED || no_render_frame_received_recently(w, now, ms_to_monotonic_t(250ll))) request_frame_render(w);
|
|
// dont respect render frames soon after a resize on Wayland as they cause flicker because
|
|
// we want to fill the newly resized buffer ASAP, not at compositors convenience
|
|
if (!global_state.is_wayland || (monotonic() - w->viewport_resized_at) > s_double_to_monotonic_t(1)) continue;
|
|
}
|
|
w->render_calls++;
|
|
make_os_window_context_current(w);
|
|
if (w->live_resize.in_progress && OPT(resize_draw_strategy) >= RESIZE_DRAW_BLANK) {
|
|
blank_os_window(w);
|
|
if (OPT(resize_draw_strategy) == RESIZE_DRAW_SIZE) draw_resizing_text(w);
|
|
swap_window_buffers(w);
|
|
if (USE_RENDER_FRAMES) request_frame_render(w);
|
|
continue;
|
|
}
|
|
if (w->live_resize.in_progress && OPT(resize_draw_strategy) == RESIZE_DRAW_STATIC) blank_os_window(w);
|
|
bool needs_render = w->is_damaged || w->live_resize.in_progress;
|
|
if (w->viewport_size_dirty) {
|
|
w->clear_count = 0;
|
|
update_surface_size(w->viewport_width, w->viewport_height, w->offscreen_texture_id);
|
|
w->viewport_size_dirty = false;
|
|
needs_render = true;
|
|
}
|
|
unsigned int active_window_id = 0, num_visible_windows = 0;
|
|
bool all_windows_have_same_bg;
|
|
color_type active_window_bg = 0;
|
|
if (!w->fonts_data) { log_error("No fonts data found for window id: %llu", w->id); continue; }
|
|
if (prepare_to_render_os_window(w, now, &active_window_id, &active_window_bg, &num_visible_windows, &all_windows_have_same_bg, scan_for_animated_images)) needs_render = true;
|
|
if (w->last_active_window_id != active_window_id || w->last_active_tab != w->active_tab || w->focused_at_last_render != w->is_focused) needs_render = true;
|
|
if (w->render_calls < 3 && w->bgimage && w->bgimage->texture_id) needs_render = true;
|
|
if (needs_render) render_os_window(w, active_window_id, active_window_bg, num_visible_windows, all_windows_have_same_bg);
|
|
if (w->is_focused) change_menubar_title(w->window_title);
|
|
}
|
|
last_render_at = now;
|
|
#undef TD
|
|
}
|
|
|
|
|
|
typedef struct { int fd; uint8_t *buf; size_t sz; } ThreadWriteData;
|
|
|
|
static ThreadWriteData*
|
|
alloc_twd(size_t sz) {
|
|
ThreadWriteData *data = calloc(1, sizeof(ThreadWriteData));
|
|
if (data != NULL) {
|
|
data->sz = sz;
|
|
data->buf = malloc(sz);
|
|
if (data->buf == NULL) { free(data); data = NULL; }
|
|
}
|
|
return data;
|
|
}
|
|
|
|
static void
|
|
free_twd(ThreadWriteData *x) {
|
|
if (x != NULL) free(x->buf);
|
|
free(x);
|
|
}
|
|
|
|
static PyObject*
|
|
sig_queue(PyObject *self UNUSED, PyObject *args) {
|
|
int pid, signal, value;
|
|
if (!PyArg_ParseTuple(args, "iii", &pid, &signal, &value)) return NULL;
|
|
#ifdef NO_SIGQUEUE
|
|
if (kill(pid, signal) != 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
|
|
#else
|
|
union sigval v;
|
|
v.sival_int = value;
|
|
if (sigqueue(pid, signal, v) != 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
|
|
#endif
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
static PyObject*
|
|
monitor_pid(PyObject *self UNUSED, PyObject *args) {
|
|
int pid;
|
|
bool ok = true;
|
|
if (!PyArg_ParseTuple(args, "i", &pid)) return NULL;
|
|
children_mutex(lock);
|
|
if (monitored_pids_count >= arraysz(monitored_pids)) {
|
|
PyErr_SetString(PyExc_RuntimeError, "Too many monitored pids");
|
|
ok = false;
|
|
} else {
|
|
monitored_pids[monitored_pids_count++] = pid;
|
|
}
|
|
children_mutex(unlock);
|
|
if (!ok) return NULL;
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
static void
|
|
report_reaped_pids(void) {
|
|
children_mutex(lock);
|
|
if (reaped_pids_count) {
|
|
for (size_t i = 0; i < reaped_pids_count; i++) {
|
|
call_boss(on_monitored_pid_death, "ii", (int)reaped_pids[i].pid, reaped_pids[i].status);
|
|
}
|
|
reaped_pids_count = 0;
|
|
}
|
|
children_mutex(unlock);
|
|
}
|
|
|
|
static void*
|
|
thread_write(void *x) {
|
|
ThreadWriteData *data = (ThreadWriteData*)x;
|
|
set_thread_name("KittyWriteStdin");
|
|
int flags = fcntl(data->fd, F_GETFL, 0);
|
|
if (flags == -1) { free_twd(data); return 0; }
|
|
flags &= ~O_NONBLOCK;
|
|
fcntl(data->fd, F_SETFL, flags);
|
|
size_t pos = 0;
|
|
while (pos < data->sz) {
|
|
errno = 0;
|
|
ssize_t nbytes = write(data->fd, data->buf + pos, data->sz - pos);
|
|
if (nbytes < 0) {
|
|
if (errno == EAGAIN || errno == EINTR) continue;
|
|
break;
|
|
}
|
|
if (nbytes == 0) break;
|
|
pos += nbytes;
|
|
}
|
|
if (pos < data->sz) {
|
|
log_error("Failed to write all data to STDIN of child process with error: %s", strerror(errno));
|
|
}
|
|
safe_close(data->fd, __FILE__, __LINE__);
|
|
free_twd(data);
|
|
return 0;
|
|
}
|
|
|
|
PyObject*
|
|
cm_thread_write(PyObject UNUSED *self, PyObject *args) {
|
|
static pthread_t thread;
|
|
int fd;
|
|
Py_ssize_t sz;
|
|
const char *buf;
|
|
if (!PyArg_ParseTuple(args, "is#", &fd, &buf, &sz)) return NULL;
|
|
ThreadWriteData *data = alloc_twd(sz);
|
|
if (data == NULL) return PyErr_NoMemory();
|
|
data->fd = fd;
|
|
memcpy(data->buf, buf, data->sz);
|
|
int ret = pthread_create(&thread, NULL, thread_write, data);
|
|
if (ret != 0) { safe_close(fd, __FILE__, __LINE__); free_twd(data); return PyErr_Format(PyExc_OSError, "Failed to start write thread with error: %s", strerror(ret)); }
|
|
pthread_detach(thread);
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
static void
|
|
python_timer_callback(id_type timer_id, void *data) {
|
|
PyObject *callback = (PyObject*)data;
|
|
unsigned long long id = timer_id;
|
|
PyObject *ret = PyObject_CallFunction(callback, "K", id);
|
|
if (ret == NULL) PyErr_Print();
|
|
else Py_DECREF(ret);
|
|
}
|
|
|
|
static void
|
|
python_timer_cleanup(id_type timer_id UNUSED, void *data) {
|
|
if (data) Py_DECREF((PyObject*)data);
|
|
}
|
|
|
|
static PyObject*
|
|
add_python_timer(PyObject *self UNUSED, PyObject *args) {
|
|
PyObject *callback;
|
|
double interval;
|
|
int repeats = 1;
|
|
if (!PyArg_ParseTuple(args, "Od|p", &callback, &interval, &repeats)) return NULL;
|
|
unsigned long long timer_id = add_main_loop_timer(s_double_to_monotonic_t(interval), repeats ? true: false, python_timer_callback, callback, python_timer_cleanup);
|
|
Py_INCREF(callback);
|
|
return Py_BuildValue("K", timer_id);
|
|
}
|
|
|
|
static PyObject*
|
|
remove_python_timer(PyObject *self UNUSED, PyObject *args) {
|
|
unsigned long long timer_id;
|
|
if (!PyArg_ParseTuple(args, "K", &timer_id)) return NULL;
|
|
remove_main_loop_timer(timer_id);
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
|
|
static void
|
|
process_pending_resizes(monotonic_t now) {
|
|
global_state.has_pending_resizes = false;
|
|
for (size_t i = 0; i < global_state.num_os_windows; i++) {
|
|
OSWindow *w = global_state.os_windows + i;
|
|
if (w->live_resize.in_progress) {
|
|
bool update_viewport = false;
|
|
if (w->live_resize.from_os_notification) {
|
|
if (w->live_resize.os_says_resize_complete || (now - w->live_resize.last_resize_event_at) > 1) update_viewport = true;
|
|
} else {
|
|
monotonic_t debounce_time = OPT(resize_debounce_time);
|
|
// if more than one resize event has occurred, wait at least 0.2 secs
|
|
// before repainting, to avoid rapid transitions between the cells banner
|
|
// and the normal screen
|
|
if (w->live_resize.num_of_resize_events > 1 && OPT(resize_draw_strategy) == RESIZE_DRAW_SIZE) debounce_time = MAX(ms_to_monotonic_t(200ll), debounce_time);
|
|
if (now - w->live_resize.last_resize_event_at >= debounce_time) update_viewport = true;
|
|
else {
|
|
global_state.has_pending_resizes = true;
|
|
set_maximum_wait(OPT(resize_debounce_time) - now + w->live_resize.last_resize_event_at);
|
|
}
|
|
}
|
|
if (update_viewport) {
|
|
static const LiveResizeInfo empty = {0};
|
|
update_os_window_viewport(w, true);
|
|
w->live_resize = empty;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void
|
|
close_os_window(ChildMonitor *self, OSWindow *os_window) {
|
|
int w = os_window->content_area_width, h = os_window->content_area_height;
|
|
if (os_window->before_fullscreen.is_set && is_os_window_fullscreen(os_window)) {
|
|
w = os_window->before_fullscreen.w; h = os_window->before_fullscreen.h;
|
|
}
|
|
destroy_os_window(os_window);
|
|
call_boss(on_os_window_closed, "Kii", os_window->id, w, h);
|
|
for (size_t t=0; t < os_window->num_tabs; t++) {
|
|
Tab *tab = os_window->tabs + t;
|
|
for (size_t w = 0; w < tab->num_windows; w++) mark_child_for_close(self, tab->windows[w].id);
|
|
}
|
|
remove_os_window(os_window->id);
|
|
}
|
|
|
|
static bool
|
|
process_pending_closes(ChildMonitor *self) {
|
|
if (global_state.quit_request == CONFIRMABLE_CLOSE_REQUESTED) {
|
|
call_boss(quit, "");
|
|
}
|
|
if (global_state.quit_request == IMPERATIVE_CLOSE_REQUESTED) {
|
|
for (size_t w = 0; w < global_state.num_os_windows; w++) global_state.os_windows[w].close_request = IMPERATIVE_CLOSE_REQUESTED;
|
|
}
|
|
bool has_open_windows = false;
|
|
for (size_t w = global_state.num_os_windows; w > 0; w--) {
|
|
OSWindow *os_window = global_state.os_windows + w - 1;
|
|
switch(os_window->close_request) {
|
|
case NO_CLOSE_REQUESTED:
|
|
has_open_windows = true;
|
|
break;
|
|
case CONFIRMABLE_CLOSE_REQUESTED:
|
|
os_window->close_request = CLOSE_BEING_CONFIRMED;
|
|
call_boss(confirm_os_window_close, "K", os_window->id);
|
|
if (os_window->close_request == IMPERATIVE_CLOSE_REQUESTED) {
|
|
close_os_window(self, os_window);
|
|
} else has_open_windows = true;
|
|
break;
|
|
case CLOSE_BEING_CONFIRMED:
|
|
has_open_windows = true;
|
|
break;
|
|
case IMPERATIVE_CLOSE_REQUESTED:
|
|
close_os_window(self, os_window);
|
|
break;
|
|
}
|
|
}
|
|
global_state.has_pending_closes = false;
|
|
#ifdef __APPLE__
|
|
if (!OPT(macos_quit_when_last_window_closed)) {
|
|
if (!has_open_windows && global_state.quit_request != IMPERATIVE_CLOSE_REQUESTED) has_open_windows = true;
|
|
}
|
|
#endif
|
|
return !has_open_windows;
|
|
}
|
|
|
|
#ifdef __APPLE__
|
|
// If we create new OS windows during wait_events(), using global menu actions
|
|
// via the mouse causes a crash because of the way autorelease pools work in
|
|
// glfw/cocoa. So we use a flag instead.
|
|
static bool cocoa_pending_actions[NUM_COCOA_PENDING_ACTIONS] = {0};
|
|
static bool has_cocoa_pending_actions = false;
|
|
typedef struct {
|
|
char* wd;
|
|
char **open_urls;
|
|
size_t open_urls_count;
|
|
size_t open_urls_capacity;
|
|
} CocoaPendingActionsData;
|
|
static CocoaPendingActionsData cocoa_pending_actions_data = {0};
|
|
|
|
void
|
|
set_cocoa_pending_action(CocoaPendingAction action, const char *data) {
|
|
if (data) {
|
|
if (action == LAUNCH_URLS) {
|
|
ensure_space_for(&cocoa_pending_actions_data, open_urls, char*, cocoa_pending_actions_data.open_urls_count + 8, open_urls_capacity, 8, true);
|
|
cocoa_pending_actions_data.open_urls[cocoa_pending_actions_data.open_urls_count++] = strdup(data);
|
|
} else {
|
|
if (cocoa_pending_actions_data.wd) free(cocoa_pending_actions_data.wd);
|
|
cocoa_pending_actions_data.wd = strdup(data);
|
|
}
|
|
}
|
|
cocoa_pending_actions[action] = true;
|
|
has_cocoa_pending_actions = true;
|
|
// The main loop may be blocking on the event queue, if e.g. unfocused.
|
|
// Unjam it so the pending action is processed right now.
|
|
wakeup_main_loop();
|
|
}
|
|
|
|
static void
|
|
process_cocoa_pending_actions(void) {
|
|
if (cocoa_pending_actions[PREFERENCES_WINDOW]) { call_boss(edit_config_file, NULL); }
|
|
if (cocoa_pending_actions[NEW_OS_WINDOW]) { call_boss(new_os_window, NULL); }
|
|
if (cocoa_pending_actions[CLOSE_OS_WINDOW]) { call_boss(close_os_window, NULL); }
|
|
if (cocoa_pending_actions[CLOSE_TAB]) { call_boss(close_tab, NULL); }
|
|
if (cocoa_pending_actions[NEW_TAB]) { call_boss(new_tab, NULL); }
|
|
if (cocoa_pending_actions[NEXT_TAB]) { call_boss(next_tab, NULL); }
|
|
if (cocoa_pending_actions[PREVIOUS_TAB]) { call_boss(previous_tab, NULL); }
|
|
if (cocoa_pending_actions[DETACH_TAB]) { call_boss(detach_tab, NULL); }
|
|
if (cocoa_pending_actions[NEW_WINDOW]) { call_boss(new_window, NULL); }
|
|
if (cocoa_pending_actions[CLOSE_WINDOW]) { call_boss(close_window, NULL); }
|
|
if (cocoa_pending_actions[RESET_TERMINAL]) { call_boss(clear_terminal, "sO", "reset", Py_True ); }
|
|
if (cocoa_pending_actions[CLEAR_TERMINAL_AND_SCROLLBACK]) { call_boss(clear_terminal, "sO", "to_cursor", Py_True ); }
|
|
if (cocoa_pending_actions[RELOAD_CONFIG]) { call_boss(load_config_file, NULL); }
|
|
if (cocoa_pending_actions[TOGGLE_MACOS_SECURE_KEYBOARD_ENTRY]) { call_boss(toggle_macos_secure_keyboard_entry, NULL); }
|
|
if (cocoa_pending_actions[TOGGLE_FULLSCREEN]) { call_boss(toggle_fullscreen, NULL); }
|
|
if (cocoa_pending_actions[OPEN_KITTY_WEBSITE]) { call_boss(open_kitty_website, NULL); }
|
|
if (cocoa_pending_actions[HIDE]) { call_boss(hide_macos_app, NULL); }
|
|
if (cocoa_pending_actions[HIDE_OTHERS]) { call_boss(hide_macos_other_apps, NULL); }
|
|
if (cocoa_pending_actions[MINIMIZE]) { call_boss(minimize_macos_window, NULL); }
|
|
if (cocoa_pending_actions[QUIT]) { call_boss(quit, NULL); }
|
|
if (cocoa_pending_actions_data.wd) {
|
|
if (cocoa_pending_actions[NEW_OS_WINDOW_WITH_WD]) { call_boss(new_os_window_with_wd, "sO", cocoa_pending_actions_data.wd, Py_True); }
|
|
if (cocoa_pending_actions[NEW_TAB_WITH_WD]) { call_boss(new_tab_with_wd, "sO", cocoa_pending_actions_data.wd, Py_True); }
|
|
free(cocoa_pending_actions_data.wd);
|
|
cocoa_pending_actions_data.wd = NULL;
|
|
}
|
|
if (cocoa_pending_actions_data.open_urls_count) {
|
|
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_urls_count; cpa++) {
|
|
if (cocoa_pending_actions_data.open_urls[cpa]) {
|
|
call_boss(launch_urls, "s", cocoa_pending_actions_data.open_urls[cpa]);
|
|
free(cocoa_pending_actions_data.open_urls[cpa]);
|
|
cocoa_pending_actions_data.open_urls[cpa] = NULL;
|
|
}
|
|
}
|
|
cocoa_pending_actions_data.open_urls_count = 0;
|
|
}
|
|
memset(cocoa_pending_actions, 0, sizeof(cocoa_pending_actions));
|
|
has_cocoa_pending_actions = false;
|
|
|
|
}
|
|
#endif
|
|
|
|
static void process_global_state(void *data);
|
|
|
|
static void
|
|
do_state_check(id_type timer_id UNUSED, void *data) {
|
|
EVDBG("State check timer fired");
|
|
process_global_state(data);
|
|
}
|
|
|
|
static id_type state_check_timer = 0;
|
|
|
|
static void
|
|
process_global_state(void *data) {
|
|
EVDBG("Processing global state");
|
|
ChildMonitor *self = data;
|
|
maximum_wait = -1;
|
|
bool state_check_timer_enabled = false;
|
|
bool input_read = false;
|
|
|
|
monotonic_t now = monotonic();
|
|
if (global_state.has_pending_resizes) {
|
|
process_pending_resizes(now);
|
|
input_read = true;
|
|
}
|
|
if (parse_input(self)) input_read = true;
|
|
render(now, input_read);
|
|
#ifdef __APPLE__
|
|
if (has_cocoa_pending_actions) {
|
|
process_cocoa_pending_actions();
|
|
maximum_wait = 0; // ensure loop ticks again so that the actions side effects are performed immediately
|
|
}
|
|
#endif
|
|
report_reaped_pids();
|
|
bool should_quit = false;
|
|
if (global_state.has_pending_closes) should_quit = process_pending_closes(self);
|
|
if (should_quit) {
|
|
stop_main_loop();
|
|
} else {
|
|
if (maximum_wait >= 0) {
|
|
if (maximum_wait == 0) request_tick_callback();
|
|
else state_check_timer_enabled = true;
|
|
}
|
|
}
|
|
update_main_loop_timer(state_check_timer, MAX(0, maximum_wait), state_check_timer_enabled);
|
|
}
|
|
|
|
static PyObject*
|
|
main_loop(ChildMonitor *self, PyObject *a UNUSED) {
|
|
#define main_loop_doc "The main thread loop"
|
|
state_check_timer = add_main_loop_timer(1000, true, do_state_check, self, NULL);
|
|
run_main_loop(process_global_state, self);
|
|
#ifdef __APPLE__
|
|
if (cocoa_pending_actions_data.wd) { free(cocoa_pending_actions_data.wd); cocoa_pending_actions_data.wd = NULL; }
|
|
if (cocoa_pending_actions_data.open_urls) {
|
|
for (unsigned cpa = 0; cpa < cocoa_pending_actions_data.open_urls_count; cpa++) {
|
|
if (cocoa_pending_actions_data.open_urls[cpa]) free(cocoa_pending_actions_data.open_urls[cpa]);
|
|
}
|
|
free(cocoa_pending_actions_data.open_urls); cocoa_pending_actions_data.open_urls = NULL;
|
|
}
|
|
#endif
|
|
if (PyErr_Occurred()) return NULL;
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
// }}}
|
|
|
|
// I/O thread functions {{{
|
|
|
|
static void
|
|
add_children(ChildMonitor *self) {
|
|
for (; add_queue_count > 0 && self->count < MAX_CHILDREN;) {
|
|
add_queue_count--;
|
|
children[self->count] = add_queue[add_queue_count];
|
|
add_queue[add_queue_count] = EMPTY_CHILD;
|
|
children_fds[EXTRA_FDS + self->count].fd = children[self->count].fd;
|
|
children_fds[EXTRA_FDS + self->count].events = POLLIN;
|
|
self->count++;
|
|
}
|
|
}
|
|
|
|
|
|
static void
|
|
hangup(pid_t pid) {
|
|
errno = 0;
|
|
pid_t pgid = getpgid(pid);
|
|
if (errno == ESRCH) return;
|
|
if (errno != 0) { perror("Failed to get process group id for child"); return; }
|
|
if (killpg(pgid, SIGHUP) != 0) {
|
|
if (errno != ESRCH) perror("Failed to kill child");
|
|
}
|
|
}
|
|
|
|
|
|
static void
|
|
cleanup_child(ssize_t i) {
|
|
safe_close(children[i].fd, __FILE__, __LINE__);
|
|
hangup(children[i].pid);
|
|
}
|
|
|
|
|
|
static void
|
|
remove_children(ChildMonitor *self) {
|
|
if (self->count > 0) {
|
|
size_t count = 0;
|
|
for (ssize_t i = self->count - 1; i >= 0; i--) {
|
|
if (children[i].needs_removal) {
|
|
count++;
|
|
cleanup_child(i);
|
|
remove_queue[remove_queue_count] = children[i];
|
|
remove_queue_count++;
|
|
children[i] = EMPTY_CHILD;
|
|
children_fds[EXTRA_FDS + i].fd = -1;
|
|
size_t num_to_right = self->count - 1 - i;
|
|
if (num_to_right > 0) {
|
|
memmove(children + i, children + i + 1, num_to_right * sizeof(Child));
|
|
memmove(children_fds + EXTRA_FDS + i, children_fds + EXTRA_FDS + i + 1, num_to_right * sizeof(struct pollfd));
|
|
}
|
|
}
|
|
}
|
|
self->count -= count;
|
|
}
|
|
}
|
|
|
|
|
|
static bool
|
|
read_bytes(int fd, Screen *screen) {
|
|
ssize_t len;
|
|
size_t available_buffer_space, orig_sz;
|
|
|
|
screen_mutex(lock, read);
|
|
orig_sz = screen->read_buf_sz;
|
|
if (orig_sz >= READ_BUF_SZ) { screen_mutex(unlock, read); return true; } // screen read buffer is full
|
|
available_buffer_space = READ_BUF_SZ - orig_sz;
|
|
screen_mutex(unlock, read);
|
|
|
|
while(true) {
|
|
len = read(fd, screen->read_buf + orig_sz, available_buffer_space);
|
|
if (len < 0) {
|
|
if (errno == EINTR || errno == EAGAIN) continue;
|
|
if (errno != EIO) perror("Call to read() from child fd failed");
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
if (UNLIKELY(len == 0)) return false;
|
|
|
|
screen_mutex(lock, read);
|
|
if (screen->new_input_at == 0) screen->new_input_at = monotonic();
|
|
if (orig_sz != screen->read_buf_sz) {
|
|
// The other thread consumed some of the screen read buffer
|
|
memmove(screen->read_buf + screen->read_buf_sz, screen->read_buf + orig_sz, len);
|
|
}
|
|
screen->read_buf_sz += len;
|
|
screen_mutex(unlock, read);
|
|
return true;
|
|
}
|
|
|
|
|
|
typedef struct { bool kill_signal, child_died, reload_config; } SignalSet;
|
|
|
|
static bool
|
|
handle_signal(const siginfo_t *siginfo, void *data) {
|
|
SignalSet *ss = data;
|
|
switch(siginfo->si_signo) {
|
|
case SIGINT:
|
|
case SIGTERM:
|
|
case SIGHUP:
|
|
ss->kill_signal = true;
|
|
break;
|
|
case SIGCHLD:
|
|
ss->child_died = true;
|
|
break;
|
|
case SIGUSR1:
|
|
ss->reload_config = true;
|
|
break;
|
|
case SIGUSR2:
|
|
log_error("Received SIGUSR2: %d\n", siginfo->si_value.sival_int);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
mark_child_for_removal(ChildMonitor *self, pid_t pid) {
|
|
children_mutex(lock);
|
|
for (size_t i = 0; i < self->count; i++) {
|
|
if (children[i].pid == pid) {
|
|
children[i].needs_removal = true;
|
|
break;
|
|
}
|
|
}
|
|
children_mutex(unlock);
|
|
}
|
|
|
|
static void
|
|
mark_monitored_pids(pid_t pid, int status) {
|
|
children_mutex(lock);
|
|
for (ssize_t i = monitored_pids_count - 1; i >= 0; i--) {
|
|
if (pid == monitored_pids[i]) {
|
|
if (reaped_pids_count < arraysz(reaped_pids)) {
|
|
reaped_pids[reaped_pids_count].status = status;
|
|
reaped_pids[reaped_pids_count++].pid = pid;
|
|
}
|
|
remove_i_from_array(monitored_pids, (size_t)i, monitored_pids_count);
|
|
}
|
|
}
|
|
children_mutex(unlock);
|
|
}
|
|
|
|
static void
|
|
reap_prewarmed_children(ChildMonitor *self, int fd, bool enable_close_on_child_death) {
|
|
static char buf[256];
|
|
static size_t buf_pos = 0;
|
|
while(true) {
|
|
ssize_t len = read(fd, buf + buf_pos, sizeof(buf) - buf_pos);
|
|
if (len < 0) {
|
|
if (errno == EINTR) continue;
|
|
if (errno != EIO && errno != EAGAIN) log_error("Call to read() from reap_prewarmed_children() failed with error: %s", strerror(errno));
|
|
break;
|
|
}
|
|
buf_pos += len;
|
|
char *nl;
|
|
while (buf_pos > 1 && (nl = memchr(buf, '\n', buf_pos)) != NULL) {
|
|
size_t sz = nl - buf + 1;
|
|
if (enable_close_on_child_death) {
|
|
*nl = 0;
|
|
int pid = atoi(buf);
|
|
if (pid) mark_child_for_removal(self, pid);
|
|
}
|
|
memmove(buf, buf + sz, sz);
|
|
buf_pos -= sz;
|
|
}
|
|
if (len == 0) break;
|
|
}
|
|
|
|
}
|
|
|
|
static void
|
|
reap_children(ChildMonitor *self, bool enable_close_on_child_death) {
|
|
int status;
|
|
pid_t pid;
|
|
(void)self;
|
|
while(true) {
|
|
pid = waitpid(-1, &status, WNOHANG);
|
|
if (pid == -1) {
|
|
if (errno != EINTR) break;
|
|
} else if (pid > 0) {
|
|
if (enable_close_on_child_death) mark_child_for_removal(self, pid);
|
|
mark_monitored_pids(pid, status);
|
|
} else break;
|
|
}
|
|
}
|
|
|
|
#ifdef KITTY_PRINT_BYTES_SENT_TO_CHILD
|
|
static void
|
|
print_text(const unsigned char *text, ssize_t sz) {
|
|
for (ssize_t i = 0; i < sz; i++) {
|
|
unsigned char ch = text[i];
|
|
if (32 <= ch && ch < 127) {
|
|
if (ch == '\\') fprintf(stderr, "%c", ch);
|
|
fprintf(stderr, "%c", ch);
|
|
} else fprintf(stderr, "\\x%02x", ch);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
|
|
static void
|
|
write_to_child(int fd, Screen *screen) {
|
|
size_t written = 0;
|
|
ssize_t ret = 0;
|
|
screen_mutex(lock, write);
|
|
while (written < screen->write_buf_used) {
|
|
ret = write(fd, screen->write_buf + written, screen->write_buf_used - written);
|
|
#ifdef KITTY_PRINT_BYTES_SENT_TO_CHILD
|
|
fprintf(stderr, "Wrote: %zd bytes: ", ret);
|
|
#endif
|
|
if (ret > 0) {
|
|
#ifdef KITTY_PRINT_BYTES_SENT_TO_CHILD
|
|
print_text(screen->write_buf + written, ret);
|
|
#endif
|
|
written += ret;
|
|
}
|
|
else if (ret == 0) {
|
|
// could mean anything, ignore
|
|
break;
|
|
} else {
|
|
if (errno == EINTR) continue;
|
|
if (errno == EWOULDBLOCK || errno == EAGAIN) break;
|
|
perror("Call to write() to child fd failed, discarding data.");
|
|
written = screen->write_buf_used;
|
|
}
|
|
#ifdef KITTY_PRINT_BYTES_SENT_TO_CHILD
|
|
fprintf(stderr, "\n");
|
|
#endif
|
|
}
|
|
if (written) {
|
|
screen->write_buf_used -= written;
|
|
if (screen->write_buf_used) {
|
|
memmove(screen->write_buf, screen->write_buf + written, screen->write_buf_used);
|
|
}
|
|
}
|
|
screen_mutex(unlock, write);
|
|
}
|
|
|
|
static void*
|
|
io_loop(void *data) {
|
|
// The I/O thread loop
|
|
size_t i;
|
|
int ret;
|
|
bool has_more, data_received, has_pending_wakeups = false;
|
|
monotonic_t last_main_loop_wakeup_at = -1, now = -1;
|
|
Screen *screen;
|
|
ChildMonitor *self = (ChildMonitor*)data;
|
|
set_thread_name("KittyChildMon");
|
|
|
|
while (LIKELY(!self->shutting_down)) {
|
|
children_mutex(lock);
|
|
remove_children(self);
|
|
add_children(self);
|
|
children_mutex(unlock);
|
|
data_received = false;
|
|
for (i = 0; i < self->count + EXTRA_FDS; i++) children_fds[i].revents = 0;
|
|
for (i = 0; i < self->count; i++) {
|
|
screen = children[i].screen;
|
|
/* printf("i:%lu id:%lu fd: %d read_buf_sz: %lu write_buf_used: %lu\n", i, children[i].id, children[i].fd, screen->read_buf_sz, screen->write_buf_used); */
|
|
screen_mutex(lock, read); screen_mutex(lock, write);
|
|
children_fds[EXTRA_FDS + i].events = (screen->read_buf_sz < READ_BUF_SZ ? POLLIN : 0) | (screen->write_buf_used ? POLLOUT : 0);
|
|
screen_mutex(unlock, read); screen_mutex(unlock, write);
|
|
}
|
|
if (has_pending_wakeups) {
|
|
now = monotonic();
|
|
monotonic_t time_delta = OPT(input_delay) - (now - last_main_loop_wakeup_at);
|
|
if (time_delta >= 0) ret = poll(children_fds, self->count + EXTRA_FDS, monotonic_t_to_ms(time_delta));
|
|
else ret = 0;
|
|
} else {
|
|
ret = poll(children_fds, self->count + EXTRA_FDS, -1);
|
|
}
|
|
if (ret > 0) {
|
|
if (children_fds[0].revents && POLLIN) drain_fd(children_fds[0].fd); // wakeup
|
|
if (children_fds[1].revents && POLLIN) {
|
|
SignalSet ss = {0};
|
|
data_received = true;
|
|
read_signals(children_fds[1].fd, handle_signal, &ss);
|
|
if (ss.kill_signal || ss.reload_config) {
|
|
children_mutex(lock);
|
|
if (ss.kill_signal) kill_signal_received = true;
|
|
if (ss.reload_config) reload_config_signal_received = true;
|
|
children_mutex(unlock);
|
|
}
|
|
if (ss.child_died) reap_children(self, OPT(close_on_child_death));
|
|
}
|
|
if (children_fds[2].revents && POLLIN) {
|
|
reap_prewarmed_children(self, children_fds[2].fd, OPT(close_on_child_death));
|
|
}
|
|
for (i = 0; i < self->count; i++) {
|
|
if (children_fds[EXTRA_FDS + i].revents & (POLLIN | POLLHUP)) {
|
|
data_received = true;
|
|
has_more = read_bytes(children_fds[EXTRA_FDS + i].fd, children[i].screen);
|
|
if (!has_more) {
|
|
// child is dead
|
|
children_mutex(lock);
|
|
children[i].needs_removal = true;
|
|
children_mutex(unlock);
|
|
}
|
|
}
|
|
if (children_fds[EXTRA_FDS + i].revents & POLLOUT) {
|
|
write_to_child(children[i].fd, children[i].screen);
|
|
}
|
|
if (children_fds[EXTRA_FDS + i].revents & POLLNVAL) {
|
|
// fd was closed
|
|
children_mutex(lock);
|
|
children[i].needs_removal = true;
|
|
children_mutex(unlock);
|
|
log_error("The child %lu had its fd unexpectedly closed", children[i].id);
|
|
}
|
|
}
|
|
#ifdef DEBUG_POLL_EVENTS
|
|
for (i = 0; i < self->count + EXTRA_FDS; i++) {
|
|
#define P(w) if (children_fds[i].revents & w) printf("i:%lu %s\n", i, #w);
|
|
P(POLLIN); P(POLLPRI); P(POLLOUT); P(POLLERR); P(POLLHUP); P(POLLNVAL);
|
|
#undef P
|
|
}
|
|
#endif
|
|
} else if (ret < 0) {
|
|
if (errno != EAGAIN && errno != EINTR) {
|
|
perror("Call to poll() failed");
|
|
}
|
|
}
|
|
#define WAKEUP { wakeup_main_loop(); last_main_loop_wakeup_at = now; has_pending_wakeups = false; }
|
|
// we only wakeup the main loop after input_delay as wakeup is an expensive operation
|
|
// on some platforms, such as cocoa
|
|
if (data_received) {
|
|
if ((now = monotonic()) - last_main_loop_wakeup_at > OPT(input_delay)) WAKEUP
|
|
else has_pending_wakeups = true;
|
|
} else {
|
|
if (has_pending_wakeups && (now = monotonic()) - last_main_loop_wakeup_at > OPT(input_delay)) WAKEUP
|
|
}
|
|
}
|
|
#undef WAKEUP
|
|
children_mutex(lock);
|
|
for (i = 0; i < self->count; i++) children[i].needs_removal = true;
|
|
remove_children(self);
|
|
children_mutex(unlock);
|
|
return 0;
|
|
}
|
|
// }}}
|
|
|
|
// {{{ Talk thread functions
|
|
|
|
typedef struct {
|
|
id_type id;
|
|
size_t num_of_unresponded_messages_sent_to_main_thread, fd_array_idx;
|
|
bool finished_reading;
|
|
int fd;
|
|
struct {
|
|
char *data;
|
|
size_t capacity, used, command_end;
|
|
bool finished;
|
|
} read;
|
|
struct {
|
|
char *data;
|
|
size_t capacity, used;
|
|
bool failed;
|
|
} write;
|
|
} Peer;
|
|
static id_type peer_id_counter = 0;
|
|
|
|
typedef struct {
|
|
size_t num_peers, peers_capacity;
|
|
Peer *peers;
|
|
LoopData loop_data;
|
|
} TalkData;
|
|
static TalkData talk_data = {0};
|
|
|
|
typedef struct pollfd PollFD;
|
|
#define PEER_LIMIT 256
|
|
#define nuke_socket(s) { shutdown(s, SHUT_RDWR); safe_close(s, __FILE__, __LINE__); }
|
|
|
|
static bool
|
|
accept_peer(int listen_fd, bool shutting_down) {
|
|
int peer = accept(listen_fd, NULL, NULL);
|
|
if (UNLIKELY(peer == -1)) {
|
|
if (errno == EINTR) return true;
|
|
if (!shutting_down) perror("accept() on talk socket failed!");
|
|
return false;
|
|
}
|
|
if (talk_data.num_peers < PEER_LIMIT) {
|
|
ensure_space_for(&talk_data, peers, Peer, talk_data.num_peers + 8, peers_capacity, 8, false);
|
|
Peer *p = talk_data.peers + talk_data.num_peers++;
|
|
memset(p, 0, sizeof(Peer));
|
|
p->fd = peer; p->id = ++peer_id_counter;
|
|
if (!p->id) p->id = ++peer_id_counter;
|
|
} else {
|
|
log_error("Too many peers want to talk, ignoring one.");
|
|
nuke_socket(peer);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static void
|
|
free_peer(Peer *peer) {
|
|
free(peer->read.data); peer->read.data = NULL;
|
|
free(peer->write.data); peer->write.data = NULL;
|
|
if (peer->fd > -1) { nuke_socket(peer->fd); peer->fd = -1; }
|
|
}
|
|
|
|
#define KITTY_CMD_PREFIX "\x1bP@kitty-cmd{"
|
|
|
|
static void
|
|
queue_peer_message(ChildMonitor *self, Peer *peer) {
|
|
talk_mutex(lock);
|
|
ensure_space_for(self, messages, Message, self->messages_count + 16, messages_capacity, 16, true);
|
|
Message *m = self->messages + self->messages_count++;
|
|
memset(m, 0, sizeof(Message));
|
|
if (peer->read.used) {
|
|
m->data = malloc(peer->read.used);
|
|
if (m->data) {
|
|
memcpy(m->data, peer->read.data, peer->read.used);
|
|
m->sz = peer->read.used;
|
|
}
|
|
}
|
|
m->peer_id = peer->id;
|
|
peer->num_of_unresponded_messages_sent_to_main_thread++;
|
|
talk_mutex(unlock);
|
|
wakeup_main_loop();
|
|
}
|
|
|
|
static bool
|
|
has_complete_peer_command(Peer *peer) {
|
|
peer->read.command_end = 0;
|
|
if (peer->read.used > sizeof(KITTY_CMD_PREFIX) && memcmp(peer->read.data, KITTY_CMD_PREFIX, sizeof(KITTY_CMD_PREFIX)-1) == 0) {
|
|
for (size_t i = sizeof(KITTY_CMD_PREFIX)-1; i < peer->read.used - 1; i++) {
|
|
if (peer->read.data[i] == 0x1b && peer->read.data[i+1] == '\\') {
|
|
peer->read.command_end = i + 2;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return peer->read.command_end ? true : false;
|
|
}
|
|
|
|
|
|
static void
|
|
dispatch_peer_command(ChildMonitor *self, Peer *peer) {
|
|
if (peer->read.command_end) {
|
|
size_t used = peer->read.used;
|
|
peer->read.used = peer->read.command_end;
|
|
queue_peer_message(self, peer);
|
|
peer->read.used = used;
|
|
if (peer->read.used > peer->read.command_end) {
|
|
peer->read.used -= peer->read.command_end;
|
|
memmove(peer->read.data, peer->read.data + peer->read.command_end, peer->read.used);
|
|
} else peer->read.used = 0;
|
|
peer->read.command_end = 0;
|
|
}
|
|
}
|
|
|
|
static void
|
|
read_from_peer(ChildMonitor *self, Peer *peer) {
|
|
#define failed(msg) { log_error("Reading from peer failed: %s", msg); shutdown(peer->fd, SHUT_RD); peer->read.finished = true; return; }
|
|
if (peer->read.used >= peer->read.capacity) {
|
|
if (peer->read.capacity >= 64 * 1024) failed("Ignoring too large message from peer");
|
|
peer->read.capacity = MAX(8192u, peer->read.capacity * 2);
|
|
peer->read.data = realloc(peer->read.data, peer->read.capacity);
|
|
if (!peer->read.data) failed("Out of memory");
|
|
}
|
|
ssize_t n = recv(peer->fd, peer->read.data + peer->read.used, peer->read.capacity - peer->read.used, 0);
|
|
if (n == 0) {
|
|
peer->read.finished = true;
|
|
shutdown(peer->fd, SHUT_RD);
|
|
while (has_complete_peer_command(peer)) dispatch_peer_command(self, peer);
|
|
queue_peer_message(self, peer);
|
|
free(peer->read.data); peer->read.data = NULL;
|
|
peer->read.used = 0; peer->read.capacity = 0;
|
|
} else if (n < 0) {
|
|
if (errno != EINTR) failed(strerror(errno));
|
|
} else {
|
|
peer->read.used += n;
|
|
while (has_complete_peer_command(peer)) dispatch_peer_command(self, peer);
|
|
}
|
|
#undef failed
|
|
}
|
|
|
|
static void
|
|
write_to_peer(Peer *peer) {
|
|
talk_mutex(lock);
|
|
ssize_t n = send(peer->fd, peer->write.data, peer->write.used, MSG_NOSIGNAL);
|
|
if (n == 0) { log_error("send() to peer failed to send any data"); peer->write.used = 0; peer->write.failed = true; }
|
|
else if (n < 0) {
|
|
if (errno != EINTR) { log_error("write() to peer socket failed with error: %s", strerror(errno)); peer->write.used = 0; peer->write.failed = true; }
|
|
} else {
|
|
if ((size_t)n > peer->write.used) memmove(peer->write.data, peer->write.data + n, peer->write.used - n);
|
|
peer->write.used -= n;
|
|
}
|
|
talk_mutex(unlock);
|
|
}
|
|
|
|
static void
|
|
wakeup_talk_loop(bool in_signal_handler) {
|
|
if (talk_thread_started) wakeup_loop(&talk_data.loop_data, in_signal_handler, "talk_loop");
|
|
}
|
|
|
|
|
|
static void
|
|
prune_peers(void) {
|
|
for (size_t idx = talk_data.num_peers; idx-- > 0;) {
|
|
Peer *p = talk_data.peers + idx;
|
|
if (p->read.finished && !p->num_of_unresponded_messages_sent_to_main_thread && !p->write.used) {
|
|
free_peer(p);
|
|
remove_i_from_array(talk_data.peers, idx, talk_data.num_peers);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void*
|
|
talk_loop(void *data) {
|
|
// The talk thread loop
|
|
ChildMonitor *self = (ChildMonitor*)data;
|
|
set_thread_name("KittyPeerMon");
|
|
if (!init_loop_data(&talk_data.loop_data, 0)) { log_error("Failed to create wakeup fd for talk thread with error: %s", strerror(errno)); }
|
|
PollFD fds[PEER_LIMIT + 8] = {{0}};
|
|
size_t num_listen_fds = 0, num_peer_fds = 0;
|
|
#define add_listener(which) \
|
|
if (self->which > -1) { \
|
|
fds[num_listen_fds].fd = self->which; fds[num_listen_fds++].events = POLLIN; \
|
|
}
|
|
add_listener(talk_fd); add_listener(listen_fd);
|
|
#undef add_listener
|
|
fds[num_listen_fds].fd = talk_data.loop_data.wakeup_read_fd; fds[num_listen_fds++].events = POLLIN;
|
|
|
|
while (LIKELY(!self->shutting_down)) {
|
|
num_peer_fds = 0;
|
|
if (talk_data.num_peers > 0) {
|
|
talk_mutex(lock);
|
|
prune_peers();
|
|
for (size_t i = 0; i < talk_data.num_peers; i++) {
|
|
Peer *p = talk_data.peers + i;
|
|
if (!p->read.finished || p->write.used) {
|
|
p->fd_array_idx = num_listen_fds + num_peer_fds++;
|
|
fds[p->fd_array_idx].fd = p->fd;
|
|
fds[p->fd_array_idx].revents = 0;
|
|
int flags = 0;
|
|
if (!p->read.finished) flags |= POLLIN;
|
|
if (p->write.used) flags |= POLLOUT;
|
|
fds[p->fd_array_idx].events = flags;
|
|
} else p->fd_array_idx = 0;
|
|
}
|
|
talk_mutex(unlock);
|
|
}
|
|
for (size_t i = 0; i < num_listen_fds; i++) fds[i].revents = 0;
|
|
int ret = poll(fds, num_listen_fds + num_peer_fds, -1);
|
|
if (ret > 0) {
|
|
for (size_t i = 0; i < num_listen_fds - 1; i++) {
|
|
if (fds[i].revents & POLLIN) {
|
|
if (!accept_peer(fds[i].fd, self->shutting_down)) goto end;
|
|
}
|
|
}
|
|
if (fds[num_listen_fds - 1].revents & POLLIN) {
|
|
drain_fd(fds[num_listen_fds - 1].fd); // wakeup
|
|
}
|
|
for (size_t k = 0; k < talk_data.num_peers; k++) {
|
|
Peer *p = talk_data.peers + k;
|
|
if (p->fd_array_idx) {
|
|
if (fds[p->fd_array_idx].revents & (POLLIN | POLLHUP)) read_from_peer(self, p);
|
|
if (fds[p->fd_array_idx].revents & POLLOUT) write_to_peer(p);
|
|
if (fds[p->fd_array_idx].revents & POLLNVAL) {
|
|
p->read.finished = true;
|
|
p->write.failed = true; p->write.used = 0;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
} else if (ret < 0) { if (errno != EAGAIN && errno != EINTR) perror("poll() on talk fds failed"); }
|
|
}
|
|
end:
|
|
free_loop_data(&talk_data.loop_data);
|
|
for (size_t i = 0; i < talk_data.num_peers; i++) free_peer(talk_data.peers + i);
|
|
free(talk_data.peers);
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
send_response_to_peer(id_type peer_id, const char *msg, size_t msg_sz) {
|
|
bool wakeup = false;
|
|
talk_mutex(lock);
|
|
for (size_t i = 0; i < talk_data.num_peers; i++) {
|
|
Peer *peer = talk_data.peers + i;
|
|
if (peer->id == peer_id) {
|
|
if (peer->num_of_unresponded_messages_sent_to_main_thread) peer->num_of_unresponded_messages_sent_to_main_thread--;
|
|
if (!peer->write.failed) {
|
|
if (peer->write.capacity - peer->write.used < msg_sz) {
|
|
void *data = realloc(peer->write.data, peer->write.capacity + msg_sz);
|
|
if (data) {
|
|
peer->write.data = data;
|
|
peer->write.capacity += msg_sz;
|
|
} else fatal("Out of memory");
|
|
}
|
|
if (msg_sz && msg) {
|
|
memcpy(peer->write.data + peer->write.used, msg, msg_sz);
|
|
peer->write.used += msg_sz;
|
|
}
|
|
}
|
|
wakeup = true;
|
|
break;
|
|
}
|
|
}
|
|
talk_mutex(unlock);
|
|
if (wakeup) wakeup_talk_loop(false);
|
|
}
|
|
|
|
// }}}
|
|
|
|
// Boilerplate {{{
|
|
static PyMethodDef methods[] = {
|
|
METHOD(add_child, METH_VARARGS)
|
|
METHOD(needs_write, METH_VARARGS)
|
|
METHOD(start, METH_NOARGS)
|
|
METHOD(wakeup, METH_NOARGS)
|
|
METHOD(shutdown_monitor, METH_NOARGS)
|
|
METHOD(main_loop, METH_NOARGS)
|
|
METHOD(mark_for_close, METH_VARARGS)
|
|
METHOD(resize_pty, METH_VARARGS)
|
|
METHODB(handled_signals, METH_NOARGS),
|
|
{"set_iutf8_winid", (PyCFunction)pyset_iutf8, METH_VARARGS, ""},
|
|
{NULL} /* Sentinel */
|
|
};
|
|
|
|
|
|
PyTypeObject ChildMonitor_Type = {
|
|
PyVarObject_HEAD_INIT(NULL, 0)
|
|
.tp_name = "fast_data_types.ChildMonitor",
|
|
.tp_basicsize = sizeof(ChildMonitor),
|
|
.tp_dealloc = (destructor)dealloc,
|
|
.tp_flags = Py_TPFLAGS_DEFAULT,
|
|
.tp_doc = "ChildMonitor",
|
|
.tp_methods = methods,
|
|
.tp_new = new,
|
|
};
|
|
|
|
|
|
|
|
static PyObject*
|
|
safe_pipe(PyObject *self UNUSED, PyObject *args) {
|
|
int nonblock = 1;
|
|
if (!PyArg_ParseTuple(args, "|p", &nonblock)) return NULL;
|
|
int fds[2] = {0};
|
|
if (!self_pipe(fds, nonblock)) return PyErr_SetFromErrno(PyExc_OSError);
|
|
return Py_BuildValue("ii", fds[0], fds[1]);
|
|
}
|
|
|
|
static PyObject*
|
|
cocoa_set_menubar_title(PyObject *self UNUSED, PyObject *args UNUSED) {
|
|
#ifdef __APPLE__
|
|
PyObject *title = NULL;
|
|
if (!PyArg_ParseTuple(args, "U", &title)) return NULL;
|
|
change_menubar_title(title);
|
|
#endif
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
static PyObject*
|
|
send_data_to_peer(PyObject *self UNUSED, PyObject *args) {
|
|
char * msg; Py_ssize_t sz;
|
|
unsigned long long peer_id;
|
|
if (!PyArg_ParseTuple(args, "Ks#", &peer_id, &msg, &sz)) return NULL;
|
|
send_response_to_peer(peer_id, msg, sz);
|
|
Py_RETURN_NONE;
|
|
}
|
|
|
|
static PyObject *
|
|
random_unix_socket(PyObject *self UNUSED, PyObject *args UNUSED) {
|
|
#ifndef SO_PASSCRED
|
|
errno = ENOTSUP;
|
|
return PyErr_SetFromErrno(PyExc_OSError);
|
|
#else
|
|
int fd, optval = 1;
|
|
struct sockaddr_un bind_addr = {.sun_family=AF_UNIX};
|
|
fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
if (fd < 0) return PyErr_SetFromErrno(PyExc_OSError);
|
|
if (setsockopt(fd, SOL_SOCKET, SO_PASSCRED, &optval, sizeof optval) != 0) goto fail;
|
|
if (bind(fd, (struct sockaddr *)&bind_addr, sizeof(sa_family_t)) != 0) goto fail;
|
|
return PyLong_FromLong((long)fd);
|
|
fail:
|
|
safe_close(fd, __FILE__, __LINE__);
|
|
return PyErr_SetFromErrno(PyExc_OSError);
|
|
#endif
|
|
}
|
|
|
|
static PyMethodDef module_methods[] = {
|
|
METHODB(safe_pipe, METH_VARARGS),
|
|
METHODB(random_unix_socket, METH_NOARGS),
|
|
{"add_timer", (PyCFunction)add_python_timer, METH_VARARGS, ""},
|
|
{"remove_timer", (PyCFunction)remove_python_timer, METH_VARARGS, ""},
|
|
METHODB(monitor_pid, METH_VARARGS),
|
|
METHODB(send_data_to_peer, METH_VARARGS),
|
|
METHODB(cocoa_set_menubar_title, METH_VARARGS),
|
|
METHODB(mask_kitty_signals_process_wide, METH_NOARGS),
|
|
{"sigqueue", (PyCFunction)sig_queue, METH_VARARGS, ""},
|
|
{NULL} /* Sentinel */
|
|
};
|
|
|
|
bool
|
|
init_child_monitor(PyObject *module) {
|
|
if (PyType_Ready(&ChildMonitor_Type) < 0) return false;
|
|
if (PyModule_AddObject(module, "ChildMonitor", (PyObject *)&ChildMonitor_Type) != 0) return false;
|
|
Py_INCREF(&ChildMonitor_Type);
|
|
if (PyModule_AddFunctions(module, module_methods) != 0) return false;
|
|
#ifdef NO_SIGQUEUE
|
|
PyModule_AddIntConstant(module, "has_sigqueue", 0);
|
|
#else
|
|
PyModule_AddIntConstant(module, "has_sigqueue", 1);
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
// }}}
|