##// END OF EJS Templates
Merge pull request #6962 from takluyver/nb-dir-and-file-to-run...
Thomas Kluyver -
r19004:2fda8e1a merge
parent child Browse files
Show More
@@ -1,993 +1,994 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 base64
10 10 import errno
11 11 import io
12 12 import json
13 13 import logging
14 14 import os
15 15 import random
16 16 import re
17 17 import select
18 18 import signal
19 19 import socket
20 20 import sys
21 21 import threading
22 22 import time
23 23 import webbrowser
24 24
25 25
26 26 # check for pyzmq 2.1.11
27 27 from IPython.utils.zmqrelated import check_for_zmq
28 28 check_for_zmq('2.1.11', 'IPython.html')
29 29
30 30 from jinja2 import Environment, FileSystemLoader
31 31
32 32 # Install the pyzmq ioloop. This has to be done before anything else from
33 33 # tornado is imported.
34 34 from zmq.eventloop import ioloop
35 35 ioloop.install()
36 36
37 37 # check for tornado 3.1.0
38 38 msg = "The IPython Notebook requires tornado >= 4.0"
39 39 try:
40 40 import tornado
41 41 except ImportError:
42 42 raise ImportError(msg)
43 43 try:
44 44 version_info = tornado.version_info
45 45 except AttributeError:
46 46 raise ImportError(msg + ", but you have < 1.1.0")
47 47 if version_info < (4,0):
48 48 raise ImportError(msg + ", but you have %s" % tornado.version)
49 49
50 50 from tornado import httpserver
51 51 from tornado import web
52 52 from tornado.log import LogFormatter, app_log, access_log, gen_log
53 53
54 54 from IPython.html import (
55 55 DEFAULT_STATIC_FILES_PATH,
56 56 DEFAULT_TEMPLATE_PATH_LIST,
57 57 )
58 58 from .base.handlers import Template404
59 59 from .log import log_request
60 60 from .services.kernels.kernelmanager import MappingKernelManager
61 61 from .services.contents.manager import ContentsManager
62 62 from .services.contents.filemanager import FileContentsManager
63 63 from .services.clusters.clustermanager import ClusterManager
64 64 from .services.sessions.sessionmanager import SessionManager
65 65
66 66 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
67 67
68 68 from IPython.config import Config
69 69 from IPython.config.application import catch_config_error, boolean_flag
70 70 from IPython.core.application import (
71 71 BaseIPythonApplication, base_flags, base_aliases,
72 72 )
73 73 from IPython.core.profiledir import ProfileDir
74 74 from IPython.kernel import KernelManager
75 75 from IPython.kernel.kernelspec import KernelSpecManager
76 76 from IPython.kernel.zmq.session import default_secure, Session
77 77 from IPython.nbformat.sign import NotebookNotary
78 78 from IPython.utils.importstring import import_item
79 79 from IPython.utils import submodule
80 80 from IPython.utils.process import check_pid
81 81 from IPython.utils.traitlets import (
82 82 Dict, Unicode, Integer, List, Bool, Bytes, Instance,
83 83 DottedObjectName, TraitError,
84 84 )
85 85 from IPython.utils import py3compat
86 86 from IPython.utils.path import filefind, get_ipython_dir
87 87
88 88 from .utils import url_path_join
89 89
90 90 #-----------------------------------------------------------------------------
91 91 # Module globals
92 92 #-----------------------------------------------------------------------------
93 93
94 94 _examples = """
95 95 ipython notebook # start the notebook
96 96 ipython notebook --profile=sympy # use the sympy profile
97 97 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
98 98 """
99 99
100 100 #-----------------------------------------------------------------------------
101 101 # Helper functions
102 102 #-----------------------------------------------------------------------------
103 103
104 104 def random_ports(port, n):
105 105 """Generate a list of n random ports near the given port.
106 106
107 107 The first 5 ports will be sequential, and the remaining n-5 will be
108 108 randomly selected in the range [port-2*n, port+2*n].
109 109 """
110 110 for i in range(min(5, n)):
111 111 yield port + i
112 112 for i in range(n-5):
113 113 yield max(1, port + random.randint(-2*n, 2*n))
114 114
115 115 def load_handlers(name):
116 116 """Load the (URL pattern, handler) tuples for each component."""
117 117 name = 'IPython.html.' + name
118 118 mod = __import__(name, fromlist=['default_handlers'])
119 119 return mod.default_handlers
120 120
121 121 #-----------------------------------------------------------------------------
122 122 # The Tornado web application
123 123 #-----------------------------------------------------------------------------
124 124
125 125 class NotebookWebApplication(web.Application):
126 126
127 127 def __init__(self, ipython_app, kernel_manager, contents_manager,
128 128 cluster_manager, session_manager, kernel_spec_manager, log,
129 129 base_url, default_url, settings_overrides, jinja_env_options):
130 130
131 131 settings = self.init_settings(
132 132 ipython_app, kernel_manager, contents_manager, cluster_manager,
133 133 session_manager, kernel_spec_manager, log, base_url, default_url,
134 134 settings_overrides, jinja_env_options)
135 135 handlers = self.init_handlers(settings)
136 136
137 137 super(NotebookWebApplication, self).__init__(handlers, **settings)
138 138
139 139 def init_settings(self, ipython_app, kernel_manager, contents_manager,
140 140 cluster_manager, session_manager, kernel_spec_manager,
141 141 log, base_url, default_url, settings_overrides,
142 142 jinja_env_options=None):
143 143
144 144 _template_path = settings_overrides.get(
145 145 "template_path",
146 146 ipython_app.template_file_path,
147 147 )
148 148 if isinstance(_template_path, str):
149 149 _template_path = (_template_path,)
150 150 template_path = [os.path.expanduser(path) for path in _template_path]
151 151
152 152 jenv_opt = jinja_env_options if jinja_env_options else {}
153 153 env = Environment(loader=FileSystemLoader(template_path), **jenv_opt)
154 154 settings = dict(
155 155 # basics
156 156 log_function=log_request,
157 157 base_url=base_url,
158 158 default_url=default_url,
159 159 template_path=template_path,
160 160 static_path=ipython_app.static_file_path,
161 161 static_handler_class = FileFindHandler,
162 162 static_url_prefix = url_path_join(base_url,'/static/'),
163 163
164 164 # authentication
165 165 cookie_secret=ipython_app.cookie_secret,
166 166 login_url=url_path_join(base_url,'/login'),
167 167 password=ipython_app.password,
168 168
169 169 # managers
170 170 kernel_manager=kernel_manager,
171 171 contents_manager=contents_manager,
172 172 cluster_manager=cluster_manager,
173 173 session_manager=session_manager,
174 174 kernel_spec_manager=kernel_spec_manager,
175 175
176 176 # IPython stuff
177 177 nbextensions_path = ipython_app.nbextensions_path,
178 178 websocket_url=ipython_app.websocket_url,
179 179 mathjax_url=ipython_app.mathjax_url,
180 180 config=ipython_app.config,
181 181 jinja2_env=env,
182 182 terminals_available=False, # Set later if terminals are available
183 183 profile_dir = ipython_app.profile_dir.location,
184 184 )
185 185
186 186 # allow custom overrides for the tornado web app.
187 187 settings.update(settings_overrides)
188 188 return settings
189 189
190 190 def init_handlers(self, settings):
191 191 """Load the (URL pattern, handler) tuples for each component."""
192 192
193 193 # Order matters. The first handler to match the URL will handle the request.
194 194 handlers = []
195 195 handlers.extend(load_handlers('tree.handlers'))
196 196 handlers.extend(load_handlers('auth.login'))
197 197 handlers.extend(load_handlers('auth.logout'))
198 198 handlers.extend(load_handlers('files.handlers'))
199 199 handlers.extend(load_handlers('notebook.handlers'))
200 200 handlers.extend(load_handlers('nbconvert.handlers'))
201 201 handlers.extend(load_handlers('kernelspecs.handlers'))
202 202 handlers.extend(load_handlers('services.config.handlers'))
203 203 handlers.extend(load_handlers('services.kernels.handlers'))
204 204 handlers.extend(load_handlers('services.contents.handlers'))
205 205 handlers.extend(load_handlers('services.clusters.handlers'))
206 206 handlers.extend(load_handlers('services.sessions.handlers'))
207 207 handlers.extend(load_handlers('services.nbconvert.handlers'))
208 208 handlers.extend(load_handlers('services.kernelspecs.handlers'))
209 209 handlers.append(
210 210 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
211 211 )
212 212 # register base handlers last
213 213 handlers.extend(load_handlers('base.handlers'))
214 214 # set the URL that will be redirected from `/`
215 215 handlers.append(
216 216 (r'/?', web.RedirectHandler, {
217 217 'url' : url_path_join(settings['base_url'], settings['default_url']),
218 218 'permanent': False, # want 302, not 301
219 219 })
220 220 )
221 221 # prepend base_url onto the patterns that we match
222 222 new_handlers = []
223 223 for handler in handlers:
224 224 pattern = url_path_join(settings['base_url'], handler[0])
225 225 new_handler = tuple([pattern] + list(handler[1:]))
226 226 new_handlers.append(new_handler)
227 227 # add 404 on the end, which will catch everything that falls through
228 228 new_handlers.append((r'(.*)', Template404))
229 229 return new_handlers
230 230
231 231
232 232 class NbserverListApp(BaseIPythonApplication):
233 233
234 234 description="List currently running notebook servers in this profile."
235 235
236 236 flags = dict(
237 237 json=({'NbserverListApp': {'json': True}},
238 238 "Produce machine-readable JSON output."),
239 239 )
240 240
241 241 json = Bool(False, config=True,
242 242 help="If True, each line of output will be a JSON object with the "
243 243 "details from the server info file.")
244 244
245 245 def start(self):
246 246 if not self.json:
247 247 print("Currently running servers:")
248 248 for serverinfo in list_running_servers(self.profile):
249 249 if self.json:
250 250 print(json.dumps(serverinfo))
251 251 else:
252 252 print(serverinfo['url'], "::", serverinfo['notebook_dir'])
253 253
254 254 #-----------------------------------------------------------------------------
255 255 # Aliases and Flags
256 256 #-----------------------------------------------------------------------------
257 257
258 258 flags = dict(base_flags)
259 259 flags['no-browser']=(
260 260 {'NotebookApp' : {'open_browser' : False}},
261 261 "Don't open the notebook in a browser after startup."
262 262 )
263 263 flags['pylab']=(
264 264 {'NotebookApp' : {'pylab' : 'warn'}},
265 265 "DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib."
266 266 )
267 267 flags['no-mathjax']=(
268 268 {'NotebookApp' : {'enable_mathjax' : False}},
269 269 """Disable MathJax
270 270
271 271 MathJax is the javascript library IPython uses to render math/LaTeX. It is
272 272 very large, so you may want to disable it if you have a slow internet
273 273 connection, or for offline use of the notebook.
274 274
275 275 When disabled, equations etc. will appear as their untransformed TeX source.
276 276 """
277 277 )
278 278
279 279 # Add notebook manager flags
280 280 flags.update(boolean_flag('script', 'FileContentsManager.save_script',
281 281 'DEPRECATED, IGNORED',
282 282 'DEPRECATED, IGNORED'))
283 283
284 284 aliases = dict(base_aliases)
285 285
286 286 aliases.update({
287 287 'ip': 'NotebookApp.ip',
288 288 'port': 'NotebookApp.port',
289 289 'port-retries': 'NotebookApp.port_retries',
290 290 'transport': 'KernelManager.transport',
291 291 'keyfile': 'NotebookApp.keyfile',
292 292 'certfile': 'NotebookApp.certfile',
293 293 'notebook-dir': 'NotebookApp.notebook_dir',
294 294 'browser': 'NotebookApp.browser',
295 295 'pylab': 'NotebookApp.pylab',
296 296 })
297 297
298 298 #-----------------------------------------------------------------------------
299 299 # NotebookApp
300 300 #-----------------------------------------------------------------------------
301 301
302 302 class NotebookApp(BaseIPythonApplication):
303 303
304 304 name = 'ipython-notebook'
305 305
306 306 description = """
307 307 The IPython HTML Notebook.
308 308
309 309 This launches a Tornado based HTML Notebook Server that serves up an
310 310 HTML5/Javascript Notebook client.
311 311 """
312 312 examples = _examples
313 313 aliases = aliases
314 314 flags = flags
315 315
316 316 classes = [
317 317 KernelManager, ProfileDir, Session, MappingKernelManager,
318 318 ContentsManager, FileContentsManager, NotebookNotary,
319 319 ]
320 320 flags = Dict(flags)
321 321 aliases = Dict(aliases)
322 322
323 323 subcommands = dict(
324 324 list=(NbserverListApp, NbserverListApp.description.splitlines()[0]),
325 325 )
326 326
327 327 ipython_kernel_argv = List(Unicode)
328 328
329 329 _log_formatter_cls = LogFormatter
330 330
331 331 def _log_level_default(self):
332 332 return logging.INFO
333 333
334 334 def _log_datefmt_default(self):
335 335 """Exclude date from default date format"""
336 336 return "%H:%M:%S"
337 337
338 338 def _log_format_default(self):
339 339 """override default log format to include time"""
340 340 return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
341 341
342 342 # create requested profiles by default, if they don't exist:
343 343 auto_create = Bool(True)
344 344
345 345 # file to be opened in the notebook server
346 346 file_to_run = Unicode('', config=True)
347 def _file_to_run_changed(self, name, old, new):
348 path, base = os.path.split(new)
349 if path:
350 self.file_to_run = base
351 self.notebook_dir = path
352 347
353 348 # Network related information
354 349
355 350 allow_origin = Unicode('', config=True,
356 351 help="""Set the Access-Control-Allow-Origin header
357 352
358 353 Use '*' to allow any origin to access your server.
359 354
360 355 Takes precedence over allow_origin_pat.
361 356 """
362 357 )
363 358
364 359 allow_origin_pat = Unicode('', config=True,
365 360 help="""Use a regular expression for the Access-Control-Allow-Origin header
366 361
367 362 Requests from an origin matching the expression will get replies with:
368 363
369 364 Access-Control-Allow-Origin: origin
370 365
371 366 where `origin` is the origin of the request.
372 367
373 368 Ignored if allow_origin is set.
374 369 """
375 370 )
376 371
377 372 allow_credentials = Bool(False, config=True,
378 373 help="Set the Access-Control-Allow-Credentials: true header"
379 374 )
380 375
381 376 default_url = Unicode('/tree', config=True,
382 377 help="The default URL to redirect to from `/`"
383 378 )
384 379
385 380 ip = Unicode('localhost', config=True,
386 381 help="The IP address the notebook server will listen on."
387 382 )
388 383
389 384 def _ip_changed(self, name, old, new):
390 385 if new == u'*': self.ip = u''
391 386
392 387 port = Integer(8888, config=True,
393 388 help="The port the notebook server will listen on."
394 389 )
395 390 port_retries = Integer(50, config=True,
396 391 help="The number of additional ports to try if the specified port is not available."
397 392 )
398 393
399 394 certfile = Unicode(u'', config=True,
400 395 help="""The full path to an SSL/TLS certificate file."""
401 396 )
402 397
403 398 keyfile = Unicode(u'', config=True,
404 399 help="""The full path to a private key file for usage with SSL/TLS."""
405 400 )
406 401
407 402 cookie_secret_file = Unicode(config=True,
408 403 help="""The file where the cookie secret is stored."""
409 404 )
410 405 def _cookie_secret_file_default(self):
411 406 if self.profile_dir is None:
412 407 return ''
413 408 return os.path.join(self.profile_dir.security_dir, 'notebook_cookie_secret')
414 409
415 410 cookie_secret = Bytes(b'', config=True,
416 411 help="""The random bytes used to secure cookies.
417 412 By default this is a new random number every time you start the Notebook.
418 413 Set it to a value in a config file to enable logins to persist across server sessions.
419 414
420 415 Note: Cookie secrets should be kept private, do not share config files with
421 416 cookie_secret stored in plaintext (you can read the value from a file).
422 417 """
423 418 )
424 419 def _cookie_secret_default(self):
425 420 if os.path.exists(self.cookie_secret_file):
426 421 with io.open(self.cookie_secret_file, 'rb') as f:
427 422 return f.read()
428 423 else:
429 424 secret = base64.encodestring(os.urandom(1024))
430 425 self._write_cookie_secret_file(secret)
431 426 return secret
432 427
433 428 def _write_cookie_secret_file(self, secret):
434 429 """write my secret to my secret_file"""
435 430 self.log.info("Writing notebook server cookie secret to %s", self.cookie_secret_file)
436 431 with io.open(self.cookie_secret_file, 'wb') as f:
437 432 f.write(secret)
438 433 try:
439 434 os.chmod(self.cookie_secret_file, 0o600)
440 435 except OSError:
441 436 self.log.warn(
442 437 "Could not set permissions on %s",
443 438 self.cookie_secret_file
444 439 )
445 440
446 441 password = Unicode(u'', config=True,
447 442 help="""Hashed password to use for web authentication.
448 443
449 444 To generate, type in a python/IPython shell:
450 445
451 446 from IPython.lib import passwd; passwd()
452 447
453 448 The string should be of the form type:salt:hashed-password.
454 449 """
455 450 )
456 451
457 452 open_browser = Bool(True, config=True,
458 453 help="""Whether to open in a browser after starting.
459 454 The specific browser used is platform dependent and
460 455 determined by the python standard library `webbrowser`
461 456 module, unless it is overridden using the --browser
462 457 (NotebookApp.browser) configuration option.
463 458 """)
464 459
465 460 browser = Unicode(u'', config=True,
466 461 help="""Specify what command to use to invoke a web
467 462 browser when opening the notebook. If not specified, the
468 463 default browser will be determined by the `webbrowser`
469 464 standard library module, which allows setting of the
470 465 BROWSER environment variable to override it.
471 466 """)
472 467
473 468 webapp_settings = Dict(config=True,
474 469 help="DEPRECATED, use tornado_settings"
475 470 )
476 471 def _webapp_settings_changed(self, name, old, new):
477 472 self.log.warn("\n webapp_settings is deprecated, use tornado_settings.\n")
478 473 self.tornado_settings = new
479 474
480 475 tornado_settings = Dict(config=True,
481 476 help="Supply overrides for the tornado.web.Application that the "
482 477 "IPython notebook uses.")
483 478
484 479 jinja_environment_options = Dict(config=True,
485 480 help="Supply extra arguments that will be passed to Jinja environment.")
486 481
487 482
488 483 enable_mathjax = Bool(True, config=True,
489 484 help="""Whether to enable MathJax for typesetting math/TeX
490 485
491 486 MathJax is the javascript library IPython uses to render math/LaTeX. It is
492 487 very large, so you may want to disable it if you have a slow internet
493 488 connection, or for offline use of the notebook.
494 489
495 490 When disabled, equations etc. will appear as their untransformed TeX source.
496 491 """
497 492 )
498 493 def _enable_mathjax_changed(self, name, old, new):
499 494 """set mathjax url to empty if mathjax is disabled"""
500 495 if not new:
501 496 self.mathjax_url = u''
502 497
503 498 base_url = Unicode('/', config=True,
504 499 help='''The base URL for the notebook server.
505 500
506 501 Leading and trailing slashes can be omitted,
507 502 and will automatically be added.
508 503 ''')
509 504 def _base_url_changed(self, name, old, new):
510 505 if not new.startswith('/'):
511 506 self.base_url = '/'+new
512 507 elif not new.endswith('/'):
513 508 self.base_url = new+'/'
514 509
515 510 base_project_url = Unicode('/', config=True, help="""DEPRECATED use base_url""")
516 511 def _base_project_url_changed(self, name, old, new):
517 512 self.log.warn("base_project_url is deprecated, use base_url")
518 513 self.base_url = new
519 514
520 515 extra_static_paths = List(Unicode, config=True,
521 516 help="""Extra paths to search for serving static files.
522 517
523 518 This allows adding javascript/css to be available from the notebook server machine,
524 519 or overriding individual files in the IPython"""
525 520 )
526 521 def _extra_static_paths_default(self):
527 522 return [os.path.join(self.profile_dir.location, 'static')]
528 523
529 524 @property
530 525 def static_file_path(self):
531 526 """return extra paths + the default location"""
532 527 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
533 528
534 529 extra_template_paths = List(Unicode, config=True,
535 530 help="""Extra paths to search for serving jinja templates.
536 531
537 532 Can be used to override templates from IPython.html.templates."""
538 533 )
539 534 def _extra_template_paths_default(self):
540 535 return []
541 536
542 537 @property
543 538 def template_file_path(self):
544 539 """return extra paths + the default locations"""
545 540 return self.extra_template_paths + DEFAULT_TEMPLATE_PATH_LIST
546 541
547 542 nbextensions_path = List(Unicode, config=True,
548 543 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
549 544 )
550 545 def _nbextensions_path_default(self):
551 546 return [os.path.join(get_ipython_dir(), 'nbextensions')]
552 547
553 548 websocket_url = Unicode("", config=True,
554 549 help="""The base URL for websockets,
555 550 if it differs from the HTTP server (hint: it almost certainly doesn't).
556 551
557 552 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
558 553 """
559 554 )
560 555 mathjax_url = Unicode("", config=True,
561 556 help="""The url for MathJax.js."""
562 557 )
563 558 def _mathjax_url_default(self):
564 559 if not self.enable_mathjax:
565 560 return u''
566 561 static_url_prefix = self.tornado_settings.get("static_url_prefix",
567 562 url_path_join(self.base_url, "static")
568 563 )
569 564
570 565 # try local mathjax, either in nbextensions/mathjax or static/mathjax
571 566 for (url_prefix, search_path) in [
572 567 (url_path_join(self.base_url, "nbextensions"), self.nbextensions_path),
573 568 (static_url_prefix, self.static_file_path),
574 569 ]:
575 570 self.log.debug("searching for local mathjax in %s", search_path)
576 571 try:
577 572 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
578 573 except IOError:
579 574 continue
580 575 else:
581 576 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
582 577 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
583 578 return url
584 579
585 580 # no local mathjax, serve from CDN
586 581 url = u"https://cdn.mathjax.org/mathjax/latest/MathJax.js"
587 582 self.log.info("Using MathJax from CDN: %s", url)
588 583 return url
589 584
590 585 def _mathjax_url_changed(self, name, old, new):
591 586 if new and not self.enable_mathjax:
592 587 # enable_mathjax=False overrides mathjax_url
593 588 self.mathjax_url = u''
594 589 else:
595 590 self.log.info("Using MathJax: %s", new)
596 591
597 592 contents_manager_class = DottedObjectName('IPython.html.services.contents.filemanager.FileContentsManager',
598 593 config=True,
599 594 help='The notebook manager class to use.'
600 595 )
601 596 kernel_manager_class = DottedObjectName('IPython.html.services.kernels.kernelmanager.MappingKernelManager',
602 597 config=True,
603 598 help='The kernel manager class to use.'
604 599 )
605 600 session_manager_class = DottedObjectName('IPython.html.services.sessions.sessionmanager.SessionManager',
606 601 config=True,
607 602 help='The session manager class to use.'
608 603 )
609 604 cluster_manager_class = DottedObjectName('IPython.html.services.clusters.clustermanager.ClusterManager',
610 605 config=True,
611 606 help='The cluster manager class to use.'
612 607 )
613 608
614 609 kernel_spec_manager = Instance(KernelSpecManager)
615 610
616 611 def _kernel_spec_manager_default(self):
617 612 return KernelSpecManager(ipython_dir=self.ipython_dir)
618 613
619 614 trust_xheaders = Bool(False, config=True,
620 615 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
621 616 "sent by the upstream reverse proxy. Necessary if the proxy handles SSL")
622 617 )
623 618
624 619 info_file = Unicode()
625 620
626 621 def _info_file_default(self):
627 622 info_file = "nbserver-%s.json"%os.getpid()
628 623 return os.path.join(self.profile_dir.security_dir, info_file)
629 624
630 notebook_dir = Unicode(py3compat.getcwd(), config=True,
631 help="The directory to use for notebooks and kernels."
632 )
633
634 625 pylab = Unicode('disabled', config=True,
635 626 help="""
636 627 DISABLED: use %pylab or %matplotlib in the notebook to enable matplotlib.
637 628 """
638 629 )
639 630 def _pylab_changed(self, name, old, new):
640 631 """when --pylab is specified, display a warning and exit"""
641 632 if new != 'warn':
642 633 backend = ' %s' % new
643 634 else:
644 635 backend = ''
645 636 self.log.error("Support for specifying --pylab on the command line has been removed.")
646 637 self.log.error(
647 638 "Please use `%pylab{0}` or `%matplotlib{0}` in the notebook itself.".format(backend)
648 639 )
649 640 self.exit(1)
650 641
642 notebook_dir = Unicode(config=True,
643 help="The directory to use for notebooks and kernels."
644 )
645
646 def _notebook_dir_default(self):
647 if self.file_to_run:
648 return os.path.dirname(os.path.abspath(self.file_to_run))
649 else:
650 return py3compat.getcwd()
651
651 652 def _notebook_dir_changed(self, name, old, new):
652 653 """Do a bit of validation of the notebook dir."""
653 654 if not os.path.isabs(new):
654 655 # If we receive a non-absolute path, make it absolute.
655 656 self.notebook_dir = os.path.abspath(new)
656 657 return
657 658 if not os.path.isdir(new):
658 659 raise TraitError("No such notebook dir: %r" % new)
659 660
660 661 # setting App.notebook_dir implies setting notebook and kernel dirs as well
661 662 self.config.FileContentsManager.root_dir = new
662 663 self.config.MappingKernelManager.root_dir = new
663 664
664 665
665 666 def parse_command_line(self, argv=None):
666 667 super(NotebookApp, self).parse_command_line(argv)
667 668
668 669 if self.extra_args:
669 670 arg0 = self.extra_args[0]
670 671 f = os.path.abspath(arg0)
671 672 self.argv.remove(arg0)
672 673 if not os.path.exists(f):
673 674 self.log.critical("No such file or directory: %s", f)
674 675 self.exit(1)
675 676
676 677 # Use config here, to ensure that it takes higher priority than
677 678 # anything that comes from the profile.
678 679 c = Config()
679 680 if os.path.isdir(f):
680 681 c.NotebookApp.notebook_dir = f
681 682 elif os.path.isfile(f):
682 683 c.NotebookApp.file_to_run = f
683 684 self.update_config(c)
684 685
685 686 def init_kernel_argv(self):
686 687 """add the profile-dir to arguments to be passed to IPython kernels"""
687 688 # FIXME: remove special treatment of IPython kernels
688 689 # Kernel should get *absolute* path to profile directory
689 690 self.ipython_kernel_argv = ["--profile-dir", self.profile_dir.location]
690 691
691 692 def init_configurables(self):
692 693 # force Session default to be secure
693 694 default_secure(self.config)
694 695 kls = import_item(self.kernel_manager_class)
695 696 self.kernel_manager = kls(
696 697 parent=self, log=self.log, ipython_kernel_argv=self.ipython_kernel_argv,
697 698 connection_dir = self.profile_dir.security_dir,
698 699 )
699 700 kls = import_item(self.contents_manager_class)
700 701 self.contents_manager = kls(parent=self, log=self.log)
701 702 kls = import_item(self.session_manager_class)
702 703 self.session_manager = kls(parent=self, log=self.log,
703 704 kernel_manager=self.kernel_manager,
704 705 contents_manager=self.contents_manager)
705 706 kls = import_item(self.cluster_manager_class)
706 707 self.cluster_manager = kls(parent=self, log=self.log)
707 708 self.cluster_manager.update_profiles()
708 709
709 710 def init_logging(self):
710 711 # This prevents double log messages because tornado use a root logger that
711 712 # self.log is a child of. The logging module dipatches log messages to a log
712 713 # and all of its ancenstors until propagate is set to False.
713 714 self.log.propagate = False
714 715
715 716 for log in app_log, access_log, gen_log:
716 717 # consistent log output name (NotebookApp instead of tornado.access, etc.)
717 718 log.name = self.log.name
718 719 # hook up tornado 3's loggers to our app handlers
719 720 logger = logging.getLogger('tornado')
720 721 logger.propagate = True
721 722 logger.parent = self.log
722 723 logger.setLevel(self.log.level)
723 724
724 725 def init_webapp(self):
725 726 """initialize tornado webapp and httpserver"""
726 727 self.tornado_settings['allow_origin'] = self.allow_origin
727 728 if self.allow_origin_pat:
728 729 self.tornado_settings['allow_origin_pat'] = re.compile(self.allow_origin_pat)
729 730 self.tornado_settings['allow_credentials'] = self.allow_credentials
730 731
731 732 self.web_app = NotebookWebApplication(
732 733 self, self.kernel_manager, self.contents_manager,
733 734 self.cluster_manager, self.session_manager, self.kernel_spec_manager,
734 735 self.log, self.base_url, self.default_url, self.tornado_settings,
735 736 self.jinja_environment_options
736 737 )
737 738 if self.certfile:
738 739 ssl_options = dict(certfile=self.certfile)
739 740 if self.keyfile:
740 741 ssl_options['keyfile'] = self.keyfile
741 742 else:
742 743 ssl_options = None
743 744 self.web_app.password = self.password
744 745 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
745 746 xheaders=self.trust_xheaders)
746 747 if not self.ip:
747 748 warning = "WARNING: The notebook server is listening on all IP addresses"
748 749 if ssl_options is None:
749 750 self.log.critical(warning + " and not using encryption. This "
750 751 "is not recommended.")
751 752 if not self.password:
752 753 self.log.critical(warning + " and not using authentication. "
753 754 "This is highly insecure and not recommended.")
754 755 success = None
755 756 for port in random_ports(self.port, self.port_retries+1):
756 757 try:
757 758 self.http_server.listen(port, self.ip)
758 759 except socket.error as e:
759 760 if e.errno == errno.EADDRINUSE:
760 761 self.log.info('The port %i is already in use, trying another random port.' % port)
761 762 continue
762 763 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
763 764 self.log.warn("Permission to listen on port %i denied" % port)
764 765 continue
765 766 else:
766 767 raise
767 768 else:
768 769 self.port = port
769 770 success = True
770 771 break
771 772 if not success:
772 773 self.log.critical('ERROR: the notebook server could not be started because '
773 774 'no available port could be found.')
774 775 self.exit(1)
775 776
776 777 @property
777 778 def display_url(self):
778 779 ip = self.ip if self.ip else '[all ip addresses on your system]'
779 780 return self._url(ip)
780 781
781 782 @property
782 783 def connection_url(self):
783 784 ip = self.ip if self.ip else 'localhost'
784 785 return self._url(ip)
785 786
786 787 def _url(self, ip):
787 788 proto = 'https' if self.certfile else 'http'
788 789 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_url)
789 790
790 791 def init_terminals(self):
791 792 try:
792 793 from .terminal import initialize
793 794 initialize(self.web_app)
794 795 self.web_app.settings['terminals_available'] = True
795 796 except ImportError as e:
796 797 self.log.info("Terminals not available (error was %s)", e)
797 798
798 799 def init_signal(self):
799 800 if not sys.platform.startswith('win'):
800 801 signal.signal(signal.SIGINT, self._handle_sigint)
801 802 signal.signal(signal.SIGTERM, self._signal_stop)
802 803 if hasattr(signal, 'SIGUSR1'):
803 804 # Windows doesn't support SIGUSR1
804 805 signal.signal(signal.SIGUSR1, self._signal_info)
805 806 if hasattr(signal, 'SIGINFO'):
806 807 # only on BSD-based systems
807 808 signal.signal(signal.SIGINFO, self._signal_info)
808 809
809 810 def _handle_sigint(self, sig, frame):
810 811 """SIGINT handler spawns confirmation dialog"""
811 812 # register more forceful signal handler for ^C^C case
812 813 signal.signal(signal.SIGINT, self._signal_stop)
813 814 # request confirmation dialog in bg thread, to avoid
814 815 # blocking the App
815 816 thread = threading.Thread(target=self._confirm_exit)
816 817 thread.daemon = True
817 818 thread.start()
818 819
819 820 def _restore_sigint_handler(self):
820 821 """callback for restoring original SIGINT handler"""
821 822 signal.signal(signal.SIGINT, self._handle_sigint)
822 823
823 824 def _confirm_exit(self):
824 825 """confirm shutdown on ^C
825 826
826 827 A second ^C, or answering 'y' within 5s will cause shutdown,
827 828 otherwise original SIGINT handler will be restored.
828 829
829 830 This doesn't work on Windows.
830 831 """
831 832 info = self.log.info
832 833 info('interrupted')
833 834 print(self.notebook_info())
834 835 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
835 836 sys.stdout.flush()
836 837 r,w,x = select.select([sys.stdin], [], [], 5)
837 838 if r:
838 839 line = sys.stdin.readline()
839 840 if line.lower().startswith('y') and 'n' not in line.lower():
840 841 self.log.critical("Shutdown confirmed")
841 842 ioloop.IOLoop.instance().stop()
842 843 return
843 844 else:
844 845 print("No answer for 5s:", end=' ')
845 846 print("resuming operation...")
846 847 # no answer, or answer is no:
847 848 # set it back to original SIGINT handler
848 849 # use IOLoop.add_callback because signal.signal must be called
849 850 # from main thread
850 851 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
851 852
852 853 def _signal_stop(self, sig, frame):
853 854 self.log.critical("received signal %s, stopping", sig)
854 855 ioloop.IOLoop.instance().stop()
855 856
856 857 def _signal_info(self, sig, frame):
857 858 print(self.notebook_info())
858 859
859 860 def init_components(self):
860 861 """Check the components submodule, and warn if it's unclean"""
861 862 status = submodule.check_submodule_status()
862 863 if status == 'missing':
863 864 self.log.warn("components submodule missing, running `git submodule update`")
864 865 submodule.update_submodules(submodule.ipython_parent())
865 866 elif status == 'unclean':
866 867 self.log.warn("components submodule unclean, you may see 404s on static/components")
867 868 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
868 869
869 870 @catch_config_error
870 871 def initialize(self, argv=None):
871 872 super(NotebookApp, self).initialize(argv)
872 873 self.init_logging()
873 874 self.init_kernel_argv()
874 875 self.init_configurables()
875 876 self.init_components()
876 877 self.init_webapp()
877 878 self.init_terminals()
878 879 self.init_signal()
879 880
880 881 def cleanup_kernels(self):
881 882 """Shutdown all kernels.
882 883
883 884 The kernels will shutdown themselves when this process no longer exists,
884 885 but explicit shutdown allows the KernelManagers to cleanup the connection files.
885 886 """
886 887 self.log.info('Shutting down kernels')
887 888 self.kernel_manager.shutdown_all()
888 889
889 890 def notebook_info(self):
890 891 "Return the current working directory and the server url information"
891 892 info = self.contents_manager.info_string() + "\n"
892 893 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
893 894 return info + "The IPython Notebook is running at: %s" % self.display_url
894 895
895 896 def server_info(self):
896 897 """Return a JSONable dict of information about this server."""
897 898 return {'url': self.connection_url,
898 899 'hostname': self.ip if self.ip else 'localhost',
899 900 'port': self.port,
900 901 'secure': bool(self.certfile),
901 902 'base_url': self.base_url,
902 903 'notebook_dir': os.path.abspath(self.notebook_dir),
903 904 'pid': os.getpid()
904 905 }
905 906
906 907 def write_server_info_file(self):
907 908 """Write the result of server_info() to the JSON file info_file."""
908 909 with open(self.info_file, 'w') as f:
909 910 json.dump(self.server_info(), f, indent=2)
910 911
911 912 def remove_server_info_file(self):
912 913 """Remove the nbserver-<pid>.json file created for this server.
913 914
914 915 Ignores the error raised when the file has already been removed.
915 916 """
916 917 try:
917 918 os.unlink(self.info_file)
918 919 except OSError as e:
919 920 if e.errno != errno.ENOENT:
920 921 raise
921 922
922 923 def start(self):
923 924 """ Start the IPython Notebook server app, after initialization
924 925
925 926 This method takes no arguments so all configuration and initialization
926 927 must be done prior to calling this method."""
927 928 if self.subapp is not None:
928 929 return self.subapp.start()
929 930
930 931 info = self.log.info
931 932 for line in self.notebook_info().split("\n"):
932 933 info(line)
933 934 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
934 935
935 936 self.write_server_info_file()
936 937
937 938 if self.open_browser or self.file_to_run:
938 939 try:
939 940 browser = webbrowser.get(self.browser or None)
940 941 except webbrowser.Error as e:
941 942 self.log.warn('No web browser found: %s.' % e)
942 943 browser = None
943 944
944 945 if self.file_to_run:
945 fullpath = os.path.join(self.notebook_dir, self.file_to_run)
946 if not os.path.exists(fullpath):
947 self.log.critical("%s does not exist" % fullpath)
946 if not os.path.exists(self.file_to_run):
947 self.log.critical("%s does not exist" % self.file_to_run)
948 948 self.exit(1)
949
950 uri = url_path_join('notebooks', self.file_to_run)
949
950 relpath = os.path.relpath(self.file_to_run, self.notebook_dir)
951 uri = url_path_join('notebooks', *relpath.split(os.sep))
951 952 else:
952 953 uri = 'tree'
953 954 if browser:
954 955 b = lambda : browser.open(url_path_join(self.connection_url, uri),
955 956 new=2)
956 957 threading.Thread(target=b).start()
957 958 try:
958 959 ioloop.IOLoop.instance().start()
959 960 except KeyboardInterrupt:
960 961 info("Interrupted...")
961 962 finally:
962 963 self.cleanup_kernels()
963 964 self.remove_server_info_file()
964 965
965 966
966 967 def list_running_servers(profile='default'):
967 968 """Iterate over the server info files of running notebook servers.
968 969
969 970 Given a profile name, find nbserver-* files in the security directory of
970 971 that profile, and yield dicts of their information, each one pertaining to
971 972 a currently running notebook server instance.
972 973 """
973 974 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
974 975 for file in os.listdir(pd.security_dir):
975 976 if file.startswith('nbserver-'):
976 977 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
977 978 info = json.load(f)
978 979
979 980 # Simple check whether that process is really still running
980 981 if check_pid(info['pid']):
981 982 yield info
982 983 else:
983 984 # If the process has died, try to delete its info file
984 985 try:
985 986 os.unlink(file)
986 987 except OSError:
987 988 pass # TODO: This should warn or log or something
988 989 #-----------------------------------------------------------------------------
989 990 # Main entry point
990 991 #-----------------------------------------------------------------------------
991 992
992 993 launch_new_instance = NotebookApp.launch_instance
993 994
@@ -1,559 +1,565 b''
1 1 """A contents manager that uses the local file system for storage."""
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 base64
7 7 import io
8 8 import os
9 9 import glob
10 10 import shutil
11 11
12 12 from tornado import web
13 13
14 14 from .manager import ContentsManager
15 15 from IPython import nbformat
16 16 from IPython.utils.io import atomic_writing
17 17 from IPython.utils.path import ensure_dir_exists
18 18 from IPython.utils.traitlets import Unicode, Bool, TraitError
19 19 from IPython.utils.py3compat import getcwd
20 20 from IPython.utils import tz
21 21 from IPython.html.utils import is_hidden, to_os_path, url_path_join
22 22
23 23
24 24 class FileContentsManager(ContentsManager):
25 25
26 root_dir = Unicode(getcwd(), config=True)
26 root_dir = Unicode(config=True)
27
28 def _root_dir_default(self):
29 try:
30 return self.parent.notebook_dir
31 except AttributeError:
32 return getcwd()
27 33
28 34 save_script = Bool(False, config=True, help='DEPRECATED, IGNORED')
29 35 def _save_script_changed(self):
30 36 self.log.warn("""
31 37 Automatically saving notebooks as scripts has been removed.
32 38 Use `ipython nbconvert --to python [notebook]` instead.
33 39 """)
34 40
35 41 def _root_dir_changed(self, name, old, new):
36 42 """Do a bit of validation of the root_dir."""
37 43 if not os.path.isabs(new):
38 44 # If we receive a non-absolute path, make it absolute.
39 45 self.root_dir = os.path.abspath(new)
40 46 return
41 47 if not os.path.isdir(new):
42 48 raise TraitError("%r is not a directory" % new)
43 49
44 50 checkpoint_dir = Unicode('.ipynb_checkpoints', config=True,
45 51 help="""The directory name in which to keep file checkpoints
46 52
47 53 This is a path relative to the file's own directory.
48 54
49 55 By default, it is .ipynb_checkpoints
50 56 """
51 57 )
52 58
53 59 def _copy(self, src, dest):
54 60 """copy src to dest
55 61
56 62 like shutil.copy2, but log errors in copystat
57 63 """
58 64 shutil.copyfile(src, dest)
59 65 try:
60 66 shutil.copystat(src, dest)
61 67 except OSError as e:
62 68 self.log.debug("copystat on %s failed", dest, exc_info=True)
63 69
64 70 def _get_os_path(self, path):
65 71 """Given an API path, return its file system path.
66 72
67 73 Parameters
68 74 ----------
69 75 path : string
70 76 The relative API path to the named file.
71 77
72 78 Returns
73 79 -------
74 80 path : string
75 81 Native, absolute OS path to for a file.
76 82 """
77 83 return to_os_path(path, self.root_dir)
78 84
79 85 def dir_exists(self, path):
80 86 """Does the API-style path refer to an extant directory?
81 87
82 88 API-style wrapper for os.path.isdir
83 89
84 90 Parameters
85 91 ----------
86 92 path : string
87 93 The path to check. This is an API path (`/` separated,
88 94 relative to root_dir).
89 95
90 96 Returns
91 97 -------
92 98 exists : bool
93 99 Whether the path is indeed a directory.
94 100 """
95 101 path = path.strip('/')
96 102 os_path = self._get_os_path(path=path)
97 103 return os.path.isdir(os_path)
98 104
99 105 def is_hidden(self, path):
100 106 """Does the API style path correspond to a hidden directory or file?
101 107
102 108 Parameters
103 109 ----------
104 110 path : string
105 111 The path to check. This is an API path (`/` separated,
106 112 relative to root_dir).
107 113
108 114 Returns
109 115 -------
110 116 hidden : bool
111 117 Whether the path exists and is hidden.
112 118 """
113 119 path = path.strip('/')
114 120 os_path = self._get_os_path(path=path)
115 121 return is_hidden(os_path, self.root_dir)
116 122
117 123 def file_exists(self, path):
118 124 """Returns True if the file exists, else returns False.
119 125
120 126 API-style wrapper for os.path.isfile
121 127
122 128 Parameters
123 129 ----------
124 130 path : string
125 131 The relative path to the file (with '/' as separator)
126 132
127 133 Returns
128 134 -------
129 135 exists : bool
130 136 Whether the file exists.
131 137 """
132 138 path = path.strip('/')
133 139 os_path = self._get_os_path(path)
134 140 return os.path.isfile(os_path)
135 141
136 142 def exists(self, path):
137 143 """Returns True if the path exists, else returns False.
138 144
139 145 API-style wrapper for os.path.exists
140 146
141 147 Parameters
142 148 ----------
143 149 path : string
144 150 The API path to the file (with '/' as separator)
145 151
146 152 Returns
147 153 -------
148 154 exists : bool
149 155 Whether the target exists.
150 156 """
151 157 path = path.strip('/')
152 158 os_path = self._get_os_path(path=path)
153 159 return os.path.exists(os_path)
154 160
155 161 def _base_model(self, path):
156 162 """Build the common base of a contents model"""
157 163 os_path = self._get_os_path(path)
158 164 info = os.stat(os_path)
159 165 last_modified = tz.utcfromtimestamp(info.st_mtime)
160 166 created = tz.utcfromtimestamp(info.st_ctime)
161 167 # Create the base model.
162 168 model = {}
163 169 model['name'] = path.rsplit('/', 1)[-1]
164 170 model['path'] = path
165 171 model['last_modified'] = last_modified
166 172 model['created'] = created
167 173 model['content'] = None
168 174 model['format'] = None
169 175 return model
170 176
171 177 def _dir_model(self, path, content=True):
172 178 """Build a model for a directory
173 179
174 180 if content is requested, will include a listing of the directory
175 181 """
176 182 os_path = self._get_os_path(path)
177 183
178 184 four_o_four = u'directory does not exist: %r' % os_path
179 185
180 186 if not os.path.isdir(os_path):
181 187 raise web.HTTPError(404, four_o_four)
182 188 elif is_hidden(os_path, self.root_dir):
183 189 self.log.info("Refusing to serve hidden directory %r, via 404 Error",
184 190 os_path
185 191 )
186 192 raise web.HTTPError(404, four_o_four)
187 193
188 194 model = self._base_model(path)
189 195 model['type'] = 'directory'
190 196 if content:
191 197 model['content'] = contents = []
192 198 os_dir = self._get_os_path(path)
193 199 for name in os.listdir(os_dir):
194 200 os_path = os.path.join(os_dir, name)
195 201 # skip over broken symlinks in listing
196 202 if not os.path.exists(os_path):
197 203 self.log.warn("%s doesn't exist", os_path)
198 204 continue
199 205 elif not os.path.isfile(os_path) and not os.path.isdir(os_path):
200 206 self.log.debug("%s not a regular file", os_path)
201 207 continue
202 208 if self.should_list(name) and not is_hidden(os_path, self.root_dir):
203 209 contents.append(self.get(
204 210 path='%s/%s' % (path, name),
205 211 content=False)
206 212 )
207 213
208 214 model['format'] = 'json'
209 215
210 216 return model
211 217
212 218 def _file_model(self, path, content=True, format=None):
213 219 """Build a model for a file
214 220
215 221 if content is requested, include the file contents.
216 222
217 223 format:
218 224 If 'text', the contents will be decoded as UTF-8.
219 225 If 'base64', the raw bytes contents will be encoded as base64.
220 226 If not specified, try to decode as UTF-8, and fall back to base64
221 227 """
222 228 model = self._base_model(path)
223 229 model['type'] = 'file'
224 230 if content:
225 231 os_path = self._get_os_path(path)
226 232 if not os.path.isfile(os_path):
227 233 # could be FIFO
228 234 raise web.HTTPError(400, "Cannot get content of non-file %s" % os_path)
229 235 with io.open(os_path, 'rb') as f:
230 236 bcontent = f.read()
231 237
232 238 if format != 'base64':
233 239 try:
234 240 model['content'] = bcontent.decode('utf8')
235 241 except UnicodeError as e:
236 242 if format == 'text':
237 243 raise web.HTTPError(400, "%s is not UTF-8 encoded" % path)
238 244 else:
239 245 model['format'] = 'text'
240 246
241 247 if model['content'] is None:
242 248 model['content'] = base64.encodestring(bcontent).decode('ascii')
243 249 model['format'] = 'base64'
244 250
245 251 return model
246 252
247 253
248 254 def _notebook_model(self, path, content=True):
249 255 """Build a notebook model
250 256
251 257 if content is requested, the notebook content will be populated
252 258 as a JSON structure (not double-serialized)
253 259 """
254 260 model = self._base_model(path)
255 261 model['type'] = 'notebook'
256 262 if content:
257 263 os_path = self._get_os_path(path)
258 264 with io.open(os_path, 'r', encoding='utf-8') as f:
259 265 try:
260 266 nb = nbformat.read(f, as_version=4)
261 267 except Exception as e:
262 268 raise web.HTTPError(400, u"Unreadable Notebook: %s %r" % (os_path, e))
263 269 self.mark_trusted_cells(nb, path)
264 270 model['content'] = nb
265 271 model['format'] = 'json'
266 272 self.validate_notebook_model(model)
267 273 return model
268 274
269 275 def get(self, path, content=True, type_=None, format=None):
270 276 """ Takes a path for an entity and returns its model
271 277
272 278 Parameters
273 279 ----------
274 280 path : str
275 281 the API path that describes the relative path for the target
276 282 content : bool
277 283 Whether to include the contents in the reply
278 284 type_ : str, optional
279 285 The requested type - 'file', 'notebook', or 'directory'.
280 286 Will raise HTTPError 400 if the content doesn't match.
281 287 format : str, optional
282 288 The requested format for file contents. 'text' or 'base64'.
283 289 Ignored if this returns a notebook or directory model.
284 290
285 291 Returns
286 292 -------
287 293 model : dict
288 294 the contents model. If content=True, returns the contents
289 295 of the file or directory as well.
290 296 """
291 297 path = path.strip('/')
292 298
293 299 if not self.exists(path):
294 300 raise web.HTTPError(404, u'No such file or directory: %s' % path)
295 301
296 302 os_path = self._get_os_path(path)
297 303 if os.path.isdir(os_path):
298 304 if type_ not in (None, 'directory'):
299 305 raise web.HTTPError(400,
300 306 u'%s is a directory, not a %s' % (path, type_))
301 307 model = self._dir_model(path, content=content)
302 308 elif type_ == 'notebook' or (type_ is None and path.endswith('.ipynb')):
303 309 model = self._notebook_model(path, content=content)
304 310 else:
305 311 if type_ == 'directory':
306 312 raise web.HTTPError(400,
307 313 u'%s is not a directory')
308 314 model = self._file_model(path, content=content, format=format)
309 315 return model
310 316
311 317 def _save_notebook(self, os_path, model, path=''):
312 318 """save a notebook file"""
313 319 # Save the notebook file
314 320 nb = nbformat.from_dict(model['content'])
315 321
316 322 self.check_and_sign(nb, path)
317 323
318 324 with atomic_writing(os_path, encoding='utf-8') as f:
319 325 nbformat.write(nb, f, version=nbformat.NO_CONVERT)
320 326
321 327 def _save_file(self, os_path, model, path=''):
322 328 """save a non-notebook file"""
323 329 fmt = model.get('format', None)
324 330 if fmt not in {'text', 'base64'}:
325 331 raise web.HTTPError(400, "Must specify format of file contents as 'text' or 'base64'")
326 332 try:
327 333 content = model['content']
328 334 if fmt == 'text':
329 335 bcontent = content.encode('utf8')
330 336 else:
331 337 b64_bytes = content.encode('ascii')
332 338 bcontent = base64.decodestring(b64_bytes)
333 339 except Exception as e:
334 340 raise web.HTTPError(400, u'Encoding error saving %s: %s' % (os_path, e))
335 341 with atomic_writing(os_path, text=False) as f:
336 342 f.write(bcontent)
337 343
338 344 def _save_directory(self, os_path, model, path=''):
339 345 """create a directory"""
340 346 if is_hidden(os_path, self.root_dir):
341 347 raise web.HTTPError(400, u'Cannot create hidden directory %r' % os_path)
342 348 if not os.path.exists(os_path):
343 349 os.mkdir(os_path)
344 350 elif not os.path.isdir(os_path):
345 351 raise web.HTTPError(400, u'Not a directory: %s' % (os_path))
346 352 else:
347 353 self.log.debug("Directory %r already exists", os_path)
348 354
349 355 def save(self, model, path=''):
350 356 """Save the file model and return the model with no content."""
351 357 path = path.strip('/')
352 358
353 359 if 'type' not in model:
354 360 raise web.HTTPError(400, u'No file type provided')
355 361 if 'content' not in model and model['type'] != 'directory':
356 362 raise web.HTTPError(400, u'No file content provided')
357 363
358 364 # One checkpoint should always exist
359 365 if self.file_exists(path) and not self.list_checkpoints(path):
360 366 self.create_checkpoint(path)
361 367
362 368 os_path = self._get_os_path(path)
363 369 self.log.debug("Saving %s", os_path)
364 370 try:
365 371 if model['type'] == 'notebook':
366 372 self._save_notebook(os_path, model, path)
367 373 elif model['type'] == 'file':
368 374 self._save_file(os_path, model, path)
369 375 elif model['type'] == 'directory':
370 376 self._save_directory(os_path, model, path)
371 377 else:
372 378 raise web.HTTPError(400, "Unhandled contents type: %s" % model['type'])
373 379 except web.HTTPError:
374 380 raise
375 381 except Exception as e:
376 382 raise web.HTTPError(400, u'Unexpected error while saving file: %s %s' % (os_path, e))
377 383
378 384 validation_message = None
379 385 if model['type'] == 'notebook':
380 386 self.validate_notebook_model(model)
381 387 validation_message = model.get('message', None)
382 388
383 389 model = self.get(path, content=False)
384 390 if validation_message:
385 391 model['message'] = validation_message
386 392 return model
387 393
388 394 def update(self, model, path):
389 395 """Update the file's path
390 396
391 397 For use in PATCH requests, to enable renaming a file without
392 398 re-uploading its contents. Only used for renaming at the moment.
393 399 """
394 400 path = path.strip('/')
395 401 new_path = model.get('path', path).strip('/')
396 402 if path != new_path:
397 403 self.rename(path, new_path)
398 404 model = self.get(new_path, content=False)
399 405 return model
400 406
401 407 def delete(self, path):
402 408 """Delete file at path."""
403 409 path = path.strip('/')
404 410 os_path = self._get_os_path(path)
405 411 rm = os.unlink
406 412 if os.path.isdir(os_path):
407 413 listing = os.listdir(os_path)
408 414 # don't delete non-empty directories (checkpoints dir doesn't count)
409 415 if listing and listing != [self.checkpoint_dir]:
410 416 raise web.HTTPError(400, u'Directory %s not empty' % os_path)
411 417 elif not os.path.isfile(os_path):
412 418 raise web.HTTPError(404, u'File does not exist: %s' % os_path)
413 419
414 420 # clear checkpoints
415 421 for checkpoint in self.list_checkpoints(path):
416 422 checkpoint_id = checkpoint['id']
417 423 cp_path = self.get_checkpoint_path(checkpoint_id, path)
418 424 if os.path.isfile(cp_path):
419 425 self.log.debug("Unlinking checkpoint %s", cp_path)
420 426 os.unlink(cp_path)
421 427
422 428 if os.path.isdir(os_path):
423 429 self.log.debug("Removing directory %s", os_path)
424 430 shutil.rmtree(os_path)
425 431 else:
426 432 self.log.debug("Unlinking file %s", os_path)
427 433 rm(os_path)
428 434
429 435 def rename(self, old_path, new_path):
430 436 """Rename a file."""
431 437 old_path = old_path.strip('/')
432 438 new_path = new_path.strip('/')
433 439 if new_path == old_path:
434 440 return
435 441
436 442 new_os_path = self._get_os_path(new_path)
437 443 old_os_path = self._get_os_path(old_path)
438 444
439 445 # Should we proceed with the move?
440 446 if os.path.exists(new_os_path):
441 447 raise web.HTTPError(409, u'File already exists: %s' % new_path)
442 448
443 449 # Move the file
444 450 try:
445 451 shutil.move(old_os_path, new_os_path)
446 452 except Exception as e:
447 453 raise web.HTTPError(500, u'Unknown error renaming file: %s %s' % (old_path, e))
448 454
449 455 # Move the checkpoints
450 456 old_checkpoints = self.list_checkpoints(old_path)
451 457 for cp in old_checkpoints:
452 458 checkpoint_id = cp['id']
453 459 old_cp_path = self.get_checkpoint_path(checkpoint_id, old_path)
454 460 new_cp_path = self.get_checkpoint_path(checkpoint_id, new_path)
455 461 if os.path.isfile(old_cp_path):
456 462 self.log.debug("Renaming checkpoint %s -> %s", old_cp_path, new_cp_path)
457 463 shutil.move(old_cp_path, new_cp_path)
458 464
459 465 # Checkpoint-related utilities
460 466
461 467 def get_checkpoint_path(self, checkpoint_id, path):
462 468 """find the path to a checkpoint"""
463 469 path = path.strip('/')
464 470 parent, name = ('/' + path).rsplit('/', 1)
465 471 parent = parent.strip('/')
466 472 basename, ext = os.path.splitext(name)
467 473 filename = u"{name}-{checkpoint_id}{ext}".format(
468 474 name=basename,
469 475 checkpoint_id=checkpoint_id,
470 476 ext=ext,
471 477 )
472 478 os_path = self._get_os_path(path=parent)
473 479 cp_dir = os.path.join(os_path, self.checkpoint_dir)
474 480 ensure_dir_exists(cp_dir)
475 481 cp_path = os.path.join(cp_dir, filename)
476 482 return cp_path
477 483
478 484 def get_checkpoint_model(self, checkpoint_id, path):
479 485 """construct the info dict for a given checkpoint"""
480 486 path = path.strip('/')
481 487 cp_path = self.get_checkpoint_path(checkpoint_id, path)
482 488 stats = os.stat(cp_path)
483 489 last_modified = tz.utcfromtimestamp(stats.st_mtime)
484 490 info = dict(
485 491 id = checkpoint_id,
486 492 last_modified = last_modified,
487 493 )
488 494 return info
489 495
490 496 # public checkpoint API
491 497
492 498 def create_checkpoint(self, path):
493 499 """Create a checkpoint from the current state of a file"""
494 500 path = path.strip('/')
495 501 if not self.file_exists(path):
496 502 raise web.HTTPError(404)
497 503 src_path = self._get_os_path(path)
498 504 # only the one checkpoint ID:
499 505 checkpoint_id = u"checkpoint"
500 506 cp_path = self.get_checkpoint_path(checkpoint_id, path)
501 507 self.log.debug("creating checkpoint for %s", path)
502 508 self._copy(src_path, cp_path)
503 509
504 510 # return the checkpoint info
505 511 return self.get_checkpoint_model(checkpoint_id, path)
506 512
507 513 def list_checkpoints(self, path):
508 514 """list the checkpoints for a given file
509 515
510 516 This contents manager currently only supports one checkpoint per file.
511 517 """
512 518 path = path.strip('/')
513 519 checkpoint_id = "checkpoint"
514 520 os_path = self.get_checkpoint_path(checkpoint_id, path)
515 521 if not os.path.exists(os_path):
516 522 return []
517 523 else:
518 524 return [self.get_checkpoint_model(checkpoint_id, path)]
519 525
520 526
521 527 def restore_checkpoint(self, checkpoint_id, path):
522 528 """restore a file to a checkpointed state"""
523 529 path = path.strip('/')
524 530 self.log.info("restoring %s from checkpoint %s", path, checkpoint_id)
525 531 nb_path = self._get_os_path(path)
526 532 cp_path = self.get_checkpoint_path(checkpoint_id, path)
527 533 if not os.path.isfile(cp_path):
528 534 self.log.debug("checkpoint file does not exist: %s", cp_path)
529 535 raise web.HTTPError(404,
530 536 u'checkpoint does not exist: %s@%s' % (path, checkpoint_id)
531 537 )
532 538 # ensure notebook is readable (never restore from an unreadable notebook)
533 539 if cp_path.endswith('.ipynb'):
534 540 with io.open(cp_path, 'r', encoding='utf-8') as f:
535 541 nbformat.read(f, as_version=4)
536 542 self._copy(cp_path, nb_path)
537 543 self.log.debug("copying %s -> %s", cp_path, nb_path)
538 544
539 545 def delete_checkpoint(self, checkpoint_id, path):
540 546 """delete a file's checkpoint"""
541 547 path = path.strip('/')
542 548 cp_path = self.get_checkpoint_path(checkpoint_id, path)
543 549 if not os.path.isfile(cp_path):
544 550 raise web.HTTPError(404,
545 551 u'Checkpoint does not exist: %s@%s' % (path, checkpoint_id)
546 552 )
547 553 self.log.debug("unlinking %s", cp_path)
548 554 os.unlink(cp_path)
549 555
550 556 def info_string(self):
551 557 return "Serving notebooks from local directory: %s" % self.root_dir
552 558
553 559 def get_kernel_path(self, path, model=None):
554 560 """Return the initial working dir a kernel associated with a given notebook"""
555 561 if '/' in path:
556 562 parent_dir = path.rsplit('/', 1)[0]
557 563 else:
558 564 parent_dir = ''
559 565 return self._get_os_path(parent_dir)
@@ -1,119 +1,127 b''
1 1 """A MultiKernelManager for use in the notebook webserver
2 2
3 3 - raises HTTPErrors
4 4 - creates REST API models
5 5 """
6 6
7 7 # Copyright (c) IPython Development Team.
8 8 # Distributed under the terms of the Modified BSD License.
9 9
10 10 import os
11 11
12 12 from tornado import web
13 13
14 14 from IPython.kernel.multikernelmanager import MultiKernelManager
15 from IPython.utils.traitlets import Unicode, TraitError
15 from IPython.utils.traitlets import List, Unicode, TraitError
16 16
17 17 from IPython.html.utils import to_os_path
18 18 from IPython.utils.py3compat import getcwd
19 19
20 20
21 21 class MappingKernelManager(MultiKernelManager):
22 22 """A KernelManager that handles notebook mapping and HTTP error handling"""
23 23
24 24 def _kernel_manager_class_default(self):
25 25 return "IPython.kernel.ioloop.IOLoopKernelManager"
26 26
27 root_dir = Unicode(getcwd(), config=True)
27 kernel_argv = List(Unicode)
28
29 root_dir = Unicode(config=True)
30
31 def _root_dir_default(self):
32 try:
33 return self.parent.notebook_dir
34 except AttributeError:
35 return getcwd()
28 36
29 37 def _root_dir_changed(self, name, old, new):
30 38 """Do a bit of validation of the root dir."""
31 39 if not os.path.isabs(new):
32 40 # If we receive a non-absolute path, make it absolute.
33 41 self.root_dir = os.path.abspath(new)
34 42 return
35 43 if not os.path.exists(new) or not os.path.isdir(new):
36 44 raise TraitError("kernel root dir %r is not a directory" % new)
37 45
38 46 #-------------------------------------------------------------------------
39 47 # Methods for managing kernels and sessions
40 48 #-------------------------------------------------------------------------
41 49
42 50 def _handle_kernel_died(self, kernel_id):
43 51 """notice that a kernel died"""
44 52 self.log.warn("Kernel %s died, removing from map.", kernel_id)
45 53 self.remove_kernel(kernel_id)
46 54
47 55 def cwd_for_path(self, path):
48 56 """Turn API path into absolute OS path."""
49 57 # short circuit for NotebookManagers that pass in absolute paths
50 58 if os.path.exists(path):
51 59 return path
52 60
53 61 os_path = to_os_path(path, self.root_dir)
54 62 # in the case of notebooks and kernels not being on the same filesystem,
55 63 # walk up to root_dir if the paths don't exist
56 64 while not os.path.exists(os_path) and os_path != self.root_dir:
57 65 os_path = os.path.dirname(os_path)
58 66 return os_path
59 67
60 68 def start_kernel(self, kernel_id=None, path=None, kernel_name='python', **kwargs):
61 69 """Start a kernel for a session and return its kernel_id.
62 70
63 71 Parameters
64 72 ----------
65 73 kernel_id : uuid
66 74 The uuid to associate the new kernel with. If this
67 75 is not None, this kernel will be persistent whenever it is
68 76 requested.
69 77 path : API path
70 78 The API path (unicode, '/' delimited) for the cwd.
71 79 Will be transformed to an OS path relative to root_dir.
72 80 kernel_name : str
73 81 The name identifying which kernel spec to launch. This is ignored if
74 82 an existing kernel is returned, but it may be checked in the future.
75 83 """
76 84 if kernel_id is None:
77 85 if path is not None:
78 86 kwargs['cwd'] = self.cwd_for_path(path)
79 87 kernel_id = super(MappingKernelManager, self).start_kernel(
80 88 kernel_name=kernel_name, **kwargs)
81 89 self.log.info("Kernel started: %s" % kernel_id)
82 90 self.log.debug("Kernel args: %r" % kwargs)
83 91 # register callback for failed auto-restart
84 92 self.add_restart_callback(kernel_id,
85 93 lambda : self._handle_kernel_died(kernel_id),
86 94 'dead',
87 95 )
88 96 else:
89 97 self._check_kernel_id(kernel_id)
90 98 self.log.info("Using existing kernel: %s" % kernel_id)
91 99 return kernel_id
92 100
93 101 def shutdown_kernel(self, kernel_id, now=False):
94 102 """Shutdown a kernel by kernel_id"""
95 103 self._check_kernel_id(kernel_id)
96 104 super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
97 105
98 106 def kernel_model(self, kernel_id):
99 107 """Return a dictionary of kernel information described in the
100 108 JSON standard model."""
101 109 self._check_kernel_id(kernel_id)
102 110 model = {"id":kernel_id,
103 111 "name": self._kernels[kernel_id].kernel_name}
104 112 return model
105 113
106 114 def list_kernels(self):
107 115 """Returns a list of kernel_id's of kernels running."""
108 116 kernels = []
109 117 kernel_ids = super(MappingKernelManager, self).list_kernel_ids()
110 118 for kernel_id in kernel_ids:
111 119 model = self.kernel_model(kernel_id)
112 120 kernels.append(model)
113 121 return kernels
114 122
115 123 # override _check_kernel_id to raise 404 instead of KeyError
116 124 def _check_kernel_id(self, kernel_id):
117 125 """Check a that a kernel_id exists and raise 404 if not."""
118 126 if kernel_id not in self:
119 127 raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
General Comments 0
You need to be logged in to leave comments. Login now