#!/bin/sh
# Imprion OS — install, at boot and before the session, what is already
# downloaded (README decisions 290, 292). Run by imprion-boot-update.service.
#
# Rules this script keeps, each of them a line below:
#   * no network: --no-download, and the unit has PrivateNetwork=yes;
#   * once per boot: $RUN_DIR/done is written first and checked first;
#   * a hard time limit on apt (LIMIT_SECONDS), and the unit's own
#     TimeoutStartSec above it;
#   * never a prompt: noninteractive debconf, --force-confnew/--force-confdef;
#   * nothing a postinst starts or stops may wait on the session this unit
#     is holding back: a temporary policy-rc.d answers 101 for any unit that
#     still has a queued boot job (gdm, the session, anything ordered after
#     this one), and the display manager by name;
#   * whatever happens, exit 0: the result goes to $DATA_DIR/result for the
#     agent to report, and the session starts.
set -u

RUN_DIR=${IMPRION_BOOT_UPDATE_RUN_DIR:-/run/imprion-boot-update}
DATA_DIR=${IMPRION_BOOT_UPDATE_DATA_DIR:-/var/lib/imprion-boot-update}
ARCHIVES=${IMPRION_BOOT_UPDATE_ARCHIVES:-/var/cache/apt/archives}
POLICY_RC=${IMPRION_BOOT_UPDATE_POLICY_RC:-/usr/sbin/policy-rc.d}
APT_GET=${IMPRION_BOOT_UPDATE_APT_GET:-apt-get}
DPKG=${IMPRION_BOOT_UPDATE_DPKG:-dpkg}
SYSTEMCTL=${IMPRION_BOOT_UPDATE_SYSTEMCTL:-systemctl}
LIMIT_SECONDS=${IMPRION_BOOT_UPDATE_LIMIT_SECONDS:-1200}
REPAIR_SECONDS=${IMPRION_BOOT_UPDATE_REPAIR_SECONDS:-300}
DISPLAY_UNIT=imprion-boot-update-display.service
TAG='# imprion-boot-update: temporary'

LC_ALL=C
DEBIAN_FRONTEND=noninteractive
APT_LISTCHANGES_FRONTEND=none
NEEDRESTART_SUSPEND=1
export LC_ALL DEBIAN_FRONTEND APT_LISTCHANGES_FRONTEND NEEDRESTART_SUSPEND

log() {
    echo "boot-update: $*"
}

mkdir -p "$RUN_DIR" "$DATA_DIR"

# --- once per boot ----------------------------------------------------------
if [ -e "$RUN_DIR/done" ]; then
    log "already ran this boot"
    exit 0
fi
: > "$RUN_DIR/done"

# A policy-rc.d left by a power cut in the middle of the last run.
if [ -f "$POLICY_RC" ] && grep -qF "$TAG" "$POLICY_RC"; then
    rm -f "$POLICY_RC"
    log "removed a stale policy-rc.d"
fi

APT_OPTS="-o DPkg::Lock::Timeout=60 -o Dpkg::Options::=--force-confnew -o Dpkg::Options::=--force-confdef -o APT::Get::Download=false -o Acquire::Retries=0"

# --- repair an interrupted install ------------------------------------------
# A run cut off by the time limit or a power cut leaves packages unpacked and
# not configured. Configuring them needs no network.
if [ -n "$($DPKG --audit 2>/dev/null)" ]; then
    log "dpkg reports an interrupted install, configuring"
    timeout --kill-after=30 "$REPAIR_SECONDS" $DPKG --configure -a --force-confnew --force-confdef || log "dpkg --configure -a failed"
fi

# --- what is downloaded -----------------------------------------------------
# One line per upgradable package: name, candidate, architecture, whether it
# is new to this terminal.
plan=$($APT_GET -s -o Debug::NoLocking=1 dist-upgrade 2>/dev/null | sed -n 's/^Inst \([^ ]*\) \(\[[^]]*\] \)\{0,1\}(\([^ ]*\) [^)]*\[\([^]]*\)\]).*$/\1 \3 \4 \2/p')

if [ -z "$plan" ]; then
    log "nothing to install"
    exit 0
