Writing a basic service for GNU Guix

Table of Contents

Let's write a service for GNU Guix that will automatically start kmonad at boot, and keep it running until shutdown.

Note: before we proceed, we could make a kmonad "daemon" without going to the trouble of writing a Guix-specific service: we could start kmonad in a shell profile, write a cron job, or something else. The user experience of these methods might be the same, but writing a service keeps things organized and seems like the "right" way. If nothing else, it's a great learning opportunity.

Reading the docs

First let's read the documentation of GNU Guix. We see that Guix offers "services" which extend the functionality of the operating system. The manual says:

Guix system services are connected by extensions. For instance, the secure shell service extends the Shepherd—the initialization system, running as PID 1—by giving it the command lines to start and stop the secure shell daemon (see openssh-service-type); the UPower service extends the D-Bus service by passing it its .service specification, and extends the udev service by passing it device management rules (see upower-service); the Guix daemon service extends the Shepherd by passing it the command lines to start and stop the daemon, and extends the account service by passing it a list of required build user accounts (see Base Services).

The Guix daemon sounds very similar to what we're looking to construct. Nearby, we see that the Guix daemon defines its own "service type":

(define guix-service-type
  (service-type
   (name 'guix)
   (extensions
    (list (service-extension shepherd-root-service-type guix-shepherd-service)
          (service-extension account-service-type guix-accounts)
          (service-extension activation-service-type guix-activation)))
   (default-value (guix-configuration))))

Hm. I didn't expect we'd need to instatiate our own data type just to create a simple init daemon. After all, kmonad is simpler than the guix daemon - it just needs to start at boot, and that's it. The documentation is terse and it's not clear whether there's a better way to proceed. Maybe if we find a simple example of service, we can build off of it.

Reading some source

I saw that "Game Services" were listed in the manual. Maybe there's a simple one we can make a template of. First, we find the definition of the games services /gnu/services/games.scm:

;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2018 Arun Isaac <arunisaac@systemreboot.net>
;;;
;;; This file is part of GNU Guix.
;;;
;;; GNU Guix is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or (at
;;; your option) any later version.
;;;
;;; GNU Guix is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with GNU Guix.  If not, see <http://www.gnu.org/licenses/>.

(define-module (gnu services games)
  #:use-module (gnu services)
  #:use-module (gnu services shepherd)
  #:use-module (gnu packages admin)
  #:use-module (gnu packages games)
  #:use-module (gnu system shadow)
  #:use-module (guix gexp)
  #:use-module (guix modules)
  #:use-module (guix records)
  #:use-module (ice-9 match)
  #:export (wesnothd-configuration
            wesnothd-configuration?
            wesnothd-service-type))

;;;
;;; The Battle for Wesnoth server
;;;

(define-record-type* <wesnothd-configuration>
  wesnothd-configuration make-wesnothd-configuration wesnothd-configuration?
  (package wesnothd-configuration-package
           (default wesnoth-server))
  (port wesnothd-configuration-port
        (default 15000)))

