* NEW: afpd: new volume option "nonetids"
* NEW: afpd: ACL access check caching
+* NEW: afpd: FCE event notifications
* UPD: Support for Berkeley DB 5.1
* UPD: case-conversion is based on Unicode 6.0.0
* UPD: cnid_metad: allow up to 4096 volumes
Makefile.in
netacnv
afpldaptest
+fce
logger_test
.deps
.libs
pkgconfdir = @PKGCONFDIR@
bin_PROGRAMS =
-noinst_PROGRAMS = netacnv logger_test
+noinst_PROGRAMS = netacnv logger_test fce
netacnv_SOURCES = netacnv.c
netacnv_LDADD = $(top_builddir)/libatalk/libatalk.la
logger_test_SOURCES = logger_test.c
logger_test_LDADD = $(top_builddir)/libatalk/libatalk.la
+fce_SOOURCE = fce.c
+fce_LDADD = $(top_builddir)/libatalk/libatalk.la
+fce_CFLAGS = -I$(top_srcdir)/include
+
bin_PROGRAMS += afpldaptest
afpldaptest_SOURCES = uuidtest.c
afpldaptest_CFLAGS = -D_PATH_ACL_LDAPCONF=\"$(pkgconfdir)/afp_ldap.conf\"
--- /dev/null
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif /* HAVE_CONFIG_H */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <inttypes.h>
+#include <sys/types.h>
+
+#include <sys/param.h>
+#include <sys/cdefs.h>
+
+#include <atalk/fce_api.h>
+#include <atalk/util.h>
+
+#define MAXBUFLEN 1024
+
+static char *fce_ev_names[] = {
+ "",
+ "FCE_FILE_MODIFY",
+ "FCE_FILE_DELETE",
+ "FCE_DIR_DELETE",
+ "FCE_FILE_CREATE",
+ "FCE_DIR_CREATE",
+ "FCE_TM_SIZE"
+};
+
+// get sockaddr, IPv4 or IPv6:
+static void *get_in_addr(struct sockaddr *sa)
+{
+ if (sa->sa_family == AF_INET) {
+ return &(((struct sockaddr_in*)sa)->sin_addr);
+ }
+
+ return &(((struct sockaddr_in6*)sa)->sin6_addr);
+}
+
+static int unpack_fce_packet(unsigned char *buf, struct fce_packet *packet)
+{
+ unsigned char *p = buf;
+
+ memcpy(&packet->magic[0], p, sizeof(packet->magic));
+ p += sizeof(packet->magic);
+
+ packet->version = *p;
+ p++;
+
+ packet->mode = *p;
+ p++;
+
+ memcpy(&packet->event_id, p, sizeof(packet->event_id));
+ p += sizeof(packet->event_id);
+ packet->event_id = ntohl(packet->event_id);
+
+ memcpy(&packet->datalen, p, sizeof(packet->datalen));
+ p += sizeof(packet->datalen);
+ packet->datalen = ntohs(packet->datalen);
+
+ memcpy(&packet->data[0], p, packet->datalen);
+ p += packet->datalen;
+
+ return 0;
+}
+
+int main(void)
+{
+ int sockfd;
+ struct addrinfo hints, *servinfo, *p;
+ int rv;
+ int numbytes;
+ struct sockaddr_storage their_addr;
+ char buf[MAXBUFLEN];
+ socklen_t addr_len;
+ char s[INET6_ADDRSTRLEN];
+ uint64_t tmsize;
+
+ memset(&hints, 0, sizeof hints);
+ hints.ai_family = AF_UNSPEC; // set to AF_INET to force IPv4
+ hints.ai_socktype = SOCK_DGRAM;
+
+ if ((rv = getaddrinfo(NULL, FCE_DEFAULT_PORT_STRING, &hints, &servinfo)) != 0) {
+ fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(rv));
+ return 1;
+ }
+
+ // loop through all the results and bind to the first we can
+ for(p = servinfo; p != NULL; p = p->ai_next) {
+ if ((sockfd = socket(p->ai_family, p->ai_socktype,
+ p->ai_protocol)) == -1) {
+ perror("listener: socket");
+ continue;
+ }
+
+ if (bind(sockfd, p->ai_addr, p->ai_addrlen) == -1) {
+ close(sockfd);
+ perror("listener: bind");
+ continue;
+ }
+
+ break;
+ }
+
+ if (p == NULL) {
+ fprintf(stderr, "listener: failed to bind socket\n");
+ return 2;
+ }
+
+ freeaddrinfo(servinfo);
+
+ printf("listener: waiting to recvfrom...\n");
+
+ addr_len = sizeof their_addr;
+
+ struct fce_packet packet;
+ while (1) {
+ if ((numbytes = recvfrom(sockfd,
+ buf,
+ MAXBUFLEN - 1,
+ 0,
+ (struct sockaddr *)&their_addr,
+ &addr_len)) == -1) {
+ perror("recvfrom");
+ exit(1);
+ }
+
+ unpack_fce_packet(buf, &packet);
+
+ if (memcmp(packet.magic, FCE_PACKET_MAGIC, sizeof(packet.magic)) == 0) {
+
+ switch (packet.mode) {
+ case FCE_TM_SIZE:
+ memcpy(&tmsize, packet.data, sizeof(uint64_t));
+ tmsize = ntoh64(tmsize);
+ printf("ID: %" PRIu32 ", Event: %s, Volume: %s, TM used size: %" PRIu64 " \n",
+ packet.event_id, fce_ev_names[packet.mode], packet.data + sizeof(uint64_t), tmsize);
+ break;
+
+ case FCE_CONN_START:
+ printf("FCE Start\n");
+ break;
+
+ case FCE_CONN_BROKEN:
+ printf("Broken FCE connection\n");
+ break;
+
+ default:
+ printf("ID: %" PRIu32 ", Event: %s, Path: %s\n",
+ packet.event_id, fce_ev_names[packet.mode], packet.data);
+ break;
+ }
+ }
+ }
+
+ close(sockfd);
+ return 0;
+}
Makefile
Makefile.in
afpd
+fce
hash
test_parse_mtab
.deps
pkgconfdir = @PKGCONFDIR@
sbin_PROGRAMS = afpd
-noinst_PROGRAMS = hash
+noinst_PROGRAMS = hash fce
afpd_SOURCES = \
afp_asp.c \
directory.c \
enumerate.c \
extattrs.c \
+ fce_api.c \
+ fce_util.c \
file.c \
filedir.c \
fork.c \
hash_SOURCES = hash.c
hash_CFLAGS = -DKAZLIB_TEST_MAIN -I$(top_srcdir)/include
+
+fce_SOURCES = fce_api.c fce_util.c
+fce_CFLAGS = -DFCE_TEST_MAIN -I$(top_srcdir)/include
+fce_LDADD = $(top_builddir)/libatalk/libatalk.la
#include <atalk/util.h>
#include <atalk/compat.h>
#include <atalk/globals.h>
+#include <atalk/fce_api.h>
#include "status.h"
#include "auth.h"
if ((c = getoption(buf, "-tcprcvbuf")))
options->tcp_rcvbuf = atoi(c);
+ if ((c = getoption(buf, "-fcelistener"))) {
+ LOG(log_note, logtype_afpd, "Adding fce listener \"%s\"", c);
+ fce_add_udp_socket(c);
+ }
+ if ((c = getoption(buf, "-fcecoalesce"))) {
+ LOG(log_note, logtype_afpd, "Fce coalesce: %s", c);
+ fce_set_coalesce(c);
+ }
+ if ((c = getoption(buf, "-fceevents"))) {
+ LOG(log_note, logtype_afpd, "Fce events: %s", c);
+ fce_set_events(c);
+ }
+
return 1;
}
#include <atalk/bstradd.h>
#include <atalk/errchk.h>
#include <atalk/globals.h>
+#include <atalk/fce_api.h>
#include "directory.h"
#include "dircache.h"
ad_setname(&ad, s_path->m_name);
ad_setid( &ad, s_path->st.st_dev, s_path->st.st_ino, dir->d_did, did, vol->v_stamp);
+ fce_register_new_dir(s_path);
+
ad_flush( &ad);
ad_close_metadata( &ad);
--- /dev/null
+/*\r
+ * Copyright (c) 2010 Mark Williams\r
+ *\r
+ * File change event API for netatalk\r
+ *\r
+ * for every detected filesystem change a UDP packet is sent to an arbitrary list\r
+ * of listeners. Each packet contains unix path of modified filesystem element,\r
+ * event reason, and a consecutive event id (32 bit). Technically we are UDP client and are sending\r
+ * out packets synchronuosly as they are created by the afp functions. This should not affect\r
+ * performance measurably. The only delaying calls occur during initialization, if we have to\r
+ * resolve non-IP hostnames to IP. All numeric data inside the packet is network byte order, so use\r
+ * ntohs / ntohl to resolve length and event id. Ideally a listener receives every packet with\r
+ * no gaps in event ids, starting with event id 1 and mode FCE_CONN_START followed by\r
+ * data events from id 2 up to 0xFFFFFFFF, followed by 0 to 0xFFFFFFFF and so on.\r
+ *\r
+ * A gap or not starting with 1 mode FCE_CONN_START or receiving mode FCE_CONN_BROKEN means that\r
+ * the listener has lost at least one filesystem event\r
+ * \r
+ * All Rights Reserved. See COPYRIGHT.\r
+ */\r
+\r
+#ifdef HAVE_CONFIG_H\r
+#include "config.h"\r
+#endif /* HAVE_CONFIG_H */\r
+\r
+#include <stdio.h>\r
+\r
+#include <string.h>\r
+#include <stdlib.h>\r
+#include <errno.h>\r
+#include <time.h>\r
+\r
+\r
+#include <sys/param.h>\r
+#include <sys/socket.h>\r
+#include <netinet/in.h>\r
+#include <arpa/inet.h>\r
+#include <netdb.h>\r
+\r
+#include <netatalk/at.h>\r
+\r
+#include <atalk/adouble.h>\r
+#include <atalk/vfs.h>\r
+#include <atalk/logger.h>\r
+#include <atalk/afp.h>\r
+#include <atalk/util.h>\r
+#include <atalk/cnid.h>\r
+#include <atalk/unix.h>\r
+#include <atalk/fce_api.h>\r
+\r
+#include "fork.h"\r
+#include "file.h"\r
+#include "globals.h"\r
+#include "directory.h"\r
+#include "desktop.h"\r
+#include "volume.h"\r
+\r
+// ONLY USED IN THIS FILE\r
+#include "fce_api_internal.h"\r
+\r
+#define FCE_TRUE 1\r
+#define FCE_FALSE 0\r
+\r
+/* We store our connection data here */\r
+static struct udp_entry udp_socket_list[FCE_MAX_UDP_SOCKS];\r
+static int udp_sockets = 0;\r
+static int udp_initialized = FCE_FALSE;\r
+static unsigned long fce_ev_enabled =\r
+ (1 << FCE_FILE_MODIFY) |\r
+ (1 << FCE_FILE_DELETE) |\r
+ (1 << FCE_DIR_DELETE) |\r
+ (1 << FCE_FILE_CREATE) |\r
+ (1 << FCE_DIR_CREATE);\r
+\r
+static uint64_t tm_used; /* used for passing to event handler */\r
+#define MAXIOBUF 1024\r
+static char iobuf[MAXIOBUF];\r
+static const char *skip_files[] = \r
+{\r
+ ".DS_Store",\r
+ NULL\r
+};\r
+\r
+/*\r
+ *\r
+ * Initialize network structs for any listeners\r
+ * We dont give return code because all errors are handled internally (I hope..)\r
+ *\r
+ * */\r
+void fce_init_udp()\r
+{\r
+ int rv;\r
+ struct addrinfo hints, *servinfo, *p;\r
+\r
+ if (udp_initialized == FCE_TRUE)\r
+ return;\r
+\r
+ memset(&hints, 0, sizeof hints);\r
+ hints.ai_family = AF_UNSPEC;\r
+ hints.ai_socktype = SOCK_DGRAM;\r
+\r
+ for (int i = 0; i < udp_sockets; i++) {\r
+ struct udp_entry *udp_entry = udp_socket_list + i;\r
+\r
+ /* Close any pending sockets */\r
+ if (udp_entry->sock != -1)\r
+ close(udp_entry->sock);\r
+\r
+ if ((rv = getaddrinfo(udp_entry->addr, udp_entry->port, &hints, &servinfo)) != 0) {\r
+ LOG(log_error, logtype_afpd, "fce_init_udp: getaddrinfo(%s:%s): %s",\r
+ udp_entry->addr, udp_entry->port, gai_strerror(rv));\r
+ continue;\r
+ }\r
+\r
+ /* loop through all the results and make a socket */\r
+ for (p = servinfo; p != NULL; p = p->ai_next) {\r
+ if ((udp_entry->sock = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) == -1) {\r
+ LOG(log_error, logtype_afpd, "fce_init_udp: socket(%s:%s): %s",\r
+ udp_entry->addr, udp_entry->port, strerror(errno));\r
+ continue;\r
+ }\r
+ break;\r
+ }\r
+\r
+ if (p == NULL) {\r
+ LOG(log_error, logtype_afpd, "fce_init_udp: no socket for %s:%s",\r
+ udp_entry->addr, udp_entry->port);\r
+ }\r
+ udp_entry->addrinfo = *p;\r
+ memcpy(&udp_entry->addrinfo, p, sizeof(struct addrinfo));\r
+ memcpy(&udp_entry->sockaddr, p->ai_addr, sizeof(struct sockaddr_storage));\r
+ freeaddrinfo(servinfo);\r
+ }\r
+\r
+ udp_initialized = FCE_TRUE;\r
+}\r
+\r
+void fce_cleanup()\r
+{\r
+ if (udp_initialized == FCE_FALSE )\r
+ return;\r
+\r
+ for (int i = 0; i < udp_sockets; i++)\r
+ {\r
+ struct udp_entry *udp_entry = udp_socket_list + i;\r
+\r
+ /* Close any pending sockets */\r
+ if (udp_entry->sock != -1)\r
+ {\r
+ close( udp_entry->sock );\r
+ udp_entry->sock = -1;\r
+ }\r
+ }\r
+ udp_initialized = FCE_FALSE;\r
+}\r
+\r
+\r
+/*\r
+ * Construct a UDP packet for our listeners and return packet size\r
+ * */\r
+static ssize_t build_fce_packet( struct fce_packet *packet, char *path, int mode, uint32_t event_id )\r
+{\r
+ size_t pathlen;\r
+ ssize_t data_len = 0;\r
+\r
+ strncpy(packet->magic, FCE_PACKET_MAGIC, sizeof(packet->magic) );\r
+ packet->version = FCE_PACKET_VERSION;\r
+ packet->mode = mode;\r
+ packet->event_id = event_id;\r
+\r
+ pathlen = strlen(path) + 1; /* include string terminator */\r
+\r
+ /* This should never happen, but before we bust this server, we send nonsense, fce listener has to cope */\r
+ if (pathlen >= MAXPATHLEN)\r
+ pathlen = MAXPATHLEN - 1;\r
+\r
+ /* This is the payload len. Means: the stream has len bytes more until packet is finished */\r
+ /* A server should read the first 16 byte, decode them and then fetch the rest */\r
+ data_len = FCE_PACKET_HEADER_SIZE + pathlen;\r
+ packet->datalen = pathlen;\r
+\r
+ switch (mode) {\r
+ case FCE_TM_SIZE:\r
+ tm_used = hton64(tm_used);\r
+ memcpy(packet->data, &tm_used, sizeof(tm_used));\r
+ strncpy(packet->data + sizeof(tm_used), path, pathlen);\r
+\r
+ packet->datalen += sizeof(tm_used);\r
+ data_len += sizeof(tm_used);\r
+ break;\r
+ default:\r
+ strncpy(packet->data, path, pathlen);\r
+ break;\r
+ }\r
+\r
+ /* return the packet len */\r
+ return data_len;\r
+}\r
+\r
+static int pack_fce_packet(struct fce_packet *packet, unsigned char *buf)\r
+{\r
+ unsigned char *p = buf;\r
+\r
+ memcpy(p, &packet->magic[0], sizeof(packet->magic));\r
+ p += sizeof(packet->magic);\r
+\r
+ *p = packet->version;\r
+ p++;\r
+ \r
+ *p = packet->mode;\r
+ p++;\r
+ \r
+ uint32_t id = htonl(packet->event_id);\r
+ memcpy(p, &id, sizeof(id));\r
+ p += sizeof(packet->event_id);\r
+\r
+ uint16_t l = htons(packet->datalen);\r
+ memcpy(p, &l, sizeof(l));\r
+ p += sizeof(l);\r
+\r
+ memcpy(p, &packet->data[0], packet->datalen);\r
+ p += packet->datalen;\r
+\r
+ return 0;\r
+}\r
+\r
+/*\r
+ * Send the fce information to all (connected) listeners\r
+ * We dont give return code because all errors are handled internally (I hope..)\r
+ * */\r
+static void send_fce_event( char *path, int mode )\r
+{ \r
+ struct fce_packet packet;\r
+ void *data = &packet;\r
+ static uint32_t event_id = 0; /* the unique packet couter to detect packet/data loss. Going from 0xFFFFFFFF to 0x0 is a valid increment */\r
+\r
+ LOG(log_debug, logtype_afpd, "send_fce_event: start");\r
+\r
+ time_t now = time(NULL);\r
+\r
+ /* build our data packet */\r
+ ssize_t data_len = build_fce_packet( &packet, path, mode, ++event_id );\r
+ pack_fce_packet(&packet, iobuf);\r
+\r
+ for (int i = 0; i < udp_sockets; i++)\r
+ {\r
+ int sent_data = 0;\r
+ struct udp_entry *udp_entry = udp_socket_list + i;\r
+\r
+ /* we had a problem earlier ? */\r
+ if (udp_entry->sock == -1)\r
+ {\r
+ /* We still have to wait ?*/\r
+ if (now < udp_entry->next_try_on_error)\r
+ continue;\r
+\r
+ /* Reopen socket */\r
+ udp_entry->sock = socket(udp_entry->addrinfo.ai_family,\r
+ udp_entry->addrinfo.ai_socktype,\r
+ udp_entry->addrinfo.ai_protocol);\r
+ \r
+ if (udp_entry->sock == -1) {\r
+ /* failed again, so go to rest again */\r
+ LOG(log_error, logtype_afpd, "Cannot recreate socket for fce UDP connection: errno %d", errno );\r
+\r
+ udp_entry->next_try_on_error = now + FCE_SOCKET_RETRY_DELAY_S;\r
+ continue;\r
+ }\r
+\r
+ udp_entry->next_try_on_error = 0;\r
+\r
+ /* Okay, we have a running socket again, send server that we had a problem on our side*/\r
+ data_len = build_fce_packet( &packet, "", FCE_CONN_BROKEN, 0 );\r
+ pack_fce_packet(&packet, iobuf);\r
+\r
+ sendto(udp_entry->sock,\r
+ iobuf,\r
+ data_len,\r
+ 0,\r
+ (struct sockaddr *)&udp_entry->sockaddr,\r
+ udp_entry->addrinfo.ai_addrlen);\r
+\r
+ /* Rebuild our original data packet */\r
+ data_len = build_fce_packet( &packet, path, mode, event_id );\r
+ pack_fce_packet(&packet, iobuf);\r
+ }\r
+\r
+ sent_data = sendto(udp_entry->sock,\r
+ iobuf,\r
+ data_len,\r
+ 0,\r
+ (struct sockaddr *)&udp_entry->sockaddr,\r
+ udp_entry->addrinfo.ai_addrlen);\r
+\r
+ /* Problems ? */\r
+ if (sent_data != data_len) {\r
+ /* Argh, socket broke, we close and retry later */\r
+ LOG(log_error, logtype_afpd, "send_fce_event: error sending packet to %s:%s, transfered %d of %d: %s",\r
+ udp_entry->addr, udp_entry->port, sent_data, data_len, strerror(errno));\r
+\r
+ close( udp_entry->sock );\r
+ udp_entry->sock = -1;\r
+ udp_entry->next_try_on_error = now + FCE_SOCKET_RETRY_DELAY_S;\r
+ }\r
+ }\r
+}\r
+\r
+static int add_udp_socket(const char *target_ip, const char *target_port )\r
+{\r
+ if (target_port == NULL)\r
+ target_port = FCE_DEFAULT_PORT_STRING;\r
+\r
+ if (udp_sockets >= FCE_MAX_UDP_SOCKS) {\r
+ LOG(log_error, logtype_afpd, "Too many file change api UDP connections (max %d allowed)", FCE_MAX_UDP_SOCKS );\r
+ return AFPERR_PARAM;\r
+ }\r
+\r
+ udp_socket_list[udp_sockets].addr = strdup(target_ip);\r
+ udp_socket_list[udp_sockets].port = strdup(target_port);\r
+ udp_socket_list[udp_sockets].sock = -1;\r
+ memset(&udp_socket_list[udp_sockets].addrinfo, 0, sizeof(struct addrinfo));\r
+ memset(&udp_socket_list[udp_sockets].sockaddr, 0, sizeof(struct sockaddr_storage));\r
+ udp_socket_list[udp_sockets].next_try_on_error = 0;\r
+\r
+ udp_sockets++;\r
+\r
+ return AFP_OK;\r
+}\r
+\r
+/*\r
+ *\r
+ * Dispatcher for all incoming file change events\r
+ *\r
+ * */\r
+static int register_fce(const char *u_name, int is_dir, int mode)\r
+{\r
+ if (udp_sockets == 0)\r
+ /* No listeners configured */\r
+ return AFP_OK;\r
+\r
+ if (u_name == NULL)\r
+ return AFPERR_PARAM;\r
+\r
+ static int first_event = FCE_TRUE;\r
+\r
+ /* do some initialization on the fly the first time */\r
+ if (first_event) {\r
+ fce_initialize_history();\r
+ }\r
+\r
+ /* handle files which should not cause events (.DS_Store atc. ) */\r
+ for (int i = 0; skip_files[i] != NULL; i++)\r
+ {\r
+ if (!strcmp( u_name, skip_files[i]))\r
+ return AFP_OK;\r
+ }\r
+\r
+\r
+ char full_path_buffer[MAXPATHLEN + 1] = {""};\r
+ const char *cwd = getcwdpath();\r
+\r
+ if (mode == FCE_TM_SIZE) {\r
+ strlcpy(full_path_buffer, u_name, MAXPATHLEN);\r
+ } else if (!is_dir || mode == FCE_DIR_DELETE) {\r
+ if (strlen( cwd ) + strlen( u_name) + 1 >= MAXPATHLEN) {\r
+ LOG(log_error, logtype_afpd, "FCE file name too long: %s/%s", cwd, u_name );\r
+ return AFPERR_PARAM;\r
+ }\r
+ sprintf( full_path_buffer, "%s/%s", cwd, u_name );\r
+ } else {\r
+ if (strlen( cwd ) >= MAXPATHLEN) {\r
+ LOG(log_error, logtype_afpd, "FCE directory name too long: %s", cwd);\r
+ return AFPERR_PARAM;\r
+ }\r
+ strcpy( full_path_buffer, cwd);\r
+ }\r
+\r
+ /* Can we ignore this event based on type or history? */\r
+ if (!(mode & FCE_TM_SIZE) && fce_handle_coalescation( full_path_buffer, is_dir, mode ))\r
+ {\r
+ LOG(log_debug9, logtype_afpd, "Coalesced fc event <%d> for <%s>", mode, full_path_buffer );\r
+ return AFP_OK;\r
+ }\r
+\r
+ LOG(log_debug9, logtype_afpd, "Detected fc event <%d> for <%s>", mode, full_path_buffer );\r
+\r
+\r
+ /* we do initilization on the fly, no blocking calls in here \r
+ * (except when using FQDN in broken DNS environment)\r
+ */\r
+ if (first_event == FCE_TRUE)\r
+ {\r
+ fce_init_udp();\r
+ \r
+ /* Notify listeners the we start from the beginning */\r
+ send_fce_event( "", FCE_CONN_START );\r
+ \r
+ first_event = FCE_FALSE;\r
+ }\r
+\r
+ /* Handle UDP transport */\r
+ send_fce_event( full_path_buffer, mode );\r
+\r
+ return AFP_OK;\r
+}\r
+\r
+\r
+/******************** External calls start here **************************/\r
+\r
+/*\r
+ * API-Calls for file change api, called form outside (file.c directory.c ofork.c filedir.c)\r
+ * */\r
+#ifndef FCE_TEST_MAIN\r
+\r
+int fce_register_delete_file( struct path *path )\r
+{\r
+ int ret = AFP_OK;\r
+\r
+ if (path == NULL)\r
+ return AFPERR_PARAM;\r
+\r
+ if (!(fce_ev_enabled & (1 << FCE_FILE_DELETE)))\r
+ return ret;\r
+ \r
+ ret = register_fce( path->u_name, FALSE, FCE_FILE_DELETE );\r
+\r
+ return ret;\r
+}\r
+int fce_register_delete_dir( char *name )\r
+{\r
+ int ret = AFP_OK;\r
+\r
+ if (name == NULL)\r
+ return AFPERR_PARAM;\r
+\r
+ if (!(fce_ev_enabled & (1 << FCE_DIR_DELETE)))\r
+ return ret;\r
+ \r
+ ret = register_fce( name, TRUE, FCE_DIR_DELETE);\r
+\r
+ return ret;\r
+}\r
+\r
+int fce_register_new_dir( struct path *path )\r
+{\r
+ int ret = AFP_OK;\r
+\r
+ if (path == NULL)\r
+ return AFPERR_PARAM;\r
+\r
+ if (!(fce_ev_enabled & (1 << FCE_DIR_CREATE)))\r
+ return ret;\r
+\r
+ ret = register_fce( path->u_name, TRUE, FCE_DIR_CREATE );\r
+\r
+ return ret;\r
+}\r
+\r
+\r
+int fce_register_new_file( struct path *path )\r
+{\r
+ int ret = AFP_OK;\r
+\r
+ if (path == NULL)\r
+ return AFPERR_PARAM;\r
+\r
+ if (!(fce_ev_enabled & (1 << FCE_FILE_CREATE)))\r
+ return ret;\r
+\r
+ ret = register_fce( path->u_name, FALSE, FCE_FILE_CREATE );\r
+\r
+ return ret;\r
+}\r
+\r
+int fce_register_file_modification( struct ofork *ofork )\r
+{\r
+ char *u_name = NULL;\r
+ struct vol *vol;\r
+ int ret = AFP_OK;\r
+\r
+ if (ofork == NULL || ofork->of_vol == NULL)\r
+ return AFPERR_PARAM;\r
+\r
+ if (!(fce_ev_enabled & (1 << FCE_FILE_MODIFY)))\r
+ return ret;\r
+\r
+ vol = ofork->of_vol;\r
+\r
+ if (NULL == (u_name = mtoupath(vol, of_name(ofork), ofork->of_did, utf8_encoding()))) \r
+ {\r
+ return AFPERR_MISC;\r
+ }\r
+ \r
+ ret = register_fce( u_name, FALSE, FCE_FILE_MODIFY );\r
+ \r
+ return ret; \r
+}\r
+\r
+int fce_register_tm_size(const char *vol, size_t used)\r
+{\r
+ int ret = AFP_OK;\r
+\r
+ if (vol == NULL)\r
+ return AFPERR_PARAM;\r
+\r
+ if (!(fce_ev_enabled & (1 << FCE_TM_SIZE)))\r
+ return ret;\r
+\r
+ tm_used = used; /* oh what a hack */\r
+ ret = register_fce(vol, FALSE, FCE_TM_SIZE);\r
+\r
+ return ret;\r
+}\r
+#endif\r
+\r
+/*\r
+ *\r
+ * Extern connect to afpd parameter, can be called multiple times for multiple listeners (up to MAX_UDP_SOCKS times)\r
+ *\r
+ * */\r
+int fce_add_udp_socket(const char *target)\r
+{\r
+ const char *port = FCE_DEFAULT_PORT_STRING;\r
+ char target_ip[256] = {""};\r
+\r
+ strncpy(target_ip, target, sizeof(target_ip) -1);\r
+\r
+ char *port_delim = strchr( target_ip, ':' );\r
+ if (port_delim) {\r
+ *port_delim = 0;\r
+ port = port_delim + 1;\r
+ }\r
+ return add_udp_socket(target_ip, port);\r
+}\r
+\r
+int fce_set_events(const char *events)\r
+{\r
+ char *e;\r
+ char *p;\r
+ \r
+ if (events == NULL)\r
+ return AFPERR_PARAM;\r
+\r
+ e = strdup(events);\r
+\r
+ fce_ev_enabled = 0;\r
+\r
+ for (p = strtok(e, ","); p; p = strtok(NULL, ",")) {\r
+ if (strcmp(p, "fmod") == 0) {\r
+ fce_ev_enabled |= (1 << FCE_FILE_MODIFY);\r
+ } else if (strcmp(p, "fdel") == 0) {\r
+ fce_ev_enabled |= (1 << FCE_FILE_DELETE);\r
+ } else if (strcmp(p, "ddel") == 0) {\r
+ fce_ev_enabled |= (1 << FCE_DIR_DELETE);\r
+ } else if (strcmp(p, "fcre") == 0) {\r
+ fce_ev_enabled |= (1 << FCE_FILE_CREATE);\r
+ } else if (strcmp(p, "dcre") == 0) {\r
+ fce_ev_enabled |= (1 << FCE_DIR_CREATE);\r
+ } else if (strcmp(p, "tmsz") == 0) {\r
+ fce_ev_enabled |= (1 << FCE_TM_SIZE);\r
+ }\r
+ }\r
+\r
+ free(e);\r
+}\r
+\r
+#ifdef FCE_TEST_MAIN\r
+\r
+\r
+void shortsleep( unsigned int us )\r
+{ \r
+ usleep( us );\r
+}\r
+int main( int argc, char*argv[] )\r
+{\r
+ int c,ret;\r
+\r
+ char *port = FCE_DEFAULT_PORT_STRING;\r
+ char *host = "localhost";\r
+ int delay_between_events = 1000;\r
+ int event_code = FCE_FILE_MODIFY;\r
+ char pathbuff[1024];\r
+ int duration_in_seconds = 0; // TILL ETERNITY\r
+ char target[256];\r
+ char *path = getcwd( pathbuff, sizeof(pathbuff) );\r
+\r
+ // FULLSPEED TEST IS "-s 1001" -> delay is 0 -> send packets without pause\r
+\r
+ while ((c = getopt(argc, argv, "d:e:h:p:P:s:")) != -1) {\r
+ switch(c) {\r
+ case '?':\r
+ fprintf(stdout, "%s: [ -p Port -h Listener1 [ -h Listener2 ...] -P path -s Delay_between_events_in_us -e event_code -d Duration ]\n", argv[0]);\r
+ exit(1);\r
+ break;\r
+ case 'd':\r
+ duration_in_seconds = atoi(optarg);\r
+ break;\r
+ case 'e':\r
+ event_code = atoi(optarg);\r
+ break;\r
+ case 'h':\r
+ host = strdup(optarg);\r
+ break;\r
+ case 'p':\r
+ port = strdup(optarg);\r
+ break;\r
+ case 'P':\r
+ path = strdup(optarg);\r
+ break;\r
+ case 's':\r
+ delay_between_events = atoi(optarg);\r
+ break;\r
+ }\r
+ }\r
+\r
+ sprintf(target, "%s:%s", host, port);\r
+ if (fce_add_udp_socket(target) != 0)\r
+ return 1;\r
+\r
+ int ev_cnt = 0;\r
+ time_t start_time = time(NULL);\r
+ time_t end_time = 0;\r
+\r
+ if (duration_in_seconds)\r
+ end_time = start_time + duration_in_seconds;\r
+\r
+ while (1)\r
+ {\r
+ time_t now = time(NULL);\r
+ if (now > start_time)\r
+ {\r
+ start_time = now;\r
+ fprintf( stdout, "%d events/s\n", ev_cnt );\r
+ ev_cnt = 0;\r
+ }\r
+ if (end_time && now >= end_time)\r
+ break;\r
+\r
+ register_fce( path, 0, event_code );\r
+ ev_cnt++;\r
+\r
+ \r
+ shortsleep( delay_between_events );\r
+ }\r
+}\r
+#endif /* TESTMAIN*/\r
--- /dev/null
+/* \r
+ * File: fce_api_internal.h\r
+ * Author: mw\r
+ *\r
+ * Created on 1. Oktober 2010, 23:48\r
+ */\r
+\r
+#ifndef _FCE_API_INTERNAL_H\r
+#define _FCE_API_INTERNAL_H\r
+\r
+#define FCE_MAX_UDP_SOCKS 5 /* Allow a maximum of udp listeners for file change events */\r
+#define FCE_SOCKET_RETRY_DELAY_S 600 /* Pause this time in s after socket was broken */\r
+#define FCE_PACKET_VERSION 1\r
+#define FCE_HISTORY_LEN 10 /* This is used to coalesce events */\r
+#define MAX_COALESCE_TIME_MS 1000 /* Events oldeer than this are not coalesced */\r
+\r
+struct udp_entry\r
+{\r
+ int sock;\r
+ char *addr;\r
+ char *port;\r
+ struct addrinfo addrinfo;\r
+ struct sockaddr_storage sockaddr;\r
+ time_t next_try_on_error; /* In case of error set next timestamp to retry */\r
+};\r
+\r
+struct fce_history\r
+{\r
+ unsigned char mode;\r
+ int is_dir;\r
+ char path[MAXPATHLEN + 1];\r
+ struct timeval tv;\r
+};\r
+\r
+#define PACKET_HDR_LEN (sizeof(struct fce_packet) - FCE_MAX_PATH_LEN)\r
+\r
+int fce_handle_coalescation( char *path, int is_dir, int mode );\r
+void fce_initialize_history();\r
+\r
+\r
+#endif /* _FCE_API_INTERNAL_H */\r
+\r
--- /dev/null
+/*\r
+ * $Id: fce_api.c,v 0.01 2010-10-01 00:00:0 mw Exp $\r
+ *\r
+ * Copyright (c) 2010 Mark Williams\r
+ *\r
+ * File change event API for netatalk\r
+ *\r
+ * for every detected filesystem change a UDP packet is sent to an arbitrary list\r
+ * of listeners. Each packet contains unix path of modified filesystem element,\r
+ * event reason, and a consecutive event id (32 bit). Technically we are UDP client and are sending\r
+ * out packets synchronuosly as they are created by the afp functions. This should not affect\r
+ * performance measurably. The only delaying calls occur during initialization, if we have to\r
+ * resolve non-IP hostnames to IP. All numeric data inside the packet is network byte order, so use\r
+ * ntohs / ntohl to resolve length and event id. Ideally a listener receives every packet with\r
+ * no gaps in event ids, starting with event id 1 and mode FCE_CONN_START followed by\r
+ * data events from id 2 up to 0xFFFFFFFF, followed by 0 to 0xFFFFFFFF and so on.\r
+ *\r
+ * A gap or not starting with 1 mode FCE_CONN_START or receiving mode FCE_CONN_BROKEN means that\r
+ * the listener has lost at least one filesystem event\r
+ * \r
+ * All Rights Reserved. See COPYRIGHT.\r
+ */\r
+\r
+#ifdef HAVE_CONFIG_H\r
+#include "config.h"\r
+#endif /* HAVE_CONFIG_H */\r
+\r
+#include <stdio.h>\r
+\r
+#include <string.h>\r
+#include <stdlib.h>\r
+#include <errno.h>\r
+#include <time.h>\r
+\r
+\r
+#include <sys/param.h>\r
+#include <sys/socket.h>\r
+#include <netinet/in.h>\r
+#include <arpa/inet.h>\r
+#include <netdb.h>\r
+\r
+#include <netatalk/at.h>\r
+\r
+#include <atalk/adouble.h>\r
+#include <atalk/vfs.h>\r
+#include <atalk/logger.h>\r
+#include <atalk/afp.h>\r
+#include <atalk/util.h>\r
+#include <atalk/cnid.h>\r
+#include <atalk/unix.h>\r
+#include <atalk/fce_api.h>\r
+\r
+#include "fork.h"\r
+#include "file.h"\r
+#include "globals.h"\r
+#include "directory.h"\r
+#include "desktop.h"\r
+#include "volume.h"\r
+\r
+// ONLY USED IN THIS FILE\r
+#include "fce_api_internal.h"\r
+\r
+#define FCE_TRUE 1\r
+#define FCE_FALSE 0\r
+\r
+/* We store our connection data here */\r
+static char coalesce[80] = {""};\r
+static struct fce_history fce_history_list[FCE_HISTORY_LEN];\r
+\r
+\r
+\r
+\r
+/****\r
+* With coalesce we try to reduce the events over UDP, the eventlistener would throw these \r
+* events away anyway.\r
+* This works only, if the connected listener uses the events on a "per directory" base\r
+* It is a very simple aproach, but saves a lot of events sent to listeners.\r
+* Every "child element" event is ignored as long as its parent event is not older \r
+* than MAX_COALESCE_TIME_MS ms. If large directory trees or large files are created or deleted, \r
+* this probably will not work recursive, because the time to copy data will exceed this \r
+* event timeout. \r
+* \r
+****/\r
+static int coalesce_none()\r
+{\r
+ return coalesce[0] == 0;\r
+}\r
+static int coalesce_all()\r
+{\r
+ return !strcmp( coalesce, "all" );\r
+}\r
+static int coalesce_create()\r
+{\r
+ return !strcmp( coalesce, "create" ) || coalesce_all();\r
+}\r
+static int coalesce_delete()\r
+{\r
+ return !strcmp( coalesce, "delete" ) || coalesce_all();\r
+}\r
+\r
+void fce_initialize_history()\r
+{\r
+ for (int i = 0; i < FCE_HISTORY_LEN; i++)\r
+ {\r
+ memset( &fce_history_list[i], 0, sizeof(fce_history_list[i]) );\r
+ }\r
+}\r
+\r
+static long get_ms_difftime ( struct timeval *tv1, struct timeval *tv2 )\r
+{\r
+ unsigned long s = tv2->tv_sec - tv1->tv_sec;\r
+ long us = tv2->tv_usec - tv1->tv_usec;\r
+\r
+ return s * 1000 + us/1000;\r
+}\r
+\r
+int fce_handle_coalescation( char *path, int is_dir, int mode )\r
+{\r
+ if (coalesce_none())\r
+ return FALSE;\r
+\r
+ \r
+\r
+ // First one:\r
+ // After a file creation *ALWAYS* a file modification is produced\r
+ if (mode == FCE_FILE_CREATE)\r
+ {\r
+ if (coalesce_create())\r
+ {\r
+ return TRUE;\r
+ }\r
+ }\r
+\r
+ /* get timestamp */\r
+ struct timeval tv;\r
+ gettimeofday(&tv, 0);\r
+\r
+\r
+ /* These two are used to eval our next index in history */\r
+ /* the history is unsorted, speed should not be a problem, length is 10 */\r
+ unsigned long oldest_entry = (unsigned long )((long)-1);\r
+ int oldest_entry_idx = -1;\r
+\r
+ /* Now detect events in the very near history */\r
+ for (int i = 0; i < FCE_HISTORY_LEN; i++)\r
+ {\r
+ struct fce_history *fh = &fce_history_list[i];\r
+\r
+ //* Not inited ? */\r
+ if (fh->tv.tv_sec == 0)\r
+ {\r
+ /* we can use it for new elements */\r
+ oldest_entry = 0;\r
+ oldest_entry_idx = i;\r
+ continue;\r
+ }\r
+\r
+ //* Too old ? */\r
+ if (get_ms_difftime( &fh->tv, &tv ) > MAX_COALESCE_TIME_MS)\r
+ {\r
+ /* Invalidate entry */\r
+ fh->tv.tv_sec = 0;\r
+\r
+ oldest_entry = 0;\r
+ oldest_entry_idx = i; \r
+ continue;\r
+ }\r
+\r
+\r
+ /* If we find a parent dir wich was created we are done */\r
+ if (coalesce_create() && fh->mode == FCE_DIR_CREATE)\r
+ {\r
+ //* Parent dir ? */\r
+ if (!strncmp( fh->path, path, strlen( fh->path ) ) )\r
+ {\r
+ return TRUE;\r
+ }\r
+ }\r
+\r
+ /* If we find a parent dir we should be DELETED we are done */\r
+ if (coalesce_delete() && fh->is_dir && (mode == FCE_FILE_DELETE || mode == FCE_DIR_DELETE))\r
+ {\r
+ //* Parent dir ? */\r
+ if (!strncmp( fh->path, path, strlen( fh->path ) ) )\r
+ {\r
+ return TRUE;\r
+ }\r
+ }\r
+\r
+ //* Detect oldest entry for next new entry */\r
+ if (oldest_entry_idx == -1 || fh->tv.tv_sec < oldest_entry)\r
+ {\r
+ oldest_entry = fh->tv.tv_sec;\r
+ oldest_entry_idx = i;\r
+ }\r
+ }\r
+\r
+ /* We have a new entry for the history, register it */\r
+ fce_history_list[oldest_entry_idx].tv = tv;\r
+ fce_history_list[oldest_entry_idx].mode = mode;\r
+ fce_history_list[oldest_entry_idx].is_dir = is_dir;\r
+ strncpy( fce_history_list[oldest_entry_idx].path, path, MAXPATHLEN);\r
+\r
+ /* we have to handle this event */\r
+ return FALSE;\r
+}\r
+\r
+/*\r
+ *\r
+ * Set event coalescation to reduce number of events sent over UDP \r
+ * all|delete|create\r
+ *\r
+ *\r
+ * */\r
+\r
+int fce_set_coalesce( char *coalesce_opt )\r
+{\r
+ strncpy( coalesce, coalesce_opt, sizeof(coalesce) - 1 ); \r
+}\r
+\r
+\r
+\r
+\r
#include <atalk/cnid.h>
#include <atalk/unix.h>
#include <atalk/globals.h>
+#include <atalk/fce_api.h>
#include "directory.h"
#include "dircache.h"
(void)get_id(vol, adp, &st, dir->d_did, upath, strlen(upath));
ad_flush( adp);
+
+ fce_register_new_file(s_path);
+
ad_close( adp, ADFLAGS_DF|ADFLAGS_HF );
createfile_done:
#include <atalk/bstradd.h>
#include <atalk/acl.h>
#include <atalk/globals.h>
+#include <atalk/fce_api.h>
#include "directory.h"
#include "dircache.h"
upath = s_path->u_name;
if ( path_isadir( s_path) ) {
- if (*s_path->m_name != '\0' || curdir->d_did == DIRDID_ROOT)
+ if (*s_path->m_name != '\0' || curdir->d_did == DIRDID_ROOT) {
rc = AFPERR_ACCESS;
- else
- rc = deletecurdir( vol);
+ } else {
+ /* we have to cache this, the structs are lost in deletcurdir*/
+ /* but we need the positive returncode to send our event */
+ bstring dname;
+ if ((dname = bstrcpy(curdir->d_u_name)) == NULL)
+ return AFPERR_MISC;
+ if ((rc = deletecurdir(vol)) == AFP_OK)
+ fce_register_delete_dir(cfrombstr(dname));
+ bdestroy(dname);
+ }
} else if (of_findname(s_path)) {
rc = AFPERR_BUSY;
} else {
*/
if (s_path->st_valid && s_path->st_errno == ENOENT) {
rc = AFPERR_NOOBJ;
- }
- else {
- rc = deletefile(vol, -1, upath, 1);
+ } else {
+ if ((rc = deletefile(vol, -1, upath, 1)) == AFP_OK)
+ fce_register_delete_file( s_path );
struct dir *cachedfile;
if ((cachedfile = dircache_search_by_name(vol, dir, upath, strlen(upath)))) {
if ( ad_meta_fileno( ofork->of_ad ) != -1 ) /* META */
ofork->of_flags |= AFPFORK_DIRTY;
+ /* we have modified any fork, remember until close_fork */
+ ofork->of_flags |= AFPFORK_MODIFIED;
+
*rbuflen = set_off_t (offset, rbuf, is64);
return( AFP_OK );
#define AFPFORK_ACCRD (1<<4)
#define AFPFORK_ACCWR (1<<5)
#define AFPFORK_ACCMASK (AFPFORK_ACCRD | AFPFORK_ACCWR)
+#define AFPFORK_MODIFIED (1<<6) /* used in FCE for modified files */
#ifdef AFS
extern struct ofork *writtenfork;
#include <atalk/bstrlib.h>
#include <atalk/bstradd.h>
#include <atalk/globals.h>
+#include <atalk/fce_api.h>
#include "volume.h"
#include "directory.h"
}
}
}
+
+ /* Somone has used write_fork, we assume file was changed, register it to file change event api */
+ if (ofork->of_flags & AFPFORK_MODIFIED) {
+ fce_register_file_modification(ofork);
+ }
+
ret = 0;
if ( ad_close( ofork->of_ad, adflags ) < 0 ) {
ret = -1;
#define MIN(a, b) ((a) < (b) ? (a) : (b))
#endif /* ! MIN */
-#ifndef NO_LARGE_VOL_SUPPORT
-#if BYTE_ORDER == BIG_ENDIAN
-#define hton64(x) (x)
-#define ntoh64(x) (x)
-#else /* BYTE_ORDER == BIG_ENDIAN */
-#define hton64(x) ((u_int64_t) (htonl(((x) >> 32) & 0xffffffffLL)) | \
- (u_int64_t) ((htonl(x) & 0xffffffffLL) << 32))
-#define ntoh64(x) (hton64(x))
-#endif /* BYTE_ORDER == BIG_ENDIAN */
-#endif /* ! NO_LARGE_VOL_SUPPORT */
-
#ifndef UUID_PRINTABLE_STRING_LENGTH
#define UUID_PRINTABLE_STRING_LENGTH 37
#endif
server_ipc.h tdb.h uam.h unicode.h util.h uuid.h volinfo.h \
zip.h ea.h acl.h unix.h directory.h hash.h volume.h
-noinst_HEADERS = cnid_dbd_private.h cnid_private.h bstradd.h bstrlib.h errchk.h ftw.h globals.h
+noinst_HEADERS = cnid_dbd_private.h cnid_private.h bstradd.h bstrlib.h errchk.h ftw.h globals.h fce_api.h
--- /dev/null
+/*
+ * File: fce_api.h
+ * Author: mw
+ *
+ * Created on 1. Oktober 2010, 21:35
+ *
+ * API calls for file change event api
+ */
+
+#ifndef _FCE_API_H
+#define _FCE_API_H
+
+/* fce_packet.mode */
+#define FCE_FILE_MODIFY 1
+#define FCE_FILE_DELETE 2
+#define FCE_DIR_DELETE 3
+#define FCE_FILE_CREATE 4
+#define FCE_DIR_CREATE 5
+#define FCE_TM_SIZE 6
+#define FCE_CONN_START 42
+#define FCE_CONN_BROKEN 99
+
+
+/* fce_packet.fce_magic */
+#define FCE_PACKET_MAGIC "at_fcapi"
+
+/* This packet goes over the network, so we want to
+ * be shure about datastructs and type sizes between platforms.
+ * Format is network byte order.
+ */
+#define FCE_PACKET_HEADER_SIZE 8+1+1+4+2
+struct fce_packet
+{
+ char magic[8];
+ unsigned char version;
+ unsigned char mode;
+ uint32_t event_id;
+ uint16_t datalen;
+ char data[MAXPATHLEN];
+};
+
+struct path;
+struct ofork;
+
+int fce_register_delete_file( struct path *path );
+int fce_register_delete_dir( char *name );
+int fce_register_new_dir( struct path *path );
+int fce_register_new_file( struct path *path );
+int fce_register_file_modification( struct ofork *ofork );
+int fce_register_tm_size(const char *vol, size_t used);
+
+int fce_add_udp_socket(const char *target ); // IP or IP:Port
+int fce_set_coalesce( char *coalesce_opt ); // all|delete|create
+int fce_set_events(const char *events); /* fmod,fdel,ddel,fcre,dcre,tmsz (default is all except tmsz) */
+
+#define FCE_DEFAULT_PORT 12250
+#define FCE_DEFAULT_PORT_STRING "12250"
+
+#endif /* _FCE_API_H */
+
#define STRCMP(a,b,c) (strcmp(a,c) b 0)
+#if BYTE_ORDER == BIG_ENDIAN
+#define hton64(x) (x)
+#define ntoh64(x) (x)
+#else /* BYTE_ORDER == BIG_ENDIAN */
+#define hton64(x) ((uint64_t) (htonl(((x) >> 32) & 0xffffffffLL)) | \
+ (uint64_t) ((htonl(x) & 0xffffffffLL) << 32))
+#define ntoh64(x) (hton64(x))
+#endif /* BYTE_ORDER == BIG_ENDIAN */
+
#ifdef WITH_SENDFILE
extern ssize_t sys_sendfile (int __out_fd, int __in_fd, off_t *__offset,size_t __count);
#endif
Default size is 8192, maximum size is 131072\&. Given value is rounded up to nearest power of 2\&. Each entry takes about 100 bytes, which is not much, but remember that every afpd child process for every connected user has its cache\&.
.RE
.PP
+\-fcelistener \fIhost[:port]\fR
+.RS 4
+Enables sending FCE events to the specified
+\fIhost\fR, default
+\fIport\fR
+is 12250 if not specified\&. Specifying mutliple listeners is done by having this option once for each of them\&.
+.RE
+.PP
+\-fceevents \fIfmod,fdel,ddel,fcre,dcre,tmsz\fR
+.RS 4
+Speficies which FCE events are active, default is
+\fIfmod,fdel,ddel,fcre,dcre\fR\&.
+.RE
+.PP
+\-fcecoalesce \fIall|delete|create\fR
+.RS 4
+Coalesce FCE events\&.
+.RE
+.PP
\-guestname \fI[name]\fR
.RS 4
Specifies the user that guests should use (default is "nobody")\&. The name should be quoted\&.