[
  {
    "path": ".github/workflows/build.yml",
    "content": "on: [push, pull_request]\n\nname: build-and-test\n\njobs:\n  armv7-glibc:\n    name: Ubuntu 18.04 (for ARMv7 - glibc)\n    runs-on: ubuntu-18.04\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions-rs/toolchain@v1\n        with:\n          toolchain: stable\n          target: armv7-unknown-linux-gnueabihf\n          override: true\n\n      - name: Install binutils-arm-none-eabi\n        run: |\n          sudo apt-get update\n          sudo apt-get install binutils-arm-none-eabi\n      \n      - uses: actions-rs/cargo@v1\n        with:\n          use-cross: true\n          command: build\n          args: --release --target=armv7-unknown-linux-gnueabihf\n          \n      - name: Strip binary\n        run: arm-none-eabi-strip target/armv7-unknown-linux-gnueabihf/release/bustd\n          \n      - name: Upload binary\n        uses: actions/upload-artifact@v2\n        with:\n          name: 'bustd-linux-armv7-glibc'\n          path: target/armv7-unknown-linux-gnueabihf/release/bustd\n\n  armv7-musl:\n    name: Ubuntu 20.01 (for ARMv7 - musl)\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - uses: actions-rs/toolchain@v1\n        with:\n          toolchain: stable\n          target: armv7-unknown-linux-musleabihf\n          override: true\n\n      - name: Install binutils-arm-none-eabi\n        run: |\n          sudo apt-get update\n          sudo apt-get install binutils-arm-none-eabi\n      \n      - name: Run cargo build\n        uses: actions-rs/cargo@v1\n        with:\n          use-cross: true\n          command: build\n          args: --release --target=armv7-unknown-linux-musleabihf\n          \n      - name: Strip binary\n        run: arm-none-eabi-strip target/armv7-unknown-linux-musleabihf/release/bustd\n          \n      - name: Upload binary\n        uses: actions/upload-artifact@v2\n        with:\n          name: 'bustd-linux-armv7-musl'\n          path: target/armv7-unknown-linux-musleabihf/release/bustd\n\n  ubuntu:\n    name: Ubuntu 20.04\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        rust:\n          - stable\n    steps:\n      - name: Checkout sources\n        uses: actions/checkout@v2\n\n      - name: Install toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          toolchain: stable\n          target: x86_64-unknown-linux-musl\n          override: true\n\n      - name: Install dependencies for musl libc\n        run: |\n          sudo apt-get update\n          sudo apt-get install musl-tools\n      - name: Run cargo build\n        uses: actions-rs/cargo@v1\n        with:\n          command: build\n          args: --release --target x86_64-unknown-linux-musl\n\n      - name: Run cargo test\n        uses: actions-rs/cargo@v1\n        with:\n          command: test\n          args: --release --target x86_64-unknown-linux-musl\n\n      - name: Strip binary\n        run: strip target/x86_64-unknown-linux-musl/release/bustd\n\n      - name: Upload binary\n        uses: actions/upload-artifact@v2\n        with:\n          name: 'bustd-x86-64-musl'\n          path: target/x86_64-unknown-linux-musl/release/bustd\n          \n  ubuntu-glibc:\n    name: Ubuntu 18.04 - glibc\n    runs-on: ubuntu-18.04\n    strategy:\n      matrix:\n        rust:\n          - stable\n    steps:\n      - name: Checkout sources\n        uses: actions/checkout@v2\n\n      - name: Install toolchain\n        uses: actions-rs/toolchain@v1\n        with:\n          toolchain: stable\n\n      - name: Run cargo build\n        uses: actions-rs/cargo@v1\n        with:\n          command: build\n          args: --release\n\n      - name: Run cargo test\n        uses: actions-rs/cargo@v1\n        with:\n          command: test\n          args: --release\n\n      - name: Strip binary\n        run: strip target/release/bustd\n\n      - name: Upload binary\n        uses: actions/upload-artifact@v2\n        with:\n          name: 'bustd-x86-64-glibc'\n          path: target/release/bustd\n"
  },
  {
    "path": ".gitignore",
    "content": "target/\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Vinícius R. Miguel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n \n"
  },
  {
    "path": "README.md",
    "content": "# `bustd`: Available memory or bust!\n\n`bustd` is a lightweight process killer daemon for out-of-memory scenarios for Linux!\n\n## Features\n\n### Small memory usage!\n\n`bustd` seems to use less memory than some other lean daemons such as `earlyoom`:\n\n```console\n$ ps -F -C bustd\nUID          PID    PPID  C    SZ   RSS PSR STIME TTY          TIME CMD\nvrmiguel  353609  187407  5   151     8   2 01:20 pts/2    00:00:00 target/x86_64-unknown-linux-musl/release/bustd -V -n\n\n$ ps -F -C earlyoom\nUID          PID    PPID  C    SZ   RSS PSR STIME TTY          TIME CMD\nvrmiguel  350497    9498  0   597   688   6 01:12 pts/1    00:00:00 ./earlyoom/\n```\n\n¹: RSS stands for resident set size and represents the portion of RAM occupied by a process.\n\n²: Compared when bustd was in [this commit](https://github.com/vrmiguel/bustd/commit/61beb097b3631afb231a76bb9187b802c9818793) and earlyoom in [this one](https://github.com/rfjakob/earlyoom/commit/509df072be79b3be2a1de6581499e360ab0180be).\n`bustd` compiled with musl libc and earlyoom with glibc through GCC 11.1. Different configurations would likely change these figures.\n\n\n### Small CPU usage\n\nMuch like `earlyoom` and `nohang`, `bustd` uses adaptive sleep times during its memory polling. Unlike these two, however, `bustd` does not read from `/proc/meminfo`, instead opting for the `sysinfo` syscall.\n\nThis approach has its up- and downsides. The amount of free RAM that `sysinfo` reads does not account for cached memory, while `MemAvailable` in `/proc/meminfo` does.\n\nThe `sysinfo` syscall is one order of magnitude faster, at least according to [this kernel patch](https://sourceware.org/legacy-ml/libc-alpha/2015-08/msg00512.html) (granted, from 2015).\n\nAs `bustd` can't solely rely on the free RAM readings of `sysinfo`, we check for memory stress through [Pressure Stall Information](https://www.kernel.org/doc/html/v5.8/accounting/psi.html).\n\n### `bustd` will try to lock all pages mapped into its address space\n\nMuch like `earlyoom`, `bustd` uses [`mlockall`](https://www.ibm.com/docs/en/aix/7.2?topic=m-mlockall-munlockall-subroutine) to avoid being sent to swap, which allows the daemon to remain responsive even when the system memory is under heavy load and susceptible to [thrashing](https://en.wikipedia.org/wiki/Thrashing_(computer_science)).\n\n### Checks for Pressure Stall Information\n\nThe Linux kernel, since version 4.20 (and built with `CONFIG_PSI=y`), presents canonical new pressure metrics for memory, CPU, and IO.\nIn the words of [Facebook Incubator](https://facebookmicrosites.github.io/psi/docs/overview):\n\n```\nPSI stats are like barometers that provide fair warning of impending resource \nshortages, enabling you to take more proactive, granular, and nuanced steps \nwhen resources start becoming scarce.\n```\n\nMore specifically, `bustd` checks for how long, in microseconds, processes have stalled in the last 10 seconds. By default, `bustd` will kill a process when processes have stalled for 25 microseconds in the last ten seconds.\n\n## Packaging\n\n### Arch Linux\n\nAvailable on the Arch User Repository\n\n### Gentoo\n\nAvailable on the [GURU project](https://gitweb.gentoo.org/repo/proj/guru.git)\n\n###  Pop!_OS\n\nAvailable on the [Pop!_OS PPA](https://launchpad.net/~system76/+archive/ubuntu/pop) (outdated)\n\n\n## Building\n\nRequirements:\n* [Rust toolchain](https://rustup.rs/)\n* Any C compiler\n* Linux 4.20+ built with `CONFIG_PSI=y`\n\n```shell\ngit clone https://github.com/vrmiguel/bustd\ncd bustd && cargo run --release\n```\n\nThe `-n, --no-daemon` flag is useful for running `bustd` through an init system such as `systemd`.\n\n## Prebuilt binaries\n\nBinaries are generated at every commit through [GitHub Actions](https://github.com/vrmiguel/bustd/actions)\n\n## TODO\n\n- [x] Allow for customization of the critical scenario (PSI cutoff)\n- [x] Command-line argument for disabling daemonization (useful for runnning `bustd` as a systemd service)\n- [x] Command-line argument to enable killing the entire process group, not just the chosen process itself\n- [x] Allow the user to setup a list of software that `bustd` should never kill\n- [ ] Notification sending and general notification customization settings\n"
  },
  {
    "path": "rust/Cargo.toml",
    "content": "[package]\nname = \"bustd\"\nauthors = [\"Vinícius R. Miguel <vrmiguel99@gmail.com>\"]\nversion = \"0.1.1\"\nedition = \"2018\"\nreadme = \"README.md\"\nrepository = \"https://github.com/vrmiguel/bustd\"\ndescription = \"Lightweight process killer daemon for out-of-memory scenarios\"\ncategories = [\"command-line-utilities\", \"memory-management\"]\nlicense = \"MIT\"\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[dependencies]\nglob = { version = \"0.3.1\", optional = true }\nlibc      = \"0.2.144\"\ncfg-if    = \"1.0.0\"\ndaemonize = \"0.5.0\"\nargh      = \"0.1.10\"\nmemchr    = \"2.5.0\"\n\n[build-dependencies]\ncc   = \"1.0.68\"\nlibc = \"0.2.97\"\n\n[dev-dependencies]\n# Using a somewhat popular crate, `procfs`, to test our own\n# implementation of proc-fs reads.\n# Probably not the best decision possible but OK for now\nprocfs = { version = \"0.14.2\", default-features = false }\n\n[features]\nglob-ignore = [\"glob\"]\n\n[profile.release]\nlto = true\ncodegen-units = 1\nopt-level = 3\nstrip = true\n"
  },
  {
    "path": "rust/build.rs",
    "content": "fn main() {\n    cc::Build::new().file(\"cc/helper.c\").compile(\"helper\");\n\n    println!(\"cargo:rerun-if-changed=cc/helper.c\");\n}\n"
  },
  {
    "path": "rust/cc/helper.c",
    "content": "#include <sys/mman.h>\n\n#ifdef MCL_ONFAULT\nconst int _MCL_ONFAULT = MCL_ONFAULT;\n#else\nconst int _MCL_ONFAULT = -1;\n#endif"
  },
  {
    "path": "rust/src/cli.rs",
    "content": "use argh::FromArgs;\n\n#[derive(FromArgs)]\n/// Lightweight process killer daemon for out-of-memory scenarios\npub struct CommandLineArgs {\n    /// toggles on verbose output\n    #[argh(switch, short = 'V')]\n    pub verbose: bool,\n\n    /// when set, the process will not be daemonized\n    #[argh(switch, short = 'n')]\n    pub no_daemon: bool,\n\n    /// when set, the victim's entire process group will be killed\n    #[argh(switch, short = 'g')]\n    pub kill_pgroup: bool,\n\n    /// sets the PSI value on which, if surpassed, a process will be killed\n    #[argh(option, short = 'p', long = \"psi\", default = \"25.0\")]\n    pub cutoff_psi: f32, // TODO: responsitivity multiplier?\n\n    #[cfg(feature = \"glob-ignore\")]\n    /// all processes whose names match any of the supplied vertical bar-separated glob patterns will never be chosen to be killed\n    #[argh(\n        option,\n        short = 'u',\n        long = \"unkillables\",\n        from_str_fn(parse_unkillables)\n    )]\n    pub ignored: Option<Vec<glob::Pattern>>,\n}\n\n#[cfg(feature = \"glob-ignore\")]\nfn parse_unkillables(arg: &str) -> Result<Vec<glob::Pattern>, String> {\n    let unkillables: Result<Vec<_>, _> = arg.split('|').map(glob::Pattern::new).collect();\n\n    unkillables.map_err(|err| err.to_string())\n}\n"
  },
  {
    "path": "rust/src/daemon.rs",
    "content": "use std::fs::OpenOptions;\n\nuse daemonize::Daemonize;\n\nuse crate::{error::Result, utils};\n\npub fn daemonize() -> Result<()> {\n    let running_as_sudo = utils::running_as_sudo();\n\n    let username = if running_as_sudo {\n        \"root\".into()\n    } else {\n        utils::get_username().unwrap_or_else(|| \"nobody\".into())\n    };\n\n    let open_opts = OpenOptions::new()\n        .truncate(false)\n        .create(true)\n        .write(true)\n        .to_owned();\n\n    let (stdout_path, stderr_path, pidfile_path) = if running_as_sudo {\n        (\n            \"/var/log/bustd.out\",\n            \"/var/log/bustd.err\",\n            \"/var/run/bustd.pid\",\n        )\n    } else {\n        (\"/tmp/bustd.out\", \"/tmp/bustd.err\", \"/tmp/bustd.pid\")\n    };\n\n    let stdout = open_opts.open(stdout_path)?;\n    let stderr = open_opts.open(stderr_path)?;\n\n    let daemonize = Daemonize::new()\n        .user(&*username)\n        .pid_file(pidfile_path)\n        .chown_pid_file(false)\n        .working_directory(\"/tmp\")\n        .stdout(stdout)\n        .stderr(stderr);\n\n    daemonize.start()?;\n\n    println!(\n        \"[LOG] User {} has started the daemon successfully.\",\n        username\n    );\n\n    Ok(())\n}\n"
  },
  {
    "path": "rust/src/errno.rs",
    "content": "use cfg_if::cfg_if;\nuse libc::{self, c_int};\n\ncfg_if! {\n    if #[cfg(target_os = \"android\")] {\n        unsafe fn _errno() -> *mut c_int {\n            libc::__errno()\n        }\n    } else if #[cfg(target_os = \"linux\")] {\n        unsafe fn _errno() -> *mut c_int {\n            libc::__errno_location()\n        }\n    }\n}\n\n#[allow(clippy::unnecessary_cast)]\npub fn errno() -> i32 {\n    unsafe { (*_errno()) as i32 }\n}\n"
  },
  {
    "path": "rust/src/error.rs",
    "content": "#![allow(unused)]\n\nuse std::{any::Any, str::Utf8Error};\n\n#[derive(Debug)]\npub enum Error {\n    // Only possible uname error: \"buf is invalid\"\n    UnameFailed,\n    ProcessNotFound(&'static str),\n    InvalidPidSupplied,\n    ProcessGroupNotFound,\n    InvalidSignal,\n    Io {\n        reason: String,\n    },\n    Daemonize {\n        error: daemonize::Error,\n    },\n    #[allow(unused)]\n    Unicode {\n        error: Utf8Error,\n    },\n    NoPermission,\n\n    // mlockall-specific errors\n    CouldNotLockMemory,\n    TooMuchMemoryToLock,\n    InvalidFlags,\n    // Should not happen but better safe than sorry\n    UnknownMlockall,\n    UnknownKill,\n    UnknownGetpguid,\n\n    #[cfg(feature = \"glob-ignore\")]\n    GlobPattern {\n        error: glob::PatternError,\n    },\n\n    // Errors that are likely impossible to happen\n    InvalidLinuxVersion,\n    MalformedStatm,\n    MalformedPressureFile,\n    ParseInt,\n    ParseFloat,\n    SysConfFailed,\n    SysInfoFailed,\n}\n\npub type Result<T> = std::result::Result<T, Error>;\n\nimpl From<std::io::Error> for Error {\n    fn from(err: std::io::Error) -> Self {\n        Self::Io {\n            reason: err.to_string(),\n        }\n    }\n}\n\nimpl From<std::num::ParseIntError> for Error {\n    fn from(_: std::num::ParseIntError) -> Self {\n        Self::ParseInt\n    }\n}\n\nimpl From<std::num::ParseFloatError> for Error {\n    fn from(_: std::num::ParseFloatError) -> Self {\n        Self::ParseFloat\n    }\n}\n\nimpl From<daemonize::Error> for Error {\n    fn from(error: daemonize::Error) -> Self {\n        Self::Daemonize { error }\n    }\n}\n\nimpl From<std::str::Utf8Error> for Error {\n    fn from(error: std::str::Utf8Error) -> Self {\n        Self::Unicode { error }\n    }\n}\n\n#[cfg(feature = \"glob-ignore\")]\nimpl From<glob::PatternError> for Error {\n    fn from(error: glob::PatternError) -> Self {\n        Self::GlobPattern { error }\n    }\n}\n"
  },
  {
    "path": "rust/src/kill.rs",
    "content": "use std::fs;\nuse std::time::Duration;\nuse std::time::Instant;\n\nuse libc::kill;\nuse libc::{EINVAL, EPERM, ESRCH, SIGKILL, SIGTERM};\n\nuse crate::errno::errno;\nuse crate::error::{Error, Result};\nuse crate::process::Process;\nuse crate::{cli, utils};\n\npub fn choose_victim(\n    proc_buf: &mut [u8],\n    buf: &mut [u8],\n    args: &cli::CommandLineArgs,\n) -> Result<Process> {\n    let now = Instant::now();\n\n    // `args` is currently only used when checking for unkillable patterns\n    #[cfg(not(feature = \"glob-ignore\"))]\n    let _ = args;\n\n    let mut processes = fs::read_dir(\"/proc/\")?\n        .filter_map(|e| e.ok())\n        .filter_map(|entry| entry.file_name().to_str()?.trim().parse::<u32>().ok())\n        .filter(|pid| *pid > 1)\n        .filter_map(|pid| Process::from_pid(pid, proc_buf).ok());\n\n    let first_process = processes.next();\n    if first_process.is_none() {\n        // Likely an impossible scenario but we found no process to kill!\n        return Err(Error::ProcessNotFound(\"choose_victim\"));\n    }\n\n    let mut victim = first_process.unwrap();\n    // TODO: find another victim if victim.vm_rss_kib() fails here\n    let mut victim_vm_rss_kib = victim.vm_rss_kib(buf)?;\n\n    for process in processes {\n        if victim.oom_score > process.oom_score {\n            // Our current victim is less innocent than the process being analysed\n            continue;\n        }\n\n        #[cfg(feature = \"glob-ignore\")]\n        {\n            if let Some(patterns) = &args.ignored {\n                if matches!(process.is_unkillable(buf, patterns), Ok(true)) {\n                    continue;\n                }\n            }\n        }\n\n        let cur_vm_rss_kib = process.vm_rss_kib(buf)?;\n        if cur_vm_rss_kib == 0 {\n            // Current process is a kernel thread\n            continue;\n        }\n\n        if process.oom_score == victim.oom_score && cur_vm_rss_kib <= victim_vm_rss_kib {\n            continue;\n        }\n\n        let cur_oom_score_adj = match process.oom_score_adj(buf) {\n            Ok(oom_score_adj) => oom_score_adj,\n            // TODO: warn that this error happened\n            Err(_) => continue,\n        };\n\n        if cur_oom_score_adj == -1000 {\n            // Follow the behaviour of the standard OOM killer: don't kill processes with oom_score_adj equals to -1000\n            continue;\n        }\n\n        // eprintln!(\"[DBG] New victim with PID={}!\", process.pid);\n        victim = process;\n        victim_vm_rss_kib = cur_vm_rss_kib;\n    }\n\n    println!(\"[LOG] Found victim in {} secs.\", now.elapsed().as_secs());\n    println!(\n        \"[LOG] Victim => pid: {}, comm: {}, oom_score: {}\",\n        victim.pid,\n        victim.comm(buf).unwrap_or(\"unknown\").trim(),\n        victim.oom_score\n    );\n\n    Ok(victim)\n}\n\npub fn kill_process(pid: i32, signal: i32) -> Result<()> {\n    let res = unsafe { kill(pid, signal) };\n\n    if res == -1 {\n        return Err(match errno() {\n            // An invalid signal was specified\n            EINVAL => Error::InvalidSignal,\n            // Calling process doesn't have permission to send signals to any\n            // of the target processes\n            EPERM => Error::NoPermission,\n            // The target process or process group does not exist.\n            ESRCH => Error::ProcessNotFound(\"kill\"),\n            _ => Error::UnknownKill,\n        });\n    }\n\n    Ok(())\n}\n\npub fn kill_process_group(process: Process) -> Result<()> {\n    let pid = process.pid;\n\n    let pgid = utils::get_process_group(pid as i32)?;\n\n    // TODO: kill and wait\n    let _ = kill_process(-pgid, SIGTERM);\n\n    Ok(())\n}\n\n/// Tries to kill a process and wait for it to exit\n/// Will first send the victim a SIGTERM and escalate to SIGKILL if necessary\n/// Returns Ok(true) if the victim was successfully terminated\npub fn kill_and_wait(process: Process) -> Result<bool> {\n    let pid = process.pid;\n    let now = Instant::now();\n\n    let _ = kill_process(pid as i32, SIGTERM);\n\n    let half_a_sec = Duration::from_secs_f32(0.5);\n    let mut sigkill_sent = false;\n\n    for _ in 0..20 {\n        std::thread::sleep(half_a_sec);\n        if !process.is_alive() {\n            println!(\"[LOG] Process with PID {} has exited.\\n\", pid);\n            return Ok(true);\n        }\n        if !sigkill_sent {\n            let _ = kill_process(pid as i32, SIGKILL);\n            sigkill_sent = true;\n            println!(\n                \"[LOG] Escalated to SIGKILL after {} nanosecs\",\n                now.elapsed().as_nanos()\n            );\n        }\n    }\n\n    Ok(false)\n}\n"
  },
  {
    "path": "rust/src/linux_version.rs",
    "content": "use std::{cmp::Ordering, fmt::Display};\n\n#[derive(Debug, PartialEq, Eq)]\npub struct LinuxVersion {\n    pub major: u8,\n    pub minor: u8,\n}\n\nimpl LinuxVersion {\n    /// Given a release string (e.g. as given by `uname -r`), attempt\n    /// to extract the major and minor values of the Linux version\n    pub fn from_str(release: &str) -> Option<Self> {\n        // The position of the first dot in the 'release' string\n        let dot_idx = release.find('.')?;\n\n        let (major, minor): (&str, &str) = release.split_at(dot_idx);\n\n        let major: u8 = major.parse().ok()?;\n\n        // Eat the leading dot in front of minor\n        let minor = &minor[1..];\n        let dot_idx = minor.find('.')?;\n\n        let minor: u8 = minor[0..dot_idx].parse().ok()?;\n\n        Some(Self { major, minor })\n    }\n}\n\nimpl Display for LinuxVersion {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        let (major, minor) = (self.major, self.minor);\n        write!(f, \"Linux {major}.{minor}\")\n    }\n}\n\nimpl PartialOrd for LinuxVersion {\n    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {\n        match self.major.partial_cmp(&other.major) {\n            Some(Ordering::Equal) => {}\n            ord => return ord,\n        }\n\n        self.minor.partial_cmp(&other.minor)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    use crate::linux_version::LinuxVersion;\n\n    #[test]\n    fn should_be_able_to_parse_linux_versions() {\n        assert_eq!(\n            LinuxVersion::from_str(\"5.16.18-1-MANJARO\").unwrap(),\n            LinuxVersion {\n                major: 5,\n                minor: 16\n            }\n        );\n\n        assert_eq!(\n            LinuxVersion::from_str(\"3.8.3-Fedora\").unwrap(),\n            LinuxVersion { major: 3, minor: 8 }\n        );\n    }\n\n    #[test]\n    fn should_be_able_to_compare_linux_versions() {\n        assert!(LinuxVersion::from_str(\"3.8.3\") >= LinuxVersion::from_str(\"3.6.9\"));\n\n        // We do not require PATCH accuracy\n        assert!(\n            LinuxVersion::from_str(\"3.8.3\").unwrap() == LinuxVersion::from_str(\"3.8.9\").unwrap()\n        );\n\n        assert!(\n            LinuxVersion::from_str(\"5.8.3\").unwrap() > LinuxVersion::from_str(\"3.13.9\").unwrap()\n        );\n        assert!(\n            LinuxVersion::from_str(\"5.8.3\").unwrap() < LinuxVersion::from_str(\"5.13.9\").unwrap()\n        );\n        assert!(\n            LinuxVersion::from_str(\"5.8.3\").unwrap() > LinuxVersion::from_str(\"4.20.0\").unwrap()\n        );\n        assert!(\n            LinuxVersion::from_str(\"4.21.0\").unwrap() > LinuxVersion::from_str(\"4.20.0\").unwrap()\n        );\n        assert!(\n            LinuxVersion::from_str(\"4.15.0\").unwrap() < LinuxVersion::from_str(\"4.20.0\").unwrap()\n        );\n    }\n}\n"
  },
  {
    "path": "rust/src/main.rs",
    "content": "// use uname::Uname;\n\nuse std::ops::Not;\n\nuse linux_version::LinuxVersion;\nuse uname::Uname;\n\nuse crate::{error::Error, memory::lock_memory_pages, monitor::Monitor};\n\nmod cli;\nmod daemon;\nmod errno;\nmod error;\nmod kill;\nmod linux_version;\nmod memory;\nmod monitor;\nmod process;\nmod uname;\nmod utils;\n\n/// The first Linux version in which PSI information became available\nconst LINUX_4_20: LinuxVersion = LinuxVersion {\n    major: 4,\n    minor: 20,\n};\n\nfn main() -> error::Result<()> {\n    let args: cli::CommandLineArgs = argh::from_env();\n    let should_daemonize = args.no_daemon.not();\n\n    // Show uname info and return the Linux version running\n    {\n        let ensure_msg = \"Ensure you're running at least Linux 4.20\";\n        let uname = Uname::new()?;\n        uname.print_info()?;\n\n        match uname.parse_version() {\n            Ok(version) => {\n                if version < LINUX_4_20 {\n                    eprintln!(\n                        \"{version} does not meet minimum requirements for bustd!\\n{ensure_msg}\"\n                    );\n                    return Err(Error::InvalidLinuxVersion);\n                }\n            }\n            Err(_) => {\n                eprintln!(\"Failed to parse Linux version!\\n{ensure_msg}\");\n            }\n        }\n\n        if let Ok(version) = uname.parse_version() {\n            if version < LINUX_4_20 {\n                eprintln!(\"{version} does not meet minimum requirements for bustd!\\n{ensure_msg}\");\n                return Err(Error::InvalidLinuxVersion);\n            }\n        } else {\n            eprintln!(\"Failed to parse Linux version!\\n{ensure_msg}\");\n        }\n    };\n\n    // In order to correctly use `mlockall`, we'll try our best to avoid heap allocations and\n    // reuse these buffers right here, even though it makes the code less readable.\n    // Buffer specific to process creation\n    let proc_buf = [0_u8; 50];\n\n    // Buffer for anything else\n    let buf = [0_u8; 100];\n\n    if should_daemonize {\n        // Daemonize current process\n        println!(\"\\nStarting daemonization process!\");\n        daemon::daemonize()?;\n    }\n\n    // Attempt to lock the memory pages mapped to the daemon\n    // in order to avoid being sent to swap when the system\n    // memory is stressed\n    if let Err(err) = lock_memory_pages() {\n        eprintln!(\"Failed to lock memory pages: {:?}. Continuing anyway.\", err);\n    } else {\n        // Save this on both bustd.out and bustd.err\n        println!(\"Memory pages locked!\");\n        eprintln!(\"Memory pages locked!\");\n    }\n\n    Monitor::new(proc_buf, buf, args)?.poll()\n}\n"
  },
  {
    "path": "rust/src/memory/mem_info.rs",
    "content": "use std::{fmt, mem};\n\nuse libc::sysinfo;\n\nuse crate::{\n    error::{Error, Result},\n    utils::bytes_to_megabytes,\n};\n\n#[derive(Debug, Default)]\npub struct MemoryInfo {\n    pub total_ram_mb: u64,\n    pub total_swap_mb: u64,\n    pub available_ram_mb: u64,\n    pub available_swap_mb: u64,\n    pub available_ram_percent: u8,\n    pub available_swap_percent: u8,\n}\n\n/// Simple wrapper over libc's sysinfo\nfn sys_info() -> Result<sysinfo> {\n    // Safety: the all-zero byte pattern is a valid sysinfo struct\n    let mut sys_info: sysinfo = unsafe { mem::zeroed() };\n\n    // Safety: sysinfo() is safe and must not fail when passed a valid reference\n    let ret_val = unsafe { libc::sysinfo(&mut sys_info) };\n\n    if ret_val != 0 {\n        // The only error that sysinfo() can have happens when\n        // it is supplied an invalid struct sysinfo pointer\n        //\n        // This error should really not happen during this function\n        return Err(Error::SysInfoFailed);\n    }\n\n    Ok(sys_info)\n}\n\nimpl MemoryInfo {\n    pub fn new() -> Result<MemoryInfo> {\n        let sysinfo {\n            mem_unit,\n            freeram,\n            totalram,\n            totalswap,\n            freeswap,\n            ..\n        } = sys_info()?;\n\n        let ratio = |x, y| ((x as f32 / y as f32) * 100.0) as u8;\n\n        let available_ram_mb = bytes_to_megabytes(freeram, mem_unit);\n        let total_ram_mb = bytes_to_megabytes(totalram, mem_unit);\n        let total_swap_mb = bytes_to_megabytes(totalswap, mem_unit);\n        let available_swap_mb = bytes_to_megabytes(freeswap, mem_unit);\n\n        let available_ram_percent = ratio(available_ram_mb, total_ram_mb);\n        let available_swap_percent = if total_swap_mb != 0 {\n            ratio(available_swap_mb, total_swap_mb)\n        } else {\n            0\n        };\n\n        Ok(MemoryInfo {\n            total_ram_mb,\n            available_ram_mb,\n            total_swap_mb,\n            available_swap_mb,\n            available_ram_percent,\n            available_swap_percent,\n        })\n    }\n}\n\nimpl fmt::Display for MemoryInfo {\n    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {\n        writeln!(f, \"Total RAM: {} MB\", self.total_ram_mb)?;\n        writeln!(\n            f,\n            \"Available RAM: {} MB ({}%)\",\n            self.available_ram_mb, self.available_ram_percent\n        )?;\n        writeln!(f, \"Total swap: {} MB\", self.total_swap_mb)?;\n        writeln!(\n            f,\n            \"Available swap: {} MB ({} %)\",\n            self.available_swap_mb, self.available_swap_percent\n        )\n    }\n}\n"
  },
  {
    "path": "rust/src/memory/mem_lock.rs",
    "content": "use libc::{c_int, mlockall};\nuse libc::{EAGAIN, EINVAL, ENOMEM, EPERM};\nuse libc::{MCL_CURRENT, MCL_FUTURE};\n\nuse crate::errno::errno;\nuse crate::error::{Error, Result};\n\nextern \"C\" {\n    pub static _MCL_ONFAULT: libc::c_int;\n}\n\npub fn _mlockall_wrapper(flags: c_int) -> Result<()> {\n    let err = unsafe { mlockall(flags) };\n    if err == 0 {\n        return Ok(());\n    }\n\n    // If err != 0, errno was set to describe the error that mlockall had\n    Err(match errno() {\n        // Some or all of the memory identified by the operation could not be locked when the call was made.\n        EAGAIN => Error::CouldNotLockMemory,\n        // The flags argument is zero, or includes unimplemented flags.\n        EINVAL => Error::InvalidFlags,\n\n        // Locking all of the pages currently mapped into the address space of the process\n        // would exceed an implementation-defined limit on the amount of memory\n        // that the process may lock.\n        ENOMEM => Error::TooMuchMemoryToLock,\n        // The calling process does not have appropriate privileges to perform the requested operation\n        EPERM => Error::NoPermission,\n        // Should not happen\n        _ => Error::UnknownMlockall,\n    })\n}\n\npub fn lock_memory_pages() -> Result<()> {\n    // TODO: check for _MCL_ONFAULT == -1\n\n    #[allow(non_snake_case)]\n    let MCL_ONFAULT: c_int = unsafe { _MCL_ONFAULT };\n    match _mlockall_wrapper(MCL_CURRENT | MCL_FUTURE | MCL_ONFAULT) {\n        Err(err) => {\n            eprintln!(\"First try at mlockall failed: {:?}\", err);\n        }\n        Ok(_) => return Ok(()),\n    }\n\n    _mlockall_wrapper(MCL_CURRENT | MCL_FUTURE)\n}\n"
  },
  {
    "path": "rust/src/memory/mod.rs",
    "content": "mod mem_info;\nmod mem_lock;\npub mod pressure;\n\npub use mem_info::MemoryInfo;\npub use mem_lock::lock_memory_pages;\n"
  },
  {
    "path": "rust/src/memory/pressure.rs",
    "content": "use std::fs::File;\nuse std::io::Read;\n\nuse crate::error::{Error, Result};\nuse crate::utils::str_from_bytes;\n\nmacro_rules! malformed {\n    () => {\n        Error::MalformedPressureFile\n    };\n}\n\n/// Returns the avg10 value in the `some` row of `/proc/pressure/memory`, which\n/// indicates the absolute stall time (in us) in which at least some tasks were stalled.\n///\n/// The data we're reading looks like:\n/// ```some avg10=0.00 avg60=0.00 avg300=0.00 total=0```\n///\npub fn pressure_some_avg10(buf: &mut [u8]) -> Result<f32> {\n    let mut file = File::open(\"/proc/pressure/memory\")?;\n    buf.fill(0);\n\n    // `buf` won't be large enough to fit all of `/proc/pressure/memory`\n    // but will be large enough to hold at least the first line, which has the data we want\n    file.read(buf)?;\n    let contents = str_from_bytes(buf)?;\n\n    let line = contents.lines().next().ok_or(malformed!())?;\n    let mut words = line.split_ascii_whitespace();\n    if let Some(indicator) = words.next() {\n        // This has to be the case but checking to be sure\n        if indicator == \"some\" {\n            let entry = words.next().ok_or(malformed!())?;\n\n            // The entry is of the form `avg10=0.00`\n            // We'll break this string in two in order to parse the value on the right-hand side\n            let equals_pos = entry.find('=').ok_or(malformed!())?;\n            let avg10 = entry.get(equals_pos + 1..).ok_or(malformed!())?;\n            let avg10: f32 = avg10.trim().parse()?;\n            return Ok(avg10);\n        }\n    }\n\n    Err(malformed!())\n}\n"
  },
  {
    "path": "rust/src/monitor.rs",
    "content": "use std::time::Duration;\n\nuse crate::cli::CommandLineArgs;\nuse crate::error::Result;\nuse crate::kill;\nuse crate::memory;\nuse crate::memory::MemoryInfo;\nuse crate::process::Process;\n\nenum MemoryStatus {\n    NearTerminal(f32),\n    Okay,\n}\n\npub struct Monitor {\n    memory_info: MemoryInfo,\n    proc_buf: [u8; 50],\n    buf: [u8; 100],\n    status: MemoryStatus,\n    args: CommandLineArgs,\n}\n\nimpl Monitor {\n    /// Determines how much oomf should sleep\n    /// This function is essentially a copy of how earlyoom calculates its sleep time\n    ///\n    /// Credits: https://github.com/rfjakob/earlyoom/blob/dea92ae67997fcb1a0664489c13d49d09d472d40/main.c#L365\n    /// MIT Licensed\n    pub fn sleep_time_ms(&self) -> Duration {\n        // Maximum expected memory fill rate as seen\n        // with `stress -m 4 --vm-bytes 4G`\n        const RAM_FILL_RATE: i64 = 6000;\n        // Maximum expected swap fill rate as seen\n        // with membomb on zRAM\n        const SWAP_FILL_RATE: i64 = 800;\n\n        // Maximum and minimum time to sleep, in ms.\n        const MIN_SLEEP: i64 = 100;\n        const MAX_SLEEP: i64 = 1000;\n\n        // TODO: make these percentages configurable by args./config. file\n        const RAM_TERMINAL_PERCENT: f64 = 10.;\n        const SWAP_TERMINAL_PERCENT: f64 = 10.;\n\n        let ram_headroom_kib = (self.memory_info.available_ram_percent as f64\n            - RAM_TERMINAL_PERCENT)\n            * (self.memory_info.total_ram_mb as f64 * 10.0);\n        let swap_headroom_kib = (self.memory_info.available_swap_percent as f64\n            - SWAP_TERMINAL_PERCENT)\n            * (self.memory_info.total_swap_mb as f64 * 10.0);\n\n        let ram_headroom_kib = i64::max(ram_headroom_kib as i64, 0);\n        let swap_headroom_kib = i64::max(swap_headroom_kib as i64, 0);\n\n        let time_to_sleep = ram_headroom_kib / RAM_FILL_RATE + swap_headroom_kib / SWAP_FILL_RATE;\n        let time_to_sleep = i64::min(time_to_sleep, MAX_SLEEP);\n        let time_to_sleep = i64::max(time_to_sleep, MIN_SLEEP);\n\n        Duration::from_millis(time_to_sleep as u64)\n    }\n\n    pub fn new(proc_buf: [u8; 50], mut buf: [u8; 100], args: CommandLineArgs) -> Result<Self> {\n        let memory_info = MemoryInfo::new()?;\n        let status = if memory_info.available_ram_percent <= 15 {\n            MemoryStatus::NearTerminal(memory::pressure::pressure_some_avg10(&mut buf)?)\n        } else {\n            MemoryStatus::Okay\n        };\n\n        Ok(Self {\n            memory_info,\n            proc_buf,\n            buf,\n            status,\n            args,\n        })\n    }\n\n    fn memory_is_low(&self) -> bool {\n        let terminal_psi = self.args.cutoff_psi;\n        matches!(self.status, MemoryStatus::NearTerminal(psi) if psi >= terminal_psi)\n    }\n\n    fn get_victim(&mut self) -> Result<Process> {\n        kill::choose_victim(&mut self.proc_buf, &mut self.buf, &self.args)\n    }\n\n    fn update_memory_stats(&mut self) -> Result<()> {\n        self.memory_info = memory::MemoryInfo::new()?;\n        self.status = if self.memory_info.available_ram_percent <= 15 {\n            let psi = memory::pressure::pressure_some_avg10(&mut self.buf)?;\n            MemoryStatus::NearTerminal(psi)\n        } else {\n            MemoryStatus::Okay\n        };\n        Ok(())\n    }\n\n    fn free_up_memory(&mut self) -> Result<()> {\n        let victim = self.get_victim()?;\n\n        // TODO: is this necessary?\n        //\n        // Check for memory stats again to see if the\n        // low-memory situation was solved while\n        // we were searching for our victim\n        self.update_memory_stats()?;\n        if self.memory_is_low() {\n            if self.args.kill_pgroup {\n                kill::kill_process_group(victim)?;\n            } else {\n                kill::kill_and_wait(victim)?;\n            }\n        }\n        Ok(())\n    }\n\n    // Use the never type here whenever it reaches stable\n    #[allow(unreachable_code)]\n    pub fn poll(&mut self) -> Result<()> {\n        loop {\n            // Update our memory readings\n            self.update_memory_stats()?;\n            if self.memory_is_low() {\n                self.free_up_memory()?;\n            }\n\n            // Calculating the adaptive sleep time\n            let sleep_time = self.sleep_time_ms();\n            if self.args.verbose {\n                eprintln!(\"[adaptive-sleep] {}ms\", sleep_time.as_millis());\n            }\n\n            std::thread::sleep(sleep_time);\n        }\n        Ok(())\n    }\n}\n"
  },
  {
    "path": "rust/src/process.rs",
    "content": "use std::io::Read;\nuse std::io::Write;\n\nuse libc::getpgid;\n\nuse crate::{\n    error::{Error, Result},\n    utils::{self, str_from_bytes},\n};\n\n#[derive(Debug, Default)]\npub struct Process {\n    pub pid: u32,\n    pub oom_score: i16,\n}\n\nimpl Process {\n    pub fn from_pid(pid: u32, buf: &mut [u8]) -> Result<Self> {\n        let oom_score =\n            Self::oom_score_from_pid(pid, buf).or(Err(Error::ProcessNotFound(\"from_pid\")))?;\n        Ok(Self { pid, oom_score })\n    }\n\n    #[allow(dead_code)]\n    /// Returns the current process represented as a Process struct\n    /// Unused in the actual code but very often used when debugging\n    pub fn this(buf: &mut [u8]) -> Result<Self> {\n        let pid = unsafe { libc::getpid() } as u32;\n\n        Self::from_pid(pid, buf)\n    }\n\n    /// Return true if the process is alive\n    /// Could still return true if the process has exited but hasn't yet been reaped.\n    /// TODO: would it be better to check for /proc/<PID>/ in here?\n    pub fn is_alive_from_pid(pid: u32) -> bool {\n        // Safety: `getpgid` is memory safe\n        let group_id = unsafe { getpgid(pid as i32) };\n\n        group_id > 0\n    }\n\n    pub fn is_alive(&self) -> bool {\n        Self::is_alive_from_pid(self.pid)\n    }\n\n    pub fn comm<'a>(&self, buf: &'a mut [u8]) -> Result<&'a str> {\n        write!(&mut *buf, \"/proc/{}/comm\\0\", self.pid)?;\n        {\n            let mut file = utils::file_from_buffer(buf)?;\n            buf.fill(0);\n            let _ = file.read(buf)?;\n        }\n\n        str_from_bytes(buf)\n    }\n\n    pub fn oom_score_from_pid(pid: u32, buf: &mut [u8]) -> Result<i16> {\n        write!(&mut *buf, \"/proc/{}/oom_score\\0\", pid)?;\n        let contents = {\n            let mut file = utils::file_from_buffer(buf)?;\n            buf.fill(0);\n            let _ = file.read(buf)?;\n\n            str_from_bytes(buf)?.trim()\n        };\n\n        Ok(contents.parse()?)\n    }\n\n    /// Reads VmRSS from /proc/<PID>/statm\n    /// In order to match the VmRSS value in /proc/<PID>/status, we'll\n    /// multiply the number of pages in `statm` by the page size of our system and then convert\n    /// that value to KiB\n    pub fn vm_rss_kib(&self, buf: &mut [u8]) -> Result<i64> {\n        write!(&mut *buf, \"/proc/{}/statm\\0\", self.pid)?;\n        let mut columns = {\n            let mut file = utils::file_from_buffer(buf)?;\n            buf.fill(0);\n            let _ = file.read(buf)?;\n\n            str_from_bytes(buf)?.split_ascii_whitespace()\n        };\n        let vm_rss: i64 = columns.nth(1).ok_or(Error::MalformedStatm)?.parse()?;\n\n        let page_size = utils::page_size()?;\n\n        // Converting VM RSS to KiB\n        let vm_rss_kib = vm_rss * page_size / 1024;\n        Ok(vm_rss_kib)\n    }\n\n    #[cfg(feature = \"glob-ignore\")]\n    /// Checks if the process' name matches any of the given glob patterns\n    pub fn is_unkillable(&self, buf: &mut [u8], patterns: &[glob::Pattern]) -> Result<bool> {\n        let comm = self.comm(buf)?.trim();\n        for pattern in patterns {\n            if pattern.matches(comm) {\n                println!(\n                    \"Skipping \\\"{}\\\" since it matches an unkillable pattern\",\n                    comm\n                );\n                return Ok(true);\n            }\n        }\n\n        Ok(false)\n    }\n\n    pub fn oom_score_adj(&self, buf: &mut [u8]) -> Result<i16> {\n        write!(&mut *buf, \"/proc/{}/oom_score_adj\\0\", self.pid)?;\n        let contents = {\n            let mut file = utils::file_from_buffer(buf)?;\n            buf.fill(0);\n            let _ = file.read(buf)?;\n\n            str_from_bytes(buf)?.trim()\n        };\n\n        Ok(contents.parse()?)\n    }\n}\n\n#[cfg(test)]\nmod tests {\n    // We'll use the Process struct from procfs\n    // in order to test our own Process struct.\n    //\n    // The reason we don't use `procfs` directly is\n    // because our implementation is considerably leaner.\n    use procfs;\n\n    // Returns the Process representing the\n    // process of the caller test\n    fn this() -> ([u8; 100], crate::process::Process) {\n        let mut buf = [0_u8; 100];\n        (buf, crate::process::Process::this(&mut buf).unwrap())\n    }\n\n    #[test]\n    fn comm() {\n        let (mut buf, this) = this();\n        let comm = this.comm(&mut buf).unwrap();\n\n        // We'll now represent the current process using\n        // the external `procfs` crate as well\n        let _this = procfs::process::Process::myself().unwrap();\n        let _stat = _this.stat().unwrap();\n        let _comm = _stat.comm;\n\n        assert_eq!(comm.trim(), _comm)\n    }\n\n    #[test]\n    fn oom_score() {\n        let (_, this) = this();\n\n        let _this = procfs::process::Process::myself().unwrap();\n\n        assert_eq!(this.oom_score, _this.oom_score().unwrap() as i16);\n    }\n\n    #[test]\n    fn pid() {\n        let (_, this) = this();\n\n        let _this = procfs::process::Process::myself().unwrap();\n\n        assert_eq!(this.pid as i32, _this.pid);\n    }\n}\n"
  },
  {
    "path": "rust/src/uname.rs",
    "content": "use std::ffi::CStr;\nuse std::mem;\n\nuse crate::error::{Error, Result};\nuse crate::linux_version::LinuxVersion;\nuse libc::{uname, utsname};\n\npub struct Uname {\n    uts_struct: utsname,\n}\n\nimpl Uname {\n    pub fn new() -> Result<Self> {\n        // Safety: libc::utsname is a bunch of char arrays and therefore\n        //         can be safely zeroed.\n        let mut uts_struct: utsname = unsafe { mem::zeroed() };\n\n        let ret_val = unsafe { uname(&mut uts_struct) };\n\n        // uname returns a negative number upon failure\n        if ret_val < 0 {\n            return Err(Error::UnameFailed);\n        }\n\n        Ok(Self { uts_struct })\n    }\n\n    pub fn print_info(&self) -> Result<()> {\n        // Safety: dereference of these raw pointers are safe since we know they're not NULL, since\n        // the buffers in struct utsname are all correctly allocated in the stack at this moment\n        let sysname = unsafe { CStr::from_ptr(self.uts_struct.sysname.as_ptr()) };\n        let hostname = unsafe { CStr::from_ptr(self.uts_struct.nodename.as_ptr()) };\n        let release = unsafe { CStr::from_ptr(self.uts_struct.release.as_ptr()) };\n        let arch = unsafe { CStr::from_ptr(self.uts_struct.machine.as_ptr()) };\n\n        let sysname = sysname.to_str()?;\n        let hostname = hostname.to_str()?;\n        let release = release.to_str()?;\n        let arch = arch.to_str()?;\n\n        println!(\"OS:           {}\", sysname);\n        println!(\"Hostname:     {}\", hostname);\n        println!(\"Version:      {}\", release);\n        println!(\"Architecture: {}\", arch);\n\n        Ok(())\n    }\n\n    pub fn parse_version(&self) -> Result<LinuxVersion> {\n        let release = unsafe { CStr::from_ptr(self.uts_struct.release.as_ptr()) };\n        let release = release.to_str()?;\n\n        LinuxVersion::from_str(release).ok_or(Error::InvalidLinuxVersion)\n    }\n}\n"
  },
  {
    "path": "rust/src/utils.rs",
    "content": "use std::ffi::OsStr;\nuse std::fs::File;\nuse std::os::unix::prelude::OsStrExt;\nuse std::path::Path;\nuse std::{ffi::CStr, mem, ptr, str};\n\nuse libc::_SC_PAGESIZE;\nuse libc::{getpgid, sysconf, EINVAL, EPERM, ESRCH};\nuse libc::{getpwuid_r, passwd};\nuse memchr::memchr;\n\nuse crate::errno::errno;\nuse crate::error::{Error, Result};\n\n/// Gets the effective user ID of the calling process\nfn effective_user_id() -> u32 {\n    // Safety: the POSIX Programmer's Manual states that\n    // geteuid will always be successful.\n    unsafe { libc::geteuid() }\n}\n\n/// Gets the process group of the process\n/// with the given PID.\npub fn get_process_group(pid: i32) -> Result<i32> {\n    let pgid = unsafe { getpgid(pid) };\n    if pgid == -1 {\n        return Err(match errno() {\n            EPERM => Error::NoPermission,\n            ESRCH => Error::ProcessGroupNotFound,\n            EINVAL => Error::InvalidPidSupplied,\n            _ => Error::UnknownGetpguid,\n        });\n    }\n\n    Ok(pgid)\n}\n\n/// Checks if the program is running with sudo permissions.\npub fn running_as_sudo() -> bool {\n    effective_user_id() == 0\n}\n\n/// Get the size of the system's memory page in bytes.\npub fn page_size() -> Result<i64> {\n    // _SC_PAGESIZE is defined in POSIX.1\n    // Safety: no memory unsafety can arise from `sysconf`\n    let page_size = unsafe { sysconf(_SC_PAGESIZE) };\n    if page_size == -1 {\n        return Err(Error::SysConfFailed);\n    }\n\n    #[allow(clippy::useless_conversion)]\n    // The type of page_size differs between architectures\n    // so we use .into() to convert to i64 if necessary\n    Ok(page_size.into())\n}\n\n/// Attempt to get the user's username from the system's password bank\npub fn get_username() -> Option<String> {\n    let mut buf = [0; 2048];\n    let mut result = ptr::null_mut();\n    let mut passwd: passwd = unsafe { mem::zeroed() };\n\n    let uid = effective_user_id();\n\n    let getpwuid_r_code =\n        unsafe { getpwuid_r(uid, &mut passwd, buf.as_mut_ptr(), buf.len(), &mut result) };\n\n    if getpwuid_r_code == 0 && !result.is_null() {\n        // If getpwuid_r succeeded, let's get the username from it\n        let username = unsafe { CStr::from_ptr(passwd.pw_name) };\n        let username = String::from_utf8_lossy(username.to_bytes());\n\n        return Some(username.into());\n    }\n\n    None\n}\n\nfn bytes_until_first_nil(buf: &[u8]) -> &[u8] {\n    let first_nul_idx = memchr(0, buf).unwrap_or(buf.len());\n\n    &buf[0..first_nul_idx]\n}\n\n/// Construct a string slice ranging from the first position to the position of the first nul byte\npub fn str_from_bytes(buf: &[u8]) -> Result<&str> {\n    let bytes = bytes_until_first_nil(buf);\n\n    Ok(str::from_utf8(bytes)?)\n}\n\nfn path_from_bytes(buf: &[u8]) -> &Path {\n    let bytes = bytes_until_first_nil(buf);\n\n    Path::new(OsStr::from_bytes(bytes))\n}\n\n/// Given a slice of bytes, try to interpret it as a file path and open the corresponding file.\npub fn file_from_buffer(buf: &[u8]) -> Result<File> {\n    let path = path_from_bytes(buf);\n    let file = File::open(path)?;\n    Ok(file)\n}\n\npub fn bytes_to_megabytes(bytes: impl Into<u64>, mem_unit: impl Into<u64>) -> u64 {\n    const B_TO_MB: u64 = 1000 * 1000;\n    bytes.into() / B_TO_MB * mem_unit.into()\n}\n\n#[cfg(test)]\nmod tests {\n    use super::str_from_bytes;\n\n    #[test]\n    fn should_construct_string_slice_from_bytes() {\n        assert_eq!(str_from_bytes(b\"ABC\\0\").unwrap(), \"ABC\");\n        assert_eq!(str_from_bytes(b\"ABC\\0abc\").unwrap(), \"ABC\");\n    }\n}\n"
  },
  {
    "path": "tools/.gitignore",
    "content": "mem-eater\n"
  },
  {
    "path": "tools/mem-eater.c",
    "content": "// `bustd`'s memory eater\n\n#include <stdio.h>\n#include <time.h>\n#include <unistd.h>\n#include <stdbool.h>\n#include <stdlib.h>\n#include <string.h>\n\nstruct free_mem_s {\n    unsigned available_mem_mib;\n    unsigned available_swap_mib;\n};\n\ntypedef struct free_mem_s free_mem_t;\n\nvoid display(free_mem_t * mem, float psi) {\n    printf(\"\\rFree RAM: %d MiB. Free swap: %d MiB. PSI: %0.2f\", mem->available_mem_mib, mem->available_swap_mib, psi);\n    fflush(stdout);\n}\n\nfloat memory_pressure_some_avg_10(void) {\n    FILE * memory_pressure = fopen(\"/proc/pressure/memory\", \"r\");\n    if(!memory_pressure) {\n        perror(\"/proc/pressure/memory. Exiting.\\n\");\n        fclose(memory_pressure);\n        _exit(1);\n    }\n\n    float psi;\n\n    if (EOF == fscanf(memory_pressure, \"some avg10=%f\", &psi)) {\n        perror(\"Failed to read memory pressure values. Exiting.\\n\");\n        fclose(memory_pressure);\n        _exit(1);\n    }\n\n    fclose(memory_pressure);\n    return psi;\n}\n\nfree_mem_t poll_free_mem(void) {\n    FILE * meminfo = fopen(\"/proc/meminfo\", \"r\");\n    if(!meminfo) {\n        fprintf(stderr, \"/proc/meminfo not found. Exiting.\\n\");\n        fclose(meminfo);\n        _exit(1);\n    }\n\n    char line[256];\n    bool avail_mem_read = false;\n    bool avail_swap_read = false;\n    free_mem_t free_mem;\n\n    while((!avail_mem_read || !avail_swap_read) && fgets(line, sizeof(line), meminfo))\n    {\n        int val;\n        if(sscanf(line, \"MemAvailable: %d kB\", &val) == 1)\n        {\n            avail_mem_read = true;\n            free_mem.available_mem_mib = (unsigned) val / 1024;\n        }\n\n        if(sscanf(line, \"SwapFree: %d kB\", &val) == 1)\n        {\n            avail_swap_read = true;\n            free_mem.available_swap_mib = (unsigned) val / 1024;\n        }\n    }\n\n    for (int i = 0; i < 100; i++) {\n        putchar(' ');\n    }\n\n    if (!avail_swap_read || !avail_mem_read) {\n        fprintf(stderr, \"failed to read available system memory or swap amounts. Exiting.\\n\");\n        fclose(meminfo);\n        _exit(1);\n    }\n\n    fclose(meminfo);\n    return free_mem;\n}\n\nint main(void) {\n    time_t start, now;\n    float time_left = 4.0;\n\n    time(&start);\n\n    while(time_left > 0.0) {\n    \ttime(&now);\n        time_left = 4.0 - difftime(now, start);\n\n    \tprintf(\"\\rmem-eater will start consuming system memory in: %.2f secs. Press Ctrl+C if you don't want that to happen.\", time_left);\n    \tfflush(stdout);\n    \tusleep(20);\n    }\n\n    while(1)\n    {\n        free_mem_t free_mem = poll_free_mem();\n        float psi = memory_pressure_some_avg_10();\n        display(&free_mem, psi);\n        void *m = malloc(1024*1024);\n        memset(m,0,1024*1024);\n    }\n\n\n\treturn 0;\n}\n"
  },
  {
    "path": "zig/.gitignore",
    "content": "zig-cache/\nzig-out/\n"
  },
  {
    "path": "zig/LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Vinícius Miguel\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "zig/README.md",
    "content": ":warning: This will be marged with the original [bustd](https://github.com/vrmiguel/bustd) repository.\n\n# `buztd`: Available memory or bust!\n\n`buztd` is a lightweight process killer daemon for out-of-memory scenarios for Linux!\n\nThis particular project is a Zig version of the [original `bustd` project](https://github.com/vrmiguel/bustd).\n\n## Features\n\n### Extremely thin memory usage\n\nThe Zig version of `bustd` makes no heap allocations and relies solely on a single 128-byte buffer in the stack for all its allocation needs. \n\n### Small CPU usage\n\nMuch like `earlyoom` and `nohang`, `buztd` uses adaptive sleep times during its memory polling. \n\nUnlike these two, however, `buztd` does not read from `/proc/meminfo`, instead opting for the `sysinfo` syscall.\n\nThis approach has its up- and downsides. The amount of free RAM that `sysinfo` reads does not account for cached memory, while `MemAvailable` in `/proc/meminfo` does.\n\nHowever, the `sysinfo` syscall is one order of magnitude faster than parsing `/proc/meminfo`, at least according to [this kernel patch](https://sourceware.org/legacy-ml/libc-alpha/2015-08/msg00512.html) (granted, from 2015).\n\nAs `buztd` can't solely rely on the free RAM readings of `sysinfo`, we check for memory stress through [Pressure Stall Information](https://www.kernel.org/doc/html/v5.8/accounting/psi.html).\n\nMore on that below.\n\n### `buztd` will try to lock all pages mapped into its address space\n\nMuch like `earlyoom`, `buztd` uses [`mlockall`](https://www.ibm.com/docs/en/aix/7.2?topic=m-mlockall-munlockall-subroutine) to avoid being sent to swap, which allows the daemon to remain responsive even when the system memory is under heavy load and susceptible to [thrashing](https://en.wikipedia.org/wiki/Thrashing_(computer_science)).\n\n### Checks for Pressure Stall Information\n\nThe Linux kernel, since version 4.20 (and built with `CONFIG_PSI=y`), presents canonical new pressure metrics for memory, CPU, and IO.\nIn the words of [Facebook Incubator](https://facebookmicrosites.github.io/psi/docs/overview):\n\n```\nPSI stats are like barometers that provide fair warning of impending resource \nshortages, enabling you to take more proactive, granular, and nuanced steps \nwhen resources start becoming scarce.\n```\n\nMore specifically, `buztd` checks for how long, in microseconds, processes have stalled in the last 10 seconds. By default, `buztd` will kill a process when processes have stalled for 25 microseconds in the last ten seconds.\n\nExample:\n```\n   some avg10=0.00 avg60=0.00 avg300=0.00 total=11220657\n   full avg10=0.00 avg60=0.00 avg300=0.00 total=10947429\n```\n\nThese ratios are percentages of recent trends over ten, sixty, and  three hundred second windows.\n\nThe `some` row indicates the percentage of time n that given time frame in which _any_ process has stalled due to memory thrashing.\n\n`buztd` allows you to configure the value of `some avg10` in which, if surpassed, some process will be killed.\n\nThe ideal value for this cutoff varies a lot between systems.\n\nTry messing around with `tools/mem-eater.c` to guesstimate a value that works well for you.\n\n## Building\n\nRequirements:\n* [Zig 0.10](https://ziglang.org/)\n* Linux 4.20+ built with `CONFIG_PSI=y`\n\n```shell\ngit clone https://github.com/vrmiguel/buztd\ncd buztd\n\n# Choose which compilation mode you'd like:\nzig build -Drelease-fast # Turns on optimization and disables safety checks\nzig build -Drelease-safe # Turns on optimization and keeps safety checks\nzig build -Drelease-small # Turns on size optimizations and disables safety checks\n```\n\n## Configuration\n\nAs of the time of writing, this version of `buztd` offers no command-line argument parsing, but allows easy configuration through the `src/config.zig` file.\n\n\n```zig\n/// Sets whether or not buztd should daemonize\n/// itself. Don't use this if running buztd as a systemd\n/// service or something of the sort.\npub const should_daemonize: bool = false;\n\n/// Free RAM percentage figures below this threshold are considered to be near terminal, meaning \n/// that buztd will start to check for Pressure Stall Information whenever the\n/// free RAM figures go below this.\n/// However, this free RAM amount is what the sysinfo syscall gives us, which does not take in consideration\n/// reclaimable or cached pages. The true free RAM amount available to the OS is bigger than what it indicates.\npub const free_ram_threshold: u8 = 15;\n\n/// The Linux kernel presents canonical pressure metrics for memory, found in `/proc/pressure/memory`.\n/// Example:\n///    some avg10=0.00 avg60=0.00 avg300=0.00 total=11220657\n///    full avg10=0.00 avg60=0.00 avg300=0.00 total=10947429\n/// These ratios are percentages of recent trends over ten, sixty, and \n/// three hundred second windows. The `some` row indicates the percentage of time\n// in that given time frame in which _any_ process has stalled due to memory thrashing.\n///\n/// This value configured here is the value of `some avg10` in which, if surpassed, some \n/// process will be killed.\n///\n/// The ideal value for this cutoff varies a lot between systems.\n/// Try messing around with `tools/mem-eater.c` to guesstimate a value that works well for you.\npub const cutoff_psi: f32 = 0.05;\n\n/// Sets processes that buztd must never kill.\n/// The values expected here are the `comm` values of the process you don't want to have terminated.\n/// A comm-value is the filename of the executable truncated to 16 characters..\npub const unkillables = std.ComptimeStringMap(void, .{\n         .{ \"firefox\", void },\n         .{ \"rustc\", void },\n         .{ \"electron\", void },\n});\n\n\n/// If any error occurs, restarts the monitoring instead of exiting with an unsuccesful status code\npub const retry: bool = true;\n```\n\n\n"
  },
  {
    "path": "zig/build.zig",
    "content": "const std = @import(\"std\");\n\npub fn build(b: *std.build.Builder) void {\n    // Standard target options allows the person running `zig build` to choose\n    // what target to build for. Here we do not override the defaults, which\n    // means any target is allowed, and the default is native. Other options\n    // for restricting supported target set are available.\n    const target = b.standardTargetOptions(.{});\n\n    // Standard release options allow the person running `zig build` to select\n    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.\n    const mode = b.standardReleaseOptions();\n\n    const exe = b.addExecutable(\"buztd\", \"src/main.zig\");\n    exe.setTarget(target);\n    exe.setBuildMode(mode);\n    exe.linkLibC();\n    exe.install();\n\n    const run_cmd = exe.run();\n    run_cmd.step.dependOn(b.getInstallStep());\n    if (b.args) |args| {\n        run_cmd.addArgs(args);\n    }\n\n    const run_step = b.step(\"run\", \"Run the app\");\n    run_step.dependOn(&run_cmd.step);\n\n    const exe_tests = b.addTest(\"src/main.zig\");\n    exe_tests.setTarget(target);\n    exe_tests.setBuildMode(mode);\n\n    const test_step = b.step(\"test\", \"Run unit tests\");\n    test_step.dependOn(&exe_tests.step);\n}\n"
  },
  {
    "path": "zig/src/config.zig",
    "content": "//! The configuration file for buztd\nconst std = @import(\"std\");\n\n/// Sets whether or not buztd should daemonize\n/// itself. Don't use this if running buztd as a systemd\n/// service or something of the sort.\npub const should_daemonize: bool = false;\n\n/// Free RAM percentage figures below this threshold are considered to be near terminal, meaning\n/// that buztd will start to check for Pressure Stall Information whenever the\n/// free RAM figures go below this.\n/// However, this free RAM amount is what the sysinfo syscall gives us, which does not take in consideration\n/// reclaimable or cached pages. The true free RAM amount available to the OS is bigger than what it indicates.\npub const free_ram_threshold: u8 = 15;\n\n/// The Linux kernel presents canonical pressure metrics for memory, found in `/proc/pressure/memory`.\n/// Example:\n///    some avg10=0.00 avg60=0.00 avg300=0.00 total=11220657\n///    full avg10=0.00 avg60=0.00 avg300=0.00 total=10947429\n/// These ratios are percentages of recent trends over ten, sixty, and\n/// three hundred second windows. The `some` row indicates the percentage of time\n// in that given time frame in which _any_ process has stalled due to memory thrashing.\n///\n/// This value configured here is the value of `some avg10` in which, if surpassed, some\n/// process will be killed.\n///\n/// The ideal value for this cutoff varies a lot between systems.\n/// Try messing around with `tools/mem-eater.c` to guesstimate a value that works well for you.\npub const cutoff_psi: f32 = 0.05;\n\n/// Sets processes that buztd must never kill.\n/// The values expected here are the `comm` values of the process you don't want to have terminated.\n/// A comm-value is the filename of the executable truncated to 16 characters..\n///\n/// Example:\n/// pub const unkillables = std.ComptimeStringMap(void, .{\n///         .{ \"firefox\", void },\n///         .{ \"rustc\", void },\n///         .{ \"electron\", void },\n/// });\npub const unkillables = std.ComptimeStringMap(void, .{\n    // Ideally, don't kill the oomkiller\n    .{ \"buztd\", void },\n});\n\n/// If any error occurs, restarts the monitoring instead of exiting with an unsuccesful status code\npub const retry: bool = true;\n"
  },
  {
    "path": "zig/src/daemonize.zig",
    "content": "const std = @import(\"std\");\nconst os = std.os;\n\nconst unistd = @cImport({\n    @cInclude(\"unistd.h\");\n});\n\nconst signal = @cImport({\n    @cInclude(\"signal.h\");\n});\n\nconst stat = @cImport({\n    @cInclude(\"sys/stat.h\");\n});\n\n/// Any error that might come up during process daemonization\nconst DaemonizeError = error{FailedToSetSessionId} || os.ForkError;\n\nconst SignalHandler = struct {\n    fn ignore(sig: i32, info: *const os.siginfo_t, ctx_ptr: ?*const anyopaque) callconv(.C) void {\n        // Ignore the signal received\n        _ = sig;\n        _ = ctx_ptr;\n        _ = info;\n        _ = ctx_ptr;\n    }\n};\n\n/// Forks the current process and makes\n/// the parent process quit\nfn fork_and_keep_child() os.ForkError!void {\n    const is_parent_proc = (try os.fork()) != 0;\n    // Exit off of the parent process\n    if (is_parent_proc) {\n        os.exit(0);\n    }\n}\n\n// TODO:\n// * Add logging\n// * Chdir\n/// Daemonizes the calling process\npub fn daemonize() DaemonizeError!void {\n    try fork_and_keep_child();\n\n    if (unistd.setsid() < 0) {\n        return error.FailedToSetSessionId;\n    }\n\n    // Setup signal handling\n    var act = os.Sigaction{\n        .handler = .{ .sigaction = SignalHandler.ignore },\n        .mask = os.empty_sigset,\n        .flags = (os.SA.SIGINFO | os.SA.RESTART | os.SA.RESETHAND),\n    };\n    os.sigaction(signal.SIGCHLD, &act, null);\n    os.sigaction(signal.SIGHUP, &act, null);\n\n    // Fork yet again and keep only the child process\n    try fork_and_keep_child();\n\n    // Set new file permissions\n    _ = stat.umask(0);\n\n    var fd: u8 = 0;\n    // The maximum number of files a process can have open\n    // at any time\n    const max_files_opened = unistd.sysconf(unistd._SC_OPEN_MAX);\n    while (fd < max_files_opened) : (fd += 1) {\n        _ = unistd.close(fd);\n    }\n}\n\ntest \"fork_and_keep_child works\" {\n    const getpid = os.linux.getpid;\n    const expect = std.testing.expect;\n    const linux = std.os.linux;\n    const fmt = std.fmt;\n\n    const first_pid = getpid();\n    try fork_and_keep_child();\n\n    const new_pid = getpid();\n    // We should now be running on a new process\n    try expect(first_pid != new_pid);\n\n    var stat_buf: linux.Stat = undefined;\n    var buf = [_:0]u8{0} ** 128;\n\n    // Current process is alive (obviously)\n    _ = try fmt.bufPrint(&buf, \"/proc/{}/stat\", .{new_pid});\n\n    try expect(linux.stat(&buf, &stat_buf) == 0);\n\n    // Old process should now be dead\n    _ = try fmt.bufPrint(&buf, \"/proc/{}/stat\", .{first_pid});\n\n    // Give the OS some time to reap the old process\n    std.time.sleep(250_000);\n\n    try expect(\n    // Stat should now fail\n    linux.stat(&buf, &stat_buf) != 0);\n}\n"
  },
  {
    "path": "zig/src/main.zig",
    "content": "const std = @import(\"std\");\n\ntest \"imports\" {\n    _ = @import(\"pressure.zig\");\n    _ = @import(\"daemonize.zig\");\n    _ = @import(\"process.zig\");\n    _ = @import(\"memory.zig\");\n    _ = @import(\"monitor.zig\");\n    _ = @import(\"config.zig\");\n}\n\nconst pressure = @import(\"pressure.zig\");\nconst daemon = @import(\"daemonize.zig\");\nconst process = @import(\"process.zig\");\nconst memory = @import(\"memory.zig\");\nconst monitor = @import(\"monitor.zig\");\nconst config = @import(\"config.zig\");\nconst syscalls = @import(\"missing_syscalls.zig\");\nconst MCL = syscalls.MCL;\n\npub fn startMonitoring() anyerror!void {\n    if (config.should_daemonize) {\n        try daemon.daemonize();\n    }\n\n    var buffer: [128]u8 = undefined;\n\n    if (syscalls.mlockall(MCL.CURRENT | MCL.FUTURE | MCL.ONFAULT)) {\n        std.log.warn(\"Memory pages locked.\", .{});\n    } else |err| {\n        std.log.warn(\"Failed to lock memory pages: {}. Continuing.\", .{err});\n    }\n\n    var m = try monitor.Monitor.new(&buffer);\n    try m.poll();\n}\n\npub fn main() anyerror!void {\n    startMonitoring() catch |err| {\n        // If config.retry is set, get back up and running\n        if (config.retry) {\n            std.log.err(\"{s}. Continuing.\", .{err});\n            try main();\n        } else {\n            return err;\n        }\n    };\n}\n"
  },
  {
    "path": "zig/src/memory.zig",
    "content": "const syscalls = @import(\"missing_syscalls.zig\");\n\npub const MemoryInfo = struct {\n    const Self = @This();\n\n    total_ram_mb: u64,\n    total_swap_mb: u64,\n    available_ram_mb: u64,\n    available_swap_mb: u64,\n    available_ram_percent: u8,\n    available_swap_percent: u8,\n\n    fn bytes_to_megabytes(bytes: u64, mem_unit: u64) u64 {\n        const B_TO_MB: u64 = 1000 * 1000;\n        return bytes / B_TO_MB * mem_unit;\n    }\n\n    fn ratio(x: u64, y: u64) u8 {\n        const xf = @intToFloat(f32, x);\n        const yf = @intToFloat(f32, y);\n\n        const _ratio = (xf / yf) * 100.0;\n\n        return @floatToInt(u8, _ratio);\n    }\n\n    pub fn new() !Self {\n        var si: syscalls.SysInfo = undefined;\n        try syscalls.sysinfo(&si);\n\n        const mem_unit = @intCast(u64, si.mem_unit);\n\n        const available_ram_mb = bytes_to_megabytes(si.freeram, mem_unit);\n        const total_ram_mb = bytes_to_megabytes(si.totalram, mem_unit);\n        const total_swap_mb = bytes_to_megabytes(si.totalswap, mem_unit);\n        const available_swap_mb = bytes_to_megabytes(si.freeswap, mem_unit);\n        const available_ram_percent = ratio(available_ram_mb, total_ram_mb);\n        const available_swap_percent = blk: {\n            if (total_swap_mb != 0) {\n                break :blk ratio(available_swap_mb, total_swap_mb);\n            } else {\n                break :blk 0;\n            }\n        };\n\n        return Self{\n            .available_ram_mb = available_ram_mb,\n            .total_ram_mb = total_ram_mb,\n            .total_swap_mb = total_swap_mb,\n            .available_swap_mb = available_swap_mb,\n            .available_ram_percent = available_ram_percent,\n            .available_swap_percent = available_swap_percent,\n        };\n    }\n};\n"
  },
  {
    "path": "zig/src/missing_syscalls.zig",
    "content": "// Until mlockall and sysinfo are added to zig's stdlib\n\nconst std = @import(\"std\");\nconst os = std.os;\nconst l = std.os.linux;\n\n// Flag magic numbers\npub const MCL = struct {\n    pub const CURRENT = 1;\n    pub const FUTURE = 2;\n    pub const ONFAULT = 4;\n};\n\n// TODO: test if this works outside of x86_64\n/// Contains certain statistics on memory and swap usage, as well as the load average\npub const SysInfo = extern struct {\n    uptime: c_long,\n    loads: [3]c_ulong,\n    totalram: c_ulong,\n    freeram: c_ulong,\n    sharedram: c_ulong,\n    bufferram: c_ulong,\n    totalswap: c_ulong,\n    freeswap: c_ulong,\n    procs: c_ushort,\n    totalhigh: c_ulong,\n    freehigh: c_ulong,\n    mem_unit: c_int,\n    // pad\n    _f: [20 - 2 * @sizeOf(c_long) - @sizeOf(c_int)]u8,\n};\n\npub const MLockError = error{ CouldNotLock, SystemResources, PermissionDenied } || os.UnexpectedError;\n\nfn syscall_mlockall(flags: i32) usize {\n    return l.syscall1(.mlockall, @bitCast(usize, @as(isize, flags)));\n}\n\npub fn mlockall(flags: i32) MLockError!void {\n    const rc = l.getErrno(syscall_mlockall(flags));\n    switch (rc) {\n        .SUCCESS => return,\n        .AGAIN => return error.CouldNotLock,\n        .PERM => return error.PermissionDenied,\n        .NOMEM => return error.SystemResources,\n        .INVAL => unreachable,\n        else => |err| return os.unexpectedErrno(err),\n    }\n}\n\nfn syscall_sysinfo(info: *SysInfo) usize {\n    return l.syscall1(.sysinfo, @ptrToInt(info));\n}\n\npub fn sysinfo(info: *SysInfo) os.UnexpectedError!void {\n    const rc = l.getErrno(syscall_sysinfo(info));\n    switch (rc) {\n        .SUCCESS => return,\n        .FAULT => unreachable,\n        else => |err| return os.unexpectedErrno(err),\n    }\n}\n"
  },
  {
    "path": "zig/src/monitor.zig",
    "content": "const std = @import(\"std\");\nconst time = std.time;\nconst math = std.math;\n\nconst memory = @import(\"memory.zig\");\nconst pressure = @import(\"pressure.zig\");\nconst process = @import(\"process.zig\");\nconst config = @import(\"config.zig\");\n\nconst MemoryStatusTag = enum {\n    ok,\n    near_terminal,\n};\n\nconst MemoryStatus = union(MemoryStatusTag) {\n    /// Memory is \"okay\": basically no risk of memory thrashing\n    ok: void,\n    /// Nearing the terminal PSI cutoff: memory thrashing is occurring or close to it. Holds the current PSI value.\n    near_terminal: f32,\n};\n\npub const Monitor = struct {\n    mem_info: memory.MemoryInfo,\n    /// Memory status as of last checked\n    status: MemoryStatus,\n    /// A pointer to a buffer of at least 128 bytes\n    buffer: []u8,\n    const Self = @This();\n\n    pub fn new(buffer: []u8) !Self {\n        var self = Self{\n            .mem_info = undefined,\n            .status = undefined,\n            .buffer = buffer,\n        };\n\n        try self.updateMemoryStats();\n\n        return self;\n    }\n\n    pub fn updateMemoryStats(self: *Self) !void {\n        self.mem_info = try memory.MemoryInfo.new();\n        self.status = blk: {\n            if (self.mem_info.available_ram_percent <= config.free_ram_threshold) {\n                const psi = try pressure.pressureSomeAvg10(self.buffer);\n                std.log.warn(\"read avg10: {}\", .{psi});\n                break :blk MemoryStatus{ .near_terminal = psi };\n            } else {\n                break :blk MemoryStatus.ok;\n            }\n        };\n    }\n\n    fn freeUpMemory(self: *Self) !void {\n        const victim_process = try process.findVictimProcess(self.buffer);\n\n        // Check for memory stats again to see if the\n        // low-memory situation was solved while\n        // we were searching for our victim\n        try self.updateMemoryStats();\n        if (self.isMemoryLow()) {\n            try victim_process.terminateSelf();\n        }\n    }\n\n    pub fn poll(self: *Self) !void {\n        while (true) {\n            if (self.isMemoryLow()) {\n                try self.freeUpMemory();\n            }\n\n            try self.updateMemoryStats();\n            const sleep_time = self.sleepTimeNs();\n            std.log.warn(\"sleeping for {}ms, {}% of RAM is free\", .{ sleep_time, self.mem_info.available_ram_percent });\n\n            // Convert ms to ns\n            time.sleep(sleep_time * 1000000);\n        }\n    }\n\n    /// Determines for how long buztd should sleep\n    /// This function is essentially a copy of how earlyoom calculates its sleep time\n    ///\n    /// Credits: https://github.com/rfjakob/earlyoom/blob/dea92ae67997fcb1a0664489c13d49d09d472d40/main.c#L365\n    /// MIT Licensed\n    fn sleepTimeNs(self: *const Self) u64 {\n        // Maximum expected memory fill rate as seen\n        // with `stress -m 4 --vm-bytes 4G`\n        const ram_fill_rate: i64 = 6000;\n        // Maximum expected swap fill rate as seen\n        // with membomb on zRAM\n        const swap_fill_rate: i64 = 800;\n\n        // Maximum and minimum sleep times (in ms)\n        const min_sleep: i64 = 100;\n        const max_sleep: i64 = 1000;\n\n        // TODO: make these percentages configurable by args./config. file\n        const ram_terminal_percent: f64 = 10.0;\n        const swap_terminal_percent: f64 = 10.0;\n\n        const f_ram_headroom_kib = (@intToFloat(f64, self.mem_info.available_ram_percent) - ram_terminal_percent) * (@intToFloat(f64, self.mem_info.total_ram_mb) * 10.0);\n        const f_swap_headroom_kib = (@intToFloat(f64, self.mem_info.available_swap_percent) - swap_terminal_percent) * (@intToFloat(f64, self.mem_info.total_swap_mb) * 10.0);\n\n        const i_ram_headroom_kib = math.max(0, @floatToInt(i64, f_ram_headroom_kib));\n        const i_swap_headroom_kib = math.max(0, @floatToInt(i64, f_swap_headroom_kib));\n\n        var time_to_sleep = @divFloor(i_ram_headroom_kib, ram_fill_rate) + @divFloor(i_swap_headroom_kib, swap_fill_rate);\n        time_to_sleep = math.min(time_to_sleep, max_sleep);\n        time_to_sleep = math.max(time_to_sleep, min_sleep);\n\n        return @intCast(u64, time_to_sleep);\n    }\n\n    fn isMemoryLow(self: *const Self) bool {\n        return switch (self.status) {\n            MemoryStatusTag.ok => false,\n            MemoryStatusTag.near_terminal => |psi| psi >= config.cutoff_psi,\n        };\n    }\n};\n"
  },
  {
    "path": "zig/src/pressure.zig",
    "content": "const std = @import(\"std\");\nconst os = std.os;\nconst fmt = std.fmt;\nconst fs = std.fs;\nconst mem = std.mem;\nconst assert = std.debug.assert;\n\npub fn pressureSomeAvg10(buffer: []u8) !f32 {\n    assert(buffer.len >= 128);\n\n    const memory_pressure_file = try fs.cwd().openFile(\"/proc/pressure/memory\", .{});\n    defer memory_pressure_file.close();\n\n    var memory_pressure_reader = memory_pressure_file.reader();\n\n    // Read \"some\"\n    const some = try memory_pressure_reader.readUntilDelimiter(buffer, ' ');\n    assert(mem.eql(u8, some, \"some\"));\n\n    // Read \"avg10=\" (`readUntilDelimiter` will eat the '=')\n    const avg10 = try memory_pressure_reader.readUntilDelimiter(buffer, '=');\n    assert(mem.eql(u8, avg10, \"avg10\"));\n\n    // Next up is the value we want\n    const avg10_value = try memory_pressure_reader.readUntilDelimiter(buffer, ' ');\n    std.log.info(\"avg10: {s}\", .{avg10_value});\n\n    return try fmt.parseFloat(f32, avg10_value);\n}\n"
  },
  {
    "path": "zig/src/process.zig",
    "content": "const std = @import(\"std\");\nconst csig = @cImport({\n    @cInclude(\"signal.h\");\n});\nconst unistd = @cImport({\n    @cInclude(\"unistd.h\");\n});\nconst config = @import(\"config.zig\");\nconst fs = std.fs;\nconst fmt = std.fmt;\nconst mem = std.mem;\nconst os = std.os;\nconst libc = std.c;\nconst time = std.time;\n\nfn signalToString(signal: u8) []const u8 {\n    return switch (signal) {\n        csig.SIGTERM => \"SIGTERM\",\n        csig.SIGKILL => \"SIGKILL\",\n        else => \"unknown\",\n    };\n}\n\npub const Process = struct {\n    pid: u32,\n    oom_score: i16,\n    buffer: []u8,\n\n    const Self = @This();\n\n    const ProcessError = error{ MalformedOomScore, MalformedOomScoreAdj, MalformedVmRss };\n\n    fn fromPid(pid: u32, buffer: []u8) !Self {\n        const oom_score = try oomScoreFromPid(pid, buffer);\n\n        return Self{ .pid = pid, .oom_score = oom_score, .buffer = buffer };\n    }\n\n    fn oomScoreFromPid(pid: u32, buffer: []u8) !i16 {\n        const path = try fmt.bufPrint(buffer, \"/proc/{}/oom_score\", .{pid});\n\n        // The file descriptor for the oom_score file of this process\n        const oom_score_fd = try os.open(path, os.O.RDONLY, 0);\n        defer os.close(oom_score_fd);\n\n        const bytes_read = try os.read(oom_score_fd, buffer);\n\n        const oom_score = parse(i16, buffer[0 .. bytes_read - 1]) orelse return error.MalformedOomScore;\n\n        return oom_score;\n    }\n\n    pub fn oomScoreAdj(self: *const Self) !i16 {\n        const path = try fmt.bufPrint(self.buffer, \"/proc/{}/oom_score_adj\", .{self.pid});\n\n        // The file descriptor for the oom_score file of this process\n        const oom_score_adj_fd = try os.open(path, os.O.RDONLY, 0);\n        defer os.close(oom_score_adj_fd);\n\n        const bytes_read = try os.read(oom_score_adj_fd, self.buffer);\n\n        const oom_score_adj = parse(i16, self.buffer[0 .. bytes_read - 1]) orelse return error.MalformedOomScoreAdj;\n\n        return oom_score_adj;\n    }\n\n    pub fn comm(self: *const Self) ![]u8 {\n        const path = try fmt.bufPrint(self.buffer, \"/proc/{}/comm\", .{self.pid});\n\n        // The file descriptor for the oom_score file of this process\n        const comm_fd = try os.open(path, os.O.RDONLY, 0);\n        defer os.close(comm_fd);\n\n        const bytes_read = try os.read(comm_fd, self.buffer);\n\n        return self.buffer[0 .. bytes_read - 1];\n    }\n\n    pub fn isAlive(self: *const Self) bool {\n        const group_id = unistd.getpgid(@intCast(c_int, self.pid));\n\n        return group_id > 0;\n    }\n\n    pub fn vmRss(self: *const Self) !usize {\n        var filename = try fmt.bufPrint(self.buffer, \"/proc/{}/statm\", .{self.pid});\n\n        var statm_file = try fs.cwd().openFile(filename, .{});\n        defer statm_file.close();\n        var statm_reader = statm_file.reader();\n\n        // Skip first field (total program size)\n        try statm_reader.skipUntilDelimiterOrEof(' ');\n        var rss_str = try statm_reader.readUntilDelimiter(self.buffer, ' ');\n\n        var ret = parse(usize, rss_str) orelse return error.MalformedVmRss;\n        return (ret * std.mem.page_size) / 1024;\n    }\n\n    pub fn signalSelf(self: *const Self, signal: u8) void {\n        // Don't warn `kill` failure if the process is no longer alive\n        if (0 != libc.kill(@intCast(i32, self.pid), signal) and self.isAlive()) {\n            std.log.warn(\"Failed to send {s} to process {}\", .{ signalToString(signal), self.pid });\n        } else {\n            std.log.warn(\"Successfully sent {s} to process {}\", .{ signalToString(signal), self.pid });\n        }\n    }\n\n    pub fn terminateSelf(self: Self) !void {\n        const quarter_sec_in_ns: u64 = 250000000;\n\n        self.signalSelf(csig.SIGTERM);\n\n        var attempt: u8 = 0;\n\n        while (attempt < 20) : (attempt += 1) {\n            time.sleep(quarter_sec_in_ns);\n            if (!self.isAlive()) {\n                std.log.warn(\"Process {} has exited.\", .{self.pid});\n                return;\n            }\n            // Escalate to sigkill\n            self.signalSelf(csig.SIGKILL);\n        }\n    }\n};\n\n/// Wrapper over fmt.parseInt which returns null\n/// in failure instead of an error\nfn parse(comptime T: type, buf: []const u8) ?T {\n    return fmt.parseInt(T, buf, 10) catch null;\n}\n\n/// Used to try to give LLVM hints on branch prediction.\n///\n/// I have no idea how effective this is in practice.\nfn coldNoOp() void {\n    @setCold(true);\n}\n\n/// Searches for a process to kill in order to\n/// free up memory\npub fn findVictimProcess(buffer: []u8) !Process {\n    var victim: Process = undefined;\n    var victim_vm_rss: usize = undefined;\n    var victim_is_undefined = true;\n\n    var timer = try time.Timer.start();\n\n    var proc_dir = try fs.cwd().openIterableDir(\"/proc\", .{ .access_sub_paths = false });\n    var proc_it = proc_dir.iterate();\n\n    while (try proc_it.next()) |proc_entry| {\n        // We're only interested in directories of /proc\n        if (proc_entry.kind != .Directory) {\n            continue;\n        } else {\n            // `/proc` usually has much more directories than it has files\n            coldNoOp();\n        }\n\n        // But we're not interested in files that don't relate to a PID\n        const pid = parse(u32, proc_entry.name) orelse continue;\n\n        // Don't consider killing the init system\n        if (pid <= 1) {\n            coldNoOp();\n            continue;\n        }\n\n        const process = try Process.fromPid(pid, buffer);\n\n        if (victim_is_undefined) {\n            // We're still reading the first process so a victim hasn't been chosen\n            coldNoOp();\n            victim = process;\n            victim_vm_rss = try victim.vmRss();\n            victim_is_undefined = false;\n            std.log.info(\"First victim set\", .{});\n        }\n\n        if (victim.oom_score > process.oom_score) {\n            // Our current victim is less innocent than the process being analysed\n            continue;\n        }\n\n        const victim_comm = try victim.comm();\n        if (config.unkillables.get(victim_comm) != null) {\n            // The current process was set as unkillable\n            continue;\n        }\n\n        const current_vm_rss = try process.vmRss();\n        if (current_vm_rss == 0) {\n            // Current process is a kernel thread\n            continue;\n        }\n\n        // TODO: recheck this\n        if (process.oom_score == victim.oom_score and current_vm_rss <= victim_vm_rss) {\n            continue;\n        }\n\n        const current_oom_score_adj = process.oomScoreAdj() catch {\n            std.log.warn(\"Failed to read adj. OOM score for PID {}. Continuing.\", .{process.pid});\n            continue;\n        };\n\n        if (current_oom_score_adj == -1000) {\n            // Follow the behaviour of the standard OOM killer: don't kill processes with oom_score_adj equals to -1000\n            continue;\n        }\n\n        victim = process;\n        victim_vm_rss = current_vm_rss;\n    }\n\n    const ns_elapsed = timer.read();\n    std.debug.print(\"Victim found in {} ns.: {s} with PID {} and OOM score {}\\n\", .{ ns_elapsed, try victim.comm(), victim.pid, victim.oom_score });\n\n    return victim;\n}\n"
  }
]