// SPDX-License-Identifier: MIT

/* Kill the most memory-hungy process */

#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <limits.h> // for PATH_MAX
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "globals.h"
#include "kill.h"
#include "meminfo.h"
#include "msg.h"

#define BADNESS_PREFER 300
#define BADNESS_AVOID -300

static int isnumeric(char* str)
{
    int i = 0;

    // Empty string is not numeric
    if (str[0] == 0)
        return 0;

    while (1) {
        if (str[i] == 0) // End of string
            return 1;

        if (isdigit(str[i]) == 0)
            return 0;

        i++;
    }
}

static void notify(const char* summary, const char* body)
{
    int pid = fork();
    if (pid > 0) {
        // parent
        return;
    }
    char summary2[1024] = { 0 };
    snprintf(summary2, sizeof(summary2), "string:%s", summary);
    char body2[1024] = "string:";
    if (body != NULL) {
        snprintf(body2, sizeof(body2), "string:%s", body);
    }
    // Complete command line looks like this:
    // dbus-send --system / net.nuetzlich.SystemNotifications.Notify 'string:summary text' 'string:and body text'
    execl("/usr/bin/dbus-send", "dbus-send", "--system", "/", "net.nuetzlich.SystemNotifications.Notify",
        summary2, body2, NULL);
    warn("notify: exec failed: %s\n", strerror(errno));
    exit(1);
}

/*
 * Send the selected signal to "pid" and wait for the process to exit
 * (max 10 seconds)
 */
int kill_wait(const poll_loop_args_t* args, pid_t pid, int sig)
{
    if (args->dryrun && sig != 0) {
        warn("dryrun, not actually sending any signal\n");
        return 0;
    }
    meminfo_t m = { 0 };
    const unsigned poll_ms = 100;
    int res = kill(pid, sig);
    if (res != 0) {
        return res;
    }
    /* signal 0 does not kill the process. Don't wait for it to exit */
    if (sig == 0) {
        return 0;
    }
    for (unsigned i = 0; i < 100; i++) {
        float secs = (float)(i * poll_ms) / 1000;
        // We have sent SIGTERM but now have dropped below SIGKILL limits.
        // Escalate to SIGKILL.
        if (sig != SIGKILL) {
            m = parse_meminfo();
            print_mem_stats(debug, m);
            if (m.MemAvailablePercent <= args->mem_kill_percent && m.SwapFreePercent <= args->swap_kill_percent) {
                sig = SIGKILL;
                res = kill(pid, sig);
                // kill first, print after
                warn("escalating to SIGKILL after %.1f seconds\n", secs);
                if (res != 0) {
                    return res;
                }
            }
        } else if (enable_debug) {
            m = parse_meminfo();
            print_mem_stats(printf, m);
        }
        if (!is_alive(pid)) {
            warn("process exited after %.1f seconds\n", secs);
            return 0;
        }
        struct timespec req = { .tv_sec = (time_t)(poll_ms / 1000), .tv_nsec = (poll_ms % 1000) * 1000000 };
        nanosleep(&req, NULL);
    }
    errno = ETIME;
    return -1;
}

/*
 * Find the process with the largest oom_score.
 */
