diff --git a/cmd/compose/watch.go b/cmd/compose/watch.go index 6e895edc1..5c3a26a7a 100644 --- a/cmd/compose/watch.go +++ b/cmd/compose/watch.go @@ -21,6 +21,8 @@ import ( "fmt" "os" + "github.com/docker/compose/v2/internal/locker" + "github.com/docker/compose/v2/pkg/api" "github.com/spf13/cobra" ) @@ -57,5 +59,13 @@ func runWatch(ctx context.Context, backend api.Service, opts watchOptions, servi return err } + l, err := locker.NewPidfile(project.Name) + if err != nil { + return fmt.Errorf("cannot take exclusive lock for project %q: %v", project.Name, err) + } + if err := l.Lock(); err != nil { + return fmt.Errorf("cannot take exclusive lock for project %q: %v", project.Name, err) + } + return backend.Watch(ctx, project, services, api.WatchOptions{}) } diff --git a/go.mod b/go.mod index 3ae3648c3..7a6c5170d 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21 require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/Microsoft/go-winio v0.6.1 + github.com/adrg/xdg v0.4.0 github.com/buger/goterm v1.0.4 github.com/compose-spec/compose-go v1.18.2 github.com/containerd/console v1.0.3 diff --git a/go.sum b/go.sum index a16ceccb8..60fb854c3 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,8 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/O github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= +github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= diff --git a/internal/locker/pidfile.go b/internal/locker/pidfile.go new file mode 100644 index 000000000..9e1ec1e31 --- /dev/null +++ b/internal/locker/pidfile.go @@ -0,0 +1,41 @@ +/* + Copyright 2023 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 locker + +import ( + "fmt" + "os" + + "github.com/adrg/xdg" + "github.com/docker/docker/pkg/pidfile" +) + +type Pidfile struct { + path string +} + +func NewPidfile(projectName string) (*Pidfile, error) { + path, err := xdg.RuntimeFile(fmt.Sprintf("docker-compose.%s.pid", projectName)) + if err != nil { + return nil, err + } + return &Pidfile{path: path}, nil +} + +func (f *Pidfile) Lock() error { + return pidfile.Write(f.path, os.Getpid()) +} diff --git a/pkg/e2e/watch_test.go b/pkg/e2e/watch_test.go index c109831eb..c9bb1a141 100644 --- a/pkg/e2e/watch_test.go +++ b/pkg/e2e/watch_test.go @@ -71,6 +71,9 @@ func doTest(t *testing.T, svcName string, tarSync bool) { CopyFile(t, filepath.Join("fixtures", "watch", "compose.yaml"), composeFilePath) projName := "e2e-watch-" + svcName + if tarSync { + projName += "-tar" + } env := []string{ "COMPOSE_FILE=" + composeFilePath, "COMPOSE_PROJECT_NAME=" + projName, @@ -96,6 +99,7 @@ func doTest(t *testing.T, svcName string, tarSync bool) { t.Cleanup(func() { // IMPORTANT: watch doesn't exit on its own, don't leak processes! if r.Cmd.Process != nil { + t.Logf("Killing watch process: pid[%d]", r.Cmd.Process.Pid) _ = r.Cmd.Process.Kill() } }) diff --git a/pkg/remote/git.go b/pkg/remote/git.go index a32e230b7..84dd101ad 100644 --- a/pkg/remote/git.go +++ b/pkg/remote/git.go @@ -25,6 +25,8 @@ import ( "regexp" "strconv" + "github.com/adrg/xdg" + "github.com/compose-spec/compose-go/cli" "github.com/compose-spec/compose-go/loader" "github.com/compose-spec/compose-go/types" @@ -45,19 +47,15 @@ func GitRemoteLoaderEnabled() (bool, error) { } func NewGitRemoteLoader() (loader.ResourceLoader, error) { - var base string - if cacheHome := os.Getenv("XDG_CACHE_HOME"); cacheHome != "" { - base = cacheHome - } else { - home, err := os.UserHomeDir() - if err != nil { - return nil, err - } - base = filepath.Join(home, ".cache") + // xdg.CacheFile creates the parent directories for the target file path + // and returns the fully qualified path, so use "git" as a filename and + // then chop it off after, i.e. no ~/.cache/docker-compose/git file will + // ever be created + cache, err := xdg.CacheFile(filepath.Join("docker-compose", "git")) + if err != nil { + return nil, fmt.Errorf("initializing git cache: %w", err) } - cache := filepath.Join(base, "docker-compose") - - err := os.MkdirAll(cache, 0o700) + cache = filepath.Dir(cache) return gitRemoteLoader{ cache: cache, }, err