
/*
 * Copyright (c) 2004
 * All rights reserved.
 * Each circfile object maintains an internal mutex lock.  The functions
 * circfile_num_entries(), circfile_get(), circfile_put(), circfile_trim(), and
 * circfile_sync() may be safely called simultaneously from different threads.
 * 
 */

#include <sys/types.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <netinet/in.h>

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <syslog.h>
#include <unistd.h>
#include <errno.h>
#include <assert.h>
#include <string.h>
#include <pthread.h>
#include <fcntl.h>

#include "circfile.h"
#include "alog.h"

#define CIRCFILE_MAGIC		0x476ea198

#define MAX_ENTRIES		(1 << 20)
#define MAX_DATA		(1 << 24)

#define ALIGNBYTES              (sizeof(size_t) - 1)            /* ?? */
#define ALIGN(p)                (((unsigned)(p) + ALIGNBYTES) & ~ALIGNBYTES)

/* Structure passed back to client */
struct circfile {
   struct loghead  *head;	/* mmap'd file region */
   pthread_mutex_t mutex;	/* mutex lock */
   uint32_t        headlen;	/* length of head + entries */
   int             fd;		/* file descriptor for file */
};

struct logent {
   uint32_t offset;
   uint32_t length;
};

/* Initial part of a log file; all fields in host order */
struct loghead {
   uint32_t       magic;	/* magic number */
   uint32_t       maxent;	/* max # entries */
   uint32_t       maxdata;	/* max data area length */
   uint32_t       num;		/* number of valid entries */
   uint32_t       next;		/* next entry index */
#if defined(__SUNPRO_CC) || defined(__SUNPRO_C)
   struct logent  ents[1];	/* maxent entries */
#else
   struct logent  ents[0];	/* maxent entries */
#endif
};

/*
 * Open/create a new circfile.
 * circfile_open() opens the log file with pathname path.  flags may be equal
 * to zero, O_SHLOCK, or O_EXLOCK for locking purposes (see open for
 * details).	If the file is locked, circfile_open() does not block, but
 * instead returns NULL immediately with errno set to EWOULDBLOCK.
 *
 * If the named file exists, it must be a valid circfile file and maxent and
 * maxdata are ignored.  Otherwise, it is created using those parameters:
 * maxent limits the total number of entries that the file may contain, and
 * maxdata limit the total amount of entry data (in bytes) that the file may
 * contain.  When full, the file's ultimate size will be approximately
 * maxdata + (8 * maxent) + 20.
 *
 * The path may be NULL, in which case the circfile is not stored in the file
 * system and therefore is not persistent.
 */
struct circfile *
circfile_open(const char *path, int flags, uint32_t maxent, uint32_t maxdata)
{
   struct circfile *cf;
   struct loghead head;
   int initialize;
   int esave;

   /* Get and sanity check flags */
   switch (flags) {
      case 0:
#ifdef O_SHLOCK
      case O_SHLOCK:
      case O_EXLOCK:
#endif
      break;
      default:
	 errno = EINVAL;
         return (NULL);
   }
#ifdef O_SHLOCK
   if ((flags & (O_SHLOCK|O_EXLOCK)) != 0)
      flags |= O_NONBLOCK;
#endif

   /* Create object and open file */
   if ((cf = (struct circfile*)malloc(sizeof(*cf))) == NULL)
      return (NULL);
   memset(cf, 0, sizeof(*cf));
   if (path != NULL) {
      if ((cf->fd = open(path, O_CREAT|O_RDWR|flags, 0644)) == -1)
         goto fail;
      (void)fcntl(cf->fd, F_SETFD, 1);
   } else
      cf->fd = -1;

  /* See if file already existed */
   if (cf->fd != -1) {
      struct stat sb;

      if (fstat(cf->fd, &sb) == -1)
         goto fail;
      if (!(initialize = (sb.st_size == 0))) {
         int r;

	 if ((r = read(cf->fd, &head, sizeof(head))) != sizeof(head)) {
	    if (r != -1)
	       errno = EINVAL;
	    goto fail;
	 }
	 if (head.magic != CIRCFILE_MAGIC) {
	    errno = EINVAL;
	    goto fail;
	 }
	 maxdata = head.maxdata;
	 maxent = head.maxent;
      }
   } else {
      initialize = 1;
   }

   /* Sanity check parameters */
   if (maxent == 0 || maxdata == 0 || maxent > MAX_ENTRIES || maxdata > MAX_DATA) {
      errno = EINVAL;
      goto fail;
   }

   /* Compute size of header */
   cf->headlen = sizeof(*cf->head) + (maxent * sizeof(*cf->head->ents));

   /* Set file length */
   if (cf->fd != -1 && ftruncate(cf->fd, cf->headlen + maxdata) == -1)
      goto fail;

   /* Memory map file */
   if ((cf->head = (struct loghead*)mmap(NULL, cf->headlen + maxdata, PROT_READ|PROT_WRITE,
			path == NULL ? MAP_ANON : MAP_SHARED, cf->fd, 0)) == MAP_FAILED)
      goto fail;

   /* For new file, write header and initialize entries */
   if (initialize) {
      cf->head->magic = CIRCFILE_MAGIC;
      cf->head->maxdata = maxdata;
      cf->head->maxent = maxent;
      cf->head->num = 0;
      cf->head->next = 0;
      memset(cf->head->ents, 0, maxent * sizeof(*cf->head->ents));
      (void)msync((void*)cf->head, 0, MS_ASYNC);
   }

   /* Sanitize header fields */
   if (cf->head->num > cf->head->maxent)
      cf->head->num = cf->head->maxent;
   cf->head->next %= cf->head->maxent;

   /* Initialize mutex */
   if ((errno = pthread_mutex_init(&cf->mutex, NULL)) != 0)
      goto fail;

   /* Done */
   return (cf);

 fail:
   esave = errno;
   if (cf->fd != -1)
      (void)close(cf->fd);
   if (cf->head != NULL)
      (void)munmap((void*)cf->head, cf->headlen + maxdata);
   free(cf);
   errno = esave;
   return (NULL);
}

