go/task.go

78 lines
1.9 KiB
Go
Raw Normal View History

// SPDX-License-Identifier: EUPL-1.2
// Background task dispatch for the Core framework.
package core
import (
"reflect"
"slices"
"strconv"
)
// TaskState holds background task state.
type TaskState struct {
Identifier string
2026-03-20 21:00:48 +00:00
Task Task
Result any
Error error
}
// PerformAsync dispatches a task in a background goroutine.
func (c *Core) PerformAsync(t Task) Result {
if c.shutdown.Load() {
return Result{}
}
taskID := Concat("task-", strconv.FormatUint(c.taskIDCounter.Add(1), 10))
if tid, ok := t.(TaskWithIdentifier); ok {
tid.SetTaskIdentifier(taskID)
}
c.ACTION(ActionTaskStarted{TaskIdentifier: taskID, Task: t})
c.waitGroup.Go(func() {
defer func() {
if rec := recover(); rec != nil {
err := E("core.PerformAsync", Sprint("panic: ", rec), nil)
c.ACTION(ActionTaskCompleted{TaskIdentifier: taskID, Task: t, Result: nil, Error: err})
}
}()
r := c.PERFORM(t)
var err error
if !r.OK {
if e, ok := r.Value.(error); ok {
err = e
} else {
taskType := reflect.TypeOf(t)
typeName := "<nil>"
if taskType != nil {
typeName = taskType.String()
}
err = E("core.PerformAsync", Join(" ", "no handler found for task type", typeName), nil)
}
}
c.ACTION(ActionTaskCompleted{TaskIdentifier: taskID, Task: t, Result: r.Value, Error: err})
})
return Result{taskID, true}
}
// Progress broadcasts a progress update for a background task.
func (c *Core) Progress(taskID string, progress float64, message string, t Task) {
c.ACTION(ActionTaskProgress{TaskIdentifier: taskID, Task: t, Progress: progress, Message: message})
}
func (c *Core) Perform(t Task) Result {
c.ipc.taskMu.RLock()
handlers := slices.Clone(c.ipc.taskHandlers)
c.ipc.taskMu.RUnlock()
for _, h := range handlers {
r := h(c, t)
if r.OK {
return r
}
}
return Result{}
}
feat: implement RFC plans 1-5 — Registry[T], Action/Task, Process, primitives 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>
2026-03-25 15:18:25 +00:00
// Registration methods (RegisterAction, RegisterActions, RegisterTask)
// are in ipc.go — registration is IPC's responsibility.