Repository: prometheus/procfs
Pull request #92 (2018-06-11 03:55:47)

add /proc/net/unix parsing
Signed-off-by: madk <luc.mdk@gmail.com>

SuperQ: Looks great, mostly minor nits.
peonone: Fixed those nits.

Also updated the comment related to `Inode` field, and updated the type of this field to `uint64`, because it's  `unsigned long` in the code which generates the proc file.

peonone: @grobie 's comments make sense, I added validation for the length and handled the case of a blank path.
pgier: @peonone any chance you can rebase this one against latest master?  Sorry this wasn't merged earlier.  After fixing the conflict I think we can merge it.
peonone: > @peonone any chance you can rebase this one against latest master? Sorry this wasn't merged earlier. After fixing the conflict I think we can merge it.

@pgier Sure, I will do this later this week.

Patch:
diff --git a/README.md b/README.md
index 951c043..7066e11 100644
--- a/README.md
+++ b/README.md
@@ -14,12 +14,12 @@ backwards-incompatible ways without warnings. Use it at your own risk.
 
 The procfs library is organized by packages based on whether the gathered data is coming from
 /proc, /sys, or both.  Each package contains an `FS` type which represents the path to either /proc, /sys, or both.  For example, current cpu statistics are gathered from
-`/proc/stat` and are available via the root procfs package.  First, the proc filesystem mount
+`/proc/stat` and are available via the root procfs package.  First, the proc filesystem mount 
 point is initialized, and then the stat information is read.
 
 ```go
-fs, err := procfs.NewFS("/proc")
-stats, err := fs.NewStat()
+	fs, err := procfs.NewFS("/proc")
+    stats, err := fs.NewStat()
 ```
 
 ## Building and Testing
@@ -36,11 +36,11 @@ ensure the `fixtures` directory is up to date by removing the existing directory
 extracting the ttar file using `make fixtures/.unpacked` or just `make test`.
 
 ```bash
-rm -rf fixtures
-make test
+    rm -rf fixtures
+    make test
 ```
 
-Next, make the required changes to the extracted files in the `fixtures` directory.  When
-the changes are complete, run `make update_fixtures` to create a new `fixtures.ttar` file
-based on the updated `fixtures` directory.  And finally, verify the changes using
+Next, make the required changes to the extracted files in the `fixtures` directory.  When 
+the changes are complete, run `make update-fixtures` to create a new `fixtures.ttar` file
+based on the updated `fixtures` directory.  And finally, verify the changes using 
 `git diff fixtures.ttar`.
diff --git a/fixtures.ttar b/fixtures.ttar
index 0ada8ae..3ad2ce0 100644
--- a/fixtures.ttar
+++ b/fixtures.ttar
@@ -459,6 +459,26 @@ proc4 2 2 10853
 proc4ops 72 0 0 0 1098 2 0 0 0 0 8179 5896 0 0 0 0 5900 0 0 2 0 2 0 9609 0 2 150 1272 0 0 0 1236 0 0 0 0 3 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 Mode: 644
 # ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/proc/net/unix
+Lines: 6
+Num       RefCount Protocol Flags    Type St Inode Path
+0000000000000000: 00000002 00000000 00010000 0001 01 3442596 /var/run/postgresql/.s.PGSQL.5432
+0000000000000000: 0000000a 00000000 00010000 0005 01 10061 /run/udev/control
+0000000000000000: 00000007 00000000 00000000 0002 01 12392 /dev/log
+0000000000000000: 00000003 00000000 00000000 0001 03 4787297 /var/run/postgresql/.s.PGSQL.5432
+0000000000000000: 00000003 00000000 00000000 0001 03 5091797
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+Path: fixtures/proc/net/unix_without_inode
+Lines: 6
+Num       RefCount Protocol Flags    Type St Path
+0000000000000000: 00000002 00000000 00010000 0001 01 /var/run/postgresql/.s.PGSQL.5432
+0000000000000000: 0000000a 00000000 00010000 0005 01 /run/udev/control
+0000000000000000: 00000007 00000000 00000000 0002 01 /dev/log
+0000000000000000: 00000003 00000000 00000000 0001 03 /var/run/postgresql/.s.PGSQL.5432
+0000000000000000: 00000003 00000000 00000000 0001 03
+Mode: 644
+# ttar - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 Path: fixtures/proc/net/xfrm_stat
 Lines: 28
 XfrmInError                     1
