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