null

Делаем ProgressBar для gzip на Python

Как известно, замечательные утилитки типа gzip или tar настолько брутальны, что выводят о статусе распаковки на консоль ровно 0 байт информации (ну кроме ошибок разумеется :-) ). И когда распаковываешь архив в 15 гигабайт со вкуснятиной, хотелось бы все таки знать - когда же оно распакуется, поэтому я написал небольшой скрипт на python, "решающий" эту проблему.  Вообще говоря gzip и подобные - утилиты однопоточные и читают файл последовательно, так что определить прогресс несложно, это можно например сделать с помощью утилиты lsof:

myaut@myaut-leo:~> lsof -p `pgrep gzip` | grep '211447.iso$'
gzip    2512 myaut    4w   REG    8,3 271351808 15336024 /home/myaut/shared/ISO/211447.iso
myaut@myaut-leo:~> lsof -o -p `pgrep gzip` | grep '211447.iso$'
gzip    2512 myaut    4w   REG    8,3 0x1a4d0000 15336024 /home/myaut/shared/ISO/211447.iso

Получаем информацию о статусе gzip

Итак, 8е поле - имя файла, а 6е - его размер. Добавляя опцию -o в lsof, мы получаем смещение файлового указателя. Парсим вывод lsof:

def lsof(pid, fn, off):
    """lsof - возвращает размер или смещение открытого процессом файла
    pid - pid процесса
    fn - базовое имя файла
    off - если True возвращает смещение, если False - размер"""
    
    if off:
        cmd = "lsof -o -p %s" % pid
    else:
        cmd = "lsof -p %s" % pid
    
    lsof = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
    
    for line in lsof.stdout.readlines():
        cols = line.strip().split()
        
        if len(cols) < 9:        # Wrong format - ignore
            continue
        
        # Compare basenames
        if fn == os.path.basename(cols[COL_FILENAME]):    
            if '0x' in cols[COL_SIZE_OFF]:
                return int(cols[COL_SIZE_OFF], 16) 
            elif '0t' in cols[COL_SIZE_OFF]:
                return int(cols[COL_SIZE_OFF][2:], 10) 
            else:
                return int(cols[COL_SIZE_OFF], 10) 
                
    return None

Теперь вызовем ps, чтобы получить pid'ы всех gzip'ов и имена открытых ими файлов (указываются в качестве последнего аргумента вызова gzip):

def proclist():
    pslist = []
    
    ps = subprocess.Popen("ps -o pid,cmd h", shell=True, stdout=subprocess.PIPE);
    os.waitpid(ps.pid, 0)
    
    for line in ps.stdout.readlines():
        cols = line.strip().split(' ', 1)
        pid = cols[0]
        cmd = cols[1]
        
        if 'gzip' in cmd:
            # For gzip filename is last option
            fn = cmd.split(' ')[-1]
            fn = os.path.basename(fn)
            pslist.append({"pid": pid, "fn": fn, "cmd": cmd})
            

Рисуем ProgressBar

Для рисования ProgressBar я взял модуль progressbar: http://pypi.python.org/pypi/progressbar/2.2 Замечу, что на оффициальном сайте проекта доступна версия 2.3. Так как свой скрипт я запускаю асинхронно по отношению к процессам gzip, общее время ожидания (ETA) и скорость считать бесполезно, зато имеет смысл добавить виджет, показывающий, насколько выполнена упаковка в абсолютных величинах:

def nicesz(sz):
    sz = float(sz)
    fmt = '%.2f%s'
    units = ['B','K','M','G','T','P']

    for u in units:
        if sz < 1024:
            break
        sz /= 1024
    return fmt % (sz, u)

class FileSizeWidget(ProgressBarWidget):
    def __init__(self, start):
        self.start = nicesz(start)

    def update(self, pbar):
        return nicesz(pbar.currval) + '/' + self.start

Также, если ProgressBar при обновлении просто перетирает старую строчку, он не может работать в многострочном режиме. Поэтому я слегка изменил его функцию update (см. приложенный патч).

И наконец, главный цикл. Сначала получаем список процессов, потом получаем размер файла, и наконец циклически выводим прогрессбары для каждого процесса пока они не завершатся (в этом случае lsof() вернет None и proc будет удален из списка):

pslist = proclist()

os.system("clear")

# Создаем прогрессбары
for proc in pslist:
    # Получаем размер файла через lsof
    fsize = lsof(proc["pid"], proc["fn"], False)

    if not fsize:
        pslist.remove(proc)
        continue

    proc["fsize"] = fsize

    #Формат: CMD: XX% |############### | 1G/10.5G
    widgets = [proc["cmd"] + ': ', Percentage(), ' ', Bar(marker='#'), ' ', FileSizeWidget(fsize)]
    proc["pbar"] = ProgressBar(widgets=widgets, maxval=fsize).start()

while pslist:
    # Переводим курсор на позицию 0;0
    # Специфичная для терминала Escape-последовательность
    sys.stderr.write('\x1b[0;0H')

    for proc in pslist:
        foff = lsof(proc["pid"], proc["fn"], True)

        if not foff:
            # Завершаем рисовать ProgressBar
            proc["pbar"].finish()
            # Удаляем процесс из списка
            pslist.remove(proc)
            os.system("clear")
        else:
            proc["pbar"].update(foff)

    # Спим
    time.sleep(0.5)

Тестирование и результаты

Пример использования я выложил на youtube: http://www.youtube.com/watch?v=A9l57YfE6-s

Прикладываю файлы: filebar.py.gz и progressbar.patch.gz.

К списку статей

 

Интересуюсь по большей части системным анализом программного обеспечения: поиском багов и анализом неисправностей, а также системным программированием (и не оставляю надежд запилить свою операционку, хотя нехватка времени сказывается :) ). Программированием увлекаюсь с 12 лет, но так уж получилось, что стал я инженером.

Основная сфера моей деятельности связана с поддержкой Solaris и оборудования Sun/Oracle, хотя в последнее время к ним прибавились технологии виртуализации (линейка Citrix Xen) и всякое разное от IBM - от xSeries до Power. Учусь на кафедре Вычислительной Техники НИУ ИТМО.

See you...out there!

http://www.facebook.com/profile.php?id=100001947776045
https://twitter.com/AnnoyingBugs