feat: error handling

This commit is contained in:
Rob Watson 2025-03-13 18:01:17 +01:00 committed by Rob Watson
parent 96117c0a15
commit 99766c8230
6 changed files with 70 additions and 21 deletions

View File

@ -106,7 +106,12 @@ func Run(ctx context.Context, params RunParams) error {
applyServerState(serverState, state) applyServerState(serverState, state)
updateUI() updateUI()
case mpState := <-mp.C(): case mpState := <-mp.C():
applyMultiplexerState(mpState, state) destErrors := applyMultiplexerState(mpState, state)
for _, destError := range destErrors {
handleDestError(destError, mp, ui)
}
updateUI() updateUI()
} }
} }
@ -117,18 +122,49 @@ func applyServerState(serverState domain.Source, appState *domain.AppState) {
appState.Source = serverState appState.Source = serverState
} }
// destinationError holds the information needed to display a destination
// error.
type destinationError struct {
name string
url string
err error
}
// applyMultiplexerState applies the current multiplexer state to the app state. // applyMultiplexerState applies the current multiplexer state to the app state.
func applyMultiplexerState(mpState multiplexer.State, appState *domain.AppState) { //
for i, dest := range appState.Destinations { // It returns a list of destination errors that should be displayed to the user.
func applyMultiplexerState(mpState multiplexer.State, appState *domain.AppState) []destinationError {
var errorsToDisplay []destinationError
for i := range appState.Destinations {
dest := &appState.Destinations[i]
if dest.URL != mpState.URL { if dest.URL != mpState.URL {
continue continue
} }
appState.Destinations[i].Container = mpState.Container if dest.Container.Err == nil && mpState.Container.Err != nil {
appState.Destinations[i].Status = mpState.Status errorsToDisplay = append(errorsToDisplay, destinationError{
name: dest.Name,
url: dest.URL,
err: mpState.Container.Err,
})
}
dest.Container = mpState.Container
dest.Status = mpState.Status
break break
} }
return errorsToDisplay
}
// handleDestError displays a modal to the user, and stops the destination.
func handleDestError(destError destinationError, mp *multiplexer.Actor, ui *terminal.UI) {
ui.ShowDestinationErrorModal(destError.name, destError.err)
mp.StopDestination(destError.url)
} }
// newStateFromRunParams creates a new app state from the run parameters. // newStateFromRunParams creates a new app state from the run parameters.

View File

