Okay so after years of being frustrated with this, I finally did something about it and wrote a Python utility script that monitors DBus and properly receives session lock/unlock events.
The code is hosted here, but I'll include it below as well. My goal is to rewrite this in Rust for a number of reasons, but largely to make it easier for folks to use without having to install packages to get the right Python libraries installed.
Prerequisites
To run this code, you'll need:
- A recent Python 3, I wrote this on Python 3.8.5.
- Eggs:
dbus-python >=1.2,<2
PyGObject >=3.36,<4
These Python eggs are provided by certain Ubuntu packages, but might not be the right versions. On 16.04, I believe the required packages are:
python3-gi
, which is PyGObject
python3-dbus
, which is dbus-python
On different distribution versions, these packages may be different. This is one of many reasons why I want to rewrite this in Rust, I'll list my other motivations at the end of this answer.
Code
Let's get into the code.
dbus-session-lock-watcher.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib
import dbus
import logging
import sys
class ScreenSaverEventListener(object):
def __init__(self):
self.logger = logging.getLogger(self.__class__.__name__)
self.mainloop = DBusGMainLoop()
self.loop = GLib.MainLoop()
self.session_bus = dbus.SessionBus(mainloop=self.mainloop)
self.receiver_args, self.receiver_kwargs = None, None
def setup(self):
self.receiver_args = (self.on_session_activity_change,)
self.receiver_kwargs = dict(dbus_interface="org.freedesktop.DBus.Properties", path="/org/gnome/SessionManager",
signal_name="PropertiesChanged",
# callback arguments
sender_keyword="sender", destination_keyword="dest",
interface_keyword="interface", member_keyword="member", path_keyword="path",
message_keyword="message")
self.session_bus.add_signal_receiver(*self.receiver_args, **self.receiver_kwargs)
def on_session_activity_change(self, target: dbus.String, changed_properties: dbus.Dictionary, *args, **kwargs):
if target != "org.gnome.SessionManager" or "SessionIsActive" not in changed_properties:
return
if changed_properties.get("SessionIsActive"):
self.on_session_unlock()
else:
self.on_session_lock()
def on_session_lock(self):
self.logger.info("Session Locked")
def on_session_unlock(self):
self.logger.info("Session Unlocked")
def run(self):
self.logger.debug("Starting event loop.")
self.loop.run()
def shutdown(self):
self.logger.debug("Stopping event loop.")
self.session_bus.remove_signal_receiver(*self.receiver_args, **self.receiver_kwargs)
self.loop.quit()
def main():
setup_logging()
listener = ScreenSaverEventListener()
listener.setup()
try:
listener.run()
except KeyboardInterrupt:
sys.stderr.write("ctrl+c received, shutting down...\n")
listener.shutdown()
def setup_logging():
console = logging.StreamHandler(sys.stderr)
console.setFormatter(
logging.Formatter("%(asctime)s [%(levelname)-5s] %(name)s: %(message)s", datefmt="%Y-%m-%dT%H:%M:%S%z"))
logging.addLevelName(logging.WARNING, "WARN")
logging.getLogger().addHandler(console)
logging.getLogger().setLevel(logging.DEBUG)
if __name__ == "__main__":
main()
To make this code do anything interesting, edit:
ScreenSaverEventListener.on_session_lock
, which will execute when the screen has locked 🔒
ScreenSaverEventListener.on_session_unlock
, which will execute when the screen has been unlocked 🔒
Installation
First, save the script above into a file, preferably in ~/.local/bin
. Next, of course, is to make sure that it's marked as executable, via chmod +x ~/.local/bin/dbus-session-lock-watcher.py
.
I'd advise running this as a service, so let's create a systemd user unit to run the service.
Here's a sample unit:
~/.config/systemd/user/session-lock-watcher.service
[Unit]
Description=DBus Session Lock Watcher
[Service]
ExecStart=$HOME/.local/bin/dbus-session-lock-watcher.py
[Install]
WantedBy=default.target
Next, let's enable the service:
systemctl --user enable session-lock-watcher.service
Finally, let's start it:
systemctl --user start session-lock-watcher.service
To view logs from the service:
journalctl --user-unit session-lock-watcher.service
And now we can execute custom code or scripts whenever the session is locked or unlocked!
Other Info
A lot of work went into this, so I wanted to provide some of that information in case it's interesting.
Learnings
D-Bus is complicated, but I'll try to summarize it. D-Bus is a plumbing service between different applications and provides a lot of bells and whistles. If you're familiar with JMX for the JVM, it's kind of like that, except as a service bus as an external process.
There are generally two D-Bus buses: the system bus, and the session bus. The system bus is for OS-wide things, whereas the session bus is a per-user bus and the primary thing you'll need to interact with, most likely.
D-Bus contains a registry of names, each of which has a number of object paths it supports, and each of these object paths has one or more interfaces. Interfaces have methods, properties, and signals. You can call methods with or without arguments and get results if the method returns values. You can read and sometimes write properties, and signals are events which you can subscribe to that carry data.
The exact relationship between names, object paths, and interfaces is still unclear to me so I'm just going to move on. In short, you have a bunch of registered names/applications that implement certain interfaces which define methods, signals, and properties, and that's how you communicate between applications using D-Bus.
I'll try to find more documentation to understand the logic behind all of the D-Bus machinery, but this suffices for now.
What was exceptionally frustrating is how many different things I tried to simply receive a callback when the screen was locked or unlocked.
I tried org.gnome.ScreenSaver
, which supports multiple interfaces that have the same attributes, org.freedesktop.ScreenSaver
and org.gnome.ScreenSaver
. They both support an ActiveChanged
signal which carries a boolean, seemingly to indicate whether the screensaver is active or not. I never caught this signal, ever.
I tried org.gnome.SessionManager
and its corresponding interface, which exposed SessionOver
and SessionRunning
signals. I never caught either of these signals.
After trudging through tons of different names and interfaces and signals, I had all but given up on finding a signal to indicate when the session was locked and unlocked. I setup a generic signal receiver that caught all signals, set a breakpoint, and stepped through all of them until I found a property change signal for the SessionIsActive
property belonging to org.gnome.SessionManager
. Bingo.
Finally, I tuned my signal receiver to filter as well as it can on receiving only relevant signals for when this property is changed.
I'm on an Ubuntu derivative, so this code may not work on every Ubuntu distribution. Run the script, lock and unlock your session, and see if the script emits logs on lock and unlock.
Future Work
I'd really like to rewrite all this in a simple Rust daemon, as a binary will be much easier to distribute, with few or no runtime dependencies. I'd also like to provide some sort of directory where one can stash as many scripts as they'd like to run on lock and unlock events, perhaps something like ~/.config/nosleep/on-{lock,unlock}/*
. A sorted list of files will be obtained and each executable will be run in sorted order, making it much easier to write scripts around lock and unlock events.
I hope this helps someone and is informative, I know I learned a lot writing it!