/*
 * Copyright (c) 2002, The EROS Group, LLC and Johns Hopkins
 * University. All rights reserved.
 * 
 * This software was developed to support the EROS secure operating
 * system project (http://www.eros-os.org). The latest version of
 * the OpenCM software can be found at http://www.opencm.org.
 * 
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the following
 * conditions are met:
 * 
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 
 * 2. Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the following
 *    disclaimer in the documentation and/or other materials
 *    provided with the distribution.
 * 
 * 3. Neither the name of the The EROS Group, LLC nor the name of
 *    Johns Hopkins University, nor the names of its contributors
 *    may be used to endorse or promote products derived from this
 *    software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <opencm.h>

#ifndef __unix__
#error "This source file provides UNIX-specific support."
#endif

#include <sys/time.h>

/* IRIX needs statvfs.h to call statvfs

   On Solaris, we get away with including vfs.h, but the 2.6 statvfs manpage
   shows that statvfs.h is the proper include to use.

   OpenCM hasn't been tested on any other SVR4 variants yet.
      - JL (July 18, 2002)
*/
#ifdef HAVE_SVR4_STATVFS
#include <sys/types.h>
#include <sys/statvfs.h>
#endif

#ifdef HAVE_SYS_STATFS_H
#include <sys/statfs.h>
#endif

#ifdef HAVE_SYS_VFS_H
#include <sys/vfs.h>
#endif

#ifdef HAVE_SYS_PARAM_H
#include <sys/param.h>
#endif

#ifdef HAVE_SYS_MOUNT_H
#include <sys/mount.h>
#endif

#ifdef __linux__
#define NFS_SUPER_MAGIC  0x6969
#endif /* __linux__ */

#include <pwd.h>
#include <unistd.h>

char *
os_GetUserName()
{
  
  struct passwd *pw;

  uid_t uid = getuid();
  pw = getpwuid(uid);

  if (strlen(pw->pw_gecos) != 0)
    return xstrdup(pw->pw_gecos);
  else
    return xstrcat(xstrcat(pw->pw_name, "@"),
		   os_GetHostName());
}

char *
os_GetHostName()
{
  char hostbuf[1024];
  gethostname(hostbuf, 1024);

  return xstrdup(hostbuf);
}

char *
os_GetHomeDir()
{
  return getenv("HOME");
}

struct servent *
os_GetServicePort(const char *name, const char *proto)
{
  return getservbyname(name, proto);
}

/* Returns GMT time! */
char *
os_ToISO(time_t local_secs)
{
  char time_string[40];
  size_t len = 40;
  struct tm *tm_utc;
  tm_utc = gmtime(&local_secs);

  len = strftime(time_string, len, "%Y-%m-%dT%H:%M:%S", tm_utc);
  return xstrcat(time_string, "GMT");
}

