In the last few years, ruby has taken the world by storm. With different projects dependent on different versions of Ruby, many Rails, Jekyll, and Sinatra programmers have installed RVM to manage their ruby versions in OS X. I am one of them.

While RVM is awesome, it can make some previously simple tasks difficult. One such example is creating a launchd daemon that invokes a ruby script. If your gems are installed with RVM, you will need to use RVM to write your daemon. That's where it gets complicated.

This tutorial teaches you how to schedule a ruby task on OS X using launchd. You can schedule any ruby file to be run. It will be executed using the RVM ruby version of your choice.

The basic gist of this tutorial

You have a ruby file you want to schedule to run periodically with launchd. To accomplish this, we will:

  1. Set set up an RVM alias. The RVM alias will let you run your ruby file "inside of" the RVM environment... instead of OS X's pre-installed ruby
  2. Create a 1-line bash script to run your ruby file using the RVM alias
  3. Register that bash script with launchd using a plist file

Your ruby file will now run in the background of your Mac at the specified interval. Ready to get started?

Step 1: Create your ruby file

This probably goes without saying, but you will need the ruby file you want to schedule! If you have not already, create the file. Note that your ruby file can use any RVM-installed gems. Just ensure the file runs from the command prompt using your RVM ruby...

which ruby
> /Users/you/.rvm/rubies/ruby-1.9.3-p194/bin/ruby

And you're set:

ruby myfile.rb

...should return no errors, and work as expected.

Step 2: Create an RVM alias

Next, you will need to create what is called an RVM alias in order to run your ruby file inside of the launchd daemon. (Creating an alias is somewhat documented here.)

You need an RVM alias because launchd, cron, and other process schedulers operate in discrete bash shell environments. Simply calling ruby from inside your launchd or cron script will not work; that will invoke the non-RVM ruby that OS X shipped with. Instead, you need an RVM alias, which will run your file through RVM's ruby, from inside launchd.

To create the alias, first get your current RVM version of ruby by running:

which ruby

It will return something like this: /Users/you/.rvm/rubies/ruby-1.9.3-p194/bin/ruby. We want that part in the middle that reads: ruby-1.9.3-p194. That is your current ruby version string. Your ruby version may be different.

(N.B. You do not have to use the current RVM ruby version; that's just for the sake of simplicity in this tutorial. You may use any RVM-available ruby.)

Now that we know our current RVM version of ruby, we can create the actual RVM alias. Run:

rvm alias create my_app ruby-1.9.3-p194@my_app

Notice that we typed in the ruby version from the previous step here. Also, replace my_app with the name of your app, so you can identify the reason this alias was created in the future. For example, if you are making a ruby daemon that empties your OS X trash every day, my_app could be empty_trash.

Great. Now your RVM alias is set up and ready to use.

Step 3: Test run your ruby code using the RVM alias from the commandline

Usually, if you wanted to run a ruby file from a bash script, you'd just write:

ruby myfile.rb

But to run your ruby file with RVM, you will need to write:

$rvm_path/wrappers/my_app/ruby myfile.rb

Here, you are using the RVM-installed ruby to execute your file. Test this command now, and ensure it works before proceeding. (Your file should run and exit properly.)

Next, because $rvm_path cannot be expanded inside of launchd, you’ll need to substitute it with the absolute path. Just run:

echo $rvm_path
> /Users/jerzy/.rvm/

and use the output of that command instead. So now your bash code should read something like this:

/Users/jerzy/.rvm/wrappers/my_app/ruby myfile.rb

If that’s successfully running your ruby file, you are ready to go.

But before we move on to step 4, there's one last thing. If you have any arguments to your ruby file, make sure you add them in now. For example:

/Users/jerzy/.rvm/wrappers/my_app/ruby myfile.rb -a somearg

Step 4: Create a bash script to run your ruby code using the RVM alias

Next you need to create a bash file that calls your ruby file. It’s going to be exactly what we typed above, just in a bash file. This is the bash file that launchd will invoke.

Create an empty bash file called my_daemon.sh in the directory /usr/local/bin. Inside the file, simply paste in what we typed a few seconds ago at the end of step 3:

/Users/jerzy/.rvm/wrappers/my_app/ruby myfile.rb -a somearg

And save it. To be sure that your script works, try running:

/usr/local/bin/my_daemon.sh

Your ruby code should run in all its glory. (If not, stop and figure out what is wrong!)

Step 5: Create a .plist file that tells launchd where your bash script is, and how often to run it

Now you’re ready to create your launchd plist file. This is the file that tells launchd what you want to run, how you want to run it, and when you want to run it. Here’s a template that should work for you:

<?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.yourdomain.projectname</string>
 <key>ProgramArguments</key>
 <array>
 <string>/usr/local/bin/my_daemon.sh</string>
 </array>
 <key>KeepAlive</key>
 <dict>
 <key>SuccessfulExit</key>
 <false/>
 </dict>
 <key>RunAtLoad</key>
 <true/>
 <key>StartInterval</key>
 <integer>21600</integer>
</dict>
</plist>

I will not get in to all of these settings; the settings have been optimized for this tutorial use case. The only parameter you need to configure is StartInterval, which is the number of seconds between each run of your script. In the code above, you'll see I have it set to 21,600 seconds -- 6 hours.

Save this plist file to: ~/Library/LaunchAgents/com.yourdomain.projectname.plist.

Step 6: Register your .plist file

You can now register your plist file with launchd. At the commandline, type:

launchctl load ~/Library/LaunchAgents/com.yourdomain.projectname.plist

Congratulations! Your daemon is now running!

If you want to manually invoke your script through launchd (without waiting the interval specified), just type:

launchctl start com.yourdomain.projectname

... and your script will be run. This is great for debugging, as it mirrors how your code will be run by launchd. You can also unregister the plist with launchd like this:

launchctl unload ~/Library/LaunchAgents/com.yourdomain.projectname.plist

After you unregister, the daemon will no longer run at the specified interval. You can re-register it, of course, by running the first command above (launchctl load...).

Debugging it

If things aren’t working, there’s a few suggestions.

First of all, if it makes sense for your script, set the interval to 20 seconds. This will allow you to see what is happening without waiting 6 hours every time it runs.

Second, double check your permissions. They should look like this:

-rwxr--r--   1 yourosxusername  staff     548 Oct 29 15:58   com.yourdomain.projectname.plist
-rwxr-xr-x   1 yourosxusername  admin     114 Oct 29 16:31   my_daemon.sh
-rwxr-xr-x   1 yourosxusername  admin    2733 Oct 29 14:42   myfile.rb