/* $Id: srvinfo.C,v 1.37 2001/07/02 14:59:19 dm Exp $ */

/*
 *
 * Copyright (C) 1998-2000 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 "sfscd.h"
#include "parseopt.h"
#include "sfsconnect.h"

#if FIX_MNTPOINT
#define MPDOT ".linuxmnt/"
#endif /* FIX_MNTPOINT */

static str rootnfsinfo;
static const u_int64_t badfsid = (u_int64_t) -1;
static ihash<const str, srvinfo, &srvinfo::path, &srvinfo::hlink> srvtab;

static inline str
mpof (str fs)
{
#if FIX_MNTPOINT
  if (opt_fix_mntpoint)
    return strbuf ("%s/" MPDOT, sfsroot) << fs << "/r";
  else
#endif /* FIX_MNTPOINT */
    return strbuf ("%s/", sfsroot) << fs;
}

void
update_devdb ()
{
  afs_root->unlink (".devdb");
  afs_root->link (afsreg::alloc (srvinfo::devlist ()), ".devdb");
  afs_root->unlink (".nfsdb");
  afs_root->link (afsrootfile::alloc (srvinfo::nfslist ()), ".nfsdb");
}

inline
srvinfo::srvinfo (const str &p)
  : tmo (NULL), waiting (true), destroyed (false), cdmounted (false),
    visible_flag (true), devno (badfsid), error (0), path (p), cdp (NULL)
{
  assert (!srvtab[p]);
  srvtab.insert (this);
}

inline
srvinfo::~srvinfo ()
{
  timecb_remove (tmo);
  afs_sfsroot->unlink (path);
#if FIX_MNTPOINT
  if (opt_fix_mntpoint)
    afs_linuxbug->unlink (path);
#endif /* FIX_MNTPOINT */

  srvtab.remove (this);
  if (cdp) {
    cdp->servers.remove (this);
    if (cdp->c) {
      nfspath3 p (path);
      cdp->c->call (SFSCDPROC_UNMOUNT, &p, NULL, aclnt_cb_null);
    }
  }
  if (waitq.empty ())
    flushpath (path);
  else
    for (size_t n = waitq.size (); n-- > 0;)
      (*waitq.pop_front ()) (error);

  update_devdb ();
}

void
srvinfo::timeout (bool start)
{
  tmo = NULL;
#if 0
  tmo = delaycb (destroyed ? 5 : 300, wrap (this, &srvinfo::timeout, false));
#else
  if (destroyed)
    tmo = delaycb (5, wrap (this, &srvinfo::timeout, false));
#endif
  if (!start)
    unmount ((destroyed ? NUOPT_FORCE : 0) | NUOPT_NLOG);
}

void
srvinfo::fail (int err)
{
  error = err;
  delete this;
}

int
srvinfo::geterr (const str &path)
{
  srvinfo *si = srvtab[path];
  if (si) {
    if (si->error)
      return si->error;
    if (!si->visible_flag)
      return EWOULDBLOCK;
  }
  return 0;
}

srvinfo *
srvinfo::lookup (const str &path)
{
  srvinfo *si = srvtab[path];
  if (si && !si->waiting)
    return si;
  return NULL;
}

void
srvinfo::alloc (const str &path, srvinfo::alloccb_t cb)
{
  srvinfo *si = srvtab[path];
  if (si) {
    if (si->waiting)
      si->waitq.push_back (cb);
    else
      (*cb) (si->error);
    return;
  }

  str hname;
  sfs_hash hid;
  u_int16_t port;
  if (sfs_parsepath (path, &hname, &hid, &port)) {
    si = New srvinfo (path);
    si->waitq.push_back (cb);
    tcpconnect (hname, port, wrap (si, &srvinfo::connected), false);
  }
  else if (namedprotrx.match (path)) {
    str prot (namedprotrx[1]);
    named_protocol *np = nptab[prot];
    if (!np) {
      warn << "unknown named protocol " << prot << "\n";
      (*cb) (EPROTONOSUPPORT);
    }
    else if (!np->cdp->c) {
      warn << "client for protocol " << prot << " is dead.\n";
      (*cb) (EAGAIN);
    }
    else {
      si = New srvinfo (path);
      si->cdp = np->cdp;
      si->cdp->servers.insert_head (si);
      si->waitq.push_back (cb);
      sfscd_mountarg arg;
      sfs_initci (&arg.carg, path, SFS_SFS, &sfs_extensions);
      si->cdp->c->call (SFSCDPROC_MOUNT, &arg, &si->mntres,
			wrap (si, &srvinfo::gotmntres));
    }
  }
  else
    (*cb) (ENOENT);
}

