main f2d166b

transport.go RawHistoryFolder

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}

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 25

Tags 10

↑↓ moveenter openesc close