/*
*
*	Copyright (c) 2001 Fredrik Sjoholm <fredrik@sjoholm.com>
*	All rights reserved.
*	License: GPL - The GNU General Public License
*
*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <time.h>
#include <string.h>



struct {
  char* data;
  int used;
  int size;
} buf;

struct FileInfo {
  char *name;
  int fd;
  struct stat stat;	// remember info about file so we can know if it's been rotated
} out;

struct {
  char timestamp;	// { 0 | 1 }
  char * time_format;
  int max_len;
  int zap_if_disk_full;
} conf;

#define DEFAULT_TIME_FORMAT "%Y%m%d;%T: "

volatile int gotHUP;

void growbuf(int size);
int openfile (struct FileInfo* file);
int filechanged (struct FileInfo* file);
void catchHUP (int sig);
void handleHUP ();
int dotime (char* prepend_to);

int main(int argc, char** argv)
{
  char *eol, *pos;
  int done = 0;
  int opt = 0;
  int totalwn = 0;
  int disk_is_full = 0;

  // look for switches
  conf.time_format = NULL;
  while (++opt < argc) {
    if (!strcmp(argv[opt], "-t")) {
      conf.timestamp = 1;
      conf.time_format = DEFAULT_TIME_FORMAT;
    }
    else if (!strcmp(argv[opt], "-T")) {
      opt++;
      if (opt < argc) {
        conf.timestamp = 1;
        conf.time_format = argv[opt];
      }
    }
    else if (!strcmp(argv[opt], "-l")) {
      opt++;
      if (opt < argc) {
	conf.max_len = atoi (argv[opt]);
      }
    }
    else if (!strcmp(argv[opt], "-z")) {
      conf.zap_if_disk_full = 1;
    }
    else {
      break;
    }
  }

  if (opt >= argc) {
    fprintf (stderr, 
	"Usage: pipeline| %s [options] {logfile|-}  # SIGHUP will reopen logfile (v1.8)\n"
	"	-t	     prepend each line with \"YYYYMMDD;HH:MM:SS: \"\n"
	"	-T <format>  prepend each line with specified strftime(3) format\n"
	"	-l <number>  log file length limit (force truncation)\n"
	"	-z           zap (truncate) log if disk gets full (default: grow buffer)\n",
	argv[0]);
    exit(1);
  }

  out.name = argv[opt];
  openfile (&out);

  signal (SIGHUP, catchHUP);

  growbuf (4096);

  while (!done) {
    int size;

    if (buf.used >= buf.size) growbuf (buf.size*2);

    size = read (0, buf.data+buf.used, buf.size-buf.used);
    if (size > 0) {
      buf.used += size;
    }
    else if (size == 0) {
      done = 1;
    }
    else { }

    for (;;) {

      if (gotHUP) {
	handleHUP();
	signal (SIGHUP, catchHUP);
	gotHUP = 0;
      }

      pos = buf.data;
      eol = (char*) memchr(buf.data, '\n', buf.used);
      if (eol == 0) break;
      eol ++;

      if (conf.timestamp && !disk_is_full) {
	pos -= dotime (pos);	// don't prepend time again if we already did it before disk got full.
      }

      while (pos < eol) {
	int wn = write(out.fd, pos, eol-pos);
	if (wn > 0) {
	  pos += wn;
	  totalwn += wn;
	  disk_is_full = 0;
	}
	else if (errno == ENOSPC) {
	  if (conf.zap_if_disk_full) {
	    char str[256];
	    if (ftruncate(out.fd,0)) {
	      fprintf(stderr, "truncating %s: %d %s\n", out.name, errno, strerror(errno));
	      exit(1);
	    }
	    write (out.fd, str, snprintf(str, sizeof(str), "Device full: truncating %s\n\n", out.name));
	  }
	  else {
	    disk_is_full = 1;
	    break;	// give up trying to write to disk until next input
	  }
	}
	else {
	  fprintf(stderr, "write %s: %d %s\n", out.name, errno, strerror(errno));
	  exit(1);
	}
	if (conf.max_len && (totalwn > conf.max_len)) {
	  char str[256];
	  if (ftruncate(out.fd,0)) {
	    fprintf(stderr, "truncating %s: %d %s\n", out.name, errno, strerror(errno));
	    exit(1);
	  }
	  write (out.fd, str, snprintf(str, sizeof(str), "File size limit: truncating %s\n\n", out.name));
	  totalwn = 0;
	}
      }

      buf.used = buf.data + buf.used - pos;
      memmove (buf.data, pos, buf.used);
      if (disk_is_full) { break; }
    }

  }



  exit(0);
}



void growbuf (int size)
{
  if (size > buf.size) {
    // pad beginning of buffer with 100 bytes used by dotime()
    // pad end with the same, so the memmove() at end of loop above is always safe
    // even in case of a full disk while writing a prepended timestamp.
    buf.data = (char*) realloc (buf.data ? buf.data-100 : 0, size+100*2) + 100;
    buf.size = size;
  }
}




int openfile (struct FileInfo* file)
{
  int fd;
  if (strcmp(file->name,"-") != 0) {
#ifdef O_LARGEFILE
    fd = open (file->name, O_CREAT|O_APPEND|O_WRONLY|O_LARGEFILE, 0666);
#else
    fd = open (file->name, O_CREAT|O_APPEND|O_WRONLY, 0666);
#endif
  } else {
    fd = 2;
  }
  if (fd < 0) {
    fprintf (stderr, "open write %s: %s\n", file->name, strerror(errno));
    exit(1);
  }
  file->fd = fd;
  stat (file->name, &file->stat);	// update stat buffer
  return fd;
}


int reopenfile (struct FileInfo* file)
{
  if (strcmp(file->name,"-") != 0) {
    close (file->fd);
    return openfile (file);
  } else {
    return file->fd;
  }
}





int filechanged (struct FileInfo* file)
{
  struct stat stat2;

  if (strcmp(file->name,"-") == 0) { return 0; }

  if (stat (file->name, &stat2) < 0) {
    return 1;	// file removed or something
  }

  if (stat2.st_ino != file->stat.st_ino || stat2.st_dev != file->stat.st_dev) {
    // file changed or was moved to a different device
    return 1;
  }
  
  return 0;
}



void catchHUP (int sig)
{
  gotHUP = 1;
}



void handleHUP ()
{
  char str[256];
  if (filechanged(&out)) {
    // only reopen file and print msg if the file on disk was changed or removed
    write (out.fd, str, snprintf (str, sizeof(str), "Caught SIGHUP: Reopening %s (closed)\n\n", out.name));
    reopenfile (&out);
    write (out.fd, str, snprintf (str, sizeof(str), "Caught SIGHUP: Reopening %s (opened)\n\n", out.name));
  }
}



// format: "YYYYMMDD;HH.MM.SS: "
// make sure it's safe to prepend before the prepend_to pointer.
int dotime (char* prepend_to)
{
  time_t now;
  struct tm *t;
  static char buf[100];
  int len;

  time (&now);
  t = localtime (&now);
  len = strftime(buf, sizeof(buf), conf.time_format, t);
  memcpy (prepend_to-len, buf, len);
  return len;
}




