
/*
 * EXPIRECTL.C
 *
 * expirectl generates the expire.ctl file based on the expire.ctl.ctl
 * and expire.days file.  It dynamically adapts expire.days according to
 * the amount of disk space available in the news spool(s) which consequently
 * effects the relative expirations in expire.ctl based on factors supplied
 * in expire.ctl.ctl
 *
 * A factor in expire.ctl.ctl exists in brackets and is of the form:
 *
 *	[factor]			or
 *	[factor/minimum]
 *
 * The floating point value found in expire.days is updated according to
 * the amount of disk space available and used as a multiplier for all
 * embedded FACTORs in expire.ctl.ctl.  If the result is less the MINIMUM,
 * then MINIMUM is used instead.  If no MINIMUM is specified, a minimum
 * value of 1 is imposed.
 *
 * The result is stored in expire.ctl and used by the news system when it
 * does an expire run.  A typical line might be:
 *
 *     expirectl -v /home/news/spool/news/. -b100m -i200k -I 0.2
 *
 * -b indicates the minimum number of megabytes free on the filesystem, 
 * -i indicates the minimum number of inodes.
 * -I indicates the upward adjustment to expire.days
 *
 * If the current free space is less then the specified minimum, the amount
 * in expire.days is decremented by the -I value times 2.  If the current
 * free space is greater then the specified minimum, the amount in expire.days
 * is incremented by the -I value.  The time value stored in expire.days
 * prevents incrementation from occuring due to multiple side-by-side
 * executions of the expirectl program.  However, decrementation can occur
 * on every run.
 */

#include <sys/types.h>
#include <sys/param.h>
#include <sys/mount.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#ifdef sgi
#include <sys/statfs.h>
#define f_bavail f_bfree
#endif
#ifdef sun
#include <sys/statvfs.h>
#define f_bsize f_frsize
#define statfs statvfs
#endif

#ifndef EXPIRE_CTL_DIR
#define EXPIRE_CTL_DIR	"/home/news"
#endif

#define EXPIRE_DAYS	EXPIRE_CTL_DIR "/expire.days"
#define EXPIRE_CTL	EXPIRE_CTL_DIR "/expire.ctl"
#define EXPIRE_CTL_CTL	EXPIRE_CTL_DIR "/expire.ctl.ctl"

char *SpoolDir[256];
int   NumSpool;
double ExpInc = 1.0;

