Skip to content
Merged
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 docs/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/

Expand Down
14 changes: 8 additions & 6 deletions utils/process_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
package utils

import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"slices"
Expand All @@ -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
Expand All @@ -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
}
Expand Down
55 changes: 55 additions & 0 deletions utils/process_linux_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
Loading