/*
 * Copyright (c) 2009, Jay Loden, Giampaolo Rodola'. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include <Python.h>
#include <kvm.h>
#include <sys/proc.h>
#include <sys/sysctl.h>
#include <sys/types.h>
#include <sys/file.h>
#include <sys/vnode.h>
#ifdef PSUTIL_FREEBSD
#include <sys/user.h>
#include <libutil.h>
#endif

#include "../../arch/all/init.h"


/*
 * Collect different info about a process in one shot and return
 * them as a big Python tuple.
 */
PyObject *
psutil_proc_oneshot_info(PyObject *self, PyObject *args) {
    pid_t pid;
    long rss;
    long vms;
    long memtext;
    long memdata;
    long memstack;
    int oncpu;
#ifdef PSUTIL_NETBSD
    struct kinfo_proc2 kp;
#else
    struct kinfo_proc kp;
#endif
    long pagesize = psutil_getpagesize();
    char name_buf[256];  // buffer for process name
    PyObject *py_name = NULL;
    PyObject *py_ppid = NULL;
    PyObject *py_retlist = NULL;

    if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid))
        return NULL;
    if (psutil_kinfo_proc(pid, &kp) == -1)
        return NULL;

        // Process
#ifdef PSUTIL_FREEBSD
    str_format(name_buf, sizeof(name_buf), "%s", kp.ki_comm);
#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD)
    str_format(name_buf, sizeof(name_buf), "%s", kp.p_comm);
#endif
    py_name = PyUnicode_DecodeFSDefault(name_buf);
    if (!py_name) {
        // If decoding fails, fall back to None safely
        PyErr_Clear();
        py_name = Py_None;
        Py_INCREF(py_name);
    }

    // Calculate memory.
#ifdef PSUTIL_FREEBSD
    rss = (long)kp.ki_rssize * pagesize;
    vms = (long)kp.ki_size;
    memtext = (long)kp.ki_tsize * pagesize;
    memdata = (long)kp.ki_dsize * pagesize;
    memstack = (long)kp.ki_ssize * pagesize;
#else
    rss = (long)kp.p_vm_rssize * pagesize;
#ifdef PSUTIL_OPENBSD
    // VMS, this is how ps determines it on OpenBSD:
    // https://github.com/openbsd/src/blob/
    //     588f7f8c69786211f2d16865c552afb91b1c7cba/bin/ps/print.c#L505
    vms = (long)(kp.p_vm_dsize + kp.p_vm_ssize + kp.p_vm_tsize) * pagesize;
#elif PSUTIL_NETBSD
    // VMS, this is how top determines it on NetBSD:
    // https://github.com/IIJ-NetBSD/netbsd-src/blob/master/external/
    //     bsd/top/dist/machine/m_netbsd.c
    vms = (long)kp.p_vm_msize * pagesize;
#endif
    memtext = (long)kp.p_vm_tsize * pagesize;
    memdata = (long)kp.p_vm_dsize * pagesize;
    memstack = (long)kp.p_vm_ssize * pagesize;
#endif

#ifdef PSUTIL_FREEBSD
    // what CPU we're on; top was used as an example:
    // https://svnweb.freebsd.org/base/head/usr.bin/top/machine.c?
    //     view=markup&pathrev=273835
    // XXX - note: for "intr" PID this is -1.
    if (kp.ki_stat == SRUN && kp.ki_oncpu != NOCPU)
        oncpu = kp.ki_oncpu;
    else
        oncpu = kp.ki_lastcpu;
#else
    // On Net/OpenBSD we have kp.p_cpuid but it appears it's always
    // set to KI_NOCPU. Even if it's not, ki_lastcpu does not exist
    // so there's no way to determine where "sleeping" processes
    // were. Not supported.
    oncpu = -1;
#endif

#ifdef PSUTIL_FREEBSD
    py_ppid = PyLong_FromPid(kp.ki_ppid);
#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD)
    py_ppid = PyLong_FromPid(kp.p_ppid);
#else
    py_ppid = Py_BuildValue("i", -1);