/*
 * Close a circfile.
 * circfile_close() closes a log file previously opened using circfile_open().
 * Upon return, *cfp will be set to NULL.  If *cfp is already equal to NULL
 * when circfile_close() is invoked, nothing happens.
 */
void
circfile_close(struct circfile **cfp)
{
   struct circfile *const cf = *cfp;

   /* Check for NULL */
   if (cf == NULL)
      return;
   *cfp = NULL;

   /* Close up shop */
   (void)msync((void*)cf->head, 0, MS_SYNC);
   (void)munmap((void*)cf->head, cf->headlen + cf->head->maxdata);
   if (cf->fd != -1)
      (void)close(cf->fd);
   pthread_mutex_destroy(&cf->mutex);
   free(cf);
}

/*
 * Get the number of valid entries in a circfile.
 * circfile_num_entries() returns the number of valid entries contained in a
 * log file.
 */
uint32_t
circfile_num_entries(struct circfile *cf)
{
   uint32_t num;
   int r;

   r = pthread_mutex_lock(&cf->mutex);
   assert(r == 0);
   num = cf->head->num;
   r = pthread_mutex_unlock(&cf->mutex);
   assert(r == 0);
   return (num);
}

/*
 * Trim the number of stored entries.
 * circfile_trim() discards the oldest entries in a log file as necessary to
 * make the total number of entries be at most num.
 */
void
circfile_trim(struct circfile *cf, int num)
{
   int r;

   r = pthread_mutex_lock(&cf->mutex);
   assert(r == 0);
   if (cf->head->num > num)
      cf->head->num = num;
   r = pthread_mutex_unlock(&cf->mutex);
   assert(r == 0);
}

/*
 * Retrieve an entry.
 * circfile_get() retrieves an entry from a log file.	which must be a nega-
 * tive number: -1 is the most recently added entry, -2 is the second most
 * recently added, etc.  If lenp is not equal to NULL, then *lenp is set to
 * the length of the entry.  Entries returned by circfile_get() are contigu-
 * ous and suitably aligned for any type.  The caller should not free the
 * returned pointer.
 */
const void *
circfile_get(struct circfile *cf, int which, int *lenp)
{
   struct loghead *const head = cf->head;
   struct logent *ent;
   const void *rtn;
   int r;

   /* Lock circfile */
   r = pthread_mutex_lock(&cf->mutex);
   assert(r == 0);

   /* Find entry */
   if (which >= 0 || which < -head->num) {
      r = pthread_mutex_unlock(&cf->mutex);
      assert(r == 0);
      errno = ENOENT;
      return (NULL);
   }
   ent = &head->ents[(head->next + head->maxent + which) % head->maxent];

   /* Sanity check it */
   if (ent->offset > head->maxdata
       || ent->length > head->maxdata
       || ent->offset + ent->length > head->maxdata) {
      r = pthread_mutex_unlock(&cf->mutex);
      assert(r == 0);
      errno = EINVAL;
      return (NULL);
   }

   /* Get data and length */
   if (lenp != NULL)
      *lenp = ent->length;
   rtn = (u_char *)cf->head + cf->headlen + ent->offset;

   /* Unlock circfile */
   r = pthread_mutex_unlock(&cf->mutex);
   assert(r == 0);

   /* Done */
   return (rtn);
}

/*
 * Put an entry.
 * circfile_put() adds a new entry to a log file.  The entry is pointed to by
 *  data and has length len.
 */
