##// END OF EJS Templates
Merge pull request #7608 from minrk/default-location...
Thomas Kluyver -
r20222:184f056b merge
parent child Browse files
Show More
@@ -1,517 +1,522
1 1 """Base Tornado handlers for the notebook server."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import functools
7 7 import json
8 8 import logging
9 9 import os
10 10 import re
11 11 import sys
12 12 import traceback
13 13 try:
14 14 # py3
15 15 from http.client import responses
16 16 except ImportError:
17 17 from httplib import responses
18 18
19 19 from jinja2 import TemplateNotFound
20 20 from tornado import web
21 21
22 22 from tornado import gen
23 23 from tornado.log import app_log
24 24
25 25
26 26 import IPython
27 27 from IPython.utils.sysinfo import get_sys_info
28 28
29 29 from IPython.config import Application
30 30 from IPython.utils.path import filefind
31 31 from IPython.utils.py3compat import string_types
32 32 from IPython.html.utils import is_hidden, url_path_join, url_escape
33 33
34 34 from IPython.html.services.security import csp_report_uri
35 35
36 36 #-----------------------------------------------------------------------------
37 37 # Top-level handlers
38 38 #-----------------------------------------------------------------------------
39 39 non_alphanum = re.compile(r'[^A-Za-z0-9]')
40 40
41 41 sys_info = json.dumps(get_sys_info())
42 42
43 43 class AuthenticatedHandler(web.RequestHandler):
44 44 """A RequestHandler with an authenticated user."""
45 45
46 46 def set_default_headers(self):
47 47 headers = self.settings.get('headers', {})
48 48
49 49 if "Content-Security-Policy" not in headers:
50 50 headers["Content-Security-Policy"] = (
51 51 "frame-ancestors 'self'; "
52 52 # Make sure the report-uri is relative to the base_url
53 53 "report-uri " + url_path_join(self.base_url, csp_report_uri) + ";"
54 54 )
55 55
56 56 # Allow for overriding headers
57 57 for header_name,value in headers.items() :
58 58 try:
59 59 self.set_header(header_name, value)
60 60 except Exception as e:
61 61 # tornado raise Exception (not a subclass)
62 62 # if method is unsupported (websocket and Access-Control-Allow-Origin
63 63 # for example, so just ignore)
64 64 self.log.debug(e)
65 65
66 66 def clear_login_cookie(self):
67 67 self.clear_cookie(self.cookie_name)
68 68
69 69 def get_current_user(self):
70 70 if self.login_handler is None:
71 71 return 'anonymous'
72 72 return self.login_handler.get_user(self)
73 73
74 74 @property
75 75 def cookie_name(self):
76 76 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
77 77 self.request.host
78 78 ))
79 79 return self.settings.get('cookie_name', default_cookie_name)
80 80
81 81 @property
82 82 def logged_in(self):
83 83 """Is a user currently logged in?"""
84 84 user = self.get_current_user()
85 85 return (user and not user == 'anonymous')
86 86
87 87 @property
88 88 def login_handler(self):
89 89 """Return the login handler for this application, if any."""
90 90 return self.settings.get('login_handler_class', None)
91 91
92 92 @property
93 93 def login_available(self):
94 94 """May a user proceed to log in?
95 95
96 96 This returns True if login capability is available, irrespective of
97 97 whether the user is already logged in or not.
98 98
99 99 """
100 100 if self.login_handler is None:
101 101 return False
102 102 return bool(self.login_handler.login_available(self.settings))
103 103
104 104
105 105 class IPythonHandler(AuthenticatedHandler):
106 106 """IPython-specific extensions to authenticated handling
107 107
108 108 Mostly property shortcuts to IPython-specific settings.
109 109 """
110 110
111 111 @property
112 112 def config(self):
113 113 return self.settings.get('config', None)
114 114
115 115 @property
116 116 def log(self):
117 117 """use the IPython log by default, falling back on tornado's logger"""
118 118 if Application.initialized():
119 119 return Application.instance().log
120 120 else:
121 121 return app_log
122 122
123 123 #---------------------------------------------------------------
124 124 # URLs
125 125 #---------------------------------------------------------------
126 126
127 127 @property
128 128 def version_hash(self):
129 129 """The version hash to use for cache hints for static files"""
130 130 return self.settings.get('version_hash', '')
131 131
132 132 @property
133 133 def mathjax_url(self):
134 134 return self.settings.get('mathjax_url', '')
135 135
136 136 @property
137 137 def base_url(self):
138 138 return self.settings.get('base_url', '/')
139 139
140 140 @property
141 def default_url(self):
142 return self.settings.get('default_url', '')
143
144 @property
141 145 def ws_url(self):
142 146 return self.settings.get('websocket_url', '')
143 147
144 148 @property
145 149 def contents_js_source(self):
146 150 self.log.debug("Using contents: %s", self.settings.get('contents_js_source',
147 151 'services/contents'))
148 152 return self.settings.get('contents_js_source', 'services/contents')
149 153
150 154 #---------------------------------------------------------------
151 155 # Manager objects
152 156 #---------------------------------------------------------------
153 157
154 158 @property
155 159 def kernel_manager(self):
156 160 return self.settings['kernel_manager']
157 161
158 162 @property
159 163 def contents_manager(self):
160 164 return self.settings['contents_manager']
161 165
162 166 @property
163 167 def cluster_manager(self):
164 168 return self.settings['cluster_manager']
165 169
166 170 @property
167 171 def session_manager(self):
168 172 return self.settings['session_manager']
169 173
170 174 @property
171 175 def terminal_manager(self):
172 176 return self.settings['terminal_manager']
173 177
174 178 @property
175 179 def kernel_spec_manager(self):
176 180 return self.settings['kernel_spec_manager']
177 181
178 182 @property
179 183 def config_manager(self):
180 184 return self.settings['config_manager']
181 185
182 186 #---------------------------------------------------------------
183 187 # CORS
184 188 #---------------------------------------------------------------
185 189
186 190 @property
187 191 def allow_origin(self):
188 192 """Normal Access-Control-Allow-Origin"""
189 193 return self.settings.get('allow_origin', '')
190 194
191 195 @property
192 196 def allow_origin_pat(self):
193 197 """Regular expression version of allow_origin"""
194 198 return self.settings.get('allow_origin_pat', None)
195 199
196 200 @property
197 201 def allow_credentials(self):
198 202 """Whether to set Access-Control-Allow-Credentials"""
199 203 return self.settings.get('allow_credentials', False)
200 204
201 205 def set_default_headers(self):
202 206 """Add CORS headers, if defined"""
203 207 super(IPythonHandler, self).set_default_headers()
204 208 if self.allow_origin:
205 209 self.set_header("Access-Control-Allow-Origin", self.allow_origin)
206 210 elif self.allow_origin_pat:
207 211 origin = self.get_origin()
208 212 if origin and self.allow_origin_pat.match(origin):
209 213 self.set_header("Access-Control-Allow-Origin", origin)
210 214 if self.allow_credentials:
211 215 self.set_header("Access-Control-Allow-Credentials", 'true')
212 216
213 217 def get_origin(self):
214 218 # Handle WebSocket Origin naming convention differences
215 219 # The difference between version 8 and 13 is that in 8 the
216 220 # client sends a "Sec-Websocket-Origin" header and in 13 it's
217 221 # simply "Origin".
218 222 if "Origin" in self.request.headers:
219 223 origin = self.request.headers.get("Origin")
220 224 else:
221 225 origin = self.request.headers.get("Sec-Websocket-Origin", None)
222 226 return origin
223 227
224 228 #---------------------------------------------------------------
225 229 # template rendering
226 230 #---------------------------------------------------------------
227 231
228 232 def get_template(self, name):
229 233 """Return the jinja template object for a given name"""
230 234 return self.settings['jinja2_env'].get_template(name)
231 235
232 236 def render_template(self, name, **ns):
233 237 ns.update(self.template_namespace)
234 238 template = self.get_template(name)
235 239 return template.render(**ns)
236 240
237 241 @property
238 242 def template_namespace(self):
239 243 return dict(
240 244 base_url=self.base_url,
245 default_url=self.default_url,
241 246 ws_url=self.ws_url,
242 247 logged_in=self.logged_in,
243 248 login_available=self.login_available,
244 249 static_url=self.static_url,
245 250 sys_info=sys_info,
246 251 contents_js_source=self.contents_js_source,
247 252 version_hash=self.version_hash,
248 253 )
249 254
250 255 def get_json_body(self):
251 256 """Return the body of the request as JSON data."""
252 257 if not self.request.body:
253 258 return None
254 259 # Do we need to call body.decode('utf-8') here?
255 260 body = self.request.body.strip().decode(u'utf-8')
256 261 try:
257 262 model = json.loads(body)
258 263 except Exception:
259 264 self.log.debug("Bad JSON: %r", body)
260 265 self.log.error("Couldn't parse JSON", exc_info=True)
261 266 raise web.HTTPError(400, u'Invalid JSON in body of request')
262 267 return model
263 268
264 269 def write_error(self, status_code, **kwargs):
265 270 """render custom error pages"""
266 271 exc_info = kwargs.get('exc_info')
267 272 message = ''
268 273 status_message = responses.get(status_code, 'Unknown HTTP Error')
269 274 if exc_info:
270 275 exception = exc_info[1]
271 276 # get the custom message, if defined
272 277 try:
273 278 message = exception.log_message % exception.args
274 279 except Exception:
275 280 pass
276 281
277 282 # construct the custom reason, if defined
278 283 reason = getattr(exception, 'reason', '')
279 284 if reason:
280 285 status_message = reason
281 286
282 287 # build template namespace
283 288 ns = dict(
284 289 status_code=status_code,
285 290 status_message=status_message,
286 291 message=message,
287 292 exception=exception,
288 293 )
289 294
290 295 self.set_header('Content-Type', 'text/html')
291 296 # render the template
292 297 try:
293 298 html = self.render_template('%s.html' % status_code, **ns)
294 299 except TemplateNotFound:
295 300 self.log.debug("No template for %d", status_code)
296 301 html = self.render_template('error.html', **ns)
297 302
298 303 self.write(html)
299 304
300 305
301 306
302 307 class Template404(IPythonHandler):
303 308 """Render our 404 template"""
304 309 def prepare(self):
305 310 raise web.HTTPError(404)
306 311
307 312
308 313 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
309 314 """static files should only be accessible when logged in"""
310 315
311 316 @web.authenticated
312 317 def get(self, path):
313 318 if os.path.splitext(path)[1] == '.ipynb':
314 319 name = path.rsplit('/', 1)[-1]
315 320 self.set_header('Content-Type', 'application/json')
316 321 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
317 322
318 323 return web.StaticFileHandler.get(self, path)
319 324
320 325 def set_headers(self):
321 326 super(AuthenticatedFileHandler, self).set_headers()
322 327 # disable browser caching, rely on 304 replies for savings
323 328 if "v" not in self.request.arguments:
324 329 self.add_header("Cache-Control", "no-cache")
325 330
326 331 def compute_etag(self):
327 332 return None
328 333
329 334 def validate_absolute_path(self, root, absolute_path):
330 335 """Validate and return the absolute path.
331 336
332 337 Requires tornado 3.1
333 338
334 339 Adding to tornado's own handling, forbids the serving of hidden files.
335 340 """
336 341 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
337 342 abs_root = os.path.abspath(root)
338 343 if is_hidden(abs_path, abs_root):
339 344 self.log.info("Refusing to serve hidden file, via 404 Error")
340 345 raise web.HTTPError(404)
341 346 return abs_path
342 347
343 348
344 349 def json_errors(method):
345 350 """Decorate methods with this to return GitHub style JSON errors.
346 351
347 352 This should be used on any JSON API on any handler method that can raise HTTPErrors.
348 353
349 354 This will grab the latest HTTPError exception using sys.exc_info
350 355 and then:
351 356
352 357 1. Set the HTTP status code based on the HTTPError
353 358 2. Create and return a JSON body with a message field describing
354 359 the error in a human readable form.
355 360 """
356 361 @functools.wraps(method)
357 362 @gen.coroutine
358 363 def wrapper(self, *args, **kwargs):
359 364 try:
360 365 result = yield gen.maybe_future(method(self, *args, **kwargs))
361 366 except web.HTTPError as e:
362 367 status = e.status_code
363 368 message = e.log_message
364 369 self.log.warn(message)
365 370 self.set_status(e.status_code)
366 371 reply = dict(message=message, reason=e.reason)
367 372 self.finish(json.dumps(reply))
368 373 except Exception:
369 374 self.log.error("Unhandled error in API request", exc_info=True)
370 375 status = 500
371 376 message = "Unknown server error"
372 377 t, value, tb = sys.exc_info()
373 378 self.set_status(status)
374 379 tb_text = ''.join(traceback.format_exception(t, value, tb))
375 380 reply = dict(message=message, reason=None, traceback=tb_text)
376 381 self.finish(json.dumps(reply))
377 382 else:
378 383 # FIXME: can use regular return in generators in py3
379 384 raise gen.Return(result)
380 385 return wrapper
381 386
382 387
383 388
384 389 #-----------------------------------------------------------------------------
385 390 # File handler
386 391 #-----------------------------------------------------------------------------
387 392
388 393 # to minimize subclass changes:
389 394 HTTPError = web.HTTPError
390 395
391 396 class FileFindHandler(web.StaticFileHandler):
392 397 """subclass of StaticFileHandler for serving files from a search path"""
393 398
394 399 # cache search results, don't search for files more than once
395 400 _static_paths = {}
396 401
397 402 def set_headers(self):
398 403 super(FileFindHandler, self).set_headers()
399 404 # disable browser caching, rely on 304 replies for savings
400 405 if "v" not in self.request.arguments or \
401 406 any(self.request.path.startswith(path) for path in self.no_cache_paths):
402 407 self.add_header("Cache-Control", "no-cache")
403 408
404 409 def initialize(self, path, default_filename=None, no_cache_paths=None):
405 410 self.no_cache_paths = no_cache_paths or []
406 411
407 412 if isinstance(path, string_types):
408 413 path = [path]
409 414
410 415 self.root = tuple(
411 416 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
412 417 )
413 418 self.default_filename = default_filename
414 419
415 420 def compute_etag(self):
416 421 return None
417 422
418 423 @classmethod
419 424 def get_absolute_path(cls, roots, path):
420 425 """locate a file to serve on our static file search path"""
421 426 with cls._lock:
422 427 if path in cls._static_paths:
423 428 return cls._static_paths[path]
424 429 try:
425 430 abspath = os.path.abspath(filefind(path, roots))
426 431 except IOError:
427 432 # IOError means not found
428 433 return ''
429 434
430 435 cls._static_paths[path] = abspath
431 436 return abspath
432 437
433 438 def validate_absolute_path(self, root, absolute_path):
434 439 """check if the file should be served (raises 404, 403, etc.)"""
435 440 if absolute_path == '':
436 441 raise web.HTTPError(404)
437 442
438 443 for root in self.root:
439 444 if (absolute_path + os.sep).startswith(root):
440 445 break
441 446
442 447 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
443 448
444 449
445 450 class ApiVersionHandler(IPythonHandler):
446 451
447 452 @json_errors
448 453 def get(self):
449 454 # not authenticated, so give as few info as possible
450 455 self.finish(json.dumps({"version":IPython.__version__}))
451 456
452 457
453 458 class TrailingSlashHandler(web.RequestHandler):
454 459 """Simple redirect handler that strips trailing slashes
455 460
456 461 This should be the first, highest priority handler.
457 462 """
458 463
459 464 def get(self):
460 465 self.redirect(self.request.uri.rstrip('/'))
461 466
462 467 post = put = get
463 468
464 469
465 470 class FilesRedirectHandler(IPythonHandler):
466 471 """Handler for redirecting relative URLs to the /files/ handler"""
467 472
468 473 @staticmethod
469 474 def redirect_to_files(self, path):
470 475 """make redirect logic a reusable static method
471 476
472 477 so it can be called from other handlers.
473 478 """
474 479 cm = self.contents_manager
475 480 if cm.dir_exists(path):
476 481 # it's a *directory*, redirect to /tree
477 482 url = url_path_join(self.base_url, 'tree', path)
478 483 else:
479 484 orig_path = path
480 485 # otherwise, redirect to /files
481 486 parts = path.split('/')
482 487
483 488 if not cm.file_exists(path=path) and 'files' in parts:
484 489 # redirect without files/ iff it would 404
485 490 # this preserves pre-2.0-style 'files/' links
486 491 self.log.warn("Deprecated files/ URL: %s", orig_path)
487 492 parts.remove('files')
488 493 path = '/'.join(parts)
489 494
490 495 if not cm.file_exists(path=path):
491 496 raise web.HTTPError(404)
492 497
493 498 url = url_path_join(self.base_url, 'files', path)
494 499 url = url_escape(url)
495 500 self.log.debug("Redirecting %s to %s", self.request.path, url)
496 501 self.redirect(url)
497 502
498 503 def get(self, path=''):
499 504 return self.redirect_to_files(self, path)
500 505
501 506
502 507 #-----------------------------------------------------------------------------
503 508 # URL pattern fragments for re-use
504 509 #-----------------------------------------------------------------------------
505 510
506 511 # path matches any number of `/foo[/bar...]` or just `/` or ''
507 512 path_regex = r"(?P<path>(?:(?:/[^/]+)+|/?))"
508 513
509 514 #-----------------------------------------------------------------------------
510 515 # URL to handler mappings
511 516 #-----------------------------------------------------------------------------
512 517
513 518
514 519 default_handlers = [
515 520 (r".*/", TrailingSlashHandler),
516 521 (r"api", ApiVersionHandler)
517 522 ]
@@ -1,1105 +1,1108
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 datetime
11 11 import errno
12 12 import importlib
13 13 import io
14 14 import json
15 15 import logging
16 16 import os
17 17 import random
18 18 import re
19 19 import select
20 20 import signal
21 21 import socket
22 22 import sys
23 23 import threading
24 24 import webbrowser
25 25
26 26
27 27 # check for pyzmq 2.1.11
28 28 from IPython.utils.zmqrelated import check_for_zmq
29 29 check_for_zmq('2.1.11', 'IPython.html')
30 30
31 31 from jinja2 import Environment, FileSystemLoader
32 32
33 33 # Install the pyzmq ioloop. This has to be done before anything else from
34 34 # tornado is imported.
35 35 from zmq.eventloop import ioloop
36 36 ioloop.install()
37 37
38 38 # check for tornado 3.1.0
39 39 msg = "The IPython Notebook requires tornado >= 4.0"
40 40 try:
41 41 import tornado
42 42 except ImportError:
43 43 raise ImportError(msg)
44 44 try:
45 45 version_info = tornado.version_info
46 46 except AttributeError:
47 47 raise ImportError(msg + ", but you have < 1.1.0")
48 48 if version_info < (4,0):
49 49 raise ImportError(msg + ", but you have %s" % tornado.version)
50 50
51 51 from tornado import httpserver
52 52 from tornado import web
53 53 from tornado.log import LogFormatter, app_log, access_log, gen_log
54 54
55 55 from IPython.html import (
56 56 DEFAULT_STATIC_FILES_PATH,
57 57 DEFAULT_TEMPLATE_PATH_LIST,
58 58 )
59 59 from .base.handlers import Template404
60 60 from .log import log_request
61 61 from .services.kernels.kernelmanager import MappingKernelManager
62 62 from .services.config import ConfigManager
63 63 from .services.contents.manager import ContentsManager
64 64 from .services.contents.filemanager import FileContentsManager
65 65 from .services.clusters.clustermanager import ClusterManager
66 66 from .services.sessions.sessionmanager import SessionManager
67 67
68 68 from .auth.login import LoginHandler
69 69 from .auth.logout import LogoutHandler
70 70 from .base.handlers import IPythonHandler, FileFindHandler
71 71
72 72 from IPython.config import Config
73 73 from IPython.config.application import catch_config_error, boolean_flag
74 74 from IPython.core.application import (
75 75 BaseIPythonApplication, base_flags, base_aliases,
76 76 )
77 77 from IPython.core.profiledir import ProfileDir
78 78 from IPython.kernel import KernelManager
79 79 from IPython.kernel.kernelspec import KernelSpecManager
80 80 from IPython.kernel.zmq.session import default_secure, Session
81 81 from IPython.nbformat.sign import NotebookNotary
82 82 from IPython.utils.importstring import import_item
83 83 from IPython.utils import submodule
84 84 from IPython.utils.process import check_pid
85 85 from IPython.utils.traitlets import (
86 86 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
87 87 TraitError, Type,
88 88 )
89 89 from IPython.utils import py3compat
90 90 from IPython.utils.path import filefind, get_ipython_dir
91 91 from IPython.utils.sysinfo import get_sys_info
92 92
93 93 from .nbextensions import SYSTEM_NBEXTENSIONS_DIRS
94 94 from .utils import url_path_join
95 95
96 96 #-----------------------------------------------------------------------------
97 97 # Module globals
98 98 #-----------------------------------------------------------------------------
99 99
100 100 _examples = """
101 101 ipython notebook # start the notebook
102 102 ipython notebook --profile=sympy # use the sympy profile
103 103 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
104 104 """
105 105
106 106 #-----------------------------------------------------------------------------
107 107 # Helper functions
108 108 #-----------------------------------------------------------------------------
109 109
110 110 def random_ports(port, n):
111 111 """Generate a list of n random ports near the given port.
112 112
113 113 The first 5 ports will be sequential, and the remaining n-5 will be
114 114 randomly selected in the range [port-2*n, port+2*n].
115 115 """
116 116 for i in range(min(5, n)):
117 117 yield port + i
118 118 for i in range(n-5):
119 119 yield max(1, port + random.randint(-2*n, 2*n))
120 120
121 121 def load_handlers(name):
122 122 """Load the (URL pattern, handler) tuples for each component."""
123 123 name = 'IPython.html.' + name
124 124 mod = __import__(name, fromlist=['default_handlers'])
125 125 return mod.default_handlers
126 126
127 127 #-----------------------------------------------------------------------------
128 128 # The Tornado web application
129 129 #-----------------------------------------------------------------------------
130 130
131 131 class NotebookWebApplication(web.Application):
132 132
133 133 def __init__(self, ipython_app, kernel_manager, contents_manager,
134 134 cluster_manager, session_manager, kernel_spec_manager,
135 135 config_manager, log,
136 136 base_url, default_url, settings_overrides, jinja_env_options):
137 137
138 138 settings = self.init_settings(
139 139 ipython_app, kernel_manager, contents_manager, cluster_manager,
140 140 session_manager, kernel_spec_manager, config_manager, log, base_url,
141 141 default_url, settings_overrides, jinja_env_options)
142 142 handlers = self.init_handlers(settings)
143 143
144 144 super(NotebookWebApplication, self).__init__(handlers, **settings)
145 145
146 146 def init_settings(self, ipython_app, kernel_manager, contents_manager,
147 147 cluster_manager, session_manager, kernel_spec_manager,
148 148 config_manager,
149 149 log, base_url, default_url, settings_overrides,
150 150 jinja_env_options=None):
151 151
152 152 _template_path = settings_overrides.get(
153 153 "template_path",
154 154 ipython_app.template_file_path,
155 155 )
156 156 if isinstance(_template_path, str):
157 157 _template_path = (_template_path,)
158 158 template_path = [os.path.expanduser(path) for path in _template_path]
159 159
160 160 jenv_opt = jinja_env_options if jinja_env_options else {}
161 161 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
162 162
163 163 sys_info = get_sys_info()
164 164 if sys_info['commit_source'] == 'repository':
165 165 # don't cache (rely on 304) when working from master
166 166 version_hash = ''
167 167 else:
168 168 # reset the cache on server restart
169 169 version_hash = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
170 170
171 171 settings = dict(
172 172 # basics
173 173 log_function=log_request,
174 174 base_url=base_url,
175 175 default_url=default_url,
176 176 template_path=template_path,
177 177 static_path=ipython_app.static_file_path,
178 178 static_handler_class = FileFindHandler,
179 179 static_url_prefix = url_path_join(base_url,'/static/'),
180 180 static_handler_args = {
181 181 # don't cache custom.js
182 182 'no_cache_paths': [url_path_join(base_url, 'static', 'custom')],
183 183 },
184 184 version_hash=version_hash,
185 185
186 186 # authentication
187 187 cookie_secret=ipython_app.cookie_secret,
188 188 login_url=url_path_join(base_url,'/login'),
189 189 login_handler_class=ipython_app.login_handler_class,
190 190 logout_handler_class=ipython_app.logout_handler_class,
191 191 password=ipython_app.password,
192 192
193 193 # managers
194 194 kernel_manager=kernel_manager,
195 195 contents_manager=contents_manager,
196 196 cluster_manager=cluster_manager,
197 197 session_manager=session_manager,
198 198 kernel_spec_manager=kernel_spec_manager,
199 199 config_manager=config_manager,
200 200
201 201 # IPython stuff
202 202 nbextensions_path=ipython_app.nbextensions_path,
203 203 websocket_url=ipython_app.websocket_url,
204 204 mathjax_url=ipython_app.mathjax_url,
205 205 config=ipython_app.config,
206 206 jinja2_env=env,
207 207 terminals_available=False, # Set later if terminals are available
208 208 )
209 209
210 210 # allow custom overrides for the tornado web app.
211 211 settings.update(settings_overrides)
212 212 return settings
213 213
214 214 def init_handlers(self, settings):
215 215 """Load the (URL pattern, handler) tuples for each component."""
216 216
217 217 # Order matters. The first handler to match the URL will handle the request.
218 218 handlers = []
219 219 handlers.extend(load_handlers('tree.handlers'))
220 220 handlers.extend([(r"/login", settings['login_handler_class'])])
221 221 handlers.extend([(r"/logout", settings['logout_handler_class'])])
222 222 handlers.extend(load_handlers('files.handlers'))
223 223 handlers.extend(load_handlers('notebook.handlers'))
224 224 handlers.extend(load_handlers('nbconvert.handlers'))
225 225 handlers.extend(load_handlers('kernelspecs.handlers'))
226 226 handlers.extend(load_handlers('edit.handlers'))
227 227 handlers.extend(load_handlers('services.config.handlers'))
228 228 handlers.extend(load_handlers('services.kernels.handlers'))
229 229 handlers.extend(load_handlers('services.contents.handlers'))
230 230 handlers.extend(load_handlers('services.clusters.handlers'))
231 231 handlers.extend(load_handlers('services.sessions.handlers'))
232 232 handlers.extend(load_handlers('services.nbconvert.handlers'))
233 233 handlers.extend(load_handlers('services.kernelspecs.handlers'))
234 234 handlers.extend(load_handlers('services.security.handlers'))
235 235 handlers.append(
236 236 (r"/nbextensions/(.*)", FileFindHandler, {
237 237 'path': settings['nbextensions_path'],
238 238 'no_cache_paths': ['/'], # don't cache anything in nbextensions
239 239 }),
240 240 )
241 241 # register base handlers last
242 242 handlers.extend(load_handlers('base.handlers'))
243 243 # set the URL that will be redirected from `/`
244 244 handlers.append(
245 245 (r'/?', web.RedirectHandler, {
246 'url' : url_path_join(settings['base_url'], settings['default_url']),
246 'url' : settings['default_url'],
247 247 'permanent': False, # want 302, not 301
248 248 })
249 249 )
250 250 # prepend base_url onto the patterns that we match
251 251 new_handlers = []
252 252 for handler in handlers:
253 253 pattern = url_path_join(settings['base_url'], handler[0])
254 254 new_handler = tuple([pattern] + list(handler[1:]))
255 255 new_handlers.append(new_handler)
256 256 # add 404 on the end, which will catch everything that falls through
257 257 new_handlers.append((r'(.*)', Template404))
258 258 return new_handlers
259 259
260 260
261 261 class NbserverListApp(BaseIPythonApplication):
262 262
263 263 description="List currently running notebook servers in this profile."
264 264
265 265 flags = dict(
266 266 json=({'NbserverListApp': {'json': True}},
267 267 "Produce machine-readable JSON output."),
268 268 )
269 269
270 270 json = Bool(False, config=True,
271 271 help="If True, each line of output will be a JSON object with the "
272 272 "details from the server info file.")
273 273
274 274 def start(self):
275 275 if not self.json:
276 276 print("Currently running servers:")
277 277 for serverinfo in list_running_servers(self.profile):
278 278 if self.json:
279 279 print(json.dumps(serverinfo))
280 280 else:
281 281 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
282 282
283 283 #-----------------------------------------------------------------------------
284 284 # Aliases and Flags
285 285 #-----------------------------------------------------------------------------
286 286
287 287 flags = dict(base_flags)
288 288 flags['no-browser']=(
289 289 {'NotebookApp' : {'open_browser' : False}},
290 290 "Don't open the notebook in a browser after startup."
291 291 )
292 292 flags['pylab']=(
293 293 {'NotebookApp' : {'pylab' : 'warn'}},
294 294 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
295 295 )
296 296 flags['no-mathjax']=(
297 297 {'NotebookApp' : {'enable_mathjax' : False}},
298 298 """Disable MathJax
299 299
300 300 MathJax is the javascript library IPython uses to render math/LaTeX. It is
301 301 very large, so you may want to disable it if you have a slow internet
302 302 connection, or for offline use of the notebook.
303 303
304 304 When disabled, equations etc. will appear as their untransformed TeX source.
305 305 """
306 306 )
307 307
308 308 # Add notebook manager flags
309 309 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
310 310 'DEPRECATED, IGNORED',
311 311 'DEPRECATED, IGNORED'))
312 312
313 313 aliases = dict(base_aliases)
314 314
315 315 aliases.update({
316 316 'ip': 'NotebookApp.ip',
317 317 'port': 'NotebookApp.port',
318 318 'port-retries': 'NotebookApp.port_retries',
319 319 'transport': 'KernelManager.transport',
320 320 'keyfile': 'NotebookApp.keyfile',
321 321 'certfile': 'NotebookApp.certfile',
322 322 'notebook-dir': 'NotebookApp.notebook_dir',
323 323 'browser': 'NotebookApp.browser',
324 324 'pylab': 'NotebookApp.pylab',
325 325 })
326 326
327 327 #-----------------------------------------------------------------------------
328 328 # NotebookApp
329 329 #-----------------------------------------------------------------------------
330 330
331 331 class NotebookApp(BaseIPythonApplication):
332 332
333 333 name = 'ipython-notebook'
334 334
335 335 description = """
336 336 The IPython HTML Notebook.
337 337
338 338 This launches a Tornado based HTML Notebook Server that serves up an
339 339 HTML5/Javascript Notebook client.
340 340 """
341 341 examples = _examples
342 342 aliases = aliases
343 343 flags = flags
344 344
345 345 classes = [
346 346 KernelManager, ProfileDir, Session, MappingKernelManager,
347 347 ContentsManager, FileContentsManager, NotebookNotary,
348 348 ]
349 349 flags = Dict(flags)
350 350 aliases = Dict(aliases)
351 351
352 352 subcommands = dict(
353 353 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
354 354 )
355 355
356 356 ipython_kernel_argv = List(Unicode)
357 357
358 358 _log_formatter_cls = LogFormatter
359 359
360 360 def _log_level_default(self):
361 361 return logging.INFO
362 362
363 363 def _log_datefmt_default(self):
364 364 """Exclude date from default date format"""
365 365 return "%H:%M:%S"
366 366
367 367 def _log_format_default(self):
368 368 """override default log format to include time"""
369 369 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
370 370
371 371 # create requested profiles by default, if they don't exist:
372 372 auto_create = Bool(True)
373 373
374 374 # file to be opened in the notebook server
375 375 file_to_run = Unicode('', config=True)
376 376
377 377 # Network related information
378 378
379 379 allow_origin = Unicode('', config=True,
380 380 help="""Set the Access-Control-Allow-Origin header
381 381
382 382 Use '*' to allow any origin to access your server.
383 383
384 384 Takes precedence over allow_origin_pat.
385 385 """
386 386 )
387 387
388 388 allow_origin_pat = Unicode('', config=True,
389 389 help="""Use a regular expression for the Access-Control-Allow-Origin header
390 390
391 391 Requests from an origin matching the expression will get replies with:
392 392
393 393 Access-Control-Allow-Origin: origin
394 394
395 395 where `origin` is the origin of the request.
396 396
397 397 Ignored if allow_origin is set.
398 398 """
399 399 )
400 400
401 401 allow_credentials = Bool(False, config=True,
402 402 help="Set the Access-Control-Allow-Credentials: true header"
403 403 )
404 404
405 405 default_url = Unicode('/tree', config=True,
406 406 help="The default URL to redirect to from `/`"
407 407 )
408 408
409 409 ip = Unicode('localhost', config=True,
410 410 help="The IP address the notebook server will listen on."
411 411 )
412 412
413 413 def _ip_changed(self, name, old, new):
414 414 if new == u'*': self.ip = u''
415 415
416 416 port = Integer(8888, config=True,
417 417 help="The port the notebook server will listen on."
418 418 )
419 419 port_retries = Integer(50, config=True,
420 420 help="The number of additional ports to try if the specified port is not available."
421 421 )
422 422
423 423 certfile = Unicode(u'', config=True,
424 424 help="""The full path to an SSL/TLS certificate file."""
425 425 )
426 426
427 427 keyfile = Unicode(u'', config=True,
428 428 help="""The full path to a private key file for usage with SSL/TLS."""
429 429 )
430 430
431 431 cookie_secret_file = Unicode(config=True,
432 432 help="""The file where the cookie secret is stored."""
433 433 )
434 434 def _cookie_secret_file_default(self):
435 435 if self.profile_dir is None:
436 436 return ''
437 437 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
438 438
439 439 cookie_secret = Bytes(b'', config=True,
440 440 help="""The random bytes used to secure cookies.
441 441 By default this is a new random number every time you start the Notebook.
442 442 Set it to a value in a config file to enable logins to persist across server sessions.
443 443
444 444 Note: Cookie secrets should be kept private, do not share config files with
445 445 cookie_secret stored in plaintext (you can read the value from a file).
446 446 """
447 447 )
448 448 def _cookie_secret_default(self):
449 449 if os.path.exists(self.cookie_secret_file):
450 450 with io.open(self.cookie_secret_file, 'rb') as f:
451 451 return f.read()
452 452 else:
453 453 secret = base64.encodestring(os.urandom(1024))
454 454 self._write_cookie_secret_file(secret)
455 455 return secret
456 456
457 457 def _write_cookie_secret_file(self, secret):
458 458 """write my secret to my secret_file"""
459 459 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
460 460 with io.open(self.cookie_secret_file, 'wb') as f:
461 461 f.write(secret)
462 462 try:
463 463 os.chmod(self.cookie_secret_file, 0o600)
464 464 except OSError:
465 465 self.log.warn(
466 466 "Could not set permissions on %s",
467 467 self.cookie_secret_file
468 468 )
469 469
470 470 password = Unicode(u'', config=True,
471 471 help="""Hashed password to use for web authentication.
472 472
473 473 To generate, type in a python/IPython shell:
474 474
475 475 from IPython.lib import passwd; passwd()
476 476
477 477 The string should be of the form type:salt:hashed-password.
478 478 """
479 479 )
480 480
481 481 open_browser = Bool(True, config=True,
482 482 help="""Whether to open in a browser after starting.
483 483 The specific browser used is platform dependent and
484 484 determined by the python standard library `webbrowser`
485 485 module, unless it is overridden using the --browser
486 486 (NotebookApp.browser) configuration option.
487 487 """)
488 488
489 489 browser = Unicode(u'', config=True,
490 490 help="""Specify what command to use to invoke a web
491 491 browser when opening the notebook. If not specified, the
492 492 default browser will be determined by the `webbrowser`
493 493 standard library module, which allows setting of the
494 494 BROWSER environment variable to override it.
495 495 """)
496 496
497 497 webapp_settings = Dict(config=True,
498 498 help="DEPRECATED, use tornado_settings"
499 499 )
500 500 def _webapp_settings_changed(self, name, old, new):
501 501 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
502 502 self.tornado_settings = new
503 503
504 504 tornado_settings = Dict(config=True,
505 505 help="Supply overrides for the tornado.web.Application that the "
506 506 "IPython notebook uses.")
507 507
508 508 jinja_environment_options = Dict(config=True,
509 509 help="Supply extra arguments that will be passed to Jinja environment.")
510 510
511 511 enable_mathjax = Bool(True, config=True,
512 512 help="""Whether to enable MathJax for typesetting math/TeX
513 513
514 514 MathJax is the javascript library IPython uses to render math/LaTeX. It is
515 515 very large, so you may want to disable it if you have a slow internet
516 516 connection, or for offline use of the notebook.
517 517
518 518 When disabled, equations etc. will appear as their untransformed TeX source.
519 519 """
520 520 )
521 521 def _enable_mathjax_changed(self, name, old, new):
522 522 """set mathjax url to empty if mathjax is disabled"""
523 523 if not new:
524 524 self.mathjax_url = u''
525 525
526 526 base_url = Unicode('/', config=True,
527 527 help='''The base URL for the notebook server.
528 528
529 529 Leading and trailing slashes can be omitted,
530 530 and will automatically be added.
531 531 ''')
532 532 def _base_url_changed(self, name, old, new):
533 533 if not new.startswith('/'):
534 534 self.base_url = '/'+new
535 535 elif not new.endswith('/'):
536 536 self.base_url = new+'/'
537 537
538 538 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
539 539 def _base_project_url_changed(self, name, old, new):
540 540 self.log.warn("base_project_url is deprecated, use base_url")
541 541 self.base_url = new
542 542
543 543 extra_static_paths = List(Unicode, config=True,
544 544 help="""Extra paths to search for serving static files.
545 545
546 546 This allows adding javascript/css to be available from the notebook server machine,
547 547 or overriding individual files in the IPython"""
548 548 )
549 549 def _extra_static_paths_default(self):
550 550 return [os.path.join(self.profile_dir.location, 'static')]
551 551
552 552 @property
553 553 def static_file_path(self):
554 554 """return extra paths + the default location"""
555 555 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
556 556
557 557 extra_template_paths = List(Unicode, config=True,
558 558 help="""Extra paths to search for serving jinja templates.
559 559
560 560 Can be used to override templates from IPython.html.templates."""
561 561 )
562 562 def _extra_template_paths_default(self):
563 563 return []
564 564
565 565 @property
566 566 def template_file_path(self):
567 567 """return extra paths + the default locations"""
568 568 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
569 569
570 570 extra_nbextensions_path = List(Unicode, config=True,
571 571 help="""extra paths to look for Javascript notebook extensions"""
572 572 )
573 573
574 574 @property
575 575 def nbextensions_path(self):
576 576 """The path to look for Javascript notebook extensions"""
577 577 return self.extra_nbextensions_path + [os.path.join(get_ipython_dir(), 'nbextensions')] + SYSTEM_NBEXTENSIONS_DIRS
578 578
579 579 websocket_url = Unicode("", config=True,
580 580 help="""The base URL for websockets,
581 581 if it differs from the HTTP server (hint: it almost certainly doesn't).
582 582
583 583 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
584 584 """
585 585 )
586 586 mathjax_url = Unicode("", config=True,
587 587 help="""The url for MathJax.js."""
588 588 )
589 589 def _mathjax_url_default(self):
590 590 if not self.enable_mathjax:
591 591 return u''
592 592 static_url_prefix = self.tornado_settings.get("static_url_prefix",
593 593 url_path_join(self.base_url, "static")
594 594 )
595 595
596 596 # try local mathjax, either in nbextensions/mathjax or static/mathjax
597 597 for (url_prefix, search_path) in [
598 598 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
599 599 (static_url_prefix, self.static_file_path),
600 600 ]:
601 601 self.log.debug("searching for local mathjax in %s", search_path)
602 602 try:
603 603 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
604 604 except IOError:
605 605 continue
606 606 else:
607 607 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
608 608 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
609 609 return url
610 610
611 611 # no local mathjax, serve from CDN
612 612 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
613 613 self.log.info("Using MathJax from CDN: %s", url)
614 614 return url
615 615
616 616 def _mathjax_url_changed(self, name, old, new):
617 617 if new and not self.enable_mathjax:
618 618 # enable_mathjax=False overrides mathjax_url
619 619 self.mathjax_url = u''
620 620 else:
621 621 self.log.info("Using MathJax: %s", new)
622 622
623 623 contents_manager_class = Type(
624 624 default_value=FileContentsManager,
625 625 klass=ContentsManager,
626 626 config=True,
627 627 help='The notebook manager class to use.'
628 628 )
629 629 kernel_manager_class = Type(
630 630 default_value=MappingKernelManager,
631 631 config=True,
632 632 help='The kernel manager class to use.'
633 633 )
634 634 session_manager_class = Type(
635 635 default_value=SessionManager,
636 636 config=True,
637 637 help='The session manager class to use.'
638 638 )
639 639 cluster_manager_class = Type(
640 640 default_value=ClusterManager,
641 641 config=True,
642 642 help='The cluster manager class to use.'
643 643 )
644 644
645 645 config_manager_class = Type(
646 646 default_value=ConfigManager,
647 647 config = True,
648 648 help='The config manager class to use'
649 649 )
650 650
651 651 kernel_spec_manager = Instance(KernelSpecManager)
652 652
653 653 def _kernel_spec_manager_default(self):
654 654 return KernelSpecManager(ipython_dir=self.ipython_dir)
655 655
656 656 kernel_spec_manager_class = Type(
657 657 default_value=KernelSpecManager,
658 658 config=True,
659 659 help="""
660 660 The kernel spec manager class to use. Should be a subclass
661 661 of `IPython.kernel.kernelspec.KernelSpecManager`.
662 662
663 663 The Api of KernelSpecManager is provisional and might change
664 664 without warning between this version of IPython and the next stable one.
665 665 """
666 666 )
667 667
668 668 login_handler_class = Type(
669 669 default_value=LoginHandler,
670 670 klass=web.RequestHandler,
671 671 config=True,
672 672 help='The login handler class to use.',
673 673 )
674 674
675 675 logout_handler_class = Type(
676 676 default_value=LogoutHandler,
677 677 klass=web.RequestHandler,
678 678 config=True,
679 679 help='The logout handler class to use.',
680 680 )
681 681
682 682 trust_xheaders = Bool(False, config=True,
683 683 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
684 684 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
685 685 )
686 686
687 687 info_file = Unicode()
688 688
689 689 def _info_file_default(self):
690 690 info_file = "nbserver-%s.json"%os.getpid()
691 691 return os.path.join(self.profile_dir.security_dir, info_file)
692 692
693 693 pylab = Unicode('disabled', config=True,
694 694 help="""
695 695 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
696 696 """
697 697 )
698 698 def _pylab_changed(self, name, old, new):
699 699 """when --pylab is specified, display a warning and exit"""
700 700 if new != 'warn':
701 701 backend = ' %s' % new
702 702 else:
703 703 backend = ''
704 704 self.log.error("Support for specifying --pylab on the command line has been removed.")
705 705 self.log.error(
706 706 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
707 707 )
708 708 self.exit(1)
709 709
710 710 notebook_dir = Unicode(config=True,
711 711 help="The directory to use for notebooks and kernels."
712 712 )
713 713
714 714 def _notebook_dir_default(self):
715 715 if self.file_to_run:
716 716 return os.path.dirname(os.path.abspath(self.file_to_run))
717 717 else:
718 718 return py3compat.getcwd()
719 719
720 720 def _notebook_dir_changed(self, name, old, new):
721 721 """Do a bit of validation of the notebook dir."""
722 722 if not os.path.isabs(new):
723 723 # If we receive a non-absolute path, make it absolute.
724 724 self.notebook_dir = os.path.abspath(new)
725 725 return
726 726 if not os.path.isdir(new):
727 727 raise TraitError("No such notebook dir: %r" % new)
728 728
729 729 # setting App.notebook_dir implies setting notebook and kernel dirs as well
730 730 self.config.FileContentsManager.root_dir = new
731 731 self.config.MappingKernelManager.root_dir = new
732 732
733 733 server_extensions = List(Unicode(), config=True,
734 734 help=("Python modules to load as notebook server extensions. "
735 735 "This is an experimental API, and may change in future releases.")
736 736 )
737 737
738 738 def parse_command_line(self, argv=None):
739 739 super(NotebookApp, self).parse_command_line(argv)
740 740
741 741 if self.extra_args:
742 742 arg0 = self.extra_args[0]
743 743 f = os.path.abspath(arg0)
744 744 self.argv.remove(arg0)
745 745 if not os.path.exists(f):
746 746 self.log.critical("No such file or directory: %s", f)
747 747 self.exit(1)
748 748
749 749 # Use config here, to ensure that it takes higher priority than
750 750 # anything that comes from the profile.
751 751 c = Config()
752 752 if os.path.isdir(f):
753 753 c.NotebookApp.notebook_dir = f
754 754 elif os.path.isfile(f):
755 755 c.NotebookApp.file_to_run = f
756 756 self.update_config(c)
757 757
758 758 def init_kernel_argv(self):
759 759 """add the profile-dir to arguments to be passed to IPython kernels"""
760 760 # FIXME: remove special treatment of IPython kernels
761 761 # Kernel should get *absolute* path to profile directory
762 762 self.ipython_kernel_argv = ["--profile-dir", self.profile_dir.location]
763 763
764 764 def init_configurables(self):
765 765 # force Session default to be secure
766 766 default_secure(self.config)
767 767
768 768 self.kernel_spec_manager = self.kernel_spec_manager_class(
769 769 ipython_dir=self.ipython_dir,
770 770 )
771 771 self.kernel_manager = self.kernel_manager_class(
772 772 parent=self,
773 773 log=self.log,
774 774 ipython_kernel_argv=self.ipython_kernel_argv,
775 775 connection_dir=self.profile_dir.security_dir,
776 776 )
777 777 self.contents_manager = self.contents_manager_class(
778 778 parent=self,
779 779 log=self.log,
780 780 )
781 781 self.session_manager = self.session_manager_class(
782 782 parent=self,
783 783 log=self.log,
784 784 kernel_manager=self.kernel_manager,
785 785 contents_manager=self.contents_manager,
786 786 )
787 787 self.cluster_manager = self.cluster_manager_class(
788 788 parent=self,
789 789 log=self.log,
790 790 )
791 791
792 792 self.config_manager = self.config_manager_class(
793 793 parent=self,
794 794 log=self.log,
795 795 profile_dir=self.profile_dir.location,
796 796 )
797 797
798 798 def init_logging(self):
799 799 # This prevents double log messages because tornado use a root logger that
800 800 # self.log is a child of. The logging module dipatches log messages to a log
801 801 # and all of its ancenstors until propagate is set to False.
802 802 self.log.propagate = False
803 803
804 804 for log in app_log, access_log, gen_log:
805 805 # consistent log output name (NotebookApp instead of tornado.access, etc.)
806 806 log.name = self.log.name
807 807 # hook up tornado 3's loggers to our app handlers
808 808 logger = logging.getLogger('tornado')
809 809 logger.propagate = True
810 810 logger.parent = self.log
811 811 logger.setLevel(self.log.level)
812 812
813 813 def init_webapp(self):
814 814 """initialize tornado webapp and httpserver"""
815 815 self.tornado_settings['allow_origin'] = self.allow_origin
816 816 if self.allow_origin_pat:
817 817 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
818 818 self.tornado_settings['allow_credentials'] = self.allow_credentials
819 # ensure default_url starts with base_url
820 if not self.default_url.startswith(self.base_url):
821 self.default_url = url_path_join(self.base_url, self.default_url)
819 822
820 823 self.web_app = NotebookWebApplication(
821 824 self, self.kernel_manager, self.contents_manager,
822 825 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
823 826 self.config_manager,
824 827 self.log, self.base_url, self.default_url, self.tornado_settings,
825 828 self.jinja_environment_options
826 829 )
827 830 if self.certfile:
828 831 ssl_options = dict(certfile=self.certfile)
829 832 if self.keyfile:
830 833 ssl_options['keyfile'] = self.keyfile
831 834 else:
832 835 ssl_options = None
833 836 self.login_handler_class.validate_security(self, ssl_options=ssl_options)
834 837 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
835 838 xheaders=self.trust_xheaders)
836 839
837 840 success = None
838 841 for port in random_ports(self.port, self.port_retries+1):
839 842 try:
840 843 self.http_server.listen(port, self.ip)
841 844 except socket.error as e:
842 845 if e.errno == errno.EADDRINUSE:
843 846 self.log.info('The port %i is already in use, trying another random port.' % port)
844 847 continue
845 848 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
846 849 self.log.warn("Permission to listen on port %i denied" % port)
847 850 continue
848 851 else:
849 852 raise
850 853 else:
851 854 self.port = port
852 855 success = True
853 856 break
854 857 if not success:
855 858 self.log.critical('ERROR: the notebook server could not be started because '
856 859 'no available port could be found.')
857 860 self.exit(1)
858 861
859 862 @property
860 863 def display_url(self):
861 864 ip = self.ip if self.ip else '[all ip addresses on your system]'
862 865 return self._url(ip)
863 866
864 867 @property
865 868 def connection_url(self):
866 869 ip = self.ip if self.ip else 'localhost'
867 870 return self._url(ip)
868 871
869 872 def _url(self, ip):
870 873 proto = 'https' if self.certfile else 'http'
871 874 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
872 875
873 876 def init_terminals(self):
874 877 try:
875 878 from .terminal import initialize
876 879 initialize(self.web_app)
877 880 self.web_app.settings['terminals_available'] = True
878 881 except ImportError as e:
879 882 log = self.log.debug if sys.platform == 'win32' else self.log.warn
880 883 log("Terminals not available (error was %s)", e)
881 884
882 885 def init_signal(self):
883 886 if not sys.platform.startswith('win'):
884 887 signal.signal(signal.SIGINT, self._handle_sigint)
885 888 signal.signal(signal.SIGTERM, self._signal_stop)
886 889 if hasattr(signal, 'SIGUSR1'):
887 890 # Windows doesn't support SIGUSR1
888 891 signal.signal(signal.SIGUSR1, self._signal_info)
889 892 if hasattr(signal, 'SIGINFO'):
890 893 # only on BSD-based systems
891 894 signal.signal(signal.SIGINFO, self._signal_info)
892 895
893 896 def _handle_sigint(self, sig, frame):
894 897 """SIGINT handler spawns confirmation dialog"""
895 898 # register more forceful signal handler for ^C^C case
896 899 signal.signal(signal.SIGINT, self._signal_stop)
897 900 # request confirmation dialog in bg thread, to avoid
898 901 # blocking the App
899 902 thread = threading.Thread(target=self._confirm_exit)
900 903 thread.daemon = True
901 904 thread.start()
902 905
903 906 def _restore_sigint_handler(self):
904 907 """callback for restoring original SIGINT handler"""
905 908 signal.signal(signal.SIGINT, self._handle_sigint)
906 909
907 910 def _confirm_exit(self):
908 911 """confirm shutdown on ^C
909 912
910 913 A second ^C, or answering 'y' within 5s will cause shutdown,
911 914 otherwise original SIGINT handler will be restored.
912 915
913 916 This doesn't work on Windows.
914 917 """
915 918 info = self.log.info
916 919 info('interrupted')
917 920 print(self.notebook_info())
918 921 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
919 922 sys.stdout.flush()
920 923 r,w,x = select.select([sys.stdin], [], [], 5)
921 924 if r:
922 925 line = sys.stdin.readline()
923 926 if line.lower().startswith('y') and 'n' not in line.lower():
924 927 self.log.critical("Shutdown confirmed")
925 928 ioloop.IOLoop.current().stop()
926 929 return
927 930 else:
928 931 print("No answer for 5s:", end=' ')
929 932 print("resuming operation...")
930 933 # no answer, or answer is no:
931 934 # set it back to original SIGINT handler
932 935 # use IOLoop.add_callback because signal.signal must be called
933 936 # from main thread
934 937 ioloop.IOLoop.current().add_callback(self._restore_sigint_handler)
935 938
936 939 def _signal_stop(self, sig, frame):
937 940 self.log.critical("received signal %s, stopping", sig)
938 941 ioloop.IOLoop.current().stop()
939 942
940 943 def _signal_info(self, sig, frame):
941 944 print(self.notebook_info())
942 945
943 946 def init_components(self):
944 947 """Check the components submodule, and warn if it's unclean"""
945 948 status = submodule.check_submodule_status()
946 949 if status == 'missing':
947 950 self.log.warn("components submodule missing, running `git submodule update`")
948 951 submodule.update_submodules(submodule.ipython_parent())
949 952 elif status == 'unclean':
950 953 self.log.warn("components submodule unclean, you may see 404s on static/components")
951 954 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
952 955
953 956 def init_server_extensions(self):
954 957 """Load any extensions specified by config.
955 958
956 959 Import the module, then call the load_jupyter_server_extension function,
957 960 if one exists.
958 961
959 962 The extension API is experimental, and may change in future releases.
960 963 """
961 964 for modulename in self.server_extensions:
962 965 try:
963 966 mod = importlib.import_module(modulename)
964 967 func = getattr(mod, 'load_jupyter_server_extension', None)
965 968 if func is not None:
966 969 func(self)
967 970 except Exception:
968 971 self.log.warn("Error loading server extension %s", modulename,
969 972 exc_info=True)
970 973
971 974 @catch_config_error
972 975 def initialize(self, argv=None):
973 976 super(NotebookApp, self).initialize(argv)
974 977 self.init_logging()
975 978 self.init_kernel_argv()
976 979 self.init_configurables()
977 980 self.init_components()
978 981 self.init_webapp()
979 982 self.init_terminals()
980 983 self.init_signal()
981 984 self.init_server_extensions()
982 985
983 986 def cleanup_kernels(self):
984 987 """Shutdown all kernels.
985 988
986 989 The kernels will shutdown themselves when this process no longer exists,
987 990 but explicit shutdown allows the KernelManagers to cleanup the connection files.
988 991 """
989 992 self.log.info('Shutting down kernels')
990 993 self.kernel_manager.shutdown_all()
991 994
992 995 def notebook_info(self):
993 996 "Return the current working directory and the server url information"
994 997 info = self.contents_manager.info_string() + "\n"
995 998 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
996 999 return info + "The IPython Notebook is running at: %s" % self.display_url
997 1000
998 1001 def server_info(self):
999 1002 """Return a JSONable dict of information about this server."""
1000 1003 return {'url': self.connection_url,
1001 1004 'hostname': self.ip if self.ip else 'localhost',
1002 1005 'port': self.port,
1003 1006 'secure': bool(self.certfile),
1004 1007 'base_url': self.base_url,
1005 1008 'notebook_dir': os.path.abspath(self.notebook_dir),
1006 1009 'pid': os.getpid()
1007 1010 }
1008 1011
1009 1012 def write_server_info_file(self):
1010 1013 """Write the result of server_info() to the JSON file info_file."""
1011 1014 with open(self.info_file, 'w') as f:
1012 1015 json.dump(self.server_info(), f, indent=2)
1013 1016
1014 1017 def remove_server_info_file(self):
1015 1018 """Remove the nbserver-<pid>.json file created for this server.
1016 1019
1017 1020 Ignores the error raised when the file has already been removed.
1018 1021 """
1019 1022 try:
1020 1023 os.unlink(self.info_file)
1021 1024 except OSError as e:
1022 1025 if e.errno != errno.ENOENT:
1023 1026 raise
1024 1027
1025 1028 def start(self):
1026 1029 """ Start the IPython Notebook server app, after initialization
1027 1030
1028 1031 This method takes no arguments so all configuration and initialization
1029 1032 must be done prior to calling this method."""
1030 1033 if self.subapp is not None:
1031 1034 return self.subapp.start()
1032 1035
1033 1036 info = self.log.info
1034 1037 for line in self.notebook_info().split("\n"):
1035 1038 info(line)
1036 1039 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
1037 1040
1038 1041 self.write_server_info_file()
1039 1042
1040 1043 if self.open_browser or self.file_to_run:
1041 1044 try:
1042 1045 browser = webbrowser.get(self.browser or None)
1043 1046 except webbrowser.Error as e:
1044 1047 self.log.warn('No web browser found: %s.' % e)
1045 1048 browser = None
1046 1049
1047 1050 if self.file_to_run:
1048 1051 if not os.path.exists(self.file_to_run):
1049 1052 self.log.critical("%s does not exist" % self.file_to_run)
1050 1053 self.exit(1)
1051 1054
1052 1055 relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
1053 1056 uri = url_path_join('notebooks', *relpath.split(os.sep))
1054 1057 else:
1055 1058 uri = 'tree'
1056 1059 if browser:
1057 1060 b = lambda : browser.open(url_path_join(self.connection_url, uri),
1058 1061 new=2)
1059 1062 threading.Thread(target=b).start()
1060 1063
1061 1064 self.io_loop = ioloop.IOLoop.current()
1062 1065 try:
1063 1066 self.io_loop.start()
1064 1067 except KeyboardInterrupt:
1065 1068 info("Interrupted...")
1066 1069 finally:
1067 1070 self.cleanup_kernels()
1068 1071 self.remove_server_info_file()
1069 1072
1070 1073 def stop(self):
1071 1074 def _stop():
1072 1075 self.http_server.stop()
1073 1076 self.io_loop.stop()
1074 1077 self.io_loop.add_callback(_stop)
1075 1078
1076 1079
1077 1080 def list_running_servers(profile='default'):
1078 1081 """Iterate over the server info files of running notebook servers.
1079 1082
1080 1083 Given a profile name, find nbserver-* files in the security directory of
1081 1084 that profile, and yield dicts of their information, each one pertaining to
1082 1085 a currently running notebook server instance.
1083 1086 """
1084 1087 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
1085 1088 for file in os.listdir(pd.security_dir):
1086 1089 if file.startswith('nbserver-'):
1087 1090 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
1088 1091 info = json.load(f)
1089 1092
1090 1093 # Simple check whether that process is really still running
1091 1094 # Also remove leftover files from IPython 2.x without a pid field
1092 1095 if ('pid' in info) and check_pid(info['pid']):
1093 1096 yield info
1094 1097 else:
1095 1098 # If the process has died, try to delete its info file
1096 1099 try:
1097 1100 os.unlink(file)
1098 1101 except OSError:
1099 1102 pass # TODO: This should warn or log or something
1100 1103 #-----------------------------------------------------------------------------
1101 1104 # Main entry point
1102 1105 #-----------------------------------------------------------------------------
1103 1106
1104 1107 launch_new_instance = NotebookApp.launch_instance
1105 1108
@@ -1,119 +1,119
1 1 <!DOCTYPE HTML>
2 2 <html>
3 3
4 4 <head>
5 5 <meta charset="utf-8">
6 6
7 7 <title>{% block title %}IPython Notebook{% endblock %}</title>
8 8 {% block favicon %}<link rel="shortcut icon" type="image/x-icon" href="{{static_url("base/images/favicon.ico") }}">{% endblock %}
9 9 <meta http-equiv="X-UA-Compatible" content="IE=edge" />
10 10 <link rel="stylesheet" href="{{static_url("components/jquery-ui/themes/smoothness/jquery-ui.min.css") }}" type="text/css" />
11 11 <meta name="viewport" content="width=device-width, initial-scale=1.0">
12 12
13 13 {% block stylesheet %}
14 14 <link rel="stylesheet" href="{{ static_url("style/style.min.css") }}" type="text/css"/>
15 15 {% endblock %}
16 16 <link rel="stylesheet" href="{{ static_url("custom/custom.css") }}" type="text/css" />
17 17 <script src="{{static_url("components/es6-promise/promise.min.js")}}" type="text/javascript" charset="utf-8"></script>
18 18 <script src="{{static_url("components/requirejs/require.js") }}" type="text/javascript" charset="utf-8"></script>
19 19 <script>
20 20 require.config({
21 21 {% if version_hash %}
22 22 urlArgs: "v={{version_hash}}",
23 23 {% endif %}
24 24 baseUrl: '{{static_url("", include_version=False)}}',
25 25 paths: {
26 26 nbextensions : '{{ base_url }}nbextensions',
27 27 kernelspecs : '{{ base_url }}kernelspecs',
28 28 underscore : 'components/underscore/underscore-min',
29 29 backbone : 'components/backbone/backbone-min',
30 30 jquery: 'components/jquery/jquery.min',
31 31 bootstrap: 'components/bootstrap/js/bootstrap.min',
32 32 bootstraptour: 'components/bootstrap-tour/build/js/bootstrap-tour.min',
33 33 jqueryui: 'components/jquery-ui/ui/minified/jquery-ui.min',
34 34 moment: 'components/moment/moment',
35 35 codemirror: 'components/codemirror',
36 36 termjs: 'components/term.js/src/term',
37 37 },
38 38 shim: {
39 39 underscore: {
40 40 exports: '_'
41 41 },
42 42 backbone: {
43 43 deps: ["underscore", "jquery"],
44 44 exports: "Backbone"
45 45 },
46 46 bootstrap: {
47 47 deps: ["jquery"],
48 48 exports: "bootstrap"
49 49 },
50 50 bootstraptour: {
51 51 deps: ["bootstrap"],
52 52 exports: "Tour"
53 53 },
54 54 jqueryui: {
55 55 deps: ["jquery"],
56 56 exports: "$"
57 57 }
58 58 }
59 59 });
60 60
61 61 require.config({
62 62 map: {
63 63 '*':{
64 64 'contents': '{{ contents_js_source }}',
65 65 }
66 66 }
67 67 });
68 68 </script>
69 69
70 70 {% block meta %}
71 71 {% endblock %}
72 72
73 73 </head>
74 74
75 75 <body class="{% block bodyclasses %}{% endblock %}" {% block params %}{% endblock %}>
76 76
77 77 <noscript>
78 78 <div id='noscript'>
79 79 IPython Notebook requires JavaScript.<br>
80 80 Please enable it to proceed.
81 81 </div>
82 82 </noscript>
83 83
84 84 <div id="header">
85 85 <div id="header-container" class="container">
86 <div id="ipython_notebook" class="nav navbar-brand pull-left"><a href="{{base_url}}tree" title='dashboard'>{% block logo %}<img src='{{static_url("base/images/logo.png") }}' alt='Jupyter Notebook'/>{% endblock %}</a></div>
86 <div id="ipython_notebook" class="nav navbar-brand pull-left"><a href="{{default_url}}" title='dashboard'>{% block logo %}<img src='{{static_url("base/images/logo.png") }}' alt='Jupyter Notebook'/>{% endblock %}</a></div>
87 87
88 88 {% block login_widget %}
89 89
90 90 <span id="login_widget">
91 91 {% if logged_in %}
92 92 <button id="logout" class="btn btn-sm navbar-btn">Logout</button>
93 93 {% elif login_available and not logged_in %}
94 94 <button id="login" class="btn btn-sm navbar-btn">Login</button>
95 95 {% endif %}
96 96 </span>
97 97
98 98 {% endblock %}
99 99
100 100 {% block headercontainer %}
101 101 {% endblock %}
102 102 </div>
103 103 <div class="header-bar"></div>
104 104
105 105 {% block header %}
106 106 {% endblock %}
107 107 </div>
108 108
109 109 <div id="site">
110 110 {% block site %}
111 111 {% endblock %}
112 112 </div>
113 113
114 114 {% block script %}
115 115 {% endblock %}
116 116
117 117 </body>
118 118
119 119 </html>
General Comments 0
You need to be logged in to leave comments. Login now