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