##// END OF EJS Templates
Refactoring kernel_died method to make subclass friendly.
Brian E. Granger -
Show More
@@ -1,888 +1,892 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 Cookie
20 20 import datetime
21 21 import email.utils
22 22 import hashlib
23 23 import logging
24 24 import mimetypes
25 25 import os
26 26 import stat
27 27 import threading
28 28 import time
29 29 import uuid
30 30 import os
31 31
32 32 from tornado.escape import url_escape
33 33 from tornado import web
34 34 from tornado import websocket
35 35
36 36 from zmq.eventloop import ioloop
37 37 from zmq.utils import jsonapi
38 38
39 39 from IPython.external.decorator import decorator
40 40 from IPython.zmq.session import Session
41 41 from IPython.lib.security import passwd_check
42 42 from IPython.utils.jsonutil import date_default
43 43 from IPython.utils.path import filefind
44 44
45 45 try:
46 46 from docutils.core import publish_string
47 47 except ImportError:
48 48 publish_string = None
49 49
50 50 #-----------------------------------------------------------------------------
51 51 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
52 52 #-----------------------------------------------------------------------------
53 53
54 54 # Google Chrome, as of release 16, changed its websocket protocol number. The
55 55 # parts tornado cares about haven't really changed, so it's OK to continue
56 56 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
57 57 # version as of Oct 30/2011) the version check fails, see the issue report:
58 58
59 59 # https://github.com/facebook/tornado/issues/385
60 60
61 61 # This issue has been fixed in Tornado post 2.1.1:
62 62
63 63 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
64 64
65 65 # Here we manually apply the same patch as above so that users of IPython can
66 66 # continue to work with an officially released Tornado. We make the
67 67 # monkeypatch version check as narrow as possible to limit its effects; once
68 68 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
69 69
70 70 import tornado
71 71
72 72 if tornado.version_info <= (2,1,1):
73 73
74 74 def _execute(self, transforms, *args, **kwargs):
75 75 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
76 76
77 77 self.open_args = args
78 78 self.open_kwargs = kwargs
79 79
80 80 # The difference between version 8 and 13 is that in 8 the
81 81 # client sends a "Sec-Websocket-Origin" header and in 13 it's
82 82 # simply "Origin".
83 83 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
84 84 self.ws_connection = WebSocketProtocol8(self)
85 85 self.ws_connection.accept_connection()
86 86
87 87 elif self.request.headers.get("Sec-WebSocket-Version"):
88 88 self.stream.write(tornado.escape.utf8(
89 89 "HTTP/1.1 426 Upgrade Required\r\n"
90 90 "Sec-WebSocket-Version: 8\r\n\r\n"))
91 91 self.stream.close()
92 92
93 93 else:
94 94 self.ws_connection = WebSocketProtocol76(self)
95 95 self.ws_connection.accept_connection()
96 96
97 97 websocket.WebSocketHandler._execute = _execute
98 98 del _execute
99 99
100 100 #-----------------------------------------------------------------------------
101 101 # Decorator for disabling read-only handlers
102 102 #-----------------------------------------------------------------------------
103 103
104 104 @decorator
105 105 def not_if_readonly(f, self, *args, **kwargs):
106 106 if self.application.read_only:
107 107 raise web.HTTPError(403, "Notebook server is read-only")
108 108 else:
109 109 return f(self, *args, **kwargs)
110 110
111 111 @decorator
112 112 def authenticate_unless_readonly(f, self, *args, **kwargs):
113 113 """authenticate this page *unless* readonly view is active.
114 114
115 115 In read-only mode, the notebook list and print view should
116 116 be accessible without authentication.
117 117 """
118 118
119 119 @web.authenticated
120 120 def auth_f(self, *args, **kwargs):
121 121 return f(self, *args, **kwargs)
122 122
123 123 if self.application.read_only:
124 124 return f(self, *args, **kwargs)
125 125 else:
126 126 return auth_f(self, *args, **kwargs)
127 127
128 128 def urljoin(*pieces):
129 129 """Join componenet of url into a relative url
130 130
131 131 Use to prevent double slash when joining subpath
132 132 """
133 133 striped = [s.strip('/') for s in pieces]
134 134 return '/'.join(s for s in striped if s)
135 135
136 136 #-----------------------------------------------------------------------------
137 137 # Top-level handlers
138 138 #-----------------------------------------------------------------------------
139 139
140 140 class RequestHandler(web.RequestHandler):
141 141 """RequestHandler with default variable setting."""
142 142
143 143 def render(*args, **kwargs):
144 144 kwargs.setdefault('message', '')
145 145 return web.RequestHandler.render(*args, **kwargs)
146 146
147 147 class AuthenticatedHandler(RequestHandler):
148 148 """A RequestHandler with an authenticated user."""
149 149
150 150 def get_current_user(self):
151 151 user_id = self.get_secure_cookie(self.settings['cookie_name'])
152 152 # For now the user_id should not return empty, but it could eventually
153 153 if user_id == '':
154 154 user_id = 'anonymous'
155 155 if user_id is None:
156 156 # prevent extra Invalid cookie sig warnings:
157 157 self.clear_cookie(self.settings['cookie_name'])
158 158 if not self.application.password and not self.application.read_only:
159 159 user_id = 'anonymous'
160 160 return user_id
161 161
162 162 @property
163 163 def logged_in(self):
164 164 """Is a user currently logged in?
165 165
166 166 """
167 167 user = self.get_current_user()
168 168 return (user and not user == 'anonymous')
169 169
170 170 @property
171 171 def login_available(self):
172 172 """May a user proceed to log in?
173 173
174 174 This returns True if login capability is available, irrespective of
175 175 whether the user is already logged in or not.
176 176
177 177 """
178 178 return bool(self.application.password)
179 179
180 180 @property
181 181 def read_only(self):
182 182 """Is the notebook read-only?
183 183
184 184 """
185 185 return self.application.read_only
186 186
187 187 @property
188 188 def ws_url(self):
189 189 """websocket url matching the current request
190 190
191 191 turns http[s]://host[:port] into
192 192 ws[s]://host[:port]
193 193 """
194 194 proto = self.request.protocol.replace('http', 'ws')
195 195 host = self.application.ipython_app.websocket_host # default to config value
196 196 if host == '':
197 197 host = self.request.host # get from request
198 198 return "%s://%s" % (proto, host)
199 199
200 200
201 201 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
202 202 """static files should only be accessible when logged in"""
203 203
204 204 @authenticate_unless_readonly
205 205 def get(self, path):
206 206 return web.StaticFileHandler.get(self, path)
207 207
208 208
209 209 class ProjectDashboardHandler(AuthenticatedHandler):
210 210
211 211 @authenticate_unless_readonly
212 212 def get(self):
213 213 nbm = self.application.notebook_manager
214 214 project = nbm.notebook_dir
215 215 template = self.application.jinja2_env.get_template('projectdashboard.html')
216 216 self.write( template.render(project=project,
217 217 base_project_url=self.application.ipython_app.base_project_url,
218 218 base_kernel_url=self.application.ipython_app.base_kernel_url,
219 219 read_only=self.read_only,
220 220 logged_in=self.logged_in,
221 221 login_available=self.login_available))
222 222
223 223
224 224 class LoginHandler(AuthenticatedHandler):
225 225
226 226 def _render(self, message=None):
227 227 template = self.application.jinja2_env.get_template('login.html')
228 228 self.write( template.render(
229 229 next=url_escape(self.get_argument('next', default=self.application.ipython_app.base_project_url)),
230 230 read_only=self.read_only,
231 231 logged_in=self.logged_in,
232 232 login_available=self.login_available,
233 233 base_project_url=self.application.ipython_app.base_project_url,
234 234 message=message
235 235 ))
236 236
237 237 def get(self):
238 238 if self.current_user:
239 239 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
240 240 else:
241 241 self._render()
242 242
243 243 def post(self):
244 244 pwd = self.get_argument('password', default=u'')
245 245 if self.application.password:
246 246 if passwd_check(self.application.password, pwd):
247 247 self.set_secure_cookie(self.settings['cookie_name'], str(uuid.uuid4()))
248 248 else:
249 249 self._render(message={'error': 'Invalid password'})
250 250 return
251 251
252 252 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
253 253
254 254
255 255 class LogoutHandler(AuthenticatedHandler):
256 256
257 257 def get(self):
258 258 self.clear_cookie(self.settings['cookie_name'])
259 259 if self.login_available:
260 260 message = {'info': 'Successfully logged out.'}
261 261 else:
262 262 message = {'warning': 'Cannot log out. Notebook authentication '
263 263 'is disabled.'}
264 264 template = self.application.jinja2_env.get_template('logout.html')
265 265 self.write( template.render(
266 266 read_only=self.read_only,
267 267 logged_in=self.logged_in,
268 268 login_available=self.login_available,
269 269 base_project_url=self.application.ipython_app.base_project_url,
270 270 message=message))
271 271
272 272
273 273 class NewHandler(AuthenticatedHandler):
274 274
275 275 @web.authenticated
276 276 def get(self):
277 277 nbm = self.application.notebook_manager
278 278 project = nbm.notebook_dir
279 279 notebook_id = nbm.new_notebook()
280 280 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
281 281
282 282 class NamedNotebookHandler(AuthenticatedHandler):
283 283
284 284 @authenticate_unless_readonly
285 285 def get(self, notebook_id):
286 286 nbm = self.application.notebook_manager
287 287 project = nbm.notebook_dir
288 288 if not nbm.notebook_exists(notebook_id):
289 289 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
290 290 template = self.application.jinja2_env.get_template('notebook.html')
291 291 self.write( template.render(project=project,
292 292 notebook_id=notebook_id,
293 293 base_project_url=self.application.ipython_app.base_project_url,
294 294 base_kernel_url=self.application.ipython_app.base_kernel_url,
295 295 kill_kernel=False,
296 296 read_only=self.read_only,
297 297 logged_in=self.logged_in,
298 298 login_available=self.login_available,
299 299 mathjax_url=self.application.ipython_app.mathjax_url,))
300 300
301 301
302 302 class PrintNotebookHandler(AuthenticatedHandler):
303 303
304 304 @authenticate_unless_readonly
305 305 def get(self, notebook_id):
306 306 nbm = self.application.notebook_manager
307 307 project = nbm.notebook_dir
308 308 if not nbm.notebook_exists(notebook_id):
309 309 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
310 310 template = self.application.jinja2_env.get_template('printnotebook.html')
311 311 self.write( template.render(
312 312 project=project,
313 313 notebook_id=notebook_id,
314 314 base_project_url=self.application.ipython_app.base_project_url,
315 315 base_kernel_url=self.application.ipython_app.base_kernel_url,
316 316 kill_kernel=False,
317 317 read_only=self.read_only,
318 318 logged_in=self.logged_in,
319 319 login_available=self.login_available,
320 320 mathjax_url=self.application.ipython_app.mathjax_url,
321 321 ))
322 322
323 323 #-----------------------------------------------------------------------------
324 324 # Kernel handlers
325 325 #-----------------------------------------------------------------------------
326 326
327 327
328 328 class MainKernelHandler(AuthenticatedHandler):
329 329
330 330 @web.authenticated
331 331 def get(self):
332 332 km = self.application.kernel_manager
333 333 self.finish(jsonapi.dumps(km.list_kernel_ids()))
334 334
335 335 @web.authenticated
336 336 def post(self):
337 337 km = self.application.kernel_manager
338 338 nbm = self.application.notebook_manager
339 339 notebook_id = self.get_argument('notebook', default=None)
340 340 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
341 341 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
342 342 self.set_header('Location', '/'+kernel_id)
343 343 self.finish(jsonapi.dumps(data))
344 344
345 345
346 346 class KernelHandler(AuthenticatedHandler):
347 347
348 348 SUPPORTED_METHODS = ('DELETE')
349 349
350 350 @web.authenticated
351 351 def delete(self, kernel_id):
352 352 km = self.application.kernel_manager
353 353 km.shutdown_kernel(kernel_id)
354 354 self.set_status(204)
355 355 self.finish()
356 356
357 357
358 358 class KernelActionHandler(AuthenticatedHandler):
359 359
360 360 @web.authenticated
361 361 def post(self, kernel_id, action):
362 362 km = self.application.kernel_manager
363 363 if action == 'interrupt':
364 364 km.interrupt_kernel(kernel_id)
365 365 self.set_status(204)
366 366 if action == 'restart':
367 367 km.restart_kernel(kernel_id)
368 368 data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
369 369 self.set_header('Location', '/'+kernel_id)
370 370 self.write(jsonapi.dumps(data))
371 371 self.finish()
372 372
373 373
374 374 class ZMQStreamHandler(websocket.WebSocketHandler):
375 375
376 376 def _reserialize_reply(self, msg_list):
377 377 """Reserialize a reply message using JSON.
378 378
379 379 This takes the msg list from the ZMQ socket, unserializes it using
380 380 self.session and then serializes the result using JSON. This method
381 381 should be used by self._on_zmq_reply to build messages that can
382 382 be sent back to the browser.
383 383 """
384 384 idents, msg_list = self.session.feed_identities(msg_list)
385 385 msg = self.session.unserialize(msg_list)
386 386 try:
387 387 msg['header'].pop('date')
388 388 except KeyError:
389 389 pass
390 390 try:
391 391 msg['parent_header'].pop('date')
392 392 except KeyError:
393 393 pass
394 394 msg.pop('buffers')
395 395 return jsonapi.dumps(msg, default=date_default)
396 396
397 397 def _on_zmq_reply(self, msg_list):
398 398 try:
399 399 msg = self._reserialize_reply(msg_list)
400 400 except Exception:
401 401 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
402 402 else:
403 403 self.write_message(msg)
404 404
405 405 def allow_draft76(self):
406 406 """Allow draft 76, until browsers such as Safari update to RFC 6455.
407 407
408 408 This has been disabled by default in tornado in release 2.2.0, and
409 409 support will be removed in later versions.
410 410 """
411 411 return True
412 412
413 413
414 414 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
415 415
416 416 def open(self, kernel_id):
417 417 self.kernel_id = kernel_id.decode('ascii')
418 418 try:
419 419 cfg = self.application.config
420 420 except AttributeError:
421 421 # protect from the case where this is run from something other than
422 422 # the notebook app:
423 423 cfg = None
424 424 self.session = Session(config=cfg)
425 425 self.save_on_message = self.on_message
426 426 self.on_message = self.on_first_message
427 427
428 428 def get_current_user(self):
429 429 user_id = self.get_secure_cookie(self.settings['cookie_name'])
430 430 if user_id == '' or (user_id is None and not self.application.password):
431 431 user_id = 'anonymous'
432 432 return user_id
433 433
434 434 def _inject_cookie_message(self, msg):
435 435 """Inject the first message, which is the document cookie,
436 436 for authentication."""
437 437 if isinstance(msg, unicode):
438 438 # Cookie can't constructor doesn't accept unicode strings for some reason
439 439 msg = msg.encode('utf8', 'replace')
440 440 try:
441 441 self.request._cookies = Cookie.SimpleCookie(msg)
442 442 except:
443 443 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
444 444
445 445 def on_first_message(self, msg):
446 446 self._inject_cookie_message(msg)
447 447 if self.get_current_user() is None:
448 448 logging.warn("Couldn't authenticate WebSocket connection")
449 449 raise web.HTTPError(403)
450 450 self.on_message = self.save_on_message
451 451
452 452
453 453 class IOPubHandler(AuthenticatedZMQStreamHandler):
454 454
455 455 def initialize(self, *args, **kwargs):
456 456 self._kernel_alive = True
457 457 self._beating = False
458 458 self.iopub_stream = None
459 459 self.hb_stream = None
460 460
461 461 def on_first_message(self, msg):
462 462 try:
463 463 super(IOPubHandler, self).on_first_message(msg)
464 464 except web.HTTPError:
465 465 self.close()
466 466 return
467 467 km = self.application.kernel_manager
468 468 self.time_to_dead = km.time_to_dead
469 469 self.first_beat = km.first_beat
470 470 kernel_id = self.kernel_id
471 471 try:
472 472 self.iopub_stream = km.create_iopub_stream(kernel_id)
473 473 self.hb_stream = km.create_hb_stream(kernel_id)
474 474 except web.HTTPError:
475 475 # WebSockets don't response to traditional error codes so we
476 476 # close the connection.
477 477 if not self.stream.closed():
478 478 self.stream.close()
479 479 self.close()
480 480 else:
481 481 self.iopub_stream.on_recv(self._on_zmq_reply)
482 482 self.start_hb(self.kernel_died)
483 483
484 484 def on_message(self, msg):
485 485 pass
486 486
487 487 def on_close(self):
488 488 # This method can be called twice, once by self.kernel_died and once
489 489 # from the WebSocket close event. If the WebSocket connection is
490 490 # closed before the ZMQ streams are setup, they could be None.
491 491 self.stop_hb()
492 492 if self.iopub_stream is not None and not self.iopub_stream.closed():
493 493 self.iopub_stream.on_recv(None)
494 494 self.iopub_stream.close()
495 495 if self.hb_stream is not None and not self.hb_stream.closed():
496 496 self.hb_stream.close()
497 497
498 498 def start_hb(self, callback):
499 499 """Start the heartbeating and call the callback if the kernel dies."""
500 500 if not self._beating:
501 501 self._kernel_alive = True
502 502
503 503 def ping_or_dead():
504 504 self.hb_stream.flush()
505 505 if self._kernel_alive:
506 506 self._kernel_alive = False
507 507 self.hb_stream.send(b'ping')
508 508 # flush stream to force immediate socket send
509 509 self.hb_stream.flush()
510 510 else:
511 511 try:
512 512 callback()
513 513 except:
514 514 pass
515 515 finally:
516 516 self.stop_hb()
517 517
518 518 def beat_received(msg):
519 519 self._kernel_alive = True
520 520
521 521 self.hb_stream.on_recv(beat_received)
522 522 loop = ioloop.IOLoop.instance()
523 523 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
524 524 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
525 525 self._beating= True
526 526
527 527 def _really_start_hb(self):
528 528 """callback for delayed heartbeat start
529 529
530 530 Only start the hb loop if we haven't been closed during the wait.
531 531 """
532 532 if self._beating and not self.hb_stream.closed():
533 533 self._hb_periodic_callback.start()
534 534
535 535 def stop_hb(self):
536 536 """Stop the heartbeating and cancel all related callbacks."""
537 537 if self._beating:
538 538 self._beating = False
539 539 self._hb_periodic_callback.stop()
540 540 if not self.hb_stream.closed():
541 541 self.hb_stream.on_recv(None)
542 542
543 def kernel_died(self):
543 def _delete_kernel_data(self):
544 """Remove the kernel data and notebook mapping."""
544 545 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
545 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
546
547 def kernel_died(self):
548 self._delete_kernel_data()
549 self.application.log.error("Kernel died: %s" % self.kernel_id)
546 550 self.write_message(
547 551 {'header': {'msg_type': 'status'},
548 552 'parent_header': {},
549 553 'content': {'execution_state':'dead'}
550 554 }
551 555 )
552 556 self.on_close()
553 557
554 558
555 559 class ShellHandler(AuthenticatedZMQStreamHandler):
556 560
557 561 def initialize(self, *args, **kwargs):
558 562 self.shell_stream = None
559 563
560 564 def on_first_message(self, msg):
561 565 try:
562 566 super(ShellHandler, self).on_first_message(msg)
563 567 except web.HTTPError:
564 568 self.close()
565 569 return
566 570 km = self.application.kernel_manager
567 571 self.max_msg_size = km.max_msg_size
568 572 kernel_id = self.kernel_id
569 573 try:
570 574 self.shell_stream = km.create_shell_stream(kernel_id)
571 575 except web.HTTPError:
572 576 # WebSockets don't response to traditional error codes so we
573 577 # close the connection.
574 578 if not self.stream.closed():
575 579 self.stream.close()
576 580 self.close()
577 581 else:
578 582 self.shell_stream.on_recv(self._on_zmq_reply)
579 583
580 584 def on_message(self, msg):
581 585 if len(msg) < self.max_msg_size:
582 586 msg = jsonapi.loads(msg)
583 587 self.session.send(self.shell_stream, msg)
584 588
585 589 def on_close(self):
586 590 # Make sure the stream exists and is not already closed.
587 591 if self.shell_stream is not None and not self.shell_stream.closed():
588 592 self.shell_stream.close()
589 593
590 594
591 595 #-----------------------------------------------------------------------------
592 596 # Notebook web service handlers
593 597 #-----------------------------------------------------------------------------
594 598
595 599 class NotebookRootHandler(AuthenticatedHandler):
596 600
597 601 @authenticate_unless_readonly
598 602 def get(self):
599 603 nbm = self.application.notebook_manager
600 604 km = self.application.kernel_manager
601 605 files = nbm.list_notebooks()
602 606 for f in files :
603 607 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
604 608 self.finish(jsonapi.dumps(files))
605 609
606 610 @web.authenticated
607 611 def post(self):
608 612 nbm = self.application.notebook_manager
609 613 body = self.request.body.strip()
610 614 format = self.get_argument('format', default='json')
611 615 name = self.get_argument('name', default=None)
612 616 if body:
613 617 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
614 618 else:
615 619 notebook_id = nbm.new_notebook()
616 620 self.set_header('Location', '/'+notebook_id)
617 621 self.finish(jsonapi.dumps(notebook_id))
618 622
619 623
620 624 class NotebookHandler(AuthenticatedHandler):
621 625
622 626 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
623 627
624 628 @authenticate_unless_readonly
625 629 def get(self, notebook_id):
626 630 nbm = self.application.notebook_manager
627 631 format = self.get_argument('format', default='json')
628 632 last_mod, name, data = nbm.get_notebook(notebook_id, format)
629 633
630 634 if format == u'json':
631 635 self.set_header('Content-Type', 'application/json')
632 636 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
633 637 elif format == u'py':
634 638 self.set_header('Content-Type', 'application/x-python')
635 639 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
636 640 self.set_header('Last-Modified', last_mod)
637 641 self.finish(data)
638 642
639 643 @web.authenticated
640 644 def put(self, notebook_id):
641 645 nbm = self.application.notebook_manager
642 646 format = self.get_argument('format', default='json')
643 647 name = self.get_argument('name', default=None)
644 648 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
645 649 self.set_status(204)
646 650 self.finish()
647 651
648 652 @web.authenticated
649 653 def delete(self, notebook_id):
650 654 nbm = self.application.notebook_manager
651 655 nbm.delete_notebook(notebook_id)
652 656 self.set_status(204)
653 657 self.finish()
654 658
655 659
656 660 class NotebookCopyHandler(AuthenticatedHandler):
657 661
658 662 @web.authenticated
659 663 def get(self, notebook_id):
660 664 nbm = self.application.notebook_manager
661 665 project = nbm.notebook_dir
662 666 notebook_id = nbm.copy_notebook(notebook_id)
663 667 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
664 668
665 669
666 670 #-----------------------------------------------------------------------------
667 671 # Cluster handlers
668 672 #-----------------------------------------------------------------------------
669 673
670 674
671 675 class MainClusterHandler(AuthenticatedHandler):
672 676
673 677 @web.authenticated
674 678 def get(self):
675 679 cm = self.application.cluster_manager
676 680 self.finish(jsonapi.dumps(cm.list_profiles()))
677 681
678 682
679 683 class ClusterProfileHandler(AuthenticatedHandler):
680 684
681 685 @web.authenticated
682 686 def get(self, profile):
683 687 cm = self.application.cluster_manager
684 688 self.finish(jsonapi.dumps(cm.profile_info(profile)))
685 689
686 690
687 691 class ClusterActionHandler(AuthenticatedHandler):
688 692
689 693 @web.authenticated
690 694 def post(self, profile, action):
691 695 cm = self.application.cluster_manager
692 696 if action == 'start':
693 697 n = self.get_argument('n',default=None)
694 698 if n is None:
695 699 data = cm.start_cluster(profile)
696 700 else:
697 701 data = cm.start_cluster(profile,int(n))
698 702 if action == 'stop':
699 703 data = cm.stop_cluster(profile)
700 704 self.finish(jsonapi.dumps(data))
701 705
702 706
703 707 #-----------------------------------------------------------------------------
704 708 # RST web service handlers
705 709 #-----------------------------------------------------------------------------
706 710
707 711
708 712 class RSTHandler(AuthenticatedHandler):
709 713
710 714 @web.authenticated
711 715 def post(self):
712 716 if publish_string is None:
713 717 raise web.HTTPError(503, u'docutils not available')
714 718 body = self.request.body.strip()
715 719 source = body
716 720 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
717 721 defaults = {'file_insertion_enabled': 0,
718 722 'raw_enabled': 0,
719 723 '_disable_config': 1,
720 724 'stylesheet_path': 0
721 725 # 'template': template_path
722 726 }
723 727 try:
724 728 html = publish_string(source, writer_name='html',
725 729 settings_overrides=defaults
726 730 )
727 731 except:
728 732 raise web.HTTPError(400, u'Invalid RST')
729 733 print html
730 734 self.set_header('Content-Type', 'text/html')
731 735 self.finish(html)
732 736
733 737 # to minimize subclass changes:
734 738 HTTPError = web.HTTPError
735 739
736 740 class FileFindHandler(web.StaticFileHandler):
737 741 """subclass of StaticFileHandler for serving files from a search path"""
738 742
739 743 _static_paths = {}
740 744 # _lock is needed for tornado < 2.2.0 compat
741 745 _lock = threading.Lock() # protects _static_hashes
742 746
743 747 def initialize(self, path, default_filename=None):
744 748 if isinstance(path, basestring):
745 749 path = [path]
746 750 self.roots = tuple(
747 751 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
748 752 )
749 753 self.default_filename = default_filename
750 754
751 755 @classmethod
752 756 def locate_file(cls, path, roots):
753 757 """locate a file to serve on our static file search path"""
754 758 with cls._lock:
755 759 if path in cls._static_paths:
756 760 return cls._static_paths[path]
757 761 try:
758 762 abspath = os.path.abspath(filefind(path, roots))
759 763 except IOError:
760 764 # empty string should always give exists=False
761 765 return ''
762 766
763 767 # os.path.abspath strips a trailing /
764 768 # it needs to be temporarily added back for requests to root/
765 769 if not (abspath + os.path.sep).startswith(roots):
766 770 raise HTTPError(403, "%s is not in root static directory", path)
767 771
768 772 cls._static_paths[path] = abspath
769 773 return abspath
770 774
771 775 def get(self, path, include_body=True):
772 776 path = self.parse_url_path(path)
773 777
774 778 # begin subclass override
775 779 abspath = self.locate_file(path, self.roots)
776 780 # end subclass override
777 781
778 782 if os.path.isdir(abspath) and self.default_filename is not None:
779 783 # need to look at the request.path here for when path is empty
780 784 # but there is some prefix to the path that was already
781 785 # trimmed by the routing
782 786 if not self.request.path.endswith("/"):
783 787 self.redirect(self.request.path + "/")
784 788 return
785 789 abspath = os.path.join(abspath, self.default_filename)
786 790 if not os.path.exists(abspath):
787 791 raise HTTPError(404)
788 792 if not os.path.isfile(abspath):
789 793 raise HTTPError(403, "%s is not a file", path)
790 794
791 795 stat_result = os.stat(abspath)
792 796 modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
793 797
794 798 self.set_header("Last-Modified", modified)
795 799
796 800 mime_type, encoding = mimetypes.guess_type(abspath)
797 801 if mime_type:
798 802 self.set_header("Content-Type", mime_type)
799 803
800 804 cache_time = self.get_cache_time(path, modified, mime_type)
801 805
802 806 if cache_time > 0:
803 807 self.set_header("Expires", datetime.datetime.utcnow() + \
804 808 datetime.timedelta(seconds=cache_time))
805 809 self.set_header("Cache-Control", "max-age=" + str(cache_time))
806 810 else:
807 811 self.set_header("Cache-Control", "public")
808 812
809 813 self.set_extra_headers(path)
810 814
811 815 # Check the If-Modified-Since, and don't send the result if the
812 816 # content has not been modified
813 817 ims_value = self.request.headers.get("If-Modified-Since")
814 818 if ims_value is not None:
815 819 date_tuple = email.utils.parsedate(ims_value)
816 820 if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
817 821 if if_since >= modified:
818 822 self.set_status(304)
819 823 return
820 824
821 825 with open(abspath, "rb") as file:
822 826 data = file.read()
823 827 hasher = hashlib.sha1()
824 828 hasher.update(data)
825 829 self.set_header("Etag", '"%s"' % hasher.hexdigest())
826 830 if include_body:
827 831 self.write(data)
828 832 else:
829 833 assert self.request.method == "HEAD"
830 834 self.set_header("Content-Length", len(data))
831 835
832 836 @classmethod
833 837 def get_version(cls, settings, path):
834 838 """Generate the version string to be used in static URLs.
835 839
836 840 This method may be overridden in subclasses (but note that it
837 841 is a class method rather than a static method). The default
838 842 implementation uses a hash of the file's contents.
839 843
840 844 ``settings`` is the `Application.settings` dictionary and ``path``
841 845 is the relative location of the requested asset on the filesystem.
842 846 The returned value should be a string, or ``None`` if no version
843 847 could be determined.
844 848 """
845 849 # begin subclass override:
846 850 static_paths = settings['static_path']
847 851 if isinstance(static_paths, basestring):
848 852 static_paths = [static_paths]
849 853 roots = tuple(
850 854 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
851 855 )
852 856
853 857 try:
854 858 abs_path = filefind(path, roots)
855 859 except IOError:
856 860 logging.error("Could not find static file %r", path)
857 861 return None
858 862
859 863 # end subclass override
860 864
861 865 with cls._lock:
862 866 hashes = cls._static_hashes
863 867 if abs_path not in hashes:
864 868 try:
865 869 f = open(abs_path, "rb")
866 870 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
867 871 f.close()
868 872 except Exception:
869 873 logging.error("Could not open static file %r", path)
870 874 hashes[abs_path] = None
871 875 hsh = hashes.get(abs_path)
872 876 if hsh:
873 877 return hsh[:5]
874 878 return None
875 879
876 880
877 881 def parse_url_path(self, url_path):
878 882 """Converts a static URL path into a filesystem path.
879 883
880 884 ``url_path`` is the path component of the URL with
881 885 ``static_url_prefix`` removed. The return value should be
882 886 filesystem path relative to ``static_path``.
883 887 """
884 888 if os.path.sep != "/":
885 889 url_path = url_path.replace("/", os.path.sep)
886 890 return url_path
887 891
888 892
General Comments 0
You need to be logged in to leave comments. Login now