microsoft / terminal

The new Windows Terminal and the original Windows console host, all in the same place!
MIT License
94.23k stars 8.16k forks source link

DISCUSSION: Is my app running in Terminal? #7434

Open vefatica opened 3 years ago

vefatica commented 3 years ago

I've been using

!((BOOL) SendMessage(GetConsoleWindow(), WM_GETICON, 0, 0))

Other ideas? Thanks. - Vince

zadjii-msft commented 3 years ago

Could you clarify? What exactly are you trying to do?

DHowett commented 3 years ago

Looks like somebody is trying to set the icon by literally telling the window manager to replace it. Hm.

vefatica commented 3 years ago

I'm trying to find out if my app is running in Windows Terminal. I don't care about the icon. If I'm in a console, that evaluates to FALSE; in Terminal to TRUE.

DHowett commented 3 years ago

If you have a reason to detect if you are running inside terminal, we should probably have the discussion as to why you want to detect that.

Any behavior you believe to be Terminal-specific is not, I promise you, and your application will do the wrong thing.

vefatica commented 3 years ago

Didn't you (a while back) give me a list of Win32 API console functions that wouldn't work as expected in Terminal. I suppose much behavior IS Terminal-specific and I don't want my app doing the wrong thing.

zadjii-msft commented 3 years ago

Ah, those functions you should avoid calling in general, not just "in the Windows Terminal". Those are functions that'll work less well in any ConPTY session. This includes the Windows Terminal, ssh sessions, and other terminal emulators as well, like VsCode, hyper, mintty, etc. In those other applications, you might not be able to detect that you're running in the Windows Terminal, but those APIs would still behave weirdly.

Very specifically, GetConsoleWindow is one of those APIs πŸ˜‰

vefatica commented 3 years ago

I wish I saw the bigger picture. I don't even know what ConPTY is. For me Windows Terminal is the only alternative to conhost. I don't know how to re-phrase my question.

zadjii-msft commented 3 years ago

Don't worry about it, let me help however I can.

In short: The rest of the terminal world (*nix) uses streams of characters to have client applications interact with the Terminal. On windows however, client apps use the Console API to interact with the "terminal" (conhost.exe).

The Console API however is terrible and full of poorly documented, poorly supported APIs. Plus, it only works on Windows, so it's hard for commandline client apps to target both Windows and *nix OSes.

Enter ConPTY - Conpty is a magic layer that translates all the console API calls to streams of characters, so that they could be used by any terminal emulator. This means that terminal no longer needs to understand the ins and outs of the console API - conpty will do that bit for them.

Conpty is what powers the Terminal - fundamentally, the Terminal could be hooked up to a linux commandline client app, and work just fine, via a stream of characters.

This is a very broad overview, but should help explain the situation.

vefatica commented 3 years ago

Thanks. For me, Terminal is just a way to run command interpreters in Windows. It has features that the Windows console lacks.

My preferred command interpreter is "TCC" (grandson of "4DOS", still from JP Software). I write plugins (DLLs) to extend its capabilities. Having used the console APIs for a long time I'm pretty comfortable with them. Many things don't work in Terminal (and never will) and I'd like one of my plugins (dedicated to console-related things) to decline being loaded if TCC is running in Terminal. Hence, my question.

