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
This commit is contained in:
James Pine
2026-03-16 02:15:26 -07:00
parent b3012ed10c
commit 7c1ea0a1e1
2 changed files with 62 additions and 60 deletions
+1 -1
View File
@@ -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:
+61 -59
View File
@@ -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<u32> {
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::<u32>() {
// 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<Option<tauri_plugin_shell::process::CommandChild>>,
server_pid: Mutex<Option<u32>>,
@@ -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::<u32>() {
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::<i32>() {
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::<u32>() {
// 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();
}
}
}