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