The SendMessage was just a gimmick. I used WM_GETICON because I saw it sent to (Terminal's version of) GetConsoleWindow().

And FWIW, WindowsTerminal.exe crashed on me a few times after being spied on with Spy++. That is not reliably reproducible.

zadjii-msft commented 3 years ago

Many things don't work in Terminal (and never will)

What kinds of things are you doing specifically? I'd love to hear more about your use case

vefatica commented 3 years ago

Hmmm! I could go on for a long time. The plugin interface allows the plugin to export COMMANDs, internal _VARIABLES, variable @FUNCTIONs, and the address of a keystroke-handling function. In all, I have more than 300 items that do quite a variety of things. Here are a very few examples.

CONSIZE - set any of screen buffer size, console window size (characters), and console window position (absolute, relative, and nine canonical centered positions)

CONIMAGE/CONRESTORE - save/restore the console sceeen buffer (text and attributes),

SETICON - set the console's icon (to any from a DLL or EXE)

v:> echo %@icons[c:\windows\system32\shell32.dll] 329

v:> echo %@keytime[hkcu\environment] 2020-08-23,12:40:21.1293486

v:> echo %@up[v] 7 days 1 hour 34 minutes 17 seconds

v:> echo %@up[d] 7.06666

v:> echo %_ruler ....+....1....+....2....+....3....+....4....+....5....+....6....+....7....+....8....+....9....+....0....+....1....+....2....+...

My keyhandler's most useful function is bound to Ctrl-Del. Leaving the current prompt fixed, it erases the text above, one line at a time, while scrolling the text further-above down (to just above the command line). There is no viewport/history distinction as in Terminal so I can go right up to the top of the console screen buffer. This is very useful if I'd like to execute a sequence of commands (say to be copied and pasted into a post). If I botch one I can erase it and its output and try again. Ctrl-Shift-Del does the same 1 page at a time.

I already have a rudimentary 4WT.DLL plugin which implements Ctrl-Del = delete upwards (as best it can ... pretty easy with VT control sequences).

vefatica commented 3 years ago

I might have gotten carried away there. If you only wanted to hear about things that are or might be problematic in Terminal ...

v:> help _curchar character at mouse cursor (decimal)

I don't know if I can do that if running in Terminal. Does GetCurrentConsoleFontEx() work? I might also need Terminal's padding?????

jdebp commented 3 years ago

M. Fatica: http://jdebp.uk./FGA/capture-console-win32.html may help a little bit with the concepts and architecture.

Other people: See https://jpsoft.com/forums/threads/detect-windows-terminal.10484/ for some more context.

vefatica commented 3 years ago

Thanks, jdebp (and "hi" after many years).

DHowett commented 3 years ago

(I'm going to keep this open as a question/discussion topic.)

german-one commented 3 years ago

After thinking about it, I'd rather ask the question "Is my app running in the Windows Console?"

My conclusion is that we should have an API function that clearly identifies if the console is the user interface. As a nice-to-have it may also identify which alternative terminal is used. I'm thinking about concepts like for GetFileType or GetDriveType which return identifiers. Perhaps something like a GetCliType function which returns 0 for console, greater than 0 for a terminal (e.g. 1 for WT, 2 for ... etc.), and a negative value (HRESULT?) if the function failed.

DHowett commented 3 years ago

This cannot be known generally. ssh.exe from Windows to sshd.exe on Windows will usually be using the normal console host (conhost) as its host, but with specific requirements.

If an application can be updated to use a new API (and fall back to non-console-host behavior when the normal console host UI isn’t in use), it could just be updated to do that all the time without checking an API and suddenly it would work better everywhere!

HolisticDeveloper commented 3 years ago

I did find that when running in Windows Terminal there is a WT_SESSION environment variable defined.

vefatica commented 3 years ago

Yes, but WT_SESSION would be inherited if, for example, CMD, running in WT, did "start myapp" in which case myapp would be in a console.

chrisant996 commented 2 years ago

Clink needs to know some capabilities of the terminal host it's running inside. For example:

  1. What level of ANSI escape code support the terminal has.
  2. Whether it is safe to use SetConsoleCursorInfo to show/hide the cursor, without triggering the bug that also changes the cursor size/shape.
  3. What font rendering technology the terminal uses. For example, does the terminal render ❯ (U+276F, Heavy Right-Pointing Angle Quotation Mark Ornament) even when the selected font does not contain that glyph? Windows Terminal does; conhost and ConEmu and etc do not.

Also, does Windows Terminal run on Windows 8.1 or lower? If so, then wouldn't it bring newer terminal features to older OS versions, and then APIs like SetConsoleCursorInfo become unsafe to use even on OS versions where they would normally be safe.

The problem with deprecating console APIs and saying "don't use them" is the interop strategy is unclear. How can a console app detect whether it is running in an environment that supports the techniques that replace the console APIs? When running in an environment that doesn't support things that ConPTY/OpenConsole/etc have introduced, it's necessary to fall back to using the console APIs.

So, console programs legitimately need to be able to identify whether they are running in "the 'new' console system that is colloquially associated with Windows Terminal" versus running in some other/older console system.

(Checking WT_SESSION is unreliable because the variable gets inherited, which leads to wrong outcomes.)

Diablo-D3 commented 1 year ago

Shouldn't the question be answered as, simply, check if $TERM is set, and if you don't want to handle the particularities of terminals, pretend all terminals comply to the modern standard of "emulate xterm-256color", and emit the proper sequences?

chrisant996 commented 1 year ago

@Diablo-D3 no, and I'll try to share more details:

  1. Neither $TERM on *nix nor %TERM% on Windows indicate anything about the font, the cursor API quirks, or rendering capabilities (e.g. emoji substitutions, CJK width adjustments, etc).
  2. %TERM% doesn't address the %WT_SESSION% inheritance problem at all.
  3. Clink is a Windows program, and generally speaking %TERM% has no meaning for Windows programs (though it can have meaning for *nix programs ported to Windows). And so it's generally not set at all.
  4. Environment variables are inherited, and so %WT_SESSION% and %TERM% can be inherited when a console mode program running inside one terminal program spawns a new console window that gets hosted by a different terminal program -- thus causing malfunctions.

I don't believe that users should have to do a bunch of arcane configuration before programs can work reasonably (on Windows %TERM% qualifies as "arcane" because it's not a concept Windows users are really exposed to on Windows).

More importantly, I don't want to handle all the support requests when Windows users attempt to use a Windows console program like Clink and right out of the box it doesn't work at all, simply because the Windows users are using Windows and thus don't have %TERM% set. πŸ˜‰

Diablo-D3 commented 1 year ago

1) It is not the job of a program running in a terminal to know if the terminal supports any of those. Any program that wishes to support that either should argue for an OSC extension, or should have this as configurable behavior and rely on the user to know what they're doing.

