##// END OF EJS Templates
Merge pull request #3058 from minrk/redirect...
Brian E. Granger -
r10188:c07e21f2 merge
parent child Browse files
Show More
@@ -1,905 +1,919
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 # strip trailing .ipynb:
618 notebook_name = os.path.splitext(notebook_name)[0]
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,706 +1,709
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 import tornado
44 44 from tornado import httpserver
45 45 from tornado import web
46 46
47 47 # Our own libraries
48 48 from .kernelmanager import MappingKernelManager
49 49 from .handlers import (LoginHandler, LogoutHandler,
50 50 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
51 51 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
52 52 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
53 53 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
54 54 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
55 FileFindHandler,
55 FileFindHandler, NotebookRedirectHandler,
56 56 )
57 57 from .nbmanager import NotebookManager
58 58 from .filenbmanager import FileNotebookManager
59 59 from .clustermanager import ClusterManager
60 60
61 61 from IPython.config.application import catch_config_error, boolean_flag
62 62 from IPython.core.application import BaseIPythonApplication
63 63 from IPython.core.profiledir import ProfileDir
64 64 from IPython.frontend.consoleapp import IPythonConsoleApp
65 65 from IPython.kernel import swallow_argv
66 66 from IPython.kernel.zmq.session import Session, default_secure
67 67 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
68 68 from IPython.kernel.zmq.kernelapp import (
69 69 kernel_flags,
70 70 kernel_aliases,
71 71 IPKernelApp
72 72 )
73 73 from IPython.utils.importstring import import_item
74 74 from IPython.utils.localinterfaces import LOCALHOST
75 75 from IPython.utils.traitlets import (
76 76 Dict, Unicode, Integer, List, Enum, Bool,
77 77 DottedObjectName
78 78 )
79 79 from IPython.utils import py3compat
80 80 from IPython.utils.path import filefind
81 81
82 82 #-----------------------------------------------------------------------------
83 83 # Module globals
84 84 #-----------------------------------------------------------------------------
85 85
86 86 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
87 87 _kernel_action_regex = r"(?P<action>restart|interrupt)"
88 88 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
89 _notebook_name_regex = r"(?P<notebook_name>.+\.ipynb)"
89 90 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
90 91 _cluster_action_regex = r"(?P<action>start|stop)"
91 92
92 93 _examples = """
93 94 ipython notebook # start the notebook
94 95 ipython notebook --profile=sympy # use the sympy profile
95 96 ipython notebook --pylab=inline # pylab in inline plotting mode
96 97 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
97 98 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
98 99 """
99 100
100 101 # Packagers: modify this line if you store the notebook static files elsewhere
101 102 DEFAULT_STATIC_FILES_PATH = os.path.join(os.path.dirname(__file__), "static")
102 103
103 104 #-----------------------------------------------------------------------------
104 105 # Helper functions
105 106 #-----------------------------------------------------------------------------
106 107
107 108 def url_path_join(a,b):
108 109 if a.endswith('/') and b.startswith('/'):
109 110 return a[:-1]+b
110 111 else:
111 112 return a+b
112 113
113 114 def random_ports(port, n):
114 115 """Generate a list of n random ports near the given port.
115 116
116 117 The first 5 ports will be sequential, and the remaining n-5 will be
117 118 randomly selected in the range [port-2*n, port+2*n].
118 119 """
119 120 for i in range(min(5, n)):
120 121 yield port + i
121 122 for i in range(n-5):
122 123 yield port + random.randint(-2*n, 2*n)
123 124
124 125 #-----------------------------------------------------------------------------
125 126 # The Tornado web application
126 127 #-----------------------------------------------------------------------------
127 128
128 129 class NotebookWebApplication(web.Application):
129 130
130 131 def __init__(self, ipython_app, kernel_manager, notebook_manager,
131 132 cluster_manager, log,
132 133 base_project_url, settings_overrides):
133 134 handlers = [
134 135 (r"/", ProjectDashboardHandler),
135 136 (r"/login", LoginHandler),
136 137 (r"/logout", LogoutHandler),
137 138 (r"/new", NewHandler),
138 139 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
140 (r"/%s" % _notebook_name_regex, NotebookRedirectHandler),
139 141 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
140 142 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
141 143 (r"/kernels", MainKernelHandler),
142 144 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
143 145 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
144 146 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
145 147 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
146 148 (r"/notebooks", NotebookRootHandler),
147 149 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
148 150 (r"/rstservice/render", RSTHandler),
149 151 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
150 152 (r"/clusters", MainClusterHandler),
151 153 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
152 154 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
153 155 ]
154 156
155 157 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
156 158 # base_project_url will always be unicode, which will in turn
157 159 # make the patterns unicode, and ultimately result in unicode
158 160 # keys in kwargs to handler._execute(**kwargs) in tornado.
159 161 # This enforces that base_project_url be ascii in that situation.
160 162 #
161 163 # Note that the URLs these patterns check against are escaped,
162 164 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
163 165 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
164 166
165 167 settings = dict(
166 168 template_path=os.path.join(os.path.dirname(__file__), "templates"),
167 169 static_path=ipython_app.static_file_path,
168 170 static_handler_class = FileFindHandler,
169 171 static_url_prefix = url_path_join(base_project_url,'/static/'),
170 172 cookie_secret=os.urandom(1024),
171 173 login_url=url_path_join(base_project_url,'/login'),
172 174 cookie_name='username-%s' % uuid.uuid4(),
175 base_project_url = base_project_url,
173 176 )
174 177
175 178 # allow custom overrides for the tornado web app.
176 179 settings.update(settings_overrides)
177 180
178 181 # prepend base_project_url onto the patterns that we match
179 182 new_handlers = []
180 183 for handler in handlers:
181 184 pattern = url_path_join(base_project_url, handler[0])
182 185 new_handler = tuple([pattern]+list(handler[1:]))
183 186 new_handlers.append( new_handler )
184 187
185 188 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
186 189
187 190 self.kernel_manager = kernel_manager
188 191 self.notebook_manager = notebook_manager
189 192 self.cluster_manager = cluster_manager
190 193 self.ipython_app = ipython_app
191 194 self.read_only = self.ipython_app.read_only
192 195 self.config = self.ipython_app.config
193 196 self.use_less = self.ipython_app.use_less
194 197 self.log = log
195 198 self.jinja2_env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")))
196 199
197 200
198 201
199 202 #-----------------------------------------------------------------------------
200 203 # Aliases and Flags
201 204 #-----------------------------------------------------------------------------
202 205
203 206 flags = dict(kernel_flags)
204 207 flags['no-browser']=(
205 208 {'NotebookApp' : {'open_browser' : False}},
206 209 "Don't open the notebook in a browser after startup."
207 210 )
208 211 flags['no-mathjax']=(
209 212 {'NotebookApp' : {'enable_mathjax' : False}},
210 213 """Disable MathJax
211 214
212 215 MathJax is the javascript library IPython uses to render math/LaTeX. It is
213 216 very large, so you may want to disable it if you have a slow internet
214 217 connection, or for offline use of the notebook.
215 218
216 219 When disabled, equations etc. will appear as their untransformed TeX source.
217 220 """
218 221 )
219 222 flags['read-only'] = (
220 223 {'NotebookApp' : {'read_only' : True}},
221 224 """Allow read-only access to notebooks.
222 225
223 226 When using a password to protect the notebook server, this flag
224 227 allows unauthenticated clients to view the notebook list, and
225 228 individual notebooks, but not edit them, start kernels, or run
226 229 code.
227 230
228 231 If no password is set, the server will be entirely read-only.
229 232 """
230 233 )
231 234
232 235 # Add notebook manager flags
233 236 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
234 237 'Auto-save a .py script everytime the .ipynb notebook is saved',
235 238 'Do not auto-save .py scripts for every notebook'))
236 239
237 240 # the flags that are specific to the frontend
238 241 # these must be scrubbed before being passed to the kernel,
239 242 # or it will raise an error on unrecognized flags
240 243 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
241 244
242 245 aliases = dict(kernel_aliases)
243 246
244 247 aliases.update({
245 248 'ip': 'NotebookApp.ip',
246 249 'port': 'NotebookApp.port',
247 250 'port-retries': 'NotebookApp.port_retries',
248 251 'transport': 'KernelManager.transport',
249 252 'keyfile': 'NotebookApp.keyfile',
250 253 'certfile': 'NotebookApp.certfile',
251 254 'notebook-dir': 'NotebookManager.notebook_dir',
252 255 'browser': 'NotebookApp.browser',
253 256 })
254 257
255 258 # remove ipkernel flags that are singletons, and don't make sense in
256 259 # multi-kernel evironment:
257 260 aliases.pop('f', None)
258 261
259 262 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
260 263 u'notebook-dir']
261 264
262 265 #-----------------------------------------------------------------------------
263 266 # NotebookApp
264 267 #-----------------------------------------------------------------------------
265 268
266 269 class NotebookApp(BaseIPythonApplication):
267 270
268 271 name = 'ipython-notebook'
269 272 default_config_file_name='ipython_notebook_config.py'
270 273
271 274 description = """
272 275 The IPython HTML Notebook.
273 276
274 277 This launches a Tornado based HTML Notebook Server that serves up an
275 278 HTML5/Javascript Notebook client.
276 279 """
277 280 examples = _examples
278 281
279 282 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
280 283 FileNotebookManager]
281 284 flags = Dict(flags)
282 285 aliases = Dict(aliases)
283 286
284 287 kernel_argv = List(Unicode)
285 288
286 289 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
287 290 default_value=logging.INFO,
288 291 config=True,
289 292 help="Set the log level by value or name.")
290 293
291 294 # create requested profiles by default, if they don't exist:
292 295 auto_create = Bool(True)
293 296
294 297 # file to be opened in the notebook server
295 298 file_to_run = Unicode('')
296 299
297 300 # Network related information.
298 301
299 302 ip = Unicode(LOCALHOST, config=True,
300 303 help="The IP address the notebook server will listen on."
301 304 )
302 305
303 306 def _ip_changed(self, name, old, new):
304 307 if new == u'*': self.ip = u''
305 308
306 309 port = Integer(8888, config=True,
307 310 help="The port the notebook server will listen on."
308 311 )
309 312 port_retries = Integer(50, config=True,
310 313 help="The number of additional ports to try if the specified port is not available."
311 314 )
312 315
313 316 certfile = Unicode(u'', config=True,
314 317 help="""The full path to an SSL/TLS certificate file."""
315 318 )
316 319
317 320 keyfile = Unicode(u'', config=True,
318 321 help="""The full path to a private key file for usage with SSL/TLS."""
319 322 )
320 323
321 324 password = Unicode(u'', config=True,
322 325 help="""Hashed password to use for web authentication.
323 326
324 327 To generate, type in a python/IPython shell:
325 328
326 329 from IPython.lib import passwd; passwd()
327 330
328 331 The string should be of the form type:salt:hashed-password.
329 332 """
330 333 )
331 334
332 335 open_browser = Bool(True, config=True,
333 336 help="""Whether to open in a browser after starting.
334 337 The specific browser used is platform dependent and
335 338 determined by the python standard library `webbrowser`
336 339 module, unless it is overridden using the --browser
337 340 (NotebookApp.browser) configuration option.
338 341 """)
339 342
340 343 browser = Unicode(u'', config=True,
341 344 help="""Specify what command to use to invoke a web
342 345 browser when opening the notebook. If not specified, the
343 346 default browser will be determined by the `webbrowser`
344 347 standard library module, which allows setting of the
345 348 BROWSER environment variable to override it.
346 349 """)
347 350
348 351 read_only = Bool(False, config=True,
349 352 help="Whether to prevent editing/execution of notebooks."
350 353 )
351 354
352 355 use_less = Bool(False, config=True,
353 356 help="""Wether to use Browser Side less-css parsing
354 357 instead of compiled css version in templates that allows
355 358 it. This is mainly convenient when working on the less
356 359 file to avoid a build step, or if user want to overwrite
357 360 some of the less variables without having to recompile
358 361 everything.
359 362
360 363 You will need to install the less.js component in the static directory
361 364 either in the source tree or in your profile folder.
362 365 """)
363 366
364 367 webapp_settings = Dict(config=True,
365 368 help="Supply overrides for the tornado.web.Application that the "
366 369 "IPython notebook uses.")
367 370
368 371 enable_mathjax = Bool(True, config=True,
369 372 help="""Whether to enable MathJax for typesetting math/TeX
370 373
371 374 MathJax is the javascript library IPython uses to render math/LaTeX. It is
372 375 very large, so you may want to disable it if you have a slow internet
373 376 connection, or for offline use of the notebook.
374 377
375 378 When disabled, equations etc. will appear as their untransformed TeX source.
376 379 """
377 380 )
378 381 def _enable_mathjax_changed(self, name, old, new):
379 382 """set mathjax url to empty if mathjax is disabled"""
380 383 if not new:
381 384 self.mathjax_url = u''
382 385
383 386 base_project_url = Unicode('/', config=True,
384 387 help='''The base URL for the notebook server.
385 388
386 389 Leading and trailing slashes can be omitted,
387 390 and will automatically be added.
388 391 ''')
389 392 def _base_project_url_changed(self, name, old, new):
390 393 if not new.startswith('/'):
391 394 self.base_project_url = '/'+new
392 395 elif not new.endswith('/'):
393 396 self.base_project_url = new+'/'
394 397
395 398 base_kernel_url = Unicode('/', config=True,
396 399 help='''The base URL for the kernel server
397 400
398 401 Leading and trailing slashes can be omitted,
399 402 and will automatically be added.
400 403 ''')
401 404 def _base_kernel_url_changed(self, name, old, new):
402 405 if not new.startswith('/'):
403 406 self.base_kernel_url = '/'+new
404 407 elif not new.endswith('/'):
405 408 self.base_kernel_url = new+'/'
406 409
407 410 websocket_host = Unicode("", config=True,
408 411 help="""The hostname for the websocket server."""
409 412 )
410 413
411 414 extra_static_paths = List(Unicode, config=True,
412 415 help="""Extra paths to search for serving static files.
413 416
414 417 This allows adding javascript/css to be available from the notebook server machine,
415 418 or overriding individual files in the IPython"""
416 419 )
417 420 def _extra_static_paths_default(self):
418 421 return [os.path.join(self.profile_dir.location, 'static')]
419 422
420 423 @property
421 424 def static_file_path(self):
422 425 """return extra paths + the default location"""
423 426 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
424 427
425 428 mathjax_url = Unicode("", config=True,
426 429 help="""The url for MathJax.js."""
427 430 )
428 431 def _mathjax_url_default(self):
429 432 if not self.enable_mathjax:
430 433 return u''
431 434 static_url_prefix = self.webapp_settings.get("static_url_prefix",
432 435 "/static/")
433 436 try:
434 437 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
435 438 except IOError:
436 439 if self.certfile:
437 440 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
438 441 base = u"https://c328740.ssl.cf1.rackcdn.com"
439 442 else:
440 443 base = u"http://cdn.mathjax.org"
441 444
442 445 url = base + u"/mathjax/latest/MathJax.js"
443 446 self.log.info("Using MathJax from CDN: %s", url)
444 447 return url
445 448 else:
446 449 self.log.info("Using local MathJax from %s" % mathjax)
447 450 return static_url_prefix+u"mathjax/MathJax.js"
448 451
449 452 def _mathjax_url_changed(self, name, old, new):
450 453 if new and not self.enable_mathjax:
451 454 # enable_mathjax=False overrides mathjax_url
452 455 self.mathjax_url = u''
453 456 else:
454 457 self.log.info("Using MathJax: %s", new)
455 458
456 459 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
457 460 config=True,
458 461 help='The notebook manager class to use.')
459 462
460 463 def parse_command_line(self, argv=None):
461 464 super(NotebookApp, self).parse_command_line(argv)
462 465 if argv is None:
463 466 argv = sys.argv[1:]
464 467
465 468 # Scrub frontend-specific flags
466 469 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
467 470 # Kernel should inherit default config file from frontend
468 471 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
469 472
470 473 if self.extra_args:
471 474 f = os.path.abspath(self.extra_args[0])
472 475 if os.path.isdir(f):
473 476 nbdir = f
474 477 else:
475 478 self.file_to_run = f
476 479 nbdir = os.path.dirname(f)
477 480 self.config.NotebookManager.notebook_dir = nbdir
478 481
479 482 def init_configurables(self):
480 483 # force Session default to be secure
481 484 default_secure(self.config)
482 485 self.kernel_manager = MappingKernelManager(
483 486 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
484 487 connection_dir = self.profile_dir.security_dir,
485 488 )
486 489 kls = import_item(self.notebook_manager_class)
487 490 self.notebook_manager = kls(config=self.config, log=self.log)
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 # XXX: remove the e.errno == -9 block when we require
527 530 # tornado >= 3.0
528 531 if e.errno == -9 and tornado.version_info[0] < 3:
529 532 # The flags passed to socket.getaddrinfo from
530 533 # tornado.netutils.bind_sockets can cause "gaierror:
531 534 # [Errno -9] Address family for hostname not supported"
532 535 # when the interface is not associated, for example.
533 536 # Changing the flags to exclude socket.AI_ADDRCONFIG does
534 537 # not cause this error, but the only way to do this is to
535 538 # monkeypatch socket to remove the AI_ADDRCONFIG attribute
536 539 saved_AI_ADDRCONFIG = socket.AI_ADDRCONFIG
537 540 self.log.warn('Monkeypatching socket to fix tornado bug')
538 541 del(socket.AI_ADDRCONFIG)
539 542 try:
540 543 # retry the tornado call without AI_ADDRCONFIG flags
541 544 self.http_server.listen(port, self.ip)
542 545 except socket.error as e2:
543 546 e = e2
544 547 else:
545 548 self.port = port
546 549 success = True
547 550 break
548 551 # restore the monekypatch
549 552 socket.AI_ADDRCONFIG = saved_AI_ADDRCONFIG
550 553 if e.errno != errno.EADDRINUSE:
551 554 raise
552 555 self.log.info('The port %i is already in use, trying another random port.' % port)
553 556 else:
554 557 self.port = port
555 558 success = True
556 559 break
557 560 if not success:
558 561 self.log.critical('ERROR: the notebook server could not be started because '
559 562 'no available port could be found.')
560 563 self.exit(1)
561 564
562 565 def init_signal(self):
563 566 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
564 567 # safely extract zmq version info:
565 568 try:
566 569 zmq_v = zmq.pyzmq_version_info()
567 570 except AttributeError:
568 571 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
569 572 if 'dev' in zmq.__version__:
570 573 zmq_v.append(999)
571 574 zmq_v = tuple(zmq_v)
572 575 if zmq_v >= (2,1,9) and not sys.platform.startswith('win'):
573 576 # This won't work with 2.1.7 and
574 577 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
575 578 # but it will work
576 579 signal.signal(signal.SIGINT, self._handle_sigint)
577 580 signal.signal(signal.SIGTERM, self._signal_stop)
578 581 if hasattr(signal, 'SIGUSR1'):
579 582 # Windows doesn't support SIGUSR1
580 583 signal.signal(signal.SIGUSR1, self._signal_info)
581 584 if hasattr(signal, 'SIGINFO'):
582 585 # only on BSD-based systems
583 586 signal.signal(signal.SIGINFO, self._signal_info)
584 587
585 588 def _handle_sigint(self, sig, frame):
586 589 """SIGINT handler spawns confirmation dialog"""
587 590 # register more forceful signal handler for ^C^C case
588 591 signal.signal(signal.SIGINT, self._signal_stop)
589 592 # request confirmation dialog in bg thread, to avoid
590 593 # blocking the App
591 594 thread = threading.Thread(target=self._confirm_exit)
592 595 thread.daemon = True
593 596 thread.start()
594 597
595 598 def _restore_sigint_handler(self):
596 599 """callback for restoring original SIGINT handler"""
597 600 signal.signal(signal.SIGINT, self._handle_sigint)
598 601
599 602 def _confirm_exit(self):
600 603 """confirm shutdown on ^C
601 604
602 605 A second ^C, or answering 'y' within 5s will cause shutdown,
603 606 otherwise original SIGINT handler will be restored.
604 607
605 608 This doesn't work on Windows.
606 609 """
607 610 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
608 611 time.sleep(0.1)
609 612 info = self.log.info
610 613 info('interrupted')
611 614 print self.notebook_info()
612 615 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
613 616 sys.stdout.flush()
614 617 r,w,x = select.select([sys.stdin], [], [], 5)
615 618 if r:
616 619 line = sys.stdin.readline()
617 620 if line.lower().startswith('y'):
618 621 self.log.critical("Shutdown confirmed")
619 622 ioloop.IOLoop.instance().stop()
620 623 return
621 624 else:
622 625 print "No answer for 5s:",
623 626 print "resuming operation..."
624 627 # no answer, or answer is no:
625 628 # set it back to original SIGINT handler
626 629 # use IOLoop.add_callback because signal.signal must be called
627 630 # from main thread
628 631 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
629 632
630 633 def _signal_stop(self, sig, frame):
631 634 self.log.critical("received signal %s, stopping", sig)
632 635 ioloop.IOLoop.instance().stop()
633 636
634 637 def _signal_info(self, sig, frame):
635 638 print self.notebook_info()
636 639
637 640 @catch_config_error
638 641 def initialize(self, argv=None):
639 642 self.init_logging()
640 643 super(NotebookApp, self).initialize(argv)
641 644 self.init_configurables()
642 645 self.init_webapp()
643 646 self.init_signal()
644 647
645 648 def cleanup_kernels(self):
646 649 """Shutdown all kernels.
647 650
648 651 The kernels will shutdown themselves when this process no longer exists,
649 652 but explicit shutdown allows the KernelManagers to cleanup the connection files.
650 653 """
651 654 self.log.info('Shutting down kernels')
652 655 self.kernel_manager.shutdown_all()
653 656
654 657 def notebook_info(self):
655 658 "Return the current working directory and the server url information"
656 659 mgr_info = self.notebook_manager.info_string() + "\n"
657 660 return mgr_info +"The IPython Notebook is running at: %s" % self._url
658 661
659 662 def start(self):
660 663 """ Start the IPython Notebok server app, after initialization
661 664
662 665 This method takes no arguments so all configuration and initialization
663 666 must be done prior to calling this method."""
664 667 ip = self.ip if self.ip else '[all ip addresses on your system]'
665 668 proto = 'https' if self.certfile else 'http'
666 669 info = self.log.info
667 670 self._url = "%s://%s:%i%s" % (proto, ip, self.port,
668 671 self.base_project_url)
669 672 for line in self.notebook_info().split("\n"):
670 673 info(line)
671 674 info("Use Control-C to stop this server and shut down all kernels.")
672 675
673 676 if self.open_browser or self.file_to_run:
674 677 ip = self.ip or LOCALHOST
675 678 try:
676 679 browser = webbrowser.get(self.browser or None)
677 680 except webbrowser.Error as e:
678 681 self.log.warn('No web browser found: %s.' % e)
679 682 browser = None
680 683
681 684 if self.file_to_run:
682 685 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
683 686 url = self.notebook_manager.rev_mapping.get(name, '')
684 687 else:
685 688 url = ''
686 689 if browser:
687 690 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
688 691 self.port, self.base_project_url, url), new=2)
689 692 threading.Thread(target=b).start()
690 693 try:
691 694 ioloop.IOLoop.instance().start()
692 695 except KeyboardInterrupt:
693 696 info("Interrupted...")
694 697 finally:
695 698 self.cleanup_kernels()
696 699
697 700
698 701 #-----------------------------------------------------------------------------
699 702 # Main entry point
700 703 #-----------------------------------------------------------------------------
701 704
702 705 def launch_new_instance():
703 706 app = NotebookApp.instance()
704 707 app.initialize()
705 708 app.start()
706 709
General Comments 0
You need to be logged in to leave comments. Login now