1+ using System ;
2+ using System . Diagnostics ;
3+ using System . Runtime . InteropServices ;
4+
5+ namespace Microsoft . PowerShell
6+ {
7+ internal static class WindowsKeyboardLayoutUtil
8+ {
9+ /// <remarks>
10+ /// <para>
11+ /// This method helps to find the active keyboard layout in a terminal process that controls the current
12+ /// console application. The terminal process is not always the direct parent of the current process, but
13+ /// may be higher in the process tree in case PowerShell is a child of some other console process.
14+ /// </para>
15+ /// <para>
16+ /// Currently, we check up to 20 parent processes to see if their main window (as determined by the
17+ /// <see cref="Process.MainWindowHandle"/>) is visible.
18+ /// </para>
19+ /// <para>
20+ /// If this method returns <c>null</c>, it means it was unable to find the parent terminal process, and so
21+ /// you have to call the <see cref="GetConsoleKeyboardLayoutFallback"/>, which is known to not work properly
22+ /// in certain cases, as documented by https://github.com/PowerShell/PSReadLine/issues/1393
23+ /// </para>
24+ /// </remarks>
25+ public static IntPtr ? GetConsoleKeyboardLayout ( )
26+ {
27+ // Define a limit not get stuck in case processed form a loop (possible in case pid reuse).
28+ const int iterationLimit = 20 ;
29+
30+ var pbi = new PROCESS_BASIC_INFORMATION ( ) ;
31+ var process = Process . GetCurrentProcess ( ) ;
32+ for ( var i = 0 ; i < iterationLimit ; ++ i )
33+ {
34+ var isVisible = IsWindowVisible ( process . MainWindowHandle ) ;
35+ if ( ! isVisible )
36+ {
37+ // Main process window is invisible. This is not (likely) a terminal process.
38+ var status = NtQueryInformationProcess ( process . Handle , 0 , ref pbi , Marshal . SizeOf ( pbi ) , out var _ ) ;
39+ if ( status != 0 || pbi . InheritedFromUniqueProcessId == IntPtr . Zero )
40+ break ;
41+
42+ try
43+ {
44+ process = Process . GetProcessById ( pbi . InheritedFromUniqueProcessId . ToInt32 ( ) ) ;
45+ }
46+ catch ( Exception )
47+ {
48+ // No access to the process, or the process is already dead. Either way, we cannot determine its
49+ // keyboard layout.
50+ return null ;
51+ }
52+
53+ continue ;
54+ }
55+
56+ var tid = GetWindowThreadProcessId ( process . MainWindowHandle , out _ ) ;
57+ if ( tid == 0 ) return null ;
58+ return GetKeyboardLayout ( tid ) ;
59+ }
60+
61+ return null ;
62+ }
63+
64+ public static IntPtr GetConsoleKeyboardLayoutFallback ( )
65+ {
66+ return GetKeyboardLayout ( 0 ) ;
67+ }
68+
69+ [ DllImport ( "User32.dll" , SetLastError = true ) ]
70+ private static extern IntPtr GetKeyboardLayout ( uint idThread ) ;
71+
72+ [ DllImport ( "Ntdll.dll" ) ]
73+ static extern int NtQueryInformationProcess (
74+ IntPtr processHandle ,
75+ int processInformationClass ,
76+ ref PROCESS_BASIC_INFORMATION processInformation ,
77+ int processInformationLength ,
78+ out int returnLength ) ;
79+
80+ [ DllImport ( "user32.dll" ) ]
81+ [ return : MarshalAs ( UnmanagedType . Bool ) ]
82+ static extern bool IsWindowVisible ( IntPtr hWnd ) ;
83+
84+ [ DllImport ( "user32.dll" , SetLastError = true ) ]
85+ static extern uint GetWindowThreadProcessId ( IntPtr hwnd , out IntPtr proccess ) ;
86+
87+ [ StructLayout ( LayoutKind . Sequential ) ]
88+ private struct PROCESS_BASIC_INFORMATION
89+ {
90+ internal IntPtr Reserved1 ;
91+ internal IntPtr PebBaseAddress ;
92+ internal IntPtr Reserved2_0 ;
93+ internal IntPtr Reserved2_1 ;
94+ internal IntPtr UniqueProcessId ;
95+ internal IntPtr InheritedFromUniqueProcessId ;
96+ }
97+ }
98+ }
0 commit comments