Skip to repository content416 lines · 15.0 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:30:42.802Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
git_runtime_diagnostics.rs
1//! Best-effort runtime diagnostics gathered to accompany the git job queue
2//! debug dump. Every individual step is fallible and any failure is logged at
3//! `warn` level and silently omitted from the output. The dump itself must
4//! never fail just because diagnostics couldn't be collected.
5//!
6//! What we gather:
7//! - All transitive descendant processes of the current Zed process
8//! (cross-platform via `sysinfo`).
9//! - On Linux: each descendant's `/proc/<pid>/wchan` (kernel function the
10//! thread is currently sleeping in) and `State:` from `/proc/<pid>/status`.
11//! - On macOS: for any descendant whose executable name contains `git`, a
12//! short `sample`-based user-space stack and `lsof` output. Both require
13//! the corresponding system binaries; if they aren't present or the
14//! invocation fails we skip them.
15//! - On Windows: just the process tree (no portable way to grab another
16//! process's stack).
17//!
18//! The output is a `serde_json::Value`. Callers merge it into whatever larger
19//! JSON payload they're producing.
20//!
21//! This is invoked from a developer-only "show git job queue" action, so it
22//! is acceptable for the macOS path to spend a few seconds sampling.
23
24use serde_json::{Map, Value};
25
26pub async fn gather() -> Value {
27 let mut diag = Map::new();
28
29 match collect_process_tree() {
30 Ok(tree) => {
31 diag.insert("processes".into(), tree);
32 }
33 Err(err) => {
34 log::warn!("git runtime diagnostics: failed to collect process tree: {err:#}");
35 }
36 }
37
38 #[cfg(target_os = "linux")]
39 {
40 match collect_linux_proc_info() {
41 Ok(info) => {
42 diag.insert("linux_proc".into(), info);
43 }
44 Err(err) => {
45 log::warn!("git runtime diagnostics: failed to read /proc info: {err:#}");
46 }
47 }
48 }
49
50 #[cfg(target_os = "macos")]
51 {
52 match collect_macos_git_child_diagnostics().await {
53 Ok(info) => {
54 if !info.is_null() {
55 diag.insert("macos_git_children".into(), info);
56 }
57 }
58 Err(err) => {
59 log::warn!(
60 "git runtime diagnostics: failed to collect macOS git-child info: {err:#}"
61 );
62 }
63 }
64 }
65
66 Value::Object(diag)
67}
68
69/// Walk the descendant tree of the current process and return a JSON array
70/// describing each descendant. Cross-platform; uses `sysinfo`.
71fn collect_process_tree() -> anyhow::Result<Value> {
72 use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System, UpdateKind};
73
74 let current_pid = sysinfo::get_current_pid()
75 .map_err(|e| anyhow::anyhow!("sysinfo::get_current_pid failed: {e}"))?;
76
77 let refresh = ProcessRefreshKind::nothing()
78 .with_cmd(UpdateKind::Always)
79 .with_exe(UpdateKind::Always);
80 let mut system = System::new_with_specifics(RefreshKind::nothing().with_processes(refresh));
81 system.refresh_processes_specifics(ProcessesToUpdate::All, true, refresh);
82
83 let descendants = descendants_of(&system, current_pid);
84
85 let entries: Vec<Value> = descendants
86 .iter()
87 .filter_map(|pid| {
88 let process = system.process(*pid)?;
89 let cmd = sanitize_cmd(
90 process
91 .cmd()
92 .iter()
93 .map(|s| s.to_string_lossy().into_owned()),
94 );
95 Some(serde_json::json!({
96 "pid": pid.as_u32(),
97 "ppid": process.parent().map(|p| p.as_u32()),
98 "name": process.name().to_string_lossy(),
99 "exe": process.exe().map(|p| p.display().to_string()),
100 "cmd": cmd,
101 "status": format!("{:?}", process.status()),
102 "run_time_secs": process.run_time(),
103 }))
104 })
105 .collect();
106
107 Ok(serde_json::json!({
108 "zed_pid": current_pid.as_u32(),
109 "descendant_count": entries.len(),
110 "descendants": entries,
111 }))
112}
113
114/// Scrub a process's reported argv to avoid leaking environment-variable
115/// values. sysinfo's `Process::cmd()` on macOS goes through `KERN_PROCARGS2`
116/// and can include envp in addition to argv for some processes, which means
117/// the raw output can contain things like `ANTHROPIC_API_KEY=…`. We replace
118/// any entry that matches a conservative env-var pattern (uppercase
119/// identifier ending in `=`) with `KEY=<redacted>`. If *every* entry got
120/// redacted then sysinfo's data for this process is too garbled to trust as
121/// argv, so we return `None` so the caller emits a JSON null rather than
122/// something misleading.
123fn sanitize_cmd(cmd: impl IntoIterator<Item = String>) -> Option<Vec<String>> {
124 let sanitized: Vec<String> = cmd.into_iter().map(redact_env_var_entry).collect();
125 if sanitized.is_empty() {
126 return None;
127 }
128 let all_redacted = sanitized.iter().all(|s| s.ends_with("=<redacted>"));
129 if all_redacted { None } else { Some(sanitized) }
130}
131
132fn redact_env_var_entry(entry: String) -> String {
133 // Match `IDENT=...` where IDENT is at least two characters starting with
134 // an uppercase letter or underscore and otherwise uppercase/digit/under.
135 // CLI flags (`--foo=bar`, `-x=y`, `/path=value`) don't match.
136 let Some(eq_index) = entry.find('=') else {
137 return entry;
138 };
139 let key = &entry[..eq_index];
140 if !key.is_empty()
141 && key
142 .chars()
143 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
144 && key.starts_with(|c: char| c.is_ascii_uppercase() || c == '_')
145 {
146 format!("{key}=<redacted>")
147 } else {
148 entry
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[track_caller]
157 fn assert_redacts(input: &str, expected: &str) {
158 assert_eq!(redact_env_var_entry(input.to_string()), expected);
159 }
160
161 #[test]
162 fn redacts_secret_looking_env_vars() {
163 assert_redacts(
164 "ANTHROPIC_API_KEY=sk-ant-api03-abcdef",
165 "ANTHROPIC_API_KEY=<redacted>",
166 );
167 assert_redacts(
168 "AWS_SECRET_ACCESS_KEY=anything-at-all",
169 "AWS_SECRET_ACCESS_KEY=<redacted>",
170 );
171 assert_redacts("PATH=/usr/bin:/bin", "PATH=<redacted>");
172 assert_redacts("A=1", "A=<redacted>");
173 assert_redacts("_FOO=bar", "_FOO=<redacted>");
174 // Values may legitimately contain `=`; only the value portion is dropped.
175 assert_redacts("TOKEN=abc=def=ghi", "TOKEN=<redacted>");
176 // Empty value still redacts (and importantly, doesn't pretend to be a flag).
177 assert_redacts("PASSWORD=", "PASSWORD=<redacted>");
178 }
179
180 #[test]
181 fn leaves_real_argv_alone() {
182 // CLI flags that happen to contain `=`.
183 assert_redacts("--max-old-space-size=8092", "--max-old-space-size=8092");
184 assert_redacts("-Dfoo=bar", "-Dfoo=bar");
185 // Paths.
186 assert_redacts("/opt/homebrew/bin/node", "/opt/homebrew/bin/node");
187 // Bare strings without `=`.
188 assert_redacts("--cancellationPipeName", "--cancellationPipeName");
189 assert_redacts(
190 "npm exec mcp-remote https://example.com",
191 "npm exec mcp-remote https://example.com",
192 );
193 // Lowercase / mixed-case identifiers aren't env vars by convention; leave them.
194 assert_redacts("foo=bar", "foo=bar");
195 assert_redacts("camelCase=value", "camelCase=value");
196 // Pathological: `=value` with no key.
197 assert_redacts("=value", "=value");
198 }
199
200 #[test]
201 fn sanitize_returns_none_when_everything_redacted() {
202 let cmd = vec![
203 "FOO=1".to_string(),
204 "BAR=2".to_string(),
205 "ANTHROPIC_API_KEY=secret".to_string(),
206 ];
207 assert_eq!(sanitize_cmd(cmd), None);
208 }
209
210 #[test]
211 fn sanitize_preserves_real_argv_and_redacts_env_vars() {
212 // The exact pattern observed in a real diagnostic dump.
213 let cmd = vec![
214 "npm exec mcp-remote https://mcp.linear.app/mcp".to_string(),
215 "ALACRITTY_WINDOW_ID=38654706047".to_string(),
216 "AMP_FORCE_BEL=1".to_string(),
217 "ANTHROPIC_API_KEY=sk-ant-api03-realsecret".to_string(),
218 ];
219 assert_eq!(
220 sanitize_cmd(cmd),
221 Some(vec![
222 "npm exec mcp-remote https://mcp.linear.app/mcp".to_string(),
223 "ALACRITTY_WINDOW_ID=<redacted>".to_string(),
224 "AMP_FORCE_BEL=<redacted>".to_string(),
225 "ANTHROPIC_API_KEY=<redacted>".to_string(),
226 ])
227 );
228 }
229
230 #[test]
231 fn sanitize_handles_empty_cmd() {
232 let cmd: Vec<String> = Vec::new();
233 assert_eq!(sanitize_cmd(cmd), None);
234 }
235}
236
237fn descendants_of(system: &sysinfo::System, root: sysinfo::Pid) -> Vec<sysinfo::Pid> {
238 let mut parent_map: std::collections::HashMap<sysinfo::Pid, Vec<sysinfo::Pid>> =
239 std::collections::HashMap::new();
240 for (pid, process) in system.processes() {
241 if let Some(parent) = process.parent() {
242 parent_map.entry(parent).or_default().push(*pid);
243 }
244 }
245 let mut out = Vec::new();
246 let mut stack = vec![root];
247 while let Some(p) = stack.pop() {
248 if let Some(children) = parent_map.get(&p) {
249 for c in children {
250 out.push(*c);
251 stack.push(*c);
252 }
253 }
254 }
255 out
256}
257
258#[cfg(target_os = "linux")]
259fn collect_linux_proc_info() -> anyhow::Result<Value> {
260 use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System};
261
262 let current_pid = sysinfo::get_current_pid()
263 .map_err(|e| anyhow::anyhow!("sysinfo::get_current_pid failed: {e}"))?;
264 let refresh = ProcessRefreshKind::nothing();
265 let mut system = System::new_with_specifics(RefreshKind::nothing().with_processes(refresh));
266 system.refresh_processes_specifics(ProcessesToUpdate::All, true, refresh);
267
268 let descendants = descendants_of(&system, current_pid);
269
270 let mut entries = Vec::new();
271 for pid in descendants {
272 let pid_u32 = pid.as_u32();
273
274 let wchan = match std::fs::read_to_string(format!("/proc/{pid_u32}/wchan")) {
275 Ok(s) => Value::String(s.trim().to_string()),
276 Err(err) => {
277 log::warn!("git runtime diagnostics: failed to read /proc/{pid_u32}/wchan: {err}");
278 Value::Null
279 }
280 };
281
282 let state = match std::fs::read_to_string(format!("/proc/{pid_u32}/status")) {
283 Ok(contents) => contents
284 .lines()
285 .find(|l| l.starts_with("State:"))
286 .map(|l| Value::String(l.trim_start_matches("State:").trim().to_string()))
287 .unwrap_or(Value::Null),
288 Err(err) => {
289 log::warn!("git runtime diagnostics: failed to read /proc/{pid_u32}/status: {err}");
290 Value::Null
291 }
292 };
293
294 entries.push(serde_json::json!({
295 "pid": pid_u32,
296 "wchan": wchan,
297 "state": state,
298 }));
299 }
300
301 Ok(serde_json::json!({ "descendants": entries }))
302}
303
304#[cfg(target_os = "macos")]
305async fn collect_macos_git_child_diagnostics() -> anyhow::Result<Value> {
306 use std::path::Path;
307 use sysinfo::{ProcessRefreshKind, ProcessesToUpdate, RefreshKind, System, UpdateKind};
308
309 let sample_available = Path::new("/usr/bin/sample").exists();
310 let lsof_available =
311 Path::new("/usr/sbin/lsof").exists() || Path::new("/usr/bin/lsof").exists();
312 let lsof_path = if Path::new("/usr/sbin/lsof").exists() {
313 "/usr/sbin/lsof"
314 } else {
315 "/usr/bin/lsof"
316 };
317
318 if !sample_available && !lsof_available {
319 return Ok(Value::Null);
320 }
321
322 let current_pid = sysinfo::get_current_pid()
323 .map_err(|e| anyhow::anyhow!("sysinfo::get_current_pid failed: {e}"))?;
324
325 let refresh = ProcessRefreshKind::nothing().with_exe(UpdateKind::Always);
326 let mut system = System::new_with_specifics(RefreshKind::nothing().with_processes(refresh));
327 system.refresh_processes_specifics(ProcessesToUpdate::All, true, refresh);
328
329 let descendants = descendants_of(&system, current_pid);
330
331 // Limit ourselves to git-flavored descendants. We don't want to spend
332 // several seconds sampling unrelated children (terminals, language
333 // servers, etc.). Match by name containing "git" — covers `git`,
334 // `git-remote-https`, `git-credential-osxkeychain`, hook subprocesses
335 // named with `git` in them, etc.
336 let git_descendants: Vec<u32> = descendants
337 .iter()
338 .filter_map(|pid| {
339 let process = system.process(*pid)?;
340 let name = process.name().to_string_lossy();
341 if name.contains("git") {
342 Some(pid.as_u32())
343 } else {
344 None
345 }
346 })
347 .collect();
348
349 if git_descendants.is_empty() {
350 return Ok(Value::Null);
351 }
352
353 let mut entries = Vec::new();
354 for pid in git_descendants {
355 let mut entry = Map::new();
356 entry.insert("pid".into(), Value::from(pid));
357
358 if sample_available {
359 match run_capturing("/usr/bin/sample", &[&pid.to_string(), "2", "-mayDie"]).await {
360 Ok(output) => {
361 entry.insert("sample".into(), Value::String(truncate(output, 64 * 1024)));
362 }
363 Err(err) => {
364 log::warn!("git runtime diagnostics: `sample {pid}` failed: {err}");
365 }
366 }
367 }
368
369 if lsof_available {
370 match run_capturing(lsof_path, &["-p", &pid.to_string()]).await {
371 Ok(output) => {
372 entry.insert("lsof".into(), Value::String(truncate(output, 64 * 1024)));
373 }
374 Err(err) => {
375 log::warn!("git runtime diagnostics: `lsof -p {pid}` failed: {err}");
376 }
377 }
378 }
379
380 entries.push(Value::Object(entry));
381 }
382
383 Ok(Value::Array(entries))
384}
385
386#[cfg(target_os = "macos")]
387async fn run_capturing(program: &str, args: &[&str]) -> anyhow::Result<String> {
388 let output = util::command::new_command(program)
389 .args(args)
390 .output()
391 .await?;
392 if !output.status.success() {
393 anyhow::bail!(
394 "{program} exited with status {:?}: {}",
395 output.status,
396 String::from_utf8_lossy(&output.stderr)
397 );
398 }
399 Ok(String::from_utf8_lossy(&output.stdout).into_owned())
400}
401
402#[cfg(target_os = "macos")]
403fn truncate(mut s: String, max_bytes: usize) -> String {
404 if s.len() <= max_bytes {
405 return s;
406 }
407 // Find a UTF-8 char boundary at or before `max_bytes`.
408 let mut cut = max_bytes;
409 while cut > 0 && !s.is_char_boundary(cut) {
410 cut -= 1;
411 }
412 s.truncate(cut);
413 s.push_str("\n…(truncated)");
414 s
415}
416