From 0dd3334811c822e0caf40da9d18cbb4adc188e02 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 24 Jul 2018 11:07:53 +0530 Subject: [PATCH] Implement getting cmdline of process on macOS as well --- kitty/child.py | 28 +++---- kitty/cocoa_window.m | 12 --- kitty/data-types.c | 2 + kitty/macos_process_info.c | 147 +++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 5 files changed, 165 insertions(+), 26 deletions(-) create mode 100644 kitty/macos_process_info.c diff --git a/kitty/child.py b/kitty/child.py index e9e62cbfe..5d019c75f 100644 --- a/kitty/child.py +++ b/kitty/child.py @@ -9,21 +9,23 @@ import kitty.fast_data_types as fast_data_types from .constants import is_macos, shell_path, terminfo_dir +if is_macos: + from kitty.fast_data_types import cmdline_of_process as _cmdl, cwd_of_process as _cwd -def cwd_of_process(pid): - if is_macos: - from kitty.fast_data_types import cwd_of_process - ans = cwd_of_process(pid) - else: + def cmdline_of_process(pid): + return _cmdl(pid) + + def cwd_of_process(pid): + return os.path.realpath(_cwd(pid)) + +else: + + def cmdline_of_process(pid): + return list(filter(None, open('/proc/{}/cmdline'.format(pid), 'rb').read().decode('utf-8').split('\0'))) + + def cwd_of_process(pid): ans = '/proc/{}/cwd'.format(pid) - return os.path.realpath(ans) - - -def cmdline_of_process(pid): - if is_macos: - # TODO: macOS implementation, see DarwinProcess.c in htop for inspiration - raise NotImplementedError() - return list(filter(None, open('/proc/{}/cmdline'.format(pid), 'rb').read().decode('utf-8').split('\0'))) + return os.path.realpath(ans) def remove_cloexec(fd): diff --git a/kitty/cocoa_window.m b/kitty/cocoa_window.m index f420db8fc..2bae366f7 100644 --- a/kitty/cocoa_window.m +++ b/kitty/cocoa_window.m @@ -12,8 +12,6 @@ #include // Needed for _NSGetProgname #include -typedef void* rusage_info_t; // needed for libproc.h -#include #if (MAC_OS_X_VERSION_MAX_ALLOWED < 101200) #define NSWindowStyleMaskResizable NSResizableWindowMask @@ -234,15 +232,6 @@ cocoa_get_lang(PyObject UNUSED *self) { return Py_BuildValue("s", [locale UTF8String]); } -static PyObject* -cwd_of_process(PyObject *self UNUSED, PyObject *pid_) { - long pid = PyLong_AsLong(pid_); - struct proc_vnodepathinfo vpi; - int ret = proc_pidinfo(pid, PROC_PIDVNODEPATHINFO, 0, &vpi, sizeof(vpi)); - if (ret < 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; } - return PyUnicode_FromString(vpi.pvi_cdir.vip_path); -} - void cocoa_set_hide_from_tasks(void) { [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]; @@ -276,7 +265,6 @@ cocoa_set_titlebar_color(void *w, color_type titlebar_color) static PyMethodDef module_methods[] = { {"cocoa_get_lang", (PyCFunction)cocoa_get_lang, METH_NOARGS, ""}, - {"cwd_of_process", (PyCFunction)cwd_of_process, METH_O, ""}, {"cocoa_set_new_window_trigger", (PyCFunction)cocoa_set_new_window_trigger, METH_VARARGS, ""}, {NULL, NULL, 0, NULL} /* Sentinel */ }; diff --git a/kitty/data-types.c b/kitty/data-types.c index d33e454e7..9f24ad036 100644 --- a/kitty/data-types.c +++ b/kitty/data-types.c @@ -215,6 +215,7 @@ extern bool init_png_reader(PyObject *module); #ifdef __APPLE__ extern int init_CoreText(PyObject *); extern bool init_cocoa(PyObject *module); +extern bool init_macos_process_info(PyObject *module); #else extern bool init_freetype_library(PyObject*); #endif @@ -249,6 +250,7 @@ PyInit_fast_data_types(void) { if (!init_kittens(m)) return NULL; if (!init_png_reader(m)) return NULL; #ifdef __APPLE__ + if (!init_macos_process_info(m)) return NULL; if (!init_CoreText(m)) return NULL; if (!init_cocoa(m)) return NULL; #else diff --git a/kitty/macos_process_info.c b/kitty/macos_process_info.c new file mode 100644 index 000000000..8f917104f --- /dev/null +++ b/kitty/macos_process_info.c @@ -0,0 +1,147 @@ +/* + * macos_process_info.c + * Copyright (C) 2018 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#include "data-types.h" + +#include +typedef void* rusage_info_t; // needed for libproc.h +#include + +static PyObject* +cwd_of_process(PyObject *self UNUSED, PyObject *pid_) { + if (!PyLong_Check(pid_)) { PyErr_SetString(PyExc_TypeError, "pid must be an int"); return NULL; } + long pid = PyLong_AsLong(pid_); + if (pid < 0) { PyErr_SetString(PyExc_TypeError, "pid cannot be negative"); return NULL; } + struct proc_vnodepathinfo vpi; + int ret = proc_pidinfo(pid, PROC_PIDVNODEPATHINFO, 0, &vpi, sizeof(vpi)); + if (ret < 0) { PyErr_SetFromErrno(PyExc_OSError); return NULL; } + return PyUnicode_FromString(vpi.pvi_cdir.vip_path); +} + +// Read the maximum argument size for processes +static inline int +get_argmax() { + int argmax; + int mib[] = { CTL_KERN, KERN_ARGMAX }; + size_t size = sizeof(argmax); + + if (sysctl(mib, 2, &argmax, &size, NULL, 0) == 0) + return argmax; + return 0; +} + + +static PyObject* +cmdline_of_process(PyObject *self UNUSED, PyObject *pid_) { + // Taken from psutil, with thanks (BSD 3-clause license) + int mib[3]; + int nargs; + size_t len; + char *procargs = NULL; + char *arg_ptr; + char *arg_end; + char *curr_arg; + size_t argmax; + + PyObject *py_arg = NULL; + PyObject *py_retlist = NULL; + if (!PyLong_Check(pid_)) { PyErr_SetString(PyExc_TypeError, "pid must be an int"); goto error; } + long pid = PyLong_AsLong(pid_); + if (pid < 0) { PyErr_SetString(PyExc_TypeError, "pid cannot be negative"); goto error; } + + // special case for PID 0 (kernel_task) where cmdline cannot be fetched + if (pid == 0) + return Py_BuildValue("[]"); + + // read argmax and allocate memory for argument space. + argmax = get_argmax(); + if (!argmax) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + procargs = (char *)malloc(argmax); + if (NULL == procargs) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + // read argument space + mib[0] = CTL_KERN; + mib[1] = KERN_PROCARGS2; + mib[2] = (pid_t)pid; + if (sysctl(mib, 3, procargs, &argmax, NULL, 0) < 0) { + // In case of zombie process or non-existant process we'll get EINVAL. + if (errno == EINVAL) + PyErr_Format(PyExc_ValueError, "process with pid %ld either does not exist or is a zombie", pid); + else + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + arg_end = &procargs[argmax]; + // copy the number of arguments to nargs + memcpy(&nargs, procargs, sizeof(nargs)); + + arg_ptr = procargs + sizeof(nargs); + len = strlen(arg_ptr); + arg_ptr += len + 1; + + if (arg_ptr == arg_end) { + free(procargs); + return Py_BuildValue("[]"); + } + + // skip ahead to the first argument + for (; arg_ptr < arg_end; arg_ptr++) { + if (*arg_ptr != '\0') + break; + } + + // iterate through arguments + curr_arg = arg_ptr; + py_retlist = Py_BuildValue("[]"); + if (!py_retlist) + goto error; + while (arg_ptr < arg_end && nargs > 0) { + if (*arg_ptr++ == '\0') { + py_arg = PyUnicode_DecodeFSDefault(curr_arg); + if (! py_arg) + goto error; + if (PyList_Append(py_retlist, py_arg)) + goto error; + Py_DECREF(py_arg); + // iterate to next arg and decrement # of args + curr_arg = arg_ptr; + nargs--; + } + } + + free(procargs); + return py_retlist; + +error: + Py_XDECREF(py_arg); + Py_XDECREF(py_retlist); + if (procargs != NULL) + free(procargs); + return NULL; + +} + +static PyMethodDef module_methods[] = { + {"cwd_of_process", (PyCFunction)cwd_of_process, METH_O, ""}, + {"cmdline_of_process", (PyCFunction)cmdline_of_process, METH_O, ""}, + {NULL, NULL, 0, NULL} /* Sentinel */ +}; + + +bool +init_macos_process_info(PyObject *module) { + if (PyModule_AddFunctions(module, module_methods) != 0) return false; + return true; +} diff --git a/setup.py b/setup.py index a05c25396..9b89ba18f 100755 --- a/setup.py +++ b/setup.py @@ -402,7 +402,7 @@ def compile_c_extension(kenv, module, incremental, compilation_database, all_key def find_c_files(): ans, headers = [], [] d = os.path.join(base, 'kitty') - exclude = {'fontconfig.c', 'freetype.c', 'desktop.c'} if is_macos else {'core_text.m', 'cocoa_window.m'} + exclude = {'fontconfig.c', 'freetype.c', 'desktop.c'} if is_macos else {'core_text.m', 'cocoa_window.m', 'macos_process_info.c'} for x in os.listdir(d): ext = os.path.splitext(x)[1] if ext in ('.c', '.m') and os.path.basename(x) not in exclude: