chore(release): prepare public source release

This commit is contained in:
MercuryToolbox Release
2026-07-18 15:41:59 +08:00
commit e365e5df4d
508 changed files with 163373 additions and 0 deletions
+900
View File
@@ -0,0 +1,900 @@
//! The `proctree` command inspects process trees.
#![allow(clippy::multiple_crate_versions)]
use std::collections::{HashMap, HashSet};
use std::ffi::OsString;
use std::fmt::Write as _;
use std::process::{Command, Stdio};
use std::time::Duration;
use common::{
CliError, CommonArgs, ExitCode, RenderMode, map_result_count, parse_color_choice,
parse_format_choice, print_json, print_quick_help_error, print_structured,
};
use lexopt::prelude::{Long, Short, Value as ArgValue};
use regex_lite::{Regex, RegexBuilder};
use serde::Serialize;
use windowsupport::{ProcessDescriptor, sleep_for, snapshot_processes};
const HELP: &str = "\
Inspect Windows process trees with compact AI-friendly output.
Windows only.
Usage:
proctree [OPTIONS] system
proctree [OPTIONS] root <PID>
proctree [OPTIONS] run -- <COMMAND...>
Options:
--format <FORMAT> Structured output format: text, json, toon
--json Shortcut for --format json
--toon Shortcut for --format toon
--color <WHEN> Control ANSI color output: auto, never
--quiet Suppress non-essential status output
--match <REGEX> Filter by image name or command line
--max-depth <COUNT> Optional maximum depth to emit
--include-cmdline Include command-line text in compact output
--orphans Highlight descendants still alive after the root exits
-h, --help Show this help text
-V, --version Show the command version
Examples:
proctree system
proctree root 1234 --json | ConvertFrom-Json
proctree run -- pwsh -NoProfile -Command \"npm test\"
proctree system --match 'pwsh|Mercury' --include-cmdline
";
#[derive(Debug, Clone)]
struct Cli {
common: CommonArgs,
mode: Mode,
match_pattern: Option<Regex>,
max_depth: Option<usize>,
include_cmdline: bool,
orphans: bool,
}
#[derive(Debug, Clone)]
enum Mode {
System,
Root(u32),
Run(Vec<OsString>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ParseOutcome {
Help,
Version,
Run,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct ProcessTreeReport {
root_pid: Option<u32>,
message: Option<String>,
nodes: Vec<ProcessNode>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct ProcessNode {
pid: u32,
parent_pid: Option<u32>,
depth: usize,
image_name: String,
exe: Option<String>,
command_line: Vec<String>,
command_line_preview: String,
start_time_unix: u64,
run_time_seconds: u64,
run_time_display: String,
orphan: bool,
}
#[derive(Debug, Default)]
struct RunObservation {
root_pid: u32,
observed_processes: HashMap<u32, ProcessDescriptor>,
observed_descendants: HashSet<u32>,
}
/// Parses CLI arguments and returns a process exit code.
#[must_use]
pub fn main_entry() -> i32 {
match parse_cli_from(std::env::args_os()) {
Ok((ParseOutcome::Help, _)) => {
print!("{HELP}");
ExitCode::Success.as_i32()
}
Ok((ParseOutcome::Version, _)) => {
println!("proctree {}", env!("CARGO_PKG_VERSION"));
ExitCode::Success.as_i32()
}
Ok((ParseOutcome::Run, cli)) => match run(&cli) {
Ok(code) => code.as_i32(),
Err(error) => {
print_quick_help_error(&error, HELP);
error.exit_code().as_i32()
}
},
Err(error) => {
print_quick_help_error(&error, HELP);
error.exit_code().as_i32()
}
}
}
fn parse_cli_from<I, T>(args: I) -> Result<(ParseOutcome, Cli), CliError>
where
I: IntoIterator<Item = T>,
T: Into<OsString>,
{
let mut parser = lexopt::Parser::from_iter(args);
let mut common = CommonArgs::default();
let mut mode = None::<Mode>;
let mut match_pattern = None::<Regex>;
let mut max_depth = None::<usize>;
let mut include_cmdline = false;
let mut orphans = false;
let mut collecting_run = false;
let mut run_args = Vec::<OsString>::new();
while let Some(argument) = parser
.next()
.map_err(|error| CliError::usage(error.to_string()))?
{
if collecting_run {
if let ArgValue(value) = argument {
run_args.push(value);
continue;
}
return Err(CliError::usage(
"run only accepts command arguments after --",
));
}
match argument {
Long("help") | Short('h') => return Ok((ParseOutcome::Help, empty_cli(common))),
Long("version") | Short('V') => {
return Ok((ParseOutcome::Version, empty_cli(common)));
}
Long("json") => common.set_render_mode(RenderMode::Json),
Long("toon") => common.set_render_mode(RenderMode::Toon),
Long("format") => {
let value = parser_value_string(&mut parser, "--format")?;
common.set_render_mode(parse_format_choice(&value)?);
}
Long("color") => {
common.color = parse_color_choice(&parser_value_string(&mut parser, "--color")?)?;
}
Long("quiet") => common.quiet = true,
Long("match") => {
match_pattern = Some(parse_regex(&parser_value_string(&mut parser, "--match")?)?);
}
Long("max-depth") => {
max_depth = Some(parse_usize_flag(
"--max-depth",
&parser_value_string(&mut parser, "--max-depth")?,
)?);
}
Long("include-cmdline") => include_cmdline = true,
Long("orphans") => orphans = true,
ArgValue(value) => {
if matches!(mode, Some(Mode::Run(_))) {
collecting_run = true;
run_args.push(value);
continue;
}
let text = arg_to_string(value, "subcommand")?;
if mode.is_none() {
mode = Some(parse_mode_token(&text, &mut parser)?);
} else {
return Err(CliError::usage(
"unexpected positional argument; use --help to see available options",
));
}
}
_ => {
return Err(CliError::usage(
"unsupported argument; use --help to see available options",
));
}
}
}
let mode = match mode {
Some(Mode::Run(_)) => {
if run_args.is_empty() {
return Err(CliError::usage("run requires a command after --"));
}
Mode::Run(run_args)
}
Some(mode) => mode,
None => {
return Err(CliError::usage(
"provide a subcommand: system, root, or run",
));
}
};
Ok((
ParseOutcome::Run,
Cli {
common,
mode,
match_pattern,
max_depth,
include_cmdline,
orphans,
},
))
}
const fn empty_cli(common: CommonArgs) -> Cli {
Cli {
common,
mode: Mode::System,
match_pattern: None,
max_depth: None,
include_cmdline: false,
orphans: false,
}
}
fn arg_to_string(value: OsString, context: &str) -> Result<String, CliError> {
value.into_string().map_err(|invalid| {
CliError::usage(format!(
"{context} expects UTF-8 text, got '{}'",
invalid.to_string_lossy()
))
})
}
fn parse_mode_token(text: &str, parser: &mut lexopt::Parser) -> Result<Mode, CliError> {
match text {
"system" => Ok(Mode::System),
"root" => {
let pid = parser_value_string(parser, "root pid")?
.parse::<u32>()
.map_err(|error| CliError::usage(format!("invalid root pid: {error}")))?;
Ok(Mode::Root(pid))
}
"run" => Ok(Mode::Run(Vec::new())),
_ => Err(CliError::usage(
"unknown subcommand; expected system, root, or run",
)),
}
}
fn parser_value_string(parser: &mut lexopt::Parser, flag: &str) -> Result<String, CliError> {
let value = parser
.value()
.map_err(|error| CliError::usage(error.to_string()))?;
value.into_string().map_err(|invalid| {
CliError::usage(format!(
"{flag} expects UTF-8 text, got '{}'",
invalid.to_string_lossy()
))
})
}
fn parse_regex(value: &str) -> Result<Regex, CliError> {
RegexBuilder::new(value)
.case_insensitive(true)
.build()
.map_err(|error| CliError::usage(format!("invalid --match regex '{value}': {error}")))
}
fn parse_usize_flag(flag: &str, value: &str) -> Result<usize, CliError> {
value
.parse::<usize>()
.map_err(|error| CliError::usage(format!("invalid {flag} value '{value}': {error}")))
}
fn run(cli: &Cli) -> Result<ExitCode, CliError> {
let report = match &cli.mode {
Mode::System => build_system_report(cli),
Mode::Root(pid) => build_root_report(cli, *pid, false),
Mode::Run(command) => build_run_report(cli, command)?,
};
match cli.common.render_mode() {
RenderMode::Json => print_json(&report)?,
RenderMode::Toon => print_structured(&report, RenderMode::Toon)?,
RenderMode::Text => print!("{}", render_report(&report, cli.include_cmdline)),
}
Ok(map_result_count(report.nodes.len()))
}
fn build_system_report(cli: &Cli) -> ProcessTreeReport {
let processes = filter_processes(snapshot_processes(), cli.match_pattern.as_ref());
let nodes = flatten_system_nodes(&processes, cli.max_depth);
ProcessTreeReport {
root_pid: None,
message: None,
nodes,
}
}
fn build_root_report(cli: &Cli, root_pid: u32, orphan_mode: bool) -> ProcessTreeReport {
let processes = filter_processes(snapshot_processes(), cli.match_pattern.as_ref());
let nodes = flatten_subtree_nodes(&processes, root_pid, cli.max_depth, orphan_mode);
ProcessTreeReport {
root_pid: Some(root_pid),
message: nodes
.is_empty()
.then(|| format!("root process {root_pid} not found at snapshot time")),
nodes,
}
}
fn build_run_report(cli: &Cli, command: &[OsString]) -> Result<ProcessTreeReport, CliError> {
let Some(program) = command.first() else {
return Err(CliError::usage("run requires a command"));
};
let mut child = Command::new(program)
.args(&command[1..])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.map_err(|error| CliError::runtime(format!("failed to launch command: {error}")))?;
let root_pid = child.id();
let mut observation = RunObservation::new(root_pid);
loop {
let snapshot = snapshot_processes();
observation.record(&snapshot);
if child
.try_wait()
.map_err(|error| CliError::runtime(format!("failed to poll command: {error}")))?
.is_some()
{
break;
}
sleep_for(Duration::from_millis(75));
}
let mut final_snapshot = snapshot_processes();
observation.record(&final_snapshot);
if cli.orphans {
sleep_for(Duration::from_millis(150));
final_snapshot = snapshot_processes();
observation.record(&final_snapshot);
}
Ok(observation.build_report(cli, &final_snapshot))
}
impl RunObservation {
fn new(root_pid: u32) -> Self {
Self {
root_pid,
observed_processes: HashMap::new(),
observed_descendants: HashSet::new(),
}
}
fn record(&mut self, processes: &[ProcessDescriptor]) {
let process_map = processes
.iter()
.cloned()
.map(|process| (process.pid, process))
.collect::<HashMap<_, _>>();
if let Some(root) = process_map.get(&self.root_pid) {
self.observed_processes.insert(self.root_pid, root.clone());
}
let children = build_children_map(processes);
let descendants = collect_descendant_ids(&children, self.root_pid);
for pid in descendants.iter().copied() {
if let Some(process) = process_map.get(&pid) {
self.observed_processes.insert(pid, process.clone());
}
}
self.observed_descendants.extend(descendants);
}
fn build_report(&self, cli: &Cli, live_processes: &[ProcessDescriptor]) -> ProcessTreeReport {
let historical = self
.observed_processes
.values()
.cloned()
.collect::<Vec<_>>();
let mut nodes = flatten_subtree_nodes(&historical, self.root_pid, cli.max_depth, false);
if let Some(pattern) = cli.match_pattern.as_ref() {
nodes.retain(|node| {
pattern.is_match(&node.image_name)
|| node.command_line.iter().any(|arg| pattern.is_match(arg))
});
}
if cli.orphans {
let live_pids = live_processes
.iter()
.map(|process| process.pid)
.collect::<HashSet<_>>();
for node in &mut nodes {
node.orphan = node.pid != self.root_pid
&& self.observed_descendants.contains(&node.pid)
&& live_pids.contains(&node.pid);
}
}
ProcessTreeReport {
root_pid: Some(self.root_pid),
message: nodes
.is_empty()
.then(|| format!("root process {} exited before snapshot", self.root_pid)),
nodes,
}
}
}
fn filter_processes(
processes: Vec<ProcessDescriptor>,
pattern: Option<&Regex>,
) -> Vec<ProcessDescriptor> {
if let Some(pattern) = pattern {
processes
.into_iter()
.filter(|process| {
pattern.is_match(&process.image_name)
|| process.command_line.iter().any(|arg| pattern.is_match(arg))
})
.collect()
} else {
processes
}
}
fn flatten_system_nodes(
processes: &[ProcessDescriptor],
max_depth: Option<usize>,
) -> Vec<ProcessNode> {
let children = build_children_map(processes);
let process_map = processes
.iter()
.cloned()
.map(|process| (process.pid, process))
.collect::<HashMap<_, _>>();
let mut roots = processes
.iter()
.filter(|process| {
process
.parent_pid
.is_none_or(|parent| !process_map.contains_key(&parent))
})
.map(|process| process.pid)
.collect::<Vec<_>>();
roots.sort_unstable();
let mut nodes = Vec::new();
for root in roots {
collect_tree_nodes(
root,
0,
&children,
&process_map,
max_depth,
false,
&mut nodes,
);
}
nodes
}
fn flatten_subtree_nodes(
processes: &[ProcessDescriptor],
root_pid: u32,
max_depth: Option<usize>,
orphan_mode: bool,
) -> Vec<ProcessNode> {
let children = build_children_map(processes);
let process_map = processes
.iter()
.cloned()
.map(|process| (process.pid, process))
.collect::<HashMap<_, _>>();
let mut nodes = Vec::new();
if process_map.contains_key(&root_pid) {
collect_tree_nodes(
root_pid,
0,
&children,
&process_map,
max_depth,
orphan_mode,
&mut nodes,
);
} else if orphan_mode {
let descendants = collect_descendant_ids(&children, root_pid);
let mut orphan_pids = descendants.into_iter().collect::<Vec<_>>();
orphan_pids.sort_unstable();
for pid in orphan_pids {
if let Some(process) = process_map.get(&pid) {
nodes.push(ProcessNode {
pid: process.pid,
parent_pid: process.parent_pid,
depth: 0,
image_name: process.image_name.clone(),
exe: process.exe.clone(),
command_line: process.command_line.clone(),
command_line_preview: compact_command_line(&process.command_line),
start_time_unix: process.start_time_unix,
run_time_seconds: process.run_time_seconds,
run_time_display: format_run_time(process.run_time_seconds),
orphan: true,
});
}
}
}
nodes
}
fn build_children_map(processes: &[ProcessDescriptor]) -> HashMap<u32, Vec<u32>> {
let mut map = HashMap::<u32, Vec<u32>>::new();
for process in processes {
if let Some(parent) = process.parent_pid {
map.entry(parent).or_default().push(process.pid);
}
}
for children in map.values_mut() {
children.sort_unstable();
}
map
}
fn collect_tree_nodes(
pid: u32,
depth: usize,
children: &HashMap<u32, Vec<u32>>,
process_map: &HashMap<u32, ProcessDescriptor>,
max_depth: Option<usize>,
orphan: bool,
nodes: &mut Vec<ProcessNode>,
) {
if max_depth.is_some_and(|max_depth| depth > max_depth) {
return;
}
let Some(process) = process_map.get(&pid) else {
return;
};
nodes.push(ProcessNode {
pid: process.pid,
parent_pid: process.parent_pid,
depth,
image_name: process.image_name.clone(),
exe: process.exe.clone(),
command_line: process.command_line.clone(),
command_line_preview: compact_command_line(&process.command_line),
start_time_unix: process.start_time_unix,
run_time_seconds: process.run_time_seconds,
run_time_display: format_run_time(process.run_time_seconds),
orphan,
});
if let Some(child_pids) = children.get(&pid) {
for child in child_pids {
collect_tree_nodes(
*child,
depth + 1,
children,
process_map,
max_depth,
orphan,
nodes,
);
}
}
}
fn collect_descendant_ids(children: &HashMap<u32, Vec<u32>>, root_pid: u32) -> HashSet<u32> {
let mut seen = HashSet::new();
let mut stack = Vec::new();
if let Some(root_children) = children.get(&root_pid) {
stack.extend(root_children.iter().copied());
}
while let Some(pid) = stack.pop() {
if seen.insert(pid)
&& let Some(next) = children.get(&pid)
{
stack.extend(next.iter().copied());
}
}
seen
}
fn render_report(report: &ProcessTreeReport, include_cmdline: bool) -> String {
let mut rendered = String::new();
if let Some(root_pid) = report.root_pid {
writeln!(rendered, "root_pid={root_pid} nodes={}", report.nodes.len())
.expect("writing to a String cannot fail");
} else {
writeln!(rendered, "system nodes={}", report.nodes.len())
.expect("writing to a String cannot fail");
}
if let Some(message) = &report.message {
writeln!(rendered, "message={message}").expect("writing to a String cannot fail");
}
for node in &report.nodes {
let indent = " ".repeat(node.depth);
let orphan = if node.orphan { " orphan" } else { "" };
if include_cmdline && !node.command_line.is_empty() {
writeln!(
rendered,
"{indent}pid={} image={} runtime={}{orphan} cmd={}",
node.pid,
node.image_name,
node.run_time_display,
compact_command_line(&node.command_line)
)
.expect("writing to a String cannot fail");
} else {
writeln!(
rendered,
"{indent}pid={} image={} runtime={}{orphan}",
node.pid, node.image_name, node.run_time_display
)
.expect("writing to a String cannot fail");
}
}
rendered
}
fn compact_command_line(arguments: &[String]) -> String {
const MAX_COMMAND_LINE_CHARS: usize = 120;
let rendered = join_command_line(&sanitize_command_line(arguments));
let character_count = rendered.chars().count();
if character_count <= MAX_COMMAND_LINE_CHARS {
return rendered;
}
let visible = MAX_COMMAND_LINE_CHARS.saturating_sub(3);
let prefix = rendered.chars().take(visible).collect::<String>();
format!("{prefix}...")
}
fn join_command_line(arguments: &[String]) -> String {
let mut rendered = String::new();
for argument in arguments {
if !rendered.is_empty() {
rendered.push(' ');
}
rendered.push_str(argument);
}
rendered
}
fn sanitize_command_line(arguments: &[String]) -> Vec<String> {
let mut sanitized = Vec::with_capacity(arguments.len());
let mut index = 0_usize;
while index < arguments.len() {
let value = &arguments[index];
if value.eq_ignore_ascii_case("-encodedcommand")
|| value.eq_ignore_ascii_case("-ec")
|| value.eq_ignore_ascii_case("/encodedcommand")
{
sanitized.push(value.clone());
if let Some(payload) = arguments.get(index + 1) {
sanitized.push(format!("<encoded:{} chars>", payload.chars().count()));
index += 2;
continue;
}
}
sanitized.push(value.clone());
index += 1;
}
sanitized
}
fn format_run_time(run_time_seconds: u64) -> String {
if run_time_seconds == 0 {
"<1s".to_string()
} else {
format!("{run_time_seconds}s")
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_processes() -> Vec<ProcessDescriptor> {
vec![
ProcessDescriptor {
pid: 1,
parent_pid: None,
image_name: "root.exe".to_string(),
exe: Some("C:\\root.exe".to_string()),
command_line: vec!["root".to_string()],
start_time_unix: 0,
run_time_seconds: 10,
},
ProcessDescriptor {
pid: 10,
parent_pid: Some(1),
image_name: "child.exe".to_string(),
exe: None,
command_line: vec!["child".to_string(), "--watch".to_string()],
start_time_unix: 1,
run_time_seconds: 5,
},
ProcessDescriptor {
pid: 11,
parent_pid: Some(10),
image_name: "grand.exe".to_string(),
exe: None,
command_line: vec!["grand".to_string()],
start_time_unix: 2,
run_time_seconds: 1,
},
]
}
#[test]
fn parser_accepts_run_mode_with_common_flags_after_subcommand() {
let (_, cli) = parse_cli_from([
"proctree",
"run",
"--json",
"--max-depth",
"2",
"--",
"pwsh",
"-NoProfile",
])
.expect("cli");
assert!(cli.common.json);
assert_eq!(cli.max_depth, Some(2));
assert!(matches!(cli.mode, Mode::Run(_)));
}
#[test]
fn flatten_subtree_marks_orphans() {
let processes = sample_processes().into_iter().skip(1).collect::<Vec<_>>();
let nodes = flatten_subtree_nodes(&processes, 1, None, true);
assert_eq!(nodes.len(), 2);
assert!(nodes.iter().all(|node| node.orphan));
}
#[test]
fn help_and_root_modes_parse_cleanly() {
let (outcome, _) = parse_cli_from(["proctree", "--help"]).expect("help");
assert_eq!(outcome, ParseOutcome::Help);
let (_, cli) = parse_cli_from([
"proctree",
"--match",
"child",
"--include-cmdline",
"root",
"10",
])
.expect("root");
assert!(cli.include_cmdline);
assert!(cli.match_pattern.is_some());
assert!(matches!(cli.mode, Mode::Root(10)));
}
#[test]
fn helper_parsers_and_filters_cover_error_paths() {
assert!(matches!(
parse_regex("["),
Err(CliError::Usage(message)) if message.contains("invalid --match regex")
));
assert!(matches!(
parse_usize_flag("--max-depth", "bad"),
Err(CliError::Usage(message)) if message.contains("invalid --max-depth")
));
assert!(matches!(
parse_mode_token("wat", &mut lexopt::Parser::from_iter(["proctree"])),
Err(CliError::Usage(message)) if message.contains("unknown subcommand")
));
let regex = parse_regex("watch").expect("regex");
let filtered = filter_processes(sample_processes(), Some(&regex));
assert_eq!(filtered.len(), 1);
assert_eq!(filtered[0].pid, 10);
}
#[test]
fn system_and_root_rendering_stay_compact() {
let processes = sample_processes();
let system_nodes = flatten_system_nodes(&processes, Some(1));
let root_nodes = flatten_subtree_nodes(&processes, 1, Some(2), false);
let report = ProcessTreeReport {
root_pid: Some(1),
message: None,
nodes: root_nodes,
};
let text = render_report(&report, true);
assert_eq!(system_nodes.len(), 2);
assert!(text.contains("root_pid=1"));
assert!(text.contains("cmd=child --watch"));
assert!(text.contains("runtime=5s"));
assert_eq!(format_run_time(0), "<1s");
}
#[test]
fn compact_command_line_truncates_long_output() {
let command = compact_command_line(&[
"pwsh".to_string(),
"-EncodedCommand".to_string(),
"A".repeat(200),
]);
assert_eq!(command, "pwsh -EncodedCommand <encoded:200 chars>");
}
#[test]
fn children_maps_and_descendants_are_stable() {
let processes = sample_processes();
let children = build_children_map(&processes);
let descendants = collect_descendant_ids(&children, 1);
assert_eq!(children.get(&1), Some(&vec![10]));
assert!(descendants.contains(&10));
assert!(descendants.contains(&11));
}
#[test]
fn run_observation_preserves_seen_descendants_after_root_exits() {
let root = ProcessDescriptor {
pid: 1,
parent_pid: None,
image_name: "root.exe".to_string(),
exe: None,
command_line: vec!["root".to_string()],
start_time_unix: 0,
run_time_seconds: 1,
};
let child = ProcessDescriptor {
pid: 10,
parent_pid: Some(1),
image_name: "child.exe".to_string(),
exe: None,
command_line: vec!["child".to_string()],
start_time_unix: 0,
run_time_seconds: 1,
};
let detached_child = ProcessDescriptor {
parent_pid: None,
..child.clone()
};
let mut observation = RunObservation::new(1);
observation.record(&[root, child]);
observation.record(std::slice::from_ref(&detached_child));
let cli = Cli {
common: CommonArgs::default(),
mode: Mode::System,
match_pattern: None,
max_depth: None,
include_cmdline: false,
orphans: true,
};
let report = observation.build_report(&cli, &[detached_child]);
assert_eq!(report.root_pid, Some(1));
assert_eq!(report.message, None);
assert_eq!(report.nodes.len(), 2);
assert!(report.nodes.iter().any(|node| node.pid == 1));
assert!(
report
.nodes
.iter()
.find(|node| node.pid == 10)
.is_some_and(|node| node.orphan)
);
assert!(
report
.nodes
.iter()
.find(|node| node.pid == 10)
.is_some_and(|node| node.command_line_preview == "child")
);
}
}
+6
View File
@@ -0,0 +1,6 @@
//! Binary entry point for `proctree`.
#![allow(clippy::multiple_crate_versions)]
fn main() {
std::process::exit(proctree::main_entry());
}