##// END OF EJS Templates
Implementing kernel status messages.
Brian Granger -
Show More
@@ -1,549 +1,554 b''
1 1 from __future__ import print_function
2 2
3 3 # Standard library imports
4 4 from collections import namedtuple
5 5 import sys
6 6
7 7 # System library imports
8 8 from pygments.lexers import PythonLexer
9 9 from PyQt4 import QtCore, QtGui
10 10
11 11 # Local imports
12 12 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
13 13 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
14 14 from IPython.utils.traitlets import Bool
15 15 from bracket_matcher import BracketMatcher
16 16 from call_tip_widget import CallTipWidget
17 17 from completion_lexer import CompletionLexer
18 18 from history_console_widget import HistoryConsoleWidget
19 19 from pygments_highlighter import PygmentsHighlighter
20 20
21 21
22 22 class FrontendHighlighter(PygmentsHighlighter):
23 23 """ A PygmentsHighlighter that can be turned on and off and that ignores
24 24 prompts.
25 25 """
26 26
27 27 def __init__(self, frontend):
28 28 super(FrontendHighlighter, self).__init__(frontend._control.document())
29 29 self._current_offset = 0
30 30 self._frontend = frontend
31 31 self.highlighting_on = False
32 32
33 33 def highlightBlock(self, qstring):
34 34 """ Highlight a block of text. Reimplemented to highlight selectively.
35 35 """
36 36 if not self.highlighting_on:
37 37 return
38 38
39 39 # The input to this function is unicode string that may contain
40 40 # paragraph break characters, non-breaking spaces, etc. Here we acquire
41 41 # the string as plain text so we can compare it.
42 42 current_block = self.currentBlock()
43 43 string = self._frontend._get_block_plain_text(current_block)
44 44
45 45 # Decide whether to check for the regular or continuation prompt.
46 46 if current_block.contains(self._frontend._prompt_pos):
47 47 prompt = self._frontend._prompt
48 48 else:
49 49 prompt = self._frontend._continuation_prompt
50 50
51 51 # Don't highlight the part of the string that contains the prompt.
52 52 if string.startswith(prompt):
53 53 self._current_offset = len(prompt)
54 54 qstring.remove(0, len(prompt))
55 55 else:
56 56 self._current_offset = 0
57 57
58 58 PygmentsHighlighter.highlightBlock(self, qstring)
59 59
60 60 def rehighlightBlock(self, block):
61 61 """ Reimplemented to temporarily enable highlighting if disabled.
62 62 """
63 63 old = self.highlighting_on
64 64 self.highlighting_on = True
65 65 super(FrontendHighlighter, self).rehighlightBlock(block)
66 66 self.highlighting_on = old
67 67
68 68 def setFormat(self, start, count, format):
69 69 """ Reimplemented to highlight selectively.
70 70 """
71 71 start += self._current_offset
72 72 PygmentsHighlighter.setFormat(self, start, count, format)
73 73
74 74
75 75 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
76 76 """ A Qt frontend for a generic Python kernel.
77 77 """
78 78
79 79 # An option and corresponding signal for overriding the default kernel
80 80 # interrupt behavior.
81 81 custom_interrupt = Bool(False)
82 82 custom_interrupt_requested = QtCore.pyqtSignal()
83 83
84 84 # An option and corresponding signals for overriding the default kernel
85 85 # restart behavior.
86 86 custom_restart = Bool(False)
87 87 custom_restart_kernel_died = QtCore.pyqtSignal(float)
88 88 custom_restart_requested = QtCore.pyqtSignal()
89 89
90 90 # Emitted when an 'execute_reply' has been received from the kernel and
91 91 # processed by the FrontendWidget.
92 92 executed = QtCore.pyqtSignal(object)
93 93
94 94 # Emitted when an exit request has been received from the kernel.
95 95 exit_requested = QtCore.pyqtSignal()
96 96
97 97 # Protected class variables.
98 98 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
99 99 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
100 100 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
101 101 _input_splitter_class = InputSplitter
102 102
103 103 #---------------------------------------------------------------------------
104 104 # 'object' interface
105 105 #---------------------------------------------------------------------------
106 106
107 107 def __init__(self, *args, **kw):
108 108 super(FrontendWidget, self).__init__(*args, **kw)
109 109
110 110 # FrontendWidget protected variables.
111 111 self._bracket_matcher = BracketMatcher(self._control)
112 112 self._call_tip_widget = CallTipWidget(self._control)
113 113 self._completion_lexer = CompletionLexer(PythonLexer())
114 114 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
115 115 self._hidden = False
116 116 self._highlighter = FrontendHighlighter(self)
117 117 self._input_splitter = self._input_splitter_class(input_mode='cell')
118 118 self._kernel_manager = None
119 119 self._request_info = {}
120 120
121 121 # Configure the ConsoleWidget.
122 122 self.tab_width = 4
123 123 self._set_continuation_prompt('... ')
124 124
125 125 # Configure the CallTipWidget.
126 126 self._call_tip_widget.setFont(self.font)
127 127 self.font_changed.connect(self._call_tip_widget.setFont)
128 128
129 129 # Configure actions.
130 130 action = self._copy_raw_action
131 131 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
132 132 action.setEnabled(False)
133 133 action.setShortcut(QtGui.QKeySequence(key))
134 134 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
135 135 action.triggered.connect(self.copy_raw)
136 136 self.copy_available.connect(action.setEnabled)
137 137 self.addAction(action)
138 138
139 139 # Connect signal handlers.
140 140 document = self._control.document()
141 141 document.contentsChange.connect(self._document_contents_change)
142 142
143 143 #---------------------------------------------------------------------------
144 144 # 'ConsoleWidget' public interface
145 145 #---------------------------------------------------------------------------
146 146
147 147 def copy(self):
148 148 """ Copy the currently selected text to the clipboard, removing prompts.
149 149 """
150 150 text = unicode(self._control.textCursor().selection().toPlainText())
151 151 if text:
152 152 lines = map(transform_classic_prompt, text.splitlines())
153 153 text = '\n'.join(lines)
154 154 QtGui.QApplication.clipboard().setText(text)
155 155
156 156 #---------------------------------------------------------------------------
157 157 # 'ConsoleWidget' abstract interface
158 158 #---------------------------------------------------------------------------
159 159
160 160 def _is_complete(self, source, interactive):
161 161 """ Returns whether 'source' can be completely processed and a new
162 162 prompt created. When triggered by an Enter/Return key press,
163 163 'interactive' is True; otherwise, it is False.
164 164 """
165 165 complete = self._input_splitter.push(source)
166 166 if interactive:
167 167 complete = not self._input_splitter.push_accepts_more()
168 168 return complete
169 169
170 170 def _execute(self, source, hidden):
171 171 """ Execute 'source'. If 'hidden', do not show any output.
172 172
173 173 See parent class :meth:`execute` docstring for full details.
174 174 """
175 175 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
176 176 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
177 177 self._hidden = hidden
178 178
179 179 def _prompt_started_hook(self):
180 180 """ Called immediately after a new prompt is displayed.
181 181 """
182 182 if not self._reading:
183 183 self._highlighter.highlighting_on = True
184 184
185 185 def _prompt_finished_hook(self):
186 186 """ Called immediately after a prompt is finished, i.e. when some input
187 187 will be processed and a new prompt displayed.
188 188 """
189 189 if not self._reading:
190 190 self._highlighter.highlighting_on = False
191 191
192 192 def _tab_pressed(self):
193 193 """ Called when the tab key is pressed. Returns whether to continue
194 194 processing the event.
195 195 """
196 196 # Perform tab completion if:
197 197 # 1) The cursor is in the input buffer.
198 198 # 2) There is a non-whitespace character before the cursor.
199 199 text = self._get_input_buffer_cursor_line()
200 200 if text is None:
201 201 return False
202 202 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
203 203 if complete:
204 204 self._complete()
205 205 return not complete
206 206
207 207 #---------------------------------------------------------------------------
208 208 # 'ConsoleWidget' protected interface
209 209 #---------------------------------------------------------------------------
210 210
211 211 def _context_menu_make(self, pos):
212 212 """ Reimplemented to add an action for raw copy.
213 213 """
214 214 menu = super(FrontendWidget, self)._context_menu_make(pos)
215 215 for before_action in menu.actions():
216 216 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
217 217 QtGui.QKeySequence.ExactMatch:
218 218 menu.insertAction(before_action, self._copy_raw_action)
219 219 break
220 220 return menu
221 221
222 222 def _event_filter_console_keypress(self, event):
223 223 """ Reimplemented for execution interruption and smart backspace.
224 224 """
225 225 key = event.key()
226 226 if self._control_key_down(event.modifiers(), include_command=False):
227 227
228 228 if key == QtCore.Qt.Key_C and self._executing:
229 229 self.interrupt_kernel()
230 230 return True
231 231
232 232 elif key == QtCore.Qt.Key_Period:
233 233 message = 'Are you sure you want to restart the kernel?'
234 234 self.restart_kernel(message, now=False)
235 235 return True
236 236
237 237 elif not event.modifiers() & QtCore.Qt.AltModifier:
238 238
239 239 # Smart backspace: remove four characters in one backspace if:
240 240 # 1) everything left of the cursor is whitespace
241 241 # 2) the four characters immediately left of the cursor are spaces
242 242 if key == QtCore.Qt.Key_Backspace:
243 243 col = self._get_input_buffer_cursor_column()
244 244 cursor = self._control.textCursor()
245 245 if col > 3 and not cursor.hasSelection():
246 246 text = self._get_input_buffer_cursor_line()[:col]
247 247 if text.endswith(' ') and not text.strip():
248 248 cursor.movePosition(QtGui.QTextCursor.Left,
249 249 QtGui.QTextCursor.KeepAnchor, 4)
250 250 cursor.removeSelectedText()
251 251 return True
252 252
253 253 return super(FrontendWidget, self)._event_filter_console_keypress(event)
254 254
255 255 def _insert_continuation_prompt(self, cursor):
256 256 """ Reimplemented for auto-indentation.
257 257 """
258 258 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
259 259 cursor.insertText(' ' * self._input_splitter.indent_spaces)
260 260
261 261 #---------------------------------------------------------------------------
262 262 # 'BaseFrontendMixin' abstract interface
263 263 #---------------------------------------------------------------------------
264 264
265 265 def _handle_complete_reply(self, rep):
266 266 """ Handle replies for tab completion.
267 267 """
268 268 cursor = self._get_cursor()
269 269 info = self._request_info.get('complete')
270 270 if info and info.id == rep['parent_header']['msg_id'] and \
271 271 info.pos == cursor.position():
272 272 text = '.'.join(self._get_context())
273 273 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
274 274 self._complete_with_items(cursor, rep['content']['matches'])
275 275
276 276 def _handle_execute_reply(self, msg):
277 277 """ Handles replies for code execution.
278 278 """
279 279 info = self._request_info.get('execute')
280 280 if info and info.id == msg['parent_header']['msg_id'] and \
281 281 info.kind == 'user' and not self._hidden:
282 282 # Make sure that all output from the SUB channel has been processed
283 283 # before writing a new prompt.
284 284 self.kernel_manager.sub_channel.flush()
285 285
286 286 # Reset the ANSI style information to prevent bad text in stdout
287 287 # from messing up our colors. We're not a true terminal so we're
288 288 # allowed to do this.
289 289 if self.ansi_codes:
290 290 self._ansi_processor.reset_sgr()
291 291
292 292 content = msg['content']
293 293 status = content['status']
294 294 if status == 'ok':
295 295 self._process_execute_ok(msg)
296 296 elif status == 'error':
297 297 self._process_execute_error(msg)
298 298 elif status == 'abort':
299 299 self._process_execute_abort(msg)
300 300
301 301 self._show_interpreter_prompt_for_reply(msg)
302 302 self.executed.emit(msg)
303 303
304 304 def _handle_input_request(self, msg):
305 305 """ Handle requests for raw_input.
306 306 """
307 307 if self._hidden:
308 308 raise RuntimeError('Request for raw input during hidden execution.')
309 309
310 310 # Make sure that all output from the SUB channel has been processed
311 311 # before entering readline mode.
312 312 self.kernel_manager.sub_channel.flush()
313 313
314 314 def callback(line):
315 315 self.kernel_manager.rep_channel.input(line)
316 316 self._readline(msg['content']['prompt'], callback=callback)
317 317
318 318 def _handle_kernel_died(self, since_last_heartbeat):
319 319 """ Handle the kernel's death by asking if the user wants to restart.
320 320 """
321 321 if self.custom_restart:
322 322 self.custom_restart_kernel_died.emit(since_last_heartbeat)
323 323 else:
324 324 message = 'The kernel heartbeat has been inactive for %.2f ' \
325 325 'seconds. Do you want to restart the kernel? You may ' \
326 326 'first want to check the network connection.' % \
327 327 since_last_heartbeat
328 328 self.restart_kernel(message, now=True)
329 329
330 330 def _handle_object_info_reply(self, rep):
331 331 """ Handle replies for call tips.
332 332 """
333 333 cursor = self._get_cursor()
334 334 info = self._request_info.get('call_tip')
335 335 if info and info.id == rep['parent_header']['msg_id'] and \
336 336 info.pos == cursor.position():
337 337 doc = rep['content']['docstring']
338 338 if doc:
339 339 self._call_tip_widget.show_docstring(doc)
340 340
341 341 def _handle_pyout(self, msg):
342 342 """ Handle display hook output.
343 343 """
344 344 if not self._hidden and self._is_from_this_session(msg):
345 345 self._append_plain_text(msg['content']['data'] + '\n')
346 346
347 347 def _handle_stream(self, msg):
348 348 """ Handle stdout, stderr, and stdin.
349 349 """
350 350 if not self._hidden and self._is_from_this_session(msg):
351 351 # Most consoles treat tabs as being 8 space characters. Convert tabs
352 352 # to spaces so that output looks as expected regardless of this
353 353 # widget's tab width.
354 354 text = msg['content']['data'].expandtabs(8)
355 355
356 356 self._append_plain_text(text)
357 357 self._control.moveCursor(QtGui.QTextCursor.End)
358
358
359 def _handle_status(self, msg):
360 """ Handle kernel status messages.
361 """
362 pass
363
359 364 def _started_channels(self):
360 365 """ Called when the KernelManager channels have started listening or
361 366 when the frontend is assigned an already listening KernelManager.
362 367 """
363 368 self.reset()
364 369
365 370 #---------------------------------------------------------------------------
366 371 # 'FrontendWidget' public interface
367 372 #---------------------------------------------------------------------------
368 373
369 374 def copy_raw(self):
370 375 """ Copy the currently selected text to the clipboard without attempting
371 376 to remove prompts or otherwise alter the text.
372 377 """
373 378 self._control.copy()
374 379
375 380 def execute_file(self, path, hidden=False):
376 381 """ Attempts to execute file with 'path'. If 'hidden', no output is
377 382 shown.
378 383 """
379 384 self.execute('execfile("%s")' % path, hidden=hidden)
380 385
381 386 def interrupt_kernel(self):
382 387 """ Attempts to interrupt the running kernel.
383 388 """
384 389 if self.custom_interrupt:
385 390 self.custom_interrupt_requested.emit()
386 391 elif self.kernel_manager.has_kernel:
387 392 self.kernel_manager.interrupt_kernel()
388 393 else:
389 394 self._append_plain_text('Kernel process is either remote or '
390 395 'unspecified. Cannot interrupt.\n')
391 396
392 397 def reset(self):
393 398 """ Resets the widget to its initial state. Similar to ``clear``, but
394 399 also re-writes the banner and aborts execution if necessary.
395 400 """
396 401 if self._executing:
397 402 self._executing = False
398 403 self._request_info['execute'] = None
399 404 self._reading = False
400 405 self._highlighter.highlighting_on = False
401 406
402 407 self._control.clear()
403 408 self._append_plain_text(self._get_banner())
404 409 self._show_interpreter_prompt()
405 410
406 411 def restart_kernel(self, message, now=False):
407 412 """ Attempts to restart the running kernel.
408 413 """
409 414 # FIXME: now should be configurable via a checkbox in the dialog. Right
410 415 # now at least the heartbeat path sets it to True and the manual restart
411 416 # to False. But those should just be the pre-selected states of a
412 417 # checkbox that the user could override if so desired. But I don't know
413 418 # enough Qt to go implementing the checkbox now.
414 419
415 420 if self.custom_restart:
416 421 self.custom_restart_requested.emit()
417 422
418 423 elif self.kernel_manager.has_kernel:
419 424 # Pause the heart beat channel to prevent further warnings.
420 425 self.kernel_manager.hb_channel.pause()
421 426
422 427 # Prompt the user to restart the kernel. Un-pause the heartbeat if
423 428 # they decline. (If they accept, the heartbeat will be un-paused
424 429 # automatically when the kernel is restarted.)
425 430 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
426 431 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
427 432 message, buttons)
428 433 if result == QtGui.QMessageBox.Yes:
429 434 try:
430 435 self.kernel_manager.restart_kernel(now=now)
431 436 except RuntimeError:
432 437 self._append_plain_text('Kernel started externally. '
433 438 'Cannot restart.\n')
434 439 else:
435 440 self.reset()
436 441 else:
437 442 self.kernel_manager.hb_channel.unpause()
438 443
439 444 else:
440 445 self._append_plain_text('Kernel process is either remote or '
441 446 'unspecified. Cannot restart.\n')
442 447
443 448 #---------------------------------------------------------------------------
444 449 # 'FrontendWidget' protected interface
445 450 #---------------------------------------------------------------------------
446 451
447 452 def _call_tip(self):
448 453 """ Shows a call tip, if appropriate, at the current cursor location.
449 454 """
450 455 # Decide if it makes sense to show a call tip
451 456 cursor = self._get_cursor()
452 457 cursor.movePosition(QtGui.QTextCursor.Left)
453 458 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
454 459 return False
455 460 context = self._get_context(cursor)
456 461 if not context:
457 462 return False
458 463
459 464 # Send the metadata request to the kernel
460 465 name = '.'.join(context)
461 466 msg_id = self.kernel_manager.xreq_channel.object_info(name)
462 467 pos = self._get_cursor().position()
463 468 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
464 469 return True
465 470
466 471 def _complete(self):
467 472 """ Performs completion at the current cursor location.
468 473 """
469 474 context = self._get_context()
470 475 if context:
471 476 # Send the completion request to the kernel
472 477 msg_id = self.kernel_manager.xreq_channel.complete(
473 478 '.'.join(context), # text
474 479 self._get_input_buffer_cursor_line(), # line
475 480 self._get_input_buffer_cursor_column(), # cursor_pos
476 481 self.input_buffer) # block
477 482 pos = self._get_cursor().position()
478 483 info = self._CompletionRequest(msg_id, pos)
479 484 self._request_info['complete'] = info
480 485
481 486 def _get_banner(self):
482 487 """ Gets a banner to display at the beginning of a session.
483 488 """
484 489 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
485 490 '"license" for more information.'
486 491 return banner % (sys.version, sys.platform)
487 492
488 493 def _get_context(self, cursor=None):
489 494 """ Gets the context for the specified cursor (or the current cursor
490 495 if none is specified).
491 496 """
492 497 if cursor is None:
493 498 cursor = self._get_cursor()
494 499 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
495 500 QtGui.QTextCursor.KeepAnchor)
496 501 text = unicode(cursor.selection().toPlainText())
497 502 return self._completion_lexer.get_context(text)
498 503
499 504 def _process_execute_abort(self, msg):
500 505 """ Process a reply for an aborted execution request.
501 506 """
502 507 self._append_plain_text("ERROR: execution aborted\n")
503 508
504 509 def _process_execute_error(self, msg):
505 510 """ Process a reply for an execution request that resulted in an error.
506 511 """
507 512 content = msg['content']
508 513 traceback = ''.join(content['traceback'])
509 514 self._append_plain_text(traceback)
510 515
511 516 def _process_execute_ok(self, msg):
512 517 """ Process a reply for a successful execution equest.
513 518 """
514 519 payload = msg['content']['payload']
515 520 for item in payload:
516 521 if not self._process_execute_payload(item):
517 522 warning = 'Warning: received unknown payload of type %s'
518 523 print(warning % repr(item['source']))
519 524
520 525 def _process_execute_payload(self, item):
521 526 """ Process a single payload item from the list of payload items in an
522 527 execution reply. Returns whether the payload was handled.
523 528 """
524 529 # The basic FrontendWidget doesn't handle payloads, as they are a
525 530 # mechanism for going beyond the standard Python interpreter model.
526 531 return False
527 532
528 533 def _show_interpreter_prompt(self):
529 534 """ Shows a prompt for the interpreter.
530 535 """
531 536 self._show_prompt('>>> ')
532 537
533 538 def _show_interpreter_prompt_for_reply(self, msg):
534 539 """ Shows a prompt for the interpreter given an 'execute_reply' message.
535 540 """
536 541 self._show_interpreter_prompt()
537 542
538 543 #------ Signal handlers ----------------------------------------------------
539 544
540 545 def _document_contents_change(self, position, removed, added):
541 546 """ Called whenever the document's content changes. Display a call tip
542 547 if appropriate.
543 548 """
544 549 # Calculate where the cursor should be *after* the change:
545 550 position += added
546 551
547 552 document = self._control.document()
548 553 if position == self._get_cursor().position():
549 554 self._call_tip()
@@ -1,612 +1,627 b''
1 1 #!/usr/bin/env python
2 2 """A simple interactive kernel that talks to a frontend over 0MQ.
3 3
4 4 Things to do:
5 5
6 6 * Implement `set_parent` logic. Right before doing exec, the Kernel should
7 7 call set_parent on all the PUB objects with the message about to be executed.
8 8 * Implement random port and security key logic.
9 9 * Implement control messages.
10 10 * Implement event loop and poll version.
11 11 """
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16 from __future__ import print_function
17 17
18 18 # Standard library imports.
19 19 import __builtin__
20 20 import atexit
21 21 import sys
22 22 import time
23 23 import traceback
24 24
25 25 # System library imports.
26 26 import zmq
27 27
28 28 # Local imports.
29 29 from IPython.config.configurable import Configurable
30 30 from IPython.utils import io
31 31 from IPython.utils.jsonutil import json_clean
32 32 from IPython.lib import pylabtools
33 33 from IPython.utils.traitlets import Instance, Float
34 34 from entry_point import (base_launch_kernel, make_argument_parser, make_kernel,
35 35 start_kernel)
36 36 from iostream import OutStream
37 37 from session import Session, Message
38 38 from zmqshell import ZMQInteractiveShell
39 39
40 40 #-----------------------------------------------------------------------------
41 41 # Main kernel class
42 42 #-----------------------------------------------------------------------------
43 43
44 44 class Kernel(Configurable):
45 45
46 46 #---------------------------------------------------------------------------
47 47 # Kernel interface
48 48 #---------------------------------------------------------------------------
49 49
50 50 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
51 51 session = Instance(Session)
52 52 reply_socket = Instance('zmq.Socket')
53 53 pub_socket = Instance('zmq.Socket')
54 54 req_socket = Instance('zmq.Socket')
55 55
56 56 # Private interface
57 57
58 58 # Time to sleep after flushing the stdout/err buffers in each execute
59 59 # cycle. While this introduces a hard limit on the minimal latency of the
60 60 # execute cycle, it helps prevent output synchronization problems for
61 61 # clients.
62 62 # Units are in seconds. The minimum zmq latency on local host is probably
63 63 # ~150 microseconds, set this to 500us for now. We may need to increase it
64 64 # a little if it's not enough after more interactive testing.
65 65 _execute_sleep = Float(0.0005, config=True)
66 66
67 67 # Frequency of the kernel's event loop.
68 68 # Units are in seconds, kernel subclasses for GUI toolkits may need to
69 69 # adapt to milliseconds.
70 70 _poll_interval = Float(0.05, config=True)
71 71
72 72 # If the shutdown was requested over the network, we leave here the
73 73 # necessary reply message so it can be sent by our registered atexit
74 74 # handler. This ensures that the reply is only sent to clients truly at
75 75 # the end of our shutdown process (which happens after the underlying
76 76 # IPython shell's own shutdown).
77 77 _shutdown_message = None
78 78
79 79 # This is a dict of port number that the kernel is listening on. It is set
80 80 # by record_ports and used by connect_request.
81 81 _recorded_ports = None
82 82
83 83 def __init__(self, **kwargs):
84 84 super(Kernel, self).__init__(**kwargs)
85 85
86 86 # Before we even start up the shell, register *first* our exit handlers
87 87 # so they come before the shell's
88 88 atexit.register(self._at_shutdown)
89 89
90 90 # Initialize the InteractiveShell subclass
91 91 self.shell = ZMQInteractiveShell.instance()
92 92 self.shell.displayhook.session = self.session
93 93 self.shell.displayhook.pub_socket = self.pub_socket
94 94
95 95 # TMP - hack while developing
96 96 self.shell._reply_content = None
97 97
98 98 # Build dict of handlers for message types
99 99 msg_types = [ 'execute_request', 'complete_request',
100 100 'object_info_request', 'history_request',
101 101 'connect_request', 'shutdown_request']
102 102 self.handlers = {}
103 103 for msg_type in msg_types:
104 104 self.handlers[msg_type] = getattr(self, msg_type)
105 105
106 106 def do_one_iteration(self):
107 107 """Do one iteration of the kernel's evaluation loop.
108 108 """
109 109 try:
110 110 ident = self.reply_socket.recv(zmq.NOBLOCK)
111 111 except zmq.ZMQError, e:
112 112 if e.errno == zmq.EAGAIN:
113 113 return
114 114 else:
115 115 raise
116 116 # FIXME: Bug in pyzmq/zmq?
117 117 # assert self.reply_socket.rcvmore(), "Missing message part."
118 118 msg = self.reply_socket.recv_json()
119 119
120 120 # Print some info about this message and leave a '--->' marker, so it's
121 121 # easier to trace visually the message chain when debugging. Each
122 122 # handler prints its message at the end.
123 123 # Eventually we'll move these from stdout to a logger.
124 124 io.raw_print('\n*** MESSAGE TYPE:', msg['msg_type'], '***')
125 125 io.raw_print(' Content: ', msg['content'],
126 126 '\n --->\n ', sep='', end='')
127 127
128 128 # Find and call actual handler for message
129 129 handler = self.handlers.get(msg['msg_type'], None)
130 130 if handler is None:
131 131 io.raw_print_err("UNKNOWN MESSAGE TYPE:", msg)
132 132 else:
133 133 handler(ident, msg)
134 134
135 135 # Check whether we should exit, in case the incoming message set the
136 136 # exit flag on
137 137 if self.shell.exit_now:
138 138 io.raw_print('\nExiting IPython kernel...')
139 139 # We do a normal, clean exit, which allows any actions registered
140 140 # via atexit (such as history saving) to take place.
141 141 sys.exit(0)
142 142
143 143
144 144 def start(self):
145 145 """ Start the kernel main loop.
146 146 """
147 147 while True:
148 148 time.sleep(self._poll_interval)
149 149 self.do_one_iteration()
150 150
151 151 def record_ports(self, xrep_port, pub_port, req_port, hb_port):
152 152 """Record the ports that this kernel is using.
153 153
154 154 The creator of the Kernel instance must call this methods if they
155 155 want the :meth:`connect_request` method to return the port numbers.
156 156 """
157 157 self._recorded_ports = {
158 158 'xrep_port' : xrep_port,
159 159 'pub_port' : pub_port,
160 160 'req_port' : req_port,
161 161 'hb_port' : hb_port
162 162 }
163 163
164 164 #---------------------------------------------------------------------------
165 165 # Kernel request handlers
166 166 #---------------------------------------------------------------------------
167 167
168 168 def _publish_pyin(self, code, parent):
169 169 """Publish the code request on the pyin stream."""
170 170
171 171 pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent)
172 172 self.pub_socket.send_json(pyin_msg)
173 173
174 174 def execute_request(self, ident, parent):
175
176 status_msg = self.session.msg(
177 u'status',
178 {u'execution_state':u'busy'},
179 parent=parent
180 )
181 self.pub_socket.send_json(status_msg)
182
175 183 try:
176 184 content = parent[u'content']
177 185 code = content[u'code']
178 186 silent = content[u'silent']
179 187 except:
180 188 io.raw_print_err("Got bad msg: ")
181 189 io.raw_print_err(Message(parent))
182 190 return
183 191
184 192 shell = self.shell # we'll need this a lot here
185 193
186 194 # Replace raw_input. Note that is not sufficient to replace
187 195 # raw_input in the user namespace.
188 196 raw_input = lambda prompt='': self._raw_input(prompt, ident, parent)
189 197 __builtin__.raw_input = raw_input
190 198
191 199 # Set the parent message of the display hook and out streams.
192 200 shell.displayhook.set_parent(parent)
193 201 sys.stdout.set_parent(parent)
194 202 sys.stderr.set_parent(parent)
195 203
196 204 # Re-broadcast our input for the benefit of listening clients, and
197 205 # start computing output
198 206 if not silent:
199 207 self._publish_pyin(code, parent)
200 208
201 209 reply_content = {}
202 210 try:
203 211 if silent:
204 212 # runcode uses 'exec' mode, so no displayhook will fire, and it
205 213 # doesn't call logging or history manipulations. Print
206 214 # statements in that code will obviously still execute.
207 215 shell.runcode(code)
208 216 else:
209 217 # FIXME: runlines calls the exception handler itself.
210 218 shell._reply_content = None
211 219
212 220 # For now leave this here until we're sure we can stop using it
213 221 #shell.runlines(code)
214 222
215 223 # Experimental: cell mode! Test more before turning into
216 224 # default and removing the hacks around runlines.
217 225 shell.run_cell(code)
218 226 except:
219 227 status = u'error'
220 228 # FIXME: this code right now isn't being used yet by default,
221 229 # because the runlines() call above directly fires off exception
222 230 # reporting. This code, therefore, is only active in the scenario
223 231 # where runlines itself has an unhandled exception. We need to
224 232 # uniformize this, for all exception construction to come from a
225 233 # single location in the codbase.
226 234 etype, evalue, tb = sys.exc_info()
227 235 tb_list = traceback.format_exception(etype, evalue, tb)
228 236 reply_content.update(shell._showtraceback(etype, evalue, tb_list))
229 237 else:
230 238 status = u'ok'
231 239
232 240 reply_content[u'status'] = status
233 241 # Compute the execution counter so clients can display prompts
234 242 reply_content['execution_count'] = shell.displayhook.prompt_count
235 243
236 244 # FIXME - fish exception info out of shell, possibly left there by
237 245 # runlines. We'll need to clean up this logic later.
238 246 if shell._reply_content is not None:
239 247 reply_content.update(shell._reply_content)
240 248
241 249 # At this point, we can tell whether the main code execution succeeded
242 250 # or not. If it did, we proceed to evaluate user_variables/expressions
243 251 if reply_content['status'] == 'ok':
244 252 reply_content[u'user_variables'] = \
245 253 shell.get_user_variables(content[u'user_variables'])
246 254 reply_content[u'user_expressions'] = \
247 255 shell.eval_expressions(content[u'user_expressions'])
248 256 else:
249 257 # If there was an error, don't even try to compute variables or
250 258 # expressions
251 259 reply_content[u'user_variables'] = {}
252 260 reply_content[u'user_expressions'] = {}
253 261
254 262 # Payloads should be retrieved regardless of outcome, so we can both
255 263 # recover partial output (that could have been generated early in a
256 264 # block, before an error) and clear the payload system always.
257 265 reply_content[u'payload'] = shell.payload_manager.read_payload()
258 266 # Be agressive about clearing the payload because we don't want
259 267 # it to sit in memory until the next execute_request comes in.
260 268 shell.payload_manager.clear_payload()
261 269
262 270 # Send the reply.
263 271 reply_msg = self.session.msg(u'execute_reply', reply_content, parent)
264 272 io.raw_print(reply_msg)
265 273
266 274 # Flush output before sending the reply.
267 275 sys.stdout.flush()
268 276 sys.stderr.flush()
269 277 # FIXME: on rare occasions, the flush doesn't seem to make it to the
270 278 # clients... This seems to mitigate the problem, but we definitely need
271 279 # to better understand what's going on.
272 280 if self._execute_sleep:
273 281 time.sleep(self._execute_sleep)
274 282
275 283 self.reply_socket.send(ident, zmq.SNDMORE)
276 284 self.reply_socket.send_json(reply_msg)
277 285 if reply_msg['content']['status'] == u'error':
278 286 self._abort_queue()
279 287
288 status_msg = self.session.msg(
289 u'status',
290 {u'execution_state':u'idle'},
291 parent=parent
292 )
293 self.pub_socket.send_json(status_msg)
294
280 295 def complete_request(self, ident, parent):
281 296 txt, matches = self._complete(parent)
282 297 matches = {'matches' : matches,
283 298 'matched_text' : txt,
284 299 'status' : 'ok'}
285 300 completion_msg = self.session.send(self.reply_socket, 'complete_reply',
286 301 matches, parent, ident)
287 302 io.raw_print(completion_msg)
288 303
289 304 def object_info_request(self, ident, parent):
290 305 object_info = self.shell.object_inspect(parent['content']['oname'])
291 306 # Before we send this object over, we turn it into a dict and we scrub
292 307 # it for JSON usage
293 308 oinfo = json_clean(object_info._asdict())
294 309 msg = self.session.send(self.reply_socket, 'object_info_reply',
295 310 oinfo, parent, ident)
296 311 io.raw_print(msg)
297 312
298 313 def history_request(self, ident, parent):
299 314 output = parent['content']['output']
300 315 index = parent['content']['index']
301 316 raw = parent['content']['raw']
302 317 hist = self.shell.get_history(index=index, raw=raw, output=output)
303 318 content = {'history' : hist}
304 319 msg = self.session.send(self.reply_socket, 'history_reply',
305 320 content, parent, ident)
306 321 io.raw_print(msg)
307 322
308 323 def connect_request(self, ident, parent):
309 324 if self._recorded_ports is not None:
310 325 content = self._recorded_ports.copy()
311 326 else:
312 327 content = {}
313 328 msg = self.session.send(self.reply_socket, 'connect_reply',
314 329 content, parent, ident)
315 330 io.raw_print(msg)
316 331
317 332 def shutdown_request(self, ident, parent):
318 333 self.shell.exit_now = True
319 334 self._shutdown_message = self.session.msg(u'shutdown_reply', {}, parent)
320 335 sys.exit(0)
321 336
322 337 #---------------------------------------------------------------------------
323 338 # Protected interface
324 339 #---------------------------------------------------------------------------
325 340
326 341 def _abort_queue(self):
327 342 while True:
328 343 try:
329 344 ident = self.reply_socket.recv(zmq.NOBLOCK)
330 345 except zmq.ZMQError, e:
331 346 if e.errno == zmq.EAGAIN:
332 347 break
333 348 else:
334 349 assert self.reply_socket.rcvmore(), \
335 350 "Unexpected missing message part."
336 351 msg = self.reply_socket.recv_json()
337 352 io.raw_print("Aborting:\n", Message(msg))
338 353 msg_type = msg['msg_type']
339 354 reply_type = msg_type.split('_')[0] + '_reply'
340 355 reply_msg = self.session.msg(reply_type, {'status' : 'aborted'}, msg)
341 356 io.raw_print(reply_msg)
342 357 self.reply_socket.send(ident,zmq.SNDMORE)
343 358 self.reply_socket.send_json(reply_msg)
344 359 # We need to wait a bit for requests to come in. This can probably
345 360 # be set shorter for true asynchronous clients.
346 361 time.sleep(0.1)
347 362
348 363 def _raw_input(self, prompt, ident, parent):
349 364 # Flush output before making the request.
350 365 sys.stderr.flush()
351 366 sys.stdout.flush()
352 367
353 368 # Send the input request.
354 369 content = dict(prompt=prompt)
355 370 msg = self.session.msg(u'input_request', content, parent)
356 371 self.req_socket.send_json(msg)
357 372
358 373 # Await a response.
359 374 reply = self.req_socket.recv_json()
360 375 try:
361 376 value = reply['content']['value']
362 377 except:
363 378 io.raw_print_err("Got bad raw_input reply: ")
364 379 io.raw_print_err(Message(parent))
365 380 value = ''
366 381 return value
367 382
368 383 def _complete(self, msg):
369 384 c = msg['content']
370 385 try:
371 386 cpos = int(c['cursor_pos'])
372 387 except:
373 388 # If we don't get something that we can convert to an integer, at
374 389 # least attempt the completion guessing the cursor is at the end of
375 390 # the text, if there's any, and otherwise of the line
376 391 cpos = len(c['text'])
377 392 if cpos==0:
378 393 cpos = len(c['line'])
379 394 return self.shell.complete(c['text'], c['line'], cpos)
380 395
381 396 def _object_info(self, context):
382 397 symbol, leftover = self._symbol_from_context(context)
383 398 if symbol is not None and not leftover:
384 399 doc = getattr(symbol, '__doc__', '')
385 400 else:
386 401 doc = ''
387 402 object_info = dict(docstring = doc)
388 403 return object_info
389 404
390 405 def _symbol_from_context(self, context):
391 406 if not context:
392 407 return None, context
393 408
394 409 base_symbol_string = context[0]
395 410 symbol = self.shell.user_ns.get(base_symbol_string, None)
396 411 if symbol is None:
397 412 symbol = __builtin__.__dict__.get(base_symbol_string, None)
398 413 if symbol is None:
399 414 return None, context
400 415
401 416 context = context[1:]
402 417 for i, name in enumerate(context):
403 418 new_symbol = getattr(symbol, name, None)
404 419 if new_symbol is None:
405 420 return symbol, context[i:]
406 421 else:
407 422 symbol = new_symbol
408 423
409 424 return symbol, []
410 425
411 426 def _at_shutdown(self):
412 427 """Actions taken at shutdown by the kernel, called by python's atexit.
413 428 """
414 429 # io.rprint("Kernel at_shutdown") # dbg
415 430 if self._shutdown_message is not None:
416 431 self.reply_socket.send_json(self._shutdown_message)
417 432 io.raw_print(self._shutdown_message)
418 433 # A very short sleep to give zmq time to flush its message buffers
419 434 # before Python truly shuts down.
420 435 time.sleep(0.01)
421 436
422 437
423 438 class QtKernel(Kernel):
424 439 """A Kernel subclass with Qt support."""
425 440
426 441 def start(self):
427 442 """Start a kernel with QtPy4 event loop integration."""
428 443
429 444 from PyQt4 import QtCore
430 445 from IPython.lib.guisupport import get_app_qt4, start_event_loop_qt4
431 446
432 447 self.app = get_app_qt4([" "])
433 448 self.app.setQuitOnLastWindowClosed(False)
434 449 self.timer = QtCore.QTimer()
435 450 self.timer.timeout.connect(self.do_one_iteration)
436 451 # Units for the timer are in milliseconds
437 452 self.timer.start(1000*self._poll_interval)
438 453 start_event_loop_qt4(self.app)
439 454
440 455
441 456 class WxKernel(Kernel):
442 457 """A Kernel subclass with Wx support."""
443 458
444 459 def start(self):
445 460 """Start a kernel with wx event loop support."""
446 461
447 462 import wx
448 463 from IPython.lib.guisupport import start_event_loop_wx
449 464
450 465 doi = self.do_one_iteration
451 466 # Wx uses milliseconds
452 467 poll_interval = int(1000*self._poll_interval)
453 468
454 469 # We have to put the wx.Timer in a wx.Frame for it to fire properly.
455 470 # We make the Frame hidden when we create it in the main app below.
456 471 class TimerFrame(wx.Frame):
457 472 def __init__(self, func):
458 473 wx.Frame.__init__(self, None, -1)
459 474 self.timer = wx.Timer(self)
460 475 # Units for the timer are in milliseconds
461 476 self.timer.Start(poll_interval)
462 477 self.Bind(wx.EVT_TIMER, self.on_timer)
463 478 self.func = func
464 479
465 480 def on_timer(self, event):
466 481 self.func()
467 482
468 483 # We need a custom wx.App to create our Frame subclass that has the
469 484 # wx.Timer to drive the ZMQ event loop.
470 485 class IPWxApp(wx.App):
471 486 def OnInit(self):
472 487 self.frame = TimerFrame(doi)
473 488 self.frame.Show(False)
474 489 return True
475 490
476 491 # The redirect=False here makes sure that wx doesn't replace
477 492 # sys.stdout/stderr with its own classes.
478 493 self.app = IPWxApp(redirect=False)
479 494 start_event_loop_wx(self.app)
480 495
481 496
482 497 class TkKernel(Kernel):
483 498 """A Kernel subclass with Tk support."""
484 499
485 500 def start(self):
486 501 """Start a Tk enabled event loop."""
487 502
488 503 import Tkinter
489 504 doi = self.do_one_iteration
490 505 # Tk uses milliseconds
491 506 poll_interval = int(1000*self._poll_interval)
492 507 # For Tkinter, we create a Tk object and call its withdraw method.
493 508 class Timer(object):
494 509 def __init__(self, func):
495 510 self.app = Tkinter.Tk()
496 511 self.app.withdraw()
497 512 self.func = func
498 513
499 514 def on_timer(self):
500 515 self.func()
501 516 self.app.after(poll_interval, self.on_timer)
502 517
503 518 def start(self):
504 519 self.on_timer() # Call it once to get things going.
505 520 self.app.mainloop()
506 521
507 522 self.timer = Timer(doi)
508 523 self.timer.start()
509 524
510 525
511 526 class GTKKernel(Kernel):
512 527 """A Kernel subclass with GTK support."""
513 528
514 529 def start(self):
515 530 """Start the kernel, coordinating with the GTK event loop"""
516 531 from .gui.gtkembed import GTKEmbed
517 532
518 533 gtk_kernel = GTKEmbed(self)
519 534 gtk_kernel.start()
520 535
521 536
522 537 #-----------------------------------------------------------------------------
523 538 # Kernel main and launch functions
524 539 #-----------------------------------------------------------------------------
525 540
526 541 def launch_kernel(xrep_port=0, pub_port=0, req_port=0, hb_port=0,
527 542 independent=False, pylab=False):
528 543 """Launches a localhost kernel, binding to the specified ports.
529 544
530 545 Parameters
531 546 ----------
532 547 xrep_port : int, optional
533 548 The port to use for XREP channel.
534 549
535 550 pub_port : int, optional
536 551 The port to use for the SUB channel.
537 552
538 553 req_port : int, optional
539 554 The port to use for the REQ (raw input) channel.
540 555
541 556 hb_port : int, optional
542 557 The port to use for the hearbeat REP channel.
543 558
544 559 independent : bool, optional (default False)
545 560 If set, the kernel process is guaranteed to survive if this process
546 561 dies. If not set, an effort is made to ensure that the kernel is killed
547 562 when this process dies. Note that in this case it is still good practice
548 563 to kill kernels manually before exiting.
549 564
550 565 pylab : bool or string, optional (default False)
551 566 If not False, the kernel will be launched with pylab enabled. If a
552 567 string is passed, matplotlib will use the specified backend. Otherwise,
553 568 matplotlib's default backend will be used.
554 569
555 570 Returns
556 571 -------
557 572 A tuple of form:
558 573 (kernel_process, xrep_port, pub_port, req_port)
559 574 where kernel_process is a Popen object and the ports are integers.
560 575 """
561 576 extra_arguments = []
562 577 if pylab:
563 578 extra_arguments.append('--pylab')
564 579 if isinstance(pylab, basestring):
565 580 extra_arguments.append(pylab)
566 581 return base_launch_kernel('from IPython.zmq.ipkernel import main; main()',
567 582 xrep_port, pub_port, req_port, hb_port,
568 583 independent, extra_arguments)
569 584
570 585
571 586 def main():
572 587 """ The IPython kernel main entry point.
573 588 """
574 589 parser = make_argument_parser()
575 590 parser.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
576 591 const='auto', help = \
577 592 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
578 593 given, the GUI backend is matplotlib's, otherwise use one of: \
579 594 ['tk', 'gtk', 'qt', 'wx', 'inline'].")
580 595 namespace = parser.parse_args()
581 596
582 597 kernel_class = Kernel
583 598
584 599 kernel_classes = {
585 600 'qt' : QtKernel,
586 601 'qt4': QtKernel,
587 602 'inline': Kernel,
588 603 'wx' : WxKernel,
589 604 'tk' : TkKernel,
590 605 'gtk': GTKKernel,
591 606 }
592 607 if namespace.pylab:
593 608 if namespace.pylab == 'auto':
594 609 gui, backend = pylabtools.find_gui_and_backend()
595 610 else:
596 611 gui, backend = pylabtools.find_gui_and_backend(namespace.pylab)
597 612 kernel_class = kernel_classes.get(gui)
598 613 if kernel_class is None:
599 614 raise ValueError('GUI is not supported: %r' % gui)
600 615 pylabtools.activate_matplotlib(backend)
601 616
602 617 kernel = make_kernel(namespace, kernel_class, OutStream)
603 618
604 619 if namespace.pylab:
605 620 pylabtools.import_pylab(kernel.shell.user_ns, backend,
606 621 shell=kernel.shell)
607 622
608 623 start_kernel(namespace, kernel)
609 624
610 625
611 626 if __name__ == '__main__':
612 627 main()
@@ -1,782 +1,795 b''
1 1 .. _messaging:
2 2
3 3 ======================
4 4 Messaging in IPython
5 5 ======================
6 6
7 7
8 8 Introduction
9 9 ============
10 10
11 11 This document explains the basic communications design and messaging
12 12 specification for how the various IPython objects interact over a network
13 13 transport. The current implementation uses the ZeroMQ_ library for messaging
14 14 within and between hosts.
15 15
16 16 .. Note::
17 17
18 18 This document should be considered the authoritative description of the
19 19 IPython messaging protocol, and all developers are strongly encouraged to
20 20 keep it updated as the implementation evolves, so that we have a single
21 21 common reference for all protocol details.
22 22
23 23 The basic design is explained in the following diagram:
24 24
25 25 .. image:: frontend-kernel.png
26 26 :width: 450px
27 27 :alt: IPython kernel/frontend messaging architecture.
28 28 :align: center
29 29 :target: ../_images/frontend-kernel.png
30 30
31 31 A single kernel can be simultaneously connected to one or more frontends. The
32 32 kernel has three sockets that serve the following functions:
33 33
34 34 1. REQ: this socket is connected to a *single* frontend at a time, and it allows
35 35 the kernel to request input from a frontend when :func:`raw_input` is called.
36 36 The frontend holding the matching REP socket acts as a 'virtual keyboard'
37 37 for the kernel while this communication is happening (illustrated in the
38 38 figure by the black outline around the central keyboard). In practice,
39 39 frontends may display such kernel requests using a special input widget or
40 40 otherwise indicating that the user is to type input for the kernel instead
41 41 of normal commands in the frontend.
42 42
43 43 2. XREP: this single sockets allows multiple incoming connections from
44 44 frontends, and this is the socket where requests for code execution, object
45 45 information, prompts, etc. are made to the kernel by any frontend. The
46 46 communication on this socket is a sequence of request/reply actions from
47 47 each frontend and the kernel.
48 48
49 49 3. PUB: this socket is the 'broadcast channel' where the kernel publishes all
50 50 side effects (stdout, stderr, etc.) as well as the requests coming from any
51 51 client over the XREP socket and its own requests on the REP socket. There
52 52 are a number of actions in Python which generate side effects: :func:`print`
53 53 writes to ``sys.stdout``, errors generate tracebacks, etc. Additionally, in
54 54 a multi-client scenario, we want all frontends to be able to know what each
55 55 other has sent to the kernel (this can be useful in collaborative scenarios,
56 56 for example). This socket allows both side effects and the information
57 57 about communications taking place with one client over the XREQ/XREP channel
58 58 to be made available to all clients in a uniform manner.
59 59
60 60 All messages are tagged with enough information (details below) for clients
61 61 to know which messages come from their own interaction with the kernel and
62 62 which ones are from other clients, so they can display each type
63 63 appropriately.
64 64
65 65 The actual format of the messages allowed on each of these channels is
66 66 specified below. Messages are dicts of dicts with string keys and values that
67 67 are reasonably representable in JSON. Our current implementation uses JSON
68 68 explicitly as its message format, but this shouldn't be considered a permanent
69 69 feature. As we've discovered that JSON has non-trivial performance issues due
70 70 to excessive copying, we may in the future move to a pure pickle-based raw
71 71 message format. However, it should be possible to easily convert from the raw
72 72 objects to JSON, since we may have non-python clients (e.g. a web frontend).
73 73 As long as it's easy to make a JSON version of the objects that is a faithful
74 74 representation of all the data, we can communicate with such clients.
75 75
76 76 .. Note::
77 77
78 78 Not all of these have yet been fully fleshed out, but the key ones are, see
79 79 kernel and frontend files for actual implementation details.
80 80
81 81
82 82 Python functional API
83 83 =====================
84 84
85 85 As messages are dicts, they map naturally to a ``func(**kw)`` call form. We
86 86 should develop, at a few key points, functional forms of all the requests that
87 87 take arguments in this manner and automatically construct the necessary dict
88 88 for sending.
89 89
90 90
91 91 General Message Format
92 92 ======================
93 93
94 94 All messages send or received by any IPython process should have the following
95 95 generic structure::
96 96
97 97 {
98 98 # The message header contains a pair of unique identifiers for the
99 99 # originating session and the actual message id, in addition to the
100 100 # username for the process that generated the message. This is useful in
101 101 # collaborative settings where multiple users may be interacting with the
102 102 # same kernel simultaneously, so that frontends can label the various
103 103 # messages in a meaningful way.
104 104 'header' : { 'msg_id' : uuid,
105 105 'username' : str,
106 106 'session' : uuid
107 107 },
108 108
109 109 # In a chain of messages, the header from the parent is copied so that
110 110 # clients can track where messages come from.
111 111 'parent_header' : dict,
112 112
113 113 # All recognized message type strings are listed below.
114 114 'msg_type' : str,
115 115
116 116 # The actual content of the message must be a dict, whose structure
117 117 # depends on the message type.x
118 118 'content' : dict,
119 119 }
120 120
121 121 For each message type, the actual content will differ and all existing message
122 122 types are specified in what follows of this document.
123 123
124 124
125 125 Messages on the XREP/XREQ socket
126 126 ================================
127 127
128 128 .. _execute:
129 129
130 130 Execute
131 131 -------
132 132
133 133 This message type is used by frontends to ask the kernel to execute code on
134 134 behalf of the user, in a namespace reserved to the user's variables (and thus
135 135 separate from the kernel's own internal code and variables).
136 136
137 137 Message type: ``execute_request``::
138 138
139 139 content = {
140 140 # Source code to be executed by the kernel, one or more lines.
141 141 'code' : str,
142 142
143 143 # A boolean flag which, if True, signals the kernel to execute this
144 144 # code as quietly as possible. This means that the kernel will compile
145 145 # the code witIPython/core/tests/h 'exec' instead of 'single' (so
146 146 # sys.displayhook will not fire), and will *not*:
147 147 # - broadcast exceptions on the PUB socket
148 148 # - do any logging
149 149 # - populate any history
150 150 #
151 151 # The default is False.
152 152 'silent' : bool,
153 153
154 154 # A list of variable names from the user's namespace to be retrieved. What
155 155 # returns is a JSON string of the variable's repr(), not a python object.
156 156 'user_variables' : list,
157 157
158 158 # Similarly, a dict mapping names to expressions to be evaluated in the
159 159 # user's dict.
160 160 'user_expressions' : dict,
161 161 }
162 162
163 163 The ``code`` field contains a single string, but this may be a multiline
164 164 string. The kernel is responsible for splitting this into possibly more than
165 165 one block and deciding whether to compile these in 'single' or 'exec' mode.
166 166 We're still sorting out this policy. The current inputsplitter is capable of
167 167 splitting the input for blocks that can all be run as 'single', but in the long
168 168 run it may prove cleaner to only use 'single' mode for truly single-line
169 169 inputs, and run all multiline input in 'exec' mode. This would preserve the
170 170 natural behavior of single-line inputs while allowing long cells to behave more
171 171 likea a script. This design will be refined as we complete the implementation.
172 172
173 173 The ``user_`` fields deserve a detailed explanation. In the past, IPython had
174 174 the notion of a prompt string that allowed arbitrary code to be evaluated, and
175 175 this was put to good use by many in creating prompts that displayed system
176 176 status, path information, and even more esoteric uses like remote instrument
177 177 status aqcuired over the network. But now that IPython has a clean separation
178 178 between the kernel and the clients, the notion of embedding 'prompt'
179 179 maninpulations into the kernel itself feels awkward. Prompts should be a
180 180 frontend-side feature, and it should be even possible for different frontends
181 181 to display different prompts while interacting with the same kernel.
182 182
183 183 We have therefore abandoned the idea of a 'prompt string' to be evaluated by
184 184 the kernel, and instead provide the ability to retrieve from the user's
185 185 namespace information after the execution of the main ``code``, with two fields
186 186 of the execution request:
187 187
188 188 - ``user_variables``: If only variables from the user's namespace are needed, a
189 189 list of variable names can be passed and a dict with these names as keys and
190 190 their :func:`repr()` as values will be returned.
191 191
192 192 - ``user_expressions``: For more complex expressions that require function
193 193 evaluations, a dict can be provided with string keys and arbitrary python
194 194 expressions as values. The return message will contain also a dict with the
195 195 same keys and the :func:`repr()` of the evaluated expressions as value.
196 196
197 197 With this information, frontends can display any status information they wish
198 198 in the form that best suits each frontend (a status line, a popup, inline for a
199 199 terminal, etc).
200 200
201 201 .. Note::
202 202
203 203 In order to obtain the current execution counter for the purposes of
204 204 displaying input prompts, frontends simply make an execution request with an
205 205 empty code string and ``silent=True``.
206 206
207 207 Execution semantics
208 208 Upon completion of the execution request, the kernel *always* sends a
209 209 reply, with a status code indicating what happened and additional data
210 210 depending on the outcome.
211 211
212 212 The ``code`` field is executed first, and then the ``user_variables`` and
213 213 ``user_expressions`` are computed. This ensures that any error in the
214 214 latter don't harm the main code execution.
215 215
216 216 Any error in retrieving the ``user_variables`` or evaluating the
217 217 ``user_expressions`` will result in a simple error message in the return
218 218 fields of the form::
219 219
220 220 [ERROR] ExceptionType: Exception message
221 221
222 222 The user can simply send the same variable name or expression for
223 223 evaluation to see a regular traceback.
224 224
225 225 Execution counter (old prompt number)
226 226 The kernel has a single, monotonically increasing counter of all execution
227 227 requests that are made with ``silent=False``. This counter is used to
228 228 populate the ``In[n]``, ``Out[n]`` and ``_n`` variables, so clients will
229 229 likely want to display it in some form to the user, which will typically
230 230 (but not necessarily) be done in the prompts. The value of this counter
231 231 will be returned as the ``execution_count`` field of all ``execute_reply```
232 232 messages.
233 233
234 234 Message type: ``execute_reply``::
235 235
236 236 content = {
237 237 # One of: 'ok' OR 'error' OR 'abort'
238 238 'status' : str,
239 239
240 240 # The global kernel counter that increases by one with each non-silent
241 241 # executed request. This will typically be used by clients to display
242 242 # prompt numbers to the user. If the request was a silent one, this will
243 243 # be the current value of the counter in the kernel.
244 244 'execution_count' : int,
245 245 }
246 246
247 247 When status is 'ok', the following extra fields are present::
248 248
249 249 {
250 250 # The execution payload is a dict with string keys that may have been
251 251 # produced by the code being executed. It is retrieved by the kernel at
252 252 # the end of the execution and sent back to the front end, which can take
253 253 # action on it as needed. See main text for further details.
254 254 'payload' : dict,
255 255
256 256 # Results for the user_variables and user_expressions.
257 257 'user_variables' : dict,
258 258 'user_expressions' : dict,
259 259
260 260 # The kernel will often transform the input provided to it. If the
261 261 # '---->' transform had been applied, this is filled, otherwise it's the
262 262 # empty string. So transformations like magics don't appear here, only
263 263 # autocall ones.
264 264 'transformed_code' : str,
265 265 }
266 266
267 267 .. admonition:: Execution payloads
268 268
269 269 The notion of an 'execution payload' is different from a return value of a
270 270 given set of code, which normally is just displayed on the pyout stream
271 271 through the PUB socket. The idea of a payload is to allow special types of
272 272 code, typically magics, to populate a data container in the IPython kernel
273 273 that will be shipped back to the caller via this channel. The kernel will
274 274 have an API for this, probably something along the lines of::
275 275
276 276 ip.exec_payload_add(key, value)
277 277
278 278 though this API is still in the design stages. The data returned in this
279 279 payload will allow frontends to present special views of what just happened.
280 280
281 281
282 282 When status is 'error', the following extra fields are present::
283 283
284 284 {
285 285 'exc_name' : str, # Exception name, as a string
286 286 'exc_value' : str, # Exception value, as a string
287 287
288 288 # The traceback will contain a list of frames, represented each as a
289 289 # string. For now we'll stick to the existing design of ultraTB, which
290 290 # controls exception level of detail statefully. But eventually we'll
291 291 # want to grow into a model where more information is collected and
292 292 # packed into the traceback object, with clients deciding how little or
293 293 # how much of it to unpack. But for now, let's start with a simple list
294 294 # of strings, since that requires only minimal changes to ultratb as
295 295 # written.
296 296 'traceback' : list,
297 297 }
298 298
299 299
300 300 When status is 'abort', there are for now no additional data fields. This
301 301 happens when the kernel was interrupted by a signal.
302 302
303 303 Kernel attribute access
304 304 -----------------------
305 305
306 306 While this protocol does not specify full RPC access to arbitrary methods of
307 307 the kernel object, the kernel does allow read (and in some cases write) access
308 308 to certain attributes.
309 309
310 310 The policy for which attributes can be read is: any attribute of the kernel, or
311 311 its sub-objects, that belongs to a :class:`Configurable` object and has been
312 312 declared at the class-level with Traits validation, is in principle accessible
313 313 as long as its name does not begin with a leading underscore. The attribute
314 314 itself will have metadata indicating whether it allows remote read and/or write
315 315 access. The message spec follows for attribute read and write requests.
316 316
317 317 Message type: ``getattr_request``::
318 318
319 319 content = {
320 320 # The (possibly dotted) name of the attribute
321 321 'name' : str,
322 322 }
323 323
324 324 When a ``getattr_request`` fails, there are two possible error types:
325 325
326 326 - AttributeError: this type of error was raised when trying to access the
327 327 given name by the kernel itself. This means that the attribute likely
328 328 doesn't exist.
329 329
330 330 - AccessError: the attribute exists but its value is not readable remotely.
331 331
332 332
333 333 Message type: ``getattr_reply``::
334 334
335 335 content = {
336 336 # One of ['ok', 'AttributeError', 'AccessError'].
337 337 'status' : str,
338 338 # If status is 'ok', a JSON object.
339 339 'value' : object,
340 340 }
341 341
342 342 Message type: ``setattr_request``::
343 343
344 344 content = {
345 345 # The (possibly dotted) name of the attribute
346 346 'name' : str,
347 347
348 348 # A JSON-encoded object, that will be validated by the Traits
349 349 # information in the kernel
350 350 'value' : object,
351 351 }
352 352
353 353 When a ``setattr_request`` fails, there are also two possible error types with
354 354 similar meanings as those of the ``getattr_request`` case, but for writing.
355 355
356 356 Message type: ``setattr_reply``::
357 357
358 358 content = {
359 359 # One of ['ok', 'AttributeError', 'AccessError'].
360 360 'status' : str,
361 361 }
362 362
363 363
364 364 Object information
365 365 ------------------
366 366
367 367 One of IPython's most used capabilities is the introspection of Python objects
368 368 in the user's namespace, typically invoked via the ``?`` and ``??`` characters
369 369 (which in reality are shorthands for the ``%pinfo`` magic). This is used often
370 370 enough that it warrants an explicit message type, especially because frontends
371 371 may want to get object information in response to user keystrokes (like Tab or
372 372 F1) besides from the user explicitly typing code like ``x??``.
373 373
374 374 Message type: ``object_info_request``::
375 375
376 376 content = {
377 377 # The (possibly dotted) name of the object to be searched in all
378 378 # relevant namespaces
379 379 'name' : str,
380 380
381 381 # The level of detail desired. The default (0) is equivalent to typing
382 382 # 'x?' at the prompt, 1 is equivalent to 'x??'.
383 383 'detail_level' : int,
384 384 }
385 385
386 386 The returned information will be a dictionary with keys very similar to the
387 387 field names that IPython prints at the terminal.
388 388
389 389 Message type: ``object_info_reply``::
390 390
391 391 content = {
392 392 # Boolean flag indicating whether the named object was found or not. If
393 393 # it's false, all other fields will be empty.
394 394 'found' : bool,
395 395
396 396 # Flags for magics and system aliases
397 397 'ismagic' : bool,
398 398 'isalias' : bool,
399 399
400 400 # The name of the namespace where the object was found ('builtin',
401 401 # 'magics', 'alias', 'interactive', etc.)
402 402 'namespace' : str,
403 403
404 404 # The type name will be type.__name__ for normal Python objects, but it
405 405 # can also be a string like 'Magic function' or 'System alias'
406 406 'type_name' : str,
407 407
408 408 'string_form' : str,
409 409
410 410 # For objects with a __class__ attribute this will be set
411 411 'base_class' : str,
412 412
413 413 # For objects with a __len__ attribute this will be set
414 414 'length' : int,
415 415
416 416 # If the object is a function, class or method whose file we can find,
417 417 # we give its full path
418 418 'file' : str,
419 419
420 420 # For pure Python callable objects, we can reconstruct the object
421 421 # definition line which provides its call signature. For convenience this
422 422 # is returned as a single 'definition' field, but below the raw parts that
423 423 # compose it are also returned as the argspec field.
424 424 'definition' : str,
425 425
426 426 # The individual parts that together form the definition string. Clients
427 427 # with rich display capabilities may use this to provide a richer and more
428 428 # precise representation of the definition line (e.g. by highlighting
429 429 # arguments based on the user's cursor position). For non-callable
430 430 # objects, this field is empty.
431 431 'argspec' : { # The names of all the arguments
432 432 args : list,
433 433 # The name of the varargs (*args), if any
434 434 varargs : str,
435 435 # The name of the varkw (**kw), if any
436 436 varkw : str,
437 437 # The values (as strings) of all default arguments. Note
438 438 # that these must be matched *in reverse* with the 'args'
439 439 # list above, since the first positional args have no default
440 440 # value at all.
441 441 func_defaults : list,
442 442 },
443 443
444 444 # For instances, provide the constructor signature (the definition of
445 445 # the __init__ method):
446 446 'init_definition' : str,
447 447
448 448 # Docstrings: for any object (function, method, module, package) with a
449 449 # docstring, we show it. But in addition, we may provide additional
450 450 # docstrings. For example, for instances we will show the constructor
451 451 # and class docstrings as well, if available.
452 452 'docstring' : str,
453 453
454 454 # For instances, provide the constructor and class docstrings
455 455 'init_docstring' : str,
456 456 'class_docstring' : str,
457 457
458 458 # If it's a callable object whose call method has a separate docstring and
459 459 # definition line:
460 460 'call_def' : str,
461 461 'call_docstring' : str,
462 462
463 463 # If detail_level was 1, we also try to find the source code that
464 464 # defines the object, if possible. The string 'None' will indicate
465 465 # that no source was found.
466 466 'source' : str,
467 467 }
468 468 '
469 469
470 470 Complete
471 471 --------
472 472
473 473 Message type: ``complete_request``::
474 474
475 475 content = {
476 476 # The text to be completed, such as 'a.is'
477 477 'text' : str,
478 478
479 479 # The full line, such as 'print a.is'. This allows completers to
480 480 # make decisions that may require information about more than just the
481 481 # current word.
482 482 'line' : str,
483 483
484 484 # The entire block of text where the line is. This may be useful in the
485 485 # case of multiline completions where more context may be needed. Note: if
486 486 # in practice this field proves unnecessary, remove it to lighten the
487 487 # messages.
488 488
489 489 'block' : str,
490 490
491 491 # The position of the cursor where the user hit 'TAB' on the line.
492 492 'cursor_pos' : int,
493 493 }
494 494
495 495 Message type: ``complete_reply``::
496 496
497 497 content = {
498 498 # The list of all matches to the completion request, such as
499 499 # ['a.isalnum', 'a.isalpha'] for the above example.
500 500 'matches' : list
501 501 }
502 502
503 503
504 504 History
505 505 -------
506 506
507 507 For clients to explicitly request history from a kernel. The kernel has all
508 508 the actual execution history stored in a single location, so clients can
509 509 request it from the kernel when needed.
510 510
511 511 Message type: ``history_request``::
512 512
513 513 content = {
514 514
515 515 # If True, also return output history in the resulting dict.
516 516 'output' : bool,
517 517
518 518 # If True, return the raw input history, else the transformed input.
519 519 'raw' : bool,
520 520
521 521 # This parameter can be one of: A number, a pair of numbers, None
522 522 # If not given, last 40 are returned.
523 523 # - number n: return the last n entries.
524 524 # - pair n1, n2: return entries in the range(n1, n2).
525 525 # - None: return all history
526 526 'index' : n or (n1, n2) or None,
527 527 }
528 528
529 529 Message type: ``history_reply``::
530 530
531 531 content = {
532 532 # A dict with prompt numbers as keys and either (input, output) or input
533 533 # as the value depending on whether output was True or False,
534 534 # respectively.
535 535 'history' : dict,
536 536 }
537 537
538 538
539 539 Connect
540 540 -------
541 541
542 542 When a client connects to the request/reply socket of the kernel, it can issue
543 543 a connect request to get basic information about the kernel, such as the ports
544 544 the other ZeroMQ sockets are listening on. This allows clients to only have
545 545 to know about a single port (the XREQ/XREP channel) to connect to a kernel.
546 546
547 547 Message type: ``connect_request``::
548 548
549 549 content = {
550 550 }
551 551
552 552 Message type: ``connect_reply``::
553 553
554 554 content = {
555 555 'xrep_port' : int # The port the XREP socket is listening on.
556 556 'pub_port' : int # The port the PUB socket is listening on.
557 557 'req_port' : int # The port the REQ socket is listening on.
558 558 'hb_port' : int # The port the heartbeat socket is listening on.
559 559 }
560 560
561 561
562 562
563 563 Kernel shutdown
564 564 ---------------
565 565
566 566 The clients can request the kernel to shut itself down; this is used in
567 567 multiple cases:
568 568
569 569 - when the user chooses to close the client application via a menu or window
570 570 control.
571 571 - when the user types 'exit' or 'quit' (or their uppercase magic equivalents).
572 572 - when the user chooses a GUI method (like the 'Ctrl-C' shortcut in the
573 573 IPythonQt client) to force a kernel restart to get a clean kernel without
574 574 losing client-side state like history or inlined figures.
575 575
576 576 The client sends a shutdown request to the kernel, and once it receives the
577 577 reply message (which is otherwise empty), it can assume that the kernel has
578 578 completed shutdown safely.
579 579
580 580 Upon their own shutdown, client applications will typically execute a last
581 581 minute sanity check and forcefully terminate any kernel that is still alive, to
582 582 avoid leaving stray processes in the user's machine.
583 583
584 584 For both shutdown request and reply, there is no actual content that needs to
585 585 be sent, so the content dict is empty.
586 586
587 587 Message type: ``shutdown_request``::
588 588
589 589 content = {
590 590 }
591 591
592 592 Message type: ``shutdown_reply``::
593 593
594 594 content = {
595 595 }
596 596
597 597 .. Note::
598 598
599 599 When the clients detect a dead kernel thanks to inactivity on the heartbeat
600 600 socket, they simply send a forceful process termination signal, since a dead
601 601 process is unlikely to respond in any useful way to messages.
602 602
603 603
604 604 Messages on the PUB/SUB socket
605 605 ==============================
606 606
607 607 Streams (stdout, stderr, etc)
608 608 ------------------------------
609 609
610 610 Message type: ``stream``::
611 611
612 612 content = {
613 613 # The name of the stream is one of 'stdin', 'stdout', 'stderr'
614 614 'name' : str,
615 615
616 616 # The data is an arbitrary string to be written to that stream
617 617 'data' : str,
618 618 }
619 619
620 620 When a kernel receives a raw_input call, it should also broadcast it on the pub
621 621 socket with the names 'stdin' and 'stdin_reply'. This will allow other clients
622 622 to monitor/display kernel interactions and possibly replay them to their user
623 623 or otherwise expose them.
624 624
625 625 Python inputs
626 626 -------------
627 627
628 628 These messages are the re-broadcast of the ``execute_request``.
629 629
630 630 Message type: ``pyin``::
631 631
632 632 content = {
633 633 # Source code to be executed, one or more lines
634 634 'code' : str
635 635 }
636 636
637 637 Python outputs
638 638 --------------
639 639
640 640 When Python produces output from code that has been compiled in with the
641 641 'single' flag to :func:`compile`, any expression that produces a value (such as
642 642 ``1+1``) is passed to ``sys.displayhook``, which is a callable that can do with
643 643 this value whatever it wants. The default behavior of ``sys.displayhook`` in
644 644 the Python interactive prompt is to print to ``sys.stdout`` the :func:`repr` of
645 645 the value as long as it is not ``None`` (which isn't printed at all). In our
646 646 case, the kernel instantiates as ``sys.displayhook`` an object which has
647 647 similar behavior, but which instead of printing to stdout, broadcasts these
648 648 values as ``pyout`` messages for clients to display appropriately.
649 649
650 650 Message type: ``pyout``::
651 651
652 652 content = {
653 653 # The data is typically the repr() of the object.
654 654 'data' : str,
655 655
656 656 # The counter for this execution is also provided so that clients can
657 657 # display it, since IPython automatically creates variables called _N (for
658 658 # prompt N).
659 659 'execution_count' : int,
660 660 }
661 661
662 662 Python errors
663 663 -------------
664 664
665 665 When an error occurs during code execution
666 666
667 667 Message type: ``pyerr``::
668 668
669 669 content = {
670 670 # Similar content to the execute_reply messages for the 'error' case,
671 671 # except the 'status' field is omitted.
672 672 }
673 673
674 Kernel status
675 -------------
676
677 This message type is used by frontends to monitor the status of the kernel.
678
679 Message type: ``status``::
680
681 content = {
682 # When the kernel starts to execute code, it will enter the 'busy'
683 # state and when it finishes, it will enter the 'idle' state.
684 execution_state : ('busy', 'idle')
685 }
686
674 687 Kernel crashes
675 688 --------------
676 689
677 690 When the kernel has an unexpected exception, caught by the last-resort
678 691 sys.excepthook, we should broadcast the crash handler's output before exiting.
679 692 This will allow clients to notice that a kernel died, inform the user and
680 693 propose further actions.
681 694
682 695 Message type: ``crash``::
683 696
684 697 content = {
685 698 # Similarly to the 'error' case for execute_reply messages, this will
686 699 # contain exc_name, exc_type and traceback fields.
687 700
688 701 # An additional field with supplementary information such as where to
689 702 # send the crash message
690 703 'info' : str,
691 704 }
692 705
693 706
694 707 Future ideas
695 708 ------------
696 709
697 710 Other potential message types, currently unimplemented, listed below as ideas.
698 711
699 712 Message type: ``file``::
700 713
701 714 content = {
702 715 'path' : 'cool.jpg',
703 716 'mimetype' : str,
704 717 'data' : str,
705 718 }
706 719
707 720
708 721 Messages on the REQ/REP socket
709 722 ==============================
710 723
711 724 This is a socket that goes in the opposite direction: from the kernel to a
712 725 *single* frontend, and its purpose is to allow ``raw_input`` and similar
713 726 operations that read from ``sys.stdin`` on the kernel to be fulfilled by the
714 727 client. For now we will keep these messages as simple as possible, since they
715 728 basically only mean to convey the ``raw_input(prompt)`` call.
716 729
717 730 Message type: ``input_request``::
718 731
719 732 content = { 'prompt' : str }
720 733
721 734 Message type: ``input_reply``::
722 735
723 736 content = { 'value' : str }
724 737
725 738 .. Note::
726 739
727 740 We do not explicitly try to forward the raw ``sys.stdin`` object, because in
728 741 practice the kernel should behave like an interactive program. When a
729 742 program is opened on the console, the keyboard effectively takes over the
730 743 ``stdin`` file descriptor, and it can't be used for raw reading anymore.
731 744 Since the IPython kernel effectively behaves like a console program (albeit
732 745 one whose "keyboard" is actually living in a separate process and
733 746 transported over the zmq connection), raw ``stdin`` isn't expected to be
734 747 available.
735 748
736 749
737 750 Heartbeat for kernels
738 751 =====================
739 752
740 753 Initially we had considered using messages like those above over ZMQ for a
741 754 kernel 'heartbeat' (a way to detect quickly and reliably whether a kernel is
742 755 alive at all, even if it may be busy executing user code). But this has the
743 756 problem that if the kernel is locked inside extension code, it wouldn't execute
744 757 the python heartbeat code. But it turns out that we can implement a basic
745 758 heartbeat with pure ZMQ, without using any Python messaging at all.
746 759
747 760 The monitor sends out a single zmq message (right now, it is a str of the
748 761 monitor's lifetime in seconds), and gets the same message right back, prefixed
749 762 with the zmq identity of the XREQ socket in the heartbeat process. This can be
750 763 a uuid, or even a full message, but there doesn't seem to be a need for packing
751 764 up a message when the sender and receiver are the exact same Python object.
752 765
753 766 The model is this::
754 767
755 768 monitor.send(str(self.lifetime)) # '1.2345678910'
756 769
757 770 and the monitor receives some number of messages of the form::
758 771
759 772 ['uuid-abcd-dead-beef', '1.2345678910']
760 773
761 774 where the first part is the zmq.IDENTITY of the heart's XREQ on the engine, and
762 775 the rest is the message sent by the monitor. No Python code ever has any
763 776 access to the message between the monitor's send, and the monitor's recv.
764 777
765 778
766 779 ToDo
767 780 ====
768 781
769 782 Missing things include:
770 783
771 784 * Important: finish thinking through the payload concept and API.
772 785
773 786 * Important: ensure that we have a good solution for magics like %edit. It's
774 787 likely that with the payload concept we can build a full solution, but not
775 788 100% clear yet.
776 789
777 790 * Finishing the details of the heartbeat protocol.
778 791
779 792 * Signal handling: specify what kind of information kernel should broadcast (or
780 793 not) when it receives signals.
781 794
782 795 .. include:: ../links.rst
General Comments 0
You need to be logged in to leave comments. Login now