/**
 * gam_api.c: implementation of the library side of the gamin FAM implementation
 */

#include "config.h"
#include <pwd.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/uio.h>
#include <string.h>
#include "fam.h"
#include "gam_protocol.h"
#include "gam_data.h"
#include "gam_fork.h"
#include "gam_error.h"

#define TEST_DEBUG

#define MAX_RETRIES 25

#ifdef GAMIN_DEBUG_API
int debug_reqno = -1;
void *debug_userData = NULL;
#endif
int FAMErrno = 0;

static enum {
    FAM_OK = 0,
    FAM_ARG,	/* Bad arguments */
    FAM_FILE,	/* Bad filename */
    FAM_CONNECT,/* Connection failure */
    FAM_AUTH,	/* Authentication failure */
    FAM_MEM,	/* Memory allocation */
    FAM_UNIMPLEM/* Unimplemented */
} FAMError;

const char *FamErrlist[] = {
    "Okay",
    "Bad arguments",
    "Bad filename",
    "Connection failure",
    "Authentication failure",
    "Memory allocation failure",
    "Unimplemented function",
    NULL
};

#ifdef GAMIN_DEBUG_API
int FAMDebug(FAMConnection *fc, const char *filename, FAMRequest * fr,
             void *userData);
#endif

#ifdef TEST_DEBUG
static char *
gamin_dump_event(FAMEvent *event) {
    static char res[200];
    const char *type;

    if (event == NULL)
        return("NULL event !");
    switch (event->code) {
        case FAMChanged: type = "Changed"; break;
        case FAMDeleted: type = "Deleted"; break;
        case FAMStartExecuting: type = "StartExecuting"; break;
        case FAMStopExecuting: type = "StopExecuting"; break;
        case FAMCreated: type = "Created"; break;
        case FAMMoved: type = "Moved"; break;
        case FAMAcknowledge: type = "Acknowledge"; break;
        case FAMExists: type = "Exists"; break;
        case FAMEndExist: type = "EndExist"; break;
	default: type = "Unknown"; break;
    }
    snprintf(res, 199, "%s : %s", type, &event->filename[0]);
    return(res);
}
#endif

void gam_show_debug(void);

void
gam_show_debug(void) {
}

void gam_got_signal(void);

void
gam_got_signal(void) {
}


/************************************************************************
 *									*
 *			Path for the socket connection			*
 *									*
 ************************************************************************/

static char user_name[100] = "";

/**
 * gamin_get_user_name:
 *
 * Get the user name for the current process.
 *
 * Returns a new string or NULL in case of error.
 */
static const char *
gamin_get_user_name(void)
{
    struct passwd *pw;

    if (user_name[0] != 0)
        return (user_name);

    pw = getpwuid(getuid());

    if (pw != NULL) {
	strncpy(user_name, pw->pw_name, 99);
	user_name[99] = 0;
    }

    return(user_name);
}

/**
 * gamin_get_socket_path:
 *
 * Get the file path to the socket to connect the FAM server.
 * The fam server interface is available though a socket whose
 * id is available though an environment variable GAM_CLIENT_ID
 *
 * Returns a new string or NULL in case of error.
 */
static char *
gamin_get_socket_path(void)
{
    const char *fam_client_id;
    const char *user;
    char *ret;
    char path[MAXPATHLEN + 1];

    fam_client_id = getenv("GAM_CLIENT_ID");
    if (fam_client_id == NULL) {
        GAM_DEBUG(DEBUG_INFO, "Error getting GAM_CLIENT_ID\n");
        fam_client_id = "";
    }
    user = gamin_get_user_name();

    if (user == NULL) {
        gam_error(DEBUG_INFO, "Error getting user informations");
        return (NULL);
    }
#ifdef HAVE_ABSTRACT_SOCKETS
    snprintf(path, MAXPATHLEN, "/tmp/fam-%s-%s", user, fam_client_id);
#else
    snprintf(path, MAXPATHLEN, "/tmp/fam-%s/fam-%s", user, fam_client_id);
#endif
    path[MAXPATHLEN] = 0;
    ret = strdup(path);
    return (ret);
}

#ifndef HAVE_ABSTRACT_SOCKETS
/**
 * gamin_get_socket_dir:
 *
 * Get the directory path to the socket to connect the FAM server.
 *
 * Returns a new string or NULL in case of error.
 */
static char *
gamin_get_socket_dir(void)
{
    const char *user;
    char *ret;
    char path[MAXPATHLEN + 1];

    user = gamin_get_user_name();

    if (user == NULL) {
        gam_error(DEBUG_INFO, "Error getting user informations");
        return (NULL);
    }
    snprintf(path, MAXPATHLEN, "/tmp/fam-%s", user);
    path[MAXPATHLEN] = 0;
    ret = strdup(path);
    return (ret);
}



/************************************************************************
 *									*
 *		Security for OSes without abstract sockets		*
 *									*
 ************************************************************************/
/**
 * gamin_check_secure_dir:
 *
 * Tries to ensure that the directory used to hold the socket used
 * for communicating with is a safe directory to avoid possible attacks.
 *
 * Returns 1 if safe, 0 if missing, -1 if not safe
 */
