summaryrefslogtreecommitdiff
path: root/virtinst/_progresspriv.py
blob: 207c64796656b4c4f295c817b2738fcb154c0942 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
# This work is licensed under the GNU GPLv2 or later.
# See the COPYING file in the top-level directory.

# This file is part of urlgrabber, a high-level cross-protocol url-grabber
# Copyright 2002-2004 Michael D. Stenner, Ryan Tomayko

# This code is all straight from python-urlgrabber, which we historically
# used the system installed version of. But since the project is in
# maintenance mode upstream, and eventually we want to switch to python3,
# we are just copying this for now.


import fcntl
import struct
import sys
import termios
import time


# Code from https://mail.python.org/pipermail/python-list/2000-May/033365.html
def terminal_width(fd=1):
    """ Get the real terminal width """
    try:
        buf = 'abcdefgh'
        buf = fcntl.ioctl(fd, termios.TIOCGWINSZ, buf)
        ret = struct.unpack('hhhh', buf)[1]  # pragma: no cover
        return ret or 80  # pragma: no cover
    except IOError:
        return 80


_term_width_val = None
_term_width_last = None


def terminal_width_cached(fd=1, cache_timeout=1.000):
    """ Get the real terminal width, but cache it for a bit. """
    global _term_width_val
    global _term_width_last

    now = time.time()
    if _term_width_val is None or (now - _term_width_last) > cache_timeout:
        _term_width_val = terminal_width(fd)
        _term_width_last = now
    return _term_width_val


class TerminalLine:
    """ Help create dynamic progress bars, uses terminal_width_cached(). """

    def __init__(self, min_rest=0, beg_len=None, fd=1, cache_timeout=1.000):
        if beg_len is None:
            beg_len = min_rest
        self._min_len = min_rest
        self.llen = max(terminal_width_cached(fd, cache_timeout), beg_len)
        self._fin = False

    def __len__(self):
        """ Usable length for elements. """
        return self.llen - self._min_len

    def rest_split(self, fixed, elements=2):
        """ After a fixed length, split the rest of the line length among
            a number of different elements (default=2). """
        return max(self.llen - fixed, 0) // elements

    def add(self, element, full_len=None):
        """ If there is room left in the line, above min_len, add element.
            Note that as soon as one add fails all the rest will fail too. """

        if full_len is None:
            full_len = len(element)
        if len(self) < full_len:
            self._fin = True
        if self._fin:
            return ''

        self.llen -= len(element)
        return element

    def rest(self):
        """ Current rest of line, same as .rest_split(fixed=0, elements=1). """
        return self.llen


class BaseMeter:
    def __init__(self):
        self.update_period = 0.3  # seconds

        self.text = None
        self.size = None
        self.start_time = None
        self.last_amount_read = 0
        self.last_update_time = None
        self.re = RateEstimator()

    def start(self, text, size):
        self.text = text
        self.size = size
        assert type(size) in [int, type(None)]
        assert self.text is not None

        now = time.time()
        self.start_time = now
        self.re.start(size, now)
        self.last_amount_read = 0
        self.last_update_time = now

    def update(self, amount_read):
        # for a real gui, you probably want to override and put a call
        # to your mainloop iteration function here
        assert type(amount_read) is int

        now = time.time()
        self.last_amount_read = amount_read
        self.re.update(amount_read, now)
        if (not self.last_update_time or
                (now >= self.last_update_time + self.update_period)):
            self.last_update_time = now
            self._do_update(amount_read)

    def _do_update(self, amount_read):
        pass

    def end(self):
        self._do_end()

    def _do_end(self):
        pass


