* gnu/services/herd.scm (current-service): New procedure, mostly reusing the existing current-services. (current-services): Implement in terms of the above procedure.
		
			
				
	
	
		
			357 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			Scheme
		
	
	
	
	
	
			
		
		
	
	
			357 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			Scheme
		
	
	
	
	
	
;;; GNU Guix --- Functional package management for GNU
 | 
						|
;;; Copyright © 2016-2019, 2022-2023 Ludovic Courtès <ludo@gnu.org>
 | 
						|
;;; Copyright © 2017, 2020 Mathieu Othacehe <m.othacehe@gmail.com>
 | 
						|
;;; Copyright © 2023 Maxim Cournoyer <maxim.cournoyer@gmail.com>
 | 
						|
;;;
 | 
						|
;;; 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 herd)
 | 
						|
  #:use-module (srfi srfi-1)
 | 
						|
  #:use-module (srfi srfi-9)
 | 
						|
  #:use-module (srfi srfi-9 gnu)
 | 
						|
  #:use-module (srfi srfi-11)
 | 
						|
  #:use-module (srfi srfi-34)
 | 
						|
  #:use-module (srfi srfi-35)
 | 
						|
  #:use-module (ice-9 match)
 | 
						|
  #:export (%shepherd-socket-file
 | 
						|
            shepherd-message-port
 | 
						|
 | 
						|
            shepherd-error?
 | 
						|
            service-not-found-error?
 | 
						|
            service-not-found-error-service
 | 
						|
            action-not-found-error?
 | 
						|
            action-not-found-error-service
 | 
						|
            action-not-found-error-action
 | 
						|
            action-exception-error?
 | 
						|
            action-exception-error-service
 | 
						|
            action-exception-error-action
 | 
						|
            action-exception-error-key
 | 
						|
            action-exception-error-arguments
 | 
						|
            unknown-shepherd-error?
 | 
						|
            unknown-shepherd-error-sexp
 | 
						|
 | 
						|
            live-service
 | 
						|
            live-service?
 | 
						|
            live-service-provision
 | 
						|
            live-service-requirement
 | 
						|
            live-service-running
 | 
						|
            live-service-transient?
 | 
						|
            live-service-canonical-name
 | 
						|
 | 
						|
            with-shepherd-action
 | 
						|
            current-service
 | 
						|
            current-services
 | 
						|
            unload-services
 | 
						|
            unload-service
 | 
						|
            load-services
 | 
						|
            load-services/safe
 | 
						|
            start-service
 | 
						|
            stop-service
 | 
						|
            restart-service
 | 
						|
            wait-for-service))
 | 
						|
 | 
						|
;;; Commentary:
 | 
						|
;;;
 | 
						|
;;; This module provides an interface to the GNU Shepherd, similar to the
 | 
						|
;;; 'herd' command.  Essentially it implements a subset of the (shepherd comm)
 | 
						|
;;; module, but focusing only on the parts relevant to 'guix system
 | 
						|
;;; reconfigure'.
 | 
						|
;;;
 | 
						|
;;; Code:
 | 
						|
 | 
						|
(define %shepherd-socket-file
 | 
						|
  (make-parameter "/var/run/shepherd/socket"))
 | 
						|
 | 
						|
