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