|
20 | 20 | from typing import Any |
21 | 21 |
|
22 | 22 | import zmq |
23 | | -from jupyter_client.session import extract_header |
| 23 | +from jupyter_client.session import extract_header, Session |
24 | 24 | from tornado.ioloop import IOLoop |
25 | 25 | from zmq.eventloop.zmqstream import ZMQStream |
26 | 26 |
|
@@ -73,12 +73,75 @@ def __init__(self, socket, pipe=False): |
73 | 73 | self._event_pipe_gc_seconds: float = 10 |
74 | 74 | self._event_pipe_gc_task: asyncio.Task[Any] | None = None |
75 | 75 | self._setup_event_pipe() |
| 76 | + self._setup_xpub_listener() |
76 | 77 | self.thread = threading.Thread(target=self._thread_main, name="IOPub") |
77 | 78 | self.thread.daemon = True |
78 | 79 | self.thread.pydev_do_not_trace = True # type:ignore[attr-defined] |
79 | 80 | self.thread.is_pydev_daemon_thread = True # type:ignore[attr-defined] |
80 | 81 | self.thread.name = "IOPub" |
81 | 82 |
|
| 83 | + def _setup_xpub_listener(self): |
| 84 | + """Setup listener for XPUB subscription events""" |
| 85 | + |
| 86 | + # Checks the socket is not a DummySocket |
| 87 | + if not hasattr(self.socket, 'getsockopt'): |
| 88 | + return |
| 89 | + |
| 90 | + socket_type = self.socket.getsockopt(zmq.TYPE) |
| 91 | + if socket_type == zmq.XPUB: |
| 92 | + self._xpub_stream = ZMQStream(self.socket, self.io_loop) |
| 93 | + self._xpub_stream.on_recv(self._handle_subscription) |
| 94 | + |
| 95 | + def _handle_subscription(self, frames): |
| 96 | + """Handle subscription/unsubscription events from XPUB socket |
| 97 | +
|
| 98 | + XPUB sockets receive: |
| 99 | + - subscribe: single frame with b'\\x01' + topic |
| 100 | + - unsubscribe: single frame with b'\\x00' + topic |
| 101 | + """ |
| 102 | + |
| 103 | + for frame in frames: |
| 104 | + event_type = frame[0] |
| 105 | + if event_type == 1: |
| 106 | + subscription = frame[1:] if len(frame) > 1 else b'' |
| 107 | + try: |
| 108 | + subscription_str = subscription.decode('utf-8') |
| 109 | + except UnicodeDecodeError: |
| 110 | + continue |
| 111 | + self._send_welcome_message(subscription_str) |
| 112 | + |
| 113 | + def _send_welcome_message(self, subscription): |
| 114 | + """Send iopub_welcome message for new subscription |
| 115 | +
|
| 116 | + Parameters |
| 117 | + ---------- |
| 118 | + subscription : str |
| 119 | + The subscription topic (UTF-8 decoded) |
| 120 | + """ |
| 121 | + |
| 122 | + content = { |
| 123 | + 'subscription': subscription |
| 124 | + } |
| 125 | + |
| 126 | + header = self.session.msg_header('iopub_welcome') |
| 127 | + msg = { |
| 128 | + 'header': header, |
| 129 | + 'parent_header': {}, |
| 130 | + 'metadata': {}, |
| 131 | + 'content': content, |
| 132 | + 'buffers': [], |
| 133 | + } |
| 134 | + |
| 135 | + msg_list = self.session.serialize(msg) |
| 136 | + |
| 137 | + if subscription: |
| 138 | + identity = subscription.encode('utf-8') |
| 139 | + full_msg = [identity] + msg_list |
| 140 | + else: |
| 141 | + full_msg = msg_list |
| 142 | + # Send directly on socket (we're already in IO thread context) |
| 143 | + self.socket.send_multipart(full_msg) |
| 144 | + |
82 | 145 | def _thread_main(self): |
83 | 146 | """The inner loop that's actually run in a thread""" |
84 | 147 |
|
|
0 commit comments