From 19f66918cce43d7ad23869459eaac99ba3777af6 Mon Sep 17 00:00:00 2001 From: Milas Bowman Date: Thu, 24 Aug 2023 15:40:43 -0400 Subject: [PATCH] watch: only allow a single instance per-project This is a good place to start introducing (local) exclusivity to Compose. Now, when `alpha watch` launches, it will check for the existence of a PID file in the user XDG runtime directory, and create one if the existing one is stale or does not exist. If the PID file exists and is valid, an error is returned and Compose exits. A slight tweak to the experimental remote Git loader has been made to use the XDG package for consistency. Signed-off-by: Milas Bowman --- cmd/compose/watch.go | 10 ++++++++++ go.mod | 1 + go.sum | 2 ++ internal/locker/pidfile.go | 41 ++++++++++++++++++++++++++++++++++++++ pkg/e2e/watch_test.go | 4 ++++ pkg/remote/git.go | 22 ++++++++++---------- 6 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 internal/locker/pidfile.go 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