More AnyBar

If you’ve been reading this blog for any length of time, you won’t be surprised to hear that I’ve made some changes to the AnyBar/SuperDuper setup I talked about a couple of days ago. I think the new system is more robust than my first stab at it.

But before I get to that, I want to mention a couple of other menubar-related items. First, in response to a question/request by T.J. Luoma, Brett Terpstra has made a fork of AnyBar that allows text to be displayed in the menubar in addition to or instead of the little graphics that Nikita Prokopov’s original is limited to. With Brett’s version, you can have

• Just an image
• Just text
• An image and text

T.J.’s a cagy old bird. He knew that acting helpless within earshot would have Brett leaping to his rescue. When I saw T.J.’s tweet, I thought Brett would have a solution before the end of the day. But Brett’s slowing down—it wasn’t ready until the following morning.

Unbeknownst to all of us, though, was TextBar a \$3 app from Rich Somerfield that already does what T.J. wants. It doesn’t display graphics, but with Emoji and the rest of Unicode, I doubt that’s a significant limitation. TextBar seems to have a simpler user interface, especially when you want to display more than one signal in your menubar, but it’s meant to be used only for signals that are updated regularly. AnyBar is more flexible, but that flexibility comes with reduced simplicity.

With those announcements out of the way, let’s talk about the changes I made to my AnyBar/SuperDuper setup. As you may recall, the idea is to have an image in the menubar that tells me whether my most recent backup was successful or not. Originally, I set one of SuperDuper’s advanced options to run a script upon completion that would set the signal image. I decided this wasn’t the best way to go about it. If SuperDuper gets hung up at some point—which can happen—I have no guarantee that the signalling script will run. So I changed things to have the script run independently of SuperDuper via launchd, OS X’s system for automatically running programs.

Because I use several Launch Agents, I popped for LaunchControl by soma-zone to manage them. If you need to manage only one or two, you can probably get by with the command-line tool, launchctl, that Apple provides. Soma-zone has written a nice introduction to both launchctl and the whole launchd system.

The automatic running of the signalling script, sdsignal, by launchd is configured through the plist file com.leancrew.sdsignal.plist, which is saved in my ~/Library/LaunchAgents folder. By saving it here, it gets loaded (but not necessarily run) whenever I log in. It can also be loaded manually by running

launchctl load ~/Library/LaunchAgents/com.leancrew.sdsignal.plist


from the Terminal.

The configuration file is set to run sdsignal at 5:56 AM on Tuesday through Saturday. These days were chosen because I have SuperDuper do its backup every weekday evening. There are no backups on Saturday or Sunday night, so there’s no need to run sdsignal on Sunday or Monday morning. Here’s the complete plist file:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.leancrew.sdsignal</string>
<key>Program</key>
<string>/Users/drdrang/Dropbox/bin/sdsignal</string>
<key>StartCalendarInterval</key>
<array>
<dict>
<key>Hour</key>
<integer>5</integer>
<key>Minute</key>
<integer>56</integer>
<key>Weekday</key>
<integer>2</integer>
</dict>
<dict>
<key>Hour</key>
<integer>5</integer>
<key>Minute</key>
<integer>56</integer>
<key>Weekday</key>
<integer>3</integer>
</dict>
<dict>
<key>Hour</key>
<integer>5</integer>
<key>Minute</key>
<integer>56</integer>
<key>Weekday</key>
<integer>4</integer>
</dict>
<dict>
<key>Hour</key>
<integer>5</integer>
<key>Minute</key>
<integer>56</integer>
<key>Weekday</key>
<integer>5</integer>
</dict>
<dict>
<key>Hour</key>
<integer>5</integer>
<key>Minute</key>
<integer>56</integer>
<key>Weekday</key>
<integer>6</integer>
</dict>
</array>
</dict>
</plist>


You’d think the plist format would allow you to set day ranges, but no. You need to create a separate <dict> entry for each day. While this is not a particularly complicated plist file, I can’t take credit for writing it. LaunchControl wrote it for me after I filled in a couple of fields.

In addition to changing how sdsignal is invoked, I’ve changed the way it works, too. Here’s the new version:

python:
1:  #!/usr/bin/python
2:
3:  import os
4:  import socket
5:  from datetime import date, timedelta
6:
7:  # This script is expected to be run on the day after a backup. If it's
8:  # run on the day of a backup or more than one day after a backup, it
9:  # will indicate a failed backup even if the backup was successful.
10:
11:  # AnyBar communication info.
12:  abhost = '127.0.0.1'      # localhost IP number
13:  abport = 1738             # default AnyBar port
14:
15:  # Where the SuperDuper! log files are.
16:  logdir = (os.environ["HOME"] +
17:            "/Library/Application Support/" +
18:            "SuperDuper!/Scheduled Copies/" +
19:            "Smart Update Backup from Macintosh HD.sdsp/Logs/")
20:
21:  # Get the last log file.
22:  logfiles = [x for x in os.listdir(logdir) if x[-5:] == 'sdlog']
23:  logfiles.sort()
24:  lastlog = logdir + logfiles[-1]
25:
26:  # Get yesterday's date in the format SuperDuper! uses in its log file.
27:  y = date.today() + timedelta(days=-1)
28:  ystr = y.strftime('%a, %b %-e, %Y')   # %-e is the day of month w/o space
29:
30:  # Look for the correct "Started on" line.
31:  lastnight = False
32:  with open(lastlog) as f:
33:    for line in f:
34:      if ('| Info | Started on %s at' % ystr) in line:
35:        lastnight = True
36:        break
37:
38:  # If the date is right, look for the "Copy complete" line.
39:  good = False
40:  if lastnight:
41:    with open(lastlog) as f:
42:      for line in f:
43:        if "| Info | Copy complete." in line:
44:          good = True
45:          break
46:
47:  # At this point, good is True if the log file has the right date and
48:  # SuperDuper finished successfully.
49:
50:  # Send AnyBar the black or red signal depending on whether the backup worked.
51:  # AF_INET is for IPv4 and SOCK_DGRAM is for UDP.
52:  anybar = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
53:  anybar.connect((abhost, abport))
54:  if good:
55:    anybar.send('black')
56:  else:
57:    anybar.send('red')


The biggest differences are:

1. It now looks for the date on which the last SuperDuper backup was run and sends the failure signal if that date was not the day before sdsignal is run. This is in keeping with how SuperDuper and sdsignal are scheduled. It also, as before, sends a failure signal if SuperDuper’s log file indicates that the backup never finished.
2. It now uses a black dot to indicate success instead of a green dot. I decided a black dot was less obtrusive and more indicative of “normal” in the menubar.

This is not a particularly exciting example of automation, mainly because SuperDuper is so reliable. From my experience, it’ll be a long time before I see a red dot in my menubar.