ptools 0.2.8

Utilities for inspecting Linux processes
use roff::{bold, roman, Roff};
use std::fs;
use std::path::Path;

struct Example<'a> {
    title: &'a str,
    description: &'a str,
    code: &'a str,
}

struct ManPage<'a> {
    name: &'a str,
    about: &'a str,
    description: &'a str,
    synopsis: &'a str,
    options: &'a [(&'a str, &'a str)],
    examples: &'a [Example<'a>],
    exit_status: &'a str,
    files: &'a str,
    see_also: &'a str,
    warnings: &'a str,
}

const DEFAULT_EXIT_STATUS: &str =
    "0 on success, non-zero if an error occurs (such as no such process, \
     permission denied, or invalid option).";

const DEFAULT_FILES: &str = "/proc/pid/*\tProcess information and control files.";

fn render_man_page(page: &ManPage, out_dir: &Path) {
    let version = env!("CARGO_PKG_VERSION");
    let upper_name = page.name.to_uppercase();
    let date_version = format!("{} {}", page.name, version);
    let mut roff = Roff::default();
    roff.control("TH", [upper_name.as_str(), "1", date_version.as_str()]);
    roff.control("SH", ["NAME"]);
    roff.text([roman(format!("{} - {}", page.name, page.about))]);
    roff.control("SH", ["SYNOPSIS"]);
    roff.text([bold(page.name), roman(format!(" {}", page.synopsis))]);
    roff.control("SH", ["DESCRIPTION"]);
    roff.text([roman(page.description)]);
    if !page.options.is_empty() {
        roff.control("SH", ["OPTIONS"]);
        for (flag, help) in page.options {
            roff.control("TP", []);
            roff.text([bold(*flag)]);
            roff.text([roman(*help)]);
        }
    }
    if !page.examples.is_empty() {
        roff.control("SH", ["EXAMPLES"]);
        for example in page.examples {
            roff.text([bold(example.title)]);
            roff.text([roman(example.description)]);
            roff.control("sp", [] as [&str; 0]);
            roff.control("nf", [] as [&str; 0]);
            roff.control("RS", ["4"]);
            for line in example.code.lines() {
                roff.text([roman(line)]);
            }
            roff.control("RE", [] as [&str; 0]);
            roff.control("fi", [] as [&str; 0]);
        }
    }
    if !page.exit_status.is_empty() {
        roff.control("SH", ["EXIT STATUS"]);
        roff.text([roman(page.exit_status)]);
    }
    if !page.files.is_empty() {
        roff.control("SH", ["FILES"]);
        for line in page.files.lines() {
            if let Some((path, desc)) = line.split_once('\t') {
                roff.control("TP", []);
                roff.text([roman(path)]);
                roff.text([roman(desc)]);
            } else {
                roff.text([roman(line)]);
            }
        }
    }
    if !page.warnings.is_empty() {
        roff.control("SH", ["WARNINGS"]);
        roff.text([roman(page.warnings)]);
    }
    if !page.see_also.is_empty() {
        roff.control("SH", ["SEE ALSO"]);
        roff.text([roman(page.see_also)]);
    }
    fs::write(out_dir.join(format!("{}.1", page.name)), roff.to_roff()).unwrap();
}

