mirror of
https://github.com/tailscale/tailscale.git
synced 2025-02-22 12:58:37 +00:00

In this PR, we enable the registration of LocalBackend extensions to exclude code specific to certain platforms or environments. We then introduce desktopSessionsExt, which is included only in Windows builds and only if the ts_omit_desktop_sessions tag is disabled for the build. This extension tracks desktop sessions and switches to (or remains on) the appropriate profile when a user signs in or out, locks their screen, or disconnects a remote session. As desktopSessionsExt requires an ipn/desktop.SessionManager, we register it with tsd.System for the tailscaled subprocess on Windows. We also fix a bug in the sessionWatcher implementation where it attempts to close a nil channel on stop. Updates #14823 Updates tailscale/corp#26247 Signed-off-by: Nick Khyl <nickk@tailscale.com>
673 lines
17 KiB
Go
673 lines
17 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package desktop
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"runtime"
|
|
"sync"
|
|
"syscall"
|
|
"unsafe"
|
|
|
|
"golang.org/x/sys/windows"
|
|
"tailscale.com/ipn/ipnauth"
|
|
"tailscale.com/types/logger"
|
|
"tailscale.com/util/must"
|
|
"tailscale.com/util/set"
|
|
)
|
|
|
|
// wtsManager is a [SessionManager] implementation for Windows.
|
|
type wtsManager struct {
|
|
logf logger.Logf
|
|
ctx context.Context // cancelled when the manager is closed
|
|
ctxCancel context.CancelFunc
|
|
|
|
initOnce func() error
|
|
watcher *sessionWatcher
|
|
|
|
mu sync.Mutex
|
|
sessions map[SessionID]*wtsSession
|
|
initCbs set.HandleSet[SessionInitCallback]
|
|
stateCbs set.HandleSet[SessionStateCallback]
|
|
}
|
|
|
|
// NewSessionManager returns a new [SessionManager] for the current platform,
|
|
func NewSessionManager(logf logger.Logf) (SessionManager, error) {
|
|
ctx, ctxCancel := context.WithCancel(context.Background())
|
|
m := &wtsManager{
|
|
logf: logf,
|
|
ctx: ctx,
|
|
ctxCancel: ctxCancel,
|
|
sessions: make(map[SessionID]*wtsSession),
|
|
}
|
|
m.watcher = newSessionWatcher(m.ctx, m.logf, m.sessionEventHandler)
|
|
|
|
m.initOnce = sync.OnceValue(func() error {
|
|
if err := waitUntilWTSReady(m.ctx); err != nil {
|
|
return fmt.Errorf("WTS is not ready: %w", err)
|
|
}
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if err := m.watcher.Start(); err != nil {
|
|
return fmt.Errorf("failed to start session watcher: %w", err)
|
|
}
|
|
|
|
var err error
|
|
m.sessions, err = enumerateSessions()
|
|
return err // may be nil or non-nil
|
|
})
|
|
return m, nil
|
|
}
|
|
|
|
// Init implements [SessionManager].
|
|
func (m *wtsManager) Init() error {
|
|
return m.initOnce()
|
|
}
|
|
|
|
// Sessions implements [SessionManager].
|
|
func (m *wtsManager) Sessions() (map[SessionID]*Session, error) {
|
|
if err := m.initOnce(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
sessions := make(map[SessionID]*Session, len(m.sessions))
|
|
for _, s := range m.sessions {
|
|
sessions[s.id] = s.AsSession()
|
|
}
|
|
return sessions, nil
|
|
}
|
|
|
|
// RegisterInitCallback implements [SessionManager].
|
|
func (m *wtsManager) RegisterInitCallback(cb SessionInitCallback) (unregister func(), err error) {
|
|
if err := m.initOnce(); err != nil {
|
|
return nil, err
|
|
}
|
|
if cb == nil {
|
|
return nil, errors.New("nil callback")
|
|
}
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
handle := m.initCbs.Add(cb)
|
|
|
|
// TODO(nickkhyl): enqueue callbacks in a separate goroutine?
|
|
for _, s := range m.sessions {
|
|
if cleanup := cb(s.AsSession()); cleanup != nil {
|
|
s.cleanup = append(s.cleanup, cleanup)
|
|
}
|
|
}
|
|
|
|
return func() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
delete(m.initCbs, handle)
|
|
}, nil
|
|
}
|
|
|
|
// RegisterStateCallback implements [SessionManager].
|
|
func (m *wtsManager) RegisterStateCallback(cb SessionStateCallback) (unregister func(), err error) {
|
|
if err := m.initOnce(); err != nil {
|
|
return nil, err
|
|
}
|
|
if cb == nil {
|
|
return nil, errors.New("nil callback")
|
|
}
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
handle := m.stateCbs.Add(cb)
|
|
|
|
// TODO(nickkhyl): enqueue callbacks in a separate goroutine?
|
|
for _, s := range m.sessions {
|
|
cb(s.AsSession())
|
|
}
|
|
|
|
return func() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
delete(m.stateCbs, handle)
|
|
}, nil
|
|
}
|
|
|
|
func (m *wtsManager) sessionEventHandler(id SessionID, event uint32) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
switch event {
|
|
case windows.WTS_SESSION_LOGON:
|
|
// The session may have been created after we started watching,
|
|
// but before the initial enumeration was performed.
|
|
// Do not create a new session if it already exists.
|
|
if _, _, err := m.getOrCreateSessionLocked(id); err != nil {
|
|
m.logf("[unexpected] getOrCreateSessionLocked(%d): %v", id, err)
|
|
}
|
|
case windows.WTS_SESSION_LOCK:
|
|
if err := m.setSessionStatusLocked(id, BackgroundSession); err != nil {
|
|
m.logf("[unexpected] setSessionStatusLocked(%d, BackgroundSession): %v", id, err)
|
|
}
|
|
case windows.WTS_SESSION_UNLOCK:
|
|
if err := m.setSessionStatusLocked(id, ForegroundSession); err != nil {
|
|
m.logf("[unexpected] setSessionStatusLocked(%d, ForegroundSession): %v", id, err)
|
|
}
|
|
case windows.WTS_SESSION_LOGOFF:
|
|
if err := m.deleteSessionLocked(id); err != nil {
|
|
m.logf("[unexpected] deleteSessionLocked(%d): %v", id, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (m *wtsManager) getOrCreateSessionLocked(id SessionID) (_ *wtsSession, created bool, err error) {
|
|
if s, ok := m.sessions[id]; ok {
|
|
return s, false, nil
|
|
}
|
|
|
|
s, err := newWTSSession(id, ForegroundSession)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
m.sessions[id] = s
|
|
|
|
session := s.AsSession()
|
|
// TODO(nickkhyl): enqueue callbacks in a separate goroutine?
|
|
for _, cb := range m.initCbs {
|
|
if cleanup := cb(session); cleanup != nil {
|
|
s.cleanup = append(s.cleanup, cleanup)
|
|
}
|
|
}
|
|
for _, cb := range m.stateCbs {
|
|
cb(session)
|
|
}
|
|
|
|
return s, true, err
|
|
}
|
|
|
|
func (m *wtsManager) setSessionStatusLocked(id SessionID, status SessionStatus) error {
|
|
s, _, err := m.getOrCreateSessionLocked(id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if s.status == status {
|
|
return nil
|
|
}
|
|
|
|
s.status = status
|
|
session := s.AsSession()
|
|
// TODO(nickkhyl): enqueue callbacks in a separate goroutine?
|
|
for _, cb := range m.stateCbs {
|
|
cb(session)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *wtsManager) deleteSessionLocked(id SessionID) error {
|
|
s, ok := m.sessions[id]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
s.status = ClosedSession
|
|
session := s.AsSession()
|
|
// TODO(nickkhyl): enqueue callbacks (and [wtsSession.close]!) in a separate goroutine?
|
|
for _, cb := range m.stateCbs {
|
|
cb(session)
|
|
}
|
|
|
|
delete(m.sessions, id)
|
|
return s.close()
|
|
}
|
|
|
|
func (m *wtsManager) Close() error {
|
|
m.ctxCancel()
|
|
|
|
if m.watcher != nil {
|
|
err := m.watcher.Stop()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.watcher = nil
|
|
}
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.initCbs = nil
|
|
m.stateCbs = nil
|
|
errs := make([]error, 0, len(m.sessions))
|
|
for _, s := range m.sessions {
|
|
errs = append(errs, s.close())
|
|
}
|
|
m.sessions = nil
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
type wtsSession struct {
|
|
id SessionID
|
|
user *ipnauth.WindowsActor
|
|
|
|
status SessionStatus
|
|
|
|
cleanup []func()
|
|
}
|
|
|
|
func newWTSSession(id SessionID, status SessionStatus) (*wtsSession, error) {
|
|
var token windows.Token
|
|
if err := windows.WTSQueryUserToken(uint32(id), &token); err != nil {
|
|
return nil, err
|
|
}
|
|
user, err := ipnauth.NewWindowsActorWithToken(token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &wtsSession{id, user, status, nil}, nil
|
|
}
|
|
|
|
// enumerateSessions returns a map of all active WTS sessions.
|
|
func enumerateSessions() (map[SessionID]*wtsSession, error) {
|
|
const reserved, version uint32 = 0, 1
|
|
var numSessions uint32
|
|
var sessionInfos *windows.WTS_SESSION_INFO
|
|
if err := windows.WTSEnumerateSessions(_WTS_CURRENT_SERVER_HANDLE, reserved, version, &sessionInfos, &numSessions); err != nil {
|
|
return nil, fmt.Errorf("WTSEnumerateSessions failed: %w", err)
|
|
}
|
|
defer windows.WTSFreeMemory(uintptr(unsafe.Pointer(sessionInfos)))
|
|
|
|
sessions := make(map[SessionID]*wtsSession, numSessions)
|
|
for _, si := range unsafe.Slice(sessionInfos, numSessions) {
|
|
status := _WTS_CONNECTSTATE_CLASS(si.State).ToSessionStatus()
|
|
if status == ClosedSession {
|
|
// The session does not exist as far as we're concerned.
|
|
// It may be in the process of being created or destroyed,
|
|
// or be a special "listener" session, etc.
|
|
continue
|
|
}
|
|
id := SessionID(si.SessionID)
|
|
session, err := newWTSSession(id, status)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
sessions[id] = session
|
|
}
|
|
return sessions, nil
|
|
}
|
|
|
|
func (s *wtsSession) AsSession() *Session {
|
|
return &Session{
|
|
ID: s.id,
|
|
Status: s.status,
|
|
// wtsSession owns the user; don't let the caller close it
|
|
User: ipnauth.WithoutClose(s.user),
|
|
}
|
|
}
|
|
|
|
func (m *wtsSession) close() error {
|
|
for _, cleanup := range m.cleanup {
|
|
cleanup()
|
|
}
|
|
m.cleanup = nil
|
|
|
|
if m.user != nil {
|
|
if err := m.user.Close(); err != nil {
|
|
return err
|
|
}
|
|
m.user = nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type sessionEventHandler func(id SessionID, event uint32)
|
|
|
|
// TODO(nickkhyl): implement a sessionWatcher that does not use the message queue.
|
|
// One possible approach is to have the tailscaled service register a HandlerEx function
|
|
// and stream SERVICE_CONTROL_SESSIONCHANGE events to the tailscaled subprocess
|
|
// (the actual tailscaled backend), exposing these events via [sessionWatcher]/[wtsManager].
|
|
//
|
|
// See tailscale/corp#26477 for details and tracking.
|
|
type sessionWatcher struct {
|
|
logf logger.Logf
|
|
ctx context.Context // canceled to stop the watcher
|
|
ctxCancel context.CancelFunc // cancels the watcher
|
|
hWnd windows.HWND // window handle for receiving session change notifications
|
|
handler sessionEventHandler // called on session events
|
|
|
|
mu sync.Mutex
|
|
doneCh chan error // written to when the watcher exits; nil if not started
|
|
}
|
|
|
|
func newSessionWatcher(ctx context.Context, logf logger.Logf, handler sessionEventHandler) *sessionWatcher {
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
return &sessionWatcher{logf: logf, ctx: ctx, ctxCancel: cancel, handler: handler}
|
|
}
|
|
|
|
func (sw *sessionWatcher) Start() error {
|
|
sw.mu.Lock()
|
|
defer sw.mu.Unlock()
|
|
|
|
select {
|
|
case <-sw.ctx.Done():
|
|
return fmt.Errorf("sessionWatcher already stopped: %w", sw.ctx.Err())
|
|
default:
|
|
}
|
|
|
|
if sw.doneCh != nil {
|
|
// Already started.
|
|
return nil
|
|
}
|
|
sw.doneCh = make(chan error, 1)
|
|
|
|
startedCh := make(chan error, 1)
|
|
go sw.run(startedCh, sw.doneCh)
|
|
if err := <-startedCh; err != nil {
|
|
return err
|
|
}
|
|
|
|
// Signal the window to unsubscribe from session notifications
|
|
// and shut down gracefully when the sessionWatcher is stopped.
|
|
context.AfterFunc(sw.ctx, func() {
|
|
sendMessage(sw.hWnd, _WM_CLOSE, 0, 0)
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (sw *sessionWatcher) run(started, done chan<- error) {
|
|
runtime.LockOSThread()
|
|
defer func() {
|
|
runtime.UnlockOSThread()
|
|
close(done)
|
|
}()
|
|
err := sw.createMessageWindow()
|
|
started <- err
|
|
if err != nil {
|
|
return
|
|
}
|
|
pumpThreadMessages()
|
|
}
|
|
|
|
// Stop stops the session watcher and waits for it to exit.
|
|
func (sw *sessionWatcher) Stop() error {
|
|
sw.ctxCancel()
|
|
|
|
sw.mu.Lock()
|
|
doneCh := sw.doneCh
|
|
sw.doneCh = nil
|
|
sw.mu.Unlock()
|
|
|
|
if doneCh != nil {
|
|
return <-doneCh
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const watcherWindowClassName = "Tailscale-SessionManager"
|
|
|
|
var watcherWindowClassName16 = sync.OnceValue(func() *uint16 {
|
|
return must.Get(syscall.UTF16PtrFromString(watcherWindowClassName))
|
|
})
|
|
|
|
var registerSessionManagerWindowClass = sync.OnceValue(func() error {
|
|
var hInst windows.Handle
|
|
if err := windows.GetModuleHandleEx(0, nil, &hInst); err != nil {
|
|
return fmt.Errorf("GetModuleHandle: %w", err)
|
|
}
|
|
wc := _WNDCLASSEX{
|
|
CbSize: uint32(unsafe.Sizeof(_WNDCLASSEX{})),
|
|
HInstance: hInst,
|
|
LpfnWndProc: syscall.NewCallback(sessionWatcherWndProc),
|
|
LpszClassName: watcherWindowClassName16(),
|
|
}
|
|
if _, err := registerClassEx(&wc); err != nil {
|
|
return fmt.Errorf("RegisterClassEx(%q): %w", watcherWindowClassName, err)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
func (sw *sessionWatcher) createMessageWindow() error {
|
|
if err := registerSessionManagerWindowClass(); err != nil {
|
|
return err
|
|
}
|
|
_, err := createWindowEx(
|
|
0, // dwExStyle
|
|
watcherWindowClassName16(), // lpClassName
|
|
nil, // lpWindowName
|
|
0, // dwStyle
|
|
0, // x
|
|
0, // y
|
|
0, // nWidth
|
|
0, // nHeight
|
|
_HWND_MESSAGE, // hWndParent; message-only window
|
|
0, // hMenu
|
|
0, // hInstance
|
|
unsafe.Pointer(sw), // lpParam
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("CreateWindowEx: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (sw *sessionWatcher) wndProc(hWnd windows.HWND, msg uint32, wParam, lParam uintptr) (result uintptr) {
|
|
switch msg {
|
|
case _WM_CREATE:
|
|
err := registerSessionNotification(_WTS_CURRENT_SERVER_HANDLE, hWnd, _NOTIFY_FOR_ALL_SESSIONS)
|
|
if err != nil {
|
|
sw.logf("[unexpected] failed to register for session notifications: %v", err)
|
|
return ^uintptr(0)
|
|
}
|
|
sw.logf("registered for session notifications")
|
|
case _WM_WTSSESSION_CHANGE:
|
|
sw.handler(SessionID(lParam), uint32(wParam))
|
|
return 0
|
|
case _WM_CLOSE:
|
|
if err := destroyWindow(hWnd); err != nil {
|
|
sw.logf("[unexpected] failed to destroy window: %v", err)
|
|
}
|
|
return 0
|
|
case _WM_DESTROY:
|
|
err := unregisterSessionNotification(_WTS_CURRENT_SERVER_HANDLE, hWnd)
|
|
if err != nil {
|
|
sw.logf("[unexpected] failed to unregister session notifications callback: %v", err)
|
|
}
|
|
sw.logf("unregistered from session notifications")
|
|
return 0
|
|
case _WM_NCDESTROY:
|
|
sw.hWnd = 0
|
|
postQuitMessage(0) // quit the message loop for this thread
|
|
}
|
|
return defWindowProc(hWnd, msg, wParam, lParam)
|
|
}
|
|
|
|
func (sw *sessionWatcher) setHandle(hwnd windows.HWND) error {
|
|
sw.hWnd = hwnd
|
|
setLastError(0)
|
|
_, err := setWindowLongPtr(sw.hWnd, _GWLP_USERDATA, uintptr(unsafe.Pointer(sw)))
|
|
return err // may be nil or non-nil
|
|
}
|
|
|
|
func sessionWatcherByHandle(hwnd windows.HWND) *sessionWatcher {
|
|
val, _ := getWindowLongPtr(hwnd, _GWLP_USERDATA)
|
|
return (*sessionWatcher)(unsafe.Pointer(val))
|
|
}
|
|
|
|
func sessionWatcherWndProc(hWnd windows.HWND, msg uint32, wParam, lParam uintptr) (result uintptr) {
|
|
if msg == _WM_NCCREATE {
|
|
cs := (*_CREATESTRUCT)(unsafe.Pointer(lParam))
|
|
sw := (*sessionWatcher)(unsafe.Pointer(cs.CreateParams))
|
|
if sw == nil {
|
|
return 0
|
|
}
|
|
if err := sw.setHandle(hWnd); err != nil {
|
|
return 0
|
|
}
|
|
return defWindowProc(hWnd, msg, wParam, lParam)
|
|
}
|
|
if sw := sessionWatcherByHandle(hWnd); sw != nil {
|
|
return sw.wndProc(hWnd, msg, wParam, lParam)
|
|
}
|
|
return defWindowProc(hWnd, msg, wParam, lParam)
|
|
}
|
|
|
|
func pumpThreadMessages() {
|
|
var msg _MSG
|
|
for getMessage(&msg, 0, 0, 0) != 0 {
|
|
translateMessage(&msg)
|
|
dispatchMessage(&msg)
|
|
}
|
|
}
|
|
|
|
// waitUntilWTSReady waits until the Windows Terminal Services (WTS) is ready.
|
|
// This is necessary because the WTS API functions may fail if called before
|
|
// the WTS is ready.
|
|
//
|
|
// https://web.archive.org/web/20250207011738/https://learn.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsregistersessionnotificationex
|
|
func waitUntilWTSReady(ctx context.Context) error {
|
|
eventName16, err := windows.UTF16PtrFromString(`Global\TermSrvReadyEvent`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
event, err := windows.OpenEvent(windows.SYNCHRONIZE, false, eventName16)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer windows.CloseHandle(event)
|
|
return waitForContextOrHandle(ctx, event)
|
|
}
|
|
|
|
// waitForContextOrHandle waits for either the context to be done or a handle to be signaled.
|
|
func waitForContextOrHandle(ctx context.Context, handle windows.Handle) error {
|
|
contextDoneEvent, cleanup, err := channelToEvent(ctx.Done())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanup()
|
|
|
|
handles := []windows.Handle{contextDoneEvent, handle}
|
|
waitCode, err := windows.WaitForMultipleObjects(handles, false, windows.INFINITE)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
waitCode -= windows.WAIT_OBJECT_0
|
|
if waitCode == 0 { // contextDoneEvent
|
|
return ctx.Err()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// channelToEvent returns an auto-reset event that is set when the channel
|
|
// becomes receivable, including when the channel is closed.
|
|
func channelToEvent[T any](c <-chan T) (evt windows.Handle, cleanup func(), err error) {
|
|
evt, err = windows.CreateEvent(nil, 0, 0, nil)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
cancel := make(chan struct{})
|
|
|
|
go func() {
|
|
select {
|
|
case <-cancel:
|
|
return
|
|
case <-c:
|
|
}
|
|
windows.SetEvent(evt)
|
|
}()
|
|
|
|
cleanup = func() {
|
|
close(cancel)
|
|
windows.CloseHandle(evt)
|
|
}
|
|
|
|
return evt, cleanup, nil
|
|
}
|
|
|
|
type _WNDCLASSEX struct {
|
|
CbSize uint32
|
|
Style uint32
|
|
LpfnWndProc uintptr
|
|
CbClsExtra int32
|
|
CbWndExtra int32
|
|
HInstance windows.Handle
|
|
HIcon windows.Handle
|
|
HCursor windows.Handle
|
|
HbrBackground windows.Handle
|
|
LpszMenuName *uint16
|
|
LpszClassName *uint16
|
|
HIconSm windows.Handle
|
|
}
|
|
|
|
type _CREATESTRUCT struct {
|
|
CreateParams uintptr
|
|
Instance windows.Handle
|
|
Menu windows.Handle
|
|
Parent windows.HWND
|
|
Cy int32
|
|
Cx int32
|
|
Y int32
|
|
X int32
|
|
Style int32
|
|
Name *uint16
|
|
ClassName *uint16
|
|
ExStyle uint32
|
|
}
|
|
|
|
type _POINT struct {
|
|
X, Y int32
|
|
}
|
|
|
|
type _MSG struct {
|
|
HWnd windows.HWND
|
|
Message uint32
|
|
WParam uintptr
|
|
LParam uintptr
|
|
Time uint32
|
|
Pt _POINT
|
|
}
|
|
|
|
const (
|
|
_WM_CREATE = 1
|
|
_WM_DESTROY = 2
|
|
_WM_CLOSE = 16
|
|
_WM_NCCREATE = 129
|
|
_WM_QUIT = 18
|
|
_WM_NCDESTROY = 130
|
|
|
|
// _WM_WTSSESSION_CHANGE is a message sent to windows that have registered
|
|
// for session change notifications, informing them of changes in session state.
|
|
//
|
|
// https://web.archive.org/web/20250207012421/https://learn.microsoft.com/en-us/windows/win32/termserv/wm-wtssession-change
|
|
_WM_WTSSESSION_CHANGE = 0x02B1
|
|
)
|
|
|
|
const _GWLP_USERDATA = -21
|
|
|
|
const _HWND_MESSAGE = ^windows.HWND(2)
|
|
|
|
// _NOTIFY_FOR_ALL_SESSIONS indicates that the window should receive
|
|
// session change notifications for all sessions on the specified server.
|
|
const _NOTIFY_FOR_ALL_SESSIONS = 1
|
|
|
|
// _WTS_CURRENT_SERVER_HANDLE indicates that the window should receive
|
|
// session change notifications for the host itself rather than a remote server.
|
|
const _WTS_CURRENT_SERVER_HANDLE = windows.Handle(0)
|
|
|
|
// _WTS_CONNECTSTATE_CLASS represents the connection state of a session.
|
|
//
|
|
// https://web.archive.org/web/20250206082427/https://learn.microsoft.com/en-us/windows/win32/api/wtsapi32/ne-wtsapi32-wts_connectstate_class
|
|
type _WTS_CONNECTSTATE_CLASS int32
|
|
|
|
// ToSessionStatus converts cs to a [SessionStatus].
|
|
func (cs _WTS_CONNECTSTATE_CLASS) ToSessionStatus() SessionStatus {
|
|
switch cs {
|
|
case windows.WTSActive:
|
|
return ForegroundSession
|
|
case windows.WTSDisconnected:
|
|
return BackgroundSession
|
|
default:
|
|
// The session does not exist as far as we're concerned.
|
|
return ClosedSession
|
|
}
|
|
}
|