Files
Robot_Arm/arm/viewer/serve.py
T

133 lines
4.9 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""查看器的本地静态服务器(前端拆分后,多文件 + ES Module + Worker 都需要 http 才能跑)。
python -m arm.viewer.serve [--port 8123] [--print-only]
要点:
- docroot = 仓库根,`/` 直接给 webapp/index.html;/docs/*.json 这类数据同域可读。
- Cache-Control: no-store —— 重新生成后旧标签页仍显示旧版是这里踩过的坑,
过去只能人肉 Ctrl+F5;不缓存之后刷新即新构建。
- 显式 MIME:Windows 上 .js 常被注册成 text/plain,那会让 <script type=module>
静默不执行(页面全黑且没有明显报错),所以这里写死。
- 屏蔽 .git / backups / 定位图 / out 等目录,不把整个仓库暴露到端口上。
"""
import argparse
import os
import sys
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from arm.paths import REPO
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
DENY = (".git", "backups", ".bak", "定位图", "out", "scratch", "node_modules")
MIME = {
".js": "text/javascript; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".svg": "image/svg+xml",
".css": "text/css; charset=utf-8",
".html": "text/html; charset=utf-8",
".npz": "application/octet-stream",
".png": "image/png",
".wasm": "application/wasm",
}
class H(SimpleHTTPRequestHandler):
# HTTP/1.1 + keep-alive + 大缓冲:默认 HTTP/1.0 每请求一条连接,
# 4 个 headless Edge 同时各拉 14MB 数据脚本时会把页面加载拖到截图之后。
protocol_version = "HTTP/1.1"
wbufsize = 1 << 20
# 按 (路径, mtime) 缓存字节:同一份 14MB 数据在一次批量截图里要发几十次。
_CACHE = {}
def __init__(self, *a, **k):
super().__init__(*a, directory=REPO, **k)
def _cached_bytes(self, path):
try:
st = os.stat(path)
except OSError:
return None
key = (path, st.st_mtime_ns, st.st_size)
body = H._CACHE.get(key)
if body is None:
with open(path, "rb") as f:
body = f.read()
if st.st_size <= 64 << 20:
if len(H._CACHE) > 12:
H._CACHE.clear()
H._CACHE[key] = body
return body
def send_head(self):
rel = self.path.split("?", 1)[0].split("#", 1)[0].lstrip("/").replace("\\", "/")
top = rel.split("/")[0].lower()
if top in [d.lower() for d in DENY] or ".." in rel:
self.send_error(404, "denied")
return None
fs = self.translate_path(self.path)
if os.path.isfile(fs):
body = self._cached_bytes(fs)
if body is not None:
self.send_response(200)
self.send_header("Content-Type", self.guess_type(fs))
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
return None
return super().send_head()
def guess_type(self, path):
ext = os.path.splitext(path)[1].lower()
return MIME.get(ext) or super().guess_type(path)
def do_GET(self):
"""`/` 必须 302 到 /webapp/index.html,不能在原地把内容发出去:
那样文档基址是 `/`,页面里所有相对路径(styles/… js/… vendor/…)都会
解析到 /styles/base.css 之类而 404 —— 实测整页样式与脚本全丢。"""
if self.path.split("?", 1)[0].rstrip("/") in ("", "/"):
q = self.path.split("?", 1)[1] if "?" in self.path else ""
self.send_response(302)
self.send_header("Location", "/webapp/index.html" + ("?" + q if q else ""))
self.send_header("Content-Length", "0")
self.end_headers()
return
super().do_GET()
def end_headers(self):
self.send_header("Cache-Control", "no-store")
super().end_headers()
def log_message(self, fmt, *args):
sys.stderr.write("%s %s\n" % (self.address_string(), fmt % args))
def serve(port=8123):
srv = None
for p in range(port, port + 20):
try:
srv = ThreadingHTTPServer(("127.0.0.1", p), H)
port = p
break
except OSError:
continue
if srv is None:
raise SystemExit("端口 %d..%d 都被占用" % (port, port + 19))
url = "http://127.0.0.1:%d/" % port
print("docroot %s" % REPO, flush=True)
print("查看器: %s (Ctrl+C 停止;已禁用缓存,改完刷新即新构建)" % url, flush=True)
srv.serve_forever()
if __name__ == "__main__":
ap = argparse.ArgumentParser(description="查看器本地静态服务器")
ap.add_argument("--port", type=int, default=8123)
serve(ap.parse_args().port)