From eaccb17f03979be9ec2d13da8a5ea43c14694c0c Mon Sep 17 00:00:00 2001 From: Rob Watson Date: Mon, 12 May 2025 19:30:28 +0200 Subject: [PATCH] fixup! wip: refactor: API --- cmd/client/main.go | 69 ---------------- cmd/server/main.go | 194 --------------------------------------------- main.go | 83 +++++++++++-------- 3 files changed, 52 insertions(+), 294 deletions(-) delete mode 100644 cmd/client/main.go delete mode 100644 cmd/server/main.go diff --git a/cmd/client/main.go b/cmd/client/main.go deleted file mode 100644 index 5abc7e3..0000000 --- a/cmd/client/main.go +++ /dev/null @@ -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 -} diff --git a/cmd/server/main.go b/cmd/server/main.go deleted file mode 100644 index 630c864..0000000 --- a/cmd/server/main.go +++ /dev/null @@ -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 -} diff --git a/main.go b/main.go index 6559f83..fea522d 100644 --- a/main.go +++ b/main.go @@ -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,15 +163,17 @@ func runServer(_ *cli.Context, stderrAvailable bool) error { } logger := slog.New(slog.NewTextHandler(w, &handlerOpts)) - ch := make(chan os.Signal, 1) - signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) + if serverCfg.handleSigInt { + 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) - }() + go func() { + <-ch + logger.Info("Received interrupt signal, exiting") + signal.Stop(ch) + 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 + } }