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