#!/bin/bash

## Copyright (C) 2012 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

#set -x
#exec >  >(tee -a "/home/user/msgprogressbarlog")
#exec 2> >(tee -a "/home/user/msgprogressbarlog" >&2)

set -o errexit
set -o nounset
set -o pipefail
set -o errtrace
shopt -s inherit_errexit
shopt -s shift_verbose

## Every command-line option target. Each is assigned only when its option is
## given, and the required ones are handed straight to check(), whose job is to
## report a missing value. Under nounset an unset read aborts BEFORE check()
## can say which option is missing, replacing a precise message with a bash
## error. Empty is what these expanded to before strict mode.
identifier=""
message=""
progressbaridx=""
progressbartitlex=""

## Derived, not parsed: each is assigned only when the file it comes from
## EXISTS, and both are read unconditionally afterwards. percentage_maybe_add
## is skipped when the progress text file is absent; icon when the
## '<identifier>_icon' file is. Empty is what both expanded to before.
percentage_maybe_add=""
icon=""
progress_txt_file_progress=""
verbose=""
## Assigned only where yad is actually launched, but cleanup_self reads it and
## runs EARLIER on two paths -- including the 'already 100%, not even opening
## the progress bar' race the code handles deliberately. Under nounset that
## clean exit aborted instead.
yad_progress_pid=""

source "${HELPER_SCRIPTS_PATH:-}/usr/libexec/helper-scripts/light_sleep.bsh"

scriptname="$(basename -- "${BASH_SOURCE[0]}")"

timeout_command=("timeout" "--kill-after" "1" "2")

disable_traps() {
   trap - ERR
   trap - SIGHUP
   trap - SIGTERM
   trap - SIGINT
}

error_handler() {
   local exit_code="$?"

   disable_traps

   local msg="\
###############################################################################
## ${scriptname} script bug.
## No panic. Nothing is broken. Just some rare condition has been hit.
## Try again later. There is likely a solution for this problem.
## Please see Whonix News, Whonix Blog and Whonix User Help Forum.
## Please report this bug!
##
## BASH_COMMAND: ${BASH_COMMAND}
## exit_code: ${exit_code}
###############################################################################\
"
   printf '%s\n' "${msg}" >&2
   if [ ! -d ~/".msgcollector" ]; then
      "${timeout_command[@]}" mkdir --parents -- ~/".msgcollector"
   fi
   ## Owner-execute for traversal (see msgcollector's error_handler): defensive
   ## against a dir pre-created under a umask that masks the 0100 bit.
   "${timeout_command[@]}" chmod u+rwx -- ~/".msgcollector" || true
   "${timeout_command[@]}" append ~/".msgcollector/msgdispatcher-error.log" "$0: BASH_COMMAND: ${BASH_COMMAND} | exit_code: ${exit_code}" >/dev/null || true
   exit 1
}

parse_cmd_options() {
   ## Thanks to:
   ## http://mywiki.wooledge.org/BashFAQ/035

   while true; do
       case "${1:-}" in
           --verbose)
               set -x
               ## Parsed for compatibility with the shared option set; not
               ## read in this script.
               # shellcheck disable=SC2034
               verbose="1"
               shift
               ;;
           --identifier)
               identifier="${2:-}"
               if [ "$#" -ge 2 ]; then
                  shift 2
               else
                  shift 1
               fi
               ;;
           --progressbaridx)
               progressbaridx="${2:-}"
               if [ "$#" -ge 2 ]; then
                  shift 2
               else
                  shift 1
               fi
               ;;
           --progressbartitlex)
               progressbartitlex="${2:-}"
               if [ "$#" -ge 2 ]; then
                  shift 2
               else
                  shift 1
               fi
               ;;
           --message)
               ## Empty is tolerated: start_progress_bar renders it gracefully.
               ## Guard the shift like the other options, so a trailing --message
               ## with no value does not abort under shift_verbose.
               message="${2:-}"
               if [ "$#" -ge 2 ]; then
                  shift 2
               else
                  shift 1
               fi
               ;;
           --)
               shift
               break
               ;;
           -*)
               ## sanitize-echo: $1 is an untrusted caller argument; printing it
               ## raw would let a crafted option inject terminal escapes.
               sanitize-echo -- "$0: unknown option: $1" >&2
               exit 1
               ;;
           *)
               break
               ;;
       esac
   done

   ## If there are input files (for example) that follow the options, they
   ## will remain in the "$@" positional parameters.
}

