CCCPaste

Proxmox - Disable laptop lid and sleep

#!/usr/bin/env bash
set -euo pipefail

# -----------------------------------------------------------------------------
# disable-lid-sleep-proxmox.sh
#
# Purpose:
#   Configure a Proxmox/Debian host (especially laptops) to ignore the lid switch
#   and prevent any suspend/hibernate when the lid is closed.
#
# What it does:
#   1) Configures systemd-logind to ignore lid close events
#   2) Masks sleep/suspend/hibernate targets (hard stop)
#   3) Disables console blanking via GRUB (consoleblank=0)
#   4) Installs a systemd oneshot service to disable setterm blanking/powersave
#
# Safe features:
#   - Makes backups before editing files
#   - Idempotent: re-running won't duplicate config
#   - Verifies systemd + grub tools exist
#
# Usage:
#   sudo ./disable-lid-sleep-proxmox.sh
#
# Optional:
#   sudo ./disable-lid-sleep-proxmox.sh --undo
# -----------------------------------------------------------------------------

SCRIPT_NAME="$(basename "$0")"
STAMP="$(date +%Y%m%d-%H%M%S)"

require_root() {
  if [[ "${EUID}" -ne 0 ]]; then
    echo "ERROR: Must be run as root. Use: sudo ./${SCRIPT_NAME}"
    exit 1
  fi
}

has_cmd() { command -v "$1" >/dev/null 2>&1; }

backup_file() {
  local f="$1"
  if [[ -f "$f" ]]; then
    cp -a "$f" "${f}.bak.${STAMP}"
    echo "Backup created: ${f}.bak.${STAMP}"
  fi
}

ensure_line_in_file() {
  local file="$1"
  local line="$2"

  mkdir -p "$(dirname "$file")"
  touch "$file"

  if ! grep -Fxq "$line" "$file"; then
    echo "$line" >> "$file"
    echo "Added to $file: $line"
  else
    echo "Already present in $file: $line"
  fi
}

set_or_replace_keyvalue() {
  # Sets key=value in a config file (logind.conf style), uncommenting if needed.
  local file="$1"
  local key="$2"
  local value="$3"

  mkdir -p "$(dirname "$file")"
  touch "$file"

  if grep -Eq "^[#[:space:]]*${key}=" "$file"; then
    sed -i -E "s|^[#[:space:]]*(${key}=).*|\1${value}|" "$file"
    echo "Set $key=$value in $file"
  else
    echo "${key}=${value}" >> "$file"
    echo "Appended $key=$value to $file"
  fi
}

mask_sleep_targets() {
  local targets=(
    sleep.target
    suspend.target
    hibernate.target
    hybrid-sleep.target
  )

  for t in "${targets[@]}"; do
    systemctl mask "$t" >/dev/null 2>&1 || true
    echo "Masked: $t"
  done
}

unmask_sleep_targets() {
  local targets=(
    sleep.target
    suspend.target
    hibernate.target
    hybrid-sleep.target
  )

  for t in "${targets[@]}"; do
    systemctl unmask "$t" >/dev/null 2>&1 || true
    echo "Unmasked: $t"
  done
}

install_dpms_service() {
  local svc="/etc/systemd/system/disable-dpms.service"

  backup_file "$svc"

  cat > "$svc" <<'EOF'
[Unit]
Description=Disable monitor sleep / console blanking on boot

[Service]
Type=oneshot
ExecStart=/usr/bin/setterm -blank 0 -powersave off -powerdown 0

[Install]
WantedBy=multi-user.target
EOF

  systemctl daemon-reload
  systemctl enable --now disable-dpms.service
  echo "Installed & enabled: disable-dpms.service"
}

remove_dpms_service() {
  local svc="/etc/systemd/system/disable-dpms.service"
  if [[ -f "$svc" ]]; then
    systemctl disable --now disable-dpms.service >/dev/null 2>&1 || true
    rm -f "$svc"
    systemctl daemon-reload
    echo "Removed: disable-dpms.service"
  else
    echo "disable-dpms.service not found; skipping."
  fi
}

configure_logind() {
  local f="/etc/systemd/logind.conf"
  backup_file "$f"

  set_or_replace_keyvalue "$f" "HandleLidSwitch" "ignore"
  set_or_replace_keyvalue "$f" "HandleLidSwitchExternalPower" "ignore"
  set_or_replace_keyvalue "$f" "HandleLidSwitchDocked" "ignore"
  set_or_replace_keyvalue "$f" "LidSwitchIgnoreInhibited" "no"

  systemctl restart systemd-logind
  echo "Restarted: systemd-logind"
}