void
srvinfo::connected (int fd)
{
  if (fd < 0) {
    warn ("%s: %m\n", path.cstr ());
    fail (errno);
    return;
  }
  else {
    sockaddr_in sin;
    socklen_t len = sizeof (sin);
    bzero (&sin, sizeof (sin));
    if (getpeername (fd, reinterpret_cast<sockaddr *> (&sin), &len) < 0) {
      close (fd);
      warn ("%s: %m\n", path.cstr ());
      fail (errno);
      return;
    }
    if (badaddrs[sin.sin_addr]) {
      close (fd);
      warn ("%s: cannot connect to my own IP address\n", path.cstr ());
      fail (EDEADLK);
      return;
    }
  }

  tcp_nodelay (fd);
  ref<aclnt> c = aclnt::alloc (axprt_stream::alloc (fd), sfs_program_1);
  sfs_initci (&conarg, path, SFS_SFS, &sfs_extensions);
  sendconnect (c);
}

void
srvinfo::sendconnect (ref<aclnt> c)
{
  c->call (SFSPROC_CONNECT, &conarg, &conres,
	   wrap (this, &srvinfo::gotconres, c));
}

void
srvinfo::gotconres (ref<aclnt> c, clnt_stat err)
{
  if (err == RPC_CANTDECODEARGS && sfs_nextci (&conarg)) {
    sendconnect (c);
    return;
  }
  if (err) {
    warn << path << ": " << err << "\n";
    fail (EIO);
    return;
  }
  if (conres.status) {
    warn << path << ": " << conres.status << "\n";
    switch (conres.status) {
    default:
    case SFS_NOSUCHHOST:
      fail (ENOENT);
      return;
    case SFS_NOTSUPP:
      fail (EPROTONOSUPPORT);
      return;
    case SFS_TEMPERR:
      fail (EAGAIN);
      return;
    case SFS_REDIRECT:
      {
	ptr<revocation> r = revocation::alloc (*conres.revoke);
	if (r) {
	  timecb (timenow + 300, wrap (r, &revocation::nop));
	  flushpath (path);
	  for (size_t n = waitq.size (); n-- > 0;)
	    (*waitq.pop_front ()) (0);
	}
	fail (EAGAIN);
	return;
      }
    }
  }

  if (!sfs_ckpath (path, conres.reply->servinfo.host)) {
    warn << path << ": server is "
	 << sfs_hostinfo2path (conres.reply->servinfo.host) << "\n";
    fail (ENOENT);
    return;
  }

  cdp = release::cdlookup (conres.reply->servinfo.release,
			   conres.reply->servinfo.prog,
			   conres.reply->servinfo.vers);
  if (!cdp) {
    warn << path << ": no client for program "
	 << conres.reply->servinfo.prog << ", version "
	 << conres.reply->servinfo.vers
	 << " (release " << conres.reply->servinfo.release / 100
	 << "." << conres.reply->servinfo.release % 100
	 << ")\n";
    fail (EPROTONOSUPPORT);
    return;
  }
  cdp->servers.insert_head (this);

  if (!cdp->c) {
    warn << path << ": client for program "
	 << conres.reply->servinfo.prog << ", version "
	 << conres.reply->servinfo.vers << " is dead\n";
    fail (EAGAIN);
    return;
  }
  cdp->x->sendfd (static_cast<axprt_stream *> (c->xprt ().get ())->reclaim ());
  if (!cdp->c) {
    warn << path << ": client for program "
	 << conres.reply->servinfo.prog << ", version "
	 << conres.reply->servinfo.vers << " is dead\n";
    fail (EAGAIN);
    return;
  }

  sfscd_mountarg arg;
  arg.carg = conarg;
  *arg.cres.alloc () = *conres.reply;
  cdp->c->call (SFSCDPROC_MOUNT, &arg, &mntres,
		wrap (this, &srvinfo::gotmntres));
}

static str
sock2str (int fd)
{
  sockaddr_in sin;
  socklen_t sinlen = sizeof (sin);
  int n;
  bzero (&sin, sizeof (&sin));
  if (getsockname (fd, (sockaddr *) &sin, &sinlen) < 0
      || sin.sin_family != AF_INET)
    return "-";

  sinlen = sizeof (n);
  if (getsockopt (fd, SOL_SOCKET, SO_TYPE, (char *) &n, &sinlen) < 0)
    return "-";
  switch (n) {
  case SOCK_STREAM:
    return strbuf ("TCP:%d", ntohs (sin.sin_port));
  case SOCK_DGRAM:
    return strbuf ("UDP:%d", ntohs (sin.sin_port));
  default:
    return "-";
  }
}

