##// END OF EJS Templates
Merge pull request #6624...
MinRK -
r18366:7ad97d20 merge
parent child Browse files
Show More
1 NO CONTENT: new file 100644
@@ -0,0 +1,48 b''
1 """Serve files directly from the ContentsManager."""
2
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
5
6 import os
7 import mimetypes
8 import json
9 import base64
10
11 from tornado import web
12
13 from IPython.html.base.handlers import IPythonHandler
14
15 class FilesHandler(IPythonHandler):
16 """serve files via ContentsManager"""
17
18 @web.authenticated
19 def get(self, path):
20 cm = self.settings['contents_manager']
21 if cm.is_hidden(path):
22 self.log.info("Refusing to serve hidden file, via 404 Error")
23 raise web.HTTPError(404)
24
25 path, name = os.path.split(path)
26 model = cm.get_model(name, path)
27
28 if model['type'] == 'notebook':
29 self.set_header('Content-Type', 'application/json')
30 else:
31 cur_mime = mimetypes.guess_type(name)[0]
32 if cur_mime is not None:
33 self.set_header('Content-Type', cur_mime)
34
35 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
36
37 if model['format'] == 'base64':
38 b64_bytes = model['content'].encode('ascii')
39 self.write(base64.decodestring(b64_bytes))
40 elif model['format'] == 'json':
41 self.write(json.dumps(model['content']))
42 else:
43 self.write(model['content'])
44 self.flush()
45
46 default_handlers = [
47 (r"/files/(.*)", FilesHandler),
48 ] No newline at end of file
@@ -1,963 +1,958 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 from __future__ import print_function
8 8
9 9 import base64
10 10 import errno
11 11 import io
12 12 import json
13 13 import logging
14 14 import os
15 15 import random
16 16 import re
17 17 import select
18 18 import signal
19 19 import socket
20 20 import sys
21 21 import threading
22 22 import time
23 23 import webbrowser
24 24
25 25
26 26 # check for pyzmq 2.1.11
27 27 from IPython.utils.zmqrelated import check_for_zmq
28 28 check_for_zmq('2.1.11', 'IPython.html')
29 29
30 30 from jinja2 import Environment, FileSystemLoader
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 ioloop.install()
36 36
37 37 # check for tornado 3.1.0
38 38 msg = "The IPython Notebook requires tornado >= 3.1.0"
39 39 try:
40 40 import tornado
41 41 except ImportError:
42 42 raise ImportError(msg)
43 43 try:
44 44 version_info = tornado.version_info
45 45 except AttributeError:
46 46 raise ImportError(msg + ", but you have < 1.1.0")
47 47 if version_info < (3,1,0):
48 48 raise ImportError(msg + ", but you have %s" % tornado.version)
49 49
50 50 from tornado import httpserver
51 51 from tornado import web
52 52 from tornado.log import LogFormatter, app_log, access_log, gen_log
53 53
54 54 from IPython.html import DEFAULT_STATIC_FILES_PATH
55 55 from .base.handlers import Template404
56 56 from .log import log_request
57 57 from .services.kernels.kernelmanager import MappingKernelManager
58 58 from .services.contents.manager import ContentsManager
59 59 from .services.contents.filemanager import FileContentsManager
60 60 from .services.clusters.clustermanager import ClusterManager
61 61 from .services.sessions.sessionmanager import SessionManager
62 62
63 63 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
64 64
65 65 from IPython.config import Config
66 66 from IPython.config.application import catch_config_error, boolean_flag
67 67 from IPython.core.application import (
68 68 BaseIPythonApplication, base_flags, base_aliases,
69 69 )
70 70 from IPython.core.profiledir import ProfileDir
71 71 from IPython.kernel import KernelManager
72 72 from IPython.kernel.kernelspec import KernelSpecManager
73 73 from IPython.kernel.zmq.session import default_secure, Session
74 74 from IPython.nbformat.sign import NotebookNotary
75 75 from IPython.utils.importstring import import_item
76 76 from IPython.utils import submodule
77 77 from IPython.utils.process import check_pid
78 78 from IPython.utils.traitlets import (
79 79 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
80 80 DottedObjectName, TraitError,
81 81 )
82 82 from IPython.utils import py3compat
83 83 from IPython.utils.path import filefind, get_ipython_dir
84 84
85 85 from .utils import url_path_join
86 86
87 87 #-----------------------------------------------------------------------------
88 88 # Module globals
89 89 #-----------------------------------------------------------------------------
90 90
91 91 _examples = """
92 92 ipython notebook # start the notebook
93 93 ipython notebook --profile=sympy # use the sympy profile
94 94 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
95 95 """
96 96
97 97 #-----------------------------------------------------------------------------
98 98 # Helper functions
99 99 #-----------------------------------------------------------------------------
100 100
101 101 def random_ports(port, n):
102 102 """Generate a list of n random ports near the given port.
103 103
104 104 The first 5 ports will be sequential, and the remaining n-5 will be
105 105 randomly selected in the range [port-2*n, port+2*n].
106 106 """
107 107 for i in range(min(5, n)):
108 108 yield port + i
109 109 for i in range(n-5):
110 110 yield max(1, port + random.randint(-2*n, 2*n))
111 111
112 112 def load_handlers(name):
113 113 """Load the (URL pattern, handler) tuples for each component."""
114 114 name = 'IPython.html.' + name
115 115 mod = __import__(name, fromlist=['default_handlers'])
116 116 return mod.default_handlers
117 117
118 118 #-----------------------------------------------------------------------------
119 119 # The Tornado web application
120 120 #-----------------------------------------------------------------------------
121 121
122 122 class NotebookWebApplication(web.Application):
123 123
124 124 def __init__(self, ipython_app, kernel_manager, contents_manager,
125 125 cluster_manager, session_manager, kernel_spec_manager, log,
126 126 base_url, default_url, settings_overrides, jinja_env_options):
127 127
128 128 settings = self.init_settings(
129 129 ipython_app, kernel_manager, contents_manager, cluster_manager,
130 130 session_manager, kernel_spec_manager, log, base_url, default_url,
131 131 settings_overrides, jinja_env_options)
132 132 handlers = self.init_handlers(settings)
133 133
134 134 super(NotebookWebApplication, self).__init__(handlers, **settings)
135 135
136 136 def init_settings(self, ipython_app, kernel_manager, contents_manager,
137 137 cluster_manager, session_manager, kernel_spec_manager,
138 138 log, base_url, default_url, settings_overrides,
139 139 jinja_env_options=None):
140 140
141 141 _template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
142 142 if isinstance(_template_path, str):
143 143 _template_path = (_template_path,)
144 144 template_path = [os.path.expanduser(path) for path in _template_path]
145 145
146 146 jenv_opt = jinja_env_options if jinja_env_options else {}
147 147 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
148 148 settings = dict(
149 149 # basics
150 150 log_function=log_request,
151 151 base_url=base_url,
152 152 default_url=default_url,
153 153 template_path=template_path,
154 154 static_path=ipython_app.static_file_path,
155 155 static_handler_class = FileFindHandler,
156 156 static_url_prefix = url_path_join(base_url,'/static/'),
157 157
158 158 # authentication
159 159 cookie_secret=ipython_app.cookie_secret,
160 160 login_url=url_path_join(base_url,'/login'),
161 161 password=ipython_app.password,
162 162
163 163 # managers
164 164 kernel_manager=kernel_manager,
165 165 contents_manager=contents_manager,
166 166 cluster_manager=cluster_manager,
167 167 session_manager=session_manager,
168 168 kernel_spec_manager=kernel_spec_manager,
169 169
170 170 # IPython stuff
171 171 nbextensions_path = ipython_app.nbextensions_path,
172 172 websocket_url=ipython_app.websocket_url,
173 173 mathjax_url=ipython_app.mathjax_url,
174 174 config=ipython_app.config,
175 175 jinja2_env=env,
176 176 )
177 177
178 178 # allow custom overrides for the tornado web app.
179 179 settings.update(settings_overrides)
180 180 return settings
181 181
182 182 def init_handlers(self, settings):
183 183 # Load the (URL pattern, handler) tuples for each component.
184 184 handlers = []
185 185 handlers.extend(load_handlers('base.handlers'))
186 186 handlers.extend(load_handlers('tree.handlers'))
187 187 handlers.extend(load_handlers('auth.login'))
188 188 handlers.extend(load_handlers('auth.logout'))
189 handlers.extend(load_handlers('files.handlers'))
189 190 handlers.extend(load_handlers('notebook.handlers'))
190 191 handlers.extend(load_handlers('nbconvert.handlers'))
191 192 handlers.extend(load_handlers('kernelspecs.handlers'))
192 193 handlers.extend(load_handlers('services.kernels.handlers'))
193 194 handlers.extend(load_handlers('services.contents.handlers'))
194 195 handlers.extend(load_handlers('services.clusters.handlers'))
195 196 handlers.extend(load_handlers('services.sessions.handlers'))
196 197 handlers.extend(load_handlers('services.nbconvert.handlers'))
197 198 handlers.extend(load_handlers('services.kernelspecs.handlers'))
198 # FIXME: /files/ should be handled by the Contents service when it exists
199 cm = settings['contents_manager']
200 if hasattr(cm, 'root_dir'):
201 handlers.append(
202 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : cm.root_dir}),
203 )
204 199 handlers.append(
205 200 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
206 201 )
207 202 # set the URL that will be redirected from `/`
208 203 handlers.append(
209 204 (r'/?', web.RedirectHandler, {
210 205 'url' : url_path_join(settings['base_url'], settings['default_url']),
211 206 'permanent': False, # want 302, not 301
212 207 })
213 208 )
214 209 # prepend base_url onto the patterns that we match
215 210 new_handlers = []
216 211 for handler in handlers:
217 212 pattern = url_path_join(settings['base_url'], handler[0])
218 213 new_handler = tuple([pattern] + list(handler[1:]))
219 214 new_handlers.append(new_handler)
220 215 # add 404 on the end, which will catch everything that falls through
221 216 new_handlers.append((r'(.*)', Template404))
222 217 return new_handlers
223 218
224 219
225 220 class NbserverListApp(BaseIPythonApplication):
226 221
227 222 description="List currently running notebook servers in this profile."
228 223
229 224 flags = dict(
230 225 json=({'NbserverListApp': {'json': True}},
231 226 "Produce machine-readable JSON output."),
232 227 )
233 228
234 229 json = Bool(False, config=True,
235 230 help="If True, each line of output will be a JSON object with the "
236 231 "details from the server info file.")
237 232
238 233 def start(self):
239 234 if not self.json:
240 235 print("Currently running servers:")
241 236 for serverinfo in list_running_servers(self.profile):
242 237 if self.json:
243 238 print(json.dumps(serverinfo))
244 239 else:
245 240 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
246 241
247 242 #-----------------------------------------------------------------------------
248 243 # Aliases and Flags
249 244 #-----------------------------------------------------------------------------
250 245
251 246 flags = dict(base_flags)
252 247 flags['no-browser']=(
253 248 {'NotebookApp' : {'open_browser' : False}},
254 249 "Don't open the notebook in a browser after startup."
255 250 )
256 251 flags['pylab']=(
257 252 {'NotebookApp' : {'pylab' : 'warn'}},
258 253 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
259 254 )
260 255 flags['no-mathjax']=(
261 256 {'NotebookApp' : {'enable_mathjax' : False}},
262 257 """Disable MathJax
263 258
264 259 MathJax is the javascript library IPython uses to render math/LaTeX. It is
265 260 very large, so you may want to disable it if you have a slow internet
266 261 connection, or for offline use of the notebook.
267 262
268 263 When disabled, equations etc. will appear as their untransformed TeX source.
269 264 """
270 265 )
271 266
272 267 # Add notebook manager flags
273 268 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
274 269 'DEPRECATED, IGNORED',
275 270 'DEPRECATED, IGNORED'))
276 271
277 272 aliases = dict(base_aliases)
278 273
279 274 aliases.update({
280 275 'ip': 'NotebookApp.ip',
281 276 'port': 'NotebookApp.port',
282 277 'port-retries': 'NotebookApp.port_retries',
283 278 'transport': 'KernelManager.transport',
284 279 'keyfile': 'NotebookApp.keyfile',
285 280 'certfile': 'NotebookApp.certfile',
286 281 'notebook-dir': 'NotebookApp.notebook_dir',
287 282 'browser': 'NotebookApp.browser',
288 283 'pylab': 'NotebookApp.pylab',
289 284 })
290 285
291 286 #-----------------------------------------------------------------------------
292 287 # NotebookApp
293 288 #-----------------------------------------------------------------------------
294 289
295 290 class NotebookApp(BaseIPythonApplication):
296 291
297 292 name = 'ipython-notebook'
298 293
299 294 description = """
300 295 The IPython HTML Notebook.
301 296
302 297 This launches a Tornado based HTML Notebook Server that serves up an
303 298 HTML5/Javascript Notebook client.
304 299 """
305 300 examples = _examples
306 301 aliases = aliases
307 302 flags = flags
308 303
309 304 classes = [
310 305 KernelManager, ProfileDir, Session, MappingKernelManager,
311 306 ContentsManager, FileContentsManager, NotebookNotary,
312 307 ]
313 308 flags = Dict(flags)
314 309 aliases = Dict(aliases)
315 310
316 311 subcommands = dict(
317 312 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
318 313 )
319 314
320 315 kernel_argv = List(Unicode)
321 316
322 317 _log_formatter_cls = LogFormatter
323 318
324 319 def _log_level_default(self):
325 320 return logging.INFO
326 321
327 322 def _log_datefmt_default(self):
328 323 """Exclude date from default date format"""
329 324 return "%H:%M:%S"
330 325
331 326 def _log_format_default(self):
332 327 """override default log format to include time"""
333 328 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
334 329
335 330 # create requested profiles by default, if they don't exist:
336 331 auto_create = Bool(True)
337 332
338 333 # file to be opened in the notebook server
339 334 file_to_run = Unicode('', config=True)
340 335 def _file_to_run_changed(self, name, old, new):
341 336 path, base = os.path.split(new)
342 337 if path:
343 338 self.file_to_run = base
344 339 self.notebook_dir = path
345 340
346 341 # Network related information
347 342
348 343 allow_origin = Unicode('', config=True,
349 344 help="""Set the Access-Control-Allow-Origin header
350 345
351 346 Use '*' to allow any origin to access your server.
352 347
353 348 Takes precedence over allow_origin_pat.
354 349 """
355 350 )
356 351
357 352 allow_origin_pat = Unicode('', config=True,
358 353 help="""Use a regular expression for the Access-Control-Allow-Origin header
359 354
360 355 Requests from an origin matching the expression will get replies with:
361 356
362 357 Access-Control-Allow-Origin: origin
363 358
364 359 where `origin` is the origin of the request.
365 360
366 361 Ignored if allow_origin is set.
367 362 """
368 363 )
369 364
370 365 allow_credentials = Bool(False, config=True,
371 366 help="Set the Access-Control-Allow-Credentials: true header"
372 367 )
373 368
374 369 default_url = Unicode('/tree', config=True,
375 370 help="The default URL to redirect to from `/`"
376 371 )
377 372
378 373 ip = Unicode('localhost', config=True,
379 374 help="The IP address the notebook server will listen on."
380 375 )
381 376
382 377 def _ip_changed(self, name, old, new):
383 378 if new == u'*': self.ip = u''
384 379
385 380 port = Integer(8888, config=True,
386 381 help="The port the notebook server will listen on."
387 382 )
388 383 port_retries = Integer(50, config=True,
389 384 help="The number of additional ports to try if the specified port is not available."
390 385 )
391 386
392 387 certfile = Unicode(u'', config=True,
393 388 help="""The full path to an SSL/TLS certificate file."""
394 389 )
395 390
396 391 keyfile = Unicode(u'', config=True,
397 392 help="""The full path to a private key file for usage with SSL/TLS."""
398 393 )
399 394
400 395 cookie_secret_file = Unicode(config=True,
401 396 help="""The file where the cookie secret is stored."""
402 397 )
403 398 def _cookie_secret_file_default(self):
404 399 if self.profile_dir is None:
405 400 return ''
406 401 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
407 402
408 403 cookie_secret = Bytes(b'', config=True,
409 404 help="""The random bytes used to secure cookies.
410 405 By default this is a new random number every time you start the Notebook.
411 406 Set it to a value in a config file to enable logins to persist across server sessions.
412 407
413 408 Note: Cookie secrets should be kept private, do not share config files with
414 409 cookie_secret stored in plaintext (you can read the value from a file).
415 410 """
416 411 )
417 412 def _cookie_secret_default(self):
418 413 if os.path.exists(self.cookie_secret_file):
419 414 with io.open(self.cookie_secret_file, 'rb') as f:
420 415 return f.read()
421 416 else:
422 417 secret = base64.encodestring(os.urandom(1024))
423 418 self._write_cookie_secret_file(secret)
424 419 return secret
425 420
426 421 def _write_cookie_secret_file(self, secret):
427 422 """write my secret to my secret_file"""
428 423 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
429 424 with io.open(self.cookie_secret_file, 'wb') as f:
430 425 f.write(secret)
431 426 try:
432 427 os.chmod(self.cookie_secret_file, 0o600)
433 428 except OSError:
434 429 self.log.warn(
435 430 "Could not set permissions on %s",
436 431 self.cookie_secret_file
437 432 )
438 433
439 434 password = Unicode(u'', config=True,
440 435 help="""Hashed password to use for web authentication.
441 436
442 437 To generate, type in a python/IPython shell:
443 438
444 439 from IPython.lib import passwd; passwd()
445 440
446 441 The string should be of the form type:salt:hashed-password.
447 442 """
448 443 )
449 444
450 445 open_browser = Bool(True, config=True,
451 446 help="""Whether to open in a browser after starting.
452 447 The specific browser used is platform dependent and
453 448 determined by the python standard library `webbrowser`
454 449 module, unless it is overridden using the --browser
455 450 (NotebookApp.browser) configuration option.
456 451 """)
457 452
458 453 browser = Unicode(u'', config=True,
459 454 help="""Specify what command to use to invoke a web
460 455 browser when opening the notebook. If not specified, the
461 456 default browser will be determined by the `webbrowser`
462 457 standard library module, which allows setting of the
463 458 BROWSER environment variable to override it.
464 459 """)
465 460
466 461 webapp_settings = Dict(config=True,
467 462 help="DEPRECATED, use tornado_settings"
468 463 )
469 464 def _webapp_settings_changed(self, name, old, new):
470 465 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
471 466 self.tornado_settings = new
472 467
473 468 tornado_settings = Dict(config=True,
474 469 help="Supply overrides for the tornado.web.Application that the "
475 470 "IPython notebook uses.")
476 471
477 472 jinja_environment_options = Dict(config=True,
478 473 help="Supply extra arguments that will be passed to Jinja environment.")
479 474
480 475
481 476 enable_mathjax = Bool(True, config=True,
482 477 help="""Whether to enable MathJax for typesetting math/TeX
483 478
484 479 MathJax is the javascript library IPython uses to render math/LaTeX. It is
485 480 very large, so you may want to disable it if you have a slow internet
486 481 connection, or for offline use of the notebook.
487 482
488 483 When disabled, equations etc. will appear as their untransformed TeX source.
489 484 """
490 485 )
491 486 def _enable_mathjax_changed(self, name, old, new):
492 487 """set mathjax url to empty if mathjax is disabled"""
493 488 if not new:
494 489 self.mathjax_url = u''
495 490
496 491 base_url = Unicode('/', config=True,
497 492 help='''The base URL for the notebook server.
498 493
499 494 Leading and trailing slashes can be omitted,
500 495 and will automatically be added.
501 496 ''')
502 497 def _base_url_changed(self, name, old, new):
503 498 if not new.startswith('/'):
504 499 self.base_url = '/'+new
505 500 elif not new.endswith('/'):
506 501 self.base_url = new+'/'
507 502
508 503 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
509 504 def _base_project_url_changed(self, name, old, new):
510 505 self.log.warn("base_project_url is deprecated, use base_url")
511 506 self.base_url = new
512 507
513 508 extra_static_paths = List(Unicode, config=True,
514 509 help="""Extra paths to search for serving static files.
515 510
516 511 This allows adding javascript/css to be available from the notebook server machine,
517 512 or overriding individual files in the IPython"""
518 513 )
519 514 def _extra_static_paths_default(self):
520 515 return [os.path.join(self.profile_dir.location, 'static')]
521 516
522 517 @property
523 518 def static_file_path(self):
524 519 """return extra paths + the default location"""
525 520 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
526 521
527 522 nbextensions_path = List(Unicode, config=True,
528 523 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
529 524 )
530 525 def _nbextensions_path_default(self):
531 526 return [os.path.join(get_ipython_dir(), 'nbextensions')]
532 527
533 528 websocket_url = Unicode("", config=True,
534 529 help="""The base URL for websockets,
535 530 if it differs from the HTTP server (hint: it almost certainly doesn't).
536 531
537 532 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
538 533 """
539 534 )
540 535 mathjax_url = Unicode("", config=True,
541 536 help="""The url for MathJax.js."""
542 537 )
543 538 def _mathjax_url_default(self):
544 539 if not self.enable_mathjax:
545 540 return u''
546 541 static_url_prefix = self.tornado_settings.get("static_url_prefix",
547 542 url_path_join(self.base_url, "static")
548 543 )
549 544
550 545 # try local mathjax, either in nbextensions/mathjax or static/mathjax
551 546 for (url_prefix, search_path) in [
552 547 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
553 548 (static_url_prefix, self.static_file_path),
554 549 ]:
555 550 self.log.debug("searching for local mathjax in %s", search_path)
556 551 try:
557 552 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
558 553 except IOError:
559 554 continue
560 555 else:
561 556 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
562 557 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
563 558 return url
564 559
565 560 # no local mathjax, serve from CDN
566 561 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
567 562 self.log.info("Using MathJax from CDN: %s", url)
568 563 return url
569 564
570 565 def _mathjax_url_changed(self, name, old, new):
571 566 if new and not self.enable_mathjax:
572 567 # enable_mathjax=False overrides mathjax_url
573 568 self.mathjax_url = u''
574 569 else:
575 570 self.log.info("Using MathJax: %s", new)
576 571
577 572 contents_manager_class = DottedObjectName('IPython.html.services.contents.filemanager.FileContentsManager',
578 573 config=True,
579 574 help='The notebook manager class to use.'
580 575 )
581 576 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
582 577 config=True,
583 578 help='The kernel manager class to use.'
584 579 )
585 580 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
586 581 config=True,
587 582 help='The session manager class to use.'
588 583 )
589 584 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
590 585 config=True,
591 586 help='The cluster manager class to use.'
592 587 )
593 588
594 589 kernel_spec_manager = Instance(KernelSpecManager)
595 590
596 591 def _kernel_spec_manager_default(self):
597 592 return KernelSpecManager(ipython_dir=self.ipython_dir)
598 593
599 594 trust_xheaders = Bool(False, config=True,
600 595 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
601 596 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
602 597 )
603 598
604 599 info_file = Unicode()
605 600
606 601 def _info_file_default(self):
607 602 info_file = "nbserver-%s.json"%os.getpid()
608 603 return os.path.join(self.profile_dir.security_dir, info_file)
609 604
610 605 notebook_dir = Unicode(py3compat.getcwd(), config=True,
611 606 help="The directory to use for notebooks and kernels."
612 607 )
613 608
614 609 pylab = Unicode('disabled', config=True,
615 610 help="""
616 611 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
617 612 """
618 613 )
619 614 def _pylab_changed(self, name, old, new):
620 615 """when --pylab is specified, display a warning and exit"""
621 616 if new != 'warn':
622 617 backend = ' %s' % new
623 618 else:
624 619 backend = ''
625 620 self.log.error("Support for specifying --pylab on the command line has been removed.")
626 621 self.log.error(
627 622 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
628 623 )
629 624 self.exit(1)
630 625
631 626 def _notebook_dir_changed(self, name, old, new):
632 627 """Do a bit of validation of the notebook dir."""
633 628 if not os.path.isabs(new):
634 629 # If we receive a non-absolute path, make it absolute.
635 630 self.notebook_dir = os.path.abspath(new)
636 631 return
637 632 if not os.path.isdir(new):
638 633 raise TraitError("No such notebook dir: %r" % new)
639 634
640 635 # setting App.notebook_dir implies setting notebook and kernel dirs as well
641 636 self.config.FileContentsManager.root_dir = new
642 637 self.config.MappingKernelManager.root_dir = new
643 638
644 639
645 640 def parse_command_line(self, argv=None):
646 641 super(NotebookApp, self).parse_command_line(argv)
647 642
648 643 if self.extra_args:
649 644 arg0 = self.extra_args[0]
650 645 f = os.path.abspath(arg0)
651 646 self.argv.remove(arg0)
652 647 if not os.path.exists(f):
653 648 self.log.critical("No such file or directory: %s", f)
654 649 self.exit(1)
655 650
656 651 # Use config here, to ensure that it takes higher priority than
657 652 # anything that comes from the profile.
658 653 c = Config()
659 654 if os.path.isdir(f):
660 655 c.NotebookApp.notebook_dir = f
661 656 elif os.path.isfile(f):
662 657 c.NotebookApp.file_to_run = f
663 658 self.update_config(c)
664 659
665 660 def init_kernel_argv(self):
666 661 """construct the kernel arguments"""
667 662 # Kernel should get *absolute* path to profile directory
668 663 self.kernel_argv = ["--profile-dir", self.profile_dir.location]
669 664
670 665 def init_configurables(self):
671 666 # force Session default to be secure
672 667 default_secure(self.config)
673 668 kls = import_item(self.kernel_manager_class)
674 669 self.kernel_manager = kls(
675 670 parent=self, log=self.log, kernel_argv=self.kernel_argv,
676 671 connection_dir = self.profile_dir.security_dir,
677 672 )
678 673 kls = import_item(self.contents_manager_class)
679 674 self.contents_manager = kls(parent=self, log=self.log)
680 675 kls = import_item(self.session_manager_class)
681 676 self.session_manager = kls(parent=self, log=self.log,
682 677 kernel_manager=self.kernel_manager,
683 678 contents_manager=self.contents_manager)
684 679 kls = import_item(self.cluster_manager_class)
685 680 self.cluster_manager = kls(parent=self, log=self.log)
686 681 self.cluster_manager.update_profiles()
687 682
688 683 def init_logging(self):
689 684 # This prevents double log messages because tornado use a root logger that
690 685 # self.log is a child of. The logging module dipatches log messages to a log
691 686 # and all of its ancenstors until propagate is set to False.
692 687 self.log.propagate = False
693 688
694 689 for log in app_log, access_log, gen_log:
695 690 # consistent log output name (NotebookApp instead of tornado.access, etc.)
696 691 log.name = self.log.name
697 692 # hook up tornado 3's loggers to our app handlers
698 693 logger = logging.getLogger('tornado')
699 694 logger.propagate = True
700 695 logger.parent = self.log
701 696 logger.setLevel(self.log.level)
702 697
703 698 def init_webapp(self):
704 699 """initialize tornado webapp and httpserver"""
705 700 self.tornado_settings['allow_origin'] = self.allow_origin
706 701 if self.allow_origin_pat:
707 702 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
708 703 self.tornado_settings['allow_credentials'] = self.allow_credentials
709 704
710 705 self.web_app = NotebookWebApplication(
711 706 self, self.kernel_manager, self.contents_manager,
712 707 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
713 708 self.log, self.base_url, self.default_url, self.tornado_settings,
714 709 self.jinja_environment_options
715 710 )
716 711 if self.certfile:
717 712 ssl_options = dict(certfile=self.certfile)
718 713 if self.keyfile:
719 714 ssl_options['keyfile'] = self.keyfile
720 715 else:
721 716 ssl_options = None
722 717 self.web_app.password = self.password
723 718 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
724 719 xheaders=self.trust_xheaders)
725 720 if not self.ip:
726 721 warning = "WARNING: The notebook server is listening on all IP addresses"
727 722 if ssl_options is None:
728 723 self.log.critical(warning + " and not using encryption. This "
729 724 "is not recommended.")
730 725 if not self.password:
731 726 self.log.critical(warning + " and not using authentication. "
732 727 "This is highly insecure and not recommended.")
733 728 success = None
734 729 for port in random_ports(self.port, self.port_retries+1):
735 730 try:
736 731 self.http_server.listen(port, self.ip)
737 732 except socket.error as e:
738 733 if e.errno == errno.EADDRINUSE:
739 734 self.log.info('The port %i is already in use, trying another random port.' % port)
740 735 continue
741 736 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
742 737 self.log.warn("Permission to listen on port %i denied" % port)
743 738 continue
744 739 else:
745 740 raise
746 741 else:
747 742 self.port = port
748 743 success = True
749 744 break
750 745 if not success:
751 746 self.log.critical('ERROR: the notebook server could not be started because '
752 747 'no available port could be found.')
753 748 self.exit(1)
754 749
755 750 @property
756 751 def display_url(self):
757 752 ip = self.ip if self.ip else '[all ip addresses on your system]'
758 753 return self._url(ip)
759 754
760 755 @property
761 756 def connection_url(self):
762 757 ip = self.ip if self.ip else 'localhost'
763 758 return self._url(ip)
764 759
765 760 def _url(self, ip):
766 761 proto = 'https' if self.certfile else 'http'
767 762 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
768 763
769 764 def init_signal(self):
770 765 if not sys.platform.startswith('win'):
771 766 signal.signal(signal.SIGINT, self._handle_sigint)
772 767 signal.signal(signal.SIGTERM, self._signal_stop)
773 768 if hasattr(signal, 'SIGUSR1'):
774 769 # Windows doesn't support SIGUSR1
775 770 signal.signal(signal.SIGUSR1, self._signal_info)
776 771 if hasattr(signal, 'SIGINFO'):
777 772 # only on BSD-based systems
778 773 signal.signal(signal.SIGINFO, self._signal_info)
779 774
780 775 def _handle_sigint(self, sig, frame):
781 776 """SIGINT handler spawns confirmation dialog"""
782 777 # register more forceful signal handler for ^C^C case
783 778 signal.signal(signal.SIGINT, self._signal_stop)
784 779 # request confirmation dialog in bg thread, to avoid
785 780 # blocking the App
786 781 thread = threading.Thread(target=self._confirm_exit)
787 782 thread.daemon = True
788 783 thread.start()
789 784
790 785 def _restore_sigint_handler(self):
791 786 """callback for restoring original SIGINT handler"""
792 787 signal.signal(signal.SIGINT, self._handle_sigint)
793 788
794 789 def _confirm_exit(self):
795 790 """confirm shutdown on ^C
796 791
797 792 A second ^C, or answering 'y' within 5s will cause shutdown,
798 793 otherwise original SIGINT handler will be restored.
799 794
800 795 This doesn't work on Windows.
801 796 """
802 797 info = self.log.info
803 798 info('interrupted')
804 799 print(self.notebook_info())
805 800 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
806 801 sys.stdout.flush()
807 802 r,w,x = select.select([sys.stdin], [], [], 5)
808 803 if r:
809 804 line = sys.stdin.readline()
810 805 if line.lower().startswith('y') and 'n' not in line.lower():
811 806 self.log.critical("Shutdown confirmed")
812 807 ioloop.IOLoop.instance().stop()
813 808 return
814 809 else:
815 810 print("No answer for 5s:", end=' ')
816 811 print("resuming operation...")
817 812 # no answer, or answer is no:
818 813 # set it back to original SIGINT handler
819 814 # use IOLoop.add_callback because signal.signal must be called
820 815 # from main thread
821 816 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
822 817
823 818 def _signal_stop(self, sig, frame):
824 819 self.log.critical("received signal %s, stopping", sig)
825 820 ioloop.IOLoop.instance().stop()
826 821
827 822 def _signal_info(self, sig, frame):
828 823 print(self.notebook_info())
829 824
830 825 def init_components(self):
831 826 """Check the components submodule, and warn if it's unclean"""
832 827 status = submodule.check_submodule_status()
833 828 if status == 'missing':
834 829 self.log.warn("components submodule missing, running `git submodule update`")
835 830 submodule.update_submodules(submodule.ipython_parent())
836 831 elif status == 'unclean':
837 832 self.log.warn("components submodule unclean, you may see 404s on static/components")
838 833 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
839 834
840 835 @catch_config_error
841 836 def initialize(self, argv=None):
842 837 super(NotebookApp, self).initialize(argv)
843 838 self.init_logging()
844 839 self.init_kernel_argv()
845 840 self.init_configurables()
846 841 self.init_components()
847 842 self.init_webapp()
848 843 self.init_signal()
849 844
850 845 def cleanup_kernels(self):
851 846 """Shutdown all kernels.
852 847
853 848 The kernels will shutdown themselves when this process no longer exists,
854 849 but explicit shutdown allows the KernelManagers to cleanup the connection files.
855 850 """
856 851 self.log.info('Shutting down kernels')
857 852 self.kernel_manager.shutdown_all()
858 853
859 854 def notebook_info(self):
860 855 "Return the current working directory and the server url information"
861 856 info = self.contents_manager.info_string() + "\n"
862 857 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
863 858 return info + "The IPython Notebook is running at: %s" % self.display_url
864 859
865 860 def server_info(self):
866 861 """Return a JSONable dict of information about this server."""
867 862 return {'url': self.connection_url,
868 863 'hostname': self.ip if self.ip else 'localhost',
869 864 'port': self.port,
870 865 'secure': bool(self.certfile),
871 866 'base_url': self.base_url,
872 867 'notebook_dir': os.path.abspath(self.notebook_dir),
873 868 'pid': os.getpid()
874 869 }
875 870
876 871 def write_server_info_file(self):
877 872 """Write the result of server_info() to the JSON file info_file."""
878 873 with open(self.info_file, 'w') as f:
879 874 json.dump(self.server_info(), f, indent=2)
880 875
881 876 def remove_server_info_file(self):
882 877 """Remove the nbserver-<pid>.json file created for this server.
883 878
884 879 Ignores the error raised when the file has already been removed.
885 880 """
886 881 try:
887 882 os.unlink(self.info_file)
888 883 except OSError as e:
889 884 if e.errno != errno.ENOENT:
890 885 raise
891 886
892 887 def start(self):
893 888 """ Start the IPython Notebook server app, after initialization
894 889
895 890 This method takes no arguments so all configuration and initialization
896 891 must be done prior to calling this method."""
897 892 if self.subapp is not None:
898 893 return self.subapp.start()
899 894
900 895 info = self.log.info
901 896 for line in self.notebook_info().split("\n"):
902 897 info(line)
903 898 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
904 899
905 900 self.write_server_info_file()
906 901
907 902 if self.open_browser or self.file_to_run:
908 903 try:
909 904 browser = webbrowser.get(self.browser or None)
910 905 except webbrowser.Error as e:
911 906 self.log.warn('No web browser found: %s.' % e)
912 907 browser = None
913 908
914 909 if self.file_to_run:
915 910 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
916 911 if not os.path.exists(fullpath):
917 912 self.log.critical("%s does not exist" % fullpath)
918 913 self.exit(1)
919 914
920 915 uri = url_path_join('notebooks', self.file_to_run)
921 916 else:
922 917 uri = 'tree'
923 918 if browser:
924 919 b = lambda : browser.open(url_path_join(self.connection_url, uri),
925 920 new=2)
926 921 threading.Thread(target=b).start()
927 922 try:
928 923 ioloop.IOLoop.instance().start()
929 924 except KeyboardInterrupt:
930 925 info("Interrupted...")
931 926 finally:
932 927 self.cleanup_kernels()
933 928 self.remove_server_info_file()
934 929
935 930
936 931 def list_running_servers(profile='default'):
937 932 """Iterate over the server info files of running notebook servers.
938 933
939 934 Given a profile name, find nbserver-* files in the security directory of
940 935 that profile, and yield dicts of their information, each one pertaining to
941 936 a currently running notebook server instance.
942 937 """
943 938 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
944 939 for file in os.listdir(pd.security_dir):
945 940 if file.startswith('nbserver-'):
946 941 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
947 942 info = json.load(f)
948 943
949 944 # Simple check whether that process is really still running
950 945 if check_pid(info['pid']):
951 946 yield info
952 947 else:
953 948 # If the process has died, try to delete its info file
954 949 try:
955 950 os.unlink(file)
956 951 except OSError:
957 952 pass # TODO: This should warn or log or something
958 953 #-----------------------------------------------------------------------------
959 954 # Main entry point
960 955 #-----------------------------------------------------------------------------
961 956
962 957 launch_new_instance = NotebookApp.launch_instance
963 958
@@ -1,85 +1,135 b''
1 1 # coding: utf-8
2 2 """Test the /files/ handler."""
3 3
4 4 import io
5 5 import os
6 6 from unicodedata import normalize
7 7
8 8 pjoin = os.path.join
9 9
10 10 import requests
11 import json
12
13 from IPython.nbformat.current import (new_notebook, write, new_worksheet,
14 new_heading_cell, new_code_cell,
15 new_output)
11 16
12 17 from IPython.html.utils import url_path_join
13 18 from .launchnotebook import NotebookTestBase
14 19 from IPython.utils import py3compat
15 20
21
16 22 class FilesTest(NotebookTestBase):
17 23 def test_hidden_files(self):
18 24 not_hidden = [
19 25 u'å b',
20 26 u'å b/ç. d',
21 27 ]
22 28 hidden = [
23 29 u'.å b',
24 30 u'å b/.ç d',
25 31 ]
26 32 dirs = not_hidden + hidden
27 33
28 34 nbdir = self.notebook_dir.name
29 35 for d in dirs:
30 36 path = pjoin(nbdir, d.replace('/', os.sep))
31 37 if not os.path.exists(path):
32 38 os.mkdir(path)
33 39 with open(pjoin(path, 'foo'), 'w') as f:
34 40 f.write('foo')
35 41 with open(pjoin(path, '.foo'), 'w') as f:
36 42 f.write('.foo')
37 43 url = self.base_url()
38 44
39 45 for d in not_hidden:
40 46 path = pjoin(nbdir, d.replace('/', os.sep))
41 47 r = requests.get(url_path_join(url, 'files', d, 'foo'))
42 48 r.raise_for_status()
43 49 self.assertEqual(r.text, 'foo')
44 50 r = requests.get(url_path_join(url, 'files', d, '.foo'))
45 51 self.assertEqual(r.status_code, 404)
46 52
47 53 for d in hidden:
48 54 path = pjoin(nbdir, d.replace('/', os.sep))
49 55 for foo in ('foo', '.foo'):
50 56 r = requests.get(url_path_join(url, 'files', d, foo))
51 57 self.assertEqual(r.status_code, 404)
52 58
59 def test_contents_manager(self):
60 "make sure ContentsManager returns right files (ipynb, bin, txt)."
61
62 nbdir = self.notebook_dir.name
63 base = self.base_url()
64
65 nb = new_notebook(name='testnb')
66
67 ws = new_worksheet()
68 nb.worksheets = [ws]
69 ws.cells.append(new_heading_cell(u'Created by test ³'))
70 cc1 = new_code_cell(input=u'print(2*6)')
71 cc1.outputs.append(new_output(output_text=u'12', output_type='stream'))
72 ws.cells.append(cc1)
73
74 with io.open(pjoin(nbdir, 'testnb.ipynb'), 'w',
75 encoding='utf-8') as f:
76 write(nb, f, format='ipynb')
77
78 with io.open(pjoin(nbdir, 'test.bin'), 'wb') as f:
79 f.write(b'\xff' + os.urandom(5))
80 f.close()
81
82 with io.open(pjoin(nbdir, 'test.txt'), 'w') as f:
83 f.write(u'foobar')
84 f.close()
85
86 r = requests.get(url_path_join(base, 'files', 'testnb.ipynb'))
87 self.assertEqual(r.status_code, 200)
88 self.assertIn('print(2*6)', r.text)
89 json.loads(r.text)
90
91 r = requests.get(url_path_join(base, 'files', 'test.bin'))
92 self.assertEqual(r.status_code, 200)
93 self.assertEqual(r.headers['content-type'], 'application/octet-stream')
94 self.assertEqual(r.content[:1], b'\xff')
95 self.assertEqual(len(r.content), 6)
96
97 r = requests.get(url_path_join(base, 'files', 'test.txt'))
98 self.assertEqual(r.status_code, 200)
99 self.assertEqual(r.headers['content-type'], 'text/plain')
100 self.assertEqual(r.text, 'foobar')
101
102
53 103 def test_old_files_redirect(self):
54 104 """pre-2.0 'files/' prefixed links are properly redirected"""
55 105 nbdir = self.notebook_dir.name
56 106 base = self.base_url()
57 107
58 108 os.mkdir(pjoin(nbdir, 'files'))
59 109 os.makedirs(pjoin(nbdir, 'sub', 'files'))
60 110
61 111 for prefix in ('', 'sub'):
62 112 with open(pjoin(nbdir, prefix, 'files', 'f1.txt'), 'w') as f:
63 113 f.write(prefix + '/files/f1')
64 114 with open(pjoin(nbdir, prefix, 'files', 'f2.txt'), 'w') as f:
65 115 f.write(prefix + '/files/f2')
66 116 with open(pjoin(nbdir, prefix, 'f2.txt'), 'w') as f:
67 117 f.write(prefix + '/f2')
68 118 with open(pjoin(nbdir, prefix, 'f3.txt'), 'w') as f:
69 119 f.write(prefix + '/f3')
70 120
71 121 url = url_path_join(base, 'notebooks', prefix, 'files', 'f1.txt')
72 122 r = requests.get(url)
73 123 self.assertEqual(r.status_code, 200)
74 124 self.assertEqual(r.text, prefix + '/files/f1')
75 125
76 126 url = url_path_join(base, 'notebooks', prefix, 'files', 'f2.txt')
77 127 r = requests.get(url)
78 128 self.assertEqual(r.status_code, 200)
79 129 self.assertEqual(r.text, prefix + '/files/f2')
80 130
81 131 url = url_path_join(base, 'notebooks', prefix, 'files', 'f3.txt')
82 132 r = requests.get(url)
83 133 self.assertEqual(r.status_code, 200)
84 134 self.assertEqual(r.text, prefix + '/f3')
85 135
General Comments 0
You need to be logged in to leave comments. Login now