refactor(app): add App type

This commit is contained in:
Rob Watson 2025-04-22 16:06:27 +02:00
parent 4b464c680d
commit d7f8fb49eb
4 changed files with 73 additions and 53 deletions

View File

@ -18,8 +18,19 @@ import (
"github.com/docker/docker/client" "github.com/docker/docker/client"
) )
// RunParams holds the parameters for running the application. // App is an instance of the app.
type RunParams struct { type App struct {
configService *config.Service
dockerClient container.DockerClient
screen *terminal.Screen // Screen may be nil.
clipboardAvailable bool
configFilePath string
buildInfo domain.BuildInfo
logger *slog.Logger
}
// Params holds the parameters for running the application.
type Params struct {
ConfigService *config.Service ConfigService *config.Service
DockerClient container.DockerClient DockerClient container.DockerClient
Screen *terminal.Screen // Screen may be nil. Screen *terminal.Screen // Screen may be nil.
@ -29,14 +40,25 @@ type RunParams struct {
Logger *slog.Logger Logger *slog.Logger
} }
func New(params Params) *App {
return &App{
configService: params.ConfigService,
dockerClient: params.DockerClient,
screen: params.Screen,
clipboardAvailable: params.ClipboardAvailable,
configFilePath: params.ConfigFilePath,
buildInfo: params.BuildInfo,
logger: params.Logger,
}
}
// Run starts the application, and blocks until it exits. // Run starts the application, and blocks until it exits.
func Run(ctx context.Context, params RunParams) error { func (a *App) Run(ctx context.Context) error {
logger := params.Logger eventBus := event.NewBus(a.logger.With("component", "event_bus"))
eventBus := event.NewBus(logger.With("component", "event_bus"))
// cfg is the current configuration of the application, as reflected in the // cfg is the current configuration of the application, as reflected in the
// config file. // config file.
cfg := params.ConfigService.Current() cfg := a.configService.Current()
// state is the current state of the application, as reflected in the UI. // state is the current state of the application, as reflected in the UI.
state := new(domain.AppState) state := new(domain.AppState)
@ -49,11 +71,11 @@ func Run(ctx context.Context, params RunParams) error {
ui, err := terminal.StartUI(ctx, terminal.StartParams{ ui, err := terminal.StartUI(ctx, terminal.StartParams{
EventBus: eventBus, EventBus: eventBus,
Screen: params.Screen, Screen: a.screen,
ClipboardAvailable: params.ClipboardAvailable, ClipboardAvailable: a.clipboardAvailable,
ConfigFilePath: params.ConfigFilePath, ConfigFilePath: a.configFilePath,
BuildInfo: params.BuildInfo, BuildInfo: a.buildInfo,
Logger: logger.With("component", "ui"), Logger: a.logger.With("component", "ui"),
}) })
if err != nil { if err != nil {
return fmt.Errorf("start terminal user interface: %w", err) return fmt.Errorf("start terminal user interface: %w", err)
@ -71,7 +93,7 @@ func Run(ctx context.Context, params RunParams) error {
// non-test code is pretty low. // non-test code is pretty low.
emptyUI := func() { ui.SetState(domain.AppState{}) } emptyUI := func() { ui.SetState(domain.AppState{}) }
containerClient, err := container.NewClient(ctx, params.DockerClient, logger.With("component", "container_client")) containerClient, err := container.NewClient(ctx, a.dockerClient, a.logger.With("component", "container_client"))
if err != nil { if err != nil {
err = fmt.Errorf("create container client: %w", err) err = fmt.Errorf("create container client: %w", err)
@ -106,7 +128,7 @@ func Run(ctx context.Context, params RunParams) error {
TLSKeyPath: tlsKeyPath, TLSKeyPath: tlsKeyPath,
StreamKey: mediaserver.StreamKey(cfg.Sources.MediaServer.StreamKey), StreamKey: mediaserver.StreamKey(cfg.Sources.MediaServer.StreamKey),
ContainerClient: containerClient, ContainerClient: containerClient,
Logger: logger.With("component", "mediaserver"), Logger: a.logger.With("component", "mediaserver"),
}) })
if err != nil { if err != nil {
err = fmt.Errorf("create mediaserver: %w", err) err = fmt.Errorf("create mediaserver: %w", err)
@ -120,7 +142,7 @@ func Run(ctx context.Context, params RunParams) error {
repl := replicator.StartActor(ctx, replicator.StartActorParams{ repl := replicator.StartActor(ctx, replicator.StartActorParams{
SourceURL: srv.RTMPInternalURL(), SourceURL: srv.RTMPInternalURL(),
ContainerClient: containerClient, ContainerClient: containerClient,
Logger: logger.With("component", "replicator"), Logger: a.logger.With("component", "replicator"),
}) })
defer repl.Close() defer repl.Close()
@ -146,16 +168,16 @@ func Run(ctx context.Context, params RunParams) error {
eventBus.Send(event.MediaServerStartedEvent{RTMPURL: srv.RTMPURL(), RTMPSURL: srv.RTMPSURL()}) eventBus.Send(event.MediaServerStartedEvent{RTMPURL: srv.RTMPURL(), RTMPSURL: srv.RTMPSURL()})
} }
case <-params.ConfigService.C(): case <-a.configService.C():
// No-op, config updates are handled synchronously for now. // No-op, config updates are handled synchronously for now.
case cmd, ok := <-ui.C(): case cmd, ok := <-ui.C():
if !ok { if !ok {
// TODO: keep UI open until all containers have closed // TODO: keep UI open until all containers have closed
logger.Info("UI closed") a.logger.Info("UI closed")
return nil return nil
} }
logger.Debug("Command received", "cmd", cmd.Name()) a.logger.Debug("Command received", "cmd", cmd.Name())
switch c := cmd.(type) { switch c := cmd.(type) {
case domain.CommandAddDestination: case domain.CommandAddDestination:
newCfg := cfg newCfg := cfg
@ -163,8 +185,8 @@ func Run(ctx context.Context, params RunParams) error {
Name: c.DestinationName, Name: c.DestinationName,
URL: c.URL, URL: c.URL,
}) })
if err := params.ConfigService.SetConfig(newCfg); err != nil { if err := a.configService.SetConfig(newCfg); err != nil {
logger.Error("Config update failed", "err", err) a.logger.Error("Config update failed", "err", err)
ui.ConfigUpdateFailed(err) ui.ConfigUpdateFailed(err)
continue continue
} }
@ -177,8 +199,8 @@ func Run(ctx context.Context, params RunParams) error {
newCfg.Destinations = slices.DeleteFunc(newCfg.Destinations, func(dest config.Destination) bool { newCfg.Destinations = slices.DeleteFunc(newCfg.Destinations, func(dest config.Destination) bool {
return dest.URL == c.URL return dest.URL == c.URL
}) })
if err := params.ConfigService.SetConfig(newCfg); err != nil { if err := a.configService.SetConfig(newCfg); err != nil {
logger.Error("Config update failed", "err", err) a.logger.Error("Config update failed", "err", err)
ui.ConfigUpdateFailed(err) ui.ConfigUpdateFailed(err)
continue continue
} }
@ -200,11 +222,11 @@ func Run(ctx context.Context, params RunParams) error {
case <-uiUpdateT.C: case <-uiUpdateT.C:
updateUI() updateUI()
case serverState := <-srv.C(): case serverState := <-srv.C():
logger.Debug("Server state received", "state", serverState) a.logger.Debug("Server state received", "state", serverState)
applyServerState(serverState, state) applyServerState(serverState, state)
updateUI() updateUI()
case replState := <-repl.C(): case replState := <-repl.C():
logger.Debug("Replicator state received", "state", replState) a.logger.Debug("Replicator state received", "state", replState)
destErrors := applyReplicatorState(replState, state) destErrors := applyReplicatorState(replState, state)
for _, destError := range destErrors { for _, destError := range destErrors {

View File

@ -31,10 +31,10 @@ func buildAppParams(
screen tcell.SimulationScreen, screen tcell.SimulationScreen,
screenCaptureC chan<- terminal.ScreenCapture, screenCaptureC chan<- terminal.ScreenCapture,
logger *slog.Logger, logger *slog.Logger,
) app.RunParams { ) app.Params {
t.Helper() t.Helper()
return app.RunParams{ return app.Params{
ConfigService: configService, ConfigService: configService,
DockerClient: dockerClient, DockerClient: dockerClient,
Screen: &terminal.Screen{ Screen: &terminal.Screen{

View File

@ -132,7 +132,7 @@ func testIntegration(t *testing.T, mediaServerConfig config.MediaServerSource) {
done <- struct{}{} done <- struct{}{}
}() }()
err := app.Run(ctx, app.RunParams{ require.NoError(t, app.New(app.Params{
ConfigService: configService, ConfigService: configService,
DockerClient: dockerClient, DockerClient: dockerClient,
Screen: &terminal.Screen{ Screen: &terminal.Screen{
@ -144,8 +144,7 @@ func testIntegration(t *testing.T, mediaServerConfig config.MediaServerSource) {
ClipboardAvailable: false, ClipboardAvailable: false,
BuildInfo: domain.BuildInfo{Version: "0.0.1", GoVersion: "go1.16.3"}, BuildInfo: domain.BuildInfo{Version: "0.0.1", GoVersion: "go1.16.3"},
Logger: logger, Logger: logger,
}) }).Run(ctx))
require.NoError(t, err)
}() }()
require.EventuallyWithT( require.EventuallyWithT(
@ -320,7 +319,7 @@ func TestIntegrationCustomHost(t *testing.T) {
done <- struct{}{} done <- struct{}{}
}() }()
require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger))) require.NoError(t, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}() }()
time.Sleep(time.Second) time.Sleep(time.Second)
@ -391,7 +390,7 @@ func TestIntegrationCustomTLSCerts(t *testing.T) {
done <- struct{}{} done <- struct{}{}
}() }()
require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger))) require.NoError(t, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}() }()
require.EventuallyWithT( require.EventuallyWithT(
@ -472,7 +471,7 @@ func TestIntegrationRestartDestination(t *testing.T) {
done <- struct{}{} done <- struct{}{}
}() }()
require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger))) require.NoError(t, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}() }()
require.EventuallyWithT( require.EventuallyWithT(
@ -609,7 +608,7 @@ func TestIntegrationStartDestinationFailed(t *testing.T) {
done <- struct{}{} done <- struct{}{}
}() }()
require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger))) require.NoError(t, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}() }()
require.EventuallyWithT( require.EventuallyWithT(
@ -682,7 +681,7 @@ func TestIntegrationDestinationValidations(t *testing.T) {
done <- struct{}{} done <- struct{}{}
}() }()
require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger))) require.NoError(t, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}() }()
require.EventuallyWithT( require.EventuallyWithT(
@ -824,7 +823,7 @@ func TestIntegrationStartupCheck(t *testing.T) {
done <- struct{}{} done <- struct{}{}
}() }()
require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger))) require.NoError(t, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}() }()
require.EventuallyWithT( require.EventuallyWithT(
@ -893,7 +892,7 @@ func TestIntegrationMediaServerError(t *testing.T) {
done <- struct{}{} done <- struct{}{}
}() }()
require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger))) require.NoError(t, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}() }()
require.EventuallyWithT( require.EventuallyWithT(
@ -934,7 +933,7 @@ func TestIntegrationDockerClientError(t *testing.T) {
require.EqualError( require.EqualError(
t, t,
app.Run(ctx, buildAppParams(t, configService, &dockerClient, screen, screenCaptureC, logger)), app.New(buildAppParams(t, configService, &dockerClient, screen, screenCaptureC, logger)).Run(ctx),
"create container client: network create: boom", "create container client: network create: boom",
) )
}() }()
@ -974,7 +973,7 @@ func TestIntegrationDockerConnectionError(t *testing.T) {
done <- struct{}{} done <- struct{}{}
}() }()
err := app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)) err := app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx)
require.ErrorContains(t, err, "dial tcp: lookup docker.example.com") require.ErrorContains(t, err, "dial tcp: lookup docker.example.com")
require.ErrorContains(t, err, "no such host") require.ErrorContains(t, err, "no such host")
}() }()
@ -1070,7 +1069,7 @@ func TestIntegrationCopyURLs(t *testing.T) {
done <- struct{}{} done <- struct{}{}
}() }()
require.NoError(t, app.Run(ctx, buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger))) require.NoError(t, app.New(buildAppParams(t, configService, dockerClient, screen, screenCaptureC, logger)).Run(ctx))
}() }()
time.Sleep(3 * time.Second) time.Sleep(3 * time.Second)

View File

@ -97,9 +97,7 @@ func run(ctx context.Context) error {
return fmt.Errorf("read build info: %w", err) return fmt.Errorf("read build info: %w", err)
} }
return app.Run( app := app.New(app.Params{
ctx,
app.RunParams{
ConfigService: configService, ConfigService: configService,
DockerClient: dockerClient, DockerClient: dockerClient,
ClipboardAvailable: clipboardAvailable, ClipboardAvailable: clipboardAvailable,
@ -111,8 +109,9 @@ func run(ctx context.Context) error {
Date: date, Date: date,
}, },
Logger: logger, Logger: logger,
}, })
)
return app.Run(ctx)
} }
// editConfigFile opens the config file in the user's editor. // editConfigFile opens the config file in the user's editor.