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}