;;; GNU Guix --- Functional package management for GNU
;;; Copyright © 2013, 2014, 2015 Ludovic Courtès <ludo@gnu.org>
;;;
;;; 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 dmd)
  #:use-module (guix ui)
  #: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 (gnu services)
  #:use-module (gnu packages admin)
  #:use-module (ice-9 match)
  #:use-module (ice-9 vlist)
  #:use-module (srfi srfi-1)
  #:use-module (srfi srfi-26)
  #:use-module (srfi srfi-34)
  #:use-module (srfi srfi-35)
  #:export (dmd-root-service-type
            %dmd-root-service
            dmd-service-type

            dmd-service
            dmd-service?
            dmd-service-documentation
            dmd-service-provision
            dmd-service-requirement
            dmd-service-respawn?
            dmd-service-start
            dmd-service-stop
            dmd-service-auto-start?
            dmd-service-modules
            dmd-service-imported-modules

            %default-imported-modules
            %default-modules

            dmd-service-back-edges))

;;; Commentary:
;;;
;;; Instantiating system services as a dmd configuration file.
;;;
;;; Code:


(define (dmd-boot-gexp services)
  (mlet %store-monad ((dmd-conf (dmd-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 dmd.
                (execl (string-append #$dmd "/bin/dmd")
                       "dmd" "--config" #$dmd-conf)))))

(define dmd-root-service-type
  (service-type
   (name 'dmd-root)
   ;; Extending the root dmd service (aka. PID 1) happens by concatenating the
   ;; list of services provided by the extensions.
   (compose concatenate)
   (extend append)
   (extensions (list (service-extension boot-service-type dmd-boot-gexp)))))

(define %dmd-root-service
  ;; The root dmd service, aka. PID 1.  Its parameter is a list of
  ;; <dmd-service> objects.
  (service dmd-root-service-type '()))

(define-syntax-rule (dmd-service-type service-name proc)
  "Return a <service-type> denoting a simple dmd service--i.e., the type for a
service that extends DMD-ROOT-SERVICE-TYPE and nothing else."
  (service-type
   (name service-name)
   (extensions
    (list (service-extension dmd-root-service-type
                             (compose list proc))))))

(define %default-imported-modules
  ;; Default set of modules imported for a service's consumption.
  '((guix build utils)
    (guix build syscalls)))

(define %default-modules
  ;; Default set of modules visible in a service's file.
  `((dmd service)
    (oop goops)
    (guix build utils)
    (guix build syscalls)))

(define-record-type* <dmd-service>
  dmd-service make-dmd-service
  dmd-service?
  (documentation dmd-service-documentation        ;string
                 (default "[No documentation.]"))
  (provision     dmd-service-provision)           ;list of symbols
  (requirement   dmd-service-requirement          ;list of symbols
                 (default '()))
  (respawn?      dmd-service-respawn?             ;Boolean
                 (default #t))
  (start         dmd-service-start)               ;g-expression (procedure)
  (stop          dmd-service-stop                 ;g-expression (procedure)
                 (default #~(const #f)))
  (auto-start?   dmd-service-auto-start?          ;Boolean
                 (default #t))
  (modules       dmd-service-modules              ;list of module names
                 (default %default-modules))
  (imported-modules dmd-service-imported-modules  ;list of module names
                    (default %default-imported-modules)))


(define (assert-valid-graph services)
  "Raise an error if SERVICES does not define a valid dmd service graph, for
instance if a service requires a nonexistent service, or if more than one
service uses a given name.

These are constraints that dmd's 'register-service' verifies but we'd better
verify them here statically than wait until PID 1 halts with an assertion
failure."
  (define provisions
    ;; The set of provisions (symbols).  Bail out if a symbol is given more
    ;; than once.
    (fold (lambda (service set)
            (define (assert-unique symbol)
              (when (set-contains? set symbol)
                (raise (condition
                        (&message
                         (message
                          (format #f (_ "service '~a' provided more than once")
                                  symbol)))))))

            (for-each assert-unique (dmd-service-provision service))
            (fold set-insert set (dmd-service-provision service)))
          (setq 'dmd)
          services))

  (define (assert-satisfied-requirements service)
    ;; Bail out if the requirements of SERVICE aren't satisfied.
    (for-each (lambda (requirement)
                (unless (set-contains? provisions requirement)
                  (raise (condition
                          (&message
                           (message
                            (format #f (_ "service '~a' requires '~a', \
which is undefined")
                                    (match (dmd-service-provision service)
                                      ((head . _) head)
                                      (_          service))
                                    requirement)))))))
              (dmd-service-requirement service)))

  (for-each assert-satisfied-requirements services))

(define (dmd-service-file-name service)
  "Return the file name where the initialization code for SERVICE is to be
stored."
  (let ((provisions (string-join (map symbol->string
                                      (dmd-service-provision service)))))
    (string-append "dmd-"
                   (string-map (match-lambda
                                 (#\/ #\-)
                                 (chr chr))
                               provisions)
                   ".scm")))

(define (dmd-service-file service)
  "Return a file defining SERVICE."
  (gexp->file (dmd-service-file-name service)
              #~(begin
                  (use-modules #$@(dmd-service-modules service))

                  (make <service>
                    #:docstring '#$(dmd-service-documentation service)
                    #:provides '#$(dmd-service-provision service)
                    #:requires '#$(dmd-service-requirement service)
                    #:respawn? '#$(dmd-service-respawn? service)
                    #:start #$(dmd-service-start service)
                    #:stop #$(dmd-service-stop service)))))

(define (dmd-configuration-file services)
  "Return the dmd configuration file for SERVICES."
  (define modules
    (delete-duplicates
     (append-map dmd-service-imported-modules services)))

  (assert-valid-graph services)

  (mlet %store-monad ((modules  (imported-modules modules))
                      (compiled (compiled-modules modules))
                      (files    (mapm %store-monad dmd-service-file services)))
    (define config
      #~(begin
          (eval-when (expand load eval)
            (set! %load-path (cons #$modules %load-path))
            (set! %load-compiled-path
              (cons #$compiled %load-compiled-path)))

          (use-modules (system repl error-handling))

          ;; Arrange to spawn a REPL if loading one of FILES fails.  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 start
                    '#$(append-map dmd-service-provision
                                   (filter dmd-service-auto-start?
                                           services)))))

    (gexp->file "dmd.conf" config)))

(define (dmd-service-back-edges services)
  "Return a procedure that, when given a <dmd-service> from SERVICES, returns
the list of <dmd-service> that depend on it."
  (define provision->service
    (let ((services (fold (lambda (service result)
                            (fold (cut vhash-consq <> service <>)
                                  result
                                  (dmd-service-provision service)))
                          vlist-null
                          services)))
      (lambda (name)
        (match (vhash-assq name services)
          ((_ . service) service)
          (#f            #f)))))

  (define edges
    (fold (lambda (service edges)
            (fold (lambda (requirement edges)
                    (vhash-consq (provision->service requirement) service
                                 edges))
                  edges
                  (dmd-service-requirement service)))
          vlist-null
          services))

  (lambda (service)
    (vhash-foldq* cons '() service edges)))

;;; dmd.scm ends here