"""Start/stop the loopback-only development server without modifying saved config.""" import argparse import json import os from pathlib import Path import shutil import socket import subprocess import sys import time from datetime import datetime from urllib.request import build_opener, ProxyHandler import psutil ROOT = Path(__file__).resolve().parents[1] PYTHON = ROOT / '.venv' / 'Scripts' / 'python.exe' MANAGE = ROOT / 'manage.py' RUNTIME = ROOT / '.runtime' PID_FILE = RUNTIME / 'local-server.json' URL = 'http://127.0.0.1:10001/login' def local_environment(): import imageio_ffmpeg (RUNTIME / 'ultralytics').mkdir(parents=True, exist_ok=True) ffmpeg = Path(imageio_ffmpeg.__file__).parent / 'binaries' executables = list(ffmpeg.glob('ffmpeg*.exe')) if len(executables) != 1 or not executables[0].resolve().is_relative_to(ROOT / '.venv'): raise RuntimeError('Expected one FFmpeg executable bundled inside the project virtualenv') env = os.environ.copy() env.update({ 'DJANGO_SETTINGS_MODULE': 'framework.settings', 'MONITOR_DEBUG': 'true', 'MONITOR_ALLOWED_HOSTS': '127.0.0.1,localhost', 'MONITOR_HTTPS': 'false', 'MONITOR_SERVICE_MODE': 'disabled', 'MONITOR_BOOTSTRAP_SERVICES': 'false', 'MONITOR_TELEMETRY_ENDPOINT': '', 'MONITOR_UPDATE_ENDPOINT': '', 'MONITOR_FFMPEG': str(executables[0].resolve()), 'PYTHONUNBUFFERED': '1', 'YOLO_CONFIG_DIR': str(RUNTIME / 'ultralytics'), 'YOLO_OFFLINE': 'true', 'YOLO_AUTOINSTALL': 'false', # OpenVINO 2026 initializes import telemetry with disable_in_ci=True. # Use its upstream unattended-validation opt-out in this child only. 'CI': 'true', }) return env def backup(): target = RUNTIME / 'backups' / datetime.now().strftime('%Y%m%d-%H%M%S-%f') target.mkdir(parents=True) for name in ('monitor.sqlite3', '.runtime-secrets.json', 'config.json', 'settings.json'): source = ROOT / name if source.is_file(): shutil.copy2(source, target / name) print('Backup:', target) return target def is_project_process(proc): try: args = proc.cmdline() return (len(args) >= 5 and Path(args[0]).resolve() == PYTHON.resolve() and Path(args[1]).resolve() == MANAGE.resolve() and args[2:] == ['runserver', '127.0.0.1:10001', '--noreload']) except (psutil.Error, OSError): return False def recorded_process(): if not PID_FILE.exists(): return None data = json.loads(PID_FILE.read_text(encoding='utf-8')) if Path(data['root']).resolve() != ROOT: raise RuntimeError('Recorded project path mismatch; refusing to manage this process') try: proc = psutil.Process(data['pid']) if abs(proc.create_time() - data['create_time']) > 0.01: return None if not is_project_process(proc): raise RuntimeError('PID ownership mismatch; refusing to manage this process') return proc except psutil.NoSuchProcess: return None def port_open(): with socket.socket() as sock: sock.settimeout(1) return sock.connect_ex(('127.0.0.1', 10001)) == 0 def save_process(proc): PID_FILE.write_text(json.dumps({'pid': proc.pid, 'create_time': proc.create_time(), 'root': str(ROOT)}, indent=2), encoding='utf-8') def discover_project_listener(): """Recover ownership if the PID file was lost, including the Windows venv launcher.""" for conn in psutil.net_connections(kind='tcp'): if (conn.status != psutil.CONN_LISTEN or not conn.pid or conn.laddr.ip != '127.0.0.1' or conn.laddr.port != 10001): continue try: listener = psutil.Process(conn.pid) candidates = [listener, *listener.parents()] for candidate in reversed(candidates): if is_project_process(candidate): return candidate except psutil.Error: continue return None def start(): RUNTIME.mkdir(exist_ok=True) proc = recorded_process() if proc and port_open(): print('Already running:', URL, 'PID:', proc.pid) return if port_open() and not proc: owner = discover_project_listener() if owner: save_process(owner) print('Reused project server:', URL, 'PID:', owner.pid) return if port_open() or proc: raise RuntimeError('Port 10001 is occupied or recorded server is still starting; no process was stopped') backup() with open(RUNTIME / 'local-server.stdout.log', 'ab') as out, open(RUNTIME / 'local-server.stderr.log', 'ab') as err: child = subprocess.Popen( [str(PYTHON), str(MANAGE), 'runserver', '127.0.0.1:10001', '--noreload'], cwd=str(ROOT), env=local_environment(), stdin=subprocess.DEVNULL, stdout=out, stderr=err, creationflags=subprocess.CREATE_NO_WINDOW, ) info = psutil.Process(child.pid) save_process(info) opener = build_opener(ProxyHandler({})) for _ in range(60): if child.poll() is not None: raise RuntimeError('Server exited; inspect .runtime/local-server.stderr.log') try: with opener.open(URL, timeout=2) as response: if response.status == 200: print('Ready:', URL, 'PID:', child.pid) return except OSError: pass time.sleep(1) raise RuntimeError('Server readiness timeout; process left running for diagnosis') def stop(): proc = recorded_process() if not proc: print('No recorded project server is running') return children = proc.children(recursive=True) proc.terminate() for child in children: try: child.terminate() except psutil.NoSuchProcess: pass _, remaining = psutil.wait_procs([proc] + children, timeout=10) if remaining: raise RuntimeError('Some owned processes did not stop; inspect before retrying') PID_FILE.unlink(missing_ok=True) print('Stopped project server') if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add_argument('action', choices=('start', 'stop', 'backup')) args = parser.parse_args() {'start': start, 'stop': stop, 'backup': backup}[args.action]()