-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor ConsoleProcessList #14421
Refactor ConsoleProcessList #14421
Conversation
src/interactivity/win32/windowio.cpp
Outdated
@@ -78,7 +78,7 @@ VOID SetConsoleWindowOwner(const HWND hwnd, _Inout_opt_ ConsoleProcessHandle* pP | |||
else | |||
{ | |||
// Find a process to own the console window. If there are none then let's use conhost's. | |||
pProcessData = gci.ProcessHandleList.GetFirstProcess(); | |||
pProcessData = gci.ProcessHandleList.GetOldestProcess(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a behavior change: GetFirstProcess()
used to return the newest process, but I think this was wrong... conhost's PID was the last (= oldest) entry if anything. In fact even the SetConsoleWindowOwner
documentation says:
If ProcessData is nullptr that means the root process has exited so we need to find any old process to be the owner.
So I've chosen to rename this function to better convey its meaning and fix this bug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any old has an idiomatic meaning, though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the comment "If there are none then let's use conhost's." still accurate, or does this now use conhost's process even if others exist?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
any old has an idiomatic meaning, though.
Ah good point.
Is the comment "If there are none then let's use conhost's." still accurate, or does this now use conhost's process even if others exist?
I'm not sure it ever used to be accurate. conhost
itself isn't part of the process lists - console clients are (i.e. shells and other console subsystem applications). I wouldn't know how conhost could be its own client.
In reality this code path is only invoked via Window::SetOwner
when the "root process" exits (the first process that is launched - usually your shell, like cmd.exe
for instance), but there are still other clients attached. This is for instance reproducible if you run pwsh.exe
inside cmd.exe
and close the window. Both processes will get get a close event simultaneously, but PowerShell is slow and so it'll take longer to exit then cmd.exe
. Once cmd.exe
is gone, conhost
will need a new "root process" and in this case that would be pwsh.exe
(until it also exits a hundred or so milliseconds later).
So basically, the comment is at least generally wrong/inaccurate, but I don't want to touch it, because it might prove to be a useful nugget of information if looked up via git blame
in the future. The old code used to use the newest process as the new root process and that seems like a mistake to me. If anything it should be the oldest one as far as I can see.
@@ -30,51 +27,27 @@ using namespace Microsoft::Console::Interactivity; | |||
[[nodiscard]] HRESULT ConsoleProcessList::AllocProcessData(const DWORD dwProcessId, | |||
const DWORD dwThreadId, | |||
const ULONG ulProcessGroupId, | |||
_In_opt_ ConsoleProcessHandle* const pParentProcessData, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pParentProcessData
wasn't used for anything except to change the return value.
return E_FAIL; | ||
// TODO: MSFT: 9574803 - This fires all the time. Did it always do that? | ||
//RETURN_HR(E_FAIL); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's only two callers. ApiDispatchers::ServerGenerateConsoleCtrlEvent
always calls this with a valid pParentProcessData
and won't ever run into this condition. IoDispatchers::ConsoleHandleConnectionRequest
calls this method with a nullptr
instead. This indicates that this error triggers, because we're getting redundant connection requests for the same process ID.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So because of that, this code block never gets called, right?
else
{
if (nullptr != ppProcessData)
{
*ppProcessData = pProcessData;
}
RETURN_HR(S_OK);
}
Wouldn't we still want to keep it for correctness?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No it does get called by IoDispatchers::ConsoleHandleConnectionRequest
. It sets pParentProcessData
to nullptr
which then invokes this branch. It does that so that AllocProcessData
returns S_OK
instead of E_FAIL
if the given process ID already exists.
...but that practically makes the pParentProcessData
parameter a bool allowDuplicateProcessId
and I didn't particularly like that idea, when the caller might as well just filter the E_FAIL
message themselves. This way the AllocProcessData
is now way way simpler: It either sucessfully inserts a new process into the list or it fails. Simple as that! I felt like that would be a good idea.
{ | ||
gci.UnlockConsole(); | ||
return; | ||
} | ||
|
||
// Copy ctrl flags. | ||
auto CtrlFlags = gci.CtrlFlags; | ||
FAIL_FAST_IF(!(!((CtrlFlags & (CONSOLE_CTRL_CLOSE_FLAG | CONSOLE_CTRL_BREAK_FLAG | CONSOLE_CTRL_C_FLAG)) && (CtrlFlags & (CONSOLE_CTRL_LOGOFF_FLAG | CONSOLE_CTRL_SHUTDOWN_FLAG))))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no good reason for this to fail-fast. We can just add a default:
case to the switch case below, which I now did.
if (FAILED(hr) && hr != E_FAIL) | ||
{ | ||
RETURN_HR(hr); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that AllocProcessData
consistently returns E_FAIL
if the process ID already exists, we need this bit of extra code here. TBH I don't quite understand why this adds a "ProcessGroupId" as a process ID to the process list here...
0b65033
to
76a1367
Compare
return E_FAIL; | ||
// TODO: MSFT: 9574803 - This fires all the time. Did it always do that? | ||
//RETURN_HR(E_FAIL); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So because of that, this code block never gets called, right?
else
{
if (nullptr != ppProcessData)
{
*ppProcessData = pProcessData;
}
RETURN_HR(S_OK);
}
Wouldn't we still want to keep it for correctness?
if (rgProcessHandleList[i].hProcess != nullptr) | ||
{ | ||
CloseHandle(rgProcessHandleList[i].hProcess); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we still need this in the event that if (NT_SUCCESS(Status))
fails?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made it so that the hProcess
is a wil::unique_handle
so it'll get cleaned up automatically.
…rocess-list-refactor
b5aca7b
to
2aef629
Compare
This comment has been minimized.
This comment has been minimized.
Can I add a validation step? Launch cmd. Launch Who owns the window? My understanding of the code before your rewrite was that |
// Some applications, when reading the process list through the GetConsoleProcessList API, are expecting | ||
// the returned list of attached process IDs to be from newest to oldest. | ||
// As such, we have to put the newest process into the head of the list. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was LOAD BEARING
in the past. What happened to this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
answer: this moved to GetProcessList
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(my signoff is contingent on making sure the cmd
=> code
=> exit
case works at least close to properly!)
…rocess-list-refactor
@DHowett Did you have a specific invocation of VS Code in mind? The |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor correctness issues, but I love it nonetheless
// Arguments: | ||
// - dwProcessId - ID of the process to search for or ROOT_PROCESS_ID to find the root process. | ||
// - dwProcessId - ID of the process to search for. | ||
// Return Value: | ||
// - Pointer to the process handle information or nullptr if no match was found. | ||
ConsoleProcessHandle* ConsoleProcessList::FindProcessInList(const DWORD dwProcessId) const |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you've validated that nobody calls Find(ROOT)
even unintentionally now?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, there's only a single place calling this function with non-ROOT right now... It's ApiDispatchers::ServerGenerateConsoleCtrlEvent
which calls it with valid process IDs.
{ | ||
*ppProcessData = pProcessData; | ||
} | ||
wil::assign_to_opt_param(ppProcessData, pProcessData.release()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
SCARY! This will delete the processdata if nobody asks for the optional output param
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wait, no it won't. that's even scarier lol
Hello @lhecker! Because this pull request has the p.s. you can customize the way I help with merging this pull request, such as holding this pull request until a specific person approves. Simply @mention me (
|
This commit is just a slight refactor of `ConsoleProcessList` which I've noticed was in a poor shape. It replaces iterators with for-range loops, etc. Additionally this fixes a bug in `SetConsoleWindowOwner`, where it used to default to the newest client process instead of the oldest. Finally, it changes the process container type from a doubly linked list over to a simple array/vector, because using linked lists for heap allocated elements felt quite a bit silly. To preserve the previous behavior of `GetProcessList`, it simply iterates through the vector backwards. * All unit/feature tests pass ✅ * Launching a TUI application inside pwsh inside cmd and exiting kills all 3 applications ✅ (cherry picked from commit 391abaf) Service-Card-Id: 87207823 Service-Version: 1.15
This commit is just a slight refactor of `ConsoleProcessList` which I've noticed was in a poor shape. It replaces iterators with for-range loops, etc. Additionally this fixes a bug in `SetConsoleWindowOwner`, where it used to default to the newest client process instead of the oldest. Finally, it changes the process container type from a doubly linked list over to a simple array/vector, because using linked lists for heap allocated elements felt quite a bit silly. To preserve the previous behavior of `GetProcessList`, it simply iterates through the vector backwards. ## Validation Steps Performed * All unit/feature tests pass ✅ * Launching a TUI application inside pwsh inside cmd and exiting kills all 3 applications ✅ (cherry picked from commit 391abaf) Service-Card-Id: 87207825 Service-Version: 1.16
🎉 Handy links: |
This commit is just a slight refactor of
ConsoleProcessList
which I've noticedwas in a poor shape. It replaces iterators with for-range loops, etc.
Additionally this fixes a bug in
SetConsoleWindowOwner
, where it used todefault to the newest client process instead of the oldest.
Finally, it changes the process container type from a doubly linked list
over to a simple array/vector, because using linked lists for heap allocated
elements felt quite a bit silly. To preserve the previous behavior of
GetProcessList
, it simply iterates through the vector backwards.Validation Steps Performed
and exiting kills all 3 applications ✅