diff --git a/README.rst b/README.rst index 83e1da3..9ee9453 100644 --- a/README.rst +++ b/README.rst @@ -194,13 +194,13 @@ exe x x x uids x x x gids x x x terminal x x x -io_counters x x +io_counters x x x nice x x x x num_fds x num_ctx_switches x num_threads x x x x cpu_times x -memory_info x x x +memory_info x x x x memory_info_ex x memory_maps x open_files x diff --git a/cpu/cpu.go b/cpu/cpu.go index 0081cb7..b5417ae 100644 --- a/cpu/cpu.go +++ b/cpu/cpu.go @@ -2,6 +2,7 @@ package cpu import ( "encoding/json" + "github.com/shirou/gopsutil/internal/common" "runtime" "strconv" "strings" @@ -46,8 +47,10 @@ type cpuPercent struct { } var lastCPUPercent cpuPercent +var invoke common.Invoker func init() { + invoke = common.Invoke{} lastCPUPercent.Lock() lastCPUPercent.lastCPUTimes, _ = Times(false) lastCPUPercent.lastPerCPUTimes, _ = Times(true) diff --git a/cpu/cpu_darwin.go b/cpu/cpu_darwin.go index fbb74a8..4cb1d8c 100644 --- a/cpu/cpu_darwin.go +++ b/cpu/cpu_darwin.go @@ -36,7 +36,7 @@ func Info() ([]InfoStat, error) { if err != nil { return ret, err } - out, err := exec.Command(sysctl, "machdep.cpu").Output() + out, err := invoke.Command(sysctl, "machdep.cpu") if err != nil { return ret, err } @@ -90,7 +90,7 @@ func Info() ([]InfoStat, error) { // Use the rated frequency of the CPU. This is a static value and does not // account for low power or Turbo Boost modes. - out, err = exec.Command(sysctl, "hw.cpufrequency").Output() + out, err = invoke.Command(sysctl, "hw.cpufrequency") if err != nil { return ret, err } diff --git a/cpu/cpu_freebsd.go b/cpu/cpu_freebsd.go index ce1adf3..2f8d3ac 100644 --- a/cpu/cpu_freebsd.go +++ b/cpu/cpu_freebsd.go @@ -29,7 +29,7 @@ func init() { if err != nil { return } - out, err := exec.Command(getconf, "CLK_TCK").Output() + out, err := invoke.Command(getconf, "CLK_TCK") // ignore errors if err == nil { i, err := strconv.ParseFloat(strings.TrimSpace(string(out)), 64) diff --git a/cpu/cpu_linux.go b/cpu/cpu_linux.go index 975b75c..6df542b 100644 --- a/cpu/cpu_linux.go +++ b/cpu/cpu_linux.go @@ -19,7 +19,7 @@ func init() { if err != nil { return } - out, err := exec.Command(getconf, "CLK_TCK").Output() + out, err := invoke.Command(getconf, "CLK_TCK") // ignore errors if err == nil { i, err := strconv.ParseFloat(strings.TrimSpace(string(out)), 64) diff --git a/disk/disk.go b/disk/disk.go index b187a1d..3135c0c 100644 --- a/disk/disk.go +++ b/disk/disk.go @@ -2,8 +2,16 @@ package disk import ( "encoding/json" + + "github.com/shirou/gopsutil/internal/common" ) +var invoke common.Invoker + +func init() { + invoke = common.Invoke{} +} + type UsageStat struct { Path string `json:"path"` Fstype string `json:"fstype"` diff --git a/disk/disk_linux.go b/disk/disk_linux.go index c1c3345..f581ef7 100644 --- a/disk/disk_linux.go +++ b/disk/disk_linux.go @@ -339,7 +339,7 @@ func GetDiskSerialNumber(name string) string { return "" } - out, err := exec.Command(udevadm, "info", "--query=property", n).Output() + out, err := invoke.Command(udevadm, "info", "--query=property", n) // does not return error, just an empty string if err != nil { diff --git a/docker/docker.go b/docker/docker.go index 1bd3bdc..1d932cf 100644 --- a/docker/docker.go +++ b/docker/docker.go @@ -1,10 +1,20 @@ package docker -import "errors" +import ( + "errors" + + "github.com/shirou/gopsutil/internal/common" +) var ErrDockerNotAvailable = errors.New("docker not available") var ErrCgroupNotAvailable = errors.New("cgroup not available") +var invoke common.Invoker + +func init() { + invoke = common.Invoke{} +} + type CgroupMemStat struct { ContainerID string `json:"containerID"` Cache uint64 `json:"cache"` diff --git a/docker/docker_linux.go b/docker/docker_linux.go index 2501c16..000d2f2 100644 --- a/docker/docker_linux.go +++ b/docker/docker_linux.go @@ -23,7 +23,7 @@ func GetDockerStat() ([]CgroupDockerStat, error) { return nil, ErrDockerNotAvailable } - out, err := exec.Command(path, "ps", "-a", "--no-trunc", "--format", "{{.ID}}|{{.Image}}|{{.Names}}|{{.Status}}").Output() + out, err := invoke.Command(path, "ps", "-a", "--no-trunc", "--format", "{{.ID}}|{{.Image}}|{{.Names}}|{{.Status}}") if err != nil { return []CgroupDockerStat{}, err } @@ -65,7 +65,7 @@ func GetDockerIDList() ([]string, error) { return nil, ErrDockerNotAvailable } - out, err := exec.Command(path, "ps", "-q", "--no-trunc").Output() + out, err := invoke.Command(path, "ps", "-q", "--no-trunc") if err != nil { return []string{}, err } diff --git a/host/host.go b/host/host.go index 150eb60..1a6545b 100644 --- a/host/host.go +++ b/host/host.go @@ -2,17 +2,25 @@ package host import ( "encoding/json" + + "github.com/shirou/gopsutil/internal/common" ) +var invoke common.Invoker + +func init() { + invoke = common.Invoke{} +} + // A HostInfoStat describes the host status. // This is not in the psutil but it useful. type InfoStat struct { Hostname string `json:"hostname"` Uptime uint64 `json:"uptime"` BootTime uint64 `json:"bootTime"` - Procs uint64 `json:"procs"` // number of processes - OS string `json:"os"` // ex: freebsd, linux - Platform string `json:"platform"` // ex: ubuntu, linuxmint + Procs uint64 `json:"procs"` // number of processes + OS string `json:"os"` // ex: freebsd, linux + Platform string `json:"platform"` // ex: ubuntu, linuxmint PlatformFamily string `json:"platformFamily"` // ex: debian, rhel PlatformVersion string `json:"platformVersion"` VirtualizationSystem string `json:"virtualizationSystem"` diff --git a/host/host_darwin.go b/host/host_darwin.go index f4a8c36..ec2dc2c 100644 --- a/host/host_darwin.go +++ b/host/host_darwin.go @@ -131,12 +131,12 @@ func PlatformInformation() (string, string, string, error) { if err != nil { return "", "", "", err } - out, err := exec.Command(uname, "-s").Output() + out, err := invoke.Command(uname, "-s") if err == nil { platform = strings.ToLower(strings.TrimSpace(string(out))) } - out, err = exec.Command(uname, "-r").Output() + out, err = invoke.Command(uname, "-r") if err == nil { version = strings.ToLower(strings.TrimSpace(string(out))) } diff --git a/host/host_freebsd.go b/host/host_freebsd.go index aeb1b45..30206d2 100644 --- a/host/host_freebsd.go +++ b/host/host_freebsd.go @@ -136,12 +136,12 @@ func PlatformInformation() (string, string, string, error) { return "", "", "", err } - out, err := exec.Command(uname, "-s").Output() + out, err := invoke.Command(uname, "-s") if err == nil { platform = strings.ToLower(strings.TrimSpace(string(out))) } - out, err = exec.Command(uname, "-r").Output() + out, err = invoke.Command(uname, "-r") if err == nil { version = strings.ToLower(strings.TrimSpace(string(out))) } diff --git a/host/host_linux.go b/host/host_linux.go index 14d0935..97924bf 100644 --- a/host/host_linux.go +++ b/host/host_linux.go @@ -164,7 +164,7 @@ func getLSB() (*LSB, error) { if err != nil { return ret, err } - out, err := exec.Command(lsb_release).Output() + out, err := invoke.Command(lsb_release) if err != nil { return ret, err } diff --git a/internal/common/common.go b/internal/common/common.go index e190f4d..42cbaf9 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -8,8 +8,10 @@ package common // - windows (amd64) import ( "bufio" + "bytes" "errors" "io/ioutil" + "log" "net/url" "os" "os/exec" @@ -19,6 +21,12 @@ import ( "runtime" "strconv" "strings" + "time" +) + +var ( + Timeout = 3 * time.Second + TimeoutErr = errors.New("Command timed out.") ) type Invoker interface { @@ -28,7 +36,8 @@ type Invoker interface { type Invoke struct{} func (i Invoke) Command(name string, arg ...string) ([]byte, error) { - return exec.Command(name, arg...).Output() + cmd := exec.Command(name, arg...) + return CombinedOutputTimeout(cmd, Timeout) } type FakeInvoke struct { @@ -118,20 +127,20 @@ func IntToString(orig []int8) string { } func UintToString(orig []uint8) string { - ret := make([]byte, len(orig)) - size := -1 - for i, o := range orig { - if o == 0 { - size = i - break - } - ret[i] = byte(o) - } - if size == -1 { - size = len(orig) - } - - return string(ret[0:size]) + ret := make([]byte, len(orig)) + size := -1 + for i, o := range orig { + if o == 0 { + size = i + break + } + ret[i] = byte(o) + } + if size == -1 { + size = len(orig) + } + + return string(ret[0:size]) } func ByteToString(orig []byte) string { @@ -294,3 +303,41 @@ func HostSys(combineWith ...string) string { func HostEtc(combineWith ...string) string { return GetEnv("HOST_ETC", "/etc", combineWith...) } + +// CombinedOutputTimeout runs the given command with the given timeout and +// returns the combined output of stdout and stderr. +// If the command times out, it attempts to kill the process. +// copied from https://github.com/influxdata/telegraf +func CombinedOutputTimeout(c *exec.Cmd, timeout time.Duration) ([]byte, error) { + var b bytes.Buffer + c.Stdout = &b + c.Stderr = &b + if err := c.Start(); err != nil { + return nil, err + } + err := WaitTimeout(c, timeout) + return b.Bytes(), err +} + +// WaitTimeout waits for the given command to finish with a timeout. +// It assumes the command has already been started. +// If the command times out, it attempts to kill the process. +// copied from https://github.com/influxdata/telegraf +func WaitTimeout(c *exec.Cmd, timeout time.Duration) error { + timer := time.NewTimer(timeout) + done := make(chan error) + go func() { done <- c.Wait() }() + select { + case err := <-done: + timer.Stop() + return err + case <-timer.C: + if err := c.Process.Kill(); err != nil { + log.Printf("FATAL error killing process: %s", err) + return err + } + // wait for the command to return after killing it + <-done + return TimeoutErr + } +} diff --git a/mem/mem.go b/mem/mem.go index 5f122d1..3255890 100644 --- a/mem/mem.go +++ b/mem/mem.go @@ -2,8 +2,16 @@ package mem import ( "encoding/json" + + "github.com/shirou/gopsutil/internal/common" ) +var invoke common.Invoker + +func init() { + invoke = common.Invoke{} +} + // Memory usage statistics. Total, Available and Used contain numbers of bytes // for human consumption. // diff --git a/mem/mem_darwin_nocgo.go b/mem/mem_darwin_nocgo.go index 7094802..3eb33ab 100644 --- a/mem/mem_darwin_nocgo.go +++ b/mem/mem_darwin_nocgo.go @@ -16,7 +16,7 @@ func getVMStat(vms *VirtualMemoryStat) error { if err != nil { return err } - out, err := exec.Command(vm_stat).Output() + out, err := invoke.Command(vm_stat) if err != nil { return err } diff --git a/mem/mem_darwin_test.go b/mem/mem_darwin_test.go index 6d9e6b8..dba421f 100644 --- a/mem/mem_darwin_test.go +++ b/mem/mem_darwin_test.go @@ -3,7 +3,6 @@ package mem import ( - "os/exec" "strconv" "strings" "testing" @@ -15,7 +14,7 @@ func TestVirtualMemoryDarwin(t *testing.T) { v, err := VirtualMemory() assert.Nil(t, err) - outBytes, err := exec.Command("/usr/sbin/sysctl", "hw.memsize").Output() + outBytes, err := invoke.Command("/usr/sbin/sysctl", "hw.memsize") assert.Nil(t, err) outString := string(outBytes) outString = strings.TrimSpace(outString) diff --git a/mem/mem_freebsd.go b/mem/mem_freebsd.go index 7194057..8cca44b 100644 --- a/mem/mem_freebsd.go +++ b/mem/mem_freebsd.go @@ -93,7 +93,7 @@ func SwapMemory() (*SwapMemoryStat, error) { return nil, err } - out, err := exec.Command(swapinfo).Output() + out, err := invoke.Command(swapinfo) if err != nil { return nil, err } diff --git a/net/net_darwin.go b/net/net_darwin.go index 4fa358a..78ccb66 100644 --- a/net/net_darwin.go +++ b/net/net_darwin.go @@ -21,7 +21,7 @@ func IOCounters(pernic bool) ([]IOCountersStat, error) { if err != nil { return nil, err } - out, err := exec.Command(netstat, "-ibdnW").Output() + out, err := invoke.Command(netstat, "-ibdnW") if err != nil { return nil, err } diff --git a/net/net_freebsd.go b/net/net_freebsd.go index 3a67b4a..2b54655 100644 --- a/net/net_freebsd.go +++ b/net/net_freebsd.go @@ -16,7 +16,7 @@ func IOCounters(pernic bool) ([]IOCountersStat, error) { if err != nil { return nil, err } - out, err := exec.Command(netstat, "-ibdnW").Output() + out, err := invoke.Command(netstat, "-ibdnW") if err != nil { return nil, err } diff --git a/process/process_posix.go b/process/process_posix.go index 8853118..0751f68 100644 --- a/process/process_posix.go +++ b/process/process_posix.go @@ -9,6 +9,8 @@ import ( "strconv" "strings" "syscall" + + "github.com/shirou/gopsutil/internal/common" ) // POSIX @@ -72,7 +74,10 @@ func (p *Process) SendSignal(sig syscall.Signal) error { } cmd := exec.Command(kill, "-s", sigAsStr, strconv.Itoa(int(p.Pid))) cmd.Stderr = os.Stderr - err = cmd.Run() + if err := cmd.Start(); err != nil { + return err + } + err = common.WaitTimeout(cmd, common.Timeout) if err != nil { return err } diff --git a/process/process_windows.go b/process/process_windows.go index 3176cde..c3d2837 100644 --- a/process/process_windows.go +++ b/process/process_windows.go @@ -23,6 +23,11 @@ const ( MaxPathLength = 260 ) +var ( + modpsapi = syscall.NewLazyDLL("psapi.dll") + procGetProcessMemoryInfo = modpsapi.NewProc("GetProcessMemoryInfo") +) + type SystemProcessInformation struct { NextEntryOffset uint64 NumberOfThreads uint64 @@ -46,13 +51,18 @@ type MemoryMapsStat struct { } type Win32_Process struct { - Name string - ExecutablePath *string - CommandLine *string - Priority uint32 - CreationDate *time.Time - ProcessID uint32 - ThreadCount uint32 + Name string + ExecutablePath *string + CommandLine *string + Priority uint32 + CreationDate *time.Time + ProcessID uint32 + ThreadCount uint32 + Status *string + ReadOperationCount uint64 + ReadTransferCount uint64 + WriteOperationCount uint64 + WriteTransferCount uint64 /* CSCreationClassName string @@ -76,14 +86,9 @@ type Win32_Process struct { PeakVirtualSize uint64 PeakWorkingSetSize uint32 PrivatePageCount uint64 - ReadOperationCount uint64 - ReadTransferCount uint64 - Status *string TerminationDate *time.Time UserModeTime uint64 WorkingSetSize uint64 - WriteOperationCount uint64 - WriteTransferCount uint64 */ } @@ -158,12 +163,12 @@ func (p *Process) CmdlineSlice() ([]string, error) { } func (p *Process) CreateTime() (int64, error) { - dst, err := GetWin32Proc(p.Pid) + ru, err := getRusage(p.Pid) if err != nil { return 0, fmt.Errorf("could not get CreationDate: %s", err) } - date := *dst[0].CreationDate - return date.Unix(), nil + + return ru.CreationTime.Nanoseconds() / 1000000, nil } func (p *Process) Cwd() (string, error) { @@ -207,8 +212,20 @@ func (p *Process) Rlimit() ([]RlimitStat, error) { return rlimit, common.ErrNotImplementedError } + func (p *Process) IOCounters() (*IOCountersStat, error) { - return nil, common.ErrNotImplementedError + dst, err := GetWin32Proc(p.Pid) + if err != nil || len(dst) == 0 { + return nil, fmt.Errorf("could not get Win32Proc: %s", err) + } + ret := &IOCountersStat{ + ReadCount: uint64(dst[0].ReadOperationCount), + ReadBytes: uint64(dst[0].ReadTransferCount), + WriteCount: uint64(dst[0].WriteOperationCount), + WriteBytes: uint64(dst[0].WriteTransferCount), + } + + return ret, nil } func (p *Process) NumCtxSwitches() (*NumCtxSwitchesStat, error) { return nil, common.ErrNotImplementedError @@ -234,7 +251,17 @@ func (p *Process) CPUAffinity() ([]int32, error) { return nil, common.ErrNotImplementedError } func (p *Process) MemoryInfo() (*MemoryInfoStat, error) { - return nil, common.ErrNotImplementedError + mem, err := getMemoryInfo(p.Pid) + if err != nil { + return nil, err + } + + ret := &MemoryInfoStat{ + RSS: mem.WorkingSetSize, + VMS: mem.PagefileUsage, + } + + return ret, nil } func (p *Process) MemoryInfoEx() (*MemoryInfoExStat, error) { return nil, common.ErrNotImplementedError @@ -355,3 +382,45 @@ func getProcInfo(pid int32) (*SystemProcessInformation, error) { return &sysProcInfo, nil } + +func getRusage(pid int32) (*syscall.Rusage, error) { + var CPU syscall.Rusage + + c, err := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION, false, uint32(pid)) + if err != nil { + return nil, err + } + defer syscall.CloseHandle(c) + + if err := syscall.GetProcessTimes(c, &CPU.CreationTime, &CPU.ExitTime, &CPU.KernelTime, &CPU.UserTime); err != nil { + return nil, err + } + + return &CPU, nil +} + +func getMemoryInfo(pid int32) (PROCESS_MEMORY_COUNTERS, error) { + var mem PROCESS_MEMORY_COUNTERS + c, err := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION, false, uint32(pid)) + if err != nil { + return mem, err + } + defer syscall.CloseHandle(c) + if err := getProcessMemoryInfo(c, &mem); err != nil { + return mem, err + } + + return mem, err +} + +func getProcessMemoryInfo(h syscall.Handle, mem *PROCESS_MEMORY_COUNTERS) (err error) { + r1, _, e1 := syscall.Syscall(procGetProcessMemoryInfo.Addr(), 3, uintptr(h), uintptr(unsafe.Pointer(mem)), uintptr(unsafe.Sizeof(*mem))) + if r1 == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} diff --git a/process/process_windows_386.go b/process/process_windows_386.go new file mode 100644 index 0000000..68f3153 --- /dev/null +++ b/process/process_windows_386.go @@ -0,0 +1,16 @@ +// +build windows + +package process + +type PROCESS_MEMORY_COUNTERS struct { + CB uint32 + PageFaultCount uint32 + PeakWorkingSetSize uint32 + WorkingSetSize uint32 + QuotaPeakPagedPoolUsage uint32 + QuotaPagedPoolUsage uint32 + QuotaPeakNonPagedPoolUsage uint32 + QuotaNonPagedPoolUsage uint32 + PagefileUsage uint32 + PeakPagefileUsage uint32 +} diff --git a/process/process_windows_amd64.go b/process/process_windows_amd64.go new file mode 100644 index 0000000..df286df --- /dev/null +++ b/process/process_windows_amd64.go @@ -0,0 +1,16 @@ +// +build windows + +package process + +type PROCESS_MEMORY_COUNTERS struct { + CB uint32 + PageFaultCount uint32 + PeakWorkingSetSize uint64 + WorkingSetSize uint64 + QuotaPeakPagedPoolUsage uint64 + QuotaPagedPoolUsage uint64 + QuotaPeakNonPagedPoolUsage uint64 + QuotaNonPagedPoolUsage uint64 + PagefileUsage uint64 + PeakPagefileUsage uint64 +}