detached 246c068

fix(idle): track portal Response signal to avoid close errors on completed requests

commit
246c06857649948507c0fe2bc761883b3966a952
author
bnema · · 2025-12-25 11:45 UTC
parent
351fdaf
signature
GPG Unverified2B82207D370127A0 The key is unknown
1 file · +79 -17
M internal/infrastructure/idle/portal_inhibitor.go +79 -17
@@ -30,11 +30,12 @@ var _ port.IdleInhibitor = (*PortalInhibitor)(nil)
3030 // PortalInhibitor implements idle inhibition using XDG Desktop Portal.
3131 // This works on Wayland with any compositor (GNOME, KDE, sway, hyprland, etc.).
3232 type PortalInhibitor struct {
33- conn *dbus.Conn
34- requestPath dbus.ObjectPath // Active inhibit request handle
35- refcount int
36- supported bool
37- mu sync.Mutex
33+ conn *dbus.Conn
34+ requestPath dbus.ObjectPath // Active inhibit request handle
35+ refcount int
36+ supported bool
37+ requestComplete bool // True if the portal sent a Response signal (request no longer exists)
38+ mu sync.Mutex
3839 }
3940
4041 // NewPortalInhibitor creates a new portal-based idle inhibitor.
@@ -113,6 +114,12 @@ func (p *PortalInhibitor) Inhibit(ctx context.Context, reason string) error {
113114 }
114115
115116 p.requestPath = handlePath
117+ p.requestComplete = false
118+
119+ // Subscribe to the Response signal to know when the inhibition ends
120+ // Some portals complete the request immediately with a Response signal
121+ go p.watchForResponse(ctx, handlePath)
122+
116123 log.Info().
117124 Str("handle", string(handlePath)).
118125 Str("reason", reason).
@@ -121,6 +128,59 @@ func (p *PortalInhibitor) Inhibit(ctx context.Context, reason string) error {
121128 return nil
122129 }
123130
131+// watchForResponse monitors for the Response signal on the request object.
132+// Some portals (particularly GNOME) complete the Inhibit request immediately,
133+// which removes the Request object. We need to track this to avoid calling
134+// Close on a non-existent object.
135+func (p *PortalInhibitor) watchForResponse(ctx context.Context, handlePath dbus.ObjectPath) {
136+ log := logging.FromContext(ctx)
137+
138+ if p.conn == nil {
139+ return
140+ }
141+
142+ // Add match rule for the Response signal on this specific request
143+ matchRule := fmt.Sprintf(
144+ "type='signal',interface='%s',member='Response',path='%s'",
145+ requestIface, handlePath,
146+ )
147+
148+ if err := p.conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, matchRule).Err; err != nil {
149+ log.Debug().Err(err).Msg("idle inhibitor: failed to add signal match")
150+ return
151+ }
152+
153+ // Create a channel to receive signals
154+ signals := make(chan *dbus.Signal, 1)
155+ p.conn.Signal(signals)
156+
157+ defer func() {
158+ p.conn.RemoveSignal(signals)
159+ _ = p.conn.BusObject().Call("org.freedesktop.DBus.RemoveMatch", 0, matchRule).Err
160+ }()
161+
162+ // Wait for either a Response signal or context cancellation
163+ for {
164+ select {
165+ case sig := <-signals:
166+ if sig == nil {
167+ return
168+ }
169+ if sig.Path == handlePath && sig.Name == requestIface+".Response" {
170+ p.mu.Lock()
171+ p.requestComplete = true
172+ p.mu.Unlock()
173+ log.Debug().
174+ Str("handle", string(handlePath)).
175+ Msg("idle inhibitor: request completed by portal")
176+ return
177+ }
178+ case <-ctx.Done():
179+ return
180+ }
181+ }
182+}
183+
124184 // Uninhibit decrements the refcount. When zero, releases inhibition.
125185 func (p *PortalInhibitor) Uninhibit(ctx context.Context) error {
126186 log := logging.FromContext(ctx)
@@ -145,17 +205,19 @@ func (p *PortalInhibitor) Uninhibit(ctx context.Context) error {
145205 return nil
146206 }
147207
208+ // If the portal already completed the request with a Response signal,
209+ // the Request object no longer exists - don't try to close it
210+ if p.requestComplete {
211+ log.Info().Msg("idle inhibitor: deactivated (completed by portal)")
212+ p.requestPath = ""
213+ return nil
214+ }
215+
148216 // Close the request to release inhibition
149217 obj := p.conn.Object(portalDest, p.requestPath)
150- err := obj.Call(requestIface+".Close", 0).Err
151-
152- if err != nil {
153- log.Warn().Err(err).Msg("idle inhibitor: failed to close request")
154- // Don't return error - the request may have already been closed
155- } else {
156- log.Info().Msg("idle inhibitor: deactivated")
157- }
218+ _ = obj.Call(requestIface+".Close", 0).Err
158219
220+ log.Info().Msg("idle inhibitor: deactivated")
159221 p.requestPath = ""
160222 return nil
161223 }
@@ -172,13 +234,13 @@ func (p *PortalInhibitor) Close() error {
172234 p.mu.Lock()
173235 defer p.mu.Unlock()
174236
175- // Release any active inhibition
176- if p.conn != nil && p.requestPath != "" {
237+ // Release any active inhibition (only if not already completed by portal)
238+ if p.conn != nil && p.requestPath != "" && !p.requestComplete {
177239 obj := p.conn.Object(portalDest, p.requestPath)
178240 _ = obj.Call(requestIface+".Close", 0).Err
179- p.requestPath = ""
180241 }
181-
242+ p.requestPath = ""
243+ p.requestComplete = false
182244 p.refcount = 0
183245
184246 if p.conn != nil {
@@ -30,11 +30,12 @@ var _ port.IdleInhibitor = (*PortalInhibitor)(nil)
30 // PortalInhibitor implements idle inhibition using XDG Desktop Portal.30 // PortalInhibitor implements idle inhibition using XDG Desktop Portal.
31 // This works on Wayland with any compositor (GNOME, KDE, sway, hyprland, etc.).31 // This works on Wayland with any compositor (GNOME, KDE, sway, hyprland, etc.).
32 type PortalInhibitor struct {32 type PortalInhibitor struct {
33- conn *dbus.Conn33+ conn *dbus.Conn
34- requestPath dbus.ObjectPath // Active inhibit request handle34+ requestPath dbus.ObjectPath // Active inhibit request handle
35- refcount int35+ refcount int
36- supported bool36+ supported bool
37- mu sync.Mutex37+ requestComplete bool // True if the portal sent a Response signal (request no longer exists)
38+ mu sync.Mutex
38 }39 }
39 40
40 // NewPortalInhibitor creates a new portal-based idle inhibitor.41 // NewPortalInhibitor creates a new portal-based idle inhibitor.
@@ -113,6 +114,12 @@ func (p *PortalInhibitor) Inhibit(ctx context.Context, reason string) error {
113 }114 }
114 115
115 p.requestPath = handlePath116 p.requestPath = handlePath
117+ p.requestComplete = false
118+
119+ // Subscribe to the Response signal to know when the inhibition ends
120+ // Some portals complete the request immediately with a Response signal
121+ go p.watchForResponse(ctx, handlePath)
122+
116 log.Info().123 log.Info().
117 Str("handle", string(handlePath)).124 Str("handle", string(handlePath)).
118 Str("reason", reason).125 Str("reason", reason).
@@ -121,6 +128,59 @@ func (p *PortalInhibitor) Inhibit(ctx context.Context, reason string) error {
121 return nil128 return nil
122 }129 }
123 130
131+// watchForResponse monitors for the Response signal on the request object.
132+// Some portals (particularly GNOME) complete the Inhibit request immediately,
133+// which removes the Request object. We need to track this to avoid calling
134+// Close on a non-existent object.
135+func (p *PortalInhibitor) watchForResponse(ctx context.Context, handlePath dbus.ObjectPath) {
136+ log := logging.FromContext(ctx)
137+
138+ if p.conn == nil {
139+ return
140+ }
141+
142+ // Add match rule for the Response signal on this specific request
143+ matchRule := fmt.Sprintf(
144+ "type='signal',interface='%s',member='Response',path='%s'",
145+ requestIface, handlePath,
146+ )
147+
148+ if err := p.conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, matchRule).Err; err != nil {
149+ log.Debug().Err(err).Msg("idle inhibitor: failed to add signal match")
150+ return
151+ }
152+
153+ // Create a channel to receive signals
154+ signals := make(chan *dbus.Signal, 1)
155+ p.conn.Signal(signals)
156+
157+ defer func() {
158+ p.conn.RemoveSignal(signals)
159+ _ = p.conn.BusObject().Call("org.freedesktop.DBus.RemoveMatch", 0, matchRule).Err
160+ }()
161+
162+ // Wait for either a Response signal or context cancellation
163+ for {
164+ select {
165+ case sig := <-signals:
166+ if sig == nil {
167+ return
168+ }
169+ if sig.Path == handlePath && sig.Name == requestIface+".Response" {
170+ p.mu.Lock()
171+ p.requestComplete = true
172+ p.mu.Unlock()
173+ log.Debug().
174+ Str("handle", string(handlePath)).
175+ Msg("idle inhibitor: request completed by portal")
176+ return
177+ }
178+ case <-ctx.Done():
179+ return
180+ }
181+ }
182+}
183+
124 // Uninhibit decrements the refcount. When zero, releases inhibition.184 // Uninhibit decrements the refcount. When zero, releases inhibition.
125 func (p *PortalInhibitor) Uninhibit(ctx context.Context) error {185 func (p *PortalInhibitor) Uninhibit(ctx context.Context) error {
126 log := logging.FromContext(ctx)186 log := logging.FromContext(ctx)
@@ -145,17 +205,19 @@ func (p *PortalInhibitor) Uninhibit(ctx context.Context) error {
145 return nil205 return nil
146 }206 }
147 207
208+ // If the portal already completed the request with a Response signal,
209+ // the Request object no longer exists - don't try to close it
210+ if p.requestComplete {
211+ log.Info().Msg("idle inhibitor: deactivated (completed by portal)")
212+ p.requestPath = ""
213+ return nil
214+ }
215+
148 // Close the request to release inhibition216 // Close the request to release inhibition
149 obj := p.conn.Object(portalDest, p.requestPath)217 obj := p.conn.Object(portalDest, p.requestPath)
150- err := obj.Call(requestIface+".Close", 0).Err218+ _ = obj.Call(requestIface+".Close", 0).Err
151-
152- if err != nil {
153- log.Warn().Err(err).Msg("idle inhibitor: failed to close request")
154- // Don't return error - the request may have already been closed
155- } else {
156- log.Info().Msg("idle inhibitor: deactivated")
157- }
158 219
220+ log.Info().Msg("idle inhibitor: deactivated")
159 p.requestPath = ""221 p.requestPath = ""
160 return nil222 return nil
161 }223 }
@@ -172,13 +234,13 @@ func (p *PortalInhibitor) Close() error {
172 p.mu.Lock()234 p.mu.Lock()
173 defer p.mu.Unlock()235 defer p.mu.Unlock()
174 236
175- // Release any active inhibition237+ // Release any active inhibition (only if not already completed by portal)
176- if p.conn != nil && p.requestPath != "" {238+ if p.conn != nil && p.requestPath != "" && !p.requestComplete {
177 obj := p.conn.Object(portalDest, p.requestPath)239 obj := p.conn.Object(portalDest, p.requestPath)
178 _ = obj.Call(requestIface+".Close", 0).Err240 _ = obj.Call(requestIface+".Close", 0).Err
179- p.requestPath = ""
180 }241 }
181-242+ p.requestPath = ""
243+ p.requestComplete = false
182 p.refcount = 0244 p.refcount = 0
183 245
184 if p.conn != nil {246 if p.conn != nil {

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 8

Tags 42

↑↓ moveenter openesc close