civodul pushed a commit to branch wip-syslogd
in repository shepherd.
commit cda04452e6651057ea1e438e3c099b996c2dd10e
Author: Ludovic Courtès <[email protected]>
AuthorDate: Sat Jun 29 23:55:47 2024 +0200
DRAFT syslog service
DRAFT: Missing doc and system test.
* modules/shepherd/service/system-log.scm,
tests/services/system-log-internal.scm,
tests/services/system-log.sh: New files.
* Makefile.am (dist_servicesub_DATA): Add them.
(TESTS): Add it.
* modules/shepherd/logger.scm (open-log-file): Export.
---
Makefile.am | 5 +-
modules/shepherd/logger.scm | 4 +-
modules/shepherd/service/system-log.scm | 319 ++++++++++++++++++++++++++++++++
tests/services/system-log-internal.scm | 70 +++++++
tests/services/system-log.sh | 135 ++++++++++++++
5 files changed, 531 insertions(+), 2 deletions(-)
diff --git a/Makefile.am b/Makefile.am
index 87a3010..e8fee47 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -52,6 +52,7 @@ dist_servicesub_DATA = \
modules/shepherd/service/log-rotation.scm \
modules/shepherd/service/monitoring.scm \
modules/shepherd/service/repl.scm \
+ modules/shepherd/service/system-log.scm \
modules/shepherd/service/timer.scm
shepherdgosubdir = $(guileobjectdir)/shepherd
@@ -287,7 +288,9 @@ TESTS = \
tests/services/timer.sh \
tests/services/timer-events.scm \
tests/services/log-rotation.sh \
- tests/services/log-rotation-internal.scm
+ tests/services/log-rotation-internal.scm \
+ tests/services/system-log.sh \
+ tests/services/system-log-internal.scm
TEST_EXTENSIONS = .sh .scm
EXTRA_DIST += $(TESTS)
diff --git a/modules/shepherd/logger.scm b/modules/shepherd/logger.scm
index 68abf5d..c0f7a11 100644
--- a/modules/shepherd/logger.scm
+++ b/modules/shepherd/logger.scm
@@ -39,7 +39,9 @@
logger-recent-messages
logger-file
- rotate-log-file))
+ rotate-log-file
+
+ open-log-file))
(define default-log-history-size
;; Number of lines of service log kept in memory by default.
diff --git a/modules/shepherd/service/system-log.scm
b/modules/shepherd/service/system-log.scm
new file mode 100644
index 0000000..f9e1be0
--- /dev/null
+++ b/modules/shepherd/service/system-log.scm
@@ -0,0 +1,319 @@
+;; system-log.scm -- Reading from the "system log" or "syslog".
+;; Copyright (C) 2024 Ludovic Courtès <[email protected]>
+;;
+;; This file is part of the GNU Shepherd.
+;;
+;; The GNU Shepherd 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.
+;;
+;; The GNU Shepherd 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 the GNU Shepherd. If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (shepherd service system-log)
+ #:use-module (fibers)
+ #:use-module (shepherd service)
+ #:use-module (shepherd support)
+ #:autoload (shepherd config) (%localstatedir)
+ #:autoload (shepherd logger) (open-log-file)
+ #:autoload (shepherd comm) (system-log-file)
+ #:use-module (srfi srfi-9)
+ #:use-module (srfi srfi-71)
+ #:use-module (ice-9 match)
+ #:use-module (ice-9 regex)
+ #:autoload (ice-9 rdelim) (read-line)
+ #:use-module (ice-9 vlist)
+ #:use-module (rnrs io ports)
+ #:use-module (fibers operations)
+ #:use-module (fibers channels)
+ #:autoload (fibers io-wakeup) (wait-until-port-readable-operation)
+ #:export (system-log-message?
+ system-log-message-facility
+ system-log-message-priority
+ system-log-message-pid
+ system-log-message-sender
+ system-log-message-content
+
+ system-log-priority
+ system-log-facility
+
+ read-system-log-message
+
+ system-log-service))
+
+;; Message sent to the system log (minus its timestamp).
+(define-record-type <system-log-message>
+ (system-log-message priority+facility sender pid content)
+ system-log-message?
+ (priority+facility system-log-message-priority+facility)
+ (sender system-log-message-sender)
+ (pid system-log-message-pid)
+ (content system-log-message-content))
+
+(define-syntax define-enumerate-type
+ (syntax-rules ()
+ ((_ name->int (name id) ...)
+ (define-syntax name->int
+ (syntax-rules (name ...)
+ ((_ name) id) ...)))))
+
+;; From glibc's <syslog.h>.
+
+(define-enumerate-type system-log-priority
+ (emergency 0)
+ (alert 1)
+ (critical 2)
+ (error 3)
+ (warning 4)
+ (notice 5)
+ (info 6)
+ (debug 7))
+
+(define-enumerate-type system-log-facility
+ (kernel 0)
+ (user (ash 1 3))
+ (mail (ash 2 3))
+ (daemon (ash 3 3))
+ (authorization (ash 4 3))
+ (syslogd (ash 5 3))
+ (lpr (ash 6 3))
+ (news (ash 7 3))
+ (uucp (ash 8 3))
+ (cron (ash 9 3))
+ (authorization/private (ash 10 3))
+ (ftp (ash 11 3))
+ (local0 (ash 12 3))
+ (local1 (ash 13 3))
+ (local2 (ash 14 3))
+ (local3 (ash 15 3))
+ (local4 (ash 16 3))
+ (local5 (ash 17 3))
+ (local6 (ash 18 3))
+ (local7 (ash 19 3)))
+
+(define %system-log-facility-mask #x03f8)
+(define %system-log-priority-mask #x0007)
+
+(define (system-log-message-facility message)
+ "Return the facility @var{message} originates from."
+ (logand (system-log-message-priority+facility message)
+ %system-log-facility-mask))
+
+(define (system-log-message-priority message)
+ "Return the priority of @var{message}."
+ (logand (system-log-message-priority+facility message)
+ %system-log-priority-mask))
+
+(define %system-log-message-rx
+ ;; Regexp matching system log messages. Example:
+ ;; <29>Jun 22 16:41:30 wpa_supplicant[303]: whatever
+ (make-regexp "<([0-9]+)> ?([[:alpha:]]{3} [0-9]+ [0-9]+:[0-9]+:[0-9]+ )?\
+([[:graph:]]+): (.*)"))
+
+(define %process+pid-rx
+ ;; Regexp matching "process[123]".
+ (make-regexp "([[:graph:]]+)\\[([[:digit:]]+)\\]"))
+
+(define %default-priority (system-log-priority notice))
+(define %default-facility (system-log-facility user))
+
+(define (read-system-log-message port)
+ (match (read-line port)
+ ((? eof-object? eof)
+ eof)
+ (line
+ (match (false-if-exception (regexp-exec %system-log-message-rx line))
+ (#f
+ (system-log-message (logior %default-facility %default-priority)
+ #f #f line))
+ (m
+ (let* ((facility+priority (string->number (match:substring m 1)))
+ (process+pid (match:substring m 3))
+ (process pid (match (regexp-exec %process+pid-rx
+ process+pid)
+ (#f (values process+pid #f))
+ (m (values (match:substring m 1)
+ (string->number
+ (match:substring m 2)))))))
+ (system-log-message facility+priority
+ process pid
+ (match:substring m 4))))))))
+
+(define (read-client-log client dispatcher)
+ (let loop ()
+ (match (read-system-log-message client)
+ ((? eof-object?)
+ #t)
+ (#f
+ (loop))
+ (message
+ (put-message dispatcher message)
+ (loop)))))
+
+(define (spawn-child-service client parent id dispatcher)
+ "Register and start a new service that reads messages from @var{client}, a
+socket, passing them to @var{dispatcher} for actual logging. @var{parent} and
+@var{id} are used to generate the service's name."
+ (letrec* ((name (string->symbol
+ (string-append (symbol->string parent) "-client-"
+ (number->string id))))
+ (child (service
+ (list name)
+ #:transient? #t
+ #:respawn? #f
+ #:requirement (list parent)
+ #:start (lambda ()
+ (spawn-fiber
+ (lambda ()
+ (read-client-log client dispatcher)))
+ client)
+ #:stop (lambda (client)
+ (close-port client)
+ #f))))
+ (register-services (list child))
+ (start-service child)))
+
+(define (accept/get-message socket channel)
+ "Wait for connections on @var{socket} and for messages on @var{channel}.
+Return either the return value of the @code{accept} procedure, for incoming
+connections, or the message received on @var{channel} wrapped in a list whose
+first element is @code{'message}."
+ (perform-operation
+ (choice-operation
+ (wrap-operation (wait-until-port-readable-operation socket)
+ (lambda ()
+ (accept socket (logior SOCK_NONBLOCK SOCK_CLOEXEC))))
+ (wrap-operation (get-operation channel)
+ (lambda (message)
+ (list 'message message))))))
+
+(define* (open-socket file #:key (backlog 512))
+ "Open a socket listening on @var{file} and return it."
+ (let ((socket (socket AF_UNIX
+ (logior SOCK_STREAM SOCK_NONBLOCK SOCK_CLOEXEC)
+ 0)))
+ (catch 'system-error
+ (lambda ()
+ (bind socket AF_UNIX file))
+ (lambda args
+ (if (= (system-error-errno args) EADDRINUSE)
+ (begin
+ (false-if-exception (delete-file file))
+ (bind socket AF_UNIX file))
+ (apply throw args))))
+ (listen socket backlog)
+ socket))
+
+(define (run-system-log channel name socket dispatcher)
+ "Run the system log, where @var{name} is its service name. Listen for
+connections on @var{socket} and for control messages on @var{channel}. Send
+incoming system log messages to @var{dispatcher}."
+ (let loop ((id 0))
+ (match (accept/get-message socket channel)
+ (('message ('terminate reply))
+ (local-output (l10n "Terminating system log service."))
+ (close-port socket)
+ (put-message dispatcher `(terminate ,reply)))
+ ((port . client)
+ (local-output (l10n "New system log connection from ~s.")
+ client)
+ (spawn-child-service port name id dispatcher)
+ (loop (+ 1 id))))))
+
+(define (log-dispatcher channel log-file)
+ "Dispatch system log messages received on @var{channel} to log files. Call
+@var{log-file} for each system log message to determine the destination file
+name."
+ (define (log-line message output)
+ ;; Write MESSAGE to OUTPUT and return its timestamp.
+ (let* ((now (current-time))
+ (prefix (strftime default-logfile-date-format
+ (localtime now))))
+ ;; Avoid (ice-9 format) to reduce heap allocations.
+ (put-string output prefix)
+ (when (system-log-message-sender message)
+ (if (system-log-message-pid message)
+ (simple-format output "~a[~a]: "
+ (system-log-message-sender message)
+ (system-log-message-pid message))
+ (simple-format output "~a: "
+ (system-log-message-sender message))))
+ (put-string output (system-log-message-content message))
+ (newline output)
+ now))
+
+ (define default-log-file
+ (default-log-file-procedure))
+
+ (lambda ()
+ (let loop ((ports vlist-null))
+ (match (get-message channel)
+ ((? system-log-message? message)
+ (let ((file (or (false-if-exception (log-file message))
+ (default-log-file message))))
+ (match (vhash-assoc file ports)
+ (#f
+ (let ((port (open-log-file file)))
+ (log-line message port)
+ (loop (vhash-cons file port ports))))
+ ((_ . port)
+ (log-line message port)
+ (loop ports)))))
+ (('terminate reply)
+ (local-output (l10n "Closing ~a system log ports.")
+ (vlist-length ports))
+ (vhash-fold (lambda (file port _)
+ (close-port port)
+ #t)
+ #t
+ ports)
+ (put-message reply #t))))))
+
+(define (spawn-log-dispatcher log-file)
+ "Spawn the log dispatcher, responsible for writing system log messages to
+the file returned by @var{log-file} for each message."
+ (let ((channel (make-channel)))
+ (spawn-fiber (log-dispatcher channel log-file))
+ channel))
+
+(define (default-log-file-procedure)
+ (if (zero? (getuid))
+ (const (in-vicinity %localstatedir "log/syslog"))
+ (const (in-vicinity %user-log-dir "syslog"))))
+
+(define* (system-log-service #:optional (file (system-log-file))
+ #:key
+ (provision '(system-log syslogd))
+ (requirement '())
+ (log-file (default-log-file-procedure)))
+ "Return the system log service (@dfn{syslogd}) with the given
+@var{provision} and @var{requirement} (lists of symbols). The service accepts
+connections on @var{file}, a Unix-domain socket. Log messages are passed to
+@var{log-file}, a one-argument procedure that must return the name of the file
+to write to."
+ (define channel
+ (make-channel))
+
+ (service provision
+ #:requirement requirement
+ #:start (lambda ()
+ (let ((socket (open-socket file))
+ (dispatcher (spawn-log-dispatcher log-file)))
+ (spawn-fiber
+ (lambda ()
+ (run-system-log channel (car provision)
+ socket dispatcher)))
+ socket))
+ #:stop (lambda (socket)
+ (let ((reply (make-channel)))
+ (close-port socket)
+ (put-message channel `(terminate ,reply))
+ (get-message reply) ;wait for complete shutdown
+ #f))
+ #:respawn? #f))
diff --git a/tests/services/system-log-internal.scm
b/tests/services/system-log-internal.scm
new file mode 100644
index 0000000..2520f65
--- /dev/null
+++ b/tests/services/system-log-internal.scm
@@ -0,0 +1,70 @@
+;; GNU Shepherd --- Test the system log service.
+;; Copyright © 2024 Ludovic Courtès <[email protected]>
+;;
+;; This file is part of the GNU Shepherd.
+;;
+;; The GNU Shepherd 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.
+;;
+;; The GNU Shepherd 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 the GNU Shepherd. If not, see <http://www.gnu.org/licenses/>.
+
+(define-module (test-system-log-internal)
+ #:use-module (shepherd service system-log)
+ #:use-module (srfi srfi-64))
+
+(test-begin "system-log-internal")
+
+(test-equal "read-system-log-message, with PID"
+ (list (system-log-facility daemon)
+ (system-log-priority notice)
+ "wpa_supplicant" 303
+ "wlp0s20f0u2: CTRL-EVENT-BEACON-LOSS")
+ (call-with-input-string "<29>Jun 22 16:41:31 wpa_supplicant[303]: \
+wlp0s20f0u2: CTRL-EVENT-BEACON-LOSS"
+ (lambda (port)
+ (let ((message (read-system-log-message port)))
+ (list (system-log-message-facility message)
+ (system-log-message-priority message)
+ (system-log-message-sender message)
+ (system-log-message-pid message)
+ (system-log-message-content message))))))
+
+(test-equal "read-system-log-message, without PID"
+ (list (system-log-facility authorization/private)
+ (system-log-priority notice)
+ "sudo" #f
+ "ludo : TTY=pts/0 ; PWD=/home/ludo ; USER=root ; COMMAND=xyz")
+ (call-with-input-string "<85>Jun 29 10:45:55 \
+sudo: ludo : TTY=pts/0 ; PWD=/home/ludo ; USER=root ; COMMAND=xyz"
+ (lambda (port)
+ (let ((message (read-system-log-message port)))
+ (list (system-log-message-facility message)
+ (system-log-message-priority message)
+ (system-log-message-sender message)
+ (system-log-message-pid message)
+ (system-log-message-content message))))))
+
+(test-equal "read-system-log-message, raw"
+ (list (system-log-facility user)
+ (system-log-priority notice)
+ #f #f
+ "shepherd[1]: Stopping service tor...")
+ ;; This message lacks the usual syslog header.
+ (call-with-input-string "shepherd[1]: Stopping service tor...\n"
+ (lambda (port)
+ (let ((message (read-system-log-message port)))
+ (list (system-log-message-facility message)
+ (system-log-message-priority message)
+ (system-log-message-sender message)
+ (system-log-message-pid message)
+ (system-log-message-content message))))))
+
+(test-end)
diff --git a/tests/services/system-log.sh b/tests/services/system-log.sh
new file mode 100644
index 0000000..7f388e1
--- /dev/null
+++ b/tests/services/system-log.sh
@@ -0,0 +1,135 @@
+# GNU Shepherd --- Test system logging service (syslog).
+# Copyright © 2024 Ludovic Courtès <[email protected]>
+#
+# This file is part of the GNU Shepherd.
+#
+# The GNU Shepherd 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.
+#
+# The GNU Shepherd 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 the GNU Shepherd. If not, see <http://www.gnu.org/licenses/>.
+
+shepherd --version
+herd --version
+
+socket="t-socket-$$"
+conf="t-conf-$$"
+log="t-log-$$"
+pid="t-pid-$$"
+logger="$PWD/t-syslog-logger-$$.scm"
+syslog_file="$PWD/t-syslog-$$"
+syslog_auth_file="$PWD/t-syslog-auth-$$"
+syslog_debug_file="$PWD/t-syslog-debug-$$"
+syslog_socket="$PWD/t-syslog-socket-$$"
+
+herd="herd -s $socket"
+
+trap "cat $log || true;
+ rm -f $socket $conf $log $logger $syslog_socket;
+ rm -f $syslog_file $syslog_auth_file $syslog_debug_file;
+ test -f $pid && kill \`cat $pid\` || true; rm -f $pid" EXIT
+
+cat > "$conf" <<EOF
+(use-modules (shepherd service system-log))
+
+(define (log-file message)
+ (pk 'log-file->
+ (cond ((= (system-log-message-facility message)
+ (system-log-facility authorization/private))
+ "$syslog_auth_file")
+ ((= (system-log-message-priority message)
+ (system-log-priority debug))
+ "$syslog_debug_file")
+ (else
+ "$syslog_file"))))
+
+(register-services
+ (list (system-log-service "$syslog_socket"
+ #:log-file log-file)
+ (service
+ '(logger)
+ #:requirement '(syslogd)
+ #:start (make-forkexec-constructor '("$logger"))
+ #:stop (make-kill-destructor)
+ #:respawn? #f)))
+EOF
+
+cat > "$logger" <<EOF
+#!$GUILE --no-auto-compile
+!#
+
+(display "starting logger\n")
+(let ((sock (socket AF_UNIX SOCK_STREAM 0)))
+ (connect sock AF_UNIX "$syslog_socket")
+ (display "<86> Jun 29 10:45:54 sudo: pam_unix(sudo:session): session opened
for user root\n" sock)
+ (display "<85>Jul 14 12:32:50 sudo: pam_unix(sudo:auth): authentication
failure; logname= uid=1000\n" sock)
+ (display "<81>Jul 14 12:33:01 sudo: ludo : 3 incorrect password attempts ;
TTY=pts/34\n" sock)
+ (display "<31>Jul 14 12:18:28 ntpd[427]: new interface(s) found: waking up
resolver\n" sock)
+ (display "<38>Jul 14 12:47:33 elogind[286]: Power key pressed short.\n" sock)
+ (display "<30>Jul 14 12:47:33 NetworkManager[319]: <info> [1720954053.6685]
manager: sleep: sleep requested\n" sock))
+EOF
+
+chmod +x "$logger"
+
+file_descriptor_count ()
+{
+ ls -l /proc/"$(cat $pid)"/fd/[0-9]* | wc -l
+}
+
+rm -f "$pid"
+shepherd -I -s "$socket" -c "$conf" -l "$log" --pid="$pid" &
+
+# Wait till it's ready.
+while ! test -f "$pid" ; do sleep 0.3 ; done
+
+# Trigger startup of the finalizer thread, which creates a couple of pipes.
+# That way, those extra file descriptors won't influence the comparison with
+# INITIAL_FD_COUNT done at the end.
+$herd eval root '(gc)'
+
+initial_fd_count=$(file_descriptor_count)
+
+$herd start logger
+until $herd status logger | grep stopped; do sleep 0.3; done
+
+grep "starting logger" "$log"
+grep "sudo:.* session opened" "$syslog_auth_file"
+grep "sudo:.* authentication failure" "$syslog_auth_file"
+grep "3 incorrect password attempts" "$syslog_auth_file"
+grep "ntpd\[427\]: new interface" "$syslog_debug_file"
+grep "elogind\[286\]: Power key pressed short" "$syslog_file"
+grep "NetworkManager\[319\]: .*sleep" "$syslog_file"
+
+for file in "$syslog_file" "$syslog_auth_file" "$syslog_debug_file"
+do
+ cat "$file"
+done
+
+$herd stop system-log
+$herd eval root '(gc)'
+
+if test -d "/proc/$$/fd" # GNU/Hurd lacks /proc/*/fd.
+then
+ # At this point, shepherd should be back to INITIAL_FD_COUNT.
+ ls -l "/proc/$(cat $pid)/fd"
+ test $(file_descriptor_count) -le $initial_fd_count
+fi
+
+# Remove the logs, start it again, and ensure it's working.
+rm -f "$syslog_file" "$syslog_auth_file" "$syslog_debug_file"
+$herd enable logger
+$herd start logger
+until $herd status logger | grep stopped; do sleep 0.3; done
+for file in "$syslog_file" "$syslog_auth_file" "$syslog_debug_file"
+do
+ test -f "$file"
+done
+
+$herd stop root