24

I am trying to save power by switching desktops to suspend mode when they are inactive. But many desktops are also accessed via SSH by their owners. There is a wakeonlan solution deployed to enable owners to switch on the machine, but the problem is that machines will auto-suspend again in 10 minutes, even though the SSH connection is on.

What I am trying to do is to include the active SSH sessions in the "activity" definitions.

The question is can it be done via setting a polkit rule? Can it be done via putting a script that runs before the actual suspend, and aborts it if SSH sessions are found? I need a clean legit way of doing this. If not that, then hacky ways are also welcome.

Current naive hacky solution: edit /usr/sbin/pm-suspend:

#check for SSH sessions, and prevent suspending:
if [ "$(who | grep -cv "(:")" -gt 0 ]; then
    echo "SSH session(s) are on. Not suspending."
    exit 1
fi

This serves the purpose. But I don't know when an update will overwrite the file /usr/sbin/pm-suspend. I also don't know how this will work with other suspend implementations like tuxonice.

Bhavin Doshi
  • 2,466

7 Answers7

24

Until Ubuntu 14.10 (Upstart-based)

Have a look at pm-action(8) and search for /etc/pm/sleep.d in section “FILES”. If one of these scripts returns with a non-zero exit status, suspension is prevented.

Updated instructions for clarity:

  1. So create a file /etc/pm/sleep.d/05_ssh_keepawake.

  2. Put a shebang (#!/bin/sh) and the code mentioned in the question in this file.

  3. Set execute permissions on it:

    chmod +x /etc/pm/sleep.d/05_ssh_keepawake
    

Since Ubuntu 15.04 (systemd-based)

systemd doesn’t use pm-utils to manage its power state hooks but has its own infrastructure to the same end. Sleep inhibitor checkers are no longer executed on sleep but must be set by the action that inhibits sleep (see 1).

As such you’d have to add commands to the SSH session log-in and log-out that registers a sleep inhibitor with systemd (e. g. via systemd-inhibit(1)) and later releases the inhibitor. If somebody knows how to hook into SSH log-in and log-out, I’d welcome a comment or edit so that we could work out the relevant steps and commands.

The following section is work in progress – Only use it when you know what you’re doing!

You might be able to write a systemd unit /etc/systemd/system/ssh-inhibit-sleep.service that makes itself a dependency of sleep.target using the RequiredBy option. If your new unit fails (with a non-zero exit status from its invoked process), it will make sleep.target and thus the subsequent sleep action.

[Unit]
Description=Check for running SSH sessions and, if any, inhibit sleep
Before=sleep.target

[Service] Type=oneshot ExecStart=/bin/sh -c '! who | grep -qv "(:0)"'

[Install] RequiredBy=sleep.target

As always you need to activate systemd units for them to take effect:

sudo systemctl enable ssh-inhibit-sleep.service

For more info see systemd.unit(5) and systemd.service(5).

David Foerster
  • 36,264
  • 56
  • 94
  • 147
  • this doesn't seem to work in 18.04. Is this no longer the solution? – Michael Jarret Sep 13 '19 at 23:43
  • 1
    @MichaelJarret: Indeed. I updated my question with some info on the situation with systemd. – David Foerster Sep 15 '19 at 13:23
  • I found the following while trying to sort this out yesterday, but couldn't modify the previous script to do the job here – Michael Jarret Sep 15 '19 at 15:00
  • I can't edit my comment, but also this seems relevant – Michael Jarret Sep 15 '19 at 15:07
  • @MichaelJarret: I noticed that blog post too. Unfortunately this method cannot be used to inhibit sleep as far as I know, only to perform particular operations on sleep and at wake-up, as far as I understand. – David Foerster Sep 16 '19 at 09:12
  • Thanks for the input. I am going to write a daemon for the moment that checks in every so often and to see if an ssh user is connected and registers the sleep inhibitor, if the ssh user is disconnected for, say, 20 minutes, then I'll release the inhibitor. This isn't elegant, so further thoughts appreciated. – Michael Jarret Sep 16 '19 at 16:01
  • 1
    @MichaelJarret: I had another idea that you could try. See the update to my answer. – David Foerster Sep 17 '19 at 02:25
9

Combining two related answers (here and here), here's an /etc/pam_session.sh that does the trick on Ubuntu 19.10 (at least):

#!/bin/sh
#
# This script runs when an ssh session opens/closes, and masks/unmasks
# systemd sleep and hibernate targets, respectively.
#
# Inspired by: https://unix.stackexchange.com/a/136552/84197 and
#              https://askubuntu.com/a/954943/388360

num_ssh=$(netstat -nt | awk '$4 ~ /:22$/ && $6 == "ESTABLISHED"' | wc -l)

case "$PAM_TYPE" in open_session) if [ "${num_ssh}" -gt 1 ]; then exit fi command=mask ;;

close_session)
    if [ "${num_ssh}" -ne 0 ]; then
        exit
    fi
    command=unmask
    ;;

