From b25be12e834f835649ae7102bfc67923338c5379 Mon Sep 17 00:00:00 2001 From: Daniel Langbein Date: Wed, 22 Nov 2023 15:32:54 +0100 Subject: [PATCH] WIP: service preventing systemd suspend --- examples/signal-handling/sleeping.sh | 12 ++++ examples/signal-handling/sleeping2.sh | 21 +++++++ examples/suspend-impossible.nix | 90 +++++++++++++++++++++++++++ examples/systemd-timer.nix | 76 +++++++++++++++++----- 4 files changed, 185 insertions(+), 14 deletions(-) create mode 100755 examples/signal-handling/sleeping.sh create mode 100755 examples/signal-handling/sleeping2.sh create mode 100644 examples/suspend-impossible.nix diff --git a/examples/signal-handling/sleeping.sh b/examples/signal-handling/sleeping.sh new file mode 100755 index 0000000..9bc6486 --- /dev/null +++ b/examples/signal-handling/sleeping.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +printf '%s%s%s\n' 'START. Can be terminated with "kill -TERM ' "$$" '"' +while :; do + printf '%s\n' 'sleeping 10s' + sleep 10s + printf '%s\n' 'awoke from sleep' +done +printf '%s\n' 'END' + +# When executing `kill -TERM $PID` in another terminal, +# this script terminates instantly. diff --git a/examples/signal-handling/sleeping2.sh b/examples/signal-handling/sleeping2.sh new file mode 100755 index 0000000..1ce30ae --- /dev/null +++ b/examples/signal-handling/sleeping2.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +function handle_sigterm(){ + printf '%s\n' 'Ignored SIGTERM' >&2 +} + +# Handle SIGTERM +trap 'handle_sigterm' TERM + +printf '%s%s%s\n' 'START. Can be terminated with "kill -TERM ' "$$" '"' +while :; do + printf '%s\n' 'sleeping 10s' + sleep 10s + printf '%s\n' 'awoke from sleep' +done +printf '%s\n' 'END' + +# When executing `kill -TERM $PID` in another terminal, +# the command `sleep 10s` continues. +# After it has finished, `Ignored SIGTERM` is printed and the endless loop continues. diff --git a/examples/suspend-impossible.nix b/examples/suspend-impossible.nix new file mode 100644 index 0000000..441b081 --- /dev/null +++ b/examples/suspend-impossible.nix @@ -0,0 +1,90 @@ +{ config, pkgs, lib, ... }: + +# Relevant references: +# How to prevent systemd suspend from stopping any service with a dont-sleep.service. https://askubuntu.com/q/830442/1002706 + +# systemctl show test-block-suspend.service -p TimeoutStopUSec +#=> TimeoutStopUSec=infinity + +# systemctl cat test-block-suspend.service + +# systemctl list-dependencies --before test-block-suspend.service +#=> test-block-suspend.service +#=> ├─shutdown.target +#=> ├─sleep.target +#=> │ ├─systemd-hibernate.service +#=> │ ├─systemd-hybrid-sleep.service +#=> │ ├─systemd-suspend-then-hibernate.service +#=> │ └─systemd-suspend.service +#=> └─suspend.target +#=> └─post-resume.service + +{ + # Alternative approach to adding `conflicts`, `before` and `ExecStop`. + # + # Immediately before entering system suspend, all executables in /usr/lib/systemd/system-sleep/ are run. + # Execution of the sleep action is not continued until all have finished. + # TODO: This happens after my screen got blank, wifi disabled, screen locker activated, etc. +# environment.etc."systemd/system-sleep/test-block-suspend".source = +# pkgs.writeShellScript "test-block-suspend" '' +# set -eu -o pipefail +# if [ "$1" == "pre" ]; then +# while :; do +# systemctl is-active --quiet test-block-suspend.service || exit 0 +# ${pkgs.coreutils}/bin/echo 'Waiting until service has finished ...' +# ${pkgs.coreutils}/bin/sleep 1s +# done +# fi +# ''; + + systemd.services."test-block-suspend" = { + description = "Inhibit suspend"; + + # If suspend is initiated, this service is stopped. + conflicts = [ "sleep.target" ]; + # Start-up of `sleep.target` + # is delayed until this service has finished starting up. + before = [ "sleep.target" ]; + + serviceConfig = { + # `simple`: + # As soon as the main process (defined in the `ExecStart`) is started (forked), + # start-up is considered as finished. + # `notify`: + # The service sends a "READY=1" notification message via `sd_notify` when it has finished starting up. + # Requires `NotifyAccess`. + # See https://www.freedesktop.org/software/systemd/man/latest/systemd-notify.html + Type = "notify"; + NotifyAccess = "main"; + + # To stop this service `ExecStop` is run instead of sending SIGTERM to the main process of the service. + # The command specified by `ExecStop` does never end, + # thus stopping this service takes forever. + # TODO: However, the sleep action continues while `ExecStop` is running. As a reslut, the system suspends. + ExecStop = "${pkgs.coreutils}/bin/sleep infinity"; + TimeoutStopSec = "infinity"; + + # This service executes a shell script as main process. + ExecStart = pkgs.writeShellScript "test-block-suspend-execstart" '' + set -eu -o pipefail + + #handle_sigterm(){ + # #while :; do + # printf '%s\n' 'Ignoring SIGTERM ...' >&2 + # # ${pkgs.coreutils}/bin/sleep 5s + # #done + #} + #trap 'handle_sigterm' TERM + + #printf '%s\n' 'Start-up 30s ...' + #${pkgs.coreutils}/bin/sleep 30s + systemd-notify READY=1 + + while :; do + printf '%s\n' 'Still alive' + ${pkgs.coreutils}/bin/sleep 5s + done + ''; + }; + }; +} diff --git a/examples/systemd-timer.nix b/examples/systemd-timer.nix index 1064cde..d0466c9 100644 --- a/examples/systemd-timer.nix +++ b/examples/systemd-timer.nix @@ -1,19 +1,30 @@ { config, pkgs, ... }: # TODO Note: One can specify ExecStart and ExecStop. Maybe to pause some script during shutdown/suspend? This would be nice for backups. Just finish the current snapshot then pause. -# -# ExecStop= -# Commands to execute to stop the service started via ExecStart=. -# The command that asks the service to stop should wait for it to do so! -# After the commands configured in this option are run, it is implied that the service is stopped, and any processes remaining for it are terminated. -# -# If this option is not specified, the process is terminated when service stop is requested. # TODO: https://unix.stackexchange.com/questions/619987/stop-systemd-service-before-suspend-start-again-after-resume +# Suspend, hibernate, shutdown +# +# Some of the upstream system units: +# https://github.com/NixOS/nixpkgs/blob/5c43dee215c279dceee7861eec85862ad85cc330/nixos/modules/system/boot/systemd.nix#L104 +# - suspend.target +# - hibernate.target +# - sleep.target +# - hybrid-sleep.target +# https://github.com/NixOS/nixpkgs/blob/5c43dee215c279dceee7861eec85862ad85cc330/nixos/modules/system/boot/systemd.nix#L115 +# - reboot.target +# - poweroff.target +# +# BTRFS scrub prevents suspend2ram and proper shutdown +# https://github.com/NixOS/nixpkgs/blob/5c43dee215c279dceee7861eec85862ad85cc330/nixos/modules/tasks/filesystems/btrfs.nix#L131 +# conflicts = [ "shutdown.target" "sleep.target" ]; +# before = [ "shutdown.target" "sleep.target" ]; + # Benefits of Systemd Timers over cron: https://wiki.archlinux.org/title/Systemd/Timers#Benefits -# Example: https://nixos.wiki/wiki/Nix_Cookbook#Creating_periodic_services -# Example: https://nixos.wiki/wiki/Systemd/Timers +# Timer example: https://nixos.wiki/wiki/Nix_Cookbook#Creating_periodic_services +# Timer example: https://nixos.wiki/wiki/Systemd/Timers + { systemd.timers."hello-world" = { # description = "My Timer"; @@ -46,12 +57,19 @@ systemd.services."hello-world" = { # description = "My Oneshot Service"; - # TODO: Prevents suspend2ram or proper shutdown? - # https://github.com/NixOS/nixpkgs/blob/e9b4b56e5a20ac322c0c01ccab7ec697ab076ea0/nixos/modules/tasks/filesystems/btrfs.nix#L128-L130 + # Prevent suspend2ram and proper shutdown. + # + # TODO Additionally: + # Option I: + # Use ExecStart and ExecStop. ExecStop initiates an early stop of the service and waits until this has finished. + # In `serviceConfig`, set `Type` to `simple` (and not `oneshot`, otherwise ExecStop is not used). + # Option II: + # Set `TimeoutSec` to `infinity`. # # If the specified units are started, then this unit is stopped and vice versa. conflicts = [ "shutdown.target" "sleep.target" ]; - # If the specified units are started at the same time as this unit, delay them until this unit has started. + # If this unit and one from `before` are being started, + # the start-up of the latter is delayed until this service has finished starting up. before = [ "shutdown.target" "sleep.target" ]; # https://man.archlinux.org/man/systemd.service.5.en#OPTIONS @@ -60,6 +78,27 @@ # Example: https://github.com/NixOS/nixpkgs/blob/e9b4b56e5a20ac322c0c01ccab7ec697ab076ea0/nixos/modules/tasks/filesystems/btrfs.nix#L132-L142 serviceConfig = { Type = "oneshot"; + + # Takes a boolean value that specifies whether the service shall be considered active even when all its processes exited. + # Defaults to `false`. + #RemainAfterExit = true; + + # A shorthand for configuring both TimeoutStartSec= and TimeoutStopSec= to the specified value. + # TimeoutStopSec: + # If no ExecStop= commands are specified, the service gets the SIGTERM immediately. + # It configures the time to wait for the service itself to stop. + # If it doesn't terminate in the specified time, it will be forcibly terminated by SIGKILL (see KillMode). + # SIGTERM and SIGKILL: + # The SIGTERM signal is a generic signal used to cause program termination. + # Unlike SIGKILL, this signal can be blocked, handled, and ignored. + # It is the normal way to politely ask a program to terminate. + # SIGINT: + # The SIGINT (“program interrupt”) signal is sent when the user types the INTR character (normally C-c). + # Example: + # grow-partition.nix conflicts with `shutdown.target` and has an infinite `TimeoutStopSec` to make sure that resizing a partition is not interrupted. + # https://github.com/NixOS/nixpkgs/blob/008d84ab67c4f4ccbd7c0996005e46c8bd32d675/nixos/modules/system/boot/grow-partition.nix#L34 + #TimeoutSec = "infinity"; + PrivateTmp = true; #User = "myuser"; @@ -69,7 +108,16 @@ # Takes one of the strings realtime, best-effort or idle. IOSchedulingClass = "idle"; - #ExecStart = "${pkgs.python3.withPackages my-python-packages}/bin/netcup-dns"; + #ExecStart = "${pkgs.coreutils}/bin/echo 'Hello World'"; + # Commands to execute to stop the service started via ExecStart=. + # The command that asks the service to stop should wait for it to do so! + # After the commands configured in this option are run, it is implied that the service is stopped, and any processes remaining for it are terminated. + # + # If this option is not specified, the process is terminated when service stop is requested. + #ExecStop = pkgs.writeShellScript "cancel-hello-world" '' + # set -eu -o pipefail + # echo 'Cancelling execution of hello-world' + #''; }; # Packages required for the script. @@ -87,7 +135,7 @@ # Shell commands executed as the service's main process. script = '' set -eu -o pipefail - ${pkgs.coreutils}/bin/echo "Hello World" + ${pkgs.coreutils}/bin/echo 'Hello World' ''; }; }