Skip to content

Commit a614316

Browse files
committed
Take the terminal back before retrying a shell command in cmd2's group
When a terminal pipeline exits before a shell command can join its group, do_shell() retries in cmd2's own group. The terminal was still lent to the dead pipeline, so the command stopped with SIGTTIN on its first terminal read and the wait for it never returned. End the lend before retrying.
1 parent 06608b0 commit a614316

2 files changed

Lines changed: 26 additions & 2 deletions

File tree

‎cmd2/cmd2.py‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5317,6 +5317,10 @@ def do_shell(self, args: argparse.Namespace) -> None:
53175317
# The pipeline exited before the command could join its group.
53185318
if kwargs.pop("process_group", None) is None:
53195319
raise
5320+
# The retry runs in our own group, so take the terminal back from the dead
5321+
# pipeline first. Its watcher left it lent, and the command would otherwise
5322+
# stop with SIGTTIN on its first terminal read, with nothing to resume it.
5323+
terminal_stack.close()
53205324

53215325
# A command that joined the pipeline's job is waited for in short polls. Only the
53225326
# main thread runs Python signal handlers, and the job-control stop the pipeline's

‎tests/test_cmd2.py‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -438,13 +438,33 @@ def test_shell_falls_back_to_own_group_when_pipeline_exited(base_app, tmp_path)
438438
# pipeline can exit between the check and the spawn, like `shell sleep 1 | true`.
439439
leader = subprocess.Popen([sys.executable, "-c", "pass"], process_group=0)
440440
leader.wait()
441-
base_app._cur_pipe_proc_reader = mock.Mock(terminal_group=leader.pid, lend_terminal=contextlib.nullcontext)
442-
with (tmp_path / "output").open("w+") as output:
441+
lent = []
442+
443+
@contextlib.contextmanager
444+
def lend_terminal():
445+
lent.append(True)
446+
try:
447+
yield
448+
finally:
449+
lent.pop()
450+
451+
# The retry runs in our own group, so the terminal has to come back from the dead
452+
# pipeline first. Otherwise the command stops with SIGTTIN on its first terminal read.
453+
spawned_while_lent = []
454+
real_popen = subprocess.Popen
455+
456+
def popen(*args, **kwargs):
457+
spawned_while_lent.append(bool(lent))
458+
return real_popen(*args, **kwargs)
459+
460+
base_app._cur_pipe_proc_reader = mock.Mock(terminal_group=leader.pid, lend_terminal=lend_terminal)
461+
with (tmp_path / "output").open("w+") as output, mock.patch("subprocess.Popen", popen):
443462
base_app.stdout = output
444463
base_app.do_shell("echo joined")
445464
output.seek(0)
446465
assert output.read() == "joined\n"
447466
assert base_app.last_result == 0
467+
assert spawned_while_lent == [True, False]
448468

449469

450470
@pytest.mark.skipif(sys.platform == "win32", reason="POSIX shell executable")

0 commit comments

Comments
 (0)