Add mem.SwapDevices() method.

pull/1120/head
Tom Barker 4 years ago
parent f86a042980
commit 84a665b712

@ -91,7 +91,7 @@ type SwapMemoryStat struct {
// Linux specific numbers
// https://www.kernel.org/doc/Documentation/cgroup-v2.txt
PgMajFault uint64 `json:"pgmajfault"`
PgMajFault uint64 `json:"pgmajfault"`
}
func (m VirtualMemoryStat) String() string {
@ -103,3 +103,14 @@ func (m SwapMemoryStat) String() string {
s, _ := json.Marshal(m)
return string(s)
}
type SwapDevice struct {
Name string `json:"name"`
UsedBytes uint64 `json:"usedBytes"`
FreeBytes uint64 `json:"freeBytes"`
}
func (m SwapDevice) String() string {
s, _ := json.Marshal(m)
return string(s)
}

@ -0,0 +1,87 @@
// +build freebsd openbsd
package mem
import (
"context"
"fmt"
"os/exec"
"strconv"
"strings"
)
const swapCommand = "/sbin/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) {
output, err := exec.Command(swapCommand, "-lk").Output()
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
}

@ -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)
}

@ -8,6 +8,7 @@ import (
"fmt"
"unsafe"
"github.com/shirou/gopsutil/internal/common"
"golang.org/x/sys/unix"
)
@ -67,3 +68,11 @@ func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) {
return ret, nil
}
func SwapDevices() ([]*SwapDevice, error) {
return SwapDevicesWithContext(context.Background())
}
func SwapDevicesWithContext(ctx context.Context) ([]*SwapDevice, error) {
return nil, common.ErrNotImplementedError
}

@ -1,5 +1,4 @@
// +build darwin
// +build cgo
// +build darwin,cgo
package mem

@ -1,5 +1,4 @@
// +build darwin
// +build !cgo
// +build darwin,!cgo
package mem

@ -23,3 +23,11 @@ func SwapMemory() (*SwapMemoryStat, error) {
func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) {
return nil, common.ErrNotImplementedError
}
func SwapDevices() ([]*SwapDevice, error) {
return SwapDevicesWithContext(context.Background())
}
func SwapDevicesWithContext(ctx context.Context) ([]*SwapDevice, error) {
return nil, common.ErrNotImplementedError
}

@ -3,8 +3,11 @@
package mem
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"math"
"os"
"strconv"
@ -426,3 +429,84 @@ func calcuateAvailVmem(ret *VirtualMemoryStat, retEx *VirtualMemoryExStat) uint6
return availMemory
}
const swapsFilePath = "/proc/swaps"
// swaps file column indexes
const (
nameCol = 0
// typeCol = 1
totalCol = 2
usedCol = 3
// priorityCol = 4
)
func SwapDevices() ([]*SwapDevice, error) {
return SwapDevicesWithContext(context.Background())
}
func SwapDevicesWithContext(ctx context.Context) ([]*SwapDevice, error) {
f, err := os.Open(swapsFilePath)
if err != nil {
return nil, err
}
defer f.Close()
return parseSwapsFile(f)
}
func parseSwapsFile(r io.Reader) ([]*SwapDevice, error) {
scanner := bufio.NewScanner(r)
if !scanner.Scan() {
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("couldn't read file %q: %w", swapsFilePath, err)
}
return nil, fmt.Errorf("unexpected end-of-file in %q", swapsFilePath)
}
// Check header headerFields are as expected
headerFields := strings.Fields(scanner.Text())
if len(headerFields) < usedCol {
return nil, fmt.Errorf("couldn't parse %q: too few fields in header", swapsFilePath)
}
if headerFields[nameCol] != "Filename" {
return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapsFilePath, headerFields[nameCol], "Filename")
}
if headerFields[totalCol] != "Size" {
return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapsFilePath, headerFields[totalCol], "Size")
}
if headerFields[usedCol] != "Used" {
return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapsFilePath, headerFields[usedCol], "Used")
}
var swapDevices []*SwapDevice
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) < usedCol {
return nil, fmt.Errorf("couldn't parse %q: too few fields", swapsFilePath)
}
totalKiB, err := strconv.ParseUint(fields[totalCol], 10, 64)
if err != nil {
return nil, fmt.Errorf("couldn't parse 'Size' column in %q: %w", swapsFilePath, err)
}
usedKiB, err := strconv.ParseUint(fields[usedCol], 10, 64)
if err != nil {
return nil, fmt.Errorf("couldn't parse 'Used' column in %q: %w", swapsFilePath, err)
}
swapDevices = append(swapDevices, &SwapDevice{
Name: fields[nameCol],
UsedBytes: usedKiB * 1024,
FreeBytes: (totalKiB - usedKiB) * 1024,
})
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("couldn't read file %q: %w", swapsFilePath, err)
}
return swapDevices, nil
}

