#!/usr/bin/env bash
# ============================================================================================================================================ #
#: Title           : systemd-vboxinit                                                                                                          #
#: Sypnosis        : systemd-vboxinit OPTIONS                                                                                                  #
#: Date Created    : Wed Oct 02 07:57:25 2013 +0800  /  Wed Oct 02 03:11:58 2013 UTC                                                           #
#: Last Edit       : Tue Apr 15 08:50:59 2025 +0800  /  Tue Apr 15 00:50:59 2025 UTC                                                           #
#: License         : GPLv3                                                                                                                     #
#: Version         : 2.2.1                                                                                                                     #
#: Author          : Jason V. Ferrer '<jason.ferrer@gmail.com>'                                                                                #
#: Description     : Enable automatic startup of virtual machine sessions during system boot and save their state upon host shutdown or halt.  #
#: Options         : --about|-a, --stop|-x, --start|-s, --disable|-d, --license|-l, --help, -h                                                 #
#: Home Page       : https://github.com/Jetchisel/VBoxAutostart                                                                                #
#: Copyright       : Jason V. Ferrer 2013-2025                                                                                                 #
#: ExtComm         : cat,find,grep,less,pgrep,systemctl,VBoxManage                                                                             #
#: TODO            : KillAllVBoxProcess to find/terminate VBox daemons without relying on pgrep. Avoid hardcoding VBoxDir=/usr/lib/virtualbox  #
# ============================================================================================================================================ #

# ============================================================================================================================================ #
#                                        'Set shell option so RunningUuids can be tested in one-line.'                                         #
# ============================================================================================================================================ #

shopt -s extglob

# ============================================================================================================================================ #
#                          'Warn and die functions, for exit messages and default status or an optional exit status.'                          #
# ============================================================================================================================================ #

warn() {
  printf '%s\n' "${BASH_SOURCE##*/}: $*" ##: Print message to stdout by default.
}

die() {
  local st=$? ##: Assign the last exit command to the var st.
  case $2 in  ##: Test against a pattern for the value of `$2'
    *[^0-9]*|'') :;; ##: If the value of `$2' is not a digit. `+([!0-9])' or empty.
    *) st=$2;; ##: Save the value of `$2' in st if value is digit.
  esac

  case $st in ##: Test against a pattern for the value of `$st'
    0) warn "$1" ;;  ##: If value of `$st' is 0 then print message to stdout.
    *) warn "$1" >&2;; ##: If value of `$st' is not zero then print message to `stderr'.
  esac

  exit "$st"  ##: Exit with the status of the recent command or an optional given status.
}

# ============================================================================================================================================ #
#                               'One argument only exit immediately and avoid running the script until the end.'                               #
# ============================================================================================================================================ #

