Repository: microsoft/debugpy

Issue:
I see `print` outputs twice in Debug Console when launch config uses `"console": "internalConsole"`
Issue Type: <b>Bug</b>

Launch config:
```json
{
    "name": "Python: Current File",
    "type": "python",
    "request": "launch",
    "program": "${file}",
    "console": "internalConsole",
}
```

Then debug any file.

In Debug Console, enter `print(2)`.

# Expected Behaviour

I see "2" (once)

## Expected vs. Actual Behaviour

![image](https://user-images.githubusercontent.com/12128514/151137211-c302ddb8-4887-4ee1-869c-cda2176e76f8.png)


# Diagnostic data

-   Python version (& distribution if applicable, e.g. Anaconda): 3.10.0
-   Type of virtual environment used (e.g. conda, venv, virtualenv, etc.): Global
-   Value of the `python.languageServer` setting: Default


Extension version: 2021.12.1559732655
VS Code version: Code 1.63.2 (899d46d82c4c95423fb7e10e68eba52050e30ba3, 2021-12-15T09:40:02.816Z)
OS version: Windows_NT x64 10.0.19044
Restricted Mode: No
Remote OS version: Linux x64 5.3.18-lp152.44-default


<!-- generated by issue reporter -->

Discussion:
sonarcloud[bot]: Please retry analysis of this Pull-Request directly on [SonarCloud](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=853).
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; ![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=853&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=853&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=853&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=853&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=853&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=853&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=853&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=853&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=microsoft_debugpy&pullRequest=853&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=853&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=853&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=microsoft_debugpy&pullRequest=853&resolved=false&types=CODE_SMELL)

[![100.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/100-16px.png '100.0%')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=853&metric=new_coverage&view=list) [100.0% Coverage](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=853&metric=new_coverage&view=list)  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=853&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=microsoft_debugpy&pullRequest=853&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py
index 3cd4290f..6e41ae72 100644
--- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py
+++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_comm.py
@@ -1161,7 +1161,7 @@ def internal_evaluate_expression_json(py_db, request, thread_id):
     if hasattr(fmt, 'to_dict'):
         fmt = fmt.to_dict()
 
-    if context == 'repl':
+    if context == 'repl' and not py_db.is_output_redirected:
         ctx = pydevd_io.redirect_stream_to_pydb_io_messages_context()
     else:
         ctx = NULL
diff --git a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py
index bda519b8..d1449a40 100644
--- a/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py
+++ b/src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_process_net_command_json.py
@@ -389,11 +389,15 @@ class PyDevJsonCommandProcessor(object):
         if bool(path_mappings):
             pydevd_file_utils.setup_client_server_paths(path_mappings)
 
+        redirecting = args.get("isOutputRedirected")
         if self._options.redirect_output:
             py_db.enable_output_redirection(True, True)
+            redirecting = True
         else:
             py_db.enable_output_redirection(False, False)
 
+        py_db.is_output_redirected = redirecting
+
         self.api.set_show_return_values(py_db, self._options.show_return_value)
 
         if not self._options.break_system_exit_zero:
diff --git a/src/debugpy/_vendored/pydevd/pydevd.py b/src/debugpy/_vendored/pydevd/pydevd.py
index e729239d..4b9069b2 100644
--- a/src/debugpy/_vendored/pydevd/pydevd.py
+++ b/src/debugpy/_vendored/pydevd/pydevd.py
@@ -639,6 +639,9 @@ class PyDB(object):
         self.show_return_values = False
         self.remove_return_values_flag = False
         self.redirect_output = False
+        # Note that besides the `redirect_output` flag, we also need to consider that someone
+        # else is already redirecting (i.e.: debugpy).
+        self.is_output_redirected = False
 
         # this flag disables frame evaluation even if it's available
         self.use_frame_eval = True
diff --git a/src/debugpy/adapter/clients.py b/src/debugpy/adapter/clients.py
index c1e79f63..d0b3fa14 100644
--- a/src/debugpy/adapter/clients.py
+++ b/src/debugpy/adapter/clients.py
@@ -217,6 +217,7 @@ class Client(components.Component):
 
                 arguments = request.arguments
                 if self.launcher:
+                    redirecting = arguments.get("console") == "internalConsole"
                     if "RedirectOutput" in debug_options:
                         # The launcher is doing output redirection, so we don't need the
                         # server to do it, as well.
@@ -224,10 +225,14 @@ class Client(components.Component):
                         arguments["debugOptions"] = list(
                             debug_options - {"RedirectOutput"}
                         )
+                        redirecting = True
 
                     if arguments.get("redirectOutput"):
                         arguments = dict(arguments)
                         del arguments["redirectOutput"]
+                        redirecting = True
+
+                    arguments["isOutputRedirected"] = redirecting
 
                 # pydevd doesn't send "initialized", and responds to the start request
                 # immediately, without waiting for "configurationDone". If it changes


Test patch:
diff --git a/tests/debugpy/test_multiproc.py b/tests/debugpy/test_multiproc.py
index 3836f7e4..8ca2b6db 100644
--- a/tests/debugpy/test_multiproc.py
+++ b/tests/debugpy/test_multiproc.py
@@ -135,6 +135,7 @@ def test_multiprocessing(pyfile, target, run, start_method):
 
         expected_child_config = expected_subprocess_config(parent_session)
         child_config = parent_session.wait_for_next_event("debugpyAttach")
+        child_config.pop("isOutputRedirected", None)
         assert child_config == expected_child_config
         parent_session.proceed()
 
@@ -144,6 +145,7 @@ def test_multiprocessing(pyfile, target, run, start_method):
 
             expected_grandchild_config = expected_subprocess_config(child_session)
             grandchild_config = child_session.wait_for_next_event("debugpyAttach")
+            grandchild_config.pop("isOutputRedirected", None)
             assert grandchild_config == expected_grandchild_config
 
             with debug.Session(grandchild_config) as grandchild_session:
@@ -204,6 +206,7 @@ def test_subprocess(pyfile, target, run, subProcess):
 
         expected_child_config = expected_subprocess_config(parent_session)
         child_config = parent_session.wait_for_next_event("debugpyAttach")
+        child_config.pop("isOutputRedirected", None)
         assert child_config == expected_child_config
         parent_session.proceed()
 
diff --git a/tests/debugpy/test_output.py b/tests/debugpy/test_output.py
index 43b1ea9e..3b5de697 100644
--- a/tests/debugpy/test_output.py
+++ b/tests/debugpy/test_output.py
@@ -63,6 +63,43 @@ def test_with_tab_in_output(pyfile, target, run):
     assert session.output("stdout").startswith("Hello\tWorld")
 
 
+@pytest.mark.parametrize("redirect_mode", ["internalConsole", "redirectOutput"])
+def test_redirect_output_and_eval(pyfile, target, run, redirect_mode):
+    @pyfile
+    def code_to_debug():
+        import debuggee
+        import sys
+
+        debuggee.setup()
+        sys.stdout.write("line\n")
+        ()  # @wait_for_output
+
+    with debug.Session() as session:
+        if redirect_mode == "redirectOutput":
+            session.config["redirectOutput"] = True
+        elif redirect_mode == "internalConsole":
+            session.config["console"] = "internalConsole"
+        else:
+            raise AssertionError("Unexpected: " + redirect_mode)
+
+        with run(session, target(code_to_debug)):
+            session.set_breakpoints(code_to_debug, all)
+
+        stop = session.wait_for_stop()
+        session.request(
+            "evaluate",
+            {
+                "expression": "sys.stdout.write('evaluated\\n')",
+                "frameId": stop.frame_id,
+                "context": "repl",
+            },
+        )
+
+        session.request_continue()
+
+    assert session.output("stdout") == "line\nevaluated\n"
+
+
 @pytest.mark.parametrize("run", runners.all)
 @pytest.mark.parametrize("redirect", ["enabled", "disabled"])
 def test_redirect_output(pyfile, target, run, redirect):