Skip to content

Commit b39ca95

Browse files
committed
Added protection from SIGINT when in a critical section of code
1 parent c4065e5 commit b39ca95

File tree

2 files changed

+71
-52
lines changed

2 files changed

+71
-52
lines changed

cmd2/cmd2.py

Lines changed: 37 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -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:

cmd2/utils.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -410,14 +410,20 @@ def terminate(self) -> None:
410410

411411
def wait(self) -> None:
412412
"""Wait for the process to finish"""
413-
if self._out_thread.is_alive():
414-
self._out_thread.join()
415-
if self._err_thread.is_alive():
416-
self._err_thread.join()
417-
418-
# Handle case where the process ended before the last read could be done.
419-
# This will return None for the streams that weren't pipes.
420-
out, err = self._proc.communicate()
413+
while True:
414+
try:
415+
if self._out_thread.is_alive():
416+
self._out_thread.join()
417+
if self._err_thread.is_alive():
418+
self._err_thread.join()
419+
420+
# Handle case where the process ended before the last read could be done.
421+
# This will return None for the streams that weren't pipes.
422+
out, err = self._proc.communicate()
423+
break
424+
except KeyboardInterrupt:
425+
pass
426+
421427
if out:
422428
self._write_bytes(self._stdout, out)
423429
if err:
@@ -461,3 +467,23 @@ def _write_bytes(stream: Union[StdSim, BinaryIO, TextIO], to_write: bytes) -> No
461467
except BrokenPipeError:
462468
# This occurs if output is being piped to a process that closed
463469
pass
470+
471+
472+
class ContextFlag(object):
473+
"""
474+
A flag value that can be used in a with statement.
475+
Its main use is as a flag to prevent the SIGINT handler in cmd2 from raising a KeyboardInterrupt
476+
while another code section has set the flag to True. Because signal handling is always done on the
477+
main thread, this class is not thread since there is no need.
478+
"""
479+
def __init__(self, value):
480+
self.value = value
481+
482+
def __bool__(self):
483+
return self.value
484+
485+
def __enter__(self):
486+
self.value = True
487+
488+
def __exit__(self, *args):
489+
self.value = False

0 commit comments

Comments
 (0)