From 3f9631205724541ced5513ae334352b61e5acfec Mon Sep 17 00:00:00 2001 From: Bruno Clermont Date: Sun, 4 Sep 2016 16:36:41 +0200 Subject: [PATCH] only run ifconfig/netstat if necessary, add some tests --- net/net_darwin.go | 274 +++++++++++++++++++++++++++++++++++++------------ net/net_darwin_test.go | 140 +++++++++++++++++++++++++ 2 files changed, 347 insertions(+), 67 deletions(-) create mode 100644 net/net_darwin_test.go diff --git a/net/net_darwin.go b/net/net_darwin.go index 215ed5b..771736b 100644 --- a/net/net_darwin.go +++ b/net/net_darwin.go @@ -4,108 +4,248 @@ package net import ( "errors" + "fmt" "os/exec" + "regexp" "strconv" "strings" ) -// example of `netstat -ibdnWI lo0` output on yosemite +var ( + errNetstatHeader = errors.New("Can't parse header of netstat output") + netstatLinkRegexp = regexp.MustCompile(`^$`) +) + +const endOfLine = "\n" + +func parseNetstatLine(line string) (stat *IOCountersStat, linkId *uint, err error) { + var ( + numericValue uint64 + columns = strings.Fields(line) + ) + + if columns[0] == "Name" { + err = errNetstatHeader + return + } + + // try to extract the numeric value from + if subMatch := netstatLinkRegexp.FindStringSubmatch(columns[2]); len(subMatch) == 2 { + numericValue, err = strconv.ParseUint(subMatch[1], 10, 64) + if err != nil { + return + } + linkIdUint := uint(numericValue) + linkId = &linkIdUint + } + + base := 1 + numberColumns := len(columns) + // sometimes Address is ommitted + if numberColumns < 12 { + base = 0 + } + if numberColumns < 11 || numberColumns > 13 { + err = fmt.Errorf("Line %q do have an invalid number of columns %d", line, numberColumns) + return + } + + parsed := make([]uint64, 0, 7) + vv := []string{ + columns[base+3], // Ipkts == PacketsRecv + columns[base+4], // Ierrs == Errin + columns[base+5], // Ibytes == BytesRecv + columns[base+6], // Opkts == PacketsSent + columns[base+7], // Oerrs == Errout + columns[base+8], // Obytes == BytesSent + } + if len(columns) == 12 { + vv = append(vv, columns[base+10]) + } + + for _, target := range vv { + if target == "-" { + parsed = append(parsed, 0) + continue + } + + if numericValue, err = strconv.ParseUint(target, 10, 64); err != nil { + return + } + parsed = append(parsed, numericValue) + } + + stat = &IOCountersStat{ + Name: strings.Trim(columns[0], "*"), // remove the * that sometimes is on right on interface + PacketsRecv: parsed[0], + Errin: parsed[1], + BytesRecv: parsed[2], + PacketsSent: parsed[3], + Errout: parsed[4], + BytesSent: parsed[5], + } + if len(parsed) == 7 { + stat.Dropout = parsed[6] + } + return +} + +type netstatInterface struct { + linkId *uint + stat *IOCountersStat +} + +func parseNetstatOutput(output string) ([]netstatInterface, error) { + var ( + err error + lines = strings.Split(strings.Trim(output, endOfLine), endOfLine) + ) + + // number of interfaces is number of lines less one for the header + numberInterfaces := len(lines) - 1 + + interfaces := make([]netstatInterface, numberInterfaces) + // no output beside header + if numberInterfaces == 0 { + return interfaces, nil + } + + for index := 0; index < numberInterfaces; index++ { + nsIface := netstatInterface{} + if nsIface.stat, nsIface.linkId, err = parseNetstatLine(lines[index+1]); err != nil { + return nil, err + } + interfaces[index] = nsIface + } + return interfaces, nil +} + +// map that hold the name of a network interface and the number of usage +type mapInterfaceNameUsage map[string]uint + +func newMapInterfaceNameUsage(ifaces []netstatInterface) mapInterfaceNameUsage { + output := make(mapInterfaceNameUsage) + for index := range ifaces { + if ifaces[index].linkId != nil { + ifaceName := ifaces[index].stat.Name + usage, ok := output[ifaceName] + if ok { + output[ifaceName] = usage + 1 + } else { + output[ifaceName] = 1 + } + } + } + return output +} + +func (min mapInterfaceNameUsage) isTruncated() bool { + for _, usage := range min { + if usage > 1 { + return true + } + } + return false +} + +func (min mapInterfaceNameUsage) notTruncated() []string { + output := make([]string, 0) + for ifaceName, usage := range min { + if usage == 1 { + output = append(output, ifaceName) + } + } + return output +} + +// example of `netstat -ibdnW` output on yosemite // Name Mtu Network Address Ipkts Ierrs Ibytes Opkts Oerrs Obytes Coll Drop // lo0 16384 869107 0 169411755 869107 0 169411755 0 0 // lo0 16384 ::1/128 ::1 869107 - 169411755 869107 - 169411755 - - // lo0 16384 127 127.0.0.1 869107 - 169411755 869107 - 169411755 - - func IOCounters(pernic bool) ([]IOCountersStat, error) { - const endOfLine = "\n" - // example of `ifconfig -l` output on yosemite: - // lo0 gif0 stf0 en0 p2p0 awdl0 - ifconfig, err := exec.LookPath("/sbin/ifconfig") + var ( + ret []IOCountersStat + retIndex int + ) + + netstat, err := exec.LookPath("/usr/sbin/netstat") if err != nil { return nil, err } - netstat, err := exec.LookPath("/usr/sbin/netstat") + // try to get all interface metrics, and hope there won't be any truncated + out, err := invoke.Command(netstat, "-ibdnW") if err != nil { return nil, err } - // list all interfaces - out, err := invoke.Command(ifconfig, "-l") + nsInterfaces, err := parseNetstatOutput(string(out)) if err != nil { return nil, err } - interfaces := strings.Fields(strings.TrimRight(string(out), endOfLine)) - ret := make([]IOCountersStat, 0) - // extract metrics for all interfaces - for _, interfaceName := range interfaces { - if out, err = invoke.Command(netstat, "-ibdnWI" + interfaceName); err != nil { - return nil, err - } - lines := strings.Split(string(out), endOfLine) - if len(lines) <= 1 { - // invalid output - continue - } + ifaceUsage := newMapInterfaceNameUsage(nsInterfaces) + notTruncated := ifaceUsage.notTruncated() + ret = make([]IOCountersStat, len(notTruncated)) - if len(lines[1]) == 0 { - // interface had been removed since `ifconfig -l` had been executed - continue - } - - // only the first output is fine - values := strings.Fields(lines[1]) - - base := 1 - // sometimes Address is ommitted - if len(values) < 12 { - base = 0 + if !ifaceUsage.isTruncated() { + // no truncated interface name, return stats of all interface with + for index := range nsInterfaces { + if nsInterfaces[index].linkId != nil { + ret[retIndex] = *nsInterfaces[index].stat + retIndex++ + } } - - parsed := make([]uint64, 0, 7) - vv := []string{ - values[base+3], // Ipkts == PacketsRecv - values[base+4], // Ierrs == Errin - values[base+5], // Ibytes == BytesRecv - values[base+6], // Opkts == PacketsSent - values[base+7], // Oerrs == Errout - values[base+8], // Obytes == BytesSent + } else { + // duplicated interface, list all interfaces + ifconfig, err := exec.LookPath("/sbin/ifconfig") + if err != nil { + return nil, err } - if len(values) == 12 { - vv = append(vv, values[base+10]) + if out, err = invoke.Command(ifconfig, "-l"); err != nil { + return nil, err } + interfaceNames := strings.Fields(strings.TrimRight(string(out), endOfLine)) - for _, target := range vv { - if target == "-" { - parsed = append(parsed, 0) - continue + // for each of the interface name, run netstat if we don't have any stats yet + for _, interfaceName := range interfaceNames { + truncated := true + for index := range nsInterfaces { + if nsInterfaces[index].linkId != nil && nsInterfaces[index].stat.Name == interfaceName { + // handle the non truncated name to avoid execute netstat for them again + ret[retIndex] = *nsInterfaces[index].stat + retIndex++ + truncated = false + break + } } - - t, err := strconv.ParseUint(target, 10, 64) - if err != nil { - return nil, err + if truncated { + // run netstat with -I$ifacename + if out, err = invoke.Command(netstat, "-ibdnWI" + interfaceName);err != nil { + return nil, err + } + parsedIfaces, err := parseNetstatOutput(string(out)) + if err != nil { + return nil, err + } + if len(parsedIfaces) == 0 { + // interface had been removed since `ifconfig -l` had been executed + continue + } + for index := range parsedIfaces { + if parsedIfaces[index].linkId != nil { + ret = append(ret, *parsedIfaces[index].stat) + break + } + } } - parsed = append(parsed, t) } - - n := IOCountersStat{ - Name: interfaceName, - PacketsRecv: parsed[0], - Errin: parsed[1], - BytesRecv: parsed[2], - PacketsSent: parsed[3], - Errout: parsed[4], - BytesSent: parsed[5], - } - if len(parsed) == 7 { - n.Dropout = parsed[6] - } - ret = append(ret, n) } if pernic == false { return getIOCountersAll(ret) } - return ret, nil } diff --git a/net/net_darwin_test.go b/net/net_darwin_test.go new file mode 100644 index 0000000..cae12d5 --- /dev/null +++ b/net/net_darwin_test.go @@ -0,0 +1,140 @@ +package net + +import ( + "testing" + + assert "github.com/stretchr/testify/require" +) + +const ( + netstatTruncated = `Name Mtu Network Address Ipkts Ierrs Ibytes Opkts Oerrs Obytes Coll Drop +lo0 16384 31241 0 3769823 31241 0 3769823 0 0 +lo0 16384 ::1/128 ::1 31241 - 3769823 31241 - 3769823 - - +lo0 16384 127 127.0.0.1 31241 - 3769823 31241 - 3769823 - - +lo0 16384 fe80::1%lo0 fe80:1::1 31241 - 3769823 31241 - 3769823 - - +gif0* 1280 0 0 0 0 0 0 0 0 +stf0* 1280 0 0 0 0 0 0 0 0 +utun8 1500 286 0 27175 0 0 0 0 0 +utun8 1500 286 0 29554 0 0 0 0 0 +utun8 1500 286 0 29244 0 0 0 0 0 +utun8 1500 286 0 28267 0 0 0 0 0 +utun8 1500 286 0 28593 0 0 0 0 0` + netstatNotTruncated = `Name Mtu Network Address Ipkts Ierrs Ibytes Opkts Oerrs Obytes Coll Drop +lo0 16384 27190978 0 12824763793 27190978 0 12824763793 0 0 +lo0 16384 ::1/128 ::1 27190978 - 12824763793 27190978 - 12824763793 - - +lo0 16384 127 127.0.0.1 27190978 - 12824763793 27190978 - 12824763793 - - +lo0 16384 fe80::1%lo0 fe80:1::1 27190978 - 12824763793 27190978 - 12824763793 - - +gif0* 1280 0 0 0 0 0 0 0 0 +stf0* 1280 0 0 0 0 0 0 0 0 +en0 1500 a8:66:7f:dd:ee:ff 5708989 0 7295722068 3494252 0 379533492 0 230 +en0 1500 fe80::aa66: fe80:4::aa66:7fff 5708989 - 7295722068 3494252 - 379533492 - -` +) + +func TestparseNetstatLineHeader(t *testing.T) { + stat, linkIkd, err := parseNetstatLine(`Name Mtu Network Address Ipkts Ierrs Ibytes Opkts Oerrs Obytes Coll Drop`) + assert.Nil(t, linkIkd) + assert.Nil(t, stat) + assert.Error(t, err) + assert.Equal(t, errNetstatHeader, err) +} + +func assertLoopbackStat(t *testing.T, err error, stat *IOCountersStat) { + assert.NoError(t, err) + assert.Equal(t, 869107, stat.PacketsRecv) + assert.Equal(t, 0, stat.Errin) + assert.Equal(t, 169411755, stat.BytesRecv) + assert.Equal(t,869108, stat.PacketsSent) + assert.Equal(t, 1, stat.Errout) + assert.Equal(t, 169411756, stat.BytesSent) +} + +func TestparseNetstatLineLink(t *testing.T) { + stat, linkId, err := parseNetstatLine( + `lo0 16384 869107 0 169411755 869108 1 169411756 0 0`, + ) + assertLoopbackStat(t, err, stat) + assert.NotNil(t, linkId) + assert.Equal(t, uint(1), *linkId) +} + +func TestparseNetstatLineIPv6(t *testing.T) { + stat, linkId, err := parseNetstatLine( + `lo0 16384 ::1/128 ::1 869107 - 169411755 869108 1 169411756 - -`, + ) + assertLoopbackStat(t, err, stat) + assert.Nil(t, linkId) +} + +func TestparseNetstatLineIPv4(t *testing.T) { + stat, linkId, err := parseNetstatLine( + `lo0 16384 127 127.0.0.1 869107 - 169411755 869108 1 169411756 - -`, + ) + assertLoopbackStat(t, err, stat) + assert.Nil(t, linkId) +} + +func TestParseNetstatOutput(t *testing.T) { + nsInterfaces, err := parseNetstatOutput(netstatNotTruncated) + assert.NoError(t, err) + assert.Len(t, nsInterfaces, 8) + for index := range nsInterfaces { + assert.NotNil(t, nsInterfaces[index].stat, "Index %d", index) + } + + assert.NotNil(t, nsInterfaces[0].linkId) + assert.Equal(t, uint(1), *nsInterfaces[0].linkId) + + assert.Nil(t, nsInterfaces[1].linkId) + assert.Nil(t, nsInterfaces[2].linkId) + assert.Nil(t, nsInterfaces[3].linkId) + + assert.NotNil(t, nsInterfaces[4].linkId) + assert.Equal(t, uint(2), *nsInterfaces[4].linkId) + + assert.NotNil(t, nsInterfaces[5].linkId) + assert.Equal(t, uint(3), *nsInterfaces[5].linkId) + + assert.NotNil(t, nsInterfaces[6].linkId) + assert.Equal(t, uint(4), *nsInterfaces[6].linkId) + + assert.Nil(t, nsInterfaces[7].linkId) + + mapUsage := newMapInterfaceNameUsage(nsInterfaces) + assert.False(t, mapUsage.isTruncated()) + assert.Len(t, mapUsage.notTruncated(), 4) +} + +func TestParseNetstatTruncated(t *testing.T) { + nsInterfaces, err := parseNetstatOutput(netstatTruncated) + assert.NoError(t, err) + assert.Len(t, nsInterfaces, 11) + for index := range nsInterfaces { + assert.NotNil(t, nsInterfaces[index].stat, "Index %d", index) + } + + const truncatedIface = "utun8" + + assert.NotNil(t, nsInterfaces[6].linkId) + assert.Equal(t, uint(88), *nsInterfaces[6].linkId) + assert.Equal(t, truncatedIface, nsInterfaces[6].stat.Name) + + assert.NotNil(t, nsInterfaces[7].linkId) + assert.Equal(t,uint(90), *nsInterfaces[7].linkId) + assert.Equal(t, truncatedIface, nsInterfaces[7].stat.Name) + + assert.NotNil(t, nsInterfaces[8].linkId) + assert.Equal(t, uint(92), *nsInterfaces[8].linkId ) + assert.Equal(t, truncatedIface, nsInterfaces[8].stat.Name) + + assert.NotNil(t, nsInterfaces[9].linkId) + assert.Equal(t, uint(93), *nsInterfaces[9].linkId ) + assert.Equal(t, truncatedIface, nsInterfaces[9].stat.Name) + + assert.NotNil(t, nsInterfaces[10].linkId) + assert.Equal(t, uint(95), *nsInterfaces[10].linkId ) + assert.Equal(t, truncatedIface, nsInterfaces[10].stat.Name) + + mapUsage := newMapInterfaceNameUsage(nsInterfaces) + assert.True(t, mapUsage.isTruncated()) + assert.Equal(t, 3, len(mapUsage.notTruncated()), "en0, gif0 and stf0") +}