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