1package app
2
3import (
4 "errors"
5
6 "github.com/bnema/neferwl/internal/core"
7 "github.com/bnema/neferwl/internal/ports"
8 "github.com/bnema/zerowrap"
9)
10
11// errApplySuperseded answers a request whose configuration a reload replaced.
12var errApplySuperseded = errors.New("output apply superseded by reload")
13
14// outputApply owns output management from request to reply: runtime
15// overrides, supersede by reload, and rollback after a failed backend apply.
16// It belongs to the backend owner loop, which starts the configuration next
17// returns, reports each result through finished and drains the outbox (core
18// configuration, Wayland replies and inventory) with sends that never block
19// the loop.
20type outputApply struct {
21 state *core.OutputOverrides
22 log zerowrap.Logger
23 // running is set while the backend applies a configuration. The next
24 // one waits in start until it finishes: configurations never overlap.
25 running bool
26 start ports.Config
27 pending bool
28 // req is the request behind the backend work; nil for a reload or after
29 // a reload superseded it.
30 req *ports.OutputApply
31 previous map[string]ports.OutputConfig
32 rollback bool
33 failure error
34 // config is the latest effective configuration core has not received.
35 config ports.ConfigChanged
36 publish bool
37 replies []ports.OutputApplied
38 // headsDue is set while Wayland has not received the latest inventory.
39 headsDue bool
40}
41
42func newOutputApply(state *core.OutputOverrides, log zerowrap.Logger) *outputApply {
43 return &outputApply{state: state, log: log}
44}
45
46// reload replaces the file configuration and supersedes the pending request.
47// Only the latest reload needs backend work.
48func (a *outputApply) reload(file ports.Config) {
49 cfg := a.state.Reload(file)
50 a.setConfig(cfg)
51 if a.req != nil {
52 a.reply(a.req.ID, errApplySuperseded)
53 a.req = nil
54 }
55 if !a.state.Headless() {
56 a.due(cfg)
57 }
58}
59
60// heads records the backend inventory: it validates requests and waits for
61// Wayland in the outbox, latest only.
62func (a *outputApply) heads(heads ports.OutputHeads) {
63 a.state.SetHeads(heads)
64 a.headsDue = true
65}
66
67// request validates a protocol request. Test, invalid and headless requests
68// are answered at once; others wait for the backend.
69func (a *outputApply) request(req ports.OutputApply) {
70 if a.running || a.pending {
71 a.reply(req.ID, errors.New("output apply already pending"))
72 return
73 }
74 previous := a.state.Snapshot()
75 cfg, err := a.state.Apply(req)
76 if err != nil || req.Test {
77 a.reply(req.ID, err)
78 return
79 }
80 a.setConfig(cfg)
81 if a.state.Headless() {
82 a.reply(req.ID, nil)
83 return
84 }
85 a.req, a.previous, a.rollback, a.failure = &req, previous, false, nil
86 a.due(cfg)
87}
88
89// next returns the configuration the backend must start now, once. It
90// returns nothing while the backend applies the previous one.
91func (a *outputApply) next() (ports.Config, bool) {
92 if a.running || !a.pending {
93 return ports.Config{}, false
94 }
95 cfg := a.start
96 a.start, a.pending, a.running = ports.Config{}, false, true
97 return cfg, true
98}
99
100// finished records the backend result of the running configuration. When a
101// reload is waiting, the result belongs to a superseded configuration and is
102// dropped. After a failed request the previous overrides return and the
103// rollback configuration becomes due; the request is answered with the
104// original failure once the rollback is done.
105func (a *outputApply) finished(err error) {
106 if !a.running {
107 return
108 }
109 a.running = false
110 if a.pending {
111 return
112 }
113 req := a.req
114 if req == nil {
115 return
116 }
117 if a.rollback {
118 a.req = nil
119 a.reply(req.ID, a.failure)
120 return
121 }
122 if err != nil {
123 cfg := a.state.Restore(a.previous)
124 a.setConfig(cfg)
125 a.rollback, a.failure = true, err
126 a.due(cfg)
127 return
128 }
129 a.req = nil
130 for _, h := range req.Heads {
131 e := a.log.Info().Str("output", h.Name).Bool("enabled", h.Enabled).Float64("scale", h.Scale)
132 if h.Transform != nil {
133 e = e.Uint8("transform", uint8(*h.Transform))
134 }
135 e.Msg("output configuration applied")
136 }
137 a.reply(req.ID, nil)
138}
139
140// due replaces the configuration waiting to start: only the latest counts.
141func (a *outputApply) due(cfg ports.Config) {
142 a.start, a.pending = cfg, true
143}
144
145func (a *outputApply) setConfig(cfg ports.Config) {
146 a.config = ports.ConfigChanged{Config: cfg}
147 a.publish = true
148}
149
150func (a *outputApply) reply(id uint64, err error) {
151 if err != nil {
152 a.log.Warn().Err(err).Msg("output configuration failed")
153 }
154 a.replies = append(a.replies, ports.OutputApplied{ID: id, Err: err})
155}
156
157// configOut returns the core channel when a configuration waits, else nil so
158// the owner loop's send case stays disabled.
159func (a *outputApply) configOut(ch chan<- ports.ConfigChanged) chan<- ports.ConfigChanged {
160 if !a.publish {
161 return nil
162 }
163 return ch
164}
165
166// configSent records that core received the latest configuration.
167func (a *outputApply) configSent() { a.publish = false }
168
169// replyOut returns the reply channel and the oldest reply, or a nil channel.
170func (a *outputApply) replyOut(ch chan<- ports.OutputApplied) (chan<- ports.OutputApplied, ports.OutputApplied) {
171 if len(a.replies) == 0 {
172 return nil, ports.OutputApplied{}
173 }
174 return ch, a.replies[0]
175}
176
177// headsOut returns the Wayland inventory channel and the latest inventory,
178// or a nil channel when Wayland has it.
179func (a *outputApply) headsOut(ch chan<- ports.OutputHeads) (chan<- ports.OutputHeads, ports.OutputHeads) {
180 if !a.headsDue {
181 return nil, ports.OutputHeads{}
182 }
183 return ch, a.state.Heads()
184}
185
186// headsSent records that Wayland received the latest inventory.
187func (a *outputApply) headsSent() { a.headsDue = false }
188
189// replySent drops the delivered reply.
190func (a *outputApply) replySent() {
191 a.replies[0] = ports.OutputApplied{}
192 a.replies = a.replies[1:]
193}