// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "context" "errors" "reflect" "slices" "strings" "git.stormux.org/storm/cthulhu/internal/tentacle" ) func (owner *tentacleLifecycle) handleRequest(call tentacleManagementCall) { if err := call.ctx.Err(); err != nil { call.result <- err return } if owner.loadError != nil { call.result <- errors.New("tentacle registry could not be loaded; repair it and restart") return } owner.runtime.mutex.Lock() disabled := owner.runtime.disabled owner.runtime.mutex.Unlock() if disabled { call.result <- tentacle.ErrDisabled return } if strings.HasPrefix(call.request.Action, "repository-") { next, err := change_tentacle_repositories(owner.repositories, call.request, call.repository) if err == nil { err = owner.runtime.registry.SaveState(owner.entries, next) } if err == nil || errors.Is(err, tentacle.ErrRegistrySync) { owner.repositories = next owner.setSnapshot() } call.result <- err return } if call.packageFiles != nil { index := slices.IndexFunc(owner.repositories, func(value tentacle.Repository) bool { return value.URL == call.repository.URL }) if index < 0 || !reflect.DeepEqual(owner.repositories[index], *call.repository) { call.result <- errors.New("Repository changed during installation; refresh and try again.") return } } if _, starting := owner.startups[call.request.ID]; starting && call.request.Action != "disable" && call.request.Action != "remove" { call.result <- tentacle.ErrBusy return } if startup, exists := owner.startups[call.request.ID]; exists && startup.previous != nil { owner.failStartup(call.request.ID, "Tentacle update was cancelled by another operation.") } next, err := change_tentacle_entries(owner.entries, call.request) if err == nil && call.request.Action != "retry" && reflect.DeepEqual(next, owner.entries) { // Saved enablement is intent, not proof of a live session. An // explicit enable/update must recover a failed or starting entry. index := slices.IndexFunc(next, func(entry tentacle.Entry) bool { return entry.ID == call.request.ID }) state := tentacle.Stopped if session := owner.sessions[call.request.ID]; session != nil { state, _ = session.Status() } if !next[index].Enabled || state == tentacle.Active { call.result <- nil return } } if err == nil && call.request.Action != "retry" { err = owner.runtime.registry.Save(next) } 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(owner.entries, func(entry tentacle.Entry) bool { return entry.ID == id }) if index >= 0 && owner.entries[index].Enabled { saved := tentacle.CloneEntries(owner.entries[index : index+1])[0] previous = &saved } } owner.replyStartup(id, errors.New("Tentacle startup was cancelled by another operation.")) if call.packageFiles != nil { call.packageFiles.Retain() } if call.request.Action == "remove" || call.request.Action == "update" { for _, entry := range owner.entries { if entry.ID != id || entry.PackageDirectory == "" { continue } if call.request.Action == "update" && (previous != nil || entry.PackageDirectory == call.request.Entry.PackageDirectory) { continue } owner.removedPackages[entry.PackageDirectory] = tentacleRetiredPackage{entry: entry, session: owner.sessions[id]} } } owner.revoke(id) if owner.current != nil && owner.current.TentacleID() == id { owner.current, owner.ready = nil, nil } owner.entries = next delete(owner.problems, id) delete(owner.pending, id) for _, entry := range owner.entries { if entry.ID == id { owner.pending[id] = entry.Enabled } } // Immediately withdraw dispatch, without waiting for key release. stillActive := slices.DeleteFunc(slices.Clone(owner.active), func(session *tentacle.Session) bool { state, _ := session.Status(); return state != tentacle.Active }) owner.runtime.publishCommands(stillActive) owner.setSnapshot() if owner.pending[id] { owner.startups[id] = tentacleStartupCall{call: call, saveError: err, stop: context.AfterFunc(call.ctx, owner.notify), previous: previous} return } } call.result <- err }