// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package app import ( "context" "errors" "fmt" "slices" "time" "git.stormux.org/storm/cthulhu/internal/tentacle" ) // Preparation runs on the caller's worker. Its busy reservation remains owned // by Manage through the lifecycle reply, including rollback after cancellation. func (runtime *tentacleRuntime) beginRepositoryWork() bool { runtime.mutex.Lock() defer runtime.mutex.Unlock() if runtime.repositoryBusy { return false } runtime.repositoryBusy = true return true } func (runtime *tentacleRuntime) endRepositoryWork() { runtime.mutex.Lock() runtime.repositoryBusy = false runtime.mutex.Unlock() } func repositoryForInstallation(snapshot tentacle.Snapshot, request tentacle.ManagementRequest) (tentacle.Repository, error) { if request.Entry != nil { return tentacle.Repository{}, errors.New("unexpected tentacle entry in installation request") } if snapshot.Suppressed || snapshot.Message != "" { return tentacle.Repository{}, tentacle.ErrUnavailable } index := slices.IndexFunc(snapshot.Repositories, func(value tentacle.Repository) bool { return value.URL == request.URL }) if index < 0 { return tentacle.Repository{}, errors.New("repository is not configured") } repository := snapshot.Repositories[index] id := tentacle.PackageID(repository, request.ID) installed := slices.ContainsFunc(snapshot.Entries, func(value tentacle.EntryStatus) bool { return value.Entry.ID == id }) if request.Action == "upgrade" && !installed { return tentacle.Repository{}, errors.New("tentacle is not installed") } if request.Action != "upgrade" && installed { return tentacle.Repository{}, 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 tentacle.Repository{}, errors.New("tentacle is already at the repository version") } newer, err := tentacle.IsNewerVersion(manifest.Version, status.Entry.Version) if err != nil { return tentacle.Repository{}, fmt.Errorf("cannot compare tentacle versions: %w", err) } if !newer { return tentacle.Repository{}, errors.New("repository tentacle version is not newer than installed version") } break } break } } return repository, nil } func (runtime *tentacleRuntime) prepareManagedPackage(ctx context.Context, repository tentacle.Repository, id string) (*tentacle.PreparedPackage, error) { prepare := runtime.preparePackage if prepare == nil { prepare = tentacle.PreparePackage } prepareCtx, cancel := context.WithTimeout(ctx, 45*time.Second) stop := context.AfterFunc(runtime.lifecycleContext, cancel) prepared, err := prepare(prepareCtx, tentacle.PackageRoot(runtime.registry.Path), repository, id) stop() cancel() return prepared, err } func (runtime *tentacleRuntime) fetchManagedRepository(ctx context.Context, origin string) (tentacle.Repository, error) { fetch := runtime.fetchRepository if fetch == nil { fetch = tentacle.FetchRepository } // Fetch on the Preferences worker, leaving lifecycle and native input // free to disable, revoke, and process failures while the network waits. fetchCtx, cancel := context.WithTimeout(ctx, 45*time.Second) stop := context.AfterFunc(runtime.lifecycleContext, cancel) repository, err := fetch(fetchCtx, origin) stop() cancel() return repository, err }