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