main 07ab4d2

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

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