fix: avoid VM name collisions in recovery commands
This commit is contained in:
+4
-4
@@ -210,21 +210,21 @@ def session_helper(command: str, manifest: Path) -> int:
|
|||||||
if command == 'recover':
|
if command == 'recover':
|
||||||
print('HydraOS local recovery. VMs keep running independently.\n'
|
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'
|
'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:
|
while True:
|
||||||
try:
|
try:
|
||||||
print(json.dumps([runtime.status(name) for name in data['names']], indent=2))
|
print(json.dumps([runtime.status(name) for name in data['names']], indent=2))
|
||||||
choice = input('hydra recovery> ').strip()
|
choice = input('hydra recovery> ').strip()
|
||||||
except (EOFError, KeyboardInterrupt):
|
except (EOFError, KeyboardInterrupt):
|
||||||
return 0
|
return 0
|
||||||
if choice == 'q':
|
if choice == ':quit':
|
||||||
return 0
|
return 0
|
||||||
if choice == 's':
|
if choice == ':shell':
|
||||||
subprocess.run([os.environ.get('SHELL', '/bin/sh')])
|
subprocess.run([os.environ.get('SHELL', '/bin/sh')])
|
||||||
elif choice in data['names']:
|
elif choice in data['names']:
|
||||||
subprocess.Popen(_module_argv('reconnect:' + choice, manifest), start_new_session=True)
|
subprocess.Popen(_module_argv('reconnect:' + choice, manifest), start_new_session=True)
|
||||||
else:
|
else:
|
||||||
print('Enter a configured VM name, s, or q.')
|
print('Enter a configured VM name, :shell, or :quit.')
|
||||||
if command.startswith('reconnect:'):
|
if command.startswith('reconnect:'):
|
||||||
name = command.split(':', 1)[1]
|
name = command.split(':', 1)[1]
|
||||||
# One viewer per VM inside this session; independent host view commands
|
# One viewer per VM inside this session; independent host view commands
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
import socket
|
import socket
|
||||||
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
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()):
|
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)
|
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__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
Reference in New Issue
Block a user