Restore fast Galaxy software updates

This commit is contained in:
firestar5683
2026-10-15 08:38:50 -05:00
parent 1a137e3af2
commit 8039bd6b2a
3 changed files with 82 additions and 6 deletions
+15 -3
View File
@@ -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.')
@@ -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
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "5cfd05f33d0a6367fd072ab3463b923ca56e8eb23596f0f83ca2ea38c3df9149", "schema_version": 1}
{"protected_inventory_sha256": "2c9c3e53617a5e2309d5b1f28621c560ab0d93906581eaa532e880e7bdc9b95a", "schema_version": 1}