From f2a16ec721f13fe6e7bce97608590c3a19a86d79 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Sat, 19 Sep 2026 15:26:19 -0700 Subject: [PATCH] Reconnect persistent event assets after checkout replacement --- roadscore/prototype/native_receiver.sh | 1 + roadscore/prototype/persistent_assets.py | 39 ++++++++++++ roadscore/prototype/test_persistent_assets.py | 59 +++++++++++++++++++ roadscore/tools/bootstrap_event.sh | 1 + 4 files changed, 100 insertions(+) create mode 100644 roadscore/prototype/persistent_assets.py create mode 100644 roadscore/prototype/test_persistent_assets.py diff --git a/roadscore/prototype/native_receiver.sh b/roadscore/prototype/native_receiver.sh index f9bee8fba4..7ec7cc8971 100755 --- a/roadscore/prototype/native_receiver.sh +++ b/roadscore/prototype/native_receiver.sh @@ -3,6 +3,7 @@ set -euo pipefail cd /data/openpilot env -u OPENPILOT_PREFIX -u PARAMS_ROOT /usr/local/venv/bin/python -c 'from openpilot.common.params import Params; assert not Params().get_bool("IsOnroad")' cd /data/roadscore +/usr/local/venv/bin/python prototype/persistent_assets.py exec 9>generated/native_session.lock flock -n 9 || { echo "Another native RoadScore session owns this bench"; exit 1; } power_pid="" diff --git a/roadscore/prototype/persistent_assets.py b/roadscore/prototype/persistent_assets.py new file mode 100644 index 0000000000..c281fb4dd9 --- /dev/null +++ b/roadscore/prototype/persistent_assets.py @@ -0,0 +1,39 @@ +"""Reconnect a replaced checkout to previously provisioned event assets.""" +import argparse +from pathlib import Path + + +def restore_links(root, data=Path('/data')): + root, data = Path(root), Path(data) + assets, state = data / 'roadscore-event-assets', data / 'roadscore-event-state' + if not assets.exists() and not state.exists(): + return [] # Existing installations retain their original layout. + mapping = {name: state / name for name in ('generated', 'results', 'assets', 'runtime.json')} + mapping.update({f'experiments/ace_chestnut_20260916/{name}': assets / 'ace' / name + for name in ('weights', 'vae_weights', 'profiles')}) + missing = [] + for relative, target in mapping.items(): + valid = target.is_file() if relative == 'runtime.json' else target.is_dir() + if not valid: + raise RuntimeError(f'Persistent RoadScore asset missing: {target}; restore it before launch') + link = root / relative + if link.is_symlink(): + if link.resolve() != target.resolve(): + raise RuntimeError(f'Refusing to replace another link: {link}') + elif link.exists(): + raise RuntimeError(f'Refusing to overwrite checkout data: {link}') + else: + missing.append((link, target)) + # Validate the entire map before changing anything. Never delete or move data. + for link, target in missing: + link.parent.mkdir(parents=True, exist_ok=True) + link.symlink_to(target, target_is_directory=target.is_dir()) + return [str(link) for link, _ in missing] + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--root', type=Path, default=Path('/data/roadscore')) + args = parser.parse_args() + for path in restore_links(args.root): + print(f'Reconnected persistent RoadScore path: {path}') diff --git a/roadscore/prototype/test_persistent_assets.py b/roadscore/prototype/test_persistent_assets.py new file mode 100644 index 0000000000..9dc6a74df0 --- /dev/null +++ b/roadscore/prototype/test_persistent_assets.py @@ -0,0 +1,59 @@ +import tempfile +import unittest +from pathlib import Path + +from persistent_assets import restore_links + + +class PersistentAssetsTest(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.addCleanup(self.temp.cleanup) + self.data = Path(self.temp.name) + self.root = self.data / 'checkout' / 'roadscore' + self.root.mkdir(parents=True) + + def provision(self): + for name in ('generated', 'results', 'assets'): + (self.data / 'roadscore-event-state' / name).mkdir(parents=True) + (self.data / 'roadscore-event-state/runtime.json').write_text('{}') + for name in ('weights', 'vae_weights', 'profiles'): + (self.data / 'roadscore-event-assets/ace' / name).mkdir(parents=True) + + def test_replaced_checkout_reconnects_without_copying_and_is_idempotent(self): + self.provision() + marker = self.data / 'roadscore-event-assets/ace/weights/model' + marker.write_bytes(b'preserved') + self.assertEqual(len(restore_links(self.root, self.data)), 7) + self.assertEqual(restore_links(self.root, self.data), []) + self.assertEqual((self.root / 'experiments/ace_chestnut_20260916/weights/model').read_bytes(), b'preserved') + + def test_existing_data_is_never_overwritten_or_partially_modified(self): + self.provision() + (self.root / 'runtime.json').write_text('local settings') + with self.assertRaisesRegex(RuntimeError, 'overwrite'): + restore_links(self.root, self.data) + self.assertEqual((self.root / 'runtime.json').read_text(), 'local settings') + self.assertFalse((self.root / 'generated').exists()) + + def test_wrong_link_rejected_without_modifications(self): + self.provision() + (self.root / 'results').symlink_to(self.data / 'other') + with self.assertRaisesRegex(RuntimeError, 'another link'): + restore_links(self.root, self.data) + self.assertFalse((self.root / 'generated').exists()) + + def test_incomplete_persistent_installation_rejected_before_linking(self): + self.provision() + (self.data / 'roadscore-event-assets/ace/profiles').rmdir() + with self.assertRaisesRegex(RuntimeError, 'missing'): + restore_links(self.root, self.data) + self.assertFalse((self.root / 'generated').exists()) + + def test_unprovisioned_installation_unchanged(self): + self.assertEqual(restore_links(self.root, self.data), []) + self.assertEqual(list(self.root.iterdir()), []) + + +if __name__ == '__main__': + unittest.main() diff --git a/roadscore/tools/bootstrap_event.sh b/roadscore/tools/bootstrap_event.sh index f19c0514ef..7ecaa17e58 100755 --- a/roadscore/tools/bootstrap_event.sh +++ b/roadscore/tools/bootstrap_event.sh @@ -10,6 +10,7 @@ if [ -e /data/roadscore ]; then else ln -s "$root" /data/roadscore fi +/usr/local/venv/bin/python "$root/prototype/persistent_assets.py" --root "$root" mkdir -p "$root"/{generated,results/event_night_one,routes,assets,.cache/uv,tmp} /data/roadscore-feasibility/{cache,tmp} /data/sa3-feasibility # AGNOS /home has only a small overlay. Never use it for package downloads. export UV_CACHE_DIR="$root/.cache/uv" TMPDIR="$root/tmp"