/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/* dbus-cleanup-sockets.c  dbus-cleanup-sockets utility
 *
 * Copyright (C) 2003 Red Hat, Inc.
 * Copyright (C) 2002 Michael Meeks
 *
 * Note that this file is NOT licensed under the Academic Free License,
 * as it is based on linc-cleanup-sockets which is LGPL.
 *
 * 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 of the License, 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */
#include <config.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>

#ifndef TRUE
#define TRUE (1)
#endif

#ifndef FALSE
#define FALSE (0)
#endif

#ifndef NULL
#define NULL ((void*) 0)
#endif

static void*
xmalloc (size_t bytes)
{
  void *mem;

  if (bytes == 0)
    return NULL;

  mem = malloc (bytes);

  if (mem == NULL)
    {
      fprintf (stderr, "Allocation of %d bytes failed\n",
               (int) bytes);
      exit (1);
    }

  return mem;
}

static void*
xrealloc (void *old, size_t bytes)
{
  void *mem;

  if (bytes == 0)
    {
      free (old);
      return NULL;
    }

  mem = realloc (old, bytes);

  if (mem == NULL)
    {
      fprintf (stderr, "Reallocation of %d bytes failed\n",
               (int) bytes);
      exit (1);
    }

  return mem;
}

#ifdef AF_UNIX

typedef enum
  {
    SOCKET_UNKNOWN,
    SOCKET_FAILED_TO_HANDLE,
    SOCKET_DEAD,
    SOCKET_ALIVE,
    SOCKET_UNLINKED
  } SocketStatus;

static int alive_count = 0;
static int cleaned_count = 0;
static int unhandled_count = 0;

typedef struct
{
  char *name;
  int   fd;
  SocketStatus status;
  int   n_retries;
} SocketEntry;

static SocketEntry*
socket_entry_new (const char *dir,
                  const char *fname)
{
  SocketEntry *se;
  int len;

  se = xmalloc (sizeof (SocketEntry));

  len = strlen (dir) + strlen (fname) + 2; /* 2 = nul and '/' */
  se->name = xmalloc (len);

  strcpy (se->name, dir);
  strcat (se->name, "/");
  strcat (se->name, fname);

  se->fd = -1;

  se->status = SOCKET_UNKNOWN;

  se->n_retries = 0;

  return se;
}

#if 0
static void
free_socket_entry (SocketEntry *se)
{
  free (se->name);
  if (se->fd >= 0)
    close (se->fd);
  free (se);
}
#endif

static void
read_sockets (const char    *dir,
              SocketEntry ***entries_p,
              int           *n_entries_p)
{
  DIR   *dirh;
  struct dirent *dent;
  SocketEntry **entries;
  int n_entries;
  int allocated;

  n_entries = 0;
  allocated = 2;
  entries = xmalloc (sizeof (SocketEntry*) * allocated);

  dirh = opendir (dir);
  if (dirh == NULL)
    {
      fprintf (stderr, "Failed to open directory %s: %s\n",
               dir, strerror (errno));
      exit (1);
    }

  while ((dent = readdir (dirh)))
    {
      SocketEntry *se;

      if (strncmp (dent->d_name, "dbus-", 5) != 0)
        continue;

      se = socket_entry_new (dir, dent->d_name);

      if (n_entries == allocated)
        {
          allocated *= 2;
          entries = xrealloc (entries, sizeof (SocketEntry*) * allocated);
        }

      entries[n_entries] = se;
      n_entries += 1;
    }

  closedir (dirh);

  *entries_p = entries;
  *n_entries_p = n_entries;
}

static SocketStatus
open_socket (SocketEntry *se)
{
  int ret;
  struct sockaddr_un saddr;

  if (se->n_retries > 5)
    {
      fprintf (stderr, "Warning: giving up on socket %s after several retries; unable to determine socket's status\n",
               se->name);
      return SOCKET_FAILED_TO_HANDLE;
    }

  se->n_retries += 1;

  se->fd = socket (AF_UNIX, SOCK_STREAM, 0);
  if (se->fd < 0)
    {
      fprintf (stderr, "Warning: failed to open a socket to use for connecting: %s\n",
               strerror (errno));
      return SOCKET_UNKNOWN;
    }

  if (fcntl (se->fd, F_SETFL, O_NONBLOCK) < 0)
    {
      fprintf (stderr, "Warning: failed set socket %s nonblocking: %s\n",
               se->name, strerror (errno));
      return SOCKET_UNKNOWN;
    }


  memset (&saddr, '\0', sizeof (saddr)); /* nul-terminates the sun_path */

  saddr.sun_family = AF_UNIX;
  strncpy (saddr.sun_path, se->name, sizeof (saddr.sun_path) - 1);

  do
    {
      ret = connect (se->fd, (struct sockaddr*) &saddr, sizeof (saddr));
    }
  while (ret < 0 && errno == EINTR);

  if (ret >= 0)
    return SOCKET_ALIVE;
  else
    {
      switch (errno)
        {
        case EINPROGRESS:
        case EAGAIN:
          return SOCKET_UNKNOWN;
        case ECONNREFUSED:
          return SOCKET_DEAD;
        default:
          fprintf (stderr, "Warning: unexpected error connecting to socket %s: %s\n",
                   se->name, strerror (errno));
          return SOCKET_FAILED_TO_HANDLE;
        }
    }
}

