How to run script at wake?

I'm looking for a method to run a script (well, a single shell command) when my laptop wakes from sleep.

This question has been asked before, but 8 years ago (for OS X Lion), and several of the answers require third-party products that are now no longer developed.

Another option might be for the script to run at any time, once per day, when it's up and running, but I don't want it to miss an allotted time slot just cause it's not awake. I'm not so bothered about triggering a script at sleep time.


Solution 1:

launchd may do what you need. As long as your system isn't asleep the entire day, and it is scheduled using the StartCalendarInterval (see below), launchd will run your script once a day. Here's what I mean:

  • If you schedule your script to run at (for example) 12:00 noon, AND your mac is awake at 12:00 noon, then your script will run at 12:00 noon.
  • If you schedule your script to run at (for example) 12:00 noon, BUT your mac is asleep at 12:00 noon, then your script will run as soon as your mac wakes up.

IMHO, launchd has an advantage because it is part of MacOS. The only 3rd party tool you might want to use with it is LaunchControl (instead of the native launchctl). LaunchControl is a GUI-based app used only to help you create/edit your .plist, and can help managing & troubleshooting if that becomes necessary. The .plist file contains the instructions that will be used by launchd to start your program, and can be very simple. And of course you can avoid 3rd party software completely by simply creating the required .plist "manually" with a text editor.

As mentioned above, the configuration key you need to use in your .plist to schedule the time to run your job/script is StartCalendarInterval. According to man launchd.plist:

Unlike cron which skips job invocations when the computer is asleep, launchd will start the job the next time the computer wakes up. If multiple intervals transpire before the computer is woken, those events will be coalesced into one event upon wake from sleep.

Example

Here's an example of how to use launchdto create a User Agent. The scope of a User Agent is that it only runs for one user. Note that it is also possible to create Global Agent, or a Global Daemon that runs for multiple/all users, but we'll leave that for another day. :

  1. Create a script in your home directory (~/) to output the date and time whenever it's run:
#!/bin/bash
CURRENTDATE=`date +"%c"`
echo Current Date and Time is: ${CURRENTDATE}

Name the script echodatetime.sh & make it executable:

$ chmod 755 ~/echodatetime.sh  
  1. Create a .plist file in ~/Library/LaunchAgents/sdm.simple.exampleofPLIST.plist:

EDIT: NOTE! DO NOT USE ~/ as shortcut for user's home directory! You must use a full path specification, or it won't work.

<?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>Disabled</key>
    <false/>
    <key>Label</key>
    <string>seamus.simple.example</string>
    <key>ProgramArguments</key>
    <array>
        <string>/Users/seamus/echodatetime.sh</string>
    </array>
    <key>StandardErrorPath</key>
    <string>/Users/seamus/echodatetime.error.txt</string>
    <key>StandardOutPath</key>
    <string>/Users/seamus/echodatetime.log.txt</string>
    <key>StartCalendarInterval</key>
    <array>
      <dict>
          <key>Hour</key>
          <integer>1</integer>
          <key>Minute</key>
          <integer>25</integer>
      </dict>
      <dict>
          <key>Hour</key>
          <integer>1</integer>
          <key>Minute</key>
          <integer>26</integer>
      </dict>
      <dict>
          <key>Hour</key>
          <integer>1</integer>
          <key>Minute</key>
          <integer>27</integer>
      </dict>
      <dict>
          <key>Hour</key>
          <integer>1</integer>
          <key>Minute</key>
          <integer>28</integer>
      </dict>
      <dict>
          <key>Hour</key>
          <integer>1</integer>
          <key>Minute</key>
          <integer>29</integer>
      </dict>
    </array>
</dict>
</plist>

This .plist will cause ~/echodatetime.sh to be executed at the following times every day:
01:25
01:26
01:27
01:28
01:29

You can change the integer values in the array for the key StartCalendarInterval to get the timing that suits you. This is just an example that I used for testing.

