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