Process.Responding работает не коректно

написал вот такой код для вывода списка процессов и вывода их статуса (то есть: работает он или приостановлен) вот сам код:

foreach (var proc in Process.GetProcesses())
{
    string res = proc.Responding.ToString();
    if (res == "True")
    {
        res = "working";
    }
    else if (res == "False")
    {
        res = "suspended";
    }
    Console.WriteLine(proc.Id.ToString() + ":::" + proc.ProcessName + ":::" + res);
}

для теста приостановил VsCode: введите сюда описание изображения

но моя программа считает что приостановлен лишь 1 процесс VsCode'a:

введите сюда описание изображения

Почему так и как решить эту проблему?


Ответы (1 шт):

Автор решения: EvgeniyZ

Смотрите, два пути, заодно узнаете что такое WinAPI.

  1. Простой:
  • Для этого мы берем процессы, стандартным Process.GetProcesses();
  • Далее у каждого процесса смотрим его потоки (.Threads).
  • Если ThreadState будет Wait, а WaitReason будет Suspended, то значит можем считать этот процесс замороженным.

В итоге получаем, например такое:

var processes = Process.GetProcesses();
foreach (var process in processes.Where(x=>x.Threads.Cast<ProcessThread>().All(p => 
{
    if (p.ThreadState == ThreadState.Wait)
    {
        return p.WaitReason == ThreadWaitReason.Suspended;
    }
    return false;
})))
{
    Console.WriteLine($"[{process.Id}] {process.ProcessName} : {process.Threads.Cast<ProcessThread>().All(x => x.WaitReason == ThreadWaitReason.Suspended)}");
}

Тут уже сами делайте коллекции или что вам надо. В текущем виде на экран будет выведены все замороженные процессы.

  1. Более сложный, зато интересный - WinAPI:

Что такое WinAPI

WinAPI - это, по сути, набор библиотек Windows, который лежит у каждого на компьютере, через которые сама OS и обрабатывает все нужное. В этих библиотеках лежат простые методы, которые на вход принимают что-то, и отдают нам что-то на выход. Работа с этим всем происходит при помощи вызовов неуправляемого кода (P/Invoke). И тут благо, что большинство методов, сама Microsoft расписала на своем сайте, что не заставит труда в поиске нужного (хоть местами это весьма ужасная документация).

Что нам понадобиться

  1. NtQuerySystemInformation - это основной метод, который может предоставить нам кучу полезной информации, включая нужную нам информацию процессов. Находится он в Ntdll.dll.

  2. Перечисление NTSTATUS - так мы поймем, выполнилась нужная нам функция, или выдала ошибку, или еще что. По сути, от туда нам нужно 2 статуса STATUS_SUCCESS и STATUS_INFO_LENGTH_MISMATCH, но вы смотрите, может что-то еще может быть полезно для конкретно вашей задачи, например, STATUS_BUFFER_OVERFLOW.

  3. Перечисление SYSTEM_INFORMATION_CLASS - это входной параметр, благодаря которому метод понимает, какую именно информацию стоит выдать. В нашем случае, нам нужен лишь SystemProcessInformation (0x05).

  4. Структура SYSTEM_PROCESS_INFORMATION - это данные, которые вернет метод, внутри которых будет информация о процессе (его id, название, потоки, и прочее).

  5. Структура SYSTEM_THREAD_INFORMATION - это уже информация о потоках конкретного процесса, самая важная для нас структура, ибо именно она и содержит статус.

  6. Перечисление WaitReason, которое скажет причину простоя процесса.

  7. Перечисление ThreadState - состояние потока.

Как видите, наша задача сводится к тому, чтобы выполнить метод NtQuerySystemInformation с правильными параметрами, и прочитать из него нужные нам структуры данных. На выходе мы должны получить список процессов с их потоками и статусами.

Подготовление

Для начала все структуры и перечисления. Просто переписываем информацию по ссылкам в объекты C#:

internal enum NT_STATUS
{
    STATUS_SUCCESS = 0x00000000,
    STATUS_INFO_LENGTH_MISMATCH = unchecked((int)0xC0000004L)
}

