diff --git a/utils/wifi/scanner.py b/utils/wifi/scanner.py index 94588e7..d9e6032 100644 --- a/utils/wifi/scanner.py +++ b/utils/wifi/scanner.py @@ -301,6 +301,73 @@ class UnifiedWiFiScanner: return False + def _ensure_interface_up(self, interface: str) -> bool: + """ + Ensure a WiFi interface is up before scanning. + + Attempts to bring the interface up using 'ip link set up', + falling back to 'ifconfig up'. + + Args: + interface: Network interface name. + + Returns: + True if the interface was brought up (or was already up), + False if we failed to bring it up. + """ + # Check current state via /sys/class/net + operstate_path = f"/sys/class/net/{interface}/operstate" + try: + with open(operstate_path) as f: + state = f.read().strip() + if state == "up": + return True + logger.info(f"Interface {interface} is '{state}', attempting to bring up") + except FileNotFoundError: + # Interface might not exist or /sys not available (non-Linux) + return True + except Exception: + pass + + # Try ip link set up + if shutil.which('ip'): + try: + result = subprocess.run( + ['ip', 'link', 'set', interface, 'up'], + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0: + logger.info(f"Brought interface {interface} up via ip link") + time.sleep(1) # Brief settle time + return True + else: + logger.warning(f"ip link set {interface} up failed: {result.stderr.strip()}") + except Exception as e: + logger.warning(f"Failed to run ip link: {e}") + + # Fallback to ifconfig + if shutil.which('ifconfig'): + try: + result = subprocess.run( + ['ifconfig', interface, 'up'], + capture_output=True, + text=True, + timeout=5, + ) + if result.returncode == 0: + logger.info(f"Brought interface {interface} up via ifconfig") + time.sleep(1) + return True + else: + logger.warning(f"ifconfig {interface} up failed: {result.stderr.strip()}") + except Exception as e: + logger.warning(f"Failed to run ifconfig: {e}") + + logger.error(f"Could not bring interface {interface} up") + return False + # ========================================================================= # Quick Scan # ========================================================================= @@ -362,6 +429,9 @@ class UnifiedWiFiScanner: result.is_complete = True return result else: # Linux - try tools in order with fallback + # Ensure interface is up before scanning + self._ensure_interface_up(iface) + tools_to_try = [] if self._capabilities.has_nmcli: tools_to_try.append(('nmcli', self._scan_with_nmcli)) @@ -375,6 +445,7 @@ class UnifiedWiFiScanner: result.is_complete = True return result + interface_was_down = False for tool_name, scan_func in tools_to_try: try: logger.info(f"Attempting quick scan with {tool_name} on {iface}") @@ -386,8 +457,28 @@ class UnifiedWiFiScanner: error_msg = f"{tool_name}: {str(e)}" errors_encountered.append(error_msg) logger.warning(f"Quick scan with {tool_name} failed: {e}") + if 'is down' in str(e): + interface_was_down = True continue # Try next tool + # If all tools failed because interface was down, try bringing it up and retry + if not tool_used and interface_was_down: + logger.info(f"Interface {iface} appears down, attempting to bring up and retry scan") + if self._ensure_interface_up(iface): + errors_encountered.clear() + for tool_name, scan_func in tools_to_try: + try: + logger.info(f"Retrying scan with {tool_name} on {iface} after bringing interface up") + observations = scan_func(iface, timeout) + tool_used = tool_name + logger.info(f"Retry scan with {tool_name} found {len(observations)} networks") + break + except Exception as e: + error_msg = f"{tool_name}: {str(e)}" + errors_encountered.append(error_msg) + logger.warning(f"Retry scan with {tool_name} failed: {e}") + continue + if not tool_used: # All tools failed result.error = "All scan tools failed. " + "; ".join(errors_encountered)