fixup! wip: refactor: API
This commit is contained in:
parent
f67a456d1e
commit
0a6b9fad90
78
cmd/client/main.go
Normal file
78
cmd/client/main.go
Normal file
@ -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
|
||||||
|
}
|
@ -6,7 +6,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@ -18,7 +17,6 @@ import (
|
|||||||
"git.netflux.io/rob/octoplex/internal/config"
|
"git.netflux.io/rob/octoplex/internal/config"
|
||||||
"git.netflux.io/rob/octoplex/internal/domain"
|
"git.netflux.io/rob/octoplex/internal/domain"
|
||||||
dockerclient "github.com/docker/docker/client"
|
dockerclient "github.com/docker/docker/client"
|
||||||
"golang.design/x/clipboard"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -85,31 +83,21 @@ func run() error {
|
|||||||
return fmt.Errorf("read or create config: %w", err)
|
return fmt.Errorf("read or create config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
headless := os.Getenv("OCTO_HEADLESS") != ""
|
logger, err := buildLogger(cfg.LogFile)
|
||||||
logger, err := buildLogger(cfg.LogFile, headless)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("build logger: %w", err)
|
return fmt.Errorf("build logger: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if headless {
|
// When running in headless mode tview doesn't handle SIGINT for us.
|
||||||
// When running in headless mode tview doesn't handle SIGINT for us.
|
ch := make(chan os.Signal, 1)
|
||||||
ch := make(chan os.Signal, 1)
|
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
||||||
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
<-ch
|
<-ch
|
||||||
logger.Info("Received interrupt signal, exiting")
|
logger.Info("Received interrupt signal, exiting")
|
||||||
signal.Stop(ch)
|
signal.Stop(ch)
|
||||||
cancel(errShutdown)
|
cancel(errShutdown)
|
||||||
}()
|
}()
|
||||||
}
|
|
||||||
|
|
||||||
var clipboardAvailable bool
|
|
||||||
if err = clipboard.Init(); err != nil {
|
|
||||||
logger.Warn("Clipboard not available", "err", err)
|
|
||||||
} else {
|
|
||||||
clipboardAvailable = true
|
|
||||||
}
|
|
||||||
|
|
||||||
dockerClient, err := dockerclient.NewClientWithOpts(
|
dockerClient, err := dockerclient.NewClientWithOpts(
|
||||||
dockerclient.FromEnv,
|
dockerclient.FromEnv,
|
||||||
@ -125,11 +113,9 @@ func run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app := app.New(app.Params{
|
app := app.New(app.Params{
|
||||||
ConfigService: configService,
|
ConfigService: configService,
|
||||||
DockerClient: dockerClient,
|
DockerClient: dockerClient,
|
||||||
Headless: headless,
|
ConfigFilePath: configService.Path(),
|
||||||
ClipboardAvailable: clipboardAvailable,
|
|
||||||
ConfigFilePath: configService.Path(),
|
|
||||||
BuildInfo: domain.BuildInfo{
|
BuildInfo: domain.BuildInfo{
|
||||||
GoVersion: buildInfo.GoVersion,
|
GoVersion: buildInfo.GoVersion,
|
||||||
Version: version,
|
Version: version,
|
||||||
@ -189,32 +175,13 @@ func printUsage() {
|
|||||||
os.Stderr.WriteString("\n")
|
os.Stderr.WriteString("\n")
|
||||||
os.Stderr.WriteString("Additionally, Octoplex can be configured with the following environment variables:\n\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_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.
|
// buildLogger builds the logger, which may be a no-op logger.
|
||||||
func buildLogger(cfg config.LogFile, headless bool) (*slog.Logger, error) {
|
func buildLogger(cfg config.LogFile) (*slog.Logger, error) {
|
||||||
build := func(w io.Writer) *slog.Logger {
|
var handlerOpts slog.HandlerOptions
|
||||||
var handlerOpts slog.HandlerOptions
|
if os.Getenv("OCTO_DEBUG") != "" {
|
||||||
if os.Getenv("OCTO_DEBUG") != "" {
|
handlerOpts.Level = slog.LevelDebug
|
||||||
handlerOpts.Level = slog.LevelDebug
|
|
||||||
}
|
|
||||||
return slog.New(slog.NewTextHandler(w, &handlerOpts))
|
|
||||||
}
|
}
|
||||||
|
return slog.New(slog.NewTextHandler(os.Stderr, &handlerOpts)), nil
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,6 @@ type App struct {
|
|||||||
dispatchC chan event.Command
|
dispatchC chan event.Command
|
||||||
dockerClient container.DockerClient
|
dockerClient container.DockerClient
|
||||||
screen *terminal.Screen // Screen may be nil.
|
screen *terminal.Screen // Screen may be nil.
|
||||||
headless bool
|
|
||||||
clipboardAvailable bool
|
clipboardAvailable bool
|
||||||
configFilePath string
|
configFilePath string
|
||||||
buildInfo domain.BuildInfo
|
buildInfo domain.BuildInfo
|
||||||
@ -45,7 +44,6 @@ type Params struct {
|
|||||||
DockerClient container.DockerClient
|
DockerClient container.DockerClient
|
||||||
ChanSize int
|
ChanSize int
|
||||||
Screen *terminal.Screen // Screen may be nil.
|
Screen *terminal.Screen // Screen may be nil.
|
||||||
Headless bool
|
|
||||||
ClipboardAvailable bool
|
ClipboardAvailable bool
|
||||||
ConfigFilePath string
|
ConfigFilePath string
|
||||||
BuildInfo domain.BuildInfo
|
BuildInfo domain.BuildInfo
|
||||||
@ -64,7 +62,6 @@ func New(params Params) *App {
|
|||||||
dispatchC: make(chan event.Command, cmp.Or(params.ChanSize, defaultChanSize)),
|
dispatchC: make(chan event.Command, cmp.Or(params.ChanSize, defaultChanSize)),
|
||||||
dockerClient: params.DockerClient,
|
dockerClient: params.DockerClient,
|
||||||
screen: params.Screen,
|
screen: params.Screen,
|
||||||
headless: params.Headless,
|
|
||||||
clipboardAvailable: params.ClipboardAvailable,
|
clipboardAvailable: params.ClipboardAvailable,
|
||||||
configFilePath: params.ConfigFilePath,
|
configFilePath: params.ConfigFilePath,
|
||||||
buildInfo: params.BuildInfo,
|
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")
|
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
|
// doFatalError publishes a fatal error to the event bus, waiting for the
|
||||||
// user to acknowledge it if not in headless mode.
|
// user to acknowledge it if not in headless mode.
|
||||||
doFatalError := func(msg string) {
|
doFatalError := func(msg string) {
|
||||||
a.eventBus.Send(event.FatalErrorOccurredEvent{Message: msg})
|
a.eventBus.Send(event.FatalErrorOccurredEvent{Message: msg})
|
||||||
|
|
||||||
if a.headless {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
emptyUI()
|
|
||||||
<-a.dispatchC
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const grpcAddr = ":50051"
|
const grpcAddr = ":50051"
|
||||||
@ -195,15 +156,11 @@ func (a *App) Run(ctx context.Context) error {
|
|||||||
defer uiUpdateT.Stop()
|
defer uiUpdateT.Stop()
|
||||||
|
|
||||||
startMediaServerC := make(chan struct{}, 1)
|
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{}{}
|
startMediaServerC <- struct{}{}
|
||||||
} else {
|
|
||||||
if ok, startupErr := doStartupCheck(ctx, containerClient, a.eventBus); startupErr != nil {
|
|
||||||
doFatalError(startupErr.Error())
|
|
||||||
return startupErr
|
|
||||||
} else if ok {
|
|
||||||
startMediaServerC <- struct{}{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user