posix.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. from __future__ import unicode_literals
  2. import fcntl
  3. import os
  4. import signal
  5. import threading
  6. import time
  7. from prompt_toolkit.terminal.vt100_input import InputStream
  8. from prompt_toolkit.utils import DummyContext, in_main_thread
  9. from prompt_toolkit.input import Input
  10. from .base import EventLoop, INPUT_TIMEOUT
  11. from .callbacks import EventLoopCallbacks
  12. from .inputhook import InputHookContext
  13. from .posix_utils import PosixStdinReader
  14. from .utils import TimeIt
  15. from .select import AutoSelector, Selector, fd_to_int
  16. __all__ = (
  17. 'PosixEventLoop',
  18. )
  19. _now = time.time
  20. class PosixEventLoop(EventLoop):
  21. """
  22. Event loop for posix systems (Linux, Mac os X).
  23. """
  24. def __init__(self, inputhook=None, selector=AutoSelector):
  25. assert inputhook is None or callable(inputhook)
  26. assert issubclass(selector, Selector)
  27. self.running = False
  28. self.closed = False
  29. self._running = False
  30. self._callbacks = None
  31. self._calls_from_executor = []
  32. self._read_fds = {} # Maps fd to handler.
  33. self.selector = selector()
  34. # Create a pipe for inter thread communication.
  35. self._schedule_pipe = os.pipe()
  36. fcntl.fcntl(self._schedule_pipe[0], fcntl.F_SETFL, os.O_NONBLOCK)
  37. # Create inputhook context.
  38. self._inputhook_context = InputHookContext(inputhook) if inputhook else None
  39. def run(self, stdin, callbacks):
  40. """
  41. The input 'event loop'.
  42. """
  43. assert isinstance(stdin, Input)
  44. assert isinstance(callbacks, EventLoopCallbacks)
  45. assert not self._running
  46. if self.closed:
  47. raise Exception('Event loop already closed.')
  48. self._running = True
  49. self._callbacks = callbacks
  50. inputstream = InputStream(callbacks.feed_key)
  51. current_timeout = [INPUT_TIMEOUT] # Nonlocal
  52. # Create reader class.
  53. stdin_reader = PosixStdinReader(stdin.fileno())
  54. # Only attach SIGWINCH signal handler in main thread.
  55. # (It's not possible to attach signal handlers in other threads. In
  56. # that case we should rely on a the main thread to call this manually
  57. # instead.)
  58. if in_main_thread():
  59. ctx = call_on_sigwinch(self.received_winch)
  60. else:
  61. ctx = DummyContext()
  62. def read_from_stdin():
  63. " Read user input. "
  64. # Feed input text.
  65. data = stdin_reader.read()
  66. inputstream.feed(data)
  67. # Set timeout again.
  68. current_timeout[0] = INPUT_TIMEOUT
  69. # Quit when the input stream was closed.
  70. if stdin_reader.closed:
  71. self.stop()
  72. self.add_reader(stdin, read_from_stdin)
  73. self.add_reader(self._schedule_pipe[0], None)
  74. with ctx:
  75. while self._running:
  76. # Call inputhook.
  77. if self._inputhook_context:
  78. with TimeIt() as inputhook_timer:
  79. def ready(wait):
  80. " True when there is input ready. The inputhook should return control. "
  81. return self._ready_for_reading(current_timeout[0] if wait else 0) != []
  82. self._inputhook_context.call_inputhook(ready)
  83. inputhook_duration = inputhook_timer.duration
  84. else:
  85. inputhook_duration = 0
  86. # Calculate remaining timeout. (The inputhook consumed some of the time.)
  87. if current_timeout[0] is None:
  88. remaining_timeout = None
  89. else:
  90. remaining_timeout = max(0, current_timeout[0] - inputhook_duration)
  91. # Wait until input is ready.
  92. fds = self._ready_for_reading(remaining_timeout)
  93. # When any of the FDs are ready. Call the appropriate callback.
  94. if fds:
  95. # Create lists of high/low priority tasks. The main reason
  96. # for this is to allow painting the UI to happen as soon as
  97. # possible, but when there are many events happening, we
  98. # don't want to call the UI renderer 1000x per second. If
  99. # the eventloop is completely saturated with many CPU
  100. # intensive tasks (like processing input/output), we say
  101. # that drawing the UI can be postponed a little, to make
  102. # CPU available. This will be a low priority task in that
  103. # case.
  104. tasks = []
  105. low_priority_tasks = []
  106. now = None # Lazy load time. (Fewer system calls.)
  107. for fd in fds:
  108. # For the 'call_from_executor' fd, put each pending
  109. # item on either the high or low priority queue.
  110. if fd == self._schedule_pipe[0]:
  111. for c, max_postpone_until in self._calls_from_executor:
  112. if max_postpone_until is None:
  113. # Execute now.
  114. tasks.append(c)
  115. else:
  116. # Execute soon, if `max_postpone_until` is in the future.
  117. now = now or _now()
  118. if max_postpone_until < now:
  119. tasks.append(c)
  120. else:
  121. low_priority_tasks.append((c, max_postpone_until))
  122. self._calls_from_executor = []
  123. # Flush all the pipe content.
  124. os.read(self._schedule_pipe[0], 1024)
  125. else:
  126. handler = self._read_fds.get(fd)
  127. if handler:
  128. tasks.append(handler)
  129. # When there are high priority tasks, run all these.
  130. # Schedule low priority tasks for the next iteration.
  131. if tasks:
  132. for t in tasks:
  133. t()
  134. # Postpone low priority tasks.
  135. for t, max_postpone_until in low_priority_tasks:
  136. self.call_from_executor(t, _max_postpone_until=max_postpone_until)
  137. else:
  138. # Currently there are only low priority tasks -> run them right now.
  139. for t, _ in low_priority_tasks:
  140. t()
  141. else:
  142. # Flush all pending keys on a timeout. (This is most
  143. # important to flush the vt100 'Escape' key early when
  144. # nothing else follows.)
  145. inputstream.flush()
  146. # Fire input timeout event.
  147. callbacks.input_timeout()
  148. current_timeout[0] = None
  149. self.remove_reader(stdin)
  150. self.remove_reader(self._schedule_pipe[0])
  151. self._callbacks = None
  152. def _ready_for_reading(self, timeout=None):
  153. """
  154. Return the file descriptors that are ready for reading.
  155. """
  156. fds = self.selector.select(timeout)
  157. return fds
  158. def received_winch(self):
  159. """
  160. Notify the event loop that SIGWINCH has been received
  161. """
  162. # Process signal asynchronously, because this handler can write to the
  163. # output, and doing this inside the signal handler causes easily
  164. # reentrant calls, giving runtime errors..
  165. # Furthur, this has to be thread safe. When the CommandLineInterface
  166. # runs not in the main thread, this function still has to be called
  167. # from the main thread. (The only place where we can install signal
  168. # handlers.)
  169. def process_winch():
  170. if self._callbacks:
  171. self._callbacks.terminal_size_changed()
  172. self.call_from_executor(process_winch)
  173. def run_in_executor(self, callback):
  174. """
  175. Run a long running function in a background thread.
  176. (This is recommended for code that could block the event loop.)
  177. Similar to Twisted's ``deferToThread``.
  178. """
  179. # Wait until the main thread is idle.
  180. # We start the thread by using `call_from_executor`. The event loop
  181. # favours processing input over `calls_from_executor`, so the thread
  182. # will not start until there is no more input to process and the main
  183. # thread becomes idle for an instant. This is good, because Python
  184. # threading favours CPU over I/O -- an autocompletion thread in the
  185. # background would cause a significantly slow down of the main thread.
  186. # It is mostly noticable when pasting large portions of text while
  187. # having real time autocompletion while typing on.
  188. def start_executor():
  189. threading.Thread(target=callback).start()
  190. self.call_from_executor(start_executor)
  191. def call_from_executor(self, callback, _max_postpone_until=None):
  192. """
  193. Call this function in the main event loop.
  194. Similar to Twisted's ``callFromThread``.
  195. :param _max_postpone_until: `None` or `time.time` value. For interal
  196. use. If the eventloop is saturated, consider this task to be low
  197. priority and postpone maximum until this timestamp. (For instance,
  198. repaint is done using low priority.)
  199. """
  200. assert _max_postpone_until is None or isinstance(_max_postpone_until, float)
  201. self._calls_from_executor.append((callback, _max_postpone_until))
  202. if self._schedule_pipe:
  203. try:
  204. os.write(self._schedule_pipe[1], b'x')
  205. except (AttributeError, IndexError, OSError):
  206. # Handle race condition. We're in a different thread.
  207. # - `_schedule_pipe` could have become None in the meantime.
  208. # - We catch `OSError` (actually BrokenPipeError), because the
  209. # main thread could have closed the pipe already.
  210. pass
  211. def stop(self):
  212. """
  213. Stop the event loop.
  214. """
  215. self._running = False
  216. def close(self):
  217. self.closed = True
  218. # Close pipes.
  219. schedule_pipe = self._schedule_pipe
  220. self._schedule_pipe = None
  221. if schedule_pipe:
  222. os.close(schedule_pipe[0])
  223. os.close(schedule_pipe[1])
  224. if self._inputhook_context:
  225. self._inputhook_context.close()
  226. def add_reader(self, fd, callback):
  227. " Add read file descriptor to the event loop. "
  228. fd = fd_to_int(fd)
  229. self._read_fds[fd] = callback
  230. self.selector.register(fd)
  231. def remove_reader(self, fd):
  232. " Remove read file descriptor from the event loop. "
  233. fd = fd_to_int(fd)
  234. if fd in self._read_fds:
  235. del self._read_fds[fd]
  236. self.selector.unregister(fd)
  237. class call_on_sigwinch(object):
  238. """
  239. Context manager which Installs a SIGWINCH callback.
  240. (This signal occurs when the terminal size changes.)
  241. """
  242. def __init__(self, callback):
  243. self.callback = callback
  244. self.previous_callback = None
  245. def __enter__(self):
  246. self.previous_callback = signal.signal(signal.SIGWINCH, lambda *a: self.callback())
  247. def __exit__(self, *a, **kw):
  248. if self.previous_callback is None:
  249. # Normally, `signal.signal` should never return `None`.
  250. # For some reason it happens here:
  251. # https://github.com/jonathanslenders/python-prompt-toolkit/pull/174
  252. signal.signal(signal.SIGWINCH, 0)
  253. else:
  254. signal.signal(signal.SIGWINCH, self.previous_callback)