/*
 * @(#) log.c  RCS: $Revision: 1.6 $ $Date: 95/03/08 15:54:50 $
 *
 * Logging facility
 *
 * #define _USE_SYSLOG if you want to use logging via a socket to syslogd
 * instead of logging to a file.  I don't trust syslog's socket to be secure,
 * so I don't use syslog;  Your needs may be different.  Also, syslog does
 * a fork(), making life even more complex.
 */
#define _POSIX_SOURCE
#include <unistd.h>		/* close write _exit */
#include <stdlib.h>		/* atexit */
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>		/* S_IRUSR S_IWUSR */
#include <fcntl.h>		/* O_RDWR */
#include <string.h>		/* strerror strlen for braindamaged vsprintf */
#include <errno.h>		/* errno */
#include <time.h>		/* time() localtime() strftime() struct tms */
#include <sys/utsname.h>	/* uname() struct utsname */
#if defined(_USE_SYSLOG)
#include <syslog.h>
#endif
#include "log.h"
#if (defined(sparc) || defined(sun3)) && !defined(__SOLARIS__)
#define atexit	on_exit
#endif

#define SYSLOG_FD	32767		/* run-time value to enable syslog */

#define LOGBUFSIZ	BUFSIZ		/* POSIX2 may have a better value */

static int  logFd = 2;			/* stderr */
static char logBuf[LOGBUFSIZ];
static int  logOpen = 0;  /* non-zero if openLog has ever been successful */
static char *syslogId = "";	/* long-lived storage */

extern char *progName;

/*
 * Close the log file if it's open.  Don't do anything if openLog has
 * never been called.  Shut down syslog if it was a syslog connection.
 */
void closeLog() 
{
   if (logOpen)	{		/* only close a log if it is open */
      if (logFd == SYSLOG_FD) {
#if defined(_USE_SYSLOG)	/* to allow machines w/o syslog */
	 closelog();
#endif
	 logFd = -1;
      }
      if (logFd >= 0) {
	 close(logFd);
      }
      logFd = -1;
      logOpen = 0;
   }
}

/*
 * Open the specified log file.  If the open fails, the existing logfile 
 * remains unchanged; if called multiple times on same log, works correctly.
 * the log() function acts like fprintf(stderr,...) until the first successful
 * call to openLog(), after which it formats like syslog().
 */
int openLog(fname)
   char *fname;
{
   register int newlog = SYSLOG_FD;	/* default is success */

#if defined(_USE_SYSLOG)
   if (logFd != SYSLOG_FD) {		/* I don't trust syslog to reopen OK */
#if !defined(__bsdi__) && !defined(linux)
      /* 
       * Return value is not documented in BSD brick.
       *
       * BSD is braindamaged: How do you detect failure? it can definitely fail
       * (It often forks and opens a TCP socket to a remote machine.)
       * God only knows what BSD does if you call openlog twice; do you?
       * If so, please send mail to the author, so I can fix this code properly.
       * If your host is one of the ones that does't return a result, add
       * "|| !defined(__mymachine) above and add a -D__mymachine to CFLAGS.
       *
       * The following machines do have a return value for syslog:
       *    HP-UX
       *    DEC Alpha OSF1
       *
       * I've been told that BSDI's BSD/386 doesn't.
       */
      newlog = 
#endif
      openlog(syslogId, LOG_NDELAY|LOG_NOWAIT, LOG_AUTH);
      /*
       * We must open the log connection immediately (NDELAY) so that we don't 
       * use file descriptors 0,1, or 2 for the log connection fd.
       */
      if (newlog >= 0) {
	 newlog = SYSLOG_FD;	/* a value to signal syslog is being used */
      }
   }
#else
   newlog = open(fname, O_CREAT|O_APPEND|O_WRONLY|O_NOCTTY, S_IRUSR|S_IWUSR);
#endif
   if (newlog < 0) {		
      return -1;
   }
   if (logFd != SYSLOG_FD) {
      closeLog();
   }
   logFd = newlog;
   logOpen = 1;
   newlog = atexit(closeLog);		/* close log file upon exit */
   return 0;
}

