To work around the limitations of GenerateConsoleCtrlEvent, I have to create an intermediate "middle-man" process to handle launching some console applications. The process's main purpose is to call GenerateConsoleCtrlEvent on itself, causing itself and all child process to close cleanly in response to a ctrl+break signal (rather than using Process.Kill). This need arises from the fact that GenerateConsoelCtrlEvent basically has no effect unless the process group id is zero, which means it is only ever effective on the calling process group itself. See: https://stackoverflow.com/a/2431295/88409
So anyway... I've created this intermediate process, which starts a thread that calls Application.Run on a form which processes specific user-defined messages.
My problem is... how do send messages to this process to control it?
I have the Process object and its process id, but that's all. Process.MainWindowHandle is zero.
So I need a way to send a message to a specific process or broadcast the message to all windows in a specific process.
FindWindow is not an option, because it tries to identify a window by name and class on any process, which is unreliable. I want to send a message to a specific process with no ambiguity.
There are 3 scenarios in which a message could be thought of as being sent or posted to a process:
Approach 1 may be too specific, since it targets a specific but arbitrary window. Approach 2 may be not specific enough, since the first enumerated thread is arbitrary and may not have a message loop. Approach 3 is a hybrid approach that first identifies a window, but then posts a thread message to that window's thread, so it is not targetted at a specific window (i.e. it's a "thread message") but it is targetted at a thread that is likely to have a message loop since the thread owns at least one window.
The following is an implementation that supports all three approaches and both methods "send" and "post". Approach 1 is encompassed by the methods SendMessage and PostMessage below. Approach 2 and 3 are encompassed by the method PostThreadMessage below, depending on whether you set the optional parameter ensureTargetThreadHasWindow
(true = approach 3, false = approach 2).
public static class ProcessExtensions
{
private static class Win32
{
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("user32.dll", SetLastError = true)]
public static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("user32.dll", SetLastError = true)]
public static extern bool PostThreadMessage(uint threadId, uint msg, IntPtr wParam, IntPtr lParam);
public delegate bool EnumThreadDelegate (IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
public static extern bool EnumThreadWindows(uint dwThreadId, EnumThreadDelegate lpfn, IntPtr lParam);
[DllImport("user32.dll", SetLastError=true)]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
}
//Sends a message to the first enumerated window in the first enumerated thread with at least one window, and returns the handle of that window through the hwnd output parameter if such a window was enumerated. If a window was enumerated, the return value is the return value of the SendMessage call, otherwise the return value is zero.
public static IntPtr SendMessage( this Process p, out IntPtr hwnd, UInt32 msg, IntPtr wParam, IntPtr lParam )
{
hwnd = p.WindowHandles().FirstOrDefault();
if (hwnd != IntPtr.Zero)
return Win32.SendMessage( hwnd, msg, wParam, lParam );
else
return IntPtr.Zero;
}
//Posts a message to the first enumerated window in the first enumerated thread with at least one window, and returns the handle of that window through the hwnd output parameter if such a window was enumerated. If a window was enumerated, the return value is the return value of the PostMessage call, otherwise the return value is false.
public static bool PostMessage( this Process p, out IntPtr hwnd, UInt32 msg, IntPtr wParam, IntPtr lParam )
{
hwnd = p.WindowHandles().FirstOrDefault();
if (hwnd != IntPtr.Zero)
return Win32.PostMessage( hwnd, msg, wParam, lParam );
else
return false;
}
//Posts a thread message to the first enumerated thread (when ensureTargetThreadHasWindow is false), or posts a thread message to the first enumerated thread with a window, unless no windows are found in which case the call fails. If an appropriate thread was found, the return value is the return value of PostThreadMessage call, otherwise the return value is false.
public static bool PostThreadMessage( this Process p, UInt32 msg, IntPtr wParam, IntPtr lParam, bool ensureTargetThreadHasWindow = true )
{
uint targetThreadId = 0;
if (ensureTargetThreadHasWindow)
{
IntPtr hwnd = p.WindowHandles().FirstOrDefault();
uint processId = 0;
if (hwnd != IntPtr.Zero)
targetThreadId = Win32.GetWindowThreadProcessId( hwnd, out processId );
}
else
{
targetThreadId = (uint)p.Threads[0].Id;
}
if (targetThreadId != 0)
return Win32.PostThreadMessage( targetThreadId, msg, wParam, lParam );
else
return false;
}
public static IEnumerable<IntPtr> WindowHandles( this Process process )
{
var handles = new List<IntPtr>();
foreach (ProcessThread thread in process.Threads)
Win32.EnumThreadWindows( (uint)thread.Id, (hWnd, lParam) => { handles.Add(hWnd); return true; }, IntPtr.Zero );
return handles;
}
}
To use this extension method on a Process object:
Process process = Process.Start( exePath, args );
IntPtr hwndMessageWasSentTo = IntPtr.Zero; //this will receive a non-zero value if SendMessage was called successfully
uint msg = 0xC000; //The message you want to send
IntPtr wParam = IntPtr.Zero; //The wParam value to pass to SendMessage
IntPtr lParam = IntPtr.Zero; //The lParam value to pass to SendMessage
IntPtr returnValue = process.SendMessage( out hwndMessageWasSentTo, msg, wParam, lParam );
if (hwndMessageWasSentTo != IntPtr.Zero)
Console.WriteLine( "Message successfully sent to hwnd: " + hwndMessageWasSentTo.ToString() + " and return value was: " + returnValue.ToString() );
else
Console.WriteLine( "No windows found in process. SendMessage was not called." );