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