A tiny watchdog that keeps Atoll (Dynamic Island for macOS) able to read what's playing — including players Atoll has no dedicated controller for, such as NetEase Cloud Music (网易云音乐), QQ Music, or anything playing in a browser.
一个用来修复 "Atoll 刘海不显示音乐" 的小守护脚本。适用于 Atoll 自带控制器没有覆盖的播放器(网易云、QQ 音乐、浏览器等)。
Atoll does not read macOS's now-playing registry directly. Its NowPlayingController spawns a long-lived child process that bridges the system now-playing data:
/usr/bin/perl .../mediaremote-adapter.pl .../MediaRemoteAdapter.framework stream
That child is started exactly once, when the controller is created. Atoll has no logic to detect that it died and rebuild it:
init→ starts the streamdeinit→ cleans it up- nothing → "is it still alive? restart it"
When the child dies (mediaremoted restart, sleep/wake glitch, in-place app update, …), the media bridge goes silent permanently. Worse, Atoll then silently falls back to its Apple Music controller, which only talks to Music.app:
// MusicManager.swift
let controllerType = (self.isNowPlayingDeprecated && preferredType == .nowPlaying)
? .appleMusic : preferredType
// and when controller creation fails:
} else if controllerType != .appleMusic, let fallback = createController(for: .appleMusic) {
setActiveController(fallback) // silently becomes "Apple Music only"
}Symptom: the song is visibly playing (and macOS knows about it), but the notch never shows it — and it will never recover on its own.
Diagnosis one-liner (is the bridge alive?):
pgrep -P "$(pgrep -x Atoll)" -fl mediaremote-adapter
# no output -> the bridge is dead, this watchdog is for youEvery 20 seconds:
- Is Atoll running? If not, do nothing (your manual quit is respected).
- Is the music source set to
Now Playing? If not, do nothing — other sources don't use this bridge. - Is Atoll's
mediaremote-adapter … streamchild alive? - If it has been missing for longer than the grace period → restart Atoll.
| Rail | Why |
|---|---|
Acts only when the music source is Now Playing |
Other sources (Spotify / Apple Music / …) legitimately have no such child — avoids killing a healthy setup |
| 35 s grace period per Atoll instance | Never races Atoll's own startup / deprecation check |
| Max 5 restarts per hour | A poisoned environment can't turn into a restart storm |
pgrep -x Atoll (exact process name) instead of pgrep -f <path> |
-f is a substring match on the command line: any script, grep, or editor that merely mentions the path gets matched too — and this script kills what it matches |
Re-verification right before kill |
The kill path re-confirms the PID is still the verified Atoll binary |
| Does nothing when Atoll isn't running | Respects you quitting it (see LAUNCH_IF_NOT_RUNNING) |
git clone https://github.com/<you>/atoll-media-watchdog.git
cd atoll-media-watchdog
mkdir -p ~/Library/Application\ Support/AtollWatchdog
cp atoll-watchdog.sh ~/Library/Application\ Support/AtollWatchdog/
chmod +x ~/Library/Application\ Support/AtollWatchdog/atoll-watchdog.sh
cp com.zhuanz.atoll-watchdog.plist ~/Library/LaunchAgents/
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.zhuanz.atoll-watchdog.plistThe plist hardcodes the script path. If your username isn't
zhuanz, edit it:sed -i '' "s/zhuanz/$USER/g" com.zhuanz.atoll-watchdog.plist
# is the agent loaded and ticking?
launchctl print gui/$(id -u)/com.zhuanz.atoll-watchdog | grep -E "state|runs|last exit"
# what has it done?
tail -f ~/Library/Logs/atoll-watchdog.logHealthy operation is silent — it only logs on state changes:
2026-09-15 19:47:52 发现 Atoll 实例 PID=19265,进入 35s 宽限期
2026-09-15 19:48:32 媒体桥接正常(子进程存活)
2026-09-15 19:48:34 媒体桥接缺失(子进程不在)
2026-09-15 19:48:34 媒体桥接缺失(PID=19265 已运行 42s),重启 Atoll…
2026-09-15 19:48:45 已重启 Atoll,新 PID=20154(本小时第 2 次)
2026-09-15 19:48:48 媒体桥接正常(子进程存活)
# kill the bridge (this is exactly what happens by accident)
kill -9 "$(pgrep -P "$(pgrep -x Atoll)" -f mediaremote-adapter)"
# watch the watchdog heal it within ~20s
tail -f ~/Library/Logs/atoll-watchdog.logATOLL_WATCHDOG_DRY_RUN=1 bash ~/Library/Application\ Support/AtollWatchdog/atoll-watchdog.shlaunchctl bootout gui/$(id -u)/com.zhuanz.atoll-watchdog
rm ~/Library/LaunchAgents/com.zhuanz.atoll-watchdog.plist
rm -rf ~/Library/Application\ Support/AtollWatchdogEdit the variables at the top of atoll-watchdog.sh:
| Variable | Default | Meaning |
|---|---|---|
EXPECTED_SOURCE |
Now Playing |
Only guard while this media source is selected |
GRACE |
35 |
Seconds to wait after a new Atoll instance appears |
MAX_RESTARTS |
5 |
Restart budget per rolling hour |
LAUNCH_IF_NOT_RUNNING |
0 |
1 = also relaunch Atoll if it isn't running |
ATOLL_WATCHDOG_APP |
/Applications/Atoll.app |
Env override for the app path |
ATOLL_WATCHDOG_DRY_RUN |
0 |
Env override: 1 = detect + log only |
After editing, re-copy the script and restart the agent:
launchctl kickstart -k gui/$(id -u)/com.zhuanz.atoll-watchdogIn Atoll → Settings → Media, keep Music Source = Now Playing. It is the only source that works with all players (its own description: "works with most media apps, including browsers"). The other entries are app-specific controllers that only talk to one app each — none of them covers NetEase Cloud Music.
- This is a workaround, not an upstream fix. The proper fix is for Atoll to supervise its own bridge child; please consider filing that upstream.
- If Atoll ever supervises the child itself, this script becomes a no-op (the child will simply always be alive).
- Verified against Atoll 2.3.3 on macOS 26.
MIT — see LICENSE.
Not affiliated with Atoll or NetEase.