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