#!mkcmd
# $Id: since.m,v 1.26 2012/07/27 15:18:58 ksb Exp $
# $Compile: mkcmd %f && ${cc-gcc} -Wall -g -o prog prog.c
#
from '<sys/param.h>'
from '<sys/types.h>'
from '<sys/stat.h>'
from '<sysexits.h>'
from '<fcntl.h>'
from '<unistd.h>'
from '<dirent.h>'

basename "since" ""

require "sstamp.m"
require "std_help.m" "std_version.m"
require "util_home.m"
%i
static char rcsid[] =
	"$Id: since.m,v 1.26 2012/07/27 15:18:58 ksb Exp $";
static char acSinceDB[MAXPATHLEN];
/* Major/minor should be in types.h, on most UNIX systems -- ksb
 */
#if !defined(major)
#define major(x)	(((unsigned)(x)>>16)&0xffff)
#endif
#if !defined(minor)
#define minor(x)	((x)&0xffff)
#endif

#if USE_64_API
#define stat stat64
#define fstat fstat64
#define open open64
#define lseek lseek64
#define off_t off64_t
#endif

%%

char* 'F' {
	named "pcSinceDB" "pcTempDB"
	param "db"
	help "since time stamp meta file"
	late
}

exclude "RZLS"
int 'R' {
	named "iReplay"
	track "iGaveReplay"
	init "0"
	param "lines"
	help "number of lines to replay before previous position"
}
boolean 'Z' {
	named "fFromZero"
	init "0"
	help "start the file over at the first byte"
}
boolean 'S' {
	named "fSync"
	init "0"
	help "move the current position to the end-of-file for each target"
}
boolean 'L' {
	named "fListOnly"
	init "0"
	help "list present status of each file (only)"
}

boolean 'd' {
	named "fDelete"
	init "0"
	help "remove the database entry for each target, after processing"
}


init 3 '%rFU = 1;%rFN = acSinceDB;(void)util_home(acSinceDB, (char *)0);(void)strcat(acSinceDB, "/.");(void)strcat(acSinceDB, %b);'

augment action 'V' {
	user 'printf("%%s: default time stamp meta file: %%s\\n", %b, acSinceDB);'
}

char* named "pcTarget" {
}
letter 'c' {
	named "cOutSep"
	init "'\\n'"
	param "sep"
	help "output separator for filenames under -d"
}

every {
	update "Since(%a);"
	local param "targets"
	help "file to display"
}
zero {
	named "SinceFd"
	update 'SinceFd("stdin", fileno(stdin));'
	aborts 'exit(EX_OK);'
}

%c
/* Rewind the file by lines until we hit the beginning, in which   (petef,ksb)
 * case we return the number of lines we couldn't rewind.
 */
static int SkipBack(int iFd, int iOrig)
{
	ssize_t wCur;
	unsigned int iRead, iCursor, iTake, iLeft;
	auto char acBlock[1024];

	iLeft = iOrig;
	wCur = lseek(iFd, 0, SEEK_CUR);
	while (wCur > 0) {
		iTake = sizeof(acBlock);
		if (iTake > wCur) {
			iTake = wCur;
		}
		wCur -= iTake;
#if USE_PREAD
		iRead = pread(iFd, acBlock, iTake, wCur);
#else
		if (wCur != lseek(iFd, wCur, SEEK_SET)) {
			return iLeft;
		}
		iRead = read(iFd, acBlock, iTake);
#endif
		if (iRead != iTake) {
			return iOrig;
		}
		for (iCursor = iRead; iCursor-- > 0;) {
			if ('\n' != acBlock[iCursor]) {
				continue;
			}
			if (0 == iLeft--) {
				wCur += iCursor + 1;
				goto done;
			}
		}
	}
 done:
	if (wCur != lseek(iFd, wCur, SEEK_SET)) {
		return iOrig;
	}

	return iLeft;
}

/* do the deed for a directory						(ksb)
 * We output like "ls", skipping "." and "..", of course,
 * show file that have changed by (st_ctime) since we last looked, or
 * since the epoch if we never looked.
 */
