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