Skip to content

Commit 8d02cf8

Browse files
authored
Merge pull request #1138 from shirou/feature/v3_add_swapdevice
[mem] Add mem.SwapDevices() to v3
2 parents 8d7a3ab + 7be7e78 commit 8d02cf8

14 files changed

+343
-11
lines changed

process/process_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -473,7 +473,7 @@ func Test_Process_CreateTime(t *testing.T) {
473473
}
474474

475475
gotElapsed := time.Since(time.Unix(int64(c/1000), 0))
476-
maxElapsed := time.Duration(5 * time.Second)
476+
maxElapsed := time.Duration(20 * time.Second)
477477

478478
if gotElapsed >= maxElapsed {
479479
t.Errorf("this process has not been running for %v", gotElapsed)

v3/mem/mem.go

+11
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,14 @@ func (m SwapMemoryStat) String() string {
103103
s, _ := json.Marshal(m)
104104
return string(s)
105105
}
106+
107+
type SwapDevice struct {
108+
Name string `json:"name"`
109+
UsedBytes uint64 `json:"usedBytes"`
110+
FreeBytes uint64 `json:"freeBytes"`
111+
}
112+
113+
func (m SwapDevice) String() string {
114+
s, _ := json.Marshal(m)
115+
return string(s)
116+
}

v3/mem/mem_darwin.go

+9
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"unsafe"
1010

11+
"github.com/shirou/gopsutil/v3/internal/common"
1112
"golang.org/x/sys/unix"
1213
)
1314

@@ -67,3 +68,11 @@ func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) {
6768

6869
return ret, nil
6970
}
71+
72+
func SwapDevices() ([]*SwapDevice, error) {
73+
return SwapDevicesWithContext(context.Background())
74+
}
75+
76+
func SwapDevicesWithContext(ctx context.Context) ([]*SwapDevice, error) {
77+
return nil, common.ErrNotImplementedError
78+
}

v3/mem/mem_darwin_cgo.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
// +build darwin
2-
// +build cgo
1+
// +build darwin,cgo
32

43
package mem
54

v3/mem/mem_darwin_nocgo.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
// +build darwin
2-
// +build !cgo
1+
// +build darwin,!cgo
32

43
package mem
54

v3/mem/mem_fallback.go

+8
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,11 @@ func SwapMemory() (*SwapMemoryStat, error) {
2323
func SwapMemoryWithContext(ctx context.Context) (*SwapMemoryStat, error) {
2424
return nil, common.ErrNotImplementedError
2525
}
26+
27+
func SwapDevices() ([]*SwapDevice, error) {
28+
return SwapDevicesWithContext(context.Background())
29+
}
30+
31+
func SwapDevicesWithContext(ctx context.Context) ([]*SwapDevice, error) {
32+
return nil, common.ErrNotImplementedError
33+
}

v3/mem/mem_linux.go

+86
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
package mem
44

55
import (
6+
"bufio"
67
"context"
78
"encoding/json"
9+
"fmt"
10+
"io"
811
"math"
912
"os"
1013
"strconv"
@@ -426,3 +429,86 @@ func calcuateAvailVmem(ret *VirtualMemoryStat, retEx *VirtualMemoryExStat) uint6
426429

427430
return availMemory
428431
}
432+
433+
const swapsFilename = "swaps"
434+
435+
// swaps file column indexes
436+
const (
437+
nameCol = 0
438+
// typeCol = 1
439+
totalCol = 2
440+
usedCol = 3
441+
// priorityCol = 4
442+
)
443+
444+
func SwapDevices() ([]*SwapDevice, error) {
445+
return SwapDevicesWithContext(context.Background())
446+
}
447+
448+
func SwapDevicesWithContext(ctx context.Context) ([]*SwapDevice, error) {
449+
swapsFilePath := common.HostProc(swapsFilename)
450+
f, err := os.Open(swapsFilePath)
451+
if err != nil {
452+
return nil, err
453+
}
454+
defer f.Close()
455+
456+
return parseSwapsFile(f)
457+
}
458+
459+
func parseSwapsFile(r io.Reader) ([]*SwapDevice, error) {
460+
swapsFilePath := common.HostProc(swapsFilename)
461+
scanner := bufio.NewScanner(r)
462+
if !scanner.Scan() {
463+
if err := scanner.Err(); err != nil {
464+
return nil, fmt.Errorf("couldn't read file %q: %w", swapsFilePath, err)
465+
}
466+
return nil, fmt.Errorf("unexpected end-of-file in %q", swapsFilePath)
467+
468+
}
469+
470+
// Check header headerFields are as expected
471+
headerFields := strings.Fields(scanner.Text())
472+
if len(headerFields) < usedCol {
473+
return nil, fmt.Errorf("couldn't parse %q: too few fields in header", swapsFilePath)
474+
}
475+
if headerFields[nameCol] != "Filename" {
476+
return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapsFilePath, headerFields[nameCol], "Filename")
477+
}
478+
if headerFields[totalCol] != "Size" {
479+
return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapsFilePath, headerFields[totalCol], "Size")
480+
}
481+
if headerFields[usedCol] != "Used" {
482+
return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapsFilePath, headerFields[usedCol], "Used")
483+
}
484+
485+
var swapDevices []*SwapDevice
486+
for scanner.Scan() {
487+
fields := strings.Fields(scanner.Text())
488+
if len(fields) < usedCol {
489+
return nil, fmt.Errorf("couldn't parse %q: too few fields", swapsFilePath)
490+
}
491+
492+
totalKiB, err := strconv.ParseUint(fields[totalCol], 10, 64)
493+
if err != nil {
494+
return nil, fmt.Errorf("couldn't parse 'Size' column in %q: %w", swapsFilePath, err)
495+
}
496+
497+
usedKiB, err := strconv.ParseUint(fields[usedCol], 10, 64)
498+
if err != nil {
499+
return nil, fmt.Errorf("couldn't parse 'Used' column in %q: %w", swapsFilePath, err)
500+
}
501+
502+
swapDevices = append(swapDevices, &SwapDevice{
503+
Name: fields[nameCol],
504+
UsedBytes: usedKiB * 1024,
505+
FreeBytes: (totalKiB - usedKiB) * 1024,
506+
})
507+
}
508+
509+
if err := scanner.Err(); err != nil {
510+
return nil, fmt.Errorf("couldn't read file %q: %w", swapsFilePath, err)
511+
}
512+
513+
return swapDevices, nil
514+
}

