windowsapiwinapiuwppower-management

Detect "suspended" Windows 8/10 process


UWP (or "Metro") apps in Windows 8/10 can be suspended when they are not in the foreground. Apps in this state continue to exist but no longer consume CPU time. It looks like this change was introduced to improve performance on low-power/storage devices like tablets and phones.

What is the most elegant and simple method to detect a process in this state?

I can see 2 possible solutions at the moment:

  1. Call NtQuerySystemInformation() and the enumerate each process and each thread. A process is "suspended" if all threads are in the suspended state. This approach will require a lot of code and critically NtQuerySystemInformation() is only semi-documented and could be removed in a future OS. NtQueryInformationProcess() may also offer a similar solution with the same problem.

  2. Call GetProcessTimes() and record the counters for each process. Wait some longish time (minutes) and check again. If the process counters haven't changed then assume the process is suspended. I admit this is a hack but maybe could work if the time period is long enough.

Is there a more elegant way?


Solution

  • for this exist PROCESS_EXTENDED_BASIC_INFORMATION - meaning of flags in it described in this answer. you are need IsFrozen flag. so you need open process with PROCESS_QUERY_LIMITED_INFORMATION access (for do this for all processes, you will be need have SE_DEBUG_PRIVILEGE enabled in token). and call NtQuerySystemInformation with ProcessBasicInformation and PROCESS_EXTENDED_BASIC_INFORMATION as input. for enumerate all processes we can use NtQuerySystemInformation with SystemProcessInformation. of course possible and use CreateToolhelp32Snapshot + Process32First + Process32Next but this api very not efficient, compare direct call to NtQuerySystemInformation

    also possible enumerate all threads in process and check it state and if state wait - wait reason. this is very easy, because all this information already returned by single call to NtQuerySystemInformation with SystemProcessInformation. with this we not need open processes. usually both this ways give the same result (for suspended/frozen) processes, but however use IsFrozen is most correct solution.

    void PrintSuspended()
    {
        BOOLEAN b;
        RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, TRUE, FALSE, &b);
    
        ULONG cb = 0x1000;
        NTSTATUS status;
        do 
        {
            status = STATUS_INSUFFICIENT_RESOURCES;
    
            if (PBYTE buf = new BYTE[cb])
            {
                if (0 <= (status = NtQuerySystemInformation(SystemProcessInformation, buf, cb, &cb)))
                {
                    union {
                        PBYTE pb;
                        SYSTEM_PROCESS_INFORMATION* spi;
                    };
    
                    pb = buf;
    
                    ULONG NextEntryOffset = 0;
                    do 
                    {
                        pb += NextEntryOffset;
    
                        if (!spi->UniqueProcessId)
                        {
                            continue;
                        }
    
                        if (HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, 
                            (ULONG)(ULONG_PTR)spi->UniqueProcessId))
                        {
                            PROCESS_EXTENDED_BASIC_INFORMATION pebi;
                            if (0 <= NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pebi, sizeof(pebi), 0) &&
                                pebi.Size >= sizeof(pebi))
                            {
                                if (pebi.IsFrozen)
                                {
                                    DbgPrint("f:%x %wZ\n", spi->UniqueProcessId, spi->ImageName);
                                }
                            }
                            CloseHandle(hProcess);
                        }
    
                        if (ULONG NumberOfThreads = spi->NumberOfThreads)
                        {
                            SYSTEM_THREAD_INFORMATION* TH = spi->TH;
                            do 
                            {
                                if (TH->ThreadState != StateWait || TH->WaitReason != Suspended)
                                {
                                    break;
                                }
                            } while (TH++, --NumberOfThreads);
    
                            if (!NumberOfThreads)
                            {
                                DbgPrint("s:%x %wZ\n", spi->UniqueProcessId, spi->ImageName);
                            }
                        }
    
                    } while (NextEntryOffset = spi->NextEntryOffset);
                }
                delete [] buf;
            }
        } while (status == STATUS_INFO_LENGTH_MISMATCH);
    }