Repository: microsoft/ptvsd

Issue:
getpass function echoes password during debugging
<!-- 
Do you have a question instead of a bug report or enhancement request? Please ask it on https://stackoverflow.com/questions/tagged/visual-studio-code+python. 

Unable to install a linter or formatter? 'No installers available'? 
Windows - https://stackoverflow.com/questions/4750806/how-do-i-install-pip-on-windows
Linux - https://www.cyberciti.biz/faq/debian-ubuntu-centos-rhel-linux-install-pipclient/ , https://www.tecmint.com/install-pip-in-linux/

Python configuration issues? Please check https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites

Otherwise **please** fill in the requested details below. "XXX" markers should not be present in the final bug report.

If you think a GIF of what is happening would be helpful, consider tools like https://www.cockos.com/licecap/, https://github.com/phw/peek or https://www.screentogif.com/ .
-->

## Environment data

- VS Code version: 1.26.1
- Extension version (available under the Extensions sidebar): 2018.9.2
- OS and version: Windows 10 1803
- Python version (& distribution if applicable, e.g. Anaconda): 3.6.6 64bit
- Type of virtual environment used (N/A | venv | virtualenv | conda | ...): N/A
- Relevant/affected Python packages and their versions: 

## Actual behavior
```
PS C:\Users\pehruby\Documents\Programovani\ansible-cpoj> cd 'c:\Users\pehruby\Documents\Programovani\ansible-cpoj'; ${env:PYTHONIOENCODING}='UTF-8'; ${env:PYTHONUNBUFFERED}='1'; & 'C:\Users\pehruby\AppData\Local\Programs\Python\Python36\python.exe' 'c:\Users\pehruby\.vscode\extensions\ms-python.python-2018.9.2\pythonFiles\experimental\ptvsd_launcher.py' '65413' 'c:\Users\pehruby\Documents\Programovani\ansible-cpoj\nornir-tests\getpasswd.py'
Warning: Password input may be echoed.
Password:Secret!
Secret!
Press any key to continue . . .
PS C:\Users\pehruby\Documents\Programovani\ansible-cpoj>
```

## Expected  behavior
Extension version 2018.7.1 works as expected:
```
PS C:\Users\pehruby\Documents\Programovani\ansible-cpoj> cd 'c:\Users\pehruby\Documents\Programovani\ansible-cpoj'; ${env:PYTHONIOENCODING}='UTF-8'; ${env:PYTHONUNBUFFERED}='1'; & 'C:\Users\pehruby\AppData\Local\Programs\Python\Python36\python.exe' 'c:\Users\pehruby\.vscode\extensions\ms-python.python-2018.7.1\pythonFiles\PythonTools\visualstudio_py_launcher.py' 'c:\Users\pehruby\Documents\Programovani\ansible-cpoj' '49170' '34806ad9-833a-4524-8cd6-18ca4aa74f14' 'RedirectOutput,RedirectOutput' 'c:\Users\pehruby\Documents\Programovani\ansible-cpoj\nornir-tests\getpasswd.py'
Password:
Secret!
PS C:\Users\pehruby\Documents\Programovani\ansible-cpoj>
```


## Steps to reproduce:

Debug of the code:
```python
import getpass
pswd = getpass.getpass('Password:')
print(pswd)
```

## Logs
Output for `Python` in the `Output` panel (`View`→`Output`, change the drop-down the upper-right of the `Output` panel to `Python`)

```
N/A
```   

Output from `Console` under the `Developer Tools` panel (toggle Developer Tools on under `Help`)

```
Warning: Password input may be echoed.
Password:
Secret!
```



Discussion:
@brettcannon I understand this is for the devs of getpass to deal with?
@Talispaehn nope, it would be for ptvsd to look into. I'll move the issue to their repo.
This happens because the debugger patches `sys.stdin` and (in windows) CPython does:

```
    if sys.stdin is not sys.__stdin__:
        return fallback_getpass(prompt, stream)
```

-- this is done to give a notification that a `readline` will be done (right afterwards it calls the original `readline`) so that if a user enters something in the debug console as a response to some `input()` the debug console could redirect properly and would pass what would be an `evaluate` request as the input to `sys.stdin` (although that's currently not handled properly in `ptvsd` that's the idea...).

I think that a reasonable approach would be patching `getpass.getpass` too and having it give the input notification while restoring the original `sys.stdin` (until `getpass.getpass` returns).


As a note, this isn't a problem in Linux (where it actually uses `/dev/tty` or `sys.stdin.fileno` instead of checking if it's no longer the initial `sys.stdin`).

