23

Based on various sources I have cobbled together ~/.config/systemd/user/screenlock.service:

[Unit]
Description=Lock X session
Before=sleep.target

[Service]
Environment=DISPLAY=:0
ExecStart=/usr/bin/xautolock -locknow

[Install]
WantedBy=sleep.target

I've enabled it using systemctl --user enable screenlock.service. But after rebooting, logging in, suspending and resuming (tested both with systemctl suspend and by closing the lid) the screen is not locked and there is nothing in journalctl --user-unit screenlock.service. What am I doing wrong?

Running DISPLAY=:0 /usr/bin/xautolock -locknow locks the screen as expected.

$ systemctl --version
systemd 215
+PAM -AUDIT -SELINUX -IMA -SYSVINIT +LIBCRYPTSETUP +GCRYPT +ACL +XZ +SECCOMP -APPARMOR
$ awesome --version
awesome v3.5.5 (Kansas City Shuffle)
 • Build: Apr 11 2014 09:36:33 for x86_64 by gcc version 4.8.2 (nobody@)
 • Compiled against Lua 5.2.3 (running with Lua 5.2)
 • D-Bus support: ✔
$ slim -v
slim version 1.3.6

If I run systemctl --user start screenlock.service the screen locks immediately and I get a log message in journalctl --user-unit screenlock.service, so ExecStart clearly is correct.

Relevant .xinitrc section:

xautolock -locker slock &

Creating a system service with the same file works (that is, slock is active when resuming):

# ln -s "${HOME}/.config/systemd/user/screenlock.service" /usr/lib/systemd/system/screenlock.service
# systemctl enable screenlock.service
$ systemctl suspend

But I do not want to add a user-specific file outside $HOME for several reasons:

  • User services should be clearly separated from system services
  • User services should be controlled without using superuser privileges
  • Configuration should be easily version controlled
l0b0
  • 51,350
  • I'm using awesome as the window manager, and SLiM as the login manager. I'm not using a full desktop environment as defined by Arch, and Linux/awesome as the desktop environment as defined by Wikipedia. There doesn't seem to be anything like a "desktop manager" for Linux. – l0b0 Aug 13 '14 at 05:48
  • User services are run outside of the session, so your session data is not available to them; you might be better off using a standard service file for this: at least to test anyway... – jasonwryan Aug 13 '14 at 07:10
  • @jasonwryan Surely I would see some sort of error message in the journal if the service had been triggered? – l0b0 Aug 14 '14 at 07:17
  • I don't know: systemd-user is still very flaky; getting it to work as part of the session via the approach I outlined would help narrow down the issue; that's all I can suggest. – jasonwryan Aug 14 '14 at 07:20
  • Though it is not a perfect solution (it would still need to be managed with root permissions), you can simply use /etc/systemd/system/ or $HOME/.local/systemd/system to avoid putting anything in /usr manually. As @jasonwryan mentioned, user sessions are still not considered production-quality; but they're getting closer. – HalosGhost Aug 28 '14 at 03:20
  • 1
    There's a feature request for this here, but it seems doubtful it'll be implemented any time soon: https://github.com/systemd/systemd/issues/3312 – jokeyrhyme Mar 16 '24 at 19:35

2 Answers2

25

sleep.target is specific to system services. The reason is, sleep.target is not a magic target that automatically gets activated when going to sleep. It's just a regular target that puts the system to sleep – so the 'user' instances of course won't have an equivalent. (And unfortunately the 'user' instances currently have no way to depend on systemwide services.)

(That, and there's the whole "hardcoding $DISPLAY" business. Every time you hardcode session parameters in an OS that's based on the heavily multi-user/multi-seat Unix, root kills a kitten.)

So there are two good ways to do this (I suggest the 2nd one):

Method 1

Create a system service (or a systemd-sleep(8) hook) that makes systemd-logind broadcast the "lock all sessions" signal when the system goes to sleep:

ExecStart=/usr/bin/loginctl lock-sessions

Then, within your X11 session (i.e. from ~/.xinitrc), run something that reacts to the signal:

systemd-lock-handler slock &
xss-lock --ignore-sleep slock &

(GNOME, Cinnamon, KDE, Enlightenment already support this natively.)

Method 2

Within your X11 session, run something that directly watches for the system going to sleep, e.g. by hooking into systemd-logind's "inhibitors".

The aforementioned xss-lock actually does exactly that, even without the explicit "lock all" signal, so it is enough to have it running:

