Stopping a System with SSH Clients from Sleeping
All it used to take was write a simple script in /etc/pm/sleep.d/ and have it exit non-zero if there were SSH clients. Things are now different with systemd.
1 Towards a Different Approach
I realised looking at the logs that this had come into the hands of systemd and this took me then to the systemd-sleep
man page referring to /lib/systemd/system-sleep/
. While I thought scripts in this directory would be treated the same way as those in /etc/pm/sleep.d/
, it appears not to be the case. All the man page suggests is that sleeping will not happen until the scripts have finished running, which is a different behaviour from refusing to sleep if one of them exits non-zero. As it turns out, the same man page suggested one shouldn't use this directory anyway but instead resort to the inhibitor interface.
2 The Inhibitor Interface
The idea is to use the systemd-inhibit
command, passing it a command line. Until this command line has completed, systemd-inhibit
will hold a lock preventing the system from going to sleep. Now while it would have been straightforward to have the system check whether or not there are connected SSH clients every time it intends to sleep, I found it more convoluted to acquire a lock when an SSH client connects.
3 A Solution Around a Python Script
As always when I start writing what becomes more than a one-liner in a shell script, I end up deciding that I'm better off doing it in Python. I wrote the sshinhibit
script which runs systemd-inhibit
when logging in if it doesn't run already and kills it when logging out if it's the last SSH client that's about to hang up.
Add the following line to your
~/.bashrc
(if you use bash):Since you don't want to runif [ -n $SSH_TTY ]; then sudo sshinhibit login; fi
sshinhibit
every time you open a terminal, make sure your shell does so only for SSH connections.For the same reason, add the following line to your
~/.bash_logout
(if you still use bash):if [ -n $SSH_TTY ]; then sudo sshinhibit logout; fi
An unprivileged user cannot take a
block
lock and you may want to runsshinhibit
with sudo. To make this possible without each time being asked for a password, you'll need to add a file to/etc/sudoers.d/
looking like this, assumingINHIBITORS
is a user alias comprising people allowed to acquire locks:INHIBITORS ALL = NOPASSWD: /usr/bin/sshinhibit
This is a broken down listing of the
sshinhibit
script (the complete version lives on GitHub):import sys import subprocess import daemon import psutil
A few imports to begin with, with a few non-standard modules to annoy the reader. Nothing you can't find in the standard repositories, however.
inhibit = ['systemd-inhibit', '--what=idle:sleep:shutdown:handle-lid-switch', 'sleep', 'infinity']
This
systemd-inhibit
command line will be used for running it, for finding out whether or not it's already running and for killing it when passed topkill
. By default,systemd-inhibit
acts upon theidle
,sleep
andshutdown
operations. I just wanted to add thehandle-lid-switch
to prevent sleeping if the lid of a laptop is shut. The idea is to havesystemd-inhibit
runsleep infinity
so that it holds the lock forever. Interestingly, theinfinity
argument of thesleep
command is undocumented in its man page but does actually work.if sys.argv[1] == 'login': for proc in psutil.process_iter(): if inhibit == proc.cmdline(): return print "Starting inhibitor" with daemon.DaemonContext(): subprocess.call(inhibit)
In its first mode of operation –
login
– intended for when you open the SSH connection,sshinhibit
makes suresystemd-inhibit
isn't already running. If not, it starts it in a daemon. That's important to ensure it's not attached to the terminal, whereby a mere accidental Ctrl C could terminate it.elif sys.argv[1] == 'logout': terms = 0 for usr in psutil.users(): if usr.host != 'localhost': terms += 1 if terms > 1: return print "Stopping inhibitor" subprocess.call(['pkill', '-f', ' '.join(inhibit)])
In its second mode of operation –
logout
– intended for when you close the SSH connection,sshinhibit
makes sure the disconnecting client is the last one. If so, it kills the inhibitor.