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