diff --git a/common/common_darwin.go b/common/common_darwin.go index 7d6f3c6..cc74601 100644 --- a/common/common_darwin.go +++ b/common/common_darwin.go @@ -4,6 +4,7 @@ package common import ( "os/exec" + "strconv" "strings" "syscall" "unsafe" @@ -58,3 +59,31 @@ func CallSyscall(mib []int32) ([]byte, uint64, error) { return buf, length, nil } + +func CallLsof(invoke Invoker, pid int32, args ...string) ([]string, error) { + var cmd []string + if pid == 0 { // will get from all processes. + cmd = []string{"-a", "-n", "-P"} + } else { + cmd = []string{"-a", "-n", "-P", "-p", strconv.Itoa(int(pid))} + } + cmd = append(cmd, args...) + lsof, err := exec.LookPath("lsof") + if err != nil { + return []string{}, err + } + out, err := invoke.Command(lsof, cmd...) + if err != nil { + return []string{}, err + } + lines := strings.Split(string(out), "\n") + + var ret []string + for _, l := range lines[1:] { + if len(l) == 0 { + continue + } + ret = append(ret, l) + } + return ret, nil +} diff --git a/common/common_linux.go b/common/common_linux.go new file mode 100644 index 0000000..3c760e5 --- /dev/null +++ b/common/common_linux.go @@ -0,0 +1,37 @@ +// +build linux + +package common + +import ( + "os/exec" + "strconv" + "strings" +) + +func CallLsof(invoke Invoker, pid int32, args ...string) ([]string, error) { + var cmd []string + if pid == 0 { // will get from all processes. + cmd = []string{"-a", "-n", "-P"} + } else { + cmd = []string{"-a", "-n", "-P", "-p", strconv.Itoa(int(pid))} + } + cmd = append(cmd, args...) + lsof, err := exec.LookPath("lsof") + if err != nil { + return []string{}, err + } + out, err := invoke.Command(lsof, cmd...) + if err != nil { + return []string{}, err + } + lines := strings.Split(string(out), "\n") + + var ret []string + for _, l := range lines[1:] { + if len(l) == 0 { + continue + } + ret = append(ret, l) + } + return ret, nil +} diff --git a/net/net.go b/net/net.go index 8255177..506fe57 100644 --- a/net/net.go +++ b/net/net.go @@ -2,9 +2,21 @@ package net import ( "encoding/json" + "fmt" "net" + "strconv" + "strings" + "syscall" + + "github.com/shirou/gopsutil/common" ) +var invoke common.Invoker + +func init() { + invoke = common.Invoke{} +} + type NetIOCountersStat struct { Name string `json:"name"` // interface name BytesSent uint64 `json:"bytes_sent"` // number of bytes sent @@ -46,6 +58,13 @@ type NetInterfaceStat struct { Addrs []NetInterfaceAddr `json:"addrs"` } +var constMap = map[string]int{ + "TCP": syscall.SOCK_STREAM, + "UDP": syscall.SOCK_DGRAM, + "IPv4": syscall.AF_INET, + "IPv6": syscall.AF_INET6, +} + func (n NetIOCountersStat) String() string { s, _ := json.Marshal(n) return string(s) @@ -135,3 +154,74 @@ func getNetIOCountersAll(n []NetIOCountersStat) ([]NetIOCountersStat, error) { return []NetIOCountersStat{r}, nil } + +func parseNetLine(line string) (NetConnectionStat, error) { + f := strings.Fields(line) + if len(f) < 9 { + return NetConnectionStat{}, fmt.Errorf("wrong line,%s", line) + } + + pid, err := strconv.Atoi(f[1]) + if err != nil { + return NetConnectionStat{}, err + } + fd, err := strconv.Atoi(strings.Trim(f[3], "u")) + if err != nil { + return NetConnectionStat{}, fmt.Errorf("unknown fd, %s", f[3]) + } + netFamily, ok := constMap[f[4]] + if !ok { + return NetConnectionStat{}, fmt.Errorf("unknown family, %s", f[4]) + } + netType, ok := constMap[f[7]] + if !ok { + return NetConnectionStat{}, fmt.Errorf("unknown type, %s", f[7]) + } + + laddr, raddr, err := parseNetAddr(f[8]) + if err != nil { + return NetConnectionStat{}, fmt.Errorf("failed to parse netaddr, %s", f[8]) + } + + n := NetConnectionStat{ + Fd: uint32(fd), + Family: uint32(netFamily), + Type: uint32(netType), + Laddr: laddr, + Raddr: raddr, + Pid: int32(pid), + } + if len(f) == 10 { + n.Status = strings.Trim(f[9], "()") + } + + return n, nil +} + +func parseNetAddr(line string) (laddr Addr, raddr Addr, err error) { + parse := func(l string) (Addr, error) { + host, port, err := net.SplitHostPort(l) + if err != nil { + return Addr{}, fmt.Errorf("wrong addr, %s", l) + } + lport, err := strconv.Atoi(port) + if err != nil { + return Addr{}, err + } + return Addr{IP: host, Port: uint32(lport)}, nil + } + + addrs := strings.Split(line, "->") + if len(addrs) == 0 { + return laddr, raddr, fmt.Errorf("wrong netaddr, %s", line) + } + laddr, err = parse(addrs[0]) + if len(addrs) == 2 { // remote addr exists + raddr, err = parse(addrs[1]) + if err != nil { + return laddr, raddr, err + } + } + + return laddr, raddr, err +} diff --git a/net/net_darwin.go b/net/net_darwin.go index 65c21f8..da722dc 100644 --- a/net/net_darwin.go +++ b/net/net_darwin.go @@ -90,3 +90,58 @@ func NetIOCounters(pernic bool) ([]NetIOCountersStat, error) { return ret, nil } + +// Return a list of network connections opened by a process +func NetConnections(kind string) ([]NetConnectionStat, error) { + var ret []NetConnectionStat + + args := []string{"-i"} + switch strings.ToLower(kind) { + default: + fallthrough + case "": + fallthrough + case "all": + fallthrough + case "inet": + args = append(args, "tcp") + case "inet4": + args = append(args, "4") + case "inet6": + args = append(args, "6") + case "tcp": + args = append(args, "tcp") + case "tcp4": + args = append(args, "4tcp") + case "tcp6": + args = append(args, "6tcp") + case "udp": + args = append(args, "udp") + case "udp4": + args = append(args, "6udp") + case "udp6": + args = append(args, "6udp") + case "unix": + return ret, common.NotImplementedError + } + + // we can not use -F filter to get all of required information at once. + r, err := common.CallLsof(invoke, 0, args...) + if err != nil { + return nil, err + } + for _, rr := range r { + if strings.HasPrefix(rr, "COMMAND") { + continue + } + n, err := parseNetLine(rr) + if err != nil { + // fmt.Println(err) // TODO: should debug print? + continue + } + + ret = append(ret, n) + } + + return ret, nil +} diff --git a/net/net_freebsd.go b/net/net_freebsd.go index 703d321..7cc93a5 100644 --- a/net/net_freebsd.go +++ b/net/net_freebsd.go @@ -81,3 +81,9 @@ func NetIOCounters(pernic bool) ([]NetIOCountersStat, error) { return ret, nil } + +func NetConnections(kind string) ([]NetConnectionStat, error) { + var ret []NetConnectionStat + + return ret, common.NotImplementedError +} diff --git a/net/net_linux.go b/net/net_linux.go index 9184439..de0d65a 100644 --- a/net/net_linux.go +++ b/net/net_linux.go @@ -89,3 +89,57 @@ func NetIOCounters(pernic bool) ([]NetIOCountersStat, error) { return ret, nil } + +func NetConnections(kind string) ([]NetConnectionStat, error) { + var ret []NetConnectionStat + + args := []string{"-i"} + switch strings.ToLower(kind) { + default: + fallthrough + case "": + fallthrough + case "all": + fallthrough + case "inet": + args = append(args, "tcp") + case "inet4": + args = append(args, "4") + case "inet6": + args = append(args, "6") + case "tcp": + args = append(args, "tcp") + case "tcp4": + args = append(args, "4tcp") + case "tcp6": + args = append(args, "6tcp") + case "udp": + args = append(args, "udp") + case "udp4": + args = append(args, "6udp") + case "udp6": + args = append(args, "6udp") + case "unix": + return ret, common.NotImplementedError + } + + // we can not use -F filter to get all of required information at once. + r, err := common.CallLsof(invoke, 0, args...) + if err != nil { + return nil, err + } + for _, rr := range r { + if strings.HasPrefix(rr, "COMMAND") { + continue + } + n, err := parseNetLine(rr) + if err != nil { + // fmt.Println(err) // TODO: should debug print? + continue + } + + ret = append(ret, n) + } + + return ret, nil +} diff --git a/net/net_test.go b/net/net_test.go index a6648fb..9337e53 100644 --- a/net/net_test.go +++ b/net/net_test.go @@ -2,6 +2,7 @@ package net import ( "fmt" + "os" "testing" ) @@ -120,3 +121,23 @@ func TestNetInterfaces(t *testing.T) { } } } + +func TestNetConnections(t *testing.T) { + if ci := os.Getenv("CI"); ci != "" { // skip if test on drone.io + return + } + + v, err := NetConnections("inet") + if err != nil { + t.Errorf("could not get NetConnections: %v", err) + } + if len(v) == 0 { + t.Errorf("could not get NetConnections: %v", v) + } + for _, vv := range v { + if vv.Family == 0 { + t.Errorf("invalid NetConnections: %v", vv) + } + } + +} diff --git a/process/process_darwin.go b/process/process_darwin.go index d4c0e2e..634d5cf 100644 --- a/process/process_darwin.go +++ b/process/process_darwin.go @@ -4,6 +4,7 @@ package process import ( "bytes" + "fmt" "strconv" "strings" "syscall" @@ -92,7 +93,22 @@ func (p *Process) Cwd() (string, error) { return "", common.NotImplementedError } func (p *Process) Parent() (*Process, error) { - return p, common.NotImplementedError + rr, err := common.CallLsof(invoke, p.Pid, "-FR") + if err != nil { + return nil, err + } + for _, r := range rr { + if strings.HasPrefix(r, "p") { // skip if process + continue + } + l := string(r) + v, err := strconv.Atoi(strings.Replace(l, "R", "", 1)) + if err != nil { + return nil, err + } + return NewProcess(int32(v)) + } + return nil, fmt.Errorf("could not find parent line") } func (p *Process) Status() (string, error) { r, err := callPs("state", p.Pid, false) @@ -166,15 +182,6 @@ func (p *Process) NumFDs() (int32, error) { return 0, common.NotImplementedError } func (p *Process) NumThreads() (int32, error) { - /* - k, err := p.getKProc() - if err != nil { - return 0, err - } - - return k.KiNumthreads, nil - */ - r, err := callPs("utime,stime", p.Pid, true) if err != nil { return 0, err diff --git a/process/process_linux.go b/process/process_linux.go index 9c145e6..79039bf 100644 --- a/process/process_linux.go +++ b/process/process_linux.go @@ -4,6 +4,7 @@ package process import ( "encoding/json" + "fmt" "io/ioutil" "os" "path/filepath" @@ -11,10 +12,10 @@ import ( "strings" "syscall" - common "github.com/shirou/gopsutil/common" - cpu "github.com/shirou/gopsutil/cpu" - host "github.com/shirou/gopsutil/host" - net "github.com/shirou/gopsutil/net" + "github.com/shirou/gopsutil/common" + "github.com/shirou/gopsutil/cpu" + "github.com/shirou/gopsutil/host" + "github.com/shirou/gopsutil/net" ) const ( @@ -94,7 +95,18 @@ func (p *Process) Cwd() (string, error) { return p.fillFromCwd() } func (p *Process) Parent() (*Process, error) { - return nil, common.NotImplementedError + r, err := callLsof("R", p.Pid) + if err != nil { + return nil, err + } + if len(r) != 1 { // TODO: pid 1 + return nil, fmt.Errorf("wrong number of parents") + } + v, err := strconv.Atoi(r[0]) + if err != nil { + return nil, err + } + return NewProcess(int32(v)) } func (p *Process) Status() (string, error) { err := p.fillFromStatus() @@ -619,3 +631,26 @@ func Pids() ([]int32, error) { return ret, nil } + +func callLsof(arg string, pid int32) ([]string, error) { + var cmd []string + if pid == 0 { // will get from all processes. + cmd = []string{"-F" + arg} + } else { + cmd = []string{"-a", "-F" + arg, "-p", strconv.Itoa(int(pid))} + } + out, err := invoke.Command("/usr/bin/lsof", cmd...) + if err != nil { + return []string{}, err + } + lines := strings.Split(string(out), "\n") + + var ret []string + for _, l := range lines[1:] { + if strings.HasPrefix(l, arg) { + ret = append(ret, l[1:]) // delete first char + } + } + + return ret, nil +} diff --git a/process/process_test.go b/process/process_test.go index 84d311b..dba0ed9 100644 --- a/process/process_test.go +++ b/process/process_test.go @@ -1,7 +1,6 @@ package process import ( - "fmt" "os" "runtime" "strings" @@ -38,7 +37,7 @@ func Test_Pids_Fail(t *testing.T) { mu.Lock() defer mu.Unlock() - invoke = common.FakeInvoke{Suffix: "fail", Error: fmt.Errorf("hoge")} + invoke = common.FakeInvoke{Suffix: "fail"} ret, err := Pids() invoke = common.Invoke{} if err != nil { @@ -48,7 +47,6 @@ func Test_Pids_Fail(t *testing.T) { t.Errorf("wrong getted pid nums: %v/%d", ret, len(ret)) } } - func Test_Pid_exists(t *testing.T) { checkPid := os.Getpid() @@ -276,3 +274,18 @@ func Test_Process_CreateTime(t *testing.T) { t.Errorf("process created time is wrong.") } } + +func Test_Parent(t *testing.T) { + p := testGetProcess() + + c, err := p.Parent() + if err != nil { + t.Fatalf("error %v", err) + } + if c == nil { + t.Fatalf("could not get parent") + } + if c.Pid == 0 { + t.Fatalf("wrong parent pid") + } +}