undo_logind() {
  # We don't "restore defaults" perfectly (depends on distro), but we do remove
  # the explicit settings by commenting them out if present.
  local f="/etc/systemd/logind.conf"
  if [[ -f "$f" ]]; then
    backup_file "$f"
    sed -i -E \
      -e 's|^(HandleLidSwitch=)|#\1|' \
      -e 's|^(HandleLidSwitchExternalPower=)|#\1|' \
      -e 's|^(HandleLidSwitchDocked=)|#\1|' \
      -e 's|^(LidSwitchIgnoreInhibited=)|#\1|' \
      "$f"
    systemctl restart systemd-logind
    echo "Commented out lid settings in $f and restarted logind."
  else
    echo "$f not found; skipping."
  fi
}

configure_grub_consoleblank() {
  if ! has_cmd update-grub && ! has_cmd grub-mkconfig; then
    echo "WARNING: Neither update-grub nor grub-mkconfig found; skipping GRUB step."
    return 0
  fi

  local f="/etc/default/grub.d/nolidsleep.cfg"
  mkdir -p /etc/default/grub.d
  backup_file "$f"

  # Ensure the commandline includes consoleblank=0 without duplicating.
  if [[ -f "$f" ]] && grep -q 'consoleblank=0' "$f"; then
    echo "GRUB consoleblank already configured in $f"
  else
    cat > "$f" <<'EOF'
# Keep the local console from blanking (useful on laptop Proxmox installs)
GRUB_CMDLINE_LINUX_DEFAULT="$GRUB_CMDLINE_LINUX_DEFAULT consoleblank=0"
EOF
    echo "Wrote GRUB config snippet: $f"
  fi

  if has_cmd update-grub; then
    update-grub
    echo "Ran: update-grub"
  else
    # Fallback (uncommon on Debian/Proxmox, but harmless)
    grub-mkconfig -o /boot/grub/grub.cfg
    echo "Ran: grub-mkconfig -o /boot/grub/grub.cfg"
  fi
}

undo_grub_consoleblank() {
  local f="/etc/default/grub.d/nolidsleep.cfg"
  if [[ -f "$f" ]]; then
    backup_file "$f"
    rm -f "$f"
    echo "Removed: $f"

    if has_cmd update-grub; then
      update-grub
      echo "Ran: update-grub"
    elif has_cmd grub-mkconfig; then
      grub-mkconfig -o /boot/grub/grub.cfg
      echo "Ran: grub-mkconfig -o /boot/grub/grub.cfg"
    else
      echo "WARNING: No grub update tool found; GRUB not regenerated."
    fi
  else
    echo "$f not found; skipping."
  fi
}

show_status() {
  echo
  echo "---- STATUS CHECKS ----"
  echo "logind.conf lid settings:"
  grep -E '^(#\s*)?(HandleLidSwitch|HandleLidSwitchExternalPower|HandleLidSwitchDocked|LidSwitchIgnoreInhibited)=' \
    /etc/systemd/logind.conf 2>/dev/null || echo "(no matching lines found)"

  echo
  echo "Masked sleep targets:"
  for t in sleep.target suspend.target hibernate.target hybrid-sleep.target; do
    systemctl is-enabled "$t" 2>/dev/null || true
  done

  echo
  echo "DPMS service:"
  systemctl status disable-dpms.service --no-pager -l 2>/dev/null || echo "(service not installed)"

  echo
  echo "GRUB snippet:"
  if [[ -f /etc/default/grub.d/nolidsleep.cfg ]]; then
    cat /etc/default/grub.d/nolidsleep.cfg
  else
    echo "(not present)"
  fi
  echo "-----------------------"
  echo
}

main() {
  require_root

  local mode="apply"
  if [[ "${1:-}" == "--undo" ]]; then
    mode="undo"
  fi

  if [[ "$mode" == "apply" ]]; then
    echo "Applying lid-close ignore + anti-sleep configuration..."
    configure_logind
    mask_sleep_targets
    configure_grub_consoleblank
    install_dpms_service
    show_status
    echo "DONE. Reboot recommended to ensure GRUB consoleblank=0 takes effect:"
    echo "  reboot"
  else
    echo "Undoing configuration..."
    undo_logind
    unmask_sleep_targets
    undo_grub_consoleblank
    remove_dpms_service
    show_status
    echo "DONE. Reboot recommended:"
    echo "  reboot"
  fi
}

main "$@"