Pama

Mac mini Mode:让 MacBook 合盖也不睡

· 约 3 分钟读完

目录

手边有台 MacBook,想把它放在家里当 Mac mini 用:跑点服务、当个无头主机,盖子合上塞进角落就行。问题是 MacBook 一合盖就睡,常见的办法都有点别扭:

  • 接一个 HDMI 假负载,骗系统说有外接显示器;
  • 开着终端跑 caffeinate,关掉窗口就失效;
  • sudo pmset disablesleep 1,有效,但得记着关,不然哪天塞进包里它还醒着,一路发热。

我想要的其实很简单:插着电就别睡,拔了电就正常睡。于是写了 Mac mini Mode。

Mac mini Mode 安装器

它做了什么

  • 插电自动开启:接上电源适配器后不再睡眠,合盖、不接显示器也照常运行。
  • 拔电自动关闭:切到电池马上恢复正常睡眠;如果这时已经合盖,直接让它睡下去。
  • 菜单栏三档:自动、始终开启、始终关闭,当前状态一眼能看到。

安装包签了名、过了 Apple 公证,支持 macOS 13 及以上,Apple 芯片和 Intel 都能用。直接下载,双击装完菜单栏就多一个图标,不用重启。

怎么做的

整个项目三百多行 Swift,分成两个进程:

  1. 守护进程 macmini-moded:以 root 身份作为 LaunchDaemon 常驻,真正去改系统的睡眠设置。
  2. 菜单栏程序:普通用户权限,只负责显示状态和让你切换模式。

盯着电源变化

核心就是监听电源来源的变化。IOKit 提供了 IOPSNotificationCreateRunLoopSource,电源一变就回调:

guard let runLoopSource = IOPSNotificationCreateRunLoopSource({ _ in sync() }, nil)?.takeRetainedValue() else {
    exit(1)
}
CFRunLoopAddSource(CFRunLoopGetMain(), runLoopSource, .defaultMode)

判断当前是不是插着电:

func onACPower() -> Bool {
    let info = IOPSCopyPowerSourcesInfo().takeRetainedValue()
    let type = IOPSGetProvidingPowerSourceType(info).takeUnretainedValue() as String
    return type == kIOPSACPowerValue
}

以系统里的真实值为准

sync() 每次都去读 IOPMrootDomain 上的 SleepDisabled,跟期望值不一致才调用 pmset -a disablesleep 1/0。这样即使别的工具或者手动敲的 pmset 改掉了设置,下一轮对账也能纠正回来。

另外还有个每分钟一次的定时器兜底。万一漏掉一次通知,比如睡眠期间拔了电,醒来后最多一分钟就能对上。

拔电后补一脚

这里有个坑:把 disablesleep 改回 0,已经合上盖的机器并不会自己睡下去。如果不处理,拔电塞进包里,它还是醒着的。

所以关掉 Mac mini 模式两秒后,守护进程会再看一眼:

guard !wantsMacMiniMode(),
      rootDomainFlag("AppleClamshellState"),
      rootDomainFlag("AppleClamshellCausesSleep") else { return }
pmset(["sleepnow"])

AppleClamshellState 表示盖子是合着的,AppleClamshellCausesSleep 是内核自己的判断:接着外接显示器时它是 false,这时就不去打扰。两个都满足才 sleepnow。

两个进程怎么说话

菜单栏程序没有 root 权限,不能直接改睡眠设置。它和守护进程之间用的是 Darwin 通知(notify.h),非常轻:

  • 菜单栏发 com.macminimode.set-auto / set-on / set-off;
  • 守护进程收到后切换模式,把当前模式写进 com.macminimode.state 通知的 state 值里;
  • 菜单栏用 notify_get_state 读出来更新界面。

不需要 XPC,也不需要开端口。模式会存到 /var/db/macmini-mode,重启后保留。

退出时恢复原样

守护进程收到 SIGTERM 或 SIGINT 时,会先执行 pmset -a disablesleep 0 再退出。卸载或者进程被停掉,机器都会回到正常的睡眠行为,不会留下一个「永远不睡」的状态。

最后

这是一个很小的工具,但解决的是每天都会碰到的事。代码在 GitHub 上,MIT 协议,有问题欢迎提 issue。