96 lines
4.7 KiB
Python
96 lines
4.7 KiB
Python
"""Private, bounded worker RPC. It exposes registered plans, never shell commands."""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import socket
|
|
import socketserver
|
|
import stat
|
|
import threading
|
|
import uuid
|
|
from . import native_plans, test_catalog
|
|
from .catalog import safe_path
|
|
from .store import Store
|
|
|
|
|
|
def handle(settings, store, data):
|
|
operation = data.get("op")
|
|
if operation == "input":
|
|
identity, text = data.get("run_id", ""), data.get("text")
|
|
if not re.fullmatch(r"[a-f0-9]{32}", identity) or not isinstance(text, str) or "\x00" in text or len(text.encode()) > 1024:
|
|
raise ValueError("菜单输入格式不正确")
|
|
if not re.fullmatch(r"(?:[0-9 ,:./+\-]*|[yYnNqQ])", text):
|
|
raise ValueError("菜单通道仅接受数字选项、数字列表或 y/n/q;不接受命令、控制键或凭据")
|
|
run = store.run(identity)
|
|
execution = run.get("execution", {})
|
|
if run["state"] != "running" or execution.get("mode") != "native" or not execution.get("interactive"):
|
|
raise ValueError("当前任务不接受菜单输入")
|
|
path = safe_path(settings.native_socket.parent / ("input-" + identity), settings.native_socket.parent)
|
|
if not stat.S_ISFIFO(path.lstat().st_mode):
|
|
raise ValueError("菜单输入通道不可用")
|
|
try:
|
|
descriptor = os.open(path, os.O_WRONLY | os.O_NONBLOCK | os.O_NOFOLLOW)
|
|
try:
|
|
payload = (text.rstrip("\r\n") + "\n").encode()
|
|
if os.write(descriptor, payload) != len(payload):
|
|
raise ValueError("菜单输入未完整送达")
|
|
finally:os.close(descriptor)
|
|
except OSError as exc:
|
|
raise ValueError("菜单暂未就绪或已结束") from exc
|
|
return {"accepted": True}
|
|
if operation not in {"preflight", "queue"}:
|
|
raise ValueError("未知原环境操作")
|
|
identity = data.get("script_id", "")
|
|
if not re.fullmatch(r"[a-f0-9]{32}", identity):
|
|
raise ValueError("测试项目编号不合法")
|
|
if operation == "queue":
|
|
request_id = str(uuid.UUID(data.get("request_id", "")))
|
|
previous = store.run_for_request(request_id)
|
|
if previous:
|
|
if previous["script_id"] != identity:
|
|
raise ValueError("请求编号已被其他测试使用")
|
|
return previous
|
|
script = test_catalog.describe(settings, store.script(identity), store.scripts())
|
|
plan = native_plans.build_plan(settings, script)
|
|
if operation == "preflight":
|
|
return native_plans.public_plan(plan)
|
|
if not script["enabled"] or script["status"] != "ready":
|
|
raise ValueError("请先配置并启用测试项目")
|
|
if not plan["ready"]:
|
|
raise ValueError("原环境预检未通过,请查看缺失条件")
|
|
if data.get("plan_token") != plan["plan_token"]:
|
|
raise ValueError("原入口、参数或配置已变化,请重新预检并确认")
|
|
acknowledged = data.get("impact_acknowledged") is True
|
|
if plan["impact"]["requires_confirmation"] and not acknowledged:
|
|
raise ValueError("此测试会使用共享资源或切换服务,需要本次运行的明确确认")
|
|
execution = native_plans.public_plan(plan)
|
|
execution.update(impact_acknowledged=acknowledged, verification="启动方案已核对;本次结果以真实运行记录为准")
|
|
return store.queue(script, request_id, data.get("source_ip", "unknown"), execution=execution)
|
|
|
|
|
|
def start(settings, store):
|
|
folder = settings.native_socket.parent
|
|
safe_path(folder, settings.root, must_exist=False).mkdir(mode=0o750, exist_ok=True)
|
|
if settings.native_socket.exists():
|
|
if not stat.S_ISSOCK(settings.native_socket.lstat().st_mode):
|
|
raise ValueError("原环境 IPC 路径不是 socket")
|
|
settings.native_socket.unlink()
|
|
class Handler(socketserver.StreamRequestHandler):
|
|
def handle(self):
|
|
self.connection.settimeout(15)
|
|
try:
|
|
raw = self.rfile.readline(8193)
|
|
if len(raw) > 8192:
|
|
raise ValueError("原环境请求过大")
|
|
result = handle(settings, store, json.loads(raw))
|
|
except (ValueError, KeyError) as exc:
|
|
result = {"error": str(exc.args[0])}
|
|
except Exception:
|
|
result = {"error": "原环境操作失败,请检查执行器状态"}
|
|
try:self.wfile.write(json.dumps(result).encode() + b"\n")
|
|
except OSError:pass
|
|
server = socketserver.UnixStreamServer(str(settings.native_socket), Handler)
|
|
os.chmod(settings.native_socket, 0o600)
|
|
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
return server
|