internal enum SYSTEM_INFORMATION_CLASS
{
    SystemBasicInformation = 0,
    SystemPerformanceInformation = 2,
    SystemTimeOfDayInformation = 3,
    SystemProcessInformation = 5,
    SystemProcessorPerformanceInformation = 8,
    SystemHandleInformation = 16,
    SystemInterruptInformation = 23,
    SystemExceptionInformation = 33,
    SystemRegistryQuotaInformation = 37,
    SystemLookasideInformation = 45
}

[StructLayout(LayoutKind.Sequential)]
internal class SYSTEM_PROCESS_INFORMATION
{
    internal uint NextEntryOffset;
    internal uint NumberOfThreads;
    private long _SpareLi1;
    private long _SpareLi2;
    private long _SpareLi3;
    private long _CreateTime;
    private long _UserTime;
    private long _KernelTime;

    internal ushort NameLength;
    internal ushort MaximumNameLength;
    internal IntPtr NamePtr;

    internal int BasePriority;
    internal IntPtr UniqueProcessId;
    internal IntPtr InheritedFromUniqueProcessId;
    internal uint HandleCount;
    internal uint SessionId;
    internal UIntPtr PageDirectoryBase;
    internal UIntPtr PeakVirtualSize;
    internal UIntPtr VirtualSize;
    internal uint PageFaultCount;

    internal UIntPtr PeakWorkingSetSize;
    internal UIntPtr WorkingSetSize;
    internal UIntPtr QuotaPeakPagedPoolUsage;
    internal UIntPtr QuotaPagedPoolUsage;
    internal UIntPtr QuotaPeakNonPagedPoolUsage;
    internal UIntPtr QuotaNonPagedPoolUsage;
    internal UIntPtr PagefileUsage;
    internal UIntPtr PeakPagefileUsage;
    internal UIntPtr PrivatePageCount;

    private long _ReadOperationCount;
    private long _WriteOperationCount;
    private long _OtherOperationCount;
    private long _ReadTransferCount;
    private long _WriteTransferCount;
    private long _OtherTransferCount;
}

[StructLayout(LayoutKind.Sequential)]
internal class SYSTEM_THREAD_INFORMATION
{
    private long _KernelTime;
    private long _UserTime;
    private long _CreateTime;

    private uint _WaitTime;
    internal IntPtr StartAddress;
    internal IntPtr UniqueProcess;
    internal IntPtr UniqueThread;
    internal int Priority;
    internal int BasePriority;
    internal uint ContextSwitches;
    internal uint ThreadState;
    internal uint WaitReason;
}

Как видите WaitReason это простое число, некий код, не очень информативный для нас, так давайте сделаем метод, который это число превратит в нужное нам значение. В C# есть для этого готовый enum, зовется ThreadWaitReason, наша задача получить его из числа, но увы, получаемое число не совсем соответсвует тому, что нам надо, поэтому при помощи простого switch, нам придется делать все руками:

internal static ThreadWaitReason GetThreadWaitReason(int value) 
    => value switch
{
    0 or 7 => ThreadWaitReason.Executive,
    1 or 8 => ThreadWaitReason.FreePage,
    2 or 9 => ThreadWaitReason.PageIn,
    3 or 10 => ThreadWaitReason.SystemAllocation,
    4 or 11 => ThreadWaitReason.ExecutionDelay,
    5 or 12 => ThreadWaitReason.Suspended,
    6 or 13 => ThreadWaitReason.UserRequest,
    14 => ThreadWaitReason.EventPairHigh,
    15 => ThreadWaitReason.EventPairLow,
    16 => ThreadWaitReason.LpcReceive,
    17 => ThreadWaitReason.LpcReply,
    18 => ThreadWaitReason.VirtualMemory,
    19 => ThreadWaitReason.PageOut,
    _ => ThreadWaitReason.Unknown,
};