diff --git a/net_unix.go b/net_unix.go
new file mode 100644
index 0000000..240340a
--- /dev/null
+++ b/net_unix.go
@@ -0,0 +1,275 @@
+// Copyright 2018 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package procfs
+
+import (
+	"bufio"
+	"errors"
+	"fmt"
+	"io"
+	"os"
+	"strconv"
+	"strings"
+)
+
+// For the proc file format details,
+// see https://elixir.bootlin.com/linux/v4.17/source/net/unix/af_unix.c#L2815
+// and https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/net.h#L48.
+
+const (
+	netUnixKernelPtrIdx = iota
+	netUnixRefCountIdx
+	_
+	netUnixFlagsIdx
+	netUnixTypeIdx
+	netUnixStateIdx
+	netUnixInodeIdx
+
+	// Inode and Path are optional.
+	netUnixStaticFieldsCnt = 6
+)
+
+const (
+	netUnixTypeStream    = 1
+	netUnixTypeDgram     = 2
+	netUnixTypeSeqpacket = 5
+
+	netUnixFlagListen = 1 << 16
+
+	netUnixStateUnconnected  = 1
+	netUnixStateConnecting   = 2
+	netUnixStateConnected    = 3
+	netUnixStateDisconnected = 4
+)
+
+var errInvalidKernelPtrFmt = errors.New("Invalid Num(the kernel table slot number) format")
+
+// NetUnixType is the type of the type field.
+type NetUnixType uint64
+
+// NetUnixFlags is the type of the flags field.
+type NetUnixFlags uint64
+
+// NetUnixState is the type of the state field.
+type NetUnixState uint64
+
+// NetUnixLine represents a line of /proc/net/unix.
+type NetUnixLine struct {
+	KernelPtr string
+	RefCount  uint64
+	Protocol  uint64
+	Flags     NetUnixFlags
+	Type      NetUnixType
+	State     NetUnixState
+	Inode     uint64
+	Path      string
+}
+
+// NetUnix holds the data read from /proc/net/unix.
+type NetUnix struct {
+	Rows []*NetUnixLine
+}
+
+// NewNetUnix returns data read from /proc/net/unix.
+func NewNetUnix() (*NetUnix, error) {
+	fs, err := NewFS(DefaultMountPoint)
+	if err != nil {
+		return nil, err
+	}
+
+	return fs.NewNetUnix()
+}
+
+// NewNetUnix returns data read from /proc/net/unix.
+func (fs FS) NewNetUnix() (*NetUnix, error) {
+	return NewNetUnixByPath(fs.proc.Path("net/unix"))
+}
+
+// NewNetUnixByPath returns data read from /proc/net/unix by file path.
+// It might returns an error with partial parsed data, if an error occur after some data parsed.
+func NewNetUnixByPath(path string) (*NetUnix, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer f.Close()
+	return NewNetUnixByReader(f)
+}
+
+// NewNetUnixByReader returns data read from /proc/net/unix by a reader.
+// It might returns an error with partial parsed data, if an error occur after some data parsed.
+func NewNetUnixByReader(reader io.Reader) (*NetUnix, error) {
+	nu := &NetUnix{
+		Rows: make([]*NetUnixLine, 0, 32),
+	}
+	scanner := bufio.NewScanner(reader)
+	// Omit the header line.
+	scanner.Scan()
+	header := scanner.Text()
+	// From the man page of proc(5), it does not contain an Inode field,
+	// but in actually it exists.
+	// This code works for both cases.
+	hasInode := strings.Contains(header, "Inode")
+
+	minFieldsCnt := netUnixStaticFieldsCnt
+	if hasInode {
+		minFieldsCnt++
+	}
+	for scanner.Scan() {
+		line := scanner.Text()
+		item, err := nu.parseLine(line, hasInode, minFieldsCnt)
+		if err != nil {
+			return nu, err
+		}
+		nu.Rows = append(nu.Rows, item)
+	}
+
+	return nu, scanner.Err()
+}
+
+func (u *NetUnix) parseLine(line string, hasInode bool, minFieldsCnt int) (*NetUnixLine, error) {
+	fields := strings.Fields(line)
+	fieldsLen := len(fields)
+	if fieldsLen < minFieldsCnt {
+		return nil, fmt.Errorf(
+			"Parse Unix domain failed: expect at least %d fields but got %d",
+			minFieldsCnt, fieldsLen)
+	}
+	kernelPtr, err := u.parseKernelPtr(fields[netUnixKernelPtrIdx])
+	if err != nil {
+		return nil, fmt.Errorf("Parse Unix domain num(%s) failed: %s", fields[netUnixKernelPtrIdx], err)
+	}
+	users, err := u.parseUsers(fields[netUnixRefCountIdx])
+	if err != nil {
+		return nil, fmt.Errorf("Parse Unix domain ref count(%s) failed: %s", fields[netUnixRefCountIdx], err)
+	}
+	flags, err := u.parseFlags(fields[netUnixFlagsIdx])
+	if err != nil {
+		return nil, fmt.Errorf("Parse Unix domain flags(%s) failed: %s", fields[netUnixFlagsIdx], err)
+	}
+	typ, err := u.parseType(fields[netUnixTypeIdx])
+	if err != nil {
+		return nil, fmt.Errorf("Parse Unix domain type(%s) failed: %s", fields[netUnixTypeIdx], err)
+	}
+	state, err := u.parseState(fields[netUnixStateIdx])
+	if err != nil {
+		return nil, fmt.Errorf("Parse Unix domain state(%s) failed: %s", fields[netUnixStateIdx], err)
+	}
+	var inode uint64
+	if hasInode {
+		inodeStr := fields[netUnixInodeIdx]
+		inode, err = u.parseInode(inodeStr)
+		if err != nil {
+			return nil, fmt.Errorf("Parse Unix domain inode(%s) failed: %s", inodeStr, err)
+		}
+	}
+
+	nuLine := &NetUnixLine{
+		KernelPtr: kernelPtr,
+		RefCount:  users,
+		Type:      typ,
+		Flags:     flags,
+		State:     state,
+		Inode:     inode,
+	}
+
+	// Path field is optional.
+	if fieldsLen > minFieldsCnt {
+		pathIdx := netUnixInodeIdx + 1
+		if !hasInode {
+			pathIdx--
+		}
+		nuLine.Path = fields[pathIdx]
+	}
+
+	return nuLine, nil
+}
+
+func (u NetUnix) parseKernelPtr(str string) (string, error) {
+	if !strings.HasSuffix(str, ":") {
+		return "", errInvalidKernelPtrFmt
+	}
+	return str[:len(str)-1], nil
+}
+
+func (u NetUnix) parseUsers(hexStr string) (uint64, error) {
+	return strconv.ParseUint(hexStr, 16, 32)
+}
+
+func (u NetUnix) parseProtocol(hexStr string) (uint64, error) {
+	return strconv.ParseUint(hexStr, 16, 32)
+}
+
+func (u NetUnix) parseType(hexStr string) (NetUnixType, error) {
+	typ, err := strconv.ParseUint(hexStr, 16, 16)
+	if err != nil {
+		return 0, err
+	}
+	return NetUnixType(typ), nil
+}
+
+func (u NetUnix) parseFlags(hexStr string) (NetUnixFlags, error) {
+	flags, err := strconv.ParseUint(hexStr, 16, 32)
+	if err != nil {
+		return 0, err
+	}
+	return NetUnixFlags(flags), nil
+}
+
+func (u NetUnix) parseState(hexStr string) (NetUnixState, error) {
+	st, err := strconv.ParseInt(hexStr, 16, 8)
+	if err != nil {
+		return 0, err
+	}
+	return NetUnixState(st), nil
+}
+
+func (u NetUnix) parseInode(inodeStr string) (uint64, error) {
+	return strconv.ParseUint(inodeStr, 10, 64)
+}
+
+func (t NetUnixType) String() string {
+	switch t {
+	case netUnixTypeStream:
+		return "stream"
+	case netUnixTypeDgram:
+		return "dgram"
+	case netUnixTypeSeqpacket:
+		return "seqpacket"
+	}
+	return "unknown"
+}
+
+func (f NetUnixFlags) String() string {
+	switch f {
+	case netUnixFlagListen:
+		return "listen"
+	default:
+		return "default"
+	}
+}
+
+func (s NetUnixState) String() string {
+	switch s {
+	case netUnixStateUnconnected:
+		return "unconnected"
+	case netUnixStateConnecting:
+		return "connecting"
+	case netUnixStateConnected:
+		return "connected"
+	case netUnixStateDisconnected:
+		return "disconnected"
+	}
+	return "unknown"
+}


