Add terminal VM menu and focused session handoff

This commit is contained in:
Codex
2026-10-13 15:18:03 +03:00
parent b547d6c535
commit c6099103b2
5 changed files with 675 additions and 65 deletions
+136 -18
View File
@@ -4,6 +4,7 @@ import io
import json
import os
import shlex
import shutil
import socket
import subprocess
import tempfile
@@ -13,7 +14,7 @@ from unittest.mock import patch
from xml.etree import ElementTree as ET
from hydra.profiles import Profile
from hydra.runtime import Runtime
from hydra.runtime import Runtime, RuntimeError as VMError
from hydra import session
from hydra.cli import main, parser
@@ -34,7 +35,7 @@ class SessionTests(unittest.TestCase):
self.assertEqual([n.text for n in tree.findall('./desktops/names/name')], ['alpha', 'beta'])
self.assertEqual(tree.findtext('./desktops/popupTime'), '0')
rules = tree.findall('./windowRules/windowRule')
self.assertEqual([r.get('title') for r in rules], ['HydraOS: alpha', 'HydraOS: beta'])
self.assertEqual([r.get('title') for r in rules], ['Hydra OS: alpha', 'Hydra OS: beta'])
for index, rule in enumerate(rules, 1):
self.assertEqual(rule.get('serverDecoration'), 'no')
self.assertEqual(rule.find("./action[@name='SendToDesktop']").get('to'), str(index))
@@ -214,7 +215,7 @@ sys.exit(0)
tree = ET.fromstring(seen[1]['xml'])
self.assertEqual(tree.find('desktops').get('number'), '2')
self.assertEqual([rule.get('title') for rule in tree.findall('./windowRules/windowRule')],
['HydraOS: first', 'HydraOS: second'])
['Hydra OS: first', 'Hydra OS: second'])
self.assertFalse(manager.paths('first').disk.exists())
def test_no_running_vms_opens_recovery_without_start_or_stop(self):
@@ -226,7 +227,7 @@ sys.exit(0)
manifest.write_text(json.dumps({'config': str(self.manager.config), 'state': str(self.manager.state), 'runtime': str(self.manager.runtime), 'names': ['alpha', 'beta'], 'terminal': str(terminal), 'directory': str(self.root)}))
with patch.dict(os.environ, {'CAPTURE': str(capture)}), patch.object(Runtime, 'start', side_effect=AssertionError('must not start')), patch.object(Runtime, 'stop', side_effect=AssertionError('must not stop')):
self.assertEqual(session.session_helper('clients', manifest), 0)
self.assertEqual(json.loads(capture.read_text())[0], '--title=HydraOS-recovery')
self.assertEqual(json.loads(capture.read_text())[0], '--title=Hydra OS recovery')
self.assertEqual(self.manager.status('alpha')['status'], 'stopped')
self.assertFalse(self.manager.paths('alpha').disk.exists())
@@ -265,27 +266,22 @@ else:
return manifest, {'WAYLAND_DISPLAY': 'unused', 'HYDRA_VIEWER': str(viewer), 'RECOVERY_CAPTURE': str(self.root), 'SHELL': str(shell)}
def _check_recovery_vm_name(self, name):
import importlib.util
self.assertIsNotNone(importlib.util.find_spec('hydra.menu'), 'Terminal menu is missing')
from hydra import menu
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()):
with patch.dict(os.environ, env), patch('builtins.input', side_effect=[name, ':quit']), \
patch.object(menu, 'open_vm', return_value=0) as 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.assertEqual(launch.call_args.args[1], name)
self.assertEqual(launch.call_args.kwargs['manifest'], manifest)
self.assertFalse((self.root / 'shell.opened').exists())
self.assertFalse(self.manager.paths(name).disk.exists())
def test_recovery_reconnects_vm_named_s(self):
def test_recovery_selects_vm_named_s(self):
self._check_recovery_vm_name('s')
def test_recovery_reconnects_vm_named_q(self):
def test_recovery_selects_vm_named_q(self):
self._check_recovery_vm_name('q')
def test_recovery_colon_commands_open_shell_and_close_terminal(self):
@@ -297,5 +293,127 @@ else:
self.assertFalse((self.root / 'q.connected').exists())
def test_refresh_payload_selects_fresh_profile_and_legacy_empty_request_is_accepted(self):
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': ['alpha'], 'terminal': '/bin/sh'}))
self.assertTrue(hasattr(session, 'request_refresh'), 'Selected refresh API is missing')
self.assertEqual(session.request_refresh(manifest, selected='beta'), 0)
request = self.root / 'refresh.request'
self.assertEqual(json.loads(request.read_text()), {'selected': 'beta'})
self.assertEqual(request.stat().st_mode & 0o777, 0o600)
request.unlink()
self.assertEqual(session.request_refresh(manifest), 0)
self.assertEqual(request.read_bytes(), b'')
def test_refresh_rejects_invalid_or_removed_selection_without_publishing(self):
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': ['alpha'], 'terminal': '/bin/sh'}))
self.assertTrue(hasattr(session, 'request_refresh'), 'Selected refresh API is missing')
for name in ('missing', '../alpha', 'bad*'):
with self.assertRaises((ValueError, OSError)):
session.request_refresh(manifest, selected=name)
self.assertFalse((self.root / 'refresh.request').exists())
def test_selected_second_workspace_survives_compositor_refresh_without_starting_guests(self):
capture = self.root / 'selected-capture.json'
compositor = self.root / 'selected-labwc'
compositor.write_text('#!' + os.sys.executable + '\n' + r'''import json, os, sys
from pathlib import Path
if '--version' in sys.argv:
print('labwc 0.20.2')
sys.exit(0)
config = Path(sys.argv[sys.argv.index('-C') + 1])
manifest = config.parent / 'session.json'
capture = Path(os.environ['CAPTURE'])
seen = json.loads(capture.read_text()) if capture.exists() else []
seen.append({'xml': (config / 'rc.xml').read_text(), 'manifest_env': os.environ.get('HYDRA_SESSION_MANIFEST'),
'manifest_path': str(manifest)})
capture.write_text(json.dumps(seen))
if len(seen) == 1:
(manifest.parent / 'refresh.request').write_text('{"selected": "beta"}')
''')
compositor.chmod(0o700)
with patch.dict(os.environ, {'HYDRA_LABWC': str(compositor), 'HYDRA_TERMINAL': str(compositor),
'HYDRA_BRIGHTNESS': str(compositor), 'CAPTURE': str(capture)}), \
patch.object(Runtime, 'start', side_effect=AssertionError('session must never start guests')):
self.assertEqual(session.launch_session(self.manager), 0)
seen = json.loads(capture.read_text())
self.assertEqual(len(seen), 2)
self.assertEqual(ET.fromstring(seen[1]['xml']).findtext('./desktops/initial'), '2')
self.assertTrue(all(s['manifest_env'] == s['manifest_path'] for s in seen))
self.assertFalse(self.manager.paths('alpha').disk.exists())
self.assertFalse(self.manager.paths('beta').disk.exists())
def test_recovery_failure_leaves_emergency_shell_accessible(self):
manifest, env = self._recovery_fixture()
import importlib.util
self.assertIsNotNone(importlib.util.find_spec('hydra.menu'), 'Terminal menu is missing')
from hydra import menu
with patch.dict(os.environ, env), patch.object(menu, 'run_menu', side_effect=TypeError('broken menu')), \
patch('builtins.input', side_effect=[':shell', ':quit']), contextlib.redirect_stdout(io.StringIO()) as output:
try:
code = session.session_helper('recover', manifest)
except TypeError as exc:
self.fail(f'Menu failure escaped emergency recovery: {exc}')
self.assertEqual(code, 0)
self.assertIn('emergency', output.getvalue().lower())
self.assertTrue((self.root / 'shell.opened').exists())
def test_runtime_status_error_opens_recovery_without_starting_guests(self):
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': ['alpha'], 'terminal': shutil.which('true')}))
with patch.object(Runtime, 'status', side_effect=VMError('private runtime permission problem')), \
patch.object(Runtime, 'start', side_effect=AssertionError('must not start')), \
contextlib.redirect_stderr(io.StringIO()) as error:
try:
code = session.session_helper('clients', manifest)
except VMError as exc:
self.fail(f'Status failure escaped session recovery: {exc}')
self.assertEqual(code, 0)
self.assertIn('private runtime permission problem', error.getvalue())
def test_invalid_profile_is_excluded_from_compositor_but_valid_profiles_remain(self):
(self.manager.config / 'bad.toml').write_text('memory = "broken"')
capture = self.root / 'invalid-capture.json'
compositor = self.root / 'invalid-labwc'
compositor.write_text('#!' + os.sys.executable + '\n' + r'''import json, os, sys
from pathlib import Path
if '--version' in sys.argv:
print('labwc 0.20.2')
else:
config = Path(sys.argv[sys.argv.index('-C') + 1])
Path(os.environ['CAPTURE']).write_text((config.parent / 'session.json').read_text())
''')
compositor.chmod(0o700)
with patch.dict(os.environ, {'HYDRA_LABWC': str(compositor), 'HYDRA_TERMINAL': str(compositor),
'HYDRA_BRIGHTNESS': str(compositor), 'CAPTURE': str(capture)}), \
contextlib.redirect_stderr(io.StringIO()) as error:
self.assertEqual(session.launch_session(self.manager), 0)
self.assertEqual(json.loads(capture.read_text())['names'], ['alpha', 'beta'])
self.assertIn('bad.toml', error.getvalue())
def test_paused_guests_reconnect_viewers_without_start_or_resume(self):
manifest, env = self._recovery_fixture()
terminal = self.root / 'exit-terminal'
terminal.write_text('#!' + os.sys.executable + '\n')
terminal.chmod(0o700)
data = json.loads(manifest.read_text())
data['terminal'] = str(terminal)
manifest.write_text(json.dumps(data))
with patch.dict(os.environ, env), patch.object(Runtime, 'status', return_value={'status': 'paused'}), \
patch.object(Runtime, 'start', side_effect=AssertionError('must not start/resume')), \
patch.object(Runtime, 'stop', side_effect=AssertionError('must not stop')), \
contextlib.redirect_stderr(io.StringIO()):
self.assertEqual(session.session_helper('clients', manifest), 0)
for name in ('s', 'q'):
self.assertTrue((self.root / (name + '.connected')).exists(), f'Paused {name} was not reconnected')
self.assertFalse(self.manager.paths(name).disk.exists())
if __name__ == '__main__':
unittest.main()