*)
    exit

esac

logger "${command}ing sleep and suspend targets (num_ssh=${num_ssh})" sudo systemctl ${command} sleep.target suspend.target

Make sure to add the line:

session     optional    pam_exec.so quiet /etc/pam_session.sh

to /etc/pam.d/sshd in the session section, and make the /etc/pam_session.sh script executable:

chmod +x /etc/pam_session.sh

Note that the /etc/pam_session.sh script appears to run as the the user logging in or out, so that user requires sudo access to mask/unmask systemd targets.

  • 2
    Seems to be the most elegant solution. Despite its age, confirmed working on 20.10. One step missing in the answer: Make sure to add the line session optional pam_exec.so quiet /etc/pam_session.sh to /etc/pam.d/sshd in the session part and make the /etc/pam_session.sh executable. – emk2203 Oct 26 '20 at 15:18
  • Thanks @emk2203, I will add this to the answer. – claymation Nov 26 '20 at 01:41
6

In modern systemd systems, you can also use the ssh.socket service together with systemd-inhibit. Instead of running the usual ssh.service standalone, you can let systemd manage the incoming connections. Each open session now gets handled by ssh.socket which creates the instance units based on ssh@.service, so it is just a matter creating a service that binds directly to that. This has the added benefit of tracking and logging each ssh connection with systemd. We can then use this identifier to provide more useful information when blocking sleep. This method also prevents degrading systemd with a failing unit like some other examples.

First you need to check to make sure to enable ssh.socket if you are not already using it.

systemctl is-active ssh.socket

Should say active otherwise you will need to enable it:

sudo systemctl disable ssh
sudo systemctl stop ssh
sudo systemctl enable ssh.socket
sudo systemctl start ssh.socket

Then you just need is to create /etc/systemd/system/ssh-no-sleep@.service with

[Unit]
Description=ssh no sleep
BindsTo=ssh@%i.service

[Service] ExecStart=/usr/bin/systemd-inhibit --mode block --what sleep --who "ssh session "%I --why "session still active" /usr/bin/sleep infinity

[Install] WantedBy=ssh@.service

And enable it with:

sudo systemctl enable ssh-no-sleep@

In order for the ssh@.service alias (represented with the @) to be triggered you have to make sure your ssh.socket service has the [Socket] setting Accept=yes, otherwise the no sleep service will never trigger for each new connection. You can test your systemd default settings by running:

sudo systemctl show --no-pager --property Accept ssh.socket

If this returns no, you will need to override it to yes by creating a conf file /etc/systemd/system/ssh.socket.d/accept.conf with:

[Socket]
Accept=yes

If your system is missing ssh@.service or ssh.socket, here is the one from Debian Bullseye as an example that you can work off of:

/etc/systemd/system/ssh@.service

[Unit]
Description=OpenBSD Secure Shell server per-connection daemon
Documentation=man:sshd(8) man:sshd_config(5)
After=auditd.service

[Service] EnvironmentFile=-/etc/default/ssh ExecStart=-/usr/sbin/sshd -i $SSHD_OPTS StandardInput=socket RuntimeDirectory=sshd RuntimeDirectoryMode=0755

/etc/systemd/system/ssh.socket

[Unit]
Description=OpenBSD Secure Shell server socket
Before=sockets.target
ConditionPathExists=!/etc/ssh/sshd_not_to_be_run

[Socket] ListenStream=22 Accept=no

[Install] WantedBy=sockets.target

After saving this, running systemctl daemon-reload and systemctl restart ssh.socket , you can re-run the prior show command to confirm Accept=yes

Now when someone tries to suspend, they will get a useful message of all the blocking ssh sessions

sudo systemctl suspend

Will then return something like:

