Move parsing of macos-launch-services-cmdline into native code

Avoids expensive re-exec and simplifies various things. Much faster
for single instance usage.
This commit is contained in:
Kovid Goyal
2025-04-25 14:39:36 +05:30
parent 639ad3e8a6
commit 7bd7709685
10 changed files with 186 additions and 107 deletions

View File

@@ -239,6 +239,10 @@ workaround that limitation, |kitty| will read command line options from the file
:file:`<kitty config dir>/macos-launch-services-cmdline` when it is launched
from the GUI, i.e. by clicking the |kitty| application icon or using
``open -a kitty``. Note that this file is *only read* when running via the GUI.
The contents of the file are assumed to be the command line to pass to kitty in
shell syntax, for example::
--single-instance --override background=red
You can, of course, also run |kitty| from a terminal with command line options,
using: :file:`/Applications/kitty.app/Contents/MacOS/kitty`.

View File

@@ -652,6 +652,17 @@ abspath(PyObject *self UNUSED, PyObject *path) {
return PyUnicode_FromString(buf);
}
static PyObject*
read_file(PyObject *self UNUSED, PyObject *path) {
if (!PyUnicode_Check(path)) { PyErr_SetString(PyExc_TypeError, "path must a string"); return NULL; }
size_t sz;
char *result = read_full_file(PyUnicode_AsUTF8(path), &sz);
if (!result) { PyErr_SetFromErrno(PyExc_OSError); return NULL; }
PyObject *ans = PyBytes_FromStringAndSize(result, sz);
free(result);
return ans;
}
static PyObject*
py_makedirs(PyObject *self UNUSED, PyObject *args) {
int mode = 0755; const char *p;
@@ -670,6 +681,7 @@ py_get_config_dir(PyObject *self UNUSED, PyObject *args UNUSED) {
static PyMethodDef module_methods[] = {
METHODB(replace_c0_codes_except_nl_space_tab, METH_O),
METHODB(read_file, METH_O),
{"wcwidth", (PyCFunction)wcwidth_wrap, METH_O, ""},
{"expanduser", (PyCFunction)expanduser, METH_O, ""},
{"abspath", (PyCFunction)abspath, METH_O, ""},

81
kitty/launcher/cmdline.c Normal file
View File

@@ -0,0 +1,81 @@
/*
* cmdline.c
* Copyright (C) 2025 Kovid Goyal <kovid at kovidgoyal.net>
*
* Distributed under terms of the GPL3 license.
*/
#include "shlex.h"
#include "utils.h"
#include "launcher.h"
void
free_argv_array(argv_array *a) {
if (a && a->needs_free) {
free(a->buf); free(a->argv);
*a = (argv_array){0};
}
}
static bool
add_to_argv(argv_array *a, const char* arg, size_t sz) {
if (a->count + 2 > a->capacity) {
size_t cap = a->capacity * 2;
if (!cap) cap = 256;
void *m = realloc(a->argv, cap * sizeof(a->argv[0]));
if (!m) return false;
a->argv = m;
a->argv[a->count] = 0;
a->capacity = cap;
a->needs_free = true;
}
memcpy(a->buf + a->pos, arg, sz);
a->argv[a->count++] = a->buf + a->pos;
a->argv[a->count] = 0;
a->pos += sz;
a->buf[a->pos++] = 0;
return true;
}
bool
get_argv_from(const char *filename, const char *argv0, argv_array *final_ans) {
(void)get_config_dir;
if (!filename || !filename[0]) return true;
size_t src_sz;
char* src = read_full_file(filename, &src_sz);
if (!src) {
fprintf(stderr, "Failed to read from %s ", filename); perror("with error");
return false;
}
ShlexState s = {0};
argv_array ans = {0};
bool ok = false;
ans.buf = malloc(src_sz + strlen(argv0) + 64);
if (!ans.buf) { errno = ENOMEM; goto end; }
ans.needs_free = true;
if (!add_to_argv(&ans, argv0, strlen(argv0))) goto end;
if (!alloc_shlex_state(&s, src, src_sz, false)) { errno = ENOMEM; goto end; }
bool keep_going = true;
while (keep_going) {
ssize_t q = next_word(&s);
switch(q) {
case -1: fprintf(stderr, "Failed to parse %s with error: %s\n", filename, s.err); goto end;
case -2: keep_going = false; break;
default:
if (ans.count == 1 && strcmp(s.buf, "kitty") == 0) continue;
if (!add_to_argv(&ans, s.buf, q)) { goto end; }
break;
}
}
ok = true;
end:
free(src); dealloc_shlex_state(&s);
if (ok) *final_ans = ans;
else {
free_argv_array(&ans);
fprintf(stderr, "Failed to read from %s ", filename); perror("with error");
}
return ok;
}

View File

@@ -8,6 +8,7 @@
#pragma once
#include <stdbool.h>
#include <stddef.h>
typedef struct CLIOptions {
const char *session, *instance_group, *detached_log;
@@ -16,5 +17,12 @@ typedef struct CLIOptions {
} CLIOptions;
void
single_instance_main(int argc, char *argv[], const CLIOptions *opts);
typedef struct argv_array {
char **argv, *buf; size_t capacity, count, pos;
bool needs_free;
} argv_array;
void single_instance_main(int argc, char *argv[], const CLIOptions *opts);
bool get_argv_from(const char *filename, const char* argv0, argv_array *ans);
void free_argv_array(argv_array *a);

View File

@@ -92,67 +92,13 @@ set_kitty_run_data(RunData *run_data, bool from_source, wchar_t *extensions_dir)
#ifdef FOR_BUNDLE
#include <bypy-freeze.h>
static bool
canonicalize_path(const char *srcpath, char *dstpath, size_t sz) {
// remove . and .. path segments
bool ok = false;
size_t plen = strlen(srcpath) + 1, chk;
RAII_ALLOC(char, wtmp, malloc(plen));
RAII_ALLOC(char*, tokv, malloc(sizeof(char*) * plen));
if (!wtmp || !tokv) goto end;
char *s, *tok, *sav;
bool relpath = *srcpath != '/';
// use a buffer as strtok modifies its input
memcpy(wtmp, srcpath, plen);
tok = strtok_r(wtmp, "/", &sav);
int ti = 0;
while (tok != NULL) {
if (strcmp(tok, "..") == 0) {
if (ti > 0) ti--;
} else if (strcmp(tok, ".") != 0) {
tokv[ti++] = tok;
}
tok = strtok_r(NULL, "/", &sav);
}
chk = 0;
s = dstpath;
for (int i = 0; i < ti; i++) {
size_t token_sz = strlen(tokv[i]);
if (i > 0 || !relpath) {
if (++chk >= sz) goto end;
*s++ = '/';
}
chk += token_sz;
if (chk >= sz) goto end;
memcpy(s, tokv[i], token_sz);
s += token_sz;
}
if (s == dstpath) {
if (++chk >= sz) goto end;
*s++ = relpath ? '.' : '/';
}
*s = '\0';
ok = true;
end:
return ok;
}
static bool
static void
canonicalize_path_wide(const char *srcpath, wchar_t *dest, size_t sz) {
char buf[sz + 1];
bool ret = canonicalize_path(srcpath, buf, sz);
lexical_absolute_path(srcpath, buf, sz);
buf[sz] = 0;
mbstowcs(dest, buf, sz - 1);
dest[sz-1] = 0;
return ret;
}
static int
@@ -164,18 +110,12 @@ run_embedded(RunData *run_data) {
#else
const char *python_relpath = "../" KITTY_LIB_DIR_NAME;
#endif
int num = safe_snprintf(extensions_dir_full, PATH_MAX, "%s/%s/kitty-extensions", run_data->exe_dir, python_relpath);
if (num < 0 || num >= PATH_MAX) { fprintf(stderr, "Failed to create path to extensions_dir: %s/%s\n", run_data->exe_dir, python_relpath); return 1; }
wchar_t extensions_dir[num+2];
if (!canonicalize_path_wide(extensions_dir_full, extensions_dir, num+1)) {
fprintf(stderr, "Failed to canonicalize the path: %s\n", extensions_dir_full); return 1; }
num = snprintf(python_home_full, PATH_MAX, "%s/%s/python%s", run_data->exe_dir, python_relpath, PYVER);
if (num < 0 || num >= PATH_MAX) { fprintf(stderr, "Failed to create path to python home: %s/%s\n", run_data->exe_dir, python_relpath); return 1; }
wchar_t python_home[num+2];
if (!canonicalize_path_wide(python_home_full, python_home, num+1)) {
fprintf(stderr, "Failed to canonicalize the path: %s\n", python_home_full); return 1; }
safe_snprintf(extensions_dir_full, PATH_MAX, "%s/%s/kitty-extensions", run_data->exe_dir, python_relpath);
wchar_t extensions_dir[PATH_MAX];
canonicalize_path_wide(extensions_dir_full, extensions_dir, PATH_MAX);
safe_snprintf(python_home_full, PATH_MAX, "%s/%s/python%s", run_data->exe_dir, python_relpath, PYVER);
wchar_t python_home[PATH_MAX];
canonicalize_path_wide(python_home_full, python_home, PATH_MAX);
bypy_initialize_interpreter(
L"kitty", python_home, L"kitty_main", extensions_dir, run_data->argc, run_data->argv);
if (!set_kitty_run_data(run_data, false, extensions_dir)) return 1;
@@ -498,29 +438,36 @@ delegate_to_kitten_if_possible(int argc, char *argv[], char* exe_dir) {
return false;
}
int main(int argc, char *argv[], char* envp[]) {
if (argc < 1 || !argv) { fprintf(stderr, "Invalid argc/argv\n"); return 1; }
int main(int argc_, char *argv_[], char* envp[]) {
if (argc_ < 1 || !argv_) { fprintf(stderr, "Invalid argc/argv\n"); return 1; }
if (!ensure_working_stdio()) return 1;
char exe[PATH_MAX+1] = {0};
char exe_dir_buf[PATH_MAX+1] = {0};
RAII_ALLOC(const char, lc_ctype, NULL);
bool launched_by_launch_services = false;
const char *config_dir = NULL;
argv_array argva = {.argv = argv_, .count = argc_};
#ifdef __APPLE__
lc_ctype = getenv("LC_CTYPE");
if (lc_ctype) lc_ctype = strdup(lc_ctype);
char abuf[PATH_MAX+1];
if (getenv("KITTY_LAUNCHED_BY_LAUNCH_SERVICES")) {
launched_by_launch_services = true;
unsetenv("KITTY_LAUNCHED_BY_LAUNCH_SERVICES");
char buf[PATH_MAX+1];
if (!get_config_dir(buf,sizeof(buf))) buf[0] = 0;
config_dir = buf;
if (!get_config_dir(abuf, sizeof(abuf))) abuf[0] = 0;
config_dir = abuf;
if (launched_by_launch_services && config_dir[0]) {
char cbuf[PATH_MAX];
safe_snprintf(cbuf, sizeof(cbuf), "%s/macos-launch-services-cmdline", config_dir);
if (!get_argv_from(cbuf, argva.argv[0], &argva)) exit(1);
}
}
#endif
(void)read_full_file;
if (!read_exe_path(exe, sizeof(exe))) return 1;
strncpy(exe_dir_buf, exe, sizeof(exe_dir_buf));
char *exe_dir = dirname(exe_dir_buf);
if (!delegate_to_kitten_if_possible(argc, argv, exe_dir)) handle_fast_commandline(argc, argv, NULL);
if (!delegate_to_kitten_if_possible(argva.count, argva.argv, exe_dir)) handle_fast_commandline(argva.count, argva.argv, NULL);
int ret=0;
char lib[PATH_MAX+1] = {0};
if (KITTY_LIB_PATH[0] == '/') {
@@ -529,10 +476,11 @@ int main(int argc, char *argv[], char* envp[]) {
safe_snprintf(lib, PATH_MAX, "%s/%s", exe_dir, KITTY_LIB_PATH);
}
RunData run_data = {
.exe = exe, .exe_dir = exe_dir, .lib_dir = lib, .argc = argc, .argv = argv, .lc_ctype = lc_ctype,
.exe = exe, .exe_dir = exe_dir, .lib_dir = lib, .argc = argva.count, .argv = argva.argv, .lc_ctype = lc_ctype,
.launched_by_launch_services=launched_by_launch_services, .config_dir = config_dir,
};
ret = run_embedded(&run_data);
free_argv_array(&argva);
single_instance_main(-1, NULL, NULL);
Py_FinalizeEx();
return ret;

View File

@@ -86,6 +86,7 @@ write_unich(ShlexState *self, unsigned long ch) {
static size_t
get_word(ShlexState *self) {
size_t ans = self->buf_pos; self->buf_pos = 0;
self->buf[ans] = 0;
return ans;
}

View File

@@ -195,3 +195,40 @@ get_config_dir(char *output, size_t outputsz) {
#undef expand
#undef check_and_ret
}
static ssize_t
safe_read_stream(void* ptr, size_t size, FILE* stream) {
errno = 0;
ssize_t total = 0, bytes_to_read = size;
while (total < bytes_to_read) {
size_t n = fread((char*)ptr + total, 1, bytes_to_read - total, stream);
if (n > 0) total += n;
else {
if (!ferror(stream)) break; // eof
if (errno != EINTR) return -1;
clearerr(stream);
}
}
return total;
}
static char*
read_full_file(const char* filename, size_t *sz) {
FILE* file = fopen(filename, "rb");
if (!file) return NULL;
fseek(file, 0, SEEK_END);
unsigned long file_size = ftell(file);
rewind(file);
char* buffer = (char*)malloc(file_size + 1); // +1 for the null terminator
if (!buffer) {
errno = ENOMEM;
fclose(file);
return NULL;
}
ssize_t q = safe_read_stream(buffer, file_size, file);
fclose(file);
if (q < 0) { free(buffer); buffer = NULL; if (sz) *sz = 0; }
else { if (sz) { *sz = q; } buffer[q] = 0; }
return buffer;
}

View File

@@ -20,7 +20,6 @@ from .constants import (
appname,
beam_cursor_data_file,
clear_handled_signals,
config_dir,
glfw_path,
is_macos,
is_wayland,
@@ -63,7 +62,6 @@ from .utils import (
log_error,
parse_os_window_state,
safe_mtime,
shlex_split,
startup_notification_handler,
)
@@ -337,30 +335,6 @@ def setup_profiling() -> Generator[None, None, None]:
print('To view the graphical call data, use: kcachegrind', cg)
def macos_cmdline(argv_args: list[str]) -> list[str]:
try:
with open(os.path.join(config_dir, 'macos-launch-services-cmdline')) as f:
raw = f.read()
except FileNotFoundError:
return argv_args
raw = raw.strip()
ans = list(shlex_split(raw))
if ans and ans[0] == 'kitty':
del ans[0]
if '-1' in ans or '--single-instance' in ans:
if 'KITTY_SI_DATA' in os.environ:
# C code will already have setup single instance
log_error(
'--single-instance supplied in both command line arguments and macos-launch-services-cmdline,'
' ignoring any --instance-group in macos-launch-services-cmdline')
else:
# Re-exec with new argv so that the C code that handles single instance
# can pick up the modified argv
os.environ['KITTY_LAUNCHED_BY_LAUNCH_SERVICES'] = '2' # so that use_os_log is set in the re-execed process
os.execl(kitty_exe(), 'kitty', *(ans + argv_args))
return ans + argv_args
def expand_listen_on(listen_on: str, from_config_file: bool) -> str:
if from_config_file and listen_on == 'none':
return ''
@@ -483,7 +457,6 @@ def _main() -> None:
args = sys.argv[1:]
if is_macos and launched_by_launch_services:
os.chdir(os.path.expanduser('~'))
args = macos_cmdline(args)
set_use_os_log(True)
try:
cwd_ok = os.path.isdir(os.getcwd())

View File

@@ -4,10 +4,11 @@
import json
import os
import shutil
import subprocess
import sys
import tempfile
from kitty.constants import read_kitty_resource
from kitty.constants import is_macos, kitty_exe, read_kitty_resource
from kitty.fast_data_types import (
Color,
HistoryBuf,
@@ -19,6 +20,7 @@ from kitty.fast_data_types import (
get_config_dir,
makedirs,
parse_input_from_terminal,
read_file,
replace_c0_codes_except_nl_space_tab,
split_into_graphemes,
strip_csi,
@@ -492,9 +494,22 @@ class TestDataTypes(BaseTest):
if os.path.exists(dot_config):
shutil.rmtree(dot_config)
with tempfile.TemporaryDirectory() as tdir:
with open(tdir + '/macos-launch-services-cmdline', 'w') as f:
print('kitty +runpy "import sys; print(sys.argv[-1])"', file=f)
print('next-line', file=f)
print()
if is_macos:
env = os.environ.copy()
env['KITTY_CONFIG_DIRECTORY'] = tdir
env['KITTY_LAUNCHED_BY_LAUNCH_SERVICES'] = '1'
actual = subprocess.check_output([kitty_exe(), '+runpy', 'import json, sys; print(json.dumps(sys.argv))'], env=env).strip().decode()
self.ae('next-line', actual)
os.makedirs(tdir + '/good/kitty')
open(tdir + '/good/kitty/kitty.conf', 'w').close()
open(tdir + '/f', 'w').close()
data = os.urandom(32879)
with open(tdir + '/f', 'wb') as f:
f.write(data)
self.ae(data, read_file(f.name))
for x in (
(f'KITTY_CONFIG_DIRECTORY={tdir}', f'{tdir}'),
(f'XDG_CONFIG_HOME={tdir}/good', f'{tdir}/good/kitty'),

View File

@@ -1345,7 +1345,7 @@ def build_launcher(args: Options, launcher_dir: str = '.', bundle_type: str = 's
objects = []
cppflags.append('-DKITTY_CLI_BOOL_OPTIONS=" ' + ' '.join(kitty_cli_boolean_options()) + ' "')
cppflags.append('-DKITTY_VERSION="' + '.'.join(map(str, version)) + '"')
for src in ('kitty/launcher/main.c', 'kitty/launcher/single-instance.c'):
for src in ('kitty/launcher/main.c', 'kitty/launcher/single-instance.c', 'kitty/launcher/cmdline.c'):
obj = os.path.join(build_dir, src.replace('/', '-').replace('.c', '.o'))
objects.append(obj)
cmd = env.cc + cppflags + cflags + ['-c', src, '-o', obj]