record_lifecycle.go RawHistoryFolder
1package app
2
3import (
4 "context"
5 "errors"
6 "sync/atomic"
7
8 "github.com/bnema/nefercap/internal/adapters/uierrors"
9)
10
11var (
12 // errUserStop is the cancellation cause of a stop requested by the user
13 // (control command or HUD button). It is not a failure.
14 errUserStop = errors.New("recording stopped by user")
15 // errHUDClosed and errHUDClosedEarly report a HUD that ended although
16 // nobody asked it to.
17 errHUDClosed = errors.New("recording controls closed unexpectedly")
18 errHUDClosedEarly = errors.New("recording controls closed before authorization")
19)
20
21// recordingOutcome says what a recording left behind, independently of the
22// error: only a Saved recording has a path worth printing.
23type recordingOutcome struct{ Saved bool }
24
25// lifecycle is the state shared by the recording owner, the stop watchers and
26// the compositor's detach callback. Everything here is safe for concurrent use
27// and non-blocking. It never touches the Source: the owner goroutine alone
28// runs capture, so borrowed pixels and protocol state stay single-owner.
29//
30// The recording context carries the first cause that ended it: errUserStop for
31// a requested stop, or the first real failure. A failure is also kept in a
32// one-slot channel so it survives a parent cancellation that won the context.
33type lifecycle struct {
34 parent context.Context
35 cancelRecord context.CancelCauseFunc
36 cancelHUD context.CancelFunc
37 shuttingDown atomic.Bool
38 failure chan error
39}
40
41func newLifecycle(parent context.Context, cancelRecord context.CancelCauseFunc, cancelHUD context.CancelFunc) *lifecycle {
42 return &lifecycle{parent: parent, cancelRecord: cancelRecord, cancelHUD: cancelHUD, failure: make(chan error, 1)}
43}
44
45// userStop ends the recording and its HUD without a failure. The flag is set
46// first so the detach the HUD teardown provokes is recognised as an echo.
47func (l *lifecycle) userStop() {
48 l.shuttingDown.Store(true)
49 l.cancelRecord(errUserStop)
50 l.cancelHUD()
51}
52
53// beginShutdown marks the owner's own teardown; call it before stopping the HUD.
54func (l *lifecycle) beginShutdown() { l.shuttingDown.Store(true) }
55
56// fail records the first real failure, and ends recording and HUD with it as
57// the cause. It reports whether err was the first.
58func (l *lifecycle) fail(err error) bool {
59 select {
60 case l.failure <- err:
61 default:
62 return false
63 }
64 l.cancelRecord(err)
65 l.cancelHUD()
66 return true
67}
68
69// detached is the compositor's layer-detach callback. A detach that arrives
70// before shutdown begins is real: the exclusion is gone and recording must
71// stop. One that follows our own shutdown or the parent's cancellation is the
72// echo of closing the HUD and is ignored.
73func (l *lifecycle) detached(err error) {
74 if l.shuttingDown.Load() || l.parent.Err() != nil {
75 return
76 }
77 if err == nil {
78 err = errHUDClosed
79 }
80 l.fail(err)
81}
82
83// hudEnded handles a HUD that returned. If nobody asked it to stop, that is a
84// failure: its own real error, or a closed error when it ended quietly. It
85// returns the part of err that classify must still consider.
86func (l *lifecycle) hudEnded(err error, early bool) error {
87 if l.shuttingDown.Load() || l.parent.Err() != nil {
88 return err
89 }
90 own := err != nil && !uierrors.IsCancellation(err)
91 reason := err
92 if !own {
93 reason = errHUDClosed
94 if early {
95 reason = errHUDClosedEarly
96 }
97 }
98 if !l.fail(reason) || !own {
99 return err
100 }
101 return nil
102}
103
104// failed returns the first recorded failure, if any. Call it once the
105// recording context is done, so the slot has been written.
106func (l *lifecycle) failed() error {
107 select {
108 case err := <-l.failure:
109 return err
110 default:
111 return nil
112 }
113}
114
115// stopOnSignal turns one HUD button press into a user stop. Its lifetime is
116// bounded by ctx and the completion channel is joined by the caller.
117func stopOnSignal(ctx context.Context, signal <-chan struct{}, stop context.CancelFunc) <-chan struct{} {
118 done := make(chan struct{})
119 go func() {
120 defer close(done)
121 select {
122 case <-ctx.Done():
123 case <-signal:
124 stop()
125 }
126 }()
127 return done
128}
129
130// ending gathers everything that explains how one recording ended.
131type ending struct {
132 runErr error // core.Run, nil when it did not run
133 hudErr error // HUD result after it was stopped
134 failure error // first real failure recorded by the lifecycle
135 parentErr error // parent context error at the end
136 userStop bool
137}
138
139// classify maps an ending to the error of the invocation. Real failures win
140// over any cancellation, including ones joined with a cancellation; a bare
141// user stop is success; a bare parent cancellation returns the parent's error.
142func classify(e ending) error {
143 var real []error
144 if e.failure != nil {
145 real = append(real, e.failure)
146 }
147 for _, err := range []error{e.runErr, e.hudErr} {
148 if err != nil && !uierrors.IsCancellation(err) {
149 real = append(real, err)
150 }
151 }
152 switch len(real) {
153 case 0:
154 case 1:
155 return real[0]
156 default:
157 return errors.Join(real...)
158 }
159 switch {
160 case e.userStop:
161 return nil
162 case e.parentErr != nil:
163 return e.parentErr
164 }
165 return e.runErr
166}