@ -1,10 +1,15 @@
// +build linux
package mem
import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestVirtualMemoryEx(t *testing.T) {
@ -115,3 +120,41 @@ func TestVirtualMemoryLinux(t *testing.T) {
})
}
}
const validFile = `Filename Type Size Used Priority
/dev/dm-2 partition 67022844 490788 -2
/swapfile file 2 1 -3
`
const invalidFile = `INVALID Type Size Used Priority
/dev/dm-2 partition 67022844 490788 -2
/swapfile file 1048572 0 -3
`
func TestParseSwapsFile_ValidFile(t *testing.T) {
assert := assert.New(t)
stats, err := parseSwapsFile(strings.NewReader(validFile))
assert.NoError(err)
assert.Equal(*stats[0], SwapDevice{
Name: "/dev/dm-2",
UsedBytes: 502566912,
FreeBytes: 68128825344,
})
assert.Equal(*stats[1], SwapDevice{
Name: "/swapfile",
UsedBytes: 1024,
FreeBytes: 1024,
})
}
func TestParseSwapsFile_InvalidFile(t *testing.T) {
_, err := parseSwapsFile(strings.NewReader(invalidFile))
assert.Error(t, err)
}
func TestParseSwapsFile_EmptyFile(t *testing.T) {
_, err := parseSwapsFile(strings.NewReader(""))
assert.Error(t, err)
}

@ -1,5 +1,5 @@
// +build openbsd
// +build 386
// +build openbsd,386
// Code generated by cmd/cgo -godefs; DO NOT EDIT.
// cgo -godefs mem/types_openbsd.go

@ -1,5 +1,5 @@
// +build openbsd
// +build arm64
// +build openbsd,arm64
// Code generated by cmd/cgo -godefs; DO NOT EDIT.
// cgo -godefs mem/types_openbsd.go

@ -1,3 +1,5 @@
// +build solaris
package mem
import (
@ -119,3 +121,81 @@ func nonGlobalZoneMemoryCapacity() (uint64, error) {
return memSizeBytes, nil
}
const swapsCommand = "swap"
// The blockSize as reported by `swap -l`. See https://docs.oracle.com/cd/E23824_01/html/821-1459/fsswap-52195.html
const blockSize = 512
// swapctl column indexes
const (
nameCol = 0
// devCol = 1
// swaploCol = 2
totalBlocksCol = 3
freeBlocksCol = 4
)
func SwapDevices() ([]*SwapDevice, error) {
return SwapDevicesWithContext(context.Background())
}
func SwapDevicesWithContext(ctx context.Context) ([]*SwapDevice, error) {
output, err := exec.Command(swapsCommand, "-l").Output()
if err != nil {
return nil, fmt.Errorf("could not execute %q: %w", swapsCommand, err)
}
return parseSwapsCommandOutput(string(output))
}
func parseSwapsCommandOutput(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", swapsCommand, output)
}
// Check header headerFields are as expected.
headerFields := strings.Fields(lines[0])
if len(headerFields) < freeBlocksCol {
return nil, fmt.Errorf("couldn't parse %q: too few fields in header %q", swapsCommand, lines[0])
}
if headerFields[nameCol] != "swapfile" {
return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapsCommand, headerFields[nameCol], "swapfile")
}
if headerFields[totalBlocksCol] != "blocks" {
return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapsCommand, headerFields[totalBlocksCol], "blocks")
}
if headerFields[freeBlocksCol] != "free" {
return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapsCommand, headerFields[freeBlocksCol], "free")
}
var swapDevices []*SwapDevice
for _, line := range lines[1:] {
if line == "" {
continue // the terminal line is typically empty
}
fields := strings.Fields(line)
if len(fields) < freeBlocksCol {
return nil, fmt.Errorf("couldn't parse %q: too few fields", swapsCommand)
}
totalBlocks, err := strconv.ParseUint(fields[totalBlocksCol], 10, 64)
if err != nil {
return nil, fmt.Errorf("couldn't parse 'Size' column in %q: %w", swapsCommand, err)
}
freeBlocks, err := strconv.ParseUint(fields[freeBlocksCol], 10, 64)
if err != nil {
return nil, fmt.Errorf("couldn't parse 'Used' column in %q: %w", swapsCommand, err)
}
swapDevices = append(swapDevices, &SwapDevice{
Name: fields[nameCol],
UsedBytes: (totalBlocks - freeBlocks) * blockSize,
FreeBytes: freeBlocks * blockSize,
})
}
return swapDevices, nil
}

