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