mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-30 22:26:42 +00:00
360 lines
9.9 KiB
Go
360 lines
9.9 KiB
Go
//go:build darwin && !ios
|
|
|
|
package sleep
|
|
|
|
import (
|
|
"fmt"
|
|
"runtime"
|
|
"sync"
|
|
"time"
|
|
"unsafe"
|
|
|
|
"github.com/ebitengine/purego"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
// IOKit message types from IOKit/IOMessage.h.
|
|
const (
|
|
kIOMessageCanSystemSleep uintptr = 0xe0000270
|
|
kIOMessageSystemWillSleep uintptr = 0xe0000280
|
|
kIOMessageSystemHasPoweredOn uintptr = 0xe0000300
|
|
)
|
|
|
|
var (
|
|
ioKit iokitFuncs
|
|
cf cfFuncs
|
|
cfCommonModes uintptr
|
|
|
|
libInitOnce sync.Once
|
|
libInitErr error
|
|
|
|
// callbackThunk is the single C-callable trampoline registered with IOKit.
|
|
callbackThunk uintptr
|
|
|
|
serviceRegistry = make(map[*Detector]struct{})
|
|
serviceRegistryMu sync.Mutex
|
|
session *runLoopSession
|
|
|
|
// lifecycleMu serializes Register/Deregister so a new registration can't
|
|
// start a second runloop while a previous teardown is still pending.
|
|
lifecycleMu sync.Mutex
|
|
)
|
|
|
|
// iokitFuncs holds IOKit symbols resolved once at init.
|
|
type iokitFuncs struct {
|
|
IORegisterForSystemPower func(refcon uintptr, portRef *uintptr, callback uintptr, notifier *uintptr) uintptr
|
|
IODeregisterForSystemPower func(notifier *uintptr) int32
|
|
IOAllowPowerChange func(kernelPort uintptr, notificationID uintptr) int32
|
|
IOServiceClose func(connect uintptr) int32
|
|
IONotificationPortGetRunLoopSource func(port uintptr) uintptr
|
|
IONotificationPortDestroy func(port uintptr)
|
|
}
|
|
|
|
// cfFuncs holds CoreFoundation symbols resolved once at init.
|
|
type cfFuncs struct {
|
|
CFRunLoopGetCurrent func() uintptr
|
|
CFRunLoopRun func()
|
|
CFRunLoopStop func(rl uintptr)
|
|
CFRunLoopAddSource func(rl, source, mode uintptr)
|
|
CFRunLoopRemoveSource func(rl, source, mode uintptr)
|
|
}
|
|
|
|
// runLoopSession bundles the handles owned by one CFRunLoop lifetime. A nil
|
|
// session means no runloop is active and the next Register must start one.
|
|
type runLoopSession struct {
|
|
rl uintptr
|
|
port uintptr
|
|
notifier uintptr
|
|
rp uintptr
|
|
}
|
|
|
|
// detectorSnapshot pins a detector's callback and done channel so dispatch
|
|
// runs with values valid at snapshot time, even if a concurrent
|
|
// Deregister/Register rewrites the detector's fields.
|
|
type detectorSnapshot struct {
|
|
detector *Detector
|
|
callback func(event EventType)
|
|
done <-chan struct{}
|
|
}
|
|
|
|
// Detector delivers sleep and wake events to a registered callback.
|
|
type Detector struct {
|
|
callback func(event EventType)
|
|
done chan struct{}
|
|
}
|
|
|
|
// Register installs callback for power events. The first registration starts
|
|
// the CFRunLoop on a dedicated OS-locked thread and blocks until IOKit
|
|
// registration succeeds or fails; subsequent registrations just add to the
|
|
// dispatch set.
|
|
func (d *Detector) Register(callback func(event EventType)) error {
|
|
lifecycleMu.Lock()
|
|
defer lifecycleMu.Unlock()
|
|
|
|
serviceRegistryMu.Lock()
|
|
if _, exists := serviceRegistry[d]; exists {
|
|
serviceRegistryMu.Unlock()
|
|
return fmt.Errorf("detector service already registered")
|
|
}
|
|
d.callback = callback
|
|
d.done = make(chan struct{})
|
|
serviceRegistry[d] = struct{}{}
|
|
needSetup := session == nil
|
|
serviceRegistryMu.Unlock()
|
|
|
|
if !needSetup {
|
|
return nil
|
|
}
|
|
|
|
errCh := make(chan error, 1)
|
|
go runRunLoop(errCh)
|
|
if err := <-errCh; err != nil {
|
|
serviceRegistryMu.Lock()
|
|
delete(serviceRegistry, d)
|
|
close(d.done)
|
|
d.done = nil
|
|
serviceRegistryMu.Unlock()
|
|
return err
|
|
}
|
|
|
|
log.Info("sleep detection service started on macOS")
|
|
return nil
|
|
}
|
|
|
|
// Deregister removes the detector. When the last detector leaves, IOKit
|
|
// notifications are torn down and the runloop is stopped.
|
|
func (d *Detector) Deregister() error {
|
|
lifecycleMu.Lock()
|
|
defer lifecycleMu.Unlock()
|
|
|
|
serviceRegistryMu.Lock()
|
|
if _, exists := serviceRegistry[d]; !exists {
|
|
serviceRegistryMu.Unlock()
|
|
return nil
|
|
}
|
|
close(d.done)
|
|
delete(serviceRegistry, d)
|
|
|
|
if len(serviceRegistry) > 0 {
|
|
serviceRegistryMu.Unlock()
|
|
return nil
|
|
}
|
|
sess := session
|
|
serviceRegistryMu.Unlock()
|
|
|
|
log.Info("sleep detection service stopping (deregister)")
|
|
|
|
if sess == nil {
|
|
return nil
|
|
}
|
|
|
|
if sess.rl != 0 && sess.port != 0 {
|
|
source := ioKit.IONotificationPortGetRunLoopSource(sess.port)
|
|
cf.CFRunLoopRemoveSource(sess.rl, source, cfCommonModes)
|
|
}
|
|
if sess.notifier != 0 {
|
|
n := sess.notifier
|
|
ioKit.IODeregisterForSystemPower(&n)
|
|
}
|
|
|
|
// Clear session only after IODeregisterForSystemPower returns so any
|
|
// in-flight powerCallback can still look up session.rp to ack sleep.
|
|
serviceRegistryMu.Lock()
|
|
session = nil
|
|
serviceRegistryMu.Unlock()
|
|
|
|
if sess.rp != 0 {
|
|
ioKit.IOServiceClose(sess.rp)
|
|
}
|
|
if sess.port != 0 {
|
|
ioKit.IONotificationPortDestroy(sess.port)
|
|
}
|
|
if sess.rl != 0 {
|
|
cf.CFRunLoopStop(sess.rl)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Detector) triggerCallback(event EventType, cb func(event EventType), done <-chan struct{}) {
|
|
if cb == nil || done == nil {
|
|
return
|
|
}
|
|
|
|
select {
|
|
case <-done:
|
|
return
|
|
default:
|
|
}
|
|
|
|
doneChan := make(chan struct{})
|
|
timeout := time.NewTimer(500 * time.Millisecond)
|
|
defer timeout.Stop()
|
|
|
|
go func() {
|
|
defer close(doneChan)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Errorf("panic in sleep callback: %v", r)
|
|
}
|
|
}()
|
|
log.Info("sleep detection event fired")
|
|
cb(event)
|
|
}()
|
|
|
|
select {
|
|
case <-doneChan:
|
|
case <-done:
|
|
case <-timeout.C:
|
|
log.Warn("sleep callback timed out")
|
|
}
|
|
}
|
|
|
|
// NewDetector initializes IOKit/CoreFoundation bindings and returns a Detector.
|
|
func NewDetector() (*Detector, error) {
|
|
if err := initLibs(); err != nil {
|
|
return nil, err
|
|
}
|
|
return &Detector{}, nil
|
|
}
|
|
|
|
func initLibs() error {
|
|
libInitOnce.Do(func() {
|
|
iokit, err := purego.Dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", purego.RTLD_NOW|purego.RTLD_GLOBAL)
|
|
if err != nil {
|
|
libInitErr = fmt.Errorf("dlopen IOKit: %w", err)
|
|
return
|
|
}
|
|
cfLib, err := purego.Dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", purego.RTLD_NOW|purego.RTLD_GLOBAL)
|
|
if err != nil {
|
|
libInitErr = fmt.Errorf("dlopen CoreFoundation: %w", err)
|
|
return
|
|
}
|
|
|
|
purego.RegisterLibFunc(&ioKit.IORegisterForSystemPower, iokit, "IORegisterForSystemPower")
|
|
purego.RegisterLibFunc(&ioKit.IODeregisterForSystemPower, iokit, "IODeregisterForSystemPower")
|
|
purego.RegisterLibFunc(&ioKit.IOAllowPowerChange, iokit, "IOAllowPowerChange")
|
|
purego.RegisterLibFunc(&ioKit.IOServiceClose, iokit, "IOServiceClose")
|
|
purego.RegisterLibFunc(&ioKit.IONotificationPortGetRunLoopSource, iokit, "IONotificationPortGetRunLoopSource")
|
|
purego.RegisterLibFunc(&ioKit.IONotificationPortDestroy, iokit, "IONotificationPortDestroy")
|
|
|
|
purego.RegisterLibFunc(&cf.CFRunLoopGetCurrent, cfLib, "CFRunLoopGetCurrent")
|
|
purego.RegisterLibFunc(&cf.CFRunLoopRun, cfLib, "CFRunLoopRun")
|
|
purego.RegisterLibFunc(&cf.CFRunLoopStop, cfLib, "CFRunLoopStop")
|
|
purego.RegisterLibFunc(&cf.CFRunLoopAddSource, cfLib, "CFRunLoopAddSource")
|
|
purego.RegisterLibFunc(&cf.CFRunLoopRemoveSource, cfLib, "CFRunLoopRemoveSource")
|
|
|
|
modeAddr, err := purego.Dlsym(cfLib, "kCFRunLoopCommonModes")
|
|
if err != nil {
|
|
libInitErr = fmt.Errorf("dlsym kCFRunLoopCommonModes: %w", err)
|
|
return
|
|
}
|
|
// Launder the uintptr-to-pointer conversion through a Go variable so
|
|
// go vet's unsafeptr analyzer doesn't flag a system-library global.
|
|
cfCommonModes = **(**uintptr)(unsafe.Pointer(&modeAddr))
|
|
|
|
// NewCallback slots are a finite, non-reclaimable resource, so register
|
|
// a single thunk that dispatches to the current Detector set.
|
|
callbackThunk = purego.NewCallback(powerCallback)
|
|
})
|
|
return libInitErr
|
|
}
|
|
|
|
// powerCallback is the IOServiceInterestCallback trampoline, invoked on the
|
|
// runloop thread. A Go panic crossing the purego boundary has undefined
|
|
// behavior, so contain it here.
|
|
func powerCallback(refcon, service, messageType, messageArgument uintptr) uintptr {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Errorf("panic in sleep powerCallback: %v", r)
|
|
}
|
|
}()
|
|
switch messageType {
|
|
case kIOMessageCanSystemSleep:
|
|
// Not acknowledging forces a 30s IOKit timeout before idle sleep.
|
|
allowPowerChange(messageArgument)
|
|
case kIOMessageSystemWillSleep:
|
|
dispatchEvent(EventTypeSleep)
|
|
allowPowerChange(messageArgument)
|
|
case kIOMessageSystemHasPoweredOn:
|
|
dispatchEvent(EventTypeWakeUp)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func allowPowerChange(messageArgument uintptr) {
|
|
serviceRegistryMu.Lock()
|
|
var port uintptr
|
|
if session != nil {
|
|
port = session.rp
|
|
}
|
|
serviceRegistryMu.Unlock()
|
|
if port != 0 {
|
|
ioKit.IOAllowPowerChange(port, messageArgument)
|
|
}
|
|
}
|
|
|
|
func dispatchEvent(event EventType) {
|
|
serviceRegistryMu.Lock()
|
|
snaps := make([]detectorSnapshot, 0, len(serviceRegistry))
|
|
for d := range serviceRegistry {
|
|
snaps = append(snaps, detectorSnapshot{
|
|
detector: d,
|
|
callback: d.callback,
|
|
done: d.done,
|
|
})
|
|
}
|
|
serviceRegistryMu.Unlock()
|
|
|
|
for _, s := range snaps {
|
|
s.detector.triggerCallback(event, s.callback, s.done)
|
|
}
|
|
}
|
|
|
|
// runRunLoop owns the OS-locked thread that CFRunLoop is pinned to. Setup
|
|
// result is reported on errCh so Register can surface failures synchronously.
|
|
func runRunLoop(errCh chan<- error) {
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
|
|
sess, err := setupSession()
|
|
if err == nil {
|
|
serviceRegistryMu.Lock()
|
|
session = sess
|
|
serviceRegistryMu.Unlock()
|
|
}
|
|
errCh <- err
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Errorf("panic in sleep runloop: %v", r)
|
|
}
|
|
}()
|
|
cf.CFRunLoopRun()
|
|
}
|
|
|
|
// setupSession performs the IOKit registration on the current thread. Panics
|
|
// are converted to errors so runRunLoop never leaves errCh unsent.
|
|
func setupSession() (s *runLoopSession, err error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
err = fmt.Errorf("panic during runloop setup: %v", r)
|
|
}
|
|
}()
|
|
|
|
var portRef, notifier uintptr
|
|
rp := ioKit.IORegisterForSystemPower(0, &portRef, callbackThunk, ¬ifier)
|
|
if rp == 0 {
|
|
return nil, fmt.Errorf("IORegisterForSystemPower returned zero")
|
|
}
|
|
|
|
rl := cf.CFRunLoopGetCurrent()
|
|
source := ioKit.IONotificationPortGetRunLoopSource(portRef)
|
|
cf.CFRunLoopAddSource(rl, source, cfCommonModes)
|
|
|
|
return &runLoopSession{rl: rl, port: portRef, notifier: notifier, rp: rp}, nil
|
|
}
|