diff --git a/osu_wifi_login/service.py b/osu_wifi_login/service.py index accb749..f549680 100644 --- a/osu_wifi_login/service.py +++ b/osu_wifi_login/service.py @@ -13,7 +13,10 @@ from .windows import ( ensure_hardware_mac, get_wifi_status, randomize_mac, + request_wifi_scan, restart_adapter, + set_interface_enabled, + set_wlan_autoconfig, test_connectivity, wait_for_ssid, ) @@ -149,9 +152,24 @@ class WifiBackgroundService: self.state.last_mac_refresh_monotonic = time.monotonic() self.logger.info("Randomized MAC to %s", mac_address) - disconnect_wifi(interface_name) - restart_adapter(interface_name) + self._prepare_interface_for_connect(interface_name) + + try: + 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) + 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) + self.logger.info("Reconnecting to %s after recovery actions", network.ssid) connect_wifi( network.profile_name, interface_name, @@ -166,7 +184,9 @@ class WifiBackgroundService: self.state.last_recovery_monotonic = time.monotonic() 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) @@ -186,13 +206,14 @@ class WifiBackgroundService: network.ssid, interface_name, ) + self.state.last_connect_attempt_monotonic = time.monotonic() + self._prepare_interface_for_connect(interface_name) connect_wifi( network.profile_name, interface_name, ssid=network.ssid, auto_create_open_profile=network.auto_create_open_profile, ) - self.state.last_connect_attempt_monotonic = time.monotonic() new_status = wait_for_ssid( network.ssid, @@ -200,7 +221,11 @@ class WifiBackgroundService: interface_name, ) 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 self.logger.info("Connected to %s without adapter reset", network.ssid) @@ -249,3 +274,18 @@ class WifiBackgroundService: return ( now - self.state.last_connect_attempt_monotonic ) >= 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}" diff --git a/osu_wifi_login/windows.py b/osu_wifi_login/windows.py index 6912fe5..d545909 100644 --- a/osu_wifi_login/windows.py +++ b/osu_wifi_login/windows.py @@ -92,6 +92,23 @@ def restart_adapter(interface_name: str) -> None: 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: command = ["netsh", "wlan", "disconnect"] if interface_name: @@ -106,13 +123,9 @@ def connect_wifi( ssid: str | None = None, auto_create_open_profile: bool = False, ) -> None: - command = ["netsh", "wlan", "connect", f"name={profile_name}"] - if ssid: - command.append(f"ssid={ssid}") - if interface_name: - command.append(f"interface={interface_name}") + commands = _build_connect_commands(profile_name, interface_name, ssid) try: - run_command(command) + _run_first_successful_command(commands) except WifiCommandError as exc: if auto_create_open_profile and _is_missing_profile_error(str(exc), profile_name): create_open_wifi_profile( @@ -120,7 +133,7 @@ def connect_wifi( ssid=ssid or profile_name, interface_name=interface_name, ) - run_command(command) + _run_first_successful_command(commands) return if _is_missing_profile_error(str(exc), profile_name): available_profiles = list_wifi_profiles(interface_name) @@ -133,6 +146,40 @@ def connect_wifi( 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]: command = ["netsh", "wlan", "show", "profiles"] if interface_name: diff --git a/winsw/WinSW.NET461.exe b/winsw/WinSW.NET461.exe new file mode 100644 index 0000000..ece8691 Binary files /dev/null and b/winsw/WinSW.NET461.exe differ diff --git a/winsw/osu-wifi-login.xml b/winsw/osu-wifi-login-service.xml similarity index 100% rename from winsw/osu-wifi-login.xml rename to winsw/osu-wifi-login-service.xml