int
circfile_put(struct circfile *cf, const void *data, int len)
{
   struct loghead *const head = cf->head;
   struct logent *ent;
   uint32_t start;
   int wrap = 0;
   int r;

   if (len < 0) {
      errno = EINVAL;
      return (-1);
   }
   if (len > head->maxdata) {
      errno = EMSGSIZE;
      return (-1);
   }

   /* Lock circfile */
   r = pthread_mutex_lock(&cf->mutex);
   assert(r == 0);

   /* Figure out where this entry's data will go */
   if (head->num > 0) {
      ent = &head->ents[(head->next + head->maxent - 1) % head->maxent];
      start = ALIGN(ent->offset + ent->length);
      if (start + len > head->maxdata) {	/* won't fit, wrap it */
	  wrap = start;	/* point where we were forced to wrap */
	  start = 0;
      }
   } else {
      head->next = 0;
      start = 0;
   }

   /* Remove all entries whose data overlaps the new guy's data */
   for ( ; head->num > 0; head->num--) {
      ent = &head->ents[(head->next + head->maxent - head->num) % head->maxent];
      if (wrap != 0) {	/* clear out end region we skipped */
	 if (ent->offset >= wrap)
	   continue;
	 wrap = 0;
      }
      if (ent->offset + ent->length <= start
	  || ent->offset >= start + len)
	 break;
   }

   /* Save entry */
   ent = &head->ents[head->next];
   ent->offset = start;
   ent->length = len;
   memcpy((u_char *)cf->head + cf->headlen + ent->offset, data, len);
   if (head->num < head->maxent)
      head->num++;
   head->next = (head->next + 1) % head->maxent;

   /* Unlock circfile */
   r = pthread_mutex_unlock(&cf->mutex);
   assert(r == 0);
   
   /* Done */
   return (0);
}

/*
 * Sync circfile to disk.
 * circfile_sync() synchronously flushes any unwritten entries to permanent
 * storage.
 */
void
circfile_sync(struct circfile *cf)
{
   int r;

   r = pthread_mutex_lock(&cf->mutex);
   assert(r == 0);
   (void)msync((void*)cf->head, 0, MS_SYNC);
   r = pthread_mutex_unlock(&cf->mutex);
   assert(r == 0);
}

#ifdef CIRCFILE_TEST


int
main(int ac, char **av)
{
   const time_t now = time(NULL);
   struct circfile *cf;
   int maxent = 0;
   int maxdata = 0;
   int readonly = 0;
   int alog_ents = 0;
   long seed = 0;
   int num = -1;
   char *path;
   int i;
   int ch;

   srandom(time(NULL));

   while ((ch = getopt(ac, av, "s:n:ra")) != -1) {
      switch (ch) {
         case 's':
	    seed = atol(optarg);
	    break;
         case 'n':
	   num = atol(optarg);
	   break;
         case 'a':
	   alog_ents = 1;
	   break;
         case 'r':
	   readonly = 1;
	   break;
         default:
	   goto usage;
      }
   }
   ac -= optind;
   av += optind;

   /* Sanity */
   if (!readonly)
      alog_ents = 0;

   if (!readonly && seed == 0) {
      seed = random();
      printf("Seed is %ld.\n", seed);
   }
   srandom(seed);

   switch (ac) {
      case 3:
	maxent = atoi(av[1]);
	maxdata = atoi(av[2]);
	/* fall through */
      case 1:
	path = av[0];
	break;
      default:
  usage: fprintf(stderr, "usage: circfile [-r] [-s seed] [-n num]"
				" path [maxent maxdata]\n");
	exit(1);
   }

   /* Open log */
   if ((cf = circfile_open(strcmp(path, "-") == 0 ? NULL : path, 0,
			  maxent, maxdata)) == NULL)
      fprintf(stderr, "Error : %s", path);

   /* Read only? */
   if (readonly)
      goto readit;

   /* Write some entries into it */
   printf("Circfile \"%s\" has %d entries.\n", path, circfile_num_entries(cf));
   if (num == -1)
      num = random() % 64;
   printf("Writing %d entries...\n", num);
   for (i = 0; i < num; i++) {
      char buf[256];
      int nex;
      int j;

      snprintf(buf, sizeof(buf), "%03ld:%03d ", now % 1000, i);
      nex = random() % 32;
      for (j = 0; j < nex; j++) {
	 snprintf(buf + strlen(buf), sizeof(buf) - strlen(buf),
		  "%02lx", random() % 0x100);
      }
      strcat(buf, "\n", sizeof(buf));
      if (circfile_put(cf, buf, strlen(buf) + 1) == -1)
         printf("warning: circfile_put: \"%s\"", buf);
   }

 readit:
   num = circfile_num_entries(cf);
   printf("Circfile \"%s\" now has %d entries\n", path, num);
   printf("\t maxent=%u\n", cf->head->maxent);
   printf("\tmaxdata=%u\n", cf->head->maxdata);
   printf("\t   next=%u\n", cf->head->next);
   for (i = -num; i < 0; i++) {
      const void *e;
      int len;

      printf("%4d: ", i + num);
      if ((e = circfile_get(cf, i, &len)) == NULL) {
         printf("warning: circfile_get(%d)", i);
	 continue;
      }
      if (alog_ents) {
         const struct alog_entry *const ent = e;
	 struct tm tm;
	 char tbuf[64];

	 strftime(tbuf, sizeof(tbuf),
		  "%b %e %T", (const struct tm *)localtime_r(&ent->when, &tm));
	 printf("%s [%d] %s\n", tbuf, ent->sev, ent->msg);
      } else
         printf("(%2d) %s", len, (const char *)e);
   }
   printf("Closing circfile...\n");
   circfile_close(&cf);
   return (0);
}

#endif /* CIRCFILE_TEST */


