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