main f2d166b

session_lock.go RawHistoryFolder

1package wayland
2
3import (
4	"context"
5
6	"github.com/bnema/go-wayland-bindings/server/extsessionlock"
7	"github.com/bnema/go-wayland-bindings/server/wayland"
8	"github.com/bnema/neferwl/internal/ports"
9	"github.com/bnema/purego-libwayland/server"
10)
11
12// sessionLock is owned by the display goroutine. Resource destruction removes
13// its owner, never the protection gate. An ordinary new owner may take over.
14type sessionLock struct {
15	s        *Server
16	res      *extsessionlock.ExtSessionLockV1
17	state    ports.SecurityState
18	locked   bool
19	surfaces map[*output]*lockSurface
20}
21
22type sessionLockManager struct{ s *Server }
23
24// registerSessionLock is enabled only when the display and backend share
25// protection control and authoritative, non-dropping proof channels.
26func registerSessionLock(d *server.Display, s *Server) error {
27	return extsessionlock.NewExtSessionLockManagerV1Global(d, 1, func(c server.Client, version, id uint32) {
28		_, _ = extsessionlock.NewExtSessionLockManagerV1(c, int32(version), id, sessionLockManager{s})
29	})
30}
31func (sessionLockManager) Destroy(*extsessionlock.ExtSessionLockManagerV1) {}
32
33func (m sessionLockManager) Lock(r *extsessionlock.ExtSessionLockManagerV1, id uint32) {
34	s := m.s
35	l := &sessionLock{s: s, surfaces: make(map[*output]*lockSurface)}
36	res, err := extsessionlock.NewExtSessionLockV1(r.Client(), r.Version(), id, l)
37	if err != nil {
38		return
39	}
40	l.res = res
41	if s.securityController == nil || s.channels.SecurityChanges == nil || s.channels.SecurityEvents == nil || s.sessionLock != nil || len(s.securityPending) >= maxSecurityPending {
42		res.SendFinished()
43		return
44	}
45	// Engage before queueing any core event. No display-owned protocol
46	// admission can observe the old policy after this point.
47	state, err := s.securityController.Engage()
48	if err != nil {
49		res.SendFinished()
50		return
51	}
52	l.state = state
53	s.sessionLock = l
54	s.lockReadiness.Begin(state.Generation)
55	res.OnDestroy = l.orphan
56	s.resetProtectionInput()
57	s.endCaptureSessions()
58	for _, lease := range s.activeLeases {
59		lease.destroy()
60	}
61	// Preserve card tombstones until late replies are consumed/revoked, but
62	// revoke their protocol authority across both acquire and release.
63	for id, lease := range s.pendingLeases {
64		lease.finish()
65		delete(s.pendingLeases, id)
66	}
67	s.updateWorkspaceManagers(s.workspaceSnapshot)
68	s.emit(ports.SessionLockChanged{State: state})
69	s.requestBackendSecurity(state)
70	s.maybeLocked()
71}
72
73func (l *sessionLock) Destroy(r *extsessionlock.ExtSessionLockV1) {
74	if l.locked {
75		r.PostError(uint32(extsessionlock.ExtSessionLockV1ErrorInvalidDestroy), "destroy while locked")
76	}
77}
78func (l *sessionLock) UnlockAndDestroy(r *extsessionlock.ExtSessionLockV1) {
79	s := l.s
80	if !l.locked || s.sessionLock != l {
81		r.PostError(uint32(extsessionlock.ExtSessionLockV1ErrorInvalidUnlock), "unlock before protection confirmation")
82		return
83	}
84	state, err := s.securityController.Release(l.state.Generation)
85	if err != nil {
86		r.PostError(uint32(extsessionlock.ExtSessionLockV1ErrorInvalidUnlock), "protection release rejected")
87		return
88	}
89	// Clear input before any desktop focus enter; no password held keys.
90	s.resetProtectionInput()
91	s.lockReadiness.End(l.state.Generation)
92	s.sessionLock = nil
93	s.refreshToplevels()
94	s.castInventoryChanged() // the windows are listed again
95	s.updateWorkspaceManagers(s.workspaceSnapshot)
96	for _, surface := range l.surfaces {
97		surface.unmap()
98	}
99	s.emit(ports.SessionLockChanged{State: state})
100	s.requestBackendSecurity(state)
101}
102
103func (l *sessionLock) GetLockSurface(r *extsessionlock.ExtSessionLockV1, id uint32, wl *wayland.Surface, out *wayland.Output) {
104	s := l.s
105	if s.sessionLock != l {
106		_, _ = extsessionlock.NewExtSessionLockSurfaceV1(r.Client(), r.Version(), id, refusedLockSurface{})
107		return
108	}
109	surface, output := s.surfaceOf(wl), s.outputOf(out)
110	if surface == nil || output == nil || wl == nil || out == nil || wl.Client() != r.Client() || out.Client() != r.Client() {
111		r.PostError(uint32(extsessionlock.ExtSessionLockV1ErrorRole), "lock surface and output must belong to lock owner")
112		return
113	}
114	if _, exists := l.surfaces[output]; output != nil && exists {
115		r.PostError(uint32(extsessionlock.ExtSessionLockV1ErrorDuplicateOutput), "duplicate lock output")
116		return
117	}
118	ls, err := s.newLockSurface(r, id, surface, output, func(*lockSurface) { s.lockSurfaceChanged() })
119	if err != nil || ls == nil {
120		return
121	}
122	l.surfaces[output] = ls
123	s.lockSurfaceChanged()
124}
125
126func (l *sessionLock) orphan() {
127	s := l.s
128	if s.sessionLock != l {
129		return
130	}
131	// Never release or send finished merely because a client died.
132	s.resetProtectionInput()
133	s.sessionLock = nil
134	for _, surface := range l.surfaces {
135		surface.unmap()
136	}
137	s.emit(ports.SessionLockChanged{State: l.state})
138}
139
140func (s *Server) resetProtectionInput() {
141	s.cursorOverride = ""
142	s.dndCancelAll()
143	s.changeFocus(0)
144	s.changePointerFocus(0, 0, 0)
145	s.deactivateText()
146	s.deactivateConstraint()
147	clear(s.seat.heldKeys)
148	clear(s.seat.grabKeys)
149	s.seat.modState = ports.ModState{}
150	s.seat.press, s.seat.pressClient = 0, server.Client{}
151	s.seat.grabSerial, s.seat.grabButton = 0, 0
152	s.seat.wheelRest, s.seat.wheelHeld = [2]int32{}, [2]float64{}
153	s.cursorSurface = nil
154}
155
156// maxSecurityPending bounds takeover churn while the backend is stalled.
157// Acquire checks this bound before engaging; release retains a reserved slot.
158const maxSecurityPending = 16
159
160func (s *Server) requestBackendSecurity(state ports.SecurityState) {
161	s.securityPending = append(s.securityPending, state)
162	select {
163	case s.securityWake <- struct{}{}:
164	default:
165	}
166}
167
168// forwardBackendSecurity transfers immutable states without blocking display
169// dispatch. Queue mutation is always on the display owner, not this goroutine.
170func (s *Server) forwardBackendSecurity(ctx context.Context) {
171	for {
172		var state ports.SecurityState
173		var pending bool
174		if !s.display.Do(func() {
175			if len(s.securityPending) != 0 {
176				state, pending = s.securityPending[0], true
177			}
178		}) {
179			return
180		}
181		if !pending {
182			select {
183			case <-ctx.Done():
184				return
185			case <-s.display.Stopped():
186				return
187			case <-s.securityWake:
188				continue
189			}
190		}
191		select {
192		case <-ctx.Done():
193			return
194		case <-s.display.Stopped():
195			return
196		case s.channels.SecurityChanges <- state:
197		}
198		if !s.display.Do(func() {
199			// Only this sender pops; appending on display preserves the head.
200			if len(s.securityPending) != 0 && s.securityPending[0] == state {
201				s.securityPending[0] = ports.SecurityState{}
202				s.securityPending = s.securityPending[1:]
203			}
204		}) {
205			return
206		}
207	}
208}
209
210func (s *Server) forwardSecurityEvents(ctx context.Context) {
211	for {
212		select {
213		case <-ctx.Done():
214			return
215		case <-s.display.Stopped():
216			return
217		case event, ok := <-s.channels.SecurityEvents:
218			if !ok {
219				return
220			}
221			if !s.display.Do(func() { s.applySecurityEvent(event) }) {
222				return
223			}
224		}
225	}
226}
227
228func (s *Server) applySecurityEvent(event ports.SecurityBackendEvent) {
229	switch v := event.(type) {
230	case ports.SecurityOutputAdded:
231		s.lockReadiness.Add(v.Instance)
232	case ports.SecurityOutputRemoved:
233		s.lockReadiness.Remove(v.Instance)
234	case ports.SecurityOutputProof:
235		s.lockReadiness.Record(v.Proof)
236	case ports.SecurityOutputInvalidated:
237		s.lockReadiness.Invalidate(v.Generation, v.Instance)
238	case ports.SecurityBackendBarrier:
239		if v.Err == nil {
240			s.lockReadiness.BackendBarrier(v.Generation)
241		} else {
242			s.log.Error().Err(v.Err).Msg("session security backend barrier failed")
243		}
244	}
245	s.maybeLocked()
246}
247
248func (s *Server) lockInputTarget(id ports.WindowID) bool {
249	l := s.sessionLock
250	if l == nil || !l.locked {
251		return false
252	}
253	surface := s.lockSurfaces[id]
254	if surface == nil || !surface.mapped || surface.closed || !surface.resource.Alive() {
255		return false
256	}
257	for _, owned := range l.surfaces {
258		if surface == owned {
259			return true
260		}
261	}
262	return false
263}
264
265func (s *Server) maybeLocked() {
266	l := s.sessionLock
267	if l == nil || l.locked || !l.res.Alive() {
268		return
269	}
270	if len(s.captureInflight) == 0 {
271		s.lockReadiness.CaptureBarrier(l.state.Generation)
272	}
273	if !s.lockReadiness.Ready() {
274		return
275	}
276	l.locked = true
277	l.res.SendLocked()
278	s.lockSurfaceChanged()
279}

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 26

Tags 10

↑↓ moveenter openesc close