terminal
terminal copied to clipboard
Process.CloseMainWindow() and Process.Close() are ignored for processes running in terminal
Windows Terminal version
1.11.2921.0
Windows build number
10.0.22000.258
Other Software
.NET Framework 4.7.1
Steps to reproduce
Configure Terminal as default terminal application.
Write application (in our case .NET framework 4.7.1 WPF application) that starts some other console process:
var startInfo = new ProcessStartInfo { UseShellExecute = false }; var process = new Process { EnableRaisingEvents = true, StartInfo = startInfo }; process.Start();
then try to stop this process with the following code:
Process process = Process.GetProcessById(processId); if (!process.HasExited) { process.CloseMainWindow(); if (!process.HasExited) { if (!process.WaitForExit(5000)) { process.Close(); } } }
Expected Behavior
Process is stopped and the terminal tab is closed (same as in the case cmd.exe is the default terminal application).
Actual Behavior
Nothing happens.
process.CloseMainWindow();
The console session [1] that gets created by Windows Terminal is a pseudoconsole session, for which the root process doesn't effectively own any window. To make the above work, the window that gets returned by GetConsoleWindow()
in a pseudoconsole session would have to implement an effective owner, and the window manager would have to special case the reported owner for windows of type "PseudoConsoleWindow", just like it already does for windows of type "ConsoleWindowClass".
In a classic console session, the window that's returned by GetConsoleWindow()
-- if there is one [2] -- stores the process ID and thread ID of its effective owner as window data. The console host maintains the effective owner as the root process in the console's list of processes. This shifts to the next process when the current root process either exits or detaches via FreeConsole()
. See SetConsoleWindowOwner()
. The window-manager function GetWindowThreadProcessId()
is special cased for "ConsoleWindowClass" to return the window's effective owner instead of the real owner. The following example uses a Python shell in a classic console session, in which Python is the root process.
>>> (kernel32.GetCurrentProcessId(), kernel32.GetCurrentThreadId())
(5556, 2720)
>>> proc_list = (ctypes.c_ulong * 10)()
>>> n = kernel32.GetConsoleProcessList(proc_list, len(proc_list))
>>> proc_list[:n]
[5556]
>>> hwnd = kernel32.GetConsoleWindow()
>>> tid = user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
>>> (pid.value, tid)
(5556, 2720)
>>> (user32.GetWindowLongPtrW(hwnd, 0), user32.GetWindowLongPtrW(hwnd, 4))
(5556, 2720)
A process can be gracefully terminated by enumerating the desktop's top-level windows and message-only windows in order to post WM_CLOSE
to the windows that the process owns, as determined by GetWindowThreadProcessId()
. For example, this is what "taskkill.exe" does without the /F
(force) option. This means that a process that allocates a classic console session can be closed gracefully via taskkill /pid <pid>
. Every process in the console session has to exit when the console is closed. Each gets a CTRL_CLOSE_EVENT
with up to "HungAppTimeout" (5000 ms default) to handle it.
Anyway, none of this is implemented for the dummy window that gets returned by GetConsoleWindow()
in a pseudoconsole session. Here's another Python example, but this time under Windows terminal. This example shows that dummy window is owned by "openconsole.exe".
>>> hwnd = kernel32.GetConsoleWindow()
>>> pid = ctypes.c_ulong()
>>> tid = user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
>>> hproc = kernel32.OpenProcess(0x0200_0000, False, pid)
>>> name = (ctypes.c_wchar * 256)()
>>> psapi.GetModuleBaseNameW(hproc, None, name, len(name))
15
>>> name.value
'OpenConsole.exe'
[1] A console session has nothing inherently to do with cmd.exe, which is a shell, not a terminal. The host for a console session is "conhost.exe", or the build distributed with Windows Terminal, "openconsole.exe. An application can inherit, allocate, or attach to a console session, which is a system resource.
[2] A console session can be created without a window, per the CREATE_NO_WINDOW
process creation flag. In this case GetConsoleWindow()
returns NULL
.
The window-manager function
GetWindowThreadProcessId()
is special cased for "ConsoleWindowClass" to return the window's effective owner instead of the real owner.
omfg
is that the solution to #2988 that I've been looking for? I can't find the particular code in the OS that's looking exactly for ConsoleWindowClass
, but there's a lot of code that does that. Maybe we need to be setting the pseudoconsole HWND to that class name (even if it's a lie)
I can't find the particular code in the OS that's looking exactly for
ConsoleWindowClass
, but there's a lot of code that does that.
It's implemented by the system call NtUserQueryWindow()
. Information classes 0 and 2 return the window owner's process ID and thread ID. For example:
>>> hwnd = kernel32.GetConsoleWindow()
>>> kernel32.GetCurrentProcessId(), kernel32.GetCurrentThreadId()
(6552, 6452)
>>> win32u.NtUserQueryWindow(hwnd, 0), win32u.NtUserQueryWindow(hwnd, 2)
(6552, 6452)
In a kernel debugging session, I see that NtUserQueryWindow()
in win32kfull.sys checks for a flag value (0x800) to determine whether to get the window owner's pid and tid values from the window data instead of using the real owner. The "PseudoConsoleWindow" class could be special cased internally to include the same flag. The console host would also have to be updated to support this. Currently, with a classic console window, SetOwner()
is called by RemoveConsole()
, which is initially set in InitWindowsSubsystem()
via SetConsoleWindowOwner()
.
FYI, apparently information class 1 is the pid of the real owner, i.e. "conhost.exe". For example:
>>> win32u.NtUserQueryWindow(hwnd, 1)
1096
>>> name = (ctypes.c_wchar * 256)()
>>> hproc = kernel32.OpenProcess(0x0200_0000, False, 1096)
>>> psapi.GetModuleBaseNameW(hproc, None, name, len(name))
11
>>> name.value
'conhost.exe'
Maybe we need to be setting the pseudoconsole HWND to that class name (even if it's a lie)
The pseudoconsole window shouldn't have the same window procedure. But it should at least have a window procedure that closes the console session when WM_CLOSE
is posted to the window. The processes in the console session will be sent CTRL_CLOSE_EVENT
to let them shut down gracefully. That ties into the above discussion about reporting the primary process in the session as the effective owner of the pseudoconsole window. This allows using the pid of the effective owner to gracefully shut down the entire console session, e.g. via taskkill /pid <pid>
.
But it should at least have a window procedure that closes the console session when
WM_CLOSE
is posted to the window
That seems reasonable, even if people really really shouldn't be using the console HWND, that seems valid.
Notes to self: the flag is set by the ConsoleControl
private api, and you can find the relevant OS code with bConsoleWindow = TRUE
. Not helpful for external folks, but that'll get me close enough.
I wonder if this might have anything to do with the orphaning on update, too. #9914 is the one I'm thinking of.
This allows using the pid of the effective owner to gracefully shut down the entire console session, e.g. via
taskkill /pid <pid>
.
Unfortunately this won't help with .NET Process.CloseMainWindow()
, nor "End Task" in Task Manager. They only send the close message to the visible main window of a process. taskkill.exe, on the other hand, posts the close message to all top-level windows and message-only windows that the process owns. It searches for windows on all desktops of all window stations in the session. Most "graceful terminate" (like Unix SIGTERM
) implementations that I've seen work similarly, at least in part. Few are as comprehensive as taskkill, but they're not as limited as CloseMainWindow()
. For example, Qt's QProcesss::terminate()
has a simple implementation that posts the close message to all top-level windows that the child process owns on the calling thread's desktop. It also tries to post the close message to the main (initial) thread in the process.
@lhecker did this one get fixed too?
Hmm I've been reading through this issue and I feel like my change didn't address this issue. CloseMainWindow()
is similar to running taskkill
without /F
as mentioned above right? And yeah:
C:\Users\lhecker> taskkill /IM cmd.exe
ERROR: The process "cmd.exe" with PID 9256 could not be terminated.
Reason: This process can only be terminated forcefully (with /F option).
As @eryksun writes:
The pseudoconsole window shouldn't have the same window procedure. But it should at least have a window procedure that closes the console session when
WM_CLOSE
is posted to the window.
That seems like a simple enough change to me? All we'd have to do is to call VtIo::SendCloseEvent()
if we receive a WM_CLOSE
.
Edit:
Doesn't work, because cmd.exe receives the message and not the OpenConsole instance that manages it and owns the pseudo window. duh. As mentioned above:
The "PseudoConsoleWindow" class could be special cased internally to include the same flag.
Hi folks here, @eryksun @zadjii-msft @zadjii @lhecker I got confused, possibly due to my limited English language skills and professional knowledge.
Is eryksun recommending openconsole.exe to behave like conhost.exe?
Let me use some examples to illustrate my confusions.
eg1: I launched cmd.exe(pid1), so conhost.exe(pid2) got launched too, then in this console, I launched python.exe(pid3).
In python, kernel32.GetConsoleWindow()
return hwnd
, then user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
set pid to pid1(not pid2, nor pid3), so here why not set pid to pid2?
eg2: I launched cmd.exe(pid11), so openconsole.exe(pid12) got launched too, then in this console, I launched python.exe(pid13).
In python, kernel32.GetConsoleWindow()
return hwnd
, then user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
set pid to pid12(not pid11, nor pid13), so eryksun recommend to let pid=pid11 as the case of conhost.exe?