diff --git a/recording/recorder.go b/recording/recorder.go index 4f40dbe..c1e7365 100644 --- a/recording/recorder.go +++ b/recording/recorder.go @@ -57,14 +57,19 @@ func New(directory string, format string, now time.Time, frameSize int, interval if err := os.MkdirAll(directory, 0755); err != nil { return nil, err } - path := UniquePath(directory, now, format) + path, err := reservePath(directory, now, format) + if err != nil { + return nil, err + } args := ffmpegArgs(format, path) cmd := exec.Command("ffmpeg", args...) stdin, err := cmd.StdinPipe() if err != nil { + _ = os.Remove(path) return nil, err } if err := cmd.Start(); err != nil { + _ = os.Remove(path) return nil, err } recorder := &Recorder{ @@ -91,6 +96,24 @@ func NormalizeFormat(format string) string { return format } +func reservePath(directory string, now time.Time, format string) (string, error) { + for { + path := UniquePath(directory, now, format) + file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600) + if os.IsExist(err) { + continue + } + if err != nil { + return "", err + } + if err := file.Close(); err != nil { + _ = os.Remove(path) + return "", err + } + return path, nil + } +} + func UniquePath(directory string, now time.Time, format string) string { base := fmt.Sprintf("barnard-recording-%s", now.Format("20060102-150405")) path := filepath.Join(directory, base+"."+format) diff --git a/recording/recorder_test.go b/recording/recorder_test.go index 70a8e04..155cca9 100644 --- a/recording/recorder_test.go +++ b/recording/recorder_test.go @@ -4,6 +4,7 @@ import ( "io" "os" "path/filepath" + "sync" "testing" "time" ) @@ -58,6 +59,39 @@ func TestUniquePathAvoidsCollision(t *testing.T) { } } +func TestReservePathPreventsConcurrentRecordingCollisions(t *testing.T) { + dir := t.TempDir() + now := time.Date(2026, 5, 14, 12, 30, 0, 0, time.Local) + paths := make(chan string, 2) + errs := make(chan error, 2) + var wg sync.WaitGroup + for range 2 { + wg.Add(1) + go func() { + defer wg.Done() + path, err := reservePath(dir, now, "flac") + if err != nil { + errs <- err + return + } + paths <- path + }() + } + wg.Wait() + close(paths) + close(errs) + for err := range errs { + t.Fatal(err) + } + var reserved []string + for path := range paths { + reserved = append(reserved, path) + } + if len(reserved) != 2 || reserved[0] == reserved[1] { + t.Fatalf("reserved paths = %#v", reserved) + } +} + func TestNormalizeStereoFrame(t *testing.T) { mono := NormalizeStereoFrame([]int16{1, -2}, 3) wantMono := []int16{1, 1, -2, -2, 0, 0}