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

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