/*
 * Get the local host name  (not the domain name!)
 *
 * A bombproof version of BSD's gethostname, which always succeeds and
 * has a sensible return value. 
 *
 * returns in the array hostName the '\0' terminated name of the local host.  
 * The size argument specifies space (in bytes) available.  
 * Returns the number of characters stored not including the '\0'.   
 * If the host cannot be determined, 0 is returned.
 * The array returned is always '\0'-terminated except if size is 0.
 * Only the characters before the first dot '.' character are returned.
 *
 * Returns: the number of characters stored (not including the final '\0');
 */
static size_t getHostName(hostName, size)
   register char *hostName;
   register size_t size;
{
    struct utsname hstats;
    register char *chp;
    int res;
    register size_t i = 0;

    res = uname(&hstats);
    if ((res >= 0) && size) {
       chp = hstats.nodename;
       --size;
       if (size) do {
	  if ((*chp == '.') || (*chp == '\0')) {
	      break;
	  }
	  *hostName++ = *chp++;
	  i++;
       } while (--size);
       *hostName = '\0';
    }
    return i;
}

#if !defined(__STDC__) 	/* { */
#include <varargs.h>
void log(va_alist) va_dcl
#else			/* } { */
#include <stdarg.h>
#if defined(__STDC__) || defined(__cplusplus) 	/* { */
void log(char *fmt, ...) 
#else			/* } { */
void log(fmt) 
   char *fmt;
#endif	   /* } */
#endif	/* } */
{
   size_t count = 0;
   int ok = 1;
   char *chp = logBuf;
   time_t now = time((time_t *)0);
   va_list args;
#if !defined(__STDC__)	/* { */
   char *fmt;
   va_start(args); fmt = va_arg(args, char *);
#else /* } { */
   va_start(args , fmt);
#endif /* } */
   if (logFd != -1) {		/* do nothing if log fd not valid */
      if (logOpen && (logFd != SYSLOG_FD)) {
	 /*
	  * Prepend the date time, and hostname like syslog does.
	  */
	 /* strftime is in <time.h> for ANSI C */
	 count  = strftime(chp, 64, "%b %d %H:%M:%S ", localtime(&now));
	 chp   += count;
	 count  = getHostName(chp, 64);
	 chp   += count;
	 if (count != 0) {
	    *chp++ = ' ';
	    *chp = '\0';
	 }
	 if (chp != logBuf) {
	    *chp++ = ':';
	    *chp++ = ' ';
	    *chp = '\0';
	 }
      }
      count = vsprintf(chp, fmt, args);
      /* 
       * If you get a compiler warning on the vsprintf line stating
       * pointer to int  conversion, you're in big trouble.  This
       * attempts to "fix" the braindamage.
       *
       * ANSI C states vsprintf returns the character count.  
       * Some BSD machines (even when compiled with ANSI compilers!) 
       * return a pointer to the first argument instead.
       */
      if (((char *) count) == chp) {
	 while (*chp != '\0') {
	    chp++;
	 }
      } else {
	 chp += count;
      }
      count = chp - &logBuf[0];
      if (count > 0) {		/* if log message not empty */
	 /*
	  * This is a kludge for now.  Posix 2 has a constant LINE_MAX which
	  * may solve this problem.  Unfortunately, I don't have a copy of 
	  * the posix 2 standard.
	  */
	 if (count > LOGBUFSIZ) {
	    count = 37;	/* length of message below + 1 for the '\0' */
	    memcpy(logBuf, "PANIC! log.c:log() buffer overflow!\n", count);
	    ok = 0;
	 }
	 if (logFd == SYSLOG_FD) {
#if defined(_USE_SYSLOG)
	    syslog(LOG_INFO, "%-.*s", count, logBuf);
#endif
	 } else {
	    /* Use write here instead of stdio to make atomic updates */
	    write(logFd, logBuf, count);
	 }
	 if (!ok) {
	    closeLog();
	    _exit(1);
	 }
      }
   }

   va_end(args);
}
