From 4984e8efef07acb5b86781308d444a16e41a4c64 Mon Sep 17 00:00:00 2001 From: Viktor Liu Date: Mon, 3 Aug 2026 10:07:53 +0200 Subject: [PATCH] Draw inverted monochrome cursor pixels so the text caret is visible --- client/vnc/server/agent_windows.go | 24 +++++++++++++++++++++++- client/vnc/server/cursor_windows.go | 23 +++++++++++++++++++---- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/client/vnc/server/agent_windows.go b/client/vnc/server/agent_windows.go index f61cf9ab5..57fb983a7 100644 --- a/client/vnc/server/agent_windows.go +++ b/client/vnc/server/agent_windows.go @@ -43,6 +43,7 @@ var ( procCreateJobObjectW = kernel32.NewProc("CreateJobObjectW") procSetInformationJobObject = kernel32.NewProc("SetInformationJobObject") procAssignProcessToJobObject = kernel32.NewProc("AssignProcessToJobObject") + procIsProcessInJob = kernel32.NewProc("IsProcessInJob") procSetTokenInformation = advapi32.NewProc("SetTokenInformation") procCreateEnvironmentBlock = userenv.NewProc("CreateEnvironmentBlock") procDestroyEnvironmentBlock = userenv.NewProc("DestroyEnvironmentBlock") @@ -323,7 +324,13 @@ func spawnAgentInSession(sessionID uint32, socketPath, authToken string, jobHand if jobHandle != 0 { r, _, e := procAssignProcessToJobObject.Call(uintptr(jobHandle), uintptr(pi.Process)) if r == 0 { - log.Warnf("assign agent to job object: %v (orphan possible on service crash)", e) + // Report the job state of both ends: assignment is refused when the + // child already belongs to a job that will not accept ours, which is + // the difference between a breakaway that did not happen and a + // rights problem on the handles. + log.Warnf("assign agent to job object: %v (orphan possible on service crash); "+ + "daemon in a job: %s, agent in a job: %s", + e, describeInJob(windows.CurrentProcess()), describeInJob(pi.Process)) } } @@ -404,6 +411,21 @@ func newSessionManager() *sessionManager { return m } +// describeInJob reports whether a process belongs to any job object, for the +// diagnostic on a failed assignment. Returns the error text when the query +// itself fails, since that is equally informative there. +func describeInJob(h windows.Handle) string { + var inJob int32 + r, _, e := procIsProcessInJob.Call(uintptr(h), 0, uintptr(unsafe.Pointer(&inJob))) + if r == 0 { + return fmt.Sprintf("unknown (%v)", e) + } + if inJob != 0 { + return "yes" + } + return "no" +} + // createKillOnCloseJob returns a Job Object configured so that closing its // handle (process exit or explicit Close) terminates every process assigned // to it. Used to keep orphaned vnc-agent processes from outliving the service. diff --git a/client/vnc/server/cursor_windows.go b/client/vnc/server/cursor_windows.go index be65a4b1a..51815b7db 100644 --- a/client/vnc/server/cursor_windows.go +++ b/client/vnc/server/cursor_windows.go @@ -337,15 +337,30 @@ func decodeMonoCursor(hbmMask windows.Handle) (*image.RGBA, error) { img := image.NewRGBA(image.Rect(0, 0, int(w), int(h))) for y := int32(0); y < h; y++ { for x := int32(0); x < w; x++ { - and := data[(y*w+x)*4] - xor := data[((y+h)*w+x)*4] + and := data[(y*w+x)*4] != 0 + xor := data[((y+h)*w+x)*4] != 0 di := (y*w + x) * 4 - if and != 0 { + + // The two halves of the mask encode four states: + // + // AND=0 XOR=0 black + // AND=0 XOR=1 white + // AND=1 XOR=0 transparent + // AND=1 XOR=1 inverts whatever is on screen + // + // The cursor pseudo-encoding has no inversion, and the text + // caret (IDC_IBEAM) is drawn almost entirely from inverted + // pixels, which is how it stays legible over both light and dark + // text. Reading them as transparent left it invisible while + // every other stock cursor rendered, so they are drawn black + // here: that is what the other VNC servers settle on, and text + // fields are light far more often than not. + if and && !xor { img.Pix[di+3] = 0 continue } c := byte(0) - if xor != 0 { + if !and && xor { c = 255 } img.Pix[di+0] = c