static int
gamin_check_secure_dir(void)
{
    char *dir;
    struct stat st;
    int ret;

    dir = gamin_get_socket_dir();
    if (dir == NULL) {
	gam_error(DEBUG_INFO, "Failed to get path to socket directory\n");
        return(0);
    }
    ret = stat(dir, &st);
    if (ret < 0) {
	free(dir);
	return(0);
    }
    if (st.st_uid != getuid()) {
	gam_error(DEBUG_INFO,
		  "Socket directory %s has different owner\n",
		  dir);
	goto unsafe;
    }
    if (!S_ISDIR (st.st_mode)) {
	gam_error(DEBUG_INFO, "Socket path %s is not a directory\n",
		  dir);
	goto unsafe;
    }
    if (st.st_mode & (S_IRWXG|S_IRWXO)) {
	gam_error(DEBUG_INFO,
		  "Socket directory %s has wrong permissions\n",
		  dir);
	goto unsafe;
    }
    if (((st.st_mode & (S_IRWXU)) != S_IRWXU)) {
	gam_error(DEBUG_INFO,
		  "Socket directory %s has wrong permissions\n",
		  dir);
	goto unsafe;
    }

    /*
     * all checks on existing dir seems okay
     */
    GAM_DEBUG(DEBUG_INFO, "Reusing socket directory %s\n", dir);
    free(dir);
    return(1);

unsafe:
    /*
     * The path to the directory is considered unsafe
     * try to remove the given path to rebuild the directory.
     */
    ret = rmdir(dir);
    if (ret < 0) {
	ret = unlink(dir);
	if (ret < 0) {
	    gam_error(DEBUG_INFO, "Failed to remove unsafe path %s\n",
	              dir);
	    free(dir);
	    return(-1);
	}
    }
    GAM_DEBUG(DEBUG_INFO, "Removed %s\n", dir);
    free(dir);
    return(0);
}

/**
 * gamin_check_secure_path:
 * @path: path to the (possibly abstract) socket
 *
 * Tries to create or ensure that the socket used for communicating with
 * the clients are in a safe directory to avoid possible attacks.
 *
 * Returns 1 if safe, 0 if missing, -1 if not safe
 */
static int
gamin_check_secure_path(const char *path)
{
    struct stat st;
    int ret;

    ret = gamin_check_secure_dir();
    if (ret <= 0)
        return(ret);

    /*
     * Check the existing socket if any
     */
    ret = stat(path, &st);
    if (ret < 0)
	return(0);
    
    if (st.st_uid != getuid()) {
	gam_error(DEBUG_INFO,
		  "Socket %s has different owner\n",
		  path);
	goto cleanup;
    }
#ifdef S_ISSOCK
    if (!S_ISSOCK (st.st_mode)) {
	gam_error(DEBUG_INFO, "Socket path %s is not a socket\n",
		  path);
	goto cleanup;
    }
#endif
    if (st.st_mode & (S_IRWXG|S_IRWXO)) {
	gam_error(DEBUG_INFO,
		  "Socket %s has wrong permissions\n",
		  path);
	goto cleanup;
    }
    /*
     * Looks good though binding may fail due to an existing server
     */
    return(1);

cleanup:
    /*
     * the existing file at the socket location seems strange, try to remove it
     */
    ret = unlink(path);
    if (ret < 0) {
	gam_error(DEBUG_INFO, "Failed to remove %s\n", path);
	return(-1);
    }
    return(0);
}
#endif /* ! HAVE_ABSTRACT_SOCKETS */

/************************************************************************
 *									*
 *			Connection socket shutdown			*
 *									*
 ************************************************************************/

/**
 * gamin_connect_unix_socket:
 * @path: path to the (possibly abstract) socket

 * Returns the socket file descriptor or -1 in case of error.
 */
