comparison tincan.py @ 45:969f515b505b draft

Make it easy to leave stdout and stderr alone (untested).
author David Barts <n5jrn@me.com>
date Thu, 30 May 2019 14:44:09 -0700
parents 7261459351fa
children 997d0c8c174f
comparison
equal deleted inserted replaced
44:7261459351fa 45:969f515b505b
11 import email.utils 11 import email.utils
12 import functools 12 import functools
13 import importlib 13 import importlib
14 from inspect import isclass 14 from inspect import isclass
15 import io 15 import io
16 import logging
16 import mimetypes 17 import mimetypes
17 import py_compile 18 import py_compile
18 from stat import S_ISDIR, S_ISREG 19 from stat import S_ISDIR, S_ISREG
19 from string import whitespace 20 from string import whitespace
20 from threading import Lock 21 from threading import Lock
21 import time 22 import time
22 import traceback
23 import urllib 23 import urllib
24 import uuid
24 25
25 import bottle 26 import bottle
26 27
27 # E x c e p t i o n s 28 # E x c e p t i o n s
28 29
396 self.lock = Lock() 397 self.lock = Lock()
397 self.get_template = _get_template_nocache 398 self.get_template = _get_template_nocache
398 else: 399 else:
399 self.lock = _DummyLock() 400 self.lock = _DummyLock()
400 self.get_template = _get_template_cache 401 self.get_template = _get_template_cache
402 self.logger = launcher.logger
401 403
402 def urljoin(self, *args): 404 def urljoin(self, *args):
403 """ 405 """
404 Normalize a parsed-out URL fragment. 406 Normalize a parsed-out URL fragment.
405 """ 407 """
435 self._type += "; charset=" + launcher.encoding 437 self._type += "; charset=" + launcher.encoding
436 else: 438 else:
437 self._encoding = None 439 self._encoding = None
438 440
439 def launch(self): 441 def launch(self):
440 # print("adding static route:", self._urlpath) # debug 442 self.logger.info("adding static route: %s", self._urlpath)
441 self._app.route(self._urlpath, 'GET', self) 443 self._app.route(self._urlpath, 'GET', self)
442 444
443 def _parse_date(self, ims): 445 def _parse_date(self, ims):
444 """ 446 """
445 Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. 447 Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch.
460 except FileNotFoundError as e: 462 except FileNotFoundError as e:
461 return bottle.HTTPError(status=404, exception=e) 463 return bottle.HTTPError(status=404, exception=e)
462 except PermissionError as e: 464 except PermissionError as e:
463 return bottle.HTTPError(status=403, exception=e) 465 return bottle.HTTPError(status=403, exception=e)
464 except OSError as e: 466 except OSError as e:
467 self.logger.exception("unexpected exception reading %r", self._fspath)
465 return bottle.HTTPError(status=500, exception=e) 468 return bottle.HTTPError(status=500, exception=e)
466 # Establish preliminary standard headers. 469 # Establish preliminary standard headers.
467 headers = { 470 headers = {
468 "Content-Type": self._type, 471 "Content-Type": self._type,
469 "Last-Modified": time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(mtime)), 472 "Last-Modified": time.strftime("%a, %d %b %Y %H:%M:%S GMT", time.gmtime(mtime)),
486 A route to an error page. These don't get routes created for them, 489 A route to an error page. These don't get routes created for them,
487 and are only reached if an error routes them there. Unless you create 490 and are only reached if an error routes them there. Unless you create
488 custom code-behind, only two variables are available to your template: 491 custom code-behind, only two variables are available to your template:
489 request (bottle.Request) and error (bottle.HTTPError). 492 request (bottle.Request) and error (bottle.HTTPError).
490 """ 493 """
491 def __init__(self, template, loads, klass, lock): 494 def __init__(self, template, loads, klass, lock, logger):
492 self._template = template 495 self._template = template
493 self._template.prepare() 496 self._template.prepare()
494 self._loads = loads 497 self._loads = loads
495 self._class = klass 498 self._class = klass
496 self.lock = lock 499 self.lock = lock
500 self.logger = logger
497 501
498 def __call__(self, e): 502 def __call__(self, e):
499 bottle.request.environ[_FTYPE] = True 503 bottle.request.environ[_FTYPE] = True
500 try: 504 try:
501 obj = self._class(bottle.request, e) 505 obj = self._class(bottle.request, e)
505 with self.lock: 509 with self.lock:
506 return self._template.render(tvars).lstrip('\n') 510 return self._template.render(tvars).lstrip('\n')
507 except bottle.HTTPResponse as e: 511 except bottle.HTTPResponse as e:
508 return e 512 return e
509 except Exception as e: 513 except Exception as e:
510 traceback.print_exc() 514 self.logger.exception("unexpected exception in error page")
511 # Bottle doesn't allow error handlers to themselves cause 515 # Bottle doesn't allow error handlers to themselves cause
512 # errors, most likely as a measure to prevent looping. So 516 # errors, most likely as a measure to prevent looping. So
513 # this will cause a "Critical error while processing request" 517 # this will cause a "Critical error while processing request"
514 # page to be displayed, and any installed error pages to be 518 # page to be displayed, and any installed error pages to be
515 # ignored. 519 # ignored.
556 oheader = self._header # save original header 560 oheader = self._header # save original header
557 elif (oheader.errors is None) != (self._header.errors is None): 561 elif (oheader.errors is None) != (self._header.errors is None):
558 raise TinCanError("{0}: invalid #forward".format(self._origin)) 562 raise TinCanError("{0}: invalid #forward".format(self._origin))
559 if self._header.forward is None: 563 if self._header.forward is None:
560 break 564 break
561 # print("forwarding from:", self._urlpath) # debug 565 # self.logger.debug("forwarding from: %s", self._urlpath) # debug
562 self._redirect() 566 self._redirect()
563 # print("forwarded to:", self._urlpath) # debug 567 # self.logger.debug("forwarded to: %s", self._urlpath) # debug
564 # If this is a #hidden page, we ignore it for now, since hidden pages 568 # If this is a #hidden page, we ignore it for now, since hidden pages
565 # don't get routes made for them. 569 # don't get routes made for them.
566 if oheader.hidden and not oheader.errors: 570 if oheader.hidden and not oheader.errors:
567 return 571 return
568 # Get the code-behind #python 572 # Get the code-behind #python
617 else: 621 else:
618 methods = [ i.upper() for i in self._header.methods.split() ] 622 methods = [ i.upper() for i in self._header.methods.split() ]
619 if not methods: 623 if not methods:
620 raise TinCanError("{0}: no #methods specified".format(self._urlpath)) 624 raise TinCanError("{0}: no #methods specified".format(self._urlpath))
621 # Register this thing with Bottle 625 # Register this thing with Bottle
622 # print("adding route:", self._origin, '('+','.join(methods)+')') # debug 626 self.logger.info("adding route: %s (%s)", self._origin, ','.join(methods))
623 self._app.route(self._origin, methods, self) 627 self._app.route(self._origin, methods, self)
624 628
625 def _splitpath(self, unsplit): 629 def _splitpath(self, unsplit):
626 return _normpath(self._subdir, unsplit) 630 return _normpath(self._subdir, unsplit)
627 631
632 raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e 636 raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e
633 if not errors: 637 if not errors:
634 errors = range(_ERRMIN, _ERRMAX+1) 638 errors = range(_ERRMIN, _ERRMAX+1)
635 route = _TinCanErrorRoute( 639 route = _TinCanErrorRoute(
636 ChameleonTemplate(source=self._template.body, encoding=self._encoding), 640 ChameleonTemplate(source=self._template.body, encoding=self._encoding),
637 self._loads, self._class, self.lock) 641 self._loads, self._class, self.lock, self.logger)
638 for error in errors: 642 for error in errors:
639 if error < _ERRMIN or error > _ERRMAX: 643 if error < _ERRMIN or error > _ERRMAX:
640 raise TinCanError("{0}: bad #errors code".format(self._urlpath)) 644 raise TinCanError("{0}: bad #errors code".format(self._urlpath))
641 self._app.error_handler[error] = route # XXX 645 self._app.error_handler[error] = route # XXX
642 646
748 except ForwardException as fwd: 752 except ForwardException as fwd:
749 target = fwd.target 753 target = fwd.target
750 except bottle.HTTPResponse as e: 754 except bottle.HTTPResponse as e:
751 return e 755 return e
752 except Exception as e: 756 except Exception as e:
753 traceback.print_exc() 757 self.logger.exception("%s: unexpected exception", self._urlpath)
754 raise bottle.HTTPError(status=500, exception=e) 758 raise bottle.HTTPError(status=500, exception=e)
755 if target is None: 759 if target is None:
756 message = "{0}: unexpected null target".format(self._urlpath) 760 self.logger.error("%s: unexpected null target", self._urlpath)
757 sys.stderr.write(message + '\n')
758 raise bottle.HTTPError(status=500, exception=TinCanError(message)) 761 raise bottle.HTTPError(status=500, exception=TinCanError(message))
759 # We get here if we are doing a server-side programmatic 762 # We get here if we are doing a server-side programmatic
760 # forward. 763 # forward.
761 environ = bottle.request.environ 764 environ = bottle.request.environ
762 if _FORIG not in environ: 765 if _FORIG not in environ:
763 environ[_FORIG] = self._urlpath 766 environ[_FORIG] = self._urlpath
764 if _FLOOP not in environ: 767 if _FLOOP not in environ:
765 environ[_FLOOP] = set([self._urlpath]) 768 environ[_FLOOP] = set([self._urlpath])
766 elif target in environ[_FLOOP]: 769 elif target in environ[_FLOOP]:
767 message = "{0}: forward loop detected".format(environ[_FORIG]) 770 self.logger.error("%s: forward loop detected", environ[_FORIG])
768 sys.stderr.write(message + '\n')
769 raise bottle.HTTPError(status=500, exception=TinCanError(message)) 771 raise bottle.HTTPError(status=500, exception=TinCanError(message))
770 environ[_FLOOP].add(target) 772 environ[_FLOOP].add(target)
771 environ['bottle.raw_path'] = target 773 environ['bottle.raw_path'] = target
772 environ['PATH_INFO'] = urllib.parse.quote(target) 774 environ['PATH_INFO'] = urllib.parse.quote(target)
773 route, args = self._app.router.match(environ) 775 route, args = self._app.router.match(environ)
803 805
804 class _Launcher(object): 806 class _Launcher(object):
805 """ 807 """
806 Helper class for launching webapps. 808 Helper class for launching webapps.
807 """ 809 """
808 def __init__(self, fsroot, urlroot, logger, multithread=True): 810 def __init__(self, fsroot, urlroot, multithread):
809 """ 811 """
810 Lightweight constructor. The real action happens in .launch() below. 812 Lightweight constructor. The real action happens in .launch() below.
811 """ 813 """
812 self.fsroot = fsroot 814 self.fsroot = fsroot
813 self.urlroot = urlroot 815 self.urlroot = urlroot
814 self.logger = logger
815 self.app = None 816 self.app = None
816 self.errors = 0 817 self.errors = 0
817 self.debug = False 818 self.debug = False
818 self.encoding = ENCODING 819 self.encoding = ENCODING
819 self.static = False 820 self.static = False
820 self.multithread = multithread 821 self.multithread = multithread
822 self.logger = None
821 823
822 def launch(self): 824 def launch(self):
823 """ 825 """
824 Does the actual work of launching something. XXX - modifies sys.path 826 Does the actual work of launching something. XXX - modifies sys.path
825 and never un-modifies it. 827 and never un-modifies it.
871 else: 873 else:
872 continue 874 continue
873 try: 875 try:
874 route.launch() 876 route.launch()
875 except TinCanError as e: 877 except TinCanError as e:
876 self.logger(str(e)) 878 if self.logger.getEffectiveLevel() <= logging.DEBUG:
877 if self.debug: 879 self.logger.exception(str(e))
878 while e.__cause__ != None: 880 else:
879 e = e.__cause__ 881 self.logger.error(str(e))
880 self.logger("\t{0}: {1!s}".format(e.__class__.__name__, e))
881 self.errors += 1 882 self.errors += 1
882 elif S_ISDIR(etype): 883 elif S_ISDIR(etype):
883 self._launch(subdir + [entry]) 884 self._launch(subdir + [entry])
884 885
885 def _logger(message): 886 def launch(fsroot=None, urlroot='/', logger=None, debug=False,
886 sys.stderr.write(message)
887 sys.stderr.write('\n')
888
889 def launch(fsroot=None, urlroot='/', logger=_logger, debug=False,
890 encoding=ENCODING, static=False, multithread=True): 887 encoding=ENCODING, static=False, multithread=True):
891 """ 888 """
892 Launch and return a TinCan webapp. Does not run the app; it is the 889 Launch and return a TinCan webapp. Does not run the app; it is the
893 caller's responsibility to call app.run() 890 caller's responsibility to call app.run()
894 """ 891 """
892 if logger is None:
893 logger = logging.getLogger("{0!s}-{1!s}".format(__name__, uuid.uuid1()))
894 logger.addHandler(logging.StreamHandler())
895 logger.setLevel(logging.DEBUG if debug else logging.INFO)
895 if fsroot is None: 896 if fsroot is None:
896 fsroot = os.getcwd() 897 fsroot = os.getcwd()
897 launcher = _Launcher(fsroot, urlroot, logger, multithread=multithread) 898 launcher = _Launcher(fsroot, urlroot, multithread)
898 launcher.debug = debug 899 launcher.logger = logger
899 launcher.encoding = encoding 900 launcher.encoding = encoding
900 launcher.static = static 901 launcher.static = static
901 launcher.launch() 902 launcher.launch()
902 return launcher.app, launcher.errors 903 return launcher.app, launcher.errors
903 904