From f6f8ffc3d2363fc2fbce29d53dabca3bfbbcfe14 Mon Sep 17 00:00:00 2001 From: Simone Date: Sun, 24 May 2026 02:15:51 +0200 Subject: [PATCH] fix(template): move snapshot conflict check to pre-flight (before WinRM) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pypsrp alters terminal tty state during remote sessions. Any readline() call after WinRM work receives \r instead of \n on Enter (terminal in raw mode), causing the prompt to spin printing ^M and never return. Fix: check for existing snapshot and ask the user at the very beginning of the command, before any WinRM/pypsrp connections are opened, when the terminal is still in canonical mode. If the user declines, abort early. At the end, if the snapshot still exists (race-safe), delete silently and recreate — no second prompt needed. Co-Authored-By: Claude Sonnet 4.6 --- src/ci_orchestrator/commands/template.py | 36 +++++++++++++----------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/ci_orchestrator/commands/template.py b/src/ci_orchestrator/commands/template.py index 3bc15d4..59c35ae 100644 --- a/src/ci_orchestrator/commands/template.py +++ b/src/ci_orchestrator/commands/template.py @@ -432,10 +432,26 @@ def template_prepare_win( if not build_password: build_password = click.prompt(f"New {build_username} password", hide_input=True) - # --- Backend: start VM and detect IP --- + # --- Pre-flight snapshot conflict check (terminal still clean here) --- + # Must happen before any WinRM/pypsrp calls, which alter tty state and + # prevent readline() from receiving \n on Enter. config = load_config() backend = WorkstationVmrunBackend(vmrun_path=config.vmrun_path) handle = VmHandle(identifier=str(vmx_path), name=vmx_path.stem) + existing_snapshots = backend.list_snapshots(handle) + do_recreate_snapshot = True + if snapshot in existing_snapshots: + click.echo(f"[prepare-win] Snapshot '{snapshot}' already exists.") + if recreate_snapshot: + click.echo(f"[prepare-win] --recreate-snapshot set: will delete and recreate.") + else: + sys.stdout.write(f"[prepare-win] Delete and recreate '{snapshot}'? [y/N] ") + sys.stdout.flush() + raw = sys.stdin.readline() + do_recreate_snapshot = raw.strip().lower() in ("y", "yes") + if not do_recreate_snapshot: + click.echo("[prepare-win] Keeping existing snapshot. Provisioning aborted.") + return if not backend.is_running(handle): click.echo(f"[prepare-win] Starting VM: {vmx_path.name} ...") @@ -664,22 +680,10 @@ Register-ScheduledTask -TaskName 'CI-StaticIp' ` raise click.ClickException("VM did not power off within 120s.") click.echo("[prepare-win] VM powered off.") - # Create snapshot + # Create snapshot — conflict already resolved at pre-flight above. vmrun_exe = backend.vmrun_path - existing = backend.list_snapshots(handle) - if snapshot in existing: - click.echo(f"[prepare-win] Snapshot '{snapshot}' already exists.") - if recreate_snapshot: - do_recreate = True - else: - sys.stdout.write(f"[prepare-win] Delete and recreate '{snapshot}'? [y/N] ") - sys.stdout.flush() - raw = sys.stdin.readline() - do_recreate = raw.strip().lower() in ("y", "yes") - if not do_recreate: - click.echo("[prepare-win] Keeping existing snapshot. Provisioning complete.") - return - click.echo(f"[prepare-win] Deleting snapshot '{snapshot}' ...") + if snapshot in backend.list_snapshots(handle): + click.echo(f"[prepare-win] Deleting existing snapshot '{snapshot}' ...") subprocess.run( [vmrun_exe, "-T", "ws", "deleteSnapshot", str(vmx_path), snapshot], check=True,