From 7bd770968575baf58fe4d43e50c68fb25bf4c8fc Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 25 Apr 2025 14:39:36 +0530 Subject: [PATCH] Move parsing of macos-launch-services-cmdline into native code Avoids expensive re-exec and simplifies various things. Much faster for single instance usage. --- docs/faq.rst | 4 ++ kitty/data-types.c | 12 +++++ kitty/launcher/cmdline.c | 81 ++++++++++++++++++++++++++++++++ kitty/launcher/launcher.h | 12 ++++- kitty/launcher/main.c | 98 +++++++++------------------------------ kitty/launcher/shlex.h | 1 + kitty/launcher/utils.h | 37 +++++++++++++++ kitty/main.py | 27 ----------- kitty_tests/datatypes.py | 19 +++++++- setup.py | 2 +- 10 files changed, 186 insertions(+), 107 deletions(-) create mode 100644 kitty/launcher/cmdline.c diff --git a/docs/faq.rst b/docs/faq.rst index e58b5f5e5..60a77d7d0 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -239,6 +239,10 @@ workaround that limitation, |kitty| will read command line options from the file :file:`/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`. diff --git a/kitty/data-types.c b/kitty/data-types.c index 9172f319d..16cc46d20 100644 --- a/kitty/data-types.c +++ b/kitty/data-types.c @@ -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, ""}, diff --git a/kitty/launcher/cmdline.c b/kitty/launcher/cmdline.c new file mode 100644 index 000000000..c3ff1a17b --- /dev/null +++ b/kitty/launcher/cmdline.c @@ -0,0 +1,81 @@ +/* + * cmdline.c + * Copyright (C) 2025 Kovid Goyal + * + * 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; +} + diff --git a/kitty/launcher/launcher.h b/kitty/launcher/launcher.h index d4ece6199..a9cca92bc 100644 --- a/kitty/launcher/launcher.h +++ b/kitty/launcher/launcher.h @@ -8,6 +8,7 @@ #pragma once #include +#include 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); diff --git a/kitty/launcher/main.c b/kitty/launcher/main.c index e6377b5a7..ca47ab6e9 100644 --- a/kitty/launcher/main.c +++ b/kitty/launcher/main.c @@ -92,67 +92,13 @@ set_kitty_run_data(RunData *run_data, bool from_source, wchar_t *extensions_dir) #ifdef FOR_BUNDLE #include -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; diff --git a/kitty/launcher/shlex.h b/kitty/launcher/shlex.h index 51194bc14..c04ad4adb 100644 --- a/kitty/launcher/shlex.h +++ b/kitty/launcher/shlex.h @@ -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; } diff --git a/kitty/launcher/utils.h b/kitty/launcher/utils.h index 0764a53bd..13164df9f 100644 --- a/kitty/launcher/utils.h +++ b/kitty/launcher/utils.h @@ -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; +} diff --git a/kitty/main.py b/kitty/main.py index b8b6253df..0557e69c8 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -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()) diff --git a/kitty_tests/datatypes.py b/kitty_tests/datatypes.py index 9f4db3f26..bb5de3849 100644 --- a/kitty_tests/datatypes.py +++ b/kitty_tests/datatypes.py @@ -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'), diff --git a/setup.py b/setup.py index 257e3b55f..dafcdcce7 100755 --- a/setup.py +++ b/setup.py @@ -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]