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:
@@ -29,6 +29,11 @@ function formatInvokeError(err: unknown, fallback: string) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function isAlreadyRunningError(err: unknown) {
|
||||
const message = formatInvokeError(err, "");
|
||||
return message.toLowerCase().includes("already installed and running");
|
||||
}
|
||||
|
||||
function currentProfileLabel(state: EnrollmentState | null) {
|
||||
if (!state) {
|
||||
return "Not provisioned";
|
||||
@@ -189,6 +194,14 @@ export function App() {
|
||||
setError(null);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!connected && isAlreadyRunningError(err)) {
|
||||
const active = await waitForTunnelStatus(true);
|
||||
setConnected(active);
|
||||
if (active) {
|
||||
setError(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setError(formatInvokeError(err, "Tunnel action failed"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,6 +508,10 @@ fn is_windows_admin() -> Result<bool, String> {
|
||||
fn connect_direct(profile: &Path) -> Result<(), String> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if tunnel_service_is_active(profile)? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let wireguard = find_windows_wireguard()?;
|
||||
let output = Command::new(wireguard)
|
||||
.arg("/installtunnelservice")
|
||||
@@ -515,9 +519,13 @@ fn connect_direct(profile: &Path) -> Result<(), String> {
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output()
|
||||
.map_err(|err| format!("unable to launch WireGuard runtime: {err}"))?;
|
||||
if is_already_running_error(&output) {
|
||||
return Ok(());
|
||||
}
|
||||
if !output.status.success() {
|
||||
return Err(format_windows_runtime_error("connect", &output));
|
||||
}
|
||||
wait_for_windows_tunnel_running(profile)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -634,6 +642,59 @@ fn tunnel_service_is_active(profile: &Path) -> Result<bool, String> {
|
||||
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> {
|
||||
profile
|
||||
.file_stem()
|
||||
@@ -720,3 +781,11 @@ fn format_windows_runtime_error(action: &str, output: &Output) -> String {
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user