@ -263,11 +263,8 @@ func (a *Client) runContainerLoop(
case resp := <-respC: case resp := <-respC:
// Check if the container is restarting. If it is not then we don't // Check if the container is restarting. If it is not then we don't
// want to wait for it again and can return early. // want to wait for it again and can return early.
//
// Low priority: is the API call necessary?
ctr, err := a.apiClient.ContainerInspect(ctx, containerID) ctr, err := a.apiClient.ContainerInspect(ctx, containerID)
if err != nil { if err != nil {
// TODO: error handling?
a.logger.Error("Error inspecting container", "err", err, "id", shortID(containerID)) a.logger.Error("Error inspecting container", "err", err, "id", shortID(containerID))
containerErrC <- err containerErrC <- err
return return
@ -282,7 +279,6 @@ func (a *Client) runContainerLoop(
} }
case err := <-errC: case err := <-errC:
// Otherwise, this is probably unexpected and we need to handle it. // Otherwise, this is probably unexpected and we need to handle it.
// TODO: improve handling
containerErrC <- err containerErrC <- err
return return
case <-ctx.Done(): case <-ctx.Done():

View File

@ -39,6 +39,7 @@ type Source struct {
ExitReason string ExitReason string
} }
// DestinationStatus reflects the high-level status of a single destination.
type DestinationStatus int type DestinationStatus int
const ( const (
@ -81,4 +82,5 @@ type Container struct {
RxSince time.Time RxSince time.Time
RestartCount int RestartCount int
ExitCode *int ExitCode *int
Err error // Err is set if any error was received from the container client.
} }

View File

@ -234,6 +234,7 @@ func (s *Actor) actorLoop(containerStateC <-chan domain.Container, errC <-chan e
} }
} }
// TODO: refactor to use container.Err?
func (s *Actor) handleContainerExit(err error) { func (s *Actor) handleContainerExit(err error) {
if s.state.Container.ExitCode != nil { if s.state.Container.ExitCode != nil {
s.state.ExitReason = fmt.Sprintf("Server process exited with code %d.", *s.state.Container.ExitCode) s.state.ExitReason = fmt.Sprintf("Server process exited with code %d.", *s.state.Container.ExitCode)

View File

@ -84,6 +84,9 @@ func (a *Actor) StartDestination(url string) {
return return
} }
a.nextIndex++
a.currURLs[url] = struct{}{}
a.logger.Info("Starting live stream", "url", url) a.logger.Info("Starting live stream", "url", url)
containerStateC, errC := a.containerClient.RunContainer(a.ctx, container.RunContainerParams{ containerStateC, errC := a.containerClient.RunContainer(a.ctx, container.RunContainerParams{
@ -108,9 +111,6 @@ func (a *Actor) StartDestination(url string) {
NetworkCountConfig: container.NetworkCountConfig{Rx: "eth1", Tx: "eth0"}, NetworkCountConfig: container.NetworkCountConfig{Rx: "eth1", Tx: "eth0"},
}) })
a.nextIndex++
a.currURLs[url] = struct{}{}
a.wg.Add(1) a.wg.Add(1)
go func() { go func() {
defer a.wg.Done() defer a.wg.Done()
@ -146,12 +146,6 @@ func (a *Actor) StopDestination(url string) {
// destLoop is the actor loop for a destination stream. // destLoop is the actor loop for a destination stream.
func (a *Actor) destLoop(url string, containerStateC <-chan domain.Container, errC <-chan error) { func (a *Actor) destLoop(url string, containerStateC <-chan domain.Container, errC <-chan error) {
defer func() {
a.actorC <- func() {
delete(a.currURLs, url)
}
}()
state := &State{URL: url} state := &State{URL: url}
sendState := func() { a.stateC <- *state } sendState := func() { a.stateC <- *state }
@ -171,10 +165,11 @@ func (a *Actor) destLoop(url string, containerStateC <-chan domain.Container, er
} }
sendState() sendState()
case err := <-errC: case err := <-errC:
// TODO: error handling
if err != nil { if err != nil {
a.logger.Error("Error from container client", "err", err) a.logger.Error("Error from container client", "err", err)
} }
state.Container.Err = err
sendState()
return return
} }
} }

View File

@ -251,7 +251,6 @@ func (ui *UI) ShowStartupCheckModal() bool {
[]string{"Continue", "Exit"}, []string{"Continue", "Exit"},
func(buttonIndex int, _ string) { func(buttonIndex int, _ string) {
if buttonIndex == 0 { if buttonIndex == 0 {
ui.pages.RemovePage("modal")
ui.app.SetFocus(ui.destView) ui.app.SetFocus(ui.destView)
done <- true done <- true
} else { } else {
@ -264,6 +263,27 @@ func (ui *UI) ShowStartupCheckModal() bool {
return <-done return <-done
} }
func (ui *UI) ShowDestinationErrorModal(name string, err error) {
done := make(chan struct{})
ui.app.QueueUpdateDraw(func() {
ui.showModal(
modalGroupStartupCheck,
fmt.Sprintf(
"Streaming to %s failed:\n\n%s",
cmp.Or(name, "this destination"),
err,
),
[]string{"Ok"},
func(int, string) {
done <- struct{}{}
},
)
})
<-done
}
// AllowQuit enables the quit action. // AllowQuit enables the quit action.
func (ui *UI) AllowQuit() { func (ui *UI) AllowQuit() {
ui.mu.Lock() ui.mu.Lock()
@ -580,7 +600,6 @@ func (ui *UI) confirmQuit() {
[]string{"Quit", "Cancel"}, []string{"Quit", "Cancel"},
func(buttonIndex int, _ string) { func(buttonIndex int, _ string) {
if buttonIndex == 0 { if buttonIndex == 0 {
ui.logger.Info("quitting")
ui.commandCh <- CommandQuit{} ui.commandCh <- CommandQuit{}
return return
} }