procinfo_t find_largest_process(const poll_loop_args_t* args)
{
    DIR* procdir = opendir("/proc");
    if (procdir == NULL) {
        fatal(5, "%s: could not open /proc: %s", __func__, strerror(errno));
    }

    struct timespec t0 = { 0 }, t1 = { 0 };
    if (enable_debug) {
        clock_gettime(CLOCK_MONOTONIC, &t0);
    }

    procinfo_t victim = { 0 };
    while (1) {
        errno = 0;
        struct dirent* d = readdir(procdir);
        if (d == NULL) {
            if (errno != 0)
                warn("%s: readdir error: %s", __func__, strerror(errno));
            break;
        }

        // proc contains lots of directories not related to processes,
        // skip them
        if (!isnumeric(d->d_name))
            continue;

        procinfo_t cur = {
            .pid = (int)strtol(d->d_name, NULL, 10),
            .uid = -1,
            .badness = -1,
            .VmRSSkiB = -1,
        };

        if (cur.pid <= 1)
            // Let's not kill init.
            continue;

        debug("pid %5d:", cur.pid);

        {
            int res = get_oom_score(cur.pid);
            if (res < 0) {
                debug(" error reading oom_score: %s\n", strerror(-res));
                continue;
            }
            cur.badness = res;
        }
        if (args->ignore_oom_score_adj) {
            int oom_score_adj = 0;
            int res = get_oom_score_adj(cur.pid, &oom_score_adj);
            if (res < 0) {
                debug(" error reading oom_score_adj: %s\n", strerror(-res));
                continue;
            }
            if (oom_score_adj > 0) {
                cur.badness -= oom_score_adj;
            }
        }

        if ((args->prefer_regex || args->avoid_regex)) {
            int res = get_comm(cur.pid, cur.name, sizeof(cur.name));
            if (res < 0) {
                debug(" error reading process name: %s\n", strerror(-res));
                continue;
            }
            if (args->prefer_regex && regexec(args->prefer_regex, cur.name, (size_t)0, NULL, 0) == 0) {
                cur.badness += BADNESS_PREFER;
            }
            if (args->avoid_regex && regexec(args->avoid_regex, cur.name, (size_t)0, NULL, 0) == 0) {
                cur.badness += BADNESS_AVOID;
            }
        }

        debug(" badness %3d", cur.badness);

        if (cur.badness < victim.badness) {
            // skip "type 1", encoded as 1 space
            debug(" \n");
            continue;
        }

        {
            long long res = get_vm_rss_kib(cur.pid);
            if (res < 0) {
                debug(" error reading rss: %s\n", strerror((int)-res));
                continue;
            }
            cur.VmRSSkiB = res;
        }
        debug(" vm_rss %7llu", cur.VmRSSkiB);
        if (cur.VmRSSkiB == 0) {
            // Kernel threads have zero rss
            // skip "type 2", encoded as 2 spaces
            debug("  \n");
            continue;
        }
        if (cur.badness == victim.badness && cur.VmRSSkiB <= victim.VmRSSkiB) {
            // skip "type 3", encoded as 3 spaces
            debug("   \n");
            continue;
        }

        // Skip processes with oom_score_adj = -1000, like the
        // kernel oom killer would.
        int oom_score_adj = 0;
        {
            int res = get_oom_score_adj(cur.pid, &oom_score_adj);
            if (res < 0) {
                debug(" error reading oom_score_adj: %s\n", strerror(-res));
                continue;
            }
            if (oom_score_adj == -1000) {
                // skip "type 4", encoded as 3 spaces
                debug("    \n");
                continue;
            }
        }

        // Fill out remaining fields
        if (strlen(cur.name) == 0) {
            int res = get_comm(cur.pid, cur.name, sizeof(cur.name));
            if (res < 0) {
                debug(" error reading process name: %s\n", strerror(-res));
                continue;
            }
        }
        {
            int res = get_uid(cur.pid);
            if (res < 0) {
                debug(" error reading uid: %s\n", strerror(-res));
                continue;
            }
            cur.uid = res;
        }

        // Save new victim
        victim = cur;
        debug(" uid %4d oom_score_adj %4d \"%s\" <--- new victim\n", victim.uid, oom_score_adj, victim.name);

    } // end of while(1) loop
    closedir(procdir);

    if (enable_debug) {
        clock_gettime(CLOCK_MONOTONIC, &t1);
        long delta = (t1.tv_sec - t0.tv_sec) * 1000000 + (t1.tv_nsec - t0.tv_nsec) / 1000;
        debug("selecting victim took %ld.%03ld ms\n", delta / 1000, delta % 1000);
    }

    if (victim.pid == getpid()) {
        warn("%s: selected myself (pid %d). Do you use hidpid? See https://github.com/rfjakob/earlyoom/wiki/proc-hidepid\n",
            __func__, victim.pid);
        // zeroize victim struct
        victim = (const procinfo_t) { 0 };
    }

    return victim;
}

/*
 * Kill the victim process, wait for it to exit, send a gui notification
 * (if enabled).
 */
void kill_process(const poll_loop_args_t* args, int sig, const procinfo_t victim)
{
    if (victim.pid <= 0) {
        warn("Could not find a process to kill. Sleeping 1 second.\n");
        if (args->notify) {
            notify("earlyoom", "Error: Could not find a process to kill. Sleeping 1 second.");
        }
        sleep(1);
        return;
    }

    char* sig_name = "?";
    if (sig == SIGTERM) {
        sig_name = "SIGTERM";
    } else if (sig == SIGKILL) {
        sig_name = "SIGKILL";
    } else if (sig == 0) {
        sig_name = "0 (no-op signal)";
    }
    // sig == 0 is used as a self-test during startup. Don't notifiy the user.
    if (sig != 0 || enable_debug) {
        warn("sending %s to process %d uid %d \"%s\": badness %d, VmRSS %lld MiB\n",
            sig_name, victim.pid, victim.uid, victim.name, victim.badness, victim.VmRSSkiB / 1024);
    }

    int res = kill_wait(args, victim.pid, sig);
    int saved_errno = errno;

    // Send the GUI notification AFTER killing a process. This makes it more likely
    // that there is enough memory to spawn the notification helper.
    if (sig != 0) {
        char notif_args[PATH_MAX + 1000];
        snprintf(notif_args, sizeof(notif_args),
            "Low memory! Killing process %d %s", victim.pid, victim.name);
        if (args->notify) {
            notify("earlyoom", notif_args);
        }
    }

    if (sig == 0) {
        return;
    }

    if (res != 0) {
        warn("kill failed: %s\n", strerror(saved_errno));
        if (args->notify) {
            notify("earlyoom", "Error: Failed to kill process");
        }
        // Killing the process may have failed because we are not running as root.
        // In that case, trying again in 100ms will just yield the same error.
        // Throttle ourselves to not spam the log.
        if (saved_errno == EPERM) {
            warn("sleeping 1 second\n");
            sleep(1);
        }
    }
}
