##// END OF EJS Templates
progress: flush stderr after clearing...
Kyle Lippincott -
r44485:4e0a6d15 stable
parent child Browse files
Show More
@@ -1,315 +1,316 b''
1 1 # progress.py progress bars related code
2 2 #
3 3 # Copyright (C) 2010 Augie Fackler <durin42@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import threading
12 12 import time
13 13
14 14 from .i18n import _
15 15 from . import encoding
16 16
17 17
18 18 def spacejoin(*args):
19 19 return b' '.join(s for s in args if s)
20 20
21 21
22 22 def shouldprint(ui):
23 23 return not (ui.quiet or ui.plain(b'progress')) and (
24 24 ui._isatty(ui.ferr) or ui.configbool(b'progress', b'assume-tty')
25 25 )
26 26
27 27
28 28 def fmtremaining(seconds):
29 29 """format a number of remaining seconds in human readable way
30 30
31 31 This will properly display seconds, minutes, hours, days if needed"""
32 32 if seconds < 60:
33 33 # i18n: format XX seconds as "XXs"
34 34 return _(b"%02ds") % seconds
35 35 minutes = seconds // 60
36 36 if minutes < 60:
37 37 seconds -= minutes * 60
38 38 # i18n: format X minutes and YY seconds as "XmYYs"
39 39 return _(b"%dm%02ds") % (minutes, seconds)
40 40 # we're going to ignore seconds in this case
41 41 minutes += 1
42 42 hours = minutes // 60
43 43 minutes -= hours * 60
44 44 if hours < 30:
45 45 # i18n: format X hours and YY minutes as "XhYYm"
46 46 return _(b"%dh%02dm") % (hours, minutes)
47 47 # we're going to ignore minutes in this case
48 48 hours += 1
49 49 days = hours // 24
50 50 hours -= days * 24
51 51 if days < 15:
52 52 # i18n: format X days and YY hours as "XdYYh"
53 53 return _(b"%dd%02dh") % (days, hours)
54 54 # we're going to ignore hours in this case
55 55 days += 1
56 56 weeks = days // 7
57 57 days -= weeks * 7
58 58 if weeks < 55:
59 59 # i18n: format X weeks and YY days as "XwYYd"
60 60 return _(b"%dw%02dd") % (weeks, days)
61 61 # we're going to ignore days and treat a year as 52 weeks
62 62 weeks += 1
63 63 years = weeks // 52
64 64 weeks -= years * 52
65 65 # i18n: format X years and YY weeks as "XyYYw"
66 66 return _(b"%dy%02dw") % (years, weeks)
67 67
68 68
69 69 # file_write() and file_flush() of Python 2 do not restart on EINTR if
70 70 # the file is attached to a "slow" device (e.g. a terminal) and raise
71 71 # IOError. We cannot know how many bytes would be written by file_write(),
72 72 # but a progress text is known to be short enough to be written by a
73 73 # single write() syscall, so we can just retry file_write() with the whole
74 74 # text. (issue5532)
75 75 #
76 76 # This should be a short-term workaround. We'll need to fix every occurrence
77 77 # of write() to a terminal or pipe.
78 78 def _eintrretry(func, *args):
79 79 while True:
80 80 try:
81 81 return func(*args)
82 82 except IOError as err:
83 83 if err.errno == errno.EINTR:
84 84 continue
85 85 raise
86 86
87 87
88 88 class progbar(object):
89 89 def __init__(self, ui):
90 90 self.ui = ui
91 91 self._refreshlock = threading.Lock()
92 92 self.resetstate()
93 93
94 94 def resetstate(self):
95 95 self.topics = []
96 96 self.topicstates = {}
97 97 self.starttimes = {}
98 98 self.startvals = {}
99 99 self.printed = False
100 100 self.lastprint = time.time() + float(
101 101 self.ui.config(b'progress', b'delay')
102 102 )
103 103 self.curtopic = None
104 104 self.lasttopic = None
105 105 self.indetcount = 0
106 106 self.refresh = float(self.ui.config(b'progress', b'refresh'))
107 107 self.changedelay = max(
108 108 3 * self.refresh, float(self.ui.config(b'progress', b'changedelay'))
109 109 )
110 110 self.order = self.ui.configlist(b'progress', b'format')
111 111 self.estimateinterval = self.ui.configwith(
112 112 float, b'progress', b'estimateinterval'
113 113 )
114 114
115 115 def show(self, now, topic, pos, item, unit, total):
116 116 if not shouldprint(self.ui):
117 117 return
118 118 termwidth = self.width()
119 119 self.printed = True
120 120 head = b''
121 121 needprogress = False
122 122 tail = b''
123 123 for indicator in self.order:
124 124 add = b''
125 125 if indicator == b'topic':
126 126 add = topic
127 127 elif indicator == b'number':
128 128 if total:
129 129 add = b'%*d/%d' % (len(str(total)), pos, total)
130 130 else:
131 131 add = b'%d' % pos
132 132 elif indicator.startswith(b'item') and item:
133 133 slice = b'end'
134 134 if b'-' in indicator:
135 135 wid = int(indicator.split(b'-')[1])
136 136 elif b'+' in indicator:
137 137 slice = b'beginning'
138 138 wid = int(indicator.split(b'+')[1])
139 139 else:
140 140 wid = 20
141 141 if slice == b'end':
142 142 add = encoding.trim(item, wid, leftside=True)
143 143 else:
144 144 add = encoding.trim(item, wid)
145 145 add += (wid - encoding.colwidth(add)) * b' '
146 146 elif indicator == b'bar':
147 147 add = b''
148 148 needprogress = True
149 149 elif indicator == b'unit' and unit:
150 150 add = unit
151 151 elif indicator == b'estimate':
152 152 add = self.estimate(topic, pos, total, now)
153 153 elif indicator == b'speed':
154 154 add = self.speed(topic, pos, unit, now)
155 155 if not needprogress:
156 156 head = spacejoin(head, add)
157 157 else:
158 158 tail = spacejoin(tail, add)
159 159 if needprogress:
160 160 used = 0
161 161 if head:
162 162 used += encoding.colwidth(head) + 1
163 163 if tail:
164 164 used += encoding.colwidth(tail) + 1
165 165 progwidth = termwidth - used - 3
166 166 if total and pos <= total:
167 167 amt = pos * progwidth // total
168 168 bar = b'=' * (amt - 1)
169 169 if amt > 0:
170 170 bar += b'>'
171 171 bar += b' ' * (progwidth - amt)
172 172 else:
173 173 progwidth -= 3
174 174 self.indetcount += 1
175 175 # mod the count by twice the width so we can make the
176 176 # cursor bounce between the right and left sides
177 177 amt = self.indetcount % (2 * progwidth)
178 178 amt -= progwidth
179 179 bar = (
180 180 b' ' * int(progwidth - abs(amt))
181 181 + b'<=>'
182 182 + b' ' * int(abs(amt))
183 183 )
184 184 prog = b''.join((b'[', bar, b']'))
185 185 out = spacejoin(head, prog, tail)
186 186 else:
187 187 out = spacejoin(head, tail)
188 188 self._writeerr(b'\r' + encoding.trim(out, termwidth))
189 189 self.lasttopic = topic
190 190 self._flusherr()
191 191
192 192 def clear(self):
193 193 if not self.printed or not self.lastprint or not shouldprint(self.ui):
194 194 return
195 195 self._writeerr(b'\r%s\r' % (b' ' * self.width()))
196 self._flusherr()
196 197 if self.printed:
197 198 # force immediate re-paint of progress bar
198 199 self.lastprint = 0
199 200
200 201 def complete(self):
201 202 if not shouldprint(self.ui):
202 203 return
203 204 if self.ui.configbool(b'progress', b'clear-complete'):
204 205 self.clear()
205 206 else:
206 207 self._writeerr(b'\n')
207 208 self._flusherr()
208 209
209 210 def _flusherr(self):
210 211 _eintrretry(self.ui.ferr.flush)
211 212
212 213 def _writeerr(self, msg):
213 214 _eintrretry(self.ui.ferr.write, msg)
214 215
215 216 def width(self):
216 217 tw = self.ui.termwidth()
217 218 return min(int(self.ui.config(b'progress', b'width', default=tw)), tw)
218 219
219 220 def estimate(self, topic, pos, total, now):
220 221 if total is None:
221 222 return b''
222 223 initialpos = self.startvals[topic]
223 224 target = total - initialpos
224 225 delta = pos - initialpos
225 226 if delta > 0:
226 227 elapsed = now - self.starttimes[topic]
227 228 seconds = (elapsed * (target - delta)) // delta + 1
228 229 return fmtremaining(seconds)
229 230 return b''
230 231
231 232 def speed(self, topic, pos, unit, now):
232 233 initialpos = self.startvals[topic]
233 234 delta = pos - initialpos
234 235 elapsed = now - self.starttimes[topic]
235 236 if elapsed > 0:
236 237 return _(b'%d %s/sec') % (delta / elapsed, unit)
237 238 return b''
238 239
239 240 def _oktoprint(self, now):
240 241 '''Check if conditions are met to print - e.g. changedelay elapsed'''
241 242 if (
242 243 self.lasttopic is None # first time we printed
243 244 # not a topic change
244 245 or self.curtopic == self.lasttopic
245 246 # it's been long enough we should print anyway
246 247 or now - self.lastprint >= self.changedelay
247 248 ):
248 249 return True
249 250 else:
250 251 return False
251 252
252 253 def _calibrateestimate(self, topic, now, pos):
253 254 '''Adjust starttimes and startvals for topic so ETA works better
254 255
255 256 If progress is non-linear (ex. get much slower in the last minute),
256 257 it's more friendly to only use a recent time span for ETA and speed
257 258 calculation.
258 259
259 260 [======================================> ]
260 261 ^^^^^^^
261 262 estimateinterval, only use this for estimation
262 263 '''
263 264 interval = self.estimateinterval
264 265 if interval <= 0:
265 266 return
266 267 elapsed = now - self.starttimes[topic]
267 268 if elapsed > interval:
268 269 delta = pos - self.startvals[topic]
269 270 newdelta = delta * interval / elapsed
270 271 # If a stall happens temporarily, ETA could change dramatically
271 272 # frequently. This is to avoid such dramatical change and make ETA
272 273 # smoother.
273 274 if newdelta < 0.1:
274 275 return
275 276 self.startvals[topic] = pos - newdelta
276 277 self.starttimes[topic] = now - interval
277 278
278 279 def progress(self, topic, pos, item=b'', unit=b'', total=None):
279 280 if pos is None:
280 281 self.closetopic(topic)
281 282 return
282 283 now = time.time()
283 284 with self._refreshlock:
284 285 if topic not in self.topics:
285 286 self.starttimes[topic] = now
286 287 self.startvals[topic] = pos
287 288 self.topics.append(topic)
288 289 self.topicstates[topic] = pos, item, unit, total
289 290 self.curtopic = topic
290 291 self._calibrateestimate(topic, now, pos)
291 292 if now - self.lastprint >= self.refresh and self.topics:
292 293 if self._oktoprint(now):
293 294 self.lastprint = now
294 295 self.show(now, topic, *self.topicstates[topic])
295 296
296 297 def closetopic(self, topic):
297 298 with self._refreshlock:
298 299 self.starttimes.pop(topic, None)
299 300 self.startvals.pop(topic, None)
300 301 self.topicstates.pop(topic, None)
301 302 # reset the progress bar if this is the outermost topic
302 303 if self.topics and self.topics[0] == topic and self.printed:
303 304 self.complete()
304 305 self.resetstate()
305 306 # truncate the list of topics assuming all topics within
306 307 # this one are also closed
307 308 if topic in self.topics:
308 309 self.topics = self.topics[: self.topics.index(topic)]
309 310 # reset the last topic to the one we just unwound to,
310 311 # so that higher-level topics will be stickier than
311 312 # lower-level topics
312 313 if self.topics:
313 314 self.lasttopic = self.topics[-1]
314 315 else:
315 316 self.lasttopic = None
General Comments 0
You need to be logged in to leave comments. Login now