#endif
    if (!py_ppid)
        return NULL;

    // Return a single big tuple with all process info.
    py_retlist = Py_BuildValue(
#if defined(__FreeBSD_version) && __FreeBSD_version >= 1200031
        "(OillllllLdllllddddlllllbO)",
#else
        "(OillllllidllllddddlllllbO)",
#endif
#ifdef PSUTIL_FREEBSD
        py_ppid,  // (pid_t) ppid
        (int)kp.ki_stat,  // (int) status
        // UIDs
        (long)kp.ki_ruid,  // (long) real uid
        (long)kp.ki_uid,  // (long) effective uid
        (long)kp.ki_svuid,  // (long) saved uid
        // GIDs
        (long)kp.ki_rgid,  // (long) real gid
        (long)kp.ki_groups[0],  // (long) effective gid
        (long)kp.ki_svuid,  // (long) saved gid
        //
        kp.ki_tdev,  // (int or long long) tty nr
        PSUTIL_TV2DOUBLE(kp.ki_start),  // (double) create time
        // ctx switches
        kp.ki_rusage.ru_nvcsw,  // (long) ctx switches (voluntary)
        kp.ki_rusage.ru_nivcsw,  // (long) ctx switches (unvoluntary)
        // IO count
        kp.ki_rusage.ru_inblock,  // (long) read io count
        kp.ki_rusage.ru_oublock,  // (long) write io count
        // CPU times: convert from micro seconds to seconds.
        PSUTIL_TV2DOUBLE(kp.ki_rusage.ru_utime),  // (double) user time
        PSUTIL_TV2DOUBLE(kp.ki_rusage.ru_stime),  // (double) sys time
        PSUTIL_TV2DOUBLE(kp.ki_rusage_ch.ru_utime),  // (double) children utime
        PSUTIL_TV2DOUBLE(kp.ki_rusage_ch.ru_stime),  // (double) children stime
        // memory
        rss,  // (long) rss
        vms,  // (long) vms
        memtext,  // (long) mem text
        memdata,  // (long) mem data
        memstack,  // (long) mem stack
        // others
        oncpu,  // (int) the CPU we are on
#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD)
        py_ppid,  // (pid_t) ppid
        (int)kp.p_stat,  // (int) status
        // UIDs
        (long)kp.p_ruid,  // (long) real uid
        (long)kp.p_uid,  // (long) effective uid
        (long)kp.p_svuid,  // (long) saved uid
        // GIDs
        (long)kp.p_rgid,  // (long) real gid
        (long)kp.p_groups[0],  // (long) effective gid
        (long)kp.p_svuid,  // (long) saved gid
        //
        kp.p_tdev,  // (int) tty nr
        PSUTIL_KPT2DOUBLE(kp.p_ustart),  // (double) create time
        // ctx switches
        kp.p_uru_nvcsw,  // (long) ctx switches (voluntary)
        kp.p_uru_nivcsw,  // (long) ctx switches (unvoluntary)
        // IO count
        kp.p_uru_inblock,  // (long) read io count
        kp.p_uru_oublock,  // (long) write io count
        // CPU times: convert from micro seconds to seconds.
        PSUTIL_KPT2DOUBLE(kp.p_uutime),  // (double) user time
        PSUTIL_KPT2DOUBLE(kp.p_ustime),  // (double) sys time
        // OpenBSD and NetBSD provide children user + system times summed
        // together (no distinction).
        kp.p_uctime_sec + kp.p_uctime_usec / 1000000.0,  // (double) ch utime
        kp.p_uctime_sec + kp.p_uctime_usec / 1000000.0,  // (double) ch stime
        // memory
        rss,  // (long) rss
        vms,  // (long) vms
        memtext,  // (long) mem text
        memdata,  // (long) mem data
        memstack,  // (long) mem stack
        // others
        oncpu,  // (int) the CPU we are on
#endif
        py_name  // (pystr) name
    );

    Py_DECREF(py_name);
    Py_DECREF(py_ppid);
    return py_retlist;
}


PyObject *
psutil_proc_name(PyObject *self, PyObject *args) {
    pid_t pid;
#ifdef PSUTIL_NETBSD
    struct kinfo_proc2 kp;
#else
    struct kinfo_proc kp;
#endif
    char str[1000];

    if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid))
        return NULL;
    if (psutil_kinfo_proc(pid, &kp) == -1)
        return NULL;

