conhost.exe --headless doesn't exit with the child process's exit code. #21622

Closed
opened 2026-01-31 07:49:52 +00:00 by claunia · 8 comments
Owner

Originally created by @mjr4077au on GitHub (May 2, 2024).

Windows Terminal version

1.19.10821.0

Windows build number

10.0.22621.3447

Other Software

No response

Steps to reproduce

  1. From a command prompt, run start /wait conhost.exe --headless powershell.exe -Command "exit 1" or start /wait conhost.exe --headless cmd.exe /c "exit 1"
  2. Check the resulting exit code with echo %errorlevel%.

Without routing the above through conhost.exe --headless, %errorlevel% contains the process's exit code as expected.

Expected Behavior

C:\Users\mjr40>start /wait conhost.exe --headless cmd.exe /c "exit 15"

C:\Users\mjr40>echo %errorlevel%
15

C:\Users\mjr40>

Actual Behavior

C:\Users\mjr40>start /wait conhost.exe --headless cmd.exe /c "exit 15"

C:\Users\mjr40>echo %errorlevel%
0

C:\Users\mjr40>

Originally created by @mjr4077au on GitHub (May 2, 2024). ### Windows Terminal version 1.19.10821.0 ### Windows build number 10.0.22621.3447 ### Other Software _No response_ ### Steps to reproduce 1. From a command prompt, run `start /wait conhost.exe --headless powershell.exe -Command "exit 1"` or `start /wait conhost.exe --headless cmd.exe /c "exit 1"` 2. Check the resulting exit code with `echo %errorlevel%`. Without routing the above through `conhost.exe --headless`, %errorlevel% contains the process's exit code as expected. ### Expected Behavior C:\Users\mjr40>start /wait conhost.exe --headless cmd.exe /c "exit 15" C:\Users\mjr40>echo %errorlevel% 15 C:\Users\mjr40> ### Actual Behavior C:\Users\mjr40>start /wait conhost.exe --headless cmd.exe /c "exit 15" C:\Users\mjr40>echo %errorlevel% 0 C:\Users\mjr40>
claunia added the Issue-QuestionNeeds-TriageIssue-BugResolution-Answered labels 2026-01-31 07:49:52 +00:00
Author
Owner

@zadjii-msft commented on GitHub (May 2, 2024):

Nope, it doesn't. --headless is only there to support ConPty. I don't think conhost powershell -Command "exit 1" would set %errorlevel% either.

@zadjii-msft commented on GitHub (May 2, 2024): Nope, it doesn't. `--headless` is only there to support ConPty. I don't think `conhost powershell -Command "exit 1"` would set %errorlevel% either.
Author
Owner

@mjr4077au commented on GitHub (May 2, 2024):

Is there any reason why it couldn't though? I don't know if I'm looking in the right spot, but on this line is where the lpProcessInformation pointer is passed through: a5835b01b1/src/tools/vtpipeterm/VtConsole.cpp (L311)
Wouldn't it just be a matter of adding the following (or something similar) after it?

DWORD exitcode = 0;
WaitForSingleObject(_piPty.hProcess, INFINITE);
bool result = GetExitCodeProcess(_piPty.hProcess, &exitCode);
@mjr4077au commented on GitHub (May 2, 2024): Is there any reason why it couldn't though? I don't know if I'm looking in the right spot, but on this line is where the lpProcessInformation pointer is passed through: https://github.com/microsoft/terminal/blob/a5835b01b1607bfac1141f0cd4ce9839f260a91f/src/tools/vtpipeterm/VtConsole.cpp#L311 Wouldn't it just be a matter of adding the following (or something similar) after it? ```C DWORD exitcode = 0; WaitForSingleObject(_piPty.hProcess, INFINITE); bool result = GetExitCodeProcess(_piPty.hProcess, &exitCode); ```
Author
Owner

@zadjii-msft commented on GitHub (May 2, 2024):

Is there any reason why it couldn't though

That's complicated. Technically, multiple command-line apps can all attach to the same console. And they can do so in a way that's not necessarily a "tree" of processes. There could be multiple top-level things attached to the console. In that case, who's exit code should be returned? If the first thing you spawned exits, but there's still something else attached to the console, should we exit with the first process's exit code (terminating the other process when the console goes away)?


Maybe we should take a step back here - what are you trying to accomplish? conhost --headless is barely documented. It's almost certainly not what you want to use, for whatever you're trying to accomplish.

@zadjii-msft commented on GitHub (May 2, 2024): > Is there any reason why it couldn't though That's complicated. Technically, multiple command-line apps can all attach to the same console. And they can do so in a way that's not necessarily a "tree" of processes. There could be multiple top-level things attached to the console. In that case, who's exit code should be returned? If the first thing you spawned exits, but there's still something else attached to the console, should we exit with the first process's exit code (terminating the other process when the console goes away)? --- Maybe we should take a step back here - what are you trying to accomplish? `conhost --headless` is barely documented. It's almost certainly not what you want to use, for whatever you're trying to accomplish.
Author
Owner

@mjr4077au commented on GitHub (May 2, 2024):

