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