Package: runit
Version: 2.2.0-6
Severity: wishlist
Tags: patch

Hi,

runit's shutdown(8) doesn't support scheduled shutdowns. This isn't normally
a problem (if you want, you can use at(1) to schedule a shutdown); but it
breaks the "Unattended-Upgrade::Automatic-Reboot-Time" feature of
unattended-upgrades.

I wrote a wrapper (in plain Bourne shell instead of zsh this time) for
runit's shutdown to support more of the features of systemd shutdown(8) more
or less transparently. This includes scheduled shutdowns (which are
implemented via at(1)). In my so far admittedly fairly minimal testing it
worked as intended.

Please consider including the wrapper in the runit package and enabling it
by default.

(I'd appreciate quick feedback on whether you will do this, because if not,
I'll build my own package, for my own use, that diverts runit's shutdown and
replaces it with my wrapper.)

Thanks!

András

-- System Information:
Init: runit (via /run/runit.stopit)

-- 
             When you starve with a tiger, the tiger starves last.
#!/bin/sh
#
# This is a wrapper around the /usr/lib/runit/shutdown that's shipped by the
# Debian runit package. Its purpose is to add support for reboot times
# other than "now", by way of scheduling the future reboot as an at(1) job;
# but while I'm at it I might as well add support for all the other systemd
# shutdown options as well.
#
# Copyright (c) 2025 András Korn.
#  
# Licensed under the GPL v3, or, at your option, and only if it's included
# in the runit package, under the BSD-3-clause license.

real_shutdown=/usr/lib/runit/shutdown.distrib
rundir=/run/runit/scheduled-shutdown
[ $# -eq 0 ] && exec "$real_shutdown"

wallmessage=""
timespec=""
wallonly=0
wall=1
shutdown_args=""

cancel_shutdown() {
        if cd "$rundir" 2>/dev/null; then
                j=$(echo *)
                if ! [ "$j" = "*" ]; then
                        atrm $j
                        ret=$?
                        rm $j
                        cd /    # currently superfluous as nothing that is run 
after calling cancel_shutdown is sensitive to PWD, but it's good hygiene
                        return $ret
                else
                        return 0
                fi
        else
                return 0
        fi
}

show_shutdown() {
        if cd "$rundir" 2>/dev/null; then
                j=$(echo *)
                if ! [ "$j" = "*" ]; then
                        timespec=$(cat $j | sort -t: -n -k1,1 -k2,2 | head -1)
                        echo "shutdown or reboot scheduled for $timespec, use 
'shutdown -c' to cancel."
                fi
        fi
        exit 0
}

while [ -n "$1" ]; do
        case "$1" in
                -[hrfFHP])                      shutdown_args="$shutdown_args 
$1";;
                now)                            shift; [ $# -gt 0 ] && 
wallmessage="${*}"; break;;
                -k)                             wallonly=1;;
                --no-wall)                      wall=0;;
                -c)                             cancel_shutdown; exit $?;;
                --show)                         show_shutdown;;         # 
doesn't return
                +[0-9]*|[012][0-9]:[0-5][0-9])  timespec="$1"; shift; [ $# -gt 
0 ] && wallmessage="${*}"; break;;
                *)                              echo "$0: WARNING: argument 
'$1' is unknown. Ignoring";;        # Ignore unknown arguments. Reasoning: it's 
better to be able to shut down without supporting some bell or whistle than to 
prevent shutdown.
        esac
        shift
done

case "$timespec" in
        "")             [ -n "$wallmessage" ] && [ "$wall" = 1 ] && wall "The 
system is going down NOW for: $wallmessage";;
        +[0-9]*)        timespec="$(date --date "now $timespec minutes" 
'+%H:%M')";;    # needs GNU date(1)
esac

if [ -n "$timespec" ]; then
        if [ -x /usr/bin/at ]; then
                # possible improvements: 1. include kind of shutdown in wall 
message (reboot or halt); 2. schedule some wall messages a few minutes before 
the actual shutdown.
                cd /    # make sure the pwd of the atjob is /, not some 
directory on e.g. a network fs
                if atoutput=$({
                        if [ "$wall" = 1 ]; then
                                if [ -n "$wallmessage" ]; then
                                        echo "wall 'The system is going down 
NOW for: $wallmessage'"
                                else
                                        echo "wall 'The system is going down 
NOW!'"
                                fi
                                [ "$wallonly" = 0 ] && echo "exec 
\"$real_shutdown\" $shutdown_args"
                        fi
                } | at "$timespec" 2>&1); then
                        jobnum=$(echo "$atoutput" | sed -n '/^job /{s/^job 
//;s/ .*//;p}')
                        case "$jobnum" in       # Is $jobnum an integer?
                                ''|*[!0-9]*)
                                        echo "$0: WARNING: I tried to schedule 
an at(1) job for $timespec to perform a 'shutdown $shutdown_args', and the job 
number is apparently '$jobnum', which is not an integer. I don't know what 
happened, or whether the shutdown will take place at the scheduled time. Please 
investigate." >&2;;
                                *)
                                        mkdir -p "$rundir"
                                        cancel_shutdown                         
# if there was a shutdown scheduled, cancel it -- we just scheduled a new one
                                        echo "$timespec" >"$rundir/$jobnum"     
# this is needed by show_shutdown and cancel_shutdown
                                        ;;
                        esac
                else
                        echo "$0: WARNING: I tried to schedule an at(1) job for 
$timespec to perform a 'shutdown $shutdown_args', and at(1) returned an error. 
The system will probably not shut down at the scheduled time. For your 
reference, here is the entire output of at(1):" >&2
                        echo "$atoutput" >&2
                        exit 2
                fi
        else
                echo "FATAL: can't schedule shutdown for $timespec because 
at(1) is apparently not available (/usr/bin/at can't be executed)." >&2
                exit 1
        fi
else
        if [ -n "$wallmessage" ] && [ "$wall" = 1 ]; then
                echo "wall 'The system is going down NOW for: $wallmessage'"
        else
                echo "wall 'The system is going down NOW!'"
        fi
        [ "$wallonly" = 0 ] && exec "$real_shutdown" $shutdown_args
fi
exit 0

Reply via email to