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; fisshinhibitevery 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; fiAn unprivileged user cannot take a
blocklock and you may want to runsshinhibitwith 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, assumingINHIBITORSis a user alias comprising people allowed to acquire locks:INHIBITORS ALL = NOPASSWD: /usr/bin/sshinhibitThis is a broken down listing of the
sshinhibitscript (the complete version lives on GitHub):import sys import subprocess import daemon import psutilA 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-inhibitcommand 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-inhibitacts upon theidle,sleepandshutdownoperations. I just wanted to add thehandle-lid-switchto prevent sleeping if the lid of a laptop is shut. The idea is to havesystemd-inhibitrunsleep infinityso that it holds the lock forever. Interestingly, theinfinityargument of thesleepcommand 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,sshinhibitmakes suresystemd-inhibitisn'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,sshinhibitmakes sure the disconnecting client is the last one. If so, it kills the inhibitor.
