1package atspi
2
3import (
4 "errors"
5 "fmt"
6 "strings"
7 "sync/atomic"
8 "syscall"
9 "time"
10
11 "github.com/bnema/zerobus"
12 "golang.org/x/sys/unix"
13)
14
15// D-Bus names, paths and interfaces.
16const (
17 busDaemon = "org.freedesktop.DBus"
18 busDaemonPath = "/org/freedesktop/DBus"
19 ifaceProps = "org.freedesktop.DBus.Properties"
20
21 a11yBusName = "org.a11y.Bus"
22 a11yBusPath = "/org/a11y/bus"
23 a11yStatus = "org.a11y.Status"
24 registryName = "org.a11y.atspi.Registry"
25 registryRoot = "/org/a11y/atspi/accessible/root"
26
27 ifaceAccessible = "org.a11y.atspi.Accessible"
28 ifaceComponent = "org.a11y.atspi.Component"
29 ifaceCollection = "org.a11y.atspi.Collection"
30
31 // coordWindow is ATSPI_COORD_TYPE_WINDOW: extents relative to the
32 // top-level window.
33 coordWindow = 1
34
35 // maxInflight bounds the calls sent before a reply is read, so the bus
36 // never waits on a full socket while this side waits on it.
37 maxInflight = 64
38 // maxRefs bounds the objects read from one reply.
39 maxRefs = 4096
40)
41
42// op is a call the source makes.
43type op uint8
44
45const (
46 opChildren op = iota // Accessible.GetChildren -> refs
47 opPID // GetConnectionUnixProcessID(name) -> u
48 opRole // Accessible.GetRole -> u
49 opState // Accessible.GetState -> state
50 opExtents // Component.GetExtents(arg) -> rect
51 opMatches // Collection.GetMatches(rule) -> refs
52)
53
54// ref is an accessible object: a bus name and a path.
55type ref struct{ name, path string }
56
57// call is one request of a batch. name and path address the object; for
58// opPID, name is the bus name asked about.
59type call struct {
60 op op
61 name, path string
62 arg uint32
63 rule matchRule
64}
65
66// reply answers the call at the same index of the batch.
67type reply struct {
68 err error
69 refs []ref
70 u uint32
71 state uint64
72 rect [4]int32 // x, y, w, h
73}
74
75// transport sends batches of calls on one bus connection. It is used by one
76// goroutine at a time.
77type transport interface {
78 // Do sends calls pipelined and returns one reply per call, in order.
79 // The batch ends at deadline. A call not answered idle after it was
80 // sent gets ErrTimeout. When the connection fails or the deadline
81 // finds calls unanswered, those calls get an error wrapping
82 // ErrBroken: the connection must be dropped.
83 Do(deadline time.Time, idle time.Duration, calls []call) []reply
84 Close() error
85}
86
87// ErrTimeout is the error of a call not answered in time.
88var ErrTimeout = errors.New("atspi: call timed out")
89
90// busError is a D-Bus error reply.
91type busError struct{ name, msg string }
92
93func (e *busError) Error() string {
94 if e.msg == "" {
95 return e.name
96 }
97 return e.name + ": " + e.msg
98}
99
100// conn is a transport on a zerobus connection.
101type conn struct {
102 c *zerobus.Conn
103 raw syscall.RawConn
104}
105
106func newConn(c *zerobus.Conn) (*conn, error) {
107 raw, err := c.SyscallConn()
108 if err != nil {
109 _ = c.Close()
110 return nil, err
111 }
112 return &conn{c: c, raw: raw}, nil
113}
114
115func (b *conn) Close() error { return b.c.Close() }
116
117// readable waits until a message can be read or d passes.
118func (b *conn) readable(d time.Duration) (bool, error) {
119 if b.c.Buffered() {
120 return true, nil
121 }
122 ms := int((d + time.Millisecond - 1) / time.Millisecond)
123 if ms < 1 {
124 ms = 1
125 }
126 var n int
127 var perr error
128 if err := b.raw.Control(func(fd uintptr) {
129 fds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN}}
130 for {
131 n, perr = unix.Poll(fds, ms)
132 if perr != unix.EINTR {
133 return
134 }
135 }
136 }); err != nil {
137 return false, err
138 }
139 return n > 0, perr
140}
141
142// ErrBroken is the error of calls on a connection that failed or was closed
143// at the deadline; the owner must drop it and connect again.
144var ErrBroken = errors.New("atspi: connection broken")
145
146// errDeadline is the error of calls still unanswered at the batch deadline:
147// late replies and half-read messages leave the connection unusable.
148var errDeadline = fmt.Errorf("%w: %w", ErrBroken, ErrTimeout)
149
150// watchdogGrace is how long after the deadline the watchdog closes a socket
151// that the batch loop could not leave by itself (a stuck read or write).
152const watchdogGrace = 20 * time.Millisecond
153
154// Do sends the calls pipelined. A watchdog closes the socket shortly after
155// deadline, so neither a blocked write nor a read stuck in the middle of a
156// message can outlive the batch; the connection is then broken. Each call
157// has its own idle timeout, counted from its send: traffic for other calls
158// does not extend it.
159func (b *conn) Do(deadline time.Time, idle time.Duration, calls []call) []reply {
160 out := make([]reply, len(calls))
161 answered := make([]bool, len(calls))
162 type inflight struct {
163 idx int
164 sent time.Time
165 }
166 pending := make(map[uint32]inflight, min(len(calls), maxInflight))
167 next, done := 0, 0
168 var fired atomic.Bool
169 watchdog := time.AfterFunc(time.Until(deadline)+watchdogGrace, func() {
170 fired.Store(true)
171 _ = b.c.Close()
172 })
173 defer watchdog.Stop()
174 // finish gives err to every call without an answer.
175 finish := func(err error) []reply {
176 if !errors.Is(err, ErrBroken) && (fired.Load() || !errors.Is(err, ErrTimeout)) {
177 err = fmt.Errorf("%w: %w", ErrBroken, err)
178 }
179 if errors.Is(err, ErrBroken) {
180 _ = b.c.Close() // late replies would desynchronize the next batch
181 }
182 for i := range out {
183 if !answered[i] {
184 out[i].err = err
185 }
186 }
187 return out
188 }
189 for done < len(calls) {
190 for next < len(calls) && len(pending) < maxInflight {
191 if !time.Now().Before(deadline) {
192 return finish(errDeadline)
193 }
194 serial, err := b.send(calls[next])
195 if err != nil {
196 return finish(err)
197 }
198 pending[serial] = inflight{idx: next, sent: time.Now()}
199 next++
200 }
201 // Calls that waited longer than idle get no reply.
202 now := time.Now()
203 var oldest time.Time
204 for serial, p := range pending {
205 if now.Sub(p.sent) >= idle {
206 out[p.idx].err = ErrTimeout
207 answered[p.idx] = true
208 done++
209 delete(pending, serial)
210 } else if oldest.IsZero() || p.sent.Before(oldest) {
211 oldest = p.sent
212 }
213 }
214 if len(pending) == 0 {
215 continue
216 }
217 wait := min(time.Until(oldest.Add(idle)), time.Until(deadline))
218 if wait <= 0 {
219 if !time.Now().Before(deadline) {
220 break
221 }
222 continue
223 }
224 ok, err := b.readable(wait)
225 if err != nil {
226 return finish(err)
227 }
228 if !ok {
229 continue
230 }
231 m, err := b.c.ReadMessage()
232 if err != nil {
233 return finish(err)
234 }
235 p, found := pending[m.ReplySerial]
236 if !found || (m.Type != zerobus.TypeMethodReturn && m.Type != zerobus.TypeError) {
237 // A signal, or the late reply of an earlier batch.
238 continue
239 }
240 delete(pending, m.ReplySerial)
241 decode(m, calls[p.idx], &out[p.idx])
242 answered[p.idx] = true
243 done++
244 }
245 if done == len(calls) {
246 return out
247 }
248 return finish(errDeadline)
249}
250
251// send builds and sends one call and returns its serial.
252func (b *conn) send(c call) (uint32, error) {
253 switch c.op {
254 case opChildren:
255 b.c.NewCall(c.name, c.path, ifaceAccessible, "GetChildren", "")
256 case opPID:
257 b.c.NewCall(busDaemon, busDaemonPath, busDaemon, "GetConnectionUnixProcessID", "s").Str(c.name)
258 case opRole:
259 b.c.NewCall(c.name, c.path, ifaceAccessible, "GetRole", "")
260 case opState:
261 b.c.NewCall(c.name, c.path, ifaceAccessible, "GetState", "")
262 case opExtents:
263 b.c.NewCall(c.name, c.path, ifaceComponent, "GetExtents", "u").Uint32(c.arg)
264 case opMatches:
265 e := b.c.NewCall(c.name, c.path, ifaceCollection, "GetMatches", "(aiia{ss}iaiiasib)uib")
266 encodeRule(e, c.rule)
267 e.Uint32(sortCanonical)
268 e.Int32(matchCount)
269 e.Bool(true)
270 }
271 return b.c.Send()
272}
273
274// encodeRule writes the MatchRule struct (aiia{ss}iaiiasib): states, state
275// match, attributes, attribute match, roles, role match, interfaces,
276// interface match, invert.
277func encodeRule(e *zerobus.Encoder, r matchRule) {
278 e.Struct()
279 a := e.BeginArray('i')
280 for _, w := range stateBits(stateShowing, stateVisible) {
281 e.Int32(w)
282 }
283 e.EndArray(a)
284 e.Int32(matchAll)
285 m := e.BeginArray('{')
286 e.EndArray(m)
287 e.Int32(matchAll)
288 a = e.BeginArray('i')
289 for _, w := range roleBits(r.roles()) {
290 e.Int32(w)
291 }
292 e.EndArray(a)
293 e.Int32(matchAny)
294 s := e.BeginArray('s')
295 e.EndArray(s)
296 e.Int32(matchAll)
297 e.Bool(false)
298}
299
300// decode reads the reply m of c into out.
301func decode(m *zerobus.Message, c call, out *reply) {
302 if m.Type == zerobus.TypeError {
303 be := &busError{name: strings.Clone(m.ErrorName)}
304 if strings.HasPrefix(m.Signature, "s") {
305 be.msg = strings.Clone(m.Body().Str())
306 }
307 out.err = be
308 return
309 }
310 r := m.Body()
311 want := func(sig string) bool {
312 if m.Signature != sig {
313 out.err = fmt.Errorf("atspi: reply signature %q, want %q", m.Signature, sig)
314 return false
315 }
316 return true
317 }
318 switch c.op {
319 case opChildren, opMatches:
320 if !want("a(so)") {
321 return
322 }
323 end := r.Array('(')
324 for r.More(end) {
325 if len(out.refs) >= maxRefs {
326 break
327 }
328 r.Struct()
329 name := r.Str()
330 path := r.ObjectPath()
331 if r.Err() != nil {
332 break
333 }
334 out.refs = append(out.refs, ref{strings.Clone(name), strings.Clone(path)})
335 }
336 case opPID, opRole:
337 if want("u") {
338 out.u = r.Uint32()
339 }
340 case opState:
341 if !want("au") {
342 return
343 }
344 end := r.Array('u')
345 var words [2]uint32
346 for i := 0; r.More(end); i++ {
347 w := r.Uint32()
348 if i < len(words) {
349 words[i] = w
350 }
351 }
352 out.state = uint64(words[0]) | uint64(words[1])<<32
353 case opExtents:
354 if want("(iiii)") {
355 r.Struct()
356 out.rect = [4]int32{r.Int32(), r.Int32(), r.Int32(), r.Int32()}
357 }
358 }
359 if out.err == nil && r.Err() != nil {
360 out.err = r.Err()
361 }
362}