Files
xas-test-library/console/native_rpc.py
T

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