use roff::{bold, roman, Roff};
use std::fs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
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)],
operands: &'a [(&'a str, &'a str)],
examples: &'a [Example<'a>],
exit_status: &'a str,
files: &'a str,
notes: &'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.";
const CORE_OPERANDS: &[(&str, &str)] = &[
("pid", "Process ID list."),
(
"core",
"Process core file, as produced by systemd-coredump(8). The core file \
does not need to exist on disk; if it has been removed, the \
corresponding systemd journal entry will be used instead. See \
NOTES below.",
),
];
const CORE_NOTES: &str = "When a core file has been removed by systemd-tmpfiles(8) or \
by storage limits configured in coredump.conf(5), the \
systemd-coredump(8) journal entry for the crash may still be \
available. In this case, the path to the deleted core file \
can be passed as the core operand even though the file no \
longer exists on disk, and process metadata will be retrieved \
from the journal entry instead. Use coredumpctl(1) to obtain \
the path of a missing core file, e.g., \
coredumpctl list <name> -F COREDUMP_FILENAME.";
fn build_date() -> String {
let days = (SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
/ 86400) as i64;
let z = days + 719468;
let era = (if z >= 0 { z } else { z - 146096 }) / 146097;
let doe = (z - era * 146097) as u64;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe as i64 + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
const MONTHS: [&str; 12] = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
format!("{} {}", MONTHS[(m - 1) as usize], y)
}
fn render_man_page(page: &ManPage, out_dir: &Path) {
let version = env!("CARGO_PKG_VERSION");
let upper_name = page.name.to_uppercase();
let date = build_date();
let source = format!("{} {}", page.name, version);
let mut roff = Roff::default();
roff.control(
"TH",
[upper_name.as_str(), "1", date.as_str(), source.as_str()],
);
roff.control("SH", ["NAME"]);
roff.text([roman(format!("{} - {}", page.name, page.about))]);
roff.control("SH", ["SYNOPSIS"]);
let synopses: Vec<&str> = page.synopsis.split('\n').collect();
roff.text([bold(page.name), roman(format!(" {}", synopses[0]))]);
for syn in &synopses[1..] {
roff.control("br", [] as [&str; 0]);
roff.text([bold(page.name), roman(format!(" {}", syn))]);
}
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.operands.is_empty() {
roff.control("SH", ["OPERANDS"]);
for (name, desc) in page.operands {
roff.control("TP", []);
roff.text([bold(*name)]);
roff.text([roman(*desc)]);
}
}
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.notes.is_empty() {
roff.control("SH", ["NOTES"]);
roff.text([roman(page.notes)]);
}
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() {
pkg_config::Config::new()
.atleast_version("246")
.probe("libsystemd")
.expect("libsystemd not found (install systemd-devel or libsystemd-dev)");
pkg_config::probe_library("libdw")
.expect("libdw not found (install elfutils-devel or libdw-dev)");
let out_dir = Path::new("target/man");
fs::create_dir_all(out_dir).unwrap();
render_man_page(
&ManPage {
name: "pargs",
about: "print process arguments, environment variables, or auxiliary vector",
description: "Examine a target process or process core file \
and print arguments, environment variables and values, or the \
process auxiliary vector. \
The pauxv command is equivalent to running pargs(1) with the -x option. \
The penv command is equivalent to running pargs(1) with the -e option.",
synopsis: "[-l] [-a|--args] [-e|--env] [-x|--auxv] [pid | core]...",
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.",
),
],
operands: CORE_OPERANDS,
examples: &[],
exit_status: DEFAULT_EXIT_STATUS,
files: DEFAULT_FILES,
notes: CORE_NOTES,
see_also: "pauxv(1), penv(1), coredumpctl(1), proc(5)",
warnings: "",
},
out_dir,
);
render_man_page(
&ManPage {
name: "pauxv",
about: "print process auxiliary vector",
description: "Examine a target process or process core file \
and print the process auxiliary vector. \
This command is equivalent to running pargs(1) with the -x option.",
synopsis: "[pid | core]...",
options: &[],
operands: CORE_OPERANDS,
examples: &[],
exit_status: DEFAULT_EXIT_STATUS,
files: DEFAULT_FILES,
notes: CORE_NOTES,
see_also: "pargs(1), penv(1), coredumpctl(1), proc(5)",
warnings: "",
},
out_dir,
);
render_man_page(
&ManPage {
name: "penv",
about: "print process environment variables",
description: "Examine a target process or process core file \
and print environment variables and values. \
This command is equivalent to running pargs(1) with the -e option.",
synopsis: "[pid | core]...",
options: &[],
operands: CORE_OPERANDS,
examples: &[],
exit_status: DEFAULT_EXIT_STATUS,
files: DEFAULT_FILES,
notes: CORE_NOTES,
see_also: "pargs(1), pauxv(1), coredumpctl(1), environ(7), proc(5)",
warnings: "",
},
out_dir,
);
render_man_page(
&ManPage {
name: "pcred",
about: "print process credentials",
description: "Print the credentials (effective, real, saved, and filesystem \
UIDs and GIDs) of each process or process core file. By \
default, if the effective, real, saved-set, and filesystem \
user (group) IDs are identical, they are printed in condensed \
form as e/r/s/fsuid (e/r/s/fsgid); otherwise they are printed \
individually. Supplementary groups are also displayed.",
synopsis: "[-a] [pid | core]...",
options: &[(
"-a, --all",
"Report all credential information separately. By default, if the \
effective, real, saved-set, and filesystem user (group) IDs are \
identical, they are reported in condensed form.",
)],
operands: CORE_OPERANDS,
examples: &[],
exit_status: DEFAULT_EXIT_STATUS,
files: DEFAULT_FILES,
notes: CORE_NOTES,
see_also: "pfiles(1), coredumpctl(1), proc(5), credentials(7)",
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 or process core file. 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 | core]...",
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.",
)],
operands: CORE_OPERANDS,
examples: &[],
exit_status: DEFAULT_EXIT_STATUS,
files: DEFAULT_FILES,
notes: CORE_NOTES,
see_also: "fstat(2), fcntl(2), coredumpctl(1), 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 or process \
core file. 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.",
synopsis: "[pid | core]...",
options: &[],
operands: CORE_OPERANDS,
examples: &[],
exit_status: DEFAULT_EXIT_STATUS,
files: DEFAULT_FILES,
notes: CORE_NOTES,
see_also: "kill(1), signal(7), coredumpctl(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: &[],
operands: &[],
examples: &[],
exit_status: DEFAULT_EXIT_STATUS,
files: DEFAULT_FILES,
notes: "",
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(1)).",
synopsis: "PID...",
options: &[],
operands: &[],
examples: &[],
exit_status: DEFAULT_EXIT_STATUS,
files: DEFAULT_FILES,
notes: "",
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.",
)],
operands: &[],
examples: &[],
exit_status: DEFAULT_EXIT_STATUS,
files: DEFAULT_FILES,
notes: "",
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.",
),
],
operands: &[],
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,
notes: "",
see_also: "pargs(1), pgrep(1), ps(1), proc(5)",
warnings: "",
},
out_dir,
);
render_man_page(
&ManPage {
name: "plgrp",
about: "display current NUMA node and thread CPU affinities",
description: "Display the current NUMA node for each thread in the specified \
processes or process core files. The node is the NUMA node \
of the CPU on which the thread is currently (or was last) \
running. With the -a option, also display whether each thread's \
CPU affinity mask covers all, some, or none of the CPUs on the \
requested nodes. For core files, the NODE column shows ? because \
the running CPU is not captured by systemd-coredump(8), and CPU \
affinity is derived from Cpus_allowed_list in the saved process \
status.",
synopsis: "[-a node_list] [pid[/tid] | core] ...",
options: &[(
"-a node_list",
"Display affinity information for the specified NUMA nodes. \
The node_list is a comma-separated list of node IDs, ranges \
(e.g. 0-3), or the keywords all or leaves (all online nodes). \
Nodes are grouped by affinity: all means the thread's CPU \
affinity mask includes every CPU on that node, some means it \
includes some but not all, and none means it includes no CPUs \
on that node.",
)],
operands: &[
(
"pid[/tid]",
"Process ID, optionally followed by a slash and a thread ID \
to display a single thread.",
),
(
"core",
"Process core file, as produced by systemd-coredump(8). The core file \
does not need to exist on disk; if it has been removed, the \
corresponding systemd journal entry will be used instead. See \
NOTES below.",
),
],
examples: &[
Example {
title: "Example 1 Display current nodes",
description: "Display the current NUMA node for each thread of the shell:",
code: "\
$ plgrp $$
PID/TID NODE
3401/3401 1",
},
Example {
title: "Example 2 Display affinities",
description: "Display current node and affinity for nodes 0 through 2:",
code: "\
$ plgrp -a 0-2 101398
PID/TID NODE AFFINITY
101398/101398 1 0,2/all,1/none
101398/101412 0 0,2/all,1/none",
},
],
exit_status: DEFAULT_EXIT_STATUS,
files: "/proc/pid/task/tid/stat\tThread scheduling information.\n\
/sys/devices/system/node/\tNUMA topology information.",
notes: CORE_NOTES,
see_also: "taskset(1), numactl(8), coredumpctl(1), sched_getaffinity(2), proc(5)",
warnings: "For core files, the NODE column always shows ? because \
systemd-coredump(8) does not capture which CPU each thread \
was running on at the time of the crash. Only the main thread \
is available from core files; information for other threads \
is not displayed.",
},
out_dir,
);
render_man_page(
&ManPage {
name: "pstack",
about: "print stack traces of a running process or core dump",
description: "Print the stack backtraces of all threads in each running process \
or process core file. \
For live processes, it attaches to the target using the ptrace(2) \
debugging interface. \
For core files, modules and threads are discovered from the ELF \
core image. \
The first line of output displays the PID and binary name. \
For each thread, the thread ID and name is displayed followed \
by its backtrace. Each frame shows an address and a symbol with offset. \
C++ symbol names are demangled by default.\n\n\
pstack(1) can operate on core files. A core file is a snapshot of a \
process's state, produced by the kernel when terminating a process \
with a signal or by the gcore(1) utility. To provide symbol table \
information, pstack(1) needs to locate the executable corresponding \
to the process that dumped core and any shared libraries it was \
using. If pstack(1) cannot find these files, some symbol information \
will be unavailable. Similarly, if a core file from one OS release \
is examined on a different release, symbol information for shared \
libraries may not be available. Symbol names also cannot be resolved \
if the corresponding binary or shared object has been deleted from \
disk, since pstack(1) reads symbols from the on-disk ELF image. \
This commonly occurs when a binary or library is reinstalled while \
a process still uses the older version.",
synopsis: "[-amv] [-n count] [pid[/tid] | core]...",
options: &[
(
"-a, --args",
"Show values of arguments passed to functions. \
Requires DWARF debug information.",
),
("-m, --module", "Show module file paths."),
(
"-n count",
"For each thread, print at most count frames in the backtrace. \
The default is 64. Use 0 for unlimited.",
),
(
"-v, --verbose",
"Show source locations and inline frames. \
By default, only demangled symbol names are shown.",
),
],
operands: &[
(
"pid[/tid]",
"Process ID, optionally followed by a slash and a thread ID \
to display a single thread.",
),
("core", "Process core file"),
],
examples: &[],
exit_status: DEFAULT_EXIT_STATUS,
files: DEFAULT_FILES,
notes: "pstack(1) only works on processes executing ELF binaries. \
A process cannot be traced if another debugger is already attached to it. \
The ptrace(2) interface used to obtain live process information may cause \
some syscalls in the target to return EINTR on detach.\n\n\
On systems where the Yama Linux Security Module is enabled with \
kernel.yama.ptrace_scope set to 1 or higher, ptrace(2) is restricted and \
pstack(1) cannot attach to arbitrary same-user processes. In this case, \
pstack(1) must be run as root or with the CAP_SYS_PTRACE capability. \
Alternatively, the restriction can be relaxed by setting \
kernel.yama.ptrace_scope to 0 (classic ptrace permissions). See the \
Yama documentation in the kernel source for details.",
see_also: "ptrace(2), proc(5)",
warnings: "pstack(1) stops the entire target process while inspecting it, even if \
invoked against an individual thread. The process can do nothing while \
stopped. Stopping a heavily loaded process in a production environment, \
even briefly, can cause severe bottlenecks or hangs, making the process \
unavailable to users. Some applications, such as database servers, may \
terminate abnormally. Use caution when tracing production processes.\n\n\
The -v (verbose) option uses DWARF debug information to show source code \
locations (file and line number) and inlined function frames. The -a \
(args) option additionally reads function argument values, which requires \
ptrace access to process registers and memory. If DWARF debug information \
is not installed, this information may not be available. These options may \
slow down stack tracing.",
},
out_dir,
);
render_man_page(
&ManPage {
name: "ptime",
about: "time a command",
description: "Without the -p option, invoke the given command with the given \
arguments, and when the command completes, write timing \
statistics to standard error.\n\n\
With the -p option, display a snapshot of accumulated timing \
statistics for the specified processes.\n\n\
When /proc/[pid]/schedstat is available, the output includes \
nanosecond-precision scheduling statistics: cpu (on-CPU run \
time), lat (run-queue wait time), and slp (all other sleep time, \
computed as real minus cpu minus lat), along with each component's \
percentage of real time. A trailing * indicates the value is \
from a lower-precision source; percentages remain relative to real. \
With -p, real \
and slp carry a trailing * because the process start time from \
/proc is recorded in clock ticks (typically 10ms granularity). \
The user and sys values always carry a trailing * because they \
are derived from clock-tick counters when reading from /proc, or \
microsecond-precision rusage when timing a command directly. \
These values may not sum exactly to cpu.",
synopsis: "command [arg]...\n-p pidlist",
options: &[(
"-p pidlist",
"Display a snapshot of timing statistics for the specified processes. \
The pidlist is a list of process IDs separated by commas, whitespace, \
or any combination of the two (e.g., \"1,2\", \"1, 2\", \"1 2\").",
)],
operands: &[
("command", "The command to execute."),
("arg", "Arguments to the command."),
(
"pidlist",
"A list of process IDs separated by commas, whitespace, \
or any combination of the two.",
),
],
examples: &[],
exit_status: "If the command is invoked successfully, ptime(1) returns the exit \
status of the command. If the command is terminated by a signal, \
ptime(1) returns 128 plus the signal number. If the command is not \
found, ptime(1) returns 127. If the command is found but cannot be \
invoked, ptime(1) returns 126. If an error occurs in ptime(1) itself, \
ptime(1) returns 1.",
files: DEFAULT_FILES,
notes: "",
see_also: "time(1), proc(5)",
warnings: "",
},
out_dir,
);
println!("cargo:rerun-if-changed=build.rs");
}