detached 4c78479

fix(powerkey): keep the old lock until a new one is taken, correct keysym docs, wait on log events in tests

commit
4c7847973334598a8c10b19021ad4752da942828
author
brice · · 2026-10-08 09:02 UTC
parent
e0d4819
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
refs
feat/powerkey-inhibit
3 files · +51 -22
M docs/desktop.md +10 -3
@@ -102,14 +102,21 @@ exec wlopm --off '*'
102102
103103 ## Power keys
104104
105-NeferWL takes logind's `handle-power-key`, `handle-suspend-key`, `handle-hibernate-key` and `handle-reboot-key` inhibitor locks. logind then no longer powers off, suspends, hibernates or reboots when one of those keys is pressed: the key reaches NeferWL as `XF86PowerOff`, `XF86Sleep` or `XF86Suspend`, `XF86Hibernate`, and nothing happens unless a bind uses it.
105+NeferWL takes logind's `handle-power-key`, `handle-suspend-key`, `handle-hibernate-key` and `handle-reboot-key` inhibitor locks. logind then no longer powers off, suspends, hibernates or reboots when one of those keys is pressed: the key reaches NeferWL, and nothing happens unless a bind uses it.
106+
107+| logind key | Kernel key | Bind |
108+|---|---|---|
109+| power | `KEY_POWER` | `XF86PowerOff` |
110+| suspend | `KEY_SLEEP` | `XF86Sleep` |
111+| hibernate | `KEY_SUSPEND` | `XF86Suspend` |
112+| reboot | `KEY_RESTART` | `code:408` (no keysym) |
106113
107114 ```text
108115 bind.XF86PowerOff = spawn systemctl suspend
109-bind.XF86Hibernate = spawn systemctl hibernate
116+bind.XF86Suspend = spawn systemctl hibernate
110117 ```
111118
112-The lid switch is not covered: logind still handles it. The locks last as long as NeferWL runs; if it crashes, logind handles the keys again. If the system bus or logind is missing, NeferWL logs a warning and tries again later, and the keys keep logind's behaviour until it succeeds.
119+The lid switch is not covered: logind still handles it. The locks last as long as NeferWL runs; if it crashes, logind handles the keys again. When logind restarts, NeferWL keeps the old locks until it has taken new ones. If the system bus or logind is missing, NeferWL logs a warning and tries again later, and the keys keep logind's behaviour until it succeeds.
113120
114121 ## Bars
115122
@@ -102,14 +102,21 @@ exec wlopm --off '*'
102 102
103 ## Power keys103 ## Power keys
104 104
105-NeferWL takes logind's `handle-power-key`, `handle-suspend-key`, `handle-hibernate-key` and `handle-reboot-key` inhibitor locks. logind then no longer powers off, suspends, hibernates or reboots when one of those keys is pressed: the key reaches NeferWL as `XF86PowerOff`, `XF86Sleep` or `XF86Suspend`, `XF86Hibernate`, and nothing happens unless a bind uses it.105+NeferWL takes logind's `handle-power-key`, `handle-suspend-key`, `handle-hibernate-key` and `handle-reboot-key` inhibitor locks. logind then no longer powers off, suspends, hibernates or reboots when one of those keys is pressed: the key reaches NeferWL, and nothing happens unless a bind uses it.
106+
107+| logind key | Kernel key | Bind |
108+|---|---|---|
109+| power | `KEY_POWER` | `XF86PowerOff` |
110+| suspend | `KEY_SLEEP` | `XF86Sleep` |
111+| hibernate | `KEY_SUSPEND` | `XF86Suspend` |
112+| reboot | `KEY_RESTART` | `code:408` (no keysym) |
106 113
107 ```text114 ```text
108 bind.XF86PowerOff = spawn systemctl suspend115 bind.XF86PowerOff = spawn systemctl suspend
109-bind.XF86Hibernate = spawn systemctl hibernate116+bind.XF86Suspend = spawn systemctl hibernate
110 ```117 ```
111 118
112-The lid switch is not covered: logind still handles it. The locks last as long as NeferWL runs; if it crashes, logind handles the keys again. If the system bus or logind is missing, NeferWL logs a warning and tries again later, and the keys keep logind's behaviour until it succeeds.119+The lid switch is not covered: logind still handles it. The locks last as long as NeferWL runs; if it crashes, logind handles the keys again. When logind restarts, NeferWL keeps the old locks until it has taken new ones. If the system bus or logind is missing, NeferWL logs a warning and tries again later, and the keys keep logind's behaviour until it succeeds.
113 120
114 ## Bars121 ## Bars
115 122
M internal/adapters/powerkey/powerkey.go +22 -11
@@ -31,17 +31,25 @@ const (
3131
3232 // Serve holds the locks until ctx ends. address is the bus to use, the
3333 // system bus when empty. When the bus or logind goes, it takes the locks
34-// again after retry, doubling up to busretry.Max: logind drops a lock with
35-// its own restart, and then handles the keys itself until we ask again.
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.
3638 func 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+ }()
3745 busretry.Run(ctx, retry, "logind power key lock", log, func(ctx context.Context) error {
38- return hold(ctx, address, log)
46+ return hold(ctx, address, log, &lock)
3947 })
4048 }
4149
42-// hold takes the locks and keeps them until ctx ends (nil) or the bus or
43-// logind goes (an error).
44-func hold(ctx context.Context, address string, log zerowrap.Logger) error {
50+// hold takes the locks, replaces *lock with them, and waits until ctx ends
51+// (nil) or the bus or logind goes (an error).
52+func hold(ctx context.Context, address string, log zerowrap.Logger, lock **os.File) error {
4553 conn, err := connect(ctx, address)
4654 if err != nil {
4755 return err
@@ -60,9 +68,12 @@ func hold(ctx context.Context, address string, log zerowrap.Logger) error {
6068 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 {
6169 return fmt.Errorf("inhibit %s: %w", what, err)
6270 }
63- // logind releases the lock when this descriptor closes.
64- lock := os.NewFile(uintptr(fd), "logind-inhibit")
65- defer func() { _ = lock.Close() }()
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+ }
6677 log.Info().Str("what", what).Msg("holding logind key locks")
6778 for {
6879 select {
@@ -91,14 +102,14 @@ func connect(ctx context.Context, address string) (*dbus.Conn, error) {
91102 conn, err = dbus.Dial(address, dbus.WithContext(ctx))
92103 }
93104 if err != nil {
94- return nil, fmt.Errorf("system bus: %w", err)
105+ return nil, fmt.Errorf("bus: %w", err)
95106 }
96107 if err = conn.Auth(nil); err == nil {
97108 err = conn.Hello()
98109 }
99110 if err != nil {
100111 _ = conn.Close()
101- return nil, fmt.Errorf("system bus: %w", err)
112+ return nil, fmt.Errorf("bus: %w", err)
102113 }
103114 return conn, nil
104115 }
@@ -31,17 +31,25 @@ const (
31 31
32 // Serve holds the locks until ctx ends. address is the bus to use, the32 // 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 locks33 // system bus when empty. When the bus or logind goes, it takes the locks
34-// again after retry, doubling up to busretry.Max: logind drops a lock with34+// again after retry, doubling up to a minute. Meanwhile the old lock stays
35-// its own restart, and then handles the keys itself until we ask again.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.
36 func Serve(ctx context.Context, address string, retry time.Duration, log zerowrap.Logger) {38 func 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+ }()
37 busretry.Run(ctx, retry, "logind power key lock", log, func(ctx context.Context) error {45 busretry.Run(ctx, retry, "logind power key lock", log, func(ctx context.Context) error {
38- return hold(ctx, address, log)46+ return hold(ctx, address, log, &lock)
39 })47 })
40 }48 }
41 49
42-// hold takes the locks and keeps them until ctx ends (nil) or the bus or50+// hold takes the locks, replaces *lock with them, and waits until ctx ends
43-// logind goes (an error).51+// (nil) or the bus or logind goes (an error).
44-func hold(ctx context.Context, address string, log zerowrap.Logger) error {52+func hold(ctx context.Context, address string, log zerowrap.Logger, lock **os.File) error {
45 conn, err := connect(ctx, address)53 conn, err := connect(ctx, address)
46 if err != nil {54 if err != nil {
47 return err55 return err
@@ -60,9 +68,12 @@ func hold(ctx context.Context, address string, log zerowrap.Logger) error {
60 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 {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 {
61 return fmt.Errorf("inhibit %s: %w", what, err)69 return fmt.Errorf("inhibit %s: %w", what, err)
62 }70 }
63- // logind releases the lock when this descriptor closes.71+ // logind releases a lock when its descriptor closes.
64- lock := os.NewFile(uintptr(fd), "logind-inhibit")72+ old := *lock
65- defer func() { _ = lock.Close() }()73+ *lock = os.NewFile(uintptr(fd), "logind-inhibit")
74+ if old != nil {
75+ _ = old.Close()
76+ }
66 log.Info().Str("what", what).Msg("holding logind key locks")77 log.Info().Str("what", what).Msg("holding logind key locks")
67 for {78 for {
68 select {79 select {
@@ -91,14 +102,14 @@ func connect(ctx context.Context, address string) (*dbus.Conn, error) {
91 conn, err = dbus.Dial(address, dbus.WithContext(ctx))102 conn, err = dbus.Dial(address, dbus.WithContext(ctx))
92 }103 }
93 if err != nil {104 if err != nil {
94- return nil, fmt.Errorf("system bus: %w", err)105+ return nil, fmt.Errorf("bus: %w", err)
95 }106 }
96 if err = conn.Auth(nil); err == nil {107 if err = conn.Auth(nil); err == nil {
97 err = conn.Hello()108 err = conn.Hello()
98 }109 }
99 if err != nil {110 if err != nil {
100 _ = conn.Close()111 _ = conn.Close()
101- return nil, fmt.Errorf("system bus: %w", err)112+ return nil, fmt.Errorf("bus: %w", err)
102 }113 }
103 return conn, nil114 return conn, nil
104 }115 }
M internal/adapters/powerkey/powerkey_test.go +19 -8
@@ -120,17 +120,23 @@ func (l *logBuf) count(s string) int {
120120 return strings.Count(l.buf.String(), s)
121121 }
122122
123-// holding waits until Serve has received n lock answers: only then does it
124-// own the descriptor.
125-func (l *logBuf) holding(t *testing.T, n int) {
123+// wait blocks until the log has n lines containing msg.
124+func (l *logBuf) wait(t *testing.T, msg string, n int) {
126125 t.Helper()
127- for deadline := time.Now().Add(5 * time.Second); l.count("holding logind key locks") < n; time.Sleep(5 * time.Millisecond) {
126+ for deadline := time.Now().Add(5 * time.Second); l.count(msg) < n; time.Sleep(5 * time.Millisecond) {
128127 if time.Now().After(deadline) {
129- t.Fatalf("Serve never held lock %d", n)
128+ t.Fatalf("log never had %d x %q", n, msg)
130129 }
131130 }
132131 }
133132
133+// holding waits until Serve has received n lock answers: only then does it
134+// own the descriptor.
135+func (l *logBuf) holding(t *testing.T, n int) {
136+ t.Helper()
137+ l.wait(t, "holding logind key locks", n)
138+}
139+
134140 func run(t *testing.T, address string) (stop func(), log *logBuf) {
135141 t.Helper()
136142 ctx, cancel := context.WithCancel(context.Background())
@@ -184,19 +190,24 @@ func TestServeHoldsAndReleasesTheLock(t *testing.T) {
184190 }
185191 }
186192
187-// When logind restarts, the locks are taken again.
193+// When logind restarts, the locks are taken again and the old ones closed.
188194 func TestServeTakesTheLockAgainAfterLogindRestarts(t *testing.T) {
189195 address := startBus(t)
190196 peer := newPeer(t, address)
191197 _, log := run(t, address)
192- peer.next(t)
198+ first := peer.next(t)
193199 log.holding(t, 1)
194200 if _, err := peer.conn.ReleaseName(logind); err != nil {
195201 t.Fatal(err)
196202 }
203+ log.wait(t, "ignored until the next attempt", 1)
204+ // The gap: the old lock is still ours, logind keeps it through a restart.
197205 peer.own(t)
198206 peer.next(t)
199207 log.holding(t, 2)
208+ if !released(t, first) {
209+ t.Fatal("the first lock was not closed once the second was taken")
210+ }
200211 }
201212
202213 // Without logind on the bus, Serve keeps trying until it appears.
@@ -207,7 +218,7 @@ func TestServeWaitsForLogind(t *testing.T) {
207218 t.Fatal(err)
208219 }
209220 _, log := run(t, address)
210- time.Sleep(50 * time.Millisecond)
221+ log.wait(t, "ignored until the next attempt", 1)
211222 peer.own(t)
212223 peer.next(t)
213224 log.holding(t, 1)
@@ -120,17 +120,23 @@ func (l *logBuf) count(s string) int {
120 return strings.Count(l.buf.String(), s)120 return strings.Count(l.buf.String(), s)
121 }121 }
122 122
123-// holding waits until Serve has received n lock answers: only then does it123+// wait blocks until the log has n lines containing msg.
124-// own the descriptor.124+func (l *logBuf) wait(t *testing.T, msg string, n int) {
125-func (l *logBuf) holding(t *testing.T, n int) {
126 t.Helper()125 t.Helper()
127- for deadline := time.Now().Add(5 * time.Second); l.count("holding logind key locks") < n; time.Sleep(5 * time.Millisecond) {126+ for deadline := time.Now().Add(5 * time.Second); l.count(msg) < n; time.Sleep(5 * time.Millisecond) {
128 if time.Now().After(deadline) {127 if time.Now().After(deadline) {
129- t.Fatalf("Serve never held lock %d", n)128+ t.Fatalf("log never had %d x %q", n, msg)
130 }129 }
131 }130 }
132 }131 }
133 132
133+// holding waits until Serve has received n lock answers: only then does it
134+// own the descriptor.
135+func (l *logBuf) holding(t *testing.T, n int) {
136+ t.Helper()
137+ l.wait(t, "holding logind key locks", n)
138+}
139+
134 func run(t *testing.T, address string) (stop func(), log *logBuf) {140 func run(t *testing.T, address string) (stop func(), log *logBuf) {
135 t.Helper()141 t.Helper()
136 ctx, cancel := context.WithCancel(context.Background())142 ctx, cancel := context.WithCancel(context.Background())
@@ -184,19 +190,24 @@ func TestServeHoldsAndReleasesTheLock(t *testing.T) {
184 }190 }
185 }191 }
186 192
187-// When logind restarts, the locks are taken again.193+// When logind restarts, the locks are taken again and the old ones closed.
188 func TestServeTakesTheLockAgainAfterLogindRestarts(t *testing.T) {194 func TestServeTakesTheLockAgainAfterLogindRestarts(t *testing.T) {
189 address := startBus(t)195 address := startBus(t)
190 peer := newPeer(t, address)196 peer := newPeer(t, address)
191 _, log := run(t, address)197 _, log := run(t, address)
192- peer.next(t)198+ first := peer.next(t)
193 log.holding(t, 1)199 log.holding(t, 1)
194 if _, err := peer.conn.ReleaseName(logind); err != nil {200 if _, err := peer.conn.ReleaseName(logind); err != nil {
195 t.Fatal(err)201 t.Fatal(err)
196 }202 }
203+ log.wait(t, "ignored until the next attempt", 1)
204+ // The gap: the old lock is still ours, logind keeps it through a restart.
197 peer.own(t)205 peer.own(t)
198 peer.next(t)206 peer.next(t)
199 log.holding(t, 2)207 log.holding(t, 2)
208+ if !released(t, first) {
209+ t.Fatal("the first lock was not closed once the second was taken")
210+ }
200 }211 }
201 212
202 // Without logind on the bus, Serve keeps trying until it appears.213 // Without logind on the bus, Serve keeps trying until it appears.
@@ -207,7 +218,7 @@ func TestServeWaitsForLogind(t *testing.T) {
207 t.Fatal(err)218 t.Fatal(err)
208 }219 }
209 _, log := run(t, address)220 _, log := run(t, address)
210- time.Sleep(50 * time.Millisecond)221+ log.wait(t, "ignored until the next attempt", 1)
211 peer.own(t)222 peer.own(t)
212 peer.next(t)223 peer.next(t)
213 log.holding(t, 1)224 log.holding(t, 1)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close