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()