From 3a364a43b653e798b3a0d5c360dbf7cae11cc4e1 Mon Sep 17 00:00:00 2001 From: Pavel Minaev Date: Tue, 18 Aug 2020 15:17:44 -0700 Subject: [PATCH] Fix #370: Terminal Keyboard Inputs not being accepted Make the debuggee process group the foreground group in its session. Add a test for input(), and improve existing stdin test to cover more cases. --- src/debugpy/launcher/debuggee.py | 31 +++++++++++++--- tests/debugpy/test_input.py | 61 ++++++++++++++++++++++++++++++++ tests/debugpy/test_output.py | 18 ---------- 3 files changed, 87 insertions(+), 23 deletions(-) create mode 100644 tests/debugpy/test_input.py diff --git a/src/debugpy/launcher/debuggee.py b/src/debugpy/launcher/debuggee.py index 5ca0dbae6..df291e382 100644 --- a/src/debugpy/launcher/debuggee.py +++ b/src/debugpy/launcher/debuggee.py @@ -61,9 +61,26 @@ def spawn(process_name, cmdline, env, redirect_output): kwargs = {} if sys.platform != "win32": - # Start the debuggee in a new process group, so that the launcher can kill - # the entire process tree later. - kwargs.update(preexec_fn=os.setpgrp) + + def preexec_fn(): + # Start the debuggee in a new process group, so that the launcher can + # kill the entire process tree later. + os.setpgrp() + + # Make the new process group the foreground group in its session, so + # that it can interact with the terminal. The debuggee will receive + # SIGTTOU when tcsetpgrp() is called, and must ignore it. + hdlr = signal.signal(signal.SIGTTOU, signal.SIG_IGN) + try: + tty = os.open("/dev/tty", os.O_RDWR) + try: + os.tcsetpgrp(tty, os.getpgrp()) + finally: + os.close(tty) + finally: + signal.signal(signal.SIGTTOU, hdlr) + + kwargs.update(preexec_fn=preexec_fn) try: global process @@ -94,7 +111,9 @@ def spawn(process_name, cmdline, env, redirect_output): # Setting this flag ensures that the job will be terminated by the OS once the # launcher exits, even if it doesn't terminate the job explicitly. - job_info.BasicLimitInformation.LimitFlags |= winapi.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE + job_info.BasicLimitInformation.LimitFlags |= ( + winapi.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE + ) winapi.kernel32.SetInformationJobObject( job_handle, winapi.JobObjectExtendedLimitInformation, @@ -103,7 +122,9 @@ def spawn(process_name, cmdline, env, redirect_output): ) process_handle = winapi.kernel32.OpenProcess( - winapi.PROCESS_TERMINATE | winapi.PROCESS_SET_QUOTA, False, process.pid + winapi.PROCESS_TERMINATE | winapi.PROCESS_SET_QUOTA, + False, + process.pid, ) winapi.kernel32.AssignProcessToJobObject(job_handle, process_handle) diff --git a/tests/debugpy/test_input.py b/tests/debugpy/test_input.py new file mode 100644 index 000000000..ea9853277 --- /dev/null +++ b/tests/debugpy/test_input.py @@ -0,0 +1,61 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root +# for license information. + +from __future__ import absolute_import, division, print_function, unicode_literals + +import pytest + +from tests import debug +from tests.debug import runners + + +@pytest.mark.parametrize("run", runners.all) +@pytest.mark.parametrize("redirect_output", ["", "redirect_output"]) +def test_stdin_not_patched(pyfile, target, run, redirect_output): + @pyfile + def code_to_debug(): + import sys + import debuggee + from debuggee import backchannel + + debuggee.setup() + backchannel.send(sys.stdin is sys.__stdin__) + + with debug.Session() as session: + session.config["redirectOutput"] = bool(redirect_output) + + backchannel = session.open_backchannel() + with run(session, target(code_to_debug)): + pass + + is_original_stdin = backchannel.receive() + assert is_original_stdin, "Expected sys.stdin and sys.__stdin__ to be the same." + + +@pytest.mark.parametrize("run", runners.all_launch_terminal + runners.all_attach) +@pytest.mark.parametrize("redirect_output", ["", "redirect_output"]) +def test_input(pyfile, target, run, redirect_output): + @pyfile + def code_to_debug(): + import debuggee + from debuggee import backchannel + + try: + input = raw_input + except NameError: + pass + + debuggee.setup() + backchannel.send(input()) + + with debug.Session() as session: + session.config["redirectOutput"] = bool(redirect_output) + + backchannel = session.open_backchannel() + with run(session, target(code_to_debug)): + pass + + session.debuggee.stdin.write(b"ok\n") + s = backchannel.receive() + assert s == "ok" diff --git a/tests/debugpy/test_output.py b/tests/debugpy/test_output.py index c208725df..43b1ea9e4 100644 --- a/tests/debugpy/test_output.py +++ b/tests/debugpy/test_output.py @@ -122,24 +122,6 @@ def code_to_debug(): ) -def test_stdin_not_patched(pyfile, target, run): - @pyfile - def code_to_debug(): - import sys - import debuggee - from debuggee import backchannel - - debuggee.setup() - backchannel.send(sys.stdin == sys.__stdin__) - - with debug.Session() as session: - backchannel = session.open_backchannel() - with run(session, target(code_to_debug)): - pass - is_original_stdin = backchannel.receive() - assert is_original_stdin, 'Expected sys.stdin and sys.__stdin__ to be the same.' - - if sys.platform == "win32": @pytest.mark.parametrize("redirect_output", ["", "redirect_output"])