@@ -433,8 +433,8 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent
433433 # Used load command to store the current script dir as a LIFO queue to support _relative_load command
434434 self ._script_dir = []
435435
436- # A flag used to protect the setting up of redirection from a KeyboardInterrupt
437- self .setting_up_redirection = False
436+ # A flag used to protect critical sections in the main thread from stopping due to a KeyboardInterrupt
437+ self .sigint_protection = utils . ContextFlag ( False )
438438
439439 # When this is not None, then it holds a ProcReader for the pipe process created by the current command
440440 self .cur_pipe_proc_reader = None
@@ -706,22 +706,14 @@ def ppaged(self, msg: str, end: str = '\n', chop: bool = False) -> None:
706706 pager = self .pager
707707 if chop :
708708 pager = self .pager_chop
709- pipe_proc = subprocess .Popen (pager , shell = True , stdin = subprocess .PIPE )
710- try :
709+
710+ # Prevent KeyboardInterrupts while in the pager. The pager application will
711+ # still receive the SIGINT since it is in the same process group as us.
712+ with self .sigint_protection :
713+ pipe_proc = subprocess .Popen (pager , shell = True , stdin = subprocess .PIPE )
711714 pipe_proc .stdin .write (msg_str .encode ('utf-8' , 'replace' ))
712715 pipe_proc .stdin .close ()
713- except (OSError , KeyboardInterrupt ):
714- pass
715-
716- # Wait in a loop until the process exits. Ignore Ctrl-C events because that doesn't
717- # mean the process is closed. For instance, less does not exit on Ctrl-C.
718- while True :
719- try :
720- pipe_proc .wait ()
721- except KeyboardInterrupt :
722- pass
723- else :
724- break
716+ pipe_proc .communicate ()
725717 else :
726718 self .decolorized_write (self .stdout , msg_str )
727719 except BrokenPipeError :
@@ -1666,16 +1658,13 @@ def sigint_handler(self, signum: int, frame) -> None:
16661658 :param signum: signal number
16671659 :param frame
16681660 """
1669- # Don't do anything if we are setting up redirection
1670- if self .setting_up_redirection :
1671- return
1672-
16731661 if self .cur_pipe_proc_reader is not None :
16741662 # Terminate the current pipe process
16751663 self .cur_pipe_proc_reader .terminate ()
16761664
1677- # Re-raise a KeyboardInterrupt so other parts of the code can catch it
1678- raise KeyboardInterrupt ("Got a keyboard interrupt" )
1665+ # Check if we are allowed to re-raise the KeyboardInterrupt
1666+ if not self .sigint_protection :
1667+ raise KeyboardInterrupt ("Got a keyboard interrupt" )
16791668
16801669 def precmd (self , statement : Statement ) -> Statement :
16811670 """Hook method executed just before the command is processed by ``onecmd()`` and after adding it to the history.
@@ -1739,14 +1728,13 @@ def onecmd_plus_hooks(self, line: str) -> bool:
17391728 saved_state = None
17401729
17411730 try :
1742- # Prevent a Ctrl-C from messing up our state while we set up redirection
1743- self .setting_up_redirection = True
1731+ with self .sigint_protection :
1732+ # Set up our redirection state variables
1733+ redir_error , saved_state = self ._redirect_output (statement )
1734+ self .cur_pipe_proc_reader = saved_state .pipe_proc_reader
17441735
1745- redir_error , saved_state = self ._redirect_output (statement )
1746- self .cur_pipe_proc_reader = saved_state .pipe_proc_reader
1747-
1748- # End Ctrl-C protection
1749- self .setting_up_redirection = False
1736+ if self ._in_py :
1737+ self ._last_result = None
17501738
17511739 # Do not continue if an error occurred while trying to redirect
17521740 if not redir_error :
@@ -1755,14 +1743,13 @@ def onecmd_plus_hooks(self, line: str) -> bool:
17551743 self .redirecting = saved_state .redirecting
17561744
17571745 timestart = datetime .datetime .now ()
1758- if self ._in_py :
1759- self ._last_result = None
17601746
17611747 # precommand hooks
17621748 data = plugin .PrecommandData (statement )
17631749 for func in self ._precmd_hooks :
17641750 data = func (data )
17651751 statement = data .statement
1752+
17661753 # call precmd() for compatibility with cmd.Cmd
17671754 statement = self .precmd (statement )
17681755
@@ -1773,20 +1760,23 @@ def onecmd_plus_hooks(self, line: str) -> bool:
17731760 data = plugin .PostcommandData (stop , statement )
17741761 for func in self ._postcmd_hooks :
17751762 data = func (data )
1763+
17761764 # retrieve the final value of stop, ignoring any statement modification from the hooks
17771765 stop = data .stop
1766+
17781767 # call postcmd() for compatibility with cmd.Cmd
17791768 stop = self .postcmd (stop , statement )
17801769
17811770 if self .timing :
17821771 self .pfeedback ('Elapsed: {}' .format (datetime .datetime .now () - timestart ))
17831772 finally :
1784- # Make sure _redirect_output completed
1785- if saved_state is not None :
1786- self ._restore_output (statement , saved_state )
1773+ # Get sigint protection while we restore stuff
1774+ with self .sigint_protection :
1775+ if saved_state is not None :
1776+ self ._restore_output (statement , saved_state )
17871777
1788- if not already_redirecting :
1789- self .redirecting = False
1778+ if not already_redirecting :
1779+ self .redirecting = False
17901780
17911781 except EmptyStatement :
17921782 # don't do anything, but do allow command finalization hooks to run
@@ -2996,14 +2986,17 @@ def do_shell(self, args: argparse.Namespace) -> None:
29962986
29972987 expanded_command = ' ' .join (tokens )
29982988
2999- # For any stream that is a StdSim, we will use a pipe so we can capture its output
3000- proc = subprocess .Popen (expanded_command ,
3001- stdout = subprocess .PIPE if isinstance (self .stdout , utils .StdSim ) else self .stdout ,
3002- stderr = subprocess .PIPE if isinstance (sys .stderr , utils .StdSim ) else sys .stderr ,
3003- shell = True )
3004-
3005- proc_reader = utils .ProcReader (proc , self .stdout , sys .stderr )
3006- proc_reader .wait ()
2989+ # Prevent KeyboardInterrupts while in the shell process. The shell process will
2990+ # still receive the SIGINT since it is in the same process group as us.
2991+ with self .sigint_protection :
2992+ # For any stream that is a StdSim, we will use a pipe so we can capture its output
2993+ proc = subprocess .Popen (expanded_command ,
2994+ stdout = subprocess .PIPE if isinstance (self .stdout , utils .StdSim ) else self .stdout ,
2995+ stderr = subprocess .PIPE if isinstance (sys .stderr , utils .StdSim ) else sys .stderr ,
2996+ shell = True )
2997+
2998+ proc_reader = utils .ProcReader (proc , self .stdout , sys .stderr )
2999+ proc_reader .wait ()
30073000
30083001 @staticmethod
30093002 def _reset_py_display () -> None :
0 commit comments