time_t
os_FromISO(const char *isoTime)
{
  struct tm tm;
  struct tm local_tm;
  struct tm gmt_tm;
  time_t now_local_secs;

  bzero(&tm, sizeof(tm));
  bzero(&local_tm, sizeof(local_tm));
  bzero(&gmt_tm, sizeof(gmt_tm));

  /* Get a representation of "now" in local timezone */
  now_local_secs = time(0);
  localtime_r(&now_local_secs, &local_tm);
  gmtime_r(&now_local_secs, &gmt_tm);

  /* Grab off the year */
  while (isdigit(*isoTime)) {
    tm.tm_year *= 10;
    tm.tm_year += (*isoTime - '0');

    isoTime++;
  }

  isoTime++;           /* skip the '-' */

  /* Grab off the month */
  while (isdigit(*isoTime)) {
    tm.tm_mon *= 10;
    tm.tm_mon += (*isoTime - '0');

    isoTime++;
  }

  isoTime++;           /* skip the '-' */

  /* Grab off the day */
  while (isdigit(*isoTime)) {
    tm.tm_mday *= 10;
    tm.tm_mday += (*isoTime - '0');

    isoTime++;
  }

  isoTime++;           /* skip the 'T' */

  /* Grab off the hour */
  while (isdigit(*isoTime)) {
    tm.tm_hour *= 10;
    tm.tm_hour += (*isoTime - '0');

    isoTime++;
  }

  isoTime++;           /* skip the ':' */

  /* Grab off the minute */
  while (isdigit(*isoTime)) {
    tm.tm_min *= 10;
    tm.tm_min += (*isoTime - '0');

    isoTime++;
  }

  isoTime++;           /* skip the ':' */

  /* Grab off the second */
  while (isdigit(*isoTime)) {
    tm.tm_sec *= 10;
    tm.tm_sec += (*isoTime - '0');

    isoTime++;
  }

  /* There should not be fractional seconds (yet): */
  if (*isoTime == '.')
    THROW(ExBadValue, 
     "Cannot handle fractional seconds in modtimes (yet)");

  /* Skip fractional component, if any */
  while (isdigit(*isoTime) || *isoTime == '.' || isspace(*isoTime))
    isoTime++;

  if (strcmp(isoTime, "GMT") != 0)
    THROW(ExBadValue, 
     format("Don't know how to decode non-GMT iso time \"%s\"\n",
        isoTime));

  /* mktime() demands a struct tm that is expressed in *local* time. 
     While BSD systems have added a GMT offset field to struct TM, the
     mktime() implementation is unable to use this for reasons of
     compatibility. We therefore take the above-constructed struct tm
     whose values reflect GMT and apply the deltas derived from a
     comparison of the current time expressed locally and as GMT. */

  tm.tm_year -= 1900;      /* UNIX time baseline */
  tm.tm_mon -= 1;      /* Jan is month zero */

  /* now apply the bias between local time and gmtime to all of the
     fields that are used by mktime (what a nuisance!) */
  tm.tm_year += (local_tm.tm_year - gmt_tm.tm_year);
  tm.tm_mon  += (local_tm.tm_mon - gmt_tm.tm_mon);
  tm.tm_mday += (local_tm.tm_mday - gmt_tm.tm_mday);
  tm.tm_hour += (local_tm.tm_hour - gmt_tm.tm_hour);
  tm.tm_min  += (local_tm.tm_min - gmt_tm.tm_min);
  tm.tm_sec  += (local_tm.tm_sec - gmt_tm.tm_sec);

  /* Pick up the DST flag from local time, which may be wrong if the
     user mis-set the timezone, but it is wrong in a way that is
     consistent with what the behavior of mktime() will be: */
  tm.tm_isdst = local_tm.tm_isdst;

  return mktime(&tm);
}

char *
os_GetISOTime(void)
{
  struct timeval tv;
  time_t now_secs;
  struct tm *tm_now;

  char time_string[40];
  char time_frac[7];		/* microseconds fraction, 6 digits */
  size_t len = 40;

  /* This is mildly tricky, because we need the microseconds component
     to deliver a decently accurate time string. Use gettimeofday(2),
     copy the seconds component over to a time_t value that can be
     passed to strftime(), and then mangle the resulting string to
     patch in the decimal seconds fraction per ISO-8601. */

  gettimeofday(&tv, 0);
  now_secs = tv.tv_sec;
  tm_now = gmtime(&now_secs);

  len = strftime(time_string, len, "%Y-%m-%dT%H:%M:%S.", tm_now);
  sprintf(time_frac, "%06lu", tv.tv_usec);

  return xstrcat(time_string, xstrcat(time_frac,"GMT"));
}

uint32_t
os_GetPid(void)
{
  pid_t mypid = getpid();
  return mypid;
}

static OC_bool
try_create_local_lockfile(const char *path)
{
  OC_bool result = FALSE;
  int fd;
  uint32_t mypid = os_GetPid();
  const char *tmpPath = format("%s.%d", path, mypid);
  const char *contents = format("%d\n", mypid);

  fd = creat(tmpPath, 0666);
  if (fd < 0)
    THROW(ExNoObject, 
	  format("Could not create temporary file %s for locking (errno %d)", path, errno));
  
  write(fd, contents, strlen(contents));
  close(fd);

  if (link(tmpPath, path) >= 0)
    result = TRUE;

  unlink(tmpPath);

  onthrow_remove(path);

  return result;
}

OC_bool
os_pid_exists(uint32_t pid)
{
  /* Send sig==0, which will check to see if the process exists, but no actual signal
     will be sent to the process */
  int result = kill(pid, 0); /* error check, no real signal */
  if (result < 0 && errno == ESRCH)
    return FALSE;
  return TRUE;
}

