diff options
author | Oleksii Shevchuk <alxchk@gmail.com> | 2017-05-21 22:27:38 +0300 |
---|---|---|
committer | Giampaolo Rodola <g.rodola@gmail.com> | 2017-05-21 21:27:38 +0200 |
commit | 68e04def9b1f04462fa8f2eefcc5287b1b943cfe (patch) | |
tree | a71b523b97b3ca3ee5621d34663f56a0bdfe4951 | |
parent | a92cfcd574e876189b64449c8e1eea505768a83e (diff) | |
download | psutil-68e04def9b1f04462fa8f2eefcc5287b1b943cfe.tar.gz |
Add environment parsing (#1091)
* Add common functions to extract information from SunOS process address space
* SunOS feature: Add .environ()
-rw-r--r-- | psutil/_pssunos.py | 4 | ||||
-rw-r--r-- | psutil/_psutil_sunos.c | 76 | ||||
-rw-r--r-- | psutil/arch/solaris/process_as_utils.c | 386 | ||||
-rw-r--r-- | psutil/arch/solaris/process_as_utils.h | 21 | ||||
-rwxr-xr-x | setup.py | 1 |
5 files changed, 488 insertions, 0 deletions
diff --git a/psutil/_pssunos.py b/psutil/_pssunos.py index b1ba6b45..53821829 100644 --- a/psutil/_pssunos.py +++ b/psutil/_pssunos.py @@ -415,6 +415,10 @@ class Process(object): return self._proc_name_and_args()[1].split(' ') @wrap_exceptions + def environ(self): + return cext.proc_environ(self.pid, self._procfs_path) + + @wrap_exceptions def create_time(self): return self._proc_basic_info()[proc_info_map['create_time']] diff --git a/psutil/_psutil_sunos.c b/psutil/_psutil_sunos.c index fcfbd1ac..0af8bc47 100644 --- a/psutil/_psutil_sunos.c +++ b/psutil/_psutil_sunos.c @@ -50,6 +50,8 @@ #include "_psutil_common.h" #include "_psutil_posix.h" +#include "arch/solaris/process_as_utils.h" + #define PSUTIL_TV2DOUBLE(t) (((t).tv_nsec * 0.000000001) + (t).tv_sec) @@ -154,6 +156,78 @@ error: return NULL; } +/* + * Return process environ block + */ +static PyObject * +psutil_proc_environ(PyObject *self, PyObject *args) { + int pid; + char path[1000]; + psinfo_t info; + const char *procfs_path; + char **env = NULL; + ssize_t env_count = -1; + char *dm; + int i = 0; + PyObject *py_retdict = NULL; + PyObject *py_envname = NULL; + PyObject *py_envval = NULL; + + if (! PyArg_ParseTuple(args, "is", &pid, &procfs_path)) + goto error; + + sprintf(path, "%s/%i/psinfo", procfs_path, pid); + if (! psutil_file_to_struct(path, (void *)&info, sizeof(info))) + goto error; + + env = psutil_read_raw_env(info, procfs_path, &env_count); + if (! env && env_count != 0) + goto error; + + py_retdict = PyDict_New(); + if (! py_retdict) { + PyErr_NoMemory(); + goto error; + } + + for (i=0; i<env_count; i++) { + if (! env[i]) + break; + + /* Environment corrupted */ + dm = strchr(env[i], '='); + if (! dm) + break; + + *dm = '\0'; + + py_envname = PyUnicode_DecodeFSDefault(env[i]); + if (! py_envname) + goto error; + + py_envval = PyUnicode_DecodeFSDefault(dm+1); + if (! py_envname) + goto error; + + if (PyDict_SetItem(py_retdict, py_envname, py_envval) < 0) + goto error; + + Py_DECREF(py_envname); + Py_DECREF(py_envval); + } + + psutil_free_cstrings_array(env, env_count); + return py_retdict; + + error: + if (env && env_count >= 0) + psutil_free_cstrings_array(env, env_count); + + Py_XDECREF(py_envname); + Py_XDECREF(py_envval); + Py_XDECREF(py_retdict); + return NULL; +} /* * Return process user and system CPU times as a Python tuple. @@ -1475,6 +1549,8 @@ PsutilMethods[] = { "Return process ppid, rss, vms, ctime, nice, nthreads, status and tty"}, {"proc_name_and_args", psutil_proc_name_and_args, METH_VARARGS, "Return process name and args."}, + {"proc_environ", psutil_proc_environ, METH_VARARGS, + "Return process environment."}, {"proc_cpu_times", psutil_proc_cpu_times, METH_VARARGS, "Return process user and system CPU times."}, {"proc_cred", psutil_proc_cred, METH_VARARGS, diff --git a/psutil/arch/solaris/process_as_utils.c b/psutil/arch/solaris/process_as_utils.c new file mode 100644 index 00000000..6af8f04a --- /dev/null +++ b/psutil/arch/solaris/process_as_utils.c @@ -0,0 +1,386 @@ +/* + * Copyright (c) 2017, Oleksii Shevchuk. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * Functions specific to Sun OS Solaris platforms. + */ + +#define _STRUCTURED_PROC 1 + +#if !defined(_LP64) && _FILE_OFFSET_BITS == 64 +# undef _FILE_OFFSET_BITS +# undef _LARGEFILE64_SOURCE +#endif + +#include <Python.h> + +#include <sys/types.h> +#include <sys/procfs.h> +#include <sys/stat.h> +#include <fcntl.h> + +#include "process_as_utils.h" + +/** Function opens address space of specified process and return file + * descriptor. + * @param pid a pid of process. + * @param procfs_path a path to mounted procfs filesystem. + * @return file descriptor or -1 in case of error. + */ +static int +open_address_space(pid_t pid, const char *procfs_path) { + int fd; + char proc_path[PATH_MAX]; + + snprintf(proc_path, PATH_MAX, "%s/%i/as", procfs_path, pid); + fd = open(proc_path, O_RDONLY); + if (fd < 0) + PyErr_SetFromErrno(PyExc_OSError); + + return fd; +} + +/** Function reads chunk of data by offset to specified + * buffer of the same size. + * @param fd a file descriptor. + * @param offset an required offset in file. + * @param buf a buffer where to store result. + * @param buf_size a size of buffer where data will be stored. + * @return amount of bytes stored to the buffer or -1 in case of + * error. + */ +static int +read_offt(int fd, off_t offset, char *buf, size_t buf_size) { + size_t to_read = buf_size; + size_t stored = 0; + + while (to_read) { + int r = pread(fd, buf + stored, to_read, offset + stored); + if (r < 0) + goto error; + else if (r == 0) + break; + + to_read -= r; + stored += r; + } + + return stored; + + error: + PyErr_SetFromErrno(PyExc_OSError); + return -1; +} + +#define STRING_SEARCH_BUF_SIZE 512 + +/** Function reads null-terminated string from file descriptor starting from + * specified offset. + * @param fd a file descriptor of opened address space. + * @param offset an offset in specified file descriptor. + * @return allocated null-terminated string or NULL in case of error. +*/ +static char * +read_cstring_offt(int fd, off_t offset) { + int r; + int i = 0; + off_t end = offset; + size_t len; + char buf[STRING_SEARCH_BUF_SIZE]; + char *result = NULL; + + if (lseek(fd, offset, SEEK_SET) == (off_t)-1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + + // Search end of string + for (;;) { + r = read(fd, buf, sizeof(buf)); + if (r == -1) { + PyErr_SetFromErrno(PyExc_OSError); + goto error; + } + else if (r == 0) + break; + else + for (i=0; i<r; i++) + if (! buf[i]) + goto found; + + end += r; + } + + found: + len = end + i - offset; + + result = malloc(len+1); + if (! result) { + PyErr_NoMemory(); + goto error; + } + + if (len) + if (read_offt(fd, offset, result, len) < 0) + goto error; + + result[len] = '\0'; + return result; + + error: + if (result) + free(result); + + return NULL; +} + +/** Function reads block of addresses by offset, dereference them one by one + * and create an array of null terminated C strings from them. + * @param fd a file descriptor of address space of interesting process. + * @param offset an offset of address block in address space. + * @param ptr_size a size of pointer. Only 4 or 8 are valid values. + * @param count amount of pointers in block. + * @return allocated array of strings dereferenced and read by offset. Number of + * elements in array are count. In case of error function returns NULL. + */ +static char ** +read_cstrings_block(int fd, off_t offset, size_t ptr_size, size_t count) { + char **result = NULL; + char *pblock = NULL; + size_t pblock_size; + int i; + + assert(ptr_size == 4 || ptr_size == 8); + + if (!count) + goto error; + + pblock_size = ptr_size * count; + + pblock = malloc(pblock_size); + if (! pblock) { + PyErr_NoMemory(); + goto error; + } + + if (read_offt(fd, offset, pblock, pblock_size) != pblock_size) + goto error; + + result = (char **) calloc(count, sizeof(char *)); + if (! result) { + PyErr_NoMemory(); + goto error; + } + + for (i=0; i<count; i++) { + result[i] = read_cstring_offt( + fd, (ptr_size == 4? + ((uint32_t *) pblock)[i]: + ((uint64_t *) pblock)[i])); + + if (!result[i]) + goto error; + } + + free(pblock); + return result; + + error: + if (result) + psutil_free_cstrings_array(result, i); + if (pblock) + free(pblock); + return NULL; +} + +/** Checks that caller process can extract proper values from psinfo_t structure. + * @param info a ponter to process info (psinfo_t) structure of the + * interesting process. + * @return 1 in case if caller process can extract proper values from psinfo_t + * structure, or 0 otherwise. + */ +static inline int +is_ptr_dereference_possible(psinfo_t info) { +#if !defined(_LP64) + return info.pr_dmodel == PR_MODEL_ILP32; +#else + return 1; +#endif +} + +/** Return pointer size according to psinfo_t structure + * @param info a ponter to process info (psinfo_t) structure of the + * interesting process. + * @return pointer size (4 or 8). + */ +static inline int +ptr_size_by_psinfo(psinfo_t info) { + return info.pr_dmodel == PR_MODEL_ILP32? 4 : 8; +} + +/** Count amount of pointers in a block which ends with NULL. + * @param fd a discriptor of /proc/PID/as special file. + * @param offt an offset of block of pointers at the file. + * @param ptr_size a pointer size (allowed values: {4, 8}). + * @return amount of non-NULL pointers or -1 in case of error. + */ +static int +search_pointers_vector_size_offt(int fd, off_t offt, size_t ptr_size) { + int count = 0; + int r; + char buf[8]; + static const char zeros[8] = { 0, 0, 0, 0, 0, 0, 0, 0 }; + + assert(ptr_size == 4 || ptr_size == 8); + + if (lseek(fd, offt, SEEK_SET) == (off_t)-1) + goto error; + + for (;; count ++) { + r = read(fd, buf, ptr_size); + + if (r < 0) + goto error; + + if (r == 0) + break; + + if (r != ptr_size) { + PyErr_SetString( + PyExc_RuntimeError, "Pointer block is truncated"); + + return -1; + } + + if (! memcmp(buf, zeros, ptr_size)) + break; + } + + return count; + + error: + PyErr_SetFromErrno(PyExc_OSError); + return -1; +} + +/** Derefence and read array of strings by psinfo_t.pr_argv pointer from remote process. + * @param info a ponter to process info (psinfo_t) structure of the + * interesting process + * @param procfs_path a cstring with path to mounted procfs filesystem. + * @param count a pointer to variable where to store amount of elements in + * returned array. In case of error value of variable will not be changed. + * @return allocated array of cstrings or NULL in case of error. + */ +char ** +psutil_read_raw_args(psinfo_t info, const char *procfs_path, size_t *count) { + int as; + char **result; + + if (! is_ptr_dereference_possible(info)) { + PyErr_SetString( + PyExc_RuntimeError, "Dereferencing procfs pointers of " + "64bit process is not possible"); + + return NULL; + } + + if (! (info.pr_argv && info.pr_argc)) { + PyErr_SetString( + PyExc_RuntimeError, "Process doesn't have arguments block"); + + return NULL; + } + + as = open_address_space(info.pr_pid, procfs_path); + if (as < 0) + return NULL; + + result = read_cstrings_block( + as, info.pr_argv, ptr_size_by_psinfo(info), info.pr_argc + ); + + if (result && count) + *count = info.pr_argc; + + close(as); + + return result; +} + +/** Dereference and read array of strings by psinfo_t.pr_envp pointer from remote process. + * @param info a ponter to process info (psinfo_t) structure of the + * interesting process. + * @param procfs_path a cstring with path to mounted procfs filesystem. + * @param count a pointer to variable where to store amount of elements in + * returned array. In case of error value of variable will not be + * changed. To detect special case (described later) variable should be + * initialized by -1 or other negative value. + * @return allocated array of cstrings or NULL in case of error. + * Special case: count set to 0, return NULL. + * Special case means there is no error acquired, but no data retrieved. + * Special case exists because the nature of the process. From the + * beginning it's not clean how many pointers in envp array. Also + * situation when environment is empty is common for kernel processes. + */ +char ** +psutil_read_raw_env(psinfo_t info, const char *procfs_path, ssize_t *count) { + int as; + int env_count; + int ptr_size; + char **result = NULL; + + if (! is_ptr_dereference_possible(info)) { + PyErr_SetString( + PyExc_RuntimeError, "Dereferencing procfs pointers of " + "64bit process is not possible"); + + return NULL; + } + + if (! info.pr_envp) { + if (count) + *count = 0; + + return NULL; + } + + as = open_address_space(info.pr_pid, procfs_path); + if (as < 0) + return NULL; + + ptr_size = ptr_size_by_psinfo(info); + + env_count = search_pointers_vector_size_offt( + as, info.pr_envp, ptr_size); + + if (env_count >= 0 && count) + *count = env_count; + + if (env_count > 0) + result = read_cstrings_block( + as, info.pr_envp, ptr_size, env_count + ); + + close(as); + return result; +} + +/** Free array of cstrings. + * @param array an array of cstrings returned by psutil_read_raw_env, + * psutil_read_raw_args or any other function. + * @param count a count of strings in the passed array + */ +void +psutil_free_cstrings_array(char **array, size_t count) { + int i; + + if (!array) + return; + + for (i=0; i<count; i++) + if (array[i]) + free(array[i]); + + free(array); +} diff --git a/psutil/arch/solaris/process_as_utils.h b/psutil/arch/solaris/process_as_utils.h new file mode 100644 index 00000000..f886767c --- /dev/null +++ b/psutil/arch/solaris/process_as_utils.h @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2017, Oleksii Shevchuk. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + * + * Functions specific to Sun OS Solaris platforms. + */ + +#ifndef PROCESS_AS_UTILS_H +#define PROCESS_AS_UTILS_H + +char ** +psutil_read_raw_args(psinfo_t info, const char *procfs_path, size_t *count); + +char ** +psutil_read_raw_env(psinfo_t info, const char *procfs_path, ssize_t *count); + +void +psutil_free_cstrings_array(char **array, size_t count); + +#endif // PROCESS_AS_UTILS_H @@ -238,6 +238,7 @@ elif SUNOS: sources=sources + [ 'psutil/_psutil_sunos.c', 'psutil/arch/solaris/v10/ifaddrs.c', + 'psutil/arch/solaris/process_as_utils.c' ], define_macros=macros, libraries=['kstat', 'nsl', 'socket']) |