Mercurial > cgi-bin > hgweb.cgi > tincan
comparison tincan.py @ 43:0bd9b9ae9998 draft
Make it thread-safe.
author | David Barts <n5jrn@me.com> |
---|---|
date | Thu, 30 May 2019 08:59:35 -0700 |
parents | 8948020c54fd |
children | 7261459351fa |
comparison
equal
deleted
inserted
replaced
42:8948020c54fd | 43:0bd9b9ae9998 |
---|---|
15 import io | 15 import io |
16 import mimetypes | 16 import mimetypes |
17 import py_compile | 17 import py_compile |
18 from stat import S_ISDIR, S_ISREG | 18 from stat import S_ISDIR, S_ISREG |
19 from string import whitespace | 19 from string import whitespace |
20 from threading import Lock | |
20 import time | 21 import time |
21 import traceback | 22 import traceback |
22 import urllib | 23 import urllib |
23 | 24 |
24 import bottle | 25 import bottle |
354 raise ValueError("empty file name") | 355 raise ValueError("empty file name") |
355 if not self.fname.endswith(_IEXTEN): | 356 if not self.fname.endswith(_IEXTEN): |
356 raise ValueError("file does not end in {0}".format(_IEXTEN)) | 357 raise ValueError("file does not end in {0}".format(_IEXTEN)) |
357 | 358 |
358 # Using a cache is likely to help efficiency a lot, since many pages | 359 # Using a cache is likely to help efficiency a lot, since many pages |
359 # will typically #load the same standard stuff. | 360 # will typically #load the same standard stuff. Except if we're |
361 # multithreading, then we want each page's templates to be private. | |
360 _tcache = {} | 362 _tcache = {} |
361 def _get_template(name, direct, coding): | 363 def _get_template_cache(name, direct, coding): |
362 aname = os.path.abspath(os.path.join(direct, name)) | 364 aname = os.path.abspath(os.path.join(direct, name)) |
363 if aname not in _tcache: | 365 if aname not in _tcache: |
364 tmpl = ChameleonTemplate(name=name, lookup=[direct], encoding=coding) | 366 tmpl = ChameleonTemplate(name=name, lookup=[direct], encoding=coding) |
365 tmpl.prepare() | |
366 assert aname == tmpl.filename | 367 assert aname == tmpl.filename |
367 _tcache[aname] = tmpl | 368 _tcache[aname] = tmpl |
368 return _tcache[aname] | 369 return _tcache[aname] |
370 | |
371 def _get_template_nocache(name, direct, coding): | |
372 return ChameleonTemplate(name=name, lookup=[direct], encoding=coding) | |
369 | 373 |
370 # R o u t e s | 374 # R o u t e s |
371 # | 375 # |
372 # Represents a route in TinCan. Our launcher creates these on-the-fly based | 376 # Represents a route in TinCan. Our launcher creates these on-the-fly based |
373 # on the files it finds. | 377 # on the files it finds. |
379 _TEXTEN = ".pspx" | 383 _TEXTEN = ".pspx" |
380 _FLOOP = "tincan.forwards" | 384 _FLOOP = "tincan.forwards" |
381 _FORIG = "tincan.origin" | 385 _FORIG = "tincan.origin" |
382 _FTYPE = "tincan.iserror" | 386 _FTYPE = "tincan.iserror" |
383 | 387 |
384 class _TinCanStaticRoute(object): | 388 class _TinCanBaseRoute(object): |
389 """ | |
390 The base class for all NON ERROR routes. Error routes are just a little | |
391 bit different. | |
392 """ | |
393 def __init__(self, launcher, name, subdir): | |
394 global _get_template_cache, _get_template_nocache | |
395 if launcher.multithread: | |
396 self.lock = Lock() | |
397 self.get_template = _get_template_nocache | |
398 else: | |
399 self.lock = _DummyLock() | |
400 self.get_template = _get_template_cache | |
401 | |
402 def urljoin(self, *args): | |
403 """ | |
404 Normalize a parsed-out URL fragment. | |
405 """ | |
406 args = list(args) | |
407 if args[0] == '/': | |
408 args[0] = '' | |
409 return '/'.join(args) | |
410 | |
411 def launch(self): | |
412 raise NotImplementedError("This must be overridden.") | |
413 | |
414 def __call__(self): | |
415 raise NotImplementedError("This must be overridden.") | |
416 | |
417 class _TinCanStaticRoute(_TinCanBaseRoute): | |
385 """ | 418 """ |
386 A route to a static file. These are useful for test servers. For | 419 A route to a static file. These are useful for test servers. For |
387 production servers, one is better off using a real web server and | 420 production servers, one is better off using a real web server and |
388 a WSGI plugin, and having that handle static files. Much of this | 421 a WSGI plugin, and having that handle static files. Much of this |
389 logic is cribbed from the Bottle source code (we don't call it | 422 logic is cribbed from the Bottle source code (we don't call it |
390 directly because it is undocumented and thus subject to change). | 423 directly because it is undocumented and thus subject to change). |
391 """ | 424 """ |
392 def __init__(self, launcher, name, subdir): | 425 def __init__(self, launcher, name, subdir): |
426 super().__init__(launcher, name, subdir) | |
393 self._app = launcher.app | 427 self._app = launcher.app |
394 self._fspath = os.path.join(launcher.fsroot, *subdir, name) | 428 self._fspath = os.path.join(launcher.fsroot, *subdir, name) |
395 self._urlpath = self._urljoin(launcher.urlroot, *subdir, name) | 429 self._urlpath = self.urljoin(launcher.urlroot, *subdir, name) |
396 self._type = mimetypes.guess_type(name)[0] | 430 self._type = mimetypes.guess_type(name)[0] |
397 if self._type is None: | 431 if self._type is None: |
398 self._type = "application/octet-stream" | 432 self._type = "application/octet-stream" |
399 if self._type.startswith("text/"): | 433 if self._type.startswith("text/"): |
400 self._encoding = launcher.encoding | 434 self._encoding = launcher.encoding |
403 self._encoding = None | 437 self._encoding = None |
404 | 438 |
405 def launch(self): | 439 def launch(self): |
406 print("adding static route:", self._urlpath) # debug | 440 print("adding static route:", self._urlpath) # debug |
407 self._app.route(self._urlpath, 'GET', self) | 441 self._app.route(self._urlpath, 'GET', self) |
408 | |
409 def _urljoin(self, *args): | |
410 args = list(args) | |
411 if args[0] == '/': | |
412 args[0] = '' | |
413 return '/'.join(args) | |
414 | 442 |
415 def _parse_date(self, ims): | 443 def _parse_date(self, ims): |
416 """ | 444 """ |
417 Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. | 445 Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. |
418 """ | 446 """ |
458 A route to an error page. These don't get routes created for them, | 486 A route to an error page. These don't get routes created for them, |
459 and are only reached if an error routes them there. Unless you create | 487 and are only reached if an error routes them there. Unless you create |
460 custom code-behind, only two variables are available to your template: | 488 custom code-behind, only two variables are available to your template: |
461 request (bottle.Request) and error (bottle.HTTPError). | 489 request (bottle.Request) and error (bottle.HTTPError). |
462 """ | 490 """ |
463 def __init__(self, template, loads, klass): | 491 def __init__(self, template, loads, klass, lock): |
464 self._template = template | 492 self._template = template |
465 self._template.prepare() | 493 self._template.prepare() |
466 self._loads = loads | 494 self._loads = loads |
467 self._class = klass | 495 self._class = klass |
496 self.lock = lock | |
468 | 497 |
469 def __call__(self, e): | 498 def __call__(self, e): |
470 bottle.request.environ[_FTYPE] = True | 499 bottle.request.environ[_FTYPE] = True |
471 try: | 500 try: |
472 obj = self._class(bottle.request, e) | 501 obj = self._class(bottle.request, e) |
473 obj.handle() | 502 obj.handle() |
474 tvars = self._loads.copy() | 503 tvars = self._loads.copy() |
475 tvars.update(obj.export()) | 504 tvars.update(obj.export()) |
476 return self._template.render(tvars).lstrip('\n') | 505 with self.lock: |
506 return self._template.render(tvars).lstrip('\n') | |
477 except bottle.HTTPResponse as e: | 507 except bottle.HTTPResponse as e: |
478 return e | 508 return e |
479 except Exception as e: | 509 except Exception as e: |
480 traceback.print_exc() | 510 traceback.print_exc() |
481 # Bottle doesn't allow error handlers to themselves cause | 511 # Bottle doesn't allow error handlers to themselves cause |
483 # this will cause a "Critical error while processing request" | 513 # this will cause a "Critical error while processing request" |
484 # page to be displayed, and any installed error pages to be | 514 # page to be displayed, and any installed error pages to be |
485 # ignored. | 515 # ignored. |
486 raise bottle.HTTPError(status=500, exception=e) | 516 raise bottle.HTTPError(status=500, exception=e) |
487 | 517 |
488 class _TinCanRoute(object): | 518 class _TinCanRoute(_TinCanBaseRoute): |
489 """ | 519 """ |
490 A route created by the TinCan launcher. | 520 A route created by the TinCan launcher. |
491 """ | 521 """ |
492 def __init__(self, launcher, name, subdir): | 522 def __init__(self, launcher, name, subdir): |
523 super().__init__(launcher, name, subdir) | |
493 self._fsroot = launcher.fsroot | 524 self._fsroot = launcher.fsroot |
494 self._urlroot = launcher.urlroot | 525 self._urlroot = launcher.urlroot |
495 self._name = name | 526 self._name = name |
496 self._python = name + _PEXTEN | 527 self._python = name + _PEXTEN |
497 self._fspath = os.path.join(launcher.fsroot, *subdir, name + _TEXTEN) | 528 self._fspath = os.path.join(launcher.fsroot, *subdir, name + _TEXTEN) |
498 self._urlpath = self._urljoin(launcher.urlroot, *subdir, name + _TEXTEN) | 529 self._urlpath = self.urljoin(launcher.urlroot, *subdir, name + _TEXTEN) |
499 self._origin = self._urlpath | 530 self._origin = self._urlpath |
500 self._subdir = subdir | 531 self._subdir = subdir |
501 self._seen = set() | 532 self._seen = set() |
502 self._app = launcher.app | 533 self._app = launcher.app |
503 self._encoding = launcher.encoding | 534 self._encoding = launcher.encoding |
570 if load.in_lib: | 601 if load.in_lib: |
571 fdir = os.path.join(self._fsroot, _WINF, "tlib") | 602 fdir = os.path.join(self._fsroot, _WINF, "tlib") |
572 else: | 603 else: |
573 fdir = os.path.join(self._fsroot, *self._subdir) | 604 fdir = os.path.join(self._fsroot, *self._subdir) |
574 try: | 605 try: |
575 tmpl = _get_template(load.fname, fdir, self._encoding) | 606 tmpl = self.get_template(load.fname, fdir, self._encoding) |
576 except Exception as e: | 607 except Exception as e: |
577 raise TinCanError("{0}: bad #load: {1!s}".format(self._urlpath, e)) from e | 608 raise TinCanError("{0}: bad #load: {1!s}".format(self._urlpath, e)) from e |
578 self._loads[load.vname] = tmpl.tpl | 609 self._loads[load.vname] = tmpl.tpl |
579 # If this is an #errors page, register it as such. | 610 # If this is an #errors page, register it as such. |
580 if oheader.errors is not None: | 611 if oheader.errors is not None: |
601 raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e | 632 raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e |
602 if not errors: | 633 if not errors: |
603 errors = range(_ERRMIN, _ERRMAX+1) | 634 errors = range(_ERRMIN, _ERRMAX+1) |
604 route = _TinCanErrorRoute( | 635 route = _TinCanErrorRoute( |
605 ChameleonTemplate(source=self._template.body, encoding=self._encoding), | 636 ChameleonTemplate(source=self._template.body, encoding=self._encoding), |
606 self._loads, self._class) | 637 self._loads, self._class, self.lock) |
607 for error in errors: | 638 for error in errors: |
608 if error < _ERRMIN or error > _ERRMAX: | 639 if error < _ERRMIN or error > _ERRMAX: |
609 raise TinCanError("{0}: bad #errors code".format(self._urlpath)) | 640 raise TinCanError("{0}: bad #errors code".format(self._urlpath)) |
610 self._app.error_handler[error] = route # XXX | 641 self._app.error_handler[error] = route # XXX |
611 | 642 |
698 if ext != _TEXTEN: | 729 if ext != _TEXTEN: |
699 raise TinCanError("{0}: invalid #forward".format(self._urlpath)) | 730 raise TinCanError("{0}: invalid #forward".format(self._urlpath)) |
700 self._subdir = rlist | 731 self._subdir = rlist |
701 self._python = name + _PEXTEN | 732 self._python = name + _PEXTEN |
702 self._fspath = os.path.join(self._fsroot, *self._subdir, rname) | 733 self._fspath = os.path.join(self._fsroot, *self._subdir, rname) |
703 self._urlpath = '/' + self._urljoin(*self._subdir, rname) | 734 self._urlpath = '/' + self.urljoin(*self._subdir, rname) |
704 | |
705 def _urljoin(self, *args): | |
706 args = list(args) | |
707 if args[0] == '/': | |
708 args[0] = '' | |
709 return '/'.join(args) | |
710 | 735 |
711 def __call__(self): | 736 def __call__(self): |
712 """ | 737 """ |
713 This gets called by the framework AFTER the page is launched. | 738 This gets called by the framework AFTER the page is launched. |
714 """ | 739 """ |
716 try: | 741 try: |
717 obj = self._class(bottle.request, bottle.response) | 742 obj = self._class(bottle.request, bottle.response) |
718 obj.handle() | 743 obj.handle() |
719 tvars = self._loads.copy() | 744 tvars = self._loads.copy() |
720 tvars.update(obj.export()) | 745 tvars.update(obj.export()) |
721 return self._body.render(tvars).lstrip('\n') | 746 with self.lock: |
747 return self._body.render(tvars).lstrip('\n') | |
722 except ForwardException as fwd: | 748 except ForwardException as fwd: |
723 target = fwd.target | 749 target = fwd.target |
724 except bottle.HTTPResponse as e: | 750 except bottle.HTTPResponse as e: |
725 return e | 751 return e |
726 except Exception as e: | 752 except Exception as e: |
747 route, args = self._app.router.match(environ) | 773 route, args = self._app.router.match(environ) |
748 environ['route.handle'] = environ['bottle.route'] = route | 774 environ['route.handle'] = environ['bottle.route'] = route |
749 environ['route.url_args'] = args | 775 environ['route.url_args'] = args |
750 return route.call(**args) | 776 return route.call(**args) |
751 | 777 |
778 # M u t e x | |
779 # | |
780 # A dummy lock class, which is what we use if we don't need locking. | |
781 | |
782 class _DummyLock(object): | |
783 def acquire(self, blocking=True, timeout=-1): | |
784 pass | |
785 | |
786 def release(self): | |
787 pass | |
788 | |
789 def __enter__(self): | |
790 self.acquire() | |
791 return self | |
792 | |
793 def __exit__(self, exc_type, exc_value, traceback): | |
794 self.release() | |
795 return False | |
796 | |
752 # L a u n c h e r | 797 # L a u n c h e r |
753 | 798 |
754 _WINF = "WEB-INF" | 799 _WINF = "WEB-INF" |
755 _BANNED = set([_WINF]) | 800 _BANNED = set([_WINF]) |
756 _EBANNED = set([_IEXTEN, _TEXTEN, _PEXTEN, _PEXTEN+"c"]) | 801 _EBANNED = set([_IEXTEN, _TEXTEN, _PEXTEN, _PEXTEN+"c"]) |
758 | 803 |
759 class _Launcher(object): | 804 class _Launcher(object): |
760 """ | 805 """ |
761 Helper class for launching webapps. | 806 Helper class for launching webapps. |
762 """ | 807 """ |
763 def __init__(self, fsroot, urlroot, logger): | 808 def __init__(self, fsroot, urlroot, logger, multithread=True): |
764 """ | 809 """ |
765 Lightweight constructor. The real action happens in .launch() below. | 810 Lightweight constructor. The real action happens in .launch() below. |
766 """ | 811 """ |
767 self.fsroot = fsroot | 812 self.fsroot = fsroot |
768 self.urlroot = urlroot | 813 self.urlroot = urlroot |
770 self.app = None | 815 self.app = None |
771 self.errors = 0 | 816 self.errors = 0 |
772 self.debug = False | 817 self.debug = False |
773 self.encoding = ENCODING | 818 self.encoding = ENCODING |
774 self.static = False | 819 self.static = False |
820 self.multithread = multithread | |
775 | 821 |
776 def launch(self): | 822 def launch(self): |
777 """ | 823 """ |
778 Does the actual work of launching something. XXX - modifies sys.path | 824 Does the actual work of launching something. XXX - modifies sys.path |
779 and never un-modifies it. | 825 and never un-modifies it. |