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