]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blob - src/allmydata/web/operations.py
Increase ophandle expiration times, per #577
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / web / operations.py
1
2 import time
3 from zope.interface import implements
4 from nevow import rend, url, tags as T
5 from nevow.inevow import IRequest
6 from twisted.python.failure import Failure
7 from twisted.internet import reactor, defer
8 from twisted.web.http import NOT_FOUND
9 from twisted.web.html import escape
10 from twisted.application import service
11
12 from allmydata.web.common import IOpHandleTable, WebError, \
13      get_root, get_arg, boolean_of_arg
14
15 MINUTE = 60
16 HOUR = 60*MINUTE
17 DAY = 24*HOUR
18
19 (MONITOR, RENDERER, WHEN_ADDED) = range(3)
20
21 class OphandleTable(rend.Page, service.Service):
22     implements(IOpHandleTable)
23
24     UNCOLLECTED_HANDLE_LIFETIME = 4*DAY
25     COLLECTED_HANDLE_LIFETIME = 1*DAY
26
27     def __init__(self):
28         # both of these are indexed by ophandle
29         self.handles = {} # tuple of (monitor, renderer, when_added)
30         self.timers = {}
31
32     def stopService(self):
33         for t in self.timers.values():
34             if t.active():
35                 t.cancel()
36         del self.handles # this is not restartable
37         del self.timers
38         return service.Service.stopService(self)
39
40     def add_monitor(self, ctx, monitor, renderer):
41         ophandle = get_arg(ctx, "ophandle")
42         assert ophandle
43         now = time.time()
44         self.handles[ophandle] = (monitor, renderer, now)
45         retain_for = get_arg(ctx, "retain-for", None)
46         if retain_for is not None:
47             self._set_timer(ophandle, int(retain_for))
48         monitor.when_done().addBoth(self._operation_complete, ophandle)
49
50     def _operation_complete(self, res, ophandle):
51         if ophandle in self.handles:
52             if ophandle not in self.timers:
53                 # the client has not provided a retain-for= value for this
54                 # handle, so we set our own.
55                 now = time.time()
56                 added = self.handles[ophandle][WHEN_ADDED]
57                 when = max(self.UNCOLLECTED_HANDLE_LIFETIME, now - added)
58                 self._set_timer(ophandle, when)
59             # if we already have a timer, the client must have provided the
60             # retain-for= value, so don't touch it.
61
62     def redirect_to(self, ctx):
63         ophandle = get_arg(ctx, "ophandle")
64         assert ophandle
65         target = get_root(ctx) + "/operations/" + ophandle
66         output = get_arg(ctx, "output")
67         if output:
68             target = target + "?output=%s" % output
69         return url.URL.fromString(target)
70
71     def childFactory(self, ctx, name):
72         ophandle = name
73         if ophandle not in self.handles:
74             raise WebError("unknown/expired handle '%s'" % escape(ophandle),
75                            NOT_FOUND)
76         (monitor, renderer, when_added) = self.handles[ophandle]
77
78         request = IRequest(ctx)
79         t = get_arg(ctx, "t", "status")
80         if t == "cancel" and request.method == "POST":
81             monitor.cancel()
82             # return the status anyways, but release the handle
83             self._release_ophandle(ophandle)
84
85         else:
86             retain_for = get_arg(ctx, "retain-for", None)
87             if retain_for is not None:
88                 self._set_timer(ophandle, int(retain_for))
89
90             if monitor.is_finished():
91                 if boolean_of_arg(get_arg(ctx, "release-after-complete", "false")):
92                     self._release_ophandle(ophandle)
93                 if retain_for is None:
94                     # this GET is collecting the ophandle, so change its timer
95                     self._set_timer(ophandle, self.COLLECTED_HANDLE_LIFETIME)
96
97         status = monitor.get_status()
98         if isinstance(status, Failure):
99             return defer.fail(status)
100
101         return renderer
102
103     def _set_timer(self, ophandle, when):
104         if ophandle in self.timers and self.timers[ophandle].active():
105             self.timers[ophandle].cancel()
106         t = reactor.callLater(when, self._release_ophandle, ophandle)
107         self.timers[ophandle] = t
108
109     def _release_ophandle(self, ophandle):
110         if ophandle in self.timers and self.timers[ophandle].active():
111             self.timers[ophandle].cancel()
112         self.timers.pop(ophandle, None)
113         self.handles.pop(ophandle, None)
114
115 class ReloadMixin:
116     REFRESH_TIME = 1*MINUTE
117
118     def render_refresh(self, ctx, data):
119         if self.monitor.is_finished():
120             return ""
121         # dreid suggests ctx.tag(**dict([("http-equiv", "refresh")]))
122         # but I can't tell if he's joking or not
123         ctx.tag.attributes["http-equiv"] = "refresh"
124         ctx.tag.attributes["content"] = str(self.REFRESH_TIME)
125         return ctx.tag
126
127     def render_reload(self, ctx, data):
128         if self.monitor.is_finished():
129             return ""
130         req = IRequest(ctx)
131         # url.gethere would break a proxy, so the correct thing to do is
132         # req.path[-1] + queryargs
133         ophandle = req.prepath[-1]
134         reload_target = ophandle + "?output=html"
135         cancel_target = ophandle + "?t=cancel"
136         cancel_button = T.form(action=cancel_target, method="POST",
137                                enctype="multipart/form-data")[
138             T.input(type="submit", value="Cancel"),
139             ]
140
141         return [T.h2["Operation still running: ",
142                      T.a(href=reload_target)["Reload"],
143                      ],
144                 cancel_button,
145                 ]