arblant
firefox-vpn-client / core / internal / desktop / endsession_windows.go
endsession_windows.go168 lines · 4.3 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
//go:build windows

package desktop

import (
	"errors"
	"runtime"
	"sync"
	"unsafe"

	"golang.org/x/sys/windows"
)

// Windows tells programs that the user signs out or the computer shuts down
// only through messages to their top-level windows (WM_QUERYENDSESSION,
// then WM_ENDSESSION), and ends them soon after. The core of the desktop
// app has no window and no console of its own, so without one it would be
// ended before it restores the system proxy, which would then point every
// application at a closed port after the next sign-in. A hidden window
// receives those messages; the work runs before WM_ENDSESSION returns,
// which Windows waits for.

var (
	user32End            = windows.NewLazySystemDLL("user32.dll")
	procRegisterClassExW = user32End.NewProc("RegisterClassExW")
	procCreateWindowExW  = user32End.NewProc("CreateWindowExW")
	procDefWindowProcW   = user32End.NewProc("DefWindowProcW")
	procGetMessageW      = user32End.NewProc("GetMessageW")
	procDispatchMessageW = user32End.NewProc("DispatchMessageW")
	procPostMessageW     = user32End.NewProc("PostMessageW")
	procPostQuitMessage  = user32End.NewProc("PostQuitMessage")
)

const (
	wmDestroy         = 0x0002
	wmClose           = 0x0010
	wmQueryEndSession = 0x0011
	wmEndSession      = 0x0016
)

type wndClassExW struct {
	size       uint32
	style      uint32
	wndProc    uintptr
	clsExtra   int32
	wndExtra   int32
	instance   windows.Handle
	icon       windows.Handle
	cursor     windows.Handle
	background windows.Handle
	menuName   *uint16
	className  *uint16
	iconSm     windows.Handle
}

type msgW struct {
	hwnd    uintptr
	message uint32
	wParam  uintptr
	lParam  uintptr
	time    uint32
	pt      struct{ x, y int32 }
	private uint32
}

// endSession is the one hidden window of the process and what it calls.
var endSession struct {
	mu    sync.Mutex
	onEnd func()
	once  *sync.Once
}

var registerEndSessionClass = sync.OnceValues(func() (*uint16, error) {
	name, _ := windows.UTF16PtrFromString("FirefoxVPNCoreEndSession")
	cls := wndClassExW{
		wndProc:   windows.NewCallback(endSessionWndProc),
		className: name,
	}
	cls.size = uint32(unsafe.Sizeof(cls))
	if r, _, err := procRegisterClassExW.Call(uintptr(unsafe.Pointer(&cls))); r == 0 {
		return nil, err
	}
	return name, nil
})

func endSessionWndProc(hwnd, msg, wParam, lParam uintptr) uintptr {
	switch msg {
	case wmQueryEndSession:
		return 1 // agree: the work happens on WM_ENDSESSION
	case wmEndSession:
		if wParam != 0 {
			endSession.mu.Lock()
			onEnd, once := endSession.onEnd, endSession.once
			endSession.mu.Unlock()
			if onEnd != nil {
				once.Do(onEnd)
			}
		}
		return 0
	case wmDestroy:
		procPostQuitMessage.Call(0)
		return 0
	}
	r, _, _ := procDefWindowProcW.Call(hwnd, msg, wParam, lParam)
	return r
}

// startEndSessionWindow creates the hidden window that calls onEnd (once)
// when Windows ends the session, and returns it with a function that
// removes it.
func startEndSessionWindow(onEnd func()) (uintptr, func(), error) {
	class, err := registerEndSessionClass()
	if err != nil {
		return 0, nil, err
	}
	endSession.mu.Lock()
	endSession.onEnd, endSession.once = onEnd, new(sync.Once)
	endSession.mu.Unlock()

	created := make(chan uintptr, 1)
	failed := make(chan error, 1)
	go func() {
		// A window belongs to the thread that made it and gets its
		// messages there.
		runtime.LockOSThread()
		defer runtime.UnlockOSThread()
		hwnd, _, err := procCreateWindowExW.Call(0, uintptr(unsafe.Pointer(class)), 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
		if hwnd == 0 {
			failed <- err
			return
		}
		created <- hwnd
		var m msgW
		for {
			r, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
			if int32(r) <= 0 {
				return
			}
			procDispatchMessageW.Call(uintptr(unsafe.Pointer(&m)))
		}
	}()
	select {
	case hwnd := <-created:
		stop := func() {
			endSession.mu.Lock()
			endSession.onEnd = nil
			endSession.mu.Unlock()
			// DefWindowProc destroys the window on WM_CLOSE.
			procPostMessageW.Call(hwnd, wmClose, 0, 0)
		}
		return hwnd, stop, nil
	case err := <-failed:
		if err == nil {
			err = errors.New("creating the window failed")
		}
		return 0, nil, err
	}
}

// watchEndSession calls onEnd when Windows signs the user out or shuts
// down; the returned function stops watching.
func watchEndSession(onEnd func()) (func(), error) {
	_, stop, err := startEndSessionWindow(onEnd)
	if err != nil {
		return func() {}, err
	}
	return stop, nil
}