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