This commit is contained in:
firestar5683
2026-08-23 11:29:46 -05:00
parent db5922db43
commit 2db8d95905
8 changed files with 436 additions and 1379 deletions
-14
View File
@@ -29,20 +29,6 @@ function agnos_init {
# set success flag for current boot slot # set success flag for current boot slot
sudo abctl --set_success sudo abctl --set_success
# Restore SSH access after a user-triggered reset if keys were backed up to /cache.
SSH_BACKUP_DIR="/cache/reset_backup"
if [ -d "$SSH_BACKUP_DIR" ]; then
sudo mkdir -p /data/params/d
for key in GithubSshKeys SshEnabled; do
if [ -f "$SSH_BACKUP_DIR/$key" ]; then
sudo cp "$SSH_BACKUP_DIR/$key" "/data/params/d/$key"
fi
done
sudo chown comma:comma /data/params/d/GithubSshKeys /data/params/d/SshEnabled 2>/dev/null || true
sudo chmod 600 /data/params/d/GithubSshKeys /data/params/d/SshEnabled 2>/dev/null || true
sudo rm -rf "$SSH_BACKUP_DIR"
fi
# TODO: do this without udev in AGNOS # TODO: do this without udev in AGNOS
# udev does this, but sometimes we startup faster # udev does this, but sometimes we startup faster
sudo chgrp gpu /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 sudo chgrp gpu /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0
+1 -1
View File
@@ -21,7 +21,7 @@ fi
export QCOM_PRIORITY=12 export QCOM_PRIORITY=12
if [ -z "$AGNOS_VERSION" ]; then if [ -z "$AGNOS_VERSION" ]; then
export AGNOS_VERSION="19.6.2" export AGNOS_VERSION="19.6.3"
fi fi
if [ -z "$AGNOS_ACCEPTED_VERSIONS" ]; then if [ -z "$AGNOS_ACCEPTED_VERSIONS" ]; then
@@ -1,6 +1,8 @@
import subprocess import subprocess
import time import time
from openpilot.system.hardware import HARDWARE
_DELETE_TIMEOUT_S = 1800 _DELETE_TIMEOUT_S = 1800
_DELETE_RETRY_ATTEMPTS = 20 _DELETE_RETRY_ATTEMPTS = 20
@@ -8,6 +10,11 @@ _DELETE_RETRY_DELAY_S = 0.25
_DIRECTORY_NOT_EMPTY_ERROR = "directory not empty" _DIRECTORY_NOT_EMPTY_ERROR = "directory not empty"
def request_system_factory_reset():
"""Request AGNOS's stock userdata reset and reboot flow."""
HARDWARE.uninstall()
def remove_path(path): def remove_path(path):
# Managed processes can recreate entries under /data/params while rm is finishing. # Managed processes can recreate entries under /data/params while rm is finishing.
for attempt in range(_DELETE_RETRY_ATTEMPTS): for attempt in range(_DELETE_RETRY_ATTEMPTS):
@@ -16,6 +16,24 @@ def _load_factory_reset_module():
factory_reset = _load_factory_reset_module() factory_reset = _load_factory_reset_module()
def test_request_system_factory_reset_uses_agnos_uninstall(monkeypatch):
calls = []
monkeypatch.setattr(factory_reset.HARDWARE, "uninstall", lambda: calls.append(True))
factory_reset.request_system_factory_reset()
assert calls == [True]
def test_galaxy_factory_reset_worker_does_not_partially_delete_userdata():
source = (Path(__file__).resolve().parents[1] / "the_galaxy.py").read_text(encoding="utf-8")
worker = source.split("def _factory_reset_worker():", 1)[1].split("\ndef ", 1)[0]
assert "_request_system_factory_reset()" in worker
assert "_run_factory_reset_delete" not in worker
assert "_finish_update_and_reboot" not in worker
def test_remove_path_retries_directory_not_empty(monkeypatch): def test_remove_path_retries_directory_not_empty(monkeypatch):
results = iter( results = iter(
[ [
+8 -37
View File
@@ -98,7 +98,10 @@ from openpilot.starpilot.common.testing_grounds import (
TESTING_GROUNDS_STATE_PATH as SHARED_TESTING_GROUNDS_STATE_PATH, TESTING_GROUNDS_STATE_PATH as SHARED_TESTING_GROUNDS_STATE_PATH,
) )
from openpilot.starpilot.navigation.destination_store import normalize_destination_payload, update_recent_destinations from openpilot.starpilot.navigation.destination_store import normalize_destination_payload, update_recent_destinations
from openpilot.starpilot.system.the_galaxy.factory_reset import remove_path as _run_factory_reset_delete from openpilot.starpilot.system.the_galaxy.factory_reset import (
remove_path as _run_factory_reset_delete,
request_system_factory_reset as _request_system_factory_reset,
)
from openpilot.starpilot.system.the_galaxy import flm_workspace, utilities from openpilot.starpilot.system.the_galaxy import flm_workspace, utilities
from openpilot.starpilot.system.the_galaxy.update_recovery import inspect_interrupted_update, public_recovery_status, recover_interrupted_update from openpilot.starpilot.system.the_galaxy.update_recovery import inspect_interrupted_update, public_recovery_status, recover_interrupted_update
@@ -1437,22 +1440,6 @@ _fast_update_state = {
} }
_ROUTE_DELETE_LOCK = threading.Lock() _ROUTE_DELETE_LOCK = threading.Lock()
_FACTORY_RESET_WIPE_PATHS = [
"/data/params",
"/cache/starpilot/params",
"/cache/params",
"/data/media/0/realdata",
"/data/media/0/realdata_HD",
"/data/media/0/realdata_konik",
"/data/models",
"/data/toggle_backups",
"/data/backups",
"/data/themes",
"/data/media/0/osm/offline",
"/cache/use_HD",
"/cache/use_konik",
]
_PLOTS_POLL_INTERVAL_S = 0.75 _PLOTS_POLL_INTERVAL_S = 0.75
_PLOTS_BOOT_STABILIZATION_WINDOW_S = 45.0 _PLOTS_BOOT_STABILIZATION_WINDOW_S = 45.0
_PLOTS_BOOT_POLL_INTERVAL_S = 1.0 _PLOTS_BOOT_POLL_INTERVAL_S = 1.0
@@ -2543,34 +2530,18 @@ def _factory_reset_worker():
started_at = time.time() started_at = time.time()
try: try:
_set_fast_update_progress(1, "Preparing factory reset", 10.0, "Cleaning up legacy device state...") _set_fast_update_progress(1, "Preparing factory reset", 10.0, "Requesting AGNOS system reset...")
_set_fast_update_state( _set_fast_update_state(
running=True, running=True,
stage="factory-resetting", stage="factory-resetting",
message="Factory reset started. Wiping device state...", message="Factory reset requested. AGNOS will erase userdata after reboot...",
lastError="", lastError="",
lastMode="factory-reset", lastMode="factory-reset",
startedAt=started_at, startedAt=started_at,
finishedAt=0.0, finishedAt=0.0,
) )
_set_fast_update_progress(1, "Preparing factory reset", 100.0, "Factory reset initialized.") _set_fast_update_progress(1, "Preparing factory reset", 100.0, "AGNOS reset trigger initialized.")
_request_system_factory_reset()
total_paths = max(1, len(_FACTORY_RESET_WIPE_PATHS))
for index, path in enumerate(_FACTORY_RESET_WIPE_PATHS, start=1):
step_percent = ((index - 1) / total_paths) * 100.0
_set_fast_update_progress(2, "Wiping device data", step_percent, f"Removing {path}...")
_run_factory_reset_delete(path)
_set_fast_update_progress(2, "Wiping device data", (index / total_paths) * 100.0, f"Removed {path}.")
_set_fast_update_progress(3, "Resetting factory state", 100.0, "Legacy device state removed.")
_set_fast_update_progress(4, "Finalizing reset", 50.0, "Syncing filesystem before reboot...")
subprocess.run(["sync"], capture_output=True, text=True, timeout=60, check=False)
_set_fast_update_progress(4, "Finalizing reset", 100.0, "Filesystem sync complete.")
_finish_update_and_reboot(
"Factory reset complete. Device is rebooting now. Please wait for reconnection."
)
except Exception as exception: except Exception as exception:
_set_fast_update_error_state("Factory reset failed.", exception) _set_fast_update_error_state("Factory reset failed.", exception)
+4 -4
View File
@@ -67,13 +67,13 @@
}, },
{ {
"name": "system", "name": "system",
"url": "https://www.dropbox.com/scl/fi/pewhzpqzi3aewuiaffc6m/system10.img.xz?rlkey=olzrzulhs93zzghnjrskmdwxt&st=exnfk2oz&dl=1", "url": "https://cloud.floonetwork.io/s/w4tQdX5kS9Rgeiw/download",
"hash": "ab395d4c963a908ab86709f1a6580a62dd24cb34ee71cf5fd5ec29d7d48d0e10", "hash": "8478e080cf5fbfdcc84d6910f7cd1cb6b2a5a2939eb9e56f2425f87ee33d146d",
"hash_raw": "ab395d4c963a908ab86709f1a6580a62dd24cb34ee71cf5fd5ec29d7d48d0e10", "hash_raw": "8478e080cf5fbfdcc84d6910f7cd1cb6b2a5a2939eb9e56f2425f87ee33d146d",
"size": 4718592000, "size": 4718592000,
"sparse": false, "sparse": false,
"full_check": false, "full_check": false,
"has_ab": true, "has_ab": true,
"ondevice_hash": "ab395d4c963a908ab86709f1a6580a62dd24cb34ee71cf5fd5ec29d7d48d0e10" "ondevice_hash": "8478e080cf5fbfdcc84d6910f7cd1cb6b2a5a2939eb9e56f2425f87ee33d146d"
} }
] ]
File diff suppressed because it is too large Load Diff
+69 -87
View File
@@ -1,106 +1,88 @@
import importlib.util
import json
from pathlib import Path from pathlib import Path
import runpy
import pytest import pytest
from tools.agnos.patch_system_reset_image import (
AMDGPU_FIRMWARE_SHA256, def _load_patch_module():
COMMA_SH_DISPLAY_WAIT_PATCH_MARKER, path = Path(__file__).resolve().parent / "patch_system_reset_image.py"
comma_sh_has_expected_display_wait, spec = importlib.util.spec_from_file_location("patch_system_reset_image_under_test", path)
find_default_reference_manifest, module = importlib.util.module_from_spec(spec)
format_debugfs_mode, spec.loader.exec_module(module)
patch_comma_sh_display_wait, return module
patch_setup_branding_script,
sha256_zstd_payload,
)
ORIGINAL_DISPLAY_WAIT = b'''#!/usr/bin/env bash patch_image = _load_patch_module()
echo "waiting for magic" ALLOWED_IMAGE_MUTATIONS = patch_image.ALLOWED_IMAGE_MUTATIONS
for i in {1..200}; do UPSTREAM_PROTECTED_PAYLOADS = patch_image.UPSTREAM_PROTECTED_PAYLOADS
if systemctl is-active --quiet magic && [ -S /tmp/drmfd.sock ]; then VERSION_PATH_IN_IMAGE = patch_image.VERSION_PATH_IN_IMAGE
break resolve_upstream_manifest = patch_image.resolve_upstream_manifest
fi update_manifest_system_entry = patch_image.update_manifest_system_entry
sleep 0.1 write_allowed_file = patch_image.write_allowed_file
done expected_upstream_version = patch_image.expected_upstream_version
verify_official_raw_hash = patch_image.verify_official_raw_hash
if systemctl is-active --quiet magic && [ -S /tmp/drmfd.sock ]; then
echo "magic ready after ${SECONDS}s"
else
echo "timed out waiting for magic, ${SECONDS}s"
fi
exec /data/continue.sh
'''
def test_patch_comma_sh_display_wait_uses_available_display_service(): def test_only_version_is_mutable():
patched = patch_comma_sh_display_wait(ORIGINAL_DISPLAY_WAIT) assert ALLOWED_IMAGE_MUTATIONS == {VERSION_PATH_IN_IMAGE}
assert set(UPSTREAM_PROTECTED_PAYLOADS) >= {
assert COMMA_SH_DISPLAY_WAIT_PATCH_MARKER.encode() in patched "/usr/comma/comma.sh",
assert b"systemctl cat magic.service" in patched "/usr/comma/reset",
assert b"systemctl is-active --quiet magic" in patched "/usr/comma/setup",
assert b"systemctl is-active --quiet weston-ready" in patched "/usr/comma/updater",
assert b"[ -S /var/tmp/weston/wayland-0 ]" in patched "/etc/NetworkManager/NetworkManager.conf",
assert comma_sh_has_expected_display_wait(patched) "/etc/NetworkManager/conf.d/10-globally-managed-devices.conf",
assert patch_comma_sh_display_wait(patched) == patched "/lib/systemd/system/NetworkManager.service",
}
def test_patch_comma_sh_display_wait_rejects_unknown_layout(): def test_starpilot_revision_maps_to_exact_upstream_version():
with pytest.raises(RuntimeError, match="display readiness wait"): assert expected_upstream_version("19.6.3", None) == "19.6"
patch_comma_sh_display_wait(b"#!/usr/bin/env bash\nexec /data/continue.sh\n") assert expected_upstream_version("19.6.3", "19.6-test") == "19.6-test"
with pytest.raises(RuntimeError, match="revision suffix"):
expected_upstream_version("19.6", None)
@pytest.mark.parametrize("slider_text", ["slide to use", "slide to install"]) def test_official_source_hash_is_enforced(tmp_path):
def test_patch_mici_setup_branding_handles_old_and_new_labels(slider_text): image = tmp_path / "official.img"
original = f'''OPENPILOT_URL = "https://openpilot.comma.ai" image.write_bytes(b"official")
self._openpilot_slider = LargerSlider("{slider_text}\\nopenpilot", callback) digest = patch_image.sha256_file(image)
self._continue_button = BigPillButton("install openpilot", green=True)
self._continue_button.set_text("install openpilot" if not custom_software else "choose software")
'''.encode()
patched = patch_setup_branding_script(original, "openpilot/system/ui/mici_setup.py") assert verify_official_raw_hash({"hash_raw": digest}, image) == digest
with pytest.raises(RuntimeError, match="hash mismatch"):
assert b"installer.comma.ai/firestar5683/StarPilot" in patched verify_official_raw_hash({"hash_raw": "0" * 64}, image)
assert f"{slider_text}\\nstarpilot".encode() in patched
assert b"install StarPilot" in patched
assert b"install openpilot" not in patched
@pytest.mark.parametrize(("mode", "expected"), [ def test_write_rejects_non_version_path(tmp_path):
("100775", "0100775"), with pytest.raises(RuntimeError, match="Refusing non-C3 AGNOS system mutation"):
("100644", "0100644"), write_allowed_file("debugfs", tmp_path / "system.img", "/usr/comma/setup", tmp_path / "setup")
("040755", "040755"),
("120777", "0120777"),
])
def test_format_debugfs_mode(mode, expected):
assert format_debugfs_mode(mode) == expected
def test_external_gpu_firmware_matches_tinygrad_requirements(): def test_upstream_manifest_cannot_be_primary(tmp_path):
firmware_metadata = Path(__file__).resolve().parents[2] / "tinygrad/runtime/autogen/am/fw.py" primary = tmp_path / "agnos.json"
hashes = runpy.run_path(firmware_metadata)["hashes"]
expected = {filename.removesuffix(".zst"): digest for filename, digest in AMDGPU_FIRMWARE_SHA256.items()}
assert all(hashes[filename] == digest for filename, digest in expected.items())
def test_zstd_payload_hash(tmp_path):
import hashlib
import zstandard
payload = b"external GPU firmware payload"
compressed = tmp_path / "firmware.bin.zst"
compressed.write_bytes(zstandard.ZstdCompressor().compress(payload))
assert sha256_zstd_payload(compressed) == hashlib.sha256(payload).hexdigest()
def test_default_reference_manifest_uses_sibling_openpilot(tmp_path):
primary = tmp_path / "starpilot/system/hardware/tici/agnos.json"
reference = tmp_path / "openpilot/openpilot/system/hardware/tici/agnos.json"
primary.parent.mkdir(parents=True)
reference.parent.mkdir(parents=True)
primary.write_text("[]") primary.write_text("[]")
reference.write_text("[]") with pytest.raises(RuntimeError, match="Refusing to use StarPilot"):
resolve_upstream_manifest(str(primary), primary)
assert find_default_reference_manifest(primary) == reference.resolve()
def test_update_manifest_changes_only_system_entry():
original = [
{"name": "boot", "url": "custom-boot", "hash": "boot-hash"},
{"name": "system", "url": "official", "hash": "old", "alt": {"url": "old-alt"}},
]
updated = update_manifest_system_entry(original, "hosted", "new-hash", 123)
assert updated[0] == original[0]
assert updated[1] == {
"name": "system",
"url": "hosted",
"hash": "new-hash",
"hash_raw": "new-hash",
"size": 123,
"sparse": False,
"full_check": False,
"has_ab": True,
"ondevice_hash": "new-hash",
}
assert json.dumps(original)