##// END OF EJS Templates
Added heartbeat support.
Brian Granger -
Show More
@@ -0,0 +1,43
1 """The client and server for a basic ping-pong style heartbeat.
2 """
3
4 #-----------------------------------------------------------------------------
5 # Copyright (C) 2008-2010 The IPython Development Team
6 #
7 # Distributed under the terms of the BSD License. The full license is in
8 # the file COPYING, distributed as part of this software.
9 #-----------------------------------------------------------------------------
10
11 #-----------------------------------------------------------------------------
12 # Imports
13 #-----------------------------------------------------------------------------
14
15 import sys
16 from threading import Thread
17
18 import zmq
19
20 #-----------------------------------------------------------------------------
21 # Code
22 #-----------------------------------------------------------------------------
23
24
25 class Heartbeat(Thread):
26 "A simple ping-pong style heartbeat that runs in a thread."
27
28 def __init__(self, context, addr=('127.0.0.1', 0)):
29 Thread.__init__(self)
30 self.context = context
31 self.addr = addr
32 self.ip = addr[0]
33 self.port = addr[1]
34 self.daemon = True
35
36 def run(self):
37 self.socket = self.context.socket(zmq.REP)
38 if self.port == 0:
39 self.port = self.socket.bind_to_random_port('tcp://%s' % self.ip)
40 else:
41 self.socket.bind('tcp://%s:%i' % self.addr)
42 zmq.device(zmq.FORWARDER, self.socket, self.socket)
43
@@ -1,93 +1,109
1 1 """ Defines a convenient mix-in class for implementing Qt frontends.
2 2 """
3 3
4 4 class BaseFrontendMixin(object):
5 5 """ A mix-in class for implementing Qt frontends.
6 6
7 7 To handle messages of a particular type, frontends need only define an
8 8 appropriate handler method. For example, to handle 'stream' messaged, define
9 9 a '_handle_stream(msg)' method.
10 10 """
11 11
12 12 #---------------------------------------------------------------------------
13 13 # 'BaseFrontendMixin' concrete interface
14 14 #---------------------------------------------------------------------------
15 15
16 16 def _get_kernel_manager(self):
17 17 """ Returns the current kernel manager.
18 18 """
19 19 return self._kernel_manager
20 20
21 21 def _set_kernel_manager(self, kernel_manager):
22 22 """ Disconnect from the current kernel manager (if any) and set a new
23 23 kernel manager.
24 24 """
25 25 # Disconnect the old kernel manager, if necessary.
26 26 old_manager = self._kernel_manager
27 27 if old_manager is not None:
28 28 old_manager.started_channels.disconnect(self._started_channels)
29 29 old_manager.stopped_channels.disconnect(self._stopped_channels)
30 30
31 31 # Disconnect the old kernel manager's channels.
32 32 old_manager.sub_channel.message_received.disconnect(self._dispatch)
33 33 old_manager.xreq_channel.message_received.disconnect(self._dispatch)
34 old_manager.rep_channel.message_received.connect(self._dispatch)
35
34 old_manager.rep_channel.message_received.disconnect(self._dispatch)
35 old_manager.hb_channel.kernel_died.disconnect(self._handle_kernel_died)
36
36 37 # Handle the case where the old kernel manager is still listening.
37 38 if old_manager.channels_running:
38 39 self._stopped_channels()
39 40
40 41 # Set the new kernel manager.
41 42 self._kernel_manager = kernel_manager
42 43 if kernel_manager is None:
43 44 return
44 45
45 46 # Connect the new kernel manager.
46 47 kernel_manager.started_channels.connect(self._started_channels)
47 48 kernel_manager.stopped_channels.connect(self._stopped_channels)
48 49
49 50 # Connect the new kernel manager's channels.
50 51 kernel_manager.sub_channel.message_received.connect(self._dispatch)
51 52 kernel_manager.xreq_channel.message_received.connect(self._dispatch)
52 53 kernel_manager.rep_channel.message_received.connect(self._dispatch)
53
54 kernel_manager.hb_channel.kernel_died.connect(self._handle_kernel_died)
55
54 56 # Handle the case where the kernel manager started channels before
55 57 # we connected.
56 58 if kernel_manager.channels_running:
57 59 self._started_channels()
58 60
59 61 kernel_manager = property(_get_kernel_manager, _set_kernel_manager)
60 62
61 63 #---------------------------------------------------------------------------
62 64 # 'BaseFrontendMixin' abstract interface
63 65 #---------------------------------------------------------------------------
64 66
65 67 def _started_channels(self):
66 68 """ Called when the KernelManager channels have started listening or
67 69 when the frontend is assigned an already listening KernelManager.
68 70 """
69 71
70 72 def _stopped_channels(self):
71 73 """ Called when the KernelManager channels have stopped listening or
72 74 when a listening KernelManager is removed from the frontend.
73 75 """
74 76
75 77 #---------------------------------------------------------------------------
76 78 # 'BaseFrontendMixin' protected interface
77 79 #---------------------------------------------------------------------------
78 80
79 81 def _dispatch(self, msg):
80 82 """ Calls the frontend handler associated with the message type of the
81 83 given message.
82 84 """
83 85 msg_type = msg['msg_type']
84 86 handler = getattr(self, '_handle_' + msg_type, None)
85 87 if handler:
86 88 handler(msg)
87 89
88 90 def _is_from_this_session(self, msg):
89 91 """ Returns whether a reply from the kernel originated from a request
90 92 from this frontend.
91 93 """
92 94 session = self._kernel_manager.session.session
93 95 return msg['parent_header']['session'] == session
96
97 def _handle_kernel_died(self, since_last_heartbeat):
98 """ This is called when the ``kernel_died`` signal is emitted.
99
100 This method is called when the kernel heartbeat has not been
101 active for a certain amount of time. The typical action will be to
102 give the user the option of restarting the kernel.
103
104 Parameters
105 ----------
106 since_last_heartbeat : float
107 The time since the heartbeat was last received.
108 """
109 pass
@@ -1,425 +1,436
1 1 # Standard library imports
2 2 import signal
3 3 import sys
4 4
5 5 # System library imports
6 6 from pygments.lexers import PythonLexer
7 7 from PyQt4 import QtCore, QtGui
8 8
9 9 # Local imports
10 10 from IPython.core.inputsplitter import InputSplitter
11 11 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
12 12 from IPython.utils.traitlets import Bool
13 13 from bracket_matcher import BracketMatcher
14 14 from call_tip_widget import CallTipWidget
15 15 from completion_lexer import CompletionLexer
16 16 from console_widget import HistoryConsoleWidget
17 17 from pygments_highlighter import PygmentsHighlighter
18 18
19 19
20 20 class FrontendHighlighter(PygmentsHighlighter):
21 21 """ A PygmentsHighlighter that can be turned on and off and that ignores
22 22 prompts.
23 23 """
24 24
25 25 def __init__(self, frontend):
26 26 super(FrontendHighlighter, self).__init__(frontend._control.document())
27 27 self._current_offset = 0
28 28 self._frontend = frontend
29 29 self.highlighting_on = False
30 30
31 31 def highlightBlock(self, qstring):
32 32 """ Highlight a block of text. Reimplemented to highlight selectively.
33 33 """
34 34 if not self.highlighting_on:
35 35 return
36 36
37 37 # The input to this function is unicode string that may contain
38 38 # paragraph break characters, non-breaking spaces, etc. Here we acquire
39 39 # the string as plain text so we can compare it.
40 40 current_block = self.currentBlock()
41 41 string = self._frontend._get_block_plain_text(current_block)
42 42
43 43 # Decide whether to check for the regular or continuation prompt.
44 44 if current_block.contains(self._frontend._prompt_pos):
45 45 prompt = self._frontend._prompt
46 46 else:
47 47 prompt = self._frontend._continuation_prompt
48 48
49 49 # Don't highlight the part of the string that contains the prompt.
50 50 if string.startswith(prompt):
51 51 self._current_offset = len(prompt)
52 52 qstring.remove(0, len(prompt))
53 53 else:
54 54 self._current_offset = 0
55 55
56 56 PygmentsHighlighter.highlightBlock(self, qstring)
57 57
58 58 def rehighlightBlock(self, block):
59 59 """ Reimplemented to temporarily enable highlighting if disabled.
60 60 """
61 61 old = self.highlighting_on
62 62 self.highlighting_on = True
63 63 super(FrontendHighlighter, self).rehighlightBlock(block)
64 64 self.highlighting_on = old
65 65
66 66 def setFormat(self, start, count, format):
67 67 """ Reimplemented to highlight selectively.
68 68 """
69 69 start += self._current_offset
70 70 PygmentsHighlighter.setFormat(self, start, count, format)
71 71
72 72
73 73 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
74 74 """ A Qt frontend for a generic Python kernel.
75 75 """
76 76
77 77 # An option and corresponding signal for overriding the default kernel
78 78 # interrupt behavior.
79 79 custom_interrupt = Bool(False)
80 80 custom_interrupt_requested = QtCore.pyqtSignal()
81 81
82 82 # An option and corresponding signal for overriding the default kernel
83 83 # restart behavior.
84 84 custom_restart = Bool(False)
85 85 custom_restart_requested = QtCore.pyqtSignal()
86 86
87 87 # Emitted when an 'execute_reply' has been received from the kernel and
88 88 # processed by the FrontendWidget.
89 89 executed = QtCore.pyqtSignal(object)
90 90
91 91 # Protected class variables.
92 92 _input_splitter_class = InputSplitter
93 _possible_kernel_restart = Bool(False)
93 94
94 95 #---------------------------------------------------------------------------
95 96 # 'object' interface
96 97 #---------------------------------------------------------------------------
97 98
98 99 def __init__(self, *args, **kw):
99 100 super(FrontendWidget, self).__init__(*args, **kw)
100 101
101 102 # FrontendWidget protected variables.
102 103 self._bracket_matcher = BracketMatcher(self._control)
103 104 self._call_tip_widget = CallTipWidget(self._control)
104 105 self._completion_lexer = CompletionLexer(PythonLexer())
105 106 self._hidden = False
106 107 self._highlighter = FrontendHighlighter(self)
107 108 self._input_splitter = self._input_splitter_class(input_mode='block')
108 109 self._kernel_manager = None
109 110
110 111 # Configure the ConsoleWidget.
111 112 self.tab_width = 4
112 113 self._set_continuation_prompt('... ')
113 114
114 115 # Connect signal handlers.
115 116 document = self._control.document()
116 117 document.contentsChange.connect(self._document_contents_change)
117 118
118 119 #---------------------------------------------------------------------------
119 120 # 'ConsoleWidget' abstract interface
120 121 #---------------------------------------------------------------------------
121 122
122 123 def _is_complete(self, source, interactive):
123 124 """ Returns whether 'source' can be completely processed and a new
124 125 prompt created. When triggered by an Enter/Return key press,
125 126 'interactive' is True; otherwise, it is False.
126 127 """
127 128 complete = self._input_splitter.push(source.expandtabs(4))
128 129 if interactive:
129 130 complete = not self._input_splitter.push_accepts_more()
130 131 return complete
131 132
132 133 def _execute(self, source, hidden):
133 134 """ Execute 'source'. If 'hidden', do not show any output.
134 135 """
135 136 self.kernel_manager.xreq_channel.execute(source, hidden)
136 137 self._hidden = hidden
137 138
138 139 def _prompt_started_hook(self):
139 140 """ Called immediately after a new prompt is displayed.
140 141 """
141 142 if not self._reading:
142 143 self._highlighter.highlighting_on = True
143 144
144 145 def _prompt_finished_hook(self):
145 146 """ Called immediately after a prompt is finished, i.e. when some input
146 147 will be processed and a new prompt displayed.
147 148 """
148 149 if not self._reading:
149 150 self._highlighter.highlighting_on = False
150 151
151 152 def _tab_pressed(self):
152 153 """ Called when the tab key is pressed. Returns whether to continue
153 154 processing the event.
154 155 """
155 156 # Perform tab completion if:
156 157 # 1) The cursor is in the input buffer.
157 158 # 2) There is a non-whitespace character before the cursor.
158 159 text = self._get_input_buffer_cursor_line()
159 160 if text is None:
160 161 return False
161 162 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
162 163 if complete:
163 164 self._complete()
164 165 return not complete
165 166
166 167 #---------------------------------------------------------------------------
167 168 # 'ConsoleWidget' protected interface
168 169 #---------------------------------------------------------------------------
169 170
170 171 def _event_filter_console_keypress(self, event):
171 172 """ Reimplemented to allow execution interruption.
172 173 """
173 174 key = event.key()
174 175 if self._control_key_down(event.modifiers()):
175 176 if key == QtCore.Qt.Key_C and self._executing:
176 177 self._kernel_interrupt()
177 178 return True
178 179 elif key == QtCore.Qt.Key_Period:
179 self._kernel_restart()
180 message = 'Are you sure you want to restart the kernel?'
181 self._kernel_restart(message)
180 182 return True
181 183 return super(FrontendWidget, self)._event_filter_console_keypress(event)
182 184
183 185 def _insert_continuation_prompt(self, cursor):
184 186 """ Reimplemented for auto-indentation.
185 187 """
186 188 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
187 189 spaces = self._input_splitter.indent_spaces
188 190 cursor.insertText('\t' * (spaces / self.tab_width))
189 191 cursor.insertText(' ' * (spaces % self.tab_width))
190 192
191 193 #---------------------------------------------------------------------------
192 194 # 'BaseFrontendMixin' abstract interface
193 195 #---------------------------------------------------------------------------
194 196
195 197 def _handle_complete_reply(self, rep):
196 198 """ Handle replies for tab completion.
197 199 """
198 200 cursor = self._get_cursor()
199 201 if rep['parent_header']['msg_id'] == self._complete_id and \
200 202 cursor.position() == self._complete_pos:
201 203 text = '.'.join(self._get_context())
202 204 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
203 205 self._complete_with_items(cursor, rep['content']['matches'])
204 206
205 207 def _handle_execute_reply(self, msg):
206 208 """ Handles replies for code execution.
207 209 """
208 210 if not self._hidden:
209 211 # Make sure that all output from the SUB channel has been processed
210 212 # before writing a new prompt.
211 213 self.kernel_manager.sub_channel.flush()
212 214
213 215 content = msg['content']
214 216 status = content['status']
215 217 if status == 'ok':
216 218 self._process_execute_ok(msg)
217 219 elif status == 'error':
218 220 self._process_execute_error(msg)
219 221 elif status == 'abort':
220 222 self._process_execute_abort(msg)
221 223
222 224 self._show_interpreter_prompt_for_reply(msg)
223 225 self.executed.emit(msg)
224 226
225 227 def _handle_input_request(self, msg):
226 228 """ Handle requests for raw_input.
227 229 """
228 230 if self._hidden:
229 231 raise RuntimeError('Request for raw input during hidden execution.')
230 232
231 233 # Make sure that all output from the SUB channel has been processed
232 234 # before entering readline mode.
233 235 self.kernel_manager.sub_channel.flush()
234 236
235 237 def callback(line):
236 238 self.kernel_manager.rep_channel.input(line)
237 239 self._readline(msg['content']['prompt'], callback=callback)
238 240
239 241 def _handle_object_info_reply(self, rep):
240 242 """ Handle replies for call tips.
241 243 """
242 244 cursor = self._get_cursor()
243 245 if rep['parent_header']['msg_id'] == self._call_tip_id and \
244 246 cursor.position() == self._call_tip_pos:
245 247 doc = rep['content']['docstring']
246 248 if doc:
247 249 self._call_tip_widget.show_docstring(doc)
248 250
249 251 def _handle_pyout(self, msg):
250 252 """ Handle display hook output.
251 253 """
252 254 if not self._hidden and self._is_from_this_session(msg):
253 255 self._append_plain_text(msg['content']['data'] + '\n')
254 256
255 257 def _handle_stream(self, msg):
256 258 """ Handle stdout, stderr, and stdin.
257 259 """
258 260 if not self._hidden and self._is_from_this_session(msg):
259 261 self._append_plain_text(msg['content']['data'])
260 262 self._control.moveCursor(QtGui.QTextCursor.End)
261 263
262 264 def _started_channels(self):
263 265 """ Called when the KernelManager channels have started listening or
264 266 when the frontend is assigned an already listening KernelManager.
265 267 """
266 268 self._control.clear()
267 269 self._append_plain_text(self._get_banner())
268 270 self._show_interpreter_prompt()
269 271
270 272 def _stopped_channels(self):
271 273 """ Called when the KernelManager channels have stopped listening or
272 274 when a listening KernelManager is removed from the frontend.
273 275 """
274 276 self._executing = self._reading = False
275 277 self._highlighter.highlighting_on = False
276 278
277 279 #---------------------------------------------------------------------------
278 280 # 'FrontendWidget' interface
279 281 #---------------------------------------------------------------------------
280 282
281 283 def execute_file(self, path, hidden=False):
282 284 """ Attempts to execute file with 'path'. If 'hidden', no output is
283 285 shown.
284 286 """
285 287 self.execute('execfile("%s")' % path, hidden=hidden)
286 288
287 289 #---------------------------------------------------------------------------
288 290 # 'FrontendWidget' protected interface
289 291 #---------------------------------------------------------------------------
290 292
291 293 def _call_tip(self):
292 294 """ Shows a call tip, if appropriate, at the current cursor location.
293 295 """
294 296 # Decide if it makes sense to show a call tip
295 297 cursor = self._get_cursor()
296 298 cursor.movePosition(QtGui.QTextCursor.Left)
297 299 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
298 300 return False
299 301 context = self._get_context(cursor)
300 302 if not context:
301 303 return False
302 304
303 305 # Send the metadata request to the kernel
304 306 name = '.'.join(context)
305 307 self._call_tip_id = self.kernel_manager.xreq_channel.object_info(name)
306 308 self._call_tip_pos = self._get_cursor().position()
307 309 return True
308 310
309 311 def _complete(self):
310 312 """ Performs completion at the current cursor location.
311 313 """
312 314 context = self._get_context()
313 315 if context:
314 316 # Send the completion request to the kernel
315 317 self._complete_id = self.kernel_manager.xreq_channel.complete(
316 318 '.'.join(context), # text
317 319 self._get_input_buffer_cursor_line(), # line
318 320 self._get_input_buffer_cursor_column(), # cursor_pos
319 321 self.input_buffer) # block
320 322 self._complete_pos = self._get_cursor().position()
321 323
322 324 def _get_banner(self):
323 325 """ Gets a banner to display at the beginning of a session.
324 326 """
325 327 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
326 328 '"license" for more information.'
327 329 return banner % (sys.version, sys.platform)
328 330
329 331 def _get_context(self, cursor=None):
330 332 """ Gets the context for the specified cursor (or the current cursor
331 333 if none is specified).
332 334 """
333 335 if cursor is None:
334 336 cursor = self._get_cursor()
335 337 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
336 338 QtGui.QTextCursor.KeepAnchor)
337 339 text = str(cursor.selection().toPlainText())
338 340 return self._completion_lexer.get_context(text)
339 341
340 342 def _kernel_interrupt(self):
341 343 """ Attempts to interrupt the running kernel.
342 344 """
343 345 if self.custom_interrupt:
344 346 self.custom_interrupt_requested.emit()
345 347 elif self.kernel_manager.has_kernel:
346 348 self.kernel_manager.signal_kernel(signal.SIGINT)
347 349 else:
348 350 self._append_plain_text('Kernel process is either remote or '
349 351 'unspecified. Cannot interrupt.\n')
350 352
351 def _kernel_restart(self):
353 def _kernel_restart(self, message):
352 354 """ Attempts to restart the running kernel.
353 355 """
354 if self.custom_restart:
355 self.custom_restart_requested.emit()
356 elif self.kernel_manager.has_kernel:
357 message = 'Are you sure you want to restart the kernel?'
358 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
359 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
360 message, buttons)
361 if result == QtGui.QMessageBox.Yes:
362 try:
363 self.kernel_manager.restart_kernel()
364 except RuntimeError:
365 message = 'Kernel started externally. Cannot restart.\n'
366 self._append_plain_text(message)
367 else:
368 self._stopped_channels()
369 self._append_plain_text('Kernel restarting...\n')
370 self._show_interpreter_prompt()
371 else:
372 self._append_plain_text('Kernel process is either remote or '
373 'unspecified. Cannot restart.\n')
356 # We want to make sure that if this dialog is already happening, that
357 # other signals don't trigger it again. This can happen when the
358 # kernel_died heartbeat signal is emitted and the user is slow to
359 # respond to the dialog.
360 if not self._possible_kernel_restart:
361 if self.custom_restart:
362 self.custom_restart_requested.emit()
363 elif self.kernel_manager.has_kernel:
364 # Setting this to True will prevent this logic from happening
365 # again until the current pass is completed.
366 self._possible_kernel_restart = True
367 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
368 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
369 message, buttons)
370 if result == QtGui.QMessageBox.Yes:
371 try:
372 self.kernel_manager.restart_kernel()
373 except RuntimeError:
374 message = 'Kernel started externally. Cannot restart.\n'
375 self._append_plain_text(message)
376 else:
377 self._stopped_channels()
378 self._append_plain_text('Kernel restarting...\n')
379 self._show_interpreter_prompt()
380 # This might need to be moved to another location?
381 self._possible_kernel_restart = False
382 else:
383 self._append_plain_text('Kernel process is either remote or '
384 'unspecified. Cannot restart.\n')
374 385
375 386 def _process_execute_abort(self, msg):
376 387 """ Process a reply for an aborted execution request.
377 388 """
378 389 self._append_plain_text("ERROR: execution aborted\n")
379 390
380 391 def _process_execute_error(self, msg):
381 392 """ Process a reply for an execution request that resulted in an error.
382 393 """
383 394 content = msg['content']
384 395 traceback = ''.join(content['traceback'])
385 396 self._append_plain_text(traceback)
386 397
387 398 def _process_execute_ok(self, msg):
388 399 """ Process a reply for a successful execution equest.
389 400 """
390 401 payload = msg['content']['payload']
391 402 for item in payload:
392 403 if not self._process_execute_payload(item):
393 404 warning = 'Received unknown payload of type %s\n'
394 405 self._append_plain_text(warning % repr(item['source']))
395 406
396 407 def _process_execute_payload(self, item):
397 408 """ Process a single payload item from the list of payload items in an
398 409 execution reply. Returns whether the payload was handled.
399 410 """
400 411 # The basic FrontendWidget doesn't handle payloads, as they are a
401 412 # mechanism for going beyond the standard Python interpreter model.
402 413 return False
403 414
404 415 def _show_interpreter_prompt(self):
405 416 """ Shows a prompt for the interpreter.
406 417 """
407 418 self._show_prompt('>>> ')
408 419
409 420 def _show_interpreter_prompt_for_reply(self, msg):
410 421 """ Shows a prompt for the interpreter given an 'execute_reply' message.
411 422 """
412 423 self._show_interpreter_prompt()
413 424
414 425 #------ Signal handlers ----------------------------------------------------
415 426
416 427 def _document_contents_change(self, position, removed, added):
417 428 """ Called whenever the document's content changes. Display a call tip
418 429 if appropriate.
419 430 """
420 431 # Calculate where the cursor should be *after* the change:
421 432 position += added
422 433
423 434 document = self._control.document()
424 435 if position == self._get_cursor().position():
425 436 self._call_tip()
@@ -1,359 +1,367
1 1 """ A FrontendWidget that emulates the interface of the console IPython and
2 2 supports the additional functionality provided by the IPython kernel.
3 3
4 4 TODO: Add support for retrieving the system default editor. Requires code
5 5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
6 6 Linux (use the xdg system).
7 7 """
8 8
9 9 # Standard library imports
10 10 from collections import namedtuple
11 11 from subprocess import Popen
12 12
13 13 # System library imports
14 14 from PyQt4 import QtCore, QtGui
15 15
16 16 # Local imports
17 17 from IPython.core.inputsplitter import IPythonInputSplitter
18 18 from IPython.core.usage import default_banner
19 19 from IPython.utils.traitlets import Bool, Str
20 20 from frontend_widget import FrontendWidget
21 21
22 22 # The default style sheet: black text on a white background.
23 23 default_style_sheet = '''
24 24 .error { color: red; }
25 25 .in-prompt { color: navy; }
26 26 .in-prompt-number { font-weight: bold; }
27 27 .out-prompt { color: darkred; }
28 28 .out-prompt-number { font-weight: bold; }
29 29 '''
30 30 default_syntax_style = 'default'
31 31
32 32 # A dark style sheet: white text on a black background.
33 33 dark_style_sheet = '''
34 34 QPlainTextEdit, QTextEdit { background-color: black; color: white }
35 35 QFrame { border: 1px solid grey; }
36 36 .error { color: red; }
37 37 .in-prompt { color: lime; }
38 38 .in-prompt-number { color: lime; font-weight: bold; }
39 39 .out-prompt { color: red; }
40 40 .out-prompt-number { color: red; font-weight: bold; }
41 41 '''
42 42 dark_syntax_style = 'monokai'
43 43
44 44 # Default prompts.
45 45 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
46 46 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
47 47
48 48
49 49 class IPythonWidget(FrontendWidget):
50 50 """ A FrontendWidget for an IPython kernel.
51 51 """
52 52
53 53 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
54 54 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
55 55 # settings.
56 56 custom_edit = Bool(False)
57 57 custom_edit_requested = QtCore.pyqtSignal(object, object)
58 58
59 59 # A command for invoking a system text editor. If the string contains a
60 60 # {filename} format specifier, it will be used. Otherwise, the filename will
61 61 # be appended to the end the command.
62 62 editor = Str('default', config=True)
63 63
64 64 # The editor command to use when a specific line number is requested. The
65 65 # string should contain two format specifiers: {line} and {filename}. If
66 66 # this parameter is not specified, the line number option to the %edit magic
67 67 # will be ignored.
68 68 editor_line = Str(config=True)
69 69
70 70 # A CSS stylesheet. The stylesheet can contain classes for:
71 71 # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
72 72 # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
73 73 # 3. IPython: .error, .in-prompt, .out-prompt, etc
74 74 style_sheet = Str(default_style_sheet, config=True)
75 75
76 76 # If not empty, use this Pygments style for syntax highlighting. Otherwise,
77 77 # the style sheet is queried for Pygments style information.
78 78 syntax_style = Str(default_syntax_style, config=True)
79 79
80 80 # Prompts.
81 81 in_prompt = Str(default_in_prompt, config=True)
82 82 out_prompt = Str(default_out_prompt, config=True)
83 83
84 84 # FrontendWidget protected class variables.
85 85 _input_splitter_class = IPythonInputSplitter
86 86
87 87 # IPythonWidget protected class variables.
88 88 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
89 89 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
90 90 _payload_source_page = 'IPython.zmq.page.page'
91 91
92 92 #---------------------------------------------------------------------------
93 93 # 'object' interface
94 94 #---------------------------------------------------------------------------
95 95
96 96 def __init__(self, *args, **kw):
97 97 super(IPythonWidget, self).__init__(*args, **kw)
98 98
99 99 # IPythonWidget protected variables.
100 100 self._previous_prompt_obj = None
101 101
102 102 # Initialize widget styling.
103 103 self._style_sheet_changed()
104 104 self._syntax_style_changed()
105 105
106 106 #---------------------------------------------------------------------------
107 107 # 'BaseFrontendMixin' abstract interface
108 108 #---------------------------------------------------------------------------
109 109
110 110 def _handle_complete_reply(self, rep):
111 111 """ Reimplemented to support IPython's improved completion machinery.
112 112 """
113 113 cursor = self._get_cursor()
114 114 if rep['parent_header']['msg_id'] == self._complete_id and \
115 115 cursor.position() == self._complete_pos:
116 116 # The completer tells us what text was actually used for the
117 117 # matching, so we must move that many characters left to apply the
118 118 # completions.
119 119 text = rep['content']['matched_text']
120 120 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
121 121 self._complete_with_items(cursor, rep['content']['matches'])
122 122
123 123 def _handle_history_reply(self, msg):
124 124 """ Implemented to handle history replies, which are only supported by
125 125 the IPython kernel.
126 126 """
127 127 history_dict = msg['content']['history']
128 128 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
129 129 self._set_history(items)
130 130
131 131 def _handle_prompt_reply(self, msg):
132 132 """ Implemented to handle prompt number replies, which are only
133 133 supported by the IPython kernel.
134 134 """
135 135 content = msg['content']
136 136 self._show_interpreter_prompt(content['prompt_number'],
137 137 content['input_sep'])
138 138
139 139 def _handle_pyout(self, msg):
140 140 """ Reimplemented for IPython-style "display hook".
141 141 """
142 142 if not self._hidden and self._is_from_this_session(msg):
143 143 content = msg['content']
144 144 prompt_number = content['prompt_number']
145 145 self._append_plain_text(content['output_sep'])
146 146 self._append_html(self._make_out_prompt(prompt_number))
147 147 self._append_plain_text(content['data'] + '\n' +
148 148 content['output_sep2'])
149 149
150 150 def _started_channels(self):
151 151 """ Reimplemented to make a history request.
152 152 """
153 153 super(IPythonWidget, self)._started_channels()
154 154 # FIXME: Disabled until history requests are properly implemented.
155 155 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
156 156
157 def _handle_kernel_died(self, since_last_heartbeat):
158 """ Handle the kernel's death by asking if the user wants to restart.
159 """
160 message = 'The kernel heartbeat has been inactive for %.2f ' \
161 'seconds. Do you want to restart the kernel? You may ' \
162 'first want to check the network connection.' % since_last_heartbeat
163 self._kernel_restart(message)
164
157 165 #---------------------------------------------------------------------------
158 166 # 'FrontendWidget' interface
159 167 #---------------------------------------------------------------------------
160 168
161 169 def execute_file(self, path, hidden=False):
162 170 """ Reimplemented to use the 'run' magic.
163 171 """
164 172 self.execute('%%run %s' % path, hidden=hidden)
165 173
166 174 #---------------------------------------------------------------------------
167 175 # 'FrontendWidget' protected interface
168 176 #---------------------------------------------------------------------------
169 177
170 178 def _complete(self):
171 179 """ Reimplemented to support IPython's improved completion machinery.
172 180 """
173 181 # We let the kernel split the input line, so we *always* send an empty
174 182 # text field. Readline-based frontends do get a real text field which
175 183 # they can use.
176 184 text = ''
177 185
178 186 # Send the completion request to the kernel
179 187 self._complete_id = self.kernel_manager.xreq_channel.complete(
180 188 text, # text
181 189 self._get_input_buffer_cursor_line(), # line
182 190 self._get_input_buffer_cursor_column(), # cursor_pos
183 191 self.input_buffer) # block
184 192 self._complete_pos = self._get_cursor().position()
185 193
186 194 def _get_banner(self):
187 195 """ Reimplemented to return IPython's default banner.
188 196 """
189 197 return default_banner + '\n'
190 198
191 199 def _process_execute_error(self, msg):
192 200 """ Reimplemented for IPython-style traceback formatting.
193 201 """
194 202 content = msg['content']
195 203 traceback = '\n'.join(content['traceback']) + '\n'
196 204 if False:
197 205 # FIXME: For now, tracebacks come as plain text, so we can't use
198 206 # the html renderer yet. Once we refactor ultratb to produce
199 207 # properly styled tracebacks, this branch should be the default
200 208 traceback = traceback.replace(' ', '&nbsp;')
201 209 traceback = traceback.replace('\n', '<br/>')
202 210
203 211 ename = content['ename']
204 212 ename_styled = '<span class="error">%s</span>' % ename
205 213 traceback = traceback.replace(ename, ename_styled)
206 214
207 215 self._append_html(traceback)
208 216 else:
209 217 # This is the fallback for now, using plain text with ansi escapes
210 218 self._append_plain_text(traceback)
211 219
212 220 def _process_execute_payload(self, item):
213 221 """ Reimplemented to handle %edit and paging payloads.
214 222 """
215 223 if item['source'] == self._payload_source_edit:
216 224 self._edit(item['filename'], item['line_number'])
217 225 return True
218 226 elif item['source'] == self._payload_source_page:
219 227 self._page(item['data'])
220 228 return True
221 229 else:
222 230 return False
223 231
224 232 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
225 233 """ Reimplemented for IPython-style prompts.
226 234 """
227 235 # If a number was not specified, make a prompt number request.
228 236 if number is None:
229 237 self.kernel_manager.xreq_channel.prompt()
230 238 return
231 239
232 240 # Show a new prompt and save information about it so that it can be
233 241 # updated later if the prompt number turns out to be wrong.
234 242 self._prompt_sep = input_sep
235 243 self._show_prompt(self._make_in_prompt(number), html=True)
236 244 block = self._control.document().lastBlock()
237 245 length = len(self._prompt)
238 246 self._previous_prompt_obj = self._PromptBlock(block, length, number)
239 247
240 248 # Update continuation prompt to reflect (possibly) new prompt length.
241 249 self._set_continuation_prompt(
242 250 self._make_continuation_prompt(self._prompt), html=True)
243 251
244 252 def _show_interpreter_prompt_for_reply(self, msg):
245 253 """ Reimplemented for IPython-style prompts.
246 254 """
247 255 # Update the old prompt number if necessary.
248 256 content = msg['content']
249 257 previous_prompt_number = content['prompt_number']
250 258 if self._previous_prompt_obj and \
251 259 self._previous_prompt_obj.number != previous_prompt_number:
252 260 block = self._previous_prompt_obj.block
253 261
254 262 # Make sure the prompt block has not been erased.
255 263 if block.isValid() and not block.text().isEmpty():
256 264
257 265 # Remove the old prompt and insert a new prompt.
258 266 cursor = QtGui.QTextCursor(block)
259 267 cursor.movePosition(QtGui.QTextCursor.Right,
260 268 QtGui.QTextCursor.KeepAnchor,
261 269 self._previous_prompt_obj.length)
262 270 prompt = self._make_in_prompt(previous_prompt_number)
263 271 self._prompt = self._insert_html_fetching_plain_text(
264 272 cursor, prompt)
265 273
266 274 # When the HTML is inserted, Qt blows away the syntax
267 275 # highlighting for the line, so we need to rehighlight it.
268 276 self._highlighter.rehighlightBlock(cursor.block())
269 277
270 278 self._previous_prompt_obj = None
271 279
272 280 # Show a new prompt with the kernel's estimated prompt number.
273 281 next_prompt = content['next_prompt']
274 282 self._show_interpreter_prompt(next_prompt['prompt_number'],
275 283 next_prompt['input_sep'])
276 284
277 285 #---------------------------------------------------------------------------
278 286 # 'IPythonWidget' protected interface
279 287 #---------------------------------------------------------------------------
280 288
281 289 def _edit(self, filename, line=None):
282 290 """ Opens a Python script for editing.
283 291
284 292 Parameters:
285 293 -----------
286 294 filename : str
287 295 A path to a local system file.
288 296
289 297 line : int, optional
290 298 A line of interest in the file.
291 299 """
292 300 if self.custom_edit:
293 301 self.custom_edit_requested.emit(filename, line)
294 302 elif self.editor == 'default':
295 303 self._append_plain_text('No default editor available.\n')
296 304 else:
297 305 try:
298 306 filename = '"%s"' % filename
299 307 if line and self.editor_line:
300 308 command = self.editor_line.format(filename=filename,
301 309 line=line)
302 310 else:
303 311 try:
304 312 command = self.editor.format()
305 313 except KeyError:
306 314 command = self.editor.format(filename=filename)
307 315 else:
308 316 command += ' ' + filename
309 317 except KeyError:
310 318 self._append_plain_text('Invalid editor command.\n')
311 319 else:
312 320 try:
313 321 Popen(command, shell=True)
314 322 except OSError:
315 323 msg = 'Opening editor with command "%s" failed.\n'
316 324 self._append_plain_text(msg % command)
317 325
318 326 def _make_in_prompt(self, number):
319 327 """ Given a prompt number, returns an HTML In prompt.
320 328 """
321 329 body = self.in_prompt % number
322 330 return '<span class="in-prompt">%s</span>' % body
323 331
324 332 def _make_continuation_prompt(self, prompt):
325 333 """ Given a plain text version of an In prompt, returns an HTML
326 334 continuation prompt.
327 335 """
328 336 end_chars = '...: '
329 337 space_count = len(prompt.lstrip('\n')) - len(end_chars)
330 338 body = '&nbsp;' * space_count + end_chars
331 339 return '<span class="in-prompt">%s</span>' % body
332 340
333 341 def _make_out_prompt(self, number):
334 342 """ Given a prompt number, returns an HTML Out prompt.
335 343 """
336 344 body = self.out_prompt % number
337 345 return '<span class="out-prompt">%s</span>' % body
338 346
339 347 #------ Trait change handlers ---------------------------------------------
340 348
341 349 def _style_sheet_changed(self):
342 350 """ Set the style sheets of the underlying widgets.
343 351 """
344 352 self.setStyleSheet(self.style_sheet)
345 353 self._control.document().setDefaultStyleSheet(self.style_sheet)
346 354 if self._page_control:
347 355 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
348 356
349 357 bg_color = self._control.palette().background().color()
350 358 self._ansi_processor.set_background_color(bg_color)
351 359
352 360 def _syntax_style_changed(self):
353 361 """ Set the style for the syntax highlighter.
354 362 """
355 363 if self.syntax_style:
356 364 self._highlighter.set_style(self.syntax_style)
357 365 else:
358 366 self._highlighter.set_style_sheet(self.style_sheet)
359 367
@@ -1,99 +1,102
1 1 #!/usr/bin/env python
2 2
3 3 """ A minimal application using the Qt console-style IPython frontend.
4 4 """
5 5
6 6 # Systemm library imports
7 7 from PyQt4 import QtGui
8 8
9 9 # Local imports
10 10 from IPython.external.argparse import ArgumentParser
11 11 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
12 12 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
13 13 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
14 14 from IPython.frontend.qt.kernelmanager import QtKernelManager
15 15
16 16 # Constants
17 17 LOCALHOST = '127.0.0.1'
18 18
19 19
20 20 def main():
21 21 """ Entry point for application.
22 22 """
23 23 # Parse command line arguments.
24 24 parser = ArgumentParser()
25 25 kgroup = parser.add_argument_group('kernel options')
26 26 kgroup.add_argument('-e', '--existing', action='store_true',
27 27 help='connect to an existing kernel')
28 28 kgroup.add_argument('--ip', type=str, default=LOCALHOST,
29 29 help='set the kernel\'s IP address [default localhost]')
30 30 kgroup.add_argument('--xreq', type=int, metavar='PORT', default=0,
31 31 help='set the XREQ channel port [default random]')
32 32 kgroup.add_argument('--sub', type=int, metavar='PORT', default=0,
33 33 help='set the SUB channel port [default random]')
34 34 kgroup.add_argument('--rep', type=int, metavar='PORT', default=0,
35 35 help='set the REP channel port [default random]')
36 kgroup.add_argument('--hb', type=int, metavar='PORT', default=0,
37 help='set the heartbeat port [default: random]')
36 38
37 39 egroup = kgroup.add_mutually_exclusive_group()
38 40 egroup.add_argument('--pure', action='store_true', help = \
39 41 'use a pure Python kernel instead of an IPython kernel')
40 42 egroup.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
41 43 const='auto', help = \
42 44 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
43 45 given, the GUI backend is matplotlib's, otherwise use one of: \
44 46 ['tk', 'gtk', 'qt', 'wx', 'payload-svg'].")
45 47
46 48 wgroup = parser.add_argument_group('widget options')
47 49 wgroup.add_argument('--paging', type=str, default='inside',
48 50 choices = ['inside', 'hsplit', 'vsplit', 'none'],
49 51 help='set the paging style [default inside]')
50 52 wgroup.add_argument('--rich', action='store_true',
51 53 help='enable rich text support')
52 54 wgroup.add_argument('--tab-simple', action='store_true',
53 55 help='do tab completion ala a Unix terminal')
54 56
55 57 args = parser.parse_args()
56 58
57 59 # Don't let Qt or ZMQ swallow KeyboardInterupts.
58 60 import signal
59 61 signal.signal(signal.SIGINT, signal.SIG_DFL)
60 62
61 63 # Create a KernelManager and start a kernel.
62 64 kernel_manager = QtKernelManager(xreq_address=(args.ip, args.xreq),
63 65 sub_address=(args.ip, args.sub),
64 rep_address=(args.ip, args.rep))
66 rep_address=(args.ip, args.rep),
67 hb_address=(args.ip, args.hb))
65 68 if args.ip == LOCALHOST and not args.existing:
66 69 if args.pure:
67 70 kernel_manager.start_kernel(ipython=False)
68 71 elif args.pylab:
69 72 if args.rich:
70 73 kernel_manager.start_kernel(pylab='payload-svg')
71 74 else:
72 75 if args.pylab == 'auto':
73 76 kernel_manager.start_kernel(pylab='qt4')
74 77 else:
75 78 kernel_manager.start_kernel(pylab=args.pylab)
76 79 else:
77 80 kernel_manager.start_kernel()
78 81 kernel_manager.start_channels()
79 82
80 83 # Create the widget.
81 84 app = QtGui.QApplication([])
82 85 if args.pure:
83 86 kind = 'rich' if args.rich else 'plain'
84 87 widget = FrontendWidget(kind=kind, paging=args.paging)
85 88 elif args.rich:
86 89 widget = RichIPythonWidget(paging=args.paging)
87 90 else:
88 91 widget = IPythonWidget(paging=args.paging)
89 92 widget.gui_completion = not args.tab_simple
90 93 widget.kernel_manager = kernel_manager
91 94 widget.setWindowTitle('Python' if args.pure else 'IPython')
92 95 widget.show()
93 96
94 97 # Start the application main loop.
95 98 app.exec_()
96 99
97 100
98 101 if __name__ == '__main__':
99 102 main()
@@ -1,193 +1,220
1 1 """ Defines a KernelManager that provides signals and slots.
2 2 """
3 3
4 4 # System library imports.
5 5 from PyQt4 import QtCore
6 6 import zmq
7 7
8 8 # IPython imports.
9 9 from IPython.utils.traitlets import Type
10 10 from IPython.zmq.kernelmanager import KernelManager, SubSocketChannel, \
11 XReqSocketChannel, RepSocketChannel
11 XReqSocketChannel, RepSocketChannel, HBSocketChannel
12 12 from util import MetaQObjectHasTraits
13 13
14 14 # When doing multiple inheritance from QtCore.QObject and other classes
15 15 # the calling of the parent __init__'s is a subtle issue:
16 16 # * QtCore.QObject does not call super so you can't use super and put
17 17 # QObject first in the inheritance list.
18 18 # * QtCore.QObject.__init__ takes 1 argument, the parent. So if you are going
19 19 # to use super, any class that comes before QObject must pass it something
20 20 # reasonable.
21 21 # In summary, I don't think using super in these situations will work.
22 22 # Instead we will need to call the __init__ methods of both parents
23 23 # by hand. Not pretty, but it works.
24 24
25 25 class QtSubSocketChannel(SubSocketChannel, QtCore.QObject):
26 26
27 27 # Emitted when any message is received.
28 28 message_received = QtCore.pyqtSignal(object)
29 29
30 30 # Emitted when a message of type 'stream' is received.
31 31 stream_received = QtCore.pyqtSignal(object)
32 32
33 33 # Emitted when a message of type 'pyin' is received.
34 34 pyin_received = QtCore.pyqtSignal(object)
35 35
36 36 # Emitted when a message of type 'pyout' is received.
37 37 pyout_received = QtCore.pyqtSignal(object)
38 38
39 39 # Emitted when a message of type 'pyerr' is received.
40 40 pyerr_received = QtCore.pyqtSignal(object)
41 41
42 42 # Emitted when a crash report message is received from the kernel's
43 43 # last-resort sys.excepthook.
44 44 crash_received = QtCore.pyqtSignal(object)
45 45
46 46 #---------------------------------------------------------------------------
47 47 # 'object' interface
48 48 #---------------------------------------------------------------------------
49 49
50 50 def __init__(self, *args, **kw):
51 51 """ Reimplemented to ensure that QtCore.QObject is initialized first.
52 52 """
53 53 QtCore.QObject.__init__(self)
54 54 SubSocketChannel.__init__(self, *args, **kw)
55 55
56 56 #---------------------------------------------------------------------------
57 57 # 'SubSocketChannel' interface
58 58 #---------------------------------------------------------------------------
59 59
60 60 def call_handlers(self, msg):
61 61 """ Reimplemented to emit signals instead of making callbacks.
62 62 """
63 63 # Emit the generic signal.
64 64 self.message_received.emit(msg)
65 65
66 66 # Emit signals for specialized message types.
67 67 msg_type = msg['msg_type']
68 68 signal = getattr(self, msg_type + '_received', None)
69 69 if signal:
70 70 signal.emit(msg)
71 71 elif msg_type in ('stdout', 'stderr'):
72 72 self.stream_received.emit(msg)
73 73
74 74 def flush(self):
75 75 """ Reimplemented to ensure that signals are dispatched immediately.
76 76 """
77 77 super(QtSubSocketChannel, self).flush()
78 78 QtCore.QCoreApplication.instance().processEvents()
79 79
80 80
81 81 class QtXReqSocketChannel(XReqSocketChannel, QtCore.QObject):
82 82
83 83 # Emitted when any message is received.
84 84 message_received = QtCore.pyqtSignal(object)
85 85
86 86 # Emitted when a reply has been received for the corresponding request type.
87 87 execute_reply = QtCore.pyqtSignal(object)
88 88 complete_reply = QtCore.pyqtSignal(object)
89 89 object_info_reply = QtCore.pyqtSignal(object)
90 90
91 91 #---------------------------------------------------------------------------
92 92 # 'object' interface
93 93 #---------------------------------------------------------------------------
94 94
95 95 def __init__(self, *args, **kw):
96 96 """ Reimplemented to ensure that QtCore.QObject is initialized first.
97 97 """
98 98 QtCore.QObject.__init__(self)
99 99 XReqSocketChannel.__init__(self, *args, **kw)
100 100
101 101 #---------------------------------------------------------------------------
102 102 # 'XReqSocketChannel' interface
103 103 #---------------------------------------------------------------------------
104 104
105 105 def call_handlers(self, msg):
106 106 """ Reimplemented to emit signals instead of making callbacks.
107 107 """
108 108 # Emit the generic signal.
109 109 self.message_received.emit(msg)
110 110
111 111 # Emit signals for specialized message types.
112 112 msg_type = msg['msg_type']
113 113 signal = getattr(self, msg_type, None)
114 114 if signal:
115 115 signal.emit(msg)
116 116
117 117
118 118 class QtRepSocketChannel(RepSocketChannel, QtCore.QObject):
119 119
120 120 # Emitted when any message is received.
121 121 message_received = QtCore.pyqtSignal(object)
122 122
123 123 # Emitted when an input request is received.
124 124 input_requested = QtCore.pyqtSignal(object)
125 125
126 126 #---------------------------------------------------------------------------
127 127 # 'object' interface
128 128 #---------------------------------------------------------------------------
129 129
130 130 def __init__(self, *args, **kw):
131 131 """ Reimplemented to ensure that QtCore.QObject is initialized first.
132 132 """
133 133 QtCore.QObject.__init__(self)
134 134 RepSocketChannel.__init__(self, *args, **kw)
135 135
136 136 #---------------------------------------------------------------------------
137 137 # 'RepSocketChannel' interface
138 138 #---------------------------------------------------------------------------
139 139
140 140 def call_handlers(self, msg):
141 141 """ Reimplemented to emit signals instead of making callbacks.
142 142 """
143 143 # Emit the generic signal.
144 144 self.message_received.emit(msg)
145 145
146 146 # Emit signals for specialized message types.
147 147 msg_type = msg['msg_type']
148 148 if msg_type == 'input_request':
149 149 self.input_requested.emit(msg)
150 150
151 151
152 class QtHBSocketChannel(HBSocketChannel, QtCore.QObject):
153
154 # Emitted when the kernel has died.
155 kernel_died = QtCore.pyqtSignal(object)
156
157 #---------------------------------------------------------------------------
158 # 'object' interface
159 #---------------------------------------------------------------------------
160
161 def __init__(self, *args, **kw):
162 """ Reimplemented to ensure that QtCore.QObject is initialized first.
163 """
164 QtCore.QObject.__init__(self)
165 HBSocketChannel.__init__(self, *args, **kw)
166
167 #---------------------------------------------------------------------------
168 # 'RepSocketChannel' interface
169 #---------------------------------------------------------------------------
170
171 def call_handlers(self, since_last_heartbeat):
172 """ Reimplemented to emit signals instead of making callbacks.
173 """
174 # Emit the generic signal.
175 self.kernel_died.emit(since_last_heartbeat)
176
177
152 178 class QtKernelManager(KernelManager, QtCore.QObject):
153 179 """ A KernelManager that provides signals and slots.
154 180 """
155 181
156 182 __metaclass__ = MetaQObjectHasTraits
157 183
158 184 # Emitted when the kernel manager has started listening.
159 185 started_channels = QtCore.pyqtSignal()
160 186
161 187 # Emitted when the kernel manager has stopped listening.
162 188 stopped_channels = QtCore.pyqtSignal()
163 189
164 190 # Use Qt-specific channel classes that emit signals.
165 191 sub_channel_class = Type(QtSubSocketChannel)
166 192 xreq_channel_class = Type(QtXReqSocketChannel)
167 193 rep_channel_class = Type(QtRepSocketChannel)
194 hb_channel_class = Type(QtHBSocketChannel)
168 195
169 196 #---------------------------------------------------------------------------
170 197 # 'object' interface
171 198 #---------------------------------------------------------------------------
172 199
173 200 def __init__(self, *args, **kw):
174 201 """ Reimplemented to ensure that QtCore.QObject is initialized first.
175 202 """
176 203 QtCore.QObject.__init__(self)
177 204 KernelManager.__init__(self, *args, **kw)
178 205
179 206 #---------------------------------------------------------------------------
180 207 # 'KernelManager' interface
181 208 #---------------------------------------------------------------------------
182 209
183 210 def start_channels(self):
184 211 """ Reimplemented to emit signal.
185 212 """
186 213 super(QtKernelManager, self).start_channels()
187 214 self.started_channels.emit()
188 215
189 216 def stop_channels(self):
190 217 """ Reimplemented to emit signal.
191 218 """
192 219 super(QtKernelManager, self).stop_channels()
193 220 self.stopped_channels.emit()
@@ -1,199 +1,212
1 1 """ Defines helper functions for creating kernel entry points and process
2 2 launchers.
3 3 """
4 4
5 5 # Standard library imports.
6 6 import os
7 7 import socket
8 8 from subprocess import Popen
9 9 import sys
10 10
11 11 # System library imports.
12 12 import zmq
13 13
14 14 # Local imports.
15 15 from IPython.core.ultratb import FormattedTB
16 16 from IPython.external.argparse import ArgumentParser
17 17 from IPython.utils import io
18 18 from exitpoller import ExitPollerUnix, ExitPollerWindows
19 19 from displayhook import DisplayHook
20 20 from iostream import OutStream
21 21 from session import Session
22
22 from heartbeat import Heartbeat
23 23
24 24 def bind_port(socket, ip, port):
25 25 """ Binds the specified ZMQ socket. If the port is zero, a random port is
26 26 chosen. Returns the port that was bound.
27 27 """
28 28 connection = 'tcp://%s' % ip
29 29 if port <= 0:
30 30 port = socket.bind_to_random_port(connection)
31 31 else:
32 32 connection += ':%i' % port
33 33 socket.bind(connection)
34 34 return port
35 35
36 36
37 37 def make_argument_parser():
38 38 """ Creates an ArgumentParser for the generic arguments supported by all
39 39 kernel entry points.
40 40 """
41 41 parser = ArgumentParser()
42 42 parser.add_argument('--ip', type=str, default='127.0.0.1',
43 43 help='set the kernel\'s IP address [default: local]')
44 44 parser.add_argument('--xrep', type=int, metavar='PORT', default=0,
45 45 help='set the XREP channel port [default: random]')
46 46 parser.add_argument('--pub', type=int, metavar='PORT', default=0,
47 47 help='set the PUB channel port [default: random]')
48 48 parser.add_argument('--req', type=int, metavar='PORT', default=0,
49 49 help='set the REQ channel port [default: random]')
50 parser.add_argument('--hb', type=int, metavar='PORT', default=0,
51 help='set the heartbeat port [default: random]')
50 52
51 53 if sys.platform == 'win32':
52 54 parser.add_argument('--parent', type=int, metavar='HANDLE',
53 55 default=0, help='kill this process if the process '
54 56 'with HANDLE dies')
55 57 else:
56 58 parser.add_argument('--parent', action='store_true',
57 59 help='kill this process if its parent dies')
58 60
59 61 return parser
60 62
61 63
62 64 def make_kernel(namespace, kernel_factory,
63 65 out_stream_factory=None, display_hook_factory=None):
64 66 """ Creates a kernel.
65 67 """
66 68 # Install minimal exception handling
67 69 sys.excepthook = FormattedTB(mode='Verbose', color_scheme='NoColor',
68 70 ostream=sys.__stdout__)
69 71
70 72 # Create a context, a session, and the kernel sockets.
71 73 io.raw_print("Starting the kernel...")
72 74 context = zmq.Context()
73 75 session = Session(username=u'kernel')
74 76
75 77 reply_socket = context.socket(zmq.XREP)
76 78 xrep_port = bind_port(reply_socket, namespace.ip, namespace.xrep)
77 79 io.raw_print("XREP Channel on port", xrep_port)
78 80
79 81 pub_socket = context.socket(zmq.PUB)
80 82 pub_port = bind_port(pub_socket, namespace.ip, namespace.pub)
81 83 io.raw_print("PUB Channel on port", pub_port)
82 84
83 85 req_socket = context.socket(zmq.XREQ)
84 86 req_port = bind_port(req_socket, namespace.ip, namespace.req)
85 87 io.raw_print("REQ Channel on port", req_port)
86 88
89 hb = Heartbeat(context, (namespace.ip, namespace.hb))
90 hb.start()
91 io.raw_print("Heartbeat REP Channel on port", hb.port)
92
87 93 # Redirect input streams and set a display hook.
88 94 if out_stream_factory:
89 95 pass
90 96 sys.stdout = out_stream_factory(session, pub_socket, u'stdout')
91 97 sys.stderr = out_stream_factory(session, pub_socket, u'stderr')
92 98 if display_hook_factory:
93 99 sys.displayhook = display_hook_factory(session, pub_socket)
94 100
95 101 # Create the kernel.
96 102 return kernel_factory(session=session, reply_socket=reply_socket,
97 103 pub_socket=pub_socket, req_socket=req_socket)
98 104
99 105
100 106 def start_kernel(namespace, kernel):
101 107 """ Starts a kernel.
102 108 """
103 109 # Configure this kernel/process to die on parent termination, if necessary.
104 110 if namespace.parent:
105 111 if sys.platform == 'win32':
106 112 poller = ExitPollerWindows(namespace.parent)
107 113 else:
108 114 poller = ExitPollerUnix()
109 115 poller.start()
110 116
111 117 # Start the kernel mainloop.
112 118 kernel.start()
113 119
114 120
115 121 def make_default_main(kernel_factory):
116 122 """ Creates the simplest possible kernel entry point.
117 123 """
118 124 def main():
119 125 namespace = make_argument_parser().parse_args()
120 126 kernel = make_kernel(namespace, kernel_factory, OutStream, DisplayHook)
121 127 start_kernel(namespace, kernel)
122 128 return main
123 129
124 130
125 def base_launch_kernel(code, xrep_port=0, pub_port=0, req_port=0,
131 def base_launch_kernel(code, xrep_port=0, pub_port=0, req_port=0, hb_port=0,
126 132 independent=False, extra_arguments=[]):
127 133 """ Launches a localhost kernel, binding to the specified ports.
128 134
129 135 Parameters
130 136 ----------
131 137 code : str,
132 138 A string of Python code that imports and executes a kernel entry point.
133 139
134 140 xrep_port : int, optional
135 141 The port to use for XREP channel.
136 142
137 143 pub_port : int, optional
138 144 The port to use for the SUB channel.
139 145
140 146 req_port : int, optional
141 147 The port to use for the REQ (raw input) channel.
142 148
149 hb_port : int, optional
150 The port to use for the hearbeat REP channel.
151
143 152 independent : bool, optional (default False)
144 153 If set, the kernel process is guaranteed to survive if this process
145 154 dies. If not set, an effort is made to ensure that the kernel is killed
146 155 when this process dies. Note that in this case it is still good practice
147 156 to kill kernels manually before exiting.
148 157
149 158 extra_arguments = list, optional
150 159 A list of extra arguments to pass when executing the launch code.
151 160
152 161 Returns
153 162 -------
154 163 A tuple of form:
155 164 (kernel_process, xrep_port, pub_port, req_port)
156 165 where kernel_process is a Popen object and the ports are integers.
157 166 """
158 167 # Find open ports as necessary.
159 168 ports = []
160 ports_needed = int(xrep_port <= 0) + int(pub_port <= 0) + int(req_port <= 0)
169 ports_needed = int(xrep_port <= 0) + int(pub_port <= 0) + \
170 int(req_port <= 0) + int(hb_port <= 0)
161 171 for i in xrange(ports_needed):
162 172 sock = socket.socket()
163 173 sock.bind(('', 0))
164 174 ports.append(sock)
165 175 for i, sock in enumerate(ports):
166 176 port = sock.getsockname()[1]
167 177 sock.close()
168 178 ports[i] = port
169 179 if xrep_port <= 0:
170 180 xrep_port = ports.pop(0)
171 181 if pub_port <= 0:
172 182 pub_port = ports.pop(0)
173 183 if req_port <= 0:
174 184 req_port = ports.pop(0)
185 if hb_port <= 0:
186 hb_port = ports.pop(0)
175 187
176 188 # Build the kernel launch command.
177 189 arguments = [ sys.executable, '-c', code, '--xrep', str(xrep_port),
178 '--pub', str(pub_port), '--req', str(req_port) ]
190 '--pub', str(pub_port), '--req', str(req_port),
191 '--hb', str(hb_port) ]
179 192 arguments.extend(extra_arguments)
180 193
181 194 # Spawn a kernel.
182 195 if independent:
183 196 if sys.platform == 'win32':
184 197 proc = Popen(['start', '/b'] + arguments, shell=True)
185 198 else:
186 199 proc = Popen(arguments, preexec_fn=lambda: os.setsid())
187 200 else:
188 201 if sys.platform == 'win32':
189 202 from _subprocess import DuplicateHandle, GetCurrentProcess, \
190 203 DUPLICATE_SAME_ACCESS
191 204 pid = GetCurrentProcess()
192 205 handle = DuplicateHandle(pid, pid, pid, 0,
193 206 True, # Inheritable by new processes.
194 207 DUPLICATE_SAME_ACCESS)
195 208 proc = Popen(arguments + ['--parent', str(int(handle))])
196 209 else:
197 210 proc = Popen(arguments + ['--parent'])
198 211
199 return proc, xrep_port, pub_port, req_port
212 return proc, xrep_port, pub_port, req_port, hb_port
@@ -1,459 +1,462
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 sys
21 21 import time
22 22 import traceback
23 23
24 24 # System library imports.
25 25 import zmq
26 26
27 27 # Local imports.
28 28 from IPython.config.configurable import Configurable
29 29 from IPython.utils import io
30 30 from IPython.lib import pylabtools
31 31 from IPython.utils.traitlets import Instance
32 32 from entry_point import base_launch_kernel, make_argument_parser, make_kernel, \
33 33 start_kernel
34 34 from iostream import OutStream
35 35 from session import Session, Message
36 36 from zmqshell import ZMQInteractiveShell
37 37
38 38 #-----------------------------------------------------------------------------
39 39 # Main kernel class
40 40 #-----------------------------------------------------------------------------
41 41
42 42 class Kernel(Configurable):
43 43
44 44 #---------------------------------------------------------------------------
45 45 # Kernel interface
46 46 #---------------------------------------------------------------------------
47 47
48 48 shell = Instance('IPython.core.interactiveshell.InteractiveShellABC')
49 49 session = Instance(Session)
50 50 reply_socket = Instance('zmq.Socket')
51 51 pub_socket = Instance('zmq.Socket')
52 52 req_socket = Instance('zmq.Socket')
53 53
54 54 def __init__(self, **kwargs):
55 55 super(Kernel, self).__init__(**kwargs)
56 56
57 57 # Initialize the InteractiveShell subclass
58 58 self.shell = ZMQInteractiveShell.instance()
59 59 self.shell.displayhook.session = self.session
60 60 self.shell.displayhook.pub_socket = self.pub_socket
61 61
62 62 # TMP - hack while developing
63 63 self.shell._reply_content = None
64 64
65 65 # Build dict of handlers for message types
66 66 msg_types = [ 'execute_request', 'complete_request',
67 67 'object_info_request', 'prompt_request',
68 68 'history_request' ]
69 69 self.handlers = {}
70 70 for msg_type in msg_types:
71 71 self.handlers[msg_type] = getattr(self, msg_type)
72 72
73 73 def do_one_iteration(self):
74 74 try:
75 75 ident = self.reply_socket.recv(zmq.NOBLOCK)
76 76 except zmq.ZMQError, e:
77 77 if e.errno == zmq.EAGAIN:
78 78 return
79 79 else:
80 80 raise
81 81 # FIXME: Bug in pyzmq/zmq?
82 82 # assert self.reply_socket.rcvmore(), "Missing message part."
83 83 msg = self.reply_socket.recv_json()
84 84 omsg = Message(msg)
85 85 io.raw_print('\n')
86 86 io.raw_print(omsg)
87 87 handler = self.handlers.get(omsg.msg_type, None)
88 88 if handler is None:
89 89 io.raw_print_err("UNKNOWN MESSAGE TYPE:", omsg)
90 90 else:
91 91 handler(ident, omsg)
92 92
93 93 def start(self):
94 94 """ Start the kernel main loop.
95 95 """
96 96 while True:
97 97 time.sleep(0.05)
98 98 self.do_one_iteration()
99 99
100 100
101 101 #---------------------------------------------------------------------------
102 102 # Kernel request handlers
103 103 #---------------------------------------------------------------------------
104 104
105 105 def execute_request(self, ident, parent):
106 106 try:
107 107 code = parent[u'content'][u'code']
108 108 except:
109 109 io.raw_print_err("Got bad msg: ")
110 110 io.raw_print_err(Message(parent))
111 111 return
112 112 pyin_msg = self.session.msg(u'pyin',{u'code':code}, parent=parent)
113 113 self.pub_socket.send_json(pyin_msg)
114 114
115 115 try:
116 116 # Replace raw_input. Note that is not sufficient to replace
117 117 # raw_input in the user namespace.
118 118 raw_input = lambda prompt='': self._raw_input(prompt, ident, parent)
119 119 __builtin__.raw_input = raw_input
120 120
121 121 # Set the parent message of the display hook and out streams.
122 122 self.shell.displayhook.set_parent(parent)
123 123 sys.stdout.set_parent(parent)
124 124 sys.stderr.set_parent(parent)
125 125
126 126 # FIXME: runlines calls the exception handler itself. We should
127 127 # clean this up.
128 128 self.shell._reply_content = None
129 129 self.shell.runlines(code)
130 130 except:
131 131 # FIXME: this code right now isn't being used yet by default,
132 132 # because the runlines() call above directly fires off exception
133 133 # reporting. This code, therefore, is only active in the scenario
134 134 # where runlines itself has an unhandled exception. We need to
135 135 # uniformize this, for all exception construction to come from a
136 136 # single location in the codbase.
137 137 etype, evalue, tb = sys.exc_info()
138 138 tb_list = traceback.format_exception(etype, evalue, tb)
139 139 reply_content = self.shell._showtraceback(etype, evalue, tb_list)
140 140 else:
141 141 payload = self.shell.payload_manager.read_payload()
142 142 # Be agressive about clearing the payload because we don't want
143 143 # it to sit in memory until the next execute_request comes in.
144 144 self.shell.payload_manager.clear_payload()
145 145 reply_content = { 'status' : 'ok', 'payload' : payload }
146 146
147 147 # Compute the prompt information
148 148 prompt_number = self.shell.displayhook.prompt_count
149 149 reply_content['prompt_number'] = prompt_number
150 150 prompt_string = self.shell.displayhook.prompt1.peek_next_prompt()
151 151 next_prompt = {'prompt_string' : prompt_string,
152 152 'prompt_number' : prompt_number+1,
153 153 'input_sep' : self.shell.displayhook.input_sep}
154 154 reply_content['next_prompt'] = next_prompt
155 155
156 156 # TMP - fish exception info out of shell, possibly left there by
157 157 # runlines
158 158 if self.shell._reply_content is not None:
159 159 reply_content.update(self.shell._reply_content)
160 160
161 161 # Flush output before sending the reply.
162 162 sys.stderr.flush()
163 163 sys.stdout.flush()
164 164
165 165 # Send the reply.
166 166 reply_msg = self.session.msg(u'execute_reply', reply_content, parent)
167 167 io.raw_print(Message(reply_msg))
168 168 self.reply_socket.send(ident, zmq.SNDMORE)
169 169 self.reply_socket.send_json(reply_msg)
170 170 if reply_msg['content']['status'] == u'error':
171 171 self._abort_queue()
172 172
173 173 def complete_request(self, ident, parent):
174 174 txt, matches = self._complete(parent)
175 175 matches = {'matches' : matches,
176 176 'matched_text' : txt,
177 177 'status' : 'ok'}
178 178 completion_msg = self.session.send(self.reply_socket, 'complete_reply',
179 179 matches, parent, ident)
180 180 io.raw_print(completion_msg)
181 181
182 182 def object_info_request(self, ident, parent):
183 183 context = parent['content']['oname'].split('.')
184 184 object_info = self._object_info(context)
185 185 msg = self.session.send(self.reply_socket, 'object_info_reply',
186 186 object_info, parent, ident)
187 187 io.raw_print(msg)
188 188
189 189 def prompt_request(self, ident, parent):
190 190 prompt_number = self.shell.displayhook.prompt_count
191 191 prompt_string = self.shell.displayhook.prompt1.peek_next_prompt()
192 192 content = {'prompt_string' : prompt_string,
193 193 'prompt_number' : prompt_number+1,
194 194 'input_sep' : self.shell.displayhook.input_sep}
195 195 msg = self.session.send(self.reply_socket, 'prompt_reply',
196 196 content, parent, ident)
197 197 io.raw_print(msg)
198 198
199 199 def history_request(self, ident, parent):
200 200 output = parent['content']['output']
201 201 index = parent['content']['index']
202 202 raw = parent['content']['raw']
203 203 hist = self.shell.get_history(index=index, raw=raw, output=output)
204 204 content = {'history' : hist}
205 205 msg = self.session.send(self.reply_socket, 'history_reply',
206 206 content, parent, ident)
207 207 io.raw_print(msg)
208 208
209 209 #---------------------------------------------------------------------------
210 210 # Protected interface
211 211 #---------------------------------------------------------------------------
212 212
213 213 def _abort_queue(self):
214 214 while True:
215 215 try:
216 216 ident = self.reply_socket.recv(zmq.NOBLOCK)
217 217 except zmq.ZMQError, e:
218 218 if e.errno == zmq.EAGAIN:
219 219 break
220 220 else:
221 221 assert self.reply_socket.rcvmore(), "Unexpected missing message part."
222 222 msg = self.reply_socket.recv_json()
223 223 io.raw_print("Aborting:\n", Message(msg))
224 224 msg_type = msg['msg_type']
225 225 reply_type = msg_type.split('_')[0] + '_reply'
226 226 reply_msg = self.session.msg(reply_type, {'status' : 'aborted'}, msg)
227 227 io.raw_print(Message(reply_msg))
228 228 self.reply_socket.send(ident,zmq.SNDMORE)
229 229 self.reply_socket.send_json(reply_msg)
230 230 # We need to wait a bit for requests to come in. This can probably
231 231 # be set shorter for true asynchronous clients.
232 232 time.sleep(0.1)
233 233
234 234 def _raw_input(self, prompt, ident, parent):
235 235 # Flush output before making the request.
236 236 sys.stderr.flush()
237 237 sys.stdout.flush()
238 238
239 239 # Send the input request.
240 240 content = dict(prompt=prompt)
241 241 msg = self.session.msg(u'input_request', content, parent)
242 242 self.req_socket.send_json(msg)
243 243
244 244 # Await a response.
245 245 reply = self.req_socket.recv_json()
246 246 try:
247 247 value = reply['content']['value']
248 248 except:
249 249 io.raw_print_err("Got bad raw_input reply: ")
250 250 io.raw_print_err(Message(parent))
251 251 value = ''
252 252 return value
253 253
254 254 def _complete(self, msg):
255 255 c = msg['content']
256 256 try:
257 257 cpos = int(c['cursor_pos'])
258 258 except:
259 259 # If we don't get something that we can convert to an integer, at
260 260 # least attempt the completion guessing the cursor is at the end of
261 261 # the text, if there's any, and otherwise of the line
262 262 cpos = len(c['text'])
263 263 if cpos==0:
264 264 cpos = len(c['line'])
265 265 return self.shell.complete(c['text'], c['line'], cpos)
266 266
267 267 def _object_info(self, context):
268 268 symbol, leftover = self._symbol_from_context(context)
269 269 if symbol is not None and not leftover:
270 270 doc = getattr(symbol, '__doc__', '')
271 271 else:
272 272 doc = ''
273 273 object_info = dict(docstring = doc)
274 274 return object_info
275 275
276 276 def _symbol_from_context(self, context):
277 277 if not context:
278 278 return None, context
279 279
280 280 base_symbol_string = context[0]
281 281 symbol = self.shell.user_ns.get(base_symbol_string, None)
282 282 if symbol is None:
283 283 symbol = __builtin__.__dict__.get(base_symbol_string, None)
284 284 if symbol is None:
285 285 return None, context
286 286
287 287 context = context[1:]
288 288 for i, name in enumerate(context):
289 289 new_symbol = getattr(symbol, name, None)
290 290 if new_symbol is None:
291 291 return symbol, context[i:]
292 292 else:
293 293 symbol = new_symbol
294 294
295 295 return symbol, []
296 296
297 297
298 298 class QtKernel(Kernel):
299 299 """A Kernel subclass with Qt support."""
300 300
301 301 def start(self):
302 302 """Start a kernel with QtPy4 event loop integration."""
303 303
304 304 from PyQt4 import QtGui, QtCore
305 305 from IPython.lib.guisupport import (
306 306 get_app_qt4, start_event_loop_qt4
307 307 )
308 308 self.app = get_app_qt4([" "])
309 309 self.app.setQuitOnLastWindowClosed(False)
310 310 self.timer = QtCore.QTimer()
311 311 self.timer.timeout.connect(self.do_one_iteration)
312 312 self.timer.start(50)
313 313 start_event_loop_qt4(self.app)
314 314
315 315 class WxKernel(Kernel):
316 316 """A Kernel subclass with Wx support."""
317 317
318 318 def start(self):
319 319 """Start a kernel with wx event loop support."""
320 320
321 321 import wx
322 322 from IPython.lib.guisupport import start_event_loop_wx
323 323 doi = self.do_one_iteration
324 324
325 325 # We have to put the wx.Timer in a wx.Frame for it to fire properly.
326 326 # We make the Frame hidden when we create it in the main app below.
327 327 class TimerFrame(wx.Frame):
328 328 def __init__(self, func):
329 329 wx.Frame.__init__(self, None, -1)
330 330 self.timer = wx.Timer(self)
331 331 self.timer.Start(50)
332 332 self.Bind(wx.EVT_TIMER, self.on_timer)
333 333 self.func = func
334 334 def on_timer(self, event):
335 335 self.func()
336 336
337 337 # We need a custom wx.App to create our Frame subclass that has the
338 338 # wx.Timer to drive the ZMQ event loop.
339 339 class IPWxApp(wx.App):
340 340 def OnInit(self):
341 341 self.frame = TimerFrame(doi)
342 342 self.frame.Show(False)
343 343 return True
344 344
345 345 # The redirect=False here makes sure that wx doesn't replace
346 346 # sys.stdout/stderr with its own classes.
347 347 self.app = IPWxApp(redirect=False)
348 348 start_event_loop_wx(self.app)
349 349
350 350
351 351 class TkKernel(Kernel):
352 352 """A Kernel subclass with Tk support."""
353 353
354 354 def start(self):
355 355 """Start a Tk enabled event loop."""
356 356
357 357 import Tkinter
358 358 doi = self.do_one_iteration
359 359
360 360 # For Tkinter, we create a Tk object and call its withdraw method.
361 361 class Timer(object):
362 362 def __init__(self, func):
363 363 self.app = Tkinter.Tk()
364 364 self.app.withdraw()
365 365 self.func = func
366 366 def on_timer(self):
367 367 self.func()
368 368 self.app.after(50, self.on_timer)
369 369 def start(self):
370 370 self.on_timer() # Call it once to get things going.
371 371 self.app.mainloop()
372 372
373 373 self.timer = Timer(doi)
374 374 self.timer.start()
375 375
376 376 #-----------------------------------------------------------------------------
377 377 # Kernel main and launch functions
378 378 #-----------------------------------------------------------------------------
379 379
380 def launch_kernel(xrep_port=0, pub_port=0, req_port=0, independent=False,
381 pylab=False):
380 def launch_kernel(xrep_port=0, pub_port=0, req_port=0, hb_port=0,
381 independent=False, pylab=False):
382 382 """ Launches a localhost kernel, binding to the specified ports.
383 383
384 384 Parameters
385 385 ----------
386 386 xrep_port : int, optional
387 387 The port to use for XREP channel.
388 388
389 389 pub_port : int, optional
390 390 The port to use for the SUB channel.
391 391
392 392 req_port : int, optional
393 393 The port to use for the REQ (raw input) channel.
394 394
395 hb_port : int, optional
396 The port to use for the hearbeat REP channel.
397
395 398 independent : bool, optional (default False)
396 399 If set, the kernel process is guaranteed to survive if this process
397 400 dies. If not set, an effort is made to ensure that the kernel is killed
398 401 when this process dies. Note that in this case it is still good practice
399 402 to kill kernels manually before exiting.
400 403
401 404 pylab : bool or string, optional (default False)
402 405 If not False, the kernel will be launched with pylab enabled. If a
403 406 string is passed, matplotlib will use the specified backend. Otherwise,
404 407 matplotlib's default backend will be used.
405 408
406 409 Returns
407 410 -------
408 411 A tuple of form:
409 412 (kernel_process, xrep_port, pub_port, req_port)
410 413 where kernel_process is a Popen object and the ports are integers.
411 414 """
412 415 extra_arguments = []
413 416 if pylab:
414 417 extra_arguments.append('--pylab')
415 418 if isinstance(pylab, basestring):
416 419 extra_arguments.append(pylab)
417 420 return base_launch_kernel('from IPython.zmq.ipkernel import main; main()',
418 xrep_port, pub_port, req_port, independent,
419 extra_arguments)
421 xrep_port, pub_port, req_port, hb_port,
422 independent, extra_arguments)
420 423
421 424 def main():
422 425 """ The IPython kernel main entry point.
423 426 """
424 427 parser = make_argument_parser()
425 428 parser.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
426 429 const='auto', help = \
427 430 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
428 431 given, the GUI backend is matplotlib's, otherwise use one of: \
429 432 ['tk', 'gtk', 'qt', 'wx', 'payload-svg'].")
430 433 namespace = parser.parse_args()
431 434
432 435 kernel_class = Kernel
433 436
434 437 _kernel_classes = {
435 438 'qt' : QtKernel,
436 439 'qt4' : QtKernel,
437 440 'payload-svg':Kernel,
438 441 'wx' : WxKernel,
439 442 'tk' : TkKernel
440 443 }
441 444 if namespace.pylab:
442 445 if namespace.pylab == 'auto':
443 446 gui, backend = pylabtools.find_gui_and_backend()
444 447 else:
445 448 gui, backend = pylabtools.find_gui_and_backend(namespace.pylab)
446 449 kernel_class = _kernel_classes.get(gui)
447 450 if kernel_class is None:
448 451 raise ValueError('GUI is not supported: %r' % gui)
449 452 pylabtools.activate_matplotlib(backend)
450 453
451 454 kernel = make_kernel(namespace, kernel_class, OutStream)
452 455
453 456 if namespace.pylab:
454 457 pylabtools.import_pylab(kernel.shell.user_ns)
455 458
456 459 start_kernel(namespace, kernel)
457 460
458 461 if __name__ == '__main__':
459 462 main()
@@ -1,632 +1,715
1 1 """Base classes to manage the interaction with a running kernel.
2 2
3 3 Todo
4 4 ====
5 5
6 6 * Create logger to handle debugging and console messages.
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2008-2010 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 # Standard library imports.
21 21 from Queue import Queue, Empty
22 22 from subprocess import Popen
23 23 from threading import Thread
24 24 import time
25 25
26 26 # System library imports.
27 27 import zmq
28 28 from zmq import POLLIN, POLLOUT, POLLERR
29 29 from zmq.eventloop import ioloop
30 30
31 31 # Local imports.
32 32 from IPython.utils.traitlets import HasTraits, Any, Instance, Type, TCPAddress
33 33 from session import Session
34 34
35 35 #-----------------------------------------------------------------------------
36 36 # Constants and exceptions
37 37 #-----------------------------------------------------------------------------
38 38
39 39 LOCALHOST = '127.0.0.1'
40 40
41 41 class InvalidPortNumber(Exception):
42 42 pass
43 43
44 44 #-----------------------------------------------------------------------------
45 45 # ZMQ Socket Channel classes
46 46 #-----------------------------------------------------------------------------
47 47
48 48 class ZmqSocketChannel(Thread):
49 49 """The base class for the channels that use ZMQ sockets.
50 50 """
51 51 context = None
52 52 session = None
53 53 socket = None
54 54 ioloop = None
55 55 iostate = None
56 56 _address = None
57 57
58 58 def __init__(self, context, session, address):
59 59 """Create a channel
60 60
61 61 Parameters
62 62 ----------
63 63 context : :class:`zmq.Context`
64 64 The ZMQ context to use.
65 65 session : :class:`session.Session`
66 66 The session to use.
67 67 address : tuple
68 68 Standard (ip, port) tuple that the kernel is listening on.
69 69 """
70 70 super(ZmqSocketChannel, self).__init__()
71 71 self.daemon = True
72 72
73 73 self.context = context
74 74 self.session = session
75 75 if address[1] == 0:
76 76 message = 'The port number for a channel cannot be 0.'
77 77 raise InvalidPortNumber(message)
78 78 self._address = address
79 79
80 80 def stop(self):
81 81 """Stop the channel's activity.
82 82
83 83 This calls :method:`Thread.join` and returns when the thread
84 84 terminates. :class:`RuntimeError` will be raised if
85 85 :method:`self.start` is called again.
86 86 """
87 87 self.join()
88 88
89 89 @property
90 90 def address(self):
91 91 """Get the channel's address as an (ip, port) tuple.
92 92
93 93 By the default, the address is (localhost, 0), where 0 means a random
94 94 port.
95 95 """
96 96 return self._address
97 97
98 98 def add_io_state(self, state):
99 99 """Add IO state to the eventloop.
100 100
101 101 Parameters
102 102 ----------
103 103 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
104 104 The IO state flag to set.
105 105
106 106 This is thread safe as it uses the thread safe IOLoop.add_callback.
107 107 """
108 108 def add_io_state_callback():
109 109 if not self.iostate & state:
110 110 self.iostate = self.iostate | state
111 111 self.ioloop.update_handler(self.socket, self.iostate)
112 112 self.ioloop.add_callback(add_io_state_callback)
113 113
114 114 def drop_io_state(self, state):
115 115 """Drop IO state from the eventloop.
116 116
117 117 Parameters
118 118 ----------
119 119 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
120 120 The IO state flag to set.
121 121
122 122 This is thread safe as it uses the thread safe IOLoop.add_callback.
123 123 """
124 124 def drop_io_state_callback():
125 125 if self.iostate & state:
126 126 self.iostate = self.iostate & (~state)
127 127 self.ioloop.update_handler(self.socket, self.iostate)
128 128 self.ioloop.add_callback(drop_io_state_callback)
129 129
130 130
131 131 class XReqSocketChannel(ZmqSocketChannel):
132 132 """The XREQ channel for issues request/replies to the kernel.
133 133 """
134 134
135 135 command_queue = None
136 136
137 137 def __init__(self, context, session, address):
138 138 self.command_queue = Queue()
139 139 super(XReqSocketChannel, self).__init__(context, session, address)
140 140
141 141 def run(self):
142 142 """The thread's main activity. Call start() instead."""
143 143 self.socket = self.context.socket(zmq.XREQ)
144 144 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
145 145 self.socket.connect('tcp://%s:%i' % self.address)
146 146 self.ioloop = ioloop.IOLoop()
147 147 self.iostate = POLLERR|POLLIN
148 148 self.ioloop.add_handler(self.socket, self._handle_events,
149 149 self.iostate)
150 150 self.ioloop.start()
151 151
152 152 def stop(self):
153 153 self.ioloop.stop()
154 154 super(XReqSocketChannel, self).stop()
155 155
156 156 def call_handlers(self, msg):
157 157 """This method is called in the ioloop thread when a message arrives.
158 158
159 159 Subclasses should override this method to handle incoming messages.
160 160 It is important to remember that this method is called in the thread
161 161 so that some logic must be done to ensure that the application leve
162 162 handlers are called in the application thread.
163 163 """
164 164 raise NotImplementedError('call_handlers must be defined in a subclass.')
165 165
166 166 def execute(self, code, silent=False):
167 167 """Execute code in the kernel.
168 168
169 169 Parameters
170 170 ----------
171 171 code : str
172 172 A string of Python code.
173 173 silent : bool, optional (default False)
174 174 If set, the kernel will execute the code as quietly possible.
175 175
176 176 Returns
177 177 -------
178 178 The msg_id of the message sent.
179 179 """
180 180 # Create class for content/msg creation. Related to, but possibly
181 181 # not in Session.
182 182 content = dict(code=code, silent=silent)
183 183 msg = self.session.msg('execute_request', content)
184 184 self._queue_request(msg)
185 185 return msg['header']['msg_id']
186 186
187 187 def complete(self, text, line, cursor_pos, block=None):
188 188 """Tab complete text in the kernel's namespace.
189 189
190 190 Parameters
191 191 ----------
192 192 text : str
193 193 The text to complete.
194 194 line : str
195 195 The full line of text that is the surrounding context for the
196 196 text to complete.
197 197 cursor_pos : int
198 198 The position of the cursor in the line where the completion was
199 199 requested.
200 200 block : str, optional
201 201 The full block of code in which the completion is being requested.
202 202
203 203 Returns
204 204 -------
205 205 The msg_id of the message sent.
206 206 """
207 207 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
208 208 msg = self.session.msg('complete_request', content)
209 209 self._queue_request(msg)
210 210 return msg['header']['msg_id']
211 211
212 212 def object_info(self, oname):
213 213 """Get metadata information about an object.
214 214
215 215 Parameters
216 216 ----------
217 217 oname : str
218 218 A string specifying the object name.
219 219
220 220 Returns
221 221 -------
222 222 The msg_id of the message sent.
223 223 """
224 224 content = dict(oname=oname)
225 225 msg = self.session.msg('object_info_request', content)
226 226 self._queue_request(msg)
227 227 return msg['header']['msg_id']
228 228
229 229 def history(self, index=None, raw=False, output=True):
230 230 """Get the history list.
231 231
232 232 Parameters
233 233 ----------
234 234 index : n or (n1, n2) or None
235 235 If n, then the last entries. If a tuple, then all in
236 236 range(n1, n2). If None, then all entries. Raises IndexError if
237 237 the format of index is incorrect.
238 238 raw : bool
239 239 If True, return the raw input.
240 240 output : bool
241 241 If True, then return the output as well.
242 242
243 243 Returns
244 244 -------
245 245 The msg_id of the message sent.
246 246 """
247 247 content = dict(index=index, raw=raw, output=output)
248 248 msg = self.session.msg('history_request', content)
249 249 self._queue_request(msg)
250 250 return msg['header']['msg_id']
251 251
252 252 def prompt(self):
253 253 """Requests a prompt number from the kernel.
254 254
255 255 Returns
256 256 -------
257 257 The msg_id of the message sent.
258 258 """
259 259 msg = self.session.msg('prompt_request')
260 260 self._queue_request(msg)
261 261 return msg['header']['msg_id']
262 262
263 263 def _handle_events(self, socket, events):
264 264 if events & POLLERR:
265 265 self._handle_err()
266 266 if events & POLLOUT:
267 267 self._handle_send()
268 268 if events & POLLIN:
269 269 self._handle_recv()
270 270
271 271 def _handle_recv(self):
272 272 msg = self.socket.recv_json()
273 273 self.call_handlers(msg)
274 274
275 275 def _handle_send(self):
276 276 try:
277 277 msg = self.command_queue.get(False)
278 278 except Empty:
279 279 pass
280 280 else:
281 281 self.socket.send_json(msg)
282 282 if self.command_queue.empty():
283 283 self.drop_io_state(POLLOUT)
284 284
285 285 def _handle_err(self):
286 286 # We don't want to let this go silently, so eventually we should log.
287 287 raise zmq.ZMQError()
288 288
289 289 def _queue_request(self, msg):
290 290 self.command_queue.put(msg)
291 291 self.add_io_state(POLLOUT)
292 292
293 293
294 294 class SubSocketChannel(ZmqSocketChannel):
295 295 """The SUB channel which listens for messages that the kernel publishes.
296 296 """
297 297
298 298 def __init__(self, context, session, address):
299 299 super(SubSocketChannel, self).__init__(context, session, address)
300 300
301 301 def run(self):
302 302 """The thread's main activity. Call start() instead."""
303 303 self.socket = self.context.socket(zmq.SUB)
304 304 self.socket.setsockopt(zmq.SUBSCRIBE,'')
305 305 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
306 306 self.socket.connect('tcp://%s:%i' % self.address)
307 307 self.ioloop = ioloop.IOLoop()
308 308 self.iostate = POLLIN|POLLERR
309 309 self.ioloop.add_handler(self.socket, self._handle_events,
310 310 self.iostate)
311 311 self.ioloop.start()
312 312
313 313 def stop(self):
314 314 self.ioloop.stop()
315 315 super(SubSocketChannel, self).stop()
316 316
317 317 def call_handlers(self, msg):
318 318 """This method is called in the ioloop thread when a message arrives.
319 319
320 320 Subclasses should override this method to handle incoming messages.
321 321 It is important to remember that this method is called in the thread
322 322 so that some logic must be done to ensure that the application leve
323 323 handlers are called in the application thread.
324 324 """
325 325 raise NotImplementedError('call_handlers must be defined in a subclass.')
326 326
327 327 def flush(self, timeout=1.0):
328 328 """Immediately processes all pending messages on the SUB channel.
329 329
330 330 Callers should use this method to ensure that :method:`call_handlers`
331 331 has been called for all messages that have been received on the
332 332 0MQ SUB socket of this channel.
333 333
334 334 This method is thread safe.
335 335
336 336 Parameters
337 337 ----------
338 338 timeout : float, optional
339 339 The maximum amount of time to spend flushing, in seconds. The
340 340 default is one second.
341 341 """
342 342 # We do the IOLoop callback process twice to ensure that the IOLoop
343 343 # gets to perform at least one full poll.
344 344 stop_time = time.time() + timeout
345 345 for i in xrange(2):
346 346 self._flushed = False
347 347 self.ioloop.add_callback(self._flush)
348 348 while not self._flushed and time.time() < stop_time:
349 349 time.sleep(0.01)
350 350
351 351 def _handle_events(self, socket, events):
352 352 # Turn on and off POLLOUT depending on if we have made a request
353 353 if events & POLLERR:
354 354 self._handle_err()
355 355 if events & POLLIN:
356 356 self._handle_recv()
357 357
358 358 def _handle_err(self):
359 359 # We don't want to let this go silently, so eventually we should log.
360 360 raise zmq.ZMQError()
361 361
362 362 def _handle_recv(self):
363 363 # Get all of the messages we can
364 364 while True:
365 365 try:
366 366 msg = self.socket.recv_json(zmq.NOBLOCK)
367 367 except zmq.ZMQError:
368 368 # Check the errno?
369 369 # Will this trigger POLLERR?
370 370 break
371 371 else:
372 372 self.call_handlers(msg)
373 373
374 374 def _flush(self):
375 375 """Callback for :method:`self.flush`."""
376 376 self._flushed = True
377 377
378 378
379 379 class RepSocketChannel(ZmqSocketChannel):
380 380 """A reply channel to handle raw_input requests that the kernel makes."""
381 381
382 382 msg_queue = None
383 383
384 384 def __init__(self, context, session, address):
385 385 self.msg_queue = Queue()
386 386 super(RepSocketChannel, self).__init__(context, session, address)
387 387
388 388 def run(self):
389 389 """The thread's main activity. Call start() instead."""
390 390 self.socket = self.context.socket(zmq.XREQ)
391 391 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
392 392 self.socket.connect('tcp://%s:%i' % self.address)
393 393 self.ioloop = ioloop.IOLoop()
394 394 self.iostate = POLLERR|POLLIN
395 395 self.ioloop.add_handler(self.socket, self._handle_events,
396 396 self.iostate)
397 397 self.ioloop.start()
398 398
399 399 def stop(self):
400 400 self.ioloop.stop()
401 401 super(RepSocketChannel, self).stop()
402 402
403 403 def call_handlers(self, msg):
404 404 """This method is called in the ioloop thread when a message arrives.
405 405
406 406 Subclasses should override this method to handle incoming messages.
407 407 It is important to remember that this method is called in the thread
408 408 so that some logic must be done to ensure that the application leve
409 409 handlers are called in the application thread.
410 410 """
411 411 raise NotImplementedError('call_handlers must be defined in a subclass.')
412 412
413 413 def input(self, string):
414 414 """Send a string of raw input to the kernel."""
415 415 content = dict(value=string)
416 416 msg = self.session.msg('input_reply', content)
417 417 self._queue_reply(msg)
418 418
419 419 def _handle_events(self, socket, events):
420 420 if events & POLLERR:
421 421 self._handle_err()
422 422 if events & POLLOUT:
423 423 self._handle_send()
424 424 if events & POLLIN:
425 425 self._handle_recv()
426 426
427 427 def _handle_recv(self):
428 428 msg = self.socket.recv_json()
429 429 self.call_handlers(msg)
430 430
431 431 def _handle_send(self):
432 432 try:
433 433 msg = self.msg_queue.get(False)
434 434 except Empty:
435 435 pass
436 436 else:
437 437 self.socket.send_json(msg)
438 438 if self.msg_queue.empty():
439 439 self.drop_io_state(POLLOUT)
440 440
441 441 def _handle_err(self):
442 442 # We don't want to let this go silently, so eventually we should log.
443 443 raise zmq.ZMQError()
444 444
445 445 def _queue_reply(self, msg):
446 446 self.msg_queue.put(msg)
447 447 self.add_io_state(POLLOUT)
448 448
449 449
450 class HBSocketChannel(ZmqSocketChannel):
451 """The heartbeat channel which monitors the kernel heartbeat.
452 """
453
454 time_to_dead = 5.0
455 socket = None
456 poller = None
457
458 def __init__(self, context, session, address):
459 super(HBSocketChannel, self).__init__(context, session, address)
460
461 def _create_socket(self):
462 self.socket = self.context.socket(zmq.REQ)
463 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
464 self.socket.connect('tcp://%s:%i' % self.address)
465 self.poller = zmq.Poller()
466 self.poller.register(self.socket, zmq.POLLIN)
467
468 def run(self):
469 """The thread's main activity. Call start() instead."""
470 self._create_socket()
471
472 while True:
473 since_last_heartbeat = 0.0
474 request_time = time.time()
475 try:
476 self.socket.send_json('ping')
477 except zmq.ZMQError, e:
478 if e.errno == zmq.EFSM:
479 time.sleep(self.time_to_dead)
480 self._create_socket()
481 else:
482 raise
483 else:
484 while True:
485 try:
486 reply = self.socket.recv_json(zmq.NOBLOCK)
487 except zmq.ZMQError, e:
488 if e.errno == zmq.EAGAIN:
489 until_dead = self.time_to_dead-(time.time()-request_time)
490 self.poller.poll(until_dead)
491 since_last_heartbeat = time.time() - request_time
492 if since_last_heartbeat > self.time_to_dead:
493 self.call_handlers(since_last_heartbeat)
494 break
495 else:
496 # We should probably log this instead
497 raise
498 else:
499 until_dead = self.time_to_dead-(time.time()-request_time)
500 if until_dead > 0.0:
501 time.sleep(until_dead)
502 break
503
504 def call_handlers(self, since_last_heartbeat):
505 """This method is called in the ioloop thread when a message arrives.
506
507 Subclasses should override this method to handle incoming messages.
508 It is important to remember that this method is called in the thread
509 so that some logic must be done to ensure that the application leve
510 handlers are called in the application thread.
511 """
512 raise NotImplementedError('call_handlers must be defined in a subclass.')
513
514
450 515 #-----------------------------------------------------------------------------
451 516 # Main kernel manager class
452 517 #-----------------------------------------------------------------------------
453 518
454 519 class KernelManager(HasTraits):
455 520 """ Manages a kernel for a frontend.
456 521
457 522 The SUB channel is for the frontend to receive messages published by the
458 523 kernel.
459 524
460 525 The REQ channel is for the frontend to make requests of the kernel.
461 526
462 527 The REP channel is for the kernel to request stdin (raw_input) from the
463 528 frontend.
464 529 """
465 530 # The PyZMQ Context to use for communication with the kernel.
466 531 context = Instance(zmq.Context,(),{})
467 532
468 533 # The Session to use for communication with the kernel.
469 534 session = Instance(Session,(),{})
470 535
471 536 # The kernel process with which the KernelManager is communicating.
472 537 kernel = Instance(Popen)
473 538
474 539 # The addresses for the communication channels.
475 540 xreq_address = TCPAddress((LOCALHOST, 0))
476 541 sub_address = TCPAddress((LOCALHOST, 0))
477 542 rep_address = TCPAddress((LOCALHOST, 0))
543 hb_address = TCPAddress((LOCALHOST, 0))
478 544
479 545 # The classes to use for the various channels.
480 546 xreq_channel_class = Type(XReqSocketChannel)
481 547 sub_channel_class = Type(SubSocketChannel)
482 548 rep_channel_class = Type(RepSocketChannel)
549 hb_channel_class = Type(HBSocketChannel)
483 550
484 551 # Protected traits.
485 552 _launch_args = Any
486 553 _xreq_channel = Any
487 554 _sub_channel = Any
488 555 _rep_channel = Any
556 _hb_channel = Any
489 557
490 558 #--------------------------------------------------------------------------
491 559 # Channel management methods:
492 560 #--------------------------------------------------------------------------
493 561
494 562 def start_channels(self):
495 563 """Starts the channels for this kernel.
496 564
497 565 This will create the channels if they do not exist and then start
498 566 them. If port numbers of 0 are being used (random ports) then you
499 567 must first call :method:`start_kernel`. If the channels have been
500 568 stopped and you call this, :class:`RuntimeError` will be raised.
501 569 """
502 570 self.xreq_channel.start()
503 571 self.sub_channel.start()
504 572 self.rep_channel.start()
573 self.hb_channel.start()
505 574
506 575 def stop_channels(self):
507 576 """Stops the channels for this kernel.
508 577
509 578 This stops the channels by joining their threads. If the channels
510 579 were not started, :class:`RuntimeError` will be raised.
511 580 """
512 581 self.xreq_channel.stop()
513 582 self.sub_channel.stop()
514 583 self.rep_channel.stop()
584 self.hb_channel.stop()
515 585
516 586 @property
517 587 def channels_running(self):
518 588 """Are all of the channels created and running?"""
519 589 return self.xreq_channel.is_alive() \
520 590 and self.sub_channel.is_alive() \
521 and self.rep_channel.is_alive()
591 and self.rep_channel.is_alive() \
592 and self.hb_channel.is_alive()
522 593
523 594 #--------------------------------------------------------------------------
524 595 # Kernel process management methods:
525 596 #--------------------------------------------------------------------------
526 597
527 598 def start_kernel(self, **kw):
528 599 """Starts a kernel process and configures the manager to use it.
529 600
530 601 If random ports (port=0) are being used, this method must be called
531 602 before the channels are created.
532 603
533 604 Parameters:
534 605 -----------
535 606 ipython : bool, optional (default True)
536 607 Whether to use an IPython kernel instead of a plain Python kernel.
537 608 """
538 xreq, sub, rep = self.xreq_address, self.sub_address, self.rep_address
539 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or rep[0] != LOCALHOST:
609 xreq, sub, rep, hb = self.xreq_address, self.sub_address, \
610 self.rep_address, self.hb_address
611 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or rep[0] != LOCALHOST or hb[0] != LOCALHOST:
540 612 raise RuntimeError("Can only launch a kernel on localhost."
541 613 "Make sure that the '*_address' attributes are "
542 614 "configured properly.")
543 615
544 616 self._launch_args = kw.copy()
545 617 if kw.pop('ipython', True):
546 618 from ipkernel import launch_kernel as launch
547 619 else:
548 620 from pykernel import launch_kernel as launch
549 self.kernel, xrep, pub, req = launch(xrep_port=xreq[1], pub_port=sub[1],
550 req_port=rep[1], **kw)
621 self.kernel, xrep, pub, req, hb = launch(
622 xrep_port=xreq[1], pub_port=sub[1], req_port=rep[1],
623 hb_port=hb[1], **kw)
551 624 self.xreq_address = (LOCALHOST, xrep)
552 625 self.sub_address = (LOCALHOST, pub)
553 626 self.rep_address = (LOCALHOST, req)
627 self.hb_address = (LOCALHOST, hb)
554 628
555 629 def restart_kernel(self):
556 630 """Restarts a kernel with the same arguments that were used to launch
557 631 it. If the old kernel was launched with random ports, the same ports
558 632 will be used for the new kernel.
559 633 """
560 634 if self._launch_args is None:
561 635 raise RuntimeError("Cannot restart the kernel. "
562 636 "No previous call to 'start_kernel'.")
563 637 else:
564 638 if self.has_kernel:
565 639 self.kill_kernel()
566 640 self.start_kernel(*self._launch_args)
567 641
568 642 @property
569 643 def has_kernel(self):
570 644 """Returns whether a kernel process has been specified for the kernel
571 645 manager.
572 646 """
573 647 return self.kernel is not None
574 648
575 649 def kill_kernel(self):
576 650 """ Kill the running kernel. """
577 651 if self.kernel is not None:
578 652 self.kernel.kill()
579 653 self.kernel = None
580 654 else:
581 655 raise RuntimeError("Cannot kill kernel. No kernel is running!")
582 656
583 657 def signal_kernel(self, signum):
584 658 """ Sends a signal to the kernel. """
585 659 if self.kernel is not None:
586 660 self.kernel.send_signal(signum)
587 661 else:
588 662 raise RuntimeError("Cannot signal kernel. No kernel is running!")
589 663
590 664 @property
591 665 def is_alive(self):
592 666 """Is the kernel process still running?"""
593 667 if self.kernel is not None:
594 668 if self.kernel.poll() is None:
595 669 return True
596 670 else:
597 671 return False
598 672 else:
599 673 # We didn't start the kernel with this KernelManager so we don't
600 674 # know if it is running. We should use a heartbeat for this case.
601 675 return True
602 676
603 677 #--------------------------------------------------------------------------
604 678 # Channels used for communication with the kernel:
605 679 #--------------------------------------------------------------------------
606 680
607 681 @property
608 682 def xreq_channel(self):
609 683 """Get the REQ socket channel object to make requests of the kernel."""
610 684 if self._xreq_channel is None:
611 685 self._xreq_channel = self.xreq_channel_class(self.context,
612 686 self.session,
613 687 self.xreq_address)
614 688 return self._xreq_channel
615 689
616 690 @property
617 691 def sub_channel(self):
618 692 """Get the SUB socket channel object."""
619 693 if self._sub_channel is None:
620 694 self._sub_channel = self.sub_channel_class(self.context,
621 695 self.session,
622 696 self.sub_address)
623 697 return self._sub_channel
624 698
625 699 @property
626 700 def rep_channel(self):
627 701 """Get the REP socket channel object to handle stdin (raw_input)."""
628 702 if self._rep_channel is None:
629 703 self._rep_channel = self.rep_channel_class(self.context,
630 704 self.session,
631 705 self.rep_address)
632 706 return self._rep_channel
707
708 @property
709 def hb_channel(self):
710 """Get the REP socket channel object to handle stdin (raw_input)."""
711 if self._hb_channel is None:
712 self._hb_channel = self.hb_channel_class(self.context,
713 self.session,
714 self.hb_address)
715 return self._hb_channel
General Comments 0
You need to be logged in to leave comments. Login now