source: trunk/src/allmydata/web/operations.py

Last change on this file was 1cfe843d, checked in by Alexandre Detiste <alexandre.detiste@…>, at 2024-02-22T23:40:25Z

more python2 removal

  • Property mode set to 100644
File size: 6.1 KB
Line 
1"""
2Ported to Python 3.
3"""
4
5import time
6from hyperlink import (
7    DecodedURL,
8)
9from twisted.web.template import (
10    renderer,
11    tags as T,
12)
13from twisted.python.urlpath import (
14    URLPath,
15)
16from twisted.python.failure import Failure
17from twisted.internet import reactor, defer
18from twisted.web import resource
19from twisted.web.http import NOT_FOUND
20from twisted.web.html import escape
21from twisted.application import service
22
23from allmydata.web.common import (
24    WebError,
25    get_arg,
26    boolean_of_arg,
27    exception_to_child,
28)
29
30MINUTE = 60
31HOUR = 60*MINUTE
32DAY = 24*HOUR
33
34(MONITOR, RENDERER, WHEN_ADDED) = range(3)
35
36class OphandleTable(resource.Resource, service.Service):
37    """Renders /operations/%d."""
38    # The type in Twisted for services is wrong in 22.10...
39    # https://github.com/twisted/twisted/issues/10135
40    name = "operations"  # type: ignore[assignment]
41
42    UNCOLLECTED_HANDLE_LIFETIME = 4*DAY
43    COLLECTED_HANDLE_LIFETIME = 1*DAY
44
45    def __init__(self, clock=None):
46        super(OphandleTable, self).__init__()
47        # both of these are indexed by ophandle
48        self.handles = {} # tuple of (monitor, renderer, when_added)
49        self.timers = {}
50        # The tests will provide a deterministic clock
51        # (twisted.internet.task.Clock) that they can control so that
52        # they can test ophandle expiration. If this is provided, I'll
53        # use it schedule the expiration of ophandles.
54        self.clock = clock
55
56    def stopService(self):
57        for t in self.timers.values():
58            if t.active():
59                t.cancel()
60        del self.handles # this is not restartable
61        del self.timers
62        return service.Service.stopService(self)
63
64    def add_monitor(self, req, monitor, renderer):
65        """
66        :param allmydata.webish.MyRequest req:
67        :param allmydata.monitor.Monitor monitor:
68        :param allmydata.web.directory.ManifestResults renderer:
69        """
70        ophandle = get_arg(req, "ophandle")
71        assert ophandle
72        now = time.time()
73        self.handles[ophandle] = (monitor, renderer, now)
74        retain_for = get_arg(req, "retain-for", None)
75        if retain_for is not None:
76            self._set_timer(ophandle, int(retain_for))
77        monitor.when_done().addBoth(self._operation_complete, ophandle)
78
79    def _operation_complete(self, res, ophandle):
80        if ophandle in self.handles:
81            if ophandle not in self.timers:
82                # the client has not provided a retain-for= value for this
83                # handle, so we set our own.
84                now = time.time()
85                added = self.handles[ophandle][WHEN_ADDED]
86                when = max(self.UNCOLLECTED_HANDLE_LIFETIME, now - added)
87                self._set_timer(ophandle, when)
88            # if we already have a timer, the client must have provided the
89            # retain-for= value, so don't touch it.
90
91    def redirect_to(self, req):
92        """
93        :param allmydata.webish.MyRequest req:
94        """
95        ophandle = get_arg(req, "ophandle").decode("utf-8")
96        assert ophandle
97        here = DecodedURL.from_text(str(URLPath.fromRequest(req)))
98        target = here.click(u"/").child(u"operations", ophandle)
99        output = get_arg(req, "output")
100        if output:
101            target = target.add(u"output", output.decode("utf-8"))
102        return target
103
104    @exception_to_child
105    def getChild(self, name, req):
106        ophandle = name
107        if ophandle not in self.handles:
108            raise WebError("unknown/expired handle '%s'" % escape(str(ophandle, "utf-8")),
109                           NOT_FOUND)
110        (monitor, renderer, when_added) = self.handles[ophandle]
111
112        t = get_arg(req, "t", "status")
113        if t == b"cancel" and req.method == b"POST":
114            monitor.cancel()
115            # return the status anyways, but release the handle
116            self._release_ophandle(ophandle)
117
118        else:
119            retain_for = get_arg(req, "retain-for", None)
120            if retain_for is not None:
121                self._set_timer(ophandle, int(retain_for))
122
123            if monitor.is_finished():
124                if boolean_of_arg(get_arg(req, "release-after-complete", "false")):
125                    self._release_ophandle(ophandle)
126                if retain_for is None:
127                    # this GET is collecting the ophandle, so change its timer
128                    self._set_timer(ophandle, self.COLLECTED_HANDLE_LIFETIME)
129
130        status = monitor.get_status()
131        if isinstance(status, Failure):
132            return defer.fail(status)
133
134        return renderer
135
136    def _set_timer(self, ophandle, when):
137        if ophandle in self.timers and self.timers[ophandle].active():
138            self.timers[ophandle].cancel()
139        if self.clock:
140            t = self.clock.callLater(when, self._release_ophandle, ophandle)
141        else:
142            t = reactor.callLater(when, self._release_ophandle, ophandle)
143        self.timers[ophandle] = t
144
145    def _release_ophandle(self, ophandle):
146        if ophandle in self.timers and self.timers[ophandle].active():
147            self.timers[ophandle].cancel()
148        self.timers.pop(ophandle, None)
149        self.handles.pop(ophandle, None)
150
151
152class ReloadMixin(object):
153    REFRESH_TIME = 1*MINUTE
154
155    @renderer
156    def refresh(self, req, tag):
157        if self.monitor.is_finished():
158            return ""
159        tag.attributes["http-equiv"] = "refresh"
160        tag.attributes["content"] = str(self.REFRESH_TIME)
161        return tag
162
163    @renderer
164    def reload(self, req, tag):
165        if self.monitor.is_finished():
166            return b""
167        # url.gethere would break a proxy, so the correct thing to do is
168        # req.path[-1] + queryargs
169        ophandle = req.prepath[-1]
170        reload_target = ophandle + b"?output=html"
171        cancel_target = ophandle + b"?t=cancel"
172        cancel_button = T.form(T.input(type="submit", value="Cancel"),
173                               action=cancel_target,
174                               method="POST",
175                               enctype="multipart/form-data",)
176
177        return (T.h2("Operation still running: ",
178                     T.a("Reload", href=reload_target),
179                     ),
180                cancel_button,)
Note: See TracBrowser for help on using the repository browser.