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