|
|
- # coding: utf-8
- """Wrappers for forwarding stdout/stderr over zmq"""
-
- # Copyright (c) IPython Development Team.
- # Distributed under the terms of the Modified BSD License.
-
- from __future__ import print_function
- import atexit
- from binascii import b2a_hex
- from collections import deque
- try:
- from importlib import lock_held as import_lock_held
- except ImportError:
- from imp import lock_held as import_lock_held
- import os
- import sys
- import threading
- import warnings
- from io import StringIO, TextIOBase
-
- import zmq
- from zmq.eventloop.ioloop import IOLoop
- from zmq.eventloop.zmqstream import ZMQStream
-
- from jupyter_client.session import extract_header
-
- from ipython_genutils import py3compat
- from ipython_genutils.py3compat import unicode_type
-
- #-----------------------------------------------------------------------------
- # Globals
- #-----------------------------------------------------------------------------
-
- MASTER = 0
- CHILD = 1
-
- #-----------------------------------------------------------------------------
- # IO classes
- #-----------------------------------------------------------------------------
-
- class IOPubThread(object):
- """An object for sending IOPub messages in a background thread
-
- Prevents a blocking main thread from delaying output from threads.
-
- IOPubThread(pub_socket).background_socket is a Socket-API-providing object
- whose IO is always run in a thread.
- """
-
- def __init__(self, socket, pipe=False):
- """Create IOPub thread
-
- Parameters
- ----------
-
- socket: zmq.PUB Socket
- the socket on which messages will be sent.
- pipe: bool
- Whether this process should listen for IOPub messages
- piped from subprocesses.
- """
- self.socket = socket
- self.background_socket = BackgroundSocket(self)
- self._master_pid = os.getpid()
- self._pipe_flag = pipe
- self.io_loop = IOLoop(make_current=False)
- if pipe:
- self._setup_pipe_in()
- self._local = threading.local()
- self._events = deque()
- self._setup_event_pipe()
- self.thread = threading.Thread(target=self._thread_main)
- self.thread.daemon = True
-
- def _thread_main(self):
- """The inner loop that's actually run in a thread"""
- self.io_loop.make_current()
- self.io_loop.start()
- self.io_loop.close(all_fds=True)
-
- def _setup_event_pipe(self):
- """Create the PULL socket listening for events that should fire in this thread."""
- ctx = self.socket.context
- pipe_in = ctx.socket(zmq.PULL)
- pipe_in.linger = 0
-
- _uuid = b2a_hex(os.urandom(16)).decode('ascii')
- iface = self._event_interface = 'inproc://%s' % _uuid
- pipe_in.bind(iface)
- self._event_puller = ZMQStream(pipe_in, self.io_loop)
- self._event_puller.on_recv(self._handle_event)
-
- @property
- def _event_pipe(self):
- """thread-local event pipe for signaling events that should be processed in the thread"""
- try:
- event_pipe = self._local.event_pipe
- except AttributeError:
- # new thread, new event pipe
- ctx = self.socket.context
- event_pipe = ctx.socket(zmq.PUSH)
- event_pipe.linger = 0
- event_pipe.connect(self._event_interface)
- self._local.event_pipe = event_pipe
- return event_pipe
-
- def _handle_event(self, msg):
- """Handle an event on the event pipe
-
- Content of the message is ignored.
-
- Whenever *an* event arrives on the event stream,
- *all* waiting events are processed in order.
- """
- # freeze event count so new writes don't extend the queue
- # while we are processing
- n_events = len(self._events)
- for i in range(n_events):
- event_f = self._events.popleft()
- event_f()
-
- def _setup_pipe_in(self):
- """setup listening pipe for IOPub from forked subprocesses"""
- ctx = self.socket.context
-
- # use UUID to authenticate pipe messages
- self._pipe_uuid = os.urandom(16)
-
- pipe_in = ctx.socket(zmq.PULL)
- pipe_in.linger = 0
-
- try:
- self._pipe_port = pipe_in.bind_to_random_port("tcp://127.0.0.1")
- except zmq.ZMQError as e:
- warnings.warn("Couldn't bind IOPub Pipe to 127.0.0.1: %s" % e +
- "\nsubprocess output will be unavailable."
- )
- self._pipe_flag = False
- pipe_in.close()
- return
- self._pipe_in = ZMQStream(pipe_in, self.io_loop)
- self._pipe_in.on_recv(self._handle_pipe_msg)
-
- def _handle_pipe_msg(self, msg):
- """handle a pipe message from a subprocess"""
- if not self._pipe_flag or not self._is_master_process():
- return
- if msg[0] != self._pipe_uuid:
- print("Bad pipe message: %s", msg, file=sys.__stderr__)
- return
- self.send_multipart(msg[1:])
-
- def _setup_pipe_out(self):
- # must be new context after fork
- ctx = zmq.Context()
- pipe_out = ctx.socket(zmq.PUSH)
- pipe_out.linger = 3000 # 3s timeout for pipe_out sends before discarding the message
- pipe_out.connect("tcp://127.0.0.1:%i" % self._pipe_port)
- return ctx, pipe_out
-
- def _is_master_process(self):
- return os.getpid() == self._master_pid
-
- def _check_mp_mode(self):
- """check for forks, and switch to zmq pipeline if necessary"""
- if not self._pipe_flag or self._is_master_process():
- return MASTER
- else:
- return CHILD
-
- def start(self):
- """Start the IOPub thread"""
- self.thread.start()
- # make sure we don't prevent process exit
- # I'm not sure why setting daemon=True above isn't enough, but it doesn't appear to be.
- atexit.register(self.stop)
-
- def stop(self):
- """Stop the IOPub thread"""
- if not self.thread.is_alive():
- return
- self.io_loop.add_callback(self.io_loop.stop)
- self.thread.join()
- if hasattr(self._local, 'event_pipe'):
- self._local.event_pipe.close()
-
- def close(self):
- self.socket.close()
- self.socket = None
-
- @property
- def closed(self):
- return self.socket is None
-
- def schedule(self, f):
- """Schedule a function to be called in our IO thread.
-
- If the thread is not running, call immediately.
- """
- if self.thread.is_alive():
- self._events.append(f)
- # wake event thread (message content is ignored)
- self._event_pipe.send(b'')
- else:
- f()
-
- def send_multipart(self, *args, **kwargs):
- """send_multipart schedules actual zmq send in my thread.
-
- If my thread isn't running (e.g. forked process), send immediately.
- """
- self.schedule(lambda : self._really_send(*args, **kwargs))
-
- def _really_send(self, msg, *args, **kwargs):
- """The callback that actually sends messages"""
- mp_mode = self._check_mp_mode()
-
- if mp_mode != CHILD:
- # we are master, do a regular send
- self.socket.send_multipart(msg, *args, **kwargs)
- else:
- # we are a child, pipe to master
- # new context/socket for every pipe-out
- # since forks don't teardown politely, use ctx.term to ensure send has completed
- ctx, pipe_out = self._setup_pipe_out()
- pipe_out.send_multipart([self._pipe_uuid] + msg, *args, **kwargs)
- pipe_out.close()
- ctx.term()
-
-
- class BackgroundSocket(object):
- """Wrapper around IOPub thread that provides zmq send[_multipart]"""
- io_thread = None
-
- def __init__(self, io_thread):
- self.io_thread = io_thread
-
- def __getattr__(self, attr):
- """Wrap socket attr access for backward-compatibility"""
- if attr.startswith('__') and attr.endswith('__'):
- # don't wrap magic methods
- super(BackgroundSocket, self).__getattr__(attr)
- if hasattr(self.io_thread.socket, attr):
- warnings.warn("Accessing zmq Socket attribute %s on BackgroundSocket" % attr,
- DeprecationWarning, stacklevel=2)
- return getattr(self.io_thread.socket, attr)
- super(BackgroundSocket, self).__getattr__(attr)
-
- def __setattr__(self, attr, value):
- if attr == 'io_thread' or (attr.startswith('__' and attr.endswith('__'))):
- super(BackgroundSocket, self).__setattr__(attr, value)
- else:
- warnings.warn("Setting zmq Socket attribute %s on BackgroundSocket" % attr,
- DeprecationWarning, stacklevel=2)
- setattr(self.io_thread.socket, attr, value)
-
- def send(self, msg, *args, **kwargs):
- return self.send_multipart([msg], *args, **kwargs)
-
- def send_multipart(self, *args, **kwargs):
- """Schedule send in IO thread"""
- return self.io_thread.send_multipart(*args, **kwargs)
-
-
- class OutStream(TextIOBase):
- """A file like object that publishes the stream to a 0MQ PUB socket.
-
- Output is handed off to an IO Thread
- """
-
- # timeout for flush to avoid infinite hang
- # in case of misbehavior
- flush_timeout = 10
- # The time interval between automatic flushes, in seconds.
- flush_interval = 0.2
- topic = None
- encoding = 'UTF-8'
-
- def __init__(self, session, pub_thread, name, pipe=None, echo=None):
- if pipe is not None:
- warnings.warn("pipe argument to OutStream is deprecated and ignored",
- DeprecationWarning)
- # This is necessary for compatibility with Python built-in streams
- self.session = session
- if not isinstance(pub_thread, IOPubThread):
- # Backward-compat: given socket, not thread. Wrap in a thread.
- warnings.warn("OutStream should be created with IOPubThread, not %r" % pub_thread,
- DeprecationWarning, stacklevel=2)
- pub_thread = IOPubThread(pub_thread)
- pub_thread.start()
- self.pub_thread = pub_thread
- self.name = name
- self.topic = b'stream.' + py3compat.cast_bytes(name)
- self.parent_header = {}
- self._master_pid = os.getpid()
- self._flush_pending = False
- self._io_loop = pub_thread.io_loop
- self._new_buffer()
- self.echo = None
-
- if echo:
- if hasattr(echo, 'read') and hasattr(echo, 'write'):
- self.echo = echo
- else:
- raise ValueError("echo argument must be a file like object")
-
- def _is_master_process(self):
- return os.getpid() == self._master_pid
-
- def set_parent(self, parent):
- self.parent_header = extract_header(parent)
-
- def close(self):
- self.pub_thread = None
-
- @property
- def closed(self):
- return self.pub_thread is None
-
- def _schedule_flush(self):
- """schedule a flush in the IO thread
-
- call this on write, to indicate that flush should be called soon.
- """
- if self._flush_pending:
- return
- self._flush_pending = True
-
- # add_timeout has to be handed to the io thread via event pipe
- def _schedule_in_thread():
- self._io_loop.call_later(self.flush_interval, self._flush)
- self.pub_thread.schedule(_schedule_in_thread)
-
- def flush(self):
- """trigger actual zmq send
-
- send will happen in the background thread
- """
- if self.pub_thread.thread.is_alive():
- # request flush on the background thread
- self.pub_thread.schedule(self._flush)
- # wait for flush to actually get through, if we can.
- # waiting across threads during import can cause deadlocks
- # so only wait if import lock is not held
- if not import_lock_held():
- evt = threading.Event()
- self.pub_thread.schedule(evt.set)
- # and give a timeout to avoid
- if not evt.wait(self.flush_timeout):
- # write directly to __stderr__ instead of warning because
- # if this is happening sys.stderr may be the problem.
- print("IOStream.flush timed out", file=sys.__stderr__)
- else:
- self._flush()
-
- def _flush(self):
- """This is where the actual send happens.
-
- _flush should generally be called in the IO thread,
- unless the thread has been destroyed (e.g. forked subprocess).
- """
- self._flush_pending = False
-
- if self.echo is not None:
- try:
- self.echo.flush()
- except OSError as e:
- if self.echo is not sys.__stderr__:
- print("Flush failed: {}".format(e),
- file=sys.__stderr__)
-
- data = self._flush_buffer()
- if data:
- # FIXME: this disables Session's fork-safe check,
- # since pub_thread is itself fork-safe.
- # There should be a better way to do this.
- self.session.pid = os.getpid()
- content = {u'name':self.name, u'text':data}
- self.session.send(self.pub_thread, u'stream', content=content,
- parent=self.parent_header, ident=self.topic)
-
- def write(self, string):
- if self.echo is not None:
- try:
- self.echo.write(string)
- except OSError as e:
- if self.echo is not sys.__stderr__:
- print("Write failed: {}".format(e),
- file=sys.__stderr__)
-
- if self.pub_thread is None:
- raise ValueError('I/O operation on closed file')
- else:
- # Make sure that we're handling unicode
- if not isinstance(string, unicode_type):
- string = string.decode(self.encoding, 'replace')
-
- is_child = (not self._is_master_process())
- # only touch the buffer in the IO thread to avoid races
- self.pub_thread.schedule(lambda : self._buffer.write(string))
- if is_child:
- # newlines imply flush in subprocesses
- # mp.Pool cannot be trusted to flush promptly (or ever),
- # and this helps.
- if '\n' in string:
- self.flush()
- else:
- self._schedule_flush()
-
- def writelines(self, sequence):
- if self.pub_thread is None:
- raise ValueError('I/O operation on closed file')
- else:
- for string in sequence:
- self.write(string)
-
- def writable(self):
- return True
-
- def _flush_buffer(self):
- """clear the current buffer and return the current buffer data.
-
- This should only be called in the IO thread.
- """
- data = u''
- if self._buffer is not None:
- buf = self._buffer
- self._new_buffer()
- data = buf.getvalue()
- buf.close()
- return data
-
- def _new_buffer(self):
- self._buffer = StringIO()
|