static void
SinceDir(char *pcDir, struct stat *pstArg, char *pcCallme)
{
	register int fNewFile, i;
	register off_t lNew;
	register SINCE_STAMP *pSS;
	register struct dirent *pDE;
	register DIR *pDR;
	register size_t w;
	auto time_t wLast;
	auto struct stat stItem;
	auto char acItem[MAXPATHLEN+4];

	pSS = since_from_log((SINCE_STAMP *)0, pcSinceDB, pstArg);
	fNewFile = (SINCE_STAMP *)0 == pSS;
	if (fNewFile) {
		pSS = (SINCE_STAMP *)calloc(1, sizeof(SINCE_STAMP));
		pSS->wlength = 0L;
		wLast = pSS->wwhen = (time_t)0;
		pSS->wnode = pstArg->st_ino;
		pSS->wdevice = pstArg->st_dev;
	} else {
		wLast = pSS->wwhen;
	}
	if (fFromZero) {
		pSS->wwhen = (time_t)0;
	}
	if (fSync) {
		pSS->wwhen = time((time_t *)0);
	}

	/* Check for common errors with the input, the kernel
	 * should enforce maxpathlen, but we'll cross-check anyway.
	 */
	if ((DIR *)0 == (pDR = opendir(pcDir))) {
		fprintf(stderr, "%s: opendir: %s: %s\n", progname, pcDir, strerror(errno));
		return;
	}
	if (MAXPATHLEN < (w = strlen(pcDir))) {
		fprintf(stderr, "%s: path length limit: %s\n", progname, strerror(ENAMETOOLONG));
		return;
	}
	(void)strncpy(acItem, pcDir, sizeof(acItem));
	if (0 == w || '/' != acItem[w-1]) {
		acItem[w++] = '/';
	}
	lNew = 0L;
	/* We don't do -R here, and I don't think we can -- ksb
	 */
	for (i = 0; (struct dirent *)0 != (pDE = readdir(pDR)); /*nada*/) {
		if ('.' == pDE->d_name[0] && ('\000' == pDE->d_name[1] || ('.' == pDE->d_name[1] && '\000' == pDE->d_name[2])))
			continue;
		++i;
		if (w + strlen(pDE->d_name) > MAXPATHLEN) {
			fprintf(stderr, "%s: path lenght limit exceeded: %s/%s\n", progname, pcDir, pDE->d_name);
			continue;
		}
		(void)strcpy(acItem+w, pDE->d_name);
		if (-1 == stat(acItem, &stItem)) {
			fprintf(stderr, "%s: stat: %s: %s\n", progname, acItem, strerror(errno));
			continue;
		}
		if (stItem.st_ctime <= pSS->wwhen) {
			continue;
		}
		if (pstArg->st_ctime < stItem.st_ctime) {
			pstArg->st_ctime = stItem.st_ctime;
		}
		if (fListOnly) {
			++lNew;
			continue;
		}
		printf("%s%c", pDE->d_name, cOutSep);
	}
	closedir(pDR);
	pSS->istamp = 1;
	if (fListOnly) {
		static int fHeader = 1;
		auto char acDevInode[256], acNumber[64];

		if (fHeader) {
			printf("%-5s %16s %16s %12s %s\n", "Mod", "Last-count", "Count", "Device/inode", "Name");
			fHeader = 0;
		}
#if HAVE_SNPRINTF
		snprintf(acNumber, sizeof(acNumber), 
#if USE_64_API
							"%lld",
#else
							"%ld",
#endif
								lNew);
		snprintf(acDevInode, sizeof(acDevInode), "%d,%d/%d", major(pSS->wdevice), minor(pSS->wdevice), pSS->wnode);
#else
		sprintf(acNumber,
#if USE_64_API
				"%lld",
#else
				"%ld",
#endif
					(long int)lNew);
		sprintf(acDevInode, "%ld,%ld/%ld", (long)major(pSS->wdevice), (long)minor(pSS->wdevice), (long)pSS->wnode);
#endif
		printf("%-5s "
#if USE_64_API
			"%16lld"
#else
			"%16ld"
#endif
			"%16ld %12s %s\n", fNewFile ? "new" : 0L != lNew ? acNumber : "no", (long)pSS->wlength, (long)i, acDevInode, pcCallme);

		/* Put the record back as it was in the db, if it existed.
		 */
		if (fNewFile) {
			return;
		}
		if (!fDelete) {
			pSS->wwhen = wLast;
			since_to_log(pSS, pcSinceDB);
		}
		return;
	}
	pSS->wwhen = pstArg->st_ctime;
	pSS->wlength = i;

	/* Record our new state, if we are supposed to, in the db file
	 */
	if (!fDelete)
		since_to_log(pSS, pcSinceDB);
}

/* do the deed for a file					  (petef,ksb)
 */
