From 48a6584baf3896fd2fb6531fa4130918f0e568f4 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Thu, 16 Apr 2026 17:30:10 +0100 Subject: [PATCH] fstest/testserver: add CleanupAll for end-of-run server sweep Cleanup today is entirely per-goroutine via the stop closure that Start returns. If the driver process is killed or panics, those deferred stops never run and the underlying container keeps running. Track every remote Start has brought up in a process-local map, and expose CleanupAll which force-stops each tracked remote via the new run.bash "force-stop" verb. The returned stop closure is now sync.Once-wrapped so it and CleanupAll can both fire harmlessly. No callers yet; wired up in fstest/test_all in a follow-up commit. --- fstest/testserver/testserver.go | 50 +++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/fstest/testserver/testserver.go b/fstest/testserver/testserver.go index 9967bf546..687078fe6 100644 --- a/fstest/testserver/testserver.go +++ b/fstest/testserver/testserver.go @@ -21,6 +21,13 @@ import ( var ( findConfigOnce sync.Once configDir string // where the config is stored + + // trackedServers counts how many live Start/stop pairs this + // process holds for each remote. Used by CleanupAll to force-stop + // everything this process started even if individual stop + // functions never ran (e.g. on signal, panic, fs.Fatalf). + trackedMu sync.Mutex + trackedServers = map[string]int{} ) // Assume we are run somewhere within the rclone root @@ -190,9 +197,48 @@ func Start(remote string) (fn func(), err error) { return nil, err } - // And return a function to stop it + trackedMu.Lock() + trackedServers[name]++ + trackedMu.Unlock() + + // And return a function to stop it. The returned closure is + // idempotent: calling it twice (e.g. by defer and by CleanupAll) + // only decrements once. + var once sync.Once return func() { - stop(name) + once.Do(func() { + trackedMu.Lock() + if trackedServers[name] > 0 { + trackedServers[name]-- + } + trackedMu.Unlock() + stop(name) + }) }, nil } + +// CleanupAll force-stops every server this process ever started via +// Start, regardless of refcount. Safe to call multiple times and from +// any goroutine. Intended for end-of-run or signal-handler cleanup in +// long-running test drivers like fstest/test_all. +func CleanupAll() { + trackedMu.Lock() + names := make([]string, 0, len(trackedServers)) + for name, count := range trackedServers { + if count > 0 { + names = append(names, name) + } + } + for _, name := range names { + trackedServers[name] = 0 + } + trackedMu.Unlock() + + for _, name := range names { + fs.Logf(name, "Force-stopping test server") + if _, err := run(name, "force-stop"); err != nil { + fs.Errorf(name, "Failed to force-stop test server: %v", err) + } + } +}