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