diff --git a/internal/web/web.go b/internal/web/web.go index 6ecc972a0..b77683217 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -53,6 +53,12 @@ var distFS embed.FS var startTime = time.Now() +// cronPanicLogger adapts the package logger to cron's Printf-style logger so a +// panicking scheduled job is recovered and logged instead of crashing the panel. +type cronPanicLogger struct{} + +func (cronPanicLogger) Printf(format string, args ...any) { logger.Errorf(format, args...) } + // wrapDistFS adapts the embedded `dist/` directory so it can be mounted // as the panel's `/assets/` static route. Vite emits its bundled JS/CSS // under `dist/assets/`; serving the FS rooted at `dist/assets` makes @@ -435,7 +441,9 @@ func (s *Server) start(restartXray bool, startTgBot bool) (err error) { } service.StartTrafficWriter() - s.cron = cron.New(cron.WithLocation(loc), cron.WithSeconds()) + // cron.Recover wraps every job so a panic is logged and the scheduler keeps + // running, instead of the panic taking down the whole panel process. + s.cron = cron.New(cron.WithLocation(loc), cron.WithSeconds(), cron.WithChain(cron.Recover(cron.PrintfLogger(cronPanicLogger{})))) s.cron.Start() // Wire the inbound-runtime manager once so InboundService can route