diff --git a/README.md b/README.md index 4535335..584f28f 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,20 @@ go-windows is a library for Go (golang) that provides wrappers to various Windows APIs that are not covered by the stdlib or by [golang.org/x/sys/windows](https://godoc.org/golang.org/x/sys/windows). -Goals / Features +## Goals / Features - Does not use cgo. - Provide abstractions to make using the APIs easier. + +## Adding new Syscalls + +Adding syscalls to zsyscall_windows.go is done by adding a comment under Syscalls in kernel32.go, psapi.go, etc describing the syscall. + +Example: + +``` +// Syscalls +//sys _GetPerformanceInfo(pi *PerformanceInformation, cb uint32) (err error) = psapi.GetPerformanceInfo +``` + +And then calling `go generate` as described in [doc.go](./doc.go) \ No newline at end of file diff --git a/psapi.go b/psapi.go index 01fcf10..18d12cf 100644 --- a/psapi.go +++ b/psapi.go @@ -30,6 +30,7 @@ import ( //sys _GetProcessMemoryInfo(handle syscall.Handle, psmemCounters *ProcessMemoryCountersEx, cb uint32) (err error) = psapi.GetProcessMemoryInfo //sys _GetProcessImageFileNameA(handle syscall.Handle, imageFileName *byte, nSize uint32) (len uint32, err error) = psapi.GetProcessImageFileNameA //sys _EnumProcesses(lpidProcess *uint32, cb uint32, lpcbNeeded *uint32) (err error) = psapi.EnumProcesses +//sys _GetPerformanceInfo(pi *PerformanceInformation, cb uint32) (err error) = psapi.GetPerformanceInfo var ( sizeofProcessMemoryCountersEx = uint32(unsafe.Sizeof(ProcessMemoryCountersEx{})) @@ -98,3 +99,35 @@ func EnumProcesses() (pids []uint32, err error) { } } } + +// PerformanceInformation represents the [PERFORMANCE_INFORMATION] structure +// +// [PERFORMANCE_INFORMATION]: https://learn.microsoft.com/en-us/windows/win32/api/psapi/ns-psapi-performance_information +type PerformanceInformation struct { + cb uint32 + CommitTotal uintptr // The number of pages currently committed by the system. Note that committing pages (using VirtualAlloc with MEM_COMMIT) changes this value immediately; however, the physical memory is not charged until the pages are accessed. + CommitLimit uintptr // The current maximum number of pages that can be committed by the system without extending the paging file(s). This number can change if memory is added or deleted, or if pagefiles have grown, shrunk, or been added. If the paging file can be extended, this is a soft limit. + CommitPeak uintptr // The maximum number of pages that were simultaneously in the committed state since the last system reboot. + PhysicalTotal uintptr // The amount of actual physical memory, in pages. + PhysicalAvailable uintptr // The amount of physical memory currently available, in pages. This is the amount of physical memory that can be immediately reused without having to write its contents to disk first. It is the sum of the size of the standby, free, and zero lists. + SystemCache uintptr // The amount of system cache memory, in pages. This is the size of the standby list plus the system working set. + KernelTotal uintptr // The sum of the memory currently in the paged and nonpaged kernel pools, in pages. + KernelPaged uintptr // The memory currently in the paged kernel pool, in pages. + KernelNonpaged uintptr // The memory currently in the nonpaged kernel pool, in pages. + PageSize uintptr // The size of a page, in bytes. + HandleCount uint32 // The current number of open handles. + ProcessCount uint32 // The current number of processes. + ThreadCount uint32 // The current number of threads. +} + +// GetPerformanceInfo retrieves performance information for the system. +// https://learn.microsoft.com/en-us/windows/win32/api/psapi/nf-psapi-getperformanceinfo +func GetPerformanceInfo() (PerformanceInformation, error) { + var pi PerformanceInformation + pi.CB = uint32(unsafe.Sizeof(pi)) + + if err := _GetPerformanceInfo(&pi, pi.CB); err != nil { + return PerformanceInformation{}, fmt.Errorf("GetPerformanceInfo failed: %w", err) + } + return pi, nil +} diff --git a/psapi_test.go b/psapi_test.go new file mode 100644 index 0000000..0c12480 --- /dev/null +++ b/psapi_test.go @@ -0,0 +1,126 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +//go:build windows +// +build windows + +package windows + +import ( + "slices" + "syscall" + "testing" + "unsafe" + + "github.com/stretchr/testify/assert" +) + +func TestGetPerformanceInfo(t *testing.T) { + pi, err := GetPerformanceInfo() + if err != nil { + t.Fatal(err) + } + + // Verify the structure was populated correctly + assert.Equal(t, uint32(unsafe.Sizeof(pi)), pi.CB, "CB field should contain the size of the structure") + + // Verify that physical memory values are reasonable + assert.True(t, pi.PhysicalTotal > 0, "PhysicalTotal should be greater than 0") + assert.True(t, pi.PhysicalAvailable <= pi.PhysicalTotal, "PhysicalAvailable should not exceed PhysicalTotal") + + // Verify that commit values are reasonable + assert.True(t, pi.CommitTotal > 0, "CommitTotal should be greater than 0") + assert.True(t, pi.CommitLimit > 0, "CommitLimit should be greater than 0") + assert.True(t, pi.CommitTotal <= pi.CommitLimit, "CommitTotal should not exceed CommitLimit") + + // Verify that page size is reasonable (typically at least 4KB on x86/x64) + assert.True(t, pi.PageSize >= 4096, "PageSize should be at least 4KB") + + // Verify that process and thread counts are reasonable + assert.True(t, pi.ProcessCount > 0, "ProcessCount should be greater than 0") + assert.True(t, pi.ThreadCount > 0, "ThreadCount should be greater than 0") + assert.True(t, pi.ThreadCount >= pi.ProcessCount, "ThreadCount should be at least equal to ProcessCount") + + // Verify that handle count is reasonable + assert.True(t, pi.HandleCount > 0, "HandleCount should be greater than 0") + + // Verify that kernel memory values are reasonable + assert.True(t, pi.KernelTotal > 0, "KernelTotal should be greater than 0") + assert.True(t, pi.KernelPaged <= pi.KernelTotal, "KernelPaged should not exceed KernelTotal") + assert.True(t, pi.KernelNonpaged <= pi.KernelTotal, "KernelNonpaged should not exceed KernelTotal") + + // Verify that system cache is reasonable + assert.True(t, pi.SystemCache > 0, "SystemCache should be greater than 0") +} + +func TestGetProcessMemoryInfo(t *testing.T) { + h, err := syscall.GetCurrentProcess() + if err != nil { + t.Fatal(err) + } + + info, err := GetProcessMemoryInfo(h) + if err != nil { + t.Fatal(err) + } + + // Verify the structure was populated correctly + assert.Equal(t, uint32(unsafe.Sizeof(info)), info.cb, "cb field should contain the size of the structure") + + // Verify that memory values are reasonable + assert.True(t, info.WorkingSetSize > 0, "WorkingSetSize should be greater than 0") + assert.True(t, info.PeakWorkingSetSize >= info.WorkingSetSize, "PeakWorkingSetSize should be at least equal to WorkingSetSize") + assert.True(t, info.PrivateUsage > 0, "PrivateUsage should be greater than 0") + + // Verify that page fault count is reasonable + assert.True(t, info.PageFaultCount > 0, "PageFaultCount should be greater than 0") +} + +func TestGetProcessImageFileName(t *testing.T) { + h, err := syscall.GetCurrentProcess() + if err != nil { + t.Fatal(err) + } + + filename, err := GetProcessImageFileName(h) + if err != nil { + t.Fatal(err) + } + + // Verify that we got a valid device path + assert.True(t, len(filename) > 0, "Filename should not be empty") + assert.Contains(t, filename, "\\Device\\", "Filename should be a device path starting with \\Device\\") + + // The path should contain the executable name + assert.Contains(t, filename, ".exe", "Filename should contain .exe extension") +} + +func TestEnumProcesses(t *testing.T) { + pids, err := EnumProcesses() + if err != nil { + t.Fatal(err) + } + + // Verify that we got a reasonable number of processes + assert.True(t, len(pids) > 0, "Should return at least one process") + assert.True(t, len(pids) < 10000, "Should not return an unreasonable number of processes") + + // Verify that the current process is in the list + currentPID := uint32(syscall.Getpid()) + found := slices.Contains(pids, currentPID) + assert.True(t, found, "Current process PID should be in the returned list") +} diff --git a/zsyscall_windows.go b/zsyscall_windows.go index 67b5add..8df304c 100644 --- a/zsyscall_windows.go +++ b/zsyscall_windows.go @@ -70,6 +70,7 @@ var ( procReadProcessMemory = modkernel32.NewProc("ReadProcessMemory") procNtQueryInformationProcess = modntdll.NewProc("NtQueryInformationProcess") procEnumProcesses = modpsapi.NewProc("EnumProcesses") + procGetPerformanceInfo = modpsapi.NewProc("GetPerformanceInfo") procGetProcessImageFileNameA = modpsapi.NewProc("GetProcessImageFileNameA") procGetProcessMemoryInfo = modpsapi.NewProc("GetProcessMemoryInfo") procGetFileVersionInfoSizeW = modversion.NewProc("GetFileVersionInfoSizeW") @@ -137,6 +138,14 @@ func _EnumProcesses(lpidProcess *uint32, cb uint32, lpcbNeeded *uint32) (err err return } +func _GetPerformanceInfo(pi *PerformanceInformation, cb uint32) (err error) { + r1, _, e1 := syscall.Syscall(procGetPerformanceInfo.Addr(), 2, uintptr(unsafe.Pointer(pi)), uintptr(cb), 0) + if r1 == 0 { + err = errnoErr(e1) + } + return +} + func _GetProcessImageFileNameA(handle syscall.Handle, imageFileName *byte, nSize uint32) (len uint32, err error) { r0, _, e1 := syscall.Syscall(procGetProcessImageFileNameA.Addr(), 3, uintptr(handle), uintptr(unsafe.Pointer(imageFileName)), uintptr(nSize)) len = uint32(r0)