Add packaged Insta360 X4 integration and recover paired Node channels

Discover independent camera instances and prepare their versioned runtime
from Node or remote Core. Add isolated SDK workers, camera controls, raw
dual-fisheye WebRTC preview, and shared action/region loading states.

Recover existing Node bindings over known Tailscale addresses after a Core
LAN address change. Preserve identities and trust, pin both peers, migrate
endpoints with revision checks, and require real heartbeats for online status.
Fix the Python client certificate profile for Go X509 verification.

Pin Design Guideline 8c53f73 and retain installer/build/acceptance history.
Node 0.8.19 is installed; X4 0.1.3-3 is bundled but hardware activation is pending.

Validation: qualified DG/Node builds and Go race tests; 31 fleet tests;
Python-to-Go certificate interoperability and live tailnet recovery with five
fresh heartbeats; prior 38 X4 tests and bounded remote WebRTC acceptance.
Clean-OS, replug/power autonomy, local X4 video and long-run stability remain open.
This commit is contained in:
DCCONSTRUCTIONS
2026-09-10 09:21:24 +03:00
parent 54a85fdf50
commit a3c15e11e9
125 changed files with 11916 additions and 251 deletions
@@ -1,6 +1,7 @@
// An authenticated Node action may start only this fixed model job.
polkit.addRule(function(action, subject) {
if (subject.user === "mission-core-node" && action.id === "org.freedesktop.systemd1.manage-units" && action.lookup("unit") === "mission-core-node-realsense-prepare.service" && action.lookup("verb") === "start") {
var unit = action.lookup("unit");
if (subject.user === "mission-core-node" && action.id === "org.freedesktop.systemd1.manage-units" && (unit === "mission-core-node-realsense-prepare.service" || unit === "mission-core-node-insta360-x4-profile.service") && action.lookup("verb") === "start") {
return polkit.Result.YES;
}
});
+1 -1
View File
@@ -11,7 +11,7 @@ import sys
from build_deb import build, VERSION, BRAND_SHA256
ROOT = Path(__file__).resolve().parents[1]
DG_COMMIT = "b10fd5d645ddfb8c373ae6105efa0850aef2509c"
DG_COMMIT = "8c53f73ee521561a1cc7944a7f7cf113702f40b5"
def guideline_sources():
+18 -2
View File
@@ -11,7 +11,8 @@ import sys
ROOT = Path(__file__).resolve().parents[1]
VERSION = "0.8.16"
BINARY_VERSION = "0.8.19"
VERSION = "0.8.19"
sys.path.insert(0, str(ROOT.parents[1] / "scripts/packaging"))
from debian import package
@@ -43,7 +44,7 @@ Architecture: amd64
Maintainer: NODE.DC local build <noreply@example.invalid>
Section: admin
Priority: optional
Depends: adduser, systemd, python3, python3-gi, gir1.2-gtk-3.0, gir1.2-webkit2-4.1, pkexec, polkitd, ca-certificates, hicolor-icon-theme, network-manager, iproute2, python3-psycopg2, postgresql-16 (>= 16.15), timescaledb-2-oss-postgresql-16 (= 2.29.2~ubuntu24.04-1615)
Depends: adduser, systemd (>= 255), python3 (>= 3.12), python3 (<< 3.13), python3-gi, gir1.2-gtk-3.0, gir1.2-webkit2-4.1, pkexec, polkitd, ca-certificates, hicolor-icon-theme, network-manager, iproute2, python3-psycopg2, postgresql-16 (>= 16.15), timescaledb-2-oss-postgresql-16 (= 2.29.2~ubuntu24.04-1615), udev, libc6 (>= 2.35), libstdc++6 (>= 12), libgcc-s1, zlib1g
Description: Mission Core onboard computer configuration
Local graphical setup, host inventory, SSH access and persistent node identity.
""".encode()
@@ -80,6 +81,21 @@ Description: Mission Core onboard computer configuration
for name in ("mission-core-realsense.service", "mission-core-node-realsense-prepare.service"):
files.append(("usr/lib/systemd/system/" + name, (p / name).read_bytes(), 0o644))
files.append(("usr/share/polkit-1/rules.d/50-mission-core-device-prepare.rules", (p / "50-mission-core-device-prepare.rules").read_bytes(), 0o644))
# Node owns the first-use bootstrap; the optional model package owns its
# SDK, worker processes, USB grants and runtime preparation. No overlapping
# dpkg file ownership and no compiler/download prerequisite at first use.
profile_raw = (p / "insta360-profile.json").read_bytes()
profile = json.loads(profile_raw)
package_name = "mission-core-insta360-x4_" + profile["version"] + "_amd64.deb"
x4 = (ROOT / "build/model-packages" / package_name).read_bytes()
if len(x4) != profile["bytes"] or hashlib.sha256(x4).hexdigest() != profile["sha256"]:
raise ValueError("Bundled X4 package differs from the admitted release")
files.extend([
("usr/lib/mission-core-node/insta360_profile.py", (p / "insta360_profile.py").read_bytes(), 0o644),
("usr/lib/systemd/system/mission-core-node-insta360-x4-profile.service", (p / "mission-core-node-insta360-x4-profile.service").read_bytes(), 0o644),
("usr/share/mission-core-node/profiles/insta360-x4/profile.json", profile_raw, 0o644),
("usr/share/mission-core-node/profiles/insta360-x4/" + package_name, x4, 0o644),
])
files.append(("usr/share/mission-core-node/realsense/70-mission-core-realsense.rules", (p / "70-mission-core-realsense.rules").read_bytes(), 0o644))
bundle = json.loads((p / "realsense-bundle.json").read_text())
files.append(("usr/share/mission-core-node/realsense/bundle.json", (p / "realsense-bundle.json").read_bytes(), 0o644))
@@ -0,0 +1,128 @@
"""Snapshot the reviewed Node/Core sources and Linux build inputs for the Mini."""
import argparse
import hashlib
import json
import stat
import subprocess
import zipfile
from pathlib import Path
NODE = Path(__file__).resolve().parents[1]
REPO = NODE.parents[1]
DG = REPO.parent / "NODEDC_DESIGN_GUIDELINE"
DG_COMMIT = "8c53f73ee521561a1cc7944a7f7cf113702f40b5"
def files(root):
values = (
subprocess.check_output(
["git", "ls-files", "-c", "-o", "--exclude-standard", "-z"], cwd=root
)
.decode()
.split("\0")
)
for name in sorted(set(values)):
path = root / name
if not name or name.startswith(".codex/") or not path.is_file():
continue
if path.is_symlink() or not path.resolve().is_relative_to(root):
raise ValueError("Unexpected source link")
yield path
def build(qualified, node_only=False):
commit = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=DG, text=True).strip()
if commit != DG_COMMIT:
raise ValueError("Design Guideline revision differs from the admitted source")
paths = list(files(REPO))
# Build DG from its own pinned workspace lock. Its generated dist and
# transitive dependencies are not copied from a developer installation.
paths += [
path
for path in files(DG)
if path.relative_to(DG).parts[0]
in ("packages", "registry", "docs", "scripts", "server", "apps")
]
paths += [
DG / name
for name in (
"package.json",
"package-lock.json",
"tsconfig.base.json",
"apps/catalog/package.json",
)
]
lock = json.loads((NODE / "packaging/linux-toolchains.json").read_text())
paths += [NODE / "build/linux-toolchains" / item["name"] for item in lock["files"]]
for item in lock["files"]:
data = (NODE / "build/linux-toolchains" / item["name"]).read_bytes()
if hashlib.sha256(data).hexdigest() != item["sha256"]:
raise ValueError("Build toolchain changed")
wheel_lock = json.loads((NODE / "packaging/realsense-bundle.json").read_text())
paths += [NODE / "build/realsense-wheels" / item["name"] for item in wheel_lock["wheels"]]
profile = json.loads((NODE / "packaging/insta360-profile.json").read_text())
package_name = "mission-core-insta360-x4_" + profile["version"] + "_amd64.deb"
if qualified.name != package_name:
raise ValueError("Unexpected model package name")
package = qualified.read_bytes()
if len(package) != profile["bytes"] or hashlib.sha256(package).hexdigest() != profile["sha256"]:
raise ValueError("Qualified model package changed")
entries = {str(path.relative_to(REPO.parent)): path for path in sorted(set(paths))}
# The source artifact owns this build staging copy; no manual copy on the
# board, runtime path override or operator compiler dependency is needed.
virtual = str(REPO.name + "/apps/node-agent/build/model-packages/" + package_name)
entries[virtual] = qualified
metadata = {}
for name, path in entries.items():
data = path.read_bytes()
if len(data) > 100 * 1024 * 1024:
raise ValueError("Source file exceeds the artifact bound")
metadata[name] = {
"bytes": len(data),
"sha256": hashlib.sha256(data).hexdigest(),
"mode": 0o755 if path.stat().st_mode & stat.S_IXUSR else 0o644,
}
entrypoint = (NODE / "packaging/linux_build_entry.py").read_bytes()
manifest = {
"schema": "missioncore.node.linux-build-source/v1",
"profile": "node-only" if node_only else "node-core",
"repository": REPO.name,
"base_commit": subprocess.check_output(
["git", "rev-parse", "HEAD"], cwd=REPO, text=True
).strip(),
"design_guideline_commit": commit,
"toolchains": lock,
"entrypoint_sha256": hashlib.sha256(entrypoint).hexdigest(),
"files": metadata,
}
raw = json.dumps(manifest, sort_keys=True, indent=2).encode() + b"\n"
identifier = hashlib.sha256(raw).hexdigest()[:24]
output = NODE / "build" / ("mission-core-node-linux-build-" + identifier + ".pyz")
with output.open("xb") as stream, zipfile.ZipFile(stream, "w") as archive:
for name, data in (("__main__.py", entrypoint), ("source.json", raw)):
archive.writestr(name, data)
for name, path in entries.items():
archive.write(
path,
"source/" + name,
compress_type=zipfile.ZIP_DEFLATED
if path.stat().st_size < 4 * 1024 * 1024
else zipfile.ZIP_STORED,
)
output.chmod(0o600)
return {
"artifact": str(output),
"source_id": identifier,
"files": len(entries),
"bytes": output.stat().st_size,
"sha256": hashlib.sha256(output.read_bytes()).hexdigest(),
}
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--model-package", type=Path, required=True)
parser.add_argument("--node-only", action="store_true")
args = parser.parse_args()
print(json.dumps(build(args.model_package.resolve(), args.node_only)))
@@ -0,0 +1,72 @@
"""Package the Ubuntu-qualified Node installer with the shared release launcher."""
import argparse
import hashlib
import json
import re
import zipfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
REPO = ROOT.parents[1]
def build(qualified):
evidence = json.loads((qualified / "qualification.json").read_text())
if evidence["state"] != "complete":
raise ValueError("A completed Ubuntu qualification is required")
candidates = [name for name in evidence["artifacts"] if name.startswith("mission-core-node_")]
if len(candidates) != 1:
raise ValueError("One qualified Node package is required")
package = candidates[0]
match = re.fullmatch(
r"mission-core-node_([0-9]+\.[0-9]+\.[0-9]+(?:-[1-9][0-9]*)?)_amd64\.deb", package
)
if not match:
raise ValueError("Invalid package name")
payload = (qualified / package).read_bytes()
expected = evidence["artifacts"][package]
if (
len(payload) != expected["bytes"]
or hashlib.sha256(payload).hexdigest() != expected["sha256"]
):
raise ValueError("Qualified Node package changed")
files = {
package: payload,
"install": (ROOT / "packaging/install-owner-release").read_bytes(),
"install_release.py": (ROOT / "packaging/install_owner_release.py").read_bytes(),
}
entry = (REPO / "plugins/insta360-x4/packaging/owner_release_entry.py").read_bytes()
manifest = {
"schema": "missioncore.node.owner-release/v1",
"version": match[1],
"qualification_profile": "node-package-and-local-ui",
"qualification_sha256": hashlib.sha256(
(qualified / "qualification.json").read_bytes()
).hexdigest(),
"clean_image_qualified": False,
"entrypoint_sha256": hashlib.sha256(entry).hexdigest(),
"files": {
name: {"bytes": len(data), "sha256": hashlib.sha256(data).hexdigest()}
for name, data in files.items()
},
}
raw = json.dumps(manifest, indent=2, sort_keys=True).encode() + b"\n"
identifier = hashlib.sha256(raw).hexdigest()[:24]
output = ROOT / "build" / ("mission-core-node-install-" + identifier + ".pyz")
with output.open("xb") as stream, zipfile.ZipFile(stream, "w") as archive:
for name, data in {"__main__.py": entry, "release.json": raw, **files}.items():
archive.writestr(name, data)
output.chmod(0o600)
return {
"artifact": str(output),
"release_id": identifier,
"bytes": output.stat().st_size,
"sha256": hashlib.sha256(output.read_bytes()).hexdigest(),
}
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--qualified", type=Path, required=True)
print(json.dumps(build(parser.parse_args().qualified)))
@@ -0,0 +1,260 @@
"""Versioned Python/Go X509 interoperability check; synthetic public certificates only."""
import hashlib
import json
import os
import platform
import shutil
import subprocess
import sys
import tarfile
import tempfile
import time
import zipfile
from datetime import UTC, datetime
from pathlib import Path
GO_SOURCE = r"""package main
import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"os"
)
func cert(name string) *x509.Certificate {
raw, err := os.ReadFile(name); if err != nil { panic(err) }
block, _ := pem.Decode(raw); if block == nil { panic("invalid fixture") }
value, err := x509.ParseCertificate(block.Bytes); if err != nil { panic(err) }; return value
}
func main() {
root := cert("root.pem"); bad := cert("legacy.pem"); good := cert("fixed.pem")
roots := x509.NewCertPool(); roots.AddCert(root)
options := x509.VerifyOptions{
Roots: roots, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
_, legacyErr := bad.Verify(options)
if _, ok := legacyErr.(x509.UnknownAuthorityError); !ok { panic("legacy failure not reproduced") }
if _, err := good.Verify(options); err != nil { panic(err) }
if string(good.RawSubjectPublicKeyInfo) != string(root.RawSubjectPublicKeyInfo) {
panic("Core key changed")
}
if string(good.RawSubject) == string(root.RawSubject) { panic("ambiguous leaf subject") }
json.NewEncoder(os.Stdout).Encode(map[string]any{"legacy_rejected":true,"fixed_verified":true,"core_key_preserved":true})
}
"""
def digest(data):
return hashlib.sha256(data).hexdigest()
def build():
from cryptography import x509
from cryptography.x509.oid import ExtendedKeyUsageOID
from k1link.fleet.recovery import client_context
from k1link.fleet.trust import CoreTrust, pem
node = Path(__file__).resolve().parents[1]
lock = json.loads((node / "packaging/linux-toolchains.json").read_text())
go = next(item for item in lock["files"] if item["directory"] == "go")
toolchain = (node / "build/linux-toolchains" / go["name"]).read_bytes()
if digest(toolchain) != go["sha256"]:
raise ValueError("Pinned toolchain changed")
with tempfile.TemporaryDirectory(prefix="missioncore-synthetic-ca-") as temporary:
trust = CoreTrust(Path(temporary))
client_context(trust)
fixed = x509.load_pem_x509_certificates(
(Path(temporary) / "recovery-client.pem").read_bytes()
)[0]
legacy = (
trust.builder(trust.ca.subject, trust.key.public_key(), 1)
.issuer_name(trust.ca.subject)
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
.add_extension(x509.ExtendedKeyUsage([ExtendedKeyUsageOID.CLIENT_AUTH]), critical=False)
.sign(trust.key, None)
)
files = {
"root.pem": pem(trust.ca).encode(),
"fixed.pem": pem(fixed).encode(),
"legacy.pem": pem(legacy).encode(),
"check.go": GO_SOURCE.encode(),
"go.tar.gz": toolchain,
}
entry = Path(__file__).read_bytes()
manifest = {
"schema": "missioncore.channel-x509-check/v1",
"toolchain": go,
"entry_sha256": digest(entry),
"files": {
name: {"bytes": len(data), "sha256": digest(data)} for name, data in files.items()
},
}
raw = json.dumps(manifest, sort_keys=True).encode()
identifier = digest(raw)[:24]
path = node / "build" / ("mission-core-channel-check-" + identifier + ".pyz")
with path.open("xb") as stream, zipfile.ZipFile(stream, "w") as archive:
for name, data in {"__main__.py": entry, "check.json": raw, **files}.items():
archive.writestr(name, data)
path.chmod(0o600)
print(
json.dumps(
{
"artifact": str(path),
"id": identifier,
"bytes": path.stat().st_size,
"sha256": digest(path.read_bytes()),
}
)
)
def run():
if os.geteuid() == 0 or (platform.system(), platform.machine()) != ("Linux", "x86_64"):
raise ValueError("Use the unprivileged Ubuntu account")
if sys.argv[1:] not in ([], ["--job"], ["--clean"]):
raise ValueError("Only the fixed certificate check is allowed")
os.umask(0o077)
artifact = Path(sys.argv[0]).resolve()
with zipfile.ZipFile(artifact) as archive:
raw = archive.read("check.json")
manifest = json.loads(raw)
names = {"root.pem", "legacy.pem", "fixed.pem", "check.go", "go.tar.gz"}
if (
manifest["schema"] != "missioncore.channel-x509-check/v1"
or set(manifest["files"]) != names
or set(archive.namelist()) != names | {"__main__.py", "check.json"}
or len(archive.namelist()) != 7
or digest(archive.read("__main__.py")) != manifest["entry_sha256"]
):
raise ValueError("Unexpected check artifact")
identifier = digest(raw)[:24]
root = Path("/var/tmp/mission-core-channel-checks")
folder = root / identifier
for path in (root, folder):
path.mkdir(mode=0o700, exist_ok=True)
info = path.lstat()
if path.is_symlink() or info.st_uid != os.geteuid() or info.st_mode & 0o077:
raise ValueError("Check directory is not private and owned")
if sys.argv[1:] == ["--clean"]:
if not (folder / "report.json").exists():
raise ValueError("Preserve a completed attempt before cleanup")
shutil.rmtree(folder)
print(json.dumps({"cleaned": identifier}))
return
if not sys.argv[1:]:
if any(folder.iterdir()):
raise ValueError("Preserve the prior attempt before retry")
for name, expected in manifest["files"].items():
data = archive.read(name)
if len(data) != expected["bytes"] or digest(data) != expected["sha256"]:
raise ValueError("Check input changed")
(folder / name).write_bytes(data)
(folder / "report.json").write_text(
json.dumps(
{
"state": "staging",
"id": identifier,
"started_at": datetime.now(UTC).isoformat(),
}
)
+ "\n"
)
with tarfile.open(folder / "go.tar.gz") as tar:
members = tar.getmembers()
if len(members) > 40000 or sum(m.size for m in members) > 1024**3:
raise ValueError("Toolchain extraction exceeds bound")
if any(
Path(m.name).is_absolute()
or Path(m.name).parts[0] != "go"
or ".." in Path(m.name).parts
for m in members
):
raise ValueError("Unexpected toolchain path")
tar.extractall(folder, members=members, filter="data")
subprocess.run(
[
"/usr/bin/systemd-run",
"--user",
"--quiet",
"--collect",
"--wait",
"--pipe",
"--unit=mission-core-channel-check-" + identifier,
"--property=MemoryMax=1G",
"--property=CPUQuota=150%",
"--property=TasksMax=128",
"--property=RuntimeMaxSec=180",
"--property=KillMode=control-group",
"--property=Nice=10",
"/usr/bin/python3",
str(artifact),
"--job",
],
check=True,
)
print((folder / "report.json").read_text())
return
group = next(
line.split(":", 2)[2]
for line in Path("/proc/self/cgroup").read_text().splitlines()
if line.startswith("0::")
)
control = Path("/sys/fs/cgroup") / group.lstrip("/")
quota, period = map(int, (control / "cpu.max").read_text().split())
if (
int((control / "memory.max").read_text()) > 1024**3
or quota / period > 1.5
or int((control / "pids.max").read_text()) > 128
):
raise ValueError("Check limits not enforced")
started = time.monotonic()
report = {
"schema": manifest["schema"],
"id": identifier,
"started_at": datetime.now(UTC).isoformat(),
"monotonic": started,
"artifact_sha256": digest(artifact.read_bytes()),
}
env = {
"PATH": str(folder / "go/bin") + ":/usr/bin:/bin",
"GOENV": "off",
"GOCACHE": str(folder / "cache"),
"GOPATH": str(folder / "gopath"),
"GOTOOLCHAIN": "local",
"GOPROXY": "off",
"GOMAXPROCS": "2",
"GOMEMLIMIT": "512MiB",
"CGO_ENABLED": "0",
"GOWORK": "off",
}
# No SDK, USB, installed files, network fetches, or real host credentials.
result = subprocess.run(
[str(folder / "go/bin/go"), "run", "-p", "1", "check.go"],
cwd=folder,
env=env,
capture_output=True,
timeout=150,
)
(folder / "stdout").write_bytes(result.stdout)
(folder / "stderr").write_bytes(result.stderr)
report.update(
state="complete" if result.returncode == 0 else "error",
exit_code=result.returncode,
duration_seconds=time.monotonic() - started,
finished_at=datetime.now(UTC).isoformat(),
stdout_sha256=digest(result.stdout),
stderr_sha256=digest(result.stderr),
)
if result.returncode == 0:
report["checks"] = json.loads(result.stdout)
(folder / "report.json").write_text(json.dumps(report, indent=2) + "\n")
if result.returncode:
raise RuntimeError("Certificate interop check failed; inspect private report")
if __name__ == "__main__":
if sys.argv[1:] == ["--build"]:
build()
else:
run()
@@ -0,0 +1,67 @@
"""Run only synthetic X4 bootstrap checks in disposable unprivileged Ubuntu staging."""
import hashlib
import json
import os
import platform
import subprocess
import sys
import tempfile
import time
import zipfile
from datetime import UTC, datetime
from pathlib import Path
FILES = {
"apps/node-agent/packaging/insta360_profile.py",
"plugins/insta360-x4/tests/check_profile.py",
}
def main():
if os.geteuid() == 0 or platform.freedesktop_os_release().get("VERSION_ID") != "24.04":
raise ValueError("Run as an ordinary Ubuntu 24.04 user")
started, monotonic = datetime.now(UTC).isoformat(), time.monotonic()
with zipfile.ZipFile(sys.argv[0]) as archive:
manifest = json.loads(archive.read("manifest.json"))
if set(archive.namelist()) != FILES | {"__main__.py", "manifest.json"} or set(
manifest
) != FILES | {"__main__.py"}:
raise ValueError("Unexpected check artifact")
payload = {name: archive.read(name) for name in manifest}
if any(hashlib.sha256(raw).hexdigest() != manifest[name] for name, raw in payload.items()):
raise ValueError("Check artifact changed")
with tempfile.TemporaryDirectory(
prefix="mission-core-x4-profile-check-", dir="/var/tmp"
) as folder:
root = Path(folder)
for name in FILES:
target = root / name
target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
target.write_bytes(payload[name])
(root / "plugins/insta360-x4/build").mkdir(mode=0o700)
result = subprocess.run(
["/usr/bin/python3", "-I", str(root / "plugins/insta360-x4/tests/check_profile.py")],
capture_output=True,
text=True,
timeout=30,
)
print(
json.dumps(
{
"schema": "missioncore.node.x4-profile-check/v1",
"started_at": started,
"monotonic_started": monotonic,
"duration_seconds": time.monotonic() - monotonic,
"state": "complete" if result.returncode == 0 else "error",
"files": manifest,
"stdout": result.stdout,
"stderr": result.stderr,
}
)
)
return result.returncode
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,138 @@
"""D01: display only the latest failed X4 installer logs in an owner sudo TTY.
No package installation, service operation, camera command or permission change.
The owner terminal records stdout in a private user-owned diagnostic directory.
"""
import hashlib
import json
import os
import re
import stat
import subprocess
import sys
import time
from datetime import UTC, datetime
from pathlib import Path
STATE = Path("/var/lib/mission-core-node-profiles/insta360-x4")
def read_root(path, limit):
for parent in reversed(path.parents):
info = parent.lstat()
if not stat.S_ISDIR(info.st_mode) or info.st_uid or info.st_mode & 0o022:
raise ValueError("Untrusted diagnostic path")
fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW)
try:
info = os.fstat(fd)
if not stat.S_ISREG(info.st_mode) or info.st_uid or info.st_mode & 0o022:
raise ValueError("Untrusted diagnostic file")
data = os.read(fd, limit + 1)
if len(data) > limit:
raise ValueError("Diagnostic file exceeds bound")
return data
finally:
os.close(fd)
def report():
if os.geteuid() != 0:
raise ValueError("Use the owner terminal")
state = json.loads(read_root(STATE / "preparation.json", 65536))
run_id = state.get("run_id", "")
if state.get("state") != "error" or not re.fullmatch("[0-9a-f]{32}", run_id):
raise ValueError("No completed failed X4 profile")
logs = {}
for name in ("1.stdout", "1.stderr"):
data = read_root(STATE / run_id / name, 1024 * 1024)
logs[name] = {
"sha256": hashlib.sha256(data).hexdigest(),
"text": data.decode(errors="replace"),
}
print(
json.dumps(
{
"schema": "missioncore.node.x4-profile-diagnostic/v1",
"utc": datetime.now(UTC).isoformat(),
"monotonic": time.monotonic(),
"profile": state,
"logs": logs,
},
ensure_ascii=False,
),
flush=True,
)
def launch():
if os.geteuid() == 0:
raise ValueError("Launch as the desktop owner")
source = Path(__file__).resolve()
if not re.fullmatch(r"/var/tmp/mission-core-x4-diagnostic-[0-9a-f]{24}\.py", str(source)):
raise ValueError("Use the versioned diagnostic artifact")
raw = source.read_bytes()
if source.stem != "mission-core-x4-diagnostic-" + hashlib.sha256(raw).hexdigest()[:24]:
raise ValueError("Diagnostic artifact changed")
os.umask(0o077)
folder = source.with_suffix("")
folder.mkdir(mode=0o700, exist_ok=True)
info = folder.lstat()
if folder.is_symlink() or info.st_uid != os.getuid() or info.st_mode & 0o077:
raise ValueError("Diagnostic directory must be private")
script = folder / "read-report"
script.write_text(
"#!/bin/bash\nset -uo pipefail\numask 077\nprintf '%s\\n' "
"'Mission Core: чтение ошибки подготовки X4' "
"'Изменений системы и команд камеры не будет. Введите пароль Ubuntu.'\n"
"/usr/bin/sudo /usr/bin/python3 -I '"
+ str(source)
+ "' --read 2>&1 | /usr/bin/tee '"
+ str(folder / "report.json")
+ "'\nmc_x4_result=${PIPESTATUS[0]}\nprintf "
"'\\nКод завершения: %s\\nНажмите Enter, чтобы закрыть.\\n' "
'"$mc_x4_result"\nread -r mc_x4_close\nexit "$mc_x4_result"\n'
)
script.chmod(0o700)
env = dict(os.environ)
result = subprocess.run(
["/usr/bin/systemctl", "--user", "show-environment"],
capture_output=True,
text=True,
timeout=5,
check=True,
)
for line in result.stdout.splitlines():
key, separator, value = line.partition("=")
if separator and key in {
"DISPLAY",
"WAYLAND_DISPLAY",
"XDG_RUNTIME_DIR",
"DBUS_SESSION_BUS_ADDRESS",
"XAUTHORITY",
}:
env[key] = value
with (folder / "launcher.log").open("ab") as output:
subprocess.Popen(
[
"/usr/bin/gnome-terminal",
"--wait",
"--title=Mission Core · Диагностика X4",
"--",
str(script),
],
env=env,
stdout=output,
stderr=output,
start_new_session=True,
)
print(json.dumps({"directory": str(folder), "sha256": hashlib.sha256(raw).hexdigest()}))
if __name__ == "__main__":
if sys.argv[1:] == ["--read"]:
report()
elif sys.argv[1:] == ["--launch"]:
launch()
else:
sys.exit("Use --launch or --read")
@@ -0,0 +1,33 @@
"""Build-only pinned toolchains; never an operator installation prerequisite."""
import hashlib
import json
import os
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def main():
lock = json.loads((ROOT / "packaging/linux-toolchains.json").read_text())
output = ROOT / "build/linux-toolchains"
output.mkdir(parents=True, mode=0o700, exist_ok=True)
os.umask(0o077)
for item in lock["files"]:
target = output / item["name"]
if not target.exists():
with urllib.request.urlopen(item["url"], timeout=60) as response:
data = response.read(100 * 1024 * 1024 + 1)
if len(data) > 100 * 1024 * 1024 or hashlib.sha256(data).hexdigest() != item["sha256"]:
raise ValueError("Official toolchain differs from the pinned release")
with target.open("xb") as stream:
stream.write(data)
data = target.read_bytes()
if hashlib.sha256(data).hexdigest() != item["sha256"]:
raise ValueError("Cached toolchain hash mismatch")
print(json.dumps({"file": target.name, "bytes": len(data), "sha256": item["sha256"]}))
if __name__ == "__main__":
main()
@@ -0,0 +1,8 @@
{
"schema": "missioncore.node.bundled-model/v1",
"model_id": "insta360.x4",
"version": "0.1.3-3",
"revision": "b291418f2a404cbdafc57431",
"bytes": 58199970,
"sha256": "c10f9c1e7f8237d9df202bc29483d61f27280920d76611b1aa1c4f3a72332540"
}
@@ -0,0 +1,229 @@
"""Node-owned X4 bootstrap; the same fixed job serves local and paired Core UI.
This exists before the optional model package is installed. All package bytes
and hashes come from the Node release, never from a device or a client request.
"""
import fcntl
import hashlib
import json
import os
import platform
import re
import subprocess
import sys
import tempfile
import time
import uuid
from pathlib import Path
SHARE = Path("/usr/share/mission-core-node/profiles/insta360-x4")
STATE = Path("/var/lib/mission-core-node-profiles/insta360-x4")
PLUGIN_STATE = Path("/var/lib/mission-core-insta360")
UNIT = "mission-core-node-insta360-x4-prepare.service"
PACKAGE = "mission-core-insta360-x4"
def trusted(path, directory=False):
info = path.lstat()
if path.is_symlink() or info.st_uid != 0 or info.st_mode & 0o022:
raise RuntimeError("Небезопасный файл профиля камеры.")
if path.is_dir() != directory:
raise RuntimeError("Недопустимый файл профиля камеры.")
return path
def publish(value):
fd, name = tempfile.mkstemp(prefix=".preparation-", dir=STATE)
try:
with os.fdopen(fd, "w") as stream:
os.fchmod(stream.fileno(), 0o644)
json.dump(value, stream, ensure_ascii=False)
stream.flush()
os.fsync(stream.fileno())
target = STATE / "preparation.json"
if target.exists() or target.is_symlink():
trusted(target)
os.replace(name, target)
descriptor = os.open(STATE, os.O_RDONLY)
try:
os.fsync(descriptor)
finally:
os.close(descriptor)
finally:
if os.path.exists(name):
os.unlink(name)
def payload():
for path in (SHARE.parent, SHARE):
trusted(path, True)
value = json.loads(trusted(SHARE / "profile.json").read_text())
if value.get("schema") != "missioncore.node.bundled-model/v1":
raise RuntimeError("Неизвестный профиль камеры.")
version = value["version"]
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:-[1-9][0-9]*)?", version):
raise RuntimeError("Некорректная версия профиля камеры.")
if not re.fullmatch(r"[0-9a-f]{24}", value["revision"]):
raise RuntimeError("Некорректная версия драйвера.")
path = trusted(SHARE / (PACKAGE + "_" + version + "_amd64.deb"))
data = path.read_bytes()
if len(data) != value["bytes"] or hashlib.sha256(data).hexdigest() != value["sha256"]:
raise RuntimeError("Встроенный пакет камеры повреждён. Переустановите Mission Core Node.")
return value, path
def prepare():
for path in (STATE.parent, STATE):
path.mkdir(mode=0o755, exist_ok=True)
trusted(path, True)
fd = os.open(STATE / "prepare.lock", os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, "r+b") as lock:
trusted(STATE / "prepare.lock")
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
return prepare_locked()
def prepare_locked():
report = {
"schema": "missioncore.node.device-preparation/v1",
"model_id": "insta360.x4",
"run_id": uuid.uuid4().hex,
"started_at": time.time(),
"monotonic_started": time.monotonic(),
"state": "running",
"steps": [
{"id": key, "label": label, "state": "pending"}
for key, label in (
("platform", "Проверка совместимости системы"),
("payload", "Проверка встроенного пакета камеры"),
("package", "Установка драйвера камеры"),
("prepare", "Подготовка камеры"),
)
],
}
env = {
"PATH": "/usr/sbin:/usr/bin:/sbin:/bin",
"LANG": "C.UTF-8",
"DEBIAN_FRONTEND": "noninteractive",
}
evidence = STATE / report["run_id"]
evidence.mkdir(mode=0o700)
trusted(evidence, True)
sequence = 0
def run(command, timeout=60):
nonlocal sequence
sequence += 1
result = subprocess.run(command, capture_output=True, env=env, timeout=timeout)
for suffix, data in (("stdout", result.stdout), ("stderr", result.stderr)):
path = evidence / (str(sequence) + "." + suffix)
with path.open("xb") as stream:
os.fchmod(stream.fileno(), 0o600)
stream.write(data)
if result.returncode:
raise RuntimeError(
"Этап установки камеры не завершён. Повторите подготовку устройства."
)
return result.stdout.decode().strip()
publish(report)
try:
for step in report["steps"]:
step["state"] = "running"
publish(report)
if step["id"] == "platform":
release = platform.freedesktop_os_release()
if (release.get("ID"), release.get("VERSION_ID"), platform.machine()) != (
"ubuntu",
"24.04",
"x86_64",
):
raise RuntimeError("Этот профиль поддерживает Ubuntu 24.04 amd64.")
elif step["id"] == "payload":
bundle, path = payload()
report.update(revision=bundle["revision"], package_sha256=bundle["sha256"])
elif step["id"] == "package":
result = subprocess.run(
["/usr/bin/dpkg-query", "-W", "-f", "${Version}\t${Status}", PACKAGE],
env=env,
capture_output=True,
text=True,
timeout=10,
)
previous = result.stdout.strip().split("\t") if result.returncode == 0 else []
if (
previous
and subprocess.run(
[
"/usr/bin/dpkg",
"--compare-versions",
previous[0],
"gt",
bundle["version"],
],
env=env,
capture_output=True,
timeout=10,
).returncode
== 0
):
raise RuntimeError(
"Установлен более новый драйвер X4. Обновите Mission Core Node."
)
if previous != [bundle["version"], "install ok installed"]:
# Node already declares every OS dependency. Install only
# the hash-verified local archive: APT --no-download drops
# its local-file acquisition path on Ubuntu 24.04. dpkg
# retains dependency and package-lock checks, without a
# network acquisition or changes to unrelated packages.
# Model preinst/prerm retain recording/preview safety.
run(
[
"/usr/bin/dpkg",
"--install",
str(path),
],
timeout=None,
)
elif step["id"] == "prepare":
current = PLUGIN_STATE / "preparation.json"
# Package postinst prepares an existing installation during an
# upgrade. Avoid a second preparation while its SDK connects.
value = json.loads(trusted(current).read_text()) if current.exists() else {}
same_upgrade = (
previous != [bundle["version"], "install ok installed"]
and value.get("started_at", 0) >= report["started_at"]
and value.get("state") == "complete"
and value.get("revision") == bundle["revision"]
)
if not same_upgrade:
run(["/usr/bin/systemctl", "start", UNIT], timeout=200)
value = json.loads(trusted(current).read_text())
if value.get("state") != "complete" or value.get("revision") != bundle["revision"]:
raise RuntimeError("Подготовка драйвера камеры не подтверждена.")
step["state"] = "complete"
publish(report)
report["state"] = "complete"
except (OSError, ValueError, KeyError, RuntimeError, subprocess.SubprocessError) as error:
report["state"] = "error"
report["message"] = (
str(error)[:300]
if isinstance(error, RuntimeError)
else "Не удалось подготовить камеру."
)
for step in report["steps"]:
if step["state"] == "running":
step.update(state="error", message=report["message"])
elif step["state"] == "pending":
step["state"] = "blocked"
report["duration_seconds"] = time.monotonic() - report["monotonic_started"]
publish(report)
return report["state"] == "complete"
if __name__ == "__main__":
if os.geteuid() or sys.argv[1:]:
sys.exit(1)
os.umask(0o022)
sys.exit(0 if prepare() else 1)
@@ -0,0 +1,14 @@
#!/bin/bash
set -euo pipefail
umask 077
mc_node_release_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
if [ ! -t 0 ]; then
exec /usr/bin/gnome-terminal --wait --title="Mission Core Node" -- "$mc_node_release_dir/install"
fi
printf '%s\n' 'Mission Core Node' 'Обновление приложения и встроенных пакетов камер на этом Ubuntu-компьютере.' 'Подготовка X4 выполняется затем из списка устройств в Node или Core.' 'Пароль администратора вводится только в этом локальном окне Ubuntu.'
set +e
/usr/bin/sudo /usr/bin/python3 -I "$mc_node_release_dir/install_release.py" 2>&1 | /usr/bin/tee -a "$mc_node_release_dir/install-output.log"
mc_node_install_result=${PIPESTATUS[0]}
printf '\nКод завершения: %s\nНажмите Enter, чтобы закрыть окно.\n' "$mc_node_install_result"
read -r mc_node_close
exit "$mc_node_install_result"
@@ -0,0 +1,296 @@
"""Fixed Node release installation, authenticated only in the owner's Ubuntu TTY."""
import hashlib
import http.client
import json
import os
import platform
import pwd
import re
import signal
import subprocess
import sys
import time
import uuid
from datetime import UTC, datetime
from pathlib import Path
ROOT = Path("/var/tmp/mission-core-node-installs")
SERVICES = (
"mission-core-node.service",
"mission-core-k1.service",
"mission-core-realsense.service",
)
def private(path):
path.mkdir(mode=0o700, exist_ok=True)
info = path.lstat()
if path.is_symlink() or not path.is_dir() or info.st_uid != 0 or info.st_mode & 0o077:
raise ValueError("Installer evidence directory is not private and root-owned")
def finish_legacy_monitor_pager():
"""Close only the known 0.8.17 postinst pager; never signal apt/dpkg/SQL."""
query = ["/usr/bin/dpkg-query", "-W", "-f", "${Version} ${Status}", "mission-core-node"]
state = subprocess.run(query, capture_output=True, text=True, timeout=5).stdout.strip()
if state != "0.8.17 install ok half-configured":
return {"required": False}
monitor_uid = pwd.getpwnam("mission-core-monitor").pw_uid
processes = {}
for path in Path("/proc").glob("[0-9]*"):
try:
fields = (path / "stat").read_text().split(") ", 1)[1].split()
args = (path / "cmdline").read_bytes().rstrip(b"\0").decode().split("\0")
processes[int(path.name)] = {
"parent": int(fields[1]),
"start": fields[19],
"args": args,
"uid": path.stat().st_uid,
}
except (OSError, ValueError, IndexError, UnicodeError):
continue
def parent(row):
return processes.get(row.get("parent"), {})
selected = []
for pid, row in processes.items():
if row["args"] != ["pager"] or row["uid"] != monitor_uid:
continue
shell = parent(row)
sql, user = parent(shell), parent(parent(shell))
setup, post = parent(user), parent(parent(user))
dpkg, apt = parent(post), parent(parent(post))
if (
shell.get("args") != ["sh", "-c", "--", "pager"]
or sql.get("args")
!= [
"/usr/lib/postgresql/16/bin/psql",
"-X",
"-v",
"ON_ERROR_STOP=1",
"-h",
"/run/mission-core-monitor-db",
"-p",
"5433",
"-d",
"mission_core_monitor",
"-f",
"/usr/lib/mission-core-node/monitor/schema.sql",
]
or setup.get("args") != ["/bin/sh", "/usr/lib/mission-core-node/setup-monitor"]
or post.get("args", [])[:3]
!= ["/bin/sh", "/var/lib/dpkg/info/mission-core-node.postinst", "configure"]
or dpkg.get("args", [""])[0] != "/usr/bin/dpkg"
or apt.get("args", [])[:4] != ["/usr/bin/apt-get", "install", "-y", "--no-remove"]
or len(apt["args"]) != 5
or not re.fullmatch(
r"/var/tmp/mission-core-node-installs/[0-9a-f]{32}/"
r"mission-core-node_0\.8\.17_amd64\.deb",
apt["args"][4],
)
):
continue
old_package = Path(apt["args"][4])
if hashlib.sha256(old_package.read_bytes()).hexdigest() != (
"f929c440e4965f728aa6cd00eb122e7cad09e5ade82da763cc36661f50ec66b5"
):
continue
selected.append((pid, row["start"]))
if len(selected) != 1:
raise RuntimeError("Не подтверждён известный просмотрщик старого установщика.")
pid, started = selected[0]
descriptor = os.pidfd_open(pid)
try:
current = (Path("/proc") / str(pid) / "stat").read_text().split(") ", 1)[1].split()
if current[19] != started:
raise RuntimeError("Процесс просмотрщика изменился.")
print(
"Закрываем зависший просмотрщик старого установщика; APT продолжает работу.", flush=True
)
signal.pidfd_send_signal(descriptor, signal.SIGTERM)
finally:
os.close(descriptor)
deadline = time.monotonic() + 45
while time.monotonic() < deadline:
state = subprocess.run(query, capture_output=True, text=True, timeout=5).stdout.strip()
if state == "0.8.17 install ok installed":
return {"required": True, "pager_pid": pid, "old_package_configured": True}
time.sleep(1)
raise RuntimeError("Старая транзакция ещё не завершилась; она не прерывалась.")
def main():
if os.geteuid() or sys.argv[1:]:
raise ValueError("Use the release's local Ubuntu installer")
release = platform.freedesktop_os_release()
if (release.get("ID"), release.get("VERSION_ID"), platform.machine()) != (
"ubuntu",
"24.04",
"x86_64",
):
raise ValueError("This package requires Ubuntu 24.04 amd64")
os.umask(0o077)
source = Path(__file__).resolve().parent
raw = (source / "release.json").read_bytes()
manifest = json.loads(raw)
version = manifest["version"]
if (
manifest.get("schema") != "missioncore.node.owner-release/v1"
or manifest.get("qualification_profile") != "node-package-and-local-ui"
or not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:-[1-9][0-9]*)?", version)
):
raise ValueError("Unknown Node release")
package = "mission-core-node_" + version + "_amd64.deb"
if set(manifest["files"]) != {package, "install", "install_release.py"}:
raise ValueError("Unexpected installer contents")
contents = {}
for name, expected in manifest["files"].items():
data = (source / name).read_bytes()
if len(data) != expected["bytes"] or hashlib.sha256(data).hexdigest() != expected["sha256"]:
raise ValueError("Installer payload changed")
contents[name] = data
private(ROOT)
folder = ROOT / uuid.uuid4().hex
private(folder)
(folder / package).write_bytes(contents[package])
report = {
"schema": "missioncore.node.install-run/v1",
"session_id": folder.name,
"release_sha256": hashlib.sha256(raw).hexdigest(),
"package_sha256": manifest["files"][package]["sha256"],
"started_at": datetime.now(UTC).isoformat(),
"monotonic_started": time.monotonic(),
"state": "running",
"scope": "Node-package-and-UI-only; X4-prepare-remains-in-application",
"steps": [],
}
env = {
"PATH": "/usr/sbin:/usr/bin:/sbin:/bin",
"LANG": "C.UTF-8",
"DEBIAN_FRONTEND": "noninteractive",
"PAGER": "/bin/cat",
"PSQL_PAGER": "/bin/cat",
}
def publish():
(folder / "report.json").write_text(json.dumps(report, indent=2) + "\n")
def run(name, command, timeout=60):
print("Установка Node: " + name, flush=True)
item = {"id": name, "state": "running", "started_at": datetime.now(UTC).isoformat()}
report["steps"].append(item)
publish()
result = subprocess.run(command, env=env, capture_output=True, timeout=timeout)
for suffix, data in (("stdout", result.stdout), ("stderr", result.stderr)):
(folder / (name + "." + suffix)).write_bytes(data)
item.update(
state="complete" if result.returncode == 0 else "error", exit_code=result.returncode
)
publish()
if result.returncode:
raise RuntimeError("Не завершён этап установки: " + name)
return result.stdout.decode().strip()
publish()
try:
report["legacy_pager_recovery"] = finish_legacy_monitor_pager()
publish()
report["services_before"] = run(
"baseline",
[
"/usr/bin/systemctl",
"show",
*SERVICES,
"-p",
"Id",
"-p",
"ActiveState",
"-p",
"NRestarts",
],
)
# APT refuses package removal and downgrade without explicit flags.
# Its transaction is never killed by an observer timeout.
run(
"apt-plan",
["/usr/bin/apt-get", "--simulate", "--no-remove", "install", str(folder / package)],
)
run(
"package",
[
"/usr/bin/apt-get",
"-o",
"Dpkg::Use-Pty=0",
"-o",
"DPkg::Lock::Timeout=45",
"install",
"-y",
"--no-remove",
str(folder / package),
],
timeout=None,
)
installed = run(
"installed",
["/usr/bin/dpkg-query", "-W", "-f", "${Version}\t${Status}", "mission-core-node"],
)
if installed != version + "\tinstall ok installed":
raise RuntimeError("Версия установленного Node не подтверждена.")
run(
"node-service",
["/usr/bin/systemctl", "is-active", "--quiet", "mission-core-node.service"],
)
deadline = time.monotonic() + 30
while True:
client = http.client.HTTPConnection("127.0.0.1", 8780, timeout=2)
try:
client.request("GET", "/")
response = client.getresponse()
body = response.read(262145)
if response.status == 200 and len(body) <= 262144 and b"<html" in body.lower():
report["ui_ready"] = True
break
except (OSError, http.client.HTTPException):
pass
finally:
client.close()
if time.monotonic() >= deadline:
raise RuntimeError("Пакет установлен, но локальный интерфейс Node не ответил.")
time.sleep(1)
report["services_after"] = run(
"services-after",
[
"/usr/bin/systemctl",
"show",
*SERVICES,
"-p",
"Id",
"-p",
"ActiveState",
"-p",
"NRestarts",
],
)
report["state"] = "complete"
except Exception as error:
report.update(state="error", error=str(error)[:500])
finally:
report.update(
finished_at=datetime.now(UTC).isoformat(),
duration_seconds=time.monotonic() - report["monotonic_started"],
)
publish()
(folder / package).unlink(missing_ok=True)
print("MISSION_CORE_NODE_RESULT " + json.dumps(report, ensure_ascii=False), flush=True)
if report["state"] != "complete":
raise RuntimeError(report["error"])
print(
"Mission Core Node обновлён. X4 можно подготовить из списка устройств в Node или Core.",
flush=True,
)
if __name__ == "__main__":
main()
@@ -0,0 +1,18 @@
{
"schema": "missioncore.node.build-toolchains/v1",
"platform": "linux-amd64",
"files": [
{
"name": "go1.26.8.linux-amd64.tar.gz",
"url": "https://go.dev/dl/go1.26.8.linux-amd64.tar.gz",
"sha256": "d0f743b33e8d8945e6b1f432edd15785c70507121d6e2a723b21285eddf8b57b",
"directory": "go"
},
{
"name": "node-v24.9.0-linux-x64.tar.xz",
"url": "https://nodejs.org/dist/v24.9.0/node-v24.9.0-linux-x64.tar.xz",
"sha256": "f52ec50e959d72d5c680d9731420b2661cd2a8070e94c7369b6ddfcd8b7278be",
"directory": "node-v24.9.0-linux-x64"
}
]
}
@@ -0,0 +1,186 @@
"""Unprivileged self-verifying build, bounded by a transient user cgroup."""
import fcntl
import hashlib
import json
import os
import platform
import shutil
import subprocess
import sys
import time
import zipfile
from datetime import UTC, datetime
from pathlib import Path, PurePosixPath
ROOT = Path("/var/tmp/mission-core-node-builds")
def digest(data):
return hashlib.sha256(data).hexdigest()
def private(path):
path.mkdir(mode=0o700, exist_ok=True)
info = path.lstat()
if (
path.is_symlink()
or not path.is_dir()
or info.st_uid != os.geteuid()
or info.st_mode & 0o077
):
raise RuntimeError("Build staging is not private and owned")
def main():
if os.geteuid() == 0 or (platform.system(), platform.machine()) != ("Linux", "x86_64"):
raise RuntimeError("Use the unprivileged Ubuntu build account")
if sys.argv[1:] not in ([], ["--clean"]):
raise ValueError("No arbitrary build commands or paths are accepted")
os.umask(0o077)
started, monotonic = datetime.now(UTC).isoformat(), time.monotonic()
artifact = Path(sys.argv[0]).resolve()
with zipfile.ZipFile(artifact) as archive:
raw = archive.read("source.json")
manifest = json.loads(raw)
if manifest["schema"] != "missioncore.node.linux-build-source/v1":
raise ValueError("Unsupported source artifact")
if digest(archive.read("__main__.py")) != manifest["entrypoint_sha256"]:
raise ValueError("Build entry point changed")
identifier = digest(raw)[:24]
private(ROOT)
folder = ROOT / identifier
private(folder)
with (folder / "build.lock").open("a") as handle:
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
if sys.argv[1:]:
shutil.rmtree(folder)
print(json.dumps({"cleaned": identifier}))
return
if (folder / "result.tar.gz").exists():
print(
json.dumps(
{
"result": str(folder / "result.tar.gz"),
"sha256": digest((folder / "result.tar.gz").read_bytes()),
"reused": True,
}
)
)
return
if (folder / "source").exists():
raise RuntimeError("Preserve prior failure evidence and use --clean before retry")
if shutil.disk_usage(folder).free < 6 * 1024**3:
raise RuntimeError("Build requires 6 GiB free temporary disk space")
admitted = {"source.json", "__main__.py"} | {
"source/" + name for name in manifest["files"]
}
if set(archive.namelist()) != admitted or len(archive.namelist()) != len(admitted):
raise ValueError("Unexpected source contents")
for name, expected in manifest["files"].items():
path = PurePosixPath(name)
if (
path.is_absolute()
or ".." in path.parts
or path.as_posix() != name
or "\\" in name
):
raise ValueError("Unsafe source path")
entry = archive.getinfo("source/" + name)
if entry.file_size != expected["bytes"] or entry.file_size > 100 * 1024**2:
raise ValueError("Invalid source size")
data = archive.read(entry)
if digest(data) != expected["sha256"] or expected["mode"] not in (0o644, 0o755):
raise ValueError("Invalid source bytes or mode")
target = folder / "source" / name
target.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
target.write_bytes(data)
target.chmod(expected["mode"])
(folder / "source.json").write_bytes(raw)
report = {
"schema": "missioncore.node.linux-build-run/v1",
"id": identifier,
"started_at": started,
"monotonic_started": monotonic,
"state": "running",
"artifact_sha256": digest(artifact.read_bytes()),
"scope": "Node-Core-build-and-synthetic-tests",
"resource_limits": {
"memory_bytes": 3 * 1024**3,
"cpu_percent": 150,
"tasks": 256,
"seconds": 1200,
},
}
(folder / "report.json").write_text(json.dumps(report, indent=2) + "\n")
job = (
folder
/ "source"
/ manifest["repository"]
/ "apps/node-agent/packaging/linux_build_job.py"
)
try:
result = subprocess.run(
[
"/usr/bin/systemd-run",
"--user",
"--quiet",
"--collect",
"--wait",
"--pipe",
"--unit=mission-core-node-build-" + identifier,
"--property=MemoryMax=3G",
"--property=CPUQuota=150%",
"--property=TasksMax=256",
"--property=RuntimeMaxSec=1200",
"--property=TimeoutStopSec=10",
"--property=KillMode=control-group",
"--property=Nice=10",
"/usr/bin/python3",
"-I",
str(job),
str(folder),
],
capture_output=True,
)
(folder / "job.stdout").write_bytes(result.stdout)
(folder / "job.stderr").write_bytes(result.stderr)
if result.returncode:
raise RuntimeError("Node/Core build failed; inspect the private attempt report")
report.update(
state="complete",
finished_at=datetime.now(UTC).isoformat(),
duration_seconds=time.monotonic() - monotonic,
)
(folder / "report.json").write_text(json.dumps(report, indent=2) + "\n")
# The job creates the artifact list; these fixed files only
# are collected after its cgroup has exited successfully.
import tarfile
with tarfile.open(folder / "result.tar.gz", "w:gz") as result_archive:
for path in sorted((folder / "output").iterdir()):
if not path.is_file() or path.is_symlink():
raise ValueError("Unexpected build result")
result_archive.add(path, arcname=path.name, recursive=False)
result_archive.add(folder / "report.json", arcname="build-report.json")
print(
json.dumps(
{
"result": str(folder / "result.tar.gz"),
"state": "complete",
"sha256": digest((folder / "result.tar.gz").read_bytes()),
}
)
)
except Exception:
report.update(
state="error",
finished_at=datetime.now(UTC).isoformat(),
duration_seconds=time.monotonic() - monotonic,
)
(folder / "report.json").write_text(json.dumps(report, indent=2) + "\n")
raise
if __name__ == "__main__":
main()
@@ -0,0 +1,359 @@
"""Fixed sequential Node/Core build inside the source artifact's user cgroup."""
import hashlib
import json
import os
import re
import shutil
import subprocess
import sys
import tarfile
import time
from datetime import UTC, datetime
from pathlib import Path, PurePosixPath
def main():
if os.geteuid() == 0 or len(sys.argv) != 2:
raise ValueError("Use the versioned unprivileged build entry point")
folder = Path(sys.argv[1])
if folder.parent != Path("/var/tmp/mission-core-node-builds") or not re.fullmatch(
r"[0-9a-f]{24}", folder.name
):
raise ValueError("Unknown build staging")
manifest = json.loads((folder / "source.json").read_text())
if manifest.get("profile", "node-core") not in ("node-core", "node-only"):
raise ValueError("Unknown build profile")
source = folder / "source"
repo, dg = source / manifest["repository"], source / "NODEDC_DESIGN_GUIDELINE"
node = repo / "apps/node-agent"
if Path(__file__).resolve() != node / "packaging/linux_build_job.py":
raise ValueError("Build source path changed")
cgroup = next(
line.split(":", 2)[2]
for line in Path("/proc/self/cgroup").read_text().splitlines()
if line.startswith("0::")
)
control = Path("/sys/fs/cgroup") / cgroup.lstrip("/")
memory = (control / "memory.max").read_text().strip()
cpu, period = (control / "cpu.max").read_text().split()
tasks = (control / "pids.max").read_text().strip()
if (
memory == "max"
or int(memory) > 3 * 1024**3
or cpu == "max"
or int(cpu) / int(period) > 1.5
or tasks == "max"
or int(tasks) > 256
):
raise RuntimeError("Build resource limits are not enforced")
tools = folder / "toolchains"
tools.mkdir(mode=0o700)
for entry in manifest["toolchains"]["files"]:
path = node / "build/linux-toolchains" / entry["name"]
if hashlib.sha256(path.read_bytes()).hexdigest() != entry["sha256"]:
raise ValueError("Toolchain changed before extraction")
with tarfile.open(path) as archive:
members = archive.getmembers()
if len(members) > 40000 or sum(item.size for item in members) > 1024**3:
raise ValueError("Toolchain exceeds extraction budget")
for item in members:
name = PurePosixPath(item.name)
if (
name.is_absolute()
or ".." in name.parts
or not name.parts
or name.parts[0] != entry["directory"]
):
raise ValueError("Unexpected toolchain contents")
# Python's data filter also rejects links escaping the owned tree,
# devices, FIFOs and privilege-bearing modes in upstream archives.
archive.extractall(tools, members=members, filter="data")
go = tools / "go/bin/go"
nodejs = tools / "node-v24.9.0-linux-x64/bin/node"
npm = tools / "node-v24.9.0-linux-x64/lib/node_modules/npm/bin/npm-cli.js"
output = folder / "output"
output.mkdir(mode=0o700)
temporary = folder / "temporary"
temporary.mkdir(mode=0o700)
env = {
"PATH": str(nodejs.parent) + ":" + str(go.parent) + ":/usr/bin:/bin",
"LANG": "C.UTF-8",
"PYTHONDONTWRITEBYTECODE": "1",
"TMPDIR": str(temporary),
"GOTOOLCHAIN": "local",
"GOMAXPROCS": "2",
"GOMEMLIMIT": "768MiB",
"GOCACHE": str(folder / "go-cache"),
"GOPATH": str(folder / "go-path"),
"GOMODCACHE": str(folder / "go-mod"),
"npm_config_cache": str(folder / "npm-cache"),
"npm_config_audit": "false",
"npm_config_fund": "false",
"npm_config_update_notifier": "false",
"NODE_OPTIONS": "--max-old-space-size=1024",
}
report = {
"schema": "missioncore.node.linux-build-jobs/v1",
"started_at": datetime.now(UTC).isoformat(),
"monotonic_started": time.monotonic(),
"state": "running",
"jobs": [],
"limits_observed": {"memory.max": memory, "cpu.max": [cpu, period], "pids.max": tasks},
}
def publish():
(output / "qualification.json").write_text(json.dumps(report, indent=2) + "\n")
def run(name, command, cwd=repo, overrides=None, timeout=300):
started = time.monotonic()
item = {"id": name, "started_at": datetime.now(UTC).isoformat(), "state": "running"}
report["jobs"].append(item)
publish()
try:
result = subprocess.run(
command,
cwd=cwd,
env={**env, **(overrides or {})},
capture_output=True,
timeout=timeout,
)
except subprocess.TimeoutExpired as error:
(output / (name + ".stdout")).write_bytes(error.stdout or b"")
(output / (name + ".stderr")).write_bytes(error.stderr or b"")
item.update(
state="error", reason="timeout", duration_seconds=time.monotonic() - started
)
publish()
raise
(output / (name + ".stdout")).write_bytes(result.stdout)
(output / (name + ".stderr")).write_bytes(result.stderr)
item.update(
state="complete" if result.returncode == 0 else "error",
exit_code=result.returncode,
duration_seconds=time.monotonic() - started,
)
publish()
if result.returncode:
raise RuntimeError(name + " failed")
return result.stdout.decode().strip()
def app_dependencies(app, name):
lock = json.loads((app / "package-lock.json").read_text())
for value in lock["packages"].values():
resolved = value.get("resolved", "")
if "://" in resolved and not resolved.startswith("https://registry.npmjs.org/"):
raise ValueError("An npm dependency is outside the locked public registry")
command = [
str(nodejs),
str(npm),
"ci",
"--ignore-scripts",
"--no-audit",
"--no-fund",
"--prefer-offline",
"--maxsockets=4",
"--fetch-timeout=20000",
"--fetch-retries=1",
"--fetch-retry-mintimeout=1000",
"--fetch-retry-maxtimeout=2000",
]
for attempt in range(1, 4):
step = name + "-dependencies-" + str(attempt)
try:
run(step, command, cwd=app, timeout=45)
break
except subprocess.TimeoutExpired:
# npm can stall after a TLS download without surfacing its
# fetch timeout. Its killed process cannot overlap this retry.
if attempt == 3:
raise
time.sleep(1)
except RuntimeError:
error = (output / (step + ".stderr")).read_text()
if attempt == 3 or not any(
code in error for code in ("ETIMEDOUT", "ECONNRESET", "EAI_AGAIN")
):
raise
# Retry only a failed network fetch, retaining this attempt's
# integrity-checked cache and every failed command log.
time.sleep(1)
try:
if run("go-version", [str(go), "version"]).split()[2] != "go1.26.8":
raise ValueError("Unexpected Go toolchain")
if run("node-version", [str(nodejs), "--version"]) != "v24.9.0":
raise ValueError("Unexpected Node.js toolchain")
app_dependencies(dg, "design-guideline")
run("design-guideline-build", [str(nodejs), str(npm), "run", "build:packages"], cwd=dg)
run(
"design-guideline-typecheck",
[str(nodejs), str(npm), "run", "typecheck", "--workspaces", "--if-present"],
cwd=dg,
)
run(
"design-guideline-registry", [str(nodejs), str(npm), "run", "validate:registry"], cwd=dg
)
run(
"design-guideline-loading-tests",
[str(nodejs), str(npm), "run", "test:activity-indicator"],
cwd=dg,
)
run(
"design-guideline-catalog",
[str(nodejs), str(npm), "run", "build", "--workspace", "@nodedc/ui-catalog"],
cwd=dg,
overrides={"NODE_OPTIONS": "--max-old-space-size=2048"},
)
ui = node / "ui"
app_dependencies(ui, "node-ui")
run(
"node-ui-tests",
[
str(nodejs),
"--test",
"--test-concurrency=1",
*[str(path) for path in sorted((ui / "test").glob("*.test.mjs"))],
],
cwd=ui,
)
run("node-ui-build", [str(nodejs), str(npm), "run", "build"], cwd=ui)
assets = node / "web/dist"
if assets.exists():
shutil.rmtree(assets)
shutil.copytree(ui / "dist", assets)
# The real UI must exist before Go compiles web/assets.go's embed.
# Tests use private synthetic USB roots, never the Mini's inventory.
run(
"node-go-format-diff",
[
str(go.parent / "gofmt"),
"-d",
*[str(path) for path in sorted((node / "internal/node").glob("pairing*.go"))],
],
cwd=node,
)
run(
"node-go-tests",
[str(go), "test", "-race", "-p", "1", "./...", "-count=1"],
cwd=node,
overrides={"CGO_ENABLED": "1"},
timeout=480,
)
sys.path.insert(0, str(node / "packaging"))
from build_deb import BINARY_VERSION, VERSION, build
binary = node / "build/node-agent-linux-amd64"
run(
"node-binary",
[
str(go),
"build",
"-p",
"1",
"-trimpath",
"-ldflags=-s -w -X main.version=" + BINARY_VERSION,
"-o",
str(binary),
"./cmd/node-agent",
],
cwd=node,
overrides={"CGO_ENABLED": "0", "GOOS": "linux", "GOARCH": "amd64"},
)
provenance = {
"schema": "missioncore.node.source-provenance/v1",
"version": VERSION,
"base_commit": manifest["base_commit"],
"design_guideline_commit": manifest["design_guideline_commit"],
"source_manifest_sha256": hashlib.sha256(
(folder / "source.json").read_bytes()
).hexdigest(),
"toolchains": manifest["toolchains"],
"files": manifest["files"],
}
(node / "build/provenance.json").write_text(json.dumps(provenance, indent=2) + "\n")
package = output / ("mission-core-node_" + VERSION + "_amd64.deb")
build(binary, package)
if manifest.get("profile") == "node-only":
report.update(
state="complete",
profile="node-only",
finished_at=datetime.now(UTC).isoformat(),
duration_seconds=time.monotonic() - report["monotonic_started"],
memory_peak_bytes=int((control / "memory.peak").read_text()),
)
report["artifacts"] = {
package.name: {
"bytes": package.stat().st_size,
"sha256": hashlib.sha256(package.read_bytes()).hexdigest(),
}
}
publish()
return
core = repo / "apps/control-station"
app_dependencies(core, "core")
run(
"core-architecture",
[str(nodejs), "--test", "test/applicationArchitecture.test.mjs"],
cwd=core,
)
run("core-typecheck", [str(nodejs), str(npm), "run", "typecheck"], cwd=core)
# This existing K1 integration case generates RRD via a developer
# Python/Rerun environment. That environment is not a Node/X4 build
# dependency. Keep every other test; report this acceptance gap.
report["unqualified_tests"] = [
{
"name": "native RRD crosses many RTC fragments "
"and reaches decoder once, byte-for-byte",
"reason": "Python/Rerun fixture generator is outside the Node/X4 build inputs",
}
]
run(
"core-tests",
[
str(nodejs),
"--test",
"--test-concurrency=1",
"--test-skip-pattern=^native RRD crosses many RTC fragments "
"and reaches decoder once, byte-for-byte$",
*[str(path) for path in sorted((core / "test").glob("*.test.mjs"))],
],
cwd=core,
timeout=480,
)
run(
"core-build",
[str(nodejs), str(npm), "run", "build"],
cwd=core,
overrides={"NODE_OPTIONS": "--max-old-space-size=2048"},
)
with tarfile.open(output / "core-dist.tar.gz", "w:gz") as archive:
for path in sorted((core / "dist").rglob("*")):
if path.is_file():
archive.add(path, arcname=str(path.relative_to(core / "dist")), recursive=False)
report.update(
state="complete",
finished_at=datetime.now(UTC).isoformat(),
duration_seconds=time.monotonic() - report["monotonic_started"],
memory_peak_bytes=int((control / "memory.peak").read_text()),
)
report["artifacts"] = {
path.name: {
"bytes": path.stat().st_size,
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
}
for path in (package, output / "core-dist.tar.gz")
}
publish()
except Exception:
report.update(
state="error",
finished_at=datetime.now(UTC).isoformat(),
duration_seconds=time.monotonic() - report["monotonic_started"],
)
publish()
raise
if __name__ == "__main__":
main()
@@ -0,0 +1,10 @@
[Unit]
Description=Mission Core fixed bundled Insta360 X4 profile
After=systemd-udevd.service
[Service]
Type=oneshot
ExecStart=/usr/bin/python3 -I /usr/lib/mission-core-node/insta360_profile.py
# Never kill dpkg in the middle of package configuration. The requesting Node
# operation has its own deadline; an expired observer cannot cancel this job.
TimeoutStartSec=infinity
UMask=0022
+4
View File
@@ -14,6 +14,10 @@ if [ "$1" = install ] || [ "$1" = upgrade ]; then
exit 1
fi
fi
mc_node_x4_job=$(systemctl show --property=ActiveState --value mission-core-node-insta360-x4-profile.service 2>/dev/null || true)
case "$mc_node_x4_job" in
active|activating) echo "Mission Core Node: дождитесь завершения подготовки X4." >&2; exit 1 ;;
esac
mc_node_device_job=$(systemctl show --property=ActiveState --value mission-core-node-realsense-prepare.service 2>/dev/null || true)
case "$mc_node_device_job" in
active|activating) echo "Mission Core Node: дождитесь завершения подготовки устройства." >&2; exit 1 ;;
+4
View File
@@ -13,6 +13,10 @@ if [ -d /run/systemd/system ]; then
exit 1
fi
fi
mc_node_x4_job=$(systemctl show --property=ActiveState --value mission-core-node-insta360-x4-profile.service 2>/dev/null || true)
case "$mc_node_x4_job" in
active|activating) echo "Mission Core Node: дождитесь завершения подготовки X4." >&2; exit 1 ;;
esac
mc_node_device_job=$(systemctl show --property=ActiveState --value mission-core-node-realsense-prepare.service 2>/dev/null || true)
case "$mc_node_device_job" in
active|activating) echo "Mission Core Node: дождитесь завершения подготовки устройства." >&2; exit 1 ;;
@@ -0,0 +1,100 @@
"""Repack qualified N08: only disable psql paging in the monitoring installer."""
import hashlib
import io
import json
import sys
import tarfile
from pathlib import Path
NODE = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(NODE.parents[1] / "scripts/packaging"))
from debian import package # noqa: E402
BASE = "f929c440e4965f728aa6cd00eb122e7cad09e5ade82da763cc36661f50ec66b5"
TARGET = "usr/lib/mission-core-node/setup-monitor"
def main():
original = NODE / "build/qualified-n08/mission-core-node_0.8.17_amd64.deb"
raw = original.read_bytes()
if hashlib.sha256(raw).hexdigest() != BASE or raw[:8] != b"!<arch>\n":
raise ValueError("The qualified Node package changed")
entries, cursor = {}, 8
while cursor < len(raw):
header = raw[cursor : cursor + 60]
if header[58:] != b"`\n":
raise ValueError("Invalid Debian archive")
name, size = header[:16].decode().strip().rstrip("/"), int(header[48:58])
if name in entries:
raise ValueError("Duplicate archive member")
entries[name] = raw[cursor + 60 : cursor + 60 + size]
cursor += 60 + size + size % 2
if set(entries) != {"debian-binary", "control.tar.gz", "data.tar.gz"}:
raise ValueError("Unexpected Debian contents")
def files(name):
result = []
with tarfile.open(fileobj=io.BytesIO(entries[name]), mode="r:gz") as archive:
for member in archive.getmembers():
if member.isdir():
continue
if not member.isfile() or member.uid or member.gid:
raise ValueError("Unexpected package member")
result.append((member.name, archive.extractfile(member).read(), member.mode))
return result
controls, data = files("control.tar.gz"), files("data.tar.gz")
control_count = sum(name == "control" for name, _, _ in controls)
if control_count != 1:
raise ValueError("Unexpected control metadata")
controls = [
(
name,
value.replace(b"Version: 0.8.17\n", b"Version: 0.8.17-1\n")
if name == "control"
else value,
mode,
)
for name, value, mode in controls
]
fixed = (NODE / "packaging/setup-monitor").read_bytes()
originals = [value for name, value, _ in data if name == TARGET]
if len(originals) != 1 or fixed.count(b" -P pager=off") != 3:
raise ValueError("Unexpected monitor patch")
if fixed.replace(b" -P pager=off", b"") != originals[0]:
raise ValueError("This recovery admits only the three pager flags")
data = [(name, fixed if name == TARGET else value, mode) for name, value, mode in data]
proof = {
"schema": "missioncore.node.packaging-revision/v1",
"version": "0.8.17-1",
"qualified_base_sha256": BASE,
"changed_files": [TARGET],
"binary_ui_and_model_packages_unchanged": True,
"setup_monitor_sha256": hashlib.sha256(fixed).hexdigest(),
}
data.append(
(
"usr/share/doc/mission-core-node/packaging-revision.json",
(json.dumps(proof, indent=2) + "\n").encode(),
0o644,
)
)
folder = NODE / "build/qualified-n08-r2"
folder.mkdir(mode=0o700)
path = folder / "mission-core-node_0.8.17-1_amd64.deb"
path.write_bytes(package(controls, data))
evidence = json.loads((NODE / "build/qualified-n08/qualification.json").read_text())
evidence["packaging_revision"] = proof
evidence["artifacts"] = {
path.name: {
"bytes": path.stat().st_size,
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
}
}
(folder / "qualification.json").write_text(json.dumps(evidence, indent=2) + "\n")
print(json.dumps({"folder": str(folder), "artifacts": evidence["artifacts"]}))
if __name__ == "__main__":
main()
@@ -0,0 +1,125 @@
"""P07: deliver the qualified X4 streaming fix without recompiling Node or its UI."""
import hashlib
import io
import json
import sys
import tarfile
from pathlib import Path
NODE = Path(__file__).resolve().parents[1]
REPO = NODE.parents[1]
sys.path.insert(0, str(REPO / "scripts/packaging"))
from debian import package # noqa: E402
BASE = "00f4baefd0c6f4e8f56bbf881329dfd36935e2334f35e2d17eb685a969d4c181"
def main():
original = NODE / "build/qualified-n08-r4/mission-core-node_0.8.17-3_amd64.deb"
raw = original.read_bytes()
if hashlib.sha256(raw).hexdigest() != BASE or raw[:8] != b"!<arch>\n":
raise ValueError("Qualified Node package changed")
entries, cursor = {}, 8
while cursor < len(raw):
header = raw[cursor : cursor + 60]
name, size = header[:16].decode().strip().rstrip("/"), int(header[48:58])
if header[58:] != b"`\n" or name in entries:
raise ValueError("Invalid archive")
entries[name] = raw[cursor + 60 : cursor + 60 + size]
cursor += 60 + size + size % 2
if set(entries) != {"debian-binary", "control.tar.gz", "data.tar.gz"}:
raise ValueError("Unexpected archive contents")
def files(name):
result = []
with tarfile.open(fileobj=io.BytesIO(entries[name]), mode="r:gz") as archive:
for member in archive.getmembers():
if member.isdir():
continue
if not member.isfile() or member.uid or member.gid:
raise ValueError("Unexpected package member")
result.append((member.name, archive.extractfile(member).read(), member.mode))
return result
controls, data = files("control.tar.gz"), files("data.tar.gz")
old = {name: value for name, value, _ in data}
profile_path = "usr/lib/mission-core-node/insta360_profile.py"
profile = (NODE / "packaging/insta360_profile.py").read_bytes()
if profile != old[profile_path]:
raise ValueError("The qualified bootstrap must remain unchanged")
qualified = REPO / "plugins/insta360-x4/build/package-b10"
result = json.loads((qualified / "package-result.json").read_text())
checks = json.loads((qualified / "build-report.json").read_text())
if checks["state"] != "complete" or result["version"] != "0.1.3-2":
raise ValueError("The fixed X4 package is not qualified")
model_name = "mission-core-insta360-x4_0.1.3-2_amd64.deb"
model = (qualified / model_name).read_bytes()
if len(model) != result["bytes"] or hashlib.sha256(model).hexdigest() != result["sha256"]:
raise ValueError("Qualified model package changed")
manifest = {
"schema": "missioncore.node.bundled-model/v1",
"model_id": "insta360.x4",
**{key: result[key] for key in ("version", "revision", "bytes", "sha256")},
}
model_root = "usr/share/mission-core-node/profiles/insta360-x4/"
changes = {
profile_path: profile,
model_root + "profile.json": (json.dumps(manifest, indent=2) + "\n").encode(),
}
proof_path = "usr/share/doc/mission-core-node/packaging-revision.json"
proof = {
"schema": "missioncore.node.packaging-revision/v1",
"version": "0.8.17-4",
"qualified_base_sha256": BASE,
"changed_files": list(changes) + [model_root + model_name],
"binary_and_ui_unchanged": True,
"model": manifest,
"model_checks": checks,
}
changes[proof_path] = (json.dumps(proof, indent=2) + "\n").encode()
if not set(changes) <= old.keys():
raise ValueError("Unexpected replacement paths")
old_model = model_root + "mission-core-insta360-x4_0.1.3-1_amd64.deb"
if (
hashlib.sha256(old[old_model]).hexdigest()
!= "c11977a4d4c9779b96ee38ff3a474bdd820d9e3e5ecc642e7711762def14b6da"
):
raise ValueError("Unexpected prior model bundle")
data = [
(name, changes.get(name, value), mode) for name, value, mode in data if name != old_model
]
data.append((model_root + model_name, model, 0o644))
for index, (name, value, mode) in enumerate(controls):
if name == "control":
if value.count(b"Version: 0.8.17-3\n") != 1:
raise ValueError("Unexpected Node version")
controls[index] = (
name,
value.replace(b"Version: 0.8.17-3\n", b"Version: 0.8.17-4\n"),
mode,
)
folder = NODE / "build/qualified-n08-r5"
folder.mkdir(mode=0o700)
target = folder / "mission-core-node_0.8.17-4_amd64.deb"
target.write_bytes(package(controls, data))
evidence = json.loads((NODE / "build/qualified-n08-r4/qualification.json").read_text())
evidence["packaging_revision"] = proof
evidence["artifacts"] = {
target.name: {
"bytes": target.stat().st_size,
"sha256": hashlib.sha256(target.read_bytes()).hexdigest(),
}
}
(folder / "qualification.json").write_text(json.dumps(evidence, indent=2) + "\n")
(NODE / "packaging/insta360-profile.json").write_bytes(changes[model_root + "profile.json"])
model_directory = NODE / "build/model-packages"
model_directory.mkdir(mode=0o700, exist_ok=True)
(model_directory / model_name).write_bytes(model)
print(
json.dumps({"folder": str(folder), "artifacts": evidence["artifacts"], "model": manifest})
)
if __name__ == "__main__":
main()
+130
View File
@@ -0,0 +1,130 @@
"""P09: deliver B11 through qualified Node 0.8.18 without changing binary/UI."""
import hashlib
import io
import json
import sys
import tarfile
from pathlib import Path
NODE = Path(__file__).resolve().parents[1]
REPO = NODE.parents[1]
sys.path.insert(0, str(REPO / "scripts/packaging"))
from debian import package # noqa: E402
BASE = "db340f7d117a7cdf4707bbf43f58c0e8307c8ac3813b7bc7dd5ca8a4a47f73b2"
def main():
original = NODE / "build/qualified-n09/mission-core-node_0.8.18_amd64.deb"
raw = original.read_bytes()
if hashlib.sha256(raw).hexdigest() != BASE or raw[:8] != b"!<arch>\n":
raise ValueError("Qualified Node package changed")
entries, cursor = {}, 8
while cursor < len(raw):
header = raw[cursor : cursor + 60]
name, size = header[:16].decode().strip().rstrip("/"), int(header[48:58])
if header[58:] != b"`\n" or name in entries:
raise ValueError("Invalid archive")
entries[name] = raw[cursor + 60 : cursor + 60 + size]
cursor += 60 + size + size % 2
if set(entries) != {"debian-binary", "control.tar.gz", "data.tar.gz"}:
raise ValueError("Unexpected archive contents")
def files(name):
result = []
with tarfile.open(fileobj=io.BytesIO(entries[name]), mode="r:gz") as archive:
for member in archive.getmembers():
if member.isdir():
continue
if not member.isfile() or member.uid or member.gid:
raise ValueError("Unexpected package member")
result.append((member.name, archive.extractfile(member).read(), member.mode))
return result
controls, data = files("control.tar.gz"), files("data.tar.gz")
old = {name: value for name, value, _ in data}
profile_path = "usr/lib/mission-core-node/insta360_profile.py"
profile = (NODE / "packaging/insta360_profile.py").read_bytes()
if profile != old[profile_path]:
raise ValueError("The qualified bootstrap must remain unchanged")
qualified = REPO / "plugins/insta360-x4/build/package-b11"
result = json.loads((qualified / "package-result.json").read_text())
checks = json.loads((qualified / "build-report.json").read_text())
if checks["state"] != "complete" or result["version"] != "0.1.3-3":
raise ValueError("The fixed X4 package is not qualified")
model_name = "mission-core-insta360-x4_0.1.3-3_amd64.deb"
model = (qualified / model_name).read_bytes()
if len(model) != result["bytes"] or hashlib.sha256(model).hexdigest() != result["sha256"]:
raise ValueError("Qualified model package changed")
manifest = {
"schema": "missioncore.node.bundled-model/v1",
"model_id": "insta360.x4",
**{key: result[key] for key in ("version", "revision", "bytes", "sha256")},
}
model_root = "usr/share/mission-core-node/profiles/insta360-x4/"
changes = {
profile_path: profile,
model_root + "profile.json": (json.dumps(manifest, indent=2) + "\n").encode(),
}
proof_path = "usr/share/doc/mission-core-node/packaging-revision.json"
proof = {
"schema": "missioncore.node.packaging-revision/v1",
"version": "0.8.18-1",
"qualified_base_sha256": BASE,
"changed_files": list(changes) + [model_root + model_name],
"binary_and_ui_unchanged": True,
"model": manifest,
"model_checks": checks,
}
changes[proof_path] = (json.dumps(proof, indent=2) + "\n").encode()
if not set(changes) - {proof_path} <= old.keys():
raise ValueError("Unexpected replacement paths")
old_model = model_root + "mission-core-insta360-x4_0.1.3-2_amd64.deb"
if (
hashlib.sha256(old[old_model]).hexdigest()
!= "2931a5206478fcac67f74bd1fb579b98c0fcc43eda4c05e5ac4a721947619130"
):
raise ValueError("Unexpected prior model bundle")
data = [
(name, changes.get(name, value), mode) for name, value, mode in data if name != old_model
]
data.append((model_root + model_name, model, 0o644))
if proof_path not in old:
data.append((proof_path, changes[proof_path], 0o644))
for name, content, _mode in data:
if name not in changes and name != model_root + model_name and content != old[name]:
raise ValueError("Unrelated qualified payload changed")
for index, (name, value, mode) in enumerate(controls):
if name == "control":
if value.count(b"Version: 0.8.18\n") != 1:
raise ValueError("Unexpected Node version")
controls[index] = (
name,
value.replace(b"Version: 0.8.18\n", b"Version: 0.8.18-1\n"),
mode,
)
folder = NODE / "build/qualified-n09-r1"
folder.mkdir(mode=0o700)
target = folder / "mission-core-node_0.8.18-1_amd64.deb"
target.write_bytes(package(controls, data))
evidence = json.loads((NODE / "build/qualified-n09/qualification.json").read_text())
evidence["packaging_revision"] = proof
evidence["artifacts"] = {
target.name: {
"bytes": target.stat().st_size,
"sha256": hashlib.sha256(target.read_bytes()).hexdigest(),
}
}
(folder / "qualification.json").write_text(json.dumps(evidence, indent=2) + "\n")
(NODE / "packaging/insta360-profile.json").write_bytes(changes[model_root + "profile.json"])
model_directory = NODE / "build/model-packages"
model_directory.mkdir(mode=0o700, exist_ok=True)
(model_directory / model_name).write_bytes(model)
print(
json.dumps({"folder": str(folder), "artifacts": evidence["artifacts"], "model": manifest})
)
if __name__ == "__main__":
main()
@@ -0,0 +1,108 @@
"""P05: replace only X4 bootstrap in qualified Node 0.8.17-1; retain compiled bytes."""
import hashlib
import io
import json
import sys
import tarfile
from pathlib import Path
NODE = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(NODE.parents[1] / "scripts/packaging"))
from debian import package # noqa: E402
BASE = "7033ad608765231b20773dee45edac3d8f63e65525410de2346c79b3f8756a4b"
TARGET = "usr/lib/mission-core-node/insta360_profile.py"
PROOF = "usr/share/doc/mission-core-node/packaging-revision.json"
def main():
original = NODE / "build/qualified-n08-r2/mission-core-node_0.8.17-1_amd64.deb"
raw = original.read_bytes()
if hashlib.sha256(raw).hexdigest() != BASE or raw[:8] != b"!<arch>\n":
raise ValueError("Qualified package changed")
entries, cursor = {}, 8
while cursor < len(raw):
header = raw[cursor : cursor + 60]
name, size = header[:16].decode().strip().rstrip("/"), int(header[48:58])
if header[58:] != b"`\n" or name in entries:
raise ValueError("Invalid archive")
entries[name] = raw[cursor + 60 : cursor + 60 + size]
cursor += 60 + size + size % 2
if set(entries) != {"debian-binary", "control.tar.gz", "data.tar.gz"}:
raise ValueError("Unexpected package contents")
def files(name):
result = []
with tarfile.open(fileobj=io.BytesIO(entries[name]), mode="r:gz") as archive:
for member in archive.getmembers():
if member.isdir():
continue
if not member.isfile() or member.uid or member.gid:
raise ValueError("Unexpected package member")
result.append((member.name, archive.extractfile(member).read(), member.mode))
return result
controls, data = files("control.tar.gz"), files("data.tar.gz")
original_control = [value for name, value, _ in controls if name == "control"]
if len(original_control) != 1 or original_control[0].count(b"Version: 0.8.17-1\n") != 1:
raise ValueError("Unexpected control version")
fixed = (NODE / "packaging/insta360_profile.py").read_bytes()
checks = json.loads((NODE / "build/x4-profile-p05-check.json").read_text())
if (
checks["state"] != "complete"
or checks["files"]["apps/node-agent/packaging/insta360_profile.py"]
!= hashlib.sha256(fixed).hexdigest()
):
raise ValueError("Current profile has not passed Ubuntu checks")
if sum(name == TARGET for name, _, _ in data) != 1:
raise ValueError("Missing profile")
proof = {
"schema": "missioncore.node.packaging-revision/v1",
"version": "0.8.17-2",
"qualified_base_sha256": BASE,
"changed_files": [TARGET],
"binary_ui_and_model_packages_unchanged": True,
"profile_sha256": hashlib.sha256(fixed).hexdigest(),
"profile_check": checks,
}
controls = [
(
name,
value.replace(b"Version: 0.8.17-1\n", b"Version: 0.8.17-2\n")
if name == "control"
else value,
mode,
)
for name, value, mode in controls
]
data = [
(
name,
fixed
if name == TARGET
else (json.dumps(proof, indent=2) + "\n").encode()
if name == PROOF
else value,
mode,
)
for name, value, mode in data
]
folder = NODE / "build/qualified-n08-r3"
folder.mkdir(mode=0o700)
path = folder / "mission-core-node_0.8.17-2_amd64.deb"
path.write_bytes(package(controls, data))
evidence = json.loads((NODE / "build/qualified-n08-r2/qualification.json").read_text())
evidence["packaging_revision"] = proof
evidence["artifacts"] = {
path.name: {
"bytes": path.stat().st_size,
"sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
}
}
(folder / "qualification.json").write_text(json.dumps(evidence, indent=2) + "\n")
print(json.dumps({"folder": str(folder), "artifacts": evidence["artifacts"]}))
if __name__ == "__main__":
main()
@@ -0,0 +1,179 @@
"""One fixed recovery of N08's qualified sources after the Core V8 heap limit.
No dependency installation, source edits, Go recompilation or hardware access.
The original failed report and all stderr remain part of the result archive.
"""
import fcntl
import hashlib
import json
import os
import platform
import resource
import subprocess
import sys
import tarfile
import time
from datetime import UTC, datetime
from pathlib import Path
IDENTIFIER = "511010461b4f2cd4c0546af9"
SOURCE_HASH = "511010461b4f2cd4c0546af946adcb696a03d3134dc0f68c4b5c62b8bc57d766"
REPORT_HASH = "e5481d0cb9e2799ffe2ca4ec60390d1557e363b93e192e52df92e58fae3d21e6"
PACKAGE_HASH = "f929c440e4965f728aa6cd00eb122e7cad09e5ade82da763cc36661f50ec66b5"
FOLDER = Path("/var/tmp/mission-core-node-builds") / IDENTIFIER
PACKAGE = "mission-core-node_0.8.17_amd64.deb"
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def main():
if os.geteuid() == 0 or (platform.system(), platform.machine()) != ("Linux", "x86_64"):
raise ValueError("Use the unprivileged Ubuntu build account")
if sys.argv[1:] not in ([], ["--job"]):
raise ValueError("Only the fixed N08 Core recovery is admitted")
os.umask(0o077)
resource.setrlimit(resource.RLIMIT_CORE, (0, 0))
for path in (FOLDER.parent, FOLDER):
info = path.lstat()
if path.is_symlink() or info.st_uid != os.geteuid() or info.st_mode & 0o077:
raise ValueError("Build attempt is not private and owned")
output = FOLDER / "output"
if digest(FOLDER / "source.json") != SOURCE_HASH:
raise ValueError("Source manifest changed")
manifest = json.loads((FOLDER / "source.json").read_text())
if digest(output / PACKAGE) != PACKAGE_HASH:
raise ValueError("Qualified Node package changed")
if not sys.argv[1:]:
with (FOLDER / "build.lock").open("a") as handle:
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
subprocess.run(
[
"/usr/bin/systemd-run",
"--user",
"--collect",
"--wait",
"--pipe",
"--unit=mission-core-node-core-resume-" + IDENTIFIER,
"--property=MemoryMax=3G",
"--property=CPUQuota=150%",
"--property=TasksMax=256",
"--property=RuntimeMaxSec=300",
"--property=Nice=10",
"/usr/bin/python3",
str(Path(__file__).resolve()),
"--job",
],
check=True,
)
with tarfile.open(FOLDER / "result.tar.gz", "w:gz") as archive:
for path in sorted(output.iterdir()):
if not path.is_file() or path.is_symlink():
raise ValueError("Unexpected build output")
archive.add(path, arcname=path.name, recursive=False)
archive.add(FOLDER / "report.json", arcname="build-report.json", recursive=False)
print(
json.dumps(
{
"result": str(FOLDER / "result.tar.gz"),
"sha256": digest(FOLDER / "result.tar.gz"),
}
)
)
return
group = next(
line.split(":", 2)[2]
for line in Path("/proc/self/cgroup").read_text().splitlines()
if line.startswith("0::")
)
control = Path("/sys/fs/cgroup") / group.lstrip("/")
cpu, period = map(int, (control / "cpu.max").read_text().split())
if (
int((control / "memory.max").read_text()) != 3 * 1024**3
or cpu / period > 1.5
or int((control / "pids.max").read_text()) > 256
):
raise ValueError("Recovery cgroup limits are not enforced")
for name, expected in manifest["files"].items():
path = FOLDER / "source" / name
if path.is_symlink() or digest(path) != expected["sha256"]:
raise ValueError("A previously qualified input changed: " + name)
if digest(output / "qualification.json") != REPORT_HASH:
raise ValueError("Qualification checkpoint changed")
report = json.loads((output / "qualification.json").read_text())
if (
report["state"] != "error"
or report["jobs"][-1]["id"] != "core-build"
or report["jobs"][-1]["exit_code"] != 134
):
raise ValueError("This recovery only admits N08's V8 heap failure")
with (output / "qualification-before-core-resume.json").open("xb") as stream:
stream.write((output / "qualification.json").read_bytes())
repo = FOLDER / "source" / manifest["repository"]
tool = FOLDER / "toolchains/node-v24.9.0-linux-x64"
env = {
"PATH": str(tool / "bin") + ":/usr/bin:/bin",
"LANG": "C.UTF-8",
"TMPDIR": str(FOLDER / "temporary"),
"NODE_OPTIONS": "--max-old-space-size=2048",
"npm_config_cache": str(FOLDER / "npm-cache"),
"npm_config_audit": "false",
"npm_config_fund": "false",
"npm_config_update_notifier": "false",
}
started = time.monotonic()
job = {
"id": "core-build-memory-recovery",
"started_at": datetime.now(UTC).isoformat(),
"monotonic_started": started,
"memory_max_bytes": 3 * 1024**3,
"v8_heap_mib": 2048,
"source_manifest_sha256": SOURCE_HASH,
}
with (
(output / "core-build-recovery.stdout").open("wb") as out,
(output / "core-build-recovery.stderr").open("wb") as err,
):
result = subprocess.run(
[
str(tool / "bin/node"),
str(tool / "lib/node_modules/npm/bin/npm-cli.js"),
"run",
"build",
],
cwd=repo / "apps/control-station",
env=env,
stdout=out,
stderr=err,
timeout=280,
)
job.update(
exit_code=result.returncode,
state="complete" if result.returncode == 0 else "error",
finished_at=datetime.now(UTC).isoformat(),
duration_seconds=time.monotonic() - started,
memory_peak_bytes=int((control / "memory.peak").read_text()),
)
(output / "core-resume-report.json").write_text(json.dumps(job, indent=2) + "\n")
if result.returncode:
raise RuntimeError("Core build recovery failed; preserve its evidence")
dist = repo / "apps/control-station/dist"
with tarfile.open(output / "core-dist.tar.gz", "w:gz") as archive:
for path in sorted(dist.rglob("*")):
if path.is_file():
archive.add(path, arcname=str(path.relative_to(dist)), recursive=False)
report["jobs"].append(job)
report.update(
state="complete", finished_at=datetime.now(UTC).isoformat(), recovered_from=REPORT_HASH
)
report["artifacts"] = {
path.name: {"bytes": path.stat().st_size, "sha256": digest(path)}
for path in (output / PACKAGE, output / "core-dist.tar.gz")
}
(output / "qualification.json").write_text(json.dumps(report, indent=2) + "\n")
if __name__ == "__main__":
main()
@@ -0,0 +1,230 @@
"""Fixed N09 test-contract recovery; no installed files or camera access."""
import fcntl
import hashlib
import json
import os
import platform
import subprocess
import sys
import tarfile
import time
import zipfile
from datetime import UTC, datetime
from pathlib import Path
IDENTIFIER = "d7aa79960717e4d62ab69633"
SOURCE_HASH = "d7aa79960717e4d62ab69633ec9ddc1ddabfa33998043635bdef2dbac807a3ca"
REPORT_HASH = "0d7b2390c5d546e1e442c1283afcbab6e63e03ac8f3033489f7b86cc3de71278"
PACKAGE_HASH = "db340f7d117a7cdf4707bbf43f58c0e8307c8ac3813b7bc7dd5ca8a4a47f73b2"
FOLDER = Path("/var/tmp/mission-core-node-builds") / IDENTIFIER
PACKAGE = "mission-core-node_0.8.18_amd64.deb"
TESTS = {
"apps/control-station/test/k1ManualControl.test.mjs",
"apps/control-station/test/k1SupervisorPresentation.test.mjs",
}
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def main():
if os.geteuid() == 0 or (platform.system(), platform.machine()) != ("Linux", "x86_64"):
raise ValueError("Use the unprivileged Ubuntu account")
if sys.argv[1:] not in ([], ["--job"]):
raise ValueError("Only the fixed N09 recovery is admitted")
os.umask(0o077)
artifact = Path(sys.argv[0]).resolve()
for p in (FOLDER.parent, FOLDER):
info = p.lstat()
if p.is_symlink() or info.st_uid != os.geteuid() or info.st_mode & 0o077:
raise ValueError("Staging is not private and owned")
with zipfile.ZipFile(artifact) as z:
patch = json.loads(z.read("patch.json"))
if (
set(patch["files"]) != TESTS
or set(z.namelist()) != {"__main__.py", "patch.json"} | TESTS
):
raise ValueError("Only the two test files can change")
payload = {name: z.read(name) for name in TESTS}
if hashlib.sha256(z.read("__main__.py")).hexdigest() != patch["entrypoint_sha256"]:
raise ValueError("Entry point changed")
for name, data in payload.items():
if (
len(data) != patch["files"][name]["bytes"]
or hashlib.sha256(data).hexdigest() != patch["files"][name]["sha256"]
):
raise ValueError("Test payload changed")
output = FOLDER / "output"
if digest(FOLDER / "source.json") != SOURCE_HASH or digest(output / PACKAGE) != PACKAGE_HASH:
raise ValueError("N09 checkpoint changed")
manifest = json.loads((FOLDER / "source.json").read_text())
repo = FOLDER / "source" / manifest["repository"]
if not sys.argv[1:]:
with (FOLDER / "build.lock").open("a") as handle:
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
if (FOLDER / "result.tar.gz").exists():
raise ValueError("Recovery already completed")
subprocess.run(
[
"/usr/bin/systemd-run",
"--user",
"--collect",
"--wait",
"--pipe",
"--unit=mission-core-node-core-resume-" + IDENTIFIER,
"--property=MemoryMax=3G",
"--property=CPUQuota=150%",
"--property=TasksMax=256",
"--property=RuntimeMaxSec=600",
"--property=KillMode=control-group",
"--property=Nice=10",
"/usr/bin/python3",
str(artifact),
"--job",
],
check=True,
)
with tarfile.open(FOLDER / "result.tar.gz", "w:gz") as archive:
for path in sorted(output.iterdir()):
if path.is_symlink() or not path.is_file():
raise ValueError("Unexpected result")
archive.add(path, arcname=path.name, recursive=False)
archive.add(FOLDER / "report.json", arcname="build-report.json", recursive=False)
print(
json.dumps(
{
"result": str(FOLDER / "result.tar.gz"),
"sha256": digest(FOLDER / "result.tar.gz"),
}
)
)
return
group = next(
x.split(":", 2)[2]
for x in Path("/proc/self/cgroup").read_text().splitlines()
if x.startswith("0::")
)
control = Path("/sys/fs/cgroup") / group.lstrip("/")
cpu, period = map(int, (control / "cpu.max").read_text().split())
if (
int((control / "memory.max").read_text()) != 3 * 1024**3
or cpu / period > 1.5
or int((control / "pids.max").read_text()) > 256
):
raise ValueError("Resource limits not enforced")
for name, expected in manifest["files"].items():
path = FOLDER / "source" / name
if path.is_symlink() or digest(path) != expected["sha256"]:
raise ValueError("Original input changed: " + name)
if digest(output / "qualification.json") != REPORT_HASH:
raise ValueError("Qualification checkpoint changed")
report = json.loads((output / "qualification.json").read_text())
if report["state"] != "error" or report["jobs"][-1]["id"] != "core-tests":
raise ValueError("Only the observed N09 test failure can be resumed")
with (output / "qualification-before-n09-resume.json").open("xb") as f:
f.write((output / "qualification.json").read_bytes())
for name, data in payload.items():
(repo / name).write_bytes(data)
(output / "test-contract-patch.json").write_text(json.dumps(patch, indent=2))
tool = FOLDER / "toolchains/node-v24.9.0-linux-x64"
env = {
"PATH": str(tool / "bin") + ":/usr/bin:/bin",
"LANG": "C.UTF-8",
"TMPDIR": str(FOLDER / "temporary"),
"NODE_OPTIONS": "--max-old-space-size=2048",
"npm_config_cache": str(FOLDER / "npm-cache"),
"npm_config_audit": "false",
"npm_config_fund": "false",
"npm_config_update_notifier": "false",
}
started = time.monotonic()
report.update(
state="running",
recovered_from=REPORT_HASH,
recovery_started_at=datetime.now(UTC).isoformat(),
recovery_monotonic_started=started,
)
def save():
(output / "qualification.json").write_text(json.dumps(report, indent=2) + "\n")
def run(name, args, timeout):
job = {"id": name, "started_at": datetime.now(UTC).isoformat(), "state": "running"}
report["jobs"].append(job)
save()
begin = time.monotonic()
with (
(output / (name + ".stdout")).open("wb") as out,
(output / (name + ".stderr")).open("wb") as err,
):
result = subprocess.run(
args,
cwd=repo / "apps/control-station",
env=env,
stdout=out,
stderr=err,
timeout=timeout,
)
job.update(
exit_code=result.returncode,
state="complete" if result.returncode == 0 else "error",
duration_seconds=time.monotonic() - begin,
)
save()
if result.returncode:
raise RuntimeError(name + " failed")
try:
run(
"core-tests-n09-contract",
[
str(tool / "bin/node"),
"--test",
"--test-concurrency=1",
"--test-skip-pattern=^native RRD crosses many RTC fragments "
"and reaches decoder once, byte-for-byte$",
*[str(p) for p in sorted((repo / "apps/control-station/test").glob("*.test.mjs"))],
],
300,
)
run(
"core-build-n09",
[
str(tool / "bin/node"),
str(tool / "lib/node_modules/npm/bin/npm-cli.js"),
"run",
"build",
],
240,
)
roots = {
"core-dist.tar.gz": repo / "apps/control-station/dist",
"design-guideline-catalog.tar.gz": FOLDER
/ "source/NODEDC_DESIGN_GUIDELINE/apps/catalog/dist",
}
for name, root in roots.items():
with tarfile.open(output / name, "w:gz") as archive:
for path in sorted(root.rglob("*")):
if path.is_file() and not path.is_symlink():
archive.add(path, arcname=str(path.relative_to(root)), recursive=False)
report.update(
state="complete",
finished_at=datetime.now(UTC).isoformat(),
recovery_duration_seconds=time.monotonic() - started,
memory_peak_bytes=int((control / "memory.peak").read_text()),
)
report["artifacts"] = {
p.name: {"bytes": p.stat().st_size, "sha256": digest(p)}
for p in [output / PACKAGE, *[output / n for n in roots]]
}
save()
except Exception:
report.update(state="error", finished_at=datetime.now(UTC).isoformat())
save()
raise
if __name__ == "__main__":
main()
+3 -3
View File
@@ -55,11 +55,11 @@ Nice=10
CONF
systemctl daemon-reload
systemctl restart postgresql@16-ndcmonitor.service
runuser -u postgres -- psql -X -v ON_ERROR_STOP=1 -h /run/mission-core-monitor-db -p 5433 -d postgres <<'SQL'
runuser -u postgres -- psql -X -P pager=off -v ON_ERROR_STOP=1 -h /run/mission-core-monitor-db -p 5433 -d postgres <<'SQL'
SELECT 'CREATE ROLE "mission-core-monitor" LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE' WHERE NOT EXISTS(SELECT FROM pg_roles WHERE rolname='mission-core-monitor') \gexec
SELECT 'CREATE DATABASE mission_core_monitor OWNER "mission-core-monitor"' WHERE NOT EXISTS(SELECT FROM pg_database WHERE datname='mission_core_monitor') \gexec
SQL
runuser -u postgres -- psql -X -v ON_ERROR_STOP=1 -h /run/mission-core-monitor-db -p 5433 -d mission_core_monitor -c 'CREATE EXTENSION IF NOT EXISTS timescaledb'
runuser -u mission-core-monitor -- psql -X -v ON_ERROR_STOP=1 -h /run/mission-core-monitor-db -p 5433 -d mission_core_monitor -f /usr/lib/mission-core-node/monitor/schema.sql
runuser -u postgres -- psql -X -P pager=off -v ON_ERROR_STOP=1 -h /run/mission-core-monitor-db -p 5433 -d mission_core_monitor -c 'CREATE EXTENSION IF NOT EXISTS timescaledb'
runuser -u mission-core-monitor -- psql -X -P pager=off -v ON_ERROR_STOP=1 -h /run/mission-core-monitor-db -p 5433 -d mission_core_monitor -f /usr/lib/mission-core-node/monitor/schema.sql
systemctl enable mission-core-node-monitor.service
systemctl restart mission-core-node-monitor.service