##// END OF EJS Templates
Add info, error and warning message boxes.
Stefan van der Walt -
Show More
@@ -1,576 +1,576 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 logging
19 import logging
20 import Cookie
20 import Cookie
21 import uuid
21 import uuid
22
22
23 from tornado import web
23 from tornado import web
24 from tornado import websocket
24 from tornado import websocket
25
25
26 from zmq.eventloop import ioloop
26 from zmq.eventloop import ioloop
27 from zmq.utils import jsonapi
27 from zmq.utils import jsonapi
28
28
29 from IPython.external.decorator import decorator
29 from IPython.external.decorator import decorator
30 from IPython.zmq.session import Session
30 from IPython.zmq.session import Session
31 from IPython.lib.security import passwd_check
31 from IPython.lib.security import passwd_check
32
32
33 try:
33 try:
34 from docutils.core import publish_string
34 from docutils.core import publish_string
35 except ImportError:
35 except ImportError:
36 publish_string = None
36 publish_string = None
37
37
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
39 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41
41
42 # Google Chrome, as of release 16, changed its websocket protocol number. The
42 # Google Chrome, as of release 16, changed its websocket protocol number. The
43 # parts tornado cares about haven't really changed, so it's OK to continue
43 # parts tornado cares about haven't really changed, so it's OK to continue
44 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
44 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
45 # version as of Oct 30/2011) the version check fails, see the issue report:
45 # version as of Oct 30/2011) the version check fails, see the issue report:
46
46
47 # https://github.com/facebook/tornado/issues/385
47 # https://github.com/facebook/tornado/issues/385
48
48
49 # This issue has been fixed in Tornado post 2.1.1:
49 # This issue has been fixed in Tornado post 2.1.1:
50
50
51 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
51 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
52
52
53 # Here we manually apply the same patch as above so that users of IPython can
53 # Here we manually apply the same patch as above so that users of IPython can
54 # continue to work with an officially released Tornado. We make the
54 # continue to work with an officially released Tornado. We make the
55 # monkeypatch version check as narrow as possible to limit its effects; once
55 # monkeypatch version check as narrow as possible to limit its effects; once
56 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
56 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
57
57
58 import tornado
58 import tornado
59
59
60 if tornado.version_info <= (2,1,1):
60 if tornado.version_info <= (2,1,1):
61
61
62 def _execute(self, transforms, *args, **kwargs):
62 def _execute(self, transforms, *args, **kwargs):
63 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
63 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
64
64
65 self.open_args = args
65 self.open_args = args
66 self.open_kwargs = kwargs
66 self.open_kwargs = kwargs
67
67
68 # The difference between version 8 and 13 is that in 8 the
68 # The difference between version 8 and 13 is that in 8 the
69 # client sends a "Sec-Websocket-Origin" header and in 13 it's
69 # client sends a "Sec-Websocket-Origin" header and in 13 it's
70 # simply "Origin".
70 # simply "Origin".
71 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
71 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
72 self.ws_connection = WebSocketProtocol8(self)
72 self.ws_connection = WebSocketProtocol8(self)
73 self.ws_connection.accept_connection()
73 self.ws_connection.accept_connection()
74
74
75 elif self.request.headers.get("Sec-WebSocket-Version"):
75 elif self.request.headers.get("Sec-WebSocket-Version"):
76 self.stream.write(tornado.escape.utf8(
76 self.stream.write(tornado.escape.utf8(
77 "HTTP/1.1 426 Upgrade Required\r\n"
77 "HTTP/1.1 426 Upgrade Required\r\n"
78 "Sec-WebSocket-Version: 8\r\n\r\n"))
78 "Sec-WebSocket-Version: 8\r\n\r\n"))
79 self.stream.close()
79 self.stream.close()
80
80
81 else:
81 else:
82 self.ws_connection = WebSocketProtocol76(self)
82 self.ws_connection = WebSocketProtocol76(self)
83 self.ws_connection.accept_connection()
83 self.ws_connection.accept_connection()
84
84
85 websocket.WebSocketHandler._execute = _execute
85 websocket.WebSocketHandler._execute = _execute
86 del _execute
86 del _execute
87
87
88 #-----------------------------------------------------------------------------
88 #-----------------------------------------------------------------------------
89 # Decorator for disabling read-only handlers
89 # Decorator for disabling read-only handlers
90 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
91
91
92 @decorator
92 @decorator
93 def not_if_readonly(f, self, *args, **kwargs):
93 def not_if_readonly(f, self, *args, **kwargs):
94 if self.application.read_only:
94 if self.application.read_only:
95 raise web.HTTPError(403, "Notebook server is read-only")
95 raise web.HTTPError(403, "Notebook server is read-only")
96 else:
96 else:
97 return f(self, *args, **kwargs)
97 return f(self, *args, **kwargs)
98
98
99 @decorator
99 @decorator
100 def authenticate_unless_readonly(f, self, *args, **kwargs):
100 def authenticate_unless_readonly(f, self, *args, **kwargs):
101 """authenticate this page *unless* readonly view is active.
101 """authenticate this page *unless* readonly view is active.
102
102
103 In read-only mode, the notebook list and print view should
103 In read-only mode, the notebook list and print view should
104 be accessible without authentication.
104 be accessible without authentication.
105 """
105 """
106
106
107 @web.authenticated
107 @web.authenticated
108 def auth_f(self, *args, **kwargs):
108 def auth_f(self, *args, **kwargs):
109 return f(self, *args, **kwargs)
109 return f(self, *args, **kwargs)
110 if self.application.read_only:
110 if self.application.read_only:
111 return f(self, *args, **kwargs)
111 return f(self, *args, **kwargs)
112 else:
112 else:
113 return auth_f(self, *args, **kwargs)
113 return auth_f(self, *args, **kwargs)
114
114
115 #-----------------------------------------------------------------------------
115 #-----------------------------------------------------------------------------
116 # Top-level handlers
116 # Top-level handlers
117 #-----------------------------------------------------------------------------
117 #-----------------------------------------------------------------------------
118
118
119 class RequestHandler(web.RequestHandler):
119 class RequestHandler(web.RequestHandler):
120 """RequestHandler with default variable setting."""
120 """RequestHandler with default variable setting."""
121
121
122 def render(*args, **kwargs):
122 def render(*args, **kwargs):
123 kwargs.setdefault('message', '')
123 kwargs.setdefault('message', '')
124 return web.RequestHandler.render(*args, **kwargs)
124 return web.RequestHandler.render(*args, **kwargs)
125
125
126 class AuthenticatedHandler(RequestHandler):
126 class AuthenticatedHandler(RequestHandler):
127 """A RequestHandler with an authenticated user."""
127 """A RequestHandler with an authenticated user."""
128
128
129 def get_current_user(self):
129 def get_current_user(self):
130 user_id = self.get_secure_cookie("username")
130 user_id = self.get_secure_cookie("username")
131 # For now the user_id should not return empty, but it could eventually
131 # For now the user_id should not return empty, but it could eventually
132 if user_id == '':
132 if user_id == '':
133 user_id = 'anonymous'
133 user_id = 'anonymous'
134 if user_id is None:
134 if user_id is None:
135 # prevent extra Invalid cookie sig warnings:
135 # prevent extra Invalid cookie sig warnings:
136 self.clear_cookie('username')
136 self.clear_cookie('username')
137 if not self.application.password and not self.application.read_only:
137 if not self.application.password and not self.application.read_only:
138 user_id = 'anonymous'
138 user_id = 'anonymous'
139 return user_id
139 return user_id
140
140
141 @property
141 @property
142 def read_only(self):
142 def read_only(self):
143 if self.application.read_only:
143 if self.application.read_only:
144 if self.application.password:
144 if self.application.password:
145 return self.get_current_user() is None
145 return self.get_current_user() is None
146 else:
146 else:
147 return True
147 return True
148 else:
148 else:
149 return False
149 return False
150
150
151 @property
151 @property
152 def ws_url(self):
152 def ws_url(self):
153 """websocket url matching the current request
153 """websocket url matching the current request
154
154
155 turns http[s]://host[:port] into
155 turns http[s]://host[:port] into
156 ws[s]://host[:port]
156 ws[s]://host[:port]
157 """
157 """
158 proto = self.request.protocol.replace('http', 'ws')
158 proto = self.request.protocol.replace('http', 'ws')
159 return "%s://%s" % (proto, self.request.host)
159 return "%s://%s" % (proto, self.request.host)
160
160
161
161
162 class ProjectDashboardHandler(AuthenticatedHandler):
162 class ProjectDashboardHandler(AuthenticatedHandler):
163
163
164 @authenticate_unless_readonly
164 @authenticate_unless_readonly
165 def get(self):
165 def get(self):
166 nbm = self.application.notebook_manager
166 nbm = self.application.notebook_manager
167 project = nbm.notebook_dir
167 project = nbm.notebook_dir
168 self.render(
168 self.render(
169 'projectdashboard.html', project=project,
169 'projectdashboard.html', project=project,
170 base_project_url=u'/', base_kernel_url=u'/',
170 base_project_url=u'/', base_kernel_url=u'/',
171 read_only=self.read_only,
171 read_only=self.read_only,
172 )
172 )
173
173
174
174
175 class LoginHandler(AuthenticatedHandler):
175 class LoginHandler(AuthenticatedHandler):
176
176
177 def _render(self, message=''):
177 def _render(self, message=None):
178 self.render('login.html',
178 self.render('login.html',
179 next=self.get_argument('next', default='/'),
179 next=self.get_argument('next', default='/'),
180 read_only=self.read_only,
180 read_only=self.read_only,
181 message=message
181 message=message
182 )
182 )
183
183
184 def get(self):
184 def get(self):
185 self._render()
185 self._render()
186
186
187 def post(self):
187 def post(self):
188 pwd = self.get_argument('password', default=u'')
188 pwd = self.get_argument('password', default=u'')
189 if self.application.password:
189 if self.application.password:
190 if passwd_check(self.application.password, pwd):
190 if passwd_check(self.application.password, pwd):
191 self.set_secure_cookie('username', str(uuid.uuid4()))
191 self.set_secure_cookie('username', str(uuid.uuid4()))
192 else:
192 else:
193 self._render(message='Invalid password')
193 self._render(message={'error': 'Invalid password'})
194 return
194 return
195
195
196 self.redirect(self.get_argument('next', default='/'))
196 self.redirect(self.get_argument('next', default='/'))
197
197
198
198
199 class LogoutHandler(AuthenticatedHandler):
199 class LogoutHandler(AuthenticatedHandler):
200
200
201 def get(self):
201 def get(self):
202 self.clear_cookie('username')
202 self.clear_cookie('username')
203 self.render('logout.html')
203 self.render('logout.html', message={'info': 'Successfully logged out.'})
204
204
205
205
206 class NewHandler(AuthenticatedHandler):
206 class NewHandler(AuthenticatedHandler):
207
207
208 @web.authenticated
208 @web.authenticated
209 def get(self):
209 def get(self):
210 nbm = self.application.notebook_manager
210 nbm = self.application.notebook_manager
211 project = nbm.notebook_dir
211 project = nbm.notebook_dir
212 notebook_id = nbm.new_notebook()
212 notebook_id = nbm.new_notebook()
213 self.render(
213 self.render(
214 'notebook.html', project=project,
214 'notebook.html', project=project,
215 notebook_id=notebook_id,
215 notebook_id=notebook_id,
216 base_project_url=u'/', base_kernel_url=u'/',
216 base_project_url=u'/', base_kernel_url=u'/',
217 kill_kernel=False,
217 kill_kernel=False,
218 read_only=False,
218 read_only=False,
219 )
219 )
220
220
221
221
222 class NamedNotebookHandler(AuthenticatedHandler):
222 class NamedNotebookHandler(AuthenticatedHandler):
223
223
224 @authenticate_unless_readonly
224 @authenticate_unless_readonly
225 def get(self, notebook_id):
225 def get(self, notebook_id):
226 nbm = self.application.notebook_manager
226 nbm = self.application.notebook_manager
227 project = nbm.notebook_dir
227 project = nbm.notebook_dir
228 if not nbm.notebook_exists(notebook_id):
228 if not nbm.notebook_exists(notebook_id):
229 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
229 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
230
230
231 self.render(
231 self.render(
232 'notebook.html', project=project,
232 'notebook.html', project=project,
233 notebook_id=notebook_id,
233 notebook_id=notebook_id,
234 base_project_url=u'/', base_kernel_url=u'/',
234 base_project_url=u'/', base_kernel_url=u'/',
235 kill_kernel=False,
235 kill_kernel=False,
236 read_only=self.read_only,
236 read_only=self.read_only,
237 )
237 )
238
238
239
239
240 #-----------------------------------------------------------------------------
240 #-----------------------------------------------------------------------------
241 # Kernel handlers
241 # Kernel handlers
242 #-----------------------------------------------------------------------------
242 #-----------------------------------------------------------------------------
243
243
244
244
245 class MainKernelHandler(AuthenticatedHandler):
245 class MainKernelHandler(AuthenticatedHandler):
246
246
247 @web.authenticated
247 @web.authenticated
248 def get(self):
248 def get(self):
249 km = self.application.kernel_manager
249 km = self.application.kernel_manager
250 self.finish(jsonapi.dumps(km.kernel_ids))
250 self.finish(jsonapi.dumps(km.kernel_ids))
251
251
252 @web.authenticated
252 @web.authenticated
253 def post(self):
253 def post(self):
254 km = self.application.kernel_manager
254 km = self.application.kernel_manager
255 notebook_id = self.get_argument('notebook', default=None)
255 notebook_id = self.get_argument('notebook', default=None)
256 kernel_id = km.start_kernel(notebook_id)
256 kernel_id = km.start_kernel(notebook_id)
257 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
257 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
258 self.set_header('Location', '/'+kernel_id)
258 self.set_header('Location', '/'+kernel_id)
259 self.finish(jsonapi.dumps(data))
259 self.finish(jsonapi.dumps(data))
260
260
261
261
262 class KernelHandler(AuthenticatedHandler):
262 class KernelHandler(AuthenticatedHandler):
263
263
264 SUPPORTED_METHODS = ('DELETE')
264 SUPPORTED_METHODS = ('DELETE')
265
265
266 @web.authenticated
266 @web.authenticated
267 def delete(self, kernel_id):
267 def delete(self, kernel_id):
268 km = self.application.kernel_manager
268 km = self.application.kernel_manager
269 km.kill_kernel(kernel_id)
269 km.kill_kernel(kernel_id)
270 self.set_status(204)
270 self.set_status(204)
271 self.finish()
271 self.finish()
272
272
273
273
274 class KernelActionHandler(AuthenticatedHandler):
274 class KernelActionHandler(AuthenticatedHandler):
275
275
276 @web.authenticated
276 @web.authenticated
277 def post(self, kernel_id, action):
277 def post(self, kernel_id, action):
278 km = self.application.kernel_manager
278 km = self.application.kernel_manager
279 if action == 'interrupt':
279 if action == 'interrupt':
280 km.interrupt_kernel(kernel_id)
280 km.interrupt_kernel(kernel_id)
281 self.set_status(204)
281 self.set_status(204)
282 if action == 'restart':
282 if action == 'restart':
283 new_kernel_id = km.restart_kernel(kernel_id)
283 new_kernel_id = km.restart_kernel(kernel_id)
284 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
284 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
285 self.set_header('Location', '/'+new_kernel_id)
285 self.set_header('Location', '/'+new_kernel_id)
286 self.write(jsonapi.dumps(data))
286 self.write(jsonapi.dumps(data))
287 self.finish()
287 self.finish()
288
288
289
289
290 class ZMQStreamHandler(websocket.WebSocketHandler):
290 class ZMQStreamHandler(websocket.WebSocketHandler):
291
291
292 def _reserialize_reply(self, msg_list):
292 def _reserialize_reply(self, msg_list):
293 """Reserialize a reply message using JSON.
293 """Reserialize a reply message using JSON.
294
294
295 This takes the msg list from the ZMQ socket, unserializes it using
295 This takes the msg list from the ZMQ socket, unserializes it using
296 self.session and then serializes the result using JSON. This method
296 self.session and then serializes the result using JSON. This method
297 should be used by self._on_zmq_reply to build messages that can
297 should be used by self._on_zmq_reply to build messages that can
298 be sent back to the browser.
298 be sent back to the browser.
299 """
299 """
300 idents, msg_list = self.session.feed_identities(msg_list)
300 idents, msg_list = self.session.feed_identities(msg_list)
301 msg = self.session.unserialize(msg_list)
301 msg = self.session.unserialize(msg_list)
302 try:
302 try:
303 msg['header'].pop('date')
303 msg['header'].pop('date')
304 except KeyError:
304 except KeyError:
305 pass
305 pass
306 try:
306 try:
307 msg['parent_header'].pop('date')
307 msg['parent_header'].pop('date')
308 except KeyError:
308 except KeyError:
309 pass
309 pass
310 msg.pop('buffers')
310 msg.pop('buffers')
311 return jsonapi.dumps(msg)
311 return jsonapi.dumps(msg)
312
312
313 def _on_zmq_reply(self, msg_list):
313 def _on_zmq_reply(self, msg_list):
314 try:
314 try:
315 msg = self._reserialize_reply(msg_list)
315 msg = self._reserialize_reply(msg_list)
316 except:
316 except:
317 self.application.log.critical("Malformed message: %r" % msg_list)
317 self.application.log.critical("Malformed message: %r" % msg_list)
318 else:
318 else:
319 self.write_message(msg)
319 self.write_message(msg)
320
320
321
321
322 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
322 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
323
323
324 def open(self, kernel_id):
324 def open(self, kernel_id):
325 self.kernel_id = kernel_id.decode('ascii')
325 self.kernel_id = kernel_id.decode('ascii')
326 try:
326 try:
327 cfg = self.application.ipython_app.config
327 cfg = self.application.ipython_app.config
328 except AttributeError:
328 except AttributeError:
329 # protect from the case where this is run from something other than
329 # protect from the case where this is run from something other than
330 # the notebook app:
330 # the notebook app:
331 cfg = None
331 cfg = None
332 self.session = Session(config=cfg)
332 self.session = Session(config=cfg)
333 self.save_on_message = self.on_message
333 self.save_on_message = self.on_message
334 self.on_message = self.on_first_message
334 self.on_message = self.on_first_message
335
335
336 def get_current_user(self):
336 def get_current_user(self):
337 user_id = self.get_secure_cookie("username")
337 user_id = self.get_secure_cookie("username")
338 if user_id == '' or (user_id is None and not self.application.password):
338 if user_id == '' or (user_id is None and not self.application.password):
339 user_id = 'anonymous'
339 user_id = 'anonymous'
340 return user_id
340 return user_id
341
341
342 def _inject_cookie_message(self, msg):
342 def _inject_cookie_message(self, msg):
343 """Inject the first message, which is the document cookie,
343 """Inject the first message, which is the document cookie,
344 for authentication."""
344 for authentication."""
345 if isinstance(msg, unicode):
345 if isinstance(msg, unicode):
346 # Cookie can't constructor doesn't accept unicode strings for some reason
346 # Cookie can't constructor doesn't accept unicode strings for some reason
347 msg = msg.encode('utf8', 'replace')
347 msg = msg.encode('utf8', 'replace')
348 try:
348 try:
349 self.request._cookies = Cookie.SimpleCookie(msg)
349 self.request._cookies = Cookie.SimpleCookie(msg)
350 except:
350 except:
351 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
351 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
352
352
353 def on_first_message(self, msg):
353 def on_first_message(self, msg):
354 self._inject_cookie_message(msg)
354 self._inject_cookie_message(msg)
355 if self.get_current_user() is None:
355 if self.get_current_user() is None:
356 logging.warn("Couldn't authenticate WebSocket connection")
356 logging.warn("Couldn't authenticate WebSocket connection")
357 raise web.HTTPError(403)
357 raise web.HTTPError(403)
358 self.on_message = self.save_on_message
358 self.on_message = self.save_on_message
359
359
360
360
361 class IOPubHandler(AuthenticatedZMQStreamHandler):
361 class IOPubHandler(AuthenticatedZMQStreamHandler):
362
362
363 def initialize(self, *args, **kwargs):
363 def initialize(self, *args, **kwargs):
364 self._kernel_alive = True
364 self._kernel_alive = True
365 self._beating = False
365 self._beating = False
366 self.iopub_stream = None
366 self.iopub_stream = None
367 self.hb_stream = None
367 self.hb_stream = None
368
368
369 def on_first_message(self, msg):
369 def on_first_message(self, msg):
370 try:
370 try:
371 super(IOPubHandler, self).on_first_message(msg)
371 super(IOPubHandler, self).on_first_message(msg)
372 except web.HTTPError:
372 except web.HTTPError:
373 self.close()
373 self.close()
374 return
374 return
375 km = self.application.kernel_manager
375 km = self.application.kernel_manager
376 self.time_to_dead = km.time_to_dead
376 self.time_to_dead = km.time_to_dead
377 kernel_id = self.kernel_id
377 kernel_id = self.kernel_id
378 try:
378 try:
379 self.iopub_stream = km.create_iopub_stream(kernel_id)
379 self.iopub_stream = km.create_iopub_stream(kernel_id)
380 self.hb_stream = km.create_hb_stream(kernel_id)
380 self.hb_stream = km.create_hb_stream(kernel_id)
381 except web.HTTPError:
381 except web.HTTPError:
382 # WebSockets don't response to traditional error codes so we
382 # WebSockets don't response to traditional error codes so we
383 # close the connection.
383 # close the connection.
384 if not self.stream.closed():
384 if not self.stream.closed():
385 self.stream.close()
385 self.stream.close()
386 self.close()
386 self.close()
387 else:
387 else:
388 self.iopub_stream.on_recv(self._on_zmq_reply)
388 self.iopub_stream.on_recv(self._on_zmq_reply)
389 self.start_hb(self.kernel_died)
389 self.start_hb(self.kernel_died)
390
390
391 def on_message(self, msg):
391 def on_message(self, msg):
392 pass
392 pass
393
393
394 def on_close(self):
394 def on_close(self):
395 # This method can be called twice, once by self.kernel_died and once
395 # This method can be called twice, once by self.kernel_died and once
396 # from the WebSocket close event. If the WebSocket connection is
396 # from the WebSocket close event. If the WebSocket connection is
397 # closed before the ZMQ streams are setup, they could be None.
397 # closed before the ZMQ streams are setup, they could be None.
398 self.stop_hb()
398 self.stop_hb()
399 if self.iopub_stream is not None and not self.iopub_stream.closed():
399 if self.iopub_stream is not None and not self.iopub_stream.closed():
400 self.iopub_stream.on_recv(None)
400 self.iopub_stream.on_recv(None)
401 self.iopub_stream.close()
401 self.iopub_stream.close()
402 if self.hb_stream is not None and not self.hb_stream.closed():
402 if self.hb_stream is not None and not self.hb_stream.closed():
403 self.hb_stream.close()
403 self.hb_stream.close()
404
404
405 def start_hb(self, callback):
405 def start_hb(self, callback):
406 """Start the heartbeating and call the callback if the kernel dies."""
406 """Start the heartbeating and call the callback if the kernel dies."""
407 if not self._beating:
407 if not self._beating:
408 self._kernel_alive = True
408 self._kernel_alive = True
409
409
410 def ping_or_dead():
410 def ping_or_dead():
411 if self._kernel_alive:
411 if self._kernel_alive:
412 self._kernel_alive = False
412 self._kernel_alive = False
413 self.hb_stream.send(b'ping')
413 self.hb_stream.send(b'ping')
414 else:
414 else:
415 try:
415 try:
416 callback()
416 callback()
417 except:
417 except:
418 pass
418 pass
419 finally:
419 finally:
420 self._hb_periodic_callback.stop()
420 self._hb_periodic_callback.stop()
421
421
422 def beat_received(msg):
422 def beat_received(msg):
423 self._kernel_alive = True
423 self._kernel_alive = True
424
424
425 self.hb_stream.on_recv(beat_received)
425 self.hb_stream.on_recv(beat_received)
426 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000)
426 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000)
427 self._hb_periodic_callback.start()
427 self._hb_periodic_callback.start()
428 self._beating= True
428 self._beating= True
429
429
430 def stop_hb(self):
430 def stop_hb(self):
431 """Stop the heartbeating and cancel all related callbacks."""
431 """Stop the heartbeating and cancel all related callbacks."""
432 if self._beating:
432 if self._beating:
433 self._hb_periodic_callback.stop()
433 self._hb_periodic_callback.stop()
434 if not self.hb_stream.closed():
434 if not self.hb_stream.closed():
435 self.hb_stream.on_recv(None)
435 self.hb_stream.on_recv(None)
436
436
437 def kernel_died(self):
437 def kernel_died(self):
438 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
438 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
439 self.write_message(
439 self.write_message(
440 {'header': {'msg_type': 'status'},
440 {'header': {'msg_type': 'status'},
441 'parent_header': {},
441 'parent_header': {},
442 'content': {'execution_state':'dead'}
442 'content': {'execution_state':'dead'}
443 }
443 }
444 )
444 )
445 self.on_close()
445 self.on_close()
446
446
447
447
448 class ShellHandler(AuthenticatedZMQStreamHandler):
448 class ShellHandler(AuthenticatedZMQStreamHandler):
449
449
450 def initialize(self, *args, **kwargs):
450 def initialize(self, *args, **kwargs):
451 self.shell_stream = None
451 self.shell_stream = None
452
452
453 def on_first_message(self, msg):
453 def on_first_message(self, msg):
454 try:
454 try:
455 super(ShellHandler, self).on_first_message(msg)
455 super(ShellHandler, self).on_first_message(msg)
456 except web.HTTPError:
456 except web.HTTPError:
457 self.close()
457 self.close()
458 return
458 return
459 km = self.application.kernel_manager
459 km = self.application.kernel_manager
460 self.max_msg_size = km.max_msg_size
460 self.max_msg_size = km.max_msg_size
461 kernel_id = self.kernel_id
461 kernel_id = self.kernel_id
462 try:
462 try:
463 self.shell_stream = km.create_shell_stream(kernel_id)
463 self.shell_stream = km.create_shell_stream(kernel_id)
464 except web.HTTPError:
464 except web.HTTPError:
465 # WebSockets don't response to traditional error codes so we
465 # WebSockets don't response to traditional error codes so we
466 # close the connection.
466 # close the connection.
467 if not self.stream.closed():
467 if not self.stream.closed():
468 self.stream.close()
468 self.stream.close()
469 self.close()
469 self.close()
470 else:
470 else:
471 self.shell_stream.on_recv(self._on_zmq_reply)
471 self.shell_stream.on_recv(self._on_zmq_reply)
472
472
473 def on_message(self, msg):
473 def on_message(self, msg):
474 if len(msg) < self.max_msg_size:
474 if len(msg) < self.max_msg_size:
475 msg = jsonapi.loads(msg)
475 msg = jsonapi.loads(msg)
476 self.session.send(self.shell_stream, msg)
476 self.session.send(self.shell_stream, msg)
477
477
478 def on_close(self):
478 def on_close(self):
479 # Make sure the stream exists and is not already closed.
479 # Make sure the stream exists and is not already closed.
480 if self.shell_stream is not None and not self.shell_stream.closed():
480 if self.shell_stream is not None and not self.shell_stream.closed():
481 self.shell_stream.close()
481 self.shell_stream.close()
482
482
483
483
484 #-----------------------------------------------------------------------------
484 #-----------------------------------------------------------------------------
485 # Notebook web service handlers
485 # Notebook web service handlers
486 #-----------------------------------------------------------------------------
486 #-----------------------------------------------------------------------------
487
487
488 class NotebookRootHandler(AuthenticatedHandler):
488 class NotebookRootHandler(AuthenticatedHandler):
489
489
490 @authenticate_unless_readonly
490 @authenticate_unless_readonly
491 def get(self):
491 def get(self):
492
492
493 nbm = self.application.notebook_manager
493 nbm = self.application.notebook_manager
494 files = nbm.list_notebooks()
494 files = nbm.list_notebooks()
495 self.finish(jsonapi.dumps(files))
495 self.finish(jsonapi.dumps(files))
496
496
497 @web.authenticated
497 @web.authenticated
498 def post(self):
498 def post(self):
499 nbm = self.application.notebook_manager
499 nbm = self.application.notebook_manager
500 body = self.request.body.strip()
500 body = self.request.body.strip()
501 format = self.get_argument('format', default='json')
501 format = self.get_argument('format', default='json')
502 name = self.get_argument('name', default=None)
502 name = self.get_argument('name', default=None)
503 if body:
503 if body:
504 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
504 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
505 else:
505 else:
506 notebook_id = nbm.new_notebook()
506 notebook_id = nbm.new_notebook()
507 self.set_header('Location', '/'+notebook_id)
507 self.set_header('Location', '/'+notebook_id)
508 self.finish(jsonapi.dumps(notebook_id))
508 self.finish(jsonapi.dumps(notebook_id))
509
509
510
510
511 class NotebookHandler(AuthenticatedHandler):
511 class NotebookHandler(AuthenticatedHandler):
512
512
513 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
513 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
514
514
515 @authenticate_unless_readonly
515 @authenticate_unless_readonly
516 def get(self, notebook_id):
516 def get(self, notebook_id):
517 nbm = self.application.notebook_manager
517 nbm = self.application.notebook_manager
518 format = self.get_argument('format', default='json')
518 format = self.get_argument('format', default='json')
519 last_mod, name, data = nbm.get_notebook(notebook_id, format)
519 last_mod, name, data = nbm.get_notebook(notebook_id, format)
520
520
521 if format == u'json':
521 if format == u'json':
522 self.set_header('Content-Type', 'application/json')
522 self.set_header('Content-Type', 'application/json')
523 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
523 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
524 elif format == u'py':
524 elif format == u'py':
525 self.set_header('Content-Type', 'application/x-python')
525 self.set_header('Content-Type', 'application/x-python')
526 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
526 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
527 self.set_header('Last-Modified', last_mod)
527 self.set_header('Last-Modified', last_mod)
528 self.finish(data)
528 self.finish(data)
529
529
530 @web.authenticated
530 @web.authenticated
531 def put(self, notebook_id):
531 def put(self, notebook_id):
532 nbm = self.application.notebook_manager
532 nbm = self.application.notebook_manager
533 format = self.get_argument('format', default='json')
533 format = self.get_argument('format', default='json')
534 name = self.get_argument('name', default=None)
534 name = self.get_argument('name', default=None)
535 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
535 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
536 self.set_status(204)
536 self.set_status(204)
537 self.finish()
537 self.finish()
538
538
539 @web.authenticated
539 @web.authenticated
540 def delete(self, notebook_id):
540 def delete(self, notebook_id):
541 nbm = self.application.notebook_manager
541 nbm = self.application.notebook_manager
542 nbm.delete_notebook(notebook_id)
542 nbm.delete_notebook(notebook_id)
543 self.set_status(204)
543 self.set_status(204)
544 self.finish()
544 self.finish()
545
545
546 #-----------------------------------------------------------------------------
546 #-----------------------------------------------------------------------------
547 # RST web service handlers
547 # RST web service handlers
548 #-----------------------------------------------------------------------------
548 #-----------------------------------------------------------------------------
549
549
550
550
551 class RSTHandler(AuthenticatedHandler):
551 class RSTHandler(AuthenticatedHandler):
552
552
553 @web.authenticated
553 @web.authenticated
554 def post(self):
554 def post(self):
555 if publish_string is None:
555 if publish_string is None:
556 raise web.HTTPError(503, u'docutils not available')
556 raise web.HTTPError(503, u'docutils not available')
557 body = self.request.body.strip()
557 body = self.request.body.strip()
558 source = body
558 source = body
559 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
559 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
560 defaults = {'file_insertion_enabled': 0,
560 defaults = {'file_insertion_enabled': 0,
561 'raw_enabled': 0,
561 'raw_enabled': 0,
562 '_disable_config': 1,
562 '_disable_config': 1,
563 'stylesheet_path': 0
563 'stylesheet_path': 0
564 # 'template': template_path
564 # 'template': template_path
565 }
565 }
566 try:
566 try:
567 html = publish_string(source, writer_name='html',
567 html = publish_string(source, writer_name='html',
568 settings_overrides=defaults
568 settings_overrides=defaults
569 )
569 )
570 except:
570 except:
571 raise web.HTTPError(400, u'Invalid RST')
571 raise web.HTTPError(400, u'Invalid RST')
572 print html
572 print html
573 self.set_header('Content-Type', 'text/html')
573 self.set_header('Content-Type', 'text/html')
574 self.finish(html)
574 self.finish(html)
575
575
576
576
@@ -1,115 +1,130 b''
1
1
2 .border-box-sizing {
2 .border-box-sizing {
3 box-sizing: border-box;
3 box-sizing: border-box;
4 -moz-box-sizing: border-box;
4 -moz-box-sizing: border-box;
5 -webkit-box-sizing: border-box;
5 -webkit-box-sizing: border-box;
6 }
6 }
7
7
8 /* Flexible box model classes */
8 /* Flexible box model classes */
9 /* Taken from Alex Russell http://infrequently.org/2009/08/css-3-progress/ */
9 /* Taken from Alex Russell http://infrequently.org/2009/08/css-3-progress/ */
10
10
11 .hbox {
11 .hbox {
12 display: -webkit-box;
12 display: -webkit-box;
13 -webkit-box-orient: horizontal;
13 -webkit-box-orient: horizontal;
14 -webkit-box-align: stretch;
14 -webkit-box-align: stretch;
15
15
16 display: -moz-box;
16 display: -moz-box;
17 -moz-box-orient: horizontal;
17 -moz-box-orient: horizontal;
18 -moz-box-align: stretch;
18 -moz-box-align: stretch;
19
19
20 display: box;
20 display: box;
21 box-orient: horizontal;
21 box-orient: horizontal;
22 box-align: stretch;
22 box-align: stretch;
23 }
23 }
24
24
25 .hbox > * {
25 .hbox > * {
26 -webkit-box-flex: 0;
26 -webkit-box-flex: 0;
27 -moz-box-flex: 0;
27 -moz-box-flex: 0;
28 box-flex: 0;
28 box-flex: 0;
29 }
29 }
30
30
31 .vbox {
31 .vbox {
32 display: -webkit-box;
32 display: -webkit-box;
33 -webkit-box-orient: vertical;
33 -webkit-box-orient: vertical;
34 -webkit-box-align: stretch;
34 -webkit-box-align: stretch;
35
35
36 display: -moz-box;
36 display: -moz-box;
37 -moz-box-orient: vertical;
37 -moz-box-orient: vertical;
38 -moz-box-align: stretch;
38 -moz-box-align: stretch;
39
39
40 display: box;
40 display: box;
41 box-orient: vertical;
41 box-orient: vertical;
42 box-align: stretch;
42 box-align: stretch;
43 }
43 }
44
44
45 .vbox > * {
45 .vbox > * {
46 -webkit-box-flex: 0;
46 -webkit-box-flex: 0;
47 -moz-box-flex: 0;
47 -moz-box-flex: 0;
48 box-flex: 0;
48 box-flex: 0;
49 }
49 }
50
50
51 .reverse {
51 .reverse {
52 -webkit-box-direction: reverse;
52 -webkit-box-direction: reverse;
53 -moz-box-direction: reverse;
53 -moz-box-direction: reverse;
54 box-direction: reverse;
54 box-direction: reverse;
55 }
55 }
56
56
57 .box-flex0 {
57 .box-flex0 {
58 -webkit-box-flex: 0;
58 -webkit-box-flex: 0;
59 -moz-box-flex: 0;
59 -moz-box-flex: 0;
60 box-flex: 0;
60 box-flex: 0;
61 }
61 }
62
62
63 .box-flex1, .box-flex {
63 .box-flex1, .box-flex {
64 -webkit-box-flex: 1;
64 -webkit-box-flex: 1;
65 -moz-box-flex: 1;
65 -moz-box-flex: 1;
66 box-flex: 1;
66 box-flex: 1;
67 }
67 }
68
68
69 .box-flex2 {
69 .box-flex2 {
70 -webkit-box-flex: 2;
70 -webkit-box-flex: 2;
71 -moz-box-flex: 2;
71 -moz-box-flex: 2;
72 box-flex: 2;
72 box-flex: 2;
73 }
73 }
74
74
75 .box-group1 {
75 .box-group1 {
76 -webkit-box-flex-group: 1;
76 -webkit-box-flex-group: 1;
77 -moz-box-flex-group: 1;
77 -moz-box-flex-group: 1;
78 box-flex-group: 1;
78 box-flex-group: 1;
79 }
79 }
80
80
81 .box-group2 {
81 .box-group2 {
82 -webkit-box-flex-group: 2;
82 -webkit-box-flex-group: 2;
83 -moz-box-flex-group: 2;
83 -moz-box-flex-group: 2;
84 box-flex-group: 2;
84 box-flex-group: 2;
85 }
85 }
86
86
87 .start {
87 .start {
88 -webkit-box-pack: start;
88 -webkit-box-pack: start;
89 -moz-box-pack: start;
89 -moz-box-pack: start;
90 box-pack: start;
90 box-pack: start;
91 }
91 }
92
92
93 .end {
93 .end {
94 -webkit-box-pack: end;
94 -webkit-box-pack: end;
95 -moz-box-pack: end;
95 -moz-box-pack: end;
96 box-pack: end;
96 box-pack: end;
97 }
97 }
98
98
99 .center {
99 .center {
100 -webkit-box-pack: center;
100 -webkit-box-pack: center;
101 -moz-box-pack: center;
101 -moz-box-pack: center;
102 box-pack: center;
102 box-pack: center;
103 }
103 }
104
104
105 #message {
105 .message {
106 border: 1px solid red;
106 border-width: 1px;
107 background-color: #FFD3D1;
107 border-style: solid;
108 text-align: center;
108 text-align: center;
109 padding: 0.5em;
109 padding: 0.5em;
110 margin: 0.5em;
110 margin: 0.5em 0;
111 }
112
113 .message.error {
114 background-color: #FFD3D1;
115 border-color: red;
116 }
117
118 .message.warning {
119 background-color: #FFD09E;
120 border-color: orange;
121 }
122
123 .message.info {
124 background-color: #CBFFBA;
125 border-color: green;
111 }
126 }
112
127
113 #content_panel {
128 #content_panel {
114 margin: 0.5em;
129 margin: 0.5em;
115 } No newline at end of file
130 }
@@ -1,72 +1,75 b''
1 <!DOCTYPE HTML>
1 <!DOCTYPE HTML>
2 <html>
2 <html>
3
3
4 <head>
4 <head>
5 <meta charset="utf-8">
5 <meta charset="utf-8">
6
6
7 <title>{% block title %}IPython Notebook{% end %}</title>
7 <title>{% block title %}IPython Notebook{% end %}</title>
8
8
9 <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
9 <link rel="stylesheet" href="static/jquery/css/themes/aristo/jquery-wijmo.css" type="text/css" />
10 <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
10 <link rel="stylesheet" href="static/css/boilerplate.css" type="text/css" />
11 <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
11 <link rel="stylesheet" href="static/css/layout.css" type="text/css" />
12 <link rel="stylesheet" href="static/css/base.css" type="text/css"/>
12 <link rel="stylesheet" href="static/css/base.css" type="text/css"/>
13 {% block stylesheet %}
13 {% block stylesheet %}
14 {% end %}
14 {% end %}
15
15
16 {% block meta %}
16 {% block meta %}
17 {% end %}
17 {% end %}
18
18
19 </head>
19 </head>
20
20
21 <body {% block params %}{% end %}>
21 <body {% block params %}{% end %}>
22
22
23 <div id="header">
23 <div id="header">
24 <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
24 <span id="ipython_notebook"><h1>IPython Notebook</h1></span>
25 <span id="login_widget">
25 <span id="login_widget">
26 <button id="logout">Logout</button>
26 <button id="logout">Logout</button>
27 </span>
27 </span>
28 {% block header %}
28 {% block header %}
29 {% end %}
29 {% end %}
30 </div>
30 </div>
31
31
32 <div id="header_border"></div>
32 <div id="header_border"></div>
33
33
34 <div id="main_app">
34 <div id="main_app">
35
35
36 <div id="app_hbox">
36 <div id="app_hbox">
37
37
38 <div id="left_panel">
38 <div id="left_panel">
39 {% block left_panel %}
39 {% block left_panel %}
40 {% end %}
40 {% end %}
41 </div>
41 </div>
42
42
43 <div id="content_panel">
43 <div id="content_panel">
44 {% if message %}
44 {% if message %}
45 <div id="message">
45
46 {{message}}
46 {% for key in message %}
47 <div class="message {{key}}">
48 {{message[key]}}
47 </div>
49 </div>
48 {% end %}
50 {% end %}
51 {% end %}
49
52
50 {% block content_panel %}
53 {% block content_panel %}
51 {% end %}
54 {% end %}
52 </div>
55 </div>
53 <div id="right_panel">
56 <div id="right_panel">
54 {% block right_panel %}
57 {% block right_panel %}
55 {% end %}
58 {% end %}
56 </div>
59 </div>
57
60
58 </div>
61 </div>
59
62
60 </div>
63 </div>
61
64
62 <script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
65 <script src="static/jquery/js/jquery-1.6.2.min.js" type="text/javascript" charset="utf-8"></script>
63 <script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
66 <script src="static/jquery/js/jquery-ui-1.8.14.custom.min.js" type="text/javascript" charset="utf-8"></script>
64 <script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
67 <script src="static/js/namespace.js" type="text/javascript" charset="utf-8"></script>
65 <script src="static/js/loginmain.js" type="text/javascript" charset="utf-8"></script>
68 <script src="static/js/loginmain.js" type="text/javascript" charset="utf-8"></script>
66 <script src="static/js/loginwidget.js" type="text/javascript" charset="utf-8"></script>
69 <script src="static/js/loginwidget.js" type="text/javascript" charset="utf-8"></script>
67 {% block script %}
70 {% block script %}
68 {% end %}
71 {% end %}
69
72
70 </body>
73 </body>
71
74
72 </html>
75 </html>
@@ -1,5 +1,5 b''
1 {% extends layout.html %}
1 {% extends layout.html %}
2
2
3 {% block content_panel %}
3 {% block content_panel %}
4 You've been successfully logged out.
4 Proceed to the <a href="/login">login page</a>.
5 {% end %}
5 {% end %}
General Comments 0
You need to be logged in to leave comments. Login now