Skip to content

Commit 34e2de2

Browse files
polidogclaude
andcommitted
fix: Improve port detection fallback logic on Linux
- Fix check_port_unix_optimized to properly fallback to ss when lsof returns Ok(None) instead of immediately returning "port available" - Merge results from both lsof and ss in list commands to detect all ports (lsof may miss some processes due to permission restrictions) - Fix parse_ss_output to use the protocol parameter instead of incorrectly using the state column (LISTEN) as the protocol - Update test expectation for protocol field This fixes the issue where ports were not detected on Linux when lsof couldn't find them but ss could. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6d63cd1 commit 34e2de2

1 file changed

Lines changed: 85 additions & 19 deletions

File tree

src/port/mod.rs

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,47 @@ impl PortManager {
6767
}
6868

6969
async fn list_processes_unix(&self, protocol: &str) -> Result<Vec<ProcessInfo>> {
70-
// Try lsof first, fallback to ss, then netstat
71-
if let Ok(result) = self.try_lsof(protocol).await {
72-
return Ok(result);
70+
use std::collections::HashSet;
71+
72+
// Collect results from lsof
73+
let lsof_result = self.try_lsof(protocol).await;
74+
75+
// Collect results from ss
76+
let ss_result = self.try_ss(protocol).await;
77+
78+
// Merge results from both sources, preferring lsof data when available
79+
// but including ports only found by ss
80+
let mut merged_results: Vec<ProcessInfo> = Vec::new();
81+
let mut seen_ports: HashSet<(u16, String)> = HashSet::new();
82+
83+
// Add lsof results first (they tend to have more complete process info)
84+
if let Ok(lsof_processes) = lsof_result {
85+
for process in lsof_processes {
86+
let key = (process.port, process.protocol.clone());
87+
if !seen_ports.contains(&key) {
88+
seen_ports.insert(key);
89+
merged_results.push(process);
90+
}
91+
}
92+
}
93+
94+
// Add ss results for ports not found by lsof
95+
if let Ok(ss_processes) = ss_result {
96+
for process in ss_processes {
97+
let key = (process.port, process.protocol.clone());
98+
if !seen_ports.contains(&key) {
99+
seen_ports.insert(key);
100+
merged_results.push(process);
101+
}
102+
}
73103
}
74104

75-
if let Ok(result) = self.try_ss(protocol).await {
76-
return Ok(result);
105+
// If we found any processes, return them
106+
if !merged_results.is_empty() {
107+
return Ok(merged_results);
77108
}
78109

110+
// Final fallback: netstat
79111
self.try_netstat_unix(protocol).await
80112
}
81113

@@ -86,13 +118,14 @@ impl PortManager {
86118
protocol: &str,
87119
) -> Result<Option<ProcessInfo>> {
88120
// Try lsof for specific port first - much faster than scanning all ports
89-
if let Ok(result) = self.try_lsof_specific_port(port, protocol).await {
90-
return Ok(result);
121+
// Only return if we found a process, otherwise fall through to next method
122+
if let Ok(Some(result)) = self.try_lsof_specific_port(port, protocol).await {
123+
return Ok(Some(result));
91124
}
92125

93126
// Fallback to ss for specific port
94-
if let Ok(result) = self.try_ss_specific_port(port, protocol).await {
95-
return Ok(result);
127+
if let Ok(Some(result)) = self.try_ss_specific_port(port, protocol).await {
128+
return Ok(Some(result));
96129
}
97130

98131
// Final fallback: netstat for specific port
@@ -107,23 +140,55 @@ impl PortManager {
107140
where
108141
F: Fn(&str) + Send + Sync,
109142
{
143+
use std::collections::HashSet;
144+
110145
if let Some(ref cb) = callback {
111146
cb("Executing port scan with lsof...");
112147
}
113148

114-
// Try lsof first, fallback to ss, then netstat
115-
if let Ok(result) = self.try_lsof_with_callback(protocol, &callback).await {
116-
return Ok(result);
117-
}
149+
// Collect results from lsof
150+
let lsof_result = self.try_lsof_with_callback(protocol, &callback).await;
118151

119152
if let Some(ref cb) = callback {
120153
cb("Trying alternative method (ss)...");
121154
}
122155

123-
if let Ok(result) = self.try_ss(protocol).await {
124-
return Ok(result);
156+
// Collect results from ss
157+
let ss_result = self.try_ss(protocol).await;
158+
159+
// Merge results from both sources, preferring lsof data when available
160+
// but including ports only found by ss
161+
let mut merged_results: Vec<ProcessInfo> = Vec::new();
162+
let mut seen_ports: HashSet<(u16, String)> = HashSet::new();
163+
164+
// Add lsof results first (they tend to have more complete process info)
165+
if let Ok(lsof_processes) = lsof_result {
166+
for process in lsof_processes {
167+
let key = (process.port, process.protocol.clone());
168+
if !seen_ports.contains(&key) {
169+
seen_ports.insert(key);
170+
merged_results.push(process);
171+
}
172+
}
125173
}
126174

175+
// Add ss results for ports not found by lsof
176+
if let Ok(ss_processes) = ss_result {
177+
for process in ss_processes {
178+
let key = (process.port, process.protocol.clone());
179+
if !seen_ports.contains(&key) {
180+
seen_ports.insert(key);
181+
merged_results.push(process);
182+
}
183+
}
184+
}
185+
186+
// If we found any processes, return them
187+
if !merged_results.is_empty() {
188+
return Ok(merged_results);
189+
}
190+
191+
// Final fallback: netstat
127192
if let Some(ref cb) = callback {
128193
cb("Trying fallback method (netstat)...");
129194
}
@@ -753,7 +818,7 @@ impl PortManager {
753818
Ok(processes)
754819
}
755820

756-
async fn parse_ss_output(&self, output: &str, _protocol: &str) -> Result<Vec<ProcessInfo>> {
821+
async fn parse_ss_output(&self, output: &str, protocol: &str) -> Result<Vec<ProcessInfo>> {
757822
let mut processes = Vec::new();
758823

759824
for line in output.lines().skip(1) {
@@ -763,7 +828,8 @@ impl PortManager {
763828
continue;
764829
}
765830

766-
let protocol = parts[0].to_lowercase();
831+
// parts[0] is the state (LISTEN, ESTAB, etc.), not protocol
832+
// Use the protocol parameter from the caller instead
767833
let local_address = parts[3];
768834
let process_info = parts[5..].join(" ");
769835

@@ -831,7 +897,7 @@ impl PortManager {
831897
executable_path,
832898
working_directory,
833899
port,
834-
protocol,
900+
protocol: protocol.to_string(),
835901
address,
836902
inode: None, // Legacy implementation doesn't track inodes
837903
});
@@ -1521,7 +1587,7 @@ LISTEN 0 128 *:3000 *:* users:(("node",pid=1234,fd
15211587
assert_eq!(processes[0].port, 3000);
15221588
assert_eq!(processes[0].pid, 1234);
15231589
assert_eq!(processes[0].address, "*");
1524-
assert_eq!(processes[0].protocol, "listen");
1590+
assert_eq!(processes[0].protocol, "tcp");
15251591
}
15261592

15271593
#[tokio::test]

0 commit comments

Comments
 (0)