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