Files
cthulhu/internal/app/tentacle_startup.go
2026-11-01 22:01:32 -05:00

201 lines
6.2 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"errors"
"slices"
"git.stormux.org/storm/cthulhu/internal/tentacle"
)
func (owner *tentacleLifecycle) ensureManager() error {
if owner.manager != nil {
return nil
}
if owner.runtime.input == nil {
return errors.New("native bindings are unavailable")
}
var err error
owner.manager, err = owner.runtime.makeManager()
return err
}
func (owner *tentacleLifecycle) revoke(id string) {
if session := owner.sessions[id]; session != nil {
session.Disable()
owner.runtime.output.RetireRevoked()
}
}
func (owner *tentacleLifecycle) replyStartup(id string, err error) {
startup, exists := owner.startups[id]
if !exists {
return
}
delete(owner.startups, id)
startup.stop()
if err == nil {
if startup.previous != nil {
previous := *startup.previous
// Starting the replacement already waited for the old session.
owner.removedPackages[previous.PackageDirectory] = tentacleRetiredPackage{entry: previous}
}
err = startup.saveError
}
owner.setSnapshot()
startup.call.result <- err
}
func (owner *tentacleLifecycle) failStartup(id, message string) {
owner.problems[id] = message
startup, exists := owner.startups[id]
if !exists {
return
}
owner.revoke(id)
if owner.current != nil && owner.current.TentacleID() == id {
owner.current, owner.ready = nil, nil
}
delete(owner.pending, id)
next := tentacle.CloneEntries(owner.entries)
for index := range next {
if next[index].ID == id {
if startup.previous != nil {
next[index] = *startup.previous
} else {
next[index].Enabled = false
}
}
}
err := owner.runtime.registry.Save(next)
if err == nil || errors.Is(err, tentacle.ErrRegistrySync) {
if startup.previous != nil {
failed := slices.IndexFunc(owner.entries, func(entry tentacle.Entry) bool { return entry.ID == id })
if failed >= 0 {
entry := owner.entries[failed]
owner.removedPackages[entry.PackageDirectory] = tentacleRetiredPackage{entry: entry, session: owner.sessions[id]}
}
owner.runtime.mutex.Lock()
disabled := owner.runtime.disabled
owner.runtime.mutex.Unlock()
owner.pending[id] = startup.previous.Enabled && !disabled && owner.ctx.Err() == nil
message += " The previous package was restored."
}
owner.entries = next
}
if err != nil {
message += " The recovery state could not be saved durably: " + err.Error()
}
owner.replyStartup(id, errors.New(message))
}
func (owner *tentacleLifecycle) cancelStartups() {
for id := range owner.startups {
owner.failStartup(id, "Tentacle startup was cancelled before activation.")
}
}
func (owner *tentacleLifecycle) startPending() {
for _, entry := range owner.entries {
if !owner.pending[entry.ID] || owner.sessions[entry.ID] != nil {
continue
}
owner.pending[entry.ID] = false
err := owner.ensureManager()
var session *tentacle.Session
if err == nil {
session, err = owner.manager.Start(owner.ctx, entry.Spec())
}
if err != nil {
owner.failStartup(entry.ID, "Tentacle could not start; check resource containment and its program, then enable it again.")
owner.runtime.logger.Warn("start configured tentacle", "tentacle", entry.ID, "error", err)
owner.runtime.report("A tentacle could not start.")
owner.notify()
break
}
owner.sessions[entry.ID] = session
owner.runtime.mutex.Lock()
owner.runtime.sessions = append(owner.runtime.sessions, session)
if owner.runtime.disabled {
session.Disable()
}
owner.runtime.mutex.Unlock()
owner.current, owner.ready = session, session.Ready()
done := make(chan struct{})
// Prune joined watcher handles so retries never accumulate history.
owner.watchers = slices.DeleteFunc(owner.watchers, func(done <-chan struct{}) bool {
select {
case <-done:
return true
default:
return false
}
})
owner.watchers = append(owner.watchers, done)
go func() {
defer close(done)
select {
case <-session.Revoked():
case <-owner.ctx.Done():
}
owner.runtime.output.RetireRevoked()
owner.runtime.signal_extensions()
owner.notify()
<-session.Done()
owner.notify()
}()
break
}
}
func (owner *tentacleLifecycle) activateReady() error {
session := owner.current
owner.current, owner.ready = nil, nil
if state, _ := session.Status(); state != tentacle.Ready {
owner.failStartup(session.TentacleID(), "Tentacle failed during startup; check its program and resource limits, then enable it again.")
return nil
}
if startup, exists := owner.startups[session.TentacleID()]; exists && startup.call.ctx.Err() != nil {
owner.failStartup(session.TentacleID(), "Tentacle startup was cancelled before activation.")
return nil
}
candidate := append(slices.Clone(owner.active), session)
if err := owner.runtime.bind(owner.ctx, candidate); err != nil {
session.Disable()
owner.runtime.logger.Warn("configured tentacle bindings rejected", "tentacle", session.TentacleID(), "error", err)
owner.runtime.report("A tentacle could not acquire its keyboard shortcuts.")
owner.failStartup(session.TentacleID(), "Tentacle could not acquire its keyboard shortcuts; enable it again after resolving the conflict.")
return nil
}
// Native binding setup can wait for held keys. Cancellation during
// that wait must roll back the new grabs before reporting failure.
activationError := owner.ctx.Err()
if startup, exists := owner.startups[session.TentacleID()]; exists && activationError == nil {
activationError = startup.call.ctx.Err()
}
if activationError == nil {
activationError = session.Activate()
}
if activationError != nil {
session.Disable()
if err := owner.runtime.bind(owner.ctx, owner.active); err != nil && owner.ctx.Err() == nil {
select {
case owner.runtime.failures <- err:
default:
}
return err
}
owner.failStartup(session.TentacleID(), "Tentacle stopped before activation; enable it again to retry.")
return nil
}
owner.active = candidate
delete(owner.problems, session.TentacleID())
owner.runtime.publishCommands(owner.active)
owner.replyStartup(session.TentacleID(), nil)
owner.runtime.logger.Info("configured tentacle active", "tentacle", session.TentacleID(), "session", session.Identity())
return nil
}