Unverified Commit 7c9d2b1f authored by Kroese's avatar Kroese Committed by GitHub
Browse files

feat: Improved start and stop logic (#1178)

parent b68d243b
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -66,5 +66,5 @@ jobs:
          level: warning
          fail_level: error
          reporter: github-pr-review
          shellcheck_flags: -x -e SC2001 -e SC2034 -e SC2064 -e SC2317 -e SC2153 -e SC2028
          shellcheck_flags: -x -e SC1091 -e SC2001 -e SC2034 -e SC2064 -e SC2317 -e SC2153 -e SC2028
          github_token: ${{ secrets.GITHUB_TOKEN }}
+1 −0
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ set -Eeuo pipefail

DEF_OPTS="-nodefaults -boot strict=on"
RAM_OPTS=$(echo "-m ${RAM_SIZE^^}" | sed 's/MB/M/g;s/GB/G/g;s/TB/T/g')
MON_OPTS="-name $PROCESS,process=$PROCESS,debug-threads=on -pidfile $QEMU_PID"
CPU_OPTS="-cpu $CPU_FLAGS -smp $CPU_CORES,sockets=1,dies=1,cores=$CPU_CORES,threads=1"
MAC_OPTS="-machine type=q35,smm=off,usb=off,vmport=off,dump-guest-core=off,hpet=off${KVM_OPTS}"
DEV_OPTS="-device virtio-balloon-pci,id=balloon0,bus=pcie.0,addr=0x4"
+13 −9
Original line number Diff line number Diff line
@@ -24,19 +24,23 @@ cd /run

trap - ERR

version=$(qemu-system-x86_64 --version | head -n 1 | cut -d '(' -f 1 | awk '{ print $NF }')
cmd=(qemu-system-x86_64)
version=$("${cmd[@]}" --version | awk 'NR==1 { print $4 }')
info "Booting $APP using QEMU v$version..."

if [[ "$CONSOLE" == [Yy]* ]]; then
  exec qemu-system-x86_64 ${ARGS:+ $ARGS}
if [[ "$SHUTDOWN" != [Yy1]* ]]; then
  exec "${cmd[@]}" ${ARGS:+ $ARGS}
fi

{ qemu-system-x86_64 ${ARGS:+ $ARGS} >"$QEMU_OUT" 2>"$QEMU_LOG"; rc=$?; } || :
(( rc != 0 )) && error "$(<"$QEMU_LOG")" && exit 15
if [ ! -t 1 ] || [ ! -c /dev/tty ]; then
  "${cmd[@]}" ${ARGS:+ $ARGS} &
else
  "${cmd[@]}" ${ARGS:+ $ARGS} </dev/tty >/dev/tty &
fi

terminal
tail -fn +0 "$QEMU_LOG" --pid=$$ 2>/dev/null &
cat "$QEMU_TERM" 2>/dev/null & wait $! || :
rc=0
wait $! || rc=$?
[ -f "$QEMU_END" ] && exit "$rc"

sleep 1 & wait $!
[ ! -f "$QEMU_END" ] && finish 0
finish "$rc"
+6 −16
Original line number Diff line number Diff line
@@ -198,9 +198,7 @@ configureDNS() {

getHostPorts() {

  local list=""
  list+="$MON_PORT,"
  list+="${HOST_PORTS// /},"
  local list="${HOST_PORTS// /},"

  # Remove duplicates
  list=$(echo "${list//,,/,}," | awk 'BEGIN{RS=ORS=","} !seen[$0]++' | sed 's/,*$//g')
@@ -590,11 +588,8 @@ configureNAT() {

closeBridge() {

  [ -s "$PASST_PID" ] && pKill "$(<"$PASST_PID")"
  rm -f "$PASST_PID"

  [ -s "$DNSMASQ_PID" ] && pKill "$(<"$DNSMASQ_PID")"
  rm -f "$DNSMASQ_PID"
  local pids=( "$PASST_PID" "$DNSMASQ_PID" )
  mKill "${pids[@]}"

  ip link set "$VM_NET_TAP" down promisc off &> /dev/null || :
  ip link delete "$VM_NET_TAP" &> /dev/null || :
@@ -608,14 +603,8 @@ closeBridge() {

closeWeb() {

  # Shutdown nginx
  nginx -s stop 2> /dev/null
  fWait "nginx"

  # Shutdown websocket
  local pid="/var/run/websocketd.pid"
  [ -s "$pid" ] && pKill "$(<"$pid")"
  rm -f "$pid"
  local pids=( "$WEB_PID" "$WSD_PID" )
  mKill "${pids[@]}"

  return 0
}
@@ -632,6 +621,7 @@ closeNetwork() {
  exec 40<&- || true

  closeBridge

  return 0
}

+85 −115
Original line number Diff line number Diff line
#!/usr/bin/env bash
set -Eeuo pipefail

: "${API_TIMEOUT:="50"}"   # API Call timeout
: "${QEMU_TIMEOUT:="50"}"  # QEMU Termination timeout
: "${SHUTDOWN:="Y"}"        # Graceful ACPI shutdown
: "${TIMEOUT:="115"}"       # QEMU termination timeout
: "${API_TIMEOUT:="90"}"    # External API call timeout

# Configure QEMU for graceful shutdown

API_CMD=6
API_HOST="127.0.0.1:$COM_PORT"

QEMU_TERM=""
QEMU_LOG="$QEMU_DIR/qemu.log"
QEMU_OUT="$QEMU_DIR/qemu.out"
QEMU_END="$QEMU_DIR/qemu.end"
# Configure QEMU for graceful shutdown

if [[ "$KVM" == [Nn]* ]]; then
  API_TIMEOUT=$(( API_TIMEOUT*2 ))
  QEMU_TIMEOUT=$(( QEMU_TIMEOUT*2 ))
fi
QEMU_END="$QEMU_DIR/qemu.end"

_trap() {
  local func="$1"; shift
  local sig
  TRAP_PID=$BASHPID

  for sig; do
    trap "$func $sig" "$sig"
  done
}

app() {
  echo "$APP" && return 0
}

finish() {

  local pid
  local cnt=0
  local i=0
  local pid=""
  local reason=$1
  local pids=( "${HOST_PID:-}" "${WSD_PID:-}" \
               "${WEB_PID:-}" "${PASST_PID:-}" "${DNSMASQ_PID:-}" )

  touch "$QEMU_END"

  if [ -s "$QEMU_PID" ]; then

    pid=$(<"$QEMU_PID")
    echo && error "Forcefully terminating Virtual DSM, reason: $reason..."
    { kill -15 "$pid" || true; } 2>/dev/null

    while isAlive "$pid"; do

      sleep 1
      (( cnt++ ))

      # Workaround for zombie pid
      [ ! -s "$QEMU_PID" ] && break

      if [ "$cnt" -eq 5 ]; then
        echo && error "QEMU did not terminate itself, forcefully killing process..."
        { kill -9 "$pid" || true; } 2>/dev/null
    if read -r pid <"$QEMU_PID"; then
      if [ -n "$pid" ] && isAlive "$pid"; then
        local display="$reason"
        case "$reason" in
          129 ) display="SIGHUP" ;;
          130 ) display="SIGINT" ;;
          131 ) display="SIGQUIT" ;;
          134 ) display="SIGABRT" ;;
          143 ) display="SIGTERM" ;;
        esac
        error "Forcefully terminating $(app), reason: $display..."
        { disown "$pid" || :; kill -9 -- "$pid" || :; } 2>/dev/null
      fi
    fi

    done

  fi

  mKill "${pids[@]}"
  fKill "print.sh"
  fKill "host.bin"

  closeNetwork

  sleep 1
  echo && echo "❯ Shutdown completed!"

  exit "$reason"
}

terminal() {

  local dev=""

  if [ -s "$QEMU_OUT" ]; then

    local msg
    msg=$(<"$QEMU_OUT")

    if [ -n "$msg" ]; then

      if [[ "${msg,,}" != "char"* ||  "$msg" != *"serial0)" ]]; then
        echo "$msg"
      fi

      dev="${msg#*/dev/p}"
      dev="/dev/p${dev%% *}"

    fi
  fi

  if [ ! -c "$dev" ]; then
    dev=$(echo 'info chardev' | nc -q 1 -w 1 localhost "$MON_PORT" | tr -d '\000')
    dev="${dev#*serial0}"
    dev="${dev#*pty:}"
    dev="${dev%%$'\n'*}"
    dev="${dev%%$'\r'*}"
  if ! waitPidFile "$QEMU_PID" 10; then
    warn "Timed out while waiting for $(app) to exit!"
  fi

  if [ ! -c "$dev" ]; then
    error "Device '$dev' not found!"
    finish 34 && return 34
  fi

  QEMU_TERM="$dev"
  return 0
  (( reason != 1 )) && echo && echo "❯ Shutdown completed!"
  exit "$reason"
}

_graceful_shutdown() {
graceful_shutdown() {

  local sig="$1"
  local pid=""
  local code=0
  local pid url response
  local start url response elapsed

  [[ $BASHPID != "$TRAP_PID" ]] && return

  case "$sig" in
    SIGTERM) code=143 ;;
    SIGINT)  code=130 ;;
    SIGHUP)  code=129 ;;
    SIGABRT) code=134 ;;
    SIGINT)  code=130 ;;
    SIGQUIT) code=131 ;;
    SIGABRT) code=134 ;;
    SIGTERM) code=143 ;;
  esac

  if [ -f "$QEMU_END" ]; then
