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