For about a quarter of a century, now, we've had toolsets that allow unprivileged users to run nonce services ad hoc. Daniel J. Bernstein's daemontools is one of the earliest. One doesn't have to set up scan directories and other infrastructure. One can just run a program under supervise
directly. M. Bernstein's UCSPI-TCP toolset, furthermore, is an example of the several toolsets that we've had for a similar length of time that handle services that need to accept TCP connections and do stuff.
There are other toolsets that do the same, without need for systemd, and of course with systemd one can also set up ad hoc per-user services.
It is 2020. There is no good reason to write new programs that use the rickety and dangerous PID file mechanism. There's no need for any of that PID file code that you've written.
Similarly, it's a good idea to write a TCP server so that it can inherit its listening socket, as an already open file descriptor. This is in fact the way that system-wide services, run from inted, conventionally have operated since the 1980s. It can be done with per-user services, too. The s6 and nosh toolsets, and indeed systemd, all provide ways in which a service can be given its listening socket. There's no need for any of that nonce URL parsing code that you have written, which like many off-the-cuff parsers doesn't handle all of the forms that a URL can take.
Unfortunately, as I said, your cpp-httplib
library does not have a constructor or function member where it can be given the file descriptor for the listening socket. This is a major oversight in the design of that library, given that inheriting socket file descriptors has been a normal practice for three and a bit decades.
something other than systemd
Both my nosh toolset and Laurent Bercot's s6 toolset provide ways to run nonce programs as ad hoc per-user services.
Under the nosh toolset, one would have a helpcovid
subdirectory, with a service
subdirectory beneath that containing the various programs for handling the service.
helpcovid/service/start
and helpcovid/service/stop
are fairly minimal:
#!/bin/nosh
#Start file generated from ./helpcovid.socketand ./helpcovid.service
true
#!/bin/nosh
#Stop file generated from ./helpcovid.socketand ./helpcovid.service
true
The meat is in helpcovid/service/run
and helpcovid/service/service
:
#!/bin/nosh
#Run file generated from ./helpcovid.socketand ./helpcovid.service
#Starynkevitch helpcovid listening socket
tcp-socket-listen --systemd-compatibility ::0 50002
envdir env
setenv LC_ALL fr_FR.UTF-8
chdir /home/basile/dev/helpcovid/test
./service
#!/bin/nosh
#Service file generated from ./helpcovid.service
#Starynkevitch helpcovid service
sh -c 'exec /home/basile/dev/helpcovid/work/helpcovid ${flags}'
And helpcovid/service/restart
controls the restart logic:
#!/bin/sh
#Restart file generated from ./helpcovid.service
sleep 5
exec true # ignore script arguments
This one would run by giving it to a per-user instance of service-manager
. The toolset does the binding and listening on the socket, reads environment variable configuration from an envdir, and passes the open file descriptor to the eventual program using the LISTEN_FDS
protocol, having changed working directory to the place where your test webroot/
directory lives.
A similar structure is employed by s6, although the tool names are different (e.g. s6-tcpserver6-socketbinder
rather than tcp-socket-listen
) and the restart logic is differently handled. s6 does not require a service manager. Like the original daemontools, one can just run s6-supervise
by hand to invoke a service:
s6-supervise ./helpcovid/service/
I'm not going to go into a lot of detail, as the main point is that not having systemd is no excuse for bad dæmon design (indeed good dæmon design principles originated with much older non-systemd mechanisms) and that the mechanisms for passing open file descriptors for listening sockets apply to and can be used with more than just systemd.
systemd
The aforegiven programs were actually converted from a systemd socket unit and service unit that I quickly threw together. They show how one would set up a per-user service on systemd, with files in ~/.config/systemd/user/
:
# helpcovid.socket
[Unit]
Description=Starynkevitch helpcovid listening socket
[Socket]
ListenStream=50002
Accept=No
[Install]
Wanted-By=default.target
# helpcovid.service
[Unit]
Description=Starynkevitch helpcovid service
[Service]
Environment=LC_ALL=fr_FR.UTF-8
Restart=always
RestartSec=5
ExecStart=/home/basile/dev/helpcovid/work/helpcovid ${flags}
WorkingDirectory=/home/basile/dev/helpcovid/test
#This has no meaning for systemd.
EnvironmentDirectory=env
This is controlled with the likes of systemctl --user start helpcovid.socket
.
In the world of systemd, these are the configuration files. The user wants to change the TCP port number? The user changes the ListenStream=
setting. No need for redundant extra environment variables for setting the locale. The user just sets the actual LC_ALL
(or whatever) variable in the manner shown.
logging
Logging is also handled by service management. The nosh toolset and s6 way is to feed the standard output and standard error through a pipe to a secondary service running cyclog
, s6-log
, or something. systemd stuffs log output into the user part of the centralized systemd journal, which you use journalctl --user
to read.
your program
Your program needs to log to std::clog
(i.e. standard error), call setlocale()
with NULL
to read the standard environment variables, and have code that handles the LISTEN_FDS
mechanism. There are plenty of choices for that last, from a non-portable helper function library that comes with systemd to other people's more portable workalikes.
It's definitely far less code to put in than all of that TCP port number parsing code, rickety and dangerous PID file code, and HELPCOVID_LOCALE
and --locale
code that can be taken out this way. ☺
Further reading