diff --git a/cmd/client/main.go b/cmd/client/main.go new file mode 100644 index 0000000..5d04a6f --- /dev/null +++ b/cmd/client/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + "os" + "runtime/debug" + + "git.netflux.io/rob/octoplex/internal/domain" + "git.netflux.io/rob/octoplex/internal/event" + "git.netflux.io/rob/octoplex/internal/terminal" + "golang.design/x/clipboard" +) + +var ( + // version is the version of the application. + version string + // commit is the commit hash of the application. + commit string + // date is the date of the build. + date string +) + +func main() { + if err := run(); err != nil { + os.Stderr.WriteString("Error: " + err.Error() + "\n") + } +} + +func run() error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // TODO: logger from config + fptr, err := os.OpenFile("octoplex.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + return fmt.Errorf("open log file: %w", err) + } + logger := slog.New(slog.NewTextHandler(fptr, nil)) + + bus := event.NewBus(logger) + + var clipboardAvailable bool + if err = clipboard.Init(); err != nil { + logger.Warn("Clipboard not available", "err", err) + } else { + clipboardAvailable = true + } + + buildInfo, ok := debug.ReadBuildInfo() + if !ok { + return fmt.Errorf("read build info: %w", err) + } + + ui, err := terminal.StartUI(ctx, terminal.StartParams{ + EventBus: bus, + Dispatcher: func(cmd event.Command) { + // TODO: this must call the gRPC client + logger.Info("Command dispatched", "cmd", cmd) + }, + ClipboardAvailable: clipboardAvailable, + ConfigFilePath: "TODO", + BuildInfo: domain.BuildInfo{ + GoVersion: buildInfo.GoVersion, + Version: version, + Commit: commit, + Date: date, + }, + Logger: logger.With("component", "ui"), + }) + if err != nil { + return fmt.Errorf("start terminal user interface: %w", err) + } + defer ui.Close() + + return nil +} diff --git a/cmd/server/main.go b/cmd/server/main.go index 4573104..15b2ab2 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -6,7 +6,6 @@ import ( "errors" "flag" "fmt" - "io" "log/slog" "os" "os/exec" @@ -18,7 +17,6 @@ import ( "git.netflux.io/rob/octoplex/internal/config" "git.netflux.io/rob/octoplex/internal/domain" dockerclient "github.com/docker/docker/client" - "golang.design/x/clipboard" ) var ( @@ -85,31 +83,21 @@ func run() error { return fmt.Errorf("read or create config: %w", err) } - headless := os.Getenv("OCTO_HEADLESS") != "" - logger, err := buildLogger(cfg.LogFile, headless) + logger, err := buildLogger(cfg.LogFile) if err != nil { return fmt.Errorf("build logger: %w", err) } - if headless { - // When running in headless mode tview doesn't handle SIGINT for us. - ch := make(chan os.Signal, 1) - signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + // When running in headless mode tview doesn't handle SIGINT for us. + ch := make(chan os.Signal, 1) + signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) - go func() { - <-ch - logger.Info("Received interrupt signal, exiting") - signal.Stop(ch) - cancel(errShutdown) - }() - } - - var clipboardAvailable bool - if err = clipboard.Init(); err != nil { - logger.Warn("Clipboard not available", "err", err) - } else { - clipboardAvailable = true - } + go func() { + <-ch + logger.Info("Received interrupt signal, exiting") + signal.Stop(ch) + cancel(errShutdown) + }() dockerClient, err := dockerclient.NewClientWithOpts( dockerclient.FromEnv, @@ -125,11 +113,9 @@ func run() error { } app := app.New(app.Params{ - ConfigService: configService, - DockerClient: dockerClient, - Headless: headless, - ClipboardAvailable: clipboardAvailable, - ConfigFilePath: configService.Path(), + ConfigService: configService, + DockerClient: dockerClient, + ConfigFilePath: configService.Path(), BuildInfo: domain.BuildInfo{ GoVersion: buildInfo.GoVersion, Version: version, @@ -189,32 +175,13 @@ func printUsage() { os.Stderr.WriteString("\n") os.Stderr.WriteString("Additionally, Octoplex can be configured with the following environment variables:\n\n") os.Stderr.WriteString(" OCTO_DEBUG Enables debug logging if set\n") - os.Stderr.WriteString(" OCTO_HEADLESS Enables headless mode if set (experimental)\n\n") } // buildLogger builds the logger, which may be a no-op logger. -func buildLogger(cfg config.LogFile, headless bool) (*slog.Logger, error) { - build := func(w io.Writer) *slog.Logger { - var handlerOpts slog.HandlerOptions - if os.Getenv("OCTO_DEBUG") != "" { - handlerOpts.Level = slog.LevelDebug - } - return slog.New(slog.NewTextHandler(w, &handlerOpts)) +func buildLogger(cfg config.LogFile) (*slog.Logger, error) { + var handlerOpts slog.HandlerOptions + if os.Getenv("OCTO_DEBUG") != "" { + handlerOpts.Level = slog.LevelDebug } - - // In headless mode, always log to stderr. - if headless { - return build(os.Stderr), nil - } - - if !cfg.Enabled { - return slog.New(slog.DiscardHandler), nil - } - - fptr, err := os.OpenFile(cfg.GetPath(), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) - if err != nil { - return nil, fmt.Errorf("error opening log file: %w", err) - } - - return build(fptr), nil + return slog.New(slog.NewTextHandler(os.Stderr, &handlerOpts)), nil } diff --git a/internal/app/app.go b/internal/app/app.go index 779875c..27f07fb 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -32,7 +32,6 @@ type App struct { dispatchC chan event.Command dockerClient container.DockerClient screen *terminal.Screen // Screen may be nil. - headless bool clipboardAvailable bool configFilePath string buildInfo domain.BuildInfo @@ -45,7 +44,6 @@ type Params struct { DockerClient container.DockerClient ChanSize int Screen *terminal.Screen // Screen may be nil. - Headless bool ClipboardAvailable bool ConfigFilePath string BuildInfo domain.BuildInfo @@ -64,7 +62,6 @@ func New(params Params) *App { dispatchC: make(chan event.Command, cmp.Or(params.ChanSize, defaultChanSize)), dockerClient: params.DockerClient, screen: params.Screen, - headless: params.Headless, clipboardAvailable: params.ClipboardAvailable, configFilePath: params.ConfigFilePath, buildInfo: params.BuildInfo, @@ -83,46 +80,10 @@ func (a *App) Run(ctx context.Context) error { return errors.New("config: either sources.mediaServer.rtmp.enabled or sources.mediaServer.rtmps.enabled must be set") } - if !a.headless { - ui, err := terminal.StartUI(ctx, terminal.StartParams{ - EventBus: a.eventBus, - Dispatcher: func(cmd event.Command) { a.dispatchC <- cmd }, - Screen: a.screen, - ClipboardAvailable: a.clipboardAvailable, - ConfigFilePath: a.configFilePath, - BuildInfo: a.buildInfo, - Logger: a.logger.With("component", "ui"), - }) - if err != nil { - return fmt.Errorf("start terminal user interface: %w", err) - } - defer ui.Close() - } - - // emptyUI is a dummy function that sets the UI state to an empty state, and - // re-renders the screen. - // - // This is a workaround for a weird interaction between tview and - // tcell.SimulationScreen which leads to newly-added pages not rendering if - // the UI is not re-rendered for a second time. - // It is only needed for integration tests when rendering modals before the - // main loop starts. It would be nice to remove this but the risk/impact on - // non-test code is pretty low. - emptyUI := func() { - a.eventBus.Send(event.AppStateChangedEvent{State: domain.AppState{}}) - } - // doFatalError publishes a fatal error to the event bus, waiting for the // user to acknowledge it if not in headless mode. doFatalError := func(msg string) { a.eventBus.Send(event.FatalErrorOccurredEvent{Message: msg}) - - if a.headless { - return - } - - emptyUI() - <-a.dispatchC } const grpcAddr = ":50051" @@ -195,15 +156,11 @@ func (a *App) Run(ctx context.Context) error { defer uiUpdateT.Stop() startMediaServerC := make(chan struct{}, 1) - if a.headless { // disable startup check in headless mode for now + if ok, startupErr := doStartupCheck(ctx, containerClient, a.eventBus); startupErr != nil { + doFatalError(startupErr.Error()) + return startupErr + } else if ok { startMediaServerC <- struct{}{} - } else { - if ok, startupErr := doStartupCheck(ctx, containerClient, a.eventBus); startupErr != nil { - doFatalError(startupErr.Error()) - return startupErr - } else if ok { - startMediaServerC <- struct{}{} - } } for {