mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-08 14:18:26 +02:00
No longer pay the overhead of starting the Python interpreter just to write a message to the single instance socket. This reduces the time for kitty --single-instance (for second and later instances) from 70ms to 3ms an almost 25x improvement. Needs testing on macOS and also porting of the +open handling.
291 lines
10 KiB
C
291 lines
10 KiB
C
/*
|
|
* single-instance.c
|
|
* Copyright (C) 2024 Kovid Goyal <kovid at kovidgoyal.net>
|
|
*
|
|
* Distributed under terms of the GPL3 license.
|
|
*/
|
|
|
|
#ifdef __APPLE__
|
|
// Needed for _CS_DARWIN_USER_CACHE_DIR
|
|
#define _DARWIN_C_SOURCE
|
|
#include <unistd.h>
|
|
#undef _DARWIN_C_SOURCE
|
|
#else
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#include "launcher.h"
|
|
#include "../safe-wrappers.h"
|
|
#include <stdbool.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/un.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <errno.h>
|
|
#include <pwd.h>
|
|
#include <sys/types.h>
|
|
|
|
#define CHARSETS_STORAGE static inline
|
|
#define NO_SINGLE_BYTE_CHARSETS
|
|
#include "../charsets.c"
|
|
|
|
#define fail_on_errno(msg) { perror(msg); exit(1); }
|
|
|
|
void
|
|
log_error(const char *fmt, ...) {
|
|
va_list ar;
|
|
va_start(ar, fmt);
|
|
vfprintf(stderr, fmt, ar);
|
|
va_end(ar);
|
|
}
|
|
|
|
static bool
|
|
is_ok_tmpdir(const char *x) {
|
|
if (!x || !x[0]) return false;
|
|
char path[2048];
|
|
snprintf(path, sizeof(path), "%s/kitty-si-test-tmpdir-XXXXXXXXXXXX", x);
|
|
int fd = safe_mkstemp(path);
|
|
if (fd > -1) {
|
|
safe_close(fd, __FILE__, __LINE__);
|
|
unlink(path);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void
|
|
get_socket_dir(char *output, size_t output_capacity) {
|
|
#define ret_if_ok(x) if (is_ok_tmpdir(x)) { if (snprintf(output, output_capacity, "%s", x) < output_capacity-1); return; }
|
|
#ifdef __APPLE__
|
|
if (confstr(_CS_DARWIN_USER_CACHE_DIR, output, output_capacity)) return output;
|
|
snprintf(output, output_capacity, "%s", "/Library/Caches");
|
|
#else
|
|
#define test_env(x) { const char *e = getenv(#x); ret_if_ok(e); }
|
|
test_env(XDG_RUNTIME_DIR); test_env(TMPDIR); test_env(TEMP); test_env(TMP);
|
|
|
|
ret_if_ok("/tmp"); ret_if_ok("/var/tmp"); ret_if_ok("/usr/tmp");
|
|
|
|
test_env(HOME);
|
|
|
|
const char *home = getpwuid(geteuid())->pw_dir;
|
|
ret_if_ok(home);
|
|
|
|
if (getcwd(output, output_capacity)) return;
|
|
snprintf(output, output_capacity, "%s", ".");
|
|
#undef test_env
|
|
#endif
|
|
}
|
|
|
|
static void
|
|
set_single_instance_socket(int fd, const char *socket_path) {
|
|
if (listen(fd, 5) != 0) fail_on_errno("Failed to listen on single instance socket");
|
|
char buf[256];
|
|
if (socket_path && socket_path[0]) snprintf(buf, sizeof(buf), "%d:%s", fd, socket_path);
|
|
else snprintf(buf, sizeof(buf), "%d", fd);
|
|
setenv("KITTY_SI_DATA", buf, 1);
|
|
}
|
|
|
|
typedef struct membuf {
|
|
char *data;
|
|
size_t used, capacity;
|
|
} membuf;
|
|
|
|
static void
|
|
write_to_membuf(membuf *m, void *data, size_t sz) {
|
|
ensure_space_for(m, data, char, m->used + sz, capacity, 8192, false);
|
|
memcpy(m->data + m->used, data, sz); m->used += sz;
|
|
}
|
|
|
|
static void
|
|
write_escaped_char(membuf *m, char ch) {
|
|
char buf[8];
|
|
int n = snprintf(buf, sizeof(buf), "\\u%04x", ch);
|
|
write_to_membuf(m, buf, n);
|
|
}
|
|
|
|
static void
|
|
write_json_string(membuf *m, const char *src, size_t src_len) {
|
|
ensure_space_for(m, data, char, m->used + 2 + 8 * src_len, capacity, 8192, false);
|
|
m->data[m->used++] = '"';
|
|
uint32_t codep = 0;
|
|
UTF8State state = 0, prev = UTF8_ACCEPT;
|
|
for (size_t i = 0; i < src_len; i++) {
|
|
switch(decode_utf8(&state, &codep, src[i])) {
|
|
case UTF8_ACCEPT:
|
|
switch(codep) {
|
|
case '"': write_to_membuf(m, "\\\"", 2); break;
|
|
case '\\': write_to_membuf(m, "\\\\", 2); break;
|
|
case '\t': write_to_membuf(m, "\\t", 2); break;
|
|
case '\n': write_to_membuf(m, "\\n", 2); break;
|
|
case '\r': write_to_membuf(m, "\\r", 2); break;
|
|
START_ALLOW_CASE_RANGE
|
|
case 0 ... 8: case 11: case 12: case 14 ... 31:
|
|
write_escaped_char(m, codep); break;
|
|
END_ALLOW_CASE_RANGE
|
|
default: m->used += encode_utf8(codep, m->data + m->used);
|
|
}
|
|
break;
|
|
case UTF8_REJECT:
|
|
state = UTF8_ACCEPT;
|
|
if (prev != UTF8_ACCEPT && i > 0) i--;
|
|
break;
|
|
}
|
|
prev = state;
|
|
}
|
|
m->data[m->used++] = '"';
|
|
}
|
|
|
|
static void
|
|
write_json_string_array(membuf *m, int argc, char *argv[]) {
|
|
write_to_membuf(m, "[", 1);
|
|
for (int i = 0; i < argc; i++) {
|
|
if (i) write_to_membuf(m, ",", 1);
|
|
write_json_string(m, argv[i], strlen(argv[i]));
|
|
}
|
|
write_to_membuf(m, "]", 1);
|
|
}
|
|
|
|
static void
|
|
read_till_eof(FILE *f, membuf *m) {
|
|
while (!feof(f)) {
|
|
ensure_space_for(m, data, char, m->used + 8192, capacity, 4*8192, false);
|
|
m->used += fread(m->data, 1, m->capacity - m->used, f);
|
|
if (ferror(f)) { fclose(f); fail_on_errno("Failed to read from session file"); }
|
|
}
|
|
write_to_membuf(m, "\0", 1);
|
|
fclose(f);
|
|
}
|
|
|
|
static bool
|
|
bind_unix_socket(int s, const char *basename, struct sockaddr_un *addr) {
|
|
addr->sun_family = AF_UNIX;
|
|
const size_t blen = strlen(basename);
|
|
// First try abstract socket
|
|
addr->sun_path[0] = 0;
|
|
memcpy(addr->sun_path + 1, basename, blen + 1);
|
|
if (safe_bind(s, (struct sockaddr*)addr, sizeof(sa_family_t) + 1 + blen) > -1) return true;
|
|
if (errno != ENOENT) return false;
|
|
// Try an actual filesystem file
|
|
get_socket_dir(addr->sun_path, sizeof(addr->sun_path) - blen - 2);
|
|
const size_t dlen = strlen(addr->sun_path);
|
|
if (snprintf(addr->sun_path + dlen, sizeof(addr->sun_path) - dlen, "/%s", basename) < blen + 1) {
|
|
fprintf(stderr, "Socket directory has path too long for single instance socket file %s\n", addr->sun_path);
|
|
exit(1);
|
|
}
|
|
if (safe_bind(s, (struct sockaddr*)addr, sizeof(*addr)) > -1) return true;
|
|
return false;
|
|
}
|
|
|
|
static int
|
|
create_unix_socket(void) {
|
|
int s = socket(AF_UNIX, SOCK_STREAM, 0);
|
|
if (s < 0) fail_on_errno("Failed to create single instance socket object");
|
|
int flags;
|
|
if ((flags = fcntl(s, F_GETFD)) == -1) fail_on_errno("Failed to get fcntl flags for single instance socket");
|
|
if (fcntl(s, F_SETFD, flags | FD_CLOEXEC) == -1) fail_on_errno("Failed to set single instance socket to CLOEXEC");
|
|
return s;
|
|
}
|
|
|
|
extern char **environ;
|
|
|
|
static void
|
|
talk_to_instance(int s, struct sockaddr_un *server_addr, int argc, char *argv[], const CLIOptions *opts) {
|
|
membuf session_data = {0};
|
|
if (opts->session && opts->session[0]) {
|
|
if (strcmp(opts->session, "none") == 0) {
|
|
session_data.data = "none"; session_data.used = 4;
|
|
} else if (strcmp(opts->session, "-")) {
|
|
read_till_eof(stdin, &session_data);
|
|
} else {
|
|
FILE *f = safe_fopen(opts->session, "r");
|
|
if (f == NULL) fail_on_errno("Failed to open session file for reading");
|
|
read_till_eof(f, &session_data);
|
|
}
|
|
}
|
|
membuf output = {0};
|
|
#define w(literal) write_to_membuf(&output, literal, sizeof(literal)-1)
|
|
w("{\"cmd\":\"new_instance\",\"session_data\":");
|
|
if (session_data.used) write_json_string(&output, session_data.data, session_data.used);
|
|
else write_json_string(&output, "", 0);
|
|
w(",\"args\":"); write_json_string_array(&output, argc, argv);
|
|
char cwd[4096];
|
|
if (!getcwd(cwd, sizeof(cwd))) fail_on_errno("Failed to get cwd");
|
|
w(",\"cwd\":"); write_json_string(&output, cwd, strlen(cwd));
|
|
w(",\"environ\":{");
|
|
char **e = environ;
|
|
for (; *e; e++) {
|
|
const char *eq = strchr(*e, '=');
|
|
if (eq) {
|
|
if (e != environ) write_to_membuf(&output, ",", 1);
|
|
write_json_string(&output, *e, eq - *e);
|
|
w(":");
|
|
write_json_string(&output, eq + 1, strlen(eq + 1));
|
|
}
|
|
}
|
|
w("}");
|
|
|
|
w(",\"cmdline_args_for_open\":[]");
|
|
|
|
w(",\"notify_on_os_window_death\":");
|
|
int notify_socket = -1;
|
|
if (opts->wait_for_single_instance_window_close) {
|
|
notify_socket = create_unix_socket();
|
|
struct sockaddr_un server_addr;
|
|
char addr[128];
|
|
snprintf(addr, sizeof(addr), "kitty-os-window-close-notify-%d-%d", getpid(), geteuid());
|
|
if (!bind_unix_socket(notify_socket, addr, &server_addr)) fail_on_errno("Failed to bind notification socket");
|
|
size_t len = strlen(server_addr.sun_path);
|
|
if (len == 0) len = 1 + strlen(server_addr.sun_path +1);
|
|
if (listen(notify_socket, 5) != 0) fail_on_errno("Failed to listen on notify socket");
|
|
write_json_string(&output, server_addr.sun_path, len);
|
|
} else w("null");
|
|
|
|
w("}");
|
|
#undef w
|
|
size_t addr_len = strlen(server_addr->sun_path);
|
|
if (!addr_len) addr_len = strlen(server_addr->sun_path + 1) + 1;
|
|
if (safe_connect(s, (struct sockaddr*)server_addr, sizeof(sa_family_t) + addr_len) != 0) {
|
|
fail_on_errno("Failed to connect to single instance socket");
|
|
}
|
|
size_t pos = 0;
|
|
while (pos < output.used) {
|
|
errno = 0;
|
|
ssize_t nbytes = write(s, output.data + pos, output.used - pos);
|
|
if (nbytes <= 0) {
|
|
if (errno == EAGAIN || errno == EINTR || errno == EWOULDBLOCK) continue;
|
|
break;
|
|
}
|
|
pos += nbytes;
|
|
}
|
|
if (pos < output.used) fail_on_errno("Failed to write message to single instance socket");
|
|
shutdown(s, SHUT_RDWR);
|
|
safe_close(s, __FILE__, __LINE__);
|
|
if (notify_socket > -1) {
|
|
int fd = safe_accept(notify_socket, NULL, NULL);
|
|
if (fd < 0) fail_on_errno("Failed to accept connection on notify socket");
|
|
char rbuf;
|
|
while (true) {
|
|
ssize_t n = recv(notify_socket, &rbuf, 1, 0);
|
|
if (n < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) continue;
|
|
break;
|
|
}
|
|
shutdown(notify_socket, SHUT_RDWR);
|
|
safe_close(notify_socket, __FILE__, __LINE__);
|
|
}
|
|
}
|
|
|
|
void
|
|
single_instance_main(int argc, char *argv[], const CLIOptions *opts) {
|
|
struct sockaddr_un server_addr;
|
|
char addr_buf[sizeof(server_addr.sun_path)-1];
|
|
if (opts->instance_group) snprintf(addr_buf, sizeof(addr_buf), "kitty-ipc-%d-%s", geteuid(), opts->instance_group);
|
|
else snprintf(addr_buf, sizeof(addr_buf), "kitty-ipc-%d", geteuid());
|
|
|
|
int s = create_unix_socket();
|
|
if (!bind_unix_socket(s, addr_buf, &server_addr)) {
|
|
if (errno == EADDRINUSE) { talk_to_instance(s, &server_addr, argc, argv, opts); exit(0); }
|
|
else fail_on_errno("Failed to bind single instance socket");
|
|
} else set_single_instance_socket(s, server_addr.sun_path);
|
|
}
|
|
|