loading...

Finding Distributed Notifications on macOS Catalina

technocoder profile image Technocoder Originally published at Medium on ・4 min read

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.

A notification generated using AppleScript

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).

iTunes Discord 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.

Code that listens for all distributed notifications

All right, let’s start it up and wait for all the data to pour in.

It certainly looks like it’s running!

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:

Searching for Spotify’s notification

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.

List of strings containing the bundle identifier

One of those names looks suspiciously similar to an iTunes notification. In particular: com.apple.Music.playerInfo. Let’s try it out!

Updated code for a particular notification

Output from the notification listener

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 in addObserver 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.

Discussion

pic
Editor guide
Collapse
andrewrothman profile image
Andrew Rothman

That's really interesting. I didn't know macOS had this feature.