xss-lock slock &

It will run slock as soon as it sees systemd-logind preparing to suspend the computer.

  • Could you please elaborate a bit on the Enlightenment and others' native support? It's not clear what exactly they support natively from the answer. – Pavel Šimerda Dec 27 '14 at 18:21
  • @PavelŠimerda: The "lock session" signal from systemd-logind (...the entire section is about it...) Also, I was wrong, e19 doesn't actually support it. – u1686_grawity Dec 28 '14 at 14:02
  • Thanks for the info about E19. The answer still lacks explanation on what exactly Gnome and others support. Listening to systemd's D-Bus signal (even that's not written there) is one thing, what actions are done in reaction and what actions and how can the user configure to be done is another. Also there's no information on what systemd-lock-handler does and where it comes from. – Pavel Šimerda Dec 28 '14 at 16:12
  • xss-lock is in the AUR, so there's no need to build it manually. – l0b0 Jul 19 '15 at 08:05
  • This works beautifully under Debian testing. Thanks for posting. It is quite disappointing that systemd does not allow user services to depend on systems services... – cgogolin Jun 01 '16 at 14:51
  • @cgogolin: Well, it's not exactly trivial to implement either. – u1686_grawity Jun 01 '16 at 16:00
  • Thank you very much, awesome solution! I don't always have an X session running and hate when this is assumed. I was hoping for a simple command to signal "userland systemd" and have services launch upon these "events" - since we can't access system targets from there. Anyway your solution is lean enough. – Rolf Mar 29 '18 at 20:41
  • In essence, some services (eg: the screen locker) need to run as, or be aware of the user who triggered them. This information gets lost in systemd.

    One downside of your solution is that there is no way to inhibit sleep until the locker has properly loaded.

    – Rolf Mar 29 '18 at 21:00
  • I tried to launch systemd_lock_handler from within a systemd --user service, and it complains about $XDG_SESSION_ID not being set. Is there any way around that? – Rolf Mar 30 '18 at 00:14
  • At the moment I'm just importing the environment into userland systemd. I think it should be possible to get the session ID from the user object ( https://www.freedesktop.org/wiki/Software/systemd/logind/ ) , then systemd_lock_handler can run from within a user service unit without doing any tricks. – Rolf Mar 30 '18 at 00:54
-1

systemd-lock-handler is a Python script which can accomplish this: https://github.com/grawity/code/blob/master/desktop/systemd-lock-handler.

#!/usr/bin/env python
# systemd-lock-handler -- proxy between systemd-logind's "Lock" signal and your
#   favourite screen lock command

from __future__ import print_function
import os, sys, dbus, dbus.mainloop.glib
from gi.repository import GLib

def trace(*args):
    global arg0
    print("%s:" % arg0, *args)

def setup_signal(signal_handler):
    global session_id
    bus = dbus.SystemBus()
    manager = bus.get_object("org.freedesktop.login1", "/org/freedesktop/login1")
    # yecch
    manager = dbus.Interface(manager, "org.freedesktop.login1.Manager")
    session_path = manager.GetSession(session_id)
    session = bus.get_object("org.freedesktop.login1", session_path)
    session.connect_to_signal("Lock", signal_handler)

def handler_dbus_fdo():
    trace("locking session using DBus")
    bus = dbus.SessionBus()
    screensaver = bus.get_object("org.freedesktop.ScreenSaver", "/ScreenSaver")
    screensaver.Lock()

def handler_external():
    global lock_command
    trace("locking session using %r" % lock_command[0])
    os.spawnvp(os.P_NOWAIT, lock_command[0], lock_command)

def main():
    global arg0, lock_command, session_id
    arg0 = sys.argv[0].split("/")[-1]
    lock_command = sys.argv[1:] or ["--dbus"]
    try:
        session_id = os.environ["XDG_SESSION_ID"]
    except KeyError:
        print("error: $XDG_SESSION_ID not set; are you using pam_systemd?",
            file=sys.stderr)
        sys.exit(1)
    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
    if lock_command == ["--dbus"]:
        trace("using freedesktop.org DBus API")
        setup_signal(handler_dbus_fdo)
    else:
        trace("using external command %r" % lock_command[0])
        setup_signal(handler_external)
    trace("waiting for lock signals on session %s" % session_id)
    try:
        loop = GLib.MainLoop()
        loop.run()
    except KeyboardInterrupt:
        sys.exit(0)

main()