diff --git a/openpilot/starpilot/software/fast_update.py b/openpilot/starpilot/software/fast_update.py index 496d187849..95b6f6503e 100644 --- a/openpilot/starpilot/software/fast_update.py +++ b/openpilot/starpilot/software/fast_update.py @@ -180,8 +180,20 @@ def fast_update(repo, branch, *, params, parked, expected_commit, current_os=Non if not valid_receipt(git, target) and revision_digest(git, previous) != revision_digest(git, target): raise FastUpdateIncompatible('Native build inputs or artifacts changed without a matching build receipt; use a full validated update') tracked = set(git('ls-tree', '-rz', '--name-only', target).split('\0')) - {''} - untracked = set(git('ls-files', '--others', '-z').split('\0')) - {''} - directories = {str(parent) for name in tracked for parent in Path(name).parents if str(parent) != '.'} + indexed = set(git('ls-files', '-z').split('\0')) - {''} + candidates = tracked - indexed + for name in tuple(candidates): + for parent in Path(name).parents: + if str(parent) != '.' and str(parent) not in indexed: + local = repo / parent + if local.is_symlink() or local.exists() and not local.is_dir(): + candidates.add(str(parent)) + paths = sorted(candidates) + untracked = set() + for offset in range(0, len(paths), 256): + untracked.update(git('--literal-pathspecs', 'ls-files', '--others', '-z', '--', *paths[offset:offset + 256]).split('\0')) + untracked.discard('') + directories = {str(parent) for name in tracked for parent in Path(name).parents if str(parent) != '.'} if untracked else set() for name in untracked: if name in tracked or name in directories or any(str(parent) in tracked for parent in Path(name).parents): raise FastUpdateError('Update would overwrite untracked data') @@ -218,7 +230,7 @@ def fast_update(repo, branch, *, params, parked, expected_commit, current_os=Non raise FastUpdateError('Parked state changed before restart') notify('submodules', 'No submodules configured; dependencies are vendored in this release.') notify('rebooting', 'Update complete. Device is rebooting; please wait for reconnection.') - (sleep or time.sleep)(6.0) + (sleep or time.sleep)(0.25) if not parked(): raise FastUpdateError('Parked state changed before restart') notify('complete', 'Update installed. Device is restarting; please wait for reconnection.') diff --git a/openpilot/starpilot/software/tests/test_fast_update.py b/openpilot/starpilot/software/tests/test_fast_update.py index e8a4b75c18..af6a1e9144 100644 --- a/openpilot/starpilot/software/tests/test_fast_update.py +++ b/openpilot/starpilot/software/tests/test_fast_update.py @@ -435,7 +435,7 @@ class TestFastUpdate(unittest.TestCase): self.assertEqual(self.git(self.repo, 'symbolic-ref', '--short', 'HEAD'), 'Other') self.assertEqual(self.git(self.repo, 'rev-parse', 'HEAD'), other) self.assertEqual(self.git(self.repo, 'config', '--get', 'starpilot.previousVersion'), f'{self.previous} Dom') - self.assertIn(('sleep', 6.0), stages) + self.assertIn(('sleep', 0.25), stages) arguments['expected_commit'] = other self.assertEqual(owner.fast_update(self.repo, 'Other', rollback=True, **arguments), 'reboot-requested') self.assertEqual(self.git(self.repo, 'symbolic-ref', '--short', 'HEAD'), 'Dom') @@ -486,7 +486,7 @@ class TestFastUpdate(unittest.TestCase): with self.assertRaises(owner.FastUpdateError): self.update(parked=lambda: parked[0], sleep=notice) - self.assertEqual(waits, [6.0]) + self.assertEqual(waits, [0.25]) self.assertEqual(self.git(self.repo, 'rev-parse', 'HEAD'), self.previous) self.assertFalse(self.params.calls) @@ -500,6 +500,70 @@ class TestFastUpdate(unittest.TestCase): self.assertEqual((self.repo / 'new-model').read_bytes(), b'model') self.assertFalse(self.invalidations) + def test_ignored_model_cache_is_not_scanned_or_removed(self): + self.git(self.repo, 'config', 'core.excludesFile', str(self.repo / '.git/local-ignore')) + (self.repo / '.git/local-ignore').write_text('models/\n') + cache = self.repo / 'models/nested/download' + cache.parent.mkdir(parents=True) + cache.write_bytes(b'keep downloaded model') + commands = [] + + def traced(command, cwd): + commands.append(command) + return owner.run(command, cwd) + + self.assertEqual(self.update(run=traced), 'reboot-requested') + self.assertEqual(cache.read_bytes(), b'keep downloaded model') + self.assertFalse(any('--others' in command for command in commands)) + + def test_ignored_update_collision_is_rejected(self): + self.git(self.repo, 'config', 'core.excludesFile', str(self.repo / '.git/local-ignore')) + (self.repo / '.git/local-ignore').write_text('[[]model]\n') + local = self.repo / '[model]' + local.write_bytes(b'private model') + (self.remote / '[model]').write_bytes(b'new tracked model') + self.git(self.remote, 'add', '[model]') + self.receipt_commit() + with self.assertRaisesRegex(owner.FastUpdateError, 'overwrite untracked data'): + self.update() + self.assertEqual(local.read_bytes(), b'private model') + self.assertEqual(self.git(self.repo, 'rev-parse', 'HEAD'), self.previous) + + def test_untracked_parent_file_and_directory_contents_are_protected(self): + for parent_file in (True, False): + with self.subTest(parent_file=parent_file): + folder = self.repo / 'new-data' + target = self.remote / 'new-data' + if parent_file: + folder.write_bytes(b'private data') + target.mkdir() + (target / 'source').write_text('new tracked source') + else: + folder.mkdir() + (folder / 'download').write_bytes(b'private data') + target.write_text('new tracked source') + self.git(self.remote, 'add', 'new-data') + self.receipt_commit() + with self.assertRaisesRegex(owner.FastUpdateError, 'overwrite untracked data'): + self.update() + self.assertEqual((folder if parent_file else folder / 'download').read_bytes(), b'private data') + self.assertEqual(self.git(self.repo, 'rev-parse', 'HEAD'), self.previous) + self.git(self.remote, 'reset', '--hard', self.target) + if parent_file: + folder.unlink() + else: + (folder / 'download').unlink() + folder.rmdir() + + def test_tracked_directory_can_be_replaced_by_file(self): + self.git(self.remote, 'rm', '-r', 'openpilot/starpilot/ui') + (self.remote / 'openpilot/starpilot').mkdir(parents=True, exist_ok=True) + (self.remote / 'openpilot/starpilot/ui').write_text('replacement') + self.git(self.remote, 'add', '.') + self.receipt_commit() + self.assertEqual(self.update(), 'reboot-requested') + self.assertEqual((self.repo / 'openpilot/starpilot/ui').read_text(), 'replacement') + def test_post_apply_validation_failure_rolls_back(self): original = owner.validate_revision diff --git a/prebuilt.json b/prebuilt.json index a188725664..3258fa5420 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "5cfd05f33d0a6367fd072ab3463b923ca56e8eb23596f0f83ca2ea38c3df9149", "schema_version": 1} +{"protected_inventory_sha256": "2c9c3e53617a5e2309d5b1f28621c560ab0d93906581eaa532e880e7bdc9b95a", "schema_version": 1}