summaryrefslogtreecommitdiff
path: root/lib/ansible/plugins/action/pause.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ansible/plugins/action/pause.py')
-rw-r--r--lib/ansible/plugins/action/pause.py257
1 files changed, 47 insertions, 210 deletions
diff --git a/lib/ansible/plugins/action/pause.py b/lib/ansible/plugins/action/pause.py
index 4c98cbbf..d306fbfa 100644
--- a/lib/ansible/plugins/action/pause.py
+++ b/lib/ansible/plugins/action/pause.py
@@ -18,92 +18,15 @@ from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
import datetime
-import signal
-import sys
-import termios
import time
-import tty
-from os import (
- getpgrp,
- isatty,
- tcgetpgrp,
-)
-from ansible.errors import AnsibleError
-from ansible.module_utils._text import to_text, to_native
-from ansible.module_utils.parsing.convert_bool import boolean
+from ansible.errors import AnsibleError, AnsiblePromptInterrupt, AnsiblePromptNoninteractive
+from ansible.module_utils.common.text.converters import to_text
from ansible.plugins.action import ActionBase
from ansible.utils.display import Display
display = Display()
-try:
- import curses
- import io
-
- # Nest the try except since curses.error is not available if curses did not import
- try:
- curses.setupterm()
- HAS_CURSES = True
- except (curses.error, TypeError, io.UnsupportedOperation):
- HAS_CURSES = False
-except ImportError:
- HAS_CURSES = False
-
-MOVE_TO_BOL = b'\r'
-CLEAR_TO_EOL = b'\x1b[K'
-if HAS_CURSES:
- # curses.tigetstr() returns None in some circumstances
- MOVE_TO_BOL = curses.tigetstr('cr') or MOVE_TO_BOL
- CLEAR_TO_EOL = curses.tigetstr('el') or CLEAR_TO_EOL
-
-
-def setraw(fd, when=termios.TCSAFLUSH):
- """Put terminal into a raw mode.
-
- Copied from ``tty`` from CPython 3.11.0, and modified to not remove OPOST from OFLAG
-
- OPOST is kept to prevent an issue with multi line prompts from being corrupted now that display
- is proxied via the queue from forks. The problem is a race condition, in that we proxy the display
- over the fork, but before it can be displayed, this plugin will have continued executing, potentially
- setting stdout and stdin to raw which remove output post processing that commonly converts NL to CRLF
- """
- mode = termios.tcgetattr(fd)
- mode[tty.IFLAG] = mode[tty.IFLAG] & ~(termios.BRKINT | termios.ICRNL | termios.INPCK | termios.ISTRIP | termios.IXON)
- # mode[tty.OFLAG] = mode[tty.OFLAG] & ~(termios.OPOST)
- mode[tty.CFLAG] = mode[tty.CFLAG] & ~(termios.CSIZE | termios.PARENB)
- mode[tty.CFLAG] = mode[tty.CFLAG] | termios.CS8
- mode[tty.LFLAG] = mode[tty.LFLAG] & ~(termios.ECHO | termios.ICANON | termios.IEXTEN | termios.ISIG)
- mode[tty.CC][termios.VMIN] = 1
- mode[tty.CC][termios.VTIME] = 0
- termios.tcsetattr(fd, when, mode)
-
-
-class AnsibleTimeoutExceeded(Exception):
- pass
-
-
-def timeout_handler(signum, frame):
- raise AnsibleTimeoutExceeded
-
-
-def clear_line(stdout):
- stdout.write(b'\x1b[%s' % MOVE_TO_BOL)
- 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):
''' pauses execution for a length or time, or until input is received '''
@@ -169,143 +92,57 @@ class ActionModule(ActionBase):
result['start'] = to_text(datetime.datetime.now())
result['user_input'] = b''
- stdin_fd = None
- old_settings = None
- try:
- if seconds is not None:
- if seconds < 1:
- seconds = 1
-
- # setup the alarm handler
- signal.signal(signal.SIGALRM, timeout_handler)
- signal.alarm(seconds)
+ default_input_complete = None
+ if seconds is not None:
+ if seconds < 1:
+ seconds = 1
- # show the timer and control prompts
- display.display("Pausing for %d seconds%s" % (seconds, echo_prompt))
- display.display("(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)\r"),
-
- # show the prompt specified in the task
- if new_module_args['prompt']:
- display.display(prompt)
+ # show the timer and control prompts
+ display.display("Pausing for %d seconds%s" % (seconds, echo_prompt))
+ # show the prompt specified in the task
+ if new_module_args['prompt']:
+ display.display("(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)\r")
else:
- display.display(prompt)
+ # corner case where enter does not continue, wait for timeout/interrupt only
+ prompt = "(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)\r"
- # save the attributes on the existing (duped) stdin so
- # that we can restore them later after we set raw mode
- stdin_fd = None
- stdout_fd = None
- try:
- stdin = self._connection._new_stdin.buffer
- stdout = sys.stdout.buffer
- stdin_fd = stdin.fileno()
- stdout_fd = stdout.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 windoze
- 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
+ # don't complete on LF/CR; we expect a timeout/interrupt and ignore user input when a pause duration is specified
+ default_input_complete = tuple()
- # get backspace sequences
- try:
- backspace = termios.tcgetattr(stdin_fd)[6][termios.VERASE]
- except Exception:
- backspace = [b'\x7f', b'\x08']
+ # Only echo input if no timeout is specified
+ echo = seconds is None and echo
- old_settings = termios.tcgetattr(stdin_fd)
- setraw(stdin_fd)
-
- # Only set stdout to raw mode if it is a TTY. This is needed when redirecting
- # stdout to a file since a file cannot be set to raw mode.
- if isatty(stdout_fd):
- setraw(stdout_fd)
-
- # Only echo input if no timeout is specified
- if not seconds and echo:
- new_settings = termios.tcgetattr(stdin_fd)
- 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:
- if not interactive:
- if seconds is None:
- 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:
- key_pressed = stdin.read(1)
-
- if key_pressed == intr: # value for Ctrl+C
- clear_line(stdout)
- raise KeyboardInterrupt
-
- if not seconds:
- # read key presses and act accordingly
- if key_pressed in (b'\r', b'\n'):
- clear_line(stdout)
- break
- 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:
- signal.alarm(0)
- display.display("Press 'C' to continue the play or 'A' to abort \r"),
- if self._c_or_a(stdin):
- clear_line(stdout)
- break
-
- clear_line(stdout)
-
- raise AnsibleError('user requested abort!')
-
- except AnsibleTimeoutExceeded:
- # this is the exception we expect when the alarm signal
- # fires, so we simply ignore it to move into the cleanup
- pass
- finally:
- # cleanup and save some information
- # restore the old settings for the duped stdin stdin_fd
- if not (None in (stdin_fd, old_settings)) and isatty(stdin_fd):
- termios.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings)
-
- duration = time.time() - start
- result['stop'] = to_text(datetime.datetime.now())
- result['delta'] = int(duration)
-
- if duration_unit == 'minutes':
- duration = round(duration / 60.0, 2)
+ user_input = b''
+ try:
+ _user_input = display.prompt_until(prompt, private=not echo, seconds=seconds, complete_input=default_input_complete)
+ except AnsiblePromptInterrupt:
+ user_input = None
+ except AnsiblePromptNoninteractive:
+ if seconds is None:
+ display.warning("Not waiting for response to prompt as stdin is not interactive")
else:
- duration = round(duration, 2)
- result['stdout'] = "Paused for %s %s" % (duration, duration_unit)
+ # wait specified duration
+ time.sleep(seconds)
+ else:
+ if seconds is None:
+ user_input = _user_input
+ # user interrupt
+ if user_input is None:
+ prompt = "Press 'C' to continue the play or 'A' to abort \r"
+ try:
+ user_input = display.prompt_until(prompt, private=not echo, interrupt_input=(b'a',), complete_input=(b'c',))
+ except AnsiblePromptInterrupt:
+ raise AnsibleError('user requested abort!')
- result['user_input'] = to_text(result['user_input'], errors='surrogate_or_strict')
- return result
+ duration = time.time() - start
+ result['stop'] = to_text(datetime.datetime.now())
+ result['delta'] = int(duration)
- def _c_or_a(self, stdin):
- while True:
- key_pressed = stdin.read(1)
- if key_pressed.lower() == b'a':
- return False
- elif key_pressed.lower() == b'c':
- return True
+ if duration_unit == 'minutes':
+ duration = round(duration / 60.0, 2)
+ else:
+ duration = round(duration, 2)
+ result['stdout'] = "Paused for %s %s" % (duration, duration_unit)
+ result['user_input'] = to_text(user_input, errors='surrogate_or_strict')
+ return result