Merge pull request #5 from bnema/fix/logind-session-fallback
fix(logind): fall back to XDG_SESSION_ID when the PID has no session
2 files · +54 -2
M internal/adapters/logind/logind.go +23 -2
| @@ -97,9 +97,9 @@ func (a *Adapter) Probe(ctx context.Context) error { | |||
| 97 | 97 | a.owner = owner | |
| 98 | 98 | a.caps = ports.SystemCapabilities{} | |
| 99 | 99 | if a.requirements.Session { | |
| 100 | - | body, err = a.call(ctx, managerPath, managerInterface+".GetSessionByPID", uint32(os.Getpid())) | |
| 100 | + | body, err = a.currentSession(ctx) | |
| 101 | 101 | if err != nil { | |
| 102 | - | return fmt.Errorf("current process session unavailable: %w", err) | |
| 102 | + | return err | |
| 103 | 103 | } | |
| 104 | 104 | if len(body) != 1 { | |
| 105 | 105 | return errors.New("invalid session reply") | |
| @@ -157,6 +157,27 @@ func (a *Adapter) Probe(ctx context.Context) error { | |||
| 157 | 157 | } | |
| 158 | 158 | 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 | 181 | func (a *Adapter) Capabilities() ports.SystemCapabilities { | |
| 161 | 182 | c := a.caps | |
| 162 | 183 | c.Missing = append([]string(nil), c.Missing...) | |
| @@ -97,9 +97,9 @@ func (a *Adapter) Probe(ctx context.Context) error { | |||||
| 97 | a.owner = owner | 97 | 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 nil | 158 | 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.caps | 182 | 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) { | |||
| 39 | 39 | } | |
| 40 | 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 | 73 | func TestProbeRejectsOtherUID(t *testing.T) { | |
| 43 | 74 | b := newMockbus(t) | |
| 44 | 75 | 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) | ||