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}