diff --git a/scripts/setup/machine-setup/lib/system.sh b/scripts/setup/machine-setup/lib/system.sh index 2e1ecccc..20ca708a 100644 --- a/scripts/setup/machine-setup/lib/system.sh +++ b/scripts/setup/machine-setup/lib/system.sh @@ -355,6 +355,89 @@ EOF sysctl -q -w "fs.inotify.max_user_instances=${INOTIFY_INSTANCES}" } +# ----------------------------------------------------------------------------- +# Sleep and suspend +# ----------------------------------------------------------------------------- +# +# A server that suspends is a server that is off. The machine stops answering, +# and on a box with no keyboard attached there is nothing to wake it — which is +# the whole failure: it looks like a crash, and the only fix is physical. +# +# Two independent mechanisms, and both have to be dealt with: +# +# the sleep targets what suspend/hibernate hang off. Masking them means +# nothing can trigger a sleep, including a stray +# `systemctl suspend` +# logind's handlers what closing a lid, pressing the power button or going +# idle DO. These are what a desktop image sets, and they +# act before anything reaches a target +# +# Written as a drop-in rather than by editing logind.conf in place, so what this +# script set is one file that can be read or deleted on its own. + +SLEEP_TARGETS=(sleep.target suspend.target hibernate.target hybrid-sleep.target) +LOGIND_DROPIN=/etc/systemd/logind.conf.d/99-machine-setup.conf + +# The value actually in force for a logind setting, or empty for the default. +# Drop-ins override the main file and later ones override earlier, so the last +# match wins — reading only logind.conf would miss a setting made by a drop-in +# and report the machine as unconfigured when it is not. +logind_effective() { + local key="$1" + { + [[ -r /etc/systemd/logind.conf ]] && grep -hE "^${key}=" /etc/systemd/logind.conf + for f in /etc/systemd/logind.conf.d/*.conf; do + [[ -r "$f" ]] && grep -hE "^${key}=" "$f" + done + } 2>/dev/null | tail -1 | cut -d= -f2- +} + +sleep_targets_masked() { + local t + for t in "${SLEEP_TARGETS[@]}"; do + [[ "$(systemctl is-enabled "$t" 2>/dev/null)" == "masked" ]] || return 1 + done +} + +# What the machine should be set to. RuntimeDirectorySize is deliberately NOT +# here: the original set it to 10% alongside these, which is both unrelated to +# sleeping — it is the size of /run — and systemd's own default, so the line +# never did anything. +logind_wanted() { + cat <<'EOF' +HandleLidSwitch=ignore +HandleLidSwitchExternalPower=ignore +HandleLidSwitchDocked=ignore +HandlePowerKey=ignore +IdleAction=none +EOF +} + +# Is every wanted setting already in force? +logind_is_configured() { + local line key value + while IFS= read -r line; do + key="${line%%=*}" + value="${line#*=}" + [[ "$(logind_effective "$key")" == "$value" ]] || return 1 + done < <(logind_wanted) +} + +disable_sleep() { + systemctl mask "${SLEEP_TARGETS[@]}" >/dev/null 2>&1 + + mkdir -p "$(dirname "$LOGIND_DROPIN")" + { + echo "# Written by machine-setup: this machine is a server and must not sleep." + echo "[Login]" + logind_wanted + } >"$LOGIND_DROPIN" + + # Only restart when something actually changed — a needless restart of logind + # disturbs live sessions, and this step runs on every pass. + systemctl restart systemd-logind +} + # ----------------------------------------------------------------------------- # Timezone # ----------------------------------------------------------------------------- diff --git a/scripts/setup/machine-setup/machine-setup.sh b/scripts/setup/machine-setup/machine-setup.sh index 95422092..e98fcfb8 100755 --- a/scripts/setup/machine-setup/machine-setup.sh +++ b/scripts/setup/machine-setup/machine-setup.sh @@ -617,13 +617,63 @@ if ! skip; then step_ok fi +# ============================================================================= +# 12. Sleep and suspend +# ============================================================================= + +step "Sleep and suspend" +# A laptop should sleep. Disabling it on the machine somebody carries around is +# how you get a hot bag and a flat battery, so dev is skipped — visibly. +if ! skip && is_role dev; then + echo "" + info "Sleep and suspend — left alone on a ${MACHINE_ROLE} machine" + echo " Suspending is what a machine you carry should do. Only a server has" + echo " to be stopped from it." + SUMMARY+=("Sleep: left alone on ${MACHINE_ROLE}") + step_ok +elif ! skip; then + echo "" + info "Sleep and suspend — stop this machine putting itself to sleep" + echo " A server that suspends is a server that is off: it stops answering," + echo " and with no keyboard attached there is nothing to wake it. It looks" + echo " like a crash and the fix is a physical visit." + echo "" + echo " sleep targets: $(sleep_targets_masked && echo 'masked' || echo 'available')" + echo " lid closed: $(logind_effective HandleLidSwitch || echo 'default (suspend)')" + echo " idle: $(logind_effective IdleAction || echo 'default (ignore)')" + echo " power button: $(logind_effective HandlePowerKey || echo 'default (power off)')" + + if [[ "$IS_WSL" == true ]]; then + echo " WSL has no logind and cannot suspend — nothing to do" + SUMMARY+=("Sleep: not applicable under WSL") + elif sleep_targets_masked && logind_is_configured; then + echo " already configured, nothing to do" + SUMMARY+=("Sleep: already disabled") + else + echo "" + echo " This masks the four sleep targets and tells logind to ignore a" + echo " closed lid, an idle session and the power button. Note the last" + echo " one: after this, physically pressing power does nothing, so a" + echo " clean shutdown is 'sudo poweroff' rather than the button." + if confirm "Proceed?"; then + disable_sleep + ok "sleep disabled, logind reloaded" + SUMMARY+=("Sleep: disabled (targets masked, logind handlers ignored)") + else + warn "skipped by request" + SUMMARY+=("Sleep: SKIPPED by request") + fi + fi + step_ok +fi + # ============================================================================= # NOT PORTED YET # ============================================================================= # # Sections still to move across from scripts/setup-old/setup-ubuntu.sh, in order: # -# auto-suspend · boot-hang fix · user creation · +# boot-hang fix · user creation · # ssh keys · ssh hardening · dns · static ip · fail2ban · unattended-upgrades · # git config · docker · zsh + prompt · tailscale · neovim · js runtimes · # dev tools · ufw · zshrc