Skip to content

Commit 8c00d34

Browse files
authored
Merge pull request #811 from python-cmd2/win_fix
Fixed bug where pipe processes were not being stopped by Ctrl-C on Windows
2 parents 73535e1 + 1a77e7b commit 8c00d34

File tree

4 files changed

+38
-25
lines changed

4 files changed

+38
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
## 0.9.21 (TBD, 2019)
22
* Bug Fixes
3-
* Fixed bug where pipe processes were not being stopped by Ctrl-C on Linux/Mac
3+
* Fixed bug where pipe processes were not being stopped by Ctrl-C
44
* Enhancements
55
* Added `read_input()` function that is used to read from stdin. Unlike the Python built-in `input()`, it also has
66
an argument to disable tab completion while input is being entered.

cmd2/cmd2.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,9 @@
5656

5757
# Set up readline
5858
if rl_type == RlType.NONE: # pragma: no cover
59-
rl_warning = "Readline features including tab completion have been disabled since no \n" \
60-
"supported version of readline was found. To resolve this, install \n" \
61-
"pyreadline on Windows or gnureadline on Mac.\n\n"
59+
rl_warning = ("Readline features including tab completion have been disabled since no\n"
60+
"supported version of readline was found. To resolve this, install pyreadline\n"
61+
"on Windows or gnureadline on Mac.\n\n")
6262
sys.stderr.write(ansi.style_warning(rl_warning))
6363
else:
6464
from .rl_utils import rl_force_redisplay, readline
@@ -1824,24 +1824,22 @@ def _redirect_output(self, statement: Statement) -> Tuple[bool, utils.Redirectio
18241824
subproc_stdin = io.open(read_fd, 'r')
18251825
new_stdout = io.open(write_fd, 'w')
18261826

1827-
# Set options to not forward signals to the pipe process. If a Ctrl-C event occurs,
1828-
# our sigint handler will forward it only to the most recent pipe process. This makes
1829-
# sure pipe processes close in the right order (most recent first).
1827+
# Create pipe process in a separate group to isolate our signals from it. If a Ctrl-C event occurs,
1828+
# our sigint handler will forward it only to the most recent pipe process. This makes sure pipe
1829+
# processes close in the right order (most recent first).
1830+
kwargs = dict()
18301831
if sys.platform == 'win32':
1831-
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
1832-
start_new_session = False
1832+
kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
18331833
else:
1834-
creationflags = 0
1835-
start_new_session = True
1834+
kwargs['start_new_session'] = True
18361835

18371836
# For any stream that is a StdSim, we will use a pipe so we can capture its output
18381837
proc = subprocess.Popen(statement.pipe_to,
18391838
stdin=subproc_stdin,
18401839
stdout=subprocess.PIPE if isinstance(self.stdout, utils.StdSim) else self.stdout,
18411840
stderr=subprocess.PIPE if isinstance(sys.stderr, utils.StdSim) else sys.stderr,
1842-
creationflags=creationflags,
1843-
start_new_session=start_new_session,
1844-
shell=True)
1841+
shell=True,
1842+
**kwargs)
18451843

18461844
# Popen was called with shell=True so the user can chain pipe commands and redirect their output
18471845
# like: !ls -l | grep user | wc -l > out.txt. But this makes it difficult to know if the pipe process

cmd2/utils.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -517,10 +517,12 @@ def __init__(self, proc: subprocess.Popen, stdout: Union[StdSim, TextIO],
517517
self._err_thread.start()
518518

519519
def send_sigint(self) -> None:
520-
"""Send a SIGINT to the process similar to if <Ctrl>+C were pressed."""
520+
"""Send a SIGINT to the process similar to if <Ctrl>+C were pressed"""
521521
import signal
522522
if sys.platform.startswith('win'):
523-
self._proc.send_signal(signal.CTRL_C_EVENT)
523+
# cmd2 started the Windows process in a new process group. Therefore
524+
# a CTRL_C_EVENT can't be sent to it. Send a CTRL_BREAK_EVENT instead.
525+
self._proc.send_signal(signal.CTRL_BREAK_EVENT)
524526
else:
525527
# Since cmd2 uses shell=True in its Popen calls, we need to send the SIGINT to
526528
# the whole process group to make sure it propagates further than the shell

tests/test_utils.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66
import signal
77
import sys
8+
import time
89

910
import pytest
1011

@@ -228,27 +229,31 @@ def pr_none():
228229
import subprocess
229230

230231
# Start a long running process so we have time to run tests on it before it finishes
231-
# Put the new process into a separate group so signals sent to it won't interfere with this process
232+
# Put the new process into a separate group so its signal are isolated from ours
233+
kwargs = dict()
232234
if sys.platform.startswith('win'):
233235
command = 'timeout -t 5 /nobreak'
234-
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
235-
start_new_session = False
236+
kwargs['creationflags'] = subprocess.CREATE_NEW_PROCESS_GROUP
236237
else:
237238
command = 'sleep 5'
238-
creationflags = 0
239-
start_new_session = True
239+
kwargs['start_new_session'] = True
240240

241-
proc = subprocess.Popen(command,
242-
creationflags=creationflags,
243-
start_new_session=start_new_session,
244-
shell=True)
241+
proc = subprocess.Popen(command, shell=True, **kwargs)
245242
pr = cu.ProcReader(proc, None, None)
246243
return pr
247244

248245
def test_proc_reader_send_sigint(pr_none):
249246
assert pr_none._proc.poll() is None
250247
pr_none.send_sigint()
248+
249+
wait_start = time.monotonic()
251250
pr_none.wait()
251+
wait_finish = time.monotonic()
252+
253+
# Make sure the process exited before sleep of 5 seconds finished
254+
# 3 seconds accounts for some delay but is long enough for the process to exit
255+
assert wait_finish - wait_start < 3
256+
252257
ret_code = pr_none._proc.poll()
253258
if sys.platform.startswith('win'):
254259
assert ret_code is not None
@@ -258,7 +263,15 @@ def test_proc_reader_send_sigint(pr_none):
258263
def test_proc_reader_terminate(pr_none):
259264
assert pr_none._proc.poll() is None
260265
pr_none.terminate()
266+
267+
wait_start = time.monotonic()
261268
pr_none.wait()
269+
wait_finish = time.monotonic()
270+
271+
# Make sure the process exited before sleep of 5 seconds finished
272+
# 3 seconds accounts for some delay but is long enough for the process to exit
273+
assert wait_finish - wait_start < 3
274+
262275
ret_code = pr_none._proc.poll()
263276
if sys.platform.startswith('win'):
264277
assert ret_code is not None

0 commit comments

Comments
 (0)