From 903e7cf35339e50783a1bb3790b06e3204efab4d Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 13 Oct 2026 05:39:59 +0300 Subject: [PATCH] Wait for installer exit and live shell before followup probes --- dev/iso-smoke.py | 12 ++++++++++++ tests/test_iso_smoke.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/dev/iso-smoke.py b/dev/iso-smoke.py index 5fe431b..d834db2 100644 --- a/dev/iso-smoke.py +++ b/dev/iso-smoke.py @@ -505,6 +505,17 @@ class Console: re.escape(token.encode()) + rb':INSTALL_EXIT:[0-9]+\x1f') return self.expect(pattern, timeout, failure_pattern=failure) + def finish_installer(self, token, timeout=30): + # Completion text can arrive before the process exits and readline + # resumes. Input sent during that gap can be discarded by the TTY. + deadline = time.monotonic() + timeout + returned = self.expect(rb'\x1e' + re.escape(token.encode()) + + rb':INSTALL_EXIT:([0-9]+)\x1f', timeout) + status = int(returned.group(1)) + if status: + raise SmokeError(f'Installer returned status {status} after completion') + self.expect(rb'hydra-live[^\r\n]*[#$] ', max(0, deadline - time.monotonic())) + def command(self, command, timeout=60): token = secrets.token_hex(12) self.buffer = b'' @@ -818,6 +829,7 @@ def main(argv=None): console.send(password + '\n') console.expect_installer(rb'Installation completed\. Diagnostic log: [^\r\n]+', args.install_timeout, installer_token) + console.finish_installer(installer_token) checkpoint('real_installer', b'Installer completed via guest controlling TTY') console.command("sudo -n mkdir -p /mnt/hydra-validation && sudo -n mount /dev/vda2 /mnt/hydra-validation && printf %s " + shlex.quote(sentinel) + " | sudo -n tee /mnt/hydra-validation/var/lib/hydra-acceptance-sentinel >/dev/null && sudo -n umount /mnt/hydra-validation", 60) diff --git a/tests/test_iso_smoke.py b/tests/test_iso_smoke.py index 37499c7..5990f30 100644 --- a/tests/test_iso_smoke.py +++ b/tests/test_iso_smoke.py @@ -2,6 +2,7 @@ import importlib.util from pathlib import Path import socket +import select import subprocess import tempfile import threading @@ -152,6 +153,35 @@ class SmokeTests(unittest.TestCase): console.expect_installer(rb'Repeat password: ', 0.05, 'testtoken') self.assertNotIn('test-secret', str(caught.exception)) + def test_installer_followup_waits_for_exit_and_new_shell_prompt(self): + console, guest = self.installer_console() + premature = [] + def respond(): + guest.sendall(b'Installation completed. Diagnostic log: /run/installer.log\r\n') + premature.append(bool(select.select([guest], [], [], 0.05)[0])) + guest.sendall(b'\x1etesttoken:INSTALL_EXIT:0\x1f') + premature.append(bool(select.select([guest], [], [], 0.05)[0])) + guest.sendall(b'\x1b[?2004hhydra-live@hydra-os ~ $ ') + data = b'' + while not data.endswith(b'\n'): + data += guest.recv(65536) + result = subprocess.run(['/bin/sh', '-c', data.decode()], capture_output=True) + guest.sendall(result.stdout) + worker = threading.Thread(target=respond, daemon=True) + worker.start() + console.expect_installer(rb'Installation completed\. Diagnostic log: [^\r\n]+', + 1, 'testtoken') + console.finish_installer('testtoken', timeout=1) + self.assertEqual(console.command('printf ready', timeout=1), b'ready') + worker.join(timeout=1) + self.assertEqual(premature, [False, False], 'command reached installer before shell was ready') + + def test_installer_completion_does_not_override_nonzero_return(self): + console, guest = self.installer_console() + guest.sendall(b'\x1etesttoken:INSTALL_EXIT:1\x1fhydra-live@hydra-os ~ $ ') + with self.assertRaisesRegex(self.mod.SmokeError, 'Installer.*status 1'): + console.finish_installer('testtoken', timeout=0.05) + def test_frame_real_shell_roundtrip_does_not_accept_sentinel_in_echo(self): one, two = socket.socketpair() self.addCleanup(one.close)