Как известно, замечательные утилитки типа 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
VIDEO
Прикладываю файлы: filebar.py.gz и progressbar.patch.gz .