├── .gitignore ├── src ├── lib.rs ├── utils.rs ├── main.rs ├── dump.rs ├── test_utils.rs ├── log.rs ├── procinfo.rs ├── config.rs ├── stacktrace.rs ├── run.rs └── report.rs ├── examples └── daemon.toml ├── AGENTS.md ├── tests ├── spawn_failure.rs ├── nonexistent_pid.rs ├── metadata.rs ├── pid_exit.rs ├── run_command.rs ├── cli.rs ├── python_stack.rs ├── multi_thread.rs ├── symbols.rs ├── fd_events.rs ├── output_format.rs └── report.rs ├── Cargo.toml ├── .github └── workflows │ └── ci.yml ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod test_utils; 2 | pub mod utils; 3 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use chrono::Utc; 2 | 3 | /// Returns current date as YYYYMMDD string in UTC. 4 | pub fn current_date_string() -> String { 5 | Utc::now().format("%Y%m%d").to_string() 6 | } 7 | -------------------------------------------------------------------------------- /examples/daemon.toml: -------------------------------------------------------------------------------- 1 | # Recommended settings when running fuzmon as a daemon 2 | 3 | [output] 4 | format = "msgpack" 5 | path = "/var/log/fuzmon/" 6 | compress = true 7 | 8 | [monitor] 9 | interval_sec = 60 10 | record_cpu_time_percent_threshold = 0.0 11 | stacktrace_cpu_time_percent_threshold = 1.0 12 | -------------------------------------------------------------------------------- /AGENTS.md: -------------------------------------------------------------------------------- 1 | ## Style 2 | 3 | - Remove all compiler warnings 4 | - Use info! log for non-frequent events and use warn! for errors 5 | - Never copy-and-paste code. Eliminate duplicated code 6 | - Run `cargo fmt` 7 | - Never use `allow(dead_code)` 8 | 9 | ## Testing 10 | 11 | - Add appropriate tests 12 | - Get `cargo test` pass 13 | - Never add sleep 14 | -------------------------------------------------------------------------------- /tests/spawn_failure.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | 3 | #[test] 4 | fn spawn_invalid_command_prints_message() { 5 | let out = Command::new(env!("CARGO_BIN_EXE_fuzmon")) 6 | .args(["run", "/bin/hogeee"]) 7 | .output() 8 | .expect("run"); 9 | let stdout = String::from_utf8_lossy(&out.stdout); 10 | assert!(stdout.contains("failed to spawn"), "{}", stdout); 11 | } 12 | -------------------------------------------------------------------------------- /tests/nonexistent_pid.rs: -------------------------------------------------------------------------------- 1 | use std::process::Command; 2 | use std::time::Instant; 3 | use tempfile::tempdir; 4 | 5 | #[test] 6 | fn nonexistent_pid_exits_immediately() { 7 | let dir = tempdir().expect("dir"); 8 | let start = Instant::now(); 9 | let out = Command::new(env!("CARGO_BIN_EXE_fuzmon")) 10 | .args(["run", "-p", "9999999", "-o", dir.path().to_str().unwrap()]) 11 | .output() 12 | .expect("run"); 13 | assert!(start.elapsed().as_secs() < 2, "took {:?}", start.elapsed()); 14 | let stdout = String::from_utf8_lossy(&out.stdout); 15 | assert!(stdout.contains("not found"), "{}", stdout); 16 | } 17 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod dump; 3 | mod log; 4 | mod procinfo; 5 | mod report; 6 | mod run; 7 | mod stacktrace; 8 | 9 | use crate::config::{Cli, Commands, parse_cli}; 10 | use clap::CommandFactory; 11 | 12 | fn main() { 13 | env_logger::init(); 14 | let cli = parse_cli(); 15 | if let Some(cmd) = cli.command { 16 | match cmd { 17 | Commands::Run(args) => run::run(args), 18 | Commands::Dump(args) => dump::dump(&args.path), 19 | Commands::Report(args) => report::report(&args), 20 | } 21 | } else { 22 | Cli::command().print_help().unwrap(); 23 | println!(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fuzmon" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [lib] 7 | path = "src/lib.rs" 8 | 9 | [dependencies] 10 | nix = { version = "0.28", features = ["ptrace", "process"] } 11 | addr2line = "0.25" 12 | object = "0.37" 13 | memmap2 = "0.9" 14 | serde = { version = "1", features = ["derive"] } 15 | serde_json = "1" 16 | regex = "1" 17 | py-spy = { version = "0.4", default-features = false } 18 | rmp-serde = "1" 19 | chrono = { version = "0.4", features = ["clock"] } 20 | toml = "0.8" 21 | clap = { version = "4", features = ["derive"] } 22 | zstd = "0.13" 23 | log = "0.4" 24 | env_logger = "0.10" 25 | ctrlc = "3" 26 | tempfile = "3" 27 | num_cpus = "1" 28 | html-escape = "0.2" 29 | plotters = "0.3" 30 | plotters-svg = "0.3" 31 | -------------------------------------------------------------------------------- /src/dump.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::path::Path; 3 | 4 | use crate::log::read_log_entries; 5 | 6 | pub fn dump(path: &str) { 7 | let p = Path::new(path); 8 | if p.is_dir() { 9 | if let Ok(entries) = fs::read_dir(p) { 10 | for entry in entries.flatten() { 11 | let file_path = entry.path(); 12 | if file_path.is_file() { 13 | dump_file(&file_path); 14 | } 15 | } 16 | } 17 | } else { 18 | dump_file(p); 19 | } 20 | } 21 | 22 | fn dump_file(path: &Path) { 23 | println!("{}", path.display()); 24 | match read_log_entries(path) { 25 | Ok(entries) => { 26 | for e in entries { 27 | println!("{:?}", e); 28 | } 29 | } 30 | Err(e) => eprintln!("failed to read {}: {}", path.display(), e), 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tests/metadata.rs: -------------------------------------------------------------------------------- 1 | use fuzmon::test_utils::run_fuzmon; 2 | use serde_json::Value; 3 | use std::process::{Command, Stdio}; 4 | use tempfile::tempdir; 5 | 6 | #[test] 7 | fn first_entry_contains_cmdline_and_env() { 8 | let logdir = tempdir().expect("logdir"); 9 | let mut child = Command::new("sleep") 10 | .arg("1") 11 | .env("META_VAR", "xyz") 12 | .stdout(Stdio::null()) 13 | .spawn() 14 | .expect("spawn sleep"); 15 | let pid = child.id(); 16 | let log = run_fuzmon(env!("CARGO_BIN_EXE_fuzmon"), pid, &logdir); 17 | fuzmon::test_utils::kill_with_sigint_and_wait(&mut child); 18 | let first = log.lines().next().expect("line"); 19 | let v: Value = serde_json::from_str(first).expect("json"); 20 | assert_eq!(v.get("cmdline").and_then(|s| s.as_str()), Some("sleep 1")); 21 | let env = v.get("env").and_then(|s| s.as_str()).unwrap_or(""); 22 | assert!(env.contains("META_VAR=xyz"), "{}", env); 23 | } 24 | -------------------------------------------------------------------------------- /tests/pid_exit.rs: -------------------------------------------------------------------------------- 1 | use fuzmon::test_utils::{create_config, run_fuzmon_output}; 2 | use std::process::{Command, Stdio}; 3 | use tempfile::tempdir; 4 | 5 | #[test] 6 | fn exits_when_pid_disappears() { 7 | let mut child = Command::new("sh") 8 | .args(["-c", "read dummy"]) 9 | .stdin(Stdio::piped()) 10 | .spawn() 11 | .expect("spawn read"); 12 | let pid = child.id(); 13 | 14 | let logdir = tempdir().expect("logdir"); 15 | let cfg = create_config(1000.0); 16 | 17 | let handle = std::thread::spawn(move || { 18 | run_fuzmon_output(env!("CARGO_BIN_EXE_fuzmon"), pid, &logdir, &cfg) 19 | }); 20 | 21 | // Close stdin so the script exits 22 | drop(child.stdin.take()); 23 | 24 | let out = handle.join().expect("join fuzmon"); 25 | 26 | let _ = child.wait(); 27 | let stdout = String::from_utf8_lossy(&out.stdout); 28 | assert!(stdout.contains("disappeared, exiting"), "{}", stdout); 29 | } 30 | -------------------------------------------------------------------------------- /tests/run_command.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::process::Command; 3 | use tempfile::tempdir; 4 | 5 | use fuzmon::test_utils::{collect_log_content, create_config}; 6 | use fuzmon::utils::current_date_string; 7 | 8 | #[test] 9 | fn spawn_and_monitor_command() { 10 | let dir = tempdir().expect("dir"); 11 | let cfg = create_config(0.0); 12 | let out = Command::new(env!("CARGO_BIN_EXE_fuzmon")) 13 | .args([ 14 | "run", 15 | "-o", 16 | dir.path().to_str().unwrap(), 17 | "-c", 18 | cfg.path().to_str().unwrap(), 19 | "/bin/sleep", 20 | "1", 21 | ]) 22 | .output() 23 | .expect("run"); 24 | assert!(out.status.success()); 25 | let date = current_date_string(); 26 | let sub = dir.path().join(date); 27 | let log_content = collect_log_content(&dir); 28 | assert!(fs::read_dir(sub).unwrap().next().is_some(), "no log file"); 29 | assert!(!log_content.is_empty(), "log empty"); 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: dtolnay/rust-toolchain@stable 13 | - name: Cache apt packages 14 | uses: actions/cache@v3 15 | with: 16 | path: | 17 | /var/cache/apt/archives 18 | /var/lib/apt/lists 19 | key: ${{ runner.os }}-apt-${{ hashFiles('.github/workflows/ci.yml') }} 20 | restore-keys: ${{ runner.os }}-apt- 21 | - name: Install build tools 22 | run: sudo apt-get update && sudo apt-get install -y gcc libunwind-dev libfontconfig-dev 23 | - name: Allow ptrace 24 | run: sudo sysctl -w kernel.yama.ptrace_scope=0 25 | - name: Build 26 | run: cargo build --verbose 27 | - name: Run basic check 28 | run: | 29 | timeout -k 3 ./target/debug/fuzmon > output.txt 2>&1 || true 30 | test -s output.txt 31 | - name: Test 32 | run: cargo test --verbose -- --nocapture 33 | -------------------------------------------------------------------------------- /tests/cli.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::process::Command; 3 | use tempfile::tempdir; 4 | 5 | #[test] 6 | fn dump_outputs_entries() { 7 | let dir = tempdir().expect("tempdir"); 8 | let log_path = dir.path().join("1.jsonl"); 9 | fs::write(&log_path, "{\"timestamp\":\"0\",\"pid\":1,\"process_name\":\"t\",\"cpu_time_percent\":0,\"memory\":{\"rss_kb\":0,\"vsz_kb\":0,\"swap_kb\":0}}\n").unwrap(); 10 | 11 | let out = Command::new(env!("CARGO_BIN_EXE_fuzmon")) 12 | .args(["dump", log_path.to_str().unwrap()]) 13 | .output() 14 | .expect("run fuzmon dump"); 15 | let stdout = String::from_utf8_lossy(&out.stdout); 16 | assert!(stdout.contains("1.jsonl")); 17 | assert!(stdout.contains("process_name")); 18 | } 19 | 20 | #[test] 21 | fn help_subcommand_shows_usage() { 22 | let out = Command::new(env!("CARGO_BIN_EXE_fuzmon")) 23 | .arg("help") 24 | .output() 25 | .expect("run fuzmon help"); 26 | let stdout = String::from_utf8_lossy(&out.stdout); 27 | assert!(stdout.contains("fuzmon")); 28 | assert!(stdout.contains("run")); 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fuzmon 2 | 3 | Lightweight fuzzy process monitor for Linux. 4 | Logs can be written in JSON (default) or MessagePack when `format = "msgpack"` is set in the config. 5 | Python processes are traced using an embedded `py-spy` integration when possible. 6 | 7 | ``` 8 | fuzmon -o logs/ # write logs under ./logs 9 | fuzmon -c config.toml # use configuration file 10 | # monitor a specific PID and write logs 11 | fuzmon -p 1234 -o logs/ 12 | # logs default to /tmp/fuzmon when -o not specified 13 | ``` 14 | 15 | Log files are written under a date directory such as `logs/20250615/`. A new 16 | directory is created if the date changes while running. 17 | 18 | Each line in the log file is a JSON object similar to: 19 | 20 | ```json 21 | { 22 | "timestamp": "2025-06-14T14:23:51Z", 23 | "pid": 12345, 24 | "process_name": "python3", 25 | "cpu_time_percent": 12.3, 26 | "memory": { "rss_kb": 20480, "vsz_kb": 105000, "swap_kb": 0 }, 27 | "stacktrace": [[" 0: 0xdeadbeef main at main.c:42"]] 28 | } 29 | ``` 30 | CPU usage is reported in the same way as the `top` command, so values can 31 | exceed 100% when multiple threads are busy. 32 | -------------------------------------------------------------------------------- /tests/python_stack.rs: -------------------------------------------------------------------------------- 1 | use fuzmon::test_utils::run_fuzmon; 2 | use std::fs; 3 | use std::io::{BufRead, BufReader, Write}; 4 | use std::process::{Command, Stdio}; 5 | use tempfile::tempdir; 6 | 7 | #[test] 8 | fn python_stack_trace_contains_functions() { 9 | let dir = tempdir().expect("tempdir"); 10 | let script = dir.path().join("test.py"); 11 | fs::write( 12 | &script, 13 | r#" 14 | import sys 15 | 16 | def foo(): 17 | bar() 18 | 19 | def bar(): 20 | print("ready", flush=True) 21 | sys.stdin.readline() 22 | 23 | if __name__ == '__main__': 24 | foo() 25 | "#, 26 | ) 27 | .expect("write script"); 28 | 29 | let mut child = Command::new("python3") 30 | .arg(&script) 31 | .stdin(Stdio::piped()) 32 | .stdout(Stdio::piped()) 33 | .spawn() 34 | .expect("spawn python"); 35 | let mut child_in = child.stdin.take().expect("child stdin"); 36 | let mut child_out = BufReader::new(child.stdout.take().expect("child stdout")); 37 | 38 | let mut line = String::new(); 39 | child_out.read_line(&mut line).expect("read line"); 40 | assert_eq!(line.trim(), "ready"); 41 | 42 | let pid = child.id(); 43 | 44 | let logdir = tempdir().expect("logdir"); 45 | let log = run_fuzmon(env!("CARGO_BIN_EXE_fuzmon"), pid, &logdir); 46 | 47 | child_in.write_all(b"\n").unwrap(); 48 | drop(child_in); 49 | let _ = child.wait(); 50 | assert!(log.contains("foo"), "{}", log); 51 | assert!(log.contains("bar"), "{}", log); 52 | assert!(log.contains("test.py"), "{}", log); 53 | let first = log.lines().next().expect("line"); 54 | let entry: serde_json::Value = serde_json::from_str(first).expect("json"); 55 | let threads = entry 56 | .get("threads") 57 | .and_then(|v| v.as_array()) 58 | .expect("threads"); 59 | let mut has_c = false; 60 | let mut has_py = false; 61 | for t in threads { 62 | if t.get("stacktrace").is_some() { 63 | has_c = true; 64 | } 65 | if t.get("python_stacktrace").is_some() { 66 | has_py = true; 67 | } 68 | } 69 | assert!(has_c, "no c stacktrace: {}", first); 70 | assert!(has_py, "no python stacktrace: {}", first); 71 | } 72 | -------------------------------------------------------------------------------- /tests/multi_thread.rs: -------------------------------------------------------------------------------- 1 | use fuzmon::test_utils::run_fuzmon; 2 | use serde_json::Value; 3 | use std::fs; 4 | use std::io::Write; 5 | use std::process::{Command, Stdio}; 6 | 7 | use tempfile::tempdir; 8 | 9 | #[test] 10 | fn multi_thread_stacktrace_has_multiple_entries() { 11 | let dir = tempdir().expect("tempdir"); 12 | let src = dir.path().join("prog.c"); 13 | fs::write( 14 | &src, 15 | r#" 16 | #include 17 | #include 18 | 19 | static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 20 | static pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 21 | static int done = 0; 22 | 23 | void* worker(void* arg) { 24 | (void)arg; 25 | pthread_mutex_lock(&mutex); 26 | while (!done) { 27 | pthread_cond_wait(&cond, &mutex); 28 | } 29 | pthread_mutex_unlock(&mutex); 30 | return NULL; 31 | } 32 | 33 | int main() { 34 | pthread_t t; 35 | pthread_create(&t, NULL, worker, NULL); 36 | 37 | char buf; 38 | read(0, &buf, 1); 39 | 40 | pthread_mutex_lock(&mutex); 41 | done = 1; 42 | pthread_cond_signal(&cond); 43 | pthread_mutex_unlock(&mutex); 44 | 45 | pthread_join(t, NULL); 46 | return 0; 47 | } 48 | "#, 49 | ) 50 | .expect("write src"); 51 | let exe = dir.path().join("prog"); 52 | assert!( 53 | Command::new("gcc") 54 | .args([ 55 | "-g", 56 | "-pthread", 57 | src.to_str().unwrap(), 58 | "-o", 59 | exe.to_str().unwrap() 60 | ]) 61 | .status() 62 | .expect("compile") 63 | .success() 64 | ); 65 | 66 | let mut child = Command::new(&exe) 67 | .stdin(Stdio::piped()) 68 | .stdout(Stdio::null()) 69 | .spawn() 70 | .expect("spawn"); 71 | let mut child_in = child.stdin.take().expect("child stdin"); 72 | 73 | let pid = child.id(); 74 | 75 | let logdir = tempdir().expect("logdir"); 76 | let log = run_fuzmon(env!("CARGO_BIN_EXE_fuzmon"), pid, &logdir); 77 | 78 | child_in.write_all(b"\n").unwrap(); 79 | drop(child_in); 80 | let _ = child.wait(); 81 | let line = log.lines().next().expect("line"); 82 | let entry: Value = serde_json::from_str(line).expect("json"); 83 | let threads = entry 84 | .get("threads") 85 | .and_then(|v| v.as_array()) 86 | .expect("array"); 87 | assert!(threads.len() >= 2, "len {}", threads.len()); 88 | } 89 | -------------------------------------------------------------------------------- /tests/symbols.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::io::{BufRead, BufReader}; 3 | use std::process::{Command, Stdio}; 4 | use tempfile::tempdir; 5 | 6 | use fuzmon::test_utils::run_fuzmon_and_check; 7 | 8 | fn run_symbol_test(flags: &[&str], expected: &[&str]) { 9 | let dir = tempdir().expect("tempdir"); 10 | let src_path = dir.path().join("testprog.c"); 11 | fs::write( 12 | &src_path, 13 | r#" 14 | #include 15 | #include 16 | 17 | __attribute__((noinline)) 18 | static void block_read() { 19 | char buf; 20 | int r = read(0, &buf, 1); 21 | if (r < 0) { 22 | fprintf(stderr, "Read error: %d\\n", r); 23 | } 24 | } 25 | 26 | __attribute__((noinline)) 27 | void target_function() { 28 | puts("ready"); 29 | fflush(stdout); 30 | while (1) { 31 | block_read(); 32 | } 33 | } 34 | 35 | __attribute__((noinline)) 36 | int main() { 37 | target_function(); 38 | return 0; 39 | } 40 | "#, 41 | ) 42 | .expect("write src"); 43 | 44 | let exe_path = dir.path().join("testprog"); 45 | let mut gcc_args: Vec<&str> = flags.to_vec(); 46 | gcc_args.push("-fno-optimize-sibling-calls"); 47 | gcc_args.push("-fno-omit-frame-pointer"); 48 | gcc_args.push(src_path.to_str().unwrap()); 49 | gcc_args.push("-o"); 50 | gcc_args.push(exe_path.to_str().unwrap()); 51 | 52 | let status = Command::new("gcc") 53 | .args(&gcc_args) 54 | .status() 55 | .expect("compile test program"); 56 | assert!(status.success()); 57 | 58 | let mut child = Command::new(&exe_path) 59 | .stdin(Stdio::piped()) 60 | .stdout(Stdio::piped()) 61 | .spawn() 62 | .expect("spawn test program"); 63 | let mut child_out = BufReader::new(child.stdout.take().expect("stdout")); 64 | let mut line = String::new(); 65 | child_out.read_line(&mut line).expect("read line"); 66 | assert_eq!(line.trim(), "ready"); 67 | 68 | let pid = child.id(); 69 | let logdir = tempdir().expect("logdir"); 70 | run_fuzmon_and_check(env!("CARGO_BIN_EXE_fuzmon"), pid, &logdir, expected); 71 | 72 | fuzmon::test_utils::kill_with_sigint_and_wait(&mut child); 73 | } 74 | 75 | #[test] 76 | fn symbolized_stack_trace_contains_function() { 77 | run_symbol_test(&["-g", "-O0"], &["target_function", "main", "testprog.c"]); 78 | } 79 | 80 | #[test] 81 | fn symbolized_stack_trace_contains_function_no_pie() { 82 | run_symbol_test( 83 | &["-g", "-O0", "-no-pie"], 84 | &["target_function", "main", "testprog.c"], 85 | ); 86 | } 87 | 88 | #[test] 89 | fn symbolized_stack_trace_contains_function_no_debug() { 90 | run_symbol_test(&["-O0"], &["target_function", "main"]); 91 | } 92 | 93 | #[test] 94 | fn symbolized_stack_trace_contains_function_g1() { 95 | run_symbol_test(&["-g1", "-O0"], &["target_function", "main", "testprog.c"]); 96 | } 97 | 98 | #[test] 99 | fn symbolized_stack_trace_contains_function_o2() { 100 | run_symbol_test(&["-g", "-O2"], &["target_function", "main", "testprog.c"]); 101 | } 102 | -------------------------------------------------------------------------------- /src/test_utils.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::current_date_string; 2 | use std::fs; 3 | use std::process::{Child, Command, Stdio}; 4 | use std::{thread, time::Duration}; 5 | use tempfile::{NamedTempFile, TempDir}; 6 | use zstd::stream; 7 | 8 | fn build_fuzmon_command( 9 | bin: &str, 10 | pid: u32, 11 | log_dir: &TempDir, 12 | cfg_file: &NamedTempFile, 13 | ) -> Command { 14 | let pid_s = pid.to_string(); 15 | let mut cmd = Command::new(bin); 16 | cmd.args([ 17 | "run", 18 | "-p", 19 | &pid_s, 20 | "-o", 21 | log_dir.path().to_str().unwrap(), 22 | "-c", 23 | cfg_file.path().to_str().unwrap(), 24 | ]); 25 | cmd 26 | } 27 | 28 | pub fn wait_until_file_appears(logdir: &TempDir, pid: u32) { 29 | let date = current_date_string(); 30 | let dir = logdir.path().join(&date); 31 | let plain = dir.join(format!("{pid}.jsonl")); 32 | let zst = dir.join(format!("{pid}.jsonl.zst")); 33 | for _ in 0..80 { 34 | if plain.exists() || zst.exists() { 35 | break; 36 | } 37 | thread::sleep(Duration::from_millis(10)); 38 | } 39 | } 40 | 41 | pub fn kill_with_sigint_and_wait(child: &mut Child) { 42 | unsafe { 43 | let _ = nix::libc::kill(child.id() as i32, nix::libc::SIGINT); 44 | } 45 | let _ = child.wait(); 46 | } 47 | 48 | pub fn create_config(threshold: f64) -> NamedTempFile { 49 | let cfg_file = NamedTempFile::new().expect("cfg"); 50 | fs::write( 51 | cfg_file.path(), 52 | format!( 53 | "[monitor]\nstacktrace_cpu_time_percent_threshold = {}", 54 | threshold 55 | ), 56 | ) 57 | .expect("write cfg"); 58 | cfg_file 59 | } 60 | 61 | pub fn run_fuzmon(bin: &str, pid: u32, log_dir: &TempDir) -> String { 62 | let cfg_file = create_config(0.0); 63 | 64 | let mut mon = build_fuzmon_command(bin, pid, log_dir, &cfg_file) 65 | .stdout(Stdio::null()) 66 | .spawn() 67 | .expect("run fuzmon"); 68 | 69 | wait_until_file_appears(log_dir, pid); 70 | kill_with_sigint_and_wait(&mut mon); 71 | 72 | collect_log_content(log_dir) 73 | } 74 | 75 | pub fn collect_log_content(log_dir: &TempDir) -> String { 76 | let mut log_content = String::new(); 77 | for entry in fs::read_dir(log_dir.path()).expect("read_dir") { 78 | let path = entry.expect("entry").path(); 79 | if path.is_dir() { 80 | for sub in fs::read_dir(&path).expect("read_dir") { 81 | let sub_path = sub.expect("subentry").path(); 82 | append_file(&sub_path, &mut log_content); 83 | } 84 | } else { 85 | append_file(&path, &mut log_content); 86 | } 87 | } 88 | log_content 89 | } 90 | 91 | fn append_file(path: &std::path::Path, log_content: &mut String) { 92 | if let Some(ext) = path.extension() { 93 | if ext == "zst" { 94 | if let Ok(data) = fs::read(path) { 95 | if let Ok(decoded) = stream::decode_all(&*data) { 96 | log_content.push_str(&String::from_utf8_lossy(&decoded)); 97 | return; 98 | } 99 | } 100 | } 101 | } 102 | if let Ok(s) = fs::read_to_string(path) { 103 | log_content.push_str(&s); 104 | } 105 | } 106 | 107 | pub fn run_fuzmon_output( 108 | bin: &str, 109 | pid: u32, 110 | log_dir: &TempDir, 111 | cfg_file: &NamedTempFile, 112 | ) -> std::process::Output { 113 | build_fuzmon_command(bin, pid, log_dir, cfg_file) 114 | .output() 115 | .expect("run fuzmon") 116 | } 117 | 118 | pub fn run_fuzmon_and_check(bin: &str, pid: u32, log_dir: &TempDir, expected: &[&str]) { 119 | let log_content = run_fuzmon(bin, pid, log_dir); 120 | 121 | for e in expected { 122 | assert!( 123 | log_content.contains(e), 124 | "expected '{}' in {}", 125 | e, 126 | log_content 127 | ); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/fd_events.rs: -------------------------------------------------------------------------------- 1 | use fuzmon::test_utils::wait_until_file_appears; 2 | use fuzmon::utils::current_date_string; 3 | use std::fs; 4 | use std::io::Write; 5 | use std::process::{Command, Stdio}; 6 | use std::{thread, time::Duration}; 7 | use tempfile::tempdir; 8 | use zstd::stream; 9 | 10 | #[test] 11 | fn detect_fd_open_close() { 12 | let dir = tempdir().expect("tempdir"); 13 | let file_path = dir.path().join("testfile"); 14 | let script = dir.path().join("script.py"); 15 | fs::write( 16 | &script, 17 | r#"import sys 18 | sys.stdin.readline() 19 | f=open("testfile", 'w') 20 | sys.stdin.readline() 21 | f.close() 22 | sys.stdin.readline() 23 | "#, 24 | ) 25 | .expect("write script"); 26 | 27 | let mut child = Command::new("python3") 28 | .arg(&script) 29 | .stdin(Stdio::piped()) 30 | .stdout(Stdio::null()) 31 | .spawn() 32 | .expect("spawn python"); 33 | 34 | let pid = child.id(); 35 | let mut child_in = child.stdin.take().expect("stdin"); 36 | 37 | let logdir = tempdir().expect("logdir"); 38 | let mut mon = Command::new(env!("CARGO_BIN_EXE_fuzmon")) 39 | .args([ 40 | "run", 41 | "-p", 42 | &pid.to_string(), 43 | "-o", 44 | logdir.path().to_str().unwrap(), 45 | ]) 46 | .stdout(Stdio::null()) 47 | .spawn() 48 | .expect("run fuzmon"); 49 | 50 | let date = current_date_string(); 51 | let base_dir = logdir.path().join(&date); 52 | let plain = base_dir.join(format!("{}.jsonl", pid)); 53 | let zst = base_dir.join(format!("{}.jsonl.zst", pid)); 54 | wait_until_file_appears(&logdir, pid); 55 | 56 | child_in.write_all(b"\n").unwrap(); 57 | child_in.flush().unwrap(); 58 | 59 | for _ in 0..50 { 60 | let path = if plain.exists() { &plain } else { &zst }; 61 | if path.exists() { 62 | let content = if path.extension().and_then(|e| e.to_str()) == Some("zst") { 63 | let data = fs::read(path).unwrap(); 64 | match stream::decode_all(&*data) { 65 | Ok(d) => String::from_utf8_lossy(&d).into_owned(), 66 | Err(_) => String::new(), 67 | } 68 | } else { 69 | fs::read_to_string(path).unwrap_or_default() 70 | }; 71 | if content.contains("\"event\":\"open\"") 72 | && content.contains(file_path.to_str().unwrap()) 73 | { 74 | break; 75 | } 76 | } 77 | thread::sleep(Duration::from_millis(10)); 78 | } 79 | 80 | child_in.write_all(b"\n").unwrap(); 81 | child_in.flush().unwrap(); 82 | 83 | for _ in 0..50 { 84 | let path = if plain.exists() { &plain } else { &zst }; 85 | if path.exists() { 86 | let content = if path.extension().and_then(|e| e.to_str()) == Some("zst") { 87 | let data = fs::read(path).unwrap(); 88 | match stream::decode_all(&*data) { 89 | Ok(d) => String::from_utf8_lossy(&d).into_owned(), 90 | Err(_) => String::new(), 91 | } 92 | } else { 93 | fs::read_to_string(path).unwrap_or_default() 94 | }; 95 | if content.contains("\"event\":\"close\"") { 96 | break; 97 | } 98 | } 99 | thread::sleep(Duration::from_millis(10)); 100 | } 101 | 102 | drop(child_in); 103 | 104 | let _ = child.wait(); 105 | fuzmon::test_utils::kill_with_sigint_and_wait(&mut mon); 106 | 107 | let path = if plain.exists() { &plain } else { &zst }; 108 | let log_content = if path.extension().and_then(|e| e.to_str()) == Some("zst") { 109 | let data = fs::read(path).unwrap(); 110 | match stream::decode_all(&*data) { 111 | Ok(d) => String::from_utf8_lossy(&d).into_owned(), 112 | Err(_) => String::new(), 113 | } 114 | } else { 115 | fs::read_to_string(path).unwrap_or_default() 116 | }; 117 | assert!( 118 | log_content.contains("\"event\":\"open\""), 119 | "{}", 120 | log_content 121 | ); 122 | assert!(log_content.contains("testfile"), "{}", log_content); 123 | assert!( 124 | log_content.contains("\"event\":\"close\""), 125 | "{}", 126 | log_content 127 | ); 128 | } 129 | -------------------------------------------------------------------------------- /tests/output_format.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::process::{Command, Stdio}; 3 | use tempfile::{NamedTempFile, tempdir}; 4 | 5 | use fuzmon::test_utils::wait_until_file_appears; 6 | use fuzmon::utils::current_date_string; 7 | 8 | fn run_with_format(fmt: &str) -> (tempfile::TempDir, std::path::PathBuf) { 9 | let logdir = tempdir().expect("logdir"); 10 | let cfg_file = NamedTempFile::new().expect("cfg"); 11 | let compress = if fmt.ends_with(".zst") { 12 | "true" 13 | } else { 14 | "false" 15 | }; 16 | fs::write( 17 | cfg_file.path(), 18 | format!( 19 | "[output]\npath='{}'\nformat='{}'\ncompress={}", 20 | logdir.path().display(), 21 | fmt, 22 | compress 23 | ), 24 | ) 25 | .expect("write cfg"); 26 | 27 | let mut child = Command::new("python3") 28 | .arg("-c") 29 | .arg("import sys; sys.stdin.read()") 30 | .stdin(Stdio::piped()) 31 | .stdout(Stdio::null()) 32 | .spawn() 33 | .expect("spawn python"); 34 | 35 | let pid = child.id(); 36 | 37 | let pid_s = pid.to_string(); 38 | let cfg_path = cfg_file.path().to_str().unwrap().to_string(); 39 | let logdir_s = logdir.path().to_str().unwrap().to_string(); 40 | let cmd_args = vec!["run", "-p", &pid_s, "-o", &logdir_s, "-c", &cfg_path]; 41 | let mut mon = Command::new(env!("CARGO_BIN_EXE_fuzmon")) 42 | .args(&cmd_args) 43 | .stdout(Stdio::null()) 44 | .spawn() 45 | .expect("run fuzmon"); 46 | 47 | wait_until_file_appears(&logdir, pid); 48 | fuzmon::test_utils::kill_with_sigint_and_wait(&mut mon); 49 | 50 | fuzmon::test_utils::kill_with_sigint_and_wait(&mut child); 51 | 52 | let date = current_date_string(); 53 | let subdir = logdir.path().join(date); 54 | let path = fs::read_dir(&subdir) 55 | .unwrap() 56 | .next() 57 | .unwrap() 58 | .unwrap() 59 | .path(); 60 | (logdir, path) 61 | } 62 | 63 | fn run_default() -> (tempfile::TempDir, std::path::PathBuf) { 64 | let logdir = tempdir().expect("logdir"); 65 | let mut child = Command::new("python3") 66 | .arg("-c") 67 | .arg("import sys; sys.stdin.read()") 68 | .stdin(Stdio::piped()) 69 | .stdout(Stdio::null()) 70 | .spawn() 71 | .expect("spawn python"); 72 | 73 | let pid = child.id(); 74 | 75 | let pid_s = pid.to_string(); 76 | let logdir_s = logdir.path().to_str().unwrap().to_string(); 77 | let cmd_args = vec!["run", "-p", &pid_s, "-o", &logdir_s]; 78 | let mut mon = Command::new(env!("CARGO_BIN_EXE_fuzmon")) 79 | .args(&cmd_args) 80 | .stdout(Stdio::null()) 81 | .spawn() 82 | .expect("run fuzmon"); 83 | 84 | wait_until_file_appears(&logdir, pid); 85 | fuzmon::test_utils::kill_with_sigint_and_wait(&mut mon); 86 | 87 | fuzmon::test_utils::kill_with_sigint_and_wait(&mut child); 88 | 89 | let date = current_date_string(); 90 | let subdir = logdir.path().join(date); 91 | let path = fs::read_dir(&subdir) 92 | .unwrap() 93 | .next() 94 | .unwrap() 95 | .unwrap() 96 | .path(); 97 | (logdir, path) 98 | } 99 | 100 | fn dump_file(path: &std::path::Path) -> String { 101 | let out = Command::new(env!("CARGO_BIN_EXE_fuzmon")) 102 | .args(["dump", path.to_str().unwrap()]) 103 | .output() 104 | .expect("run dump"); 105 | let mut s = String::new(); 106 | s.push_str(&String::from_utf8_lossy(&out.stdout)); 107 | s.push_str(&String::from_utf8_lossy(&out.stderr)); 108 | s 109 | } 110 | 111 | #[test] 112 | fn default_is_jsonl_zst() { 113 | let (dir, path) = run_default(); 114 | assert_eq!(path.extension().and_then(|e| e.to_str()), Some("zst")); 115 | let out = dump_file(&path); 116 | println!("out: {}", out); 117 | assert!(out.contains("process_name")); 118 | drop(dir); 119 | } 120 | 121 | #[test] 122 | fn jsonl_output_and_dump() { 123 | let (dir, path) = run_with_format("jsonl"); 124 | assert_eq!(path.extension().and_then(|e| e.to_str()), Some("jsonl")); 125 | let out = dump_file(&path); 126 | println!("out: {}", out); 127 | assert!(out.contains("process_name")); 128 | drop(dir); 129 | } 130 | 131 | #[test] 132 | fn msgpacks_output_and_dump() { 133 | let (dir, path) = run_with_format("msgpacks"); 134 | assert_eq!(path.extension().and_then(|e| e.to_str()), Some("msgpacks")); 135 | let out = dump_file(&path); 136 | println!("out: {}", out); 137 | assert!(out.contains("process_name")); 138 | drop(dir); 139 | } 140 | 141 | #[test] 142 | fn msgpacks_zst_output_and_dump() { 143 | let (dir, path) = run_with_format("msgpacks.zst"); 144 | assert_eq!(path.extension().and_then(|e| e.to_str()), Some("zst")); 145 | let out = dump_file(&path); 146 | println!("out: {}", out); 147 | assert!(out.contains("process_name")); 148 | drop(dir); 149 | } 150 | -------------------------------------------------------------------------------- /src/log.rs: -------------------------------------------------------------------------------- 1 | use log::warn; 2 | use rmp_serde::decode::{Error as MsgpackError, from_read as read_msgpack}; 3 | use rmp_serde::encode::write_named; 4 | use serde::{Deserialize, Serialize}; 5 | use std::fs::{self, OpenOptions}; 6 | use std::io::{self, BufRead, BufReader, Write}; 7 | use std::path::Path; 8 | 9 | use fuzmon::utils::current_date_string; 10 | 11 | #[derive(Serialize, Deserialize, Debug)] 12 | pub struct MemoryInfo { 13 | pub rss_kb: u64, 14 | pub vsz_kb: u64, 15 | pub swap_kb: u64, 16 | } 17 | 18 | #[derive(Serialize, Deserialize, Debug, Clone)] 19 | pub struct Frame { 20 | #[serde(skip_serializing_if = "Option::is_none")] 21 | pub addr: Option, 22 | #[serde(skip_serializing_if = "Option::is_none")] 23 | pub func: Option, 24 | #[serde(skip_serializing_if = "Option::is_none")] 25 | pub file: Option, 26 | #[serde(skip_serializing_if = "Option::is_none")] 27 | pub line: Option, 28 | } 29 | 30 | #[derive(Serialize, Deserialize, Debug)] 31 | pub struct ThreadInfo { 32 | pub tid: u32, 33 | #[serde(skip_serializing_if = "Option::is_none")] 34 | pub stacktrace: Option>, 35 | #[serde(skip_serializing_if = "Option::is_none")] 36 | pub python_stacktrace: Option>, 37 | } 38 | 39 | #[derive(Serialize, Deserialize, Debug)] 40 | pub struct FdLogEvent { 41 | pub fd: i32, 42 | pub event: String, 43 | pub path: String, 44 | } 45 | 46 | #[derive(Serialize, Deserialize, Debug)] 47 | pub struct LogEntry { 48 | pub timestamp: String, 49 | pub pid: u32, 50 | pub process_name: String, 51 | pub cpu_time_percent: f64, 52 | pub memory: MemoryInfo, 53 | #[serde(skip_serializing_if = "Option::is_none")] 54 | pub cmdline: Option, 55 | #[serde(skip_serializing_if = "Option::is_none")] 56 | pub env: Option, 57 | #[serde(skip_serializing_if = "Option::is_none")] 58 | pub fd_events: Option>, 59 | #[serde(default, skip_serializing_if = "Vec::is_empty")] 60 | pub threads: Vec, 61 | } 62 | 63 | pub fn write_log(dir: &str, entry: &LogEntry, use_msgpack: bool, compress: bool) { 64 | let date = current_date_string(); 65 | let dir = format!("{}/{}", dir.trim_end_matches('/'), date); 66 | if let Err(e) = fs::create_dir_all(&dir) { 67 | warn!("failed to create {}: {}", dir, e); 68 | } 69 | let ext = if use_msgpack { "msgpacks" } else { "jsonl" }; 70 | let base = format!("{}/{}.{}", dir, entry.pid, ext); 71 | let path = if compress { 72 | format!("{}.zst", base) 73 | } else { 74 | base 75 | }; 76 | match OpenOptions::new().create(true).append(true).open(&path) { 77 | Ok(file) => { 78 | if compress { 79 | match zstd::Encoder::new(file, 0) { 80 | Ok(mut enc) => { 81 | if use_msgpack { 82 | if let Err(e) = write_named(&mut enc, entry) { 83 | warn!("write msgpack failed: {}", e); 84 | } 85 | } else { 86 | if serde_json::to_writer(&mut enc, entry).is_err() { 87 | warn!("write json failed"); 88 | } 89 | if enc.write_all(b"\n").is_err() { 90 | warn!("write newline failed"); 91 | } 92 | } 93 | if let Err(e) = enc.finish() { 94 | warn!("finish zstd failed: {}", e); 95 | } 96 | } 97 | Err(e) => warn!("zstd init failed: {}", e), 98 | } 99 | } else { 100 | let mut file = file; 101 | if use_msgpack { 102 | if let Err(e) = write_named(&mut file, entry) { 103 | warn!("write msgpack failed: {}", e); 104 | } 105 | } else { 106 | if serde_json::to_writer(&mut file, entry).is_err() { 107 | warn!("write json failed"); 108 | } 109 | if file.write_all(b"\n").is_err() { 110 | warn!("write newline failed"); 111 | } 112 | } 113 | } 114 | } 115 | Err(e) => warn!("open {} failed: {}", path, e), 116 | } 117 | } 118 | 119 | pub fn read_log_entries(path: &Path) -> io::Result> { 120 | let file = fs::File::open(path)?; 121 | let is_zst = path.extension().and_then(|e| e.to_str()) == Some("zst"); 122 | let reader: Box = if is_zst { 123 | Box::new(zstd::Decoder::new(file)?) 124 | } else { 125 | Box::new(file) 126 | }; 127 | 128 | let ext = { 129 | let mut base = path.to_path_buf(); 130 | if is_zst { 131 | base.set_extension(""); 132 | } 133 | base.extension() 134 | .and_then(|e| e.to_str()) 135 | .unwrap_or("") 136 | .to_string() 137 | }; 138 | 139 | if ext == "msgpacks" { 140 | let mut r = reader; 141 | let mut entries = Vec::new(); 142 | loop { 143 | match read_msgpack(&mut r) { 144 | Ok(e) => entries.push(e), 145 | Err(MsgpackError::InvalidMarkerRead(ref ioe)) 146 | | Err(MsgpackError::InvalidDataRead(ref ioe)) 147 | if ioe.kind() == io::ErrorKind::UnexpectedEof => 148 | { 149 | break; 150 | } 151 | Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)), 152 | } 153 | } 154 | Ok(entries) 155 | } else { 156 | let buf = BufReader::new(reader); 157 | let mut entries = Vec::new(); 158 | for line in buf.lines() { 159 | let line = line?; 160 | if line.trim().is_empty() { 161 | continue; 162 | } 163 | match serde_json::from_str::(&line) { 164 | Ok(e) => entries.push(e), 165 | Err(e) => return Err(io::Error::new(io::ErrorKind::InvalidData, e)), 166 | } 167 | } 168 | Ok(entries) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/procinfo.rs: -------------------------------------------------------------------------------- 1 | use log::warn; 2 | use std::collections::HashMap; 3 | 4 | fn compute_cpu_percent(delta_proc: u64, delta_total: u64, num_cpus: usize) -> f32 { 5 | if delta_total == 0 { 6 | return 0.0; 7 | } 8 | 100.0 * delta_proc as f32 / delta_total as f32 * num_cpus as f32 9 | } 10 | use std::fs; 11 | use std::os::unix::fs::MetadataExt; 12 | 13 | #[derive(Default)] 14 | pub struct ProcState { 15 | pub prev_proc_time: u64, 16 | pub prev_total_time: u64, 17 | pub fds: HashMap, 18 | pub pending_fd_events: Vec, 19 | pub metadata_written: bool, 20 | } 21 | 22 | pub fn pid_uid(pid: u32) -> Option { 23 | match fs::metadata(format!("/proc/{}", pid)) { 24 | Ok(m) => Some(m.uid()), 25 | Err(e) => { 26 | warn!("metadata for {} failed: {}", pid, e); 27 | None 28 | } 29 | } 30 | } 31 | 32 | pub fn proc_exists(pid: u32) -> bool { 33 | match fs::read_to_string(format!("/proc/{}/stat", pid)) { 34 | Ok(data) => { 35 | let parts: Vec<&str> = data.split_whitespace().collect(); 36 | if let Some(state) = parts.get(2) { 37 | return *state != "Z"; 38 | } 39 | true 40 | } 41 | Err(_) => false, 42 | } 43 | } 44 | 45 | pub fn read_pids() -> Vec { 46 | let mut pids = Vec::new(); 47 | if let Ok(entries) = fs::read_dir("/proc") { 48 | for entry in entries.flatten() { 49 | if let Ok(name) = entry.file_name().into_string() { 50 | if let Ok(pid) = name.parse::() { 51 | pids.push(pid); 52 | } 53 | } 54 | } 55 | } else { 56 | warn!("read_dir /proc failed"); 57 | } 58 | pids 59 | } 60 | 61 | fn read_proc_stat(pid: u32) -> Option<(u64, u64)> { 62 | let data = match fs::read_to_string(format!("/proc/{}/stat", pid)) { 63 | Ok(d) => d, 64 | Err(e) => { 65 | warn!("read stat {} failed: {}", pid, e); 66 | return None; 67 | } 68 | }; 69 | let parts: Vec<&str> = data.split_whitespace().collect(); 70 | let utime = parts.get(13)?.parse::().ok()?; // field 14 71 | let stime = parts.get(14)?.parse::().ok()?; // field 15 72 | Some((utime, stime)) 73 | } 74 | 75 | pub fn read_fd_map(pid: u32) -> HashMap { 76 | let mut map = HashMap::new(); 77 | if let Ok(entries) = fs::read_dir(format!("/proc/{}/fd", pid)) { 78 | for entry in entries.flatten() { 79 | if let Ok(name) = entry.file_name().into_string() { 80 | if let Ok(fd) = name.parse::() { 81 | match fs::read_link(entry.path()) { 82 | Ok(target) => { 83 | if let Some(path) = target.to_str() { 84 | map.insert(fd, path.to_string()); 85 | } 86 | } 87 | Err(e) => warn!("read_link for {} fd {} failed: {}", pid, fd, e), 88 | } 89 | } 90 | } 91 | } 92 | } else { 93 | warn!("read_dir fd for {} failed", pid); 94 | } 95 | map 96 | } 97 | 98 | #[derive(Debug)] 99 | pub struct FdEvent { 100 | pub fd: i32, 101 | pub old_path: Option, 102 | pub new_path: Option, 103 | } 104 | 105 | pub fn detect_fd_events(pid: u32, state: &mut ProcState) -> Vec { 106 | let current = read_fd_map(pid); 107 | let mut events = Vec::new(); 108 | for (fd, old_path) in &state.fds { 109 | match current.get(fd) { 110 | None => events.push(FdEvent { 111 | fd: *fd, 112 | old_path: Some(old_path.clone()), 113 | new_path: None, 114 | }), 115 | Some(new_path) if new_path != old_path => events.push(FdEvent { 116 | fd: *fd, 117 | old_path: Some(old_path.clone()), 118 | new_path: Some(new_path.clone()), 119 | }), 120 | _ => {} 121 | } 122 | } 123 | for (fd, new_path) in ¤t { 124 | if !state.fds.contains_key(fd) { 125 | events.push(FdEvent { 126 | fd: *fd, 127 | old_path: None, 128 | new_path: Some(new_path.clone()), 129 | }); 130 | } 131 | } 132 | state.fds = current; 133 | events 134 | } 135 | 136 | fn read_status_value(pid: u32, key: &str) -> Option { 137 | let status = match fs::read_to_string(format!("/proc/{}/status", pid)) { 138 | Ok(s) => s, 139 | Err(e) => { 140 | warn!("read status {} failed: {}", pid, e); 141 | return None; 142 | } 143 | }; 144 | for line in status.lines() { 145 | if line.starts_with(key) { 146 | let parts: Vec<&str> = line.split_whitespace().collect(); 147 | if let Some(val) = parts.get(1) { 148 | return val.parse::().ok(); 149 | } 150 | } 151 | } 152 | None 153 | } 154 | 155 | pub fn process_name(pid: u32) -> Option { 156 | fs::read_to_string(format!("/proc/{}/comm", pid)) 157 | .ok() 158 | .map(|s| s.trim().to_string()) 159 | } 160 | 161 | pub fn vsz_kb(pid: u32) -> Option { 162 | read_status_value(pid, "VmSize:") 163 | } 164 | 165 | pub fn swap_kb(pid: u32) -> Option { 166 | read_status_value(pid, "VmSwap:") 167 | } 168 | 169 | pub fn cmdline(pid: u32) -> Option { 170 | fs::read(format!("/proc/{}/cmdline", pid)).ok().map(|data| { 171 | data.split(|&b| b == 0) 172 | .filter_map(|s| std::str::from_utf8(s).ok()) 173 | .filter(|s| !s.is_empty()) 174 | .collect::>() 175 | .join(" ") 176 | }) 177 | } 178 | 179 | pub fn environ(pid: u32) -> Option { 180 | fs::read(format!("/proc/{}/environ", pid)).ok().map(|data| { 181 | data.split(|&b| b == 0) 182 | .filter_map(|s| std::str::from_utf8(s).ok()) 183 | .filter(|s| !s.is_empty()) 184 | .collect::>() 185 | .join("\n") 186 | }) 187 | } 188 | 189 | fn read_total_cpu_time() -> Option { 190 | let data = match fs::read_to_string("/proc/stat") { 191 | Ok(d) => d, 192 | Err(e) => { 193 | warn!("read /proc/stat failed: {}", e); 194 | return None; 195 | } 196 | }; 197 | let line = data.lines().next()?; 198 | let mut total = 0u64; 199 | for v in line.split_whitespace().skip(1) { 200 | total += v.parse::().ok()?; 201 | } 202 | Some(total) 203 | } 204 | 205 | pub fn rss_kb(pid: u32) -> Option { 206 | let status = match fs::read_to_string(format!("/proc/{}/status", pid)) { 207 | Ok(s) => s, 208 | Err(e) => { 209 | warn!("read rss {} failed: {}", pid, e); 210 | return None; 211 | } 212 | }; 213 | for line in status.lines() { 214 | if line.starts_with("VmRSS:") { 215 | let parts: Vec<&str> = line.split_whitespace().collect(); 216 | if let Some(val) = parts.get(1) { 217 | return val.parse::().ok(); 218 | } 219 | } 220 | } 221 | None 222 | } 223 | 224 | pub fn get_proc_usage(pid: u32, state: &mut ProcState) -> Option<(f32, u64)> { 225 | let (u, s) = read_proc_stat(pid)?; 226 | let total = read_total_cpu_time()?; 227 | let proc_total = u + s; 228 | if state.prev_total_time == 0 { 229 | state.prev_proc_time = proc_total; 230 | state.prev_total_time = total; 231 | return None; 232 | } 233 | let delta_proc = proc_total.saturating_sub(state.prev_proc_time); 234 | let delta_total = total.saturating_sub(state.prev_total_time); 235 | state.prev_proc_time = proc_total; 236 | state.prev_total_time = total; 237 | if delta_total == 0 { 238 | return None; 239 | } 240 | let cpu = compute_cpu_percent(delta_proc, delta_total, num_cpus::get()); 241 | let rss = rss_kb(pid).unwrap_or(0); 242 | Some((cpu, rss)) 243 | } 244 | 245 | pub fn should_suppress(cpu: f32, rss_kb: u64) -> bool { 246 | cpu == 0.0 && rss_kb < 100 * 1024 247 | } 248 | 249 | #[cfg(test)] 250 | mod tests { 251 | use super::compute_cpu_percent; 252 | 253 | #[test] 254 | fn busy_two_threads_reports_200_percent() { 255 | let percent = compute_cpu_percent(2, 2, 2); 256 | assert!((percent - 200.0).abs() < f32::EPSILON); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | use log::warn; 3 | use serde::Deserialize; 4 | use std::fs; 5 | 6 | #[derive(Parser)] 7 | #[command(name = "fuzmon")] 8 | pub struct Cli { 9 | #[command(subcommand)] 10 | pub command: Option, 11 | } 12 | 13 | #[derive(Subcommand)] 14 | pub enum Commands { 15 | /// Run the monitor 16 | Run(RunArgs), 17 | /// Dump logs 18 | Dump(DumpArgs), 19 | /// Generate HTML report 20 | Report(ReportArgs), 21 | } 22 | 23 | #[derive(Parser, Clone)] 24 | pub struct DumpArgs { 25 | /// Path to log file or directory 26 | pub path: String, 27 | } 28 | 29 | #[derive(Parser, Clone)] 30 | pub struct ReportArgs { 31 | /// Path to log file or directory 32 | pub path: String, 33 | /// Path to configuration file 34 | #[arg(short = 'c', long)] 35 | pub config: Option, 36 | /// Output directory for HTML report 37 | #[arg(short = 'o', long)] 38 | pub output: Option, 39 | } 40 | 41 | #[derive(Parser, Default, Clone)] 42 | pub struct RunArgs { 43 | /// PID to trace 44 | #[arg(short, long)] 45 | pub pid: Option, 46 | /// Path to configuration file 47 | #[arg(short = 'c', long)] 48 | pub config: Option, 49 | /// User name filter 50 | #[arg(long)] 51 | pub target_user: Option, 52 | /// Output directory for logs 53 | #[arg(short = 'o', long)] 54 | pub output: Option, 55 | /// Verbose output 56 | #[arg(short, long)] 57 | pub verbose: bool, 58 | /// Command to run and monitor 59 | #[arg(trailing_var_arg = true)] 60 | pub command: Vec, 61 | } 62 | 63 | #[derive(Default, Deserialize)] 64 | #[serde(deny_unknown_fields)] 65 | pub struct FilterConfig { 66 | #[serde(default)] 67 | pub target_user: Option, 68 | #[serde(default)] 69 | pub ignore_process_name: Option>, 70 | } 71 | 72 | #[derive(Default, Deserialize)] 73 | #[serde(deny_unknown_fields)] 74 | pub struct OutputConfig { 75 | #[serde(default)] 76 | pub format: Option, 77 | #[serde(default)] 78 | pub path: Option, 79 | #[serde(default)] 80 | pub compress: Option, 81 | } 82 | 83 | #[derive(Default, Deserialize)] 84 | #[serde(deny_unknown_fields)] 85 | pub struct MonitorConfig { 86 | #[serde(default)] 87 | pub interval_sec: Option, 88 | #[serde(default)] 89 | pub record_cpu_time_percent_threshold: Option, 90 | #[serde(default)] 91 | pub stacktrace_cpu_time_percent_threshold: Option, 92 | } 93 | 94 | #[derive(Default, Deserialize, Clone)] 95 | #[serde(deny_unknown_fields)] 96 | pub struct ReportConfig { 97 | #[serde(default)] 98 | pub top_cpu: Option, 99 | #[serde(default)] 100 | pub top_rss: Option, 101 | } 102 | 103 | #[derive(Default, Deserialize)] 104 | #[serde(deny_unknown_fields)] 105 | pub struct Config { 106 | #[serde(default)] 107 | pub filter: FilterConfig, 108 | #[serde(default)] 109 | pub output: OutputConfig, 110 | #[serde(default)] 111 | pub monitor: MonitorConfig, 112 | #[serde(default)] 113 | pub report: ReportConfig, 114 | } 115 | 116 | pub fn load_config(path: &str) -> Config { 117 | let data = fs::read_to_string(path).unwrap_or_else(|e| { 118 | warn!("failed to read {}: {}", path, e); 119 | panic!("failed to read {}: {}", path, e); 120 | }); 121 | toml::from_str(&data).unwrap_or_else(|e| { 122 | warn!("failed to parse {}: {}", path, e); 123 | panic!("failed to parse {}: {}", path, e); 124 | }) 125 | } 126 | 127 | pub fn uid_from_name(name: &str) -> Option { 128 | let passwd = fs::read_to_string("/etc/passwd").ok()?; 129 | for line in passwd.lines() { 130 | let mut parts = line.split(':'); 131 | if let (Some(user), Some(_), Some(uid_str)) = (parts.next(), parts.next(), parts.next()) { 132 | if user == name { 133 | if let Ok(uid) = uid_str.parse::() { 134 | return Some(uid); 135 | } 136 | } 137 | } 138 | } 139 | None 140 | } 141 | 142 | pub fn merge_config(mut cfg: Config, args: &RunArgs) -> Config { 143 | if let Some(ref u) = args.target_user { 144 | cfg.filter.target_user = Some(u.clone()); 145 | } 146 | if let Some(ref p) = args.output { 147 | cfg.output.path = Some(p.clone()); 148 | } 149 | if cfg.output.path.is_none() { 150 | cfg.output.path = Some("/tmp/fuzmon".into()); 151 | } 152 | if cfg.output.compress.is_none() { 153 | cfg.output.compress = Some(true); 154 | } 155 | if cfg.monitor.record_cpu_time_percent_threshold.is_none() { 156 | cfg.monitor.record_cpu_time_percent_threshold = Some(0.0); 157 | } 158 | if cfg.monitor.stacktrace_cpu_time_percent_threshold.is_none() { 159 | cfg.monitor.stacktrace_cpu_time_percent_threshold = Some(1.0); 160 | } 161 | cfg 162 | } 163 | 164 | pub fn finalize_report_config(mut cfg: ReportConfig) -> ReportConfig { 165 | if cfg.top_cpu.is_none() { 166 | cfg.top_cpu = Some(10); 167 | } 168 | if cfg.top_rss.is_none() { 169 | cfg.top_rss = Some(10); 170 | } 171 | cfg 172 | } 173 | 174 | pub fn parse_cli() -> Cli { 175 | Cli::parse() 176 | } 177 | 178 | #[cfg(test)] 179 | mod tests { 180 | use super::*; 181 | use std::fs; 182 | use tempfile::NamedTempFile; 183 | 184 | #[test] 185 | fn load_example_config() { 186 | let cfg = load_config("examples/daemon.toml"); 187 | assert_eq!(cfg.output.format.as_deref(), Some("msgpack")); 188 | assert_eq!(cfg.output.path.as_deref(), Some("/var/log/fuzmon/")); 189 | assert_eq!(cfg.output.compress, Some(true)); 190 | assert_eq!(cfg.monitor.interval_sec, Some(60)); 191 | assert_eq!(cfg.monitor.record_cpu_time_percent_threshold, Some(0.0)); 192 | assert_eq!(cfg.monitor.stacktrace_cpu_time_percent_threshold, Some(1.0)); 193 | assert_eq!(cfg.report.top_cpu, None); 194 | assert_eq!(cfg.report.top_rss, None); 195 | } 196 | 197 | #[test] 198 | fn cli_overrides_config() { 199 | let tmp = NamedTempFile::new().expect("tmp"); 200 | fs::write( 201 | tmp.path(), 202 | "[filter]\ntarget_user = \"hoge\"\n[output]\npath = \"/tmp/a\"", 203 | ) 204 | .unwrap(); 205 | let cfg = load_config(tmp.path().to_str().unwrap()); 206 | let args = RunArgs { 207 | target_user: Some("foo".into()), 208 | output: Some("/tmp/b".into()), 209 | ..Default::default() 210 | }; 211 | let merged = merge_config(cfg, &args); 212 | assert_eq!(merged.filter.target_user.as_deref(), Some("foo")); 213 | assert_eq!(merged.output.path.as_deref(), Some("/tmp/b")); 214 | } 215 | 216 | #[test] 217 | fn default_output_path() { 218 | let cfg = Config::default(); 219 | let args = RunArgs::default(); 220 | let merged = merge_config(cfg, &args); 221 | assert_eq!(merged.output.path.as_deref(), Some("/tmp/fuzmon")); 222 | assert_eq!(merged.output.compress, Some(true)); 223 | assert_eq!(merged.monitor.record_cpu_time_percent_threshold, Some(0.0)); 224 | assert_eq!( 225 | merged.monitor.stacktrace_cpu_time_percent_threshold, 226 | Some(1.0) 227 | ); 228 | } 229 | 230 | #[test] 231 | fn report_config_defaults() { 232 | let cfg = finalize_report_config(ReportConfig::default()); 233 | assert_eq!(cfg.top_cpu, Some(10)); 234 | assert_eq!(cfg.top_rss, Some(10)); 235 | } 236 | 237 | #[test] 238 | fn invalid_config_panics() { 239 | let tmp = NamedTempFile::new().expect("tmp"); 240 | fs::write(tmp.path(), "[filter]\nignore_process_name = false").unwrap(); 241 | let result = std::panic::catch_unwind(|| load_config(tmp.path().to_str().unwrap())); 242 | assert!(result.is_err()); 243 | let err = result.err().unwrap(); 244 | let msg = if let Some(s) = err.downcast_ref::() { 245 | s.as_str() 246 | } else if let Some(s) = err.downcast_ref::<&str>() { 247 | *s 248 | } else { 249 | "" 250 | }; 251 | assert!(msg.contains("ignore_process_name")); 252 | assert!(msg.contains("invalid type")); 253 | } 254 | 255 | #[test] 256 | fn unknown_field_panics() { 257 | let tmp = NamedTempFile::new().expect("tmp"); 258 | fs::write(tmp.path(), "[output]\nfoo=1").unwrap(); 259 | let result = std::panic::catch_unwind(|| load_config(tmp.path().to_str().unwrap())); 260 | assert!(result.is_err()); 261 | let err = result.err().unwrap(); 262 | let msg = if let Some(s) = err.downcast_ref::() { 263 | s.as_str() 264 | } else if let Some(s) = err.downcast_ref::<&str>() { 265 | *s 266 | } else { 267 | "" 268 | }; 269 | assert!(msg.contains("foo")); 270 | assert!(msg.contains("unknown field")); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /tests/report.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use std::process::{Command, Stdio}; 3 | use tempfile::{NamedTempFile, tempdir}; 4 | 5 | #[test] 6 | fn html_report_has_stats() { 7 | let dir = tempdir().expect("dir"); 8 | let mut child = Command::new("sleep") 9 | .arg("5") 10 | .env("REPORT_VAR", "123") 11 | .stdout(Stdio::null()) 12 | .spawn() 13 | .expect("spawn"); 14 | let pid = child.id(); 15 | let log_path = dir.path().join(format!("{pid}.jsonl")); 16 | fs::write( 17 | &log_path, 18 | format!( 19 | "{{\"timestamp\":\"2025-06-14T00:00:00Z\",\"pid\":{pid},\"process_name\":\"sleep\",\"cpu_time_percent\":50.0,\"memory\":{{\"rss_kb\":1000,\"vsz_kb\":0,\"swap_kb\":0}},\"cmdline\":\"sleep 5\",\"env\":\"REPORT_VAR=123\"}}\n{{\"timestamp\":\"2025-06-14T00:00:10Z\",\"pid\":{pid},\"process_name\":\"sleep\",\"cpu_time_percent\":0.0,\"memory\":{{\"rss_kb\":2000,\"vsz_kb\":0,\"swap_kb\":0}}}}\n" 20 | ), 21 | ) 22 | .unwrap(); 23 | 24 | unsafe { 25 | let _ = nix::libc::kill(child.id() as i32, nix::libc::SIGKILL); 26 | } 27 | let _ = child.wait(); 28 | 29 | let outdir = tempdir().expect("outdir"); 30 | let out = Command::new(env!("CARGO_BIN_EXE_fuzmon")) 31 | .args([ 32 | "report", 33 | log_path.to_str().unwrap(), 34 | "-o", 35 | outdir.path().to_str().unwrap(), 36 | ]) 37 | .output() 38 | .expect("run report"); 39 | let stdout = String::from_utf8_lossy(&out.stdout); 40 | assert!( 41 | stdout.contains(outdir.path().to_str().unwrap()), 42 | "{}", 43 | stdout 44 | ); 45 | let html = fs::read_to_string(outdir.path().join("index.html")).unwrap(); 46 | assert!(html.contains("Total runtime: 10"), "{}", html); 47 | assert!(html.contains("Total CPU time"), "{}", html); 48 | assert!(html.contains("Average CPU usage"), "{}", html); 49 | assert!(html.contains("2000"), "{}", html); 50 | assert!(html.contains("REPORT_VAR"), "{}", html); 51 | assert!(html.contains("Start"), "{}", html); 97 | assert!(html.contains("End"), "{}", html); 98 | let pos1 = html.find("1111").expect("1111"); 99 | let pos2 = html.find("2222").expect("2222"); 100 | assert!(pos1 < pos2, "order: {}", html); 101 | assert!(outdir.path().join("1111.html").exists()); 102 | assert!(outdir.path().join("2222.html").exists()); 103 | assert!(outdir.path().join("1111_cpu.svg").exists()); 104 | assert!(outdir.path().join("1111_rss.svg").exists()); 105 | assert!(outdir.path().join("2222_cpu.svg").exists()); 106 | assert!(outdir.path().join("2222_rss.svg").exists()); 107 | assert!(outdir.path().join("top_cpu.svg").exists()); 108 | assert!(outdir.path().join("top_rss.svg").exists()); 109 | assert!(html.contains("top_cpu.svg"), "{}", html); 110 | assert!(html.contains("top_rss.svg"), "{}", html); 111 | } 112 | 113 | #[test] 114 | fn command_column_collapsed() { 115 | let dir = tempdir().expect("dir"); 116 | let log = dir.path().join("1234.jsonl"); 117 | fs::write( 118 | &log, 119 | "{\"timestamp\":\"2025-06-14T00:00:00Z\",\"pid\":1234,\"process_name\":\"a\",\"cpu_time_percent\":0.0,\"memory\":{\"rss_kb\":1000,\"vsz_kb\":0,\"swap_kb\":0},\"cmdline\":\"a very very long command line that should be collapsed\"}\n{\"timestamp\":\"2025-06-14T00:00:10Z\",\"pid\":1234,\"process_name\":\"a\",\"cpu_time_percent\":0.0,\"memory\":{\"rss_kb\":1000,\"vsz_kb\":0,\"swap_kb\":0}}\n", 120 | ) 121 | .unwrap(); 122 | 123 | let outdir = tempdir().expect("outdir"); 124 | let out = Command::new(env!("CARGO_BIN_EXE_fuzmon")) 125 | .args([ 126 | "report", 127 | dir.path().to_str().unwrap(), 128 | "-o", 129 | outdir.path().to_str().unwrap(), 130 | ]) 131 | .output() 132 | .expect("run report dir"); 133 | assert!(out.status.success()); 134 | let html = fs::read_to_string(outdir.path().join("index.html")).unwrap(); 135 | assert!(html.contains("
"), "{}", html); 136 | assert!(html.contains("border-collapse"), "{}", html); 137 | } 138 | 139 | #[test] 140 | fn trace_json_created_with_stacktrace() { 141 | use fuzmon::test_utils::run_fuzmon; 142 | use fuzmon::utils::current_date_string; 143 | use std::io::{BufRead, BufReader, Write}; 144 | 145 | let dir = tempdir().expect("dir"); 146 | let script = dir.path().join("test.py"); 147 | fs::write( 148 | &script, 149 | r#"import sys 150 | def foo(): 151 | print('ready', flush=True) 152 | sys.stdin.readline() 153 | foo() 154 | "#, 155 | ) 156 | .unwrap(); 157 | 158 | let mut child = Command::new("python3") 159 | .arg(&script) 160 | .stdin(Stdio::piped()) 161 | .stdout(Stdio::piped()) 162 | .spawn() 163 | .expect("spawn python"); 164 | let mut child_in = child.stdin.take().unwrap(); 165 | let mut child_out = BufReader::new(child.stdout.take().unwrap()); 166 | let mut line = String::new(); 167 | child_out.read_line(&mut line).unwrap(); 168 | assert_eq!(line.trim(), "ready"); 169 | 170 | let pid = child.id(); 171 | let logdir = tempdir().expect("logdir"); 172 | run_fuzmon(env!("CARGO_BIN_EXE_fuzmon"), pid, &logdir); 173 | 174 | child_in.write_all(b"\n").unwrap(); 175 | drop(child_in); 176 | let _ = child.wait(); 177 | 178 | let date = current_date_string(); 179 | let base = logdir.path().join(&date).join(format!("{pid}.jsonl")); 180 | let log_path = if base.exists() { 181 | base 182 | } else { 183 | base.with_extension("jsonl.zst") 184 | }; 185 | 186 | let outdir = tempdir().expect("outdir"); 187 | let out = Command::new(env!("CARGO_BIN_EXE_fuzmon")) 188 | .args([ 189 | "report", 190 | log_path.to_str().unwrap(), 191 | "-o", 192 | outdir.path().to_str().unwrap(), 193 | ]) 194 | .output() 195 | .expect("run report"); 196 | assert!(out.status.success()); 197 | let trace_path = outdir.path().join(format!("{pid}_trace.json")); 198 | assert!(trace_path.exists()); 199 | let trace = fs::read_to_string(trace_path).unwrap(); 200 | assert!(trace.contains("traceEvents"), "{}", trace); 201 | let html = fs::read_to_string(outdir.path().join("index.html")).unwrap(); 202 | assert!( 203 | html.contains(&format!(" = HashSet::new(); 308 | for e in events { 309 | if let Some(tid) = e.get("tid").and_then(|v| v.as_u64()) { 310 | tids.insert(tid); 311 | } 312 | } 313 | 314 | let mut has_pair = false; 315 | for tid in &tids { 316 | if tid % 2 == 0 && tids.contains(&(tid + 1)) { 317 | has_pair = true; 318 | break; 319 | } 320 | } 321 | assert!(has_pair, "no separate python row: {:?}", tids); 322 | } 323 | -------------------------------------------------------------------------------- /src/stacktrace.rs: -------------------------------------------------------------------------------- 1 | use addr2line::Loader; 2 | use log::{info, warn}; 3 | use nix::sys::{ptrace, wait::waitpid}; 4 | use nix::unistd::Pid; 5 | use object::{Object, ObjectKind}; 6 | use py_spy::{Config as PySpyConfig, PythonSpy}; 7 | use std::borrow::Cow; 8 | use std::cell::RefCell; 9 | use std::collections::HashMap; 10 | use std::fs; 11 | use std::io::Read; 12 | use std::rc::Rc; 13 | use std::time::SystemTime; 14 | 15 | use crate::log::Frame; 16 | 17 | struct CachedModule { 18 | module: Option>, 19 | mtime: Option, 20 | } 21 | 22 | thread_local! { 23 | static MODULE_CACHE: RefCell> = RefCell::new(HashMap::new()); 24 | } 25 | 26 | pub struct ModuleData { 27 | loader: Rc, 28 | is_pic: bool, 29 | } 30 | 31 | fn get_module(path: &str) -> Option> { 32 | if path.starts_with("[") { 33 | return None; 34 | } 35 | let meta = match fs::metadata(path) { 36 | Ok(m) => m, 37 | Err(_) => return None, 38 | }; 39 | if !meta.file_type().is_file() { 40 | return None; 41 | } 42 | let mtime = meta.modified().ok(); 43 | MODULE_CACHE.with(|cache| { 44 | let mut map = cache.borrow_mut(); 45 | if let Some(entry) = map.get(path) { 46 | if entry.mtime == mtime { 47 | return entry.module.clone(); 48 | } 49 | info!( 50 | "mmaped file {} mtime changed, reloading: old_mtime={:?} new_mtime={:?}", 51 | path, entry.mtime, mtime 52 | ); 53 | map.remove(path); 54 | } 55 | let mut header = [0u8; 4]; 56 | match fs::File::open(path).and_then(|mut f| f.read_exact(&mut header)) { 57 | Ok(_) => { 58 | if header != [0x7f, b'E', b'L', b'F'] { 59 | map.insert( 60 | path.to_string(), 61 | CachedModule { 62 | module: None, 63 | mtime, 64 | }, 65 | ); 66 | return None; 67 | } 68 | } 69 | Err(e) => { 70 | warn!("read {} failed: {}", path, e); 71 | map.insert( 72 | path.to_string(), 73 | CachedModule { 74 | module: None, 75 | mtime, 76 | }, 77 | ); 78 | return None; 79 | } 80 | } 81 | match Loader::new(path) { 82 | Ok(loader) => { 83 | info!("load debug symbols from {}", path); 84 | let mut is_pic = false; 85 | match fs::read(path) { 86 | Ok(data) => match object::File::parse(&*data) { 87 | Ok(obj) => { 88 | is_pic = matches!(obj.kind(), ObjectKind::Dynamic); 89 | } 90 | Err(e) => warn!("parse {} failed: {}", path, e), 91 | }, 92 | Err(e) => warn!("read {} failed: {}", path, e), 93 | } 94 | let rc = Rc::new(ModuleData { 95 | loader: Rc::new(loader), 96 | is_pic, 97 | }); 98 | map.insert( 99 | path.to_string(), 100 | CachedModule { 101 | module: Some(rc.clone()), 102 | mtime, 103 | }, 104 | ); 105 | Some(rc) 106 | } 107 | Err(e) => { 108 | warn!("Loader::new {} failed: {}", path, e); 109 | map.insert( 110 | path.to_string(), 111 | CachedModule { 112 | module: None, 113 | mtime, 114 | }, 115 | ); 116 | None 117 | } 118 | } 119 | }) 120 | } 121 | 122 | pub struct ExeInfo { 123 | pub start: u64, 124 | pub end: u64, 125 | pub offset: u64, 126 | } 127 | 128 | pub struct Module { 129 | pub loader: Rc, 130 | pub info: ExeInfo, 131 | pub is_pic: bool, 132 | } 133 | 134 | pub fn load_loaders(pid: i32) -> Vec { 135 | let maps = match fs::read_to_string(format!("/proc/{}/maps", pid)) { 136 | Ok(m) => m, 137 | Err(e) => { 138 | warn!("read maps {} failed: {}", pid, e); 139 | return Vec::new(); 140 | } 141 | }; 142 | let mut infos: HashMap = HashMap::new(); 143 | for line in maps.lines() { 144 | let mut parts = line.split_whitespace(); 145 | let range = match parts.next() { 146 | Some(v) => v, 147 | None => continue, 148 | }; 149 | let _perms = match parts.next() { 150 | Some(v) => v, 151 | None => continue, 152 | }; 153 | let offset = match parts.next() { 154 | Some(v) => v, 155 | None => continue, 156 | }; 157 | let _dev = parts.next(); 158 | let _inode = parts.next(); 159 | let path = match parts.next() { 160 | Some(v) => v, 161 | None => continue, 162 | }; 163 | if let Some((start, end)) = range.split_once('-') { 164 | if let (Ok(start_addr), Ok(end_addr), Ok(off)) = ( 165 | u64::from_str_radix(start, 16), 166 | u64::from_str_radix(end, 16), 167 | u64::from_str_radix(offset, 16), 168 | ) { 169 | let entry = infos.entry(path.to_string()).or_insert(ExeInfo { 170 | start: start_addr, 171 | end: end_addr, 172 | offset: off, 173 | }); 174 | if start_addr < entry.start { 175 | entry.start = start_addr; 176 | entry.offset = off; 177 | } 178 | if end_addr > entry.end { 179 | entry.end = end_addr; 180 | } 181 | } 182 | } 183 | } 184 | let mut modules = Vec::new(); 185 | for (path, info) in infos { 186 | if let Some(data) = get_module(&path) { 187 | modules.push(Module { 188 | loader: data.loader.clone(), 189 | info, 190 | is_pic: data.is_pic, 191 | }); 192 | } 193 | } 194 | modules 195 | } 196 | 197 | fn describe_addr(loader: &Rc, info: &ExeInfo, addr: u64, is_pic: bool) -> Option { 198 | if addr < info.start || addr >= info.end { 199 | return None; 200 | } 201 | let mut probe = addr; 202 | if is_pic { 203 | probe = addr.wrapping_sub(info.start).wrapping_add(info.offset); 204 | } 205 | probe = probe.wrapping_sub(loader.relative_address_base()); 206 | 207 | let mut func = None; 208 | let mut file = None; 209 | let mut line = None; 210 | let mut found_frames = false; 211 | if let Ok(mut frames) = loader.find_frames(probe) { 212 | while let Ok(Some(frame)) = frames.next() { 213 | found_frames = true; 214 | if func.is_none() { 215 | if let Some(f) = &frame.function { 216 | func = Some(f.demangle().unwrap_or_else(|_| Cow::from("??")).into()); 217 | } 218 | } 219 | if let Some(loc) = frame.location { 220 | if file.is_none() { 221 | file = loc.file.map(|s| s.to_string()); 222 | } 223 | if line.is_none() { 224 | line = loc.line.map(|l| l as i32); 225 | } 226 | } 227 | } 228 | } 229 | if !found_frames { 230 | if let Some(sym) = loader.find_symbol(probe) { 231 | func = Some(sym.to_string()); 232 | } 233 | } 234 | Some(Frame { 235 | addr: Some(addr as i64), 236 | func, 237 | file, 238 | line, 239 | }) 240 | } 241 | 242 | fn get_stack_trace(pid: Pid, max_frames: usize) -> nix::Result> { 243 | let regs = ptrace::getregs(pid)?; 244 | let mut rbp = regs.rbp as u64; 245 | let mut addrs = Vec::new(); 246 | addrs.push(regs.rip as u64); 247 | 248 | for _ in 0..max_frames { 249 | if rbp == 0 || rbp >= 0xfffffffffffffff8 { 250 | break; 251 | } 252 | let next_rip = ptrace::read(pid, (rbp + 8) as ptrace::AddressType)? as u64; 253 | addrs.push(next_rip); 254 | let next_rbp = ptrace::read(pid, rbp as ptrace::AddressType)? as u64; 255 | if next_rbp == 0 { 256 | break; 257 | } 258 | rbp = next_rbp; 259 | } 260 | 261 | Ok(addrs) 262 | } 263 | 264 | pub fn capture_stack_trace(pid: i32) -> nix::Result> { 265 | let target = Pid::from_raw(pid); 266 | ptrace::attach(target)?; 267 | waitpid(target, None)?; 268 | 269 | let res = (|| { 270 | let stack = get_stack_trace(target, 32)?; 271 | let modules = load_loaders(pid); 272 | let mut frames = Vec::new(); 273 | for addr in stack { 274 | let mut added = false; 275 | for m in &modules { 276 | if let Some(info) = describe_addr(&m.loader, &m.info, addr, m.is_pic) { 277 | frames.push(info); 278 | added = true; 279 | break; 280 | } 281 | } 282 | if !added { 283 | frames.push(Frame { 284 | addr: Some(addr as i64), 285 | func: None, 286 | file: None, 287 | line: None, 288 | }); 289 | } 290 | } 291 | Ok(frames) 292 | })(); 293 | 294 | if let Err(e) = ptrace::detach(target, None) { 295 | warn!("detach failed: {}", e); 296 | } 297 | res 298 | } 299 | 300 | pub fn capture_c_stack_traces(pid: i32) -> Vec<(i32, Option>)> { 301 | let mut tids: Vec = match fs::read_dir(format!("/proc/{}/task", pid)) { 302 | Ok(d) => d 303 | .filter_map(|e| e.ok()) 304 | .filter_map(|e| e.file_name().into_string().ok()) 305 | .filter_map(|s| s.parse::().ok()) 306 | .collect(), 307 | Err(_) => Vec::new(), 308 | }; 309 | tids.sort_unstable(); 310 | let mut traces = Vec::new(); 311 | for tid in tids { 312 | match capture_stack_trace(tid) { 313 | Ok(t) => traces.push((tid, Some(t))), 314 | Err(_) => traces.push((tid, None)), 315 | } 316 | } 317 | traces 318 | } 319 | 320 | pub fn capture_python_stack_traces( 321 | pid: i32, 322 | ) -> Result>, Box> { 323 | let config = PySpyConfig::default(); 324 | let mut spy = PythonSpy::new(pid as py_spy::Pid, &config)?; 325 | let traces = spy.get_stack_traces()?; 326 | let mut result = HashMap::new(); 327 | for t in traces { 328 | if let Some(tid) = t.os_thread_id { 329 | let mut frames = Vec::new(); 330 | for f in t.frames { 331 | frames.push(Frame { 332 | addr: None, 333 | func: Some(f.name), 334 | file: Some(f.filename), 335 | line: Some(f.line as i32), 336 | }); 337 | } 338 | result.insert(tid as u32, frames); 339 | } 340 | } 341 | Ok(result) 342 | } 343 | 344 | #[cfg(test)] 345 | mod tests { 346 | use super::*; 347 | use std::process::Command; 348 | use tempfile::tempdir; 349 | 350 | fn clear_cache() { 351 | MODULE_CACHE.with(|c| c.borrow_mut().clear()); 352 | } 353 | 354 | #[test] 355 | fn loader_none_for_nonexistent() { 356 | clear_cache(); 357 | assert!(get_module("/no/such/file").is_none()); 358 | } 359 | 360 | #[test] 361 | fn loader_none_for_non_regular() { 362 | clear_cache(); 363 | assert!(get_module("/dev/null").is_none()); 364 | } 365 | 366 | #[test] 367 | fn loader_none_for_non_elf() { 368 | clear_cache(); 369 | let dir = tempdir().unwrap(); 370 | let file = dir.path().join("plain.txt"); 371 | std::fs::write(&file, b"plain").unwrap(); 372 | assert!(get_module(file.to_str().unwrap()).is_none()); 373 | assert!(get_module(file.to_str().unwrap()).is_none()); 374 | } 375 | 376 | #[test] 377 | fn loader_retry_after_update() { 378 | clear_cache(); 379 | let dir = tempdir().unwrap(); 380 | let exe = dir.path().join("tprog"); 381 | std::fs::write(&exe, b"bad").unwrap(); 382 | assert!(get_module(exe.to_str().unwrap()).is_none()); 383 | assert!(get_module(exe.to_str().unwrap()).is_none()); 384 | 385 | let src = dir.path().join("t.c"); 386 | std::fs::write(&src, "int main(){return 0;}").unwrap(); 387 | let status = Command::new("gcc") 388 | .args([src.to_str().unwrap(), "-o", exe.to_str().unwrap()]) 389 | .status() 390 | .expect("compile"); 391 | assert!(status.success()); 392 | assert!(get_module(exe.to_str().unwrap()).is_some()); 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /src/run.rs: -------------------------------------------------------------------------------- 1 | use chrono::Utc; 2 | use log::{info, warn}; 3 | use regex::Regex; 4 | use std::collections::{HashMap, HashSet}; 5 | use std::fs; 6 | use std::sync::{ 7 | Arc, 8 | atomic::{AtomicBool, Ordering}, 9 | }; 10 | use std::thread::sleep; 11 | use std::time::Duration; 12 | 13 | use crate::config::{Config, RunArgs, load_config, merge_config, uid_from_name}; 14 | use crate::log::{FdLogEvent, LogEntry, MemoryInfo, ThreadInfo, write_log}; 15 | use crate::procinfo::{ 16 | ProcState, cmdline, detect_fd_events, environ, get_proc_usage, pid_uid, proc_exists, 17 | process_name, read_pids, rss_kb, should_suppress, swap_kb, vsz_kb, 18 | }; 19 | use crate::stacktrace::{capture_c_stack_traces, capture_python_stack_traces}; 20 | 21 | pub fn run(args: RunArgs) { 22 | let config = match args.config.as_deref() { 23 | Some(path) => load_config(path), 24 | None => Config::default(), 25 | }; 26 | let config = merge_config(config, &args); 27 | 28 | let ignore_patterns: Vec = config 29 | .filter 30 | .ignore_process_name 31 | .unwrap_or_default() 32 | .into_iter() 33 | .filter_map(|p| Regex::new(&p).ok()) 34 | .collect(); 35 | 36 | let mut format = config.output.format.as_deref().unwrap_or("jsonl.zst"); 37 | format = match format { 38 | "json" => "jsonl", 39 | "json.zst" => "jsonl.zst", 40 | "msgpack" => "msgpacks", 41 | "msgpack.zst" => "msgpacks.zst", 42 | other => other, 43 | }; 44 | let use_msgpack = matches!(format, "msgpacks" | "msgpacks.zst"); 45 | let compress = config 46 | .output 47 | .compress 48 | .unwrap_or_else(|| format.ends_with(".zst")); 49 | let verbose = args.verbose; 50 | 51 | let output_dir = config.output.path.as_deref(); 52 | if let Some(dir) = output_dir { 53 | if let Err(e) = fs::create_dir_all(dir) { 54 | warn!("failed to create {}: {}", dir, e); 55 | } 56 | } 57 | 58 | let mut child = None; 59 | let mut target_pid = args.pid.map(|p| p as u32); 60 | if target_pid.is_none() && !args.command.is_empty() { 61 | let mut cmd = std::process::Command::new(&args.command[0]); 62 | if args.command.len() > 1 { 63 | cmd.args(&args.command[1..]); 64 | } 65 | match cmd.spawn() { 66 | Ok(c) => { 67 | target_pid = Some(c.id()); 68 | child = Some(c); 69 | info!("spawned {} as pid {}", args.command[0], target_pid.unwrap()); 70 | } 71 | Err(e) => { 72 | let msg = format!("failed to spawn {}: {}", args.command[0], e); 73 | println!("{}", msg); 74 | warn!("{}", msg); 75 | return; 76 | } 77 | } 78 | } 79 | 80 | if let Some(pid) = target_pid { 81 | if fs::metadata(format!("/proc/{}", pid)).is_err() { 82 | let msg = format!("pid {} not found", pid); 83 | println!("{}", msg); 84 | warn!("{}", msg); 85 | return; 86 | } 87 | } 88 | 89 | let target_uid = config.filter.target_user.as_deref().and_then(uid_from_name); 90 | 91 | let interval = config.monitor.interval_sec.unwrap_or(0); 92 | let sleep_dur = if interval == 0 { 93 | Duration::from_millis(200) 94 | } else { 95 | Duration::from_secs(interval) 96 | }; 97 | 98 | let record_cpu_percent_threshold = config 99 | .monitor 100 | .record_cpu_time_percent_threshold 101 | .unwrap_or(0.0); 102 | let stacktrace_cpu_percent_threshold = config 103 | .monitor 104 | .stacktrace_cpu_time_percent_threshold 105 | .unwrap_or(1.0); 106 | 107 | let term = Arc::new(AtomicBool::new(false)); 108 | { 109 | let t = term.clone(); 110 | ctrlc::set_handler(move || { 111 | t.store(true, Ordering::SeqCst); 112 | info!("SIGINT received, shutting down"); 113 | }) 114 | .expect("set SIGINT handler"); 115 | } 116 | 117 | let mut states: HashMap = HashMap::new(); 118 | loop { 119 | if let Some(pid) = target_pid { 120 | if !proc_exists(pid) { 121 | let name = process_name(pid).unwrap_or_else(|| "?".to_string()); 122 | let msg = format!("Process {pid} ({name}) disappeared, exiting"); 123 | println!("{}", msg); 124 | info!("{}", msg); 125 | break; 126 | } 127 | } 128 | monitor_iteration( 129 | &mut states, 130 | target_pid, 131 | target_uid, 132 | &ignore_patterns, 133 | record_cpu_percent_threshold, 134 | stacktrace_cpu_percent_threshold, 135 | output_dir, 136 | use_msgpack, 137 | compress, 138 | verbose, 139 | ); 140 | if let Some(ref mut c) = child { 141 | if c.try_wait().ok().flatten().is_some() { 142 | break; 143 | } 144 | } else if let Some(pid) = target_pid { 145 | if fs::metadata(format!("/proc/{}", pid)).is_err() { 146 | break; 147 | } 148 | } 149 | if term.load(Ordering::SeqCst) { 150 | break; 151 | } 152 | let mut elapsed = Duration::from_millis(0); 153 | while elapsed < sleep_dur { 154 | if term.load(Ordering::SeqCst) { 155 | return; 156 | } 157 | let step = std::cmp::min(Duration::from_millis(100), sleep_dur - elapsed); 158 | sleep(step); 159 | elapsed += step; 160 | } 161 | if term.load(Ordering::SeqCst) { 162 | break; 163 | } 164 | } 165 | if term.load(Ordering::SeqCst) { 166 | monitor_iteration( 167 | &mut states, 168 | target_pid, 169 | target_uid, 170 | &ignore_patterns, 171 | record_cpu_percent_threshold, 172 | stacktrace_cpu_percent_threshold, 173 | output_dir, 174 | use_msgpack, 175 | compress, 176 | verbose, 177 | ); 178 | } 179 | if let Some(mut c) = child { 180 | let _ = c.wait(); 181 | } 182 | } 183 | 184 | fn monitor_iteration( 185 | states: &mut HashMap, 186 | target_pid: Option, 187 | target_uid: Option, 188 | ignore_patterns: &[Regex], 189 | record_cpu_percent_threshold: f64, 190 | stacktrace_cpu_percent_threshold: f64, 191 | output_dir: Option<&str>, 192 | use_msgpack: bool, 193 | compress: bool, 194 | verbose: bool, 195 | ) { 196 | let pids = collect_pids(target_pid, target_uid); 197 | if verbose { 198 | println!("Found {} PIDs", pids.len()); 199 | } 200 | prune_states(states, &pids, output_dir, use_msgpack, compress); 201 | for pid in &pids { 202 | process_pid( 203 | *pid, 204 | states, 205 | target_pid, 206 | ignore_patterns, 207 | record_cpu_percent_threshold, 208 | stacktrace_cpu_percent_threshold, 209 | output_dir, 210 | use_msgpack, 211 | compress, 212 | verbose, 213 | ); 214 | } 215 | } 216 | 217 | fn collect_pids(target_pid: Option, target_uid: Option) -> Vec { 218 | let mut pids = if let Some(pid) = target_pid { 219 | if fs::metadata(format!("/proc/{}", pid)).is_ok() { 220 | vec![pid] 221 | } else { 222 | Vec::new() 223 | } 224 | } else { 225 | read_pids() 226 | }; 227 | if target_pid.is_none() { 228 | if let Some(uid) = target_uid { 229 | pids.retain(|p| pid_uid(*p) == Some(uid)); 230 | } 231 | } 232 | pids 233 | } 234 | 235 | fn prune_states( 236 | states: &mut HashMap, 237 | pids: &[u32], 238 | output_dir: Option<&str>, 239 | use_msgpack: bool, 240 | compress: bool, 241 | ) { 242 | let existing: Vec = states.keys().copied().collect(); 243 | let pid_set: HashSet = pids.iter().copied().collect(); 244 | for old in &existing { 245 | if !pid_set.contains(old) { 246 | if let Some(mut state) = states.remove(old) { 247 | if let Some(dir) = output_dir { 248 | let events: Vec = state 249 | .fds 250 | .drain() 251 | .map(|(fd, path)| FdLogEvent { 252 | fd, 253 | event: "close".into(), 254 | path, 255 | }) 256 | .collect(); 257 | if !events.is_empty() { 258 | let entry = LogEntry { 259 | timestamp: Utc::now() 260 | .to_rfc3339_opts(chrono::SecondsFormat::Secs, true), 261 | pid: *old, 262 | process_name: process_name(*old).unwrap_or_else(|| "?".into()), 263 | cpu_time_percent: 0.0, 264 | memory: MemoryInfo { 265 | rss_kb: 0, 266 | vsz_kb: 0, 267 | swap_kb: 0, 268 | }, 269 | cmdline: None, 270 | env: None, 271 | fd_events: Some(events), 272 | threads: Vec::new(), 273 | }; 274 | write_log(dir, &entry, use_msgpack, compress); 275 | } 276 | } 277 | } 278 | info!("process {} disappeared", old); 279 | } 280 | } 281 | } 282 | 283 | fn process_pid( 284 | pid: u32, 285 | states: &mut HashMap, 286 | target_pid: Option, 287 | ignore_patterns: &[Regex], 288 | record_cpu_percent_threshold: f64, 289 | stacktrace_cpu_percent_threshold: f64, 290 | output_dir: Option<&str>, 291 | use_msgpack: bool, 292 | compress: bool, 293 | verbose: bool, 294 | ) { 295 | let is_new = !states.contains_key(&pid); 296 | let state = states.entry(pid).or_default(); 297 | let usage = get_proc_usage(pid, state); 298 | let cpu = usage.map(|u| u.0).unwrap_or(0.0); 299 | if should_skip_pid( 300 | pid, 301 | target_pid, 302 | ignore_patterns, 303 | record_cpu_percent_threshold, 304 | cpu, 305 | ) { 306 | return; 307 | } 308 | if is_new { 309 | info!("new process {}", pid); 310 | } 311 | let raw_events = detect_fd_events(pid, state); 312 | state.pending_fd_events.extend(raw_events); 313 | let rss = usage 314 | .map(|u| u.1) 315 | .unwrap_or_else(|| rss_kb(pid).unwrap_or(0)); 316 | let fd_log_events: Vec = state 317 | .pending_fd_events 318 | .drain(..) 319 | .flat_map(|ev| { 320 | let mut events = Vec::new(); 321 | if let Some(old_path) = ev.old_path { 322 | events.push(FdLogEvent { 323 | fd: ev.fd, 324 | event: "close".into(), 325 | path: old_path, 326 | }); 327 | } 328 | if let Some(new_path) = ev.new_path { 329 | events.push(FdLogEvent { 330 | fd: ev.fd, 331 | event: "open".into(), 332 | path: new_path, 333 | }); 334 | } 335 | events 336 | }) 337 | .collect(); 338 | 339 | if verbose && !should_suppress(cpu, rss) { 340 | println!("PID {:>5}: {:>5.1}% CPU, {:>8} KB RSS", pid, cpu, rss); 341 | } 342 | 343 | if let Some(dir) = output_dir { 344 | let entry = build_log_entry( 345 | pid, 346 | state, 347 | cpu, 348 | rss, 349 | fd_log_events, 350 | stacktrace_cpu_percent_threshold, 351 | ); 352 | if verbose { 353 | if let Ok(line) = serde_json::to_string(&entry) { 354 | println!("{}", line); 355 | } 356 | } 357 | write_log(dir, &entry, use_msgpack, compress); 358 | } 359 | } 360 | 361 | fn should_skip_pid( 362 | pid: u32, 363 | target_pid: Option, 364 | ignore_patterns: &[Regex], 365 | record_cpu_percent_threshold: f64, 366 | cpu_percent: f32, 367 | ) -> bool { 368 | if target_pid.is_none() { 369 | if let Some(name) = process_name(pid) { 370 | if ignore_patterns.iter().any(|re| re.is_match(&name)) { 371 | return true; 372 | } 373 | } 374 | if cpu_percent < record_cpu_percent_threshold as f32 { 375 | return true; 376 | } 377 | } 378 | false 379 | } 380 | 381 | fn build_log_entry( 382 | pid: u32, 383 | state: &mut ProcState, 384 | cpu_percent: f32, 385 | rss: u64, 386 | fd_events: Vec, 387 | stacktrace_cpu_percent_threshold: f64, 388 | ) -> LogEntry { 389 | let mut entry = LogEntry { 390 | timestamp: Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true), 391 | pid, 392 | process_name: process_name(pid).unwrap_or_else(|| "?".into()), 393 | cpu_time_percent: cpu_percent as f64, 394 | memory: MemoryInfo { 395 | rss_kb: rss, 396 | vsz_kb: vsz_kb(pid).unwrap_or(0), 397 | swap_kb: swap_kb(pid).unwrap_or(0), 398 | }, 399 | cmdline: None, 400 | env: None, 401 | fd_events: if fd_events.is_empty() { 402 | None 403 | } else { 404 | Some(fd_events) 405 | }, 406 | threads: Vec::new(), 407 | }; 408 | if !state.metadata_written { 409 | entry.cmdline = cmdline(pid); 410 | entry.env = environ(pid); 411 | state.metadata_written = true; 412 | } 413 | if cpu_percent >= stacktrace_cpu_percent_threshold as f32 { 414 | let name = &entry.process_name; 415 | let mut c_traces = capture_c_stack_traces(pid as i32); 416 | let mut py_traces = if name.starts_with("python") { 417 | match capture_python_stack_traces(pid as i32) { 418 | Ok(t) => t, 419 | Err(e) => { 420 | warn!("python trace failed: {}", e); 421 | HashMap::new() 422 | } 423 | } 424 | } else { 425 | HashMap::new() 426 | }; 427 | for (tid, c) in c_traces.drain(..) { 428 | let py = py_traces.remove(&(tid as u32)); 429 | entry.threads.push(ThreadInfo { 430 | tid: tid as u32, 431 | stacktrace: c, 432 | python_stacktrace: py, 433 | }); 434 | } 435 | for (tid, py) in py_traces.into_iter() { 436 | entry.threads.push(ThreadInfo { 437 | tid, 438 | stacktrace: None, 439 | python_stacktrace: Some(py), 440 | }); 441 | } 442 | } 443 | entry 444 | } 445 | -------------------------------------------------------------------------------- /src/report.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Local}; 2 | use html_escape::encode_text; 3 | use log::warn; 4 | use plotters::prelude::*; 5 | use serde_json::json; 6 | use std::collections::HashMap; 7 | use std::fs; 8 | use std::io; 9 | use std::path::{Path, PathBuf}; 10 | 11 | use crate::config::{ReportArgs, finalize_report_config, load_config}; 12 | use crate::log::{Frame, LogEntry, read_log_entries}; 13 | 14 | const CPU_MIN: f64 = 0.1; 15 | 16 | #[derive(Clone)] 17 | struct Stats { 18 | pid: u32, 19 | cmd: String, 20 | env: Option, 21 | start: DateTime, 22 | end: DateTime, 23 | runtime: i64, 24 | cpu: f64, 25 | avg_cpu: f64, 26 | peak_rss: u64, 27 | path: String, 28 | } 29 | 30 | fn calc_stats(path: &Path, entries: &[LogEntry]) -> Option { 31 | if entries.is_empty() { 32 | return None; 33 | } 34 | let mut sorted: Vec<&LogEntry> = entries.iter().collect(); 35 | sorted.sort_by_key(|e| e.timestamp.clone()); 36 | let first = sorted[0]; 37 | let pid = first.pid; 38 | let cmd = first.cmdline.clone().unwrap_or_else(|| "(unknown)".into()); 39 | let env = first.env.clone(); 40 | let start = chrono::DateTime::parse_from_rfc3339(&first.timestamp) 41 | .map(|t| t.with_timezone(&Local)) 42 | .unwrap(); 43 | let end = chrono::DateTime::parse_from_rfc3339(&sorted.last().unwrap().timestamp) 44 | .map(|t| t.with_timezone(&Local)) 45 | .unwrap(); 46 | let runtime = (end - start).num_seconds(); 47 | let mut cpu = 0.0f64; 48 | let mut peak_rss = 0u64; 49 | for win in sorted.windows(2) { 50 | if let [a, b] = win { 51 | let ta = chrono::DateTime::parse_from_rfc3339(&a.timestamp) 52 | .map(|t| t.with_timezone(&Local)) 53 | .unwrap(); 54 | let tb = chrono::DateTime::parse_from_rfc3339(&b.timestamp) 55 | .map(|t| t.with_timezone(&Local)) 56 | .unwrap(); 57 | let dt = (tb - ta).num_seconds() as f64; 58 | cpu += a.cpu_time_percent * dt / 100.0; 59 | } 60 | } 61 | for e in &sorted { 62 | peak_rss = peak_rss.max(e.memory.rss_kb); 63 | } 64 | let avg_cpu = if runtime > 0 { 65 | cpu * 100.0 / runtime as f64 66 | } else { 67 | 0.0 68 | }; 69 | Some(Stats { 70 | pid, 71 | cmd, 72 | env, 73 | start, 74 | end, 75 | runtime, 76 | cpu, 77 | avg_cpu, 78 | peak_rss, 79 | path: path.display().to_string(), 80 | }) 81 | } 82 | 83 | fn collect_files(dir: &Path, files: &mut Vec) { 84 | if let Ok(entries) = fs::read_dir(dir) { 85 | for entry in entries.flatten() { 86 | let p = entry.path(); 87 | if p.is_dir() { 88 | collect_files(&p, files); 89 | } else if p.is_file() { 90 | files.push(p); 91 | } 92 | } 93 | } 94 | } 95 | 96 | #[derive(Clone, Copy)] 97 | enum GraphField { 98 | Cpu, 99 | Rss, 100 | } 101 | 102 | fn write_svg(entries: &[LogEntry], out: &Path, field: GraphField) -> io::Result<()> { 103 | if entries.is_empty() { 104 | return Ok(()); 105 | } 106 | let mut sorted: Vec<&LogEntry> = entries.iter().collect(); 107 | sorted.sort_by_key(|e| e.timestamp.clone()); 108 | let start = chrono::DateTime::parse_from_rfc3339(&sorted[0].timestamp) 109 | .map(|t| t.with_timezone(&Local)) 110 | .unwrap(); 111 | let end = chrono::DateTime::parse_from_rfc3339(&sorted.last().unwrap().timestamp) 112 | .map(|t| t.with_timezone(&Local)) 113 | .unwrap(); 114 | 115 | let mut max_val = 0.0f64; 116 | let mut series = Vec::new(); 117 | for e in &sorted { 118 | let t = chrono::DateTime::parse_from_rfc3339(&e.timestamp) 119 | .map(|tt| tt.with_timezone(&Local)) 120 | .unwrap(); 121 | let v = match field { 122 | GraphField::Cpu => e.cpu_time_percent, 123 | GraphField::Rss => e.memory.rss_kb as f64, 124 | }; 125 | max_val = max_val.max(v); 126 | series.push((t, v)); 127 | } 128 | if max_val <= 0.0 { 129 | max_val = 1.0; 130 | } 131 | 132 | let root = SVGBackend::new(out, (600, 300)).into_drawing_area(); 133 | root.fill(&WHITE) 134 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 135 | let (y_desc, caption, scale) = match field { 136 | GraphField::Cpu => ("CPU %", "CPU usage (%)", 1.0), 137 | GraphField::Rss => { 138 | if max_val >= 1024.0 * 1024.0 { 139 | ("RSS GB", "Resident set size (GB)", 1024.0 * 1024.0) 140 | } else { 141 | ("RSS MB", "Resident set size (MB)", 1024.0) 142 | } 143 | } 144 | }; 145 | let y_max = (max_val / scale).max(1.0); 146 | if matches!(field, GraphField::Cpu) { 147 | let mut chart = ChartBuilder::on(&root) 148 | .caption(caption, ("sans-serif", 20)) 149 | .margin(5) 150 | .x_label_area_size(40) 151 | .y_label_area_size(40) 152 | .build_cartesian_2d(start..end, (CPU_MIN..y_max).log_scale()) 153 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 154 | chart 155 | .configure_mesh() 156 | .x_desc("time") 157 | .y_desc(y_desc) 158 | .x_labels(5) 159 | .y_labels(5) 160 | .x_label_formatter(&|dt| dt.format("%H:%M:%S").to_string()) 161 | .draw() 162 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 163 | chart 164 | .draw_series(LineSeries::new( 165 | series.into_iter().map(|(x, v)| { 166 | let val = v / scale; 167 | let val = if val < CPU_MIN { CPU_MIN } else { val }; 168 | (x, val) 169 | }), 170 | &BLUE, 171 | )) 172 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 173 | root.present() 174 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) 175 | } else { 176 | let mut chart = ChartBuilder::on(&root) 177 | .caption(caption, ("sans-serif", 20)) 178 | .margin(5) 179 | .x_label_area_size(40) 180 | .y_label_area_size(40) 181 | .build_cartesian_2d(start..end, 0f64..y_max) 182 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 183 | chart 184 | .configure_mesh() 185 | .x_desc("time") 186 | .y_desc(y_desc) 187 | .x_labels(5) 188 | .y_labels(5) 189 | .x_label_formatter(&|dt| dt.format("%H:%M:%S").to_string()) 190 | .draw() 191 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 192 | chart 193 | .draw_series(LineSeries::new( 194 | series.into_iter().map(|(x, v)| (x, v / scale)), 195 | &BLUE, 196 | )) 197 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; 198 | root.present() 199 | .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) 200 | } 201 | } 202 | 203 | fn collect_series( 204 | entries: &[LogEntry], 205 | field: GraphField, 206 | ) -> ( 207 | Vec<(DateTime, f64)>, 208 | DateTime, 209 | DateTime, 210 | ) { 211 | if entries.is_empty() { 212 | let now = Local::now(); 213 | return (Vec::new(), now, now); 214 | } 215 | let mut sorted: Vec<&LogEntry> = entries.iter().collect(); 216 | sorted.sort_by_key(|e| e.timestamp.clone()); 217 | let start = chrono::DateTime::parse_from_rfc3339(&sorted[0].timestamp) 218 | .map(|t| t.with_timezone(&Local)) 219 | .unwrap(); 220 | let end = chrono::DateTime::parse_from_rfc3339(&sorted.last().unwrap().timestamp) 221 | .map(|t| t.with_timezone(&Local)) 222 | .unwrap(); 223 | let mut series = Vec::new(); 224 | for e in &sorted { 225 | let t = chrono::DateTime::parse_from_rfc3339(&e.timestamp) 226 | .map(|tt| tt.with_timezone(&Local)) 227 | .unwrap(); 228 | let v = match field { 229 | GraphField::Cpu => e.cpu_time_percent, 230 | GraphField::Rss => e.memory.rss_kb as f64, 231 | }; 232 | series.push((t, v)); 233 | } 234 | (series, start, end) 235 | } 236 | 237 | fn write_multi_svg(stats: &[Stats], out: &Path, field: GraphField) { 238 | let mut data = Vec::new(); 239 | let mut start_all: Option> = None; 240 | let mut end_all: Option> = None; 241 | let mut max_val = 0.0f64; 242 | for s in stats { 243 | if let Ok(entries) = read_log_entries(Path::new(&s.path)) { 244 | let (series, start, end) = collect_series(&entries, field); 245 | if series.is_empty() { 246 | continue; 247 | } 248 | start_all = Some(start_all.map_or(start, |cur| cur.min(start))); 249 | end_all = Some(end_all.map_or(end, |cur| cur.max(end))); 250 | for &(_, v) in &series { 251 | max_val = max_val.max(v); 252 | } 253 | let token = s.cmd.split_whitespace().next().unwrap_or(""); 254 | let base = Path::new(token) 255 | .file_name() 256 | .map(|b| b.to_string_lossy().into_owned()) 257 | .unwrap_or_else(|| token.to_string()); 258 | let label = format!("{} {}", s.pid, base); 259 | data.push((label, series)); 260 | } 261 | } 262 | if data.is_empty() { 263 | return; 264 | } 265 | if max_val <= 0.0 { 266 | max_val = 1.0; 267 | } 268 | let start = match start_all { 269 | Some(s) => s, 270 | None => return, 271 | }; 272 | let end = end_all.unwrap_or(start + chrono::Duration::seconds(1)); 273 | let root = SVGBackend::new(out, (600, 300)).into_drawing_area(); 274 | if root.fill(&WHITE).is_err() { 275 | return; 276 | } 277 | let (y_desc, caption, scale) = match field { 278 | GraphField::Cpu => ("CPU %", "Top CPU usage", 1.0), 279 | GraphField::Rss => { 280 | if max_val >= 1024.0 * 1024.0 { 281 | ("RSS GB", "Top RSS (GB)", 1024.0 * 1024.0) 282 | } else { 283 | ("RSS MB", "Top RSS (MB)", 1024.0) 284 | } 285 | } 286 | }; 287 | let y_max = (max_val / scale).max(1.0); 288 | if matches!(field, GraphField::Cpu) { 289 | let mut chart = match ChartBuilder::on(&root) 290 | .caption(caption, ("sans-serif", 20)) 291 | .margin(5) 292 | .x_label_area_size(40) 293 | .y_label_area_size(40) 294 | .build_cartesian_2d(start..end, (CPU_MIN..y_max).log_scale()) 295 | { 296 | Ok(c) => c, 297 | Err(_) => return, 298 | }; 299 | if chart 300 | .configure_mesh() 301 | .x_desc("time") 302 | .y_desc(y_desc) 303 | .x_labels(5) 304 | .y_labels(5) 305 | .x_label_formatter(&|dt| dt.format("%H:%M:%S").to_string()) 306 | .draw() 307 | .is_err() 308 | { 309 | return; 310 | } 311 | for (i, (label, series)) in data.into_iter().enumerate() { 312 | let color = Palette99::pick(i).mix(0.9); 313 | if chart 314 | .draw_series(LineSeries::new( 315 | series.into_iter().map(|(x, v)| { 316 | let val = v / scale; 317 | let val = if val < CPU_MIN { CPU_MIN } else { val }; 318 | (x, val) 319 | }), 320 | &color, 321 | )) 322 | .map(|l| { 323 | l.label(label).legend(move |(x, y)| { 324 | PathElement::new(vec![(x, y), (x + 20, y)], color.clone()) 325 | }) 326 | }) 327 | .is_err() 328 | { 329 | return; 330 | } 331 | } 332 | let _ = chart.configure_series_labels().border_style(&BLACK).draw(); 333 | let _ = root.present(); 334 | } else { 335 | let mut chart = match ChartBuilder::on(&root) 336 | .caption(caption, ("sans-serif", 20)) 337 | .margin(5) 338 | .x_label_area_size(40) 339 | .y_label_area_size(40) 340 | .build_cartesian_2d(start..end, 0f64..y_max) 341 | { 342 | Ok(c) => c, 343 | Err(_) => return, 344 | }; 345 | if chart 346 | .configure_mesh() 347 | .x_desc("time") 348 | .y_desc(y_desc) 349 | .x_labels(5) 350 | .y_labels(5) 351 | .x_label_formatter(&|dt| dt.format("%H:%M:%S").to_string()) 352 | .draw() 353 | .is_err() 354 | { 355 | return; 356 | } 357 | for (i, (label, series)) in data.into_iter().enumerate() { 358 | let color = Palette99::pick(i).mix(0.9); 359 | if chart 360 | .draw_series(LineSeries::new( 361 | series.into_iter().map(|(x, v)| (x, v / scale)), 362 | &color, 363 | )) 364 | .map(|l| { 365 | l.label(label).legend(move |(x, y)| { 366 | PathElement::new(vec![(x, y), (x + 20, y)], color.clone()) 367 | }) 368 | }) 369 | .is_err() 370 | { 371 | return; 372 | } 373 | } 374 | let _ = chart.configure_series_labels().border_style(&BLACK).draw(); 375 | let _ = root.present(); 376 | } 377 | } 378 | 379 | fn write_chrome_trace(entries: &[LogEntry], out: &Path) -> io::Result<()> { 380 | if entries.is_empty() { 381 | return Ok(()); 382 | } 383 | let mut sorted: Vec<&LogEntry> = entries.iter().collect(); 384 | sorted.sort_by_key(|e| e.timestamp.clone()); 385 | let mut events = Vec::new(); 386 | use std::collections::HashMap; 387 | let mut active: HashMap<(u32, usize), (String, serde_json::Value, i64, u32)> = HashMap::new(); 388 | 389 | fn handle_frames( 390 | tid: u32, 391 | frames: &[&Frame], 392 | pid: u32, 393 | ts: i64, 394 | active: &mut HashMap<(u32, usize), (String, serde_json::Value, i64, u32)>, 395 | events: &mut Vec, 396 | ) { 397 | if frames.is_empty() { 398 | return; 399 | } 400 | 401 | // handle existing events beyond current depth 402 | let mut depth = frames.len(); 403 | loop { 404 | let key = (tid, depth); 405 | if let Some((name, args, start, pid_saved)) = active.remove(&key) { 406 | let dur = ts - start; 407 | events.push(json!({ 408 | "name": name, 409 | "ph": "X", 410 | "pid": pid_saved, 411 | "tid": tid, 412 | "ts": start, 413 | "dur": if dur <= 0 { 1 } else { dur }, 414 | "args": args, 415 | })); 416 | depth += 1; 417 | } else { 418 | break; 419 | } 420 | } 421 | 422 | for (idx, frame) in frames.iter().enumerate() { 423 | let name = if let Some(f) = &frame.func { 424 | f.clone() 425 | } else if let Some(a) = frame.addr { 426 | format!("{:#x}", a) 427 | } else { 428 | "?".to_string() 429 | }; 430 | let args = json!({ 431 | "addr": frame.addr, 432 | "file": frame.file, 433 | "line": frame.line, 434 | }); 435 | let key = (tid, idx); 436 | match active.get_mut(&key) { 437 | Some((cur, cur_args, _start, _pid)) if cur == &name => { 438 | *cur_args = args; 439 | } 440 | Some((cur, cur_args, start, pid_saved)) => { 441 | let dur = ts - *start; 442 | events.push(json!({ 443 | "name": cur, 444 | "ph": "X", 445 | "pid": *pid_saved, 446 | "tid": tid, 447 | "ts": *start, 448 | "dur": if dur <= 0 { 1 } else { dur }, 449 | "args": cur_args.clone(), 450 | })); 451 | *cur = name; 452 | *cur_args = args; 453 | *start = ts; 454 | *pid_saved = pid; 455 | } 456 | None => { 457 | active.insert(key, (name, args, ts, pid)); 458 | } 459 | } 460 | } 461 | } 462 | 463 | for (i, e) in sorted.iter().enumerate() { 464 | if e.threads.is_empty() { 465 | continue; 466 | } 467 | let dt = chrono::DateTime::parse_from_rfc3339(&e.timestamp) 468 | .map(|t| t.with_timezone(&Local)) 469 | .map_err(|er| io::Error::new(io::ErrorKind::InvalidData, er))?; 470 | let ts = dt.timestamp_micros(); 471 | 472 | for t in &e.threads { 473 | if let Some(st) = &t.stacktrace { 474 | let frames: Vec<&Frame> = st.iter().collect(); 475 | handle_frames(t.tid << 1, &frames, e.pid, ts, &mut active, &mut events); 476 | } 477 | if let Some(py) = &t.python_stacktrace { 478 | let frames: Vec<&Frame> = py.iter().collect(); 479 | handle_frames( 480 | (t.tid << 1) | 1, 481 | &frames, 482 | e.pid, 483 | ts, 484 | &mut active, 485 | &mut events, 486 | ); 487 | } 488 | } 489 | 490 | if i == sorted.len() - 1 { 491 | let final_ts = ts; 492 | for ((tid, _idx), (name, args, start, pid)) in active.drain() { 493 | let dur = final_ts - start; 494 | events.push(json!({ 495 | "name": name, 496 | "ph": "X", 497 | "pid": pid, 498 | "tid": tid, 499 | "ts": start, 500 | "dur": if dur <= 0 { 1 } else { dur }, 501 | "args": args, 502 | })); 503 | } 504 | } 505 | } 506 | if events.is_empty() { 507 | return Ok(()); 508 | } 509 | let obj = json!({ "traceEvents": events }); 510 | fs::write(out, serde_json::to_vec(&obj)?) 511 | } 512 | 513 | fn write_graphs(entries: &[LogEntry], out_dir: &Path, pid: u32) { 514 | let cpu_path = out_dir.join(format!("{}_cpu.svg", pid)); 515 | if let Err(e) = write_svg(entries, &cpu_path, GraphField::Cpu) { 516 | warn!("failed to write {}: {}", cpu_path.display(), e); 517 | } 518 | let rss_path = out_dir.join(format!("{}_rss.svg", pid)); 519 | if let Err(e) = write_svg(entries, &rss_path, GraphField::Rss) { 520 | warn!("failed to write {}: {}", rss_path.display(), e); 521 | } 522 | } 523 | 524 | fn write_trace(entries: &[LogEntry], out_dir: &Path, pid: u32) -> bool { 525 | let path = out_dir.join(format!("{}_trace.json", pid)); 526 | if let Err(e) = write_chrome_trace(entries, &path) { 527 | warn!("failed to write {}: {}", path.display(), e); 528 | return false; 529 | } 530 | path.exists() 531 | } 532 | 533 | fn truncate(s: &str, len: usize) -> String { 534 | let mut out = String::new(); 535 | for (i, c) in s.chars().enumerate() { 536 | if i >= len { 537 | out.push_str("..."); 538 | break; 539 | } 540 | out.push(c); 541 | } 542 | out 543 | } 544 | 545 | fn render_single(s: &Stats, has_trace: bool) -> String { 546 | let mut out = String::new(); 547 | out.push_str("\n"); 548 | out.push_str(&format!("

