
Photo by Jakub Żerdzicki on Unsplash
ASCII Saver turns your camera feed into live ASCII art and shows it when you walk away from your Mac. It needs the camera to do that, obviously. What it should not do is keep the camera running after your displays have gone to sleep.
It did, following the conversion from screensaver to menu-bar app.
The app already handled this properly in every case I had thought about. Lock the screen and it stops the camera. Quit it and it stops the camera. Put the whole machine to sleep and it stops the camera. There is even a comment in the code, next to the lock-screen handler, spelling out the reasoning:
Nobody can see the frames under loginwindow, and leaving the camera live behind a lock screen would be indefensible.
Perfectly logical reasoning I thought. And then I did not apply it to the one case that matters most to anyone paying attention to their machine, which is the displays going into standby while the screensaver is still up.
There was no handler for that at all. Not a broken one — an absent one. The app listened for the screens waking and had never once been told to care about them sleeping. So the screens went dark, the app carried on rendering ASCII art to nobody, and the camera light stayed on.
Here is what that looked like on my own machine, from macOS’s power log alongside the app’s own:
12:05:30 Display is turned off
12:36:49 Display is turned on
11:20:20 capture started
12:36:50 capture stopped
Thirty-one minutes of camera, with the screen black. It stopped one second after the display came back — not because anything noticed, but because I woke the machine myself, which dismissed the screensaver.
The fix is four lines: listen for the displays sleeping, pause the rendering, stop the camera. It handles the whole machine going to sleep too, which macOS would have dealt with anyway, but it is better that the app agrees with reality than discovers it later.
I shipped that fix at breakfast. Then I tested it, and the camera was still on.
This is where it gets instructive, because three separate things told me the fix was live, and all three were correct, and the fix was not running.
The app bundle on disk said version 2.0.1. Sparkle, the updater, put up a dialog saying “You’re up to date — ASCII Saver 2.0.1 is currently the newest version available.” And I had verified the release myself: the right version stamped, signed, notarised, stapled.
Every one of those reads the app on disk. None of them says anything about the code actually executing in memory.
What had happened is mundane. The bundle was replaced at 06:42. The running process had started at 06:37, five minutes earlier, and had not restarted it. On macOS a running program carries on executing the file it was launched from even after that file has been replaced underneath it. So the folder contained 2.0.1 while the thing actually running was 2.0.0, and every version check I could think of was reading the folder.
The way to tell is to ask the process what it is actually running, rather than asking the filesystem what is sitting there:
running process → inode 187827496, 1236576 bytes
on disk → inode 187855967, 1237152 bytes
Different files. Case closed, and it took one command.
I have a note to myself about this — verify the running binary before theorising — written after being caught by exactly this class of thing before. I did not follow it. I announced a fix, moved on to something else, and let the machine tell me a comfortable story for six hours.
Once the right code was actually running, I still had to demonstrate the fix worked, and that turned out to be awkward in a way I enjoyed.
The obvious test is: bring the screensaver up, put the displays to sleep, see what happens. But you cannot type a command to sleep the displays while the screensaver is showing, because typing dismisses the screensaver. The test destroys its own conditions.
The answer was to have the machine do it unattended: a script that watches the app’s log until the camera starts, waits a few seconds, sleeps the displays itself, then reports what the app did. Bring up the screensaver, take your hands off the keyboard, and the whole thing runs without you.
The result, with timestamps:
12:58:16.937 capture started
12:59:26.386 screens slept — pausing render and stopping capture
12:59:26.476 capture stopped
The display went dark at 12:59:26. The camera was off ninety milliseconds later. Compare that with thirty-one minutes.
There was one honest false alarm along the way, which I mention because it is the failure mode of tests generally. My first attempt reported FAIL — the app had not stopped the camera. But the line above the verdict said the command to sleep the displays had itself errored, and the displays had never slept. The test was reporting on an event that never happened. A verdict that does not check its own preconditions is not a verdict, it is a coin toss with good grammar.
Nothing, if you have automatic updates on — but do check that ASCII Saver has actually restarted since the update landed, which is a sentence I would not have written yesterday. Sparkle relaunches the app when it installs, so under normal circumstances you are fine. My own instance got into that state because I was building and replacing the bundle by hand.
Version 2.0.1 is available now, via the download page or Homebrew.
The behaviour change is small and one-directional: when your displays sleep, the camera stops, and the screensaver stops drawing frames nobody can see. Everything else is exactly as it was.