With a new macOS update comes new breaking changes. One particular breaking change on Catalina is how observing distributed notifications works.
But first, what are distributed notifications?
Many people will be familiar with the concept of regular notifications. You know, that alert that pops up somewhere on your screen that gives you some information about something.
Distributed notifications are similar to regular notifications (in fact, they send the same NSNotification
structure) but instead of showing them to the user they can be broadcasted to other processes that listen for that particular notification. More generally, distributed notifications are a form of interprocess communication.
What can we use distributed notifications for?
One very useful notification that iTunes issues is the com.apple.iTunes.playerInfo
notification. This notification is issued whenever the user changes the state of the current track including when they stop playing or select a new track. Associated with the notification is a bunch of useful fields about the change such as the track name, artist, and total track length.
By registering an observer to this notification I created an iTunes Discord Rich Presence client that automatically updates what song I am currently playing (somewhat similar to Spotify’s Rich Presence).
The main benefit of this compared to polling the iTunes player state with AppleScript is that it uses less processor time and is hence more energy efficient.
The problem with macOS Catalina
Catalina replaced the beloved iTunes application with Apple Music (which is incidentally a lot slower and laggier but that is a story for another time). Of course, that means the name of any iTunes related distributed notifications have changed.
The fix is simple. Just replace the old notification name with the new one. One of the parameters for registering the notification observer is:
notificationName
The name of the notification for which to register the observer; that is, only notifications with this name are delivered to the observer. When
nil
, the notification center doesn’t use a notification’s name to decide whether to deliver it to the observer.
Perfect! All we have to do is leave the name as nil
and that will let us listen to all of the distributed notifications. Then we just have to find the one emitted by changing a track.
All right, let’s start it up and wait for all the data to pour in.
Nothing’s happening?
Here is a hint. The problem isn’t with my code. This method does not work on macOS Catalina. The behaviour of the addObserver
method has changed so that leaving the name asnil
will do nothing if the process is not privileged. Incidentally, running the code under sudo
does not seem to make it work either.
Finding the name of the notification
At first I considered two main options:
- Tracing the Music application’s system calls
- Brute forcing every possible notification name
The former option was quickly ruled out when dtruss
(a system call tracer) failed to start due to System Integrity Protection (and I could not be bothered turning it off). The latter option seemed quite feasible but when I did the math:
- 26 lowercase and 26 uppercase letters
- At least ten characters in the name
- 52¹⁰ is about 144 quadrillion operations
Yeah, that is not going to work.
That is when I realised there was another option that finds a lot of use in reverse engineering: analysing the executable itself. Since the notification name is a string we can use the strings tool to find all the strings in the executable.
To test out my idea I tried running strings
on Spotify first. The bulk of the actual executable is located inside of the application bundle at: Contents/MacOS/Spotify
. We know that one of Spotify’s notification names iscom.spotify.client.PlaybackStateChanged
so we can match that against the output from strings Spotify
:
Great! Let us try doing the same with Apple Music now.
On Catalina the Music application is located in /System/Applications
. Even though we don’t know the exact name of the notification we can filter out a lot of the strings by matching against com.apple.Music
. It seems that all of the notifications have to contain the application’s bundle identifier.
One of those names looks suspiciously similar to an iTunes notification. In particular: com.apple.Music.playerInfo
. Let’s try it out!
Brilliant! We finally get some data printed out. It is a little bit different from the iTunes notification (for example, there is no Display Line 0
field anymore) but fixing that is a comparatively easy task.
In summary
- Distributed notifications are a way for different processes to send messages to each other
- Catalina breaks using
nil
as a value for the name parameter inaddObserver
to listen for all distributed notifications - The
strings
tool can be used to find a list of potential notification names for an application by filtering against the bundle identifier
Ideally, Apple should update the documentation to reflect this change in behaviour but it seems that it is too niche to warrant a mention. I am just glad I got my Music Rich Presence working again.
Top comments (1)
That's really interesting. I didn't know macOS had this feature.