##// END OF EJS Templates
WebSocket url is now passed to browser when a kernel is started.
Brian E. Granger -
Show More
@@ -1,297 +1,326 b''
1 1 """Tornado handlers for the notebook."""
2 2
3 3 #-----------------------------------------------------------------------------
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 7
8 8 from tornado import web
9 9 from tornado import websocket
10 10
11 11 from zmq.eventloop import ioloop
12 12 from zmq.utils import jsonapi
13 13
14 14 from IPython.zmq.session import Session
15 15
16 16 try:
17 17 from docutils.core import publish_string
18 18 except ImportError:
19 19 publish_string = None
20 20
21 21
22 22
23 23 #-----------------------------------------------------------------------------
24 24 # Top-level handlers
25 25 #-----------------------------------------------------------------------------
26 26
27 27
28 28 class NBBrowserHandler(web.RequestHandler):
29 29 def get(self):
30 30 nbm = self.application.notebook_manager
31 31 project = nbm.notebook_dir
32 32 self.render('nbbrowser.html', project=project)
33 33
34 34
35 35 class NewHandler(web.RequestHandler):
36 36 def get(self):
37 37 notebook_id = self.application.notebook_manager.new_notebook()
38 38 self.render('notebook.html', notebook_id=notebook_id)
39 39
40 40
41 41 class NamedNotebookHandler(web.RequestHandler):
42 42 def get(self, notebook_id):
43 43 nbm = self.application.notebook_manager
44 44 if not nbm.notebook_exists(notebook_id):
45 45 raise web.HTTPError(404)
46 46 self.render('notebook.html', notebook_id=notebook_id)
47 47
48 48
49 49 #-----------------------------------------------------------------------------
50 50 # Kernel handlers
51 51 #-----------------------------------------------------------------------------
52 52
53 53
54 54 class MainKernelHandler(web.RequestHandler):
55 55
56 56 def get(self):
57 57 km = self.application.kernel_manager
58 58 self.finish(jsonapi.dumps(km.kernel_ids))
59 59
60 60 def post(self):
61 61 km = self.application.kernel_manager
62 62 notebook_id = self.get_argument('notebook', default=None)
63 63 kernel_id = km.start_kernel(notebook_id)
64 ws_url = self.application.ipython_app.get_ws_url()
65 data = {'ws_url':ws_url,'kernel_id':kernel_id}
64 66 self.set_header('Location', '/'+kernel_id)
65 self.finish(jsonapi.dumps(kernel_id))
67 self.finish(jsonapi.dumps(data))
66 68
67 69
68 70 class KernelHandler(web.RequestHandler):
69 71
70 72 SUPPORTED_METHODS = ('DELETE')
71 73
72 74 def delete(self, kernel_id):
73 75 km = self.application.kernel_manager
74 76 km.kill_kernel(kernel_id)
75 77 self.set_status(204)
76 78 self.finish()
77 79
78 80
79 81 class KernelActionHandler(web.RequestHandler):
80 82
81 83 def post(self, kernel_id, action):
82 84 km = self.application.kernel_manager
83 85 if action == 'interrupt':
84 86 km.interrupt_kernel(kernel_id)
85 87 self.set_status(204)
86 88 if action == 'restart':
87 89 new_kernel_id = km.restart_kernel(kernel_id)
88 self.write(jsonapi.dumps(new_kernel_id))
90 ws_url = self.application.ipython_app.get_ws_url()
91 data = {'ws_url':ws_url,'kernel_id':new_kernel_id}
92 self.set_header('Location', '/'+new_kernel_id)
93 self.write(jsonapi.dumps(data))
89 94 self.finish()
90 95
91 96
92 97 class ZMQStreamHandler(websocket.WebSocketHandler):
93 98
94 99 def _reserialize_reply(self, msg_list):
95 100 """Reserialize a reply message using JSON.
96 101
97 102 This takes the msg list from the ZMQ socket, unserializes it using
98 103 self.session and then serializes the result using JSON. This method
99 104 should be used by self._on_zmq_reply to build messages that can
100 105 be sent back to the browser.
101 106 """
102 107 idents, msg_list = self.session.feed_identities(msg_list)
103 108 msg = self.session.unserialize(msg_list)
104 109 try:
105 110 msg['header'].pop('date')
106 111 except KeyError:
107 112 pass
108 113 try:
109 114 msg['parent_header'].pop('date')
110 115 except KeyError:
111 116 pass
112 117 msg.pop('buffers')
113 118 return jsonapi.dumps(msg)
114 119
115 120 def _on_zmq_reply(self, msg_list):
116 121 try:
117 122 msg = self._reserialize_reply(msg_list)
118 123 except:
119 124 self.application.kernel_manager.log.critical("Malformed message: %r" % msg_list)
120 125 else:
121 126 self.write_message(msg)
122 127
123 128
124 129 class IOPubHandler(ZMQStreamHandler):
125 130
126 131 def initialize(self, *args, **kwargs):
127 132 self._kernel_alive = True
128 133 self._beating = False
134 self.iopub_stream = None
135 self.hb_stream = None
129 136
130 137 def open(self, kernel_id):
131 138 km = self.application.kernel_manager
132 139 self.kernel_id = kernel_id
133 140 self.session = Session()
134 141 self.time_to_dead = km.time_to_dead
142 try:
135 143 self.iopub_stream = km.create_iopub_stream(kernel_id)
136 144 self.hb_stream = km.create_hb_stream(kernel_id)
145 except web.HTTPError:
146 # WebSockets don't response to traditional error codes so we
147 # close the connection.
148 if not self.stream.closed():
149 self.stream.close()
150 else:
137 151 self.iopub_stream.on_recv(self._on_zmq_reply)
138 152 self.start_hb(self.kernel_died)
139 153
140 154 def on_close(self):
155 # This method can be called twice, once by self.kernel_died and once
156 # from the WebSocket close event. If the WebSocket connection is
157 # closed before the ZMQ streams are setup, they could be None.
141 158 self.stop_hb()
159 if self.iopub_stream is not None and not self.iopub_stream.closed():
160 self.iopub_stream.on_recv(None)
142 161 self.iopub_stream.close()
162 if self.hb_stream is not None and not self.hb_stream.closed():
143 163 self.hb_stream.close()
144 164
145 165 def start_hb(self, callback):
146 166 """Start the heartbeating and call the callback if the kernel dies."""
147 167 if not self._beating:
148 168 self._kernel_alive = True
149 169
150 170 def ping_or_dead():
151 171 if self._kernel_alive:
152 172 self._kernel_alive = False
153 173 self.hb_stream.send(b'ping')
154 174 else:
155 175 try:
156 176 callback()
157 177 except:
158 178 pass
159 179 finally:
160 180 self._hb_periodic_callback.stop()
161 181
162 182 def beat_received(msg):
163 183 self._kernel_alive = True
164 184
165 185 self.hb_stream.on_recv(beat_received)
166 186 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000)
167 187 self._hb_periodic_callback.start()
168 188 self._beating= True
169 189
170 190 def stop_hb(self):
171 191 """Stop the heartbeating and cancel all related callbacks."""
172 192 if self._beating:
173 193 self._hb_periodic_callback.stop()
174 194 if not self.hb_stream.closed():
175 195 self.hb_stream.on_recv(None)
176 196
177 197 def kernel_died(self):
178 198 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
179 199 self.write_message(
180 200 {'header': {'msg_type': 'status'},
181 201 'parent_header': {},
182 202 'content': {'execution_state':'dead'}
183 203 }
184 204 )
185 205 self.on_close()
186 206
187 207
188 208 class ShellHandler(ZMQStreamHandler):
189 209
190 210 def initialize(self, *args, **kwargs):
191 pass
211 self.shell_stream = None
192 212
193 213 def open(self, kernel_id):
194 214 km = self.application.kernel_manager
195 215 self.max_msg_size = km.max_msg_size
196 216 self.kernel_id = kernel_id
217 try:
218 self.shell_stream = km.create_shell_stream(kernel_id)
219 except web.HTTPError:
220 # WebSockets don't response to traditional error codes so we
221 # close the connection.
222 if not self.stream.closed():
223 self.stream.close()
224 else:
197 225 self.session = Session()
198 self.shell_stream = self.application.kernel_manager.create_shell_stream(kernel_id)
199 226 self.shell_stream.on_recv(self._on_zmq_reply)
200 227
201 228 def on_message(self, msg):
202 229 if len(msg) < self.max_msg_size:
203 230 msg = jsonapi.loads(msg)
204 231 self.session.send(self.shell_stream, msg)
205 232
206 233 def on_close(self):
234 # Make sure the stream exists and is not already closed.
235 if self.shell_stream is not None and not self.shell_stream.closed():
207 236 self.shell_stream.close()
208 237
209 238
210 239 #-----------------------------------------------------------------------------
211 240 # Notebook web service handlers
212 241 #-----------------------------------------------------------------------------
213 242
214 243 class NotebookRootHandler(web.RequestHandler):
215 244
216 245 def get(self):
217 246 nbm = self.application.notebook_manager
218 247 files = nbm.list_notebooks()
219 248 self.finish(jsonapi.dumps(files))
220 249
221 250 def post(self):
222 251 nbm = self.application.notebook_manager
223 252 body = self.request.body.strip()
224 253 format = self.get_argument('format', default='json')
225 254 name = self.get_argument('name', default=None)
226 255 if body:
227 256 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
228 257 else:
229 258 notebook_id = nbm.new_notebook()
230 259 self.set_header('Location', '/'+notebook_id)
231 260 self.finish(jsonapi.dumps(notebook_id))
232 261
233 262
234 263 class NotebookHandler(web.RequestHandler):
235 264
236 265 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
237 266
238 267 def get(self, notebook_id):
239 268 nbm = self.application.notebook_manager
240 269 format = self.get_argument('format', default='json')
241 270 last_mod, name, data = nbm.get_notebook(notebook_id, format)
242 271 if format == u'json':
243 272 self.set_header('Content-Type', 'application/json')
244 273 self.set_header('Content-Disposition','attachment; filename="%s.json"' % name)
245 274 elif format == u'xml':
246 275 self.set_header('Content-Type', 'application/xml')
247 276 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
248 277 elif format == u'py':
249 278 self.set_header('Content-Type', 'application/x-python')
250 279 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
251 280 self.set_header('Last-Modified', last_mod)
252 281 self.finish(data)
253 282
254 283 def put(self, notebook_id):
255 284 nbm = self.application.notebook_manager
256 285 format = self.get_argument('format', default='json')
257 286 name = self.get_argument('name', default=None)
258 287 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
259 288 self.set_status(204)
260 289 self.finish()
261 290
262 291 def delete(self, notebook_id):
263 292 nbm = self.application.notebook_manager
264 293 nbm.delete_notebook(notebook_id)
265 294 self.set_status(204)
266 295 self.finish()
267 296
268 297 #-----------------------------------------------------------------------------
269 298 # RST web service handlers
270 299 #-----------------------------------------------------------------------------
271 300
272 301
273 302 class RSTHandler(web.RequestHandler):
274 303
275 304 def post(self):
276 305 if publish_string is None:
277 306 raise web.HTTPError(503)
278 307 body = self.request.body.strip()
279 308 source = body
280 309 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
281 310 defaults = {'file_insertion_enabled': 0,
282 311 'raw_enabled': 0,
283 312 '_disable_config': 1,
284 313 'stylesheet_path': 0
285 314 # 'template': template_path
286 315 }
287 316 try:
288 317 html = publish_string(source, writer_name='html',
289 318 settings_overrides=defaults
290 319 )
291 320 except:
292 321 raise web.HTTPError(400)
293 322 print html
294 323 self.set_header('Content-Type', 'text/html')
295 324 self.finish(html)
296 325
297 326
@@ -1,306 +1,320 b''
1 1 """A kernel manager for multiple kernels."""
2 2
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (C) 2011 The IPython Development Team
5 5 #
6 6 # Distributed under the terms of the BSD License. The full license is in
7 7 # the file COPYING.txt, distributed as part of this software.
8 8 #-----------------------------------------------------------------------------
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Imports
12 12 #-----------------------------------------------------------------------------
13 13
14 14 import signal
15 15 import sys
16 16 import uuid
17 17
18 18 import zmq
19 19 from zmq.eventloop.zmqstream import ZMQStream
20 20
21 21 from tornado import web
22 22
23 23 from IPython.config.configurable import LoggingConfigurable
24 24 from IPython.zmq.ipkernel import launch_kernel
25 25 from IPython.utils.traitlets import Instance, Dict, List, Unicode, Float, Int
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Classes
29 29 #-----------------------------------------------------------------------------
30 30
31 31 class DuplicateKernelError(Exception):
32 32 pass
33 33
34 34
35 35 class KernelManager(LoggingConfigurable):
36 36 """A class for managing multiple kernels."""
37 37
38 38 context = Instance('zmq.Context')
39 39 def _context_default(self):
40 40 return zmq.Context.instance()
41 41
42 42 _kernels = Dict()
43 43
44 44 @property
45 45 def kernel_ids(self):
46 46 """Return a list of the kernel ids of the active kernels."""
47 47 return self._kernels.keys()
48 48
49 49 def __len__(self):
50 50 """Return the number of running kernels."""
51 51 return len(self.kernel_ids)
52 52
53 53 def __contains__(self, kernel_id):
54 54 if kernel_id in self.kernel_ids:
55 55 return True
56 56 else:
57 57 return False
58 58
59 59 def start_kernel(self, **kwargs):
60 60 """Start a new kernel."""
61 61 kernel_id = unicode(uuid.uuid4())
62 62 (process, shell_port, iopub_port, stdin_port, hb_port) = launch_kernel(**kwargs)
63 63 # Store the information for contacting the kernel. This assumes the kernel is
64 64 # running on localhost.
65 65 d = dict(
66 66 process = process,
67 67 stdin_port = stdin_port,
68 68 iopub_port = iopub_port,
69 69 shell_port = shell_port,
70 70 hb_port = hb_port,
71 71 ip = '127.0.0.1'
72 72 )
73 73 self._kernels[kernel_id] = d
74 74 return kernel_id
75 75
76 76 def kill_kernel(self, kernel_id):
77 77 """Kill a kernel by its kernel uuid.
78 78
79 79 Parameters
80 80 ==========
81 81 kernel_id : uuid
82 82 The id of the kernel to kill.
83 83 """
84 84 kernel_process = self.get_kernel_process(kernel_id)
85 85 if kernel_process is not None:
86 86 # Attempt to kill the kernel.
87 87 try:
88 88 kernel_process.kill()
89 89 except OSError, e:
90 90 # In Windows, we will get an Access Denied error if the process
91 91 # has already terminated. Ignore it.
92 92 if not (sys.platform == 'win32' and e.winerror == 5):
93 93 raise
94 94 del self._kernels[kernel_id]
95 95
96 96 def interrupt_kernel(self, kernel_id):
97 97 """Interrupt (SIGINT) the kernel by its uuid.
98 98
99 99 Parameters
100 100 ==========
101 101 kernel_id : uuid
102 102 The id of the kernel to interrupt.
103 103 """
104 104 kernel_process = self.get_kernel_process(kernel_id)
105 105 if kernel_process is not None:
106 106 if sys.platform == 'win32':
107 107 from parentpoller import ParentPollerWindows as Poller
108 108 Poller.send_interrupt(kernel_process.win32_interrupt_event)
109 109 else:
110 110 kernel_process.send_signal(signal.SIGINT)
111 111
112 112
113 113 def signal_kernel(self, kernel_id, signum):
114 114 """ Sends a signal to the kernel by its uuid.
115 115
116 116 Note that since only SIGTERM is supported on Windows, this function
117 117 is only useful on Unix systems.
118 118
119 119 Parameters
120 120 ==========
121 121 kernel_id : uuid
122 122 The id of the kernel to signal.
123 123 """
124 124 kernel_process = self.get_kernel_process(kernel_id)
125 125 if kernel_process is not None:
126 126 kernel_process.send_signal(signum)
127 127
128 128 def get_kernel_process(self, kernel_id):
129 129 """Get the process object for a kernel by its uuid.
130 130
131 131 Parameters
132 132 ==========
133 133 kernel_id : uuid
134 134 The id of the kernel.
135 135 """
136 136 d = self._kernels.get(kernel_id)
137 137 if d is not None:
138 138 return d['process']
139 139 else:
140 140 raise KeyError("Kernel with id not found: %s" % kernel_id)
141 141
142 142 def get_kernel_ports(self, kernel_id):
143 143 """Return a dictionary of ports for a kernel.
144 144
145 145 Parameters
146 146 ==========
147 147 kernel_id : uuid
148 148 The id of the kernel.
149 149
150 150 Returns
151 151 =======
152 152 port_dict : dict
153 153 A dict of key, value pairs where the keys are the names
154 154 (stdin_port,iopub_port,shell_port) and the values are the
155 155 integer port numbers for those channels.
156 156 """
157 157 d = self._kernels.get(kernel_id)
158 158 if d is not None:
159 159 dcopy = d.copy()
160 160 dcopy.pop('process')
161 161 dcopy.pop('ip')
162 162 return dcopy
163 163 else:
164 164 raise KeyError("Kernel with id not found: %s" % kernel_id)
165 165
166 166 def get_kernel_ip(self, kernel_id):
167 167 """Return ip address for a kernel.
168 168
169 169 Parameters
170 170 ==========
171 171 kernel_id : uuid
172 172 The id of the kernel.
173 173
174 174 Returns
175 175 =======
176 176 ip : str
177 177 The ip address of the kernel.
178 178 """
179 179 d = self._kernels.get(kernel_id)
180 180 if d is not None:
181 181 return d['ip']
182 182 else:
183 183 raise KeyError("Kernel with id not found: %s" % kernel_id)
184 184
185 185 def create_connected_stream(self, ip, port, socket_type):
186 186 sock = self.context.socket(socket_type)
187 187 addr = "tcp://%s:%i" % (ip, port)
188 188 self.log.info("Connecting to: %s" % addr)
189 189 sock.connect(addr)
190 190 return ZMQStream(sock)
191 191
192 192 def create_iopub_stream(self, kernel_id):
193 193 ip = self.get_kernel_ip(kernel_id)
194 194 ports = self.get_kernel_ports(kernel_id)
195 195 iopub_stream = self.create_connected_stream(ip, ports['iopub_port'], zmq.SUB)
196 196 iopub_stream.socket.setsockopt(zmq.SUBSCRIBE, b'')
197 197 return iopub_stream
198 198
199 199 def create_shell_stream(self, kernel_id):
200 200 ip = self.get_kernel_ip(kernel_id)
201 201 ports = self.get_kernel_ports(kernel_id)
202 202 shell_stream = self.create_connected_stream(ip, ports['shell_port'], zmq.XREQ)
203 203 return shell_stream
204 204
205 205 def create_hb_stream(self, kernel_id):
206 206 ip = self.get_kernel_ip(kernel_id)
207 207 ports = self.get_kernel_ports(kernel_id)
208 208 hb_stream = self.create_connected_stream(ip, ports['hb_port'], zmq.REQ)
209 209 return hb_stream
210 210
211 211
212 212 class MappingKernelManager(KernelManager):
213 213 """A KernelManager that handles notebok mapping and HTTP error handling"""
214 214
215 215 kernel_argv = List(Unicode)
216 216 kernel_manager = Instance(KernelManager)
217 217 time_to_dead = Float(3.0, config=True, help="""Kernel heartbeat interval in seconds.""")
218 218 max_msg_size = Int(65536, config=True, help="""
219 219 The max raw message size accepted from the browser
220 220 over a WebSocket connection.
221 221 """)
222 222
223 223 _notebook_mapping = Dict()
224 224
225 225 #-------------------------------------------------------------------------
226 226 # Methods for managing kernels and sessions
227 227 #-------------------------------------------------------------------------
228 228
229 229 def kernel_for_notebook(self, notebook_id):
230 230 """Return the kernel_id for a notebook_id or None."""
231 231 return self._notebook_mapping.get(notebook_id)
232 232
233 233 def set_kernel_for_notebook(self, notebook_id, kernel_id):
234 234 """Associate a notebook with a kernel."""
235 235 if notebook_id is not None:
236 236 self._notebook_mapping[notebook_id] = kernel_id
237 237
238 238 def notebook_for_kernel(self, kernel_id):
239 239 """Return the notebook_id for a kernel_id or None."""
240 240 notebook_ids = [k for k, v in self._notebook_mapping.iteritems() if v == kernel_id]
241 241 if len(notebook_ids) == 1:
242 242 return notebook_ids[0]
243 243 else:
244 244 return None
245 245
246 246 def delete_mapping_for_kernel(self, kernel_id):
247 247 """Remove the kernel/notebook mapping for kernel_id."""
248 248 notebook_id = self.notebook_for_kernel(kernel_id)
249 249 if notebook_id is not None:
250 250 del self._notebook_mapping[notebook_id]
251 251
252 252 def start_kernel(self, notebook_id=None):
253 253 """Start a kernel for a notebok an return its kernel_id.
254 254
255 255 Parameters
256 256 ----------
257 257 notebook_id : uuid
258 258 The uuid of the notebook to associate the new kernel with. If this
259 259 is not None, this kernel will be persistent whenever the notebook
260 260 requests a kernel.
261 261 """
262 262 kernel_id = self.kernel_for_notebook(notebook_id)
263 263 if kernel_id is None:
264 264 kwargs = dict()
265 265 kwargs['extra_arguments'] = self.kernel_argv
266 266 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
267 267 self.set_kernel_for_notebook(notebook_id, kernel_id)
268 268 self.log.info("Kernel started: %s" % kernel_id)
269 269 self.log.debug("Kernel args: %r" % kwargs)
270 270 else:
271 271 self.log.info("Using existing kernel: %s" % kernel_id)
272 272 return kernel_id
273 273
274 274 def kill_kernel(self, kernel_id):
275 275 """Kill a kernel and remove its notebook association."""
276 276 if kernel_id not in self:
277 277 raise web.HTTPError(404)
278 278 super(MappingKernelManager, self).kill_kernel(kernel_id)
279 279 self.delete_mapping_for_kernel(kernel_id)
280 280 self.log.info("Kernel killed: %s" % kernel_id)
281 281
282 282 def interrupt_kernel(self, kernel_id):
283 283 """Interrupt a kernel."""
284 284 if kernel_id not in self:
285 285 raise web.HTTPError(404)
286 286 super(MappingKernelManager, self).interrupt_kernel(kernel_id)
287 287 self.log.info("Kernel interrupted: %s" % kernel_id)
288 288
289 289 def restart_kernel(self, kernel_id):
290 290 """Restart a kernel while keeping clients connected."""
291 291 if kernel_id not in self:
292 292 raise web.HTTPError(404)
293 293
294 294 # Get the notebook_id to preserve the kernel/notebook association.
295 295 notebook_id = self.notebook_for_kernel(kernel_id)
296 296 # Create the new kernel first so we can move the clients over.
297 297 new_kernel_id = self.start_kernel()
298 298 # Now kill the old kernel.
299 299 self.kill_kernel(kernel_id)
300 300 # Now save the new kernel/notebook association. We have to save it
301 301 # after the old kernel is killed as that will delete the mapping.
302 302 self.set_kernel_for_notebook(notebook_id, new_kernel_id)
303 self.log.debug("Kernel restarted: %s" % new_kernel_id)
303 self.log.info("Kernel restarted: %s" % new_kernel_id)
304 304 return new_kernel_id
305 305
306 def create_iopub_stream(self, kernel_id):
307 if kernel_id not in self:
308 raise web.HTTPError(404)
309 return super(MappingKernelManager, self).create_iopub_stream(kernel_id)
310
311 def create_shell_stream(self, kernel_id):
312 if kernel_id not in self:
313 raise web.HTTPError(404)
314 return super(MappingKernelManager, self).create_shell_stream(kernel_id)
315
316 def create_hb_stream(self, kernel_id):
317 if kernel_id not in self:
318 raise web.HTTPError(404)
319 return super(MappingKernelManager, self).create_hb_stream(kernel_id)
306 320
@@ -1,244 +1,261 b''
1 1 """A tornado based IPython notebook server."""
2 2
3 3 #-----------------------------------------------------------------------------
4 4 # Copyright (C) 2011 The IPython Development Team
5 5 #
6 6 # Distributed under the terms of the BSD License. The full license is in
7 7 # the file COPYING.txt, distributed as part of this software.
8 8 #-----------------------------------------------------------------------------
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Imports
12 12 #-----------------------------------------------------------------------------
13 13
14 14 import errno
15 15 import logging
16 16 import os
17 17 import signal
18 18 import socket
19 19 import sys
20 20
21 21 import zmq
22 22
23 23 # Install the pyzmq ioloop. This has to be done before anything else from
24 24 # tornado is imported.
25 25 from zmq.eventloop import ioloop
26 26 import tornado.ioloop
27 27 tornado.ioloop = ioloop
28 28
29 29 from tornado import httpserver
30 30 from tornado import web
31 31
32 32 from .kernelmanager import MappingKernelManager
33 33 from .handlers import (
34 34 NBBrowserHandler, NewHandler, NamedNotebookHandler,
35 35 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
36 36 ShellHandler, NotebookRootHandler, NotebookHandler, RSTHandler
37 37 )
38 38 from .notebookmanager import NotebookManager
39 39
40 40 from IPython.core.application import BaseIPythonApplication
41 41 from IPython.core.profiledir import ProfileDir
42 42 from IPython.zmq.session import Session
43 43 from IPython.zmq.zmqshell import ZMQInteractiveShell
44 44 from IPython.zmq.ipkernel import (
45 45 flags as ipkernel_flags,
46 46 aliases as ipkernel_aliases,
47 47 IPKernelApp
48 48 )
49 49 from IPython.utils.traitlets import Dict, Unicode, Int, List, Enum
50 50
51 51 #-----------------------------------------------------------------------------
52 52 # Module globals
53 53 #-----------------------------------------------------------------------------
54 54
55 55 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
56 56 _kernel_action_regex = r"(?P<action>restart|interrupt)"
57 57 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
58 58
59 59 LOCALHOST = '127.0.0.1'
60 60
61 61 _examples = """
62 62 ipython notebook # start the notebook
63 63 ipython notebook --profile=sympy # use the sympy profile
64 64 ipython notebook --pylab=inline # pylab in inline plotting mode
65 65 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
66 66 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
67 67 """
68 68
69 69 #-----------------------------------------------------------------------------
70 70 # The Tornado web application
71 71 #-----------------------------------------------------------------------------
72 72
73 73 class NotebookWebApplication(web.Application):
74 74
75 def __init__(self, kernel_manager, notebook_manager, log):
75 def __init__(self, ipython_app, kernel_manager, notebook_manager, log):
76 76 handlers = [
77 77 (r"/", NBBrowserHandler),
78 78 (r"/new", NewHandler),
79 79 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
80 80 (r"/kernels", MainKernelHandler),
81 81 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
82 82 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
83 83 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
84 84 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
85 85 (r"/notebooks", NotebookRootHandler),
86 86 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
87 87 (r"/rstservice/render", RSTHandler)
88 88 ]
89 89 settings = dict(
90 90 template_path=os.path.join(os.path.dirname(__file__), "templates"),
91 91 static_path=os.path.join(os.path.dirname(__file__), "static"),
92 92 )
93 93 web.Application.__init__(self, handlers, **settings)
94 94
95 95 self.kernel_manager = kernel_manager
96 96 self.log = log
97 97 self.notebook_manager = notebook_manager
98 self.ipython_app = ipython_app
98 99
99 100
100 101 #-----------------------------------------------------------------------------
101 102 # Aliases and Flags
102 103 #-----------------------------------------------------------------------------
103 104
104 105 flags = dict(ipkernel_flags)
105 106
106 107 # the flags that are specific to the frontend
107 108 # these must be scrubbed before being passed to the kernel,
108 109 # or it will raise an error on unrecognized flags
109 110 notebook_flags = []
110 111
111 112 aliases = dict(ipkernel_aliases)
112 113
113 114 aliases.update({
114 115 'ip': 'IPythonNotebookApp.ip',
115 116 'port': 'IPythonNotebookApp.port',
116 117 'keyfile': 'IPythonNotebookApp.keyfile',
117 118 'certfile': 'IPythonNotebookApp.certfile',
119 'ws-hostname': 'IPythonNotebookApp.ws_hostname',
118 120 'notebook-dir': 'NotebookManager.notebook_dir'
119 121 })
120 122
121 123 #-----------------------------------------------------------------------------
122 124 # IPythonNotebookApp
123 125 #-----------------------------------------------------------------------------
124 126
125 127 class IPythonNotebookApp(BaseIPythonApplication):
126 128
127 129 name = 'ipython-notebook'
128 130 default_config_file_name='ipython_notebook_config.py'
129 131
130 132 description = """
131 133 The IPython HTML Notebook.
132 134
133 135 This launches a Tornado based HTML Notebook Server that serves up an
134 136 HTML5/Javascript Notebook client.
135 137 """
136 138 examples = _examples
137 139
138 140 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session,
139 141 MappingKernelManager, NotebookManager]
140 142 flags = Dict(flags)
141 143 aliases = Dict(aliases)
142 144
143 145 kernel_argv = List(Unicode)
144 146
145 147 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
146 148 default_value=logging.INFO,
147 149 config=True,
148 150 help="Set the log level by value or name.")
149 151
150 152 # Network related information.
151 153
152 154 ip = Unicode(LOCALHOST, config=True,
153 155 help="The IP address the notebook server will listen on."
154 156 )
155 157
156 158 def _ip_changed(self, name, old, new):
157 159 if new == u'*': self.ip = u''
158 160
159 161 port = Int(8888, config=True,
160 162 help="The port the notebook server will listen on."
161 163 )
162 164
165 ws_hostname = Unicode(LOCALHOST, config=True,
166 help="""The FQDN or IP for WebSocket connections. The default will work
167 fine when the server is listening on localhost, but this needs to
168 be set if the ip option is used. It will be used as the hostname part
169 of the WebSocket url: ws://hostname/path."""
170 )
171
163 172 certfile = Unicode(u'', config=True,
164 173 help="""The full path to an SSL/TLS certificate file."""
165 174 )
166 175
167 176 keyfile = Unicode(u'', config=True,
168 177 help="""The full path to a private key file for usage with SSL/TLS."""
169 178 )
170 179
180 def get_ws_url(self):
181 """Return the WebSocket URL for this server."""
182 if self.certfile:
183 prefix = u'wss://'
184 else:
185 prefix = u'ws://'
186 return prefix + self.ws_hostname + u':' + unicode(self.port)
187
171 188 def parse_command_line(self, argv=None):
172 189 super(IPythonNotebookApp, self).parse_command_line(argv)
173 190 if argv is None:
174 191 argv = sys.argv[1:]
175 192
176 193 self.kernel_argv = list(argv) # copy
177 194 # kernel should inherit default config file from frontend
178 195 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
179 196 # scrub frontend-specific flags
180 197 for a in argv:
181 198 if a.startswith('-') and a.lstrip('-') in notebook_flags:
182 199 self.kernel_argv.remove(a)
183 200
184 201 def init_configurables(self):
185 202 # Don't let Qt or ZMQ swallow KeyboardInterupts.
186 203 signal.signal(signal.SIGINT, signal.SIG_DFL)
187 204
188 205 # Create a KernelManager and start a kernel.
189 206 self.kernel_manager = MappingKernelManager(
190 207 config=self.config, log=self.log, kernel_argv=self.kernel_argv
191 208 )
192 209 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
193 210
194 211 def init_logging(self):
195 212 super(IPythonNotebookApp, self).init_logging()
196 213 # This prevents double log messages because tornado use a root logger that
197 214 # self.log is a child of. The logging module dipatches log messages to a log
198 215 # and all of its ancenstors until propagate is set to False.
199 216 self.log.propagate = False
200 217
201 218 def initialize(self, argv=None):
202 219 super(IPythonNotebookApp, self).initialize(argv)
203 220 self.init_configurables()
204 221 self.web_app = NotebookWebApplication(
205 self.kernel_manager, self.notebook_manager, self.log
222 self, self.kernel_manager, self.notebook_manager, self.log
206 223 )
207 224 if self.certfile:
208 225 ssl_options = dict(certfile=self.certfile)
209 226 if self.keyfile:
210 227 ssl_options['keyfile'] = self.keyfile
211 228 else:
212 229 ssl_options = None
213 230 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
214 231 if ssl_options is None and not self.ip:
215 232 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
216 233 'but not using any encryption or authentication. This is highly '
217 234 'insecure and not recommended.')
218 235 for i in range(10):
219 236 try:
220 237 port = self.port + i
221 238 self.http_server.listen(port, self.ip)
222 239 except socket.error, e:
223 240 if e.errno != errno.EADDRINUSE:
224 241 raise
225 242 self.log.info('The port %i is already in use, trying: %i' % (port, port+1))
226 243 else:
227 244 self.port = port
228 245 break
229 246
230 247
231 248 def start(self):
232 249 ip = self.ip if self.ip else '[all ip addresses on your system]'
233 250 self.log.info("The IPython Notebook is running at: http://%s:%i" % (ip, self.port))
234 251 ioloop.IOLoop.instance().start()
235 252
236 253 #-----------------------------------------------------------------------------
237 254 # Main entry point
238 255 #-----------------------------------------------------------------------------
239 256
240 257 def launch_new_instance():
241 258 app = IPythonNotebookApp()
242 259 app.initialize()
243 260 app.start()
244 261
@@ -1,141 +1,143 b''
1 1
2 2 //============================================================================
3 3 // Kernel
4 4 //============================================================================
5 5
6 6 var IPython = (function (IPython) {
7 7
8 8 var utils = IPython.utils;
9 9
10 10 var Kernel = function () {
11 11 this.kernel_id = null;
12 12 this.base_url = "/kernels";
13 13 this.kernel_url = null;
14 14 this.shell_channel = null;
15 15 this.iopub_channel = null;
16 16 this.running = false;
17 17 };
18 18
19 19
20 20 Kernel.prototype.get_msg = function (msg_type, content) {
21 21 var msg = {
22 22 header : {
23 23 msg_id : utils.uuid(),
24 24 username : "username",
25 25 session: this.session_id,
26 26 msg_type : msg_type
27 27 },
28 28 content : content,
29 29 parent_header : {}
30 30 };
31 31 return msg;
32 32 }
33 33
34 34 Kernel.prototype.start = function (notebook_id, callback) {
35 35 var that = this;
36 36 if (!this.running) {
37 37 var qs = $.param({notebook:notebook_id});
38 38 $.post(this.base_url + '?' + qs,
39 39 function (kernel_id) {
40 40 that._handle_start_kernel(kernel_id, callback);
41 41 },
42 42 'json'
43 43 );
44 44 };
45 45 };
46 46
47 47
48 48 Kernel.prototype.restart = function (callback) {
49 49 IPython.kernel_status_widget.status_restarting();
50 50 var url = this.kernel_url + "/restart";
51 51 var that = this;
52 52 if (this.running) {
53 53 this.stop_channels();
54 54 $.post(url,
55 55 function (kernel_id) {
56 56 that._handle_start_kernel(kernel_id, callback);
57 57 },
58 58 'json'
59 59 );
60 60 };
61 61 };
62 62
63 63
64 Kernel.prototype._handle_start_kernel = function (kernel_id, callback) {
64 Kernel.prototype._handle_start_kernel = function (json, callback) {
65 65 this.running = true;
66 this.kernel_id = kernel_id;
66 this.kernel_id = json.kernel_id;
67 this.ws_url = json.ws_url;
67 68 this.kernel_url = this.base_url + "/" + this.kernel_id;
68 69 this.start_channels();
69 70 callback();
70 71 IPython.kernel_status_widget.status_idle();
71 72 };
72 73
73 74
74 75 Kernel.prototype.start_channels = function () {
75 76 this.stop_channels();
76 var ws_url = "ws://127.0.0.1:8888" + this.kernel_url;
77 var ws_url = this.ws_url + this.kernel_url;
78 console.log("Starting WS:", ws_url);
77 79 this.shell_channel = new WebSocket(ws_url + "/shell");
78 80 this.iopub_channel = new WebSocket(ws_url + "/iopub");
79 81 };
80 82
81 83
82 84 Kernel.prototype.stop_channels = function () {
83 85 if (this.shell_channel !== null) {
84 86 this.shell_channel.close();
85 87 this.shell_channel = null;
86 88 };
87 89 if (this.iopub_channel !== null) {
88 90 this.iopub_channel.close();
89 91 this.iopub_channel = null;
90 92 };
91 93 };
92 94
93 95 Kernel.prototype.execute = function (code) {
94 96 var content = {
95 97 code : code,
96 98 silent : false,
97 99 user_variables : [],
98 100 user_expressions : {}
99 101 };
100 102 var msg = this.get_msg("execute_request", content);
101 103 this.shell_channel.send(JSON.stringify(msg));
102 104 return msg.header.msg_id;
103 105 }
104 106
105 107
106 108 Kernel.prototype.complete = function (line, cursor_pos) {
107 109 var content = {
108 110 text : '',
109 111 line : line,
110 112 cursor_pos : cursor_pos
111 113 };
112 114 var msg = this.get_msg("complete_request", content);
113 115 this.shell_channel.send(JSON.stringify(msg));
114 116 return msg.header.msg_id;
115 117 }
116 118
117 119
118 120 Kernel.prototype.interrupt = function () {
119 121 if (this.running) {
120 122 $.post(this.kernel_url + "/interrupt");
121 123 };
122 124 };
123 125
124 126
125 127 Kernel.prototype.kill = function () {
126 128 if (this.running) {
127 129 this.running = false;
128 130 var settings = {
129 131 cache : false,
130 132 type : "DELETE",
131 133 };
132 134 $.ajax(this.kernel_url, settings);
133 135 };
134 136 };
135 137
136 138 IPython.Kernel = Kernel;
137 139
138 140 return IPython;
139 141
140 142 }(IPython));
141 143
General Comments 0
You need to be logged in to leave comments. Login now