//go:build windows package windowstray // A deliberately small Win32 notification-area host. The tray has no // durable state and no command execution path; it only renders a tooltip, // sends enum actions over the authenticated service pipe, and opens paths // returned by the service with the exact Windows Explorer executable. import ( "context" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "strings" "sync" "syscall" "unicode/utf8" "unsafe" winapi "golang.org/x/sys/windows" ) const ( wmDestroy = 0x0002 wmClose = 0x0010 wmCommand = 0x0111 wmRButtonUp = 0x0205 wmLButtonDblClick = 0x0203 wmApp = 0x8000 trayMessage = wmApp + 1 trayIconID = 1 trayNIMAdd = 0 trayNIMModify = 1 trayNIMDelete = 2 trayNIFMessage = 0x00000001 trayNIFIcon = 0x00000002 trayNIFTip = 0x00000004 trayTPMRightBtn = 0x0002 trayMFString = 0x00000000 trayMFSeparator = 0x00000800 traySWHide = 0 trayIDIApplication = 32512 trayIDCArrow = 32512 ) var ( ErrTrayAlreadyRunning = errors.New("RVBox tray is already running in this session") user32Tray = syscall.NewLazyDLL("user32.dll") shell32Tray = syscall.NewLazyDLL("shell32.dll") procRegisterClassEx = user32Tray.NewProc("RegisterClassExW") procCreateWindowEx = user32Tray.NewProc("CreateWindowExW") procDefWindowProc = user32Tray.NewProc("DefWindowProcW") procGetMessage = user32Tray.NewProc("GetMessageW") procTranslate = user32Tray.NewProc("TranslateMessage") procDispatch = user32Tray.NewProc("DispatchMessageW") procPostMessage = user32Tray.NewProc("PostMessageW") procDestroyWindow = user32Tray.NewProc("DestroyWindow") procShowWindow = user32Tray.NewProc("ShowWindow") procPostQuitMessage = user32Tray.NewProc("PostQuitMessage") procLoadIcon = user32Tray.NewProc("LoadIconW") procLoadCursor = user32Tray.NewProc("LoadCursorW") procCreatePopup = user32Tray.NewProc("CreatePopupMenu") procAppendMenu = user32Tray.NewProc("AppendMenuW") procTrackPopup = user32Tray.NewProc("TrackPopupMenu") procDestroyMenu = user32Tray.NewProc("DestroyMenu") procGetCursorPos = user32Tray.NewProc("GetCursorPos") procSetForeground = user32Tray.NewProc("SetForegroundWindow") procGetModuleHandle = user32Tray.NewProc("GetModuleHandleW") procRegisterMessage = user32Tray.NewProc("RegisterWindowMessageW") procShellNotify = shell32Tray.NewProc("Shell_NotifyIconW") trayCallback = syscall.NewCallback(trayWindowProc) trayWindows sync.Map // hwnd -> *trayWindow taskbarCreatedMessage uint32 ) type trayPoint struct{ X, Y int32 } type trayMessageRecord struct { HWnd uintptr Message uint32 WParam uintptr LParam uintptr Time uint32 Point trayPoint } type trayClass struct { CbSize uint32 Style uint32 WndProc uintptr CbClsExtra int32 CbWndExtra int32 HInstance uintptr HIcon uintptr HCursor uintptr HbrBackground uintptr MenuName *uint16 ClassName *uint16 HIconSm uintptr } type trayIconData struct { CbSize uint32 HWnd uintptr UID uint32 UFlags uint32 UCallbackMessage uint32 HIcon uintptr Tip [128]uint16 State uint32 StateMask uint32 InfoFlags uint32 InfoTitle [64]uint16 InfoData [256]uint16 Guid [16]byte BalloonIcon uintptr } type trayWindow struct { hwnd uintptr icon trayIconData class *uint16 tooltip string output io.Writer closeOnce sync.Once } // Run starts one notification icon for the current logged-in session. It // returns when the icon is closed, the service becomes unavailable, or ctx is // cancelled; none of those outcomes affect the machine-wide service. func Run(ctx context.Context, output io.Writer) error { mutex, err := acquireTrayMutex() if err != nil { return err } defer winapi.CloseHandle(mutex) response, err := Request(ctx, Frame{Action: ActionStatus}) tooltip := "RVBox service unavailable" if err == nil { tooltip = string(response.Payload) } if tooltip == "" { tooltip = "RVBox service" } ready := make(chan *trayWindow, 1) done := make(chan error, 1) go runTrayMessageLoop(output, tooltip, ready, done) var window *trayWindow select { case window = <-ready: case err := <-done: return err case <-ctx.Done(): return nil } select { case <-ctx.Done(): if window != nil { _, _, _ = procPostMessage.Call(window.hwnd, wmClose, 0, 0) } return <-done case err := <-done: return err } } func acquireTrayMutex() (winapi.Handle, error) { var sessionID uint32 if err := winapi.ProcessIdToSessionId(winapi.GetCurrentProcessId(), &sessionID); err != nil { return 0, err } name, err := winapi.UTF16PtrFromString(fmt.Sprintf("Local\\RVBoxTrayV1-%d", sessionID)) if err != nil { return 0, err } mutex, err := winapi.CreateMutex(nil, true, name) if errors.Is(err, winapi.ERROR_ALREADY_EXISTS) { if mutex != 0 { _ = winapi.CloseHandle(mutex) } return 0, ErrTrayAlreadyRunning } if err != nil { return 0, err } return mutex, nil } func runTrayMessageLoop(output io.Writer, tooltip string, ready chan<- *trayWindow, done chan<- error) { className, err := winapi.UTF16PtrFromString("RVBoxTrayWindowV1") if err != nil { done <- err return } instance, _, _ := procGetModuleHandle.Call(0) icon, _, _ := procLoadIcon.Call(0, uintptr(trayIDIApplication)) cursor, _, _ := procLoadCursor.Call(0, uintptr(trayIDCArrow)) class := trayClass{CbSize: uint32(unsafe.Sizeof(trayClass{})), WndProc: trayCallback, HInstance: instance, HIcon: icon, HCursor: cursor, ClassName: className, HIconSm: icon} if result, _, callErr := procRegisterClassEx.Call(uintptr(unsafe.Pointer(&class))); result == 0 && !errors.Is(callErr, winapi.ERROR_CLASS_ALREADY_EXISTS) { done <- callErr return } title, _ := winapi.UTF16PtrFromString("RVBox") hwnd, _, callErr := procCreateWindowEx.Call(0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(title)), 0, 0, 0, 0, 0, 0, 0, instance, 0) if hwnd == 0 { done <- callErr return } window := &trayWindow{hwnd: hwnd, class: className, tooltip: tooltip, output: output} window.icon = trayIconData{CbSize: uint32(unsafe.Sizeof(trayIconData{})), HWnd: hwnd, UID: trayIconID, UFlags: trayNIFMessage | trayNIFIcon | trayNIFTip, UCallbackMessage: trayMessage, HIcon: icon} copy(window.icon.Tip[:], winapi.StringToUTF16(tooltip)) trayWindows.Store(hwnd, window) messageName, _ := winapi.UTF16PtrFromString("TaskbarCreated") registeredMessage, _, _ := procRegisterMessage.Call(uintptr(unsafe.Pointer(messageName))) taskbarCreatedMessage = uint32(registeredMessage) if err := window.addIcon(); err != nil { trayWindows.Delete(hwnd) _, _, _ = procDestroyWindow.Call(hwnd) done <- err return } _, _, _ = procShowWindow.Call(hwnd, traySWHide) ready <- window for { var message trayMessageRecord result, _, getErr := procGetMessage.Call(uintptr(unsafe.Pointer(&message)), 0, 0, 0) if int32(result) == -1 { window.removeIcon() done <- getErr return } if result == 0 { window.removeIcon() done <- nil return } _, _, _ = procTranslate.Call(uintptr(unsafe.Pointer(&message))) _, _, _ = procDispatch.Call(uintptr(unsafe.Pointer(&message))) } } func trayWindowProc(hwnd uintptr, message uint32, wParam, lParam uintptr) uintptr { value, _ := trayWindows.Load(hwnd) window, _ := value.(*trayWindow) switch message { case wmClose: _, _, _ = procDestroyWindow.Call(hwnd) return 0 case wmDestroy: if window != nil { window.removeIcon() } trayWindows.Delete(hwnd) procPostQuitMessage.Call(0) return 0 case wmCommand: if window != nil { window.action(Action(uint16(wParam))) } return 0 case trayMessage: if window != nil && (uint32(lParam) == wmRButtonUp || uint32(lParam) == wmLButtonDblClick) { window.showMenu() } return 0 } if window != nil && taskbarCreatedMessage != 0 && message == taskbarCreatedMessage { // Explorer may restart without destroying the tray window. Re-add the // icon with the current bounded tooltip so the frontend recovers without // restarting the service or tray process. _ = window.addIcon() return 0 } result, _, _ := procDefWindowProc.Call(hwnd, uintptr(message), wParam, lParam) return result } func (window *trayWindow) addIcon() error { window.icon.UFlags = trayNIFMessage | trayNIFIcon | trayNIFTip if result, _, err := procShellNotify.Call(uintptr(trayNIMAdd), uintptr(unsafe.Pointer(&window.icon))); result == 0 { if err == syscall.Errno(0) { err = syscall.GetLastError() } return err } return nil } func (window *trayWindow) removeIcon() { window.closeOnce.Do(func() { window.icon.UFlags = 0 _, _, _ = procShellNotify.Call(uintptr(trayNIMDelete), uintptr(unsafe.Pointer(&window.icon))) }) } func (window *trayWindow) showMenu() { menu, _, _ := procCreatePopup.Call() if menu == 0 { return } defer procDestroyMenu.Call(menu) add := func(action Action, label string) { text, _ := winapi.UTF16PtrFromString(label) procAppendMenu.Call(menu, trayMFString, uintptr(action), uintptr(unsafe.Pointer(text))) } add(ActionStatus, "Status") add(ActionOpenConfig, "Open config") add(ActionOpenLog, "Open log") procAppendMenu.Call(menu, trayMFSeparator, 0, 0) add(ActionStartService, "Start service") add(ActionStopService, "Stop service") add(ActionRestartService, "Restart service") add(ActionSetAutomatic, "Start automatically") add(ActionSetManual, "Start manually") procAppendMenu.Call(menu, trayMFSeparator, 0, 0) add(ActionExitTray, "Exit") var point trayPoint procGetCursorPos.Call(uintptr(unsafe.Pointer(&point))) procSetForeground.Call(window.hwnd) procTrackPopup.Call(menu, trayTPMRightBtn, uintptr(point.X), uintptr(point.Y), 0, window.hwnd, 0) } func (window *trayWindow) action(action Action) { if action == ActionExitTray { _, _, _ = procDestroyWindow.Call(window.hwnd) return } response, err := Request(context.Background(), Frame{Action: action}) if err != nil { window.setTooltip("RVBox service unavailable") return } if isServiceMutation(action) && strings.HasPrefix(string(response.Payload), "error:") { if err := runElevatedServiceAction(action); err == nil { window.setTooltip("RVBox elevation requested") return } } if action == ActionOpenConfig || action == ActionOpenLog { if err := openExactPath(string(response.Payload)); err != nil { window.setTooltip("RVBox open failed") } return } if len(response.Payload) > 0 { window.setTooltip(string(response.Payload)) } } func isServiceMutation(action Action) bool { return action == ActionStartService || action == ActionStopService || action == ActionRestartService || action == ActionSetAutomatic || action == ActionSetManual } // runElevatedServiceAction is the tray's narrow UAC fallback. The service // remains the authorization boundary; this helper only replays the enum as a // canonical rvbox mode under an administrator token when the tray's filtered // token cannot mutate SCM state directly. func runElevatedServiceAction(action Action) error { mode := "" switch action { case ActionStartService: mode = "--start-service" case ActionStopService: mode = "--stop-service" case ActionRestartService: mode = "--restart-service" case ActionSetAutomatic: mode = "--configure-service --startup automatic" case ActionSetManual: mode = "--configure-service --startup manual" default: return errors.New("unsupported elevated tray action") } executable, err := os.Executable() if err != nil { return err } configPath := "" for index := 0; index+1 < len(os.Args); index++ { if os.Args[index] == "--config" { configPath = os.Args[index+1] break } } if configPath == "" { return errors.New("tray config path is unavailable") } args, err := winapi.UTF16PtrFromString(mode + " --config " + syscall.EscapeArg(configPath)) if err != nil { return err } file, err := winapi.UTF16PtrFromString(executable) if err != nil { return err } if err := winapi.ShellExecute(0, mustUTF16("runas"), file, args, nil, winapi.SW_SHOWNORMAL); err != nil { return err } return nil } func mustUTF16(value string) *uint16 { encoded, _ := winapi.UTF16PtrFromString(value) return encoded } func (window *trayWindow) setTooltip(value string) { value = strings.TrimSpace(value) if value == "" { value = "RVBox service" } window.tooltip = value window.icon.UFlags = trayNIFTip for index := range window.icon.Tip { window.icon.Tip[index] = 0 } copy(window.icon.Tip[:], winapi.StringToUTF16(value)) _, _, _ = procShellNotify.Call(uintptr(trayNIMModify), uintptr(unsafe.Pointer(&window.icon))) } func openExactPath(path string) error { if path == "" || strings.TrimSpace(path) != path || !utf8.ValidString(path) || !filepath.IsAbs(path) { return errors.New("invalid path returned by service") } info, err := os.Lstat(path) if err != nil || !info.Mode().IsRegular() { return errors.New("service path is not a regular file") } windowsDir, err := winapi.GetWindowsDirectory() if err != nil { return err } explorer := filepath.Join(windowsDir, "explorer.exe") if info, err := os.Lstat(explorer); err != nil || !info.Mode().IsRegular() { return errors.New("Windows Explorer is unavailable") } return exec.Command(explorer, path).Start() }