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