##// END OF EJS Templates
Backport PR #6134: remove rackcdn https workaround for mathjax cdn...
MinRK -
Show More
@@ -1,883 +1,877 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 from __future__ import print_function
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2013 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 # stdlib
21 21 import errno
22 22 import io
23 23 import json
24 24 import logging
25 25 import os
26 26 import random
27 27 import re
28 28 import select
29 29 import signal
30 30 import socket
31 31 import sys
32 32 import threading
33 33 import time
34 34 import webbrowser
35 35
36 36
37 37 # Third party
38 38 # check for pyzmq 2.1.11
39 39 from IPython.utils.zmqrelated import check_for_zmq
40 40 check_for_zmq('2.1.11', 'IPython.html')
41 41
42 42 from jinja2 import Environment, FileSystemLoader
43 43
44 44 # Install the pyzmq ioloop. This has to be done before anything else from
45 45 # tornado is imported.
46 46 from zmq.eventloop import ioloop
47 47 ioloop.install()
48 48
49 49 # check for tornado 3.1.0
50 50 msg = "The IPython Notebook requires tornado >= 3.1.0"
51 51 try:
52 52 import tornado
53 53 except ImportError:
54 54 raise ImportError(msg)
55 55 try:
56 56 version_info = tornado.version_info
57 57 except AttributeError:
58 58 raise ImportError(msg + ", but you have < 1.1.0")
59 59 if version_info < (3,1,0):
60 60 raise ImportError(msg + ", but you have %s" % tornado.version)
61 61
62 62 from tornado import httpserver
63 63 from tornado import web
64 64
65 65 # Our own libraries
66 66 from IPython.html import DEFAULT_STATIC_FILES_PATH
67 67 from .base.handlers import Template404
68 68 from .log import log_request
69 69 from .services.kernels.kernelmanager import MappingKernelManager
70 70 from .services.notebooks.nbmanager import NotebookManager
71 71 from .services.notebooks.filenbmanager import FileNotebookManager
72 72 from .services.clusters.clustermanager import ClusterManager
73 73 from .services.sessions.sessionmanager import SessionManager
74 74
75 75 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
76 76
77 77 from IPython.config import Config
78 78 from IPython.config.application import catch_config_error, boolean_flag
79 79 from IPython.core.application import BaseIPythonApplication
80 80 from IPython.core.profiledir import ProfileDir
81 81 from IPython.consoleapp import IPythonConsoleApp
82 82 from IPython.kernel import swallow_argv
83 83 from IPython.kernel.zmq.session import default_secure
84 84 from IPython.kernel.zmq.kernelapp import (
85 85 kernel_flags,
86 86 kernel_aliases,
87 87 )
88 88 from IPython.nbformat.sign import NotebookNotary
89 89 from IPython.utils.importstring import import_item
90 90 from IPython.utils import submodule
91 91 from IPython.utils.traitlets import (
92 92 Dict, Unicode, Integer, List, Bool, Bytes,
93 93 DottedObjectName, TraitError,
94 94 )
95 95 from IPython.utils import py3compat
96 96 from IPython.utils.path import filefind, get_ipython_dir
97 97
98 98 from .utils import url_path_join
99 99
100 100 #-----------------------------------------------------------------------------
101 101 # Module globals
102 102 #-----------------------------------------------------------------------------
103 103
104 104 _examples = """
105 105 ipython notebook # start the notebook
106 106 ipython notebook --profile=sympy # use the sympy profile
107 107 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
108 108 """
109 109
110 110 #-----------------------------------------------------------------------------
111 111 # Helper functions
112 112 #-----------------------------------------------------------------------------
113 113
114 114 def random_ports(port, n):
115 115 """Generate a list of n random ports near the given port.
116 116
117 117 The first 5 ports will be sequential, and the remaining n-5 will be
118 118 randomly selected in the range [port-2*n, port+2*n].
119 119 """
120 120 for i in range(min(5, n)):
121 121 yield port + i
122 122 for i in range(n-5):
123 123 yield max(1, port + random.randint(-2*n, 2*n))
124 124
125 125 def load_handlers(name):
126 126 """Load the (URL pattern, handler) tuples for each component."""
127 127 name = 'IPython.html.' + name
128 128 mod = __import__(name, fromlist=['default_handlers'])
129 129 return mod.default_handlers
130 130
131 131 #-----------------------------------------------------------------------------
132 132 # The Tornado web application
133 133 #-----------------------------------------------------------------------------
134 134
135 135 class NotebookWebApplication(web.Application):
136 136
137 137 def __init__(self, ipython_app, kernel_manager, notebook_manager,
138 138 cluster_manager, session_manager, log, base_url,
139 139 settings_overrides, jinja_env_options):
140 140
141 141 settings = self.init_settings(
142 142 ipython_app, kernel_manager, notebook_manager, cluster_manager,
143 143 session_manager, log, base_url, settings_overrides, jinja_env_options)
144 144 handlers = self.init_handlers(settings)
145 145
146 146 super(NotebookWebApplication, self).__init__(handlers, **settings)
147 147
148 148 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
149 149 cluster_manager, session_manager, log, base_url,
150 150 settings_overrides, jinja_env_options=None):
151 151 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
152 152 # base_url will always be unicode, which will in turn
153 153 # make the patterns unicode, and ultimately result in unicode
154 154 # keys in kwargs to handler._execute(**kwargs) in tornado.
155 155 # This enforces that base_url be ascii in that situation.
156 156 #
157 157 # Note that the URLs these patterns check against are escaped,
158 158 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
159 159 base_url = py3compat.unicode_to_str(base_url, 'ascii')
160 160 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
161 161 jenv_opt = jinja_env_options if jinja_env_options else {}
162 162 env = Environment(loader=FileSystemLoader(template_path),**jenv_opt )
163 163 settings = dict(
164 164 # basics
165 165 log_function=log_request,
166 166 base_url=base_url,
167 167 template_path=template_path,
168 168 static_path=ipython_app.static_file_path,
169 169 static_handler_class = FileFindHandler,
170 170 static_url_prefix = url_path_join(base_url,'/static/'),
171 171
172 172 # authentication
173 173 cookie_secret=ipython_app.cookie_secret,
174 174 login_url=url_path_join(base_url,'/login'),
175 175 password=ipython_app.password,
176 176
177 177 # managers
178 178 kernel_manager=kernel_manager,
179 179 notebook_manager=notebook_manager,
180 180 cluster_manager=cluster_manager,
181 181 session_manager=session_manager,
182 182
183 183 # IPython stuff
184 184 nbextensions_path = ipython_app.nbextensions_path,
185 185 mathjax_url=ipython_app.mathjax_url,
186 186 config=ipython_app.config,
187 187 jinja2_env=env,
188 188 )
189 189
190 190 # allow custom overrides for the tornado web app.
191 191 settings.update(settings_overrides)
192 192 return settings
193 193
194 194 def init_handlers(self, settings):
195 195 # Load the (URL pattern, handler) tuples for each component.
196 196 handlers = []
197 197 handlers.extend(load_handlers('base.handlers'))
198 198 handlers.extend(load_handlers('tree.handlers'))
199 199 handlers.extend(load_handlers('auth.login'))
200 200 handlers.extend(load_handlers('auth.logout'))
201 201 handlers.extend(load_handlers('notebook.handlers'))
202 202 handlers.extend(load_handlers('nbconvert.handlers'))
203 203 handlers.extend(load_handlers('services.kernels.handlers'))
204 204 handlers.extend(load_handlers('services.notebooks.handlers'))
205 205 handlers.extend(load_handlers('services.clusters.handlers'))
206 206 handlers.extend(load_handlers('services.sessions.handlers'))
207 207 handlers.extend(load_handlers('services.nbconvert.handlers'))
208 208 # FIXME: /files/ should be handled by the Contents service when it exists
209 209 nbm = settings['notebook_manager']
210 210 if hasattr(nbm, 'notebook_dir'):
211 211 handlers.extend([
212 212 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : nbm.notebook_dir}),
213 213 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
214 214 ])
215 215 # prepend base_url onto the patterns that we match
216 216 new_handlers = []
217 217 for handler in handlers:
218 218 pattern = url_path_join(settings['base_url'], handler[0])
219 219 new_handler = tuple([pattern] + list(handler[1:]))
220 220 new_handlers.append(new_handler)
221 221 # add 404 on the end, which will catch everything that falls through
222 222 new_handlers.append((r'(.*)', Template404))
223 223 return new_handlers
224 224
225 225
226 226 class NbserverListApp(BaseIPythonApplication):
227 227
228 228 description="List currently running notebook servers in this profile."
229 229
230 230 flags = dict(
231 231 json=({'NbserverListApp': {'json': True}},
232 232 "Produce machine-readable JSON output."),
233 233 )
234 234
235 235 json = Bool(False, config=True,
236 236 help="If True, each line of output will be a JSON object with the "
237 237 "details from the server info file.")
238 238
239 239 def start(self):
240 240 if not self.json:
241 241 print("Currently running servers:")
242 242 for serverinfo in list_running_servers(self.profile):
243 243 if self.json:
244 244 print(json.dumps(serverinfo))
245 245 else:
246 246 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
247 247
248 248 #-----------------------------------------------------------------------------
249 249 # Aliases and Flags
250 250 #-----------------------------------------------------------------------------
251 251
252 252 flags = dict(kernel_flags)
253 253 flags['no-browser']=(
254 254 {'NotebookApp' : {'open_browser' : False}},
255 255 "Don't open the notebook in a browser after startup."
256 256 )
257 257 flags['no-mathjax']=(
258 258 {'NotebookApp' : {'enable_mathjax' : False}},
259 259 """Disable MathJax
260 260
261 261 MathJax is the javascript library IPython uses to render math/LaTeX. It is
262 262 very large, so you may want to disable it if you have a slow internet
263 263 connection, or for offline use of the notebook.
264 264
265 265 When disabled, equations etc. will appear as their untransformed TeX source.
266 266 """
267 267 )
268 268
269 269 # Add notebook manager flags
270 270 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
271 271 'Auto-save a .py script everytime the .ipynb notebook is saved',
272 272 'Do not auto-save .py scripts for every notebook'))
273 273
274 274 # the flags that are specific to the frontend
275 275 # these must be scrubbed before being passed to the kernel,
276 276 # or it will raise an error on unrecognized flags
277 277 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
278 278
279 279 aliases = dict(kernel_aliases)
280 280
281 281 aliases.update({
282 282 'ip': 'NotebookApp.ip',
283 283 'port': 'NotebookApp.port',
284 284 'port-retries': 'NotebookApp.port_retries',
285 285 'transport': 'KernelManager.transport',
286 286 'keyfile': 'NotebookApp.keyfile',
287 287 'certfile': 'NotebookApp.certfile',
288 288 'notebook-dir': 'NotebookApp.notebook_dir',
289 289 'browser': 'NotebookApp.browser',
290 290 })
291 291
292 292 # remove ipkernel flags that are singletons, and don't make sense in
293 293 # multi-kernel evironment:
294 294 aliases.pop('f', None)
295 295
296 296 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
297 297 u'notebook-dir', u'profile', u'profile-dir', 'browser']
298 298
299 299 #-----------------------------------------------------------------------------
300 300 # NotebookApp
301 301 #-----------------------------------------------------------------------------
302 302
303 303 class NotebookApp(BaseIPythonApplication):
304 304
305 305 name = 'ipython-notebook'
306 306
307 307 description = """
308 308 The IPython HTML Notebook.
309 309
310 310 This launches a Tornado based HTML Notebook Server that serves up an
311 311 HTML5/Javascript Notebook client.
312 312 """
313 313 examples = _examples
314 314
315 315 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
316 316 FileNotebookManager, NotebookNotary]
317 317 flags = Dict(flags)
318 318 aliases = Dict(aliases)
319 319
320 320 subcommands = dict(
321 321 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
322 322 )
323 323
324 324 kernel_argv = List(Unicode)
325 325
326 326 def _log_level_default(self):
327 327 return logging.INFO
328 328
329 329 def _log_format_default(self):
330 330 """override default log format to include time"""
331 331 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
332 332
333 333 # create requested profiles by default, if they don't exist:
334 334 auto_create = Bool(True)
335 335
336 336 # file to be opened in the notebook server
337 337 file_to_run = Unicode('', config=True)
338 338 def _file_to_run_changed(self, name, old, new):
339 339 path, base = os.path.split(new)
340 340 if path:
341 341 self.file_to_run = base
342 342 self.notebook_dir = path
343 343
344 344 # Network related information
345 345
346 346 allow_origin = Unicode('', config=True,
347 347 help="""Set the Access-Control-Allow-Origin header
348 348
349 349 Use '*' to allow any origin to access your server.
350 350
351 351 Takes precedence over allow_origin_pat.
352 352 """
353 353 )
354 354
355 355 allow_origin_pat = Unicode('', config=True,
356 356 help="""Use a regular expression for the Access-Control-Allow-Origin header
357 357
358 358 Requests from an origin matching the expression will get replies with:
359 359
360 360 Access-Control-Allow-Origin: origin
361 361
362 362 where `origin` is the origin of the request.
363 363
364 364 Ignored if allow_origin is set.
365 365 """
366 366 )
367 367
368 368 allow_credentials = Bool(False, config=True,
369 369 help="Set the Access-Control-Allow-Credentials: true header"
370 370 )
371 371
372 372 ip = Unicode('localhost', config=True,
373 373 help="The IP address the notebook server will listen on."
374 374 )
375 375
376 376 def _ip_changed(self, name, old, new):
377 377 if new == u'*': self.ip = u''
378 378
379 379 port = Integer(8888, config=True,
380 380 help="The port the notebook server will listen on."
381 381 )
382 382 port_retries = Integer(50, config=True,
383 383 help="The number of additional ports to try if the specified port is not available."
384 384 )
385 385
386 386 certfile = Unicode(u'', config=True,
387 387 help="""The full path to an SSL/TLS certificate file."""
388 388 )
389 389
390 390 keyfile = Unicode(u'', config=True,
391 391 help="""The full path to a private key file for usage with SSL/TLS."""
392 392 )
393 393
394 394 cookie_secret = Bytes(b'', config=True,
395 395 help="""The random bytes used to secure cookies.
396 396 By default this is a new random number every time you start the Notebook.
397 397 Set it to a value in a config file to enable logins to persist across server sessions.
398 398
399 399 Note: Cookie secrets should be kept private, do not share config files with
400 400 cookie_secret stored in plaintext (you can read the value from a file).
401 401 """
402 402 )
403 403 def _cookie_secret_default(self):
404 404 return os.urandom(1024)
405 405
406 406 password = Unicode(u'', config=True,
407 407 help="""Hashed password to use for web authentication.
408 408
409 409 To generate, type in a python/IPython shell:
410 410
411 411 from IPython.lib import passwd; passwd()
412 412
413 413 The string should be of the form type:salt:hashed-password.
414 414 """
415 415 )
416 416
417 417 open_browser = Bool(True, config=True,
418 418 help="""Whether to open in a browser after starting.
419 419 The specific browser used is platform dependent and
420 420 determined by the python standard library `webbrowser`
421 421 module, unless it is overridden using the --browser
422 422 (NotebookApp.browser) configuration option.
423 423 """)
424 424
425 425 browser = Unicode(u'', config=True,
426 426 help="""Specify what command to use to invoke a web
427 427 browser when opening the notebook. If not specified, the
428 428 default browser will be determined by the `webbrowser`
429 429 standard library module, which allows setting of the
430 430 BROWSER environment variable to override it.
431 431 """)
432 432
433 433 webapp_settings = Dict(config=True,
434 434 help="Supply overrides for the tornado.web.Application that the "
435 435 "IPython notebook uses.")
436 436
437 437 jinja_environment_options = Dict(config=True,
438 438 help="Supply extra arguments that will be passed to Jinja environment.")
439 439
440 440
441 441 enable_mathjax = Bool(True, config=True,
442 442 help="""Whether to enable MathJax for typesetting math/TeX
443 443
444 444 MathJax is the javascript library IPython uses to render math/LaTeX. It is
445 445 very large, so you may want to disable it if you have a slow internet
446 446 connection, or for offline use of the notebook.
447 447
448 448 When disabled, equations etc. will appear as their untransformed TeX source.
449 449 """
450 450 )
451 451 def _enable_mathjax_changed(self, name, old, new):
452 452 """set mathjax url to empty if mathjax is disabled"""
453 453 if not new:
454 454 self.mathjax_url = u''
455 455
456 456 base_url = Unicode('/', config=True,
457 457 help='''The base URL for the notebook server.
458 458
459 459 Leading and trailing slashes can be omitted,
460 460 and will automatically be added.
461 461 ''')
462 462 def _base_url_changed(self, name, old, new):
463 463 if not new.startswith('/'):
464 464 self.base_url = '/'+new
465 465 elif not new.endswith('/'):
466 466 self.base_url = new+'/'
467 467
468 468 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
469 469 def _base_project_url_changed(self, name, old, new):
470 470 self.log.warn("base_project_url is deprecated, use base_url")
471 471 self.base_url = new
472 472
473 473 extra_static_paths = List(Unicode, config=True,
474 474 help="""Extra paths to search for serving static files.
475 475
476 476 This allows adding javascript/css to be available from the notebook server machine,
477 477 or overriding individual files in the IPython"""
478 478 )
479 479 def _extra_static_paths_default(self):
480 480 return [os.path.join(self.profile_dir.location, 'static')]
481 481
482 482 @property
483 483 def static_file_path(self):
484 484 """return extra paths + the default location"""
485 485 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
486 486
487 487 nbextensions_path = List(Unicode, config=True,
488 488 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
489 489 )
490 490 def _nbextensions_path_default(self):
491 491 return [os.path.join(get_ipython_dir(), 'nbextensions')]
492 492
493 493 mathjax_url = Unicode("", config=True,
494 494 help="""The url for MathJax.js."""
495 495 )
496 496 def _mathjax_url_default(self):
497 497 if not self.enable_mathjax:
498 498 return u''
499 499 static_url_prefix = self.webapp_settings.get("static_url_prefix",
500 500 url_path_join(self.base_url, "static")
501 501 )
502 502
503 503 # try local mathjax, either in nbextensions/mathjax or static/mathjax
504 504 for (url_prefix, search_path) in [
505 505 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
506 506 (static_url_prefix, self.static_file_path),
507 507 ]:
508 508 self.log.debug("searching for local mathjax in %s", search_path)
509 509 try:
510 510 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
511 511 except IOError:
512 512 continue
513 513 else:
514 514 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
515 515 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
516 516 return url
517 517
518 518 # no local mathjax, serve from CDN
519 if self.certfile:
520 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
521 host = u"https://c328740.ssl.cf1.rackcdn.com"
522 else:
523 host = u"http://cdn.mathjax.org"
524
525 url = host + u"/mathjax/latest/MathJax.js"
519 url = u"//cdn.mathjax.org/mathjax/latest/MathJax.js"
526 520 self.log.info("Using MathJax from CDN: %s", url)
527 521 return url
528 522
529 523 def _mathjax_url_changed(self, name, old, new):
530 524 if new and not self.enable_mathjax:
531 525 # enable_mathjax=False overrides mathjax_url
532 526 self.mathjax_url = u''
533 527 else:
534 528 self.log.info("Using MathJax: %s", new)
535 529
536 530 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
537 531 config=True,
538 532 help='The notebook manager class to use.')
539 533
540 534 trust_xheaders = Bool(False, config=True,
541 535 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
542 536 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
543 537 )
544 538
545 539 info_file = Unicode()
546 540
547 541 def _info_file_default(self):
548 542 info_file = "nbserver-%s.json"%os.getpid()
549 543 return os.path.join(self.profile_dir.security_dir, info_file)
550 544
551 545 notebook_dir = Unicode(py3compat.getcwd(), config=True,
552 546 help="The directory to use for notebooks and kernels."
553 547 )
554 548
555 549 def _notebook_dir_changed(self, name, old, new):
556 550 """Do a bit of validation of the notebook dir."""
557 551 if not os.path.isabs(new):
558 552 # If we receive a non-absolute path, make it absolute.
559 553 self.notebook_dir = os.path.abspath(new)
560 554 return
561 555 if not os.path.isdir(new):
562 556 raise TraitError("No such notebook dir: %r" % new)
563 557
564 558 # setting App.notebook_dir implies setting notebook and kernel dirs as well
565 559 self.config.FileNotebookManager.notebook_dir = new
566 560 self.config.MappingKernelManager.root_dir = new
567 561
568 562
569 563 def parse_command_line(self, argv=None):
570 564 super(NotebookApp, self).parse_command_line(argv)
571 565
572 566 if self.extra_args:
573 567 arg0 = self.extra_args[0]
574 568 f = os.path.abspath(arg0)
575 569 self.argv.remove(arg0)
576 570 if not os.path.exists(f):
577 571 self.log.critical("No such file or directory: %s", f)
578 572 self.exit(1)
579 573
580 574 # Use config here, to ensure that it takes higher priority than
581 575 # anything that comes from the profile.
582 576 c = Config()
583 577 if os.path.isdir(f):
584 578 c.NotebookApp.notebook_dir = f
585 579 elif os.path.isfile(f):
586 580 c.NotebookApp.file_to_run = f
587 581 self.update_config(c)
588 582
589 583 def init_kernel_argv(self):
590 584 """construct the kernel arguments"""
591 585 # Scrub frontend-specific flags
592 586 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
593 587 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
594 588 self.log.warn('\n '.join([
595 589 "Starting all kernels in pylab mode is not recommended,",
596 590 "and will be disabled in a future release.",
597 591 "Please use the %matplotlib magic to enable matplotlib instead.",
598 592 "pylab implies many imports, which can have confusing side effects",
599 593 "and harm the reproducibility of your notebooks.",
600 594 ]))
601 595 # Kernel should inherit default config file from frontend
602 596 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
603 597 # Kernel should get *absolute* path to profile directory
604 598 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
605 599
606 600 def init_configurables(self):
607 601 # force Session default to be secure
608 602 default_secure(self.config)
609 603 self.kernel_manager = MappingKernelManager(
610 604 parent=self, log=self.log, kernel_argv=self.kernel_argv,
611 605 connection_dir = self.profile_dir.security_dir,
612 606 )
613 607 kls = import_item(self.notebook_manager_class)
614 608 self.notebook_manager = kls(parent=self, log=self.log)
615 609 self.session_manager = SessionManager(parent=self, log=self.log)
616 610 self.cluster_manager = ClusterManager(parent=self, log=self.log)
617 611 self.cluster_manager.update_profiles()
618 612
619 613 def init_logging(self):
620 614 # This prevents double log messages because tornado use a root logger that
621 615 # self.log is a child of. The logging module dipatches log messages to a log
622 616 # and all of its ancenstors until propagate is set to False.
623 617 self.log.propagate = False
624 618
625 619 # hook up tornado 3's loggers to our app handlers
626 620 for name in ('access', 'application', 'general'):
627 621 logger = logging.getLogger('tornado.%s' % name)
628 622 logger.parent = self.log
629 623 logger.setLevel(self.log.level)
630 624
631 625 def init_webapp(self):
632 626 """initialize tornado webapp and httpserver"""
633 627 self.webapp_settings['allow_origin'] = self.allow_origin
634 628 if self.allow_origin_pat:
635 629 self.webapp_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
636 630 self.webapp_settings['allow_credentials'] = self.allow_credentials
637 631
638 632 self.web_app = NotebookWebApplication(
639 633 self, self.kernel_manager, self.notebook_manager,
640 634 self.cluster_manager, self.session_manager,
641 635 self.log, self.base_url, self.webapp_settings,
642 636 self.jinja_environment_options
643 637 )
644 638 if self.certfile:
645 639 ssl_options = dict(certfile=self.certfile)
646 640 if self.keyfile:
647 641 ssl_options['keyfile'] = self.keyfile
648 642 else:
649 643 ssl_options = None
650 644 self.web_app.password = self.password
651 645 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
652 646 xheaders=self.trust_xheaders)
653 647 if not self.ip:
654 648 warning = "WARNING: The notebook server is listening on all IP addresses"
655 649 if ssl_options is None:
656 650 self.log.critical(warning + " and not using encryption. This "
657 651 "is not recommended.")
658 652 if not self.password:
659 653 self.log.critical(warning + " and not using authentication. "
660 654 "This is highly insecure and not recommended.")
661 655 success = None
662 656 for port in random_ports(self.port, self.port_retries+1):
663 657 try:
664 658 self.http_server.listen(port, self.ip)
665 659 except socket.error as e:
666 660 if e.errno == errno.EADDRINUSE:
667 661 self.log.info('The port %i is already in use, trying another random port.' % port)
668 662 continue
669 663 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
670 664 self.log.warn("Permission to listen on port %i denied" % port)
671 665 continue
672 666 else:
673 667 raise
674 668 else:
675 669 self.port = port
676 670 success = True
677 671 break
678 672 if not success:
679 673 self.log.critical('ERROR: the notebook server could not be started because '
680 674 'no available port could be found.')
681 675 self.exit(1)
682 676
683 677 @property
684 678 def display_url(self):
685 679 ip = self.ip if self.ip else '[all ip addresses on your system]'
686 680 return self._url(ip)
687 681
688 682 @property
689 683 def connection_url(self):
690 684 ip = self.ip if self.ip else 'localhost'
691 685 return self._url(ip)
692 686
693 687 def _url(self, ip):
694 688 proto = 'https' if self.certfile else 'http'
695 689 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
696 690
697 691 def init_signal(self):
698 692 if not sys.platform.startswith('win'):
699 693 signal.signal(signal.SIGINT, self._handle_sigint)
700 694 signal.signal(signal.SIGTERM, self._signal_stop)
701 695 if hasattr(signal, 'SIGUSR1'):
702 696 # Windows doesn't support SIGUSR1
703 697 signal.signal(signal.SIGUSR1, self._signal_info)
704 698 if hasattr(signal, 'SIGINFO'):
705 699 # only on BSD-based systems
706 700 signal.signal(signal.SIGINFO, self._signal_info)
707 701
708 702 def _handle_sigint(self, sig, frame):
709 703 """SIGINT handler spawns confirmation dialog"""
710 704 # register more forceful signal handler for ^C^C case
711 705 signal.signal(signal.SIGINT, self._signal_stop)
712 706 # request confirmation dialog in bg thread, to avoid
713 707 # blocking the App
714 708 thread = threading.Thread(target=self._confirm_exit)
715 709 thread.daemon = True
716 710 thread.start()
717 711
718 712 def _restore_sigint_handler(self):
719 713 """callback for restoring original SIGINT handler"""
720 714 signal.signal(signal.SIGINT, self._handle_sigint)
721 715
722 716 def _confirm_exit(self):
723 717 """confirm shutdown on ^C
724 718
725 719 A second ^C, or answering 'y' within 5s will cause shutdown,
726 720 otherwise original SIGINT handler will be restored.
727 721
728 722 This doesn't work on Windows.
729 723 """
730 724 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
731 725 time.sleep(0.1)
732 726 info = self.log.info
733 727 info('interrupted')
734 728 print(self.notebook_info())
735 729 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
736 730 sys.stdout.flush()
737 731 r,w,x = select.select([sys.stdin], [], [], 5)
738 732 if r:
739 733 line = sys.stdin.readline()
740 734 if line.lower().startswith('y') and 'n' not in line.lower():
741 735 self.log.critical("Shutdown confirmed")
742 736 ioloop.IOLoop.instance().stop()
743 737 return
744 738 else:
745 739 print("No answer for 5s:", end=' ')
746 740 print("resuming operation...")
747 741 # no answer, or answer is no:
748 742 # set it back to original SIGINT handler
749 743 # use IOLoop.add_callback because signal.signal must be called
750 744 # from main thread
751 745 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
752 746
753 747 def _signal_stop(self, sig, frame):
754 748 self.log.critical("received signal %s, stopping", sig)
755 749 ioloop.IOLoop.instance().stop()
756 750
757 751 def _signal_info(self, sig, frame):
758 752 print(self.notebook_info())
759 753
760 754 def init_components(self):
761 755 """Check the components submodule, and warn if it's unclean"""
762 756 status = submodule.check_submodule_status()
763 757 if status == 'missing':
764 758 self.log.warn("components submodule missing, running `git submodule update`")
765 759 submodule.update_submodules(submodule.ipython_parent())
766 760 elif status == 'unclean':
767 761 self.log.warn("components submodule unclean, you may see 404s on static/components")
768 762 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
769 763
770 764 @catch_config_error
771 765 def initialize(self, argv=None):
772 766 super(NotebookApp, self).initialize(argv)
773 767 self.init_logging()
774 768 self.init_kernel_argv()
775 769 self.init_configurables()
776 770 self.init_components()
777 771 self.init_webapp()
778 772 self.init_signal()
779 773
780 774 def cleanup_kernels(self):
781 775 """Shutdown all kernels.
782 776
783 777 The kernels will shutdown themselves when this process no longer exists,
784 778 but explicit shutdown allows the KernelManagers to cleanup the connection files.
785 779 """
786 780 self.log.info('Shutting down kernels')
787 781 self.kernel_manager.shutdown_all()
788 782
789 783 def notebook_info(self):
790 784 "Return the current working directory and the server url information"
791 785 info = self.notebook_manager.info_string() + "\n"
792 786 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
793 787 return info + "The IPython Notebook is running at: %s" % self.display_url
794 788
795 789 def server_info(self):
796 790 """Return a JSONable dict of information about this server."""
797 791 return {'url': self.connection_url,
798 792 'hostname': self.ip if self.ip else 'localhost',
799 793 'port': self.port,
800 794 'secure': bool(self.certfile),
801 795 'base_url': self.base_url,
802 796 'notebook_dir': os.path.abspath(self.notebook_dir),
803 797 }
804 798
805 799 def write_server_info_file(self):
806 800 """Write the result of server_info() to the JSON file info_file."""
807 801 with open(self.info_file, 'w') as f:
808 802 json.dump(self.server_info(), f, indent=2)
809 803
810 804 def remove_server_info_file(self):
811 805 """Remove the nbserver-<pid>.json file created for this server.
812 806
813 807 Ignores the error raised when the file has already been removed.
814 808 """
815 809 try:
816 810 os.unlink(self.info_file)
817 811 except OSError as e:
818 812 if e.errno != errno.ENOENT:
819 813 raise
820 814
821 815 def start(self):
822 816 """ Start the IPython Notebook server app, after initialization
823 817
824 818 This method takes no arguments so all configuration and initialization
825 819 must be done prior to calling this method."""
826 820 if self.subapp is not None:
827 821 return self.subapp.start()
828 822
829 823 info = self.log.info
830 824 for line in self.notebook_info().split("\n"):
831 825 info(line)
832 826 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
833 827
834 828 self.write_server_info_file()
835 829
836 830 if self.open_browser or self.file_to_run:
837 831 try:
838 832 browser = webbrowser.get(self.browser or None)
839 833 except webbrowser.Error as e:
840 834 self.log.warn('No web browser found: %s.' % e)
841 835 browser = None
842 836
843 837 if self.file_to_run:
844 838 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
845 839 if not os.path.exists(fullpath):
846 840 self.log.critical("%s does not exist" % fullpath)
847 841 self.exit(1)
848 842
849 843 uri = url_path_join('notebooks', self.file_to_run)
850 844 else:
851 845 uri = 'tree'
852 846 if browser:
853 847 b = lambda : browser.open(url_path_join(self.connection_url, uri),
854 848 new=2)
855 849 threading.Thread(target=b).start()
856 850 try:
857 851 ioloop.IOLoop.instance().start()
858 852 except KeyboardInterrupt:
859 853 info("Interrupted...")
860 854 finally:
861 855 self.cleanup_kernels()
862 856 self.remove_server_info_file()
863 857
864 858
865 859 def list_running_servers(profile='default'):
866 860 """Iterate over the server info files of running notebook servers.
867 861
868 862 Given a profile name, find nbserver-* files in the security directory of
869 863 that profile, and yield dicts of their information, each one pertaining to
870 864 a currently running notebook server instance.
871 865 """
872 866 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
873 867 for file in os.listdir(pd.security_dir):
874 868 if file.startswith('nbserver-'):
875 869 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
876 870 yield json.load(f)
877 871
878 872 #-----------------------------------------------------------------------------
879 873 # Main entry point
880 874 #-----------------------------------------------------------------------------
881 875
882 876 launch_new_instance = NotebookApp.launch_instance
883 877
General Comments 0
You need to be logged in to leave comments. Login now