201 lines
6.2 KiB
Go
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
|
|
}
|