diff --git a/.github/workflows/windows-arm64-diagnostics.yml b/.github/workflows/windows-arm64-diagnostics.yml index 6f9abec..daaf868 100644 --- a/.github/workflows/windows-arm64-diagnostics.yml +++ b/.github/workflows/windows-arm64-diagnostics.yml @@ -56,21 +56,13 @@ jobs: whoami /groups | Select-String -Pattern 'Mandatory|Administrators' foreach ($name in 'pwsh', 'powershell', 'cmd', 'python') { $command = Get-Command $name -ErrorAction SilentlyContinue - if ($null -eq $command) { "$name: not found"; continue } + if ($null -eq $command) { "${name}: not found"; continue } $bytes = [IO.File]::ReadAllBytes($command.Source) $offset = [BitConverter]::ToInt32($bytes, 0x3C) $machine = [BitConverter]::ToUInt16($bytes, $offset + 4) "{0}: {1} machine={2:x4}" -f $name, $command.Source, $machine } python -c "import platform, sys; print(sys.version, platform.machine())" - - name: ConPTY smoke with an ordinary token - if: always() - shell: pwsh - run: python scripts/conformance/windows_standard_user.py scripts/conformance/conpty_smoke.py --runtime diagnostic-runtime/runtime --output diagnostic-output/smoke-ordinary.json - - name: ConPTY smoke with the runner token - if: always() - shell: pwsh - run: python scripts/conformance/conpty_smoke.py --runtime diagnostic-runtime/runtime --output diagnostic-output/smoke-runner.json - name: Conformance with the side-loaded runtime if: always() shell: pwsh @@ -82,6 +74,14 @@ jobs: Copy-Item -Recurse diagnostic-runtime diagnostic-runtime-inbox Remove-Item diagnostic-runtime-inbox/runtime/conpty.dll, diagnostic-runtime-inbox/runtime/OpenConsole.exe python scripts/conformance/windows_standard_user.py scripts/conformance/run.py --app diagnostic-runtime-inbox/pebrel.exe --platform windows-aarch64 --output diagnostic-output/inbox/windows-arm64-report.json --no-platform-golden + - name: ConPTY smoke with an ordinary token + if: always() + shell: pwsh + run: python scripts/conformance/windows_standard_user.py scripts/conformance/conpty_smoke.py --runtime diagnostic-runtime/runtime --wait 25 --prompt-script "$env:TEMP/pebrel_prompt.ps1" --output diagnostic-output/smoke-ordinary.json + - name: ConPTY smoke with the runner token + if: always() + shell: pwsh + run: python scripts/conformance/conpty_smoke.py --runtime diagnostic-runtime/runtime --wait 25 --prompt-script "$env:TEMP/pebrel_prompt.ps1" --output diagnostic-output/smoke-runner.json - uses: actions/upload-artifact@v4 if: always() with: diff --git a/scripts/conformance/conpty_smoke.py b/scripts/conformance/conpty_smoke.py index cfad172..8b6f1ee 100644 --- a/scripts/conformance/conpty_smoke.py +++ b/scripts/conformance/conpty_smoke.py @@ -82,7 +82,14 @@ def api(library: str | None) -> tuple[CreateFn, CloseFn, str]: return create, close, library or "kernel32" -def run_case(library: str | None, command: str, flags: int, wait_seconds: float) -> dict[str, object]: +def run_case( + library: str | None, + command: str, + flags: int, + wait_seconds: float, + done_marker: bytes = b"CONPTY_SMOKE_OK", +) -> dict[str, object]: + """Interactive shells never exit; the marker ends the wait once it renders.""" result: dict[str, object] = {"library": library or "kernel32", "command": command, "flags": flags} try: create, close, _ = api(library) @@ -142,11 +149,16 @@ def run_case(library: str | None, command: str, flags: int, wait_seconds: float) thread.start() deadline = time.monotonic() + wait_seconds first_output_ms: int | None = None + marker_ms: int | None = None while time.monotonic() < deadline: if chunks and first_output_ms is None: first_output_ms = round((time.monotonic() - started) * 1000) + if marker_ms is None and done_marker in b"".join(chunks): + marker_ms = round((time.monotonic() - started) * 1000) + break if kernel.WaitForSingleObject(process.hProcess, 100) == 0: break + result["marker_ms"] = marker_ms code = wt.DWORD() kernel.GetExitCodeProcess(process.hProcess, ctypes.byref(code)) if code.value == 259: # STILL_ACTIVE @@ -178,7 +190,13 @@ def main() -> int: parser.add_argument("--runtime", help="directory holding conpty.dll and OpenConsole.exe") parser.add_argument("--wait", type=float, default=15.0) parser.add_argument("--output") + parser.add_argument( + "--prompt-script", + help="also start Windows PowerShell interactively with this integration script, like Pebrel does", + ) args = parser.parse_args() + # Terminal bytes include private-use glyphs that legacy code pages reject. + sys.stdout.reconfigure(encoding="utf-8", errors="replace") shells = [] for name in ("pwsh", "powershell", "cmd"): @@ -207,6 +225,23 @@ def main() -> int: case["shell"] = name print(json.dumps(case, ensure_ascii=False), flush=True) report["cases"].append(case) + powershell = dict(shells).get("powershell") + if args.prompt_script and powershell: + # The prompt function emits OSC 133;A once PowerShell reaches its + # first interactive prompt; a bare interactive shell shows "PS ". + script = os.path.abspath(args.prompt_script) + interactive = { + "powershell-interactive": (f'"{powershell}" -NoLogo -NoExit', b"PS "), + "powershell-integration": ( + f'"{powershell}" -NoLogo -NoExit -ExecutionPolicy Bypass -Command ". \'{script}\'"', + b"133;A", + ), + } + for name, (command, marker) in interactive.items(): + case = run_case(library, command, PSEUDOCONSOLE_WIN32_INPUT_MODE, args.wait, marker) + case["shell"] = name + print(json.dumps(case, ensure_ascii=False), flush=True) + report["cases"].append(case) if args.output: os.makedirs(os.path.dirname(os.path.abspath(args.output)), exist_ok=True) with open(args.output, "w", encoding="utf-8") as handle: