fixup! wip: refactor: API

This commit is contained in:
Rob Watson 2025-05-12 19:30:28 +02:00
parent 7a3f1335c1
commit eaccb17f03
3 changed files with 52 additions and 294 deletions

View File

@ -1,69 +0,0 @@
package main
import (
"context"
"fmt"
"log/slog"
"os"
"runtime/debug"
"git.netflux.io/rob/octoplex/internal/client"
"git.netflux.io/rob/octoplex/internal/domain"
"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")
os.Exit(1)
}
}
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))
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)
}
app := client.New(client.NewParams{
ClipboardAvailable: clipboardAvailable,
BuildInfo: domain.BuildInfo{
GoVersion: buildInfo.GoVersion,
Version: version,
Commit: commit,
Date: date,
},
Logger: logger,
})
if err := app.Run(ctx); err != nil {
return fmt.Errorf("run app: %w", err)
}
return nil
}

View File

@ -1,194 +0,0 @@
package main
import (
"cmp"
"context"
"errors"
"flag"
"fmt"
"log/slog"
"os"
"os/exec"
"os/signal"
"runtime"
"syscall"
"git.netflux.io/rob/octoplex/internal/config"
"git.netflux.io/rob/octoplex/internal/domain"
"git.netflux.io/rob/octoplex/internal/server"
dockerclient "github.com/docker/docker/client"
)
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
)
var errShutdown = errors.New("shutdown")
func main() {
var exitStatus int
if err := run(); errors.Is(err, errShutdown) {
exitStatus = 130
} else if err != nil {
exitStatus = 1
_, _ = os.Stderr.WriteString("Error: " + err.Error() + "\n")
}
os.Exit(exitStatus)
}
func run() error {
ctx, cancel := context.WithCancelCause(context.Background())
defer cancel(nil)
configService, err := config.NewDefaultService()
if err != nil {
return fmt.Errorf("build config service: %w", err)
}
help := flag.Bool("h", false, "Show help")
flag.Parse()
if *help {
printUsage()
return nil
}
if narg := flag.NArg(); narg > 1 {
printUsage()
return fmt.Errorf("too many arguments")
} else if narg == 1 {
switch flag.Arg(0) {
case "edit-config":
return editConfigFile(configService)
case "print-config":
return printConfigPath(configService.Path())
case "version":
return printVersion()
case "help":
printUsage()
return nil
}
}
cfg, err := configService.ReadOrCreateConfig()
if err != nil {
return fmt.Errorf("read or create config: %w", err)
}
logger, err := buildLogger(cfg.LogFile)
if err != nil {
return fmt.Errorf("build logger: %w", err)
}
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)
}()
dockerClient, err := dockerclient.NewClientWithOpts(
dockerclient.FromEnv,
dockerclient.WithAPIVersionNegotiation(),
)
if err != nil {
return fmt.Errorf("new docker client: %w", err)
}
app := server.New(server.Params{
ConfigService: configService,
DockerClient: dockerClient,
ConfigFilePath: configService.Path(),
Logger: logger,
})
logger.Info(
"Starting application",
"version",
cmp.Or(version, "devel"),
"commit",
cmp.Or(commit, "unknown"),
"date",
cmp.Or(date, "unknown"),
"go_version",
runtime.Version(),
)
if err := app.Run(ctx); err != nil {
if errors.Is(err, context.Canceled) && context.Cause(ctx) == errShutdown {
return errShutdown
}
return err
}
return nil
}
// editConfigFile opens the config file in the user's editor.
func editConfigFile(configService *config.Service) error {
if _, err := configService.ReadOrCreateConfig(); err != nil {
return fmt.Errorf("read or create config: %w", err)
}
editor := os.Getenv("EDITOR")
if editor == "" {
editor = "vi"
}
binary, err := exec.LookPath(editor)
if err != nil {
return fmt.Errorf("look path: %w", err)
}
fmt.Fprintf(os.Stderr, "Editing config file: %s\n", configService.Path())
fmt.Println(binary)
if err := syscall.Exec(binary, []string{"--", configService.Path()}, os.Environ()); err != nil {
return fmt.Errorf("exec: %w", err)
}
return nil
}
// printConfigPath prints the path to the config file to stderr.
func printConfigPath(configPath string) error {
fmt.Fprintln(os.Stderr, configPath)
return nil
}
// printVersion prints the version of the application to stderr.
func printVersion() error {
fmt.Fprintf(os.Stderr, "%s version %s\n", domain.AppName, cmp.Or(version, "0.0.0-dev"))
return nil
}
func printUsage() {
os.Stderr.WriteString("Usage: octoplex [command]\n\n")
os.Stderr.WriteString("Commands:\n\n")
os.Stderr.WriteString(" edit-config Edit the config file\n")
os.Stderr.WriteString(" print-config Print the path to the config file\n")
os.Stderr.WriteString(" version Print the version of the application\n")
os.Stderr.WriteString(" help Print this help message\n")
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")
}
// buildLogger builds the logger, which may be a no-op logger.
func buildLogger(cfg config.LogFile) (*slog.Logger, error) {
var handlerOpts slog.HandlerOptions
if os.Getenv("OCTO_DEBUG") != "" {
handlerOpts.Level = slog.LevelDebug
}
return slog.New(slog.NewTextHandler(os.Stderr, &handlerOpts)), nil
}

69
main.go
View File

@ -32,18 +32,17 @@ var (
date string
)
var errShutdown = errors.New("shutdown")
type errInterrupt struct{}
func (e errInterrupt) Error() string {
return "interrupt signal received"
}
func (e errInterrupt) ExitCode() int {
return 130
}
func main() {
// when server is running:
// server log goes to wherever config defined it
// client log does not exist
// when client is running:
// server log does not exist
// client log goes to ??? but for now octoplex.log
// when both are running:
// server log goes to wherever config defined it
// client logs goes to ??? but for now octoplex.log
app := &cli.App{
Name: "Octoplex",
Usage: "Octoplex is a live video restreamer for Docker.",
@ -53,7 +52,7 @@ func main() {
Usage: "Run the client",
Flags: []cli.Flag{ /* client flags */ },
Action: func(c *cli.Context) error {
return runClient(c)
return runClient(c.Context, c)
},
},
{
@ -61,7 +60,7 @@ func main() {
Usage: "Run the server",
Flags: []cli.Flag{ /* server flags */ },
Action: func(c *cli.Context) error {
return runServer(c, true)
return runServer(c.Context, c, serverConfig{stderrAvailable: true, handleSigInt: true})
},
},
{
@ -81,8 +80,8 @@ func main() {
}
}
func runClient(_ *cli.Context) error {
ctx, cancel := context.WithCancel(context.Background())
func runClient(ctx context.Context, _ *cli.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// TODO: logger from config
@ -91,6 +90,7 @@ func runClient(_ *cli.Context) error {
return fmt.Errorf("open log file: %w", err)
}
logger := slog.New(slog.NewTextHandler(fptr, nil))
logger.Info("Starting client", "version", cmp.Or(version, "devel"), "commit", cmp.Or(commit, "unknown"), "date", cmp.Or(date, "unknown"), "go_version", runtime.Version())
var clipboardAvailable bool
if err = clipboard.Init(); err != nil {
@ -121,8 +121,13 @@ func runClient(_ *cli.Context) error {
return nil
}
func runServer(_ *cli.Context, stderrAvailable bool) error {
ctx, cancel := context.WithCancelCause(context.Background())
type serverConfig struct {
stderrAvailable bool
handleSigInt bool
}
func runServer(ctx context.Context, _ *cli.Context, serverCfg serverConfig) error {
ctx, cancel := context.WithCancelCause(ctx)
defer cancel(nil)
configService, err := config.NewDefaultService()
@ -141,7 +146,7 @@ func runServer(_ *cli.Context, stderrAvailable bool) error {
// fallback to the legacy configuration but this should be bought more
// in-line with the client/server split.
var w io.Writer
if stderrAvailable {
if serverCfg.stderrAvailable {
w = os.Stdout
} else if !cfg.LogFile.Enabled {
w = io.Discard
@ -158,6 +163,7 @@ func runServer(_ *cli.Context, stderrAvailable bool) error {
}
logger := slog.New(slog.NewTextHandler(w, &handlerOpts))
if serverCfg.handleSigInt {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
@ -165,8 +171,9 @@ func runServer(_ *cli.Context, stderrAvailable bool) error {
<-ch
logger.Info("Received interrupt signal, exiting")
signal.Stop(ch)
cancel(errShutdown)
cancel(errInterrupt{})
}()
}
dockerClient, err := dockerclient.NewClientWithOpts(
dockerclient.FromEnv,
@ -196,8 +203,8 @@ func runServer(_ *cli.Context, stderrAvailable bool) error {
)
if err := app.Run(ctx); err != nil {
if errors.Is(err, context.Canceled) && context.Cause(ctx) == errShutdown {
return errShutdown
if errors.Is(err, context.Canceled) && errors.Is(context.Cause(ctx), errInterrupt{}) {
return context.Cause(ctx)
}
return err
}
@ -206,15 +213,29 @@ func runServer(_ *cli.Context, stderrAvailable bool) error {
}
func runClientAndServer(c *cli.Context) error {
g, _ := errgroup.WithContext(c.Context)
errNoErr := errors.New("no error")
g, ctx := errgroup.WithContext(c.Context)
g.Go(func() error {
return runClient(c)
if err := runClient(ctx, c); err != nil {
return err
}
return errNoErr
})
g.Go(func() error {
return runServer(c, false)
if err := runServer(ctx, c, serverConfig{stderrAvailable: false, handleSigInt: false}); err != nil {
return err
}
return errNoErr
})
return g.Wait()
if err := g.Wait(); err == errNoErr {
return nil
} else {
return err
}
}