diff --git a/internal/duplicate/duplicate.go b/internal/duplicate/duplicate.go index bbf1e74..a7ad4fe 100644 --- a/internal/duplicate/duplicate.go +++ b/internal/duplicate/duplicate.go @@ -18,6 +18,7 @@ import ( "sync/atomic" "github.com/andydixon/mapsize/internal/inventory" + "github.com/andydixon/mapsize/internal/platform" ) // Group is a set of files verified to have identical content. @@ -226,7 +227,7 @@ func (f *Finder) splitHash(ctx context.Context, g []cand, workers int) []fullGro // open opens a candidate and checks it still has the inventoried size. func open(c cand) (*os.File, bool) { - fh, err := os.Open(c.path) + fh, err := platform.OpenContent(c.path) if err != nil { return nil, false } diff --git a/internal/duplicate/fifo_unix_test.go b/internal/duplicate/fifo_unix_test.go new file mode 100644 index 0000000..fd5b515 --- /dev/null +++ b/internal/duplicate/fifo_unix_test.go @@ -0,0 +1,51 @@ +//go:build unix + +package duplicate + +import ( + "context" + "os" + "path/filepath" + "syscall" + "testing" + "time" + + "github.com/andydixon/mapsize/internal/scan" +) + +// A candidate replaced by a FIFO (or a symlink to one) after the scan must +// be skipped, not block the search forever in open(2). +func TestFindSkipsSwappedFIFO(t *testing.T) { + root := t.TempDir() + for _, n := range []string{"a", "b", "c"} { + os.WriteFile(filepath.Join(root, n), []byte("same"), 0o644) + } + s, err := scan.Start(context.Background(), root, scan.Options{Workers: 1}) + if err != nil { + t.Fatal(err) + } + <-s.Done() + os.Remove(filepath.Join(root, "b")) + if err := syscall.Mkfifo(filepath.Join(root, "b"), 0o644); err != nil { + t.Skip("mkfifo:", err) + } + fifo2 := filepath.Join(t.TempDir(), "p") + syscall.Mkfifo(fifo2, 0o644) + os.Remove(filepath.Join(root, "c")) + os.Symlink(fifo2, filepath.Join(root, "c")) + + done := make(chan error, 1) + var f Finder + go func() { _, err := f.Find(context.Background(), s.Tree, Options{}); done <- err }() + select { + case err := <-done: + if err != nil { + t.Fatal(err) + } + case <-time.After(5 * time.Second): + t.Fatal("duplicate search blocked on a FIFO") + } + if p := f.Progress(); p.Skipped != 2 { + t.Fatalf("skipped=%d want 2", p.Skipped) + } +} diff --git a/internal/platform/open_other.go b/internal/platform/open_other.go new file mode 100644 index 0000000..4aed149 --- /dev/null +++ b/internal/platform/open_other.go @@ -0,0 +1,8 @@ +//go:build !unix + +package platform + +import "os" + +// OpenContent opens a file whose content is about to be read. +func OpenContent(path string) (*os.File, error) { return os.Open(path) } diff --git a/internal/platform/open_unix.go b/internal/platform/open_unix.go new file mode 100644 index 0000000..5446760 --- /dev/null +++ b/internal/platform/open_unix.go @@ -0,0 +1,15 @@ +//go:build unix + +package platform + +import ( + "os" + "syscall" +) + +// OpenContent opens a file whose content is about to be read. It does not +// follow a final symlink, and O_NONBLOCK keeps a FIFO swapped in since the +// scan from blocking open(2) forever; callers still check the file type. +func OpenContent(path string) (*os.File, error) { + return os.OpenFile(path, os.O_RDONLY|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0) +}