#
#       update: No size (minimal: 17 chars)
#       -----------------------------------
# <text>                          <rate> | <current size> <elapsed time>
#  8-48                          1    8  3             6 1            9 5
#
# Order: 1. <text>+<current size> (17)
#        2. +<elapsed time>       (10, total: 27)
#        3. +                     ( 5, total: 32)
#        4. +<rate>               ( 9, total: 41)
#
#       update: Size, Single file
#       -------------------------
# <text>            <pc>  <bar> <rate> | <current size> <eta time> ETA
#  8-25            1 3-4 1 6-16 1   8  3             6 1        9 1  3 1
#
# Order: 1. <text>+<current size> (17)
#        2. +<eta time>           (10, total: 27)
#        3. +ETA                  ( 5, total: 32)
#        4. +<pc>                 ( 4, total: 36)
#        5. +<rate>               ( 9, total: 45)
#        6. +<bar>                ( 7, total: 52)
#
#       update: Size, All files
#       -----------------------
# <text> <total pc> <pc>  <bar> <rate> | <current size> <eta time> ETA
#  8-22 1      5-7 1 3-4 1 6-12 1   8  3             6 1        9 1  3 1
#
# Order: 1. <text>+<current size> (17)
#        2. +<eta time>           (10, total: 27)
#        3. +ETA                  ( 5, total: 32)
#        4. +<total pc>           ( 5, total: 37)
#        4. +<pc>                 ( 4, total: 41)
#        5. +<rate>               ( 9, total: 50)
#        6. +<bar>                ( 7, total: 57)
#
#       end
#       ---
# <text>                                 | <current size> <elapsed time>
#  8-56                                  3             6 1            9 5
#
# Order: 1. <text>                ( 8)
#        2. +<current size>       ( 9, total: 17)
#        3. +<elapsed time>       (10, total: 27)
#        4. +                     ( 5, total: 32)
#


def _term_add_bar(tl, bar_max_length, pc):
    bar_len = bar_max_length * pc
    ibar_len = int(bar_len)
    progressbar = '=' * ibar_len
    if (bar_len - ibar_len) >= 0.5:
        progressbar += '-'
    return tl.add(' [%-*.*s]' % (bar_max_length, bar_max_length,
                                 progressbar))


def _term_add_end(tl, osize, size):
    if osize:  # osize should be None or >0, but that's been broken.
        if size > osize:  # Is ??? better? Really need something to say < vs >.
            return tl.add(' !!! '), True
        elif size != osize:
            return tl.add(' ... '), True
    return tl.add(' ' * 5), False


class TextMeter(BaseMeter):
    def __init__(self, output=sys.stderr):
        BaseMeter.__init__(self)
        self.output = output

    def _do_update(self, amount_read):
        etime = self.re.elapsed_time()
        fread = format_number(amount_read)

        ave_dl = format_number(self.re.average_rate())

        # Include text + ui_rate in minimal
        tl = TerminalLine(8, 8 + 1 + 8)
        # For big screens, make it more readable.
        use_hours = bool(tl.llen > 80)
        ui_size = tl.add(' | %5sB' % fread)
        if self.size is None:
            ui_time = tl.add('  %s' % format_time(etime, use_hours))
            ui_end = tl.add(' ' * 5)
            ui_rate = tl.add(' %5sB/s' % ave_dl)
            out = '%-*.*s%s%s%s%s\r' % (tl.rest(), tl.rest(), self.text,
                                        ui_rate, ui_size, ui_time, ui_end)
        else:
            rtime = self.re.remaining_time()
            frtime = format_time(rtime, use_hours)
            frac = self.re.fraction_read()

            ui_time = tl.add('  %s' % frtime)
            ui_end = tl.add(' ETA ')

            ui_pc = tl.add(' %2i%%' % (frac * 100))
            ui_rate = tl.add(' %5sB/s' % ave_dl)
            # Make text grow a bit before we start growing the bar too
            blen = 4 + tl.rest_split(8 + 8 + 4)
            ui_bar = _term_add_bar(tl, blen, frac)
            out = '\r%-*.*s%s%s%s%s%s%s\r' % (
                tl.rest(), tl.rest(), self.text,
                ui_pc, ui_bar,
                ui_rate, ui_size, ui_time, ui_end
            )

        self.output.write(out)
        self.output.flush()

    def _do_end(self):
        amount_read = self.last_amount_read
        total_size = format_number(amount_read)

        tl = TerminalLine(8)
        # For big screens, make it more readable.
        use_hours = bool(tl.llen > 80)
        ui_time = tl.add('  %s' % format_time(self.re.elapsed_time(),
                                              use_hours))
        ui_end, not_done = _term_add_end(tl, self.size, amount_read)
        if not not_done and amount_read == 0:
            # Doesn't need to print total_size
            ui_size = tl.add(' | %5s ' % ' ')
        else:
            ui_size = tl.add(' | %5sB' % total_size)

        out = '\r%-*.*s%s%s%s\n' % (tl.rest(), tl.rest(), self.text,
                                    ui_size, ui_time, ui_end)
        self.output.write(out)
        self.output.flush()


######################################################################
# support classes and functions

