@@ -282,7 +282,7 @@ func newDaemonCobraCmd(daemonCmd *DaemonCmd) *cobra.Command {
282
282
283
283
cobraCommand .MarkFlagsMutuallyExclusive ("dev" , flagAddr )
284
284
285
- // Note : Additional CORS validation required to check CORS flags are present alongside --cors-enable.
285
+ // NOTE : Additional CORS validation required to check CORS flags are present alongside --cors-enable.
286
286
cobraCommand .MarkFlagsRequiredTogether (flagCORSEnable , flagCORSOrigin )
287
287
288
288
return cobraCommand
@@ -315,10 +315,16 @@ func (c *DaemonCmd) run(cmd *cobra.Command, _ []string) error {
315
315
return err
316
316
}
317
317
318
+ // Load the new configuration.
319
+ cfg , err := c .LoadConfig (c .cfgLoader )
320
+ if err != nil {
321
+ return fmt .Errorf ("%w: %w" , config .ErrConfigLoadFailed , err )
322
+ }
323
+
318
324
// Load configuration layers (config file, then flag overrides).
319
- warnings , err := c .loadConfigurationLayers (logger , cmd )
325
+ warnings , err := c .loadConfigurationLayers (logger , cmd , cfg )
320
326
if err != nil {
321
- return fmt . Errorf ( "failed to load configuration: %w" , err )
327
+ return err
322
328
}
323
329
324
330
if c .dev && len (warnings ) > 0 {
@@ -342,10 +348,14 @@ func (c *DaemonCmd) run(cmd *cobra.Command, _ []string) error {
342
348
return err
343
349
}
344
350
345
- // Load runtime servers from config and context.
346
- runtimeServers , err := c .loadRuntimeServers ()
351
+ execCtx , err := c .ctxLoader .Load (flags .RuntimeFile )
347
352
if err != nil {
348
- return fmt .Errorf ("error loading runtime servers: %w" , err )
353
+ return fmt .Errorf ("failed to load runtime context: %w" , err )
354
+ }
355
+
356
+ runtimeServers , err := runtime .AggregateConfigs (cfg , execCtx )
357
+ if err != nil {
358
+ return fmt .Errorf ("failed to aggregate configs: %w" , err )
349
359
}
350
360
351
361
deps , err := daemon .NewDependencies (logger , addr , runtimeServers )
@@ -368,16 +378,22 @@ func (c *DaemonCmd) run(cmd *cobra.Command, _ []string) error {
368
378
return fmt .Errorf ("failed to create mcpd daemon instance: %w" , err )
369
379
}
370
380
371
- daemonCtx , daemonCtxCancel := signal .NotifyContext (
372
- context .Background (),
373
- os .Interrupt ,
374
- syscall .SIGTERM , syscall .SIGINT ,
375
- )
376
- defer daemonCtxCancel ()
381
+ // Create signal contexts for shutdown and reload.
382
+ shutdownCtx , shutdownCancel := context .WithCancel (context .Background ())
383
+ defer shutdownCancel ()
384
+
385
+ // Setup signal handling.
386
+ sigChan := make (chan os.Signal , 1 )
387
+ signal .Notify (sigChan , os .Interrupt , syscall .SIGTERM , syscall .SIGINT , syscall .SIGHUP )
388
+ defer signal .Stop (sigChan )
389
+
390
+ // Create reload channel for SIGHUP handling.
391
+ reloadChan := make (chan struct {}, 1 )
392
+ defer close (reloadChan ) // Ensure channel is closed on function exit
377
393
378
394
runErr := make (chan error , 1 )
379
395
go func () {
380
- if err := d .StartAndManage (daemonCtx ); err != nil && ! errors .Is (err , context .Canceled ) {
396
+ if err := d .StartAndManage (shutdownCtx ); err != nil && ! errors .Is (err , context .Canceled ) {
381
397
runErr <- err
382
398
}
383
399
close (runErr )
@@ -387,15 +403,36 @@ func (c *DaemonCmd) run(cmd *cobra.Command, _ []string) error {
387
403
c .printDevBanner (cmd .OutOrStdout (), logger , addr )
388
404
}
389
405
390
- select {
391
- case <- daemonCtx .Done ():
392
- logger .Info ("Shutting down daemon..." )
393
- err := <- runErr // Wait for cleanup and deferred logging
394
- logger .Info ("Shutdown complete" )
395
- return err // Graceful Ctrl+C / SIGTERM
396
- case err := <- runErr :
397
- logger .Error ("daemon exited with error" , "error" , err )
398
- return err // Propagate daemon failure
406
+ // Start signal handling in background.
407
+ go c .handleSignals (logger , sigChan , reloadChan , shutdownCancel )
408
+
409
+ // Start the daemon's main loop which responds to reloads, shutdowns and startup errors.
410
+ for {
411
+ select {
412
+ case <- reloadChan :
413
+ logger .Info ("Reloading servers..." )
414
+ if err := c .reloadServers (shutdownCtx , d ); err != nil {
415
+ logger .Error ("Failed to reload servers, exiting to prevent inconsistent state" , "error" , err )
416
+ // Signal shutdown to exit cleanly.
417
+ shutdownCancel ()
418
+ return fmt .Errorf ("configuration reload failed with unrecoverable error: %w" , err )
419
+ }
420
+
421
+ logger .Info ("Configuration reloaded successfully" )
422
+ case <- shutdownCtx .Done ():
423
+ logger .Info ("Shutting down daemon..." )
424
+ err := <- runErr // Wait for cleanup and deferred logging
425
+ logger .Info ("Shutdown complete" )
426
+
427
+ return err // Graceful shutdown
428
+ case err := <- runErr :
429
+ if err != nil {
430
+ logger .Error ("daemon exited with error" , "error" , err )
431
+ return err // Propagate daemon failure
432
+ }
433
+
434
+ return nil
435
+ }
399
436
}
400
437
}
401
438
@@ -428,19 +465,17 @@ func formatValue(value any) string {
428
465
// It follows the precedence order: flags > config file > defaults.
429
466
// CLI flags override config file values when explicitly set.
430
467
// Returns warnings for each flag override and any error encountered.
431
- func (c * DaemonCmd ) loadConfigurationLayers (logger hclog.Logger , cmd * cobra.Command ) ([]string , error ) {
432
- cfgModifier , err := c .cfgLoader .Load (flags .ConfigFile )
433
- if err != nil {
434
- return nil , err
435
- }
436
-
437
- cfg , ok := cfgModifier .(* config.Config )
438
- if ! ok {
439
- return nil , fmt .Errorf ("config file contains invalid configuration structure" )
468
+ func (c * DaemonCmd ) loadConfigurationLayers (
469
+ logger hclog.Logger ,
470
+ cmd * cobra.Command ,
471
+ cfg * config.Config ,
472
+ ) ([]string , error ) {
473
+ if cfg == nil {
474
+ return nil , fmt .Errorf ("config data not present, cannot apply configuration layers" )
440
475
}
441
476
477
+ // No daemon config section - flags and defaults will be used.
442
478
if cfg .Daemon == nil {
443
- // No daemon config section - flags and defaults will be used.
444
479
return nil , nil
445
480
}
446
481
@@ -728,24 +763,62 @@ func (c *DaemonCmd) loadConfigCORS(cors *config.CORSConfigSection, logger hclog.
728
763
return warnings
729
764
}
730
765
731
- // loadRuntimeServers loads the configuration and aggregates it with runtime context to produce runtime servers.
732
- func (c * DaemonCmd ) loadRuntimeServers () ([]runtime.Server , error ) {
733
- cfgModifier , err := c .cfgLoader .Load (flags .ConfigFile )
766
+ // handleSignals processes OS signals for daemon lifecycle management.
767
+ // This function is intended to be called in a dedicated goroutine.
768
+ //
769
+ // SIGHUP signals trigger configuration reloads via reloadChan.
770
+ // Termination signals (SIGTERM, SIGINT, os.Interrupt) trigger graceful shutdown via shutdownCancel.
771
+ // The function runs until a shutdown signal is received or sigChan is closed.
772
+ // Non-blocking sends to reloadChan prevent duplicate reload requests.
773
+ func (c * DaemonCmd ) handleSignals (
774
+ logger hclog.Logger ,
775
+ sigChan <- chan os.Signal ,
776
+ reloadChan chan <- struct {},
777
+ shutdownCancel context.CancelFunc ,
778
+ ) {
779
+ for sig := range sigChan {
780
+ switch sig {
781
+ case syscall .SIGHUP :
782
+ logger .Info ("Received SIGHUP, triggering config reload" )
783
+ select {
784
+ case reloadChan <- struct {}{}:
785
+ // Reload signal sent.
786
+ default :
787
+ // Reload already pending, skip.
788
+ logger .Warn ("Config reload already in progress, skipping" )
789
+ }
790
+ case os .Interrupt , syscall .SIGTERM , syscall .SIGINT :
791
+ logger .Info ("Received shutdown signal" , "signal" , sig )
792
+ shutdownCancel ()
793
+ return
794
+ }
795
+ }
796
+ }
797
+
798
+ // reloadServers reloads server configuration from config files.
799
+ // This method only reloads runtime servers; daemon config changes require a restart.
800
+ func (c * DaemonCmd ) reloadServers (ctx context.Context , d * daemon.Daemon ) error {
801
+ cfg , err := c .LoadConfig (c .cfgLoader )
734
802
if err != nil {
735
- return nil , fmt .Errorf ("failed to load config : %w" , err )
803
+ return fmt .Errorf ("%w : %w" , config . ErrConfigLoadFailed , err )
736
804
}
737
805
738
806
execCtx , err := c .ctxLoader .Load (flags .RuntimeFile )
739
807
if err != nil {
740
- return nil , fmt .Errorf ("failed to load runtime context: %w" , err )
808
+ return fmt .Errorf ("failed to load runtime context: %w" , err )
741
809
}
742
810
743
- servers , err := runtime .AggregateConfigs (cfgModifier , execCtx )
811
+ newServers , err := runtime .AggregateConfigs (cfg , execCtx )
744
812
if err != nil {
745
- return nil , fmt .Errorf ("failed to aggregate configs: %w" , err )
813
+ return fmt .Errorf ("failed to aggregate configs: %w" , err )
814
+ }
815
+
816
+ // Reload the servers in the daemon.
817
+ if err := d .ReloadServers (ctx , newServers ); err != nil {
818
+ return fmt .Errorf ("failed to reload servers: %w" , err )
746
819
}
747
820
748
- return servers , nil
821
+ return nil
749
822
}
750
823
751
824
// validateFlags validates the command flags and their relationships.
0 commit comments