Files
codex-app-server/cloud/chv/api_client.py
T
host df8c64bd64 feat(chv): 架构/可维护性/前端只读侧推进——生产实验分离、配置片段化、VM 只读展示
架构:WarmPool 从 cloud/chv/pool.py 移到 experimental_pool.py,
生产残留清理彻底分文件到 cloud/chv/orphans.py(facade 直接读
registry 文件清理,不构造池、不碰 prewarm/acquire/release);
cloud/chv 与 cloud/sandbox 不再导出 WarmPool,旧路径一律
ImportError(CHV 未合并、无生产用户,兼容保留已无保留理由);
worker 改走 orphans facade,不再持有 _warm_pool;#215 对齐从
接口形状推进到 REPLACEMENT_POINTS 替换清单(R1/R2 可机械替换、
R3 永不替换,#215 模块当前不存在由测试钉住)。

可维护性:build_chv_config 拆成 base/ssh/network/quota/disk/
timeout 六片段(组装只做 dict 合并,键冲突即报错;38 字段集
与取值不变,单项非法报错文案逐字一致);ADR-12 模块职责图
与禁 import 清单(AST 钉住);cloud/chv/ 公开 API 全量类型
标注、无 Any 逃逸,并修正三处旧标注错误;死代码扫一遍
(删实验池已无调用的 _quarantine 委托与未用 _LOGGER)。

前端(只读侧,不做控制按钮):chv.ts 新增纯逻辑谓词与文案
(执行器精确匹配、附件守卫、VM 状态文案、列表徽标);列表
行新增 VM 已报告死亡徽标;详情对全部 CHV 任务显示 VM 行
(已报告死亡/未报告死亡,不猜 live);WorkersSection 与成本
卡沿用已有数据,不新开池管理/成本页;服务端未新增第二个
生命周期字段(持久化事实只有三项,chv_vm_dead 已是完整派生),
只把派生抽成 _chv_vm_dead_from_row 并补 goal 回落形态测试。
2026-10-31 06:55:26 +00:00

144 lines
5.2 KiB
Python
Raw 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.
"""Cloud Hypervisor REST API 传输(叶子模块,不依赖
cloud.sandbox,避免循环导入)。
只用标准库 raw HTTP over Unix socket,无第三方依赖。
"""
import json
import os
import socket
def _read_header(sock):
"""读满响应头:返回 (header_text, 已收体部前缀)。
超时或提前关流即 RuntimeError,绝不把半截头当结束
(feat-chv-fix P1-6 的语义,纯移动)。"""
buf = b""
header_end = None
timed_out = False
while header_end is None:
try:
data = sock.recv(65536)
except socket.timeout:
timed_out = True
break
if not data:
break
buf += data
header_end = buf.find(b"\r\n\r\n")
if timed_out or header_end is None:
raise RuntimeError("CHV API 响应头不完整(超时或提前关流)")
header = buf[:header_end].decode("utf-8", errors="replace")
return header, buf[header_end + 4:]
def _parse_content_length(header):
"""从响应头取 Content-Length:chunked 或无长度即拒绝。
chunked 明确拒绝(CHV REST 不应发 chunked);无长度则
无法证完整性,同样拒绝。"""
content_length = None
is_chunked = False
for line in header.split("\r\n")[1:]:
if ":" not in line:
continue
name, value = line.split(":", 1)
name = name.strip().lower()
value = value.strip()
if name == "content-length":
try:
content_length = int(value)
except ValueError:
raise RuntimeError("CHV API Content-Length 非法")
elif name == "transfer-encoding" \
and "chunked" in value.lower():
is_chunked = True
if is_chunked:
raise RuntimeError("CHV API 响应为 chunked,拒绝解析")
if content_length is None:
raise RuntimeError("CHV API 响应无 Content-Length,拒绝解析")
return content_length
def _read_body(sock, body, content_length):
"""按 Content-Length 读满体部:超时或提前关流即截断错误。"""
while len(body) < content_length:
try:
data = sock.recv(65536)
except socket.timeout:
raise RuntimeError(
"CHV API 响应体截断:已收 %d / 需 %d 字节"
% (len(body), content_length))
if not data:
raise RuntimeError(
"CHV API 响应体截断:已收 %d / 需 %d 字节"
% (len(body), content_length))
body += data
return body[:content_length]
def _parse_response(header, body):
"""从已读满的头与体部解析 (status, body):能解析 JSON
时为 dict,否则为原文本;状态行解析失败时 status 为 0。"""
text = header + "\r\n\r\n" + body.decode("utf-8", errors="replace")
status = 0
if text.startswith("HTTP/"):
try:
status = int(text.split()[1])
except (IndexError, ValueError):
status = 0
body_text = body.decode("utf-8", errors="replace")
try:
parsed = json.loads(body_text) if body_text.strip() else {}
except ValueError:
parsed = body_text
return status, parsed
def _unix_socket_api(socket_path, method, path, payload=None):
"""Cloud Hypervisor REST API 的默认传输:标准库 raw HTTP
over Unix socket(无第三方依赖)。返回 (status, body),
body 能解析 JSON 时为 dict,否则为原文本。socket 不存在
或连接失败时 raise RuntimeError(调用方据此判 api 超时
/失败,不编造 200)。读满语义见 _read_header /
_parse_content_length / _read_body(P1-6,纯移动)。"""
if not os.path.exists(socket_path):
raise RuntimeError("api socket 不存在:%s" % socket_path)
body = b""
if payload is not None:
body = json.dumps(payload).encode("utf-8")
req_lines = ["%s /api/v1%s HTTP/1.1" % (method, path),
"Host: localhost",
"Accept: application/json",
"Content-Type: application/json",
"Content-Length: %d" % len(body),
"Connection: close", "", ""]
raw = "\r\n".join(req_lines).encode("utf-8") + body
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.settimeout(5)
try:
sock.connect(socket_path)
sock.sendall(raw)
header, body = _read_header(sock)
content_length = _parse_content_length(header)
body = _read_body(sock, body, content_length)
finally:
sock.close()
return _parse_response(header, body)
def normalize_api_result(result: dict) -> tuple:
"""API 调用归一化:fake api_client 可返回 (status, body)
或 {"status":..., "body":...} 或带 .status 的对象,本
函数统一成 (status:int, body)。"""
if isinstance(result, tuple) and len(result) == 2:
return int(result[0]), result[1]
if isinstance(result, dict):
status = result.get("status", result.get("status_code"))
body = result.get("body", result)
return (int(status) if status is not None else 0), body
status = getattr(result, "status", None) \
or getattr(result, "status_code", None)
return (int(status) if status is not None else 0), result