Operation inhibited by "ssh session 1/127.0.0.1:22/127.0.0.1:12345" (PID 12345 "systemd-inhibit", user root), reason is "session still active".
Operation inhibited by "ssh session 2/2001:db8:1::1:22/2001:db8:2::1:12345" (PID 54321 "systemd-inhibit", user root), reason is "session still active".
Please retry operation after closing inhibitors and logging out other users.
Alternatively, ignore inhibitors and users with 'systemctl suspend -i'.

Added benefit of ssh.socket is that you can also use:

systemctl status ssh.socket

Which returns active users and system stats:

* ssh.socket - OpenBSD Secure Shell server socket
     Loaded: loaded (/lib/systemd/system/ssh.socket; enabled; vendor preset: enabled)
     Active: active (listening) since Mon 2021-12-20 14:46:05 EST; 1h 6min ago
   Triggers: * ssh@2-2001:db8:1::1:22-2001:db8:2::1:12345.service
             * ssh@1-127.0.0.1:22-127.0.0.1:12345.service
     Listen: [::]:22 (Stream)
   Accepted: 2; Connected: 2;
      Tasks: 0 (limit: 18775)
     Memory: 16.0K
        CPU: 2ms
     CGroup: /system.slice/ssh.socket
Jude
  • 161
  • sadly it looks like ssh.socket was removed starting in openssh 8.0p1-3 because it was susceptible to DDOS – phiresky May 01 '22 at 09:27
  • 2
    @phiresky ssh.socket and ssh@.service systemd units have nothing to do with openssh version. It is up to the package maintainer for your specific distro to include it or not. I can confirm it works just fine in latest Debian and probably all Debian derivatives. You can just copy the missing systemd unit files from them. I disagree with Arch package maintainers for no longer including those units based on DDoS concerns as it doesn't line up with my threat model. Security is a balance and different distros have different opinions. – Jude May 08 '22 at 18:40
  • Thanks for the nice answer. Can you explain the @ in the file name? Is that your personal preference or is that some necessary magic? – Harald Jan 08 '23 at 11:05
  • 1
    @Harald the @ indicates a systemd template¹ service@argument.service syntax. In this case it templates a service to be run for every user that is ssh'd in and binds ssh-no-sleep service to each.

    ¹ https://www.freedesktop.org/software/systemd/man/systemd.service.html#Service%20Templates

    – Jude Jan 12 '23 at 21:59
  • 1
    Nice solution and Ubuntu is now using ssh.socket by default: https://discourse.ubuntu.com/t/sshd-now-uses-socket-based-activation-ubuntu-22-10-and-later/30189 – Cas Jan 28 '23 at 11:18
2

not sure if this will solve what you want, but it did for my case.

After login via ssh, use:

    DISPLAY=:0 caffeinate bash

(or any other shell installed instead of bash)

It will prevent sleeping while you are connected to the machine via ssh. You can take a look at the caffeinate manpage.

0

My own solution involves adding the following to .bashrc/.zshrc/…

if [ "$SSH_CLIENT" ] &&
     ! pstree -ps $$ |
       grep -q -- '-systemd-inhibit(' >/dev/null; then

echo "Inhibit automatic standby" exec /usr/bin/systemd-inhibit
--what=idle --why='Interactive SSH Session' --
"$SHELL" "$@"

fi

It works by routing/wrapping the shell on the remote through/using systemd-inhibit (and also making sure that there is only a single systemd-inhibit in the call chain, so it is not called again in case you enter a sub-shell).

Obviously, it only applies to SSH sessions with a shell.

phk
  • 391
  • 1
  • 3
  • 15
0

A simpler method would be adding the following python script to the root crontab (echo "*/1 * * * * python3 /path/to/inhibit.py" | sudo tee -a /etc/crontab) to check if an active SSH session is present every minute and inhibit sleep:

import subprocess, time, dbus  # noqa: E401
from filelock import FileLock

def run_command(command): result = subprocess.run( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, ) return result.stdout.decode("utf-8")

