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