55
66import asyncio
77import os
8+ import shutil
89import socket
910import subprocess
1011import sys
@@ -239,19 +240,121 @@ def _aks_bastion_get_current_shell_cmd():
239240
240241 ppid = os .getppid ()
241242 parent = psutil .Process (ppid )
242- return parent .name ()
243+ parent_name = parent .name ()
244+ logger .debug ("Immediate parent process: %s (PID: %s)" , parent_name , ppid )
245+
246+ # On Windows, Azure CLI is often invoked as az.cmd, which means the immediate parent
247+ # is cmd.exe but the actual user shell (PowerShell) is the grandparent process
248+ if not sys .platform .startswith ("win" ):
249+ logger .debug ("Using parent process name as shell: %s" , parent_name )
250+ return parent_name
251+
252+ return _get_windows_shell_cmd (parent , parent_name )
253+
254+
255+ def _get_windows_shell_cmd (parent , parent_name ):
256+ """Get the shell command on Windows, handling az.cmd wrapper scenarios."""
257+ try :
258+ parent_exe = parent .exe ()
259+ logger .debug ("Parent executable path: %s" , parent_exe )
260+
261+ # If the immediate parent is cmd.exe, check if it's wrapping az.cmd for PowerShell
262+ if "cmd" in parent_name .lower ():
263+ return _handle_cmd_parent (parent )
264+
265+ # For direct PowerShell processes (not wrapped by cmd)
266+ if "pwsh" in parent_name .lower () or "powershell" in parent_name .lower ():
267+ return _handle_powershell_parent (parent_exe , parent_name )
268+
269+ logger .debug ("Other Windows shell detected: %s" , parent_name )
270+ return parent_exe if parent_exe else parent_name
271+
272+ except (psutil .NoSuchProcess , psutil .AccessDenied ) as e :
273+ logger .debug ("Cannot access parent process details: %s" , e )
274+ return parent_name
275+
276+
277+ def _handle_cmd_parent (parent ):
278+ """Handle case where immediate parent is cmd.exe - check for PowerShell grandparent."""
279+ try :
280+ # Get the grandparent process (parent of cmd.exe)
281+ grandparent = parent .parent ()
282+ if not grandparent :
283+ return "cmd"
284+
285+ grandparent_name = grandparent .name ()
286+ logger .debug ("Detected grandparent process: %s (PID: %s)" , grandparent_name , grandparent .pid )
287+
288+ # If grandparent is PowerShell, that's the actual user shell
289+ if "pwsh" in grandparent_name .lower () or "powershell" in grandparent_name .lower ():
290+ return _get_powershell_executable (grandparent )
291+
292+ logger .debug ("Grandparent is not PowerShell - using cmd as target shell" )
293+ return "cmd"
294+
295+ except (psutil .NoSuchProcess , psutil .AccessDenied ) as e :
296+ # If we can't access grandparent, assume cmd is the actual shell
297+ logger .debug ("Cannot access grandparent process: %s - using cmd as target shell" , e )
298+ return "cmd"
299+
300+
301+ def _handle_powershell_parent (parent_exe , parent_name ):
302+ """Handle direct PowerShell parent process."""
303+ logger .debug ("Direct PowerShell parent detected" )
304+ return _get_powershell_executable_from_path () or parent_exe or parent_name
305+
306+
307+ def _get_powershell_executable (grandparent ):
308+ """Get PowerShell executable, preferring pwsh over powershell."""
309+ logger .debug ("Grandparent is PowerShell - using PowerShell as target shell" )
310+ powershell_cmd = _get_powershell_executable_from_path ()
311+ if powershell_cmd :
312+ return powershell_cmd
313+
314+ # If we can't find pwsh/powershell in PATH, use the detected grandparent
315+ logger .debug ("PowerShell not found in PATH, using detected grandparent executable" )
316+ return grandparent .exe () if grandparent .exe () else grandparent .name ()
317+
318+
319+ def _get_powershell_executable_from_path ():
320+ """Try to find PowerShell executable in PATH, preferring pwsh over powershell."""
321+ pwsh_path = shutil .which ("pwsh" )
322+ if pwsh_path :
323+ logger .debug ("Found pwsh at: %s" , pwsh_path )
324+ return "pwsh"
325+
326+ powershell_path = shutil .which ("powershell" )
327+ if powershell_path :
328+ logger .debug ("Found powershell at: %s" , powershell_path )
329+ return "powershell"
330+
331+ return None
243332
244333
245334def _aks_bastion_prepare_shell_cmd (kubeconfig_path ):
246335 """Prepare the shell command to launch a subshell with KUBECONFIG set."""
247336
248337 shell_cmd = _aks_bastion_get_current_shell_cmd ()
249338 updated_shell_cmd = shell_cmd
339+
340+ # Handle different shell types
250341 if shell_cmd .endswith ("bash" ) and os .path .exists (os .path .expanduser ("~/.bashrc" )):
251342 updated_shell_cmd = (
252343 f"""{ shell_cmd } -c '{ shell_cmd } --rcfile <(cat ~/.bashrc; """
253344 f"""echo "export KUBECONFIG={ kubeconfig_path } ")'"""
254345 )
346+ elif shell_cmd in ["pwsh" , "powershell" ] or "pwsh" in shell_cmd .lower () or "powershell" in shell_cmd .lower ():
347+ # PowerShell: Set environment variable and start new session
348+ # Use proper PowerShell syntax for setting environment variables
349+ escaped_path = kubeconfig_path .replace ("'" , "''" ) # Escape single quotes for PowerShell
350+ if shell_cmd == "pwsh" or "pwsh" in shell_cmd .lower ():
351+ updated_shell_cmd = f'pwsh -NoExit -Command "$env:KUBECONFIG=\' { escaped_path } \' "'
352+ else :
353+ updated_shell_cmd = f'powershell -NoExit -Command "$env:KUBECONFIG=\' { escaped_path } \' "'
354+ elif shell_cmd == "cmd" or "cmd" in shell_cmd .lower ():
355+ # CMD: Set environment variable and keep session open
356+ updated_shell_cmd = f'cmd /k "set KUBECONFIG={ kubeconfig_path } "'
357+
255358 return shell_cmd , updated_shell_cmd
256359
257360
@@ -260,6 +363,8 @@ def _aks_bastion_restore_shell(shell_cmd):
260363
261364 if shell_cmd .endswith ("bash" ):
262365 subprocess .run (["stty" , "sane" ], stdin = sys .stdin )
366+ # PowerShell and CMD on Windows typically don't need special restoration
367+ # as they handle terminal state management internally
263368
264369
265370async def _aks_bastion_launch_subshell (kubeconfig_path , port ):
0 commit comments