Understanding Windows Console Host
If you open cmd.exe
or powershell.exe
in Windows, you will always find conhost.exe
alongside them. As a matter of fact, conhost.exe
has been around for more than a decade. Every (console based) Windows program has a "console" with them, for example when you compile and run a "command line" program, it brings up a black console window.
For conhost.exe
specifically, there's no documentation. If you need to customize console/terminal settings, like you do in Linux/MacOS, you need to right click on the title bar and choose Properties
, and the user settings are saved in registry or .lnk
file that's associated with your shell program.
If no user settings are present or requested, for example when launched directly via conhost.exe cmd.exe
, default settings are used, which can be found in registry under HKCU\Console
.
If we are going to implement a remote Windows shell and get all the features provided by Console Host, essentially we will need to match the console window that lives alongside our shell (for example cmd.exe
), meaning remotely our shell should have the same buffer/window size as the local one, and pipe conhost.exe
's IO with our network connection to get whatever displayed in the console window and put it on our remote terminal.
I suggest read Microsoft's blog post for Windows Console Host to get a detailed understanding of Console Host.
Setting Name | Type | Description |
---|---|---|
FontSize | Coordinate (REG_DWORD) |
Size of font in pixels |
FontFamily | REG_DWORD |
GDI Font family |
ScreenBufferSize | Coordinate (REG_DWORD) |
Size of the screen buffer in WxH characters |
CursorSize | REG_DWORD |
Cursor height as percentage of a single character |
WindowSize | Coordinate (REG_DWORD) |
Initial size of the window in WxH characters |
WindowPosition | Coordinate (REG_DWORD) |
Initial position of the window in WxH pixels (if not set, use auto-positioning) |
WindowAlpha | REG_DWORD |
Opacity of the window (valid range: 0x4D-0xFF ) |
ScreenColors | REG_DWORD |
Default foreground and background colors |
PopupColors | REG_DWORD |
FG and BG colors used when displaying a popup window (e.g. when F2 is pressed in CMD.exe) |
QuickEdit | REG_DWORD |
Whether QuickEdit is on by default or not |
FaceName | REG_SZ |
Name of font to use (or __DefaultTTFont__ , which defaults to whichever font is deemed most appropriate for your codepage) |
FontWeight | REG_DWORD |
GDI font weight |
InsertMode | REG_DWORD |
Whether Insert mode is on by default or not |
HistoryBufferSize | REG_DWORD |
Number of history entries to retain |
NumberOfHistoryBuffers | REG_DWORD |
Number of history buffers to retain |
HistoryNoDup | REG_DWORD |
Whether to retain duplicate history entries or not |
ColorTable%% | REG_DWORD |
For each of the 16 colors in the palette, the RGB value of the color to use |
ExtendedEditKey | REG_DWORD |
Whether to allow the use of extended edit keys or not |
WordDelimiters | REG_SZ |
A list of characters that are considered as delimiting words |
TrimLeadingZeros | REG_DWORD |
Whether to remove zeroes from the beginning of a selected string on copy (e.g. 00000001 becomes 1 ) |
EnableColorSelection | REG_DWORD |
Whether to allow selection colorization or not |
ScrollScale | REG_DWORD |
How many lines to scroll when using SHIFT / Scroll Wheel |
CodePage | REG_DWORD |
The default codepage to use |
ForceV2 | REG_DWORD |
Whether to use the improved version of the Windows Console Host |
LineSelection* | REG_DWORD |
Whether to use wrapped text selection |
FilterOnPaste* | REG_DWORD |
Whether to replace characters on paste (e.g. Word “smart quotes” are replaced with regular quotes) |
LineWrap | REG_DWORD |
Whether to have the Windows Console Host break long lines into multiple rows |
CtrlKeyShortcutsDisabled | REG_DWORD |
Disables new control key shortcuts |
AllowAltF4Close | REG_DWORD |
Allows the user to disable the Alt-F4 hotkey |
VirtualTerminalLevel | REG_DWORD |
The level of VT support provided by the Windows Console Host |
Not easy to understand at first, and Microsoft gives no further explanation.
I will tell you how this works:
Let's take FontSize
for example, if you open registry editor and get the value, it will be a REG_DWORD
or say double word integer, why FontSize
can be this huge???
Well it's actually not, considering Microsoft explains in the table:
Coordinate, Size of font in pixels
If you get a value 0x100000
, you need to split it into two since it's a "Coordinate" type (I don't know why since the lower 4 bytes are always zero), that is, you get 0x10
and 0x0
0x10
is 16
, which can be found in default console settings, font size is 16
indeed, but why in pixels?
I did some calculation with different window size and font size, and I can confirm that 16
as font size means 1 character takes 16
pixels in height, and 16/2 = 8
pixels in width, regardless of font family.
The same applies to WindowsSize
, except it's in characters.
Dynamically Ajust Window Size
If you use a remote shell on Linux, you always need to ajust your local terminal size to match the remote one, Linux provides TIOCSWINSZ
to do that.
Things on Windows will be much harder, but I can assure you it works the same way.
If you resize the console window and view it's window size change in Process Hacker, you will see ScreenBufferSize
changing accordingly.
You won't be able to use ScreenBufferSize
since the console belongs to another process, my method is to use SetWindPos
to resize the whole console window, just like you would do if it's a local console.
To use SetWindPos
, you have to know what size (in pixels) you will need, including the window title bar, scroll bar, and even drop-shadow.
Firstly, since we are launching the shell via conhost.exe cmd.exe
, it will use default settings which can be found in the registry, so we read the window size (in characters), and font size (in pixels), then we do some math and get current window size (in pixels), note that this size includes only client rectangle, see the screenshot to get a view of it.
To get the whole size of the Console window, use GetWindowRect
, then you can calculate the extra width/height that will be used later.
Remember, you are trying to resize in window so its buffer size (in characters) becomes what you need, with all the above calculations, you should be able to get the exact size in pixels, and call SetWindPos
to resize the window, so the buffer size updates as well.
Here's my approach in Go:
// SetCosoleWinsize resize main window of given console process
// w/h: width/height in characters
// window position resets to 0, 0 (pixel)
func SetCosoleWinsize(pid, w, h int) {
whandle, err := GetWindowHandleByPID(pid, true)
if err != nil {
log.Printf("SetWinsize: %v", err)
return
}
// read default font size from registry
console_reg_key, err := registry.OpenKey(registry.CURRENT_USER, "Console", registry.QUERY_VALUE)
if err != nil {
log.Printf("SetCosoleWinsize: %v", err)
return
}
defer console_reg_key.Close()
font_size_val, _, err := console_reg_key.GetIntegerValue("FontSize")
if err != nil {
log.Printf("SetConsoleWinSize: query fontsize: %v", err)
return
}
font_size := int(font_size_val >> 16) // font height in pixels, width = h/2
log.Printf("Default font size of console host is %d (0x%x), parsed from 0x%x",
font_size, font_size, font_size_val)
// what size in pixels we need
w_px := w * font_size / 2
h_px := h * font_size
if ConsoleExtraHeight == 0 && ConsoleExtraWidth == 0 {
// Get default window size
now_size, _, err := console_reg_key.GetIntegerValue("WindowSize")
if err != nil {
log.Printf("window size: %v", err)
return
}
// in chars
default_width := int(now_size & 0xffff)
default_height := int(now_size >> 16)
// in pixels
default_w_px := default_width * font_size / 2
default_h_px := default_height * font_size
log.Printf("Default window (client rectangle) is %dx%d (chars) or %dx%d (pixels)",
default_width, default_height,
default_w_px, default_h_px)
// window size in pixels, including title bar and frame
now_rect := w32.GetWindowRect(whandle)
now_w_px := int(now_rect.Width())
now_h_px := int(now_rect.Height())
if now_h_px <= 0 || now_w_px <= 0 {
log.Printf("Now window (normal rectangle) size is %dx%d, aborting", now_w_px, now_h_px)
return
}
// calculate extra width and height
ConsoleExtraHeight = now_h_px - default_h_px
ConsoleExtraWidth = now_w_px - default_w_px
if ConsoleExtraWidth <= 0 || ConsoleExtraHeight <= 0 {
log.Printf("Extra width %d, extra height %d, aborting", ConsoleExtraWidth, ConsoleExtraHeight)
return
}
}
w_px = w_px + ConsoleExtraWidth
h_px = h_px + ConsoleExtraHeight
// set window size in pixels
if w32.SetWindowPos(whandle, whandle, 0, 0, w_px, h_px, w32.SWP_NOMOVE|w32.SWP_NOZORDER) {
log.Printf("Window (0x%x) of %d has been resized to %dx%d (chars) or %dx%d (pixels)",
whandle, pid, w, h, w_px, h_px)
}
}
Comments
comments powered by Disqus