static int
gamin_connect_unix_socket(const char *path)
{
    int fd;
    struct sockaddr_un addr;
    int retries = 0;

  retry_start:
    fd = socket(PF_UNIX, SOCK_STREAM, 0);
    if (fd < 0) {
        gam_error(DEBUG_INFO, "Failed to create unix socket\n");
        return (-1);
    }
    memset(&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
#ifdef HAVE_ABSTRACT_SOCKETS
    addr.sun_path[0] = '\0';
    strncpy(&addr.sun_path[1], path, (sizeof(addr) - 4) - 2);
#else
    /*
     * if the socket is exposed at the filesystem level we need to take
     * some extra protection checks. Also make sure the socket is created
     * with restricted mode
     */
    if (gamin_check_secure_path(path) < 0) {
	return (-1);
    }
    strncpy(&addr.sun_path[0], path, (sizeof(addr) - 4) - 1);
#endif

    if (connect(fd, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
        if (retries == 0) {
            const char *fam_client_id = getenv("GAM_CLIENT_ID");

            if (fam_client_id == NULL)
                fam_client_id = "";
            /*
             * need to close it here to avoid inheriting it
             * otherwise autoshudown won't fail since the server
             * itself is still connected to the socket.
             */
            close(fd);
            gamin_fork_server(fam_client_id);
            retries++;
            goto retry_start;
        }
        if (retries < MAX_RETRIES) {
            close(fd);
            usleep(50000);
            retries++;
            goto retry_start;
        }

        gam_error(DEBUG_INFO, "Failed to connect to socket %s\n", path);
        close(fd);
        return (-1);
    }
    GAM_DEBUG(DEBUG_INFO, "Connected to socket %s : %d\n", path, fd);

    return (fd);
}

/**
 * gamin_write_credential_byte:
 * @fd: the file descriptor for the socket
 *
 * The authentication on the server receiving side need to receive some
 * data from the client to be able to assert the client credential. So
 * this routine simply output a 0 byte to allow the kernel to pass that
 * information.
 *
 * Returns -1 in case of error, 0 otherwise
 */
static int
gamin_write_credential_byte(int fd)
{
    char data[2] = { 0, 0 };
    int written;
#if defined(HAVE_CMSGCRED) && (!defined(LOCAL_CREDS) || defined(__FreeBSD__))
    union {
	    struct cmsghdr hdr;
	    char cred[CMSG_SPACE (sizeof (struct cmsgcred))];
    } cmsg;
    struct iovec iov;
    struct msghdr msg;

    iov.iov_base = &data[0];
    iov.iov_len = 1;

    memset (&msg, 0, sizeof (msg));
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

    msg.msg_control = (caddr_t) &cmsg;
    msg.msg_controllen = CMSG_SPACE (sizeof (struct cmsgcred));
    memset (&cmsg, 0, sizeof (cmsg));
    cmsg.hdr.cmsg_len = CMSG_LEN (sizeof (struct cmsgcred));
    cmsg.hdr.cmsg_level = SOL_SOCKET;
    cmsg.hdr.cmsg_type = SCM_CREDS;
#endif

retry:
#if defined(HAVE_CMSGCRED) && (!defined(LOCAL_CREDS) || defined(__FreeBSD__))
    written = sendmsg(fd, &msg, 0);
#else
    written = write(fd, &data[0], 1);
#endif
    if (written < 0) {
        if (errno == EINTR)
            goto retry;
        gam_error(DEBUG_INFO,
                  "Failed to write credential bytes to socket %d\n", fd);
        return (-1);
    }
    if (written != 1) {
        gam_error(DEBUG_INFO, "Wrote %d credential bytes to socket %d\n",
                  written, fd);
        return (-1);
    }
    GAM_DEBUG(DEBUG_INFO, "Wrote credential bytes to socket %d\n", fd);
    return (0);
}

/**
 * gamin_data_available:
 * @fd: the file descriptor for the socket
 *
 * Check if there is some incoming data to be read from the file descriptor
 *
 * Returns -1 in case of error, 0 if no data, 1 if data can be read
 */
static int
gamin_data_available(int fd)
{
    fd_set read_set;
    struct timeval tv;
    int avail;

    if (fd < 0) {
        GAM_DEBUG(DEBUG_INFO, "gamin_data_available wrong fd %d\n", fd);
        return (-1);
    }
    GAM_DEBUG(DEBUG_INFO, "Checking data available on %d\n", fd);

retry:
    FD_ZERO(&read_set);
    FD_SET(fd, &read_set);
    tv.tv_sec = 0;
    tv.tv_usec = 0;

    avail = select(fd + 1, &read_set, NULL, NULL, &tv);
    if (avail < 0) {
        if (errno == EINTR)
            goto retry;
        gam_error(DEBUG_INFO,
                  "Failed to check data availability on socket %d\n", fd);
        return (-1);
    }
    if (avail == 0)
        return (0);
    return (1);
}

/**
 * gamin_write_byte:
 * @fd: the file descriptor for the socket
 * @data: pointer to the data
 * @len: length of the data in bytes
 *
 * Write some data to the server socket.
 *
 * Returns -1 in case of error, 0 otherwise
 */
static int
gamin_write_byte(int fd, const char *data, size_t len)
{
    int written;
    int remaining;

    remaining = len;
    do {
	written = write(fd, data, remaining);
	if (written < 0) {
	    if (errno == EINTR)
		continue;

	    GAM_DEBUG(DEBUG_INFO,
		      "%s: Failed to write bytes to socket %d: %s\n",
		      __FUNCTION__, fd, strerror (errno));
	    return -1;
	}

	data += written;
	remaining -= written;
    } while (remaining > 0);

    GAM_DEBUG(DEBUG_INFO, "Wrote %d bytes to socket %d\n", written, fd);
    return (0);
}

/**
 * gamin_send_request:
 * @type: the GAMReqType for the request
 * @fd: the file descriptor for the socket
 * @filename,: the filename for the file or directory
 * @fr: the fam request
 * @userData: user data associated to this request
 * @has_reqnum: indicate if fr already has a request number
 */
static int
gamin_send_request(GAMReqType type, int fd, const char *filename,
                   FAMRequest * fr, void *userData, GAMDataPtr data,
		   int has_reqnum)
{
    int reqnum;
    size_t len, tlen;
    GAMPacket req;
    int ret;

#ifdef GAMIN_DEBUG_API
    if (type == GAM_REQ_DEBUG) {
        len = strlen(filename);
        if (len > MAXPATHLEN) {
	    FAMErrno = FAM_FILE;
            return (-1);
	}
        reqnum = gamin_data_get_reqnum(data, filename, (int) type, userData);
        if (reqnum < 0) {
	    FAMErrno = FAM_ARG;
            return (-1);
	}
        reqnum = fr->reqnum;
    } else
#endif
    if (filename == NULL) {
        len = 0;
        reqnum = fr->reqnum;
    } else if (has_reqnum == 0) {
        len = strlen(filename);
        if (len > MAXPATHLEN) {
	    FAMErrno = FAM_FILE;
            return (-1);
	}
        reqnum = gamin_data_get_reqnum(data, filename, (int) type, userData);
        if (reqnum < 0) {
	    FAMErrno = FAM_ARG;
            return (-1);
	}
	fr->reqnum = reqnum;
    } else {
        len = strlen(filename);
        if (len > MAXPATHLEN) {
	    FAMErrno = FAM_FILE;
            return (-1);
	}
        reqnum = gamin_data_get_request(data, filename, (int) type, userData,
	                                fr->reqnum);
        if (reqnum < 0) {
	    FAMErrno = FAM_MEM;
            return (-1);
	}
    }
    tlen = GAM_PACKET_HEADER_LEN + len;
    /* We use only local socket so no need for network byte order conversion */
    req.len = (unsigned short) tlen;
    req.version = GAM_PROTO_VERSION;
    req.seq = reqnum;
    req.type = (unsigned short) type;
    if ((type == GAM_REQ_DIR) && (gamin_data_get_exists(data) == 0)) {
        req.type |= GAM_OPT_NOEXISTS;
    }
        
    req.pathlen = len;
    if (len > 0)
        memcpy(&req.path[0], filename, len);
    ret = gamin_write_byte(fd, (const char *) &req, tlen);

    GAM_DEBUG(DEBUG_INFO, "gamin_send_request %d for socket %d\n", reqnum,
              fd);
    if (ret < 0) {
        FAMErrno = FAM_CONNECT;
    }
    return (ret);
}

/**
 * gamin_check_cred:
 *
 * The first read on the connection gathers credentials from the server
 * and checks them. Parts directly borrowed from DBus code.
 *
 * Returns 0 in case of success and -1 in case of error.
 */
static int
gamin_check_cred(GAMDataPtr conn, int fd)
{
    struct msghdr msg;
    struct iovec iov;
    char buf;
    pid_t c_pid;
    uid_t c_uid, s_uid;
    gid_t c_gid;

#ifdef HAVE_CMSGCRED
    struct cmsgcred *cred;
    union {
	    struct cmsghdr hdr;
	    char cred[CMSG_SPACE (sizeof (struct cmsgcred))];
    } cmsg;
#endif

    s_uid = getuid();

#if defined(LOCAL_CREDS) && defined(HAVE_CMSGCRED) && !defined(__FreeBSD__)
    /* Set the socket to receive credentials on the next message */
    {
        int on = 1;

        if (setsockopt(fd, 0, LOCAL_CREDS, &on, sizeof(on)) < 0) {
            gam_error(DEBUG_INFO, "Unable to set LOCAL_CREDS socket option\n");
            return(-1);
        }
    }
#endif

    iov.iov_base = &buf;
    iov.iov_len = 1;

    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

#ifdef HAVE_CMSGCRED
    memset(&cmsg, 0, sizeof(cmsg));
    msg.msg_control = (caddr_t) &cmsg;
    msg.msg_controllen = CMSG_SPACE (sizeof (struct cmsgcred));
#endif

retry:
    if (recvmsg(fd, &msg, 0) < 0) {
        if (errno == EINTR)
            goto retry;

        GAM_DEBUG(DEBUG_INFO, "Failed to read credentials byte on %d\n", fd);
        goto failed;
    }

    if (buf != '\0') {
        GAM_DEBUG(DEBUG_INFO, "Credentials byte was not nul on %d\n", fd);
        goto failed;
    }
#ifdef HAVE_CMSGCRED
    if (cmsg.hdr.cmsg_len < CMSG_LEN (sizeof (struct cmsgcred)) || cmsg.hdr.cmsg_type != SCM_CREDS) {
        GAM_DEBUG(DEBUG_INFO,
                  "Message from recvmsg() was not SCM_CREDS\n");
        goto failed;
    }
#endif

    GAM_DEBUG(DEBUG_INFO, "read credentials byte\n");

    {
#ifdef SO_PEERCRED
        struct ucred cr;
        socklen_t cr_len = sizeof(cr);

        if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cr, &cr_len) ==
            0 && cr_len == sizeof(cr)) {
            c_pid = cr.pid;
            c_uid = cr.uid;
            c_gid = cr.gid;
        } else {
            GAM_DEBUG(DEBUG_INFO,
                      "Failed to getsockopt() credentials on %d, returned len %d/%d\n",
                      fd, cr_len, (int) sizeof(cr));
            goto failed;
        }
#elif defined(HAVE_CMSGCRED)
        cred = (struct cmsgcred *) CMSG_DATA (&cmsg);
        c_pid = cred->cmcred_pid;
        c_uid = cred->cmcred_euid;
        c_gid = cred->cmcred_groups[0];
#else /* !SO_PEERCRED && !HAVE_CMSGCRED */
        GAM_DEBUG(DEBUG_INFO,
                  "Socket credentials not supported on this OS\n");
        goto failed;
#endif
    }

    if (s_uid != c_uid) {
        GAM_DEBUG(DEBUG_INFO,
                  "Credentials check failed: s_uid %d, c_uid %d\n",
                  (int) s_uid, (int) c_uid);
        goto failed;
    }
    GAM_DEBUG(DEBUG_INFO,
              "Credentials: s_uid %d, c_uid %d, c_gid %d, c_pid %d\n",
              (int) s_uid, (int) c_uid, (int) c_gid, (int) c_pid);
    gamin_data_done_auth(conn);

    return(0);

failed:
    return (-1);
}

