0001import threading
0002import urllib
0003from itertools import count
0004import time
0005import md5
0006from paste.request import path_info_pop, construct_url, get_cookies, parse_formvars
0007from paste import httpexceptions
0008from paste import httpheaders
0009from paste.util.template import Template
0010import simplejson
0011import re
0012
0013counter = count()
0014
0015def make_id():
0016    value = str(time.time()) + str(counter.next())
0017    h = md5.new(value).hexdigest()
0018    return h
0019
0020class WaitForIt(object):
0021
0022    def __init__(self, app, time_limit=10, poll_time=10,
0023                 template=None):
0024        self.app = app
0025        self.time_limit = time_limit
0026        self.poll_time = poll_time
0027        self.pending = {}
0028        if template is None:
0029            template = TEMPLATE
0030        if isinstance(template, basestring):
0031            template = Template(template)
0032        self.template = template
0033
0034    def __call__(self, environ, start_response):
0035        assert not environ['wsgi.multiprocess'], (
0036            "WaitForIt does not work in a multiprocess environment")
0037        path_info = environ.get('PATH_INFO', '')
0038        if path_info.startswith('/.waitforit/'):
0039            path_info_pop(environ)
0040            return self.check_status(environ, start_response)
0041        try:
0042            id = self.get_id(environ)
0043            if id:
0044                if id in self.pending:
0045                    return self.send_wait_page(environ, start_response, id=id)
0046                else:
0047                    # Bad id, remove it from QS:
0048                    qs = environ['QUERY_STRING']
0049                    qs = re.sub(r'&?waitforit_id=[a-f0-9]*', '', qs)
0050                    qs = re.sub(r'&send$', '', qs)
0051                    environ['QUERY_STRING'] = qs
0052                    # Then redirect:
0053                    exc = httpexceptions.HTTPMovedPermanently(
0054                        headers=[('Location', construct_url(environ))])
0055                    return exc(environ, start_response)
0056        except KeyError:
0057            # Fresh request
0058            pass
0059        if not self.accept_html(environ):
0060            return self.app(environ, start_response)
0061        data = []
0062        progress = {}
0063        environ['waitforit.progress'] = progress
0064        event = threading.Event()
0065        self.launch_application(environ, data, event, progress)
0066        event.wait(self.time_limit)
0067        if not data and progress.get('synchronous'):
0068            # The application has signaled that we should handle this
0069            # request synchronously
0070            event.wait()
0071        if not data:
0072            # Response hasn't come through in time
0073            id = make_id()
0074            self.pending[id] = [data, event, progress]
0075            return self.start_wait_page(environ, start_response, id)
0076        else:
0077            # Response came through before time_limit
0078            return self.send_page(start_response, data)
0079
0080    def accept_html(self, environ):
0081        accept = httpheaders.ACCEPT.parse(environ)
0082        if not accept:
0083            return True
0084        for arg in accept:
0085            if ';' in arg:
0086                arg = arg.split(';', 1)[0]
0087            if arg in ('*/*', 'text/*', 'text/html', 'application/xhtml+xml',
0088                       'application/xml', 'text/xml'):
0089                return True
0090        return False
0091
0092    def send_wait_page(self, environ, start_response, id=None):
0093        if id is None:
0094            id = self.get_id(environ)
0095        self.get_id(environ)
0096        if self.pending[id][0]:
0097            # Response has come through
0098            # FIXME: delete cookie
0099            data, event, progress = self.pending.pop(id)
0100            return self.send_page(start_response, data)
0101        request_url = construct_url(environ)
0102        waitforit_url = construct_url(environ, path_info='/.waitforit/')
0103        page = self.template.substitute(
0104            request_url=request_url,
0105            waitforit_url=waitforit_url,
0106            poll_time=self.poll_time,
0107            time_limit=self.time_limit,
0108            environ=environ,
0109            id=id)
0110        if isinstance(page, unicode):
0111            page = page.encode('utf8')
0112        start_response('200 OK',
0113                       [('Content-Type', 'text/html; charset=utf8'),
0114                        ('Content-Length', str(len(page))),
0115                        ('Set-Cookie', 'waitforit_id=%s' % id),
0116                        ])
0117        return [page]
0118
0119    def start_wait_page(self, environ, start_response, id):
0120        url = construct_url(environ)
0121        if '?' in url:
0122            url += '&'
0123        else:
0124            url += '?'
0125        url += 'waitforit_id=%s' % urllib.quote(id)
0126        exc = httpexceptions.HTTPTemporaryRedirect(
0127            headers=[('Location', url)])
0128        return exc(environ, start_response)
0129
0130    def send_page(self, start_response, data):
0131        status, headers, exc_info, app_iter = data
0132        start_response(status, headers, exc_info)
0133        return app_iter
0134
0135    def get_id(self, environ):
0136        qs = parse_formvars(environ)
0137        return qs['waitforit_id']
0138
0139    def check_status(self, environ, start_response, id=None):
0140        assert environ['PATH_INFO'] == '/status.json', (
0141            "Bad PATH_INFO=%r for %r" % (environ['PATH_INFO'], construct_url(environ)))
0142        if id is None:
0143            try:
0144                id = self.get_id(environ)
0145            except KeyError:
0146                body = "There is no pending request with the id %s" % id
0147                start_response('400 Bad Request', [
0148                    ('Content-type', 'text/plain'),
0149                    ('Content-length', str(len(body)))])
0150                return [body]
0151        try:
0152            data, event, progress = self.pending[id]
0153        except KeyError:
0154            data, event, progress = [True, None, None]
0155        if not data:
0156            result = {'done': False, 'progress': progress}
0157        else:
0158            result = {'done': True}
0159        start_response('200 OK',
0160                       [('Content-Type', 'application/json'),
0161                        ('Content-Length', str(len(result))),
0162                        ])
0163        return [simplejson.dumps(result)]
0164
0165    def launch_application(self, environ, data, event, progress):
0166        t = threading.Thread(target=self.run_application,
0167                             args=(environ, data, event, progress))
0168        t.setDaemon(True)
0169        t.start()
0170
0171    def run_application(self, environ, data, event, progress):
0172        start_response_data = []
0173        output = []
0174        def start_response(status, headers, exc_info=None):
0175            start_response_data[:] = [status, headers, exc_info]
0176            return output.append
0177        app_iter = self.app(environ, start_response)
0178        if output:
0179            # Stupid start_response writer...
0180            output.extend(app_iter)
0181            app_iter = output
0182        elif not start_response_data:
0183            # Stupid out-of-order call...
0184            app_iter = list(app_iter)
0185            assert start_response_data
0186        start_response_data.append(app_iter)
0187        data[:] = start_response_data
0188        event.set()
0189
0190# TODO: handle case when there's no Javascript (it'd just refresh)
0191
0192TEMPLATE = '''\
0193<html>
0194 <head>
0195  <title>Please wait</title>
0196  <script type="text/javascript">
0197    waitforit_url = "{{waitforit_url}}";
0198    poll_time = {{poll_time}};
0199    <<JAVASCRIPT>>
0200  </script>
0201  <style type="text/css">
0202    <<CSS>>
0203  </style>
0204 </head>
0205 <body onload="checkStatus()">
0206
0207 <h1>Please wait...</h1>
0208
0209 <p>
0210   The page you have requested is taking a while to generate...
0211 </p>
0212
0213 <p id="progress-box">
0214 </p>
0215
0216 <p id="percent-box">
0217
0218 <p id="error-box">
0219 </p>
0220 
0221 </body>
0222</html>
0223'''
0224
0225JAVASCRIPT = '''\
0226function getXMLHttpRequest() {
0227    var tryThese = [
0228        function () { return new XMLHttpRequest(); },
0229        function () { return new ActiveXObject('Msxml2.XMLHTTP'); },
0230        function () { return new ActiveXObject('Microsoft.XMLHTTP'); },
0231        function () { return new ActiveXObject('Msxml2.XMLHTTP.4.0'); }
0232        ];
0233    for (var i = 0; i < tryThese.length; i++) {
0234        var func = tryThese[i];
0235        try {
0236            return func();
0237        } catch (e) {
0238            // pass
0239        }
0240    }
0241}
0242
0243function checkStatus() {
0244    var xhr = getXMLHttpRequest();
0245    xhr.onreadystatechange = function () {
0246        if (xhr.readyState == 4) {
0247            statusReceived(xhr);
0248        }
0249    };
0250    if (waitforit_url.indexOf("?") != -1) {
0251        var parts = waitforit_url.split("?");
0252        var base = parts[0];
0253        var qs = "?" + parts[1];
0254    } else {
0255        var base = waitforit_url;
0256        var qs = '';
0257    }
0258    var status_url = base + "status.json" + qs;
0259    xhr.open("GET", status_url);
0260    xhr.send(null);
0261}
0262
0263var percent_inner = null;
0264
0265function statusReceived(req) {
0266    if (req.status != 200) {
0267        var el = document.getElementById("error-box");
0268        el.innerHTML = req.responseText;
0269        return;
0270    }
0271    var status = eval("dummy="+req.responseText);
0272    if (typeof status.done == "undefined") {
0273        // Something went wrong
0274        var el = document.getElementById("error-box");
0275        el.innerHTML = req.responseText;
0276        return;
0277    }
0278    if (status.done) {
0279        window.location.href = window.location.href + "&send";
0280        return;
0281    }
0282    if (status.progress.message) {
0283        var el = document.getElementById("progress-box");
0284        el.innerHTML = status.progress.message;
0285    }
0286    if (status.progress.percent) {
0287        if (! percent_inner) {
0288            var outer = document.createElement("div");
0289            outer.setAttribute("id", "percent-container");
0290            percent_inner = document.createElement("div");
0291            percent_inner.setAttribute("id", "percent-inner");
0292            //percent_inner.innerHTML = "&nbsp;";
0293            outer.appendChild(percent_inner);
0294            var parent = document.getElementById("percent-box");
0295            parent.appendChild(outer);
0296        }
0297        percent_inner.style.width = ""+Math.round(status.progress.percent) + "%";
0298    }
0299    setTimeout("checkStatus()", poll_time*1000);
0300}
0301'''
0302
0303CSS = '''\
0304body {
0305  font-family: sans-serif;
0306}
0307div#percent-container {
0308  border: 1px solid #000;
0309  width: 100%;
0310  height: 20px;
0311}
0312div#percent-inner {
0313  background-color: #999;
0314  height: 100%;
0315}
0316'''
0317
0318TEMPLATE = TEMPLATE.replace('<<JAVASCRIPT>>', JAVASCRIPT);
0319TEMPLATE = TEMPLATE.replace('<<CSS>>', CSS);