fix(template): restore tty state before snapshot prompt to fix ^M issue
pypsrp alters terminal tty settings during WinRM sessions. This causes
readline() at the snapshot prompt to receive \r (not \n) on Enter,
printing ^M repeatedly without ever unblocking.
Fix: save termios attributes at command start (before any WinRM work,
terminal is clean) and restore them with tcsetattr(TCSADRAIN) before
the interactive snapshot prompt. Gracefully skips save/restore when
stdin is not a tty (tests, pipes, CI).
Revert the pre-flight snapshot check approach (commit f6f8ffc) — checking
at start was architecturally wrong (snapshot state may change during
provisioning). The prompt stays at the end where it belongs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -432,26 +432,21 @@ 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)
|
||||||
|
|
||||||
# --- Pre-flight snapshot conflict check (terminal still clean here) ---
|
# Save tty state now (terminal clean) so we can restore it before any
|
||||||
# Must happen before any WinRM/pypsrp calls, which alter tty state and
|
# interactive prompt at the end — pypsrp alters tty settings during
|
||||||
# prevent readline() from receiving \n on Enter.
|
# WinRM sessions, causing readline() to receive \r instead of \n.
|
||||||
|
_tty_fd = sys.stdin.fileno() if sys.stdin.isatty() else -1
|
||||||
|
_saved_tty_attrs: list[object] = []
|
||||||
|
if _tty_fd >= 0: # pragma: no cover
|
||||||
|
try:
|
||||||
|
import termios
|
||||||
|
_saved_tty_attrs.append(termios.tcgetattr(_tty_fd))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
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} ...")
|
||||||
@@ -680,10 +675,31 @@ 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 — conflict already resolved at pre-flight above.
|
# Restore tty state before interactive prompt — pypsrp alters terminal
|
||||||
|
# settings and readline() would receive \r instead of \n on Enter.
|
||||||
|
if _saved_tty_attrs and _tty_fd >= 0: # pragma: no cover
|
||||||
|
try:
|
||||||
|
import termios
|
||||||
|
termios.tcsetattr(_tty_fd, termios.TCSADRAIN, _saved_tty_attrs[0]) # type: ignore[arg-type]
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Create snapshot
|
||||||
vmrun_exe = backend.vmrun_path
|
vmrun_exe = backend.vmrun_path
|
||||||
if snapshot in backend.list_snapshots(handle):
|
existing = backend.list_snapshots(handle)
|
||||||
click.echo(f"[prepare-win] Deleting existing snapshot '{snapshot}' ...")
|
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}' ...")
|
||||||
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user