Why doesn't the default Super+L shortcut create a Lock() signal?

I recently came across systemd-lock-handler to run systemd user services on lock and unlock events. It looks for org.freedesktop.login1.Session.Lock () and org.freedesktop.login1.Session.Unlock () signals to create lock.target and unlock.target. I can then use these targets to run systemd user services as systemd doesn’t provide these targets by default. What I noticed is when I use the default Super+L shortcut to lock, it doesn’t send the org.freedesktop.login1.Session.Lock () signal. But when I lock using loginctl lock-session as mentioned in systemd-lock-handler’s documentation, it correctly sends the org.freedesktop.login1.Session.Lock () signal. The signals can be monitored with gdbus monitor -y -d org.freedesktop.login1 | grep -e “org.freedesktop.login1.Session.Lock ()” -e “org.freedesktop.login1.Session.Unlock ()”

This is with the default Super+L after locking and unlocking:

This is with Super+L mapped to loginctl lock-session using Custom Shortcuts after locking and unlocking:

Does anyone know why the default Super+L shortcut doesn’t create a Lock() signal? systemd-lock-handler works great for lock events after setting the custom shortcut for Super+L, but I’m curious why the default shortcut doesn’t work.

Maybe this is the result of the shell you are using.
There are differences between ‘bash’ and ‘fish’, and those screenshots appear to indicate you are using fish.

Doesn’t look like the shell is the problem. I tried the same thing in bash and it gives identical result:

Which command does the default shortcut use?

It sets LockedHint to true.
ref: Making sure you're not a bot!

1 Like

Yes, I know about LockedHint. Unfortunately, it’s inside the metadata and systemd-lock-handler doesn’t check for that. It only checks for the Lock() signal. Thanks for linking the issue. I didn’t know there was an issue open for this. There is also a LockedHint implementation example in Go in one of the examples there. I’ll try to use it in systemd-lock-handler as it also uses Go.

Update 1: The LockedHint mod worked perfectly in systemd-lock-handler with default Super+L. Here’s the updated main.go file if anyone needs it:

package main

import (
	"context"
	"fmt"
	"log"
	"os/user"

	"github.com/coreos/go-systemd/v22/daemon"
	systemd "github.com/coreos/go-systemd/v22/dbus"
	"github.com/coreos/go-systemd/v22/login1"
	dbus "github.com/godbus/dbus/v5"
)

// Starts a systemd unit and blocks until the job is completed.
func StartSystemdUserUnit(unitName string) error {
	conn, err := systemd.NewUserConnectionContext(context.Background())
	if err != nil {
		return fmt.Errorf("failed to connect to systemd user session: %v", err)
	}

	ch := make(chan string, 1)

	_, err = conn.StartUnitContext(context.Background(), unitName, "replace", ch)
	if err != nil {
		return fmt.Errorf("failed to start unit: %v", err)
	}

	result := <-ch
	if result == "done" {
		log.Println("Started systemd unit:", unitName)
	} else {
		return fmt.Errorf("failed to start unit %v: %v", unitName, result)
	}

	return nil
}

func ListenForSleep() {
	conn, err := dbus.ConnectSystemBus()
	if err != nil {
		log.Fatalln("Could not connect to the system D-Bus", err)
	}

	// TODO: Should I also stop `sleep.target` after the system comes back
	// from sleeping? (`lock.target` will continue running anyway).
	err = conn.AddMatchSignal(
		dbus.WithMatchObjectPath("/org/freedesktop/login1"),
		dbus.WithMatchInterface("org.freedesktop.login1.Manager"),
		dbus.WithMatchMember("PrepareForSleep"),
	)
	if err != nil {
		log.Fatalln("Failed to listen for sleep signals", err)
	}

	c := make(chan *dbus.Signal, 10)
	logind, err := login1.New()
	if err != nil {
		log.Fatalln("Failed to connect to logind")
	}

	go func() {
		for {
			// We need to inhibit sleeping so we have time to execute our actions before the system sleeps.
			lock, err := logind.Inhibit("sleep", "systemd-lock-handler", "Start pre-sleep target", "delay")
			if err != nil {
				log.Fatalln("Failed to grab sleep inhibitor lock", err)
			}
			log.Println("Got lock on sleep inhibitor")

			if err := waitPrepareForSleep(c, true); err != nil {
				log.Fatalln("Before releasing inhibitor lock:", err)
			}

			log.Println("Starting sleep.target")

			if err = StartSystemdUserUnit("sleep.target"); err != nil {
				log.Println("Error starting sleep.target:", err)
			}
			// Uninhibit sleeping. I.e.: let the system actually go to sleep.
			if err := lock.Close(); err != nil {
				log.Fatalln("Error releasing inhibitor lock:", err)
			}

			if err := waitPrepareForSleep(c, false); err != nil {
				log.Fatalln("After releasing inhibitor lock:", err)
			}

			log.Println("Started sleep.target, the system is going to sleep")
		}
	}()

	conn.Signal(c)
	log.Println("Listening for sleep events...")
}

