session.go RawHistoryFolder

1package app
2
3import (
4	"bufio"
5	"context"
6	"errors"
7	"os"
8	"runtime"
9	"sync"
10	"time"
11
12	"github.com/bnema/neferwl/internal/adapters/captureallow"
13	"github.com/bnema/neferwl/internal/adapters/clock"
14	"github.com/bnema/neferwl/internal/adapters/config"
15	"github.com/bnema/neferwl/internal/adapters/drm"
16	"github.com/bnema/neferwl/internal/adapters/headlessinput"
17	"github.com/bnema/neferwl/internal/adapters/launcher"
18	"github.com/bnema/neferwl/internal/adapters/libinput"
19	"github.com/bnema/neferwl/internal/adapters/logging"
20	"github.com/bnema/neferwl/internal/adapters/sched"
21	"github.com/bnema/neferwl/internal/adapters/screensaver"
22	"github.com/bnema/neferwl/internal/adapters/sessionsecurity"
23	"github.com/bnema/neferwl/internal/adapters/statefile"
24	"github.com/bnema/neferwl/internal/adapters/vulkan"
25	"github.com/bnema/neferwl/internal/adapters/wayland"
26	"github.com/bnema/neferwl/internal/adapters/workspaceid"
27	"github.com/bnema/neferwl/internal/adapters/xkb"
28	"github.com/bnema/neferwl/internal/adapters/xwayland"
29	"github.com/bnema/neferwl/internal/core"
30	"github.com/bnema/neferwl/internal/ports"
31	"github.com/bnema/zerowrap"
32)
33
34// session holds the dependencies assembleSession acquired and wired; run
35// starts them. The session owns hw, curs and xdisplay (released by close)
36// until run ends; the keymap and the wayland server own themselves.
37type session struct {
38	opts Options
39	log  zerowrap.Logger
40
41	clock    ports.Clock
42	security *sessionsecurity.Gate
43	p        *pipes
44	sizes    [][2]int
45	path     string
46
47	hw           *drmBackend
48	km           *xkb.Keymap
49	core         *core.Core
50	dmabuf       ports.DMABufSupport
51	wsIDs        *workspaceid.IDs
52	captureAllow *captureallow.Store
53	server       *wayland.Server
54	childEnv     []string
55	statePath    string
56	xdisplay     *xwayland.Display
57	child        *launcher.Launcher
58	curs         *cursors
59
60	newRenderer func(w, h int) (ports.Renderer, error)
61}
62
63// assembleSession acquires and wires every dependency in startup order and
64// starts no long-lived worker.
65//
66// Invariant: wayland.New is the last fallible step. *wayland.Server has no
67// Close: its sockets are released only by Server.Run. Every later step
68// (statefile.Path, openXwayland, launcher.New, newCursors) degrades with a
69// warning or nil instead of failing. A failure therefore releases exactly
70// the DRM backend and the keymap (abort); nothing else is owned yet.
71func assembleSession(ctx context.Context, opts Options) (s *session, err error) {
72	s = &session{opts: opts, log: logging.For(ctx, "app"), clock: clock.System{}, security: &sessionsecurity.Gate{}}
73	defer func() {
74		if err != nil {
75			s.abort()
76			s = nil
77		}
78	}()
79	log := s.log
80	s.sizes = opts.Sizes
81	if len(s.sizes) == 0 {
82		s.sizes = [][2]int{{1920, 1080}}
83	}
84	if opts.Backend == "drm" {
85		if s.hw, err = openDRM(ctx, opts.Config); err != nil {
86			return s, err
87		}
88	}
89	s.p = newPipes(s.hw != nil)
90	// core() carries channels only; the gate, clock and terminal flag are
91	// explicit core.Options.
92	if s.core, err = core.New(opts.Config, s.p.core(), core.Options{Security: s.security, Clock: s.clock, Terminal: !opts.NoTerminal}); err != nil {
93		return s, err
94	}
95	if s.km, err = xkb.New(xkb.RMLVO{Layout: opts.Config.Keyboard.Layout, Variant: opts.Config.Keyboard.Variant, Options: opts.Config.Keyboard.Options}); err != nil {
96		return s, err
97	}
98	runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
99	// Every output renders on the same GPU: its formats are the clients'.
100	s.dmabuf = vulkan.Probe()
101	log.Info().Int("formats", len(s.dmabuf.Formats)).Msg("dmabuf")
102	// One launch prefix for ext-workspace ids and the state file.
103	s.wsIDs = workspaceid.New()
104	// The path is fixed: a config key would let a program of the same user
105	// widen who may capture.
106	// Production: the fixed path, a root-owned file. Only tests set a path,
107	// and an owner for it.
108	captureAllowLog := logging.For(ctx, "captureallow")
109	if opts.captureAllowPath != "" {
110		s.captureAllow = captureallow.NewStoreOwnedBy(opts.captureAllowPath, opts.captureAllowOwner, captureAllowLog)
111	} else {
112		s.captureAllow = captureallow.NewStore(captureallow.DefaultPath, captureAllowLog)
113	}
114	if s.server, err = wayland.New(wayland.Options{
115		CaptureAllow: s.captureAllow,
116		WorkspaceIDs: s.wsIDs,
117		Security:     s.security,
118		RuntimeDir:   runtimeDir,
119		DMABuf:       s.dmabuf,
120		SyncobjNode:  renderNode(s.dmabuf.Device),
121		Keymap:       s.km.String(),
122		RepeatRate:   opts.Config.Keyboard.RepeatRate,
123		RepeatDelay:  opts.Config.Keyboard.RepeatDelay,
124	}, s.p.wayland(), logging.For(ctx, "wayland")); err != nil {
125		return s, err
126	}
127	log.Info().Str("WAYLAND_DISPLAY", s.server.SocketName()).Msg("listening")
128	s.childEnv = launcher.ChildEnv(os.Environ(), s.server.SocketName(), runtimeDir, cursorSize())
129	statePath, serr := statefile.Path(runtimeDir, s.server.SocketName())
130	if serr != nil {
131		log.Warn().Err(serr).Msg("state file disabled")
132	} else {
133		s.statePath = statePath
134		s.childEnv = append(s.childEnv, statefile.Env+"="+statePath)
135	}
136	if s.xdisplay = openXwayland(ctx, opts, s.childEnv, logging.For(ctx, "xwayland")); s.xdisplay != nil {
137		s.childEnv = append(s.childEnv, "DISPLAY="+s.xdisplay.Name())
138	}
139	s.child = launcher.New(s.childEnv, s.security, logging.For(ctx, "launcher"))
140	s.curs = newCursors(s.clock, opts.Config.Cursor.HideAfter)
141	s.path = opts.ConfigPath
142	if s.path == "" {
143		s.path = config.DefaultPath()
144	}
145	renderLog := logging.For(ctx, "render")
146	if s.hw != nil {
147		s.newRenderer = newVulkanRenderer(renderLog, false, false)
148	} else {
149		s.newRenderer = newVulkanRenderer(renderLog, true, opts.ScreenshotRaw)
150	}
151	return s, nil
152}
153
154// abort releases what assembly acquired before ownership passed on: the DRM
155// backend and the keymap. It runs only when assembly fails, before
156// anything else is owned.
157func (s *session) abort() {
158	if s.hw != nil {
159		s.hw.close()
160	}
161	if s.km != nil {
162		s.km.Close()
163	}
164}
165
166// close releases what the session owns after its workers have joined. The
167// keymap and the wayland server are not here: the input goroutine and
168// Server.Run own them.
169func (s *session) close() {
170	s.curs.stop()
171	if s.xdisplay != nil {
172		s.xdisplay.Close()
173	}
174	if s.hw != nil {
175		s.hw.close()
176	}
177}
178
179// newVulkanRenderer returns the concrete Vulkan renderer factory. Virtual
180// outputs have no display to list modifiers, so a headless renderer is told
181// it drives one: without it a real GPU refuses the HDR targets and the
182// output falls back to SDR. readback makes the HDR targets readable
183// (HDRPixels). These settings are Vulkan-only and not on ports.Renderer.
184func newVulkanRenderer(log zerowrap.Logger, virtual, readback bool) func(w, h int) (ports.Renderer, error) {
185	return func(w, h int) (ports.Renderer, error) {
186		r, err := vulkan.New(w, h)
187		if err != nil {
188			return nil, err
189		}
190		r.SetLogger(log)
191		log.Info().Str("queue_priority", r.QueuePriority()).Msg("vulkan queue")
192		r.SetVirtualOutput(virtual)
193		if readback {
194			r.SetHDRReadback(true)
195		}
196		return r, nil
197	}
198}
199
200// run starts the workers and waits for the session to end; it releases the
201// session's resources after every worker has joined.
202func (s *session) run(ctx context.Context, inject func(chan<- ports.InputEvent)) error {
203	defer s.close()
204	ctx, cancel := context.WithCancel(ctx)
205	defer cancel()
206	opts, log, p, hw, km, security := s.opts, s.log, s.p, s.hw, s.km, s.security
207	childEnv, curs, xdisplay, statePath := s.childEnv, s.curs, s.xdisplay, s.statePath
208	server, captureAllow, c, child, wsIDs, dmabuf, sizes, path := s.server, s.captureAllow, s.core, s.child, s.wsIDs, s.dmabuf, s.sizes, s.path
209	if opts.Session {
210		launcher.ExportSession(ctx, childEnv, logging.For(ctx, "launcher"))
211		launcher.NotifyReady(os.Getenv("NOTIFY_SOCKET"), logging.For(ctx, "launcher"))
212		defer launcher.UnexportSession(childEnv, logging.For(ctx, "launcher"))
213	}
214	if inject != nil {
215		inject(p.input)
216	}
217	var workers sync.WaitGroup
218	workers.Add(8)
219	done := make(chan error, 8)
220	go func() {
221		defer workers.Done()
222		done <- config.Watch(ctx, path, p.watched, logging.For(ctx, "config"))
223	}()
224	if p.scales != nil {
225		workers.Add(1)
226		go func() {
227			defer workers.Done()
228			core.PersistScales(ctx, p.scales, config.ScaleStore{Path: path}, launcher.NewNotifier(ctx, childEnv, logging.For(ctx, "launcher")), s.clock, time.Second)
229		}()
230	}
231	workers.Add(1)
232	go func() {
233		defer workers.Done()
234		relayConfig(ctx, opts.Config, p.watched, p.filtered, p.keymaps, p.deviceConfigs, curs, p.commands, logging.For(ctx, "config"))
235	}()
236	script := make(chan string)
237	go func() {
238		defer workers.Done()
239		if hw != nil {
240			// Core sends the first layout once DRM reports the outputs.
241			var layout ports.Layout
242			select {
243			case layout = <-p.layouts:
244			case <-ctx.Done():
245				km.Close()
246				return
247			}
248			done <- safe("input", func() error {
249				if opts.Config.Performance.Realtime {
250					runtime.LockOSThread()
251					// Let this dedicated thread exit with its scheduling policy.
252					schedLog := logging.For(ctx, "sched")
253					if err := sched.Realtime(schedLog); err != nil {
254						schedLog.Warn().Err(err).Msg("input scheduling")
255					}
256				}
257				return libinput.Run(ctx, libinput.Options{
258					Security:      security,
259					Seat:          hw.seat,
260					SeatName:      hw.seat.Name(),
261					Keymap:        km,
262					Keymaps:       p.keymaps,
263					Layout:        layout,
264					Layouts:       p.layouts,
265					Constraints:   p.constraints,
266					DeviceConfig:  opts.Config.InputDevicesConfig,
267					DeviceConfigs: p.deviceConfigs,
268					Active:        hw.seat.Subscribe(),
269					MoveCursor:    curs.move,
270					Log:           logging.For(ctx, "input"),
271					LogMotion:     logging.Enabled(ctx, "input-motion"),
272					LogKeys:       logging.Enabled(ctx, "input-keys"),
273				}, p.input)
274			})
275			return
276		}
277		if err := headlessinput.RunSecure(ctx, km, p.keymaps, script, p.input, p.layouts, func(o string, x, y float64) { curs.move(o, x, y, true) }, logging.For(ctx, "input"), security); err != nil && !errors.Is(err, context.Canceled) {
278			select {
279			case done <- err:
280			case <-ctx.Done():
281			}
282		}
283	}()
284	if opts.Script != nil {
285		go func() { <-ctx.Done(); _ = opts.Script.Close() }()
286		go func() {
287			defer close(script)
288			scanner := bufio.NewScanner(opts.Script)
289			for scanner.Scan() {
290				select {
291				case <-ctx.Done():
292					return
293				case script <- scanner.Text():
294				}
295			}
296			if err := scanner.Err(); err != nil {
297				log.Warn().Err(err).Msg("reading input script")
298			}
299		}()
300	} else {
301		close(script)
302	}
303	go func() { defer workers.Done(); done <- server.Run(ctx) }()
304	go func() {
305		defer workers.Done()
306		// Without a watch the allowlist stays as loaded: restart to apply.
307		if err := captureAllow.Run(ctx); err != nil {
308			log.Warn().Err(err).Msg("capture allowlist not watched, restart to apply changes")
309		}
310	}()
311	go func() { defer workers.Done(); done <- child.Run(ctx, p.spawn) }()
312	go func() { defer workers.Done(); done <- c.Run(ctx) }()
313	if xdisplay != nil {
314		workers.Add(1)
315		// X11 failing leaves the Wayland session running; Run logs why.
316		go func() { defer workers.Done(); _ = xdisplay.Run(ctx) }()
317	}
318	if statePath != "" {
319		workers.Add(1)
320		go func() {
321			defer workers.Done()
322			if err := statefile.Run(ctx, statePath, p.states, wsIDs, logging.For(ctx, "statefile")); err != nil {
323				// Scripts lose their state; the session goes on.
324				log.Warn().Err(err).Msg("state file disabled")
325			}
326		}()
327	}
328	go func() {
329		defer workers.Done()
330		consumeScenes(ctx, p.scenes, p.configErrors, opts.testScenes, p.renderScenes)
331	}()
332	workers.Add(1)
333	go func() { defer workers.Done(); relayPresented(ctx, p.flips, p.presented, p.frames) }()
334
335	go func() {
336		defer workers.Done()
337		outputIO := p.outputs(security)
338		apply := newOutputApply(newOutputOverrides(opts.Config, hw == nil), logging.For(ctx, "app"))
339		if hw != nil {
340			done <- safe("output", func() error {
341				want := func(cfg ports.Config) drm.Want {
342					w := wantFromConfig(cfg)
343					w.Sampled = dmabuf.Formats
344					w.TraceFlips = logging.Enabled(ctx, "drm-flip")
345					return w
346				}
347				return hw.runOutputs(ctx, want, opts.Config, apply, outputIO, curs, s.newRenderer, logging.For(ctx, "drm"))
348			})
349			return
350		}
351		done <- runHeadless(ctx, headlessOptions{sizes: sizes, shots: opts.ScreenshotDir, hdr: opts.HeadlessHDR, raw: opts.ScreenshotRaw}, apply, outputIO, curs, s.newRenderer, logging.For(ctx, "render"))
352	}()
353
354	if hw != nil {
355		workers.Add(1)
356		go func() { defer workers.Done(); done <- safe("seat", func() error { return hw.seat.Run(ctx) }) }()
357		// Only a real session serves the session bus: headless runs would take
358		// the name from the desktop they run in.
359		workers.Add(1)
360		go func() {
361			defer workers.Done()
362			reports := screensaver.Reports{Held: p.idleInhibited, Activity: p.idleActivity}
363			screensaver.Serve(ctx, "", reports, screensaverRetry, logging.For(ctx, "screensaver"))
364		}()
365	}
366	var result error
367	select {
368	case <-ctx.Done():
369		if errors.Is(context.Cause(ctx), errTimeout) {
370			log.Info().Str("reason", "timeout").Dur("after", opts.Timeout).Msg("stopping")
371		}
372	case result = <-done:
373		log.Info().AnErr("cause", result).Msg("stopping")
374	}
375	cancel()
376	workers.Wait()
377	// The wayland server has stopped: no request can be queued any more.
378	drainCaptures(ctx, p.captures, nil)
379	// A keymap the input goroutine never took is still ours to free.
380	select {
381	case km := <-p.keymaps:
382		km.Close()
383	default:
384	}
385	for len(done) > 0 {
386		err := <-done
387		if result == nil && err != nil {
388			result = err
389		}
390	}
391	if errors.Is(result, core.ErrQuit) || errors.Is(result, libinput.ErrEmergencyQuit) {
392		return nil
393	}
394	return result
395}

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