1// Package powerkey takes logind's inhibitor locks for the power, suspend,
2// hibernate and reboot keys, so pressing one no longer powers the machine
3// off or to sleep by itself: the key reaches the compositor as a key event
4// (XF86PowerOff, XF86Sleep, ...) and only a bind acts on it.
5package powerkey
6
7import (
8 "context"
9 "errors"
10 "fmt"
11 "os"
12 "time"
13
14 "github.com/bnema/neferwl/internal/adapters/busretry"
15 "github.com/bnema/zerowrap"
16 "github.com/godbus/dbus/v5"
17)
18
19const (
20 logind = "org.freedesktop.login1"
21 logindPath = "/org/freedesktop/login1"
22 manager = "org.freedesktop.login1.Manager"
23 // busDaemon is the sender of the bus's own signals. Any client can send
24 // a NameOwnerChanged to us directly, so only this sender is trusted.
25 busDaemon = "org.freedesktop.DBus"
26 // what lists the logind actions NeferWL takes over.
27 what = "handle-power-key:handle-suspend-key:handle-hibernate-key:handle-reboot-key"
28 // callTimeout bounds the Inhibit call.
29 callTimeout = 3 * time.Second
30)
31
32// Serve holds the locks until ctx ends. address is the bus to use, the
33// system bus when empty. When the bus or logind goes, it takes the locks
34// again after retry, doubling up to a minute. Meanwhile the old lock stays
35// open: logind keeps an inhibitor across its own restart for as long as a
36// client holds the descriptor, so the keys stay blocked in the gap, and the
37// old lock is closed only once a new one is in place.
38func Serve(ctx context.Context, address string, retry time.Duration, log zerowrap.Logger) {
39 var lock *os.File
40 defer func() {
41 if lock != nil {
42 _ = lock.Close()
43 }
44 }()
45 busretry.Run(ctx, retry, "logind power key lock", log, func(ctx context.Context) error {
46 return hold(ctx, address, log, &lock)
47 })
48}
49
50// hold takes the locks, replaces *lock with them, and waits until ctx ends
51// (nil) or the bus or logind goes (an error).
52func hold(ctx context.Context, address string, log zerowrap.Logger, lock **os.File) error {
53 conn, err := connect(ctx, address)
54 if err != nil {
55 return err
56 }
57 defer func() { _ = conn.Close() }()
58 signals := make(chan *dbus.Signal, 16)
59 conn.Signal(signals)
60 // Ask for logind's departure before the lock exists: a restart between
61 // the two would go unseen.
62 if err := conn.AddMatchSignalContext(ctx, dbus.WithMatchSender(busDaemon), dbus.WithMatchInterface(busDaemon), dbus.WithMatchMember("NameOwnerChanged"), dbus.WithMatchArg(0, logind)); err != nil {
63 return fmt.Errorf("watch %s: %w", logind, err)
64 }
65 callCtx, cancel := context.WithTimeout(ctx, callTimeout)
66 defer cancel()
67 var fd dbus.UnixFD
68 if err := conn.Object(logind, logindPath).CallWithContext(callCtx, manager+".Inhibit", 0, what, "neferwl", "power keys are bound in the compositor", "block").Store(&fd); err != nil {
69 return fmt.Errorf("inhibit %s: %w", what, err)
70 }
71 // logind releases a lock when its descriptor closes.
72 old := *lock
73 *lock = os.NewFile(uintptr(fd), "logind-inhibit")
74 if old != nil {
75 _ = old.Close()
76 }
77 log.Info().Str("what", what).Msg("holding logind key locks")
78 for {
79 select {
80 case <-ctx.Done():
81 return nil
82 case sig, ok := <-signals:
83 if !ok {
84 if ctx.Err() != nil {
85 return nil // WithContext closed the connection
86 }
87 return errors.New("bus connection closed")
88 }
89 if lost(sig) {
90 return errors.New(logind + " left the bus")
91 }
92 }
93 }
94}
95
96func connect(ctx context.Context, address string) (*dbus.Conn, error) {
97 var conn *dbus.Conn
98 var err error
99 if address == "" {
100 conn, err = dbus.SystemBusPrivate(dbus.WithContext(ctx))
101 } else {
102 conn, err = dbus.Dial(address, dbus.WithContext(ctx))
103 }
104 if err != nil {
105 return nil, fmt.Errorf("bus: %w", err)
106 }
107 if err = conn.Auth(nil); err == nil {
108 err = conn.Hello()
109 }
110 if err != nil {
111 _ = conn.Close()
112 return nil, fmt.Errorf("bus: %w", err)
113 }
114 return conn, nil
115}
116
117// lost reports whether sig is the bus saying logind has no owner any more.
118func lost(sig *dbus.Signal) bool {
119 if sig.Sender != busDaemon || sig.Name != busDaemon+".NameOwnerChanged" || len(sig.Body) != 3 {
120 return false
121 }
122 name, ok1 := sig.Body[0].(string)
123 owner, ok2 := sig.Body[2].(string)
124 return ok1 && ok2 && name == logind && owner == ""
125}