func waitPrepareForSleep(c <-chan *dbus.Signal, want bool) error {
	s := <-c

	if len(s.Body) == 0 {
		return fmt.Errorf("empty signal arguments: %v", s)
	}

	got, ok := s.Body[0].(bool)
	if !ok {
		return fmt.Errorf("active argument not a bool: %v", s.Body[0])
	}

	if want != got {
		return fmt.Errorf("expected PrepareForSleep(%v), got %v", want, got)
	}

	return nil
}

func ListenForLock(user *user.User) {
	conn, err := dbus.ConnectSystemBus()
	if err != nil {
		log.Fatalln("Could not connect to the system D-Bus", err)
	}
	defer conn.Close()

	if err = conn.AddMatchSignal(
		// dbus.WithMatchObjectPath("/org/freedesktop/login1/session/_xx"), // Specific session
		dbus.WithMatchPathNamespace("/org/freedesktop/login1/session"), // All sessions
		dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
		dbus.WithMatchSender("org.freedesktop.login1"),
		dbus.WithMatchMember("PropertiesChanged"),
	); err != nil {
		panic(err)
	}

	c := make(chan *dbus.Signal)
	conn.Signal(c)
	for v := range c {
		var target string
		signalName := v.Name
		changedProperties := v.Body[1].(map[string]dbus.Variant)
		lockedHintProperty, hasLockedHint := changedProperties["LockedHint"]
		if !hasLockedHint {
			continue
		}

		isLocked := lockedHintProperty.Value().(bool)
		if isLocked {
			target = "lock.target"
		} else {
			target = "unlock.target"
		}

		// Get the (un)locked session...
		obj := conn.Object("org.freedesktop.login1", v.Path)

		name, err := obj.GetProperty("org.freedesktop.login1.Session.Name")
		if err != nil {
			log.Println("WARNING: Could not obtain details for locked session:", err)
			continue
		}

		// ... And check that it belongs to the current user:
		if name.Value() != user.Username {
			continue
		}
		log.Println("Session signal for current user:", signalName)

		if err = StartSystemdUserUnit(target); err != nil {
			log.Println("Error starting target:", target, err)
		}
	}

	conn.Signal(c)
	log.Println("Listening for LockedHint property...")
}

func main() {
	log.SetFlags(log.Lshortfile)

	user, err := user.Current()
	if err != nil {
		log.Fatalln("Failed to get username:", err)
	}
	log.Println("Running for user:", user.Username)

	ListenForSleep()
	ListenForLock(user)

	log.Println("Initialization complete.")

	sent, err := daemon.SdNotify(true, daemon.SdNotifyReady)
	if !sent {
		log.Println("Couldn't call sd_notify. Not running via systemd?")
	}
	if err != nil {
		log.Println("Call to sd_notify failed:", err)
	}

	select {}
}

Update 2: Upon further testing, using LockedHint seems to be unreliable. I’m using systemd user scripts to log lock and unlock times. Sometimes it logs two locks or two unlocks in a row which is just wrong. Using the original Lock() signal I haven’t encountered this. So I’ll just be using the original systemd-lock-handler with a custom shortcut for Super+L.

1 Like