OC_bool
os_validate_local_lockfile(const char *path)
{
  char *buf;
  struct stat statbuf;
  int lfd;

  /* Check if the process allegedly holding the lock file still
     exists. It may have died leaving an orphaned lock file. Note
     that we may be running this algorithm in parallel with other
     applications, so the low-level system calls may fail. This is
     perfectly okay, as it means that the lock file has been nuked
     by the other process. */

  if (stat(path, &statbuf) < 0) {
    if (errno == ENOENT)
      return TRUE;		/* make enclosing loop retry */

    THROW(ExLockFail,
	  format("Could not validate lock file \"%s\" (errno %d)",
		 path, errno));
  }

  if ((lfd = open(path, O_RDONLY)) < 0) {
    if (errno == ENOENT)
      return TRUE;		/* make enclosing loop retry */

    THROW(ExLockFail,
	  format("Could not validate lock file \"%s\" (errno %d)",
		 path, errno));
  }

  buf = GC_MALLOC_ATOMIC(statbuf.st_size + 1);
  read(lfd, buf, statbuf.st_size);
  buf[statbuf.st_size] = 0;

  close(lfd);

  {
    uint32_t lockPid = strtoul(buf, 0, 0);

    if (os_pid_exists(lockPid))
      return TRUE;
    else
      return FALSE;
  }
}

void
os_create_local_lockfile(const char *path)
{
  uint32_t count;

  for(count = 0; count < 400; count++) {	/* MAX 20 seconds */
    OC_bool try = try_create_local_lockfile(path);

    if (try == TRUE)
      return;

    {
      /* Couldn't grab it. Check validity. Sleep if valid */
      OC_bool valid = os_validate_local_lockfile(path);

      if (valid == FALSE) {
	const char *metapath = xstrcat(path, ".META");
      
	OC_bool meta = try_create_local_lockfile(metapath);

	if (meta) {
	  unlink(path);
	  unlink(metapath);
	}
	else
	  os_nap(10);		/* allow competitor time to clean it */
      }
    }

    os_nap(50);			/* wait for competitor to finish */
  }

  THROW(ExLockFail,
	format("Could not establish lock file \"%s\" "
	       "(errno %d) Remove by hand?",
	       path, errno));
}

void
os_nap(uint32_t ms)
{
  struct timeval tv;
  tv.tv_sec = 0;
  tv.tv_usec = ms * 1000;

  select(0, 0, 0, 0, &tv);
}

#ifdef HAVE_SVR4_STATVFS

#  ifdef __NetBSD__
#  define BASETYPE	f_fstypename
#  define TYPE_LENGTH	_VFS_NAMELEN
#  else
#  define BASETYPE	f_basetype
#  define TYPE_LENGTH	FSTYPSZ
#  endif

OC_bool 
os_is_local_path(const char *path)
{
  struct statvfs sfs;
  statvfs(path, &sfs);

  if (strncmp(sfs.BASETYPE, "nfs", TYPE_LENGTH) == 0)
    return FALSE;
  return TRUE;
}
#else
OC_bool 
os_is_local_path(const char *path)
{
  struct statfs sfs;

  statfs(path, &sfs);

#ifdef __linux__
  switch(sfs.f_type) {
  case NFS_SUPER_MAGIC:
    return FALSE;
  default:
    return TRUE;
  }
#elif defined(__OpenBSD__)
  if (sfs.f_flags & MNT_LOCAL)
    return TRUE;
  return FALSE;
#else
  return TRUE;
#endif /* __linux __ */
}
#endif

void
os_background(void)
{
  if (setsid() == -1)
    THROW(ExSubprocess, 
	  format("Could not establish new session: %s\n",
		 strerror(errno)));
}

void
os_daemonize(void)
{
  int fd;
  pid_t pid;

  pid = fork();
  if (pid == -1)
    THROW(ExSubprocess, format("Daemon fork(): %s", strerror(errno)));

  if (pid != 0)
    do_exit(0);

  /*
   * We're the child.
   */

  os_background();

  /* FIX: This really should be closing stdout, stderr too, but I want
   * to be able to see the error messages until we get a real logging
   * mechanism in place. */

  fd = open("/dev/null", O_RDWR, 0);
  if (fd != -1) {
    close(STDIN_FILENO);
    (void)dup2(fd, STDIN_FILENO);
#if 0
    close(STDOUT_FILENO);
    (void)dup2(fd, STDOUT_FILENO);
    close(STDERR_FILENO);
    (void)dup2(fd, STDERR_FILENO);
#endif
    if (fd != STDIN_FILENO &&
	fd != STDOUT_FILENO &&
	fd != STDERR_FILENO)
      (void)close(fd);
  }
}