2) $WT_SESSION seems like something that third party programs shouldn't use to infer behavior. If I run clink via ssh, using Microsoft's official openssh server binary, $WT_SESSION will not be set.

3) Having $TERM unset tells you that you're on an extremely plain terminal, and since you only target Windows, its clearly the old conhost.

4) Your situation only makes sense if I've explicitly invoked a console program in a way that it spawns a new conhost. If I did that, I would consider it user error, as I expect all child processes that are also console programs to run in the same console.

You're right in that Windows users are unfamiliar with $TERM. Microsoft chose not to implement this 30+ years ago, and Microsoft customers have been paying for that oversight ever since. However, terminal programs in general are "arcane"; the average user on any OS (even Linux, nowadays) are going to be unfamiliar with terminals.

DHowett commented 1 year ago

its clearly the old conhost.

Which, on Windows 10+, supports a whole host of control sequences... which muddies the water significantly.

chrisant996 commented 1 year ago
  1. It is not the job of a program running in a terminal to know if the terminal supports any of those. Any program that wishes to support that either should argue for an OSC extension, or should have this as configurable behavior and rely on the user to know what they're doing.

Almost. But Clink uses IAT hooking to extend CMD.exe. And Clink includes a built in terminal emulator for when it's running in a host that lacks support for escape sequences. And some terminal GUI hosts such as ZConsole just pass escape sequences to the underlying console. Clink needs to know a little about the environment in which it's running. %TERM% plainly does not achieve that, because it's not present.

  1. $WT_SESSION seems like something that third party programs shouldn't use to infer behavior. If I run clink via ssh, using Microsoft's official openssh server binary, $WT_SESSION will not be set.

It is the only way for a program to infer whether it is running inside Microsoft Terminal. Clink needs a reliable way to detect that. %TERM% plainly does not achieve that.

  1. Having $TERM unset tells you that you're on an extremely plain terminal, and since you only target Windows, its clearly the old conhost.

No. How did you reach that conclusion?

  1. Your situation only makes sense if I've explicitly invoked a console program in a way that it spawns a new conhost. If I did that, I would consider it user error, as I expect all child processes that are also console programs to run in the same console.

It is not "user error" to start Windows Terminal, then start a GUI app from WT, then start a console program from the GUI app. That is a perfectly reasonable thing for a user to do, and it indeed happens.

