diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-17 14:11:52 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-09-17 14:11:52 +0200 |
| commit | ed86f44a926fd1f0d438cbe5e2e10ad5b063db55 (patch) | |
| tree | a0a4c53a9ed5f207b303623e4dfd3cac16ed302a /gui/internal/model/plan.go | |
| parent | 4c6fadfab5434317357ad0272ace7927d2945942 (diff) | |
| download | krino-ed86f44a926fd1f0d438cbe5e2e10ad5b063db55.tar.gz krino-ed86f44a926fd1f0d438cbe5e2e10ad5b063db55.zip | |
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.
Diffstat (limited to 'gui/internal/model/plan.go')
| -rw-r--r-- | gui/internal/model/plan.go | 25 |
1 files changed, 25 insertions, 0 deletions
diff --git a/gui/internal/model/plan.go b/gui/internal/model/plan.go index ccbf17b..c3e9654 100644 --- a/gui/internal/model/plan.go +++ b/gui/internal/model/plan.go @@ -7,6 +7,7 @@ package model import ( "context" + "errors" "fmt" "sort" "time" @@ -50,8 +51,24 @@ type PlanTab struct { sess *engine.Session dp *engine.DirPlan lock *lock.Lock + + // applying is set while Apply is in flight. Close must refuse then: + // releasing the lock and closing the log under a running apply moves + // files the log never records, so undo cannot see them, and unlocks a + // directory krino is still working in. The window can reach Close from + // several places while an apply runs - saving rules or settings, adding + // a directory, closing the window - so the refusal lives here rather + // than in whichever of them remembers. + applying bool + + // testBeforeApply, when set, is called just before the engine's Apply + // begins, so a test can act while the apply is in flight. + testBeforeApply func() } +// ErrApplying is returned by Close while an apply is still running. +var ErrApplying = errors.New("krino is still applying this plan") + // Counts is the plan's summary line. type Counts struct { Scanned, Acting, Excluded, Skipped, Unmatched, Warned int @@ -95,6 +112,9 @@ func (t *PlanTab) Run() string { return t.sess.Run() } // Close releases the directory's lock and ends the run, which Apply also // does once the plan is history. Closing twice is not an error. func (t *PlanTab) Close() error { + if t.applying { + return ErrApplying + } if t.lock == nil { return nil } @@ -303,7 +323,12 @@ func (t *PlanTab) Apply(ctx context.Context) (*engine.ApplyResult, error) { approved[r.Rel] = true } } + t.applying = true + if t.testBeforeApply != nil { + t.testBeforeApply() + } res, err := t.sess.Apply(ctx, t.dp, approved) + t.applying = false t.Applied = true // An applied plan is history: the run is over and the directory free // again, without closing the window (GUI design ยง3). |