v3/mem/mem_linux_test.go

+43
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
// +build linux
2+
13
package mem
24

35
import (
46
"os"
57
"path/filepath"
68
"reflect"
9+
"strings"
710
"testing"
11+
12+
"github.com/stretchr/testify/assert"
813
)
914

1015
func TestVirtualMemoryEx(t *testing.T) {
@@ -115,3 +120,41 @@ func TestVirtualMemoryLinux(t *testing.T) {
115120
})
116121
}
117122
}
123+
124+
const validFile = `Filename Type Size Used Priority
125+
/dev/dm-2 partition 67022844 490788 -2
126+
/swapfile file 2 1 -3
127+
`
128+
129+
const invalidFile = `INVALID Type Size Used Priority
130+
/dev/dm-2 partition 67022844 490788 -2
131+
/swapfile file 1048572 0 -3
132+
`
133+
134+
func TestParseSwapsFile_ValidFile(t *testing.T) {
135+
assert := assert.New(t)
136+
stats, err := parseSwapsFile(strings.NewReader(validFile))
137+
assert.NoError(err)
138+
139+
assert.Equal(*stats[0], SwapDevice{
140+
Name: "/dev/dm-2",
141+
UsedBytes: 502566912,
142+
FreeBytes: 68128825344,
143+
})
144+
145+
assert.Equal(*stats[1], SwapDevice{
146+
Name: "/swapfile",
147+
UsedBytes: 1024,
148+
FreeBytes: 1024,
149+
})
150+
}
151+
152+
func TestParseSwapsFile_InvalidFile(t *testing.T) {
153+
_, err := parseSwapsFile(strings.NewReader(invalidFile))
154+
assert.Error(t, err)
155+
}
156+
157+
func TestParseSwapsFile_EmptyFile(t *testing.T) {
158+
_, err := parseSwapsFile(strings.NewReader(""))
159+
assert.Error(t, err)
160+
}

v3/mem/mem_openbsd_386.go

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

v3/mem/mem_openbsd_arm64.go

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

v3/mem/mem_solaris.go

+84
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// +build solaris
2+
13
package mem
24

