use std::collections::HashMap;
use std::io::{self, Write};
use serde::Serialize;
use crate::output::Theme;
use crate::types::*;
#[derive(Serialize)]
struct PipeConnection {
kind: String,
identifier: String,
endpoints: Vec<PipeEndpoint>,
}
#[derive(Serialize)]
struct PipeEndpoint {
pid: i32,
command: String,
fd: String,
}
fn pipe_identifier(file: &OpenFile) -> Option<(String, String)> {
let name = &file.name;
if file.file_type == FileType::Pipe {
if let Some(pos) = name.find("0x") {
let hex = name[pos..]
.split_whitespace()
.next()
.unwrap_or(&name[pos..]);
return Some(("pipe".to_string(), hex.to_string()));
}
if let Some(start) = name.find("pipe:[") {
let rest = &name[start + 6..];
if let Some(end) = rest.find(']') {
return Some(("pipe".to_string(), rest[..end].to_string()));
}
}
return Some(("pipe".to_string(), name.clone()));
}
if file.file_type == FileType::Unix {
if let Some(start) = name.find("socket:[") {
let rest = &name[start + 8..];
if let Some(end) = rest.find(']') {
return Some(("unix".to_string(), rest[..end].to_string()));
}
}
if let Some(pos) = name.find("0x") {
let hex = name[pos..]
.split_whitespace()
.next()
.unwrap_or(&name[pos..]);
return Some(("unix".to_string(), hex.to_string()));
}
}
None
}
pub fn print_pipe_chain(procs: &[Process], theme: &Theme, json: bool) {
let mut groups: HashMap<(String, String), Vec<PipeEndpoint>> = HashMap::new();
for p in procs {
for f in &p.files {
if let Some((kind, id)) = pipe_identifier(f) {
groups.entry((kind, id)).or_default().push(PipeEndpoint {
pid: p.pid,
command: p.command.clone(),
fd: f.fd.with_access(f.access),
});
}
}
}
let mut connections: Vec<PipeConnection> = groups
.into_iter()
.filter(|(_, endpoints)| endpoints.len() >= 2)
.map(|((kind, id), endpoints)| PipeConnection {
kind,
identifier: id,
endpoints,
})
.collect();
connections.sort_by_key(|c| c.endpoints.first().map(|e| e.pid).unwrap_or(0));
if json {
print_pipe_chain_json(&connections);
} else {
print_pipe_chain_text(&connections, theme);
}
}
fn print_pipe_chain_text(connections: &[PipeConnection], theme: &Theme) {
let out = io::stdout();
let mut out = out.lock();
if connections.is_empty() {
let _ = writeln!(out, "No pipe/socket IPC connections found.");
return;
}
let _ = writeln!(
out,
"\n{bold}═══ Pipe/Socket IPC Topology ═══{reset}\n",
bold = theme.bold(),
reset = theme.reset(),
);
for conn in connections {
let _ = write!(
out,
"{dim}[{kind}:{id}]{reset} ",
dim = theme.dim(),
kind = conn.kind,
id = conn.identifier,
reset = theme.reset(),
);
for (i, ep) in conn.endpoints.iter().enumerate() {
if i > 0 {
let _ = write!(
out,
" {cyan}──{kind}──>{reset} ",
cyan = theme.cyan(),
kind = conn.kind,
reset = theme.reset(),
);
}
let _ = write!(
out,
"{mag}{pid}{reset}/{bold}{cmd}{reset}({green}{fd}{reset})",
mag = theme.magenta(),
pid = ep.pid,
reset = theme.reset(),
bold = theme.bold(),
cmd = ep.command,
green = theme.green(),
fd = ep.fd,
);
}
let _ = writeln!(out);
}
let _ = writeln!(
out,
"\n{dim} {} IPC connection(s) found{reset}\n",
connections.len(),
dim = theme.dim(),
reset = theme.reset(),
);
}
fn print_pipe_chain_json(connections: &[PipeConnection]) {
let out = io::stdout();
let mut out = out.lock();
let wrapper = serde_json::json!({ "pipe_chains": connections });
let _ = serde_json::to_writer_pretty(&mut out, &wrapper);
let _ = writeln!(out);
}
#[cfg(test)]
mod tests {
use super::*;
fn make_proc(pid: i32, cmd: &str, files: Vec<OpenFile>) -> Process {
Process {
pid,
ppid: 1,
pgid: pid,
uid: 0,
command: cmd.to_string(),
files,
sel_flags: 0,
sel_state: 0,
}
}
fn make_pipe(fd: i32, name: &str) -> OpenFile {
OpenFile {
fd: FdName::Number(fd),
access: Access::ReadWrite,
file_type: FileType::Pipe,
name: name.to_string(),
..Default::default()
}
}
fn make_unix(fd: i32, name: &str) -> OpenFile {
OpenFile {
fd: FdName::Number(fd),
access: Access::ReadWrite,
file_type: FileType::Unix,
name: name.to_string(),
..Default::default()
}
}
#[test]
fn pipe_identifier_macos_pipe() {
let f = make_pipe(3, "->0xabcdef1234");
let result = pipe_identifier(&f);
assert!(result.is_some());
let (kind, id) = result.unwrap();
assert_eq!(kind, "pipe");
assert_eq!(id, "0xabcdef1234");
}
#[test]
fn pipe_identifier_macos_hex_first_token_only() {
let f = make_pipe(3, "->0xf00f00c0 trailing-garbage");
let (kind, id) = pipe_identifier(&f).unwrap();
assert_eq!(kind, "pipe");
assert_eq!(id, "0xf00f00c0");
}
#[test]
fn pipe_identifier_linux_pipe() {
let f = make_pipe(3, "pipe:[12345]");
let result = pipe_identifier(&f);
assert!(result.is_some());
let (kind, id) = result.unwrap();
assert_eq!(kind, "pipe");
assert_eq!(id, "12345");
}
#[test]
fn pipe_identifier_linux_unix_socket() {
let f = make_unix(3, "socket:[67890]");
let result = pipe_identifier(&f);
assert!(result.is_some());
let (kind, id) = result.unwrap();
assert_eq!(kind, "unix");
assert_eq!(id, "67890");
}
#[test]
fn pipe_identifier_macos_unix_socket() {
let f = make_unix(3, "->0xdeadbeef");
let result = pipe_identifier(&f);
assert!(result.is_some());
let (kind, id) = result.unwrap();
assert_eq!(kind, "unix");
assert_eq!(id, "0xdeadbeef");
}
#[test]
fn pipe_identifier_regular_file_none() {
let f = OpenFile {
fd: FdName::Number(3),
access: Access::Read,
file_type: FileType::Reg,
name: "/tmp/foo".to_string(),
..Default::default()
};
assert!(pipe_identifier(&f).is_none());
}
#[test]
fn pipe_identifier_generic_sock_none() {
let f = OpenFile {
fd: FdName::Number(3),
access: Access::ReadWrite,
file_type: FileType::Sock,
name: "socket".to_string(),
..Default::default()
};
assert!(pipe_identifier(&f).is_none());
}
#[test]
fn pipe_identifier_linux_pipe_incomplete_bracket_uses_fallback() {
let f = make_pipe(3, "pipe:[123");
let r = pipe_identifier(&f);
assert!(r.is_some());
let (kind, id) = r.unwrap();
assert_eq!(kind, "pipe");
assert_eq!(id, "pipe:[123");
}
#[test]
fn pipe_identifier_linux_unix_socket_incomplete_bracket_returns_none() {
let f = make_unix(3, "socket:[99");
assert!(
pipe_identifier(&f).is_none(),
"incomplete socket:[ without closing ] has no extracted inode"
);
}
#[test]
fn pipe_identifier_unix_path_without_socket_pattern_returns_none() {
let f = make_unix(3, "/var/run/docker.sock");
assert!(pipe_identifier(&f).is_none());
}
#[test]
fn pipe_identifier_pipe_fallback_name() {
let f = make_pipe(3, "some-pipe-name");
let result = pipe_identifier(&f);
assert!(result.is_some());
let (kind, id) = result.unwrap();
assert_eq!(kind, "pipe");
assert_eq!(id, "some-pipe-name");
}
#[test]
fn print_pipe_chain_empty_no_panic() {
let theme = Theme::new(false);
print_pipe_chain(&[], &theme, false);
}
#[test]
fn print_pipe_chain_connected_processes() {
let theme = Theme::new(false);
let procs = vec![
make_proc(100, "writer", vec![make_pipe(1, "->0xabc123")]),
make_proc(200, "reader", vec![make_pipe(0, "->0xabc123")]),
];
print_pipe_chain(&procs, &theme, false);
}
#[test]
fn print_pipe_chain_json_no_panic() {
let theme = Theme::new(false);
let procs = vec![
make_proc(100, "writer", vec![make_pipe(1, "->0xabc123")]),
make_proc(200, "reader", vec![make_pipe(0, "->0xabc123")]),
];
print_pipe_chain(&procs, &theme, true);
}
#[test]
fn print_pipe_chain_single_endpoint_filtered() {
let theme = Theme::new(false);
let procs = vec![make_proc(100, "solo", vec![make_pipe(1, "->0xunique")])];
print_pipe_chain(&procs, &theme, false);
}
#[test]
fn print_pipe_chain_unix_sockets() {
let theme = Theme::new(false);
let procs = vec![
make_proc(100, "server", vec![make_unix(3, "socket:[99999]")]),
make_proc(200, "client", vec![make_unix(4, "socket:[99999]")]),
];
print_pipe_chain(&procs, &theme, false);
}
}