Launch a node script at boot on MacOs
Tags: macos, automation, node
Published: 2022-03-07 07:14:51


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](https://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
<?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>
```

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
<?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>
```


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
<?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>
```

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
<?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>
```

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
<?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>
```

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
<?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>
```

#### 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
<?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>
```

*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](https://github.com/wecraftforfun/launch-tui) to manage your agents and this one is completly free.

[EDIT]

## Sources 

[LaunchD info](https://launchd.info/)

[Launchctl 2.0 Syntax](https://babodee.wordpress.com/2016/04/09/launchctl-2-0-syntax/)

[Launchctl cheatsheet](https://gist.github.com/masklinn/a532dfe55bdeab3d60ab8e46ccc38a68)