Можете спросить, а почему 0 и 7 это Executive, а 5 и 12 - это Suspended (и т.д.)? А вы откройте структуру, увидите там есть повторяющиеся статусы, такие как WrExecutive, вот индексы подобных повторений мы и соединяем в один статус, где 0 - это Executive, а 7 - это WrExecutive.

И так, с подготовлением данных мы завершили, остался сам метод, который будет выглядеть примерно так:

[DllImport("ntdll.dll")]
internal static extern NT_STATUS NtQuerySystemInformation(
       [In] SYSTEM_INFORMATION_CLASS SystemInformationClass,
       [In] IntPtr SystemInformation,
       [In] int SystemInformationLength,
       [Out] out int ReturnLength);

Собственно, это все, что нам нужно, осталось выполнить и получить данные.

Пытаемся все это вызвать

Метод работает по принципу записи байт в буфер определенного размера, если этот буфер будет для нас мал, то в ответ мы получим статус STATUS_INFO_LENGTH_MISMATCH, а в out ReturnLength будет требуемый для выполнения размер буфера. Собственно, на данный момент, наша задача, это вызвать метод, чтобы тот записал в буфер данные, а в случае его нехватки, буфер должен сам увеличится. Выглядеть это будет примерно так:

int bufferSize = 128 * 1024;
int requiredSize = 0;
NT_STATUS status;

GCHandle bufferHandle = new GCHandle();

try
{
    do
    {
        long[] buffer = new long[bufferSize];
        bufferHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned);

        status = NtQuerySystemInformation(SYSTEM_INFORMATION_CLASS.SystemProcessInformation, bufferHandle.AddrOfPinnedObject(), bufferSize, out requiredSize);

        if (status == NT_STATUS.STATUS_INFO_LENGTH_MISMATCH)
        {
            if (bufferHandle.IsAllocated) bufferHandle.Free();
            bufferSize = requiredSize + 1024 * 10; // добавляем пару кб на случай появления доп. процессов во время очередного выполнения метода.
        }

    } while (status == NT_STATUS.STATUS_INFO_LENGTH_MISMATCH);
}
finally
{
    if (bufferHandle.IsAllocated) bufferHandle.Free();
}
  • int bufferSize = 128 * 1024; - мы определяем размер буфера таким, чтоб в него поместилась вся информация, о всех процессах.
  • int requiredSize = 0; - в эту переменную метод будет возвращать размер требуемого ему буфера, чтобы уместить все данные.
  • NT_STATUS status; - статус последнего выполнения метода, по которому мы будем крутить ниже цикл.
  • GCHandle bufferHandle = new GCHandle(); - некий объект, который будет помогать нам контролировать память.
  • try/finally - служит для того, чтобы в самом конце выполнения всех действий, задействованная нами память почистилась.
  • do/while - это цикл, который будет пытаться вызвать метод до тех пор, пока ему не хватит размера буфера для получения всех данных.
  • Внутренности цикла думаю и так понятны, мы просто создаем новый буфер, вызываем метод, если метод кричит, что ему не хватает памяти, мы меняем размер буфера (добавив чуть про запас), и очищаем старый буфер, и крутим все по новой.

Так, с этим разобрались, теперь приступим к написанию кода, который в случае успешного статуса, получит нужную нам структуру данных:

if (status == NT_STATUS.STATUS_SUCCESS)
{
    long totalOffset = 0;

    while (true)
    {
        IntPtr currentPtr = checked((IntPtr)(bufferHandle.AddrOfPinnedObject() + totalOffset));
        var processInformation = new SYSTEM_PROCESS_INFORMATION();
        Marshal.PtrToStructure(currentPtr, processInformation);

        //TODO

        if (processInformation.NextEntryOffset is 0) break;
        totalOffset += processInformation.NextEntryOffset;
    }
}
  • так, как метод отдает нам весь список процессов, мы используем цикл while до тех пор, пока метод не отдаст значением NextEntryOffset 0. А до тех пор, мы крутим цикл, забирая нужные нам данные, каждый раз смещаясь по памяти на определенное кол-во байт (которые указаны в NextEntryOffset), попутно записывая в заранее подготовленную структуру все данные. На данном моменте мы уже начинаем получать данные, например processInformation.UniqueProcessId вернет нам id процесса. Нас, по сути, интересует тут лишь id и название, давайте их получим:

      var processId = processInformation.UniqueProcessId.ToInt32();
    
      string processName;
      if (processInformation.NamePtr == IntPtr.Zero)
      {
          processName = processId switch
          {
              0 => "Idle",
              4 => "System",
              _ => processId.ToString(CultureInfo.InvariantCulture)
          };
      }
      else
      {
          processName = Marshal.PtrToStringUni(processInformation.NamePtr, processInformation.NameLength / sizeof(char));
      }
    
      Console.WriteLine($"[{processId}] {processName}");
    

