-
Notifications
You must be signed in to change notification settings - Fork 3
/
eventloop.go
130 lines (111 loc) · 3.69 KB
/
eventloop.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package clio
import (
"context"
"errors"
"os"
"github.com/wagoodman/go-partybus"
"github.com/anchore/go-logger"
)
// eventloop listens to worker errors (from execution path), worker events (from a partybus subscription), and
// signal interrupts. Is responsible for handling each event relative to a given UI to coordinate eventing until
// an eventual graceful exit.
//
//nolint:gocognit,funlen
func eventloop(ctx context.Context, log logger.Logger, subscription *partybus.Subscription, workerErrs <-chan error, uis ...UI) error {
var events <-chan partybus.Event
if subscription != nil {
events = subscription.Events()
} else {
noEvents := make(chan partybus.Event)
close(noEvents)
events = noEvents
}
var ux UI
for _, ui := range uis {
if err := ui.Setup(subscription); err != nil {
log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err)
continue
}
ux = ui
break
}
var retErr []error
var forceTeardown bool
for {
if workerErrs == nil && events == nil {
break
}
select {
case err, isOpen := <-workerErrs:
if !isOpen {
log.Trace("worker stopped")
workerErrs = nil
continue
}
if err != nil {
// capture the error from the worker and unsubscribe to complete a graceful shutdown
retErr = append(retErr, err)
if subscription != nil {
_ = subscription.Unsubscribe()
}
// the worker has exited, we may have been mid-handling events for the UI which should now be
// ignored, in which case forcing a teardown of the UI regardless of the state is required.
forceTeardown = true
}
case e, isOpen := <-events:
if !isOpen {
log.Trace("bus stopped")
events = nil
continue
}
if e.Type == ExitEventType {
events = nil
if e.Value == os.Interrupt {
// on top of listening to signals from the OS, we also listen to interrupt events from the UI.
// Why provide two ways to do the same thing? In an application that has a UI where the terminal
// has been set to raw mode, ctrl-c will not register as an interrupt signal to the application.
// Instead the UI will capture the ctrl-c and need to signal the event loop to exit gracefully.
// Using the same signal channel for both OS signals and UI signals is not advisable and requires
// injecting the channel into the UI (also not advisable). Providing a bus event for the UI to
// use is a better solution here.
log.Trace("signal interrupt")
workerErrs = nil
forceTeardown = true
} else {
log.Trace("signal exit")
}
if subscription != nil {
_ = subscription.Unsubscribe()
}
}
if ux == nil {
continue
}
if err := ux.Handle(e); err != nil {
if errors.Is(err, partybus.ErrUnsubscribe) {
events = nil
} else {
retErr = append(retErr, err)
// TODO: should we unsubscribe? should we try to halt execution? or continue?
}
}
case <-ctx.Done():
log.Trace("signal interrupt")
// ignore further results from any event source and exit ASAP, but ensure that all cache is cleaned up.
// we ignore further errors since cleaning up the tmp directories will affect running catalogers that are
// reading/writing from/to their nested temp dirs. This is acceptable since we are bailing without result.
// TODO: potential future improvement would be to pass context into workers with a cancel function that is
// to the event loop. In this way we can have a more controlled shutdown even at the most nested levels
// of processing.
events = nil
workerErrs = nil
forceTeardown = true
}
}
if ux != nil {
if err := ux.Teardown(forceTeardown); err != nil {
retErr = append(retErr, err)
}
}
return errors.Join(retErr...)
}