##// END OF EJS Templates
Merge pull request #3088 from minrk/nbsettings...
Brian E. Granger -
r10362:dd76a232 merge
parent child Browse files
Show More
@@ -1,876 +1,927 b''
1 """Tornado handlers for the notebook.
1 """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) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-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 import Cookie
19 import Cookie
20 import datetime
20 import datetime
21 import email.utils
21 import email.utils
22 import hashlib
22 import hashlib
23 import logging
23 import logging
24 import mimetypes
24 import mimetypes
25 import os
25 import os
26 import stat
26 import stat
27 import threading
27 import threading
28 import time
28 import time
29 import uuid
29 import uuid
30
30
31 from tornado.escape import url_escape
31 from tornado.escape import url_escape
32 from tornado import web
32 from tornado import web
33 from tornado import websocket
33 from tornado import websocket
34
34
35 try:
36 from tornado.log import app_log
37 except ImportError:
38 app_log = logging.getLogger()
39
35 from zmq.eventloop import ioloop
40 from zmq.eventloop import ioloop
36 from zmq.utils import jsonapi
41 from zmq.utils import jsonapi
37
42
43 from IPython.config import Application
38 from IPython.external.decorator import decorator
44 from IPython.external.decorator import decorator
39 from IPython.kernel.zmq.session import Session
45 from IPython.kernel.zmq.session import Session
40 from IPython.lib.security import passwd_check
46 from IPython.lib.security import passwd_check
41 from IPython.utils.jsonutil import date_default
47 from IPython.utils.jsonutil import date_default
42 from IPython.utils.path import filefind
48 from IPython.utils.path import filefind
43 from IPython.utils.py3compat import PY3
49 from IPython.utils.py3compat import PY3
44
50
45 try:
51 try:
46 from docutils.core import publish_string
52 from docutils.core import publish_string
47 except ImportError:
53 except ImportError:
48 publish_string = None
54 publish_string = None
49
55
50 #-----------------------------------------------------------------------------
56 #-----------------------------------------------------------------------------
51 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
57 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
52 #-----------------------------------------------------------------------------
58 #-----------------------------------------------------------------------------
53
59
54 # Google Chrome, as of release 16, changed its websocket protocol number. The
60 # Google Chrome, as of release 16, changed its websocket protocol number. The
55 # parts tornado cares about haven't really changed, so it's OK to continue
61 # parts tornado cares about haven't really changed, so it's OK to continue
56 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
62 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
57 # version as of Oct 30/2011) the version check fails, see the issue report:
63 # version as of Oct 30/2011) the version check fails, see the issue report:
58
64
59 # https://github.com/facebook/tornado/issues/385
65 # https://github.com/facebook/tornado/issues/385
60
66
61 # This issue has been fixed in Tornado post 2.1.1:
67 # This issue has been fixed in Tornado post 2.1.1:
62
68
63 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
69 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
64
70
65 # Here we manually apply the same patch as above so that users of IPython can
71 # Here we manually apply the same patch as above so that users of IPython can
66 # continue to work with an officially released Tornado. We make the
72 # continue to work with an officially released Tornado. We make the
67 # monkeypatch version check as narrow as possible to limit its effects; once
73 # monkeypatch version check as narrow as possible to limit its effects; once
68 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
74 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
69
75
70 import tornado
76 import tornado
71
77
72 if tornado.version_info <= (2,1,1):
78 if tornado.version_info <= (2,1,1):
73
79
74 def _execute(self, transforms, *args, **kwargs):
80 def _execute(self, transforms, *args, **kwargs):
75 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
81 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
76
82
77 self.open_args = args
83 self.open_args = args
78 self.open_kwargs = kwargs
84 self.open_kwargs = kwargs
79
85
80 # The difference between version 8 and 13 is that in 8 the
86 # The difference between version 8 and 13 is that in 8 the
81 # client sends a "Sec-Websocket-Origin" header and in 13 it's
87 # client sends a "Sec-Websocket-Origin" header and in 13 it's
82 # simply "Origin".
88 # simply "Origin".
83 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
89 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
84 self.ws_connection = WebSocketProtocol8(self)
90 self.ws_connection = WebSocketProtocol8(self)
85 self.ws_connection.accept_connection()
91 self.ws_connection.accept_connection()
86
92
87 elif self.request.headers.get("Sec-WebSocket-Version"):
93 elif self.request.headers.get("Sec-WebSocket-Version"):
88 self.stream.write(tornado.escape.utf8(
94 self.stream.write(tornado.escape.utf8(
89 "HTTP/1.1 426 Upgrade Required\r\n"
95 "HTTP/1.1 426 Upgrade Required\r\n"
90 "Sec-WebSocket-Version: 8\r\n\r\n"))
96 "Sec-WebSocket-Version: 8\r\n\r\n"))
91 self.stream.close()
97 self.stream.close()
92
98
93 else:
99 else:
94 self.ws_connection = WebSocketProtocol76(self)
100 self.ws_connection = WebSocketProtocol76(self)
95 self.ws_connection.accept_connection()
101 self.ws_connection.accept_connection()
96
102
97 websocket.WebSocketHandler._execute = _execute
103 websocket.WebSocketHandler._execute = _execute
98 del _execute
104 del _execute
99
105
100 #-----------------------------------------------------------------------------
106 #-----------------------------------------------------------------------------
101 # Decorator for disabling read-only handlers
107 # Decorator for disabling read-only handlers
102 #-----------------------------------------------------------------------------
108 #-----------------------------------------------------------------------------
103
109
104 @decorator
110 @decorator
105 def not_if_readonly(f, self, *args, **kwargs):
111 def not_if_readonly(f, self, *args, **kwargs):
106 if self.application.read_only:
112 if self.settings.get('read_only', False):
107 raise web.HTTPError(403, "Notebook server is read-only")
113 raise web.HTTPError(403, "Notebook server is read-only")
108 else:
114 else:
109 return f(self, *args, **kwargs)
115 return f(self, *args, **kwargs)
110
116
111 @decorator
117 @decorator
112 def authenticate_unless_readonly(f, self, *args, **kwargs):
118 def authenticate_unless_readonly(f, self, *args, **kwargs):
113 """authenticate this page *unless* readonly view is active.
119 """authenticate this page *unless* readonly view is active.
114
120
115 In read-only mode, the notebook list and print view should
121 In read-only mode, the notebook list and print view should
116 be accessible without authentication.
122 be accessible without authentication.
117 """
123 """
118
124
119 @web.authenticated
125 @web.authenticated
120 def auth_f(self, *args, **kwargs):
126 def auth_f(self, *args, **kwargs):
121 return f(self, *args, **kwargs)
127 return f(self, *args, **kwargs)
122
128
123 if self.application.read_only:
129 if self.settings.get('read_only', False):
124 return f(self, *args, **kwargs)
130 return f(self, *args, **kwargs)
125 else:
131 else:
126 return auth_f(self, *args, **kwargs)
132 return auth_f(self, *args, **kwargs)
127
133
128 def urljoin(*pieces):
134 def urljoin(*pieces):
129 """Join componenet of url into a relative url
135 """Join components of url into a relative url
130
136
131 Use to prevent double slash when joining subpath
137 Use to prevent double slash when joining subpath
132 """
138 """
133 striped = [s.strip('/') for s in pieces]
139 striped = [s.strip('/') for s in pieces]
134 return '/'.join(s for s in striped if s)
140 return '/'.join(s for s in striped if s)
135
141
136 #-----------------------------------------------------------------------------
142 #-----------------------------------------------------------------------------
137 # Top-level handlers
143 # Top-level handlers
138 #-----------------------------------------------------------------------------
144 #-----------------------------------------------------------------------------
139
145
140 class RequestHandler(web.RequestHandler):
146 class RequestHandler(web.RequestHandler):
141 """RequestHandler with default variable setting."""
147 """RequestHandler with default variable setting."""
142
148
143 def render(*args, **kwargs):
149 def render(*args, **kwargs):
144 kwargs.setdefault('message', '')
150 kwargs.setdefault('message', '')
145 return web.RequestHandler.render(*args, **kwargs)
151 return web.RequestHandler.render(*args, **kwargs)
146
152
147 class AuthenticatedHandler(RequestHandler):
153 class AuthenticatedHandler(RequestHandler):
148 """A RequestHandler with an authenticated user."""
154 """A RequestHandler with an authenticated user."""
149
155
156 def clear_login_cookie(self):
157 self.clear_cookie(self.cookie_name)
158
150 def get_current_user(self):
159 def get_current_user(self):
151 user_id = self.get_secure_cookie(self.settings['cookie_name'])
160 user_id = self.get_secure_cookie(self.cookie_name)
152 # For now the user_id should not return empty, but it could eventually
161 # For now the user_id should not return empty, but it could eventually
153 if user_id == '':
162 if user_id == '':
154 user_id = 'anonymous'
163 user_id = 'anonymous'
155 if user_id is None:
164 if user_id is None:
156 # prevent extra Invalid cookie sig warnings:
165 # prevent extra Invalid cookie sig warnings:
157 self.clear_cookie(self.settings['cookie_name'])
166 self.clear_login_cookie()
158 if not self.application.password and not self.application.read_only:
167 if not self.read_only and not self.login_available:
159 user_id = 'anonymous'
168 user_id = 'anonymous'
160 return user_id
169 return user_id
161
170
162 @property
171 @property
172 def cookie_name(self):
173 return self.settings.get('cookie_name', '')
174
175 @property
176 def password(self):
177 """our password"""
178 return self.settings.get('password', '')
179
180 @property
163 def logged_in(self):
181 def logged_in(self):
164 """Is a user currently logged in?
182 """Is a user currently logged in?
165
183
166 """
184 """
167 user = self.get_current_user()
185 user = self.get_current_user()
168 return (user and not user == 'anonymous')
186 return (user and not user == 'anonymous')
169
187
170 @property
188 @property
171 def login_available(self):
189 def login_available(self):
172 """May a user proceed to log in?
190 """May a user proceed to log in?
173
191
174 This returns True if login capability is available, irrespective of
192 This returns True if login capability is available, irrespective of
175 whether the user is already logged in or not.
193 whether the user is already logged in or not.
176
194
177 """
195 """
178 return bool(self.application.password)
196 return bool(self.settings.get('password', ''))
179
197
180 @property
198 @property
181 def read_only(self):
199 def read_only(self):
182 """Is the notebook read-only?
200 """Is the notebook read-only?
183
201
184 """
202 """
185 return self.application.read_only
203 return self.settings.get('read_only', False)
204
186
205
206 class IPythonHandler(AuthenticatedHandler):
207 """IPython-specific extensions to authenticated handling
208
209 Mostly property shortcuts to IPython-specific settings.
210 """
211
212 @property
213 def config(self):
214 return self.settings.get('config', None)
215
216 @property
217 def log(self):
218 """use the IPython log by default, falling back on tornado's logger"""
219 if Application.initialized():
220 return Application.instance().log
221 else:
222 return app_log
223
187 @property
224 @property
188 def use_less(self):
225 def use_less(self):
189 """Use less instead of css in templates"""
226 """Use less instead of css in templates"""
190 return self.application.use_less
227 return self.settings.get('use_less', False)
191
228
229 #---------------------------------------------------------------
230 # URLs
231 #---------------------------------------------------------------
232
192 @property
233 @property
193 def ws_url(self):
234 def ws_url(self):
194 """websocket url matching the current request
235 """websocket url matching the current request
195
236
196 turns http[s]://host[:port] into
237 turns http[s]://host[:port] into
197 ws[s]://host[:port]
238 ws[s]://host[:port]
198 """
239 """
199 proto = self.request.protocol.replace('http', 'ws')
240 proto = self.request.protocol.replace('http', 'ws')
200 host = self.application.ipython_app.websocket_host # default to config value
241 host = self.settings.get('websocket_host', '')
242 # default to config value
201 if host == '':
243 if host == '':
202 host = self.request.host # get from request
244 host = self.request.host # get from request
203 return "%s://%s" % (proto, host)
245 return "%s://%s" % (proto, host)
204
246
247 @property
248 def mathjax_url(self):
249 return self.settings.get('mathjax_url', '')
250
251 @property
252 def base_project_url(self):
253 return self.settings.get('base_project_url', '/')
254
255 @property
256 def base_kernel_url(self):
257 return self.settings.get('base_kernel_url', '/')
258
259 #---------------------------------------------------------------
260 # Manager objects
261 #---------------------------------------------------------------
262
263 @property
264 def kernel_manager(self):
265 return self.settings['kernel_manager']
266
267 @property
268 def notebook_manager(self):
269 return self.settings['notebook_manager']
270
271 @property
272 def cluster_manager(self):
273 return self.settings['cluster_manager']
274
275 @property
276 def project(self):
277 return self.notebook_manager.notebook_dir
278
279 #---------------------------------------------------------------
280 # template rendering
281 #---------------------------------------------------------------
282
283 def get_template(self, name):
284 """Return the jinja template object for a given name"""
285 return self.settings['jinja2_env'].get_template(name)
286
287 def render_template(self, name, **ns):
288 ns.update(self.template_namespace)
289 template = self.get_template(name)
290 return template.render(**ns)
291
292 @property
293 def template_namespace(self):
294 return dict(
295 base_project_url=self.base_project_url,
296 base_kernel_url=self.base_kernel_url,
297 read_only=self.read_only,
298 logged_in=self.logged_in,
299 login_available=self.login_available,
300 use_less=self.use_less,
301 )
205
302
206 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
303 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
207 """static files should only be accessible when logged in"""
304 """static files should only be accessible when logged in"""
208
305
209 @authenticate_unless_readonly
306 @authenticate_unless_readonly
210 def get(self, path):
307 def get(self, path):
211 return web.StaticFileHandler.get(self, path)
308 return web.StaticFileHandler.get(self, path)
212
309
213
310
214 class ProjectDashboardHandler(AuthenticatedHandler):
311 class ProjectDashboardHandler(IPythonHandler):
215
312
216 @authenticate_unless_readonly
313 @authenticate_unless_readonly
217 def get(self):
314 def get(self):
218 nbm = self.application.notebook_manager
315 self.write(self.render_template('projectdashboard.html',
219 project = nbm.notebook_dir
316 project=self.project,
220 template = self.application.jinja2_env.get_template('projectdashboard.html')
317 project_component=self.project.split('/'),
221 self.write( template.render(
318 ))
222 project=project,
223 project_component=project.split('/'),
224 base_project_url=self.application.ipython_app.base_project_url,
225 base_kernel_url=self.application.ipython_app.base_kernel_url,
226 read_only=self.read_only,
227 logged_in=self.logged_in,
228 use_less=self.use_less,
229 login_available=self.login_available))
230
319
231
320
232 class LoginHandler(AuthenticatedHandler):
321 class LoginHandler(IPythonHandler):
233
322
234 def _render(self, message=None):
323 def _render(self, message=None):
235 template = self.application.jinja2_env.get_template('login.html')
324 self.write(self.render_template('login.html',
236 self.write( template.render(
325 next=url_escape(self.get_argument('next', default=self.base_project_url)),
237 next=url_escape(self.get_argument('next', default=self.application.ipython_app.base_project_url)),
326 message=message,
238 read_only=self.read_only,
239 logged_in=self.logged_in,
240 login_available=self.login_available,
241 base_project_url=self.application.ipython_app.base_project_url,
242 message=message
243 ))
327 ))
244
328
245 def get(self):
329 def get(self):
246 if self.current_user:
330 if self.current_user:
247 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
331 self.redirect(self.get_argument('next', default=self.base_project_url))
248 else:
332 else:
249 self._render()
333 self._render()
250
334
251 def post(self):
335 def post(self):
252 pwd = self.get_argument('password', default=u'')
336 pwd = self.get_argument('password', default=u'')
253 if self.application.password:
337 if self.login_available:
254 if passwd_check(self.application.password, pwd):
338 if passwd_check(self.password, pwd):
255 self.set_secure_cookie(self.settings['cookie_name'], str(uuid.uuid4()))
339 self.set_secure_cookie(self.cookie_name, str(uuid.uuid4()))
256 else:
340 else:
257 self._render(message={'error': 'Invalid password'})
341 self._render(message={'error': 'Invalid password'})
258 return
342 return
259
343
260 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
344 self.redirect(self.get_argument('next', default=self.base_project_url))
261
345
262
346
263 class LogoutHandler(AuthenticatedHandler):
347 class LogoutHandler(IPythonHandler):
264
348
265 def get(self):
349 def get(self):
266 self.clear_cookie(self.settings['cookie_name'])
350 self.clear_login_cookie()
267 if self.login_available:
351 if self.login_available:
268 message = {'info': 'Successfully logged out.'}
352 message = {'info': 'Successfully logged out.'}
269 else:
353 else:
270 message = {'warning': 'Cannot log out. Notebook authentication '
354 message = {'warning': 'Cannot log out. Notebook authentication '
271 'is disabled.'}
355 'is disabled.'}
272 template = self.application.jinja2_env.get_template('logout.html')
356 self.write(self.render_template('logout.html',
273 self.write( template.render(
274 read_only=self.read_only,
275 logged_in=self.logged_in,
276 login_available=self.login_available,
277 base_project_url=self.application.ipython_app.base_project_url,
278 message=message))
357 message=message))
279
358
280
359
281 class NewHandler(AuthenticatedHandler):
360 class NewHandler(IPythonHandler):
282
361
283 @web.authenticated
362 @web.authenticated
284 def get(self):
363 def get(self):
285 nbm = self.application.notebook_manager
364 notebook_id = self.notebook_manager.new_notebook()
286 project = nbm.notebook_dir
365 self.redirect('/' + urljoin(self.base_project_url, notebook_id))
287 notebook_id = nbm.new_notebook()
288 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
289
366
290 class NamedNotebookHandler(AuthenticatedHandler):
367 class NamedNotebookHandler(IPythonHandler):
291
368
292 @authenticate_unless_readonly
369 @authenticate_unless_readonly
293 def get(self, notebook_id):
370 def get(self, notebook_id):
294 nbm = self.application.notebook_manager
371 nbm = self.notebook_manager
295 project = nbm.notebook_dir
296 if not nbm.notebook_exists(notebook_id):
372 if not nbm.notebook_exists(notebook_id):
297 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
373 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
298 template = self.application.jinja2_env.get_template('notebook.html')
374 self.write(self.render_template('notebook.html',
299 self.write( template.render(
375 project=self.project,
300 project=project,
301 notebook_id=notebook_id,
376 notebook_id=notebook_id,
302 base_project_url=self.application.ipython_app.base_project_url,
303 base_kernel_url=self.application.ipython_app.base_kernel_url,
304 kill_kernel=False,
377 kill_kernel=False,
305 read_only=self.read_only,
378 mathjax_url=self.mathjax_url,
306 logged_in=self.logged_in,
307 login_available=self.login_available,
308 mathjax_url=self.application.ipython_app.mathjax_url,
309 use_less=self.use_less
310 )
379 )
311 )
380 )
312
381
313
382
314 class PrintNotebookHandler(AuthenticatedHandler):
383 class PrintNotebookHandler(IPythonHandler):
315
384
316 @authenticate_unless_readonly
385 @authenticate_unless_readonly
317 def get(self, notebook_id):
386 def get(self, notebook_id):
318 nbm = self.application.notebook_manager
387 if not self.notebook_manager.notebook_exists(notebook_id):
319 project = nbm.notebook_dir
320 if not nbm.notebook_exists(notebook_id):
321 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
388 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
322 template = self.application.jinja2_env.get_template('printnotebook.html')
389 self.write( self.render_template('printnotebook.html',
323 self.write( template.render(
390 project=self.project,
324 project=project,
325 notebook_id=notebook_id,
391 notebook_id=notebook_id,
326 base_project_url=self.application.ipython_app.base_project_url,
327 base_kernel_url=self.application.ipython_app.base_kernel_url,
328 kill_kernel=False,
392 kill_kernel=False,
329 read_only=self.read_only,
393 mathjax_url=self.mathjax_url,
330 logged_in=self.logged_in,
331 login_available=self.login_available,
332 mathjax_url=self.application.ipython_app.mathjax_url,
333 ))
394 ))
334
395
335 #-----------------------------------------------------------------------------
396 #-----------------------------------------------------------------------------
336 # Kernel handlers
397 # Kernel handlers
337 #-----------------------------------------------------------------------------
398 #-----------------------------------------------------------------------------
338
399
339
400
340 class MainKernelHandler(AuthenticatedHandler):
401 class MainKernelHandler(IPythonHandler):
341
402
342 @web.authenticated
403 @web.authenticated
343 def get(self):
404 def get(self):
344 km = self.application.kernel_manager
405 km = self.kernel_manager
345 self.finish(jsonapi.dumps(km.list_kernel_ids()))
406 self.finish(jsonapi.dumps(km.list_kernel_ids()))
346
407
347 @web.authenticated
408 @web.authenticated
348 def post(self):
409 def post(self):
349 km = self.application.kernel_manager
410 km = self.kernel_manager
350 nbm = self.application.notebook_manager
411 nbm = self.notebook_manager
351 notebook_id = self.get_argument('notebook', default=None)
412 notebook_id = self.get_argument('notebook', default=None)
352 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
413 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
353 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
414 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
354 self.set_header('Location', '/'+kernel_id)
415 self.set_header('Location', '/'+kernel_id)
355 self.finish(jsonapi.dumps(data))
416 self.finish(jsonapi.dumps(data))
356
417
357
418
358 class KernelHandler(AuthenticatedHandler):
419 class KernelHandler(IPythonHandler):
359
420
360 SUPPORTED_METHODS = ('DELETE')
421 SUPPORTED_METHODS = ('DELETE')
361
422
362 @web.authenticated
423 @web.authenticated
363 def delete(self, kernel_id):
424 def delete(self, kernel_id):
364 km = self.application.kernel_manager
425 km = self.kernel_manager
365 km.shutdown_kernel(kernel_id)
426 km.shutdown_kernel(kernel_id)
366 self.set_status(204)
427 self.set_status(204)
367 self.finish()
428 self.finish()
368
429
369
430
370 class KernelActionHandler(AuthenticatedHandler):
431 class KernelActionHandler(IPythonHandler):
371
432
372 @web.authenticated
433 @web.authenticated
373 def post(self, kernel_id, action):
434 def post(self, kernel_id, action):
374 km = self.application.kernel_manager
435 km = self.kernel_manager
375 if action == 'interrupt':
436 if action == 'interrupt':
376 km.interrupt_kernel(kernel_id)
437 km.interrupt_kernel(kernel_id)
377 self.set_status(204)
438 self.set_status(204)
378 if action == 'restart':
439 if action == 'restart':
379 km.restart_kernel(kernel_id)
440 km.restart_kernel(kernel_id)
380 data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
441 data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
381 self.set_header('Location', '/'+kernel_id)
442 self.set_header('Location', '/'+kernel_id)
382 self.write(jsonapi.dumps(data))
443 self.write(jsonapi.dumps(data))
383 self.finish()
444 self.finish()
384
445
385
446
386 class ZMQStreamHandler(websocket.WebSocketHandler):
447 class ZMQStreamHandler(websocket.WebSocketHandler):
448
449 def clear_cookie(self, *args, **kwargs):
450 """meaningless for websockets"""
451 pass
387
452
388 def _reserialize_reply(self, msg_list):
453 def _reserialize_reply(self, msg_list):
389 """Reserialize a reply message using JSON.
454 """Reserialize a reply message using JSON.
390
455
391 This takes the msg list from the ZMQ socket, unserializes it using
456 This takes the msg list from the ZMQ socket, unserializes it using
392 self.session and then serializes the result using JSON. This method
457 self.session and then serializes the result using JSON. This method
393 should be used by self._on_zmq_reply to build messages that can
458 should be used by self._on_zmq_reply to build messages that can
394 be sent back to the browser.
459 be sent back to the browser.
395 """
460 """
396 idents, msg_list = self.session.feed_identities(msg_list)
461 idents, msg_list = self.session.feed_identities(msg_list)
397 msg = self.session.unserialize(msg_list)
462 msg = self.session.unserialize(msg_list)
398 try:
463 try:
399 msg['header'].pop('date')
464 msg['header'].pop('date')
400 except KeyError:
465 except KeyError:
401 pass
466 pass
402 try:
467 try:
403 msg['parent_header'].pop('date')
468 msg['parent_header'].pop('date')
404 except KeyError:
469 except KeyError:
405 pass
470 pass
406 msg.pop('buffers')
471 msg.pop('buffers')
407 return jsonapi.dumps(msg, default=date_default)
472 return jsonapi.dumps(msg, default=date_default)
408
473
409 def _on_zmq_reply(self, msg_list):
474 def _on_zmq_reply(self, msg_list):
410 # Sometimes this gets triggered when the on_close method is scheduled in the
475 # Sometimes this gets triggered when the on_close method is scheduled in the
411 # eventloop but hasn't been called.
476 # eventloop but hasn't been called.
412 if self.stream.closed(): return
477 if self.stream.closed(): return
413 try:
478 try:
414 msg = self._reserialize_reply(msg_list)
479 msg = self._reserialize_reply(msg_list)
415 except Exception:
480 except Exception:
416 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
481 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
417 else:
482 else:
418 self.write_message(msg)
483 self.write_message(msg)
419
484
420 def allow_draft76(self):
485 def allow_draft76(self):
421 """Allow draft 76, until browsers such as Safari update to RFC 6455.
486 """Allow draft 76, until browsers such as Safari update to RFC 6455.
422
487
423 This has been disabled by default in tornado in release 2.2.0, and
488 This has been disabled by default in tornado in release 2.2.0, and
424 support will be removed in later versions.
489 support will be removed in later versions.
425 """
490 """
426 return True
491 return True
427
492
428
493
429 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
494 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
430
495
431 def open(self, kernel_id):
496 def open(self, kernel_id):
432 self.kernel_id = kernel_id.decode('ascii')
497 self.kernel_id = kernel_id.decode('ascii')
433 try:
498 self.session = Session(config=self.config)
434 cfg = self.application.config
435 except AttributeError:
436 # protect from the case where this is run from something other than
437 # the notebook app:
438 cfg = None
439 self.session = Session(config=cfg)
440 self.save_on_message = self.on_message
499 self.save_on_message = self.on_message
441 self.on_message = self.on_first_message
500 self.on_message = self.on_first_message
442
501
443 def get_current_user(self):
444 user_id = self.get_secure_cookie(self.settings['cookie_name'])
445 if user_id == '' or (user_id is None and not self.application.password):
446 user_id = 'anonymous'
447 return user_id
448
449 def _inject_cookie_message(self, msg):
502 def _inject_cookie_message(self, msg):
450 """Inject the first message, which is the document cookie,
503 """Inject the first message, which is the document cookie,
451 for authentication."""
504 for authentication."""
452 if not PY3 and isinstance(msg, unicode):
505 if not PY3 and isinstance(msg, unicode):
453 # Cookie constructor doesn't accept unicode strings
506 # Cookie constructor doesn't accept unicode strings
454 # under Python 2.x for some reason
507 # under Python 2.x for some reason
455 msg = msg.encode('utf8', 'replace')
508 msg = msg.encode('utf8', 'replace')
456 try:
509 try:
457 self.request._cookies = Cookie.SimpleCookie(msg)
510 self.request._cookies = Cookie.SimpleCookie(msg)
458 except:
511 except:
459 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
512 self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
460
513
461 def on_first_message(self, msg):
514 def on_first_message(self, msg):
462 self._inject_cookie_message(msg)
515 self._inject_cookie_message(msg)
463 if self.get_current_user() is None:
516 if self.get_current_user() is None:
464 logging.warn("Couldn't authenticate WebSocket connection")
517 self.log.warn("Couldn't authenticate WebSocket connection")
465 raise web.HTTPError(403)
518 raise web.HTTPError(403)
466 self.on_message = self.save_on_message
519 self.on_message = self.save_on_message
467
520
468
521
469 class IOPubHandler(AuthenticatedZMQStreamHandler):
522 class IOPubHandler(AuthenticatedZMQStreamHandler):
470
523
471 def initialize(self, *args, **kwargs):
524 def initialize(self, *args, **kwargs):
472 self.iopub_stream = None
525 self.iopub_stream = None
473
526
474 def on_first_message(self, msg):
527 def on_first_message(self, msg):
475 try:
528 try:
476 super(IOPubHandler, self).on_first_message(msg)
529 super(IOPubHandler, self).on_first_message(msg)
477 except web.HTTPError:
530 except web.HTTPError:
478 self.close()
531 self.close()
479 return
532 return
480 km = self.application.kernel_manager
533 km = self.kernel_manager
481 kernel_id = self.kernel_id
534 kernel_id = self.kernel_id
482 km.add_restart_callback(kernel_id, self.on_kernel_restarted)
535 km.add_restart_callback(kernel_id, self.on_kernel_restarted)
483 km.add_restart_callback(kernel_id, self.on_restart_failed, 'dead')
536 km.add_restart_callback(kernel_id, self.on_restart_failed, 'dead')
484 try:
537 try:
485 self.iopub_stream = km.connect_iopub(kernel_id)
538 self.iopub_stream = km.connect_iopub(kernel_id)
486 except web.HTTPError:
539 except web.HTTPError:
487 # WebSockets don't response to traditional error codes so we
540 # WebSockets don't response to traditional error codes so we
488 # close the connection.
541 # close the connection.
489 if not self.stream.closed():
542 if not self.stream.closed():
490 self.stream.close()
543 self.stream.close()
491 self.close()
544 self.close()
492 else:
545 else:
493 self.iopub_stream.on_recv(self._on_zmq_reply)
546 self.iopub_stream.on_recv(self._on_zmq_reply)
494
547
495 def on_message(self, msg):
548 def on_message(self, msg):
496 pass
549 pass
497
550
498 def _send_status_message(self, status):
551 def _send_status_message(self, status):
499 msg = self.session.msg("status",
552 msg = self.session.msg("status",
500 {'execution_state': status}
553 {'execution_state': status}
501 )
554 )
502 self.write_message(jsonapi.dumps(msg, default=date_default))
555 self.write_message(jsonapi.dumps(msg, default=date_default))
503
556
504 def on_kernel_restarted(self):
557 def on_kernel_restarted(self):
505 logging.warn("kernel %s restarted", self.kernel_id)
558 self.log.warn("kernel %s restarted", self.kernel_id)
506 self._send_status_message('restarting')
559 self._send_status_message('restarting')
507
560
508 def on_restart_failed(self):
561 def on_restart_failed(self):
509 logging.error("kernel %s restarted failed!", self.kernel_id)
562 self.log.error("kernel %s restarted failed!", self.kernel_id)
510 self._send_status_message('dead')
563 self._send_status_message('dead')
511
564
512 def on_close(self):
565 def on_close(self):
513 # This method can be called twice, once by self.kernel_died and once
566 # This method can be called twice, once by self.kernel_died and once
514 # from the WebSocket close event. If the WebSocket connection is
567 # from the WebSocket close event. If the WebSocket connection is
515 # closed before the ZMQ streams are setup, they could be None.
568 # closed before the ZMQ streams are setup, they could be None.
516 km = self.application.kernel_manager
569 km = self.kernel_manager
517 if self.kernel_id in km:
570 if self.kernel_id in km:
518 km.remove_restart_callback(
571 km.remove_restart_callback(
519 self.kernel_id, self.on_kernel_restarted,
572 self.kernel_id, self.on_kernel_restarted,
520 )
573 )
521 km.remove_restart_callback(
574 km.remove_restart_callback(
522 self.kernel_id, self.on_restart_failed, 'dead',
575 self.kernel_id, self.on_restart_failed, 'dead',
523 )
576 )
524 if self.iopub_stream is not None and not self.iopub_stream.closed():
577 if self.iopub_stream is not None and not self.iopub_stream.closed():
525 self.iopub_stream.on_recv(None)
578 self.iopub_stream.on_recv(None)
526 self.iopub_stream.close()
579 self.iopub_stream.close()
527
580
528
581
529 class ShellHandler(AuthenticatedZMQStreamHandler):
582 class ShellHandler(AuthenticatedZMQStreamHandler):
583
584 @property
585 def max_msg_size(self):
586 return self.settings.get('max_msg_size', 65535)
530
587
531 def initialize(self, *args, **kwargs):
588 def initialize(self, *args, **kwargs):
532 self.shell_stream = None
589 self.shell_stream = None
533
590
534 def on_first_message(self, msg):
591 def on_first_message(self, msg):
535 try:
592 try:
536 super(ShellHandler, self).on_first_message(msg)
593 super(ShellHandler, self).on_first_message(msg)
537 except web.HTTPError:
594 except web.HTTPError:
538 self.close()
595 self.close()
539 return
596 return
540 km = self.application.kernel_manager
597 km = self.kernel_manager
541 self.max_msg_size = km.max_msg_size
542 kernel_id = self.kernel_id
598 kernel_id = self.kernel_id
543 try:
599 try:
544 self.shell_stream = km.connect_shell(kernel_id)
600 self.shell_stream = km.connect_shell(kernel_id)
545 except web.HTTPError:
601 except web.HTTPError:
546 # WebSockets don't response to traditional error codes so we
602 # WebSockets don't response to traditional error codes so we
547 # close the connection.
603 # close the connection.
548 if not self.stream.closed():
604 if not self.stream.closed():
549 self.stream.close()
605 self.stream.close()
550 self.close()
606 self.close()
551 else:
607 else:
552 self.shell_stream.on_recv(self._on_zmq_reply)
608 self.shell_stream.on_recv(self._on_zmq_reply)
553
609
554 def on_message(self, msg):
610 def on_message(self, msg):
555 if len(msg) < self.max_msg_size:
611 if len(msg) < self.max_msg_size:
556 msg = jsonapi.loads(msg)
612 msg = jsonapi.loads(msg)
557 self.session.send(self.shell_stream, msg)
613 self.session.send(self.shell_stream, msg)
558
614
559 def on_close(self):
615 def on_close(self):
560 # Make sure the stream exists and is not already closed.
616 # Make sure the stream exists and is not already closed.
561 if self.shell_stream is not None and not self.shell_stream.closed():
617 if self.shell_stream is not None and not self.shell_stream.closed():
562 self.shell_stream.close()
618 self.shell_stream.close()
563
619
564
620
565 #-----------------------------------------------------------------------------
621 #-----------------------------------------------------------------------------
566 # Notebook web service handlers
622 # Notebook web service handlers
567 #-----------------------------------------------------------------------------
623 #-----------------------------------------------------------------------------
568
624
569 class NotebookRedirectHandler(AuthenticatedHandler):
625 class NotebookRedirectHandler(IPythonHandler):
570
626
571 @authenticate_unless_readonly
627 @authenticate_unless_readonly
572 def get(self, notebook_name):
628 def get(self, notebook_name):
573 app = self.application
574 # strip trailing .ipynb:
629 # strip trailing .ipynb:
575 notebook_name = os.path.splitext(notebook_name)[0]
630 notebook_name = os.path.splitext(notebook_name)[0]
576 notebook_id = app.notebook_manager.rev_mapping.get(notebook_name, '')
631 notebook_id = self.notebook_manager.rev_mapping.get(notebook_name, '')
577 if notebook_id:
632 if notebook_id:
578 url = self.settings.get('base_project_url', '/') + notebook_id
633 url = self.settings.get('base_project_url', '/') + notebook_id
579 return self.redirect(url)
634 return self.redirect(url)
580 else:
635 else:
581 raise HTTPError(404)
636 raise HTTPError(404)
582
637
583 class NotebookRootHandler(AuthenticatedHandler):
638
639 class NotebookRootHandler(IPythonHandler):
584
640
585 @authenticate_unless_readonly
641 @authenticate_unless_readonly
586 def get(self):
642 def get(self):
587 nbm = self.application.notebook_manager
643 nbm = self.notebook_manager
588 km = self.application.kernel_manager
644 km = self.kernel_manager
589 files = nbm.list_notebooks()
645 files = nbm.list_notebooks()
590 for f in files :
646 for f in files :
591 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
647 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
592 self.finish(jsonapi.dumps(files))
648 self.finish(jsonapi.dumps(files))
593
649
594 @web.authenticated
650 @web.authenticated
595 def post(self):
651 def post(self):
596 nbm = self.application.notebook_manager
652 nbm = self.notebook_manager
597 body = self.request.body.strip()
653 body = self.request.body.strip()
598 format = self.get_argument('format', default='json')
654 format = self.get_argument('format', default='json')
599 name = self.get_argument('name', default=None)
655 name = self.get_argument('name', default=None)
600 if body:
656 if body:
601 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
657 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
602 else:
658 else:
603 notebook_id = nbm.new_notebook()
659 notebook_id = nbm.new_notebook()
604 self.set_header('Location', '/'+notebook_id)
660 self.set_header('Location', '/'+notebook_id)
605 self.finish(jsonapi.dumps(notebook_id))
661 self.finish(jsonapi.dumps(notebook_id))
606
662
607
663
608 class NotebookHandler(AuthenticatedHandler):
664 class NotebookHandler(IPythonHandler):
609
665
610 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
666 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
611
667
612 @authenticate_unless_readonly
668 @authenticate_unless_readonly
613 def get(self, notebook_id):
669 def get(self, notebook_id):
614 nbm = self.application.notebook_manager
670 nbm = self.notebook_manager
615 format = self.get_argument('format', default='json')
671 format = self.get_argument('format', default='json')
616 last_mod, name, data = nbm.get_notebook(notebook_id, format)
672 last_mod, name, data = nbm.get_notebook(notebook_id, format)
617
673
618 if format == u'json':
674 if format == u'json':
619 self.set_header('Content-Type', 'application/json')
675 self.set_header('Content-Type', 'application/json')
620 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
676 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
621 elif format == u'py':
677 elif format == u'py':
622 self.set_header('Content-Type', 'application/x-python')
678 self.set_header('Content-Type', 'application/x-python')
623 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
679 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
624 self.set_header('Last-Modified', last_mod)
680 self.set_header('Last-Modified', last_mod)
625 self.finish(data)
681 self.finish(data)
626
682
627 @web.authenticated
683 @web.authenticated
628 def put(self, notebook_id):
684 def put(self, notebook_id):
629 nbm = self.application.notebook_manager
685 nbm = self.notebook_manager
630 format = self.get_argument('format', default='json')
686 format = self.get_argument('format', default='json')
631 name = self.get_argument('name', default=None)
687 name = self.get_argument('name', default=None)
632 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
688 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
633 self.set_status(204)
689 self.set_status(204)
634 self.finish()
690 self.finish()
635
691
636 @web.authenticated
692 @web.authenticated
637 def delete(self, notebook_id):
693 def delete(self, notebook_id):
638 nbm = self.application.notebook_manager
694 self.notebook_manager.delete_notebook(notebook_id)
639 nbm.delete_notebook(notebook_id)
640 self.set_status(204)
695 self.set_status(204)
641 self.finish()
696 self.finish()
642
697
643
698
644 class NotebookCopyHandler(AuthenticatedHandler):
699 class NotebookCopyHandler(IPythonHandler):
645
700
646 @web.authenticated
701 @web.authenticated
647 def get(self, notebook_id):
702 def get(self, notebook_id):
648 nbm = self.application.notebook_manager
703 notebook_id = self.notebook_manager.copy_notebook(notebook_id)
649 project = nbm.notebook_dir
704 self.redirect('/'+urljoin(self.base_project_url, notebook_id))
650 notebook_id = nbm.copy_notebook(notebook_id)
651 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
652
705
653
706
654 #-----------------------------------------------------------------------------
707 #-----------------------------------------------------------------------------
655 # Cluster handlers
708 # Cluster handlers
656 #-----------------------------------------------------------------------------
709 #-----------------------------------------------------------------------------
657
710
658
711
659 class MainClusterHandler(AuthenticatedHandler):
712 class MainClusterHandler(IPythonHandler):
660
713
661 @web.authenticated
714 @web.authenticated
662 def get(self):
715 def get(self):
663 cm = self.application.cluster_manager
716 self.finish(jsonapi.dumps(self.cluster_manager.list_profiles()))
664 self.finish(jsonapi.dumps(cm.list_profiles()))
665
717
666
718
667 class ClusterProfileHandler(AuthenticatedHandler):
719 class ClusterProfileHandler(IPythonHandler):
668
720
669 @web.authenticated
721 @web.authenticated
670 def get(self, profile):
722 def get(self, profile):
671 cm = self.application.cluster_manager
723 self.finish(jsonapi.dumps(self.cluster_manager.profile_info(profile)))
672 self.finish(jsonapi.dumps(cm.profile_info(profile)))
673
724
674
725
675 class ClusterActionHandler(AuthenticatedHandler):
726 class ClusterActionHandler(IPythonHandler):
676
727
677 @web.authenticated
728 @web.authenticated
678 def post(self, profile, action):
729 def post(self, profile, action):
679 cm = self.application.cluster_manager
730 cm = self.cluster_manager
680 if action == 'start':
731 if action == 'start':
681 n = self.get_argument('n',default=None)
732 n = self.get_argument('n',default=None)
682 if n is None:
733 if n is None:
683 data = cm.start_cluster(profile)
734 data = cm.start_cluster(profile)
684 else:
735 else:
685 data = cm.start_cluster(profile,int(n))
736 data = cm.start_cluster(profile, int(n))
686 if action == 'stop':
737 if action == 'stop':
687 data = cm.stop_cluster(profile)
738 data = cm.stop_cluster(profile)
688 self.finish(jsonapi.dumps(data))
739 self.finish(jsonapi.dumps(data))
689
740
690
741
691 #-----------------------------------------------------------------------------
742 #-----------------------------------------------------------------------------
692 # RST web service handlers
743 # RST web service handlers
693 #-----------------------------------------------------------------------------
744 #-----------------------------------------------------------------------------
694
745
695
746
696 class RSTHandler(AuthenticatedHandler):
747 class RSTHandler(IPythonHandler):
697
748
698 @web.authenticated
749 @web.authenticated
699 def post(self):
750 def post(self):
700 if publish_string is None:
751 if publish_string is None:
701 raise web.HTTPError(503, u'docutils not available')
752 raise web.HTTPError(503, u'docutils not available')
702 body = self.request.body.strip()
753 body = self.request.body.strip()
703 source = body
754 source = body
704 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
755 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
705 defaults = {'file_insertion_enabled': 0,
756 defaults = {'file_insertion_enabled': 0,
706 'raw_enabled': 0,
757 'raw_enabled': 0,
707 '_disable_config': 1,
758 '_disable_config': 1,
708 'stylesheet_path': 0
759 'stylesheet_path': 0
709 # 'template': template_path
760 # 'template': template_path
710 }
761 }
711 try:
762 try:
712 html = publish_string(source, writer_name='html',
763 html = publish_string(source, writer_name='html',
713 settings_overrides=defaults
764 settings_overrides=defaults
714 )
765 )
715 except:
766 except:
716 raise web.HTTPError(400, u'Invalid RST')
767 raise web.HTTPError(400, u'Invalid RST')
717 print html
768 print html
718 self.set_header('Content-Type', 'text/html')
769 self.set_header('Content-Type', 'text/html')
719 self.finish(html)
770 self.finish(html)
720
771
721 # to minimize subclass changes:
772 # to minimize subclass changes:
722 HTTPError = web.HTTPError
773 HTTPError = web.HTTPError
723
774
724 class FileFindHandler(web.StaticFileHandler):
775 class FileFindHandler(web.StaticFileHandler):
725 """subclass of StaticFileHandler for serving files from a search path"""
776 """subclass of StaticFileHandler for serving files from a search path"""
726
777
727 _static_paths = {}
778 _static_paths = {}
728 # _lock is needed for tornado < 2.2.0 compat
779 # _lock is needed for tornado < 2.2.0 compat
729 _lock = threading.Lock() # protects _static_hashes
780 _lock = threading.Lock() # protects _static_hashes
730
781
731 def initialize(self, path, default_filename=None):
782 def initialize(self, path, default_filename=None):
732 if isinstance(path, basestring):
783 if isinstance(path, basestring):
733 path = [path]
784 path = [path]
734 self.roots = tuple(
785 self.roots = tuple(
735 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
786 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
736 )
787 )
737 self.default_filename = default_filename
788 self.default_filename = default_filename
738
789
739 @classmethod
790 @classmethod
740 def locate_file(cls, path, roots):
791 def locate_file(cls, path, roots):
741 """locate a file to serve on our static file search path"""
792 """locate a file to serve on our static file search path"""
742 with cls._lock:
793 with cls._lock:
743 if path in cls._static_paths:
794 if path in cls._static_paths:
744 return cls._static_paths[path]
795 return cls._static_paths[path]
745 try:
796 try:
746 abspath = os.path.abspath(filefind(path, roots))
797 abspath = os.path.abspath(filefind(path, roots))
747 except IOError:
798 except IOError:
748 # empty string should always give exists=False
799 # empty string should always give exists=False
749 return ''
800 return ''
750
801
751 # os.path.abspath strips a trailing /
802 # os.path.abspath strips a trailing /
752 # it needs to be temporarily added back for requests to root/
803 # it needs to be temporarily added back for requests to root/
753 if not (abspath + os.path.sep).startswith(roots):
804 if not (abspath + os.path.sep).startswith(roots):
754 raise HTTPError(403, "%s is not in root static directory", path)
805 raise HTTPError(403, "%s is not in root static directory", path)
755
806
756 cls._static_paths[path] = abspath
807 cls._static_paths[path] = abspath
757 return abspath
808 return abspath
758
809
759 def get(self, path, include_body=True):
810 def get(self, path, include_body=True):
760 path = self.parse_url_path(path)
811 path = self.parse_url_path(path)
761
812
762 # begin subclass override
813 # begin subclass override
763 abspath = self.locate_file(path, self.roots)
814 abspath = self.locate_file(path, self.roots)
764 # end subclass override
815 # end subclass override
765
816
766 if os.path.isdir(abspath) and self.default_filename is not None:
817 if os.path.isdir(abspath) and self.default_filename is not None:
767 # need to look at the request.path here for when path is empty
818 # need to look at the request.path here for when path is empty
768 # but there is some prefix to the path that was already
819 # but there is some prefix to the path that was already
769 # trimmed by the routing
820 # trimmed by the routing
770 if not self.request.path.endswith("/"):
821 if not self.request.path.endswith("/"):
771 self.redirect(self.request.path + "/")
822 self.redirect(self.request.path + "/")
772 return
823 return
773 abspath = os.path.join(abspath, self.default_filename)
824 abspath = os.path.join(abspath, self.default_filename)
774 if not os.path.exists(abspath):
825 if not os.path.exists(abspath):
775 raise HTTPError(404)
826 raise HTTPError(404)
776 if not os.path.isfile(abspath):
827 if not os.path.isfile(abspath):
777 raise HTTPError(403, "%s is not a file", path)
828 raise HTTPError(403, "%s is not a file", path)
778
829
779 stat_result = os.stat(abspath)
830 stat_result = os.stat(abspath)
780 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
831 modified = datetime.datetime.utcfromtimestamp(stat_result[stat.ST_MTIME])
781
832
782 self.set_header("Last-Modified", modified)
833 self.set_header("Last-Modified", modified)
783
834
784 mime_type, encoding = mimetypes.guess_type(abspath)
835 mime_type, encoding = mimetypes.guess_type(abspath)
785 if mime_type:
836 if mime_type:
786 self.set_header("Content-Type", mime_type)
837 self.set_header("Content-Type", mime_type)
787
838
788 cache_time = self.get_cache_time(path, modified, mime_type)
839 cache_time = self.get_cache_time(path, modified, mime_type)
789
840
790 if cache_time > 0:
841 if cache_time > 0:
791 self.set_header("Expires", datetime.datetime.utcnow() + \
842 self.set_header("Expires", datetime.datetime.utcnow() + \
792 datetime.timedelta(seconds=cache_time))
843 datetime.timedelta(seconds=cache_time))
793 self.set_header("Cache-Control", "max-age=" + str(cache_time))
844 self.set_header("Cache-Control", "max-age=" + str(cache_time))
794 else:
845 else:
795 self.set_header("Cache-Control", "public")
846 self.set_header("Cache-Control", "public")
796
847
797 self.set_extra_headers(path)
848 self.set_extra_headers(path)
798
849
799 # Check the If-Modified-Since, and don't send the result if the
850 # Check the If-Modified-Since, and don't send the result if the
800 # content has not been modified
851 # content has not been modified
801 ims_value = self.request.headers.get("If-Modified-Since")
852 ims_value = self.request.headers.get("If-Modified-Since")
802 if ims_value is not None:
853 if ims_value is not None:
803 date_tuple = email.utils.parsedate(ims_value)
854 date_tuple = email.utils.parsedate(ims_value)
804 if_since = datetime.datetime(*date_tuple[:6])
855 if_since = datetime.datetime(*date_tuple[:6])
805 if if_since >= modified:
856 if if_since >= modified:
806 self.set_status(304)
857 self.set_status(304)
807 return
858 return
808
859
809 with open(abspath, "rb") as file:
860 with open(abspath, "rb") as file:
810 data = file.read()
861 data = file.read()
811 hasher = hashlib.sha1()
862 hasher = hashlib.sha1()
812 hasher.update(data)
863 hasher.update(data)
813 self.set_header("Etag", '"%s"' % hasher.hexdigest())
864 self.set_header("Etag", '"%s"' % hasher.hexdigest())
814 if include_body:
865 if include_body:
815 self.write(data)
866 self.write(data)
816 else:
867 else:
817 assert self.request.method == "HEAD"
868 assert self.request.method == "HEAD"
818 self.set_header("Content-Length", len(data))
869 self.set_header("Content-Length", len(data))
819
870
820 @classmethod
871 @classmethod
821 def get_version(cls, settings, path):
872 def get_version(cls, settings, path):
822 """Generate the version string to be used in static URLs.
873 """Generate the version string to be used in static URLs.
823
874
824 This method may be overridden in subclasses (but note that it
875 This method may be overridden in subclasses (but note that it
825 is a class method rather than a static method). The default
876 is a class method rather than a static method). The default
826 implementation uses a hash of the file's contents.
877 implementation uses a hash of the file's contents.
827
878
828 ``settings`` is the `Application.settings` dictionary and ``path``
879 ``settings`` is the `Application.settings` dictionary and ``path``
829 is the relative location of the requested asset on the filesystem.
880 is the relative location of the requested asset on the filesystem.
830 The returned value should be a string, or ``None`` if no version
881 The returned value should be a string, or ``None`` if no version
831 could be determined.
882 could be determined.
832 """
883 """
833 # begin subclass override:
884 # begin subclass override:
834 static_paths = settings['static_path']
885 static_paths = settings['static_path']
835 if isinstance(static_paths, basestring):
886 if isinstance(static_paths, basestring):
836 static_paths = [static_paths]
887 static_paths = [static_paths]
837 roots = tuple(
888 roots = tuple(
838 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
889 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
839 )
890 )
840
891
841 try:
892 try:
842 abs_path = filefind(path, roots)
893 abs_path = filefind(path, roots)
843 except IOError:
894 except IOError:
844 logging.error("Could not find static file %r", path)
895 app_log.error("Could not find static file %r", path)
845 return None
896 return None
846
897
847 # end subclass override
898 # end subclass override
848
899
849 with cls._lock:
900 with cls._lock:
850 hashes = cls._static_hashes
901 hashes = cls._static_hashes
851 if abs_path not in hashes:
902 if abs_path not in hashes:
852 try:
903 try:
853 f = open(abs_path, "rb")
904 f = open(abs_path, "rb")
854 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
905 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
855 f.close()
906 f.close()
856 except Exception:
907 except Exception:
857 logging.error("Could not open static file %r", path)
908 app_log.error("Could not open static file %r", path)
858 hashes[abs_path] = None
909 hashes[abs_path] = None
859 hsh = hashes.get(abs_path)
910 hsh = hashes.get(abs_path)
860 if hsh:
911 if hsh:
861 return hsh[:5]
912 return hsh[:5]
862 return None
913 return None
863
914
864
915
865 def parse_url_path(self, url_path):
916 def parse_url_path(self, url_path):
866 """Converts a static URL path into a filesystem path.
917 """Converts a static URL path into a filesystem path.
867
918
868 ``url_path`` is the path component of the URL with
919 ``url_path`` is the path component of the URL with
869 ``static_url_prefix`` removed. The return value should be
920 ``static_url_prefix`` removed. The return value should be
870 filesystem path relative to ``static_path``.
921 filesystem path relative to ``static_path``.
871 """
922 """
872 if os.path.sep != "/":
923 if os.path.sep != "/":
873 url_path = url_path.replace("/", os.path.sep)
924 url_path = url_path.replace("/", os.path.sep)
874 return url_path
925 return url_path
875
926
876
927
@@ -1,115 +1,110 b''
1 """A kernel manager relating notebooks and kernels
1 """A kernel manager relating notebooks and kernels
2
2
3 Authors:
3 Authors:
4
4
5 * Brian Granger
5 * Brian Granger
6 """
6 """
7
7
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2013 The IPython Development Team
9 # Copyright (C) 2013 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 from tornado import web
19 from tornado import web
20
20
21 from IPython.kernel.multikernelmanager import MultiKernelManager
21 from IPython.kernel.multikernelmanager import MultiKernelManager
22 from IPython.utils.traitlets import (
22 from IPython.utils.traitlets import (
23 Dict, List, Unicode, Integer,
23 Dict, List, Unicode, Integer,
24 )
24 )
25
25
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 # Classes
27 # Classes
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29
29
30
30
31 class MappingKernelManager(MultiKernelManager):
31 class MappingKernelManager(MultiKernelManager):
32 """A KernelManager that handles notebok mapping and HTTP error handling"""
32 """A KernelManager that handles notebook mapping and HTTP error handling"""
33
33
34 def _kernel_manager_class_default(self):
34 def _kernel_manager_class_default(self):
35 return "IPython.kernel.ioloop.IOLoopKernelManager"
35 return "IPython.kernel.ioloop.IOLoopKernelManager"
36
36
37 kernel_argv = List(Unicode)
37 kernel_argv = List(Unicode)
38
38
39 max_msg_size = Integer(65536, config=True, help="""
40 The max raw message size accepted from the browser
41 over a WebSocket connection.
42 """)
43
44 _notebook_mapping = Dict()
39 _notebook_mapping = Dict()
45
40
46 #-------------------------------------------------------------------------
41 #-------------------------------------------------------------------------
47 # Methods for managing kernels and sessions
42 # Methods for managing kernels and sessions
48 #-------------------------------------------------------------------------
43 #-------------------------------------------------------------------------
49
44
50 def kernel_for_notebook(self, notebook_id):
45 def kernel_for_notebook(self, notebook_id):
51 """Return the kernel_id for a notebook_id or None."""
46 """Return the kernel_id for a notebook_id or None."""
52 return self._notebook_mapping.get(notebook_id)
47 return self._notebook_mapping.get(notebook_id)
53
48
54 def set_kernel_for_notebook(self, notebook_id, kernel_id):
49 def set_kernel_for_notebook(self, notebook_id, kernel_id):
55 """Associate a notebook with a kernel."""
50 """Associate a notebook with a kernel."""
56 if notebook_id is not None:
51 if notebook_id is not None:
57 self._notebook_mapping[notebook_id] = kernel_id
52 self._notebook_mapping[notebook_id] = kernel_id
58
53
59 def notebook_for_kernel(self, kernel_id):
54 def notebook_for_kernel(self, kernel_id):
60 """Return the notebook_id for a kernel_id or None."""
55 """Return the notebook_id for a kernel_id or None."""
61 for notebook_id, kid in self._notebook_mapping.iteritems():
56 for notebook_id, kid in self._notebook_mapping.iteritems():
62 if kernel_id == kid:
57 if kernel_id == kid:
63 return notebook_id
58 return notebook_id
64 return None
59 return None
65
60
66 def delete_mapping_for_kernel(self, kernel_id):
61 def delete_mapping_for_kernel(self, kernel_id):
67 """Remove the kernel/notebook mapping for kernel_id."""
62 """Remove the kernel/notebook mapping for kernel_id."""
68 notebook_id = self.notebook_for_kernel(kernel_id)
63 notebook_id = self.notebook_for_kernel(kernel_id)
69 if notebook_id is not None:
64 if notebook_id is not None:
70 del self._notebook_mapping[notebook_id]
65 del self._notebook_mapping[notebook_id]
71
66
72 def _handle_kernel_died(self, kernel_id):
67 def _handle_kernel_died(self, kernel_id):
73 """notice that a kernel died"""
68 """notice that a kernel died"""
74 self.log.warn("Kernel %s died, removing from map.", kernel_id)
69 self.log.warn("Kernel %s died, removing from map.", kernel_id)
75 self.delete_mapping_for_kernel(kernel_id)
70 self.delete_mapping_for_kernel(kernel_id)
76 self.remove_kernel(kernel_id, now=True)
71 self.remove_kernel(kernel_id, now=True)
77
72
78 def start_kernel(self, notebook_id=None, **kwargs):
73 def start_kernel(self, notebook_id=None, **kwargs):
79 """Start a kernel for a notebook an return its kernel_id.
74 """Start a kernel for a notebook an return its kernel_id.
80
75
81 Parameters
76 Parameters
82 ----------
77 ----------
83 notebook_id : uuid
78 notebook_id : uuid
84 The uuid of the notebook to associate the new kernel with. If this
79 The uuid of the notebook to associate the new kernel with. If this
85 is not None, this kernel will be persistent whenever the notebook
80 is not None, this kernel will be persistent whenever the notebook
86 requests a kernel.
81 requests a kernel.
87 """
82 """
88 kernel_id = self.kernel_for_notebook(notebook_id)
83 kernel_id = self.kernel_for_notebook(notebook_id)
89 if kernel_id is None:
84 if kernel_id is None:
90 kwargs['extra_arguments'] = self.kernel_argv
85 kwargs['extra_arguments'] = self.kernel_argv
91 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
86 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
92 self.set_kernel_for_notebook(notebook_id, kernel_id)
87 self.set_kernel_for_notebook(notebook_id, kernel_id)
93 self.log.info("Kernel started: %s" % kernel_id)
88 self.log.info("Kernel started: %s" % kernel_id)
94 self.log.debug("Kernel args: %r" % kwargs)
89 self.log.debug("Kernel args: %r" % kwargs)
95 # register callback for failed auto-restart
90 # register callback for failed auto-restart
96 self.add_restart_callback(kernel_id,
91 self.add_restart_callback(kernel_id,
97 lambda : self._handle_kernel_died(kernel_id),
92 lambda : self._handle_kernel_died(kernel_id),
98 'dead',
93 'dead',
99 )
94 )
100 else:
95 else:
101 self.log.info("Using existing kernel: %s" % kernel_id)
96 self.log.info("Using existing kernel: %s" % kernel_id)
102
97
103 return kernel_id
98 return kernel_id
104
99
105 def shutdown_kernel(self, kernel_id, now=False):
100 def shutdown_kernel(self, kernel_id, now=False):
106 """Shutdown a kernel by kernel_id"""
101 """Shutdown a kernel by kernel_id"""
107 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
102 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
108 self.delete_mapping_for_kernel(kernel_id)
103 self.delete_mapping_for_kernel(kernel_id)
109
104
110 # override _check_kernel_id to raise 404 instead of KeyError
105 # override _check_kernel_id to raise 404 instead of KeyError
111 def _check_kernel_id(self, kernel_id):
106 def _check_kernel_id(self, kernel_id):
112 """Check a that a kernel_id exists and raise 404 if not."""
107 """Check a that a kernel_id exists and raise 404 if not."""
113 if kernel_id not in self:
108 if kernel_id not in self:
114 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
109 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
115
110
@@ -1,718 +1,740 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 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-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 # stdlib
19 # stdlib
20 import errno
20 import errno
21 import logging
21 import logging
22 import os
22 import os
23 import random
23 import random
24 import re
24 import re
25 import select
25 import select
26 import signal
26 import signal
27 import socket
27 import socket
28 import sys
28 import sys
29 import threading
29 import threading
30 import time
30 import time
31 import uuid
31 import uuid
32 import webbrowser
32 import webbrowser
33
33
34
34
35 # Third party
35 # Third party
36 # check for pyzmq 2.1.11
36 # check for pyzmq 2.1.11
37 from IPython.utils.zmqrelated import check_for_zmq
37 from IPython.utils.zmqrelated import check_for_zmq
38 check_for_zmq('2.1.11', 'IPython.frontend.html.notebook')
38 check_for_zmq('2.1.11', 'IPython.frontend.html.notebook')
39
39
40 import zmq
40 import zmq
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 2.1.0
48 # check for tornado 2.1.0
49 msg = "The IPython Notebook requires tornado >= 2.1.0"
49 msg = "The IPython Notebook requires tornado >= 2.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 < (2,1,0):
58 if version_info < (2,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.frontend.html.notebook import DEFAULT_STATIC_FILES_PATH
65 from IPython.frontend.html.notebook import DEFAULT_STATIC_FILES_PATH
66 from .kernelmanager import MappingKernelManager
66 from .kernelmanager import MappingKernelManager
67 from .handlers import (LoginHandler, LogoutHandler,
67 from .handlers import (LoginHandler, LogoutHandler,
68 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
68 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
69 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
69 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
70 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
70 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
71 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
71 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
72 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
72 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler,
73 FileFindHandler, NotebookRedirectHandler,
73 FileFindHandler, NotebookRedirectHandler,
74 )
74 )
75 from .nbmanager import NotebookManager
75 from .nbmanager import NotebookManager
76 from .filenbmanager import FileNotebookManager
76 from .filenbmanager import FileNotebookManager
77 from .clustermanager import ClusterManager
77 from .clustermanager import ClusterManager
78
78
79 from IPython.config.application import catch_config_error, boolean_flag
79 from IPython.config.application import catch_config_error, boolean_flag
80 from IPython.core.application import BaseIPythonApplication
80 from IPython.core.application import BaseIPythonApplication
81 from IPython.core.profiledir import ProfileDir
81 from IPython.core.profiledir import ProfileDir
82 from IPython.frontend.consoleapp import IPythonConsoleApp
82 from IPython.frontend.consoleapp import IPythonConsoleApp
83 from IPython.kernel import swallow_argv
83 from IPython.kernel import swallow_argv
84 from IPython.kernel.zmq.session import Session, default_secure
84 from IPython.kernel.zmq.session import Session, default_secure
85 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
85 from IPython.kernel.zmq.zmqshell import ZMQInteractiveShell
86 from IPython.kernel.zmq.kernelapp import (
86 from IPython.kernel.zmq.kernelapp import (
87 kernel_flags,
87 kernel_flags,
88 kernel_aliases,
88 kernel_aliases,
89 IPKernelApp
89 IPKernelApp
90 )
90 )
91 from IPython.utils.importstring import import_item
91 from IPython.utils.importstring import import_item
92 from IPython.utils.localinterfaces import LOCALHOST
92 from IPython.utils.localinterfaces import LOCALHOST
93 from IPython.utils.traitlets import (
93 from IPython.utils.traitlets import (
94 Dict, Unicode, Integer, List, Enum, Bool,
94 Dict, Unicode, Integer, List, Enum, Bool,
95 DottedObjectName
95 DottedObjectName
96 )
96 )
97 from IPython.utils import py3compat
97 from IPython.utils import py3compat
98 from IPython.utils.path import filefind
98 from IPython.utils.path import filefind
99
99
100 #-----------------------------------------------------------------------------
100 #-----------------------------------------------------------------------------
101 # Module globals
101 # Module globals
102 #-----------------------------------------------------------------------------
102 #-----------------------------------------------------------------------------
103
103
104 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
104 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
105 _kernel_action_regex = r"(?P<action>restart|interrupt)"
105 _kernel_action_regex = r"(?P<action>restart|interrupt)"
106 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
106 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
107 _notebook_name_regex = r"(?P<notebook_name>.+\.ipynb)"
107 _notebook_name_regex = r"(?P<notebook_name>.+\.ipynb)"
108 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
108 _profile_regex = r"(?P<profile>[^\/]+)" # there is almost no text that is invalid
109 _cluster_action_regex = r"(?P<action>start|stop)"
109 _cluster_action_regex = r"(?P<action>start|stop)"
110
110
111 _examples = """
111 _examples = """
112 ipython notebook # start the notebook
112 ipython notebook # start the notebook
113 ipython notebook --profile=sympy # use the sympy profile
113 ipython notebook --profile=sympy # use the sympy profile
114 ipython notebook --pylab=inline # pylab in inline plotting mode
114 ipython notebook --pylab=inline # pylab in inline plotting mode
115 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
115 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
116 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
116 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
117 """
117 """
118
118
119 #-----------------------------------------------------------------------------
119 #-----------------------------------------------------------------------------
120 # Helper functions
120 # Helper functions
121 #-----------------------------------------------------------------------------
121 #-----------------------------------------------------------------------------
122
122
123 def url_path_join(a,b):
123 def url_path_join(a,b):
124 if a.endswith('/') and b.startswith('/'):
124 if a.endswith('/') and b.startswith('/'):
125 return a[:-1]+b
125 return a[:-1]+b
126 else:
126 else:
127 return a+b
127 return a+b
128
128
129 def random_ports(port, n):
129 def random_ports(port, n):
130 """Generate a list of n random ports near the given port.
130 """Generate a list of n random ports near the given port.
131
131
132 The first 5 ports will be sequential, and the remaining n-5 will be
132 The first 5 ports will be sequential, and the remaining n-5 will be
133 randomly selected in the range [port-2*n, port+2*n].
133 randomly selected in the range [port-2*n, port+2*n].
134 """
134 """
135 for i in range(min(5, n)):
135 for i in range(min(5, n)):
136 yield port + i
136 yield port + i
137 for i in range(n-5):
137 for i in range(n-5):
138 yield port + random.randint(-2*n, 2*n)
138 yield port + random.randint(-2*n, 2*n)
139
139
140 #-----------------------------------------------------------------------------
140 #-----------------------------------------------------------------------------
141 # The Tornado web application
141 # The Tornado web application
142 #-----------------------------------------------------------------------------
142 #-----------------------------------------------------------------------------
143
143
144 class NotebookWebApplication(web.Application):
144 class NotebookWebApplication(web.Application):
145
145
146 def __init__(self, ipython_app, kernel_manager, notebook_manager,
146 def __init__(self, ipython_app, kernel_manager, notebook_manager,
147 cluster_manager, log,
147 cluster_manager, log,
148 base_project_url, settings_overrides):
148 base_project_url, settings_overrides):
149 handlers = [
149 handlers = [
150 (r"/", ProjectDashboardHandler),
150 (r"/", ProjectDashboardHandler),
151 (r"/login", LoginHandler),
151 (r"/login", LoginHandler),
152 (r"/logout", LogoutHandler),
152 (r"/logout", LogoutHandler),
153 (r"/new", NewHandler),
153 (r"/new", NewHandler),
154 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
154 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
155 (r"/%s" % _notebook_name_regex, NotebookRedirectHandler),
155 (r"/%s" % _notebook_name_regex, NotebookRedirectHandler),
156 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
156 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
157 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
157 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
158 (r"/kernels", MainKernelHandler),
158 (r"/kernels", MainKernelHandler),
159 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
159 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
160 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
160 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
161 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
161 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
162 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
162 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
163 (r"/notebooks", NotebookRootHandler),
163 (r"/notebooks", NotebookRootHandler),
164 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
164 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
165 (r"/rstservice/render", RSTHandler),
165 (r"/rstservice/render", RSTHandler),
166 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
166 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
167 (r"/clusters", MainClusterHandler),
167 (r"/clusters", MainClusterHandler),
168 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
168 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
169 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
169 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
170 ]
170 ]
171
171
172 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
172 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
173 # base_project_url will always be unicode, which will in turn
173 # base_project_url will always be unicode, which will in turn
174 # make the patterns unicode, and ultimately result in unicode
174 # make the patterns unicode, and ultimately result in unicode
175 # keys in kwargs to handler._execute(**kwargs) in tornado.
175 # keys in kwargs to handler._execute(**kwargs) in tornado.
176 # This enforces that base_project_url be ascii in that situation.
176 # This enforces that base_project_url be ascii in that situation.
177 #
177 #
178 # Note that the URLs these patterns check against are escaped,
178 # Note that the URLs these patterns check against are escaped,
179 # and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'.
179 # and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'.
180 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
180 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
181
181 template_path = os.path.join(os.path.dirname(__file__), "templates")
182 settings = dict(
182 settings = dict(
183 template_path=os.path.join(os.path.dirname(__file__), "templates"),
183 # basics
184 base_project_url=base_project_url,
185 base_kernel_url=ipython_app.base_kernel_url,
186 template_path=template_path,
184 static_path=ipython_app.static_file_path,
187 static_path=ipython_app.static_file_path,
185 static_handler_class = FileFindHandler,
188 static_handler_class = FileFindHandler,
186 static_url_prefix = url_path_join(base_project_url,'/static/'),
189 static_url_prefix = url_path_join(base_project_url,'/static/'),
190
191 # authentication
187 cookie_secret=os.urandom(1024),
192 cookie_secret=os.urandom(1024),
188 login_url=url_path_join(base_project_url,'/login'),
193 login_url=url_path_join(base_project_url,'/login'),
189 cookie_name='username-%s' % uuid.uuid4(),
194 cookie_name='username-%s' % uuid.uuid4(),
190 base_project_url = base_project_url,
195 read_only=ipython_app.read_only,
196 password=ipython_app.password,
197
198 # managers
199 kernel_manager=kernel_manager,
200 notebook_manager=notebook_manager,
201 cluster_manager=cluster_manager,
202
203 # IPython stuff
204 max_msg_size=ipython_app.max_msg_size,
205 config=ipython_app.config,
206 use_less=ipython_app.use_less,
207 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
191 )
208 )
192
209
193 # allow custom overrides for the tornado web app.
210 # allow custom overrides for the tornado web app.
194 settings.update(settings_overrides)
211 settings.update(settings_overrides)
195
212
196 # prepend base_project_url onto the patterns that we match
213 # prepend base_project_url onto the patterns that we match
197 new_handlers = []
214 new_handlers = []
198 for handler in handlers:
215 for handler in handlers:
199 pattern = url_path_join(base_project_url, handler[0])
216 pattern = url_path_join(base_project_url, handler[0])
200 new_handler = tuple([pattern]+list(handler[1:]))
217 new_handler = tuple([pattern] + list(handler[1:]))
201 new_handlers.append( new_handler )
218 new_handlers.append(new_handler)
202
219
203 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
220 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
204
221
205 self.kernel_manager = kernel_manager
206 self.notebook_manager = notebook_manager
207 self.cluster_manager = cluster_manager
208 self.ipython_app = ipython_app
209 self.read_only = self.ipython_app.read_only
210 self.config = self.ipython_app.config
211 self.use_less = self.ipython_app.use_less
212 self.log = log
213 self.jinja2_env = Environment(loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), "templates")))
214
215
222
216
223
217 #-----------------------------------------------------------------------------
224 #-----------------------------------------------------------------------------
218 # Aliases and Flags
225 # Aliases and Flags
219 #-----------------------------------------------------------------------------
226 #-----------------------------------------------------------------------------
220
227
221 flags = dict(kernel_flags)
228 flags = dict(kernel_flags)
222 flags['no-browser']=(
229 flags['no-browser']=(
223 {'NotebookApp' : {'open_browser' : False}},
230 {'NotebookApp' : {'open_browser' : False}},
224 "Don't open the notebook in a browser after startup."
231 "Don't open the notebook in a browser after startup."
225 )
232 )
226 flags['no-mathjax']=(
233 flags['no-mathjax']=(
227 {'NotebookApp' : {'enable_mathjax' : False}},
234 {'NotebookApp' : {'enable_mathjax' : False}},
228 """Disable MathJax
235 """Disable MathJax
229
236
230 MathJax is the javascript library IPython uses to render math/LaTeX. It is
237 MathJax is the javascript library IPython uses to render math/LaTeX. It is
231 very large, so you may want to disable it if you have a slow internet
238 very large, so you may want to disable it if you have a slow internet
232 connection, or for offline use of the notebook.
239 connection, or for offline use of the notebook.
233
240
234 When disabled, equations etc. will appear as their untransformed TeX source.
241 When disabled, equations etc. will appear as their untransformed TeX source.
235 """
242 """
236 )
243 )
237 flags['read-only'] = (
244 flags['read-only'] = (
238 {'NotebookApp' : {'read_only' : True}},
245 {'NotebookApp' : {'read_only' : True}},
239 """Allow read-only access to notebooks.
246 """Allow read-only access to notebooks.
240
247
241 When using a password to protect the notebook server, this flag
248 When using a password to protect the notebook server, this flag
242 allows unauthenticated clients to view the notebook list, and
249 allows unauthenticated clients to view the notebook list, and
243 individual notebooks, but not edit them, start kernels, or run
250 individual notebooks, but not edit them, start kernels, or run
244 code.
251 code.
245
252
246 If no password is set, the server will be entirely read-only.
253 If no password is set, the server will be entirely read-only.
247 """
254 """
248 )
255 )
249
256
250 # Add notebook manager flags
257 # Add notebook manager flags
251 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
258 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
252 'Auto-save a .py script everytime the .ipynb notebook is saved',
259 'Auto-save a .py script everytime the .ipynb notebook is saved',
253 'Do not auto-save .py scripts for every notebook'))
260 'Do not auto-save .py scripts for every notebook'))
254
261
255 # the flags that are specific to the frontend
262 # the flags that are specific to the frontend
256 # these must be scrubbed before being passed to the kernel,
263 # these must be scrubbed before being passed to the kernel,
257 # or it will raise an error on unrecognized flags
264 # or it will raise an error on unrecognized flags
258 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
265 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
259
266
260 aliases = dict(kernel_aliases)
267 aliases = dict(kernel_aliases)
261
268
262 aliases.update({
269 aliases.update({
263 'ip': 'NotebookApp.ip',
270 'ip': 'NotebookApp.ip',
264 'port': 'NotebookApp.port',
271 'port': 'NotebookApp.port',
265 'port-retries': 'NotebookApp.port_retries',
272 'port-retries': 'NotebookApp.port_retries',
266 'transport': 'KernelManager.transport',
273 'transport': 'KernelManager.transport',
267 'keyfile': 'NotebookApp.keyfile',
274 'keyfile': 'NotebookApp.keyfile',
268 'certfile': 'NotebookApp.certfile',
275 'certfile': 'NotebookApp.certfile',
269 'notebook-dir': 'NotebookManager.notebook_dir',
276 'notebook-dir': 'NotebookManager.notebook_dir',
270 'browser': 'NotebookApp.browser',
277 'browser': 'NotebookApp.browser',
271 })
278 })
272
279
273 # remove ipkernel flags that are singletons, and don't make sense in
280 # remove ipkernel flags that are singletons, and don't make sense in
274 # multi-kernel evironment:
281 # multi-kernel evironment:
275 aliases.pop('f', None)
282 aliases.pop('f', None)
276
283
277 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
284 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
278 u'notebook-dir']
285 u'notebook-dir']
279
286
280 #-----------------------------------------------------------------------------
287 #-----------------------------------------------------------------------------
281 # NotebookApp
288 # NotebookApp
282 #-----------------------------------------------------------------------------
289 #-----------------------------------------------------------------------------
283
290
284 class NotebookApp(BaseIPythonApplication):
291 class NotebookApp(BaseIPythonApplication):
285
292
286 name = 'ipython-notebook'
293 name = 'ipython-notebook'
287 default_config_file_name='ipython_notebook_config.py'
294 default_config_file_name='ipython_notebook_config.py'
288
295
289 description = """
296 description = """
290 The IPython HTML Notebook.
297 The IPython HTML Notebook.
291
298
292 This launches a Tornado based HTML Notebook Server that serves up an
299 This launches a Tornado based HTML Notebook Server that serves up an
293 HTML5/Javascript Notebook client.
300 HTML5/Javascript Notebook client.
294 """
301 """
295 examples = _examples
302 examples = _examples
296
303
297 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
304 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
298 FileNotebookManager]
305 FileNotebookManager]
299 flags = Dict(flags)
306 flags = Dict(flags)
300 aliases = Dict(aliases)
307 aliases = Dict(aliases)
301
308
302 kernel_argv = List(Unicode)
309 kernel_argv = List(Unicode)
303
310
304 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
311 max_msg_size = Integer(65536, config=True, help="""
305 default_value=logging.INFO,
312 The max raw message size accepted from the browser
306 config=True,
313 over a WebSocket connection.
307 help="Set the log level by value or name.")
314 """)
315
316 def _log_level_default(self):
317 return logging.INFO
318
319 def _log_format_default(self):
320 """override default log format to include time"""
321 return u"%(asctime)s.%(msecs).03d [%(name)s] %(message)s"
308
322
309 # create requested profiles by default, if they don't exist:
323 # create requested profiles by default, if they don't exist:
310 auto_create = Bool(True)
324 auto_create = Bool(True)
311
325
312 # file to be opened in the notebook server
326 # file to be opened in the notebook server
313 file_to_run = Unicode('')
327 file_to_run = Unicode('')
314
328
315 # Network related information.
329 # Network related information.
316
330
317 ip = Unicode(LOCALHOST, config=True,
331 ip = Unicode(LOCALHOST, config=True,
318 help="The IP address the notebook server will listen on."
332 help="The IP address the notebook server will listen on."
319 )
333 )
320
334
321 def _ip_changed(self, name, old, new):
335 def _ip_changed(self, name, old, new):
322 if new == u'*': self.ip = u''
336 if new == u'*': self.ip = u''
323
337
324 port = Integer(8888, config=True,
338 port = Integer(8888, config=True,
325 help="The port the notebook server will listen on."
339 help="The port the notebook server will listen on."
326 )
340 )
327 port_retries = Integer(50, config=True,
341 port_retries = Integer(50, config=True,
328 help="The number of additional ports to try if the specified port is not available."
342 help="The number of additional ports to try if the specified port is not available."
329 )
343 )
330
344
331 certfile = Unicode(u'', config=True,
345 certfile = Unicode(u'', config=True,
332 help="""The full path to an SSL/TLS certificate file."""
346 help="""The full path to an SSL/TLS certificate file."""
333 )
347 )
334
348
335 keyfile = Unicode(u'', config=True,
349 keyfile = Unicode(u'', config=True,
336 help="""The full path to a private key file for usage with SSL/TLS."""
350 help="""The full path to a private key file for usage with SSL/TLS."""
337 )
351 )
338
352
339 password = Unicode(u'', config=True,
353 password = Unicode(u'', config=True,
340 help="""Hashed password to use for web authentication.
354 help="""Hashed password to use for web authentication.
341
355
342 To generate, type in a python/IPython shell:
356 To generate, type in a python/IPython shell:
343
357
344 from IPython.lib import passwd; passwd()
358 from IPython.lib import passwd; passwd()
345
359
346 The string should be of the form type:salt:hashed-password.
360 The string should be of the form type:salt:hashed-password.
347 """
361 """
348 )
362 )
349
363
350 open_browser = Bool(True, config=True,
364 open_browser = Bool(True, config=True,
351 help="""Whether to open in a browser after starting.
365 help="""Whether to open in a browser after starting.
352 The specific browser used is platform dependent and
366 The specific browser used is platform dependent and
353 determined by the python standard library `webbrowser`
367 determined by the python standard library `webbrowser`
354 module, unless it is overridden using the --browser
368 module, unless it is overridden using the --browser
355 (NotebookApp.browser) configuration option.
369 (NotebookApp.browser) configuration option.
356 """)
370 """)
357
371
358 browser = Unicode(u'', config=True,
372 browser = Unicode(u'', config=True,
359 help="""Specify what command to use to invoke a web
373 help="""Specify what command to use to invoke a web
360 browser when opening the notebook. If not specified, the
374 browser when opening the notebook. If not specified, the
361 default browser will be determined by the `webbrowser`
375 default browser will be determined by the `webbrowser`
362 standard library module, which allows setting of the
376 standard library module, which allows setting of the
363 BROWSER environment variable to override it.
377 BROWSER environment variable to override it.
364 """)
378 """)
365
379
366 read_only = Bool(False, config=True,
380 read_only = Bool(False, config=True,
367 help="Whether to prevent editing/execution of notebooks."
381 help="Whether to prevent editing/execution of notebooks."
368 )
382 )
369
383
370 use_less = Bool(False, config=True,
384 use_less = Bool(False, config=True,
371 help="""Wether to use Browser Side less-css parsing
385 help="""Wether to use Browser Side less-css parsing
372 instead of compiled css version in templates that allows
386 instead of compiled css version in templates that allows
373 it. This is mainly convenient when working on the less
387 it. This is mainly convenient when working on the less
374 file to avoid a build step, or if user want to overwrite
388 file to avoid a build step, or if user want to overwrite
375 some of the less variables without having to recompile
389 some of the less variables without having to recompile
376 everything.
390 everything.
377
391
378 You will need to install the less.js component in the static directory
392 You will need to install the less.js component in the static directory
379 either in the source tree or in your profile folder.
393 either in the source tree or in your profile folder.
380 """)
394 """)
381
395
382 webapp_settings = Dict(config=True,
396 webapp_settings = Dict(config=True,
383 help="Supply overrides for the tornado.web.Application that the "
397 help="Supply overrides for the tornado.web.Application that the "
384 "IPython notebook uses.")
398 "IPython notebook uses.")
385
399
386 enable_mathjax = Bool(True, config=True,
400 enable_mathjax = Bool(True, config=True,
387 help="""Whether to enable MathJax for typesetting math/TeX
401 help="""Whether to enable MathJax for typesetting math/TeX
388
402
389 MathJax is the javascript library IPython uses to render math/LaTeX. It is
403 MathJax is the javascript library IPython uses to render math/LaTeX. It is
390 very large, so you may want to disable it if you have a slow internet
404 very large, so you may want to disable it if you have a slow internet
391 connection, or for offline use of the notebook.
405 connection, or for offline use of the notebook.
392
406
393 When disabled, equations etc. will appear as their untransformed TeX source.
407 When disabled, equations etc. will appear as their untransformed TeX source.
394 """
408 """
395 )
409 )
396 def _enable_mathjax_changed(self, name, old, new):
410 def _enable_mathjax_changed(self, name, old, new):
397 """set mathjax url to empty if mathjax is disabled"""
411 """set mathjax url to empty if mathjax is disabled"""
398 if not new:
412 if not new:
399 self.mathjax_url = u''
413 self.mathjax_url = u''
400
414
401 base_project_url = Unicode('/', config=True,
415 base_project_url = Unicode('/', config=True,
402 help='''The base URL for the notebook server.
416 help='''The base URL for the notebook server.
403
417
404 Leading and trailing slashes can be omitted,
418 Leading and trailing slashes can be omitted,
405 and will automatically be added.
419 and will automatically be added.
406 ''')
420 ''')
407 def _base_project_url_changed(self, name, old, new):
421 def _base_project_url_changed(self, name, old, new):
408 if not new.startswith('/'):
422 if not new.startswith('/'):
409 self.base_project_url = '/'+new
423 self.base_project_url = '/'+new
410 elif not new.endswith('/'):
424 elif not new.endswith('/'):
411 self.base_project_url = new+'/'
425 self.base_project_url = new+'/'
412
426
413 base_kernel_url = Unicode('/', config=True,
427 base_kernel_url = Unicode('/', config=True,
414 help='''The base URL for the kernel server
428 help='''The base URL for the kernel server
415
429
416 Leading and trailing slashes can be omitted,
430 Leading and trailing slashes can be omitted,
417 and will automatically be added.
431 and will automatically be added.
418 ''')
432 ''')
419 def _base_kernel_url_changed(self, name, old, new):
433 def _base_kernel_url_changed(self, name, old, new):
420 if not new.startswith('/'):
434 if not new.startswith('/'):
421 self.base_kernel_url = '/'+new
435 self.base_kernel_url = '/'+new
422 elif not new.endswith('/'):
436 elif not new.endswith('/'):
423 self.base_kernel_url = new+'/'
437 self.base_kernel_url = new+'/'
424
438
425 websocket_host = Unicode("", config=True,
439 websocket_host = Unicode("", config=True,
426 help="""The hostname for the websocket server."""
440 help="""The hostname for the websocket server."""
427 )
441 )
428
442
429 extra_static_paths = List(Unicode, config=True,
443 extra_static_paths = List(Unicode, config=True,
430 help="""Extra paths to search for serving static files.
444 help="""Extra paths to search for serving static files.
431
445
432 This allows adding javascript/css to be available from the notebook server machine,
446 This allows adding javascript/css to be available from the notebook server machine,
433 or overriding individual files in the IPython"""
447 or overriding individual files in the IPython"""
434 )
448 )
435 def _extra_static_paths_default(self):
449 def _extra_static_paths_default(self):
436 return [os.path.join(self.profile_dir.location, 'static')]
450 return [os.path.join(self.profile_dir.location, 'static')]
437
451
438 @property
452 @property
439 def static_file_path(self):
453 def static_file_path(self):
440 """return extra paths + the default location"""
454 """return extra paths + the default location"""
441 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
455 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
442
456
443 mathjax_url = Unicode("", config=True,
457 mathjax_url = Unicode("", config=True,
444 help="""The url for MathJax.js."""
458 help="""The url for MathJax.js."""
445 )
459 )
446 def _mathjax_url_default(self):
460 def _mathjax_url_default(self):
447 if not self.enable_mathjax:
461 if not self.enable_mathjax:
448 return u''
462 return u''
449 static_url_prefix = self.webapp_settings.get("static_url_prefix",
463 static_url_prefix = self.webapp_settings.get("static_url_prefix",
450 "/static/")
464 "/static/")
451 try:
465 try:
452 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
466 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), self.static_file_path)
453 except IOError:
467 except IOError:
454 if self.certfile:
468 if self.certfile:
455 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
469 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
456 base = u"https://c328740.ssl.cf1.rackcdn.com"
470 base = u"https://c328740.ssl.cf1.rackcdn.com"
457 else:
471 else:
458 base = u"http://cdn.mathjax.org"
472 base = u"http://cdn.mathjax.org"
459
473
460 url = base + u"/mathjax/latest/MathJax.js"
474 url = base + u"/mathjax/latest/MathJax.js"
461 self.log.info("Using MathJax from CDN: %s", url)
475 self.log.info("Using MathJax from CDN: %s", url)
462 return url
476 return url
463 else:
477 else:
464 self.log.info("Using local MathJax from %s" % mathjax)
478 self.log.info("Using local MathJax from %s" % mathjax)
465 return static_url_prefix+u"mathjax/MathJax.js"
479 return static_url_prefix+u"mathjax/MathJax.js"
466
480
467 def _mathjax_url_changed(self, name, old, new):
481 def _mathjax_url_changed(self, name, old, new):
468 if new and not self.enable_mathjax:
482 if new and not self.enable_mathjax:
469 # enable_mathjax=False overrides mathjax_url
483 # enable_mathjax=False overrides mathjax_url
470 self.mathjax_url = u''
484 self.mathjax_url = u''
471 else:
485 else:
472 self.log.info("Using MathJax: %s", new)
486 self.log.info("Using MathJax: %s", new)
473
487
474 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
488 notebook_manager_class = DottedObjectName('IPython.frontend.html.notebook.filenbmanager.FileNotebookManager',
475 config=True,
489 config=True,
476 help='The notebook manager class to use.')
490 help='The notebook manager class to use.')
477
491
478 trust_xheaders = Bool(False, config=True,
492 trust_xheaders = Bool(False, config=True,
479 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
493 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
480 "sent by the upstream reverse proxy. Neccesary if the proxy handles SSL")
494 "sent by the upstream reverse proxy. Neccesary if the proxy handles SSL")
481 )
495 )
482
496
483 def parse_command_line(self, argv=None):
497 def parse_command_line(self, argv=None):
484 super(NotebookApp, self).parse_command_line(argv)
498 super(NotebookApp, self).parse_command_line(argv)
485 if argv is None:
499 if argv is None:
486 argv = sys.argv[1:]
500 argv = sys.argv[1:]
487
501
488 # Scrub frontend-specific flags
502 # Scrub frontend-specific flags
489 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
503 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
490 # Kernel should inherit default config file from frontend
504 # Kernel should inherit default config file from frontend
491 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
505 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
492
506
493 if self.extra_args:
507 if self.extra_args:
494 f = os.path.abspath(self.extra_args[0])
508 f = os.path.abspath(self.extra_args[0])
495 if os.path.isdir(f):
509 if os.path.isdir(f):
496 nbdir = f
510 nbdir = f
497 else:
511 else:
498 self.file_to_run = f
512 self.file_to_run = f
499 nbdir = os.path.dirname(f)
513 nbdir = os.path.dirname(f)
500 self.config.NotebookManager.notebook_dir = nbdir
514 self.config.NotebookManager.notebook_dir = nbdir
501
515
502 def init_configurables(self):
516 def init_configurables(self):
503 # force Session default to be secure
517 # force Session default to be secure
504 default_secure(self.config)
518 default_secure(self.config)
505 self.kernel_manager = MappingKernelManager(
519 self.kernel_manager = MappingKernelManager(
506 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
520 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
507 connection_dir = self.profile_dir.security_dir,
521 connection_dir = self.profile_dir.security_dir,
508 )
522 )
509 kls = import_item(self.notebook_manager_class)
523 kls = import_item(self.notebook_manager_class)
510 self.notebook_manager = kls(config=self.config, log=self.log)
524 self.notebook_manager = kls(config=self.config, log=self.log)
511 self.notebook_manager.load_notebook_names()
525 self.notebook_manager.load_notebook_names()
512 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
526 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
513 self.cluster_manager.update_profiles()
527 self.cluster_manager.update_profiles()
514
528
515 def init_logging(self):
529 def init_logging(self):
516 # This prevents double log messages because tornado use a root logger that
530 # This prevents double log messages because tornado use a root logger that
517 # self.log is a child of. The logging module dipatches log messages to a log
531 # self.log is a child of. The logging module dipatches log messages to a log
518 # and all of its ancenstors until propagate is set to False.
532 # and all of its ancenstors until propagate is set to False.
519 self.log.propagate = False
533 self.log.propagate = False
534
535 # set the date format
536 formatter = logging.Formatter(self.log_format, datefmt="%Y-%m-%d %H:%M:%S")
537 self.log.handlers[0].setFormatter(formatter)
538
539 # hook up tornado 3's loggers to our app handlers
540 for name in ('access', 'application', 'general'):
541 logging.getLogger('tornado.%s' % name).handlers = self.log.handlers
520
542
521 def init_webapp(self):
543 def init_webapp(self):
522 """initialize tornado webapp and httpserver"""
544 """initialize tornado webapp and httpserver"""
523 self.web_app = NotebookWebApplication(
545 self.web_app = NotebookWebApplication(
524 self, self.kernel_manager, self.notebook_manager,
546 self, self.kernel_manager, self.notebook_manager,
525 self.cluster_manager, self.log,
547 self.cluster_manager, self.log,
526 self.base_project_url, self.webapp_settings
548 self.base_project_url, self.webapp_settings
527 )
549 )
528 if self.certfile:
550 if self.certfile:
529 ssl_options = dict(certfile=self.certfile)
551 ssl_options = dict(certfile=self.certfile)
530 if self.keyfile:
552 if self.keyfile:
531 ssl_options['keyfile'] = self.keyfile
553 ssl_options['keyfile'] = self.keyfile
532 else:
554 else:
533 ssl_options = None
555 ssl_options = None
534 self.web_app.password = self.password
556 self.web_app.password = self.password
535 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
557 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
536 xheaders=self.trust_xheaders)
558 xheaders=self.trust_xheaders)
537 if not self.ip:
559 if not self.ip:
538 warning = "WARNING: The notebook server is listening on all IP addresses"
560 warning = "WARNING: The notebook server is listening on all IP addresses"
539 if ssl_options is None:
561 if ssl_options is None:
540 self.log.critical(warning + " and not using encryption. This"
562 self.log.critical(warning + " and not using encryption. This"
541 "is not recommended.")
563 "is not recommended.")
542 if not self.password and not self.read_only:
564 if not self.password and not self.read_only:
543 self.log.critical(warning + "and not using authentication."
565 self.log.critical(warning + "and not using authentication."
544 "This is highly insecure and not recommended.")
566 "This is highly insecure and not recommended.")
545 success = None
567 success = None
546 for port in random_ports(self.port, self.port_retries+1):
568 for port in random_ports(self.port, self.port_retries+1):
547 try:
569 try:
548 self.http_server.listen(port, self.ip)
570 self.http_server.listen(port, self.ip)
549 except socket.error as e:
571 except socket.error as e:
550 # XXX: remove the e.errno == -9 block when we require
572 # XXX: remove the e.errno == -9 block when we require
551 # tornado >= 3.0
573 # tornado >= 3.0
552 if e.errno == -9 and tornado.version_info[0] < 3:
574 if e.errno == -9 and tornado.version_info[0] < 3:
553 # The flags passed to socket.getaddrinfo from
575 # The flags passed to socket.getaddrinfo from
554 # tornado.netutils.bind_sockets can cause "gaierror:
576 # tornado.netutils.bind_sockets can cause "gaierror:
555 # [Errno -9] Address family for hostname not supported"
577 # [Errno -9] Address family for hostname not supported"
556 # when the interface is not associated, for example.
578 # when the interface is not associated, for example.
557 # Changing the flags to exclude socket.AI_ADDRCONFIG does
579 # Changing the flags to exclude socket.AI_ADDRCONFIG does
558 # not cause this error, but the only way to do this is to
580 # not cause this error, but the only way to do this is to
559 # monkeypatch socket to remove the AI_ADDRCONFIG attribute
581 # monkeypatch socket to remove the AI_ADDRCONFIG attribute
560 saved_AI_ADDRCONFIG = socket.AI_ADDRCONFIG
582 saved_AI_ADDRCONFIG = socket.AI_ADDRCONFIG
561 self.log.warn('Monkeypatching socket to fix tornado bug')
583 self.log.warn('Monkeypatching socket to fix tornado bug')
562 del(socket.AI_ADDRCONFIG)
584 del(socket.AI_ADDRCONFIG)
563 try:
585 try:
564 # retry the tornado call without AI_ADDRCONFIG flags
586 # retry the tornado call without AI_ADDRCONFIG flags
565 self.http_server.listen(port, self.ip)
587 self.http_server.listen(port, self.ip)
566 except socket.error as e2:
588 except socket.error as e2:
567 e = e2
589 e = e2
568 else:
590 else:
569 self.port = port
591 self.port = port
570 success = True
592 success = True
571 break
593 break
572 # restore the monekypatch
594 # restore the monekypatch
573 socket.AI_ADDRCONFIG = saved_AI_ADDRCONFIG
595 socket.AI_ADDRCONFIG = saved_AI_ADDRCONFIG
574 if e.errno != errno.EADDRINUSE:
596 if e.errno != errno.EADDRINUSE:
575 raise
597 raise
576 self.log.info('The port %i is already in use, trying another random port.' % port)
598 self.log.info('The port %i is already in use, trying another random port.' % port)
577 else:
599 else:
578 self.port = port
600 self.port = port
579 success = True
601 success = True
580 break
602 break
581 if not success:
603 if not success:
582 self.log.critical('ERROR: the notebook server could not be started because '
604 self.log.critical('ERROR: the notebook server could not be started because '
583 'no available port could be found.')
605 'no available port could be found.')
584 self.exit(1)
606 self.exit(1)
585
607
586 def init_signal(self):
608 def init_signal(self):
587 if not sys.platform.startswith('win'):
609 if not sys.platform.startswith('win'):
588 signal.signal(signal.SIGINT, self._handle_sigint)
610 signal.signal(signal.SIGINT, self._handle_sigint)
589 signal.signal(signal.SIGTERM, self._signal_stop)
611 signal.signal(signal.SIGTERM, self._signal_stop)
590 if hasattr(signal, 'SIGUSR1'):
612 if hasattr(signal, 'SIGUSR1'):
591 # Windows doesn't support SIGUSR1
613 # Windows doesn't support SIGUSR1
592 signal.signal(signal.SIGUSR1, self._signal_info)
614 signal.signal(signal.SIGUSR1, self._signal_info)
593 if hasattr(signal, 'SIGINFO'):
615 if hasattr(signal, 'SIGINFO'):
594 # only on BSD-based systems
616 # only on BSD-based systems
595 signal.signal(signal.SIGINFO, self._signal_info)
617 signal.signal(signal.SIGINFO, self._signal_info)
596
618
597 def _handle_sigint(self, sig, frame):
619 def _handle_sigint(self, sig, frame):
598 """SIGINT handler spawns confirmation dialog"""
620 """SIGINT handler spawns confirmation dialog"""
599 # register more forceful signal handler for ^C^C case
621 # register more forceful signal handler for ^C^C case
600 signal.signal(signal.SIGINT, self._signal_stop)
622 signal.signal(signal.SIGINT, self._signal_stop)
601 # request confirmation dialog in bg thread, to avoid
623 # request confirmation dialog in bg thread, to avoid
602 # blocking the App
624 # blocking the App
603 thread = threading.Thread(target=self._confirm_exit)
625 thread = threading.Thread(target=self._confirm_exit)
604 thread.daemon = True
626 thread.daemon = True
605 thread.start()
627 thread.start()
606
628
607 def _restore_sigint_handler(self):
629 def _restore_sigint_handler(self):
608 """callback for restoring original SIGINT handler"""
630 """callback for restoring original SIGINT handler"""
609 signal.signal(signal.SIGINT, self._handle_sigint)
631 signal.signal(signal.SIGINT, self._handle_sigint)
610
632
611 def _confirm_exit(self):
633 def _confirm_exit(self):
612 """confirm shutdown on ^C
634 """confirm shutdown on ^C
613
635
614 A second ^C, or answering 'y' within 5s will cause shutdown,
636 A second ^C, or answering 'y' within 5s will cause shutdown,
615 otherwise original SIGINT handler will be restored.
637 otherwise original SIGINT handler will be restored.
616
638
617 This doesn't work on Windows.
639 This doesn't work on Windows.
618 """
640 """
619 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
641 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
620 time.sleep(0.1)
642 time.sleep(0.1)
621 info = self.log.info
643 info = self.log.info
622 info('interrupted')
644 info('interrupted')
623 print self.notebook_info()
645 print self.notebook_info()
624 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
646 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
625 sys.stdout.flush()
647 sys.stdout.flush()
626 r,w,x = select.select([sys.stdin], [], [], 5)
648 r,w,x = select.select([sys.stdin], [], [], 5)
627 if r:
649 if r:
628 line = sys.stdin.readline()
650 line = sys.stdin.readline()
629 if line.lower().startswith('y'):
651 if line.lower().startswith('y'):
630 self.log.critical("Shutdown confirmed")
652 self.log.critical("Shutdown confirmed")
631 ioloop.IOLoop.instance().stop()
653 ioloop.IOLoop.instance().stop()
632 return
654 return
633 else:
655 else:
634 print "No answer for 5s:",
656 print "No answer for 5s:",
635 print "resuming operation..."
657 print "resuming operation..."
636 # no answer, or answer is no:
658 # no answer, or answer is no:
637 # set it back to original SIGINT handler
659 # set it back to original SIGINT handler
638 # use IOLoop.add_callback because signal.signal must be called
660 # use IOLoop.add_callback because signal.signal must be called
639 # from main thread
661 # from main thread
640 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
662 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
641
663
642 def _signal_stop(self, sig, frame):
664 def _signal_stop(self, sig, frame):
643 self.log.critical("received signal %s, stopping", sig)
665 self.log.critical("received signal %s, stopping", sig)
644 ioloop.IOLoop.instance().stop()
666 ioloop.IOLoop.instance().stop()
645
667
646 def _signal_info(self, sig, frame):
668 def _signal_info(self, sig, frame):
647 print self.notebook_info()
669 print self.notebook_info()
648
670
649 @catch_config_error
671 @catch_config_error
650 def initialize(self, argv=None):
672 def initialize(self, argv=None):
651 self.init_logging()
673 self.init_logging()
652 super(NotebookApp, self).initialize(argv)
674 super(NotebookApp, self).initialize(argv)
653 self.init_configurables()
675 self.init_configurables()
654 self.init_webapp()
676 self.init_webapp()
655 self.init_signal()
677 self.init_signal()
656
678
657 def cleanup_kernels(self):
679 def cleanup_kernels(self):
658 """Shutdown all kernels.
680 """Shutdown all kernels.
659
681
660 The kernels will shutdown themselves when this process no longer exists,
682 The kernels will shutdown themselves when this process no longer exists,
661 but explicit shutdown allows the KernelManagers to cleanup the connection files.
683 but explicit shutdown allows the KernelManagers to cleanup the connection files.
662 """
684 """
663 self.log.info('Shutting down kernels')
685 self.log.info('Shutting down kernels')
664 self.kernel_manager.shutdown_all()
686 self.kernel_manager.shutdown_all()
665
687
666 def notebook_info(self):
688 def notebook_info(self):
667 "Return the current working directory and the server url information"
689 "Return the current working directory and the server url information"
668 mgr_info = self.notebook_manager.info_string() + "\n"
690 mgr_info = self.notebook_manager.info_string() + "\n"
669 return mgr_info +"The IPython Notebook is running at: %s" % self._url
691 return mgr_info +"The IPython Notebook is running at: %s" % self._url
670
692
671 def start(self):
693 def start(self):
672 """ Start the IPython Notebok server app, after initialization
694 """ Start the IPython Notebook server app, after initialization
673
695
674 This method takes no arguments so all configuration and initialization
696 This method takes no arguments so all configuration and initialization
675 must be done prior to calling this method."""
697 must be done prior to calling this method."""
676 ip = self.ip if self.ip else '[all ip addresses on your system]'
698 ip = self.ip if self.ip else '[all ip addresses on your system]'
677 proto = 'https' if self.certfile else 'http'
699 proto = 'https' if self.certfile else 'http'
678 info = self.log.info
700 info = self.log.info
679 self._url = "%s://%s:%i%s" % (proto, ip, self.port,
701 self._url = "%s://%s:%i%s" % (proto, ip, self.port,
680 self.base_project_url)
702 self.base_project_url)
681 for line in self.notebook_info().split("\n"):
703 for line in self.notebook_info().split("\n"):
682 info(line)
704 info(line)
683 info("Use Control-C to stop this server and shut down all kernels.")
705 info("Use Control-C to stop this server and shut down all kernels.")
684
706
685 if self.open_browser or self.file_to_run:
707 if self.open_browser or self.file_to_run:
686 ip = self.ip or LOCALHOST
708 ip = self.ip or LOCALHOST
687 try:
709 try:
688 browser = webbrowser.get(self.browser or None)
710 browser = webbrowser.get(self.browser or None)
689 except webbrowser.Error as e:
711 except webbrowser.Error as e:
690 self.log.warn('No web browser found: %s.' % e)
712 self.log.warn('No web browser found: %s.' % e)
691 browser = None
713 browser = None
692
714
693 if self.file_to_run:
715 if self.file_to_run:
694 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
716 name, _ = os.path.splitext(os.path.basename(self.file_to_run))
695 url = self.notebook_manager.rev_mapping.get(name, '')
717 url = self.notebook_manager.rev_mapping.get(name, '')
696 else:
718 else:
697 url = ''
719 url = ''
698 if browser:
720 if browser:
699 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
721 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
700 self.port, self.base_project_url, url), new=2)
722 self.port, self.base_project_url, url), new=2)
701 threading.Thread(target=b).start()
723 threading.Thread(target=b).start()
702 try:
724 try:
703 ioloop.IOLoop.instance().start()
725 ioloop.IOLoop.instance().start()
704 except KeyboardInterrupt:
726 except KeyboardInterrupt:
705 info("Interrupted...")
727 info("Interrupted...")
706 finally:
728 finally:
707 self.cleanup_kernels()
729 self.cleanup_kernels()
708
730
709
731
710 #-----------------------------------------------------------------------------
732 #-----------------------------------------------------------------------------
711 # Main entry point
733 # Main entry point
712 #-----------------------------------------------------------------------------
734 #-----------------------------------------------------------------------------
713
735
714 def launch_new_instance():
736 def launch_new_instance():
715 app = NotebookApp.instance()
737 app = NotebookApp.instance()
716 app.initialize()
738 app.initialize()
717 app.start()
739 app.start()
718
740
1 NO CONTENT: file was removed
NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now