#!/bin/bash
# Executed by init script

# One first boot run at a time: keel-init refuses while this one holds the
# lock, and this one waits while a keel-init holds it (lib/init-lock.sh).
# Taken before the defaults are read, so that a run which waited reads the
# RUN_FIRSTBOOT the other run left behind.
# shellcheck source=lib/init-lock.sh
source "$(dirname "${BASH_SOURCE[0]}")/lib/init-lock.sh"
init_lock_take "$INITHOOKS_LOCK" || exit 1
# whether anybody can answer the console, asked once for the notices and
# handed to the hooks in INITHOOKS_UNATTENDED
# shellcheck source=lib/console.sh
source "$(dirname "${BASH_SOURCE[0]}")/lib/console.sh"

# load/set general global vars
INITHOOKS_DEFAULT="${INITHOOKS_DEFAULT:-/etc/default/inithooks}"
# - give shellcheck explict repo path for linting package
# shellcheck source=default/inithooks
source "$INITHOOKS_DEFAULT"
TERM=${TERM:-linux}
RUN_FIRSTBOOT="${RUN_FIRSTBOOT,,}"
TKLINFO="${TKLINFO:-/var/lib/turnkey-info}"
INITHOOKS_COMPLETE="${INITHOOKS_COMPLETE:-/run/inithooks-complete}"
REDIRECT_OUTPUT="${REDIRECT_OUTPUT,,}"
PID=
REBOOT_REQUIRED=

# the hooks read $INITHOOKS_CONF, and some of them write it: 29preseed of
# the headless and qemu overlays creates the file, and 00declarative renders
# it from the instance description. Export it whether or not it exists yet,
# or a hook whose job is to create it gets an empty path and an ambiguous
# redirect instead.
export INITHOOKS_CONF="${INITHOOKS_CONF:-/etc/inithooks.conf}"

# load preseeds if they exist - although preseeds file should always be empty
# unless RUN_FIRSTBOOT=true (firstboot will wipe preseeds file)
if [[ -f $INITHOOKS_CONF ]]; then
    # hide this shellcheck warning for now, although we probably should
    # include an example conf file?!
    # shellcheck source=/dev/null
    source "$INITHOOKS_CONF"
fi

# ensure that log file exists and has appropriate permissions
export INITHOOKS_LOGFILE="${INITHOOKS_LOGFILE:-/var/log/inithooks.log}"
mkdir -p "$(dirname "$INITHOOKS_LOGFILE")"
touch "$INITHOOKS_LOGFILE"
chmod 640 "$INITHOOKS_LOGFILE"

# The back title of the first boot screens (libinithooks/dialog_wrapper.py,
# BRAND), on the notices below as well.
FIRSTBOOT_TITLE="Keel Linux - First boot configuration"
BOOT_WAITED=

# How long a notice may take to reach the console before the notices are
# given up for this run.
NOTICE_TIMEOUT="${NOTICE_TIMEOUT:-2}"
NOTICES_OFF=
# where the hooks print, once nobody can answer the console
HOOK_OUTPUT=

# notice TEXT
# Shows TEXT in a box on the terminal the hooks draw on, and goes on: the
# screen of the hook before stays up otherwise, and it looks frozen while a
# hook works or the boot is waited for (2026-10-02, a Proxmox console left
# on "Did you save the password?" after <Saved>). Nothing is drawn when the
# output is not a terminal or goes to the log (REDIRECT_OUTPUT), and a
# notice that cannot be drawn is not an error.
#
# A notice may never hold the boot. In an LXC container tty1 is a pty whose
# master only an attached console (pct console, lxc-console) reads: with
# nobody attached, the published core booted headless on 2026-10-03 wrote
# one box per hook until the pty's buffer was full and the next write
# blocked for good, after 30turnkey-init-fence. So no notice is drawn on a
# console nobody can answer (lib/console.sh: one with no size, or one that
# does not take a write), and a notice that does not reach the console
# within NOTICE_TIMEOUT is given up, with the ones after it. Either way the
# hooks are told (unattended), and print into the log from then on. Each
# case is said once, in the log, so an operator can tell.
notice() {
    [[ -z "$NOTICES_OFF" ]] || return 0
    if [[ ! -t 1 ]] || [[ "$REDIRECT_OUTPUT" == "true" ]]; then
        notices_off "the output is not a terminal"
        return 0
    fi
    if console_unattended; then
        unattended
        return 0
    fi
    timeout --foreground "$NOTICE_TIMEOUT" dialog --backtitle "$FIRSTBOOT_TITLE" \
        --infobox "$1" 5 60 2>/dev/null
    if [[ $? -eq 124 ]]; then
        INITHOOKS_UNATTENDED="the console did not take a notice in ${NOTICE_TIMEOUT} s, nobody is reading it"
        unattended
    fi
    return 0
}

# unattended: nobody can answer the console, for INITHOOKS_UNATTENDED's
# reason. No more notices, the hooks are told, and what they print goes to
# the log: a console nobody reads blocks its writer once its buffer is
# full (95secupdates prints the whole upgrade).
unattended() {
    export INITHOOKS_UNATTENDED
    notices_off "$INITHOOKS_UNATTENDED"
    HOOK_OUTPUT=$INITHOOKS_LOGFILE
    log info "the first boot questions are not asked, and the hooks print into $INITHOOKS_LOGFILE"
}

# notices_off REASON: no more notices this run, said once
notices_off() {
    NOTICES_OFF=true
    log info "first boot notices not drawn: $1"
}

