package uvm import ( "context" "strings" "github.com/Microsoft/go-winio/pkg/guid" "github.com/Microsoft/go-winio/pkg/process" "github.com/Microsoft/hcsshim/cmd/containerd-shim-runhcs-v1/stats" hcsschema "github.com/Microsoft/hcsshim/internal/hcs/schema2" "github.com/Microsoft/hcsshim/internal/log" "github.com/pkg/errors" "github.com/sirupsen/logrus" "golang.org/x/sys/windows" ) // checkProcess checks if the process identified by the given pid has a name // matching `desiredProcessName`, and is running as a user with domain // `desiredDomain` and user name `desiredUser`. If the process matches, it // returns a handle to the process. If the process does not match, it returns // 0. func checkProcess(ctx context.Context, pid uint32, desiredProcessName string, desiredDomain string, desiredUser string) (p windows.Handle, err error) { desiredProcessName = strings.ToUpper(desiredProcessName) desiredDomain = strings.ToUpper(desiredDomain) desiredUser = strings.ToUpper(desiredUser) p, err = windows.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION|windows.PROCESS_VM_READ, false, pid) if err != nil { return 0, err } defer func(openedProcess windows.Handle) { // If we don't return this process handle, close it so it doesn't leak. if p == 0 { windows.Close(openedProcess) } }(p) // Querying vmmem's image name as a win32 path returns ERROR_GEN_FAILURE // for some reason, so we query it as an NT path instead. name, err := process.QueryFullProcessImageName(p, process.ImageNameFormatNTPath) if err != nil { return 0, err } if strings.ToUpper(name) == desiredProcessName { var t windows.Token if err := windows.OpenProcessToken(p, windows.TOKEN_QUERY, &t); err != nil { return 0, err } defer t.Close() tUser, err := t.GetTokenUser() if err != nil { return 0, err } user, domain, _, err := tUser.User.Sid.LookupAccount("") if err != nil { return 0, err } log.G(ctx).WithFields(logrus.Fields{ "name": name, "domain": domain, "user": user, }).Debug("checking vmmem process identity") if strings.ToUpper(domain) == desiredDomain && strings.ToUpper(user) == desiredUser { return p, nil } } return 0, nil } // lookupVMMEM locates the vmmem process for a VM given the VM ID. It returns // a handle to the vmmem process. The lookup is implemented by enumerating all // processes on the system, and finding a process with full name "vmmem", // running as "NT VIRTUAL MACHINE\". func lookupVMMEM(ctx context.Context, vmID guid.GUID) (proc windows.Handle, err error) { vmIDStr := strings.ToUpper(vmID.String()) log.G(ctx).WithField("vmID", vmIDStr).Debug("looking up vmmem") pids, err := process.EnumProcesses() if err != nil { return 0, errors.Wrap(err, "failed to enumerate processes") } for _, pid := range pids { p, err := checkProcess(ctx, pid, "vmmem", "NT VIRTUAL MACHINE", vmIDStr) if err != nil { // Checking the process could fail for a variety of reasons, such as // the process exiting since we called EnumProcesses, or not having // access to open the process (even as SYSTEM). In the case of an // error, we just log and continue looking at the other processes. log.G(ctx).WithField("pid", pid).Debug("failed to check process") continue } if p != 0 { log.G(ctx).WithField("pid", pid).Debug("found vmmem match") return p, nil } } return 0, errors.New("failed to find matching vmmem process") } // getVMMEMProcess returns a handle to the vmmem process associated with this // UVM. It only does the actual process lookup once, after which it caches the // process handle in the UVM object. func (uvm *UtilityVM) getVMMEMProcess(ctx context.Context) (windows.Handle, error) { uvm.vmmemOnce.Do(func() { uvm.vmmemProcess, uvm.vmmemErr = lookupVMMEM(ctx, uvm.runtimeID) }) return uvm.vmmemProcess, uvm.vmmemErr } // Stats returns various UVM statistics. func (uvm *UtilityVM) Stats(ctx context.Context) (*stats.VirtualMachineStatistics, error) { s := &stats.VirtualMachineStatistics{} props, err := uvm.hcsSystem.PropertiesV2(ctx, hcsschema.PTStatistics, hcsschema.PTMemory) if err != nil { return nil, err } s.Processor = &stats.VirtualMachineProcessorStatistics{} s.Processor.TotalRuntimeNS = uint64(props.Statistics.Processor.TotalRuntime100ns * 100) s.Memory = &stats.VirtualMachineMemoryStatistics{} if uvm.physicallyBacked { // If the uvm is physically backed we set the working set to the total amount allocated // to the UVM. AssignedMemory returns the number of 4KB pages. Will always be 4KB // regardless of what the UVMs actual page size is so we don't need that information. if props.Memory != nil { s.Memory.WorkingSetBytes = props.Memory.VirtualMachineMemory.AssignedMemory * 4096 } } else { // The HCS properties does not return sufficient information to calculate // working set size for a VA-backed UVM. To work around this, we instead // locate the vmmem process for the VM, and query that process's working set // instead, which will be the working set for the VM. vmmemProc, err := uvm.getVMMEMProcess(ctx) if err != nil { return nil, err } memCounters, err := process.GetProcessMemoryInfo(vmmemProc) if err != nil { return nil, err } s.Memory.WorkingSetBytes = uint64(memCounters.WorkingSetSize) } if props.Memory != nil { s.Memory.VirtualNodeCount = props.Memory.VirtualNodeCount s.Memory.VmMemory = &stats.VirtualMachineMemory{} s.Memory.VmMemory.AvailableMemory = props.Memory.VirtualMachineMemory.AvailableMemory s.Memory.VmMemory.AvailableMemoryBuffer = props.Memory.VirtualMachineMemory.AvailableMemoryBuffer s.Memory.VmMemory.ReservedMemory = props.Memory.VirtualMachineMemory.ReservedMemory s.Memory.VmMemory.AssignedMemory = props.Memory.VirtualMachineMemory.AssignedMemory s.Memory.VmMemory.SlpActive = props.Memory.VirtualMachineMemory.SlpActive s.Memory.VmMemory.BalancingEnabled = props.Memory.VirtualMachineMemory.BalancingEnabled s.Memory.VmMemory.DmOperationInProgress = props.Memory.VirtualMachineMemory.DmOperationInProgress } return s, nil }