(( $# > 1 )) && die "Too many arguments, try --help" 1

# ============================================================================================================================================ #
#                                        'Check if VirtualBox is installed if not exit with an error.'                                         #
# ============================================================================================================================================ #

NotInstalledMessage="VBoxManage is either not installed or it's not in your PATH!"

if ! type -P VBoxManage >/dev/null; then
  if ! type -P vboxmanage >/dev/null; then
    [[ -f /usr/lib/virtualbox/VBoxManage ]] || die "$NotInstalledMessage" 127
  fi
fi

# ============================================================================================================================================ #
#                                'Check for the required app/executable is with in your PATH, exit otherwise.'                                 #
# ============================================================================================================================================ #

Missing=()
ExtComm=(cat find grep less pgrep systemctl VBoxManage)
MissingMessage="is either not installed or it is not in your PATH!"
ExitMessage="Please install the following: "

for apps in "${ExtComm[@]}"; do
  if ! type -P "$apps" >/dev/null; then
    printf '%s %s\n' "$apps" "$MissingMessage" >&2
    Missing+=("$apps")
  fi
done

(( ${#Missing[*]} )) && die "${ExitMessage}[${Missing[*]}] exiting now!" 127

# ============================================================================================================================================ #
#                                    'Check if vboxdrv kernel module is loaded if not exit with an error.'                                     #
# ============================================================================================================================================ #

grep -q "^vboxdrv" /proc/modules || die "vboxdrv is not loaded!"

# ============================================================================================================================================ #
#                                  'Assign VirtualBox directory /usr/lib/virtualbox to the variable VBoxDir.'                                  #
# ============================================================================================================================================ #

VBoxDir=/usr/lib/virtualbox

# ============================================================================================================================================ #
#                         'Use the VBoxManage in /usr/lib/virtualbox if it exist, else use whatever is in your PATH.'                          #
# ============================================================================================================================================ #

VBoxManage() {
  if [[ -f $VBoxDir/VBoxManage && -x $VBoxDir/VBoxManage ]]; then
    "${VBoxDir}"/VBoxManage "$@"
  else
    if type -P VBoxManage >/dev/null; then
      command -p VBoxManage "$@"
    else
      command -p vboxmanage "$@"
    fi
  fi
}

# ============================================================================================================================================ #
#                                                           'VBoxManage Functions.'                                                            #
# ============================================================================================================================================ #

ExtraData() {
  VBoxManage getextradata "$AllUuid" 'pvbx/startupMode'
}

ListRunningVms() {
  VBoxManage list runningvms
}

ListVms() {
  VBoxManage list vms
}

StartVms() {
  VBoxManage startvm "$AllUuid" --type headless
}

SaveVms() {
  VBoxManage controlvm "$AllUuid" savestate
}

# ============================================================================================================================================ #
#                                  'Function to check for vms. If there is none found exit without an error.'                                  #
# ============================================================================================================================================ #

NoVmExit() {
  mapfile -u5 -t AllVms 5< <(ListVms)
  (( ${#AllVms[*]} )) || die "No virtual machine found!" 0
}

# ============================================================================================================================================ #
#                        'Put the running/sanitized vms uuids in an array and format it in the variable RunningUuids.'                         #
# ============================================================================================================================================ #

uuids=()
while IFS= read -u6 -r vm; do
  running_uuid=${vm##*"{"}
  uuids+=("${running_uuid%"}"*}")
done 6< <(ListRunningVms)

# RunningUuids=$(IFS='|'; printf '%s' "@(${uuids[*]})")
RunningUuids=$(IFS='|'; printf '@(%s)' "${uuids[*]}")

# ============================================================================================================================================ #
#                        'Function to sanitize/extract the vmname and uuid using P.E. inside the start,stop function.'                         #
# ============================================================================================================================================ #

ExtractVmNameUuid() {
  AllUuid=${line##*"{"}
  AllUuid=${AllUuid%"}"*}
  VmName=${line#*'"'}
  VmName=${VmName%'"'*}
}

# ============================================================================================================================================ #
#                                  'Function to print the vms status when started, e.g. running,auto,noauto.'                                  #
# ============================================================================================================================================ #

VmStatus() {
  local i j k
  if (( ${#RunningVms[*]} )); then
    for i in "${!RunningVms[@]}"; do
      printf '%s\n' "Machine '${RunningVms[i]}' is already running..."
    done
  fi

  if (( ${#NoAutoRunning[*]} )); then
    for j in "${!NoAutoRunning[@]}"; do
      printf '%s\n' "Machine '${NoAutoRunning[j]}' is already running but not on auto..."
    done
  fi

  if (( ${#NoAutoVms[*]} )); then
    for k in "${!NoAutoVms[@]}"; do
      printf '%s\n' "Machine '${NoAutoVms[k]}' is not set to auto..."
    done
  fi
}

# ============================================================================================================================================ #
#                                                    'Function to start the vms Headless.'                                                     #
# ============================================================================================================================================ #
# shellcheck disable=SC2053
start() {
  NoVmExit
  local line IFS
  declare -a NoAutoVms RunningVms NoAutoRunning

  while IFS= read -u7 -r line; do
    ExtractVmNameUuid
    if [[ "$(ExtraData)" == *" auto" && $AllUuid != $RunningUuids ]]; then
      printf '\n%s\n' "Starting Machine '$VmName'..."
      StartVms
    elif [[ "$(ExtraData)" != *" auto" && $AllUuid == $RunningUuids ]]; then
      NoAutoRunning+=("$VmName")
    elif [[ "$(ExtraData)" == *" auto" && $AllUuid == $RunningUuids ]]; then
      RunningVms+=("$VmName")
    elif [[ "$(ExtraData)" != *" auto" ]]; then
      NoAutoVms+=("$VmName")
    fi
  done 7< <(ListVms)

  VmStatus
}

# ============================================================================================================================================ #
#                   'Function to print/list files/executables (without extension) inside "$VBoxDir" (/usr/lib/virtualbox).'                    #
# ============================================================================================================================================ #

VBoxDaemons() {
  find "$VBoxDir" -type f -iname '*v*box*' \! -name '*.*' -print
}

# ============================================================================================================================================ #
#                                             'Function to stop/kill all running vbox processes.'                                              #
# ============================================================================================================================================ #

KillAllVBoxProcess() {
  local entry pid daemon daemon_name is_running IFS
  declare -A pids

  while IFS= read -ru9 daemon; do
    daemon_name=${daemon##*/}
    while IFS= read -ru8 pid; do
      [[ -n "$pid" ]] && pids["$daemon_name"]="$pid"
    done 8< <(pgrep -u "$LOGNAME" -x -- "$daemon_name")
  done 9< <(VBoxDaemons)

  for entry in "${!pids[@]}"; do
    printf 'Stopping process %s (PID: %d) with SIGTERM... ' "$entry"  "${pids[$entry]}"
    ##: If the daemon was stopped already print the modified error message, and skip it!
    is_running=$(kill -15 "${pids[$entry]}" 2>&1) || {
      printf '%s (PID: %s ' "$entry" "${is_running##*\(}" &&
      continue
    }
    # sleep 1 ##: Using sleep will give an error `No such process`
    #: `kill -9` would be BRUTAL and NO MERCY!!! but necessary for an update/reinstall of VirtualBox/rebuild of the kernel module.
     kill -0 "${pid[$entry]}" 2>/dev/null && {
       printf 'Force killing process %s (PID: %d) with SIGKILL...\n' "$entry" "${pids[$entry]}"
       kill -9 "${pids[$entry]}"
     }
    printf '%s stopped gracefully.\n' "$entry"
  done
}

# ============================================================================================================================================ #
#                                      'Function to save vms state instead of shutting down completely.'                                       #
# ============================================================================================================================================ #

stop() {
  local line
  (( ${#uuids[*]} )) || die "No virtual machine runnning!" 0
  while IFS= read -u8 -r line; do
    ExtractVmNameUuid
    printf '%s\n' "Saving machine '$VmName' state..."
    SaveVms
  done 8< <(ListRunningVms)

  if ! systemctl is-active vboxdrv.service >/dev/null; then
    KillAllVBoxProcess
  fi
}

# ============================================================================================================================================ #
#                                                     'Assign UpdateMessage in an array.'                                                      #
# ============================================================================================================================================ #

InstallMessage="
All VirtualBox process that is owned by $LOGNAME has been stopped.
You can now do the following:
  • Install/reinstall VirtualBox (Update to the latest or install an old_version.)
  • Rebuild the VirtualBox kernel modules."

# ============================================================================================================================================ #
#                         'Function to save-state of the runningvms & stop vbox daemons in preparation for an update.'                         #
# ============================================================================================================================================ #

disable() {
  local line IFS

  if (( ${#uuids[*]} )); then
    while IFS= read -u8 -r line; do
      ExtractVmNameUuid &&
      printf '%s\n' "Saving machine '$VmName' state..." &&
      SaveVms
    done 8< <(ListRunningVms)
  else
    printf 'No virtual machine running!\n' >&2
  fi
  printf 'Trying to stop all VBox daemons...\n' &&
  KillAllVBoxProcess &&
  printf '%s\n' "$InstallMessage"
}

# ============================================================================================================================================ #
#                                                              'Usage Function.'                                                               #
# ============================================================================================================================================ #

help() {
  cat <<EOF

  Usage: ${BASH_SOURCE##*/} OPTION

  Options:
  -s, --start    Start enabled virtual machines otherwise show the state.
  -x, --stop     Save the state of all running virtual machines enabled or not.
  -d, --disable  Like stop but also stops the vbox daemons, useful before a vbox update.
  -h, --help     Show this help.
  -a, --about    A brief info.
  -l, --license  Show license.

EOF
return
}

# ============================================================================================================================================ #
#                                                              'About function.'                                                               #
# ============================================================================================================================================ #

about() {
  cat <<EOF

                         Systemd-vboxinit

  Copyright (C) 2013-2025 Jason V. Ferrer '<jason.ferrer@gmail.com>'

  Auto start  sessions when  booting and save sessions  when host is
  stopped, using systemd as its start up daemon.

  This  program is  free software;  you can  redistribute  it and/or
  modify  it  under the  terms  of  the  GNU  General Public License
  version 3 as published by the Free Software Foundation.

  This  program  is distributed  in the hope that it will be useful,
  but  WITHOUT ANY WARRANTY;  without even  the  implied warranty of
  MERCHANTABILITY or FITNESS FOR  A  PARTICULAR PURPOSE. See the GNU
  General Public License for more details.

  You should  have received a copy of the GNU General Public License
  (see The LICENSE file.) along  with this program; if not, write to
  the Free Software Foundation, Inc. 51 Franklin Street, Fifth Floor
  Boston, MA  02110-1301, USA.

EOF
return
}

# ============================================================================================================================================ #
#                                         'License function.  (required by GPL, see the LICENSE file)'                                         #
# ============================================================================================================================================ #

license() {
  lisensya=/usr/share/doc/packages/systemd-vboxinit/LICENSE
  [[ -f $lisensya ]] || die "Can't find license file: $lisensya" 1
  less /usr/share/doc/packages/systemd-vboxinit/LICENSE || return 1
  return
}

# ============================================================================================================================================ #
#                                                      'Check for a command line option.'                                                      #
# ============================================================================================================================================ #

case $1 in
  --about|-a) about
    ;;
  --disable|-d) disable
    ;;
  --help|-h) help; exit 0
    ;;
  --start|-s) start
    ;;
  --stop|-x) stop
    ;;
  --license|-l) license
    ;;
  *) help >&2; exit 1
    ;;
esac

# ============================================================================================================================================ #
#                                                       '>> END OF SYSTEMD-VBOXINIT <<'                                                        #
# ============================================================================================================================================ #
