diff --git a/internal/jobcontainers/jobcontainer.go b/internal/jobcontainers/jobcontainer.go index c4f589357b..45ba259c40 100644 --- a/internal/jobcontainers/jobcontainer.go +++ b/internal/jobcontainers/jobcontainer.go @@ -10,6 +10,7 @@ import ( "sync" "syscall" "time" + "unsafe" "github.com/Microsoft/go-winio/pkg/guid" "github.com/Microsoft/hcsshim/internal/cow" @@ -20,6 +21,7 @@ import ( "github.com/Microsoft/hcsshim/internal/queue" "github.com/Microsoft/hcsshim/internal/schema1" hcsschema "github.com/Microsoft/hcsshim/internal/schema2" + "github.com/Microsoft/hcsshim/internal/winapi" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -368,12 +370,88 @@ func (c *JobContainer) shutdown(ctx context.Context) error { // to adhere to the interface for containers on Windows it is partially implemented. The only // supported property is schema2.PTStatistics. func (c *JobContainer) PropertiesV2(ctx context.Context, types ...hcsschema.PropertyType) (*hcsschema.Properties, error) { - return nil, errors.New("`PropertiesV2` call is not implemented for job containers") + if len(types) == 0 { + return nil, errors.New("no property types supplied for PropertiesV2 call") + } + if types[0] != hcsschema.PTStatistics { + return nil, errors.New("PTStatistics is the only supported property type for job containers") + } + + memInfo, err := c.job.QueryMemoryStats() + if err != nil { + return nil, errors.Wrap(err, "failed to query for job containers memory information") + } + + processorInfo, err := c.job.QueryProcessorStats() + if err != nil { + return nil, errors.Wrap(err, "failed to query for job containers processor information") + } + + storageInfo, err := c.job.QueryStorageStats() + if err != nil { + return nil, errors.Wrap(err, "failed to query for job containers storage information") + } + + var privateWorkingSet uint64 + err = forEachProcessInfo(c.job, func(procInfo *winapi.SYSTEM_PROCESS_INFORMATION) { + privateWorkingSet += uint64(procInfo.WorkingSetPrivateSize) + }) + if err != nil { + return nil, errors.Wrap(err, "failed to get private working set for container") + } + + return &hcsschema.Properties{ + Statistics: &hcsschema.Statistics{ + Timestamp: time.Now(), + Uptime100ns: uint64(time.Since(c.startTimestamp)) / 100, + ContainerStartTime: c.startTimestamp, + Memory: &hcsschema.MemoryStats{ + MemoryUsageCommitBytes: memInfo.JobMemory, + MemoryUsageCommitPeakBytes: memInfo.PeakJobMemoryUsed, + MemoryUsagePrivateWorkingSetBytes: privateWorkingSet, + }, + Processor: &hcsschema.ProcessorStats{ + RuntimeKernel100ns: uint64(processorInfo.TotalKernelTime), + RuntimeUser100ns: uint64(processorInfo.TotalUserTime), + TotalRuntime100ns: uint64(processorInfo.TotalKernelTime + processorInfo.TotalUserTime), + }, + Storage: &hcsschema.StorageStats{ + ReadCountNormalized: storageInfo.IoInfo.ReadOperationCount, + ReadSizeBytes: storageInfo.IoInfo.ReadTransferCount, + WriteCountNormalized: storageInfo.IoInfo.WriteOperationCount, + WriteSizeBytes: storageInfo.IoInfo.WriteTransferCount, + }, + }, + }, nil } // Properties is not implemented for job containers. This is just to satisfy the cow.Container interface. func (c *JobContainer) Properties(ctx context.Context, types ...schema1.PropertyType) (*schema1.ContainerProperties, error) { - return nil, errors.New("`Properties` call is not implemented for job containers") + if len(types) == 0 { + return nil, errors.New("no property types supplied for Properties call") + } + if types[0] != schema1.PropertyTypeProcessList { + return nil, errors.New("ProcessList is the only supported property type for job containers") + } + + var processList []schema1.ProcessListItem + err := forEachProcessInfo(c.job, func(procInfo *winapi.SYSTEM_PROCESS_INFORMATION) { + proc := schema1.ProcessListItem{} + proc.CreateTimestamp = time.Unix(0, procInfo.CreateTime) + proc.ProcessId = uint32(procInfo.UniqueProcessID) + proc.ImageName = procInfo.ImageName.String() + proc.UserTime100ns = uint64(procInfo.UserTime) + proc.KernelTime100ns = uint64(procInfo.KernelTime) + proc.MemoryCommitBytes = uint64(procInfo.PrivatePageCount) + proc.MemoryWorkingSetPrivateBytes = uint64(procInfo.WorkingSetPrivateSize) + proc.MemoryWorkingSetSharedBytes = uint64(procInfo.WorkingSetSize) - uint64(procInfo.WorkingSetPrivateSize) + processList = append(processList, proc) + }) + if err != nil { + return nil, errors.Wrap(err, "failed to get process ") + } + + return &schema1.ContainerProperties{ProcessList: processList}, nil } // Terminate terminates the job object (kills every process in the job). @@ -447,3 +525,62 @@ func (c *JobContainer) IsOCI() bool { func (c *JobContainer) OS() string { return "windows" } + +// Filter +func forEachProcessInfo(job *jobobject.JobObject, run func(*winapi.SYSTEM_PROCESS_INFORMATION)) error { + procInfos, err := systemProcessInformation() + if err != nil { + return err + } + + pids, err := job.Pids() + if err != nil { + return err + } + + pidsMap := make(map[uint32]struct{}) + for _, pid := range pids { + pidsMap[pid] = struct{}{} + } + + for _, procInfo := range procInfos { + if _, ok := pidsMap[uint32(procInfo.UniqueProcessID)]; ok { + run(procInfo) + } + } + return nil +} + +// Get a slice of SYSTEM_PROCESS_INFORMATION for all of the processes running on the system. +func systemProcessInformation() ([]*winapi.SYSTEM_PROCESS_INFORMATION, error) { + var ( + systemProcInfo *winapi.SYSTEM_PROCESS_INFORMATION + procInfos []*winapi.SYSTEM_PROCESS_INFORMATION + size = uint32(1024 * 512) + ) + for { + b := make([]byte, size) + systemProcInfo = (*winapi.SYSTEM_PROCESS_INFORMATION)(unsafe.Pointer(&b[0])) + status := winapi.NtQuerySystemInformation( + winapi.SystemProcessInformation, + uintptr(unsafe.Pointer(systemProcInfo)), + size, + &size, + ) + if winapi.NTSuccess(status) { + break + } else if status != winapi.STATUS_INFO_LENGTH_MISMATCH { + return nil, winapi.RtlNtStatusToDosError(status) + } + } + + for { + procInfos = append(procInfos, systemProcInfo) + if systemProcInfo.NextEntryOffset == 0 { + break + } + systemProcInfo = (*winapi.SYSTEM_PROCESS_INFORMATION)(unsafe.Pointer(uintptr(unsafe.Pointer(systemProcInfo)) + uintptr(systemProcInfo.NextEntryOffset))) + } + + return procInfos, nil +} diff --git a/internal/jobcontainers/system_test.go b/internal/jobcontainers/system_test.go new file mode 100644 index 0000000000..0b4a882e28 --- /dev/null +++ b/internal/jobcontainers/system_test.go @@ -0,0 +1,12 @@ +package jobcontainers + +import ( + "testing" +) + +func TestSystemInfo(t *testing.T) { + _, err := systemProcessInformation() + if err != nil { + t.Fatal(err) + } +} diff --git a/internal/winapi/system.go b/internal/winapi/system.go new file mode 100644 index 0000000000..327f57d7c2 --- /dev/null +++ b/internal/winapi/system.go @@ -0,0 +1,52 @@ +package winapi + +import "golang.org/x/sys/windows" + +const SystemProcessInformation = 5 + +const STATUS_INFO_LENGTH_MISMATCH = 0xC0000004 + +// __kernel_entry NTSTATUS NtQuerySystemInformation( +// SYSTEM_INFORMATION_CLASS SystemInformationClass, +// PVOID SystemInformation, +// ULONG SystemInformationLength, +// PULONG ReturnLength +// ); +//sys NtQuerySystemInformation(systemInfoClass int, systemInformation uintptr, systemInfoLength uint32, returnLength *uint32) (status uint32) = ntdll.NtQuerySystemInformation + +type SYSTEM_PROCESS_INFORMATION struct { + NextEntryOffset uint32 // ULONG + NumberOfThreads uint32 // ULONG + WorkingSetPrivateSize int64 // LARGE_INTEGER + HardFaultCount uint32 // ULONG + NumberOfThreadsHighWatermark uint32 // ULONG + CycleTime uint64 // ULONGLONG + CreateTime int64 // LARGE_INTEGER + UserTime int64 // LARGE_INTEGER + KernelTime int64 // LARGE_INTEGER + ImageName UnicodeString // UNICODE_STRING + BasePriority int32 // KPRIORITY + UniqueProcessID windows.Handle // HANDLE + InheritedFromUniqueProcessID windows.Handle // HANDLE + HandleCount uint32 // ULONG + SessionID uint32 // ULONG + UniqueProcessKey *uint32 // ULONG_PTR + PeakVirtualSize uintptr // SIZE_T + VirtualSize uintptr // SIZE_T + PageFaultCount uint32 // ULONG + PeakWorkingSetSize uintptr // SIZE_T + WorkingSetSize uintptr // SIZE_T + QuotaPeakPagedPoolUsage uintptr // SIZE_T + QuotaPagedPoolUsage uintptr // SIZE_T + QuotaPeakNonPagedPoolUsage uintptr // SIZE_T + QuotaNonPagedPoolUsage uintptr // SIZE_T + PagefileUsage uintptr // SIZE_T + PeakPagefileUsage uintptr // SIZE_T + PrivatePageCount uintptr // SIZE_T + ReadOperationCount int64 // LARGE_INTEGER + WriteOperationCount int64 // LARGE_INTEGER + OtherOperationCount int64 // LARGE_INTEGER + ReadTransferCount int64 // LARGE_INTEGER + WriteTransferCount int64 // LARGE_INTEGER + OtherTransferCount int64 // LARGE_INTEGER +} diff --git a/internal/winapi/thread.go b/internal/winapi/thread.go index 2a1dac26ac..4724713e3e 100644 --- a/internal/winapi/thread.go +++ b/internal/winapi/thread.go @@ -1,3 +1,12 @@ package winapi +// HANDLE CreateRemoteThread( +// HANDLE hProcess, +// LPSECURITY_ATTRIBUTES lpThreadAttributes, +// SIZE_T dwStackSize, +// LPTHREAD_START_ROUTINE lpStartAddress, +// LPVOID lpParameter, +// DWORD dwCreationFlags, +// LPDWORD lpThreadId +// ); //sys CreateRemoteThread(process windows.Handle, sa *windows.SecurityAttributes, stackSize uint32, startAddr uintptr, parameter uintptr, creationFlags uint32, threadID *uint32) (handle windows.Handle, err error) = kernel32.CreateRemoteThread diff --git a/internal/winapi/winapi.go b/internal/winapi/winapi.go index e783d61f66..ec88c0d212 100644 --- a/internal/winapi/winapi.go +++ b/internal/winapi/winapi.go @@ -2,4 +2,4 @@ // be thought of as an extension to golang.org/x/sys/windows. package winapi -//go:generate go run ..\..\mksyscall_windows.go -output zsyscall_windows.go net.go path.go thread.go iocp.go jobobject.go logon.go memory.go process.go processor.go devices.go filesystem.go errors.go +//go:generate go run ..\..\mksyscall_windows.go -output zsyscall_windows.go system.go net.go path.go thread.go iocp.go jobobject.go logon.go memory.go process.go processor.go devices.go filesystem.go errors.go diff --git a/internal/winapi/zsyscall_windows.go b/internal/winapi/zsyscall_windows.go index 8dd3d3e9bd..2941b0f980 100644 --- a/internal/winapi/zsyscall_windows.go +++ b/internal/winapi/zsyscall_windows.go @@ -37,13 +37,14 @@ func errnoErr(e syscall.Errno) error { } var ( + modntdll = windows.NewLazySystemDLL("ntdll.dll") modiphlpapi = windows.NewLazySystemDLL("iphlpapi.dll") modkernel32 = windows.NewLazySystemDLL("kernel32.dll") - modntdll = windows.NewLazySystemDLL("ntdll.dll") modadvapi32 = windows.NewLazySystemDLL("advapi32.dll") modpsapi = windows.NewLazySystemDLL("psapi.dll") modcfgmgr32 = windows.NewLazySystemDLL("cfgmgr32.dll") + procNtQuerySystemInformation = modntdll.NewProc("NtQuerySystemInformation") procSetJobCompartmentId = modiphlpapi.NewProc("SetJobCompartmentId") procSearchPathW = modkernel32.NewProc("SearchPathW") procCreateRemoteThread = modkernel32.NewProc("CreateRemoteThread") @@ -73,6 +74,12 @@ var ( procRtlNtStatusToDosError = modntdll.NewProc("RtlNtStatusToDosError") ) +func NtQuerySystemInformation(systemInfoClass int, systemInformation uintptr, systemInfoLength uint32, returnLength *uint32) (status uint32) { + r0, _, _ := syscall.Syscall6(procNtQuerySystemInformation.Addr(), 4, uintptr(systemInfoClass), uintptr(systemInformation), uintptr(systemInfoLength), uintptr(unsafe.Pointer(returnLength)), 0, 0) + status = uint32(r0) + return +} + func SetJobCompartmentId(handle windows.Handle, compartmentId uint32) (win32Err error) { r0, _, _ := syscall.Syscall(procSetJobCompartmentId.Addr(), 2, uintptr(handle), uintptr(compartmentId), 0) if r0 != 0 {