(define* (open-connection #:optional (file (%shepherd-socket-file)))
 | 
						|
  "Open a connection to the daemon, using the Unix-domain socket at FILE, and
 | 
						|
return the socket."
 | 
						|
  ;; The protocol is sexp-based and UTF-8-encoded.
 | 
						|
  (with-fluids ((%default-port-encoding "UTF-8"))
 | 
						|
    (let ((sock    (socket PF_UNIX SOCK_STREAM 0))
 | 
						|
          (address (make-socket-address PF_UNIX file)))
 | 
						|
      (catch 'system-error
 | 
						|
        (lambda ()
 | 
						|
          (connect sock address)
 | 
						|
          (setvbuf sock 'block 1024)
 | 
						|
          sock)
 | 
						|
        (lambda args
 | 
						|
          (close-port sock)
 | 
						|
          (apply throw args))))))
 | 
						|
 | 
						|
(define-syntax-rule (with-shepherd connection body ...)
 | 
						|
  "Evaluate BODY... with CONNECTION bound to an open socket to PID 1."
 | 
						|
  (let ((connection (open-connection)))
 | 
						|
    (dynamic-wind
 | 
						|
      (const #t)
 | 
						|
      (lambda ()
 | 
						|
        body ...)
 | 
						|
      (lambda ()
 | 
						|
        (close-port connection)))))
 | 
						|
 | 
						|
(define-condition-type &shepherd-error &error
 | 
						|
  shepherd-error?)
 | 
						|
 | 
						|
(define-condition-type &service-not-found-error &shepherd-error
 | 
						|
  service-not-found-error?
 | 
						|
  (service service-not-found-error-service))
 | 
						|
 | 
						|
(define-condition-type &action-not-found-error &shepherd-error
 | 
						|
  action-not-found-error?
 | 
						|
  (service action-not-found-error-service)
 | 
						|
  (action  action-not-found-error-action))
 | 
						|
 | 
						|
(define-condition-type &action-exception-error &shepherd-error
 | 
						|
  action-exception-error?
 | 
						|
  (service action-exception-error-service)
 | 
						|
  (action  action-exception-error-action)
 | 
						|
  (key     action-exception-error-key)
 | 
						|
  (args    action-exception-error-arguments))
 | 
						|
 | 
						|
(define-condition-type &unknown-shepherd-error &shepherd-error
 | 
						|
  unknown-shepherd-error?
 | 
						|
  (sexp   unknown-shepherd-error-sexp))
 | 
						|
 | 
						|
(define (raise-shepherd-error error)
 | 
						|
  "Raise an error condition corresponding to ERROR, an sexp received by a
 | 
						|
shepherd client in reply to COMMAND, a command object.  Return #t if ERROR
 | 
						|
does not denote an error."
 | 
						|
  (match error
 | 
						|
    (('error ('version 0 x ...) 'service-not-found service)
 | 
						|
     (raise (condition (&service-not-found-error
 | 
						|
                        (service service)))))
 | 
						|
    (('error ('version 0 x ...) 'action-not-found action service)
 | 
						|
     (raise (condition (&action-not-found-error
 | 
						|
                        (service service)
 | 
						|
                        (action action)))))
 | 
						|
    (('error ('version 0 x ...) 'action-exception action service
 | 
						|
             key (args ...))
 | 
						|
     (raise (condition (&action-exception-error
 | 
						|
                        (service service)
 | 
						|
                        (action action)
 | 
						|
                        (key key) (args args)))))
 | 
						|
    (('error . _)
 | 
						|
     (raise (condition (&unknown-shepherd-error (sexp error)))))
 | 
						|
    (#f                                           ;not an error
 | 
						|
     #t)))
 | 
						|
 | 
						|
(define shepherd-message-port
 | 
						|
  ;; Port where messages coming from shepherd are printed.
 | 
						|
  (make-parameter (current-error-port)))
 | 
						|
 | 
						|
(define (display-message message)
 | 
						|
  (format (shepherd-message-port) "shepherd: ~a~%" message))
 | 
						|
 | 
						|
(define* (invoke-action service action arguments cont)
 | 
						|
  "Invoke ACTION on SERVICE with ARGUMENTS.  On success, call CONT with the
 | 
						|
list of results (one result per instance with the name SERVICE).  Otherwise
 | 
						|
return #f."
 | 
						|
  (with-shepherd sock
 | 
						|
    (write `(shepherd-command (version 0)
 | 
						|
                              (action ,action)
 | 
						|
                              (service ,service)
 | 
						|
                              (arguments ,arguments)
 | 
						|
                              (directory ,(getcwd)))
 | 
						|
           sock)
 | 
						|
    (force-output sock)
 | 
						|
 | 
						|
    (match (read sock)
 | 
						|
      (('reply ('version 0 _ ...) ('result result) ('error #f)
 | 
						|
               ('messages messages))
 | 
						|
       (for-each display-message messages)
 | 
						|
       (cont result))
 | 
						|
      (('reply ('version 0 x ...) ('result y) ('error error)
 | 
						|
               ('messages messages))
 | 
						|
       (for-each display-message messages)
 | 
						|
       (raise-shepherd-error error)
 | 
						|
       #f)
 | 
						|
      (x
 | 
						|
       ;; invalid reply
 | 
						|
       #f))))
 | 
						|
 | 
						|
(define-syntax-rule (with-shepherd-action service (action args ...)
 | 
						|
                      result body ...)
 | 
						|
  "Invoke ACTION on SERVICE with the given ARGS, and evaluate BODY with RESULT
 | 
						|
bound to the action's result."
 | 
						|
  (invoke-action service action (list args ...)
 | 
						|
                 (lambda (result) body ...)))
 | 
						|
 | 
						|
(define-syntax alist-let*
 | 
						|
  (syntax-rules ()
 | 
						|
    "Bind the given KEYs in EXP to the corresponding items in ALIST.  ALIST
 | 
						|
is assumed to be a list of two-element tuples rather than a traditional list
 | 
						|
of pairs."
 | 
						|
    ((_ alist (key ...) exp ...)
 | 
						|
     (let ((key (and=> (assoc-ref alist 'key) car)) ...)
 | 
						|
       exp ...))))
 | 
						|
 | 
						|
;; Information about live Shepherd services.
 | 
						|
(define-record-type <live-service>
 | 
						|
  (live-service provision requirement transient? running)
 | 
						|
  live-service?
 | 
						|
  (provision    live-service-provision)           ;list of symbols
 | 
						|
  (requirement  live-service-requirement)         ;list of symbols
 | 
						|
  (transient?   live-service-transient?)          ;Boolean
 | 
						|
  (running      live-service-running))            ;#f | object
 | 
						|
 | 
						|
(define (live-service-canonical-name service)
 | 
						|
  "Return the 'canonical name' of SERVICE."
 | 
						|
  (first (live-service-provision service)))
 | 
						|
 | 
						|
(define (current-service name)
 | 
						|
  "Return the currently defined Shepherd service NAME, as a <live-service>
 | 
						|
object.  Return #f if the service could not be obtained.  As a special case,
 | 
						|
@code{(current-service 'root)} returns all the current services."
 | 
						|
  (define (process-services services)
 | 
						|
    (resolve-transients
 | 
						|
     (map (lambda (service)
 | 
						|
            (alist-let* service (provides requires running transient?)
 | 
						|
              ;; The Shepherd 0.9.0 would not provide 'transient?' in
 | 
						|
              ;; its status sexp.  Thus, when it's missing, query it
 | 
						|
              ;; via an "eval" request.
 | 
						|
              (live-service provides requires
 | 
						|
                            (if (sloppy-assq 'transient? service)
 | 
						|
                                transient?
 | 
						|
                                (and running *unspecified*))
 | 
						|
                            running)))
 | 
						|
          services)))
 | 
						|
 | 
						|
  (with-shepherd-action name ('status) results
 | 
						|
    ;; We get a list of results, one for each service with the name NAME.
 | 
						|
    ;; In practice there's only one such service though.
 | 
						|
    (match results
 | 
						|
      ((services _ ...)
 | 
						|
       (match services
 | 
						|
         ((('service ('version 0 _ ...) _ ...) ...)
 | 
						|
          ;; Summary of all services (when NAME is 'root or 'shepherd).
 | 
						|
          (process-services services))
 | 
						|
         (('service ('version 0 _ ...) _ ...) ;single service
 | 
						|
          (first (process-services (list services))))
 | 
						|
         (x
 | 
						|
          #f))))))                ;singleton
 | 
						|
 | 
						|
(define (current-services)
 | 
						|
  "Return the list of currently defined Shepherd services, represented as
 | 
						|
<live-service> objects.  Return #f if the list of services could not be
 | 
						|
obtained."
 | 
						|
  (current-service 'root))
 | 
						|
 | 
						|
(define (resolve-transients services)
 | 
						|
  "Resolve the subset of SERVICES whose 'transient?' field is undefined.  This
 | 
						|
is necessary to deal with Shepherd 0.9.0, which did not communicate whether a
 | 
						|
service is transient."
 | 
						|
  ;; All the fuss here is to make sure we make a single "eval root" request
 | 
						|
  ;; for all of SERVICES.
 | 
						|
  (let* ((unresolved (filter (compose unspecified? live-service-transient?)
 | 
						|
                             services))
 | 
						|
         (values     (or (and (pair? unresolved)
 | 
						|
                              (eval-there
 | 
						|
                               `(and (defined? 'transient?) ;shepherd >= 0.9.0
 | 
						|
                                     (map (compose transient? lookup-running)
 | 
						|
                                          ',(map (compose first
 | 
						|
                                                          live-service-provision)
 | 
						|
                                                 unresolved)))))
 | 
						|
                         (make-list (length unresolved) #f)))
 | 
						|
         (resolved   (map (lambda (unresolved transient?)
 | 
						|
                            (cons unresolved
 | 
						|
                                  (set-field unresolved
 | 
						|
                                             (live-service-transient?)
 | 
						|
                                             transient?)))
 | 
						|
                          unresolved values)))
 | 
						|
    (map (lambda (service)
 | 
						|
           (or (assq-ref resolved service) service))
 | 
						|
         services)))
 | 
						|
 | 
						|
(define (unload-service service)
 | 
						|
  "Unload SERVICE, a symbol name; return #t on success."
 | 
						|
  (with-shepherd-action 'root ('unload (symbol->string service)) result
 | 
						|
    (first result)))
 | 
						|
 | 
						|
(define (%load-file file)
 | 
						|
  "Load FILE in the Shepherd."
 | 
						|
  (with-shepherd-action 'root ('load file) result
 | 
						|
    (first result)))
 | 
						|
 | 
						|
(define (eval-there exp)
 | 
						|
  "Eval EXP in the Shepherd."
 | 
						|
  (with-shepherd-action 'root ('eval (object->string exp)) result
 | 
						|
    (first result)))
 | 
						|
 | 
						|
(define (load-services files)
 | 
						|
  "Load and register the services from FILES, where FILES contain code that
 | 
						|
returns a shepherd <service> object."
 | 
						|
  (eval-there `(register-services
 | 
						|
                ,@(map (lambda (file)
 | 
						|
                         `(primitive-load ,file))
 | 
						|
                       files))))
 | 
						|
 | 
						|
