fix(device-test): contain tool processes and add reviewed preview
This commit is contained in:
1 parent
c98d7527a0
commit
7d31e09cb7
59 files changed
+23722
No files matched your search
@@ -0,0 +1,353 @@
|
||||
"""Portable dependency discovery around the byte-preserved Skill runner."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import struct
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
|
||||
PACKAGE = Path(__file__).resolve().parent
|
||||
# -I excludes even the script directory. Only add this installed package, never cwd.
|
||||
sys.path.insert(0, str(PACKAGE))
|
||||
from dependency_checks import check_function, check_startup, python_runtime
|
||||
|
||||
|
||||
def upstream_runner():
|
||||
name = "artemis_device_test_upstream_runner"
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
name, PACKAGE / "skill" / "scripts" / "tool_runner.py"
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
UPSTREAM = upstream_runner()
|
||||
|
||||
|
||||
class DependencyError(ValueError):
|
||||
"""Only stable, public error codes belong here."""
|
||||
|
||||
|
||||
def dll_machine(path: Path) -> int:
|
||||
"""Read just the DOS/PE headers; discovering a DLL never loads it."""
|
||||
try:
|
||||
with path.open("rb") as stream:
|
||||
header = stream.read(64)
|
||||
if len(header) != 64 or header[:2] != b"MZ":
|
||||
raise ValueError
|
||||
offset = struct.unpack_from("<I", header, 60)[0]
|
||||
if not 64 <= offset <= 1024 * 1024:
|
||||
raise ValueError
|
||||
stream.seek(offset)
|
||||
pe = stream.read(24)
|
||||
if len(pe) != 24 or pe[:4] != b"PE\x00\x00":
|
||||
raise ValueError
|
||||
return struct.unpack_from("<H", pe, 4)[0]
|
||||
except (OSError, ValueError, struct.error):
|
||||
raise DependencyError("mediainfo_invalid_or_unreadable_pe") from None
|
||||
|
||||
|
||||
def validate_library(path: Path) -> None:
|
||||
machine = dll_machine(path)
|
||||
expected = 0x8664 if struct.calcsize("P") == 8 else 0x14C
|
||||
if machine != expected:
|
||||
raise DependencyError("mediainfo_architecture_mismatch")
|
||||
|
||||
|
||||
def path_directories():
|
||||
seen = set()
|
||||
for value in os.environ.get("PATH", "").split(os.pathsep):
|
||||
value = value.strip().strip('"')
|
||||
if not value or value.startswith(("\\\\", "//")):
|
||||
continue
|
||||
path = Path(value)
|
||||
# Ignore empty/relative PATH entries: they must not search the working directory.
|
||||
if not path.is_absolute() or not path.is_dir():
|
||||
continue
|
||||
resolved = path.resolve()
|
||||
if resolved not in seen:
|
||||
seen.add(resolved)
|
||||
yield resolved
|
||||
|
||||
|
||||
def media_library(profile, roots):
|
||||
configured = profile.get("libraries", {}).get("mediainfo")
|
||||
if configured:
|
||||
path = Path(UPSTREAM.configured_local_path(configured, "mediainfo", r"MediaInfo\.dll"))
|
||||
validate_library(path)
|
||||
return str(path), "profile", []
|
||||
rejected = []
|
||||
candidates = [
|
||||
(path, "tool_root")
|
||||
for path in UPSTREAM.root_candidates(roots)
|
||||
if path.name.lower() == "mediainfo.dll" and path.is_file()
|
||||
]
|
||||
candidates.extend((directory / "MediaInfo.dll", "path") for directory in path_directories())
|
||||
seen = set()
|
||||
for path, source in candidates:
|
||||
if not path.is_file() or path.resolve() in seen:
|
||||
continue
|
||||
seen.add(path.resolve())
|
||||
try:
|
||||
validate_library(path)
|
||||
except DependencyError as exc:
|
||||
rejected.append({"path": str(path), "error_code": str(exc)})
|
||||
continue
|
||||
return str(path.resolve()), source, rejected
|
||||
if rejected:
|
||||
raise DependencyError("mediainfo_no_matching_architecture_or_valid_pe")
|
||||
raise UPSTREAM.ToolMissing("mediainfo")
|
||||
|
||||
|
||||
def resolve_dependency(tool_id, profile, roots):
|
||||
"""Resolve one dependency only; an unrelated broken dependency cannot block it."""
|
||||
effective = copy.deepcopy(profile)
|
||||
rejected = []
|
||||
if tool_id == "mediainfo":
|
||||
path, source, rejected = media_library(profile, roots)
|
||||
effective.setdefault("libraries", {})["mediainfo"] = path
|
||||
elif (
|
||||
tool_id == "binwalk"
|
||||
and not profile.get("runtimes", {}).get("binwalk_python")
|
||||
and not profile.get("tools", {}).get("binwalk")
|
||||
):
|
||||
# One documented opt-in directory convention, never an arbitrary Python on PATH.
|
||||
for root in roots:
|
||||
runtime = Path(root) / "binwalk-python" / "Scripts" / "python.exe"
|
||||
if runtime.is_file():
|
||||
effective.setdefault("runtimes", {})["binwalk_python"] = str(runtime.resolve())
|
||||
break
|
||||
prefix = UPSTREAM.command_prefix(tool_id, effective, roots)
|
||||
if tool_id != "mediainfo":
|
||||
path = prefix[0]
|
||||
if profile.get("runtimes", {}).get("binwalk_python") and tool_id == "binwalk":
|
||||
source = "profile.runtime"
|
||||
elif profile.get("tools", {}).get(tool_id):
|
||||
source = "profile"
|
||||
elif tool_id == "binwalk" and effective.get("runtimes", {}).get("binwalk_python"):
|
||||
source = "tool_root.binwalk-python"
|
||||
else:
|
||||
source = (
|
||||
"tool_root"
|
||||
if any(Path(path).is_relative_to(Path(root).resolve()) for root in roots)
|
||||
else "path"
|
||||
)
|
||||
return (
|
||||
effective,
|
||||
prefix,
|
||||
{"status": "found", "source": source, "path": path, "rejected": rejected},
|
||||
)
|
||||
|
||||
|
||||
CAPABILITIES = {
|
||||
"probe_media": (
|
||||
"ffprobe",
|
||||
["RTSP connectivity and business expectations require task evidence"],
|
||||
),
|
||||
"media_info": ("mediainfo", []),
|
||||
"analyze_pcap": ("tshark", []),
|
||||
"capture_packets": (
|
||||
"dumpcap",
|
||||
["selected interface, Npcap and capture permission; no capture attempted"],
|
||||
),
|
||||
"scan_ports": ("nmap", ["target connectivity and requested port states; no scan attempted"]),
|
||||
"scan_firmware": (
|
||||
"binwalk",
|
||||
["signature detection is not firmware compatibility or encryption acceptance"],
|
||||
),
|
||||
"capture_serial": (
|
||||
"powershell",
|
||||
["adapter driver, selected COM, baud, permission and exclusive access; no port opened"],
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def capability_status(tools):
|
||||
by_id = {row["tool_id"]: row for row in tools}
|
||||
result = {}
|
||||
for capability, (tool_id, conditions) in CAPABILITIES.items():
|
||||
row = by_id[tool_id]
|
||||
discovery, startup, function = (
|
||||
row[key]["status"] for key in ("discovery", "startup", "functional")
|
||||
)
|
||||
blocked = (
|
||||
discovery != "found"
|
||||
or "failed" in {startup, function}
|
||||
or "timed_out" in {startup, function}
|
||||
)
|
||||
result[capability] = {
|
||||
"dependency": tool_id,
|
||||
"status": "blocked"
|
||||
if blocked
|
||||
else "local_verified"
|
||||
if function == "passed"
|
||||
else "available_unverified",
|
||||
"external_conditions": conditions,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def doctor(profile, roots, *, check_versions=False, check_functional=False, timeout=42):
|
||||
started = time.monotonic()
|
||||
deadline = started + max(0.1, timeout - 1)
|
||||
UPSTREAM.validate_profile(profile)
|
||||
rows, prefixes = [], {}
|
||||
catalog = json.loads(UPSTREAM.CATALOG.read_text("utf-8"))
|
||||
for entry in catalog["tools"]:
|
||||
row = dict(entry)
|
||||
tool_id = entry["tool_id"]
|
||||
if tool_id == "mediainfo":
|
||||
row["capability"] = (
|
||||
"通过 profile/统一目录/PATH 发现匹配位数的本地 MediaInfo.dll;独立子进程固定 API 和白名单字段,不使用 GUI/CLI 替代。"
|
||||
)
|
||||
if tool_id not in UPSTREAM.EXECUTABLES and tool_id != "mediainfo":
|
||||
row["availability"] = {
|
||||
"mcp": "mcp-check-required",
|
||||
"orchestrator": "orchestrator-context",
|
||||
}.get(entry.get("interface"), "manual-interface")
|
||||
rows.append(row)
|
||||
continue
|
||||
row.update(startup={"status": "not_checked"}, functional={"status": "not_checked"})
|
||||
try:
|
||||
_, prefix, discovery = resolve_dependency(tool_id, profile, roots)
|
||||
prefixes[tool_id] = prefix
|
||||
row["discovery"] = discovery
|
||||
row["availability"] = (
|
||||
"library-found"
|
||||
if tool_id == "mediainfo"
|
||||
else "python-module-configured"
|
||||
if tool_id == "binwalk" and len(prefix) > 1
|
||||
else "executable-found"
|
||||
)
|
||||
except UPSTREAM.ToolMissing:
|
||||
row.update(
|
||||
availability="missing",
|
||||
discovery={"status": "missing", "error_code": "dependency_not_found"},
|
||||
)
|
||||
except (UPSTREAM.ValidationError, DependencyError) as exc:
|
||||
row.update(
|
||||
availability="invalid", discovery={"status": "invalid", "error_code": str(exc)}
|
||||
)
|
||||
except OSError:
|
||||
row.update(
|
||||
availability="unavailable",
|
||||
discovery={"status": "unavailable", "error_code": "dependency_path_unreadable"},
|
||||
)
|
||||
rows.append(row)
|
||||
runtime = python_runtime(deadline, check_versions or check_functional)
|
||||
with tempfile.TemporaryDirectory(prefix="artemis-dependency-check-") as temporary:
|
||||
directory = Path(temporary)
|
||||
# Complete startup checks first, so a slow functional check cannot hide discovery/load results.
|
||||
if check_versions or check_functional:
|
||||
for row in rows:
|
||||
tool_id = row["tool_id"]
|
||||
if tool_id in prefixes:
|
||||
row["startup"] = check_startup(tool_id, prefixes[tool_id], directory, deadline)
|
||||
if check_functional:
|
||||
for row in rows:
|
||||
tool_id = row["tool_id"]
|
||||
if tool_id in prefixes:
|
||||
row["functional"] = (
|
||||
check_function(tool_id, prefixes[tool_id], directory, deadline)
|
||||
if row["startup"]["status"] == "passed"
|
||||
else {"status": "not_checked", "reason": "startup_not_verified"}
|
||||
)
|
||||
return {
|
||||
"execution_status": "completed",
|
||||
"test_verdict": "not_evaluated",
|
||||
"tools": rows,
|
||||
"capabilities": capability_status(rows),
|
||||
"python_runtime": runtime,
|
||||
"checks": {
|
||||
"startup_requested": check_versions or check_functional,
|
||||
"functional_requested": check_functional,
|
||||
},
|
||||
"external_conditions": {
|
||||
"capture_driver": "interface enumeration does not prove packet capture permission",
|
||||
"serial_driver": "port enumeration does not prove adapter driver or communication",
|
||||
"gui_and_websites": "require actual application/window, authorized session and permissions",
|
||||
"android": "requires Artemis device checks in the current task",
|
||||
},
|
||||
"device_actions_executed": False,
|
||||
"catalog_evidence_is_historical": True,
|
||||
"elapsed_seconds": round(time.monotonic() - started, 3),
|
||||
"note": "Independent local checks; no scan, capture, serial open, download, installation or business verdict. PATH is inherited from the Artemis host process.",
|
||||
}
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
arguments = list(sys.argv[1:] if argv is None else argv)
|
||||
functional = "--check-functional" in arguments
|
||||
if functional:
|
||||
arguments.remove("--check-functional")
|
||||
try:
|
||||
args = UPSTREAM.parse_args(arguments)
|
||||
if functional and args.command != "doctor":
|
||||
raise UPSTREAM.ValidationError("functional_check_only_supported_for_doctor")
|
||||
profile = UPSTREAM.load_profile(args.profile)
|
||||
if args.command in {"list", "doctor"}:
|
||||
result = doctor(
|
||||
profile,
|
||||
args.tool_root,
|
||||
check_versions=bool(getattr(args, "check_versions", False) and not args.dry_run),
|
||||
check_functional=functional and not args.dry_run,
|
||||
timeout=UPSTREAM.bounded_int(
|
||||
args.timeout or "42", 1, 3600, "timeout_must_be_1_to_3600_seconds"
|
||||
),
|
||||
)
|
||||
else:
|
||||
tool_id = {
|
||||
"probe": "ffprobe",
|
||||
"media-info": "mediainfo",
|
||||
"analyze-pcap": "tshark",
|
||||
"capture": "dumpcap",
|
||||
"scan": "nmap",
|
||||
"firmware-scan": "binwalk",
|
||||
"serial": "powershell",
|
||||
}[args.command]
|
||||
effective, _, discovery = resolve_dependency(tool_id, profile, args.tool_root)
|
||||
plan = UPSTREAM.build_plan(args, effective, os.environ)
|
||||
result = (
|
||||
{"execution_status": "planned", "test_verdict": "not_evaluated", **plan.public()}
|
||||
if args.dry_run
|
||||
else UPSTREAM.execute(plan)
|
||||
)
|
||||
result["dependency_discovery"] = discovery
|
||||
code = 0 if result["execution_status"] in {"planned", "completed"} else 1
|
||||
except UPSTREAM.ToolMissing as exc:
|
||||
result = {
|
||||
"execution_status": "tool_missing",
|
||||
"test_verdict": "not_evaluated",
|
||||
"tool_id": str(exc),
|
||||
"error_code": "prepare_dependency_in_path_tool_root_or_explicit_profile",
|
||||
}
|
||||
code = 3
|
||||
except (UPSTREAM.ValidationError, DependencyError) as exc:
|
||||
result = {
|
||||
"execution_status": "invalid_arguments",
|
||||
"test_verdict": "not_evaluated",
|
||||
"error_code": str(exc),
|
||||
}
|
||||
code = 2
|
||||
except (OSError, ValueError):
|
||||
result = {
|
||||
"execution_status": "configuration_error",
|
||||
"test_verdict": "not_evaluated",
|
||||
"error_code": "local_file_unreadable_or_invalid",
|
||||
}
|
||||
code = 2
|
||||
print(json.dumps(result, ensure_ascii=True, indent=2))
|
||||
return code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user