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    resolve_targets_impl(targets, false)
115}
116
117/// Resolve multiple targets, excluding the current process (self)
118///
119/// Use this for destructive commands (kill, stop) to avoid proc killing itself
120/// when the target pattern matches proc's own command line arguments.
121pub fn resolve_targets_excluding_self(targets: &[String]) -> (Vec<Process>, Vec<String>) {
122    resolve_targets_impl(targets, true)
123}
124
125fn resolve_targets_impl(targets: &[String], exclude_self: bool) -> (Vec<Process>, Vec<String>) {
126    use std::collections::HashSet;
127
128    let mut all_processes = Vec::new();
129    let mut seen_pids = HashSet::new();
130    let mut not_found = Vec::new();
131    let self_pid = std::process::id();
132
133    for target in targets {
134        match resolve_target(target) {
135            Ok(processes) => {
136                for proc in processes {
137                    // Skip self if requested (for destructive commands)
138                    if exclude_self && proc.pid == self_pid {
139                        continue;
140                    }
141                    if seen_pids.insert(proc.pid) {
142                        all_processes.push(proc);
143                    }
144                }
145            }
146            Err(_) => not_found.push(target.clone()),
147        }
148    }
149
150    (all_processes, not_found)
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn test_parse_targets_single() {
159        assert_eq!(parse_targets(":3000"), vec![":3000"]);
160        assert_eq!(parse_targets("node"), vec!["node"]);
161        assert_eq!(parse_targets("1234"), vec!["1234"]);
162    }
163
164    #[test]
165    fn test_parse_targets_multiple() {
166        assert_eq!(parse_targets(":3000,:8080"), vec![":3000", ":8080"]);
167        assert_eq!(parse_targets("node,python"), vec!["node", "python"]);
168        assert_eq!(
169            parse_targets(":3000,1234,node"),
170            vec![":3000", "1234", "node"]
171        );
172    }
173
174    #[test]
175    fn test_parse_targets_with_whitespace() {
176        assert_eq!(
177            parse_targets(":3000, :8080, :9000"),
178            vec![":3000", ":8080", ":9000"]
179        );
180        assert_eq!(parse_targets(" node , python "), vec!["node", "python"]);
181    }
182
183    #[test]
184    fn test_parse_targets_empty_entries() {
185        assert_eq!(parse_targets(":3000,,,:8080"), vec![":3000", ":8080"]);
186        assert_eq!(parse_targets(",,node,,"), vec!["node"]);
187    }
188
189    #[test]
190    fn test_parse_target_port() {
191        assert!(matches!(parse_target(":3000"), TargetType::Port(3000)));
192        assert!(matches!(parse_target(":8080"), TargetType::Port(8080)));
193    }
194
195    #[test]
196    fn test_parse_target_pid() {
197        assert!(matches!(parse_target("1234"), TargetType::Pid(1234)));
198        assert!(matches!(parse_target("99999"), TargetType::Pid(99999)));
199    }
200
201    #[test]
202    fn test_parse_target_name() {
203        assert!(matches!(parse_target("node"), TargetType::Name(_)));
204        assert!(matches!(parse_target("my-process"), TargetType::Name(_)));
205    }
206}