Subclassing own CMD window

Windows specific questions.
Post Reply
UEZ
Posts: 988
Joined: May 05, 2017 19:59
Location: Germany

Subclassing own CMD window

Post by UEZ »

I am looking for a way to hook / subclasses the code compiled as CMD in Freebasic so that I can query the WM_* codes. The exe does not start in the terminal window, but in the CMD window, started via conhost.exe if you have Win11 22H2. Otherwise conhost.exe directly.

Code: Select all

#cmdline "-s console"

#Include "win\shlobj.bi"
#Include "win\tlhelp32.bi"
#Include "windows.bi"

Function _WinAPI_GetBinaryType(sFilename As String) As DWORD
	Dim As DWORD lpBinaryType
	If GetBinaryTypeW(sFilename, @lpBinaryType) = 0 Then Return -1 'https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getbinarytypew
	Return lpBinaryType
End Function

Function _WinAPI_FindFile(sPath As String, sFilename As String) As String
	Dim As WIN32_FIND_DATA FindFileData
	Dim As HANDLE hFind
	Static As String sFile, sTmp
	hFind = FindFirstFile(sPath, @FindFileData)
	If hFind = INVALID_HANDLE_VALUE Then Return ""
	While FindNextFile(hFind, @FindFileData) <> 0
		If FindFileData.cFileName <> ".." Then
			If (FindFileData.dwFileAttributes And FILE_ATTRIBUTE_DIRECTORY) = FILE_ATTRIBUTE_DIRECTORY Then Return _WinAPI_FindFile(Left(sPath, InStrRev(sPath, "\")) & FindFileData.cFileName & "\*", sFilename)
			sTmp = Rtrim(sPath, "*") & sFilename
			Dim As Dword lpBinaryType = _WinAPI_GetBinaryType(sTmp) 
			If FindFileData.cFileName = sFilename And (lpBinaryType = 6 Or lpBinaryType = 0) Then sFile = sTmp
		Endif
    Wend
	
	FindClose(hFind)
	Return sFile
End Function

Function _WinAPI_IsOSx86() As Boolean 'https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getnativesysteminfo?redirectedfrom=MSDN
	Dim As SYSTEM_INFO lpSystemInfo
	GetNativeSystemInfo(@lpSystemInfo)
	Return lpSystemInfo.wProcessorArchitecture = PROCESSOR_ARCHITECTURE_INTEL
End Function

Function _WinAPI_GetParentProcess(iPID As Integer = 0) As Integer
    Dim As DWORD pid = Iif(iPID = 0, GetCurrentProcessId(), iPID), pid_parent = 0
    Dim As HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
    Dim As PROCESSENTRY32 tPROCESSENTRY32
    tPROCESSENTRY32.dwSize = Sizeof(tPROCESSENTRY32)
    Process32First(hSnapshot, @tPROCESSENTRY32)
    While TRUE
        If tPROCESSENTRY32.th32ProcessID = pid Then
            pid_parent = tPROCESSENTRY32.th32ParentProcessID
            Exit While
        End If
        Process32Next(hSnapshot, @tPROCESSENTRY32)
    Wend
    CloseHandle(hSnapshot)
    Return pid_parent
End Function

Function _WinAPI_GetProcessName(iPid As DWORD) As String
    Dim As HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, iPid)
    If hSnapshot = 0 Then Return ""
    Dim As PROCESSENTRY32W tPROCESSENTRY32W
    tPROCESSENTRY32W.dwSize = Sizeof(PROCESSENTRY32W)
    Process32FirstW(hSnapshot, @tPROCESSENTRY32W)
    While True
        If tPROCESSENTRY32W.th32ProcessID = iPid Then Exit While
        If Process32NextW(hSnapshot,  @tPROCESSENTRY32W) = 0 Then Exit While
    Wend
    CloseHandle(hSnapshot)
    Return tPROCESSENTRY32W.szExeFile
End Function

Function _WinAPI_TerminateProcess(iPID As Integer, iExitCode As Integer = 0, bInheritHandle As Boolean = True) As Boolean
    Dim As Long dwDesiredAccess = PROCESS_TERMINATE
    Dim As Handle hProcess = OpenProcess(dwDesiredAccess, bInheritHandle, iPID)
    If hProcess = Null Then Return False
    TerminateProcess(hProcess, iExitCode)
    CloseHandle(hProcess)
    Return True
End Function

Sub GetCommandLineArguments(Byref sArguments As String)
    Dim As Ubyte i = 1
    Dim As String s
    While True
        s = Command(i)
        If Len(s) = 0 Then Exit While
        sArguments &= s & " "
        i += 1
    Wend
End Sub

Sub SetConsoleSize(cols As Long, lines As Long)
    Shell "MODE CON: COLS=" + Str(cols) + "LINES=" + Str(lines)
End Sub

    
Declare Function RtlGetVersion Lib "NtDll.dll" Alias "RtlGetVersion" (OsVersionInformation As RTL_OSVERSIONINFOW) As Long

Dim As RTL_OSVERSIONINFOW OS
OS.dwOSVersionInfoSize = Sizeof(RTL_OSVERSIONINFOW)
RtlGetVersion(OS)

Dim Shared As HWND hConsole
Dim As Handle hStdOut, hStdIn

AllocConsole()

hStdOut = GetStdHandle(STD_OUTPUT_HANDLE)
hStdIn = GetStdHandle(STD_INPUT_HANDLE)
hConsole = GetConsoleWindow()
SetConsoleSize(80, 15)

Dim As Integer iStyle = GetWindowLong(hConsole, GWL_STYLE)

Dim As Wstring * 4096 sClassname
GetClassNameW(hConsole, @sClassname, 4096)

Dim As String sArguments = " "
GetCommandLineArguments(sArguments)

'Restert exe if it is in a Terminal Window
If (SendMessageW(hConsole, WM_GETICON, Iif(OS.dwBuildNumber < 9200, 1, 0), 0) = 0 And sClassname = "PseudoConsoleWindow") And Command(1) <> "restart" Then 'restart app in CMD window
    #ifdef __FB_64BIT__
        Shell("conhost.exe """ &  Command(0) & """ restart" & sArguments)
    #Else
        If _WinAPI_IsOSx86() = False Then
            Dim As String sConhost = _WinAPI_FindFile("C:\Windows\WinSxS\amd64_microsoft-onecore-console-host-core_*", "conhost.exe")
            'If sConhost <> "" Then Shell("""" & sConhost & """" & " """ & Command(0) & """ restart" & sArguments)
            If sConhost <> "" Then 
                Shell(sConhost & " """ & Command(0) & """ restart" & sArguments)
            Else
                ? "Couldn't find conhost.exe"
                Sleep
            Endif
        Else
            Shell("conhost.exe """ &  Command(0) & """ restart" & sArguments)
        Endif
    #endif 
    FreeConsole()
    End 1000
Endif

If Command(1) = "restart" Then
    Dim As String exeName = Mid(Command(0), Instrrev(Command(0), "/") + 1, Len(Command(0)))
    Dim As Ubyte countParentPIDs = 0
    Dim As Integer iPID, parentPID = _WinAPI_GetParentProcess()
    While True
        iPID = _WinAPI_GetParentProcess(parentPID)
        If _WinAPI_GetProcessName(iPID) <> "cmd.exe" Then 
            parentPID = iPID
            countParentPIDs += 1
            If countParentPIDs > 5 Then Exit While
        Else
            _WinAPI_TerminateProcess(iPID, 0, True)
            Exit While
        Endif
    Wend
Endif

Dim Shared As Integer iOldStyle
iOldStyle = GetWindowLong(hConsole, GWL_STYLE)
SetWindowLong(hConsole, GWL_STYLE, iOldStyle And Not WS_MAXIMIZEBOX And Not WS_VSCROLL And Not WS_HSCROLL) 'Not WS_SIZEBOX And

Dim Shared As LONG_PTR g_OldWndProc

Function WindowProc(hWnd As HWND, uMsg As UINT, wParam As WPARAM, lParam As LPARAM) As LRESULT
    Select Case uMsg
        Case WM_CLOSE
            PostQuitMessage(0)
        Case WM_WINDOWPOSCHANGING
            Dim As WINDOWPOS Ptr tWinPos
            tWinPos = Cast(WINDOWPOS Ptr, lParam)
            ? tWinPos->cx, tWinPos->cy
            Return 0
    End Select
    ? hWnd, hConsole, uMsg
    Return CallWindowProc(Cast(WNDPROC, g_OldWndProc), hwnd, uMsg, wParam, lParam)
End Function

g_OldWndProc =  SetWindowLongPtr(hConsole, GWLP_WNDPROC, Cast(LONG_PTR, @WindowProc))

If g_OldWndProc = 0 Then ? "Error subclassing hConsole"

Dim msg As MSG

While GetMessage(@msg, 0, 0, 0)
    TranslateMessage(@msg)
    DispatchMessage(@msg)
Wend
SetWindowLongPtr(hCOnsole, GWLP_WNDPROC, Cast(LONG_PTR, @g_OldWndProc))
FreeConsole()
Maybe the own conhost.exe process is protected against subclassing / hooking...

Any idea?
Last edited by UEZ on Jun 22, 2023 19:17, edited 2 times in total.
deltarho[1859]
Posts: 4313
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: Subclassing own CMD window

Post by deltarho[1859] »

@UEZ

I cannot get very far because you have a missing procedure: _WinAPI_IsOSx86()
UEZ
Posts: 988
Joined: May 05, 2017 19:59
Location: Germany

Re: Subclassing own CMD window

Post by UEZ »

deltarho[1859] wrote: Jun 22, 2023 18:15 @UEZ

I cannot get very far because you have a missing procedure: _WinAPI_IsOSx86()
Thanks, grrr, I could swear that I had tested the code before -> added function to 1st post's code.

Anyhow, here using a hidden GUI but still no luck.

Code: Select all

#cmdline "-s console"

#Include "win\shlobj.bi"
#Include "win\tlhelp32.bi"
#Include "windows.bi"

Function _WinAPI_GetBinaryType(sFilename As String) As DWORD
	Dim As DWORD lpBinaryType
	If GetBinaryTypeW(sFilename, @lpBinaryType) = 0 Then Return -1 'https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getbinarytypew
	Return lpBinaryType
End Function

Function _WinAPI_FindFile(sPath As String, sFilename As String) As String
	Dim As WIN32_FIND_DATA FindFileData
	Dim As HANDLE hFind
	Static As String sFile, sTmp
	hFind = FindFirstFile(sPath, @FindFileData)
	If hFind = INVALID_HANDLE_VALUE Then Return ""
	While FindNextFile(hFind, @FindFileData) <> 0
		If FindFileData.cFileName <> ".." Then
			If (FindFileData.dwFileAttributes And FILE_ATTRIBUTE_DIRECTORY) = FILE_ATTRIBUTE_DIRECTORY Then Return _WinAPI_FindFile(Left(sPath, InStrRev(sPath, "\")) & FindFileData.cFileName & "\*", sFilename)
			sTmp = Rtrim(sPath, "*") & sFilename
			Dim As Dword lpBinaryType = _WinAPI_GetBinaryType(sTmp) 
			If FindFileData.cFileName = sFilename And (lpBinaryType = 6 Or lpBinaryType = 0) Then sFile = sTmp
		Endif
    Wend
	
	FindClose(hFind)
	Return sFile
End Function

Function _WinAPI_IsOSx86() As Boolean 'https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getnativesysteminfo?redirectedfrom=MSDN
	Dim As SYSTEM_INFO lpSystemInfo
	GetNativeSystemInfo(@lpSystemInfo)
	Return lpSystemInfo.wProcessorArchitecture = PROCESSOR_ARCHITECTURE_INTEL
End Function

Function _WinAPI_GetParentProcess(iPID As Integer = 0) As Integer
    Dim As DWORD pid = Iif(iPID = 0, GetCurrentProcessId(), iPID), pid_parent = 0
    Dim As HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
    Dim As PROCESSENTRY32 tPROCESSENTRY32
    tPROCESSENTRY32.dwSize = Sizeof(tPROCESSENTRY32)
    Process32First(hSnapshot, @tPROCESSENTRY32)
    While TRUE
        If tPROCESSENTRY32.th32ProcessID = pid Then
            pid_parent = tPROCESSENTRY32.th32ParentProcessID
            Exit While
        End If
        Process32Next(hSnapshot, @tPROCESSENTRY32)
    Wend
    CloseHandle(hSnapshot)
    Return pid_parent
End Function

Function _WinAPI_GetProcessName(iPid As DWORD) As String
    Dim As HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, iPid)
    If hSnapshot = 0 Then Return ""
    Dim As PROCESSENTRY32W tPROCESSENTRY32W
    tPROCESSENTRY32W.dwSize = Sizeof(PROCESSENTRY32W)
    Process32FirstW(hSnapshot, @tPROCESSENTRY32W)
    While True
        If tPROCESSENTRY32W.th32ProcessID = iPid Then Exit While
        If Process32NextW(hSnapshot,  @tPROCESSENTRY32W) = 0 Then Exit While
    Wend
    CloseHandle(hSnapshot)
    Return tPROCESSENTRY32W.szExeFile
End Function

Function _WinAPI_TerminateProcess(iPID As Integer, iExitCode As Integer = 0, bInheritHandle As Boolean = True) As Boolean
    Dim As Long dwDesiredAccess = PROCESS_TERMINATE
    Dim As Handle hProcess = OpenProcess(dwDesiredAccess, bInheritHandle, iPID)
    If hProcess = Null Then Return False
    TerminateProcess(hProcess, iExitCode)
    CloseHandle(hProcess)
    Return True
End Function

Sub GetCommandLineArguments(Byref sArguments As String)
    Dim As Ubyte i = 1
    Dim As String s
    While True
        s = Command(i)
        If Len(s) = 0 Then Exit While
        sArguments &= s & " "
        i += 1
    Wend
End Sub

Sub SetConsoleSize(cols As Long, lines As Long)
    Shell "MODE CON: COLS=" + Str(cols) + "LINES=" + Str(lines)
End Sub

    
Declare Function RtlGetVersion Lib "NtDll.dll" Alias "RtlGetVersion" (OsVersionInformation As RTL_OSVERSIONINFOW) As Long

Dim As RTL_OSVERSIONINFOW OS
OS.dwOSVersionInfoSize = Sizeof(RTL_OSVERSIONINFOW)
RtlGetVersion(OS)

Dim Shared As HWND hConsole
Dim As Handle hStdOut, hStdIn

AllocConsole()

hStdOut = GetStdHandle(STD_OUTPUT_HANDLE)
hStdIn = GetStdHandle(STD_INPUT_HANDLE)
hConsole = GetConsoleWindow()
SetConsoleSize(80, 15)

Dim As Integer iStyle = GetWindowLong(hConsole, GWL_STYLE)

Dim As Wstring * 4096 sClassname
GetClassNameW(hConsole, @sClassname, 4096)

Dim As String sArguments = " "
GetCommandLineArguments(sArguments)

'Restert exe if it is in a Terminal Window
If (SendMessageW(hConsole, WM_GETICON, Iif(OS.dwBuildNumber < 9200, 1, 0), 0) = 0 And sClassname = "PseudoConsoleWindow") And Command(1) <> "restart" Then 'restart app in CMD window
    #ifdef __FB_64BIT__
        Shell("conhost.exe """ &  Command(0) & """ restart" & sArguments)
    #Else
        If _WinAPI_IsOSx86() = False Then
            Dim As String sConhost = _WinAPI_FindFile("C:\Windows\WinSxS\amd64_microsoft-onecore-console-host-core_*", "conhost.exe")
            'If sConhost <> "" Then Shell("""" & sConhost & """" & " """ & Command(0) & """ restart" & sArguments)
            If sConhost <> "" Then 
                Shell(sConhost & " """ & Command(0) & """ restart" & sArguments)
            Else
                ? "Couldn't find conhost.exe"
                Sleep
            Endif
        Else
            Shell("conhost.exe """ &  Command(0) & """ restart" & sArguments)
        Endif
    #endif 
    FreeConsole()
    End 1000
Endif

If Command(1) = "restart" Then
    Dim As String exeName = Mid(Command(0), Instrrev(Command(0), "/") + 1, Len(Command(0)))
    Dim As Ubyte countParentPIDs = 0
    Dim As Integer iPID, parentPID = _WinAPI_GetParentProcess()
    While True
        iPID = _WinAPI_GetParentProcess(parentPID)
        If _WinAPI_GetProcessName(iPID) <> "cmd.exe" Then 
            parentPID = iPID
            countParentPIDs += 1
            If countParentPIDs > 5 Then Exit While
        Else
            _WinAPI_TerminateProcess(iPID, 0, True)
            Exit While
        Endif
    Wend
Endif

Dim Shared As Integer iOldStyle
iOldStyle = GetWindowLong(hConsole, GWL_STYLE)
SetWindowLong(hConsole, GWL_STYLE, iOldStyle And Not WS_MAXIMIZEBOX And Not WS_VSCROLL And Not WS_HSCROLL) 'Not WS_SIZEBOX And

Function CtrlHandler(fdwCtrlType As DWORD) As Boolean
	Select Case fdwCtrlType
		Case CTRL_C_EVENT
			? "Ctrl-C pressed and blocked!"
			Return True
		Case CTRL_CLOSE_EVENT
			? "Ctrl-Close event triggered!"
			Return True			
	End Select
	Return False
End Function

Function WindowProc(hWnd As HWND, uMsg As UINT, wParam As WPARAM, lParam As LPARAM) As LRESULT
	Select Case uMsg
		Case WM_CLOSE
			PostQuitMessage(0)
		Case WM_WINDOWPOSCHANGED
			Dim As WINDOWPOS Ptr tWinPos
			tWinPos = Cast(WINDOWPOS Ptr, lParam)
			? tWinPos->x, tWinPos->y
			Return 0
	End Select
	? "Subclassing", hWnd, hConsole, Hex(uMsg)
	Return DefWindowProc(hWnd, uMsg, wParam, lParam)
End Function	

Dim Shared As HWND hWND
Dim szAppName As ZString * 30 => "FB GUI"
Dim wc As WNDCLASSEX
Dim msg As MSG
wc.lpszClassName = @szAppName
wc.lpfnWndProc   = @WindowProc
wc.cbSize		 = SizeOf(WNDCLASSEX)
RegisterClassEx(@wc)
hWND = CreateWindowEx( 0, wc.lpszClassName, "Test", _
					   0, _
					   CW_USEDEFAULT, CW_USEDEFAULT, _
					   CW_USEDEFAULT, CW_USEDEFAULT, _
					   hConsole, NULL, NULL, NULL)

ShowWindow(hWND, SW_HIDE)

If hWND Then ? "Hidden GUI created"
If SetConsoleCtrlHandler(Cast(Any Ptr, @CtrlHandler), True) Then ? "Control handler set"

While GetMessage(@msg, 0, 0, 0)
	TranslateMessage(@msg)
	DispatchMessage(@msg)
Wend
FreeConsole()
Last edited by UEZ on Jun 22, 2023 19:18, edited 1 time in total.
deltarho[1859]
Posts: 4313
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: Subclassing own CMD window

Post by deltarho[1859] »

@UEZ

_WinAPI_FindFile hasn't been declared, either.

Put the kettle on – you are due another coffee. :)
UEZ
Posts: 988
Joined: May 05, 2017 19:59
Location: Germany

Re: Subclassing own CMD window

Post by UEZ »

deltarho[1859] wrote: Jun 22, 2023 18:57 @UEZ

_WinAPI_FindFile hasn't been declared, either.

Put the kettle on – you are due another coffee. :)
I know why it has worked because I usually compile my scripts as x64 and it works without any error message.

Anyhow, I added both missing functions to the codes above.
deltarho[1859]
Posts: 4313
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: Subclassing own CMD window

Post by deltarho[1859] »

@UEZ

OK

Presently, I cannot see why we are getting 'Error subclassing hConsole'.

However, I see that you are using an old-fashioned way of subclassing. From Windows XP we have SetWindowSubclass(). I used that for my Encrypternet application six years ago to subclass two controls.

The core statements were:

Code: Select all

Dim lResult As Boolean
lResult = SetWindowSubclass(GetDlgItem( Hwnd, IDC_Encrypt), Cast(SUBCLASSPROC, Procptr(DragDropProc)), 1, null )
lResult = SetWindowSubclass(GetDlgItem( Hwnd, IDC_Decrypt), Cast(SUBCLASSPROC, Procptr(DragDropProc)), 2, null )
During development I checked the return value and removed it when all was well.

In Case WM_DESTROY we have:

Code: Select all

lResult = RemoveWindowSubclass(GetDlgItem( Hwnd, IDC_Encrypt ), Cast(SUBCLASSPROC, Procptr(DragDropProc)), 1)
lResult = RemoveWindowSubclass(GetDlgItem( Hwnd, IDC_Decrypt ), Cast(SUBCLASSPROC, Procptr(DragDropProc)), 2)
PostQuitMessage(0)
Exit Function
I am not saying this will help, but who kmows?

The resulting code is easier to understand.
Last edited by deltarho[1859] on Jun 22, 2023 21:00, edited 1 time in total.
UEZ
Posts: 988
Joined: May 05, 2017 19:59
Location: Germany

Re: Subclassing own CMD window

Post by UEZ »

@deltarho[1859]: thanks for your reply but it doesn't change anything but you are right old-fashioned way.
deltarho[1859]
Posts: 4313
Joined: Jan 02, 2017 0:34
Location: UK
Contact:

Re: Subclassing own CMD window

Post by deltarho[1859] »

Oh, well – it was worth a try. :)
Maybe the own conhost.exe process is protected against subclassing / hooking...
Perhaps, but I don't believe that.
adeyblue
Posts: 300
Joined: Nov 07, 2019 20:08

Re: Subclassing own CMD window

Post by adeyblue »

I don't think you can subclass a cmd window. I don't think you ever could, and I'm not sure you've ever been able to subclass a window belonging to a different process, but that might not be true.

---

You can set either type of message hook (WH_GETMESSAGE or WH_CALLWNDPROC, as in SetWindowsHookEx() hook), but for that you need to know the thread id in the correct conhost that's processing the messages.

Normally to do that, you'd do:
GetWindowProcessThreadId(hWindow, @proc)
and the thread id it returns is the one to hook, except there's special code in conhost and the kernel to 'fake' who actually created the window (though it's probably for compatability rather than 'protection') so they will return values for the cmd process and thread if you run the app via cmd, or your app if you run it directly, so there's no easy way to know the thread id you need to hook for the messages.

Also there's no documented way to know which conhost goes with which console app so a global hook wouldn't be able to know which is the correct one if there are multiple console apps open, and the undocumented way to know which conhost goes with which app only works in native bitness (the following code doesn't work in a 32-bit process on 64-bit windows for example)

Code: Select all

dim conhostPid As ULONG_PTR
dim retLen as ULONG
dim queryVal as ULONG = 49 '' ProcessConsoleHostProcess
NtQueryInformationProcess(GetCurrentProcess(), queryVal, @conhostPid, sizeof(conhostPid), @retLen)
Print "Conhost pid for this app is " & Str(conhostPid) & " or " & Str( conhostPid And Not 0x3 )
Even what that returns has changed over time, from Windows 10 you need to mask off the lowest two bits to get the proper process id.

You can set a hook on all the threads in the conhost that pid refers to and one of them will be the message processing one. The hook code has to be in a dll though and it'll be injected into conhost and run there. So if you want an exe to react to any messages, you'll need to set up the inter-process communication yourself.
Of course, once injected then you're in the same process as conhost and can probably subclass the window normally (but I haven't done that).
UEZ
Posts: 988
Joined: May 05, 2017 19:59
Location: Germany

Re: Subclassing own CMD window

Post by UEZ »

adeyblue wrote: Jun 22, 2023 22:18 I don't think you can subclass a cmd window. I don't think you ever could, and I'm not sure you've ever been able to subclass a window belonging to a different process, but that might not be true.

---

You can set either type of message hook (WH_GETMESSAGE or WH_CALLWNDPROC, as in SetWindowsHookEx() hook), but for that you need to know the thread id in the correct conhost that's processing the messages.

Normally to do that, you'd do:
GetWindowProcessThreadId(hWindow, @proc)
and the thread id it returns is the one to hook, except there's special code in conhost and the kernel to 'fake' who actually created the window (though it's probably for compatability rather than 'protection') so they will return values for the cmd process and thread if you run the app via cmd, or your app if you run it directly, so there's no easy way to know the thread id you need to hook for the messages.

Also there's no documented way to know which conhost goes with which console app so a global hook wouldn't be able to know which is the correct one if there are multiple console apps open, and the undocumented way to know which conhost goes with which app only works in native bitness (the following code doesn't work in a 32-bit process on 64-bit windows for example)

Code: Select all

dim conhostPid As ULONG_PTR
dim retLen as ULONG
dim queryVal as ULONG = 49 '' ProcessConsoleHostProcess
NtQueryInformationProcess(GetCurrentProcess(), queryVal, @conhostPid, sizeof(conhostPid), @retLen)
Print "Conhost pid for this app is " & Str(conhostPid) & " or " & Str( conhostPid And Not 0x3 )
Even what that returns has changed over time, from Windows 10 you need to mask off the lowest two bits to get the proper process id.

You can set a hook on all the threads in the conhost that pid refers to and one of them will be the message processing one. The hook code has to be in a dll though and it'll be injected into conhost and run there. So if you want an exe to react to any messages, you'll need to set up the inter-process communication yourself.
Of course, once injected then you're in the same process as conhost and can probably subclass the window normally (but I haven't done that).
I'm trying the hooking but still no luck. I get a hook handle but either my code is wrong or it is not possible.

In this example is started by conhost.exe with should be the parent process. To get the pid of conhost.exe (parent) also for x86 you can use

Code: Select all

_WinAPI_GetParentProcess(GetCurrentProcessId())
adeyblue
Posts: 300
Joined: Nov 07, 2019 20:08

Re: Subclassing own CMD window

Post by adeyblue »

Oh yeah, message hooks don't work between apps of different bit-ness. They're set, so it doesn't fail, but they're never called. 64-bit one works fine

Dll

Code: Select all

#define _WIN32_WINNT &h0601
#include "windows.bi"
#include "common.bi"

Dim Shared g_mailslot As HANDLE
Dim Shared g_pPacketListHead As PSLIST_HEADER

Type MsgPacket
    as SLIST_ENTRY link
    as OVERLAPPED ol
    as HookMsgType msgBuffer
End Type

Const PACKET_SLAB_SIZE As ULong = 4096

Sub MessageWritten stdcall(ByVal errCode as ULong, ByVal bytes as ULong, ByVal pOverlapped as OVERLAPPED ptr)
    Dim as MsgPacket ptr pPacket = CONTAINING_RECORD(pOverlapped, MsgPacket, ol)
    InterlockedPushEntrySList(g_pPacketListHead, @pPacket->link)
End Sub

Function DoSetup(ByVal tid as ULong) As Long

    Dim as String mailslotName = (*g_mailslotNameStem) & Str(tid)
    g_mailslot = CreateFile( _
        StrPtr(mailslotName), _
        GENERIC_WRITE, _
        0, _
        NULL, _
        OPEN_EXISTING, _
        FILE_FLAG_OVERLAPPED, _
        NULL _
    )
    If(g_mailslot = INVALID_HANDLE_VALUE) Then
        dim as ULong errNum = GetLastError()
        dim as string errorStr = "CHook: Couldn't open mailslot " & mailslotName & ", error " & Str(errNum)
        OutputDebugString(StrPtr(errorStr))
        Return 0
    End If
    dim as string message = "Opened mailslot " & mailslotName
    OutputDebugString(StrPtr(message))
    BindIoCompletionCallback(g_mailSlot, @MessageWritten, 0)
    Dim as Any Ptr pMemory = VirtualAlloc(NULL, PACKET_SLAB_SIZE, MEM_COMMIT, PAGE_READWRITE)
    If pMemory = NULL Then
        dim as ULong errNum = GetLastError()
        dim as string errorStr = "CHook: Couldn't allocate memory, error " & Str(errNum)
        OutputDebugString(StrPtr(errorStr))
        CloseHandle(g_mailslot)
        Return 0
    End If
    
    message = "Allocated memory at " & Hex(pMemory)
    OutputDebugString(StrPtr(message))
    
    g_pPacketListHead = pMemory
    Dim as Any Ptr pMemIter = pMemory + 16
    Dim as Any Ptr pMemEnd = pMemory + PACKET_SLAB_SIZE - 1
    Dim as ULong align = SizeOf(MsgPacket) And 15
    align = IIf(align <> 0, 16 - align, 0)
    Dim as ULong packSize = SizeOf(MsgPacket) + align
    
    While pMemIter + packSize < pMemEnd
        Dim as MsgPacket ptr pPacket = pMemIter
        message = "Created pack at " & Hex(pPacket)
        OutputDebugString(StrPtr(message))
        InterlockedPushEntrySList(g_pPacketListHead, @pPacket->link)
        pMemIter += packSize
    Wend
    Return 1

End Function

Sub SendMessageToExe(ByVal pNewMsg as HookMsgType ptr)

    Dim as MsgPacket ptr pPacket = cast(MsgPacket ptr, InterlockedPopEntrySList(g_pPacketListHead))
    If(pPacket = NULL) Then
        OutputDebugString("CHook: No packets available, missing message")
        Exit Sub
    End If
    
    Dim as String message = "Got MsgPack " & Hex(pPacket) & " for message of " & SizeOf(*pNewMsg) & " bytes"
    OutputDebugString(StrPtr(message))
    
    memcpy(@pPacket->msgBuffer, pNewMsg, SizeOf(*pNewMsg))
    WriteFile(g_mailslot, @pPacket->msgBuffer, Sizeof(*pNewMsg), NULL, @pPacket->ol)

End Sub

Extern "Windows-MS"

    Function MessageHook stdcall(ByVal code as Long, ByVal firstArg as WPARAM, ByVal secondArg as LPARAM) as LRESULT export
        If code = HC_ACTION Then            
            SendMessageToExe(Cast(HookMsgType ptr, secondArg))
        End If
        Return CallNextHookEx(NULL, code, firstArg, secondArg)
    End Function

End Extern

Function IsCorrectApp() As Long

    If IsNonNativeApp() Then Return 0
    
    Dim as ZString*260 procName
    GetModuleFileName(NULL, @procName, SizeOf(procName))
    dim as String logString = "CHook: loaded in " & procName
    OutputDebugString(StrPtr(logString))
    CharLower(procName)    
    Return InStr(procName, CONHOST_EXE) <> 0
    
End Function

Sub Startup() constructor
    dim as string msg = "In Startup - " & Str(GetCurrentThreadId())
    OutputDebugString(StrPtr(msg))
    If IsCorrectApp() Then
        DoSetup(GetCurrentThreadId())
    End If
End Sub

'' This never happens
Sub Shutdown() destructor
    dim as string msg = "In Shutdown - " & Str(GetCurrentThreadId())
    If g_mailslot Then
        CancelIoEx(g_mailslot, NULL)
        CloseHandle(g_mailslot)
        g_mailslot = 0
        VirtualFree(g_pPacketListHead, 0, MEM_FREE)
    End If
End Sub
Exe

Code: Select all

#define _WIN32_WINNT &h0601
#include "windows.bi"
#include "win/tlhelp32.bi"
#include "common.bi"
#include "wm_messages.bi"

Type EnumArgs
    as HWND hCon
    as ULong found
End Type

Function WindowEnum stdcall(ByVal wnd as HWND, ByVal p as LPARAM) As Long
    dim pEnumArgs as EnumArgs ptr = cast(EnumArgs ptr, p)
    dim cont as Long = 1
    If(pEnumArgs->hCon = wnd) Then
        pEnumArgs->found = 1
        cont = 0
    End If
    Return cont
End Function

Function FindConsoleWindowTid(ByVal hCon as HWND, ByVal hSnap as HANDLE, ByVal pid as Ulong) As Ulong

    dim as ULong tid = 0
    dim as EnumArgs enumArgs = Type(hCon, 0)
    dim as THREADENTRY32 te
    te.dwSize = SizeOf(te)
    Thread32First(hSnap, @te)
    Do
        If (te.th32OwnerProcessID = pid) Then
            EnumThreadWindows(te.th32ThreadId, @WindowEnum, cast(LPARAM, @enumArgs))
            If enumArgs.found <> 0 Then 
                tid = te.th32ThreadId
                Exit Do
            End If
        End If
        te.dwSize = SizeOf(te)
    Loop While Thread32Next(hSnap, @te)
    Return tid

End Function

Function FindConhostTid() As Ulong

    dim as HWND hCon = GetConsoleWindow()
    dim as HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS Or TH32CS_SNAPTHREAD, 0)
    dim as PROCESSENTRY32 pe
    dim as Ulong conThreadId
    pe.dwSize = SizeOf(pe)
    Process32First(hSnap, @pe)
    Do
        CharLower(pe.szExeFile)
        If(InStr(pe.szExeFile, CONHOST_EXE)) Then
           Print "Conhost pid is " & Str(pe.th32ProcessId)
           conThreadId = FindConsoleWindowTid(hCon, hSnap, pe.th32ProcessId)
           If conThreadId <> 0 Then Exit Do
        End If
        pe.dwSize = SizeOf(pe)
    Loop While Process32Next(hSnap, @pe)
    CloseHandle(hSnap)
    Return conThreadId
End Function

Function HookThreadMessages(ByVal tidToHook as ULong) As HHOOK
    Dim as HMODULE chook = LoadLibrary("chook.dll")
    Dim as HOOKPROC realFnAddress = cast(HOOKPROC, GetProcAddress(chook, "MessageHook"))
    Print "Chook is at " & Hex(chook) & ", hookFn at " & Hex(realFnAddress)
    Return SetWindowsHookEx(WH_CALLWNDPROC, realFnAddress, chook, tidToHook)
End Function

Type MessagePacket
    as OVERLAPPED ol
    as Any Ptr pMsgBuffer
    as Ulong bufferSize
    as HANDLE hMailslot
End Type

Sub MessageReceived stdcall(ByVal errCode as ULong, ByVal bytes as ULong, ByVal pOverlapped as OVERLAPPED ptr)

	Const STATUS_CANCELLED As ULong = &hC0000120
	dim as MessagePacket ptr pPacket = cast(MessagePacket ptr, pOverlapped)
	'' Print Using "Messagereceived err=&, bytes=&, pOverlapped=&"; errCode; bytes; Hex(pOverlapped)
	
	If errCode = 0 Then
		dim as HookMsgType ptr pMsgs = pPacket->pMsgBuffer
		dim as Ulong numMsgs = bytes \ SizeOf(HookMsgType)
	    
		For i as ULong = 0 To numMsgs - 1
		    dim as UInt msgNum = pMsgs[i].message
		    dim as PCSTR msgName = IIf(msgNum < 1024, g_wmMessageStrings(msgNum), @"Unknown")
			Print Using "Got msg & (0x&), wParam &, lParam &"; *msgName; Hex(msgNum); Hex(pMsgs[i].wParam); Hex(pMsgs[i].lParam)
		Next
		ReadFile(pPacket->hMailslot, pPacket->pMsgBuffer, pPacket->bufferSize, NULL, @pPacket->ol)
	ElseIf(errCode <> STATUS_CANCELLED) Then
		Print "Mailslot read failed with error 0x" & Hex(errCode)
	End If    
End Sub

Sub DoTheThing()

	dim as BOOL nonNativeBitness = IsNonNativeApp()
	If nonNativeBitness Then
	    Print "Please compile as 64-bit, this doesn't work as 32-bit on the 64-bit version of Windows"
	    Exit Sub
	End If
    dim as ULong tidToHook = FindConhostTid()
    If tidToHook = 0 Then 
        Print "Couldn't find thread to hook"
        Exit Sub
    End If
    Print "Conhost tid is " & Str(tidToHook)
    dim as String mailslotName = (*g_mailslotNameStem) & Str(tidToHook)
    dim as HANDLE hMailslot = CreateMailslot(StrPtr(mailslotName), 0, MAILSLOT_WAIT_FOREVER, NULL)
    If(hMailslot = INVALID_HANDLE_VALUE) Then
        Dim as Ulong errCode = GetLastError()
        Print "Failed to create mailslot, err = " & Str(errCode)
        Exit Sub
    End If
    BindIoCompletionCallback(hMailslot, @MessageReceived, 0)
    dim as ZString*4096 buffer
    dim as MessagePacket msg
    msg.pMsgBuffer = @buffer
    msg.bufferSize = SizeOf(buffer)
    msg.hMailslot = hMailslot
    ReadFile(hMailslot, msg.pMsgBuffer, msg.bufferSize, NULL, @msg.ol)
    
    Dim as HHOOK hook = HookThreadMessages(tidToHook)
    If hook = NULL Then
        dim as ULong lastErr = GetLastError()
        Print "Couldn't hook thread " & Str(tidToHook) & ", error = " & Str(lastErr)
        CloseHandle(hMailslot)
        Exit Sub
    End If
    Print "Hook value = " & Hex(hook)
    
    Sleep
    
    Print "Closing"
    UnhookWindowsHookEx(hook)
    CancelIoEx(hMailslot, NULL)
    CloseHandle(hMailslot)

End Sub

DoTheThing()
common.bi

Code: Select all

Dim Shared as Const ZString Ptr g_mailslotNameStem = @"\\.\mailslot\conhosthook\"
Const CONHOST_EXE = "conhost.exe"
Type HookMsgType as CWPSTRUCT

Function IsNonNativeApp() as Long
    dim as BOOL isWow
    IsWow64Process(GetCurrentProcess(), @isWow)
    Return isWow
End Function

Extern "Windows-MS"
    Declare Function MessageHook stdcall(ByVal code as Long, ByVal firstArg as WPARAM, ByVal secondArg as LPARAM) as LRESULT
End Extern
wm_messages.bi is in this post
UEZ
Posts: 988
Joined: May 05, 2017 19:59
Location: Germany

Re: Subclassing own CMD window

Post by UEZ »

@adeyblue: thanks for you reply but it doesn't work for me or at least I don't know how to use it properly.

I started my test cli compiled test.exe first and after the code from you. It is listing the pid but getting em: Couldn't find thread to hook


Any instruction how to use it properly?
adeyblue
Posts: 300
Joined: Nov 07, 2019 20:08

Re: Subclassing own CMD window

Post by adeyblue »

I mean, you have to run it under conhost not Terminal
Image
UEZ
Posts: 988
Joined: May 05, 2017 19:59
Location: Germany

Re: Subclassing own CMD window

Post by UEZ »

@adeyblue: thanks. Indeed, I started it in the Terminal Window. Now it works, plenty of messages.

Using mailslots for interprocess communications between the processes is nice.

Now I need to understand how to use it for my purposes...
Post Reply