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