35
import (
@@ -119,3 +121,85 @@ func nonGlobalZoneMemoryCapacity() (uint64, error) {
119121

120122
return memSizeBytes, nil
121123
}
124+
125+
const swapsCommand = "swap"
126+
127+
// The blockSize as reported by `swap -l`. See https://docs.oracle.com/cd/E23824_01/html/821-1459/fsswap-52195.html
128+
const blockSize = 512
129+
130+
// swapctl column indexes
131+
const (
132+
nameCol = 0
133+
// devCol = 1
134+
// swaploCol = 2
135+
totalBlocksCol = 3
136+
freeBlocksCol = 4
137+
)
138+
139+
func SwapDevices() ([]*SwapDevice, error) {
140+
return SwapDevicesWithContext(context.Background())
141+
}
142+
143+
func SwapDevicesWithContext(ctx context.Context) ([]*SwapDevice, error) {
144+
swapsCommandPath, err := exec.LookPath(swapsCommand)
145+
if err != nil {
146+
return nil, fmt.Errorf("could not find command %q: %w", swapCommand, err)
147+
}
148+
output, err := invoke.CommandWithContext(swapsCommandPath, "-l")
149+
if err != nil {
150+
return nil, fmt.Errorf("could not execute %q: %w", swapsCommand, err)
151+
}
152+
153+
return parseSwapsCommandOutput(string(output))
154+
}
155+
156+
func parseSwapsCommandOutput(output string) ([]*SwapDevice, error) {
157+
lines := strings.Split(output, "\n")
158+
if len(lines) == 0 {
159+
return nil, fmt.Errorf("could not parse output of %q: no lines in %q", swapsCommand, output)
160+
}
161+
162+
// Check header headerFields are as expected.
163+
headerFields := strings.Fields(lines[0])
164+
if len(headerFields) < freeBlocksCol {
165+
return nil, fmt.Errorf("couldn't parse %q: too few fields in header %q", swapsCommand, lines[0])
166+
}
167+
if headerFields[nameCol] != "swapfile" {
168+
return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapsCommand, headerFields[nameCol], "swapfile")
169+
}
170+
if headerFields[totalBlocksCol] != "blocks" {
171+
return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapsCommand, headerFields[totalBlocksCol], "blocks")
172+
}
173+
if headerFields[freeBlocksCol] != "free" {
174+
return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapsCommand, headerFields[freeBlocksCol], "free")
175+
}
176+
177+
var swapDevices []*SwapDevice
178+
for _, line := range lines[1:] {
179+
if line == "" {
180+
continue // the terminal line is typically empty
181+
}
182+
fields := strings.Fields(line)
183+
if len(fields) < freeBlocksCol {
184+
return nil, fmt.Errorf("couldn't parse %q: too few fields", swapsCommand)
185+
}
186+
187+
totalBlocks, err := strconv.ParseUint(fields[totalBlocksCol], 10, 64)
188+
if err != nil {
189+
return nil, fmt.Errorf("couldn't parse 'Size' column in %q: %w", swapsCommand, err)
190+
}
191+
192+
freeBlocks, err := strconv.ParseUint(fields[freeBlocksCol], 10, 64)
193+
if err != nil {
194+
return nil, fmt.Errorf("couldn't parse 'Used' column in %q: %w", swapsCommand, err)
195+
}
196+
197+
swapDevices = append(swapDevices, &SwapDevice{
198+
Name: fields[nameCol],
199+
UsedBytes: (totalBlocks - freeBlocks) * blockSize,
200+
FreeBytes: freeBlocks * blockSize,
201+
})
202+
}
203+
204+
return swapDevices, nil
205+
}

v3/mem/mem_test.go

+26
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,29 @@ func TestSwapMemoryStat_String(t *testing.T) {
110110
t.Errorf("SwapMemoryStat string is invalid: %v", v)
111111
}
112112
}
113+
114+
func TestSwapDevices(t *testing.T) {
115+
v, err := SwapDevices()
116+
skipIfNotImplementedErr(t, err)
117+
if err != nil {
118+
t.Fatalf("error calling SwapDevices: %v", err)
119+
}
120+
121+
t.Logf("SwapDevices() -> %+v", v)
122+
123+
if len(v) == 0 {
124+
t.Fatalf("no swap devices found. [this is expected if the host has swap disabled]")
125+
}
126+
127+
for _, device := range v {
128+
if device.Name == "" {
129+
t.Fatalf("deviceName not set in %+v", device)
130+
}
131+
if device.FreeBytes == 0 {
132+
t.Logf("[WARNING] free-bytes is zero in %+v. This might be expected", device)
133+
}
134+
if device.UsedBytes == 0 {
135+
t.Logf("[WARNING] used-bytes is zero in %+v. This might be expected", device)
136+
}
137+
}
138+
}

0 commit comments

Comments
 (0)