detached bbdfe3f

Merge pull request #5 from bnema/fix/logind-session-fallback

fix(logind): fall back to XDG_SESSION_ID when the PID has no session
commit
bbdfe3fc12c0714336ba724525d2ec601e858834
author
bnema · · 2026-10-01 10:11 UTC
committer
GitHub · · 2026-10-01 10:11 UTC
merge of
c196c81 + db1783e · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
2 files · +54 -2
M internal/adapters/logind/logind.go +23 -2
@@ -97,9 +97,9 @@ func (a *Adapter) Probe(ctx context.Context) error {
9797 a.owner = owner
9898 a.caps = ports.SystemCapabilities{}
9999 if a.requirements.Session {
100- body, err = a.call(ctx, managerPath, managerInterface+".GetSessionByPID", uint32(os.Getpid()))
100+ body, err = a.currentSession(ctx)
101101 if err != nil {
102- return fmt.Errorf("current process session unavailable: %w", err)
102+ return err
103103 }
104104 if len(body) != 1 {
105105 return errors.New("invalid session reply")
@@ -157,6 +157,27 @@ func (a *Adapter) Probe(ctx context.Context) error {
157157 }
158158 return nil
159159 }
160+
161+// currentSession resolves the process's session by PID. A compositor started
162+// as a systemd user service runs outside the session scope, so its children
163+// have no PID session; the session the display manager opened is then taken
164+// from XDG_SESSION_ID. Probe checks either result against the real UID.
165+func (a *Adapter) currentSession(ctx context.Context) ([]any, error) {
166+ body, err := a.call(ctx, managerPath, managerInterface+".GetSessionByPID", uint32(os.Getpid()))
167+ if err == nil {
168+ return body, nil
169+ }
170+ id := os.Getenv("XDG_SESSION_ID")
171+ if id == "" {
172+ return nil, fmt.Errorf("current process session unavailable: %w", err)
173+ }
174+ body, idErr := a.call(ctx, managerPath, managerInterface+".GetSession", id)
175+ if idErr != nil {
176+ return nil, fmt.Errorf("current process session unavailable: %w; XDG_SESSION_ID: %w", err, idErr)
177+ }
178+ return body, nil
179+}
180+
160181 func (a *Adapter) Capabilities() ports.SystemCapabilities {
161182 c := a.caps
162183 c.Missing = append([]string(nil), c.Missing...)
@@ -97,9 +97,9 @@ func (a *Adapter) Probe(ctx context.Context) error {
97 a.owner = owner97 a.owner = owner
98 a.caps = ports.SystemCapabilities{}98 a.caps = ports.SystemCapabilities{}
99 if a.requirements.Session {99 if a.requirements.Session {
100- body, err = a.call(ctx, managerPath, managerInterface+".GetSessionByPID", uint32(os.Getpid()))100+ body, err = a.currentSession(ctx)
101 if err != nil {101 if err != nil {
102- return fmt.Errorf("current process session unavailable: %w", err)102+ return err
103 }103 }
104 if len(body) != 1 {104 if len(body) != 1 {
105 return errors.New("invalid session reply")105 return errors.New("invalid session reply")
@@ -157,6 +157,27 @@ func (a *Adapter) Probe(ctx context.Context) error {
157 }157 }
158 return nil158 return nil
159 }159 }
160+
161+// currentSession resolves the process's session by PID. A compositor started
162+// as a systemd user service runs outside the session scope, so its children
163+// have no PID session; the session the display manager opened is then taken
164+// from XDG_SESSION_ID. Probe checks either result against the real UID.
165+func (a *Adapter) currentSession(ctx context.Context) ([]any, error) {
166+ body, err := a.call(ctx, managerPath, managerInterface+".GetSessionByPID", uint32(os.Getpid()))
167+ if err == nil {
168+ return body, nil
169+ }
170+ id := os.Getenv("XDG_SESSION_ID")
171+ if id == "" {
172+ return nil, fmt.Errorf("current process session unavailable: %w", err)
173+ }
174+ body, idErr := a.call(ctx, managerPath, managerInterface+".GetSession", id)
175+ if idErr != nil {
176+ return nil, fmt.Errorf("current process session unavailable: %w; XDG_SESSION_ID: %w", err, idErr)
177+ }
178+ return body, nil
179+}
180+
160 func (a *Adapter) Capabilities() ports.SystemCapabilities {181 func (a *Adapter) Capabilities() ports.SystemCapabilities {
161 c := a.caps182 c := a.caps
162 c.Missing = append([]string(nil), c.Missing...)183 c.Missing = append([]string(nil), c.Missing...)
M internal/adapters/logind/logind_test.go +31 -0
@@ -39,6 +39,37 @@ func TestProbeCurrentPIDRealUIDAndReadOnly(t *testing.T) {
3939 }
4040 // No mutating call was configured: generated mock rejects Inhibit/Suspend.
4141 }
42+
43+// A compositor run as a systemd user service gives its children no PID
44+// session; the display manager's XDG_SESSION_ID is used, UID-checked.
45+func TestProbeFallsBackToXDGSessionID(t *testing.T) {
46+ t.Setenv("XDG_SESSION_ID", "c1")
47+ for name, uid := range map[string]uint32{"same uid": uint32(os.Getuid()), "other uid": uint32(os.Getuid()) + 1} {
48+ t.Run(name, func(t *testing.T) {
49+ b := newMockbus(t)
50+ b.EXPECT().call(mock.Anything, "org.freedesktop.DBus", dbus.ObjectPath("/org/freedesktop/DBus"), "org.freedesktop.DBus.GetNameOwner", []any{service}).Return([]any{testOwner}, nil).Once()
51+ b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".GetSessionByPID", []any{uint32(os.Getpid())}).Return(nil, errors.New("no session")).Once()
52+ b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".GetSession", []any{"c1"}).Return([]any{testSession}, nil).Once()
53+ b.EXPECT().call(mock.Anything, testOwner, testSession, "org.freedesktop.DBus.Properties.Get", []any{sessionInterface, "User"}).Return([]any{dbus.MakeVariant([]any{uid, dbus.ObjectPath("/org/freedesktop/login1/user/_1000")})}, nil).Once()
54+ a := newAdapter(b, ports.SystemRequirements{Session: true})
55+ err := a.Probe(context.Background())
56+ if ok := uid == uint32(os.Getuid()); ok != (err == nil) || ok != a.Capabilities().Session || ok != (a.session == testSession) {
57+ t.Fatalf("err=%v caps=%+v session=%q", err, a.Capabilities(), a.session)
58+ }
59+ })
60+ }
61+}
62+
63+func TestProbeWithoutPIDSessionOrXDGSessionIDFails(t *testing.T) {
64+ t.Setenv("XDG_SESSION_ID", "")
65+ b := newMockbus(t)
66+ b.EXPECT().call(mock.Anything, "org.freedesktop.DBus", dbus.ObjectPath("/org/freedesktop/DBus"), "org.freedesktop.DBus.GetNameOwner", []any{service}).Return([]any{testOwner}, nil).Once()
67+ b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".GetSessionByPID", []any{uint32(os.Getpid())}).Return(nil, errors.New("no session")).Once()
68+ if err := newAdapter(b, ports.SystemRequirements{Session: true}).Probe(context.Background()); err == nil {
69+ t.Fatal("probe succeeded without any session")
70+ }
71+}
72+
4273 func TestProbeRejectsOtherUID(t *testing.T) {
4374 b := newMockbus(t)
4475 expectProbe(t, b, uint32(os.Getuid())+1)
@@ -39,6 +39,37 @@ func TestProbeCurrentPIDRealUIDAndReadOnly(t *testing.T) {
39 }39 }
40 // No mutating call was configured: generated mock rejects Inhibit/Suspend.40 // No mutating call was configured: generated mock rejects Inhibit/Suspend.
41 }41 }
42+
43+// A compositor run as a systemd user service gives its children no PID
44+// session; the display manager's XDG_SESSION_ID is used, UID-checked.
45+func TestProbeFallsBackToXDGSessionID(t *testing.T) {
46+ t.Setenv("XDG_SESSION_ID", "c1")
47+ for name, uid := range map[string]uint32{"same uid": uint32(os.Getuid()), "other uid": uint32(os.Getuid()) + 1} {
48+ t.Run(name, func(t *testing.T) {
49+ b := newMockbus(t)
50+ b.EXPECT().call(mock.Anything, "org.freedesktop.DBus", dbus.ObjectPath("/org/freedesktop/DBus"), "org.freedesktop.DBus.GetNameOwner", []any{service}).Return([]any{testOwner}, nil).Once()
51+ b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".GetSessionByPID", []any{uint32(os.Getpid())}).Return(nil, errors.New("no session")).Once()
52+ b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".GetSession", []any{"c1"}).Return([]any{testSession}, nil).Once()
53+ b.EXPECT().call(mock.Anything, testOwner, testSession, "org.freedesktop.DBus.Properties.Get", []any{sessionInterface, "User"}).Return([]any{dbus.MakeVariant([]any{uid, dbus.ObjectPath("/org/freedesktop/login1/user/_1000")})}, nil).Once()
54+ a := newAdapter(b, ports.SystemRequirements{Session: true})
55+ err := a.Probe(context.Background())
56+ if ok := uid == uint32(os.Getuid()); ok != (err == nil) || ok != a.Capabilities().Session || ok != (a.session == testSession) {
57+ t.Fatalf("err=%v caps=%+v session=%q", err, a.Capabilities(), a.session)
58+ }
59+ })
60+ }
61+}
62+
63+func TestProbeWithoutPIDSessionOrXDGSessionIDFails(t *testing.T) {
64+ t.Setenv("XDG_SESSION_ID", "")
65+ b := newMockbus(t)
66+ b.EXPECT().call(mock.Anything, "org.freedesktop.DBus", dbus.ObjectPath("/org/freedesktop/DBus"), "org.freedesktop.DBus.GetNameOwner", []any{service}).Return([]any{testOwner}, nil).Once()
67+ b.EXPECT().call(mock.Anything, testOwner, managerPath, managerInterface+".GetSessionByPID", []any{uint32(os.Getpid())}).Return(nil, errors.New("no session")).Once()
68+ if err := newAdapter(b, ports.SystemRequirements{Session: true}).Probe(context.Background()); err == nil {
69+ t.Fatal("probe succeeded without any session")
70+ }
71+}
72+
42 func TestProbeRejectsOtherUID(t *testing.T) {73 func TestProbeRejectsOtherUID(t *testing.T) {
43 b := newMockbus(t)74 b := newMockbus(t)
44 expectProbe(t, b, uint32(os.Getuid())+1)75 expectProbe(t, b, uint32(os.Getuid())+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 2

Tags 9

↑↓ moveenter openesc close