Repository: ansible/ansible

Issue:
Pause prompt broken in python3
##### ISSUE TYPE
 - Bug Report

##### COMPONENT NAME
pause

##### ANSIBLE VERSION

```
ansible 2.3.2.0 (detached HEAD 7424e1c417) last updated 2017/06/30 11:12:33 (GMT +200)
  config file = 
  configured module search path = Default w/o overrides
  python version = 3.6.1 (default, Apr  4 2017, 09:40:21) [GCC 4.2.1 Compatible Apple LLVM 8.1.0 (clang-802.0.38)]

```

##### CONFIGURATION
Default

##### OS / ENVIRONMENT
MacOS

##### SUMMARY
Using the `prompt` functionality of the `pause` module, the interactive input does not stop anymore when pressing Enter. If the input is interrupted using Ctrl+C and the process is continued by pressing "C", the returned `user_input` also contains the newline characters.

##### STEPS TO REPRODUCE

```yaml
---

- name: Input
  pause:
    prompt: "Username?"
  register: r_prompt

- debug:
    msg: "{{r_prompt.user_input}}"
```

- Run this tasks and enter `123<ENTER>456`
- Because process does not continue, press CTRL+C and then C

##### EXPECTED RESULTS
 - expect process to continue when pressing <ENTER>
 - expect `user_input` to be `123`

##### ACTUAL RESULTS

```
TASK [basis : Input] ************************************************************************************************************************************************************************************************************************************************
task path: /Users/phsc/Projects/sandbox/test/roles/basis/tasks/test.yml:3
[basis : Input]
Username?:
Press 'C' to continue the play or 'A' to abort 
ok: [localhost] => {
    "changed": false,
    "delta": 5,
    "rc": 0,
    "start": "2017-06-30 11:21:19.867562",
    "stderr": "",
    "stdout": "Paused for 0.1 minutes",
    "stop": "2017-06-30 11:21:25.623051",
    "user_input": "123\n456"
}

TASK [basis : debug] ************************************************************************************************************************************************************************************************************************************************
task path: /Users/phsc/Projects/sandbox/test/roles/basis/tasks/test.yml:8
ok: [localhost] => {
    "msg": "123\n456"
}
META: ran handlers
META: ran handlers

```

allow input plugins to be py3 compat
##### SUMMARY
<!--- Describe the change, including rationale and design decisions -->
fixes #26278


##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bugfix Pull Request