Resolving patch:
diff --git a/src/ptvsd/_vendored/pydevd/_pydev_bundle/pydev_console_utils.py b/src/ptvsd/_vendored/pydevd/_pydev_bundle/pydev_console_utils.py
--- a/src/ptvsd/_vendored/pydevd/_pydev_bundle/pydev_console_utils.py
+++ b/src/ptvsd/_vendored/pydevd/_pydev_bundle/pydev_console_utils.py
@@ -6,8 +6,11 @@
 from _pydev_imps._pydev_saved_modules import thread
 from _pydevd_bundle import pydevd_vars
 from _pydevd_bundle import pydevd_xml
-from _pydevd_bundle.pydevd_constants import IS_JYTHON, dict_iter_items, NEXT_VALUE_SEPARATOR, Null
+from _pydevd_bundle.pydevd_constants import (IS_JYTHON, dict_iter_items, NEXT_VALUE_SEPARATOR, Null,
+    get_global_debugger)
 import signal
+from contextlib import contextmanager
+from _pydev_bundle import pydev_log
 
 try:
     import cStringIO as StringIO  # may not always be available @UnusedImport
@@ -109,31 +112,44 @@ class DebugConsoleStdIn(BaseStdIn):
         Object to be added to stdin (to emulate it as non-blocking while the next line arrives)
     '''
 
-    def __init__(self, dbg, original_stdin):
+    def __init__(self, py_db, original_stdin):
+        '''
+        :param py_db:
+            If None, get_global_debugger() is used.
+        '''
         BaseStdIn.__init__(self, original_stdin)
-        self.debugger = dbg
+        self._py_db = py_db
+        self._in_notification = 0
 
-    def __pydev_run_command(self, is_started):
+    def __send_input_requested_message(self, is_started):
         try:
-            cmd = self.debugger.cmd_factory.make_input_requested_message(is_started)
-            self.debugger.writer.add_command(cmd)
+            py_db = self._py_db
+            if py_db is None:
+                py_db = get_global_debugger()
+            cmd = py_db.cmd_factory.make_input_requested_message(is_started)
+            py_db.writer.add_command(cmd)
         except Exception:
-            traceback.print_exc()
-            return '\n'
+            pydev_log.exception()
+
+    @contextmanager
+    def notify_input_requested(self):
+        self._in_notification += 1
+        if self._in_notification == 1:
+            self.__send_input_requested_message(True)
+        try:
+            yield
+        finally:
+            self._in_notification -= 1
+            if self._in_notification == 0:
+                self.__send_input_requested_message(False)
 
     def readline(self, *args, **kwargs):
-        # Notify Java side about input and call original function
-        self.__pydev_run_command(True)
-        result = self.original_stdin.readline(*args, **kwargs)
-        self.__pydev_run_command(False)
-        return result
+        with self.notify_input_requested():
+            return self.original_stdin.readline(*args, **kwargs)
 
     def read(self, *args, **kwargs):
-        # Notify Java side about input and call original function
-        self.__pydev_run_command(True)
-        result = self.original_stdin.read(*args, **kwargs)
-        self.__pydev_run_command(False)
-        return result
+        with self.notify_input_requested():
+            return self.original_stdin.read(*args, **kwargs)
 
 
 class CodeFragment:
@@ -220,7 +236,7 @@ def create_std_in(self, debugger=None, original_std_in=None):
         if debugger is None:
             return StdIn(self, self.host, self.client_port, original_stdin=original_std_in)
         else:
-            return DebugConsoleStdIn(dbg=debugger, original_stdin=original_std_in)
+            return DebugConsoleStdIn(py_db=debugger, original_stdin=original_std_in)
 
     def add_exec(self, code_fragment, debugger=None):
         # In case sys.excepthook called, use original excepthook #PyDev-877: Debug console freezes with Python 3.5+
diff --git a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_console.py b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_console.py
--- a/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_console.py
+++ b/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_console.py
@@ -23,6 +23,7 @@
 class ConsoleMessage:
     """Console Messages
     """
+
     def __init__(self):
         self.more = False
         # List of tuple [('error', 'error_message'), ('message_list', 'output_message')]
@@ -62,15 +63,16 @@ def to_xml(self):
 
 
 #=======================================================================================================================
-# DebugConsoleStdIn
+# _DebugConsoleStdIn
 #=======================================================================================================================
-class DebugConsoleStdIn(BaseStdIn):
+class _DebugConsoleStdIn(BaseStdIn):
 
     @overrides(BaseStdIn.readline)
     def readline(self, *args, **kwargs):
         sys.stderr.write('Warning: Reading from stdin is still not supported in this console.\n')
         return '\n'
 
+
 #=======================================================================================================================
 # DebugConsole
 #=======================================================================================================================
@@ -87,8 +89,7 @@ def create_std_in(self, *args, **kwargs):
         except:
             pass
 
-        return DebugConsoleStdIn() #If buffered, raw_input is not supported in this console.
-
+        return _DebugConsoleStdIn()  # If buffered, raw_input is not supported in this console.
 
     @overrides(InteractiveConsole.push)
     def push(self, line, frame, buffer_output=True):
@@ -121,7 +122,7 @@ def push(self, line, frame, buffer_output=True):
                 else:
                     sys.stderr.write("Internal Error: %s\n" % (exc,))
         finally:
-            #Remove frame references.
+            # Remove frame references.
             self.frame = None
             frame = None
             if buffer_output:
@@ -133,12 +134,10 @@ def push(self, line, frame, buffer_output=True):
         else:
             return more, [], []
 
-
     @overrides(BaseInterpreterInterface.do_add_exec)
     def do_add_exec(self, line):
         return InteractiveConsole.push(self, line)
 
-
     @overrides(InteractiveConsole.runcode)
     def runcode(self, code):
         """Execute a code object.
@@ -183,7 +182,7 @@ class InteractiveConsoleCache:
     interactive_console_instance = None
 
 
-#Note: On Jython 2.1 we can't use classmethod or staticmethod, so, just make the functions below free-functions.
+# Note: On Jython 2.1 we can't use classmethod or staticmethod, so, just make the functions below free-functions.
 def get_interactive_console(thread_id, frame_id, frame, console_message):
     """returns the global interactive console.
     interactive console should have been initialized by this time
@@ -198,7 +197,7 @@ def get_interactive_console(thread_id, frame_id, frame, console_message):
 
     console_stacktrace = traceback.extract_stack(frame, limit=1)
     if console_stacktrace:
-        current_context = console_stacktrace[0] # top entry from stacktrace
+        current_context = console_stacktrace[0]  # top entry from stacktrace
         context_message = 'File "%s", line %s, in %s' % (current_context[0], current_context[1], current_context[2])
         console_message.add_console_message(CONSOLE_OUTPUT, "[Current context]: %s" % (context_message,))
     return InteractiveConsoleCache.interactive_console_instance
@@ -247,7 +246,3 @@ def get_completions(frame, act_tok):
     """
     return _pydev_completer.generate_completions_as_xml(frame, act_tok)
 
-
-
-
-
diff --git a/src/ptvsd/_vendored/pydevd/pydevd.py b/src/ptvsd/_vendored/pydevd/pydevd.py
--- a/src/ptvsd/_vendored/pydevd/pydevd.py
+++ b/src/ptvsd/_vendored/pydevd/pydevd.py
@@ -15,6 +15,8 @@
 import os
 import traceback
 import weakref
+import getpass as getpass_mod
+import functools
 
 from _pydev_bundle import pydev_imports, pydev_log
 from _pydev_bundle._pydev_filesystem_encoding import getfilesystemencoding
@@ -27,6 +29,7 @@
 from _pydevd_bundle.pydevd_filtering import FilesFiltering
 from _pydevd_bundle import pydevd_io, pydevd_vm_type
 from _pydevd_bundle import pydevd_utils
+from _pydev_bundle.pydev_console_utils import DebugConsoleStdIn
 from _pydevd_bundle.pydevd_additional_thread_info import set_additional_thread_info
 from _pydevd_bundle.pydevd_breakpoints import ExceptionBreakpoint, get_exception_breakpoint
 from _pydevd_bundle.pydevd_comm_constants import (CMD_THREAD_SUSPEND, CMD_STEP_INTO, CMD_SET_BREAK,
@@ -36,7 +39,7 @@
 from _pydevd_bundle.pydevd_constants import (IS_JYTH_LESS25, get_thread_id, get_current_thread_id,
     dict_keys, dict_iter_items, DebugInfoHolder, PYTHON_SUSPEND, STATE_SUSPEND, STATE_RUN, get_frame,
     clear_cached_thread_id, INTERACTIVE_MODE_AVAILABLE, SHOW_DEBUG_INFO_ENV, IS_PY34_OR_GREATER, IS_PY2, NULL,
-    NO_FTRACE, IS_IRONPYTHON, JSON_PROTOCOL, IS_CPYTHON)
+    NO_FTRACE, IS_IRONPYTHON, JSON_PROTOCOL, IS_CPYTHON, call_only_once)
 from _pydevd_bundle.pydevd_defaults import PydevdCustomization
 from _pydevd_bundle.pydevd_custom_frames import CustomFramesContainer, custom_frames_container_init
 from _pydevd_bundle.pydevd_dont_trace_files import DONT_TRACE, PYDEV_FILE, LIB_FILE
@@ -2250,7 +2253,7 @@ def _locked_settrace(
         if bufferStdErrToServer:
             init_stderr_redirect()
 
-        patch_stdin(debugger)
+        patch_stdin()
 
         t = threadingCurrentThread()
         additional_info = set_additional_thread_info(t)
@@ -2464,12 +2467,37 @@ def apply_debugger_options(setup_options):
         enable_qt_support(setup_options['qt-support'])
 
 
-def patch_stdin(debugger):
-    from _pydev_bundle.pydev_console_utils import DebugConsoleStdIn
-    orig_stdin = sys.stdin
-    sys.stdin = DebugConsoleStdIn(debugger, orig_stdin)
+@call_only_once
+def patch_stdin():
+    _internal_patch_stdin(None, sys, getpass_mod)
 
-# Dispatch on_debugger_modules_loaded here, after all primary debugger modules are loaded
+
+def _internal_patch_stdin(py_db=None, sys=None, getpass_mod=None):
+    '''
+    Note: don't use this function directly, use `patch_stdin()` instead.
+    (this function is only meant to be used on test-cases to avoid patching the actual globals).
+    '''
+    # Patch stdin so that we notify when readline() is called.
+    original_sys_stdin = sys.stdin
+    debug_console_stdin = DebugConsoleStdIn(py_db, original_sys_stdin)
+    sys.stdin = debug_console_stdin
+
+    _original_getpass = getpass_mod.getpass
+
+    @functools.wraps(_original_getpass)
+    def getpass(*args, **kwargs):
+        with DebugConsoleStdIn.notify_input_requested(debug_console_stdin):
+            try:
+                curr_stdin = sys.stdin
+                if curr_stdin is debug_console_stdin:
+                    sys.stdin = original_sys_stdin
+                return _original_getpass(*args, **kwargs)
+            finally:
+                sys.stdin = curr_stdin
+
+    getpass_mod.getpass = getpass
+
+# Dispatch on_debugger_modules_loaded here, after all primary py_db modules are loaded
 
 
 for handler in pydevd_extension_utils.extensions_of_type(DebuggerEventHandler):
@@ -2606,7 +2634,7 @@ def main():
             pass
 
     is_module = setup['module']
-    patch_stdin(debugger)
+    patch_stdin()
 
     if setup['json-dap']:
         PyDevdAPI().set_protocol(debugger, 0, JSON_PROTOCOL)


Test patch:
diff --git a/src/ptvsd/_vendored/pydevd/tests_python/test_pydevd_io.py b/src/ptvsd/_vendored/pydevd/tests_python/test_pydevd_io.py
--- a/src/ptvsd/_vendored/pydevd/tests_python/test_pydevd_io.py
+++ b/src/ptvsd/_vendored/pydevd/tests_python/test_pydevd_io.py
@@ -52,6 +52,50 @@ def __init__(self):
         self.writer = _DummyWriter()
 
 
+def test_patch_stdin():
+    from pydevd import _internal_patch_stdin
+
+    py_db = _DummyPyDb()
+
+    class _Stub(object):
+        pass
+
+    actions = []
+
+    class OriginalStdin(object):
+
+        def readline(self):
+            # On a readline we keep the patched version.
+            assert sys_mod.stdin is not original_stdin
+            actions.append('readline')
+            return 'read'
+
+    def getpass_stub(*args, **kwargs):
+        # On getpass we need to revert to the original version.
+        actions.append('getpass')
+        assert sys_mod.stdin is original_stdin
+        return 'pass'
+
+    sys_mod = _Stub()
+    original_stdin = sys_mod.stdin = OriginalStdin()
+
+    getpass_mod = _Stub()
+    getpass_mod.getpass = getpass_stub
+
+    _internal_patch_stdin(py_db, sys_mod, getpass_mod)
+
+    assert sys_mod.stdin.readline() == 'read'
+
+    assert py_db.writer.command_meanings == ['CMD_INPUT_REQUESTED', 'CMD_INPUT_REQUESTED']
+    del py_db.writer.command_meanings[:]
+    assert actions == ['readline']
+    del actions[:]
+
+    assert getpass_mod.getpass() == 'pass'
+    assert py_db.writer.command_meanings == ['CMD_INPUT_REQUESTED', 'CMD_INPUT_REQUESTED']
+    del py_db.writer.command_meanings[:]
+
+
 def test_debug_console():
     from _pydev_bundle.pydev_console_utils import DebugConsoleStdIn
 
@@ -67,4 +111,10 @@ def readline(self):
     assert debug_console_std_in.readline() == 'read'
 
     assert py_db.writer.command_meanings == ['CMD_INPUT_REQUESTED', 'CMD_INPUT_REQUESTED']
+    del py_db.writer.command_meanings[:]
+
+    with debug_console_std_in.notify_input_requested():
+        with debug_console_std_in.notify_input_requested():
+            pass
+    assert py_db.writer.command_meanings == ['CMD_INPUT_REQUESTED', 'CMD_INPUT_REQUESTED']