You're right in that Windows users are unfamiliar with $TERM. Microsoft chose not to implement this 30+ years ago, and Microsoft customers have been paying for that oversight ever since. However, terminal programs in general are "arcane"; the average user on any OS (even Linux, nowadays) are going to be unfamiliar with terminals.

My point is that Clink wants to provide a seamless experience for users, and that there is no way to do so. It seems you believe that programs should not do that, and that users should not use shells without both configuring the shell and configuring the terminal in advance. I can respect that viewpoint, but I cannot require my program's users to do all that. Especially when there is no documentation or system in place for that on Windows.

chrisant996 commented 1 year ago

I expect all child processes that are also console programs to run in the same console.

The expectation is mistaken:

On Windows, there are many ways for a console program to run in a new console window. There are APIs for controlling that, the start command controls that, and when a console then GUI then console the second console is separate because GUIs are not attached to any console.

vefatica commented 1 year ago

With specific regard to a shell wanting to know where the Windows Terminal user interface is ...

I revisit this every now and then. I'm using the process ID of GetConsoleWindow() and a Toolhelp32Snapshot to find the parent process of OpenConsole.exe. Then I use EnumWindows() to find a CASCADIA_HOSTING_WINDOW_CLASS window belonging to that parent. It works, at least to my satisfaction, but it's a bit cumbersome.

