##// END OF EJS Templates
Backport PR #6105: only set allow_origin_pat if defined...
MinRK -
Show More
@@ -1,882 +1,883 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 519 if self.certfile:
520 520 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
521 521 host = u"https://c328740.ssl.cf1.rackcdn.com"
522 522 else:
523 523 host = u"http://cdn.mathjax.org"
524 524
525 525 url = host + u"/mathjax/latest/MathJax.js"
526 526 self.log.info("Using MathJax from CDN: %s", url)
527 527 return url
528 528
529 529 def _mathjax_url_changed(self, name, old, new):
530 530 if new and not self.enable_mathjax:
531 531 # enable_mathjax=False overrides mathjax_url
532 532 self.mathjax_url = u''
533 533 else:
534 534 self.log.info("Using MathJax: %s", new)
535 535
536 536 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
537 537 config=True,
538 538 help='The notebook manager class to use.')
539 539
540 540 trust_xheaders = Bool(False, config=True,
541 541 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
542 542 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
543 543 )
544 544
545 545 info_file = Unicode()
546 546
547 547 def _info_file_default(self):
548 548 info_file = "nbserver-%s.json"%os.getpid()
549 549 return os.path.join(self.profile_dir.security_dir, info_file)
550 550
551 551 notebook_dir = Unicode(py3compat.getcwd(), config=True,
552 552 help="The directory to use for notebooks and kernels."
553 553 )
554 554
555 555 def _notebook_dir_changed(self, name, old, new):
556 556 """Do a bit of validation of the notebook dir."""
557 557 if not os.path.isabs(new):
558 558 # If we receive a non-absolute path, make it absolute.
559 559 self.notebook_dir = os.path.abspath(new)
560 560 return
561 561 if not os.path.isdir(new):
562 562 raise TraitError("No such notebook dir: %r" % new)
563 563
564 564 # setting App.notebook_dir implies setting notebook and kernel dirs as well
565 565 self.config.FileNotebookManager.notebook_dir = new
566 566 self.config.MappingKernelManager.root_dir = new
567 567
568 568
569 569 def parse_command_line(self, argv=None):
570 570 super(NotebookApp, self).parse_command_line(argv)
571 571
572 572 if self.extra_args:
573 573 arg0 = self.extra_args[0]
574 574 f = os.path.abspath(arg0)
575 575 self.argv.remove(arg0)
576 576 if not os.path.exists(f):
577 577 self.log.critical("No such file or directory: %s", f)
578 578 self.exit(1)
579 579
580 580 # Use config here, to ensure that it takes higher priority than
581 581 # anything that comes from the profile.
582 582 c = Config()
583 583 if os.path.isdir(f):
584 584 c.NotebookApp.notebook_dir = f
585 585 elif os.path.isfile(f):
586 586 c.NotebookApp.file_to_run = f
587 587 self.update_config(c)
588 588
589 589 def init_kernel_argv(self):
590 590 """construct the kernel arguments"""
591 591 # Scrub frontend-specific flags
592 592 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
593 593 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
594 594 self.log.warn('\n '.join([
595 595 "Starting all kernels in pylab mode is not recommended,",
596 596 "and will be disabled in a future release.",
597 597 "Please use the %matplotlib magic to enable matplotlib instead.",
598 598 "pylab implies many imports, which can have confusing side effects",
599 599 "and harm the reproducibility of your notebooks.",
600 600 ]))
601 601 # Kernel should inherit default config file from frontend
602 602 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
603 603 # Kernel should get *absolute* path to profile directory
604 604 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
605 605
606 606 def init_configurables(self):
607 607 # force Session default to be secure
608 608 default_secure(self.config)
609 609 self.kernel_manager = MappingKernelManager(
610 610 parent=self, log=self.log, kernel_argv=self.kernel_argv,
611 611 connection_dir = self.profile_dir.security_dir,
612 612 )
613 613 kls = import_item(self.notebook_manager_class)
614 614 self.notebook_manager = kls(parent=self, log=self.log)
615 615 self.session_manager = SessionManager(parent=self, log=self.log)
616 616 self.cluster_manager = ClusterManager(parent=self, log=self.log)
617 617 self.cluster_manager.update_profiles()
618 618
619 619 def init_logging(self):
620 620 # This prevents double log messages because tornado use a root logger that
621 621 # self.log is a child of. The logging module dipatches log messages to a log
622 622 # and all of its ancenstors until propagate is set to False.
623 623 self.log.propagate = False
624 624
625 625 # hook up tornado 3's loggers to our app handlers
626 626 for name in ('access', 'application', 'general'):
627 627 logger = logging.getLogger('tornado.%s' % name)
628 628 logger.parent = self.log
629 629 logger.setLevel(self.log.level)
630 630
631 631 def init_webapp(self):
632 632 """initialize tornado webapp and httpserver"""
633 633 self.webapp_settings['allow_origin'] = self.allow_origin
634 if self.allow_origin_pat:
634 635 self.webapp_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
635 636 self.webapp_settings['allow_credentials'] = self.allow_credentials
636 637
637 638 self.web_app = NotebookWebApplication(
638 639 self, self.kernel_manager, self.notebook_manager,
639 640 self.cluster_manager, self.session_manager,
640 641 self.log, self.base_url, self.webapp_settings,
641 642 self.jinja_environment_options
642 643 )
643 644 if self.certfile:
644 645 ssl_options = dict(certfile=self.certfile)
645 646 if self.keyfile:
646 647 ssl_options['keyfile'] = self.keyfile
647 648 else:
648 649 ssl_options = None
649 650 self.web_app.password = self.password
650 651 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
651 652 xheaders=self.trust_xheaders)
652 653 if not self.ip:
653 654 warning = "WARNING: The notebook server is listening on all IP addresses"
654 655 if ssl_options is None:
655 656 self.log.critical(warning + " and not using encryption. This "
656 657 "is not recommended.")
657 658 if not self.password:
658 659 self.log.critical(warning + " and not using authentication. "
659 660 "This is highly insecure and not recommended.")
660 661 success = None
661 662 for port in random_ports(self.port, self.port_retries+1):
662 663 try:
663 664 self.http_server.listen(port, self.ip)
664 665 except socket.error as e:
665 666 if e.errno == errno.EADDRINUSE:
666 667 self.log.info('The port %i is already in use, trying another random port.' % port)
667 668 continue
668 669 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
669 670 self.log.warn("Permission to listen on port %i denied" % port)
670 671 continue
671 672 else:
672 673 raise
673 674 else:
674 675 self.port = port
675 676 success = True
676 677 break
677 678 if not success:
678 679 self.log.critical('ERROR: the notebook server could not be started because '
679 680 'no available port could be found.')
680 681 self.exit(1)
681 682
682 683 @property
683 684 def display_url(self):
684 685 ip = self.ip if self.ip else '[all ip addresses on your system]'
685 686 return self._url(ip)
686 687
687 688 @property
688 689 def connection_url(self):
689 690 ip = self.ip if self.ip else 'localhost'
690 691 return self._url(ip)
691 692
692 693 def _url(self, ip):
693 694 proto = 'https' if self.certfile else 'http'
694 695 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
695 696
696 697 def init_signal(self):
697 698 if not sys.platform.startswith('win'):
698 699 signal.signal(signal.SIGINT, self._handle_sigint)
699 700 signal.signal(signal.SIGTERM, self._signal_stop)
700 701 if hasattr(signal, 'SIGUSR1'):
701 702 # Windows doesn't support SIGUSR1
702 703 signal.signal(signal.SIGUSR1, self._signal_info)
703 704 if hasattr(signal, 'SIGINFO'):
704 705 # only on BSD-based systems
705 706 signal.signal(signal.SIGINFO, self._signal_info)
706 707
707 708 def _handle_sigint(self, sig, frame):
708 709 """SIGINT handler spawns confirmation dialog"""
709 710 # register more forceful signal handler for ^C^C case
710 711 signal.signal(signal.SIGINT, self._signal_stop)
711 712 # request confirmation dialog in bg thread, to avoid
712 713 # blocking the App
713 714 thread = threading.Thread(target=self._confirm_exit)
714 715 thread.daemon = True
715 716 thread.start()
716 717
717 718 def _restore_sigint_handler(self):
718 719 """callback for restoring original SIGINT handler"""
719 720 signal.signal(signal.SIGINT, self._handle_sigint)
720 721
721 722 def _confirm_exit(self):
722 723 """confirm shutdown on ^C
723 724
724 725 A second ^C, or answering 'y' within 5s will cause shutdown,
725 726 otherwise original SIGINT handler will be restored.
726 727
727 728 This doesn't work on Windows.
728 729 """
729 730 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
730 731 time.sleep(0.1)
731 732 info = self.log.info
732 733 info('interrupted')
733 734 print(self.notebook_info())
734 735 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
735 736 sys.stdout.flush()
736 737 r,w,x = select.select([sys.stdin], [], [], 5)
737 738 if r:
738 739 line = sys.stdin.readline()
739 740 if line.lower().startswith('y') and 'n' not in line.lower():
740 741 self.log.critical("Shutdown confirmed")
741 742 ioloop.IOLoop.instance().stop()
742 743 return
743 744 else:
744 745 print("No answer for 5s:", end=' ')
745 746 print("resuming operation...")
746 747 # no answer, or answer is no:
747 748 # set it back to original SIGINT handler
748 749 # use IOLoop.add_callback because signal.signal must be called
749 750 # from main thread
750 751 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
751 752
752 753 def _signal_stop(self, sig, frame):
753 754 self.log.critical("received signal %s, stopping", sig)
754 755 ioloop.IOLoop.instance().stop()
755 756
756 757 def _signal_info(self, sig, frame):
757 758 print(self.notebook_info())
758 759
759 760 def init_components(self):
760 761 """Check the components submodule, and warn if it's unclean"""
761 762 status = submodule.check_submodule_status()
762 763 if status == 'missing':
763 764 self.log.warn("components submodule missing, running `git submodule update`")
764 765 submodule.update_submodules(submodule.ipython_parent())
765 766 elif status == 'unclean':
766 767 self.log.warn("components submodule unclean, you may see 404s on static/components")
767 768 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
768 769
769 770 @catch_config_error
770 771 def initialize(self, argv=None):
771 772 super(NotebookApp, self).initialize(argv)
772 773 self.init_logging()
773 774 self.init_kernel_argv()
774 775 self.init_configurables()
775 776 self.init_components()
776 777 self.init_webapp()
777 778 self.init_signal()
778 779
779 780 def cleanup_kernels(self):
780 781 """Shutdown all kernels.
781 782
782 783 The kernels will shutdown themselves when this process no longer exists,
783 784 but explicit shutdown allows the KernelManagers to cleanup the connection files.
784 785 """
785 786 self.log.info('Shutting down kernels')
786 787 self.kernel_manager.shutdown_all()
787 788
788 789 def notebook_info(self):
789 790 "Return the current working directory and the server url information"
790 791 info = self.notebook_manager.info_string() + "\n"
791 792 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
792 793 return info + "The IPython Notebook is running at: %s" % self.display_url
793 794
794 795 def server_info(self):
795 796 """Return a JSONable dict of information about this server."""
796 797 return {'url': self.connection_url,
797 798 'hostname': self.ip if self.ip else 'localhost',
798 799 'port': self.port,
799 800 'secure': bool(self.certfile),
800 801 'base_url': self.base_url,
801 802 'notebook_dir': os.path.abspath(self.notebook_dir),
802 803 }
803 804
804 805 def write_server_info_file(self):
805 806 """Write the result of server_info() to the JSON file info_file."""
806 807 with open(self.info_file, 'w') as f:
807 808 json.dump(self.server_info(), f, indent=2)
808 809
809 810 def remove_server_info_file(self):
810 811 """Remove the nbserver-<pid>.json file created for this server.
811 812
812 813 Ignores the error raised when the file has already been removed.
813 814 """
814 815 try:
815 816 os.unlink(self.info_file)
816 817 except OSError as e:
817 818 if e.errno != errno.ENOENT:
818 819 raise
819 820
820 821 def start(self):
821 822 """ Start the IPython Notebook server app, after initialization
822 823
823 824 This method takes no arguments so all configuration and initialization
824 825 must be done prior to calling this method."""
825 826 if self.subapp is not None:
826 827 return self.subapp.start()
827 828
828 829 info = self.log.info
829 830 for line in self.notebook_info().split("\n"):
830 831 info(line)
831 832 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
832 833
833 834 self.write_server_info_file()
834 835
835 836 if self.open_browser or self.file_to_run:
836 837 try:
837 838 browser = webbrowser.get(self.browser or None)
838 839 except webbrowser.Error as e:
839 840 self.log.warn('No web browser found: %s.' % e)
840 841 browser = None
841 842
842 843 if self.file_to_run:
843 844 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
844 845 if not os.path.exists(fullpath):
845 846 self.log.critical("%s does not exist" % fullpath)
846 847 self.exit(1)
847 848
848 849 uri = url_path_join('notebooks', self.file_to_run)
849 850 else:
850 851 uri = 'tree'
851 852 if browser:
852 853 b = lambda : browser.open(url_path_join(self.connection_url, uri),
853 854 new=2)
854 855 threading.Thread(target=b).start()
855 856 try:
856 857 ioloop.IOLoop.instance().start()
857 858 except KeyboardInterrupt:
858 859 info("Interrupted...")
859 860 finally:
860 861 self.cleanup_kernels()
861 862 self.remove_server_info_file()
862 863
863 864
864 865 def list_running_servers(profile='default'):
865 866 """Iterate over the server info files of running notebook servers.
866 867
867 868 Given a profile name, find nbserver-* files in the security directory of
868 869 that profile, and yield dicts of their information, each one pertaining to
869 870 a currently running notebook server instance.
870 871 """
871 872 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
872 873 for file in os.listdir(pd.security_dir):
873 874 if file.startswith('nbserver-'):
874 875 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
875 876 yield json.load(f)
876 877
877 878 #-----------------------------------------------------------------------------
878 879 # Main entry point
879 880 #-----------------------------------------------------------------------------
880 881
881 882 launch_new_instance = NotebookApp.launch_instance
882 883
General Comments 0
You need to be logged in to leave comments. Login now