fix(windows): load WTSGetActiveConsoleSessionId from kernel32

getActiveConsoleUser looked up WTSGetActiveConsoleSessionId in
wtsapi32.dll. Windows exports it from kernel32.dll, so Find failed on
every host and console-session discovery always returned no user.

Previously, the registry fallbacks hid this. Daemon start now uses
session-only discovery, so on Windows every managed start omitted
username even with a user signed in at the console.

Use windows.WTSGetActiveConsoleSessionId from golang.org/x/sys, which
binds the kernel32 export. Add a Windows test that compares
discoverSessionUser with the active console row of quser.
This commit is contained in:
Dev Scribe authored and Cuong Manh Le committed 2026-10-23 00:05:36 +07:00
1 parent 01f0c0a6a8
commit f76f07efab
2 files changed
+55 -11

No files matched your search

+6 -11
View File
@@ -14,10 +14,9 @@ import (
)
var (
wtsapi32 = windows.NewLazySystemDLL("wtsapi32.dll")
procWTSGetActiveConsoleSessionId = wtsapi32.NewProc("WTSGetActiveConsoleSessionId")
procWTSQuerySessionInformation = wtsapi32.NewProc("WTSQuerySessionInformationW")
procWTSFreeMemory = wtsapi32.NewProc("WTSFreeMemory")
wtsapi32 = windows.NewLazySystemDLL("wtsapi32.dll")
procWTSQuerySessionInformation = wtsapi32.NewProc("WTSQuerySessionInformationW")
procWTSFreeMemory = wtsapi32.NewProc("WTSFreeMemory")
)
const (
@@ -75,12 +74,8 @@ func discoverSessionUser(ctx context.Context) string {
// getActiveConsoleUser gets the username of the active console session
func getActiveConsoleUser(ctx context.Context) string {
// Guard against missing WTS procedures (e.g., Windows Server Core).
if err := procWTSGetActiveConsoleSessionId.Find(); err != nil {
ProxyLogger.Load().Debug().Err(err).Msg("WTSGetActiveConsoleSessionId not available, skipping console session check")
return ""
}
sessionId, _, _ := procWTSGetActiveConsoleSessionId.Call()
// WTSGetActiveConsoleSessionId is exported by kernel32.dll, not wtsapi32.dll.
sessionId := windows.WTSGetActiveConsoleSessionId()
if sessionId == 0xFFFFFFFF { // Invalid session
ProxyLogger.Load().Debug().Msg("no active console session found")
return ""
@@ -95,7 +90,7 @@ func getActiveConsoleUser(ctx context.Context) string {
}
ret, _, _ := procWTSQuerySessionInformation.Call(
0, // WTS_CURRENT_SERVER_HANDLE
sessionId,
uintptr(sessionId),
uintptr(WTSUserName),
uintptr(unsafe.Pointer(&buffer)),
uintptr(unsafe.Pointer(&bytesReturned)),
+49
View File
@@ -0,0 +1,49 @@
//go:build windows
package ctrld
import (
"bufio"
"bytes"
"context"
"os/exec"
"strings"
"testing"
)
// Every procedure that console-session discovery loads at runtime must exist in
// the DLL it is loaded from. A wrong DLL makes Find fail on every host, so
// discovery silently reports no user instead of the signed-in one.
func TestConsoleSessionProceduresResolve(t *testing.T) {
for _, p := range []interface {
Find() error
}{procWTSQuerySessionInformation, procWTSFreeMemory} {
if err := p.Find(); err != nil {
t.Fatalf("console session procedure does not resolve: %v", err)
}
}
}
// discoverSessionUser must agree with Windows' own view of the console session:
// the user shown on the active "console" row of `query user`, or no user when
// that row is absent.
func TestDiscoverSessionUserMatchesConsoleSession(t *testing.T) {
quser, err := exec.LookPath("quser.exe")
if err != nil {
t.Skip("quser.exe not available")
}
// quser exits 1 when no session exists; its output still answers the question.
out, _ := exec.Command(quser).CombinedOutput()
want := ""
sc := bufio.NewScanner(bytes.NewReader(out))
for sc.Scan() {
f := strings.Fields(strings.TrimPrefix(sc.Text(), ">"))
if len(f) >= 4 && strings.EqualFold(f[1], "console") && strings.EqualFold(f[3], "Active") {
want = f[0]
}
}
got := discoverSessionUser(context.Background())
if !strings.EqualFold(got, want) {
t.Fatalf("discoverSessionUser() = %q, want %q (quser output: %q)", got, want, out)
}
}