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.

3
  • 2
    May I suggest using grep -cv :0 instead of passing through wc?
    – terdon
    Sep 8, 2014 at 18:49
  • 1
    Another idea for more elegance: You can drop the invocation of [ if you write if who | grep -qv :0; then (assuming you have a POSIX conforming grep like GNU grep). Sep 30, 2014 at 23:53
  • who outputs times too, so ":0" is not unique to the console user (e.g. "15:07")
    – Greg Bell
    Dec 19, 2023 at 6:18

8 Answers 8

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).

7
  • this doesn't seem to work in 18.04. Is this no longer the solution? Sep 13, 2019 at 23:43
  • 1
    @MichaelJarret: Indeed. I updated my question with some info on the situation with systemd. Sep 15, 2019 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 Sep 15, 2019 at 15:00
  • I can't edit my comment, but also this seems relevant Sep 15, 2019 at 15:07
  • 1
    @MichaelJarret: I had another idea that you could try. See the update to my answer. Sep 17, 2019 at 2: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
  • 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, 2020 at 15:18
  • Thanks @emk2203, I will add this to the answer.
    – claymation
    Nov 26, 2020 at 1: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 [email protected], 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/[email protected] 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]
[email protected]

And enable it with:

sudo systemctl enable ssh-no-sleep@

In order for the [email protected] 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 [email protected] or ssh.socket, here is the one from Debian Bullseye as an example that you can work off of:

/etc/systemd/system/[email protected]

[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
             * [email protected]: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
5
  • sadly it looks like ssh.socket was removed starting in openssh 8.0p1-3 because it was susceptible to DDOS
    – phiresky
    May 1, 2022 at 9:27
  • 2
    @phiresky ssh.socket and [email protected] 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 8, 2022 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 8, 2023 at 11:05
  • 1
    @Harald the @ indicates a systemd template¹ [email protected] 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. ¹ freedesktop.org/software/systemd/man/…
    – Jude
    Jan 12, 2023 at 21:59
  • 1
    Nice solution and Ubuntu is now using ssh.socket by default: discourse.ubuntu.com/t/…
    – Cas
    Jan 28, 2023 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.

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!

0

If you want a one-off solution, for an individual ssh session, the following works well on systemd-based systems:

systemd-inhibit --what=idle $SHELL

The default value of the --what argument is idle:sleep:shutdown and the last two states require root privileges. By specifying only idle the command can be run by any user.


Update: This doesn't always work. I think it depends on what desktop environment you're using. Gnome Shell, for example, seems to be doing its own idle detection and requests sleep directly.

With Gnome, you can do this instead:

gnome-session-inhibit --inhibit suspend:idle $SHELL

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .