Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion internal/locker/pidfile_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion internal/locker/pidfile_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down
67 changes: 67 additions & 0 deletions internal/pidfile/pidfile.go
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +17 to +23
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Package comment says it provides helper functions to “create and remove PID files”, but this package currently only implements Read and Write (no remove/delete helper). Consider either adding a Remove helper (if intended) or adjusting the package comment to match the actual API surface.

Copilot uses AI. Check for mistakes.

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)
}
42 changes: 42 additions & 0 deletions internal/pidfile/pidfile_unix.go
Original file line number Diff line number Diff line change
@@ -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
}
Comment on lines +34 to +41
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On non-Windows platforms, alive() falls back to checking for /proc/ for all OSes except darwin. This is less reliable than using unix.Kill(pid, 0) (which also lets you treat EPERM as “alive”) and can be wrong on Linux configurations that restrict /proc visibility (e.g., hidepid) and on Unix variants without /proc. Consider using unix.Kill(pid, 0) for the default case as well, similar to the darwin branch.

Copilot uses AI. Check for mistakes.
}
38 changes: 38 additions & 0 deletions internal/pidfile/pidfile_windows.go
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +35 to +37
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alive() always returns true if OpenProcess and GetExitCodeProcess succeed, regardless of whether the process has actually exited (exit code != STILL_ACTIVE). Additionally, when GetExitCodeProcess returns an error, the function compares an untrusted/undefined exit-code value against STATUS_PENDING. This can cause stale pidfiles to be treated as “still running” indefinitely on Windows. Consider returning false on GetExitCodeProcess error, and otherwise checking the returned exit code (e.g., windows.STILL_ACTIVE / equivalent) before returning true.

Suggested change
return c == uint32(windows.STATUS_PENDING)
}
return true
return false
}
return c == windows.STILL_ACTIVE

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's leave that for the module we're moving; ISTR there were some reasons to do this, but would have to check history 😂

}
59 changes: 56 additions & 3 deletions pkg/compose/logs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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()
Expand Down Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion pkg/compose/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Loading