Skip to content

fix(v3/windows): don't fatal on GetCursorPos failure (locked/secure desktop)#5789

Open
wayneforrest wants to merge 1 commit into
wailsapp:masterfrom
wayneforrest:fix/getcursorpos-pr
Open

fix(v3/windows): don't fatal on GetCursorPos failure (locked/secure desktop)#5789
wayneforrest wants to merge 1 commit into
wailsapp:masterfrom
wayneforrest:fix/getcursorpos-pr

Conversation

@wayneforrest

@wayneforrest wayneforrest commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Problem

On Windows, w32.GetAllScreens() queries GetCursorPos (only to set the cosmetic Screen.IsCurrent flag) and treats a failure as a hard error:

ret, _, _ := procGetCursorPos.Call(uintptr(unsafe.Pointer(&cursor)))
if ret == 0 {
    return nil, fmt.Errorf("GetCursorPos failed")
}

GetCursorPos returns FALSE / ERROR_ACCESS_DENIED whenever the calling process is not attached to the interactive input desktop — the workstation is locked, on the secure/UAC desktop, or mid session switch (logon, RDP, fast-user-switch).

At startup this is fatal. processAndCacheScreens() runs during App.Run() (via newPlatformApp) and turns the error into os.Exit(1):

err = result.processAndCacheScreens()
if err != nil {
    app.handleFatalError(err) // -> os.Exit(1)
}

So an app launched onto a non-interactive desktop dies before it ever shows, printing the "catastrophic failure … GetCursorPos failed" banner. This is reproducible for packaged/MSIX apps registered as a StartupTask (the shell launches them at logon, before the session's input desktop is switched away from Winlogon), and for background-update restarts that land while the machine is locked. We hit it in the field on a Microsoft Store build.

Fix

The cursor position is used only to compute IsCurrent (which monitor the pointer is on), and that field has no readers — it isn't even propagated to the application-level Screen. So a failed query should degrade gracefully instead of aborting:

  • GetAllScreens now reads the cursor through a small package seam and, when it fails, leaves IsCurrent = false and keeps enumerating. The flag self-corrects on the next WM_DISPLAYCHANGE / SPI_SETWORKAREA re-cache, which already handles errors non-fatally via handleError.
  • Win32Menu.ShowAtCursor() had the same globalApplication.fatal("GetCursorPos failed"). A context menu simply cannot be shown on a non-input desktop, so it now returns (with a debug log) — matching the already-graceful windowsMenu.ShowAtCursor(), which does if !ok { return }.

A Windows regression test overrides the seam to simulate the failure and asserts GetAllScreens no longer returns a cursor error and marks no screen current.

Notes

  • Behaviour-preserving on the happy path; no public API change.
  • The two spots were inconsistent before this: windowsMenu.ShowAtCursor() already tolerated the failure while Win32Menu.ShowAtCursor() and GetAllScreens() treated it as fatal.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Prevented cursor-position errors from crashing the application when opening popup menus.
    • Improved screen detection reliability when the cursor position cannot be retrieved.
    • The app now continues operating without incorrectly marking a screen as current when cursor information is unavailable.
  • Tests

    • Added coverage for cursor-position failures during popup menu and screen handling.

…esktop)

GetAllScreens() aborted with "GetCursorPos failed" when GetCursorPos
returned FALSE. GetCursorPos returns ERROR_ACCESS_DENIED whenever the
calling process is not on the interactive input desktop - the workstation
is locked, on the secure/UAC desktop, or mid session switch (logon, RDP,
fast-user-switch). At startup, processAndCacheScreens() turns that error
into a fatal os.Exit, so an app launched onto a locked desktop (e.g. an
MSIX StartupTask firing at logon before the desktop is interactive) dies
before it ever shows.

The cursor is only used to set the cosmetic IsCurrent flag, which has no
readers. Degrade gracefully: query the cursor through a seam, and when it
fails leave IsCurrent=false and keep enumerating. The flag self-corrects
on the next WM_DISPLAYCHANGE re-cache (already non-fatal).

Also fix the sibling Win32Menu.ShowAtCursor(), which fatal()s on the same
failure; a context menu simply cannot be shown on a non-input desktop, so
skip it - matching windowsMenu.ShowAtCursor()'s graceful return.

Adds a Windows regression test that overrides the cursor seam.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jmjTLaZ8nFB5veaPjRnhA
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e60de67f-8867-4fa0-8831-95d348bf0396

📥 Commits

Reviewing files that changed from the base of the PR and between 107e42c and 9cece36.

📒 Files selected for processing (3)
  • v3/pkg/application/popupmenu_windows.go
  • v3/pkg/w32/screen.go
  • v3/pkg/w32/screen_cursorfail_test.go

Walkthrough

Cursor-position failures on Windows are now handled gracefully. Popup menus log and return instead of terminating, while screen enumeration continues without marking a current monitor when the cursor location is unavailable. Windows-only tests cover both behaviors and the default cursor-position seam.

Changes

Cursor Failure Handling

Layer / File(s) Summary
Graceful screen enumeration
v3/pkg/w32/screen.go, v3/pkg/w32/screen_cursorfail_test.go
Screen enumeration tolerates cursor-position failure, leaves all screens non-current when the position is unknown, and adds Windows-only regression and seam tests.
Nonfatal popup positioning
v3/pkg/application/popupmenu_windows.go
ShowAtCursor logs cursor-position failure and returns without terminating the application.

Estimated code review effort: 2 (Simple) | ~10 minutes

Poem

I’m a rabbit with a menu to show,
But lost cursor paths can make it slow.
Screens still count, no crash in sight,
No monitor claims the missing light.
Hop, hop—graceful handling done! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main Windows fix: handling GetCursorPos failures without fatal errors.
Description check ✅ Passed The description covers the problem, fix, motivation, and testing, with only template sections like issue reference and checklists missing.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain modules listed in go.work or their selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@wayneforrest

Copy link
Copy Markdown
Contributor Author

cc @leaanthony @atterpac

One more PR - then my app will be crash free.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant