Files
os-project/webui_server.py

270 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
import json
import os
import subprocess
import sys
import threading
import time
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlparse
ROOT = Path(__file__).resolve().parent
STATE_DIR = ROOT / "data"
STATE_FILE = STATE_DIR / "state.json"
WEB_DIR = ROOT / "web"
BIN = ROOT / ("process_manager.exe" if os.name == "nt" else "process_manager")
class CProcessBridge:
"""
把浏览器请求转换成 C 程序命令,并读取 C 程序生成的 JSON 状态。
这个类相当于“桥接层”:
浏览器不会直接运行 C 函数,而是请求 Python API;
Python 把 API 翻译成命令行文本,写入 C 程序 stdin;
C 程序执行命令后写 state.json;
Python 再读取 state.json 返回给浏览器。
"""
def __init__(self):
self.process = None
self.lock = threading.Lock()
def start(self):
STATE_DIR.mkdir(exist_ok=True)
if self.process and self.process.poll() is None:
return
# C 程序以 --json 模式启动后,会把系统状态持续写入 data/state.json。
# previous_mtime 用来判断启动后是否真的产生了新的状态文件。
previous_mtime = STATE_FILE.stat().st_mtime_ns if STATE_FILE.exists() else 0
self.process = subprocess.Popen(
[str(BIN), "--json", str(STATE_FILE)],
cwd=str(ROOT),
stdin=subprocess.PIPE,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
text=True,
bufsize=1,
)
self._wait_for_state(previous_mtime)
def stop(self):
if self.process and self.process.poll() is None:
# 优先发送 exit,让 C 程序正常退出;超时后再 terminate。
if self.process.stdin is not None:
self.process.stdin.write("exit\n")
self.process.stdin.flush()
try:
self.process.wait(timeout=1)
except subprocess.TimeoutExpired:
self.process.terminate()
def reset(self):
with self.lock:
# 重置时销毁旧 C 进程和旧 JSON,再启动一个全新的模拟系统。
self.stop()
if STATE_FILE.exists():
STATE_FILE.unlink()
self.process = None
self.start()
def command(self, line):
with self.lock:
self.start()
if self.process.stdin is None:
raise RuntimeError("C process stdin is unavailable")
# 记录命令前的修改时间,随后等待 state.json 被 C 程序重新写入。
# lock 保证同一时刻只有一个浏览器请求在写 C 程序 stdin,避免命令交错。
previous_mtime = STATE_FILE.stat().st_mtime_ns if STATE_FILE.exists() else 0
self.process.stdin.write(line + "\n")
self.process.stdin.flush()
self._wait_for_state(previous_mtime)
def state(self):
with self.lock:
self.start()
self._wait_for_state()
# state.json 是 C 程序输出的完整系统快照,Python 只负责转发。
with STATE_FILE.open("r", encoding="utf-8") as file:
return json.load(file)
def _wait_for_state(self, previous_mtime=0):
# 简单轮询足够应对此项目:C 程序每次命令执行完都会写完整 JSON。
# 50 * 0.02s 最多等待约 1 秒,避免浏览器请求无限卡住。
for _ in range(50):
if (STATE_FILE.exists() and
STATE_FILE.stat().st_size > 0 and
STATE_FILE.stat().st_mtime_ns > previous_mtime):
return
time.sleep(0.02)
bridge = CProcessBridge()
def first(query, name, default=""):
values = query.get(name)
if not values:
return default
return values[0]
def integer(query, name, default=-1):
try:
# API 查询参数都是字符串,这里集中做整数转换和默认值处理。
return int(first(query, name, str(default)))
except ValueError:
return default
class WebUIHandler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=str(WEB_DIR), **kwargs)
def log_message(self, fmt, *args):
sys.stderr.write("%s\n" % (fmt % args))
def do_GET(self):
parsed = urlparse(self.path)
if parsed.path.startswith("/api/"):
# /api/* 是操作接口,其它路径交给 SimpleHTTPRequestHandler 返回静态页面。
# 这样一个 Python 文件同时承担静态资源服务和后端 API。
self.handle_api(parsed.path, parse_qs(parsed.query))
return
super().do_GET()
def send_json(self, payload, status=200):
body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def handle_api(self, path, query):
try:
# 每个 API 分支只做参数整理,然后拼成 C 程序已经支持的命令。
# 真实业务逻辑仍在 C 代码里,避免 Python 和 C 各维护一套状态。
if path == "/api/state":
pass
elif path == "/api/create":
name = first(query, "name", "process").replace(" ", "_")
priority = integer(query, "priority", 1)
service_time = integer(query, "service_time", 1)
bridge.command(f"create {name} {priority} {service_time}")
elif path == "/api/fork":
bridge.command(f"fork {integer(query, 'pid')}")
elif path == "/api/schedule":
algo = first(query, "type", "fcfs")
quantum = integer(query, "quantum", 2)
bridge.command(f"schedule {algo} {quantum}")
elif path == "/api/step":
bridge.command("step")
elif path == "/api/run":
bridge.command("run")
elif path == "/api/block":
bridge.command(f"block {integer(query, 'pid')}")
elif path == "/api/wakeup":
bridge.command(f"wakeup {integer(query, 'pid')}")
elif path == "/api/kill":
bridge.command(f"kill {integer(query, 'pid')}")
elif path == "/api/killtree":
bridge.command(f"killtree {integer(query, 'pid')}")
elif path == "/api/send":
from_pid = integer(query, "from")
to_pid = integer(query, "to")
# 命令行协议用空格分隔参数,因此这里把换行去掉,避免破坏 C 端解析。
content = first(query, "content", "").replace("\n", " ").strip()
bridge.command(f"send {from_pid} {to_pid} {content}")
elif path == "/api/recv":
bridge.command(f"recv {integer(query, 'pid')}")
elif path == "/api/sem_create":
name = first(query, "name", "sem").replace(" ", "_")
value = integer(query, "value", 1)
bridge.command(f"sem_create {name} {value}")
elif path == "/api/sem_wait":
bridge.command(f"sem_wait {integer(query, 'sem_id')} {integer(query, 'pid')}")
elif path == "/api/sem_signal":
bridge.command(f"sem_signal {integer(query, 'sem_id')}")
elif path == "/api/shm_create":
bridge.command(f"shm_create {integer(query, 'size', 64)}")
elif path == "/api/shm_attach":
bridge.command(f"shm_attach {integer(query, 'shm_id')} {integer(query, 'pid')}")
elif path == "/api/shm_write":
shm_id = integer(query, "shm_id")
pid = integer(query, "pid")
offset = integer(query, "offset", 0)
# 共享内存写入内容作为命令末尾文本传给 C 程序。
data = first(query, "data", "").replace("\n", " ").strip()
bridge.command(f"shm_write {shm_id} {pid} {offset} {data}")
elif path == "/api/shm_read":
bridge.command(
f"shm_read {integer(query, 'shm_id')} {integer(query, 'pid')} {integer(query, 'offset', 0)} {integer(query, 'length', 32)}"
)
elif path == "/api/shm_destroy":
bridge.command(f"shm_destroy {integer(query, 'shm_id')}")
elif path == "/api/pipe_create":
bridge.command("pipe_create")
elif path == "/api/pipe_write":
pipe_id = integer(query, "pipe_id")
pid = integer(query, "pid")
data = first(query, "data", "").replace("\n", " ").strip()
bridge.command(f"pipe_write {pipe_id} {pid} {data}")
elif path == "/api/pipe_read":
bridge.command(f"pipe_read {integer(query, 'pipe_id')} {integer(query, 'pid')}")
elif path == "/api/pipe_close":
bridge.command(
f"pipe_close {integer(query, 'pipe_id')} {integer(query, 'close_reader', 0)} {integer(query, 'close_writer', 0)}"
)
elif path == "/api/resource_add":
name = first(query, "name", "R").replace(" ", "_")
bridge.command(f"resource_add {name} {integer(query, 'total', 1)}")
elif path == "/api/resource_request":
bridge.command(
f"request {integer(query, 'pid')} {integer(query, 'resource_id')} {integer(query, 'amount', 1)}"
)
elif path == "/api/resource_release":
bridge.command(
f"release {integer(query, 'pid')} {integer(query, 'resource_id')} {integer(query, 'amount', 1)}"
)
elif path == "/api/detect_deadlock":
bridge.command("detect_deadlock")
elif path == "/api/reset":
bridge.reset()
else:
self.send_json({"error": "unknown api"}, status=404)
return
# 每个操作后都返回最新状态,前端不需要再额外请求一次。
self.send_json(bridge.state())
except Exception as exc:
# 出错时返回 JSON,前端可以统一显示错误信息。
self.send_json({"error": str(exc)}, status=500)
def main():
if not BIN.exists():
raise SystemExit("process_manager not found. Run `make` first.")
# PORT 可通过环境变量覆盖,默认使用 8080,方便本地演示。
port = int(os.environ.get("PORT", "8080"))
bridge.start()
server = ThreadingHTTPServer(("127.0.0.1", port), WebUIHandler)
print(f"Python WebUI server: http://127.0.0.1:{port}")
print(f"C JSON state file: {STATE_FILE}")
try:
server.serve_forever()
except KeyboardInterrupt:
pass
finally:
bridge.stop()
server.server_close()
if __name__ == "__main__":
main()