Added tentacle updater.

This commit is contained in:
Kendell Clark authored and Storm Dragon committed 2026-11-01 12:21:55 -05:00
1 parent f2cc47c8a0
commit 7591f65b58
17 files changed
+948 -22

No files matched your search

+111 -16
View File
@@ -30,6 +30,13 @@ 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
}
func (runtime *tentacleRuntime) Snapshot() tentacle.Snapshot {
@@ -43,6 +50,7 @@ func (runtime *tentacleRuntime) Snapshot() tentacle.Snapshot {
defer runtime.mutex.Unlock()
result := runtime.snapshot
result.Repositories = tentacle.CloneRepositories(result.Repositories)
result.Updates = slices.Clone(result.Updates)
result.Entries = append([]tentacle.EntryStatus(nil), result.Entries...)
var bindings []input.BindingOverride
if runtime.input != nil && !runtime.disabled {
@@ -124,7 +132,7 @@ func (runtime *tentacleRuntime) Manage(ctx context.Context, request tentacle.Man
if disabled {
return tentacle.ErrDisabled
}
if request.Action == "install" || request.Action == "install-enable" {
if request.Action == "install" || request.Action == "install-enable" || request.Action == "upgrade" {
if request.Entry != nil {
return errors.New("unexpected tentacle entry in installation request")
}
@@ -138,9 +146,37 @@ func (runtime *tentacleRuntime) Manage(ctx context.Context, request tentacle.Man
}
repository := snapshot.Repositories[index]
id := tentacle.PackageID(repository, request.ID)
if slices.ContainsFunc(snapshot.Entries, func(value tentacle.EntryStatus) bool { return value.Entry.ID == id }) {
installed := slices.ContainsFunc(snapshot.Entries, func(value tentacle.EntryStatus) bool { return value.Entry.ID == id })
if request.Action == "upgrade" && !installed {
return errors.New("tentacle is not installed")
}
if request.Action != "upgrade" && installed {
return errors.New("tentacle is already installed")
}
if request.Action == "upgrade" {
for _, status := range snapshot.Entries {
if status.Entry.ID != id {
continue
}
for _, manifest := range repository.Tentacles {
if manifest.ID != request.ID {
continue
}
if manifest.Version == status.Entry.Version {
return errors.New("tentacle is already at the repository version")
}
newer, err := tentacle.IsNewerVersion(manifest.Version, status.Entry.Version)
if err != nil {
return fmt.Errorf("cannot compare tentacle versions: %w", err)
}
if !newer {
return errors.New("repository tentacle version is not newer than installed version")
}
break
}
break
}
}
runtime.mutex.Lock()
if runtime.repositoryBusy {
runtime.mutex.Unlock()
@@ -162,9 +198,15 @@ func (runtime *tentacleRuntime) Manage(ctx context.Context, request tentacle.Man
return err
}
defer prepared.Discard()
prepared.Entry.Enabled = request.Action == "install-enable"
operation := "add"
if request.Action == "upgrade" {
operation = "update"
// The lifecycle owner preserves enablement at commit time.
} else {
prepared.Entry.Enabled = request.Action == "install-enable"
}
call.packageFiles, call.repository = prepared, &repository
call.request = tentacle.ManagementRequest{Action: "add", ID: prepared.Entry.ID, Entry: &prepared.Entry}
call.request = tentacle.ManagementRequest{Action: operation, ID: prepared.Entry.ID, Entry: &prepared.Entry}
}
if request.Action == "repository-add" || request.Action == "repository-refresh" {
if request.Entry != nil || request.ID != "" {
@@ -240,6 +282,7 @@ func change_tentacle_entries(entries []tentacle.Entry, request tentacle.Manageme
next = append(next, *request.Entry)
case "update":
next[index] = *request.Entry
next[index].Enabled = entries[index].Enabled
case "remove":
next = slices.Delete(next, index, index+1)
case "enable":
@@ -268,7 +311,7 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
sessions := make(map[string]*tentacle.Session)
problems := make(map[string]string)
startups := make(map[string]tentacleStartupCall)
removedPackages := make(map[string]tentacle.Entry)
removedPackages := make(map[string]tentacleRetiredPackage)
for _, entry := range entries {
pending[entry.ID] = entry.Enabled
}
@@ -315,6 +358,7 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
}
result.Entries = append(result.Entries, status)
}
result.Updates = tentacle.AvailableUpdates(result)
runtime.mutex.Lock()
runtime.snapshot = result
runtime.mutex.Unlock()
@@ -338,7 +382,8 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
if manager != nil {
managerError = manager.Shutdown(cleanup)
}
for _, entry := range removedPackages {
for _, retired := range removedPackages {
entry := retired.entry
if managerError == nil {
if err := tentacle.RemovePackage(tentacle.PackageRoot(runtime.registry.Path), entry); err != nil {
runtime.logger.Warn("remove tentacle package", "error", err)
@@ -382,6 +427,11 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
delete(startups, id)
startup.stop()
if err == nil {
if startup.previous != nil {
previous := *startup.previous
// Starting the replacement already waited for the old session.
removedPackages[previous.PackageDirectory] = tentacleRetiredPackage{entry: previous}
}
err = startup.saveError
}
setSnapshot()
@@ -389,7 +439,8 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
}
failStartup := func(id, message string) {
problems[id] = message
if _, exists := startups[id]; !exists {
startup, exists := startups[id]
if !exists {
return
}
revoke(id)
@@ -400,15 +451,31 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
next := tentacle.CloneEntries(entries)
for index := range next {
if next[index].ID == id {
next[index].Enabled = false
if startup.previous != nil {
next[index] = *startup.previous
} else {
next[index].Enabled = false
}
}
}
err := runtime.registry.Save(next)
if err == nil || errors.Is(err, tentacle.ErrRegistrySync) {
if startup.previous != nil {
failed := slices.IndexFunc(entries, func(entry tentacle.Entry) bool { return entry.ID == id })
if failed >= 0 {
entry := entries[failed]
removedPackages[entry.PackageDirectory] = tentacleRetiredPackage{entry: entry, session: sessions[id]}
}
runtime.mutex.Lock()
disabled := runtime.disabled
runtime.mutex.Unlock()
pending[id] = startup.previous.Enabled && !disabled && ctx.Err() == nil
message += " The previous package was restored."
}
entries = next
}
if err != nil {
message += " The disabled state could not be saved durably: " + err.Error()
message += " The recovery state could not be saved durably: " + err.Error()
}
replyStartup(id, errors.New(message))
}
@@ -467,15 +534,26 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
}
}
if current == nil {
for id, entry := range removedPackages {
if sessions[id] != nil {
for directory, retired := range removedPackages {
entry := retired.entry
if retired.session != nil {
select {
case <-retired.session.Done():
default:
continue
}
}
if slices.ContainsFunc(entries, func(active tentacle.Entry) bool {
return active.PackageDirectory == directory
}) {
continue
}
if err := tentacle.RemovePackage(tentacle.PackageRoot(runtime.registry.Path), entry); err != nil {
runtime.logger.Warn("remove tentacle package", "error", err)
runtime.report("Tentacle was removed, but its package files could not be deleted.")
continue
}
delete(removedPackages, id)
delete(removedPackages, directory)
}
for _, entry := range entries {
if !pending[entry.ID] || sessions[entry.ID] != nil {
@@ -572,6 +650,9 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
call.result <- tentacle.ErrBusy
continue
}
if startup, exists := startups[call.request.ID]; exists && startup.previous != nil {
failStartup(call.request.ID, "Tentacle update was cancelled by another operation.")
}
next, err := change_tentacle_entries(entries, call.request)
if err == nil && call.request.Action != "retry" && reflect.DeepEqual(next, entries) {
// Saved enablement is intent, not proof of a live session. An
@@ -591,15 +672,28 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
}
if err == nil || errors.Is(err, tentacle.ErrRegistrySync) {
id := call.request.ID
var previous *tentacle.Entry
if call.packageFiles != nil && call.request.Action == "update" {
index := slices.IndexFunc(entries, func(entry tentacle.Entry) bool { return entry.ID == id })
if index >= 0 && entries[index].Enabled {
saved := tentacle.CloneEntries(entries[index : index+1])[0]
previous = &saved
}
}
replyStartup(id, errors.New("Tentacle startup was cancelled by another operation."))
if call.packageFiles != nil {
call.packageFiles.Retain()
}
if call.request.Action == "remove" {
if call.request.Action == "remove" || call.request.Action == "update" {
for _, entry := range entries {
if entry.ID == id && entry.PackageDirectory != "" {
removedPackages[id] = entry
if entry.ID != id || entry.PackageDirectory == "" {
continue
}
if call.request.Action == "update" && (previous != nil ||
entry.PackageDirectory == call.request.Entry.PackageDirectory) {
continue
}
removedPackages[entry.PackageDirectory] = tentacleRetiredPackage{entry: entry, session: sessions[id]}
}
}
revoke(id)
@@ -619,7 +713,7 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
runtime.publishCommands(stillActive)
setSnapshot()
if pending[id] {
startups[id] = tentacleStartupCall{call: call, saveError: err, stop: context.AfterFunc(call.ctx, notify)}
startups[id] = tentacleStartupCall{call: call, saveError: err, stop: context.AfterFunc(call.ctx, notify), previous: previous}
continue
}
}
@@ -665,6 +759,7 @@ func (runtime *tentacleRuntime) run_managed(ctx context.Context) error {
continue
}
active = candidate
delete(problems, session.TentacleID())
runtime.publishCommands(active)
replyStartup(session.TentacleID(), nil)
runtime.logger.Info("configured tentacle active", "tentacle", session.TentacleID(), "session", session.Identity())