Skip to content

Commit 8d7a3ab

Browse files
authored
Merge pull request #1131 from punya/tbarker25-swap-contd
Add swapdevices method (continued from #1120)
2 parents 532241f + 582bb14 commit 8d7a3ab

15 files changed

+541
-10
lines changed

mem/mem.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ type SwapMemoryStat struct {
9191

9292
// Linux specific numbers
9393
// https://www.kernel.org/doc/Documentation/cgroup-v2.txt
94-
PgMajFault uint64 `json:"pgmajfault"`
94+
PgMajFault uint64 `json:"pgmajfault"`
9595
}
9696

9797
func (m VirtualMemoryStat) String() string {
@@ -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+
}

mem/mem_bsd.go

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// +build freebsd openbsd
2+
3+
package mem
4+
5+
import (
6+
"context"
7+
"fmt"
8+
"os/exec"
9+
"strconv"
10+
"strings"
11+
)
12+
13+
const swapCommand = "swapctl"
14+
15+
// swapctl column indexes
16+
const (
17+
nameCol = 0
18+
totalKiBCol = 1
19+
usedKiBCol = 2
20+
)
21+
22+
func SwapDevices() ([]*SwapDevice, error) {
23+
return SwapDevicesWithContext(context.Background())
24+
}
25+
26+
func SwapDevicesWithContext(ctx context.Context) ([]*SwapDevice, error) {
27+
swapCommandPath, err := exec.LookPath(swapCommand)
28+
if err != nil {
29+
return nil, fmt.Errorf("could not find command %q: %w", swapCommand, err)
30+
}
31+
output, err := invoke.CommandWithContext(swapCommandPath, "-lk")
32+
if err != nil {
33+
return nil, fmt.Errorf("could not execute %q: %w", swapCommand, err)
34+
}
35+
36+
return parseSwapctlOutput(string(output))
37+
}
38+
39+
func parseSwapctlOutput(output string) ([]*SwapDevice, error) {
40+
lines := strings.Split(output, "\n")
41+
if len(lines) == 0 {
42+
return nil, fmt.Errorf("could not parse output of %q: no lines in %q", swapCommand, output)
43+
}
44+
45+
// Check header headerFields are as expected.
46+
header := lines[0]
47+
header = strings.ToLower(header)
48+
header = strings.ReplaceAll(header, ":", "")
49+
headerFields := strings.Fields(header)
50+
if len(headerFields) < usedKiBCol {
51+
return nil, fmt.Errorf("couldn't parse %q: too few fields in header %q", swapCommand, header)
52+
}
53+
if headerFields[nameCol] != "device" {
54+
return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapCommand, headerFields[nameCol], "device")
55+
}
56+
if headerFields[totalKiBCol] != "1kb-blocks" && headerFields[totalKiBCol] != "1k-blocks" {
57+
return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapCommand, headerFields[totalKiBCol], "1kb-blocks")
58+
}
59+
if headerFields[usedKiBCol] != "used" {
60+
return nil, fmt.Errorf("couldn't parse %q: expected %q to be %q", swapCommand, headerFields[usedKiBCol], "used")
61+
}
62+
63+
var swapDevices []*SwapDevice
64+
for _, line := range lines[1:] {
65+
if line == "" {
66+
continue // the terminal line is typically empty
67+
}
68+
fields := strings.Fields(line)
69+
if len(fields) < usedKiBCol {
70+
return nil, fmt.Errorf("couldn't parse %q: too few fields", swapCommand)
71+
}
72+
73+
totalKiB, err := strconv.ParseUint(fields[totalKiBCol], 10, 64)
74+
if err != nil {
75+
return nil, fmt.Errorf("couldn't parse 'Size' column in %q: %w", swapCommand, err)
76+
}
77+
78+
usedKiB, err := strconv.ParseUint(fields[usedKiBCol], 10, 64)
79+
if err != nil {
80+
return nil, fmt.Errorf("couldn't parse 'Used' column in %q: %w", swapCommand, err)
81+
}
82+
83+
swapDevices = append(swapDevices, &SwapDevice{
84+
Name: fields[nameCol],
85+
UsedBytes: usedKiB * 1024,
86+
FreeBytes: (totalKiB - usedKiB) * 1024,
87+
})
88+
}
89+
90+
return swapDevices, nil
91+
}

mem/mem_bsd_test.go

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// +build freebsd openbsd
2+
3+
package mem
4+
5+
import (
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
const validFreeBSD = `Device: 1kB-blocks Used:
12+
/dev/gpt/swapfs 1048576 1234
13+
/dev/md0 1048576 666
14+
`
15+
16+
const validOpenBSD = `Device 1K-blocks Used Avail Capacity Priority
17+
/dev/wd0b 655025 1234 653791 1% 0
18+
`
19+
20+
const invalid = `Device: 512-blocks Used:
21+
/dev/gpt/swapfs 1048576 1234
22+
/dev/md0 1048576 666
23+
`
24+
25+
func TestParseSwapctlOutput_FreeBSD(t *testing.T) {
26+
assert := assert.New(t)
27+
stats, err := parseSwapctlOutput(validFreeBSD)
28+
assert.NoError(err)
29+
30+
assert.Equal(*stats[0], SwapDevice{
31+
Name: "/dev/gpt/swapfs",
32+
UsedBytes: 1263616,
33+
FreeBytes: 1072478208,
34+
})
35+
36+
assert.Equal(*stats[1], SwapDevice{
37+
Name: "/dev/md0",
38+
UsedBytes: 681984,
39+
FreeBytes: 1073059840,
40+
})
41+
}
42+
43+
func TestParseSwapctlOutput_OpenBSD(t *testing.T) {
44+
assert := assert.New(t)
45+
stats, err := parseSwapctlOutput(validOpenBSD)
46+
assert.NoError(err)
47+
48+
assert.Equal(*stats[0], SwapDevice{
49+
Name: "/dev/wd0b",
50+
UsedBytes: 1234 * 1024,
51+
FreeBytes: 653791 * 1024,
52+
})
53+
}
54+
55+
func TestParseSwapctlOutput_Invalid(t *testing.T) {
56+
_, err := parseSwapctlOutput(invalid)
57+
assert.Error(t, err)
58+
}
59+
60+
func TestParseSwapctlOutput_Empty(t *testing.T) {
61+
_, err := parseSwapctlOutput("")
62+
assert.Error(t, err)
63+
}

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/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+
}

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

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

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+
}

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+
}

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+
}

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.

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.

0 commit comments

Comments
 (0)