Code review and cleanup.
This commit is contained in:
1 parent
bbe931335f
commit
92d6ad3cc4
23 files changed
+1497
-886
No files matched your search
@@ -0,0 +1,268 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// Authors: Storm Dragon, Angus Kola
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"git.stormux.org/storm/cthulhu/internal/tentacle"
|
||||
)
|
||||
|
||||
// A startup request is owned by the lifecycle worker until activation or failure.
|
||||
// Cancellation only wakes that worker; it never mutates registry or session state.
|
||||
type tentacleStartupCall struct {
|
||||
call tentacleManagementCall
|
||||
saveError error
|
||||
stop func() bool
|
||||
previous *tentacle.Entry
|
||||
}
|
||||
|
||||
// A retired package stays on disk until the session using it has joined.
|
||||
type tentacleRetiredPackage struct {
|
||||
entry tentacle.Entry
|
||||
session *tentacle.Session
|
||||
}
|
||||
|
||||
// tentacleLifecycle owns managed state for one run, on the lifecycle goroutine.
|
||||
// Watchers and cancellation callbacks only notify it; they never mutate these
|
||||
// maps or transitions. Startup rollback runs before shutdown joins sessions.
|
||||
type tentacleLifecycle struct {
|
||||
ctx context.Context
|
||||
runtime *tentacleRuntime
|
||||
entries []tentacle.Entry
|
||||
repositories []tentacle.Repository
|
||||
loadError error
|
||||
pending map[string]bool
|
||||
sessions map[string]*tentacle.Session
|
||||
problems map[string]string
|
||||
startups map[string]tentacleStartupCall
|
||||
removedPackages map[string]tentacleRetiredPackage
|
||||
wake chan struct{}
|
||||
manager *tentacle.Manager
|
||||
current *tentacle.Session
|
||||
ready <-chan struct{}
|
||||
active []*tentacle.Session
|
||||
watchers []<-chan struct{}
|
||||
}
|
||||
|
||||
func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
|
||||
entries, loadError := runtime.registry.Load()
|
||||
owner := &tentacleLifecycle{
|
||||
ctx: ctx,
|
||||
runtime: runtime,
|
||||
entries: entries,
|
||||
loadError: loadError,
|
||||
repositories: runtime.registry.Repositories(),
|
||||
pending: make(map[string]bool),
|
||||
sessions: make(map[string]*tentacle.Session),
|
||||
problems: make(map[string]string),
|
||||
startups: make(map[string]tentacleStartupCall),
|
||||
removedPackages: make(map[string]tentacleRetiredPackage),
|
||||
wake: make(chan struct{}, 1),
|
||||
}
|
||||
if loadError == nil {
|
||||
if err := tentacle.RecoverPackages(tentacle.PackageRoot(runtime.registry.Path), entries); err != nil {
|
||||
runtime.logger.Warn("recover tentacle packages", "error", err)
|
||||
}
|
||||
}
|
||||
for _, entry := range entries {
|
||||
owner.pending[entry.ID] = entry.Enabled
|
||||
}
|
||||
owner.setSnapshot()
|
||||
if loadError != nil {
|
||||
runtime.report("Tentacles could not start.")
|
||||
}
|
||||
defer owner.shutdown()
|
||||
defer owner.cancelStartups()
|
||||
return owner.run()
|
||||
}
|
||||
|
||||
func (owner *tentacleLifecycle) notify() {
|
||||
select {
|
||||
case owner.wake <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (owner *tentacleLifecycle) setSnapshot() {
|
||||
result := tentacle.Snapshot{Repositories: tentacle.CloneRepositories(owner.repositories)}
|
||||
if owner.loadError != nil {
|
||||
result.Message = "Tentacle registry could not be loaded; no tentacles were started."
|
||||
}
|
||||
for _, entry := range owner.entries {
|
||||
status := tentacle.EntryStatus{Entry: entry, State: "disabled", Message: owner.problems[entry.ID]}
|
||||
if entry.Enabled {
|
||||
status.State = "starting"
|
||||
}
|
||||
if session := owner.sessions[entry.ID]; session != nil {
|
||||
state, _ := session.Status()
|
||||
status.State = string(state)
|
||||
if state == tentacle.Ready {
|
||||
status.State = "starting"
|
||||
}
|
||||
if (state == tentacle.Failed || state == tentacle.Stopped) && entry.Enabled && status.Message == "" {
|
||||
status.Message = "Tentacle stopped; retry to start it again."
|
||||
}
|
||||
}
|
||||
if owner.pending[entry.ID] {
|
||||
status.State = "starting"
|
||||
}
|
||||
if status.Message != "" && !owner.pending[entry.ID] {
|
||||
status.State = "failed"
|
||||
}
|
||||
if !entry.Enabled && owner.sessions[entry.ID] == nil {
|
||||
status.State = "disabled"
|
||||
}
|
||||
result.Entries = append(result.Entries, status)
|
||||
}
|
||||
result.Updates = tentacle.AvailableUpdates(result)
|
||||
owner.runtime.mutex.Lock()
|
||||
owner.runtime.snapshot = result
|
||||
owner.runtime.mutex.Unlock()
|
||||
}
|
||||
|
||||
func (owner *tentacleLifecycle) shutdown() {
|
||||
owner.runtime.Disable()
|
||||
cleanup, cancel := context.WithTimeout(context.Background(), 7*time.Second)
|
||||
defer cancel()
|
||||
var bindingError error
|
||||
if owner.runtime.input != nil {
|
||||
bindingError = owner.runtime.input.SetTentacleBindings(cleanup, nil)
|
||||
}
|
||||
if owner.runtime.readerContext.Err() != nil {
|
||||
bindingError = nil
|
||||
}
|
||||
var managerError error
|
||||
if owner.manager != nil {
|
||||
managerError = owner.manager.Shutdown(cleanup)
|
||||
}
|
||||
for _, retired := range owner.removedPackages {
|
||||
entry := retired.entry
|
||||
if managerError == nil {
|
||||
if err := tentacle.RemovePackage(tentacle.PackageRoot(owner.runtime.registry.Path), entry); err != nil {
|
||||
owner.runtime.logger.Warn("remove tentacle package", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, done := range owner.watchers {
|
||||
<-done
|
||||
}
|
||||
owner.runtime.results.Wait()
|
||||
owner.runtime.stopError = errors.Join(bindingError, managerError)
|
||||
if owner.runtime.stopError != nil && owner.runtime.readerContext.Err() == nil {
|
||||
select {
|
||||
case owner.runtime.failures <- owner.runtime.stopError:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (owner *tentacleLifecycle) reconcileBindings() error {
|
||||
retained := owner.active[:0]
|
||||
for _, session := range owner.active {
|
||||
if state, _ := session.Status(); state == tentacle.Active {
|
||||
retained = append(retained, session)
|
||||
}
|
||||
}
|
||||
changed := len(retained) != len(owner.active)
|
||||
owner.active = retained
|
||||
if changed {
|
||||
owner.runtime.publishCommands(owner.active)
|
||||
if err := owner.runtime.bind(owner.ctx, owner.active); err != nil && owner.ctx.Err() == nil {
|
||||
select {
|
||||
case owner.runtime.failures <- err:
|
||||
default:
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (owner *tentacleLifecycle) reapSessions() {
|
||||
for id, session := range owner.sessions {
|
||||
select {
|
||||
case <-session.Done():
|
||||
if state, failure := session.Status(); state == tentacle.Failed {
|
||||
owner.runtime.logger.Warn("configured tentacle failed", "tentacle", id, "error", failure)
|
||||
}
|
||||
if !owner.pending[id] {
|
||||
index := slices.IndexFunc(owner.entries, func(entry tentacle.Entry) bool { return entry.ID == id })
|
||||
if index >= 0 && owner.entries[index].Enabled && owner.problems[id] == "" {
|
||||
owner.problems[id] = "Tentacle stopped; retry to start it again."
|
||||
owner.runtime.report("A tentacle stopped unexpectedly.")
|
||||
}
|
||||
}
|
||||
delete(owner.sessions, id)
|
||||
owner.runtime.mutex.Lock()
|
||||
owner.runtime.sessions = slices.DeleteFunc(owner.runtime.sessions, func(value *tentacle.Session) bool { return value == session })
|
||||
owner.runtime.mutex.Unlock()
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (owner *tentacleLifecycle) removeRetiredPackages() {
|
||||
for directory, retired := range owner.removedPackages {
|
||||
entry := retired.entry
|
||||
if retired.session != nil {
|
||||
select {
|
||||
case <-retired.session.Done():
|
||||
default:
|
||||
continue
|
||||
}
|
||||
}
|
||||
if slices.ContainsFunc(owner.entries, func(candidateEntry tentacle.Entry) bool {
|
||||
return candidateEntry.PackageDirectory == directory
|
||||
}) {
|
||||
continue
|
||||
}
|
||||
if err := tentacle.RemovePackage(tentacle.PackageRoot(owner.runtime.registry.Path), entry); err != nil {
|
||||
owner.runtime.logger.Warn("remove tentacle package", "error", err)
|
||||
owner.runtime.report("Tentacle was removed, but its package files could not be deleted.")
|
||||
continue
|
||||
}
|
||||
delete(owner.removedPackages, directory)
|
||||
}
|
||||
}
|
||||
|
||||
func (owner *tentacleLifecycle) run() error {
|
||||
for {
|
||||
if owner.ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
for id, startup := range owner.startups {
|
||||
if startup.call.ctx.Err() != nil {
|
||||
owner.failStartup(id, "Tentacle startup was cancelled before activation.")
|
||||
}
|
||||
}
|
||||
// Reconcile revoked sessions before staging new native grabs, and release
|
||||
// joined sessions before starting replacements or removing their files.
|
||||
if err := owner.reconcileBindings(); err != nil {
|
||||
return err
|
||||
}
|
||||
owner.reapSessions()
|
||||
if owner.current == nil {
|
||||
owner.removeRetiredPackages()
|
||||
owner.startPending()
|
||||
}
|
||||
owner.setSnapshot()
|
||||
select {
|
||||
case <-owner.ctx.Done():
|
||||
return nil
|
||||
case <-owner.wake:
|
||||
case call := <-owner.runtime.requests:
|
||||
owner.handleRequest(call)
|
||||
case <-owner.ready:
|
||||
if err := owner.activateReady(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user