##// END OF EJS Templates
Merge pull request #5759 from minrk/travis-3.4...
Thomas Kluyver -
r16553:51fb84bb merge
parent child Browse files
Show More
@@ -1,20 +1,26 b''
1 # http://travis-ci.org/#!/ipython/ipython
1 # http://travis-ci.org/#!/ipython/ipython
2 language: python
2 language: python
3 python:
3 python:
4 - 3.4
4 - 2.7
5 - 2.7
5 - 3.3
6 - 3.3
6 env:
7 env:
7 - GROUP=
8 - GROUP=js
8 - GROUP=js
9 - GROUP=
9 before_install:
10 before_install:
10 # workaround for https://github.com/travis-ci/travis-cookbooks/issues/155
11 # workaround for https://github.com/travis-ci/travis-cookbooks/issues/155
11 - sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm
12 - sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm
12 # Pierre Carrier's PPA for PhantomJS and CasperJS
13 # Pierre Carrier's PPA for PhantomJS and CasperJS
13 - time sudo add-apt-repository -y ppa:pcarrier/ppa
14 - time sudo add-apt-repository -y ppa:pcarrier/ppa
14 - time sudo apt-get update
15 - time sudo apt-get update
15 - time sudo apt-get install pandoc casperjs nodejs libzmq3-dev
16 - time sudo apt-get install pandoc casperjs nodejs libzmq3-dev
16 - time pip install -f https://nipy.bic.berkeley.edu/wheelhouse/travis jinja2 sphinx pygments tornado requests mock pyzmq
17 - time pip install -f https://nipy.bic.berkeley.edu/wheelhouse/travis jinja2 sphinx pygments tornado requests mock pyzmq
17 install:
18 install:
18 - time python setup.py install -q
19 - time python setup.py install -q
19 script:
20 script:
20 - cd /tmp && iptest $GROUP
21 - cd /tmp && iptest $GROUP
22
23 matrix:
24 exclude:
25 - python: 3.3
26 env: GROUP=js
@@ -1,195 +1,198 b''
1 """Tornado handlers for the notebook.
1 """Tornado handlers for the notebook.
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 import logging
19 import logging
20 from tornado import web
20 from tornado import web
21
21
22 from zmq.utils import jsonapi
22 from zmq.utils import jsonapi
23
23
24 from IPython.utils.jsonutil import date_default
24 from IPython.utils.jsonutil import date_default
25 from IPython.html.utils import url_path_join, url_escape
25 from IPython.html.utils import url_path_join, url_escape
26
26
27 from ...base.handlers import IPythonHandler, json_errors
27 from ...base.handlers import IPythonHandler, json_errors
28 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler
28 from ...base.zmqhandlers import AuthenticatedZMQStreamHandler
29
29
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31 # Kernel handlers
31 # Kernel handlers
32 #-----------------------------------------------------------------------------
32 #-----------------------------------------------------------------------------
33
33
34
34
35 class MainKernelHandler(IPythonHandler):
35 class MainKernelHandler(IPythonHandler):
36
36
37 @web.authenticated
37 @web.authenticated
38 @json_errors
38 @json_errors
39 def get(self):
39 def get(self):
40 km = self.kernel_manager
40 km = self.kernel_manager
41 self.finish(jsonapi.dumps(km.list_kernels()))
41 self.finish(jsonapi.dumps(km.list_kernels()))
42
42
43 @web.authenticated
43 @web.authenticated
44 @json_errors
44 @json_errors
45 def post(self):
45 def post(self):
46 km = self.kernel_manager
46 km = self.kernel_manager
47 kernel_id = km.start_kernel()
47 kernel_id = km.start_kernel()
48 model = km.kernel_model(kernel_id)
48 model = km.kernel_model(kernel_id)
49 location = url_path_join(self.base_url, 'api', 'kernels', kernel_id)
49 location = url_path_join(self.base_url, 'api', 'kernels', kernel_id)
50 self.set_header('Location', url_escape(location))
50 self.set_header('Location', url_escape(location))
51 self.set_status(201)
51 self.set_status(201)
52 self.finish(jsonapi.dumps(model))
52 self.finish(jsonapi.dumps(model))
53
53
54
54
55 class KernelHandler(IPythonHandler):
55 class KernelHandler(IPythonHandler):
56
56
57 SUPPORTED_METHODS = ('DELETE', 'GET')
57 SUPPORTED_METHODS = ('DELETE', 'GET')
58
58
59 @web.authenticated
59 @web.authenticated
60 @json_errors
60 @json_errors
61 def get(self, kernel_id):
61 def get(self, kernel_id):
62 km = self.kernel_manager
62 km = self.kernel_manager
63 km._check_kernel_id(kernel_id)
63 km._check_kernel_id(kernel_id)
64 model = km.kernel_model(kernel_id)
64 model = km.kernel_model(kernel_id)
65 self.finish(jsonapi.dumps(model))
65 self.finish(jsonapi.dumps(model))
66
66
67 @web.authenticated
67 @web.authenticated
68 @json_errors
68 @json_errors
69 def delete(self, kernel_id):
69 def delete(self, kernel_id):
70 km = self.kernel_manager
70 km = self.kernel_manager
71 km.shutdown_kernel(kernel_id)
71 km.shutdown_kernel(kernel_id)
72 self.set_status(204)
72 self.set_status(204)
73 self.finish()
73 self.finish()
74
74
75
75
76 class KernelActionHandler(IPythonHandler):
76 class KernelActionHandler(IPythonHandler):
77
77
78 @web.authenticated
78 @web.authenticated
79 @json_errors
79 @json_errors
80 def post(self, kernel_id, action):
80 def post(self, kernel_id, action):
81 km = self.kernel_manager
81 km = self.kernel_manager
82 if action == 'interrupt':
82 if action == 'interrupt':
83 km.interrupt_kernel(kernel_id)
83 km.interrupt_kernel(kernel_id)
84 self.set_status(204)
84 self.set_status(204)
85 if action == 'restart':
85 if action == 'restart':
86 km.restart_kernel(kernel_id)
86 km.restart_kernel(kernel_id)
87 model = km.kernel_model(kernel_id)
87 model = km.kernel_model(kernel_id)
88 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_url, kernel_id))
88 self.set_header('Location', '{0}api/kernels/{1}'.format(self.base_url, kernel_id))
89 self.write(jsonapi.dumps(model))
89 self.write(jsonapi.dumps(model))
90 self.finish()
90 self.finish()
91
91
92
92
93 class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
93 class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
94
94
95 def create_stream(self):
95 def create_stream(self):
96 km = self.kernel_manager
96 km = self.kernel_manager
97 meth = getattr(km, 'connect_%s' % self.channel)
97 meth = getattr(km, 'connect_%s' % self.channel)
98 self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
98 self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
99
99
100 def initialize(self, *args, **kwargs):
100 def initialize(self, *args, **kwargs):
101 self.zmq_stream = None
101 self.zmq_stream = None
102
102
103 def on_first_message(self, msg):
103 def on_first_message(self, msg):
104 try:
104 try:
105 super(ZMQChannelHandler, self).on_first_message(msg)
105 super(ZMQChannelHandler, self).on_first_message(msg)
106 except web.HTTPError:
106 except web.HTTPError:
107 self.close()
107 self.close()
108 return
108 return
109 try:
109 try:
110 self.create_stream()
110 self.create_stream()
111 except web.HTTPError:
111 except web.HTTPError:
112 # WebSockets don't response to traditional error codes so we
112 # WebSockets don't response to traditional error codes so we
113 # close the connection.
113 # close the connection.
114 if not self.stream.closed():
114 if not self.stream.closed():
115 self.stream.close()
115 self.stream.close()
116 self.close()
116 self.close()
117 else:
117 else:
118 self.zmq_stream.on_recv(self._on_zmq_reply)
118 self.zmq_stream.on_recv(self._on_zmq_reply)
119
119
120 def on_message(self, msg):
120 def on_message(self, msg):
121 msg = jsonapi.loads(msg)
121 msg = jsonapi.loads(msg)
122 self.session.send(self.zmq_stream, msg)
122 self.session.send(self.zmq_stream, msg)
123
123
124 def on_close(self):
124 def on_close(self):
125 # This method can be called twice, once by self.kernel_died and once
125 # This method can be called twice, once by self.kernel_died and once
126 # from the WebSocket close event. If the WebSocket connection is
126 # from the WebSocket close event. If the WebSocket connection is
127 # closed before the ZMQ streams are setup, they could be None.
127 # closed before the ZMQ streams are setup, they could be None.
128 if self.zmq_stream is not None and not self.zmq_stream.closed():
128 if self.zmq_stream is not None and not self.zmq_stream.closed():
129 self.zmq_stream.on_recv(None)
129 self.zmq_stream.on_recv(None)
130 # close the socket directly, don't wait for the stream
131 socket = self.zmq_stream.socket
130 self.zmq_stream.close()
132 self.zmq_stream.close()
133 socket.close()
131
134
132
135
133 class IOPubHandler(ZMQChannelHandler):
136 class IOPubHandler(ZMQChannelHandler):
134 channel = 'iopub'
137 channel = 'iopub'
135
138
136 def create_stream(self):
139 def create_stream(self):
137 super(IOPubHandler, self).create_stream()
140 super(IOPubHandler, self).create_stream()
138 km = self.kernel_manager
141 km = self.kernel_manager
139 km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
142 km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
140 km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
143 km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
141
144
142 def on_close(self):
145 def on_close(self):
143 km = self.kernel_manager
146 km = self.kernel_manager
144 if self.kernel_id in km:
147 if self.kernel_id in km:
145 km.remove_restart_callback(
148 km.remove_restart_callback(
146 self.kernel_id, self.on_kernel_restarted,
149 self.kernel_id, self.on_kernel_restarted,
147 )
150 )
148 km.remove_restart_callback(
151 km.remove_restart_callback(
149 self.kernel_id, self.on_restart_failed, 'dead',
152 self.kernel_id, self.on_restart_failed, 'dead',
150 )
153 )
151 super(IOPubHandler, self).on_close()
154 super(IOPubHandler, self).on_close()
152
155
153 def _send_status_message(self, status):
156 def _send_status_message(self, status):
154 msg = self.session.msg("status",
157 msg = self.session.msg("status",
155 {'execution_state': status}
158 {'execution_state': status}
156 )
159 )
157 self.write_message(jsonapi.dumps(msg, default=date_default))
160 self.write_message(jsonapi.dumps(msg, default=date_default))
158
161
159 def on_kernel_restarted(self):
162 def on_kernel_restarted(self):
160 logging.warn("kernel %s restarted", self.kernel_id)
163 logging.warn("kernel %s restarted", self.kernel_id)
161 self._send_status_message('restarting')
164 self._send_status_message('restarting')
162
165
163 def on_restart_failed(self):
166 def on_restart_failed(self):
164 logging.error("kernel %s restarted failed!", self.kernel_id)
167 logging.error("kernel %s restarted failed!", self.kernel_id)
165 self._send_status_message('dead')
168 self._send_status_message('dead')
166
169
167 def on_message(self, msg):
170 def on_message(self, msg):
168 """IOPub messages make no sense"""
171 """IOPub messages make no sense"""
169 pass
172 pass
170
173
171
174
172 class ShellHandler(ZMQChannelHandler):
175 class ShellHandler(ZMQChannelHandler):
173 channel = 'shell'
176 channel = 'shell'
174
177
175
178
176 class StdinHandler(ZMQChannelHandler):
179 class StdinHandler(ZMQChannelHandler):
177 channel = 'stdin'
180 channel = 'stdin'
178
181
179
182
180 #-----------------------------------------------------------------------------
183 #-----------------------------------------------------------------------------
181 # URL to handler mappings
184 # URL to handler mappings
182 #-----------------------------------------------------------------------------
185 #-----------------------------------------------------------------------------
183
186
184
187
185 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
188 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
186 _kernel_action_regex = r"(?P<action>restart|interrupt)"
189 _kernel_action_regex = r"(?P<action>restart|interrupt)"
187
190
188 default_handlers = [
191 default_handlers = [
189 (r"/api/kernels", MainKernelHandler),
192 (r"/api/kernels", MainKernelHandler),
190 (r"/api/kernels/%s" % _kernel_id_regex, KernelHandler),
193 (r"/api/kernels/%s" % _kernel_id_regex, KernelHandler),
191 (r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
194 (r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
192 (r"/api/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
195 (r"/api/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
193 (r"/api/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
196 (r"/api/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
194 (r"/api/kernels/%s/stdin" % _kernel_id_regex, StdinHandler)
197 (r"/api/kernels/%s/stdin" % _kernel_id_regex, StdinHandler)
195 ]
198 ]
@@ -1,634 +1,637 b''
1 """Base classes to manage a Client's interaction with a running kernel
1 """Base classes to manage a Client's interaction with a running kernel
2 """
2 """
3
3
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (C) 2013 The IPython Development Team
5 # Copyright (C) 2013 The IPython Development Team
6 #
6 #
7 # Distributed under the terms of the BSD License. The full license is in
7 # Distributed under the terms of the BSD License. The full license is in
8 # the file COPYING, distributed as part of this software.
8 # the file COPYING, distributed as part of this software.
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Imports
12 # Imports
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 from __future__ import absolute_import
15 from __future__ import absolute_import
16
16
17 # Standard library imports
17 # Standard library imports
18 import atexit
18 import atexit
19 import errno
19 import errno
20 from threading import Thread
20 from threading import Thread
21 import time
21 import time
22
22
23 import zmq
23 import zmq
24 # import ZMQError in top-level namespace, to avoid ugly attribute-error messages
24 # import ZMQError in top-level namespace, to avoid ugly attribute-error messages
25 # during garbage collection of threads at exit:
25 # during garbage collection of threads at exit:
26 from zmq import ZMQError
26 from zmq import ZMQError
27 from zmq.eventloop import ioloop, zmqstream
27 from zmq.eventloop import ioloop, zmqstream
28
28
29 # Local imports
29 # Local imports
30 from .channelsabc import (
30 from .channelsabc import (
31 ShellChannelABC, IOPubChannelABC,
31 ShellChannelABC, IOPubChannelABC,
32 HBChannelABC, StdInChannelABC,
32 HBChannelABC, StdInChannelABC,
33 )
33 )
34 from IPython.utils.py3compat import string_types, iteritems
34 from IPython.utils.py3compat import string_types, iteritems
35
35
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37 # Constants and exceptions
37 # Constants and exceptions
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39
39
40 class InvalidPortNumber(Exception):
40 class InvalidPortNumber(Exception):
41 pass
41 pass
42
42
43 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
44 # Utility functions
44 # Utility functions
45 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
46
46
47 # some utilities to validate message structure, these might get moved elsewhere
47 # some utilities to validate message structure, these might get moved elsewhere
48 # if they prove to have more generic utility
48 # if they prove to have more generic utility
49
49
50 def validate_string_list(lst):
50 def validate_string_list(lst):
51 """Validate that the input is a list of strings.
51 """Validate that the input is a list of strings.
52
52
53 Raises ValueError if not."""
53 Raises ValueError if not."""
54 if not isinstance(lst, list):
54 if not isinstance(lst, list):
55 raise ValueError('input %r must be a list' % lst)
55 raise ValueError('input %r must be a list' % lst)
56 for x in lst:
56 for x in lst:
57 if not isinstance(x, string_types):
57 if not isinstance(x, string_types):
58 raise ValueError('element %r in list must be a string' % x)
58 raise ValueError('element %r in list must be a string' % x)
59
59
60
60
61 def validate_string_dict(dct):
61 def validate_string_dict(dct):
62 """Validate that the input is a dict with string keys and values.
62 """Validate that the input is a dict with string keys and values.
63
63
64 Raises ValueError if not."""
64 Raises ValueError if not."""
65 for k,v in iteritems(dct):
65 for k,v in iteritems(dct):
66 if not isinstance(k, string_types):
66 if not isinstance(k, string_types):
67 raise ValueError('key %r in dict must be a string' % k)
67 raise ValueError('key %r in dict must be a string' % k)
68 if not isinstance(v, string_types):
68 if not isinstance(v, string_types):
69 raise ValueError('value %r in dict must be a string' % v)
69 raise ValueError('value %r in dict must be a string' % v)
70
70
71
71
72 #-----------------------------------------------------------------------------
72 #-----------------------------------------------------------------------------
73 # ZMQ Socket Channel classes
73 # ZMQ Socket Channel classes
74 #-----------------------------------------------------------------------------
74 #-----------------------------------------------------------------------------
75
75
76 class ZMQSocketChannel(Thread):
76 class ZMQSocketChannel(Thread):
77 """The base class for the channels that use ZMQ sockets."""
77 """The base class for the channels that use ZMQ sockets."""
78 context = None
78 context = None
79 session = None
79 session = None
80 socket = None
80 socket = None
81 ioloop = None
81 ioloop = None
82 stream = None
82 stream = None
83 _address = None
83 _address = None
84 _exiting = False
84 _exiting = False
85 proxy_methods = []
85 proxy_methods = []
86
86
87 def __init__(self, context, session, address):
87 def __init__(self, context, session, address):
88 """Create a channel.
88 """Create a channel.
89
89
90 Parameters
90 Parameters
91 ----------
91 ----------
92 context : :class:`zmq.Context`
92 context : :class:`zmq.Context`
93 The ZMQ context to use.
93 The ZMQ context to use.
94 session : :class:`session.Session`
94 session : :class:`session.Session`
95 The session to use.
95 The session to use.
96 address : zmq url
96 address : zmq url
97 Standard (ip, port) tuple that the kernel is listening on.
97 Standard (ip, port) tuple that the kernel is listening on.
98 """
98 """
99 super(ZMQSocketChannel, self).__init__()
99 super(ZMQSocketChannel, self).__init__()
100 self.daemon = True
100 self.daemon = True
101
101
102 self.context = context
102 self.context = context
103 self.session = session
103 self.session = session
104 if isinstance(address, tuple):
104 if isinstance(address, tuple):
105 if address[1] == 0:
105 if address[1] == 0:
106 message = 'The port number for a channel cannot be 0.'
106 message = 'The port number for a channel cannot be 0.'
107 raise InvalidPortNumber(message)
107 raise InvalidPortNumber(message)
108 address = "tcp://%s:%i" % address
108 address = "tcp://%s:%i" % address
109 self._address = address
109 self._address = address
110 atexit.register(self._notice_exit)
110 atexit.register(self._notice_exit)
111
111
112 def _notice_exit(self):
112 def _notice_exit(self):
113 self._exiting = True
113 self._exiting = True
114
114
115 def _run_loop(self):
115 def _run_loop(self):
116 """Run my loop, ignoring EINTR events in the poller"""
116 """Run my loop, ignoring EINTR events in the poller"""
117 while True:
117 while True:
118 try:
118 try:
119 self.ioloop.start()
119 self.ioloop.start()
120 except ZMQError as e:
120 except ZMQError as e:
121 if e.errno == errno.EINTR:
121 if e.errno == errno.EINTR:
122 continue
122 continue
123 else:
123 else:
124 raise
124 raise
125 except Exception:
125 except Exception:
126 if self._exiting:
126 if self._exiting:
127 break
127 break
128 else:
128 else:
129 raise
129 raise
130 else:
130 else:
131 break
131 break
132
132
133 def stop(self):
133 def stop(self):
134 """Stop the channel's event loop and join its thread.
134 """Stop the channel's event loop and join its thread.
135
135
136 This calls :meth:`~threading.Thread.join` and returns when the thread
136 This calls :meth:`~threading.Thread.join` and returns when the thread
137 terminates. :class:`RuntimeError` will be raised if
137 terminates. :class:`RuntimeError` will be raised if
138 :meth:`~threading.Thread.start` is called again.
138 :meth:`~threading.Thread.start` is called again.
139 """
139 """
140 if self.ioloop is not None:
140 if self.ioloop is not None:
141 self.ioloop.stop()
141 self.ioloop.stop()
142 self.join()
142 self.join()
143 self.close()
143 self.close()
144
144
145 def close(self):
145 def close(self):
146 if self.ioloop is not None:
146 if self.ioloop is not None:
147 try:
147 try:
148 self.ioloop.close(all_fds=True)
148 self.ioloop.close(all_fds=True)
149 except Exception:
149 except Exception:
150 pass
150 pass
151 if self.socket is not None:
151 if self.socket is not None:
152 try:
152 try:
153 self.socket.close(linger=0)
153 self.socket.close(linger=0)
154 except Exception:
154 except Exception:
155 pass
155 pass
156 self.socket = None
156 self.socket = None
157
157
158 @property
158 @property
159 def address(self):
159 def address(self):
160 """Get the channel's address as a zmq url string.
160 """Get the channel's address as a zmq url string.
161
161
162 These URLS have the form: 'tcp://127.0.0.1:5555'.
162 These URLS have the form: 'tcp://127.0.0.1:5555'.
163 """
163 """
164 return self._address
164 return self._address
165
165
166 def _queue_send(self, msg):
166 def _queue_send(self, msg):
167 """Queue a message to be sent from the IOLoop's thread.
167 """Queue a message to be sent from the IOLoop's thread.
168
168
169 Parameters
169 Parameters
170 ----------
170 ----------
171 msg : message to send
171 msg : message to send
172
172
173 This is threadsafe, as it uses IOLoop.add_callback to give the loop's
173 This is threadsafe, as it uses IOLoop.add_callback to give the loop's
174 thread control of the action.
174 thread control of the action.
175 """
175 """
176 def thread_send():
176 def thread_send():
177 self.session.send(self.stream, msg)
177 self.session.send(self.stream, msg)
178 self.ioloop.add_callback(thread_send)
178 self.ioloop.add_callback(thread_send)
179
179
180 def _handle_recv(self, msg):
180 def _handle_recv(self, msg):
181 """Callback for stream.on_recv.
181 """Callback for stream.on_recv.
182
182
183 Unpacks message, and calls handlers with it.
183 Unpacks message, and calls handlers with it.
184 """
184 """
185 ident,smsg = self.session.feed_identities(msg)
185 ident,smsg = self.session.feed_identities(msg)
186 self.call_handlers(self.session.unserialize(smsg))
186 self.call_handlers(self.session.unserialize(smsg))
187
187
188
188
189
189
190 class ShellChannel(ZMQSocketChannel):
190 class ShellChannel(ZMQSocketChannel):
191 """The shell channel for issuing request/replies to the kernel."""
191 """The shell channel for issuing request/replies to the kernel."""
192
192
193 command_queue = None
193 command_queue = None
194 # flag for whether execute requests should be allowed to call raw_input:
194 # flag for whether execute requests should be allowed to call raw_input:
195 allow_stdin = True
195 allow_stdin = True
196 proxy_methods = [
196 proxy_methods = [
197 'execute',
197 'execute',
198 'complete',
198 'complete',
199 'object_info',
199 'object_info',
200 'history',
200 'history',
201 'kernel_info',
201 'kernel_info',
202 'shutdown',
202 'shutdown',
203 ]
203 ]
204
204
205 def __init__(self, context, session, address):
205 def __init__(self, context, session, address):
206 super(ShellChannel, self).__init__(context, session, address)
206 super(ShellChannel, self).__init__(context, session, address)
207 self.ioloop = ioloop.IOLoop()
207 self.ioloop = ioloop.IOLoop()
208
208
209 def run(self):
209 def run(self):
210 """The thread's main activity. Call start() instead."""
210 """The thread's main activity. Call start() instead."""
211 self.socket = self.context.socket(zmq.DEALER)
211 self.socket = self.context.socket(zmq.DEALER)
212 self.socket.linger = 1000
212 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
213 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
213 self.socket.connect(self.address)
214 self.socket.connect(self.address)
214 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
215 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
215 self.stream.on_recv(self._handle_recv)
216 self.stream.on_recv(self._handle_recv)
216 self._run_loop()
217 self._run_loop()
217
218
218 def call_handlers(self, msg):
219 def call_handlers(self, msg):
219 """This method is called in the ioloop thread when a message arrives.
220 """This method is called in the ioloop thread when a message arrives.
220
221
221 Subclasses should override this method to handle incoming messages.
222 Subclasses should override this method to handle incoming messages.
222 It is important to remember that this method is called in the thread
223 It is important to remember that this method is called in the thread
223 so that some logic must be done to ensure that the application level
224 so that some logic must be done to ensure that the application level
224 handlers are called in the application thread.
225 handlers are called in the application thread.
225 """
226 """
226 raise NotImplementedError('call_handlers must be defined in a subclass.')
227 raise NotImplementedError('call_handlers must be defined in a subclass.')
227
228
228 def execute(self, code, silent=False, store_history=True,
229 def execute(self, code, silent=False, store_history=True,
229 user_variables=None, user_expressions=None, allow_stdin=None):
230 user_variables=None, user_expressions=None, allow_stdin=None):
230 """Execute code in the kernel.
231 """Execute code in the kernel.
231
232
232 Parameters
233 Parameters
233 ----------
234 ----------
234 code : str
235 code : str
235 A string of Python code.
236 A string of Python code.
236
237
237 silent : bool, optional (default False)
238 silent : bool, optional (default False)
238 If set, the kernel will execute the code as quietly possible, and
239 If set, the kernel will execute the code as quietly possible, and
239 will force store_history to be False.
240 will force store_history to be False.
240
241
241 store_history : bool, optional (default True)
242 store_history : bool, optional (default True)
242 If set, the kernel will store command history. This is forced
243 If set, the kernel will store command history. This is forced
243 to be False if silent is True.
244 to be False if silent is True.
244
245
245 user_variables : list, optional
246 user_variables : list, optional
246 A list of variable names to pull from the user's namespace. They
247 A list of variable names to pull from the user's namespace. They
247 will come back as a dict with these names as keys and their
248 will come back as a dict with these names as keys and their
248 :func:`repr` as values.
249 :func:`repr` as values.
249
250
250 user_expressions : dict, optional
251 user_expressions : dict, optional
251 A dict mapping names to expressions to be evaluated in the user's
252 A dict mapping names to expressions to be evaluated in the user's
252 dict. The expression values are returned as strings formatted using
253 dict. The expression values are returned as strings formatted using
253 :func:`repr`.
254 :func:`repr`.
254
255
255 allow_stdin : bool, optional (default self.allow_stdin)
256 allow_stdin : bool, optional (default self.allow_stdin)
256 Flag for whether the kernel can send stdin requests to frontends.
257 Flag for whether the kernel can send stdin requests to frontends.
257
258
258 Some frontends (e.g. the Notebook) do not support stdin requests.
259 Some frontends (e.g. the Notebook) do not support stdin requests.
259 If raw_input is called from code executed from such a frontend, a
260 If raw_input is called from code executed from such a frontend, a
260 StdinNotImplementedError will be raised.
261 StdinNotImplementedError will be raised.
261
262
262 Returns
263 Returns
263 -------
264 -------
264 The msg_id of the message sent.
265 The msg_id of the message sent.
265 """
266 """
266 if user_variables is None:
267 if user_variables is None:
267 user_variables = []
268 user_variables = []
268 if user_expressions is None:
269 if user_expressions is None:
269 user_expressions = {}
270 user_expressions = {}
270 if allow_stdin is None:
271 if allow_stdin is None:
271 allow_stdin = self.allow_stdin
272 allow_stdin = self.allow_stdin
272
273
273
274
274 # Don't waste network traffic if inputs are invalid
275 # Don't waste network traffic if inputs are invalid
275 if not isinstance(code, string_types):
276 if not isinstance(code, string_types):
276 raise ValueError('code %r must be a string' % code)
277 raise ValueError('code %r must be a string' % code)
277 validate_string_list(user_variables)
278 validate_string_list(user_variables)
278 validate_string_dict(user_expressions)
279 validate_string_dict(user_expressions)
279
280
280 # Create class for content/msg creation. Related to, but possibly
281 # Create class for content/msg creation. Related to, but possibly
281 # not in Session.
282 # not in Session.
282 content = dict(code=code, silent=silent, store_history=store_history,
283 content = dict(code=code, silent=silent, store_history=store_history,
283 user_variables=user_variables,
284 user_variables=user_variables,
284 user_expressions=user_expressions,
285 user_expressions=user_expressions,
285 allow_stdin=allow_stdin,
286 allow_stdin=allow_stdin,
286 )
287 )
287 msg = self.session.msg('execute_request', content)
288 msg = self.session.msg('execute_request', content)
288 self._queue_send(msg)
289 self._queue_send(msg)
289 return msg['header']['msg_id']
290 return msg['header']['msg_id']
290
291
291 def complete(self, text, line, cursor_pos, block=None):
292 def complete(self, text, line, cursor_pos, block=None):
292 """Tab complete text in the kernel's namespace.
293 """Tab complete text in the kernel's namespace.
293
294
294 Parameters
295 Parameters
295 ----------
296 ----------
296 text : str
297 text : str
297 The text to complete.
298 The text to complete.
298 line : str
299 line : str
299 The full line of text that is the surrounding context for the
300 The full line of text that is the surrounding context for the
300 text to complete.
301 text to complete.
301 cursor_pos : int
302 cursor_pos : int
302 The position of the cursor in the line where the completion was
303 The position of the cursor in the line where the completion was
303 requested.
304 requested.
304 block : str, optional
305 block : str, optional
305 The full block of code in which the completion is being requested.
306 The full block of code in which the completion is being requested.
306
307
307 Returns
308 Returns
308 -------
309 -------
309 The msg_id of the message sent.
310 The msg_id of the message sent.
310 """
311 """
311 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
312 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
312 msg = self.session.msg('complete_request', content)
313 msg = self.session.msg('complete_request', content)
313 self._queue_send(msg)
314 self._queue_send(msg)
314 return msg['header']['msg_id']
315 return msg['header']['msg_id']
315
316
316 def object_info(self, oname, detail_level=0):
317 def object_info(self, oname, detail_level=0):
317 """Get metadata information about an object in the kernel's namespace.
318 """Get metadata information about an object in the kernel's namespace.
318
319
319 Parameters
320 Parameters
320 ----------
321 ----------
321 oname : str
322 oname : str
322 A string specifying the object name.
323 A string specifying the object name.
323 detail_level : int, optional
324 detail_level : int, optional
324 The level of detail for the introspection (0-2)
325 The level of detail for the introspection (0-2)
325
326
326 Returns
327 Returns
327 -------
328 -------
328 The msg_id of the message sent.
329 The msg_id of the message sent.
329 """
330 """
330 content = dict(oname=oname, detail_level=detail_level)
331 content = dict(oname=oname, detail_level=detail_level)
331 msg = self.session.msg('object_info_request', content)
332 msg = self.session.msg('object_info_request', content)
332 self._queue_send(msg)
333 self._queue_send(msg)
333 return msg['header']['msg_id']
334 return msg['header']['msg_id']
334
335
335 def history(self, raw=True, output=False, hist_access_type='range', **kwargs):
336 def history(self, raw=True, output=False, hist_access_type='range', **kwargs):
336 """Get entries from the kernel's history list.
337 """Get entries from the kernel's history list.
337
338
338 Parameters
339 Parameters
339 ----------
340 ----------
340 raw : bool
341 raw : bool
341 If True, return the raw input.
342 If True, return the raw input.
342 output : bool
343 output : bool
343 If True, then return the output as well.
344 If True, then return the output as well.
344 hist_access_type : str
345 hist_access_type : str
345 'range' (fill in session, start and stop params), 'tail' (fill in n)
346 'range' (fill in session, start and stop params), 'tail' (fill in n)
346 or 'search' (fill in pattern param).
347 or 'search' (fill in pattern param).
347
348
348 session : int
349 session : int
349 For a range request, the session from which to get lines. Session
350 For a range request, the session from which to get lines. Session
350 numbers are positive integers; negative ones count back from the
351 numbers are positive integers; negative ones count back from the
351 current session.
352 current session.
352 start : int
353 start : int
353 The first line number of a history range.
354 The first line number of a history range.
354 stop : int
355 stop : int
355 The final (excluded) line number of a history range.
356 The final (excluded) line number of a history range.
356
357
357 n : int
358 n : int
358 The number of lines of history to get for a tail request.
359 The number of lines of history to get for a tail request.
359
360
360 pattern : str
361 pattern : str
361 The glob-syntax pattern for a search request.
362 The glob-syntax pattern for a search request.
362
363
363 Returns
364 Returns
364 -------
365 -------
365 The msg_id of the message sent.
366 The msg_id of the message sent.
366 """
367 """
367 content = dict(raw=raw, output=output, hist_access_type=hist_access_type,
368 content = dict(raw=raw, output=output, hist_access_type=hist_access_type,
368 **kwargs)
369 **kwargs)
369 msg = self.session.msg('history_request', content)
370 msg = self.session.msg('history_request', content)
370 self._queue_send(msg)
371 self._queue_send(msg)
371 return msg['header']['msg_id']
372 return msg['header']['msg_id']
372
373
373 def kernel_info(self):
374 def kernel_info(self):
374 """Request kernel info."""
375 """Request kernel info."""
375 msg = self.session.msg('kernel_info_request')
376 msg = self.session.msg('kernel_info_request')
376 self._queue_send(msg)
377 self._queue_send(msg)
377 return msg['header']['msg_id']
378 return msg['header']['msg_id']
378
379
379 def shutdown(self, restart=False):
380 def shutdown(self, restart=False):
380 """Request an immediate kernel shutdown.
381 """Request an immediate kernel shutdown.
381
382
382 Upon receipt of the (empty) reply, client code can safely assume that
383 Upon receipt of the (empty) reply, client code can safely assume that
383 the kernel has shut down and it's safe to forcefully terminate it if
384 the kernel has shut down and it's safe to forcefully terminate it if
384 it's still alive.
385 it's still alive.
385
386
386 The kernel will send the reply via a function registered with Python's
387 The kernel will send the reply via a function registered with Python's
387 atexit module, ensuring it's truly done as the kernel is done with all
388 atexit module, ensuring it's truly done as the kernel is done with all
388 normal operation.
389 normal operation.
389 """
390 """
390 # Send quit message to kernel. Once we implement kernel-side setattr,
391 # Send quit message to kernel. Once we implement kernel-side setattr,
391 # this should probably be done that way, but for now this will do.
392 # this should probably be done that way, but for now this will do.
392 msg = self.session.msg('shutdown_request', {'restart':restart})
393 msg = self.session.msg('shutdown_request', {'restart':restart})
393 self._queue_send(msg)
394 self._queue_send(msg)
394 return msg['header']['msg_id']
395 return msg['header']['msg_id']
395
396
396
397
397
398
398 class IOPubChannel(ZMQSocketChannel):
399 class IOPubChannel(ZMQSocketChannel):
399 """The iopub channel which listens for messages that the kernel publishes.
400 """The iopub channel which listens for messages that the kernel publishes.
400
401
401 This channel is where all output is published to frontends.
402 This channel is where all output is published to frontends.
402 """
403 """
403
404
404 def __init__(self, context, session, address):
405 def __init__(self, context, session, address):
405 super(IOPubChannel, self).__init__(context, session, address)
406 super(IOPubChannel, self).__init__(context, session, address)
406 self.ioloop = ioloop.IOLoop()
407 self.ioloop = ioloop.IOLoop()
407
408
408 def run(self):
409 def run(self):
409 """The thread's main activity. Call start() instead."""
410 """The thread's main activity. Call start() instead."""
410 self.socket = self.context.socket(zmq.SUB)
411 self.socket = self.context.socket(zmq.SUB)
412 self.socket.linger = 1000
411 self.socket.setsockopt(zmq.SUBSCRIBE,b'')
413 self.socket.setsockopt(zmq.SUBSCRIBE,b'')
412 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
414 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
413 self.socket.connect(self.address)
415 self.socket.connect(self.address)
414 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
416 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
415 self.stream.on_recv(self._handle_recv)
417 self.stream.on_recv(self._handle_recv)
416 self._run_loop()
418 self._run_loop()
417
419
418 def call_handlers(self, msg):
420 def call_handlers(self, msg):
419 """This method is called in the ioloop thread when a message arrives.
421 """This method is called in the ioloop thread when a message arrives.
420
422
421 Subclasses should override this method to handle incoming messages.
423 Subclasses should override this method to handle incoming messages.
422 It is important to remember that this method is called in the thread
424 It is important to remember that this method is called in the thread
423 so that some logic must be done to ensure that the application leve
425 so that some logic must be done to ensure that the application leve
424 handlers are called in the application thread.
426 handlers are called in the application thread.
425 """
427 """
426 raise NotImplementedError('call_handlers must be defined in a subclass.')
428 raise NotImplementedError('call_handlers must be defined in a subclass.')
427
429
428 def flush(self, timeout=1.0):
430 def flush(self, timeout=1.0):
429 """Immediately processes all pending messages on the iopub channel.
431 """Immediately processes all pending messages on the iopub channel.
430
432
431 Callers should use this method to ensure that :meth:`call_handlers`
433 Callers should use this method to ensure that :meth:`call_handlers`
432 has been called for all messages that have been received on the
434 has been called for all messages that have been received on the
433 0MQ SUB socket of this channel.
435 0MQ SUB socket of this channel.
434
436
435 This method is thread safe.
437 This method is thread safe.
436
438
437 Parameters
439 Parameters
438 ----------
440 ----------
439 timeout : float, optional
441 timeout : float, optional
440 The maximum amount of time to spend flushing, in seconds. The
442 The maximum amount of time to spend flushing, in seconds. The
441 default is one second.
443 default is one second.
442 """
444 """
443 # We do the IOLoop callback process twice to ensure that the IOLoop
445 # We do the IOLoop callback process twice to ensure that the IOLoop
444 # gets to perform at least one full poll.
446 # gets to perform at least one full poll.
445 stop_time = time.time() + timeout
447 stop_time = time.time() + timeout
446 for i in range(2):
448 for i in range(2):
447 self._flushed = False
449 self._flushed = False
448 self.ioloop.add_callback(self._flush)
450 self.ioloop.add_callback(self._flush)
449 while not self._flushed and time.time() < stop_time:
451 while not self._flushed and time.time() < stop_time:
450 time.sleep(0.01)
452 time.sleep(0.01)
451
453
452 def _flush(self):
454 def _flush(self):
453 """Callback for :method:`self.flush`."""
455 """Callback for :method:`self.flush`."""
454 self.stream.flush()
456 self.stream.flush()
455 self._flushed = True
457 self._flushed = True
456
458
457
459
458 class StdInChannel(ZMQSocketChannel):
460 class StdInChannel(ZMQSocketChannel):
459 """The stdin channel to handle raw_input requests that the kernel makes."""
461 """The stdin channel to handle raw_input requests that the kernel makes."""
460
462
461 msg_queue = None
463 msg_queue = None
462 proxy_methods = ['input']
464 proxy_methods = ['input']
463
465
464 def __init__(self, context, session, address):
466 def __init__(self, context, session, address):
465 super(StdInChannel, self).__init__(context, session, address)
467 super(StdInChannel, self).__init__(context, session, address)
466 self.ioloop = ioloop.IOLoop()
468 self.ioloop = ioloop.IOLoop()
467
469
468 def run(self):
470 def run(self):
469 """The thread's main activity. Call start() instead."""
471 """The thread's main activity. Call start() instead."""
470 self.socket = self.context.socket(zmq.DEALER)
472 self.socket = self.context.socket(zmq.DEALER)
473 self.socket.linger = 1000
471 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
474 self.socket.setsockopt(zmq.IDENTITY, self.session.bsession)
472 self.socket.connect(self.address)
475 self.socket.connect(self.address)
473 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
476 self.stream = zmqstream.ZMQStream(self.socket, self.ioloop)
474 self.stream.on_recv(self._handle_recv)
477 self.stream.on_recv(self._handle_recv)
475 self._run_loop()
478 self._run_loop()
476
479
477 def call_handlers(self, msg):
480 def call_handlers(self, msg):
478 """This method is called in the ioloop thread when a message arrives.
481 """This method is called in the ioloop thread when a message arrives.
479
482
480 Subclasses should override this method to handle incoming messages.
483 Subclasses should override this method to handle incoming messages.
481 It is important to remember that this method is called in the thread
484 It is important to remember that this method is called in the thread
482 so that some logic must be done to ensure that the application leve
485 so that some logic must be done to ensure that the application leve
483 handlers are called in the application thread.
486 handlers are called in the application thread.
484 """
487 """
485 raise NotImplementedError('call_handlers must be defined in a subclass.')
488 raise NotImplementedError('call_handlers must be defined in a subclass.')
486
489
487 def input(self, string):
490 def input(self, string):
488 """Send a string of raw input to the kernel."""
491 """Send a string of raw input to the kernel."""
489 content = dict(value=string)
492 content = dict(value=string)
490 msg = self.session.msg('input_reply', content)
493 msg = self.session.msg('input_reply', content)
491 self._queue_send(msg)
494 self._queue_send(msg)
492
495
493
496
494 class HBChannel(ZMQSocketChannel):
497 class HBChannel(ZMQSocketChannel):
495 """The heartbeat channel which monitors the kernel heartbeat.
498 """The heartbeat channel which monitors the kernel heartbeat.
496
499
497 Note that the heartbeat channel is paused by default. As long as you start
500 Note that the heartbeat channel is paused by default. As long as you start
498 this channel, the kernel manager will ensure that it is paused and un-paused
501 this channel, the kernel manager will ensure that it is paused and un-paused
499 as appropriate.
502 as appropriate.
500 """
503 """
501
504
502 time_to_dead = 3.0
505 time_to_dead = 3.0
503 socket = None
506 socket = None
504 poller = None
507 poller = None
505 _running = None
508 _running = None
506 _pause = None
509 _pause = None
507 _beating = None
510 _beating = None
508
511
509 def __init__(self, context, session, address):
512 def __init__(self, context, session, address):
510 super(HBChannel, self).__init__(context, session, address)
513 super(HBChannel, self).__init__(context, session, address)
511 self._running = False
514 self._running = False
512 self._pause =True
515 self._pause =True
513 self.poller = zmq.Poller()
516 self.poller = zmq.Poller()
514
517
515 def _create_socket(self):
518 def _create_socket(self):
516 if self.socket is not None:
519 if self.socket is not None:
517 # close previous socket, before opening a new one
520 # close previous socket, before opening a new one
518 self.poller.unregister(self.socket)
521 self.poller.unregister(self.socket)
519 self.socket.close()
522 self.socket.close()
520 self.socket = self.context.socket(zmq.REQ)
523 self.socket = self.context.socket(zmq.REQ)
521 self.socket.setsockopt(zmq.LINGER, 0)
524 self.socket.linger = 1000
522 self.socket.connect(self.address)
525 self.socket.connect(self.address)
523
526
524 self.poller.register(self.socket, zmq.POLLIN)
527 self.poller.register(self.socket, zmq.POLLIN)
525
528
526 def _poll(self, start_time):
529 def _poll(self, start_time):
527 """poll for heartbeat replies until we reach self.time_to_dead.
530 """poll for heartbeat replies until we reach self.time_to_dead.
528
531
529 Ignores interrupts, and returns the result of poll(), which
532 Ignores interrupts, and returns the result of poll(), which
530 will be an empty list if no messages arrived before the timeout,
533 will be an empty list if no messages arrived before the timeout,
531 or the event tuple if there is a message to receive.
534 or the event tuple if there is a message to receive.
532 """
535 """
533
536
534 until_dead = self.time_to_dead - (time.time() - start_time)
537 until_dead = self.time_to_dead - (time.time() - start_time)
535 # ensure poll at least once
538 # ensure poll at least once
536 until_dead = max(until_dead, 1e-3)
539 until_dead = max(until_dead, 1e-3)
537 events = []
540 events = []
538 while True:
541 while True:
539 try:
542 try:
540 events = self.poller.poll(1000 * until_dead)
543 events = self.poller.poll(1000 * until_dead)
541 except ZMQError as e:
544 except ZMQError as e:
542 if e.errno == errno.EINTR:
545 if e.errno == errno.EINTR:
543 # ignore interrupts during heartbeat
546 # ignore interrupts during heartbeat
544 # this may never actually happen
547 # this may never actually happen
545 until_dead = self.time_to_dead - (time.time() - start_time)
548 until_dead = self.time_to_dead - (time.time() - start_time)
546 until_dead = max(until_dead, 1e-3)
549 until_dead = max(until_dead, 1e-3)
547 pass
550 pass
548 else:
551 else:
549 raise
552 raise
550 except Exception:
553 except Exception:
551 if self._exiting:
554 if self._exiting:
552 break
555 break
553 else:
556 else:
554 raise
557 raise
555 else:
558 else:
556 break
559 break
557 return events
560 return events
558
561
559 def run(self):
562 def run(self):
560 """The thread's main activity. Call start() instead."""
563 """The thread's main activity. Call start() instead."""
561 self._create_socket()
564 self._create_socket()
562 self._running = True
565 self._running = True
563 self._beating = True
566 self._beating = True
564
567
565 while self._running:
568 while self._running:
566 if self._pause:
569 if self._pause:
567 # just sleep, and skip the rest of the loop
570 # just sleep, and skip the rest of the loop
568 time.sleep(self.time_to_dead)
571 time.sleep(self.time_to_dead)
569 continue
572 continue
570
573
571 since_last_heartbeat = 0.0
574 since_last_heartbeat = 0.0
572 # io.rprint('Ping from HB channel') # dbg
575 # io.rprint('Ping from HB channel') # dbg
573 # no need to catch EFSM here, because the previous event was
576 # no need to catch EFSM here, because the previous event was
574 # either a recv or connect, which cannot be followed by EFSM
577 # either a recv or connect, which cannot be followed by EFSM
575 self.socket.send(b'ping')
578 self.socket.send(b'ping')
576 request_time = time.time()
579 request_time = time.time()
577 ready = self._poll(request_time)
580 ready = self._poll(request_time)
578 if ready:
581 if ready:
579 self._beating = True
582 self._beating = True
580 # the poll above guarantees we have something to recv
583 # the poll above guarantees we have something to recv
581 self.socket.recv()
584 self.socket.recv()
582 # sleep the remainder of the cycle
585 # sleep the remainder of the cycle
583 remainder = self.time_to_dead - (time.time() - request_time)
586 remainder = self.time_to_dead - (time.time() - request_time)
584 if remainder > 0:
587 if remainder > 0:
585 time.sleep(remainder)
588 time.sleep(remainder)
586 continue
589 continue
587 else:
590 else:
588 # nothing was received within the time limit, signal heart failure
591 # nothing was received within the time limit, signal heart failure
589 self._beating = False
592 self._beating = False
590 since_last_heartbeat = time.time() - request_time
593 since_last_heartbeat = time.time() - request_time
591 self.call_handlers(since_last_heartbeat)
594 self.call_handlers(since_last_heartbeat)
592 # and close/reopen the socket, because the REQ/REP cycle has been broken
595 # and close/reopen the socket, because the REQ/REP cycle has been broken
593 self._create_socket()
596 self._create_socket()
594 continue
597 continue
595
598
596 def pause(self):
599 def pause(self):
597 """Pause the heartbeat."""
600 """Pause the heartbeat."""
598 self._pause = True
601 self._pause = True
599
602
600 def unpause(self):
603 def unpause(self):
601 """Unpause the heartbeat."""
604 """Unpause the heartbeat."""
602 self._pause = False
605 self._pause = False
603
606
604 def is_beating(self):
607 def is_beating(self):
605 """Is the heartbeat running and responsive (and not paused)."""
608 """Is the heartbeat running and responsive (and not paused)."""
606 if self.is_alive() and not self._pause and self._beating:
609 if self.is_alive() and not self._pause and self._beating:
607 return True
610 return True
608 else:
611 else:
609 return False
612 return False
610
613
611 def stop(self):
614 def stop(self):
612 """Stop the channel's event loop and join its thread."""
615 """Stop the channel's event loop and join its thread."""
613 self._running = False
616 self._running = False
614 super(HBChannel, self).stop()
617 super(HBChannel, self).stop()
615
618
616 def call_handlers(self, since_last_heartbeat):
619 def call_handlers(self, since_last_heartbeat):
617 """This method is called in the ioloop thread when a message arrives.
620 """This method is called in the ioloop thread when a message arrives.
618
621
619 Subclasses should override this method to handle incoming messages.
622 Subclasses should override this method to handle incoming messages.
620 It is important to remember that this method is called in the thread
623 It is important to remember that this method is called in the thread
621 so that some logic must be done to ensure that the application level
624 so that some logic must be done to ensure that the application level
622 handlers are called in the application thread.
625 handlers are called in the application thread.
623 """
626 """
624 raise NotImplementedError('call_handlers must be defined in a subclass.')
627 raise NotImplementedError('call_handlers must be defined in a subclass.')
625
628
626
629
627 #---------------------------------------------------------------------#-----------------------------------------------------------------------------
630 #---------------------------------------------------------------------#-----------------------------------------------------------------------------
628 # ABC Registration
631 # ABC Registration
629 #-----------------------------------------------------------------------------
632 #-----------------------------------------------------------------------------
630
633
631 ShellChannelABC.register(ShellChannel)
634 ShellChannelABC.register(ShellChannel)
632 IOPubChannelABC.register(IOPubChannel)
635 IOPubChannelABC.register(IOPubChannel)
633 HBChannelABC.register(HBChannel)
636 HBChannelABC.register(HBChannel)
634 StdInChannelABC.register(StdInChannel)
637 StdInChannelABC.register(StdInChannel)
@@ -1,569 +1,571 b''
1 """Utilities for connecting to kernels
1 """Utilities for connecting to kernels
2
2
3 Notable contents:
3 Notable contents:
4 - ConnectionFileMixin class
4 - ConnectionFileMixin class
5 encapsulates the logic related to writing and reading connections files.
5 encapsulates the logic related to writing and reading connections files.
6 """
6 """
7 # Copyright (c) IPython Development Team.
7 # Copyright (c) IPython Development Team.
8 # Distributed under the terms of the Modified BSD License.
8 # Distributed under the terms of the Modified BSD License.
9
9
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11 # Imports
11 # Imports
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13
13
14 from __future__ import absolute_import
14 from __future__ import absolute_import
15
15
16 import glob
16 import glob
17 import json
17 import json
18 import os
18 import os
19 import socket
19 import socket
20 import sys
20 import sys
21 from getpass import getpass
21 from getpass import getpass
22 from subprocess import Popen, PIPE
22 from subprocess import Popen, PIPE
23 import tempfile
23 import tempfile
24
24
25 import zmq
25 import zmq
26
26
27 # external imports
27 # external imports
28 from IPython.external.ssh import tunnel
28 from IPython.external.ssh import tunnel
29
29
30 # IPython imports
30 # IPython imports
31 from IPython.config import Configurable
31 from IPython.config import Configurable
32 from IPython.core.profiledir import ProfileDir
32 from IPython.core.profiledir import ProfileDir
33 from IPython.utils.localinterfaces import localhost
33 from IPython.utils.localinterfaces import localhost
34 from IPython.utils.path import filefind, get_ipython_dir
34 from IPython.utils.path import filefind, get_ipython_dir
35 from IPython.utils.py3compat import (str_to_bytes, bytes_to_str, cast_bytes_py2,
35 from IPython.utils.py3compat import (str_to_bytes, bytes_to_str, cast_bytes_py2,
36 string_types)
36 string_types)
37 from IPython.utils.traitlets import (
37 from IPython.utils.traitlets import (
38 Bool, Integer, Unicode, CaselessStrEnum,
38 Bool, Integer, Unicode, CaselessStrEnum,
39 )
39 )
40
40
41
41
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
43 # Working with Connection Files
43 # Working with Connection Files
44 #-----------------------------------------------------------------------------
44 #-----------------------------------------------------------------------------
45
45
46 def write_connection_file(fname=None, shell_port=0, iopub_port=0, stdin_port=0, hb_port=0,
46 def write_connection_file(fname=None, shell_port=0, iopub_port=0, stdin_port=0, hb_port=0,
47 control_port=0, ip='', key=b'', transport='tcp',
47 control_port=0, ip='', key=b'', transport='tcp',
48 signature_scheme='hmac-sha256',
48 signature_scheme='hmac-sha256',
49 ):
49 ):
50 """Generates a JSON config file, including the selection of random ports.
50 """Generates a JSON config file, including the selection of random ports.
51
51
52 Parameters
52 Parameters
53 ----------
53 ----------
54
54
55 fname : unicode
55 fname : unicode
56 The path to the file to write
56 The path to the file to write
57
57
58 shell_port : int, optional
58 shell_port : int, optional
59 The port to use for ROUTER (shell) channel.
59 The port to use for ROUTER (shell) channel.
60
60
61 iopub_port : int, optional
61 iopub_port : int, optional
62 The port to use for the SUB channel.
62 The port to use for the SUB channel.
63
63
64 stdin_port : int, optional
64 stdin_port : int, optional
65 The port to use for the ROUTER (raw input) channel.
65 The port to use for the ROUTER (raw input) channel.
66
66
67 control_port : int, optional
67 control_port : int, optional
68 The port to use for the ROUTER (control) channel.
68 The port to use for the ROUTER (control) channel.
69
69
70 hb_port : int, optional
70 hb_port : int, optional
71 The port to use for the heartbeat REP channel.
71 The port to use for the heartbeat REP channel.
72
72
73 ip : str, optional
73 ip : str, optional
74 The ip address the kernel will bind to.
74 The ip address the kernel will bind to.
75
75
76 key : str, optional
76 key : str, optional
77 The Session key used for message authentication.
77 The Session key used for message authentication.
78
78
79 signature_scheme : str, optional
79 signature_scheme : str, optional
80 The scheme used for message authentication.
80 The scheme used for message authentication.
81 This has the form 'digest-hash', where 'digest'
81 This has the form 'digest-hash', where 'digest'
82 is the scheme used for digests, and 'hash' is the name of the hash function
82 is the scheme used for digests, and 'hash' is the name of the hash function
83 used by the digest scheme.
83 used by the digest scheme.
84 Currently, 'hmac' is the only supported digest scheme,
84 Currently, 'hmac' is the only supported digest scheme,
85 and 'sha256' is the default hash function.
85 and 'sha256' is the default hash function.
86
86
87 """
87 """
88 if not ip:
88 if not ip:
89 ip = localhost()
89 ip = localhost()
90 # default to temporary connector file
90 # default to temporary connector file
91 if not fname:
91 if not fname:
92 fd, fname = tempfile.mkstemp('.json')
92 fd, fname = tempfile.mkstemp('.json')
93 os.close(fd)
93 os.close(fd)
94
94
95 # Find open ports as necessary.
95 # Find open ports as necessary.
96
96
97 ports = []
97 ports = []
98 ports_needed = int(shell_port <= 0) + \
98 ports_needed = int(shell_port <= 0) + \
99 int(iopub_port <= 0) + \
99 int(iopub_port <= 0) + \
100 int(stdin_port <= 0) + \
100 int(stdin_port <= 0) + \
101 int(control_port <= 0) + \
101 int(control_port <= 0) + \
102 int(hb_port <= 0)
102 int(hb_port <= 0)
103 if transport == 'tcp':
103 if transport == 'tcp':
104 for i in range(ports_needed):
104 for i in range(ports_needed):
105 sock = socket.socket()
105 sock = socket.socket()
106 # struct.pack('ii', (0,0)) is 8 null bytes
106 # struct.pack('ii', (0,0)) is 8 null bytes
107 sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, b'\0' * 8)
107 sock.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, b'\0' * 8)
108 sock.bind(('', 0))
108 sock.bind(('', 0))
109 ports.append(sock)
109 ports.append(sock)
110 for i, sock in enumerate(ports):
110 for i, sock in enumerate(ports):
111 port = sock.getsockname()[1]
111 port = sock.getsockname()[1]
112 sock.close()
112 sock.close()
113 ports[i] = port
113 ports[i] = port
114 else:
114 else:
115 N = 1
115 N = 1
116 for i in range(ports_needed):
116 for i in range(ports_needed):
117 while os.path.exists("%s-%s" % (ip, str(N))):
117 while os.path.exists("%s-%s" % (ip, str(N))):
118 N += 1
118 N += 1
119 ports.append(N)
119 ports.append(N)
120 N += 1
120 N += 1
121 if shell_port <= 0:
121 if shell_port <= 0:
122 shell_port = ports.pop(0)
122 shell_port = ports.pop(0)
123 if iopub_port <= 0:
123 if iopub_port <= 0:
124 iopub_port = ports.pop(0)
124 iopub_port = ports.pop(0)
125 if stdin_port <= 0:
125 if stdin_port <= 0:
126 stdin_port = ports.pop(0)
126 stdin_port = ports.pop(0)
127 if control_port <= 0:
127 if control_port <= 0:
128 control_port = ports.pop(0)
128 control_port = ports.pop(0)
129 if hb_port <= 0:
129 if hb_port <= 0:
130 hb_port = ports.pop(0)
130 hb_port = ports.pop(0)
131
131
132 cfg = dict( shell_port=shell_port,
132 cfg = dict( shell_port=shell_port,
133 iopub_port=iopub_port,
133 iopub_port=iopub_port,
134 stdin_port=stdin_port,
134 stdin_port=stdin_port,
135 control_port=control_port,
135 control_port=control_port,
136 hb_port=hb_port,
136 hb_port=hb_port,
137 )
137 )
138 cfg['ip'] = ip
138 cfg['ip'] = ip
139 cfg['key'] = bytes_to_str(key)
139 cfg['key'] = bytes_to_str(key)
140 cfg['transport'] = transport
140 cfg['transport'] = transport
141 cfg['signature_scheme'] = signature_scheme
141 cfg['signature_scheme'] = signature_scheme
142
142
143 with open(fname, 'w') as f:
143 with open(fname, 'w') as f:
144 f.write(json.dumps(cfg, indent=2))
144 f.write(json.dumps(cfg, indent=2))
145
145
146 return fname, cfg
146 return fname, cfg
147
147
148
148
149 def get_connection_file(app=None):
149 def get_connection_file(app=None):
150 """Return the path to the connection file of an app
150 """Return the path to the connection file of an app
151
151
152 Parameters
152 Parameters
153 ----------
153 ----------
154 app : IPKernelApp instance [optional]
154 app : IPKernelApp instance [optional]
155 If unspecified, the currently running app will be used
155 If unspecified, the currently running app will be used
156 """
156 """
157 if app is None:
157 if app is None:
158 from IPython.kernel.zmq.kernelapp import IPKernelApp
158 from IPython.kernel.zmq.kernelapp import IPKernelApp
159 if not IPKernelApp.initialized():
159 if not IPKernelApp.initialized():
160 raise RuntimeError("app not specified, and not in a running Kernel")
160 raise RuntimeError("app not specified, and not in a running Kernel")
161
161
162 app = IPKernelApp.instance()
162 app = IPKernelApp.instance()
163 return filefind(app.connection_file, ['.', app.profile_dir.security_dir])
163 return filefind(app.connection_file, ['.', app.profile_dir.security_dir])
164
164
165
165
166 def find_connection_file(filename, profile=None):
166 def find_connection_file(filename, profile=None):
167 """find a connection file, and return its absolute path.
167 """find a connection file, and return its absolute path.
168
168
169 The current working directory and the profile's security
169 The current working directory and the profile's security
170 directory will be searched for the file if it is not given by
170 directory will be searched for the file if it is not given by
171 absolute path.
171 absolute path.
172
172
173 If profile is unspecified, then the current running application's
173 If profile is unspecified, then the current running application's
174 profile will be used, or 'default', if not run from IPython.
174 profile will be used, or 'default', if not run from IPython.
175
175
176 If the argument does not match an existing file, it will be interpreted as a
176 If the argument does not match an existing file, it will be interpreted as a
177 fileglob, and the matching file in the profile's security dir with
177 fileglob, and the matching file in the profile's security dir with
178 the latest access time will be used.
178 the latest access time will be used.
179
179
180 Parameters
180 Parameters
181 ----------
181 ----------
182 filename : str
182 filename : str
183 The connection file or fileglob to search for.
183 The connection file or fileglob to search for.
184 profile : str [optional]
184 profile : str [optional]
185 The name of the profile to use when searching for the connection file,
185 The name of the profile to use when searching for the connection file,
186 if different from the current IPython session or 'default'.
186 if different from the current IPython session or 'default'.
187
187
188 Returns
188 Returns
189 -------
189 -------
190 str : The absolute path of the connection file.
190 str : The absolute path of the connection file.
191 """
191 """
192 from IPython.core.application import BaseIPythonApplication as IPApp
192 from IPython.core.application import BaseIPythonApplication as IPApp
193 try:
193 try:
194 # quick check for absolute path, before going through logic
194 # quick check for absolute path, before going through logic
195 return filefind(filename)
195 return filefind(filename)
196 except IOError:
196 except IOError:
197 pass
197 pass
198
198
199 if profile is None:
199 if profile is None:
200 # profile unspecified, check if running from an IPython app
200 # profile unspecified, check if running from an IPython app
201 if IPApp.initialized():
201 if IPApp.initialized():
202 app = IPApp.instance()
202 app = IPApp.instance()
203 profile_dir = app.profile_dir
203 profile_dir = app.profile_dir
204 else:
204 else:
205 # not running in IPython, use default profile
205 # not running in IPython, use default profile
206 profile_dir = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), 'default')
206 profile_dir = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), 'default')
207 else:
207 else:
208 # find profiledir by profile name:
208 # find profiledir by profile name:
209 profile_dir = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), profile)
209 profile_dir = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), profile)
210 security_dir = profile_dir.security_dir
210 security_dir = profile_dir.security_dir
211
211
212 try:
212 try:
213 # first, try explicit name
213 # first, try explicit name
214 return filefind(filename, ['.', security_dir])
214 return filefind(filename, ['.', security_dir])
215 except IOError:
215 except IOError:
216 pass
216 pass
217
217
218 # not found by full name
218 # not found by full name
219
219
220 if '*' in filename:
220 if '*' in filename:
221 # given as a glob already
221 # given as a glob already
222 pat = filename
222 pat = filename
223 else:
223 else:
224 # accept any substring match
224 # accept any substring match
225 pat = '*%s*' % filename
225 pat = '*%s*' % filename
226 matches = glob.glob( os.path.join(security_dir, pat) )
226 matches = glob.glob( os.path.join(security_dir, pat) )
227 if not matches:
227 if not matches:
228 raise IOError("Could not find %r in %r" % (filename, security_dir))
228 raise IOError("Could not find %r in %r" % (filename, security_dir))
229 elif len(matches) == 1:
229 elif len(matches) == 1:
230 return matches[0]
230 return matches[0]
231 else:
231 else:
232 # get most recent match, by access time:
232 # get most recent match, by access time:
233 return sorted(matches, key=lambda f: os.stat(f).st_atime)[-1]
233 return sorted(matches, key=lambda f: os.stat(f).st_atime)[-1]
234
234
235
235
236 def get_connection_info(connection_file=None, unpack=False, profile=None):
236 def get_connection_info(connection_file=None, unpack=False, profile=None):
237 """Return the connection information for the current Kernel.
237 """Return the connection information for the current Kernel.
238
238
239 Parameters
239 Parameters
240 ----------
240 ----------
241 connection_file : str [optional]
241 connection_file : str [optional]
242 The connection file to be used. Can be given by absolute path, or
242 The connection file to be used. Can be given by absolute path, or
243 IPython will search in the security directory of a given profile.
243 IPython will search in the security directory of a given profile.
244 If run from IPython,
244 If run from IPython,
245
245
246 If unspecified, the connection file for the currently running
246 If unspecified, the connection file for the currently running
247 IPython Kernel will be used, which is only allowed from inside a kernel.
247 IPython Kernel will be used, which is only allowed from inside a kernel.
248 unpack : bool [default: False]
248 unpack : bool [default: False]
249 if True, return the unpacked dict, otherwise just the string contents
249 if True, return the unpacked dict, otherwise just the string contents
250 of the file.
250 of the file.
251 profile : str [optional]
251 profile : str [optional]
252 The name of the profile to use when searching for the connection file,
252 The name of the profile to use when searching for the connection file,
253 if different from the current IPython session or 'default'.
253 if different from the current IPython session or 'default'.
254
254
255
255
256 Returns
256 Returns
257 -------
257 -------
258 The connection dictionary of the current kernel, as string or dict,
258 The connection dictionary of the current kernel, as string or dict,
259 depending on `unpack`.
259 depending on `unpack`.
260 """
260 """
261 if connection_file is None:
261 if connection_file is None:
262 # get connection file from current kernel
262 # get connection file from current kernel
263 cf = get_connection_file()
263 cf = get_connection_file()
264 else:
264 else:
265 # connection file specified, allow shortnames:
265 # connection file specified, allow shortnames:
266 cf = find_connection_file(connection_file, profile=profile)
266 cf = find_connection_file(connection_file, profile=profile)
267
267
268 with open(cf) as f:
268 with open(cf) as f:
269 info = f.read()
269 info = f.read()
270
270
271 if unpack:
271 if unpack:
272 info = json.loads(info)
272 info = json.loads(info)
273 # ensure key is bytes:
273 # ensure key is bytes:
274 info['key'] = str_to_bytes(info.get('key', ''))
274 info['key'] = str_to_bytes(info.get('key', ''))
275 return info
275 return info
276
276
277
277
278 def connect_qtconsole(connection_file=None, argv=None, profile=None):
278 def connect_qtconsole(connection_file=None, argv=None, profile=None):
279 """Connect a qtconsole to the current kernel.
279 """Connect a qtconsole to the current kernel.
280
280
281 This is useful for connecting a second qtconsole to a kernel, or to a
281 This is useful for connecting a second qtconsole to a kernel, or to a
282 local notebook.
282 local notebook.
283
283
284 Parameters
284 Parameters
285 ----------
285 ----------
286 connection_file : str [optional]
286 connection_file : str [optional]
287 The connection file to be used. Can be given by absolute path, or
287 The connection file to be used. Can be given by absolute path, or
288 IPython will search in the security directory of a given profile.
288 IPython will search in the security directory of a given profile.
289 If run from IPython,
289 If run from IPython,
290
290
291 If unspecified, the connection file for the currently running
291 If unspecified, the connection file for the currently running
292 IPython Kernel will be used, which is only allowed from inside a kernel.
292 IPython Kernel will be used, which is only allowed from inside a kernel.
293 argv : list [optional]
293 argv : list [optional]
294 Any extra args to be passed to the console.
294 Any extra args to be passed to the console.
295 profile : str [optional]
295 profile : str [optional]
296 The name of the profile to use when searching for the connection file,
296 The name of the profile to use when searching for the connection file,
297 if different from the current IPython session or 'default'.
297 if different from the current IPython session or 'default'.
298
298
299
299
300 Returns
300 Returns
301 -------
301 -------
302 subprocess.Popen instance running the qtconsole frontend
302 subprocess.Popen instance running the qtconsole frontend
303 """
303 """
304 argv = [] if argv is None else argv
304 argv = [] if argv is None else argv
305
305
306 if connection_file is None:
306 if connection_file is None:
307 # get connection file from current kernel
307 # get connection file from current kernel
308 cf = get_connection_file()
308 cf = get_connection_file()
309 else:
309 else:
310 cf = find_connection_file(connection_file, profile=profile)
310 cf = find_connection_file(connection_file, profile=profile)
311
311
312 cmd = ';'.join([
312 cmd = ';'.join([
313 "from IPython.qt.console import qtconsoleapp",
313 "from IPython.qt.console import qtconsoleapp",
314 "qtconsoleapp.main()"
314 "qtconsoleapp.main()"
315 ])
315 ])
316
316
317 return Popen([sys.executable, '-c', cmd, '--existing', cf] + argv,
317 return Popen([sys.executable, '-c', cmd, '--existing', cf] + argv,
318 stdout=PIPE, stderr=PIPE, close_fds=(sys.platform != 'win32'),
318 stdout=PIPE, stderr=PIPE, close_fds=(sys.platform != 'win32'),
319 )
319 )
320
320
321
321
322 def tunnel_to_kernel(connection_info, sshserver, sshkey=None):
322 def tunnel_to_kernel(connection_info, sshserver, sshkey=None):
323 """tunnel connections to a kernel via ssh
323 """tunnel connections to a kernel via ssh
324
324
325 This will open four SSH tunnels from localhost on this machine to the
325 This will open four SSH tunnels from localhost on this machine to the
326 ports associated with the kernel. They can be either direct
326 ports associated with the kernel. They can be either direct
327 localhost-localhost tunnels, or if an intermediate server is necessary,
327 localhost-localhost tunnels, or if an intermediate server is necessary,
328 the kernel must be listening on a public IP.
328 the kernel must be listening on a public IP.
329
329
330 Parameters
330 Parameters
331 ----------
331 ----------
332 connection_info : dict or str (path)
332 connection_info : dict or str (path)
333 Either a connection dict, or the path to a JSON connection file
333 Either a connection dict, or the path to a JSON connection file
334 sshserver : str
334 sshserver : str
335 The ssh sever to use to tunnel to the kernel. Can be a full
335 The ssh sever to use to tunnel to the kernel. Can be a full
336 `user@server:port` string. ssh config aliases are respected.
336 `user@server:port` string. ssh config aliases are respected.
337 sshkey : str [optional]
337 sshkey : str [optional]
338 Path to file containing ssh key to use for authentication.
338 Path to file containing ssh key to use for authentication.
339 Only necessary if your ssh config does not already associate
339 Only necessary if your ssh config does not already associate
340 a keyfile with the host.
340 a keyfile with the host.
341
341
342 Returns
342 Returns
343 -------
343 -------
344
344
345 (shell, iopub, stdin, hb) : ints
345 (shell, iopub, stdin, hb) : ints
346 The four ports on localhost that have been forwarded to the kernel.
346 The four ports on localhost that have been forwarded to the kernel.
347 """
347 """
348 if isinstance(connection_info, string_types):
348 if isinstance(connection_info, string_types):
349 # it's a path, unpack it
349 # it's a path, unpack it
350 with open(connection_info) as f:
350 with open(connection_info) as f:
351 connection_info = json.loads(f.read())
351 connection_info = json.loads(f.read())
352
352
353 cf = connection_info
353 cf = connection_info
354
354
355 lports = tunnel.select_random_ports(4)
355 lports = tunnel.select_random_ports(4)
356 rports = cf['shell_port'], cf['iopub_port'], cf['stdin_port'], cf['hb_port']
356 rports = cf['shell_port'], cf['iopub_port'], cf['stdin_port'], cf['hb_port']
357
357
358 remote_ip = cf['ip']
358 remote_ip = cf['ip']
359
359
360 if tunnel.try_passwordless_ssh(sshserver, sshkey):
360 if tunnel.try_passwordless_ssh(sshserver, sshkey):
361 password=False
361 password=False
362 else:
362 else:
363 password = getpass("SSH Password for %s: " % cast_bytes_py2(sshserver))
363 password = getpass("SSH Password for %s: " % cast_bytes_py2(sshserver))
364
364
365 for lp,rp in zip(lports, rports):
365 for lp,rp in zip(lports, rports):
366 tunnel.ssh_tunnel(lp, rp, sshserver, remote_ip, sshkey, password)
366 tunnel.ssh_tunnel(lp, rp, sshserver, remote_ip, sshkey, password)
367
367
368 return tuple(lports)
368 return tuple(lports)
369
369
370
370
371 #-----------------------------------------------------------------------------
371 #-----------------------------------------------------------------------------
372 # Mixin for classes that work with connection files
372 # Mixin for classes that work with connection files
373 #-----------------------------------------------------------------------------
373 #-----------------------------------------------------------------------------
374
374
375 channel_socket_types = {
375 channel_socket_types = {
376 'hb' : zmq.REQ,
376 'hb' : zmq.REQ,
377 'shell' : zmq.DEALER,
377 'shell' : zmq.DEALER,
378 'iopub' : zmq.SUB,
378 'iopub' : zmq.SUB,
379 'stdin' : zmq.DEALER,
379 'stdin' : zmq.DEALER,
380 'control': zmq.DEALER,
380 'control': zmq.DEALER,
381 }
381 }
382
382
383 port_names = [ "%s_port" % channel for channel in ('shell', 'stdin', 'iopub', 'hb', 'control')]
383 port_names = [ "%s_port" % channel for channel in ('shell', 'stdin', 'iopub', 'hb', 'control')]
384
384
385 class ConnectionFileMixin(Configurable):
385 class ConnectionFileMixin(Configurable):
386 """Mixin for configurable classes that work with connection files"""
386 """Mixin for configurable classes that work with connection files"""
387
387
388 # The addresses for the communication channels
388 # The addresses for the communication channels
389 connection_file = Unicode('', config=True,
389 connection_file = Unicode('', config=True,
390 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
390 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
391
391
392 This file will contain the IP, ports, and authentication key needed to connect
392 This file will contain the IP, ports, and authentication key needed to connect
393 clients to this kernel. By default, this file will be created in the security dir
393 clients to this kernel. By default, this file will be created in the security dir
394 of the current profile, but can be specified by absolute path.
394 of the current profile, but can be specified by absolute path.
395 """)
395 """)
396 _connection_file_written = Bool(False)
396 _connection_file_written = Bool(False)
397
397
398 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
398 transport = CaselessStrEnum(['tcp', 'ipc'], default_value='tcp', config=True)
399
399
400 ip = Unicode(config=True,
400 ip = Unicode(config=True,
401 help="""Set the kernel\'s IP address [default localhost].
401 help="""Set the kernel\'s IP address [default localhost].
402 If the IP address is something other than localhost, then
402 If the IP address is something other than localhost, then
403 Consoles on other machines will be able to connect
403 Consoles on other machines will be able to connect
404 to the Kernel, so be careful!"""
404 to the Kernel, so be careful!"""
405 )
405 )
406
406
407 def _ip_default(self):
407 def _ip_default(self):
408 if self.transport == 'ipc':
408 if self.transport == 'ipc':
409 if self.connection_file:
409 if self.connection_file:
410 return os.path.splitext(self.connection_file)[0] + '-ipc'
410 return os.path.splitext(self.connection_file)[0] + '-ipc'
411 else:
411 else:
412 return 'kernel-ipc'
412 return 'kernel-ipc'
413 else:
413 else:
414 return localhost()
414 return localhost()
415
415
416 def _ip_changed(self, name, old, new):
416 def _ip_changed(self, name, old, new):
417 if new == '*':
417 if new == '*':
418 self.ip = '0.0.0.0'
418 self.ip = '0.0.0.0'
419
419
420 # protected traits
420 # protected traits
421
421
422 hb_port = Integer(0, config=True,
422 hb_port = Integer(0, config=True,
423 help="set the heartbeat port [default: random]")
423 help="set the heartbeat port [default: random]")
424 shell_port = Integer(0, config=True,
424 shell_port = Integer(0, config=True,
425 help="set the shell (ROUTER) port [default: random]")
425 help="set the shell (ROUTER) port [default: random]")
426 iopub_port = Integer(0, config=True,
426 iopub_port = Integer(0, config=True,
427 help="set the iopub (PUB) port [default: random]")
427 help="set the iopub (PUB) port [default: random]")
428 stdin_port = Integer(0, config=True,
428 stdin_port = Integer(0, config=True,
429 help="set the stdin (ROUTER) port [default: random]")
429 help="set the stdin (ROUTER) port [default: random]")
430 control_port = Integer(0, config=True,
430 control_port = Integer(0, config=True,
431 help="set the control (ROUTER) port [default: random]")
431 help="set the control (ROUTER) port [default: random]")
432
432
433 @property
433 @property
434 def ports(self):
434 def ports(self):
435 return [ getattr(self, name) for name in port_names ]
435 return [ getattr(self, name) for name in port_names ]
436
436
437 #--------------------------------------------------------------------------
437 #--------------------------------------------------------------------------
438 # Connection and ipc file management
438 # Connection and ipc file management
439 #--------------------------------------------------------------------------
439 #--------------------------------------------------------------------------
440
440
441 def get_connection_info(self):
441 def get_connection_info(self):
442 """return the connection info as a dict"""
442 """return the connection info as a dict"""
443 return dict(
443 return dict(
444 transport=self.transport,
444 transport=self.transport,
445 ip=self.ip,
445 ip=self.ip,
446 shell_port=self.shell_port,
446 shell_port=self.shell_port,
447 iopub_port=self.iopub_port,
447 iopub_port=self.iopub_port,
448 stdin_port=self.stdin_port,
448 stdin_port=self.stdin_port,
449 hb_port=self.hb_port,
449 hb_port=self.hb_port,
450 control_port=self.control_port,
450 control_port=self.control_port,
451 signature_scheme=self.session.signature_scheme,
451 signature_scheme=self.session.signature_scheme,
452 key=self.session.key,
452 key=self.session.key,
453 )
453 )
454
454
455 def cleanup_connection_file(self):
455 def cleanup_connection_file(self):
456 """Cleanup connection file *if we wrote it*
456 """Cleanup connection file *if we wrote it*
457
457
458 Will not raise if the connection file was already removed somehow.
458 Will not raise if the connection file was already removed somehow.
459 """
459 """
460 if self._connection_file_written:
460 if self._connection_file_written:
461 # cleanup connection files on full shutdown of kernel we started
461 # cleanup connection files on full shutdown of kernel we started
462 self._connection_file_written = False
462 self._connection_file_written = False
463 try:
463 try:
464 os.remove(self.connection_file)
464 os.remove(self.connection_file)
465 except (IOError, OSError, AttributeError):
465 except (IOError, OSError, AttributeError):
466 pass
466 pass
467
467
468 def cleanup_ipc_files(self):
468 def cleanup_ipc_files(self):
469 """Cleanup ipc files if we wrote them."""
469 """Cleanup ipc files if we wrote them."""
470 if self.transport != 'ipc':
470 if self.transport != 'ipc':
471 return
471 return
472 for port in self.ports:
472 for port in self.ports:
473 ipcfile = "%s-%i" % (self.ip, port)
473 ipcfile = "%s-%i" % (self.ip, port)
474 try:
474 try:
475 os.remove(ipcfile)
475 os.remove(ipcfile)
476 except (IOError, OSError):
476 except (IOError, OSError):
477 pass
477 pass
478
478
479 def write_connection_file(self):
479 def write_connection_file(self):
480 """Write connection info to JSON dict in self.connection_file."""
480 """Write connection info to JSON dict in self.connection_file."""
481 if self._connection_file_written and os.path.exists(self.connection_file):
481 if self._connection_file_written and os.path.exists(self.connection_file):
482 return
482 return
483
483
484 self.connection_file, cfg = write_connection_file(self.connection_file,
484 self.connection_file, cfg = write_connection_file(self.connection_file,
485 transport=self.transport, ip=self.ip, key=self.session.key,
485 transport=self.transport, ip=self.ip, key=self.session.key,
486 stdin_port=self.stdin_port, iopub_port=self.iopub_port,
486 stdin_port=self.stdin_port, iopub_port=self.iopub_port,
487 shell_port=self.shell_port, hb_port=self.hb_port,
487 shell_port=self.shell_port, hb_port=self.hb_port,
488 control_port=self.control_port,
488 control_port=self.control_port,
489 signature_scheme=self.session.signature_scheme,
489 signature_scheme=self.session.signature_scheme,
490 )
490 )
491 # write_connection_file also sets default ports:
491 # write_connection_file also sets default ports:
492 for name in port_names:
492 for name in port_names:
493 setattr(self, name, cfg[name])
493 setattr(self, name, cfg[name])
494
494
495 self._connection_file_written = True
495 self._connection_file_written = True
496
496
497 def load_connection_file(self):
497 def load_connection_file(self):
498 """Load connection info from JSON dict in self.connection_file."""
498 """Load connection info from JSON dict in self.connection_file."""
499 self.log.debug(u"Loading connection file %s", self.connection_file)
499 self.log.debug(u"Loading connection file %s", self.connection_file)
500 with open(self.connection_file) as f:
500 with open(self.connection_file) as f:
501 cfg = json.load(f)
501 cfg = json.load(f)
502 self.transport = cfg.get('transport', self.transport)
502 self.transport = cfg.get('transport', self.transport)
503 self.ip = cfg.get('ip', self._ip_default())
503 self.ip = cfg.get('ip', self._ip_default())
504
504
505 for name in port_names:
505 for name in port_names:
506 if getattr(self, name) == 0 and name in cfg:
506 if getattr(self, name) == 0 and name in cfg:
507 # not overridden by config or cl_args
507 # not overridden by config or cl_args
508 setattr(self, name, cfg[name])
508 setattr(self, name, cfg[name])
509 if 'key' in cfg:
509 if 'key' in cfg:
510 self.config.Session.key = str_to_bytes(cfg['key'])
510 self.config.Session.key = str_to_bytes(cfg['key'])
511 if 'signature_scheme' in cfg:
511 if 'signature_scheme' in cfg:
512 self.config.Session.signature_scheme = cfg['signature_scheme']
512 self.config.Session.signature_scheme = cfg['signature_scheme']
513 #--------------------------------------------------------------------------
513 #--------------------------------------------------------------------------
514 # Creating connected sockets
514 # Creating connected sockets
515 #--------------------------------------------------------------------------
515 #--------------------------------------------------------------------------
516
516
517 def _make_url(self, channel):
517 def _make_url(self, channel):
518 """Make a ZeroMQ URL for a given channel."""
518 """Make a ZeroMQ URL for a given channel."""
519 transport = self.transport
519 transport = self.transport
520 ip = self.ip
520 ip = self.ip
521 port = getattr(self, '%s_port' % channel)
521 port = getattr(self, '%s_port' % channel)
522
522
523 if transport == 'tcp':
523 if transport == 'tcp':
524 return "tcp://%s:%i" % (ip, port)
524 return "tcp://%s:%i" % (ip, port)
525 else:
525 else:
526 return "%s://%s-%s" % (transport, ip, port)
526 return "%s://%s-%s" % (transport, ip, port)
527
527
528 def _create_connected_socket(self, channel, identity=None):
528 def _create_connected_socket(self, channel, identity=None):
529 """Create a zmq Socket and connect it to the kernel."""
529 """Create a zmq Socket and connect it to the kernel."""
530 url = self._make_url(channel)
530 url = self._make_url(channel)
531 socket_type = channel_socket_types[channel]
531 socket_type = channel_socket_types[channel]
532 self.log.debug("Connecting to: %s" % url)
532 self.log.debug("Connecting to: %s" % url)
533 sock = self.context.socket(socket_type)
533 sock = self.context.socket(socket_type)
534 # set linger to 1s to prevent hangs at exit
535 sock.linger = 1000
534 if identity:
536 if identity:
535 sock.identity = identity
537 sock.identity = identity
536 sock.connect(url)
538 sock.connect(url)
537 return sock
539 return sock
538
540
539 def connect_iopub(self, identity=None):
541 def connect_iopub(self, identity=None):
540 """return zmq Socket connected to the IOPub channel"""
542 """return zmq Socket connected to the IOPub channel"""
541 sock = self._create_connected_socket('iopub', identity=identity)
543 sock = self._create_connected_socket('iopub', identity=identity)
542 sock.setsockopt(zmq.SUBSCRIBE, b'')
544 sock.setsockopt(zmq.SUBSCRIBE, b'')
543 return sock
545 return sock
544
546
545 def connect_shell(self, identity=None):
547 def connect_shell(self, identity=None):
546 """return zmq Socket connected to the Shell channel"""
548 """return zmq Socket connected to the Shell channel"""
547 return self._create_connected_socket('shell', identity=identity)
549 return self._create_connected_socket('shell', identity=identity)
548
550
549 def connect_stdin(self, identity=None):
551 def connect_stdin(self, identity=None):
550 """return zmq Socket connected to the StdIn channel"""
552 """return zmq Socket connected to the StdIn channel"""
551 return self._create_connected_socket('stdin', identity=identity)
553 return self._create_connected_socket('stdin', identity=identity)
552
554
553 def connect_hb(self, identity=None):
555 def connect_hb(self, identity=None):
554 """return zmq Socket connected to the Heartbeat channel"""
556 """return zmq Socket connected to the Heartbeat channel"""
555 return self._create_connected_socket('hb', identity=identity)
557 return self._create_connected_socket('hb', identity=identity)
556
558
557 def connect_control(self, identity=None):
559 def connect_control(self, identity=None):
558 """return zmq Socket connected to the Heartbeat channel"""
560 """return zmq Socket connected to the Heartbeat channel"""
559 return self._create_connected_socket('control', identity=identity)
561 return self._create_connected_socket('control', identity=identity)
560
562
561
563
562 __all__ = [
564 __all__ = [
563 'write_connection_file',
565 'write_connection_file',
564 'get_connection_file',
566 'get_connection_file',
565 'find_connection_file',
567 'find_connection_file',
566 'get_connection_info',
568 'get_connection_info',
567 'connect_qtconsole',
569 'connect_qtconsole',
568 'tunnel_to_kernel',
570 'tunnel_to_kernel',
569 ]
571 ]
@@ -1,67 +1,68 b''
1 """The client and server for a basic ping-pong style heartbeat.
1 """The client and server for a basic ping-pong style heartbeat.
2 """
2 """
3
3
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (C) 2008-2011 The IPython Development Team
5 # Copyright (C) 2008-2011 The IPython Development Team
6 #
6 #
7 # Distributed under the terms of the BSD License. The full license is in
7 # Distributed under the terms of the BSD License. The full license is in
8 # the file COPYING, distributed as part of this software.
8 # the file COPYING, distributed as part of this software.
9 #-----------------------------------------------------------------------------
9 #-----------------------------------------------------------------------------
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Imports
12 # Imports
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 import errno
15 import errno
16 import os
16 import os
17 import socket
17 import socket
18 from threading import Thread
18 from threading import Thread
19
19
20 import zmq
20 import zmq
21
21
22 from IPython.utils.localinterfaces import localhost
22 from IPython.utils.localinterfaces import localhost
23
23
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Code
25 # Code
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27
27
28
28
29 class Heartbeat(Thread):
29 class Heartbeat(Thread):
30 "A simple ping-pong style heartbeat that runs in a thread."
30 "A simple ping-pong style heartbeat that runs in a thread."
31
31
32 def __init__(self, context, addr=None):
32 def __init__(self, context, addr=None):
33 if addr is None:
33 if addr is None:
34 addr = ('tcp', localhost(), 0)
34 addr = ('tcp', localhost(), 0)
35 Thread.__init__(self)
35 Thread.__init__(self)
36 self.context = context
36 self.context = context
37 self.transport, self.ip, self.port = addr
37 self.transport, self.ip, self.port = addr
38 if self.port == 0:
38 if self.port == 0:
39 if addr[0] == 'tcp':
39 if addr[0] == 'tcp':
40 s = socket.socket()
40 s = socket.socket()
41 # '*' means all interfaces to 0MQ, which is '' to socket.socket
41 # '*' means all interfaces to 0MQ, which is '' to socket.socket
42 s.bind(('' if self.ip == '*' else self.ip, 0))
42 s.bind(('' if self.ip == '*' else self.ip, 0))
43 self.port = s.getsockname()[1]
43 self.port = s.getsockname()[1]
44 s.close()
44 s.close()
45 elif addr[0] == 'ipc':
45 elif addr[0] == 'ipc':
46 self.port = 1
46 self.port = 1
47 while os.path.exists("%s-%s" % (self.ip, self.port)):
47 while os.path.exists("%s-%s" % (self.ip, self.port)):
48 self.port = self.port + 1
48 self.port = self.port + 1
49 else:
49 else:
50 raise ValueError("Unrecognized zmq transport: %s" % addr[0])
50 raise ValueError("Unrecognized zmq transport: %s" % addr[0])
51 self.addr = (self.ip, self.port)
51 self.addr = (self.ip, self.port)
52 self.daemon = True
52 self.daemon = True
53
53
54 def run(self):
54 def run(self):
55 self.socket = self.context.socket(zmq.REP)
55 self.socket = self.context.socket(zmq.REP)
56 self.socket.linger = 1000
56 c = ':' if self.transport == 'tcp' else '-'
57 c = ':' if self.transport == 'tcp' else '-'
57 self.socket.bind('%s://%s' % (self.transport, self.ip) + c + str(self.port))
58 self.socket.bind('%s://%s' % (self.transport, self.ip) + c + str(self.port))
58 while True:
59 while True:
59 try:
60 try:
60 zmq.device(zmq.FORWARDER, self.socket, self.socket)
61 zmq.device(zmq.FORWARDER, self.socket, self.socket)
61 except zmq.ZMQError as e:
62 except zmq.ZMQError as e:
62 if e.errno == errno.EINTR:
63 if e.errno == errno.EINTR:
63 continue
64 continue
64 else:
65 else:
65 raise
66 raise
66 else:
67 else:
67 break
68 break
@@ -1,411 +1,415 b''
1 """An Application for launching a kernel
1 """An Application for launching a kernel
2 """
2 """
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7 # Imports
7 # Imports
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9
9
10 from __future__ import print_function
10 from __future__ import print_function
11
11
12 # Standard library imports
12 # Standard library imports
13 import atexit
13 import atexit
14 import os
14 import os
15 import sys
15 import sys
16 import signal
16 import signal
17
17
18 # System library imports
18 # System library imports
19 import zmq
19 import zmq
20 from zmq.eventloop import ioloop
20 from zmq.eventloop import ioloop
21 from zmq.eventloop.zmqstream import ZMQStream
21 from zmq.eventloop.zmqstream import ZMQStream
22
22
23 # IPython imports
23 # IPython imports
24 from IPython.core.ultratb import FormattedTB
24 from IPython.core.ultratb import FormattedTB
25 from IPython.core.application import (
25 from IPython.core.application import (
26 BaseIPythonApplication, base_flags, base_aliases, catch_config_error
26 BaseIPythonApplication, base_flags, base_aliases, catch_config_error
27 )
27 )
28 from IPython.core.profiledir import ProfileDir
28 from IPython.core.profiledir import ProfileDir
29 from IPython.core.shellapp import (
29 from IPython.core.shellapp import (
30 InteractiveShellApp, shell_flags, shell_aliases
30 InteractiveShellApp, shell_flags, shell_aliases
31 )
31 )
32 from IPython.utils import io
32 from IPython.utils import io
33 from IPython.utils.path import filefind
33 from IPython.utils.path import filefind
34 from IPython.utils.traitlets import (
34 from IPython.utils.traitlets import (
35 Any, Instance, Dict, Unicode, Integer, Bool, DottedObjectName,
35 Any, Instance, Dict, Unicode, Integer, Bool, DottedObjectName,
36 )
36 )
37 from IPython.utils.importstring import import_item
37 from IPython.utils.importstring import import_item
38 from IPython.kernel import write_connection_file
38 from IPython.kernel import write_connection_file
39 from IPython.kernel.connect import ConnectionFileMixin
39 from IPython.kernel.connect import ConnectionFileMixin
40
40
41 # local imports
41 # local imports
42 from .heartbeat import Heartbeat
42 from .heartbeat import Heartbeat
43 from .ipkernel import Kernel
43 from .ipkernel import Kernel
44 from .parentpoller import ParentPollerUnix, ParentPollerWindows
44 from .parentpoller import ParentPollerUnix, ParentPollerWindows
45 from .session import (
45 from .session import (
46 Session, session_flags, session_aliases, default_secure,
46 Session, session_flags, session_aliases, default_secure,
47 )
47 )
48 from .zmqshell import ZMQInteractiveShell
48 from .zmqshell import ZMQInteractiveShell
49
49
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51 # Flags and Aliases
51 # Flags and Aliases
52 #-----------------------------------------------------------------------------
52 #-----------------------------------------------------------------------------
53
53
54 kernel_aliases = dict(base_aliases)
54 kernel_aliases = dict(base_aliases)
55 kernel_aliases.update({
55 kernel_aliases.update({
56 'ip' : 'IPKernelApp.ip',
56 'ip' : 'IPKernelApp.ip',
57 'hb' : 'IPKernelApp.hb_port',
57 'hb' : 'IPKernelApp.hb_port',
58 'shell' : 'IPKernelApp.shell_port',
58 'shell' : 'IPKernelApp.shell_port',
59 'iopub' : 'IPKernelApp.iopub_port',
59 'iopub' : 'IPKernelApp.iopub_port',
60 'stdin' : 'IPKernelApp.stdin_port',
60 'stdin' : 'IPKernelApp.stdin_port',
61 'control' : 'IPKernelApp.control_port',
61 'control' : 'IPKernelApp.control_port',
62 'f' : 'IPKernelApp.connection_file',
62 'f' : 'IPKernelApp.connection_file',
63 'parent': 'IPKernelApp.parent_handle',
63 'parent': 'IPKernelApp.parent_handle',
64 'transport': 'IPKernelApp.transport',
64 'transport': 'IPKernelApp.transport',
65 })
65 })
66 if sys.platform.startswith('win'):
66 if sys.platform.startswith('win'):
67 kernel_aliases['interrupt'] = 'IPKernelApp.interrupt'
67 kernel_aliases['interrupt'] = 'IPKernelApp.interrupt'
68
68
69 kernel_flags = dict(base_flags)
69 kernel_flags = dict(base_flags)
70 kernel_flags.update({
70 kernel_flags.update({
71 'no-stdout' : (
71 'no-stdout' : (
72 {'IPKernelApp' : {'no_stdout' : True}},
72 {'IPKernelApp' : {'no_stdout' : True}},
73 "redirect stdout to the null device"),
73 "redirect stdout to the null device"),
74 'no-stderr' : (
74 'no-stderr' : (
75 {'IPKernelApp' : {'no_stderr' : True}},
75 {'IPKernelApp' : {'no_stderr' : True}},
76 "redirect stderr to the null device"),
76 "redirect stderr to the null device"),
77 'pylab' : (
77 'pylab' : (
78 {'IPKernelApp' : {'pylab' : 'auto'}},
78 {'IPKernelApp' : {'pylab' : 'auto'}},
79 """Pre-load matplotlib and numpy for interactive use with
79 """Pre-load matplotlib and numpy for interactive use with
80 the default matplotlib backend."""),
80 the default matplotlib backend."""),
81 })
81 })
82
82
83 # inherit flags&aliases for any IPython shell apps
83 # inherit flags&aliases for any IPython shell apps
84 kernel_aliases.update(shell_aliases)
84 kernel_aliases.update(shell_aliases)
85 kernel_flags.update(shell_flags)
85 kernel_flags.update(shell_flags)
86
86
87 # inherit flags&aliases for Sessions
87 # inherit flags&aliases for Sessions
88 kernel_aliases.update(session_aliases)
88 kernel_aliases.update(session_aliases)
89 kernel_flags.update(session_flags)
89 kernel_flags.update(session_flags)
90
90
91 _ctrl_c_message = """\
91 _ctrl_c_message = """\
92 NOTE: When using the `ipython kernel` entry point, Ctrl-C will not work.
92 NOTE: When using the `ipython kernel` entry point, Ctrl-C will not work.
93
93
94 To exit, you will have to explicitly quit this process, by either sending
94 To exit, you will have to explicitly quit this process, by either sending
95 "quit" from a client, or using Ctrl-\\ in UNIX-like environments.
95 "quit" from a client, or using Ctrl-\\ in UNIX-like environments.
96
96
97 To read more about this, see https://github.com/ipython/ipython/issues/2049
97 To read more about this, see https://github.com/ipython/ipython/issues/2049
98
98
99 """
99 """
100
100
101 #-----------------------------------------------------------------------------
101 #-----------------------------------------------------------------------------
102 # Application class for starting an IPython Kernel
102 # Application class for starting an IPython Kernel
103 #-----------------------------------------------------------------------------
103 #-----------------------------------------------------------------------------
104
104
105 class IPKernelApp(BaseIPythonApplication, InteractiveShellApp,
105 class IPKernelApp(BaseIPythonApplication, InteractiveShellApp,
106 ConnectionFileMixin):
106 ConnectionFileMixin):
107 name='ipkernel'
107 name='ipkernel'
108 aliases = Dict(kernel_aliases)
108 aliases = Dict(kernel_aliases)
109 flags = Dict(kernel_flags)
109 flags = Dict(kernel_flags)
110 classes = [Kernel, ZMQInteractiveShell, ProfileDir, Session]
110 classes = [Kernel, ZMQInteractiveShell, ProfileDir, Session]
111 # the kernel class, as an importstring
111 # the kernel class, as an importstring
112 kernel_class = DottedObjectName('IPython.kernel.zmq.ipkernel.Kernel', config=True,
112 kernel_class = DottedObjectName('IPython.kernel.zmq.ipkernel.Kernel', config=True,
113 help="""The Kernel subclass to be used.
113 help="""The Kernel subclass to be used.
114
114
115 This should allow easy re-use of the IPKernelApp entry point
115 This should allow easy re-use of the IPKernelApp entry point
116 to configure and launch kernels other than IPython's own.
116 to configure and launch kernels other than IPython's own.
117 """)
117 """)
118 kernel = Any()
118 kernel = Any()
119 poller = Any() # don't restrict this even though current pollers are all Threads
119 poller = Any() # don't restrict this even though current pollers are all Threads
120 heartbeat = Instance(Heartbeat)
120 heartbeat = Instance(Heartbeat)
121 session = Instance('IPython.kernel.zmq.session.Session')
121 session = Instance('IPython.kernel.zmq.session.Session')
122 ports = Dict()
122 ports = Dict()
123
123
124 # ipkernel doesn't get its own config file
124 # ipkernel doesn't get its own config file
125 def _config_file_name_default(self):
125 def _config_file_name_default(self):
126 return 'ipython_config.py'
126 return 'ipython_config.py'
127
127
128 # inherit config file name from parent:
128 # inherit config file name from parent:
129 parent_appname = Unicode(config=True)
129 parent_appname = Unicode(config=True)
130 def _parent_appname_changed(self, name, old, new):
130 def _parent_appname_changed(self, name, old, new):
131 if self.config_file_specified:
131 if self.config_file_specified:
132 # it was manually specified, ignore
132 # it was manually specified, ignore
133 return
133 return
134 self.config_file_name = new.replace('-','_') + u'_config.py'
134 self.config_file_name = new.replace('-','_') + u'_config.py'
135 # don't let this count as specifying the config file
135 # don't let this count as specifying the config file
136 self.config_file_specified.remove(self.config_file_name)
136 self.config_file_specified.remove(self.config_file_name)
137
137
138 # connection info:
138 # connection info:
139
139
140 @property
140 @property
141 def abs_connection_file(self):
141 def abs_connection_file(self):
142 if os.path.basename(self.connection_file) == self.connection_file:
142 if os.path.basename(self.connection_file) == self.connection_file:
143 return os.path.join(self.profile_dir.security_dir, self.connection_file)
143 return os.path.join(self.profile_dir.security_dir, self.connection_file)
144 else:
144 else:
145 return self.connection_file
145 return self.connection_file
146
146
147
147
148 # streams, etc.
148 # streams, etc.
149 no_stdout = Bool(False, config=True, help="redirect stdout to the null device")
149 no_stdout = Bool(False, config=True, help="redirect stdout to the null device")
150 no_stderr = Bool(False, config=True, help="redirect stderr to the null device")
150 no_stderr = Bool(False, config=True, help="redirect stderr to the null device")
151 outstream_class = DottedObjectName('IPython.kernel.zmq.iostream.OutStream',
151 outstream_class = DottedObjectName('IPython.kernel.zmq.iostream.OutStream',
152 config=True, help="The importstring for the OutStream factory")
152 config=True, help="The importstring for the OutStream factory")
153 displayhook_class = DottedObjectName('IPython.kernel.zmq.displayhook.ZMQDisplayHook',
153 displayhook_class = DottedObjectName('IPython.kernel.zmq.displayhook.ZMQDisplayHook',
154 config=True, help="The importstring for the DisplayHook factory")
154 config=True, help="The importstring for the DisplayHook factory")
155
155
156 # polling
156 # polling
157 parent_handle = Integer(0, config=True,
157 parent_handle = Integer(0, config=True,
158 help="""kill this process if its parent dies. On Windows, the argument
158 help="""kill this process if its parent dies. On Windows, the argument
159 specifies the HANDLE of the parent process, otherwise it is simply boolean.
159 specifies the HANDLE of the parent process, otherwise it is simply boolean.
160 """)
160 """)
161 interrupt = Integer(0, config=True,
161 interrupt = Integer(0, config=True,
162 help="""ONLY USED ON WINDOWS
162 help="""ONLY USED ON WINDOWS
163 Interrupt this process when the parent is signaled.
163 Interrupt this process when the parent is signaled.
164 """)
164 """)
165
165
166 def init_crash_handler(self):
166 def init_crash_handler(self):
167 # Install minimal exception handling
167 # Install minimal exception handling
168 sys.excepthook = FormattedTB(mode='Verbose', color_scheme='NoColor',
168 sys.excepthook = FormattedTB(mode='Verbose', color_scheme='NoColor',
169 ostream=sys.__stdout__)
169 ostream=sys.__stdout__)
170
170
171 def init_poller(self):
171 def init_poller(self):
172 if sys.platform == 'win32':
172 if sys.platform == 'win32':
173 if self.interrupt or self.parent_handle:
173 if self.interrupt or self.parent_handle:
174 self.poller = ParentPollerWindows(self.interrupt, self.parent_handle)
174 self.poller = ParentPollerWindows(self.interrupt, self.parent_handle)
175 elif self.parent_handle:
175 elif self.parent_handle:
176 self.poller = ParentPollerUnix()
176 self.poller = ParentPollerUnix()
177
177
178 def _bind_socket(self, s, port):
178 def _bind_socket(self, s, port):
179 iface = '%s://%s' % (self.transport, self.ip)
179 iface = '%s://%s' % (self.transport, self.ip)
180 if self.transport == 'tcp':
180 if self.transport == 'tcp':
181 if port <= 0:
181 if port <= 0:
182 port = s.bind_to_random_port(iface)
182 port = s.bind_to_random_port(iface)
183 else:
183 else:
184 s.bind("tcp://%s:%i" % (self.ip, port))
184 s.bind("tcp://%s:%i" % (self.ip, port))
185 elif self.transport == 'ipc':
185 elif self.transport == 'ipc':
186 if port <= 0:
186 if port <= 0:
187 port = 1
187 port = 1
188 path = "%s-%i" % (self.ip, port)
188 path = "%s-%i" % (self.ip, port)
189 while os.path.exists(path):
189 while os.path.exists(path):
190 port = port + 1
190 port = port + 1
191 path = "%s-%i" % (self.ip, port)
191 path = "%s-%i" % (self.ip, port)
192 else:
192 else:
193 path = "%s-%i" % (self.ip, port)
193 path = "%s-%i" % (self.ip, port)
194 s.bind("ipc://%s" % path)
194 s.bind("ipc://%s" % path)
195 return port
195 return port
196
196
197 def write_connection_file(self):
197 def write_connection_file(self):
198 """write connection info to JSON file"""
198 """write connection info to JSON file"""
199 cf = self.abs_connection_file
199 cf = self.abs_connection_file
200 self.log.debug("Writing connection file: %s", cf)
200 self.log.debug("Writing connection file: %s", cf)
201 write_connection_file(cf, ip=self.ip, key=self.session.key, transport=self.transport,
201 write_connection_file(cf, ip=self.ip, key=self.session.key, transport=self.transport,
202 shell_port=self.shell_port, stdin_port=self.stdin_port, hb_port=self.hb_port,
202 shell_port=self.shell_port, stdin_port=self.stdin_port, hb_port=self.hb_port,
203 iopub_port=self.iopub_port, control_port=self.control_port)
203 iopub_port=self.iopub_port, control_port=self.control_port)
204
204
205 def cleanup_connection_file(self):
205 def cleanup_connection_file(self):
206 cf = self.abs_connection_file
206 cf = self.abs_connection_file
207 self.log.debug("Cleaning up connection file: %s", cf)
207 self.log.debug("Cleaning up connection file: %s", cf)
208 try:
208 try:
209 os.remove(cf)
209 os.remove(cf)
210 except (IOError, OSError):
210 except (IOError, OSError):
211 pass
211 pass
212
212
213 self.cleanup_ipc_files()
213 self.cleanup_ipc_files()
214
214
215 def init_connection_file(self):
215 def init_connection_file(self):
216 if not self.connection_file:
216 if not self.connection_file:
217 self.connection_file = "kernel-%s.json"%os.getpid()
217 self.connection_file = "kernel-%s.json"%os.getpid()
218 try:
218 try:
219 self.connection_file = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
219 self.connection_file = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
220 except IOError:
220 except IOError:
221 self.log.debug("Connection file not found: %s", self.connection_file)
221 self.log.debug("Connection file not found: %s", self.connection_file)
222 # This means I own it, so I will clean it up:
222 # This means I own it, so I will clean it up:
223 atexit.register(self.cleanup_connection_file)
223 atexit.register(self.cleanup_connection_file)
224 return
224 return
225 try:
225 try:
226 self.load_connection_file()
226 self.load_connection_file()
227 except Exception:
227 except Exception:
228 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
228 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
229 self.exit(1)
229 self.exit(1)
230
230
231 def init_sockets(self):
231 def init_sockets(self):
232 # Create a context, a session, and the kernel sockets.
232 # Create a context, a session, and the kernel sockets.
233 self.log.info("Starting the kernel at pid: %i", os.getpid())
233 self.log.info("Starting the kernel at pid: %i", os.getpid())
234 context = zmq.Context.instance()
234 context = zmq.Context.instance()
235 # Uncomment this to try closing the context.
235 # Uncomment this to try closing the context.
236 # atexit.register(context.term)
236 # atexit.register(context.term)
237
237
238 self.shell_socket = context.socket(zmq.ROUTER)
238 self.shell_socket = context.socket(zmq.ROUTER)
239 self.shell_socket.linger = 1000
239 self.shell_port = self._bind_socket(self.shell_socket, self.shell_port)
240 self.shell_port = self._bind_socket(self.shell_socket, self.shell_port)
240 self.log.debug("shell ROUTER Channel on port: %i" % self.shell_port)
241 self.log.debug("shell ROUTER Channel on port: %i" % self.shell_port)
241
242
242 self.iopub_socket = context.socket(zmq.PUB)
243 self.iopub_socket = context.socket(zmq.PUB)
244 self.iopub_socket.linger = 1000
243 self.iopub_port = self._bind_socket(self.iopub_socket, self.iopub_port)
245 self.iopub_port = self._bind_socket(self.iopub_socket, self.iopub_port)
244 self.log.debug("iopub PUB Channel on port: %i" % self.iopub_port)
246 self.log.debug("iopub PUB Channel on port: %i" % self.iopub_port)
245
247
246 self.stdin_socket = context.socket(zmq.ROUTER)
248 self.stdin_socket = context.socket(zmq.ROUTER)
249 self.stdin_socket.linger = 1000
247 self.stdin_port = self._bind_socket(self.stdin_socket, self.stdin_port)
250 self.stdin_port = self._bind_socket(self.stdin_socket, self.stdin_port)
248 self.log.debug("stdin ROUTER Channel on port: %i" % self.stdin_port)
251 self.log.debug("stdin ROUTER Channel on port: %i" % self.stdin_port)
249
252
250 self.control_socket = context.socket(zmq.ROUTER)
253 self.control_socket = context.socket(zmq.ROUTER)
254 self.control_socket.linger = 1000
251 self.control_port = self._bind_socket(self.control_socket, self.control_port)
255 self.control_port = self._bind_socket(self.control_socket, self.control_port)
252 self.log.debug("control ROUTER Channel on port: %i" % self.control_port)
256 self.log.debug("control ROUTER Channel on port: %i" % self.control_port)
253
257
254 def init_heartbeat(self):
258 def init_heartbeat(self):
255 """start the heart beating"""
259 """start the heart beating"""
256 # heartbeat doesn't share context, because it mustn't be blocked
260 # heartbeat doesn't share context, because it mustn't be blocked
257 # by the GIL, which is accessed by libzmq when freeing zero-copy messages
261 # by the GIL, which is accessed by libzmq when freeing zero-copy messages
258 hb_ctx = zmq.Context()
262 hb_ctx = zmq.Context()
259 self.heartbeat = Heartbeat(hb_ctx, (self.transport, self.ip, self.hb_port))
263 self.heartbeat = Heartbeat(hb_ctx, (self.transport, self.ip, self.hb_port))
260 self.hb_port = self.heartbeat.port
264 self.hb_port = self.heartbeat.port
261 self.log.debug("Heartbeat REP Channel on port: %i" % self.hb_port)
265 self.log.debug("Heartbeat REP Channel on port: %i" % self.hb_port)
262 self.heartbeat.start()
266 self.heartbeat.start()
263
267
264 def log_connection_info(self):
268 def log_connection_info(self):
265 """display connection info, and store ports"""
269 """display connection info, and store ports"""
266 basename = os.path.basename(self.connection_file)
270 basename = os.path.basename(self.connection_file)
267 if basename == self.connection_file or \
271 if basename == self.connection_file or \
268 os.path.dirname(self.connection_file) == self.profile_dir.security_dir:
272 os.path.dirname(self.connection_file) == self.profile_dir.security_dir:
269 # use shortname
273 # use shortname
270 tail = basename
274 tail = basename
271 if self.profile != 'default':
275 if self.profile != 'default':
272 tail += " --profile %s" % self.profile
276 tail += " --profile %s" % self.profile
273 else:
277 else:
274 tail = self.connection_file
278 tail = self.connection_file
275 lines = [
279 lines = [
276 "To connect another client to this kernel, use:",
280 "To connect another client to this kernel, use:",
277 " --existing %s" % tail,
281 " --existing %s" % tail,
278 ]
282 ]
279 # log connection info
283 # log connection info
280 # info-level, so often not shown.
284 # info-level, so often not shown.
281 # frontends should use the %connect_info magic
285 # frontends should use the %connect_info magic
282 # to see the connection info
286 # to see the connection info
283 for line in lines:
287 for line in lines:
284 self.log.info(line)
288 self.log.info(line)
285 # also raw print to the terminal if no parent_handle (`ipython kernel`)
289 # also raw print to the terminal if no parent_handle (`ipython kernel`)
286 if not self.parent_handle:
290 if not self.parent_handle:
287 io.rprint(_ctrl_c_message)
291 io.rprint(_ctrl_c_message)
288 for line in lines:
292 for line in lines:
289 io.rprint(line)
293 io.rprint(line)
290
294
291 self.ports = dict(shell=self.shell_port, iopub=self.iopub_port,
295 self.ports = dict(shell=self.shell_port, iopub=self.iopub_port,
292 stdin=self.stdin_port, hb=self.hb_port,
296 stdin=self.stdin_port, hb=self.hb_port,
293 control=self.control_port)
297 control=self.control_port)
294
298
295 def init_session(self):
299 def init_session(self):
296 """create our session object"""
300 """create our session object"""
297 default_secure(self.config)
301 default_secure(self.config)
298 self.session = Session(parent=self, username=u'kernel')
302 self.session = Session(parent=self, username=u'kernel')
299
303
300 def init_blackhole(self):
304 def init_blackhole(self):
301 """redirects stdout/stderr to devnull if necessary"""
305 """redirects stdout/stderr to devnull if necessary"""
302 if self.no_stdout or self.no_stderr:
306 if self.no_stdout or self.no_stderr:
303 blackhole = open(os.devnull, 'w')
307 blackhole = open(os.devnull, 'w')
304 if self.no_stdout:
308 if self.no_stdout:
305 sys.stdout = sys.__stdout__ = blackhole
309 sys.stdout = sys.__stdout__ = blackhole
306 if self.no_stderr:
310 if self.no_stderr:
307 sys.stderr = sys.__stderr__ = blackhole
311 sys.stderr = sys.__stderr__ = blackhole
308
312
309 def init_io(self):
313 def init_io(self):
310 """Redirect input streams and set a display hook."""
314 """Redirect input streams and set a display hook."""
311 if self.outstream_class:
315 if self.outstream_class:
312 outstream_factory = import_item(str(self.outstream_class))
316 outstream_factory = import_item(str(self.outstream_class))
313 sys.stdout = outstream_factory(self.session, self.iopub_socket, u'stdout')
317 sys.stdout = outstream_factory(self.session, self.iopub_socket, u'stdout')
314 sys.stderr = outstream_factory(self.session, self.iopub_socket, u'stderr')
318 sys.stderr = outstream_factory(self.session, self.iopub_socket, u'stderr')
315 if self.displayhook_class:
319 if self.displayhook_class:
316 displayhook_factory = import_item(str(self.displayhook_class))
320 displayhook_factory = import_item(str(self.displayhook_class))
317 sys.displayhook = displayhook_factory(self.session, self.iopub_socket)
321 sys.displayhook = displayhook_factory(self.session, self.iopub_socket)
318
322
319 def init_signal(self):
323 def init_signal(self):
320 signal.signal(signal.SIGINT, signal.SIG_IGN)
324 signal.signal(signal.SIGINT, signal.SIG_IGN)
321
325
322 def init_kernel(self):
326 def init_kernel(self):
323 """Create the Kernel object itself"""
327 """Create the Kernel object itself"""
324 shell_stream = ZMQStream(self.shell_socket)
328 shell_stream = ZMQStream(self.shell_socket)
325 control_stream = ZMQStream(self.control_socket)
329 control_stream = ZMQStream(self.control_socket)
326
330
327 kernel_factory = import_item(str(self.kernel_class))
331 kernel_factory = import_item(str(self.kernel_class))
328
332
329 kernel = kernel_factory(parent=self, session=self.session,
333 kernel = kernel_factory(parent=self, session=self.session,
330 shell_streams=[shell_stream, control_stream],
334 shell_streams=[shell_stream, control_stream],
331 iopub_socket=self.iopub_socket,
335 iopub_socket=self.iopub_socket,
332 stdin_socket=self.stdin_socket,
336 stdin_socket=self.stdin_socket,
333 log=self.log,
337 log=self.log,
334 profile_dir=self.profile_dir,
338 profile_dir=self.profile_dir,
335 user_ns=self.user_ns,
339 user_ns=self.user_ns,
336 )
340 )
337 kernel.record_ports(self.ports)
341 kernel.record_ports(self.ports)
338 self.kernel = kernel
342 self.kernel = kernel
339
343
340 def init_gui_pylab(self):
344 def init_gui_pylab(self):
341 """Enable GUI event loop integration, taking pylab into account."""
345 """Enable GUI event loop integration, taking pylab into account."""
342
346
343 # Provide a wrapper for :meth:`InteractiveShellApp.init_gui_pylab`
347 # Provide a wrapper for :meth:`InteractiveShellApp.init_gui_pylab`
344 # to ensure that any exception is printed straight to stderr.
348 # to ensure that any exception is printed straight to stderr.
345 # Normally _showtraceback associates the reply with an execution,
349 # Normally _showtraceback associates the reply with an execution,
346 # which means frontends will never draw it, as this exception
350 # which means frontends will never draw it, as this exception
347 # is not associated with any execute request.
351 # is not associated with any execute request.
348
352
349 shell = self.shell
353 shell = self.shell
350 _showtraceback = shell._showtraceback
354 _showtraceback = shell._showtraceback
351 try:
355 try:
352 # replace pyerr-sending traceback with stderr
356 # replace pyerr-sending traceback with stderr
353 def print_tb(etype, evalue, stb):
357 def print_tb(etype, evalue, stb):
354 print ("GUI event loop or pylab initialization failed",
358 print ("GUI event loop or pylab initialization failed",
355 file=io.stderr)
359 file=io.stderr)
356 print (shell.InteractiveTB.stb2text(stb), file=io.stderr)
360 print (shell.InteractiveTB.stb2text(stb), file=io.stderr)
357 shell._showtraceback = print_tb
361 shell._showtraceback = print_tb
358 InteractiveShellApp.init_gui_pylab(self)
362 InteractiveShellApp.init_gui_pylab(self)
359 finally:
363 finally:
360 shell._showtraceback = _showtraceback
364 shell._showtraceback = _showtraceback
361
365
362 def init_shell(self):
366 def init_shell(self):
363 self.shell = self.kernel.shell
367 self.shell = self.kernel.shell
364 self.shell.configurables.append(self)
368 self.shell.configurables.append(self)
365
369
366 @catch_config_error
370 @catch_config_error
367 def initialize(self, argv=None):
371 def initialize(self, argv=None):
368 super(IPKernelApp, self).initialize(argv)
372 super(IPKernelApp, self).initialize(argv)
369 self.init_blackhole()
373 self.init_blackhole()
370 self.init_connection_file()
374 self.init_connection_file()
371 self.init_session()
375 self.init_session()
372 self.init_poller()
376 self.init_poller()
373 self.init_sockets()
377 self.init_sockets()
374 self.init_heartbeat()
378 self.init_heartbeat()
375 # writing/displaying connection info must be *after* init_sockets/heartbeat
379 # writing/displaying connection info must be *after* init_sockets/heartbeat
376 self.log_connection_info()
380 self.log_connection_info()
377 self.write_connection_file()
381 self.write_connection_file()
378 self.init_io()
382 self.init_io()
379 self.init_signal()
383 self.init_signal()
380 self.init_kernel()
384 self.init_kernel()
381 # shell init steps
385 # shell init steps
382 self.init_path()
386 self.init_path()
383 self.init_shell()
387 self.init_shell()
384 self.init_gui_pylab()
388 self.init_gui_pylab()
385 self.init_extensions()
389 self.init_extensions()
386 self.init_code()
390 self.init_code()
387 # flush stdout/stderr, so that anything written to these streams during
391 # flush stdout/stderr, so that anything written to these streams during
388 # initialization do not get associated with the first execution request
392 # initialization do not get associated with the first execution request
389 sys.stdout.flush()
393 sys.stdout.flush()
390 sys.stderr.flush()
394 sys.stderr.flush()
391
395
392 def start(self):
396 def start(self):
393 if self.poller is not None:
397 if self.poller is not None:
394 self.poller.start()
398 self.poller.start()
395 self.kernel.start()
399 self.kernel.start()
396 try:
400 try:
397 ioloop.IOLoop.instance().start()
401 ioloop.IOLoop.instance().start()
398 except KeyboardInterrupt:
402 except KeyboardInterrupt:
399 pass
403 pass
400
404
401 launch_new_instance = IPKernelApp.launch_instance
405 launch_new_instance = IPKernelApp.launch_instance
402
406
403 def main():
407 def main():
404 """Run an IPKernel as an application"""
408 """Run an IPKernel as an application"""
405 app = IPKernelApp.instance()
409 app = IPKernelApp.instance()
406 app.initialize()
410 app.initialize()
407 app.start()
411 app.start()
408
412
409
413
410 if __name__ == '__main__':
414 if __name__ == '__main__':
411 main()
415 main()
@@ -1,594 +1,625 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """IPython Test Process Controller
2 """IPython Test Process Controller
3
3
4 This module runs one or more subprocesses which will actually run the IPython
4 This module runs one or more subprocesses which will actually run the IPython
5 test suite.
5 test suite.
6
6
7 """
7 """
8
8
9 #-----------------------------------------------------------------------------
9 # Copyright (c) IPython Development Team.
10 # Copyright (C) 2009-2011 The IPython Development Team
10 # Distributed under the terms of the Modified BSD License.
11 #
11
12 # Distributed under the terms of the BSD License. The full license is in
13 # the file COPYING, distributed as part of this software.
14 #-----------------------------------------------------------------------------
15
16 #-----------------------------------------------------------------------------
17 # Imports
18 #-----------------------------------------------------------------------------
19 from __future__ import print_function
12 from __future__ import print_function
20
13
21 import argparse
14 import argparse
22 import json
15 import json
23 import multiprocessing.pool
16 import multiprocessing.pool
24 import os
17 import os
25 import shutil
18 import shutil
26 import signal
19 import signal
27 import sys
20 import sys
28 import subprocess
21 import subprocess
29 import time
22 import time
30
23
31 from .iptest import have, test_group_names as py_test_group_names, test_sections, StreamCapturer
24 from .iptest import have, test_group_names as py_test_group_names, test_sections, StreamCapturer
32 from IPython.utils.path import compress_user
25 from IPython.utils.path import compress_user
33 from IPython.utils.py3compat import bytes_to_str
26 from IPython.utils.py3compat import bytes_to_str
34 from IPython.utils.sysinfo import get_sys_info
27 from IPython.utils.sysinfo import get_sys_info
35 from IPython.utils.tempdir import TemporaryDirectory
28 from IPython.utils.tempdir import TemporaryDirectory
36
29
30 try:
31 # Python >= 3.3
32 from subprocess import TimeoutExpired
33 def popen_wait(p, timeout):
34 return p.wait(timeout)
35 except ImportError:
36 class TimeoutExpired(Exception):
37 pass
38 def popen_wait(p, timeout):
39 """backport of Popen.wait from Python 3"""
40 for i in range(int(10 * timeout)):
41 if p.poll() is not None:
42 return
43 time.sleep(0.1)
44 if p.poll() is None:
45 raise TimeoutExpired
46
47 NOTEBOOK_SHUTDOWN_TIMEOUT = 10
37
48
38 class TestController(object):
49 class TestController(object):
39 """Run tests in a subprocess
50 """Run tests in a subprocess
40 """
51 """
41 #: str, IPython test suite to be executed.
52 #: str, IPython test suite to be executed.
42 section = None
53 section = None
43 #: list, command line arguments to be executed
54 #: list, command line arguments to be executed
44 cmd = None
55 cmd = None
45 #: dict, extra environment variables to set for the subprocess
56 #: dict, extra environment variables to set for the subprocess
46 env = None
57 env = None
47 #: list, TemporaryDirectory instances to clear up when the process finishes
58 #: list, TemporaryDirectory instances to clear up when the process finishes
48 dirs = None
59 dirs = None
49 #: subprocess.Popen instance
60 #: subprocess.Popen instance
50 process = None
61 process = None
51 #: str, process stdout+stderr
62 #: str, process stdout+stderr
52 stdout = None
63 stdout = None
53
64
54 def __init__(self):
65 def __init__(self):
55 self.cmd = []
66 self.cmd = []
56 self.env = {}
67 self.env = {}
57 self.dirs = []
68 self.dirs = []
58
69
59 def setup(self):
70 def setup(self):
60 """Create temporary directories etc.
71 """Create temporary directories etc.
61
72
62 This is only called when we know the test group will be run. Things
73 This is only called when we know the test group will be run. Things
63 created here may be cleaned up by self.cleanup().
74 created here may be cleaned up by self.cleanup().
64 """
75 """
65 pass
76 pass
66
77
67 def launch(self, buffer_output=False):
78 def launch(self, buffer_output=False):
68 # print('*** ENV:', self.env) # dbg
79 # print('*** ENV:', self.env) # dbg
69 # print('*** CMD:', self.cmd) # dbg
80 # print('*** CMD:', self.cmd) # dbg
70 env = os.environ.copy()
81 env = os.environ.copy()
71 env.update(self.env)
82 env.update(self.env)
72 output = subprocess.PIPE if buffer_output else None
83 output = subprocess.PIPE if buffer_output else None
73 stdout = subprocess.STDOUT if buffer_output else None
84 stdout = subprocess.STDOUT if buffer_output else None
74 self.process = subprocess.Popen(self.cmd, stdout=output,
85 self.process = subprocess.Popen(self.cmd, stdout=output,
75 stderr=stdout, env=env)
86 stderr=stdout, env=env)
76
87
77 def wait(self):
88 def wait(self):
78 self.stdout, _ = self.process.communicate()
89 self.stdout, _ = self.process.communicate()
79 return self.process.returncode
90 return self.process.returncode
80
91
81 def print_extra_info(self):
92 def print_extra_info(self):
82 """Print extra information about this test run.
93 """Print extra information about this test run.
83
94
84 If we're running in parallel and showing the concise view, this is only
95 If we're running in parallel and showing the concise view, this is only
85 called if the test group fails. Otherwise, it's called before the test
96 called if the test group fails. Otherwise, it's called before the test
86 group is started.
97 group is started.
87
98
88 The base implementation does nothing, but it can be overridden by
99 The base implementation does nothing, but it can be overridden by
89 subclasses.
100 subclasses.
90 """
101 """
91 return
102 return
92
103
93 def cleanup_process(self):
104 def cleanup_process(self):
94 """Cleanup on exit by killing any leftover processes."""
105 """Cleanup on exit by killing any leftover processes."""
95 subp = self.process
106 subp = self.process
96 if subp is None or (subp.poll() is not None):
107 if subp is None or (subp.poll() is not None):
97 return # Process doesn't exist, or is already dead.
108 return # Process doesn't exist, or is already dead.
98
109
99 try:
110 try:
100 print('Cleaning up stale PID: %d' % subp.pid)
111 print('Cleaning up stale PID: %d' % subp.pid)
101 subp.kill()
112 subp.kill()
102 except: # (OSError, WindowsError) ?
113 except: # (OSError, WindowsError) ?
103 # This is just a best effort, if we fail or the process was
114 # This is just a best effort, if we fail or the process was
104 # really gone, ignore it.
115 # really gone, ignore it.
105 pass
116 pass
106 else:
117 else:
107 for i in range(10):
118 for i in range(10):
108 if subp.poll() is None:
119 if subp.poll() is None:
109 time.sleep(0.1)
120 time.sleep(0.1)
110 else:
121 else:
111 break
122 break
112
123
113 if subp.poll() is None:
124 if subp.poll() is None:
114 # The process did not die...
125 # The process did not die...
115 print('... failed. Manual cleanup may be required.')
126 print('... failed. Manual cleanup may be required.')
116
127
117 def cleanup(self):
128 def cleanup(self):
118 "Kill process if it's still alive, and clean up temporary directories"
129 "Kill process if it's still alive, and clean up temporary directories"
119 self.cleanup_process()
130 self.cleanup_process()
120 for td in self.dirs:
131 for td in self.dirs:
121 td.cleanup()
132 td.cleanup()
122
133
123 __del__ = cleanup
134 __del__ = cleanup
124
135
125 class PyTestController(TestController):
136 class PyTestController(TestController):
126 """Run Python tests using IPython.testing.iptest"""
137 """Run Python tests using IPython.testing.iptest"""
127 #: str, Python command to execute in subprocess
138 #: str, Python command to execute in subprocess
128 pycmd = None
139 pycmd = None
129
140
130 def __init__(self, section, options):
141 def __init__(self, section, options):
131 """Create new test runner."""
142 """Create new test runner."""
132 TestController.__init__(self)
143 TestController.__init__(self)
133 self.section = section
144 self.section = section
134 # pycmd is put into cmd[2] in PyTestController.launch()
145 # pycmd is put into cmd[2] in PyTestController.launch()
135 self.cmd = [sys.executable, '-c', None, section]
146 self.cmd = [sys.executable, '-c', None, section]
136 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
147 self.pycmd = "from IPython.testing.iptest import run_iptest; run_iptest()"
137 self.options = options
148 self.options = options
138
149
139 def setup(self):
150 def setup(self):
140 ipydir = TemporaryDirectory()
151 ipydir = TemporaryDirectory()
141 self.dirs.append(ipydir)
152 self.dirs.append(ipydir)
142 self.env['IPYTHONDIR'] = ipydir.name
153 self.env['IPYTHONDIR'] = ipydir.name
143 self.workingdir = workingdir = TemporaryDirectory()
154 self.workingdir = workingdir = TemporaryDirectory()
144 self.dirs.append(workingdir)
155 self.dirs.append(workingdir)
145 self.env['IPTEST_WORKING_DIR'] = workingdir.name
156 self.env['IPTEST_WORKING_DIR'] = workingdir.name
146 # This means we won't get odd effects from our own matplotlib config
157 # This means we won't get odd effects from our own matplotlib config
147 self.env['MPLCONFIGDIR'] = workingdir.name
158 self.env['MPLCONFIGDIR'] = workingdir.name
148
159
149 # From options:
160 # From options:
150 if self.options.xunit:
161 if self.options.xunit:
151 self.add_xunit()
162 self.add_xunit()
152 if self.options.coverage:
163 if self.options.coverage:
153 self.add_coverage()
164 self.add_coverage()
154 self.env['IPTEST_SUBPROC_STREAMS'] = self.options.subproc_streams
165 self.env['IPTEST_SUBPROC_STREAMS'] = self.options.subproc_streams
155 self.cmd.extend(self.options.extra_args)
166 self.cmd.extend(self.options.extra_args)
156
167
157 @property
168 @property
158 def will_run(self):
169 def will_run(self):
159 try:
170 try:
160 return test_sections[self.section].will_run
171 return test_sections[self.section].will_run
161 except KeyError:
172 except KeyError:
162 return True
173 return True
163
174
164 def add_xunit(self):
175 def add_xunit(self):
165 xunit_file = os.path.abspath(self.section + '.xunit.xml')
176 xunit_file = os.path.abspath(self.section + '.xunit.xml')
166 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
177 self.cmd.extend(['--with-xunit', '--xunit-file', xunit_file])
167
178
168 def add_coverage(self):
179 def add_coverage(self):
169 try:
180 try:
170 sources = test_sections[self.section].includes
181 sources = test_sections[self.section].includes
171 except KeyError:
182 except KeyError:
172 sources = ['IPython']
183 sources = ['IPython']
173
184
174 coverage_rc = ("[run]\n"
185 coverage_rc = ("[run]\n"
175 "data_file = {data_file}\n"
186 "data_file = {data_file}\n"
176 "source =\n"
187 "source =\n"
177 " {source}\n"
188 " {source}\n"
178 ).format(data_file=os.path.abspath('.coverage.'+self.section),
189 ).format(data_file=os.path.abspath('.coverage.'+self.section),
179 source="\n ".join(sources))
190 source="\n ".join(sources))
180 config_file = os.path.join(self.workingdir.name, '.coveragerc')
191 config_file = os.path.join(self.workingdir.name, '.coveragerc')
181 with open(config_file, 'w') as f:
192 with open(config_file, 'w') as f:
182 f.write(coverage_rc)
193 f.write(coverage_rc)
183
194
184 self.env['COVERAGE_PROCESS_START'] = config_file
195 self.env['COVERAGE_PROCESS_START'] = config_file
185 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
196 self.pycmd = "import coverage; coverage.process_startup(); " + self.pycmd
186
197
187 def launch(self, buffer_output=False):
198 def launch(self, buffer_output=False):
188 self.cmd[2] = self.pycmd
199 self.cmd[2] = self.pycmd
189 super(PyTestController, self).launch(buffer_output=buffer_output)
200 super(PyTestController, self).launch(buffer_output=buffer_output)
190
201
191 js_prefix = 'js/'
202 js_prefix = 'js/'
192
203
193 def get_js_test_dir():
204 def get_js_test_dir():
194 import IPython.html.tests as t
205 import IPython.html.tests as t
195 return os.path.join(os.path.dirname(t.__file__), '')
206 return os.path.join(os.path.dirname(t.__file__), '')
196
207
197 def all_js_groups():
208 def all_js_groups():
198 import glob
209 import glob
199 test_dir = get_js_test_dir()
210 test_dir = get_js_test_dir()
200 all_subdirs = glob.glob(test_dir + '*/')
211 all_subdirs = glob.glob(test_dir + '*/')
201 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs if os.path.relpath(x, test_dir) != '__pycache__']
212 return [js_prefix+os.path.relpath(x, test_dir) for x in all_subdirs if os.path.relpath(x, test_dir) != '__pycache__']
202
213
203 class JSController(TestController):
214 class JSController(TestController):
204 """Run CasperJS tests """
215 """Run CasperJS tests """
205 def __init__(self, section):
216 def __init__(self, section):
206 """Create new test runner."""
217 """Create new test runner."""
207 TestController.__init__(self)
218 TestController.__init__(self)
208 self.section = section
219 self.section = section
209 js_test_dir = get_js_test_dir()
220 js_test_dir = get_js_test_dir()
210 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
221 includes = '--includes=' + os.path.join(js_test_dir,'util.js')
211 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
222 test_cases = os.path.join(js_test_dir, self.section[len(js_prefix):])
212 self.cmd = ['casperjs', 'test', includes, test_cases]
223 self.cmd = ['casperjs', 'test', includes, test_cases]
213
224
214 def setup(self):
225 def setup(self):
215 self.ipydir = TemporaryDirectory()
226 self.ipydir = TemporaryDirectory()
216 self.nbdir = TemporaryDirectory()
227 self.nbdir = TemporaryDirectory()
217 self.dirs.append(self.ipydir)
228 self.dirs.append(self.ipydir)
218 self.dirs.append(self.nbdir)
229 self.dirs.append(self.nbdir)
219 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
230 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir1', u'sub βˆ‚ir 1a')))
220 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
231 os.makedirs(os.path.join(self.nbdir.name, os.path.join(u'sub βˆ‚ir2', u'sub βˆ‚ir 1b')))
221
232
222 # start the ipython notebook, so we get the port number
233 # start the ipython notebook, so we get the port number
223 self.server_port = 0
234 self.server_port = 0
224 self._init_server()
235 self._init_server()
225 if self.server_port:
236 if self.server_port:
226 self.cmd.append("--port=%i" % self.server_port)
237 self.cmd.append("--port=%i" % self.server_port)
227 else:
238 else:
228 # don't launch tests if the server didn't start
239 # don't launch tests if the server didn't start
229 self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
240 self.cmd = [sys.executable, '-c', 'raise SystemExit(1)']
230
241
231 def print_extra_info(self):
242 def print_extra_info(self):
232 print("Running tests with notebook directory %r" % self.nbdir.name)
243 print("Running tests with notebook directory %r" % self.nbdir.name)
233
244
234 @property
245 @property
235 def will_run(self):
246 def will_run(self):
236 return all(have[a] for a in ['zmq', 'tornado', 'jinja2', 'casperjs', 'sqlite3'])
247 return all(have[a] for a in ['zmq', 'tornado', 'jinja2', 'casperjs', 'sqlite3'])
237
248
238 def _init_server(self):
249 def _init_server(self):
239 "Start the notebook server in a separate process"
250 "Start the notebook server in a separate process"
240 self.server_command = command = [sys.executable,
251 self.server_command = command = [sys.executable,
241 '-m', 'IPython.html',
252 '-m', 'IPython.html',
242 '--no-browser',
253 '--no-browser',
243 '--ipython-dir', self.ipydir.name,
254 '--ipython-dir', self.ipydir.name,
244 '--notebook-dir', self.nbdir.name,
255 '--notebook-dir', self.nbdir.name,
245 ]
256 ]
246 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
257 # ipc doesn't work on Windows, and darwin has crazy-long temp paths,
247 # which run afoul of ipc's maximum path length.
258 # which run afoul of ipc's maximum path length.
248 if sys.platform.startswith('linux'):
259 if sys.platform.startswith('linux'):
249 command.append('--KernelManager.transport=ipc')
260 command.append('--KernelManager.transport=ipc')
250 self.stream_capturer = c = StreamCapturer()
261 self.stream_capturer = c = StreamCapturer()
251 c.start()
262 c.start()
252 self.server = subprocess.Popen(command, stdout=c.writefd, stderr=subprocess.STDOUT)
263 self.server = subprocess.Popen(command, stdout=c.writefd, stderr=subprocess.STDOUT)
253 self.server_info_file = os.path.join(self.ipydir.name,
264 self.server_info_file = os.path.join(self.ipydir.name,
254 'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
265 'profile_default', 'security', 'nbserver-%i.json' % self.server.pid
255 )
266 )
256 self._wait_for_server()
267 self._wait_for_server()
257
268
258 def _wait_for_server(self):
269 def _wait_for_server(self):
259 """Wait 30 seconds for the notebook server to start"""
270 """Wait 30 seconds for the notebook server to start"""
260 for i in range(300):
271 for i in range(300):
261 if self.server.poll() is not None:
272 if self.server.poll() is not None:
262 return self._failed_to_start()
273 return self._failed_to_start()
263 if os.path.exists(self.server_info_file):
274 if os.path.exists(self.server_info_file):
264 self._load_server_info()
275 self._load_server_info()
265 return
276 return
266 time.sleep(0.1)
277 time.sleep(0.1)
267 print("Notebook server-info file never arrived: %s" % self.server_info_file,
278 print("Notebook server-info file never arrived: %s" % self.server_info_file,
268 file=sys.stderr
279 file=sys.stderr
269 )
280 )
270
281
271 def _failed_to_start(self):
282 def _failed_to_start(self):
272 """Notebook server exited prematurely"""
283 """Notebook server exited prematurely"""
273 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
284 captured = self.stream_capturer.get_buffer().decode('utf-8', 'replace')
274 print("Notebook failed to start: ", file=sys.stderr)
285 print("Notebook failed to start: ", file=sys.stderr)
275 print(self.server_command)
286 print(self.server_command)
276 print(captured, file=sys.stderr)
287 print(captured, file=sys.stderr)
277
288
278 def _load_server_info(self):
289 def _load_server_info(self):
279 """Notebook server started, load connection info from JSON"""
290 """Notebook server started, load connection info from JSON"""
280 with open(self.server_info_file) as f:
291 with open(self.server_info_file) as f:
281 info = json.load(f)
292 info = json.load(f)
282 self.server_port = info['port']
293 self.server_port = info['port']
283
294
284 def cleanup(self):
295 def cleanup(self):
285 try:
296 try:
286 self.server.terminate()
297 self.server.terminate()
287 except OSError:
298 except OSError:
288 # already dead
299 # already dead
289 pass
300 pass
290 self.server.wait()
301 # wait 10s for the server to shutdown
302 try:
303 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
304 except TimeoutExpired:
305 # server didn't terminate, kill it
306 try:
307 print("Failed to terminate notebook server, killing it.",
308 file=sys.stderr
309 )
310 self.server.kill()
311 except OSError:
312 # already dead
313 pass
314 # wait another 10s
315 try:
316 popen_wait(self.server, NOTEBOOK_SHUTDOWN_TIMEOUT)
317 except TimeoutExpired:
318 print("Notebook server still running (%s)" % self.server_info_file,
319 file=sys.stderr
320 )
321
291 self.stream_capturer.halt()
322 self.stream_capturer.halt()
292 TestController.cleanup(self)
323 TestController.cleanup(self)
293
324
294
325
295 def prepare_controllers(options):
326 def prepare_controllers(options):
296 """Returns two lists of TestController instances, those to run, and those
327 """Returns two lists of TestController instances, those to run, and those
297 not to run."""
328 not to run."""
298 testgroups = options.testgroups
329 testgroups = options.testgroups
299
330
300 if testgroups:
331 if testgroups:
301 py_testgroups = [g for g in testgroups if (g in py_test_group_names) \
332 py_testgroups = [g for g in testgroups if (g in py_test_group_names) \
302 or g.startswith('IPython.')]
333 or g.startswith('IPython.')]
303 if 'js' in testgroups:
334 if 'js' in testgroups:
304 js_testgroups = all_js_groups()
335 js_testgroups = all_js_groups()
305 else:
336 else:
306 js_testgroups = [g for g in testgroups if g not in py_testgroups]
337 js_testgroups = [g for g in testgroups if g not in py_testgroups]
307 else:
338 else:
308 py_testgroups = py_test_group_names
339 py_testgroups = py_test_group_names
309 if not options.all:
340 if not options.all:
310 js_testgroups = []
341 js_testgroups = []
311 test_sections['parallel'].enabled = False
342 test_sections['parallel'].enabled = False
312 else:
343 else:
313 js_testgroups = all_js_groups()
344 js_testgroups = all_js_groups()
314
345
315 c_js = [JSController(name) for name in js_testgroups]
346 c_js = [JSController(name) for name in js_testgroups]
316 c_py = [PyTestController(name, options) for name in py_testgroups]
347 c_py = [PyTestController(name, options) for name in py_testgroups]
317
348
318 controllers = c_py + c_js
349 controllers = c_py + c_js
319 to_run = [c for c in controllers if c.will_run]
350 to_run = [c for c in controllers if c.will_run]
320 not_run = [c for c in controllers if not c.will_run]
351 not_run = [c for c in controllers if not c.will_run]
321 return to_run, not_run
352 return to_run, not_run
322
353
323 def do_run(controller, buffer_output=True):
354 def do_run(controller, buffer_output=True):
324 """Setup and run a test controller.
355 """Setup and run a test controller.
325
356
326 If buffer_output is True, no output is displayed, to avoid it appearing
357 If buffer_output is True, no output is displayed, to avoid it appearing
327 interleaved. In this case, the caller is responsible for displaying test
358 interleaved. In this case, the caller is responsible for displaying test
328 output on failure.
359 output on failure.
329
360
330 Returns
361 Returns
331 -------
362 -------
332 controller : TestController
363 controller : TestController
333 The same controller as passed in, as a convenience for using map() type
364 The same controller as passed in, as a convenience for using map() type
334 APIs.
365 APIs.
335 exitcode : int
366 exitcode : int
336 The exit code of the test subprocess. Non-zero indicates failure.
367 The exit code of the test subprocess. Non-zero indicates failure.
337 """
368 """
338 try:
369 try:
339 try:
370 try:
340 controller.setup()
371 controller.setup()
341 if not buffer_output:
372 if not buffer_output:
342 controller.print_extra_info()
373 controller.print_extra_info()
343 controller.launch(buffer_output=buffer_output)
374 controller.launch(buffer_output=buffer_output)
344 except Exception:
375 except Exception:
345 import traceback
376 import traceback
346 traceback.print_exc()
377 traceback.print_exc()
347 return controller, 1 # signal failure
378 return controller, 1 # signal failure
348
379
349 exitcode = controller.wait()
380 exitcode = controller.wait()
350 return controller, exitcode
381 return controller, exitcode
351
382
352 except KeyboardInterrupt:
383 except KeyboardInterrupt:
353 return controller, -signal.SIGINT
384 return controller, -signal.SIGINT
354 finally:
385 finally:
355 controller.cleanup()
386 controller.cleanup()
356
387
357 def report():
388 def report():
358 """Return a string with a summary report of test-related variables."""
389 """Return a string with a summary report of test-related variables."""
359 inf = get_sys_info()
390 inf = get_sys_info()
360 out = []
391 out = []
361 def _add(name, value):
392 def _add(name, value):
362 out.append((name, value))
393 out.append((name, value))
363
394
364 _add('IPython version', inf['ipython_version'])
395 _add('IPython version', inf['ipython_version'])
365 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
396 _add('IPython commit', "{} ({})".format(inf['commit_hash'], inf['commit_source']))
366 _add('IPython package', compress_user(inf['ipython_path']))
397 _add('IPython package', compress_user(inf['ipython_path']))
367 _add('Python version', inf['sys_version'].replace('\n',''))
398 _add('Python version', inf['sys_version'].replace('\n',''))
368 _add('sys.executable', compress_user(inf['sys_executable']))
399 _add('sys.executable', compress_user(inf['sys_executable']))
369 _add('Platform', inf['platform'])
400 _add('Platform', inf['platform'])
370
401
371 width = max(len(n) for (n,v) in out)
402 width = max(len(n) for (n,v) in out)
372 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
403 out = ["{:<{width}}: {}\n".format(n, v, width=width) for (n,v) in out]
373
404
374 avail = []
405 avail = []
375 not_avail = []
406 not_avail = []
376
407
377 for k, is_avail in have.items():
408 for k, is_avail in have.items():
378 if is_avail:
409 if is_avail:
379 avail.append(k)
410 avail.append(k)
380 else:
411 else:
381 not_avail.append(k)
412 not_avail.append(k)
382
413
383 if avail:
414 if avail:
384 out.append('\nTools and libraries available at test time:\n')
415 out.append('\nTools and libraries available at test time:\n')
385 avail.sort()
416 avail.sort()
386 out.append(' ' + ' '.join(avail)+'\n')
417 out.append(' ' + ' '.join(avail)+'\n')
387
418
388 if not_avail:
419 if not_avail:
389 out.append('\nTools and libraries NOT available at test time:\n')
420 out.append('\nTools and libraries NOT available at test time:\n')
390 not_avail.sort()
421 not_avail.sort()
391 out.append(' ' + ' '.join(not_avail)+'\n')
422 out.append(' ' + ' '.join(not_avail)+'\n')
392
423
393 return ''.join(out)
424 return ''.join(out)
394
425
395 def run_iptestall(options):
426 def run_iptestall(options):
396 """Run the entire IPython test suite by calling nose and trial.
427 """Run the entire IPython test suite by calling nose and trial.
397
428
398 This function constructs :class:`IPTester` instances for all IPython
429 This function constructs :class:`IPTester` instances for all IPython
399 modules and package and then runs each of them. This causes the modules
430 modules and package and then runs each of them. This causes the modules
400 and packages of IPython to be tested each in their own subprocess using
431 and packages of IPython to be tested each in their own subprocess using
401 nose.
432 nose.
402
433
403 Parameters
434 Parameters
404 ----------
435 ----------
405
436
406 All parameters are passed as attributes of the options object.
437 All parameters are passed as attributes of the options object.
407
438
408 testgroups : list of str
439 testgroups : list of str
409 Run only these sections of the test suite. If empty, run all the available
440 Run only these sections of the test suite. If empty, run all the available
410 sections.
441 sections.
411
442
412 fast : int or None
443 fast : int or None
413 Run the test suite in parallel, using n simultaneous processes. If None
444 Run the test suite in parallel, using n simultaneous processes. If None
414 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
445 is passed, one process is used per CPU core. Default 1 (i.e. sequential)
415
446
416 inc_slow : bool
447 inc_slow : bool
417 Include slow tests, like IPython.parallel. By default, these tests aren't
448 Include slow tests, like IPython.parallel. By default, these tests aren't
418 run.
449 run.
419
450
420 xunit : bool
451 xunit : bool
421 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
452 Produce Xunit XML output. This is written to multiple foo.xunit.xml files.
422
453
423 coverage : bool or str
454 coverage : bool or str
424 Measure code coverage from tests. True will store the raw coverage data,
455 Measure code coverage from tests. True will store the raw coverage data,
425 or pass 'html' or 'xml' to get reports.
456 or pass 'html' or 'xml' to get reports.
426
457
427 extra_args : list
458 extra_args : list
428 Extra arguments to pass to the test subprocesses, e.g. '-v'
459 Extra arguments to pass to the test subprocesses, e.g. '-v'
429 """
460 """
430 to_run, not_run = prepare_controllers(options)
461 to_run, not_run = prepare_controllers(options)
431
462
432 def justify(ltext, rtext, width=70, fill='-'):
463 def justify(ltext, rtext, width=70, fill='-'):
433 ltext += ' '
464 ltext += ' '
434 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
465 rtext = (' ' + rtext).rjust(width - len(ltext), fill)
435 return ltext + rtext
466 return ltext + rtext
436
467
437 # Run all test runners, tracking execution time
468 # Run all test runners, tracking execution time
438 failed = []
469 failed = []
439 t_start = time.time()
470 t_start = time.time()
440
471
441 print()
472 print()
442 if options.fast == 1:
473 if options.fast == 1:
443 # This actually means sequential, i.e. with 1 job
474 # This actually means sequential, i.e. with 1 job
444 for controller in to_run:
475 for controller in to_run:
445 print('Test group:', controller.section)
476 print('Test group:', controller.section)
446 sys.stdout.flush() # Show in correct order when output is piped
477 sys.stdout.flush() # Show in correct order when output is piped
447 controller, res = do_run(controller, buffer_output=False)
478 controller, res = do_run(controller, buffer_output=False)
448 if res:
479 if res:
449 failed.append(controller)
480 failed.append(controller)
450 if res == -signal.SIGINT:
481 if res == -signal.SIGINT:
451 print("Interrupted")
482 print("Interrupted")
452 break
483 break
453 print()
484 print()
454
485
455 else:
486 else:
456 # Run tests concurrently
487 # Run tests concurrently
457 try:
488 try:
458 pool = multiprocessing.pool.ThreadPool(options.fast)
489 pool = multiprocessing.pool.ThreadPool(options.fast)
459 for (controller, res) in pool.imap_unordered(do_run, to_run):
490 for (controller, res) in pool.imap_unordered(do_run, to_run):
460 res_string = 'OK' if res == 0 else 'FAILED'
491 res_string = 'OK' if res == 0 else 'FAILED'
461 print(justify('Test group: ' + controller.section, res_string))
492 print(justify('Test group: ' + controller.section, res_string))
462 if res:
493 if res:
463 controller.print_extra_info()
494 controller.print_extra_info()
464 print(bytes_to_str(controller.stdout))
495 print(bytes_to_str(controller.stdout))
465 failed.append(controller)
496 failed.append(controller)
466 if res == -signal.SIGINT:
497 if res == -signal.SIGINT:
467 print("Interrupted")
498 print("Interrupted")
468 break
499 break
469 except KeyboardInterrupt:
500 except KeyboardInterrupt:
470 return
501 return
471
502
472 for controller in not_run:
503 for controller in not_run:
473 print(justify('Test group: ' + controller.section, 'NOT RUN'))
504 print(justify('Test group: ' + controller.section, 'NOT RUN'))
474
505
475 t_end = time.time()
506 t_end = time.time()
476 t_tests = t_end - t_start
507 t_tests = t_end - t_start
477 nrunners = len(to_run)
508 nrunners = len(to_run)
478 nfail = len(failed)
509 nfail = len(failed)
479 # summarize results
510 # summarize results
480 print('_'*70)
511 print('_'*70)
481 print('Test suite completed for system with the following information:')
512 print('Test suite completed for system with the following information:')
482 print(report())
513 print(report())
483 took = "Took %.3fs." % t_tests
514 took = "Took %.3fs." % t_tests
484 print('Status: ', end='')
515 print('Status: ', end='')
485 if not failed:
516 if not failed:
486 print('OK (%d test groups).' % nrunners, took)
517 print('OK (%d test groups).' % nrunners, took)
487 else:
518 else:
488 # If anything went wrong, point out what command to rerun manually to
519 # If anything went wrong, point out what command to rerun manually to
489 # see the actual errors and individual summary
520 # see the actual errors and individual summary
490 failed_sections = [c.section for c in failed]
521 failed_sections = [c.section for c in failed]
491 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
522 print('ERROR - {} out of {} test groups failed ({}).'.format(nfail,
492 nrunners, ', '.join(failed_sections)), took)
523 nrunners, ', '.join(failed_sections)), took)
493 print()
524 print()
494 print('You may wish to rerun these, with:')
525 print('You may wish to rerun these, with:')
495 print(' iptest', *failed_sections)
526 print(' iptest', *failed_sections)
496 print()
527 print()
497
528
498 if options.coverage:
529 if options.coverage:
499 from coverage import coverage
530 from coverage import coverage
500 cov = coverage(data_file='.coverage')
531 cov = coverage(data_file='.coverage')
501 cov.combine()
532 cov.combine()
502 cov.save()
533 cov.save()
503
534
504 # Coverage HTML report
535 # Coverage HTML report
505 if options.coverage == 'html':
536 if options.coverage == 'html':
506 html_dir = 'ipy_htmlcov'
537 html_dir = 'ipy_htmlcov'
507 shutil.rmtree(html_dir, ignore_errors=True)
538 shutil.rmtree(html_dir, ignore_errors=True)
508 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
539 print("Writing HTML coverage report to %s/ ... " % html_dir, end="")
509 sys.stdout.flush()
540 sys.stdout.flush()
510
541
511 # Custom HTML reporter to clean up module names.
542 # Custom HTML reporter to clean up module names.
512 from coverage.html import HtmlReporter
543 from coverage.html import HtmlReporter
513 class CustomHtmlReporter(HtmlReporter):
544 class CustomHtmlReporter(HtmlReporter):
514 def find_code_units(self, morfs):
545 def find_code_units(self, morfs):
515 super(CustomHtmlReporter, self).find_code_units(morfs)
546 super(CustomHtmlReporter, self).find_code_units(morfs)
516 for cu in self.code_units:
547 for cu in self.code_units:
517 nameparts = cu.name.split(os.sep)
548 nameparts = cu.name.split(os.sep)
518 if 'IPython' not in nameparts:
549 if 'IPython' not in nameparts:
519 continue
550 continue
520 ix = nameparts.index('IPython')
551 ix = nameparts.index('IPython')
521 cu.name = '.'.join(nameparts[ix:])
552 cu.name = '.'.join(nameparts[ix:])
522
553
523 # Reimplement the html_report method with our custom reporter
554 # Reimplement the html_report method with our custom reporter
524 cov._harvest_data()
555 cov._harvest_data()
525 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
556 cov.config.from_args(omit='*{0}tests{0}*'.format(os.sep), html_dir=html_dir,
526 html_title='IPython test coverage',
557 html_title='IPython test coverage',
527 )
558 )
528 reporter = CustomHtmlReporter(cov, cov.config)
559 reporter = CustomHtmlReporter(cov, cov.config)
529 reporter.report(None)
560 reporter.report(None)
530 print('done.')
561 print('done.')
531
562
532 # Coverage XML report
563 # Coverage XML report
533 elif options.coverage == 'xml':
564 elif options.coverage == 'xml':
534 cov.xml_report(outfile='ipy_coverage.xml')
565 cov.xml_report(outfile='ipy_coverage.xml')
535
566
536 if failed:
567 if failed:
537 # Ensure that our exit code indicates failure
568 # Ensure that our exit code indicates failure
538 sys.exit(1)
569 sys.exit(1)
539
570
540 argparser = argparse.ArgumentParser(description='Run IPython test suite')
571 argparser = argparse.ArgumentParser(description='Run IPython test suite')
541 argparser.add_argument('testgroups', nargs='*',
572 argparser.add_argument('testgroups', nargs='*',
542 help='Run specified groups of tests. If omitted, run '
573 help='Run specified groups of tests. If omitted, run '
543 'all tests.')
574 'all tests.')
544 argparser.add_argument('--all', action='store_true',
575 argparser.add_argument('--all', action='store_true',
545 help='Include slow tests not run by default.')
576 help='Include slow tests not run by default.')
546 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
577 argparser.add_argument('-j', '--fast', nargs='?', const=None, default=1, type=int,
547 help='Run test sections in parallel. This starts as many '
578 help='Run test sections in parallel. This starts as many '
548 'processes as you have cores, or you can specify a number.')
579 'processes as you have cores, or you can specify a number.')
549 argparser.add_argument('--xunit', action='store_true',
580 argparser.add_argument('--xunit', action='store_true',
550 help='Produce Xunit XML results')
581 help='Produce Xunit XML results')
551 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
582 argparser.add_argument('--coverage', nargs='?', const=True, default=False,
552 help="Measure test coverage. Specify 'html' or "
583 help="Measure test coverage. Specify 'html' or "
553 "'xml' to get reports.")
584 "'xml' to get reports.")
554 argparser.add_argument('--subproc-streams', default='capture',
585 argparser.add_argument('--subproc-streams', default='capture',
555 help="What to do with stdout/stderr from subprocesses. "
586 help="What to do with stdout/stderr from subprocesses. "
556 "'capture' (default), 'show' and 'discard' are the options.")
587 "'capture' (default), 'show' and 'discard' are the options.")
557
588
558 def default_options():
589 def default_options():
559 """Get an argparse Namespace object with the default arguments, to pass to
590 """Get an argparse Namespace object with the default arguments, to pass to
560 :func:`run_iptestall`.
591 :func:`run_iptestall`.
561 """
592 """
562 options = argparser.parse_args([])
593 options = argparser.parse_args([])
563 options.extra_args = []
594 options.extra_args = []
564 return options
595 return options
565
596
566 def main():
597 def main():
567 # iptest doesn't work correctly if the working directory is the
598 # iptest doesn't work correctly if the working directory is the
568 # root of the IPython source tree. Tell the user to avoid
599 # root of the IPython source tree. Tell the user to avoid
569 # frustration.
600 # frustration.
570 if os.path.exists(os.path.join(os.getcwd(),
601 if os.path.exists(os.path.join(os.getcwd(),
571 'IPython', 'testing', '__main__.py')):
602 'IPython', 'testing', '__main__.py')):
572 print("Don't run iptest from the IPython source directory",
603 print("Don't run iptest from the IPython source directory",
573 file=sys.stderr)
604 file=sys.stderr)
574 sys.exit(1)
605 sys.exit(1)
575 # Arguments after -- should be passed through to nose. Argparse treats
606 # Arguments after -- should be passed through to nose. Argparse treats
576 # everything after -- as regular positional arguments, so we separate them
607 # everything after -- as regular positional arguments, so we separate them
577 # first.
608 # first.
578 try:
609 try:
579 ix = sys.argv.index('--')
610 ix = sys.argv.index('--')
580 except ValueError:
611 except ValueError:
581 to_parse = sys.argv[1:]
612 to_parse = sys.argv[1:]
582 extra_args = []
613 extra_args = []
583 else:
614 else:
584 to_parse = sys.argv[1:ix]
615 to_parse = sys.argv[1:ix]
585 extra_args = sys.argv[ix+1:]
616 extra_args = sys.argv[ix+1:]
586
617
587 options = argparser.parse_args(to_parse)
618 options = argparser.parse_args(to_parse)
588 options.extra_args = extra_args
619 options.extra_args = extra_args
589
620
590 run_iptestall(options)
621 run_iptestall(options)
591
622
592
623
593 if __name__ == '__main__':
624 if __name__ == '__main__':
594 main()
625 main()
General Comments 0
You need to be logged in to leave comments. Login now