fix: avoid VM name collisions in recovery commands

This commit is contained in:
Codex
2026-10-13 04:24:38 +03:00
parent 8b3281950a
commit 533ae511b8
2 changed files with 63 additions and 4 deletions
+4 -4
View File
@@ -210,21 +210,21 @@ def session_helper(command: str, manifest: Path) -> int:
if command == 'recover':
print('HydraOS local recovery. VMs keep running independently.\n'
'Ctrl+Alt+Shift+Left/Right switches workspaces; Ctrl+Alt+Shift+R releases input; Ctrl+Alt+Shift+U opens USB.\n'
'Enter a VM name to reconnect, s for a host shell, q to close this terminal.')
'Enter a VM name to reconnect, :shell for a host shell, :quit to close this terminal.')
while True:
try:
print(json.dumps([runtime.status(name) for name in data['names']], indent=2))
choice = input('hydra recovery> ').strip()
except (EOFError, KeyboardInterrupt):
return 0
if choice == 'q':
if choice == ':quit':
return 0
if choice == 's':
if choice == ':shell':
subprocess.run([os.environ.get('SHELL', '/bin/sh')])
elif choice in data['names']:
subprocess.Popen(_module_argv('reconnect:' + choice, manifest), start_new_session=True)
else:
print('Enter a configured VM name, s, or q.')
print('Enter a configured VM name, :shell, or :quit.')
if command.startswith('reconnect:'):
name = command.split(':', 1)[1]
# One viewer per VM inside this session; independent host view commands
+59
View File
@@ -5,6 +5,7 @@ import json
import os
import shlex
import socket
import subprocess
import tempfile
import unittest
from pathlib import Path
@@ -192,6 +193,64 @@ else:
with patch.object(session, 'view', side_effect=AssertionError('must not duplicate')), contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(session.session_helper('reconnect:alpha', manifest), 0)
def _recovery_fixture(self):
for name in ('s', 'q'):
self.manager.add(Profile(name, self.iso))
paths = self.manager.paths(name)
paths.spice.parent.mkdir(parents=True)
fixture = socket.socket(socket.AF_UNIX)
self.addCleanup(fixture.close)
fixture.bind(str(paths.spice))
viewer = self.root / 'viewer'
viewer.write_text('#!' + os.sys.executable + '\n' + '''import os, sys
from pathlib import Path
from urllib.parse import unquote, urlsplit
if '--help' in sys.argv:
print('--hide-toolbar')
else:
name = Path(unquote(urlsplit(sys.argv[-1]).path)).parent.name
Path(os.environ['RECOVERY_CAPTURE'], name + '.connected').write_text(name)
''')
viewer.chmod(0o700)
shell = self.root / 'shell'
shell.write_text('#!' + os.sys.executable + '\nimport os\nfrom pathlib import Path\nPath(os.environ["RECOVERY_CAPTURE"], "shell.opened").touch()\n')
shell.chmod(0o700)
manifest = self.root / 'session.json'
manifest.write_text(json.dumps({'config': str(self.manager.config), 'state': str(self.manager.state), 'runtime': str(self.manager.runtime), 'names': ['s', 'q'], 'terminal': '/bin/sh'}))
return manifest, {'WAYLAND_DISPLAY': 'unused', 'HYDRA_VIEWER': str(viewer), 'RECOVERY_CAPTURE': str(self.root), 'SHELL': str(shell)}
def _check_recovery_vm_name(self, name):
manifest, env = self._recovery_fixture()
processes = []
original_popen = subprocess.Popen
def launch(*args, **kwargs):
process = original_popen(*args, **kwargs)
processes.append(process)
return process
with patch.dict(os.environ, env), patch('builtins.input', side_effect=[name, ':quit', EOFError()]), patch.object(subprocess, 'Popen', launch), contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(session.session_helper('recover', manifest), 0)
for process in processes:
self.assertEqual(process.wait(timeout=5), 0)
connected = self.root / (name + '.connected')
self.assertTrue(connected.exists(), f'VM {name} was not reconnected')
self.assertEqual(connected.read_text(), name)
self.assertFalse((self.root / 'shell.opened').exists())
self.assertFalse(self.manager.paths(name).disk.exists())
def test_recovery_reconnects_vm_named_s(self):
self._check_recovery_vm_name('s')
def test_recovery_reconnects_vm_named_q(self):
self._check_recovery_vm_name('q')
def test_recovery_colon_commands_open_shell_and_close_terminal(self):
manifest, env = self._recovery_fixture()
with patch.dict(os.environ, env), patch('builtins.input', side_effect=[':shell', ':quit', EOFError()]), contextlib.redirect_stdout(io.StringIO()):
self.assertEqual(session.session_helper('recover', manifest), 0)
self.assertTrue((self.root / 'shell.opened').exists())
self.assertFalse((self.root / 's.connected').exists())
self.assertFalse((self.root / 'q.connected').exists())
if __name__ == '__main__':
unittest.main()