diff --git a/pkg/core/contract.go b/pkg/core/contract.go index 2f52e6b..15ba428 100644 --- a/pkg/core/contract.go +++ b/pkg/core/contract.go @@ -86,6 +86,7 @@ func New(opts ...Options) *Core { services: &serviceRegistry{services: make(map[string]*Service)}, commands: &commandRegistry{commands: make(map[string]*Command)}, } + c.ctx, c.cancel = context.WithCancel(context.Background()) if len(opts) > 0 { cp := make(Options, len(opts[0])) diff --git a/pkg/core/core.go b/pkg/core/core.go index b88b0ed..1ec66d0 100644 --- a/pkg/core/core.go +++ b/pkg/core/core.go @@ -6,6 +6,7 @@ package core import ( + "context" "sync" "sync/atomic" ) @@ -29,6 +30,8 @@ type Core struct { ipc *Ipc // c.IPC() — Message bus for IPC i18n *I18n // c.I18n() — Internationalisation and locale collection + ctx context.Context + cancel context.CancelFunc taskIDCounter atomic.Uint64 wg sync.WaitGroup shutdown atomic.Bool @@ -48,6 +51,7 @@ func (c *Core) Log() *ErrorLog { return c.log } func (c *Core) Cli() *Cli { return c.cli } func (c *Core) IPC() *Ipc { return c.ipc } func (c *Core) I18n() *I18n { return c.i18n } +func (c *Core) Context() context.Context { return c.ctx } func (c *Core) Core() *Core { return c } // --- IPC (uppercase aliases) --- diff --git a/pkg/core/runtime.go b/pkg/core/runtime.go index a81afbd..6eae703 100644 --- a/pkg/core/runtime.go +++ b/pkg/core/runtime.go @@ -34,6 +34,7 @@ func (r *ServiceRuntime[T]) Config() *Config { return r.core.Config() } // ServiceStartup runs OnStart for all registered services that have one. func (c *Core) ServiceStartup(ctx context.Context, options any) Result { c.shutdown.Store(false) + c.ctx, c.cancel = context.WithCancel(ctx) startables := c.Startables() if startables.OK { for _, s := range startables.Value.([]*Service) { @@ -53,11 +54,10 @@ func (c *Core) ServiceStartup(ctx context.Context, options any) Result { // ServiceShutdown drains background tasks, then stops all registered services. func (c *Core) ServiceShutdown(ctx context.Context) Result { c.shutdown.Store(true) + c.cancel() // signal all context-aware tasks to stop c.ACTION(ActionServiceShutdown{}) // Drain background tasks before stopping services. - // On timeout, the waiter goroutine persists until tasks complete — - // this is inherent to sync.WaitGroup (no cancel mechanism). done := make(chan struct{}) go func() { c.wg.Wait()