Mercurial > cgi-bin > hgweb.cgi > tincan
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 |