/* vim: ts=2 sw=1 sts=2:et ai: pipemeter - A program to show status of a pipe Copyright Clint Byrum 2004, All Rights Reserved. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA --- $Id$ */ #define _LARGEFILE_SOURCE #define _LARGEFILE64_SOURCE #include "config.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef HAVE_GETOPT_H #include #endif #define VERSION PACKAGE_VERSION #define DEFAULT_BLOCK_SIZE 8192 #define DEFAULT_INTERVAL 1 // block sizes over 8M rarely make any sense. #define DEFAULT_MAX_BLOCK_SIZE (8*1024*1024) #define DEFCOLS 70 //#define PBAR "[-----------------------------------------------------]" #define PBCHAR '-' #define PBLEFT '[' #define PBRIGHT ']' #define PBFILL '*' //#define STARS "*****************************************************" //#define PBAR_LEN 53 // This must be changed if averages and rate formats are changed // Note: Defines the space everything BUT the pbar takes up #define OTHER_LEN 34 // Might want to turn down for slower machines. // This defines the number of samples to average. // -- 0.8 lowered to 12 to start adaptive block sizing sooner. #define LAST_MAX 24 /* This defines the adaptive block sizing sampling frequency * This means, every X times that avg_bytes is called, we'll check the * rate and change the block size if needed. */ #define SAMPLE_FREQ 3 // If rate increases by this much or more, block size gets increased - // This is a percentage #define RATE_INCREASE 0.8 #define RATE_SAME -0.05 #define MIN_BLOCK 64 // Increase by this much #define INC_PCT 0.10 // Decrease by this much #define DEC_PCT -0.10 // TODO: async/multi threaded to seperate reading and writing. enum getbytesmode { report, normal }; void show_just_rate(int sig); void show_progress(int sig); void parseopts(int argc, char *argv[]); void formatbytes(char *obuffer,double b); size_t full_write (int desc, const char *ptr, size_t len); double avg_bytes(off_t abytes); void setblock(char mode); void adapt_blocksize(double pctchange); time_t get_eta(double bps,off_t bytesleft); time_t get_elapsed(void); void formattime(char * outbuf,size_t outbufsize,time_t formatme); double get_bytespersecond(enum getbytesmode mode); // must...get..rid..of..globals... rrggh off_t parse_size(char *optarg); // XXX: ok.. probably too many globals .. may have to clean this up extern char **environ; /* DEATH TO GLOBALS - maybe in the 2.0 rewrite. */ off_t bytes; off_t lastbytes = 0; off_t filesize = 0; double itimer_seconds; // Hrm.. should this be time_t? :-P off_t block_size; off_t max_block_size; char **filenames=NULL; int filenames_count=0; int report_mode=0; struct timeval start_time; char *progressbar; char *progressfill; unsigned int pbarlen; // Used for calculating a more pertinent average. off_t last[LAST_MAX]; unsigned char lcnt; // only used for indexing the above array /* Adaptive block sizing */ double recordedrate=0; double highestrate=0; double lowestrate=0; long last_bsize[LAST_MAX]; char *buffer; char lastchange; // Used to tell the adaptive block sizing what we did last cycle unsigned char needs_resize=0; long new_block_size; unsigned char adaptivemode=0; // Gets set to 1 if override stays 1 unsigned char adaptiveoverride=1; // set to 0 if user passes -a char *trailer; // no \r's TODO: timestamps? unsigned char tcount=0; int main(int argc, char *argv[]) { struct itimerval it; //FIXME: these should be initialized! (sig_empty or something?) // (I have a book I don't feel like looking for right now that explains it) struct sigaction sa,sa_orig; struct timeval interval; long bytesin,bytesout; long wholeseconds,microseconds; int in,out; int save_errno; int thisfile=0; int filesizeoverride=0; unsigned int columns,i; char *colstr; char *newbuffer; // Must be same type/makeup as buffer out=fileno(stdout); lastbytes=0; bytes=0; parseopts(argc,argv); if(filenames != NULL) { if(filesize) { fprintf(stderr, "Warning: -s overrides size of file given by -f!\n"); filesizeoverride=1; } for(thisfile=0;thisfile 0) { bytes += bytesin; bytesout=full_write(out,buffer,bytesin); if(bytesout != bytesin) { /* Its possible full_write cleared errno, but we know the write * failed for some reason. */ if(!errno) { save_errno=1; } else { save_errno=errno; } perror("write failed, aborting"); exit(save_errno); } /* This has to be done here, so that we don't realloc away data in * the middle of a read/write */ if(needs_resize) { block_size=new_block_size; while(!(newbuffer=(char *)realloc(buffer,block_size))) { #ifdef DEBUG fprintf(stderr,"\nDEBUG: bs=%ld,nbs=%ld\n",block_size,new_block_size); #endif new_block_size=new_block_size/2; /* even though this may mean we're decreasing it, I want it to * look like we left it alone - since we're only decreasing it * because of memory constraints, not performance */ setblock(0); } buffer=newbuffer; needs_resize=0; } } else { /* * If there's no data available, we get an EINTR error. This is not bad. * But some platforms don't define EINTR. */ int ignore=0; #ifdef EINTR ignore=(errno==EINTR); #endif if(!ignore) { /* ignore can only be true if errno was == EINTR, so that * assumption is carried to the exit call below. If the logic * changes then we must reexamine this assumption later on. */ perror("read failed, aborting"); exit(errno); } } } close(in); } // Final status report if(filesize) { show_progress(0); } else { show_just_rate(0); } fprintf(stderr, "\n"); return 0; } void show_just_rate(int sig) { /* if no progress bar, we use this function */ char numbuf[512]; // XXX: hrm... bad form, but so much simpler char timebuf[10]; double bytespersecond; enum getbytesmode gbmode = normal; time_t elapsedtime; elapsedtime = get_elapsed(); if(sig != SIGALRM) { // If we're reporting gbmode=report; itimer_seconds = elapsedtime; lastbytes=0; #ifdef DEBUG fprintf(stderr,"\nDEBUG: elapsedtime=%lf itimer_seconds=%lf\n",elapsedtime,itimer_seconds); #endif } bytespersecond=get_bytespersecond(gbmode); formatbytes(numbuf,bytespersecond); fprintf(stderr,"%s/s",numbuf); /* Show total bytes through */ /* Thanks to Sean Reifschneider for this idea */ formatbytes(numbuf,bytes); fprintf(stderr," %s",numbuf); formatbytes(numbuf,block_size); fprintf(stderr," %s",numbuf); formattime(timebuf,sizeof(timebuf),elapsedtime); fprintf(stderr," %s",timebuf); lastbytes=bytes; if(sig == 0) { fprintf(stderr,"\n"); exit(0); } else if(sig==SIGTERM || sig==SIGINT) { /* This way if the user aborts, something like this won't rm a file early: * pipemeter -f file > /somewhereelse/newfile && rm -f file * DOH! */ fprintf(stderr,"\n"); exit(1); } else { fprintf(stderr,trailer); } } /* I'm harping on it, because I have to do it. This provides us with a nice * example of why we need to use fewer globals. * Takes mode argument. Just calculates bytespersecond based on global stuff. * :/ Someone please convince me I'm wrong about this!!! ;) */ double get_bytespersecond(enum getbytesmode mode) { double bytespersecond; if(mode==report) tcount=0; // Forcing using the actual values, rather than the array if(tcount < LAST_MAX) { #ifdef DEBUG fprintf(stderr,"\nDEBUG: bytes=%lld lastbytes=%lld\n" ,(long long) bytes,(long long) lastbytes); #endif bytespersecond=(double)(bytes-lastbytes)/(double)itimer_seconds; avg_bytes(bytes-lastbytes); tcount++; if(tcount >= LAST_MAX) { // The array is filled, turn it on if(adaptiveoverride) { adaptivemode=1; } } } else { bytespersecond=avg_bytes(bytes-lastbytes); } return bytespersecond; } void show_progress(int sig) { //char buf[512]; // XXX: yes, this is bad form. char buf2[512]; // XXX: hopefully we fix them before there are 100 or so double percent = (double)bytes / (double)filesize * 100; int progress; double bytespersecond; char etabuf[10]; // Seriously, I dare you to overflow it enum getbytesmode gbmode=normal; time_t elapsedtime=0; progress = (double)bytes / (double)filesize * pbarlen; #ifdef DEBUG fprintf(stderr,"DEBUG: prog=%d b=%lld pbl=%d",progress,(long long) bytes,pbarlen); #endif if(sig != SIGALRM) { // If we're reporting gbmode=report; elapsedtime=get_elapsed(); itimer_seconds = elapsedtime; lastbytes=0; #ifdef DEBUG fprintf(stderr,"\nDEBUG: elapsedtime=%lf itimer_seconds=%lf\n",elapsedtime,itimer_seconds); #endif } bytespersecond = get_bytespersecond(gbmode); formatbytes(buf2,bytespersecond); lastbytes=bytes; if(sig != SIGALRM) { formattime(etabuf,sizeof(etabuf),elapsedtime); } else { formattime(etabuf,sizeof(etabuf),get_eta(bytespersecond,filesize-bytes)); } // This seems kludgy -- do I even care? ARGH GLOBALS! if(gbmode != report) { lastbytes=bytes; } if(progress > pbarlen) { progress = pbarlen; // just keep printing full progress bars. } strncpy(progressbar+1,progressfill,progress); fprintf(stderr, progressbar); fprintf(stderr," %s/s",buf2); formatbytes(buf2,bytes); fprintf(stderr," %s",buf2); fprintf(stderr, " %5.1f%%", percent); fprintf(stderr, " %s", etabuf); if(sig == 0 || sig==SIGTERM || sig==SIGINT) { fprintf(stderr,"\n"); exit(0); } else { fprintf(stderr,trailer); } } void parseopts(int argc, char *argv[]) { int c; // // Thanks to WaruiInu on #linuxhelp-Undernet for reminding me to include getopt.h // // 22:28 <+WaruiInu> i suspect that another .h must be included // 22:29 <+WaruiInu> i think the .h you have included only has struct options ; // 22:29 <+WaruiInu> for declaring later // 22:29 <+WaruiInu> and the later defining is missing // 22:29 <+WaruiInu> it is my guess :) // 22:34 <+WaruiInu> warui = bad, inu = dog :D // #ifdef HAVEGETOPTLONG static struct option longopts[] = { {"file", 1,NULL,'f'} ,{"size", 1,NULL,'s'} ,{"blocksize",1,NULL,'b'} ,{"interval", 1,NULL,'i'} ,{"maxblock", 1,NULL,'m'} ,{"report", 0,NULL,'r'} ,{"version", 0,NULL,'V'} ,{"autooff", 0,NULL,'a'} ,{"log", 0,NULL,'l'} ,{NULL, 0,NULL,0} }; #endif // Set some defaults itimer_seconds=DEFAULT_INTERVAL; block_size=DEFAULT_BLOCK_SIZE; max_block_size=DEFAULT_MAX_BLOCK_SIZE; filenames = NULL; trailer="\r"; do { #ifdef HAVEGETOPTLONG c=getopt_long(argc,argv,"f:s:b:i:m:rVal",longopts,NULL); #else c=getopt(argc,argv,"f:s:b:i:m:rVal"); #endif switch(c) { case -1: // No more options break; case 0: // No options passed, this is fine. break; case 'b': block_size=parse_size(optarg); break; case 'i': itimer_seconds=strtod(optarg,NULL); if(itimer_seconds <= 0.0) { fprintf(stderr,"Bad interval: %s\n",optarg); exit(1); } break; case 's': filesize=parse_size(optarg); break; case 'f': filenames=(char **)realloc(filenames,sizeof(char *)*(filenames_count+1)); if(filenames == NULL) { fprintf(stderr,"Error allocating memory for filenames list.\n"); exit(1); } // +1 for \0 filenames[filenames_count]=(char *)malloc(sizeof(char)*(strlen(optarg)+1)); if(filenames[filenames_count] == NULL) { fprintf(stderr,"Error allocating memory for filename. %s\n",optarg); exit(1); } /* don't get on my back for using strlen. we just allocated exactly enough space for the string as returned by strlen. strncpy is superfluous. */ strcpy(filenames[filenames_count],optarg); filenames_count++; break; case 'm': max_block_size = parse_size(optarg); break; case 'r': report_mode=1; break; case 'V': // Exiting here because otherwise we'll start trying to read/write fprintf(stderr,"pipemeter v%s\n",VERSION); exit(0); case 'a': // Turn off adaptive block sizing adaptiveoverride=0; break; case 'l': trailer="\n"; break; case '?': case ':': // XXX: better errors default: fprintf(stderr,"usage: pipemeter { -b blocksize } { -i interval } { -ahlr }\n"); //fprintf(stderr,"debug: %c %d\n",(char)c,c); exit(1); } } while(c > 0); buffer=(char *)malloc(block_size*sizeof(char)); } void formatbytes(char *obuffer,double b) { double tmp; if(b>1073741824) { tmp=b/1073741824; // If you can get it to go faster than 999.99G/s ... You win. sprintf(obuffer,"%7.2fG",tmp); } else if(b>1048576) { tmp=b/1048576; sprintf(obuffer,"%7.2fM",tmp); } else if(b>2048) { // under 2k, let it be tmp=b/1024; sprintf(obuffer,"%7.2fk",tmp); } else sprintf(obuffer,"%7.2fB",b); } /* This is used to get a smoother average */ // TODO: moving average? double avg_bytes(off_t abytes) { off_t tmp=0; unsigned char i; double lastrate,ratediff,pctchange; last[lcnt]=abytes; if(lcnt == LAST_MAX-1) { lcnt=0; } else { lcnt++; } /* Adaptive block sizing */ if(adaptivemode) { #ifdef DEBUG fprintf(stderr,"DEBUG: lcnt=%d\n",lcnt); #endif if((lcnt % SAMPLE_FREQ)==1) { /* check last SAMPLE_FREQ rates to see if they improved */ for(i=0;i highestrate) { highestrate=lastrate; pctchange=100.0; /* This should encourage more increases */ } else if (lastrate < lowestrate) { lowestrate=lastrate; pctchange=-100.0; /* This should force a reversal */ } else { pctchange=ratediff/recordedrate; } adapt_blocksize(pctchange); recordedrate=lastrate; } } tmp=0; for(i=0;i RATE_INCREASE) { /* enough to be considered a higher rate */ switch(lastchange) { case 1: /* We increased it last time and had success... more! */ setblock(1); break; case 0: /* We left it alone last time and had a rate increase. Need more info. */ setblock(0); break; case -1: /* We decreased it last time and had an increase. Lets try again. */ setblock(-1); } } else if(pctchange > RATE_SAME) { /* enough to be considered as the same rate */ switch(lastchange) { case 1: /* We increased it last time and it stayed the same. More! */ setblock(1); break; case 0: /* Left it alone and it stayed the same. Duh! Increase it. */ setblock(1); break; case -1: /* We decreased it and it stayed the same. Lets stay the same. */ /* TODO: investigate whether it wouldn't be better to increase */ setblock(0); break; } } else { /* low enough to be considered a loss */ switch(lastchange) { case 1: /* We increased it, and had a decrease. Lets bump it back down */ setblock(-1); break; case 0: /* Left it alone and it went down. Lets go up. */ setblock(1); break; case -1: /* we decreased it and it went down. Back up. */ setblock(1); break; } } /* blah */ } void setblock(char mode) { #ifdef DEBUG fprintf(stderr,"DEBUG: setblock(%d) - lastchange=%d\n\n",mode,lastchange); #endif if(mode==1) { //new_block_size=block_size*2; // double/half is too dramatic new_block_size=block_size+(block_size*INC_PCT); // 8 byte alignment while(new_block_size%8 != 0) { // Increase until 8 byte alignment is achieved new_block_size++; } if((new_block_size) < 0) { /* OOPS! we just went over our limit. Throttle back */ new_block_size=block_size; mode=0; } else { needs_resize=1; } } else if(mode==-1) { /* This is to prevent going to stupid block sizes like 32 bytes */ if(block_size > MIN_BLOCK) { //new_block_size=block_size/2; new_block_size=block_size+(block_size*DEC_PCT); while(new_block_size%8 !=0 && new_block_size > 8) { new_block_size--; } needs_resize=1; } } if(new_block_size > max_block_size) { new_block_size=block_size; needs_resize=0; return; } else { lastchange=mode; } } /* returns estimated time until completion in unix time */ /* TODO: make this more sophisticated */ time_t get_eta(double bps,off_t bytesleft) { return (time_t)(bytesleft/bps); } /* returns elapsed time */ time_t get_elapsed(void) { struct timeval tnow; time_t tx,ty; gettimeofday(&tnow,NULL); tx=tnow.tv_sec+(tnow.tv_usec * 0.000001); ty=start_time.tv_sec+(start_time.tv_usec * 0.000001); return (tx - ty); } /* Formats time for ETA display */ void formattime(char *outbuf,size_t outbufsize,time_t formatme) { time_t hours; time_t minutes; time_t seconds; hours=(time_t)formatme/3600; seconds=formatme-(hours*3600); minutes=(time_t)seconds/60; seconds -= (minutes*60); if(outbuf==NULL) { fprintf(stderr,"Null pointer passed to formattime()! Abort Abort Abort!\n"); exit(1); } /* Thanks to Petr Adamek for this bug report, and this fix (slightly * modified by me for clarity and sanity. ;) */ if (formatme < 0 || hours >= 999) { strncpy(outbuf,"---:--:--",outbufsize-1); } else { snprintf(outbuf,outbufsize,"%3ld:%02ld:%02ld",hours,minutes,seconds); } } /* taken from fileutils... yay GPL (and thanks GNU for the code) */ size_t full_write (int desc, const char *ptr, size_t len) { size_t total_written = 0; while (len > 0) { ssize_t written = write (desc, ptr, len); if (written <= 0) { /* Some buggy drivers return 0 when you fall off a device's end. */ if (written == 0) errno = ENOSPC; #ifdef EINTR if (errno == EINTR) continue; #endif break; } total_written += written; ptr += written; len -= written; } return total_written; } off_t parse_size(char *optarg) { int mult=1; off_t temp; // blocksize qualifiers by Ian McMahon, tmbg@hardcoders.org, 9/07/2002 // we're gonna examine the last char, and if it matches [kKmMgG] we'll act accordingly switch(optarg[strlen(optarg) - 1]) { case 'g': /* FALLTHRU */ case 'G': /* FALLTHRU */ mult *= 1024; case 'm': /* FALLTHRU */ case 'M': /* FALLTHRU */ mult *= 1024; case 'k': /* FALLTHRU */ case 'K': mult *= 1024; optarg[strlen(optarg) - 1] = '\0'; break; default: break; } // Fixes a bug where a specific size in bytes over 2GB could not be Input #if SIZEOF_OFF_T > 4 temp=strtoll(optarg,NULL,10); #else temp=strtol(optarg,NULL,10); #endif temp *= mult; if(temp==LONG_MIN || temp==LONG_MAX || temp <= 0) { fprintf(stderr,"Bad size: %s\n",optarg); exit(1); } return temp; }