diff --git a/internal/server/serverapp.go b/internal/server/serverapp.go index 5343dbd..044585e 100644 --- a/internal/server/serverapp.go +++ b/internal/server/serverapp.go @@ -45,6 +45,10 @@ type Params struct { // defaultChanSize is the default size of the dispatch channel. const defaultChanSize = 64 +// ErrOtherInstanceDetected is returned when another instance of the app is +// detected on startup. +var ErrOtherInstanceDetected = errors.New("another instance is currently running") + // New creates a new application instance. func New(params Params) *App { return &App{ @@ -180,6 +184,10 @@ func (a *App) Run(ctx context.Context) error { return startupErr } else if ok { startMediaServerC <- struct{}{} + } else if internalAPI.GetClientCount() == 0 { + // startup check failed, and there are no clients connected to the API so + // probably in server-only mode. In this case, we just bail out. + return ErrOtherInstanceDetected } for { @@ -447,3 +455,14 @@ func buildNetAddr(src config.RTMPSource) mediaserver.OptionalNetAddr { return mediaserver.OptionalNetAddr{Enabled: true, NetAddr: domain.NetAddr(src.NetAddr)} } + +// Stop stops all containers and networks created by any instance of the app. +func (a *App) Stop(ctx context.Context) error { + containerClient, err := container.NewClient(ctx, a.dockerClient, a.logger.With("component", "container_client")) + if err != nil { + return fmt.Errorf("create container client: %w", err) + } + defer containerClient.Close() + + return closeOtherInstances(ctx, containerClient) +} diff --git a/main.go b/main.go index 9403798..d30d4d5 100644 --- a/main.go +++ b/main.go @@ -48,23 +48,6 @@ func (e errInterrupt) ExitCode() int { } func main() { - serverSubcommands := []*cli.Command{ - { - Name: "print-config", - Usage: "Print the config file path", - Action: func(*cli.Context) error { - return printConfig() - }, - }, - { - Name: "edit-config", - Usage: "Edit the config file", - Action: func(*cli.Context) error { - return editConfig() - }, - }, - } - app := &cli.App{ Name: "Octoplex", Usage: "Octoplex is a live video restreamer for Docker.", @@ -78,23 +61,58 @@ func main() { }, { Name: "server", - Usage: "Run the server", + Usage: "Manage the standalone server.", Action: func(c *cli.Context) error { - return runServer(c.Context, c, serverConfig{ - stderrAvailable: true, - handleSigInt: true, - waitForClient: false, - }) + return c.App.Command("server").Subcommands[0].Action(c) + }, + Subcommands: []*cli.Command{ + { + Name: "start", + Usage: "Start the server", + Description: "Start the standalone server, without a CLI client attached.", + Action: func(c *cli.Context) error { + return runServer(c.Context, c, serverConfig{ + stderrAvailable: true, + handleSigInt: true, + waitForClient: false, + }) + }, + }, + { + Name: "stop", + Usage: "Stop the server", + Description: "Stop all containers and networks created by Octoplex, and exit.", + Action: func(c *cli.Context) error { + return runServer(c.Context, c, serverConfig{ + stderrAvailable: true, + handleSigInt: false, + waitForClient: false, + }) + }, + }, + { + Name: "print-config", + Usage: "Print the config file path", + Action: func(*cli.Context) error { + return printConfig() + }, + }, + { + Name: "edit-config", + Usage: "Edit the config file", + Action: func(*cli.Context) error { + return editConfig() + }, + }, }, - Subcommands: serverSubcommands, }, { - Name: "run", - Usage: "Run server and client in the same process", + Name: "run", + Usage: "Run server and client in the same process", + Description: "Run the server and client in the same process. This is useful for testing, debugging or running for a single user.", Action: func(c *cli.Context) error { return runClientAndServer(c) }, - Subcommands: serverSubcommands, }, { Name: "version", @@ -159,7 +177,7 @@ type serverConfig struct { waitForClient bool } -func runServer(ctx context.Context, _ *cli.Context, serverCfg serverConfig) error { +func runServer(ctx context.Context, c *cli.Context, serverCfg serverConfig) error { ctx, cancel := context.WithCancelCause(ctx) defer cancel(nil) @@ -224,6 +242,10 @@ func runServer(ctx context.Context, _ *cli.Context, serverCfg serverConfig) erro Logger: logger, }) + if c.Command.Name == "stop" { + return app.Stop(ctx) + } + logger.Info( "Starting server", "version", @@ -240,6 +262,11 @@ func runServer(ctx context.Context, _ *cli.Context, serverCfg serverConfig) erro if errors.Is(err, context.Canceled) && errors.Is(context.Cause(ctx), errInterrupt{}) { return context.Cause(ctx) } + if errors.Is(err, server.ErrOtherInstanceDetected) { + msg := "Another instance of the server may be running.\n" + + "To stop the server, run `octoplex server stop`." + return cli.Exit(msg, 1) + } return err }