X-Git-Url: https://git.hcoop.net/jackhill/guix/guix.git/blobdiff_plain/a5d78eb64bcb87440a0b3ff25eec5568df0bc47c..fe1cd098d2b83737e96f19438612291f5a9316e4:/gnu/services/shepherd.scm diff --git a/gnu/services/shepherd.scm b/gnu/services/shepherd.scm index 3cfca8574e..e14ceca231 100644 --- a/gnu/services/shepherd.scm +++ b/gnu/services/shepherd.scm @@ -1,5 +1,8 @@ ;;; GNU Guix --- Functional package management for GNU -;;; Copyright © 2013, 2014, 2015, 2016 Ludovic Courtès +;;; Copyright © 2013, 2014, 2015, 2016, 2018, 2019, 2020 Ludovic Courtès +;;; Copyright © 2017 Clément Lassieur +;;; Copyright © 2018 Carlo Zancanaro +;;; Copyright © 2020 Jan (janneke) Nieuwenhuizen ;;; ;;; This file is part of GNU Guix. ;;; @@ -21,10 +24,11 @@ #:use-module (guix sets) #:use-module (guix gexp) #:use-module (guix store) - #:use-module (guix monads) #:use-module (guix records) #:use-module (guix derivations) ;imported-modules, etc. + #:use-module (guix utils) #:use-module (gnu services) + #:use-module (gnu services herd) #:use-module (gnu packages admin) #:use-module (ice-9 match) #:use-module (ice-9 vlist) @@ -42,18 +46,28 @@ shepherd-service-provision shepherd-service-canonical-name shepherd-service-requirement + shepherd-service-one-shot? shepherd-service-respawn? shepherd-service-start shepherd-service-stop shepherd-service-auto-start? shepherd-service-modules + shepherd-action + shepherd-action? + shepherd-action-name + shepherd-action-documentation + shepherd-action-procedure + %default-modules shepherd-service-file shepherd-service-lookup-procedure - shepherd-service-back-edges)) + shepherd-service-back-edges + shepherd-service-upgrade + + user-processes-service-type)) ;;; Commentary: ;;; @@ -63,25 +77,25 @@ (define (shepherd-boot-gexp services) - (mlet %store-monad ((shepherd-conf (shepherd-configuration-file services))) - (return #~(begin - ;; Keep track of the booted system. - (false-if-exception (delete-file "/run/booted-system")) - (symlink (readlink "/run/current-system") - "/run/booted-system") - - ;; Close any remaining open file descriptors to be on the safe - ;; side. This must be the very last thing we do, because - ;; Guile has internal FDs such as 'sleep_pipe' that need to be - ;; alive. - (let loop ((fd 3)) - (when (< fd 1024) - (false-if-exception (close-fdes fd)) - (loop (+ 1 fd)))) - - ;; Start shepherd. - (execl (string-append #$shepherd "/bin/shepherd") - "shepherd" "--config" #$shepherd-conf))))) + #~(begin + ;; Keep track of the booted system. + (false-if-exception (delete-file "/run/booted-system")) + (symlink (readlink "/run/current-system") + "/run/booted-system") + + ;; Close any remaining open file descriptors to be on the safe + ;; side. This must be the very last thing we do, because + ;; Guile has internal FDs such as 'sleep_pipe' that need to be + ;; alive. + (let loop ((fd 3)) + (when (< fd 1024) + (false-if-exception (close-fdes fd)) + (loop (+ 1 fd)))) + + ;; Start shepherd. + (execl #$(file-append shepherd "/bin/shepherd") + "shepherd" "--config" + #$(shepherd-configuration-file services)))) (define shepherd-root-service-type (service-type @@ -93,21 +107,35 @@ (extensions (list (service-extension boot-service-type shepherd-boot-gexp) (service-extension profile-service-type - (const (list shepherd))))))) + (const (list shepherd))))) + (description + "Run the GNU Shepherd as PID 1---i.e., the operating system's first +process. The Shepherd takes care of managing services such as daemons by +ensuring they are started and stopped in the right order."))) (define %shepherd-root-service ;; The root shepherd service, aka. PID 1. Its parameter is a list of ;; objects. (service shepherd-root-service-type '())) -(define-syntax-rule (shepherd-service-type service-name proc) - "Return a denoting a simple shepherd service--i.e., the type -for a service that extends SHEPHERD-ROOT-SERVICE-TYPE and nothing else." - (service-type - (name service-name) - (extensions - (list (service-extension shepherd-root-service-type - (compose list proc)))))) +(define-syntax shepherd-service-type + (syntax-rules () + "Return a denoting a simple shepherd service--i.e., the type +for a service that extends SHEPHERD-ROOT-SERVICE-TYPE and nothing else. When +DEFAULT is given, use it as the service's default value." + ((_ service-name proc default) + (service-type + (name service-name) + (extensions + (list (service-extension shepherd-root-service-type + (compose list proc)))) + (default-value default))) + ((_ service-name proc) + (service-type + (name service-name) + (extensions + (list (service-extension shepherd-root-service-type + (compose list proc)))))))) (define %default-imported-modules ;; Default set of modules imported for a service's consumption. @@ -118,7 +146,7 @@ for a service that extends SHEPHERD-ROOT-SERVICE-TYPE and nothing else." ;; Default set of modules visible in a service's file. `((shepherd service) (oop goops) - (guix build utils) + ((guix build utils) #:hide (delete)) (guix build syscalls))) (define-record-type* @@ -129,16 +157,27 @@ for a service that extends SHEPHERD-ROOT-SERVICE-TYPE and nothing else." (provision shepherd-service-provision) ;list of symbols (requirement shepherd-service-requirement ;list of symbols (default '())) + (one-shot? shepherd-service-one-shot? ;Boolean + (default #f)) (respawn? shepherd-service-respawn? ;Boolean (default #t)) (start shepherd-service-start) ;g-expression (procedure) (stop shepherd-service-stop ;g-expression (procedure) (default #~(const #f))) + (actions shepherd-service-actions ;list of + (default '())) (auto-start? shepherd-service-auto-start? ;Boolean (default #t)) (modules shepherd-service-modules ;list of module names (default %default-modules))) +(define-record-type* + shepherd-action make-shepherd-action + shepherd-action? + (name shepherd-action-name) ;symbol + (procedure shepherd-action-procedure) ;gexp + (documentation shepherd-action-documentation)) ;string + (define (shepherd-service-canonical-name service) "Return the 'canonical name' of SERVICE." (first (shepherd-service-provision service))) @@ -160,7 +199,7 @@ assertion failure." (raise (condition (&message (message - (format #f (_ "service '~a' provided more than once") + (format #f (G_ "service '~a' provided more than once") symbol))))))) (for-each assert-unique (shepherd-service-provision service)) @@ -175,7 +214,7 @@ assertion failure." (raise (condition (&message (message - (format #f (_ "service '~a' requires '~a', \ + (format #f (G_ "service '~a' requires '~a', \ which is not provided by any service") (match (shepherd-service-provision service) ((head . _) head) @@ -193,62 +232,118 @@ stored." (string-append "shepherd-" (string-map (match-lambda (#\/ #\-) + (#\ #\-) (chr chr)) provisions) ".scm"))) (define (shepherd-service-file service) "Return a file defining SERVICE." - (gexp->file (shepherd-service-file-name service) - (with-imported-modules %default-imported-modules - #~(begin - (use-modules #$@(shepherd-service-modules service)) - - (make - #:docstring '#$(shepherd-service-documentation service) - #:provides '#$(shepherd-service-provision service) - #:requires '#$(shepherd-service-requirement service) - #:respawn? '#$(shepherd-service-respawn? service) - #:start #$(shepherd-service-start service) - #:stop #$(shepherd-service-stop service)))))) + (scheme-file (shepherd-service-file-name service) + (with-imported-modules %default-imported-modules + #~(begin + (use-modules #$@(shepherd-service-modules service)) + + (make + #:docstring '#$(shepherd-service-documentation service) + #:provides '#$(shepherd-service-provision service) + #:requires '#$(shepherd-service-requirement service) + + ;; The 'one-shot?' slot is new in Shepherd 0.6.0. + ;; Older versions ignore it. + #:one-shot? '#$(shepherd-service-one-shot? service) + + #:respawn? '#$(shepherd-service-respawn? service) + #:start #$(shepherd-service-start service) + #:stop #$(shepherd-service-stop service) + #:actions + (make-actions + #$@(map (match-lambda + (($ name proc doc) + #~(#$name #$doc #$proc))) + (shepherd-service-actions service)))))))) + +(define (scm->go file) + "Compile FILE, which contains code to be loaded by shepherd's config file, +and return the resulting '.go' file." + (let-system (system target) + (with-extensions (list shepherd) + (computed-file (string-append (basename (scheme-file-name file) ".scm") + ".go") + #~(begin + (use-modules (system base compile) + (system base target)) + + ;; Do the same as the Shepherd's 'load-in-user-module'. + (let ((env (make-fresh-user-module))) + (module-use! env (resolve-interface '(oop goops))) + (module-use! env (resolve-interface '(shepherd service))) + (with-target #$(or target #~%host-type) + (lambda _ + (compile-file #$file #:output-file #$output + #:env env))))) + + ;; It's faster to build locally than to download. + #:options '(#:local-build? #t + #:substitutable? #f))))) (define (shepherd-configuration-file services) "Return the shepherd configuration file for SERVICES." (assert-valid-graph services) - (mlet %store-monad ((files (mapm %store-monad - shepherd-service-file services))) + (let ((files (map shepherd-service-file services))) (define config #~(begin (use-modules (srfi srfi-34) (system repl error-handling)) + ;; Specify the default environment visible to all the services. + ;; Without this statement, all the environment variables of PID 1 + ;; are inherited by child services. + (default-environment-variables + '("PATH=/run/current-system/profile/bin")) + + ;; Booting off a DVD, especially on a slow machine, can make + ;; everything slow. Thus, increase the timeout compared to the + ;; default 5s in the Shepherd 0.7.0. See + ;; . + (default-pid-file-timeout 30) + ;; Arrange to spawn a REPL if something goes wrong. This is better ;; than a kernel panic. (call-with-error-handling (lambda () - (apply register-services (map primitive-load '#$files)) - - ;; guix-daemon 0.6 aborts if 'PATH' is undefined, so work around - ;; it. - (setenv "PATH" "/run/current-system/profile/bin") - - (format #t "starting services...~%") - (for-each (lambda (service) - ;; In the Shepherd 0.3 the 'start' method can raise - ;; '&action-runtime-error' if it fails, so protect - ;; against it. (XXX: 'action-runtime-error?' is not - ;; exported is 0.3, hence 'service-error?'.) - (guard (c ((service-error? c) - (format (current-error-port) - "failed to start service '~a'~%" - service))) - (start service))) - '#$(append-map shepherd-service-provision - (filter shepherd-service-auto-start? - services))))))) - - (gexp->file "shepherd.conf" config))) + (apply register-services + (parameterize ((current-warning-port + (%make-void-port "w"))) + (map load-compiled '#$(map scm->go files)))))) + + (format #t "starting services...~%") + (for-each (lambda (service) + ;; In the Shepherd 0.3 the 'start' method can raise + ;; '&action-runtime-error' if it fails, so protect + ;; against it. (XXX: 'action-runtime-error?' is not + ;; exported is 0.3, hence 'service-error?'.) + (guard (c ((service-error? c) + (format (current-error-port) + "failed to start service '~a'~%" + service))) + (start service))) + '#$(append-map shepherd-service-provision + (filter shepherd-service-auto-start? + services))) + + ;; Hang up stdin. At this point, we assume that 'start' methods + ;; that required user interaction on the console (e.g., + ;; 'cryptsetup open' invocations, post-fsck emergency REPL) have + ;; completed. User interaction becomes impossible after this + ;; call; this avoids situations where services wrongfully lead + ;; PID 1 to read from stdin (the console), which users may not + ;; have access to (see ). + (redirect-port (open-input-file "/dev/null") + (current-input-port)))) + + (scheme-file "shepherd.conf" config))) (define* (shepherd-service-lookup-procedure services #:optional @@ -268,11 +363,17 @@ procedure that takes a service and returns the list of symbols it provides." ((_ . service) service) (#f #f))))) -(define (shepherd-service-back-edges services) +(define* (shepherd-service-back-edges services + #:key + (provision shepherd-service-provision) + (requirement shepherd-service-requirement)) "Return a procedure that, when given a from SERVICES, -returns the list of that depend on it." +returns the list of that depend on it. + +Use PROVISION and REQUIREMENT as one-argument procedures that return the +symbols provided/required by a service." (define provision->service - (shepherd-service-lookup-procedure services)) + (shepherd-service-lookup-procedure services provision)) (define edges (fold (lambda (service edges) @@ -280,11 +381,173 @@ returns the list of that depend on it." (vhash-consq (provision->service requirement) service edges)) edges - (shepherd-service-requirement service))) + (requirement service))) vlist-null services)) (lambda (service) (vhash-foldq* cons '() service edges))) +(define (shepherd-service-upgrade live target) + "Return two values: the subset of LIVE (a list of ) that needs +to be unloaded, and the subset of TARGET (a list of ) that +need to be restarted to complete their upgrade." + (define (essential? service) + (memq (first (live-service-provision service)) + '(root shepherd))) + + (define lookup-target + (shepherd-service-lookup-procedure target + shepherd-service-provision)) + + (define lookup-live + (shepherd-service-lookup-procedure live + live-service-provision)) + + (define (running? service) + (and=> (lookup-live (shepherd-service-canonical-name service)) + live-service-running)) + + (define live-service-dependents + (shepherd-service-back-edges live + #:provision live-service-provision + #:requirement live-service-requirement)) + + (define (obsolete? service) + (match (lookup-target (first (live-service-provision service))) + (#f (every obsolete? (live-service-dependents service))) + (_ #f))) + + (define to-restart + ;; Restart services that are currently running. + (filter running? target)) + + (define to-unload + ;; Unload services that are no longer required. + (remove essential? (filter obsolete? live))) + + (values to-unload to-restart)) + + +;;; +;;; User processes. +;;; + +(define %do-not-kill-file + ;; Name of the file listing PIDs of processes that must survive when halting + ;; the system. Typical example is user-space file systems. + "/etc/shepherd/do-not-kill") + +(define (user-processes-shepherd-service requirements) + "Return the 'user-processes' Shepherd service with dependencies on +REQUIREMENTS (a list of service names). + +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." + (define grace-delay + ;; Delay after sending SIGTERM and before sending SIGKILL. + 4) + + (list (shepherd-service + (documentation "When stopped, terminate all user processes.") + (provision '(user-processes)) + (requirement requirements) + (start #~(const #t)) + (stop #~(lambda _ + (define (kill-except omit signal) + ;; Kill all the processes with SIGNAL except those listed + ;; in OMIT and the current process. + (let ((omit (cons (getpid) omit))) + (for-each (lambda (pid) + (unless (memv pid omit) + (false-if-exception + (kill pid signal)))) + (processes)))) + + (define omitted-pids + ;; List of PIDs that must not be killed. + (if (file-exists? #$%do-not-kill-file) + (map string->number + (call-with-input-file #$%do-not-kill-file + (compose string-tokenize + (@ (ice-9 rdelim) read-string)))) + '())) + + (define (now) + (car (gettimeofday))) + + (define (sleep* n) + ;; Really sleep N seconds. + ;; Work around . + (define start (now)) + (let loop ((elapsed 0)) + (when (> n elapsed) + (sleep (- n elapsed)) + (loop (- (now) start))))) + + (define lset= (@ (srfi srfi-1) lset=)) + + (display "sending all processes the TERM signal\n") + + (if (null? omitted-pids) + (begin + ;; Easy: terminate all of them. + (kill -1 SIGTERM) + (sleep* #$grace-delay) + (kill -1 SIGKILL)) + (begin + ;; Kill them all except OMITTED-PIDS. XXX: We would + ;; like to (kill -1 SIGSTOP) to get a fixed list of + ;; processes, like 'killall5' does, but that seems + ;; unreliable. + (kill-except omitted-pids SIGTERM) + (sleep* #$grace-delay) + (kill-except omitted-pids SIGKILL) + (delete-file #$%do-not-kill-file))) + + (let wait () + ;; Reap children, if any, so that we don't end up with + ;; zombies and enter an infinite loop. + (let reap-children () + (define result + (false-if-exception + (waitpid WAIT_ANY (if (null? omitted-pids) + 0 + WNOHANG)))) + + (when (and (pair? result) + (not (zero? (car result)))) + (reap-children))) + + (let ((pids (processes))) + (unless (lset= = pids (cons 1 omitted-pids)) + (format #t "waiting for process termination\ + (processes left: ~s)~%" + pids) + (sleep* 2) + (wait)))) + + (display "all processes have been terminated\n") + #f)) + (respawn? #f)))) + +(define user-processes-service-type + (service-type + (name 'user-processes) + (extensions (list (service-extension shepherd-root-service-type + user-processes-shepherd-service))) + (compose concatenate) + (extend append) + + ;; The value is the list of Shepherd services 'user-processes' depends on. + ;; Extensions can add new services to this list. + (default-value '()) + + (description "The @code{user-processes} service is responsible for +terminating all the processes so that the root file system can be re-mounted +read-only, just before rebooting/halting. Processes still running after a few +seconds after @code{SIGTERM} has been sent are terminated with +@code{SIGKILL}."))) + ;;; shepherd.scm ends here