#!/usr/bin/env python3 """Static file server with HTTP Range support, for the OpenRA3 wasm build. Emscripten's lazy MEMFS reads assets back as byte ranges, so the server must honour the ``Range`` header -- Python's stdlib ``http.server`` does not. python3 apps/web/serve.py --root build/wasm/bin --port 8199 # then open http://localhost:8199/openra3.html Serve ``--root`` containing ``openra3.html/.js/.wasm``, ``assets.manifest.json`` and the extracted ``assets/`` tree (a directory or a junction/symlink to it). """ import argparse import functools import os import re import sys from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer _RANGE = re.compile(r"bytes=(\d*)-(\d*)$") class RangeHandler(SimpleHTTPRequestHandler): # SharedArrayBuffer (needed by the pthreads build) requires cross-origin # isolation. def end_headers(self): self.send_header("Cross-Origin-Opener-Policy", "same-origin") self.send_header("Cross-Origin-Embedder-Policy", "require-corp") super().end_headers() def send_head(self): header = self.headers.get("Range") if not header: return super().send_head() path = self.translate_path(self.path) if os.path.isdir(path): return super().send_head() try: handle = open(path, "rb") except OSError: self.send_error(404, "File not found") return None match = _RANGE.match(header.strip()) if not match: handle.close() return super().send_head() size = os.fstat(handle.fileno()).st_size start = int(match.group(1)) if match.group(1) else 0 end = int(match.group(2)) if match.group(2) else size - 1 if start > end or start >= size: handle.close() self.send_error(416, "Requested Range Not Satisfiable") return None end = min(end, size - 1) self.send_response(206) self.send_header("Content-Type", self.guess_type(path)) self.send_header("Accept-Ranges", "bytes") self.send_header("Content-Range", f"bytes {start}-{end}/{size}") self.send_header("Content-Length", str(end - start + 1)) self.send_header("Cache-Control", "no-store") self.end_headers() handle.seek(start) self._remaining = end - start + 1 return handle def copyfile(self, source, outputfile): remaining = getattr(self, "_remaining", None) if remaining is None: return super().copyfile(source, outputfile) try: while remaining > 0: chunk = source.read(min(65536, remaining)) if not chunk: break outputfile.write(chunk) remaining -= len(chunk) finally: self._remaining = None def main() -> int: parser = argparse.ArgumentParser(description="OpenRA3 wasm dev server") parser.add_argument("--root", default="build/wasm/bin", help="directory to serve") parser.add_argument("--port", type=int, default=8199) parser.add_argument("--bind", default="127.0.0.1") args = parser.parse_args() if not os.path.isdir(args.root): print(f"root not found: {args.root}", file=sys.stderr) return 1 handler = functools.partial(RangeHandler, directory=args.root) server = ThreadingHTTPServer((args.bind, args.port), handler) print(f"serving {args.root} at http://{args.bind}:{args.port}/openra3.html") try: server.serve_forever() except KeyboardInterrupt: pass return 0 if __name__ == "__main__": raise SystemExit(main())