444 lines
25 KiB
Python
444 lines
25 KiB
Python
#!/usr/bin/env python3
|
||
"""准备嵌入式图片协商候选版本;本工具不停止进程、不替换在用程序、不修改在用配置。"""
|
||
import argparse
|
||
import configparser
|
||
import hashlib
|
||
import io
|
||
import ipaddress
|
||
import json
|
||
import os
|
||
from pathlib import Path
|
||
import platform
|
||
import re
|
||
import shutil
|
||
import subprocess
|
||
import tarfile
|
||
import time
|
||
import uuid
|
||
|
||
|
||
# 本次仅交付图片地址协商代码;其他业务源码必须以当前开发板为基线。
|
||
# 不整份上传历史工程,避免覆盖最新日志保留、串口或RTP收流修改。
|
||
PATCH_FILES = (
|
||
"util/PlatformWireCodec.h", "util/PlatformWireCodec.cpp",
|
||
"util/PlatformImageRouteClient.h", "util/PlatformImageRouteClient.cpp",
|
||
"util/PlatformImageSender.h", "util/PlatformImageSender.cpp",
|
||
)
|
||
# 测试只运行临时目录与loopback,不使用实际设备密钥和采集端口。
|
||
TEST_FILES = (
|
||
"tests/image_route/native_image_route_check.pro",
|
||
"tests/image_route/native_image_route_check.cpp",
|
||
"tests/logging/native_logging_check.pro",
|
||
"tests/logging/native_logging_check.cpp",
|
||
)
|
||
ALLOWED = set(PATCH_FILES + TEST_FILES)
|
||
# 这些文件只记录哈希,以验证部署保留现有功能;本工具不会覆盖它们。
|
||
PRESERVED = (
|
||
"main.cpp", "util/log.cpp", "util/log.h", "util/CollectorLogRetention.cpp",
|
||
"util/CollectorLogRetention.h", "start.sh",
|
||
"Go2Controller/network/rtpreceiver.cpp", "Fly2Controller/network/RtpReceiver.cpp",
|
||
"util/ModuleVideoReceiver.cpp", "CJ2Controller/serialworker.cpp", "mainwindow.cpp",
|
||
)
|
||
|
||
|
||
def digest(path):
|
||
# 分块计算,避免相机SDK等大文件一次读入内存。
|
||
value = hashlib.sha256()
|
||
with path.open("rb") as stream:
|
||
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
||
value.update(block)
|
||
return value.hexdigest()
|
||
|
||
|
||
def write_json(path, value):
|
||
# 部署记录留在私有stage;不输出配置全文、密钥文件或进程环境。
|
||
path.write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8")
|
||
|
||
|
||
def package(args):
|
||
# 使用当前工作空间源码,禁止调用旧prepare-board-deployment脚本重建全量包。
|
||
# 包只携带本次代码与测试,不携带平台IP、上传配置、密钥或旧可执行文件。
|
||
# 压缩包不含旧可执行文件,必须经实际ARM工具链重新构建。
|
||
# 不读取本机现网INI,候选IP必须在板端stage时显式指定。
|
||
source = args.source.resolve(strict=True)
|
||
# 同名产物不覆盖,方便将每次候选与原始校验记录一一对应。
|
||
if args.package.exists():
|
||
raise RuntimeError("Package already exists; choose a new output filename")
|
||
args.package.parent.mkdir(parents=True, exist_ok=True)
|
||
manifest = {"protocol": "COLLECTOR_IMAGE_ROUTE", "format": 1, "files": []}
|
||
# 精确白名单不包含任何ini/密钥/运行目录,也不递归打包未知文件。
|
||
with tarfile.open(str(args.package), "w:gz") as archive:
|
||
for relative in PATCH_FILES + TEST_FILES:
|
||
path = source / relative
|
||
if path.is_symlink() or not path.resolve(strict=True).is_relative_to(source):
|
||
raise RuntimeError("Unexpected source path: " + relative)
|
||
data = path.read_bytes()
|
||
item = tarfile.TarInfo(relative)
|
||
item.size = len(data)
|
||
item.mode = 0o644
|
||
archive.addfile(item, io.BytesIO(data))
|
||
manifest["files"].append({"path": relative, "sha256": hashlib.sha256(data).hexdigest()})
|
||
data = json.dumps(manifest, ensure_ascii=False, indent=2).encode("utf-8")
|
||
item = tarfile.TarInfo("image-route-manifest.json")
|
||
item.size = len(data)
|
||
item.mode = 0o644
|
||
archive.addfile(item, io.BytesIO(data))
|
||
print(json.dumps({"package": str(args.package.resolve()), "files": len(ALLOWED),
|
||
"sha256": digest(args.package)}, ensure_ascii=False))
|
||
|
||
|
||
def live_root(args):
|
||
# 路径必须是已存在工程,禁止根目录及符号链接冒充部署目录。
|
||
if args.root.is_symlink():
|
||
raise RuntimeError("Collector root cannot be a symbolic link")
|
||
# 所有后续路径由规范化根目录拼接,不接收任意远端目标路径。
|
||
root = args.root.resolve(strict=True)
|
||
if root == Path(root.anchor) or not (root / "px_Collect.pro").is_file():
|
||
raise RuntimeError("Collector project root is invalid")
|
||
return root
|
||
|
||
|
||
def process_ids(root):
|
||
target = str(root / "bin/px_JKGL")
|
||
# 没有/proc时只用于本地安全测试,不声称发现了实际板端进程。
|
||
matches = []
|
||
# 仅核对可执行文件真实路径,不用进程名字匹配其他同名程序。
|
||
for entry in Path("/proc").iterdir() if Path("/proc").is_dir() else []:
|
||
if not entry.name.isdigit():
|
||
continue
|
||
try:
|
||
if os.readlink(str(entry / "exe")).replace(" (deleted)", "") == target:
|
||
matches.append(int(entry.name))
|
||
except (FileNotFoundError, PermissionError):
|
||
pass
|
||
return sorted(matches)
|
||
|
||
|
||
def inspect(args):
|
||
# 不主动建立SSH,操作者负责将工具上传到已确认的目标板再运行。
|
||
# inspect不改网络、时钟、配置、库或进程,输出可供root比对的哈希。
|
||
# 输出源码哈希供当前板与本机差异核对,不能把历史记录当成现网事实。
|
||
root = live_root(args)
|
||
# 地址和目标路径来自root确认,inspect只给非敏感状态与源码哈希。
|
||
print(json.dumps({"root": str(root), "architecture": platform.machine(),
|
||
"collectorPids": process_ids(root), "binaryPresent": (root / "bin/px_JKGL").is_file(),
|
||
"qmake": shutil.which("qmake"), "make": shutil.which("make"),
|
||
"preservedSources": {name: digest(root / name) for name in PRESERVED if (root / name).is_file()},
|
||
"sourceHashes": {name: digest(root / name) if (root / name).is_file() else None for name in PATCH_FILES},
|
||
"projectSha256": digest(root / "px_Collect.pro")}, ensure_ascii=False, indent=2))
|
||
|
||
|
||
def ignore_files(directory, names):
|
||
# 不在stage复制在用配置和密钥;测试自行生成临时密钥。
|
||
# 对bin/runtime/logs/build的排除也避免复制采集历史和巨量日志。
|
||
ignored = []
|
||
for name in names:
|
||
lower = name.lower()
|
||
if name in {"bin", "runtime", "logs", "build", "debug", "release", ".git", "__pycache__"} \
|
||
or name.startswith(("Makefile", "moc_", ".qmake")) \
|
||
or lower.endswith((".ini", ".o", ".obj", ".user", ".pyc")) \
|
||
or ("key" in lower and lower.endswith((".json", ".properties"))):
|
||
ignored.append(name)
|
||
return ignored
|
||
|
||
|
||
def payload(args):
|
||
# 逐个读取白名单文件,不调用tar.extract,拒绝目录穿越、链接或重复条目。
|
||
# 文件总数和每项大小均受限,未知条目即使名称看似安全也不接受。
|
||
with tarfile.open(str(args.package), "r:gz") as archive:
|
||
names = archive.getnames()
|
||
if len(names) != len(set(names)) or set(names) != ALLOWED | {"image-route-manifest.json"}:
|
||
raise RuntimeError("Unexpected archive contents")
|
||
content = {}
|
||
for name in names:
|
||
item = archive.getmember(name)
|
||
if not item.isfile() or item.size <= 0 or item.size > 2 * 1024 * 1024:
|
||
raise RuntimeError("Invalid archive entry: " + name)
|
||
content[name] = archive.extractfile(item).read()
|
||
# manifest也受大小和类型约束,只有白名单完整且逐文件哈希匹配才继续。
|
||
manifest = json.loads(content.pop("image-route-manifest.json"))
|
||
if manifest.get("protocol") != "COLLECTOR_IMAGE_ROUTE" or manifest.get("format") != 1:
|
||
raise RuntimeError("Unknown package format")
|
||
if len(manifest.get("files", [])) != len(ALLOWED):
|
||
raise RuntimeError("Manifest entry count is invalid")
|
||
checks = {item["path"]: item["sha256"] for item in manifest["files"]}
|
||
if set(checks) != ALLOWED or any(hashlib.sha256(data).hexdigest() != checks[name]
|
||
for name, data in content.items()):
|
||
raise RuntimeError("Package hash validation failed")
|
||
return content
|
||
|
||
|
||
def project_append(project):
|
||
text = project.read_text(encoding="utf-8")
|
||
# 保留板上已有依赖/DEFINES,尤其GST、日志保留与hiredis真实库位置。
|
||
# 仅补缺少的HEADERS/SOURCES,不拿本机历史pro覆盖当前ARM工程。
|
||
# 注释提到文件名不算真实注册;追加动作仅发生在stage副本。
|
||
uncommented = "\n".join(line.split("#", 1)[0] for line in text.splitlines())
|
||
# 六个编译单元的类型由白名单后缀决定,不接受manifest提供qmake指令。
|
||
additions = []
|
||
for relative in PATCH_FILES:
|
||
if not re.search(r"(?<![A-Za-z0-9_])" + re.escape(relative) + r"(?![A-Za-z0-9_.])", uncommented):
|
||
additions.append(("HEADERS" if relative.endswith(".h") else "SOURCES") + " += " + relative)
|
||
if additions:
|
||
text += "\n# 图片上传地址协商:仅追加编译单元,保留当前板端全部原有依赖。\n"
|
||
text += "\n".join(additions) + "\n"
|
||
project.write_text(text, encoding="utf-8")
|
||
|
||
|
||
def candidate_config(original, gateway, gateway_port, fallback, fallback_port):
|
||
# 使用严格解析检查重复节/键,再只编辑指定键,保留其他值、注释和大小写。
|
||
parser = configparser.ConfigParser(interpolation=None)
|
||
try:
|
||
parser.read_string(original)
|
||
except configparser.Error:
|
||
# INI解析错误可能包含原配置行,禁止把潜在凭证写入部署日志。
|
||
raise RuntimeError("Existing collector INI has unsupported or duplicate entries") from None
|
||
if not parser.getboolean("image_upload", "enabled", fallback=False):
|
||
raise RuntimeError("Existing image_upload must already be enabled")
|
||
# 协商target由可信回复动态决定,此处不写节点IP,也不固定每台节点的路数。
|
||
# 独立fallback_ip是部署必填项,避免fixed_json指向网关后继承错误地址。
|
||
changes = {"image_upload": {"udp_host": fallback, "udp_port": str(fallback_port)},
|
||
"image_negotiation": {"enabled": "true", "gateway_host": gateway,
|
||
"gateway_port": str(gateway_port)}}
|
||
current = None
|
||
result = []
|
||
# seen区分已有节与真正缺失的节,避免生成会被QSettings错误解释的重复配置。
|
||
seen = set()
|
||
|
||
def missing():
|
||
# 原节结束前补缺项,不追加重复节;不会重写fixed_json、heartbeat或RTP。
|
||
if current in changes:
|
||
for key, value in changes[current].items():
|
||
result.append(key + "=" + value)
|
||
changes[current].clear()
|
||
|
||
# 逐行保留未修改设置,避免ConfigParser整份重写丢失现网注释和自定义字段。
|
||
for line in original.splitlines():
|
||
section = re.match(r"^\s*\[([^]]+)\]\s*(?:[;#].*)?$", line)
|
||
if section:
|
||
missing()
|
||
current = section.group(1)
|
||
seen.add(current)
|
||
entry = re.match(r"^\s*([^=;#]+?)\s*=", line)
|
||
key = entry.group(1).strip() if entry else None
|
||
if current in changes and key in changes[current]:
|
||
line = key + "=" + changes[current].pop(key)
|
||
result.append(line)
|
||
missing()
|
||
for section, values in changes.items():
|
||
if values and section not in seen:
|
||
result += ["", "; 节点地址失效回退PXHJ原图入口,不能借用网关作为fallback。", "[" + section + "]"]
|
||
result += [key + "=" + value for key, value in values.items()]
|
||
# 配置只返回字符串,不在此函数触碰生产config.ini。
|
||
return "\n".join(result) + "\n"
|
||
|
||
|
||
def stage(args):
|
||
root = live_root(args)
|
||
archive = payload(args)
|
||
# 默认stage落在工程runtime内且使用唯一ID;已有目录绝不覆盖或递归删除。
|
||
parent = root / "runtime"
|
||
parent.mkdir(exist_ok=True)
|
||
if parent.is_symlink():
|
||
raise RuntimeError("Runtime directory cannot be a symbolic link")
|
||
directory = parent / ("image-route-stage-" + time.strftime("%Y%m%d-%H%M%S") + "-" + uuid.uuid4().hex[:8])
|
||
# stage私有权限保护原配置备份;候选config另设600权限。
|
||
directory.mkdir(mode=0o700)
|
||
source = directory / "source"
|
||
# 由当前板端源码和SDK创建隔离副本,不在生产源目录做增量make。
|
||
# 保留相对库链接;后续所有写入路径还会核对没有经过链接逃出stage。
|
||
shutil.copytree(root, source, symlinks=True, ignore=ignore_files)
|
||
required = {"main.cpp": "CollectorLogRetention::install",
|
||
"px_Collect.pro": "COLLECTOR_WITH_GST",
|
||
"Go2Controller/network/rtpreceiver.cpp": "PlatformImageSender",
|
||
"Fly2Controller/network/RtpReceiver.cpp": "PlatformImageSender"}
|
||
# 发现旧工程缺少既有内置日志或JPG收流链路时停止,不能静默退回旧功能。
|
||
# 操作者需先审阅差异并合并当前源码,不能让本工具盲目覆盖main或RTP文件。
|
||
for relative, marker in required.items():
|
||
path = source / relative
|
||
if not path.is_file() or marker not in path.read_text(encoding="utf-8"):
|
||
raise RuntimeError("Required existing feature missing: " + relative)
|
||
# 记录当前板的受保留源码,build再次确认副本没有被覆盖为旧版本。
|
||
baseline = {name: digest(root / name) for name in PRESERVED if (root / name).is_file()}
|
||
# stage失败时留存证据目录,不自动删除备份或任何生产文件。
|
||
previous = {name: digest(root / name) if (root / name).is_file() else None for name in PATCH_FILES}
|
||
backup = directory / "backup"
|
||
backup.mkdir(mode=0o700)
|
||
# 切换前回退材料提前保存;备份config为私有文件,不打印其中任何内容。
|
||
for relative in PATCH_FILES + ("px_Collect.pro", "bin/px_JKGL", "config.ini"):
|
||
live = root / relative
|
||
if live.is_symlink() or not live.parent.resolve().is_relative_to(root):
|
||
raise RuntimeError("Unexpected live symbolic link: " + relative)
|
||
if live.is_file():
|
||
destination = backup / relative
|
||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||
shutil.copy2(live, destination)
|
||
os.chmod(str(backup / "config.ini"), 0o600)
|
||
# 仅写副本白名单;在用util源码保持原样直到root执行已授权切换。
|
||
for name, data in archive.items():
|
||
destination = source / name
|
||
if destination.is_symlink() or not destination.parent.resolve().is_relative_to(source.resolve()):
|
||
raise RuntimeError("Unexpected stage symbolic link: " + name)
|
||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||
destination.write_bytes(data)
|
||
project_append(source / "px_Collect.pro")
|
||
# 图片fallback单独指定PXHJ,不读取或改变固定采集目标来猜测原图目的地。
|
||
config = candidate_config((root / "config.ini").read_text(encoding="utf-8-sig"),
|
||
args.gateway_host, args.gateway_port, args.fallback_ip, args.fallback_port)
|
||
candidate = directory / "candidate-config.ini"
|
||
# 协商enabled=true只进入候选配置,此时当前程序仍读取原配置。
|
||
candidate.write_text(config, encoding="utf-8")
|
||
os.chmod(str(candidate), 0o600)
|
||
# 元数据只含文件哈希和路径,回退材料与候选config均由stage目录权限保护。
|
||
# 原来不存在的新文件以None记录,切换时不会把新增与覆盖混为一谈。
|
||
metadata = {"format": 1, "root": str(root), "stage": str(directory),
|
||
"packageSha256": digest(args.package), "previousPatchHashes": previous,
|
||
"preservedHashes": baseline, "previousProjectSha256": digest(root / "px_Collect.pro"),
|
||
"previousBinarySha256": digest(root / "bin/px_JKGL"),
|
||
"previousConfigSha256": digest(root / "config.ini"),
|
||
"candidateConfigSha256": digest(candidate),
|
||
"candidatePatchHashes": {name: digest(source / name) for name in PATCH_FILES},
|
||
"candidateProjectSha256": digest(source / "px_Collect.pro"), "built": False}
|
||
write_json(directory / "stage.json", metadata)
|
||
print(json.dumps({"stage": str(directory), "liveChanged": False,
|
||
"backup": str(backup), "candidateConfig": str(candidate)}, ensure_ascii=False))
|
||
|
||
|
||
def stage_record(args):
|
||
root = live_root(args)
|
||
# 构建与回退都必须绑定创建时的root/stage身份,不能复用别的工程记录。
|
||
if args.stage.is_symlink():
|
||
raise RuntimeError("Stage directory cannot be a symbolic link")
|
||
directory = args.stage.resolve(strict=True)
|
||
if not directory.is_relative_to((root / "runtime").resolve()) or directory == root / "runtime":
|
||
raise RuntimeError("Stage must stay inside the collector runtime directory")
|
||
# 不根据文件名猜测stage所属,记录内的绝对路径必须和当前解析结果相同。
|
||
record = json.loads((directory / "stage.json").read_text(encoding="utf-8"))
|
||
if record.get("root") != str(root) or record.get("stage") != str(directory):
|
||
raise RuntimeError("Stage record does not match its target root")
|
||
# 返回规范化路径供build/verify/rollback共用,防止作用范围逐步漂移。
|
||
return root, directory, record
|
||
|
||
|
||
def verify(args):
|
||
root, directory, record = stage_record(args)
|
||
# 任一在用源码/程序/配置从stage以后改变,就停止,不能覆盖更新后的版本。
|
||
# 新文件原本不存在也要记录:出现外部新增版本时同样不能覆盖。
|
||
expected = dict(record["preservedHashes"])
|
||
expected.update(record["previousPatchHashes"])
|
||
expected.update({"px_Collect.pro": record["previousProjectSha256"],
|
||
"bin/px_JKGL": record["previousBinarySha256"], "config.ini": record["previousConfigSha256"]})
|
||
# 不打印任何差异内容,仅报告路径;例如config变化不能泄漏其中的凭证。
|
||
changed = [name for name, value in expected.items()
|
||
if (digest(root / name) if (root / name).is_file() else None) != value]
|
||
if changed:
|
||
raise RuntimeError("Live files changed since staging: " + ", ".join(changed))
|
||
print(json.dumps({"stage": str(directory), "liveUnchanged": True, "built": record["built"]}))
|
||
|
||
|
||
def run_logged(command, cwd, log):
|
||
# 不通过shell拼接命令,参数中的路径作为独立argv传递。
|
||
# check=True保证失败停止后续步骤,不把半编译候选标成built。
|
||
# 构建输出只写stage日志,失败保留全部证据;不占用原run.log预算。
|
||
with log.open("wb") as stream:
|
||
subprocess.run(command, cwd=str(cwd), stdout=stream, stderr=subprocess.STDOUT, check=True)
|
||
|
||
|
||
def build(args):
|
||
# 本机Windows测试不能代替全ARM工程,本模式明确限定aarch64环境。
|
||
# 当前代码不提供任何去掉相机/GStreamer/日志依赖来勉强构建的降级分支。
|
||
root, directory, record = stage_record(args)
|
||
if platform.machine() not in {"aarch64", "arm64"}:
|
||
raise RuntimeError("Full application build must run on the ARM64 target environment")
|
||
# build前再次核对生产文件没被其他部署修改,避免基于陈旧源生成候选。
|
||
verify(args)
|
||
source = directory / "source"
|
||
# 源码保留核对发生在任何qmake/make之前,避免事后才发现构建了旧main。
|
||
for name in PRESERVED:
|
||
if name in record["preservedHashes"] and digest(source / name) != record["preservedHashes"][name]:
|
||
raise RuntimeError("Preserved staged source changed: " + name)
|
||
build_dir = directory / "build"
|
||
build_dir.mkdir(exist_ok=True)
|
||
# 空本地stash防止继承祖先缓存;不修改板上已用工程的.qmake.stash。
|
||
(build_dir / ".qmake.stash").touch()
|
||
run_logged(["qmake", str(source / "px_Collect.pro"), "-after", "INCLUDEPATH+=" + str(source)],
|
||
build_dir, directory / "qmake.log")
|
||
# 已知板时钟可能早于Qt依赖,阻止Makefile自身反复重生成,但正常编译依赖仍保留。
|
||
run_logged(["make", "-o", "Makefile", "-j" + str(args.jobs)], build_dir, directory / "build.log")
|
||
# qmake的PWD是隔离source,DESTDIR因此只写stage/source/bin,生产bin不变。
|
||
executable = source / "bin/px_JKGL"
|
||
if not executable.is_file() or not os.access(str(executable), os.X_OK):
|
||
raise RuntimeError("Full build did not produce an executable")
|
||
# ldd只检查候选库可解析;LD_LIBRARY_PATH沿用当前启动脚本的SDK/hiredis路径。
|
||
environment = dict(os.environ)
|
||
environment["LD_LIBRARY_PATH"] = str(root / "3rdparty/libs") + ":/usr/local/lib/pt_libs:" + environment.get("LD_LIBRARY_PATH", "")
|
||
# 子进程环境不输出到日志,只保留ldd给出的库路径和解析结果。
|
||
linked = subprocess.run(["ldd", str(executable)], env=environment, capture_output=True, text=True, check=True)
|
||
(directory / "ldd.log").write_text(linked.stdout, encoding="utf-8")
|
||
# 可执行文件存在不代表能启动,未解析的相机/Qt/hiredis库必须在切换前处理。
|
||
if "not found" in linked.stdout:
|
||
raise RuntimeError("Candidate has unresolved shared libraries; inspect staged ldd.log")
|
||
# 原日志测试证明预算与入口不退化;协商测试证明回程、租约和原图协议。
|
||
for name, project, executable_name in (
|
||
("logging", "native_logging_check", "native_logging_check"),
|
||
("image_route", "native_image_route_check", "native_image_route_check"),
|
||
):
|
||
test_dir = directory / ("test-" + name)
|
||
test_dir.mkdir(exist_ok=True)
|
||
(test_dir / ".qmake.stash").touch()
|
||
run_logged(["qmake", str(source / "tests" / name / (project + ".pro"))],
|
||
test_dir, directory / (name + "-qmake.log"))
|
||
# 测试也遵守目标板时钟限制,不改系统时间来规避工具链问题。
|
||
run_logged(["make", "-o", "Makefile", "-j" + str(args.jobs)],
|
||
test_dir, directory / (name + "-build.log"))
|
||
# 只运行隔离QtTest,不启动候选主程序,所以不会抢6000/9200或产生真实数据。
|
||
run_logged([str(test_dir / executable_name), "-o", str(directory / (name + "-tests.txt")) + ",txt"],
|
||
test_dir, directory / (name + "-run.log"))
|
||
# 只有完整程序、动态库检查和两套隔离测试都成功才记录built。
|
||
# 准备原图与迁移的业务验收仍需root通过真实PXHJ执行,不能由此标为完成。
|
||
record.update({"built": True, "candidateBinary": str(executable),
|
||
"candidateBinarySha256": digest(executable)})
|
||
# built标记仅描述候选准备,实际部署和业务验收还未由本工具执行。
|
||
write_json(directory / "stage.json", record)
|
||
print(json.dumps({"stage": str(directory), "buildPassed": True, "liveChanged": False,
|
||
"candidateBinary": str(executable), "candidateBinarySha256": digest(executable)}))
|
||
|
||
|
||
def arguments():
|
||
# 默认inspect,防止操作者漏写模式便执行任何准备或构建写入。
|
||
# 所有stage参数都显式给出,不沿用历史部署的IP猜测当前网卡状态。
|
||
parser = argparse.ArgumentParser(description=__doc__)
|
||
parser.add_argument("mode", choices=("package", "inspect", "stage", "build", "verify"), nargs="?", default="inspect")
|
||
parser.add_argument("--source", type=Path, default=Path("E:/Codex/qrs/SY_26_PX_Collect"))
|
||
parser.add_argument("--root", type=Path, default=Path("/home/Tronlong/SY26-PX-Collect"))
|
||
parser.add_argument("--package", type=Path)
|
||
parser.add_argument("--stage", type=Path)
|
||
parser.add_argument("--gateway-host")
|
||
parser.add_argument("--gateway-port", type=int, default=19002)
|
||
parser.add_argument("--fallback-ip")
|
||
parser.add_argument("--fallback-port", type=int, default=6020)
|
||
parser.add_argument("--jobs", type=int, default=4)
|
||
# mode本身就是明确操作入口;本脚本没有activate或自动启动功能。
|
||
args = parser.parse_args()
|
||
if args.mode in {"package", "stage"} and args.package is None:
|
||
parser.error("--package is required")
|
||
if args.mode in {"build", "verify"} and args.stage is None:
|
||
parser.error("--stage is required")
|
||
if not 1 <= args.jobs <= 16 or any(not 1 <= port <= 65535 for port in (args.gateway_port, args.fallback_port)):
|
||
parser.error("Invalid build jobs or UDP port")
|
||
if args.mode == "stage":
|
||
# 禁止DNS猜测和广播,gateway及fallback必须由root明确给出当前部署IP。
|
||
for name, value in (("gateway-host", args.gateway_host), ("fallback-ip", args.fallback_ip)):
|
||
try:
|
||
address = ipaddress.IPv4Address(value)
|
||
if address.is_unspecified or address.is_multicast or int(address) == 0xffffffff:
|
||
raise ValueError()
|
||
except (ValueError, TypeError):
|
||
parser.error("--" + name + " must be an explicit unicast IPv4 address")
|
||
return args
|
||
|
||
|
||
if __name__ == "__main__":
|
||
options = arguments()
|
||
{"package": package, "inspect": inspect, "stage": stage, "build": build, "verify": verify}[options.mode](options)
|