static void
SinceFile(char *pcFile, struct stat *pstArg, int iFd)
{
	register int iRet, iBlockSize, fNewFile = 0;
	register SINCE_STAMP *pSS;
	register char *pcBuf;

	/* Fetch our state from the sincedb, write what we've seen since
	 * last time, and write our state back out
	 */
	pSS = since_from_log((SINCE_STAMP *)0, pcSinceDB, pstArg);
	if (fListOnly) {
		static int fHeader = 1;
		auto char acDevInode[256];
		auto off_t wCur = lseek(iFd, 0l, SEEK_END);

		if (fHeader) {
			printf("%-3s %16s %16s %12s %s\n", "Mod", "Last-end", "Present-size", "Device/inode", "Name");
			fHeader = 0;
		}
		if ((SINCE_STAMP *)0 == pSS) {
			printf("new %16s "
#if USE_64_API
					"%16lld"
#else
					"%16ld"
#endif
						"16lld %12s %s\n", "~", (long)lseek(iFd, 0l, SEEK_END), "~/~", pcFile);

			return;
		}
#if HAVE_SNPRINTF
		snprintf(acDevInode, sizeof(acDevInode), "%d,%d/%d", major(pSS->wdevice), minor(pSS->wdevice), pSS->wnode);
#else
		sprintf(acDevInode, "%ld,%ld/%ld", (long)major(pSS->wdevice), (long)minor(pSS->wdevice), (long)pSS->wnode);
#endif
		printf("%-3s "
#if USE_64_API
			"%16lld %16lld"
#else
			"%16ld %16ld"
#endif
					" %12s %s\n", (pSS->wlength != wCur || pstArg->st_ino != pSS->wnode || pstArg->st_dev != pSS->wdevice) ? "yes" : "no", (long)pSS->wlength, (long)wCur, acDevInode, pcFile);
		if (!fDelete) {
			pSS->istamp = 1;
			since_to_log(pSS, pcSinceDB);
		}
		return;
	}
	if ((SINCE_STAMP *)0 == pSS) {
		fNewFile = 1;
		pSS = (SINCE_STAMP *)calloc(1, sizeof(SINCE_STAMP));
		pSS->wlength = 0;
		pSS->wnode = pstArg->st_ino;
		pSS->wdevice = pstArg->st_dev;
	}
	iBlockSize = pstArg->st_blksize;
	pcBuf = (char *)calloc(iBlockSize, sizeof(char));

	lseek(iFd, pSS->wlength, SEEK_SET);

	/* Do we need to rewind to show previous lines?
	 * If we can't find all the lines we pad with blanks ones, this
	 * makes the caller's invar. code path way easier.
	 */
	if (fFromZero && 0L != lseek(iFd, 0l, SEEK_SET)) {
		fprintf(stderr, "%s: lseek: %s: %s\n", progname, pcFile, strerror(errno));
		/* If we can't rewind the file delete the db entry,
		 * this is the correct thing to do most of the time.
		 */
		return;
	}
	if (fSync) {
		lseek(iFd, 0l, SEEK_END);
	}
	if (iGaveReplay) {
		iReplay = SkipBack(iFd, iReplay);
		while (iReplay-- > 0) {
			write(1, "\n", 1);
		}
	}

	/* Pump out all the data we have at present
	 */
	while ((iRet = read(iFd, pcBuf, iBlockSize)) > 0) {
		if (iRet != write(1, pcBuf, iRet))
			break;
	}
	if (-1 == iRet) {
		fprintf(stderr, "%s: read: %s: %s\n", progname, pcFile, strerror(iRet));
		exit(EX_OSERR);
	}

	/* Record our new state, if we are supposed to, in the db file
	 */
	pSS->wlength = lseek(iFd, 0l, SEEK_CUR);
	if (!fDelete)
		since_to_log(pSS, pcSinceDB);
}

/* read the data from a file the called has open (maybe flockd)		(ksb)
 */
static void
SinceFd(char *pcName, int iFd)
{
	auto struct stat stFd;
	auto int iWasIn;

	if (isatty(iFd)) {
		fprintf(stderr, "%s: no targets given\n", progname);
		exit(EX_USAGE);
	}
	if (-1 == fstat(iFd, & stFd)) {
		fprintf(stderr, "%s: fstat: %d: %s\n", progname, iFd, strerror(errno));
		exit(EX_OSERR);
	}
	if (S_ISREG(stFd.st_mode)) {
		SinceFile(pcName, &stFd, iFd);
	} else if (!S_ISDIR(stFd.st_mode)) {
		fprintf(stderr, "%s: %s: must be either a plain file or a directory\n", progname, pcName);
		exit(EX_DATAERR);
	}
	if (-1 == (iWasIn = open(".", O_RDONLY, 0711))) {
		fprintf(stderr, "%s: open: %s: %s\n", progname, ".", strerror(errno));
		exit(EX_NOINPUT);
	}
	if (-1 == fchdir(iFd)) {
		fprintf(stderr, "%s: fchdir: %d: %s\n", progname, iFd, strerror(errno));
		return;
	}
	SinceDir(".", &stFd, pcName);
	if (-1 == fchdir(iWasIn)) {
		fprintf(stderr, "%s: fchdir: %s: %s\n", progname, "back", strerror(errno));
		exit(EX_NOINPUT);
	}
	close(iWasIn);
}

/* find out which way to since the input node				(ksb)
 */
static void
Since(char *pcTarget)
{
	auto struct stat stArg;
	register int iFd;

	if ('-' == pcTarget[0] && '\000' == pcTarget[1]) {
		SinceFd(pcTarget, 0);
		return;
	}
	if (-1 == stat(pcTarget, & stArg)) {
		fprintf(stderr, "%s: stat: %s: %s\n", progname, pcTarget, strerror(errno));
		exit(EX_OSERR);
	}
	if (S_ISREG(stArg.st_mode)) {
		/* nada, we like plain files */;
	} else if (S_ISDIR(stArg.st_mode)) {
		SinceDir(pcTarget, &stArg, pcTarget);
		return;
	} else {
		fprintf(stderr, "%s: %s: not a plain file or directory\n", progname, pcTarget);
		exit(EX_DATAERR);
	}
	if (-1 == (iFd = open(pcTarget, O_RDONLY, 0))) {
		fprintf(stderr, "%s: open: %s: %s\n", progname, pcTarget, strerror(errno));
		exit(EX_NOINPUT);
	}
	SinceFile(pcTarget, &stArg, iFd);
	close(iFd);
}
%%
