Browse Source
New src/tests/testsupport/consoleinject.ps1: the G-106 recipe's injection half as a committed helper - FreeConsole/AttachConsole to a target pid's console, open CONIN$, type lines as WriteConsoleInput key events. tclshcmd.test: constraint punkconsole (windows + PUNK_TEST_CONSOLE=1; normal runs skip) gates two tests driving the reopen path end-to-end in the child's OWN hidden console (outer Start-Process -WindowStyle Hidden wrapper owns a fresh console; wrapper runs '<kit> tclsh' there with std handles redirected to files; piped code sets dorepl; injected lines + exit 0 finish it). Pinned: reopened flips 0 -> 1 at reopen with istty immutable at 0, inputbuffer published empty, reopen notice silent by default and STDERR-only under TCLSH_PIPEREPL_DEBUG=1, injected exit completes with code 0 - closing the item-1/7 console-side residue. Deadlines + wrapper-tree kill keep the tests hang-proof; the piped-harness HANG RULE stands for everything else. Verified: PUNK_TEST_CONSOLE=1 run 21/21 against punk9_beta; normal run 19 pass + 2 console skips; shell/*** suite 64 pass / 4 skips / 0 fail. AGENTS updates: src/tests/shell (tclshcmd bullet - coverage now exists, refined hang rule), src/tests (testsupport index + .ps1 helpers wording). Assisted-by: harness=claude; primary-model=claude-fable-5; api-location=anthropic.commaster
5 changed files with 291 additions and 8 deletions
@ -0,0 +1,84 @@
|
||||
# consoleinject.ps1 - type lines into the console of another process (windows). |
||||
# |
||||
# The G-106 hidden-console recipe's injection half, committed for test use (G-118 |
||||
# item 11 console-reopen coverage in src/tests/shell/testsuites/punkexe/tclshcmd.test): |
||||
# detach from our own console (FreeConsole), attach to the target pid's console |
||||
# (AttachConsole), open CONIN$ and write key-down/key-up KEY_EVENT records for each |
||||
# character of each line plus Enter (VK_RETURN + CR) after every line. Events are |
||||
# buffered by the console, so injection may precede the target's console read. |
||||
# |
||||
# The target pid may be ANY process attached to the target console (e.g. a wrapper |
||||
# that launched the real target with redirected std handles in that console). |
||||
# |
||||
# usage: powershell -NoProfile -ExecutionPolicy Bypass -File consoleinject.ps1 -TargetPid <pid> -TextFile <file> |
||||
# stdout: 'INJECT-OK lines=N' on success, 'INJECT-FAIL rc=<1|2|3> lasterr=<winerr>' on failure |
||||
# (rc 1 attach failed, 2 CONIN$ open failed, 3 WriteConsoleInput failed); exit code 0/1. |
||||
param( |
||||
[Parameter(Mandatory=$true)][int]$TargetPid, |
||||
[Parameter(Mandatory=$true)][string]$TextFile |
||||
) |
||||
$ErrorActionPreference = 'Stop' |
||||
Add-Type -TypeDefinition @' |
||||
using System; |
||||
using System.Runtime.InteropServices; |
||||
public static class PunkConsoleInject { |
||||
[DllImport("kernel32.dll", SetLastError=true)] |
||||
public static extern bool FreeConsole(); |
||||
[DllImport("kernel32.dll", SetLastError=true)] |
||||
public static extern bool AttachConsole(uint dwProcessId); |
||||
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)] |
||||
public static extern IntPtr CreateFileW(string lpFileName, uint dwDesiredAccess, uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition, uint dwFlagsAndAttributes, IntPtr hTemplateFile); |
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)] |
||||
public struct KEY_EVENT_RECORD { |
||||
public int bKeyDown; |
||||
public ushort wRepeatCount; |
||||
public ushort wVirtualKeyCode; |
||||
public ushort wVirtualScanCode; |
||||
public char UnicodeChar; |
||||
public uint dwControlKeyState; |
||||
} |
||||
[StructLayout(LayoutKind.Explicit)] |
||||
public struct INPUT_RECORD { |
||||
[FieldOffset(0)] public ushort EventType; |
||||
[FieldOffset(4)] public KEY_EVENT_RECORD KeyEvent; |
||||
} |
||||
[DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)] |
||||
public static extern bool WriteConsoleInputW(IntPtr hConsoleInput, INPUT_RECORD[] lpBuffer, uint nLength, out uint lpNumberOfEventsWritten); |
||||
|
||||
// returns 0 ok; 1 attach failed; 2 conin open failed; 3 write failed. lastErr = GetLastError of the failing call. |
||||
public static int Inject(uint targetPid, string[] lines, out int lastErr) { |
||||
lastErr = 0; |
||||
FreeConsole(); |
||||
if (!AttachConsole(targetPid)) { lastErr = Marshal.GetLastWin32Error(); return 1; } |
||||
IntPtr h = CreateFileW("CONIN$", 0xC0000000u, 0x3u, IntPtr.Zero, 3u, 0u, IntPtr.Zero); |
||||
if (h == (IntPtr)(-1)) { lastErr = Marshal.GetLastWin32Error(); return 2; } |
||||
foreach (string line in lines) { |
||||
string full = line + "\r"; |
||||
INPUT_RECORD[] recs = new INPUT_RECORD[full.Length * 2]; |
||||
int i = 0; |
||||
foreach (char c in full) { |
||||
ushort vk = (c == '\r') ? (ushort)0x0D : (ushort)0; |
||||
KEY_EVENT_RECORD kd = new KEY_EVENT_RECORD(); |
||||
kd.bKeyDown = 1; kd.wRepeatCount = 1; kd.wVirtualKeyCode = vk; |
||||
kd.wVirtualScanCode = 0; kd.UnicodeChar = c; kd.dwControlKeyState = 0; |
||||
KEY_EVENT_RECORD ku = kd; ku.bKeyDown = 0; |
||||
recs[i].EventType = 1; recs[i].KeyEvent = kd; i++; |
||||
recs[i].EventType = 1; recs[i].KeyEvent = ku; i++; |
||||
} |
||||
uint written; |
||||
if (!WriteConsoleInputW(h, recs, (uint)recs.Length, out written)) { lastErr = Marshal.GetLastWin32Error(); return 3; } |
||||
} |
||||
return 0; |
||||
} |
||||
} |
||||
'@ |
||||
$lines = @(Get-Content -LiteralPath $TextFile) |
||||
$lastErr = 0 |
||||
$rc = [PunkConsoleInject]::Inject([uint32]$TargetPid, [string[]]$lines, [ref]$lastErr) |
||||
if ($rc -ne 0) { |
||||
Write-Output "INJECT-FAIL rc=$rc lasterr=$lastErr" |
||||
exit 1 |
||||
} |
||||
Write-Output "INJECT-OK lines=$($lines.Count)" |
||||
exit 0 |
||||
Loading…
Reference in new issue