Report for PID {}

\n", s.pid)); 549 | out.push_str(&format!("

Command: {}

\n", encode_text(&s.cmd))); 550 | out.push_str("
    \n"); 551 | out.push_str(&format!("
  • Total runtime: {} sec
  • \n", s.runtime)); 552 | out.push_str(&format!("
  • Total CPU time: {:.1} sec
  • \n", s.cpu)); 553 | out.push_str(&format!("
  • Average CPU usage: {:.1}%
  • \n", s.avg_cpu)); 554 | out.push_str(&format!("
  • Peak RSS: {} KB
  • \n", s.peak_rss)); 555 | out.push_str("
\n"); 556 | if let Some(e) = &s.env { 557 | if !e.is_empty() { 558 | out.push_str(&format!( 559 | "
Environment
{}
\n", 560 | encode_text(e) 561 | )); 562 | } 563 | } else { 564 | out.push_str("

Environment: unknown

\n"); 565 | } 566 | out.push_str(&format!( 567 | "

CPU usage
\"CPU

\n", 568 | s.pid 569 | )); 570 | out.push_str(&format!( 571 | "

RSS
\"RSS

\n", 572 | s.pid 573 | )); 574 | if has_trace { 575 | out.push_str(&format!( 576 | "

Trace JSON

\n", 577 | s.pid 578 | )); 579 | } 580 | out.push_str("\n"); 581 | out 582 | } 583 | 584 | fn render_index(stats: &[Stats], link: bool) -> String { 585 | let mut out = String::new(); 586 | out.push_str("\n"); 587 | out.push_str("

