fix(server): exit with an error code when the server fails to start (#6236)

When a startup step failed (for example, the port was already in use), runNavidrome only logged the error and returned. In service mode, service.Run() kept waiting for a stop signal, so the process stayed up serving nothing and the service manager never restarted it. A plain run exited with code 0.

runNavidrome now returns the error, unless its context was cancelled by a normal shutdown. Both the plain run and the service goroutine exit with code 1 on that error. The systemd unit no longer lists 1, 2 and 8 in SuccessExitStatus, so Restart=on-failure restarts the service on exit code 1.

Fixes #6235
This commit is contained in:
Deluan Quintão 2026-09-27 14:18:24 -04:00 • committed by GitHub
commit ce484083bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 19 additions and 10 deletions

View file

@ -44,7 +44,9 @@ Complete documentation is available at https://www.navidrome.org/docs`,
preRun()
},
Run: func(cmd *cobra.Command, args []string) {
runNavidrome(cmd.Context())
if err := runNavidrome(cmd.Context()); err != nil {
log.Fatal("Fatal error in Navidrome. Aborting", err)
}
},
PostRun: func(cmd *cobra.Command, args []string) {
postRun()
@ -76,12 +78,12 @@ func postRun() {
}
// runNavidrome is the main entry point for the Navidrome server. It starts all the services and blocks.
// If any of the services returns an error, it will log it and exit. If the process receives a signal to exit,
// it will cancel the context and exit gracefully.
func runNavidrome(ctx context.Context) {
defer db.Init(ctx)()
// If any of the services returns an error, it stops the others and returns that error, so the caller can
// exit with a non-zero code. If the context is cancelled (a signal or a service stop), it returns nil.
func runNavidrome(parentCtx context.Context) error {
defer db.Init(parentCtx)()
g, ctx := errgroup.WithContext(ctx)
g, ctx := errgroup.WithContext(parentCtx)
g.Go(startServer(ctx))
g.Go(startSignaller(ctx))
g.Go(startScheduler(ctx))
@ -102,9 +104,11 @@ func runNavidrome(ctx context.Context) {
log.Warn(ctx, "Automatic Scanning is DISABLED")
}
if err := g.Wait(); err != nil {
log.Error("Fatal error in Navidrome. Aborting", err)
// Errors caused by a normal shutdown are not failures
if err := g.Wait(); err != nil && parentCtx.Err() == nil {
return err
}
return nil
}
// mainContext returns a context that is cancelled when the process receives a signal to exit.

View file

@ -53,8 +53,13 @@ func (p *svcControl) Start(service.Service) error {
p.done = make(chan struct{})
p.ctx, p.cancel = context.WithCancel(context.Background())
go func() {
runNavidrome(p.ctx)
err := runNavidrome(p.ctx)
close(p.done)
// service.Run() only returns when it gets a stop request, so exit here to let the
// service manager see the failure and restart the service
if err != nil {
log.Fatal("Fatal error in Navidrome. Aborting", err)
}
}()
return nil
}
@ -74,7 +79,7 @@ func (p *svcControl) Stop(service.Service) error {
var svcInstance = sync.OnceValue(func() service.Service {
options := make(service.KeyValue)
options["Restart"] = "on-failure"
options["SuccessExitStatus"] = "1 2 8 SIGKILL"
options["SuccessExitStatus"] = "SIGKILL"
options["UserService"] = false
options["LogDirectory"] = conf.Server.DataFolder.String()
options["SystemdScript"] = systemdScript