diff options
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/krino/history_test.go | 39 | ||||
| -rw-r--r-- | cmd/krino/matching_test.go | 14 | ||||
| -rw-r--r-- | cmd/krino/sort.go | 17 |
3 files changed, 58 insertions, 12 deletions
diff --git a/cmd/krino/history_test.go b/cmd/krino/history_test.go index ad7f6f6..6277e7b 100644 --- a/cmd/krino/history_test.go +++ b/cmd/krino/history_test.go @@ -5,7 +5,6 @@ package main import ( "bytes" "context" - "fmt" "os" "path/filepath" "strings" @@ -14,6 +13,7 @@ import ( "git.labunix.xyz/krino/internal/engine" "git.labunix.xyz/krino/internal/journal" + "git.labunix.xyz/krino/internal/lock" ) func TestLogListsRunsAndUndoReverses(t *testing.T) { @@ -101,12 +101,13 @@ func TestUndoFailsImmediatelyWithHeldLock(t *testing.T) { } held := filepath.Join(h, ".local", "state", "krino", "dl.lock") - if err := os.MkdirAll(filepath.Dir(held), 0o755); err != nil { - t.Fatal(err) - } - if err := os.WriteFile(held, []byte(fmt.Sprintf("pid %d\n", os.Getpid())), 0o644); err != nil { + // A real lock, not a file that looks like one: the lock is the + // kernel's, so holding it means holding a descriptor. + l, err := lock.Acquire(context.Background(), held, false) + if err != nil { t.Fatal(err) } + defer l.Release() code, out, errOut := runCLI(t, "undo", "-y") if code != 1 || !strings.Contains(errOut, "another krino") { @@ -560,3 +561,31 @@ func TestIgnoredGlobalFlagsAreRefused(t *testing.T) { t.Errorf("log -n 3 (its own count flag) was refused: %d %q", code, errOut) } } + +// TestWaitingForALockSaysSo: every run but -y waits for a held lock, which +// the spec intends (§3, §11) - but waiting silently is indistinguishable +// from a hang, and the wait has no timeout. A run about to wait must say +// what it is waiting for before it blocks. +func TestWaitingForALockSaysSo(t *testing.T) { + h := matchingFixture(t) + held := filepath.Join(h, ".local", "state", "krino", "dl.lock") + l, err := lock.Acquire(context.Background(), held, false) + if err != nil { + t.Fatal(err) + } + go func() { + time.Sleep(250 * time.Millisecond) + l.Release() + }() + + code, _, errOut := runCLI(t, "-n") + if code != 0 { + t.Fatalf("dry run after the lock was released: %d %s", code, errOut) + } + if !strings.Contains(errOut, "waiting") { + t.Errorf("a run that waited for the lock said nothing about it:\n%q", errOut) + } + if !strings.Contains(errOut, held) { + t.Errorf("the notice does not name the lock file %q:\n%q", held, errOut) + } +} diff --git a/cmd/krino/matching_test.go b/cmd/krino/matching_test.go index 4ce2205..6bdc994 100644 --- a/cmd/krino/matching_test.go +++ b/cmd/krino/matching_test.go @@ -4,13 +4,15 @@ package main import ( "bytes" + "context" "encoding/json" - "fmt" "os" "path/filepath" "strings" "testing" "time" + + "git.labunix.xyz/krino/internal/lock" ) const dlRules = ` @@ -412,13 +414,13 @@ func TestRefusesWithoutTerminalAndWithoutFlags(t *testing.T) { func TestSecondRunFailsImmediatelyWithYes(t *testing.T) { h := matchingFixture(t) held := filepath.Join(h, ".local", "state", "krino", "dl.lock") - if err := os.MkdirAll(filepath.Dir(held), 0o755); err != nil { - t.Fatal(err) - } - // A lock held by this very process, so it is not stale. - if err := os.WriteFile(held, []byte(fmt.Sprintf("pid %d\n", os.Getpid())), 0o644); err != nil { + // A lock really held: it is the kernel's, so a file that merely looks + // like one holds nothing. + l, err := lock.Acquire(context.Background(), held, false) + if err != nil { t.Fatal(err) } + defer l.Release() code, _, errOut := runCLI(t, "-y") if code != 1 || !strings.Contains(errOut, "another krino") { t.Errorf("-y against a held lock: %d %q", code, errOut) diff --git a/cmd/krino/sort.go b/cmd/krino/sort.go index 35d8123..a381239 100644 --- a/cmd/krino/sort.go +++ b/cmd/krino/sort.go @@ -18,6 +18,7 @@ import ( "golang.org/x/term" "git.labunix.xyz/krino/internal/engine" + "git.labunix.xyz/krino/internal/lock" "git.labunix.xyz/krino/internal/plan" "git.labunix.xyz/krino/internal/scan" "git.labunix.xyz/krino/internal/xdg" @@ -126,7 +127,7 @@ func cmdSort(g *globals, names []string, stdout, stderr io.Writer) int { // behind a stuck run. lock.Acquire takes ctx precisely so that wait // is not unbounded in practice: a signal cancels it and Acquire // returns ctx.Err() promptly instead of polling forever. - l, err := sess.Lock(ctx, d, !g.yes) + l, err := lockDir(ctx, sess, d, !g.yes, stderr) if err != nil { if interrupted(err) { // Interrupted while waiting for the lock: an interrupt, not @@ -602,3 +603,17 @@ func interrupted(err error) bool { func stopAfterApply(action rune, err error) bool { return interrupted(err) || action == 'w' } + +// lockDir takes d's lock, saying out loud what a silent wait would hide. +// The lock is tried without waiting first: when it is held and this run may +// wait, the holder is named before the wait begins, so a run that is +// waiting does not look like a run that has hung - there is no timeout on +// the wait, and until it prints something the two are indistinguishable. +func lockDir(ctx context.Context, sess *engine.Session, d *engine.Dir, wait bool, stderr io.Writer) (*lock.Lock, error) { + l, err := sess.Lock(ctx, d, false) + if wait && errors.Is(err, lock.ErrHeld) { + fmt.Fprintf(stderr, "krino: %s: %v; waiting\n", d.Name, err) + return sess.Lock(ctx, d, true) + } + return l, err +} |
