From 4ff9aa73313821f14e59d935a6d2bcac44c29a0d Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Wed, 28 Jan 2026 17:48:51 +0100 Subject: [PATCH] fix panic Signed-off-by: Nicolas De Loof --- pkg/watch/watcher_darwin.go | 14 +++++++--- pkg/watch/watcher_darwin_test.go | 48 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 pkg/watch/watcher_darwin_test.go diff --git a/pkg/watch/watcher_darwin.go b/pkg/watch/watcher_darwin.go index 56da94b353..a63612aedf 100644 --- a/pkg/watch/watcher_darwin.go +++ b/pkg/watch/watcher_darwin.go @@ -22,6 +22,7 @@ import ( "fmt" "os" "path/filepath" + "sync" "time" "github.com/fsnotify/fsevents" @@ -38,6 +39,7 @@ type fseventNotify struct { stop chan struct{} pathsWereWatching map[string]any + closeOnce sync.Once } func (d *fseventNotify) loop() { @@ -81,6 +83,8 @@ func (d *fseventNotify) Start() error { return nil } + d.closeOnce = sync.Once{} + numberOfWatches.Add(int64(len(d.stream.Paths))) err := d.stream.Start() @@ -92,11 +96,13 @@ func (d *fseventNotify) Start() error { } func (d *fseventNotify) Close() error { - numberOfWatches.Add(int64(-len(d.stream.Paths))) + d.closeOnce.Do(func() { + numberOfWatches.Add(int64(-len(d.stream.Paths))) - d.stream.Stop() - close(d.errors) - close(d.stop) + d.stream.Stop() + close(d.errors) + close(d.stop) + }) return nil } diff --git a/pkg/watch/watcher_darwin_test.go b/pkg/watch/watcher_darwin_test.go new file mode 100644 index 0000000000..50eb442b06 --- /dev/null +++ b/pkg/watch/watcher_darwin_test.go @@ -0,0 +1,48 @@ +//go:build fsnotify + +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package watch + +import ( + "testing" + + "gotest.tools/v3/assert" +) + +func TestFseventNotifyCloseIdempotent(t *testing.T) { + // Create a watcher with a temporary directory + tmpDir := t.TempDir() + watcher, err := newWatcher([]string{tmpDir}) + assert.NilError(t, err) + + // Start the watcher + err = watcher.Start() + assert.NilError(t, err) + + // Close should work the first time + err = watcher.Close() + assert.NilError(t, err) + + // Close should be idempotent - calling it again should not panic + err = watcher.Close() + assert.NilError(t, err) + + // Even a third time should be safe + err = watcher.Close() + assert.NilError(t, err) +}