main 5d6fa26

bus.go RawHistoryFolder

1package screencast
2
3import (
4	"errors"
5	"strconv"
6	"strings"
7
8	"github.com/bnema/zerobus"
9)
10
11// Object paths, interfaces and errors of the served contract.
12const (
13	pathScreenCast    = "/org/gnome/Mutter/ScreenCast"
14	prefixSession     = pathScreenCast + "/Session/u"
15	prefixStream      = pathScreenCast + "/Stream/u"
16	pathDisplayConfig = "/org/gnome/Mutter/DisplayConfig"
17	pathIntrospect    = "/org/gnome/Shell/Introspect"
18	pathScreenshot    = "/org/gnome/Shell/Screenshot"
19
20	ifaceScreenCast     = "org.gnome.Mutter.ScreenCast"
21	ifaceSession        = ifaceScreenCast + ".Session"
22	ifaceStream         = ifaceScreenCast + ".Stream"
23	ifaceDisplayConfig  = "org.gnome.Mutter.DisplayConfig"
24	ifaceIntrospect     = "org.gnome.Shell.Introspect"
25	ifaceScreenshot     = "org.gnome.Shell.Screenshot"
26	ifaceProps          = "org.freedesktop.DBus.Properties"
27	ifacePeer           = "org.freedesktop.DBus.Peer"
28	ifaceIntrospectable = "org.freedesktop.DBus.Introspectable"
29
30	errAccessDenied     = "org.freedesktop.DBus.Error.AccessDenied"
31	errLimits           = "org.freedesktop.DBus.Error.LimitsExceeded"
32	errNotSupported     = "org.freedesktop.DBus.Error.NotSupported"
33	errInvalidArgs      = "org.freedesktop.DBus.Error.InvalidArgs"
34	errUnknownObject    = "org.freedesktop.DBus.Error.UnknownObject"
35	errUnknownProperty  = "org.freedesktop.DBus.Error.UnknownProperty"
36	errUnknownInterface = "org.freedesktop.DBus.Error.UnknownInterface"
37	errPropertyReadOnly = "org.freedesktop.DBus.Error.PropertyReadOnly"
38	errFailed           = "org.freedesktop.DBus.Error.Failed"
39)
40
41// objKind is the object a call is addressed to.
42type objKind uint8
43
44const (
45	objNone objKind = iota
46	objScreenCast
47	objSession
48	objStream
49	objDisplayConfig
50	objIntrospect
51	objScreenshot
52)
53
54// method is a call the service understands, after its (path, interface,
55// member) were resolved.
56type method uint8
57
58const (
59	mNotSupported method = iota
60	mCreateSession
61	mStart
62	mStop
63	mRecordMonitor
64	mRecordWindow
65	mGet
66	mGetAll
67	mSet
68	mPing
69	mIntrospect
70	mGetState
71	mGetWindows
72	mShot
73	mShotArea
74	mShotInteractive
75	mUnknownObject
76)
77
78// request is a decoded method call. Every value is copied out of the receive
79// buffer with a length cap, so the request can wait for the caller check and
80// the reply can be built after other messages were read.
81type request struct {
82	sender  string
83	serial  uint32
84	noReply bool
85	obj     objKind
86	id      uint32 // session or stream number from the path
87	method  method
88	bad     bool // the body does not match the method
89
90	text, text2 string // connector; Properties interface and name
91	window      uint64
92	cursor      uint32
93	remote      bool // CreateSession: remote-desktop-session-id given
94	x, y, w, h  int32
95	flag        bool
96}
97
98// privileged reports the calls that read the caller's executable again.
99func (r *request) privileged() bool {
100	switch r.method {
101	case mCreateSession, mStart, mShot, mShotArea, mShotInteractive:
102		return true
103	}
104	return false
105}
106
107// capStr copies s, cut to maxArg bytes.
108func capStr(s string) string {
109	if len(s) > maxArg {
110		s = s[:maxArg]
111	}
112	return strings.Clone(s)
113}
114
115// objectOf finds the object of a path. A served object needs its bus name.
116func (s *Service) objectOf(path string) (objKind, uint32) {
117	switch path {
118	case pathScreenCast:
119		return objScreenCast, 0
120	case pathDisplayConfig:
121		return objDisplayConfig, 0
122	case pathIntrospect:
123		return objIntrospect, 0
124	case pathScreenshot:
125		return objScreenshot, 0
126	}
127	if n, ok := strings.CutPrefix(path, prefixSession); ok {
128		id, err := strconv.ParseUint(n, 10, 32)
129		return objSession, uint32(id) | boolBit(err != nil)
130	}
131	if n, ok := strings.CutPrefix(path, prefixStream); ok {
132		id, err := strconv.ParseUint(n, 10, 32)
133		return objStream, uint32(id) | boolBit(err != nil)
134	}
135	return objNone, 0
136}
137
138// boolBit marks an unparsable number with an id no object has.
139func boolBit(bad bool) uint32 {
140	if bad {
141		return 0xffffffff
142	}
143	return 0
144}
145
146// served reports whether the bus name of the object is ours.
147func (s *Service) served(k objKind) bool {
148	switch k {
149	case objScreenCast, objSession, objStream:
150		return s.owns[0]
151	case objDisplayConfig:
152		return s.owns[1]
153	case objIntrospect:
154		return s.owns[2]
155	case objScreenshot:
156		return s.owns[3]
157	}
158	return false
159}
160
161// decode reads a call completely: nothing of m is used after it returns.
162func (s *Service) decode(m *zerobus.Message) request {
163	req := request{sender: strings.Clone(m.Sender), serial: m.Serial, noReply: m.Flags&zerobus.FlagNoReplyExpected != 0}
164	req.obj, req.id = s.objectOf(m.Path)
165	if !s.served(req.obj) {
166		req.method = mUnknownObject
167		return req
168	}
169	r := m.Body()
170	sig := m.Signature
171	want := func(w string) bool {
172		if sig != w {
173			req.bad = true
174			return false
175		}
176		return true
177	}
178	switch m.Interface {
179	case ifaceProps:
180		switch m.Member {
181		case "Get":
182			req.method = mGet
183			if want("ss") {
184				req.text, req.text2 = capStr(r.Str()), capStr(r.Str())
185			}
186		case "GetAll":
187			req.method = mGetAll
188			if want("s") {
189				req.text = capStr(r.Str())
190			}
191		case "Set":
192			req.method = mSet
193		}
194	case ifacePeer:
195		if m.Member == "Ping" {
196			req.method = mPing
197			want("")
198		}
199	case ifaceIntrospectable:
200		if m.Member == "Introspect" {
201			req.method = mIntrospect
202			want("")
203		}
204	case ifaceScreenCast:
205		if req.obj == objScreenCast && m.Member == "CreateSession" {
206			req.method = mCreateSession
207			if want("a{sv}") {
208				s.readProps(r, &req)
209			}
210		}
211	case ifaceSession:
212		if req.obj == objSession {
213			switch m.Member {
214			case "Start":
215				req.method = mStart
216				want("")
217			case "Stop":
218				req.method = mStop
219				want("")
220			case "RecordMonitor":
221				req.method = mRecordMonitor
222				if want("sa{sv}") {
223					req.text = capStr(r.Str())
224					s.readProps(r, &req)
225				}
226			case "RecordWindow":
227				req.method = mRecordWindow
228				if want("a{sv}") {
229					s.readProps(r, &req)
230				}
231			}
232		}
233	case ifaceDisplayConfig:
234		if req.obj == objDisplayConfig && m.Member == "GetCurrentState" {
235			req.method = mGetState
236			want("")
237		}
238	case ifaceIntrospect:
239		if req.obj == objIntrospect && m.Member == "GetWindows" {
240			req.method = mGetWindows
241			want("")
242		}
243	case ifaceScreenshot:
244		if req.obj == objScreenshot {
245			switch m.Member {
246			case "Screenshot":
247				req.method = mShot
248				if want("bbs") {
249					_ = r.Bool()
250					req.flag = r.Bool()
251					_ = r.Str()
252				}
253			case "ScreenshotArea":
254				req.method = mShotArea
255				if want("iiiibs") {
256					req.x, req.y, req.w, req.h = r.Int32(), r.Int32(), r.Int32(), r.Int32()
257					_ = r.Bool()
258					_ = r.Str()
259				}
260			case "InteractiveScreenshot":
261				req.method = mShotInteractive
262				want("")
263			}
264		}
265	}
266	if r.Err() != nil {
267		req.bad = true
268	}
269	return req
270}
271
272// readProps reads an a{sv} and keeps the keys the service knows.
273func (s *Service) readProps(r *zerobus.Reader, req *request) {
274	end := r.Array('{')
275	for r.More(end) {
276		r.Struct()
277		key := r.Str()
278		sig := r.Variant()
279		switch {
280		case key == "cursor-mode" && sig == "u":
281			req.cursor = r.Uint32()
282		case key == "window-id" && sig == "t":
283			req.window = r.Uint64()
284		case key == "remote-desktop-session-id":
285			req.remote = true
286			r.Skip(sig)
287		default:
288			r.Skip(sig)
289		}
290	}
291}
292
293// onMessage handles one message from the bus.
294func (s *Service) onMessage(m *zerobus.Message) {
295	switch m.Type {
296	case zerobus.TypeMethodCall:
297		if m.Sender != "" {
298			req := s.decode(m)
299			s.admit(&req)
300		}
301	case zerobus.TypeMethodReturn, zerobus.TypeError:
302		if m.Sender == busDaemon {
303			s.onBusReply(m)
304		}
305	case zerobus.TypeSignal:
306		if m.Sender == busDaemon && m.Interface == busDaemon && m.Member == "NameOwnerChanged" {
307			r := m.Body()
308			name := r.Str()
309			_ = r.Str()
310			owner := r.Str()
311			if r.Err() == nil && owner == "" && strings.HasPrefix(name, ":") {
312				s.onGone(name)
313			}
314		}
315	}
316}
317
318// send sends the message that was built. A broken connection ends the
319// attempt; any other failure drops that one message.
320func (s *Service) send() {
321	if _, err := s.conn.Send(); err != nil {
322		if errors.Is(err, zerobus.ErrClosed) {
323			s.fatal = err
324			return
325		}
326		s.log.Warn().Err(err).Msg("message not sent")
327	}
328}
329
330// replyTo builds the reply of req, or nil when the caller wants none.
331func (s *Service) replyTo(req *request, sig string) *zerobus.Encoder {
332	if req.noReply {
333		return nil
334	}
335	return s.conn.NewReply(&zerobus.Message{Sender: req.sender, Serial: req.serial}, sig)
336}
337
338// reply sends an empty reply.
339func (s *Service) reply(req *request) {
340	if s.replyTo(req, "") != nil {
341		s.send()
342	}
343}
344
345// replyErr sends an error reply.
346func (s *Service) replyErr(req *request, name, text string) {
347	if req.noReply {
348		return
349	}
350	s.conn.NewError(&zerobus.Message{Sender: req.sender, Serial: req.serial}, name, "s").Str(text)
351	s.send()
352}
353
354// emit sends a signal to one peer.
355func (s *Service) emitTo(dest, path, iface, member string) *zerobus.Encoder {
356	return s.conn.NewSignalTo(dest, path, iface, member, "")
357}
358
359// dispatch runs a call of a checked caller.
360func (s *Service) dispatch(c *caller, req *request) {
361	if !s.allowed(c.exe) {
362		s.deny(req)
363		return
364	}
365	if req.privileged() && !s.recheck(c) {
366		s.deny(req)
367		return
368	}
369	switch {
370	case req.method == mUnknownObject:
371		s.replyErr(req, errUnknownObject, "no such object")
372		return
373	case req.bad:
374		s.replyErr(req, errInvalidArgs, "unexpected arguments")
375		return
376	}
377	var sess *session
378	var st *stream
379	switch req.obj {
380	case objSession:
381		if sess = s.sessionByID(req.id); sess == nil {
382			s.replyErr(req, errUnknownObject, "no such session")
383			return
384		}
385	case objStream:
386		if st = s.streamByID(req.id); st == nil {
387			s.replyErr(req, errUnknownObject, "no such stream")
388			return
389		}
390		sess = st.sess
391	}
392	if sess != nil && sess.owner != req.sender {
393		s.deny(req)
394		return
395	}
396	switch req.method {
397	case mPing:
398		s.reply(req)
399	case mIntrospect:
400		if e := s.replyTo(req, "s"); e != nil {
401			e.Str("<node/>")
402			s.send()
403		}
404	case mGet, mGetAll, mSet:
405		s.properties(req, sess, st)
406	case mCreateSession:
407		s.createSession(req)
408	case mStart:
409		s.start(req, sess)
410	case mStop:
411		s.reply(req)
412		s.closeSession(sess, true)
413	case mRecordMonitor, mRecordWindow:
414		s.record(req, sess)
415	case mGetState:
416		s.replyState(req)
417	case mGetWindows:
418		s.replyWindows(req)
419	case mShot, mShotArea, mShotInteractive:
420		s.screenshot(req)
421	default:
422		s.replyErr(req, errNotSupported, "not supported")
423	}
424}
425
426// deny refuses a call.
427func (s *Service) deny(req *request) {
428	s.replyErr(req, errAccessDenied, "caller not allowed to capture the screen")
429}

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 23

Tags 10

↑↓ moveenter openesc close