#!/usr/bin/env python3 """ TraceWaves Local Scan Agent ============================ A lightweight HTTP server that runs WiFi scans using OS-native tools and serves results to the TraceWaves PWA at https://tracewaves.app. Zero external dependencies -- uses only Python 3.6+ stdlib. On macOS, uses CoreWLAN framework or falls back to system_profiler. Usage: python3 tracewaves-agent.py # Start on default port 7273 python3 tracewaves-agent.py --install # Install as background service python3 tracewaves-agent.py --uninstall # Remove background service python3 tracewaves-agent.py --status # Check install status The agent listens on 127.0.0.1 only (never exposed to the network). Install mode registers a per-user background service (launchd/systemd) that auto-starts on login -- no root or admin privileges required. """ import http.server import json import os import platform import re import shutil import subprocess import sys from pathlib import Path from urllib.parse import urlparse VERSION = "1.1.1" PORT = 7273 ALLOWED_ORIGINS = [ "https://tracewaves.app", "http://localhost:5201", "http://localhost:3000", ] # ── WiFi Scanning ───────────────────────────────────────────── def scan_wifi(): """Run OS-appropriate WiFi scan and return structured result dict.""" os_name = platform.system() if os_name == "Darwin": return scan_macos() elif os_name == "Linux": return scan_linux() elif os_name == "Windows": return scan_windows() else: return {"error": f"Unsupported OS: {os_name}"} def normalize_mac(raw): """Normalize MAC address to XX:XX:XX:XX:XX:XX format.""" cleaned = re.sub(r"[^0-9a-fA-F]", "", raw) if len(cleaned) != 12: return raw.upper() return ":".join(cleaned[i:i+2] for i in range(0, 12, 2)).upper() def channel_to_band(ch): if ch is None: return None if 1 <= ch <= 14: return "2.4ghz" if 32 <= ch <= 177: return "5ghz" if ch > 177: return "6ghz" return None def normalize_security(raw): # No security information from the OS is "unknown", never "open": the web # app leaves such networks ungraded instead of guessing. Callers that know # an empty value means an open network (nmcli terse output) pass "open". if not raw: return "unknown" s = raw.lower().strip() if "wpa3" in s and "enterprise" in s: return "wpa3-enterprise" if "wpa3" in s or "sae" in s: return "wpa3-personal" if "wpa2" in s and ("enterprise" in s or "802.1x" in s): return "wpa2-enterprise" if "wpa2" in s: return "wpa2-personal" if "wpa" in s: return "wpa" if "wep" in s: return "wep" if "owe" in s: return "owe" if "none" in s or "open" in s or s == "--": return "open" return "unknown" # ── macOS ───────────────────────────────────────────────────── def scan_macos(): warnings = [] # Try CoreWLAN via pyobjc (best method) try: aps, has_location = scan_macos_corewlan() if not has_location: warnings.append( "SSIDs and BSSIDs are hidden. Grant Location Services access to " "Terminal (or your Python app) in System Settings > Privacy & Security " "> Location Services for full network details." ) resp = {"accessPoints": aps, "count": len(aps), "locationAccess": has_location} if warnings: resp["warnings"] = warnings return resp except Exception as e: pass # Try legacy airport command (older macOS) try: airport = "/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport" result = subprocess.run( [airport, "-s"], capture_output=True, text=True, timeout=15 ) if result.returncode == 0: aps = parse_airport_output(result.stdout) if aps: return {"accessPoints": aps, "count": len(aps), "locationAccess": True} except (FileNotFoundError, subprocess.TimeoutExpired): pass # Fallback: system_profiler (always available, limited data) sp_result = scan_macos_system_profiler() if isinstance(sp_result, dict) and "error" in sp_result: return sp_result warnings.append( "Using system_profiler fallback — only current and cached networks shown. " "For full scanning: pip3 install pyobjc-framework-CoreWLAN" ) resp = {"accessPoints": sp_result, "count": len(sp_result), "locationAccess": True} if warnings: resp["warnings"] = warnings return resp def scan_macos_corewlan(): """Use CoreWLAN framework for a real WiFi scan. Returns (aps_list, has_location_access). """ import CoreWLAN iface = CoreWLAN.CWInterface.interface() if not iface: raise RuntimeError("No WiFi interface") networks, error = iface.scanForNetworksWithName_error_(None, None) if error: raise RuntimeError(str(error)) # CWSecurity enum values SEC_NONE = 0 SEC_WEP = 1 SEC_WPA_PERSONAL = 2 SEC_WPA_PERSONAL_MIXED = 3 SEC_WPA2_PERSONAL = 4 SEC_PERSONAL = 5 SEC_DYNAMIC_WEP = 6 SEC_WPA_ENTERPRISE = 7 SEC_WPA_ENTERPRISE_MIXED = 8 SEC_WPA2_ENTERPRISE = 9 SEC_ENTERPRISE = 10 SEC_WPA3_PERSONAL = 11 SEC_WPA3_ENTERPRISE = 12 SEC_WPA3_TRANSITION = 13 SEC_OWE = 14 SEC_OWE_TRANSITION = 15 aps = [] has_location = False placeholder_idx = 0 for net in networks: ssid_raw = net.ssid() ssid = str(ssid_raw) if ssid_raw else None bssid_raw = net.bssid() bssid = str(bssid_raw) if bssid_raw else None if ssid or bssid: has_location = True mac = normalize_mac(bssid) if bssid else None if not mac or len(mac) != 17: placeholder_idx += 1 mac = f"02:TW:00:00:{placeholder_idx >> 8 & 0xFF:02X}:{placeholder_idx & 0xFF:02X}" channel_obj = net.wlanChannel() channel = int(channel_obj.channelNumber()) if channel_obj else None # Detect security — check strongest to weakest security_raw = "" try: if net.supportsSecurity_(SEC_WPA3_ENTERPRISE): security_raw = "wpa3-enterprise" elif net.supportsSecurity_(SEC_WPA3_PERSONAL) or net.supportsSecurity_(SEC_WPA3_TRANSITION): security_raw = "wpa3-personal" elif net.supportsSecurity_(SEC_WPA2_ENTERPRISE) or net.supportsSecurity_(SEC_ENTERPRISE): security_raw = "wpa2-enterprise" elif net.supportsSecurity_(SEC_WPA2_PERSONAL) or net.supportsSecurity_(SEC_PERSONAL): security_raw = "wpa2-personal" elif net.supportsSecurity_(SEC_WPA_ENTERPRISE) or net.supportsSecurity_(SEC_WPA_ENTERPRISE_MIXED): security_raw = "wpa-enterprise" elif net.supportsSecurity_(SEC_WPA_PERSONAL) or net.supportsSecurity_(SEC_WPA_PERSONAL_MIXED): security_raw = "wpa" elif net.supportsSecurity_(SEC_OWE) or net.supportsSecurity_(SEC_OWE_TRANSITION): security_raw = "owe" elif net.supportsSecurity_(SEC_WEP) or net.supportsSecurity_(SEC_DYNAMIC_WEP): security_raw = "wep" elif net.supportsSecurity_(SEC_NONE): security_raw = "open" except Exception: security_raw = "" aps.append({ "ssid": ssid, "bssid": mac, "rssiDbm": int(net.rssiValue()), "channel": channel, "band": channel_to_band(channel), "securityMode": normalize_security(security_raw), "frequencyMhz": None, }) if not aps: raise RuntimeError("CoreWLAN scan returned no networks") return aps, has_location def scan_macos_system_profiler(): """Fallback using system_profiler — parses current + cached networks.""" try: result = subprocess.run( ["system_profiler", "SPAirPortDataType", "-json"], capture_output=True, text=True, timeout=20 ) except (FileNotFoundError, subprocess.TimeoutExpired): return {"error": "system_profiler not available."} if result.returncode != 0: return {"error": f"system_profiler failed: {result.stderr[:200]}"} try: data = json.loads(result.stdout) except json.JSONDecodeError: return {"error": "Could not parse system_profiler JSON output."} aps = [] placeholder_idx = 0 def parse_channel(ch_str): try: return int(str(ch_str).split()[0].split("(")[0].split(",")[0]) except (ValueError, IndexError, AttributeError): return None def parse_rssi(noise_str): try: return int(str(noise_str).split("/")[0].strip().replace(" dBm", "")) except (ValueError, IndexError, AttributeError): return None def make_placeholder_mac(): nonlocal placeholder_idx placeholder_idx += 1 return f"02:SP:{placeholder_idx >> 8 & 0xFF:02X}:{placeholder_idx & 0xFF:02X}:00:00" for item in data.get("SPAirPortDataType", []): interfaces = item.get("spairport_airport_interfaces", []) for iface in interfaces: current = iface.get("spairport_current_network_information", {}) if isinstance(current, dict) and current.get("_name"): name = current.get("_name", "") channel = parse_channel(current.get("spairport_network_channel", "")) rssi = parse_rssi(current.get("spairport_signal_noise", "")) security = current.get("spairport_security_mode", "") aps.append({ "ssid": name if name and name != "" else None, "bssid": make_placeholder_mac(), "rssiDbm": rssi, "channel": channel, "band": channel_to_band(channel), "securityMode": normalize_security(security), "frequencyMhz": None, }) other = iface.get("spairport_airport_other_local_wireless_networks", []) if isinstance(other, list): for net in other: name = net.get("_name", "") channel = parse_channel(net.get("spairport_network_channel", "")) rssi = parse_rssi(net.get("spairport_signal_noise", net.get("spairport_network_signal_noise", ""))) security = net.get("spairport_security_mode", "") aps.append({ "ssid": name if name and name != "" else None, "bssid": make_placeholder_mac(), "rssiDbm": rssi, "channel": channel, "band": channel_to_band(channel), "securityMode": normalize_security(security), "frequencyMhz": None, }) if not aps: return {"error": "No networks found. Install pyobjc-framework-CoreWLAN for full scanning: pip3 install pyobjc-framework-CoreWLAN"} return aps def parse_airport_output(stdout): """Parse output from the legacy airport -s command.""" lines = stdout.strip().split("\n") aps = [] header = None for line in lines: if re.match(r"\s*SSID\s+BSSID", line): header = line break if not header: return [] bssid_start = header.index("BSSID") rssi_start = header.index("RSSI") channel_start = header.index("CHANNEL") ht_start = header.index("HT") if "HT" in header else None sec_start = header.index("SECURITY") if "SECURITY" in header else None for line in lines: if "BSSID" in line and "RSSI" in line: continue if not line.strip(): continue bssid_raw = line[bssid_start:rssi_start].strip() mac = normalize_mac(bssid_raw) if len(mac) != 17: continue ssid = line[:bssid_start].strip() or None rssi = None try: rssi = int(line[rssi_start:channel_start].strip()) except (ValueError, IndexError): pass channel = None ch_end = ht_start if ht_start else (sec_start if sec_start else len(line)) try: ch_str = line[channel_start:ch_end].strip() channel = int(ch_str.split(",")[0].strip()) except (ValueError, IndexError): pass security = None if sec_start is not None: security = line[sec_start:].strip() aps.append({ "ssid": ssid, "bssid": mac, "rssiDbm": rssi, "channel": channel, "band": channel_to_band(channel), "securityMode": normalize_security(security), "frequencyMhz": None, }) return aps # ── Linux ───────────────────────────────────────────────────── def scan_linux(): try: result = subprocess.run( ["nmcli", "-t", "-f", "SSID,BSSID,SIGNAL,CHAN,FREQ,SECURITY", "dev", "wifi", "list", "--rescan", "yes"], capture_output=True, text=True, timeout=20 ) except FileNotFoundError: return {"error": "nmcli not found. Install NetworkManager: apt install network-manager"} except subprocess.TimeoutExpired: return {"error": "WiFi scan timed out after 20 seconds."} if result.returncode != 0: return {"error": f"nmcli failed (exit {result.returncode}): {result.stderr.strip()[:200]}"} aps = [] for line in result.stdout.strip().split("\n"): if not line.strip(): continue parts = re.split(r"(? 5 else None aps.append({ "ssid": ssid, "bssid": mac, "rssiDbm": signal, "channel": channel, "band": channel_to_band(channel), "securityMode": normalize_security(security), "frequencyMhz": freq, }) return {"accessPoints": aps, "count": len(aps), "locationAccess": True} # ── Windows ─────────────────────────────────────────────────── def scan_windows(): try: result = subprocess.run( ["netsh", "wlan", "show", "networks", "mode=bssid"], capture_output=True, text=True, timeout=15, creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0) ) except FileNotFoundError: return {"error": "netsh not found."} except subprocess.TimeoutExpired: return {"error": "WiFi scan timed out after 15 seconds."} if result.returncode != 0: return {"error": f"netsh failed (exit {result.returncode}): {result.stderr.strip()[:200]}"} aps = [] blocks = re.split(r"SSID\s+\d+\s*:", result.stdout) for block in blocks: if not block.strip(): continue def get_val(key): m = re.search(rf"{key}\s*:\s*(.+)", block, re.IGNORECASE) return m.group(1).strip() if m else None ssid = block.split("\n")[0].strip() or None bssid_raw = get_val(r"BSSID\s*\d*") if not bssid_raw: m = re.search(r"([0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2})", block, re.IGNORECASE) if not m: continue bssid_raw = m.group(1) mac = normalize_mac(bssid_raw) if len(mac) != 17: continue signal = None signal_str = get_val("Signal") if signal_str: try: pct = int(signal_str.replace("%", "").strip()) signal = int(-30 - (100 - pct) * 0.6) except ValueError: pass channel = None ch_str = get_val("Channel") if ch_str: try: channel = int(ch_str) except ValueError: pass auth = get_val("Authentication") or "" enc = get_val("Encryption") or "" security = f"{auth} {enc}".strip() aps.append({ "ssid": ssid, "bssid": mac, "rssiDbm": signal, "channel": channel, "band": channel_to_band(channel), "securityMode": normalize_security(security), "frequencyMhz": None, }) return {"accessPoints": aps, "count": len(aps), "locationAccess": True} # ── HTTP Server ─────────────────────────────────────────────── class AgentHandler(http.server.BaseHTTPRequestHandler): def log_message(self, format, *args): sys.stdout.write(f" {args[0]}\n") sys.stdout.flush() def send_cors_headers(self, origin): if origin in ALLOWED_ORIGINS: self.send_header("Access-Control-Allow-Origin", origin) self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS") self.send_header("Access-Control-Allow-Headers", "Content-Type") self.send_header("Cache-Control", "no-store") def send_json(self, data, status=200): body = json.dumps(data).encode() origin = self.headers.get("Origin", "") self.send_response(status) self.send_header("Content-Type", "application/json") self.send_cors_headers(origin) self.send_header("Content-Length", str(len(body))) self.end_headers() self.wfile.write(body) def do_OPTIONS(self): origin = self.headers.get("Origin", "") self.send_response(204) self.send_cors_headers(origin) self.end_headers() def do_GET(self): parsed = urlparse(self.path) path = parsed.path if path == "/status": self.send_json({ "agent": "tracewaves", "version": VERSION, "platform": platform.system(), "hostname": platform.node(), "installed": is_installed(), }) elif path == "/scan": result = scan_wifi() if isinstance(result, dict) and "error" in result: self.send_json(result, status=500) elif isinstance(result, dict): self.send_json(result) else: self.send_json({"accessPoints": result, "count": len(result), "locationAccess": True}) else: self.send_json({"error": "Not found"}, status=404) # == Service Install / Uninstall ========================================== INSTALL_DIR_NAME = ".tracewaves" AGENT_FILENAME = "tracewaves-agent.py" LAUNCHD_LABEL = "app.tracewaves.agent" SYSTEMD_UNIT = "tracewaves-agent.service" def get_install_dir(): """~/.tracewaves/ -- user-local, no elevated privileges.""" return Path.home() / INSTALL_DIR_NAME def get_agent_path(): return get_install_dir() / AGENT_FILENAME def is_installed(): """Check if the agent is installed as a background service.""" os_name = platform.system() if os_name == "Darwin": plist = Path.home() / "Library" / "LaunchAgents" / f"{LAUNCHD_LABEL}.plist" return plist.exists() and get_agent_path().exists() elif os_name == "Linux": unit = Path.home() / ".config" / "systemd" / "user" / SYSTEMD_UNIT return unit.exists() and get_agent_path().exists() elif os_name == "Windows": startup = Path(os.environ.get("APPDATA", "")) / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup" / "tracewaves-agent.vbs" return startup.exists() and get_agent_path().exists() return False def install_agent(): """Install the agent as a per-user background service.""" os_name = platform.system() script_src = Path(__file__).resolve() install_dir = get_install_dir() agent_dst = get_agent_path() # Create install directory (user-owned, restricted permissions) install_dir.mkdir(mode=0o700, exist_ok=True) # Copy script to install location shutil.copy2(str(script_src), str(agent_dst)) agent_dst.chmod(0o700) python = sys.executable or "python3" if os_name == "Darwin": _install_macos(python, agent_dst) elif os_name == "Linux": _install_linux(python, agent_dst) elif os_name == "Windows": _install_windows(python, agent_dst) else: print(f" x Unsupported platform: {os_name}") sys.exit(1) def _install_macos(python, agent_path): """Register a launchd user agent -- starts on login, restarts on crash.""" plist_dir = Path.home() / "Library" / "LaunchAgents" plist_dir.mkdir(parents=True, exist_ok=True) plist_path = plist_dir / f"{LAUNCHD_LABEL}.plist" log_dir = get_install_dir() / "logs" log_dir.mkdir(mode=0o700, exist_ok=True) # XML plist -- no shell interpretation, direct exec plist_content = f""" Label {LAUNCHD_LABEL} ProgramArguments {python} {agent_path} RunAtLoad KeepAlive SuccessfulExit ThrottleInterval 10 StandardOutPath {log_dir}/agent.log StandardErrorPath {log_dir}/agent.err ProcessType Background """ plist_path.write_text(plist_content) plist_path.chmod(0o600) # Unload first in case already installed (ignore errors) subprocess.run(["launchctl", "unload", str(plist_path)], capture_output=True) # Load the new agent result = subprocess.run( ["launchctl", "load", str(plist_path)], capture_output=True, text=True ) if result.returncode != 0: print(f" x launchctl load failed: {result.stderr.strip()}") sys.exit(1) print() print(" +-------------------------------------------+") print(" | Agent installed (macOS launchd) |") print(" +-------------------------------------------+") print(f" | Agent: {agent_path}") print(f" | Plist: {plist_path}") print(f" | Logs: {log_dir}/") print(" | |") print(" | Starts automatically on login. |") print(" | Running now on 127.0.0.1:7273 |") print(" | |") print(" | Uninstall: |") print(" | python3 tracewaves-agent.py --uninstall |") print(" +-------------------------------------------+") print() def _install_linux(python, agent_path): """Register a systemd user service.""" unit_dir = Path.home() / ".config" / "systemd" / "user" unit_dir.mkdir(parents=True, exist_ok=True) unit_path = unit_dir / SYSTEMD_UNIT unit_content = f"""[Unit] Description=TraceWaves Local Scan Agent After=network.target [Service] Type=simple ExecStart={python} {agent_path} Restart=on-failure RestartSec=10 [Install] WantedBy=default.target """ unit_path.write_text(unit_content) unit_path.chmod(0o600) subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True) subprocess.run(["systemctl", "--user", "enable", SYSTEMD_UNIT], capture_output=True) result = subprocess.run( ["systemctl", "--user", "start", SYSTEMD_UNIT], capture_output=True, text=True ) if result.returncode != 0: print(f" x systemctl start failed: {result.stderr.strip()}") sys.exit(1) # Enable lingering so service runs even when not in a desktop session subprocess.run(["loginctl", "enable-linger"], capture_output=True) print() print(" +-------------------------------------------+") print(" | Agent installed (systemd user) |") print(" +-------------------------------------------+") print(f" | Agent: {agent_path}") print(f" | Unit: {unit_path}") print(" | |") print(" | Starts automatically on login. |") print(" | Running now on 127.0.0.1:7273 |") print(" | |") print(" | Logs: journalctl --user -u tracewaves-agent") print(" | |") print(" | Uninstall: |") print(" | python3 tracewaves-agent.py --uninstall |") print(" +-------------------------------------------+") print() def _install_windows(python, agent_path): """Create a VBScript wrapper in Startup folder (runs hidden, no console).""" startup_dir = Path(os.environ.get("APPDATA", "")) / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup" vbs_path = startup_dir / "tracewaves-agent.vbs" # VBScript runs python hidden (no console window) py_escaped = str(python).replace('"', '""') agent_escaped = str(agent_path).replace('"', '""') vbs_content = 'Set ws = CreateObject("WScript.Shell")\n' vbs_content += f'ws.Run """{py_escaped}"" ""{agent_escaped}""", 0, False\n' vbs_path.write_text(vbs_content) # Start it now subprocess.Popen( [python, str(agent_path)], creationflags=getattr(subprocess, "CREATE_NO_WINDOW", 0), close_fds=True, ) print() print(" +-------------------------------------------+") print(" | Agent installed (Windows Startup) |") print(" +-------------------------------------------+") print(f" | Agent: {agent_path}") print(f" | Startup: {vbs_path}") print(" | |") print(" | Starts automatically on login. |") print(" | Running now on 127.0.0.1:7273 |") print(" | |") print(" | Uninstall: |") print(" | python3 tracewaves-agent.py --uninstall |") print(" +-------------------------------------------+") print() def uninstall_agent(): """Stop and remove the background service and installed files.""" os_name = platform.system() install_dir = get_install_dir() if os_name == "Darwin": plist_path = Path.home() / "Library" / "LaunchAgents" / f"{LAUNCHD_LABEL}.plist" if plist_path.exists(): subprocess.run(["launchctl", "unload", str(plist_path)], capture_output=True) plist_path.unlink() print(" Removed launchd agent") else: print(" No launchd agent found") elif os_name == "Linux": unit_path = Path.home() / ".config" / "systemd" / "user" / SYSTEMD_UNIT if unit_path.exists(): subprocess.run(["systemctl", "--user", "stop", SYSTEMD_UNIT], capture_output=True) subprocess.run(["systemctl", "--user", "disable", SYSTEMD_UNIT], capture_output=True) unit_path.unlink() subprocess.run(["systemctl", "--user", "daemon-reload"], capture_output=True) print(" Removed systemd user service") else: print(" No systemd service found") elif os_name == "Windows": vbs_path = Path(os.environ.get("APPDATA", "")) / "Microsoft" / "Windows" / "Start Menu" / "Programs" / "Startup" / "tracewaves-agent.vbs" if vbs_path.exists(): vbs_path.unlink() print(" Removed startup script") else: print(" No startup script found") # Remove installed files if install_dir.exists(): shutil.rmtree(str(install_dir)) print(f" Removed {install_dir}") else: print(f" {install_dir} not found") print() print(" Agent uninstalled. You can delete this script too:") print(f" rm {Path(__file__).resolve()}") print() def show_status(): """Print installation and running status.""" os_name = platform.system() installed = is_installed() agent_path = get_agent_path() # Check if running running = False try: import urllib.request req = urllib.request.Request(f"http://127.0.0.1:{PORT}/status") resp = urllib.request.urlopen(req, timeout=2) data = json.loads(resp.read()) running = data.get("agent") == "tracewaves" except Exception: pass print() print(" TraceWaves Agent Status") print(" -----------------------") print(f" Version: {VERSION}") print(f" Platform: {os_name}") print(f" Installed: {'Yes' if installed else 'No'}") print(f" Running: {'Yes (port %d)' % PORT if running else 'No'}") if installed: print(f" Agent at: {agent_path}") if os_name == "Darwin": print(f" Plist: ~/Library/LaunchAgents/{LAUNCHD_LABEL}.plist") elif os_name == "Linux": print(f" Unit: ~/.config/systemd/user/{SYSTEMD_UNIT}") print() # == Server + CLI ========================================================== def run_server(): """Start the HTTP server on localhost.""" server = http.server.HTTPServer(("127.0.0.1", PORT), AgentHandler) os_name = platform.system() print() print(" +-------------------------------------------+") print(" | TraceWaves Local Scan Agent |") print(" +-------------------------------------------+") print(f" | Listening: http://127.0.0.1:{PORT} |") print(f" | Platform: {os_name:<28s} |") print(f" | Version: {VERSION:<28s} |") print(" | Status: Ready |") print(" +-------------------------------------------+") print(" | Open https://tracewaves.app/discover |") print(" | and click Scan to capture networks. |") print(" | |") print(" | Press Ctrl+C to stop. |") print(" +-------------------------------------------+") print() try: server.serve_forever() except KeyboardInterrupt: print("\n Agent stopped.") server.server_close() def main(): args = sys.argv[1:] if "--install" in args: install_agent() elif "--uninstall" in args: uninstall_agent() elif "--status" in args: show_status() else: run_server() if __name__ == "__main__": main()