fn main() {
    let out_dir = Path::new("target/man");
    fs::create_dir_all(out_dir).unwrap();

    render_man_page(
        &ManPage {
            name: "pargs",
            about: "print process arguments",
            description: "Examine a target process and print arguments, environment variables \
                          and values, or the process auxiliary vector. \
                          The pauxv command is equivalent to running pargs with the -x option. \
                          The penv command is equivalent to running pargs with the -e option.",
            synopsis: "[-l] [-a|--args] [-e|--env] [-x|--auxv] PID...",
            options: &[
                (
                    "-l",
                    "Display the arguments as a single command line. The command line is \
                     printed in a manner suitable for interpretation by /bin/sh.",
                ),
                (
                    "-a, --args",
                    "Print process arguments as contained in /proc/pid/cmdline (default).",
                ),
                (
                    "-e, --env",
                    "Print process environment variables and values as contained in \
                     /proc/pid/environ.",
                ),
                (
                    "-x, --auxv",
                    "Print the process auxiliary vector as contained in /proc/pid/auxv.",
                ),
            ],
            examples: &[],
            exit_status: DEFAULT_EXIT_STATUS,
            files: DEFAULT_FILES,
            see_also: "pauxv(1), penv(1), pflags(1), proc(5)",
            warnings: "",
        },
        out_dir,
    );

    render_man_page(
        &ManPage {
            name: "pauxv",
            about: "print process auxiliary vector",
            description: "Examine a target process and print the auxiliary vector. \
                          This command is equivalent to running pargs with the -x option.",
            synopsis: "PID...",
            options: &[],
            examples: &[],
            exit_status: DEFAULT_EXIT_STATUS,
            files: DEFAULT_FILES,
            see_also: "pargs(1), penv(1), proc(5)",
            warnings: "",
        },
        out_dir,
    );

    render_man_page(
        &ManPage {
            name: "penv",
            about: "print process environment variables",
            description: "Examine a target process and print environment variables and values. \
                          This command is equivalent to running pargs with the -e option.",
            synopsis: "PID...",
            options: &[],
            examples: &[],
            exit_status: DEFAULT_EXIT_STATUS,
            files: DEFAULT_FILES,
            see_also: "pargs(1), pauxv(1), environ(7), proc(5)",
            warnings: "",
        },
        out_dir,
    );

    render_man_page(
        &ManPage {
            name: "pfiles",
            about: "report open file information",
            description: "Print fstat(2) and fcntl(2) information for all open files in each \
                          process. For network endpoints, provide local address information and \
                          peer address information when connected. For sockets, provide the \
                          socket type, socket options, and send and receive buffer sizes. Also \
                          print a path to the file when that information is available from \
                          /proc/pid/fd. This is not necessarily the same name used to open the \
                          file. In addition, print the current soft and hard RLIMIT_NOFILE \
                          limits and the process umask. See proc(5) for more information.",
            synopsis: "[-n] PID...",
            options: &[(
                "-n",
                "Set non-verbose mode. Do not display verbose information for each file \
                 descriptor. Instead, limit output to the information that the process \
                 would retrieve by applying fstat(2) to each of its file descriptors.",
            )],
            examples: &[],
            exit_status: DEFAULT_EXIT_STATUS,
            files: DEFAULT_FILES,
            see_also: "fstat(2), fcntl(2), proc(5)",
            warnings: "",
        },
        out_dir,
    );

    render_man_page(
        &ManPage {
            name: "psig",
            about: "list process signal actions",
            description: "List the signal actions and handlers of each process. For each \
                          signal, print whether the signal is caught, ignored, or handled by \
                          default, and whether the signal is blocked or pending. Real-time \
                          signals (SIGRTMIN through SIGRTMAX) are also displayed. Use \
                          pflags(1) to see more information about currently pending signals \
                          and signal masks.",
            synopsis: "PID...",
            options: &[],
            examples: &[],
            exit_status: DEFAULT_EXIT_STATUS,
            files: DEFAULT_FILES,
            see_also: "kill(1), pflags(1), signal(7), proc(5)",
            warnings: "",
        },
        out_dir,
    );

    render_man_page(
        &ManPage {
            name: "pflags",
            about: "print process status flags",
            description: "Print the data model, process flags, pending and held signals, \
                          and other status information for each process or specified threads \
                          in each process. For each thread, print its state, current system \
                          call (if any), and signal mask. If a thread has a non-empty signal \
                          mask, it will be printed.",
            synopsis: "PID[/TID]...",
            options: &[],
            examples: &[],
            exit_status: DEFAULT_EXIT_STATUS,
            files: DEFAULT_FILES,
            see_also: "psig(1), proc(5)",
            warnings: "",
        },
        out_dir,
    );

    render_man_page(
        &ManPage {
            name: "pstop",
            about: "stop processes",
            description: "Stop each process by sending SIGSTOP.",
            synopsis: "PID...",
            options: &[],
            examples: &[],
            exit_status: DEFAULT_EXIT_STATUS,
            files: DEFAULT_FILES,
            see_also: "prun(1), kill(1), proc(5)",
            warnings: "A process can do nothing while it is stopped. Stopping a heavily \
                       used process in a production environment, even for a short amount of \
                       time, can cause severe bottlenecks and even hangs of dependent \
                       processes, causing them to be unavailable to users. Because of this, \
                       stopping a process in a production environment should be avoided.",
        },
        out_dir,
    );

    render_man_page(
        &ManPage {
            name: "prun",
            about: "set stopped processes running",
            description: "Set running each process by sending SIGCONT (the inverse of pstop).",
            synopsis: "PID...",
            options: &[],
            examples: &[],
            exit_status: DEFAULT_EXIT_STATUS,
            files: DEFAULT_FILES,
            see_also: "pstop(1), kill(1), proc(5)",
            warnings: "",
        },
        out_dir,
    );

    render_man_page(
        &ManPage {
            name: "pwait",
            about: "wait for processes to terminate",
            description: "Wait for all of the specified processes to terminate. Unlike \
                          wait(1), the target processes do not need to be children of \
                          the calling process.",
            synopsis: "[-v] PID...",
            options: &[(
                "-v",
                "Verbose. Reports terminations to standard output. When the target \
                 process is a child of the calling process, the wait status is also \
                 displayed.",
            )],
            examples: &[],
            exit_status: DEFAULT_EXIT_STATUS,
            files: DEFAULT_FILES,
            see_also: "wait(1), proc(5)",
            warnings: "",
        },
        out_dir,
    );

    render_man_page(
        &ManPage {
            name: "ptree",
            about: "print process trees",
            description: "Print process trees containing the specified pids or users, with \
                          child processes indented from their respective parent processes. An \
                          argument of all digits is taken to be a process ID; otherwise it is \
                          assumed to be a user login name. The default is all processes.",
            synopsis: "[-ag] [pid|user]...",
            options: &[
                (
                    "-a, --all",
                    "All. Print all processes, including children of process ID 0.",
                ),
                (
                    "-g, --graph",
                    "Use line drawing characters. If the current locale is a UTF-8 \
                     locale, the UTF-8 line drawing characters are used, otherwise \
                     ASCII line drawing characters are used.",
                ),
            ],
            examples: &[
                Example {
                    title: "Example 1 Using ptree",
                    description: "The following example prints the process tree \
                                  (including children of process 0) for processes \
                                  which match the command name ssh:",
                    code: "\
$ ptree -a `pgrep ssh`
        1  /sbin/init
          100909  /usr/bin/sshd
            569150  /usr/bin/sshd
              569157  /usr/bin/sshd
                569159  -bash
                  569171  bash
                    569173  /usr/bin/bash
                      569193  bash",
                },
                Example {
                    title: "Example 2",
                    description: "The following example prints the process tree \
                                  (including children of process 0) for processes \
                                  which match the command name ssh with ASCII line \
                                  drawing characters:",
                    code: "\
$ ptree -ag `pgrep ssh`
        1  /sbin/init
        `-100909  /usr/bin/sshd
          `-569150  /usr/bin/sshd
            `-569157  /usr/bin/sshd
              `-569159  -bash
                `-569171  bash
                  `-569173  /usr/bin/bash
                    `-569193  bash",
                },
            ],
            exit_status: DEFAULT_EXIT_STATUS,
            files: DEFAULT_FILES,
            see_also: "pargs(1), pgrep(1), ps(1), proc(5)",
            warnings: "",
        },
        out_dir,
    );

    println!("cargo:rerun-if-changed=build.rs");
}