package app import ( "context" "errors" "log/slog" "os" "path/filepath" "strings" "testing" "time" "git.stormux.org/storm/cthulhu/internal/config" "git.stormux.org/storm/cthulhu/internal/preferences" "git.stormux.org/storm/cthulhu/internal/tentacle" ) func managed_tentacle_fixture(t *testing.T, path string, contained bool) (*tentacleRuntime, *tentacleBindingRecorder) { t.Helper() bindings := &tentacleBindingRecorder{} runtime := newTentacleRuntime(slog.New(slog.DiscardHandler), bindings, "", "auto") runtime.registry = &tentacle.Registry{Path: path} runtime.requests = make(chan tentacleManagementCall, 16) if !contained { runtime.newManager = func() (*tentacle.Manager, error) { return tentacle.NewManager(tentacle.DefaultLimits(), nil) } } t.Cleanup(func() { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := runtime.Stop(ctx); err != nil { t.Error(err) } }) return runtime, bindings } func manage_tentacle(t *testing.T, runtime *tentacleRuntime, action, id string, entry *tentacle.Entry) { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second) defer cancel() if err := runtime.Manage(ctx, tentacle.ManagementRequest{Action: action, ID: id, Entry: entry}); err != nil { t.Fatalf("%s: %v", action, err) } } func probe_entry(t *testing.T, id, mode string) tentacle.Entry { t.Helper() executable, err := os.Executable() if err != nil { t.Fatal(err) } return tentacle.Entry{ID: id, Program: []string{executable, "-test.run=^TestTentacleReaderPeer$", "--", "reader-peer", filepath.Join(t.TempDir(), mode)}} } func wait_managed_state(t *testing.T, runtime *tentacleRuntime, id, state string) { t.Helper() waitTentacle(t, func() bool { for _, status := range runtime.Snapshot().Entries { if status.Entry.ID == id { return status.State == state } } return false }) } func TestManagedTentaclePersistenceLifecycleAndRecovery(t *testing.T) { path := filepath.Join(t.TempDir(), "tentacles.toml") runtime, bindings := managed_tentacle_fixture(t, path, false) if err := runtime.Start(context.Background()); err != nil { t.Fatal(err) } first := probe_entry(t, "first", "called") second := probe_entry(t, "second", "called") manage_tentacle(t, runtime, "add", first.ID, &first) manage_tentacle(t, runtime, "add", second.ID, &second) if len(bindings.TentacleBindings()) != 0 { t.Fatal("adding disabled entries launched them") } manage_tentacle(t, runtime, "enable", first.ID, nil) wait_managed_state(t, runtime, first.ID, "active") firstCommand := bindings.TentacleBindings()[0].Command manage_tentacle(t, runtime, "enable", first.ID, nil) if bindings.TentacleBindings()[0].Command != firstCommand { t.Fatal("enable of active entry restarted it") } manage_tentacle(t, runtime, "enable", second.ID, nil) wait_managed_state(t, runtime, second.ID, "active") manage_tentacle(t, runtime, "disable", first.ID, nil) wait_managed_state(t, runtime, first.ID, "disabled") waitTentacle(t, func() bool { bound := bindings.TentacleBindings() return len(bound) > 0 && bound[0].Command != firstCommand }) runtime.Invoke(context.Background(), firstCommand) if _, err := os.Stat(first.Program[len(first.Program)-1]); !os.IsNotExist(err) { t.Fatal("stale command invoked disabled child") } secondCommand := bindings.TentacleBindings()[0].Command manage_tentacle(t, runtime, "retry", second.ID, nil) wait_managed_state(t, runtime, second.ID, "active") if bindings.TentacleBindings()[0].Command == secondCommand { t.Fatal("retry reused old session identity") } loaded, err := (&tentacle.Registry{Path: path}).Load() if err != nil || len(loaded) != 2 || loaded[0].Enabled || !loaded[1].Enabled { t.Fatalf("persisted state: %v %v", loaded, err) } runtime.Disable() if err := runtime.Manage(context.Background(), tentacle.ManagementRequest{Action: "enable", ID: first.ID}); !errors.Is(err, tentacle.ErrDisabled) { t.Fatalf("emergency bypass: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := runtime.Stop(ctx); err != nil { t.Fatal(err) } restarted, _ := managed_tentacle_fixture(t, path, false) if err := restarted.Start(context.Background()); err != nil { t.Fatal(err) } wait_managed_state(t, restarted, second.ID, "active") wait_managed_state(t, restarted, first.ID, "disabled") manage_tentacle(t, restarted, "remove", first.ID, nil) manage_tentacle(t, restarted, "remove", second.ID, nil) loaded, err = (&tentacle.Registry{Path: path}).Load() if err != nil || len(loaded) != 0 { t.Fatalf("remove all: %v %v", loaded, err) } if _, err := os.Stat(second.Program[0]); err != nil { t.Fatal("removing configuration deleted the executable") } } func TestManagedTentacleFailureRetryAndSaveFailure(t *testing.T) { path := filepath.Join(t.TempDir(), "tentacles.toml") runtime, bindings := managed_tentacle_fixture(t, path, false) if err := runtime.Start(context.Background()); err != nil { t.Fatal(err) } entry := probe_entry(t, "probe", "crash") entry.Enabled = true manage_tentacle(t, runtime, "add", entry.ID, &entry) wait_managed_state(t, runtime, entry.ID, "active") runtime.Invoke(context.Background(), bindings.TentacleBindings()[0].Command) wait_managed_state(t, runtime, entry.ID, "failed") if !runtime.Snapshot().Entries[0].Entry.Enabled { t.Fatal("failure changed desired enablement") } manage_tentacle(t, runtime, "retry", entry.ID, nil) wait_managed_state(t, runtime, entry.ID, "active") entry.Program[len(entry.Program)-1] = filepath.Join(t.TempDir(), "called") entry.Presentation = true manage_tentacle(t, runtime, "update", entry.ID, &entry) wait_managed_state(t, runtime, entry.ID, "active") if !runtime.Snapshot().Entries[0].Entry.Presentation { t.Fatal("grant lost") } if err := os.WriteFile(path, []byte("version = 1\n"), 0600); err != nil { t.Fatal(err) } if err := runtime.Manage(context.Background(), tentacle.ManagementRequest{Action: "disable", ID: entry.ID}); err == nil { t.Fatal("overwrote external edits") } if runtime.Snapshot().Entries[0].State != "active" { t.Fatal("failed persistence changed runtime") } } func TestManagedTentacleEmptyAndBrokenRegistryDoNotLaunch(t *testing.T) { for _, broken := range []bool{false, true} { path := filepath.Join(t.TempDir(), "tentacles.toml") if broken { if err := os.WriteFile(path, []byte("invalid"), 0600); err != nil { t.Fatal(err) } } runtime, _ := managed_tentacle_fixture(t, path, false) runtime.newManager = func() (*tentacle.Manager, error) { t.Error("empty/broken registry initialized host") return nil, tentacle.ErrUnavailable } if err := runtime.Start(context.Background()); err != nil { t.Fatal(err) } entry := probe_entry(t, "probe", "called") err := runtime.Manage(context.Background(), tentacle.ManagementRequest{Action: "add", ID: entry.ID, Entry: &entry}) if (err != nil) != broken { t.Fatalf("broken=%v: %v", broken, err) } if len(runtime.sessions) != 0 { t.Fatal("launched unexpected child") } } } func TestPreferencesTentacleOperationsAreGlobalAndSeparate(t *testing.T) { runtime, _ := managed_tentacle_fixture(t, filepath.Join(t.TempDir(), "tentacles.toml"), false) if err := runtime.Start(context.Background()); err != nil { t.Fatal(err) } worker := newPreferencesWorker(slog.New(slog.DiscardHandler), config.DefaultSettings(), nil) worker.tentacles = runtime entry := probe_entry(t, "probe", "called") request := preferences.Request{Tentacle: &tentacle.ManagementRequest{Action: "add", ID: entry.ID, Entry: &entry}} worker.editApp = "Example" if response := worker.manage_tentacle(context.Background(), request); response.Accepted { t.Fatal("application preferences added a program") } worker.editApp = "" request.Save = true if response := worker.manage_tentacle(context.Background(), request); response.Accepted { t.Fatal("mixed settings request accepted") } request.Save = false if response := worker.manage_tentacle(context.Background(), request); !response.Accepted || response.Tentacles == nil || len(response.Tentacles.Entries) != 1 { t.Fatalf("add: %+v", response) } } func TestManagedTentacleGuardianPeer(t *testing.T) { if len(os.Args) < 3 || os.Args[len(os.Args)-2] != "managed-guardian" { return } if err := tentacle.RunGuardian(os.Stdin, os.Stdout, os.Args[len(os.Args)-1]); err != nil { os.Exit(1) } os.Exit(0) } func TestNativeManagedTentacle(t *testing.T) { if os.Getenv("CTHULHU_TENTACLE_MANAGED") != "1" { t.Skip("requires a systemd user manager and cgroup v2") } runtime, bindings := managed_tentacle_fixture(t, filepath.Join(t.TempDir(), "tentacles.toml"), true) readerGroup, err := os.ReadFile("/proc/self/cgroup") if err != nil { t.Fatal(err) } runtime.logger = slog.New(slog.NewTextHandler(os.Stderr, nil)) defer func() { t.Logf("final status: %+v", runtime.Snapshot()) }() executable, _ := os.Executable() runtime.guardianProgram = []string{executable, "-test.run=^TestManagedTentacleGuardianPeer$", "--", "managed-guardian"} if binary := os.Getenv("CTHULHU_TENTACLE_GUARDIAN_BINARY"); binary != "" { runtime.guardianProgram = []string{binary, "--tentacle-guardian"} } if err := runtime.Start(context.Background()); err != nil { t.Fatal(err) } entry := probe_entry(t, "probe", "called") entry.Enabled = true manage_tentacle(t, runtime, "add", entry.ID, &entry) wait_managed_state(t, runtime, entry.ID, "active") runtime.Invoke(context.Background(), bindings.TentacleBindings()[0].Command) marker := entry.Program[len(entry.Program)-1] waitTentacle(t, func() bool { _, err := os.Stat(marker); return err == nil }) membership, err := os.ReadFile(marker) if err != nil { t.Fatal(err) } childGroup := filepath.Join("/sys/fs/cgroup", strings.TrimSpace(strings.TrimPrefix(string(membership), "0::"))) for name, want := range map[string]string{"cpu.max": "25000 100000", "memory.max": "268435456", "pids.max": "64"} { value, err := os.ReadFile(filepath.Join(childGroup, name)) if err != nil || strings.TrimSpace(string(value)) != want { t.Fatalf("%s: %q %v", name, value, err) } } afterGroup, err := os.ReadFile("/proc/self/cgroup") if err != nil || string(readerGroup) != string(afterGroup) { t.Fatal("management moved the reader") } manage_tentacle(t, runtime, "disable", entry.ID, nil) waitTentacle(t, func() bool { return len(bindings.TentacleBindings()) == 0 }) manage_tentacle(t, runtime, "enable", entry.ID, nil) wait_managed_state(t, runtime, entry.ID, "active") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := runtime.Stop(ctx); err != nil { t.Fatal(err) } serviceGroup := filepath.Dir(filepath.Dir(childGroup)) waitTentacle(t, func() bool { _, err := os.Stat(serviceGroup); return os.IsNotExist(err) }) }