Improve Wi-Fi recovery and reconnect handling

This commit is contained in:
2026-04-25 15:00:45 -04:00
parent 18cc9b6bab
commit 94952c7136
4 changed files with 99 additions and 12 deletions

View File

@@ -13,7 +13,10 @@ from .windows import (
ensure_hardware_mac, ensure_hardware_mac,
get_wifi_status, get_wifi_status,
randomize_mac, randomize_mac,
request_wifi_scan,
restart_adapter, restart_adapter,
set_interface_enabled,
set_wlan_autoconfig,
test_connectivity, test_connectivity,
wait_for_ssid, wait_for_ssid,
) )
@@ -149,9 +152,24 @@ class WifiBackgroundService:
self.state.last_mac_refresh_monotonic = time.monotonic() self.state.last_mac_refresh_monotonic = time.monotonic()
self.logger.info("Randomized MAC to %s", mac_address) self.logger.info("Randomized MAC to %s", mac_address)
self._prepare_interface_for_connect(interface_name)
try:
disconnect_wifi(interface_name) disconnect_wifi(interface_name)
except WifiCommandError as exc:
self.logger.warning("Wi-Fi disconnect before recovery failed, continuing: %s", exc)
try:
self.logger.info("Restarting Wi-Fi adapter %s", interface_name)
restart_adapter(interface_name) restart_adapter(interface_name)
except WifiCommandError as exc:
self.logger.warning(
"Adapter restart failed, continuing with direct reconnect attempt: %s",
exc,
)
time.sleep(self.config.monitor.reconnect_wait_seconds) time.sleep(self.config.monitor.reconnect_wait_seconds)
self.logger.info("Reconnecting to %s after recovery actions", network.ssid)
connect_wifi( connect_wifi(
network.profile_name, network.profile_name,
interface_name, interface_name,
@@ -166,7 +184,9 @@ class WifiBackgroundService:
self.state.last_recovery_monotonic = time.monotonic() self.state.last_recovery_monotonic = time.monotonic()
if not status.is_connected or status.ssid != network.ssid: if not status.is_connected or status.ssid != network.ssid:
raise WifiCommandError(f"Failed to reconnect to {network.ssid}") raise WifiCommandError(
f"Failed to reconnect to {network.ssid}; final status: {self._format_status(status)}",
)
self.logger.info("Reconnected to %s on interface %s", status.ssid, interface_name) self.logger.info("Reconnected to %s on interface %s", status.ssid, interface_name)
@@ -186,13 +206,14 @@ class WifiBackgroundService:
network.ssid, network.ssid,
interface_name, interface_name,
) )
self.state.last_connect_attempt_monotonic = time.monotonic()
self._prepare_interface_for_connect(interface_name)
connect_wifi( connect_wifi(
network.profile_name, network.profile_name,
interface_name, interface_name,
ssid=network.ssid, ssid=network.ssid,
auto_create_open_profile=network.auto_create_open_profile, auto_create_open_profile=network.auto_create_open_profile,
) )
self.state.last_connect_attempt_monotonic = time.monotonic()
new_status = wait_for_ssid( new_status = wait_for_ssid(
network.ssid, network.ssid,
@@ -200,7 +221,11 @@ class WifiBackgroundService:
interface_name, interface_name,
) )
if not new_status.is_connected or new_status.ssid != network.ssid: if not new_status.is_connected or new_status.ssid != network.ssid:
self.logger.warning("Direct Wi-Fi connection to %s did not succeed", network.ssid) self.logger.warning(
"Direct Wi-Fi connection to %s did not succeed; final status: %s",
network.ssid,
self._format_status(new_status),
)
return False return False
self.logger.info("Connected to %s without adapter reset", network.ssid) self.logger.info("Connected to %s without adapter reset", network.ssid)
@@ -249,3 +274,18 @@ class WifiBackgroundService:
return ( return (
now - self.state.last_connect_attempt_monotonic now - self.state.last_connect_attempt_monotonic
) >= self.config.monitor.connect_retry_cooldown_seconds ) >= self.config.monitor.connect_retry_cooldown_seconds
def _prepare_interface_for_connect(self, interface_name: str) -> None:
for action_name, action in (
("enable interface", lambda: set_interface_enabled(interface_name, True)),
("enable WLAN autoconfig", lambda: set_wlan_autoconfig(interface_name, True)),
("scan Wi-Fi networks", lambda: request_wifi_scan(interface_name)),
):
try:
action()
except WifiCommandError as exc:
self.logger.warning("Could not %s for %s: %s", action_name, interface_name, exc)
@staticmethod
def _format_status(status) -> str:
return f"interface={status.interface_name} state={status.state} ssid={status.ssid}"

