"""Cloud Hypervisor REST API 传输(叶子模块,不依赖 cloud.sandbox,避免循环导入)。 只用标准库 raw HTTP over Unix socket,无第三方依赖。 """ import json import os import socket 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)。""" 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) chunks = [] # feat-chv-fix(P1-6):读超时不再当“响应结束”。 # 先读满响应头,再按 Content-Length 读满体部;超时、 # 提前关流、体部短于 Content-Length 一律 RuntimeError # (调用方判 api_timeout / 传输失败),绝不解析半截 # 响应。chunked 与无 Content-Length 响应明确拒绝 # (Cloud Hypervisor REST 不应发 chunked;无长度则 # 无法证完整性)。 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") body = buf[header_end + 4:] 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,拒绝解析") 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 body = body[:content_length] finally: sock.close() 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 normalize_api_result(result): """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