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