(define load-services/safe
 | 
						|
  ;; Deprecated.  It used to behave differently before service replacements
 | 
						|
  ;; were a thing.
 | 
						|
  load-services)
 | 
						|
 | 
						|
(define* (start-service name #:optional (arguments '()))
 | 
						|
  (invoke-action name 'start arguments
 | 
						|
                 (lambda (result)
 | 
						|
                   result)))
 | 
						|
 | 
						|
(define (stop-service name)
 | 
						|
  (with-shepherd-action name ('stop) result
 | 
						|
    result))
 | 
						|
 | 
						|
(define (restart-service name)
 | 
						|
  (with-shepherd-action name ('restart) result
 | 
						|
    result))
 | 
						|
 | 
						|
(define* (wait-for-service name #:key (timeout 20))
 | 
						|
  "Wait for the service providing NAME, a symbol, to be up and running, and
 | 
						|
return its \"running value\".  Give up after TIMEOUT seconds and raise a
 | 
						|
'&shepherd-error' exception.  Raise a '&service-not-found-error' exception
 | 
						|
when NAME is not found."
 | 
						|
  (define (relevant-service? service)
 | 
						|
    (memq name (live-service-provision service)))
 | 
						|
 | 
						|
  (define start
 | 
						|
    (car (gettimeofday)))
 | 
						|
 | 
						|
  ;; Note: As of Shepherd 0.9.1, we cannot just call the 'start' method and
 | 
						|
  ;; wait for it: it would spawn an additional elogind process.  Thus, poll.
 | 
						|
  (let loop ((attempts 0))
 | 
						|
    (define services
 | 
						|
      (current-services))
 | 
						|
 | 
						|
    (define now
 | 
						|
      (car (gettimeofday)))
 | 
						|
 | 
						|
    (when (>= (- now start) timeout)
 | 
						|
      (raise (condition (&shepherd-error))))      ;XXX: better exception?
 | 
						|
 | 
						|
    (match (find relevant-service? services)
 | 
						|
      (#f
 | 
						|
       (raise (condition (&service-not-found-error
 | 
						|
                          (service name)))))
 | 
						|
      (service
 | 
						|
       (or (live-service-running service)
 | 
						|
           (begin
 | 
						|
             (sleep 1)
 | 
						|
             (loop (+ attempts 1))))))))
 | 
						|
 | 
						|
;; Local Variables:
 | 
						|
;; eval: (put 'alist-let* 'scheme-indent-function 2)
 | 
						|
;; eval: (put 'with-shepherd 'scheme-indent-function 1)
 | 
						|
;; eval: (put 'with-shepherd-action 'scheme-indent-function 3)
 | 
						|
;; End:
 | 
						|
 | 
						|
;;; herd.scm ends here
 |