diff --git a/go.mod b/go.mod index bee27a683ed..b3e1b8e53df 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,6 @@ require ( github.com/docker/buildx v0.33.0 github.com/docker/cli v29.3.1+incompatible github.com/docker/cli-docs-tool v0.11.0 - github.com/docker/docker v28.5.2+incompatible github.com/docker/go-units v0.5.0 github.com/eiannone/keyboard v0.0.0-20220611211555-0d226195f203 github.com/fsnotify/fsevents v0.2.0 @@ -73,6 +72,7 @@ require ( github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker v28.5.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.5 // indirect github.com/docker/go-connections v0.6.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect diff --git a/internal/locker/pidfile_unix.go b/internal/locker/pidfile_unix.go index 484b65d8250..820dfe71e83 100644 --- a/internal/locker/pidfile_unix.go +++ b/internal/locker/pidfile_unix.go @@ -21,7 +21,7 @@ package locker import ( "os" - "github.com/docker/docker/pkg/pidfile" + "github.com/docker/compose/v5/internal/pidfile" ) func (f *Pidfile) Lock() error { diff --git a/internal/locker/pidfile_windows.go b/internal/locker/pidfile_windows.go index adc151827dc..2dac9ff031b 100644 --- a/internal/locker/pidfile_windows.go +++ b/internal/locker/pidfile_windows.go @@ -21,7 +21,7 @@ package locker import ( "os" - "github.com/docker/docker/pkg/pidfile" + "github.com/docker/compose/v5/internal/pidfile" "github.com/mitchellh/go-ps" ) diff --git a/internal/pidfile/pidfile.go b/internal/pidfile/pidfile.go new file mode 100644 index 00000000000..81a7a4fd7eb --- /dev/null +++ b/internal/pidfile/pidfile.go @@ -0,0 +1,67 @@ +/* + Copyright 2025 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 pidfile provides helper functions to create and remove PID files. +// A PID file is usually a file used to store the process ID of a running +// process. +// +// This is a temporary copy of github.com/moby/moby/v2/pkg/pidfile, and +// should be replaced once pidfile is available as a standalone module. +package pidfile + +import ( + "bytes" + "fmt" + "os" + "strconv" +) + +// Read reads the "PID file" at path, and returns the PID if it contains a +// valid PID of a running process, or 0 otherwise. It returns an error when +// failing to read the file, or if the file doesn't exist, but malformed content +// is ignored. Consumers should therefore check if the returned PID is a non-zero +// value before use. +func Read(path string) (pid int, _ error) { + pidByte, err := os.ReadFile(path) + if err != nil { + return 0, err + } + pid, err = strconv.Atoi(string(bytes.TrimSpace(pidByte))) + if err != nil { + return 0, nil + } + if pid != 0 && alive(pid) { + return pid, nil + } + return 0, nil +} + +// Write writes a "PID file" at the specified path. It returns an error if the +// file exists and contains a valid PID of a running process, or when failing +// to write the file. +func Write(path string, pid int) error { + if pid < 1 { + return fmt.Errorf("invalid PID (%d): only positive PIDs are allowed", pid) + } + oldPID, err := Read(path) + if err != nil && !os.IsNotExist(err) { + return err + } + if oldPID != 0 { + return fmt.Errorf("process with PID %d is still running", oldPID) + } + return os.WriteFile(path, []byte(strconv.Itoa(pid)), 0o644) +} diff --git a/internal/pidfile/pidfile_unix.go b/internal/pidfile/pidfile_unix.go new file mode 100644 index 00000000000..84bcd191d5d --- /dev/null +++ b/internal/pidfile/pidfile_unix.go @@ -0,0 +1,42 @@ +//go:build !windows + +/* + Copyright 2025 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 pidfile + +import ( + "errors" + "os" + "runtime" + "strconv" + + "golang.org/x/sys/unix" +) + +func alive(pid int) bool { + if pid < 1 { + return false + } + switch runtime.GOOS { + case "darwin": + err := unix.Kill(pid, 0) + return err == nil || errors.Is(err, unix.EPERM) + default: + _, err := os.Stat("/proc/" + strconv.Itoa(pid)) + return err == nil + } +} diff --git a/internal/pidfile/pidfile_windows.go b/internal/pidfile/pidfile_windows.go new file mode 100644 index 00000000000..622318bb8b3 --- /dev/null +++ b/internal/pidfile/pidfile_windows.go @@ -0,0 +1,38 @@ +//go:build windows + +/* + Copyright 2025 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 pidfile + +import "golang.org/x/sys/windows" + +func alive(pid int) bool { + if pid < 1 { + return false + } + h, err := windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, uint32(pid)) + if err != nil { + return false + } + var c uint32 + err = windows.GetExitCodeProcess(h, &c) + _ = windows.CloseHandle(h) + if err != nil { + return c == uint32(windows.STATUS_PENDING) + } + return true +} diff --git a/pkg/compose/logs_test.go b/pkg/compose/logs_test.go index 2f21ad9f033..3b02ef965e6 100644 --- a/pkg/compose/logs_test.go +++ b/pkg/compose/logs_test.go @@ -17,13 +17,16 @@ package compose import ( + "bytes" + "encoding/binary" + "errors" "io" "strings" "sync" "testing" "github.com/compose-spec/compose-go/v2/types" - "github.com/docker/docker/pkg/stdcopy" + "github.com/moby/moby/api/pkg/stdcopy" containerType "github.com/moby/moby/api/types/container" "github.com/moby/moby/client" "go.uber.org/mock/gomock" @@ -33,6 +36,56 @@ import ( compose "github.com/docker/compose/v5/pkg/api" ) +// newStdWriter is copied from github.com/moby/moby/daemon/internal/stdcopymux +// because NewStdWriter was moved to a daemon-internal package in moby v2 and +// is no longer publicly importable. We need it in tests to produce multiplexed +// streams that stdcopy.StdCopy can demultiplex. + +const ( + stdWriterPrefixLen = 8 + stdWriterFdIndex = 0 + stdWriterSizeIndex = 4 +) + +var bufPool = &sync.Pool{New: func() any { return bytes.NewBuffer(nil) }} + +type stdWriter struct { + io.Writer + prefix byte +} + +func (w *stdWriter) Write(p []byte) (int, error) { + if w == nil || w.Writer == nil { + return 0, errors.New("writer not instantiated") + } + if p == nil { + return 0, nil + } + + header := [stdWriterPrefixLen]byte{stdWriterFdIndex: w.prefix} + binary.BigEndian.PutUint32(header[stdWriterSizeIndex:], uint32(len(p))) + buf := bufPool.Get().(*bytes.Buffer) + buf.Write(header[:]) + buf.Write(p) + + n, err := w.Writer.Write(buf.Bytes()) + n -= stdWriterPrefixLen + if n < 0 { + n = 0 + } + + buf.Reset() + bufPool.Put(buf) + return n, err +} + +func newStdWriter(w io.Writer, streamType stdcopy.StdType) io.Writer { + return &stdWriter{ + Writer: w, + prefix: byte(streamType), + } +} + func TestComposeService_Logs_Demux(t *testing.T) { mockCtrl := gomock.NewController(t) defer mockCtrl.Finish() @@ -68,8 +121,8 @@ func TestComposeService_Logs_Demux(t *testing.T) { _ = c1Reader.Close() _ = c1Writer.Close() }) - c1Stdout := stdcopy.NewStdWriter(c1Writer, stdcopy.Stdout) - c1Stderr := stdcopy.NewStdWriter(c1Writer, stdcopy.Stderr) + c1Stdout := newStdWriter(c1Writer, stdcopy.Stdout) + c1Stderr := newStdWriter(c1Writer, stdcopy.Stderr) go func() { _, err := c1Stdout.Write([]byte("hello stdout\n")) assert.NilError(t, err, "Writing to fake stdout") diff --git a/pkg/compose/model.go b/pkg/compose/model.go index e4614d199c3..345ad1272bc 100644 --- a/pkg/compose/model.go +++ b/pkg/compose/model.go @@ -29,7 +29,7 @@ import ( "github.com/compose-spec/compose-go/v2/types" "github.com/containerd/errdefs" "github.com/docker/cli/cli-plugins/manager" - "github.com/docker/docker/api/types/versions" + "github.com/moby/moby/client/pkg/versions" "github.com/spf13/cobra" "golang.org/x/sync/errgroup"