exclusion.go RawHistoryFolder

1package wayland
2
3import (
4	"crypto/rand"
5	"crypto/subtle"
6	"encoding/hex"
7	"os"
8	"slices"
9
10	ext "github.com/bnema/go-wayland-bindings/server/extimagecopycapture"
11	"github.com/bnema/go-wayland-bindings/server/wayland"
12	"github.com/bnema/neferwl/internal/adapters/wayland/imagecapture"
13	"github.com/bnema/neferwl/internal/ports"
14	"github.com/bnema/purego-libwayland/server"
15)
16
17// Capture exclusion (neferwl_capture_exclusion_manager_v1). A recorder asks
18// for the exclusion of its ext-image-copy-capture session and gets a token;
19// its HUD, usually on another connection of the same user, attaches layer
20// surfaces with it. Core owns the decisions (ports/capture.go): this file
21// relays requests and tells clients what core decided. Only one exclusion is
22// live in the compositor, and it ends with its session.
23//
24// Privacy: the token never reaches a log, and neither does any peer
25// identity; only lifecycle events are logged.
26
27// exclusion is the state behind one neferwl_capture_exclusion_v1.
28type exclusion struct {
29	s       *Server
30	res     *imagecapture.NeferwlCaptureExclusionV1
31	session *captureSession
32	token   string
33	// layers are the attached layer surfaces; attached their attachment
34	// objects, pending until a core state lists the layer.
35	layers   []*layerSurface
36	attached []*layerAttachment
37	ended    bool
38}
39
40// layerAttachment is one neferwl_capture_layer_v1: it reports the result of
41// an attach_surface and the layer's end.
42type layerAttachment struct {
43	res       *imagecapture.NeferwlCaptureLayerV1
44	layer     *layerSurface
45	confirmed bool // attached sent
46}
47
48func (a *layerAttachment) fail(reason imagecapture.NeferwlCaptureLayerV1Failure) {
49	if a.res.Alive() {
50		a.res.SendFailed(uint32(reason))
51	}
52}
53
54// finish ends an attachment: detached after attached, failed before it.
55func (a *layerAttachment) finish(failure imagecapture.NeferwlCaptureLayerV1Failure, detach imagecapture.NeferwlCaptureLayerV1DetachReason) {
56	if !a.res.Alive() {
57		return
58	}
59	if a.confirmed {
60		a.res.SendDetached(uint32(detach))
61	} else {
62		a.res.SendFailed(uint32(failure))
63	}
64}
65
66// trustedPeer reports whether a peer with these credentials may attach
67// surfaces: its credentials are readable and it runs as the compositor's own
68// user.
69func trustedPeer(own uint32, peer server.Credentials, err error) bool {
70	return err == nil && peer.UID == own
71}
72
73func (s *Server) trusted(c server.Client) bool {
74	cr, err := c.Credentials()
75	return trustedPeer(s.ownUID, cr, err)
76}
77
78func registerExclusion(d *server.Display, s *Server) error {
79	s.ownUID = uint32(os.Geteuid())
80	return imagecapture.NewNeferwlCaptureExclusionManagerV1Global(d, 1, func(c server.Client, v, id uint32) {
81		m := &exclusionManager{s: s, trusted: s.trusted(c)}
82		_, _ = imagecapture.NewNeferwlCaptureExclusionManagerV1(c, int32(v), id, m)
83	})
84}
85
86type exclusionManager struct {
87	s       *Server
88	trusted bool
89}
90
91func (*exclusionManager) Destroy(*imagecapture.NeferwlCaptureExclusionManagerV1) {}
92
93// sessionOf finds the registered session behind an ext session object.
94func (s *Server) sessionOf(res *ext.ExtImageCopyCaptureSessionV1) *captureSession {
95	if res == nil {
96		return nil
97	}
98	for c := range s.captureSessions {
99		if c.res != nil && c.res.Resource == res.Resource {
100			return c
101		}
102	}
103	return nil
104}
105
106func (m *exclusionManager) GetExclusion(r *imagecapture.NeferwlCaptureExclusionManagerV1, id uint32, session *ext.ExtImageCopyCaptureSessionV1) {
107	s := m.s
108	c := s.sessionOf(session)
109	ex := &exclusion{s: s, session: c}
110	res, err := imagecapture.NewNeferwlCaptureExclusionV1(r.Client(), r.Version(), id, ex)
111	if err != nil {
112		return
113	}
114	ex.res = res
115	if c != nil && c.excl != nil {
116		r.PostError(uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorAlreadyExcluded), "the capture session already has an exclusion")
117		return
118	}
119	switch {
120	case c == nil || c.stopped || c.id == 0:
121		ex.session = nil
122		ex.ended = true
123		res.SendFailed(uint32(imagecapture.NeferwlCaptureExclusionV1FailureSessionStopped))
124		return
125	case s.excl != nil:
126		ex.session = nil
127		ex.ended = true
128		s.log.Info().Str("reason", "busy").Msg("capture exclusion refused")
129		res.SendFailed(uint32(imagecapture.NeferwlCaptureExclusionV1FailureBusy))
130		return
131	}
132	var raw [32]byte
133	if _, err := rand.Read(raw[:]); err != nil {
134		s.log.Warn().Msg("capture exclusion refused: no randomness")
135		ex.session, ex.ended = nil, true
136		res.SendFailed(uint32(imagecapture.NeferwlCaptureExclusionV1FailureSessionStopped))
137		return
138	}
139	ex.token = hex.EncodeToString(raw[:])
140	s.excl, c.excl = ex, ex
141	s.log.Info().Uint64("session", c.id).Msg("capture exclusion begin")
142	s.emit(ports.CaptureExclusionBegin{Session: c.id})
143	res.SendToken(ex.token)
144}
145
146// Destroy: the owner let go of the exclusion; the session lives on.
147func (ex *exclusion) Destroy(*imagecapture.NeferwlCaptureExclusionV1) {
148	if !ex.ended {
149		ex.end(true)
150	}
151}
152
153func (m *exclusionManager) AttachSurface(r *imagecapture.NeferwlCaptureExclusionManagerV1, id uint32, token string, surface *wayland.Surface) {
154	s := m.s
155	if surface == nil || s.surfaces[surface.Resource] == nil || surface.Resource.Client() != r.Client() {
156		r.PostError(uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorInvalidSurface), "surface is not yours")
157		return
158	}
159	state := s.surfaces[surface.Resource]
160	l := state.layer
161	att := &layerAttachment{layer: l}
162	res, err := imagecapture.NewNeferwlCaptureLayerV1(r.Client(), r.Version(), id, att)
163	if err != nil {
164		return
165	}
166	att.res = res
167	// Same user as the compositor, and the owner of the exclusion passed the
168	// same check by holding the token.
169	ex := s.excl
170	switch {
171	case s.protected() || !m.trusted || !s.mayCapture(r.Client()): // DISPLAY protection; the UID of this connection, checked once at bind; the capture allowlist
172		att.fail(imagecapture.NeferwlCaptureLayerV1FailureUnauthorized)
173		return
174	case ex == nil || subtle.ConstantTimeCompare([]byte(token), []byte(ex.token)) != 1:
175		att.fail(imagecapture.NeferwlCaptureLayerV1FailureUnknownToken)
176		return
177	case state.kind != roleLayer || l == nil:
178		r.PostError(uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorSurfaceRole), "not a layer surface")
179		return
180	case l.pending.layer != ports.LayerTop && l.pending.layer != ports.LayerOverlay:
181		r.PostError(uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorInvalidLayer), "layer must be top or overlay")
182		return
183	case l.pending.keyboard == 1:
184		r.PostError(uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorExclusiveKeyboard), "exclusive keyboard")
185		return
186	case l.mapped || l.closed:
187		r.PostError(uint32(imagecapture.NeferwlCaptureExclusionManagerV1ErrorInvalidSurface), "already mapped")
188		return
189	case l.capture != nil:
190		att.fail(imagecapture.NeferwlCaptureLayerV1FailureAlreadyAttached)
191		return
192	case len(ex.layers) >= ports.MaxExclusionLayers:
193		att.fail(imagecapture.NeferwlCaptureLayerV1FailureTooManyLayers)
194		return
195	}
196	l.capture, l.attachment = ex, att
197	l.pending.keyboard, l.current.keyboard = 0, 0
198	ex.layers = append(ex.layers, l)
199	ex.attached = append(ex.attached, att)
200	// Queued before any LayerChanged of this layer: core excludes it from
201	// the first scene that shows it.
202	s.emit(ports.CaptureExclusionLayer{Session: ex.session.id, Layer: l.id, Attached: true})
203}
204
205func (*layerAttachment) Destroy(*imagecapture.NeferwlCaptureLayerV1) {}
206
207// end ends the exclusion: every attachment is told (detached, or failed
208// before it was confirmed) and its layer surface is closed so none lingers
209// into the next exclusion. notify tells core; it is false when the session
210// goes (core forgets the exclusion with it, or decided the end itself).
211func (ex *exclusion) end(notify bool) {
212	if ex.ended {
213		return
214	}
215	s := ex.s
216	ex.ended = true
217	if s.excl == ex {
218		s.excl = nil
219	}
220	sess := ex.session
221	if sess != nil {
222		// exclSeen stays as core's last state says: core keeps the ended
223		// exclusion's layers excluded while a screen lists them, and frames
224		// keep asking for that exclusion (fenced by its revision) until a
225		// state says it is gone.
226		sess.excl = nil
227	}
228	layers, attached := ex.layers, ex.attached
229	ex.layers, ex.attached = nil, nil
230	for _, a := range attached {
231		a.finish(imagecapture.NeferwlCaptureLayerV1FailureExclusionEnded, imagecapture.NeferwlCaptureLayerV1DetachReasonExclusionEnded)
232	}
233	for _, l := range layers {
234		l.capture, l.attachment = nil, nil
235		// Closing unmaps the surface: the LayerChanged that removes it is
236		// queued before the CaptureExclusionEnd below.
237		l.close()
238	}
239	if notify && sess != nil {
240		s.log.Info().Uint64("session", sess.id).Msg("capture exclusion end")
241		s.emit(ports.CaptureExclusionEnd{Session: sess.id})
242	}
243}
244
245// confirm sends attached to the attachments core now lists: only then is it
246// safe for the client to show the layer.
247func (ex *exclusion) confirm(listed []ports.WindowID) {
248	for _, a := range ex.attached {
249		if !a.confirmed && slices.Contains(listed, a.layer.id) {
250			a.confirmed = true
251			if a.res.Alive() {
252				a.res.SendAttached()
253			}
254		}
255	}
256}
257
258// captureDetach drops a layer surface whose role is gone from its exclusion.
259// The caller unmapped it first, so the LayerChanged that removes the layer is
260// already queued: core keeps excluding the layer until it reads that.
261func (l *layerSurface) captureDetach() {
262	ex, att := l.capture, l.attachment
263	if ex == nil {
264		return
265	}
266	l.capture, l.attachment = nil, nil
267	ex.layers = removeItem(ex.layers, l)
268	ex.attached = removeItem(ex.attached, att)
269	att.finish(imagecapture.NeferwlCaptureLayerV1FailureLayerDestroyed, imagecapture.NeferwlCaptureLayerV1DetachReasonLayerDestroyed)
270	if !ex.ended && ex.session != nil {
271		ex.s.emit(ports.CaptureExclusionLayer{Session: ex.session.id, Layer: l.id})
272	}
273}

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