From 30956aabd27f2906b478005dca4e4edbf31b4980 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Sat, 17 Oct 2026 11:41:18 +0200 Subject: [PATCH] R-977: a new FileBrowser run is given ~20 s to answer in the same tick rc.1 on 9202: the tick that brought FileBrowser back probed it while it was starting; the default login stayed open for one 5-minute round. TestFileBrowserAdmin_R977_NewRunIsWaitedForInTheSameTick, red-proved. Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- CHANGELOG.md | 6 ++-- .../internal/stacks/filebrowser_password.go | 30 +++++++++++++++- .../stacks/filebrowser_password_test.go | 34 +++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c301fee..7352086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,10 @@ (the container's StartedAt), not once per box. Measured first on scratch 9202 with 0.324.0: FileBrowser and its data volume removed, the box brought it back on its own — the default login answered 200 while the box kept its old „generated" record. Now such a run is reset at its first tick (a generated password, verified both ways, stored - encrypted); a run on any other password — the household's own — changes nothing. `TestFileBrowserAdmin_R977_*`, - red-proved (the old once-per-box return keeps the default). The code comment „before a claim there is no tunnel" + encrypted); a run on any other password — the household's own — changes nothing. A NEW run is given ~20 s to answer + in the same tick (rc.1 on 9202: the tick that brought FileBrowser back probed it while it was starting, and the + default stayed open for one whole 5-minute round). `TestFileBrowserAdmin_R977_*` (2), each red-proved (the old + once-per-box return keeps the default; one try per tick leaves it open). The code comment „before a claim there is no tunnel" was false (the claim is reached through the tunnel) and is corrected; the tunnel itself is NOT changed — R-1120. - **R-1116** — the decoy suite is a gate of the full run (below, the helper's note). diff --git a/controller/internal/stacks/filebrowser_password.go b/controller/internal/stacks/filebrowser_password.go index 81ad87f..b91904d 100644 --- a/controller/internal/stacks/filebrowser_password.go +++ b/controller/internal/stacks/filebrowser_password.go @@ -92,6 +92,15 @@ func (m *Manager) fbStartedAt() string { return strings.TrimSpace(string(out)) } +// fbProbeWait is the pause between two probes of a new FileBrowser run: 2 s in production, 1 ms against a test +// FileBrowser. +func (m *Manager) fbProbeWait() time.Duration { + if m.fbHTTP != nil || m.fbBaseURL != "" { + return time.Millisecond + } + return 2 * time.Second +} + // EnsureFileBrowserAdminPassword probes FileBrowser's default login once per FileBrowser run (R-977) and replaces it // when it works. Safe to call every tick: a run already probed returns at once. Returns an error only for logging. func (m *Manager) EnsureFileBrowserAdminPassword() error { @@ -110,7 +119,26 @@ func (m *Manager) EnsureFileBrowserAdminPassword() error { } now := time.Now().UTC().Format(time.RFC3339) - token, code, err := fbLogin(do, base, "admin") + // A NEW run is usually seconds old — the same tick that brought FileBrowser back calls this right after — so it is + // given ~20 s to answer before the probe gives up until the next tick (measured on 9202 2026-10-17: without the + // wait, a re-created FileBrowser kept its default login for one whole base-stack round, 5 minutes). A run already + // probed, or one that cannot be told, gets one try, as before. + tries := 1 + if run != "" && run != m.fbProbedRun { + tries = 10 + } + var token string + var code int + var err error + for i := 0; i < tries; i++ { + token, code, err = fbLogin(do, base, "admin") + if err == nil && (code == http.StatusOK || code == http.StatusUnauthorized || code == http.StatusForbidden) { + break + } + if i+1 < tries { + time.Sleep(m.fbProbeWait()) + } + } if err != nil { return fmt.Errorf("filebrowser admin probe: %w (will retry)", err) } diff --git a/controller/internal/stacks/filebrowser_password_test.go b/controller/internal/stacks/filebrowser_password_test.go index a1984ac..f643a12 100644 --- a/controller/internal/stacks/filebrowser_password_test.go +++ b/controller/internal/stacks/filebrowser_password_test.go @@ -210,3 +210,37 @@ func TestFileBrowserAdmin_R977_DefaultBackOnANewRunIsReplaced(t *testing.T) { t.Fatalf("the recorded decision moved on a run that is not on the default: state=%q", s) } } + +// R-977, measured live: the tick that brings FileBrowser back probes it while it is still starting, and without a wait +// the default login stayed open until the next round (5 minutes on 9202). A NEW run is given a short wait. +// RED-PROOF (2026-10-17): with one try per tick this fails at „the default stayed open". +func TestFileBrowserAdmin_R977_NewRunIsWaitedForInTheSameTick(t *testing.T) { + fb := &fakeFB{password: "admin"} + starting := 3 // the first three answers: FileBrowser not ready yet + inner := fb.handler() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fb.mu.Lock() + notReady := starting > 0 + if notReady { + starting-- + } + fb.mu.Unlock() + if notReady { + w.WriteHeader(http.StatusBadGateway) + return + } + inner.ServeHTTP(w, r) + })) + defer srv.Close() + m, st, _ := fbManager(t, srv.URL) + m.fbStarted = func() string { return "2026-10-17T09:24:35Z" } + if err := m.EnsureFileBrowserAdminPassword(); err != nil { + t.Fatalf("one tick: %v", err) + } + if _, c, _ := fbLogin(srv.Client().Do, srv.URL, "admin"); c == http.StatusOK { + t.Fatal("the default stayed open after the tick that started FileBrowser") + } + if s, _, _ := st.GetFileBrowserAdmin(); s != settings.FileBrowserAdminGenerated { + t.Fatalf("state %q, want generated", s) + } +}