/**
 * gamin_read_data:
 * @conn: the connection
 * @fd: the file descriptor for the socket
 * @block: allow blocking
 *
 * Read the available data on the file descriptor. This is a potentially
 * blocking operation.
 *
 * Return 0 in case of success, -1 in case of error.
 */
static int
gamin_read_data(GAMDataPtr conn, int fd, int block)
{
    int ret;
    char *data;
    int size;

    ret = gamin_data_need_auth(conn);
    if (ret == 1) {
        GAM_DEBUG(DEBUG_INFO, "Client need auth %d\n", fd);
        if (gamin_check_cred(conn, fd) < 0) {
	    return (-1);
	}
	if (!block) {
	    ret = gamin_data_available(fd);
	    if (ret < 0)
	        return(-1);
	    if (ret == 0)
	        return(0);
	}
    } else if (ret != 0) {
        goto error;
    }
    ret = gamin_data_get_data(conn, &data, &size);
    if (ret < 0) {
        GAM_DEBUG(DEBUG_INFO, "Failed getting connection data\n");
        goto error;
    }
retry:
    ret = read(fd, (char *) data, size);
    if (ret < 0) {
        if (errno == EINTR) {
	    GAM_DEBUG(DEBUG_INFO, "client read() call interrupted\n");
            goto retry;
	}
        gam_error(DEBUG_INFO, "failed to read() from server connection\n");
        goto error;
    }
    if (ret == 0) {
        gam_error(DEBUG_INFO, "end from FAM server connection\n");
        goto error;
    }
    GAM_DEBUG(DEBUG_INFO, "read %d bytes from server\n", ret);

    if (gamin_data_conn_data(conn, ret) < 0) {
        gam_error(DEBUG_INFO, "Failed to process %d bytes from server\n",
                  ret);
        goto error;
    }
    return (0);
error:
    return(-1);
}

