#!/usr/bin/env python3 """ Cross-platform development server script. Starts pnpm CSS watcher and Django dev server, handling cleanup on exit. Works on both Windows and Unix systems. """ import atexit import shutil import signal import subprocess import sys import time def find_pnpm(): """ Find pnpm executable on the system. Returns the path to pnpm or None if not found. """ # Try to find pnpm using shutil.which # On Windows, this will find pnpm.cmd if it's in PATH pnpm_path = shutil.which("pnpm") if pnpm_path: return pnpm_path # On Windows, also try pnpm.cmd explicitly if sys.platform == "win32": pnpm_cmd = shutil.which("pnpm.cmd") if pnpm_cmd: return pnpm_cmd return None class DevServerManager: """Manages background processes for development server.""" def __init__(self): self.processes = [] self._cleanup_registered = False def start_process(self, command, description, shell=False): """Start a background process and return the process object.""" print(f"Starting {description}...") try: if shell: # Use shell=True for commands that need shell interpretation process = subprocess.Popen( command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, ) else: # Split command into list for subprocess process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, ) self.processes.append((process, description)) print(f"✓ Started {description} (PID: {process.pid})") return process except Exception as e: print(f"✗ Failed to start {description}: {e}", file=sys.stderr) self.cleanup() sys.exit(1) def cleanup(self): """Terminate all running processes.""" if not self.processes: return print("\nShutting down...") for process, description in self.processes: if process.poll() is None: # Process is still running try: # Try graceful termination first if sys.platform == "win32": process.terminate() else: process.send_signal(signal.SIGTERM) # Wait up to 5 seconds for graceful shutdown try: process.wait(timeout=5) except subprocess.TimeoutExpired: # Force kill if graceful shutdown failed if sys.platform == "win32": process.kill() else: process.send_signal(signal.SIGKILL) process.wait() print(f"✓ {description} stopped") except Exception as e: print(f"✗ Error stopping {description}: {e}", file=sys.stderr) self.processes.clear() def register_cleanup(self): """Register cleanup handlers for various exit scenarios.""" if self._cleanup_registered: return self._cleanup_registered = True # Register atexit handler (works on all platforms) atexit.register(self.cleanup) # Register signal handlers (Unix only) if sys.platform != "win32": signal.signal(signal.SIGINT, self._signal_handler) signal.signal(signal.SIGTERM, self._signal_handler) def _signal_handler(self, signum, frame): """Handle Unix signals.""" self.cleanup() sys.exit(0) def wait_for_process(self, process, description): """Wait for a process to finish and handle its output.""" try: # Stream output from the process for line in iter(process.stdout.readline, ""): if line: print(f"[{description}] {line.rstrip()}") process.wait() return process.returncode except KeyboardInterrupt: # Handle Ctrl+C self.cleanup() sys.exit(0) except Exception as e: print(f"Error waiting for {description}: {e}", file=sys.stderr) self.cleanup() sys.exit(1) def main(): """Main entry point.""" manager = DevServerManager() manager.register_cleanup() # Find pnpm executable pnpm_path = find_pnpm() if not pnpm_path: print("Error: pnpm not found in PATH.", file=sys.stderr) print("\nPlease install pnpm:", file=sys.stderr) print(" Windows: https://pnpm.io/installation#on-windows", file=sys.stderr) print(" Unix: https://pnpm.io/installation#on-posix-systems", file=sys.stderr) sys.exit(1) # Determine shell usage based on platform use_shell = sys.platform == "win32" # Start pnpm CSS watcher # Use the found pnpm path to ensure it works on Windows pnpm_command = f'"{pnpm_path}" run dev' if use_shell else [pnpm_path, "run", "dev"] manager.start_process( pnpm_command, "CSS watcher", shell=use_shell, ) # Give pnpm a moment to start time.sleep(1) # Start Django dev server django_process = manager.start_process( ["uv", "run", "python", "manage.py", "runserver"], "Django server", shell=False, ) print("\nDevelopment servers are running. Press Ctrl+C to stop.\n") try: # Wait for Django server (main process) # If Django exits, we should clean up everything return_code = manager.wait_for_process(django_process, "Django") # If Django exited unexpectedly, clean up and exit if return_code != 0: manager.cleanup() sys.exit(return_code) except KeyboardInterrupt: # Ctrl+C was pressed manager.cleanup() sys.exit(0) if __name__ == "__main__": main()