#!/usr/bin/env python import sys,os,glob from datetime import datetime from datetime import timedelta import sqlite3 import re,signal import subprocess,threading VERSION=2 W= '30' R= '31' G= '32' Y= '33' B= '34' M= '35' C= '36' S= '1' E= '0' BR= '41' CLR = '\033[2J' SAVE = '\033[s' LOAD = '\033[u' CLRLN = '\033[K' CLRBLN = '\033[1K' DOWN = '\033[1B' SORTCONVERT = lambda text: float(text) if is_number(text) else -1 SORTKEY = lambda key: SORTCONVERT(key[2][:-1]) def setup_options(): ''' Setup the command line options ''' from argparse import ArgumentParser import argparse parser=ArgumentParser(description=''' Tool to clean up and colorize the output of Anduril. Example: anduril run yourscript.and | %(prog)s You can tap in to an existing log with: tail -f -n +0 log/_global | %(prog)s''',formatter_class=argparse.RawTextHelpFormatter) parser.add_argument("--no-colors",'--nc',action="store_false",dest="colors",default=True, help="Disable colored output") parser.add_argument("-a",action="store_false",dest="activities",default=True, help="Disable activities output") parser.add_argument("--version",action='version', version=VERSION) options=parser.parse_args() return options def c(attribs): ''' ANSI colorizer ''' if not options.colors: return "" return '\033['+';'.join(attribs)+'m' def pos(y,x): ''' ANSI absolute position set ''' return "\033["+str(y)+";"+str(x)+"H" def colorize(string): ''' colorizes a string based on color_match ''' if not options.colors: return string for c in color_match: string=color_match[c][0].sub(color_match[c][1],string) return string def count_running(string, stats): ''' Counts the running executions ''' spl=[i.strip() for i in string.split('|')] if len(spl)!=4: return stats spl.append(datetime.now()) if spl[3] in stats['files']: index=stats['files'].index(spl[3]) stats['running'][index]=spl else: stats['running'].append(spl) stats['running'].sort(key=SORTKEY, reverse=True) stats['files']=[i[3] for i in stats['running']] return stats class EndProgram( Exception ): ''' Nice way of exiting the program ''' pass def remove_running(stats): ''' Remove Done files ''' if not stats['running']: return stats # Remove Done/Fail older than 60sec for e in enumerate(stats['running']): if (datetime.now() - e[1][4] > timedelta(seconds=60)): if e[1][2]=='Done' or e[1][2]=='Failed': stats['running'].pop(e[0]) stats['files'].pop(e[0]) # Remove Done/Fail if there are too many: if len(stats['running'])>(stats['size'][0]-6): for e in enumerate(stats['running']): if e[1][2]=='Done': stats['running'].pop(e[0]) stats['files'].pop(e[0]) return stats if e[1][2]=='Failed': stats['running'].pop(e[0]) stats['files'].pop(e[0]) return stats return stats def is_number(s): ''' Check if string is float ''' try: out=float(s) return True except: return False def str_short(s,stats): ''' shorten text to fit screen ''' maxL=stats['size'][1] - 16 if len(s) 1024: suffixIndex += 1 size = size/1024.0 defPrecision=precision return "%.*f%s"%(defPrecision,size,suffixes[suffixIndex]) def readinput(lf): try: line = lf.stdout.readline() #line=lf.readline() return line except: return "CleanerTimeout" def termsize(): rows, columns = os.popen('stty size', 'r').read().split() return (int(rows),int(columns)) def get_partial_dir(): sql_file=os.path.join(os.path.expanduser("~"), ".aerofs", "conf") conn=sqlite3.connect(sql_file) db=conn.cursor() conn.text_factory=str db.execute("SELECT v FROM c WHERE k = 'root'") for row in db: continue conn.close() cache_dir=glob.glob(os.path.join(os.path.dirname(row[0]),'.aerofs.aux*')) return os.path.join(cache_dir[0],'p') def get_partial_size(dir): return sum([os.path.getsize(os.path.join(dir,f)) for f in os.listdir(dir) if os.path.isfile(os.path.join(dir,f))]) def partial_update(stats): ''' Calculate average speed of transfer ''' stats['psize'].pop(0) stats['psize'].append( ( get_partial_size(stats['pdir']), datetime.now()) ) speedlist=[] for i in range(len(stats['psize'])-1): sizediff=stats['psize'][i+1][0] - stats['psize'][i][0] timediff=(stats['psize'][i+1][1] - stats['psize'][i][1]).total_seconds() if timediff>0 and sizediff>0: speedlist.append( sizediff / timediff ) if len(speedlist)==0: speed=0.0 else: speed=sum(speedlist)/len(speedlist) stats['pspeed']=speed return stats class Threaded(threading.Thread): def __init__(self,command): self.stdout = None self.stderr = None self.command=command self.p = None threading.Thread.__init__(self) def run(self): self.p = subprocess.Popen(self.command.split(), shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) def readline(self): try: line = self.p.stdout.readline() #line=lf.readline() return line except: return "CleanerTimeout" def readstdout(self): self.stdout, self.stderr = self.p.communicate() return self.stdout def stop(self): self.p.terminate() options=setup_options() color_match={#'line_ends':(re.compile('$'),c.END), 'err':(re.compile('(Failed)'),c([R,S])+'\\1'+c([E])), 'done':(re.compile('(Done)'),c([G,S])+'\\1'+c([E])), 'percent':(re.compile('([0-9]+%)'),c([Y,S])+'\\1'+c([E])), } stats={'time':datetime.now()-timedelta(seconds=25), 'running':[], 'files':[], 'size': termsize(), 'pdir': get_partial_dir(), 'psize': [( get_partial_size(get_partial_dir()), datetime.now())]*5, 'pspeed': 0.0 } sys.stdout.write(CLR+pos(0,0)+"Launching...") #proc = subprocess.Popen(['aerofs-sh','transfers'],stdout=subprocess.PIPE) transfers = Threaded("aerofs-sh transfers") transfers.start() for e in range(5): sys.stdout.write(pos(e+1,0)+CLRLN) activities = print_activities(None) while 1: try: sys.stdout.flush() # set a 5 second timeout for the line read. signal.signal(signal.SIGALRM, transfers.readline) signal.alarm(5) line=transfers.readline() if not line: raise EndProgram if ( datetime.now() - stats['time'] > timedelta(seconds=30) ) and options.activities: activities=print_activities(activities) stats=partial_update(stats) stats['time'] = datetime.now() stats=remove_running(stats) stats=count_running(line,stats) print_stats(stats) except EndProgram,KeyboardInterrupt: transfers.stop() transfers.join() sys.stdout.write(DOWN+'\n') sys.stdout.flush() sys.exit(0)