/**
 * gamin_resend_request:
 * @fd: the file descriptor for the socket
 * @type: the GAMReqType for the request
 * @filename,: the filename for the file or directory
 * @reqnum: the request number.
 *
 * Reemit a request, used on a reconnection.
 *
 * Returns 0 in case of success and -1 in case of error
 */
static int
gamin_resend_request(int fd, GAMReqType type, const char *filename,
                     int reqnum)
{
    size_t len, tlen;
    GAMPacket req;
    int ret;

    if ((filename == NULL) || (fd < 0))
        return(-1);

    len = strlen(filename);
    tlen = GAM_PACKET_HEADER_LEN + len;
    /* We use only local socket so no need for network byte order conversion */
    req.len = (unsigned short) tlen;
    req.version = GAM_PROTO_VERSION;
    req.seq = reqnum;
    /* GAM_OPT_NOEXISTS to avoid filling up the connection with
       events we don't need and discard */
    req.type = (unsigned short) (type | GAM_OPT_NOEXISTS);
    /* req.type = (unsigned short) type; */
    req.pathlen = len;
    if (len > 0)
        memcpy(&req.path[0], filename, len);
    ret = gamin_write_byte(fd, (const char *) &req, tlen);

    GAM_DEBUG(DEBUG_INFO, "gamin_resend_request %d for socket %d\n", reqnum,
              fd);
    return (ret);
}

/**
 * gamin_try_reconnect:
 * @conn: the connection
 * @fd: the file descriptor for the socket
 *
 * The last read or write resulted in a failure, connection to the server
 * has been broken, close the socket and try to reconnect and register
 * the monitors again. Reusing the same fd is needed since applications
 * are unlikely to recheck it.
 *
 * Return 0 in case of success, -1 in case of error.
 */
static int
gamin_try_reconnect(GAMDataPtr conn, int fd)
{
    int newfd, i, ret, nb_req;
    GAMReqDataPtr *reqs;
    char *socket_name;


    if ((conn == NULL) || (fd < 0))
        return(-1);
    GAM_DEBUG(DEBUG_INFO, "Trying to reconnect to server on %d\n", fd);

    /*
     * the connection is no more in an usable state
     */
    /*conn->fd = -1; */

    socket_name = gamin_get_socket_path();
    if (socket_name == NULL)
        return (-1);

    /*
     * try to reopen a connection to the server
     */
    newfd = gamin_connect_unix_socket(socket_name);
    free(socket_name);
    if (newfd < 0) {
        return (-1);
    }

    /*
     * seems we managed to rebuild a connection to the server.
     * start the authentication again and resubscribe all existing
     * monitoring commands.
     */
    ret = gamin_write_credential_byte(newfd);
    if (ret != 0) {
        close(newfd);
        return (-1);
    }

    /*
     * reuse the same descriptor. We never close the original fd, dup2
     * atomically overwrites it and closes the original. This way we
     * never leave the original fd closed, since that can cause trouble
     * if the app keeps the fd around.
     */
    ret = dup2(newfd, fd);
    close(newfd);
    if (ret < 0) {
	gam_error(DEBUG_INFO,
		  "Failed to reuse descriptor %d on reconnect\n",
		  fd);
	return (-1);
    }
    
    nb_req = gamin_data_reset(conn, &reqs);
    if (reqs != NULL) {
	for (i = 0; i < nb_req;i++) {
	    gamin_resend_request(fd, reqs[i]->type, reqs[i]->filename,
	                         reqs[i]->reqno);
	}
    }
    return(0);
}

/************************************************************************
 *									*
 *			Public interfaces				*
 *									*
 ************************************************************************/

/**
 * FAMOpen:
 * @fc:  pointer to an uninitialized connection structure
 *
 * This function tries to open a connection to the FAM server.
 *
 * Returns -1 in case of error, 0 otherwise
 */
int
FAMOpen(FAMConnection * fc)
{
    char *socket_name;
    int fd, ret;

    gam_error_init();

    GAM_DEBUG(DEBUG_INFO, "FAMOpen()\n");

    if (fc == NULL) {
        FAMErrno = FAM_ARG;
        return (-1);
    }

    socket_name = gamin_get_socket_path();
    if (socket_name == NULL) {
        FAMErrno = FAM_CONNECT;
        return (-1);
    }

    fd = gamin_connect_unix_socket(socket_name);

    free(socket_name);
    if (fd < 0) {
        FAMErrno = FAM_CONNECT;
        return (-1);
    }
    ret = gamin_write_credential_byte(fd);
    if (ret != 0) {
        FAMErrno = FAM_CONNECT;
        close(fd);
        return (-1);
    }
    fc->fd = fd;
    fc->client = (void *) gamin_data_new();
    if (fc->client == NULL) {
        FAMErrno = FAM_MEM;
        close(fd);
        return (-1);
    }
    return (0);
}