@@ -126,23 +91,22 @@ _graceful_shutdown() {
  fi

  set +e
  start=$SECONDS
  touch "$QEMU_END"
  echo && info "Received $1 signal, sending shutdown command..."

  if [ ! -s "$QEMU_PID" ]; then
    echo && error "QEMU PID file does not exist?"
    finish "$code" && return "$code"
  if [ ! -s "$QEMU_PID" ] || ! read -r pid <"$QEMU_PID"; then
    warn "QEMU PID file ($QEMU_PID) does not exist?"
    finish "$code"
  fi

  pid=$(<"$QEMU_PID")

  if ! isAlive "$pid"; then
    echo && error "QEMU process does not exist?"
    finish "$code" && return "$code"
  if [ -z "$pid" ] || ! isAlive "$pid"; then
    warn "QEMU process with PID $pid does not exist?"
    finish "$code"
  fi

  # Don't send the powerdown signal because vDSM ignores ACPI signals
  # echo 'system_powerdown' | nc -q 1 -w 1 localhost "$MON_PORT" > /dev/null
  # nc -q 1 -w 1 -U "$QEMU_DIR/monitor.sock" &> /dev/null <<<'system_powerdown' || :

  # Send shutdown command to guest agent via serial port
  url="http://$API_HOST/read?command=$API_CMD&timeout=$API_TIMEOUT"
@@ -157,46 +121,52 @@ _graceful_shutdown() {
    response="${response#*message\"\: \"}"
    [ -z "$response" ] && response="second signal"
    echo && error "Forcefully terminating because of: ${response%%\"*}"
    { kill -15 "$pid" || true; } 2>/dev/null
    { kill -15 -- "$pid" || :; } 2>/dev/null

  fi

  local cnt=0
  local cnt=0 abort=0 factor=3 offset=3 min max name

  while [ "$cnt" -lt "$QEMU_TIMEOUT" ]; do
  [[ "$TIMEOUT" =~ ^[0-9]+$ ]] || TIMEOUT=115
  [ "$TIMEOUT" -ge 15 ] && factor=4 && offset=4
  [ "$TIMEOUT" -ge 30 ] && factor=5 && offset=5
  min=$(( factor + offset + 1 ))
  [ "$TIMEOUT" -lt "$min" ] && TIMEOUT="$min"
  elapsed=$(( SECONDS - start ))
  max=$(( TIMEOUT - offset - elapsed ))
  [ "$max" -lt "$factor" ] && max=$(( factor + 1 ))
  abort=$(( max - factor ))
  name="$(app)"

    ! isAlive "$pid" && break

    sleep 1
    (( cnt++ ))
  while [ "$cnt" -le "$max" ]; do

    [[ "$DEBUG" == [Yy1]* ]] && info "Shutting down, waiting... ($cnt/$QEMU_TIMEOUT)"
    sleep 1 &
    local slp=$!

    ! isAlive "$pid" && break
    # Workaround for zombie pid
    [ ! -s "$QEMU_PID" ] && break

  done

  if [ "$cnt" -ge "$QEMU_TIMEOUT" ]; then
    echo && error "Shutdown timeout reached, aborting..."
    if [ "$cnt" -ne "$abort" ]; then
      if [ "$cnt" -gt 0 ] && [[ "$DEBUG" == [Yy1]* ]]; then
        info "Waiting for $name to shut down... ($cnt/$max)"
      fi
    else
      info "${name^} is still running, sending SIGTERM... ($cnt/$max)"
      { kill -15 -- "$pid" || :; } 2>/dev/null
    fi

  finish "$code" && return "$code"
}

touch "$QEMU_LOG"

MON_OPTS="\
        -pidfile $QEMU_PID \
        -name $PROCESS,process=$PROCESS,debug-threads=on \
        -monitor telnet:localhost:$MON_PORT,server,nowait,nodelay"
    wait $slp
    (( cnt++ ))

if [[ "$CONSOLE" != [Yy]* ]]; then
  done

  MON_OPTS+=" -daemonize -D $QEMU_LOG"
  finish "$code"
}

  _trap _graceful_shutdown SIGTERM SIGHUP SIGINT SIGABRT SIGQUIT
[[ "$SHUTDOWN" != [Yy1]* ]] && return 0
[ -n "${QEMU_TIMEOUT:-}" ] && TIMEOUT="$QEMU_TIMEOUT"

fi
_trap graceful_shutdown SIGTERM SIGHUP SIGABRT SIGQUIT

return 0
Loading