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