fix(e2e): AHK driver died on first Log() — no-console stdout write throws

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
This commit is contained in:
Teknium
2026-08-11 00:47:15 -07:00
committed by ethernet
parent 381cc65c78
commit af7b3d3af4
2 changed files with 116 additions and 41 deletions
+113 -40
View File
@@ -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.
+3 -1
View File
@@ -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.