##// END OF EJS Templates
make CORS configurable...
MinRK -
Show More
@@ -1,376 +1,418 b''
1 1 """Base Tornado handlers for the notebook."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import functools
7 7 import json
8 8 import logging
9 9 import os
10 10 import re
11 11 import sys
12 12 import traceback
13 13 try:
14 14 # py3
15 15 from http.client import responses
16 16 except ImportError:
17 17 from httplib import responses
18 18
19 19 from jinja2 import TemplateNotFound
20 20 from tornado import web
21 21
22 22 try:
23 23 from tornado.log import app_log
24 24 except ImportError:
25 25 app_log = logging.getLogger()
26 26
27 27 from IPython.config import Application
28 28 from IPython.utils.path import filefind
29 29 from IPython.utils.py3compat import string_types
30 30 from IPython.html.utils import is_hidden
31 31
32 32 #-----------------------------------------------------------------------------
33 33 # Top-level handlers
34 34 #-----------------------------------------------------------------------------
35 35 non_alphanum = re.compile(r'[^A-Za-z0-9]')
36 36
37 37 class AuthenticatedHandler(web.RequestHandler):
38 38 """A RequestHandler with an authenticated user."""
39 39
40 40 def set_default_headers(self):
41 41 headers = self.settings.get('headers', {})
42 42 for header_name,value in headers.items() :
43 43 try:
44 44 self.set_header(header_name, value)
45 45 except Exception:
46 46 # tornado raise Exception (not a subclass)
47 47 # if method is unsupported (websocket and Access-Control-Allow-Origin
48 48 # for example, so just ignore)
49 49 pass
50 50
51 51 def clear_login_cookie(self):
52 52 self.clear_cookie(self.cookie_name)
53 53
54 54 def get_current_user(self):
55 55 user_id = self.get_secure_cookie(self.cookie_name)
56 56 # For now the user_id should not return empty, but it could eventually
57 57 if user_id == '':
58 58 user_id = 'anonymous'
59 59 if user_id is None:
60 60 # prevent extra Invalid cookie sig warnings:
61 61 self.clear_login_cookie()
62 62 if not self.login_available:
63 63 user_id = 'anonymous'
64 64 return user_id
65 65
66 66 @property
67 67 def cookie_name(self):
68 68 default_cookie_name = non_alphanum.sub('-', 'username-{}'.format(
69 69 self.request.host
70 70 ))
71 71 return self.settings.get('cookie_name', default_cookie_name)
72 72
73 73 @property
74 74 def password(self):
75 75 """our password"""
76 76 return self.settings.get('password', '')
77 77
78 78 @property
79 79 def logged_in(self):
80 80 """Is a user currently logged in?
81 81
82 82 """
83 83 user = self.get_current_user()
84 84 return (user and not user == 'anonymous')
85 85
86 86 @property
87 87 def login_available(self):
88 88 """May a user proceed to log in?
89 89
90 90 This returns True if login capability is available, irrespective of
91 91 whether the user is already logged in or not.
92 92
93 93 """
94 94 return bool(self.settings.get('password', ''))
95 95
96 96
97 97 class IPythonHandler(AuthenticatedHandler):
98 98 """IPython-specific extensions to authenticated handling
99 99
100 100 Mostly property shortcuts to IPython-specific settings.
101 101 """
102 102
103 103 @property
104 104 def config(self):
105 105 return self.settings.get('config', None)
106 106
107 107 @property
108 108 def log(self):
109 109 """use the IPython log by default, falling back on tornado's logger"""
110 110 if Application.initialized():
111 111 return Application.instance().log
112 112 else:
113 113 return app_log
114 114
115 115 #---------------------------------------------------------------
116 116 # URLs
117 117 #---------------------------------------------------------------
118 118
119 119 @property
120 120 def mathjax_url(self):
121 121 return self.settings.get('mathjax_url', '')
122 122
123 123 @property
124 124 def base_url(self):
125 125 return self.settings.get('base_url', '/')
126 126
127 127 #---------------------------------------------------------------
128 128 # Manager objects
129 129 #---------------------------------------------------------------
130 130
131 131 @property
132 132 def kernel_manager(self):
133 133 return self.settings['kernel_manager']
134 134
135 135 @property
136 136 def notebook_manager(self):
137 137 return self.settings['notebook_manager']
138 138
139 139 @property
140 140 def cluster_manager(self):
141 141 return self.settings['cluster_manager']
142 142
143 143 @property
144 144 def session_manager(self):
145 145 return self.settings['session_manager']
146 146
147 147 @property
148 148 def kernel_spec_manager(self):
149 149 return self.settings['kernel_spec_manager']
150 150
151 151 @property
152 152 def project_dir(self):
153 153 return self.notebook_manager.notebook_dir
154 154
155 155 #---------------------------------------------------------------
156 # CORS
157 #---------------------------------------------------------------
158
159 @property
160 def cors_origin(self):
161 """Normal Access-Control-Allow-Origin"""
162 return self.settings.get('cors_origin', '')
163
164 @property
165 def cors_origin_pat(self):
166 """Regular expression version of cors_origin"""
167 return self.settings.get('cors_origin_pat', None)
168
169 @property
170 def cors_credentials(self):
171 """Whether to set Access-Control-Allow-Credentials"""
172 return self.settings.get('cors_credentials', False)
173
174 def set_default_headers(self):
175 """Add CORS headers, if defined"""
176 super(IPythonHandler, self).set_default_headers()
177 if self.cors_origin:
178 self.set_header("Access-Control-Allow-Origin", self.cors_origin)
179 elif self.cors_origin_pat:
180 origin = self.get_origin()
181 if origin and self.cors_origin_pat.match(origin):
182 self.set_header("Access-Control-Allow-Origin", origin)
183 if self.cors_credentials:
184 self.set_header("Access-Control-Allow-Credentials", 'true')
185
186 def get_origin(self):
187 # Handle WebSocket Origin naming convention differences
188 # The difference between version 8 and 13 is that in 8 the
189 # client sends a "Sec-Websocket-Origin" header and in 13 it's
190 # simply "Origin".
191 if "Origin" in self.request.headers:
192 origin = self.request.headers.get("Origin")
193 else:
194 origin = self.request.headers.get("Sec-Websocket-Origin", None)
195 return origin
196
197 #---------------------------------------------------------------
156 198 # template rendering
157 199 #---------------------------------------------------------------
158 200
159 201 def get_template(self, name):
160 202 """Return the jinja template object for a given name"""
161 203 return self.settings['jinja2_env'].get_template(name)
162 204
163 205 def render_template(self, name, **ns):
164 206 ns.update(self.template_namespace)
165 207 template = self.get_template(name)
166 208 return template.render(**ns)
167 209
168 210 @property
169 211 def template_namespace(self):
170 212 return dict(
171 213 base_url=self.base_url,
172 214 logged_in=self.logged_in,
173 215 login_available=self.login_available,
174 216 static_url=self.static_url,
175 217 )
176 218
177 219 def get_json_body(self):
178 220 """Return the body of the request as JSON data."""
179 221 if not self.request.body:
180 222 return None
181 223 # Do we need to call body.decode('utf-8') here?
182 224 body = self.request.body.strip().decode(u'utf-8')
183 225 try:
184 226 model = json.loads(body)
185 227 except Exception:
186 228 self.log.debug("Bad JSON: %r", body)
187 229 self.log.error("Couldn't parse JSON", exc_info=True)
188 230 raise web.HTTPError(400, u'Invalid JSON in body of request')
189 231 return model
190 232
191 233 def get_error_html(self, status_code, **kwargs):
192 234 """render custom error pages"""
193 235 exception = kwargs.get('exception')
194 236 message = ''
195 237 status_message = responses.get(status_code, 'Unknown HTTP Error')
196 238 if exception:
197 239 # get the custom message, if defined
198 240 try:
199 241 message = exception.log_message % exception.args
200 242 except Exception:
201 243 pass
202 244
203 245 # construct the custom reason, if defined
204 246 reason = getattr(exception, 'reason', '')
205 247 if reason:
206 248 status_message = reason
207 249
208 250 # build template namespace
209 251 ns = dict(
210 252 status_code=status_code,
211 253 status_message=status_message,
212 254 message=message,
213 255 exception=exception,
214 256 )
215 257
216 258 # render the template
217 259 try:
218 260 html = self.render_template('%s.html' % status_code, **ns)
219 261 except TemplateNotFound:
220 262 self.log.debug("No template for %d", status_code)
221 263 html = self.render_template('error.html', **ns)
222 264 return html
223 265
224 266
225 267 class Template404(IPythonHandler):
226 268 """Render our 404 template"""
227 269 def prepare(self):
228 270 raise web.HTTPError(404)
229 271
230 272
231 273 class AuthenticatedFileHandler(IPythonHandler, web.StaticFileHandler):
232 274 """static files should only be accessible when logged in"""
233 275
234 276 @web.authenticated
235 277 def get(self, path):
236 278 if os.path.splitext(path)[1] == '.ipynb':
237 279 name = os.path.basename(path)
238 280 self.set_header('Content-Type', 'application/json')
239 281 self.set_header('Content-Disposition','attachment; filename="%s"' % name)
240 282
241 283 return web.StaticFileHandler.get(self, path)
242 284
243 285 def compute_etag(self):
244 286 return None
245 287
246 288 def validate_absolute_path(self, root, absolute_path):
247 289 """Validate and return the absolute path.
248 290
249 291 Requires tornado 3.1
250 292
251 293 Adding to tornado's own handling, forbids the serving of hidden files.
252 294 """
253 295 abs_path = super(AuthenticatedFileHandler, self).validate_absolute_path(root, absolute_path)
254 296 abs_root = os.path.abspath(root)
255 297 if is_hidden(abs_path, abs_root):
256 298 self.log.info("Refusing to serve hidden file, via 404 Error")
257 299 raise web.HTTPError(404)
258 300 return abs_path
259 301
260 302
261 303 def json_errors(method):
262 304 """Decorate methods with this to return GitHub style JSON errors.
263 305
264 306 This should be used on any JSON API on any handler method that can raise HTTPErrors.
265 307
266 308 This will grab the latest HTTPError exception using sys.exc_info
267 309 and then:
268 310
269 311 1. Set the HTTP status code based on the HTTPError
270 312 2. Create and return a JSON body with a message field describing
271 313 the error in a human readable form.
272 314 """
273 315 @functools.wraps(method)
274 316 def wrapper(self, *args, **kwargs):
275 317 try:
276 318 result = method(self, *args, **kwargs)
277 319 except web.HTTPError as e:
278 320 status = e.status_code
279 321 message = e.log_message
280 322 self.log.warn(message)
281 323 self.set_status(e.status_code)
282 324 self.finish(json.dumps(dict(message=message)))
283 325 except Exception:
284 326 self.log.error("Unhandled error in API request", exc_info=True)
285 327 status = 500
286 328 message = "Unknown server error"
287 329 t, value, tb = sys.exc_info()
288 330 self.set_status(status)
289 331 tb_text = ''.join(traceback.format_exception(t, value, tb))
290 332 reply = dict(message=message, traceback=tb_text)
291 333 self.finish(json.dumps(reply))
292 334 else:
293 335 return result
294 336 return wrapper
295 337
296 338
297 339
298 340 #-----------------------------------------------------------------------------
299 341 # File handler
300 342 #-----------------------------------------------------------------------------
301 343
302 344 # to minimize subclass changes:
303 345 HTTPError = web.HTTPError
304 346
305 347 class FileFindHandler(web.StaticFileHandler):
306 348 """subclass of StaticFileHandler for serving files from a search path"""
307 349
308 350 # cache search results, don't search for files more than once
309 351 _static_paths = {}
310 352
311 353 def initialize(self, path, default_filename=None):
312 354 if isinstance(path, string_types):
313 355 path = [path]
314 356
315 357 self.root = tuple(
316 358 os.path.abspath(os.path.expanduser(p)) + os.sep for p in path
317 359 )
318 360 self.default_filename = default_filename
319 361
320 362 def compute_etag(self):
321 363 return None
322 364
323 365 @classmethod
324 366 def get_absolute_path(cls, roots, path):
325 367 """locate a file to serve on our static file search path"""
326 368 with cls._lock:
327 369 if path in cls._static_paths:
328 370 return cls._static_paths[path]
329 371 try:
330 372 abspath = os.path.abspath(filefind(path, roots))
331 373 except IOError:
332 374 # IOError means not found
333 375 return ''
334 376
335 377 cls._static_paths[path] = abspath
336 378 return abspath
337 379
338 380 def validate_absolute_path(self, root, absolute_path):
339 381 """check if the file should be served (raises 404, 403, etc.)"""
340 382 if absolute_path == '':
341 383 raise web.HTTPError(404)
342 384
343 385 for root in self.root:
344 386 if (absolute_path + os.sep).startswith(root):
345 387 break
346 388
347 389 return super(FileFindHandler, self).validate_absolute_path(root, absolute_path)
348 390
349 391
350 392 class TrailingSlashHandler(web.RequestHandler):
351 393 """Simple redirect handler that strips trailing slashes
352 394
353 395 This should be the first, highest priority handler.
354 396 """
355 397
356 398 SUPPORTED_METHODS = ['GET']
357 399
358 400 def get(self):
359 401 self.redirect(self.request.uri.rstrip('/'))
360 402
361 403 #-----------------------------------------------------------------------------
362 404 # URL pattern fragments for re-use
363 405 #-----------------------------------------------------------------------------
364 406
365 407 path_regex = r"(?P<path>(?:/.*)*)"
366 408 notebook_name_regex = r"(?P<name>[^/]+\.ipynb)"
367 409 notebook_path_regex = "%s/%s" % (path_regex, notebook_name_regex)
368 410
369 411 #-----------------------------------------------------------------------------
370 412 # URL to handler mappings
371 413 #-----------------------------------------------------------------------------
372 414
373 415
374 416 default_handlers = [
375 417 (r".*/", TrailingSlashHandler)
376 418 ]
@@ -1,134 +1,150 b''
1 1 """Tornado handlers for WebSocket <-> ZMQ sockets."""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 import json
7 7
8 8 try:
9 9 from urllib.parse import urlparse # Py 3
10 10 except ImportError:
11 11 from urlparse import urlparse # Py 2
12 12
13 13 try:
14 14 from http.cookies import SimpleCookie # Py 3
15 15 except ImportError:
16 16 from Cookie import SimpleCookie # Py 2
17 17 import logging
18
19 import tornado
18 20 from tornado import web
19 21 from tornado import websocket
20 22
21 23 from IPython.kernel.zmq.session import Session
22 24 from IPython.utils.jsonutil import date_default
23 25 from IPython.utils.py3compat import PY3, cast_unicode
24 26
25 27 from .handlers import IPythonHandler
26 28
27 29
28 30 class ZMQStreamHandler(websocket.WebSocketHandler):
29
30 def same_origin(self):
31 """Check to see that origin and host match in the headers."""
32
33 # The difference between version 8 and 13 is that in 8 the
34 # client sends a "Sec-Websocket-Origin" header and in 13 it's
35 # simply "Origin".
36 if self.request.headers.get("Sec-WebSocket-Version") in ("7", "8"):
37 origin_header = self.request.headers.get("Sec-Websocket-Origin")
38 else:
39 origin_header = self.request.headers.get("Origin")
31
32 def check_origin(self, origin):
33 """Check Origin == Host or CORS origins."""
34 if self.cors_origin == '*':
35 return True
40 36
41 37 host = self.request.headers.get("Host")
42 38
43 39 # If no header is provided, assume we can't verify origin
44 if(origin_header is None or host is None):
40 if(origin is None or host is None):
41 return False
42
43 host_origin = "{0}://{1}".format(self.request.protocol, host)
44
45 # OK if origin matches host
46 if origin == host_origin:
47 return True
48
49 # Check CORS headers
50 if self.cors_origin:
51 if self.cors_origin == '*':
52 return True
53 else:
54 return self.cors_origin == origin
55 elif self.cors_origin_pat:
56 return bool(self.cors_origin_pat.match(origin))
57 else:
58 # No CORS headers, deny the request
45 59 return False
46
47 parsed_origin = urlparse(origin_header)
48 origin = parsed_origin.netloc
49
50 # Check to see that origin matches host directly, including ports
51 return origin == host
52 60
53 61 def clear_cookie(self, *args, **kwargs):
54 62 """meaningless for websockets"""
55 63 pass
56 64
57 65 def _reserialize_reply(self, msg_list):
58 66 """Reserialize a reply message using JSON.
59 67
60 68 This takes the msg list from the ZMQ socket, unserializes it using
61 69 self.session and then serializes the result using JSON. This method
62 70 should be used by self._on_zmq_reply to build messages that can
63 71 be sent back to the browser.
64 72 """
65 73 idents, msg_list = self.session.feed_identities(msg_list)
66 74 msg = self.session.unserialize(msg_list)
67 75 try:
68 76 msg['header'].pop('date')
69 77 except KeyError:
70 78 pass
71 79 try:
72 80 msg['parent_header'].pop('date')
73 81 except KeyError:
74 82 pass
75 83 msg.pop('buffers')
76 84 return json.dumps(msg, default=date_default)
77 85
78 86 def _on_zmq_reply(self, msg_list):
79 87 # Sometimes this gets triggered when the on_close method is scheduled in the
80 88 # eventloop but hasn't been called.
81 89 if self.stream.closed(): return
82 90 try:
83 91 msg = self._reserialize_reply(msg_list)
84 92 except Exception:
85 93 self.log.critical("Malformed message: %r" % msg_list, exc_info=True)
86 94 else:
87 95 self.write_message(msg)
88 96
89 97 def allow_draft76(self):
90 98 """Allow draft 76, until browsers such as Safari update to RFC 6455.
91 99
92 100 This has been disabled by default in tornado in release 2.2.0, and
93 101 support will be removed in later versions.
94 102 """
95 103 return True
96 104
97 105
98 106 class AuthenticatedZMQStreamHandler(ZMQStreamHandler, IPythonHandler):
107 def set_default_headers(self):
108 """Undo the set_default_headers in IPythonHandler
109
110 which doesn't make sense for websockets
111 """
112 pass
99 113
100 114 def open(self, kernel_id):
101 115 self.kernel_id = cast_unicode(kernel_id, 'ascii')
102 116 # Check to see that origin matches host directly, including ports
103 if not self.same_origin():
104 self.log.warn("Cross Origin WebSocket Attempt.")
105 raise web.HTTPError(404)
117 # Tornado 4 already does CORS checking
118 if tornado.version_info[0] < 4:
119 if not self.check_origin(self.get_origin()):
120 self.log.warn("Cross Origin WebSocket Attempt.")
121 raise web.HTTPError(404)
106 122
107 123 self.session = Session(config=self.config)
108 124 self.save_on_message = self.on_message
109 125 self.on_message = self.on_first_message
110 126
111 127 def _inject_cookie_message(self, msg):
112 128 """Inject the first message, which is the document cookie,
113 129 for authentication."""
114 130 if not PY3 and isinstance(msg, unicode):
115 131 # Cookie constructor doesn't accept unicode strings
116 132 # under Python 2.x for some reason
117 133 msg = msg.encode('utf8', 'replace')
118 134 try:
119 135 identity, msg = msg.split(':', 1)
120 136 self.session.session = cast_unicode(identity, 'ascii')
121 137 except Exception:
122 138 logging.error("First ws message didn't have the form 'identity:[cookie]' - %r", msg)
123 139
124 140 try:
125 141 self.request._cookies = SimpleCookie(msg)
126 142 except:
127 143 self.log.warn("couldn't parse cookie string: %s",msg, exc_info=True)
128 144
129 145 def on_first_message(self, msg):
130 146 self._inject_cookie_message(msg)
131 147 if self.get_current_user() is None:
132 148 self.log.warn("Couldn't authenticate WebSocket connection")
133 149 raise web.HTTPError(403)
134 150 self.on_message = self.save_on_message
@@ -1,870 +1,901 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server."""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 from __future__ import print_function
8 8
9 9 import errno
10 10 import io
11 11 import json
12 12 import logging
13 13 import os
14 14 import random
15 import re
15 16 import select
16 17 import signal
17 18 import socket
18 19 import sys
19 20 import threading
20 21 import time
21 22 import webbrowser
22 23
23 24
24 25 # check for pyzmq 2.1.11
25 26 from IPython.utils.zmqrelated import check_for_zmq
26 27 check_for_zmq('2.1.11', 'IPython.html')
27 28
28 29 from jinja2 import Environment, FileSystemLoader
29 30
30 31 # Install the pyzmq ioloop. This has to be done before anything else from
31 32 # tornado is imported.
32 33 from zmq.eventloop import ioloop
33 34 ioloop.install()
34 35
35 36 # check for tornado 3.1.0
36 37 msg = "The IPython Notebook requires tornado >= 3.1.0"
37 38 try:
38 39 import tornado
39 40 except ImportError:
40 41 raise ImportError(msg)
41 42 try:
42 43 version_info = tornado.version_info
43 44 except AttributeError:
44 45 raise ImportError(msg + ", but you have < 1.1.0")
45 46 if version_info < (3,1,0):
46 47 raise ImportError(msg + ", but you have %s" % tornado.version)
47 48
48 49 from tornado import httpserver
49 50 from tornado import web
50 51 from tornado.log import LogFormatter
51 52
52 53 from IPython.html import DEFAULT_STATIC_FILES_PATH
53 54 from .base.handlers import Template404
54 55 from .log import log_request
55 56 from .services.kernels.kernelmanager import MappingKernelManager
56 57 from .services.notebooks.nbmanager import NotebookManager
57 58 from .services.notebooks.filenbmanager import FileNotebookManager
58 59 from .services.clusters.clustermanager import ClusterManager
59 60 from .services.sessions.sessionmanager import SessionManager
60 61
61 62 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
62 63
63 64 from IPython.config import Config
64 65 from IPython.config.application import catch_config_error, boolean_flag
65 66 from IPython.core.application import (
66 67 BaseIPythonApplication, base_flags, base_aliases,
67 68 )
68 69 from IPython.core.profiledir import ProfileDir
69 70 from IPython.kernel import KernelManager
70 71 from IPython.kernel.kernelspec import KernelSpecManager
71 72 from IPython.kernel.zmq.session import default_secure, Session
72 73 from IPython.nbformat.sign import NotebookNotary
73 74 from IPython.utils.importstring import import_item
74 75 from IPython.utils import submodule
75 76 from IPython.utils.traitlets import (
76 77 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
77 78 DottedObjectName, TraitError,
78 79 )
79 80 from IPython.utils import py3compat
80 81 from IPython.utils.path import filefind, get_ipython_dir
81 82
82 83 from .utils import url_path_join
83 84
84 85 #-----------------------------------------------------------------------------
85 86 # Module globals
86 87 #-----------------------------------------------------------------------------
87 88
88 89 _examples = """
89 90 ipython notebook # start the notebook
90 91 ipython notebook --profile=sympy # use the sympy profile
91 92 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
92 93 """
93 94
94 95 #-----------------------------------------------------------------------------
95 96 # Helper functions
96 97 #-----------------------------------------------------------------------------
97 98
98 99 def random_ports(port, n):
99 100 """Generate a list of n random ports near the given port.
100 101
101 102 The first 5 ports will be sequential, and the remaining n-5 will be
102 103 randomly selected in the range [port-2*n, port+2*n].
103 104 """
104 105 for i in range(min(5, n)):
105 106 yield port + i
106 107 for i in range(n-5):
107 108 yield max(1, port + random.randint(-2*n, 2*n))
108 109
109 110 def load_handlers(name):
110 111 """Load the (URL pattern, handler) tuples for each component."""
111 112 name = 'IPython.html.' + name
112 113 mod = __import__(name, fromlist=['default_handlers'])
113 114 return mod.default_handlers
114 115
115 116 #-----------------------------------------------------------------------------
116 117 # The Tornado web application
117 118 #-----------------------------------------------------------------------------
118 119
119 120 class NotebookWebApplication(web.Application):
120 121
121 122 def __init__(self, ipython_app, kernel_manager, notebook_manager,
122 123 cluster_manager, session_manager, kernel_spec_manager, log,
123 124 base_url, settings_overrides, jinja_env_options):
124 125
125 126 settings = self.init_settings(
126 127 ipython_app, kernel_manager, notebook_manager, cluster_manager,
127 128 session_manager, kernel_spec_manager, log, base_url,
128 129 settings_overrides, jinja_env_options)
129 130 handlers = self.init_handlers(settings)
130 131
131 132 super(NotebookWebApplication, self).__init__(handlers, **settings)
132 133
133 134 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
134 135 cluster_manager, session_manager, kernel_spec_manager,
135 136 log, base_url, settings_overrides,
136 137 jinja_env_options=None):
137 138 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
138 139 # base_url will always be unicode, which will in turn
139 140 # make the patterns unicode, and ultimately result in unicode
140 141 # keys in kwargs to handler._execute(**kwargs) in tornado.
141 142 # This enforces that base_url be ascii in that situation.
142 143 #
143 144 # Note that the URLs these patterns check against are escaped,
144 145 # and thus guaranteed to be ASCII: 'héllo' is really 'h%C3%A9llo'.
145 146 base_url = py3compat.unicode_to_str(base_url, 'ascii')
146 147 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
147 148 jenv_opt = jinja_env_options if jinja_env_options else {}
148 149 env = Environment(loader=FileSystemLoader(template_path),**jenv_opt )
149 150 settings = dict(
150 151 # basics
151 152 log_function=log_request,
152 153 base_url=base_url,
153 154 template_path=template_path,
154 155 static_path=ipython_app.static_file_path,
155 156 static_handler_class = FileFindHandler,
156 157 static_url_prefix = url_path_join(base_url,'/static/'),
157 158
158 159 # authentication
159 160 cookie_secret=ipython_app.cookie_secret,
160 161 login_url=url_path_join(base_url,'/login'),
161 162 password=ipython_app.password,
162 163
163 164 # managers
164 165 kernel_manager=kernel_manager,
165 166 notebook_manager=notebook_manager,
166 167 cluster_manager=cluster_manager,
167 168 session_manager=session_manager,
168 169 kernel_spec_manager=kernel_spec_manager,
169 170
170 171 # IPython stuff
171 172 nbextensions_path = ipython_app.nbextensions_path,
172 173 mathjax_url=ipython_app.mathjax_url,
173 174 config=ipython_app.config,
174 175 jinja2_env=env,
175 176 )
176 177
177 178 # allow custom overrides for the tornado web app.
178 179 settings.update(settings_overrides)
179 180 return settings
180 181
181 182 def init_handlers(self, settings):
182 183 # Load the (URL pattern, handler) tuples for each component.
183 184 handlers = []
184 185 handlers.extend(load_handlers('base.handlers'))
185 186 handlers.extend(load_handlers('tree.handlers'))
186 187 handlers.extend(load_handlers('auth.login'))
187 188 handlers.extend(load_handlers('auth.logout'))
188 189 handlers.extend(load_handlers('notebook.handlers'))
189 190 handlers.extend(load_handlers('nbconvert.handlers'))
190 191 handlers.extend(load_handlers('kernelspecs.handlers'))
191 192 handlers.extend(load_handlers('services.kernels.handlers'))
192 193 handlers.extend(load_handlers('services.notebooks.handlers'))
193 194 handlers.extend(load_handlers('services.clusters.handlers'))
194 195 handlers.extend(load_handlers('services.sessions.handlers'))
195 196 handlers.extend(load_handlers('services.nbconvert.handlers'))
196 197 handlers.extend(load_handlers('services.kernelspecs.handlers'))
197 198 # FIXME: /files/ should be handled by the Contents service when it exists
198 199 nbm = settings['notebook_manager']
199 200 if hasattr(nbm, 'notebook_dir'):
200 201 handlers.extend([
201 202 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : nbm.notebook_dir}),
202 203 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
203 204 ])
204 205 # prepend base_url onto the patterns that we match
205 206 new_handlers = []
206 207 for handler in handlers:
207 208 pattern = url_path_join(settings['base_url'], handler[0])
208 209 new_handler = tuple([pattern] + list(handler[1:]))
209 210 new_handlers.append(new_handler)
210 211 # add 404 on the end, which will catch everything that falls through
211 212 new_handlers.append((r'(.*)', Template404))
212 213 return new_handlers
213 214
214 215
215 216 class NbserverListApp(BaseIPythonApplication):
216 217
217 218 description="List currently running notebook servers in this profile."
218 219
219 220 flags = dict(
220 221 json=({'NbserverListApp': {'json': True}},
221 222 "Produce machine-readable JSON output."),
222 223 )
223 224
224 225 json = Bool(False, config=True,
225 226 help="If True, each line of output will be a JSON object with the "
226 227 "details from the server info file.")
227 228
228 229 def start(self):
229 230 if not self.json:
230 231 print("Currently running servers:")
231 232 for serverinfo in list_running_servers(self.profile):
232 233 if self.json:
233 234 print(json.dumps(serverinfo))
234 235 else:
235 236 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
236 237
237 238 #-----------------------------------------------------------------------------
238 239 # Aliases and Flags
239 240 #-----------------------------------------------------------------------------
240 241
241 242 flags = dict(base_flags)
242 243 flags['no-browser']=(
243 244 {'NotebookApp' : {'open_browser' : False}},
244 245 "Don't open the notebook in a browser after startup."
245 246 )
246 247 flags['pylab']=(
247 248 {'NotebookApp' : {'pylab' : 'warn'}},
248 249 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
249 250 )
250 251 flags['no-mathjax']=(
251 252 {'NotebookApp' : {'enable_mathjax' : False}},
252 253 """Disable MathJax
253 254
254 255 MathJax is the javascript library IPython uses to render math/LaTeX. It is
255 256 very large, so you may want to disable it if you have a slow internet
256 257 connection, or for offline use of the notebook.
257 258
258 259 When disabled, equations etc. will appear as their untransformed TeX source.
259 260 """
260 261 )
261 262
262 263 # Add notebook manager flags
263 264 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
264 265 'Auto-save a .py script everytime the .ipynb notebook is saved',
265 266 'Do not auto-save .py scripts for every notebook'))
266 267
267 268 aliases = dict(base_aliases)
268 269
269 270 aliases.update({
270 271 'ip': 'NotebookApp.ip',
271 272 'port': 'NotebookApp.port',
272 273 'port-retries': 'NotebookApp.port_retries',
273 274 'transport': 'KernelManager.transport',
274 275 'keyfile': 'NotebookApp.keyfile',
275 276 'certfile': 'NotebookApp.certfile',
276 277 'notebook-dir': 'NotebookApp.notebook_dir',
277 278 'browser': 'NotebookApp.browser',
278 279 'pylab': 'NotebookApp.pylab',
279 280 })
280 281
281 282 #-----------------------------------------------------------------------------
282 283 # NotebookApp
283 284 #-----------------------------------------------------------------------------
284 285
285 286 class NotebookApp(BaseIPythonApplication):
286 287
287 288 name = 'ipython-notebook'
288 289
289 290 description = """
290 291 The IPython HTML Notebook.
291 292
292 293 This launches a Tornado based HTML Notebook Server that serves up an
293 294 HTML5/Javascript Notebook client.
294 295 """
295 296 examples = _examples
296 297 aliases = aliases
297 298 flags = flags
298 299
299 300 classes = [
300 301 KernelManager, ProfileDir, Session, MappingKernelManager,
301 302 NotebookManager, FileNotebookManager, NotebookNotary,
302 303 ]
303 304 flags = Dict(flags)
304 305 aliases = Dict(aliases)
305 306
306 307 subcommands = dict(
307 308 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
308 309 )
309 310
310 311 kernel_argv = List(Unicode)
311 312
312 313 _log_formatter_cls = LogFormatter
313 314
314 315 def _log_level_default(self):
315 316 return logging.INFO
316 317
317 318 def _log_datefmt_default(self):
318 319 """Exclude date from default date format"""
319 320 return "%H:%M:%S"
320 321
321 322 def _log_format_default(self):
322 323 """override default log format to include time"""
323 324 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
324 325
325 326 # create requested profiles by default, if they don't exist:
326 327 auto_create = Bool(True)
327 328
328 329 # file to be opened in the notebook server
329 330 file_to_run = Unicode('', config=True)
330 331 def _file_to_run_changed(self, name, old, new):
331 332 path, base = os.path.split(new)
332 333 if path:
333 334 self.file_to_run = base
334 335 self.notebook_dir = path
335 336
336 # Network related information.
337
337 # Network related information
338
339 cors_origin = Unicode('', config=True,
340 help="""Set the Access-Control-Allow-Origin header
341
342 Use '*' to allow any origin to access your server.
343
344 Mutually exclusive with cors_origin_pat.
345 """
346 )
347
348 cors_origin_pat = Unicode('', config=True,
349 help="""Use a regular expression for the Access-Control-Allow-Origin header
350
351 Requests from an origin matching the expression will get replies with:
352
353 Access-Control-Allow-Origin: origin
354
355 where `origin` is the origin of the request.
356
357 Mutually exclusive with cors_origin.
358 """
359 )
360
361 cors_credentials = Bool(False, config=True,
362 help="Set the Access-Control-Allow-Credentials: true header"
363 )
364
338 365 ip = Unicode('localhost', config=True,
339 366 help="The IP address the notebook server will listen on."
340 367 )
341 368
342 369 def _ip_changed(self, name, old, new):
343 370 if new == u'*': self.ip = u''
344 371
345 372 port = Integer(8888, config=True,
346 373 help="The port the notebook server will listen on."
347 374 )
348 375 port_retries = Integer(50, config=True,
349 376 help="The number of additional ports to try if the specified port is not available."
350 377 )
351 378
352 379 certfile = Unicode(u'', config=True,
353 380 help="""The full path to an SSL/TLS certificate file."""
354 381 )
355 382
356 383 keyfile = Unicode(u'', config=True,
357 384 help="""The full path to a private key file for usage with SSL/TLS."""
358 385 )
359 386
360 387 cookie_secret = Bytes(b'', config=True,
361 388 help="""The random bytes used to secure cookies.
362 389 By default this is a new random number every time you start the Notebook.
363 390 Set it to a value in a config file to enable logins to persist across server sessions.
364 391
365 392 Note: Cookie secrets should be kept private, do not share config files with
366 393 cookie_secret stored in plaintext (you can read the value from a file).
367 394 """
368 395 )
369 396 def _cookie_secret_default(self):
370 397 return os.urandom(1024)
371 398
372 399 password = Unicode(u'', config=True,
373 400 help="""Hashed password to use for web authentication.
374 401
375 402 To generate, type in a python/IPython shell:
376 403
377 404 from IPython.lib import passwd; passwd()
378 405
379 406 The string should be of the form type:salt:hashed-password.
380 407 """
381 408 )
382 409
383 410 open_browser = Bool(True, config=True,
384 411 help="""Whether to open in a browser after starting.
385 412 The specific browser used is platform dependent and
386 413 determined by the python standard library `webbrowser`
387 414 module, unless it is overridden using the --browser
388 415 (NotebookApp.browser) configuration option.
389 416 """)
390 417
391 418 browser = Unicode(u'', config=True,
392 419 help="""Specify what command to use to invoke a web
393 420 browser when opening the notebook. If not specified, the
394 421 default browser will be determined by the `webbrowser`
395 422 standard library module, which allows setting of the
396 423 BROWSER environment variable to override it.
397 424 """)
398 425
399 426 webapp_settings = Dict(config=True,
400 427 help="Supply overrides for the tornado.web.Application that the "
401 428 "IPython notebook uses.")
402 429
403 430 jinja_environment_options = Dict(config=True,
404 431 help="Supply extra arguments that will be passed to Jinja environment.")
405 432
406 433
407 434 enable_mathjax = Bool(True, config=True,
408 435 help="""Whether to enable MathJax for typesetting math/TeX
409 436
410 437 MathJax is the javascript library IPython uses to render math/LaTeX. It is
411 438 very large, so you may want to disable it if you have a slow internet
412 439 connection, or for offline use of the notebook.
413 440
414 441 When disabled, equations etc. will appear as their untransformed TeX source.
415 442 """
416 443 )
417 444 def _enable_mathjax_changed(self, name, old, new):
418 445 """set mathjax url to empty if mathjax is disabled"""
419 446 if not new:
420 447 self.mathjax_url = u''
421 448
422 449 base_url = Unicode('/', config=True,
423 450 help='''The base URL for the notebook server.
424 451
425 452 Leading and trailing slashes can be omitted,
426 453 and will automatically be added.
427 454 ''')
428 455 def _base_url_changed(self, name, old, new):
429 456 if not new.startswith('/'):
430 457 self.base_url = '/'+new
431 458 elif not new.endswith('/'):
432 459 self.base_url = new+'/'
433 460
434 461 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
435 462 def _base_project_url_changed(self, name, old, new):
436 463 self.log.warn("base_project_url is deprecated, use base_url")
437 464 self.base_url = new
438 465
439 466 extra_static_paths = List(Unicode, config=True,
440 467 help="""Extra paths to search for serving static files.
441 468
442 469 This allows adding javascript/css to be available from the notebook server machine,
443 470 or overriding individual files in the IPython"""
444 471 )
445 472 def _extra_static_paths_default(self):
446 473 return [os.path.join(self.profile_dir.location, 'static')]
447 474
448 475 @property
449 476 def static_file_path(self):
450 477 """return extra paths + the default location"""
451 478 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
452 479
453 480 nbextensions_path = List(Unicode, config=True,
454 481 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
455 482 )
456 483 def _nbextensions_path_default(self):
457 484 return [os.path.join(get_ipython_dir(), 'nbextensions')]
458 485
459 486 mathjax_url = Unicode("", config=True,
460 487 help="""The url for MathJax.js."""
461 488 )
462 489 def _mathjax_url_default(self):
463 490 if not self.enable_mathjax:
464 491 return u''
465 492 static_url_prefix = self.webapp_settings.get("static_url_prefix",
466 493 url_path_join(self.base_url, "static")
467 494 )
468 495
469 496 # try local mathjax, either in nbextensions/mathjax or static/mathjax
470 497 for (url_prefix, search_path) in [
471 498 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
472 499 (static_url_prefix, self.static_file_path),
473 500 ]:
474 501 self.log.debug("searching for local mathjax in %s", search_path)
475 502 try:
476 503 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
477 504 except IOError:
478 505 continue
479 506 else:
480 507 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
481 508 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
482 509 return url
483 510
484 511 # no local mathjax, serve from CDN
485 512 if self.certfile:
486 513 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
487 514 host = u"https://c328740.ssl.cf1.rackcdn.com"
488 515 else:
489 516 host = u"http://cdn.mathjax.org"
490 517
491 518 url = host + u"/mathjax/latest/MathJax.js"
492 519 self.log.info("Using MathJax from CDN: %s", url)
493 520 return url
494 521
495 522 def _mathjax_url_changed(self, name, old, new):
496 523 if new and not self.enable_mathjax:
497 524 # enable_mathjax=False overrides mathjax_url
498 525 self.mathjax_url = u''
499 526 else:
500 527 self.log.info("Using MathJax: %s", new)
501 528
502 529 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
503 530 config=True,
504 531 help='The notebook manager class to use.'
505 532 )
506 533 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
507 534 config=True,
508 535 help='The kernel manager class to use.'
509 536 )
510 537 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
511 538 config=True,
512 539 help='The session manager class to use.'
513 540 )
514 541 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
515 542 config=True,
516 543 help='The cluster manager class to use.'
517 544 )
518 545
519 546 kernel_spec_manager = Instance(KernelSpecManager)
520 547
521 548 def _kernel_spec_manager_default(self):
522 549 return KernelSpecManager(ipython_dir=self.ipython_dir)
523 550
524 551 trust_xheaders = Bool(False, config=True,
525 552 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
526 553 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
527 554 )
528 555
529 556 info_file = Unicode()
530 557
531 558 def _info_file_default(self):
532 559 info_file = "nbserver-%s.json"%os.getpid()
533 560 return os.path.join(self.profile_dir.security_dir, info_file)
534 561
535 562 notebook_dir = Unicode(py3compat.getcwd(), config=True,
536 563 help="The directory to use for notebooks and kernels."
537 564 )
538 565
539 566 pylab = Unicode('disabled', config=True,
540 567 help="""
541 568 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
542 569 """
543 570 )
544 571 def _pylab_changed(self, name, old, new):
545 572 """when --pylab is specified, display a warning and exit"""
546 573 if new != 'warn':
547 574 backend = ' %s' % new
548 575 else:
549 576 backend = ''
550 577 self.log.error("Support for specifying --pylab on the command line has been removed.")
551 578 self.log.error(
552 579 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
553 580 )
554 581 self.exit(1)
555 582
556 583 def _notebook_dir_changed(self, name, old, new):
557 584 """Do a bit of validation of the notebook dir."""
558 585 if not os.path.isabs(new):
559 586 # If we receive a non-absolute path, make it absolute.
560 587 self.notebook_dir = os.path.abspath(new)
561 588 return
562 589 if not os.path.isdir(new):
563 590 raise TraitError("No such notebook dir: %r" % new)
564 591
565 592 # setting App.notebook_dir implies setting notebook and kernel dirs as well
566 593 self.config.FileNotebookManager.notebook_dir = new
567 594 self.config.MappingKernelManager.root_dir = new
568 595
569 596
570 597 def parse_command_line(self, argv=None):
571 598 super(NotebookApp, self).parse_command_line(argv)
572 599
573 600 if self.extra_args:
574 601 arg0 = self.extra_args[0]
575 602 f = os.path.abspath(arg0)
576 603 self.argv.remove(arg0)
577 604 if not os.path.exists(f):
578 605 self.log.critical("No such file or directory: %s", f)
579 606 self.exit(1)
580 607
581 608 # Use config here, to ensure that it takes higher priority than
582 609 # anything that comes from the profile.
583 610 c = Config()
584 611 if os.path.isdir(f):
585 612 c.NotebookApp.notebook_dir = f
586 613 elif os.path.isfile(f):
587 614 c.NotebookApp.file_to_run = f
588 615 self.update_config(c)
589 616
590 617 def init_kernel_argv(self):
591 618 """construct the kernel arguments"""
592 619 # Kernel should get *absolute* path to profile directory
593 620 self.kernel_argv = ["--profile-dir", self.profile_dir.location]
594 621
595 622 def init_configurables(self):
596 623 # force Session default to be secure
597 624 default_secure(self.config)
598 625 kls = import_item(self.kernel_manager_class)
599 626 self.kernel_manager = kls(
600 627 parent=self, log=self.log, kernel_argv=self.kernel_argv,
601 628 connection_dir = self.profile_dir.security_dir,
602 629 )
603 630 kls = import_item(self.notebook_manager_class)
604 631 self.notebook_manager = kls(parent=self, log=self.log)
605 632 kls = import_item(self.session_manager_class)
606 633 self.session_manager = kls(parent=self, log=self.log)
607 634 kls = import_item(self.cluster_manager_class)
608 635 self.cluster_manager = kls(parent=self, log=self.log)
609 636 self.cluster_manager.update_profiles()
610 637
611 638 def init_logging(self):
612 639 # This prevents double log messages because tornado use a root logger that
613 640 # self.log is a child of. The logging module dipatches log messages to a log
614 641 # and all of its ancenstors until propagate is set to False.
615 642 self.log.propagate = False
616 643
617 644 # hook up tornado 3's loggers to our app handlers
618 645 logger = logging.getLogger('tornado')
619 646 logger.propagate = True
620 647 logger.parent = self.log
621 648 logger.setLevel(self.log.level)
622 649
623 650 def init_webapp(self):
624 651 """initialize tornado webapp and httpserver"""
652 self.webapp_settings['cors_origin'] = self.cors_origin
653 self.webapp_settings['cors_origin_pat'] = re.compile(self.cors_origin_pat)
654 self.webapp_settings['cors_credentials'] = self.cors_credentials
655
625 656 self.web_app = NotebookWebApplication(
626 657 self, self.kernel_manager, self.notebook_manager,
627 658 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
628 659 self.log, self.base_url, self.webapp_settings,
629 660 self.jinja_environment_options
630 661 )
631 662 if self.certfile:
632 663 ssl_options = dict(certfile=self.certfile)
633 664 if self.keyfile:
634 665 ssl_options['keyfile'] = self.keyfile
635 666 else:
636 667 ssl_options = None
637 668 self.web_app.password = self.password
638 669 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
639 670 xheaders=self.trust_xheaders)
640 671 if not self.ip:
641 672 warning = "WARNING: The notebook server is listening on all IP addresses"
642 673 if ssl_options is None:
643 674 self.log.critical(warning + " and not using encryption. This "
644 675 "is not recommended.")
645 676 if not self.password:
646 677 self.log.critical(warning + " and not using authentication. "
647 678 "This is highly insecure and not recommended.")
648 679 success = None
649 680 for port in random_ports(self.port, self.port_retries+1):
650 681 try:
651 682 self.http_server.listen(port, self.ip)
652 683 except socket.error as e:
653 684 if e.errno == errno.EADDRINUSE:
654 685 self.log.info('The port %i is already in use, trying another random port.' % port)
655 686 continue
656 687 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
657 688 self.log.warn("Permission to listen on port %i denied" % port)
658 689 continue
659 690 else:
660 691 raise
661 692 else:
662 693 self.port = port
663 694 success = True
664 695 break
665 696 if not success:
666 697 self.log.critical('ERROR: the notebook server could not be started because '
667 698 'no available port could be found.')
668 699 self.exit(1)
669 700
670 701 @property
671 702 def display_url(self):
672 703 ip = self.ip if self.ip else '[all ip addresses on your system]'
673 704 return self._url(ip)
674 705
675 706 @property
676 707 def connection_url(self):
677 708 ip = self.ip if self.ip else 'localhost'
678 709 return self._url(ip)
679 710
680 711 def _url(self, ip):
681 712 proto = 'https' if self.certfile else 'http'
682 713 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
683 714
684 715 def init_signal(self):
685 716 if not sys.platform.startswith('win'):
686 717 signal.signal(signal.SIGINT, self._handle_sigint)
687 718 signal.signal(signal.SIGTERM, self._signal_stop)
688 719 if hasattr(signal, 'SIGUSR1'):
689 720 # Windows doesn't support SIGUSR1
690 721 signal.signal(signal.SIGUSR1, self._signal_info)
691 722 if hasattr(signal, 'SIGINFO'):
692 723 # only on BSD-based systems
693 724 signal.signal(signal.SIGINFO, self._signal_info)
694 725
695 726 def _handle_sigint(self, sig, frame):
696 727 """SIGINT handler spawns confirmation dialog"""
697 728 # register more forceful signal handler for ^C^C case
698 729 signal.signal(signal.SIGINT, self._signal_stop)
699 730 # request confirmation dialog in bg thread, to avoid
700 731 # blocking the App
701 732 thread = threading.Thread(target=self._confirm_exit)
702 733 thread.daemon = True
703 734 thread.start()
704 735
705 736 def _restore_sigint_handler(self):
706 737 """callback for restoring original SIGINT handler"""
707 738 signal.signal(signal.SIGINT, self._handle_sigint)
708 739
709 740 def _confirm_exit(self):
710 741 """confirm shutdown on ^C
711 742
712 743 A second ^C, or answering 'y' within 5s will cause shutdown,
713 744 otherwise original SIGINT handler will be restored.
714 745
715 746 This doesn't work on Windows.
716 747 """
717 748 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
718 749 time.sleep(0.1)
719 750 info = self.log.info
720 751 info('interrupted')
721 752 print(self.notebook_info())
722 753 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
723 754 sys.stdout.flush()
724 755 r,w,x = select.select([sys.stdin], [], [], 5)
725 756 if r:
726 757 line = sys.stdin.readline()
727 758 if line.lower().startswith('y') and 'n' not in line.lower():
728 759 self.log.critical("Shutdown confirmed")
729 760 ioloop.IOLoop.instance().stop()
730 761 return
731 762 else:
732 763 print("No answer for 5s:", end=' ')
733 764 print("resuming operation...")
734 765 # no answer, or answer is no:
735 766 # set it back to original SIGINT handler
736 767 # use IOLoop.add_callback because signal.signal must be called
737 768 # from main thread
738 769 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
739 770
740 771 def _signal_stop(self, sig, frame):
741 772 self.log.critical("received signal %s, stopping", sig)
742 773 ioloop.IOLoop.instance().stop()
743 774
744 775 def _signal_info(self, sig, frame):
745 776 print(self.notebook_info())
746 777
747 778 def init_components(self):
748 779 """Check the components submodule, and warn if it's unclean"""
749 780 status = submodule.check_submodule_status()
750 781 if status == 'missing':
751 782 self.log.warn("components submodule missing, running `git submodule update`")
752 783 submodule.update_submodules(submodule.ipython_parent())
753 784 elif status == 'unclean':
754 785 self.log.warn("components submodule unclean, you may see 404s on static/components")
755 786 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
756 787
757 788 @catch_config_error
758 789 def initialize(self, argv=None):
759 790 super(NotebookApp, self).initialize(argv)
760 791 self.init_logging()
761 792 self.init_kernel_argv()
762 793 self.init_configurables()
763 794 self.init_components()
764 795 self.init_webapp()
765 796 self.init_signal()
766 797
767 798 def cleanup_kernels(self):
768 799 """Shutdown all kernels.
769 800
770 801 The kernels will shutdown themselves when this process no longer exists,
771 802 but explicit shutdown allows the KernelManagers to cleanup the connection files.
772 803 """
773 804 self.log.info('Shutting down kernels')
774 805 self.kernel_manager.shutdown_all()
775 806
776 807 def notebook_info(self):
777 808 "Return the current working directory and the server url information"
778 809 info = self.notebook_manager.info_string() + "\n"
779 810 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
780 811 return info + "The IPython Notebook is running at: %s" % self.display_url
781 812
782 813 def server_info(self):
783 814 """Return a JSONable dict of information about this server."""
784 815 return {'url': self.connection_url,
785 816 'hostname': self.ip if self.ip else 'localhost',
786 817 'port': self.port,
787 818 'secure': bool(self.certfile),
788 819 'base_url': self.base_url,
789 820 'notebook_dir': os.path.abspath(self.notebook_dir),
790 821 }
791 822
792 823 def write_server_info_file(self):
793 824 """Write the result of server_info() to the JSON file info_file."""
794 825 with open(self.info_file, 'w') as f:
795 826 json.dump(self.server_info(), f, indent=2)
796 827
797 828 def remove_server_info_file(self):
798 829 """Remove the nbserver-<pid>.json file created for this server.
799 830
800 831 Ignores the error raised when the file has already been removed.
801 832 """
802 833 try:
803 834 os.unlink(self.info_file)
804 835 except OSError as e:
805 836 if e.errno != errno.ENOENT:
806 837 raise
807 838
808 839 def start(self):
809 840 """ Start the IPython Notebook server app, after initialization
810 841
811 842 This method takes no arguments so all configuration and initialization
812 843 must be done prior to calling this method."""
813 844 if self.subapp is not None:
814 845 return self.subapp.start()
815 846
816 847 info = self.log.info
817 848 for line in self.notebook_info().split("\n"):
818 849 info(line)
819 850 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
820 851
821 852 self.write_server_info_file()
822 853
823 854 if self.open_browser or self.file_to_run:
824 855 try:
825 856 browser = webbrowser.get(self.browser or None)
826 857 except webbrowser.Error as e:
827 858 self.log.warn('No web browser found: %s.' % e)
828 859 browser = None
829 860
830 861 if self.file_to_run:
831 862 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
832 863 if not os.path.exists(fullpath):
833 864 self.log.critical("%s does not exist" % fullpath)
834 865 self.exit(1)
835 866
836 867 uri = url_path_join('notebooks', self.file_to_run)
837 868 else:
838 869 uri = 'tree'
839 870 if browser:
840 871 b = lambda : browser.open(url_path_join(self.connection_url, uri),
841 872 new=2)
842 873 threading.Thread(target=b).start()
843 874 try:
844 875 ioloop.IOLoop.instance().start()
845 876 except KeyboardInterrupt:
846 877 info("Interrupted...")
847 878 finally:
848 879 self.cleanup_kernels()
849 880 self.remove_server_info_file()
850 881
851 882
852 883 def list_running_servers(profile='default'):
853 884 """Iterate over the server info files of running notebook servers.
854 885
855 886 Given a profile name, find nbserver-* files in the security directory of
856 887 that profile, and yield dicts of their information, each one pertaining to
857 888 a currently running notebook server instance.
858 889 """
859 890 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
860 891 for file in os.listdir(pd.security_dir):
861 892 if file.startswith('nbserver-'):
862 893 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
863 894 yield json.load(f)
864 895
865 896 #-----------------------------------------------------------------------------
866 897 # Main entry point
867 898 #-----------------------------------------------------------------------------
868 899
869 900 launch_new_instance = NotebookApp.launch_instance
870 901
General Comments 0
You need to be logged in to leave comments. Login now