1package sessionlock
2
3import "github.com/bnema/neferwl/internal/ports"
4
5// Readiness is pure readiness bookkeeping owned by one goroutine. Its zero
6// value is an empty, disengaged ledger. It must not be copied after first use;
7// it owns its inventory and never exposes mutable state.
8//
9// Readiness is not authentication or protocol policy. All acknowledgements
10// must be affirmative evidence from the respective owner, never an inference
11// from a failed operation.
12type Readiness struct {
13 generation ports.LockGeneration // high-water mark, retained after End
14 active bool
15 backend bool
16 capture bool
17 outputs map[ports.OutputInstance]lockOutputReadiness
18}
19
20type lockOutputReadiness struct {
21 registered bool
22 proof ports.ProtectionKind
23}
24
25// Add registers a new output lifetime before or during a lock. Zero, duplicate
26// and retired IDs are rejected without changing state. A new output has no
27// proof, so adding it while engaged revokes Ready until it is protected. IDs
28// remain as tombstones after Remove to prevent accepting a replaced lifetime.
29func (r *Readiness) Add(instance ports.OutputInstance) bool {
30 if instance == 0 {
31 return false
32 }
33 if _, seen := r.outputs[instance]; seen {
34 return false
35 }
36 if r.outputs == nil {
37 r.outputs = make(map[ports.OutputInstance]lockOutputReadiness)
38 }
39 r.outputs[instance] = lockOutputReadiness{registered: true}
40 return true
41}
42
43// Remove retires a lifetime only when the caller has confirmed it is actually
44// disconnected or inactive. An error (including "gone") is not confirmation.
45// An inactive lifetime that may resume should instead remain registered with
46// an inactive-output proof. Unknown or already retired instances are rejected.
47func (r *Readiness) Remove(instance ports.OutputInstance) bool {
48 output, ok := r.outputs[instance]
49 if !ok || !output.registered {
50 return false
51 }
52 r.outputs[instance] = lockOutputReadiness{}
53 return true
54}
55
56// Begin snapshots the registered inventory into a new readiness round by
57// clearing every proof and both barriers. Registrations may subsequently
58// change. Generations must increase strictly, even across End; an invalid
59// Begin leaves the current round untouched. Exhaustion must not wrap to zero.
60func (r *Readiness) Begin(generation ports.LockGeneration) bool {
61 if generation == 0 || generation <= r.generation {
62 return false
63 }
64 r.generation, r.active = generation, true
65 r.backend, r.capture = false, false
66 for instance, output := range r.outputs {
67 output.proof = ""
68 r.outputs[instance] = output
69 }
70 return true
71}
72
73// End clears only the exact active round, retaining inventory and the
74// generation high-water mark. Late End calls cannot cancel a newer round, and
75// late proofs or barriers cannot reactivate an ended round.
76func (r *Readiness) End(generation ports.LockGeneration) bool {
77 if !r.accepts(generation) {
78 return false
79 }
80 r.active, r.backend, r.capture = false, false, false
81 for instance, output := range r.outputs {
82 output.proof = ""
83 r.outputs[instance] = output
84 }
85 return true
86}
87
88// Record accepts only affirmative protection evidence for a registered
89// lifetime in the exact active generation. Repeated valid proofs are harmless;
90// invalid evidence never overwrites an existing proof.
91func (r *Readiness) Record(proof ports.OutputProtection) bool {
92 if !r.accepts(proof.Generation) {
93 return false
94 }
95 switch proof.Kind {
96 case ports.ProtectionProtectedFrame, ports.ProtectionInactiveOutput:
97 default:
98 return false
99 }
100 output, ok := r.outputs[proof.Instance]
101 if !ok || !output.registered {
102 return false
103 }
104 output.proof = proof.Kind
105 r.outputs[proof.Instance] = output
106 return true
107}
108
109// Invalidate revokes a prior proof within the active epoch. A recovered or
110// resumed lifetime must provide fresh affirmative protection, never reuse its
111// earlier presentation. Unknown, retired and stale epochs have no effect.
112func (r *Readiness) Invalidate(generation ports.LockGeneration, instance ports.OutputInstance) bool {
113 if !r.accepts(generation) {
114 return false
115 }
116 output, ok := r.outputs[instance]
117 if !ok || !output.registered {
118 return false
119 }
120 output.proof = ""
121 r.outputs[instance] = output
122 return true
123}
124
125// BackendBarrier acknowledges authoritative output inventory, drainage of all
126// backend leases, and completed security handoff for the active generation.
127// This is required even with zero outputs; an empty local inventory is not an
128// acknowledgement. It does not acknowledge capture memory-copy completion.
129func (r *Readiness) BackendBarrier(generation ports.LockGeneration) bool {
130 if !r.accepts(generation) {
131 return false
132 }
133 r.backend = true
134 return true
135}
136
137// CaptureBarrier acknowledges that all admitted prelock memory-copy work,
138// including writes to capture FDs, has completed for the active generation.
139// Output protection and backend drainage cannot substitute for this barrier.
140func (r *Readiness) CaptureBarrier(generation ports.LockGeneration) bool {
141 if !r.accepts(generation) {
142 return false
143 }
144 r.capture = true
145 return true
146}
147
148// Ready requires an active round, both independent barriers, and affirmative
149// protection evidence for every currently registered output lifetime.
150func (r *Readiness) Ready() bool {
151 if !r.active || !r.backend || !r.capture {
152 return false
153 }
154 for _, output := range r.outputs {
155 if output.registered && output.proof == "" {
156 return false
157 }
158 }
159 return true
160}
161
162func (r *Readiness) accepts(generation ports.LockGeneration) bool {
163 return r.active && generation != 0 && generation == r.generation
164}