##// END OF EJS Templates
exit notebook cleanly on SIGINT, SIGTERM...
MinRK -
Show More
@@ -1,504 +1,513 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) 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 # stdlib
20 20 import errno
21 21 import logging
22 22 import os
23 23 import signal
24 24 import socket
25 25 import sys
26 26 import threading
27 27 import webbrowser
28 28
29 29 # Third party
30 30 import zmq
31 31
32 32 # Install the pyzmq ioloop. This has to be done before anything else from
33 33 # tornado is imported.
34 34 from zmq.eventloop import ioloop
35 35 # FIXME: ioloop.install is new in pyzmq-2.1.7, so remove this conditional
36 36 # when pyzmq dependency is updated beyond that.
37 37 if hasattr(ioloop, 'install'):
38 38 ioloop.install()
39 39 else:
40 40 import tornado.ioloop
41 41 tornado.ioloop.IOLoop = ioloop.IOLoop
42 42
43 43 from tornado import httpserver
44 44 from tornado import web
45 45
46 46 # Our own libraries
47 47 from .kernelmanager import MappingKernelManager
48 48 from .handlers import (LoginHandler, LogoutHandler,
49 49 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
50 50 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
51 51 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
52 52 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
53 53 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
54 54 )
55 55 from .notebookmanager import NotebookManager
56 56 from .clustermanager import ClusterManager
57 57
58 58 from IPython.config.application import catch_config_error, boolean_flag
59 59 from IPython.core.application import BaseIPythonApplication
60 60 from IPython.core.profiledir import ProfileDir
61 61 from IPython.lib.kernel import swallow_argv
62 62 from IPython.zmq.session import Session, default_secure
63 63 from IPython.zmq.zmqshell import ZMQInteractiveShell
64 64 from IPython.zmq.ipkernel import (
65 65 flags as ipkernel_flags,
66 66 aliases as ipkernel_aliases,
67 67 IPKernelApp
68 68 )
69 69 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
70 70 from IPython.utils import py3compat
71 71
72 72 #-----------------------------------------------------------------------------
73 73 # Module globals
74 74 #-----------------------------------------------------------------------------
75 75
76 76 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
77 77 _kernel_action_regex = r"(?P<action>restart|interrupt)"
78 78 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
79 79 _profile_regex = r"(?P<profile>[a-zA-Z0-9]+)"
80 80 _cluster_action_regex = r"(?P<action>start|stop)"
81 81
82 82
83 83 LOCALHOST = '127.0.0.1'
84 84
85 85 _examples = """
86 86 ipython notebook # start the notebook
87 87 ipython notebook --profile=sympy # use the sympy profile
88 88 ipython notebook --pylab=inline # pylab in inline plotting mode
89 89 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
90 90 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
91 91 """
92 92
93 93 #-----------------------------------------------------------------------------
94 94 # Helper functions
95 95 #-----------------------------------------------------------------------------
96 96
97 97 def url_path_join(a,b):
98 98 if a.endswith('/') and b.startswith('/'):
99 99 return a[:-1]+b
100 100 else:
101 101 return a+b
102 102
103 103 #-----------------------------------------------------------------------------
104 104 # The Tornado web application
105 105 #-----------------------------------------------------------------------------
106 106
107 107 class NotebookWebApplication(web.Application):
108 108
109 109 def __init__(self, ipython_app, kernel_manager, notebook_manager,
110 110 cluster_manager, log,
111 111 base_project_url, settings_overrides):
112 112 handlers = [
113 113 (r"/", ProjectDashboardHandler),
114 114 (r"/login", LoginHandler),
115 115 (r"/logout", LogoutHandler),
116 116 (r"/new", NewHandler),
117 117 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
118 118 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
119 119 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
120 120 (r"/kernels", MainKernelHandler),
121 121 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
122 122 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
123 123 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
124 124 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
125 125 (r"/notebooks", NotebookRootHandler),
126 126 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
127 127 (r"/rstservice/render", RSTHandler),
128 128 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
129 129 (r"/clusters", MainClusterHandler),
130 130 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
131 131 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
132 132 ]
133 133 settings = dict(
134 134 template_path=os.path.join(os.path.dirname(__file__), "templates"),
135 135 static_path=os.path.join(os.path.dirname(__file__), "static"),
136 136 cookie_secret=os.urandom(1024),
137 137 login_url="/login",
138 138 )
139 139
140 140 # allow custom overrides for the tornado web app.
141 141 settings.update(settings_overrides)
142 142
143 143 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
144 144 # base_project_url will always be unicode, which will in turn
145 145 # make the patterns unicode, and ultimately result in unicode
146 146 # keys in kwargs to handler._execute(**kwargs) in tornado.
147 147 # This enforces that base_project_url be ascii in that situation.
148 148 #
149 149 # Note that the URLs these patterns check against are escaped,
150 150 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
151 151 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
152 152
153 153 # prepend base_project_url onto the patterns that we match
154 154 new_handlers = []
155 155 for handler in handlers:
156 156 pattern = url_path_join(base_project_url, handler[0])
157 157 new_handler = tuple([pattern]+list(handler[1:]))
158 158 new_handlers.append( new_handler )
159 159
160 160 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
161 161
162 162 self.kernel_manager = kernel_manager
163 163 self.notebook_manager = notebook_manager
164 164 self.cluster_manager = cluster_manager
165 165 self.ipython_app = ipython_app
166 166 self.read_only = self.ipython_app.read_only
167 167 self.log = log
168 168
169 169
170 170 #-----------------------------------------------------------------------------
171 171 # Aliases and Flags
172 172 #-----------------------------------------------------------------------------
173 173
174 174 flags = dict(ipkernel_flags)
175 175 flags['no-browser']=(
176 176 {'NotebookApp' : {'open_browser' : False}},
177 177 "Don't open the notebook in a browser after startup."
178 178 )
179 179 flags['no-mathjax']=(
180 180 {'NotebookApp' : {'enable_mathjax' : False}},
181 181 """Disable MathJax
182 182
183 183 MathJax is the javascript library IPython uses to render math/LaTeX. It is
184 184 very large, so you may want to disable it if you have a slow internet
185 185 connection, or for offline use of the notebook.
186 186
187 187 When disabled, equations etc. will appear as their untransformed TeX source.
188 188 """
189 189 )
190 190 flags['read-only'] = (
191 191 {'NotebookApp' : {'read_only' : True}},
192 192 """Allow read-only access to notebooks.
193 193
194 194 When using a password to protect the notebook server, this flag
195 195 allows unauthenticated clients to view the notebook list, and
196 196 individual notebooks, but not edit them, start kernels, or run
197 197 code.
198 198
199 199 If no password is set, the server will be entirely read-only.
200 200 """
201 201 )
202 202
203 203 # Add notebook manager flags
204 204 flags.update(boolean_flag('script', 'NotebookManager.save_script',
205 205 'Auto-save a .py script everytime the .ipynb notebook is saved',
206 206 'Do not auto-save .py scripts for every notebook'))
207 207
208 208 # the flags that are specific to the frontend
209 209 # these must be scrubbed before being passed to the kernel,
210 210 # or it will raise an error on unrecognized flags
211 211 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
212 212
213 213 aliases = dict(ipkernel_aliases)
214 214
215 215 aliases.update({
216 216 'ip': 'NotebookApp.ip',
217 217 'port': 'NotebookApp.port',
218 218 'keyfile': 'NotebookApp.keyfile',
219 219 'certfile': 'NotebookApp.certfile',
220 220 'notebook-dir': 'NotebookManager.notebook_dir',
221 221 'browser': 'NotebookApp.browser',
222 222 })
223 223
224 224 # remove ipkernel flags that are singletons, and don't make sense in
225 225 # multi-kernel evironment:
226 226 aliases.pop('f', None)
227 227
228 228 notebook_aliases = [u'port', u'ip', u'keyfile', u'certfile',
229 229 u'notebook-dir']
230 230
231 231 #-----------------------------------------------------------------------------
232 232 # NotebookApp
233 233 #-----------------------------------------------------------------------------
234 234
235 235 class NotebookApp(BaseIPythonApplication):
236 236
237 237 name = 'ipython-notebook'
238 238 default_config_file_name='ipython_notebook_config.py'
239 239
240 240 description = """
241 241 The IPython HTML Notebook.
242 242
243 243 This launches a Tornado based HTML Notebook Server that serves up an
244 244 HTML5/Javascript Notebook client.
245 245 """
246 246 examples = _examples
247 247
248 248 classes = [IPKernelApp, ZMQInteractiveShell, ProfileDir, Session,
249 249 MappingKernelManager, NotebookManager]
250 250 flags = Dict(flags)
251 251 aliases = Dict(aliases)
252 252
253 253 kernel_argv = List(Unicode)
254 254
255 255 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
256 256 default_value=logging.INFO,
257 257 config=True,
258 258 help="Set the log level by value or name.")
259 259
260 260 # create requested profiles by default, if they don't exist:
261 261 auto_create = Bool(True)
262 262
263 263 # Network related information.
264 264
265 265 ip = Unicode(LOCALHOST, config=True,
266 266 help="The IP address the notebook server will listen on."
267 267 )
268 268
269 269 def _ip_changed(self, name, old, new):
270 270 if new == u'*': self.ip = u''
271 271
272 272 port = Integer(8888, config=True,
273 273 help="The port the notebook server will listen on."
274 274 )
275 275
276 276 certfile = Unicode(u'', config=True,
277 277 help="""The full path to an SSL/TLS certificate file."""
278 278 )
279 279
280 280 keyfile = Unicode(u'', config=True,
281 281 help="""The full path to a private key file for usage with SSL/TLS."""
282 282 )
283 283
284 284 password = Unicode(u'', config=True,
285 285 help="""Hashed password to use for web authentication.
286 286
287 287 To generate, type in a python/IPython shell:
288 288
289 289 from IPython.lib import passwd; passwd()
290 290
291 291 The string should be of the form type:salt:hashed-password.
292 292 """
293 293 )
294 294
295 295 open_browser = Bool(True, config=True,
296 296 help="""Whether to open in a browser after starting.
297 297 The specific browser used is platform dependent and
298 298 determined by the python standard library `webbrowser`
299 299 module, unless it is overridden using the --browser
300 300 (NotebookApp.browser) configuration option.
301 301 """)
302 302
303 303 browser = Unicode(u'', config=True,
304 304 help="""Specify what command to use to invoke a web
305 305 browser when opening the notebook. If not specified, the
306 306 default browser will be determined by the `webbrowser`
307 307 standard library module, which allows setting of the
308 308 BROWSER environment variable to override it.
309 309 """)
310 310
311 311 read_only = Bool(False, config=True,
312 312 help="Whether to prevent editing/execution of notebooks."
313 313 )
314 314
315 315 webapp_settings = Dict(config=True,
316 316 help="Supply overrides for the tornado.web.Application that the "
317 317 "IPython notebook uses.")
318 318
319 319 enable_mathjax = Bool(True, config=True,
320 320 help="""Whether to enable MathJax for typesetting math/TeX
321 321
322 322 MathJax is the javascript library IPython uses to render math/LaTeX. It is
323 323 very large, so you may want to disable it if you have a slow internet
324 324 connection, or for offline use of the notebook.
325 325
326 326 When disabled, equations etc. will appear as their untransformed TeX source.
327 327 """
328 328 )
329 329 def _enable_mathjax_changed(self, name, old, new):
330 330 """set mathjax url to empty if mathjax is disabled"""
331 331 if not new:
332 332 self.mathjax_url = u''
333 333
334 334 base_project_url = Unicode('/', config=True,
335 335 help='''The base URL for the notebook server''')
336 336 base_kernel_url = Unicode('/', config=True,
337 337 help='''The base URL for the kernel server''')
338 338 websocket_host = Unicode("", config=True,
339 339 help="""The hostname for the websocket server."""
340 340 )
341 341
342 342 mathjax_url = Unicode("", config=True,
343 343 help="""The url for MathJax.js."""
344 344 )
345 345 def _mathjax_url_default(self):
346 346 if not self.enable_mathjax:
347 347 return u''
348 348 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
349 349 static_url_prefix = self.webapp_settings.get("static_url_prefix",
350 350 "/static/")
351 351 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
352 352 self.log.info("Using local MathJax")
353 353 return static_url_prefix+u"mathjax/MathJax.js"
354 354 else:
355 355 self.log.info("Using MathJax from CDN")
356 356 hostname = "cdn.mathjax.org"
357 357 try:
358 358 # resolve mathjax cdn alias to cloudfront, because Amazon's SSL certificate
359 359 # only works on *.cloudfront.net
360 360 true_host, aliases, IPs = socket.gethostbyname_ex(hostname)
361 361 # I've run this on a few machines, and some put the right answer in true_host,
362 362 # while others gave it in the aliases list, so we check both.
363 363 aliases.insert(0, true_host)
364 364 except Exception:
365 365 self.log.warn("Couldn't determine MathJax CDN info")
366 366 else:
367 367 for alias in aliases:
368 368 parts = alias.split('.')
369 369 # want static foo.cloudfront.net, not dynamic foo.lax3.cloudfront.net
370 370 if len(parts) == 3 and alias.endswith(".cloudfront.net"):
371 371 hostname = alias
372 372 break
373 373
374 374 if not hostname.endswith(".cloudfront.net"):
375 375 self.log.error("Couldn't resolve CloudFront host, required for HTTPS MathJax.")
376 376 self.log.error("Loading from https://cdn.mathjax.org will probably fail due to invalid certificate.")
377 377 self.log.error("For unsecured HTTP access to MathJax use config:")
378 378 self.log.error("NotebookApp.mathjax_url='http://cdn.mathjax.org/mathjax/latest/MathJax.js'")
379 379 return u"https://%s/mathjax/latest/MathJax.js" % hostname
380 380
381 381 def _mathjax_url_changed(self, name, old, new):
382 382 if new and not self.enable_mathjax:
383 383 # enable_mathjax=False overrides mathjax_url
384 384 self.mathjax_url = u''
385 385 else:
386 386 self.log.info("Using MathJax: %s", new)
387 387
388 388 def parse_command_line(self, argv=None):
389 389 super(NotebookApp, self).parse_command_line(argv)
390 390 if argv is None:
391 391 argv = sys.argv[1:]
392 392
393 393 # Scrub frontend-specific flags
394 394 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
395 395 # Kernel should inherit default config file from frontend
396 396 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
397 397
398 398 def init_configurables(self):
399 399 # force Session default to be secure
400 400 default_secure(self.config)
401 401 # Create a KernelManager and start a kernel.
402 402 self.kernel_manager = MappingKernelManager(
403 403 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
404 404 connection_dir = self.profile_dir.security_dir,
405 405 )
406 406 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
407 407 self.notebook_manager.list_notebooks()
408 408 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
409 409 self.cluster_manager.update_profiles()
410 410
411 411 def init_logging(self):
412 412 super(NotebookApp, self).init_logging()
413 413 # This prevents double log messages because tornado use a root logger that
414 414 # self.log is a child of. The logging module dipatches log messages to a log
415 415 # and all of its ancenstors until propagate is set to False.
416 416 self.log.propagate = False
417 417
418 418 def init_webapp(self):
419 419 """initialize tornado webapp and httpserver"""
420 420 self.web_app = NotebookWebApplication(
421 421 self, self.kernel_manager, self.notebook_manager,
422 422 self.cluster_manager, self.log,
423 423 self.base_project_url, self.webapp_settings
424 424 )
425 425 if self.certfile:
426 426 ssl_options = dict(certfile=self.certfile)
427 427 if self.keyfile:
428 428 ssl_options['keyfile'] = self.keyfile
429 429 else:
430 430 ssl_options = None
431 431 self.web_app.password = self.password
432 432 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
433 433 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
434 434 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
435 435 'but not using any encryption or authentication. This is highly '
436 436 'insecure and not recommended.')
437 437
438 438 # Try random ports centered around the default.
439 439 from random import randint
440 440 n = 50 # Max number of attempts, keep reasonably large.
441 441 for port in range(self.port, self.port+5) + [self.port + randint(-2*n, 2*n) for i in range(n-5)]:
442 442 try:
443 443 self.http_server.listen(port, self.ip)
444 444 except socket.error, e:
445 445 if e.errno != errno.EADDRINUSE:
446 446 raise
447 447 self.log.info('The port %i is already in use, trying another random port.' % port)
448 448 else:
449 449 self.port = port
450 450 break
451 451
452 def init_signal(self):
453 signal.signal(signal.SIGINT, self._handle_signal)
454 signal.signal(signal.SIGTERM, self._handle_signal)
455
456 def _handle_signal(self, sig, frame):
457 self.log.critical("received signal %s, stopping", sig)
458 ioloop.IOLoop.instance().stop()
459
452 460 @catch_config_error
453 461 def initialize(self, argv=None):
454 462 super(NotebookApp, self).initialize(argv)
455 463 self.init_configurables()
456 464 self.init_webapp()
465 self.init_signal()
457 466
458 467 def cleanup_kernels(self):
459 468 """shutdown all kernels
460 469
461 470 The kernels will shutdown themselves when this process no longer exists,
462 471 but explicit shutdown allows the KernelManagers to cleanup the connection files.
463 472 """
464 473 self.log.info('Shutting down kernels')
465 474 km = self.kernel_manager
466 475 # copy list, since kill_kernel deletes keys
467 476 for kid in list(km.kernel_ids):
468 477 km.kill_kernel(kid)
469 478
470 479 def start(self):
471 480 ip = self.ip if self.ip else '[all ip addresses on your system]'
472 481 proto = 'https' if self.certfile else 'http'
473 482 info = self.log.info
474 483 info("The IPython Notebook is running at: %s://%s:%i%s" %
475 484 (proto, ip, self.port,self.base_project_url) )
476 485 info("Use Control-C to stop this server and shut down all kernels.")
477 486
478 487 if self.open_browser:
479 488 ip = self.ip or '127.0.0.1'
480 489 if self.browser:
481 490 browser = webbrowser.get(self.browser)
482 491 else:
483 492 browser = webbrowser.get()
484 493 b = lambda : browser.open("%s://%s:%i%s" % (proto, ip, self.port,
485 494 self.base_project_url),
486 495 new=2)
487 496 threading.Thread(target=b).start()
488 497 try:
489 498 ioloop.IOLoop.instance().start()
490 499 except KeyboardInterrupt:
491 500 info("Interrupted...")
492 501 finally:
493 502 self.cleanup_kernels()
494 503
495 504
496 505 #-----------------------------------------------------------------------------
497 506 # Main entry point
498 507 #-----------------------------------------------------------------------------
499 508
500 509 def launch_new_instance():
501 510 app = NotebookApp.instance()
502 511 app.initialize()
503 512 app.start()
504 513
General Comments 0
You need to be logged in to leave comments. Login now