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