[How-To] Get the hosting terminal app (PowerShell hybrid)

Discussion forum for all Windows batch related topics.

Moderator: DosItHelp

Post Reply
Message
Author
aGerman
Expert
Posts: 4654
Joined: 22 Jan 2010 18:01
Location: Germany

[How-To] Get the hosting terminal app (PowerShell hybrid)

#1 Post by aGerman » 27 Oct 2022 11:36

With the Win 11 update KB5019509 last week my expectations came true - Microsoft made Windows Terminal the default terminal app, making the good ol' console window kinda obsolete (it's at least another big step towards it).

In the past years I wrote a few PowerShell macros that are related to the console window. Some of them will never work for Windows Terminal again, and some of them need to get updated.
In the case of Windows Terminal, finding the one that is connected to our shell process is overly complicated. For that reason I decided to have this code in a separate macro. It returns the process ID of the process which owns the window. For the old console host this is the PID of our shell process, otherwise it will be the PID of the Windows Terminal. (3rd party terminals are not supported.)

I'm about to update macros that potentially work for Windows Terminal by adding an optional parameter for the process ID. I'm going to refer to this thread for people who either want to keep Windows Terminal their default, or don't know what terminal app might get connected for their published scripts in the wild.

FWIW: I also published transcriptions in C, C++, C#.Net, PowerShell, and VB.Net
https://github.com/german-one/termproc

Steffen

Code: Select all

@echo off &setlocal EnableDelayedExpansion

call :init_TermPid

%TermPid%
set "pid=%errorlevel%"
echo PID %pid%
for /f %%i in ('powershell.exe -nop -ep Bypass -c "(gps -id %pid% -ea SilentlyContinue).MainWindowHandle.ToString('X8')"') do echo HWND %%i

pause
goto :eof

::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
:init_TermPid
setlocal DisableDelayedExpansion
:: prefer PowerShell Core if installed
for %%i in ("pwsh.exe") do if "%%~$PATH:i"=="" (set "ps=powershell") else set "ps=pwsh"