That's complicated. Technically, multiple command-line apps can all attach to the same console. And they can do so in a way that's not necessarily a "tree" of processes. There could be multiple top-level things attached to the console. In that case, who's exit code should be returned? If the first thing you spawned exits, but there's still something else attached to the console, should we exit with the first process's exit code (terminating the other process when the console goes away)?

That's all valid information. I must admit I don't have a deep understanding about how the console host works. I guess if it was viable then it'd have been implemented.

Maybe we should take a step back here - what are you trying to accomplish? conhost --headless is barely documented. It's almost certainly not what you want to use, for whatever you're trying to accomplish.

I have a number of scripted applications within Microsoft Intune that execute within the user's context. Tickets get raised frequently regarding the console window blips that occur from this and I'm wanting to mask out the window from appearing entirely, but its crucial I get the exit code returned so Intune can pick it up.

People have done this via vbscript in the past, but that's deprecated and on the way out. I was hoping conhost.exe --headless would be a digitally signed, valid way to achieve my requirements without having to compile my own executable to hide the console window.

@mjr4077au commented on GitHub (May 2, 2024): > That's complicated. Technically, multiple command-line apps can all attach to the same console. And they can do so in a way that's not necessarily a "tree" of processes. There could be multiple top-level things attached to the console. In that case, who's exit code should be returned? If the first thing you spawned exits, but there's still something else attached to the console, should we exit with the first process's exit code (terminating the other process when the console goes away)? That's all valid information. I must admit I don't have a deep understanding about how the console host works. I guess if it was viable then it'd have been implemented. > Maybe we should take a step back here - what are you trying to accomplish? `conhost --headless` is barely documented. It's almost certainly not what you want to use, for whatever you're trying to accomplish. I have a number of scripted applications within Microsoft Intune that execute within the user's context. Tickets get raised frequently regarding the console window blips that occur from this and I'm wanting to mask out the window from appearing entirely, but its crucial I get the exit code returned so Intune can pick it up. People have done this via vbscript in the past, but that's deprecated and on the way out. I was hoping `conhost.exe --headless` would be a digitally signed, valid way to achieve my requirements without having to compile my own executable to hide the console window.
Author
Owner

@eryksun commented on GitHub (May 4, 2024):

People have done this via vbscript in the past, but that's deprecated and on the way out. I was hoping conhost.exe --headless would be a digitally signed, valid way to achieve my requirements without having to compile my own executable to hide the console window.

Can you use another scripting language that has signed executables, such as Python? For example: pyw -c "import sys, subprocess; sys.exit(subprocess.run('cmd /c \"exit 15\"', creationflags =subprocess.CREATE_NO_WINDOW).returncode)". "pyw.exe" is the launcher to execute non-console "pythonw.exe", which by default will be the highest version installed, regardless of PATH. It proxies the exit code of the launched "pythonw.exe" process, which in this case proxies the exit code of the process spawned via subprocess.run().

Note, however, that if either "pythonw.exe" or "pyw.exe" is forcibly terminated or crashes, the exit code will be unrelated to the command that's executed via subprocess.run(). The same would apply to any launcher because launchers on Windows have to spawn a child process. That's because the Windows API doesn't support an operation like POSIX exec(), which replaces the current process image.

That's complicated. Technically, multiple command-line apps can all attach to the same console. And they can do so in a way that's not necessarily a "tree" of processes. There could be multiple top-level things attached to the console. In that case, who's exit code should be returned? If the first thing you spawned exits, but there's still something else attached to the console, should we exit with the first process's exit code (terminating the other process when the console goes away)?

