0

I am trying to run grunt watch when my user logs into my OS X machine, so that I don't have to run grunt watch in my $APP_ROOT dir manually every time.

I have the following org.grunt.watch.plist file within /Library/LaunchAgents:

<?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>org.grunt.watch</string>

    <key>RunAtLoad</key>
    <true/>

     <key>ProgramArguments</key>
    <array>
      <string>/usr/local/bin/grunt-watch-launchd</string>
    </array>

  </dict>
</plist>

/usr/local/bin/grunt-watch-launchd is a shell script I created:

#!/bin/sh

APP_ROOT=/path/to/htdocs/app
/usr/local/bin/daemon -- /usr/local/bin/grunt watch --gruntfile="$APP_ROOT"/Gruntfile.js > /tmp/grunt-watch-launchd.log 2>&1

The script should use the daemon utility (installed with brew install daemon) and "daemonize" the grunt watch command which in turn loads a Gruntfile.js configuration and starts the watch task.

When I manually run /usr/local/bin/grunt-watch-launchd from the CLI with my user, everything works great. I see a daemon with ps aux | grep grunt which runs the grunt watch command. If I then edit a file inside APP_ROOT, Grunt's watch task triggers the specific tasks inside my Gruntfile.js.

But the point is that I would like that launchd automatically starts /usr/local/bin/grunt-watch-launchd when I log in with my user. The problem is that when I run launchctl:

launchctl load /Library/LaunchAgents/org.grunt.watch.plist

The service is not loaded. Or at least, launchd calls the script (because I see that the file /tmp/grunt-watch-launchd.log is created so the script runs, though /tmp/grunt-watch-launchd.log is empty), but the daemon process seems not to be created or is somehow killed by launchd.

Also, nothing appears inside /var/log/system.log. If I try to run launchctl with sudo:

sudo launchctl unload /Library/LaunchAgents/org.grunt.watch.plist && sudo launchctl load /Library/LaunchAgents/org.grunt.watch.plist

/tmp/grunt-watch-launchd.log will contain the following line:

daemon: fatal: refusing to execute unsafe program: /usr/local/bin/grunt (/usr/local/lib is group writable)

Using sudo, /var/log/system.log tells me:

Jun 11 18:22:24 antons-mbp com.apple.xpc.launchd[1] (org.grunt.watch[83009]): Service exited with abnormal code: 1

In either ways (launchctl with and without sudo), I can confirm that the service is not started:

mymachine:~ user$ launchctl list | grep grunt
-   0   org.grunt.watch

What's wrong and what's the correct way to run this script as a daemon when my user logs in?

Thanks for your attention.

EDIT: I forgot to say that I use Mac OS X 10.12 Sierra, installed Grunt with npm and I am using a MacBook Pro (13-inch, Mid 2012) (if that may help).

EDIT 2: I found the issue. When running as a launchd agent, the script was not able to find a command because the user of the agent wasn't my user. Therefore a 127 error occurred.

So this is for all the guys like me that stuck on a really simple launchd agent:

  • Always check that the commands you use are in the PATH of the user who launches the command. Use absolute paths, and if necessary, set the PATH variable on the first line of your script.

  • Redirect all the command output to a file so that you can see if an error occurs when launchd launches the script (in my case the error didn't appear in /var/log/system.log for some reason).

tonix
  • 6,671
  • 13
  • 75
  • 136

1 Answers1

1

I'm not familiar with either grunt or the daemon utility, but I'm pretty sure daemon is conflicting with the way launchd manages jobs. Essentially, you're using two different daemon-creation tools that do things differently, and you need to pick just one.

More specifically, I suspect daemon is launching grunt in the background (i.e. daemonizing it). But launchd expects the jobs it manages to stay in the foreground where it can monitor and control them. What's probably happening is that launchd launches your script, the script runs grunt watch in the background and then exits... and launchd sees that the job has exited and helpfully cleans up leftover subprocesses like grunt. Result: no actual grunt daemon running.

You could add <key>AbandonProcessGroup</key><true/> to your .plist to tell launchd not to kill off background processes, but really it'd be better to do things the launchd way and leave grunt in the foreground. So either remove the daemon command from your script, or replace it with exec which'll run grunt as a top-level process under launchd (while running it normally would make it subprocess of the shell, and leave the shell as the top-level process under launchd).

There may be other problems here, but it'll be hard to tell until this one gets fixed.

Gordon Davisson
  • 118,432
  • 16
  • 123
  • 151