##// END OF EJS Templates
add ModifyCheckpoints handler...
MinRK -
Show More
@@ -1,925 +1,928 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
31 31 from tornado.escape import url_escape
32 32 from tornado import web
33 33 from tornado import websocket
34 34
35 35 try:
36 36 from tornado.log import app_log
37 37 except ImportError:
38 38 app_log = logging.getLogger()
39 39
40 40 from zmq.eventloop import ioloop
41 41 from zmq.utils import jsonapi
42 42
43 43 from IPython.config import Application
44 44 from IPython.external.decorator import decorator
45 45 from IPython.kernel.zmq.session import Session
46 46 from IPython.lib.security import passwd_check
47 47 from IPython.utils.jsonutil import date_default
48 48 from IPython.utils.path import filefind
49 49 from IPython.utils.py3compat import PY3
50 50
51 51 try:
52 52 from docutils.core import publish_string
53 53 except ImportError:
54 54 publish_string = None
55 55
56 56 #-----------------------------------------------------------------------------
57 57 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
58 58 #-----------------------------------------------------------------------------
59 59
60 60 # Google Chrome, as of release 16, changed its websocket protocol number. The
61 61 # parts tornado cares about haven't really changed, so it's OK to continue
62 62 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
63 63 # version as of Oct 30/2011) the version check fails, see the issue report:
64 64
65 65 # https://github.com/facebook/tornado/issues/385
66 66
67 67 # This issue has been fixed in Tornado post 2.1.1:
68 68
69 69 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
70 70
71 71 # Here we manually apply the same patch as above so that users of IPython can
72 72 # continue to work with an officially released Tornado. We make the
73 73 # monkeypatch version check as narrow as possible to limit its effects; once
74 74 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
75 75
76 76 import tornado
77 77
78 78 if tornado.version_info <= (2,1,1):
79 79
80 80 def _execute(self, transforms, *args, **kwargs):
81 81 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
82 82
83 83 self.open_args = args
84 84 self.open_kwargs = kwargs
85 85
86 86 # The difference between version 8 and 13 is that in 8 the
87 87 # client sends a "Sec-Websocket-Origin" header and in 13 it's
88 88 # simply "Origin".
89 89 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
90 90 self.ws_connection = WebSocketProtocol8(self)
91 91 self.ws_connection.accept_connection()
92 92
93 93 elif self.request.headers.get("Sec-WebSocket-Version"):
94 94 self.stream.write(tornado.escape.utf8(
95 95 "HTTP/1.1 426 Upgrade Required\r\n"
96 96 "Sec-WebSocket-Version: 8\r\n\r\n"))
97 97 self.stream.close()
98 98
99 99 else:
100 100 self.ws_connection = WebSocketProtocol76(self)
101 101 self.ws_connection.accept_connection()
102 102
103 103 websocket.WebSocketHandler._execute = _execute
104 104 del _execute
105 105
106 106 #-----------------------------------------------------------------------------
107 107 # Decorator for disabling read-only handlers
108 108 #-----------------------------------------------------------------------------
109 109
110 110 @decorator
111 111 def not_if_readonly(f, self, *args, **kwargs):
112 112 if self.settings.get('read_only', False):
113 113 raise web.HTTPError(403, "Notebook server is read-only")
114 114 else:
115 115 return f(self, *args, **kwargs)
116 116
117 117 @decorator
118 118 def authenticate_unless_readonly(f, self, *args, **kwargs):
119 119 """authenticate this page *unless* readonly view is active.
120 120
121 121 In read-only mode, the notebook list and print view should
122 122 be accessible without authentication.
123 123 """
124 124
125 125 @web.authenticated
126 126 def auth_f(self, *args, **kwargs):
127 127 return f(self, *args, **kwargs)
128 128
129 129 if self.settings.get('read_only', False):
130 130 return f(self, *args, **kwargs)
131 131 else:
132 132 return auth_f(self, *args, **kwargs)
133 133
134 134 def urljoin(*pieces):
135 135 """Join components of url into a relative url
136 136
137 137 Use to prevent double slash when joining subpath
138 138 """
139 139 striped = [s.strip('/') for s in pieces]
140 140 return '/'.join(s for s in striped if s)
141 141
142 142 #-----------------------------------------------------------------------------
143 143 # Top-level handlers
144 144 #-----------------------------------------------------------------------------
145 145
146 146 class RequestHandler(web.RequestHandler):
147 147 """RequestHandler with default variable setting."""
148 148
149 149 def render(*args, **kwargs):
150 150 kwargs.setdefault('message', '')
151 151 return web.RequestHandler.render(*args, **kwargs)
152 152
153 153 class AuthenticatedHandler(RequestHandler):
154 154 """A RequestHandler with an authenticated user."""
155 155
156 156 def clear_login_cookie(self):
157 157 self.clear_cookie(self.cookie_name)
158 158
159 159 def get_current_user(self):
160 160 user_id = self.get_secure_cookie(self.cookie_name)
161 161 # For now the user_id should not return empty, but it could eventually
162 162 if user_id == '':
163 163 user_id = 'anonymous'
164 164 if user_id is None:
165 165 # prevent extra Invalid cookie sig warnings:
166 166 self.clear_login_cookie()
167 167 if not self.read_only and not self.login_available:
168 168 user_id = 'anonymous'
169 169 return user_id
170 170
171 171 @property
172 172 def cookie_name(self):
173 173 return self.settings.get('cookie_name', '')
174 174
175 175 @property
176 176 def password(self):
177 177 """our password"""
178 178 return self.settings.get('password', '')
179 179
180 180 @property
181 181 def logged_in(self):
182 182 """Is a user currently logged in?
183 183
184 184 """
185 185 user = self.get_current_user()
186 186 return (user and not user == 'anonymous')
187 187
188 188 @property
189 189 def login_available(self):
190 190 """May a user proceed to log in?
191 191
192 192 This returns True if login capability is available, irrespective of
193 193 whether the user is already logged in or not.
194 194
195 195 """
196 196 return bool(self.settings.get('password', ''))
197 197
198 198 @property
199 199 def read_only(self):
200 200 """Is the notebook read-only?
201 201
202 202 """
203 203 return self.settings.get('read_only', False)
204 204
205 205
206 206 class IPythonHandler(AuthenticatedHandler):
207 207 """IPython-specific extensions to authenticated handling
208 208
209 209 Mostly property shortcuts to IPython-specific settings.
210 210 """
211 211
212 212 @property
213 213 def config(self):
214 214 return self.settings.get('config', None)
215 215
216 216 @property
217 217 def log(self):
218 218 """use the IPython log by default, falling back on tornado's logger"""
219 219 if Application.initialized():
220 220 return Application.instance().log
221 221 else:
222 222 return app_log
223 223
224 224 @property
225 225 def use_less(self):
226 226 """Use less instead of css in templates"""
227 227 return self.settings.get('use_less', False)
228 228
229 229 #---------------------------------------------------------------
230 230 # URLs
231 231 #---------------------------------------------------------------
232 232
233 233 @property
234 234 def ws_url(self):
235 235 """websocket url matching the current request
236 236
237 237 turns http[s]://host[:port] into
238 238 ws[s]://host[:port]
239 239 """
240 240 proto = self.request.protocol.replace('http', 'ws')
241 241 host = self.settings.get('websocket_host', '')
242 242 # default to config value
243 243 if host == '':
244 244 host = self.request.host # get from request
245 245 return "%s://%s" % (proto, host)
246 246
247 247 @property
248 248 def mathjax_url(self):
249 249 return self.settings.get('mathjax_url', '')
250 250
251 251 @property
252 252 def base_project_url(self):
253 253 return self.settings.get('base_project_url', '/')
254 254
255 255 @property
256 256 def base_kernel_url(self):
257 257 return self.settings.get('base_kernel_url', '/')
258 258
259 259 #---------------------------------------------------------------
260 260 # Manager objects
261 261 #---------------------------------------------------------------
262 262
263 263 @property
264 264 def kernel_manager(self):
265 265 return self.settings['kernel_manager']
266 266
267 267 @property
268 268 def notebook_manager(self):
269 269 return self.settings['notebook_manager']
270 270
271 271 @property
272 272 def cluster_manager(self):
273 273 return self.settings['cluster_manager']
274 274
275 275 @property
276 276 def project(self):
277 277 return self.notebook_manager.notebook_dir
278 278
279 279 #---------------------------------------------------------------
280 280 # template rendering
281 281 #---------------------------------------------------------------
282 282
283 283 def get_template(self, name):
284 284 """Return the jinja template object for a given name"""
285 285 return self.settings['jinja2_env'].get_template(name)
286 286
287 287 def render_template(self, name, **ns):
288 288 ns.update(self.template_namespace)
289 289 template = self.get_template(name)
290 290 return template.render(**ns)
291 291
292 292 @property
293 293 def template_namespace(self):
294 294 return dict(
295 295 base_project_url=self.base_project_url,
296 296 base_kernel_url=self.base_kernel_url,
297 297 read_only=self.read_only,
298 298 logged_in=self.logged_in,
299 299 login_available=self.login_available,
300 300 use_less=self.use_less,
301 301 )
302 302
303 303 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
304 304 """static files should only be accessible when logged in"""
305 305
306 306 @authenticate_unless_readonly
307 307 def get(self, path):
308 308 return web.StaticFileHandler.get(self, path)
309 309
310 310
311 311 class ProjectDashboardHandler(IPythonHandler):
312 312
313 313 @authenticate_unless_readonly
314 314 def get(self):
315 315 self.write(self.render_template('projectdashboard.html',
316 316 project=self.project,
317 317 project_component=self.project.split('/'),
318 318 ))
319 319
320 320
321 321 class LoginHandler(IPythonHandler):
322 322
323 323 def _render(self, message=None):
324 324 self.write(self.render_template('login.html',
325 325 next=url_escape(self.get_argument('next', default=self.base_project_url)),
326 326 message=message,
327 327 ))
328 328
329 329 def get(self):
330 330 if self.current_user:
331 331 self.redirect(self.get_argument('next', default=self.base_project_url))
332 332 else:
333 333 self._render()
334 334
335 335 def post(self):
336 336 pwd = self.get_argument('password', default=u'')
337 337 if self.login_available:
338 338 if passwd_check(self.password, pwd):
339 339 self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
340 340 else:
341 341 self._render(message={'error': 'Invalid password'})
342 342 return
343 343
344 344 self.redirect(self.get_argument('next', default=self.base_project_url))
345 345
346 346
347 347 class LogoutHandler(IPythonHandler):
348 348
349 349 def get(self):
350 350 self.clear_login_cookie()
351 351 if self.login_available:
352 352 message = {'info': 'Successfully logged out.'}
353 353 else:
354 354 message = {'warning': 'Cannot log out. Notebook authentication '
355 355 'is disabled.'}
356 356 self.write(self.render_template('logout.html',
357 357 message=message))
358 358
359 359
360 360 class NewHandler(IPythonHandler):
361 361
362 362 @web.authenticated
363 363 def get(self):
364 364 notebook_id = self.notebook_manager.new_notebook()
365 365 self.redirect('/' + urljoin(self.base_project_url, notebook_id))
366 366
367 367 class NamedNotebookHandler(IPythonHandler):
368 368
369 369 @authenticate_unless_readonly
370 370 def get(self, notebook_id):
371 371 nbm = self.notebook_manager
372 372 if not nbm.notebook_exists(notebook_id):
373 373 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
374 374 self.write(self.render_template('notebook.html',
375 375 project=self.project,
376 376 notebook_id=notebook_id,
377 377 kill_kernel=False,
378 378 mathjax_url=self.mathjax_url,
379 379 )
380 380 )
381 381
382 382
383 383 #-----------------------------------------------------------------------------
384 384 # Kernel handlers
385 385 #-----------------------------------------------------------------------------
386 386
387 387
388 388 class MainKernelHandler(IPythonHandler):
389 389
390 390 @web.authenticated
391 391 def get(self):
392 392 km = self.kernel_manager
393 393 self.finish(jsonapi.dumps(km.list_kernel_ids()))
394 394
395 395 @web.authenticated
396 396 def post(self):
397 397 km = self.kernel_manager
398 398 nbm = self.notebook_manager
399 399 notebook_id = self.get_argument('notebook', default=None)
400 400 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
401 401 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
402 402 self.set_header('Location', '/'+kernel_id)
403 403 self.finish(jsonapi.dumps(data))
404 404
405 405
406 406 class KernelHandler(IPythonHandler):
407 407
408 408 SUPPORTED_METHODS = ('DELETE')
409 409
410 410 @web.authenticated
411 411 def delete(self, kernel_id):
412 412 km = self.kernel_manager
413 413 km.shutdown_kernel(kernel_id)
414 414 self.set_status(204)
415 415 self.finish()
416 416
417 417
418 418 class KernelActionHandler(IPythonHandler):
419 419
420 420 @web.authenticated
421 421 def post(self, kernel_id, action):
422 422 km = self.kernel_manager
423 423 if action == 'interrupt':
424 424 km.interrupt_kernel(kernel_id)
425 425 self.set_status(204)
426 426 if action == 'restart':
427 427 km.restart_kernel(kernel_id)
428 428 data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
429 429 self.set_header('Location', '/'+kernel_id)
430 430 self.write(jsonapi.dumps(data))
431 431 self.finish()
432 432
433 433
434 434 class ZMQStreamHandler(websocket.WebSocketHandler):
435 435
436 436 def clear_cookie(self, *args, **kwargs):
437 437 """meaningless for websockets"""
438 438 pass
439 439
440 440 def _reserialize_reply(self, msg_list):
441 441 """Reserialize a reply message using JSON.
442 442
443 443 This takes the msg list from the ZMQ socket, unserializes it using
444 444 self.session and then serializes the result using JSON. This method
445 445 should be used by self._on_zmq_reply to build messages that can
446 446 be sent back to the browser.
447 447 """
448 448 idents, msg_list = self.session.feed_identities(msg_list)
449 449 msg = self.session.unserialize(msg_list)
450 450 try:
451 451 msg['header'].pop('date')
452 452 except KeyError:
453 453 pass
454 454 try:
455 455 msg['parent_header'].pop('date')
456 456 except KeyError:
457 457 pass
458 458 msg.pop('buffers')
459 459 return jsonapi.dumps(msg, default=date_default)
460 460
461 461 def _on_zmq_reply(self, msg_list):
462 462 # Sometimes this gets triggered when the on_close method is scheduled in the
463 463 # eventloop but hasn't been called.
464 464 if self.stream.closed(): return
465 465 try:
466 466 msg = self._reserialize_reply(msg_list)
467 467 except Exception:
468 468 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
469 469 else:
470 470 self.write_message(msg)
471 471
472 472 def allow_draft76(self):
473 473 """Allow draft 76, until browsers such as Safari update to RFC 6455.
474 474
475 475 This has been disabled by default in tornado in release 2.2.0, and
476 476 support will be removed in later versions.
477 477 """
478 478 return True
479 479
480 480
481 481 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
482 482
483 483 def open(self, kernel_id):
484 484 self.kernel_id = kernel_id.decode('ascii')
485 485 self.session = Session(config=self.config)
486 486 self.save_on_message = self.on_message
487 487 self.on_message = self.on_first_message
488 488
489 489 def _inject_cookie_message(self, msg):
490 490 """Inject the first message, which is the document cookie,
491 491 for authentication."""
492 492 if not PY3 and isinstance(msg, unicode):
493 493 # Cookie constructor doesn't accept unicode strings
494 494 # under Python 2.x for some reason
495 495 msg = msg.encode('utf8', 'replace')
496 496 try:
497 497 identity, msg = msg.split(':', 1)
498 498 self.session.session = identity.decode('ascii')
499 499 except Exception:
500 500 logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
501 501
502 502 try:
503 503 self.request._cookies = Cookie.SimpleCookie(msg)
504 504 except:
505 505 self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
506 506
507 507 def on_first_message(self, msg):
508 508 self._inject_cookie_message(msg)
509 509 if self.get_current_user() is None:
510 510 self.log.warn("Couldn't authenticate WebSocket connection")
511 511 raise web.HTTPError(403)
512 512 self.on_message = self.save_on_message
513 513
514 514
515 515 class ZMQChannelHandler(AuthenticatedZMQStreamHandler):
516 516
517 517 @property
518 518 def max_msg_size(self):
519 519 return self.settings.get('max_msg_size', 65535)
520 520
521 521 def create_stream(self):
522 522 km = self.kernel_manager
523 523 meth = getattr(km, 'connect_%s' % self.channel)
524 524 self.zmq_stream = meth(self.kernel_id, identity=self.session.bsession)
525 525
526 526 def initialize(self, *args, **kwargs):
527 527 self.zmq_stream = None
528 528
529 529 def on_first_message(self, msg):
530 530 try:
531 531 super(ZMQChannelHandler, self).on_first_message(msg)
532 532 except web.HTTPError:
533 533 self.close()
534 534 return
535 535 try:
536 536 self.create_stream()
537 537 except web.HTTPError:
538 538 # WebSockets don't response to traditional error codes so we
539 539 # close the connection.
540 540 if not self.stream.closed():
541 541 self.stream.close()
542 542 self.close()
543 543 else:
544 544 self.zmq_stream.on_recv(self._on_zmq_reply)
545 545
546 546 def on_message(self, msg):
547 547 if len(msg) < self.max_msg_size:
548 548 msg = jsonapi.loads(msg)
549 549 self.session.send(self.zmq_stream, msg)
550 550
551 551 def on_close(self):
552 552 # This method can be called twice, once by self.kernel_died and once
553 553 # from the WebSocket close event. If the WebSocket connection is
554 554 # closed before the ZMQ streams are setup, they could be None.
555 555 if self.zmq_stream is not None and not self.zmq_stream.closed():
556 556 self.zmq_stream.on_recv(None)
557 557 self.zmq_stream.close()
558 558
559 559
560 560 class IOPubHandler(ZMQChannelHandler):
561 561 channel = 'iopub'
562 562
563 563 def create_stream(self):
564 564 super(IOPubHandler, self).create_stream()
565 565 km = self.kernel_manager
566 566 km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
567 567 km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
568 568
569 569 def on_close(self):
570 570 km = self.kernel_manager
571 571 if self.kernel_id in km:
572 572 km.remove_restart_callback(
573 573 self.kernel_id, self.on_kernel_restarted,
574 574 )
575 575 km.remove_restart_callback(
576 576 self.kernel_id, self.on_restart_failed, 'dead',
577 577 )
578 578 super(IOPubHandler, self).on_close()
579 579
580 580 def _send_status_message(self, status):
581 581 msg = self.session.msg("status",
582 582 {'execution_state': status}
583 583 )
584 584 self.write_message(jsonapi.dumps(msg, default=date_default))
585 585
586 586 def on_kernel_restarted(self):
587 587 logging.warn("kernel %s restarted", self.kernel_id)
588 588 self._send_status_message('restarting')
589 589
590 590 def on_restart_failed(self):
591 591 logging.error("kernel %s restarted failed!", self.kernel_id)
592 592 self._send_status_message('dead')
593 593
594 594 def on_message(self, msg):
595 595 """IOPub messages make no sense"""
596 596 pass
597 597
598 598 class ShellHandler(ZMQChannelHandler):
599 599 channel = 'shell'
600 600
601 601 class StdinHandler(ZMQChannelHandler):
602 602 channel = 'stdin'
603 603
604 604
605 605 #-----------------------------------------------------------------------------
606 606 # Notebook web service handlers
607 607 #-----------------------------------------------------------------------------
608 608
609 609 class NotebookRedirectHandler(IPythonHandler):
610 610
611 611 @authenticate_unless_readonly
612 612 def get(self, notebook_name):
613 613 # strip trailing .ipynb:
614 614 notebook_name = os.path.splitext(notebook_name)[0]
615 615 notebook_id = self.notebook_manager.rev_mapping.get(notebook_name, '')
616 616 if notebook_id:
617 617 url = self.settings.get('base_project_url', '/') + notebook_id
618 618 return self.redirect(url)
619 619 else:
620 620 raise HTTPError(404)
621 621
622 622
623 623 class NotebookRootHandler(IPythonHandler):
624 624
625 625 @authenticate_unless_readonly
626 626 def get(self):
627 627 nbm = self.notebook_manager
628 628 km = self.kernel_manager
629 629 files = nbm.list_notebooks()
630 630 for f in files :
631 631 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
632 632 self.finish(jsonapi.dumps(files))
633 633
634 634 @web.authenticated
635 635 def post(self):
636 636 nbm = self.notebook_manager
637 637 body = self.request.body.strip()
638 638 format = self.get_argument('format', default='json')
639 639 name = self.get_argument('name', default=None)
640 640 if body:
641 641 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
642 642 else:
643 643 notebook_id = nbm.new_notebook()
644 644 self.set_header('Location', '/'+notebook_id)
645 645 self.finish(jsonapi.dumps(notebook_id))
646 646
647 647
648 648 class NotebookHandler(IPythonHandler):
649 649
650 650 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
651 651
652 652 @authenticate_unless_readonly
653 653 def get(self, notebook_id):
654 654 nbm = self.notebook_manager
655 655 format = self.get_argument('format', default='json')
656 656 last_mod, name, data = nbm.get_notebook(notebook_id, format)
657 657
658 658 if format == u'json':
659 659 self.set_header('Content-Type', 'application/json')
660 660 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
661 661 elif format == u'py':
662 662 self.set_header('Content-Type', 'application/x-python')
663 663 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
664 664 self.set_header('Last-Modified', last_mod)
665 665 self.finish(data)
666 666
667 667 @web.authenticated
668 668 def put(self, notebook_id):
669 669 nbm = self.notebook_manager
670 670 format = self.get_argument('format', default='json')
671 671 name = self.get_argument('name', default=None)
672 672 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
673 673 self.set_status(204)
674 674 self.finish()
675 675
676 676 @web.authenticated
677 677 def delete(self, notebook_id):
678 678 self.notebook_manager.delete_notebook(notebook_id)
679 679 self.set_status(204)
680 680 self.finish()
681 681
682 class NotebookCheckpointHandler(AuthenticatedHandler):
683 682
684 SUPPORTED_METHODS = ('GET', 'POST', 'PUT', 'DELETE')
683 class NotebookCheckpointsHandler(AuthenticatedHandler):
684
685 SUPPORTED_METHODS = ('GET', 'POST')
685 686
686 687 @web.authenticated
687 688 def get(self, notebook_id):
688 689 """get lists checkpoints for a notebook"""
689 690 nbm = self.application.notebook_manager
690 691 checkpoints = nbm.list_checkpoints(notebook_id)
691 self.finish(checkpoints)
692 data = jsonapi.dumps(checkpoints, default=date_default)
693 self.finish(data)
692 694
693 695 @web.authenticated
694 696 def post(self, notebook_id):
697 """post creates a new checkpoint"""
698 nbm = self.application.notebook_manager
699 checkpoint = nbm.create_checkpoint(notebook_id)
700 data = jsonapi.dumps(checkpoint, default=date_default)
701 self.finish(data)
702
703
704 class ModifyNotebookCheckpointsHandler(AuthenticatedHandler):
705
706 SUPPORTED_METHODS = ('POST', 'DELETE')
707
708 @web.authenticated
709 def post(self, notebook_id, checkpoint_id):
695 710 """post restores a notebook from a checkpoint"""
696 711 nbm = self.application.notebook_manager
697 checkpoint_id = self.get_argument('checkpoint', None)
698 712 nbm.restore_checkpoint(notebook_id, checkpoint_id)
699 713 self.set_status(204)
700 714 self.finish()
701 715
702 716 @web.authenticated
703 def put(self, notebook_id):
704 """put saves the notebook, and creates a new checkpoint"""
705 nbm = self.application.notebook_manager
706 format = self.get_argument('format', default='json')
707 name = self.get_argument('name', default=None)
708 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
709 checkpoint = nbm.create_checkpoint(notebook_id)
710 self.finish(checkpoint)
711
712 @web.authenticated
713 def delete(self, notebook_id):
717 def delete(self, notebook_id, checkpoint_id):
714 718 """delete clears a checkpoint for a given notebook"""
715 719 nbm = self.application.notebook_manager
716 checkpoint_id = self.get_argument('checkpoint', None)
717 720 nbm.delte_checkpoint(notebook_id, checkpoint_id)
718 721 self.set_status(204)
719 722 self.finish()
720 723
721 724
722 725
723 726 class NotebookCopyHandler(IPythonHandler):
724 727
725 728 @web.authenticated
726 729 def get(self, notebook_id):
727 730 notebook_id = self.notebook_manager.copy_notebook(notebook_id)
728 731 self.redirect('/'+urljoin(self.base_project_url, notebook_id))
729 732
730 733
731 734 #-----------------------------------------------------------------------------
732 735 # Cluster handlers
733 736 #-----------------------------------------------------------------------------
734 737
735 738
736 739 class MainClusterHandler(IPythonHandler):
737 740
738 741 @web.authenticated
739 742 def get(self):
740 743 self.finish(jsonapi.dumps(self.cluster_manager.list_profiles()))
741 744
742 745
743 746 class ClusterProfileHandler(IPythonHandler):
744 747
745 748 @web.authenticated
746 749 def get(self, profile):
747 750 self.finish(jsonapi.dumps(self.cluster_manager.profile_info(profile)))
748 751
749 752
750 753 class ClusterActionHandler(IPythonHandler):
751 754
752 755 @web.authenticated
753 756 def post(self, profile, action):
754 757 cm = self.cluster_manager
755 758 if action == 'start':
756 759 n = self.get_argument('n',default=None)
757 760 if n is None:
758 761 data = cm.start_cluster(profile)
759 762 else:
760 763 data = cm.start_cluster(profile, int(n))
761 764 if action == 'stop':
762 765 data = cm.stop_cluster(profile)
763 766 self.finish(jsonapi.dumps(data))
764 767
765 768
766 769 #-----------------------------------------------------------------------------
767 770 # File handler
768 771 #-----------------------------------------------------------------------------
769 772
770 773 # to minimize subclass changes:
771 774 HTTPError = web.HTTPError
772 775
773 776 class FileFindHandler(web.StaticFileHandler):
774 777 """subclass of StaticFileHandler for serving files from a search path"""
775 778
776 779 _static_paths = {}
777 780 # _lock is needed for tornado < 2.2.0 compat
778 781 _lock = threading.Lock() # protects _static_hashes
779 782
780 783 def initialize(self, path, default_filename=None):
781 784 if isinstance(path, basestring):
782 785 path = [path]
783 786 self.roots = tuple(
784 787 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
785 788 )
786 789 self.default_filename = default_filename
787 790
788 791 @classmethod
789 792 def locate_file(cls, path, roots):
790 793 """locate a file to serve on our static file search path"""
791 794 with cls._lock:
792 795 if path in cls._static_paths:
793 796 return cls._static_paths[path]
794 797 try:
795 798 abspath = os.path.abspath(filefind(path, roots))
796 799 except IOError:
797 800 # empty string should always give exists=False
798 801 return ''
799 802
800 803 # os.path.abspath strips a trailing /
801 804 # it needs to be temporarily added back for requests to root/
802 805 if not (abspath + os.path.sep).startswith(roots):
803 806 raise HTTPError(403, "%s is not in root static directory", path)
804 807
805 808 cls._static_paths[path] = abspath
806 809 return abspath
807 810
808 811 def get(self, path, include_body=True):
809 812 path = self.parse_url_path(path)
810 813
811 814 # begin subclass override
812 815 abspath = self.locate_file(path, self.roots)
813 816 # end subclass override
814 817
815 818 if os.path.isdir(abspath) and self.default_filename is not None:
816 819 # need to look at the request.path here for when path is empty
817 820 # but there is some prefix to the path that was already
818 821 # trimmed by the routing
819 822 if not self.request.path.endswith("/"):
820 823 self.redirect(self.request.path + "/")
821 824 return
822 825 abspath = os.path.join(abspath, self.default_filename)
823 826 if not os.path.exists(abspath):
824 827 raise HTTPError(404)
825 828 if not os.path.isfile(abspath):
826 829 raise HTTPError(403, "%s is not a file", path)
827 830
828 831 stat_result = os.stat(abspath)
829 832 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
830 833
831 834 self.set_header("Last-Modified", modified)
832 835
833 836 mime_type, encoding = mimetypes.guess_type(abspath)
834 837 if mime_type:
835 838 self.set_header("Content-Type", mime_type)
836 839
837 840 cache_time = self.get_cache_time(path, modified, mime_type)
838 841
839 842 if cache_time > 0:
840 843 self.set_header("Expires", datetime.datetime.utcnow() + \
841 844 datetime.timedelta(seconds=cache_time))
842 845 self.set_header("Cache-Control", "max-age=" + str(cache_time))
843 846 else:
844 847 self.set_header("Cache-Control", "public")
845 848
846 849 self.set_extra_headers(path)
847 850
848 851 # Check the If-Modified-Since, and don't send the result if the
849 852 # content has not been modified
850 853 ims_value = self.request.headers.get("If-Modified-Since")
851 854 if ims_value is not None:
852 855 date_tuple = email.utils.parsedate(ims_value)
853 856 if_since = datetime.datetime(*date_tuple[:6])
854 857 if if_since >= modified:
855 858 self.set_status(304)
856 859 return
857 860
858 861 with open(abspath, "rb") as file:
859 862 data = file.read()
860 863 hasher = hashlib.sha1()
861 864 hasher.update(data)
862 865 self.set_header("Etag", '"%s"' % hasher.hexdigest())
863 866 if include_body:
864 867 self.write(data)
865 868 else:
866 869 assert self.request.method == "HEAD"
867 870 self.set_header("Content-Length", len(data))
868 871
869 872 @classmethod
870 873 def get_version(cls, settings, path):
871 874 """Generate the version string to be used in static URLs.
872 875
873 876 This method may be overridden in subclasses (but note that it
874 877 is a class method rather than a static method). The default
875 878 implementation uses a hash of the file's contents.
876 879
877 880 ``settings`` is the `Application.settings` dictionary and ``path``
878 881 is the relative location of the requested asset on the filesystem.
879 882 The returned value should be a string, or ``None`` if no version
880 883 could be determined.
881 884 """
882 885 # begin subclass override:
883 886 static_paths = settings['static_path']
884 887 if isinstance(static_paths, basestring):
885 888 static_paths = [static_paths]
886 889 roots = tuple(
887 890 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
888 891 )
889 892
890 893 try:
891 894 abs_path = filefind(path, roots)
892 895 except IOError:
893 896 app_log.error("Could not find static file %r", path)
894 897 return None
895 898
896 899 # end subclass override
897 900
898 901 with cls._lock:
899 902 hashes = cls._static_hashes
900 903 if abs_path not in hashes:
901 904 try:
902 905 f = open(abs_path, "rb")
903 906 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
904 907 f.close()
905 908 except Exception:
906 909 app_log.error("Could not open static file %r", path)
907 910 hashes[abs_path] = None
908 911 hsh = hashes.get(abs_path)
909 912 if hsh:
910 913 return hsh[:5]
911 914 return None
912 915
913 916
914 917 def parse_url_path(self, url_path):
915 918 """Converts a static URL path into a filesystem path.
916 919
917 920 ``url_path`` is the path component of the URL with
918 921 ``static_url_prefix`` removed. The return value should be
919 922 filesystem path relative to ``static_path``.
920 923 """
921 924 if os.path.sep != "/":
922 925 url_path = url_path.replace("/", os.path.sep)
923 926 return url_path
924 927
925 928
@@ -1,741 +1,745 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
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 # stdlib
20 20 import errno
21 21 import logging
22 22 import os
23 23 import random
24 24 import re
25 25 import select
26 26 import signal
27 27 import socket
28 28 import sys
29 29 import threading
30 30 import time
31 31 import uuid
32 32 import webbrowser
33 33
34 34
35 35 # Third party
36 36 # check for pyzmq 2.1.11
37 37 from IPython.utils.zmqrelated import check_for_zmq
38 38 check_for_zmq('2.1.11', 'IPython.frontend.html.notebook')
39 39
40 40 import zmq
41 41 from jinja2 import Environment, FileSystemLoader
42 42
43 43 # Install the pyzmq ioloop. This has to be done before anything else from
44 44 # tornado is imported.
45 45 from zmq.eventloop import ioloop
46 46 ioloop.install()
47 47
48 48 # check for tornado 2.1.0
49 49 msg = "The IPython Notebook requires tornado >= 2.1.0"
50 50 try:
51 51 import tornado
52 52 except ImportError:
53 53 raise ImportError(msg)
54 54 try:
55 55 version_info = tornado.version_info
56 56 except AttributeError:
57 57 raise ImportError(msg + ", but you have < 1.1.0")
58 58 if version_info < (2,1,0):
59 59 raise ImportError(msg + ", but you have %s" % tornado.version)
60 60
61 61 from tornado import httpserver
62 62 from tornado import web
63 63
64 64 # Our own libraries
65 65 from IPython.frontend.html.notebook import DEFAULT_STATIC_FILES_PATH
66 66 from .kernelmanager import MappingKernelManager
67 67 from .handlers import (LoginHandler, LogoutHandler,
68 68 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
69 69 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler, StdinHandler,
70 70 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
71 AuthenticatedFileHandler, MainClusterHandler, ClusterProfileHandler,
72 ClusterActionHandler, FileFindHandler,
73 NotebookRedirectHandler, NotebookCheckpointHandler,
71 NotebookRedirectHandler, NotebookCheckpointsHandler, ModifyNotebookCheckpointsHandler,
72 AuthenticatedFileHandler, FileFindHandler,
73 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
74 74 )
75 75 from .nbmanager import NotebookManager
76 76 from .filenbmanager import FileNotebookManager
77 77 from .clustermanager import ClusterManager
78 78
79 79 from IPython.config.application import catch_config_error, boolean_flag
80 80 from IPython.core.application import BaseIPythonApplication
81 81 from IPython.core.profiledir import ProfileDir
82 82 from IPython.frontend.consoleapp import IPythonConsoleApp
83 83 from IPython.kernel import swallow_argv
84 84 from IPython.kernel.zmq.session import Session, default_secure
85 85 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
86 86 from IPython.kernel.zmq.kernelapp import (
87 87 kernel_flags,
88 88 kernel_aliases,
89 89 IPKernelApp
90 90 )
91 91 from IPython.utils.importstring import import_item
92 92 from IPython.utils.localinterfaces import LOCALHOST
93 93 from IPython.utils.traitlets import (
94 94 Dict, Unicode, Integer, List, Enum, Bool,
95 95 DottedObjectName
96 96 )
97 97 from IPython.utils import py3compat
98 98 from IPython.utils.path import filefind
99 99
100 100 #-----------------------------------------------------------------------------
101 101 # Module globals
102 102 #-----------------------------------------------------------------------------
103 103
104 104 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
105 105 _kernel_action_regex = r"(?P<action>restart|interrupt)"
106 106 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
107 107 _notebook_name_regex = r"(?P<notebook_name>.+\.ipynb)"
108 _checkpoint_id_regex = r"(?P<checkpoint_id>[\w-]+)"
108 109 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
109 110 _cluster_action_regex = r"(?P<action>start|stop)"
110 111
111 112 _examples = """
112 113 ipython notebook # start the notebook
113 114 ipython notebook --profile=sympy # use the sympy profile
114 115 ipython notebook --pylab=inline # pylab in inline plotting mode
115 116 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
116 117 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
117 118 """
118 119
119 120 #-----------------------------------------------------------------------------
120 121 # Helper functions
121 122 #-----------------------------------------------------------------------------
122 123
123 124 def url_path_join(a,b):
124 125 if a.endswith('/') and b.startswith('/'):
125 126 return a[:-1]+b
126 127 else:
127 128 return a+b
128 129
129 130 def random_ports(port, n):
130 131 """Generate a list of n random ports near the given port.
131 132
132 133 The first 5 ports will be sequential, and the remaining n-5 will be
133 134 randomly selected in the range [port-2*n, port+2*n].
134 135 """
135 136 for i in range(min(5, n)):
136 137 yield port + i
137 138 for i in range(n-5):
138 139 yield port + random.randint(-2*n, 2*n)
139 140
140 141 #-----------------------------------------------------------------------------
141 142 # The Tornado web application
142 143 #-----------------------------------------------------------------------------
143 144
144 145 class NotebookWebApplication(web.Application):
145 146
146 147 def __init__(self, ipython_app, kernel_manager, notebook_manager,
147 148 cluster_manager, log,
148 149 base_project_url, settings_overrides):
149 150 handlers = [
150 151 (r"/", ProjectDashboardHandler),
151 152 (r"/login", LoginHandler),
152 153 (r"/logout", LogoutHandler),
153 154 (r"/new", NewHandler),
154 155 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
155 156 (r"/%s" % _notebook_name_regex, NotebookRedirectHandler),
156 157 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
157 158 (r"/kernels", MainKernelHandler),
158 159 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
159 160 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
160 161 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
161 162 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
162 163 (r"/kernels/%s/stdin" % _kernel_id_regex, StdinHandler),
163 164 (r"/notebooks", NotebookRootHandler),
164 165 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
165 (r"/notebooks/%s/checkpoint" % _notebook_id_regex, NotebookCheckpointHandler),
166 (r"/notebooks/%s/checkpoints" % _notebook_id_regex, NotebookCheckpointsHandler),
167 (r"/notebooks/%s/checkpoints/%s" % (_notebook_id_regex, _checkpoint_id_regex),
168 ModifyNotebookCheckpointsHandler
169 ),
166 170 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
167 171 (r"/clusters", MainClusterHandler),
168 172 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
169 173 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
170 174 ]
171 175
172 176 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
173 177 # base_project_url will always be unicode, which will in turn
174 178 # make the patterns unicode, and ultimately result in unicode
175 179 # keys in kwargs to handler._execute(**kwargs) in tornado.
176 180 # This enforces that base_project_url be ascii in that situation.
177 181 #
178 182 # Note that the URLs these patterns check against are escaped,
179 183 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
180 184 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
181 185 template_path = os.path.join(os.path.dirname(__file__), "templates")
182 186 settings = dict(
183 187 # basics
184 188 base_project_url=base_project_url,
185 189 base_kernel_url=ipython_app.base_kernel_url,
186 190 template_path=template_path,
187 191 static_path=ipython_app.static_file_path,
188 192 static_handler_class = FileFindHandler,
189 193 static_url_prefix = url_path_join(base_project_url,'/static/'),
190 194
191 195 # authentication
192 196 cookie_secret=os.urandom(1024),
193 197 login_url=url_path_join(base_project_url,'/login'),
194 198 cookie_name='username-%s' % uuid.uuid4(),
195 199 read_only=ipython_app.read_only,
196 200 password=ipython_app.password,
197 201
198 202 # managers
199 203 kernel_manager=kernel_manager,
200 204 notebook_manager=notebook_manager,
201 205 cluster_manager=cluster_manager,
202 206
203 207 # IPython stuff
204 208 mathjax_url=ipython_app.mathjax_url,
205 209 max_msg_size=ipython_app.max_msg_size,
206 210 config=ipython_app.config,
207 211 use_less=ipython_app.use_less,
208 212 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
209 213 )
210 214
211 215 # allow custom overrides for the tornado web app.
212 216 settings.update(settings_overrides)
213 217
214 218 # prepend base_project_url onto the patterns that we match
215 219 new_handlers = []
216 220 for handler in handlers:
217 221 pattern = url_path_join(base_project_url, handler[0])
218 222 new_handler = tuple([pattern] + list(handler[1:]))
219 223 new_handlers.append(new_handler)
220 224
221 225 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
222 226
223 227
224 228
225 229 #-----------------------------------------------------------------------------
226 230 # Aliases and Flags
227 231 #-----------------------------------------------------------------------------
228 232
229 233 flags = dict(kernel_flags)
230 234 flags['no-browser']=(
231 235 {'NotebookApp' : {'open_browser' : False}},
232 236 "Don't open the notebook in a browser after startup."
233 237 )
234 238 flags['no-mathjax']=(
235 239 {'NotebookApp' : {'enable_mathjax' : False}},
236 240 """Disable MathJax
237 241
238 242 MathJax is the javascript library IPython uses to render math/LaTeX. It is
239 243 very large, so you may want to disable it if you have a slow internet
240 244 connection, or for offline use of the notebook.
241 245
242 246 When disabled, equations etc. will appear as their untransformed TeX source.
243 247 """
244 248 )
245 249 flags['read-only'] = (
246 250 {'NotebookApp' : {'read_only' : True}},
247 251 """Allow read-only access to notebooks.
248 252
249 253 When using a password to protect the notebook server, this flag
250 254 allows unauthenticated clients to view the notebook list, and
251 255 individual notebooks, but not edit them, start kernels, or run
252 256 code.
253 257
254 258 If no password is set, the server will be entirely read-only.
255 259 """
256 260 )
257 261
258 262 # Add notebook manager flags
259 263 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
260 264 'Auto-save a .py script everytime the .ipynb notebook is saved',
261 265 'Do not auto-save .py scripts for every notebook'))
262 266
263 267 # the flags that are specific to the frontend
264 268 # these must be scrubbed before being passed to the kernel,
265 269 # or it will raise an error on unrecognized flags
266 270 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
267 271
268 272 aliases = dict(kernel_aliases)
269 273
270 274 aliases.update({
271 275 'ip': 'NotebookApp.ip',
272 276 'port': 'NotebookApp.port',
273 277 'port-retries': 'NotebookApp.port_retries',
274 278 'transport': 'KernelManager.transport',
275 279 'keyfile': 'NotebookApp.keyfile',
276 280 'certfile': 'NotebookApp.certfile',
277 281 'notebook-dir': 'NotebookManager.notebook_dir',
278 282 'browser': 'NotebookApp.browser',
279 283 })
280 284
281 285 # remove ipkernel flags that are singletons, and don't make sense in
282 286 # multi-kernel evironment:
283 287 aliases.pop('f', None)
284 288
285 289 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
286 290 u'notebook-dir']
287 291
288 292 #-----------------------------------------------------------------------------
289 293 # NotebookApp
290 294 #-----------------------------------------------------------------------------
291 295
292 296 class NotebookApp(BaseIPythonApplication):
293 297
294 298 name = 'ipython-notebook'
295 299 default_config_file_name='ipython_notebook_config.py'
296 300
297 301 description = """
298 302 The IPython HTML Notebook.
299 303
300 304 This launches a Tornado based HTML Notebook Server that serves up an
301 305 HTML5/Javascript Notebook client.
302 306 """
303 307 examples = _examples
304 308
305 309 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
306 310 FileNotebookManager]
307 311 flags = Dict(flags)
308 312 aliases = Dict(aliases)
309 313
310 314 kernel_argv = List(Unicode)
311 315
312 316 max_msg_size = Integer(65536, config=True, help="""
313 317 The max raw message size accepted from the browser
314 318 over a WebSocket connection.
315 319 """)
316 320
317 321 def _log_level_default(self):
318 322 return logging.INFO
319 323
320 324 def _log_format_default(self):
321 325 """override default log format to include time"""
322 326 return u"%(asctime)s.%(msecs).03d [%(name)s] %(message)s"
323 327
324 328 # create requested profiles by default, if they don't exist:
325 329 auto_create = Bool(True)
326 330
327 331 # file to be opened in the notebook server
328 332 file_to_run = Unicode('')
329 333
330 334 # Network related information.
331 335
332 336 ip = Unicode(LOCALHOST, config=True,
333 337 help="The IP address the notebook server will listen on."
334 338 )
335 339
336 340 def _ip_changed(self, name, old, new):
337 341 if new == u'*': self.ip = u''
338 342
339 343 port = Integer(8888, config=True,
340 344 help="The port the notebook server will listen on."
341 345 )
342 346 port_retries = Integer(50, config=True,
343 347 help="The number of additional ports to try if the specified port is not available."
344 348 )
345 349
346 350 certfile = Unicode(u'', config=True,
347 351 help="""The full path to an SSL/TLS certificate file."""
348 352 )
349 353
350 354 keyfile = Unicode(u'', config=True,
351 355 help="""The full path to a private key file for usage with SSL/TLS."""
352 356 )
353 357
354 358 password = Unicode(u'', config=True,
355 359 help="""Hashed password to use for web authentication.
356 360
357 361 To generate, type in a python/IPython shell:
358 362
359 363 from IPython.lib import passwd; passwd()
360 364
361 365 The string should be of the form type:salt:hashed-password.
362 366 """
363 367 )
364 368
365 369 open_browser = Bool(True, config=True,
366 370 help="""Whether to open in a browser after starting.
367 371 The specific browser used is platform dependent and
368 372 determined by the python standard library `webbrowser`
369 373 module, unless it is overridden using the --browser
370 374 (NotebookApp.browser) configuration option.
371 375 """)
372 376
373 377 browser = Unicode(u'', config=True,
374 378 help="""Specify what command to use to invoke a web
375 379 browser when opening the notebook. If not specified, the
376 380 default browser will be determined by the `webbrowser`
377 381 standard library module, which allows setting of the
378 382 BROWSER environment variable to override it.
379 383 """)
380 384
381 385 read_only = Bool(False, config=True,
382 386 help="Whether to prevent editing/execution of notebooks."
383 387 )
384 388
385 389 use_less = Bool(False, config=True,
386 390 help="""Wether to use Browser Side less-css parsing
387 391 instead of compiled css version in templates that allows
388 392 it. This is mainly convenient when working on the less
389 393 file to avoid a build step, or if user want to overwrite
390 394 some of the less variables without having to recompile
391 395 everything.
392 396
393 397 You will need to install the less.js component in the static directory
394 398 either in the source tree or in your profile folder.
395 399 """)
396 400
397 401 webapp_settings = Dict(config=True,
398 402 help="Supply overrides for the tornado.web.Application that the "
399 403 "IPython notebook uses.")
400 404
401 405 enable_mathjax = Bool(True, config=True,
402 406 help="""Whether to enable MathJax for typesetting math/TeX
403 407
404 408 MathJax is the javascript library IPython uses to render math/LaTeX. It is
405 409 very large, so you may want to disable it if you have a slow internet
406 410 connection, or for offline use of the notebook.
407 411
408 412 When disabled, equations etc. will appear as their untransformed TeX source.
409 413 """
410 414 )
411 415 def _enable_mathjax_changed(self, name, old, new):
412 416 """set mathjax url to empty if mathjax is disabled"""
413 417 if not new:
414 418 self.mathjax_url = u''
415 419
416 420 base_project_url = Unicode('/', config=True,
417 421 help='''The base URL for the notebook server.
418 422
419 423 Leading and trailing slashes can be omitted,
420 424 and will automatically be added.
421 425 ''')
422 426 def _base_project_url_changed(self, name, old, new):
423 427 if not new.startswith('/'):
424 428 self.base_project_url = '/'+new
425 429 elif not new.endswith('/'):
426 430 self.base_project_url = new+'/'
427 431
428 432 base_kernel_url = Unicode('/', config=True,
429 433 help='''The base URL for the kernel server
430 434
431 435 Leading and trailing slashes can be omitted,
432 436 and will automatically be added.
433 437 ''')
434 438 def _base_kernel_url_changed(self, name, old, new):
435 439 if not new.startswith('/'):
436 440 self.base_kernel_url = '/'+new
437 441 elif not new.endswith('/'):
438 442 self.base_kernel_url = new+'/'
439 443
440 444 websocket_host = Unicode("", config=True,
441 445 help="""The hostname for the websocket server."""
442 446 )
443 447
444 448 extra_static_paths = List(Unicode, config=True,
445 449 help="""Extra paths to search for serving static files.
446 450
447 451 This allows adding javascript/css to be available from the notebook server machine,
448 452 or overriding individual files in the IPython"""
449 453 )
450 454 def _extra_static_paths_default(self):
451 455 return [os.path.join(self.profile_dir.location, 'static')]
452 456
453 457 @property
454 458 def static_file_path(self):
455 459 """return extra paths + the default location"""
456 460 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
457 461
458 462 mathjax_url = Unicode("", config=True,
459 463 help="""The url for MathJax.js."""
460 464 )
461 465 def _mathjax_url_default(self):
462 466 if not self.enable_mathjax:
463 467 return u''
464 468 static_url_prefix = self.webapp_settings.get("static_url_prefix",
465 469 "/static/")
466 470 try:
467 471 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
468 472 except IOError:
469 473 if self.certfile:
470 474 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
471 475 base = u"https://c328740.ssl.cf1.rackcdn.com"
472 476 else:
473 477 base = u"http://cdn.mathjax.org"
474 478
475 479 url = base + u"/mathjax/latest/MathJax.js"
476 480 self.log.info("Using MathJax from CDN: %s", url)
477 481 return url
478 482 else:
479 483 self.log.info("Using local MathJax from %s" % mathjax)
480 484 return static_url_prefix+u"mathjax/MathJax.js"
481 485
482 486 def _mathjax_url_changed(self, name, old, new):
483 487 if new and not self.enable_mathjax:
484 488 # enable_mathjax=False overrides mathjax_url
485 489 self.mathjax_url = u''
486 490 else:
487 491 self.log.info("Using MathJax: %s", new)
488 492
489 493 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
490 494 config=True,
491 495 help='The notebook manager class to use.')
492 496
493 497 trust_xheaders = Bool(False, config=True,
494 498 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
495 499 "sent by the upstream reverse proxy. Neccesary if the proxy handles SSL")
496 500 )
497 501
498 502 def parse_command_line(self, argv=None):
499 503 super(NotebookApp, self).parse_command_line(argv)
500 504 if argv is None:
501 505 argv = sys.argv[1:]
502 506
503 507 # Scrub frontend-specific flags
504 508 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
505 509 # Kernel should inherit default config file from frontend
506 510 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
507 511
508 512 if self.extra_args:
509 513 f = os.path.abspath(self.extra_args[0])
510 514 if os.path.isdir(f):
511 515 nbdir = f
512 516 else:
513 517 self.file_to_run = f
514 518 nbdir = os.path.dirname(f)
515 519 self.config.NotebookManager.notebook_dir = nbdir
516 520
517 521 def init_configurables(self):
518 522 # force Session default to be secure
519 523 default_secure(self.config)
520 524 self.kernel_manager = MappingKernelManager(
521 525 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
522 526 connection_dir = self.profile_dir.security_dir,
523 527 )
524 528 kls = import_item(self.notebook_manager_class)
525 529 self.notebook_manager = kls(config=self.config, log=self.log)
526 530 self.notebook_manager.load_notebook_names()
527 531 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
528 532 self.cluster_manager.update_profiles()
529 533
530 534 def init_logging(self):
531 535 # This prevents double log messages because tornado use a root logger that
532 536 # self.log is a child of. The logging module dipatches log messages to a log
533 537 # and all of its ancenstors until propagate is set to False.
534 538 self.log.propagate = False
535 539
536 540 # set the date format
537 541 formatter = logging.Formatter(self.log_format, datefmt="%Y-%m-%d %H:%M:%S")
538 542 self.log.handlers[0].setFormatter(formatter)
539 543
540 544 # hook up tornado 3's loggers to our app handlers
541 545 for name in ('access', 'application', 'general'):
542 546 logging.getLogger('tornado.%s' % name).handlers = self.log.handlers
543 547
544 548 def init_webapp(self):
545 549 """initialize tornado webapp and httpserver"""
546 550 self.web_app = NotebookWebApplication(
547 551 self, self.kernel_manager, self.notebook_manager,
548 552 self.cluster_manager, self.log,
549 553 self.base_project_url, self.webapp_settings
550 554 )
551 555 if self.certfile:
552 556 ssl_options = dict(certfile=self.certfile)
553 557 if self.keyfile:
554 558 ssl_options['keyfile'] = self.keyfile
555 559 else:
556 560 ssl_options = None
557 561 self.web_app.password = self.password
558 562 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
559 563 xheaders=self.trust_xheaders)
560 564 if not self.ip:
561 565 warning = "WARNING: The notebook server is listening on all IP addresses"
562 566 if ssl_options is None:
563 567 self.log.critical(warning + " and not using encryption. This"
564 568 "is not recommended.")
565 569 if not self.password and not self.read_only:
566 570 self.log.critical(warning + "and not using authentication."
567 571 "This is highly insecure and not recommended.")
568 572 success = None
569 573 for port in random_ports(self.port, self.port_retries+1):
570 574 try:
571 575 self.http_server.listen(port, self.ip)
572 576 except socket.error as e:
573 577 # XXX: remove the e.errno == -9 block when we require
574 578 # tornado >= 3.0
575 579 if e.errno == -9 and tornado.version_info[0] < 3:
576 580 # The flags passed to socket.getaddrinfo from
577 581 # tornado.netutils.bind_sockets can cause "gaierror:
578 582 # [Errno -9] Address family for hostname not supported"
579 583 # when the interface is not associated, for example.
580 584 # Changing the flags to exclude socket.AI_ADDRCONFIG does
581 585 # not cause this error, but the only way to do this is to
582 586 # monkeypatch socket to remove the AI_ADDRCONFIG attribute
583 587 saved_AI_ADDRCONFIG = socket.AI_ADDRCONFIG
584 588 self.log.warn('Monkeypatching socket to fix tornado bug')
585 589 del(socket.AI_ADDRCONFIG)
586 590 try:
587 591 # retry the tornado call without AI_ADDRCONFIG flags
588 592 self.http_server.listen(port, self.ip)
589 593 except socket.error as e2:
590 594 e = e2
591 595 else:
592 596 self.port = port
593 597 success = True
594 598 break
595 599 # restore the monekypatch
596 600 socket.AI_ADDRCONFIG = saved_AI_ADDRCONFIG
597 601 if e.errno != errno.EADDRINUSE:
598 602 raise
599 603 self.log.info('The port %i is already in use, trying another random port.' % port)
600 604 else:
601 605 self.port = port
602 606 success = True
603 607 break
604 608 if not success:
605 609 self.log.critical('ERROR: the notebook server could not be started because '
606 610 'no available port could be found.')
607 611 self.exit(1)
608 612
609 613 def init_signal(self):
610 614 if not sys.platform.startswith('win'):
611 615 signal.signal(signal.SIGINT, self._handle_sigint)
612 616 signal.signal(signal.SIGTERM, self._signal_stop)
613 617 if hasattr(signal, 'SIGUSR1'):
614 618 # Windows doesn't support SIGUSR1
615 619 signal.signal(signal.SIGUSR1, self._signal_info)
616 620 if hasattr(signal, 'SIGINFO'):
617 621 # only on BSD-based systems
618 622 signal.signal(signal.SIGINFO, self._signal_info)
619 623
620 624 def _handle_sigint(self, sig, frame):
621 625 """SIGINT handler spawns confirmation dialog"""
622 626 # register more forceful signal handler for ^C^C case
623 627 signal.signal(signal.SIGINT, self._signal_stop)
624 628 # request confirmation dialog in bg thread, to avoid
625 629 # blocking the App
626 630 thread = threading.Thread(target=self._confirm_exit)
627 631 thread.daemon = True
628 632 thread.start()
629 633
630 634 def _restore_sigint_handler(self):
631 635 """callback for restoring original SIGINT handler"""
632 636 signal.signal(signal.SIGINT, self._handle_sigint)
633 637
634 638 def _confirm_exit(self):
635 639 """confirm shutdown on ^C
636 640
637 641 A second ^C, or answering 'y' within 5s will cause shutdown,
638 642 otherwise original SIGINT handler will be restored.
639 643
640 644 This doesn't work on Windows.
641 645 """
642 646 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
643 647 time.sleep(0.1)
644 648 info = self.log.info
645 649 info('interrupted')
646 650 print self.notebook_info()
647 651 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
648 652 sys.stdout.flush()
649 653 r,w,x = select.select([sys.stdin], [], [], 5)
650 654 if r:
651 655 line = sys.stdin.readline()
652 656 if line.lower().startswith('y'):
653 657 self.log.critical("Shutdown confirmed")
654 658 ioloop.IOLoop.instance().stop()
655 659 return
656 660 else:
657 661 print "No answer for 5s:",
658 662 print "resuming operation..."
659 663 # no answer, or answer is no:
660 664 # set it back to original SIGINT handler
661 665 # use IOLoop.add_callback because signal.signal must be called
662 666 # from main thread
663 667 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
664 668
665 669 def _signal_stop(self, sig, frame):
666 670 self.log.critical("received signal %s, stopping", sig)
667 671 ioloop.IOLoop.instance().stop()
668 672
669 673 def _signal_info(self, sig, frame):
670 674 print self.notebook_info()
671 675
672 676 @catch_config_error
673 677 def initialize(self, argv=None):
674 678 self.init_logging()
675 679 super(NotebookApp, self).initialize(argv)
676 680 self.init_configurables()
677 681 self.init_webapp()
678 682 self.init_signal()
679 683
680 684 def cleanup_kernels(self):
681 685 """Shutdown all kernels.
682 686
683 687 The kernels will shutdown themselves when this process no longer exists,
684 688 but explicit shutdown allows the KernelManagers to cleanup the connection files.
685 689 """
686 690 self.log.info('Shutting down kernels')
687 691 self.kernel_manager.shutdown_all()
688 692
689 693 def notebook_info(self):
690 694 "Return the current working directory and the server url information"
691 695 mgr_info = self.notebook_manager.info_string() + "\n"
692 696 return mgr_info +"The IPython Notebook is running at: %s" % self._url
693 697
694 698 def start(self):
695 699 """ Start the IPython Notebook server app, after initialization
696 700
697 701 This method takes no arguments so all configuration and initialization
698 702 must be done prior to calling this method."""
699 703 ip = self.ip if self.ip else '[all ip addresses on your system]'
700 704 proto = 'https' if self.certfile else 'http'
701 705 info = self.log.info
702 706 self._url = "%s://%s:%i%s" % (proto, ip, self.port,
703 707 self.base_project_url)
704 708 for line in self.notebook_info().split("\n"):
705 709 info(line)
706 710 info("Use Control-C to stop this server and shut down all kernels.")
707 711
708 712 if self.open_browser or self.file_to_run:
709 713 ip = self.ip or LOCALHOST
710 714 try:
711 715 browser = webbrowser.get(self.browser or None)
712 716 except webbrowser.Error as e:
713 717 self.log.warn('No web browser found: %s.' % e)
714 718 browser = None
715 719
716 720 if self.file_to_run:
717 721 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
718 722 url = self.notebook_manager.rev_mapping.get(name, '')
719 723 else:
720 724 url = ''
721 725 if browser:
722 726 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
723 727 self.port, self.base_project_url, url), new=2)
724 728 threading.Thread(target=b).start()
725 729 try:
726 730 ioloop.IOLoop.instance().start()
727 731 except KeyboardInterrupt:
728 732 info("Interrupted...")
729 733 finally:
730 734 self.cleanup_kernels()
731 735
732 736
733 737 #-----------------------------------------------------------------------------
734 738 # Main entry point
735 739 #-----------------------------------------------------------------------------
736 740
737 741 def launch_new_instance():
738 742 app = NotebookApp.instance()
739 743 app.initialize()
740 744 app.start()
741 745
General Comments 0
You need to be logged in to leave comments. Login now