#!/bin/sh
# This file is part of LTSP, https://ltsp.org
# Copyright 2019 the LTSP team, see AUTHORS
# SPDX-License-Identifier: GPL-3.0-or-later

# Main entry point for all ltsp applets; also contains the essential functions
# @LTSP.CONF: LTSP_DEBUG DEBUG_SHELL DEBUG_LOG

# Internal global variables are uppercased and begin with underscores
# Distributions should replace "1.0" below at build time using `sed`
_VERSION="1.0"
_NL="
"

# Execution sequence:
# This main() > source common/ltsp/* (and config/vendor overrides)
# > ltsp_cmdline() > ltsp/scriptname_main()s > source applet/* (and overrides)
# > applet_cmdline() > applet/scriptname_main()s
main() {
    # Try to stop on unhandled errors, http://fvue.nl/wiki/Bash:_Error_handling
    # Prefer `false || false` as it exits while `false && false` doesn't,
    # except if it's the last command of a function (return value).
    set -e

    re busybox_fallbacks
    # Usually this script is called from a symlink like /usr/sbin/ltsp,
    # so $0 points to the ltsp source directory. If that's not the case,
    # assume we're sourced, and search for the source in /usr/share/ltsp.
    _LTSP_DIR=$(re readlink -f "$(command -v "$0")")
    _LTSP_DIR=${_LTSP_DIR%/*}
    if [ -x "$_LTSP_DIR/ltsp" ] && [ -d "$_LTSP_DIR/common" ]; then
        unset _SOURCED
        # Default to a sane umask for all files generated by applets
        umask 0022
        # Default to a sane LANG; e.g. python doesn't like unset/C
        test "${LANG:-C}" != "C" || export LANG=C.UTF-8
    else
        for _LTSP_DIR in "${LTSP_DIR:-/usr/share/ltsp}" "$(re pwd)"; do
            if [ -x "$_LTSP_DIR/ltsp" ] && [ -d "$_LTSP_DIR/common" ]; then
                _SOURCED=1
                debug "$_LTSP_DIR/ltsp sourced by $0"
                # Ignore the caller command line parameters; they're not for us
                set --
                break
             fi
        done
        test "$_SOURCED" = "1" || die "Could not locate the ltsp directory"
    fi
    locate_applet_scripts "ltsp"
    source_scripts "$_SCRIPTS"
    test "$_SOURCED" = "1" || ltsp_cmdline "$@"
}

# On abnormal termination, we run both the term and exit commands.
# On normal termination, we only run the exit commands.
# For example, in initrd-bottom we don't want to unmount on normal exit.
at_exit() {
    # Don't stop on errors for the exit commands
    set +e
    # Stop trapping
    trap - 0 HUP INT QUIT SEGV PIPE TERM
    if [ "$1" = "-TERM" ] || [ "$_DIED" = "1" ]; then
        eval "$_TERM_COMMANDS"
    fi
    eval "$_EXIT_COMMANDS"
    # It's possible to manually call at_exit, run the commands,
    # then call exit_command again (e.g. for `ltsp kernel img1 img2`).
    unset _TERM_COMMANDS
    unset _EXIT_COMMANDS
    unset _HAVE_TRAP
    set -e
}

# For the external tools we need that are also provided by busybox,
# if some tool doesn't exist, create a namesake function that calls busybox.
# `/usr/lib/initramfs-tools/bin/busybox` shows the smallest list of tools.
busybox_fallbacks() {
    local busybox tool

    busybox=$(command -v busybox)
    # Ubuntu chroots ship with a "busybox-initramfs" minimal package
    if [ -z "$busybox" ] && [ -x /usr/lib/initramfs-tools/bin/busybox ]; then
        busybox=/usr/lib/initramfs-tools/bin/busybox
    fi
    if [ -z "$busybox" ]; then
        warn "Busybox not found?!"
        return 0
    fi
    for tool in awk blockdev cat chgrp chmod chown chroot chvt cp \
        cpio cut date df env expr find getopt grep head hostname id \
        insmod ionice ip kill killall ln logger losetup ls lsmod \
        mkdir mktemp modprobe mount mv nc netstat pidof ping \
        poweroff ps pwd readlink rm rmdir rmmod sed setsid sleep sort \
        swapoff swapon switch_root sync tee touch tr truncate umount \
        uname wc
    do
        # Periodically, prefix a "true" to the following line and test all
        # applets to see if we are indeed compatible with the busybox syntax
        ! is_command "$tool" || continue
        eval "$tool() {
    $busybox $tool \"\$@\"
}"
    done
}

# Show a message to stderr and /run/ltsp/debug.log if $LTSP_DEBUG is
# appropriately set
debug() {
    case ",$LTSP_DEBUG," in
        *",$_APPLET,"*|,1,)  ;;
        *)  return 0;
    esac
    warn  "$@"
}

# The debug shell is used to examine the internals of the ltsp scripts;
# to get or set variables; so, use `eval`, not `bash`.
debug_shell() {
    local cmd

    if [ "$$" != "$(sh -c 'echo $PPID')" ]; then
        warn "Not activating debug_shell while inside a subshell"
        exit 1
    fi
    echo "${*:-"Emulating a shell, type \`return [0|1]\` to continue|exit:"}"
    printf "debug:%s\$ " "$(pwd)"
    while read -r cmd; do
        eval "$cmd" || true
        printf "debug:%s\$ " "$(pwd)"
    done
}

# Print a message to stderr and exit with an error code.
# No need to pass a message if the failed command displays the error.
die() {
    test $# -eq 0 && set "Aborting ltsp"
    # Debian defaults to SPLASH="true" and only disables it when
    # nosplash*|plymouth.enable=0 is passed in the cmdline
    if [ "$_APPLET" = "initrd-bottom" ] || [ "$_APPLET" = "init" ]; then
        if is_command plymouth && pidof plymouthd >/dev/null; then
            warn "Stopping plymouth"
            rw plymouth quit
        fi
        warn "$@"
        if [ "$DEBUG_SHELL" != "1" ]; then
            warn "LTSP boot error! Enable DEBUG_SHELL to troubleshoot!"
            while sleep 1000; do
                true
            done
            return 0
        fi
    else
        warn "$@"
    fi
    test "$DEBUG_SHELL" = "1" && debug_shell
    # This notifies at_exit() to execute TERM_COMMANDS
    _DIED=1
    # If called from subshells, this just exits the subshell
    # With `set -e` though, it'll still exit on commands like x=$(false)
    exit 1
}

# POSIX recommends that printf is preferred over echo.
# But do offer a simple wrapper to avoid "%s\n" all the time.
echo() {
    printf "%s\n" "$*"
}

# You may use `exit_command "rw command"`, but not `exit_command "re command"`
exit_command() {
    if [ "$_HAVE_TRAP" != "1" ]; then
        _HAVE_TRAP=1
        trap "at_exit -TERM" HUP INT QUIT SEGV PIPE TERM
        trap "at_exit -EXIT" EXIT
    fi
    if [ "$_APPLET" = "initrd-bottom" ] || [ "$_APPLET" = "init" ]; then
        _TERM_COMMANDS="$*
$_TERM_COMMANDS"
    else
        _EXIT_COMMANDS="$*
$_EXIT_COMMANDS"
    fi
}

# Check if parameter is a command; `command -v` isn't allowed by POSIX
is_command() {
    local fun

    if [ -z "$is_command" ]; then
        command -v is_command >/dev/null ||
            die "Your shell doesn't support command -v"
        is_command=1
    fi
    for fun in "$@"; do
        command -v "$fun" >/dev/null || return $?
    done
}

# Set _APPLET, _APPLET_DIR, _APPLET_FUNCTION and _SCRIPTS
locate_applet_scripts() {
    local sub_dir script

    _APPLET=$1
    shift
    for sub_dir in common server client; do
        _APPLET_DIR="$_LTSP_DIR/$sub_dir/$_APPLET"
        test -d "$_APPLET_DIR" && break
    done
    test -d "$_APPLET_DIR" || die "Could not locate LTSP applet: $_APPLET"
    if [ "$sub_dir" = client ] && [ ! -d /run/ltsp/client ] && [ "$$" != "1" ]
    then
        die "Refusing to run client applet $_APPLET in non-ltsp client"
    fi
    # All applets are required to have an entry function ${_APPLET}_cmdline
    _APPLET_FUNCTION=$(echo "${_APPLET}_cmdline" | sed 's/[^[:alnum:]]/_/g')
    # https://www.freedesktop.org/software/systemd/man/systemd.unit.html
    # Drop-in files in /etc take precedence over those in /run
    # which in turn take precedence over those in /usr.
    _SCRIPTS=$(run_parts_list "$_APPLET_DIR" \
        "/run/ltsp/$sub_dir/$_APPLET" "/etc/ltsp/$sub_dir/$_APPLET")
}

# Run a command. Exit if it failed.
re() {
    rwr "$@" || die
}

# Run a command and return 0. Silently.
rs() {
    local _RWR_SILENCE
    # If _RWR_SILENCE isn't declared local, it might remain in the environment!
    _RWR_SILENCE=1 rwr "$@" || true
}

# Run a command silently and return $?. Used like `rsr cmd1 || cmd2`.
# This is just a shortcut for `cmd1 >/dev/null 2>&1 || cmd2`.
rsr() {
    local _RWR_SILENCE
    _RWR_SILENCE=1 rwr "$@" || return $?
}

# Run all the main_script() functions we already sourced
run_main_functions() {
    local scripts script

    scripts=$1; shift
    # 55-initrd.sh should be called as: initrd_main
    # <&3 is to allow scripts to use stdin instead of using the HEREDOC
    while read -r script <&3; do
        is_command "${script}_main" || continue
        case ",$LTSP_SKIP_SCRIPTS," in
            *",$script,"*) debug "Skipping main of script: $script" ;;
            *)  debug "Running main of script: $script"
                "${script}_main" "$@"
                ;;
        esac
    done 3<<EOF
$(echo "$scripts" | sed -e 's/.*\///' -e 's/[^[:alpha:]]*\([^.]*\).*/\1/g' -e 's/[^[:alnum:]]/_/g')
EOF
}

# Input: two optional `find` parameters and an ordered list of directories.
# Output: a list of files, including their paths, ordered by their basenames.
# Files with the same name in subsequent directories (even in subdirs)
# override previous ones.
# Restriction: directory and file names shouldn't contain \t or \n.
# Algorithm: create the list as three tab-separated columns, like:
#   99  file  /dir[/subdir]/file
# The first column is an increasing directory index.
# Then sort them in reverse order, and finally by file name,
# so that "--unique" keeps the last occurrence.
run_parts_list() {
    local param1 param2 tab i d f

    # If the first parameter starts with "-", consider it a find parameter
    if [ "$1" != "${1#-}" ]; then
        param1=$1
        param2=$2
        shift 2
    else
        param1="-name"
        param2="[0-9]*"
    fi
    tab=$(printf "\t")
    i=10
    for d in "$@"; do
        test -d "$d" || continue
        # Don't quote $param1 in case more params are ever required
        find "$d" -maxdepth 1 $param1 "$param2" -type f \
        | while IFS='' read -r f; do
            printf '%s\t%s\t%s\n' "$i" "${f##*/}" "$f"
        done
        i=$((i+1))
    done \
    | sort -r \
    | sort -t "$tab" -k 2,2 -u \
    | sed 's@[^\t]*\t[^\t]*\t@@'
}

# Run a command and return 0. Warn if it failed.
rw() {
    rwr "$@" || true
}

# Run a command. Warn if it failed. Return $?.
# Don't warn if $RWR_SILENCE is set, to easily implement rs() and rsr().
# Used like `rwr cmd1 || cmd2`.
rwr() {
    local want got

    if [ "$1" = "!" ]; then
        want=1
        shift
    else
        want=0
    fi
    got=0
    if [ "$_RWR_SILENCE" = "1" ]; then
        "$@" >/dev/null 2>&1 || got=$?
    else
        "$@" || got=$?
    fi
    # Failed if either of them is zero and the other non-zero
    # Use {} to avoid subshells and shellcheck's SC2166
    if { [ "$want" = 0 ] && [ "$got" != 0 ]; } ||
       { [ "$want" != 0 ] && [ "$got" = 0 ]; } then
        test "$_RWR_SILENCE" = "1" || warn "LTSP command failed: $*"
    fi
    return $got
}

source_scripts() {
    local scripts script

    scripts=$1
    test -n "$scripts" || die "ltsp $_APPLET contains no scripts!"
    while read -r script <&3; do
        debug "Sourcing: $script"
        . "$script"
    done 3<<EOF
$scripts
EOF
}

# Show usage. _APPLET must be set. If $1 is set, exit with that code.
usage() {
    local cmd text atext

    if [ "$_APPLET" = "ltsp" ]; then
        cmd="man ltsp"
        text=$(re man ltsp)
    else
        cmd="man ltsp $_APPLET"
        text=$(re man "ltsp-$_APPLET")
    fi
    atext=$(echo "$text" | sed -n '/^APPLETS/,/^[^ ]/s/^\(       \)//p' |
        sed 's/^\([^ ]\)   /  \1 /')
    atext=${atext:+$_NL$atext$_NL}
    printf "Usage: %s\n\n%s\n%s\nFor extensive help, run: %s\n" \
        "$(echo "$text" | sed -n '/^SYNOPSIS/,/^[^ ]/s/^\(       \|$\)//p')" \
        "$(echo "$text" | sed -n '/^DESCRIPTION/,/^[^ ]/s/^\(       \|$\)//p')" \
        "$atext" \
        "$cmd"
    test -n "$1" && exit "$1"
}

version() {
    echo "ltsp $_VERSION"
}

# Print a message to stderr and log it to /run/ltsp/debug.log
warn() {
    echo "$@" >&2

    if [ -z "$DEBUG_LOG" ]; then
        DEBUG_LOG=0
        # Only keep the last `ltsp command` log for non-ltsp boots
        test -d /run/ltsp/client || rs rm -f /run/ltsp/debug.log
        test -w /run &&
            mkdir -p /run/ltsp &&
            ( umask 0077 && touch /run/ltsp/debug.log ) &&
            DEBUG_LOG=1
    fi
    if [ "$DEBUG_LOG" = "1" ]; then
        echo "$(date +%H:%M:%S.%N) $*" >>/run/ltsp/debug.log
    fi
}

main "$@"
