返回 DeepSeek-Reasonix
tray_health_unix_test.go
根目录 / desktop / tray_health_unix_test.go
1 //go:build !windows && !darwin && cgo
2
3 package main
4
5 import (
6 "bufio"
7 "context"
8 "errors"
9 "fmt"
10 "io"
11 "net"
12 "os"
13 "os/exec"
14 "path/filepath"
15 "strings"
16 "sync"
17 "testing"
18 "time"
19
20 "github.com/godbus/dbus/v5"
21 )
22
23 type fakeStatusNotifierWatcher struct {
24 mu sync.RWMutex
25 host bool
26 items []string
27 }
28
29 func (w *fakeStatusNotifierWatcher) set(host bool, items []string) {
30 w.mu.Lock()
31 w.host = host
32 w.items = append([]string(nil), items...)
33 w.mu.Unlock()
34 }
35
36 func (w *fakeStatusNotifierWatcher) Get(iface, property string) (dbus.Variant, *dbus.Error) {
37 if iface != statusNotifierWatcherIFace {
38 return dbus.Variant{}, dbus.NewError("org.freedesktop.DBus.Error.UnknownInterface", []any{iface})
39 }
40 w.mu.RLock()
41 defer w.mu.RUnlock()
42 switch property {
43 case "IsStatusNotifierHostRegistered":
44 return dbus.MakeVariant(w.host), nil
45 case "RegisteredStatusNotifierItems":
46 return dbus.MakeVariant(append([]string(nil), w.items...)), nil
47 default:
48 return dbus.Variant{}, dbus.NewError("org.freedesktop.DBus.Error.UnknownProperty", []any{property})
49 }
50 }
51
52 func startPrivateDBusDaemon(t *testing.T) string {
53 t.Helper()
54 path, err := exec.LookPath("dbus-daemon")
55 if err != nil {
56 t.Skip("dbus-daemon is required for the StatusNotifier integration test")
57 }
58 cmd := exec.Command(path, "--session", "--nofork", "--nopidfile", "--print-address=1")
59 stdout, err := cmd.StdoutPipe()
60 if err != nil {
61 t.Fatal(err)
62 }
63 cmd.Stderr = io.Discard
64 if err := cmd.Start(); err != nil {
65 t.Fatal(err)
66 }
67 waitCh := make(chan error, 1)
68 go func() { waitCh <- cmd.Wait() }()
69 t.Cleanup(func() {
70 _ = cmd.Process.Signal(os.Interrupt)
71 select {
72 case <-waitCh:
73 case <-time.After(2 * time.Second):
74 _ = cmd.Process.Kill()
75 <-waitCh
76 }
77 })
78
79 type addressResult struct {
80 address string
81 err error
82 }
83 addressCh := make(chan addressResult, 1)
84 go func() {
85 line, readErr := bufio.NewReader(stdout).ReadString('\n')
86 addressCh <- addressResult{address: strings.TrimSpace(line), err: readErr}
87 }()
88 select {
89 case result := <-addressCh:
90 if result.err != nil {
91 t.Fatalf("read private dbus address: %v", result.err)
92 }
93 if result.address == "" {
94 t.Fatal("private dbus daemon returned an empty address")
95 }
96 return result.address
97 case <-time.After(5 * time.Second):
98 t.Fatal("private dbus daemon did not publish its address")
99 return ""
100 }
101 }
102
103 func connectTestBus(t *testing.T, address string) *dbus.Conn {
104 t.Helper()
105 conn, err := dbus.Connect(address)
106 if err != nil {
107 t.Fatal(err)
108 }
109 t.Cleanup(func() { _ = conn.Close() })
110 return conn
111 }
112
113 func requestTestBusName(t *testing.T, conn *dbus.Conn, name string) {
114 t.Helper()
115 reply, err := conn.RequestName(name, dbus.NameFlagDoNotQueue)
116 if err != nil {
117 t.Fatal(err)
118 }
119 if reply != dbus.RequestNameReplyPrimaryOwner {
120 t.Fatalf("request %s: got reply %d", name, reply)
121 }
122 }
123
124 func releaseTestBusName(t *testing.T, conn *dbus.Conn, name string) {
125 t.Helper()
126 reply, err := conn.ReleaseName(name)
127 if err != nil {
128 t.Fatal(err)
129 }
130 if reply != dbus.ReleaseNameReplyReleased {
131 t.Fatalf("release %s: got reply %d", name, reply)
132 }
133 }
134
135 func installFakeStatusNotifierWatcher(t *testing.T, conn *dbus.Conn, watcher *fakeStatusNotifierWatcher) {
136 t.Helper()
137 if err := conn.Export(watcher, statusNotifierWatcherPath, "org.freedesktop.DBus.Properties"); err != nil {
138 t.Fatal(err)
139 }
140 requestTestBusName(t, conn, statusNotifierWatcherName)
141 }
142
143 func TestStatusNotifierProbeIntegration(t *testing.T) {
144 address := startPrivateDBusDaemon(t)
145 t.Setenv("DBUS_SESSION_BUS_ADDRESS", address)
146 const itemName = "org.kde.StatusNotifierItem-4242-1"
147
148 probe := newStatusNotifierProbe()
149 t.Cleanup(probe.close)
150 assertState := func(wantReady bool, wantReason string) {
151 t.Helper()
152 ready, reason := probe.probe(context.Background(), itemName)
153 if ready != wantReady || reason != wantReason {
154 t.Fatalf("probe = (%v, %q), want (%v, %q)", ready, reason, wantReady, wantReason)
155 }
156 }
157
158 assertState(false, "no_watcher")
159
160 watcherConn := connectTestBus(t, address)
161 watcher := &fakeStatusNotifierWatcher{}
162 installFakeStatusNotifierWatcher(t, watcherConn, watcher)
163 assertState(false, "no_host")
164
165 watcher.set(true, []string{itemName + "/StatusNotifierItem"})
166 assertState(false, "item_no_owner")
167
168 itemConn := connectTestBus(t, address)
169 requestTestBusName(t, itemConn, itemName)
170 watcher.set(true, nil)
171 assertState(false, "item_not_registered")
172
173 watcher.set(true, []string{itemName + "/StatusNotifierItem"})
174 assertState(true, "")
175
176 watcher.set(false, []string{itemName + "/StatusNotifierItem"})
177 assertState(false, "no_host")
178 watcher.set(true, []string{itemName + "/StatusNotifierItem"})
179 assertState(true, "")
180
181 releaseTestBusName(t, watcherConn, statusNotifierWatcherName)
182 assertState(false, "no_watcher")
183
184 replacementConn := connectTestBus(t, address)
185 replacement := &fakeStatusNotifierWatcher{}
186 replacement.set(true, []string{itemName + "/StatusNotifierItem"})
187 installFakeStatusNotifierWatcher(t, replacementConn, replacement)
188 assertState(true, "")
189
190 releaseTestBusName(t, itemConn, itemName)
191 assertState(false, "item_no_owner")
192 replacementItemConn := connectTestBus(t, address)
193 requestTestBusName(t, replacementItemConn, itemName)
194 assertState(true, "")
195 }
196
197 func startStallingDBusSocket(t *testing.T, serve func(*net.UnixConn) error) (string, <-chan error) {
198 t.Helper()
199 socketPath := filepath.Join(t.TempDir(), "bus.sock")
200 listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: socketPath, Net: "unix"})
201 if err != nil {
202 t.Fatal(err)
203 }
204 t.Cleanup(func() { _ = listener.Close() })
205 done := make(chan error, 1)
206 go func() {
207 conn, acceptErr := listener.AcceptUnix()
208 if acceptErr != nil {
209 done <- acceptErr
210 return
211 }
212 defer conn.Close()
213 done <- serve(conn)
214 }()
215 return "unix:path=" + dbus.EscapeBusAddressValue(socketPath), done
216 }
217
218 func waitForConnectionResult(t *testing.T, resultCh <-chan error, serverDone <-chan error) {
219 t.Helper()
220 select {
221 case err := <-resultCh:
222 if !errors.Is(err, context.Canceled) {
223 t.Fatalf("connect error = %v, want context.Canceled", err)
224 }
225 case <-time.After(2 * time.Second):
226 t.Fatal("canceled DBus connection did not return")
227 }
228 select {
229 case err := <-serverDone:
230 if err != nil && !errors.Is(err, net.ErrClosed) {
231 t.Fatalf("fake DBus server: %v", err)
232 }
233 case <-time.After(2 * time.Second):
234 t.Fatal("canceled DBus connection did not close its transport")
235 }
236 }
237
238 func TestConnectStatusNotifierSessionBusCancellationInterruptsAuth(t *testing.T) {
239 accepted := make(chan struct{})
240 address, serverDone := startStallingDBusSocket(t, func(conn *net.UnixConn) error {
241 close(accepted)
242 _, err := io.Copy(io.Discard, conn)
243 return err
244 })
245 t.Setenv("DBUS_SESSION_BUS_ADDRESS", address)
246 ctx, cancel := context.WithCancel(context.Background())
247 defer cancel()
248 resultCh := make(chan error, 1)
249 go func() {
250 connection, err := connectStatusNotifierSessionBus(ctx)
251 if connection != nil {
252 connection.close()
253 }
254 resultCh <- err
255 }()
256 select {
257 case <-accepted:
258 cancel()
259 case <-time.After(2 * time.Second):
260 t.Fatal("fake DBus server did not accept the connection")
261 }
262 waitForConnectionResult(t, resultCh, serverDone)
263 }
264
265 func serveUntilHello(conn *net.UnixConn, helloStarted chan<- struct{}) error {
266 reader := bufio.NewReader(conn)
267 first, err := reader.ReadByte()
268 if err != nil {
269 return err
270 }
271 if first != 0 {
272 return fmt.Errorf("authentication prefix = %d, want 0", first)
273 }
274 readLine := func(wantPrefix string) error {
275 line, readErr := reader.ReadString('\n')
276 if readErr != nil {
277 return readErr
278 }
279 if !strings.HasPrefix(strings.TrimSpace(line), wantPrefix) {
280 return fmt.Errorf("authentication line = %q, want prefix %q", line, wantPrefix)
281 }
282 return nil
283 }
284 writeLine := func(line string) error {
285 _, writeErr := io.WriteString(conn, line+"\r\n")
286 return writeErr
287 }
288 if err := readLine("AUTH"); err != nil {
289 return err
290 }
291 if err := writeLine("REJECTED EXTERNAL"); err != nil {
292 return err
293 }
294 if err := readLine("AUTH EXTERNAL"); err != nil {
295 return err
296 }
297 if err := writeLine("OK 0123456789abcdef0123456789abcdef"); err != nil {
298 return err
299 }
300 if err := readLine("NEGOTIATE_UNIX_FD"); err != nil {
301 return err
302 }
303 if err := writeLine("ERROR"); err != nil {
304 return err
305 }
306 if err := readLine("BEGIN"); err != nil {
307 return err
308 }
309 if _, err := reader.ReadByte(); err != nil {
310 return err
311 }
312 close(helloStarted)
313 _, err = io.Copy(io.Discard, reader)
314 return err
315 }
316
317 func TestConnectStatusNotifierSessionBusCancellationInterruptsHello(t *testing.T) {
318 helloStarted := make(chan struct{})
319 address, serverDone := startStallingDBusSocket(t, func(conn *net.UnixConn) error {
320 return serveUntilHello(conn, helloStarted)
321 })
322 t.Setenv("DBUS_SESSION_BUS_ADDRESS", address)
323 ctx, cancel := context.WithCancel(context.Background())
324 defer cancel()
325 resultCh := make(chan error, 1)
326 go func() {
327 connection, err := connectStatusNotifierSessionBus(ctx)
328 if connection != nil {
329 connection.close()
330 }
331 resultCh <- err
332 }()
333 select {
334 case <-helloStarted:
335 cancel()
336 case err := <-serverDone:
337 t.Fatalf("fake DBus server exited before Hello cancellation: %v", err)
338 case <-time.After(2 * time.Second):
339 t.Fatal("client did not start the DBus Hello call")
340 }
341 waitForConnectionResult(t, resultCh, serverDone)
342 }
343
343 lines GO