Test patch:
diff --git a/net_unix_test.go b/net_unix_test.go
new file mode 100644
index 0000000..4643ced
--- /dev/null
+++ b/net_unix_test.go
@@ -0,0 +1,179 @@
+// Copyright 2018 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package procfs
+
+import (
+	"testing"
+)
+
+func TestNewNetUnix(t *testing.T) {
+	fs, err := NewFS(procTestFixtures)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	nu, err := fs.NewNetUnix()
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	lines := []*NetUnixLine{
+		&NetUnixLine{
+			KernelPtr: "0000000000000000",
+			RefCount:  2,
+			Flags:     1 << 16,
+			Type:      1,
+			State:     1,
+			Inode:     3442596,
+			Path:      "/var/run/postgresql/.s.PGSQL.5432",
+		},
+		&NetUnixLine{
+			KernelPtr: "0000000000000000",
+			RefCount:  10,
+			Flags:     1 << 16,
+			Type:      5,
+			State:     1,
+			Inode:     10061,
+			Path:      "/run/udev/control",
+		},
+		&NetUnixLine{
+			KernelPtr: "0000000000000000",
+			RefCount:  7,
+			Flags:     0,
+			Type:      2,
+			State:     1,
+			Inode:     12392,
+			Path:      "/dev/log",
+		},
+		&NetUnixLine{
+			KernelPtr: "0000000000000000",
+			RefCount:  3,
+			Flags:     0,
+			Type:      1,
+			State:     3,
+			Inode:     4787297,
+			Path:      "/var/run/postgresql/.s.PGSQL.5432",
+		},
+		&NetUnixLine{
+			KernelPtr: "0000000000000000",
+			RefCount:  3,
+			Flags:     0,
+			Type:      1,
+			State:     3,
+			Inode:     5091797,
+		},
+	}
+
+	if want, have := len(lines), len(nu.Rows); want != have {
+		t.Errorf("want %d parsed net/unix lines, have %d", want, have)
+	}
+	for i, gotLine := range nu.Rows {
+		if i >= len(lines) {
+			continue
+		}
+		line := lines[i]
+		if *line != *gotLine {
+			t.Errorf("%d item: got %v, want %v", i, *gotLine, *line)
+		}
+	}
+
+	wantedFlags := "listen"
+	flags := lines[0].Flags.String()
+	if wantedFlags != flags {
+		t.Errorf("unexpected flag str: want %s, got %s", wantedFlags, flags)
+	}
+	wantedFlags = "default"
+	flags = lines[3].Flags.String()
+	if wantedFlags != flags {
+		t.Errorf("unexpected flag str: wanted %s, got %s", wantedFlags, flags)
+	}
+
+	wantedType := "stream"
+	typ := lines[0].Type.String()
+	if wantedType != typ {
+		t.Errorf("unexpected type str: wanted %s, got %s", wantedType, typ)
+	}
+
+	wantedType = "seqpacket"
+	typ = lines[1].Type.String()
+	if wantedType != typ {
+		t.Errorf("unexpected type str: want %s, got %s", wantedType, typ)
+	}
+}
+
+func TestNewNetUnixWithoutInode(t *testing.T) {
+	fs, err := NewFS(procTestFixtures)
+	if err != nil {
+		t.Fatal(err)
+	}
+	nu, err := NewNetUnixByPath(fs.proc.Path("net/unix_without_inode"))
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	lines := []*NetUnixLine{
+		&NetUnixLine{
+			KernelPtr: "0000000000000000",
+			RefCount:  2,
+			Flags:     1 << 16,
+			Type:      1,
+			State:     1,
+			Path:      "/var/run/postgresql/.s.PGSQL.5432",
+		},
+		&NetUnixLine{
+			KernelPtr: "0000000000000000",
+			RefCount:  10,
+			Flags:     1 << 16,
+			Type:      5,
+			State:     1,
+			Path:      "/run/udev/control",
+		},
+		&NetUnixLine{
+			KernelPtr: "0000000000000000",
+			RefCount:  7,
+			Flags:     0,
+			Type:      2,
+			State:     1,
+			Path:      "/dev/log",
+		},
+		&NetUnixLine{
+			KernelPtr: "0000000000000000",
+			RefCount:  3,
+			Flags:     0,
+			Type:      1,
+			State:     3,
+			Path:      "/var/run/postgresql/.s.PGSQL.5432",
+		},
+		&NetUnixLine{
+			KernelPtr: "0000000000000000",
+			RefCount:  3,
+			Flags:     0,
+			Type:      1,
+			State:     3,
+		},
+	}
+
+	if want, have := len(lines), len(nu.Rows); want != have {
+		t.Errorf("want %d parsed net/unix lines, have %d", want, have)
+	}
+	for i, gotLine := range nu.Rows {
+		if i >= len(lines) {
+			continue
+		}
+		line := lines[i]
+		if *line != *gotLine {
+			t.Errorf("%d item: got %v, want %v", i, *gotLine, *line)
+		}
+	}
+}