@@ -419,7 +419,9 @@ def should_handle(self, stream, msg, idents):
419419 """
420420 return True
421421
422- async def dispatch_shell (self , msg , / , subshell_id : str | None = None ):
422+ async def dispatch_shell (
423+ self , msg , / , subshell_id : str | None = None , * , concurrent : bool = False
424+ ):
423425 """dispatch shell requests"""
424426 if len (msg ) == 1 and msg [0 ].buffer == b"stop aborting" :
425427 # Dummy "stop aborting" message to stop aborting execute requests on this subshell.
@@ -450,10 +452,12 @@ async def dispatch_shell(self, msg, /, subshell_id: str | None = None):
450452
451453 # Set the parent message for side effects.
452454 self .set_parent (idents , msg , channel = "shell" )
453- self ._publish_status ("busy" , "shell" )
455+ if not concurrent :
456+ self ._publish_status ("busy" , "shell" )
454457
455458 msg_type = msg ["header" ]["msg_type" ]
456- assert msg ["header" ].get ("subshell_id" ) == subshell_id
459+ if msg_type not in {"comm_msg" , "comm_close" }:
460+ assert msg ["header" ].get ("subshell_id" ) == subshell_id
457461
458462 if self ._supports_kernel_subshells :
459463 stream = self .shell_channel_thread .manager .get_subshell_to_shell_channel_socket (
@@ -483,7 +487,8 @@ async def dispatch_shell(self, msg, /, subshell_id: str | None = None):
483487 if inspect .isawaitable (should_handle ):
484488 should_handle = await should_handle
485489 if not should_handle :
486- self ._publish_status_and_flush ("idle" , "shell" , stream )
490+ if not concurrent :
491+ self ._publish_status_and_flush ("idle" , "shell" , stream )
487492 self .log .debug ("Not handling %s:%s" , msg_type , msg ["header" ].get ("msg_id" ))
488493 return
489494
@@ -492,10 +497,11 @@ async def dispatch_shell(self, msg, /, subshell_id: str | None = None):
492497 self .log .warning ("Unknown message type: %r" , msg_type )
493498 else :
494499 self .log .debug ("%s: %s" , msg_type , msg )
495- try :
496- self .pre_handler_hook ()
497- except Exception :
498- self .log .debug ("Unable to signal in pre_handler_hook:" , exc_info = True )
500+ if not concurrent :
501+ try :
502+ self .pre_handler_hook ()
503+ except Exception :
504+ self .log .debug ("Unable to signal in pre_handler_hook:" , exc_info = True )
499505 try :
500506 result = handler (stream , idents , msg )
501507 if inspect .isawaitable (result ):
@@ -506,16 +512,18 @@ async def dispatch_shell(self, msg, /, subshell_id: str | None = None):
506512 # Ctrl-c shouldn't crash the kernel here.
507513 self .log .error ("KeyboardInterrupt caught in kernel." )
508514 finally :
509- try :
510- self .post_handler_hook ()
511- except Exception :
512- self .log .debug ("Unable to signal in post_handler_hook:" , exc_info = True )
515+ if not concurrent :
516+ try :
517+ self .post_handler_hook ()
518+ except Exception :
519+ self .log .debug ("Unable to signal in post_handler_hook:" , exc_info = True )
513520
514521 if sys .stdout is not None :
515522 sys .stdout .flush ()
516523 if sys .stderr is not None :
517524 sys .stderr .flush ()
518- self ._publish_status_and_flush ("idle" , "shell" , stream )
525+ if not concurrent :
526+ self ._publish_status_and_flush ("idle" , "shell" , stream )
519527
520528 def pre_handler_hook (self ):
521529 """Hook to execute before calling message handler"""
@@ -600,6 +608,16 @@ async def shell_channel_thread_main(self, msg):
600608 msg3 = self .session .deserialize (msg2 , content = False , copy = False )
601609 subshell_id = msg3 ["header" ].get ("subshell_id" )
602610
611+ if msg3 ["header" ]["msg_type" ] in {"comm_msg" , "comm_close" } and hasattr (
612+ self , "comm_manager"
613+ ):
614+ content = self .session .unpack (msg3 ["content" ])
615+ comm = self .comm_manager .get_comm (content .get ("comm_id" ))
616+ if comm is not None :
617+ route = getattr (comm , "_reply_subshell_for" , None )
618+ if route is not None :
619+ subshell_id = route (content .get ("data" ), subshell_id )
620+
603621 # Find inproc pair socket to use to send message to correct subshell.
604622 subshell_manager = self .shell_channel_thread .manager
605623 try :
@@ -635,6 +653,26 @@ async def shell_main(self, subshell_id: str | None, msg):
635653 # async cells at the same time which would be a nice feature to have but is an API
636654 # change.
637655 assert asyncio_lock is not None
656+ if asyncio_lock .locked () and self .session is not None :
657+ try :
658+ _ , frames = self .session .feed_identities (msg , copy = False )
659+ header = self .session .deserialize (frames , content = False , copy = False )["header" ]
660+ except Exception :
661+ header = {}
662+ if header .get ("msg_type" ) in {"comm_open" , "comm_msg" , "comm_close" }:
663+ # A running async cell may be waiting for a widget reply on this
664+ # channel. Dispatch comms without waiting for the cell's lock.
665+ shell_parent = self .get_parent ("shell" )
666+ shell_ident = self ._get_shell_context_var (self ._shell_parent_ident )
667+ try :
668+ comm_task = asyncio .create_task (
669+ self .dispatch_shell (msg , subshell_id = subshell_id , concurrent = True ),
670+ context = copy_context (),
671+ )
672+ await comm_task
673+ finally :
674+ self .set_parent (shell_ident , shell_parent , channel = "shell" )
675+ return
638676 async with asyncio_lock :
639677 await self .dispatch_shell (msg , subshell_id = subshell_id )
640678
0 commit comments