From 99766c82300253d73bc7c5a293a2a88390e78dc2 Mon Sep 17 00:00:00 2001 From: Rob Watson Date: Thu, 13 Mar 2025 18:01:17 +0100 Subject: [PATCH] feat: error handling --- internal/app/app.go | 46 +++++++++++++++++++++++++---- internal/container/container.go | 4 --- internal/domain/types.go | 2 ++ internal/mediaserver/actor.go | 1 + internal/multiplexer/multiplexer.go | 15 ++++------ internal/terminal/terminal.go | 23 +++++++++++++-- 6 files changed, 70 insertions(+), 21 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index b05201c..88593c1 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -106,7 +106,12 @@ func Run(ctx context.Context, params RunParams) error { applyServerState(serverState, state) updateUI() case mpState := <-mp.C(): - applyMultiplexerState(mpState, state) + destErrors := applyMultiplexerState(mpState, state) + + for _, destError := range destErrors { + handleDestError(destError, mp, ui) + } + updateUI() } } @@ -117,18 +122,49 @@ func applyServerState(serverState domain.Source, appState *domain.AppState) { 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. -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 { continue } - appState.Destinations[i].Container = mpState.Container - appState.Destinations[i].Status = mpState.Status + if dest.Container.Err == nil && mpState.Container.Err != nil { + errorsToDisplay = append(errorsToDisplay, destinationError{ + name: dest.Name, + url: dest.URL, + err: mpState.Container.Err, + }) + } + + dest.Container = mpState.Container + dest.Status = mpState.Status 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. diff --git a/internal/container/container.go b/internal/container/container.go index 28a10c7..dff90f6 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -263,11 +263,8 @@ func (a *Client) runContainerLoop( case resp := <-respC: // Check if the container is restarting. If it is not then we don't // want to wait for it again and can return early. - // - // Low priority: is the API call necessary? ctr, err := a.apiClient.ContainerInspect(ctx, containerID) if err != nil { - // TODO: error handling? a.logger.Error("Error inspecting container", "err", err, "id", shortID(containerID)) containerErrC <- err return @@ -282,7 +279,6 @@ func (a *Client) runContainerLoop( } case err := <-errC: // Otherwise, this is probably unexpected and we need to handle it. - // TODO: improve handling containerErrC <- err return case <-ctx.Done(): diff --git a/internal/domain/types.go b/internal/domain/types.go index d19da79..9c74d7a 100644 --- a/internal/domain/types.go +++ b/internal/domain/types.go @@ -39,6 +39,7 @@ type Source struct { ExitReason string } +// DestinationStatus reflects the high-level status of a single destination. type DestinationStatus int const ( @@ -81,4 +82,5 @@ type Container struct { RxSince time.Time RestartCount int ExitCode *int + Err error // Err is set if any error was received from the container client. } diff --git a/internal/mediaserver/actor.go b/internal/mediaserver/actor.go index 8ec7913..ea1e035 100644 --- a/internal/mediaserver/actor.go +++ b/internal/mediaserver/actor.go @@ -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) { if s.state.Container.ExitCode != nil { s.state.ExitReason = fmt.Sprintf("Server process exited with code %d.", *s.state.Container.ExitCode) diff --git a/internal/multiplexer/multiplexer.go b/internal/multiplexer/multiplexer.go index fd886a3..87dcac2 100644 --- a/internal/multiplexer/multiplexer.go +++ b/internal/multiplexer/multiplexer.go @@ -84,6 +84,9 @@ func (a *Actor) StartDestination(url string) { return } + a.nextIndex++ + a.currURLs[url] = struct{}{} + a.logger.Info("Starting live stream", "url", url) 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"}, }) - a.nextIndex++ - a.currURLs[url] = struct{}{} - a.wg.Add(1) go func() { defer a.wg.Done() @@ -146,12 +146,6 @@ func (a *Actor) StopDestination(url string) { // destLoop is the actor loop for a destination stream. 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} sendState := func() { a.stateC <- *state } @@ -171,10 +165,11 @@ func (a *Actor) destLoop(url string, containerStateC <-chan domain.Container, er } sendState() case err := <-errC: - // TODO: error handling if err != nil { a.logger.Error("Error from container client", "err", err) } + state.Container.Err = err + sendState() return } } diff --git a/internal/terminal/terminal.go b/internal/terminal/terminal.go index b807e22..caedc7a 100644 --- a/internal/terminal/terminal.go +++ b/internal/terminal/terminal.go @@ -251,7 +251,6 @@ func (ui *UI) ShowStartupCheckModal() bool { []string{"Continue", "Exit"}, func(buttonIndex int, _ string) { if buttonIndex == 0 { - ui.pages.RemovePage("modal") ui.app.SetFocus(ui.destView) done <- true } else { @@ -264,6 +263,27 @@ func (ui *UI) ShowStartupCheckModal() bool { 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. func (ui *UI) AllowQuit() { ui.mu.Lock() @@ -580,7 +600,6 @@ func (ui *UI) confirmQuit() { []string{"Quit", "Cancel"}, func(buttonIndex int, _ string) { if buttonIndex == 0 { - ui.logger.Info("quitting") ui.commandCh <- CommandQuit{} return }