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