##// END OF EJS Templates
Write notebook server info file in security directory
Thomas Kluyver -
Show More
@@ -1,751 +1,810 b''
1 1 # coding: utf-8
2 2 """A tornado based IPython notebook server.
3 3
4 4 Authors:
5 5
6 6 * Brian Granger
7 7 """
8 8 from __future__ import print_function
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2013 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 # stdlib
21 21 import errno
22 import io
23 import json
22 24 import logging
23 25 import os
24 26 import random
25 27 import select
26 28 import signal
27 29 import socket
28 30 import sys
29 31 import threading
30 32 import time
31 33 import webbrowser
32 34
33 35
34 36 # Third party
35 37 # check for pyzmq 2.1.11
36 38 from IPython.utils.zmqrelated import check_for_zmq
37 39 check_for_zmq('2.1.11', 'IPython.html')
38 40
39 41 from jinja2 import Environment, FileSystemLoader
40 42
41 43 # Install the pyzmq ioloop. This has to be done before anything else from
42 44 # tornado is imported.
43 45 from zmq.eventloop import ioloop
44 46 ioloop.install()
45 47
46 48 # check for tornado 3.1.0
47 49 msg = "The IPython Notebook requires tornado >= 3.1.0"
48 50 try:
49 51 import tornado
50 52 except ImportError:
51 53 raise ImportError(msg)
52 54 try:
53 55 version_info = tornado.version_info
54 56 except AttributeError:
55 57 raise ImportError(msg + ", but you have < 1.1.0")
56 58 if version_info < (3,1,0):
57 59 raise ImportError(msg + ", but you have %s" % tornado.version)
58 60
59 61 from tornado import httpserver
60 62 from tornado import web
61 63
62 64 # Our own libraries
63 65 from IPython.html import DEFAULT_STATIC_FILES_PATH
64 66
65 67 from .services.kernels.kernelmanager import MappingKernelManager
66 68 from .services.notebooks.nbmanager import NotebookManager
67 69 from .services.notebooks.filenbmanager import FileNotebookManager
68 70 from .services.clusters.clustermanager import ClusterManager
69 71 from .services.sessions.sessionmanager import SessionManager
70 72
71 73 from .base.handlers import AuthenticatedFileHandler, FileFindHandler
72 74
73 75 from IPython.config.application import catch_config_error, boolean_flag
74 76 from IPython.core.application import BaseIPythonApplication
77 from IPython.core.profiledir import ProfileDir
75 78 from IPython.consoleapp import IPythonConsoleApp
76 79 from IPython.kernel import swallow_argv
77 80 from IPython.kernel.zmq.session import default_secure
78 81 from IPython.kernel.zmq.kernelapp import (
79 82 kernel_flags,
80 83 kernel_aliases,
81 84 )
82 85 from IPython.utils.importstring import import_item
83 86 from IPython.utils.localinterfaces import localhost
84 87 from IPython.utils import submodule
85 88 from IPython.utils.traitlets import (
86 89 Dict, Unicode, Integer, List, Bool, Bytes,
87 90 DottedObjectName
88 91 )
89 92 from IPython.utils import py3compat
90 93 from IPython.utils.path import filefind, get_ipython_dir
91 94
92 95 from .utils import url_path_join
93 96
94 97 #-----------------------------------------------------------------------------
95 98 # Module globals
96 99 #-----------------------------------------------------------------------------
97 100
98 101 _examples = """
99 102 ipython notebook # start the notebook
100 103 ipython notebook --profile=sympy # use the sympy profile
101 104 ipython notebook --certfile=mycert.pem # use SSL/TLS certificate
102 105 """
103 106
104 107 #-----------------------------------------------------------------------------
105 108 # Helper functions
106 109 #-----------------------------------------------------------------------------
107 110
108 111 def random_ports(port, n):
109 112 """Generate a list of n random ports near the given port.
110 113
111 114 The first 5 ports will be sequential, and the remaining n-5 will be
112 115 randomly selected in the range [port-2*n, port+2*n].
113 116 """
114 117 for i in range(min(5, n)):
115 118 yield port + i
116 119 for i in range(n-5):
117 120 yield max(1, port + random.randint(-2*n, 2*n))
118 121
119 122 def load_handlers(name):
120 123 """Load the (URL pattern, handler) tuples for each component."""
121 124 name = 'IPython.html.' + name
122 125 mod = __import__(name, fromlist=['default_handlers'])
123 126 return mod.default_handlers
124 127
125 128 #-----------------------------------------------------------------------------
126 129 # The Tornado web application
127 130 #-----------------------------------------------------------------------------
128 131
129 132 class NotebookWebApplication(web.Application):
130 133
131 134 def __init__(self, ipython_app, kernel_manager, notebook_manager,
132 135 cluster_manager, session_manager, log, base_project_url,
133 136 settings_overrides):
134 137
135 138 settings = self.init_settings(
136 139 ipython_app, kernel_manager, notebook_manager, cluster_manager,
137 140 session_manager, log, base_project_url, settings_overrides)
138 141 handlers = self.init_handlers(settings)
139 142
140 143 super(NotebookWebApplication, self).__init__(handlers, **settings)
141 144
142 145 def init_settings(self, ipython_app, kernel_manager, notebook_manager,
143 146 cluster_manager, session_manager, log, base_project_url,
144 147 settings_overrides):
145 148 # Python < 2.6.5 doesn't accept unicode keys in f(**kwargs), and
146 149 # base_project_url will always be unicode, which will in turn
147 150 # make the patterns unicode, and ultimately result in unicode
148 151 # keys in kwargs to handler._execute(**kwargs) in tornado.
149 152 # This enforces that base_project_url be ascii in that situation.
150 153 #
151 154 # Note that the URLs these patterns check against are escaped,
152 155 # and thus guaranteed to be ASCII: 'hΓ©llo' is really 'h%C3%A9llo'.
153 156 base_project_url = py3compat.unicode_to_str(base_project_url, 'ascii')
154 157 template_path = settings_overrides.get("template_path", os.path.join(os.path.dirname(__file__), "templates"))
155 158 settings = dict(
156 159 # basics
157 160 base_project_url=base_project_url,
158 161 base_kernel_url=ipython_app.base_kernel_url,
159 162 template_path=template_path,
160 163 static_path=ipython_app.static_file_path,
161 164 static_handler_class = FileFindHandler,
162 165 static_url_prefix = url_path_join(base_project_url,'/static/'),
163 166
164 167 # authentication
165 168 cookie_secret=ipython_app.cookie_secret,
166 169 login_url=url_path_join(base_project_url,'/login'),
167 170 password=ipython_app.password,
168 171
169 172 # managers
170 173 kernel_manager=kernel_manager,
171 174 notebook_manager=notebook_manager,
172 175 cluster_manager=cluster_manager,
173 176 session_manager=session_manager,
174 177
175 178 # IPython stuff
176 179 nbextensions_path = ipython_app.nbextensions_path,
177 180 mathjax_url=ipython_app.mathjax_url,
178 181 config=ipython_app.config,
179 182 use_less=ipython_app.use_less,
180 183 jinja2_env=Environment(loader=FileSystemLoader(template_path)),
181 184 )
182 185
183 186 # allow custom overrides for the tornado web app.
184 187 settings.update(settings_overrides)
185 188 return settings
186 189
187 190 def init_handlers(self, settings):
188 191 # Load the (URL pattern, handler) tuples for each component.
189 192 handlers = []
190 193 handlers.extend(load_handlers('base.handlers'))
191 194 handlers.extend(load_handlers('tree.handlers'))
192 195 handlers.extend(load_handlers('auth.login'))
193 196 handlers.extend(load_handlers('auth.logout'))
194 197 handlers.extend(load_handlers('notebook.handlers'))
195 198 handlers.extend(load_handlers('nbconvert.handlers'))
196 199 handlers.extend(load_handlers('services.kernels.handlers'))
197 200 handlers.extend(load_handlers('services.notebooks.handlers'))
198 201 handlers.extend(load_handlers('services.clusters.handlers'))
199 202 handlers.extend(load_handlers('services.sessions.handlers'))
200 203 handlers.extend(load_handlers('services.nbconvert.handlers'))
201 204 handlers.extend([
202 205 (r"/files/(.*)", AuthenticatedFileHandler, {'path' : settings['notebook_manager'].notebook_dir}),
203 206 (r"/nbextensions/(.*)", FileFindHandler, {'path' : settings['nbextensions_path']}),
204 207 ])
205 208 # prepend base_project_url onto the patterns that we match
206 209 new_handlers = []
207 210 for handler in handlers:
208 211 pattern = url_path_join(settings['base_project_url'], handler[0])
209 212 new_handler = tuple([pattern] + list(handler[1:]))
210 213 new_handlers.append(new_handler)
211 214 return new_handlers
212 215
213 216
214 217
215 218 #-----------------------------------------------------------------------------
216 219 # Aliases and Flags
217 220 #-----------------------------------------------------------------------------
218 221
219 222 flags = dict(kernel_flags)
220 223 flags['no-browser']=(
221 224 {'NotebookApp' : {'open_browser' : False}},
222 225 "Don't open the notebook in a browser after startup."
223 226 )
224 227 flags['no-mathjax']=(
225 228 {'NotebookApp' : {'enable_mathjax' : False}},
226 229 """Disable MathJax
227 230
228 231 MathJax is the javascript library IPython uses to render math/LaTeX. It is
229 232 very large, so you may want to disable it if you have a slow internet
230 233 connection, or for offline use of the notebook.
231 234
232 235 When disabled, equations etc. will appear as their untransformed TeX source.
233 236 """
234 237 )
235 238
236 239 # Add notebook manager flags
237 240 flags.update(boolean_flag('script', 'FileNotebookManager.save_script',
238 241 'Auto-save a .py script everytime the .ipynb notebook is saved',
239 242 'Do not auto-save .py scripts for every notebook'))
240 243
241 244 # the flags that are specific to the frontend
242 245 # these must be scrubbed before being passed to the kernel,
243 246 # or it will raise an error on unrecognized flags
244 247 notebook_flags = ['no-browser', 'no-mathjax', 'script', 'no-script']
245 248
246 249 aliases = dict(kernel_aliases)
247 250
248 251 aliases.update({
249 252 'ip': 'NotebookApp.ip',
250 253 'port': 'NotebookApp.port',
251 254 'port-retries': 'NotebookApp.port_retries',
252 255 'transport': 'KernelManager.transport',
253 256 'keyfile': 'NotebookApp.keyfile',
254 257 'certfile': 'NotebookApp.certfile',
255 258 'notebook-dir': 'NotebookManager.notebook_dir',
256 259 'browser': 'NotebookApp.browser',
257 260 })
258 261
259 262 # remove ipkernel flags that are singletons, and don't make sense in
260 263 # multi-kernel evironment:
261 264 aliases.pop('f', None)
262 265
263 266 notebook_aliases = [u'port', u'port-retries', u'ip', u'keyfile', u'certfile',
264 267 u'notebook-dir', u'profile', u'profile-dir']
265 268
266 269 #-----------------------------------------------------------------------------
267 270 # NotebookApp
268 271 #-----------------------------------------------------------------------------
269 272
270 273 class NotebookApp(BaseIPythonApplication):
271 274
272 275 name = 'ipython-notebook'
273 276
274 277 description = """
275 278 The IPython HTML Notebook.
276 279
277 280 This launches a Tornado based HTML Notebook Server that serves up an
278 281 HTML5/Javascript Notebook client.
279 282 """
280 283 examples = _examples
281 284
282 285 classes = IPythonConsoleApp.classes + [MappingKernelManager, NotebookManager,
283 286 FileNotebookManager]
284 287 flags = Dict(flags)
285 288 aliases = Dict(aliases)
286 289
287 290 kernel_argv = List(Unicode)
288 291
289 292 def _log_level_default(self):
290 293 return logging.INFO
291 294
292 295 def _log_format_default(self):
293 296 """override default log format to include time"""
294 297 return u"%(asctime)s.%(msecs).03d [%(name)s]%(highlevel)s %(message)s"
295 298
296 299 # create requested profiles by default, if they don't exist:
297 300 auto_create = Bool(True)
298 301
299 302 # file to be opened in the notebook server
300 303 file_to_run = Unicode('')
301 304
302 305 # Network related information.
303 306
304 307 ip = Unicode(config=True,
305 308 help="The IP address the notebook server will listen on."
306 309 )
307 310 def _ip_default(self):
308 311 return localhost()
309 312
310 313 def _ip_changed(self, name, old, new):
311 314 if new == u'*': self.ip = u''
312 315
313 316 port = Integer(8888, config=True,
314 317 help="The port the notebook server will listen on."
315 318 )
316 319 port_retries = Integer(50, config=True,
317 320 help="The number of additional ports to try if the specified port is not available."
318 321 )
319 322
320 323 certfile = Unicode(u'', config=True,
321 324 help="""The full path to an SSL/TLS certificate file."""
322 325 )
323 326
324 327 keyfile = Unicode(u'', config=True,
325 328 help="""The full path to a private key file for usage with SSL/TLS."""
326 329 )
327 330
328 331 cookie_secret = Bytes(b'', config=True,
329 332 help="""The random bytes used to secure cookies.
330 333 By default this is a new random number every time you start the Notebook.
331 334 Set it to a value in a config file to enable logins to persist across server sessions.
332 335
333 336 Note: Cookie secrets should be kept private, do not share config files with
334 337 cookie_secret stored in plaintext (you can read the value from a file).
335 338 """
336 339 )
337 340 def _cookie_secret_default(self):
338 341 return os.urandom(1024)
339 342
340 343 password = Unicode(u'', config=True,
341 344 help="""Hashed password to use for web authentication.
342 345
343 346 To generate, type in a python/IPython shell:
344 347
345 348 from IPython.lib import passwd; passwd()
346 349
347 350 The string should be of the form type:salt:hashed-password.
348 351 """
349 352 )
350 353
351 354 open_browser = Bool(True, config=True,
352 355 help="""Whether to open in a browser after starting.
353 356 The specific browser used is platform dependent and
354 357 determined by the python standard library `webbrowser`
355 358 module, unless it is overridden using the --browser
356 359 (NotebookApp.browser) configuration option.
357 360 """)
358 361
359 362 browser = Unicode(u'', config=True,
360 363 help="""Specify what command to use to invoke a web
361 364 browser when opening the notebook. If not specified, the
362 365 default browser will be determined by the `webbrowser`
363 366 standard library module, which allows setting of the
364 367 BROWSER environment variable to override it.
365 368 """)
366 369
367 370 use_less = Bool(False, config=True,
368 371 help="""Wether to use Browser Side less-css parsing
369 372 instead of compiled css version in templates that allows
370 373 it. This is mainly convenient when working on the less
371 374 file to avoid a build step, or if user want to overwrite
372 375 some of the less variables without having to recompile
373 376 everything.
374 377
375 378 You will need to install the less.js component in the static directory
376 379 either in the source tree or in your profile folder.
377 380 """)
378 381
379 382 webapp_settings = Dict(config=True,
380 383 help="Supply overrides for the tornado.web.Application that the "
381 384 "IPython notebook uses.")
382 385
383 386 enable_mathjax = Bool(True, config=True,
384 387 help="""Whether to enable MathJax for typesetting math/TeX
385 388
386 389 MathJax is the javascript library IPython uses to render math/LaTeX. It is
387 390 very large, so you may want to disable it if you have a slow internet
388 391 connection, or for offline use of the notebook.
389 392
390 393 When disabled, equations etc. will appear as their untransformed TeX source.
391 394 """
392 395 )
393 396 def _enable_mathjax_changed(self, name, old, new):
394 397 """set mathjax url to empty if mathjax is disabled"""
395 398 if not new:
396 399 self.mathjax_url = u''
397 400
398 401 base_project_url = Unicode('/', config=True,
399 402 help='''The base URL for the notebook server.
400 403
401 404 Leading and trailing slashes can be omitted,
402 405 and will automatically be added.
403 406 ''')
404 407 def _base_project_url_changed(self, name, old, new):
405 408 if not new.startswith('/'):
406 409 self.base_project_url = '/'+new
407 410 elif not new.endswith('/'):
408 411 self.base_project_url = new+'/'
409 412
410 413 base_kernel_url = Unicode('/', config=True,
411 414 help='''The base URL for the kernel server
412 415
413 416 Leading and trailing slashes can be omitted,
414 417 and will automatically be added.
415 418 ''')
416 419 def _base_kernel_url_changed(self, name, old, new):
417 420 if not new.startswith('/'):
418 421 self.base_kernel_url = '/'+new
419 422 elif not new.endswith('/'):
420 423 self.base_kernel_url = new+'/'
421 424
422 425 websocket_url = Unicode("", config=True,
423 426 help="""The base URL for the websocket server,
424 427 if it differs from the HTTP server (hint: it almost certainly doesn't).
425 428
426 429 Should be in the form of an HTTP origin: ws[s]://hostname[:port]
427 430 """
428 431 )
429 432
430 433 extra_static_paths = List(Unicode, config=True,
431 434 help="""Extra paths to search for serving static files.
432 435
433 436 This allows adding javascript/css to be available from the notebook server machine,
434 437 or overriding individual files in the IPython"""
435 438 )
436 439 def _extra_static_paths_default(self):
437 440 return [os.path.join(self.profile_dir.location, 'static')]
438 441
439 442 @property
440 443 def static_file_path(self):
441 444 """return extra paths + the default location"""
442 445 return self.extra_static_paths + [DEFAULT_STATIC_FILES_PATH]
443 446
444 447 nbextensions_path = List(Unicode, config=True,
445 448 help="""paths for Javascript extensions. By default, this is just IPYTHONDIR/nbextensions"""
446 449 )
447 450 def _nbextensions_path_default(self):
448 451 return [os.path.join(get_ipython_dir(), 'nbextensions')]
449 452
450 453 mathjax_url = Unicode("", config=True,
451 454 help="""The url for MathJax.js."""
452 455 )
453 456 def _mathjax_url_default(self):
454 457 if not self.enable_mathjax:
455 458 return u''
456 459 static_url_prefix = self.webapp_settings.get("static_url_prefix",
457 460 url_path_join(self.base_project_url, "static")
458 461 )
459 462
460 463 # try local mathjax, either in nbextensions/mathjax or static/mathjax
461 464 for (url_prefix, search_path) in [
462 465 (url_path_join(self.base_project_url, "nbextensions"), self.nbextensions_path),
463 466 (static_url_prefix, self.static_file_path),
464 467 ]:
465 468 self.log.debug("searching for local mathjax in %s", search_path)
466 469 try:
467 470 mathjax = filefind(os.path.join('mathjax', 'MathJax.js'), search_path)
468 471 except IOError:
469 472 continue
470 473 else:
471 474 url = url_path_join(url_prefix, u"mathjax/MathJax.js")
472 475 self.log.info("Serving local MathJax from %s at %s", mathjax, url)
473 476 return url
474 477
475 478 # no local mathjax, serve from CDN
476 479 if self.certfile:
477 480 # HTTPS: load from Rackspace CDN, because SSL certificate requires it
478 481 host = u"https://c328740.ssl.cf1.rackcdn.com"
479 482 else:
480 483 host = u"http://cdn.mathjax.org"
481 484
482 485 url = host + u"/mathjax/latest/MathJax.js"
483 486 self.log.info("Using MathJax from CDN: %s", url)
484 487 return url
485 488
486 489 def _mathjax_url_changed(self, name, old, new):
487 490 if new and not self.enable_mathjax:
488 491 # enable_mathjax=False overrides mathjax_url
489 492 self.mathjax_url = u''
490 493 else:
491 494 self.log.info("Using MathJax: %s", new)
492 495
493 496 notebook_manager_class = DottedObjectName('IPython.html.services.notebooks.filenbmanager.FileNotebookManager',
494 497 config=True,
495 498 help='The notebook manager class to use.')
496 499
497 500 trust_xheaders = Bool(False, config=True,
498 501 help=("Whether to trust or not X-Scheme/X-Forwarded-Proto and X-Real-Ip/X-Forwarded-For headers"
499 502 "sent by the upstream reverse proxy. Neccesary if the proxy handles SSL")
500 503 )
501 504
505 info_file = Unicode()
506
507 def _info_file_default(self):
508 info_file = "nbserver-%s.json"%os.getpid()
509 return os.path.join(self.profile_dir.security_dir, info_file)
510
502 511 def parse_command_line(self, argv=None):
503 512 super(NotebookApp, self).parse_command_line(argv)
504 513
505 514 if self.extra_args:
506 515 arg0 = self.extra_args[0]
507 516 f = os.path.abspath(arg0)
508 517 self.argv.remove(arg0)
509 518 if not os.path.exists(f):
510 519 self.log.critical("No such file or directory: %s", f)
511 520 self.exit(1)
512 521 if os.path.isdir(f):
513 522 self.config.FileNotebookManager.notebook_dir = f
514 523 elif os.path.isfile(f):
515 524 self.file_to_run = f
516 525
517 526 def init_kernel_argv(self):
518 527 """construct the kernel arguments"""
519 528 # Scrub frontend-specific flags
520 529 self.kernel_argv = swallow_argv(self.argv, notebook_aliases, notebook_flags)
521 530 # Kernel should inherit default config file from frontend
522 531 self.kernel_argv.append("--IPKernelApp.parent_appname='%s'" % self.name)
523 532 # Kernel should get *absolute* path to profile directory
524 533 self.kernel_argv.extend(["--profile-dir", self.profile_dir.location])
525 534
526 535 def init_configurables(self):
527 536 # force Session default to be secure
528 537 default_secure(self.config)
529 538 self.kernel_manager = MappingKernelManager(
530 539 parent=self, log=self.log, kernel_argv=self.kernel_argv,
531 540 connection_dir = self.profile_dir.security_dir,
532 541 )
533 542 kls = import_item(self.notebook_manager_class)
534 543 self.notebook_manager = kls(parent=self, log=self.log)
535 544 self.session_manager = SessionManager(parent=self, log=self.log)
536 545 self.cluster_manager = ClusterManager(parent=self, log=self.log)
537 546 self.cluster_manager.update_profiles()
538 547
539 548 def init_logging(self):
540 549 # This prevents double log messages because tornado use a root logger that
541 550 # self.log is a child of. The logging module dipatches log messages to a log
542 551 # and all of its ancenstors until propagate is set to False.
543 552 self.log.propagate = False
544 553
545 554 # hook up tornado 3's loggers to our app handlers
546 555 for name in ('access', 'application', 'general'):
547 556 logger = logging.getLogger('tornado.%s' % name)
548 557 logger.parent = self.log
549 558 logger.setLevel(self.log.level)
550 559
551 560 def init_webapp(self):
552 561 """initialize tornado webapp and httpserver"""
553 562 self.web_app = NotebookWebApplication(
554 563 self, self.kernel_manager, self.notebook_manager,
555 564 self.cluster_manager, self.session_manager,
556 565 self.log, self.base_project_url, self.webapp_settings
557 566 )
558 567 if self.certfile:
559 568 ssl_options = dict(certfile=self.certfile)
560 569 if self.keyfile:
561 570 ssl_options['keyfile'] = self.keyfile
562 571 else:
563 572 ssl_options = None
564 573 self.web_app.password = self.password
565 574 self.http_server = httpserver.HTTPServer(self.web_app, ssl_options=ssl_options,
566 575 xheaders=self.trust_xheaders)
567 576 if not self.ip:
568 577 warning = "WARNING: The notebook server is listening on all IP addresses"
569 578 if ssl_options is None:
570 579 self.log.critical(warning + " and not using encryption. This "
571 580 "is not recommended.")
572 581 if not self.password:
573 582 self.log.critical(warning + " and not using authentication. "
574 583 "This is highly insecure and not recommended.")
575 584 success = None
576 585 for port in random_ports(self.port, self.port_retries+1):
577 586 try:
578 587 self.http_server.listen(port, self.ip)
579 588 except socket.error as e:
580 589 if e.errno == errno.EADDRINUSE:
581 590 self.log.info('The port %i is already in use, trying another random port.' % port)
582 591 continue
583 592 elif e.errno in (errno.EACCES, getattr(errno, 'WSAEACCES', errno.EACCES)):
584 593 self.log.warn("Permission to listen on port %i denied" % port)
585 594 continue
586 595 else:
587 596 raise
588 597 else:
589 598 self.port = port
590 599 success = True
591 600 break
592 601 if not success:
593 602 self.log.critical('ERROR: the notebook server could not be started because '
594 603 'no available port could be found.')
595 604 self.exit(1)
596 605
606 @property
607 def display_url(self):
608 ip = self.ip if self.ip else '[all ip addresses on your system]'
609 return self._url(ip)
610
611 @property
612 def connection_url(self):
613 ip = self.ip if self.ip else localhost()
614 return self._url(ip)
615
616 def _url(self, ip):
617 proto = 'https' if self.certfile else 'http'
618 return "%s://%s:%i%s" % (proto, ip, self.port, self.base_project_url)
619
597 620 def init_signal(self):
598 621 if not sys.platform.startswith('win'):
599 622 signal.signal(signal.SIGINT, self._handle_sigint)
600 623 signal.signal(signal.SIGTERM, self._signal_stop)
601 624 if hasattr(signal, 'SIGUSR1'):
602 625 # Windows doesn't support SIGUSR1
603 626 signal.signal(signal.SIGUSR1, self._signal_info)
604 627 if hasattr(signal, 'SIGINFO'):
605 628 # only on BSD-based systems
606 629 signal.signal(signal.SIGINFO, self._signal_info)
607 630
608 631 def _handle_sigint(self, sig, frame):
609 632 """SIGINT handler spawns confirmation dialog"""
610 633 # register more forceful signal handler for ^C^C case
611 634 signal.signal(signal.SIGINT, self._signal_stop)
612 635 # request confirmation dialog in bg thread, to avoid
613 636 # blocking the App
614 637 thread = threading.Thread(target=self._confirm_exit)
615 638 thread.daemon = True
616 639 thread.start()
617 640
618 641 def _restore_sigint_handler(self):
619 642 """callback for restoring original SIGINT handler"""
620 643 signal.signal(signal.SIGINT, self._handle_sigint)
621 644
622 645 def _confirm_exit(self):
623 646 """confirm shutdown on ^C
624 647
625 648 A second ^C, or answering 'y' within 5s will cause shutdown,
626 649 otherwise original SIGINT handler will be restored.
627 650
628 651 This doesn't work on Windows.
629 652 """
630 653 # FIXME: remove this delay when pyzmq dependency is >= 2.1.11
631 654 time.sleep(0.1)
632 655 info = self.log.info
633 656 info('interrupted')
634 657 print(self.notebook_info())
635 658 sys.stdout.write("Shutdown this notebook server (y/[n])? ")
636 659 sys.stdout.flush()
637 660 r,w,x = select.select([sys.stdin], [], [], 5)
638 661 if r:
639 662 line = sys.stdin.readline()
640 663 if line.lower().startswith('y'):
641 664 self.log.critical("Shutdown confirmed")
642 665 ioloop.IOLoop.instance().stop()
643 666 return
644 667 else:
645 668 print("No answer for 5s:", end=' ')
646 669 print("resuming operation...")
647 670 # no answer, or answer is no:
648 671 # set it back to original SIGINT handler
649 672 # use IOLoop.add_callback because signal.signal must be called
650 673 # from main thread
651 674 ioloop.IOLoop.instance().add_callback(self._restore_sigint_handler)
652 675
653 676 def _signal_stop(self, sig, frame):
654 677 self.log.critical("received signal %s, stopping", sig)
655 678 ioloop.IOLoop.instance().stop()
656 679
657 680 def _signal_info(self, sig, frame):
658 681 print(self.notebook_info())
659 682
660 683 def init_components(self):
661 684 """Check the components submodule, and warn if it's unclean"""
662 685 status = submodule.check_submodule_status()
663 686 if status == 'missing':
664 687 self.log.warn("components submodule missing, running `git submodule update`")
665 688 submodule.update_submodules(submodule.ipython_parent())
666 689 elif status == 'unclean':
667 690 self.log.warn("components submodule unclean, you may see 404s on static/components")
668 691 self.log.warn("run `setup.py submodule` or `git submodule update` to update")
669
670 692
671 693 @catch_config_error
672 694 def initialize(self, argv=None):
673 695 super(NotebookApp, self).initialize(argv)
674 696 self.init_logging()
675 697 self.init_kernel_argv()
676 698 self.init_configurables()
677 699 self.init_components()
678 700 self.init_webapp()
679 701 self.init_signal()
680 702
681 703 def cleanup_kernels(self):
682 704 """Shutdown all kernels.
683 705
684 706 The kernels will shutdown themselves when this process no longer exists,
685 707 but explicit shutdown allows the KernelManagers to cleanup the connection files.
686 708 """
687 709 self.log.info('Shutting down kernels')
688 710 self.kernel_manager.shutdown_all()
689 711
690 712 def notebook_info(self):
691 713 "Return the current working directory and the server url information"
692 714 info = self.notebook_manager.info_string() + "\n"
693 715 info += "%d active kernels \n" % len(self.kernel_manager._kernels)
694 return info + "The IPython Notebook is running at: %s" % self._url
716 return info + "The IPython Notebook is running at: %s" % self.display_url
717
718 def server_info(self):
719 """Return a JSONable dict of information about this server."""
720 return {'url': self.connection_url,
721 'hostname': self.ip if self.ip else 'localhost',
722 'port': self.port,
723 'secure': bool(self.certfile),
724 'baseurlpath': self.base_project_url,
725 'notebookdir': os.path.abspath(self.notebook_manager.notebook_dir),
726 }
727
728 def write_server_info_file(self):
729 """Write the result of server_info() to the JSON file info_file."""
730 with io.open(self.info_file, 'w', encoding='utf-8') as f:
731 json.dump(self.server_info(), f)
732
733 def remove_server_info_file(self):
734 """Remove the nbserver-<pid>.json file created for this server.
735
736 Ignores the error raised when the file has already been removed.
737 """
738 try:
739 os.unlink(self.info_file)
740 except OSError as e:
741 if e.errno != errno.ENOENT:
742 raise
695 743
696 744 def start(self):
697 745 """ Start the IPython Notebook server app, after initialization
698 746
699 747 This method takes no arguments so all configuration and initialization
700 748 must be done prior to calling this method."""
701 ip = self.ip if self.ip else '[all ip addresses on your system]'
702 proto = 'https' if self.certfile else 'http'
703 749 info = self.log.info
704 self._url = "%s://%s:%i%s" % (proto, ip, self.port,
705 self.base_project_url)
706 750 for line in self.notebook_info().split("\n"):
707 751 info(line)
708 752 info("Use Control-C to stop this server and shut down all kernels (twice to skip confirmation).")
709 753
754 self.write_server_info_file()
755
710 756 if self.open_browser or self.file_to_run:
711 ip = self.ip or localhost()
712 757 try:
713 758 browser = webbrowser.get(self.browser or None)
714 759 except webbrowser.Error as e:
715 760 self.log.warn('No web browser found: %s.' % e)
716 761 browser = None
717 762
718 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
719 763 f = self.file_to_run
720 764 if f:
765 nbdir = os.path.abspath(self.notebook_manager.notebook_dir)
721 766 if f.startswith(nbdir):
722 767 f = f[len(nbdir):]
723 768 else:
724 769 self.log.warn(
725 770 "Probably won't be able to open notebook %s "
726 771 "because it is not in notebook_dir %s",
727 772 f, nbdir,
728 773 )
729 774
730 775 if os.path.isfile(self.file_to_run):
731 776 url = url_path_join('notebooks', f)
732 777 else:
733 778 url = url_path_join('tree', f)
734 779 if browser:
735 b = lambda : browser.open("%s://%s:%i%s%s" % (proto, ip,
736 self.port, self.base_project_url, url), new=2)
780 b = lambda : browser.open("%s%s" % (self.connection_url, url),
781 new=2)
737 782 threading.Thread(target=b).start()
738 783 try:
739 784 ioloop.IOLoop.instance().start()
740 785 except KeyboardInterrupt:
741 786 info("Interrupted...")
742 787 finally:
743 788 self.cleanup_kernels()
789 self.remove_server_info_file()
790
791
792 def discover_running_servers(profile='default'):
793 """Iterate over the server info files of running notebook servers.
744 794
795 Given a profile name, find nbserver-* files in the security directory of
796 that profile, and yield dicts of their information, each one pertaining to
797 a currently running notebook server instance.
798 """
799 pd = ProfileDir.find_profile_dir_by_name(get_ipython_dir(), name=profile)
800 for file in os.listdir(pd.security_dir):
801 if file.startswith('nbserver-'):
802 with io.open(os.path.join(pd.security_dir, file), encoding='utf-8') as f:
803 yield json.load(f)
745 804
746 805 #-----------------------------------------------------------------------------
747 806 # Main entry point
748 807 #-----------------------------------------------------------------------------
749 808
750 809 launch_new_instance = NotebookApp.launch_instance
751 810
General Comments 0
You need to be logged in to leave comments. Login now