# GNU Shepherd --- Test system logging service (syslog).
# Copyright © 2024-2025 Ludovic Courtès <ludo@gnu.org>
#
# 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"
kmsg="$PWD/t-syslog-kmsg-$$"
syslog_file="$PWD/t-syslog-$$"
syslog_auth_file="$PWD/t-syslog-auth-$$"
syslog_debug_file="$PWD/t-syslog-debug-$$"
syslog_remote_file="$PWD/t-syslog-remote-$$"
syslog_socket="$PWD/t-syslog-socket-$$"

herd="herd -s $socket"

trap "zcat $log.* || true; cat $log || true;
      rm -f $socket $conf $log* $logger $kmsg $syslog_socket;
      rm -f $syslog_file* $syslog_auth_file* $syslog_debug_file* $syslog_remote_file*;
      test -f $pid && kill \`cat $pid\` || true; rm -f $pid" EXIT

cat > "$conf" <<EOF
(use-modules (shepherd service system-log)
             (shepherd service log-rotation)
             (shepherd service timer)
             (shepherd endpoints)
             (srfi srfi-19))

(define (message-destination message)
  (pk 'message-destination->
      (cond ((and=> (system-log-message-sender message)
                    (lambda (address)
                      ;; On the Hurd, 'sender' is set even for
                      ;; AF_UNIX connections.
                      (not (= AF_UNIX (sockaddr:fam address)))))
             (list "$syslog_remote_file"))
            ((= (system-log-message-facility message)
		(system-log-facility authorization/private))
	     (list "$syslog_auth_file"))
            ((= (system-log-message-facility message)
		(system-log-facility mail))
	     '())  ;too much mail: discard these messages
	    ((= (system-log-message-priority message)
		(system-log-priority debug))
	     (list "$syslog_debug_file" "$syslog_file"))
	    (else
	     (list "$syslog_file")))))

(define today
  (time-utc->date (current-time time-utc)))

(define past-month
  (if (= 1 (date-month today))
      12
      (- (date-month today) 1)))

(define %endpoints
  (list (endpoint (make-socket-address AF_UNIX "$syslog_socket")
                  #:style SOCK_DGRAM)
        (endpoint (make-socket-address AF_INET INADDR_LOOPBACK 9898)
                  #:style SOCK_DGRAM)))

(register-services
  (list (system-log-service %endpoints
                            #:message-destination message-destination
                            #:kernel-log-file "$kmsg")
        (log-rotation-service
          ;; Arrange so that it does not trigger automatically.
          (calendar-event #:months (list past-month))
          #:rotation-size-threshold 0)
        (service
          '(logger)
          #:requirement '(syslogd)
          #:start (make-forkexec-constructor '("$logger"))
          #:stop (make-kill-destructor)
          #:respawn? #f)))
EOF

cat > "$logger" <<EOF
#!$GUILE --no-auto-compile
!#
;; -*- coding: utf-8 -*-
(use-modules (rnrs bytevectors))

(display "starting logger\n")
(let ((sock (socket AF_UNIX SOCK_DGRAM 0)))
  (connect sock AF_UNIX "$syslog_socket")
  (set-port-encoding! sock "UTF-8")
  (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 "<15>Jul 14 12:18:28 utf8[42]: checking we can print a λ and a 😃\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)
  (display "<20>Jul 18 22:22:22 exim[42]: too much mail in your inbox\n" sock))

(let ((sock (socket AF_UNIX SOCK_DGRAM 0)))
  (connect sock AF_UNIX "$syslog_socket")
  (set-port-encoding! sock "ISO-8859-1")
  (display "<14>Feb 26 12:12:12 latin[1]: latin1 garbage: ça alors, étrange !\n" sock))

(let ((sock (socket AF_INET SOCK_DGRAM 0))
      (address (make-socket-address AF_INET INADDR_LOOPBACK 9898)))
  (sendto sock
          (string->utf8
           "<31>Aug  4 10:05:55 mtp-probe: checking bus 1, device 111: \"/sys/devices/pci0000:00/0000:00:14.0/usb1/1-8\"\n")
          address))
EOF

chmod +x "$logger"

cat > "$kmsg" <<EOF
<6>[370383.514474] usb 1-2: USB disconnect, device number 57
EOF

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 "ntpd\[427\]: new interface" "$syslog_file" # this one in both files
grep "a λ and a 😃" "$syslog_debug_file"
grep "a λ and a 😃" "$syslog_file" # in both files too
grep "elogind\[286\]: Power key pressed short" "$syslog_file"
grep "USB disconnect, device number 57" "$syslog_file"
grep "NetworkManager\[319\]: .*sleep" "$syslog_file"
grep "mtp-probe:" "$syslog_remote_file"
grep "latin1 garbage: .*alors.*trange" "$syslog_file"

test $(wc -l < "$syslog_auth_file") -eq 3
test $(wc -l < "$syslog_debug_file") -eq 2
test $(wc -l < "$syslog_remote_file") -eq 1
test $(wc -l < "$syslog_file") -eq 6

for file in "$syslog_file" "$syslog_auth_file" "$syslog_debug_file" \
			   "$syslog_remote_file"
do
    cat "$file"
done

# The 'status' command should display the socket and kernel log file.
$herd status system-log | grep "$syslog_socket"
$herd status system-log | grep "$kmsg"

for file in "$syslog_file" "$syslog_auth_file" "$syslog_debug_file" \
			   "$syslog_remote_file"
do
    $herd status system-log | grep "Log files: .*$file"
done

# Check the "Recent messages" part of 'herd status'.
$herd status system-log
$herd status system-log -n 12 | grep "sudo: pam_unix"
$herd status system-log -n 12 | grep "USB disconnect"
$herd status system-log -n 12 | grep "mtp-probe: "

# End of file has been reached on $kmsg so it should be closed and no longer
# be polled.
until grep "Closing .*$kmsg" "$log"; do sleep 0.2; done

# Ensure logs can be rotated.
$herd start log-rotation
$herd trigger log-rotation
for file in "$syslog_file" "$syslog_auth_file" "$syslog_debug_file" \
			   "$syslog_remote_file"
do
    $herd files log-rotation | grep "$file"

    # Rotation happens asynchronously so wait for a while.
    until test -f "$file.1.gz"; do sleep 0.2; done
    gunzip < "$file.1.gz"
    rm "$file.1.gz"
    test -f "$file"		# this one should have been recreated
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.
    # Since the logger's own ports are closed asynchronously, when the service
    # sends it the 'terminate message, retry a few times.
    i=0
    while test $i -lt 20
    do
	ls -l "/proc/$(cat $pid)/fd"
	if test $(file_descriptor_count) -le $initial_fd_count
	then
	    break
	else
	    sleep 0.5		# wait and retry
	    i=$(expr $i + 1)
	fi
    done
    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 system-log

# Check that an inaccessible #:kernel-log-file does not prevent 'system-log'
# from starting.

cat > "$conf" <<EOF
(use-modules (shepherd service system-log)
             (shepherd endpoints)
             (srfi srfi-19))

(define %endpoints
  (list (endpoint (make-socket-address AF_UNIX "$syslog_socket")
                  #:style SOCK_DGRAM)))

(register-services
  (list (system-log-service %endpoints
                            #:kernel-log-file "/does/not/exist")))
EOF

$herd load root "$conf"
$herd start system-log
$herd status system-log | grep "$syslog_socket"
if $herd status system-log | grep "/does/not/exist"; then false; else true; fi
grep "Dismissing kernel log.*/does/not/exist" "$log"

$herd stop root

# Local Variables:
# coding: utf-8
# End:
