I’ve been playing a lot lately with AppleScript and the Mac Automater app, both of which can do some pretty cool stuff. This gave me an idea: wouldn’t it be great if I could close certain applications when my commuters go to sleep?
I have a handful of programs that I run on both my MacBook and Mac Pro desktop at work, using file syncing tools like Dropbox or BTSync to keep the data files up to date on both machines. This generally works great, except that I have to remember to close the programs when I switch machines, otherwise I sometimes end up with unexpected results like locked files or, in some cases, data loss. I’m not so good at this and Apple doesn’t appear to provide a way to do it for me.
It didn’t take long, however, to find a free third-party utility that does. Sleepwatcher is a small daemon process that monitors the sate of your system and kicks off a shell script when certain system events occur. It can monitor for events such as sleep and wake up, display sleep and dimming, and even system idle (a specified period with no keyboard or mouse activity) and power status (when a MacBook switches from AC to battery power and vice-versa). It can also prevent the machine from sleeping based on the result of a script or it can run a script when another process prevents the system from sleeping. Sleepwatcher can be run as a system process (always running in the background, even when no users are logged in), as a user process for individual logged-in users, or both.
Before we get too far along, it should be noted that this may not be the best approach for all applications. The method I’m about to describe works best with applications that save their data files automatically, and exit without user intervention. While the applications will be shut down in a clean manner, you may have unexpected results if the application prompts you to save or to confirm that you really want to quit.
It is also very important to note that you should be extremely careful when implementing these scripts. An improper configuration could render your machine unable to sleep, wakeup, or even boot, so be sure to carefully test your scripts before enabling them to run automatically.
Installing Sleepwatcher from the developer’s site is a bit tricky as it comes with no installer and assumes some knowledge of the Unix command line. A much easier way to install it is to use a package manager such as Homebrew (I believe MacPorts can also install Sleepwatcher, but I’ve only done it with Homebrew). If you aren’t familiar with Homebrew, it is definitely worth checking out.
Assuming that you have Homebrew installed and working correctly, installing Sleepwatcher is as easy as running the following command in a terminal window:
brew install sleepwatcher
Homebrew will download and install Sleepwatcher at /usr/local/sbin/sleepwatcher. Note the followup instructions that Homebrew provides after installation, as we’ll get to them in a moment. Once the installation is is done, I recommend reading the man page, as it is the best way to get to know everything Sleepwatcher can do:
man sleepwatcher
Now it’s time to write a sleep script. As we’ll find out in a moment, Sleepwatcher looks for user scripts named ~/.sleep
and ~/.wakeup
and system scripts named /etc/rc.sleep
and /etc/rc.wakeup
. Since we want to close programs that are running under our own UID, let’s choose the local user option.
First, create a file named .sleep
in your home directory using your editor of choice (mine is vi for this kind of stuff):
vi ~/.sleep
Then add the following to the file (in this example, I’m going to close the program KeePassX, my password manager, using AppleScript):
#!/bin/bash
osascript -e 'tell application "KeyPassX" to quit'
If you want to close additional applications, simply add another osascript command for each additional application.
Why use AppleScript instead of something more bash-like, such as kill? AppleScript works inside the application, telling it to do a clean exit, such as if I pressed Comand-Q to close it myself. This allows the program to make sure files are saved and everything is in order before the process ends. Kill simply aborts the running process, regardless of what’s happening, which could result in data loss and other instabilities, which we’re trying to prevent in the first place.
After the script is saved, you’ll need to give it execute premissions:
chmod 700 ~/.sleep
New we’re ready to test the script. Enter the following in a terminal window to start Sleepwatcher:
/usr/local/sbin/sleepwatcher --verbose --sleep ~/.sleep
You won’t see anything happen; in fact, it will look like the terminal is hanging. Make sure KeePassX (or whatever program you added to the .sleep
file) is running and then close the lid of your MacBook (or go to Apple Menu > Sleep if you aren’t on a MacBook). Wait until the computer’s power light starts to slowly blink on and off, and then wake it by opening the lid and/or pressing the power button. The computer should resume exactly as you left it except that your target application should no longer be running. If anything went wrong, check the terminal window where you ran Sleepwatcher, it should show any errors that occurred. Press Control-C to stop Sleepwatcher.
Now it is time to configure launchd to run Sleepwatcher at startup or login. To do this, we need to add a plist file to our user or system’s LaunchAgents directory. Sleepwatcher comes with sample plist files that handle the four use cases mentioned above: user sleep, user wakeup, system sleep, and system wakeup.
If you only need support for sleep and wakeup, you can simply symlink the sample files to the proper LauchAgents directories. For this activity we only need to set up the user LaunchAgent since we aren’t using any system scripts:
ln -sfv /usr/local/Cellar/sleepwatcher/2.2/de.bernhard-baehr.sleepwatcher-20compatibility-localuser.plist ~/Library/LaunchAgents/
Then we tell launchd to load the configuration:
launchctl load ~/Library/LaunchAgents/de.bernhard-baehr.sleepwatcher-20compatibility-localuser.plist
Although not required in this example, to install the system agent, do the following:
sudo ln -sfv /usr/local/Cellar/sleepwatcher/2.2/de.bernhard-baehr.sleepwatcher-20compatibility.plist /Library/LaunchAgents/
sudo launchctl /Library/LaunchAgents/de.bernhard-baehr.sleepwatcher-20compatibility.plist
Note that you’ll need to be an administrator of the machine to install the system agent.
That’s about all there is to it! Sleepwatcher is now running in the background waiting for your computer to go to sleep. When they system does, Sleepwatcher will kick off your .sleep
script and, when the system resumes, Sleepwatcher will run your .wakeup
script (if you create one). And, since you also added the plist file to your LaunchAgents directory, launchd will find it and start Sleepwatcher every time your machine starts up or whenever you log in.
If you want to script other events, such as system idle or power status, you’ll need to make a copy of the sample plist file(s) and edit them by hand. Understanding the plist file format should be fairly straightforward if you compare the sample with the options described on the man page. Xcode features a graphical plist editor, but plists are simple XML files, so you can edit them in any text editor if you prefer.
Do you have a creative use for Sleepwatcher? Let me know about it in the comments.
Thanks for this Kodiak,
I’m using sleepwatcher to halt Vagrant boxes and clear hosts files on sleep, it’s great to be able to manage the package with brew.
One thing that drove me a bit crazy, in your examples above you have a typo:
LuanchAgents should be LaunchAgents
Thanks, Kevin, glad you found it useful. I’ve fixed the typo.
Hi, this is a great article as I am trying to replicate a similar event on waking up my mac from sleep. I am running OSX EL Capitan – and for some reason, when I awake from a sleep state, the mail app is active on my desktop, even when the window is closed (but active in the dock). Not sure if this is a bug, but I haven’t been able to find any way to fix this other than running sleepwatcher until apple figures out the issue. I have installed Sleepwatcher correctly, typing “man sleepwatcher” I do get a text return in terminal. I have made a .”wakeup” file in my home location (/Users/GHOST/hidemail.wakeup/hide\ mail.scpt) and have written my script to hide mail:
tell application “System Events”
set visible of process “Mail” to false
end tell
– and saved it to the .wakeup file in the home location. I am not sure where to take it from here. I understand I need to add the plist file to the launch agent directory, just not sure what the plist should show? Thanks in advance for the help.
It was very useful. I used for same reason of Kevin: vagrant.
Thank very much Kodiak.
Glad you found it useful.
.wakeup file is not a file extension, it is the name of the file you should be using for your script. In Unix-based systems, any file that starts with a period (commonly called “dotfiles”) are normally hidden from directory listings. They are generally used for configuration files; since they’re hidden, they’re less likely to get changed or deleted accidentally.
When you symlink the “localuser” plist, it already has support for .wakeup scripts (as well as .sleep) so there’s nothing more for you to do. Sleepwatcher also supports a handful of other system states, such as display sleep, which are not mapped to script names in the default plist, so if you want to use any of these, you’ll need to edit the plist file. But if you’re content with just “sleep” and “wakeup,” you can follow my tutorial exactly as I wrote it.
Hi, this is great. I have a couple of questions.
1. Where can I find the logs output of the sleep or wakeup script ?
2. How do I add enhancements to sleepwatcher ? Is it open source ?
SleepWatcher was developed by Bernhard Baehr. You can find it on his website at http://www.bernhard-baehr.de/. I don’t know if it’s open sources, though Baehr does provide a link to the source of an older version on his site. I’m not sure if/where SleepWatcher logs script output as I only use it with simple scripts that produce no output.
You can get logs by adding the following to the plist file
StandardOutPath/tmp/sleepwatcher.out
StandardErrorPath/tmp/sleepwatcher.err
Daniel,
Thanks for sharing the tip about output logging. That’s good to know.
Super useful, thanks Kodiak! I had the exact same use case, using DropBox as a “backend” for my KeePassX files. Not sure what I did wrong, but I had to deviate from your .sleep structure slightly to get it to work:
#!/bin/bash
osascript -e ‘tell application “KeePassX”
quit
end tell’
Anyway, thanks again!
You’re welcome. Glad you found it helpful. I’m definitely no AppleScript expert, so I can’t really speculate why my examples didn’t work for you, but as long as you got it working, that’s what counts!
Hi Kodiak!
I’m trying to figure out how to trigger a script after login from the screensaver. Sleepwatcher works on resume from sleep or a fresh login, but not from a screensaver login.
Any tips?
Cheers,
Dis
Hi Dis,
Sorry for taking so long to reply. Unfortunately, I don’t believe there is a way to trigger sleepwatcher when the screensaver activates, but there are a couple of workarounds. For either of these, you’ll need to modify the default .plist file that comes with sleepwatcher since that file only supports “sleep” and “wakeup” functionality. To do this, I’d recommend copying the default file instead of linking it, as I did in my original post, so to prevent an update from overwriting your changes down the road.
cp /usr/local/Cellar/sleepwatcher/2.2/de.bernhard-baehr.sleepwatcher-20compatibility-localuser.plist ~/Library/LaunchAgents/
Then open the
~/Library/LaunchAgents/de.bernhard-baehr.sleepwatcher-20compatibility-localuser.plist
file with XCode, if you have it installed, or your favorite text editor. You’ll see two lines in about the middle of the file that look like this:These are the arguments passed to the sleepwatcher command when you log in that instruct it on what system events to monitor. Simply add additional blocks below those two lines with additional arguments to monitor different events.
The two events you may want to consider are Display Sleep (-S) and Idle (-i).
Display Sleep is triggered when the computer’s display goes to sleep, which you can configure in the Energy Saver pane in your System Preferences. Display sleep generally kicks in a while after the screen saver does (otherwise you’d never see the screen saver), but this may be an acceptable work-around, depending on your application. To monitor display sleep, add the following to your plist file:
If you need your script to run sooner than when the display sleeps, you might consider using the Idle monitor, which kicks in after a specified duration with no keyboard or mouse activity. If you set it to the same duration as your screensaver timeout, you should get a result that’s pretty close to perfect. The idle argument requires a second timeout argument, so add both of these to the .plist files, replacing the XXXX in the second line with your desired timout. This value is counted in tenths of seconds, so for one minute use 600, for ten minutes use 6000, etc.
You may also want to add arguments for display wakeup (-W) and idle resume (-R), but I’ll let you figure those out yourself.
Once you’ve edited and saved the .plist file, you’ll need to reload it for your changes to take effect. To do this run these two commands in a Terminal window:
launchctl unload ~/Library/LaunchAgents/de.bernhard-baehr.sleepwatcher-20compatibility-localuser.plist
launchctl load ~/Library/LaunchAgents/de.bernhard-baehr.sleepwatcher-20compatibility-localuser.plist
Then be sure to add your .displaysleep and/or .idle scripts to your home directory (and make sure you set permissions to make them executable).
Thanks! I’ve used this to auto turn on/off Philips Hue lights of my studio when computer awakes/sleeps
Thanks for those clear explanations. I’m using it to turn all internet connections off and back on while computer is asleep.
As of July 2020 the software is running v2.2.1, you want to make sure to specific
ln -sfv /usr/local/Cellar/sleepwatcher/2.2.1/de.bernhard-baehr.sleepwatcher-20compatibility-localuser.plist ~/Library/LaunchAgents/
for it to work properly.
Thank you Marius for the update! Thank you for the grate work having this program. 😉
Hi! Thanks for everything. I’m stuck at the launchd load configuration, that fails with the message “Load failed: 5: Input/output error
Try running `launchctl bootstrap` as root for richer errors.”
I don’t know what to do!!
Any hints?
Great write up, just what I needed to solve an issue with my 2.5GB USB NIC failing after sleep.
As of 11 June 2022, macOS 12.4 – I had to use this command to activate the user plist
ln -sfv /opt/homebrew/Cellar/sleepwatcher/2.2.1/homebrew.mxcl.sleepwatcher.plist ~/Library/LaunchAgents/
Kodiak,
This article was super helpful, sleepwatcher is the exact tool I needed to automate some tasks and make my life much easier, but finding documentation (and good documentation) on it was extremely difficult. One thing I might mention is that when I installed sleepwatcher using homebrew, my files were in /opt/homebrew, so the commands were slightly different for me (probably because this article was written 8 years ago):
“ln -sfv /usr/local/Cellar/sleepwatcher/2.2/de.bernhard-baehr.sleepwatcher-20compatibility-localuser.plist ~/Library/LaunchAgents/”
was instead
“ln -sfv /opt/homebrew/Cellar/sleepwatcher/2.2.1/homebrew.mxcl.sleepwatcher.plist ~/Library/LaunchAgents/”
and naturally,
“launchctl load ~/Library/LaunchAgents/de.bernhard-baehr.sleepwatcher-20compatibility-localuser.plist”
was instead
“launchctl load ~/Library/LaunchAgents/homebrew.mxcl.sleepwatcher.plist”
Hopefully this helps if anyone ends up being stuck on not being able to find the necessary files.
It was really surprising and relieving that an 8 year old article helped me implement something I’d been struggling with for a few hours. It seems like the sky is the limit for automating tasks (both useful and for fun) now that this is working on my computer.
Thanks again Kodiak, you’re a lifesaver!
Philo,
I’m not sure if it’ll solve your specific problem, but I was getting this same error when trying to enable OpenSC on my new work computer. On my case, the OpenSC module isn’t loading correctly for some reason (I still don’t know why), but the OS sees it as loaded, causing that error. In my case I had to `launchctl unload` and the `launchct load` again and then OpenSC started working. Maybe you’re having a similar issue? Good luck.
Kodiak (and/or anyone having the same issue),
I tried this on macOS Monterey 12.6.3, specifically the display sleep trigger “-S .displaysleep” and it does not seem to be working. Running the command prompted me to grant “Input Monitoring” privacy access, but even after doing so and re-launching the command putting the display to sleep (via a hot corner) did not run the script despite it being set to mode 700. Any assistance here would be greatly appreciated.