CPU usage
\"Top

\n"); 588 | out.push_str("

Peak RSS
\"Top

\n"); 589 | if let (Some(start), Some(end)) = ( 590 | stats.iter().map(|s| s.start).min(), 591 | stats.iter().map(|s| s.end).max(), 592 | ) { 593 | out.push_str(&format!("

Start: {}

\n", start)); 594 | out.push_str(&format!("

End: {}

\n", end)); 595 | } 596 | out.push_str("\n"); 597 | out.push_str( 598 | "\n", 599 | ); 600 | for s in stats { 601 | let pid_cell = if link { 602 | format!("{}", s.pid, s.pid) 603 | } else { 604 | s.pid.to_string() 605 | }; 606 | let summary = truncate(&s.cmd, 30); 607 | let cmd_cell = format!( 608 | "
{}
{}
", 609 | encode_text(&summary), 610 | encode_text(&s.cmd) 611 | ); 612 | out.push_str(&format!( 613 | "\n", 614 | pid_cell, 615 | cmd_cell, 616 | s.runtime, 617 | s.cpu, 618 | s.avg_cpu, 619 | s.peak_rss, 620 | s.start, 621 | s.end 622 | )); 623 | } 624 | out.push_str("
PIDCommandTotal runtimeTotal CPU timeAvg CPU (%)Peak RSSStartEnd
{}{}{}{:.1}{:.1}{}{}{}
\n"); 625 | out 626 | } 627 | 628 | fn report_file(path: &Path, out_dir: &Path) { 629 | match read_log_entries(path) { 630 | Ok(entries) => { 631 | if let Some(s) = calc_stats(path, &entries) { 632 | write_graphs(&entries, out_dir, s.pid); 633 | let has_trace = write_trace(&entries, out_dir, s.pid); 634 | let html = render_single(&s, has_trace); 635 | let index = out_dir.join("index.html"); 636 | if let Err(e) = fs::write(&index, html) { 637 | warn!("failed to write {}: {}", index.display(), e); 638 | } 639 | } else { 640 | let index = out_dir.join("index.html"); 641 | if let Err(e) = fs::write(&index, "