The example above shows a "burst" of five (5) runs at one minute intervals. This was done to show the somewhat odd syntax required for such a schedule. Scheduling a single event to run once a day, every day, at 12:00 noon may be accomplished by substituting the simpler StartCalendarInterval key shown below into the .plist shown above:

    <key>StartCalendarInterval</key>
    <array>
        <dict>
            <key>Hour</key>
            <integer>12</integer>
            <key>Minute</key>
            <integer>0</integer>
        </dict>
    </array>  
  1. load your job & check that it's running:
$ launchctl load ~/Library/LaunchAgents/sdm.simple.exampleofPLIST.plist  
$ launchctl list | grep seamus
  -   0   seamus.simple.example 
  1. Monitor the output file declared in the StandardOutPath key:
$ tail -f ~/echodatetime.log.txt  

After loading the .plist file, you will see the datetime output at the times designated in the StartCalendarInterval key: at one minute intervals beginning at 01:25 and ending at 01:29 if you use my .plist file.

Miscellany

  • The .plist files above have been tested, and operated successfully on my macbook pro running Mojave (ver 10.14.6). Also verified the behavior of launchd when an event schedule occurs during sleep: The task ran immediately after the mac "woke up", and logged the time it awakened (i.e. not the scheduled time).

  • Yes, the .plist syntax is arcane! Consider using LaunchControl instead of manually hacking these files.

  • A potentially useful hint: You can check the syntax of your .plist file like so:

$ plutil -lint /Users/seamus/Library/LaunchAgents/sdm.simple.exampleofPLIST.plist  
/Users/seamus/Library/LaunchAgents/sdm.simple.exampleofPLIST.plist: OK  
  • When you're finished with this example, you may remove the job from launchd, and then delete or move the file from your folder ~/Library/LaunchAgents. Otherwise, your job will be re-started by launchd next time you log in:
$ launchctl remove seamus.simple.example  
$ mv ~/LaunchAgents/sdm.simple.exampleofPLIST.plist ~/archive

Alternatively, leave the file where it is, and set the Disabled key to true in the .plist:

    <key>Disabled</key>
    <true/>  

Solution 2:


There is no way to do “run on wake” without 3rd-party software. launchd does not support this functionality natively, so any solution that tries to use launchd will be highly likely to fail.

Having said that, there are several good 3rd party options.

SleepWatcher, mentioned by Edd Growl is the only free solution that I am aware of. It has not been updated in a long time, but as far as I know, it still works.

Power Manager which was also mentioned above is an incredibly powerful and capable tool. I wanted to echo the recommendation for that app since I am not involved with its development, but am just a happy paying customer. (I can also say that Power Manager’s developer is highly responsive.)

Keyboard Maestro can run scripts on wake, or sleep, and probably hundreds of other things. It’s such a versatile tool that I recommend every serious Mac owner should own a copy. (It can replace at least a dozen other utilities.) Keyboard Maestro is one of the first apps that I have to install on a new Mac or else it just feels broken. This is the solution that I recommend. Power Manager is a close second.


Edited to add: If you want to run something once-per-day, then launchd is a good solution, and it’s free -- although the syntax is difficult to I would recommend LaunchControl.

Solution 3:

This functionality of running script after waking up can be achieved with the powerful hammerspoon:

function printf(s,...)  print(s:format(...)) end
wather = hs.caffeinate.watcher.new(function(eventType)    
    -- screensDidWake, systemDidWake, screensDidUnlock
    if eventType == hs.caffeinate.watcher.systemDidWake then
        local output = hs.execute("/bin/echo -n hello", false)
        hs.notify.new({title="TestTitle", informativeText=output}):send()
        printf("%s, world", output)
    end
end)
wather:start()

Put those script to $HOME/.hammerspoon/init.lua and reload hammerspoon, and you can check the above /bin/echo output in the hammerspoon console.
There are several wake/sleep events, including screensDidWake, systemDidWake, screensDidUnlock, screensDidSleep, systemWillSleep. See hs.caffeinate.watcher api doc for details.

Solution 4:

You could install SleepWatcher via homebrew and set a .wake code with your script that will launch every time your laptop wakes from sleep.

To use SleepWatcher: make a shell script in your home directory named .wake and give it permission with the command chmod +x ~/.wake.

You can find out how to use SleepWatcher here