preparation() {
   fifo="${msgcollector_run_dir}/${identifier}_${progressbaridx}_fifo"
   progress_txt_file="${msgcollector_run_dir}/${identifier}_${progressbaridx}_progresstxt"
   parent_pid_file="${msgcollector_run_dir}/${identifier}_${progressbaridx}_parentpid"
   yad_progress_pid_file="${msgcollector_run_dir}/${identifier}_${progressbaridx}_yadprogresspid"
}

cleanup_self() {
   true "$0: Check if yad_progress_pid ${yad_progress_pid} is still running..."
   if kill -0 -- "${yad_progress_pid}" &>/dev/null; then
      true "$0: cleanup_self: yad still running: yes"
      true "$0: killing yad_progress_pid ${yad_progress_pid}..."
      if ! kill -s sigterm -- "${yad_progress_pid}" &>/dev/null; then
         kill -s sigkill -- "${yad_progress_pid}" &>/dev/null
      fi
   else
      true "$0: cleanup_self: yad still running: no"
   fi
   "${timeout_command[@]}" safe-rm --force -- "${fifo}"
   "${timeout_command[@]}" safe-rm --force -- "${progress_txt_file}"
   "${timeout_command[@]}" safe-rm --force -- "${yad_progress_pid_file}"
   "${timeout_command[@]}" safe-rm --force -- "${parent_pid_file}"
}

output_ex() {
   ## This function is called when the cancel button in yad gets pressed or
   ## when $progress_txt_file contains 100.

   ## Debugging.
   #printf '%s\n' "progress: $progress | caller: $PPID | $caller | killing $yad_progress_pid" >> /home/user/progresslog

   if [ -f "${parent_pid_file}" ]; then
      "${timeout_command[@]}" "${MSGCOLLECTOR_REPO:-}/usr/libexec/msgcollector/msgprogressbar_kill_helper" "${identifier}" "${progressbaridx}"
   else
      true "${output_signal_caught} caught. Not killing parentpid, because ${parent_pid_file} does not exist."
   fi

   cleanup_self
   exit 0
}

output_sighup() {
   output_signal_caught="SIGHUP"
   output_ex
}

output_sigint() {
   output_signal_caught="SIGINT"
   output_ex
}

output_sigterm() {
   output_signal_caught="SIGTERM"
   output_ex
}

