# Sandmann

Sandmann ([mythical character in European folklore](https://en.wikipedia.org/wiki/Sandman)) is an autosuspend
and wakeup daemon. Sandmann is written in [Scala 3](https://scala-lang.org/).

Sandmann's core functionality is to suspend (or poweroff, see below) the system once it becomes idle. In
addition, Sandmann can schedule wakeups from suspend, typically based on the time the next persistent systemd
timer fires. Furthermore, Sandmann provides a hook interface for custom idle checks.

Similar functionality is provided by projects like systemd's logind and [autsuspend](https://github.com/languitar/autosuspend/).

## Design Principles

Sandmann has the following design principles:
- Follow the principle of least privilege
- Use modern Linux and systemd APIs

Sandmann respects systemd inhibit locks and schedules system wakeups based on persistent systemd timers. The
latter can be used, for example, to periodically update the system if there is a persistent systemd timer that
does so. Please note that the system-update mechanism should ideally hold a systemd inhibit lock while
updating the system.

By default, Sandmann will suspend the system if users are logged in. However, if no user is logged in then
Sandmann will poweroff the system. Scheduled wakeups, typically based on the next time a persistent systemd
timer is scheduled, are setup using either Linux's `CLOCK_REALTIME_ALARM` (to wakeup from suspend) or the
ioctl interface of `/dev/rtcX` (to wakeup from poweroff).

Furthermore, Sandmann mimic's systemd's suspend-then-hibernate mechanism. But without taking the battery
status into consideration, since Sandmann's primary use-case are non-portable systems that do not have a
battery. Unlike, systemd's suspend-then-hibernate, Sandmann can be configured to keep the system suspended
during a time range (typically at daytime) and only hibernate the system at the end of the range or when
within the range.

Sandmann runs as unprivileged user. It requires CAP_WAKE_ALARM (cf. man capabilities(7)) *if* it should be
able to schedule a wakeup from suspend. Furthermore, the ability to suspend and shutdown the system as
unprivileged user is enabled via [polkit rules](./sandmann.polkit.rules).

## Comparison with autosuspend

- Sandmann does not run as root, but user with limited privileges. Those are `CAP_WAKE_ALARM`, write access to
  `/dev/rtc0`, and polkit rules to allow the user to suspend, hibernate, and poweroff the system.
- Sandmann respects systemd inhibit locks and the inhibit locks do not delay the accumulation of idle time.
- Sandmann defaults to powering the system off if no user is logged in.
- Sandmann automatically schedules a wakeup, even from poweroff, based on the next execution of a persistent
  systemd timer.

## License

Sandmann is licensed under the GPL version 3, or any later version (SPDX-License-Identifier:
GPL-3.0-or-later).  version). Some parts are under LGPL version 3, or any later version
(SPDX-License-Identifier: LGPL-3.0-or-later).

## Installation

After cloning the repository and ensuring that the submodules are initialized (`git submodule update --init --recursive`) simply run


```bash
# make full-deploy
```

to install Sandmann.

## Out of Repo Run

You can use the provided `./run` script to run Sandmann directly. However, you may want to pass the `-p`
(pretend) option or otherwise Sandmann may fail when running as unprivileged user without CAP_WAKE_ALARM
trying to set the realtime alarm.

## Systemd Service

Sandmann comes with a systemd service. Simply

```bash
$ systemctl enable --now sandmann
```

to enable and start Sandmann.

## Activity Check Hook Directory

Custom activity checks can be placed in the hook directory `/etc/sandmann/activity-check.d`. The hooks report
to Sandmann if the system is active or idle. To do so, the hooks must invoke either the `sand-report-active`, `sand-report-idle`, or `sand-report-inhibit-idle` binary, which is found the hook's `PATH`. The hooks also must terminate with a zero exit
status.

The example below is a hook written in bash that checks the current block time of a bitcoin node, and reports
that the system is active if the block time is older than two hours:

`*/etc/sandmann/activity-check.d/bitcoin-lag*`:
```bash
#!/usr/bin/env bash
set -e

BITCOIN_RPC_PASSWORD="TODO: Set me"
declare -i BEST_BLOCK_UNIX_TIME
# https://developer.bitcoin.org/reference/rpc/getchaintxstats.html
BEST_BLOCK_UNIX_TIME=$(echo "${BITCOIN_RPC_PASSWORD}" |\
                           bitcoin-cli -stdinrpcpass getchaintxstats |\
                           jq .time)

# bitcoin-cli will return an exit status of 28 if the bitcoin node is
# still verifying the blocks on initial startup.
if [[ $? -eq 28 ]]; then
	sand-report-active
	exit 0
fi

CURRENT_UNIX_TIME=$(date +%s)

LAG_SECONDS=$(( CURRENT_UNIX_TIME - BEST_BLOCK_UNIX_TIME ))

# Two hours
declare -ri MAX_LAG_SECONDS=$(( 60 * 60 * 2 ))

if [[ ${LAG_SECONDS} -gt ${MAX_LAG_SECONDS} ]]; then
    echo "Bitcoin node lag ${LAG_SECONDS}s, which is greater than max lag of ${MAX_LAG_SECONDS}s. Reporting INHIBIT IDLE."
    sand-report-inhibit-idle
else
    echo "Bitcoin node lag ${LAG_SECONDS}s, which is less than max lag of ${MAX_LAG_SECONDS}s. Reporting IDLE."
    sand-report-idle
fi
```

## Configuration

The default configuration file is `/etc/sandmann/sandmann.yaml`. Using no configuration file is often fine, as
Sandmann defaults should be reasonable for many use cases.

### Home (Office) System

The following is an example configuration of a home system. Setting `wakeupTime` to 12:00 ensures that the
system only awakes after 12:00 taking into consideration the randomized delay, and not in the middle of the night which
could be annoying if your home office ins part of your bedroom. We configured a randomized delay so that your update servers of your Linux distribution are not hammered at the same time and that your power grid does not experience a power surge.

Furthermore, the configuration sets the hibernation time range from 21:30 to 07:30. Sandmann will only
schedule a hibernation after `hibernationDelay` if Sandmann suspends the system while the local time is within
that range. If the system is outside the range, then Sandmann will schedule the hibernation at the start of
the hibernation time range (21:30 in the example below).

The hibernate time range feature may seem strange at first. But some systems flash the power light when
suspended, which, again, could be annoying if the system is located in your bedroom.

```yaml
wakeupTime: 12:00
wakeupTimeRandomizedDelay: PT37M
hibernationTimeRange: 21:30..07:30
hibernationDelay: PT10M
```

### Office System

The office system Sandmann configuration may wants to set the `wakeupDaysOfWeek` to weekdays. This has the
effect that the system will not wakeup on the weekend, saving energy. Assume that the system has a persistent
systemd timer for every day at 05:30 to update the system. With the configuration below, the system will wake
up at 05:30 on each weekday and perform the update task.

```yaml
wakeupDaysOfWeek:
  - MONDAY
  - TUESDAY
  - WEDNESDAY
  - THURSDAY
  - FRIDAY
```

### Complete Configuration

Here is an example full `/etc/sandmann/sandmann.yaml`:

```yaml
logLevel: debug
stateDir: "/var/lib/sandmann"
idleDuration: PT30M
checkInterval: PT3M
wakeupTimers:
  - update-system.timer
  - weekly-maintenance.timer
wakeupTime: 12:00
wakeupTimeRandomizedDelay: PT37M
wakeupDaysOfWeek:
  - MONDAY
  - TUESDAY
  - WEDNESDAY
  - THURSDAY
  - FRIDAY
wakeupFromHibernationOrPoweroff: false
hibernationTimeRange: 23:30..08:30
hibernationDelay: PT3H
```

<!-- Local Variables: -->
<!-- fill-column: 110 -->
<!-- End: -->
