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