:: - BRIEF -
::  Get the process ID of the terminal app which is connected to the batch process.
::   The PID is returned as errorlevel value.
::   NOTE: Only console host and Windows Terminal are supported.
:: - SYNTAX -
::  %TermPid%
:: - EXAMPLES -
::  Get the process ID of the terminal app:
::    %TermPid%
::    echo PID: %errorlevel%
set TermPid=^
%=% %ps%.exe -nop -ep Bypass -c ^"^
%===% Add-Type '^
%=====% using System;^
%=====% using System.Diagnostics;^
%=====% using System.IO;^
%=====% using System.Runtime.ConstrainedExecution;^
%=====% using System.Runtime.InteropServices;^
%=====% using System.Text;^
%=====% public static class WinTerm {^
%=======% private static class NativeMethods {^
%=========% [DllImport(\"kernel32.dll\")]^
%=========% internal static extern int CloseHandle(IntPtr Hndl);^
%=========% [DllImport(\"kernelbase.dll\")]^
%=========% internal static extern int CompareObjectHandles(IntPtr hFirst, IntPtr hSecond);^
%=========% [DllImport(\"kernel32.dll\")]^
%=========% internal static extern int DuplicateHandle(IntPtr SrcProcHndl, IntPtr SrcHndl, IntPtr TrgtProcHndl, out IntPtr TrgtHndl, int Acc, int Inherit, int Opts);^
%=========% [DllImport(\"kernel32.dll\")]^
%=========% internal static extern IntPtr GetConsoleWindow();^
%=========% [DllImport(\"kernel32.dll\")]^
%=========% internal static extern IntPtr GetCurrentProcess();^
%=========% [DllImport(\"user32.dll\")]^
%=========% internal static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint procId);^
%=========% [DllImport(\"ntdll.dll\")]^
%=========% internal static extern int NtQuerySystemInformation(int SysInfClass, IntPtr SysInf, int SysInfLen, out int RetLen);^
%=========% [DllImport(\"kernel32.dll\")]^
%=========% internal static extern IntPtr OpenProcess(int Acc, int Inherit, uint ProcId);^
%=========% [DllImport(\"kernel32.dll\", CharSet = CharSet.Unicode)]^
%=========% internal static extern int QueryFullProcessImageNameW(IntPtr Proc, int Flgs, StringBuilder Name, ref int Size);^
%=========% [DllImport(\"user32.dll\")]^
%=========% internal static extern IntPtr SendMessageW(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);^
%=======% }^
%=======% private class SafeRes : CriticalFinalizerObject, IDisposable {^
%=========% internal enum ResType { MemoryPointer, Handle }^
%=========% private IntPtr raw = IntPtr.Zero;^
%=========% private readonly ResType resourceType = ResType.MemoryPointer;^
%=========% internal IntPtr Raw { get { return raw; } }^
%=========% internal bool IsInvalid { get { return raw == IntPtr.Zero ^^^|^^^| raw == new IntPtr(-1); } }^
%=========% internal SafeRes(IntPtr raw, ResType resourceType) {^
%===========% this.raw = raw;^
%===========% this.resourceType = resourceType;^
%=========% }^
%=========% ~SafeRes() { Dispose(false); }^
%=========% public void Dispose() {^
%===========% Dispose(true);^
%===========% GC.SuppressFinalize(this);^
%=========% }^
%=========% protected virtual void Dispose(bool disposing) {^
%===========% if (IsInvalid) { return; }^
%===========% if (resourceType == ResType.MemoryPointer) {^
%=============% Marshal.FreeHGlobal(raw);^
%=============% raw = IntPtr.Zero;^
%=============% return;^
%===========% }^
%===========% if ((NativeMethods.CloseHandle(raw) == 0) == false) { raw = new IntPtr(-1); }^
%=========% }^
%=========% internal virtual void Reset(IntPtr raw) {^
%===========% Dispose();^
%===========% this.raw = raw;^
%=========% }^
%=======% }^
%=======% [StructLayout(LayoutKind.Sequential)]^
%=======% private struct SystemHandle {^
%=========% internal readonly uint ProcId;^
%=========% internal readonly byte ObjTypeId;^
%=========% internal readonly byte Flgs;^
%=========% internal readonly ushort Handle;^
%=========% internal readonly IntPtr pObj;^
%=========% internal readonly uint Acc;^
%=======% }^
%=======% private static string GetProcBaseName(SafeRes sHProc) {^
%=========% int size = 1024;^
%=========% StringBuilder nameBuf = new StringBuilder(size);^
%=========% return NativeMethods.QueryFullProcessImageNameW(sHProc.Raw, 0, nameBuf, ref size) == 0 ? \"\" : Path.GetFileNameWithoutExtension(nameBuf.ToString(0, size));^
%=======% }^
%=======% private static uint GetPidOfNamedProcWithOpenProcHandle(string searchProcName, uint findOpenProcId) {^
%=========% const int PROCESS_DUP_HANDLE = 0x0040,^
%===================% PROCESS_QUERY_LIMITED_INFORMATION = 0x1000,^
%===================% STATUS_INFO_LENGTH_MISMATCH = -1073741820,^
%===================% SystemHandleInformation = 16;^
%=========% const byte OB_TYPE_INDEX_JOB = 7;^
%=========% int status, infSize = 0x200000, len;^
%=========% using (SafeRes sPSysHndlInf = new SafeRes(Marshal.AllocHGlobal(infSize), SafeRes.ResType.MemoryPointer)) {^
%===========% while ((status = NativeMethods.NtQuerySystemInformation(SystemHandleInformation, sPSysHndlInf.Raw, infSize, out len)) == STATUS_INFO_LENGTH_MISMATCH) {^
%=============% sPSysHndlInf.Reset(Marshal.AllocHGlobal(infSize = len + 0x1000));^
%===========% }^
%===========% if (status ^^^< 0) { return 0; }^
%===========% using (SafeRes sHFindOpenProc = new SafeRes(NativeMethods.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, findOpenProcId), SafeRes.ResType.Handle)) {^
%=============% if (sHFindOpenProc.IsInvalid) { return 0; }^
%=============% uint foundPid = 0, curPid = 0;^
%=============% IntPtr hThis = NativeMethods.GetCurrentProcess();^
%=============% int sysHndlSize = Marshal.SizeOf(typeof(SystemHandle));^
%=============% using (SafeRes sHCur = new SafeRes(IntPtr.Zero, SafeRes.ResType.Handle)) {^
%===============% for (IntPtr pSysHndl = (IntPtr)((long)sPSysHndlInf.Raw + IntPtr.Size), pEnd = (IntPtr)((long)pSysHndl + Marshal.ReadInt32(sPSysHndlInf.Raw) * sysHndlSize);^
%====================% (pSysHndl == pEnd) == false;^
%====================% pSysHndl = (IntPtr)((long)pSysHndl + sysHndlSize)) {^
%=================% SystemHandle sysHndl = (SystemHandle)Marshal.PtrToStructure(pSysHndl, typeof(SystemHandle));^
%=================% if ((sysHndl.ObjTypeId == OB_TYPE_INDEX_JOB) == false) { continue; }^
%=================% if ((curPid == sysHndl.ProcId) == false) {^
%===================% curPid = sysHndl.ProcId;^
%===================% sHCur.Reset(NativeMethods.OpenProcess(PROCESS_DUP_HANDLE ^^^| PROCESS_QUERY_LIMITED_INFORMATION, 0, curPid));^
%=================% }^
%=================% IntPtr hCurOpenDup;^
%=================% if (sHCur.IsInvalid ^^^|^^^|^
%=====================% NativeMethods.DuplicateHandle(sHCur.Raw, (IntPtr)sysHndl.Handle, hThis, out hCurOpenDup, PROCESS_QUERY_LIMITED_INFORMATION, 0, 0) == 0) {^
%===================% continue;^
%=================% }^
%=================% using (SafeRes sHCurOpenDup = new SafeRes(hCurOpenDup, SafeRes.ResType.Handle)) {^
%===================% if ((NativeMethods.CompareObjectHandles(sHCurOpenDup.Raw, sHFindOpenProc.Raw) == 0) == false ^^^&^^^&^
%=======================% searchProcName == GetProcBaseName(sHCur)) {^
%=====================% foundPid = curPid;^
%=====================% break;^
%===================% }^
%=================% }^
%===============% }^
%=============% }^
%=============% return foundPid;^
%===========% }^
%=========% }^
%=======% }^
%=======% private static readonly IntPtr conWnd = NativeMethods.GetConsoleWindow();^
%=======% private static Process GetTermProc() {^
%=========% const int WM_GETICON = 0x007F;^
%=========% uint shellPid;^
%=========% if (NativeMethods.GetWindowThreadProcessId(conWnd, out shellPid) == 0) { return null; }^
%=========% if ((NativeMethods.SendMessageW(conWnd, WM_GETICON, IntPtr.Zero, IntPtr.Zero) == IntPtr.Zero) == false) {^
%===========% return Process.GetProcessById((int)shellPid);^
%=========% }^
%=========% uint termPid = GetPidOfNamedProcWithOpenProcHandle(\"WindowsTerminal\", shellPid);^
%=========% return termPid == 0 ? null : Process.GetProcessById((int)termPid);^
%=======% }^
%=======% private static readonly Process termProc = GetTermProc();^
%=======% public static Process TermProc { get { return termProc; } }^
%=====% }^
%===% ';^
%===% $termProc = [WinTerm]::TermProc;^
%===% exit $(if ($termProc) { $termProc.Id } else { 0 });^
%=% ^"

endlocal &set "TermPid=%TermPid%"
exit /b
::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
Last edited by aGerman on 29 Dec 2022 16:04, edited 3 times in total.
Reason: optimize searching from O(n²) to O(n)

Post Reply