feat: startup check
This commit is contained in:
parent
3d5e1a091d
commit
4e3a72893b
12
app/app.go
12
app/app.go
@ -45,6 +45,18 @@ func Run(
|
|||||||
}
|
}
|
||||||
defer containerClient.Close()
|
defer containerClient.Close()
|
||||||
|
|
||||||
|
if exists, err := containerClient.ContainerRunning(ctx, container.AllContainers()); err != nil {
|
||||||
|
return fmt.Errorf("check existing containers: %w", err)
|
||||||
|
} else if exists {
|
||||||
|
if <-ui.ShowStartupCheckModal() {
|
||||||
|
if err = containerClient.RemoveContainers(ctx, container.AllContainers()); err != nil {
|
||||||
|
return fmt.Errorf("remove existing containers: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
srv := mediaserver.StartActor(ctx, mediaserver.StartActorParams{
|
srv := mediaserver.StartActor(ctx, mediaserver.StartActorParams{
|
||||||
ContainerClient: containerClient,
|
ContainerClient: containerClient,
|
||||||
Logger: logger.With("component", "mediaserver"),
|
Logger: logger.With("component", "mediaserver"),
|
||||||
|
@ -344,7 +344,7 @@ func (a *Client) Close() error {
|
|||||||
ctx, cancel := context.WithTimeout(context.Background(), stopTimeout)
|
ctx, cancel := context.WithTimeout(context.Background(), stopTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
containerList, err := a.containersMatchingLabels(ctx, nil)
|
containerList, err := a.containersMatchingLabels(ctx, a.instanceLabels())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("container list: %w", err)
|
return fmt.Errorf("container list: %w", err)
|
||||||
}
|
}
|
||||||
@ -382,8 +382,8 @@ func (a *Client) removeContainer(ctx context.Context, id string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ContainerRunning checks if a container with the given labels is running.
|
// ContainerRunning checks if a container with the given labels is running.
|
||||||
func (a *Client) ContainerRunning(ctx context.Context, labels map[string]string) (bool, error) {
|
func (a *Client) ContainerRunning(ctx context.Context, labelOptions LabelOptions) (bool, error) {
|
||||||
containers, err := a.containersMatchingLabels(ctx, labels)
|
containers, err := a.containersMatchingLabels(ctx, labelOptions())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("container list: %w", err)
|
return false, fmt.Errorf("container list: %w", err)
|
||||||
}
|
}
|
||||||
@ -398,8 +398,8 @@ func (a *Client) ContainerRunning(ctx context.Context, labels map[string]string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RemoveContainers removes all containers with the given labels.
|
// RemoveContainers removes all containers with the given labels.
|
||||||
func (a *Client) RemoveContainers(ctx context.Context, labels map[string]string) error {
|
func (a *Client) RemoveContainers(ctx context.Context, labelOptions LabelOptions) error {
|
||||||
containers, err := a.containersMatchingLabels(ctx, labels)
|
containers, err := a.containersMatchingLabels(ctx, labelOptions())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("container list: %w", err)
|
return fmt.Errorf("container list: %w", err)
|
||||||
}
|
}
|
||||||
@ -413,11 +413,27 @@ func (a *Client) RemoveContainers(ctx context.Context, labels map[string]string)
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LabelOptions is a function that returns a map of labels.
|
||||||
|
type LabelOptions func() map[string]string
|
||||||
|
|
||||||
|
// ContainersWithLabels returns a LabelOptions function that returns the labels for
|
||||||
|
// this app instance.
|
||||||
|
func (a *Client) ContainersWithLabels(extraLabels map[string]string) LabelOptions {
|
||||||
|
return func() map[string]string {
|
||||||
|
return a.instanceLabels(extraLabels)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllContainers returns a LabelOptions function that returns the labels for any
|
||||||
|
// app instance.
|
||||||
|
func AllContainers() LabelOptions {
|
||||||
|
return func() map[string]string {
|
||||||
|
return map[string]string{"app": domain.AppName}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a *Client) containersMatchingLabels(ctx context.Context, labels map[string]string) ([]container.Summary, error) {
|
func (a *Client) containersMatchingLabels(ctx context.Context, labels map[string]string) ([]container.Summary, error) {
|
||||||
filterArgs := filters.NewArgs(
|
filterArgs := filters.NewArgs()
|
||||||
filters.Arg("label", "app="+domain.AppName),
|
|
||||||
filters.Arg("label", "app-id="+a.id.String()),
|
|
||||||
)
|
|
||||||
for k, v := range labels {
|
for k, v := range labels {
|
||||||
filterArgs.Add("label", k+"="+v)
|
filterArgs.Add("label", k+"="+v)
|
||||||
}
|
}
|
||||||
@ -427,6 +443,21 @@ func (a *Client) containersMatchingLabels(ctx context.Context, labels map[string
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *Client) instanceLabels(extraLabels ...map[string]string) map[string]string {
|
||||||
|
labels := map[string]string{
|
||||||
|
"app": domain.AppName,
|
||||||
|
"app-id": a.id.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, el := range extraLabels {
|
||||||
|
for k, v := range el {
|
||||||
|
labels[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
func shortID(id string) string {
|
func shortID(id string) string {
|
||||||
if len(id) < 12 {
|
if len(id) < 12 {
|
||||||
return id
|
return id
|
||||||
|
@ -30,7 +30,7 @@ func TestIntegrationClientStartStop(t *testing.T) {
|
|||||||
client, err := container.NewClient(ctx, apiClient, logger)
|
client, err := container.NewClient(ctx, apiClient, logger)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
running, err := client.ContainerRunning(ctx, map[string]string{"component": component})
|
running, err := client.ContainerRunning(ctx, client.ContainersWithLabels(map[string]string{"component": component}))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, running)
|
assert.False(t, running)
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ func TestIntegrationClientStartStop(t *testing.T) {
|
|||||||
require.Eventually(
|
require.Eventually(
|
||||||
t,
|
t,
|
||||||
func() bool {
|
func() bool {
|
||||||
running, err = client.ContainerRunning(ctx, map[string]string{"component": component})
|
running, err = client.ContainerRunning(ctx, client.ContainersWithLabels(map[string]string{"component": component}))
|
||||||
return err == nil && running
|
return err == nil && running
|
||||||
},
|
},
|
||||||
5*time.Second,
|
5*time.Second,
|
||||||
@ -62,7 +62,7 @@ func TestIntegrationClientStartStop(t *testing.T) {
|
|||||||
client.Close()
|
client.Close()
|
||||||
require.NoError(t, <-errC)
|
require.NoError(t, <-errC)
|
||||||
|
|
||||||
running, err = client.ContainerRunning(ctx, map[string]string{"component": component})
|
running, err = client.ContainerRunning(ctx, client.ContainersWithLabels(map[string]string{"component": component}))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, running)
|
assert.False(t, running)
|
||||||
}
|
}
|
||||||
@ -117,7 +117,7 @@ func TestIntegrationClientRemoveContainers(t *testing.T) {
|
|||||||
require.Eventually(
|
require.Eventually(
|
||||||
t,
|
t,
|
||||||
func() bool {
|
func() bool {
|
||||||
running, _ := client.ContainerRunning(ctx, map[string]string{"group": "test1"})
|
running, _ := client.ContainerRunning(ctx, client.ContainersWithLabels(map[string]string{"group": "test1"}))
|
||||||
return running
|
return running
|
||||||
},
|
},
|
||||||
5*time.Second,
|
5*time.Second,
|
||||||
@ -128,7 +128,7 @@ func TestIntegrationClientRemoveContainers(t *testing.T) {
|
|||||||
require.Eventually(
|
require.Eventually(
|
||||||
t,
|
t,
|
||||||
func() bool {
|
func() bool {
|
||||||
running, _ := client.ContainerRunning(ctx, map[string]string{"group": "test2"})
|
running, _ := client.ContainerRunning(ctx, client.ContainersWithLabels(map[string]string{"group": "test2"}))
|
||||||
return running
|
return running
|
||||||
},
|
},
|
||||||
2*time.Second,
|
2*time.Second,
|
||||||
@ -137,7 +137,7 @@ func TestIntegrationClientRemoveContainers(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// remove group 1
|
// remove group 1
|
||||||
err = client.RemoveContainers(ctx, map[string]string{"group": "test1"})
|
err = client.RemoveContainers(ctx, client.ContainersWithLabels(map[string]string{"group": "test1"}))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// check group 1 is not running
|
// check group 1 is not running
|
||||||
@ -145,7 +145,7 @@ func TestIntegrationClientRemoveContainers(t *testing.T) {
|
|||||||
t,
|
t,
|
||||||
func() bool {
|
func() bool {
|
||||||
var running bool
|
var running bool
|
||||||
running, err = client.ContainerRunning(ctx, map[string]string{"group": "test1"})
|
running, err = client.ContainerRunning(ctx, client.ContainersWithLabels(map[string]string{"group": "test1"}))
|
||||||
return err == nil && !running
|
return err == nil && !running
|
||||||
},
|
},
|
||||||
2*time.Second,
|
2*time.Second,
|
||||||
@ -154,7 +154,7 @@ func TestIntegrationClientRemoveContainers(t *testing.T) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// check group 2 is still running
|
// check group 2 is still running
|
||||||
running, err := client.ContainerRunning(ctx, map[string]string{"group": "test2"})
|
running, err := client.ContainerRunning(ctx, client.ContainersWithLabels(map[string]string{"group": "test2"}))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.True(t, running)
|
assert.True(t, running)
|
||||||
|
|
||||||
|
@ -139,7 +139,10 @@ func (s *Actor) State() domain.Source {
|
|||||||
|
|
||||||
// Close closes the media server actor.
|
// Close closes the media server actor.
|
||||||
func (s *Actor) Close() error {
|
func (s *Actor) Close() error {
|
||||||
if err := s.containerClient.RemoveContainers(context.Background(), map[string]string{"component": componentName}); err != nil {
|
if err := s.containerClient.RemoveContainers(
|
||||||
|
context.Background(),
|
||||||
|
s.containerClient.ContainersWithLabels(map[string]string{"component": componentName}),
|
||||||
|
); err != nil {
|
||||||
return fmt.Errorf("remove containers: %w", err)
|
return fmt.Errorf("remove containers: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,6 +172,11 @@ func (s *Actor) actorLoop(containerStateC <-chan domain.Container, errC <-chan e
|
|||||||
case containerState := <-containerStateC:
|
case containerState := <-containerStateC:
|
||||||
s.state.Container = containerState
|
s.state.Container = containerState
|
||||||
|
|
||||||
|
if s.state.Container.State == "exited" {
|
||||||
|
fetchStateT.Stop()
|
||||||
|
s.handleContainerExit(nil)
|
||||||
|
}
|
||||||
|
|
||||||
sendState()
|
sendState()
|
||||||
|
|
||||||
continue
|
continue
|
||||||
@ -185,17 +193,7 @@ func (s *Actor) actorLoop(containerStateC <-chan domain.Container, errC <-chan e
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetchStateT.Stop()
|
fetchStateT.Stop()
|
||||||
|
s.handleContainerExit(err)
|
||||||
if s.state.Container.ExitCode != nil {
|
|
||||||
s.state.ExitReason = fmt.Sprintf("Server process exited with code %d.", *s.state.Container.ExitCode)
|
|
||||||
} else {
|
|
||||||
s.state.ExitReason = "Server process exited unexpectedly."
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
s.state.ExitReason += "\n\n" + err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
s.state.Live = false
|
|
||||||
|
|
||||||
sendState()
|
sendState()
|
||||||
case <-fetchStateT.C:
|
case <-fetchStateT.C:
|
||||||
@ -235,6 +233,19 @@ func (s *Actor) actorLoop(containerStateC <-chan domain.Container, errC <-chan e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
s.state.ExitReason = "Server process exited unexpectedly."
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
s.state.ExitReason += "\n\n" + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.state.Live = false
|
||||||
|
}
|
||||||
|
|
||||||
// rtmpURL returns the RTMP URL for the media server, accessible from the host.
|
// rtmpURL returns the RTMP URL for the media server, accessible from the host.
|
||||||
func (s *Actor) rtmpURL() string {
|
func (s *Actor) rtmpURL() string {
|
||||||
return fmt.Sprintf("rtmp://localhost:%d/%s", s.rtmpPort, rtmpPath)
|
return fmt.Sprintf("rtmp://localhost:%d/%s", s.rtmpPort, rtmpPath)
|
||||||
|
@ -25,7 +25,7 @@ func TestIntegrationMediaServerStartStop(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() { require.NoError(t, containerClient.Close()) })
|
t.Cleanup(func() { require.NoError(t, containerClient.Close()) })
|
||||||
|
|
||||||
running, err := containerClient.ContainerRunning(t.Context(), map[string]string{"component": component})
|
running, err := containerClient.ContainerRunning(t.Context(), containerClient.ContainersWithLabels(map[string]string{"component": component}))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, running)
|
assert.False(t, running)
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ func TestIntegrationMediaServerStartStop(t *testing.T) {
|
|||||||
require.Eventually(
|
require.Eventually(
|
||||||
t,
|
t,
|
||||||
func() bool {
|
func() bool {
|
||||||
running, err = containerClient.ContainerRunning(t.Context(), map[string]string{"component": component})
|
running, err = containerClient.ContainerRunning(t.Context(), containerClient.ContainersWithLabels(map[string]string{"component": component}))
|
||||||
return err == nil && running
|
return err == nil && running
|
||||||
},
|
},
|
||||||
time.Second*10,
|
time.Second*10,
|
||||||
@ -91,7 +91,7 @@ func TestIntegrationMediaServerStartStop(t *testing.T) {
|
|||||||
|
|
||||||
mediaServer.Close()
|
mediaServer.Close()
|
||||||
|
|
||||||
running, err = containerClient.ContainerRunning(t.Context(), map[string]string{"component": component})
|
running, err = containerClient.ContainerRunning(t.Context(), containerClient.ContainersWithLabels(map[string]string{"component": component}))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, running)
|
assert.False(t, running)
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ func TestIntegrationMultiplexer(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
t.Cleanup(func() { require.NoError(t, containerClient.Close()) })
|
t.Cleanup(func() { require.NoError(t, containerClient.Close()) })
|
||||||
|
|
||||||
running, err := containerClient.ContainerRunning(t.Context(), map[string]string{"component": component})
|
running, err := containerClient.ContainerRunning(t.Context(), containerClient.ContainersWithLabels(map[string]string{"component": component}))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, running)
|
assert.False(t, running)
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ func (a *Actor) ToggleDestination(url string) {
|
|||||||
if _, ok := a.currURLs[url]; ok {
|
if _, ok := a.currURLs[url]; ok {
|
||||||
a.logger.Info("Stopping live stream", "url", url)
|
a.logger.Info("Stopping live stream", "url", url)
|
||||||
|
|
||||||
if err := a.containerClient.RemoveContainers(a.ctx, labels); err != nil {
|
if err := a.containerClient.RemoveContainers(a.ctx, a.containerClient.ContainersWithLabels(labels)); err != nil {
|
||||||
// TODO: error handling
|
// TODO: error handling
|
||||||
a.logger.Error("Failed to stop live stream", "url", url, "err", err)
|
a.logger.Error("Failed to stop live stream", "url", url, "err", err)
|
||||||
}
|
}
|
||||||
@ -171,7 +171,10 @@ func (a *Actor) C() <-chan State {
|
|||||||
|
|
||||||
// Close closes the actor.
|
// Close closes the actor.
|
||||||
func (a *Actor) Close() error {
|
func (a *Actor) Close() error {
|
||||||
if err := a.containerClient.RemoveContainers(context.Background(), map[string]string{"component": componentName}); err != nil {
|
if err := a.containerClient.RemoveContainers(
|
||||||
|
context.Background(),
|
||||||
|
a.containerClient.ContainersWithLabels(map[string]string{"component": componentName}),
|
||||||
|
); err != nil {
|
||||||
return fmt.Errorf("remove containers: %w", err)
|
return fmt.Errorf("remove containers: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,6 +212,37 @@ func (a *Actor) actorLoop(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ShowStartupCheckModal shows a modal dialog to the user, asking if they want
|
||||||
|
// to kill a running instance of Octoplex.
|
||||||
|
//
|
||||||
|
// The method will block until the user has made a choice, after which the
|
||||||
|
// channel will receive true if the user wants to quit the other instance, or
|
||||||
|
// false to quit this instance.
|
||||||
|
func (a *Actor) ShowStartupCheckModal() <-chan bool {
|
||||||
|
done := make(chan bool)
|
||||||
|
|
||||||
|
modal := tview.NewModal()
|
||||||
|
modal.SetText("Another instance of Octoplex may already be running. Pressing continue will close that instance. Continue?").
|
||||||
|
AddButtons([]string{"Continue", "Exit"}).
|
||||||
|
SetBackgroundColor(tcell.ColorBlack).
|
||||||
|
SetTextColor(tcell.ColorWhite).
|
||||||
|
SetDoneFunc(func(buttonIndex int, _ string) {
|
||||||
|
if buttonIndex == 0 {
|
||||||
|
done <- true
|
||||||
|
a.pages.RemovePage("modal")
|
||||||
|
a.app.SetFocus(a.destView)
|
||||||
|
} else {
|
||||||
|
done <- false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
modal.SetBorderStyle(tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite))
|
||||||
|
|
||||||
|
a.pages.AddPage("modal", modal, true, true)
|
||||||
|
a.app.Draw()
|
||||||
|
|
||||||
|
return done
|
||||||
|
}
|
||||||
|
|
||||||
// SetState sets the state of the terminal user interface.
|
// SetState sets the state of the terminal user interface.
|
||||||
func (a *Actor) SetState(state domain.AppState) {
|
func (a *Actor) SetState(state domain.AppState) {
|
||||||
a.ch <- func() {
|
a.ch <- func() {
|
||||||
@ -221,7 +252,7 @@ func (a *Actor) SetState(state domain.AppState) {
|
|||||||
AddButtons([]string{"Quit"}).
|
AddButtons([]string{"Quit"}).
|
||||||
SetBackgroundColor(tcell.ColorBlack).
|
SetBackgroundColor(tcell.ColorBlack).
|
||||||
SetTextColor(tcell.ColorWhite).
|
SetTextColor(tcell.ColorWhite).
|
||||||
SetDoneFunc(func(buttonIndex int, buttonLabel string) {
|
SetDoneFunc(func(int, string) {
|
||||||
// TODO: improve app cleanup
|
// TODO: improve app cleanup
|
||||||
a.app.Stop()
|
a.app.Stop()
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user