diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cdfc5627..196b1a939 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added ### Fixed +- Replaced the WMI queries with win32 apis due to high CPU usage. #11840 ### Changed diff --git a/sigar_interface_test.go b/sigar_interface_test.go index 8545484c5..cc55281f7 100644 --- a/sigar_interface_test.go +++ b/sigar_interface_test.go @@ -100,7 +100,6 @@ func TestProcState(t *testing.T) { assert.Equal(t, u.Username, state.Username) assert.True(t, state.Ppid > 0, "ppid=%v is non-positive", state.Ppid) } - assert.Error(t, state.Get(invalidPid)) } @@ -123,9 +122,12 @@ func TestProcTime(t *testing.T) { } func TestProcArgs(t *testing.T) { - args := ProcArgs{} - if assert.NoError(t, args.Get(os.Getppid())) { - assert.NotEmpty(t, args.List) + procArgs := ProcArgs{} + if assert.NoError(t, procArgs.Get(os.Getppid())) { + assert.NotEmpty(t, procArgs.List) + } + if runtime.GOOS != "darwin" { + assert.Error(t, procArgs.Get(invalidPid)) } } diff --git a/sigar_windows.go b/sigar_windows.go index 6da30808a..66f294e9c 100644 --- a/sigar_windows.go +++ b/sigar_windows.go @@ -12,26 +12,10 @@ import ( "syscall" "time" - "github.com/StackExchange/wmi" "github.com/elastic/gosigar/sys/windows" "github.com/pkg/errors" ) -// Win32_Process represents a process on the Windows operating system. If -// additional fields are added here (that match the Windows struct) they will -// automatically be populated when calling getWin32Process. -// https://msdn.microsoft.com/en-us/library/windows/desktop/aa394372(v=vs.85).aspx -type Win32_Process struct { - CommandLine *string -} - -// Win32_OperatingSystem WMI class represents a Windows-based operating system -// installed on a computer. -// https://msdn.microsoft.com/en-us/library/windows/desktop/aa394239(v=vs.85).aspx -type Win32_OperatingSystem struct { - LastBootUpTime time.Time -} - var ( // version is Windows version of the host OS. version = windows.GetWindowsVersion() @@ -83,11 +67,12 @@ func (self *Uptime) Get() error { bootTimeLock.Lock() defer bootTimeLock.Unlock() if bootTime == nil { - os, err := getWin32OperatingSystem() + uptime, err := windows.GetTickCount64() if err != nil { - return errors.Wrap(err, "failed to get boot time using WMI") + return errors.Wrap(err, "failed to get boot time using win32 api") } - bootTime = &os.LastBootUpTime + var boot = time.Unix(int64(uptime), 0) + bootTime = &boot } self.Length = time.Since(*bootTime).Seconds() @@ -251,7 +236,7 @@ func getProcStatus(pid int) (RunState, error) { var exitCode uint32 err = syscall.GetExitCodeProcess(handle, &exitCode) if err != nil { - return RunStateUnknown, errors.Wrapf(err, "GetExitCodeProcess failed for pid=%v") + return RunStateUnknown, errors.Wrapf(err, "GetExitCodeProcess failed for pid=%v", pid) } if exitCode == 259 { //still active @@ -371,15 +356,28 @@ func (self *ProcArgs) Get(pid int) error { if !version.IsWindowsVistaOrGreater() { return ErrNotImplemented{runtime.GOOS} } - - process, err := getWin32Process(int32(pid)) + handle, err := syscall.OpenProcess(processQueryLimitedInfoAccess|windows.PROCESS_VM_READ, false, uint32(pid)) + if err != nil { + return errors.Wrapf(err, "OpenProcess failed for pid=%v", pid) + } + defer syscall.CloseHandle(handle) + pbi, err := windows.NtQueryProcessBasicInformation(handle) if err != nil { - return errors.Wrapf(err, "ProcArgs failed for pid=%v", pid) + return errors.Wrapf(err, "NtQueryProcessBasicInformation failed for pid=%v", pid) } - if process.CommandLine != nil { - self.List = []string{*process.CommandLine} + if err != nil { + return nil + } + userProcParams, err := windows.GetUserProcessParams(handle, pbi) + if err != nil { + return nil + } + if argsW, err := windows.ReadProcessUnicodeString(handle, &userProcParams.CommandLine); err == nil { + self.List, err = windows.ByteSliceToStringSlice(argsW) + if err != nil { + return err + } } - return nil } @@ -396,35 +394,6 @@ func (self *FileSystemUsage) Get(path string) error { return nil } -// getWin32Process gets information about the process with the given process ID. -// It uses a WMI query to get the information from the local system. -func getWin32Process(pid int32) (Win32_Process, error) { - var dst []Win32_Process - query := fmt.Sprintf("WHERE ProcessId = %d", pid) - q := wmi.CreateQuery(&dst, query) - err := wmi.Query(q, &dst) - if err != nil { - return Win32_Process{}, fmt.Errorf("could not get Win32_Process %s: %v", query, err) - } - if len(dst) < 1 { - return Win32_Process{}, fmt.Errorf("could not get Win32_Process %s: Process not found", query) - } - return dst[0], nil -} - -func getWin32OperatingSystem() (Win32_OperatingSystem, error) { - var dst []Win32_OperatingSystem - q := wmi.CreateQuery(&dst, "") - err := wmi.Query(q, &dst) - if err != nil { - return Win32_OperatingSystem{}, errors.Wrap(err, "wmi query for Win32_OperatingSystem failed") - } - if len(dst) != 1 { - return Win32_OperatingSystem{}, errors.New("wmi query for Win32_OperatingSystem failed") - } - return dst[0], nil -} - func (self *Rusage) Get(who int) error { if who != 0 { return ErrNotImplemented{runtime.GOOS} diff --git a/sys/windows/syscall_windows.go b/sys/windows/syscall_windows.go index 36be45b30..655836701 100644 --- a/sys/windows/syscall_windows.go +++ b/sys/windows/syscall_windows.go @@ -23,6 +23,10 @@ const ( PROCESS_VM_READ uint32 = 0x0010 ) +// SizeOfRtlUserProcessParameters gives the size +// of the RtlUserProcessParameters struct. +const SizeOfRtlUserProcessParameters = unsafe.Sizeof(RtlUserProcessParameters{}) + // MAX_PATH is the maximum length for a path in Windows. // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx const MAX_PATH = 260 @@ -43,6 +47,26 @@ const ( DRIVE_RAMDISK ) +// UnicodeString is Go's equivalent for the _UNICODE_STRING struct. +type UnicodeString struct { + Size uint16 + MaximumLength uint16 + Buffer uintptr +} + +// RtlUserProcessParameters is Go's equivalent for the +// _RTL_USER_PROCESS_PARAMETERS struct. +// A few undocumented fields are exposed. +type RtlUserProcessParameters struct { + Reserved1 [16]byte + Reserved2 [5]uintptr + CurrentDirectoryPath UnicodeString + CurrentDirectoryHandle uintptr + DllPath UnicodeString + ImagePathName UnicodeString + CommandLine UnicodeString +} + func (dt DriveType) String() string { names := map[DriveType]string{ DRIVE_UNKNOWN: "unknown", @@ -441,6 +465,95 @@ func UTF16SliceToStringSlice(buffer []uint16) []string { return result } +func GetUserProcessParams(handle syscall.Handle, pbi ProcessBasicInformation) (params RtlUserProcessParameters, err error) { + const is32bitProc = unsafe.Sizeof(uintptr(0)) == 4 + + // Offset of params field within PEB structure. + // This structure is different in 32 and 64 bit. + paramsOffset := 0x20 + if is32bitProc { + paramsOffset = 0x10 + } + + // Read the PEB from the target process memory + pebSize := paramsOffset + 8 + peb := make([]byte, pebSize) + nRead, err := ReadProcessMemory(handle, pbi.PebBaseAddress, peb) + if err != nil { + return params, err + } + if nRead != uintptr(pebSize) { + return params, errors.Errorf("PEB: short read (%d/%d)", nRead, pebSize) + } + + // Get the RTL_USER_PROCESS_PARAMETERS struct pointer from the PEB + paramsAddr := *(*uintptr)(unsafe.Pointer(&peb[paramsOffset])) + + // Read the RTL_USER_PROCESS_PARAMETERS from the target process memory + paramsBuf := make([]byte, SizeOfRtlUserProcessParameters) + nRead, err = ReadProcessMemory(handle, paramsAddr, paramsBuf) + if err != nil { + return params, err + } + if nRead != uintptr(SizeOfRtlUserProcessParameters) { + return params, errors.Errorf("RTL_USER_PROCESS_PARAMETERS: short read (%d/%d)", nRead, SizeOfRtlUserProcessParameters) + } + + params = *(*RtlUserProcessParameters)(unsafe.Pointer(¶msBuf[0])) + return params, nil +} + +func ReadProcessUnicodeString(handle syscall.Handle, s *UnicodeString) ([]byte, error) { + buf := make([]byte, s.Size) + nRead, err := ReadProcessMemory(handle, s.Buffer, buf) + if err != nil { + return nil, err + } + if nRead != uintptr(s.Size) { + return nil, errors.Errorf("unicode string: short read: (%d/%d)", nRead, s.Size) + } + return buf, nil +} + +// Use Windows' CommandLineToArgv API to split an UTF-16 command line string +// into a list of parameters. +func ByteSliceToStringSlice(utf16 []byte) ([]string, error) { + if len(utf16) == 0 { + return nil, nil + } + var numArgs int32 + argsWide, err := syscall.CommandLineToArgv((*uint16)(unsafe.Pointer(&utf16[0])), &numArgs) + if err != nil { + return nil, err + } + args := make([]string, numArgs) + for idx := range args { + args[idx] = syscall.UTF16ToString(argsWide[idx][:]) + } + return args, nil +} + +// ReadProcessMemory reads from another process memory. The Handle needs to have +// the PROCESS_VM_READ right. +// A zero-byte read is a no-op, no error is returned. +func ReadProcessMemory(handle syscall.Handle, baseAddress uintptr, dest []byte) (numRead uintptr, err error) { + n := len(dest) + if n == 0 { + return 0, nil + } + if err = _ReadProcessMemory(handle, baseAddress, uintptr(unsafe.Pointer(&dest[0])), uintptr(n), &numRead); err != nil { + return 0, err + } + return numRead, nil +} + +func GetTickCount64() (uptime uint64, err error) { + if uptime, err = _GetTickCount64(); err != nil { + return 0, err + } + return uptime, nil +} + // Use "GOOS=windows go generate -v -x ." to generate the source. // Add -trace to enable debug prints around syscalls. @@ -467,3 +580,5 @@ func UTF16SliceToStringSlice(buffer []uint16) []string { //sys _FindNextVolume(handle syscall.Handle, volumeName *uint16, size uint32) (err error) = kernel32.FindNextVolumeW //sys _FindVolumeClose(handle syscall.Handle) (err error) = kernel32.FindVolumeClose //sys _GetVolumePathNamesForVolumeName(volumeName string, buffer *uint16, bufferSize uint32, length *uint32) (err error) = kernel32.GetVolumePathNamesForVolumeNameW +//sys _ReadProcessMemory(handle syscall.Handle, baseAddress uintptr, buffer uintptr, size uintptr, numRead *uintptr) (err error) = kernel32.ReadProcessMemory +//sys _GetTickCount64() (uptime uint64, err error) = kernel32.GetTickCount64 diff --git a/sys/windows/syscall_windows_test.go b/sys/windows/syscall_windows_test.go index 96e4022b7..6178e4af7 100644 --- a/sys/windows/syscall_windows_test.go +++ b/sys/windows/syscall_windows_test.go @@ -1,10 +1,12 @@ package windows import ( + "encoding/binary" "os" "runtime" "syscall" "testing" + "unsafe" "github.com/pkg/errors" "github.com/stretchr/testify/assert" @@ -29,7 +31,6 @@ func TestGetProcessMemoryInfo(t *testing.T) { if err != nil { t.Fatal(err) } - counters, err := GetProcessMemoryInfo(h) if err != nil { t.Fatal(err) @@ -175,9 +176,164 @@ func TestNtQueryProcessBasicInformation(t *testing.T) { if err != nil { t.Fatal(err) } - + defer syscall.CloseHandle(h) assert.EqualValues(t, os.Getpid(), info.UniqueProcessID) assert.EqualValues(t, os.Getppid(), info.InheritedFromUniqueProcessID) t.Logf("NtQueryProcessBasicInformation: %+v", info) } + +func TestGetTickCount64(t *testing.T) { + uptime, err := GetTickCount64() + if err != nil { + t.Fatal(err) + } + assert.NotZero(t, uptime) +} + +func TestGetUserProcessParams(t *testing.T) { + h, err := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION|PROCESS_VM_READ, false, uint32(syscall.Getpid())) + if err != nil { + t.Fatal(err) + } + info, err := NtQueryProcessBasicInformation(h) + if err != nil { + t.Fatal(err) + } + userProc, err := GetUserProcessParams(h, info) + if err != nil { + t.Fatal(err) + } + defer syscall.CloseHandle(h) + assert.NoError(t, err) + assert.EqualValues(t, os.Getpid(), info.UniqueProcessID) + assert.EqualValues(t, os.Getppid(), info.InheritedFromUniqueProcessID) + assert.NotEmpty(t, userProc.CommandLine) +} + +func TestGetUserProcessParamsInvalidHandle(t *testing.T) { + var handle syscall.Handle + var info = ProcessBasicInformation{PebBaseAddress: uintptr(0)} + userProc, err := GetUserProcessParams(handle, info) + assert.EqualValues(t, err.Error(), "The handle is invalid.") + assert.Empty(t, userProc) +} + +func TestReadProcessUnicodeString(t *testing.T) { + h, err := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION|PROCESS_VM_READ, false, uint32(syscall.Getpid())) + if err != nil { + t.Fatal(err) + } + info, err := NtQueryProcessBasicInformation(h) + if err != nil { + t.Fatal(err) + } + userProc, err := GetUserProcessParams(h, info) + if err != nil { + t.Fatal(err) + } + read, err := ReadProcessUnicodeString(h, &userProc.CommandLine) + if err != nil { + t.Fatal(err) + } + defer syscall.CloseHandle(h) + assert.NoError(t, err) + assert.NotEmpty(t, read) +} +func TestReadProcessUnicodeStringInvalidHandle(t *testing.T) { + var handle syscall.Handle + var cmd = UnicodeString{Size: 5, MaximumLength: 400, Buffer: 400} + read, err := ReadProcessUnicodeString(handle, &cmd) + assert.EqualValues(t, err.Error(), "The handle is invalid.") + assert.Empty(t, read) +} + +func TestByteSliceToStringSlice(t *testing.T) { + cmd := syscall.GetCommandLine() + b := make([]byte, unsafe.Sizeof(cmd)) + binary.LittleEndian.PutUint16(b, *cmd) + hes, err := ByteSliceToStringSlice(b) + assert.NoError(t, err) + assert.NotEmpty(t, hes) +} + +func TestByteSliceToStringSliceEmptyBytes(t *testing.T) { + b := make([]byte, 0) + cmd, err := ByteSliceToStringSlice(b) + assert.NoError(t, err) + assert.Empty(t, cmd) +} + +func TestReadProcessMemory(t *testing.T) { + h, err := syscall.OpenProcess(syscall.PROCESS_QUERY_INFORMATION|PROCESS_VM_READ, false, uint32(syscall.Getpid())) + if err != nil { + t.Fatal(err) + } + info, err := NtQueryProcessBasicInformation(h) + if err != nil { + t.Fatal(err) + } + pebSize := 0x20 + 8 + if unsafe.Sizeof(uintptr(0)) == 4 { + pebSize = 0x10 + 8 + } + defer syscall.CloseHandle(h) + peb := make([]byte, pebSize) + nRead, err := ReadProcessMemory(h, info.PebBaseAddress, peb) + assert.NoError(t, err) + assert.NotEmpty(t, nRead) + assert.EqualValues(t, nRead, uintptr(pebSize)) +} + +// A zero-byte read is a no-op, no error is returned. +func TestReadProcessMemoryZeroByteRead(t *testing.T) { + peb := make([]byte, 0) + var h syscall.Handle + var address uintptr + nRead, err := ReadProcessMemory(h, address, peb) + assert.NoError(t, err) + assert.Empty(t, nRead) +} + +func TestReadProcessMemoryInvalidHandler(t *testing.T) { + peb := make([]byte, 10) + var h syscall.Handle + var address uintptr + nRead, err := ReadProcessMemory(h, address, peb) + assert.Error(t, err) + assert.EqualValues(t, err.Error(), "The handle is invalid.") + assert.Empty(t, nRead) +} + +func TestGetAccessPaths(t *testing.T) { + paths, err := GetAccessPaths() + if err != nil { + t.Fatal(err) + } + assert.NotEmpty(t, paths) + assert.True(t, len(paths) >= 1) +} + +func TestGetVolumes(t *testing.T) { + paths, err := GetVolumes() + if err != nil { + t.Fatal(err) + } + assert.NotEmpty(t, paths) + assert.True(t, len(paths) >= 1) +} + +func TestGetVolumePathsForVolume(t *testing.T) { + volumes, err := GetVolumes() + if err != nil { + t.Fatal(err) + } + assert.NotNil(t, volumes) + assert.True(t, len(volumes) >= 1) + volumePath, err := GetVolumePathsForVolume(volumes[0]) + if err != nil { + t.Fatal(err) + } + assert.NotNil(t, volumePath) + assert.True(t, len(volumePath) >= 1) +} diff --git a/sys/windows/zsyscall_windows.go b/sys/windows/zsyscall_windows.go index 8da079aba..cd5d9ca32 100644 --- a/sys/windows/zsyscall_windows.go +++ b/sys/windows/zsyscall_windows.go @@ -1,4 +1,4 @@ -// MACHINE GENERATED BY 'go generate' COMMAND; DO NOT EDIT +// Code generated by 'go generate'; DO NOT EDIT. package windows @@ -60,6 +60,8 @@ var ( procFindNextVolumeW = modkernel32.NewProc("FindNextVolumeW") procFindVolumeClose = modkernel32.NewProc("FindVolumeClose") procGetVolumePathNamesForVolumeNameW = modkernel32.NewProc("GetVolumePathNamesForVolumeNameW") + procReadProcessMemory = modkernel32.NewProc("ReadProcessMemory") + procGetTickCount64 = modkernel32.NewProc("GetTickCount64") ) func _GlobalMemoryStatusEx(buffer *MemoryStatusEx) (err error) { @@ -347,3 +349,28 @@ func __GetVolumePathNamesForVolumeName(volumeName *uint16, buffer *uint16, buffe } return } + +func _ReadProcessMemory(handle syscall.Handle, baseAddress uintptr, buffer uintptr, size uintptr, numRead *uintptr) (err error) { + r1, _, e1 := syscall.Syscall6(procReadProcessMemory.Addr(), 5, uintptr(handle), uintptr(baseAddress), uintptr(buffer), uintptr(size), uintptr(unsafe.Pointer(numRead)), 0) + if r1 == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func _GetTickCount64() (uptime uint64, err error) { + r0, _, e1 := syscall.Syscall(procGetTickCount64.Addr(), 0, 0, 0, 0) + uptime = uint64(r0) + if uptime == 0 { + if e1 != 0 { + err = errnoErr(e1) + } else { + err = syscall.EINVAL + } + } + return +}