fi

all_downloaded=yes
names=""
while read -r name version arch current; do
    [ -n "$name" ] || continue
    file="$ARCHIVES/${name}_$(printf '%s' "$version" | sed 's/:/%3a/g')_${arch}.deb"
    if [ -f "$file" ]; then
        # An upgrade can be named; a package new to this terminal is only
        # ever pulled in by dist-upgrade.
        [ -n "$current" ] && names="$names $name"
    else
        all_downloaded=no
    fi
done <<EOF
$plan
EOF

if [ "$all_downloaded" = yes ]; then
    command="dist-upgrade"
elif [ -n "$names" ]; then
    command="install --only-upgrade$names"
else
    log "nothing downloaded yet"
    exit 0
fi

# The exact count the screen shows: the same command, simulated.
count=$($APT_GET -s -o Debug::NoLocking=1 $APT_OPTS $command 2>/dev/null | grep -c '^Inst ')
if [ "$count" -eq 0 ]; then
    log "nothing to install"
    exit 0
fi

# --- the screen -------------------------------------------------------------
started=$(date +%s)
: > "$RUN_DIR/status"
echo "$count" > "$RUN_DIR/count"
echo "$started" > "$RUN_DIR/started"
echo install > "$RUN_DIR/phase"
$SYSTEMCTL start --no-block "$DISPLAY_UNIT" || log "the updating screen could not be started"

# --- the policy-rc.d --------------------------------------------------------
own_policy=no
if [ ! -e "$POLICY_RC" ]; then
    cat > "$POLICY_RC" <<POLICY
#!/bin/sh
$TAG
# Deny any start/stop/restart of a unit that still has a queued boot job: the
# boot install holds the session back, and a postinst waiting for it would
# wait for itself. The unit starts with the new files when the boot goes on.
unit="\$1"
case "\$unit" in
    display-manager*|gdm*|imprion-enrollment*|imprion-boot-update*|graphical.target) exit 101 ;;
esac
job=\$($SYSTEMCTL show -P Job "\$unit" 2>/dev/null)
[ -n "\$job" ] && exit 101
exit 0
POLICY
    chmod 0755 "$POLICY_RC"
    own_policy=yes
fi

finish() {
    rc=$1
    error=$2
    if [ "$own_policy" = yes ]; then
        rm -f "$POLICY_RC"
    fi
    {
        echo "exit=$rc"
        echo "started=$started"
        echo "finished=$(date +%s)"
        echo "count=$count"
        echo "error=$error"
    } > "$DATA_DIR/result.tmp"
    mv "$DATA_DIR/result.tmp" "$DATA_DIR/result"
    if [ "$rc" -eq 0 ]; then
        echo done > "$RUN_DIR/phase"
    else
        echo failed > "$RUN_DIR/phase"
    fi
    timeout 20 $SYSTEMCTL stop "$DISPLAY_UNIT" || log "the updating screen could not be stopped"
    log "finished: exit $rc ${error:+($error)}"
    exit 0
}

trap 'finish 143 "stopped"' TERM INT

# --- the install ------------------------------------------------------------
log "installing $count packages ($command)"
timeout --kill-after=30 "$LIMIT_SECONDS" $APT_GET -y $APT_OPTS -o APT::Status-Fd=3 $command 3>>"$RUN_DIR/status" > "$DATA_DIR/log" 2>&1
rc=$?

error=""
case "$rc" in
    0) ;;
    124|137) error="timed out after $LIMIT_SECONDS s" ;;
    # dpkg's "error processing package X" line and the reason under it name
    # the package; apt's closing "E: Sub-process ..." line names none.
    *) error=$(awk '/^dpkg: error processing/ { line = $0; sub(/:$/, "", line); if ((getline reason) > 0 && reason !~ /^(dpkg:|E: )/) { sub(/^[ \t]+/, "", reason); line = line ": " reason }; print line; exit }' "$DATA_DIR/log")
       [ -n "$error" ] || error=$(grep -E '^(E: |dpkg: error)' "$DATA_DIR/log" | tail -n 1) ;;
esac

finish "$rc" "$error"
