WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

Commit ddcd182

Browse files
authored
[client] Sleep detection on macOS (#4859)
A macOS-specific sleep detection mechanism using IOKit and CoreFoundation via cgo is introduced, with a fallback implementation for unsupported platforms. A public Service wrapper provides an event-driven API translating system sleep/wake events into gRPC calls. The UI client integrates sleep detection to manage connectivity state based on system sleep status.
1 parent aca0398 commit ddcd182

File tree

5 files changed

+329
-5
lines changed

5 files changed

+329
-5
lines changed
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
//go:build darwin && !ios
2+
3+
package sleep
4+
5+
/*
6+
#cgo LDFLAGS: -framework IOKit -framework CoreFoundation
7+
#include <IOKit/pwr_mgt/IOPMLib.h>
8+
#include <IOKit/IOMessage.h>
9+
#include <CoreFoundation/CoreFoundation.h>
10+
11+
extern void sleepCallbackBridge();
12+
extern void poweredOnCallbackBridge();
13+
extern void suspendedCallbackBridge();
14+
extern void resumedCallbackBridge();
15+
16+
17+
// C global variables for IOKit state
18+
static IONotificationPortRef g_notifyPortRef = NULL;
19+
static io_object_t g_notifierObject = 0;
20+
static io_object_t g_generalInterestNotifier = 0;
21+
static io_connect_t g_rootPort = 0;
22+
static CFRunLoopRef g_runLoop = NULL;
23+
24+
static void sleepCallback(void* refCon, io_service_t service, natural_t messageType, void* messageArgument) {
25+
switch (messageType) {
26+
case kIOMessageSystemWillSleep:
27+
sleepCallbackBridge();
28+
IOAllowPowerChange(g_rootPort, (long)messageArgument);
29+
break;
30+
case kIOMessageSystemHasPoweredOn:
31+
poweredOnCallbackBridge();
32+
break;
33+
case kIOMessageServiceIsSuspended:
34+
suspendedCallbackBridge();
35+
break;
36+
case kIOMessageServiceIsResumed:
37+
resumedCallbackBridge();
38+
break;
39+
default:
40+
break;
41+
}
42+
}
43+
44+
static void registerNotifications() {
45+
g_rootPort = IORegisterForSystemPower(
46+
NULL,
47+
&g_notifyPortRef,
48+
(IOServiceInterestCallback)sleepCallback,
49+
&g_notifierObject
50+
);
51+
52+
if (g_rootPort == 0) {
53+
return;
54+
}
55+
56+
CFRunLoopAddSource(CFRunLoopGetCurrent(),
57+
IONotificationPortGetRunLoopSource(g_notifyPortRef),
58+
kCFRunLoopCommonModes);
59+
60+
g_runLoop = CFRunLoopGetCurrent();
61+
CFRunLoopRun();
62+
}
63+
64+
static void unregisterNotifications() {
65+
CFRunLoopRemoveSource(g_runLoop,
66+
IONotificationPortGetRunLoopSource(g_notifyPortRef),
67+
kCFRunLoopCommonModes);
68+
69+
IODeregisterForSystemPower(&g_notifierObject);
70+
IOServiceClose(g_rootPort);
71+
IONotificationPortDestroy(g_notifyPortRef);
72+
CFRunLoopStop(g_runLoop);
73+
74+
g_notifyPortRef = NULL;
75+
g_notifierObject = 0;
76+
g_rootPort = 0;
77+
g_runLoop = NULL;
78+
}
79+
80+
*/
81+
import "C"
82+
83+
import (
84+
"context"
85+
"fmt"
86+
"runtime"
87+
"sync"
88+
"time"
89+
90+
log "github.com/sirupsen/logrus"
91+
)
92+
93+
var (
94+
serviceRegistry = make(map[*Detector]struct{})
95+
serviceRegistryMu sync.Mutex
96+
)
97+
98+
//export sleepCallbackBridge
99+
func sleepCallbackBridge() {
100+
log.Info("sleepCallbackBridge event triggered")
101+
102+
serviceRegistryMu.Lock()
103+
defer serviceRegistryMu.Unlock()
104+
105+
for svc := range serviceRegistry {
106+
svc.triggerCallback(EventTypeSleep)
107+
}
108+
}
109+
110+
//export resumedCallbackBridge
111+
func resumedCallbackBridge() {
112+
log.Info("resumedCallbackBridge event triggered")
113+
}
114+
115+
//export suspendedCallbackBridge
116+
func suspendedCallbackBridge() {
117+
log.Info("suspendedCallbackBridge event triggered")
118+
}
119+
120+
//export poweredOnCallbackBridge
121+
func poweredOnCallbackBridge() {
122+
log.Info("poweredOnCallbackBridge event triggered")
123+
serviceRegistryMu.Lock()
124+
defer serviceRegistryMu.Unlock()
125+
126+
for svc := range serviceRegistry {
127+
svc.triggerCallback(EventTypeWakeUp)
128+
}
129+
}
130+
131+
type Detector struct {
132+
callback func(event EventType)
133+
ctx context.Context
134+
cancel context.CancelFunc
135+
}
136+
137+
func NewDetector() (*Detector, error) {
138+
return &Detector{}, nil
139+
}
140+
141+
func (d *Detector) Register(callback func(event EventType)) error {
142+
serviceRegistryMu.Lock()
143+
defer serviceRegistryMu.Unlock()
144+
145+
if _, exists := serviceRegistry[d]; exists {
146+
return fmt.Errorf("detector service already registered")
147+
}
148+
149+
d.callback = callback
150+
151+
d.ctx, d.cancel = context.WithCancel(context.Background())
152+
153+
if len(serviceRegistry) > 0 {
154+
serviceRegistry[d] = struct{}{}
155+
return nil
156+
}
157+
158+
serviceRegistry[d] = struct{}{}
159+
160+
// CFRunLoop must run on a single fixed OS thread
161+
go func() {
162+
runtime.LockOSThread()
163+
defer runtime.UnlockOSThread()
164+
165+
C.registerNotifications()
166+
}()
167+
168+
log.Info("sleep detection service started on macOS")
169+
return nil
170+
}
171+
172+
// Deregister removes the detector. When the last detector is removed, IOKit registration is torn down
173+
// and the runloop is stopped and cleaned up.
174+
func (d *Detector) Deregister() error {
175+
serviceRegistryMu.Lock()
176+
defer serviceRegistryMu.Unlock()
177+
_, exists := serviceRegistry[d]
178+
if !exists {
179+
return nil
180+
}
181+
182+
// cancel and remove this detector
183+
d.cancel()
184+
delete(serviceRegistry, d)
185+
186+
// If other Detectors still exist, leave IOKit running
187+
if len(serviceRegistry) > 0 {
188+
return nil
189+
}
190+
191+
log.Info("sleep detection service stopping (deregister)")
192+
193+
// Deregister IOKit notifications, stop runloop, and free resources
194+
C.unregisterNotifications()
195+
196+
return nil
197+
}
198+
199+
func (d *Detector) triggerCallback(event EventType) {
200+
doneChan := make(chan struct{})
201+
202+
timeout := time.NewTimer(500 * time.Millisecond)
203+
defer timeout.Stop()
204+
205+
cb := d.callback
206+
go func(callback func(event EventType)) {
207+
log.Info("sleep detection event fired")
208+
callback(event)
209+
close(doneChan)
210+
}(cb)
211+
212+
select {
213+
case <-doneChan:
214+
case <-d.ctx.Done():
215+
case <-timeout.C:
216+
log.Warnf("sleep callback timed out")
217+
}
218+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//go:build !darwin || ios
2+
3+
package sleep
4+
5+
import "fmt"
6+
7+
func NewDetector() (detector, error) {
8+
return nil, fmt.Errorf("sleep not supported on this platform")
9+
}

client/internal/sleep/service.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package sleep
2+
3+
var (
4+
EventTypeSleep EventType = 0
5+
EventTypeWakeUp EventType = 1
6+
)
7+
8+
type EventType int
9+
10+
type detector interface {
11+
Register(callback func(eventType EventType)) error
12+
Deregister() error
13+
}
14+
15+
type Service struct {
16+
detector detector
17+
}
18+
19+
func New() (*Service, error) {
20+
d, err := NewDetector()
21+
if err != nil {
22+
return nil, err
23+
}
24+
25+
return &Service{
26+
detector: d,
27+
}, nil
28+
}
29+
30+
func (s *Service) Register(callback func(eventType EventType)) error {
31+
return s.detector.Register(callback)
32+
}
33+
34+
func (s *Service) Deregister() error {
35+
return s.detector.Deregister()
36+
}

client/proto/daemon.pb.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/ui/client_ui.go

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
"github.com/netbirdio/netbird/client/iface"
3939
"github.com/netbirdio/netbird/client/internal"
4040
"github.com/netbirdio/netbird/client/internal/profilemanager"
41+
"github.com/netbirdio/netbird/client/internal/sleep"
4142
"github.com/netbirdio/netbird/client/proto"
4243
"github.com/netbirdio/netbird/client/ui/desktop"
4344
"github.com/netbirdio/netbird/client/ui/event"
@@ -209,10 +210,11 @@ var iconConnectedDot []byte
209210
var iconDisconnectedDot []byte
210211

211212
type serviceClient struct {
212-
ctx context.Context
213-
cancel context.CancelFunc
214-
addr string
215-
conn proto.DaemonServiceClient
213+
ctx context.Context
214+
cancel context.CancelFunc
215+
addr string
216+
conn proto.DaemonServiceClient
217+
connLock sync.Mutex
216218

217219
eventHandler *eventHandler
218220

@@ -1098,6 +1100,9 @@ func (s *serviceClient) onTrayReady() {
10981100

10991101
go s.eventManager.Start(s.ctx)
11001102
go s.eventHandler.listen(s.ctx)
1103+
1104+
// Start sleep detection listener
1105+
go s.startSleepListener()
11011106
}
11021107

11031108
func (s *serviceClient) attachOutput(cmd *exec.Cmd) *os.File {
@@ -1134,6 +1139,8 @@ func (s *serviceClient) onTrayExit() {
11341139

11351140
// getSrvClient connection to the service.
11361141
func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonServiceClient, error) {
1142+
s.connLock.Lock()
1143+
defer s.connLock.Unlock()
11371144
if s.conn != nil {
11381145
return s.conn, nil
11391146
}
@@ -1156,6 +1163,60 @@ func (s *serviceClient) getSrvClient(timeout time.Duration) (proto.DaemonService
11561163
return s.conn, nil
11571164
}
11581165

1166+
// startSleepListener initializes the sleep detection service and listens for sleep events
1167+
func (s *serviceClient) startSleepListener() {
1168+
sleepService, err := sleep.New()
1169+
if err != nil {
1170+
log.Warnf("%v", err)
1171+
return
1172+
}
1173+
1174+
if err := sleepService.Register(s.handleSleepEvents); err != nil {
1175+
log.Errorf("failed to start sleep detection: %v", err)
1176+
return
1177+
}
1178+
1179+
log.Info("sleep detection service initialized")
1180+
1181+
// Cleanup on context cancellation
1182+
go func() {
1183+
<-s.ctx.Done()
1184+
log.Info("stopping sleep event listener")
1185+
if err := sleepService.Deregister(); err != nil {
1186+
log.Errorf("failed to deregister sleep detection: %v", err)
1187+
}
1188+
}()
1189+
}
1190+
1191+
// handleSleepEvents sends a sleep notification to the daemon via gRPC
1192+
func (s *serviceClient) handleSleepEvents(event sleep.EventType) {
1193+
conn, err := s.getSrvClient(0)
1194+
if err != nil {
1195+
log.Errorf("failed to get daemon client for sleep notification: %v", err)
1196+
return
1197+
}
1198+
1199+
switch event {
1200+
case sleep.EventTypeWakeUp:
1201+
log.Infof("handle wakeup event: %v", event)
1202+
_, err = conn.Up(s.ctx, &proto.UpRequest{})
1203+
if err != nil {
1204+
log.Errorf("up service: %v", err)
1205+
return
1206+
}
1207+
return
1208+
case sleep.EventTypeSleep:
1209+
log.Infof("handle sleep event: %v", event)
1210+
_, err = conn.Down(s.ctx, &proto.DownRequest{})
1211+
if err != nil {
1212+
log.Errorf("down service: %v", err)
1213+
return
1214+
}
1215+
}
1216+
1217+
log.Info("successfully notified daemon about sleep/wakeup event")
1218+
}
1219+
11591220
// setSettingsEnabled enables or disables the settings menu based on the provided state
11601221
func (s *serviceClient) setSettingsEnabled(enabled bool) {
11611222
if s.mSettings != nil {

0 commit comments

Comments
 (0)