‹ back home

My 'lock-and-sleep' script

2024-02-26 #open-source #sway #wayland

My lock-and-sleep locks the system and puts in back to sleep after 10 seconds of idle as long as it remains locked.

Locking the system before going to sleep ensures that the system never shows an unlocked desktop when waking up from sleep. If the screen locker fails to start for some reason, this fact becomes clear sooner rather than later.

Going back to sleep automatically prevents the system from remaining active if woken up by accident. For example, if someone (some person, or some cat) presses any key during the night the system wakes up. I don’t want it to remain awake indefinitely in such situations.

When the system wakes up, it won’t always reset the idle timer. If I press the space-bar when the system is asleep, this key-press event is “swallowed” by the firmware (which wakes up the systems) and never delivered to the operating system or the compositor. The idle timer continues running since it originally started, and the compositor never send an event indicating that the system has resumed from idle.

The script

This script also exists in my dotfiles repository.

#!/usr/bin/env python3
#
# Requires swaylock > 1.7.2.
import os
import sys
import subprocess

(r, w) = os.pipe()
os.set_inheritable(r, False)
swaylock = subprocess.Popen(["swaylock", "--ready-fd", str(w)], pass_fds=(w,))
os.close(w)

# Wait for swaylock to be ready.
with os.fdopen(r, "rb") as ready_pipe:
    while True:
        pipe_data = ready_pipe.read(1)
        if pipe_data == b"":
            print("fatal: swaylock closed fd before locking")
            sys.exit(1)
        if pipe_data == b"\n":
            break

while True:
    subprocess.call(["powerctl", "mem"])  # blocks until wakeup from sleep.

    wayidle = subprocess.Popen(["wayidle", "--timeout", "10"])

    (pid, returncode) = os.wait()
    if pid == swaylock.pid:
        wayidle.kill()
        sys.exit(returncode)
    if pid == wayidle.pid and returncode != 0:
        print("fatal: wayidle exited non-zero")
        sys.exit(returncode)  # swaylock will continue running

Let’s go over it bit by bit.

swaylock > 1.7.2

This script requires swaylock > 1.7.2. It relies on swaylock’s readiness notification, which indicates once swaylock has finished locking the screen.

With previous releases, there exists no trivial way to do this and wait for swaylock to exit (more on this below).

Running swaylock

First comes the initial block which runs swaylock:

(r, w) = os.pipe()
os.set_inheritable(r, False)
swaylock = subprocess.Popen(["swaylock", "--ready-fd", str(w)], pass_fds=(w,))
os.close(w)

os.pipe() creates a pipe, a pair of file descriptors, w and r. Any data written to w gets read via r. After closing all the copies of w, r will yield an EOF (end of file).

The child process (swaylock) inherits a copy of w, and the Python script keeps the original copy. If the Python script keeps its the copy of the w descriptor open, then the r one will never reach its end. The script explicitly closes it with the os.close(w) call.

When swaylock writes to on one of these file descriptors (w), this Python script will read from the other one (r).

swaylock will then start up, and as soon as it locks the system, it will write \n into w and then close it.

Waiting for the system to lock

The next section now becomes relevant:

with os.fdopen(r, "rb") as ready_pipe:
    while True:
        pipe_data = ready_pipe.read(1)
        if pipe_data == b"":
            print("fatal: swaylock closed fd before locking")
            sys.exit(1)
        if pipe_data == b"\n":
            break

This reads from r in an infinite loop (while True). If swaylock writes a \n, this indicates that locking the screen has finished. The loop breaks and the program continues executing in the next section.

If swaylock does not print a \n, this indicates that it has exited before locking the screen. If this happens, the script exits with an error. I avoid putting the system to sleep in this case, since that would hide the fact that something went wrong with the screen locker.

The main loop

Lastly, the “main loop”. Again, an infinite loop:

while True:
    subprocess.call(["powerctl", "mem"])  # blocks until wakeup from sleep.

    wayidle = subprocess.Popen(["wayidle", "--timeout", "10"])

    (pid, returncode) = os.wait()
    if pid == swaylock.pid:
        wayidle.kill()
        sys.exit(returncode)
    if pid == wayidle.pid and returncode != 0:
        print("fatal: wayidle exited non-zero")
        sys.exit(returncode)  # swaylock will continue running

This starts off by calling powerctl mem, which puts the system to sleep. This command exits after the system has resumed from sleep. This guarantees that the next line of this script executes after the system has woken up again.

At this point, wayidle runs. wayidle sets an idle timer (for 10 seconds in this case) and exits once the timer expires. I tried using swayidle here, but it didn’t quite fit, because swayidle doesn’t know when the system wakes up again, so it won’t run another timer and the system will remain awake.

The script will then wait for one of its children (wayidle or swaylock) to exit.

If swaylock exits first, then it has unlocked the system (or crashed). In this case wayidle gets explicitly killed, and the script exits (with the status code of swaylock, to reflect any errors).

If wayidle exits first and without any errors, this means that the system reached the idle timeout. The loop starts over, executing powerctl mem and putting the system back to sleep.

If wayidle exits first but with an error, then the script exits with the same error. This shouldn’t happen, and implies an unexpected error with the idle timeout. The systems stays awake; if it went to sleep again, then it could end up in an irrecoverable sleep-loop.

Closing notes

I configured sway to run this script whenever I tap the power button on my computer:

bindsym --release XF86PowerOff exec lock-and-sleep

I don’t think I can make this script any simpler, and I welcome any feedback on it.

Feel free to re-use any bits or ideas to your liking.

Have comments or want to discuss this topic?
Send an email to my public inbox: ~whynothugo/public-inbox@lists.sr.ht.
Or feel free to reply privately by email: hugo@whynothugo.nl.

— § —