Plans 1-5 complete for core/go scope. 456 tests, 84.4% coverage, 100% AX-7 naming.
Critical bugs (Plan 1):
- P4-3+P7-3: ACTION broadcast calls all handlers with panic recovery
- P7-2+P7-4: RunE() with defer ServiceShutdown, Run() delegates
- P3-1: Startable/Stoppable return Result (breaking, clean)
- P9-1: Zero os/exec — App.Find() rewritten with os.Stat+PATH
- I3: Embed() removed, I15: New() comment fixed
- I9: CommandLifecycle removed → Command.Managed field
Registry[T] (Plan 2):
- Universal thread-safe named collection with 3 lock modes
- All 5 registries migrated: services, commands, drive, data, lock
- Insertion order preserved (fixes P4-1)
- c.RegistryOf("name") cross-cutting accessor
Action/Task system (Plan 3):
- Action type with Run()/Exists(), ActionHandler signature
- c.Action("name") dual-purpose accessor (register/invoke)
- TaskDef with Steps — sequential chain, async dispatch, previous-input piping
- Panic recovery on all Action execution
- broadcast() internal, ACTION() sugar
Process primitive (Plan 4):
- c.Process() returns Action sugar — Run/RunIn/RunWithEnv/Start/Kill/Exists
- No deps added — delegates to c.Action("process.*")
- Permission-by-registration: no handler = no capability
Missing primitives (Plan 5):
- core.ID() — atomic counter + crypto/rand suffix
- ValidateName() / SanitisePath() — reusable validation
- Fs.WriteAtomic() — write-to-temp-then-rename
- Fs.NewUnrestricted() / Fs.Root() — legitimate sandbox bypass
- AX-7: 456/456 tests renamed to TestFile_Function_{Good,Bad,Ugly}
Co-Authored-By: Virgil <virgil@lethean.io>
163 lines
5.4 KiB
Go
163 lines
5.4 KiB
Go
// SPDX-License-Identifier: EUPL-1.2
|
|
|
|
// Package core is a dependency injection and service lifecycle framework for Go.
|
|
// This file defines the Core struct, accessors, and IPC/error wrappers.
|
|
|
|
package core
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"sync"
|
|
"sync/atomic"
|
|
)
|
|
|
|
// --- Core Struct ---
|
|
|
|
// Core is the central application object that manages services, assets, and communication.
|
|
type Core struct {
|
|
options *Options // c.Options() — Input configuration used to create this Core
|
|
app *App // c.App() — Application identity + optional GUI runtime
|
|
data *Data // c.Data() — Embedded/stored content from packages
|
|
drive *Drive // c.Drive() — Resource handle registry (transports)
|
|
fs *Fs // c.Fs() — Local filesystem I/O (sandboxable)
|
|
config *Config // c.Config() — Configuration, settings, feature flags
|
|
error *ErrorPanic // c.Error() — Panic recovery and crash reporting
|
|
log *ErrorLog // c.Log() — Structured logging + error wrapping
|
|
// cli accessed via ServiceFor[*Cli](c, "cli")
|
|
commands *CommandRegistry // c.Command("path") — Command tree
|
|
services *ServiceRegistry // c.Service("name") — Service registry
|
|
lock *Lock // c.Lock("name") — Named mutexes
|
|
ipc *Ipc // c.IPC() — Message bus for IPC
|
|
info *SysInfo // c.Env("key") — Read-only system/environment information
|
|
i18n *I18n // c.I18n() — Internationalisation and locale collection
|
|
|
|
context context.Context
|
|
cancel context.CancelFunc
|
|
taskIDCounter atomic.Uint64
|
|
waitGroup sync.WaitGroup
|
|
shutdown atomic.Bool
|
|
}
|
|
|
|
// --- Accessors ---
|
|
|
|
func (c *Core) Options() *Options { return c.options }
|
|
func (c *Core) App() *App { return c.app }
|
|
func (c *Core) Data() *Data { return c.data }
|
|
func (c *Core) Drive() *Drive { return c.drive }
|
|
func (c *Core) Fs() *Fs { return c.fs }
|
|
func (c *Core) Config() *Config { return c.config }
|
|
func (c *Core) Error() *ErrorPanic { return c.error }
|
|
func (c *Core) Log() *ErrorLog { return c.log }
|
|
func (c *Core) Cli() *Cli {
|
|
cl, _ := ServiceFor[*Cli](c, "cli")
|
|
return cl
|
|
}
|
|
func (c *Core) IPC() *Ipc { return c.ipc }
|
|
func (c *Core) I18n() *I18n { return c.i18n }
|
|
func (c *Core) Env(key string) string { return Env(key) }
|
|
func (c *Core) Context() context.Context { return c.context }
|
|
func (c *Core) Core() *Core { return c }
|
|
|
|
// --- Lifecycle ---
|
|
|
|
// RunE starts all services, runs the CLI, then shuts down.
|
|
// Returns an error instead of calling os.Exit — let main() handle the exit.
|
|
// ServiceShutdown is always called via defer, even on startup failure or panic.
|
|
//
|
|
// if err := c.RunE(); err != nil {
|
|
// os.Exit(1)
|
|
// }
|
|
func (c *Core) RunE() error {
|
|
defer c.ServiceShutdown(context.Background())
|
|
|
|
r := c.ServiceStartup(c.context, nil)
|
|
if !r.OK {
|
|
if err, ok := r.Value.(error); ok {
|
|
return err
|
|
}
|
|
return E("core.Run", "startup failed", nil)
|
|
}
|
|
|
|
if cli := c.Cli(); cli != nil {
|
|
r = cli.Run()
|
|
}
|
|
|
|
if !r.OK {
|
|
if err, ok := r.Value.(error); ok {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Run starts all services, runs the CLI, then shuts down.
|
|
// Calls os.Exit(1) on failure. For error handling use RunE().
|
|
//
|
|
// c := core.New(core.WithService(myService.Register))
|
|
// c.Run()
|
|
func (c *Core) Run() {
|
|
if err := c.RunE(); err != nil {
|
|
Error(err.Error())
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// --- IPC (uppercase aliases) ---
|
|
|
|
func (c *Core) ACTION(msg Message) Result { return c.broadcast(msg) }
|
|
func (c *Core) QUERY(q Query) Result { return c.Query(q) }
|
|
func (c *Core) QUERYALL(q Query) Result { return c.QueryAll(q) }
|
|
func (c *Core) PERFORM(t Task) Result { return c.Perform(t) }
|
|
|
|
// --- Error+Log ---
|
|
|
|
// LogError logs an error and returns the Result from ErrorLog.
|
|
func (c *Core) LogError(err error, op, msg string) Result {
|
|
return c.log.Error(err, op, msg)
|
|
}
|
|
|
|
// LogWarn logs a warning and returns the Result from ErrorLog.
|
|
func (c *Core) LogWarn(err error, op, msg string) Result {
|
|
return c.log.Warn(err, op, msg)
|
|
}
|
|
|
|
// Must logs and panics if err is not nil.
|
|
func (c *Core) Must(err error, op, msg string) {
|
|
c.log.Must(err, op, msg)
|
|
}
|
|
|
|
// --- Registry Accessor ---
|
|
|
|
// RegistryOf returns a named registry for cross-cutting queries.
|
|
// Known registries: "services", "commands", "actions".
|
|
//
|
|
// c.RegistryOf("services").Names() // all service names
|
|
// c.RegistryOf("actions").List("process.*") // process capabilities
|
|
// c.RegistryOf("commands").Len() // command count
|
|
func (c *Core) RegistryOf(name string) *Registry[any] {
|
|
// Bridge typed registries to untyped access for cross-cutting queries.
|
|
// Each registry is wrapped in a read-only proxy.
|
|
switch name {
|
|
case "services":
|
|
return registryProxy(c.services.Registry)
|
|
case "commands":
|
|
return registryProxy(c.commands.Registry)
|
|
case "actions":
|
|
return registryProxy(c.ipc.actions)
|
|
default:
|
|
return NewRegistry[any]() // empty registry for unknown names
|
|
}
|
|
}
|
|
|
|
// registryProxy creates a read-only any-typed view of a typed registry.
|
|
// Copies current state — not a live view (avoids type parameter leaking).
|
|
func registryProxy[T any](src *Registry[T]) *Registry[any] {
|
|
proxy := NewRegistry[any]()
|
|
src.Each(func(name string, item T) {
|
|
proxy.Set(name, item)
|
|
})
|
|
return proxy
|
|
}
|
|
|
|
// --- Global Instance ---
|