sources/Google.Solutions.Platform/Dispatch/Win32Process.cs (274 lines of code) (raw):
//
// Copyright 2023 Google LLC
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF 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.
//
using Google.Solutions.Common.Interop;
using Google.Solutions.Common.Runtime;
using Google.Solutions.Common.Util;
using Google.Solutions.Platform.Interop;
using Google.Solutions.Platform.IO;
using Microsoft.Win32.SafeHandles;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Google.Solutions.Platform.Dispatch
{
internal class Win32Process : DisposableBase, IWin32Process
{
private readonly string imageName;
private readonly uint processId;
private readonly SafeProcessHandle process;
private readonly SafeThreadHandle mainThread;
private readonly RegisteredWaitHandle processExitedWaitHandle;
private bool resumedAtLeastOnce = false;
public Win32Process(
string imageName,
uint processId,
SafeProcessHandle process,
SafeThreadHandle mainThread)
{
this.imageName = imageName.ExpectNotNull(nameof(imageName));
this.processId = processId;
this.process = process.ExpectNotNull(nameof(process));
this.mainThread = mainThread.ExpectNotNull(nameof(mainThread));
Debug.Assert(!imageName.Contains("\\"), "Name does not contain path");
this.WaitHandle = this.process.ToWaitHandle(false);
this.processExitedWaitHandle = ThreadPool.RegisterWaitForSingleObject(
this.WaitHandle,
(state, timedOut) =>
{
this.Exited?.Invoke(this, EventArgs.Empty);
},
null,
-1,
true);
}
private void EnumerateTopLevelWindows(Action<IntPtr> action)
{
bool callback(IntPtr hwnd, int _)
{
if (!NativeMethods.IsWindowVisible(hwnd))
{
//
// Window is top-level, but hidden. Ignore.
//
}
else if (NativeMethods.GetWindowThreadProcessId(
hwnd,
out var ownerProcessId) != 0 &&
ownerProcessId == this.processId)
{
//
// This window belongs to our process.
//
action(hwnd);
}
//
// NB. There might be more top-level windows, so continue
// the search.
//
return true;
}
//
// Enumerate all top-level windows.
//
// Ignore ERROR_INVALID_PARAMETER errors as those are expected
// in non-interactive sessions.
//
if (!NativeMethods.EnumWindows(callback, IntPtr.Zero) &&
Marshal.GetLastWin32Error() is int lastError &&
(lastError != NativeMethods.ERROR_SUCCESS &&
lastError != NativeMethods.ERROR_INVALID_PARAMETER &&
lastError != NativeMethods.ERROR_INVALID_HANDLE))
{
throw DispatchException.FromLastWin32Error(
$"{this.imageName}: Enumerating windows failed");
}
}
//---------------------------------------------------------------------
// IWin32Process.
//---------------------------------------------------------------------
public event EventHandler? Exited;
public SafeProcessHandle Handle => this.process;
public string ImageName => this.imageName;
public WaitHandle WaitHandle { get; }
public uint Id
{
get => this.processId;
}
public IWtsSession Session
{
get => WtsSession.FromProcessId(this.processId);
}
public IWin32Job? Job { get; internal set; }
public IPseudoTerminal? PseudoTerminal { get; internal set; }
public bool IsRunning
{
get =>
!this.process.IsClosed &&
NativeMethods.GetExitCodeProcess(this.process, out var exitCode) &&
exitCode == NativeMethods.STILL_ACTIVE;
}
public int WindowCount
{
get
{
var windowCount = 0;
EnumerateTopLevelWindows(_ => windowCount++);
return windowCount;
}
}
public async Task<uint> WaitAsync(CancellationToken cancellationToken)
{
using (var waitHandle = this.process.ToWaitHandle(false))
{
await waitHandle
.WaitAsync(cancellationToken)
.ConfigureAwait(false);
//
// Process terminated.
//
NativeMethods.GetExitCodeProcess(this.process, out var exitCode);
return exitCode;
}
}
public async Task<bool> CloseAsync(CancellationToken cancellationToken)
{
if (!this.IsRunning)
{
return true;
}
//
// Attempt to gracefully close the process by sending a WM_CLOSE message.
//
// See https://web.archive.org/web/20150311053121/http://support.microsoft.com/kb/178893
// for details.
//
var messagesPosted = 0;
EnumerateTopLevelWindows(hwnd =>
{
//
// This window belongs to our process. Post a message to
// tell it to close.
//
NativeMethods.PostMessage(
hwnd,
NativeMethods.WM_CLOSE,
IntPtr.Zero,
IntPtr.Zero);
messagesPosted++;
});
if (messagesPosted > 0)
{
//
// Give the process some time to digest the messages.
//
using (var waitHandle = this.process.ToWaitHandle(false))
{
try
{
await waitHandle
.WaitAsync(cancellationToken)
.ConfigureAwait(false);
//
// Process exited gracefully within the timeout.
//
return true;
}
catch (Exception e) when (e.IsCancellation())
{
//
// User cancelled or timeout elapsed.
//
}
}
}
//
// Use force.
//
Terminate(0);
//
// If we posted a message and got here anyway, then it wasn't
// a graceful close.
//
return messagesPosted == 0;
}
public void Resume()
{
if (NativeMethods.ResumeThread(this.mainThread) < 0)
{
throw DispatchException.FromLastWin32Error(
$"{this.imageName}: Resuming the process failed");
}
this.resumedAtLeastOnce = true;
}
public void Terminate(uint exitCode)
{
if (!NativeMethods.TerminateProcess(this.process, exitCode))
{
throw DispatchException.FromLastWin32Error(
$"{this.imageName}: Terminating the process failed");
}
}
public override string ToString()
{
return $"{this.ImageName} (PID {this.processId})";
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!this.resumedAtLeastOnce)
{
//
// If the process is still in suspended case, merely
// closing handles will leave the process running. To
// avoid that, try terminating the process. This might
// fail if some other part has terminated the process
// in the meantime.
//
NativeMethods.TerminateProcess(this.process, 1);
}
this.mainThread.Close();
this.process.Close();
this.processExitedWaitHandle.Unregister(this.WaitHandle);
this.PseudoTerminal?.Dispose();
}
//---------------------------------------------------------------------
// Factory methods.
//---------------------------------------------------------------------
public static Win32Process FromProcessId(uint processId)
{
//
// Open process.
//
var process = NativeMethods.OpenProcess(
NativeMethods.PROCESS_QUERY_LIMITED_INFORMATION | NativeMethods.SYNCHRONIZE,
false,
processId);
if (process.IsInvalid)
{
throw DispatchException.FromLastWin32Error(
$"The process with ID {processId} does not exist or is inaccessible");
}
//
// Get image name.
//
var imageNameBuffer = new StringBuilder(260);
var imageNameBufferLength = imageNameBuffer.Capacity;
if (!NativeMethods.QueryFullProcessImageName(
process,
0,
imageNameBuffer,
ref imageNameBufferLength))
{
process.Dispose();
throw DispatchException.FromLastWin32Error(
$"Querying the image name of the process with ID {processId} failed");
}
return new Win32Process(
new FileInfo(imageNameBuffer.ToString()).Name,
processId,
process,
new SafeThreadHandle(IntPtr.Zero, true));
}
//---------------------------------------------------------------------
// P/Invoke.
//---------------------------------------------------------------------
private static class NativeMethods
{
internal const uint PROCESS_QUERY_LIMITED_INFORMATION = 0x1000;
internal const uint SYNCHRONIZE = 0x00100000;
internal const uint WM_CLOSE = 0x0010;
internal const int STILL_ACTIVE = 259;
internal const int ERROR_SUCCESS = 0;
internal const int ERROR_INVALID_HANDLE = 6;
internal const int ERROR_INVALID_PARAMETER = 87;
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern int ResumeThread(
SafeThreadHandle hThread);
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool TerminateProcess(
SafeProcessHandle hProcess,
uint exitCode);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetExitCodeProcess(
SafeProcessHandle hProcess,
out uint lpExitCode);
internal delegate bool EnumWindowsProc(
IntPtr hwnd,
int lParam);
[DllImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool EnumWindows(
EnumWindowsProc lpEnumFunc,
IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
internal static extern uint GetWindowThreadProcessId(
IntPtr hWnd,
out uint lpdwProcessId);
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool PostMessage(
IntPtr hWnd,
uint msg,
IntPtr wParam,
IntPtr lParam);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern SafeProcessHandle OpenProcess(
uint processAccess,
bool bInheritHandle,
uint processId);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool QueryFullProcessImageName(
[In] SafeProcessHandle hProcess,
[In] int dwFlags,
[Out] StringBuilder lpExeName,
ref int lpdwSize); [DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool IsWindowVisible(IntPtr hWnd);
}
}
internal class SafeThreadHandle : Win32SafeHandle
{
public SafeThreadHandle(IntPtr handle, bool ownsHandle)
: base(handle, ownsHandle)
{
}
}
}