# GNU Shepherd --- Test timed services.
# 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-$$"
service_pid="t-service-pid-$$"
service_log="t-service-log-$$"

herd="herd -s $socket"

trap "cat $log || true; rm -f $socket $conf $log $service_pid $service_log;
      test -f $pid && kill \`cat $pid\` || true; rm -f $pid" EXIT

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

(define endless
  "echo Started endless timer.; echo \$\$ > $PWD/$service_pid; sleep 500")

(define timers
  (list (service '(timer-with-command)
		 #:start (make-timer-constructor
			  (calendar-event #:seconds (iota 60))
			  (command '("sh" "-c" "echo Hi from \$PWD."))
                          #:log-file "$service_log")
		 #:stop (make-timer-destructor))
        (service '(timer-with-procedure)
                 #:start (make-timer-constructor
			  (calendar-event #:seconds (iota 60))
                          (lambda () (display "Hello from procedure.\n")))
                 #:stop (make-timer-destructor))
        (service '(timer-that-throws)
                 #:start (make-timer-constructor
			  (calendar-event #:seconds (iota 60))
                          (lambda () (display "Throwing!\n") (mkdir "/")))
                 #:stop (make-timer-destructor)
	         #:actions (list timer-trigger-action))
        (service '(endless-timer)
                 #:start (make-timer-constructor
                          (calendar-event #:seconds (iota 60))
                          (command (quasiquote ("sh" "-c" ,endless)))
                          #:wait-for-termination? #t)
                 #:stop (make-timer-destructor))
        (service '(timer-that-takes-too-long)
                 #:start (make-timer-constructor
                          (calendar-event
                            #:months
                            (if (<= (date-month (current-date)) 6)
                                '(12)
                                '(1)))
                          (command (quote ("sh" "-c" "echo Sleeping.; exec sleep 100")))
                          #:max-duration 1)
                 #:stop (make-timer-destructor)
                 #:actions (list timer-trigger-action))
        (service '(timer-that-resists)   ;ignores SIGTERM
                 #:start (make-timer-constructor
                          (calendar-event
                            #:months
                            (if (<= (date-month (current-date)) 6)
                                '(12)
                                '(1)))
                          (command (quote ("sh" "-c"
                                           "trap 'echo \$\$ eats SIGTERM' TERM; while true; do sleep 1; done")))
                          #:max-duration 1)
                 #:stop (make-timer-destructor)
                 #:actions (list timer-trigger-action))
        (service '(timer-with-two-occurrences)
                 #:start (make-timer-constructor
                          (calendar-event #:seconds (iota 60))
                          (lambda () (display "limited number of occurrences\n"))
                          #:occurrences 2)
                 #:stop (make-timer-destructor))
        (service '(timer-without-matching-events)
                 #:start (make-timer-constructor
                          (calendar-event #:seconds '())  ;broken event!
                          (lambda () (display "What?!\n")))
                 #:stop (make-timer-destructor))
        (service '(never-timer)
                 #:start (make-timer-constructor
                          (calendar-event
                            #:months
                            (if (<= (date-month (current-date)) 6)
                                '(12)
                                '(1)))
                          (command (quote ("sh" "-c" "echo Triggered from \$PWD."))
                                   #:directory "$PWD"))
                 #:stop (make-timer-destructor)
                 #:actions (list timer-trigger-action))))

(register-services timers)
EOF

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

shepherd_pid="`cat $pid`"

$herd start timer-with-command
sleep 2
grep "Hi from " "$service_log"
$herd status timer-with-command | grep "Log file: $PWD/$service_log"
$herd status timer-with-command | grep "Hi from " # recent messages
$herd status timer-with-command | grep "exited successfully" # recent runs
$herd stop timer-with-command

$herd start timer-with-procedure
sleep 2
$herd status timer-with-procedure | grep "Completed in" # recent runs
grep "Hello from procedure" "$log"

$herd start timer-that-throws
$herd trigger timer-that-throws
until grep "Throwing" "$log"; do sleep 0.3; done
$herd status timer-that-throws | grep "Exception thrown.*mkdir" # recent runs

rm -f "$service_pid"
$herd start endless-timer
sleep 2
grep "Started endless timer" "$log"
$herd status endless-timer | grep "Started endless timer" # recent messages
kill -0 $(cat "$service_pid")
$herd status endless-timer | grep "Executing timer action"
$herd status endless-timer | grep "Child process: $(cat "$service_pid")"
$herd stop endless-timer
kill -0 $(cat "$service_pid") && false
grep "Process $(cat "$service_pid") of timer 'endless-timer' terminated" "$log"

$herd start timer-that-takes-too-long
for i in $(seq 3)
do
    $herd trigger timer-that-takes-too-long
    # Check "recent messages" and "recent runs" in the output of 'herd status'.
    until $herd status timer-that-takes-too-long | \
	    grep "[0-9][0-9]:[0-9][0-9] Sleeping"; do sleep 0.3; done
    until test $($herd status timer-that-takes-too-long | \
	    grep "terminated with signal 15" | wc -l) -eq $i; do sleep 0.3; done
    if $herd status timer-that-takes-too-long | grep "Child process"
    then false; else true; fi
    test $(grep "Terminating.*timer-that-takes-too-long.*after maximum duration" "$log" | wc -l) \
	 -eq $i
done

# Start several timer processes, all of which exceed #:max-duration and
# ignore SIGTERM.  They should eventually get SIGKILL just fine.
# See <https://codeberg.org/shepherd/shepherd/issues/2>.
$herd start timer-that-resists
for i in $(seq 1 10)
do
    $herd trigger timer-that-resists
done
until test $(grep "Terminating.*timer-that-resists.*after maximum duration" "$log" | wc -l) \
	  -eq $i; do sleep 0.3; done
until test $($herd status timer-that-resists -n 20 | \
		 grep "terminated with signal 9" | wc -l) -eq $i; do sleep 0.3; done
if $herd status timer-that-resists | grep "Child process"
then false; else true; fi

$herd start timer-with-two-occurrences
until $herd status timer-with-two-occurrences | grep stopped; do sleep 0.3; done
test $(grep "limited number of occurrences" "$log" | wc -l) -eq 2

$herd start timer-without-matching-events && false
$herd status timer-without-matching-events | grep "stopped.*failing"

$herd start never-timer
grep "Triggered from $PWD" "$log" && false
$herd trigger never-timer
until grep "Triggered from $PWD" "$log"; do sleep 0.3; done

$herd stop root

while kill -0 "$(cat $pid)" ; do sleep 0.2; done

# Check error reports when config is invalid.
cat > "$conf" <<EOF
(use-modules (shepherd service timer))

(calendar-event #:days-of-week '(1 2 3))  ;wrong!
EOF
rm -f "$pid"
shepherd -I -s "$socket" -c "$conf" -l "$log" --pid="$pid" &
until grep "invalid day of week" "$log"; do sleep 0.2; done

$herd stop root
