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.