##// END OF EJS Templates
Starting to refactor heart beating of notebook kernels.
Brian E. Granger -
Show More
@@ -1,919 +1,875 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 from zmq.eventloop import ioloop
35 from zmq.eventloop import ioloop
36 from zmq.utils import jsonapi
36 from zmq.utils import jsonapi
37
37
38 from IPython.external.decorator import decorator
38 from IPython.external.decorator import decorator
39 from IPython.kernel.zmq.session import Session
39 from IPython.kernel.zmq.session import Session
40 from IPython.lib.security import passwd_check
40 from IPython.lib.security import passwd_check
41 from IPython.utils.jsonutil import date_default
41 from IPython.utils.jsonutil import date_default
42 from IPython.utils.path import filefind
42 from IPython.utils.path import filefind
43 from IPython.utils.py3compat import PY3
43 from IPython.utils.py3compat import PY3
44
44
45 try:
45 try:
46 from docutils.core import publish_string
46 from docutils.core import publish_string
47 except ImportError:
47 except ImportError:
48 publish_string = None
48 publish_string = None
49
49
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
51 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
52 #-----------------------------------------------------------------------------
52 #-----------------------------------------------------------------------------
53
53
54 # Google Chrome, as of release 16, changed its websocket protocol number. The
54 # 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
55 # 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
56 # 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:
57 # version as of Oct 30/2011) the version check fails, see the issue report:
58
58
59 # https://github.com/facebook/tornado/issues/385
59 # https://github.com/facebook/tornado/issues/385
60
60
61 # This issue has been fixed in Tornado post 2.1.1:
61 # This issue has been fixed in Tornado post 2.1.1:
62
62
63 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
63 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
64
64
65 # Here we manually apply the same patch as above so that users of IPython can
65 # 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
66 # continue to work with an officially released Tornado. We make the
67 # monkeypatch version check as narrow as possible to limit its effects; once
67 # 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.
68 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
69
69
70 import tornado
70 import tornado
71
71
72 if tornado.version_info <= (2,1,1):
72 if tornado.version_info <= (2,1,1):
73
73
74 def _execute(self, transforms, *args, **kwargs):
74 def _execute(self, transforms, *args, **kwargs):
75 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
75 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
76
76
77 self.open_args = args
77 self.open_args = args
78 self.open_kwargs = kwargs
78 self.open_kwargs = kwargs
79
79
80 # The difference between version 8 and 13 is that in 8 the
80 # 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
81 # client sends a "Sec-Websocket-Origin" header and in 13 it's
82 # simply "Origin".
82 # simply "Origin".
83 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
83 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
84 self.ws_connection = WebSocketProtocol8(self)
84 self.ws_connection = WebSocketProtocol8(self)
85 self.ws_connection.accept_connection()
85 self.ws_connection.accept_connection()
86
86
87 elif self.request.headers.get("Sec-WebSocket-Version"):
87 elif self.request.headers.get("Sec-WebSocket-Version"):
88 self.stream.write(tornado.escape.utf8(
88 self.stream.write(tornado.escape.utf8(
89 "HTTP/1.1 426 Upgrade Required\r\n"
89 "HTTP/1.1 426 Upgrade Required\r\n"
90 "Sec-WebSocket-Version: 8\r\n\r\n"))
90 "Sec-WebSocket-Version: 8\r\n\r\n"))
91 self.stream.close()
91 self.stream.close()
92
92
93 else:
93 else:
94 self.ws_connection = WebSocketProtocol76(self)
94 self.ws_connection = WebSocketProtocol76(self)
95 self.ws_connection.accept_connection()
95 self.ws_connection.accept_connection()
96
96
97 websocket.WebSocketHandler._execute = _execute
97 websocket.WebSocketHandler._execute = _execute
98 del _execute
98 del _execute
99
99
100 #-----------------------------------------------------------------------------
100 #-----------------------------------------------------------------------------
101 # Decorator for disabling read-only handlers
101 # Decorator for disabling read-only handlers
102 #-----------------------------------------------------------------------------
102 #-----------------------------------------------------------------------------
103
103
104 @decorator
104 @decorator
105 def not_if_readonly(f, self, *args, **kwargs):
105 def not_if_readonly(f, self, *args, **kwargs):
106 if self.application.read_only:
106 if self.application.read_only:
107 raise web.HTTPError(403, "Notebook server is read-only")
107 raise web.HTTPError(403, "Notebook server is read-only")
108 else:
108 else:
109 return f(self, *args, **kwargs)
109 return f(self, *args, **kwargs)
110
110
111 @decorator
111 @decorator
112 def authenticate_unless_readonly(f, self, *args, **kwargs):
112 def authenticate_unless_readonly(f, self, *args, **kwargs):
113 """authenticate this page *unless* readonly view is active.
113 """authenticate this page *unless* readonly view is active.
114
114
115 In read-only mode, the notebook list and print view should
115 In read-only mode, the notebook list and print view should
116 be accessible without authentication.
116 be accessible without authentication.
117 """
117 """
118
118
119 @web.authenticated
119 @web.authenticated
120 def auth_f(self, *args, **kwargs):
120 def auth_f(self, *args, **kwargs):
121 return f(self, *args, **kwargs)
121 return f(self, *args, **kwargs)
122
122
123 if self.application.read_only:
123 if self.application.read_only:
124 return f(self, *args, **kwargs)
124 return f(self, *args, **kwargs)
125 else:
125 else:
126 return auth_f(self, *args, **kwargs)
126 return auth_f(self, *args, **kwargs)
127
127
128 def urljoin(*pieces):
128 def urljoin(*pieces):
129 """Join componenet of url into a relative url
129 """Join componenet of url into a relative url
130
130
131 Use to prevent double slash when joining subpath
131 Use to prevent double slash when joining subpath
132 """
132 """
133 striped = [s.strip('/') for s in pieces]
133 striped = [s.strip('/') for s in pieces]
134 return '/'.join(s for s in striped if s)
134 return '/'.join(s for s in striped if s)
135
135
136 #-----------------------------------------------------------------------------
136 #-----------------------------------------------------------------------------
137 # Top-level handlers
137 # Top-level handlers
138 #-----------------------------------------------------------------------------
138 #-----------------------------------------------------------------------------
139
139
140 class RequestHandler(web.RequestHandler):
140 class RequestHandler(web.RequestHandler):
141 """RequestHandler with default variable setting."""
141 """RequestHandler with default variable setting."""
142
142
143 def render(*args, **kwargs):
143 def render(*args, **kwargs):
144 kwargs.setdefault('message', '')
144 kwargs.setdefault('message', '')
145 return web.RequestHandler.render(*args, **kwargs)
145 return web.RequestHandler.render(*args, **kwargs)
146
146
147 class AuthenticatedHandler(RequestHandler):
147 class AuthenticatedHandler(RequestHandler):
148 """A RequestHandler with an authenticated user."""
148 """A RequestHandler with an authenticated user."""
149
149
150 def get_current_user(self):
150 def get_current_user(self):
151 user_id = self.get_secure_cookie(self.settings['cookie_name'])
151 user_id = self.get_secure_cookie(self.settings['cookie_name'])
152 # For now the user_id should not return empty, but it could eventually
152 # For now the user_id should not return empty, but it could eventually
153 if user_id == '':
153 if user_id == '':
154 user_id = 'anonymous'
154 user_id = 'anonymous'
155 if user_id is None:
155 if user_id is None:
156 # prevent extra Invalid cookie sig warnings:
156 # prevent extra Invalid cookie sig warnings:
157 self.clear_cookie(self.settings['cookie_name'])
157 self.clear_cookie(self.settings['cookie_name'])
158 if not self.application.password and not self.application.read_only:
158 if not self.application.password and not self.application.read_only:
159 user_id = 'anonymous'
159 user_id = 'anonymous'
160 return user_id
160 return user_id
161
161
162 @property
162 @property
163 def logged_in(self):
163 def logged_in(self):
164 """Is a user currently logged in?
164 """Is a user currently logged in?
165
165
166 """
166 """
167 user = self.get_current_user()
167 user = self.get_current_user()
168 return (user and not user == 'anonymous')
168 return (user and not user == 'anonymous')
169
169
170 @property
170 @property
171 def login_available(self):
171 def login_available(self):
172 """May a user proceed to log in?
172 """May a user proceed to log in?
173
173
174 This returns True if login capability is available, irrespective of
174 This returns True if login capability is available, irrespective of
175 whether the user is already logged in or not.
175 whether the user is already logged in or not.
176
176
177 """
177 """
178 return bool(self.application.password)
178 return bool(self.application.password)
179
179
180 @property
180 @property
181 def read_only(self):
181 def read_only(self):
182 """Is the notebook read-only?
182 """Is the notebook read-only?
183
183
184 """
184 """
185 return self.application.read_only
185 return self.application.read_only
186
186
187 @property
187 @property
188 def use_less(self):
188 def use_less(self):
189 """Use less instead of css in templates"""
189 """Use less instead of css in templates"""
190 return self.application.use_less
190 return self.application.use_less
191
191
192 @property
192 @property
193 def ws_url(self):
193 def ws_url(self):
194 """websocket url matching the current request
194 """websocket url matching the current request
195
195
196 turns http[s]://host[:port] into
196 turns http[s]://host[:port] into
197 ws[s]://host[:port]
197 ws[s]://host[:port]
198 """
198 """
199 proto = self.request.protocol.replace('http', 'ws')
199 proto = self.request.protocol.replace('http', 'ws')
200 host = self.application.ipython_app.websocket_host # default to config value
200 host = self.application.ipython_app.websocket_host # default to config value
201 if host == '':
201 if host == '':
202 host = self.request.host # get from request
202 host = self.request.host # get from request
203 return "%s://%s" % (proto, host)
203 return "%s://%s" % (proto, host)
204
204
205
205
206 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
206 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
207 """static files should only be accessible when logged in"""
207 """static files should only be accessible when logged in"""
208
208
209 @authenticate_unless_readonly
209 @authenticate_unless_readonly
210 def get(self, path):
210 def get(self, path):
211 return web.StaticFileHandler.get(self, path)
211 return web.StaticFileHandler.get(self, path)
212
212
213
213
214 class ProjectDashboardHandler(AuthenticatedHandler):
214 class ProjectDashboardHandler(AuthenticatedHandler):
215
215
216 @authenticate_unless_readonly
216 @authenticate_unless_readonly
217 def get(self):
217 def get(self):
218 nbm = self.application.notebook_manager
218 nbm = self.application.notebook_manager
219 project = nbm.notebook_dir
219 project = nbm.notebook_dir
220 template = self.application.jinja2_env.get_template('projectdashboard.html')
220 template = self.application.jinja2_env.get_template('projectdashboard.html')
221 self.write( template.render(
221 self.write( template.render(
222 project=project,
222 project=project,
223 project_component=project.split('/'),
223 project_component=project.split('/'),
224 base_project_url=self.application.ipython_app.base_project_url,
224 base_project_url=self.application.ipython_app.base_project_url,
225 base_kernel_url=self.application.ipython_app.base_kernel_url,
225 base_kernel_url=self.application.ipython_app.base_kernel_url,
226 read_only=self.read_only,
226 read_only=self.read_only,
227 logged_in=self.logged_in,
227 logged_in=self.logged_in,
228 use_less=self.use_less,
228 use_less=self.use_less,
229 login_available=self.login_available))
229 login_available=self.login_available))
230
230
231
231
232 class LoginHandler(AuthenticatedHandler):
232 class LoginHandler(AuthenticatedHandler):
233
233
234 def _render(self, message=None):
234 def _render(self, message=None):
235 template = self.application.jinja2_env.get_template('login.html')
235 template = self.application.jinja2_env.get_template('login.html')
236 self.write( template.render(
236 self.write( template.render(
237 next=url_escape(self.get_argument('next', default=self.application.ipython_app.base_project_url)),
237 next=url_escape(self.get_argument('next', default=self.application.ipython_app.base_project_url)),
238 read_only=self.read_only,
238 read_only=self.read_only,
239 logged_in=self.logged_in,
239 logged_in=self.logged_in,
240 login_available=self.login_available,
240 login_available=self.login_available,
241 base_project_url=self.application.ipython_app.base_project_url,
241 base_project_url=self.application.ipython_app.base_project_url,
242 message=message
242 message=message
243 ))
243 ))
244
244
245 def get(self):
245 def get(self):
246 if self.current_user:
246 if self.current_user:
247 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
247 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
248 else:
248 else:
249 self._render()
249 self._render()
250
250
251 def post(self):
251 def post(self):
252 pwd = self.get_argument('password', default=u'')
252 pwd = self.get_argument('password', default=u'')
253 if self.application.password:
253 if self.application.password:
254 if passwd_check(self.application.password, pwd):
254 if passwd_check(self.application.password, pwd):
255 self.set_secure_cookie(self.settings['cookie_name'], str(uuid.uuid4()))
255 self.set_secure_cookie(self.settings['cookie_name'], str(uuid.uuid4()))
256 else:
256 else:
257 self._render(message={'error': 'Invalid password'})
257 self._render(message={'error': 'Invalid password'})
258 return
258 return
259
259
260 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
260 self.redirect(self.get_argument('next', default=self.application.ipython_app.base_project_url))
261
261
262
262
263 class LogoutHandler(AuthenticatedHandler):
263 class LogoutHandler(AuthenticatedHandler):
264
264
265 def get(self):
265 def get(self):
266 self.clear_cookie(self.settings['cookie_name'])
266 self.clear_cookie(self.settings['cookie_name'])
267 if self.login_available:
267 if self.login_available:
268 message = {'info': 'Successfully logged out.'}
268 message = {'info': 'Successfully logged out.'}
269 else:
269 else:
270 message = {'warning': 'Cannot log out. Notebook authentication '
270 message = {'warning': 'Cannot log out. Notebook authentication '
271 'is disabled.'}
271 'is disabled.'}
272 template = self.application.jinja2_env.get_template('logout.html')
272 template = self.application.jinja2_env.get_template('logout.html')
273 self.write( template.render(
273 self.write( template.render(
274 read_only=self.read_only,
274 read_only=self.read_only,
275 logged_in=self.logged_in,
275 logged_in=self.logged_in,
276 login_available=self.login_available,
276 login_available=self.login_available,
277 base_project_url=self.application.ipython_app.base_project_url,
277 base_project_url=self.application.ipython_app.base_project_url,
278 message=message))
278 message=message))
279
279
280
280
281 class NewHandler(AuthenticatedHandler):
281 class NewHandler(AuthenticatedHandler):
282
282
283 @web.authenticated
283 @web.authenticated
284 def get(self):
284 def get(self):
285 nbm = self.application.notebook_manager
285 nbm = self.application.notebook_manager
286 project = nbm.notebook_dir
286 project = nbm.notebook_dir
287 notebook_id = nbm.new_notebook()
287 notebook_id = nbm.new_notebook()
288 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
288 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
289
289
290 class NamedNotebookHandler(AuthenticatedHandler):
290 class NamedNotebookHandler(AuthenticatedHandler):
291
291
292 @authenticate_unless_readonly
292 @authenticate_unless_readonly
293 def get(self, notebook_id):
293 def get(self, notebook_id):
294 nbm = self.application.notebook_manager
294 nbm = self.application.notebook_manager
295 project = nbm.notebook_dir
295 project = nbm.notebook_dir
296 if not nbm.notebook_exists(notebook_id):
296 if not nbm.notebook_exists(notebook_id):
297 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
297 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
298 template = self.application.jinja2_env.get_template('notebook.html')
298 template = self.application.jinja2_env.get_template('notebook.html')
299 self.write( template.render(
299 self.write( template.render(
300 project=project,
300 project=project,
301 notebook_id=notebook_id,
301 notebook_id=notebook_id,
302 base_project_url=self.application.ipython_app.base_project_url,
302 base_project_url=self.application.ipython_app.base_project_url,
303 base_kernel_url=self.application.ipython_app.base_kernel_url,
303 base_kernel_url=self.application.ipython_app.base_kernel_url,
304 kill_kernel=False,
304 kill_kernel=False,
305 read_only=self.read_only,
305 read_only=self.read_only,
306 logged_in=self.logged_in,
306 logged_in=self.logged_in,
307 login_available=self.login_available,
307 login_available=self.login_available,
308 mathjax_url=self.application.ipython_app.mathjax_url,
308 mathjax_url=self.application.ipython_app.mathjax_url,
309 use_less=self.use_less
309 use_less=self.use_less
310 )
310 )
311 )
311 )
312
312
313
313
314 class PrintNotebookHandler(AuthenticatedHandler):
314 class PrintNotebookHandler(AuthenticatedHandler):
315
315
316 @authenticate_unless_readonly
316 @authenticate_unless_readonly
317 def get(self, notebook_id):
317 def get(self, notebook_id):
318 nbm = self.application.notebook_manager
318 nbm = self.application.notebook_manager
319 project = nbm.notebook_dir
319 project = nbm.notebook_dir
320 if not nbm.notebook_exists(notebook_id):
320 if not nbm.notebook_exists(notebook_id):
321 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
321 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
322 template = self.application.jinja2_env.get_template('printnotebook.html')
322 template = self.application.jinja2_env.get_template('printnotebook.html')
323 self.write( template.render(
323 self.write( template.render(
324 project=project,
324 project=project,
325 notebook_id=notebook_id,
325 notebook_id=notebook_id,
326 base_project_url=self.application.ipython_app.base_project_url,
326 base_project_url=self.application.ipython_app.base_project_url,
327 base_kernel_url=self.application.ipython_app.base_kernel_url,
327 base_kernel_url=self.application.ipython_app.base_kernel_url,
328 kill_kernel=False,
328 kill_kernel=False,
329 read_only=self.read_only,
329 read_only=self.read_only,
330 logged_in=self.logged_in,
330 logged_in=self.logged_in,
331 login_available=self.login_available,
331 login_available=self.login_available,
332 mathjax_url=self.application.ipython_app.mathjax_url,
332 mathjax_url=self.application.ipython_app.mathjax_url,
333 ))
333 ))
334
334
335 #-----------------------------------------------------------------------------
335 #-----------------------------------------------------------------------------
336 # Kernel handlers
336 # Kernel handlers
337 #-----------------------------------------------------------------------------
337 #-----------------------------------------------------------------------------
338
338
339
339
340 class MainKernelHandler(AuthenticatedHandler):
340 class MainKernelHandler(AuthenticatedHandler):
341
341
342 @web.authenticated
342 @web.authenticated
343 def get(self):
343 def get(self):
344 km = self.application.kernel_manager
344 km = self.application.kernel_manager
345 self.finish(jsonapi.dumps(km.list_kernel_ids()))
345 self.finish(jsonapi.dumps(km.list_kernel_ids()))
346
346
347 @web.authenticated
347 @web.authenticated
348 def post(self):
348 def post(self):
349 km = self.application.kernel_manager
349 km = self.application.kernel_manager
350 nbm = self.application.notebook_manager
350 nbm = self.application.notebook_manager
351 notebook_id = self.get_argument('notebook', default=None)
351 notebook_id = self.get_argument('notebook', default=None)
352 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
352 kernel_id = km.start_kernel(notebook_id, cwd=nbm.notebook_dir)
353 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
353 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
354 self.set_header('Location', '/'+kernel_id)
354 self.set_header('Location', '/'+kernel_id)
355 self.finish(jsonapi.dumps(data))
355 self.finish(jsonapi.dumps(data))
356
356
357
357
358 class KernelHandler(AuthenticatedHandler):
358 class KernelHandler(AuthenticatedHandler):
359
359
360 SUPPORTED_METHODS = ('DELETE')
360 SUPPORTED_METHODS = ('DELETE')
361
361
362 @web.authenticated
362 @web.authenticated
363 def delete(self, kernel_id):
363 def delete(self, kernel_id):
364 km = self.application.kernel_manager
364 km = self.application.kernel_manager
365 km.shutdown_kernel(kernel_id)
365 km.shutdown_kernel(kernel_id)
366 self.set_status(204)
366 self.set_status(204)
367 self.finish()
367 self.finish()
368
368
369
369
370 class KernelActionHandler(AuthenticatedHandler):
370 class KernelActionHandler(AuthenticatedHandler):
371
371
372 @web.authenticated
372 @web.authenticated
373 def post(self, kernel_id, action):
373 def post(self, kernel_id, action):
374 km = self.application.kernel_manager
374 km = self.application.kernel_manager
375 if action == 'interrupt':
375 if action == 'interrupt':
376 km.interrupt_kernel(kernel_id)
376 km.interrupt_kernel(kernel_id)
377 self.set_status(204)
377 self.set_status(204)
378 if action == 'restart':
378 if action == 'restart':
379 km.restart_kernel(kernel_id)
379 km.restart_kernel(kernel_id)
380 data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
380 data = {'ws_url':self.ws_url, 'kernel_id':kernel_id}
381 self.set_header('Location', '/'+kernel_id)
381 self.set_header('Location', '/'+kernel_id)
382 self.write(jsonapi.dumps(data))
382 self.write(jsonapi.dumps(data))
383 self.finish()
383 self.finish()
384
384
385
385
386 class ZMQStreamHandler(websocket.WebSocketHandler):
386 class ZMQStreamHandler(websocket.WebSocketHandler):
387
387
388 def _reserialize_reply(self, msg_list):
388 def _reserialize_reply(self, msg_list):
389 """Reserialize a reply message using JSON.
389 """Reserialize a reply message using JSON.
390
390
391 This takes the msg list from the ZMQ socket, unserializes it using
391 This takes the msg list from the ZMQ socket, unserializes it using
392 self.session and then serializes the result using JSON. This method
392 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
393 should be used by self._on_zmq_reply to build messages that can
394 be sent back to the browser.
394 be sent back to the browser.
395 """
395 """
396 idents, msg_list = self.session.feed_identities(msg_list)
396 idents, msg_list = self.session.feed_identities(msg_list)
397 msg = self.session.unserialize(msg_list)
397 msg = self.session.unserialize(msg_list)
398 try:
398 try:
399 msg['header'].pop('date')
399 msg['header'].pop('date')
400 except KeyError:
400 except KeyError:
401 pass
401 pass
402 try:
402 try:
403 msg['parent_header'].pop('date')
403 msg['parent_header'].pop('date')
404 except KeyError:
404 except KeyError:
405 pass
405 pass
406 msg.pop('buffers')
406 msg.pop('buffers')
407 return jsonapi.dumps(msg, default=date_default)
407 return jsonapi.dumps(msg, default=date_default)
408
408
409 def _on_zmq_reply(self, msg_list):
409 def _on_zmq_reply(self, msg_list):
410 try:
410 try:
411 msg = self._reserialize_reply(msg_list)
411 msg = self._reserialize_reply(msg_list)
412 except Exception:
412 except Exception:
413 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
413 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
414 else:
414 else:
415 self.write_message(msg)
415 self.write_message(msg)
416
416
417 def allow_draft76(self):
417 def allow_draft76(self):
418 """Allow draft 76, until browsers such as Safari update to RFC 6455.
418 """Allow draft 76, until browsers such as Safari update to RFC 6455.
419
419
420 This has been disabled by default in tornado in release 2.2.0, and
420 This has been disabled by default in tornado in release 2.2.0, and
421 support will be removed in later versions.
421 support will be removed in later versions.
422 """
422 """
423 return True
423 return True
424
424
425
425
426 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
426 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
427
427
428 def open(self, kernel_id):
428 def open(self, kernel_id):
429 self.kernel_id = kernel_id.decode('ascii')
429 self.kernel_id = kernel_id.decode('ascii')
430 try:
430 try:
431 cfg = self.application.config
431 cfg = self.application.config
432 except AttributeError:
432 except AttributeError:
433 # protect from the case where this is run from something other than
433 # protect from the case where this is run from something other than
434 # the notebook app:
434 # the notebook app:
435 cfg = None
435 cfg = None
436 self.session = Session(config=cfg)
436 self.session = Session(config=cfg)
437 self.save_on_message = self.on_message
437 self.save_on_message = self.on_message
438 self.on_message = self.on_first_message
438 self.on_message = self.on_first_message
439
439
440 def get_current_user(self):
440 def get_current_user(self):
441 user_id = self.get_secure_cookie(self.settings['cookie_name'])
441 user_id = self.get_secure_cookie(self.settings['cookie_name'])
442 if user_id == '' or (user_id is None and not self.application.password):
442 if user_id == '' or (user_id is None and not self.application.password):
443 user_id = 'anonymous'
443 user_id = 'anonymous'
444 return user_id
444 return user_id
445
445
446 def _inject_cookie_message(self, msg):
446 def _inject_cookie_message(self, msg):
447 """Inject the first message, which is the document cookie,
447 """Inject the first message, which is the document cookie,
448 for authentication."""
448 for authentication."""
449 if not PY3 and isinstance(msg, unicode):
449 if not PY3 and isinstance(msg, unicode):
450 # Cookie constructor doesn't accept unicode strings
450 # Cookie constructor doesn't accept unicode strings
451 # under Python 2.x for some reason
451 # under Python 2.x for some reason
452 msg = msg.encode('utf8', 'replace')
452 msg = msg.encode('utf8', 'replace')
453 try:
453 try:
454 self.request._cookies = Cookie.SimpleCookie(msg)
454 self.request._cookies = Cookie.SimpleCookie(msg)
455 except:
455 except:
456 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
456 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
457
457
458 def on_first_message(self, msg):
458 def on_first_message(self, msg):
459 self._inject_cookie_message(msg)
459 self._inject_cookie_message(msg)
460 if self.get_current_user() is None:
460 if self.get_current_user() is None:
461 logging.warn("Couldn't authenticate WebSocket connection")
461 logging.warn("Couldn't authenticate WebSocket connection")
462 raise web.HTTPError(403)
462 raise web.HTTPError(403)
463 self.on_message = self.save_on_message
463 self.on_message = self.save_on_message
464
464
465
465
466 class IOPubHandler(AuthenticatedZMQStreamHandler):
466 class IOPubHandler(AuthenticatedZMQStreamHandler):
467
467
468 def initialize(self, *args, **kwargs):
468 def initialize(self, *args, **kwargs):
469 self._kernel_alive = True
470 self._beating = False
471 self.iopub_stream = None
469 self.iopub_stream = None
472 self.hb_stream = None
470 self.hb_stream = None
471 self.heartbeat = None
473
472
474 def on_first_message(self, msg):
473 def on_first_message(self, msg):
475 try:
474 try:
476 super(IOPubHandler, self).on_first_message(msg)
475 super(IOPubHandler, self).on_first_message(msg)
477 except web.HTTPError:
476 except web.HTTPError:
478 self.close()
477 self.close()
479 return
478 return
480 km = self.application.kernel_manager
479 km = self.application.kernel_manager
481 self.time_to_dead = km.time_to_dead
482 self.first_beat = km.first_beat
483 kernel_id = self.kernel_id
480 kernel_id = self.kernel_id
484 try:
481 try:
485 self.iopub_stream = km.create_iopub_stream(kernel_id)
482 self.iopub_stream = km.create_iopub_stream(kernel_id)
486 self.hb_stream = km.create_hb_stream(kernel_id)
483 self.hb_stream = km.create_hb_stream(kernel_id)
484 self.heartbeat = Heartbeat(
485 stream=self.hb_stream, config=self.application.config
486 )
487 except web.HTTPError:
487 except web.HTTPError:
488 # WebSockets don't response to traditional error codes so we
488 # WebSockets don't response to traditional error codes so we
489 # close the connection.
489 # close the connection.
490 if not self.stream.closed():
490 if not self.stream.closed():
491 self.stream.close()
491 self.stream.close()
492 self.close()
492 self.close()
493 else:
493 else:
494 self.iopub_stream.on_recv(self._on_zmq_reply)
494 self.iopub_stream.on_recv(self._on_zmq_reply)
495 self.start_hb(self.kernel_died)
495 self.heartbeat.start(self.kernel_died)
496
496
497 def on_message(self, msg):
497 def on_message(self, msg):
498 pass
498 pass
499
499
500 def on_close(self):
500 def on_close(self):
501 # This method can be called twice, once by self.kernel_died and once
501 # This method can be called twice, once by self.kernel_died and once
502 # from the WebSocket close event. If the WebSocket connection is
502 # from the WebSocket close event. If the WebSocket connection is
503 # closed before the ZMQ streams are setup, they could be None.
503 # closed before the ZMQ streams are setup, they could be None.
504 self.stop_hb()
504 self.stop_hb()
505 if self.iopub_stream is not None and not self.iopub_stream.closed():
505 if self.iopub_stream is not None and not self.iopub_stream.closed():
506 self.iopub_stream.on_recv(None)
506 self.iopub_stream.on_recv(None)
507 self.iopub_stream.close()
507 self.iopub_stream.close()
508 if self.hb_stream is not None and not self.hb_stream.closed():
508 if self.hb_stream is not None and not self.hb_stream.closed():
509 self.hb_stream.close()
509 self.hb_stream.close()
510
510 # stop the heartbeat here
511 def start_hb(self, callback):
512 """Start the heartbeating and call the callback if the kernel dies."""
513 if not self._beating:
514 self._kernel_alive = True
515
516 def ping_or_dead():
517 self.hb_stream.flush()
518 if self._kernel_alive:
519 self._kernel_alive = False
520 self.hb_stream.send(b'ping')
521 # flush stream to force immediate socket send
522 self.hb_stream.flush()
523 else:
524 try:
525 callback()
526 except:
527 pass
528 finally:
529 self.stop_hb()
530
531 def beat_received(msg):
532 self._kernel_alive = True
533
534 self.hb_stream.on_recv(beat_received)
535 loop = ioloop.IOLoop.instance()
536 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
537 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
538 self._beating= True
539
540 def _really_start_hb(self):
541 """callback for delayed heartbeat start
542
543 Only start the hb loop if we haven't been closed during the wait.
544 """
545 if self._beating and not self.hb_stream.closed():
546 self._hb_periodic_callback.start()
547
548 def stop_hb(self):
549 """Stop the heartbeating and cancel all related callbacks."""
550 if self._beating:
551 self._beating = False
552 self._hb_periodic_callback.stop()
553 if not self.hb_stream.closed():
554 self.hb_stream.on_recv(None)
555
511
556 def _delete_kernel_data(self):
512 def _delete_kernel_data(self):
557 """Remove the kernel data and notebook mapping."""
513 """Remove the kernel data and notebook mapping."""
558 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
514 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
559
515
560 def kernel_died(self):
516 def kernel_died(self):
561 self._delete_kernel_data()
517 self._delete_kernel_data()
562 self.application.log.error("Kernel died: %s" % self.kernel_id)
518 self.application.log.error("Kernel died: %s" % self.kernel_id)
563 self.write_message(
519 self.write_message(
564 {'header': {'msg_type': 'status'},
520 {'header': {'msg_type': 'status'},
565 'parent_header': {},
521 'parent_header': {},
566 'content': {'execution_state':'dead'}
522 'content': {'execution_state':'dead'}
567 }
523 }
568 )
524 )
569 self.on_close()
525 self.on_close()
570
526
571
527
572 class ShellHandler(AuthenticatedZMQStreamHandler):
528 class ShellHandler(AuthenticatedZMQStreamHandler):
573
529
574 def initialize(self, *args, **kwargs):
530 def initialize(self, *args, **kwargs):
575 self.shell_stream = None
531 self.shell_stream = None
576
532
577 def on_first_message(self, msg):
533 def on_first_message(self, msg):
578 try:
534 try:
579 super(ShellHandler, self).on_first_message(msg)
535 super(ShellHandler, self).on_first_message(msg)
580 except web.HTTPError:
536 except web.HTTPError:
581 self.close()
537 self.close()
582 return
538 return
583 km = self.application.kernel_manager
539 km = self.application.kernel_manager
584 self.max_msg_size = km.max_msg_size
540 self.max_msg_size = km.max_msg_size
585 kernel_id = self.kernel_id
541 kernel_id = self.kernel_id
586 try:
542 try:
587 self.shell_stream = km.create_shell_stream(kernel_id)
543 self.shell_stream = km.create_shell_stream(kernel_id)
588 except web.HTTPError:
544 except web.HTTPError:
589 # WebSockets don't response to traditional error codes so we
545 # WebSockets don't response to traditional error codes so we
590 # close the connection.
546 # close the connection.
591 if not self.stream.closed():
547 if not self.stream.closed():
592 self.stream.close()
548 self.stream.close()
593 self.close()
549 self.close()
594 else:
550 else:
595 self.shell_stream.on_recv(self._on_zmq_reply)
551 self.shell_stream.on_recv(self._on_zmq_reply)
596
552
597 def on_message(self, msg):
553 def on_message(self, msg):
598 if len(msg) < self.max_msg_size:
554 if len(msg) < self.max_msg_size:
599 msg = jsonapi.loads(msg)
555 msg = jsonapi.loads(msg)
600 self.session.send(self.shell_stream, msg)
556 self.session.send(self.shell_stream, msg)
601
557
602 def on_close(self):
558 def on_close(self):
603 # Make sure the stream exists and is not already closed.
559 # Make sure the stream exists and is not already closed.
604 if self.shell_stream is not None and not self.shell_stream.closed():
560 if self.shell_stream is not None and not self.shell_stream.closed():
605 self.shell_stream.close()
561 self.shell_stream.close()
606
562
607
563
608 #-----------------------------------------------------------------------------
564 #-----------------------------------------------------------------------------
609 # Notebook web service handlers
565 # Notebook web service handlers
610 #-----------------------------------------------------------------------------
566 #-----------------------------------------------------------------------------
611
567
612 class NotebookRedirectHandler(AuthenticatedHandler):
568 class NotebookRedirectHandler(AuthenticatedHandler):
613
569
614 @authenticate_unless_readonly
570 @authenticate_unless_readonly
615 def get(self, notebook_name):
571 def get(self, notebook_name):
616 app = self.application
572 app = self.application
617 # strip trailing .ipynb:
573 # strip trailing .ipynb:
618 notebook_name = os.path.splitext(notebook_name)[0]
574 notebook_name = os.path.splitext(notebook_name)[0]
619 notebook_id = app.notebook_manager.rev_mapping.get(notebook_name, '')
575 notebook_id = app.notebook_manager.rev_mapping.get(notebook_name, '')
620 if notebook_id:
576 if notebook_id:
621 url = self.settings.get('base_project_url', '/') + notebook_id
577 url = self.settings.get('base_project_url', '/') + notebook_id
622 return self.redirect(url)
578 return self.redirect(url)
623 else:
579 else:
624 raise HTTPError(404)
580 raise HTTPError(404)
625
581
626 class NotebookRootHandler(AuthenticatedHandler):
582 class NotebookRootHandler(AuthenticatedHandler):
627
583
628 @authenticate_unless_readonly
584 @authenticate_unless_readonly
629 def get(self):
585 def get(self):
630 nbm = self.application.notebook_manager
586 nbm = self.application.notebook_manager
631 km = self.application.kernel_manager
587 km = self.application.kernel_manager
632 files = nbm.list_notebooks()
588 files = nbm.list_notebooks()
633 for f in files :
589 for f in files :
634 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
590 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
635 self.finish(jsonapi.dumps(files))
591 self.finish(jsonapi.dumps(files))
636
592
637 @web.authenticated
593 @web.authenticated
638 def post(self):
594 def post(self):
639 nbm = self.application.notebook_manager
595 nbm = self.application.notebook_manager
640 body = self.request.body.strip()
596 body = self.request.body.strip()
641 format = self.get_argument('format', default='json')
597 format = self.get_argument('format', default='json')
642 name = self.get_argument('name', default=None)
598 name = self.get_argument('name', default=None)
643 if body:
599 if body:
644 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
600 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
645 else:
601 else:
646 notebook_id = nbm.new_notebook()
602 notebook_id = nbm.new_notebook()
647 self.set_header('Location', '/'+notebook_id)
603 self.set_header('Location', '/'+notebook_id)
648 self.finish(jsonapi.dumps(notebook_id))
604 self.finish(jsonapi.dumps(notebook_id))
649
605
650
606
651 class NotebookHandler(AuthenticatedHandler):
607 class NotebookHandler(AuthenticatedHandler):
652
608
653 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
609 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
654
610
655 @authenticate_unless_readonly
611 @authenticate_unless_readonly
656 def get(self, notebook_id):
612 def get(self, notebook_id):
657 nbm = self.application.notebook_manager
613 nbm = self.application.notebook_manager
658 format = self.get_argument('format', default='json')
614 format = self.get_argument('format', default='json')
659 last_mod, name, data = nbm.get_notebook(notebook_id, format)
615 last_mod, name, data = nbm.get_notebook(notebook_id, format)
660
616
661 if format == u'json':
617 if format == u'json':
662 self.set_header('Content-Type', 'application/json')
618 self.set_header('Content-Type', 'application/json')
663 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
619 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
664 elif format == u'py':
620 elif format == u'py':
665 self.set_header('Content-Type', 'application/x-python')
621 self.set_header('Content-Type', 'application/x-python')
666 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
622 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
667 self.set_header('Last-Modified', last_mod)
623 self.set_header('Last-Modified', last_mod)
668 self.finish(data)
624 self.finish(data)
669
625
670 @web.authenticated
626 @web.authenticated
671 def put(self, notebook_id):
627 def put(self, notebook_id):
672 nbm = self.application.notebook_manager
628 nbm = self.application.notebook_manager
673 format = self.get_argument('format', default='json')
629 format = self.get_argument('format', default='json')
674 name = self.get_argument('name', default=None)
630 name = self.get_argument('name', default=None)
675 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
631 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
676 self.set_status(204)
632 self.set_status(204)
677 self.finish()
633 self.finish()
678
634
679 @web.authenticated
635 @web.authenticated
680 def delete(self, notebook_id):
636 def delete(self, notebook_id):
681 nbm = self.application.notebook_manager
637 nbm = self.application.notebook_manager
682 nbm.delete_notebook(notebook_id)
638 nbm.delete_notebook(notebook_id)
683 self.set_status(204)
639 self.set_status(204)
684 self.finish()
640 self.finish()
685
641
686
642
687 class NotebookCopyHandler(AuthenticatedHandler):
643 class NotebookCopyHandler(AuthenticatedHandler):
688
644
689 @web.authenticated
645 @web.authenticated
690 def get(self, notebook_id):
646 def get(self, notebook_id):
691 nbm = self.application.notebook_manager
647 nbm = self.application.notebook_manager
692 project = nbm.notebook_dir
648 project = nbm.notebook_dir
693 notebook_id = nbm.copy_notebook(notebook_id)
649 notebook_id = nbm.copy_notebook(notebook_id)
694 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
650 self.redirect('/'+urljoin(self.application.ipython_app.base_project_url, notebook_id))
695
651
696
652
697 #-----------------------------------------------------------------------------
653 #-----------------------------------------------------------------------------
698 # Cluster handlers
654 # Cluster handlers
699 #-----------------------------------------------------------------------------
655 #-----------------------------------------------------------------------------
700
656
701
657
702 class MainClusterHandler(AuthenticatedHandler):
658 class MainClusterHandler(AuthenticatedHandler):
703
659
704 @web.authenticated
660 @web.authenticated
705 def get(self):
661 def get(self):
706 cm = self.application.cluster_manager
662 cm = self.application.cluster_manager
707 self.finish(jsonapi.dumps(cm.list_profiles()))
663 self.finish(jsonapi.dumps(cm.list_profiles()))
708
664
709
665
710 class ClusterProfileHandler(AuthenticatedHandler):
666 class ClusterProfileHandler(AuthenticatedHandler):
711
667
712 @web.authenticated
668 @web.authenticated
713 def get(self, profile):
669 def get(self, profile):
714 cm = self.application.cluster_manager
670 cm = self.application.cluster_manager
715 self.finish(jsonapi.dumps(cm.profile_info(profile)))
671 self.finish(jsonapi.dumps(cm.profile_info(profile)))
716
672
717
673
718 class ClusterActionHandler(AuthenticatedHandler):
674 class ClusterActionHandler(AuthenticatedHandler):
719
675
720 @web.authenticated
676 @web.authenticated
721 def post(self, profile, action):
677 def post(self, profile, action):
722 cm = self.application.cluster_manager
678 cm = self.application.cluster_manager
723 if action == 'start':
679 if action == 'start':
724 n = self.get_argument('n',default=None)
680 n = self.get_argument('n',default=None)
725 if n is None:
681 if n is None:
726 data = cm.start_cluster(profile)
682 data = cm.start_cluster(profile)
727 else:
683 else:
728 data = cm.start_cluster(profile,int(n))
684 data = cm.start_cluster(profile,int(n))
729 if action == 'stop':
685 if action == 'stop':
730 data = cm.stop_cluster(profile)
686 data = cm.stop_cluster(profile)
731 self.finish(jsonapi.dumps(data))
687 self.finish(jsonapi.dumps(data))
732
688
733
689
734 #-----------------------------------------------------------------------------
690 #-----------------------------------------------------------------------------
735 # RST web service handlers
691 # RST web service handlers
736 #-----------------------------------------------------------------------------
692 #-----------------------------------------------------------------------------
737
693
738
694
739 class RSTHandler(AuthenticatedHandler):
695 class RSTHandler(AuthenticatedHandler):
740
696
741 @web.authenticated
697 @web.authenticated
742 def post(self):
698 def post(self):
743 if publish_string is None:
699 if publish_string is None:
744 raise web.HTTPError(503, u'docutils not available')
700 raise web.HTTPError(503, u'docutils not available')
745 body = self.request.body.strip()
701 body = self.request.body.strip()
746 source = body
702 source = body
747 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
703 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
748 defaults = {'file_insertion_enabled': 0,
704 defaults = {'file_insertion_enabled': 0,
749 'raw_enabled': 0,
705 'raw_enabled': 0,
750 '_disable_config': 1,
706 '_disable_config': 1,
751 'stylesheet_path': 0
707 'stylesheet_path': 0
752 # 'template': template_path
708 # 'template': template_path
753 }
709 }
754 try:
710 try:
755 html = publish_string(source, writer_name='html',
711 html = publish_string(source, writer_name='html',
756 settings_overrides=defaults
712 settings_overrides=defaults
757 )
713 )
758 except:
714 except:
759 raise web.HTTPError(400, u'Invalid RST')
715 raise web.HTTPError(400, u'Invalid RST')
760 print html
716 print html
761 self.set_header('Content-Type', 'text/html')
717 self.set_header('Content-Type', 'text/html')
762 self.finish(html)
718 self.finish(html)
763
719
764 # to minimize subclass changes:
720 # to minimize subclass changes:
765 HTTPError = web.HTTPError
721 HTTPError = web.HTTPError
766
722
767 class FileFindHandler(web.StaticFileHandler):
723 class FileFindHandler(web.StaticFileHandler):
768 """subclass of StaticFileHandler for serving files from a search path"""
724 """subclass of StaticFileHandler for serving files from a search path"""
769
725
770 _static_paths = {}
726 _static_paths = {}
771 # _lock is needed for tornado < 2.2.0 compat
727 # _lock is needed for tornado < 2.2.0 compat
772 _lock = threading.Lock() # protects _static_hashes
728 _lock = threading.Lock() # protects _static_hashes
773
729
774 def initialize(self, path, default_filename=None):
730 def initialize(self, path, default_filename=None):
775 if isinstance(path, basestring):
731 if isinstance(path, basestring):
776 path = [path]
732 path = [path]
777 self.roots = tuple(
733 self.roots = tuple(
778 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
734 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in path
779 )
735 )
780 self.default_filename = default_filename
736 self.default_filename = default_filename
781
737
782 @classmethod
738 @classmethod
783 def locate_file(cls, path, roots):
739 def locate_file(cls, path, roots):
784 """locate a file to serve on our static file search path"""
740 """locate a file to serve on our static file search path"""
785 with cls._lock:
741 with cls._lock:
786 if path in cls._static_paths:
742 if path in cls._static_paths:
787 return cls._static_paths[path]
743 return cls._static_paths[path]
788 try:
744 try:
789 abspath = os.path.abspath(filefind(path, roots))
745 abspath = os.path.abspath(filefind(path, roots))
790 except IOError:
746 except IOError:
791 # empty string should always give exists=False
747 # empty string should always give exists=False
792 return ''
748 return ''
793
749
794 # os.path.abspath strips a trailing /
750 # os.path.abspath strips a trailing /
795 # it needs to be temporarily added back for requests to root/
751 # it needs to be temporarily added back for requests to root/
796 if not (abspath + os.path.sep).startswith(roots):
752 if not (abspath + os.path.sep).startswith(roots):
797 raise HTTPError(403, "%s is not in root static directory", path)
753 raise HTTPError(403, "%s is not in root static directory", path)
798
754
799 cls._static_paths[path] = abspath
755 cls._static_paths[path] = abspath
800 return abspath
756 return abspath
801
757
802 def get(self, path, include_body=True):
758 def get(self, path, include_body=True):
803 path = self.parse_url_path(path)
759 path = self.parse_url_path(path)
804
760
805 # begin subclass override
761 # begin subclass override
806 abspath = self.locate_file(path, self.roots)
762 abspath = self.locate_file(path, self.roots)
807 # end subclass override
763 # end subclass override
808
764
809 if os.path.isdir(abspath) and self.default_filename is not None:
765 if os.path.isdir(abspath) and self.default_filename is not None:
810 # need to look at the request.path here for when path is empty
766 # need to look at the request.path here for when path is empty
811 # but there is some prefix to the path that was already
767 # but there is some prefix to the path that was already
812 # trimmed by the routing
768 # trimmed by the routing
813 if not self.request.path.endswith("/"):
769 if not self.request.path.endswith("/"):
814 self.redirect(self.request.path + "/")
770 self.redirect(self.request.path + "/")
815 return
771 return
816 abspath = os.path.join(abspath, self.default_filename)
772 abspath = os.path.join(abspath, self.default_filename)
817 if not os.path.exists(abspath):
773 if not os.path.exists(abspath):
818 raise HTTPError(404)
774 raise HTTPError(404)
819 if not os.path.isfile(abspath):
775 if not os.path.isfile(abspath):
820 raise HTTPError(403, "%s is not a file", path)
776 raise HTTPError(403, "%s is not a file", path)
821
777
822 stat_result = os.stat(abspath)
778 stat_result = os.stat(abspath)
823 modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
779 modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
824
780
825 self.set_header("Last-Modified", modified)
781 self.set_header("Last-Modified", modified)
826
782
827 mime_type, encoding = mimetypes.guess_type(abspath)
783 mime_type, encoding = mimetypes.guess_type(abspath)
828 if mime_type:
784 if mime_type:
829 self.set_header("Content-Type", mime_type)
785 self.set_header("Content-Type", mime_type)
830
786
831 cache_time = self.get_cache_time(path, modified, mime_type)
787 cache_time = self.get_cache_time(path, modified, mime_type)
832
788
833 if cache_time > 0:
789 if cache_time > 0:
834 self.set_header("Expires", datetime.datetime.utcnow() + \
790 self.set_header("Expires", datetime.datetime.utcnow() + \
835 datetime.timedelta(seconds=cache_time))
791 datetime.timedelta(seconds=cache_time))
836 self.set_header("Cache-Control", "max-age=" + str(cache_time))
792 self.set_header("Cache-Control", "max-age=" + str(cache_time))
837 else:
793 else:
838 self.set_header("Cache-Control", "public")
794 self.set_header("Cache-Control", "public")
839
795
840 self.set_extra_headers(path)
796 self.set_extra_headers(path)
841
797
842 # Check the If-Modified-Since, and don't send the result if the
798 # Check the If-Modified-Since, and don't send the result if the
843 # content has not been modified
799 # content has not been modified
844 ims_value = self.request.headers.get("If-Modified-Since")
800 ims_value = self.request.headers.get("If-Modified-Since")
845 if ims_value is not None:
801 if ims_value is not None:
846 date_tuple = email.utils.parsedate(ims_value)
802 date_tuple = email.utils.parsedate(ims_value)
847 if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
803 if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
848 if if_since >= modified:
804 if if_since >= modified:
849 self.set_status(304)
805 self.set_status(304)
850 return
806 return
851
807
852 with open(abspath, "rb") as file:
808 with open(abspath, "rb") as file:
853 data = file.read()
809 data = file.read()
854 hasher = hashlib.sha1()
810 hasher = hashlib.sha1()
855 hasher.update(data)
811 hasher.update(data)
856 self.set_header("Etag", '"%s"' % hasher.hexdigest())
812 self.set_header("Etag", '"%s"' % hasher.hexdigest())
857 if include_body:
813 if include_body:
858 self.write(data)
814 self.write(data)
859 else:
815 else:
860 assert self.request.method == "HEAD"
816 assert self.request.method == "HEAD"
861 self.set_header("Content-Length", len(data))
817 self.set_header("Content-Length", len(data))
862
818
863 @classmethod
819 @classmethod
864 def get_version(cls, settings, path):
820 def get_version(cls, settings, path):
865 """Generate the version string to be used in static URLs.
821 """Generate the version string to be used in static URLs.
866
822
867 This method may be overridden in subclasses (but note that it
823 This method may be overridden in subclasses (but note that it
868 is a class method rather than a static method). The default
824 is a class method rather than a static method). The default
869 implementation uses a hash of the file's contents.
825 implementation uses a hash of the file's contents.
870
826
871 ``settings`` is the `Application.settings` dictionary and ``path``
827 ``settings`` is the `Application.settings` dictionary and ``path``
872 is the relative location of the requested asset on the filesystem.
828 is the relative location of the requested asset on the filesystem.
873 The returned value should be a string, or ``None`` if no version
829 The returned value should be a string, or ``None`` if no version
874 could be determined.
830 could be determined.
875 """
831 """
876 # begin subclass override:
832 # begin subclass override:
877 static_paths = settings['static_path']
833 static_paths = settings['static_path']
878 if isinstance(static_paths, basestring):
834 if isinstance(static_paths, basestring):
879 static_paths = [static_paths]
835 static_paths = [static_paths]
880 roots = tuple(
836 roots = tuple(
881 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
837 os.path.abspath(os.path.expanduser(p)) + os.path.sep for p in static_paths
882 )
838 )
883
839
884 try:
840 try:
885 abs_path = filefind(path, roots)
841 abs_path = filefind(path, roots)
886 except IOError:
842 except IOError:
887 logging.error("Could not find static file %r", path)
843 logging.error("Could not find static file %r", path)
888 return None
844 return None
889
845
890 # end subclass override
846 # end subclass override
891
847
892 with cls._lock:
848 with cls._lock:
893 hashes = cls._static_hashes
849 hashes = cls._static_hashes
894 if abs_path not in hashes:
850 if abs_path not in hashes:
895 try:
851 try:
896 f = open(abs_path, "rb")
852 f = open(abs_path, "rb")
897 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
853 hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
898 f.close()
854 f.close()
899 except Exception:
855 except Exception:
900 logging.error("Could not open static file %r", path)
856 logging.error("Could not open static file %r", path)
901 hashes[abs_path] = None
857 hashes[abs_path] = None
902 hsh = hashes.get(abs_path)
858 hsh = hashes.get(abs_path)
903 if hsh:
859 if hsh:
904 return hsh[:5]
860 return hsh[:5]
905 return None
861 return None
906
862
907
863
908 def parse_url_path(self, url_path):
864 def parse_url_path(self, url_path):
909 """Converts a static URL path into a filesystem path.
865 """Converts a static URL path into a filesystem path.
910
866
911 ``url_path`` is the path component of the URL with
867 ``url_path`` is the path component of the URL with
912 ``static_url_prefix`` removed. The return value should be
868 ``static_url_prefix`` removed. The return value should be
913 filesystem path relative to ``static_path``.
869 filesystem path relative to ``static_path``.
914 """
870 """
915 if os.path.sep != "/":
871 if os.path.sep != "/":
916 url_path = url_path.replace("/", os.path.sep)
872 url_path = url_path.replace("/", os.path.sep)
917 return url_path
873 return url_path
918
874
919
875
@@ -1,133 +1,134 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, Float, Integer,
23 Dict, List, Unicode, Float, Integer,
24 )
24 )
25
25 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
26 # Classes
27 # Classes
27 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
28
29
29
30
30 class MappingKernelManager(MultiKernelManager):
31 class MappingKernelManager(MultiKernelManager):
31 """A KernelManager that handles notebok mapping and HTTP error handling"""
32 """A KernelManager that handles notebok mapping and HTTP error handling"""
32
33
33 kernel_argv = List(Unicode)
34 kernel_argv = List(Unicode)
34
35
35 time_to_dead = Float(3.0, config=True, help="""Kernel heartbeat interval in seconds.""")
36 time_to_dead = Float(3.0, config=True, help="""Kernel heartbeat interval in seconds.""")
36 first_beat = Float(5.0, config=True, help="Delay (in seconds) before sending first heartbeat.")
37 first_beat = Float(5.0, config=True, help="Delay (in seconds) before sending first heartbeat.")
37
38
38 max_msg_size = Integer(65536, config=True, help="""
39 max_msg_size = Integer(65536, config=True, help="""
39 The max raw message size accepted from the browser
40 The max raw message size accepted from the browser
40 over a WebSocket connection.
41 over a WebSocket connection.
41 """)
42 """)
42
43
43 _notebook_mapping = Dict()
44 _notebook_mapping = Dict()
44
45
45 #-------------------------------------------------------------------------
46 #-------------------------------------------------------------------------
46 # Methods for managing kernels and sessions
47 # Methods for managing kernels and sessions
47 #-------------------------------------------------------------------------
48 #-------------------------------------------------------------------------
48
49
49 def kernel_for_notebook(self, notebook_id):
50 def kernel_for_notebook(self, notebook_id):
50 """Return the kernel_id for a notebook_id or None."""
51 """Return the kernel_id for a notebook_id or None."""
51 return self._notebook_mapping.get(notebook_id)
52 return self._notebook_mapping.get(notebook_id)
52
53
53 def set_kernel_for_notebook(self, notebook_id, kernel_id):
54 def set_kernel_for_notebook(self, notebook_id, kernel_id):
54 """Associate a notebook with a kernel."""
55 """Associate a notebook with a kernel."""
55 if notebook_id is not None:
56 if notebook_id is not None:
56 self._notebook_mapping[notebook_id] = kernel_id
57 self._notebook_mapping[notebook_id] = kernel_id
57
58
58 def notebook_for_kernel(self, kernel_id):
59 def notebook_for_kernel(self, kernel_id):
59 """Return the notebook_id for a kernel_id or None."""
60 """Return the notebook_id for a kernel_id or None."""
60 notebook_ids = [k for k, v in self._notebook_mapping.iteritems() if v == kernel_id]
61 notebook_ids = [k for k, v in self._notebook_mapping.iteritems() if v == kernel_id]
61 if len(notebook_ids) == 1:
62 if len(notebook_ids) == 1:
62 return notebook_ids[0]
63 return notebook_ids[0]
63 else:
64 else:
64 return None
65 return None
65
66
66 def delete_mapping_for_kernel(self, kernel_id):
67 def delete_mapping_for_kernel(self, kernel_id):
67 """Remove the kernel/notebook mapping for kernel_id."""
68 """Remove the kernel/notebook mapping for kernel_id."""
68 notebook_id = self.notebook_for_kernel(kernel_id)
69 notebook_id = self.notebook_for_kernel(kernel_id)
69 if notebook_id is not None:
70 if notebook_id is not None:
70 del self._notebook_mapping[notebook_id]
71 del self._notebook_mapping[notebook_id]
71
72
72 def start_kernel(self, notebook_id=None, **kwargs):
73 def start_kernel(self, notebook_id=None, **kwargs):
73 """Start a kernel for a notebok an return its kernel_id.
74 """Start a kernel for a notebok an return its kernel_id.
74
75
75 Parameters
76 Parameters
76 ----------
77 ----------
77 notebook_id : uuid
78 notebook_id : uuid
78 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
79 is not None, this kernel will be persistent whenever the notebook
80 is not None, this kernel will be persistent whenever the notebook
80 requests a kernel.
81 requests a kernel.
81 """
82 """
82 kernel_id = self.kernel_for_notebook(notebook_id)
83 kernel_id = self.kernel_for_notebook(notebook_id)
83 if kernel_id is None:
84 if kernel_id is None:
84 kwargs['extra_arguments'] = self.kernel_argv
85 kwargs['extra_arguments'] = self.kernel_argv
85 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
86 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
86 self.set_kernel_for_notebook(notebook_id, kernel_id)
87 self.set_kernel_for_notebook(notebook_id, kernel_id)
87 self.log.info("Kernel started: %s" % kernel_id)
88 self.log.info("Kernel started: %s" % kernel_id)
88 self.log.debug("Kernel args: %r" % kwargs)
89 self.log.debug("Kernel args: %r" % kwargs)
89 else:
90 else:
90 self.log.info("Using existing kernel: %s" % kernel_id)
91 self.log.info("Using existing kernel: %s" % kernel_id)
91 return kernel_id
92 return kernel_id
92
93
93 def shutdown_kernel(self, kernel_id, now=False):
94 def shutdown_kernel(self, kernel_id, now=False):
94 """Shutdown a kernel and remove its notebook association."""
95 """Shutdown a kernel and remove its notebook association."""
95 self._check_kernel_id(kernel_id)
96 self._check_kernel_id(kernel_id)
96 super(MappingKernelManager, self).shutdown_kernel(
97 super(MappingKernelManager, self).shutdown_kernel(
97 kernel_id, now=now
98 kernel_id, now=now
98 )
99 )
99 self.delete_mapping_for_kernel(kernel_id)
100 self.delete_mapping_for_kernel(kernel_id)
100 self.log.info("Kernel shutdown: %s" % kernel_id)
101 self.log.info("Kernel shutdown: %s" % kernel_id)
101
102
102 def interrupt_kernel(self, kernel_id):
103 def interrupt_kernel(self, kernel_id):
103 """Interrupt a kernel."""
104 """Interrupt a kernel."""
104 self._check_kernel_id(kernel_id)
105 self._check_kernel_id(kernel_id)
105 super(MappingKernelManager, self).interrupt_kernel(kernel_id)
106 super(MappingKernelManager, self).interrupt_kernel(kernel_id)
106 self.log.info("Kernel interrupted: %s" % kernel_id)
107 self.log.info("Kernel interrupted: %s" % kernel_id)
107
108
108 def restart_kernel(self, kernel_id):
109 def restart_kernel(self, kernel_id):
109 """Restart a kernel while keeping clients connected."""
110 """Restart a kernel while keeping clients connected."""
110 self._check_kernel_id(kernel_id)
111 self._check_kernel_id(kernel_id)
111 super(MappingKernelManager, self).restart_kernel(kernel_id)
112 super(MappingKernelManager, self).restart_kernel(kernel_id)
112 self.log.info("Kernel restarted: %s" % kernel_id)
113 self.log.info("Kernel restarted: %s" % kernel_id)
113
114
114 def create_iopub_stream(self, kernel_id):
115 def create_iopub_stream(self, kernel_id):
115 """Create a new iopub stream."""
116 """Create a new iopub stream."""
116 self._check_kernel_id(kernel_id)
117 self._check_kernel_id(kernel_id)
117 return super(MappingKernelManager, self).create_iopub_stream(kernel_id)
118 return super(MappingKernelManager, self).create_iopub_stream(kernel_id)
118
119
119 def create_shell_stream(self, kernel_id):
120 def create_shell_stream(self, kernel_id):
120 """Create a new shell stream."""
121 """Create a new shell stream."""
121 self._check_kernel_id(kernel_id)
122 self._check_kernel_id(kernel_id)
122 return super(MappingKernelManager, self).create_shell_stream(kernel_id)
123 return super(MappingKernelManager, self).create_shell_stream(kernel_id)
123
124
124 def create_hb_stream(self, kernel_id):
125 def create_hb_stream(self, kernel_id):
125 """Create a new hb stream."""
126 """Create a new hb stream."""
126 self._check_kernel_id(kernel_id)
127 self._check_kernel_id(kernel_id)
127 return super(MappingKernelManager, self).create_hb_stream(kernel_id)
128 return super(MappingKernelManager, self).create_hb_stream(kernel_id)
128
129
129 def _check_kernel_id(self, kernel_id):
130 def _check_kernel_id(self, kernel_id):
130 """Check a that a kernel_id exists and raise 404 if not."""
131 """Check a that a kernel_id exists and raise 404 if not."""
131 if kernel_id not in self:
132 if kernel_id not in self:
132 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
133 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
133
134
General Comments 0
You need to be logged in to leave comments. Login now