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