/**
 * FAMOpen2:
 * @fc:  pointer to an uninitialized connection structure
 * @appName:  the application name
 *
 * This function tries to open a connection to the FAM server.
 * The fam server interface is available though a socket whose
 * id is available though an environment variable GAM_CLIENT_ID
 *
 * Returns -1 in case of error, 0 otherwise
 */
int
FAMOpen2(FAMConnection * fc, const char *appName)
{
    int ret;

    gam_error_init();

    GAM_DEBUG(DEBUG_INFO, "FAMOpen2()\n");

    ret = FAMOpen(fc);
    /*
    if (ret == 0)
        fc->client = (void *) appName;
     */
    return (ret);
}

/**
 * FAMClose:
 * @fc:  pointer to a connection structure.
 *
 * This function closes the connection to the FAM server.
 *
 * Returns -1 in case of error, 0 otherwise
 */
int
FAMClose(FAMConnection * fc)
{
    int ret;

    if (fc == NULL) {
        FAMErrno = FAM_ARG;
	GAM_DEBUG(DEBUG_INFO, "FAMClose() arg error\n");
        return (-1);
    }

    GAM_DEBUG(DEBUG_INFO, "FAMClose()\n");

    gamin_data_lock(fc->client);

    ret = close(fc->fd);
    fc->fd = -1;
    gamin_data_free(fc->client);
    return (ret);
}

/**
 * FAMMonitorDirectory:
 * @fc: pointer to a connection structure.
 * @filename: the directory filename, it must not be relative.
 * @fr: pointer to the request structure.
 * @userData: user data associated to this request
 *
 * Register a monitoring request for a given directory.
 *
 * Returns 0 in case of success and -1 in case of error.
 */
int
FAMMonitorDirectory(FAMConnection * fc, const char *filename,
                    FAMRequest * fr, void *userData)
{
    int retval;
     
    if ((fc == NULL) || (filename == NULL) || (fr == NULL)) {
	GAM_DEBUG(DEBUG_INFO, "FAMMonitorDirectory() arg error\n");
        FAMErrno = FAM_ARG;
        return (-1);
    }

    GAM_DEBUG(DEBUG_INFO, "FAMMonitorDirectory(%s)\n", filename);

    if ((filename[0] != '/') || (strlen(filename) >= MAXPATHLEN)) {
        FAMErrno = FAM_FILE;
        return (-1);
    }
    if ((fc->fd < 0) || (fc->client == NULL)) {
        FAMErrno = FAM_ARG;
        return (-1);
    }
    
    gamin_data_lock(fc->client);
    retval = (gamin_send_request(GAM_REQ_DIR, fc->fd, filename,
                               fr, userData, fc->client, 0));
    gamin_data_unlock(fc->client);

    return retval;
}

/**
 * FAMMonitorDirectory2:
 * @fc: pointer to a connection structure.
 * @filename: the directory filename, it must not be relative.
 * @fr: pointer to the request structure.
 *
 * Register a monitoring request for a given directory.
 *
 * Returns 0 in case of success and -1 in case of error.
 */
int
FAMMonitorDirectory2(FAMConnection * fc, const char *filename,
                     FAMRequest * fr)
{
    int retval;

    if ((fc == NULL) || (filename == NULL) || (fr == NULL)) {
	GAM_DEBUG(DEBUG_INFO, "FAMMonitorDirectory2() arg error\n");
        FAMErrno = FAM_ARG;
        return (-1);
    }

    GAM_DEBUG(DEBUG_INFO, "FAMMonitorDirectory2(%s, %d)\n",
              filename, fr->reqnum);

    if ((filename[0] != '/') || (strlen(filename) >= MAXPATHLEN)) {
        FAMErrno = FAM_FILE;
        return (-1);
    }
    if ((fc->fd < 0) || (fc->client == NULL)) {
        FAMErrno = FAM_ARG;
        return (-1);
    }

    gamin_data_lock(fc->client);
    retval = (gamin_send_request(GAM_REQ_DIR, fc->fd, filename,
                               fr, NULL, fc->client, 1));
    gamin_data_unlock(fc->client);

    return retval;
}

/**
 * FAMMonitorFile:
 * @fc: pointer to a connection structure.
 * @filename: the file filename, it must not be relative.
 * @fr: pointer to the request structure.
 * @userData: user data associated to this request
 *
 * Register a monitoring request for a given file.
 *
 * Returns 0 in case of success and -1 in case of error.
 */
int
FAMMonitorFile(FAMConnection * fc, const char *filename,
               FAMRequest * fr, void *userData)
{
    int retval;

    if ((fc == NULL) || (filename == NULL) || (fr == NULL)) {
	GAM_DEBUG(DEBUG_INFO, "FAMMonitorFile() arg error\n");
        FAMErrno = FAM_ARG;
        return (-1);
    }

    GAM_DEBUG(DEBUG_INFO, "FAMMonitorFile(%s)\n", filename);

    if ((filename[0] != '/') || (strlen(filename) >= MAXPATHLEN)) {
        FAMErrno = FAM_FILE;
        return (-1);
    }
    if ((fc->fd < 0) || (fc->client == NULL)) {
        FAMErrno = FAM_ARG;
        return (-1);
    }

    gamin_data_lock(fc->client);
    retval =  (gamin_send_request(GAM_REQ_FILE, fc->fd, filename,
                               fr, userData, fc->client, 0));
    gamin_data_unlock(fc->client);

    return retval;
}

