[mem] Add swapdevice

pull/1138/head
shirou 4 years ago
parent 8d7a3abddb
commit 8177d340fd

@ -103,3 +103,14 @@ func (m SwapMemoryStat) String() string {
s, _ := json.Marshal(m) s, _ := json.Marshal(m)
return string(s) 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)
}

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"unsafe" "unsafe"
"github.com/shirou/gopsutil/internal/common"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
) )
@ -67,3 +68,11 @@ func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) {
return ret, nil 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 darwin,cgo
// +build cgo
package mem package mem

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

@ -23,3 +23,11 @@ func SwapMemory() (*SwapMemoryStat, error) {
func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) { func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) {
return nil, common.ErrNotImplementedError 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 package mem
import ( import (
"bufio"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"io"
"math" "math"
"os" "os"
"strconv" "strconv"
@ -426,3 +429,86 @@ func calcuateAvailVmem(ret *VirtualMemoryStat, retEx *VirtualMemoryExStat) uint6
return availMemory return availMemory
} }
const swapsFilename = "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) {
swapsFilePath := common.HostProc(swapsFilename)
f, err := os.Open(swapsFilePath)
if err != nil {
return nil, err
}
defer f.Close()
return parseSwapsFile(f)
}
func parseSwapsFile(r io.Reader) ([]*SwapDevice, error) {
swapsFilePath := common.HostProc(swapsFilename)
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 package mem
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert"
) )
func TestVirtualMemoryEx(t *testing.T) { 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 openbsd,386
// +build 386
// Code generated by cmd/cgo -godefs; DO NOT EDIT. // Code generated by cmd/cgo -godefs; DO NOT EDIT.
// cgo -godefs mem/types_openbsd.go // cgo -godefs mem/types_openbsd.go

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

@ -1,3 +1,5 @@
// +build solaris
package mem package mem
import ( import (
@ -119,3 +121,85 @@ func nonGlobalZoneMemoryCapacity() (uint64, error) {
return memSizeBytes, nil 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) {
swapsCommandPath, err := exec.LookPath(swapsCommand)
if err != nil {
return nil, fmt.Errorf("could not find command %q: %w", swapCommand, err)
}
output, err := invoke.CommandWithContext(swapsCommandPath, "-l")
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
}

@ -110,3 +110,29 @@ func TestSwapMemoryStat_String(t *testing.T) {
t.Errorf("SwapMemoryStat string is invalid: %v", v) 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 ( import (
"context" "context"
"sync"
"syscall"
"unsafe" "unsafe"
"github.com/shirou/gopsutil/v3/internal/common" "github.com/shirou/gopsutil/v3/internal/common"
@ -11,8 +13,10 @@ import (
) )
var ( var (
procGlobalMemoryStatusEx = common.Modkernel32.NewProc("GlobalMemoryStatusEx") procEnumPageFilesW = common.ModPsapi.NewProc("EnumPageFilesW")
procGetNativeSystemInfo = common.Modkernel32.NewProc("GetNativeSystemInfo")
procGetPerformanceInfo = common.ModPsapi.NewProc("GetPerformanceInfo") procGetPerformanceInfo = common.ModPsapi.NewProc("GetPerformanceInfo")
procGlobalMemoryStatusEx = common.Modkernel32.NewProc("GlobalMemoryStatusEx")
) )
type memoryStatusEx struct { type memoryStatusEx struct {
@ -96,3 +100,66 @@ func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) {
return ret, nil 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