Skip to content

Commit 179a645

Browse files
committed
Ctrl-C only kills the process being piped if the current command created it
1 parent e680878 commit 179a645

File tree

2 files changed

+55
-35
lines changed

2 files changed

+55
-35
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
* Added ``allow_redirection``, ``terminators``, ``multiline_commands``, and ``shortcuts`` as optional arguments
2626
to ``cmd.Cmd.__init__()`
2727
* A few instance attributes were moved inside ``StatementParser`` and properties were created for accessing them
28-
* ``self.pipe_proc`` is now called ``self.pipe_proc_reader`` and is a ``ProcReader`` class.
28+
* ``self.pipe_proc`` is now called ``self.cur_pipe_proc_reader`` and is a ``ProcReader`` class.
2929
* Shell commands and commands being piped to while in a *pyscript* will function as if their output is going
3030
to a pipe and not a tty. This was necessary to be able to capture their output.
3131

cmd2/cmd2.py

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -302,11 +302,20 @@ class EmptyStatement(Exception):
302302
# Contains data about a disabled command which is used to restore its original functions when the command is enabled
303303
DisabledCommand = namedtuple('DisabledCommand', ['command_function', 'help_function'])
304304

305-
# Used to restore state after redirection ends
306-
# redirecting and piping are used to know what needs to be restored
307-
RedirectionSavedState = utils.namedtuple_with_defaults('RedirectionSavedState',
308-
['redirecting', 'self_stdout', 'sys_stdout',
309-
'piping', 'pipe_proc_reader'])
305+
306+
class RedirectionSavedState(object):
307+
# Created by each command to store information about their redirection
308+
def __init__(self):
309+
# Tells if the command is redirecting
310+
self.redirecting = False
311+
312+
# If the command created a process to pipe to, then then is its reader
313+
self.pipe_proc_reader = None
314+
315+
# Used to restore values after the command ends
316+
self.saved_self_stdout = None
317+
self.saved_sys_stdout = None
318+
self.saved_pipe_proc_reader = None
310319

311320

312321
class Cmd(cmd.Cmd):
@@ -424,8 +433,11 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, persistent
424433
# Used load command to store the current script dir as a LIFO queue to support _relative_load command
425434
self._script_dir = []
426435

427-
# Used when piping command output to a shell command
428-
self.pipe_proc_reader = None
436+
# A flag used to protect the setting up of redirection from a KeyboardInterrupt
437+
self.setting_up_redirection = False
438+
439+
# When this is not None, then it holds a ProcReader for the pipe process created by the current command
440+
self.cur_pipe_proc_reader = None
429441

430442
# Used by complete() for readline tab completion
431443
self.completion_matches = []
@@ -1654,12 +1666,13 @@ def sigint_handler(self, signum: int, frame) -> None:
16541666
:param signum: signal number
16551667
:param frame
16561668
"""
1657-
try:
1669+
# Don't do anything if we are setting up redirection
1670+
if self.setting_up_redirection:
1671+
return
1672+
1673+
if self.cur_pipe_proc_reader is not None:
16581674
# Terminate the current pipe process
1659-
self.pipe_proc_reader.terminate()
1660-
except AttributeError:
1661-
# Ignore since self.pipe_proc_reader was None
1662-
pass
1675+
self.cur_pipe_proc_reader.terminate()
16631676

16641677
# Re-raise a KeyboardInterrupt so other parts of the code can catch it
16651678
raise KeyboardInterrupt("Got a keyboard interrupt")
@@ -1722,13 +1735,18 @@ def onecmd_plus_hooks(self, line: str) -> bool:
17221735
# Keep track of whether or not we were already redirecting before this command
17231736
already_redirecting = self.redirecting
17241737

1725-
# When this isn't None, we know that _redirect_output completed. This prevents getting into
1726-
# a weird state if _redirect_output returns early because of a Ctrl-C event.
1738+
# This will be a RedirectionSavedState object for the command
17271739
saved_state = None
17281740

17291741
try:
1730-
# Handle any redirection for this command
1742+
# Prevent a Ctrl-C from messing up our state while we set up redirection
1743+
self.setting_up_redirection = True
1744+
17311745
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
17321750

17331751
# Do not continue if an error occurred while trying to redirect
17341752
if not redir_error:
@@ -1761,7 +1779,7 @@ def onecmd_plus_hooks(self, line: str) -> bool:
17611779
stop = self.postcmd(stop, statement)
17621780

17631781
if self.timing:
1764-
self.pfeedback('Elapsed: %s' % str(datetime.datetime.now() - timestart))
1782+
self.pfeedback('Elapsed: {}'.format(datetime.datetime.now() - timestart))
17651783
finally:
17661784
# Make sure _redirect_output completed
17671785
if saved_state is not None:
@@ -1904,7 +1922,12 @@ def _redirect_output(self, statement: Statement) -> Tuple[bool, RedirectionSaved
19041922
import subprocess
19051923

19061924
redir_error = False
1907-
saved_state = RedirectionSavedState(redirecting=False, piping=False)
1925+
1926+
# Initialize the saved state
1927+
saved_state = RedirectionSavedState()
1928+
saved_state.saved_self_stdout = self.stdout
1929+
saved_state.saved_sys_stdout = sys.stdout
1930+
saved_state.saved_pipe_proc_reader = self.cur_pipe_proc_reader
19081931

19091932
if not self.allow_redirection:
19101933
return redir_error, saved_state
@@ -1938,10 +1961,8 @@ def _redirect_output(self, statement: Statement) -> Tuple[bool, RedirectionSaved
19381961
creationflags=creationflags,
19391962
start_new_session=start_new_session)
19401963

1941-
saved_state = RedirectionSavedState(redirecting=True, self_stdout=self.stdout, sys_stdout=sys.stdout,
1942-
piping=True, pipe_proc_reader=self.pipe_proc_reader)
1943-
1944-
self.pipe_proc_reader = utils.ProcReader(proc, self.stdout, sys.stderr)
1964+
saved_state.redirecting = True
1965+
saved_state.pipe_proc_reader = utils.ProcReader(proc, self.stdout, sys.stderr)
19451966
sys.stdout = self.stdout = pipe_write
19461967
except Exception as ex:
19471968
self.perror('Failed to open pipe because - {}'.format(ex), traceback_war=False)
@@ -1965,17 +1986,17 @@ def _redirect_output(self, statement: Statement) -> Tuple[bool, RedirectionSaved
19651986
mode = 'a'
19661987
try:
19671988
new_stdout = open(statement.output_to, mode)
1968-
saved_state = RedirectionSavedState(redirecting=True, self_stdout=self.stdout,
1969-
sys_stdout=sys.stdout)
1989+
saved_state.redirecting = True
19701990
sys.stdout = self.stdout = new_stdout
19711991
except OSError as ex:
19721992
self.perror('Failed to redirect because - {}'.format(ex), traceback_war=False)
19731993
redir_error = True
19741994
else:
19751995
# going to a paste buffer
19761996
new_stdout = tempfile.TemporaryFile(mode="w+")
1977-
saved_state = RedirectionSavedState(redirecting=True, self_stdout=self.stdout, sys_stdout=sys.stdout)
1997+
saved_state.redirecting = True
19781998
sys.stdout = self.stdout = new_stdout
1999+
19792000
if statement.output == constants.REDIRECTION_APPEND:
19802001
self.poutput(get_paste_buffer())
19812002

@@ -1988,7 +2009,6 @@ def _restore_output(self, statement: Statement, saved_state: RedirectionSavedSta
19882009
:param statement: Statement object which contains the parsed input from the user
19892010
:param saved_state: contains information needed to restore state data
19902011
"""
1991-
# Check if self.stdout was redirected
19922012
if saved_state.redirecting:
19932013
# If we redirected output to the clipboard
19942014
if statement.output and not statement.output_to:
@@ -2000,17 +2020,17 @@ def _restore_output(self, statement: Statement, saved_state: RedirectionSavedSta
20002020
self.stdout.close()
20012021
except BrokenPipeError:
20022022
pass
2003-
finally:
2004-
self.stdout = saved_state.self_stdout
20052023

2006-
# Check if sys.stdout was redirected
2007-
if saved_state.sys_stdout is not None:
2008-
sys.stdout = saved_state.sys_stdout
2024+
# Restore the stdout values
2025+
self.stdout = saved_state.saved_self_stdout
2026+
sys.stdout = saved_state.saved_sys_stdout
2027+
2028+
# Check if we need to wait for the process being piped to
2029+
if self.cur_pipe_proc_reader is not None:
2030+
self.cur_pipe_proc_reader.wait()
20092031

2010-
# Check if output was being piped to a process
2011-
if saved_state.piping:
2012-
self.pipe_proc_reader.wait()
2013-
self.pipe_proc_reader = saved_state.pipe_proc_reader
2032+
# Restore cur_pipe_proc_reader
2033+
self.cur_pipe_proc_reader = saved_state.saved_pipe_proc_reader
20142034

20152035
def cmd_func(self, command: str) -> Optional[Callable]:
20162036
"""

0 commit comments

Comments
 (0)