host.py (5.3 KB)
1 #!/usr/bin/env python3 2 """ 3 Starts HTTP server for local testing 4 Automatically manages a virtual environment for dependencies 5 """ 6 7 import http.server 8 import socketserver 9 import socket 10 import sys 11 import os 12 import subprocess 13 from pathlib import Path 14 15 DEFAULT_PORT = 8000 16 SCRIPT_DIR = Path(__file__).parent.absolute() 17 VENV_DIR = SCRIPT_DIR / "venv" 18 19 20 def setup_venv(): 21 """Create and setup virtual environment if it doesn't exist""" 22 # Determine the path to pip and python in the venv 23 if sys.platform == "win32": 24 pip_path = VENV_DIR / "Scripts" / "pip" 25 python_path = VENV_DIR / "Scripts" / "python" 26 else: 27 pip_path = VENV_DIR / "bin" / "pip" 28 python_path = VENV_DIR / "bin" / "python3" 29 30 # Check if venv needs to be created or recreated 31 if not VENV_DIR.exists() or not python_path.exists(): 32 if VENV_DIR.exists(): 33 print("Virtual environment incomplete, recreating...") 34 import shutil 35 shutil.rmtree(VENV_DIR) 36 else: 37 print("Creating virtual environment...") 38 39 try: 40 subprocess.check_call([sys.executable, "-m", "venv", str(VENV_DIR)]) 41 print("Virtual environment created successfully.") 42 except subprocess.CalledProcessError as e: 43 print(f"Error creating virtual environment: {e}") 44 sys.exit(1) 45 46 # Ensure pip is available (sometimes venv doesn't include it) 47 if not pip_path.exists(): 48 print("Installing pip in virtual environment...") 49 try: 50 subprocess.check_call([str(python_path), "-m", "ensurepip", "--upgrade"]) 51 except subprocess.CalledProcessError as e: 52 print(f"Error ensuring pip: {e}") 53 sys.exit(1) 54 55 check = subprocess.run( 56 [str(python_path), "-c", "import qrcode"], 57 capture_output=True 58 ) 59 if check.returncode != 0: 60 try: 61 subprocess.check_call([str(python_path), "-m", "pip", "install", "-q", "qrcode"]) 62 except subprocess.CalledProcessError: 63 print("Note: Could not install qrcode (offline?). QR codes will be unavailable.\n") 64 65 return python_path 66 67 68 def run_in_venv(): 69 """Re-run this script in the virtual environment""" 70 python_path = setup_venv() 71 72 # Re-run this script with the venv Python 73 try: 74 subprocess.check_call([str(python_path), __file__, "--in-venv"]) 75 except (KeyboardInterrupt, subprocess.CalledProcessError): 76 pass 77 sys.exit(0) 78 79 80 def get_local_ip(): 81 """Get the local IP address for network access""" 82 try: 83 # Create a socket to determine the local IP 84 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 85 # Connect to a public DNS server (doesn't actually send data) 86 s.connect(("8.8.8.8", 80)) 87 local_ip = s.getsockname()[0] 88 s.close() 89 return local_ip 90 except Exception: 91 return "Unable to determine" 92 93 94 def find_available_port(start_port=DEFAULT_PORT, max_attempts=10): 95 """Find an available port starting from start_port""" 96 for port in range(start_port, start_port + max_attempts): 97 try: 98 with socketserver.TCPServer(("", port), None) as s: 99 return port 100 except OSError: 101 continue 102 return None 103 104 105 def print_qr_code(url): 106 """Generate and print a QR code using block characters""" 107 try: 108 import qrcode 109 110 qr = qrcode.QRCode( 111 version=1, 112 error_correction=qrcode.constants.ERROR_CORRECT_L, 113 box_size=1, 114 border=1, 115 ) 116 qr.add_data(url) 117 qr.make(fit=True) 118 119 # Get the QR code matrix 120 matrix = qr.get_matrix() 121 122 # Print QR code using block characters 123 # Use full block (█) for black and space for white 124 print("\nScan to connect:") 125 for row in matrix: 126 line = "" 127 for cell in row: 128 line += "██" if cell else " " 129 print(line) 130 print() 131 except ImportError: 132 print("\nQR code generation unavailable (qrcode library not installed)") 133 except Exception as e: 134 print(f"\nCould not generate QR code: {e}") 135 136 def start_server(): 137 """Start the HTTP server (runs after venv is set up)""" 138 # Change to script directory 139 os.chdir(SCRIPT_DIR) 140 141 # Find an available port 142 port = find_available_port(DEFAULT_PORT) 143 144 if port is None: 145 print(f"Error: Could not find an available port (tried {DEFAULT_PORT}-{DEFAULT_PORT + 9})") 146 sys.exit(1) 147 148 # Get local IP for network access 149 local_ip = get_local_ip() 150 151 # Create server 152 Handler = http.server.SimpleHTTPRequestHandler 153 154 # Suppress default logging and broken pipe errors 155 class QuietHandler(Handler): 156 def end_headers(self): 157 self.send_header('Cache-Control', 'no-cache') 158 super().end_headers() 159 160 def log_message(self, format, *args): 161 pass 162 163 def handle(self): 164 """Handle requests and suppress broken pipe errors""" 165 try: 166 super().handle() 167 except (BrokenPipeError, ConnectionResetError): 168 # Browser cancelled the request (normal for media streaming/preloading) 169 pass 170 171 try: 172 with socketserver.TCPServer(("", port), QuietHandler) as httpd: 173 local_url = f"http://localhost:{port}" 174 network_url = f"http://{local_ip}:{port}" 175 176 print("=" * 60) 177 print("💿 mixapps") 178 print("=" * 60) 179 print(f"\nServer running on port {port}") 180 181 # Print QR code for easy mobile access 182 print_qr_code(network_url) 183 184 print(f"Local access: {local_url}") 185 print(f"Network access: {network_url}") 186 print("\nPress Ctrl+C to stop the server") 187 188 # Serve forever 189 httpd.serve_forever() 190 191 except KeyboardInterrupt: 192 print("\n\nShutting down server...") 193 sys.exit(0) 194 except Exception as e: 195 print(f"\nError starting server: {e}") 196 sys.exit(1) 197 198 199 def main(): 200 """Main entry point""" 201 # Check if we're already running in venv 202 if "--in-venv" not in sys.argv: 203 run_in_venv() 204 else: 205 start_server() 206 207 208 if __name__ == "__main__": 209 main()