diff --git a/docs/install.md b/docs/install.md index e85485bf..742110b8 100644 --- a/docs/install.md +++ b/docs/install.md @@ -23,7 +23,7 @@ Download pre-built binaries from [GitHub Releases](https://github.com/cocoonstac ```bash # Linux amd64 -curl -fsSL -o cocoon.tar.gz https://github.com/cocoonstack/cocoon/releases/download/v0.4.8/cocoon_0.4.8_Linux_x86_64.tar.gz +curl -fsSL -o cocoon.tar.gz https://github.com/cocoonstack/cocoon/releases/download/v0.5.1/cocoon_0.5.1_Linux_x86_64.tar.gz tar -xzf cocoon.tar.gz install -m 0755 cocoon /usr/local/bin/ diff --git a/utils/process_linux.go b/utils/process_linux.go index f74d23d8..033b91d2 100644 --- a/utils/process_linux.go +++ b/utils/process_linux.go @@ -3,9 +3,7 @@ package utils import ( - "errors" "fmt" - "io/fs" "os" "path/filepath" "slices" @@ -21,8 +19,12 @@ type procEntry struct { // ProcScan caches /proc cmdlines for one binaryName. Batch callers scan once then Find per id, replacing N /proc walks with one. type ProcScan []procEntry -// ScanProcsByBinary walks /proc once, capturing argv[0]-basename matches. ENOENT (process exited mid-scan) is skipped; other read errors fail closed. +// ScanProcsByBinary walks /proc once, capturing argv[0]-basename matches. Read errors from processes that vanished mid-scan are skipped; errors on live processes fail closed. func ScanProcsByBinary(binaryName string) (ProcScan, error) { + return scanProcsByBinary(binaryName, os.ReadFile, IsProcessAlive) +} + +func scanProcsByBinary(binaryName string, readFile func(string) ([]byte, error), alive func(int) bool) (ProcScan, error) { entries, err := os.ReadDir("/proc") if err != nil { return nil, err @@ -34,10 +36,10 @@ func ScanProcsByBinary(binaryName string) (ProcScan, error) { if atoiErr != nil || pid <= 0 { continue } - data, readErr := os.ReadFile(fmt.Sprintf("/proc/%d/cmdline", pid)) + data, readErr := readFile(fmt.Sprintf("/proc/%d/cmdline", pid)) if readErr != nil { - if !errors.Is(readErr, fs.ErrNotExist) && firstErr == nil { - firstErr = fmt.Errorf("read /proc/%d/cmdline: %w", pid, readErr) + if firstErr == nil && alive(pid) { + firstErr = readErr } continue } diff --git a/utils/process_linux_test.go b/utils/process_linux_test.go new file mode 100644 index 00000000..8e7cdc92 --- /dev/null +++ b/utils/process_linux_test.go @@ -0,0 +1,55 @@ +//go:build linux + +package utils + +import ( + "errors" + "fmt" + "os" + "slices" + "testing" +) + +func TestScanProcsByBinaryReadErrors(t *testing.T) { + self := os.Getpid() + selfPath := fmt.Sprintf("/proc/%d/cmdline", self) + injected := errors.New("injected read failure") + failAll := func(path string) ([]byte, error) { + if path == selfPath { + return nil, injected + } + return nil, os.ErrNotExist + } + + tests := []struct { + name string + alive func(int) bool + wantErr error + }{ + {"read error on live process fails closed", func(pid int) bool { return pid == self }, injected}, + {"read error on vanished process is skipped", func(int) bool { return false }, nil}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if _, err := scanProcsByBinary("no-such-binary", failAll, tt.alive); !errors.Is(err, tt.wantErr) { + t.Errorf("scanProcsByBinary: got err %v, want %v", err, tt.wantErr) + } + }) + } + + t.Run("matches survive skipped entries", func(t *testing.T) { + readFile := func(path string) ([]byte, error) { + if path == selfPath { + return []byte("/bin/testvmm\x00--api-socket\x00/run/x.sock"), nil + } + return nil, os.ErrNotExist + } + scan, err := scanProcsByBinary("testvmm", readFile, func(int) bool { return false }) + if err != nil { + t.Fatalf("scanProcsByBinary: %v", err) + } + if got := scan.Find("/run/x.sock"); !slices.Equal(got, []int{self}) { + t.Errorf("Find: got %v, want [%d]", got, self) + } + }) +}