From ed86f44a926fd1f0d438cbe5e2e10ad5b063db55 Mon Sep 17 00:00:00 2001 From: Lukasz Kasprzak Date: Thu, 17 Sep 2026 14:11:52 +0200 Subject: the window cannot pull the lock out from under a running apply Close releases the directory lock and closes the log. The window could reach it while an apply was still running - saving rules, saving settings, adding a directory, or closing the window - and the engine then went on moving files with the log shut underneath: a file moved that no krino undo can see, the rest of the plan silently abandoned, and the directory unlocked while krino was still working in it. PlanTab and UndoTab refuse to close while their apply is in flight (model.ErrApplying), the window's close request and reloadEngine honour the refusal instead of ignoring it, and the tabs and Settings are greyed out for the duration so a button that cannot work says so by being unavailable rather than by an error afterwards. The test starts an apply, calls Close from another goroutine while it is in flight, and requires the refusal. --- gui/internal/model/plan_test.go | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) (limited to 'gui/internal/model/plan_test.go') diff --git a/gui/internal/model/plan_test.go b/gui/internal/model/plan_test.go index a36cf85..1bae2c5 100644 --- a/gui/internal/model/plan_test.go +++ b/gui/internal/model/plan_test.go @@ -462,3 +462,46 @@ func TestKeepThisCopyRefusesANonDuplicate(t *testing.T) { t.Error("a row that does not exist was accepted") } } + +// TestCloseDuringApplyIsRefused: Close releases the directory lock and +// closes the log. Called while an apply is running - which the window +// allows: saving rules, saving settings, adding a directory and closing the +// window all reach it - the engine goes on moving files with the log shut +// under it, so a file is moved that no krino undo can see, and the +// directory is unlocked while krino is still working in it. +func TestCloseDuringApplyIsRefused(t *testing.T) { + conf := "(path \"~/dl\")\n(rule \"pdfs\" (when (type pdf)) (move \"Docs\"))\n" + e, _ := sandboxDir(t, conf, map[string]string{"a.pdf": "one", "b.pdf": "two"}) + tab, err := Plan(context.Background(), e, e.Dirs[0]) + if err != nil { + t.Fatal(err) + } + for i := range tab.Rows { + tab.Rows[i].Selected = true + } + + started := make(chan struct{}) + done := make(chan struct{}) + tab.testBeforeApply = func() { + close(started) + // Hold the apply open while Close is attempted. + <-done + } + var applyErr error + go func() { + _, applyErr = tab.Apply(context.Background()) + }() + <-started + + if err := tab.Close(); err == nil { + t.Error("Close during an apply was allowed: the lock and the log go out from under it") + } + close(done) + // Let the apply finish before the sandbox is torn down. + for i := 0; i < 200 && !tab.Applied; i++ { + time.Sleep(10 * time.Millisecond) + } + if applyErr != nil { + t.Errorf("the apply itself failed: %v", applyErr) + } +} -- cgit v1.3