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