Saturday, October 24, 2020

Giampaolo Rodola: FreeBSD process environ and resource limits

New psutil 5.7.3 is out. This release adds support for 2 functionalities which were not available on BSD platforms: the ability to get the process environment (all BSD) and to get or set process resource limits (FreeBSD only), similarly to what can be done on Linux.

Process environ

Quite simply:

>>> import psutil, pprint, os
>>> pid = os.getpid()
>>> pprint.pprint(psutil.Process(pid).environ())
{'BLOCKSIZE': 'K',
 'EDITOR': 'vi',
 'GROUP': 'vagrant',
 'HOME': '/home/vagrant',
 'HOST': 'freebsd',
 'HOSTTYPE': 'FreeBSD',
 'LOGNAME': 'vagrant',
 'MACHTYPE': 'x86_64',
 'MAIL': '/var/mail/vagrant',
 'OSTYPE': 'FreeBSD',
 'PAGER': 'less',
 'PATH': '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:/home/vagrant/bin',
 'PWD': '/home/vagrant/psutil',
 'REMOTEHOST': '10.0.2.2',
 'SHELL': '/bin/csh',
 'SHLVL': '1',
 'SSH_CLIENT': '10.0.2.2 58102 22',
 'SSH_CONNECTION': '10.0.2.2 58102 10.0.2.15 22',
 'SSH_TTY': '/dev/pts/0',
 'TERM': 'xterm-256color',
 'USER': 'vagrant',
 'VENDOR': 'amd'}

This feature was already available on all other platforms except BSD. It was contributed by Armin Gruner in PR-1800 and supports all BSD variants (FreeBSD, NetBSD and OpenBSD). BSD kernels expose this information via kvm_getenvv syscall, but there are subtle differences between BSD variants which made this integration particularly thorny. This is how it's done:

PyObject *
psutil_proc_environ(PyObject *self, PyObject *args) {
    int i, cnt = -1;
    long pid;
    char *s, **envs, errbuf[_POSIX2_LINE_MAX];
    PyObject *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) {
        NoSuchProcess("kvm_getprocs");
        goto error;
    }
    if (cnt <= 0) {
        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 there no access to the environ 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 defined(PSUTIL_FREEBSD)
#if (defined(__FreeBSD_version) && __FreeBSD_version >= 700000)
    if (!((p)->ki_flag & P_INMEM) || ((p)->ki_flag & P_SYSTEM)) {
#else
    if ((p)->ki_flag & P_SYSTEM) {
#endif
#elif defined(PSUTIL_NETBSD)
    if ((p)->p_stat == SZOMB) {
#elif defined(PSUTIL_OPENBSD)
    if ((p)->p_flag & P_SYSTEM) {
#endif
        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 to "psutil" general high-level exceptions
        switch (errno) {
            case 0:
                // Process has cleared it's environment, return empty one
                kvm_close(kd);
                return py_retdict;
            case EPERM:
                AccessDenied("kvm_getenvv");
                break;
            case ESRCH:
                NoSuchProcess("kvm_getenvv");
                break;
#if defined(PSUTIL_FREEBSD)
            case ENOMEM:
                // Unfortunately, under FreeBSD kvm_getenvv() returns
                // failure for certain processes ( e.g. try
                // "sudo procstat -e <pid of your XOrg server>".)
                // Map the error condition to 'AccessDenied'.
                sprintf(errbuf,
                        "kvm_getenvv(pid=%ld, ki_uid=%d): errno=ENOMEM",
                        pid, p->ki_uid);
                AccessDenied(errbuf);
                break;
#endif
            default:
                sprintf(errbuf, "kvm_getenvv(pid=%ld)", pid);
                PyErr_SetFromOSErrnoWithSyscall(errbuf);
                break;
        }
        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 NULL;
}

Process resource limits

There's a Linux-only syscall named prlimit which lets you get or set process limits on a per-process basis. I found out very recently that this can be emulated on FreeBSD via sysctl + KERN_PROC_RLIMIT. Here's the PR and here's the relevant part. Example on how to get/set resource limits:

>>> import psutil, os
>>> pid = os.getpid()
>>> p = psutil.Process(pid)
>>> p.rlimit(psutil.RLIMIT_NOFILE, (128, 128))   # process can open max 128 file descriptors
>>> p.rlimit(psutil.RLIMIT_FSIZE, (1024, 1024))  # can create files no bigger than 1024 bytes
>>> p.rlimit(psutil.RLIMIT_FSIZE)                # get
(1024, 1024)
>>>

Relevant C code:

PyObject *
psutil_proc_getrlimit(PyObject *self, PyObject *args) {
    pid_t pid;
    int ret;
    int resource;
    size_t len;
    int name[5];
    struct rlimit rlp;

    if (! PyArg_ParseTuple(args, _Py_PARSE_PID "i", &pid, &resource))
        return NULL;

    name[0] = CTL_KERN;
    name[1] = KERN_PROC;
    name[2] = KERN_PROC_RLIMIT;
    name[3] = pid;
    name[4] = resource;
    len = sizeof(rlp);

    ret = sysctl(name, 5, &rlp, &len, NULL, 0);
    if (ret == -1)
        return PyErr_SetFromErrno(PyExc_OSError);

#if defined(HAVE_LONG_LONG)
    return Py_BuildValue("LL",
                         (PY_LONG_LONG) rlp.rlim_cur,
                         (PY_LONG_LONG) rlp.rlim_max);
#else
    return Py_BuildValue("ll",
                         (long) rlp.rlim_cur,
                         (long) rlp.rlim_max);
#endif
}


PyObject *
psutil_proc_setrlimit(PyObject *self, PyObject *args) {
    pid_t pid;
    int ret;
    int resource;
    int name[5];
    struct rlimit new;
    struct rlimit *newp = NULL;
    PyObject *py_soft = NULL;
    PyObject *py_hard = NULL;

    if (! PyArg_ParseTuple(
            args, _Py_PARSE_PID "iOO", &pid, &resource, &py_soft, &py_hard))
        return NULL;

    name[0] = CTL_KERN;
    name[1] = KERN_PROC;
    name[2] = KERN_PROC_RLIMIT;
    name[3] = pid;
    name[4] = resource;

#if defined(HAVE_LONG_LONG)
    new.rlim_cur = PyLong_AsLongLong(py_soft);
    if (new.rlim_cur == (rlim_t) - 1 && PyErr_Occurred())
        return NULL;
    new.rlim_max = PyLong_AsLongLong(py_hard);
    if (new.rlim_max == (rlim_t) - 1 && PyErr_Occurred())
        return NULL;
#else
    new.rlim_cur = PyLong_AsLong(py_soft);
    if (new.rlim_cur == (rlim_t) - 1 && PyErr_Occurred())
        return NULL;
    new.rlim_max = PyLong_AsLong(py_hard);
    if (new.rlim_max == (rlim_t) - 1 && PyErr_Occurred())
        return NULL;
#endif
    newp = &new;
    ret = sysctl(name, 5, NULL, 0, newp, sizeof(*newp));
    if (ret == -1)
        return PyErr_SetFromErrno(PyExc_OSError);
    Py_RETURN_NONE;
}

It turns out some of this can probably also be emulated on Windows via SetInformationObject and QueryInformationJobObject (see ticket), so that is something I'm looking forward to experiment with.



from Planet Python
via read more

No comments:

Post a Comment

TestDriven.io: Working with Static and Media Files in Django

This article looks at how to work with static and media files in a Django project, locally and in production. from Planet Python via read...