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/history.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/history.go')
| -rw-r--r-- | gui/internal/model/history.go | 9 |
1 files changed, 9 insertions, 0 deletions
diff --git a/gui/internal/model/history.go b/gui/internal/model/history.go index 7c166bb..87ab6e7 100644 --- a/gui/internal/model/history.go +++ b/gui/internal/model/history.go @@ -114,6 +114,10 @@ type UndoTab struct { sess *engine.Session up *engine.UndoPlan locks []*lock.Lock + + // applying is set while the reversal is in flight; Close refuses then, + // for the reason PlanTab.applying gives. + applying bool } // PlanUndo builds the reversal of runID and locks every directory it @@ -208,6 +212,8 @@ func (t *UndoTab) SelectedCount() int { // said no to (spec ยง9). Refused files ride along unchanged, as they do on // the command line. The locks are released afterwards: the plan is history. func (t *UndoTab) Apply(ctx context.Context) (*engine.ApplyResult, error) { + t.applying = true + defer func() { t.applying = false }() toApply := &engine.UndoPlan{Run: t.up.Run, Cleanup: t.up.Cleanup} for i, f := range t.up.Files { if f.Refused == "" && !t.Rows[i].Selected { @@ -266,6 +272,9 @@ func undoOutcome(row UndoRow, fr engine.FileResult) string { // does. One lock's failure never stops the rest from being released, or the // session from being closed. Closing twice is not an error. func (t *UndoTab) Close() error { + if t.applying { + return ErrApplying + } var first error for _, l := range t.locks { if err := l.Release(); err != nil && first == nil { |
