/* $Id: lockfile.C,v 1.6 2002/05/15 23:23:54 dm Exp $ */

/*
 *
 * Copyright (C) 1999 David Mazieres (dm@uun.org)
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2, or (at
 * your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 *
 */

#include "amisc.h"

bool
stat_unchanged (const struct stat *sb1, const struct stat *sb2)
{
  return sb1->st_dev == sb2->st_dev && sb1->st_ino == sb2->st_ino
    && sb1->st_mtime == sb2->st_mtime
#ifdef SFS_HAVE_STAT_ST_MTIMESPEC
    && sb1->st_mtimespec.tv_nsec == sb2->st_mtimespec.tv_nsec
#endif /* SFS_HAVE_STAT_ST_MTIMESPEC */
    && sb1->st_size == sb2->st_size;
}

static bool
checkstat (const str &path, const struct stat &sb)
{
  if (!S_ISREG (sb.st_mode))
    warn << path << ": not a regular file -- please delete\n";
  else if (sb.st_nlink > 1)
    warn << path << ": too many links -- please delete\n";
  else if (sb.st_mode & 07177)
    warn ("%s: mode 0%o should be 0600 -- please delete\n",
	  path.cstr (), int (sb.st_mode & 07777));
  else if (sb.st_size)
    warn << path << ": file should be empty -- please delete\n";
  else
    return true;
  return false;
}

lockfile::~lockfile ()
{
  if (!ok ())
    warn << path << ": stolen lockfile\n";
  else
    unlink (path);
  flock (fd, LOCK_UN);
  close (fd);
}

bool
lockfile::ok () const
{
  struct stat sb, fsb;
  if (stat (path, &sb) < 0 || fstat (fd, &fsb) < 0
      || !stat_unchanged (&sb, &fsb))
    return false;
  return true;
}

ptr<lockfile>
lockfile::alloc (const str &path, bool wait)
{
  struct stat sb, fsb;

  errno = 0;
  if (lstat (path, &sb) >= 0 && !checkstat (path, sb))
      return NULL;
  else if (errno && errno != ENOENT) {
    warn << path << ": " << strerror (errno) << "\n";
    return NULL;
  }

  int fd;
 openit:
  /* N.B., 0600 (not 0644) to stop others from wedging us with
   * LOCK_SH.  Anyone with read permission to a file can flock it
   * with LOCK_SH, thereby blocking those with write permission who
   * want to lock it with LOCK_EX. */
  fd = open (path, O_RDWR|O_CREAT, 0600);
  if (fd < 0) {
    warn << path << ": " << strerror (errno) << "\n";
    return NULL;
  }
  if (flock (fd, LOCK_EX | (wait ? 0 : LOCK_NB)) < 0) {
    close (fd);
    if (wait && errno == EINTR)
      goto openit;
    return NULL;
  }
  utimes (path, NULL);
  errno = 0;
  if (fstat (fd, &fsb) < 0 || !checkstat (path, fsb)) {
    if (errno)
      warn << "fstat (" << path << "): " << strerror (errno) << "\n";
    flock (fd, LOCK_UN);
    close (fd);
    return NULL;
  }
  if (lstat (path, &sb) < 0 || !stat_unchanged (&fsb, &sb)) {
    flock (fd, LOCK_UN);
    close (fd);
    goto openit;
  }
  close_on_exec (fd);
  return New refcounted<lockfile> (path, fd);
}
