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