"Fossies" - the Fresh Open Source Software Archive

Member "speedometer-2.8/speedometer.py" (8 Dec 2011, 33003 Bytes) of archive /linux/misc/speedometer-2.8.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "speedometer.py" see the Fossies "Dox" file reference documentation.

    1 #!/usr/bin/python
    2 
    3 # speedometer.py
    4 # Copyright (C) 2001-2011  Ian Ward
    5 #
    6 # This module is free software; you can redistribute it and/or
    7 # modify it under the terms of the GNU Lesser General Public
    8 # License as published by the Free Software Foundation; either
    9 # version 2.1 of the License, or (at your option) any later version.
   10 #
   11 # This module is distributed in the hope that it will be useful,
   12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
   13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   14 # Lesser General Public License for more details.
   15 
   16 __version__ = "2.8"
   17 
   18 import time
   19 import sys
   20 import os
   21 import string
   22 import math
   23 import re
   24 
   25 __usage__ = """Usage: speedometer [options] tap [[-c] tap]...
   26 Monitor network traffic or speed/progress of a file transfer.  At least one
   27 tap must be entered.  -c starts a new column, otherwise taps are piled
   28 vertically.
   29 
   30 Taps:
   31   -f filename [size]          display download speed [with progress bar]
   32   -r network-interface        display bytes received on network-interface
   33   -t network-interface        display bytes transmitted on network-interface
   34   -c                          start a new column for following tap arguments
   35 
   36 Options:
   37   -b                          use old blocky display instead of smoothed
   38                               display even when UTF-8 encoding is detected
   39                               (use this if you see strange characters)
   40   -i interval-in-seconds      eg. "5" or "0.25"   default: "1"
   41   -k (1|16|88|256)            set the number of colors this terminal
   42                               supports (default 16)
   43   -l                          use linear charts instead of logarithmic
   44                               you will VERY LIKELY want to set -m as well
   45   -m chart-maximum            set the maximum bytes/second displayed on
   46                               the chart (default 2^32)
   47   -n chart-minimum            set the minimum bytes/second displayed on
   48                               the chart (default 32)
   49   -p                          use original plain-text display (one tap only)
   50   -s                          use bits/s instead of bytes/s
   51   -x                          exit when files reach their expected size
   52   -z                          report zero size on files that don't exist
   53                               instead of waiting for them to be created
   54 
   55 Note: -rx and -tx are accepted as aliases for -r and -t for compatibility
   56 with earlier releases of speedometer.  -f may be also omitted for similar
   57 reasons.
   58 """
   59 
   60 __urwid_info__ = """
   61 Speedometer requires Urwid 0.9.9.1 or later when not using plain-text display.
   62 Urwid may be downloaded from:  http://excess.org/urwid/
   63 Urwid may be installed system-wide or in the same directory as speedometer.
   64 """
   65 
   66 INITIAL_DELAY = 0.5 # seconds
   67 INTERVAL_DELAY = 1.0 # seconds
   68 
   69 VALID_NUM_COLORS = (1, 16, 88, 256)
   70 
   71 # FIXME: these globals are becoming a pain
   72 # time for more encapsulation, maybe even per-chart settings?
   73 
   74 logarithmic_scale = True
   75 units_per_second = 'bytes'
   76 chart_minimum = 2**5
   77 chart_maximum = 2**32
   78 
   79 graph_scale = None
   80 def update_scale():
   81     """
   82     parse_args has set chart min/max, units_per_second and logarithmic_scale
   83     use those settings to generate a scale of values for the LHS of the graph
   84     """
   85     global graph_scale
   86     if logarithmic_scale:
   87         # be lazy and just use the same scale we always have
   88         predefined = {
   89             'bytes': [
   90                 (2**10,  ' 1KiB\n  /s'),
   91                 (2**15, '32KiB\n  /s'),
   92                 (2**20, ' 1MiB\n  /s'),
   93                 (2**25, '32MiB\n  /s'),
   94                 (2**30, ' 1GiB\n  /s'),
   95             ], 'bits': [
   96                 (2**7,  ' 1Kib\n  /s'),
   97                 (2**12,  '32Kib\n  /s'),
   98                 (2**17, ' 1Mib\n  /s'),
   99                 (2**22, '32Mib\n  /s'),
  100                 (2**27, ' 1Gib\n  /s'),
  101             ]}
  102         graph_scale = [(s, label) for s, label in
  103             predefined[units_per_second] if chart_minimum < s < chart_maximum]
  104         return
  105 
  106     # linear, we need to generate one
  107     granularity = math.log(graph_range(), 2)
  108     granularity -= 2 # magic number, creates at least 4 lines on the scale
  109     granularity = 2**int(granularity) # only want proper powers of two
  110 
  111     n, r = divmod(chart_minimum, granularity)
  112     n = n * granularity + (granularity if r else 0)
  113     graph_scale = []
  114     while n < chart_maximum:
  115         graph_scale.append((n, readable_speed(n)))
  116         n += granularity
  117 
  118 
  119 
  120 def graph_min():
  121     return math.log(chart_minimum,2) if logarithmic_scale else chart_minimum
  122 
  123 def graph_max():
  124     return math.log(chart_maximum,2) if logarithmic_scale else chart_maximum
  125 
  126 def graph_range(): return graph_max() - graph_min()
  127 
  128 def graph_lines_captions():
  129     s = graph_scale
  130     if logarithmic_scale:
  131         s = [(math.log(x, 2), cap) for x, cap in s]
  132         # XXX: quick hack to make this work like it used to
  133         delta = graph_min()
  134         s = [(x - delta, cap) for x, cap in s]
  135     return list(reversed(s))
  136 
  137 def graph_lines(): return [x[0] for x in graph_lines_captions()]
  138 
  139 URWID_IMPORTED = False
  140 URWID_UTF8 = False
  141 try:
  142     import urwid
  143     if urwid.VERSION >= (0, 9, 9, 1):
  144         URWID_IMPORTED = True
  145         URWID_UTF8 = urwid.get_encoding_mode() == "utf8"
  146 except (ImportError, AttributeError):
  147     pass
  148 
  149 
  150 class Speedometer:
  151     def __init__(self,maxlog=5):
  152         """speedometer(maxlog=5)
  153         maxlog is the number of readings that will be stored"""
  154         self.log = []
  155         self.start = None
  156         self.maxlog = maxlog
  157 
  158     def get_log(self):
  159         return self.log
  160 
  161     def update(self, bytes):
  162         """update(bytes) => None
  163         add a byte reading to the log"""
  164         t = time.time()
  165         reading = (t,bytes)
  166         if not self.start: self.start = reading
  167         self.log.append(reading)
  168         self.log = self.log[ - (self.maxlog+1):]
  169 
  170     def delta(self, readings=0, skip=0):
  171         """delta(readings=0) -> time passed, byte increase
  172         if readings is 0, time since start is given
  173         don't include the last 'skip' readings
  174         None is returned if not enough data available"""
  175         assert readings >= 0
  176         assert readings <= self.maxlog, "Log is not long enough to satisfy request"
  177         assert skip >= 0
  178         if skip > 0: assert readings > 0, "Can't skip when reading all"
  179 
  180         if skip > len(self.log)-1: return # not enough data
  181         current = self.log[-1 -skip]
  182 
  183         target = None
  184         if readings == 0: target = self.start
  185         elif len(self.log) > readings+skip:
  186             target = self.log[-(readings+skip+1)]
  187         if not target: return  # not enough data
  188 
  189         if target == current: return
  190         byte_increase = current[1]-target[1]
  191         time_passed = current[0]-target[0]
  192         return time_passed, byte_increase
  193 
  194     def speed(self, *l, **d):
  195         d = self.delta(*l, **d)
  196         if d:
  197             return delta_to_speed(d)
  198 
  199 
  200 class EndOfData(Exception):
  201     pass
  202 
  203 class MultiGraphDisplay:
  204     def __init__(self, cols, urwid_ui, exit_on_complete):
  205         smoothed = urwid_ui == "smoothed"
  206         self.displays = []
  207         l = []
  208         for c in cols:
  209             a = []
  210             for tap in c:
  211                 if tap.ftype == 'file_exp':
  212                     d = GraphDisplayProgress(tap, smoothed)
  213                 else:
  214                     d = GraphDisplay(tap, smoothed)
  215                 a.append(d)
  216                 self.displays.append(d)
  217             l.append(a)
  218 
  219         graphs = urwid.Columns([urwid.Pile(a) for a in l], 1)
  220         graphs = urwid.AttrWrap(graphs, 'background')
  221         title = urwid.Text("Speedometer "+__version__)
  222         title = urwid.AttrWrap(urwid.Filler(title), 'title')
  223         self.top = urwid.Overlay(title, graphs,
  224             ('fixed left', 5), 16, ('fixed top', 0), 1)
  225 
  226         self.urwid_ui = urwid_ui
  227         self.exit_on_complete = exit_on_complete
  228 
  229     palette = [
  230         # name,        16-color fg, bg,         mono fg,    88/256-color fg, bg
  231         # main bar graph
  232         ('background', 'dark gray', '',         '',         '#008', '#ddb',),
  233         ('bar:top',    'dark cyan', '',         '',         '#488', '#ddb'),
  234         ('bar',        '',          'dark cyan','standout', '#008', '#488'),
  235         ('bar:num',    '',          '',         '',         '#066', '#ddb'),
  236         # latest "curved" + average bar graph at right side
  237         ('ca:background', '',       '',         '',         'g66',  '#ddb'),
  238         ('ca:c:top',   'dark blue', '',         '',         '#66d', '#ddb'),
  239         ('ca:c',       '',          'dark blue','standout', 'g66',  '#66d'),
  240         ('ca:c:num',   'light blue','',         '',         '#66d', '#ddb'),
  241         ('ca:a:top',   'light gray','',         '',         '#6b6', '#ddb'),
  242         ('ca:a',       '',          'light gray','standout','g66',  '#6b6'),
  243         ('ca:a:num',   'light gray','',          'bold',    '#6b6', '#ddb'),
  244         # text headings and numeric values displayed
  245         ('title',      '',          '',   'underline,bold', '#000', '#ddb'),
  246         ('reading',    '',          '',         '',         '#886', '#ddb'),
  247         # progress bar
  248         ('pr:n',       '',          'dark blue','',         'g11', '#bb6'),
  249         ('pr:c',       '',          'dark green','standout','g11', '#fd0'),
  250         ('pr:cn',      'dark green','dark blue','',         '#fd0', '#bb6'),
  251         ]
  252 
  253 
  254     def main(self, num_colors):
  255         self.loop = urwid.MainLoop(self.top, palette=self.palette,
  256             unhandled_input=self.unhandled_input)
  257         self.loop.screen.set_terminal_properties(colors=num_colors)
  258 
  259         try:
  260             pending = self.update_readings()
  261             if self.exit_on_complete and pending == 0: return
  262         except EndOfData:
  263             return
  264         time.sleep(INITIAL_DELAY)
  265         self.update_callback()
  266         self.loop.run()
  267 
  268     def unhandled_input(self, key):
  269         "Exit on Q or ESC"
  270         if key in ('q', 'Q', 'esc'):
  271             raise urwid.ExitMainLoop()
  272 
  273     def update_callback(self, *args):
  274         next_call_in = INTERVAL_DELAY
  275         if isinstance(time, SimulatedTime):
  276             next_call_in = 0
  277             time.sleep(INTERVAL_DELAY) # update simulated time
  278 
  279         self.loop.set_alarm_in(next_call_in, self.update_callback)
  280         try:
  281             pending = self.update_readings()
  282             if self.exit_on_complete and pending == 0: return
  283         except EndOfData:
  284             self.end_of_data()
  285             raise urwid.ExitMainLoop()
  286 
  287     def update_readings(self):
  288         pending = 0
  289         for d in self.displays:
  290             if d.update_readings(): pending += 1
  291         return pending
  292 
  293     def end_of_data(self):
  294         # pause for taking screenshot of simulated data
  295         if isinstance(time, SimulatedTime):
  296             while not self.loop.screen.get_input():
  297                 pass
  298 
  299 
  300 class GraphDisplay:
  301     def __init__(self,tap, smoothed):
  302         if smoothed:
  303             self.speed_graph = SpeedGraph(
  304                 ['background','bar'],
  305                 ['background','bar'],
  306                 {(1,0):'bar:top'})
  307 
  308             self.cagraph = urwid.BarGraph(
  309                 ['ca:background', 'ca:c', 'ca:a'],
  310                 ['ca:background', 'ca:c', 'ca:a'],
  311                 {(1,0):'ca:c:top', (2,0):'ca:a:top', })
  312         else:
  313             self.speed_graph = SpeedGraph([
  314                 ('background', ' '), ('bar', ' ')],
  315                 ['background', 'bar'])
  316 
  317             self.cagraph = urwid.BarGraph([
  318                 ('ca:background', ' '),
  319                 ('ca:c',' '),
  320                 ('ca:a',' '),]
  321            )
  322 
  323         self.last_reading = urwid.Text("",align="right")
  324         scale = urwid.GraphVScale(graph_lines_captions(), graph_range())
  325         footer = self.last_reading
  326         graph_cols = urwid.Columns([('fixed', 5, scale),
  327             self.speed_graph, ('fixed', 4, self.cagraph)],
  328             dividechars = 1)
  329         self.top = urwid.Frame(graph_cols, footer=footer)
  330 
  331         self.spd = Speedometer(6)
  332         self.feed = tap.feed
  333         self.description = tap.description()
  334 
  335     def selectable(self):
  336         return False
  337 
  338     def render(self, size, focus=False):
  339         return self.top.render(size,focus)
  340 
  341     def update_readings(self):
  342         f = self.feed()
  343         if f is None: raise EndOfData
  344         self.spd.update(f)
  345         s = self.spd.speed(1) # last sample
  346         c = curve(self.spd) # "curved" reading
  347         a = self.spd.speed() # running average
  348         self.speed_graph.append_log(s)
  349 
  350         self.last_reading.set_text([
  351             ('title', [self.description, "  "]),
  352             ('bar:num', [readable_speed(s), " "]),
  353             ('ca:c:num',[readable_speed(c), " "]),
  354             ('ca:a:num',readable_speed(a)) ])
  355 
  356         self.cagraph.set_data([
  357             [speed_scale(c),0],
  358             [0,speed_scale(a)],
  359             ], graph_range())
  360 
  361 
  362 
  363 class GraphDisplayProgress(GraphDisplay):
  364     def __init__(self, tap, smoothed):
  365         GraphDisplay.__init__(self, tap, smoothed)
  366 
  367         self.spd = FileProgress(6, tap.expected_size)
  368         if smoothed:
  369             self.pb = urwid.ProgressBar('pr:n','pr:c',0,
  370                 tap.expected_size, 'pr:cn')
  371         else:
  372             self.pb = urwid.ProgressBar('pr:n','pr:c',0,
  373                 tap.expected_size)
  374         self.est = urwid.Text("")
  375         pbest = urwid.Columns([self.pb,('fixed',10,self.est)], 1)
  376         newfoot = urwid.Pile([self.top.footer, pbest])
  377         self.top.footer = newfoot
  378 
  379     def update_readings(self):
  380         GraphDisplay.update_readings(self)
  381 
  382         current, expected = self.spd.progress()
  383         self.pb.set_completion(current)
  384         e = self.spd.completion_estimate()
  385         if e is not None:
  386             self.est.set_text(readable_time(e,10))
  387         return current < expected
  388 
  389 class SpeedGraph:
  390     def __init__(self, attlist, hatt=None, satt=None):
  391         if satt is None:
  392             self.graph = urwid.BarGraph(attlist, hatt)
  393         else:
  394             self.graph = urwid.BarGraph(attlist, hatt, satt)
  395         # override BarGraph's get_data
  396         self.graph.get_data = self.get_data
  397 
  398         self.smoothed = satt is not None
  399 
  400         self.log = []
  401         self.bar = []
  402 
  403     def get_data(self, (maxcol,maxrow)):
  404         bar = self.bar[-maxcol:]
  405         if len(bar) < maxcol:
  406             bar = [[0]]*(maxcol-len(bar)) + bar
  407         return bar, graph_range(), graph_lines()
  408 
  409     def selectable(self):
  410         return False
  411 
  412     def render(self, (maxcol, maxrow), focus=False):
  413 
  414         left = max(0, len(self.log)-maxcol)
  415         pad = maxcol-(len(self.log)-left)
  416 
  417         topl = self.local_maximums(pad, left)
  418         yvals = [ max(self.bar[i]) for i in topl ]
  419         yvals = urwid.scale_bar_values(yvals, graph_range(), maxrow)
  420 
  421         graphtop = self.graph
  422         for i,y in zip(topl, yvals):
  423             s = self.log[ i ]
  424             txt = urwid.Text(readable_speed(s))
  425             label = urwid.AttrWrap(urwid.Filler(txt), 'reading')
  426 
  427             graphtop = urwid.Overlay(label, graphtop,
  428                 ('fixed left', pad+i-4-left), 10,
  429                 ('fixed top', max(0,y-2)), 1)
  430 
  431         return graphtop.render((maxcol, maxrow), focus)
  432 
  433     def local_maximums(self, pad, left):
  434         """
  435         Generate a list of indexes for the local maximums in self.log
  436         """
  437         ldist, rdist = 4,5
  438         l = self.log
  439         if len(l) <= ldist+rdist:
  440             return []
  441 
  442         dist = ldist+rdist
  443         highs = []
  444 
  445         for i in range(left+max(0, ldist-pad),len(l)-rdist+1):
  446             li = l[i]
  447             if li == 0: continue
  448             if i and l[i-1]>=li: continue
  449             if l[i+1]>li: continue
  450             highs.append((li, -i))
  451 
  452         highs.sort()
  453         highs.reverse()
  454         tag = [False]*len(l)
  455         out = []
  456 
  457         for li, i in highs:
  458             i=-i
  459             if tag[i]: continue
  460             for k in range(max(0,i-dist), min(len(l),i+dist)):
  461                 tag[k]=True
  462             out.append(i)
  463 
  464         return out
  465 
  466     def append_log(self, s):
  467         x = speed_scale(s)
  468         o = [x]
  469         self.bar = self.bar[-300:] + [o]
  470         self.log = self.log[-300:] + [s]
  471 
  472 
  473 def speed_scale(s):
  474     if s <= 0: return 0
  475     if logarithmic_scale:
  476         s = math.log(s, 2)
  477     s = min(graph_range(), max(0, s-graph_min()))
  478     return s
  479 
  480 
  481 def delta_to_speed(delta):
  482     """delta_to_speed(delta) -> speed in bytes per second"""
  483     time_passed, byte_increase = delta
  484     if time_passed <= 0: return 0
  485     if long(time_passed*1000) == 0: return 0
  486 
  487     return long(byte_increase*1000)/long(time_passed*1000)
  488 
  489 
  490 
  491 def readable_speed(speed):
  492     """
  493     readable_speed(speed) -> string
  494     speed is in bytes per second
  495     returns a readable version of the speed given
  496     """
  497 
  498     if speed == None or speed < 0: speed = 0
  499 
  500     units = "B/s  ", "KiB/s", "MiB/s", "GiB/s", "TiB/s"
  501     step = 1L
  502 
  503     for u in units:
  504 
  505         if step > 1:
  506             s = "%4.2f " %(float(speed)/step)
  507             if len(s) <= 5: return s + u
  508             s = "%4.1f " %(float(speed)/step)
  509             if len(s) <= 5: return s + u
  510 
  511         if speed/step < 1024:
  512             return "%4d " %(speed/step) + u
  513 
  514         step = step * 1024L
  515 
  516     return "%4d " % (speed/(step/1024)) + units[-1]
  517 
  518 
  519 def readable_speed_bits(speed):
  520     """
  521     bits/s version of readable_speed()
  522     """
  523     if speed == None or speed < 0: speed = 0
  524 
  525     speed = speed * 8
  526     units = "b/s  ", "Kib/s", "Mib/s", "Gib/s", "Tib/s"
  527     step = 1L
  528 
  529     for u in units:
  530 
  531         if step > 1:
  532             s = "%4.2f " %(float(speed)/step)
  533             if len(s) <= 5: return s + u
  534             s = "%4.1f " %(float(speed)/step)
  535             if len(s) <= 5: return s + u
  536 
  537         if speed/step < 1024:
  538             return "%4d " %(speed/step) + u
  539 
  540         step = step * 1024L
  541 
  542     return "%4d " % (speed/(step/1024)) + units[-1]
  543 
  544 
  545 
  546 
  547 def graphic_speed(speed):
  548     """graphic_speed(speed) -> string
  549     speed is bytes per second
  550     returns a graphic representing given speed"""
  551 
  552     if speed == None: speed = 0
  553 
  554     speed_val = [0]+[int(2**(x*5.0/3)) for x in range(20)]
  555 
  556     speed_gfx = [
  557         r"\                    ",
  558         r".\                   ",
  559         r"..\                  ",
  560         r"...\                 ",
  561         r"...:\                ",
  562         r"...::\               ",
  563         r"...:::\              ",
  564         r"...:::+|             ",
  565         r"...:::++|            ",
  566         r"...:::+++|           ",
  567         r"...:::+++#|          ",
  568         r"...:::+++##|         ",
  569         r"...:::+++###|        ",
  570         r"...:::+++###%|       ",
  571         r"...:::+++###%%/      ",
  572         r"...:::+++###%%%/     ",
  573         r"...:::+++###%%%//    ",
  574         r"...:::+++###%%%///   ",
  575         r"...:::+++###%%%////  ",
  576         r"...:::+++###%%%///// ",
  577         r"...:::+++###%%%//////",
  578         ]
  579 
  580 
  581     for i in range(len(speed_val)-1):
  582         low, high = speed_val[i], speed_val[i+1]
  583         if speed > high: continue
  584         if speed - low < high - speed:
  585             return speed_gfx[i]
  586         else:
  587             return speed_gfx[i+1]
  588 
  589     return speed_gfx[-1]
  590 
  591 
  592 
  593 def file_size_feed(filename):
  594     """file_size_feed(filename) -> function that returns given file's size"""
  595     def sizefn(filename=filename,os=os):
  596         try:
  597             return os.stat(filename)[6]
  598         except:
  599             return 0
  600     return sizefn
  601 
  602 def network_feed(device,rxtx):
  603     """network_feed(device,rxtx) -> function that returns given device stream speed
  604     rxtx is "RX" or "TX"
  605     """
  606     assert rxtx in ["RX","TX"]
  607     r = re.compile(r"^\s*" + re.escape(device) + r":(.*)$", re.MULTILINE)
  608 
  609     def networkfn(devre=r,rxtx=rxtx):
  610         f = open('/proc/net/dev')
  611         dev_lines = f.read()
  612         f.close()
  613         match = devre.search(dev_lines)
  614         if not match:
  615             return None
  616 
  617         parts = match.group(1).split()
  618         if rxtx == 'RX':
  619             return long(parts[0])
  620         else:
  621             return long(parts[8])
  622 
  623     return networkfn
  624 
  625 def simulated_feed(data):
  626     total = 0
  627     adjusted_data = [0]
  628     for d in data:
  629         d = int(d)
  630         adjusted_data.append(d + total)
  631         total += d
  632 
  633     def simfn(data=adjusted_data):
  634         if data:
  635             return long(data.pop(0))
  636         return None
  637     return simfn
  638 
  639 class SimulatedTime:
  640     def __init__(self, start):
  641         self.t = start
  642     def sleep(self, length):
  643         self.t += length
  644     def time(self):
  645         return self.t
  646 
  647 
  648 class FileProgress:
  649     """FileProgress monitors a file's size vs time and expected size to
  650     produce progress and estimated completion time readings"""
  651 
  652     samples_for_estimate = 4
  653 
  654     def __init__(self, maxlog, expected_size):
  655         """FileProgress(expected_size)
  656         expected_size is the file's expected size in bytes"""
  657 
  658         self.expected_size = expected_size
  659         self.speedometer = Speedometer(maxlog)
  660         self.current_size = None
  661         self.speed = self.speedometer.speed
  662         self.delta = self.speedometer.delta
  663 
  664     def update(self, current_size):
  665         """update(current_size)
  666         current_size is the current file size
  667         update will record the current size and time"""
  668 
  669         self.current_size = current_size
  670         self.speedometer.update(self.current_size)
  671 
  672     def progress(self):
  673         """progress() -> (current size, expected size)
  674         current size will be None until update is called"""
  675 
  676         return self.current_size, self.expected_size
  677 
  678     def completion_estimate(self):
  679         """completion_estimate() -> estimated seconds remaining
  680         will return None if not enough data is available"""
  681 
  682         d = self.speedometer.delta(self.samples_for_estimate)
  683         if not d: return None  # not enough readings
  684         (seconds,bytes) = d
  685         if bytes <= 0: return None  # currently stalled
  686         remaining = self.expected_size - self.current_size
  687         if remaining <= 0: return 0  # all done -- no time remaining
  688 
  689         seconds_left = float(remaining)*seconds/bytes
  690 
  691         return seconds_left
  692 
  693     def average_speed(self):
  694         """average_speed() -> bytes per second since start
  695         will return None if not enough data"""
  696         return self.speedometer.speed()
  697 
  698     def current_speed(self):
  699         """current_speed() -> latest bytes per second reading
  700         will return None if not enough data"""
  701         return self.speedometer.speed(1)
  702 
  703 
  704 
  705 def graphic_progress(progress, columns):
  706     """graphic_progress(progress, columns) -> string
  707     progress is a tuple of (value, max)
  708     columns is length of string returned
  709     returns a graphic representation of value vs. max"""
  710     value, max = progress
  711 
  712     f = float(value) / float(max)
  713     if f > 1: f = 1
  714     if f < 0: f = 0
  715 
  716     filled = int(f*columns)
  717     gfx = "#" * filled + "-" * (columns-filled)
  718 
  719     return gfx
  720 
  721 
  722 def time_as_units(seconds):
  723     """time_units(seconds) -> list of (count, suffix) tuples
  724     returns a unit breakdown for the given number of seconds"""
  725 
  726     if seconds==None: seconds=0
  727 
  728     # (multiplicative factor, suffix)
  729     units = (1,"s"), (60,"m"), (60,"h"), (24,"d"), (7,"w"), (52,"y")
  730 
  731     scale = 1L
  732     topunit = -1
  733     # find the top unit to use
  734     for mul, suf in units:
  735         if seconds / (scale*mul) < 1: break
  736         topunit = topunit+1
  737         scale = scale * mul
  738 
  739     # build the list reading backwards from top unit
  740     out = []
  741     for i in range(topunit, -1, -1):
  742         mul,suf = units[i]
  743         value = int(seconds/scale)
  744         seconds = seconds - value * scale
  745         scale = scale / mul
  746         out.append((value, suf))
  747 
  748     return out
  749 
  750 
  751 def readable_time(seconds, columns=None):
  752     """readable_time(seconds, columns=None) -> string
  753     return the seconds as a readable string
  754     if specified, columns is the maximum length of the returned string"""
  755 
  756     out = ""
  757     for value, suf in time_as_units(seconds):
  758         new_out = out
  759         if out: new_out = new_out + ' '
  760         new_out = new_out + `value` + suf
  761         if columns and len(new_out) > columns: break
  762         out = new_out
  763 
  764     return out
  765 
  766 
  767 class ArgumentError(Exception):
  768     pass
  769 
  770 
  771 def console():
  772     """Console mode"""
  773     try:
  774         cols, urwid_ui, zero_files, exit_on_complete, num_colors = parse_args()
  775     except ArgumentError:
  776         sys.stderr.write(__usage__)
  777         if not URWID_IMPORTED:
  778             sys.stderr.write(__urwid_info__)
  779         sys.stderr.write("""
  780 Python Version: %d.%d
  781 Urwid >= 0.9.9.1 detected: %s  UTF-8 encoding detected: %s
  782 """ % (sys.version_info[:2] + (["NO","yes"][URWID_IMPORTED],) +
  783         (["NO","yes"][URWID_UTF8],)))
  784         return
  785 
  786     update_scale()
  787 
  788     if zero_files:
  789         for c in cols:
  790             a = []
  791             for tap in c:
  792                 if hasattr(tap, 'report_zero'):
  793                     tap.report_zero()
  794 
  795     try:
  796         # wait for every tap to be able to read
  797         wait_all(cols)
  798     except KeyboardInterrupt:
  799         return
  800 
  801     # plain-text mode
  802     if not urwid_ui:
  803         [[tap]] = cols
  804 
  805         if tap.ftype == 'file_exp':
  806             do_progress(tap.feed, tap.expected_size, exit_on_complete)
  807         else:
  808             do_simple(tap.feed)
  809         return
  810 
  811     do_display(cols, urwid_ui, exit_on_complete, num_colors)
  812 
  813 
  814 def do_display(cols, urwid_ui, exit_on_complete, num_colors):
  815     mg = MultiGraphDisplay(cols, urwid_ui, exit_on_complete)
  816     mg.main(num_colors)
  817 
  818 
  819 class FileTap:
  820     def __init__(self, name):
  821         self.ftype = 'file'
  822         self.file_name = name
  823         self.feed = file_size_feed(name)
  824         self.wait = True
  825 
  826     def set_expected_size(self, size):
  827         self.expected_size = long(size)
  828         self.ftype = 'file_exp'
  829 
  830     def report_zero(self):
  831         self.wait = False
  832 
  833     def description(self):
  834         return "FILE: "+ self.file_name
  835 
  836     def wait_creation(self):
  837         if not self.wait:
  838             return
  839 
  840         if not os.path.exists(self.file_name):
  841             sys.stdout.write("Waiting for '%s' to be created...\n"
  842                 % self.file_name)
  843             while not os.path.exists(self.file_name):
  844                 time.sleep(1)
  845 
  846 class NetworkTap:
  847     def __init__(self, rxtx, interface):
  848         self.ftype = rxtx
  849         self.interface = interface
  850         self.feed = network_feed(interface, rxtx)
  851 
  852     def description(self):
  853         return self.ftype+": "+self.interface
  854 
  855     def wait_creation(self):
  856         if self.feed() is None:
  857             sys.stdout.write("Waiting for network statistics from "
  858                 "interface '%s'...\n" % self.interface)
  859             while self.feed() == None:
  860                 time.sleep(1)
  861 
  862 
  863 
  864 def parse_args():
  865     args = sys.argv[1:]
  866     tap = None
  867     if URWID_UTF8:
  868         urwid_ui = 'smoothed'
  869     elif URWID_IMPORTED:
  870         urwid_ui = 'blocky'
  871     else:
  872         urwid_ui = False
  873     zero_files = False
  874     interval_set = False
  875     exit_on_complete = False
  876     num_colors = 16
  877     colors_set = False
  878     cols = []
  879     taps = []
  880 
  881     def push_tap(tap, taps):
  882         if tap is None: return
  883         taps.append(tap)
  884 
  885     i = 0
  886     while i < len(args):
  887         op = args[i]
  888         if op in ("-h","--help"):
  889             raise ArgumentError
  890         elif op in ("-i","-r","-rx","-t","-tx","-f","-k","-m","-n"):
  891             # combine two part arguments with the following argument
  892             try:
  893                 if op != "-f": # keep support for -f being optional
  894                     args[i+1] = op + args[i+1]
  895             except IndexError:
  896                 raise ArgumentError
  897             push_tap(tap, taps)
  898             tap = None
  899         elif op == "-S":
  900             # undocumented simulation option
  901             simargs = []
  902             i += 1
  903             while i < len(args) and args[i][:1] != "-":
  904                 simargs.append(args[i])
  905                 i += 1
  906             simulate = tap
  907             if not simulate:
  908                 simulate = taps[-1]
  909             simulate.feed = simulated_feed(simargs)
  910             global time
  911             time = SimulatedTime(time.time())
  912             continue
  913         elif op == "-p":
  914             # disable urwid ui
  915             urwid_ui = False
  916         elif op == "-b":
  917             urwid_ui = 'blocky'
  918         elif op == "-s":
  919             global readable_speed
  920             global units_per_second
  921             readable_speed = readable_speed_bits
  922             units_per_second = 'bits'
  923         elif op == "-x":
  924             exit_on_complete = True
  925         elif op == "-z":
  926             zero_files = True
  927         elif op[:2] == "-k":
  928             if colors_set: raise ArgumentError
  929             try:
  930                 num_colors = int(op[2:])
  931                 assert num_colors in VALID_NUM_COLORS
  932             except:
  933                 raise ArgumentError
  934             colors_set = True
  935 
  936         elif op[:2] == "-i":
  937             if interval_set: raise ArgumentError
  938 
  939             global INTERVAL_DELAY
  940             global INITIAL_DELAY
  941             try:
  942                 INTERVAL_DELAY = float(op[2:])
  943             except:
  944                 raise ArgumentError
  945 
  946             if INTERVAL_DELAY<INITIAL_DELAY:
  947                 INITIAL_DELAY=INTERVAL_DELAY
  948             interval_set = True
  949 
  950         elif op == "-l":
  951             global logarithmic_scale
  952             logarithmic_scale = False
  953         elif op.startswith("-m"):
  954             global chart_maximum
  955             try:
  956                 chart_maximum = int(op[2:])
  957             except:
  958                 raise ArgumentError
  959 
  960         elif op.startswith("-n"):
  961             global chart_minimum
  962             try:
  963                 chart_minimum = int(op[2:])
  964             except:
  965                 raise ArgumentError
  966 
  967         elif op.startswith("-rx"):
  968             push_tap(tap, taps)
  969             tap = NetworkTap("RX", op[3:])
  970         elif op.startswith("-r"):
  971             push_tap(tap, taps)
  972             tap = NetworkTap("RX", op[2:])
  973         elif op.startswith("-tx"):
  974             push_tap(tap, taps)
  975             tap = NetworkTap("TX", op[3:])
  976         elif op.startswith("-t"):
  977             push_tap(tap, taps)
  978             tap = NetworkTap("TX", op[2:])
  979         elif op == "-c":
  980             push_tap(tap, taps)
  981             if not taps:
  982                 raise ArgumentError
  983             cols.append(taps)
  984             taps = []
  985             tap = None
  986         elif tap == None:
  987             tap = FileTap(op)
  988         elif tap and tap.ftype == 'file':
  989             try:
  990                 tap.set_expected_size(op)
  991                 push_tap(tap, taps)
  992                 tap = None
  993             except:
  994                 raise ArgumentError
  995         else:
  996             raise ArgumentError
  997 
  998         i += 1
  999 
 1000     if urwid_ui and not URWID_IMPORTED:
 1001         raise ArgumentError
 1002 
 1003     push_tap(tap, taps)
 1004     if not urwid_ui and (len(taps)>1 or cols):
 1005         raise ArgumentError
 1006 
 1007     if not taps:
 1008         raise ArgumentError
 1009     cols.append(taps)
 1010 
 1011     if chart_maximum <= chart_minimum:
 1012         raise ArgumentError
 1013 
 1014     return cols, urwid_ui, zero_files, exit_on_complete, num_colors
 1015 
 1016 
 1017 def do_simple(feed):
 1018     try:
 1019         spd = Speedometer(6)
 1020         f = feed()
 1021         if f is None: return
 1022         spd.update(f)
 1023         time.sleep(INITIAL_DELAY)
 1024         while 1:
 1025             f = feed()
 1026             if f is None: return
 1027             spd.update(f)
 1028             s = spd.speed(1) # last sample
 1029             c = curve(spd) # "curved" reading
 1030             a = spd.speed() # running average
 1031             show(s,c,a)
 1032             time.sleep(INTERVAL_DELAY)
 1033     except KeyboardInterrupt:
 1034         pass
 1035 
 1036 def curve(spd):
 1037     """Try to smooth speed fluctuations"""
 1038     val = [6, 5, 4, 3, 2, 1] # speed sampling relative weights
 1039     wtot = 0 # total weighting
 1040     ws = 0.0 # weighted speed
 1041     for i in range(len(val)):
 1042         d = spd.delta(1,i)
 1043         if d==None:
 1044             break # ran out of data
 1045         t, b = d
 1046         v = val[i]
 1047         wtot += v
 1048         ws += float(b)*v/t
 1049     return delta_to_speed((wtot, ws))
 1050 
 1051 
 1052 def show(s, c, a, out = sys.stdout.write):
 1053     out(readable_speed(s))
 1054     out("  c:" + readable_speed(c))
 1055     out("  A:" + readable_speed(a))
 1056     out("  (" + graphic_speed(s)+")")
 1057     out('\n')
 1058 
 1059 
 1060 def do_progress(feed, size, exit_on_complete):
 1061     try:
 1062         fp = FileProgress(4, long(size))
 1063         out = sys.stdout.write
 1064 
 1065         f = feed()
 1066         if f is None: return
 1067         fp.update(f)
 1068         time.sleep(INITIAL_DELAY)
 1069         while 1:
 1070             f = feed()
 1071             if f is None: return
 1072             fp.update(f)
 1073             out('('+graphic_speed(fp.current_speed())+')')
 1074             out(readable_speed(fp.current_speed()))
 1075             out(' ['+graphic_progress(fp.progress(), 36)+']')
 1076             out('  '+readable_time(fp.completion_estimate()))
 1077             out('\n')
 1078             current, expected = fp.progress()
 1079             if exit_on_complete and current >= expected: break
 1080             time.sleep(INTERVAL_DELAY)
 1081     except KeyboardInterrupt:
 1082         pass
 1083 
 1084 
 1085 def wait_all(cols):
 1086     for c in cols:
 1087         for tap in c:
 1088             tap.wait_creation()
 1089 
 1090 
 1091 if __name__ == "__main__":
 1092     try:
 1093         console()
 1094     except KeyboardInterrupt, err:
 1095         pass
 1096