"""Administrator-registered, content-bound local training sources (no client paths).""" from __future__ import annotations import hashlib import json import os import re import shutil import stat import subprocess import tempfile import threading import time from contextlib import contextmanager from copy import deepcopy from pathlib import Path SOURCE_ID = re.compile(r"^[A-Za-z0-9_-]{1,64}$") DIGEST = re.compile(r"^[0-9a-f]{64}$") LIMITS = { "checkpoint": 256 * 1024**2, "onnx": 64 * 1024**2, "env": 2 * 1024**2, "agent": 128 * 1024, } TASKS = ["Unitree-Go2-Flat", "Unitree-Go2-ObstacleAvoidance"] class SourceError(ValueError): pass def regular_bytes(path: Path, root: Path, limit: int) -> bytes: """Walk with dir_fd/O_NOFOLLOW, so symlink substitution cannot escape the root.""" if ".." in path.parts or not path.is_absolute(): raise SourceError("基础策略路径必须是允许根内的绝对路径,不能含 ..") try: parts = path.relative_to(root).parts except ValueError as error: raise SourceError("基础策略文件不在管理员配置的允许根内") from error if not parts: raise SourceError("基础策略必须是常规文件") try: fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW) except OSError as error: raise SourceError("基础策略快照目录缺失或无效,拒绝训练") from error try: for part in parts[:-1]: next_fd = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW, dir_fd=fd) os.close(fd) fd = next_fd leaf = os.open(parts[-1], os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK, dir_fd=fd) with os.fdopen(leaf, "rb") as stream: info = os.fstat(stream.fileno()) if not stat.S_ISREG(info.st_mode) or info.st_size > limit: raise SourceError("基础策略文件必须是常规文件且不能超过大小上限") data = stream.read(limit + 1) if len(data) > limit: raise SourceError("基础策略文件超过大小上限") return data except OSError as error: raise SourceError( "基础策略文件缺失或含symlink;请提供.pt、policy.onnx和params配置常规文件" ) from error finally: os.close(fd) class PretrainedSources: def __init__(self, config_path: Path | None, store: Path, python: str, trainer_root: Path): self.store = store.expanduser().resolve() self.store.mkdir(parents=True, exist_ok=True, mode=0o700) self.python = python self.trainer_root = trainer_root.resolve() self.entries: dict[str, dict] = {} self._upload_lock = threading.Lock() self._catalog_lock = threading.RLock() self._restore_uploads() if config_path is None: return try: if config_path.stat().st_size > 128 * 1024: raise SourceError("基础策略注册配置不能超过128KiB") config = json.loads(config_path.read_text()) if not isinstance(config, dict) or set(config) != {"allowedRoots", "sources"}: raise SourceError("注册配置只接受allowedRoots和sources") if ( not isinstance(config["allowedRoots"], list) or not isinstance(config["sources"], list) or len(config["sources"]) > 32 ): raise SourceError("允许根/sources必须为数组,最多32个注册条目") roots = [Path(p).expanduser().resolve(strict=True) for p in config["allowedRoots"]] if not roots or any(not p.is_dir() for p in roots): raise SourceError("请配置存在的本地允许根目录") for entry in config["sources"]: if not isinstance(entry, dict) or set(entry) != { "id", "label", "checkpoint", "onnx", }: raise SourceError("注册条目必须包含id/label/checkpoint/onnx") key = entry["id"] if not isinstance(key, str) or not SOURCE_ID.fullmatch(key) or key in self.entries: raise SourceError("注册id无效或重复") if not isinstance(entry["label"], str) or not 1 <= len(entry["label"]) <= 100: raise SourceError("基础策略label必须为1–100字符") record = {"id": key, "label": entry["label"], "ready": False, "compatibleTasks": []} self.entries[key] = {"public": record} try: checkpoint, onnx = ( Path(entry["checkpoint"]).expanduser(), Path(entry["onnx"]).expanduser(), ) if not re.fullmatch(r"[A-Za-z0-9_.-]+\.pt", checkpoint.name): raise SourceError("请选择ONNX对应的.pt训练checkpoint,ONNX不能直接续训") files = { "checkpoint": checkpoint, "onnx": onnx, "env": checkpoint.parent / "params/env.yaml", "agent": checkpoint.parent / "params/agent.yaml", } data = {} for name, path in files.items(): root = next((r for r in roots if path.is_relative_to(r)), None) if root is None: raise SourceError("注册文件不在允许根内") data[name] = regular_bytes(path, root, LIMITS[name]) names = { "checkpoint": checkpoint.name, "onnx": "policy.onnx", "env": "params/env.yaml", "agent": "params/agent.yaml", } with tempfile.TemporaryDirectory(dir=self.store, prefix="import-") as temporary: directory = Path(temporary) for name, content in data.items(): destination = directory / names[name] destination.parent.mkdir(exist_ok=True) destination.write_bytes(content) manifest = self._validate(directory, checkpoint.name, TASKS[0], None) digest = manifest["source_id"] bound = { "sourceId": digest, "registeredId": key, "label": entry["label"], "manifest": manifest, } destination = self.store / digest if not destination.exists(): shutil.copytree(directory, destination) for file in destination.rglob("*"): file.chmod(0o555 if file.is_dir() else 0o444) destination.chmod(0o555) self.verify(bound) record.update( id=digest, ready=True, compatibleTasks=TASKS, observationSizes=[47, 81, 97], initialization=bound, ) except (SourceError, OSError, ValueError, TypeError) as error: record["error"] = f"{error};请修正管理员注册配置并重启服务,不会退回随机初始化" except (OSError, TypeError, ValueError) as error: raise SourceError(f"基础策略注册配置无效:{error}") from error def _restore_uploads(self): # Only committed content directories are catalogued. Partial uploads are never sources. for directory in self.store.iterdir(): if not DIGEST.fullmatch(directory.name) or not (directory / "upload.json").is_file(): continue try: manifest = json.loads( regular_bytes(directory / "upload.json", self.store, 64 * 1024) ) label = json.loads(regular_bytes(directory / "label.json", self.store, 1024))[ "label" ] bound = { "sourceId": directory.name, "registeredId": directory.name, "label": self._display_name(label), "manifest": manifest, } if manifest["source_id"] != directory.name: raise SourceError("上传来源身份损坏") self.verify(bound) self._publish_upload(bound) except (SourceError, ValueError, KeyError, TypeError, OSError): self.entries[directory.name] = { "public": { "id": directory.name, "label": "失效的已上传策略", "ready": False, "compatibleTasks": [], "error": "上传快照损坏,拒绝随机初始化;请重新上传原文件或恢复快照", } } @staticmethod def _display_name(name): if not isinstance(name, str): return "uploaded-policy" name = name.replace("\\", "/").split("/")[-1] return ( re.sub(r"[^\w .()\-]", "_", name, flags=re.UNICODE)[:100].strip(" .") or "uploaded-policy" ) def _publish_upload(self, bound): record = { "id": bound["sourceId"], "label": bound["label"], "ready": True, "compatibleTasks": TASKS, "observationSizes": [47, 81, 97], "initialization": bound, } with self._catalog_lock: self.entries[bound["sourceId"]] = {"public": record} return deepcopy(record) @contextmanager def upload_slot(self, length): # At most one receiving/decoding upload, 32 sources and 2GiB cumulative store. if not self._upload_lock.acquire(blocking=False): raise SourceError("已有文件正在上传/验证,请稍后重试") try: files = list(self.store.rglob("*")) used = sum(p.lstat().st_size for p in files if not p.is_symlink() and p.is_file()) count = sum(1 for p in self.store.iterdir() if DIGEST.fullmatch(p.name)) if count >= 32 or used + length + 16 * 1024**2 > 2 * 1024**3: raise SourceError("基础策略存储已达32个来源或2GiB上限,请由本机维护者释放空间") with tempfile.TemporaryDirectory(dir=self.store, prefix="upload-") as temporary: yield Path(temporary) finally: self._upload_lock.release() def receive_upload(self, stream, length, fmt, template, display_name, *, set_timeout=None): if template != "go2-legacy47-v1": raise SourceError("必须明确确认go2-legacy47-v1观测与关节模板") limit = {"pt": 256 * 1024**2, "onnx": 64 * 1024**2}.get(fmt) if limit is None: raise SourceError("仅支持单个.pt或.onnx,不接受ZIP/路径/配套目录") if type(length) is not int or not 0 < length <= limit: raise SourceError("上传文件为空或超过.pt 256MiB / ONNX 64MiB上限") with self.upload_slot(length) as directory: path = directory / f"upload.{fmt}" deadline = time.monotonic() + 60 remaining = length try: with path.open("xb") as output: while remaining: timeout = deadline - time.monotonic() if timeout <= 0: raise SourceError("上传超时,请重试") try: if set_timeout is not None: set_timeout(min(10, timeout)) # BufferedReader.read(n) may perform many recv calls whose socket # timeouts reset with each trickled byte. read1 returns after one # raw read, allowing the total deadline to be checked every time. read_chunk = getattr(stream, "read1", stream.read) chunk = read_chunk(min(1024 * 1024, remaining)) except OSError as error: raise SourceError("上传超时/连接中断,临时文件已清理") from error if not chunk or len(chunk) > remaining: raise SourceError("上传连接中断或实际长度不符") output.write(chunk) remaining -= len(chunk) finally: if set_timeout is not None: set_timeout(10) request = { "path": str(path), "directory": str(directory), "format": fmt, "template": template, } env = { **os.environ, "CUDA_VISIBLE_DEVICES": "", "OMP_NUM_THREADS": "1", "OPENBLAS_NUM_THREADS": "1", "MKL_NUM_THREADS": "1", } try: result = subprocess.run( [self.python, str(Path(__file__).parent / "rl/scripts/validate_upload.py")], input=json.dumps(request), capture_output=True, text=True, cwd=self.trainer_root, env=env, timeout=60, ) manifest = json.loads(result.stdout) except (OSError, subprocess.TimeoutExpired, ValueError) as error: raise SourceError("模型验证超时/资源超限或训练Python依赖不可用") from error if result.returncode: raise SourceError(manifest.get("error", "不支持的模型文件")) digest = manifest["source_id"] if not DIGEST.fullmatch(digest): raise SourceError("模型验证返回无效身份") label = self._display_name(display_name) (directory / "upload.json").write_text(json.dumps(manifest), encoding="utf-8") (directory / "label.json").write_text(json.dumps({"label": label}), encoding="utf-8") destination = self.store / digest if destination.exists(): # Dedup never replaces a committed artifact or changes an old job binding. old = next((r for r in self.catalog() if r["id"] == digest and r["ready"]), None) if old is None: raise SourceError("同ID旧快照已损坏,请恢复快照后重试;不会覆盖旧任务来源") self.verify(old["initialization"]) return old for file in directory.iterdir(): file.chmod(0o444) directory.rename(destination) destination.chmod(0o555) bound = { "sourceId": digest, "registeredId": digest, "label": label, "manifest": manifest, } self.verify(bound) return self._publish_upload(bound) def _validate(self, directory: Path, checkpoint: str, task_id: str, task_config: dict | None): command = [self.python, str(Path(__file__).parent / "rl/scripts/validate_pretrained.py")] request = { "directory": str(directory), "checkpoint": checkpoint, "taskId": task_id, "taskConfig": task_config, "uploaded": (directory / "upload.json").is_file(), } try: result = subprocess.run( command, input=json.dumps(request), cwd=self.trainer_root, capture_output=True, text=True, timeout=60, ) except (subprocess.TimeoutExpired, OSError) as error: raise SourceError("基础策略验证器超时或不可用,请检查训练Python环境") from error if result.returncode: # Validator emits only an actionable error, never a traceback or file contents. raise SourceError( result.stderr.strip()[-1500:] or "基础策略验证器失败,请检查训练Python依赖" ) try: return json.loads(result.stdout) except ValueError as error: raise SourceError("基础策略验证器未返回有效身份描述") from error def catalog(self): with self._catalog_lock: sources = {} for entry in self.entries.values(): sources.setdefault(entry["public"]["id"], deepcopy(entry["public"])) return list(sources.values()) def bind(self, source_id, task_id: str, task_config=None): # Selection is content-addressed, so a stale UI cannot silently bind a newly # registered model under the same administrator-friendly registration name. entry = next( (entry for entry in self.catalog() if entry["id"] == source_id), None, ) if not isinstance(source_id, str) or entry is None: raise SourceError("基础策略内容ID不存在或已变化;请刷新服务连接,不接受文件路径") if not entry["ready"]: raise SourceError(entry["error"]) if task_id not in entry["compatibleTasks"]: raise SourceError("该基础策略仅兼容Flat47/Obstacle81或97;Rough高度扫描不支持迁移") bound = deepcopy(entry["initialization"]) directory = self.verify(bound) manifest = self._validate( directory, bound["manifest"]["artifacts"]["checkpoint"]["name"], task_id, task_config ) if manifest["source_id"] != bound["sourceId"]: raise SourceError("基础策略快照身份变化,拒绝初始化") return bound def verify(self, bound: dict) -> Path: try: digest = bound["sourceId"] if not isinstance(digest, str) or not DIGEST.fullmatch(digest): raise SourceError("基础策略快照身份无效") directory = self.store / digest artifacts = bound["manifest"]["artifacts"] if "sourceFormat" in bound["manifest"]: fmt = bound["manifest"]["sourceFormat"] if fmt not in ("pt", "onnx"): raise SourceError("上传格式无效") stored = json.loads(regular_bytes(directory / "upload.json", self.store, 64 * 1024)) if stored != bound["manifest"]: raise SourceError("上传manifest SHA绑定变化,拒绝初始化") for name, relative, limit in ( ( "upload", f"upload.{fmt}", LIMITS["checkpoint"] if fmt == "pt" else LIMITS["onnx"], ), ("checkpoint", "actor.pt", 16 * 1024**2), ): content = regular_bytes(directory / relative, self.store, limit) if ( artifacts[name]["name"] != relative or hashlib.sha256(content).hexdigest() != artifacts[name]["sha256"] ): raise SourceError("上传快照SHA不匹配,拒绝训练") return directory for name, relative in { "checkpoint": artifacts["checkpoint"]["name"], "onnx": "policy.onnx", "env": "params/env.yaml", "agent": "params/agent.yaml", }.items(): content = regular_bytes(directory / relative, self.store, LIMITS[name]) if hashlib.sha256(content).hexdigest() != artifacts[name]["sha256"]: raise SourceError("基础策略快照SHA不匹配,拒绝训练;请恢复原快照") return directory except (KeyError, TypeError) as error: raise SourceError("持久化基础策略描述损坏,拒绝训练") from error def arguments(self, bound: dict) -> list[str]: directory = self.verify(bound) upload_args = ( ["--pretrained-upload-manifest", str(directory / "upload.json")] if "sourceFormat" in bound["manifest"] else [] ) return upload_args + [ "--pretrained-checkpoint", str(directory / bound["manifest"]["artifacts"]["checkpoint"]["name"]), "--pretrained-allowed-roots", json.dumps([str(directory)]), "--pretrained-source-id", bound["sourceId"], ]