No entries

") { 642 | warn!("failed to write {}: {}", index.display(), e); 643 | } 644 | } 645 | } 646 | Err(e) => warn!("failed to read {}: {}", path.display(), e), 647 | } 648 | } 649 | 650 | fn report_dir(path: &Path, out_dir: &Path, top_cpu: usize, top_rss: usize) { 651 | let mut files = Vec::new(); 652 | collect_files(path, &mut files); 653 | let mut stats = Vec::new(); 654 | for f in files { 655 | match read_log_entries(&f) { 656 | Ok(entries) => { 657 | if let Some(s) = calc_stats(&f, &entries) { 658 | stats.push(s); 659 | } 660 | } 661 | Err(e) => warn!("failed to read {}: {}", f.display(), e), 662 | } 663 | } 664 | if stats.is_empty() { 665 | let index = out_dir.join("index.html"); 666 | if let Err(e) = fs::write(&index, "

No entries

") { 667 | warn!("failed to write {}: {}", index.display(), e); 668 | } 669 | return; 670 | } 671 | 672 | let mut by_cpu = stats.clone(); 673 | by_cpu.sort_by(|a, b| { 674 | let a_cpu = if a.avg_cpu <= 0.1 { 0.0 } else { a.avg_cpu }; 675 | let b_cpu = if b.avg_cpu <= 0.1 { 0.0 } else { b.avg_cpu }; 676 | b_cpu 677 | .partial_cmp(&a_cpu) 678 | .unwrap() 679 | .then_with(|| b.peak_rss.cmp(&a.peak_rss)) 680 | }); 681 | let mut by_rss = stats.clone(); 682 | by_rss.sort_by_key(|s| std::cmp::Reverse(s.peak_rss)); 683 | 684 | let cpu_top: Vec<_> = by_cpu.iter().take(top_cpu).cloned().collect(); 685 | let rss_top: Vec<_> = by_rss.iter().take(top_rss).cloned().collect(); 686 | 687 | let mut map: HashMap = HashMap::new(); 688 | for s in cpu_top.clone() { 689 | map.entry(s.path.clone()).or_insert(s); 690 | } 691 | for s in rss_top.clone() { 692 | map.entry(s.path.clone()).or_insert(s); 693 | } 694 | let mut selected: Vec<_> = map.into_values().collect(); 695 | selected.sort_by(|a, b| { 696 | let a_cpu = if a.avg_cpu <= 0.1 { 0.0 } else { a.avg_cpu }; 697 | let b_cpu = if b.avg_cpu <= 0.1 { 0.0 } else { b.avg_cpu }; 698 | b_cpu 699 | .partial_cmp(&a_cpu) 700 | .unwrap() 701 | .then_with(|| b.peak_rss.cmp(&a.peak_rss)) 702 | }); 703 | 704 | write_multi_svg(&cpu_top, &out_dir.join("top_cpu.svg"), GraphField::Cpu); 705 | write_multi_svg(&rss_top, &out_dir.join("top_rss.svg"), GraphField::Rss); 706 | 707 | // write index.html 708 | let index_html = render_index(&selected, true); 709 | let index_path = out_dir.join("index.html"); 710 | if let Err(e) = fs::write(&index_path, index_html) { 711 | warn!("failed to write {}: {}", index_path.display(), e); 712 | } 713 | 714 | // write per pid files 715 | for s in &selected { 716 | match read_log_entries(Path::new(&s.path)) { 717 | Ok(entries) => { 718 | if let Some(stats) = calc_stats(Path::new(&s.path), &entries) { 719 | write_graphs(&entries, out_dir, s.pid); 720 | let has_trace = write_trace(&entries, out_dir, s.pid); 721 | let html = render_single(&stats, has_trace); 722 | let out = out_dir.join(format!("{}.html", s.pid)); 723 | if let Err(e) = fs::write(&out, html) { 724 | warn!("failed to write {}: {}", out.display(), e); 725 | } 726 | } 727 | } 728 | Err(e) => warn!("failed to read {}: {}", s.path, e), 729 | } 730 | } 731 | } 732 | 733 | pub fn report(args: &ReportArgs) { 734 | let cfg = if let Some(ref path) = args.config { 735 | finalize_report_config(load_config(path).report) 736 | } else { 737 | finalize_report_config(Default::default()) 738 | }; 739 | let input = Path::new(&args.path); 740 | let out_dir = if let Some(ref o) = args.output { 741 | PathBuf::from(o) 742 | } else { 743 | let name = input 744 | .file_stem() 745 | .or_else(|| input.file_name()) 746 | .unwrap_or_default(); 747 | PathBuf::from(name) 748 | }; 749 | if let Err(e) = fs::create_dir_all(&out_dir) { 750 | warn!("failed to create {}: {}", out_dir.display(), e); 751 | } 752 | if input.is_dir() { 753 | report_dir( 754 | input, 755 | &out_dir, 756 | cfg.top_cpu.unwrap_or(10), 757 | cfg.top_rss.unwrap_or(10), 758 | ); 759 | } else { 760 | report_file(input, &out_dir); 761 | } 762 | println!("{}", out_dir.display()); 763 | } 764 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.24.2" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" 10 | dependencies = [ 11 | "cpp_demangle", 12 | "fallible-iterator", 13 | "gimli 0.31.1", 14 | "memmap2", 15 | "object 0.36.7", 16 | "rustc-demangle", 17 | "smallvec", 18 | "typed-arena", 19 | ] 20 | 21 | [[package]] 22 | name = "addr2line" 23 | version = "0.25.0" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "9acbfca36652500c911ddb767ed433e3ed99b032b5d935be73c6923662db1d43" 26 | dependencies = [ 27 | "cpp_demangle", 28 | "fallible-iterator", 29 | "gimli 0.32.0", 30 | "memmap2", 31 | "object 0.37.1", 32 | "rustc-demangle", 33 | "smallvec", 34 | "typed-arena", 35 | ] 36 | 37 | [[package]] 38 | name = "adler2" 39 | version = "2.0.1" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" 42 | 43 | [[package]] 44 | name = "ahash" 45 | version = "0.8.12" 46 | source = "registry+https://github.com/rust-lang/crates.io-index" 47 | checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" 48 | dependencies = [ 49 | "cfg-if", 50 | "getrandom 0.3.3", 51 | "once_cell", 52 | "version_check", 53 | "zerocopy", 54 | ] 55 | 56 | [[package]] 57 | name = "aho-corasick" 58 | version = "1.1.3" 59 | source = "registry+https://github.com/rust-lang/crates.io-index" 60 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 61 | dependencies = [ 62 | "memchr", 63 | ] 64 | 65 | [[package]] 66 | name = "android-tzdata" 67 | version = "0.1.1" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 70 | 71 | [[package]] 72 | name = "android_system_properties" 73 | version = "0.1.5" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 76 | dependencies = [ 77 | "libc", 78 | ] 79 | 80 | [[package]] 81 | name = "anstream" 82 | version = "0.6.19" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" 85 | dependencies = [ 86 | "anstyle", 87 | "anstyle-parse", 88 | "anstyle-query", 89 | "anstyle-wincon", 90 | "colorchoice", 91 | "is_terminal_polyfill", 92 | "utf8parse", 93 | ] 94 | 95 | [[package]] 96 | name = "anstyle" 97 | version = "1.0.11" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" 100 | 101 | [[package]] 102 | name = "anstyle-parse" 103 | version = "0.2.7" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" 106 | dependencies = [ 107 | "utf8parse", 108 | ] 109 | 110 | [[package]] 111 | name = "anstyle-query" 112 | version = "1.1.3" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" 115 | dependencies = [ 116 | "windows-sys 0.59.0", 117 | ] 118 | 119 | [[package]] 120 | name = "anstyle-wincon" 121 | version = "3.0.9" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" 124 | dependencies = [ 125 | "anstyle", 126 | "once_cell_polyfill", 127 | "windows-sys 0.59.0", 128 | ] 129 | 130 | [[package]] 131 | name = "anyhow" 132 | version = "1.0.98" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 135 | 136 | [[package]] 137 | name = "arrayvec" 138 | version = "0.7.6" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" 141 | 142 | [[package]] 143 | name = "atty" 144 | version = "0.2.14" 145 | source = "registry+https://github.com/rust-lang/crates.io-index" 146 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 147 | dependencies = [ 148 | "hermit-abi 0.1.19", 149 | "libc", 150 | "winapi", 151 | ] 152 | 153 | [[package]] 154 | name = "autocfg" 155 | version = "1.4.0" 156 | source = "registry+https://github.com/rust-lang/crates.io-index" 157 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 158 | 159 | [[package]] 160 | name = "bindgen" 161 | version = "0.70.1" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" 164 | dependencies = [ 165 | "bitflags 2.9.1", 166 | "cexpr", 167 | "clang-sys", 168 | "itertools", 169 | "log", 170 | "prettyplease", 171 | "proc-macro2", 172 | "quote", 173 | "regex", 174 | "rustc-hash", 175 | "shlex", 176 | "syn 2.0.103", 177 | ] 178 | 179 | [[package]] 180 | name = "bitflags" 181 | version = "1.3.2" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 184 | 185 | [[package]] 186 | name = "bitflags" 187 | version = "2.9.1" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 190 | 191 | [[package]] 192 | name = "bumpalo" 193 | version = "3.18.1" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" 196 | 197 | [[package]] 198 | name = "bytemuck" 199 | version = "1.23.1" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" 202 | 203 | [[package]] 204 | name = "byteorder" 205 | version = "1.5.0" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 208 | 209 | [[package]] 210 | name = "cc" 211 | version = "1.2.27" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" 214 | dependencies = [ 215 | "jobserver", 216 | "libc", 217 | "shlex", 218 | ] 219 | 220 | [[package]] 221 | name = "cexpr" 222 | version = "0.6.0" 223 | source = "registry+https://github.com/rust-lang/crates.io-index" 224 | checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" 225 | dependencies = [ 226 | "nom", 227 | ] 228 | 229 | [[package]] 230 | name = "cfg-if" 231 | version = "1.0.1" 232 | source = "registry+https://github.com/rust-lang/crates.io-index" 233 | checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" 234 | 235 | [[package]] 236 | name = "cfg_aliases" 237 | version = "0.1.1" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" 240 | 241 | [[package]] 242 | name = "cfg_aliases" 243 | version = "0.2.1" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 246 | 247 | [[package]] 248 | name = "chrono" 249 | version = "0.4.41" 250 | source = "registry+https://github.com/rust-lang/crates.io-index" 251 | checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" 252 | dependencies = [ 253 | "android-tzdata", 254 | "iana-time-zone", 255 | "js-sys", 256 | "num-traits", 257 | "wasm-bindgen", 258 | "windows-link", 259 | ] 260 | 261 | [[package]] 262 | name = "clang-sys" 263 | version = "1.8.1" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" 266 | dependencies = [ 267 | "glob", 268 | "libc", 269 | "libloading", 270 | ] 271 | 272 | [[package]] 273 | name = "clap" 274 | version = "3.2.25" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" 277 | dependencies = [ 278 | "atty", 279 | "bitflags 1.3.2", 280 | "clap_derive 3.2.25", 281 | "clap_lex 0.2.4", 282 | "indexmap 1.9.3", 283 | "once_cell", 284 | "strsim 0.10.0", 285 | "termcolor", 286 | "terminal_size 0.2.6", 287 | "textwrap", 288 | ] 289 | 290 | [[package]] 291 | name = "clap" 292 | version = "4.5.40" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" 295 | dependencies = [ 296 | "clap_builder", 297 | "clap_derive 4.5.40", 298 | ] 299 | 300 | [[package]] 301 | name = "clap_builder" 302 | version = "4.5.40" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" 305 | dependencies = [ 306 | "anstream", 307 | "anstyle", 308 | "clap_lex 0.7.5", 309 | "strsim 0.11.1", 310 | ] 311 | 312 | [[package]] 313 | name = "clap_complete" 314 | version = "3.2.5" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "3f7a2e0a962c45ce25afce14220bc24f9dade0a1787f185cecf96bfba7847cd8" 317 | dependencies = [ 318 | "clap 3.2.25", 319 | ] 320 | 321 | [[package]] 322 | name = "clap_derive" 323 | version = "3.2.25" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" 326 | dependencies = [ 327 | "heck 0.4.1", 328 | "proc-macro-error", 329 | "proc-macro2", 330 | "quote", 331 | "syn 1.0.109", 332 | ] 333 | 334 | [[package]] 335 | name = "clap_derive" 336 | version = "4.5.40" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" 339 | dependencies = [ 340 | "heck 0.5.0", 341 | "proc-macro2", 342 | "quote", 343 | "syn 2.0.103", 344 | ] 345 | 346 | [[package]] 347 | name = "clap_lex" 348 | version = "0.2.4" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" 351 | dependencies = [ 352 | "os_str_bytes", 353 | ] 354 | 355 | [[package]] 356 | name = "clap_lex" 357 | version = "0.7.5" 358 | source = "registry+https://github.com/rust-lang/crates.io-index" 359 | checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" 360 | 361 | [[package]] 362 | name = "color_quant" 363 | version = "1.1.0" 364 | source = "registry+https://github.com/rust-lang/crates.io-index" 365 | checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" 366 | 367 | [[package]] 368 | name = "colorchoice" 369 | version = "1.0.4" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" 372 | 373 | [[package]] 374 | name = "console" 375 | version = "0.15.11" 376 | source = "registry+https://github.com/rust-lang/crates.io-index" 377 | checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" 378 | dependencies = [ 379 | "encode_unicode", 380 | "libc", 381 | "once_cell", 382 | "unicode-width", 383 | "windows-sys 0.59.0", 384 | ] 385 | 386 | [[package]] 387 | name = "core-foundation" 388 | version = "0.9.4" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 391 | dependencies = [ 392 | "core-foundation-sys", 393 | "libc", 394 | ] 395 | 396 | [[package]] 397 | name = "core-foundation-sys" 398 | version = "0.8.7" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 401 | 402 | [[package]] 403 | name = "core-graphics" 404 | version = "0.23.2" 405 | source = "registry+https://github.com/rust-lang/crates.io-index" 406 | checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" 407 | dependencies = [ 408 | "bitflags 1.3.2", 409 | "core-foundation", 410 | "core-graphics-types", 411 | "foreign-types", 412 | "libc", 413 | ] 414 | 415 | [[package]] 416 | name = "core-graphics-types" 417 | version = "0.1.3" 418 | source = "registry+https://github.com/rust-lang/crates.io-index" 419 | checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" 420 | dependencies = [ 421 | "bitflags 1.3.2", 422 | "core-foundation", 423 | "libc", 424 | ] 425 | 426 | [[package]] 427 | name = "core-text" 428 | version = "20.1.0" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "c9d2790b5c08465d49f8dc05c8bcae9fea467855947db39b0f8145c091aaced5" 431 | dependencies = [ 432 | "core-foundation", 433 | "core-graphics", 434 | "foreign-types", 435 | "libc", 436 | ] 437 | 438 | [[package]] 439 | name = "cpp_demangle" 440 | version = "0.4.4" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "96e58d342ad113c2b878f16d5d034c03be492ae460cdbc02b7f0f2284d310c7d" 443 | dependencies = [ 444 | "cfg-if", 445 | ] 446 | 447 | [[package]] 448 | name = "crc32fast" 449 | version = "1.4.2" 450 | source = "registry+https://github.com/rust-lang/crates.io-index" 451 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 452 | dependencies = [ 453 | "cfg-if", 454 | ] 455 | 456 | [[package]] 457 | name = "crossbeam-channel" 458 | version = "0.5.15" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" 461 | dependencies = [ 462 | "crossbeam-utils", 463 | ] 464 | 465 | [[package]] 466 | name = "crossbeam-utils" 467 | version = "0.8.21" 468 | source = "registry+https://github.com/rust-lang/crates.io-index" 469 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 470 | 471 | [[package]] 472 | name = "ctrlc" 473 | version = "3.4.7" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "46f93780a459b7d656ef7f071fe699c4d3d2cb201c4b24d085b6ddc505276e73" 476 | dependencies = [ 477 | "nix 0.30.1", 478 | "windows-sys 0.59.0", 479 | ] 480 | 481 | [[package]] 482 | name = "dashmap" 483 | version = "6.1.0" 484 | source = "registry+https://github.com/rust-lang/crates.io-index" 485 | checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" 486 | dependencies = [ 487 | "cfg-if", 488 | "crossbeam-utils", 489 | "hashbrown 0.14.5", 490 | "lock_api", 491 | "once_cell", 492 | "parking_lot_core", 493 | ] 494 | 495 | [[package]] 496 | name = "dirs" 497 | version = "6.0.0" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" 500 | dependencies = [ 501 | "dirs-sys", 502 | ] 503 | 504 | [[package]] 505 | name = "dirs-sys" 506 | version = "0.5.0" 507 | source = "registry+https://github.com/rust-lang/crates.io-index" 508 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 509 | dependencies = [ 510 | "libc", 511 | "option-ext", 512 | "redox_users", 513 | "windows-sys 0.59.0", 514 | ] 515 | 516 | [[package]] 517 | name = "dlib" 518 | version = "0.5.2" 519 | source = "registry+https://github.com/rust-lang/crates.io-index" 520 | checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" 521 | dependencies = [ 522 | "libloading", 523 | ] 524 | 525 | [[package]] 526 | name = "dwrote" 527 | version = "0.11.3" 528 | source = "registry+https://github.com/rust-lang/crates.io-index" 529 | checksum = "bfe1f192fcce01590bd8d839aca53ce0d11d803bf291b2a6c4ad925a8f0024be" 530 | dependencies = [ 531 | "lazy_static", 532 | "libc", 533 | "winapi", 534 | "wio", 535 | ] 536 | 537 | [[package]] 538 | name = "either" 539 | version = "1.15.0" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" 542 | 543 | [[package]] 544 | name = "encode_unicode" 545 | version = "1.0.0" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" 548 | 549 | [[package]] 550 | name = "env_filter" 551 | version = "0.1.3" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" 554 | dependencies = [ 555 | "log", 556 | ] 557 | 558 | [[package]] 559 | name = "env_logger" 560 | version = "0.10.2" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" 563 | dependencies = [ 564 | "humantime", 565 | "is-terminal", 566 | "log", 567 | "regex", 568 | "termcolor", 569 | ] 570 | 571 | [[package]] 572 | name = "env_logger" 573 | version = "0.11.8" 574 | source = "registry+https://github.com/rust-lang/crates.io-index" 575 | checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" 576 | dependencies = [ 577 | "env_filter", 578 | "log", 579 | ] 580 | 581 | [[package]] 582 | name = "equivalent" 583 | version = "1.0.2" 584 | source = "registry+https://github.com/rust-lang/crates.io-index" 585 | checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 586 | 587 | [[package]] 588 | name = "errno" 589 | version = "0.3.12" 590 | source = "registry+https://github.com/rust-lang/crates.io-index" 591 | checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" 592 | dependencies = [ 593 | "libc", 594 | "windows-sys 0.59.0", 595 | ] 596 | 597 | [[package]] 598 | name = "fallible-iterator" 599 | version = "0.3.0" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" 602 | 603 | [[package]] 604 | name = "fastrand" 605 | version = "2.3.0" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 608 | 609 | [[package]] 610 | name = "fdeflate" 611 | version = "0.3.7" 612 | source = "registry+https://github.com/rust-lang/crates.io-index" 613 | checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" 614 | dependencies = [ 615 | "simd-adler32", 616 | ] 617 | 618 | [[package]] 619 | name = "flate2" 620 | version = "1.1.2" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" 623 | dependencies = [ 624 | "crc32fast", 625 | "miniz_oxide", 626 | ] 627 | 628 | [[package]] 629 | name = "float-ord" 630 | version = "0.3.2" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" 633 | 634 | [[package]] 635 | name = "font-kit" 636 | version = "0.14.3" 637 | source = "registry+https://github.com/rust-lang/crates.io-index" 638 | checksum = "2c7e611d49285d4c4b2e1727b72cf05353558885cc5252f93707b845dfcaf3d3" 639 | dependencies = [ 640 | "bitflags 2.9.1", 641 | "byteorder", 642 | "core-foundation", 643 | "core-graphics", 644 | "core-text", 645 | "dirs", 646 | "dwrote", 647 | "float-ord", 648 | "freetype-sys", 649 | "lazy_static", 650 | "libc", 651 | "log", 652 | "pathfinder_geometry", 653 | "pathfinder_simd", 654 | "walkdir", 655 | "winapi", 656 | "yeslogic-fontconfig-sys", 657 | ] 658 | 659 | [[package]] 660 | name = "foreign-types" 661 | version = "0.5.0" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" 664 | dependencies = [ 665 | "foreign-types-macros", 666 | "foreign-types-shared", 667 | ] 668 | 669 | [[package]] 670 | name = "foreign-types-macros" 671 | version = "0.2.3" 672 | source = "registry+https://github.com/rust-lang/crates.io-index" 673 | checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" 674 | dependencies = [ 675 | "proc-macro2", 676 | "quote", 677 | "syn 2.0.103", 678 | ] 679 | 680 | [[package]] 681 | name = "foreign-types-shared" 682 | version = "0.3.1" 683 | source = "registry+https://github.com/rust-lang/crates.io-index" 684 | checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" 685 | 686 | [[package]] 687 | name = "freetype-sys" 688 | version = "0.20.1" 689 | source = "registry+https://github.com/rust-lang/crates.io-index" 690 | checksum = "0e7edc5b9669349acfda99533e9e0bcf26a51862ab43b08ee7745c55d28eb134" 691 | dependencies = [ 692 | "cc", 693 | "libc", 694 | "pkg-config", 695 | ] 696 | 697 | [[package]] 698 | name = "fuzmon" 699 | version = "0.1.0" 700 | dependencies = [ 701 | "addr2line 0.25.0", 702 | "chrono", 703 | "clap 4.5.40", 704 | "ctrlc", 705 | "env_logger 0.10.2", 706 | "html-escape", 707 | "log", 708 | "memmap2", 709 | "nix 0.28.0", 710 | "num_cpus", 711 | "object 0.37.1", 712 | "plotters", 713 | "plotters-svg", 714 | "py-spy", 715 | "regex", 716 | "rmp-serde", 717 | "serde", 718 | "serde_json", 719 | "tempfile", 720 | "toml", 721 | "zstd", 722 | ] 723 | 724 | [[package]] 725 | name = "getrandom" 726 | version = "0.2.16" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 729 | dependencies = [ 730 | "cfg-if", 731 | "libc", 732 | "wasi 0.11.1+wasi-snapshot-preview1", 733 | ] 734 | 735 | [[package]] 736 | name = "getrandom" 737 | version = "0.3.3" 738 | source = "registry+https://github.com/rust-lang/crates.io-index" 739 | checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" 740 | dependencies = [ 741 | "cfg-if", 742 | "libc", 743 | "r-efi", 744 | "wasi 0.14.2+wasi-0.2.4", 745 | ] 746 | 747 | [[package]] 748 | name = "gif" 749 | version = "0.12.0" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" 752 | dependencies = [ 753 | "color_quant", 754 | "weezl", 755 | ] 756 | 757 | [[package]] 758 | name = "gimli" 759 | version = "0.31.1" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" 762 | dependencies = [ 763 | "fallible-iterator", 764 | "stable_deref_trait", 765 | ] 766 | 767 | [[package]] 768 | name = "gimli" 769 | version = "0.32.0" 770 | source = "registry+https://github.com/rust-lang/crates.io-index" 771 | checksum = "93563d740bc9ef04104f9ed6f86f1e3275c2cdafb95664e26584b9ca807a8ffe" 772 | dependencies = [ 773 | "fallible-iterator", 774 | "stable_deref_trait", 775 | ] 776 | 777 | [[package]] 778 | name = "glob" 779 | version = "0.3.2" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" 782 | 783 | [[package]] 784 | name = "goblin" 785 | version = "0.9.3" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "daa0a64d21a7eb230583b4c5f4e23b7e4e57974f96620f42a7e75e08ae66d745" 788 | dependencies = [ 789 | "log", 790 | "plain", 791 | "scroll", 792 | ] 793 | 794 | [[package]] 795 | name = "hashbrown" 796 | version = "0.12.3" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 799 | 800 | [[package]] 801 | name = "hashbrown" 802 | version = "0.13.2" 803 | source = "registry+https://github.com/rust-lang/crates.io-index" 804 | checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" 805 | dependencies = [ 806 | "ahash", 807 | ] 808 | 809 | [[package]] 810 | name = "hashbrown" 811 | version = "0.14.5" 812 | source = "registry+https://github.com/rust-lang/crates.io-index" 813 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 814 | 815 | [[package]] 816 | name = "hashbrown" 817 | version = "0.15.4" 818 | source = "registry+https://github.com/rust-lang/crates.io-index" 819 | checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" 820 | 821 | [[package]] 822 | name = "heck" 823 | version = "0.4.1" 824 | source = "registry+https://github.com/rust-lang/crates.io-index" 825 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 826 | 827 | [[package]] 828 | name = "heck" 829 | version = "0.5.0" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 832 | 833 | [[package]] 834 | name = "hermit-abi" 835 | version = "0.1.19" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 838 | dependencies = [ 839 | "libc", 840 | ] 841 | 842 | [[package]] 843 | name = "hermit-abi" 844 | version = "0.3.9" 845 | source = "registry+https://github.com/rust-lang/crates.io-index" 846 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 847 | 848 | [[package]] 849 | name = "hermit-abi" 850 | version = "0.5.2" 851 | source = "registry+https://github.com/rust-lang/crates.io-index" 852 | checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" 853 | 854 | [[package]] 855 | name = "html-escape" 856 | version = "0.2.13" 857 | source = "registry+https://github.com/rust-lang/crates.io-index" 858 | checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" 859 | dependencies = [ 860 | "utf8-width", 861 | ] 862 | 863 | [[package]] 864 | name = "humantime" 865 | version = "2.2.0" 866 | source = "registry+https://github.com/rust-lang/crates.io-index" 867 | checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" 868 | 869 | [[package]] 870 | name = "iana-time-zone" 871 | version = "0.1.63" 872 | source = "registry+https://github.com/rust-lang/crates.io-index" 873 | checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" 874 | dependencies = [ 875 | "android_system_properties", 876 | "core-foundation-sys", 877 | "iana-time-zone-haiku", 878 | "js-sys", 879 | "log", 880 | "wasm-bindgen", 881 | "windows-core", 882 | ] 883 | 884 | [[package]] 885 | name = "iana-time-zone-haiku" 886 | version = "0.1.2" 887 | source = "registry+https://github.com/rust-lang/crates.io-index" 888 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 889 | dependencies = [ 890 | "cc", 891 | ] 892 | 893 | [[package]] 894 | name = "image" 895 | version = "0.24.9" 896 | source = "registry+https://github.com/rust-lang/crates.io-index" 897 | checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" 898 | dependencies = [ 899 | "bytemuck", 900 | "byteorder", 901 | "color_quant", 902 | "jpeg-decoder", 903 | "num-traits", 904 | "png", 905 | ] 906 | 907 | [[package]] 908 | name = "indexmap" 909 | version = "1.9.3" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 912 | dependencies = [ 913 | "autocfg", 914 | "hashbrown 0.12.3", 915 | ] 916 | 917 | [[package]] 918 | name = "indexmap" 919 | version = "2.9.0" 920 | source = "registry+https://github.com/rust-lang/crates.io-index" 921 | checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" 922 | dependencies = [ 923 | "equivalent", 924 | "hashbrown 0.15.4", 925 | ] 926 | 927 | [[package]] 928 | name = "indicatif" 929 | version = "0.17.11" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" 932 | dependencies = [ 933 | "console", 934 | "number_prefix", 935 | "portable-atomic", 936 | "unicode-width", 937 | "web-time", 938 | ] 939 | 940 | [[package]] 941 | name = "inferno" 942 | version = "0.11.21" 943 | source = "registry+https://github.com/rust-lang/crates.io-index" 944 | checksum = "232929e1d75fe899576a3d5c7416ad0d88dbfbb3c3d6aa00873a7408a50ddb88" 945 | dependencies = [ 946 | "ahash", 947 | "clap 4.5.40", 948 | "crossbeam-channel", 949 | "crossbeam-utils", 950 | "dashmap", 951 | "env_logger 0.11.8", 952 | "indexmap 2.9.0", 953 | "is-terminal", 954 | "itoa", 955 | "log", 956 | "num-format", 957 | "once_cell", 958 | "quick-xml", 959 | "rgb", 960 | "str_stack", 961 | ] 962 | 963 | [[package]] 964 | name = "io-lifetimes" 965 | version = "1.0.11" 966 | source = "registry+https://github.com/rust-lang/crates.io-index" 967 | checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" 968 | dependencies = [ 969 | "hermit-abi 0.3.9", 970 | "libc", 971 | "windows-sys 0.48.0", 972 | ] 973 | 974 | [[package]] 975 | name = "is-terminal" 976 | version = "0.4.16" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" 979 | dependencies = [ 980 | "hermit-abi 0.5.2", 981 | "libc", 982 | "windows-sys 0.59.0", 983 | ] 984 | 985 | [[package]] 986 | name = "is_terminal_polyfill" 987 | version = "1.70.1" 988 | source = "registry+https://github.com/rust-lang/crates.io-index" 989 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 990 | 991 | [[package]] 992 | name = "itertools" 993 | version = "0.13.0" 994 | source = "registry+https://github.com/rust-lang/crates.io-index" 995 | checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" 996 | dependencies = [ 997 | "either", 998 | ] 999 | 1000 | [[package]] 1001 | name = "itoa" 1002 | version = "1.0.15" 1003 | source = "registry+https://github.com/rust-lang/crates.io-index" 1004 | checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" 1005 | 1006 | [[package]] 1007 | name = "jobserver" 1008 | version = "0.1.33" 1009 | source = "registry+https://github.com/rust-lang/crates.io-index" 1010 | checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" 1011 | dependencies = [ 1012 | "getrandom 0.3.3", 1013 | "libc", 1014 | ] 1015 | 1016 | [[package]] 1017 | name = "jpeg-decoder" 1018 | version = "0.3.1" 1019 | source = "registry+https://github.com/rust-lang/crates.io-index" 1020 | checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" 1021 | 1022 | [[package]] 1023 | name = "js-sys" 1024 | version = "0.3.77" 1025 | source = "registry+https://github.com/rust-lang/crates.io-index" 1026 | checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" 1027 | dependencies = [ 1028 | "once_cell", 1029 | "wasm-bindgen", 1030 | ] 1031 | 1032 | [[package]] 1033 | name = "lazy_static" 1034 | version = "1.5.0" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 1037 | 1038 | [[package]] 1039 | name = "libc" 1040 | version = "0.2.173" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" 1043 | 1044 | [[package]] 1045 | name = "libloading" 1046 | version = "0.8.8" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" 1049 | dependencies = [ 1050 | "cfg-if", 1051 | "windows-targets 0.52.6", 1052 | ] 1053 | 1054 | [[package]] 1055 | name = "libm" 1056 | version = "0.2.15" 1057 | source = "registry+https://github.com/rust-lang/crates.io-index" 1058 | checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" 1059 | 1060 | [[package]] 1061 | name = "libproc" 1062 | version = "0.14.10" 1063 | source = "registry+https://github.com/rust-lang/crates.io-index" 1064 | checksum = "e78a09b56be5adbcad5aa1197371688dc6bb249a26da3bca2011ee2fb987ebfb" 1065 | dependencies = [ 1066 | "bindgen", 1067 | "errno", 1068 | "libc", 1069 | ] 1070 | 1071 | [[package]] 1072 | name = "libredox" 1073 | version = "0.1.3" 1074 | source = "registry+https://github.com/rust-lang/crates.io-index" 1075 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 1076 | dependencies = [ 1077 | "bitflags 2.9.1", 1078 | "libc", 1079 | ] 1080 | 1081 | [[package]] 1082 | name = "linux-raw-sys" 1083 | version = "0.3.8" 1084 | source = "registry+https://github.com/rust-lang/crates.io-index" 1085 | checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" 1086 | 1087 | [[package]] 1088 | name = "linux-raw-sys" 1089 | version = "0.9.4" 1090 | source = "registry+https://github.com/rust-lang/crates.io-index" 1091 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 1092 | 1093 | [[package]] 1094 | name = "lock_api" 1095 | version = "0.4.13" 1096 | source = "registry+https://github.com/rust-lang/crates.io-index" 1097 | checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" 1098 | dependencies = [ 1099 | "autocfg", 1100 | "scopeguard", 1101 | ] 1102 | 1103 | [[package]] 1104 | name = "log" 1105 | version = "0.4.27" 1106 | source = "registry+https://github.com/rust-lang/crates.io-index" 1107 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 1108 | 1109 | [[package]] 1110 | name = "lru" 1111 | version = "0.10.1" 1112 | source = "registry+https://github.com/rust-lang/crates.io-index" 1113 | checksum = "718e8fae447df0c7e1ba7f5189829e63fd536945c8988d61444c19039f16b670" 1114 | dependencies = [ 1115 | "hashbrown 0.13.2", 1116 | ] 1117 | 1118 | [[package]] 1119 | name = "mach" 1120 | version = "0.3.2" 1121 | source = "registry+https://github.com/rust-lang/crates.io-index" 1122 | checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" 1123 | dependencies = [ 1124 | "libc", 1125 | ] 1126 | 1127 | [[package]] 1128 | name = "mach2" 1129 | version = "0.4.2" 1130 | source = "registry+https://github.com/rust-lang/crates.io-index" 1131 | checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" 1132 | dependencies = [ 1133 | "libc", 1134 | ] 1135 | 1136 | [[package]] 1137 | name = "mach_o_sys" 1138 | version = "0.1.1" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "3e854583a83f20cf329bb9283366335387f7db59d640d1412167e05fedb98826" 1141 | 1142 | [[package]] 1143 | name = "memchr" 1144 | version = "2.7.5" 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" 1146 | checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" 1147 | 1148 | [[package]] 1149 | name = "memmap2" 1150 | version = "0.9.5" 1151 | source = "registry+https://github.com/rust-lang/crates.io-index" 1152 | checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f" 1153 | dependencies = [ 1154 | "libc", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "minimal-lexical" 1159 | version = "0.2.1" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 1162 | 1163 | [[package]] 1164 | name = "miniz_oxide" 1165 | version = "0.8.9" 1166 | source = "registry+https://github.com/rust-lang/crates.io-index" 1167 | checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" 1168 | dependencies = [ 1169 | "adler2", 1170 | "simd-adler32", 1171 | ] 1172 | 1173 | [[package]] 1174 | name = "nix" 1175 | version = "0.26.4" 1176 | source = "registry+https://github.com/rust-lang/crates.io-index" 1177 | checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" 1178 | dependencies = [ 1179 | "bitflags 1.3.2", 1180 | "cfg-if", 1181 | "libc", 1182 | ] 1183 | 1184 | [[package]] 1185 | name = "nix" 1186 | version = "0.28.0" 1187 | source = "registry+https://github.com/rust-lang/crates.io-index" 1188 | checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" 1189 | dependencies = [ 1190 | "bitflags 2.9.1", 1191 | "cfg-if", 1192 | "cfg_aliases 0.1.1", 1193 | "libc", 1194 | ] 1195 | 1196 | [[package]] 1197 | name = "nix" 1198 | version = "0.30.1" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" 1201 | dependencies = [ 1202 | "bitflags 2.9.1", 1203 | "cfg-if", 1204 | "cfg_aliases 0.2.1", 1205 | "libc", 1206 | ] 1207 | 1208 | [[package]] 1209 | name = "nom" 1210 | version = "7.1.3" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 1213 | dependencies = [ 1214 | "memchr", 1215 | "minimal-lexical", 1216 | ] 1217 | 1218 | [[package]] 1219 | name = "num-format" 1220 | version = "0.4.4" 1221 | source = "registry+https://github.com/rust-lang/crates.io-index" 1222 | checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" 1223 | dependencies = [ 1224 | "arrayvec", 1225 | "itoa", 1226 | ] 1227 | 1228 | [[package]] 1229 | name = "num-traits" 1230 | version = "0.2.19" 1231 | source = "registry+https://github.com/rust-lang/crates.io-index" 1232 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 1233 | dependencies = [ 1234 | "autocfg", 1235 | "libm", 1236 | ] 1237 | 1238 | [[package]] 1239 | name = "num_cpus" 1240 | version = "1.17.0" 1241 | source = "registry+https://github.com/rust-lang/crates.io-index" 1242 | checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" 1243 | dependencies = [ 1244 | "hermit-abi 0.5.2", 1245 | "libc", 1246 | ] 1247 | 1248 | [[package]] 1249 | name = "number_prefix" 1250 | version = "0.4.0" 1251 | source = "registry+https://github.com/rust-lang/crates.io-index" 1252 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" 1253 | 1254 | [[package]] 1255 | name = "object" 1256 | version = "0.36.7" 1257 | source = "registry+https://github.com/rust-lang/crates.io-index" 1258 | checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" 1259 | dependencies = [ 1260 | "flate2", 1261 | "memchr", 1262 | "ruzstd 0.7.3", 1263 | ] 1264 | 1265 | [[package]] 1266 | name = "object" 1267 | version = "0.37.1" 1268 | source = "registry+https://github.com/rust-lang/crates.io-index" 1269 | checksum = "03fd943161069e1768b4b3d050890ba48730e590f57e56d4aa04e7e090e61b4a" 1270 | dependencies = [ 1271 | "flate2", 1272 | "memchr", 1273 | "ruzstd 0.8.1", 1274 | ] 1275 | 1276 | [[package]] 1277 | name = "once_cell" 1278 | version = "1.21.3" 1279 | source = "registry+https://github.com/rust-lang/crates.io-index" 1280 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 1281 | 1282 | [[package]] 1283 | name = "once_cell_polyfill" 1284 | version = "1.70.1" 1285 | source = "registry+https://github.com/rust-lang/crates.io-index" 1286 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 1287 | 1288 | [[package]] 1289 | name = "option-ext" 1290 | version = "0.2.0" 1291 | source = "registry+https://github.com/rust-lang/crates.io-index" 1292 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 1293 | 1294 | [[package]] 1295 | name = "os_str_bytes" 1296 | version = "6.6.1" 1297 | source = "registry+https://github.com/rust-lang/crates.io-index" 1298 | checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" 1299 | 1300 | [[package]] 1301 | name = "parking_lot_core" 1302 | version = "0.9.11" 1303 | source = "registry+https://github.com/rust-lang/crates.io-index" 1304 | checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" 1305 | dependencies = [ 1306 | "cfg-if", 1307 | "libc", 1308 | "redox_syscall", 1309 | "smallvec", 1310 | "windows-targets 0.52.6", 1311 | ] 1312 | 1313 | [[package]] 1314 | name = "paste" 1315 | version = "1.0.15" 1316 | source = "registry+https://github.com/rust-lang/crates.io-index" 1317 | checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" 1318 | 1319 | [[package]] 1320 | name = "pathfinder_geometry" 1321 | version = "0.5.1" 1322 | source = "registry+https://github.com/rust-lang/crates.io-index" 1323 | checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3" 1324 | dependencies = [ 1325 | "log", 1326 | "pathfinder_simd", 1327 | ] 1328 | 1329 | [[package]] 1330 | name = "pathfinder_simd" 1331 | version = "0.5.5" 1332 | source = "registry+https://github.com/rust-lang/crates.io-index" 1333 | checksum = "bf9027960355bf3afff9841918474a81a5f972ac6d226d518060bba758b5ad57" 1334 | dependencies = [ 1335 | "rustc_version", 1336 | ] 1337 | 1338 | [[package]] 1339 | name = "pkg-config" 1340 | version = "0.3.32" 1341 | source = "registry+https://github.com/rust-lang/crates.io-index" 1342 | checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" 1343 | 1344 | [[package]] 1345 | name = "plain" 1346 | version = "0.2.3" 1347 | source = "registry+https://github.com/rust-lang/crates.io-index" 1348 | checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" 1349 | 1350 | [[package]] 1351 | name = "plotters" 1352 | version = "0.3.7" 1353 | source = "registry+https://github.com/rust-lang/crates.io-index" 1354 | checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" 1355 | dependencies = [ 1356 | "chrono", 1357 | "font-kit", 1358 | "image", 1359 | "lazy_static", 1360 | "num-traits", 1361 | "pathfinder_geometry", 1362 | "plotters-backend", 1363 | "plotters-bitmap", 1364 | "plotters-svg", 1365 | "ttf-parser", 1366 | "wasm-bindgen", 1367 | "web-sys", 1368 | ] 1369 | 1370 | [[package]] 1371 | name = "plotters-backend" 1372 | version = "0.3.7" 1373 | source = "registry+https://github.com/rust-lang/crates.io-index" 1374 | checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" 1375 | 1376 | [[package]] 1377 | name = "plotters-bitmap" 1378 | version = "0.3.7" 1379 | source = "registry+https://github.com/rust-lang/crates.io-index" 1380 | checksum = "72ce181e3f6bf82d6c1dc569103ca7b1bd964c60ba03d7e6cdfbb3e3eb7f7405" 1381 | dependencies = [ 1382 | "gif", 1383 | "image", 1384 | "plotters-backend", 1385 | ] 1386 | 1387 | [[package]] 1388 | name = "plotters-svg" 1389 | version = "0.3.7" 1390 | source = "registry+https://github.com/rust-lang/crates.io-index" 1391 | checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" 1392 | dependencies = [ 1393 | "plotters-backend", 1394 | ] 1395 | 1396 | [[package]] 1397 | name = "png" 1398 | version = "0.17.16" 1399 | source = "registry+https://github.com/rust-lang/crates.io-index" 1400 | checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" 1401 | dependencies = [ 1402 | "bitflags 1.3.2", 1403 | "crc32fast", 1404 | "fdeflate", 1405 | "flate2", 1406 | "miniz_oxide", 1407 | ] 1408 | 1409 | [[package]] 1410 | name = "portable-atomic" 1411 | version = "1.11.1" 1412 | source = "registry+https://github.com/rust-lang/crates.io-index" 1413 | checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" 1414 | 1415 | [[package]] 1416 | name = "ppv-lite86" 1417 | version = "0.2.21" 1418 | source = "registry+https://github.com/rust-lang/crates.io-index" 1419 | checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" 1420 | dependencies = [ 1421 | "zerocopy", 1422 | ] 1423 | 1424 | [[package]] 1425 | name = "prettyplease" 1426 | version = "0.2.34" 1427 | source = "registry+https://github.com/rust-lang/crates.io-index" 1428 | checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" 1429 | dependencies = [ 1430 | "proc-macro2", 1431 | "syn 2.0.103", 1432 | ] 1433 | 1434 | [[package]] 1435 | name = "proc-macro-error" 1436 | version = "1.0.4" 1437 | source = "registry+https://github.com/rust-lang/crates.io-index" 1438 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 1439 | dependencies = [ 1440 | "proc-macro-error-attr", 1441 | "proc-macro2", 1442 | "quote", 1443 | "syn 1.0.109", 1444 | "version_check", 1445 | ] 1446 | 1447 | [[package]] 1448 | name = "proc-macro-error-attr" 1449 | version = "1.0.4" 1450 | source = "registry+https://github.com/rust-lang/crates.io-index" 1451 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 1452 | dependencies = [ 1453 | "proc-macro2", 1454 | "quote", 1455 | "version_check", 1456 | ] 1457 | 1458 | [[package]] 1459 | name = "proc-macro2" 1460 | version = "1.0.95" 1461 | source = "registry+https://github.com/rust-lang/crates.io-index" 1462 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 1463 | dependencies = [ 1464 | "unicode-ident", 1465 | ] 1466 | 1467 | [[package]] 1468 | name = "proc-maps" 1469 | version = "0.4.0" 1470 | source = "registry+https://github.com/rust-lang/crates.io-index" 1471 | checksum = "3db44c5aa60e193a25fcd93bb9ed27423827e8f118897866f946e2cf936c44fb" 1472 | dependencies = [ 1473 | "anyhow", 1474 | "bindgen", 1475 | "libc", 1476 | "libproc", 1477 | "mach2", 1478 | "winapi", 1479 | ] 1480 | 1481 | [[package]] 1482 | name = "py-spy" 1483 | version = "0.4.0" 1484 | source = "registry+https://github.com/rust-lang/crates.io-index" 1485 | checksum = "c4f4300b4ae8b012f43c3ec196a5c24dfb7921d293b3b47e6856617271ae6d3b" 1486 | dependencies = [ 1487 | "anyhow", 1488 | "chrono", 1489 | "clap 3.2.25", 1490 | "clap_complete", 1491 | "console", 1492 | "cpp_demangle", 1493 | "ctrlc", 1494 | "env_logger 0.10.2", 1495 | "goblin", 1496 | "indicatif", 1497 | "inferno", 1498 | "lazy_static", 1499 | "libc", 1500 | "log", 1501 | "lru", 1502 | "memmap2", 1503 | "num-traits", 1504 | "proc-maps", 1505 | "rand", 1506 | "rand_distr", 1507 | "regex", 1508 | "remoteprocess", 1509 | "serde", 1510 | "serde_derive", 1511 | "serde_json", 1512 | "tempfile", 1513 | "termios", 1514 | "winapi", 1515 | ] 1516 | 1517 | [[package]] 1518 | name = "quick-xml" 1519 | version = "0.26.0" 1520 | source = "registry+https://github.com/rust-lang/crates.io-index" 1521 | checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" 1522 | dependencies = [ 1523 | "memchr", 1524 | ] 1525 | 1526 | [[package]] 1527 | name = "quote" 1528 | version = "1.0.40" 1529 | source = "registry+https://github.com/rust-lang/crates.io-index" 1530 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 1531 | dependencies = [ 1532 | "proc-macro2", 1533 | ] 1534 | 1535 | [[package]] 1536 | name = "r-efi" 1537 | version = "5.2.0" 1538 | source = "registry+https://github.com/rust-lang/crates.io-index" 1539 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 1540 | 1541 | [[package]] 1542 | name = "rand" 1543 | version = "0.8.5" 1544 | source = "registry+https://github.com/rust-lang/crates.io-index" 1545 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1546 | dependencies = [ 1547 | "libc", 1548 | "rand_chacha", 1549 | "rand_core", 1550 | ] 1551 | 1552 | [[package]] 1553 | name = "rand_chacha" 1554 | version = "0.3.1" 1555 | source = "registry+https://github.com/rust-lang/crates.io-index" 1556 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1557 | dependencies = [ 1558 | "ppv-lite86", 1559 | "rand_core", 1560 | ] 1561 | 1562 | [[package]] 1563 | name = "rand_core" 1564 | version = "0.6.4" 1565 | source = "registry+https://github.com/rust-lang/crates.io-index" 1566 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1567 | dependencies = [ 1568 | "getrandom 0.2.16", 1569 | ] 1570 | 1571 | [[package]] 1572 | name = "rand_distr" 1573 | version = "0.4.3" 1574 | source = "registry+https://github.com/rust-lang/crates.io-index" 1575 | checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" 1576 | dependencies = [ 1577 | "num-traits", 1578 | "rand", 1579 | ] 1580 | 1581 | [[package]] 1582 | name = "read-process-memory" 1583 | version = "0.1.6" 1584 | source = "registry+https://github.com/rust-lang/crates.io-index" 1585 | checksum = "8497683b2f0b6887786f1928c118f26ecc6bb3d78bbb6ed23e8e7ba110af3bb0" 1586 | dependencies = [ 1587 | "libc", 1588 | "log", 1589 | "mach", 1590 | "winapi", 1591 | ] 1592 | 1593 | [[package]] 1594 | name = "redox_syscall" 1595 | version = "0.5.13" 1596 | source = "registry+https://github.com/rust-lang/crates.io-index" 1597 | checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" 1598 | dependencies = [ 1599 | "bitflags 2.9.1", 1600 | ] 1601 | 1602 | [[package]] 1603 | name = "redox_users" 1604 | version = "0.5.0" 1605 | source = "registry+https://github.com/rust-lang/crates.io-index" 1606 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 1607 | dependencies = [ 1608 | "getrandom 0.2.16", 1609 | "libredox", 1610 | "thiserror", 1611 | ] 1612 | 1613 | [[package]] 1614 | name = "regex" 1615 | version = "1.11.1" 1616 | source = "registry+https://github.com/rust-lang/crates.io-index" 1617 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 1618 | dependencies = [ 1619 | "aho-corasick", 1620 | "memchr", 1621 | "regex-automata", 1622 | "regex-syntax", 1623 | ] 1624 | 1625 | [[package]] 1626 | name = "regex-automata" 1627 | version = "0.4.9" 1628 | source = "registry+https://github.com/rust-lang/crates.io-index" 1629 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 1630 | dependencies = [ 1631 | "aho-corasick", 1632 | "memchr", 1633 | "regex-syntax", 1634 | ] 1635 | 1636 | [[package]] 1637 | name = "regex-syntax" 1638 | version = "0.8.5" 1639 | source = "registry+https://github.com/rust-lang/crates.io-index" 1640 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 1641 | 1642 | [[package]] 1643 | name = "remoteprocess" 1644 | version = "0.5.0" 1645 | source = "registry+https://github.com/rust-lang/crates.io-index" 1646 | checksum = "e6194770c7afc1d2ca42acde19267938eb7d52ccb5b727f1a2eafa8d4d00ff20" 1647 | dependencies = [ 1648 | "addr2line 0.24.2", 1649 | "cfg-if", 1650 | "goblin", 1651 | "lazy_static", 1652 | "libc", 1653 | "libproc", 1654 | "log", 1655 | "mach", 1656 | "mach_o_sys", 1657 | "memmap2", 1658 | "nix 0.26.4", 1659 | "object 0.36.7", 1660 | "proc-maps", 1661 | "read-process-memory", 1662 | "regex", 1663 | "winapi", 1664 | ] 1665 | 1666 | [[package]] 1667 | name = "rgb" 1668 | version = "0.8.50" 1669 | source = "registry+https://github.com/rust-lang/crates.io-index" 1670 | checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" 1671 | dependencies = [ 1672 | "bytemuck", 1673 | ] 1674 | 1675 | [[package]] 1676 | name = "rmp" 1677 | version = "0.8.14" 1678 | source = "registry+https://github.com/rust-lang/crates.io-index" 1679 | checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" 1680 | dependencies = [ 1681 | "byteorder", 1682 | "num-traits", 1683 | "paste", 1684 | ] 1685 | 1686 | [[package]] 1687 | name = "rmp-serde" 1688 | version = "1.3.0" 1689 | source = "registry+https://github.com/rust-lang/crates.io-index" 1690 | checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db" 1691 | dependencies = [ 1692 | "byteorder", 1693 | "rmp", 1694 | "serde", 1695 | ] 1696 | 1697 | [[package]] 1698 | name = "rustc-demangle" 1699 | version = "0.1.25" 1700 | source = "registry+https://github.com/rust-lang/crates.io-index" 1701 | checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" 1702 | 1703 | [[package]] 1704 | name = "rustc-hash" 1705 | version = "1.1.0" 1706 | source = "registry+https://github.com/rust-lang/crates.io-index" 1707 | checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 1708 | 1709 | [[package]] 1710 | name = "rustc_version" 1711 | version = "0.4.1" 1712 | source = "registry+https://github.com/rust-lang/crates.io-index" 1713 | checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" 1714 | dependencies = [ 1715 | "semver", 1716 | ] 1717 | 1718 | [[package]] 1719 | name = "rustix" 1720 | version = "0.37.28" 1721 | source = "registry+https://github.com/rust-lang/crates.io-index" 1722 | checksum = "519165d378b97752ca44bbe15047d5d3409e875f39327546b42ac81d7e18c1b6" 1723 | dependencies = [ 1724 | "bitflags 1.3.2", 1725 | "errno", 1726 | "io-lifetimes", 1727 | "libc", 1728 | "linux-raw-sys 0.3.8", 1729 | "windows-sys 0.48.0", 1730 | ] 1731 | 1732 | [[package]] 1733 | name = "rustix" 1734 | version = "1.0.7" 1735 | source = "registry+https://github.com/rust-lang/crates.io-index" 1736 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 1737 | dependencies = [ 1738 | "bitflags 2.9.1", 1739 | "errno", 1740 | "libc", 1741 | "linux-raw-sys 0.9.4", 1742 | "windows-sys 0.59.0", 1743 | ] 1744 | 1745 | [[package]] 1746 | name = "rustversion" 1747 | version = "1.0.21" 1748 | source = "registry+https://github.com/rust-lang/crates.io-index" 1749 | checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" 1750 | 1751 | [[package]] 1752 | name = "ruzstd" 1753 | version = "0.7.3" 1754 | source = "registry+https://github.com/rust-lang/crates.io-index" 1755 | checksum = "fad02996bfc73da3e301efe90b1837be9ed8f4a462b6ed410aa35d00381de89f" 1756 | dependencies = [ 1757 | "twox-hash 1.6.3", 1758 | ] 1759 | 1760 | [[package]] 1761 | name = "ruzstd" 1762 | version = "0.8.1" 1763 | source = "registry+https://github.com/rust-lang/crates.io-index" 1764 | checksum = "3640bec8aad418d7d03c72ea2de10d5c646a598f9883c7babc160d91e3c1b26c" 1765 | dependencies = [ 1766 | "twox-hash 2.1.1", 1767 | ] 1768 | 1769 | [[package]] 1770 | name = "ryu" 1771 | version = "1.0.20" 1772 | source = "registry+https://github.com/rust-lang/crates.io-index" 1773 | checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" 1774 | 1775 | [[package]] 1776 | name = "same-file" 1777 | version = "1.0.6" 1778 | source = "registry+https://github.com/rust-lang/crates.io-index" 1779 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1780 | dependencies = [ 1781 | "winapi-util", 1782 | ] 1783 | 1784 | [[package]] 1785 | name = "scopeguard" 1786 | version = "1.2.0" 1787 | source = "registry+https://github.com/rust-lang/crates.io-index" 1788 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1789 | 1790 | [[package]] 1791 | name = "scroll" 1792 | version = "0.12.0" 1793 | source = "registry+https://github.com/rust-lang/crates.io-index" 1794 | checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" 1795 | dependencies = [ 1796 | "scroll_derive", 1797 | ] 1798 | 1799 | [[package]] 1800 | name = "scroll_derive" 1801 | version = "0.12.1" 1802 | source = "registry+https://github.com/rust-lang/crates.io-index" 1803 | checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" 1804 | dependencies = [ 1805 | "proc-macro2", 1806 | "quote", 1807 | "syn 2.0.103", 1808 | ] 1809 | 1810 | [[package]] 1811 | name = "semver" 1812 | version = "1.0.26" 1813 | source = "registry+https://github.com/rust-lang/crates.io-index" 1814 | checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" 1815 | 1816 | [[package]] 1817 | name = "serde" 1818 | version = "1.0.219" 1819 | source = "registry+https://github.com/rust-lang/crates.io-index" 1820 | checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" 1821 | dependencies = [ 1822 | "serde_derive", 1823 | ] 1824 | 1825 | [[package]] 1826 | name = "serde_derive" 1827 | version = "1.0.219" 1828 | source = "registry+https://github.com/rust-lang/crates.io-index" 1829 | checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" 1830 | dependencies = [ 1831 | "proc-macro2", 1832 | "quote", 1833 | "syn 2.0.103", 1834 | ] 1835 | 1836 | [[package]] 1837 | name = "serde_json" 1838 | version = "1.0.140" 1839 | source = "registry+https://github.com/rust-lang/crates.io-index" 1840 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" 1841 | dependencies = [ 1842 | "itoa", 1843 | "memchr", 1844 | "ryu", 1845 | "serde", 1846 | ] 1847 | 1848 | [[package]] 1849 | name = "serde_spanned" 1850 | version = "0.6.9" 1851 | source = "registry+https://github.com/rust-lang/crates.io-index" 1852 | checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" 1853 | dependencies = [ 1854 | "serde", 1855 | ] 1856 | 1857 | [[package]] 1858 | name = "shlex" 1859 | version = "1.3.0" 1860 | source = "registry+https://github.com/rust-lang/crates.io-index" 1861 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 1862 | 1863 | [[package]] 1864 | name = "simd-adler32" 1865 | version = "0.3.7" 1866 | source = "registry+https://github.com/rust-lang/crates.io-index" 1867 | checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" 1868 | 1869 | [[package]] 1870 | name = "smallvec" 1871 | version = "1.15.1" 1872 | source = "registry+https://github.com/rust-lang/crates.io-index" 1873 | checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" 1874 | 1875 | [[package]] 1876 | name = "stable_deref_trait" 1877 | version = "1.2.0" 1878 | source = "registry+https://github.com/rust-lang/crates.io-index" 1879 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1880 | 1881 | [[package]] 1882 | name = "static_assertions" 1883 | version = "1.1.0" 1884 | source = "registry+https://github.com/rust-lang/crates.io-index" 1885 | checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" 1886 | 1887 | [[package]] 1888 | name = "str_stack" 1889 | version = "0.1.0" 1890 | source = "registry+https://github.com/rust-lang/crates.io-index" 1891 | checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb" 1892 | 1893 | [[package]] 1894 | name = "strsim" 1895 | version = "0.10.0" 1896 | source = "registry+https://github.com/rust-lang/crates.io-index" 1897 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 1898 | 1899 | [[package]] 1900 | name = "strsim" 1901 | version = "0.11.1" 1902 | source = "registry+https://github.com/rust-lang/crates.io-index" 1903 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 1904 | 1905 | [[package]] 1906 | name = "syn" 1907 | version = "1.0.109" 1908 | source = "registry+https://github.com/rust-lang/crates.io-index" 1909 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1910 | dependencies = [ 1911 | "proc-macro2", 1912 | "quote", 1913 | "unicode-ident", 1914 | ] 1915 | 1916 | [[package]] 1917 | name = "syn" 1918 | version = "2.0.103" 1919 | source = "registry+https://github.com/rust-lang/crates.io-index" 1920 | checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" 1921 | dependencies = [ 1922 | "proc-macro2", 1923 | "quote", 1924 | "unicode-ident", 1925 | ] 1926 | 1927 | [[package]] 1928 | name = "tempfile" 1929 | version = "3.20.0" 1930 | source = "registry+https://github.com/rust-lang/crates.io-index" 1931 | checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" 1932 | dependencies = [ 1933 | "fastrand", 1934 | "getrandom 0.3.3", 1935 | "once_cell", 1936 | "rustix 1.0.7", 1937 | "windows-sys 0.59.0", 1938 | ] 1939 | 1940 | [[package]] 1941 | name = "termcolor" 1942 | version = "1.4.1" 1943 | source = "registry+https://github.com/rust-lang/crates.io-index" 1944 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 1945 | dependencies = [ 1946 | "winapi-util", 1947 | ] 1948 | 1949 | [[package]] 1950 | name = "terminal_size" 1951 | version = "0.2.6" 1952 | source = "registry+https://github.com/rust-lang/crates.io-index" 1953 | checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" 1954 | dependencies = [ 1955 | "rustix 0.37.28", 1956 | "windows-sys 0.48.0", 1957 | ] 1958 | 1959 | [[package]] 1960 | name = "terminal_size" 1961 | version = "0.4.2" 1962 | source = "registry+https://github.com/rust-lang/crates.io-index" 1963 | checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" 1964 | dependencies = [ 1965 | "rustix 1.0.7", 1966 | "windows-sys 0.59.0", 1967 | ] 1968 | 1969 | [[package]] 1970 | name = "termios" 1971 | version = "0.3.3" 1972 | source = "registry+https://github.com/rust-lang/crates.io-index" 1973 | checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" 1974 | dependencies = [ 1975 | "libc", 1976 | ] 1977 | 1978 | [[package]] 1979 | name = "textwrap" 1980 | version = "0.16.2" 1981 | source = "registry+https://github.com/rust-lang/crates.io-index" 1982 | checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" 1983 | dependencies = [ 1984 | "terminal_size 0.4.2", 1985 | ] 1986 | 1987 | [[package]] 1988 | name = "thiserror" 1989 | version = "2.0.12" 1990 | source = "registry+https://github.com/rust-lang/crates.io-index" 1991 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 1992 | dependencies = [ 1993 | "thiserror-impl", 1994 | ] 1995 | 1996 | [[package]] 1997 | name = "thiserror-impl" 1998 | version = "2.0.12" 1999 | source = "registry+https://github.com/rust-lang/crates.io-index" 2000 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 2001 | dependencies = [ 2002 | "proc-macro2", 2003 | "quote", 2004 | "syn 2.0.103", 2005 | ] 2006 | 2007 | [[package]] 2008 | name = "toml" 2009 | version = "0.8.23" 2010 | source = "registry+https://github.com/rust-lang/crates.io-index" 2011 | checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" 2012 | dependencies = [ 2013 | "serde", 2014 | "serde_spanned", 2015 | "toml_datetime", 2016 | "toml_edit", 2017 | ] 2018 | 2019 | [[package]] 2020 | name = "toml_datetime" 2021 | version = "0.6.11" 2022 | source = "registry+https://github.com/rust-lang/crates.io-index" 2023 | checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" 2024 | dependencies = [ 2025 | "serde", 2026 | ] 2027 | 2028 | [[package]] 2029 | name = "toml_edit" 2030 | version = "0.22.27" 2031 | source = "registry+https://github.com/rust-lang/crates.io-index" 2032 | checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" 2033 | dependencies = [ 2034 | "indexmap 2.9.0", 2035 | "serde", 2036 | "serde_spanned", 2037 | "toml_datetime", 2038 | "toml_write", 2039 | "winnow", 2040 | ] 2041 | 2042 | [[package]] 2043 | name = "toml_write" 2044 | version = "0.1.2" 2045 | source = "registry+https://github.com/rust-lang/crates.io-index" 2046 | checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" 2047 | 2048 | [[package]] 2049 | name = "ttf-parser" 2050 | version = "0.20.0" 2051 | source = "registry+https://github.com/rust-lang/crates.io-index" 2052 | checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" 2053 | 2054 | [[package]] 2055 | name = "twox-hash" 2056 | version = "1.6.3" 2057 | source = "registry+https://github.com/rust-lang/crates.io-index" 2058 | checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" 2059 | dependencies = [ 2060 | "cfg-if", 2061 | "static_assertions", 2062 | ] 2063 | 2064 | [[package]] 2065 | name = "twox-hash" 2066 | version = "2.1.1" 2067 | source = "registry+https://github.com/rust-lang/crates.io-index" 2068 | checksum = "8b907da542cbced5261bd3256de1b3a1bf340a3d37f93425a07362a1d687de56" 2069 | 2070 | [[package]] 2071 | name = "typed-arena" 2072 | version = "2.0.2" 2073 | source = "registry+https://github.com/rust-lang/crates.io-index" 2074 | checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" 2075 | 2076 | [[package]] 2077 | name = "unicode-ident" 2078 | version = "1.0.18" 2079 | source = "registry+https://github.com/rust-lang/crates.io-index" 2080 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 2081 | 2082 | [[package]] 2083 | name = "unicode-width" 2084 | version = "0.2.1" 2085 | source = "registry+https://github.com/rust-lang/crates.io-index" 2086 | checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" 2087 | 2088 | [[package]] 2089 | name = "utf8-width" 2090 | version = "0.1.7" 2091 | source = "registry+https://github.com/rust-lang/crates.io-index" 2092 | checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" 2093 | 2094 | [[package]] 2095 | name = "utf8parse" 2096 | version = "0.2.2" 2097 | source = "registry+https://github.com/rust-lang/crates.io-index" 2098 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 2099 | 2100 | [[package]] 2101 | name = "version_check" 2102 | version = "0.9.5" 2103 | source = "registry+https://github.com/rust-lang/crates.io-index" 2104 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2105 | 2106 | [[package]] 2107 | name = "walkdir" 2108 | version = "2.5.0" 2109 | source = "registry+https://github.com/rust-lang/crates.io-index" 2110 | checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" 2111 | dependencies = [ 2112 | "same-file", 2113 | "winapi-util", 2114 | ] 2115 | 2116 | [[package]] 2117 | name = "wasi" 2118 | version = "0.11.1+wasi-snapshot-preview1" 2119 | source = "registry+https://github.com/rust-lang/crates.io-index" 2120 | checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" 2121 | 2122 | [[package]] 2123 | name = "wasi" 2124 | version = "0.14.2+wasi-0.2.4" 2125 | source = "registry+https://github.com/rust-lang/crates.io-index" 2126 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 2127 | dependencies = [ 2128 | "wit-bindgen-rt", 2129 | ] 2130 | 2131 | [[package]] 2132 | name = "wasm-bindgen" 2133 | version = "0.2.100" 2134 | source = "registry+https://github.com/rust-lang/crates.io-index" 2135 | checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" 2136 | dependencies = [ 2137 | "cfg-if", 2138 | "once_cell", 2139 | "rustversion", 2140 | "wasm-bindgen-macro", 2141 | ] 2142 | 2143 | [[package]] 2144 | name = "wasm-bindgen-backend" 2145 | version = "0.2.100" 2146 | source = "registry+https://github.com/rust-lang/crates.io-index" 2147 | checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" 2148 | dependencies = [ 2149 | "bumpalo", 2150 | "log", 2151 | "proc-macro2", 2152 | "quote", 2153 | "syn 2.0.103", 2154 | "wasm-bindgen-shared", 2155 | ] 2156 | 2157 | [[package]] 2158 | name = "wasm-bindgen-macro" 2159 | version = "0.2.100" 2160 | source = "registry+https://github.com/rust-lang/crates.io-index" 2161 | checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" 2162 | dependencies = [ 2163 | "quote", 2164 | "wasm-bindgen-macro-support", 2165 | ] 2166 | 2167 | [[package]] 2168 | name = "wasm-bindgen-macro-support" 2169 | version = "0.2.100" 2170 | source = "registry+https://github.com/rust-lang/crates.io-index" 2171 | checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" 2172 | dependencies = [ 2173 | "proc-macro2", 2174 | "quote", 2175 | "syn 2.0.103", 2176 | "wasm-bindgen-backend", 2177 | "wasm-bindgen-shared", 2178 | ] 2179 | 2180 | [[package]] 2181 | name = "wasm-bindgen-shared" 2182 | version = "0.2.100" 2183 | source = "registry+https://github.com/rust-lang/crates.io-index" 2184 | checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" 2185 | dependencies = [ 2186 | "unicode-ident", 2187 | ] 2188 | 2189 | [[package]] 2190 | name = "web-sys" 2191 | version = "0.3.77" 2192 | source = "registry+https://github.com/rust-lang/crates.io-index" 2193 | checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" 2194 | dependencies = [ 2195 | "js-sys", 2196 | "wasm-bindgen", 2197 | ] 2198 | 2199 | [[package]] 2200 | name = "web-time" 2201 | version = "1.1.0" 2202 | source = "registry+https://github.com/rust-lang/crates.io-index" 2203 | checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 2204 | dependencies = [ 2205 | "js-sys", 2206 | "wasm-bindgen", 2207 | ] 2208 | 2209 | [[package]] 2210 | name = "weezl" 2211 | version = "0.1.10" 2212 | source = "registry+https://github.com/rust-lang/crates.io-index" 2213 | checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" 2214 | 2215 | [[package]] 2216 | name = "winapi" 2217 | version = "0.3.9" 2218 | source = "registry+https://github.com/rust-lang/crates.io-index" 2219 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2220 | dependencies = [ 2221 | "winapi-i686-pc-windows-gnu", 2222 | "winapi-x86_64-pc-windows-gnu", 2223 | ] 2224 | 2225 | [[package]] 2226 | name = "winapi-i686-pc-windows-gnu" 2227 | version = "0.4.0" 2228 | source = "registry+https://github.com/rust-lang/crates.io-index" 2229 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2230 | 2231 | [[package]] 2232 | name = "winapi-util" 2233 | version = "0.1.9" 2234 | source = "registry+https://github.com/rust-lang/crates.io-index" 2235 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 2236 | dependencies = [ 2237 | "windows-sys 0.59.0", 2238 | ] 2239 | 2240 | [[package]] 2241 | name = "winapi-x86_64-pc-windows-gnu" 2242 | version = "0.4.0" 2243 | source = "registry+https://github.com/rust-lang/crates.io-index" 2244 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2245 | 2246 | [[package]] 2247 | name = "windows-core" 2248 | version = "0.61.2" 2249 | source = "registry+https://github.com/rust-lang/crates.io-index" 2250 | checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" 2251 | dependencies = [ 2252 | "windows-implement", 2253 | "windows-interface", 2254 | "windows-link", 2255 | "windows-result", 2256 | "windows-strings", 2257 | ] 2258 | 2259 | [[package]] 2260 | name = "windows-implement" 2261 | version = "0.60.0" 2262 | source = "registry+https://github.com/rust-lang/crates.io-index" 2263 | checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" 2264 | dependencies = [ 2265 | "proc-macro2", 2266 | "quote", 2267 | "syn 2.0.103", 2268 | ] 2269 | 2270 | [[package]] 2271 | name = "windows-interface" 2272 | version = "0.59.1" 2273 | source = "registry+https://github.com/rust-lang/crates.io-index" 2274 | checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" 2275 | dependencies = [ 2276 | "proc-macro2", 2277 | "quote", 2278 | "syn 2.0.103", 2279 | ] 2280 | 2281 | [[package]] 2282 | name = "windows-link" 2283 | version = "0.1.3" 2284 | source = "registry+https://github.com/rust-lang/crates.io-index" 2285 | checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" 2286 | 2287 | [[package]] 2288 | name = "windows-result" 2289 | version = "0.3.4" 2290 | source = "registry+https://github.com/rust-lang/crates.io-index" 2291 | checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" 2292 | dependencies = [ 2293 | "windows-link", 2294 | ] 2295 | 2296 | [[package]] 2297 | name = "windows-strings" 2298 | version = "0.4.2" 2299 | source = "registry+https://github.com/rust-lang/crates.io-index" 2300 | checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" 2301 | dependencies = [ 2302 | "windows-link", 2303 | ] 2304 | 2305 | [[package]] 2306 | name = "windows-sys" 2307 | version = "0.48.0" 2308 | source = "registry+https://github.com/rust-lang/crates.io-index" 2309 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 2310 | dependencies = [ 2311 | "windows-targets 0.48.5", 2312 | ] 2313 | 2314 | [[package]] 2315 | name = "windows-sys" 2316 | version = "0.59.0" 2317 | source = "registry+https://github.com/rust-lang/crates.io-index" 2318 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 2319 | dependencies = [ 2320 | "windows-targets 0.52.6", 2321 | ] 2322 | 2323 | [[package]] 2324 | name = "windows-targets" 2325 | version = "0.48.5" 2326 | source = "registry+https://github.com/rust-lang/crates.io-index" 2327 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 2328 | dependencies = [ 2329 | "windows_aarch64_gnullvm 0.48.5", 2330 | "windows_aarch64_msvc 0.48.5", 2331 | "windows_i686_gnu 0.48.5", 2332 | "windows_i686_msvc 0.48.5", 2333 | "windows_x86_64_gnu 0.48.5", 2334 | "windows_x86_64_gnullvm 0.48.5", 2335 | "windows_x86_64_msvc 0.48.5", 2336 | ] 2337 | 2338 | [[package]] 2339 | name = "windows-targets" 2340 | version = "0.52.6" 2341 | source = "registry+https://github.com/rust-lang/crates.io-index" 2342 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2343 | dependencies = [ 2344 | "windows_aarch64_gnullvm 0.52.6", 2345 | "windows_aarch64_msvc 0.52.6", 2346 | "windows_i686_gnu 0.52.6", 2347 | "windows_i686_gnullvm", 2348 | "windows_i686_msvc 0.52.6", 2349 | "windows_x86_64_gnu 0.52.6", 2350 | "windows_x86_64_gnullvm 0.52.6", 2351 | "windows_x86_64_msvc 0.52.6", 2352 | ] 2353 | 2354 | [[package]] 2355 | name = "windows_aarch64_gnullvm" 2356 | version = "0.48.5" 2357 | source = "registry+https://github.com/rust-lang/crates.io-index" 2358 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2359 | 2360 | [[package]] 2361 | name = "windows_aarch64_gnullvm" 2362 | version = "0.52.6" 2363 | source = "registry+https://github.com/rust-lang/crates.io-index" 2364 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2365 | 2366 | [[package]] 2367 | name = "windows_aarch64_msvc" 2368 | version = "0.48.5" 2369 | source = "registry+https://github.com/rust-lang/crates.io-index" 2370 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2371 | 2372 | [[package]] 2373 | name = "windows_aarch64_msvc" 2374 | version = "0.52.6" 2375 | source = "registry+https://github.com/rust-lang/crates.io-index" 2376 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2377 | 2378 | [[package]] 2379 | name = "windows_i686_gnu" 2380 | version = "0.48.5" 2381 | source = "registry+https://github.com/rust-lang/crates.io-index" 2382 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2383 | 2384 | [[package]] 2385 | name = "windows_i686_gnu" 2386 | version = "0.52.6" 2387 | source = "registry+https://github.com/rust-lang/crates.io-index" 2388 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2389 | 2390 | [[package]] 2391 | name = "windows_i686_gnullvm" 2392 | version = "0.52.6" 2393 | source = "registry+https://github.com/rust-lang/crates.io-index" 2394 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2395 | 2396 | [[package]] 2397 | name = "windows_i686_msvc" 2398 | version = "0.48.5" 2399 | source = "registry+https://github.com/rust-lang/crates.io-index" 2400 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2401 | 2402 | [[package]] 2403 | name = "windows_i686_msvc" 2404 | version = "0.52.6" 2405 | source = "registry+https://github.com/rust-lang/crates.io-index" 2406 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2407 | 2408 | [[package]] 2409 | name = "windows_x86_64_gnu" 2410 | version = "0.48.5" 2411 | source = "registry+https://github.com/rust-lang/crates.io-index" 2412 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2413 | 2414 | [[package]] 2415 | name = "windows_x86_64_gnu" 2416 | version = "0.52.6" 2417 | source = "registry+https://github.com/rust-lang/crates.io-index" 2418 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2419 | 2420 | [[package]] 2421 | name = "windows_x86_64_gnullvm" 2422 | version = "0.48.5" 2423 | source = "registry+https://github.com/rust-lang/crates.io-index" 2424 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2425 | 2426 | [[package]] 2427 | name = "windows_x86_64_gnullvm" 2428 | version = "0.52.6" 2429 | source = "registry+https://github.com/rust-lang/crates.io-index" 2430 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2431 | 2432 | [[package]] 2433 | name = "windows_x86_64_msvc" 2434 | version = "0.48.5" 2435 | source = "registry+https://github.com/rust-lang/crates.io-index" 2436 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2437 | 2438 | [[package]] 2439 | name = "windows_x86_64_msvc" 2440 | version = "0.52.6" 2441 | source = "registry+https://github.com/rust-lang/crates.io-index" 2442 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2443 | 2444 | [[package]] 2445 | name = "winnow" 2446 | version = "0.7.11" 2447 | source = "registry+https://github.com/rust-lang/crates.io-index" 2448 | checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" 2449 | dependencies = [ 2450 | "memchr", 2451 | ] 2452 | 2453 | [[package]] 2454 | name = "wio" 2455 | version = "0.2.2" 2456 | source = "registry+https://github.com/rust-lang/crates.io-index" 2457 | checksum = "5d129932f4644ac2396cb456385cbf9e63b5b30c6e8dc4820bdca4eb082037a5" 2458 | dependencies = [ 2459 | "winapi", 2460 | ] 2461 | 2462 | [[package]] 2463 | name = "wit-bindgen-rt" 2464 | version = "0.39.0" 2465 | source = "registry+https://github.com/rust-lang/crates.io-index" 2466 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 2467 | dependencies = [ 2468 | "bitflags 2.9.1", 2469 | ] 2470 | 2471 | [[package]] 2472 | name = "yeslogic-fontconfig-sys" 2473 | version = "6.0.0" 2474 | source = "registry+https://github.com/rust-lang/crates.io-index" 2475 | checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd" 2476 | dependencies = [ 2477 | "dlib", 2478 | "once_cell", 2479 | "pkg-config", 2480 | ] 2481 | 2482 | [[package]] 2483 | name = "zerocopy" 2484 | version = "0.8.25" 2485 | source = "registry+https://github.com/rust-lang/crates.io-index" 2486 | checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" 2487 | dependencies = [ 2488 | "zerocopy-derive", 2489 | ] 2490 | 2491 | [[package]] 2492 | name = "zerocopy-derive" 2493 | version = "0.8.25" 2494 | source = "registry+https://github.com/rust-lang/crates.io-index" 2495 | checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" 2496 | dependencies = [ 2497 | "proc-macro2", 2498 | "quote", 2499 | "syn 2.0.103", 2500 | ] 2501 | 2502 | [[package]] 2503 | name = "zstd" 2504 | version = "0.13.3" 2505 | source = "registry+https://github.com/rust-lang/crates.io-index" 2506 | checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" 2507 | dependencies = [ 2508 | "zstd-safe", 2509 | ] 2510 | 2511 | [[package]] 2512 | name = "zstd-safe" 2513 | version = "7.2.4" 2514 | source = "registry+https://github.com/rust-lang/crates.io-index" 2515 | checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" 2516 | dependencies = [ 2517 | "zstd-sys", 2518 | ] 2519 | 2520 | [[package]] 2521 | name = "zstd-sys" 2522 | version = "2.0.15+zstd.1.5.7" 2523 | source = "registry+https://github.com/rust-lang/crates.io-index" 2524 | checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237" 2525 | dependencies = [ 2526 | "cc", 2527 | "pkg-config", 2528 | ] 2529 | --------------------------------------------------------------------------------