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