You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

434 lines
15 KiB

4 years ago
  1. # coding: utf-8
  2. """Wrappers for forwarding stdout/stderr over zmq"""
  3. # Copyright (c) IPython Development Team.
  4. # Distributed under the terms of the Modified BSD License.
  5. from __future__ import print_function
  6. import atexit
  7. from binascii import b2a_hex
  8. from collections import deque
  9. try:
  10. from importlib import lock_held as import_lock_held
  11. except ImportError:
  12. from imp import lock_held as import_lock_held
  13. import os
  14. import sys
  15. import threading
  16. import warnings
  17. from io import StringIO, TextIOBase
  18. import zmq
  19. from zmq.eventloop.ioloop import IOLoop
  20. from zmq.eventloop.zmqstream import ZMQStream
  21. from jupyter_client.session import extract_header
  22. from ipython_genutils import py3compat
  23. from ipython_genutils.py3compat import unicode_type
  24. #-----------------------------------------------------------------------------
  25. # Globals
  26. #-----------------------------------------------------------------------------
  27. MASTER = 0
  28. CHILD = 1
  29. #-----------------------------------------------------------------------------
  30. # IO classes
  31. #-----------------------------------------------------------------------------
  32. class IOPubThread(object):
  33. """An object for sending IOPub messages in a background thread
  34. Prevents a blocking main thread from delaying output from threads.
  35. IOPubThread(pub_socket).background_socket is a Socket-API-providing object
  36. whose IO is always run in a thread.
  37. """
  38. def __init__(self, socket, pipe=False):
  39. """Create IOPub thread
  40. Parameters
  41. ----------
  42. socket: zmq.PUB Socket
  43. the socket on which messages will be sent.
  44. pipe: bool
  45. Whether this process should listen for IOPub messages
  46. piped from subprocesses.
  47. """
  48. self.socket = socket
  49. self.background_socket = BackgroundSocket(self)
  50. self._master_pid = os.getpid()
  51. self._pipe_flag = pipe
  52. self.io_loop = IOLoop(make_current=False)
  53. if pipe:
  54. self._setup_pipe_in()
  55. self._local = threading.local()
  56. self._events = deque()
  57. self._setup_event_pipe()
  58. self.thread = threading.Thread(target=self._thread_main)
  59. self.thread.daemon = True
  60. def _thread_main(self):
  61. """The inner loop that's actually run in a thread"""
  62. self.io_loop.make_current()
  63. self.io_loop.start()
  64. self.io_loop.close(all_fds=True)
  65. def _setup_event_pipe(self):
  66. """Create the PULL socket listening for events that should fire in this thread."""
  67. ctx = self.socket.context
  68. pipe_in = ctx.socket(zmq.PULL)
  69. pipe_in.linger = 0
  70. _uuid = b2a_hex(os.urandom(16)).decode('ascii')
  71. iface = self._event_interface = 'inproc://%s' % _uuid
  72. pipe_in.bind(iface)
  73. self._event_puller = ZMQStream(pipe_in, self.io_loop)
  74. self._event_puller.on_recv(self._handle_event)
  75. @property
  76. def _event_pipe(self):
  77. """thread-local event pipe for signaling events that should be processed in the thread"""
  78. try:
  79. event_pipe = self._local.event_pipe
  80. except AttributeError:
  81. # new thread, new event pipe
  82. ctx = self.socket.context
  83. event_pipe = ctx.socket(zmq.PUSH)
  84. event_pipe.linger = 0
  85. event_pipe.connect(self._event_interface)
  86. self._local.event_pipe = event_pipe
  87. return event_pipe
  88. def _handle_event(self, msg):
  89. """Handle an event on the event pipe
  90. Content of the message is ignored.
  91. Whenever *an* event arrives on the event stream,
  92. *all* waiting events are processed in order.
  93. """
  94. # freeze event count so new writes don't extend the queue
  95. # while we are processing
  96. n_events = len(self._events)
  97. for i in range(n_events):
  98. event_f = self._events.popleft()
  99. event_f()
  100. def _setup_pipe_in(self):
  101. """setup listening pipe for IOPub from forked subprocesses"""
  102. ctx = self.socket.context
  103. # use UUID to authenticate pipe messages
  104. self._pipe_uuid = os.urandom(16)
  105. pipe_in = ctx.socket(zmq.PULL)
  106. pipe_in.linger = 0
  107. try:
  108. self._pipe_port = pipe_in.bind_to_random_port("tcp://127.0.0.1")
  109. except zmq.ZMQError as e:
  110. warnings.warn("Couldn't bind IOPub Pipe to 127.0.0.1: %s" % e +
  111. "\nsubprocess output will be unavailable."
  112. )
  113. self._pipe_flag = False
  114. pipe_in.close()
  115. return
  116. self._pipe_in = ZMQStream(pipe_in, self.io_loop)
  117. self._pipe_in.on_recv(self._handle_pipe_msg)
  118. def _handle_pipe_msg(self, msg):
  119. """handle a pipe message from a subprocess"""
  120. if not self._pipe_flag or not self._is_master_process():
  121. return
  122. if msg[0] != self._pipe_uuid:
  123. print("Bad pipe message: %s", msg, file=sys.__stderr__)
  124. return
  125. self.send_multipart(msg[1:])
  126. def _setup_pipe_out(self):
  127. # must be new context after fork
  128. ctx = zmq.Context()
  129. pipe_out = ctx.socket(zmq.PUSH)
  130. pipe_out.linger = 3000 # 3s timeout for pipe_out sends before discarding the message
  131. pipe_out.connect("tcp://127.0.0.1:%i" % self._pipe_port)
  132. return ctx, pipe_out
  133. def _is_master_process(self):
  134. return os.getpid() == self._master_pid
  135. def _check_mp_mode(self):
  136. """check for forks, and switch to zmq pipeline if necessary"""
  137. if not self._pipe_flag or self._is_master_process():
  138. return MASTER
  139. else:
  140. return CHILD
  141. def start(self):
  142. """Start the IOPub thread"""
  143. self.thread.start()
  144. # make sure we don't prevent process exit
  145. # I'm not sure why setting daemon=True above isn't enough, but it doesn't appear to be.
  146. atexit.register(self.stop)
  147. def stop(self):
  148. """Stop the IOPub thread"""
  149. if not self.thread.is_alive():
  150. return
  151. self.io_loop.add_callback(self.io_loop.stop)
  152. self.thread.join()
  153. if hasattr(self._local, 'event_pipe'):
  154. self._local.event_pipe.close()
  155. def close(self):
  156. self.socket.close()
  157. self.socket = None
  158. @property
  159. def closed(self):
  160. return self.socket is None
  161. def schedule(self, f):
  162. """Schedule a function to be called in our IO thread.
  163. If the thread is not running, call immediately.
  164. """
  165. if self.thread.is_alive():
  166. self._events.append(f)
  167. # wake event thread (message content is ignored)
  168. self._event_pipe.send(b'')
  169. else:
  170. f()
  171. def send_multipart(self, *args, **kwargs):
  172. """send_multipart schedules actual zmq send in my thread.
  173. If my thread isn't running (e.g. forked process), send immediately.
  174. """
  175. self.schedule(lambda : self._really_send(*args, **kwargs))
  176. def _really_send(self, msg, *args, **kwargs):
  177. """The callback that actually sends messages"""
  178. mp_mode = self._check_mp_mode()
  179. if mp_mode != CHILD:
  180. # we are master, do a regular send
  181. self.socket.send_multipart(msg, *args, **kwargs)
  182. else:
  183. # we are a child, pipe to master
  184. # new context/socket for every pipe-out
  185. # since forks don't teardown politely, use ctx.term to ensure send has completed
  186. ctx, pipe_out = self._setup_pipe_out()
  187. pipe_out.send_multipart([self._pipe_uuid] + msg, *args, **kwargs)
  188. pipe_out.close()
  189. ctx.term()
  190. class BackgroundSocket(object):
  191. """Wrapper around IOPub thread that provides zmq send[_multipart]"""
  192. io_thread = None
  193. def __init__(self, io_thread):
  194. self.io_thread = io_thread
  195. def __getattr__(self, attr):
  196. """Wrap socket attr access for backward-compatibility"""
  197. if attr.startswith('__') and attr.endswith('__'):
  198. # don't wrap magic methods
  199. super(BackgroundSocket, self).__getattr__(attr)
  200. if hasattr(self.io_thread.socket, attr):
  201. warnings.warn("Accessing zmq Socket attribute %s on BackgroundSocket" % attr,
  202. DeprecationWarning, stacklevel=2)
  203. return getattr(self.io_thread.socket, attr)
  204. super(BackgroundSocket, self).__getattr__(attr)
  205. def __setattr__(self, attr, value):
  206. if attr == 'io_thread' or (attr.startswith('__' and attr.endswith('__'))):
  207. super(BackgroundSocket, self).__setattr__(attr, value)
  208. else:
  209. warnings.warn("Setting zmq Socket attribute %s on BackgroundSocket" % attr,
  210. DeprecationWarning, stacklevel=2)
  211. setattr(self.io_thread.socket, attr, value)
  212. def send(self, msg, *args, **kwargs):
  213. return self.send_multipart([msg], *args, **kwargs)
  214. def send_multipart(self, *args, **kwargs):
  215. """Schedule send in IO thread"""
  216. return self.io_thread.send_multipart(*args, **kwargs)
  217. class OutStream(TextIOBase):
  218. """A file like object that publishes the stream to a 0MQ PUB socket.
  219. Output is handed off to an IO Thread
  220. """
  221. # timeout for flush to avoid infinite hang
  222. # in case of misbehavior
  223. flush_timeout = 10
  224. # The time interval between automatic flushes, in seconds.
  225. flush_interval = 0.2
  226. topic = None
  227. encoding = 'UTF-8'
  228. def __init__(self, session, pub_thread, name, pipe=None, echo=None):
  229. if pipe is not None:
  230. warnings.warn("pipe argument to OutStream is deprecated and ignored",
  231. DeprecationWarning)
  232. # This is necessary for compatibility with Python built-in streams
  233. self.session = session
  234. if not isinstance(pub_thread, IOPubThread):
  235. # Backward-compat: given socket, not thread. Wrap in a thread.
  236. warnings.warn("OutStream should be created with IOPubThread, not %r" % pub_thread,
  237. DeprecationWarning, stacklevel=2)
  238. pub_thread = IOPubThread(pub_thread)
  239. pub_thread.start()
  240. self.pub_thread = pub_thread
  241. self.name = name
  242. self.topic = b'stream.' + py3compat.cast_bytes(name)
  243. self.parent_header = {}
  244. self._master_pid = os.getpid()
  245. self._flush_pending = False
  246. self._io_loop = pub_thread.io_loop
  247. self._new_buffer()
  248. self.echo = None
  249. if echo:
  250. if hasattr(echo, 'read') and hasattr(echo, 'write'):
  251. self.echo = echo
  252. else:
  253. raise ValueError("echo argument must be a file like object")
  254. def _is_master_process(self):
  255. return os.getpid() == self._master_pid
  256. def set_parent(self, parent):
  257. self.parent_header = extract_header(parent)
  258. def close(self):
  259. self.pub_thread = None
  260. @property
  261. def closed(self):
  262. return self.pub_thread is None
  263. def _schedule_flush(self):
  264. """schedule a flush in the IO thread
  265. call this on write, to indicate that flush should be called soon.
  266. """
  267. if self._flush_pending:
  268. return
  269. self._flush_pending = True
  270. # add_timeout has to be handed to the io thread via event pipe
  271. def _schedule_in_thread():
  272. self._io_loop.call_later(self.flush_interval, self._flush)
  273. self.pub_thread.schedule(_schedule_in_thread)
  274. def flush(self):
  275. """trigger actual zmq send
  276. send will happen in the background thread
  277. """
  278. if self.pub_thread.thread.is_alive():
  279. # request flush on the background thread
  280. self.pub_thread.schedule(self._flush)
  281. # wait for flush to actually get through, if we can.
  282. # waiting across threads during import can cause deadlocks
  283. # so only wait if import lock is not held
  284. if not import_lock_held():
  285. evt = threading.Event()
  286. self.pub_thread.schedule(evt.set)
  287. # and give a timeout to avoid
  288. if not evt.wait(self.flush_timeout):
  289. # write directly to __stderr__ instead of warning because
  290. # if this is happening sys.stderr may be the problem.
  291. print("IOStream.flush timed out", file=sys.__stderr__)
  292. else:
  293. self._flush()
  294. def _flush(self):
  295. """This is where the actual send happens.
  296. _flush should generally be called in the IO thread,
  297. unless the thread has been destroyed (e.g. forked subprocess).
  298. """
  299. self._flush_pending = False
  300. if self.echo is not None:
  301. try:
  302. self.echo.flush()
  303. except OSError as e:
  304. if self.echo is not sys.__stderr__:
  305. print("Flush failed: {}".format(e),
  306. file=sys.__stderr__)
  307. data = self._flush_buffer()
  308. if data:
  309. # FIXME: this disables Session's fork-safe check,
  310. # since pub_thread is itself fork-safe.
  311. # There should be a better way to do this.
  312. self.session.pid = os.getpid()
  313. content = {u'name':self.name, u'text':data}
  314. self.session.send(self.pub_thread, u'stream', content=content,
  315. parent=self.parent_header, ident=self.topic)
  316. def write(self, string):
  317. if self.echo is not None:
  318. try:
  319. self.echo.write(string)
  320. except OSError as e:
  321. if self.echo is not sys.__stderr__:
  322. print("Write failed: {}".format(e),
  323. file=sys.__stderr__)
  324. if self.pub_thread is None:
  325. raise ValueError('I/O operation on closed file')
  326. else:
  327. # Make sure that we're handling unicode
  328. if not isinstance(string, unicode_type):
  329. string = string.decode(self.encoding, 'replace')
  330. is_child = (not self._is_master_process())
  331. # only touch the buffer in the IO thread to avoid races
  332. self.pub_thread.schedule(lambda : self._buffer.write(string))
  333. if is_child:
  334. # newlines imply flush in subprocesses
  335. # mp.Pool cannot be trusted to flush promptly (or ever),
  336. # and this helps.
  337. if '\n' in string:
  338. self.flush()
  339. else:
  340. self._schedule_flush()
  341. def writelines(self, sequence):
  342. if self.pub_thread is None:
  343. raise ValueError('I/O operation on closed file')
  344. else:
  345. for string in sequence:
  346. self.write(string)
  347. def writable(self):
  348. return True
  349. def _flush_buffer(self):
  350. """clear the current buffer and return the current buffer data.
  351. This should only be called in the IO thread.
  352. """
  353. data = u''
  354. if self._buffer is not None:
  355. buf = self._buffer
  356. self._new_buffer()
  357. data = buf.getvalue()
  358. buf.close()
  359. return data
  360. def _new_buffer(self):
  361. self._buffer = StringIO()