pause - do not hang if run in the background (#72065)

* Consolidate logic for determining whether or not session is interactive
  into a single function, is_interactive()
* Increase test coverage

I wasn't able to find a good way of simulating running a backgrounded test with CI since the
whole test is essentially run not in a TTY, which is similar enough to cause the new is_interactive()
function to always return false.
This commit is contained in:
Sam Doran 2020-11-12 12:22:57 -05:00 committed by GitHub
parent aee7a3ed68
commit 4b8cb6582b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 113 additions and 54 deletions

View file

@ -0,0 +1,4 @@
bugfixes:
- >
pause - Fix indefinite hang when using a pause task on a background
process (https://github.com/ansible/ansible/issues/32142)

View file

@ -24,7 +24,11 @@ import termios
import time import time
import tty import tty
from os import isatty from os import (
getpgrp,
isatty,
tcgetpgrp,
)
from ansible.errors import AnsibleError from ansible.errors import AnsibleError
from ansible.module_utils._text import to_text, to_native from ansible.module_utils._text import to_text, to_native
from ansible.module_utils.parsing.convert_bool import boolean from ansible.module_utils.parsing.convert_bool import boolean
@ -67,6 +71,19 @@ def clear_line(stdout):
stdout.write(b'\x1b[%s' % CLEAR_TO_EOL) stdout.write(b'\x1b[%s' % CLEAR_TO_EOL)
def is_interactive(fd=None):
if fd is None:
return False
if isatty(fd):
# Compare the current process group to the process group associated
# with terminal of the given file descriptor to determine if the process
# is running in the background.
return getpgrp() == tcgetpgrp(fd)
else:
return False
class ActionModule(ActionBase): class ActionModule(ActionBase):
''' pauses execution for a length or time, or until input is received ''' ''' pauses execution for a length or time, or until input is received '''
@ -177,71 +194,69 @@ class ActionModule(ActionBase):
stdout_fd = stdout.fileno() stdout_fd = stdout.fileno()
except (ValueError, AttributeError): except (ValueError, AttributeError):
# ValueError: someone is using a closed file descriptor as stdin # ValueError: someone is using a closed file descriptor as stdin
# AttributeError: someone is using a null file descriptor as stdin on windoez # AttributeError: someone is using a null file descriptor as stdin on windoze
stdin = None stdin = None
interactive = is_interactive(stdin_fd)
if interactive:
# grab actual Ctrl+C sequence
try:
intr = termios.tcgetattr(stdin_fd)[6][termios.VINTR]
except Exception:
# unsupported/not present, use default
intr = b'\x03' # value for Ctrl+C
if stdin_fd is not None: # get backspace sequences
if isatty(stdin_fd): try:
# grab actual Ctrl+C sequence backspace = termios.tcgetattr(stdin_fd)[6][termios.VERASE]
try: except Exception:
intr = termios.tcgetattr(stdin_fd)[6][termios.VINTR] backspace = [b'\x7f', b'\x08']
except Exception:
# unsupported/not present, use default
intr = b'\x03' # value for Ctrl+C
# get backspace sequences old_settings = termios.tcgetattr(stdin_fd)
try: tty.setraw(stdin_fd)
backspace = termios.tcgetattr(stdin_fd)[6][termios.VERASE]
except Exception:
backspace = [b'\x7f', b'\x08']
old_settings = termios.tcgetattr(stdin_fd) # Only set stdout to raw mode if it is a TTY. This is needed when redirecting
tty.setraw(stdin_fd) # stdout to a file since a file cannot be set to raw mode.
if isatty(stdout_fd):
tty.setraw(stdout_fd)
# Only set stdout to raw mode if it is a TTY. This is needed when redirecting # Only echo input if no timeout is specified
# stdout to a file since a file cannot be set to raw mode. if not seconds and echo:
if isatty(stdout_fd): new_settings = termios.tcgetattr(stdin_fd)
tty.setraw(stdout_fd) new_settings[3] = new_settings[3] | termios.ECHO
termios.tcsetattr(stdin_fd, termios.TCSANOW, new_settings)
# Only echo input if no timeout is specified # flush the buffer to make sure no previous key presses
if not seconds and echo: # are read in below
new_settings = termios.tcgetattr(stdin_fd) termios.tcflush(stdin, termios.TCIFLUSH)
new_settings[3] = new_settings[3] | termios.ECHO
termios.tcsetattr(stdin_fd, termios.TCSANOW, new_settings)
# flush the buffer to make sure no previous key presses
# are read in below
termios.tcflush(stdin, termios.TCIFLUSH)
while True: while True:
if not interactive:
display.warning("Not waiting for response to prompt as stdin is not interactive")
if seconds is not None:
# Give the signal handler enough time to timeout
time.sleep(seconds + 1)
break
try: try:
if stdin_fd is not None: key_pressed = stdin.read(1)
key_pressed = stdin.read(1) if key_pressed == intr: # value for Ctrl+C
clear_line(stdout)
raise KeyboardInterrupt
if key_pressed == intr: # value for Ctrl+C # read key presses and act accordingly
clear_line(stdout) if key_pressed in (b'\r', b'\n'):
raise KeyboardInterrupt clear_line(stdout)
break
if not seconds: elif key_pressed in backspace:
if stdin_fd is None or not isatty(stdin_fd): # delete a character if backspace is pressed
display.warning("Not waiting for response to prompt as stdin is not interactive") result['user_input'] = result['user_input'][:-1]
break clear_line(stdout)
if echo:
# read key presses and act accordingly stdout.write(result['user_input'])
if key_pressed in (b'\r', b'\n'): stdout.flush()
clear_line(stdout) else:
break result['user_input'] += key_pressed
elif key_pressed in backspace:
# delete a character if backspace is pressed
result['user_input'] = result['user_input'][:-1]
clear_line(stdout)
if echo:
stdout.write(result['user_input'])
stdout.flush()
else:
result['user_input'] += key_pressed
except KeyboardInterrupt: except KeyboardInterrupt:
signal.alarm(0) signal.alarm(0)

View file

@ -0,0 +1,10 @@
- name: Test pause in a background task
hosts: localhost
gather_facts: no
become: no
tasks:
- pause:
- pause:
seconds: 1

View file

@ -4,6 +4,36 @@
become: no become: no
tasks: tasks:
- name: non-integer for duraction (EXPECTED FAILURE)
pause:
seconds: hello
register: result
ignore_errors: yes
- assert:
that:
- result is failed
- "'non-integer' in result.msg"
- name: non-boolean for echo (EXPECTED FAILURE)
pause:
echo: hello
register: result
ignore_errors: yes
- assert:
that:
- result is failed
- "'not a valid boolean' in result.msg"
- pause:
seconds: 0.1
register: results
- assert:
that:
- results.stdout is search('Paused for \d+\.\d+ seconds')
- pause: - pause:
seconds: 1 seconds: 1
register: results register: results