Skip to content

Commit 25aad94

Browse files
authored
Merge pull request #4 from arduino/devicenotification
Added `devicenotification` implementation
2 parents 6c2b583 + 33bacb3 commit 25aad94

File tree

1 file changed

+214
-0
lines changed

1 file changed

+214
-0
lines changed
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
/*
2+
* This file is part of go-win32-utils.
3+
*
4+
* Copyright 2018-2023 ARDUINO SA (http://www.arduino.cc/)
5+
*
6+
* go-win32-utils is free software; you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation; either version 2 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with this program; if not, write to the Free Software
18+
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19+
*
20+
* As a special exception, you may use this file as part of a free software
21+
* library without restriction. Specifically, if other files instantiate
22+
* templates or use macros or inline functions from this file, or you compile
23+
* this file and link it with other files to produce an executable, this
24+
* file does not by itself cause the resulting executable to be covered by
25+
* the GNU General Public License. This exception does not however
26+
* invalidate any other reasons why the executable file might be covered by
27+
* the GNU General Public License.
28+
*/
29+
30+
package devicenotification
31+
32+
import (
33+
"context"
34+
"fmt"
35+
"runtime"
36+
"sync"
37+
"sync/atomic"
38+
"syscall"
39+
"unsafe"
40+
41+
win32 "github.com/arduino/go-win32-utils"
42+
"golang.org/x/sys/windows"
43+
)
44+
45+
var osThreadId atomic.Uint32
46+
47+
// Start the device add/remove notification process, at every event a call to eventCB will be performed.
48+
// This function will block until interrupted by the given context. Errors will be passed to errorCB.
49+
// Returns error if sync process can't be started.
50+
func Start(ctx context.Context, eventCB func(), errorCB func(msg string)) error {
51+
runtime.LockOSThread()
52+
defer runtime.UnlockOSThread()
53+
osThreadId.Store(windows.GetCurrentThreadId())
54+
55+
eventsChan := make(chan bool, 1)
56+
var eventsChanLock sync.Mutex
57+
windowCallback := func(hwnd syscall.Handle, msg uint32, wParam uintptr, lParam uintptr) uintptr {
58+
// This mutex is required because the callback may be called
59+
// asynchronously by the OS threads, even after the channel has
60+
// been closed and the callback unregistered...
61+
eventsChanLock.Lock()
62+
if eventsChan != nil {
63+
select {
64+
case eventsChan <- true:
65+
default:
66+
}
67+
}
68+
eventsChanLock.Unlock()
69+
return win32.DefWindowProc(hwnd, msg, wParam, lParam)
70+
}
71+
defer func() {
72+
eventsChanLock.Lock()
73+
close(eventsChan)
74+
eventsChan = nil
75+
eventsChanLock.Unlock()
76+
}()
77+
78+
go func() {
79+
for {
80+
if _, ok := <-eventsChan; !ok {
81+
return
82+
}
83+
eventCB()
84+
}
85+
}()
86+
87+
// We must create the window used to receive notifications in the same
88+
// thread that destroys it otherwise it would fail
89+
windowHandle, className, err := createWindow(windowCallback)
90+
if err != nil {
91+
return err
92+
}
93+
defer func() {
94+
if err := destroyWindow(windowHandle, className); err != nil {
95+
errorCB(err.Error())
96+
}
97+
}()
98+
99+
notificationsDevHandle, err := registerNotifications(windowHandle)
100+
if err != nil {
101+
return err
102+
}
103+
defer func() {
104+
if err := unregisterNotifications(notificationsDevHandle); err != nil {
105+
errorCB(err.Error())
106+
}
107+
}()
108+
109+
go func() {
110+
<-ctx.Done()
111+
_ = win32.PostMessage(windowHandle, win32.WM_Quit, 0, 0)
112+
}()
113+
114+
for {
115+
// Verify running thread prerequisites
116+
if currThreadId := windows.GetCurrentThreadId(); currThreadId != osThreadId.Load() {
117+
panic(fmt.Sprintf("this function must run on the main OS Thread: currThread=%d, osThread=%d", currThreadId, osThreadId))
118+
}
119+
120+
var m win32.TagMSG
121+
if res := win32.GetMessage(&m, windowHandle, win32.WM_Quit, win32.WM_Quit); res == 0 { // 0 means we got a WM_QUIT
122+
break
123+
} else if res == -1 { // -1 means that an error occurred
124+
err := windows.GetLastError()
125+
errorCB("error consuming messages: " + err.Error())
126+
break
127+
} else {
128+
// we got a message != WM_Quit, it should not happen but, just in case...
129+
win32.TranslateMessage(&m)
130+
win32.DispatchMessage(&m)
131+
}
132+
}
133+
134+
return nil
135+
}
136+
137+
func createWindow(windowCallback win32.WindowProcCallback) (syscall.Handle, *byte, error) {
138+
// Verify running thread prerequisites
139+
if currThreadId := windows.GetCurrentThreadId(); currThreadId != osThreadId.Load() {
140+
panic(fmt.Sprintf("this function must run on the main OS Thread: currThread=%d, osThread=%d", currThreadId, osThreadId.Load()))
141+
}
142+
143+
moduleHandle, err := win32.GetModuleHandle(nil)
144+
if err != nil {
145+
return syscall.InvalidHandle, nil, err
146+
}
147+
148+
className, err := syscall.BytePtrFromString("device-notification")
149+
if err != nil {
150+
return syscall.InvalidHandle, nil, err
151+
}
152+
windowClass := &win32.WndClass{
153+
Instance: moduleHandle,
154+
ClassName: className,
155+
WndProc: syscall.NewCallback(windowCallback),
156+
}
157+
if _, err := win32.RegisterClass(windowClass); err != nil {
158+
return syscall.InvalidHandle, nil, fmt.Errorf("registering new window: %s", err)
159+
}
160+
161+
windowHandle, err := win32.CreateWindowEx(win32.WsExTopmost, className, className, 0, 0, 0, 0, 0, 0, 0, 0, 0)
162+
if err != nil {
163+
return syscall.InvalidHandle, nil, fmt.Errorf("creating window: %s", err)
164+
}
165+
return windowHandle, className, nil
166+
}
167+
168+
func destroyWindow(windowHandle syscall.Handle, className *byte) error {
169+
// Verify running thread prerequisites
170+
if currThreadId := windows.GetCurrentThreadId(); currThreadId != osThreadId.Load() {
171+
panic(fmt.Sprintf("this function must run on the main OS Thread: currThread=%d, osThread=%d", currThreadId, osThreadId.Load()))
172+
}
173+
174+
if err := win32.DestroyWindowEx(windowHandle); err != nil {
175+
return fmt.Errorf("error destroying window: %s", err)
176+
}
177+
if err := win32.UnregisterClass(className); err != nil {
178+
return fmt.Errorf("error unregistering window class: %s", err)
179+
}
180+
return nil
181+
}
182+
183+
func registerNotifications(windowHandle syscall.Handle) (syscall.Handle, error) {
184+
// Verify running thread prerequisites
185+
if currThreadId := windows.GetCurrentThreadId(); currThreadId != osThreadId.Load() {
186+
panic(fmt.Sprintf("this function must run on the main OS Thread: currThread=%d, osThread=%d", currThreadId, osThreadId.Load()))
187+
}
188+
189+
notificationFilter := win32.DevBroadcastDeviceInterface{
190+
DwDeviceType: win32.DbtDevtypeDeviceInterface,
191+
ClassGUID: win32.UsbEventGUID,
192+
}
193+
notificationFilter.DwSize = uint32(unsafe.Sizeof(notificationFilter))
194+
195+
var flags uint32 = win32.DeviceNotifyWindowHandle | win32.DeviceNotifyAllInterfaceClasses
196+
notificationsDevHandle, err := win32.RegisterDeviceNotification(windowHandle, &notificationFilter, flags)
197+
if err != nil {
198+
return syscall.InvalidHandle, err
199+
}
200+
201+
return notificationsDevHandle, nil
202+
}
203+
204+
func unregisterNotifications(notificationsDevHandle syscall.Handle) error {
205+
// Verify running thread prerequisites
206+
if currThreadId := windows.GetCurrentThreadId(); currThreadId != osThreadId.Load() {
207+
panic(fmt.Sprintf("this function must run on the main OS Thread: currThread=%d, osThread=%d", currThreadId, osThreadId.Load()))
208+
}
209+
210+
if err := win32.UnregisterDeviceNotification(notificationsDevHandle); err != nil {
211+
return fmt.Errorf("error unregistering device notifications: %s", err)
212+
}
213+
return nil
214+
}

0 commit comments

Comments
 (0)