class RateEstimator:
    def __init__(self, timescale=5.0):
        self.timescale = timescale
        self.total = None
        self.start_time = None
        self.last_update_time = None
        self.last_amount_read = 0
        self.ave_rate = None

    def start(self, total, now):
        self.total = total
        self.start_time = now
        self.last_update_time = now
        self.last_amount_read = 0
        self.ave_rate = None

    def update(self, amount_read, now):
        if amount_read == 0 or amount_read < self.last_amount_read:
            # if we just started this file, all bets are off
            self.last_update_time = now
            self.last_amount_read = amount_read
            self.ave_rate = None
            return

        time_diff = now - self.last_update_time
        read_diff = amount_read - self.last_amount_read
        # First update, on reget is the file size
        if self.last_amount_read:
            self.last_update_time = now
            self.ave_rate = self._temporal_rolling_ave(
                time_diff, read_diff, self.ave_rate, self.timescale)
        self.last_amount_read = amount_read

    #####################################################################
    # result methods
    def average_rate(self):
        "get the average transfer rate (in bytes/second)"
        return self.ave_rate

    def elapsed_time(self):
        "the time between the start of the transfer and the most recent update"
        return self.last_update_time - self.start_time

    def remaining_time(self):
        "estimated time remaining"
        if not self.ave_rate or not self.total:
            return None
        return (self.total - self.last_amount_read) / self.ave_rate

    def fraction_read(self):
        """the fraction of the data that has been read
        (can be None for unknown transfer size)"""
        if self.total is None:
            return None
        if self.total == 0:
            return 1.0  # pragma: no cover
        return float(self.last_amount_read) / self.total

    #########################################################################
    # support methods
    def _temporal_rolling_ave(self, time_diff, read_diff, last_ave, timescale):
        """a temporal rolling average performs smooth averaging even when
        updates come at irregular intervals.  This is performed by scaling
        the "epsilon" according to the time since the last update.
        Specifically, epsilon = time_diff / timescale

        As a general rule, the average will take on a completely new value
        after 'timescale' seconds."""
        epsilon = min(time_diff / timescale, 1.0)
        return self._rolling_ave(time_diff, read_diff, last_ave, epsilon)

    def _rolling_ave(self, time_diff, read_diff, last_ave, epsilon):
        """perform a "rolling average" iteration
        a rolling average "folds" new data into an existing average with
        some weight, epsilon.  epsilon must be between 0.0 and 1.0 (inclusive)
        a value of 0.0 means only the old value (initial value) counts,
        and a value of 1.0 means only the newest value is considered."""

        try:
            recent_rate = read_diff / time_diff
        except ZeroDivisionError:  # pragma: no cover
            recent_rate = None
        if last_ave is None:
            return recent_rate
        if recent_rate is None:
            return last_ave  # pragma: no cover

        # at this point, both last_ave and recent_rate are numbers
        return epsilon * recent_rate + (1 - epsilon) * last_ave


def format_time(seconds, use_hours=0):
    if seconds is None or seconds < 0:
        if use_hours:
            return '--:--:--'
        else:
            return '--:--'
    elif seconds == float('inf'):
        return 'Infinite'  # pragma: no cover
    else:
        seconds = int(seconds)
        minutes = seconds // 60
        seconds = seconds % 60
        if use_hours:
            hours = minutes // 60
            minutes = minutes % 60
            return '%02i:%02i:%02i' % (hours, minutes, seconds)
        else:
            return '%02i:%02i' % (minutes, seconds)


def format_number(number):
    """Turn numbers into human-readable metric-like numbers"""
    symbols = ['',  # (none)
               'k',  # kilo
               'M',  # mega
               'G',  # giga
               'T',  # tera
               'P',  # peta
               'E',  # exa
               'Z',  # zetta
               'Y']  # yotta

    step = 1024.0
    thresh = 999
    depth = 0
    max_depth = len(symbols) - 1
    number = number or 0

    # we want numbers between 0 and thresh, but don't exceed the length
    # of our list.  In that event, the formatting will be screwed up,
    # but it'll still show the right number.
    while number > thresh and depth < max_depth:
        depth = depth + 1
        number = number / step

    if isinstance(number, int):
        # it's an int or a long, which means it didn't get divided,
        # which means it's already short enough
        fmt = '%i%s%s'
    elif number < 9.95:
        # must use 9.95 for proper sizing.  For example, 9.99 will be
        # rounded to 10.0 with the .1f format string (which is too long)
        fmt = '%.1f%s%s'
    else:
        fmt = '%.0f%s%s'

    return fmt % (float(number or 0), " ", symbols[depth])