#ifdef PSUTIL_FREEBSD
    str_format(str, sizeof(str), "%s", kp.ki_comm);
#elif defined(PSUTIL_OPENBSD) || defined(PSUTIL_NETBSD)
    str_format(str, sizeof(str), "%s", kp.p_comm);
#endif
    return PyUnicode_DecodeFSDefault(str);
}


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) {
        psutil_oserror_nsp("kvm_getprocs");
        goto error;
    }
    if (cnt <= 0) {
        psutil_oserror_nsp(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 (!((p)->ki_flag & P_INMEM) || ((p)->ki_flag & P_SYSTEM)) {
#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:
                psutil_oserror_ad("kvm_getenvv -> EPERM");
                break;
            case ESRCH:
                psutil_oserror_nsp("kvm_getenvv -> ESRCH");
                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'.
                str_format(
                    errbuf,
                    sizeof(errbuf),
                    "kvm_getenvv(pid=%ld, ki_uid=%d) -> ENOMEM",
                    pid,
                    p->ki_uid
                );
                psutil_oserror_ad(errbuf);
                break;
#endif
            default:
                str_format(
                    errbuf, sizeof(errbuf), "kvm_getenvv(pid=%ld)", pid
                );
                psutil_oserror_wsyscall(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;
}


/*
 * Return files opened by process as a list of (path, fd) tuples.
 * TODO: this is broken as it may report empty paths. 'procstat'
 * utility has the same problem see:
 * https://github.com/giampaolo/psutil/issues/595
 */
PyObject *
psutil_proc_open_files(PyObject *self, PyObject *args) {
    pid_t pid;
    int i;
    int cnt;
    int regular;
    int fd;
    char *path;
    struct kinfo_file *freep = NULL;
    struct kinfo_file *kif;
#ifdef PSUTIL_NETBSD
    struct kinfo_proc2 kipp;
#else
    struct kinfo_proc kipp;
#endif
    PyObject *py_tuple = NULL;
    PyObject *py_path = NULL;
    PyObject *py_retlist = PyList_New(0);

    if (py_retlist == NULL)
        return NULL;
    if (!PyArg_ParseTuple(args, _Py_PARSE_PID, &pid))
        goto error;
    if (psutil_kinfo_proc(pid, &kipp) == -1)
        goto error;

    errno = 0;
    freep = kinfo_getfile(pid, &cnt);

    if (freep == NULL) {
#if defined(PSUTIL_OPENBSD)
        if ((pid == 0) && (errno == ESRCH)) {
            psutil_debug(
                "open_files() returned ESRCH for PID 0; forcing `return []`"
            );
            PyErr_Clear();
            return py_retlist;
        }
#else
        psutil_raise_for_pid(pid, "kinfo_getfile()");
#endif
        goto error;
    }

    for (i = 0; i < cnt; i++) {
        kif = &freep[i];

#ifdef PSUTIL_FREEBSD
        regular = (kif->kf_type == KF_TYPE_VNODE)
                  && (kif->kf_vnode_type == KF_VTYPE_VREG);
        fd = kif->kf_fd;
        path = kif->kf_path;
#elif PSUTIL_OPENBSD
        regular = (kif->f_type == DTYPE_VNODE) && (kif->v_type == VREG);
        fd = kif->fd_fd;
        // XXX - it appears path is not exposed in the kinfo_file struct.
        path = "";
#elif PSUTIL_NETBSD
        regular = (kif->ki_ftype == DTYPE_VNODE) && (kif->ki_vtype == VREG);
        fd = kif->ki_fd;
        // XXX - it appears path is not exposed in the kinfo_file struct.
        path = "";
#endif
        if (regular == 1) {
            py_path = PyUnicode_DecodeFSDefault(path);
            if (!py_path)
                goto error;
            py_tuple = Py_BuildValue("(Oi)", py_path, fd);
            if (py_tuple == NULL)
                goto error;
            if (PyList_Append(py_retlist, py_tuple))
                goto error;
            Py_CLEAR(py_path);
            Py_CLEAR(py_tuple);
        }
    }
    free(freep);
    return py_retlist;

error:
    Py_XDECREF(py_tuple);
    Py_DECREF(py_retlist);
    if (freep != NULL)
        free(freep);
    return NULL;
}