void
main(int ac, char **av)
{
    long minFree = 100 * 1024;	/* 100MB		*/
    long minIFree = 20 * 1024;	/* 20K inodes free 	*/
    long maxIUsed = 0x7FFFFFFF;
    double expireDays = 2.0;
    time_t expireIncTime = time(NULL) - 24 * 60 * 60;
    int modified = 0;
    int verbose = 0;
    struct statfs sfs;

    /*
     * options
     */

    {
	int i;

	for (i = 1; i < ac; ++i) {
	    char *ptr = av[i];

	    if (*ptr == '-') {
		ptr += 2;
		switch(ptr[-1]) {
		case 'V':
		    printf("ExpireCtl V1.12\n");
		    exit(0);
		case 'v':
		    verbose = 1;
		    break;
		case 'f':
		    modified = 1;
		    break;
		case 'n':
		    modified = -1;
		    break;
		case 'b':
		    minFree = strtol(((*ptr) ? ptr : av[++i]), &ptr, 0);
		    if (*ptr == 'k')
			minFree *= 1;
		    else if (*ptr == 'm')
			minFree *= 1024;
		    else
			minFree = (minFree + 512) / 1024;
		    break;
		case 'i':
		    minIFree = strtol(((*ptr) ? ptr : av[++i]), &ptr, 0);
		    if (*ptr == 'k')
			minIFree *= 1024;
		    else if (*ptr == 'm')
			minIFree *= 1024 * 1024;
		    break;
		case 'u':
		    maxIUsed = strtol(((*ptr) ? ptr : av[++i]), &ptr, 0);
		    if (*ptr == 'k')
			maxIUsed *= 1024;
		    else if (*ptr == 'm')
			maxIUsed *= 1024 * 1024;
		    break;
		case 'F':
		    SpoolDir[NumSpool++] = (*ptr) ? ptr : av[++i];
		    break;
		case 'I':
		    ExpInc = strtod(((*ptr) ? ptr : av[++i]), NULL);
		    break;
		default:
		    fprintf(stderr, "bad option: %s\n", ptr - 2);
		    exit(1);
		}
	    } else {
		SpoolDir[NumSpool++] = (*ptr) ? ptr : av[++i];
	    }
	}
    }

    {
	int i;
	struct statfs stmp;

	bzero(&sfs, sizeof(sfs));
	sfs.f_bsize = 512;
	sfs.f_bavail = 128 * 1024 * 1024 * (1024 / 512);
	sfs.f_files = 2 * 1024 * 1024;
	sfs.f_ffree = sfs.f_files;

	for (i = 0; i < NumSpool; ++i) {
#ifdef sgi
	    if (statfs(SpoolDir[i], &stmp, sizeof(stmp), 0) != 0) {
#else
	    if (statfs(SpoolDir[i], &stmp) != 0) {
#endif
		fprintf(stderr, "expirectl: failed statfs of %s\n",
		    SpoolDir[i]
		);
	    } else {
		if (verbose)
		    printf("%-20s\n", SpoolDir[i]);
		stmp.f_bavail = stmp.f_bavail * (stmp.f_bsize / 512);
		if (stmp.f_bavail < sfs.f_bavail)
		    sfs.f_bavail = stmp.f_bavail;
		if (stmp.f_ffree < sfs.f_ffree) {
		    sfs.f_ffree = stmp.f_ffree;
		    sfs.f_files = stmp.f_files;
		}
	    }
	}
    }

    /*
     * Load /home/news/expire.days
     */
    
    {
	FILE *fi;
	char buf[256];

	if ((fi = fopen(EXPIRE_DAYS, "r")) != NULL) {
	    while (fgets(buf, sizeof(buf), fi) != NULL) {
		if (strncmp(buf, "time", 4) == 0) {
		    expireIncTime = strtol(buf + 4, NULL, 0);
		} else if (strncmp(buf, "days", 4) == 0) {
		    expireDays = strtod(buf + 4, NULL);
		}
	    }
	    fclose(fi);
	} else {
	    if (modified >= 0)
		modified = 1;
	    printf("creating %s\n", EXPIRE_DAYS);
	}
    }

    /*
     * print status
     */

    if (verbose) {
	double freemb = (double)sfs.f_bsize * (double)sfs.f_bavail;
	double usedino = (double)sfs.f_files - (double)sfs.f_ffree;

	printf("spool: %4.2fM free (%2.2fx) %4.2fK INodes-used (%2.2fx)"
	       " IFree: %s ",
	    freemb / (1024.0*1024.0),
	    (double)minFree * (double)1024 / freemb,
	    usedino / 1024.0,
	    usedino / (double)minIFree,
	    ((sfs.f_ffree < minIFree) ? "not-enough" : "ok")
	);
    }

    /*
     * Check limits, update as appropriate
     */

    {
	double bytes;
	long inodes;
	long iused;

	bytes = (double)sfs.f_bsize * (double)sfs.f_bavail / 1024;
	inodes = sfs.f_ffree;
	iused = sfs.f_files - sfs.f_ffree;

	if (bytes < (double)minFree || inodes < minIFree || iused > maxIUsed) {
	    expireDays -= ExpInc * 2;
	    if (expireDays <= 1.0) {
		expireDays = 1.0;
		expireIncTime = time(NULL) - 24 * 60 * 60;
	    }
	    if (modified >= 0)
		modified = 1;
	    printf("%3.1f-\n", expireDays);
	} else if (bytes >= (double)minFree && inodes >= minIFree) {
	    long dt = (long)(time(NULL) - expireIncTime);

	    if (dt >= (long)(60.0 * 60.0 * 24.0 * ExpInc) || dt < -60) {
		expireDays += ExpInc;
		expireIncTime = time(NULL);
		if (modified >= 0)
		    modified = 1;
		printf("%3.1f+\n", expireDays);
	    } else {
		printf("%3.1f tmw\n", expireDays);
	    }
	} else if (verbose) {
	    printf("%3.1f\n", expireDays);
	}
    }

    /*
     * Write EXPIRE_CTL file from EXPIRE_CTL_CTL template
     */

    if (modified > 0) {
	FILE *fi;
	FILE *fo;

	if ((fi = fopen(EXPIRE_CTL_CTL, "r")) != NULL) {
	    if ((fo = fopen(EXPIRE_CTL ".tmp", "w")) != NULL) {
		char sbuf[2048];
		char dbuf[4096];

		while (fgets(sbuf, sizeof(sbuf), fi) != NULL) {
		    char *base = sbuf;
		    char *sptr;
		    char *dptr = dbuf;

		    while ((sptr = strchr(base, '[')) != NULL) {
			double d;
			double m = 1.0;

			bcopy(base, dptr, sptr - base);
			dptr += sptr - base;
			base = sptr;

			d = strtod(sptr + 1, &sptr);
			if (*sptr == '/')
			    m = strtod(sptr + 1, &sptr);
			if (*sptr == ']') {
			    double v = expireDays * d;
			    if (v < m)
				v = m;
			    sprintf(dptr, "%3.1f", v);
			    dptr += strlen(dptr);
			    ++sptr;
			}
			base = sptr;
		    }
		    strcpy(dptr, base);
		    fputs(dbuf, fo);
		}
		fclose(fo);
		if (rename(EXPIRE_CTL ".tmp", EXPIRE_CTL) != 0) {
		    fprintf(stderr, "rename(%s,%s): %s\n",
			EXPIRE_CTL ".tmp",
			EXPIRE_CTL,
			strerror(errno)
		    );
		}
	    }
	    fclose(fi);
	}
    }

    /*
     * Write EXPIRE_DAYS file
     */
    
    if (modified > 0) {
	FILE *fo;

	if ((fo = fopen(EXPIRE_DAYS, "w")) != NULL) {
	    fprintf(fo, "time 0x%08lx\n", expireIncTime);
	    fprintf(fo, "days %3.1f\n", expireDays);
	    fclose(fo);
	} else {
	    fprintf(stderr, "unable to create %s\n", EXPIRE_DAYS);
	}
    }
    exit(0);
}

