3838from .plots import PlotsService
3939from .session_mode import SessionMode
4040from .ui import UiService
41- from .utils import BackgroundJobQueue , JsonRecord , get_qualname
41+ from .utils import BackgroundJobQueue , JsonRecord , get_qualname , with_logging
4242from .variables import VariablesService
4343
4444if TYPE_CHECKING :
4545 from ipykernel .comm .manager import CommManager
46+ from ipykernel .control import ControlThread
4647
4748
4849class _CommTarget (str , enum .Enum ):
@@ -562,6 +563,7 @@ def _showwarning(self, message, category, filename, lineno, file=None, line=None
562563
563564
564565class PositronIPKernelApp (IPKernelApp ):
566+ control_thread : ControlThread | None
565567 kernel : PositronIPyKernel
566568
567569 # Use the PositronIPyKernel class.
@@ -570,6 +572,20 @@ class PositronIPKernelApp(IPKernelApp):
570572 # Positron-specific attributes:
571573 session_mode : SessionMode = SessionMode .trait () # type: ignore
572574
575+ def init_control (self , context ):
576+ result = super ().init_control (context )
577+ # Should be defined in init_control().
578+ assert self .control_thread is not None
579+ # Add a bunch of debug logging to control thread methods.
580+ # See: https://github.com/posit-dev/positron/issues/7142.
581+ self .control_thread .io_loop .start = with_logging (self .control_thread .io_loop .start )
582+ self .control_thread .io_loop .stop = with_logging (self .control_thread .io_loop .stop )
583+ self .control_thread .io_loop .close = with_logging (self .control_thread .io_loop .close )
584+ self .control_thread .run = with_logging (self .control_thread .run )
585+ self .control_thread .stop = with_logging (self .control_thread .stop )
586+ self .control_thread .join = with_logging (self .control_thread .join )
587+ return result
588+
573589 def init_gui_pylab (self ):
574590 # Enable the Positron matplotlib backend if we're not in a notebook.
575591 # If we're in a notebook, use IPython's default backend via the super() call below.
@@ -580,6 +596,23 @@ def init_gui_pylab(self):
580596
581597 return super ().init_gui_pylab ()
582598
599+ def close (self ):
600+ # Stop the control thread if it's still alive. This is also attempted in super().close(),
601+ # but that doesn't timeout on join() so can hang forever if the control thread is stuck.
602+ # See https://github.com/posit-dev/positron/issues/7142.
603+ if self .control_thread and self .control_thread .is_alive ():
604+ self .log .debug ("Closing control thread" )
605+ self .control_thread .stop ()
606+ self .control_thread .join (timeout = 5 )
607+ # If the thread is still alive after 5 seconds, log a warning and drop the reference.
608+ # This leaves the thread dangling, but since it's a daemon thread it won't stop the
609+ # process from exiting.
610+ if self .control_thread .is_alive () and self .control_thread .daemon :
611+ self .log .warning ("Control thread did not exit after 5 seconds, dropping it" )
612+ self .control_thread = None
613+
614+ super ().close ()
615+
583616
584617#
585618# OSC8 functionality
0 commit comments