DEV Community

Cover image for Launch a node script at boot on MacOs
mJehanno
mJehanno

Posted on • Edited on

Launch a node script at boot on MacOs

Sometimes, we need to get things started when our os is booting. In windows, we just create a service and set him to start when the session is open, or we can also move a file in a specific folder. With linux you create a daemon. How can we do the same on macOs ?
That's what we're going to explore here !

LaunchD

LaunchD is a tool installed by default on macOs. It is made to handle daemons and agents. LaunchD rely on config files placed in specific folders.
Also, it can manage cron-like task management.

Daemon or Agent ?

Before going anywhere we need to get the difference between Agents and Daemons, at least the difference made by Launchd.
It's pretty straight-forward : it depends which user is running the process.

If the process is running as the current logged user, then you will use an Agent, if it's running as root, then you will use a Daemon.

LaunchD gives you the possibility to create three types of Agents and two types of Daemon. The creation of any of these types depends on where you create your config file like shown in the table below :

Type Location Run on behalf of
User Agents ~/Library/LaunchAgents Currently logged in user
Global Agents /Library/LaunchAgents Currently logged in user
Global Daemons /Library/LaunchDaemons root or the user specified with the key UserName
System Agents /System/Library/LaunchAgents Currently logged in user
System Daemons /System/Library/LaunchDaemons root or the user specified with the key UserName

Disclaimer : this crystal-clear array is the work of LaunchD info (mentionned in the source part)

Plist file

In order to run your process, LaunchD need a plist config file placed in a specified folder as we saw previously.
A plist file is basically a simple xml file.

The minimal template of a plist file looks like this :

<?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> 
        </dict> 
    </plist>
Enter fullscreen mode Exit fullscreen mode

So, here we just have the xml Schema, a plist tag with used version of plist and a dict.

Dict here is a Dictionary type, so it works witk Key and Values.

First thing first, we need to give a name to our process, it's required by launchd and it needs to be unique as it will be used to identify our job :

<?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.mjehanno.myScript</string>
        </dict> 
    </plist>
Enter fullscreen mode Exit fullscreen mode

We can then define the program we want to run. There is two way of doing this :

You can either have a script defined in a 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.mjehanno.myScript</string>
            <key>Program</key>
            <string>~/Scripts/myScript.sh</string>
        </dict> 
    </plist>
Enter fullscreen mode Exit fullscreen mode

Or you can pass a array of argument which seems to be the prefered way when dealing with a node script (at least if you don't want to have to handle shebangs and many environnement variable).

<?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.mjehanno.myScript</string>
            <key>ProgramArguments</key>
            <array>
        <string>~/.nvm/versions/node/v14.18.2/bin/node</string>
                <string>~/Documents/Projects/Javascript/myApp/bin/myApp.js</string>
                <string>arg1</string>
                <string>arg2</string>
            </array>
        </dict> 
    </plist>
Enter fullscreen mode Exit fullscreen mode

Speaking about environnement variables, you can pass some to your job.
Let's imagine we need something in our PATH.
We just need to add a dictionary with the right Key :

<?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.mjehanno.myScript</string>
            <key>ProgramArguments</key>
            <array>
        <string>~/.nvm/versions/node/v14.18.2/bin/node</string>
                <string>~/Documents/Projects/Javascript/myApp/bin/myApp.js</string>
                <string>arg1</string>
                <string>arg2</string>
            </array>
            <key>EnvironmentVariables</key>
            <dict>
                <key>PATH</key>
                <string>    /Users/mjehanno/.nvm/versions/node/v14.18.2/bin:/Users/mjehnno/.nvm/versions/node/v14.18.2/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/Users/mjehanno/Documents/Tools</string>
            </dict>
        </dict> 
    </plist>
Enter fullscreen mode Exit fullscreen mode

And that's it ! We have a plist file, defining our Job with a unique label, the script we need to run and we even gave him some context with our environnement variable.

Nb : plist files don't really like wildcard like * so you should avoid them in path

Enable the agent

Load and Run

Before launching our Agent, we need to load our job definition file in LaunchD.

LaunchD come with an handy cli called launchctl.

So now if we want to load our job we can run the following :

launchtl bootstrap gui/502 ./com.mjehanno.myScript.plist

launchctl bootstrap takes a domain target ( gui/502 where 502 is my UserId) and a path to our plist file.

Now we can start it with :

launchctl kickstart gui/502/com.mjehanno.myScript

If you want your job to run directly when loaded there is also an option you can pass in the 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.mjehanno.myScript</string>
            <key>ProgramArguments</key>
            <array>
        <string>~/.nvm/versions/node/v14.18.2/bin/node</string>
                <string>~/Documents/Projects/Javascript/myApp/bin/myApp.js</string>
                <string>arg1</string>
                <string>arg2</string>
            </array>
            <key>EnvironmentVariables</key>
            <dict>
                <key>PATH</key>
                <string>    /Users/mjehanno/.nvm/versions/node/v14.18.2/bin:/Users/mjehnno/.nvm/versions/node/v14.18.2/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/Users/mjehanno/Documents/Tools</string>
            </dict>
            <key>RunAtLoad</key>
            <true/>
        </dict> 
    </plist>
Enter fullscreen mode Exit fullscreen mode

Error Handling

We just launched our agent but we have nothing, no return, no error, we don't know if it's running correctly or not.

Launchctl gives us the possibility to list our jobs :

launchctl list (you might want to grep on the label you defined in your plist file)

This command will just display a list of all jobs loaded with their PID (if they are running), their label and a code representing their current status. Yet we don't know what does status code means.

No problem here, launchctl at the rescue again :

launchctl error [errorCode] will give you a human readable description of the problem.

Also, you can unload your job at any time :

launchctl bootout /gui/502/com.mjehanno.myScript

Or stop it with :

launchctl kill [sigTerm] /gui/502/com.mjehanno.myScript

Finally, in your plist file you can also redirect stdout and stderr of your job to files.

<?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.mjehanno.myScript</string>
            <key>ProgramArguments</key>
            <array>
        <string>~/.nvm/versions/node/v14.18.2/bin/node</string>
                <string>~/Documents/Projects/Javascript/myApp/bin/myApp.js</string>
                <string>arg1</string>
                <string>arg2</string>
            </array>
            <key>EnvironmentVariables</key>
            <dict>
                <key>PATH</key>
                <string>    /Users/mjehanno/.nvm/versions/node/v14.18.2/bin:/Users/mjehnno/.nvm/versions/node/v14.18.2/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin:/Users/mjehanno/Documents/Tools</string>
            </dict>
            <key>RunAtLoad</key>
            <true/>
            <key>StandardOutPath</key>
            <string>/tmp/com.mjehanno.myScript.out</string>
            <key>StandardErrorPath</key>
            <string>/tmp/com.mjehanno.myScript.err</string> 
        </dict> 
    </plist>
Enter fullscreen mode Exit fullscreen mode

Warning : In case of an Agent, your user needs to have write access to the path you provided for StandardOutPath or StandardErrorPath.

Tips

I bumped into a command supposed to verify the integrity of your plist file :

plutil [pathToPlistFile]

GUI

If you don't want to deal with this stuff yourself, you can use a GUI app to manage your LaunchD configuration. At the moment I'm writing this I found two available options.
Both are paid-app but you can still use some part freely (only saving configuration will not work on the free-tier).

  • LaunchControl
  • Lingon

[EDIT]

TUI

If you're not afraid of using a terminal, you can use launch-tui to manage your agents and this one is completly free.

[EDIT]

Sources

LaunchD info

Launchctl 2.0 Syntax

Launchctl cheatsheet

Top comments (0)