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.

754 lines
27 KiB

4 years ago
  1. """Base class for a kernel that talks to frontends over 0MQ."""
  2. # Copyright (c) IPython Development Team.
  3. # Distributed under the terms of the Modified BSD License.
  4. from __future__ import print_function
  5. import sys
  6. import time
  7. import logging
  8. import uuid
  9. from datetime import datetime
  10. try:
  11. # jupyter_client >= 5, use tz-aware now
  12. from jupyter_client.session import utcnow as now
  13. except ImportError:
  14. # jupyter_client < 5, use local now()
  15. now = datetime.now
  16. from signal import signal, default_int_handler, SIGINT
  17. import zmq
  18. from tornado import ioloop
  19. from zmq.eventloop.zmqstream import ZMQStream
  20. from traitlets.config.configurable import SingletonConfigurable
  21. from IPython.core.error import StdinNotImplementedError
  22. from ipython_genutils import py3compat
  23. from ipython_genutils.py3compat import unicode_type, string_types
  24. from ipykernel.jsonutil import json_clean
  25. from traitlets import (
  26. Any, Instance, Float, Dict, List, Set, Integer, Unicode, Bool, observe, default
  27. )
  28. from jupyter_client.session import Session
  29. from ._version import kernel_protocol_version
  30. class Kernel(SingletonConfigurable):
  31. #---------------------------------------------------------------------------
  32. # Kernel interface
  33. #---------------------------------------------------------------------------
  34. # attribute to override with a GUI
  35. eventloop = Any(None)
  36. @observe('eventloop')
  37. def _update_eventloop(self, change):
  38. """schedule call to eventloop from IOLoop"""
  39. loop = ioloop.IOLoop.current()
  40. if change.new is not None:
  41. loop.add_callback(self.enter_eventloop)
  42. session = Instance(Session, allow_none=True)
  43. profile_dir = Instance('IPython.core.profiledir.ProfileDir', allow_none=True)
  44. shell_streams = List()
  45. control_stream = Instance(ZMQStream, allow_none=True)
  46. iopub_socket = Any()
  47. iopub_thread = Any()
  48. stdin_socket = Any()
  49. log = Instance(logging.Logger, allow_none=True)
  50. # identities:
  51. int_id = Integer(-1)
  52. ident = Unicode()
  53. @default('ident')
  54. def _default_ident(self):
  55. return unicode_type(uuid.uuid4())
  56. # This should be overridden by wrapper kernels that implement any real
  57. # language.
  58. language_info = {}
  59. # any links that should go in the help menu
  60. help_links = List()
  61. # Private interface
  62. _darwin_app_nap = Bool(True,
  63. help="""Whether to use appnope for compatiblity with OS X App Nap.
  64. Only affects OS X >= 10.9.
  65. """
  66. ).tag(config=True)
  67. # track associations with current request
  68. _allow_stdin = Bool(False)
  69. _parent_header = Dict()
  70. _parent_ident = Any(b'')
  71. # Time to sleep after flushing the stdout/err buffers in each execute
  72. # cycle. While this introduces a hard limit on the minimal latency of the
  73. # execute cycle, it helps prevent output synchronization problems for
  74. # clients.
  75. # Units are in seconds. The minimum zmq latency on local host is probably
  76. # ~150 microseconds, set this to 500us for now. We may need to increase it
  77. # a little if it's not enough after more interactive testing.
  78. _execute_sleep = Float(0.0005).tag(config=True)
  79. # Frequency of the kernel's event loop.
  80. # Units are in seconds, kernel subclasses for GUI toolkits may need to
  81. # adapt to milliseconds.
  82. _poll_interval = Float(0.05).tag(config=True)
  83. # If the shutdown was requested over the network, we leave here the
  84. # necessary reply message so it can be sent by our registered atexit
  85. # handler. This ensures that the reply is only sent to clients truly at
  86. # the end of our shutdown process (which happens after the underlying
  87. # IPython shell's own shutdown).
  88. _shutdown_message = None
  89. # This is a dict of port number that the kernel is listening on. It is set
  90. # by record_ports and used by connect_request.
  91. _recorded_ports = Dict()
  92. # set of aborted msg_ids
  93. aborted = Set()
  94. # Track execution count here. For IPython, we override this to use the
  95. # execution count we store in the shell.
  96. execution_count = 0
  97. msg_types = [
  98. 'execute_request', 'complete_request',
  99. 'inspect_request', 'history_request',
  100. 'comm_info_request', 'kernel_info_request',
  101. 'connect_request', 'shutdown_request',
  102. 'is_complete_request',
  103. # deprecated:
  104. 'apply_request',
  105. ]
  106. # add deprecated ipyparallel control messages
  107. control_msg_types = msg_types + ['clear_request', 'abort_request']
  108. def __init__(self, **kwargs):
  109. super(Kernel, self).__init__(**kwargs)
  110. # Build dict of handlers for message types
  111. self.shell_handlers = {}
  112. for msg_type in self.msg_types:
  113. self.shell_handlers[msg_type] = getattr(self, msg_type)
  114. self.control_handlers = {}
  115. for msg_type in self.control_msg_types:
  116. self.control_handlers[msg_type] = getattr(self, msg_type)
  117. def dispatch_control(self, msg):
  118. """dispatch control requests"""
  119. idents,msg = self.session.feed_identities(msg, copy=False)
  120. try:
  121. msg = self.session.deserialize(msg, content=True, copy=False)
  122. except:
  123. self.log.error("Invalid Control Message", exc_info=True)
  124. return
  125. self.log.debug("Control received: %s", msg)
  126. # Set the parent message for side effects.
  127. self.set_parent(idents, msg)
  128. self._publish_status(u'busy')
  129. header = msg['header']
  130. msg_type = header['msg_type']
  131. handler = self.control_handlers.get(msg_type, None)
  132. if handler is None:
  133. self.log.error("UNKNOWN CONTROL MESSAGE TYPE: %r", msg_type)
  134. else:
  135. try:
  136. handler(self.control_stream, idents, msg)
  137. except Exception:
  138. self.log.error("Exception in control handler:", exc_info=True)
  139. sys.stdout.flush()
  140. sys.stderr.flush()
  141. self._publish_status(u'idle')
  142. def should_handle(self, stream, msg, idents):
  143. """Check whether a shell-channel message should be handled
  144. Allows subclasses to prevent handling of certain messages (e.g. aborted requests).
  145. """
  146. msg_id = msg['header']['msg_id']
  147. if msg_id in self.aborted:
  148. msg_type = msg['header']['msg_type']
  149. # is it safe to assume a msg_id will not be resubmitted?
  150. self.aborted.remove(msg_id)
  151. reply_type = msg_type.split('_')[0] + '_reply'
  152. status = {'status' : 'aborted'}
  153. md = {'engine' : self.ident}
  154. md.update(status)
  155. self.session.send(stream, reply_type, metadata=md,
  156. content=status, parent=msg, ident=idents)
  157. return False
  158. return True
  159. def dispatch_shell(self, stream, msg):
  160. """dispatch shell requests"""
  161. # flush control requests first
  162. if self.control_stream:
  163. self.control_stream.flush()
  164. idents,msg = self.session.feed_identities(msg, copy=False)
  165. try:
  166. msg = self.session.deserialize(msg, content=True, copy=False)
  167. except:
  168. self.log.error("Invalid Message", exc_info=True)
  169. return
  170. # Set the parent message for side effects.
  171. self.set_parent(idents, msg)
  172. self._publish_status(u'busy')
  173. msg_type = msg['header']['msg_type']
  174. # Print some info about this message and leave a '--->' marker, so it's
  175. # easier to trace visually the message chain when debugging. Each
  176. # handler prints its message at the end.
  177. self.log.debug('\n*** MESSAGE TYPE:%s***', msg_type)
  178. self.log.debug(' Content: %s\n --->\n ', msg['content'])
  179. if not self.should_handle(stream, msg, idents):
  180. return
  181. handler = self.shell_handlers.get(msg_type, None)
  182. if handler is None:
  183. self.log.warning("Unknown message type: %r", msg_type)
  184. else:
  185. self.log.debug("%s: %s", msg_type, msg)
  186. self.pre_handler_hook()
  187. try:
  188. handler(stream, idents, msg)
  189. except Exception:
  190. self.log.error("Exception in message handler:", exc_info=True)
  191. finally:
  192. self.post_handler_hook()
  193. sys.stdout.flush()
  194. sys.stderr.flush()
  195. self._publish_status(u'idle')
  196. def pre_handler_hook(self):
  197. """Hook to execute before calling message handler"""
  198. # ensure default_int_handler during handler call
  199. self.saved_sigint_handler = signal(SIGINT, default_int_handler)
  200. def post_handler_hook(self):
  201. """Hook to execute after calling message handler"""
  202. signal(SIGINT, self.saved_sigint_handler)
  203. def enter_eventloop(self):
  204. """enter eventloop"""
  205. self.log.info("entering eventloop %s", self.eventloop)
  206. for stream in self.shell_streams:
  207. # flush any pending replies,
  208. # which may be skipped by entering the eventloop
  209. stream.flush(zmq.POLLOUT)
  210. # restore default_int_handler
  211. self.pre_handler_hook()
  212. while self.eventloop is not None:
  213. try:
  214. self.eventloop(self)
  215. except KeyboardInterrupt:
  216. # Ctrl-C shouldn't crash the kernel
  217. self.log.error("KeyboardInterrupt caught in kernel")
  218. continue
  219. else:
  220. # eventloop exited cleanly, this means we should stop (right?)
  221. self.eventloop = None
  222. break
  223. self.post_handler_hook()
  224. self.log.info("exiting eventloop")
  225. def start(self):
  226. """register dispatchers for streams"""
  227. self.io_loop = ioloop.IOLoop.current()
  228. if self.control_stream:
  229. self.control_stream.on_recv(self.dispatch_control, copy=False)
  230. def make_dispatcher(stream):
  231. def dispatcher(msg):
  232. return self.dispatch_shell(stream, msg)
  233. return dispatcher
  234. for s in self.shell_streams:
  235. s.on_recv(make_dispatcher(s), copy=False)
  236. # publish idle status
  237. self._publish_status('starting')
  238. def do_one_iteration(self):
  239. """step eventloop just once"""
  240. if self.control_stream:
  241. self.control_stream.flush()
  242. for stream in self.shell_streams:
  243. # handle at most one request per iteration
  244. stream.flush(zmq.POLLIN, 1)
  245. stream.flush(zmq.POLLOUT)
  246. def record_ports(self, ports):
  247. """Record the ports that this kernel is using.
  248. The creator of the Kernel instance must call this methods if they
  249. want the :meth:`connect_request` method to return the port numbers.
  250. """
  251. self._recorded_ports = ports
  252. #---------------------------------------------------------------------------
  253. # Kernel request handlers
  254. #---------------------------------------------------------------------------
  255. def _publish_execute_input(self, code, parent, execution_count):
  256. """Publish the code request on the iopub stream."""
  257. self.session.send(self.iopub_socket, u'execute_input',
  258. {u'code':code, u'execution_count': execution_count},
  259. parent=parent, ident=self._topic('execute_input')
  260. )
  261. def _publish_status(self, status, parent=None):
  262. """send status (busy/idle) on IOPub"""
  263. self.session.send(self.iopub_socket,
  264. u'status',
  265. {u'execution_state': status},
  266. parent=parent or self._parent_header,
  267. ident=self._topic('status'),
  268. )
  269. def set_parent(self, ident, parent):
  270. """Set the current parent_header
  271. Side effects (IOPub messages) and replies are associated with
  272. the request that caused them via the parent_header.
  273. The parent identity is used to route input_request messages
  274. on the stdin channel.
  275. """
  276. self._parent_ident = ident
  277. self._parent_header = parent
  278. def send_response(self, stream, msg_or_type, content=None, ident=None,
  279. buffers=None, track=False, header=None, metadata=None):
  280. """Send a response to the message we're currently processing.
  281. This accepts all the parameters of :meth:`jupyter_client.session.Session.send`
  282. except ``parent``.
  283. This relies on :meth:`set_parent` having been called for the current
  284. message.
  285. """
  286. return self.session.send(stream, msg_or_type, content, self._parent_header,
  287. ident, buffers, track, header, metadata)
  288. def init_metadata(self, parent):
  289. """Initialize metadata.
  290. Run at the beginning of execution requests.
  291. """
  292. # FIXME: `started` is part of ipyparallel
  293. # Remove for ipykernel 5.0
  294. return {
  295. 'started': now(),
  296. }
  297. def finish_metadata(self, parent, metadata, reply_content):
  298. """Finish populating metadata.
  299. Run after completing an execution request.
  300. """
  301. return metadata
  302. def execute_request(self, stream, ident, parent):
  303. """handle an execute_request"""
  304. try:
  305. content = parent[u'content']
  306. code = py3compat.cast_unicode_py2(content[u'code'])
  307. silent = content[u'silent']
  308. store_history = content.get(u'store_history', not silent)
  309. user_expressions = content.get('user_expressions', {})
  310. allow_stdin = content.get('allow_stdin', False)
  311. except:
  312. self.log.error("Got bad msg: ")
  313. self.log.error("%s", parent)
  314. return
  315. stop_on_error = content.get('stop_on_error', True)
  316. metadata = self.init_metadata(parent)
  317. # Re-broadcast our input for the benefit of listening clients, and
  318. # start computing output
  319. if not silent:
  320. self.execution_count += 1
  321. self._publish_execute_input(code, parent, self.execution_count)
  322. reply_content = self.do_execute(code, silent, store_history,
  323. user_expressions, allow_stdin)
  324. # Flush output before sending the reply.
  325. sys.stdout.flush()
  326. sys.stderr.flush()
  327. # FIXME: on rare occasions, the flush doesn't seem to make it to the
  328. # clients... This seems to mitigate the problem, but we definitely need
  329. # to better understand what's going on.
  330. if self._execute_sleep:
  331. time.sleep(self._execute_sleep)
  332. # Send the reply.
  333. reply_content = json_clean(reply_content)
  334. metadata = self.finish_metadata(parent, metadata, reply_content)
  335. reply_msg = self.session.send(stream, u'execute_reply',
  336. reply_content, parent, metadata=metadata,
  337. ident=ident)
  338. self.log.debug("%s", reply_msg)
  339. if not silent and reply_msg['content']['status'] == u'error' and stop_on_error:
  340. self._abort_queues()
  341. def do_execute(self, code, silent, store_history=True,
  342. user_expressions=None, allow_stdin=False):
  343. """Execute user code. Must be overridden by subclasses.
  344. """
  345. raise NotImplementedError
  346. def complete_request(self, stream, ident, parent):
  347. content = parent['content']
  348. code = content['code']
  349. cursor_pos = content['cursor_pos']
  350. matches = self.do_complete(code, cursor_pos)
  351. matches = json_clean(matches)
  352. completion_msg = self.session.send(stream, 'complete_reply',
  353. matches, parent, ident)
  354. def do_complete(self, code, cursor_pos):
  355. """Override in subclasses to find completions.
  356. """
  357. return {'matches' : [],
  358. 'cursor_end' : cursor_pos,
  359. 'cursor_start' : cursor_pos,
  360. 'metadata' : {},
  361. 'status' : 'ok'}
  362. def inspect_request(self, stream, ident, parent):
  363. content = parent['content']
  364. reply_content = self.do_inspect(content['code'], content['cursor_pos'],
  365. content.get('detail_level', 0))
  366. # Before we send this object over, we scrub it for JSON usage
  367. reply_content = json_clean(reply_content)
  368. msg = self.session.send(stream, 'inspect_reply',
  369. reply_content, parent, ident)
  370. self.log.debug("%s", msg)
  371. def do_inspect(self, code, cursor_pos, detail_level=0):
  372. """Override in subclasses to allow introspection.
  373. """
  374. return {'status': 'ok', 'data': {}, 'metadata': {}, 'found': False}
  375. def history_request(self, stream, ident, parent):
  376. content = parent['content']
  377. reply_content = self.do_history(**content)
  378. reply_content = json_clean(reply_content)
  379. msg = self.session.send(stream, 'history_reply',
  380. reply_content, parent, ident)
  381. self.log.debug("%s", msg)
  382. def do_history(self, hist_access_type, output, raw, session=None, start=None,
  383. stop=None, n=None, pattern=None, unique=False):
  384. """Override in subclasses to access history.
  385. """
  386. return {'status': 'ok', 'history': []}
  387. def connect_request(self, stream, ident, parent):
  388. if self._recorded_ports is not None:
  389. content = self._recorded_ports.copy()
  390. else:
  391. content = {}
  392. content['status'] = 'ok'
  393. msg = self.session.send(stream, 'connect_reply',
  394. content, parent, ident)
  395. self.log.debug("%s", msg)
  396. @property
  397. def kernel_info(self):
  398. return {
  399. 'protocol_version': kernel_protocol_version,
  400. 'implementation': self.implementation,
  401. 'implementation_version': self.implementation_version,
  402. 'language_info': self.language_info,
  403. 'banner': self.banner,
  404. 'help_links': self.help_links,
  405. }
  406. def kernel_info_request(self, stream, ident, parent):
  407. content = {'status': 'ok'}
  408. content.update(self.kernel_info)
  409. msg = self.session.send(stream, 'kernel_info_reply',
  410. content, parent, ident)
  411. self.log.debug("%s", msg)
  412. def comm_info_request(self, stream, ident, parent):
  413. content = parent['content']
  414. target_name = content.get('target_name', None)
  415. # Should this be moved to ipkernel?
  416. if hasattr(self, 'comm_manager'):
  417. comms = {
  418. k: dict(target_name=v.target_name)
  419. for (k, v) in self.comm_manager.comms.items()
  420. if v.target_name == target_name or target_name is None
  421. }
  422. else:
  423. comms = {}
  424. reply_content = dict(comms=comms, status='ok')
  425. msg = self.session.send(stream, 'comm_info_reply',
  426. reply_content, parent, ident)
  427. self.log.debug("%s", msg)
  428. def shutdown_request(self, stream, ident, parent):
  429. content = self.do_shutdown(parent['content']['restart'])
  430. self.session.send(stream, u'shutdown_reply', content, parent, ident=ident)
  431. # same content, but different msg_id for broadcasting on IOPub
  432. self._shutdown_message = self.session.msg(u'shutdown_reply',
  433. content, parent
  434. )
  435. self._at_shutdown()
  436. # call sys.exit after a short delay
  437. loop = ioloop.IOLoop.current()
  438. loop.add_timeout(time.time()+0.1, loop.stop)
  439. def do_shutdown(self, restart):
  440. """Override in subclasses to do things when the frontend shuts down the
  441. kernel.
  442. """
  443. return {'status': 'ok', 'restart': restart}
  444. def is_complete_request(self, stream, ident, parent):
  445. content = parent['content']
  446. code = content['code']
  447. reply_content = self.do_is_complete(code)
  448. reply_content = json_clean(reply_content)
  449. reply_msg = self.session.send(stream, 'is_complete_reply',
  450. reply_content, parent, ident)
  451. self.log.debug("%s", reply_msg)
  452. def do_is_complete(self, code):
  453. """Override in subclasses to find completions.
  454. """
  455. return {'status' : 'unknown',
  456. }
  457. #---------------------------------------------------------------------------
  458. # Engine methods (DEPRECATED)
  459. #---------------------------------------------------------------------------
  460. def apply_request(self, stream, ident, parent):
  461. self.log.warning("apply_request is deprecated in kernel_base, moving to ipyparallel.")
  462. try:
  463. content = parent[u'content']
  464. bufs = parent[u'buffers']
  465. msg_id = parent['header']['msg_id']
  466. except:
  467. self.log.error("Got bad msg: %s", parent, exc_info=True)
  468. return
  469. md = self.init_metadata(parent)
  470. reply_content, result_buf = self.do_apply(content, bufs, msg_id, md)
  471. # flush i/o
  472. sys.stdout.flush()
  473. sys.stderr.flush()
  474. md = self.finish_metadata(parent, md, reply_content)
  475. self.session.send(stream, u'apply_reply', reply_content,
  476. parent=parent, ident=ident,buffers=result_buf, metadata=md)
  477. def do_apply(self, content, bufs, msg_id, reply_metadata):
  478. """DEPRECATED"""
  479. raise NotImplementedError
  480. #---------------------------------------------------------------------------
  481. # Control messages (DEPRECATED)
  482. #---------------------------------------------------------------------------
  483. def abort_request(self, stream, ident, parent):
  484. """abort a specific msg by id"""
  485. self.log.warning("abort_request is deprecated in kernel_base. It os only part of IPython parallel")
  486. msg_ids = parent['content'].get('msg_ids', None)
  487. if isinstance(msg_ids, string_types):
  488. msg_ids = [msg_ids]
  489. if not msg_ids:
  490. self._abort_queues()
  491. for mid in msg_ids:
  492. self.aborted.add(str(mid))
  493. content = dict(status='ok')
  494. reply_msg = self.session.send(stream, 'abort_reply', content=content,
  495. parent=parent, ident=ident)
  496. self.log.debug("%s", reply_msg)
  497. def clear_request(self, stream, idents, parent):
  498. """Clear our namespace."""
  499. self.log.warning("clear_request is deprecated in kernel_base. It os only part of IPython parallel")
  500. content = self.do_clear()
  501. self.session.send(stream, 'clear_reply', ident=idents, parent=parent,
  502. content = content)
  503. def do_clear(self):
  504. """DEPRECATED"""
  505. raise NotImplementedError
  506. #---------------------------------------------------------------------------
  507. # Protected interface
  508. #---------------------------------------------------------------------------
  509. def _topic(self, topic):
  510. """prefixed topic for IOPub messages"""
  511. base = "kernel.%s" % self.ident
  512. return py3compat.cast_bytes("%s.%s" % (base, topic))
  513. def _abort_queues(self):
  514. for stream in self.shell_streams:
  515. if stream:
  516. self._abort_queue(stream)
  517. def _abort_queue(self, stream):
  518. poller = zmq.Poller()
  519. poller.register(stream.socket, zmq.POLLIN)
  520. while True:
  521. idents,msg = self.session.recv(stream, zmq.NOBLOCK, content=True)
  522. if msg is None:
  523. return
  524. self.log.info("Aborting:")
  525. self.log.info("%s", msg)
  526. msg_type = msg['header']['msg_type']
  527. reply_type = msg_type.split('_')[0] + '_reply'
  528. status = {'status' : 'aborted'}
  529. md = {'engine' : self.ident}
  530. md.update(status)
  531. self._publish_status('busy', parent=msg)
  532. reply_msg = self.session.send(stream, reply_type, metadata=md,
  533. content=status, parent=msg, ident=idents)
  534. self._publish_status('idle', parent=msg)
  535. self.log.debug("%s", reply_msg)
  536. # We need to wait a bit for requests to come in. This can probably
  537. # be set shorter for true asynchronous clients.
  538. poller.poll(50)
  539. def _no_raw_input(self):
  540. """Raise StdinNotImplentedError if active frontend doesn't support
  541. stdin."""
  542. raise StdinNotImplementedError("raw_input was called, but this "
  543. "frontend does not support stdin.")
  544. def getpass(self, prompt='', stream=None):
  545. """Forward getpass to frontends
  546. Raises
  547. ------
  548. StdinNotImplentedError if active frontend doesn't support stdin.
  549. """
  550. if not self._allow_stdin:
  551. raise StdinNotImplementedError(
  552. "getpass was called, but this frontend does not support input requests."
  553. )
  554. if stream is not None:
  555. import warnings
  556. warnings.warn("The `stream` parameter of `getpass.getpass` will have no effect when using ipykernel",
  557. UserWarning, stacklevel=2)
  558. return self._input_request(prompt,
  559. self._parent_ident,
  560. self._parent_header,
  561. password=True,
  562. )
  563. def raw_input(self, prompt=''):
  564. """Forward raw_input to frontends
  565. Raises
  566. ------
  567. StdinNotImplentedError if active frontend doesn't support stdin.
  568. """
  569. if not self._allow_stdin:
  570. raise StdinNotImplementedError(
  571. "raw_input was called, but this frontend does not support input requests."
  572. )
  573. return self._input_request(str(prompt),
  574. self._parent_ident,
  575. self._parent_header,
  576. password=False,
  577. )
  578. def _input_request(self, prompt, ident, parent, password=False):
  579. # Flush output before making the request.
  580. sys.stderr.flush()
  581. sys.stdout.flush()
  582. # flush the stdin socket, to purge stale replies
  583. while True:
  584. try:
  585. self.stdin_socket.recv_multipart(zmq.NOBLOCK)
  586. except zmq.ZMQError as e:
  587. if e.errno == zmq.EAGAIN:
  588. break
  589. else:
  590. raise
  591. # Send the input request.
  592. content = json_clean(dict(prompt=prompt, password=password))
  593. self.session.send(self.stdin_socket, u'input_request', content, parent,
  594. ident=ident)
  595. # Await a response.
  596. while True:
  597. try:
  598. ident, reply = self.session.recv(self.stdin_socket, 0)
  599. except Exception:
  600. self.log.warning("Invalid Message:", exc_info=True)
  601. except KeyboardInterrupt:
  602. # re-raise KeyboardInterrupt, to truncate traceback
  603. raise KeyboardInterrupt
  604. else:
  605. break
  606. try:
  607. value = py3compat.unicode_to_str(reply['content']['value'])
  608. except:
  609. self.log.error("Bad input_reply: %s", parent)
  610. value = ''
  611. if value == '\x04':
  612. # EOF
  613. raise EOFError
  614. return value
  615. def _at_shutdown(self):
  616. """Actions taken at shutdown by the kernel, called by python's atexit.
  617. """
  618. # io.rprint("Kernel at_shutdown") # dbg
  619. if self._shutdown_message is not None:
  620. self.session.send(self.iopub_socket, self._shutdown_message, ident=self._topic('shutdown'))
  621. self.log.debug("%s", self._shutdown_message)
  622. [ s.flush(zmq.POLLOUT) for s in self.shell_streams ]