/**
 * FAMMonitorFile2:
 * @fc: pointer to a connection structure.
 * @filename: the file filename, it must not be relative.
 * @fr: pointer to the request structure.
 *
 * Register a monitoring request for a given file.
 *
 * Returns 0 in case of success and -1 in case of error.
 */
int
FAMMonitorFile2(FAMConnection * fc, const char *filename, FAMRequest * fr)
{
    int retval;

    if ((fc == NULL) || (filename == NULL) || (fr == NULL)) {
	GAM_DEBUG(DEBUG_INFO, "FAMMonitorFile2() arg error\n");
        FAMErrno = FAM_ARG;
        return (-1);
    }

    GAM_DEBUG(DEBUG_INFO, "FAMMonitorFile2(%s, %d)\n", filename, fr->reqnum);

    if ((filename[0] != '/') || (strlen(filename) >= MAXPATHLEN)) {
        FAMErrno = FAM_FILE;
        return (-1);
    }
    if ((fc->fd < 0) || (fc->client == NULL)) {
        FAMErrno = FAM_ARG;
        return (-1);
    }

    gamin_data_lock(fc->client);
    retval = (gamin_send_request(GAM_REQ_FILE, fc->fd, filename,
                               fr, NULL, fc->client, 1));
    gamin_data_unlock(fc->client);

    return retval;
}

/**
 * FAMMonitorCollection:
 * @fc: pointer to a connection structure.
 * @filename: the file filename, it must not be relative.
 * @fr: pointer to the request structure.
 * @userData: user data associated to this request
 * @depth:  supposedly a limit in the recursion depth
 * @mask:  unknown !
 *
 * Register a extended monitoring request for a given directory.
 * NOT SUPPORTED
 *
 * Returns -1
 */
int
FAMMonitorCollection(FAMConnection * fc, const char *filename,
                     FAMRequest * fr, void *userData, int depth,
                     const char *mask)
{
    if (filename == NULL)
        filename = "NULL";
    if (mask == NULL)
        mask = "NULL";
    gam_error(DEBUG_INFO,
              "Unsupported call filename %s, depth %d, mask %s\n",
              filename, depth, mask);
    FAMErrno = FAM_UNIMPLEM;
    return (-1);
}


/**
 * FAMNextEvent:
 * @fc: pointer to a connection structure.
 * @fe: pointer to an event structure.
 *
 * Read the next event, possibly blocking on input.
 *
 * Returns 1 in case of success and -1 in case of error.
 */
int
FAMNextEvent(FAMConnection * fc, FAMEvent * fe)
{
    int ret;
    int fd;
    GAMDataPtr conn;

    if ((fc == NULL) || (fe == NULL)) {
	GAM_DEBUG(DEBUG_INFO, "FAMNextEvent() arg error\n");
        FAMErrno = FAM_ARG;
        return (-1);
    }
    conn = fc->client;
    if (conn == NULL) {
	GAM_DEBUG(DEBUG_INFO, "FAMNextEvent() arg error\n");
        FAMErrno = FAM_ARG;
        return (-1);
    }

    GAM_DEBUG(DEBUG_INFO, "FAMNextEvent(fd = %d)\n", fc->fd);

    fd = fc->fd;
    if (fd < 0) {
        return (-1);
    }

    // FIXME: drop and reacquire lock while blocked?
    gamin_data_lock(conn);
    while ((ret = gamin_data_event_ready(conn)) == 0) {
        if (gamin_read_data(conn, fc->fd, 1) < 0) {
	    gamin_try_reconnect(conn, fc->fd);
	    FAMErrno = FAM_CONNECT;
	    return (-1);
	}
    }

    if (ret > 0)
        ret = gamin_data_read_event(conn, fe);

    gamin_data_unlock(conn);

    if (ret < 0) {
        FAMErrno = FAM_CONNECT;
        return (ret);
    }
    fe->fc = fc;
#ifdef TEST_DEBUG
    GAM_DEBUG(DEBUG_INFO, "FAMNextEvent : %s\n", gamin_dump_event(fe));
#endif
    return (1);
}

/**
 * FAMPending:
 * @fc: pointer to a connection structure.
 *
 * Check for event waiting for processing.
 *
 * Returns the number of events waiting for processing or -1 in case of error.
 */
int
FAMPending(FAMConnection * fc)
{
    int ret;
    GAMDataPtr conn;

    if (fc == NULL) {
	GAM_DEBUG(DEBUG_INFO, "FAMPending() arg error\n");
        FAMErrno = FAM_ARG;
        return (-1);
    }
    conn = fc->client;
    if (conn == NULL) {
	GAM_DEBUG(DEBUG_INFO, "FAMPending() arg error\n");
        FAMErrno = FAM_ARG;
        return (-1);
    }

    GAM_DEBUG(DEBUG_INFO, "FAMPending(fd = %d)\n", fc->fd);

    gamin_data_lock(conn);
    if (gamin_data_event_ready(conn)) {
	 gamin_data_unlock(conn);
	 return (1);
    }

    /*
     * make sure we won't block if reading
     */
    ret = gamin_data_available(fc->fd);
    if (ret < 0)
        return (-1);
    if (ret > 0) {
        if (gamin_read_data(conn, fc->fd, 0) < 0) {
	    gamin_try_reconnect(conn, fc->fd);
	}
    }

    ret = (gamin_data_event_ready(conn));
    gamin_data_unlock(conn);

    return ret;
}

