##// END OF EJS Templates
Merge pull request #8587 from minrk/template-string-types...
Matthias Bussonnier -
r21513:b16c2cae merge
parent child Browse files
Show More
@@ -1,1147 +1,1147 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 from __future__ import print_function
8 8
9 9 import base64
10 10 import datetime
11 11 import errno
12 12 import importlib
13 13 import io
14 14 import json
15 15 import logging
16 16 import os
17 17 import random
18 18 import re
19 19 import select
20 20 import signal
21 21 import socket
22 22 import ssl
23 23 import sys
24 24 import threading
25 25 import webbrowser
26 26
27 27
28 28 # check for pyzmq
29 29 from IPython.utils.zmqrelated import check_for_zmq
30 30 check_for_zmq('13', 'IPython.html')
31 31
32 32 from jinja2 import Environment, FileSystemLoader
33 33
34 34 # Install the pyzmq ioloop. This has to be done before anything else from
35 35 # tornado is imported.
36 36 from zmq.eventloop import ioloop
37 37 ioloop.install()
38 38
39 39 # check for tornado 3.1.0
40 40 msg = "The IPython Notebook requires tornado >= 4.0"
41 41 try:
42 42 import tornado
43 43 except ImportError:
44 44 raise ImportError(msg)
45 45 try:
46 46 version_info = tornado.version_info
47 47 except AttributeError:
48 48 raise ImportError(msg + ", but you have < 1.1.0")
49 49 if version_info < (4,0):
50 50 raise ImportError(msg + ", but you have %s" % tornado.version)
51 51
52 52 from tornado import httpserver
53 53 from tornado import web
54 54 from tornado.log import LogFormatter, app_log, access_log, gen_log
55 55
56 56 from IPython.html import (
57 57 DEFAULT_STATIC_FILES_PATH,
58 58 DEFAULT_TEMPLATE_PATH_LIST,
59 59 )
60 60 from .base.handlers import Template404
61 61 from .log import log_request
62 62 from .services.kernels.kernelmanager import MappingKernelManager
63 63 from .services.config import ConfigManager
64 64 from .services.contents.manager import ContentsManager
65 65 from .services.contents.filemanager import FileContentsManager
66 66 from .services.clusters.clustermanager import ClusterManager
67 67 from .services.sessions.sessionmanager import SessionManager
68 68
69 69 from .auth.login import LoginHandler
70 70 from .auth.logout import LogoutHandler
71 71 from .base.handlers import IPythonHandler, FileFindHandler
72 72
73 73 from IPython.config import Config
74 74 from IPython.config.application import catch_config_error, boolean_flag
75 75 from IPython.core.application import (
76 76 BaseIPythonApplication, base_flags, base_aliases,
77 77 )
78 78 from IPython.core.profiledir import ProfileDir
79 79 from IPython.kernel import KernelManager
80 80 from IPython.kernel.kernelspec import KernelSpecManager
81 81 from IPython.kernel.zmq.session import Session
82 82 from IPython.nbformat.sign import NotebookNotary
83 83 from IPython.utils.importstring import import_item
84 84 from IPython.utils import submodule
85 85 from IPython.utils.process import check_pid
86 86 from IPython.utils.traitlets import (
87 87 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
88 88 TraitError, Type,
89 89 )
90 90 from IPython.utils import py3compat
91 91 from IPython.utils.path import filefind, get_ipython_dir
92 92 from IPython.utils.sysinfo import get_sys_info
93 93
94 94 from .nbextensions import SYSTEM_NBEXTENSIONS_DIRS
95 95 from .utils import url_path_join
96 96
97 97 #-----------------------------------------------------------------------------
98 98 # Module globals
99 99 #-----------------------------------------------------------------------------
100 100
101 101 _examples = """
102 102 ipython notebook # start the notebook
103 103 ipython notebook --profile=sympy # use the sympy profile
104 104 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
105 105 """
106 106
107 107 #-----------------------------------------------------------------------------
108 108 # Helper functions
109 109 #-----------------------------------------------------------------------------
110 110
111 111 def random_ports(port, n):
112 112 """Generate a list of n random ports near the given port.
113 113
114 114 The first 5 ports will be sequential, and the remaining n-5 will be
115 115 randomly selected in the range [port-2*n, port+2*n].
116 116 """
117 117 for i in range(min(5, n)):
118 118 yield port + i
119 119 for i in range(n-5):
120 120 yield max(1, port + random.randint(-2*n, 2*n))
121 121
122 122 def load_handlers(name):
123 123 """Load the (URL pattern, handler) tuples for each component."""
124 124 name = 'IPython.html.' + name
125 125 mod = __import__(name, fromlist=['default_handlers'])
126 126 return mod.default_handlers
127 127
128 128 #-----------------------------------------------------------------------------
129 129 # The Tornado web application
130 130 #-----------------------------------------------------------------------------
131 131
132 132 class NotebookWebApplication(web.Application):
133 133
134 134 def __init__(self, ipython_app, kernel_manager, contents_manager,
135 135 cluster_manager, session_manager, kernel_spec_manager,
136 136 config_manager, log,
137 137 base_url, default_url, settings_overrides, jinja_env_options):
138 138
139 139 settings = self.init_settings(
140 140 ipython_app, kernel_manager, contents_manager, cluster_manager,
141 141 session_manager, kernel_spec_manager, config_manager, log, base_url,
142 142 default_url, settings_overrides, jinja_env_options)
143 143 handlers = self.init_handlers(settings)
144 144
145 145 super(NotebookWebApplication, self).__init__(handlers, **settings)
146 146
147 147 def init_settings(self, ipython_app, kernel_manager, contents_manager,
148 148 cluster_manager, session_manager, kernel_spec_manager,
149 149 config_manager,
150 150 log, base_url, default_url, settings_overrides,
151 151 jinja_env_options=None):
152 152
153 153 _template_path = settings_overrides.get(
154 154 "template_path",
155 155 ipython_app.template_file_path,
156 156 )
157 if isinstance(_template_path, str):
157 if isinstance(_template_path, py3compat.string_types):
158 158 _template_path = (_template_path,)
159 159 template_path = [os.path.expanduser(path) for path in _template_path]
160 160
161 161 jenv_opt = jinja_env_options if jinja_env_options else {}
162 162 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
163 163
164 164 sys_info = get_sys_info()
165 165 if sys_info['commit_source'] == 'repository':
166 166 # don't cache (rely on 304) when working from master
167 167 version_hash = ''
168 168 else:
169 169 # reset the cache on server restart
170 170 version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
171 171
172 172 settings = dict(
173 173 # basics
174 174 log_function=log_request,
175 175 base_url=base_url,
176 176 default_url=default_url,
177 177 template_path=template_path,
178 178 static_path=ipython_app.static_file_path,
179 179 static_handler_class = FileFindHandler,
180 180 static_url_prefix = url_path_join(base_url,'/static/'),
181 181 static_handler_args = {
182 182 # don't cache custom.js
183 183 'no_cache_paths': [url_path_join(base_url, 'static', 'custom')],
184 184 },
185 185 version_hash=version_hash,
186 186
187 187 # authentication
188 188 cookie_secret=ipython_app.cookie_secret,
189 189 login_url=url_path_join(base_url,'/login'),
190 190 login_handler_class=ipython_app.login_handler_class,
191 191 logout_handler_class=ipython_app.logout_handler_class,
192 192 password=ipython_app.password,
193 193
194 194 # managers
195 195 kernel_manager=kernel_manager,
196 196 contents_manager=contents_manager,
197 197 cluster_manager=cluster_manager,
198 198 session_manager=session_manager,
199 199 kernel_spec_manager=kernel_spec_manager,
200 200 config_manager=config_manager,
201 201
202 202 # IPython stuff
203 203 jinja_template_vars=ipython_app.jinja_template_vars,
204 204 nbextensions_path=ipython_app.nbextensions_path,
205 205 websocket_url=ipython_app.websocket_url,
206 206 mathjax_url=ipython_app.mathjax_url,
207 207 config=ipython_app.config,
208 208 jinja2_env=env,
209 209 terminals_available=False, # Set later if terminals are available
210 210 )
211 211
212 212 # allow custom overrides for the tornado web app.
213 213 settings.update(settings_overrides)
214 214 return settings
215 215
216 216 def init_handlers(self, settings):
217 217 """Load the (URL pattern, handler) tuples for each component."""
218 218
219 219 # Order matters. The first handler to match the URL will handle the request.
220 220 handlers = []
221 221 handlers.extend(load_handlers('tree.handlers'))
222 222 handlers.extend([(r"/login", settings['login_handler_class'])])
223 223 handlers.extend([(r"/logout", settings['logout_handler_class'])])
224 224 handlers.extend(load_handlers('files.handlers'))
225 225 handlers.extend(load_handlers('notebook.handlers'))
226 226 handlers.extend(load_handlers('nbconvert.handlers'))
227 227 handlers.extend(load_handlers('kernelspecs.handlers'))
228 228 handlers.extend(load_handlers('edit.handlers'))
229 229 handlers.extend(load_handlers('services.config.handlers'))
230 230 handlers.extend(load_handlers('services.kernels.handlers'))
231 231 handlers.extend(load_handlers('services.contents.handlers'))
232 232 handlers.extend(load_handlers('services.clusters.handlers'))
233 233 handlers.extend(load_handlers('services.sessions.handlers'))
234 234 handlers.extend(load_handlers('services.nbconvert.handlers'))
235 235 handlers.extend(load_handlers('services.kernelspecs.handlers'))
236 236 handlers.extend(load_handlers('services.security.handlers'))
237 237 handlers.append(
238 238 (r"/nbextensions/(.*)", FileFindHandler, {
239 239 'path': settings['nbextensions_path'],
240 240 'no_cache_paths': ['/'], # don't cache anything in nbextensions
241 241 }),
242 242 )
243 243 # register base handlers last
244 244 handlers.extend(load_handlers('base.handlers'))
245 245 # set the URL that will be redirected from `/`
246 246 handlers.append(
247 247 (r'/?', web.RedirectHandler, {
248 248 'url' : settings['default_url'],
249 249 'permanent': False, # want 302, not 301
250 250 })
251 251 )
252 252 # prepend base_url onto the patterns that we match
253 253 new_handlers = []
254 254 for handler in handlers:
255 255 pattern = url_path_join(settings['base_url'], handler[0])
256 256 new_handler = tuple([pattern] + list(handler[1:]))
257 257 new_handlers.append(new_handler)
258 258 # add 404 on the end, which will catch everything that falls through
259 259 new_handlers.append((r'(.*)', Template404))
260 260 return new_handlers
261 261
262 262
263 263 class NbserverListApp(BaseIPythonApplication):
264 264
265 265 description="List currently running notebook servers in this profile."
266 266
267 267 flags = dict(
268 268 json=({'NbserverListApp': {'json': True}},
269 269 "Produce machine-readable JSON output."),
270 270 )
271 271
272 272 json = Bool(False, config=True,
273 273 help="If True, each line of output will be a JSON object with the "
274 274 "details from the server info file.")
275 275
276 276 def start(self):
277 277 if not self.json:
278 278 print("Currently running servers:")
279 279 for serverinfo in list_running_servers(self.profile):
280 280 if self.json:
281 281 print(json.dumps(serverinfo))
282 282 else:
283 283 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
284 284
285 285 #-----------------------------------------------------------------------------
286 286 # Aliases and Flags
287 287 #-----------------------------------------------------------------------------
288 288
289 289 flags = dict(base_flags)
290 290 flags['no-browser']=(
291 291 {'NotebookApp' : {'open_browser' : False}},
292 292 "Don't open the notebook in a browser after startup."
293 293 )
294 294 flags['pylab']=(
295 295 {'NotebookApp' : {'pylab' : 'warn'}},
296 296 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
297 297 )
298 298 flags['no-mathjax']=(
299 299 {'NotebookApp' : {'enable_mathjax' : False}},
300 300 """Disable MathJax
301 301
302 302 MathJax is the javascript library IPython uses to render math/LaTeX. It is
303 303 very large, so you may want to disable it if you have a slow internet
304 304 connection, or for offline use of the notebook.
305 305
306 306 When disabled, equations etc. will appear as their untransformed TeX source.
307 307 """
308 308 )
309 309
310 310 # Add notebook manager flags
311 311 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
312 312 'DEPRECATED, IGNORED',
313 313 'DEPRECATED, IGNORED'))
314 314
315 315 aliases = dict(base_aliases)
316 316
317 317 aliases.update({
318 318 'ip': 'NotebookApp.ip',
319 319 'port': 'NotebookApp.port',
320 320 'port-retries': 'NotebookApp.port_retries',
321 321 'transport': 'KernelManager.transport',
322 322 'keyfile': 'NotebookApp.keyfile',
323 323 'certfile': 'NotebookApp.certfile',
324 324 'notebook-dir': 'NotebookApp.notebook_dir',
325 325 'browser': 'NotebookApp.browser',
326 326 'pylab': 'NotebookApp.pylab',
327 327 })
328 328
329 329 #-----------------------------------------------------------------------------
330 330 # NotebookApp
331 331 #-----------------------------------------------------------------------------
332 332
333 333 class NotebookApp(BaseIPythonApplication):
334 334
335 335 name = 'ipython-notebook'
336 336
337 337 description = """
338 338 The IPython HTML Notebook.
339 339
340 340 This launches a Tornado based HTML Notebook Server that serves up an
341 341 HTML5/Javascript Notebook client.
342 342 """
343 343 examples = _examples
344 344 aliases = aliases
345 345 flags = flags
346 346
347 347 classes = [
348 348 KernelManager, ProfileDir, Session, MappingKernelManager,
349 349 ContentsManager, FileContentsManager, NotebookNotary,
350 350 KernelSpecManager,
351 351 ]
352 352 flags = Dict(flags)
353 353 aliases = Dict(aliases)
354 354
355 355 subcommands = dict(
356 356 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
357 357 )
358 358
359 359 ipython_kernel_argv = List(Unicode)
360 360
361 361 _log_formatter_cls = LogFormatter
362 362
363 363 def _log_level_default(self):
364 364 return logging.INFO
365 365
366 366 def _log_datefmt_default(self):
367 367 """Exclude date from default date format"""
368 368 return "%H:%M:%S"
369 369
370 370 def _log_format_default(self):
371 371 """override default log format to include time"""
372 372 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
373 373
374 374 # create requested profiles by default, if they don't exist:
375 375 auto_create = Bool(True)
376 376
377 377 # file to be opened in the notebook server
378 378 file_to_run = Unicode('', config=True)
379 379
380 380 # Network related information
381 381
382 382 allow_origin = Unicode('', config=True,
383 383 help="""Set the Access-Control-Allow-Origin header
384 384
385 385 Use '*' to allow any origin to access your server.
386 386
387 387 Takes precedence over allow_origin_pat.
388 388 """
389 389 )
390 390
391 391 allow_origin_pat = Unicode('', config=True,
392 392 help="""Use a regular expression for the Access-Control-Allow-Origin header
393 393
394 394 Requests from an origin matching the expression will get replies with:
395 395
396 396 Access-Control-Allow-Origin: origin
397 397
398 398 where `origin` is the origin of the request.
399 399
400 400 Ignored if allow_origin is set.
401 401 """
402 402 )
403 403
404 404 allow_credentials = Bool(False, config=True,
405 405 help="Set the Access-Control-Allow-Credentials: true header"
406 406 )
407 407
408 408 default_url = Unicode('/tree', config=True,
409 409 help="The default URL to redirect to from `/`"
410 410 )
411 411
412 412 ip = Unicode('localhost', config=True,
413 413 help="The IP address the notebook server will listen on."
414 414 )
415 415 def _ip_default(self):
416 416 """Return localhost if available, 127.0.0.1 otherwise.
417 417
418 418 On some (horribly broken) systems, localhost cannot be bound.
419 419 """
420 420 s = socket.socket()
421 421 try:
422 422 s.bind(('localhost', 0))
423 423 except socket.error as e:
424 424 self.log.warn("Cannot bind to localhost, using 127.0.0.1 as default ip\n%s", e)
425 425 return '127.0.0.1'
426 426 else:
427 427 s.close()
428 428 return 'localhost'
429 429
430 430 def _ip_changed(self, name, old, new):
431 431 if new == u'*': self.ip = u''
432 432
433 433 port = Integer(8888, config=True,
434 434 help="The port the notebook server will listen on."
435 435 )
436 436 port_retries = Integer(50, config=True,
437 437 help="The number of additional ports to try if the specified port is not available."
438 438 )
439 439
440 440 certfile = Unicode(u'', config=True,
441 441 help="""The full path to an SSL/TLS certificate file."""
442 442 )
443 443
444 444 keyfile = Unicode(u'', config=True,
445 445 help="""The full path to a private key file for usage with SSL/TLS."""
446 446 )
447 447
448 448 cookie_secret_file = Unicode(config=True,
449 449 help="""The file where the cookie secret is stored."""
450 450 )
451 451 def _cookie_secret_file_default(self):
452 452 if self.profile_dir is None:
453 453 return ''
454 454 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
455 455
456 456 cookie_secret = Bytes(b'', config=True,
457 457 help="""The random bytes used to secure cookies.
458 458 By default this is a new random number every time you start the Notebook.
459 459 Set it to a value in a config file to enable logins to persist across server sessions.
460 460
461 461 Note: Cookie secrets should be kept private, do not share config files with
462 462 cookie_secret stored in plaintext (you can read the value from a file).
463 463 """
464 464 )
465 465 def _cookie_secret_default(self):
466 466 if os.path.exists(self.cookie_secret_file):
467 467 with io.open(self.cookie_secret_file, 'rb') as f:
468 468 return f.read()
469 469 else:
470 470 secret = base64.encodestring(os.urandom(1024))
471 471 self._write_cookie_secret_file(secret)
472 472 return secret
473 473
474 474 def _write_cookie_secret_file(self, secret):
475 475 """write my secret to my secret_file"""
476 476 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
477 477 with io.open(self.cookie_secret_file, 'wb') as f:
478 478 f.write(secret)
479 479 try:
480 480 os.chmod(self.cookie_secret_file, 0o600)
481 481 except OSError:
482 482 self.log.warn(
483 483 "Could not set permissions on %s",
484 484 self.cookie_secret_file
485 485 )
486 486
487 487 password = Unicode(u'', config=True,
488 488 help="""Hashed password to use for web authentication.
489 489
490 490 To generate, type in a python/IPython shell:
491 491
492 492 from IPython.lib import passwd; passwd()
493 493
494 494 The string should be of the form type:salt:hashed-password.
495 495 """
496 496 )
497 497
498 498 open_browser = Bool(True, config=True,
499 499 help="""Whether to open in a browser after starting.
500 500 The specific browser used is platform dependent and
501 501 determined by the python standard library `webbrowser`
502 502 module, unless it is overridden using the --browser
503 503 (NotebookApp.browser) configuration option.
504 504 """)
505 505
506 506 browser = Unicode(u'', config=True,
507 507 help="""Specify what command to use to invoke a web
508 508 browser when opening the notebook. If not specified, the
509 509 default browser will be determined by the `webbrowser`
510 510 standard library module, which allows setting of the
511 511 BROWSER environment variable to override it.
512 512 """)
513 513
514 514 webapp_settings = Dict(config=True,
515 515 help="DEPRECATED, use tornado_settings"
516 516 )
517 517 def _webapp_settings_changed(self, name, old, new):
518 518 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
519 519 self.tornado_settings = new
520 520
521 521 tornado_settings = Dict(config=True,
522 522 help="Supply overrides for the tornado.web.Application that the "
523 523 "IPython notebook uses.")
524 524
525 525 ssl_options = Dict(config=True,
526 526 help="""Supply SSL options for the tornado HTTPServer.
527 527 See the tornado docs for details.""")
528 528
529 529 jinja_environment_options = Dict(config=True,
530 530 help="Supply extra arguments that will be passed to Jinja environment.")
531 531
532 532 jinja_template_vars = Dict(
533 533 config=True,
534 534 help="Extra variables to supply to jinja templates when rendering.",
535 535 )
536 536
537 537 enable_mathjax = Bool(True, config=True,
538 538 help="""Whether to enable MathJax for typesetting math/TeX
539 539
540 540 MathJax is the javascript library IPython uses to render math/LaTeX. It is
541 541 very large, so you may want to disable it if you have a slow internet
542 542 connection, or for offline use of the notebook.
543 543
544 544 When disabled, equations etc. will appear as their untransformed TeX source.
545 545 """
546 546 )
547 547 def _enable_mathjax_changed(self, name, old, new):
548 548 """set mathjax url to empty if mathjax is disabled"""
549 549 if not new:
550 550 self.mathjax_url = u''
551 551
552 552 base_url = Unicode('/', config=True,
553 553 help='''The base URL for the notebook server.
554 554
555 555 Leading and trailing slashes can be omitted,
556 556 and will automatically be added.
557 557 ''')
558 558 def _base_url_changed(self, name, old, new):
559 559 if not new.startswith('/'):
560 560 self.base_url = '/'+new
561 561 elif not new.endswith('/'):
562 562 self.base_url = new+'/'
563 563
564 564 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
565 565 def _base_project_url_changed(self, name, old, new):
566 566 self.log.warn("base_project_url is deprecated, use base_url")
567 567 self.base_url = new
568 568
569 569 extra_static_paths = List(Unicode, config=True,
570 570 help="""Extra paths to search for serving static files.
571 571
572 572 This allows adding javascript/css to be available from the notebook server machine,
573 573 or overriding individual files in the IPython"""
574 574 )
575 575 def _extra_static_paths_default(self):
576 576 return [os.path.join(self.profile_dir.location, 'static')]
577 577
578 578 @property
579 579 def static_file_path(self):
580 580 """return extra paths + the default location"""
581 581 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
582 582
583 583 extra_template_paths = List(Unicode, config=True,
584 584 help="""Extra paths to search for serving jinja templates.
585 585
586 586 Can be used to override templates from IPython.html.templates."""
587 587 )
588 588 def _extra_template_paths_default(self):
589 589 return []
590 590
591 591 @property
592 592 def template_file_path(self):
593 593 """return extra paths + the default locations"""
594 594 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
595 595
596 596 extra_nbextensions_path = List(Unicode, config=True,
597 597 help="""extra paths to look for Javascript notebook extensions"""
598 598 )
599 599
600 600 @property
601 601 def nbextensions_path(self):
602 602 """The path to look for Javascript notebook extensions"""
603 603 return self.extra_nbextensions_path + [os.path.join(get_ipython_dir(), 'nbextensions')] + SYSTEM_NBEXTENSIONS_DIRS
604 604
605 605 websocket_url = Unicode("", config=True,
606 606 help="""The base URL for websockets,
607 607 if it differs from the HTTP server (hint: it almost certainly doesn't).
608 608
609 609 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
610 610 """
611 611 )
612 612 mathjax_url = Unicode("", config=True,
613 613 help="""The url for MathJax.js."""
614 614 )
615 615 def _mathjax_url_default(self):
616 616 if not self.enable_mathjax:
617 617 return u''
618 618 static_url_prefix = self.tornado_settings.get("static_url_prefix",
619 619 url_path_join(self.base_url, "static")
620 620 )
621 621
622 622 # try local mathjax, either in nbextensions/mathjax or static/mathjax
623 623 for (url_prefix, search_path) in [
624 624 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
625 625 (static_url_prefix, self.static_file_path),
626 626 ]:
627 627 self.log.debug("searching for local mathjax in %s", search_path)
628 628 try:
629 629 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
630 630 except IOError:
631 631 continue
632 632 else:
633 633 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
634 634 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
635 635 return url
636 636
637 637 # no local mathjax, serve from CDN
638 638 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
639 639 self.log.info("Using MathJax from CDN: %s", url)
640 640 return url
641 641
642 642 def _mathjax_url_changed(self, name, old, new):
643 643 if new and not self.enable_mathjax:
644 644 # enable_mathjax=False overrides mathjax_url
645 645 self.mathjax_url = u''
646 646 else:
647 647 self.log.info("Using MathJax: %s", new)
648 648
649 649 contents_manager_class = Type(
650 650 default_value=FileContentsManager,
651 651 klass=ContentsManager,
652 652 config=True,
653 653 help='The notebook manager class to use.'
654 654 )
655 655 kernel_manager_class = Type(
656 656 default_value=MappingKernelManager,
657 657 config=True,
658 658 help='The kernel manager class to use.'
659 659 )
660 660 session_manager_class = Type(
661 661 default_value=SessionManager,
662 662 config=True,
663 663 help='The session manager class to use.'
664 664 )
665 665 cluster_manager_class = Type(
666 666 default_value=ClusterManager,
667 667 config=True,
668 668 help='The cluster manager class to use.'
669 669 )
670 670
671 671 config_manager_class = Type(
672 672 default_value=ConfigManager,
673 673 config = True,
674 674 help='The config manager class to use'
675 675 )
676 676
677 677 kernel_spec_manager = Instance(KernelSpecManager)
678 678
679 679 kernel_spec_manager_class = Type(
680 680 default_value=KernelSpecManager,
681 681 config=True,
682 682 help="""
683 683 The kernel spec manager class to use. Should be a subclass
684 684 of `IPython.kernel.kernelspec.KernelSpecManager`.
685 685
686 686 The Api of KernelSpecManager is provisional and might change
687 687 without warning between this version of IPython and the next stable one.
688 688 """
689 689 )
690 690
691 691 login_handler_class = Type(
692 692 default_value=LoginHandler,
693 693 klass=web.RequestHandler,
694 694 config=True,
695 695 help='The login handler class to use.',
696 696 )
697 697
698 698 logout_handler_class = Type(
699 699 default_value=LogoutHandler,
700 700 klass=web.RequestHandler,
701 701 config=True,
702 702 help='The logout handler class to use.',
703 703 )
704 704
705 705 trust_xheaders = Bool(False, config=True,
706 706 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
707 707 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
708 708 )
709 709
710 710 info_file = Unicode()
711 711
712 712 def _info_file_default(self):
713 713 info_file = "nbserver-%s.json"%os.getpid()
714 714 return os.path.join(self.profile_dir.security_dir, info_file)
715 715
716 716 pylab = Unicode('disabled', config=True,
717 717 help="""
718 718 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
719 719 """
720 720 )
721 721 def _pylab_changed(self, name, old, new):
722 722 """when --pylab is specified, display a warning and exit"""
723 723 if new != 'warn':
724 724 backend = ' %s' % new
725 725 else:
726 726 backend = ''
727 727 self.log.error("Support for specifying --pylab on the command line has been removed.")
728 728 self.log.error(
729 729 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
730 730 )
731 731 self.exit(1)
732 732
733 733 notebook_dir = Unicode(config=True,
734 734 help="The directory to use for notebooks and kernels."
735 735 )
736 736
737 737 def _notebook_dir_default(self):
738 738 if self.file_to_run:
739 739 return os.path.dirname(os.path.abspath(self.file_to_run))
740 740 else:
741 741 return py3compat.getcwd()
742 742
743 743 def _notebook_dir_changed(self, name, old, new):
744 744 """Do a bit of validation of the notebook dir."""
745 745 if not os.path.isabs(new):
746 746 # If we receive a non-absolute path, make it absolute.
747 747 self.notebook_dir = os.path.abspath(new)
748 748 return
749 749 if not os.path.isdir(new):
750 750 raise TraitError("No such notebook dir: %r" % new)
751 751
752 752 # setting App.notebook_dir implies setting notebook and kernel dirs as well
753 753 self.config.FileContentsManager.root_dir = new
754 754 self.config.MappingKernelManager.root_dir = new
755 755
756 756 server_extensions = List(Unicode(), config=True,
757 757 help=("Python modules to load as notebook server extensions. "
758 758 "This is an experimental API, and may change in future releases.")
759 759 )
760 760
761 761 reraise_server_extension_failures = Bool(
762 762 False,
763 763 config=True,
764 764 help="Reraise exceptions encountered loading server extensions?",
765 765 )
766 766
767 767 def parse_command_line(self, argv=None):
768 768 super(NotebookApp, self).parse_command_line(argv)
769 769
770 770 if self.extra_args:
771 771 arg0 = self.extra_args[0]
772 772 f = os.path.abspath(arg0)
773 773 self.argv.remove(arg0)
774 774 if not os.path.exists(f):
775 775 self.log.critical("No such file or directory: %s", f)
776 776 self.exit(1)
777 777
778 778 # Use config here, to ensure that it takes higher priority than
779 779 # anything that comes from the profile.
780 780 c = Config()
781 781 if os.path.isdir(f):
782 782 c.NotebookApp.notebook_dir = f
783 783 elif os.path.isfile(f):
784 784 c.NotebookApp.file_to_run = f
785 785 self.update_config(c)
786 786
787 787 def init_kernel_argv(self):
788 788 """add the profile-dir to arguments to be passed to IPython kernels"""
789 789 # FIXME: remove special treatment of IPython kernels
790 790 # Kernel should get *absolute* path to profile directory
791 791 self.ipython_kernel_argv = ["--profile-dir", self.profile_dir.location]
792 792
793 793 def init_configurables(self):
794 794 self.kernel_spec_manager = self.kernel_spec_manager_class(
795 795 parent=self,
796 796 ipython_dir=self.ipython_dir,
797 797 )
798 798 self.kernel_manager = self.kernel_manager_class(
799 799 parent=self,
800 800 log=self.log,
801 801 ipython_kernel_argv=self.ipython_kernel_argv,
802 802 connection_dir=self.profile_dir.security_dir,
803 803 )
804 804 self.contents_manager = self.contents_manager_class(
805 805 parent=self,
806 806 log=self.log,
807 807 )
808 808 self.session_manager = self.session_manager_class(
809 809 parent=self,
810 810 log=self.log,
811 811 kernel_manager=self.kernel_manager,
812 812 contents_manager=self.contents_manager,
813 813 )
814 814 self.cluster_manager = self.cluster_manager_class(
815 815 parent=self,
816 816 log=self.log,
817 817 )
818 818
819 819 self.config_manager = self.config_manager_class(
820 820 parent=self,
821 821 log=self.log,
822 822 profile_dir=self.profile_dir.location,
823 823 )
824 824
825 825 def init_logging(self):
826 826 # This prevents double log messages because tornado use a root logger that
827 827 # self.log is a child of. The logging module dipatches log messages to a log
828 828 # and all of its ancenstors until propagate is set to False.
829 829 self.log.propagate = False
830 830
831 831 for log in app_log, access_log, gen_log:
832 832 # consistent log output name (NotebookApp instead of tornado.access, etc.)
833 833 log.name = self.log.name
834 834 # hook up tornado 3's loggers to our app handlers
835 835 logger = logging.getLogger('tornado')
836 836 logger.propagate = True
837 837 logger.parent = self.log
838 838 logger.setLevel(self.log.level)
839 839
840 840 def init_webapp(self):
841 841 """initialize tornado webapp and httpserver"""
842 842 self.tornado_settings['allow_origin'] = self.allow_origin
843 843 if self.allow_origin_pat:
844 844 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
845 845 self.tornado_settings['allow_credentials'] = self.allow_credentials
846 846 # ensure default_url starts with base_url
847 847 if not self.default_url.startswith(self.base_url):
848 848 self.default_url = url_path_join(self.base_url, self.default_url)
849 849
850 850 self.web_app = NotebookWebApplication(
851 851 self, self.kernel_manager, self.contents_manager,
852 852 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
853 853 self.config_manager,
854 854 self.log, self.base_url, self.default_url, self.tornado_settings,
855 855 self.jinja_environment_options
856 856 )
857 857 ssl_options = self.ssl_options
858 858 if self.certfile:
859 859 ssl_options['certfile'] = self.certfile
860 860 if self.keyfile:
861 861 ssl_options['keyfile'] = self.keyfile
862 862 if not ssl_options:
863 863 # None indicates no SSL config
864 864 ssl_options = None
865 865 else:
866 866 # Disable SSLv3, since its use is discouraged.
867 867 ssl_options['ssl_version']=ssl.PROTOCOL_TLSv1
868 868 self.login_handler_class.validate_security(self, ssl_options=ssl_options)
869 869 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
870 870 xheaders=self.trust_xheaders)
871 871
872 872 success = None
873 873 for port in random_ports(self.port, self.port_retries+1):
874 874 try:
875 875 self.http_server.listen(port, self.ip)
876 876 except socket.error as e:
877 877 if e.errno == errno.EADDRINUSE:
878 878 self.log.info('The port %i is already in use, trying another random port.' % port)
879 879 continue
880 880 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
881 881 self.log.warn("Permission to listen on port %i denied" % port)
882 882 continue
883 883 else:
884 884 raise
885 885 else:
886 886 self.port = port
887 887 success = True
888 888 break
889 889 if not success:
890 890 self.log.critical('ERROR: the notebook server could not be started because '
891 891 'no available port could be found.')
892 892 self.exit(1)
893 893
894 894 @property
895 895 def display_url(self):
896 896 ip = self.ip if self.ip else '[all ip addresses on your system]'
897 897 return self._url(ip)
898 898
899 899 @property
900 900 def connection_url(self):
901 901 ip = self.ip if self.ip else 'localhost'
902 902 return self._url(ip)
903 903
904 904 def _url(self, ip):
905 905 proto = 'https' if self.certfile else 'http'
906 906 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
907 907
908 908 def init_terminals(self):
909 909 try:
910 910 from .terminal import initialize
911 911 initialize(self.web_app)
912 912 self.web_app.settings['terminals_available'] = True
913 913 except ImportError as e:
914 914 log = self.log.debug if sys.platform == 'win32' else self.log.warn
915 915 log("Terminals not available (error was %s)", e)
916 916
917 917 def init_signal(self):
918 918 if not sys.platform.startswith('win'):
919 919 signal.signal(signal.SIGINT, self._handle_sigint)
920 920 signal.signal(signal.SIGTERM, self._signal_stop)
921 921 if hasattr(signal, 'SIGUSR1'):
922 922 # Windows doesn't support SIGUSR1
923 923 signal.signal(signal.SIGUSR1, self._signal_info)
924 924 if hasattr(signal, 'SIGINFO'):
925 925 # only on BSD-based systems
926 926 signal.signal(signal.SIGINFO, self._signal_info)
927 927
928 928 def _handle_sigint(self, sig, frame):
929 929 """SIGINT handler spawns confirmation dialog"""
930 930 # register more forceful signal handler for ^C^C case
931 931 signal.signal(signal.SIGINT, self._signal_stop)
932 932 # request confirmation dialog in bg thread, to avoid
933 933 # blocking the App
934 934 thread = threading.Thread(target=self._confirm_exit)
935 935 thread.daemon = True
936 936 thread.start()
937 937
938 938 def _restore_sigint_handler(self):
939 939 """callback for restoring original SIGINT handler"""
940 940 signal.signal(signal.SIGINT, self._handle_sigint)
941 941
942 942 def _confirm_exit(self):
943 943 """confirm shutdown on ^C
944 944
945 945 A second ^C, or answering 'y' within 5s will cause shutdown,
946 946 otherwise original SIGINT handler will be restored.
947 947
948 948 This doesn't work on Windows.
949 949 """
950 950 info = self.log.info
951 951 info('interrupted')
952 952 print(self.notebook_info())
953 953 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
954 954 sys.stdout.flush()
955 955 r,w,x = select.select([sys.stdin], [], [], 5)
956 956 if r:
957 957 line = sys.stdin.readline()
958 958 if line.lower().startswith('y') and 'n' not in line.lower():
959 959 self.log.critical("Shutdown confirmed")
960 960 ioloop.IOLoop.current().stop()
961 961 return
962 962 else:
963 963 print("No answer for 5s:", end=' ')
964 964 print("resuming operation...")
965 965 # no answer, or answer is no:
966 966 # set it back to original SIGINT handler
967 967 # use IOLoop.add_callback because signal.signal must be called
968 968 # from main thread
969 969 ioloop.IOLoop.current().add_callback(self._restore_sigint_handler)
970 970
971 971 def _signal_stop(self, sig, frame):
972 972 self.log.critical("received signal %s, stopping", sig)
973 973 ioloop.IOLoop.current().stop()
974 974
975 975 def _signal_info(self, sig, frame):
976 976 print(self.notebook_info())
977 977
978 978 def init_components(self):
979 979 """Check the components submodule, and warn if it's unclean"""
980 980 status = submodule.check_submodule_status()
981 981 if status == 'missing':
982 982 self.log.warn("components submodule missing, running `git submodule update`")
983 983 submodule.update_submodules(submodule.ipython_parent())
984 984 elif status == 'unclean':
985 985 self.log.warn("components submodule unclean, you may see 404s on static/components")
986 986 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
987 987
988 988 def init_server_extensions(self):
989 989 """Load any extensions specified by config.
990 990
991 991 Import the module, then call the load_jupyter_server_extension function,
992 992 if one exists.
993 993
994 994 The extension API is experimental, and may change in future releases.
995 995 """
996 996 for modulename in self.server_extensions:
997 997 try:
998 998 mod = importlib.import_module(modulename)
999 999 func = getattr(mod, 'load_jupyter_server_extension', None)
1000 1000 if func is not None:
1001 1001 func(self)
1002 1002 except Exception:
1003 1003 if self.reraise_server_extension_failures:
1004 1004 raise
1005 1005 self.log.warn("Error loading server extension %s", modulename,
1006 1006 exc_info=True)
1007 1007
1008 1008 @catch_config_error
1009 1009 def initialize(self, argv=None):
1010 1010 super(NotebookApp, self).initialize(argv)
1011 1011 self.init_logging()
1012 1012 self.init_kernel_argv()
1013 1013 self.init_configurables()
1014 1014 self.init_components()
1015 1015 self.init_webapp()
1016 1016 self.init_terminals()
1017 1017 self.init_signal()
1018 1018 self.init_server_extensions()
1019 1019
1020 1020 def cleanup_kernels(self):
1021 1021 """Shutdown all kernels.
1022 1022
1023 1023 The kernels will shutdown themselves when this process no longer exists,
1024 1024 but explicit shutdown allows the KernelManagers to cleanup the connection files.
1025 1025 """
1026 1026 self.log.info('Shutting down kernels')
1027 1027 self.kernel_manager.shutdown_all()
1028 1028
1029 1029 def notebook_info(self):
1030 1030 "Return the current working directory and the server url information"
1031 1031 info = self.contents_manager.info_string() + "\n"
1032 1032 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
1033 1033 return info + "The IPython Notebook is running at: %s" % self.display_url
1034 1034
1035 1035 def server_info(self):
1036 1036 """Return a JSONable dict of information about this server."""
1037 1037 return {'url': self.connection_url,
1038 1038 'hostname': self.ip if self.ip else 'localhost',
1039 1039 'port': self.port,
1040 1040 'secure': bool(self.certfile),
1041 1041 'base_url': self.base_url,
1042 1042 'notebook_dir': os.path.abspath(self.notebook_dir),
1043 1043 'pid': os.getpid()
1044 1044 }
1045 1045
1046 1046 def write_server_info_file(self):
1047 1047 """Write the result of server_info() to the JSON file info_file."""
1048 1048 with open(self.info_file, 'w') as f:
1049 1049 json.dump(self.server_info(), f, indent=2)
1050 1050
1051 1051 def remove_server_info_file(self):
1052 1052 """Remove the nbserver-<pid>.json file created for this server.
1053 1053
1054 1054 Ignores the error raised when the file has already been removed.
1055 1055 """
1056 1056 try:
1057 1057 os.unlink(self.info_file)
1058 1058 except OSError as e:
1059 1059 if e.errno != errno.ENOENT:
1060 1060 raise
1061 1061
1062 1062 def start(self):
1063 1063 """ Start the IPython Notebook server app, after initialization
1064 1064
1065 1065 This method takes no arguments so all configuration and initialization
1066 1066 must be done prior to calling this method."""
1067 1067 if self.subapp is not None:
1068 1068 return self.subapp.start()
1069 1069
1070 1070 info = self.log.info
1071 1071 for line in self.notebook_info().split("\n"):
1072 1072 info(line)
1073 1073 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
1074 1074
1075 1075 self.write_server_info_file()
1076 1076
1077 1077 if self.open_browser or self.file_to_run:
1078 1078 try:
1079 1079 browser = webbrowser.get(self.browser or None)
1080 1080 except webbrowser.Error as e:
1081 1081 self.log.warn('No web browser found: %s.' % e)
1082 1082 browser = None
1083 1083
1084 1084 if self.file_to_run:
1085 1085 if not os.path.exists(self.file_to_run):
1086 1086 self.log.critical("%s does not exist" % self.file_to_run)
1087 1087 self.exit(1)
1088 1088
1089 1089 relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
1090 1090 uri = url_path_join('notebooks', *relpath.split(os.sep))
1091 1091 else:
1092 1092 uri = 'tree'
1093 1093 if browser:
1094 1094 b = lambda : browser.open(url_path_join(self.connection_url, uri),
1095 1095 new=2)
1096 1096 threading.Thread(target=b).start()
1097 1097
1098 1098 self.io_loop = ioloop.IOLoop.current()
1099 1099 if sys.platform.startswith('win'):
1100 1100 # add no-op to wake every 5s
1101 1101 # to handle signals that may be ignored by the inner loop
1102 1102 pc = ioloop.PeriodicCallback(lambda : None, 5000)
1103 1103 pc.start()
1104 1104 try:
1105 1105 self.io_loop.start()
1106 1106 except KeyboardInterrupt:
1107 1107 info("Interrupted...")
1108 1108 finally:
1109 1109 self.cleanup_kernels()
1110 1110 self.remove_server_info_file()
1111 1111
1112 1112 def stop(self):
1113 1113 def _stop():
1114 1114 self.http_server.stop()
1115 1115 self.io_loop.stop()
1116 1116 self.io_loop.add_callback(_stop)
1117 1117
1118 1118
1119 1119 def list_running_servers(profile='default'):
1120 1120 """Iterate over the server info files of running notebook servers.
1121 1121
1122 1122 Given a profile name, find nbserver-* files in the security directory of
1123 1123 that profile, and yield dicts of their information, each one pertaining to
1124 1124 a currently running notebook server instance.
1125 1125 """
1126 1126 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
1127 1127 for file in os.listdir(pd.security_dir):
1128 1128 if file.startswith('nbserver-'):
1129 1129 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
1130 1130 info = json.load(f)
1131 1131
1132 1132 # Simple check whether that process is really still running
1133 1133 # Also remove leftover files from IPython 2.x without a pid field
1134 1134 if ('pid' in info) and check_pid(info['pid']):
1135 1135 yield info
1136 1136 else:
1137 1137 # If the process has died, try to delete its info file
1138 1138 try:
1139 1139 os.unlink(file)
1140 1140 except OSError:
1141 1141 pass # TODO: This should warn or log or something
1142 1142 #-----------------------------------------------------------------------------
1143 1143 # Main entry point
1144 1144 #-----------------------------------------------------------------------------
1145 1145
1146 1146 launch_new_instance = NotebookApp.launch_instance
1147 1147
General Comments 0
You need to be logged in to leave comments. Login now