feat: add idempotent tunnel connection with state polling and already-running detection

Add tunnel_service_is_active check before Windows tunnel installation to skip if already running. Add is_already_running_error helper to detect "already installed and running" message in WireGuard output. Add wait_for_windows_tunnel_running that polls tunnel state up to 12 times with 500ms intervals after installation. Add describe_windows_tunnel_state for detailed error messages when tunnel fails to reach RUNNING state.
This commit is contained in:
2026-03-18 07:53:38 +01:00
parent 610c5459e5
commit bbea4f8bd0
2 changed files with 82 additions and 0 deletions

View File

@@ -29,6 +29,11 @@ function formatInvokeError(err: unknown, fallback: string) {
return fallback; return fallback;
} }
function isAlreadyRunningError(err: unknown) {
const message = formatInvokeError(err, "");
return message.toLowerCase().includes("already installed and running");
}
function currentProfileLabel(state: EnrollmentState | null) { function currentProfileLabel(state: EnrollmentState | null) {
if (!state) { if (!state) {
return "Not provisioned"; return "Not provisioned";
@@ -189,6 +194,14 @@ export function App() {
setError(null); setError(null);
} }
} catch (err) { } catch (err) {
if (!connected && isAlreadyRunningError(err)) {
const active = await waitForTunnelStatus(true);
setConnected(active);
if (active) {
setError(null);
return;
}
}
setError(formatInvokeError(err, "Tunnel action failed")); setError(formatInvokeError(err, "Tunnel action failed"));
} }
} }

View File

@@ -508,6 +508,10 @@ fn is_windows_admin() -> Result<bool, String> {
fn connect_direct(profile: &Path) -> Result<(), String> { fn connect_direct(profile: &Path) -> Result<(), String> {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
{ {
if tunnel_service_is_active(profile)? {
return Ok(());
}
let wireguard = find_windows_wireguard()?; let wireguard = find_windows_wireguard()?;
let output = Command::new(wireguard) let output = Command::new(wireguard)
.arg("/installtunnelservice") .arg("/installtunnelservice")
@@ -515,9 +519,13 @@ fn connect_direct(profile: &Path) -> Result<(), String> {
.creation_flags(CREATE_NO_WINDOW) .creation_flags(CREATE_NO_WINDOW)
.output() .output()
.map_err(|err| format!("unable to launch WireGuard runtime: {err}"))?; .map_err(|err| format!("unable to launch WireGuard runtime: {err}"))?;
if is_already_running_error(&output) {
return Ok(());
}
if !output.status.success() { if !output.status.success() {
return Err(format_windows_runtime_error("connect", &output)); return Err(format_windows_runtime_error("connect", &output));
} }
wait_for_windows_tunnel_running(profile)?;
return Ok(()); return Ok(());
} }
@@ -634,6 +642,59 @@ fn tunnel_service_is_active(profile: &Path) -> Result<bool, String> {
Err("unsupported platform".into()) Err("unsupported platform".into())
} }
#[cfg(target_os = "windows")]
fn wait_for_windows_tunnel_running(profile: &Path) -> Result<(), String> {
for _ in 0..12 {
if tunnel_service_is_active(profile)? {
return Ok(());
}
std::thread::sleep(Duration::from_millis(500));
}
Err(describe_windows_tunnel_state(profile))
}
#[cfg(target_os = "windows")]
fn describe_windows_tunnel_state(profile: &Path) -> String {
let tunnel_name = match tunnel_name(profile) {
Ok(value) => value,
Err(err) => return err,
};
let service_name = format!("WireGuardTunnel${}", tunnel_name);
let output = Command::new("sc")
.arg("query")
.arg(&service_name)
.creation_flags(CREATE_NO_WINDOW)
.output();
match output {
Ok(output) if output.status.success() => {
let stdout = String::from_utf8_lossy(&output.stdout);
let summary = stdout
.lines()
.find(|line| line.contains("STATE"))
.map(str::trim)
.unwrap_or("state unavailable");
format!("WireGuard tunnel service did not reach RUNNING state yet ({summary}).")
}
Ok(output) => {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
let details = if !stderr.trim().is_empty() {
stderr.trim()
} else {
stdout.trim()
};
if details.is_empty() {
"WireGuard tunnel service was created but could not be queried.".into()
} else {
format!("WireGuard tunnel service could not be queried: {details}")
}
}
Err(err) => format!("WireGuard tunnel service did not reach RUNNING state: {err}"),
}
}
fn tunnel_name(profile: &Path) -> Result<&str, String> { fn tunnel_name(profile: &Path) -> Result<&str, String> {
profile profile
.file_stem() .file_stem()
@@ -720,3 +781,11 @@ fn format_windows_runtime_error(action: &str, output: &Output) -> String {
format!("WireGuard runtime {} failed: {}", action, details) format!("WireGuard runtime {} failed: {}", action, details)
} }
#[cfg(target_os = "windows")]
fn is_already_running_error(output: &Output) -> bool {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let combined = format!("{}\n{}", stdout, stderr).to_ascii_lowercase();
combined.contains("already installed and running")
}