@@ -466,50 +466,81 @@ impl KernelSession {
466466 let run_in_shell = self . model . run_in_shell . unwrap_or ( false ) ;
467467
468468 // Create the command to start the kernel with the processed arguments
469- let mut command = if run_in_shell {
469+ let shell_command = if run_in_shell {
470470 #[ cfg( not( target_os = "windows" ) ) ]
471471 {
472- // On Unix systems, use a login shell if requested
473- // Get the shell from the environment, or use bash as fallback
474- let shell_path =
475- std:: env:: var ( "SHELL" ) . unwrap_or_else ( |_| String :: from ( "/bin/bash" ) ) ;
476-
477- log:: debug!(
478- "[session {}] Running kernel in login shell: {}" ,
479- self . model. session_id,
480- shell_path
481- ) ;
472+ let candidates = vec ! [
473+ std:: env:: var( "SHELL" ) . unwrap_or_else( |_| String :: from( "" ) ) ,
474+ String :: from( "/bin/bash" ) ,
475+ String :: from( "/bin/sh" ) ,
476+ ] ;
477+
478+ let mut login_shell = None ;
479+ for i in 0 ..candidates. len ( ) {
480+ let shell_path = & candidates[ i] ;
481+ // Ignore if empty (happens if SHELL is not set)
482+ if shell_path. is_empty ( ) {
483+ continue ;
484+ }
485+ // Found a valid shell path
486+ if fs:: metadata ( & shell_path) . is_ok ( ) {
487+ login_shell = Some ( shell_path) ;
488+ break ;
489+ } else if i == 0 {
490+ // The first candidate comes from $SHELL. If it doesn't exist,
491+ // log a warning but continue to try the others.
492+ log:: warn!(
493+ "[session {}] Shell path specified in $SHELL '{}' does not exist" ,
494+ self . model. session_id,
495+ shell_path
496+ ) ;
497+ }
498+ }
482499
483- // Create the original command as a string with proper shell escaping
484- let mut original_command = argv
485- . iter ( )
486- . map ( |arg| escape_for_shell ( arg) )
487- . collect :: < Vec < _ > > ( )
488- . join ( " " ) ;
489-
490- // On macOS, if the DYLD_LIBRARY_PATH environment variable was
491- // requested, set it explictly; it is not inherited by default
492- // in login shells due to SIP.
493- if let Some ( dyld_path) = resolved_env. get ( "DYLD_LIBRARY_PATH" ) {
500+ // If we found a login shell, use it to run the kernel
501+ if let Some ( login_shell) = login_shell {
494502 log:: debug!(
495- "[session {}] Explicitly forwarding DYLD_LIBRARY_PATH : {}" ,
503+ "[session {}] Running kernel in login shell : {}" ,
496504 self . model. session_id,
497- dyld_path
505+ login_shell
498506 ) ;
499- original_command = format ! (
500- "DYLD_LIBRARY_PATH={} {}" ,
501- escape_for_shell( dyld_path) ,
502- original_command
507+ // Create the original command as a string with proper shell escaping
508+ let mut original_command = argv
509+ . iter ( )
510+ . map ( |arg| escape_for_shell ( arg) )
511+ . collect :: < Vec < _ > > ( )
512+ . join ( " " ) ;
513+
514+ // On macOS, if the DYLD_LIBRARY_PATH environment variable was
515+ // requested, set it explictly; it is not inherited by default
516+ // in login shells due to SIP.
517+ if let Some ( dyld_path) = resolved_env. get ( "DYLD_LIBRARY_PATH" ) {
518+ log:: debug!(
519+ "[session {}] Explicitly forwarding DYLD_LIBRARY_PATH: {}" ,
520+ self . model. session_id,
521+ dyld_path
522+ ) ;
523+ original_command = format ! (
524+ "DYLD_LIBRARY_PATH={} {}" ,
525+ escape_for_shell( dyld_path) ,
526+ original_command
527+ ) ;
528+ }
529+
530+ // Create a command that uses the login shell. Note that we use
531+ // the short form -l rather than --login to ensure compatibility
532+ // with shells that don't support the long form, such as
533+ // sh/dash.
534+ let mut cmd = tokio:: process:: Command :: new ( login_shell) ;
535+ cmd. args ( & [ "-l" , "-c" , & original_command] ) ;
536+ Some ( cmd)
537+ } else {
538+ log:: warn!(
539+ "[session {}] No valid login shell found; running kernel without a login shell" ,
540+ self . model. session_id
503541 ) ;
542+ None
504543 }
505-
506- // Create a command that uses the login shell. Note that we use
507- // the short form -l rather than --login to ensure compatibility
508- // with shells that don't support the long form, such as
509- // sh/dash.
510- let mut cmd = tokio:: process:: Command :: new ( shell_path) ;
511- cmd. args ( & [ "-l" , "-c" , & original_command] ) ;
512- cmd
513544 }
514545
515546 #[ cfg( target_os = "windows" ) ]
@@ -519,15 +550,22 @@ impl KernelSession {
519550 "[session {}] run_in_shell parameter ignored on Windows" ,
520551 self . model. session_id
521552 ) ;
553+ None
554+ }
555+ } else {
556+ None
557+ } ;
558+
559+ // If we formed a shell command, start the kernel with the shell
560+ // command; otherwise, start it with the original command line
561+ // arguments.
562+ let mut cmd = match shell_command {
563+ Some ( command) => command,
564+ None => {
522565 let mut cmd = tokio:: process:: Command :: new ( & argv[ 0 ] ) ;
523566 cmd. args ( & argv[ 1 ..] ) ;
524567 cmd
525568 }
526- } else {
527- // Normal execution - no login shell
528- let mut cmd = tokio:: process:: Command :: new ( & argv[ 0 ] ) ;
529- cmd. args ( & argv[ 1 ..] ) ;
530- cmd
531569 } ;
532570
533571 // If a working directory was specified, test the working directory to
@@ -537,7 +575,7 @@ impl KernelSession {
537575 match fs:: metadata ( & working_directory) {
538576 Ok ( metadata) => {
539577 if metadata. is_dir ( ) {
540- command . current_dir ( & working_directory) ;
578+ cmd . current_dir ( & working_directory) ;
541579 log:: trace!(
542580 "[session {}] Using working directory '{}'" ,
543581 self . model. session_id. clone( ) ,
@@ -571,7 +609,7 @@ impl KernelSession {
571609 }
572610
573611 // Attempt to actually start the kernel process
574- let mut child = match command
612+ let mut child = match cmd
575613 . envs ( & resolved_env)
576614 . stdout ( Stdio :: piped ( ) )
577615 . stderr ( Stdio :: piped ( ) )
0 commit comments