diff --git a/psutil/_psbsd.py b/psutil/_psbsd.py index c81ef2aaf8..0568e3a853 100644 --- a/psutil/_psbsd.py +++ b/psutil/_psbsd.py @@ -668,6 +668,10 @@ def cmdline(self): else: return cext.proc_cmdline(self.pid) + @wrap_exceptions + def environ(self): + return cext.proc_environ(self.pid) + @wrap_exceptions def terminal(self): tty_nr = self.oneshot()[kinfo_proc_map['ttynr']] diff --git a/psutil/_psutil_bsd.c b/psutil/_psutil_bsd.c index e310b7ce27..f9b7af1c63 100644 --- a/psutil/_psutil_bsd.c +++ b/psutil/_psutil_bsd.c @@ -57,6 +57,7 @@ #include #include // process open files/connections #include +#include #include "_psutil_common.h" #include "_psutil_posix.h" @@ -74,6 +75,11 @@ #else #include #endif + #include + #include + #include + #include + #define PSUTIL_PROC_SKIP(p) (!((p)->ki_flag & P_INMEM) || ((p)->ki_flag & P_SYSTEM)) #elif PSUTIL_OPENBSD #include "arch/openbsd/specific.h" @@ -83,6 +89,7 @@ #include #undef _KERNEL #include // for CPUSTATES & CP_* + #define PSUTIL_PROC_SKIP(p) ((p)->p_flag & P_SYSTEM) #elif PSUTIL_NETBSD #include "arch/netbsd/specific.h" #include "arch/netbsd/socks.h" @@ -93,6 +100,7 @@ #ifndef DTYPE_VNODE #define DTYPE_VNODE 1 #endif + #define PSUTIL_PROC_SKIP(p) ((p)->p_stat == SZOMB) #endif @@ -391,6 +399,132 @@ psutil_proc_cmdline(PyObject *self, PyObject *args) { } +/* + * Return process environment as a Python dictionary + */ +PyObject * +psutil_proc_environ(PyObject *self, PyObject *args) { + int i, cnt = -1; + long pid; + char *s, **envs, errbuf[_POSIX2_LINE_MAX]; + PyObject *py_err=NULL, *py_value=NULL, *py_retdict=NULL; + kvm_t *kd; +#ifdef PSUTIL_NETBSD + struct kinfo_proc2 *p; +#else + struct kinfo_proc *p; +#endif + + if (!PyArg_ParseTuple(args, "l", &pid)) + return NULL; + +#if defined(PSUTIL_FREEBSD) + kd = kvm_openfiles(NULL, "/dev/null", NULL, 0, errbuf); +#else + kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, errbuf); +#endif + if (!kd) { + convert_kvm_err("kvm_openfiles", errbuf); + return NULL; + } + + py_retdict = PyDict_New(); + if (!py_retdict) + goto error; + +#if defined(PSUTIL_FREEBSD) + p = kvm_getprocs(kd, KERN_PROC_PID, pid, &cnt); +#elif defined(PSUTIL_OPENBSD) + p = kvm_getprocs(kd, KERN_PROC_PID, pid, sizeof(*p), &cnt); +#elif defined(PSUTIL_NETBSD) + p = kvm_getproc2(kd, KERN_PROC_PID, pid, sizeof(*p), &cnt); +#endif + if (!p) { + py_err = NoSuchProcess("kvm_getprocs"); + goto error; + } + if (cnt <= 0) { + py_err = NoSuchProcess(cnt < 0 ? kvm_geterr(kd) : "kvm_getprocs: cnt==0"); + goto error; + } + + /* On *BSD kernels there are a few kernel-only system processes without an environment. + * (See e.g. "procstat -e 0 | 1 | 2 ..." on FreeBSD.) + * + * Some system process have no stats attached at all (they are marked with P_SYSTEM.) + * + * On FreeBSD, it's possible that the process is swapped or paged out, + * then we have no access to the environment stored in the process' user area. + * + * On NetBSD, we cannot call kvm_getenvv2() for a zombie process. + * + * To make unittest suite happy, return an empty environment. + */ + if (PSUTIL_PROC_SKIP(p)) { + kvm_close(kd); + return py_retdict; + } + +#if defined(PSUTIL_NETBSD) + envs = kvm_getenvv2(kd, p, 0); +#else + envs = kvm_getenvv(kd, p, 0); +#endif + if (!envs) { + /* Map the most obvious stuff to general high-level exceptions exported by our "psutil" */ + if (errno == EPERM) + py_err = AccessDenied("kvm_getenvv"); + else if (errno == ESRCH) + py_err = NoSuchProcess("kvm_getenvv"); + else { +#if defined(PSUTIL_FREEBSD) + /* Unfortunately, under FreeBSD kvm_getenvv() returns failure for certain processes + * ( e.g. try "sudo procstat -e ".) + * To unbreak psutil.process_iter(), map the error condition to 'AccessDenied'. + * + * XXX TODO Check whether this affects other *BSD family members. + */ + if (errno == ENOMEM && p->ki_uid == 0) { + sprintf(errbuf, "kvm_getenvv(pid=%ld, ki_uid=%d): errno=ENOMEM", pid, p->ki_uid); + py_err = AccessDenied(errbuf); + goto error; + } + if (errno == 0) { + /* Process has cleared it's environment, return empty one */ + kvm_close(kd); + return py_retdict; + } +#endif + sprintf(errbuf, "kvm_getenvv(pid=%ld)", pid); + PyErr_SetFromOSErrnoWithSyscall(errbuf); + } + goto error; + } + + for (i = 0; envs[i] != NULL; i++) { + s = strchr(envs[i], '='); + if (!s) + continue; + *s++ = 0; + py_value = PyUnicode_DecodeFSDefault(s); + if (!py_value) + goto error; + if (PyDict_SetItemString(py_retdict, envs[i], py_value)) { + goto error; + } + Py_DECREF(py_value); + } + + kvm_close(kd); + return py_retdict; + +error: + Py_XDECREF(py_value); + Py_XDECREF(py_retdict); + kvm_close(kd); + return py_err; +} + /* * Return the number of logical CPUs in the system. * XXX this could be shared with macOS @@ -960,6 +1094,8 @@ static PyMethodDef mod_methods[] = { {"cpu_count_phys", psutil_cpu_count_phys, METH_VARARGS, "Return an XML string to determine the number physical CPUs."}, #endif + {"proc_environ", psutil_proc_environ, METH_VARARGS, + "Return process environment"}, // --- system-related functions diff --git a/psutil/tests/test_contracts.py b/psutil/tests/test_contracts.py index 51bbb9f097..2d9e5917e6 100755 --- a/psutil/tests/test_contracts.py +++ b/psutil/tests/test_contracts.py @@ -137,7 +137,8 @@ class TestAvailProcessAPIs(PsutilTestCase): def test_environ(self): self.assertEqual(hasattr(psutil.Process, "environ"), - LINUX or MACOS or WINDOWS or AIX or SUNOS) + LINUX or MACOS or WINDOWS or AIX or SUNOS or + FREEBSD or OPENBSD or NETBSD) def test_uids(self): self.assertEqual(hasattr(psutil.Process, "uids"), POSIX)