v0.1.0 2c367cd

conn.go RawHistoryFolder

1package zerobus
2
3import (
4	"bytes"
5	"encoding/binary"
6	"errors"
7	"fmt"
8	"io"
9	"net"
10	"os"
11	"strconv"
12	"strings"
13)
14
15const (
16	busName      = "org.freedesktop.DBus"
17	busPath      = "/org/freedesktop/DBus"
18	maxAuthLine  = 512
19	defaultBufSz = 16 << 10
20)
21
22var (
23	// ErrClosed reports a read or send on a closed or broken connection.
24	ErrClosed = errors.New("zerobus: connection closed")
25	// ErrNested reports a Call or ReadMessage made from inside Conn.Handle.
26	ErrNested = errors.New("zerobus: Call or ReadMessage inside Handle")
27)
28
29// Error is a D-Bus error reply.
30type Error struct {
31	Name    string // such as org.freedesktop.DBus.Error.ServiceUnknown
32	Message string // the first string of the body, if any
33}
34
35func (e *Error) Error() string {
36	if e.Message == "" {
37		return e.Name
38	}
39	return e.Name + ": " + e.Message
40}
41
42// Conn is a connection to a message bus.
43//
44// A Conn is not safe for concurrent use: read and send from one goroutine,
45// usually an event loop around ReadMessage. Close may be called from any
46// goroutine to stop a blocked read.
47type Conn struct {
48	c      *net.UnixConn
49	serial uint32
50	name   string
51
52	rbuf       []byte // received bytes; rbuf[start:end] are not read yet
53	start, end int
54	msg        Message
55	enc        Encoder
56	err        error // sticky: the connection is unusable after it
57	handling   bool  // inside Handle
58
59	// Handle receives the messages that arrive while Call waits for its
60	// reply. It may be nil; those messages are then dropped.
61	//
62	// Handle may answer or emit messages with NewReply, NewError,
63	// NewSignal and Send. It may not wait: Call and ReadMessage return
64	// ErrNested inside it.
65	Handle func(*Message)
66}
67
68// SessionBus connects to the session bus named by
69// DBUS_SESSION_BUS_ADDRESS, or $XDG_RUNTIME_DIR/bus when it is not set.
70func SessionBus() (*Conn, error) {
71	addr := os.Getenv("DBUS_SESSION_BUS_ADDRESS")
72	if addr == "" {
73		dir := os.Getenv("XDG_RUNTIME_DIR")
74		if dir == "" {
75			return nil, errors.New("zerobus: no session bus: DBUS_SESSION_BUS_ADDRESS and XDG_RUNTIME_DIR are not set")
76		}
77		return dialSocket(dir + "/bus")
78	}
79	return Dial(addr)
80}
81
82// SystemBus connects to the system bus named by DBUS_SYSTEM_BUS_ADDRESS, or
83// the standard socket when it is not set.
84func SystemBus() (*Conn, error) {
85	addr := os.Getenv("DBUS_SYSTEM_BUS_ADDRESS")
86	if addr == "" {
87		addr = "unix:path=/run/dbus/system_bus_socket"
88	}
89	return Dial(addr)
90}
91
92// Dial connects to a bus address such as "unix:path=/run/user/1000/bus",
93// authenticates, and registers with the bus. Only unix:path and
94// unix:abstract addresses are supported; with several addresses separated by
95// ';', the first that connects is used.
96func Dial(address string) (*Conn, error) {
97	var errs []error
98	for a := range strings.SplitSeq(address, ";") {
99		if a == "" {
100			continue
101		}
102		c, err := dialOne(a)
103		if err == nil {
104			return c, nil
105		}
106		errs = append(errs, err)
107	}
108	if len(errs) == 0 {
109		return nil, fmt.Errorf("zerobus: empty bus address")
110	}
111	return nil, errors.Join(errs...)
112}
113
114func dialOne(addr string) (*Conn, error) {
115	transport, params, ok := strings.Cut(addr, ":")
116	if !ok || transport != "unix" {
117		return nil, fmt.Errorf("zerobus: unsupported address %q", addr)
118	}
119	var sock string
120	for kv := range strings.SplitSeq(params, ",") {
121		k, v, _ := strings.Cut(kv, "=")
122		v, err := unescape(v)
123		if err != nil {
124			return nil, fmt.Errorf("zerobus: address %q: %w", addr, err)
125		}
126		switch k {
127		case "path":
128			sock = v
129		case "abstract":
130			sock = "@" + v
131		}
132	}
133	if sock == "" {
134		return nil, fmt.Errorf("zerobus: address %q has no path or abstract socket", addr)
135	}
136	return dialSocket(sock)
137}
138
139// dialSocket connects to the Unix socket sock ("@name" for an abstract
140// one), authenticates, and registers with the bus.
141func dialSocket(sock string) (*Conn, error) {
142	uc, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: sock, Net: "unix"})
143	if err != nil {
144		return nil, fmt.Errorf("zerobus: %w", err)
145	}
146	c := &Conn{c: uc, rbuf: make([]byte, defaultBufSz)}
147	c.enc.buf = make([]byte, 0, defaultBufSz)
148	if err := c.auth(); err != nil {
149		return nil, errors.Join(err, uc.Close())
150	}
151	if err := c.hello(); err != nil {
152		return nil, errors.Join(err, uc.Close())
153	}
154	return c, nil
155}
156
157// unescape decodes the %xx escapes of a D-Bus address value.
158func unescape(s string) (string, error) {
159	if !strings.Contains(s, "%") {
160		return s, nil
161	}
162	var b strings.Builder
163	for i := 0; i < len(s); i++ {
164		if s[i] != '%' {
165			b.WriteByte(s[i])
166			continue
167		}
168		if i+2 >= len(s) {
169			return "", errors.New("bad escape")
170		}
171		v, err := strconv.ParseUint(s[i+1:i+3], 16, 8)
172		if err != nil {
173			return "", errors.New("bad escape")
174		}
175		b.WriteByte(byte(v))
176		i += 2
177	}
178	return b.String(), nil
179}
180
181// auth runs the SASL EXTERNAL handshake. The bus checks the peer
182// credentials of the socket against the user id sent here.
183func (c *Conn) auth() error {
184	uid := strconv.Itoa(os.Getuid())
185	line := fmt.Appendf(nil, "\x00AUTH EXTERNAL %x\r\n", uid)
186	if _, err := c.c.Write(line); err != nil {
187		return fmt.Errorf("zerobus: auth: %w", err)
188	}
189	reply, err := c.authLine()
190	if err != nil {
191		return err
192	}
193	if !bytes.HasPrefix(reply, []byte("OK ")) {
194		return fmt.Errorf("zerobus: auth rejected: %q", reply)
195	}
196	if _, err := c.c.Write([]byte("BEGIN\r\n")); err != nil {
197		return fmt.Errorf("zerobus: auth: %w", err)
198	}
199	return nil
200}
201
202// authLine reads one line of the handshake. Bytes after it stay in rbuf for
203// the message reader.
204func (c *Conn) authLine() ([]byte, error) {
205	for {
206		if i := bytes.Index(c.rbuf[c.start:c.end], []byte("\r\n")); i >= 0 {
207			line := c.rbuf[c.start : c.start+i]
208			c.start += i + 2
209			return line, nil
210		}
211		if c.end-c.start >= maxAuthLine {
212			return nil, errors.New("zerobus: auth: line too long")
213		}
214		n, err := c.c.Read(c.rbuf[c.end:])
215		if err != nil {
216			return nil, fmt.Errorf("zerobus: auth: %w", err)
217		}
218		c.end += n
219	}
220}
221
222func (c *Conn) hello() error {
223	c.NewCall(busName, busPath, busName, "Hello", "")
224	m, err := c.Call()
225	if err != nil {
226		return fmt.Errorf("zerobus: hello: %w", err)
227	}
228	body := m.Body()
229	name := body.Str()
230	if body.Err() != nil {
231		return fmt.Errorf("zerobus: hello: %w", body.Err())
232	}
233	c.name = strings.Clone(name)
234	return nil
235}
236
237// UniqueName returns the name the bus gave this connection, such as ":1.42".
238func (c *Conn) UniqueName() string { return c.name }
239
240// Close closes the connection. A blocked ReadMessage returns ErrClosed.
241func (c *Conn) Close() error { return c.c.Close() }
242
243func (c *Conn) nextSerial() uint32 {
244	c.serial++
245	if c.serial == 0 {
246		c.serial = 1
247	}
248	return c.serial
249}
250
251// NewCall starts a method call and returns the encoder for its body, which
252// must match sig. Send it with Send or Call. Empty dest and iface are left
253// out of the header.
254//
255// The connection has one encoder: send a message before starting another.
256func (c *Conn) NewCall(dest, path, iface, member, sig string) *Encoder {
257	c.enc.begin(TypeMethodCall, c.nextSerial(), 0, dest, path, iface, member, "", sig)
258	return &c.enc
259}
260
261// NewSignal starts a signal and returns the encoder for its body.
262func (c *Conn) NewSignal(path, iface, member, sig string) *Encoder {
263	c.enc.begin(TypeSignal, c.nextSerial(), 0, "", path, iface, member, "", sig)
264	return &c.enc
265}
266
267// NewReply starts the reply to the method call m and returns the encoder for
268// its body.
269func (c *Conn) NewReply(m *Message, sig string) *Encoder {
270	c.enc.begin(TypeMethodReturn, c.nextSerial(), m.Serial, m.Sender, "", "", "", "", sig)
271	return &c.enc
272}
273
274// NewError starts an error reply to the method call m. Write a message
275// string as the body when sig is "s".
276func (c *Conn) NewError(m *Message, name, sig string) *Encoder {
277	c.enc.begin(TypeError, c.nextSerial(), m.Serial, m.Sender, "", "", "", name, sig)
278	return &c.enc
279}
280
281// Send sends the message built since the last NewCall, NewSignal, NewReply
282// or NewError and returns its serial.
283func (c *Conn) Send() (uint32, error) {
284	if c.err != nil {
285		return 0, c.err
286	}
287	b, err := c.enc.finish()
288	c.enc.body = 0
289	if err != nil {
290		return 0, err
291	}
292	if _, err := c.c.Write(b); err != nil {
293		c.err = closedErr(err)
294		return 0, c.err
295	}
296	return binary.LittleEndian.Uint32(b[8:]), nil
297}
298
299// Call sends the method call being built and waits for its reply. Messages
300// that arrive meanwhile go to Handle. An error reply returns an *Error.
301// A call flagged FlagNoReplyExpected gets no reply: send it with Send.
302//
303// The reply is valid until the next read, like ReadMessage's.
304func (c *Conn) Call() (*Message, error) {
305	if c.handling {
306		return nil, ErrNested
307	}
308	if c.enc.body != 0 && (Type(c.enc.buf[1]) != TypeMethodCall || Flags(c.enc.buf[2])&FlagNoReplyExpected != 0) {
309		c.enc.body = 0
310		return nil, ErrInvalid
311	}
312	serial, err := c.Send()
313	if err != nil {
314		return nil, err
315	}
316	for {
317		m, err := c.ReadMessage()
318		if err != nil {
319			return nil, err
320		}
321		if m.ReplySerial == serial && (m.Type == TypeMethodReturn || m.Type == TypeError) {
322			if m.Type == TypeError {
323				return m, replyError(m)
324			}
325			return m, nil
326		}
327		if c.Handle != nil {
328			c.handle(m)
329		}
330	}
331}
332
333func (c *Conn) handle(m *Message) {
334	c.handling = true
335	defer func() { c.handling = false }()
336	c.Handle(m)
337}
338
339// replyError builds an *Error from an error reply. It copies the strings so
340// the error stays valid; this is the only allocation of an error path.
341func replyError(m *Message) *Error {
342	e := &Error{Name: strings.Clone(m.ErrorName)}
343	if len(m.Signature) > 0 && m.Signature[0] == 's' {
344		if s := m.Body().Str(); s != "" {
345			e.Message = strings.Clone(s)
346		}
347	}
348	return e
349}
350
351// ReadMessage blocks until the next message arrives. The message and its
352// strings are valid until the next call to ReadMessage or Call.
353func (c *Conn) ReadMessage() (*Message, error) {
354	if c.handling {
355		return nil, ErrNested
356	}
357	if c.err != nil {
358		return nil, c.err
359	}
360	if err := c.fill(headerLen); err != nil {
361		return nil, err
362	}
363	n, err := frameLen(c.rbuf[c.start:])
364	if err != nil {
365		c.err = err
366		return nil, err
367	}
368	if err := c.fill(n); err != nil {
369		return nil, err
370	}
371	b := c.rbuf[c.start : c.start+n : c.start+n]
372	c.start += n
373	if err := parse(b, &c.msg); err != nil {
374		c.err = err
375		return nil, err
376	}
377	return &c.msg, nil
378}
379
380// fill reads until rbuf[start:] holds at least n bytes. It moves unread
381// bytes to the front, which is safe because the previous message is no
382// longer valid once a new read starts.
383func (c *Conn) fill(n int) error {
384	if c.end-c.start >= n {
385		return nil
386	}
387	if c.start > 0 {
388		c.end = copy(c.rbuf, c.rbuf[c.start:c.end])
389		c.start = 0
390	}
391	if n > len(c.rbuf) {
392		grown := make([]byte, max(n, 2*len(c.rbuf)))
393		copy(grown, c.rbuf[:c.end])
394		c.rbuf = grown
395	}
396	for c.end < n {
397		k, err := c.c.Read(c.rbuf[c.end:])
398		c.end += k
399		if err != nil && c.end < n {
400			c.err = closedErr(err)
401			return c.err
402		}
403	}
404	return nil
405}
406
407func closedErr(err error) error {
408	if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
409		return ErrClosed
410	}
411	return fmt.Errorf("zerobus: %w", err)
412}
413
414// AddMatch asks the bus to send this connection the messages that match
415// rule, such as "type='signal',interface='org.kde.StatusNotifierItem'".
416func (c *Conn) AddMatch(rule string) error {
417	c.NewCall(busName, busPath, busName, "AddMatch", "s").Str(rule)
418	_, err := c.Call()
419	return err
420}
421
422// RemoveMatch removes a rule added by AddMatch.
423func (c *Conn) RemoveMatch(rule string) error {
424	c.NewCall(busName, busPath, busName, "RemoveMatch", "s").Str(rule)
425	_, err := c.Call()
426	return err
427}
428
429// Reply codes of RequestName.
430const (
431	NamePrimaryOwner = 1
432	NameInQueue      = 2
433	NameExists       = 3
434	NameAlreadyOwner = 4
435)
436
437// Flags of RequestName.
438const (
439	NameFlagAllowReplacement = 0x1
440	NameFlagReplaceExisting  = 0x2
441	NameFlagDoNotQueue       = 0x4
442)
443
444// RequestName asks the bus for a well-known name and returns its reply code,
445// such as NamePrimaryOwner.
446func (c *Conn) RequestName(name string, flags uint32) (uint32, error) {
447	e := c.NewCall(busName, busPath, busName, "RequestName", "su")
448	e.Str(name)
449	e.Uint32(flags)
450	m, err := c.Call()
451	if err != nil {
452		return 0, err
453	}
454	body := m.Body()
455	code := body.Uint32()
456	return code, body.Err()
457}

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 1

Tags 3

↑↓ moveenter openesc close