Skip to main content

proc_cli/core/
target.rs

1//! Target resolution - Convert user input to processes
2//!
3//! Targets can be:
4//! - `:port` - Process listening on this port
5//! - `pid` - Process with this PID (numeric)
6//! - `name` - Processes matching this name
7
8use crate::core::port::{parse_port, PortInfo};
9use crate::core::Process;
10use crate::error::{ProcError, Result};
11
12/// Resolved target type
13#[derive(Debug, Clone)]
14pub enum TargetType {
15    /// Target a process by the port it listens on (e.g., `:3000`)
16    Port(u16),
17    /// Target a process by its process ID (e.g., `1234`)
18    Pid(u32),
19    /// Target processes by name pattern (e.g., `node`)
20    Name(String),
21}
22
23/// Parse a target string and determine its type
24pub fn parse_target(target: &str) -> TargetType {
25    let target = target.trim();
26
27    // Explicit port prefix
28    if target.starts_with(':') {
29        if let Ok(port) = parse_port(target) {
30            return TargetType::Port(port);
31        }
32    }
33
34    // Pure number - treat as PID
35    if let Ok(pid) = target.parse::<u32>() {
36        return TargetType::Pid(pid);
37    }
38
39    // Otherwise it's a name
40    TargetType::Name(target.to_string())
41}
42
43/// Resolve a target to processes
44pub fn resolve_target(target: &str) -> Result<Vec<Process>> {
45    match parse_target(target) {
46        TargetType::Port(port) => resolve_port(port),
47        TargetType::Pid(pid) => resolve_pid(pid),
48        TargetType::Name(name) => Process::find_by_name(&name),
49    }
50}
51
52/// Resolve a single target to exactly one process
53pub fn resolve_target_single(target: &str) -> Result<Process> {
54    let processes = resolve_target(target)?;
55
56    if processes.is_empty() {
57        return Err(ProcError::ProcessNotFound(target.to_string()));
58    }
59
60    if processes.len() > 1 {
61        return Err(ProcError::InvalidInput(format!(
62            "Target '{}' matches {} processes. Be more specific.",
63            target,
64            processes.len()
65        )));
66    }
67
68    Ok(processes.into_iter().next().unwrap())
69}
70
71/// Resolve port to process
72fn resolve_port(port: u16) -> Result<Vec<Process>> {
73    match PortInfo::find_by_port(port)? {
74        Some(port_info) => match Process::find_by_pid(port_info.pid)? {
75            Some(proc) => Ok(vec![proc]),
76            None => Err(ProcError::ProcessGone(port_info.pid)),
77        },
78        None => Err(ProcError::PortNotFound(port)),
79    }
80}
81
82/// Resolve PID to process
83fn resolve_pid(pid: u32) -> Result<Vec<Process>> {
84    match Process::find_by_pid(pid)? {
85        Some(proc) => Ok(vec![proc]),
86        None => Err(ProcError::ProcessNotFound(pid.to_string())),
87    }
88}
89
90/// Find all ports a process is listening on
91pub fn find_ports_for_pid(pid: u32) -> Result<Vec<PortInfo>> {
92    let all_ports = PortInfo::get_all_listening()?;
93    Ok(all_ports.into_iter().filter(|p| p.pid == pid).collect())
94}
95
96/// Split comma-separated targets into individual target strings
97///
98/// Examples:
99///   ":3000,:8080" -> [":3000", ":8080"]
100///   "node,python" -> ["node", "python"]
101///   ":3000, 1234, node" -> [":3000", "1234", "node"]
102pub fn parse_targets(targets_str: &str) -> Vec<String> {
103    targets_str
104        .split(',')
105        .map(|s| s.trim().to_string())
106        .filter(|s| !s.is_empty())
107        .collect()
108}
109
110/// Resolve multiple targets, deduplicating by PID
111///
112/// Returns a tuple of (found processes, not found target strings)
113pub fn resolve_targets(targets: &[String]) -> (Vec<Process>, Vec<String>) {
114    use std::collections::HashSet;
115
116    let mut all_processes = Vec::new();
117    let mut seen_pids = HashSet::new();
118    let mut not_found = Vec::new();
119
120    for target in targets {
121        match resolve_target(target) {
122            Ok(processes) => {
123                for proc in processes {
124                    if seen_pids.insert(proc.pid) {
125                        all_processes.push(proc);
126                    }
127                }
128            }
129            Err(_) => not_found.push(target.clone()),
130        }
131    }
132
133    (all_processes, not_found)
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn test_parse_targets_single() {
142        assert_eq!(parse_targets(":3000"), vec![":3000"]);
143        assert_eq!(parse_targets("node"), vec!["node"]);
144        assert_eq!(parse_targets("1234"), vec!["1234"]);
145    }
146
147    #[test]
148    fn test_parse_targets_multiple() {
149        assert_eq!(parse_targets(":3000,:8080"), vec![":3000", ":8080"]);
150        assert_eq!(parse_targets("node,python"), vec!["node", "python"]);
151        assert_eq!(
152            parse_targets(":3000,1234,node"),
153            vec![":3000", "1234", "node"]
154        );
155    }
156
157    #[test]
158    fn test_parse_targets_with_whitespace() {
159        assert_eq!(
160            parse_targets(":3000, :8080, :9000"),
161            vec![":3000", ":8080", ":9000"]
162        );
163        assert_eq!(parse_targets(" node , python "), vec!["node", "python"]);
164    }
165
166    #[test]
167    fn test_parse_targets_empty_entries() {
168        assert_eq!(parse_targets(":3000,,,:8080"), vec![":3000", ":8080"]);
169        assert_eq!(parse_targets(",,node,,"), vec!["node"]);
170    }
171
172    #[test]
173    fn test_parse_target_port() {
174        assert!(matches!(parse_target(":3000"), TargetType::Port(3000)));
175        assert!(matches!(parse_target(":8080"), TargetType::Port(8080)));
176    }
177
178    #[test]
179    fn test_parse_target_pid() {
180        assert!(matches!(parse_target("1234"), TargetType::Pid(1234)));
181        assert!(matches!(parse_target("99999"), TargetType::Pid(99999)));
182    }
183
184    #[test]
185    fn test_parse_target_name() {
186        assert!(matches!(parse_target("node"), TargetType::Name(_)));
187        assert!(matches!(parse_target("my-process"), TargetType::Name(_)));
188    }
189}