static int
handle_sockets (SocketEntry **entries,
                int           n_entries)
{
  int i;
  int n_unknown;

  n_unknown = 0;

  i = 0;
  while (i < n_entries)
    {
      SocketEntry *se;
      SocketStatus status;

      se = entries[i];
      ++i;

      if (se->fd >= 0)
        {
          fprintf (stderr, "Internal error, socket has fd  kept open while status = %d\n",
                   se->status);
          exit (1);
        }

      if (se->status != SOCKET_UNKNOWN)
        continue;

      status = open_socket (se);

      switch (status)
        {
        case SOCKET_DEAD:
          cleaned_count += 1;
          if (unlink (se->name) < 0)
            {
              fprintf (stderr, "Warning: Failed to delete %s: %s\n",
                       se->name, strerror (errno));

              se->status = SOCKET_FAILED_TO_HANDLE;
            }
          else
            se->status = SOCKET_UNLINKED;
          break;

        case SOCKET_ALIVE:
          alive_count += 1;
          /* FALL THRU */

        case SOCKET_FAILED_TO_HANDLE:
        case SOCKET_UNKNOWN:
          se->status = status;
          break;

        case SOCKET_UNLINKED:
          fprintf (stderr, "Bad status from open_socket(), should not happen\n");
          exit (1);
          break;
        }

      if (se->fd >= 0)
        {
          close (se->fd);
          se->fd = -1;
        }

      if (se->status == SOCKET_UNKNOWN)
        n_unknown += 1;
    }

  return n_unknown == 0;
}

static void
clean_dir (const char *dir)
{
  SocketEntry **entries;
  int n_entries;

  read_sockets (dir, &entries, &n_entries);

  /* open_socket() will fail conclusively after
   * several retries, so this loop is guaranteed
   * to terminate eventually
   */
  while (!handle_sockets (entries, n_entries))
    {
      fprintf (stderr, "Unable to determine state of some sockets, retrying in 2 seconds\n");
      sleep (2);
    }

  unhandled_count += (n_entries - alive_count - cleaned_count);
}

#endif /* AF_UNIX */

static void
usage (int ecode)
{
  fprintf (stderr, "dbus-cleanup-sockets [--version] [--help] <socketdir>\n");
  exit (ecode);
}

static void
version (void)
{
  printf ("D-Bus Socket Cleanup Utility %s\n"
          "Copyright (C) 2003 Red Hat, Inc.\n"
          "Copyright (C) 2002 Michael Meeks\n"
          "This is free software; see the source for copying conditions.\n"
          "There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n",
          VERSION);
  exit (0);
}

int
main (int argc, char **argv)
{
  int i;
  int saw_doubledash;
  const char *dirname;

  saw_doubledash = FALSE;
  dirname = NULL;
  i = 1;
  while (i < argc)
    {
      const char *arg = argv[i];

      if (strcmp (arg, "--help") == 0 ||
          strcmp (arg, "-h") == 0 ||
          strcmp (arg, "-?") == 0)
        usage (0);
      else if (strcmp (arg, "--version") == 0)
        version ();
      else if (!saw_doubledash)
	{
          if (strcmp (arg, "--") == 0)
            saw_doubledash = TRUE;
          else if (*arg == '-')
            usage (1);
	}
      else
        {
          if (dirname != NULL)
            {
              fprintf (stderr, "dbus-cleanup-sockets only supports a single directory name\n");
              exit (1);
            }

          dirname = arg;
        }

      ++i;
    }

  /* Default to session socket dir, usually /tmp */
  if (dirname == NULL)
    dirname = DBUS_SESSION_SOCKET_DIR;

#ifdef AF_UNIX
  clean_dir (dirname);

  printf ("Cleaned up %d sockets in %s; %d sockets are still in use; %d in unknown state\n",
          cleaned_count, dirname, alive_count, unhandled_count);
#else
  printf ("This system does not support UNIX domain sockets, so dbus-cleanup-sockets does nothing\n");
#endif

  return 0;
}