If a classic console session has a window, the window pretends to be owned by the lead attached process (it's really owned by "conhost.exe"), which is at the head of the queue of attached processes. If that process exits, the next process in the list is promoted as the reported owner, according to the order of the result from GetConsoleProcessList(&list, count). The ID of the process that effectively owns the console window is returned by GetWindowThreadProcessId(GetConsoleWindow(), &processId). With a handle for this process, one can wait for it to exit and get the exit code via GetExitCodeProcess(hProcess, &exitCode).

For reasonable consistency, if "conhost.exe" proxies an exit code (and that's a big if), I'd expect it to be that of the last reported owner of the console window. Normally that would be the last process attached to the console before it was destroyed. However, if the console session is forcibly ended by closing the window, I'd expect it to use the exit code of the process that was the effective owner of the console window at closing time. I'd expect a --headless session to be consistent with this, with regard to the order of the result from GetConsoleProcessList(&list, count).

@eryksun commented on GitHub (May 4, 2024): >People have done this via vbscript in the past, but that's deprecated and on the way out. I was hoping conhost.exe --headless would be a digitally signed, valid way to achieve my requirements without having to compile my own executable to hide the console window. Can you use another scripting language that has signed executables, such as Python? For example: `pyw -c "import sys, subprocess; sys.exit(subprocess.run('cmd /c \"exit 15\"', creationflags =subprocess.CREATE_NO_WINDOW).returncode)"`. "pyw.exe" is the launcher to execute non-console "pythonw.exe", which by default will be the highest version installed, regardless of `PATH`. It proxies the exit code of the launched "pythonw.exe" process, which in this case proxies the exit code of the process spawned via `subprocess.run()`. Note, however, that if either "pythonw.exe" or "pyw.exe" is forcibly terminated or crashes, the exit code will be unrelated to the command that's executed via `subprocess.run()`. The same would apply to any launcher because launchers on Windows have to spawn a child process. That's because the Windows API doesn't support an operation like POSIX `exec()`, which replaces the current process image. > That's complicated. Technically, multiple command-line apps can all attach to the same console. And they can do so in a way that's not necessarily a "tree" of processes. There could be multiple top-level things attached to the console. In that case, who's exit code should be returned? If the first thing you spawned exits, but there's still something else attached to the console, should we exit with the first process's exit code (terminating the other process when the console goes away)? If a classic console session has a window, the window pretends to be owned by the lead attached process (it's really owned by "conhost.exe"), which is at the head of the queue of attached processes. If that process exits, the next process in the list is promoted as the reported owner, according to the order of the result from `GetConsoleProcessList(&list, count)`. The ID of the process that effectively owns the console window is returned by `GetWindowThreadProcessId(GetConsoleWindow(), &processId)`. With a handle for this process, one can wait for it to exit and get the exit code via `GetExitCodeProcess(hProcess, &exitCode)`. For reasonable consistency, if "conhost.exe" proxies an exit code (and that's a big if), I'd expect it to be that of the last reported owner of the console window. Normally that would be the last process attached to the console before it was destroyed. However, if the console session is forcibly ended by closing the window, I'd expect it to use the exit code of the process that was the effective owner of the console window at closing time. I'd expect a `--headless` session to be consistent with this, with regard to the order of the result from `GetConsoleProcessList(&list, count)`.
Author
Owner

@mjr4077au commented on GitHub (May 5, 2024):

Can you use another scripting language that has signed executables, such as Python? For example: pyw -c "import sys, subprocess; sys.exit(subprocess.run('cmd /c "exit 15"', creationflags =subprocess.CREATE_NO_WINDOW).returncode)". "pyw.exe" is the launcher to execute non-console "pythonw.exe", which by default will be the highest version installed, regardless of PATH. It proxies the exit code of the launched "pythonw.exe" process, which in this case proxies the exit code of the process spawned via subprocess.run().

Note, however, that if either "pythonw.exe" or "pyw.exe" is forcibly terminated or crashes, the exit code will be unrelated to the command that's executed via subprocess.run(). The same would apply to any launcher because launchers on Windows have to spawn a child process. That's because the Windows API doesn't support an operation like POSIX exec(), which replaces the current process image.

I could certainly do that and that approach is something I hadn't considered, however it does introduce a dependency into my system that I'd rather avoid if possible. I thought I'd broach the topic here, not with any particular expectations but rather just to see if its within the realm of possibility.

I'm led to believe this PR might help with this problem in the future though? https://github.com/microsoft/terminal/pull/7337. It's cross-referenced on an ongoing PowerShell issue here: https://github.com/PowerShell/PowerShell/issues/3028,

@mjr4077au commented on GitHub (May 5, 2024): > Can you use another scripting language that has signed executables, such as Python? For example: pyw -c "import sys, subprocess; sys.exit(subprocess.run('cmd /c \"exit 15\"', creationflags =subprocess.CREATE_NO_WINDOW).returncode)". "pyw.exe" is the launcher to execute non-console "pythonw.exe", which by default will be the highest version installed, regardless of PATH. It proxies the exit code of the launched "pythonw.exe" process, which in this case proxies the exit code of the process spawned via subprocess.run(). > Note, however, that if either "pythonw.exe" or "pyw.exe" is forcibly terminated or crashes, the exit code will be unrelated to the command that's executed via subprocess.run(). The same would apply to any launcher because launchers on Windows have to spawn a child process. That's because the Windows API doesn't support an operation like POSIX exec(), which replaces the current process image. I could certainly do that and that approach is something I hadn't considered, however it does introduce a dependency into my system that I'd rather avoid if possible. I thought I'd broach the topic here, not with any particular expectations but rather just to see if its within the realm of possibility. I'm led to believe this PR might help with this problem in the future though? https://github.com/microsoft/terminal/pull/7337. It's cross-referenced on an ongoing PowerShell issue here: https://github.com/PowerShell/PowerShell/issues/3028,
Author
Owner

@lhecker commented on GitHub (May 8, 2024):

Yes, thanks to #7337 in the upcoming Windows versions later this year, PowerShell will be able to completely prevent a window from being created.

@lhecker commented on GitHub (May 8, 2024): Yes, thanks to #7337 in the upcoming Windows versions later this year, PowerShell will be able to completely prevent a window from being created.
Author
Owner

@mjr4077au commented on GitHub (May 9, 2024):

Sounds like this will be my best bet, but I'll investigate that Python option also. Thank for to everyone who took the time to respond to this request, it's been much appreciated!

@mjr4077au commented on GitHub (May 9, 2024): Sounds like this will be my best bet, but I'll investigate that Python option also. Thank for to everyone who took the time to respond to this request, it's been much appreciated!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/terminal#21622