From 7c1ea0a1e1bb54165cf0a25f9d62c9c5fff9ea3e Mon Sep 17 00:00:00 2001 From: James Pine Date: Mon, 16 Mar 2026 02:15:26 -0700 Subject: [PATCH] fix: replace netstat with TcpStream + PowerShell for port detection (#277) On Windows, Voicebox shelled out to netstat.exe on startup to check for existing server processes. On systems with corrupted DLLs, netstat fails with 0xc0000142, causing an infinite loading loop. Replace with: - TcpStream::connect_timeout() for port-in-use checks (pure Rust) - PowerShell Get-NetTCPConnection for port-to-PID lookup (built-in cmdlet) - tasklist for process name verification (unchanged) Closes #277 --- docs/overview/troubleshooting.mdx | 2 +- tauri/src-tauri/src/main.rs | 120 +++++++++++++++--------------- 2 files changed, 62 insertions(+), 60 deletions(-) diff --git a/docs/overview/troubleshooting.mdx b/docs/overview/troubleshooting.mdx index f57c29f2..e2b86ef0 100644 --- a/docs/overview/troubleshooting.mdx +++ b/docs/overview/troubleshooting.mdx @@ -48,7 +48,7 @@ Windows SmartScreen may warn that the app is unrecognized. lsof -i :17493 # Windows - netstat -ano | findstr :17493 + powershell -Command "Get-NetTCPConnection -LocalPort 17493 -State Listen" ``` Kill the process using the port: diff --git a/tauri/src-tauri/src/main.rs b/tauri/src-tauri/src/main.rs index f9aa6e36..209b4d90 100644 --- a/tauri/src-tauri/src/main.rs +++ b/tauri/src-tauri/src/main.rs @@ -12,6 +12,46 @@ use tokio::sync::mpsc; const LEGACY_PORT: u16 = 8000; const SERVER_PORT: u16 = 17493; +/// Find a voicebox-server process listening on a given port (Windows only). +/// +/// Uses `TcpListener::bind` to confirm the port is occupied, then falls back +/// to `tasklist` to scan for a voicebox process. Returns the PID if found. +/// This replaces the previous `netstat -ano` approach which could fail on +/// systems with corrupted system DLLs (see #277). +#[cfg(windows)] +fn find_voicebox_pid_on_port(port: u16) -> Option { + use std::process::Command; + + // Use PowerShell's Get-NetTCPConnection to find the PID listening on the port. + // This is a built-in cmdlet that doesn't depend on netstat.exe. + let ps_script = format!( + "Get-NetTCPConnection -LocalPort {} -State Listen -ErrorAction SilentlyContinue | Select-Object -ExpandProperty OwningProcess", + port + ); + if let Ok(output) = Command::new("powershell") + .args(["-NoProfile", "-Command", &ps_script]) + .output() + { + let output_str = String::from_utf8_lossy(&output.stdout); + for line in output_str.lines() { + if let Ok(pid) = line.trim().parse::() { + // Verify this PID is a voicebox process + if let Ok(tasklist_output) = Command::new("tasklist") + .args(["/FI", &format!("PID eq {}", pid), "/FO", "CSV", "/NH"]) + .output() + { + let tasklist_str = String::from_utf8_lossy(&tasklist_output.stdout); + if tasklist_str.to_lowercase().contains("voicebox") { + return Some(pid); + } + } + } + } + } + + None +} + struct ServerState { child: Mutex>, server_pid: Mutex>, @@ -68,31 +108,16 @@ async fn start_server( #[cfg(windows)] { - use std::process::Command; - if let Ok(output) = Command::new("netstat") - .args(["-ano"]) - .output() - { - let output_str = String::from_utf8_lossy(&output.stdout); - for line in output_str.lines() { - if line.contains(&format!(":{}", SERVER_PORT)) && line.contains("LISTENING") { - if let Some(pid_str) = line.split_whitespace().last() { - if let Ok(pid) = pid_str.parse::() { - if let Ok(tasklist_output) = Command::new("tasklist") - .args(["/FI", &format!("PID eq {}", pid), "/FO", "CSV", "/NH"]) - .output() - { - let tasklist_str = String::from_utf8_lossy(&tasklist_output.stdout); - if tasklist_str.to_lowercase().contains("voicebox") { - println!("Found existing voicebox-server on port {} (PID: {}), reusing it", SERVER_PORT, pid); - // Store the PID so we can kill it on exit if needed - *state.server_pid.lock().unwrap() = Some(pid); - return Ok(format!("http://127.0.0.1:{}", SERVER_PORT)); - } - } - } - } - } + use std::net::TcpStream; + if TcpStream::connect_timeout( + &format!("127.0.0.1:{}", SERVER_PORT).parse().unwrap(), + std::time::Duration::from_secs(1), + ).is_ok() { + // Port is in use — check if it's a voicebox process via tasklist + if let Some(pid) = find_voicebox_pid_on_port(SERVER_PORT) { + println!("Found existing voicebox-server on port {} (PID: {}), reusing it", SERVER_PORT, pid); + *state.server_pid.lock().unwrap() = Some(pid); + return Ok(format!("http://127.0.0.1:{}", SERVER_PORT)); } } } @@ -102,24 +127,20 @@ async fn start_server( #[cfg(unix)] { use std::process::Command; - // Find processes listening on legacy port 8000 with their command names if let Ok(output) = Command::new("lsof") .args(["-i", &format!(":{}", LEGACY_PORT), "-sTCP:LISTEN"]) .output() { let output_str = String::from_utf8_lossy(&output.stdout); - for line in output_str.lines().skip(1) { // Skip header line - // lsof output format: COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME + for line in output_str.lines().skip(1) { let parts: Vec<&str> = line.split_whitespace().collect(); if parts.len() >= 2 { let command = parts[0]; let pid_str = parts[1]; - // Only kill if it's a voicebox-server process if command.contains("voicebox") { if let Ok(pid) = pid_str.parse::() { println!("Found orphaned voicebox-server on legacy port {} (PID: {}, CMD: {}), killing it...", LEGACY_PORT, pid, command); - // Kill the process group let _ = Command::new("kill") .args(["-9", "--", &format!("-{}", pid)]) .output(); @@ -137,35 +158,16 @@ async fn start_server( #[cfg(windows)] { - use std::process::Command; - // On Windows, find PIDs on legacy port 8000, then check their names - if let Ok(output) = Command::new("netstat") - .args(["-ano"]) - .output() - { - let output_str = String::from_utf8_lossy(&output.stdout); - for line in output_str.lines() { - if line.contains(&format!(":{}", LEGACY_PORT)) && line.contains("LISTENING") { - if let Some(pid_str) = line.split_whitespace().last() { - if let Ok(pid) = pid_str.parse::() { - // Get process name for this PID - if let Ok(tasklist_output) = Command::new("tasklist") - .args(["/FI", &format!("PID eq {}", pid), "/FO", "CSV", "/NH"]) - .output() - { - let tasklist_str = String::from_utf8_lossy(&tasklist_output.stdout); - if tasklist_str.to_lowercase().contains("voicebox") { - println!("Found orphaned voicebox-server on legacy port {} (PID: {}), killing it...", LEGACY_PORT, pid); - let _ = Command::new("taskkill") - .args(["/PID", &pid.to_string(), "/T", "/F"]) - .output(); - } else { - println!("Legacy port {} is in use by non-voicebox process (PID: {}), not killing", LEGACY_PORT, pid); - } - } - } - } - } + use std::net::TcpStream; + if TcpStream::connect_timeout( + &format!("127.0.0.1:{}", LEGACY_PORT).parse().unwrap(), + std::time::Duration::from_secs(1), + ).is_ok() { + if let Some(pid) = find_voicebox_pid_on_port(LEGACY_PORT) { + println!("Found orphaned voicebox-server on legacy port {} (PID: {}), killing it...", LEGACY_PORT, pid); + let _ = std::process::Command::new("taskkill") + .args(["/PID", &pid.to_string(), "/T", "/F"]) + .output(); } } }