github.com/isyscore/isc-gobase@v1.5.3-0.20231218061332-cbc7451899e9/system/net/net_darwin.go (about)

     1  //go:build darwin
     2  
     3  package net
     4  
     5  import (
     6  	"context"
     7  	"errors"
     8  	"fmt"
     9  	"github.com/isyscore/isc-gobase/system/common"
    10  	"os/exec"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  )
    15  
    16  var (
    17  	errNetstatHeader  = errors.New("can't parse header of netstat output")
    18  	netstatLinkRegexp = regexp.MustCompile(`^<Link#(\d+)>$`)
    19  )
    20  
    21  const endOfLine = "\n"
    22  
    23  func parseNetstatLine(line string) (stat *IOCountersStat, linkID *uint, err error) {
    24  	var (
    25  		numericValue uint64
    26  		columns      = strings.Fields(line)
    27  	)
    28  
    29  	if columns[0] == "Name" {
    30  		err = errNetstatHeader
    31  		return
    32  	}
    33  
    34  	// try to extract the numeric value from <Link#123>
    35  	if subMatch := netstatLinkRegexp.FindStringSubmatch(columns[2]); len(subMatch) == 2 {
    36  		numericValue, err = strconv.ParseUint(subMatch[1], 10, 64)
    37  		if err != nil {
    38  			return
    39  		}
    40  		linkIDUint := uint(numericValue)
    41  		linkID = &linkIDUint
    42  	}
    43  
    44  	base := 1
    45  	numberColumns := len(columns)
    46  	// sometimes Address is omitted
    47  	if numberColumns < 12 {
    48  		base = 0
    49  	}
    50  	if numberColumns < 11 || numberColumns > 13 {
    51  		err = fmt.Errorf("line %q do have an invalid number of columns %d", line, numberColumns)
    52  		return
    53  	}
    54  
    55  	parsed := make([]uint64, 0, 7)
    56  	vv := []string{
    57  		columns[base+3], // Ipkts == PacketsRecv
    58  		columns[base+4], // Ierrs == Errin
    59  		columns[base+5], // Ibytes == BytesRecv
    60  		columns[base+6], // Opkts == PacketsSent
    61  		columns[base+7], // Oerrs == Errout
    62  		columns[base+8], // Obytes == BytesSent
    63  	}
    64  	if len(columns) == 12 {
    65  		vv = append(vv, columns[base+10])
    66  	}
    67  
    68  	for _, target := range vv {
    69  		if target == "-" {
    70  			parsed = append(parsed, 0)
    71  			continue
    72  		}
    73  
    74  		if numericValue, err = strconv.ParseUint(target, 10, 64); err != nil {
    75  			return
    76  		}
    77  		parsed = append(parsed, numericValue)
    78  	}
    79  
    80  	stat = &IOCountersStat{
    81  		Name:        strings.Trim(columns[0], "*"), // remove the * that sometimes is on right on interface
    82  		PacketsRecv: parsed[0],
    83  		Errin:       parsed[1],
    84  		BytesRecv:   parsed[2],
    85  		PacketsSent: parsed[3],
    86  		Errout:      parsed[4],
    87  		BytesSent:   parsed[5],
    88  	}
    89  	if len(parsed) == 7 {
    90  		stat.Dropout = parsed[6]
    91  	}
    92  	return
    93  }
    94  
    95  type netstatInterface struct {
    96  	linkID *uint
    97  	stat   *IOCountersStat
    98  }
    99  
   100  func parseNetstatOutput(output string) ([]netstatInterface, error) {
   101  	var (
   102  		err   error
   103  		lines = strings.Split(strings.Trim(output, endOfLine), endOfLine)
   104  	)
   105  
   106  	// number of interfaces is number of lines less one for the header
   107  	numberInterfaces := len(lines) - 1
   108  
   109  	interfaces := make([]netstatInterface, numberInterfaces)
   110  	// no output beside header
   111  	if numberInterfaces == 0 {
   112  		return interfaces, nil
   113  	}
   114  
   115  	for index := 0; index < numberInterfaces; index++ {
   116  		nsIface := netstatInterface{}
   117  		if nsIface.stat, nsIface.linkID, err = parseNetstatLine(lines[index+1]); err != nil {
   118  			return nil, err
   119  		}
   120  		interfaces[index] = nsIface
   121  	}
   122  	return interfaces, nil
   123  }
   124  
   125  // map that hold the name of a network interface and the number of usage
   126  type mapInterfaceNameUsage map[string]uint
   127  
   128  func newMapInterfaceNameUsage(ifaces []netstatInterface) mapInterfaceNameUsage {
   129  	output := make(mapInterfaceNameUsage)
   130  	for index := range ifaces {
   131  		if ifaces[index].linkID != nil {
   132  			ifaceName := ifaces[index].stat.Name
   133  			usage, ok := output[ifaceName]
   134  			if ok {
   135  				output[ifaceName] = usage + 1
   136  			} else {
   137  				output[ifaceName] = 1
   138  			}
   139  		}
   140  	}
   141  	return output
   142  }
   143  
   144  func (min mapInterfaceNameUsage) isTruncated() bool {
   145  	for _, usage := range min {
   146  		if usage > 1 {
   147  			return true
   148  		}
   149  	}
   150  	return false
   151  }
   152  
   153  func (min mapInterfaceNameUsage) notTruncated() []string {
   154  	output := make([]string, 0)
   155  	for ifaceName, usage := range min {
   156  		if usage == 1 {
   157  			output = append(output, ifaceName)
   158  		}
   159  	}
   160  	return output
   161  }
   162  
   163  // IOCounters example of `netstat -ibdnW` output on yosemite
   164  // Name  Mtu   Network       Address            Ipkts Ierrs     Ibytes    Opkts Oerrs     Obytes  Coll Drop
   165  // lo0   16384 <Link#1>                        869107     0  169411755   869107     0  169411755     0   0
   166  // lo0   16384 ::1/128     ::1                 869107     -  169411755   869107     -  169411755     -   -
   167  // lo0   16384 127           127.0.0.1         869107     -  169411755   869107     -  169411755     -   -
   168  func IOCounters(pernic bool) ([]IOCountersStat, error) {
   169  	return IOCountersWithContext(context.Background(), pernic)
   170  }
   171  
   172  func IOCountersWithContext(ctx context.Context, pernic bool) ([]IOCountersStat, error) {
   173  	var (
   174  		ret      []IOCountersStat
   175  		retIndex int
   176  	)
   177  
   178  	netstat, err := exec.LookPath("netstat")
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  
   183  	// try to get all interface metrics, and hope there won't be any truncated
   184  	out, err := invoke.CommandWithContext(ctx, netstat, "-ibdnW")
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  
   189  	nsInterfaces, err := parseNetstatOutput(string(out))
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  
   194  	ifaceUsage := newMapInterfaceNameUsage(nsInterfaces)
   195  	notTruncated := ifaceUsage.notTruncated()
   196  	ret = make([]IOCountersStat, len(notTruncated))
   197  
   198  	if !ifaceUsage.isTruncated() {
   199  		// no truncated interface name, return stats of all interface with <Link#...>
   200  		for index := range nsInterfaces {
   201  			if nsInterfaces[index].linkID != nil {
   202  				ret[retIndex] = *nsInterfaces[index].stat
   203  				retIndex++
   204  			}
   205  		}
   206  	} else {
   207  		// duplicated interface, list all interfaces
   208  		ifconfig, err := exec.LookPath("ifconfig")
   209  		if err != nil {
   210  			return nil, err
   211  		}
   212  		if out, err = invoke.CommandWithContext(ctx, ifconfig, "-l"); err != nil {
   213  			return nil, err
   214  		}
   215  		interfaceNames := strings.Fields(strings.TrimRight(string(out), endOfLine))
   216  
   217  		// for each of the interface name, run netstat if we don't have any stats yet
   218  		for _, interfaceName := range interfaceNames {
   219  			truncated := true
   220  			for index := range nsInterfaces {
   221  				if nsInterfaces[index].linkID != nil && nsInterfaces[index].stat.Name == interfaceName {
   222  					// handle the non-truncated name to avoid execute netstat for them again
   223  					ret[retIndex] = *nsInterfaces[index].stat
   224  					retIndex++
   225  					truncated = false
   226  					break
   227  				}
   228  			}
   229  			if truncated {
   230  				// run netstat with -I$ifacename
   231  				if out, err = invoke.CommandWithContext(ctx, netstat, "-ibdnWI"+interfaceName); err != nil {
   232  					return nil, err
   233  				}
   234  				parsedIfaces, err := parseNetstatOutput(string(out))
   235  				if err != nil {
   236  					return nil, err
   237  				}
   238  				if len(parsedIfaces) == 0 {
   239  					// interface had been removed since `ifconfig -l` had been executed
   240  					continue
   241  				}
   242  				for index := range parsedIfaces {
   243  					if parsedIfaces[index].linkID != nil {
   244  						ret = append(ret, *parsedIfaces[index].stat)
   245  						break
   246  					}
   247  				}
   248  			}
   249  		}
   250  	}
   251  
   252  	if pernic == false {
   253  		return getIOCountersAll(ret)
   254  	}
   255  	return ret, nil
   256  }
   257  
   258  // IOCountersByFile NetIOCountersByFile is a method which is added just a compatibility for linux.
   259  func IOCountersByFile(pernic bool, filename string) ([]IOCountersStat, error) {
   260  	return IOCountersByFileWithContext(context.Background(), pernic, filename)
   261  }
   262  
   263  func IOCountersByFileWithContext(ctx context.Context, pernic bool, filename string) ([]IOCountersStat, error) {
   264  	return IOCounters(pernic)
   265  }
   266  
   267  func FilterCounters() ([]FilterStat, error) {
   268  	return FilterCountersWithContext(context.Background())
   269  }
   270  
   271  func FilterCountersWithContext(ctx context.Context) ([]FilterStat, error) {
   272  	return nil, common.ErrNotImplementedError
   273  }
   274  
   275  func ConntrackStats(percpu bool) ([]ConntrackStat, error) {
   276  	return ConntrackStatsWithContext(context.Background(), percpu)
   277  }
   278  
   279  func ConntrackStatsWithContext(ctx context.Context, percpu bool) ([]ConntrackStat, error) {
   280  	return nil, common.ErrNotImplementedError
   281  }
   282  
   283  // ProtoCounters NetProtoCounters returns network statistics for the entire system
   284  // If protocols is empty then all protocols are returned, otherwise
   285  // just the protocols in the list are returned.
   286  // Not Implemented for Darwin
   287  func ProtoCounters(protocols []string) ([]ProtoCountersStat, error) {
   288  	return ProtoCountersWithContext(context.Background(), protocols)
   289  }
   290  
   291  func ProtoCountersWithContext(ctx context.Context, protocols []string) ([]ProtoCountersStat, error) {
   292  	return nil, common.ErrNotImplementedError
   293  }