Skip to content
C.W.K.
Stream
Lesson 01 of 06 · published

LaunchServices and the One Slot in /Applications

~15 min · macos-citizen, launchservices, install, open, verification

Level 0Bundle Opener
0 XP0/81 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"Identity, not timestamps. The mtime would have looked newer on the wrong binary."

The Database Behind Every Launch

LaunchServices is the macOS database that answers questions like "which app is com.example.spark", "which app handles spark://" and "what opens a .md file". A double-click in Finder, open -a, open -b, a URL clicked in a browser and NSWorkspace.openApplication all go through it. That is why every family build script ends by registering the installed bundle with lsregister -f (the tool lives in LaunchServices.framework/Support, not on PATH), so the database sees the new build immediately, and why the previous track insisted on launching with open: an app started through LaunchServices is its own responsible process, receives its open events, and activates like an app, while the executable run straight from a shell is just a child of that shell.

Two open details bite once each. open --env sets environment variables only when it starts a process, so against an app that is already running it refuses with "already running and so the additional environment variables could not be set". Add -n for a new instance, or quit the old one first. And the process is named after CFBundleExecutable, not the bundle. A smoke script that ran pkill -x with the bundle name matched nothing, because the executable inside was called something else. Read the name from Info.plist.

/Applications Is One Global Slot

On a Mac with several checkouts of one repository and several sessions working in them, /Applications/Spark.app is a single slot that all of them write. An installed-app gate for a family client failed three times in a row against a binary that did not contain the code under test. Another session had run build and install from a different checkout in the middle of the run. Every existing guard passed: bundle identifier, product version, signature, designated requirement, even CFBundleVersion. All of those are true of any build of that app. A long detour through environment delivery and argument expansion ended with strings on the installed binary: the new marker string was simply not there.

The fix is to check identity where identity lives. When the checkout holds a build product, the installed executable's SHA-256 must equal it. The same check catches the ordinary version of the mistake, which is rebuilding and forgetting to install. Timestamps prove nothing here, because the wrong binary was the newer file.

A Diagnosis Order That Works

When an installed app behaves as if your edit is not there: first strings -a the installed binary for a literal you just added (an interpolated Swift string appears as its literal pieces, so search for a prefix). Then hash the installed executable against every build product on the machine to learn which tree it came from. Only then look at the environment the running process received, with ps eww on its pid.

Code

Prove the installed app is this checkout's build, then launch it through LaunchServices·bash
app=/Applications/Spark.app
exe=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app/Contents/Info.plist")
installed="$app/Contents/MacOS/$exe"
built=".build/spark-app/Spark.app/Contents/MacOS/$exe"

if [[ -x $built ]]; then
  [[ $(shasum -a 256 "$installed" | cut -d' ' -f1) == $(shasum -a 256 "$built" | cut -d' ' -f1) ]] \
    || { echo "HARD_FAIL installed Spark is not this checkout's build"; exit 1; }
fi
strings -a "$installed" | grep -q 'spark-smoke-marker' || echo "marker missing: not your code"

# Launch through LaunchServices by full path. -n starts a new instance so --env applies.
open -n --env SPARK_SMOKE=1 -a "$app"
sleep 2
pid=$(pgrep -x "$exe") && ps eww "$pid" | tr ' ' '\n' | grep '^SPARK_SMOKE='
pkill -x "$exe"   # the executable name, which need not equal the bundle name

External links

Exercise

Add a unique literal such as spark-smoke-marker to Spark's launch log line and install the app. Write scripts/test-installed-app.sh that fails unless the installed executable's hash equals the build product's, then launches with open -n --env SPARK_SMOKE=1 and confirms the variable reached the process. Now rebuild without installing and run the gate again: it must fail, and its message must say which check failed.
Hint
ps eww <pid> prints the process environment after its arguments, separated by spaces. Splitting on spaces and matching the start of a line is enough for a variable without spaces in its value.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.