##### COMPONENT NAME
<!--- Name of the module/plugin/module/task -->
python3

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
2.4
```




Discussion:
cc @tbielawa
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
The cause of the error is that in Python3 `fdopen` is now a wrapper of the built-in `open` function which has a feature called "universal newlines" ([see docs](https://docs.python.org/3.6/library/functions.html#open)) This seems to prevent the stdin process to exit.

Changing `/lib/ansible/plugins/action/pause.py` line 152 to check for `\n` and `\r` does not solve the problem. Then still two press of ENTER are needed.

Changing `/lib/ansible/executor/process/worker.py` line 80 by adding the newline attribute like this fixes the problem:
```
self._new_stdin = os.fdopen(os.dup(fileno), newline='\r\n')
```

However this would break Python2 compatibility. Any suggestions? 
The test `ansible-test sanity --test pep8` failed with the following error:
```
lib/ansible/executor/process/worker.py:82:32: E261 at least two spaces before inline comment
```
<!-- job_id: 595d29b2812bb30700d0904c -->
<!-- job_id: 595d29b33fe72e09005ec9c3 -->
<!-- job_id: /testresults/ansible-test-sanity-pep8.json -->

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: shippable_test_result --->

Resolving patch:
diff --git a/lib/ansible/plugins/action/pause.py b/lib/ansible/plugins/action/pause.py
--- a/lib/ansible/plugins/action/pause.py
+++ b/lib/ansible/plugins/action/pause.py
@@ -25,6 +25,8 @@
 
 from os import isatty
 from ansible.errors import AnsibleError
+from ansible.module_utils.six import PY3
+from ansible.module_utils._text import to_text
 from ansible.plugins.action import ActionBase
 
 try:
@@ -85,7 +87,7 @@ def run(self, tmp=None, task_vars=None):
 
             except ValueError as e:
                 result['failed'] = True
-                result['msg'] = "non-integer value given for prompt duration:\n%s" % str(e)
+                result['msg'] = u"non-integer value given for prompt duration:\n%s" % to_text(e)
                 return result
 
         # Is 'prompt' a key in 'args'?
@@ -102,8 +104,8 @@ def run(self, tmp=None, task_vars=None):
         # Begin the hard work!
 
         start = time.time()
-        result['start'] = str(datetime.datetime.now())
-        result['user_input'] = ''
+        result['start'] = to_text(datetime.datetime.now())
+        result['user_input'] = b''
 
         fd = None
         old_settings = None
@@ -122,9 +124,13 @@ def run(self, tmp=None, task_vars=None):
 
             # save the attributes on the existing (duped) stdin so
             # that we can restore them later after we set raw mode
+            if PY3:
+                stdin = self._connection._new_stdin.buffer
+            else:
+                stdin = self._connection._new_stdin
             fd = None
             try:
-                fd = self._connection._new_stdin.fileno()
+                fd = stdin.fileno()
             except (ValueError, AttributeError):
                 # ValueError: someone is using a closed file descriptor as stdin
                 # AttributeError: someone is using a null file descriptor as stdin on windoez
@@ -136,12 +142,12 @@ def run(self, tmp=None, task_vars=None):
 
                     # flush the buffer to make sure no previous key presses
                     # are read in below
-                    termios.tcflush(self._connection._new_stdin, termios.TCIFLUSH)
+                    termios.tcflush(stdin, termios.TCIFLUSH)
             while True:
                 try:
                     if fd is not None:
-                        key_pressed = self._connection._new_stdin.read(1)
-                        if key_pressed == '\x03':
+                        key_pressed = stdin.read(1)
+                        if key_pressed == b'\x03':
                             raise KeyboardInterrupt
 
                     if not seconds:
@@ -149,7 +155,7 @@ def run(self, tmp=None, task_vars=None):
                             display.warning("Not waiting from prompt as stdin is not interactive")
                             break
                         # read key presses and act accordingly
-                        if key_pressed == '\r':
+                        if key_pressed in (b'\r', b'\n'):
                             break
                         else:
                             result['user_input'] += key_pressed
@@ -158,7 +164,7 @@ def run(self, tmp=None, task_vars=None):
                     if seconds is not None:
                         signal.alarm(0)
                     display.display("Press 'C' to continue the play or 'A' to abort \r"),
-                    if self._c_or_a():
+                    if self._c_or_a(stdin):
                         break
                     else:
                         raise AnsibleError('user requested abort!')
@@ -174,7 +180,7 @@ def run(self, tmp=None, task_vars=None):
                 termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
 
             duration = time.time() - start
-            result['stop'] = str(datetime.datetime.now())
+            result['stop'] = to_text(datetime.datetime.now())
             result['delta'] = int(duration)
 
             if duration_unit == 'minutes':
@@ -183,12 +189,13 @@ def run(self, tmp=None, task_vars=None):
                 duration = round(duration, 2)
             result['stdout'] = "Paused for %s %s" % (duration, duration_unit)
 
+        result['user_input'] = to_text(result['user_input'], errors='surrogate_or_strict')
         return result
 
-    def _c_or_a(self):
+    def _c_or_a(self, stdin):
         while True:
-            key_pressed = self._connection._new_stdin.read(1)
-            if key_pressed.lower() == 'a':
+            key_pressed = stdin.read(1)
+            if key_pressed.lower() == b'a':
                 return False
-            elif key_pressed.lower() == 'c':
+            elif key_pressed.lower() == b'c':
                 return True


Test patch: