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