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