December 14, 2017

macOS Sleep/ Wake notifications in golang

While working on my golang chime SDK I ran across the issue of properly reconnecting when macOS wakes up from sleep mode. Apple provides an official example using IOKit, so I had to wrap this with cgo to use it. This post outlines the required steps and provides an example implementation.

  1. write a small wrapper to register/ unregister notifications

    # main.h
    
    #include <ctype.h>
    #include <stdlib.h>
    #include <stdio.h>
    
    #include <mach/mach_port.h>
    #include <mach/mach_interface.h>
    #include <mach/mach_init.h>
    
    #include <IOKit/pwr_mgt/IOPMLib.h>
    #include <IOKit/IOMessage.h>
    
    io_connect_t root_port;
    IONotificationPortRef notifyPortRef;
    io_object_t notifierObject;
    void *refCon;
    CFRunLoopRef runLoop;
    
    void MySleepCallBack(void *refCon, io_service_t service, natural_t messageType, void *messageArgument) {
        switch (messageType) {
        case kIOMessageCanSystemSleep:
            if (CanSleep()) {
                IOAllowPowerChange(root_port, (long)messageArgument);
            } else {
                IOCancelPowerChange(root_port, (long)messageArgument);
            }
    
            break;
        case kIOMessageSystemWillSleep:
            WillSleep();
            IOAllowPowerChange(root_port, (long)messageArgument);
            break;
        case kIOMessageSystemWillPowerOn:
            WillWake();
            break;
        case kIOMessageSystemHasPoweredOn:
            break;
        default:
            break;
        }
    }
    
    // registerNotifications is called from go to register wake/sleep notifications
    void registerNotifications() {
        root_port = IORegisterForSystemPower(refCon, &notifyPortRef, MySleepCallBack, &notifierObject);
        if (root_port == 0) {
            printf("IORegisterForSystemPower failed\n");
        }
    
        CFRunLoopAddSource(CFRunLoopGetCurrent(),
                        IONotificationPortGetRunLoopSource(notifyPortRef), kCFRunLoopCommonModes);
    
        runLoop = CFRunLoopGetCurrent();
        CFRunLoopRun();
    }
    
    // unregisterNotifications is called from go to remove wake/sleep notifications
    void unregisterNotifications() {
        CFRunLoopRemoveSource(runLoop,
                            IONotificationPortGetRunLoopSource(notifyPortRef),
                            kCFRunLoopCommonModes);
    
        IODeregisterForSystemPower(&notifierObject);
        IOServiceClose(root_port);
        IONotificationPortDestroy(notifyPortRef);
        CFRunLoopStop(runLoop);
    }
    
  2. Expose go functions to cgo, so that we can call into existing go code for network reconnection handling:

    # gofuncs.go
    
    package main
    
    import (
        "C"
        "log"
    )
    
    //export CanSleep
    func CanSleep() C.int {
        return 1
    }
    
    //export WillWake
    func WillWake() {
        log.Printf("Will Wake")
    }
    
    //export WillSleep
    func WillSleep() {
        log.Printf("Will Sleep")
    }
    
  3. Use the notification handler:

    package main
    
    // #cgo LDFLAGS: -framework CoreFoundation -framework IOKit
    // int CanSleep();
    // void WillWake();
    // void WillSleep();
    // #include "main.h"
    import "C"
    import (
        "log"
        "time"
    )
    
    func main() {
        go func() {
            C.registerNotifications()
            log.Printf("Go bye")
        }()
    
        log.Printf("Hello, World")
        time.Sleep(time.Minute)
        C.unregisterNotifications()
        time.Sleep(time.Minute)
    }
    

that’s it - actually quite simple. The trickiest part was getting the LDFLAGS right. Hopefully I won’t need to look this up the next time I need it :)

© Raphael Randschau 2010 - 2022 | Impressum