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