This is still the header! Main site

Installing Services in One Line

2024/02/27

Installing e.g. web services on a computer is a lot more complicated than what it should be. By pretending that everything is on a LAN, you can make things a bit simpler... but then... having something run all the time is a lot harder than what it should be. You need to... write an init script? Create a systemd unit? (... what were the potential options again?)

Even though it could be stupidly simple.

The problem

I'm a fan of the s6 init system. It's as UNIX as it gets; it's lean, it is holding onto your processes instead of tracking them in broken ways via PID files; also, it has an entire scripting language that is based on just nesting UNIX commands into each other with no actual shell running (it is pretty cool in a "pretty math" way).

The way it works is... you have a /service directory, the subdirectories of which describe services. (... this "tree for configs" idea also feels familiar somehow.) You have /service/our_thing/run, which is an arbitrary executable that will launch the service. (... whenever it quits, the service is dead; "detaching" and similar weird things aren't needed here.) Meanwhile, /service/our_thing/log/run is yet another script which will get connected to the stdout of our process; what it prints goes to a log file.

Taking our Lisp service as an example, here is how a run file looks like:

#!/usr/local/bin/execlineb -P

/usr/local/bin/s6-setuidgid simon  # run as me
/usr/local/bin/fdmove -c 2 1 # redirect stderr to stdout
/usr/bin/sbcl --load /home/simon/local/slime/start-swank-unix.lisp
          

It is... nice, but... to set up all these things... and then realize that you got it wrong, edit again etc... it's... really not fun.

It's really not much prettier with systemd either:

[Unit]
Description=My Service Running SBCL with Slime

[Service]
Type=simple
User=simon
Group=simon
ExecStart=/usr/bin/sbcl --load /home/simon/local/slime/start-swank-unix.lisp
StandardOutput=inherit
StandardError=inherit

[Install]
WantedBy=multi-user.target
          

It... might be subtly wrong (... as everything that GPT 4 produces in about 10 seconds); the point is that it might be subtly wrong... also, remembering all these is... nontrivial, unless your main job involves setting up services regularly.

(There aren't a lot of people whose main job involves setting up services regularly.)

A solution

You should be able to say something like:

/usr/bin/sbcl --load /home/simon/local/slime/start-swank-unix.lisp
          

... and then watch the thing run. Or crash, depending. (It's way easier to fix crashes if they happen right in front of you.)

And then, once you got it running... installing it as a service should be a one-liner. Such as...

make-s6-service.sh our-sbcl-service /usr/bin/sbcl --load /home/simon/local/slime/start-swank-unix.lisp
          

... as in: just prepend something to your already-working command. Done. The service should be ready & configured. Or... at least ready for you to tweak further.

That should be it.

Here is an example implementation that does this. Partially from the depths of time, back when Codex was The Thing and you had to pretend you're writing comments at the beginning of your script instead of just telling GPT 4 what you want:

#!/bin/bash

# A bash script taking a service name as a parameter. It:
# - creates the directory /service/[servicename] if it doesn't exist
# - creates /service/[servicename]/run and makes it executable; puts in a single line with a shebang with /usr/bin/execlineb -P
# - creates /service/[servicename/log/run, which has something like
# #!/usr/bin/execlineb -P
# /usr/bin/s6-log T /var/log/[servicename]

# Written by Codex because hell yes

set -e

if [ $# -lt 1 ]; then
    echo "Usage: $0  [service and args to be run]*"
    exit 1
fi

# Pick off the first argument as the service dir name; we'll forward the rest to the service
SERVICE_NAME=$1
shift

if [ ! -d /service/$SERVICE_NAME ]; then
    mkdir -p /service/$SERVICE_NAME
fi

# This is where syntax highlighting fails us; note the "heredoc" syntax for multiline files
if [ ! -f /service/$SERVICE_NAME/run ]; then
    cat </service/$SERVICE_NAME/run
#!/usr/bin/execlineb -P
/usr/bin/s6-setuidgid simon
export HOME /home/simon
/usr/lib/execline/bin/fdmove -c 2 1
$@
EOF
    chmod +x /service/$SERVICE_NAME/run
fi

# Creating the "log" subtree (/service/[name]/log)
if [ ! -d /service/$SERVICE_NAME/log ]; then
    mkdir -p /service/$SERVICE_NAME/log
fi

# ... this is also an execlineb script
if [ ! -f /service/$SERVICE_NAME/log/run ]; then
    echo "#!/usr/bin/execlineb -P" > /service/$SERVICE_NAME/log/run
    echo "/usr/bin/s6-log T /var/log/$SERVICE_NAME" >> /service/$SERVICE_NAME/log/run
    chmod +x /service/$SERVICE_NAME/log/run
fi

# We also pre-create the log directory in /var/log
if [ ! -d /var/log/$SERVICE_NAME ]; then
    mkdir -p /var/log/$SERVICE_NAME
fi

# This will re-scan the services so that we can bring things up right away
sudo s6-svscanctl -an /service
          

Of course... bash scripts might be ugly and there might be better solutions for this. The important part is the interface though: the part that this is almost as easy as running a UNIX command.

(... you might argue that it's silly that there is an arbitrary distinction of files that are persistent & processes that are not... we don't quite have to fix all the things ever right away though.)