(define %wesnothd-accounts
  (list (user-account
         (name "wesnothd")
         (group "wesnothd")
         (system? #t)
         (comment "Wesnoth daemon user")
         (home-directory "/var/empty")
         (shell (file-append shadow "/sbin/nologin")))
        (user-group
         (name "wesnothd")
         (system? #t))))

(define wesnothd-shepherd-service
  (match-lambda
    (($ <wesnothd-configuration> package port)
     (with-imported-modules (source-module-closure
                             '((gnu build shepherd)))
       (shepherd-service
        (documentation "The Battle for Wesnoth server")
        (provision '(wesnoth-daemon))
        (requirement '(networking))
        (modules '((gnu build shepherd)))
        (start #~(make-forkexec-constructor/container
                  (list #$(file-append package "/bin/wesnothd")
                        "-p" #$(number->string port))
                  #:user "wesnothd" #:group "wesnothd"))
        (stop #~(make-kill-destructor)))))))

(define wesnothd-service-type
  (service-type
   (name 'wesnothd)
   (description
    "Run The Battle for Wesnoth server @command{wesnothd}.")
   (extensions
    (list (service-extension account-service-type
                             (const %wesnothd-accounts))
          (service-extension shepherd-root-service-type
                             (compose list wesnothd-shepherd-service))))
   (default-value (wesnothd-configuration))))

There's only one game contained in (gnu services games) and that is The Battle for Wesnoth Server. The Game Services reference says

To run wesnothd in the default configuration, instantiate it as:

(service wesnothd-service-type)

So far, we have a module that exports wesnothd-service-type and a way to use that binding in our system config to declare a service. Apparently, a new service-type is what we're looking for after all. Let's start by implementing a kmonad-service-type and see where that takes us.

Writing kmonad-service-type

A service-type (see Service Reference) takes a name, description, and a list of service-extension objects, each of which describes how to extend existing services. So what exactly is a <service-extension> object? The service-extension documentation says:

Scheme Procedure: service-extension target-type compute

Return a new extension for services of type target-type. compute must be a one-argument procedure: fold-services calls it, passing it the value associated with the service that provides the extension; it must return a valid value for the target service.

So in the context of (define kmonad-service-type ...), a compute function will receive the (single) value associated with kmonad-service-type (a value which we have yet to define) and will return the value required by target-type, which is the service being extended. The wesnothd-service-type extends two services: the account-service-type which is extended by a list of user-group and user-account objects; and the shepherd-root-service-type which is extended by a list of <shepherd-service> objects.

So which services should kmonad extend? It's recommended to make a dedicated user and group for a daemon (see stackoverflow), so let's extend the account-service-type with a new user and group to run the kmonad daemon. Let's also extend the shepherd-root-service-type because we want our daemon managed by the init system, Shepherd:

(define kmonad-service-type
  (service-type
   (name 'kmonad)
   (description
    "Run kmonad as a daemon.")
   (extensions
    (list (service-extension account-service-type
                             (const %kmonad-daemon-accounts))
          (service-extension shepherd-root-service-type
                             (compose list kmonad-shepherd-service))))))

It looks pretty similar to wesnothd-service-type, but without the default-value, which doesn't really apply to kmonad. Next, we've got to choose how to extend the account-service-type and the shepherd-root-service-type.

Extending the account-service-type

As noted above, (const %kmonad-daemon-accounts) needs to evaluate to a function of a single argument that returns a list of users and groups. Well, const makes a function that takes some argument, and then just returns whatever was passed to const–in this case, %kmonad-daemon-accounts. All we've got to do is define %kmonad-daemon-accounts.

We need one daemon user and one group. Our daemon user won't require login, so we'll use /sbin/nologin (see man 8 nologin) like %wesnothd-accounts. Similarly, it won't require a home directory. We can pretty much copy the %wesnothd-accounts to make %kmonad-daemon-accounts:

(define %kmonad-daemon-accounts
  (list (user-account
         (name "kmonad-daemon")
         (group "kmonad-daemon")
         (system? #t)
         (comment "kmonad daemon user")
         (home-directory "/var/empty")
         (shell (file-append shadow "/sbin/nologin")))
        (user-group
         (name "kmonad-daemon")
         (system? #t))))

Extending the shepherd-root-service-type

As noted above, (compose list kmonad-shepherd-service) needs to evaluate to a function of a single argument (a single argument of our choice) and return a list of <shepherd-service> objects. That means that kmonad-shepherd-service must take a single argument, and return a single shepherd-service! Before we write kmonad-shepherd-service, let's decide what we want to pass it. Minimally, kmonad needs a .kbd file to run. In theory, a user could also specify other stuff (e.g. a log level), but let's just start with the .kbd path.

If we're using wesnoth-shepherd-service as an example, we'll need to think about the requirement, start, and stop fields.

requirement field

We definitely need to wait for udev, but maybe we should also wait for user-processes like the other daemons in the shepherd graph do. Looking at the source for user-processes:

This is a synchronization point used to make sure user processes and daemons get started only after crucial initial services have been started—file system mounts, etc. This is similar to the 'sysvinit' target in systemd.

In theory we could explicitly wait for other shepherd services, but it seems like this catch-all applies perfectly to our use case. Come to think of it, I wonder why wesnothd doesn't depend on user-processes too.

start and stop fields

According to the Shepherd Services documentation, the start and stop fields of shepherd-service take G-Expressions. But what's a g-expression? Well, because Guix uses Scheme for both higher-level actions–like defining packages–and lower-level actions–like building derivations generated by packages– it needs a faculty for embedding lower-level code in higher-level code. So in the start field of wesnoth-shepherd-service:

(start #~(make-forkexec-constructor/container
          (list #$(file-append package "/bin/wesnothd")
                "-p" #$(number->string port))
          #:user "wesnothd" #:group "wesnothd"))

Some lower-level code is passed with #~(...), within which higher-level code is escaped with #$(...) which the compiler is able to "lower" to lower-level code. Looking at the source for make-forkexec-constructor/container, we see:

This is a variant of 'make-forkexec-constructor' that starts COMMAND in NAMESPACES, a list of Linux namespaces such as '(mnt ipc). MAPPINGS is the list of <file-system-mapping> to make in the case of a separate mount namespace, in addition to essential bind-mounts such /proc.

And the documentation for make-forkexec-contructor reads:

Return a procedure that forks a child process, closes all file descriptors except the standard output and standard error descriptors, sets the current directory to directory, sets the umask to file-creation-mask unless it is #f, changes the environment to environment-variables (using the environ procedure), sets the current user to user and the current group to group unless they are #f, and executes command (a list of strings.) The result of the procedure will be the PID of the child process. Note that this will not work as expected if the process “daemonizes” (forks); in that case, you will need to pass #:pid-file, as explained below.

When pid-file is true, it must be the name of a PID file associated with the process being launched; the return value is the PID once that file has been created. If pid-file does not show up in less than pid-file-timeout seconds, the service is considered as failing to start.

When log-file is true, it names the file to which the service’s standard output and standard error are redirected. log-file is created if it does not exist, otherwise it is appended to.

The wesnoth source appears not to use the NAMESPACES feature of the containerized-version, so we'll stick with make-forkexec-constructor as is used in the syslogd example.

kmonad-shepherd-service

Let's put it all together:

(define (kmonad-shepherd-service kbd-path)
  (shepherd-service
   (documentation "Run the kmonad daemon (kmonad-daemon)." )
   (provision '(kmonad-daemon))
   (requirement '(udev user-processes))
   (start #~(make-forkexec-constructor
             (list #$(file-append kmonad "/bin/kmonad")
                   #$kbd-path "-l info")
             #:user "kmonad-daemon" #:group "kmonad-daemon"
             #:log-file "/var/log/kmonad.log"))
   (stop #~(make-kill-destructor))))

Making a module

Now let's package our code into a module. If the location where we keep local guix modules is ~/local-guix, then we can add our module at ~/local-guix/my/services/kmonad.scm as:

(define-module (my services kmonad)
  #:use-module (gnu services)
  #:use-module (gnu services shepherd)
  #:use-module (gnu packages haskell-apps)
  #:use-module (gnu system shadow)
  #:use-module (guix gexp)
  #:export (kmonad-service-type))

(define %kmonad-daemon-accounts
  (list (user-account
         (name "kmonad-daemon")
         (group "kmonad-daemon")
         (system? #t)
         (comment "kmonad daemon user")
         (home-directory "/var/empty")
         (shell (file-append shadow "/sbin/nologin")))
        (user-group
         (name "kmonad-daemon")
         (system? #t))))

(define (kmonad-shepherd-service kbd-path)
  (shepherd-service
   (documentation "Run the kmonad daemon (kmonad-daemon)." )
   (provision '(kmonad-daemon))
   (requirement '(udev user-processes))
   (start #~(make-forkexec-constructor
             (list #$(file-append kmonad "/bin/kmonad")
                   #$kbd-path "-l info")
             #:user "kmonad-daemon" #:group "kmonad-daemon"
             #:log-file "/var/log/kmonad.log"))
   (stop #~(make-kill-destructor))))

(define kmonad-service-type
  (service-type
   (name 'kmonad)
   (description
    "Run kmonad as a daemon.")
   (extensions
    (list (service-extension account-service-type
                             (const %kmonad-daemon-accounts))
          (service-extension shepherd-root-service-type
                             (compose list kmonad-shepherd-service))))))

Using kmonad-service-type

Now adding a kmonad service is straightforward. Let's add some other kmonad-specific configuration to our system while we're at it:

(use-modules (my services kmonad))
;; more modules

(operating-system
  (users
    (append (list (user-account
                    (supplementary-groups
                      '("input" ;; needed by kmonad
                        ;; more groups
                       ))
                    ;; more fields
                  )
                  ;; more users
            )
            %base-user-accounts))
  (packages
    (append (list
             "kmonad"
             ;; more packages
            )
            %base-packages))
  (services
    (append (list (service kmonad-service-type "/path/to/config.kbd")
                  ;; more services
            )
            (modify-services %desktop-services ;; needed to add kmonad udev rules
              (udev-service-type config =>
                (udev-configuration (inherit config)
                  (rules (cons kmonad
                    (udev-configuration-rules config))))))))
  ;; more fields
)

And reconfigure:

sudo guix system -L ~/local-guix reconfigure /path/to/config.scm

A simpler daemon?

I think it would be possible to add kmonad as a simple-service on top of shepherd-root-service-type. After looking at the simple-service source, we can try:

(simple-service 'kmonad-service shepherd-root-service-type
                (list (shepherd-service
                       (documentation "Run the kmonad daemon (kmonad-daemon)." )
                       (provision '(kmonad-daemon))
                       (requirement '(udev user-processes))
                       (start #~(make-forkexec-constructor
                                 (list #$(file-append kmonad "/bin/kmonad")
                                       #$kbd-path "-l info")
                                 #:log-file "/var/log/kmonad.log"))
                       (stop #~(make-kill-destructor)))))

References

hi