##// END OF EJS Templates
update with forthcoming MathJax CDN move
MinRK -
Show More
@@ -1,563 +1,547 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 re
24 24 import select
25 25 import signal
26 26 import socket
27 27 import sys
28 28 import threading
29 29 import time
30 30 import webbrowser
31 31
32 32 # Third party
33 33 import zmq
34 34
35 35 # Install the pyzmq ioloop. This has to be done before anything else from
36 36 # tornado is imported.
37 37 from zmq.eventloop import ioloop
38 38 ioloop.install()
39 39
40 40 from tornado import httpserver
41 41 from tornado import web
42 42
43 43 # Our own libraries
44 44 from .kernelmanager import MappingKernelManager
45 45 from .handlers import (LoginHandler, LogoutHandler,
46 46 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
47 47 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
48 48 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
49 49 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
50 50 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
51 51 )
52 52 from .notebookmanager import NotebookManager
53 53 from .clustermanager import ClusterManager
54 54
55 55 from IPython.config.application import catch_config_error, boolean_flag
56 56 from IPython.core.application import BaseIPythonApplication
57 57 from IPython.core.profiledir import ProfileDir
58 58 from IPython.lib.kernel import swallow_argv
59 59 from IPython.zmq.session import Session, default_secure
60 60 from IPython.zmq.zmqshell import ZMQInteractiveShell
61 61 from IPython.zmq.ipkernel import (
62 62 flags as ipkernel_flags,
63 63 aliases as ipkernel_aliases,
64 64 IPKernelApp
65 65 )
66 66 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
67 67 from IPython.utils import py3compat
68 68
69 69 #-----------------------------------------------------------------------------
70 70 # Module globals
71 71 #-----------------------------------------------------------------------------
72 72
73 73 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
74 74 _kernel_action_regex = r"(?P<action>restart|interrupt)"
75 75 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
76 76 _profile_regex = r"(?P<profile>[a-zA-Z0-9]+)"
77 77 _cluster_action_regex = r"(?P<action>start|stop)"
78 78
79 79
80 80 LOCALHOST = '127.0.0.1'
81 81
82 82 _examples = """
83 83 ipython notebook # start the notebook
84 84 ipython notebook --profile=sympy # use the sympy profile
85 85 ipython notebook --pylab=inline # pylab in inline plotting mode
86 86 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
87 87 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
88 88 """
89 89
90 90 #-----------------------------------------------------------------------------
91 91 # Helper functions
92 92 #-----------------------------------------------------------------------------
93 93
94 94 def url_path_join(a,b):
95 95 if a.endswith('/') and b.startswith('/'):
96 96 return a[:-1]+b
97 97 else:
98 98 return a+b
99 99
100 100 #-----------------------------------------------------------------------------
101 101 # The Tornado web application
102 102 #-----------------------------------------------------------------------------
103 103
104 104 class NotebookWebApplication(web.Application):
105 105
106 106 def __init__(self, ipython_app, kernel_manager, notebook_manager,
107 107 cluster_manager, log,
108 108 base_project_url, settings_overrides):
109 109 handlers = [
110 110 (r"/", ProjectDashboardHandler),
111 111 (r"/login", LoginHandler),
112 112 (r"/logout", LogoutHandler),
113 113 (r"/new", NewHandler),
114 114 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
115 115 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
116 116 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
117 117 (r"/kernels", MainKernelHandler),
118 118 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
119 119 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
120 120 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
121 121 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
122 122 (r"/notebooks", NotebookRootHandler),
123 123 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
124 124 (r"/rstservice/render", RSTHandler),
125 125 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
126 126 (r"/clusters", MainClusterHandler),
127 127 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
128 128 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
129 129 ]
130 130 settings = dict(
131 131 template_path=os.path.join(os.path.dirname(__file__), "templates"),
132 132 static_path=os.path.join(os.path.dirname(__file__), "static"),
133 133 cookie_secret=os.urandom(1024),
134 134 login_url="/login",
135 135 )
136 136
137 137 # allow custom overrides for the tornado web app.
138 138 settings.update(settings_overrides)
139 139
140 140 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
141 141 # base_project_url will always be unicode, which will in turn
142 142 # make the patterns unicode, and ultimately result in unicode
143 143 # keys in kwargs to handler._execute(**kwargs) in tornado.
144 144 # This enforces that base_project_url be ascii in that situation.
145 145 #
146 146 # Note that the URLs these patterns check against are escaped,
147 147 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
148 148 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
149 149
150 150 # prepend base_project_url onto the patterns that we match
151 151 new_handlers = []
152 152 for handler in handlers:
153 153 pattern = url_path_join(base_project_url, handler[0])
154 154 new_handler = tuple([pattern]+list(handler[1:]))
155 155 new_handlers.append( new_handler )
156 156
157 157 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
158 158
159 159 self.kernel_manager = kernel_manager
160 160 self.notebook_manager = notebook_manager
161 161 self.cluster_manager = cluster_manager
162 162 self.ipython_app = ipython_app
163 163 self.read_only = self.ipython_app.read_only
164 164 self.log = log
165 165
166 166
167 167 #-----------------------------------------------------------------------------
168 168 # Aliases and Flags
169 169 #-----------------------------------------------------------------------------
170 170
171 171 flags = dict(ipkernel_flags)
172 172 flags['no-browser']=(
173 173 {'NotebookApp' : {'open_browser' : False}},
174 174 "Don't open the notebook in a browser after startup."
175 175 )
176 176 flags['no-mathjax']=(
177 177 {'NotebookApp' : {'enable_mathjax' : False}},
178 178 """Disable MathJax
179 179
180 180 MathJax is the javascript library IPython uses to render math/LaTeX. It is
181 181 very large, so you may want to disable it if you have a slow internet
182 182 connection, or for offline use of the notebook.
183 183
184 184 When disabled, equations etc. will appear as their untransformed TeX source.
185 185 """
186 186 )
187 187 flags['read-only'] = (
188 188 {'NotebookApp' : {'read_only' : True}},
189 189 """Allow read-only access to notebooks.
190 190
191 191 When using a password to protect the notebook server, this flag
192 192 allows unauthenticated clients to view the notebook list, and
193 193 individual notebooks, but not edit them, start kernels, or run
194 194 code.
195 195
196 196 If no password is set, the server will be entirely read-only.
197 197 """
198 198 )
199 199
200 200 # Add notebook manager flags
201 201 flags.update(boolean_flag('script', 'NotebookManager.save_script',
202 202 'Auto-save a .py script everytime the .ipynb notebook is saved',
203 203 'Do not auto-save .py scripts for every notebook'))
204 204
205 205 # the flags that are specific to the frontend
206 206 # these must be scrubbed before being passed to the kernel,
207 207 # or it will raise an error on unrecognized flags
208 208 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
209 209
210 210 aliases = dict(ipkernel_aliases)
211 211
212 212 aliases.update({
213 213 'ip': 'NotebookApp.ip',
214 214 'port': 'NotebookApp.port',
215 215 'keyfile': 'NotebookApp.keyfile',
216 216 'certfile': 'NotebookApp.certfile',
217 217 'notebook-dir': 'NotebookManager.notebook_dir',
218 218 'browser': 'NotebookApp.browser',
219 219 })
220 220
221 221 # remove ipkernel flags that are singletons, and don't make sense in
222 222 # multi-kernel evironment:
223 223 aliases.pop('f', None)
224 224
225 225 notebook_aliases = [u'port', u'ip', u'keyfile', u'certfile',
226 226 u'notebook-dir']
227 227
228 228 #-----------------------------------------------------------------------------
229 229 # NotebookApp
230 230 #-----------------------------------------------------------------------------
231 231
232 232 class NotebookApp(BaseIPythonApplication):
233 233
234 234 name = 'ipython-notebook'
235 235 default_config_file_name='ipython_notebook_config.py'
236 236
237 237 description = """
238 238 The IPython HTML Notebook.
239 239
240 240 This launches a Tornado based HTML Notebook Server that serves up an
241 241 HTML5/Javascript Notebook client.
242 242 """
243 243 examples = _examples
244 244
245 245 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session,
246 246 MappingKernelManager, NotebookManager]
247 247 flags = Dict(flags)
248 248 aliases = Dict(aliases)
249 249
250 250 kernel_argv = List(Unicode)
251 251
252 252 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
253 253 default_value=logging.INFO,
254 254 config=True,
255 255 help="Set the log level by value or name.")
256 256
257 257 # create requested profiles by default, if they don't exist:
258 258 auto_create = Bool(True)
259 259
260 260 # Network related information.
261 261
262 262 ip = Unicode(LOCALHOST, config=True,
263 263 help="The IP address the notebook server will listen on."
264 264 )
265 265
266 266 def _ip_changed(self, name, old, new):
267 267 if new == u'*': self.ip = u''
268 268
269 269 port = Integer(8888, config=True,
270 270 help="The port the notebook server will listen on."
271 271 )
272 272
273 273 certfile = Unicode(u'', config=True,
274 274 help="""The full path to an SSL/TLS certificate file."""
275 275 )
276 276
277 277 keyfile = Unicode(u'', config=True,
278 278 help="""The full path to a private key file for usage with SSL/TLS."""
279 279 )
280 280
281 281 password = Unicode(u'', config=True,
282 282 help="""Hashed password to use for web authentication.
283 283
284 284 To generate, type in a python/IPython shell:
285 285
286 286 from IPython.lib import passwd; passwd()
287 287
288 288 The string should be of the form type:salt:hashed-password.
289 289 """
290 290 )
291 291
292 292 open_browser = Bool(True, config=True,
293 293 help="""Whether to open in a browser after starting.
294 294 The specific browser used is platform dependent and
295 295 determined by the python standard library `webbrowser`
296 296 module, unless it is overridden using the --browser
297 297 (NotebookApp.browser) configuration option.
298 298 """)
299 299
300 300 browser = Unicode(u'', config=True,
301 301 help="""Specify what command to use to invoke a web
302 302 browser when opening the notebook. If not specified, the
303 303 default browser will be determined by the `webbrowser`
304 304 standard library module, which allows setting of the
305 305 BROWSER environment variable to override it.
306 306 """)
307 307
308 308 read_only = Bool(False, config=True,
309 309 help="Whether to prevent editing/execution of notebooks."
310 310 )
311 311
312 312 webapp_settings = Dict(config=True,
313 313 help="Supply overrides for the tornado.web.Application that the "
314 314 "IPython notebook uses.")
315 315
316 316 enable_mathjax = Bool(True, config=True,
317 317 help="""Whether to enable MathJax for typesetting math/TeX
318 318
319 319 MathJax is the javascript library IPython uses to render math/LaTeX. It is
320 320 very large, so you may want to disable it if you have a slow internet
321 321 connection, or for offline use of the notebook.
322 322
323 323 When disabled, equations etc. will appear as their untransformed TeX source.
324 324 """
325 325 )
326 326 def _enable_mathjax_changed(self, name, old, new):
327 327 """set mathjax url to empty if mathjax is disabled"""
328 328 if not new:
329 329 self.mathjax_url = u''
330 330
331 331 base_project_url = Unicode('/', config=True,
332 332 help='''The base URL for the notebook server''')
333 333 base_kernel_url = Unicode('/', config=True,
334 334 help='''The base URL for the kernel server''')
335 335 websocket_host = Unicode("", config=True,
336 336 help="""The hostname for the websocket server."""
337 337 )
338 338
339 339 mathjax_url = Unicode("", config=True,
340 340 help="""The url for MathJax.js."""
341 341 )
342 342 def _mathjax_url_default(self):
343 343 if not self.enable_mathjax:
344 344 return u''
345 345 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
346 346 static_url_prefix = self.webapp_settings.get("static_url_prefix",
347 347 "/static/")
348 348 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
349 349 self.log.info("Using local MathJax")
350 350 return static_url_prefix+u"mathjax/MathJax.js"
351 351 else:
352 self.log.info("Using MathJax from CDN")
353 hostname = "cdn.mathjax.org"
354 try:
355 # resolve mathjax cdn alias to cloudfront, because Amazon's SSL certificate
356 # only works on *.cloudfront.net
357 true_host, aliases, IPs = socket.gethostbyname_ex(hostname)
358 # I've run this on a few machines, and some put the right answer in true_host,
359 # while others gave it in the aliases list, so we check both.
360 aliases.insert(0, true_host)
361 except Exception:
362 self.log.warn("Couldn't determine MathJax CDN info")
352 if self.certfile:
353 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
354 base = u"https://c328740.ssl.cf1.rackcdn.com"
363 355 else:
364 for alias in aliases:
365 parts = alias.split('.')
366 # want static foo.cloudfront.net, not dynamic foo.lax3.cloudfront.net
367 if len(parts) == 3 and alias.endswith(".cloudfront.net"):
368 hostname = alias
369 break
356 base = u"http://cdn.mathjax.org"
370 357
371 if not hostname.endswith(".cloudfront.net"):
372 self.log.error("Couldn't resolve CloudFront host, required for HTTPS MathJax.")
373 self.log.error("Loading from https://cdn.mathjax.org will probably fail due to invalid certificate.")
374 self.log.error("For unsecured HTTP access to MathJax use config:")
375 self.log.error("NotebookApp.mathjax_url='http://cdn.mathjax.org/mathjax/latest/MathJax.js'")
376 return u"https://%s/mathjax/latest/MathJax.js" % hostname
358 url = base + u"/mathjax/latest/MathJax.js"
359 self.log.info("Using MathJax from CDN: %s", url)
360 return url
377 361
378 362 def _mathjax_url_changed(self, name, old, new):
379 363 if new and not self.enable_mathjax:
380 364 # enable_mathjax=False overrides mathjax_url
381 365 self.mathjax_url = u''
382 366 else:
383 367 self.log.info("Using MathJax: %s", new)
384 368
385 369 def parse_command_line(self, argv=None):
386 370 super(NotebookApp, self).parse_command_line(argv)
387 371 if argv is None:
388 372 argv = sys.argv[1:]
389 373
390 374 # Scrub frontend-specific flags
391 375 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
392 376 # Kernel should inherit default config file from frontend
393 377 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
394 378
395 379 def init_configurables(self):
396 380 # force Session default to be secure
397 381 default_secure(self.config)
398 382 # Create a KernelManager and start a kernel.
399 383 self.kernel_manager = MappingKernelManager(
400 384 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
401 385 connection_dir = self.profile_dir.security_dir,
402 386 )
403 387 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
404 388 self.notebook_manager.list_notebooks()
405 389 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
406 390 self.cluster_manager.update_profiles()
407 391
408 392 def init_logging(self):
409 393 super(NotebookApp, self).init_logging()
410 394 # This prevents double log messages because tornado use a root logger that
411 395 # self.log is a child of. The logging module dipatches log messages to a log
412 396 # and all of its ancenstors until propagate is set to False.
413 397 self.log.propagate = False
414 398
415 399 def init_webapp(self):
416 400 """initialize tornado webapp and httpserver"""
417 401 self.web_app = NotebookWebApplication(
418 402 self, self.kernel_manager, self.notebook_manager,
419 403 self.cluster_manager, self.log,
420 404 self.base_project_url, self.webapp_settings
421 405 )
422 406 if self.certfile:
423 407 ssl_options = dict(certfile=self.certfile)
424 408 if self.keyfile:
425 409 ssl_options['keyfile'] = self.keyfile
426 410 else:
427 411 ssl_options = None
428 412 self.web_app.password = self.password
429 413 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
430 414 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
431 415 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
432 416 'but not using any encryption or authentication. This is highly '
433 417 'insecure and not recommended.')
434 418
435 419 # Try random ports centered around the default.
436 420 from random import randint
437 421 n = 50 # Max number of attempts, keep reasonably large.
438 422 for port in range(self.port, self.port+5) + [self.port + randint(-2*n, 2*n) for i in range(n-5)]:
439 423 try:
440 424 self.http_server.listen(port, self.ip)
441 425 except socket.error, e:
442 426 if e.errno != errno.EADDRINUSE:
443 427 raise
444 428 self.log.info('The port %i is already in use, trying another random port.' % port)
445 429 else:
446 430 self.port = port
447 431 break
448 432
449 433 def init_signal(self):
450 434 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
451 435 # safely extract zmq version info:
452 436 try:
453 437 zmq_v = zmq.pyzmq_version_info()
454 438 except AttributeError:
455 439 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
456 440 if 'dev' in zmq.__version__:
457 441 zmq_v.append(999)
458 442 zmq_v = tuple(zmq_v)
459 443 if zmq_v >= (2,1,9):
460 444 # This won't work with 2.1.7 and
461 445 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
462 446 # but it will work
463 447 signal.signal(signal.SIGINT, self._handle_sigint)
464 448 signal.signal(signal.SIGTERM, self._signal_stop)
465 449
466 450 def _handle_sigint(self, sig, frame):
467 451 """SIGINT handler spawns confirmation dialog"""
468 452 # register more forceful signal handler for ^C^C case
469 453 signal.signal(signal.SIGINT, self._signal_stop)
470 454 # request confirmation dialog in bg thread, to avoid
471 455 # blocking the App
472 456 thread = threading.Thread(target=self._confirm_exit)
473 457 thread.daemon = True
474 458 thread.start()
475 459
476 460 def _restore_sigint_handler(self):
477 461 """callback for restoring original SIGINT handler"""
478 462 signal.signal(signal.SIGINT, self._handle_sigint)
479 463
480 464 def _confirm_exit(self):
481 465 """confirm shutdown on ^C
482 466
483 467 A second ^C, or answering 'y' within 5s will cause shutdown,
484 468 otherwise original SIGINT handler will be restored.
485 469 """
486 470 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
487 471 time.sleep(0.1)
488 472 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
489 473 sys.stdout.flush()
490 474 r,w,x = select.select([sys.stdin], [], [], 5)
491 475 if r:
492 476 line = sys.stdin.readline()
493 477 if line.lower().startswith('y'):
494 478 self.log.critical("Shutdown confirmed")
495 479 ioloop.IOLoop.instance().stop()
496 480 return
497 481 else:
498 482 print "No answer for 5s:",
499 483 print "resuming operation..."
500 484 # no answer, or answer is no:
501 485 # set it back to original SIGINT handler
502 486 # use IOLoop.add_callback because signal.signal must be called
503 487 # from main thread
504 488 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
505 489
506 490 def _signal_stop(self, sig, frame):
507 491 self.log.critical("received signal %s, stopping", sig)
508 492 ioloop.IOLoop.instance().stop()
509 493
510 494 @catch_config_error
511 495 def initialize(self, argv=None):
512 496 super(NotebookApp, self).initialize(argv)
513 497 self.init_configurables()
514 498 self.init_webapp()
515 499 self.init_signal()
516 500
517 501 def cleanup_kernels(self):
518 502 """shutdown all kernels
519 503
520 504 The kernels will shutdown themselves when this process no longer exists,
521 505 but explicit shutdown allows the KernelManagers to cleanup the connection files.
522 506 """
523 507 self.log.info('Shutting down kernels')
524 508 km = self.kernel_manager
525 509 # copy list, since kill_kernel deletes keys
526 510 for kid in list(km.kernel_ids):
527 511 km.kill_kernel(kid)
528 512
529 513 def start(self):
530 514 ip = self.ip if self.ip else '[all ip addresses on your system]'
531 515 proto = 'https' if self.certfile else 'http'
532 516 info = self.log.info
533 517 info("The IPython Notebook is running at: %s://%s:%i%s" %
534 518 (proto, ip, self.port,self.base_project_url) )
535 519 info("Use Control-C to stop this server and shut down all kernels.")
536 520
537 521 if self.open_browser:
538 522 ip = self.ip or '127.0.0.1'
539 523 if self.browser:
540 524 browser = webbrowser.get(self.browser)
541 525 else:
542 526 browser = webbrowser.get()
543 527 b = lambda : browser.open("%s://%s:%i%s" % (proto, ip, self.port,
544 528 self.base_project_url),
545 529 new=2)
546 530 threading.Thread(target=b).start()
547 531 try:
548 532 ioloop.IOLoop.instance().start()
549 533 except KeyboardInterrupt:
550 534 info("Interrupted...")
551 535 finally:
552 536 self.cleanup_kernels()
553 537
554 538
555 539 #-----------------------------------------------------------------------------
556 540 # Main entry point
557 541 #-----------------------------------------------------------------------------
558 542
559 543 def launch_new_instance():
560 544 app = NotebookApp.instance()
561 545 app.initialize()
562 546 app.start()
563 547
General Comments 0
You need to be logged in to leave comments. Login now