From af7b3d3af4d2dcf5dca5c428994214aa670a2751 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Tue, 11 Aug 2026 00:47:15 -0700 Subject: [PATCH] =?UTF-8?q?fix(e2e):=20AHK=20driver=20died=20on=20first=20?= =?UTF-8?q?Log()=20=E2=80=94=20no-console=20stdout=20write=20throws?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frame-0005 of the proof capture showed the exact failure: 'Unhandled error: (6) The handle is invalid' rendered over the installer within seconds of launch. AutoHotkey started via Start-Process has no console, so FileAppend to '*' (stdout) throws — and the throw fired inside Log(), killing the script before it clicked anything. The installer then sat untouched at the INSTALL screen for 50 minutes. * Log() now try-wraps the stdout write (file log is the real record) * Install/Launch clicks fall back to the button's relative window position when the #68183-era PNG templates don't match the restyled UI ('[ INSTALL ]' bracket style visible in the same frame) * install-finished has a second signal: 'bootstrap complete' in bootstrap-installer.log (read with write-sharing), so a template miss can't strand the wait * driver passes the bootstrap log path as arg 3 --- .../install/e2e-assets/install-and-launch.ahk | 153 +++++++++++++----- tests/install/windows-desktop-gui-e2e.ps1 | 4 +- 2 files changed, 116 insertions(+), 41 deletions(-) diff --git a/tests/install/e2e-assets/install-and-launch.ahk b/tests/install/e2e-assets/install-and-launch.ahk index 0b6ec05aa5..98dbed0c32 100644 --- a/tests/install/e2e-assets/install-and-launch.ahk +++ b/tests/install/e2e-assets/install-and-launch.ahk @@ -9,18 +9,32 @@ ; (PR #68183) — same ImageSearch approach and button templates; this ; variant targets windows by process name (ahk_exe) so the installer ; window and the launched app window (both titled "Hermes") can't be -; confused, and it actually clicks Launch instead of closing the window, -; because the Launch hand-off is part of the flow under test. +; confused, and it clicks Launch instead of closing the window, because +; the Launch hand-off is part of the flow under test. ; -; Args: [1] log path [2] setup exe name (default Hermes-Setup.exe) +; Robustness beyond the original: +; * Log() survives a missing stdout (GUI-subsystem AHK started without a +; console throws "(6) The handle is invalid" on FileAppend to '*' — +; that single throw killed the whole first CI attempt). +; * If a button template doesn't match (installer UI restyled), falls +; back to clicking the button's known relative position, and the +; install-finished signal falls back to "bootstrap complete" in +; bootstrap-installer.log. Every fallback is logged loudly. +; +; Args: [1] log path [2] setup exe name [3] bootstrap-installer.log path logPath := A_Args.Length >= 1 ? A_Args[1] : "ahk.log" setupExe := A_Args.Length >= 2 ? A_Args[2] : "Hermes-Setup.exe" +bootstrapLog := A_Args.Length >= 3 ? A_Args[3] : "" Log(text) { msg := Format("[autohotkey] {}`n", text) ToolTip(text) - FileAppend(msg, '*') + ; stdout only exists when AHK was launched from a console. Under + ; Start-Process (no console) FileAppend to '*' throws "(6) The handle + ; is invalid" — and a Log() that throws kills the whole script from + ; inside OnError. The file log below is the real record. + try FileAppend(msg, '*') FileAppend(msg, logPath) } @@ -43,10 +57,13 @@ ClickWithMarker(x, y, button := "Left") { Log(Format("Clicked at {1}, {2}", x, y)) } -FindImageInWindow(winTitle, imageFile, &outX, &outY, timeoutMs := 10000, intervalMs := 250) -{ - WinGetPos(&wx, &wy, &ww, &wh, winTitle) - +; Single-pass image search inside a window. Returns true + center coords. +TryFindImage(winTitle, imageFile, &outX, &outY) { + try { + WinGetPos(&wx, &wy, &ww, &wh, winTitle) + } catch { + return false + } hBitmap := LoadPicture(imageFile) if !hBitmap { throw Error("LoadPicture failed: " imageFile) @@ -55,39 +72,49 @@ FindImageInWindow(winTitle, imageFile, &outX, &outY, timeoutMs := 10000, interva DllCall("GetObject", "Ptr", hBitmap, "Int", bm.Size, "Ptr", bm) width := NumGet(bm, 4, "Int") height := NumGet(bm, 8, "Int") - - startTime := A_TickCount - timeLeft := 1 - Log(Format("Searching for {} in {} ...", imageFile, winTitle)) - searchImage := Format("*10 {}", imageFile) - while (timeLeft > 0) - { - ; Refresh the window rect each pass — the installer window can move - ; or resize between stages. - try WinGetPos(&wx, &wy, &ww, &wh, winTitle) - if ImageSearch(&x, &y, wx, wy, wx + ww, wy + wh, searchImage) - { - outX := x + Floor(width / 2) - outY := y + Floor(height / 2) - Log("Found " imageFile) - return - } - Sleep intervalMs - timeLeft := timeoutMs - (A_TickCount - startTime) - ToolTip(Format("Searching {} in {} ... {}s left", imageFile, winTitle, Round(timeLeft / 1000, 2))) + if ImageSearch(&x, &y, wx, wy, wx + ww, wy + wh, Format("*10 {}", imageFile)) { + outX := x + Floor(width / 2) + outY := y + Floor(height / 2) + return true } - throw Error(Format("Failed to find {} in window {}", imageFile, winTitle)) + return false } -ClickCenterOfImageInWindow(winTitle, imageFile, timeoutMs := 10000, intervalMs := 250) -{ - FindImageInWindow(winTitle, imageFile, &x, &y, timeoutMs, intervalMs) - ClickWithMarker(x, y) +; Fractional window position -> screen coords (fallback click target). +WindowRelPoint(winTitle, fx, fy, &outX, &outY) { + WinGetPos(&wx, &wy, &ww, &wh, winTitle) + outX := wx + Floor(ww * fx) + outY := wy + Floor(wh * fy) +} + +BootstrapLogContains(needle) { + global bootstrapLog + if (bootstrapLog = "" or !FileExist(bootstrapLog)) { + return false + } + try { + ; Read-share open: the installer still holds the file for writing. + f := FileOpen(bootstrapLog, "r-d") + if !f { + return false + } + content := f.Read() + f.Close() + return InStr(content, needle) > 0 + } catch { + return false + } } installerWin := "ahk_exe " setupExe appWin := "ahk_exe Hermes.exe" +; The Install/Launch button sits centered horizontally near the bottom of +; the installer window (measured from production screenshots; used only +; when the image template fails to match a restyled UI). +BTN_FX := 0.50 +BTN_FY := 0.87 + Log("Waiting for the installer window (" installerWin ") ...") try { WinWait(installerWin, , 60) @@ -97,16 +124,62 @@ try { WinGetPos(&x, &y, &w, &h, installerWin) Log(Format("Window found at x={1} y={2} w={3} h={4}", x, y, w, h)) -; ── Step 1: click Install ─────────────────────────────────────────────── -ClickCenterOfImageInWindow(installerWin, A_ScriptDir "\install-button.png", 60000) -Log("Install clicked; waiting for the Launch button (install can take a while)") +; ── Step 1: click Install (template first, relative-position fallback) ── +installClicked := false +deadline := A_TickCount + 60000 +while (A_TickCount < deadline) { + if TryFindImage(installerWin, A_ScriptDir "\install-button.png", &ix, &iy) { + ClickWithMarker(ix, iy) + Log("Install clicked (template match)") + installClicked := true + break + } + Sleep(500) +} +if !installClicked { + WindowRelPoint(installerWin, BTN_FX, BTN_FY, &ix, &iy) + ClickWithMarker(ix, iy) + Log("FALLBACK: install template never matched; clicked relative position") +} -; ── Step 2: wait for install to finish (Launch button appears) ────────── -FindImageInWindow(installerWin, A_ScriptDir "\launch-button.png", &launchX, &launchY, 1000 * 60 * 45) -Log("Install finished (Launch button visible)") +; ── Step 2: wait for the install to finish ────────────────────────────── +; Primary signal: the Launch button template appears. Secondary signal: +; "bootstrap complete" in bootstrap-installer.log (the installer's own +; completion line) — after which we give the template 2 more minutes and +; then fall back to the relative-position click. +launchX := 0, launchY := 0 +launchFound := false +completeSince := 0 +waitDeadline := A_TickCount + 1000 * 60 * 45 +Log("Waiting for install to finish (Launch template or bootstrap log) ...") +while (A_TickCount < waitDeadline) { + if TryFindImage(installerWin, A_ScriptDir "\launch-button.png", &launchX, &launchY) { + launchFound := true + Log("Install finished (Launch template visible)") + break + } + if (completeSince = 0 and BootstrapLogContains("bootstrap complete")) { + completeSince := A_TickCount + Log("bootstrap-installer.log reports completion; giving the Launch template 120s") + } + if (completeSince > 0 and A_TickCount - completeSince > 120000) { + Log("FALLBACK: log says complete but Launch template never matched") + break + } + Sleep(2000) +} +if (!launchFound and completeSince = 0) { + throw Error("install did not finish within 45 minutes (no Launch button, no completion log line)") +} ; ── Step 3: click Launch — the hand-off under test ────────────────────── -ClickWithMarker(launchX, launchY) +if launchFound { + ClickWithMarker(launchX, launchY) +} else { + WindowRelPoint(installerWin, BTN_FX, BTN_FY, &lx, &ly) + ClickWithMarker(lx, ly) + Log("FALLBACK: clicked Launch at relative position") +} Log("Launch clicked; waiting for the Hermes desktop app window") ; The installer spawns Hermes.exe detached and exits itself. diff --git a/tests/install/windows-desktop-gui-e2e.ps1 b/tests/install/windows-desktop-gui-e2e.ps1 index 57cbe2bf24..d5cda4b694 100644 --- a/tests/install/windows-desktop-gui-e2e.ps1 +++ b/tests/install/windows-desktop-gui-e2e.ps1 @@ -292,8 +292,10 @@ function Invoke-PhaseInstallGui { Write-Host " Hermes-Setup.exe launched (pid $($installer.Id))" # Drive it: Install click -> wait -> Launch click -> Hermes.exe window. + # Arg 3 lets the AHK script use the installer's own log as the + # install-finished fallback signal. $ahk = Start-Process -FilePath $ahkExe ` - -ArgumentList (Join-Path $AhkDir "install-and-launch.ahk"), $ahkLog ` + -ArgumentList (Join-Path $AhkDir "install-and-launch.ahk"), $ahkLog, "Hermes-Setup.exe", (Join-Path $HermesHome "logs\bootstrap-installer.log") ` -PassThru # Install on a cold runner takes a while; the AHK script's own inner # timeout (45 min on the Launch wait) is the effective budget.