// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
// vim: ts=8 sw=2 smarttab
/*
 * Ceph - scalable distributed file system
 *
 * Copyright (C) 2011 New Dream Network
 *
 * This is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1, as published by the Free Software
 * Foundation.  See file COPYING.
 *
 */

#include "common/debug.h"
#include "common/errno.h"
#include "common/safe_io.h"
#include "global/pidfile.h"

#include <errno.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#if defined(__FreeBSD__)
#include <sys/param.h>
#endif

#include "include/compat.h"

//
// derr can be used for functions exclusively called from pidfile_write
//
// cerr must be used for functions called by pidfile_remove because
// logging is not functional when it is called. cerr output is lost
// when the caller is daemonized but it will show if not (-f)
//
#define dout_prefix *_dout
#define dout_subsys ceph_subsys_

struct pidfh {
  int pf_fd;
  char pf_path[PATH_MAX + 1];
  dev_t pf_dev;
  ino_t pf_ino;

  pidfh() {
    reset();
  }
  ~pidfh() {
    remove();
  }

  bool is_open() {
    return pf_path[0] != '\0' && pf_fd != -1;
  }
  void reset() {
    pf_fd = -1;
    memset(pf_path, 0, sizeof(pf_path));
    pf_dev = 0;
    pf_ino = 0;
  }
  int verify();
  int remove();
  int open(const md_config_t *conf);
  int write();
};

static pidfh *pfh = nullptr;

int pidfh::verify() {
  // check that the file we opened still is the same
  if (pf_fd == -1)
    return -EINVAL;
  struct stat st;
  if (stat(pf_path, &st) == -1)
    return -errno;
  if (st.st_dev != pf_dev || st.st_ino != pf_ino)
    return -ESTALE;
  return 0;
}

int pidfh::remove()
{
  if (!pf_path[0])
    return 0;

  int ret;
  if ((ret = verify()) < 0) {
    if (pf_fd != -1) {
      ::close(pf_fd);
      reset();
    }
    return ret;
  }

  // seek to the beginning of the file before reading
  ret = ::lseek(pf_fd, 0, SEEK_SET);
  if (ret < 0) {
    std::cerr << __func__ << " lseek failed "
	      << cpp_strerror(errno) << std::endl;
    return -errno;
  }

  // check that the pid file still has our pid in it
  char buf[32];
  memset(buf, 0, sizeof(buf));
  ssize_t res = safe_read(pf_fd, buf, sizeof(buf));
  ::close(pf_fd);
  if (res < 0) {
    std::cerr << __func__ << " safe_read failed "
	      << cpp_strerror(-res) << std::endl;
    return res;
  }

  int a = atoi(buf);
  if (a != getpid()) {
    std::cerr << __func__ << " the pid found in the file is "
	      << a << " which is different from getpid() "
	      << getpid() << std::endl;
    return -EDOM;
  }
  ret = ::unlink(pf_path);
  if (ret < 0) {
    std::cerr << __func__ << " unlink " << pf_path << " failed "
	      << cpp_strerror(errno) << std::endl;
    return -errno;
  }
  reset();
  return 0;
}

int pidfh::open(const md_config_t *conf)
{
  int len = snprintf(pf_path, sizeof(pf_path),
		    "%s", conf->pid_file.c_str());

  if (len >= (int)sizeof(pf_path))
    return -ENAMETOOLONG;

  int fd;
  fd = ::open(pf_path, O_CREAT|O_RDWR, 0644);
  if (fd < 0) {
    int err = errno;
    derr << __func__ << ": failed to open pid file '"
	 << pf_path << "': " << cpp_strerror(err) << dendl;
    reset();
    return -err;
  }
  struct stat st;
  if (fstat(fd, &st) == -1) {
    int err = errno;
    derr << __func__ << ": failed to fstat pid file '"
	 << pf_path << "': " << cpp_strerror(err) << dendl;
    ::close(fd);
    reset();
    return -err;
  }

  pf_fd = fd;
  pf_dev = st.st_dev;
  pf_ino = st.st_ino;

  struct flock l = { F_WRLCK, SEEK_SET, 0, 0, 0 };
  int r = ::fcntl(pf_fd, F_SETLK, &l);
  if (r < 0) {
    derr << __func__ << ": failed to lock pidfile "
	 << pf_path << " because another process locked it." << dendl;
    ::close(pf_fd);
    reset();
    return -errno;
  }
  return 0;
}

int pidfh::write()
{
  if (!is_open())
    return 0;

  char buf[32];
  int len = snprintf(buf, sizeof(buf), "%d\n", getpid());
  if (::ftruncate(pf_fd, 0) < 0) {
    int err = errno;
    derr << __func__ << ": failed to ftruncate the pid file '"
	 << pf_path << "': " << cpp_strerror(err) << dendl;
    return err;
  }
  ssize_t res = safe_write(pf_fd, buf, len);
  if (res < 0) {
    derr << __func__ << ": failed to write to pid file '"
	 << pf_path << "': " << cpp_strerror(-res) << dendl;
    return res;
  }
  return 0;
}

void pidfile_remove()
{
  if (pfh != nullptr)
    delete pfh;
  pfh = nullptr;
}

int pidfile_write(const md_config_t *conf)
{
  if (conf->pid_file.empty()) {
    dout(0) << __func__ << ": ignore empty --pid-file" << dendl;
    return 0;
  }

  assert(pfh == nullptr);

  pfh = new pidfh();
  if (atexit(pidfile_remove)) {
    derr << __func__ << ": failed to set pidfile_remove function "
	 << "to run at exit." << dendl;
    return -EINVAL;
  }

  int r = pfh->open(conf);
  if (r != 0) {
    pidfile_remove();
    return r;
  }

  r = pfh->write();
  if (r != 0) {
    pidfile_remove();
    return r;
  }

  return 0;
}
