##// END OF EJS Templates
add `ipython notebook trust` subcommand
MinRK -
Show More
@@ -1,837 +1,874 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 from __future__ import print_function
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2013 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 # stdlib
21 21 import errno
22 22 import io
23 23 import json
24 24 import logging
25 25 import os
26 26 import random
27 27 import select
28 28 import signal
29 29 import socket
30 30 import sys
31 31 import threading
32 32 import time
33 33 import webbrowser
34 34
35 35
36 36 # Third party
37 37 # check for pyzmq 2.1.11
38 38 from IPython.utils.zmqrelated import check_for_zmq
39 39 check_for_zmq('2.1.11', 'IPython.html')
40 40
41 41 from jinja2 import Environment, FileSystemLoader
42 42
43 43 # Install the pyzmq ioloop. This has to be done before anything else from
44 44 # tornado is imported.
45 45 from zmq.eventloop import ioloop
46 46 ioloop.install()
47 47
48 48 # check for tornado 3.1.0
49 49 msg = "The IPython Notebook requires tornado >= 3.1.0"
50 50 try:
51 51 import tornado
52 52 except ImportError:
53 53 raise ImportError(msg)
54 54 try:
55 55 version_info = tornado.version_info
56 56 except AttributeError:
57 57 raise ImportError(msg + ", but you have < 1.1.0")
58 58 if version_info < (3,1,0):
59 59 raise ImportError(msg + ", but you have %s" % tornado.version)
60 60
61 61 from tornado import httpserver
62 62 from tornado import web
63 63
64 64 # Our own libraries
65 65 from IPython.html import DEFAULT_STATIC_FILES_PATH
66 66 from .base.handlers import Template404
67 67 from .log import log_request
68 68 from .services.kernels.kernelmanager import MappingKernelManager
69 69 from .services.notebooks.nbmanager import NotebookManager
70 70 from .services.notebooks.filenbmanager import FileNotebookManager
71 71 from .services.clusters.clustermanager import ClusterManager
72 72 from .services.sessions.sessionmanager import SessionManager
73 73
74 74 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
75 75
76 76 from IPython.config.application import catch_config_error, boolean_flag
77 77 from IPython.core.application import BaseIPythonApplication
78 78 from IPython.core.profiledir import ProfileDir
79 79 from IPython.consoleapp import IPythonConsoleApp
80 80 from IPython.kernel import swallow_argv
81 81 from IPython.kernel.zmq.session import default_secure
82 82 from IPython.kernel.zmq.kernelapp import (
83 83 kernel_flags,
84 84 kernel_aliases,
85 85 )
86 from IPython.nbformat import current, sign
86 87 from IPython.utils.importstring import import_item
87 88 from IPython.utils.localinterfaces import localhost
88 89 from IPython.utils import submodule
89 90 from IPython.utils.traitlets import (
90 91 Dict, Unicode, Integer, List, Bool, Bytes,
91 DottedObjectName
92 DottedObjectName, Instance,
92 93 )
93 94 from IPython.utils import py3compat
94 95 from IPython.utils.path import filefind, get_ipython_dir
95 96
96 97 from .utils import url_path_join
97 98
98 99 #-----------------------------------------------------------------------------
99 100 # Module globals
100 101 #-----------------------------------------------------------------------------
101 102
102 103 _examples = """
103 104 ipython notebook # start the notebook
104 105 ipython notebook --profile=sympy # use the sympy profile
105 106 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
106 107 """
107 108
108 109 #-----------------------------------------------------------------------------
109 110 # Helper functions
110 111 #-----------------------------------------------------------------------------
111 112
112 113 def random_ports(port, n):
113 114 """Generate a list of n random ports near the given port.
114 115
115 116 The first 5 ports will be sequential, and the remaining n-5 will be
116 117 randomly selected in the range [port-2*n, port+2*n].
117 118 """
118 119 for i in range(min(5, n)):
119 120 yield port + i
120 121 for i in range(n-5):
121 122 yield max(1, port + random.randint(-2*n, 2*n))
122 123
123 124 def load_handlers(name):
124 125 """Load the (URL pattern, handler) tuples for each component."""
125 126 name = 'IPython.html.' + name
126 127 mod = __import__(name, fromlist=['default_handlers'])
127 128 return mod.default_handlers
128 129
129 130 #-----------------------------------------------------------------------------
130 131 # The Tornado web application
131 132 #-----------------------------------------------------------------------------
132 133
133 134 class NotebookWebApplication(web.Application):
134 135
135 136 def __init__(self, ipython_app, kernel_manager, notebook_manager,
136 137 cluster_manager, session_manager, log, base_project_url,
137 138 settings_overrides):
138 139
139 140 settings = self.init_settings(
140 141 ipython_app, kernel_manager, notebook_manager, cluster_manager,
141 142 session_manager, log, base_project_url, settings_overrides)
142 143 handlers = self.init_handlers(settings)
143 144
144 145 super(NotebookWebApplication, self).__init__(handlers, **settings)
145 146
146 147 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
147 148 cluster_manager, session_manager, log, base_project_url,
148 149 settings_overrides):
149 150 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
150 151 # base_project_url will always be unicode, which will in turn
151 152 # make the patterns unicode, and ultimately result in unicode
152 153 # keys in kwargs to handler._execute(**kwargs) in tornado.
153 154 # This enforces that base_project_url be ascii in that situation.
154 155 #
155 156 # Note that the URLs these patterns check against are escaped,
156 157 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
157 158 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
158 159 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
159 160 settings = dict(
160 161 # basics
161 162 log_function=log_request,
162 163 base_project_url=base_project_url,
163 164 base_kernel_url=ipython_app.base_kernel_url,
164 165 template_path=template_path,
165 166 static_path=ipython_app.static_file_path,
166 167 static_handler_class = FileFindHandler,
167 168 static_url_prefix = url_path_join(base_project_url,'/static/'),
168 169
169 170 # authentication
170 171 cookie_secret=ipython_app.cookie_secret,
171 172 login_url=url_path_join(base_project_url,'/login'),
172 173 password=ipython_app.password,
173 174
174 175 # managers
175 176 kernel_manager=kernel_manager,
176 177 notebook_manager=notebook_manager,
177 178 cluster_manager=cluster_manager,
178 179 session_manager=session_manager,
179 180
180 181 # IPython stuff
181 182 nbextensions_path = ipython_app.nbextensions_path,
182 183 mathjax_url=ipython_app.mathjax_url,
183 184 config=ipython_app.config,
184 185 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
185 186 )
186 187
187 188 # allow custom overrides for the tornado web app.
188 189 settings.update(settings_overrides)
189 190 return settings
190 191
191 192 def init_handlers(self, settings):
192 193 # Load the (URL pattern, handler) tuples for each component.
193 194 handlers = []
194 195 handlers.extend(load_handlers('base.handlers'))
195 196 handlers.extend(load_handlers('tree.handlers'))
196 197 handlers.extend(load_handlers('auth.login'))
197 198 handlers.extend(load_handlers('auth.logout'))
198 199 handlers.extend(load_handlers('notebook.handlers'))
199 200 handlers.extend(load_handlers('nbconvert.handlers'))
200 201 handlers.extend(load_handlers('services.kernels.handlers'))
201 202 handlers.extend(load_handlers('services.notebooks.handlers'))
202 203 handlers.extend(load_handlers('services.clusters.handlers'))
203 204 handlers.extend(load_handlers('services.sessions.handlers'))
204 205 handlers.extend(load_handlers('services.nbconvert.handlers'))
205 206 handlers.extend([
206 207 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
207 208 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
208 209 ])
209 210 # prepend base_project_url onto the patterns that we match
210 211 new_handlers = []
211 212 for handler in handlers:
212 213 pattern = url_path_join(settings['base_project_url'], handler[0])
213 214 new_handler = tuple([pattern] + list(handler[1:]))
214 215 new_handlers.append(new_handler)
215 216 # add 404 on the end, which will catch everything that falls through
216 217 new_handlers.append((r'(.*)', Template404))
217 218 return new_handlers
218 219
219 220
221 #-----------------------------------------------------------------------------
222 # Subcommands
223 #-----------------------------------------------------------------------------
224
220 225 class NbserverListApp(BaseIPythonApplication):
221 226
222 227 description="List currently running notebook servers in this profile."
223 228
224 229 flags = dict(
225 230 json=({'NbserverListApp': {'json': True}},
226 231 "Produce machine-readable JSON output."),
227 232 )
228 233
229 234 json = Bool(False, config=True,
230 235 help="If True, each line of output will be a JSON object with the "
231 236 "details from the server info file.")
232 237
233 238 def start(self):
234 239 if not self.json:
235 240 print("Currently running servers:")
236 241 for serverinfo in list_running_servers(self.profile):
237 242 if self.json:
238 243 print(json.dumps(serverinfo))
239 244 else:
240 245 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
241 246
247
248 class NotebookTrustApp(BaseIPythonApplication):
249
250 description="""Sign one or more IPython notebooks with your key,
251 to trust their dynamic (HTML, Javascript) output."""
252
253 examples="""ipython notebook trust mynotebook.ipynb"""
254
255 notary = Instance(sign.NotebookNotary)
256 def _notary_default(self):
257 return sign.NotebookNotary(parent=self, profile_dir=self.profile_dir)
258
259 def sign_notebook(self, notebook_path):
260 if not os.path.exists(notebook_path):
261 self.log.error("Notebook missing: %s" % notebook_path)
262 self.exit(1)
263 with io.open(notebook_path, encoding='utf8') as f:
264 nb = current.read(f, 'json')
265 sign.trust_notebook(nb, self.notary.secret, self.notary.signature_scheme)
266 with io.open(notebook_path, 'w', encoding='utf8') as f:
267 current.write(nb, f, 'json')
268
269 def start(self):
270 if not self.extra_args:
271 self.log.critical("Specify at least one notebook to sign.")
272 self.exit(1)
273
274 for notebook_path in self.extra_args:
275 print("Signing notebook: %s" % notebook_path)
276 self.sign_notebook(notebook_path)
277
242 278 #-----------------------------------------------------------------------------
243 279 # Aliases and Flags
244 280 #-----------------------------------------------------------------------------
245 281
246 282 flags = dict(kernel_flags)
247 283 flags['no-browser']=(
248 284 {'NotebookApp' : {'open_browser' : False}},
249 285 "Don't open the notebook in a browser after startup."
250 286 )
251 287 flags['no-mathjax']=(
252 288 {'NotebookApp' : {'enable_mathjax' : False}},
253 289 """Disable MathJax
254 290
255 291 MathJax is the javascript library IPython uses to render math/LaTeX. It is
256 292 very large, so you may want to disable it if you have a slow internet
257 293 connection, or for offline use of the notebook.
258 294
259 295 When disabled, equations etc. will appear as their untransformed TeX source.
260 296 """
261 297 )
262 298
263 299 # Add notebook manager flags
264 300 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
265 301 'Auto-save a .py script everytime the .ipynb notebook is saved',
266 302 'Do not auto-save .py scripts for every notebook'))
267 303
268 304 # the flags that are specific to the frontend
269 305 # these must be scrubbed before being passed to the kernel,
270 306 # or it will raise an error on unrecognized flags
271 307 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
272 308
273 309 aliases = dict(kernel_aliases)
274 310
275 311 aliases.update({
276 312 'ip': 'NotebookApp.ip',
277 313 'port': 'NotebookApp.port',
278 314 'port-retries': 'NotebookApp.port_retries',
279 315 'transport': 'KernelManager.transport',
280 316 'keyfile': 'NotebookApp.keyfile',
281 317 'certfile': 'NotebookApp.certfile',
282 318 'notebook-dir': 'NotebookManager.notebook_dir',
283 319 'browser': 'NotebookApp.browser',
284 320 })
285 321
286 322 # remove ipkernel flags that are singletons, and don't make sense in
287 323 # multi-kernel evironment:
288 324 aliases.pop('f', None)
289 325
290 326 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
291 327 u'notebook-dir', u'profile', u'profile-dir']
292 328
293 329 #-----------------------------------------------------------------------------
294 330 # NotebookApp
295 331 #-----------------------------------------------------------------------------
296 332
297 333 class NotebookApp(BaseIPythonApplication):
298 334
299 335 name = 'ipython-notebook'
300 336
301 337 description = """
302 338 The IPython HTML Notebook.
303 339
304 340 This launches a Tornado based HTML Notebook Server that serves up an
305 341 HTML5/Javascript Notebook client.
306 342 """
307 343 examples = _examples
308 344
309 345 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
310 346 FileNotebookManager]
311 347 flags = Dict(flags)
312 348 aliases = Dict(aliases)
313 349
314 350 subcommands = dict(
315 351 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
352 trust=(NotebookTrustApp, NotebookTrustApp.description),
316 353 )
317 354
318 355 kernel_argv = List(Unicode)
319 356
320 357 def _log_level_default(self):
321 358 return logging.INFO
322 359
323 360 def _log_format_default(self):
324 361 """override default log format to include time"""
325 362 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
326 363
327 364 # create requested profiles by default, if they don't exist:
328 365 auto_create = Bool(True)
329 366
330 367 # file to be opened in the notebook server
331 368 file_to_run = Unicode('')
332 369
333 370 # Network related information.
334 371
335 372 ip = Unicode(config=True,
336 373 help="The IP address the notebook server will listen on."
337 374 )
338 375 def _ip_default(self):
339 376 return localhost()
340 377
341 378 def _ip_changed(self, name, old, new):
342 379 if new == u'*': self.ip = u''
343 380
344 381 port = Integer(8888, config=True,
345 382 help="The port the notebook server will listen on."
346 383 )
347 384 port_retries = Integer(50, config=True,
348 385 help="The number of additional ports to try if the specified port is not available."
349 386 )
350 387
351 388 certfile = Unicode(u'', config=True,
352 389 help="""The full path to an SSL/TLS certificate file."""
353 390 )
354 391
355 392 keyfile = Unicode(u'', config=True,
356 393 help="""The full path to a private key file for usage with SSL/TLS."""
357 394 )
358 395
359 396 cookie_secret = Bytes(b'', config=True,
360 397 help="""The random bytes used to secure cookies.
361 398 By default this is a new random number every time you start the Notebook.
362 399 Set it to a value in a config file to enable logins to persist across server sessions.
363 400
364 401 Note: Cookie secrets should be kept private, do not share config files with
365 402 cookie_secret stored in plaintext (you can read the value from a file).
366 403 """
367 404 )
368 405 def _cookie_secret_default(self):
369 406 return os.urandom(1024)
370 407
371 408 password = Unicode(u'', config=True,
372 409 help="""Hashed password to use for web authentication.
373 410
374 411 To generate, type in a python/IPython shell:
375 412
376 413 from IPython.lib import passwd; passwd()
377 414
378 415 The string should be of the form type:salt:hashed-password.
379 416 """
380 417 )
381 418
382 419 open_browser = Bool(True, config=True,
383 420 help="""Whether to open in a browser after starting.
384 421 The specific browser used is platform dependent and
385 422 determined by the python standard library `webbrowser`
386 423 module, unless it is overridden using the --browser
387 424 (NotebookApp.browser) configuration option.
388 425 """)
389 426
390 427 browser = Unicode(u'', config=True,
391 428 help="""Specify what command to use to invoke a web
392 429 browser when opening the notebook. If not specified, the
393 430 default browser will be determined by the `webbrowser`
394 431 standard library module, which allows setting of the
395 432 BROWSER environment variable to override it.
396 433 """)
397 434
398 435 webapp_settings = Dict(config=True,
399 436 help="Supply overrides for the tornado.web.Application that the "
400 437 "IPython notebook uses.")
401 438
402 439 enable_mathjax = Bool(True, config=True,
403 440 help="""Whether to enable MathJax for typesetting math/TeX
404 441
405 442 MathJax is the javascript library IPython uses to render math/LaTeX. It is
406 443 very large, so you may want to disable it if you have a slow internet
407 444 connection, or for offline use of the notebook.
408 445
409 446 When disabled, equations etc. will appear as their untransformed TeX source.
410 447 """
411 448 )
412 449 def _enable_mathjax_changed(self, name, old, new):
413 450 """set mathjax url to empty if mathjax is disabled"""
414 451 if not new:
415 452 self.mathjax_url = u''
416 453
417 454 base_project_url = Unicode('/', config=True,
418 455 help='''The base URL for the notebook server.
419 456
420 457 Leading and trailing slashes can be omitted,
421 458 and will automatically be added.
422 459 ''')
423 460 def _base_project_url_changed(self, name, old, new):
424 461 if not new.startswith('/'):
425 462 self.base_project_url = '/'+new
426 463 elif not new.endswith('/'):
427 464 self.base_project_url = new+'/'
428 465
429 466 base_kernel_url = Unicode('/', config=True,
430 467 help='''The base URL for the kernel server
431 468
432 469 Leading and trailing slashes can be omitted,
433 470 and will automatically be added.
434 471 ''')
435 472 def _base_kernel_url_changed(self, name, old, new):
436 473 if not new.startswith('/'):
437 474 self.base_kernel_url = '/'+new
438 475 elif not new.endswith('/'):
439 476 self.base_kernel_url = new+'/'
440 477
441 478 websocket_url = Unicode("", config=True,
442 479 help="""The base URL for the websocket server,
443 480 if it differs from the HTTP server (hint: it almost certainly doesn't).
444 481
445 482 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
446 483 """
447 484 )
448 485
449 486 extra_static_paths = List(Unicode, config=True,
450 487 help="""Extra paths to search for serving static files.
451 488
452 489 This allows adding javascript/css to be available from the notebook server machine,
453 490 or overriding individual files in the IPython"""
454 491 )
455 492 def _extra_static_paths_default(self):
456 493 return [os.path.join(self.profile_dir.location, 'static')]
457 494
458 495 @property
459 496 def static_file_path(self):
460 497 """return extra paths + the default location"""
461 498 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
462 499
463 500 nbextensions_path = List(Unicode, config=True,
464 501 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
465 502 )
466 503 def _nbextensions_path_default(self):
467 504 return [os.path.join(get_ipython_dir(), 'nbextensions')]
468 505
469 506 mathjax_url = Unicode("", config=True,
470 507 help="""The url for MathJax.js."""
471 508 )
472 509 def _mathjax_url_default(self):
473 510 if not self.enable_mathjax:
474 511 return u''
475 512 static_url_prefix = self.webapp_settings.get("static_url_prefix",
476 513 url_path_join(self.base_project_url, "static")
477 514 )
478 515
479 516 # try local mathjax, either in nbextensions/mathjax or static/mathjax
480 517 for (url_prefix, search_path) in [
481 518 (url_path_join(self.base_project_url, "nbextensions"), self.nbextensions_path),
482 519 (static_url_prefix, self.static_file_path),
483 520 ]:
484 521 self.log.debug("searching for local mathjax in %s", search_path)
485 522 try:
486 523 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
487 524 except IOError:
488 525 continue
489 526 else:
490 527 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
491 528 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
492 529 return url
493 530
494 531 # no local mathjax, serve from CDN
495 532 if self.certfile:
496 533 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
497 534 host = u"https://c328740.ssl.cf1.rackcdn.com"
498 535 else:
499 536 host = u"http://cdn.mathjax.org"
500 537
501 538 url = host + u"/mathjax/latest/MathJax.js"
502 539 self.log.info("Using MathJax from CDN: %s", url)
503 540 return url
504 541
505 542 def _mathjax_url_changed(self, name, old, new):
506 543 if new and not self.enable_mathjax:
507 544 # enable_mathjax=False overrides mathjax_url
508 545 self.mathjax_url = u''
509 546 else:
510 547 self.log.info("Using MathJax: %s", new)
511 548
512 549 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
513 550 config=True,
514 551 help='The notebook manager class to use.')
515 552
516 553 trust_xheaders = Bool(False, config=True,
517 554 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
518 555 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
519 556 )
520 557
521 558 info_file = Unicode()
522 559
523 560 def _info_file_default(self):
524 561 info_file = "nbserver-%s.json"%os.getpid()
525 562 return os.path.join(self.profile_dir.security_dir, info_file)
526 563
527 564 def parse_command_line(self, argv=None):
528 565 super(NotebookApp, self).parse_command_line(argv)
529 566
530 567 if self.extra_args:
531 568 arg0 = self.extra_args[0]
532 569 f = os.path.abspath(arg0)
533 570 self.argv.remove(arg0)
534 571 if not os.path.exists(f):
535 572 self.log.critical("No such file or directory: %s", f)
536 573 self.exit(1)
537 574 if os.path.isdir(f):
538 575 self.config.FileNotebookManager.notebook_dir = f
539 576 elif os.path.isfile(f):
540 577 self.file_to_run = f
541 578
542 579 def init_kernel_argv(self):
543 580 """construct the kernel arguments"""
544 581 # Scrub frontend-specific flags
545 582 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
546 583 if any(arg.startswith(u'--pylab') for arg in self.kernel_argv):
547 584 self.log.warn('\n '.join([
548 585 "Starting all kernels in pylab mode is not recommended,",
549 586 "and will be disabled in a future release.",
550 587 "Please use the %matplotlib magic to enable matplotlib instead.",
551 588 "pylab implies many imports, which can have confusing side effects",
552 589 "and harm the reproducibility of your notebooks.",
553 590 ]))
554 591 # Kernel should inherit default config file from frontend
555 592 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
556 593 # Kernel should get *absolute* path to profile directory
557 594 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
558 595
559 596 def init_configurables(self):
560 597 # force Session default to be secure
561 598 default_secure(self.config)
562 599 self.kernel_manager = MappingKernelManager(
563 600 parent=self, log=self.log, kernel_argv=self.kernel_argv,
564 601 connection_dir = self.profile_dir.security_dir,
565 602 )
566 603 kls = import_item(self.notebook_manager_class)
567 604 self.notebook_manager = kls(parent=self, log=self.log)
568 605 self.session_manager = SessionManager(parent=self, log=self.log)
569 606 self.cluster_manager = ClusterManager(parent=self, log=self.log)
570 607 self.cluster_manager.update_profiles()
571 608
572 609 def init_logging(self):
573 610 # This prevents double log messages because tornado use a root logger that
574 611 # self.log is a child of. The logging module dipatches log messages to a log
575 612 # and all of its ancenstors until propagate is set to False.
576 613 self.log.propagate = False
577 614
578 615 # hook up tornado 3's loggers to our app handlers
579 616 for name in ('access', 'application', 'general'):
580 617 logger = logging.getLogger('tornado.%s' % name)
581 618 logger.parent = self.log
582 619 logger.setLevel(self.log.level)
583 620
584 621 def init_webapp(self):
585 622 """initialize tornado webapp and httpserver"""
586 623 self.web_app = NotebookWebApplication(
587 624 self, self.kernel_manager, self.notebook_manager,
588 625 self.cluster_manager, self.session_manager,
589 626 self.log, self.base_project_url, self.webapp_settings
590 627 )
591 628 if self.certfile:
592 629 ssl_options = dict(certfile=self.certfile)
593 630 if self.keyfile:
594 631 ssl_options['keyfile'] = self.keyfile
595 632 else:
596 633 ssl_options = None
597 634 self.web_app.password = self.password
598 635 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
599 636 xheaders=self.trust_xheaders)
600 637 if not self.ip:
601 638 warning = "WARNING: The notebook server is listening on all IP addresses"
602 639 if ssl_options is None:
603 640 self.log.critical(warning + " and not using encryption. This "
604 641 "is not recommended.")
605 642 if not self.password:
606 643 self.log.critical(warning + " and not using authentication. "
607 644 "This is highly insecure and not recommended.")
608 645 success = None
609 646 for port in random_ports(self.port, self.port_retries+1):
610 647 try:
611 648 self.http_server.listen(port, self.ip)
612 649 except socket.error as e:
613 650 if e.errno == errno.EADDRINUSE:
614 651 self.log.info('The port %i is already in use, trying another random port.' % port)
615 652 continue
616 653 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
617 654 self.log.warn("Permission to listen on port %i denied" % port)
618 655 continue
619 656 else:
620 657 raise
621 658 else:
622 659 self.port = port
623 660 success = True
624 661 break
625 662 if not success:
626 663 self.log.critical('ERROR: the notebook server could not be started because '
627 664 'no available port could be found.')
628 665 self.exit(1)
629 666
630 667 @property
631 668 def display_url(self):
632 669 ip = self.ip if self.ip else '[all ip addresses on your system]'
633 670 return self._url(ip)
634 671
635 672 @property
636 673 def connection_url(self):
637 674 ip = self.ip if self.ip else localhost()
638 675 return self._url(ip)
639 676
640 677 def _url(self, ip):
641 678 proto = 'https' if self.certfile else 'http'
642 679 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_project_url)
643 680
644 681 def init_signal(self):
645 682 if not sys.platform.startswith('win'):
646 683 signal.signal(signal.SIGINT, self._handle_sigint)
647 684 signal.signal(signal.SIGTERM, self._signal_stop)
648 685 if hasattr(signal, 'SIGUSR1'):
649 686 # Windows doesn't support SIGUSR1
650 687 signal.signal(signal.SIGUSR1, self._signal_info)
651 688 if hasattr(signal, 'SIGINFO'):
652 689 # only on BSD-based systems
653 690 signal.signal(signal.SIGINFO, self._signal_info)
654 691
655 692 def _handle_sigint(self, sig, frame):
656 693 """SIGINT handler spawns confirmation dialog"""
657 694 # register more forceful signal handler for ^C^C case
658 695 signal.signal(signal.SIGINT, self._signal_stop)
659 696 # request confirmation dialog in bg thread, to avoid
660 697 # blocking the App
661 698 thread = threading.Thread(target=self._confirm_exit)
662 699 thread.daemon = True
663 700 thread.start()
664 701
665 702 def _restore_sigint_handler(self):
666 703 """callback for restoring original SIGINT handler"""
667 704 signal.signal(signal.SIGINT, self._handle_sigint)
668 705
669 706 def _confirm_exit(self):
670 707 """confirm shutdown on ^C
671 708
672 709 A second ^C, or answering 'y' within 5s will cause shutdown,
673 710 otherwise original SIGINT handler will be restored.
674 711
675 712 This doesn't work on Windows.
676 713 """
677 714 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
678 715 time.sleep(0.1)
679 716 info = self.log.info
680 717 info('interrupted')
681 718 print(self.notebook_info())
682 719 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
683 720 sys.stdout.flush()
684 721 r,w,x = select.select([sys.stdin], [], [], 5)
685 722 if r:
686 723 line = sys.stdin.readline()
687 724 if line.lower().startswith('y'):
688 725 self.log.critical("Shutdown confirmed")
689 726 ioloop.IOLoop.instance().stop()
690 727 return
691 728 else:
692 729 print("No answer for 5s:", end=' ')
693 730 print("resuming operation...")
694 731 # no answer, or answer is no:
695 732 # set it back to original SIGINT handler
696 733 # use IOLoop.add_callback because signal.signal must be called
697 734 # from main thread
698 735 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
699 736
700 737 def _signal_stop(self, sig, frame):
701 738 self.log.critical("received signal %s, stopping", sig)
702 739 ioloop.IOLoop.instance().stop()
703 740
704 741 def _signal_info(self, sig, frame):
705 742 print(self.notebook_info())
706 743
707 744 def init_components(self):
708 745 """Check the components submodule, and warn if it's unclean"""
709 746 status = submodule.check_submodule_status()
710 747 if status == 'missing':
711 748 self.log.warn("components submodule missing, running `git submodule update`")
712 749 submodule.update_submodules(submodule.ipython_parent())
713 750 elif status == 'unclean':
714 751 self.log.warn("components submodule unclean, you may see 404s on static/components")
715 752 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
716 753
717 754 @catch_config_error
718 755 def initialize(self, argv=None):
719 756 super(NotebookApp, self).initialize(argv)
720 757 self.init_logging()
721 758 self.init_kernel_argv()
722 759 self.init_configurables()
723 760 self.init_components()
724 761 self.init_webapp()
725 762 self.init_signal()
726 763
727 764 def cleanup_kernels(self):
728 765 """Shutdown all kernels.
729 766
730 767 The kernels will shutdown themselves when this process no longer exists,
731 768 but explicit shutdown allows the KernelManagers to cleanup the connection files.
732 769 """
733 770 self.log.info('Shutting down kernels')
734 771 self.kernel_manager.shutdown_all()
735 772
736 773 def notebook_info(self):
737 774 "Return the current working directory and the server url information"
738 775 info = self.notebook_manager.info_string() + "\n"
739 776 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
740 777 return info + "The IPython Notebook is running at: %s" % self.display_url
741 778
742 779 def server_info(self):
743 780 """Return a JSONable dict of information about this server."""
744 781 return {'url': self.connection_url,
745 782 'hostname': self.ip if self.ip else 'localhost',
746 783 'port': self.port,
747 784 'secure': bool(self.certfile),
748 785 'base_project_url': self.base_project_url,
749 786 'notebook_dir': os.path.abspath(self.notebook_manager.notebook_dir),
750 787 }
751 788
752 789 def write_server_info_file(self):
753 790 """Write the result of server_info() to the JSON file info_file."""
754 791 with open(self.info_file, 'w') as f:
755 792 json.dump(self.server_info(), f, indent=2)
756 793
757 794 def remove_server_info_file(self):
758 795 """Remove the nbserver-<pid>.json file created for this server.
759 796
760 797 Ignores the error raised when the file has already been removed.
761 798 """
762 799 try:
763 800 os.unlink(self.info_file)
764 801 except OSError as e:
765 802 if e.errno != errno.ENOENT:
766 803 raise
767 804
768 805 def start(self):
769 806 """ Start the IPython Notebook server app, after initialization
770 807
771 808 This method takes no arguments so all configuration and initialization
772 809 must be done prior to calling this method."""
773 810 if self.subapp is not None:
774 811 return self.subapp.start()
775 812
776 813 info = self.log.info
777 814 for line in self.notebook_info().split("\n"):
778 815 info(line)
779 816 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
780 817
781 818 self.write_server_info_file()
782 819
783 820 if self.open_browser or self.file_to_run:
784 821 try:
785 822 browser = webbrowser.get(self.browser or None)
786 823 except webbrowser.Error as e:
787 824 self.log.warn('No web browser found: %s.' % e)
788 825 browser = None
789 826
790 827 f = self.file_to_run
791 828 if f:
792 829 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
793 830 if f.startswith(nbdir):
794 831 f = f[len(nbdir):]
795 832 else:
796 833 self.log.warn(
797 834 "Probably won't be able to open notebook %s "
798 835 "because it is not in notebook_dir %s",
799 836 f, nbdir,
800 837 )
801 838
802 839 if os.path.isfile(self.file_to_run):
803 840 url = url_path_join('notebooks', f)
804 841 else:
805 842 url = url_path_join('tree', f)
806 843 if browser:
807 844 b = lambda : browser.open("%s%s" % (self.connection_url, url),
808 845 new=2)
809 846 threading.Thread(target=b).start()
810 847 try:
811 848 ioloop.IOLoop.instance().start()
812 849 except KeyboardInterrupt:
813 850 info("Interrupted...")
814 851 finally:
815 852 self.cleanup_kernels()
816 853 self.remove_server_info_file()
817 854
818 855
819 856 def list_running_servers(profile='default'):
820 857 """Iterate over the server info files of running notebook servers.
821 858
822 859 Given a profile name, find nbserver-* files in the security directory of
823 860 that profile, and yield dicts of their information, each one pertaining to
824 861 a currently running notebook server instance.
825 862 """
826 863 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
827 864 for file in os.listdir(pd.security_dir):
828 865 if file.startswith('nbserver-'):
829 866 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
830 867 yield json.load(f)
831 868
832 869 #-----------------------------------------------------------------------------
833 870 # Main entry point
834 871 #-----------------------------------------------------------------------------
835 872
836 873 launch_new_instance = NotebookApp.launch_instance
837 874
General Comments 0
You need to be logged in to leave comments. Login now