##// END OF EJS Templates
Merge pull request #1 from minrk/kill-bg-processes...
Takafumi Arakaki -
r7628:375a648c merge
parent child Browse files
Show More
@@ -1,737 +1,737 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 time
21 import time
22 import uuid
22 import uuid
23
23
24 from tornado import web
24 from tornado import web
25 from tornado import websocket
25 from tornado import websocket
26
26
27 from zmq.eventloop import ioloop
27 from zmq.eventloop import ioloop
28 from zmq.utils import jsonapi
28 from zmq.utils import jsonapi
29
29
30 from IPython.external.decorator import decorator
30 from IPython.external.decorator import decorator
31 from IPython.zmq.session import Session
31 from IPython.zmq.session import Session
32 from IPython.lib.security import passwd_check
32 from IPython.lib.security import passwd_check
33 from IPython.utils.jsonutil import date_default
33 from IPython.utils.jsonutil import date_default
34
34
35 try:
35 try:
36 from docutils.core import publish_string
36 from docutils.core import publish_string
37 except ImportError:
37 except ImportError:
38 publish_string = None
38 publish_string = None
39
39
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
41 # Monkeypatch for Tornado <= 2.1.1 - Remove when no longer necessary!
42 #-----------------------------------------------------------------------------
42 #-----------------------------------------------------------------------------
43
43
44 # Google Chrome, as of release 16, changed its websocket protocol number. The
44 # Google Chrome, as of release 16, changed its websocket protocol number. The
45 # parts tornado cares about haven't really changed, so it's OK to continue
45 # parts tornado cares about haven't really changed, so it's OK to continue
46 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
46 # accepting Chrome connections, but as of Tornado 2.1.1 (the currently released
47 # version as of Oct 30/2011) the version check fails, see the issue report:
47 # version as of Oct 30/2011) the version check fails, see the issue report:
48
48
49 # https://github.com/facebook/tornado/issues/385
49 # https://github.com/facebook/tornado/issues/385
50
50
51 # This issue has been fixed in Tornado post 2.1.1:
51 # This issue has been fixed in Tornado post 2.1.1:
52
52
53 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
53 # https://github.com/facebook/tornado/commit/84d7b458f956727c3b0d6710
54
54
55 # Here we manually apply the same patch as above so that users of IPython can
55 # Here we manually apply the same patch as above so that users of IPython can
56 # continue to work with an officially released Tornado. We make the
56 # continue to work with an officially released Tornado. We make the
57 # monkeypatch version check as narrow as possible to limit its effects; once
57 # monkeypatch version check as narrow as possible to limit its effects; once
58 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
58 # Tornado 2.1.1 is no longer found in the wild we'll delete this code.
59
59
60 import tornado
60 import tornado
61
61
62 if tornado.version_info <= (2,1,1):
62 if tornado.version_info <= (2,1,1):
63
63
64 def _execute(self, transforms, *args, **kwargs):
64 def _execute(self, transforms, *args, **kwargs):
65 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
65 from tornado.websocket import WebSocketProtocol8, WebSocketProtocol76
66
66
67 self.open_args = args
67 self.open_args = args
68 self.open_kwargs = kwargs
68 self.open_kwargs = kwargs
69
69
70 # The difference between version 8 and 13 is that in 8 the
70 # The difference between version 8 and 13 is that in 8 the
71 # client sends a "Sec-Websocket-Origin" header and in 13 it's
71 # client sends a "Sec-Websocket-Origin" header and in 13 it's
72 # simply "Origin".
72 # simply "Origin".
73 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
73 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8", "13"):
74 self.ws_connection = WebSocketProtocol8(self)
74 self.ws_connection = WebSocketProtocol8(self)
75 self.ws_connection.accept_connection()
75 self.ws_connection.accept_connection()
76
76
77 elif self.request.headers.get("Sec-WebSocket-Version"):
77 elif self.request.headers.get("Sec-WebSocket-Version"):
78 self.stream.write(tornado.escape.utf8(
78 self.stream.write(tornado.escape.utf8(
79 "HTTP/1.1 426 Upgrade Required\r\n"
79 "HTTP/1.1 426 Upgrade Required\r\n"
80 "Sec-WebSocket-Version: 8\r\n\r\n"))
80 "Sec-WebSocket-Version: 8\r\n\r\n"))
81 self.stream.close()
81 self.stream.close()
82
82
83 else:
83 else:
84 self.ws_connection = WebSocketProtocol76(self)
84 self.ws_connection = WebSocketProtocol76(self)
85 self.ws_connection.accept_connection()
85 self.ws_connection.accept_connection()
86
86
87 websocket.WebSocketHandler._execute = _execute
87 websocket.WebSocketHandler._execute = _execute
88 del _execute
88 del _execute
89
89
90 #-----------------------------------------------------------------------------
90 #-----------------------------------------------------------------------------
91 # Decorator for disabling read-only handlers
91 # Decorator for disabling read-only handlers
92 #-----------------------------------------------------------------------------
92 #-----------------------------------------------------------------------------
93
93
94 @decorator
94 @decorator
95 def not_if_readonly(f, self, *args, **kwargs):
95 def not_if_readonly(f, self, *args, **kwargs):
96 if self.application.read_only:
96 if self.application.read_only:
97 raise web.HTTPError(403, "Notebook server is read-only")
97 raise web.HTTPError(403, "Notebook server is read-only")
98 else:
98 else:
99 return f(self, *args, **kwargs)
99 return f(self, *args, **kwargs)
100
100
101 @decorator
101 @decorator
102 def authenticate_unless_readonly(f, self, *args, **kwargs):
102 def authenticate_unless_readonly(f, self, *args, **kwargs):
103 """authenticate this page *unless* readonly view is active.
103 """authenticate this page *unless* readonly view is active.
104
104
105 In read-only mode, the notebook list and print view should
105 In read-only mode, the notebook list and print view should
106 be accessible without authentication.
106 be accessible without authentication.
107 """
107 """
108
108
109 @web.authenticated
109 @web.authenticated
110 def auth_f(self, *args, **kwargs):
110 def auth_f(self, *args, **kwargs):
111 return f(self, *args, **kwargs)
111 return f(self, *args, **kwargs)
112
112
113 if self.application.read_only:
113 if self.application.read_only:
114 return f(self, *args, **kwargs)
114 return f(self, *args, **kwargs)
115 else:
115 else:
116 return auth_f(self, *args, **kwargs)
116 return auth_f(self, *args, **kwargs)
117
117
118 #-----------------------------------------------------------------------------
118 #-----------------------------------------------------------------------------
119 # Top-level handlers
119 # Top-level handlers
120 #-----------------------------------------------------------------------------
120 #-----------------------------------------------------------------------------
121
121
122 class RequestHandler(web.RequestHandler):
122 class RequestHandler(web.RequestHandler):
123 """RequestHandler with default variable setting."""
123 """RequestHandler with default variable setting."""
124
124
125 def render(*args, **kwargs):
125 def render(*args, **kwargs):
126 kwargs.setdefault('message', '')
126 kwargs.setdefault('message', '')
127 return web.RequestHandler.render(*args, **kwargs)
127 return web.RequestHandler.render(*args, **kwargs)
128
128
129 class AuthenticatedHandler(RequestHandler):
129 class AuthenticatedHandler(RequestHandler):
130 """A RequestHandler with an authenticated user."""
130 """A RequestHandler with an authenticated user."""
131
131
132 def get_current_user(self):
132 def get_current_user(self):
133 user_id = self.get_secure_cookie("username")
133 user_id = self.get_secure_cookie("username")
134 # For now the user_id should not return empty, but it could eventually
134 # For now the user_id should not return empty, but it could eventually
135 if user_id == '':
135 if user_id == '':
136 user_id = 'anonymous'
136 user_id = 'anonymous'
137 if user_id is None:
137 if user_id is None:
138 # prevent extra Invalid cookie sig warnings:
138 # prevent extra Invalid cookie sig warnings:
139 self.clear_cookie('username')
139 self.clear_cookie('username')
140 if not self.application.password and not self.application.read_only:
140 if not self.application.password and not self.application.read_only:
141 user_id = 'anonymous'
141 user_id = 'anonymous'
142 return user_id
142 return user_id
143
143
144 @property
144 @property
145 def logged_in(self):
145 def logged_in(self):
146 """Is a user currently logged in?
146 """Is a user currently logged in?
147
147
148 """
148 """
149 user = self.get_current_user()
149 user = self.get_current_user()
150 return (user and not user == 'anonymous')
150 return (user and not user == 'anonymous')
151
151
152 @property
152 @property
153 def login_available(self):
153 def login_available(self):
154 """May a user proceed to log in?
154 """May a user proceed to log in?
155
155
156 This returns True if login capability is available, irrespective of
156 This returns True if login capability is available, irrespective of
157 whether the user is already logged in or not.
157 whether the user is already logged in or not.
158
158
159 """
159 """
160 return bool(self.application.password)
160 return bool(self.application.password)
161
161
162 @property
162 @property
163 def read_only(self):
163 def read_only(self):
164 """Is the notebook read-only?
164 """Is the notebook read-only?
165
165
166 """
166 """
167 return self.application.read_only
167 return self.application.read_only
168
168
169 @property
169 @property
170 def ws_url(self):
170 def ws_url(self):
171 """websocket url matching the current request
171 """websocket url matching the current request
172
172
173 turns http[s]://host[:port] into
173 turns http[s]://host[:port] into
174 ws[s]://host[:port]
174 ws[s]://host[:port]
175 """
175 """
176 proto = self.request.protocol.replace('http', 'ws')
176 proto = self.request.protocol.replace('http', 'ws')
177 host = self.application.ipython_app.websocket_host # default to config value
177 host = self.application.ipython_app.websocket_host # default to config value
178 if host == '':
178 if host == '':
179 host = self.request.host # get from request
179 host = self.request.host # get from request
180 return "%s://%s" % (proto, host)
180 return "%s://%s" % (proto, host)
181
181
182
182
183 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
183 class AuthenticatedFileHandler(AuthenticatedHandler, web.StaticFileHandler):
184 """static files should only be accessible when logged in"""
184 """static files should only be accessible when logged in"""
185
185
186 @authenticate_unless_readonly
186 @authenticate_unless_readonly
187 def get(self, path):
187 def get(self, path):
188 return web.StaticFileHandler.get(self, path)
188 return web.StaticFileHandler.get(self, path)
189
189
190
190
191 class ProjectDashboardHandler(AuthenticatedHandler):
191 class ProjectDashboardHandler(AuthenticatedHandler):
192
192
193 @authenticate_unless_readonly
193 @authenticate_unless_readonly
194 def get(self):
194 def get(self):
195 nbm = self.application.notebook_manager
195 nbm = self.application.notebook_manager
196 project = nbm.notebook_dir
196 project = nbm.notebook_dir
197 self.render(
197 self.render(
198 'projectdashboard.html', project=project,
198 'projectdashboard.html', project=project,
199 base_project_url=self.application.ipython_app.base_project_url,
199 base_project_url=self.application.ipython_app.base_project_url,
200 base_kernel_url=self.application.ipython_app.base_kernel_url,
200 base_kernel_url=self.application.ipython_app.base_kernel_url,
201 read_only=self.read_only,
201 read_only=self.read_only,
202 logged_in=self.logged_in,
202 logged_in=self.logged_in,
203 login_available=self.login_available
203 login_available=self.login_available
204 )
204 )
205
205
206
206
207 class LoginHandler(AuthenticatedHandler):
207 class LoginHandler(AuthenticatedHandler):
208
208
209 def _render(self, message=None):
209 def _render(self, message=None):
210 self.render('login.html',
210 self.render('login.html',
211 next=self.get_argument('next', default='/'),
211 next=self.get_argument('next', default='/'),
212 read_only=self.read_only,
212 read_only=self.read_only,
213 logged_in=self.logged_in,
213 logged_in=self.logged_in,
214 login_available=self.login_available,
214 login_available=self.login_available,
215 base_project_url=self.application.ipython_app.base_project_url,
215 base_project_url=self.application.ipython_app.base_project_url,
216 message=message
216 message=message
217 )
217 )
218
218
219 def get(self):
219 def get(self):
220 if self.current_user:
220 if self.current_user:
221 self.redirect(self.get_argument('next', default='/'))
221 self.redirect(self.get_argument('next', default='/'))
222 else:
222 else:
223 self._render()
223 self._render()
224
224
225 def post(self):
225 def post(self):
226 pwd = self.get_argument('password', default=u'')
226 pwd = self.get_argument('password', default=u'')
227 if self.application.password:
227 if self.application.password:
228 if passwd_check(self.application.password, pwd):
228 if passwd_check(self.application.password, pwd):
229 self.set_secure_cookie('username', str(uuid.uuid4()))
229 self.set_secure_cookie('username', str(uuid.uuid4()))
230 else:
230 else:
231 self._render(message={'error': 'Invalid password'})
231 self._render(message={'error': 'Invalid password'})
232 return
232 return
233
233
234 self.redirect(self.get_argument('next', default='/'))
234 self.redirect(self.get_argument('next', default='/'))
235
235
236
236
237 class LogoutHandler(AuthenticatedHandler):
237 class LogoutHandler(AuthenticatedHandler):
238
238
239 def get(self):
239 def get(self):
240 self.clear_cookie('username')
240 self.clear_cookie('username')
241 if self.login_available:
241 if self.login_available:
242 message = {'info': 'Successfully logged out.'}
242 message = {'info': 'Successfully logged out.'}
243 else:
243 else:
244 message = {'warning': 'Cannot log out. Notebook authentication '
244 message = {'warning': 'Cannot log out. Notebook authentication '
245 'is disabled.'}
245 'is disabled.'}
246
246
247 self.render('logout.html',
247 self.render('logout.html',
248 read_only=self.read_only,
248 read_only=self.read_only,
249 logged_in=self.logged_in,
249 logged_in=self.logged_in,
250 login_available=self.login_available,
250 login_available=self.login_available,
251 base_project_url=self.application.ipython_app.base_project_url,
251 base_project_url=self.application.ipython_app.base_project_url,
252 message=message)
252 message=message)
253
253
254
254
255 class NewHandler(AuthenticatedHandler):
255 class NewHandler(AuthenticatedHandler):
256
256
257 @web.authenticated
257 @web.authenticated
258 def get(self):
258 def get(self):
259 nbm = self.application.notebook_manager
259 nbm = self.application.notebook_manager
260 project = nbm.notebook_dir
260 project = nbm.notebook_dir
261 notebook_id = nbm.new_notebook()
261 notebook_id = nbm.new_notebook()
262 self.render(
262 self.render(
263 'notebook.html', project=project,
263 'notebook.html', project=project,
264 notebook_id=notebook_id,
264 notebook_id=notebook_id,
265 base_project_url=self.application.ipython_app.base_project_url,
265 base_project_url=self.application.ipython_app.base_project_url,
266 base_kernel_url=self.application.ipython_app.base_kernel_url,
266 base_kernel_url=self.application.ipython_app.base_kernel_url,
267 kill_kernel=False,
267 kill_kernel=False,
268 read_only=False,
268 read_only=False,
269 logged_in=self.logged_in,
269 logged_in=self.logged_in,
270 login_available=self.login_available,
270 login_available=self.login_available,
271 mathjax_url=self.application.ipython_app.mathjax_url,
271 mathjax_url=self.application.ipython_app.mathjax_url,
272 )
272 )
273
273
274
274
275 class NamedNotebookHandler(AuthenticatedHandler):
275 class NamedNotebookHandler(AuthenticatedHandler):
276
276
277 @authenticate_unless_readonly
277 @authenticate_unless_readonly
278 def get(self, notebook_id):
278 def get(self, notebook_id):
279 nbm = self.application.notebook_manager
279 nbm = self.application.notebook_manager
280 project = nbm.notebook_dir
280 project = nbm.notebook_dir
281 if not nbm.notebook_exists(notebook_id):
281 if not nbm.notebook_exists(notebook_id):
282 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
282 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
283
283
284 self.render(
284 self.render(
285 'notebook.html', project=project,
285 'notebook.html', project=project,
286 notebook_id=notebook_id,
286 notebook_id=notebook_id,
287 base_project_url=self.application.ipython_app.base_project_url,
287 base_project_url=self.application.ipython_app.base_project_url,
288 base_kernel_url=self.application.ipython_app.base_kernel_url,
288 base_kernel_url=self.application.ipython_app.base_kernel_url,
289 kill_kernel=False,
289 kill_kernel=False,
290 read_only=self.read_only,
290 read_only=self.read_only,
291 logged_in=self.logged_in,
291 logged_in=self.logged_in,
292 login_available=self.login_available,
292 login_available=self.login_available,
293 mathjax_url=self.application.ipython_app.mathjax_url,
293 mathjax_url=self.application.ipython_app.mathjax_url,
294 )
294 )
295
295
296
296
297 class PrintNotebookHandler(AuthenticatedHandler):
297 class PrintNotebookHandler(AuthenticatedHandler):
298
298
299 @authenticate_unless_readonly
299 @authenticate_unless_readonly
300 def get(self, notebook_id):
300 def get(self, notebook_id):
301 nbm = self.application.notebook_manager
301 nbm = self.application.notebook_manager
302 project = nbm.notebook_dir
302 project = nbm.notebook_dir
303 if not nbm.notebook_exists(notebook_id):
303 if not nbm.notebook_exists(notebook_id):
304 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
304 raise web.HTTPError(404, u'Notebook does not exist: %s' % notebook_id)
305
305
306 self.render(
306 self.render(
307 'printnotebook.html', project=project,
307 'printnotebook.html', project=project,
308 notebook_id=notebook_id,
308 notebook_id=notebook_id,
309 base_project_url=self.application.ipython_app.base_project_url,
309 base_project_url=self.application.ipython_app.base_project_url,
310 base_kernel_url=self.application.ipython_app.base_kernel_url,
310 base_kernel_url=self.application.ipython_app.base_kernel_url,
311 kill_kernel=False,
311 kill_kernel=False,
312 read_only=self.read_only,
312 read_only=self.read_only,
313 logged_in=self.logged_in,
313 logged_in=self.logged_in,
314 login_available=self.login_available,
314 login_available=self.login_available,
315 mathjax_url=self.application.ipython_app.mathjax_url,
315 mathjax_url=self.application.ipython_app.mathjax_url,
316 )
316 )
317
317
318 #-----------------------------------------------------------------------------
318 #-----------------------------------------------------------------------------
319 # Kernel handlers
319 # Kernel handlers
320 #-----------------------------------------------------------------------------
320 #-----------------------------------------------------------------------------
321
321
322
322
323 class MainKernelHandler(AuthenticatedHandler):
323 class MainKernelHandler(AuthenticatedHandler):
324
324
325 @web.authenticated
325 @web.authenticated
326 def get(self):
326 def get(self):
327 km = self.application.kernel_manager
327 km = self.application.kernel_manager
328 self.finish(jsonapi.dumps(km.kernel_ids))
328 self.finish(jsonapi.dumps(km.kernel_ids))
329
329
330 @web.authenticated
330 @web.authenticated
331 def post(self):
331 def post(self):
332 km = self.application.kernel_manager
332 km = self.application.kernel_manager
333 notebook_id = self.get_argument('notebook', default=None)
333 notebook_id = self.get_argument('notebook', default=None)
334 kernel_id = km.start_kernel(notebook_id)
334 kernel_id = km.start_kernel(notebook_id)
335 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
335 data = {'ws_url':self.ws_url,'kernel_id':kernel_id}
336 self.set_header('Location', '/'+kernel_id)
336 self.set_header('Location', '/'+kernel_id)
337 self.finish(jsonapi.dumps(data))
337 self.finish(jsonapi.dumps(data))
338
338
339
339
340 class KernelHandler(AuthenticatedHandler):
340 class KernelHandler(AuthenticatedHandler):
341
341
342 SUPPORTED_METHODS = ('DELETE')
342 SUPPORTED_METHODS = ('DELETE')
343
343
344 @web.authenticated
344 @web.authenticated
345 def delete(self, kernel_id):
345 def delete(self, kernel_id):
346 km = self.application.kernel_manager
346 km = self.application.kernel_manager
347 km.kill_kernel(kernel_id)
347 km.shutdown_kernel(kernel_id)
348 self.set_status(204)
348 self.set_status(204)
349 self.finish()
349 self.finish()
350
350
351
351
352 class KernelActionHandler(AuthenticatedHandler):
352 class KernelActionHandler(AuthenticatedHandler):
353
353
354 @web.authenticated
354 @web.authenticated
355 def post(self, kernel_id, action):
355 def post(self, kernel_id, action):
356 km = self.application.kernel_manager
356 km = self.application.kernel_manager
357 if action == 'interrupt':
357 if action == 'interrupt':
358 km.interrupt_kernel(kernel_id)
358 km.interrupt_kernel(kernel_id)
359 self.set_status(204)
359 self.set_status(204)
360 if action == 'restart':
360 if action == 'restart':
361 new_kernel_id = km.restart_kernel(kernel_id)
361 new_kernel_id = km.restart_kernel(kernel_id)
362 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
362 data = {'ws_url':self.ws_url,'kernel_id':new_kernel_id}
363 self.set_header('Location', '/'+new_kernel_id)
363 self.set_header('Location', '/'+new_kernel_id)
364 self.write(jsonapi.dumps(data))
364 self.write(jsonapi.dumps(data))
365 self.finish()
365 self.finish()
366
366
367
367
368 class ZMQStreamHandler(websocket.WebSocketHandler):
368 class ZMQStreamHandler(websocket.WebSocketHandler):
369
369
370 def _reserialize_reply(self, msg_list):
370 def _reserialize_reply(self, msg_list):
371 """Reserialize a reply message using JSON.
371 """Reserialize a reply message using JSON.
372
372
373 This takes the msg list from the ZMQ socket, unserializes it using
373 This takes the msg list from the ZMQ socket, unserializes it using
374 self.session and then serializes the result using JSON. This method
374 self.session and then serializes the result using JSON. This method
375 should be used by self._on_zmq_reply to build messages that can
375 should be used by self._on_zmq_reply to build messages that can
376 be sent back to the browser.
376 be sent back to the browser.
377 """
377 """
378 idents, msg_list = self.session.feed_identities(msg_list)
378 idents, msg_list = self.session.feed_identities(msg_list)
379 msg = self.session.unserialize(msg_list)
379 msg = self.session.unserialize(msg_list)
380 try:
380 try:
381 msg['header'].pop('date')
381 msg['header'].pop('date')
382 except KeyError:
382 except KeyError:
383 pass
383 pass
384 try:
384 try:
385 msg['parent_header'].pop('date')
385 msg['parent_header'].pop('date')
386 except KeyError:
386 except KeyError:
387 pass
387 pass
388 msg.pop('buffers')
388 msg.pop('buffers')
389 return jsonapi.dumps(msg, default=date_default)
389 return jsonapi.dumps(msg, default=date_default)
390
390
391 def _on_zmq_reply(self, msg_list):
391 def _on_zmq_reply(self, msg_list):
392 try:
392 try:
393 msg = self._reserialize_reply(msg_list)
393 msg = self._reserialize_reply(msg_list)
394 except Exception:
394 except Exception:
395 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
395 self.application.log.critical("Malformed message: %r" % msg_list, exc_info=True)
396 else:
396 else:
397 self.write_message(msg)
397 self.write_message(msg)
398
398
399 def allow_draft76(self):
399 def allow_draft76(self):
400 """Allow draft 76, until browsers such as Safari update to RFC 6455.
400 """Allow draft 76, until browsers such as Safari update to RFC 6455.
401
401
402 This has been disabled by default in tornado in release 2.2.0, and
402 This has been disabled by default in tornado in release 2.2.0, and
403 support will be removed in later versions.
403 support will be removed in later versions.
404 """
404 """
405 return True
405 return True
406
406
407
407
408 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
408 class AuthenticatedZMQStreamHandler(ZMQStreamHandler):
409
409
410 def open(self, kernel_id):
410 def open(self, kernel_id):
411 self.kernel_id = kernel_id.decode('ascii')
411 self.kernel_id = kernel_id.decode('ascii')
412 try:
412 try:
413 cfg = self.application.ipython_app.config
413 cfg = self.application.ipython_app.config
414 except AttributeError:
414 except AttributeError:
415 # protect from the case where this is run from something other than
415 # protect from the case where this is run from something other than
416 # the notebook app:
416 # the notebook app:
417 cfg = None
417 cfg = None
418 self.session = Session(config=cfg)
418 self.session = Session(config=cfg)
419 self.save_on_message = self.on_message
419 self.save_on_message = self.on_message
420 self.on_message = self.on_first_message
420 self.on_message = self.on_first_message
421
421
422 def get_current_user(self):
422 def get_current_user(self):
423 user_id = self.get_secure_cookie("username")
423 user_id = self.get_secure_cookie("username")
424 if user_id == '' or (user_id is None and not self.application.password):
424 if user_id == '' or (user_id is None and not self.application.password):
425 user_id = 'anonymous'
425 user_id = 'anonymous'
426 return user_id
426 return user_id
427
427
428 def _inject_cookie_message(self, msg):
428 def _inject_cookie_message(self, msg):
429 """Inject the first message, which is the document cookie,
429 """Inject the first message, which is the document cookie,
430 for authentication."""
430 for authentication."""
431 if isinstance(msg, unicode):
431 if isinstance(msg, unicode):
432 # Cookie can't constructor doesn't accept unicode strings for some reason
432 # Cookie can't constructor doesn't accept unicode strings for some reason
433 msg = msg.encode('utf8', 'replace')
433 msg = msg.encode('utf8', 'replace')
434 try:
434 try:
435 self.request._cookies = Cookie.SimpleCookie(msg)
435 self.request._cookies = Cookie.SimpleCookie(msg)
436 except:
436 except:
437 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
437 logging.warn("couldn't parse cookie string: %s",msg, exc_info=True)
438
438
439 def on_first_message(self, msg):
439 def on_first_message(self, msg):
440 self._inject_cookie_message(msg)
440 self._inject_cookie_message(msg)
441 if self.get_current_user() is None:
441 if self.get_current_user() is None:
442 logging.warn("Couldn't authenticate WebSocket connection")
442 logging.warn("Couldn't authenticate WebSocket connection")
443 raise web.HTTPError(403)
443 raise web.HTTPError(403)
444 self.on_message = self.save_on_message
444 self.on_message = self.save_on_message
445
445
446
446
447 class IOPubHandler(AuthenticatedZMQStreamHandler):
447 class IOPubHandler(AuthenticatedZMQStreamHandler):
448
448
449 def initialize(self, *args, **kwargs):
449 def initialize(self, *args, **kwargs):
450 self._kernel_alive = True
450 self._kernel_alive = True
451 self._beating = False
451 self._beating = False
452 self.iopub_stream = None
452 self.iopub_stream = None
453 self.hb_stream = None
453 self.hb_stream = None
454
454
455 def on_first_message(self, msg):
455 def on_first_message(self, msg):
456 try:
456 try:
457 super(IOPubHandler, self).on_first_message(msg)
457 super(IOPubHandler, self).on_first_message(msg)
458 except web.HTTPError:
458 except web.HTTPError:
459 self.close()
459 self.close()
460 return
460 return
461 km = self.application.kernel_manager
461 km = self.application.kernel_manager
462 self.time_to_dead = km.time_to_dead
462 self.time_to_dead = km.time_to_dead
463 self.first_beat = km.first_beat
463 self.first_beat = km.first_beat
464 kernel_id = self.kernel_id
464 kernel_id = self.kernel_id
465 try:
465 try:
466 self.iopub_stream = km.create_iopub_stream(kernel_id)
466 self.iopub_stream = km.create_iopub_stream(kernel_id)
467 self.hb_stream = km.create_hb_stream(kernel_id)
467 self.hb_stream = km.create_hb_stream(kernel_id)
468 except web.HTTPError:
468 except web.HTTPError:
469 # WebSockets don't response to traditional error codes so we
469 # WebSockets don't response to traditional error codes so we
470 # close the connection.
470 # close the connection.
471 if not self.stream.closed():
471 if not self.stream.closed():
472 self.stream.close()
472 self.stream.close()
473 self.close()
473 self.close()
474 else:
474 else:
475 self.iopub_stream.on_recv(self._on_zmq_reply)
475 self.iopub_stream.on_recv(self._on_zmq_reply)
476 self.start_hb(self.kernel_died)
476 self.start_hb(self.kernel_died)
477
477
478 def on_message(self, msg):
478 def on_message(self, msg):
479 pass
479 pass
480
480
481 def on_close(self):
481 def on_close(self):
482 # This method can be called twice, once by self.kernel_died and once
482 # This method can be called twice, once by self.kernel_died and once
483 # from the WebSocket close event. If the WebSocket connection is
483 # from the WebSocket close event. If the WebSocket connection is
484 # closed before the ZMQ streams are setup, they could be None.
484 # closed before the ZMQ streams are setup, they could be None.
485 self.stop_hb()
485 self.stop_hb()
486 if self.iopub_stream is not None and not self.iopub_stream.closed():
486 if self.iopub_stream is not None and not self.iopub_stream.closed():
487 self.iopub_stream.on_recv(None)
487 self.iopub_stream.on_recv(None)
488 self.iopub_stream.close()
488 self.iopub_stream.close()
489 if self.hb_stream is not None and not self.hb_stream.closed():
489 if self.hb_stream is not None and not self.hb_stream.closed():
490 self.hb_stream.close()
490 self.hb_stream.close()
491
491
492 def start_hb(self, callback):
492 def start_hb(self, callback):
493 """Start the heartbeating and call the callback if the kernel dies."""
493 """Start the heartbeating and call the callback if the kernel dies."""
494 if not self._beating:
494 if not self._beating:
495 self._kernel_alive = True
495 self._kernel_alive = True
496
496
497 def ping_or_dead():
497 def ping_or_dead():
498 self.hb_stream.flush()
498 self.hb_stream.flush()
499 if self._kernel_alive:
499 if self._kernel_alive:
500 self._kernel_alive = False
500 self._kernel_alive = False
501 self.hb_stream.send(b'ping')
501 self.hb_stream.send(b'ping')
502 # flush stream to force immediate socket send
502 # flush stream to force immediate socket send
503 self.hb_stream.flush()
503 self.hb_stream.flush()
504 else:
504 else:
505 try:
505 try:
506 callback()
506 callback()
507 except:
507 except:
508 pass
508 pass
509 finally:
509 finally:
510 self.stop_hb()
510 self.stop_hb()
511
511
512 def beat_received(msg):
512 def beat_received(msg):
513 self._kernel_alive = True
513 self._kernel_alive = True
514
514
515 self.hb_stream.on_recv(beat_received)
515 self.hb_stream.on_recv(beat_received)
516 loop = ioloop.IOLoop.instance()
516 loop = ioloop.IOLoop.instance()
517 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
517 self._hb_periodic_callback = ioloop.PeriodicCallback(ping_or_dead, self.time_to_dead*1000, loop)
518 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
518 loop.add_timeout(time.time()+self.first_beat, self._really_start_hb)
519 self._beating= True
519 self._beating= True
520
520
521 def _really_start_hb(self):
521 def _really_start_hb(self):
522 """callback for delayed heartbeat start
522 """callback for delayed heartbeat start
523
523
524 Only start the hb loop if we haven't been closed during the wait.
524 Only start the hb loop if we haven't been closed during the wait.
525 """
525 """
526 if self._beating and not self.hb_stream.closed():
526 if self._beating and not self.hb_stream.closed():
527 self._hb_periodic_callback.start()
527 self._hb_periodic_callback.start()
528
528
529 def stop_hb(self):
529 def stop_hb(self):
530 """Stop the heartbeating and cancel all related callbacks."""
530 """Stop the heartbeating and cancel all related callbacks."""
531 if self._beating:
531 if self._beating:
532 self._beating = False
532 self._beating = False
533 self._hb_periodic_callback.stop()
533 self._hb_periodic_callback.stop()
534 if not self.hb_stream.closed():
534 if not self.hb_stream.closed():
535 self.hb_stream.on_recv(None)
535 self.hb_stream.on_recv(None)
536
536
537 def kernel_died(self):
537 def kernel_died(self):
538 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
538 self.application.kernel_manager.delete_mapping_for_kernel(self.kernel_id)
539 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
539 self.application.log.error("Kernel %s failed to respond to heartbeat", self.kernel_id)
540 self.write_message(
540 self.write_message(
541 {'header': {'msg_type': 'status'},
541 {'header': {'msg_type': 'status'},
542 'parent_header': {},
542 'parent_header': {},
543 'content': {'execution_state':'dead'}
543 'content': {'execution_state':'dead'}
544 }
544 }
545 )
545 )
546 self.on_close()
546 self.on_close()
547
547
548
548
549 class ShellHandler(AuthenticatedZMQStreamHandler):
549 class ShellHandler(AuthenticatedZMQStreamHandler):
550
550
551 def initialize(self, *args, **kwargs):
551 def initialize(self, *args, **kwargs):
552 self.shell_stream = None
552 self.shell_stream = None
553
553
554 def on_first_message(self, msg):
554 def on_first_message(self, msg):
555 try:
555 try:
556 super(ShellHandler, self).on_first_message(msg)
556 super(ShellHandler, self).on_first_message(msg)
557 except web.HTTPError:
557 except web.HTTPError:
558 self.close()
558 self.close()
559 return
559 return
560 km = self.application.kernel_manager
560 km = self.application.kernel_manager
561 self.max_msg_size = km.max_msg_size
561 self.max_msg_size = km.max_msg_size
562 kernel_id = self.kernel_id
562 kernel_id = self.kernel_id
563 try:
563 try:
564 self.shell_stream = km.create_shell_stream(kernel_id)
564 self.shell_stream = km.create_shell_stream(kernel_id)
565 except web.HTTPError:
565 except web.HTTPError:
566 # WebSockets don't response to traditional error codes so we
566 # WebSockets don't response to traditional error codes so we
567 # close the connection.
567 # close the connection.
568 if not self.stream.closed():
568 if not self.stream.closed():
569 self.stream.close()
569 self.stream.close()
570 self.close()
570 self.close()
571 else:
571 else:
572 self.shell_stream.on_recv(self._on_zmq_reply)
572 self.shell_stream.on_recv(self._on_zmq_reply)
573
573
574 def on_message(self, msg):
574 def on_message(self, msg):
575 if len(msg) < self.max_msg_size:
575 if len(msg) < self.max_msg_size:
576 msg = jsonapi.loads(msg)
576 msg = jsonapi.loads(msg)
577 self.session.send(self.shell_stream, msg)
577 self.session.send(self.shell_stream, msg)
578
578
579 def on_close(self):
579 def on_close(self):
580 # Make sure the stream exists and is not already closed.
580 # Make sure the stream exists and is not already closed.
581 if self.shell_stream is not None and not self.shell_stream.closed():
581 if self.shell_stream is not None and not self.shell_stream.closed():
582 self.shell_stream.close()
582 self.shell_stream.close()
583
583
584
584
585 #-----------------------------------------------------------------------------
585 #-----------------------------------------------------------------------------
586 # Notebook web service handlers
586 # Notebook web service handlers
587 #-----------------------------------------------------------------------------
587 #-----------------------------------------------------------------------------
588
588
589 class NotebookRootHandler(AuthenticatedHandler):
589 class NotebookRootHandler(AuthenticatedHandler):
590
590
591 @authenticate_unless_readonly
591 @authenticate_unless_readonly
592 def get(self):
592 def get(self):
593 nbm = self.application.notebook_manager
593 nbm = self.application.notebook_manager
594 km = self.application.kernel_manager
594 km = self.application.kernel_manager
595 files = nbm.list_notebooks()
595 files = nbm.list_notebooks()
596 for f in files :
596 for f in files :
597 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
597 f['kernel_id'] = km.kernel_for_notebook(f['notebook_id'])
598 self.finish(jsonapi.dumps(files))
598 self.finish(jsonapi.dumps(files))
599
599
600 @web.authenticated
600 @web.authenticated
601 def post(self):
601 def post(self):
602 nbm = self.application.notebook_manager
602 nbm = self.application.notebook_manager
603 body = self.request.body.strip()
603 body = self.request.body.strip()
604 format = self.get_argument('format', default='json')
604 format = self.get_argument('format', default='json')
605 name = self.get_argument('name', default=None)
605 name = self.get_argument('name', default=None)
606 if body:
606 if body:
607 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
607 notebook_id = nbm.save_new_notebook(body, name=name, format=format)
608 else:
608 else:
609 notebook_id = nbm.new_notebook()
609 notebook_id = nbm.new_notebook()
610 self.set_header('Location', '/'+notebook_id)
610 self.set_header('Location', '/'+notebook_id)
611 self.finish(jsonapi.dumps(notebook_id))
611 self.finish(jsonapi.dumps(notebook_id))
612
612
613
613
614 class NotebookHandler(AuthenticatedHandler):
614 class NotebookHandler(AuthenticatedHandler):
615
615
616 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
616 SUPPORTED_METHODS = ('GET', 'PUT', 'DELETE')
617
617
618 @authenticate_unless_readonly
618 @authenticate_unless_readonly
619 def get(self, notebook_id):
619 def get(self, notebook_id):
620 nbm = self.application.notebook_manager
620 nbm = self.application.notebook_manager
621 format = self.get_argument('format', default='json')
621 format = self.get_argument('format', default='json')
622 last_mod, name, data = nbm.get_notebook(notebook_id, format)
622 last_mod, name, data = nbm.get_notebook(notebook_id, format)
623
623
624 if format == u'json':
624 if format == u'json':
625 self.set_header('Content-Type', 'application/json')
625 self.set_header('Content-Type', 'application/json')
626 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
626 self.set_header('Content-Disposition','attachment; filename="%s.ipynb"' % name)
627 elif format == u'py':
627 elif format == u'py':
628 self.set_header('Content-Type', 'application/x-python')
628 self.set_header('Content-Type', 'application/x-python')
629 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
629 self.set_header('Content-Disposition','attachment; filename="%s.py"' % name)
630 self.set_header('Last-Modified', last_mod)
630 self.set_header('Last-Modified', last_mod)
631 self.finish(data)
631 self.finish(data)
632
632
633 @web.authenticated
633 @web.authenticated
634 def put(self, notebook_id):
634 def put(self, notebook_id):
635 nbm = self.application.notebook_manager
635 nbm = self.application.notebook_manager
636 format = self.get_argument('format', default='json')
636 format = self.get_argument('format', default='json')
637 name = self.get_argument('name', default=None)
637 name = self.get_argument('name', default=None)
638 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
638 nbm.save_notebook(notebook_id, self.request.body, name=name, format=format)
639 self.set_status(204)
639 self.set_status(204)
640 self.finish()
640 self.finish()
641
641
642 @web.authenticated
642 @web.authenticated
643 def delete(self, notebook_id):
643 def delete(self, notebook_id):
644 nbm = self.application.notebook_manager
644 nbm = self.application.notebook_manager
645 nbm.delete_notebook(notebook_id)
645 nbm.delete_notebook(notebook_id)
646 self.set_status(204)
646 self.set_status(204)
647 self.finish()
647 self.finish()
648
648
649
649
650 class NotebookCopyHandler(AuthenticatedHandler):
650 class NotebookCopyHandler(AuthenticatedHandler):
651
651
652 @web.authenticated
652 @web.authenticated
653 def get(self, notebook_id):
653 def get(self, notebook_id):
654 nbm = self.application.notebook_manager
654 nbm = self.application.notebook_manager
655 project = nbm.notebook_dir
655 project = nbm.notebook_dir
656 notebook_id = nbm.copy_notebook(notebook_id)
656 notebook_id = nbm.copy_notebook(notebook_id)
657 self.render(
657 self.render(
658 'notebook.html', project=project,
658 'notebook.html', project=project,
659 notebook_id=notebook_id,
659 notebook_id=notebook_id,
660 base_project_url=self.application.ipython_app.base_project_url,
660 base_project_url=self.application.ipython_app.base_project_url,
661 base_kernel_url=self.application.ipython_app.base_kernel_url,
661 base_kernel_url=self.application.ipython_app.base_kernel_url,
662 kill_kernel=False,
662 kill_kernel=False,
663 read_only=False,
663 read_only=False,
664 logged_in=self.logged_in,
664 logged_in=self.logged_in,
665 login_available=self.login_available,
665 login_available=self.login_available,
666 mathjax_url=self.application.ipython_app.mathjax_url,
666 mathjax_url=self.application.ipython_app.mathjax_url,
667 )
667 )
668
668
669
669
670 #-----------------------------------------------------------------------------
670 #-----------------------------------------------------------------------------
671 # Cluster handlers
671 # Cluster handlers
672 #-----------------------------------------------------------------------------
672 #-----------------------------------------------------------------------------
673
673
674
674
675 class MainClusterHandler(AuthenticatedHandler):
675 class MainClusterHandler(AuthenticatedHandler):
676
676
677 @web.authenticated
677 @web.authenticated
678 def get(self):
678 def get(self):
679 cm = self.application.cluster_manager
679 cm = self.application.cluster_manager
680 self.finish(jsonapi.dumps(cm.list_profiles()))
680 self.finish(jsonapi.dumps(cm.list_profiles()))
681
681
682
682
683 class ClusterProfileHandler(AuthenticatedHandler):
683 class ClusterProfileHandler(AuthenticatedHandler):
684
684
685 @web.authenticated
685 @web.authenticated
686 def get(self, profile):
686 def get(self, profile):
687 cm = self.application.cluster_manager
687 cm = self.application.cluster_manager
688 self.finish(jsonapi.dumps(cm.profile_info(profile)))
688 self.finish(jsonapi.dumps(cm.profile_info(profile)))
689
689
690
690
691 class ClusterActionHandler(AuthenticatedHandler):
691 class ClusterActionHandler(AuthenticatedHandler):
692
692
693 @web.authenticated
693 @web.authenticated
694 def post(self, profile, action):
694 def post(self, profile, action):
695 cm = self.application.cluster_manager
695 cm = self.application.cluster_manager
696 if action == 'start':
696 if action == 'start':
697 n = self.get_argument('n',default=None)
697 n = self.get_argument('n',default=None)
698 if n is None:
698 if n is None:
699 data = cm.start_cluster(profile)
699 data = cm.start_cluster(profile)
700 else:
700 else:
701 data = cm.start_cluster(profile,int(n))
701 data = cm.start_cluster(profile,int(n))
702 if action == 'stop':
702 if action == 'stop':
703 data = cm.stop_cluster(profile)
703 data = cm.stop_cluster(profile)
704 self.finish(jsonapi.dumps(data))
704 self.finish(jsonapi.dumps(data))
705
705
706
706
707 #-----------------------------------------------------------------------------
707 #-----------------------------------------------------------------------------
708 # RST web service handlers
708 # RST web service handlers
709 #-----------------------------------------------------------------------------
709 #-----------------------------------------------------------------------------
710
710
711
711
712 class RSTHandler(AuthenticatedHandler):
712 class RSTHandler(AuthenticatedHandler):
713
713
714 @web.authenticated
714 @web.authenticated
715 def post(self):
715 def post(self):
716 if publish_string is None:
716 if publish_string is None:
717 raise web.HTTPError(503, u'docutils not available')
717 raise web.HTTPError(503, u'docutils not available')
718 body = self.request.body.strip()
718 body = self.request.body.strip()
719 source = body
719 source = body
720 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
720 # template_path=os.path.join(os.path.dirname(__file__), u'templates', u'rst_template.html')
721 defaults = {'file_insertion_enabled': 0,
721 defaults = {'file_insertion_enabled': 0,
722 'raw_enabled': 0,
722 'raw_enabled': 0,
723 '_disable_config': 1,
723 '_disable_config': 1,
724 'stylesheet_path': 0
724 'stylesheet_path': 0
725 # 'template': template_path
725 # 'template': template_path
726 }
726 }
727 try:
727 try:
728 html = publish_string(source, writer_name='html',
728 html = publish_string(source, writer_name='html',
729 settings_overrides=defaults
729 settings_overrides=defaults
730 )
730 )
731 except:
731 except:
732 raise web.HTTPError(400, u'Invalid RST')
732 raise web.HTTPError(400, u'Invalid RST')
733 print html
733 print html
734 self.set_header('Content-Type', 'text/html')
734 self.set_header('Content-Type', 'text/html')
735 self.finish(html)
735 self.finish(html)
736
736
737
737
@@ -1,324 +1,344 b''
1 """A kernel manager for multiple kernels.
1 """A kernel manager for multiple kernels.
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 os
19 import os
20 import signal
20 import signal
21 import sys
21 import sys
22 import uuid
22 import uuid
23
23
24 import zmq
24 import zmq
25 from zmq.eventloop.zmqstream import ZMQStream
25 from zmq.eventloop.zmqstream import ZMQStream
26
26
27 from tornado import web
27 from tornado import web
28
28
29 from IPython.config.configurable import LoggingConfigurable
29 from IPython.config.configurable import LoggingConfigurable
30 from IPython.utils.importstring import import_item
30 from IPython.utils.importstring import import_item
31 from IPython.utils.traitlets import (
31 from IPython.utils.traitlets import (
32 Instance, Dict, List, Unicode, Float, Integer, Any, DottedObjectName,
32 Instance, Dict, List, Unicode, Float, Integer, Any, DottedObjectName,
33 )
33 )
34 #-----------------------------------------------------------------------------
34 #-----------------------------------------------------------------------------
35 # Classes
35 # Classes
36 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
37
37
38 class DuplicateKernelError(Exception):
38 class DuplicateKernelError(Exception):
39 pass
39 pass
40
40
41
41
42 class MultiKernelManager(LoggingConfigurable):
42 class MultiKernelManager(LoggingConfigurable):
43 """A class for managing multiple kernels."""
43 """A class for managing multiple kernels."""
44
44
45 kernel_manager_class = DottedObjectName(
45 kernel_manager_class = DottedObjectName(
46 "IPython.zmq.kernelmanager.KernelManager", config=True,
46 "IPython.zmq.blockingkernelmanager.BlockingKernelManager", config=True,
47 help="""The kernel manager class. This is configurable to allow
47 help="""The kernel manager class. This is configurable to allow
48 subclassing of the KernelManager for customized behavior.
48 subclassing of the KernelManager for customized behavior.
49 """
49 """
50 )
50 )
51 def _kernel_manager_class_changed(self, name, old, new):
51 def _kernel_manager_class_changed(self, name, old, new):
52 self.kernel_manager_factory = import_item(new)
52 self.kernel_manager_factory = import_item(new)
53
53
54 kernel_manager_factory = Any(help="this is kernel_manager_class after import")
54 kernel_manager_factory = Any(help="this is kernel_manager_class after import")
55 def _kernel_manager_factory_default(self):
55 def _kernel_manager_factory_default(self):
56 return import_item(self.kernel_manager_class)
56 return import_item(self.kernel_manager_class)
57
57
58 context = Instance('zmq.Context')
58 context = Instance('zmq.Context')
59 def _context_default(self):
59 def _context_default(self):
60 return zmq.Context.instance()
60 return zmq.Context.instance()
61
61
62 connection_dir = Unicode('')
62 connection_dir = Unicode('')
63
63
64 _kernels = Dict()
64 _kernels = Dict()
65
65
66 @property
66 @property
67 def kernel_ids(self):
67 def kernel_ids(self):
68 """Return a list of the kernel ids of the active kernels."""
68 """Return a list of the kernel ids of the active kernels."""
69 return self._kernels.keys()
69 return self._kernels.keys()
70
70
71 def __len__(self):
71 def __len__(self):
72 """Return the number of running kernels."""
72 """Return the number of running kernels."""
73 return len(self.kernel_ids)
73 return len(self.kernel_ids)
74
74
75 def __contains__(self, kernel_id):
75 def __contains__(self, kernel_id):
76 if kernel_id in self.kernel_ids:
76 if kernel_id in self.kernel_ids:
77 return True
77 return True
78 else:
78 else:
79 return False
79 return False
80
80
81 def start_kernel(self, **kwargs):
81 def start_kernel(self, **kwargs):
82 """Start a new kernel."""
82 """Start a new kernel."""
83 kernel_id = unicode(uuid.uuid4())
83 kernel_id = unicode(uuid.uuid4())
84 # use base KernelManager for each Kernel
84 # use base KernelManager for each Kernel
85 km = self.kernel_manager_factory(connection_file=os.path.join(
85 km = self.kernel_manager_factory(connection_file=os.path.join(
86 self.connection_dir, "kernel-%s.json" % kernel_id),
86 self.connection_dir, "kernel-%s.json" % kernel_id),
87 config=self.config,
87 config=self.config,
88 )
88 )
89 km.start_kernel(**kwargs)
89 km.start_kernel(**kwargs)
90 # start just the shell channel, needed for graceful restart
91 km.start_channels(shell=True, sub=False, stdin=False, hb=False)
90 self._kernels[kernel_id] = km
92 self._kernels[kernel_id] = km
91 return kernel_id
93 return kernel_id
92
94
95 def shutdown_kernel(self, kernel_id):
96 """Shutdown a kernel by its kernel uuid.
97
98 Parameters
99 ==========
100 kernel_id : uuid
101 The id of the kernel to shutdown.
102 """
103 self.get_kernel(kernel_id).shutdown_kernel()
104 del self._kernels[kernel_id]
105
93 def kill_kernel(self, kernel_id):
106 def kill_kernel(self, kernel_id):
94 """Kill a kernel by its kernel uuid.
107 """Kill a kernel by its kernel uuid.
95
108
96 Parameters
109 Parameters
97 ==========
110 ==========
98 kernel_id : uuid
111 kernel_id : uuid
99 The id of the kernel to kill.
112 The id of the kernel to kill.
100 """
113 """
101 self.get_kernel(kernel_id).kill_kernel()
114 self.get_kernel(kernel_id).kill_kernel()
102 del self._kernels[kernel_id]
115 del self._kernels[kernel_id]
103
116
104 def interrupt_kernel(self, kernel_id):
117 def interrupt_kernel(self, kernel_id):
105 """Interrupt (SIGINT) the kernel by its uuid.
118 """Interrupt (SIGINT) the kernel by its uuid.
106
119
107 Parameters
120 Parameters
108 ==========
121 ==========
109 kernel_id : uuid
122 kernel_id : uuid
110 The id of the kernel to interrupt.
123 The id of the kernel to interrupt.
111 """
124 """
112 return self.get_kernel(kernel_id).interrupt_kernel()
125 return self.get_kernel(kernel_id).interrupt_kernel()
113
126
114 def signal_kernel(self, kernel_id, signum):
127 def signal_kernel(self, kernel_id, signum):
115 """ Sends a signal to the kernel by its uuid.
128 """ Sends a signal to the kernel by its uuid.
116
129
117 Note that since only SIGTERM is supported on Windows, this function
130 Note that since only SIGTERM is supported on Windows, this function
118 is only useful on Unix systems.
131 is only useful on Unix systems.
119
132
120 Parameters
133 Parameters
121 ==========
134 ==========
122 kernel_id : uuid
135 kernel_id : uuid
123 The id of the kernel to signal.
136 The id of the kernel to signal.
124 """
137 """
125 return self.get_kernel(kernel_id).signal_kernel(signum)
138 return self.get_kernel(kernel_id).signal_kernel(signum)
126
139
127 def get_kernel(self, kernel_id):
140 def get_kernel(self, kernel_id):
128 """Get the single KernelManager object for a kernel by its uuid.
141 """Get the single KernelManager object for a kernel by its uuid.
129
142
130 Parameters
143 Parameters
131 ==========
144 ==========
132 kernel_id : uuid
145 kernel_id : uuid
133 The id of the kernel.
146 The id of the kernel.
134 """
147 """
135 km = self._kernels.get(kernel_id)
148 km = self._kernels.get(kernel_id)
136 if km is not None:
149 if km is not None:
137 return km
150 return km
138 else:
151 else:
139 raise KeyError("Kernel with id not found: %s" % kernel_id)
152 raise KeyError("Kernel with id not found: %s" % kernel_id)
140
153
141 def get_kernel_ports(self, kernel_id):
154 def get_kernel_ports(self, kernel_id):
142 """Return a dictionary of ports for a kernel.
155 """Return a dictionary of ports for a kernel.
143
156
144 Parameters
157 Parameters
145 ==========
158 ==========
146 kernel_id : uuid
159 kernel_id : uuid
147 The id of the kernel.
160 The id of the kernel.
148
161
149 Returns
162 Returns
150 =======
163 =======
151 port_dict : dict
164 port_dict : dict
152 A dict of key, value pairs where the keys are the names
165 A dict of key, value pairs where the keys are the names
153 (stdin_port,iopub_port,shell_port) and the values are the
166 (stdin_port,iopub_port,shell_port) and the values are the
154 integer port numbers for those channels.
167 integer port numbers for those channels.
155 """
168 """
156 # this will raise a KeyError if not found:
169 # this will raise a KeyError if not found:
157 km = self.get_kernel(kernel_id)
170 km = self.get_kernel(kernel_id)
158 return dict(shell_port=km.shell_port,
171 return dict(shell_port=km.shell_port,
159 iopub_port=km.iopub_port,
172 iopub_port=km.iopub_port,
160 stdin_port=km.stdin_port,
173 stdin_port=km.stdin_port,
161 hb_port=km.hb_port,
174 hb_port=km.hb_port,
162 )
175 )
163
176
164 def get_kernel_ip(self, kernel_id):
177 def get_kernel_ip(self, kernel_id):
165 """Return ip address for a kernel.
178 """Return ip address for a kernel.
166
179
167 Parameters
180 Parameters
168 ==========
181 ==========
169 kernel_id : uuid
182 kernel_id : uuid
170 The id of the kernel.
183 The id of the kernel.
171
184
172 Returns
185 Returns
173 =======
186 =======
174 ip : str
187 ip : str
175 The ip address of the kernel.
188 The ip address of the kernel.
176 """
189 """
177 return self.get_kernel(kernel_id).ip
190 return self.get_kernel(kernel_id).ip
178
191
179 def create_connected_stream(self, ip, port, socket_type):
192 def create_connected_stream(self, ip, port, socket_type):
180 sock = self.context.socket(socket_type)
193 sock = self.context.socket(socket_type)
181 addr = "tcp://%s:%i" % (ip, port)
194 addr = "tcp://%s:%i" % (ip, port)
182 self.log.info("Connecting to: %s" % addr)
195 self.log.info("Connecting to: %s" % addr)
183 sock.connect(addr)
196 sock.connect(addr)
184 return ZMQStream(sock)
197 return ZMQStream(sock)
185
198
186 def create_iopub_stream(self, kernel_id):
199 def create_iopub_stream(self, kernel_id):
187 ip = self.get_kernel_ip(kernel_id)
200 ip = self.get_kernel_ip(kernel_id)
188 ports = self.get_kernel_ports(kernel_id)
201 ports = self.get_kernel_ports(kernel_id)
189 iopub_stream = self.create_connected_stream(ip, ports['iopub_port'], zmq.SUB)
202 iopub_stream = self.create_connected_stream(ip, ports['iopub_port'], zmq.SUB)
190 iopub_stream.socket.setsockopt(zmq.SUBSCRIBE, b'')
203 iopub_stream.socket.setsockopt(zmq.SUBSCRIBE, b'')
191 return iopub_stream
204 return iopub_stream
192
205
193 def create_shell_stream(self, kernel_id):
206 def create_shell_stream(self, kernel_id):
194 ip = self.get_kernel_ip(kernel_id)
207 ip = self.get_kernel_ip(kernel_id)
195 ports = self.get_kernel_ports(kernel_id)
208 ports = self.get_kernel_ports(kernel_id)
196 shell_stream = self.create_connected_stream(ip, ports['shell_port'], zmq.DEALER)
209 shell_stream = self.create_connected_stream(ip, ports['shell_port'], zmq.DEALER)
197 return shell_stream
210 return shell_stream
198
211
199 def create_hb_stream(self, kernel_id):
212 def create_hb_stream(self, kernel_id):
200 ip = self.get_kernel_ip(kernel_id)
213 ip = self.get_kernel_ip(kernel_id)
201 ports = self.get_kernel_ports(kernel_id)
214 ports = self.get_kernel_ports(kernel_id)
202 hb_stream = self.create_connected_stream(ip, ports['hb_port'], zmq.REQ)
215 hb_stream = self.create_connected_stream(ip, ports['hb_port'], zmq.REQ)
203 return hb_stream
216 return hb_stream
204
217
205
218
206 class MappingKernelManager(MultiKernelManager):
219 class MappingKernelManager(MultiKernelManager):
207 """A KernelManager that handles notebok mapping and HTTP error handling"""
220 """A KernelManager that handles notebok mapping and HTTP error handling"""
208
221
209 kernel_argv = List(Unicode)
222 kernel_argv = List(Unicode)
210
223
211 time_to_dead = Float(3.0, config=True, help="""Kernel heartbeat interval in seconds.""")
224 time_to_dead = Float(3.0, config=True, help="""Kernel heartbeat interval in seconds.""")
212 first_beat = Float(5.0, config=True, help="Delay (in seconds) before sending first heartbeat.")
225 first_beat = Float(5.0, config=True, help="Delay (in seconds) before sending first heartbeat.")
213
226
214 max_msg_size = Integer(65536, config=True, help="""
227 max_msg_size = Integer(65536, config=True, help="""
215 The max raw message size accepted from the browser
228 The max raw message size accepted from the browser
216 over a WebSocket connection.
229 over a WebSocket connection.
217 """)
230 """)
218
231
219 _notebook_mapping = Dict()
232 _notebook_mapping = Dict()
220
233
221 #-------------------------------------------------------------------------
234 #-------------------------------------------------------------------------
222 # Methods for managing kernels and sessions
235 # Methods for managing kernels and sessions
223 #-------------------------------------------------------------------------
236 #-------------------------------------------------------------------------
224
237
225 def kernel_for_notebook(self, notebook_id):
238 def kernel_for_notebook(self, notebook_id):
226 """Return the kernel_id for a notebook_id or None."""
239 """Return the kernel_id for a notebook_id or None."""
227 return self._notebook_mapping.get(notebook_id)
240 return self._notebook_mapping.get(notebook_id)
228
241
229 def set_kernel_for_notebook(self, notebook_id, kernel_id):
242 def set_kernel_for_notebook(self, notebook_id, kernel_id):
230 """Associate a notebook with a kernel."""
243 """Associate a notebook with a kernel."""
231 if notebook_id is not None:
244 if notebook_id is not None:
232 self._notebook_mapping[notebook_id] = kernel_id
245 self._notebook_mapping[notebook_id] = kernel_id
233
246
234 def notebook_for_kernel(self, kernel_id):
247 def notebook_for_kernel(self, kernel_id):
235 """Return the notebook_id for a kernel_id or None."""
248 """Return the notebook_id for a kernel_id or None."""
236 notebook_ids = [k for k, v in self._notebook_mapping.iteritems() if v == kernel_id]
249 notebook_ids = [k for k, v in self._notebook_mapping.iteritems() if v == kernel_id]
237 if len(notebook_ids) == 1:
250 if len(notebook_ids) == 1:
238 return notebook_ids[0]
251 return notebook_ids[0]
239 else:
252 else:
240 return None
253 return None
241
254
242 def delete_mapping_for_kernel(self, kernel_id):
255 def delete_mapping_for_kernel(self, kernel_id):
243 """Remove the kernel/notebook mapping for kernel_id."""
256 """Remove the kernel/notebook mapping for kernel_id."""
244 notebook_id = self.notebook_for_kernel(kernel_id)
257 notebook_id = self.notebook_for_kernel(kernel_id)
245 if notebook_id is not None:
258 if notebook_id is not None:
246 del self._notebook_mapping[notebook_id]
259 del self._notebook_mapping[notebook_id]
247
260
248 def start_kernel(self, notebook_id=None):
261 def start_kernel(self, notebook_id=None):
249 """Start a kernel for a notebok an return its kernel_id.
262 """Start a kernel for a notebok an return its kernel_id.
250
263
251 Parameters
264 Parameters
252 ----------
265 ----------
253 notebook_id : uuid
266 notebook_id : uuid
254 The uuid of the notebook to associate the new kernel with. If this
267 The uuid of the notebook to associate the new kernel with. If this
255 is not None, this kernel will be persistent whenever the notebook
268 is not None, this kernel will be persistent whenever the notebook
256 requests a kernel.
269 requests a kernel.
257 """
270 """
258 kernel_id = self.kernel_for_notebook(notebook_id)
271 kernel_id = self.kernel_for_notebook(notebook_id)
259 if kernel_id is None:
272 if kernel_id is None:
260 kwargs = dict()
273 kwargs = dict()
261 kwargs['extra_arguments'] = self.kernel_argv
274 kwargs['extra_arguments'] = self.kernel_argv
262 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
275 kernel_id = super(MappingKernelManager, self).start_kernel(**kwargs)
263 self.set_kernel_for_notebook(notebook_id, kernel_id)
276 self.set_kernel_for_notebook(notebook_id, kernel_id)
264 self.log.info("Kernel started: %s" % kernel_id)
277 self.log.info("Kernel started: %s" % kernel_id)
265 self.log.debug("Kernel args: %r" % kwargs)
278 self.log.debug("Kernel args: %r" % kwargs)
266 else:
279 else:
267 self.log.info("Using existing kernel: %s" % kernel_id)
280 self.log.info("Using existing kernel: %s" % kernel_id)
268 return kernel_id
281 return kernel_id
269
282
283 def shutdown_kernel(self, kernel_id):
284 """Shutdown a kernel and remove its notebook association."""
285 self._check_kernel_id(kernel_id)
286 super(MappingKernelManager, self).shutdown_kernel(kernel_id)
287 self.delete_mapping_for_kernel(kernel_id)
288 self.log.info("Kernel shutdown: %s" % kernel_id)
289
270 def kill_kernel(self, kernel_id):
290 def kill_kernel(self, kernel_id):
271 """Kill a kernel and remove its notebook association."""
291 """Kill a kernel and remove its notebook association."""
272 self._check_kernel_id(kernel_id)
292 self._check_kernel_id(kernel_id)
273 super(MappingKernelManager, self).kill_kernel(kernel_id)
293 super(MappingKernelManager, self).kill_kernel(kernel_id)
274 self.delete_mapping_for_kernel(kernel_id)
294 self.delete_mapping_for_kernel(kernel_id)
275 self.log.info("Kernel killed: %s" % kernel_id)
295 self.log.info("Kernel killed: %s" % kernel_id)
276
296
277 def interrupt_kernel(self, kernel_id):
297 def interrupt_kernel(self, kernel_id):
278 """Interrupt a kernel."""
298 """Interrupt a kernel."""
279 self._check_kernel_id(kernel_id)
299 self._check_kernel_id(kernel_id)
280 super(MappingKernelManager, self).interrupt_kernel(kernel_id)
300 super(MappingKernelManager, self).interrupt_kernel(kernel_id)
281 self.log.info("Kernel interrupted: %s" % kernel_id)
301 self.log.info("Kernel interrupted: %s" % kernel_id)
282
302
283 def restart_kernel(self, kernel_id):
303 def restart_kernel(self, kernel_id):
284 """Restart a kernel while keeping clients connected."""
304 """Restart a kernel while keeping clients connected."""
285 self._check_kernel_id(kernel_id)
305 self._check_kernel_id(kernel_id)
286 km = self.get_kernel(kernel_id)
306 km = self.get_kernel(kernel_id)
287 km.restart_kernel(now=True)
307 km.restart_kernel()
288 self.log.info("Kernel restarted: %s" % kernel_id)
308 self.log.info("Kernel restarted: %s" % kernel_id)
289 return kernel_id
309 return kernel_id
290
310
291 # the following remains, in case the KM restart machinery is
311 # the following remains, in case the KM restart machinery is
292 # somehow unacceptable
312 # somehow unacceptable
293 # Get the notebook_id to preserve the kernel/notebook association.
313 # Get the notebook_id to preserve the kernel/notebook association.
294 notebook_id = self.notebook_for_kernel(kernel_id)
314 notebook_id = self.notebook_for_kernel(kernel_id)
295 # Create the new kernel first so we can move the clients over.
315 # Create the new kernel first so we can move the clients over.
296 new_kernel_id = self.start_kernel()
316 new_kernel_id = self.start_kernel()
297 # Now kill the old kernel.
317 # Now kill the old kernel.
298 self.kill_kernel(kernel_id)
318 self.kill_kernel(kernel_id)
299 # Now save the new kernel/notebook association. We have to save it
319 # Now save the new kernel/notebook association. We have to save it
300 # after the old kernel is killed as that will delete the mapping.
320 # after the old kernel is killed as that will delete the mapping.
301 self.set_kernel_for_notebook(notebook_id, new_kernel_id)
321 self.set_kernel_for_notebook(notebook_id, new_kernel_id)
302 self.log.info("Kernel restarted: %s" % new_kernel_id)
322 self.log.info("Kernel restarted: %s" % new_kernel_id)
303 return new_kernel_id
323 return new_kernel_id
304
324
305 def create_iopub_stream(self, kernel_id):
325 def create_iopub_stream(self, kernel_id):
306 """Create a new iopub stream."""
326 """Create a new iopub stream."""
307 self._check_kernel_id(kernel_id)
327 self._check_kernel_id(kernel_id)
308 return super(MappingKernelManager, self).create_iopub_stream(kernel_id)
328 return super(MappingKernelManager, self).create_iopub_stream(kernel_id)
309
329
310 def create_shell_stream(self, kernel_id):
330 def create_shell_stream(self, kernel_id):
311 """Create a new shell stream."""
331 """Create a new shell stream."""
312 self._check_kernel_id(kernel_id)
332 self._check_kernel_id(kernel_id)
313 return super(MappingKernelManager, self).create_shell_stream(kernel_id)
333 return super(MappingKernelManager, self).create_shell_stream(kernel_id)
314
334
315 def create_hb_stream(self, kernel_id):
335 def create_hb_stream(self, kernel_id):
316 """Create a new hb stream."""
336 """Create a new hb stream."""
317 self._check_kernel_id(kernel_id)
337 self._check_kernel_id(kernel_id)
318 return super(MappingKernelManager, self).create_hb_stream(kernel_id)
338 return super(MappingKernelManager, self).create_hb_stream(kernel_id)
319
339
320 def _check_kernel_id(self, kernel_id):
340 def _check_kernel_id(self, kernel_id):
321 """Check a that a kernel_id exists and raise 404 if not."""
341 """Check a that a kernel_id exists and raise 404 if not."""
322 if kernel_id not in self:
342 if kernel_id not in self:
323 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
343 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
324
344
@@ -1,584 +1,584 b''
1 # coding: utf-8
1 # coding: utf-8
2 """A tornado based IPython notebook server.
2 """A tornado based IPython notebook server.
3
3
4 Authors:
4 Authors:
5
5
6 * Brian Granger
6 * Brian Granger
7 """
7 """
8 #-----------------------------------------------------------------------------
8 #-----------------------------------------------------------------------------
9 # Copyright (C) 2008-2011 The IPython Development Team
9 # Copyright (C) 2008-2011 The IPython Development Team
10 #
10 #
11 # Distributed under the terms of the BSD License. The full license is in
11 # Distributed under the terms of the BSD License. The full license is in
12 # the file COPYING, distributed as part of this software.
12 # the file COPYING, distributed as part of this software.
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 # Imports
16 # Imports
17 #-----------------------------------------------------------------------------
17 #-----------------------------------------------------------------------------
18
18
19 # stdlib
19 # stdlib
20 import errno
20 import errno
21 import logging
21 import logging
22 import os
22 import os
23 import random
23 import random
24 import re
24 import re
25 import select
25 import select
26 import signal
26 import signal
27 import socket
27 import socket
28 import sys
28 import sys
29 import threading
29 import threading
30 import time
30 import time
31 import webbrowser
31 import webbrowser
32
32
33 # Third party
33 # Third party
34 import zmq
34 import zmq
35
35
36 # Install the pyzmq ioloop. This has to be done before anything else from
36 # Install the pyzmq ioloop. This has to be done before anything else from
37 # tornado is imported.
37 # tornado is imported.
38 from zmq.eventloop import ioloop
38 from zmq.eventloop import ioloop
39 ioloop.install()
39 ioloop.install()
40
40
41 from tornado import httpserver
41 from tornado import httpserver
42 from tornado import web
42 from tornado import web
43
43
44 # Our own libraries
44 # Our own libraries
45 from .kernelmanager import MappingKernelManager
45 from .kernelmanager import MappingKernelManager
46 from .handlers import (LoginHandler, LogoutHandler,
46 from .handlers import (LoginHandler, LogoutHandler,
47 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
47 ProjectDashboardHandler, NewHandler, NamedNotebookHandler,
48 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
48 MainKernelHandler, KernelHandler, KernelActionHandler, IOPubHandler,
49 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
49 ShellHandler, NotebookRootHandler, NotebookHandler, NotebookCopyHandler,
50 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
50 RSTHandler, AuthenticatedFileHandler, PrintNotebookHandler,
51 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
51 MainClusterHandler, ClusterProfileHandler, ClusterActionHandler
52 )
52 )
53 from .notebookmanager import NotebookManager
53 from .notebookmanager import NotebookManager
54 from .clustermanager import ClusterManager
54 from .clustermanager import ClusterManager
55
55
56 from IPython.config.application import catch_config_error, boolean_flag
56 from IPython.config.application import catch_config_error, boolean_flag
57 from IPython.core.application import BaseIPythonApplication
57 from IPython.core.application import BaseIPythonApplication
58 from IPython.core.profiledir import ProfileDir
58 from IPython.core.profiledir import ProfileDir
59 from IPython.frontend.consoleapp import IPythonConsoleApp
59 from IPython.frontend.consoleapp import IPythonConsoleApp
60 from IPython.lib.kernel import swallow_argv
60 from IPython.lib.kernel import swallow_argv
61 from IPython.zmq.session import Session, default_secure
61 from IPython.zmq.session import Session, default_secure
62 from IPython.zmq.zmqshell import ZMQInteractiveShell
62 from IPython.zmq.zmqshell import ZMQInteractiveShell
63 from IPython.zmq.ipkernel import (
63 from IPython.zmq.ipkernel import (
64 flags as ipkernel_flags,
64 flags as ipkernel_flags,
65 aliases as ipkernel_aliases,
65 aliases as ipkernel_aliases,
66 IPKernelApp
66 IPKernelApp
67 )
67 )
68 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
68 from IPython.utils.traitlets import Dict, Unicode, Integer, List, Enum, Bool
69 from IPython.utils import py3compat
69 from IPython.utils import py3compat
70
70
71 #-----------------------------------------------------------------------------
71 #-----------------------------------------------------------------------------
72 # Module globals
72 # Module globals
73 #-----------------------------------------------------------------------------
73 #-----------------------------------------------------------------------------
74
74
75 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
75 _kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
76 _kernel_action_regex = r"(?P<action>restart|interrupt)"
76 _kernel_action_regex = r"(?P<action>restart|interrupt)"
77 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
77 _notebook_id_regex = r"(?P<notebook_id>\w+-\w+-\w+-\w+-\w+)"
78 _profile_regex = r"(?P<profile>[a-zA-Z0-9]+)"
78 _profile_regex = r"(?P<profile>[a-zA-Z0-9]+)"
79 _cluster_action_regex = r"(?P<action>start|stop)"
79 _cluster_action_regex = r"(?P<action>start|stop)"
80
80
81
81
82 LOCALHOST = '127.0.0.1'
82 LOCALHOST = '127.0.0.1'
83
83
84 _examples = """
84 _examples = """
85 ipython notebook # start the notebook
85 ipython notebook # start the notebook
86 ipython notebook --profile=sympy # use the sympy profile
86 ipython notebook --profile=sympy # use the sympy profile
87 ipython notebook --pylab=inline # pylab in inline plotting mode
87 ipython notebook --pylab=inline # pylab in inline plotting mode
88 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
88 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
89 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
89 ipython notebook --port=5555 --ip=* # Listen on port 5555, all interfaces
90 """
90 """
91
91
92 #-----------------------------------------------------------------------------
92 #-----------------------------------------------------------------------------
93 # Helper functions
93 # Helper functions
94 #-----------------------------------------------------------------------------
94 #-----------------------------------------------------------------------------
95
95
96 def url_path_join(a,b):
96 def url_path_join(a,b):
97 if a.endswith('/') and b.startswith('/'):
97 if a.endswith('/') and b.startswith('/'):
98 return a[:-1]+b
98 return a[:-1]+b
99 else:
99 else:
100 return a+b
100 return a+b
101
101
102 def random_ports(port, n):
102 def random_ports(port, n):
103 """Generate a list of n random ports near the given port.
103 """Generate a list of n random ports near the given port.
104
104
105 The first 5 ports will be sequential, and the remaining n-5 will be
105 The first 5 ports will be sequential, and the remaining n-5 will be
106 randomly selected in the range [port-2*n, port+2*n].
106 randomly selected in the range [port-2*n, port+2*n].
107 """
107 """
108 for i in range(min(5, n)):
108 for i in range(min(5, n)):
109 yield port + i
109 yield port + i
110 for i in range(n-5):
110 for i in range(n-5):
111 yield port + random.randint(-2*n, 2*n)
111 yield port + random.randint(-2*n, 2*n)
112
112
113 #-----------------------------------------------------------------------------
113 #-----------------------------------------------------------------------------
114 # The Tornado web application
114 # The Tornado web application
115 #-----------------------------------------------------------------------------
115 #-----------------------------------------------------------------------------
116
116
117 class NotebookWebApplication(web.Application):
117 class NotebookWebApplication(web.Application):
118
118
119 def __init__(self, ipython_app, kernel_manager, notebook_manager,
119 def __init__(self, ipython_app, kernel_manager, notebook_manager,
120 cluster_manager, log,
120 cluster_manager, log,
121 base_project_url, settings_overrides):
121 base_project_url, settings_overrides):
122 handlers = [
122 handlers = [
123 (r"/", ProjectDashboardHandler),
123 (r"/", ProjectDashboardHandler),
124 (r"/login", LoginHandler),
124 (r"/login", LoginHandler),
125 (r"/logout", LogoutHandler),
125 (r"/logout", LogoutHandler),
126 (r"/new", NewHandler),
126 (r"/new", NewHandler),
127 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
127 (r"/%s" % _notebook_id_regex, NamedNotebookHandler),
128 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
128 (r"/%s/copy" % _notebook_id_regex, NotebookCopyHandler),
129 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
129 (r"/%s/print" % _notebook_id_regex, PrintNotebookHandler),
130 (r"/kernels", MainKernelHandler),
130 (r"/kernels", MainKernelHandler),
131 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
131 (r"/kernels/%s" % _kernel_id_regex, KernelHandler),
132 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
132 (r"/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
133 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
133 (r"/kernels/%s/iopub" % _kernel_id_regex, IOPubHandler),
134 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
134 (r"/kernels/%s/shell" % _kernel_id_regex, ShellHandler),
135 (r"/notebooks", NotebookRootHandler),
135 (r"/notebooks", NotebookRootHandler),
136 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
136 (r"/notebooks/%s" % _notebook_id_regex, NotebookHandler),
137 (r"/rstservice/render", RSTHandler),
137 (r"/rstservice/render", RSTHandler),
138 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
138 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : notebook_manager.notebook_dir}),
139 (r"/clusters", MainClusterHandler),
139 (r"/clusters", MainClusterHandler),
140 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
140 (r"/clusters/%s/%s" % (_profile_regex, _cluster_action_regex), ClusterActionHandler),
141 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
141 (r"/clusters/%s" % _profile_regex, ClusterProfileHandler),
142 ]
142 ]
143 settings = dict(
143 settings = dict(
144 template_path=os.path.join(os.path.dirname(__file__), "templates"),
144 template_path=os.path.join(os.path.dirname(__file__), "templates"),
145 static_path=os.path.join(os.path.dirname(__file__), "static"),
145 static_path=os.path.join(os.path.dirname(__file__), "static"),
146 cookie_secret=os.urandom(1024),
146 cookie_secret=os.urandom(1024),
147 login_url="/login",
147 login_url="/login",
148 )
148 )
149
149
150 # allow custom overrides for the tornado web app.
150 # allow custom overrides for the tornado web app.
151 settings.update(settings_overrides)
151 settings.update(settings_overrides)
152
152
153 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
153 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
154 # base_project_url will always be unicode, which will in turn
154 # base_project_url will always be unicode, which will in turn
155 # make the patterns unicode, and ultimately result in unicode
155 # make the patterns unicode, and ultimately result in unicode
156 # keys in kwargs to handler._execute(**kwargs) in tornado.
156 # keys in kwargs to handler._execute(**kwargs) in tornado.
157 # This enforces that base_project_url be ascii in that situation.
157 # This enforces that base_project_url be ascii in that situation.
158 #
158 #
159 # Note that the URLs these patterns check against are escaped,
159 # Note that the URLs these patterns check against are escaped,
160 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
160 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
161 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
161 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
162
162
163 # prepend base_project_url onto the patterns that we match
163 # prepend base_project_url onto the patterns that we match
164 new_handlers = []
164 new_handlers = []
165 for handler in handlers:
165 for handler in handlers:
166 pattern = url_path_join(base_project_url, handler[0])
166 pattern = url_path_join(base_project_url, handler[0])
167 new_handler = tuple([pattern]+list(handler[1:]))
167 new_handler = tuple([pattern]+list(handler[1:]))
168 new_handlers.append( new_handler )
168 new_handlers.append( new_handler )
169
169
170 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
170 super(NotebookWebApplication, self).__init__(new_handlers, **settings)
171
171
172 self.kernel_manager = kernel_manager
172 self.kernel_manager = kernel_manager
173 self.notebook_manager = notebook_manager
173 self.notebook_manager = notebook_manager
174 self.cluster_manager = cluster_manager
174 self.cluster_manager = cluster_manager
175 self.ipython_app = ipython_app
175 self.ipython_app = ipython_app
176 self.read_only = self.ipython_app.read_only
176 self.read_only = self.ipython_app.read_only
177 self.log = log
177 self.log = log
178
178
179
179
180 #-----------------------------------------------------------------------------
180 #-----------------------------------------------------------------------------
181 # Aliases and Flags
181 # Aliases and Flags
182 #-----------------------------------------------------------------------------
182 #-----------------------------------------------------------------------------
183
183
184 flags = dict(ipkernel_flags)
184 flags = dict(ipkernel_flags)
185 flags['no-browser']=(
185 flags['no-browser']=(
186 {'NotebookApp' : {'open_browser' : False}},
186 {'NotebookApp' : {'open_browser' : False}},
187 "Don't open the notebook in a browser after startup."
187 "Don't open the notebook in a browser after startup."
188 )
188 )
189 flags['no-mathjax']=(
189 flags['no-mathjax']=(
190 {'NotebookApp' : {'enable_mathjax' : False}},
190 {'NotebookApp' : {'enable_mathjax' : False}},
191 """Disable MathJax
191 """Disable MathJax
192
192
193 MathJax is the javascript library IPython uses to render math/LaTeX. It is
193 MathJax is the javascript library IPython uses to render math/LaTeX. It is
194 very large, so you may want to disable it if you have a slow internet
194 very large, so you may want to disable it if you have a slow internet
195 connection, or for offline use of the notebook.
195 connection, or for offline use of the notebook.
196
196
197 When disabled, equations etc. will appear as their untransformed TeX source.
197 When disabled, equations etc. will appear as their untransformed TeX source.
198 """
198 """
199 )
199 )
200 flags['read-only'] = (
200 flags['read-only'] = (
201 {'NotebookApp' : {'read_only' : True}},
201 {'NotebookApp' : {'read_only' : True}},
202 """Allow read-only access to notebooks.
202 """Allow read-only access to notebooks.
203
203
204 When using a password to protect the notebook server, this flag
204 When using a password to protect the notebook server, this flag
205 allows unauthenticated clients to view the notebook list, and
205 allows unauthenticated clients to view the notebook list, and
206 individual notebooks, but not edit them, start kernels, or run
206 individual notebooks, but not edit them, start kernels, or run
207 code.
207 code.
208
208
209 If no password is set, the server will be entirely read-only.
209 If no password is set, the server will be entirely read-only.
210 """
210 """
211 )
211 )
212
212
213 # Add notebook manager flags
213 # Add notebook manager flags
214 flags.update(boolean_flag('script', 'NotebookManager.save_script',
214 flags.update(boolean_flag('script', 'NotebookManager.save_script',
215 'Auto-save a .py script everytime the .ipynb notebook is saved',
215 'Auto-save a .py script everytime the .ipynb notebook is saved',
216 'Do not auto-save .py scripts for every notebook'))
216 'Do not auto-save .py scripts for every notebook'))
217
217
218 # the flags that are specific to the frontend
218 # the flags that are specific to the frontend
219 # these must be scrubbed before being passed to the kernel,
219 # these must be scrubbed before being passed to the kernel,
220 # or it will raise an error on unrecognized flags
220 # or it will raise an error on unrecognized flags
221 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
221 notebook_flags = ['no-browser', 'no-mathjax', 'read-only', 'script', 'no-script']
222
222
223 aliases = dict(ipkernel_aliases)
223 aliases = dict(ipkernel_aliases)
224
224
225 aliases.update({
225 aliases.update({
226 'ip': 'NotebookApp.ip',
226 'ip': 'NotebookApp.ip',
227 'port': 'NotebookApp.port',
227 'port': 'NotebookApp.port',
228 'port-retries': 'NotebookApp.port_retries',
228 'port-retries': 'NotebookApp.port_retries',
229 'keyfile': 'NotebookApp.keyfile',
229 'keyfile': 'NotebookApp.keyfile',
230 'certfile': 'NotebookApp.certfile',
230 'certfile': 'NotebookApp.certfile',
231 'notebook-dir': 'NotebookManager.notebook_dir',
231 'notebook-dir': 'NotebookManager.notebook_dir',
232 'browser': 'NotebookApp.browser',
232 'browser': 'NotebookApp.browser',
233 })
233 })
234
234
235 # remove ipkernel flags that are singletons, and don't make sense in
235 # remove ipkernel flags that are singletons, and don't make sense in
236 # multi-kernel evironment:
236 # multi-kernel evironment:
237 aliases.pop('f', None)
237 aliases.pop('f', None)
238
238
239 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
239 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
240 u'notebook-dir']
240 u'notebook-dir']
241
241
242 #-----------------------------------------------------------------------------
242 #-----------------------------------------------------------------------------
243 # NotebookApp
243 # NotebookApp
244 #-----------------------------------------------------------------------------
244 #-----------------------------------------------------------------------------
245
245
246 class NotebookApp(BaseIPythonApplication):
246 class NotebookApp(BaseIPythonApplication):
247
247
248 name = 'ipython-notebook'
248 name = 'ipython-notebook'
249 default_config_file_name='ipython_notebook_config.py'
249 default_config_file_name='ipython_notebook_config.py'
250
250
251 description = """
251 description = """
252 The IPython HTML Notebook.
252 The IPython HTML Notebook.
253
253
254 This launches a Tornado based HTML Notebook Server that serves up an
254 This launches a Tornado based HTML Notebook Server that serves up an
255 HTML5/Javascript Notebook client.
255 HTML5/Javascript Notebook client.
256 """
256 """
257 examples = _examples
257 examples = _examples
258
258
259 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager]
259 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager]
260 flags = Dict(flags)
260 flags = Dict(flags)
261 aliases = Dict(aliases)
261 aliases = Dict(aliases)
262
262
263 kernel_argv = List(Unicode)
263 kernel_argv = List(Unicode)
264
264
265 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
265 log_level = Enum((0,10,20,30,40,50,'DEBUG','INFO','WARN','ERROR','CRITICAL'),
266 default_value=logging.INFO,
266 default_value=logging.INFO,
267 config=True,
267 config=True,
268 help="Set the log level by value or name.")
268 help="Set the log level by value or name.")
269
269
270 # create requested profiles by default, if they don't exist:
270 # create requested profiles by default, if they don't exist:
271 auto_create = Bool(True)
271 auto_create = Bool(True)
272
272
273 # file to be opened in the notebook server
273 # file to be opened in the notebook server
274 file_to_run = Unicode('')
274 file_to_run = Unicode('')
275
275
276 # Network related information.
276 # Network related information.
277
277
278 ip = Unicode(LOCALHOST, config=True,
278 ip = Unicode(LOCALHOST, config=True,
279 help="The IP address the notebook server will listen on."
279 help="The IP address the notebook server will listen on."
280 )
280 )
281
281
282 def _ip_changed(self, name, old, new):
282 def _ip_changed(self, name, old, new):
283 if new == u'*': self.ip = u''
283 if new == u'*': self.ip = u''
284
284
285 port = Integer(8888, config=True,
285 port = Integer(8888, config=True,
286 help="The port the notebook server will listen on."
286 help="The port the notebook server will listen on."
287 )
287 )
288 port_retries = Integer(50, config=True,
288 port_retries = Integer(50, config=True,
289 help="The number of additional ports to try if the specified port is not available."
289 help="The number of additional ports to try if the specified port is not available."
290 )
290 )
291
291
292 certfile = Unicode(u'', config=True,
292 certfile = Unicode(u'', config=True,
293 help="""The full path to an SSL/TLS certificate file."""
293 help="""The full path to an SSL/TLS certificate file."""
294 )
294 )
295
295
296 keyfile = Unicode(u'', config=True,
296 keyfile = Unicode(u'', config=True,
297 help="""The full path to a private key file for usage with SSL/TLS."""
297 help="""The full path to a private key file for usage with SSL/TLS."""
298 )
298 )
299
299
300 password = Unicode(u'', config=True,
300 password = Unicode(u'', config=True,
301 help="""Hashed password to use for web authentication.
301 help="""Hashed password to use for web authentication.
302
302
303 To generate, type in a python/IPython shell:
303 To generate, type in a python/IPython shell:
304
304
305 from IPython.lib import passwd; passwd()
305 from IPython.lib import passwd; passwd()
306
306
307 The string should be of the form type:salt:hashed-password.
307 The string should be of the form type:salt:hashed-password.
308 """
308 """
309 )
309 )
310
310
311 open_browser = Bool(True, config=True,
311 open_browser = Bool(True, config=True,
312 help="""Whether to open in a browser after starting.
312 help="""Whether to open in a browser after starting.
313 The specific browser used is platform dependent and
313 The specific browser used is platform dependent and
314 determined by the python standard library `webbrowser`
314 determined by the python standard library `webbrowser`
315 module, unless it is overridden using the --browser
315 module, unless it is overridden using the --browser
316 (NotebookApp.browser) configuration option.
316 (NotebookApp.browser) configuration option.
317 """)
317 """)
318
318
319 browser = Unicode(u'', config=True,
319 browser = Unicode(u'', config=True,
320 help="""Specify what command to use to invoke a web
320 help="""Specify what command to use to invoke a web
321 browser when opening the notebook. If not specified, the
321 browser when opening the notebook. If not specified, the
322 default browser will be determined by the `webbrowser`
322 default browser will be determined by the `webbrowser`
323 standard library module, which allows setting of the
323 standard library module, which allows setting of the
324 BROWSER environment variable to override it.
324 BROWSER environment variable to override it.
325 """)
325 """)
326
326
327 read_only = Bool(False, config=True,
327 read_only = Bool(False, config=True,
328 help="Whether to prevent editing/execution of notebooks."
328 help="Whether to prevent editing/execution of notebooks."
329 )
329 )
330
330
331 webapp_settings = Dict(config=True,
331 webapp_settings = Dict(config=True,
332 help="Supply overrides for the tornado.web.Application that the "
332 help="Supply overrides for the tornado.web.Application that the "
333 "IPython notebook uses.")
333 "IPython notebook uses.")
334
334
335 enable_mathjax = Bool(True, config=True,
335 enable_mathjax = Bool(True, config=True,
336 help="""Whether to enable MathJax for typesetting math/TeX
336 help="""Whether to enable MathJax for typesetting math/TeX
337
337
338 MathJax is the javascript library IPython uses to render math/LaTeX. It is
338 MathJax is the javascript library IPython uses to render math/LaTeX. It is
339 very large, so you may want to disable it if you have a slow internet
339 very large, so you may want to disable it if you have a slow internet
340 connection, or for offline use of the notebook.
340 connection, or for offline use of the notebook.
341
341
342 When disabled, equations etc. will appear as their untransformed TeX source.
342 When disabled, equations etc. will appear as their untransformed TeX source.
343 """
343 """
344 )
344 )
345 def _enable_mathjax_changed(self, name, old, new):
345 def _enable_mathjax_changed(self, name, old, new):
346 """set mathjax url to empty if mathjax is disabled"""
346 """set mathjax url to empty if mathjax is disabled"""
347 if not new:
347 if not new:
348 self.mathjax_url = u''
348 self.mathjax_url = u''
349
349
350 base_project_url = Unicode('/', config=True,
350 base_project_url = Unicode('/', config=True,
351 help='''The base URL for the notebook server''')
351 help='''The base URL for the notebook server''')
352 base_kernel_url = Unicode('/', config=True,
352 base_kernel_url = Unicode('/', config=True,
353 help='''The base URL for the kernel server''')
353 help='''The base URL for the kernel server''')
354 websocket_host = Unicode("", config=True,
354 websocket_host = Unicode("", config=True,
355 help="""The hostname for the websocket server."""
355 help="""The hostname for the websocket server."""
356 )
356 )
357
357
358 mathjax_url = Unicode("", config=True,
358 mathjax_url = Unicode("", config=True,
359 help="""The url for MathJax.js."""
359 help="""The url for MathJax.js."""
360 )
360 )
361 def _mathjax_url_default(self):
361 def _mathjax_url_default(self):
362 if not self.enable_mathjax:
362 if not self.enable_mathjax:
363 return u''
363 return u''
364 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
364 static_path = self.webapp_settings.get("static_path", os.path.join(os.path.dirname(__file__), "static"))
365 static_url_prefix = self.webapp_settings.get("static_url_prefix",
365 static_url_prefix = self.webapp_settings.get("static_url_prefix",
366 "/static/")
366 "/static/")
367 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
367 if os.path.exists(os.path.join(static_path, 'mathjax', "MathJax.js")):
368 self.log.info("Using local MathJax")
368 self.log.info("Using local MathJax")
369 return static_url_prefix+u"mathjax/MathJax.js"
369 return static_url_prefix+u"mathjax/MathJax.js"
370 else:
370 else:
371 if self.certfile:
371 if self.certfile:
372 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
372 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
373 base = u"https://c328740.ssl.cf1.rackcdn.com"
373 base = u"https://c328740.ssl.cf1.rackcdn.com"
374 else:
374 else:
375 base = u"http://cdn.mathjax.org"
375 base = u"http://cdn.mathjax.org"
376
376
377 url = base + u"/mathjax/latest/MathJax.js"
377 url = base + u"/mathjax/latest/MathJax.js"
378 self.log.info("Using MathJax from CDN: %s", url)
378 self.log.info("Using MathJax from CDN: %s", url)
379 return url
379 return url
380
380
381 def _mathjax_url_changed(self, name, old, new):
381 def _mathjax_url_changed(self, name, old, new):
382 if new and not self.enable_mathjax:
382 if new and not self.enable_mathjax:
383 # enable_mathjax=False overrides mathjax_url
383 # enable_mathjax=False overrides mathjax_url
384 self.mathjax_url = u''
384 self.mathjax_url = u''
385 else:
385 else:
386 self.log.info("Using MathJax: %s", new)
386 self.log.info("Using MathJax: %s", new)
387
387
388 def parse_command_line(self, argv=None):
388 def parse_command_line(self, argv=None):
389 super(NotebookApp, self).parse_command_line(argv)
389 super(NotebookApp, self).parse_command_line(argv)
390 if argv is None:
390 if argv is None:
391 argv = sys.argv[1:]
391 argv = sys.argv[1:]
392
392
393 # Scrub frontend-specific flags
393 # Scrub frontend-specific flags
394 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
394 self.kernel_argv = swallow_argv(argv, notebook_aliases, notebook_flags)
395 # Kernel should inherit default config file from frontend
395 # Kernel should inherit default config file from frontend
396 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
396 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
397
397
398 if self.extra_args:
398 if self.extra_args:
399 self.file_to_run = os.path.abspath(self.extra_args[0])
399 self.file_to_run = os.path.abspath(self.extra_args[0])
400 self.config.NotebookManager.notebook_dir = os.path.dirname(self.file_to_run)
400 self.config.NotebookManager.notebook_dir = os.path.dirname(self.file_to_run)
401
401
402 def init_configurables(self):
402 def init_configurables(self):
403 # force Session default to be secure
403 # force Session default to be secure
404 default_secure(self.config)
404 default_secure(self.config)
405 # Create a KernelManager and start a kernel.
405 # Create a KernelManager and start a kernel.
406 self.kernel_manager = MappingKernelManager(
406 self.kernel_manager = MappingKernelManager(
407 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
407 config=self.config, log=self.log, kernel_argv=self.kernel_argv,
408 connection_dir = self.profile_dir.security_dir,
408 connection_dir = self.profile_dir.security_dir,
409 )
409 )
410 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
410 self.notebook_manager = NotebookManager(config=self.config, log=self.log)
411 self.notebook_manager.list_notebooks()
411 self.notebook_manager.list_notebooks()
412 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
412 self.cluster_manager = ClusterManager(config=self.config, log=self.log)
413 self.cluster_manager.update_profiles()
413 self.cluster_manager.update_profiles()
414
414
415 def init_logging(self):
415 def init_logging(self):
416 # This prevents double log messages because tornado use a root logger that
416 # This prevents double log messages because tornado use a root logger that
417 # self.log is a child of. The logging module dipatches log messages to a log
417 # self.log is a child of. The logging module dipatches log messages to a log
418 # and all of its ancenstors until propagate is set to False.
418 # and all of its ancenstors until propagate is set to False.
419 self.log.propagate = False
419 self.log.propagate = False
420
420
421 def init_webapp(self):
421 def init_webapp(self):
422 """initialize tornado webapp and httpserver"""
422 """initialize tornado webapp and httpserver"""
423 self.web_app = NotebookWebApplication(
423 self.web_app = NotebookWebApplication(
424 self, self.kernel_manager, self.notebook_manager,
424 self, self.kernel_manager, self.notebook_manager,
425 self.cluster_manager, self.log,
425 self.cluster_manager, self.log,
426 self.base_project_url, self.webapp_settings
426 self.base_project_url, self.webapp_settings
427 )
427 )
428 if self.certfile:
428 if self.certfile:
429 ssl_options = dict(certfile=self.certfile)
429 ssl_options = dict(certfile=self.certfile)
430 if self.keyfile:
430 if self.keyfile:
431 ssl_options['keyfile'] = self.keyfile
431 ssl_options['keyfile'] = self.keyfile
432 else:
432 else:
433 ssl_options = None
433 ssl_options = None
434 self.web_app.password = self.password
434 self.web_app.password = self.password
435 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
435 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options)
436 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
436 if ssl_options is None and not self.ip and not (self.read_only and not self.password):
437 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
437 self.log.critical('WARNING: the notebook server is listening on all IP addresses '
438 'but not using any encryption or authentication. This is highly '
438 'but not using any encryption or authentication. This is highly '
439 'insecure and not recommended.')
439 'insecure and not recommended.')
440
440
441 success = None
441 success = None
442 for port in random_ports(self.port, self.port_retries+1):
442 for port in random_ports(self.port, self.port_retries+1):
443 try:
443 try:
444 self.http_server.listen(port, self.ip)
444 self.http_server.listen(port, self.ip)
445 except socket.error, e:
445 except socket.error, e:
446 if e.errno != errno.EADDRINUSE:
446 if e.errno != errno.EADDRINUSE:
447 raise
447 raise
448 self.log.info('The port %i is already in use, trying another random port.' % port)
448 self.log.info('The port %i is already in use, trying another random port.' % port)
449 else:
449 else:
450 self.port = port
450 self.port = port
451 success = True
451 success = True
452 break
452 break
453 if not success:
453 if not success:
454 self.log.critical('ERROR: the notebook server could not be started because '
454 self.log.critical('ERROR: the notebook server could not be started because '
455 'no available port could be found.')
455 'no available port could be found.')
456 self.exit(1)
456 self.exit(1)
457
457
458 def init_signal(self):
458 def init_signal(self):
459 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
459 # FIXME: remove this check when pyzmq dependency is >= 2.1.11
460 # safely extract zmq version info:
460 # safely extract zmq version info:
461 try:
461 try:
462 zmq_v = zmq.pyzmq_version_info()
462 zmq_v = zmq.pyzmq_version_info()
463 except AttributeError:
463 except AttributeError:
464 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
464 zmq_v = [ int(n) for n in re.findall(r'\d+', zmq.__version__) ]
465 if 'dev' in zmq.__version__:
465 if 'dev' in zmq.__version__:
466 zmq_v.append(999)
466 zmq_v.append(999)
467 zmq_v = tuple(zmq_v)
467 zmq_v = tuple(zmq_v)
468 if zmq_v >= (2,1,9):
468 if zmq_v >= (2,1,9):
469 # This won't work with 2.1.7 and
469 # This won't work with 2.1.7 and
470 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
470 # 2.1.9-10 will log ugly 'Interrupted system call' messages,
471 # but it will work
471 # but it will work
472 signal.signal(signal.SIGINT, self._handle_sigint)
472 signal.signal(signal.SIGINT, self._handle_sigint)
473 signal.signal(signal.SIGTERM, self._signal_stop)
473 signal.signal(signal.SIGTERM, self._signal_stop)
474
474
475 def _handle_sigint(self, sig, frame):
475 def _handle_sigint(self, sig, frame):
476 """SIGINT handler spawns confirmation dialog"""
476 """SIGINT handler spawns confirmation dialog"""
477 # register more forceful signal handler for ^C^C case
477 # register more forceful signal handler for ^C^C case
478 signal.signal(signal.SIGINT, self._signal_stop)
478 signal.signal(signal.SIGINT, self._signal_stop)
479 # request confirmation dialog in bg thread, to avoid
479 # request confirmation dialog in bg thread, to avoid
480 # blocking the App
480 # blocking the App
481 thread = threading.Thread(target=self._confirm_exit)
481 thread = threading.Thread(target=self._confirm_exit)
482 thread.daemon = True
482 thread.daemon = True
483 thread.start()
483 thread.start()
484
484
485 def _restore_sigint_handler(self):
485 def _restore_sigint_handler(self):
486 """callback for restoring original SIGINT handler"""
486 """callback for restoring original SIGINT handler"""
487 signal.signal(signal.SIGINT, self._handle_sigint)
487 signal.signal(signal.SIGINT, self._handle_sigint)
488
488
489 def _confirm_exit(self):
489 def _confirm_exit(self):
490 """confirm shutdown on ^C
490 """confirm shutdown on ^C
491
491
492 A second ^C, or answering 'y' within 5s will cause shutdown,
492 A second ^C, or answering 'y' within 5s will cause shutdown,
493 otherwise original SIGINT handler will be restored.
493 otherwise original SIGINT handler will be restored.
494 """
494 """
495 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
495 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
496 time.sleep(0.1)
496 time.sleep(0.1)
497 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
497 sys.stdout.write("Shutdown Notebook Server (y/[n])? ")
498 sys.stdout.flush()
498 sys.stdout.flush()
499 r,w,x = select.select([sys.stdin], [], [], 5)
499 r,w,x = select.select([sys.stdin], [], [], 5)
500 if r:
500 if r:
501 line = sys.stdin.readline()
501 line = sys.stdin.readline()
502 if line.lower().startswith('y'):
502 if line.lower().startswith('y'):
503 self.log.critical("Shutdown confirmed")
503 self.log.critical("Shutdown confirmed")
504 ioloop.IOLoop.instance().stop()
504 ioloop.IOLoop.instance().stop()
505 return
505 return
506 else:
506 else:
507 print "No answer for 5s:",
507 print "No answer for 5s:",
508 print "resuming operation..."
508 print "resuming operation..."
509 # no answer, or answer is no:
509 # no answer, or answer is no:
510 # set it back to original SIGINT handler
510 # set it back to original SIGINT handler
511 # use IOLoop.add_callback because signal.signal must be called
511 # use IOLoop.add_callback because signal.signal must be called
512 # from main thread
512 # from main thread
513 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
513 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
514
514
515 def _signal_stop(self, sig, frame):
515 def _signal_stop(self, sig, frame):
516 self.log.critical("received signal %s, stopping", sig)
516 self.log.critical("received signal %s, stopping", sig)
517 ioloop.IOLoop.instance().stop()
517 ioloop.IOLoop.instance().stop()
518
518
519 @catch_config_error
519 @catch_config_error
520 def initialize(self, argv=None):
520 def initialize(self, argv=None):
521 self.init_logging()
521 self.init_logging()
522 super(NotebookApp, self).initialize(argv)
522 super(NotebookApp, self).initialize(argv)
523 self.init_configurables()
523 self.init_configurables()
524 self.init_webapp()
524 self.init_webapp()
525 self.init_signal()
525 self.init_signal()
526
526
527 def cleanup_kernels(self):
527 def cleanup_kernels(self):
528 """shutdown all kernels
528 """shutdown all kernels
529
529
530 The kernels will shutdown themselves when this process no longer exists,
530 The kernels will shutdown themselves when this process no longer exists,
531 but explicit shutdown allows the KernelManagers to cleanup the connection files.
531 but explicit shutdown allows the KernelManagers to cleanup the connection files.
532 """
532 """
533 self.log.info('Shutting down kernels')
533 self.log.info('Shutting down kernels')
534 km = self.kernel_manager
534 km = self.kernel_manager
535 # copy list, since kill_kernel deletes keys
535 # copy list, since shutdown_kernel deletes keys
536 for kid in list(km.kernel_ids):
536 for kid in list(km.kernel_ids):
537 km.kill_kernel(kid)
537 km.shutdown_kernel(kid)
538
538
539 def start(self):
539 def start(self):
540 ip = self.ip if self.ip else '[all ip addresses on your system]'
540 ip = self.ip if self.ip else '[all ip addresses on your system]'
541 proto = 'https' if self.certfile else 'http'
541 proto = 'https' if self.certfile else 'http'
542 info = self.log.info
542 info = self.log.info
543 info("The IPython Notebook is running at: %s://%s:%i%s" %
543 info("The IPython Notebook is running at: %s://%s:%i%s" %
544 (proto, ip, self.port,self.base_project_url) )
544 (proto, ip, self.port,self.base_project_url) )
545 info("Use Control-C to stop this server and shut down all kernels.")
545 info("Use Control-C to stop this server and shut down all kernels.")
546
546
547 if self.open_browser:
547 if self.open_browser:
548 ip = self.ip or '127.0.0.1'
548 ip = self.ip or '127.0.0.1'
549 if self.browser:
549 if self.browser:
550 browser = webbrowser.get(self.browser)
550 browser = webbrowser.get(self.browser)
551 else:
551 else:
552 browser = webbrowser.get()
552 browser = webbrowser.get()
553
553
554 if self.file_to_run:
554 if self.file_to_run:
555 filename, _ = os.path.splitext(os.path.basename(self.file_to_run))
555 filename, _ = os.path.splitext(os.path.basename(self.file_to_run))
556 for nb in self.notebook_manager.list_notebooks():
556 for nb in self.notebook_manager.list_notebooks():
557 if filename == nb['name']:
557 if filename == nb['name']:
558 url = nb['notebook_id']
558 url = nb['notebook_id']
559 break
559 break
560 else:
560 else:
561 url = ''
561 url = ''
562 else:
562 else:
563 url = ''
563 url = ''
564 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
564 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
565 self.port, self.base_project_url, url),
565 self.port, self.base_project_url, url),
566 new=2)
566 new=2)
567 threading.Thread(target=b).start()
567 threading.Thread(target=b).start()
568 try:
568 try:
569 ioloop.IOLoop.instance().start()
569 ioloop.IOLoop.instance().start()
570 except KeyboardInterrupt:
570 except KeyboardInterrupt:
571 info("Interrupted...")
571 info("Interrupted...")
572 finally:
572 finally:
573 self.cleanup_kernels()
573 self.cleanup_kernels()
574
574
575
575
576 #-----------------------------------------------------------------------------
576 #-----------------------------------------------------------------------------
577 # Main entry point
577 # Main entry point
578 #-----------------------------------------------------------------------------
578 #-----------------------------------------------------------------------------
579
579
580 def launch_new_instance():
580 def launch_new_instance():
581 app = NotebookApp.instance()
581 app = NotebookApp.instance()
582 app.initialize()
582 app.initialize()
583 app.start()
583 app.start()
584
584
General Comments 0
You need to be logged in to leave comments. Login now