#!/usr/bin/env python3 """Build, serve and rebuild when sources change: python3 tools/serve.py [--port 8000].""" import argparse, errno, http.server, pathlib, subprocess, sys, threading, time ROOT = pathlib.Path(__file__).resolve().parent.parent DIST = ROOT / 'dist' class Handler(http.server.SimpleHTTPRequestHandler): def __init__(self, *args, **kwargs): super().__init__(*args, directory=str(DIST), **kwargs) def end_headers(self): self.send_header('Cache-Control', 'no-store') super().end_headers() def log_message(self, format, *args): pass def build(first=False): result = subprocess.run([sys.executable, str(ROOT / 'build.py')], cwd=ROOT, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) if result.returncode: print('build failed:\n' + result.stdout, flush=True) elif first: print(result.stdout, end='', flush=True) else: print(time.strftime('rebuilt %H:%M:%S'), flush=True) def mtimes(): files = [ROOT / 'build.py', *(ROOT / 'src').rglob('*')] stamps = {} for path in files: try: if path.is_file(): stamps[path] = path.stat().st_mtime_ns except FileNotFoundError: pass return stamps def watch(stopped, previous): while not stopped.wait(0.5): current = mtimes() if current != previous: previous = current build() def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--port', type=int, default=8000) args = parser.parse_args() if not 1 <= args.port <= 65525: parser.error('--port must be between 1 and 65525 (leaves room for ten retries)') previous = mtimes() build(first=True) if not (DIST / 'index.html').is_file(): sys.exit('Cannot serve: dist/index.html does not exist. Fix the build and try again.') for port in range(args.port, args.port + 11): try: server = http.server.ThreadingHTTPServer(('127.0.0.1', port), Handler) break except OSError as error: if error.errno != errno.EADDRINUSE: sys.exit(f'Cannot serve on port {port}: {error}') else: sys.exit(f'Cannot serve: ports {args.port}-{args.port + 10} are all busy.') if port != args.port: print(f'Port {args.port} was busy; using {port}.', flush=True) print(f'Serving dist/ at http://localhost:{port} (Ctrl+C to stop)', flush=True) stopped = threading.Event() threading.Thread(target=watch, args=(stopped, previous), daemon=True).start() try: with server: server.serve_forever() finally: stopped.set() if __name__ == '__main__': try: main() except KeyboardInterrupt: print('\nServer stopped.', flush=True)