From 5d79aced8cc587b8b8889647b7cb9da79d500d64 Mon Sep 17 00:00:00 2001 From: Jim Ramsay Date: Thu, 11 Jan 2007 20:41:17 +0000 Subject: Added zeroconf service publishing using avahi git-svn-id: https://svn.musicpd.org/mpd/trunk@5238 09075e82-0dd4-0310-85a5-a0d7c8717e4f --- src/Makefile.am | 6 +- src/conf.c | 1 + src/conf.h | 1 + src/interface.c | 62 ++++++- src/ioops.h | 47 ++++++ src/listen.c | 8 + src/listen.h | 2 + src/main.c | 3 + src/zeroconf.c | 493 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/zeroconf.h | 27 ++++ 10 files changed, 646 insertions(+), 4 deletions(-) create mode 100644 src/ioops.h create mode 100644 src/zeroconf.c create mode 100644 src/zeroconf.h (limited to 'src') diff --git a/src/Makefile.am b/src/Makefile.am index 38b206d40..be9cd2a31 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -73,7 +73,8 @@ mpd_headers = \ utf8.h \ utils.h \ volume.h \ - localization.h + localization.h \ + zeroconf.h mpd_SOURCES = \ @@ -123,7 +124,8 @@ mpd_SOURCES = \ utils.c \ volume.c \ utf8.c \ - localization.c + localization.c \ + zeroconf.c mpd_CFLAGS = $(MPD_CFLAGS) diff --git a/src/conf.c b/src/conf.c index d354634ef..a2609710e 100644 --- a/src/conf.c +++ b/src/conf.c @@ -146,6 +146,7 @@ void initConf(void) registerConfigParam(CONF_BIND_TO_ADDRESS, 1, 0); registerConfigParam(CONF_PORT, 0, 0); registerConfigParam(CONF_LOG_LEVEL, 0, 0); + registerConfigParam(CONF_ZEROCONF_NAME, 0, 0); registerConfigParam(CONF_PASSWORD, 1, 0); registerConfigParam(CONF_DEFAULT_PERMS, 0, 0); registerConfigParam(CONF_AUDIO_OUTPUT, 1, 1); diff --git a/src/conf.h b/src/conf.h index 7d26cadf7..e2f568525 100644 --- a/src/conf.h +++ b/src/conf.h @@ -32,6 +32,7 @@ #define CONF_BIND_TO_ADDRESS "bind_to_address" #define CONF_PORT "port" #define CONF_LOG_LEVEL "log_level" +#define CONF_ZEROCONF_NAME "zeroconf_name" #define CONF_PASSWORD "password" #define CONF_DEFAULT_PERMS "default_permissions" #define CONF_AUDIO_OUTPUT "audio_output" diff --git a/src/interface.c b/src/interface.c index 4220935ed..1e6fb4445 100644 --- a/src/interface.c +++ b/src/interface.c @@ -26,6 +26,7 @@ #include "permission.h" #include "sllist.h" #include "utils.h" +#include "ioops.h" #include #include @@ -62,6 +63,9 @@ static size_t interface_max_command_list_size = static size_t interface_max_output_buffer_size = INTERFACE_MAX_OUTPUT_BUFFER_SIZE_DEFAULT; +/* List of registered external IO handlers */ +static struct ioOps *ioList; + /* maybe make conf option for this, or... 32 might be good enough */ static long int interface_list_cache_size = 32; @@ -488,6 +492,7 @@ int doIOForInterfaces(void) { fd_set rfds; fd_set wfds; + fd_set efds; struct timeval tv; int i; int selret; @@ -499,12 +504,43 @@ int doIOForInterfaces(void) while (1) { fdmax = 0; + FD_ZERO( &rfds ); + FD_ZERO( &wfds ); + FD_ZERO( &efds ); addInterfacesReadyToReadAndListenSocketToFdSet(&rfds, &fdmax); addInterfacesForBufferFlushToFdSet(&wfds, &fdmax); - selret = select(fdmax + 1, &rfds, &wfds, NULL, &tv); + // Add fds for all registered IO handlers + if( ioList ) { + struct ioOps *i = ioList; + while( i ) { + struct ioOps *current = i; + int fdnum; + assert( current->fdset ); + fdnum = current->fdset( &rfds, &wfds, &efds ); + if( fdmax < fdnum ) + fdmax = fdnum; + i = i->next; + } + } + + selret = select(fdmax + 1, &rfds, &wfds, &efds, &tv); + + if (selret < 0 && errno == EINTR) + break; + + // Consume fds for all registered IO handlers + if( ioList ) { + struct ioOps *i = ioList; + while( i ) { + struct ioOps *current = i; + assert( current->consume ); + selret = current->consume( selret, &rfds, &wfds, &efds ); + i = i->next; + } + } - if (selret == 0 || (selret < 0 && errno == EINTR)) + if (selret == 0) break; if (selret < 0) { @@ -794,3 +830,25 @@ static void printInterfaceOutBuffer(Interface * interface) interface->send_buf_used = 0; } + +// From ioops.h: +void registerIO( struct ioOps *ops ) +{ + assert( ops != NULL ); + + ops->next = ioList; + ioList = ops; + ops->prev = NULL; + if( ops->next ) + ops->next->prev = ops; +} + +void deregisterIO( struct ioOps *ops ) +{ + assert( ops != NULL ); + + if( ioList == ops ) + ioList = ops->next; + else if( ops->prev != NULL ) + ops->prev->next = ops->next; +} diff --git a/src/ioops.h b/src/ioops.h new file mode 100644 index 000000000..a2a6fa841 --- /dev/null +++ b/src/ioops.h @@ -0,0 +1,47 @@ +/* the Music Player Daemon (MPD) + * (c)2003-2006 by Warren Dukes (warren.dukes@gmail.com) + * This project's homepage is: http://www.musicpd.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 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef IOOPS_H +#define IOOPS_H + +#include + +struct ioOps { + struct ioOps *prev, *next; + + // Called before each 'select' statement. + // To register for IO, call FD_SET for each required queue + // Return the highest fd number you registered + int (*fdset) ( fd_set *rfds, fd_set *wfds, fd_set *efds ); + + // Called after each 'select' statement. + // fdCount is the number of fds total in all sets. It may be 0. + // For each fd you registered for in (fdset), you should FD_CLR it from the + // appropriate queue(s). + // Return the total number of fds left in all sets (Ie, return fdCount + // minus the number of times you called FD_CLR). + int (*consume) ( int fdCount, fd_set *rfds, fd_set *wfds, fd_set *efds ); +}; + +// Call this to register your io operation handler struct +void registerIO( struct ioOps *ops ); + +// Call this to deregister your io operation handler struct +void deregisterIO( struct ioOps *ops ); + +#endif diff --git a/src/listen.c b/src/listen.c index 7cc16a2d3..4caf2dc2a 100644 --- a/src/listen.c +++ b/src/listen.c @@ -47,6 +47,7 @@ int *listenSockets = NULL; int numberOfListenSockets = 0; +static int boundPort = 0; static int establishListen(unsigned int port, struct sockaddr *addrp, socklen_t addrlen) @@ -211,6 +212,8 @@ void listenOnPort(void) } } + boundPort = port; + do { parseListenConfigParam(port, param); } while ((param = getNextConfigParam(CONF_BIND_TO_ADDRESS, param))); @@ -266,3 +269,8 @@ void getConnections(fd_set * fds) } } } + +int getBoundPort() +{ + return boundPort; +} diff --git a/src/listen.h b/src/listen.h index dd2c7dc7e..74c5e6adc 100644 --- a/src/listen.h +++ b/src/listen.h @@ -36,4 +36,6 @@ void freeAllListenSockets(void); /* fdmax should be initialized to something */ void addListenSocketsToFdSet(fd_set * fds, int *fdmax); +int getBoundPort(void); + #endif diff --git a/src/main.c b/src/main.c index e32146e6c..1166237ec 100644 --- a/src/main.c +++ b/src/main.c @@ -42,6 +42,7 @@ #include "../config.h" #include "utils.h" #include "normalize.h" +#include "zeroconf.h" #include "localization.h" #include @@ -452,6 +453,7 @@ int main(int argc, char *argv[]) initAudioDriver(); initVolume(); initInterfaces(); + initZeroconf(); initReplayGainState(); initNormalization(); initInputStream(); @@ -477,6 +479,7 @@ int main(int argc, char *argv[]) write_state_file(); playerKill(); + finishZeroconf(); freeAllInterfaces(); closeAllListenSockets(); finishPlaylist(); diff --git a/src/zeroconf.c b/src/zeroconf.c new file mode 100644 index 000000000..0fa8d7398 --- /dev/null +++ b/src/zeroconf.c @@ -0,0 +1,493 @@ +/* the Music Player Daemon (MPD) + * (c)2003-2006 by Warren Dukes (warren.dukes@gmail.com) + * This project's homepage is: http://www.musicpd.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 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include +#include +#include + +#include "zeroconf.h" +#include "conf.h" +#include "log.h" +#include "listen.h" +#include "ioops.h" + +/* The dns-sd service type qualifier to publish */ +#define SERVICE_TYPE "_mpd._tcp" + +/* The default service name to publish + * (overridden by 'zeroconf_name' config parameter) + */ +#define SERVICE_NAME "Music Player" + +/* Here is the implementation for Avahi (http://avahi.org) Zeroconf support */ +#if HAVE_AVAHI + +#include +#include + +#include +#include +#include +#include + +/* Static avahi data */ +static AvahiEntryGroup *avahiGroup = NULL; +static char *avahiName = NULL; +static AvahiClient* avahiClient = NULL; +static AvahiPoll avahiPoll; +static int avahiRunning = 0; + +static int avahiFdset( fd_set* rfds, fd_set* wfds, fd_set* efds ); +static int avahiFdconsume( int fdCount, fd_set* rfds, fd_set* wfds, fd_set* efds ); +static struct ioOps avahiIo = { + .fdset = avahiFdset, + .consume = avahiFdconsume, +}; + +/* Forward Declaration */ +static void avahiRegisterService(AvahiClient *c); + +struct AvahiWatch { + struct AvahiWatch* prev; + struct AvahiWatch* next; + int fd; + AvahiWatchEvent requestedEvent; + AvahiWatchEvent observedEvent; + AvahiWatchCallback callback; + void* userdata; +}; + +struct AvahiTimeout { + struct AvahiTimeout* prev; + struct AvahiTimeout* next; + struct timeval expiry; + int enabled; + AvahiTimeoutCallback callback; + void* userdata; +}; + +static AvahiWatch* avahiWatchList; +static AvahiTimeout* avahiTimeoutList; + +static AvahiWatch* avahiWatchNew( const AvahiPoll *api, int fd, AvahiWatchEvent event, AvahiWatchCallback callback, void *userdata ) +{ + struct AvahiWatch* newWatch = malloc( sizeof(struct AvahiWatch) ); + + newWatch->fd = fd; + newWatch->requestedEvent = event; + newWatch->observedEvent = 0; + newWatch->callback = callback; + newWatch->userdata = userdata; + + /* Insert at front of list */ + newWatch->next = avahiWatchList; + avahiWatchList = newWatch; + newWatch->prev = NULL; + if( newWatch->next ) + newWatch->next->prev = newWatch; + + return newWatch; +} + +static void avahiWatchUpdate( AvahiWatch *w, AvahiWatchEvent event ) +{ + assert( w != NULL ); + w->requestedEvent = event; +} + +static AvahiWatchEvent avahiWatchGetEvents( AvahiWatch *w ) +{ + assert( w != NULL ); + return w->observedEvent; +} + +static void avahiWatchFree( AvahiWatch *w ) +{ + assert( w != NULL ); + + if( avahiWatchList == w ) + avahiWatchList = w->next; + else if( w->prev != NULL ) + w->prev->next = w->next; + + free( w ); +} + +static void avahiCheckExpiry( AvahiTimeout *t ) +{ + assert( t != NULL ); + if( t->enabled ) { + struct timeval now; + gettimeofday( &now, NULL ); + if( timercmp( &now, &(t->expiry), > ) ) { + t->enabled = 0; + t->callback( t, t->userdata ); + } + } +} + +static void avahiTimeoutUpdate( AvahiTimeout *t, const struct timeval *tv ) +{ + assert( t != NULL ); + if( tv ) { + t->enabled = 1; + t->expiry.tv_sec = tv->tv_sec; + t->expiry.tv_usec = tv->tv_usec; + } else { + t->enabled = 0; + } +} + +static void avahiTimeoutFree( AvahiTimeout *t ) +{ + assert( t != NULL ); + + if( avahiTimeoutList == t ) + avahiTimeoutList = t->next; + else if( t->prev != NULL ) + t->prev->next = t->next; + + free( t ); +} + +static AvahiTimeout* avahiTimeoutNew( const AvahiPoll *api, const struct timeval *tv, AvahiTimeoutCallback callback, void *userdata ) +{ + struct AvahiTimeout* newTimeout = malloc( sizeof(struct AvahiTimeout) ); + + newTimeout->callback = callback; + newTimeout->userdata = userdata; + + avahiTimeoutUpdate( newTimeout, tv ); + + /* Insert at front of list */ + newTimeout->next = avahiTimeoutList; + avahiTimeoutList = newTimeout; + newTimeout->prev = NULL; + if( newTimeout->next ) + newTimeout->next->prev = newTimeout; + + return newTimeout; +} + +/* Callback when the EntryGroup changes state */ +static void avahiGroupCallback( + AvahiEntryGroup *g, + AvahiEntryGroupState state, + void *userdata) +{ + assert(g); + + DEBUG( "Avahi: Service group changed to state %d\n", state ); + + switch (state) { + case AVAHI_ENTRY_GROUP_ESTABLISHED : + /* The entry group has been established successfully */ + LOG( "Avahi: Service '%s' successfully established.\n", avahiName ); + break; + + case AVAHI_ENTRY_GROUP_COLLISION : { + char *n; + + /* A service name collision happened. Let's pick a new name */ + n = avahi_alternative_service_name(avahiName); + avahi_free(avahiName); + avahiName = n; + + LOG( "Avahi: Service name collision, renaming service to '%s'\n", avahiName ); + + /* And recreate the services */ + avahiRegisterService(avahi_entry_group_get_client(g)); + break; + } + + case AVAHI_ENTRY_GROUP_FAILURE : + ERROR( "Avahi: Entry group failure: %s\n", + avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))) ); + /* Some kind of failure happened while we were registering our services */ + avahiRunning = 0; + break; + + case AVAHI_ENTRY_GROUP_UNCOMMITED: + DEBUG( "Avahi: Service group is UNCOMMITED\n" ); + break; + case AVAHI_ENTRY_GROUP_REGISTERING: + DEBUG( "Avahi: Service group is REGISTERING\n" ); + ; + } +} + +/* Registers a new service with avahi */ +static void avahiRegisterService(AvahiClient *c) +{ + int ret; + assert(c); + DEBUG( "Avahi: Registering service %s/%s\n", SERVICE_TYPE, avahiName ); + + /* If this is the first time we're called, let's create a new entry group */ + if (!avahiGroup) { + avahiGroup = avahi_entry_group_new(c, avahiGroupCallback, NULL); + if( !avahiGroup ) { + ERROR( "Avahi: Failed to create avahi EntryGroup: %s\n", avahi_strerror(avahi_client_errno(c)) ); + goto fail; + } + } + + /* Add the service */ + /* TODO: This currently binds to ALL interfaces. + * We could maybe add a service per actual bound interface, if that's better. */ + ret = avahi_entry_group_add_service(avahiGroup, + AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, 0, + avahiName, SERVICE_TYPE, + NULL, NULL, + getBoundPort(), + NULL); + if( ret < 0 ) { + ERROR( "Avahi: Failed to add service %s: %s\n", SERVICE_TYPE, avahi_strerror(ret) ); + goto fail; + } + + /* Tell the server to register the service group */ + ret = avahi_entry_group_commit(avahiGroup); + if( ret < 0 ) { + ERROR( "Avahi: Failed to commit service group: %s\n", avahi_strerror(ret) ); + goto fail; + } + return; + +fail: + avahiRunning = 0; +} + +/* Callback when avahi changes state */ +static void avahiClientCallback(AvahiClient *c, AvahiClientState state, void *userdata) +{ + assert(c); + + /* Called whenever the client or server state changes */ + DEBUG( "Avahi: Client changed to state %d\n", state ); + + switch (state) { + case AVAHI_CLIENT_S_RUNNING: + DEBUG( "Avahi: Client is RUNNING\n" ); + + /* The server has startup successfully and registered its host + * name on the network, so it's time to create our services */ + if (!avahiGroup) + avahiRegisterService(c); + break; + + case AVAHI_CLIENT_FAILURE: + { + int reason = avahi_client_errno(c); + if( reason == AVAHI_ERR_DISCONNECTED ) { + LOG( "Avahi: Client Disconnected, will reconnect shortly\n"); + avahi_entry_group_free( avahiGroup ); + avahiGroup = NULL; + avahi_client_free( avahiClient ); + avahiClient = NULL; + avahiClient = avahi_client_new( &avahiPoll, AVAHI_CLIENT_NO_FAIL, + avahiClientCallback, NULL, &reason ); + if( !avahiClient ) { + ERROR( "Avahi: Could not reconnect: %s\n", avahi_strerror(reason) ); + avahiRunning = 0; + } + } else { + ERROR( "Avahi: Client failure: %s (terminal)\n", avahi_strerror(reason)); + avahiRunning = 0; + } + } + break; + + case AVAHI_CLIENT_S_COLLISION: + DEBUG( "Avahi: Client is COLLISION\n" ); + /* Let's drop our registered services. When the server is back + * in AVAHI_SERVER_RUNNING state we will register them + * again with the new host name. */ + if (avahiGroup) { + DEBUG( "Avahi: Resetting group\n" ); + avahi_entry_group_reset(avahiGroup); + } + + case AVAHI_CLIENT_S_REGISTERING: + DEBUG( "Avahi: Client is REGISTERING\n" ); + /* The server records are now being established. This + * might be caused by a host name change. We need to wait + * for our own records to register until the host name is + * properly esatblished. */ + + if (avahiGroup) { + DEBUG( "Avahi: Resetting group\n" ); + avahi_entry_group_reset(avahiGroup); + } + + break; + + case AVAHI_CLIENT_CONNECTING: + DEBUG( "Avahi: Client is CONNECTING\n" ); + ; + } +} + +static int avahiFdset( fd_set* rfds, fd_set* wfds, fd_set* efds ) +{ + AvahiWatch* w; + int maxfd = -1; + if( !avahiRunning ) + return maxfd; + for( w = avahiWatchList; w != NULL; w = w->next ) { + if( w->requestedEvent & AVAHI_WATCH_IN ) { + FD_SET( w->fd, rfds ); + } + if( w->requestedEvent & AVAHI_WATCH_OUT ) { + FD_SET( w->fd, wfds ); + } + if( w->requestedEvent & AVAHI_WATCH_ERR ) { + FD_SET( w->fd, efds ); + } + if( w->requestedEvent & AVAHI_WATCH_HUP ) { + ERROR( "Avahi: No support for HUP events! (ignoring)\n" ); + } + + if( w->fd > maxfd ) + maxfd = w->fd; + } + return maxfd; +} + +static int avahiFdconsume( int fdCount, fd_set* rfds, fd_set* wfds, fd_set* efds ) +{ + int retval = fdCount; + + AvahiWatch* w = avahiWatchList; + while( w != NULL && retval > 0 ) { + AvahiWatch* current = w; + current->observedEvent = 0; + if( FD_ISSET( current->fd, rfds ) ) { + current->observedEvent |= AVAHI_WATCH_IN; + FD_CLR( current->fd, rfds ); + retval--; + } + if( FD_ISSET( current->fd, wfds ) ) { + current->observedEvent |= AVAHI_WATCH_OUT; + FD_CLR( current->fd, wfds ); + retval--; + } + if( FD_ISSET( current->fd, efds ) ) { + current->observedEvent |= AVAHI_WATCH_ERR; + FD_CLR( current->fd, efds ); + retval--; + } + + /* Advance to the next one right now, in case the callback + * removes itself + */ + w = w->next; + + if( current->observedEvent && avahiRunning ) { + current->callback( current, current->fd, + current->observedEvent, current->userdata ); + } + } + + AvahiTimeout* t = avahiTimeoutList; + while( t != NULL && avahiRunning ) { + AvahiTimeout* current = t; + + /* Advance to the next one right now, in case the callback + * removes itself + */ + t = t->next; + avahiCheckExpiry( current ); + } + + return retval; +} + +#endif // HAVE_AVAHI + +void initZeroconf(void) +{ + const char* serviceName = SERVICE_NAME; + ConfigParam *param; + + param = getConfigParam(CONF_ZEROCONF_NAME); + + if (param && strlen(param->value) > 0) + serviceName = param->value; + +#if HAVE_AVAHI + int error; + DEBUG( "Avahi: Initializing interface\n" ); + + if( avahi_is_valid_service_name( serviceName ) ) { + avahiName = avahi_strdup( serviceName ); + } else { + ERROR( "Invalid zeroconf_name \"%s\", defaulting to \"%s\" instead.\n", serviceName, SERVICE_NAME ); + avahiName = avahi_strdup( SERVICE_NAME ); + } + + avahiRunning = 1; + + avahiPoll.userdata = NULL; + avahiPoll.watch_new = avahiWatchNew; + avahiPoll.watch_update = avahiWatchUpdate; + avahiPoll.watch_get_events = avahiWatchGetEvents; + avahiPoll.watch_free = avahiWatchFree; + avahiPoll.timeout_new = avahiTimeoutNew; + avahiPoll.timeout_update = avahiTimeoutUpdate; + avahiPoll.timeout_free = avahiTimeoutFree; + + avahiClient = avahi_client_new( &avahiPoll, AVAHI_CLIENT_NO_FAIL, + avahiClientCallback, NULL, &error ); + + if( !avahiClient ) { + ERROR( "Avahi: Failed to create client: %s\n", avahi_strerror(error) ); + goto fail; + } + + avahiIo.fdset = avahiFdset; + avahiIo.consume = avahiFdconsume; + registerIO( &avahiIo ); + + return; + +fail: + finishZeroconf(); +#endif // HAVE_AVAHI +} + +void finishZeroconf(void) +{ +#if HAVE_AVAHI + DEBUG( "Avahi: Shutting down interface\n" ); + deregisterIO( &avahiIo ); + + if( avahiGroup ) { + avahi_entry_group_free( avahiGroup ); + avahiGroup = NULL; + } + + if( avahiClient ) { + avahi_client_free( avahiClient ); + avahiClient = NULL; + } + + avahi_free( avahiName ); + avahiName = NULL; +#endif // HAVE_AVAHI +} diff --git a/src/zeroconf.h b/src/zeroconf.h new file mode 100644 index 000000000..2301c0d40 --- /dev/null +++ b/src/zeroconf.h @@ -0,0 +1,27 @@ +/* the Music Player Daemon (MPD) + * (c)2003-2006 by Warren Dukes (warren.dukes@gmail.com) + * This project's homepage is: http://www.musicpd.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 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef ZEROCONF_H +#define ZEROCONF_H + +#include "../config.h" + +void initZeroconf(void); +void finishZeroconf(void); + +#endif -- cgit v1.2.3