Script to Start & Stop Folding@Home when System is Idle

Problem: Folding@Home's built-in idle detection doesn't work for me. So, I wrote a script to start and stop it when X11 is idle. This doesn't work for Wayland.

First we need to replace F@H's terrible init.d script with a reasonable systemd service unit file in /etc/systemd/system/FAHClient.service. Note that it should be named that (with capital letters!) so that it overrides the automatically generated service unit that systemd produces to call the init.d script. This service unit file should help whether you use the idle-detection script or not.

[Unit]
Description=Folding@Home Client
After=graphical.target
Requisite=graphical.target
After=multi-user.target
Requisite=multi-user.target

[Service]
Environment=EXTRA_OPTS=
Environment=ENABLE=true
EnvironmentFile=-/etc/default/fahclient
Type=idle
StateDirectory=fahclient
WorkingDirectory=/var/lib/fahclient
PIDFile=/run/fahclient.pid
PassEnvironment=ENABLE
# Make sure ENABLE is set to true in /etc/defaults/fahclient. This is here
# to emulate the behavior of the original init.d script.
ExecCondition=sh -c '[ "x$ENABLE" = "xtrue" ] || exit 255'
User=fahclient
ExecStartPre=/bin/sleep 10
ExecStart=/usr/bin/FAHClient /etc/fahclient/config.xml --pid-file=/run/fahclient.pid --pid
ExecReload=kill -SIGUSR1 $MAINPID
KillMode=mixed
TimeoutStartSec=20
TimeoutStopSec=10
Restart=on-abnormal
RestartSec=60

[Install]
WantedBy=graphical.target

Our idle-detection script uses the xprintidle command provided by the xprintidle package, so install that.

Then we're going to create a script /opt/fahidle.sh which is our idle-detection service:

#!/bin/bash

IDLE=300 # time in seconds

function have_command {
    # Determine if we have a command/alias/function/bash builtin
    # without running it.
    # || return $? protects us from set -o errexit
    type -t "$1" >/dev/null || return $?
}

function die {
    echo Error: "$@" >&2
    exit 1
}

function find_xauths {
    # Find X11 servers by looking for the -auth argument passed to them.
    # We need this info to connect to them anyway.
    # -Aww causes ps to print out All procs, and not cut off args (-ww)
    # -o args causes ps to only print out argument strings
    # The filename given after -auth is matched & printed by grep
    # -P is PCRE, allowing us to use negative-width lookbehind assertions
    # (?<=)
    # If this fails, it's probably because there's no X11s running,
    # the script will crash and systemd will restart it to try again later
    ps -Aww --no-headers -o args | grep -o -P -- '(?<=-[a]uth )\S+'
}

function check_idle {
    local f
    local dpy
    # Use 2^31-1 as a canary value to make sure we found at least one X server
    local -i int_max=2147483647
    local -i min_idle=int_max
    for f in $( find_xauths )
    do
        # xauth will retrieve the magic cookie X authority file, and tell us
        # what display its for (and how to connect to it)
        # in the first column, whcich we isolate with cut
        for dpy in $( xauth -f "$f" list | cut -d' ' -f 1 )
        do
            # Despite everything I can see online indicating that a 
            # display value of DISPLAY=host/unix:0 should work, it doesn't,
            # so if the display reported by xauth has that format, we just
            # cut it down to something like :0 by removing the */unix prefix.
            export DISPLAY="${dpy##*/unix}"
            export XAUTHORITY="$f"
            local -i idle
            # Use xprintidle to get us the idle time in milliseconds
            if idle="$(xprintidle 2>/dev/null)" ; then
                # Convert milliseconds to seconds
                idle=$(( idle / 1000 ))
                # Record the idle time in seconds of the least idle X server
                if ((idle < min_idle)) ; then
                    min_idle=idle
                fi
            fi
        done
    done
    if (( min_idle > IDLE && min_idle < int_max )) ; then
        # echo "Idle for $min_idle seconds, starting FAHClient"
        systemctl start FAHClient
    else
        # echo "Idle for $min_idle seconds, stopping FAHClient"
        systemctl stop FAHClient
    fi
}

function main {
    set -o nounset
    set -o pipefail
    set -o errexit

    if ! have_command xprintidle ; then
        die "Need xprintidle"
    fi

    if [[ ! -v IDLE ]] ; then
        IDLE=300 # seconds
    fi

    # Every 10 seconds, we check if all the X servers are idle
    while sleep 10 ; do
        check_idle
    done
}

# using a main function forces bash to read the entire file before doing
# anything, which is necessary because bash reads scripts line by line
# otherwise so if the script file changes while its running, strange stuff
# can happen
main "$@"

And a systemd service unit file to activate our idle script in /etc/systemd/system/FAHIdle.service:

[Unit]
Description=Folding@Home Client Idler
After=graphical.target
Requisite=graphical.target
After=multi-user.target
Requisite=multi-user.target

[Service]
Environment=ENABLE=true
EnvironmentFile=-/etc/default/fahclient
Type=idle
PassEnvironment=ENABLE
ExecCondition=sh -c '[ "x$ENABLE" = "xtrue" ] || exit 255'
ExecStart=/opt/fahidle.sh
Restart=always
RestartSec=60

[Install]
WantedBy=graphical.target

Make sure you unpause F@H and set it to run always (not just at idle) before starting FAHIdle. Finally, reload systemd and enable and start FAHIdle.

systemctl daemon-reload
systemctl disable FAHClient
systemctl stop FAHClient
systemctl enable FAHIdle
systemctl start FAHIdle

Remember that if you want to stop folding systemctl disable FAHClient and systemctl stop FAHClient will no longer work properly, because FAHIdle will be starting and stopping it, so systemctl stop FAHIdle or systemctl disable FAHIdle instead.

There are probably better ways to handle some of the things the script needs to do, and if you use Gnome you can use D-BUS instead, which would be much more efficient.