/**
 * FAMCancelMonitor:
 * @fc: pointer to a connection structure.
 * @fr: pointer to a request structure.
 *
 * This function is used to permanently stop a monitoring request.
 *
 * Returns 0 in case of success and -1 in case of error.
 */
int
FAMCancelMonitor(FAMConnection * fc, const FAMRequest * fr)
{
    int ret;
    GAMDataPtr conn;

    if ((fc == NULL) || (fr == NULL)) {
	GAM_DEBUG(DEBUG_INFO, "FAMCancelMonitor() arg error\n");
        FAMErrno = FAM_ARG;
        return (-1);
    }
    if ((fc->fd < 0) || (fc->client == NULL)) {
	GAM_DEBUG(DEBUG_INFO, "FAMCancelMonitor() arg error\n");
        FAMErrno = FAM_ARG;
        return (-1);
    }

    GAM_DEBUG(DEBUG_INFO, "FAMCancelMonitor(%d)\n", fr->reqnum);

    /*
     * Verify the request
     */
    conn = fc->client;
    gamin_data_lock(conn);
    ret = gamin_data_cancel(conn, fr->reqnum);
    if (ret < 0) {
        FAMErrno = FAM_ARG;
	gamin_data_unlock(conn);
        return (-1);
    }

    /*
     * send destruction message to the server
     */
    ret = gamin_send_request(GAM_REQ_CANCEL, fc->fd, NULL,
                             (FAMRequest *) fr, NULL, fc->client, 0);
    gamin_data_unlock(conn);

    if (ret != 0) {
        FAMErrno = FAM_CONNECT;
    }
    return (ret);
}

/**
 * FAMSuspendMonitor:
 * @fc: pointer to a connection structure.
 * @fr: pointer to a request structure.
 *
 * Unsupported call from the FAM API
 *
 * Returns 0 in case of success and -1 in case of error.
 */
int
FAMSuspendMonitor(FAMConnection *fc, const FAMRequest *fr) {
    gam_error(DEBUG_INFO,
              "Unsupported call to FAMSuspendMonitor()\n");
    FAMErrno = FAM_UNIMPLEM;
    return (-1);
}

/**
 * FAMResumeMonitor:
 * @fc: pointer to a connection structure.
 * @fr: pointer to a request structure.
 *
 * Unsupported call from the FAM API
 *
 * Returns 0 in case of success and -1 in case of error.
 */
int FAMResumeMonitor(FAMConnection *fc, const FAMRequest *fr) {
    gam_error(DEBUG_INFO,
              "Unsupported call to FAMResumeMonitor()\n");
    FAMErrno = FAM_UNIMPLEM;
    return (-1);
}

/**
 * FAMNoExists:
 * @fc: pointer to a connection structure.
 *
 * Specific extension for the core FAM API where Exists event are not
 * propagated on directory monitory listing startup. This speeds up
 * watching large directories but can introduce a mismatch between the FAM
 * view of the directory and the program own view.
 *
 * Returns 0 in case of success and -1 in case of error.
 */
int FAMNoExists(FAMConnection *fc) {
    int ret;
    GAMDataPtr conn;

    if (fc == NULL) {
	GAM_DEBUG(DEBUG_INFO, "FAMNoExists() arg error\n");
        FAMErrno = FAM_ARG;
        return (-1);
    }
    conn = fc->client;

    gamin_data_lock(conn);
    ret = gamin_data_no_exists(conn);
    gamin_data_unlock(conn);
    if (ret < 0) {
	GAM_DEBUG(DEBUG_INFO, "FAMNoExists() arg error\n");
        FAMErrno = FAM_ARG;
        return(-1);
    }
    return(0);
}

#ifdef GAMIN_DEBUG_API
/**
 * FAMDebug:
 * @fc: pointer to a connection structure.
 * @filename: file name.
 * @fr: pointer to the request structure.
 * @userdata: user provided data for the callbacks
 *
 * Specific extension for the core FAM API to request debug informations
 *
 * Returns 0 in case of success and -1 in case of error.
 */
int
FAMDebug(FAMConnection *fc, const char *filename, FAMRequest * fr,
         void *userData)
{
    int ret;

    if ((fc == NULL) || (filename == NULL) || (fr == NULL)) {
	GAM_DEBUG(DEBUG_INFO, "FAMDebug() arg error\n");
        FAMErrno = FAM_ARG;
        return (-1);
    }
    if (strlen(filename) >= MAXPATHLEN) {
        FAMErrno = FAM_FILE;
        return (-1);
    }
    if ((fc->fd < 0) || (fc->client == NULL)) {
	GAM_DEBUG(DEBUG_INFO, "FAMDebug() arg error\n");
        FAMErrno = FAM_ARG;
        return (-1);
    }

    GAM_DEBUG(DEBUG_INFO, "FAMDebug(%s)\n", filename);

    /*
     * send debug message to the server
     */
    gamin_data_lock(fc->client);
    ret = gamin_send_request(GAM_REQ_DEBUG, fc->fd, filename,
			     fr, userData, fc->client, 0);
    gamin_data_unlock(fc->client);

    if (debug_reqno == -1) {
        debug_reqno = fr->reqnum;
	debug_userData = userData;
    }
    return(ret);
}
#endif


syntax highlighted by Code2HTML, v. 0.9.1