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