##// END OF EJS Templates
Merge pull request #4546 from minrk/not-in-dir-warning...
Matthias Bussonnier -
r13643:29626775 merge
parent child Browse files
Show More
@@ -1,748 +1,749
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
7 7 """
8 8 from __future__ import print_function
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2013 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 # stdlib
21 21 import errno
22 22 import logging
23 23 import os
24 24 import random
25 25 import select
26 26 import signal
27 27 import socket
28 28 import sys
29 29 import threading
30 30 import time
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 from jinja2 import Environment, FileSystemLoader
40 40
41 41 # Install the pyzmq ioloop. This has to be done before anything else from
42 42 # tornado is imported.
43 43 from zmq.eventloop import ioloop
44 44 ioloop.install()
45 45
46 46 # check for tornado 3.1.0
47 47 msg = "The IPython Notebook requires tornado >= 3.1.0"
48 48 try:
49 49 import tornado
50 50 except ImportError:
51 51 raise ImportError(msg)
52 52 try:
53 53 version_info = tornado.version_info
54 54 except AttributeError:
55 55 raise ImportError(msg + ", but you have < 1.1.0")
56 56 if version_info < (3,1,0):
57 57 raise ImportError(msg + ", but you have %s" % tornado.version)
58 58
59 59 from tornado import httpserver
60 60 from tornado import web
61 61
62 62 # Our own libraries
63 63 from IPython.html import DEFAULT_STATIC_FILES_PATH
64 64
65 65 from .services.kernels.kernelmanager import MappingKernelManager
66 66 from .services.notebooks.nbmanager import NotebookManager
67 67 from .services.notebooks.filenbmanager import FileNotebookManager
68 68 from .services.clusters.clustermanager import ClusterManager
69 69 from .services.sessions.sessionmanager import SessionManager
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, get_ipython_dir
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 --certfile=mycert.pem # use SSL/TLS certificate
102 102 """
103 103
104 104 #-----------------------------------------------------------------------------
105 105 # Helper functions
106 106 #-----------------------------------------------------------------------------
107 107
108 108 def random_ports(port, n):
109 109 """Generate a list of n random ports near the given port.
110 110
111 111 The first 5 ports will be sequential, and the remaining n-5 will be
112 112 randomly selected in the range [port-2*n, port+2*n].
113 113 """
114 114 for i in range(min(5, n)):
115 115 yield port + i
116 116 for i in range(n-5):
117 117 yield max(1, port + random.randint(-2*n, 2*n))
118 118
119 119 def load_handlers(name):
120 120 """Load the (URL pattern, handler) tuples for each component."""
121 121 name = 'IPython.html.' + name
122 122 mod = __import__(name, fromlist=['default_handlers'])
123 123 return mod.default_handlers
124 124
125 125 #-----------------------------------------------------------------------------
126 126 # The Tornado web application
127 127 #-----------------------------------------------------------------------------
128 128
129 129 class NotebookWebApplication(web.Application):
130 130
131 131 def __init__(self, ipython_app, kernel_manager, notebook_manager,
132 132 cluster_manager, session_manager, log, base_project_url,
133 133 settings_overrides):
134 134
135 135 settings = self.init_settings(
136 136 ipython_app, kernel_manager, notebook_manager, cluster_manager,
137 137 session_manager, log, base_project_url, settings_overrides)
138 138 handlers = self.init_handlers(settings)
139 139
140 140 super(NotebookWebApplication, self).__init__(handlers, **settings)
141 141
142 142 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
143 143 cluster_manager, session_manager, log, base_project_url,
144 144 settings_overrides):
145 145 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
146 146 # base_project_url will always be unicode, which will in turn
147 147 # make the patterns unicode, and ultimately result in unicode
148 148 # keys in kwargs to handler._execute(**kwargs) in tornado.
149 149 # This enforces that base_project_url be ascii in that situation.
150 150 #
151 151 # Note that the URLs these patterns check against are escaped,
152 152 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
153 153 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
154 154 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
155 155 settings = dict(
156 156 # basics
157 157 base_project_url=base_project_url,
158 158 base_kernel_url=ipython_app.base_kernel_url,
159 159 template_path=template_path,
160 160 static_path=ipython_app.static_file_path,
161 161 static_handler_class = FileFindHandler,
162 162 static_url_prefix = url_path_join(base_project_url,'/static/'),
163 163
164 164 # authentication
165 165 cookie_secret=ipython_app.cookie_secret,
166 166 login_url=url_path_join(base_project_url,'/login'),
167 167 password=ipython_app.password,
168 168
169 169 # managers
170 170 kernel_manager=kernel_manager,
171 171 notebook_manager=notebook_manager,
172 172 cluster_manager=cluster_manager,
173 173 session_manager=session_manager,
174 174
175 175 # IPython stuff
176 176 nbextensions_path = ipython_app.nbextensions_path,
177 177 mathjax_url=ipython_app.mathjax_url,
178 178 config=ipython_app.config,
179 179 use_less=ipython_app.use_less,
180 180 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
181 181 )
182 182
183 183 # allow custom overrides for the tornado web app.
184 184 settings.update(settings_overrides)
185 185 return settings
186 186
187 187 def init_handlers(self, settings):
188 188 # Load the (URL pattern, handler) tuples for each component.
189 189 handlers = []
190 190 handlers.extend(load_handlers('base.handlers'))
191 191 handlers.extend(load_handlers('tree.handlers'))
192 192 handlers.extend(load_handlers('auth.login'))
193 193 handlers.extend(load_handlers('auth.logout'))
194 194 handlers.extend(load_handlers('notebook.handlers'))
195 195 handlers.extend(load_handlers('services.kernels.handlers'))
196 196 handlers.extend(load_handlers('services.notebooks.handlers'))
197 197 handlers.extend(load_handlers('services.clusters.handlers'))
198 198 handlers.extend(load_handlers('services.sessions.handlers'))
199 199 handlers.extend([
200 200 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
201 201 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
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
234 234 # Add notebook manager flags
235 235 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
236 236 'Auto-save a .py script everytime the .ipynb notebook is saved',
237 237 'Do not auto-save .py scripts for every notebook'))
238 238
239 239 # the flags that are specific to the frontend
240 240 # these must be scrubbed before being passed to the kernel,
241 241 # or it will raise an error on unrecognized flags
242 242 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
243 243
244 244 aliases = dict(kernel_aliases)
245 245
246 246 aliases.update({
247 247 'ip': 'NotebookApp.ip',
248 248 'port': 'NotebookApp.port',
249 249 'port-retries': 'NotebookApp.port_retries',
250 250 'transport': 'KernelManager.transport',
251 251 'keyfile': 'NotebookApp.keyfile',
252 252 'certfile': 'NotebookApp.certfile',
253 253 'notebook-dir': 'NotebookManager.notebook_dir',
254 254 'browser': 'NotebookApp.browser',
255 255 })
256 256
257 257 # remove ipkernel flags that are singletons, and don't make sense in
258 258 # multi-kernel evironment:
259 259 aliases.pop('f', None)
260 260
261 261 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
262 262 u'notebook-dir', u'profile', u'profile-dir']
263 263
264 264 #-----------------------------------------------------------------------------
265 265 # NotebookApp
266 266 #-----------------------------------------------------------------------------
267 267
268 268 class NotebookApp(BaseIPythonApplication):
269 269
270 270 name = 'ipython-notebook'
271 271
272 272 description = """
273 273 The IPython HTML Notebook.
274 274
275 275 This launches a Tornado based HTML Notebook Server that serves up an
276 276 HTML5/Javascript Notebook client.
277 277 """
278 278 examples = _examples
279 279
280 280 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
281 281 FileNotebookManager]
282 282 flags = Dict(flags)
283 283 aliases = Dict(aliases)
284 284
285 285 kernel_argv = List(Unicode)
286 286
287 287 def _log_level_default(self):
288 288 return logging.INFO
289 289
290 290 def _log_format_default(self):
291 291 """override default log format to include time"""
292 292 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
293 293
294 294 # create requested profiles by default, if they don't exist:
295 295 auto_create = Bool(True)
296 296
297 297 # file to be opened in the notebook server
298 298 file_to_run = 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 arg0 = self.extra_args[0]
505 505 f = os.path.abspath(arg0)
506 506 self.argv.remove(arg0)
507 507 if not os.path.exists(f):
508 508 self.log.critical("No such file or directory: %s", f)
509 509 self.exit(1)
510 510 if os.path.isdir(f):
511 511 self.config.FileNotebookManager.notebook_dir = f
512 512 elif os.path.isfile(f):
513 513 self.file_to_run = f
514 514
515 515 def init_kernel_argv(self):
516 516 """construct the kernel arguments"""
517 517 # Scrub frontend-specific flags
518 518 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
519 519 # Kernel should inherit default config file from frontend
520 520 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
521 521 # Kernel should get *absolute* path to profile directory
522 522 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
523 523
524 524 def init_configurables(self):
525 525 # force Session default to be secure
526 526 default_secure(self.config)
527 527 self.kernel_manager = MappingKernelManager(
528 528 parent=self, log=self.log, kernel_argv=self.kernel_argv,
529 529 connection_dir = self.profile_dir.security_dir,
530 530 )
531 531 kls = import_item(self.notebook_manager_class)
532 532 self.notebook_manager = kls(parent=self, log=self.log)
533 533 self.session_manager = SessionManager(parent=self, log=self.log)
534 534 self.cluster_manager = ClusterManager(parent=self, log=self.log)
535 535 self.cluster_manager.update_profiles()
536 536
537 537 def init_logging(self):
538 538 # This prevents double log messages because tornado use a root logger that
539 539 # self.log is a child of. The logging module dipatches log messages to a log
540 540 # and all of its ancenstors until propagate is set to False.
541 541 self.log.propagate = False
542 542
543 543 # hook up tornado 3's loggers to our app handlers
544 544 for name in ('access', 'application', 'general'):
545 545 logger = logging.getLogger('tornado.%s' % name)
546 546 logger.parent = self.log
547 547 logger.setLevel(self.log.level)
548 548
549 549 def init_webapp(self):
550 550 """initialize tornado webapp and httpserver"""
551 551 self.web_app = NotebookWebApplication(
552 552 self, self.kernel_manager, self.notebook_manager,
553 553 self.cluster_manager, self.session_manager,
554 554 self.log, self.base_project_url, self.webapp_settings
555 555 )
556 556 if self.certfile:
557 557 ssl_options = dict(certfile=self.certfile)
558 558 if self.keyfile:
559 559 ssl_options['keyfile'] = self.keyfile
560 560 else:
561 561 ssl_options = None
562 562 self.web_app.password = self.password
563 563 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
564 564 xheaders=self.trust_xheaders)
565 565 if not self.ip:
566 566 warning = "WARNING: The notebook server is listening on all IP addresses"
567 567 if ssl_options is None:
568 568 self.log.critical(warning + " and not using encryption. This "
569 569 "is not recommended.")
570 570 if not self.password:
571 571 self.log.critical(warning + " and not using authentication. "
572 572 "This is highly insecure and not recommended.")
573 573 success = None
574 574 for port in random_ports(self.port, self.port_retries+1):
575 575 try:
576 576 self.http_server.listen(port, self.ip)
577 577 except socket.error as e:
578 578 if e.errno == errno.EADDRINUSE:
579 579 self.log.info('The port %i is already in use, trying another random port.' % port)
580 580 continue
581 581 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
582 582 self.log.warn("Permission to listen on port %i denied" % port)
583 583 continue
584 584 else:
585 585 raise
586 586 else:
587 587 self.port = port
588 588 success = True
589 589 break
590 590 if not success:
591 591 self.log.critical('ERROR: the notebook server could not be started because '
592 592 'no available port could be found.')
593 593 self.exit(1)
594 594
595 595 def init_signal(self):
596 596 if not sys.platform.startswith('win'):
597 597 signal.signal(signal.SIGINT, self._handle_sigint)
598 598 signal.signal(signal.SIGTERM, self._signal_stop)
599 599 if hasattr(signal, 'SIGUSR1'):
600 600 # Windows doesn't support SIGUSR1
601 601 signal.signal(signal.SIGUSR1, self._signal_info)
602 602 if hasattr(signal, 'SIGINFO'):
603 603 # only on BSD-based systems
604 604 signal.signal(signal.SIGINFO, self._signal_info)
605 605
606 606 def _handle_sigint(self, sig, frame):
607 607 """SIGINT handler spawns confirmation dialog"""
608 608 # register more forceful signal handler for ^C^C case
609 609 signal.signal(signal.SIGINT, self._signal_stop)
610 610 # request confirmation dialog in bg thread, to avoid
611 611 # blocking the App
612 612 thread = threading.Thread(target=self._confirm_exit)
613 613 thread.daemon = True
614 614 thread.start()
615 615
616 616 def _restore_sigint_handler(self):
617 617 """callback for restoring original SIGINT handler"""
618 618 signal.signal(signal.SIGINT, self._handle_sigint)
619 619
620 620 def _confirm_exit(self):
621 621 """confirm shutdown on ^C
622 622
623 623 A second ^C, or answering 'y' within 5s will cause shutdown,
624 624 otherwise original SIGINT handler will be restored.
625 625
626 626 This doesn't work on Windows.
627 627 """
628 628 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
629 629 time.sleep(0.1)
630 630 info = self.log.info
631 631 info('interrupted')
632 632 print(self.notebook_info())
633 633 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
634 634 sys.stdout.flush()
635 635 r,w,x = select.select([sys.stdin], [], [], 5)
636 636 if r:
637 637 line = sys.stdin.readline()
638 638 if line.lower().startswith('y'):
639 639 self.log.critical("Shutdown confirmed")
640 640 ioloop.IOLoop.instance().stop()
641 641 return
642 642 else:
643 643 print("No answer for 5s:", end=' ')
644 644 print("resuming operation...")
645 645 # no answer, or answer is no:
646 646 # set it back to original SIGINT handler
647 647 # use IOLoop.add_callback because signal.signal must be called
648 648 # from main thread
649 649 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
650 650
651 651 def _signal_stop(self, sig, frame):
652 652 self.log.critical("received signal %s, stopping", sig)
653 653 ioloop.IOLoop.instance().stop()
654 654
655 655 def _signal_info(self, sig, frame):
656 656 print(self.notebook_info())
657 657
658 658 def init_components(self):
659 659 """Check the components submodule, and warn if it's unclean"""
660 660 status = submodule.check_submodule_status()
661 661 if status == 'missing':
662 662 self.log.warn("components submodule missing, running `git submodule update`")
663 663 submodule.update_submodules(submodule.ipython_parent())
664 664 elif status == 'unclean':
665 665 self.log.warn("components submodule unclean, you may see 404s on static/components")
666 666 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
667 667
668 668
669 669 @catch_config_error
670 670 def initialize(self, argv=None):
671 671 super(NotebookApp, self).initialize(argv)
672 672 self.init_logging()
673 673 self.init_kernel_argv()
674 674 self.init_configurables()
675 675 self.init_components()
676 676 self.init_webapp()
677 677 self.init_signal()
678 678
679 679 def cleanup_kernels(self):
680 680 """Shutdown all kernels.
681 681
682 682 The kernels will shutdown themselves when this process no longer exists,
683 683 but explicit shutdown allows the KernelManagers to cleanup the connection files.
684 684 """
685 685 self.log.info('Shutting down kernels')
686 686 self.kernel_manager.shutdown_all()
687 687
688 688 def notebook_info(self):
689 689 "Return the current working directory and the server url information"
690 690 info = self.notebook_manager.info_string() + "\n"
691 691 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
692 692 return info + "The IPython Notebook is running at: %s" % self._url
693 693
694 694 def start(self):
695 695 """ Start the IPython Notebook server app, after initialization
696 696
697 697 This method takes no arguments so all configuration and initialization
698 698 must be done prior to calling this method."""
699 699 ip = self.ip if self.ip else '[all ip addresses on your system]'
700 700 proto = 'https' if self.certfile else 'http'
701 701 info = self.log.info
702 702 self._url = "%s://%s:%i%s" % (proto, ip, self.port,
703 703 self.base_project_url)
704 704 for line in self.notebook_info().split("\n"):
705 705 info(line)
706 706 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
707 707
708 708 if self.open_browser or self.file_to_run:
709 709 ip = self.ip or localhost()
710 710 try:
711 711 browser = webbrowser.get(self.browser or None)
712 712 except webbrowser.Error as e:
713 713 self.log.warn('No web browser found: %s.' % e)
714 714 browser = None
715 715
716 716 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
717 717 f = self.file_to_run
718 if f and f.startswith(nbdir):
719 f = f[len(nbdir):]
720 else:
721 self.log.warn(
722 "Probably won't be able to open notebook %s "
723 "because it is not in notebook_dir %s",
724 f, nbdir,
725 )
718 if f:
719 if f.startswith(nbdir):
720 f = f[len(nbdir):]
721 else:
722 self.log.warn(
723 "Probably won't be able to open notebook %s "
724 "because it is not in notebook_dir %s",
725 f, nbdir,
726 )
726 727
727 728 if os.path.isfile(self.file_to_run):
728 729 url = url_path_join('notebooks', f)
729 730 else:
730 731 url = url_path_join('tree', f)
731 732 if browser:
732 733 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
733 734 self.port, self.base_project_url, url), new=2)
734 735 threading.Thread(target=b).start()
735 736 try:
736 737 ioloop.IOLoop.instance().start()
737 738 except KeyboardInterrupt:
738 739 info("Interrupted...")
739 740 finally:
740 741 self.cleanup_kernels()
741 742
742 743
743 744 #-----------------------------------------------------------------------------
744 745 # Main entry point
745 746 #-----------------------------------------------------------------------------
746 747
747 748 launch_new_instance = NotebookApp.launch_instance
748 749
General Comments 0
You need to be logged in to leave comments. Login now