返回 DeepSeek-Reasonix
tray_health_unix.go
根目录 / desktop / tray_health_unix.go
1 //go:build !windows && !darwin && cgo
2
3 package main
4
5 import (
6 "context"
7 "fmt"
8 "os"
9 "time"
10
11 "github.com/godbus/dbus/v5"
12 )
13
14 const (
15 statusNotifierWatcherName = "org.kde.StatusNotifierWatcher"
16 statusNotifierWatcherPath = dbus.ObjectPath("/StatusNotifierWatcher")
17 statusNotifierWatcherIFace = "org.kde.StatusNotifierWatcher"
18 statusNotifierPollInterval = time.Second
19 statusNotifierProbeTimeout = 750 * time.Millisecond
20 )
21
22 type statusNotifierBusConnection struct {
23 conn *dbus.Conn
24 cancel context.CancelFunc
25 }
26
27 func (c *statusNotifierBusConnection) close() {
28 if c == nil {
29 return
30 }
31 if c.cancel != nil {
32 c.cancel()
33 }
34 if c.conn != nil {
35 _ = c.conn.Close()
36 }
37 }
38
39 type statusNotifierProbe struct {
40 connection *statusNotifierBusConnection
41 connect func(context.Context) (*statusNotifierBusConnection, error)
42 }
43
44 func newStatusNotifierProbe() *statusNotifierProbe {
45 return &statusNotifierProbe{connect: connectStatusNotifierSessionBus}
46 }
47
48 func connectStatusNotifierSessionBus(ctx context.Context) (*statusNotifierBusConnection, error) {
49 type connectionResult struct {
50 conn *dbus.Conn
51 err error
52 }
53
54 connCtx, cancelConn := context.WithCancel(context.Background())
55 resultCh := make(chan connectionResult, 1)
56 go func() {
57 conn, err := dbus.SessionBusPrivateNoAutoStartup(dbus.WithContext(connCtx))
58 if err == nil {
59 err = conn.Auth(nil)
60 }
61 if err == nil {
62 err = conn.Hello()
63 }
64 resultCh <- connectionResult{conn: conn, err: err}
65 }()
66
67 select {
68 case result := <-resultCh:
69 if err := ctx.Err(); err != nil {
70 cancelConn()
71 if result.conn != nil {
72 _ = result.conn.Close()
73 }
74 return nil, err
75 }
76 if result.err != nil {
77 cancelConn()
78 if result.conn != nil {
79 _ = result.conn.Close()
80 }
81 return nil, result.err
82 }
83 return &statusNotifierBusConnection{conn: result.conn, cancel: cancelConn}, nil
84 case <-ctx.Done():
85 // Canceling the connection context closes the private transport. That
86 // interrupts Auth and Hello even though godbus does not expose
87 // context-aware variants for those operations.
88 cancelConn()
89 return nil, ctx.Err()
90 }
91 }
92
93 func (p *statusNotifierProbe) close() {
94 if p == nil {
95 return
96 }
97 p.connection.close()
98 p.connection = nil
99 }
100
101 func (p *statusNotifierProbe) reset() {
102 p.close()
103 }
104
105 func (p *statusNotifierProbe) probe(ctx context.Context, itemName string) (bool, string) {
106 if p == nil {
107 return false, "no_session_bus"
108 }
109 probeCtx, cancel := context.WithTimeout(ctx, statusNotifierProbeTimeout)
110 defer cancel()
111 if p.connection == nil {
112 connect := p.connect
113 if connect == nil {
114 connect = connectStatusNotifierSessionBus
115 }
116 connection, err := connect(probeCtx)
117 if err != nil {
118 return false, "no_session_bus"
119 }
120 p.connection = connection
121 }
122 conn := p.connection.conn
123 if err := conn.BusObject().CallWithContext(probeCtx, "org.freedesktop.DBus.Peer.Ping", 0).Err; err != nil {
124 p.reset()
125 return false, "no_session_bus"
126 }
127 snapshot, err := readStatusNotifierSnapshot(probeCtx, conn, itemName)
128 if err != nil {
129 return false, "watcher_unresponsive"
130 }
131 return evaluateStatusNotifierSnapshot(snapshot, itemName)
132 }
133
134 func readStatusNotifierSnapshot(ctx context.Context, conn *dbus.Conn, itemName string) (statusNotifierSnapshot, error) {
135 snapshot := statusNotifierSnapshot{}
136 bus := conn.Object("org.freedesktop.DBus", dbus.ObjectPath("/org/freedesktop/DBus"))
137 if err := bus.CallWithContext(ctx, "org.freedesktop.DBus.GetNameOwner", 0, statusNotifierWatcherName).Store(&snapshot.WatcherOwner); err != nil {
138 return snapshot, nil
139 }
140 if snapshot.WatcherOwner == "" {
141 return snapshot, nil
142 }
143 watcher := conn.Object(statusNotifierWatcherName, statusNotifierWatcherPath)
144 var value dbus.Variant
145 if err := watcher.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, statusNotifierWatcherIFace, "IsStatusNotifierHostRegistered").Store(&value); err != nil {
146 return snapshot, err
147 }
148 snapshot.Host, _ = value.Value().(bool)
149 if err := watcher.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, statusNotifierWatcherIFace, "RegisteredStatusNotifierItems").Store(&value); err != nil {
150 return snapshot, err
151 }
152 snapshot.Items, _ = value.Value().([]string)
153 _ = bus.CallWithContext(ctx, "org.freedesktop.DBus.GetNameOwner", 0, itemName).Store(&snapshot.ItemOwner)
154 return snapshot, nil
155 }
156
157 func (a *App) startTrayHealthMonitor(t *desktopTray) {
158 if a == nil || t == nil {
159 return
160 }
161 ctx, cancel := context.WithCancel(a.bootContext())
162 t.healthMu.Lock()
163 if t.healthStopped {
164 t.healthMu.Unlock()
165 cancel()
166 return
167 }
168 t.cancel = cancel
169 t.healthMu.Unlock()
170 a.goSafe("trayStatusNotifierMonitor", func() {
171 itemName := fmt.Sprintf("org.kde.StatusNotifierItem-%d-1", os.Getpid())
172 probe := newStatusNotifierProbe()
173 defer probe.close()
174
175 for {
176 ready, reason := probe.probe(ctx, itemName)
177 if ready {
178 a.setTrayHealth(t, "ready", "")
179 } else {
180 a.setTrayHealth(t, "unavailable", reason)
181 }
182 timer := time.NewTimer(statusNotifierPollInterval)
183 select {
184 case <-ctx.Done():
185 timer.Stop()
186 return
187 case <-timer.C:
188 }
189 }
190 })
191 }
192
193 // Linux readiness is established by the DBus monitor, not systray.onReady.
194 func (a *App) trayConfigured(*desktopTray) {}
195
195 lines GO