diff --git a/v3/mem/mem_bsd.go b/v3/mem/mem_bsd.go new file mode 100644 index 0000000..17ca920 --- /dev/null +++ b/v3/mem/mem_bsd.go @@ -0,0 +1,91 @@ +// +build freebsd openbsd + +package mem + +import ( + "context" + "fmt" + "os/exec" + "strconv" + "strings" +) + +const swapCommand = "swapctl" + +// swapctl column indexes +const ( + nameCol = 0 + totalKiBCol = 1 + usedKiBCol = 2 +) + +func SwapDevices() ([]*SwapDevice, error) { + return SwapDevicesWithContext(context.Background()) +} + +func SwapDevicesWithContext(ctx context.Context) ([]*SwapDevice, error) { + swapCommandPath, err := exec.LookPath(swapCommand) + if err != nil { + return nil, fmt.Errorf("could not find command %q: %w", swapCommand, err) + } + output, err := invoke.CommandWithContext(swapCommandPath, "-lk") + if err != nil { + return nil, fmt.Errorf("could not execute %q: %w", swapCommand, err) + } + + return parseSwapctlOutput(string(output)) +} + +func parseSwapctlOutput(output string) ([]*SwapDevice, error) { + lines := strings.Split(output, "\n") + if len(lines) == 0 { + return nil, fmt.Errorf("could not parse output of %q: no lines in %q", swapCommand, output) + } + + // Check header headerFields are as expected. + header := lines[0] + header = strings.ToLower(header) + header = strings.ReplaceAll(header, ":", "") + headerFields := strings.Fields(header) + if len(headerFields) < usedKiBCol { + return nil, fmt.Errorf("couldn't parse %q: too few fields in header %q", swapCommand, header) + } + if headerFields[nameCol] != "device" { + return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapCommand, headerFields[nameCol], "device") + } + if headerFields[totalKiBCol] != "1kb-blocks" && headerFields[totalKiBCol] != "1k-blocks" { + return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapCommand, headerFields[totalKiBCol], "1kb-blocks") + } + if headerFields[usedKiBCol] != "used" { + return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapCommand, headerFields[usedKiBCol], "used") + } + + var swapDevices []*SwapDevice + for _, line := range lines[1:] { + if line == "" { + continue // the terminal line is typically empty + } + fields := strings.Fields(line) + if len(fields) < usedKiBCol { + return nil, fmt.Errorf("couldn't parse %q: too few fields", swapCommand) + } + + totalKiB, err := strconv.ParseUint(fields[totalKiBCol], 10, 64) + if err != nil { + return nil, fmt.Errorf("couldn't parse 'Size' column in %q: %w", swapCommand, err) + } + + usedKiB, err := strconv.ParseUint(fields[usedKiBCol], 10, 64) + if err != nil { + return nil, fmt.Errorf("couldn't parse 'Used' column in %q: %w", swapCommand, err) + } + + swapDevices = append(swapDevices, &SwapDevice{ + Name: fields[nameCol], + UsedBytes: usedKiB * 1024, + FreeBytes: (totalKiB - usedKiB) * 1024, + }) + } + + return swapDevices, nil +} diff --git a/v3/mem/mem_bsd_test.go b/v3/mem/mem_bsd_test.go new file mode 100644 index 0000000..ad3838e --- /dev/null +++ b/v3/mem/mem_bsd_test.go @@ -0,0 +1,63 @@ +// +build freebsd openbsd + +package mem + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +const validFreeBSD = `Device: 1kB-blocks Used: +/dev/gpt/swapfs 1048576 1234 +/dev/md0 1048576 666 +` + +const validOpenBSD = `Device 1K-blocks Used Avail Capacity Priority +/dev/wd0b 655025 1234 653791 1% 0 +` + +const invalid = `Device: 512-blocks Used: +/dev/gpt/swapfs 1048576 1234 +/dev/md0 1048576 666 +` + +func TestParseSwapctlOutput_FreeBSD(t *testing.T) { + assert := assert.New(t) + stats, err := parseSwapctlOutput(validFreeBSD) + assert.NoError(err) + + assert.Equal(*stats[0], SwapDevice{ + Name: "/dev/gpt/swapfs", + UsedBytes: 1263616, + FreeBytes: 1072478208, + }) + + assert.Equal(*stats[1], SwapDevice{ + Name: "/dev/md0", + UsedBytes: 681984, + FreeBytes: 1073059840, + }) +} + +func TestParseSwapctlOutput_OpenBSD(t *testing.T) { + assert := assert.New(t) + stats, err := parseSwapctlOutput(validOpenBSD) + assert.NoError(err) + + assert.Equal(*stats[0], SwapDevice{ + Name: "/dev/wd0b", + UsedBytes: 1234 * 1024, + FreeBytes: 653791 * 1024, + }) +} + +func TestParseSwapctlOutput_Invalid(t *testing.T) { + _, err := parseSwapctlOutput(invalid) + assert.Error(t, err) +} + +func TestParseSwapctlOutput_Empty(t *testing.T) { + _, err := parseSwapctlOutput("") + assert.Error(t, err) +} diff --git a/v3/mem/mem_solaris_test.go b/v3/mem/mem_solaris_test.go new file mode 100644 index 0000000..907e49d --- /dev/null +++ b/v3/mem/mem_solaris_test.go @@ -0,0 +1,45 @@ +// +build solaris + +package mem + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +const validFile = `swapfile dev swaplo blocks free +/dev/zvol/dsk/rpool/swap 256,1 16 1058800 1058800 +/dev/dsk/c0t0d0s1 136,1 16 1638608 1600528` + +const invalidFile = `swapfile dev swaplo INVALID free +/dev/zvol/dsk/rpool/swap 256,1 16 1058800 1058800 +/dev/dsk/c0t0d0s1 136,1 16 1638608 1600528` + +func TestParseSwapsCommandOutput_Valid(t *testing.T) { + assert := assert.New(t) + stats, err := parseSwapsCommandOutput(validFile) + assert.NoError(err) + + assert.Equal(*stats[0], SwapDevice{ + Name: "/dev/zvol/dsk/rpool/swap", + UsedBytes: 0, + FreeBytes: 1058800 * 512, + }) + + assert.Equal(*stats[1], SwapDevice{ + Name: "/dev/dsk/c0t0d0s1", + UsedBytes: 38080 * 512, + FreeBytes: 1600528 * 512, + }) +} + +func TestParseSwapsCommandOutput_Invalid(t *testing.T) { + _, err := parseSwapsCommandOutput(invalidFile) + assert.Error(t, err) +} + +func TestParseSwapsCommandOutput_Empty(t *testing.T) { + _, err := parseSwapsCommandOutput("") + assert.Error(t, err) +}