Fixed bug in tentacles.

This commit is contained in:
Storm Dragon committed 2026-10-16 15:33:40 -04:00
1 parent 4be35fcf9c
commit 2fa9bc8724
22 files changed
+786 -648

No files matched your search

+84 -54
View File
@@ -7,7 +7,6 @@ import (
"context"
"errors"
"fmt"
"os"
"reflect"
"slices"
"strings"
@@ -25,6 +24,14 @@ type tentacleManagementCall struct {
packageFiles *tentacle.PreparedPackage
}
// 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
}
func (runtime *tentacleRuntime) Snapshot() tentacle.Snapshot {
if runtime == nil {
return tentacle.Snapshot{Suppressed: true, Message: "Tentacle management is unavailable for this run."}
@@ -201,22 +208,12 @@ func (runtime *tentacleRuntime) Manage(ctx context.Context, request tentacle.Man
default:
return tentacle.ErrBusy
}
if call.packageFiles != nil {
// Once queued, wait for the lifecycle owner's decision before releasing
// files. Returning on caller cancellation here could delete a committed
// package. The owner checks cancellation before saving the registry.
select {
case err := <-call.result:
return err
case <-runtime.done:
return tentacle.ErrUnavailable
}
}
// Once queued, wait for the lifecycle owner's decision. Startup requests
// wake the owner on cancellation so rollback completes before the response;
// package files remain owned until the registry decision is known.
select {
case err := <-call.result:
return err
case <-ctx.Done():
return ctx.Err()
case <-runtime.done:
return tentacle.ErrUnavailable
}
@@ -270,6 +267,7 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
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]tentacle.Entry)
for _, entry := range entries {
pending[entry.ID] = entry.Enabled
@@ -282,8 +280,6 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
}
}
var manager *tentacle.Manager
var delegation *tentacle.Delegation
var serviceDone <-chan struct{}
var current *tentacle.Session
var ready <-chan struct{}
var active []*tentacle.Session
@@ -338,13 +334,10 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
if runtime.readerContext.Err() != nil {
bindingError = nil
}
var managerError, serviceError error
var managerError error
if manager != nil {
managerError = manager.Shutdown(cleanup)
}
if delegation != nil {
serviceError = delegation.Close(cleanup)
}
for _, entry := range removedPackages {
if managerError == nil {
if err := tentacle.RemovePackage(tentacle.PackageRoot(runtime.registry.Path), entry); err != nil {
@@ -356,7 +349,7 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
<-done
}
runtime.results.Wait()
runtime.stopError = errors.Join(bindingError, managerError, serviceError)
runtime.stopError = errors.Join(bindingError, managerError)
if runtime.stopError != nil && runtime.readerContext.Err() == nil {
select {
case runtime.failures <- runtime.stopError:
@@ -372,34 +365,7 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
return errors.New("native bindings are unavailable")
}
var err error
if runtime.newManager != nil {
manager, err = runtime.newManager()
return err
}
parent := runtime.parent
if parent == "" || parent == "auto" {
program := runtime.guardianProgram
if len(program) == 0 {
executable, err := os.Executable()
if err != nil {
return err
}
program = []string{executable, "--tentacle-guardian"}
}
if delegation == nil {
delegation, err = tentacle.StartDelegation(ctx, program)
if err != nil {
return err
}
serviceDone = delegation.Done()
}
parent = delegation.Parent
}
if delegation != nil {
manager, err = tentacle.NewDelegatedManager(delegation)
} else {
manager, err = tentacle.NewContainedManager(tentacle.DefaultLimits(), nil, parent, tentacle.DefaultResourceLimits())
}
manager, err = runtime.makeManager()
return err
}
revoke := func(id string) {
@@ -408,10 +374,58 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
runtime.output.RetireRevoked()
}
}
replyStartup := func(id string, err error) {
startup, exists := startups[id]
if !exists {
return
}
delete(startups, id)
startup.stop()
if err == nil {
err = startup.saveError
}
setSnapshot()
startup.call.result <- err
}
failStartup := func(id, message string) {
problems[id] = message
if _, exists := startups[id]; !exists {
return
}
revoke(id)
if current != nil && current.TentacleID() == id {
current, ready = nil, nil
}
delete(pending, id)
next := tentacle.CloneEntries(entries)
for index := range next {
if next[index].ID == id {
next[index].Enabled = false
}
}
err := runtime.registry.Save(next)
if err == nil || errors.Is(err, tentacle.ErrRegistrySync) {
entries = next
}
if err != nil {
message += " The disabled state could not be saved durably: " + err.Error()
}
replyStartup(id, errors.New(message))
}
defer func() {
for id := range startups {
failStartup(id, "Tentacle startup was cancelled before activation.")
}
}()
for {
if ctx.Err() != nil {
return nil
}
for id, startup := range startups {
if startup.call.ctx.Err() != nil {
failStartup(id, "Tentacle startup was cancelled before activation.")
}
}
// Reconcile revoked sessions before staging new native grabs.
retained := active[:0]
for _, session := range active {
@@ -474,7 +488,7 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
session, err = manager.Start(ctx, entry.Spec())
}
if err != nil {
problems[entry.ID] = "Tentacle could not start; check resource containment and its program, then retry."
failStartup(entry.ID, "Tentacle could not start; check resource containment and its program, then enable it again.")
runtime.logger.Warn("start configured tentacle", "tentacle", entry.ID, "error", err)
runtime.report("A tentacle could not start.")
notify()
@@ -517,10 +531,6 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
select {
case <-ctx.Done():
return nil
case <-serviceDone:
// A lost containment owner invalidates every session. Do not relaunch
// into a disappearing cgroup or run uncontained.
return tentacle.ErrContainment
case <-wake:
case call := <-runtime.requests:
if err := call.ctx.Err(); err != nil {
@@ -557,6 +567,10 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
continue
}
}
if _, starting := startups[call.request.ID]; starting && call.request.Action != "disable" && call.request.Action != "remove" {
call.result <- tentacle.ErrBusy
continue
}
next, err := change_tentacle_entries(entries, call.request)
if err == nil && call.request.Action != "retry" && reflect.DeepEqual(next, entries) {
call.result <- nil
@@ -567,6 +581,7 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
}
if err == nil || errors.Is(err, tentacle.ErrRegistrySync) {
id := call.request.ID
replyStartup(id, errors.New("Tentacle startup was cancelled by another operation."))
if call.packageFiles != nil {
call.packageFiles.Retain()
}
@@ -578,6 +593,9 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
}
}
revoke(id)
if current != nil && current.TentacleID() == id {
current, ready = nil, nil
}
entries = next
delete(problems, id)
delete(pending, id)
@@ -590,12 +608,21 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
stillActive := slices.DeleteFunc(slices.Clone(active), func(session *tentacle.Session) bool { state, _ := session.Status(); return state != tentacle.Active })
runtime.publishCommands(stillActive)
setSnapshot()
if pending[id] {
startups[id] = tentacleStartupCall{call: call, saveError: err, stop: context.AfterFunc(call.ctx, notify)}
continue
}
}
call.result <- err
case <-ready:
session := current
current, ready = nil, nil
if state, _ := session.Status(); state != tentacle.Ready {
failStartup(session.TentacleID(), "Tentacle failed during startup; check its program and resource limits, then enable it again.")
continue
}
if startup, exists := startups[session.TentacleID()]; exists && startup.call.ctx.Err() != nil {
failStartup(session.TentacleID(), "Tentacle startup was cancelled before activation.")
continue
}
candidate := append(slices.Clone(active), session)
@@ -603,6 +630,7 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
session.Disable()
runtime.logger.Warn("configured tentacle bindings rejected", "tentacle", session.TentacleID(), "error", err)
runtime.report("A tentacle could not acquire its keyboard shortcuts.")
failStartup(session.TentacleID(), "Tentacle could not acquire its keyboard shortcuts; enable it again after resolving the conflict.")
continue
}
if err := session.Activate(); err != nil {
@@ -614,10 +642,12 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
}
return err
}
failStartup(session.TentacleID(), "Tentacle stopped before activation; enable it again to retry.")
continue
}
active = candidate
runtime.publishCommands(active)
replyStartup(session.TentacleID(), nil)
runtime.logger.Info("configured tentacle active", "tentacle", session.TentacleID(), "session", session.Identity())
}
}