wait_for_boot() {
    # wait up to 10 secs for system to be running before starting; minimizes chance
    # of journal overwriting inithook dialog/confconsole. Once per run: the
    # wait was before every hook from 30 on, 10 s each on a system still
    # starting, with nothing on the screen.
    [[ -z "$BOOT_WAITED" ]] || return 0
    BOOT_WAITED=true
    local count
    logger -t inithooks "systemctl is-system-running: $(systemctl is-system-running)"
    for count in {1..10}; do
        if [[ "$(systemctl is-system-running)" != "starting" ]]; then
            return 0
        fi
        logger -t inithooks "Waiting for boot to finish ($count/10 seconds)"
        notice "Waiting for the system to finish starting... please wait ($count/10 s)"
        sleep 1
    done
}

log() {
    # log to journal as well as $INITHOOKS_LOGFILE
    local level=$1 # err|warn|info|debug
    shift
    logger -t inithooks -p "${level,,}" "$@"
    if [[ -f "$INITHOOKS_LOGFILE" ]]; then
        echo "${level^^}: $*" >> "$INITHOOKS_LOGFILE"
    fi
}

if [[ "$REDIRECT_OUTPUT" == "true" ]]; then
    # on xen redirection is performed by the inithooks-xen service
    # on lxc and other headless deployments, redirection is handled below
    # otherwise redirection is handled by inithooks service and redirected to
    # tty8

    if [[ ! -f "$TKLINFO/xen" ]]; then
        TTY=$(cat /sys/devices/virtual/tty/tty0/active)
        if [[ -z $TTY ]]; then
            TTY=console
        fi
        tail -f "$INITHOOKS_LOGFILE" > "/dev/$TTY" {INIT_LOCK_FD}>&- &
        PID="$!"
    fi
fi

exec_scripts() {
    local script_dir=$1
    local firstboot=$2
    local script_executable=
    local script=
    [[ -d "$script_dir" ]] || return 0
    readarray -d '' all_scripts < <(find "$script_dir" \( -type f -or -type l \) -print0 | sort -z)
    for script_executable in "${all_scripts[@]}"; do
        # this is already sourced above is it needed again here?
        if [[ -e $INITHOOKS_CONF ]]; then
            # as per above shellcheck $INITHOOKS_CONF note
            # shellcheck source=/dev/null
            source "$INITHOOKS_CONF"
        fi
        script=$(basename "$script_executable")
        init_lock_describe "$INITHOOKS_LOCK" "${firstboot:-everyboot}" "$script"
        if [[ -n "$firstboot" ]]; then
            # if firstboot, then only run <30 scripts - then wait
            prefix="${script:0:2}"
            if [[ $prefix =~ ^[0-9]+$ ]] && (( 10#$prefix >= 30 )); then
                wait_for_boot
            fi
        fi
        if [[ ! -x "$script_executable" ]]; then
            log warn "[$script] skipping"
            continue
        fi
        log info "[$script] running"
        if [[ -n "$firstboot" ]] && [[ "$script" =~ ^[0-9]*(.+)$ ]]; then
            # until the hook draws its own screen, or for as long as it
            # works without one
            notice "Configuring ${BASH_REMATCH[1]}... please wait"
        fi
        # the hook does not get the lock's descriptor: see lib/init-lock.sh
        if [[ -n "$HOOK_OUTPUT" ]]; then
            "$script_executable" {INIT_LOCK_FD}>&- >> "$HOOK_OUTPUT"
        else
            "$script_executable" {INIT_LOCK_FD}>&-
        fi
        exit_code=$?
        if [[ "$exit_code" -eq 0 ]]; then
            log info "[$script] successfully completed"
        elif [[ "$script" = "95secupdates" ]] && [[ "$exit_code" -eq 2 ]]; then
            log info "[$script] detected live system - skipping"
        elif [[ "$script" = "95secupdates" ]] && [[ "$exit_code" -eq 42 ]]; then
            REBOOT_REQUIRED=true
            log warn "[$script] reboot is required"
        else
            log err "[$script] failed - exit code $exit_code"
        fi
    done
    return 0
}

if [[ "$RUN_FIRSTBOOT" == "true" ]]; then
    log info "Running firstboot scripts"
    exec_scripts "$INITHOOKS_PATH/firstboot.d" firstboot
fi

# ensure everyboot scripts only run once per boot. The marker also tells
# keel-init that the boot run of this boot is over (libinithooks/init_lock.py
# boot_pending), so it is written whatever RUN_FIRSTBOOT says.
if [[ ! -f "$INITHOOKS_COMPLETE" ]]; then
    log info "Running everyboot scripts"
    exec_scripts "$INITHOOKS_PATH/everyboot.d"
    touch "$INITHOOKS_COMPLETE"
fi

# no hook runs after this point; confconsole below stays up for as long as
# the operator wants, and keel-init must not be refused because of it
init_lock_release

if [[ -n "$PID" ]];  then
    log info "Killing inithooks pid $PID"
    kill -9 $PID || true
fi

log info "Inithooks run completed"
if [[ -n "$REBOOT_REQUIRED" ]]; then
    log err "Rebooting now to ensure all security updates are applied"
    systemctl reboot
    exit 0
fi

if [[ "$REDIRECT_OUTPUT" == "true" ]]; then
    log info "Inithooks exiting."
else
    # ensure confconsole --usage isn't overwritten on reboots
    wait_for_boot
    log info "Inithooks starting Confconsole"
    sleep 2 # anyway to replace this?
    log info "Confconsole started, Inithooks exiting"
    confconsole --usage
fi

exit 0