void
srvinfo::gotmntres (clnt_stat err)
{
  if (err || mntres.err) {
    fail (EIO);
    return;
  }
  cdmounted = true;
  int fd = cdp->x->recvfd ();
  if (fd < 0) {
    warn << "srvinfo::gotmntres: did not receive file descriptor\n";
    fail (EIO);
    return;
  }

  nfsinfo = strbuf ()
    << hexdump (mntres.reply->fh.base (), mntres.reply->fh.size ())
    << " "
    << sock2str (fd);

#if FIX_MNTPOINT
  if (opt_fix_mntpoint) {
    afs_linuxbug->mkdir (path)->mkdir ("r");
    afs_sfsroot->symlink (mpof (path), path);
  }
  else
#endif /* FIX_MNTPOINT */
    afs_sfsroot->afsdir::mkdir (path);
  mnt_mount (fd, /* hostname*/ path, mpof (path),
	     mntres.reply->mntflags, mntres.reply->fh,
	     wrap (this, &srvinfo::gotnfsmntres));
}

void
srvinfo::gotnfsmntres (int err, u_int64_t fsid)
{
  if (err) {
    warn << "mount (" << path << "): " << strerror (err) << "\n";
    fail (err);
    return;
  }
  devno = fsid;
  update_devdb ();
  ready ();
}

void
srvinfo::ready ()
{
  waiting = false;
  while (!waitq.empty ())
    (*waitq.pop_front ()) (error);
  if (destroyed)
    unmount (NUOPT_FORCE);
  else
    timeout ();
}

void
srvinfo::unmountcb (cbi::ptr cb, int err)
{
  if (terminating && err != EBUSY && err != EAGAIN)
    err = 0;
  if (err)
    waiting = false;
  if (cb)
    (*cb) (err);
  if (!err) {
    if (waitq.empty ())
      delete this;
    else {
      vec<alloccb_t> wq;
      str p = path;
      swap (wq, waitq);
      delete this;
      while (!wq.empty ())
	srvinfo::alloc (p, wq.pop_front ());
    }
  }
}

void
srvinfo::unmount (int flags, cbi::ptr cb)
{
#if 0
  warn << "unmounting: " << path << " (flags = "
       << strbuf ("0x%x", flags) << ")\n";
#endif
  if (!waiting) {
    waiting = true;
    mnt_umount (mpof (path), flags, wrap (this, &srvinfo::unmountcb, cb));
  }
  else if (cb)
    (*cb) (EBUSY);
}

void
srvinfo::destroy (bool stale)
{
  if (waiting && stale) {
    destroyed = true;
    mnt_umount (mpof (path), NUOPT_STALE|NUOPT_NOOP, cbi_null);
  }
  else if (!destroyed) {
    timecb_remove (tmo);
    tmo = NULL;
    destroyed = true;
    unmount (NUOPT_FORCE | (stale ? NUOPT_STALE : 0));
    timeout ();
  }
}

void
srvinfo::destroy (const str &path, cdaemon *rcdp, bool stale)
{
  srvinfo *si = srvtab[path];
  if (si && (!rcdp || rcdp == si->cdp))
    si->destroy (stale);
}

void
srvinfo::idle (const str &path, cdaemon *rcdp)
{
  srvinfo *si = srvtab[path];
  if (si && (!rcdp || rcdp == si->cdp))
    si->unmount (NUOPT_NLOG);
}

void
srvinfo::show (const str &path, cdaemon *rcdp, bool showit)
{
  srvinfo *si = srvtab[path];
  if (si && (!rcdp || rcdp == si->cdp)) {
    si->visible_flag = showit;
    if (!showit && !si->waiting)
      si->unmount (NUOPT_NLOG);
  }
}

void
srvinfo::revoke (const str &path)
{
  srvinfo *si = srvtab[path];
  if (si) {
    if (si->cdp && si->cdp->c)
      si->cdp->c->call (SFSCDPROC_CONDEMN, &si->path, NULL, aclnt_cb_null);
    si->destroy (false);
  }
}

void
srvinfo::printdev (strbuf *sb, bool donfsinfo, srvinfo *si)
{
  if (si->devno != badfsid) {
    sb->fmt ("0x%" U64F "x ", si->devno) << si->path << " " << si->cdp->name;
    if (donfsinfo && si->nfsinfo)
      (*sb) << " " << si->nfsinfo;
    (*sb) << "\n";
  }
}

str
srvinfo::devlist ()
{
  strbuf sb;
  srvtab.traverse (wrap (*srvinfo::printdev, &sb, false));
  return sb;
}

str
srvinfo::nfslist ()
{
  strbuf sb;
  if (!rootnfsinfo) {
    nfs_fh3 fh;
    afs_root->mkfh3 (&fh);
    rootnfsinfo = strbuf () << hexdump (fh.data.base (), fh.data.size ())
			    << " " << sock2str (afsfd);
  }
  sb.fmt ("0x%" U64F "x . - ", root_dev) << rootnfsinfo << "\n";
  srvtab.traverse (wrap (*srvinfo::printdev, &sb, true));
  return sb;
}
