psmux_psmux/tests/debug_paste_trace.ps1
Josh 3bf380d452 fix(paste): eliminate local paste duplication on Windows (#197)
Root cause: On Windows, Ctrl+V Release event can arrive BEFORE the
clipboard character injection (async console host behavior). When
paste_confirmed fires with empty paste_pend, the clipboard-read
fallback sends a send-paste. Then the injected chars arrive, get
buffered, trigger stage2, and send a SECOND send-paste with the
same content.

Fix: Every code path that sends paste content now sets
paste_suppress_until (2s window) to block subsequent char
accumulation and clipboard reads:

  - POST_CONFIRMED (post-event flush with pending chars)
  - CLIPBOARD_FALLBACK (Ctrl+V Release with empty buffer)
  - TOP_CONFIRMED (top-of-loop flush)
  - STAGE2_TIMEOUT (300ms no-Ctrl+V fallback)
  - Event::Paste handler (crossterm bracketed paste)

Also suppress send-key C-v on Windows since the console host
already handles clipboard injection. Forwarding C-v to the PTY
causes PowerShell to paste a second time.

SSH: Remove temporary diagnostic from ssh_input.rs feed() tilde
logging (issue #197 VT paste fix verified separately).

Verified: Win32 Ctrl+V test shows exactly 1 server paste.
SSH paste test: all 6 cases pass with no regression.
2026-04-12 03:18:06 +05:30

86 lines
2.9 KiB
PowerShell

#!/usr/bin/env pwsh
# Quick paste debug trace: start psmux, send Ctrl+V via keybd_event, dump log
$ErrorActionPreference = 'Stop'
Add-Type @"
using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Text;
public class PD {
[DllImport("user32.dll")] public static extern void keybd_event(byte bVk,byte bScan,uint dwFlags,IntPtr dwExtra);
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h);
[DllImport("user32.dll")] static extern bool EnumWindows(CallBack cb, IntPtr p);
[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h);
[DllImport("user32.dll")] static extern int GetWindowTextLength(IntPtr h);
[DllImport("user32.dll")] static extern int GetWindowText(IntPtr h, StringBuilder sb, int max);
delegate bool CallBack(IntPtr h, IntPtr p);
public static List<IntPtr> Find(string sub) {
var r = new List<IntPtr>();
EnumWindows((h,p)=>{
if(!IsWindowVisible(h)) return true;
int n=GetWindowTextLength(h); if(n==0) return true;
var sb=new StringBuilder(n+1); GetWindowText(h,sb,sb.Capacity);
if(sb.ToString().IndexOf(sub,StringComparison.OrdinalIgnoreCase)>=0) r.Add(h);
return true;
}, IntPtr.Zero);
return r;
}
}
"@
# Clean up
Stop-Process -Name psmux -Force -EA SilentlyContinue
Start-Sleep -Milliseconds 500
Remove-Item "$env:USERPROFILE\.psmux\input_debug.log" -EA SilentlyContinue
Remove-Item "$env:USERPROFILE\.psmux\client_paste.log" -EA SilentlyContinue
# Set clipboard
Set-Clipboard "PASTETEST1"
# Launch psmux with debug env
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = "$PSScriptRoot\_launch_debug.bat"
$psi.UseShellExecute = $true
$psi.WindowStyle = 'Normal'
$p = [System.Diagnostics.Process]::Start($psi)
Write-Host "Started psmux via cmd, PID=$($p.Id)"
Start-Sleep -Seconds 4
# Find psmux window
$wins = [PD]::Find("psmux")
if ($wins.Count -eq 0) { $wins = [PD]::Find("cmd") }
Write-Host "Found $($wins.Count) windows"
if ($wins.Count -gt 0) {
[PD]::SetForegroundWindow($wins[0]) | Out-Null
Start-Sleep -Milliseconds 500
}
# Ctrl+V
Write-Host "Sending Ctrl+V..."
[PD]::keybd_event(0x11, 0, 0, [IntPtr]::Zero) # Ctrl down
[PD]::keybd_event(0x56, 0, 0, [IntPtr]::Zero) # V down
[PD]::keybd_event(0x56, 0, 2, [IntPtr]::Zero) # V up
[PD]::keybd_event(0x11, 0, 2, [IntPtr]::Zero) # Ctrl up
Start-Sleep -Seconds 3
# Read log
Write-Host "`n===== CLIENT PASTE LOG ====="
$clientLog = "$env:USERPROFILE\.psmux\client_paste.log"
if (Test-Path $clientLog) {
Get-Content $clientLog
} else {
Write-Host "NO CLIENT PASTE LOG"
}
Write-Host "`n===== DEBUG LOG ====="
$logPath = "$env:USERPROFILE\.psmux\input_debug.log"
if (Test-Path $logPath) {
Get-Content $logPath
} else {
Write-Host "NO LOG FILE at $logPath"
}
# Cleanup
Stop-Process -Name psmux -Force -EA SilentlyContinue