| 1 | """ |
|---|
| 2 | Ported to Python 3. |
|---|
| 3 | """ |
|---|
| 4 | |
|---|
| 5 | import time |
|---|
| 6 | from hyperlink import ( |
|---|
| 7 | DecodedURL, |
|---|
| 8 | ) |
|---|
| 9 | from twisted.web.template import ( |
|---|
| 10 | renderer, |
|---|
| 11 | tags as T, |
|---|
| 12 | ) |
|---|
| 13 | from twisted.python.urlpath import ( |
|---|
| 14 | URLPath, |
|---|
| 15 | ) |
|---|
| 16 | from twisted.python.failure import Failure |
|---|
| 17 | from twisted.internet import reactor, defer |
|---|
| 18 | from twisted.web import resource |
|---|
| 19 | from twisted.web.http import NOT_FOUND |
|---|
| 20 | from twisted.web.html import escape |
|---|
| 21 | from twisted.application import service |
|---|
| 22 | |
|---|
| 23 | from allmydata.web.common import ( |
|---|
| 24 | WebError, |
|---|
| 25 | get_arg, |
|---|
| 26 | boolean_of_arg, |
|---|
| 27 | exception_to_child, |
|---|
| 28 | ) |
|---|
| 29 | |
|---|
| 30 | MINUTE = 60 |
|---|
| 31 | HOUR = 60*MINUTE |
|---|
| 32 | DAY = 24*HOUR |
|---|
| 33 | |
|---|
| 34 | (MONITOR, RENDERER, WHEN_ADDED) = range(3) |
|---|
| 35 | |
|---|
| 36 | class 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 | |
|---|
| 152 | class ReloadMixin: |
|---|
| 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,) |
|---|