fix(template): move snapshot conflict check to pre-flight (before WinRM)

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 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 02:15:51 +02:00
parent 5d3214d10c
commit f6f8ffc3d2
+20 -16
View File
@@ -432,10 +432,26 @@ def template_prepare_win(
if not build_password: if not build_password:
build_password = click.prompt(f"New {build_username} password", hide_input=True) 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() config = load_config()
backend = WorkstationVmrunBackend(vmrun_path=config.vmrun_path) backend = WorkstationVmrunBackend(vmrun_path=config.vmrun_path)
handle = VmHandle(identifier=str(vmx_path), name=vmx_path.stem) 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): if not backend.is_running(handle):
click.echo(f"[prepare-win] Starting VM: {vmx_path.name} ...") 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.") raise click.ClickException("VM did not power off within 120s.")
click.echo("[prepare-win] VM powered off.") click.echo("[prepare-win] VM powered off.")
# Create snapshot # Create snapshot — conflict already resolved at pre-flight above.
vmrun_exe = backend.vmrun_path vmrun_exe = backend.vmrun_path
existing = backend.list_snapshots(handle) if snapshot in backend.list_snapshots(handle):
if snapshot in existing: click.echo(f"[prepare-win] Deleting existing snapshot '{snapshot}' ...")
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}' ...")
subprocess.run( subprocess.run(
[vmrun_exe, "-T", "ws", "deleteSnapshot", str(vmx_path), snapshot], [vmrun_exe, "-T", "ws", "deleteSnapshot", str(vmx_path), snapshot],
check=True, check=True,