INHIBIT_FD = None with FileLock("/tmp/inhibit_ssh.lock", 0): while True: # Run lsof command and count the number of established connections containing "code". # Counts vscode remote-ssh server instances. You may not require this. code_conns = run_command("lsof -i | grep 'code' | grep 'ESTABLISHED'").count("\n") # Run who command and count the number of active sessions (pts) ssh_conns = run_command("who | grep 'pts'").count("\n") total_conns = code_conns + ssh_conns

    if total_conns > 0:
        if not INHIBIT_FD:
            manager = dbus.Interface(
                dbus.SystemBus().get_object(
                    "org.freedesktop.login1", "/org/freedesktop/login1"
                ),
                "org.freedesktop.login1.Manager",
            )
            res = manager.Inhibit("sleep", "sshInhibit", "SSH Active", "block")
            INHIBIT_FD = res.take()
            print("FD:", INHIBIT_FD)

        print("SSH Active")
        time.sleep(60)
    else:
        break

0

Be awared that is hibernate in lieu of suspend in my case. Please customize to yours!

Firstly, create a configuration file that placed at /etc/configure_sleep.sh (or anywhere that you like). This script aims to enable/disable the sleep based on your configuration (refer to Prevent sleep/suspend when not logged in to a specific account).

#!/bin/sh
# This script is to configure the sleep entries

TIMEOUT=60 IS_LOGIN_SCREEN=$(pgrep -u gdm gnome-session)

command=$1

if [ "${command}" = "enable" ] ; then sudo -Hu gdm dbus-launch dconf write /org/gnome/settings-daemon/plugins/power/sleep-inactive-ac-timeout ${TIMEOUT} sudo -Hu gdm dbus-launch dconf write /org/gnome/settings-daemon/plugins/power/sleep-inactive-ac-type "'hibernate'" else sudo -Hu gdm dbus-launch dconf write /org/gnome/settings-daemon/plugins/power/sleep-inactive-ac-timeout 0 sudo -Hu gdm dbus-launch dconf write /org/gnome/settings-daemon/plugins/power/sleep-inactive-ac-type "'nothing'" fi echo "I'm here" if [ -z "${IS_LOGIN_SCREEN}" ] ; then echo "In user's screen." else echo "This is login screen. Gnome binary will be forcedly reloaded." sudo killall -3 gnome-session-binary fi

Secondly, rely on Prevent machine from sleeping when SSH connections are on, but, instead of masking and unmasking the sleep targets that get an annoying problem with the broadcasting keeps announce. The first script will be launched appropriately. Put this script in /etc/pam_session.sh.

#!/bin/sh
#
# This script runs when an ssh session opens/closes,
# and changes the gnome settings, respectively.
# Inspired by: https://askubuntu.com/questions/521620/

SCRIPT_PATH="/etc/pam_session.sh" DBUS=unix:path=/run/user/1000/bus

num_ssh=$(netstat -nt | awk '$4 ~ /:22$/ && $6 == "ESTABLISHED"' | wc -l)

case "$PAM_TYPE" in open_session) if [ "${num_ssh}" -gt 1 ]; then exit fi command=mask ;;

close_session)
    if [ "${num_ssh}" -ne 0 ]; then
        exit
    fi
    command=unmask
    ;;

*)
    exit

esac

logger "${SCRIPT_PATH}: ${command}ing sleep and suspend targets (num_ssh=${num_ssh})"

if [ ${command} = unmask ] ; then /etc/configure-sleep.sh "enable" logger "${SCRIPT_PATH}: Re-enable Auto Hibernate" else /etc/configure-sleep.sh "disable" logger "${SCRIPT_PATH}: Disable Auto Hibernate" fi

Add the script into the ssh service pam and don’t forget to make these scripts executable.

Add this line to /etc/pam.d/sshd under the session session.

session     optional    pam_exec.so quiet /etc/pam_session.sh

And make them executable.

chmod +x /etc/pam_session.sh
chmod +x /etc/configure-sleep.sh

Everything seems to fine until now. Perhaps, if you turn off the computer while ssh sessions is alive (auto sleep is off), the auto sleep will not be functional in the next power cycle. My solution delegates a service to self-enable the module.

Create a file named 99-defaultHibernate.service and placed in /etc/systemd/system/.

[Unit]
Description=Setup default hibernaten

[Service] Type=oneshot ExecStartPre=/bin/sleep 10 ExecStart=/etc/configure-sleep.sh enable

[Install] WantedBy=basic.target

Make the service affects by the following procedures.

sudo systemctl daemon-reload
sudo systemctl enable 99-defaultHibernate.service
sudo systemctl start 99-defaultHibernate.service

Please not that this approach adopts the kill of gnome binary session. It might cause potential risks or unforeseen defects.

P/s: Any indication is always welcome!