@ -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)
}

@ -110,3 +110,29 @@ func TestSwapMemoryStat_String(t *testing.T) {
t.Errorf("SwapMemoryStat string is invalid: %v", v)
}
}
func TestSwapDevices(t *testing.T) {
v, err := SwapDevices()
skipIfNotImplementedErr(t, err)
if err != nil {
t.Fatalf("error calling SwapDevices: %v", err)
}
t.Logf("SwapDevices() -> %+v", v)
if len(v) == 0 {
t.Fatalf("no swap devices found. [this is expected if the host has swap disabled]")
}
for _, device := range v {
if device.Name == "" {
t.Fatalf("deviceName not set in %+v", device)
}
if device.FreeBytes == 0 {
t.Logf("[WARNING] free-bytes is zero in %+v. This might be expected", device)
}
if device.UsedBytes == 0 {
t.Logf("[WARNING] used-bytes is zero in %+v. This might be expected", device)
}
}
}

@ -4,6 +4,8 @@ package mem
import (
"context"
"sync"
"syscall"
"unsafe"
"github.com/shirou/gopsutil/internal/common"
@ -11,8 +13,10 @@ import (
)
var (
procGlobalMemoryStatusEx = common.Modkernel32.NewProc("GlobalMemoryStatusEx")
procEnumPageFilesW = common.ModPsapi.NewProc("EnumPageFilesW")
procGetNativeSystemInfo = common.Modkernel32.NewProc("GetNativeSystemInfo")
procGetPerformanceInfo = common.ModPsapi.NewProc("GetPerformanceInfo")
procGlobalMemoryStatusEx = common.Modkernel32.NewProc("GlobalMemoryStatusEx")
)
type memoryStatusEx struct {
@ -96,3 +100,66 @@ func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) {
return ret, nil
}
var (
pageSize uint64
pageSizeOnce sync.Once
)
type systemInfo struct {
wProcessorArchitecture uint16
wReserved uint16
dwPageSize uint32
lpMinimumApplicationAddress uintptr
lpMaximumApplicationAddress uintptr
dwActiveProcessorMask uintptr
dwNumberOfProcessors uint32
dwProcessorType uint32
dwAllocationGranularity uint32
wProcessorLevel uint16
wProcessorRevision uint16
}
// system type as defined in https://docs.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-enum_page_file_information
type enumPageFileInformation struct {
cb uint32
reserved uint32
totalSize uint64
totalInUse uint64
peakUsage uint64
}
func SwapDevices() ([]*SwapDevice, error) {
return SwapDevicesWithContext(context.Background())
}
func SwapDevicesWithContext(ctx context.Context) ([]*SwapDevice, error) {
pageSizeOnce.Do(func() {
var sysInfo systemInfo
procGetNativeSystemInfo.Call(uintptr(unsafe.Pointer(&sysInfo)))
pageSize = uint64(sysInfo.dwPageSize)
})
// the following system call invokes the supplied callback function once for each page file before returning
// see https://docs.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-enumpagefilesw
var swapDevices []*SwapDevice
result, _, _ := procEnumPageFilesW.Call(windows.NewCallback(pEnumPageFileCallbackW), uintptr(unsafe.Pointer(&swapDevices)))
if result == 0 {
return nil, windows.GetLastError()
}
return swapDevices, nil
}
// system callback as defined in https://docs.microsoft.com/en-us/windows/win32/api/psapi/nc-psapi-penum_page_file_callbackw
func pEnumPageFileCallbackW(swapDevices *[]*SwapDevice, enumPageFileInfo *enumPageFileInformation, lpFilenamePtr *[syscall.MAX_LONG_PATH]uint16) *bool {
*swapDevices = append(*swapDevices, &SwapDevice{
Name: syscall.UTF16ToString((*lpFilenamePtr)[:]),
UsedBytes: enumPageFileInfo.totalInUse * pageSize,
FreeBytes: (enumPageFileInfo.totalSize - enumPageFileInfo.totalInUse) * pageSize,
})
// return true to continue enumerating page files
ret := true
return &ret
}

Loading…
Cancel
Save