start_progress_bar() {
   ## An empty title or message is a caller omission, not a user-actionable bug.
   ## Never surface an alarming "please report this bug" popup to the user for it.
   ## Title: fall back to the identifier, matching msgdispatcher's convention.
   ## ${identifier} is validated non-empty by msgcollector_check before we get here.
   ## Message: leave the progress text empty; yad renders that cleanly.
   if [ "${progressbartitlex}" = "" ]; then
      true "INFO: ${scriptname}: progressbartitlex empty; falling back to identifier."
      progressbartitlex="${identifier}"
   fi

   if [ "${message}" = "" ]; then
      true "INFO: ${scriptname}: message empty; rendering an empty progress text."
   fi

   if [ -f "${msgcollector_run_dir}/${identifier}_icon" ]; then
      local icon
      icon="$("${timeout_command[@]}" cat -- "${msgcollector_run_dir}/${identifier}_icon")"
   fi

   if [ "${icon}" = "" ]; then
      true "${scriptname}: no icon set for this progress bar; using the default info icon."
      local icon="/usr/share/icons/gnome/24x24/status/info.png"
   fi

   if [ -f "${parent_pid_file}" ]; then
      true "parent_pid_file ${parent_pid_file} exists."
   else
      true "parent_pid_file ${parent_pid_file} does not exist."
   fi

   if [ -f "${progress_txt_file}" ]; then
      ## Sometimes there is a race condition preventing the progressbar
      ## being closed. Let's use this alternative approach.
      progress_txt_file_progress="$("${timeout_command[@]}" cat -- "${progress_txt_file}")"
      if [ "${progress_txt_file_progress}" = "100" ]; then
         true "Already 100%. Not even opening progress bar."
         cleanup_self
         exit 0
      fi
   fi

   ## Sanity test for $progress_txt_file_progress, because we use it without
   ## quotes as argument for yad below.
   ## Also matches when $progress_txt_file_progress is unset.
   if [[ "${progress_txt_file_progress}" != *[!0-9]* ]]; then
      true "'${progress_txt_file_progress}' is strictly numeric."
   else
      error "'${progress_txt_file_progress}' is NOT strictly numeric!"
      exit 1
   fi

   if [ ! "${progress_txt_file_progress}" = "" ]; then
      percentage_maybe_add="--percentage ${progress_txt_file_progress}"
   fi

   ## Clean up eventual old progress bar.
   "${timeout_command[@]}" safe-rm --force -- "${fifo}"

   "${timeout_command[@]}" mkfifo -- "${fifo}"

   ## sanity test
   test -p "${fifo}"

   ## Do not use '<' or '>' inside 'yad' progress messages, 'yad' will mess that up.
   ##
   ## Broken:
   ##    yad --auto-kill will result in yad sending a sighup signal upon
   ##    pressing of cancel button.
   ##    Therefore using '--no-buttons'.
   ## '${percentage_maybe_add}' below is DELIBERATELY UNQUOTED: it is
   ## either EMPTY or the two words '--percentage <n>'. It must be
   ## word-split either way.
   # shellcheck disable=SC2086
   yad \
      --timeout "86400" \
      --no-markup \
      --auto-kill \
      --auto-close \
      --no-buttons \
      --window-icon "${icon}" \
      --title="${progressbartitlex}" \
      --progress \
      ${percentage_maybe_add} \
      --text "${message}" \
      < "${fifo}" & yad_progress_pid="$!"

   exec 3>"${fifo}"

   printf '%s\n' "${yad_progress_pid}" > "${yad_progress_pid_file}"

   while true "$0: main loop"; do
      true "$0: main loop started"
      if [ -f "${progress_txt_file}" ]; then
         true "$0: progress_txt_file exists"
         ## Sometimes there is a race condition preventing the progressbar
         ## being closed. Let's use this alternative approach.
         progress_txt_file_progress="$("${timeout_command[@]}" cat -- "${progress_txt_file}")"
         if [ "${progress_txt_file_progress}" = "100" ]; then
            break
         fi
      fi
      ## Alternatively (most cases) waiting for SIGHUP.
      ## Happens when 'yad' reached 100% or when cancel button is pressed.
      ## This will call the output_sighup function.

      if kill -0 -- "${yad_progress_pid}" &>/dev/null; then
         true "$0: loop: yad still running: yes"
      else
         true "$0: loop: yad still running: no"
         break
      fi

      light_sleep "2"
   done
}

trap "error_handler" ERR
trap "output_sighup" SIGHUP
trap "output_sigterm" SIGTERM
trap "output_sigint" SIGINT

source "${MSGCOLLECTOR_REPO:-}/usr/libexec/msgcollector/msgcollector_shared"
## sets: ${msgcollector_run_dir}
folder_init

## provides: msgcollector_check, is_whole_number
source "${MSGCOLLECTOR_REPO:-}/usr/libexec/msgcollector/check"

# shellcheck source=../../../../helper-scripts/usr/libexec/helper-scripts/has.sh
source "${HELPER_SCRIPTS_PATH:-}/usr/libexec/helper-scripts/has.sh"

source "${MSGCOLLECTOR_REPO:-}/usr/libexec/msgcollector/msgfallbacks"
fallbacks ## provided by /usr/libexec/msgcollector/msgfallbacks
parse_cmd_options "$@"

msgcollector_check "${identifier}" || exit 1
msgcollector_check "${progressbaridx}" || exit 1

preparation
start_progress_bar
cleanup_self