С именем тут как видите, не все так просто, ибо его может и не быть, может быть лишь известный id процесса (системный, например), а если имя и есть, то мы получаем не строку, а Pointer - то есть, некую точку в байтах, с которой начинается информация имени. Вот имея эту точку и размер, мы при помощи Marshal.PtrToStringUni() получаем нужное нам.

В итоге, в консоль у нас должны вывестись все процессы с их Id. Почти все сделано, осталось немного, а именно потоки с их статусами. Тут все тоже самое, мы должны сместиться на определенное число байт и выдернуть нужный нам участок данных, занеся его в заранее подготовленную структуру:

currentPtr = checked((IntPtr)((long)currentPtr + Marshal.SizeOf(processInformation)));
                   
for (int i = 0; i < processInformation.NumberOfThreads; i++)
{
    SYSTEM_THREAD_INFORMATION threadInformation = new SYSTEM_THREAD_INFORMATION();
    Marshal.PtrToStructure(currentPtr, threadInformation);

    var threadProcessId = threadInformation.UniqueProcess.ToInt32();
    var threadId = threadInformation.UniqueThread.ToInt32();

    var threadState = (ThreadState)threadInformation.ThreadState;
    var threadWaitReason = GetThreadWaitReason((int)threadInformation.WaitReason);

    Console.WriteLine($"-- [{threadId}] {threadState}:{threadWaitReason}");

    currentPtr = checked((IntPtr)((long)currentPtr + Marshal.SizeOf(threadInformation)));
}

Тут я думаю вы и сами догадались как все работает. Мы сдвигаем поинтер на размер processInformation, и с этого места берем информацию о каждом потоке процесса, о кол-ве, котором говорит значение NumberOfThreads. Занеся все в нужную структуру мы без труда может вытянуть все нам необходимое, включая и тот самый статус, ради которого все это затевалось.

Результатом будет такое:

[1444] svchost.exe
-- [1440] Wait:UserRequest
-- [3576] Wait:UserRequest
-- [5380] Wait:UserRequest
-- [5388] Wait:EventPairLow
-- [5856] Wait:UserRequest
-- [5780] Wait:UserRequest
-- [5864] Wait:UserRequest
-- [37288] Wait:EventPairLow

[1500] WindscribeService.exe
-- [1512] Wait:UserRequest
-- [4540] Wait:UserRequest
-- [4560] Wait:UserRequest
-- [4596] Wait:ExecutionDelay
-- [14324] Wait:EventPairLow

[1984] svchost.exe
-- [1980] Wait:UserRequest
-- [4304] Wait:UserRequest
-- [7960] Wait:UserRequest
-- [6792] Wait:EventPairLow
-- [18268] Wait:UserRequest
-- [2944] Wait:EventPairLow

Вот по сути и все, что вам нужно, дальше уже я думаю вы сами справитесь, сделаете массив/коллекцию/словарь, в которую поместите нужную вам лично информацию, потоки, и т.д. Ну а заморожен процесс или нет, тут уже смотрите на статусы потоков и их причину, если они все имеют threadWaitReason == ThreadWaitReason.Suspended, то процесс можно считать замороженным, правда тут могу ошибаться, я не особый спец в процессах, это лишь мои наблюдения, которые основаны на подобном (серый - заморожен):

ProcessInfo

→ Ссылка