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