##// END OF EJS Templates
Minor changes to handlers.
Brian E. Granger -
Show More
@@ -1,428 +1,435 b''
1 1 """Tornado handlers for the notebook.
2 2
3 3 Authors:
4 4
5 5 * Brian Granger
6 6 """
7 7
8 8 #-----------------------------------------------------------------------------
9 9 # Copyright (C) 2008-2011 The IPython Development Team
10 10 #
11 11 # Distributed under the terms of the BSD License. The full license is in
12 12 # the file COPYING, distributed as part of this software.
13 13 #-----------------------------------------------------------------------------
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 import logging
20 20 import Cookie
21 21 import uuid
22 22
23 23 from tornado import web
24 24 from tornado import websocket
25 25
26 26 from zmq.eventloop import ioloop
27 27 from zmq.utils import jsonapi
28 28
29 29 from IPython.zmq.session import Session
30 30
31 31 try:
32 32 from docutils.core import publish_string
33 33 except ImportError:
34 34 publish_string = None
35 35
36 36
37 37
38 38 #-----------------------------------------------------------------------------
39 39 # Top-level handlers
40 40 #-----------------------------------------------------------------------------
41 41
42 42 class AuthenticatedHandler(web.RequestHandler):
43 43 """A RequestHandler with an authenticated user."""
44
44 45 def get_current_user(self):
45 46 user_id = self.get_secure_cookie("user")
47 # For now the user_id should not return empty, but it could eventually
46 48 if user_id == '':
47 49 user_id = 'anonymous'
48 50 if user_id is None:
49 51 # prevent extra Invalid cookie sig warnings:
50 52 self.clear_cookie('user')
51 53 if not self.application.password:
52 54 user_id = 'anonymous'
53 55 return user_id
54 56
55 57
56 58 class NBBrowserHandler(AuthenticatedHandler):
59
57 60 @web.authenticated
58 61 def get(self):
59 62 nbm = self.application.notebook_manager
60 63 project = nbm.notebook_dir
61 64 self.render('nbbrowser.html', project=project,
62 65 base_project_url=u'/', base_kernel_url=u'/')
63 66
67
64 68 class LoginHandler(AuthenticatedHandler):
69
65 70 def get(self):
66 user_id = self.get_secure_cookie("user") or ''
67 self.render('login.html', user_id=user_id)
71 self.render('login.html')
68 72
69 73 def post(self):
70 74 pwd = self.get_argument("password", default=u'')
71 75 if self.application.password and pwd == self.application.password:
72 76 self.set_secure_cookie("user", str(uuid.uuid4()))
73 77 url = self.get_argument("next", default="/")
74 78 self.redirect(url)
75 79
80
76 81 class NewHandler(AuthenticatedHandler):
82
77 83 @web.authenticated
78 84 def get(self):
79 85 notebook_id = self.application.notebook_manager.new_notebook()
80 86 self.render('notebook.html', notebook_id=notebook_id,
81 87 base_project_url=u'/', base_kernel_url=u'/')
82 88
83 89
84 90 class NamedNotebookHandler(AuthenticatedHandler):
91
85 92 @web.authenticated
86 93 def get(self, notebook_id):
87 94 nbm = self.application.notebook_manager
88 95 if not nbm.notebook_exists(notebook_id):
89 96 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
90 97 self.render('notebook.html', notebook_id=notebook_id,
91 98 base_project_url=u'/', base_kernel_url=u'/')
92 99
93 100
94 101 #-----------------------------------------------------------------------------
95 102 # Kernel handlers
96 103 #-----------------------------------------------------------------------------
97 104
98 105
99 106 class MainKernelHandler(AuthenticatedHandler):
100 107
101 108 @web.authenticated
102 109 def get(self):
103 110 km = self.application.kernel_manager
104 111 self.finish(jsonapi.dumps(km.kernel_ids))
105 112
106 113 @web.authenticated
107 114 def post(self):
108 115 km = self.application.kernel_manager
109 116 notebook_id = self.get_argument('notebook', default=None)
110 117 kernel_id = km.start_kernel(notebook_id)
111 118 ws_url = self.application.ipython_app.get_ws_url()
112 119 data = {'ws_url':ws_url,'kernel_id':kernel_id}
113 120 self.set_header('Location', '/'+kernel_id)
114 121 self.finish(jsonapi.dumps(data))
115 122
116 123
117 124 class KernelHandler(AuthenticatedHandler):
118 125
119 126 SUPPORTED_METHODS = ('DELETE')
120 127
121 128 @web.authenticated
122 129 def delete(self, kernel_id):
123 130 km = self.application.kernel_manager
124 131 km.kill_kernel(kernel_id)
125 132 self.set_status(204)
126 133 self.finish()
127 134
128 135
129 136 class KernelActionHandler(AuthenticatedHandler):
130 137
131 138 @web.authenticated
132 139 def post(self, kernel_id, action):
133 140 km = self.application.kernel_manager
134 141 if action == 'interrupt':
135 142 km.interrupt_kernel(kernel_id)
136 143 self.set_status(204)
137 144 if action == 'restart':
138 145 new_kernel_id = km.restart_kernel(kernel_id)
139 146 ws_url = self.application.ipython_app.get_ws_url()
140 147 data = {'ws_url':ws_url,'kernel_id':new_kernel_id}
141 148 self.set_header('Location', '/'+new_kernel_id)
142 149 self.write(jsonapi.dumps(data))
143 150 self.finish()
144 151
145 152
146 153 class ZMQStreamHandler(websocket.WebSocketHandler):
147 154
148 155 def _reserialize_reply(self, msg_list):
149 156 """Reserialize a reply message using JSON.
150 157
151 158 This takes the msg list from the ZMQ socket, unserializes it using
152 159 self.session and then serializes the result using JSON. This method
153 160 should be used by self._on_zmq_reply to build messages that can
154 161 be sent back to the browser.
155 162 """
156 163 idents, msg_list = self.session.feed_identities(msg_list)
157 164 msg = self.session.unserialize(msg_list)
158 165 try:
159 166 msg['header'].pop('date')
160 167 except KeyError:
161 168 pass
162 169 try:
163 170 msg['parent_header'].pop('date')
164 171 except KeyError:
165 172 pass
166 173 msg.pop('buffers')
167 174 return jsonapi.dumps(msg)
168 175
169 176 def _on_zmq_reply(self, msg_list):
170 177 try:
171 178 msg = self._reserialize_reply(msg_list)
172 179 except:
173 180 self.application.kernel_manager.log.critical("Malformed message: %r" % msg_list)
174 181 else:
175 182 self.write_message(msg)
176 183
177 184 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
178 185 def open(self, kernel_id):
179 186 self.kernel_id = kernel_id.decode('ascii')
180 187 try:
181 188 cfg = self.application.ipython_app.config
182 189 except AttributeError:
183 190 # protect from the case where this is run from something other than
184 191 # the notebook app:
185 192 cfg = None
186 193 self.session = Session(config=cfg)
187 194 self.save_on_message = self.on_message
188 195 self.on_message = self.on_first_message
189 196
190 197 def get_current_user(self):
191 198 user_id = self.get_secure_cookie("user")
192 199 if user_id == '' or (user_id is None and not self.application.password):
193 200 user_id = 'anonymous'
194 201 return user_id
195 202
196 203 def _inject_cookie_message(self, msg):
197 204 """Inject the first message, which is the document cookie,
198 205 for authentication."""
199 206 if isinstance(msg, unicode):
200 207 # Cookie can't constructor doesn't accept unicode strings for some reason
201 208 msg = msg.encode('utf8', 'replace')
202 209 try:
203 210 self._cookies = Cookie.SimpleCookie(msg)
204 211 except:
205 212 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
206 213
207 214 def on_first_message(self, msg):
208 215 self._inject_cookie_message(msg)
209 216 if self.get_current_user() is None:
210 217 logging.warn("Couldn't authenticate WebSocket connection")
211 218 raise web.HTTPError(403)
212 219 self.on_message = self.save_on_message
213 220
214 221
215 222 class IOPubHandler(AuthenticatedZMQStreamHandler):
216 223
217 224 def initialize(self, *args, **kwargs):
218 225 self._kernel_alive = True
219 226 self._beating = False
220 227 self.iopub_stream = None
221 228 self.hb_stream = None
222 229
223 230 def on_first_message(self, msg):
224 231 try:
225 232 super(IOPubHandler, self).on_first_message(msg)
226 233 except web.HTTPError:
227 234 self.close()
228 235 return
229 236 km = self.application.kernel_manager
230 237 self.time_to_dead = km.time_to_dead
231 238 kernel_id = self.kernel_id
232 239 try:
233 240 self.iopub_stream = km.create_iopub_stream(kernel_id)
234 241 self.hb_stream = km.create_hb_stream(kernel_id)
235 242 except web.HTTPError:
236 243 # WebSockets don't response to traditional error codes so we
237 244 # close the connection.
238 245 if not self.stream.closed():
239 246 self.stream.close()
240 247 self.close()
241 248 else:
242 249 self.iopub_stream.on_recv(self._on_zmq_reply)
243 250 self.start_hb(self.kernel_died)
244 251
245 252 def on_message(self, msg):
246 253 pass
247 254
248 255 def on_close(self):
249 256 # This method can be called twice, once by self.kernel_died and once
250 257 # from the WebSocket close event. If the WebSocket connection is
251 258 # closed before the ZMQ streams are setup, they could be None.
252 259 self.stop_hb()
253 260 if self.iopub_stream is not None and not self.iopub_stream.closed():
254 261 self.iopub_stream.on_recv(None)
255 262 self.iopub_stream.close()
256 263 if self.hb_stream is not None and not self.hb_stream.closed():
257 264 self.hb_stream.close()
258 265
259 266 def start_hb(self, callback):
260 267 """Start the heartbeating and call the callback if the kernel dies."""
261 268 if not self._beating:
262 269 self._kernel_alive = True
263 270
264 271 def ping_or_dead():
265 272 if self._kernel_alive:
266 273 self._kernel_alive = False
267 274 self.hb_stream.send(b'ping')
268 275 else:
269 276 try:
270 277 callback()
271 278 except:
272 279 pass
273 280 finally:
274 281 self._hb_periodic_callback.stop()
275 282
276 283 def beat_received(msg):
277 284 self._kernel_alive = True
278 285
279 286 self.hb_stream.on_recv(beat_received)
280 287 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000)
281 288 self._hb_periodic_callback.start()
282 289 self._beating= True
283 290
284 291 def stop_hb(self):
285 292 """Stop the heartbeating and cancel all related callbacks."""
286 293 if self._beating:
287 294 self._hb_periodic_callback.stop()
288 295 if not self.hb_stream.closed():
289 296 self.hb_stream.on_recv(None)
290 297
291 298 def kernel_died(self):
292 299 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
293 300 self.write_message(
294 301 {'header': {'msg_type': 'status'},
295 302 'parent_header': {},
296 303 'content': {'execution_state':'dead'}
297 304 }
298 305 )
299 306 self.on_close()
300 307
301 308
302 309 class ShellHandler(AuthenticatedZMQStreamHandler):
303 310
304 311 def initialize(self, *args, **kwargs):
305 312 self.shell_stream = None
306 313
307 314 def on_first_message(self, msg):
308 315 try:
309 316 super(ShellHandler, self).on_first_message(msg)
310 317 except web.HTTPError:
311 318 self.close()
312 319 return
313 320 km = self.application.kernel_manager
314 321 self.max_msg_size = km.max_msg_size
315 322 kernel_id = self.kernel_id
316 323 try:
317 324 self.shell_stream = km.create_shell_stream(kernel_id)
318 325 except web.HTTPError:
319 326 # WebSockets don't response to traditional error codes so we
320 327 # close the connection.
321 328 if not self.stream.closed():
322 329 self.stream.close()
323 330 self.close()
324 331 else:
325 332 self.shell_stream.on_recv(self._on_zmq_reply)
326 333
327 334 def on_message(self, msg):
328 335 if len(msg) < self.max_msg_size:
329 336 msg = jsonapi.loads(msg)
330 337 self.session.send(self.shell_stream, msg)
331 338
332 339 def on_close(self):
333 340 # Make sure the stream exists and is not already closed.
334 341 if self.shell_stream is not None and not self.shell_stream.closed():
335 342 self.shell_stream.close()
336 343
337 344
338 345 #-----------------------------------------------------------------------------
339 346 # Notebook web service handlers
340 347 #-----------------------------------------------------------------------------
341 348
342 349 class NotebookRootHandler(AuthenticatedHandler):
343 350
344 351 @web.authenticated
345 352 def get(self):
346 353 nbm = self.application.notebook_manager
347 354 files = nbm.list_notebooks()
348 355 self.finish(jsonapi.dumps(files))
349 356
350 357 @web.authenticated
351 358 def post(self):
352 359 nbm = self.application.notebook_manager
353 360 body = self.request.body.strip()
354 361 format = self.get_argument('format', default='json')
355 362 name = self.get_argument('name', default=None)
356 363 if body:
357 364 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
358 365 else:
359 366 notebook_id = nbm.new_notebook()
360 367 self.set_header('Location', '/'+notebook_id)
361 368 self.finish(jsonapi.dumps(notebook_id))
362 369
363 370
364 371 class NotebookHandler(AuthenticatedHandler):
365 372
366 373 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
367 374
368 375 @web.authenticated
369 376 def get(self, notebook_id):
370 377 nbm = self.application.notebook_manager
371 378 format = self.get_argument('format', default='json')
372 379 last_mod, name, data = nbm.get_notebook(notebook_id, format)
373 380 if format == u'json':
374 381 self.set_header('Content-Type', 'application/json')
375 382 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
376 383 elif format == u'py':
377 384 self.set_header('Content-Type', 'application/x-python')
378 385 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
379 386 self.set_header('Last-Modified', last_mod)
380 387 self.finish(data)
381 388
382 389 @web.authenticated
383 390 def put(self, notebook_id):
384 391 nbm = self.application.notebook_manager
385 392 format = self.get_argument('format', default='json')
386 393 name = self.get_argument('name', default=None)
387 394 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
388 395 self.set_status(204)
389 396 self.finish()
390 397
391 398 @web.authenticated
392 399 def delete(self, notebook_id):
393 400 nbm = self.application.notebook_manager
394 401 nbm.delete_notebook(notebook_id)
395 402 self.set_status(204)
396 403 self.finish()
397 404
398 405 #-----------------------------------------------------------------------------
399 406 # RST web service handlers
400 407 #-----------------------------------------------------------------------------
401 408
402 409
403 410 class RSTHandler(AuthenticatedHandler):
404 411
405 412 @web.authenticated
406 413 def post(self):
407 414 if publish_string is None:
408 415 raise web.HTTPError(503, u'docutils not available')
409 416 body = self.request.body.strip()
410 417 source = body
411 418 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
412 419 defaults = {'file_insertion_enabled': 0,
413 420 'raw_enabled': 0,
414 421 '_disable_config': 1,
415 422 'stylesheet_path': 0
416 423 # 'template': template_path
417 424 }
418 425 try:
419 426 html = publish_string(source, writer_name='html',
420 427 settings_overrides=defaults
421 428 )
422 429 except:
423 430 raise web.HTTPError(400, u'Invalid RST')
424 431 print html
425 432 self.set_header('Content-Type', 'text/html')
426 433 self.finish(html)
427 434
428 435
General Comments 0
You need to be logged in to leave comments. Login now