Could you make the handle of the CASCADIA_HOSTING_WINDOW_CLASS window more readily available? One thought is to put it in the value of the WT_SESSION environment variable (along with the GUID which is (AFAICT) useless to the shell.

    `WT_SESSION=69f813f0-06ef-49d5-a64a-95359e0305c8, 0x303C0`

A shell might be looking for the existence of that variable anyway. If it's value had more useful information in it, that would be great.

Am I wrong about the GUID? Is it of any value to the shell (or to anyone)?

german-one commented 1 year ago

Stop worrying about it. It's pretty obvious that there will be no support from the Terminal folks on this issue. I think your initial "get icon" approach is a good point to start having fun πŸ˜„ And based on that, I had some fun recently: https://github.com/german-one/termproc Now it already rings in my ears "I whouldn't if I was you ...", "Don't rely on ...". Also "But SSH ..." when people were asking about locally running applications etc. Yes, of course those work-arounds make things worse. What else but the worst can ever be expected if we don't get an easier way? 🀣

adithya-s-sekhar commented 1 year ago

What's with the whole stackoverflow attitude of "you're not supposed to do that"? Setting custom window sizes, screen buffers have been a thing on cmd since the dark ages. For an OS that touts itself so much on backward compatibility that it goes out of its way to keep even the rarest of things working the same way, this is such a stupid thing to do.

Let us have a way to detect if the script is running in Windows Terminal, we're not asking you to add features from conhost.

german-one commented 1 year ago

I archived my old termproc repo and created a new one to reflect the new process model of Windows Terminal v. 1.18 and its feature to move tabs between windows.

The new code can be found in my termwnd repo for those who are interested in.

mataha commented 12 months ago

I have a similar conundrum - not being able to detect whether ANSI sequences are supported (for, among other things, colored output and line movement) which boils down to checking if I'm running inside conhost.exe; I can't even send CSI 0 c as there won't be a response - the code will wait forever.

Use-case ```batchfile @setlocal EnableExtensions & if "%DEBUG%"=="" echo off call :query_state response "0c" c if defined response ( set /p="%response:*[=^[[%" (echo() ) "%device%" for /l %%_ in (1, 1, %index%) do (pause) <"%device%" >nul for /f "skip=1 tokens=1 delims=* eol=" %%c in ( '""%SystemRoot%\system32\replace.exe" ? . /w /u <"%device%""' ) do ( set "character=%%c" ) set /a "index+=1" set "response=%response%%character%" if defined character if not "%character%"=="%3" ( goto :query_state_unpack ) endlocal & set "%~1=%response%" & goto :EOF ```

To help with that I've cooked the following algorithm; no idea whether it's suitable for all scenarios, but I'd be happy to receive any feedback:

  1. For each child conhost.exe of the examined process (cmd.exe or whatever):
    • if it doesn't have any children - bingo
  2. Go up a process
    • if we can't - it's not conhost.exe
    • if it's conhost.exe - bingo
    • else go to step 1
chrisant996 commented 11 months ago

I have a similar conundrum - not being able to detect whether ANSI sequences are supported (for, among other things, colored output and line movement) which boils down to checking if I'm running inside conhost.exe

@mataha It sounds like you're checking whether there's a conhost.exe that can be identified as either the parent or child of the current cmd.exe, and concluding that means escape sequences are supported.

A few examples where the approach doesn't work (there are more):

Looking for conhost.exe has a low enough correlation to escape sequence support that I wouldn't recommend to pursue trying to expand on it. I'd recommend to find a different approach entirely.

With that said, if you like the results it achieves in the scenarios that your code runs in, then that's probably what's important for your own purposes. It doesn't generalize well, though.

chrisant996 commented 11 months ago

For Clink (which hooks into cmd.exe and runs within the cmd.exe process itself) the following is what I'm currently exploring/refining for determining whether cmd.exe is running in Windows Terminal.

The intent, of course, is to stop looking at %WT_SESSION% at all since it's too unreliable (and prone to both false positives and false negatives).

It's running in Windows Terminal if any of the following are true:

(Edited to clarify about OpenConsole.exe.)

lhecker commented 11 months ago

To be honest, I haven't followed the discussion from the beginning so please ignore me if I misunderstood this, but isn't the correct question to ask (if anything) "Am I running under ConPTY?" and not really about Windows Terminal at all? After all, the issues mentioned here aren't really specific to Windows Terminal, but rather for anyone using ConPTY right?

As such, couldn't you do this?

wchar_t buffer[32];
const auto length = GetClassNameW(GetConsoleWindow(), &buffer[0], 32);
const auto isConhost = length == 18 && memcmp(&buffer[0], L"ConsoleWindowClass", 36) == 0;

With ConPTY the buffer will instead contain PseudoConsoleWindow.

I would not test for PseudoConsoleWindow however. While neither class name is technically part of a public API, ConsoleWindowClass is way older and much more specific. It would also still work if a 3rd class name was ever introduced. Or if we made GetConsoleWindow return nullptr with ConPTY, etc.

chrisant996 commented 11 months ago

To be honest, I haven't followed the discussion from the beginning so please ignore me if I misunderstood this, but isn't the correct question to ask (if anything) "Am I running under ConPTY?" and not really about Windows Terminal at all? After all, the issues mentioned here aren't really specific to Windows Terminal, but rather for anyone using ConPTY right?

Both questions are valid, depending on what the code is trying to determine (or why).

For example, if code wants to know whether Windows Terminal shell integration support is present, that's a Windows Terminal feature, not ConPty.

chrisant996 commented 11 months ago

@lhecker Here are a few more examples why a console mode program may need to know specifically which terminal program is hosting them. While ConPty manages the screen buffer, individual terminal programs are responsible for rendering the screen, and they render it differently. Those differences can be crucial for console mode apps that are trying to align things or right-justify things or measure things.

"What exactly would you light up only for windows terminal users and not Visual Studio or VS Code users?"

Quick example of color emoji rendering differences

Legacy console: image

ConEmu: image

Windows Terminal: image

vefatica commented 11 months ago

What's a shell to do if its conpty is not attached to a terminal (ssh, for example)? Since starting this thread, I have worked out most of my difficulties. If you monkey around enough with GetConsoleWindow, GetClassName, GetWindowThreadProcessId, CreateToolhelp32Snapshot (for parent PIDs), and EnumWindows, you can figure out if your in WT (and get WT's PID and a handle to its CASCADIA_HOSTING_WINDOW_CLASS window if you want them). That's all I wanted.

You could probably, and similarly, figure out if you're in any terminal emulator, and which one. But what's a shell to do ... hard-code for differences among terminal emulators? That's not for me.

chrisant996 commented 11 months ago

But what's a shell to do ... hard-code for differences among terminal emulators? That's not for me.