View File

@@ -92,6 +92,23 @@ def restart_adapter(interface_name: str) -> None:
run_command(["netsh", "interface", "set", "interface", interface_name, "enable"]) run_command(["netsh", "interface", "set", "interface", interface_name, "enable"])
def set_interface_enabled(interface_name: str, enabled: bool = True) -> None:
state = "enable" if enabled else "disable"
run_command(["netsh", "interface", "set", "interface", interface_name, state])
def set_wlan_autoconfig(interface_name: str, enabled: bool = True) -> None:
state = "yes" if enabled else "no"
run_command(["netsh", "wlan", "set", "autoconfig", f"enabled={state}", f"interface={interface_name}"])
def request_wifi_scan(interface_name: str | None = None) -> None:
command = ["netsh", "wlan", "show", "networks"]
if interface_name:
command.append(f"interface={interface_name}")
run_command(command)
def disconnect_wifi(interface_name: str | None = None) -> None: def disconnect_wifi(interface_name: str | None = None) -> None:
command = ["netsh", "wlan", "disconnect"] command = ["netsh", "wlan", "disconnect"]
if interface_name: if interface_name:
@@ -106,13 +123,9 @@ def connect_wifi(
ssid: str | None = None, ssid: str | None = None,
auto_create_open_profile: bool = False, auto_create_open_profile: bool = False,
) -> None: ) -> None:
command = ["netsh", "wlan", "connect", f"name={profile_name}"] commands = _build_connect_commands(profile_name, interface_name, ssid)
if ssid:
command.append(f"ssid={ssid}")
if interface_name:
command.append(f"interface={interface_name}")
try: try:
run_command(command) _run_first_successful_command(commands)
except WifiCommandError as exc: except WifiCommandError as exc:
if auto_create_open_profile and _is_missing_profile_error(str(exc), profile_name): if auto_create_open_profile and _is_missing_profile_error(str(exc), profile_name):
create_open_wifi_profile( create_open_wifi_profile(
@@ -120,7 +133,7 @@ def connect_wifi(
ssid=ssid or profile_name, ssid=ssid or profile_name,
interface_name=interface_name, interface_name=interface_name,
) )
run_command(command) _run_first_successful_command(commands)
return return
if _is_missing_profile_error(str(exc), profile_name): if _is_missing_profile_error(str(exc), profile_name):
available_profiles = list_wifi_profiles(interface_name) available_profiles = list_wifi_profiles(interface_name)
@@ -133,6 +146,40 @@ def connect_wifi(
raise raise
def _build_connect_commands(
profile_name: str,
interface_name: str | None,
ssid: str | None,
) -> list[list[str]]:
variants: list[list[str]] = []
def add_variant(include_ssid: bool, include_interface: bool) -> None:
command = ["netsh", "wlan", "connect", f"name={profile_name}"]
if include_ssid and ssid:
command.append(f"ssid={ssid}")
if include_interface and interface_name:
command.append(f"interface={interface_name}")
if command not in variants:
variants.append(command)
add_variant(include_ssid=True, include_interface=True)
add_variant(include_ssid=False, include_interface=True)
add_variant(include_ssid=True, include_interface=False)
add_variant(include_ssid=False, include_interface=False)
return variants
def _run_first_successful_command(commands: list[list[str]]) -> None:
errors: list[str] = []
for command in commands:
try:
run_command(command)
return
except WifiCommandError as exc:
errors.append(f"{' '.join(command)} -> {exc}")
raise WifiCommandError("; ".join(errors))
def list_wifi_profiles(interface_name: str | None = None) -> list[str]: def list_wifi_profiles(interface_name: str | None = None) -> list[str]:
command = ["netsh", "wlan", "show", "profiles"] command = ["netsh", "wlan", "show", "profiles"]
if interface_name: if interface_name:

BIN
winsw/WinSW.NET461.exe Normal file

Binary file not shown.