A shell could require the user (and/or the terminal itself) to tell the shell about the terminal emulator it's running in. Like $term and terminfo in Linux. Windows has no equivalent (and certainly no standard), which leads console mode apps on Windows to try to deduce things themselves, which leads to a wide range of malfunction scenarios in different combinations of terminals and apps.

I don't know how (or if) terminfo/ncurses describe color emoji capabilities in Linux terminals.

vefatica commented 11 months ago

I don't know how terminfo works. Who writes it? Who reads it? And who implements what's in it?

chrisant996 commented 11 months ago

I don't know how terminfo works. Who writes it? Who reads it? And who implements what's in it?

A good place to start reading to understand the terminfo stuff is the link I shared on the previous reply.

tig commented 8 months ago

Terminal.Gui maintainer here.

We feel we need this. We can't move to Virtual Terminal Sequences any time soon (esp given the perf issues). In the meantime, the conhost and WT behave so differently (as have been noted above). We've gone back and forth between testing for WT_SESSION and trying to detect "If the parent process is WindowsTerminal.exe". It's just yucky and frustrating.

a-usr commented 3 months ago

I have a similar conundrum - not being able to detect whether ANSI sequences are supported (for, among other things, colored output and line movement)

@mataha You do realize that on WIndows 10+ conhost does support ANSI seqences? You just have to enable them.

Here is a function written in python that does just that:

import ctypes
import ctypes.wintypes
from time import sleep

# Windows API constants
INVALID_HANDLE_VALUE = -1

GENERIC_READ = 2147483648
GENERIC_WRITE = 1073741824

FILE_SHARE_READ = 1
FILE_SHARE_WRITE = 2

OPEN_EXISTING = 3

ENABLE_VT_PROCESSING = 4

# own constant for better code
FUNCTION_FAILED = 0

def enable_vt_processing():
    kernel32 = ctypes.windll.kernel32

    # get stdout handle
    conout = ctypes.wintypes.LPCWSTR("CONOUT$\0")
    stdoutH = kernel32.CreateFileW(conout, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ|FILE_SHARE_WRITE, ctypes.c_void_p(), OPEN_EXISTING, 0,ctypes.c_void_p())
    if stdoutH == INVALID_HANDLE_VALUE:
        raise ctypes.WinError(kernel32.GetLastError())

    # get current mode 
    mode = ctypes.wintypes.DWORD()
    if kernel32.GetConsoleMode(stdoutH, ctypes.byref(mode)) == FUNCTION_FAILED:
        raise ctypes.WinError(kernel32.GetLastError())

    # set mode if needed
    if mode.value & ENABLE_VT_PROCESSING == 0:
        if kernel32.SetConsoleMode(stdoutH, mode.value | ENABLE_VT_PROCESSING) == FUNCTION_FAILED:
            raise ctypes.WinError(kernel32.GetLastError())

And here are the links to the documentation for the Windows API functions used:

CreateFileW

GetConsoleMode SetConsoleMode

GetLastError

The reason I use CreateFileW on CONOUT$ instead of GetStdHandle is described here

NRJank commented 2 months ago

The GNU Octave is a cross platform project built to use the default OS terminal. Currently we would love to be able to identify if that is the new Windows Terminal. There is some incompatibility between conhost and the new Windows Terminal that is causing strange artifacts and program crashes for users of the new Windows Terminal that became very apparent when win11 made it the default. Octave is primarily developed on linux and crossbuilt for windows users, and we have few windows developers working that part of the program.

For now we wanted to create a script to try to identify if Octave was started in the Windows Terminal to prompt the user to switch, but so far have just settled on checking if the the registry key was set for having Windows Console Host be system default, and prompt the Windows users to switch the system default to that if it's not. Yes, it is a rather poor workaround. but it's effective. Until we have the developer resources to better troubleshoot the conhost / windows terminal incompatibility (or finish developing a separate, cross-platform compatible terminal widget to bundle with Octave), that change solves the problem for the users. But it does come with some false positives for the prompt because it gets set off when the windows setting is "let windows decide" even if that wouldn't use the new terminal, causing some other user confusion. So, it would at least be cleaner if at startup we could positively identify the Windows terminal.