[cosmetics] update date in GPL header
[vuplus_xbmc] / xbmc / network / linux / ZeroconfBrowserAvahi.cpp
1 /*
2  *      Copyright (C) 2005-2013 Team XBMC
3  *      http://www.xbmc.org
4  *
5  *  This Program is free software; you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation; either version 2, or (at your option)
8  *  any later version.
9  *
10  *  This Program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with XBMC; see the file COPYING.  If not, see
17  *  <http://www.gnu.org/licenses/>.
18  *
19  */
20
21 #include "ZeroconfBrowserAvahi.h"
22
23 #ifdef HAS_AVAHI
24
25 #include <utils/log.h>
26 #include "guilib/GUIWindowManager.h"
27 #include "guilib/GUIMessage.h"
28 #include "GUIUserMessages.h"
29 #include <avahi-common/malloc.h>
30 #include <avahi-common/error.h>
31 namespace
32 {
33 ///helper RAII-struct to block event loop for modifications
34 struct ScopedEventLoopBlock
35 {
36   ScopedEventLoopBlock ( AvahiThreadedPoll* fp_poll ) : mp_poll ( fp_poll )
37   {
38     avahi_threaded_poll_lock ( mp_poll );
39   }
40
41   ~ScopedEventLoopBlock()
42   {
43     avahi_threaded_poll_unlock ( mp_poll );
44   }
45 private:
46   AvahiThreadedPoll* mp_poll;
47 };
48 }
49
50 CZeroconfBrowserAvahi::CZeroconfBrowserAvahi() : mp_client ( 0 ), mp_poll ( 0 ), m_shutdown(false), m_thread_id(0)
51 {
52   if ( ! ( mp_poll = avahi_threaded_poll_new() ) )
53   {
54     CLog::Log ( LOGERROR, "CZeroconfAvahi::CZeroconfAvahi(): Could not create threaded poll object" );
55     //TODO: throw exception? can this even happen?
56     return;
57   }
58
59   if ( !createClient() )
60   {
61     CLog::Log ( LOGERROR, "CZeroconfAvahi::CZeroconfAvahi(): Could not create client" );
62     //yeah, what if not? but should always succeed (as client_no_fail or something is passed)
63   }
64
65   //start event loop thread
66   if ( avahi_threaded_poll_start ( mp_poll ) < 0 )
67   {
68     CLog::Log ( LOGERROR, "CZeroconfAvahi::CZeroconfAvahi(): Failed to start avahi client thread" );
69   }
70 }
71
72 CZeroconfBrowserAvahi::~CZeroconfBrowserAvahi()
73 {
74   CLog::Log ( LOGDEBUG, "CZeroconfAvahi::~CZeroconfAvahi() Going down! cleaning up..." );
75   if ( mp_poll )
76   {
77     //normally we would stop the avahi thread here and do our work, but
78     //it looks like this does not work -> www.avahi.org/ticket/251
79     //so instead of calling
80     //avahi_threaded_poll_stop(mp_poll);
81     //we set m_shutdown=true, post an event and wait for it to stop itself
82     struct timeval tv = { 0, 0 }; //TODO: does tv survive the thread?
83     AvahiTimeout* lp_timeout;
84     {
85       ScopedEventLoopBlock l_block(mp_poll);
86       const AvahiPoll* cp_apoll = avahi_threaded_poll_get(mp_poll);
87       m_shutdown = true;
88       lp_timeout = cp_apoll->timeout_new(cp_apoll,
89                                          &tv,
90                                          shutdownCallback,
91                                          this);
92     }
93
94     //now wait for the thread to stop
95     assert(m_thread_id);
96     pthread_join(m_thread_id, NULL);
97     avahi_threaded_poll_get(mp_poll)->timeout_free(lp_timeout);
98   }
99   //free the client (frees all browsers, groups, ...)
100   if ( mp_client )
101     avahi_client_free ( mp_client );
102   if ( mp_poll )
103     avahi_threaded_poll_free ( mp_poll );
104 }
105
106 bool CZeroconfBrowserAvahi::doAddServiceType ( const CStdString& fcr_service_type )
107 {
108   ScopedEventLoopBlock lock ( mp_poll );
109   tBrowserMap::iterator it = m_browsers.find ( fcr_service_type );
110   if ( it != m_browsers.end() )
111     return false;
112   else
113     it = m_browsers.insert ( std::make_pair ( fcr_service_type, ( AvahiServiceBrowser* ) 0 ) ).first;
114
115   //if the client is running, we directly create a browser for the service here
116   if ( mp_client  && avahi_client_get_state ( mp_client ) ==  AVAHI_CLIENT_S_RUNNING )
117   {
118     AvahiServiceBrowser* browser = createServiceBrowser ( fcr_service_type, mp_client, this);
119     if ( !browser )
120     {
121       m_browsers.erase ( it );
122       return false;
123     }
124     else
125     {
126       it->second = browser;
127       return true;
128     }
129   }
130   else
131   {
132     CLog::Log ( LOGINFO, "CZeroconfBrowserAvahi::doAddServiceType client not available. service browsing queued" );
133     return true;
134   }
135 }
136
137 bool CZeroconfBrowserAvahi::doRemoveServiceType ( const CStdString& fcr_service_type )
138 {
139   ScopedEventLoopBlock lock ( mp_poll );
140   tBrowserMap::iterator it = m_browsers.find ( fcr_service_type );
141   if ( it == m_browsers.end() )
142     return false;
143   else
144   {
145     if ( it->second )
146     {
147       avahi_service_browser_free ( it->second );
148       m_all_for_now_browsers.erase ( it->second );
149     }
150     m_browsers.erase ( it );
151     //remove this serviceType from the list of discovered services
152     for ( tDiscoveredServices::iterator it = m_discovered_services.begin(); it != m_discovered_services.end(); ++it )
153       if ( it->first.GetType() == fcr_service_type )
154         m_discovered_services.erase ( it++ );
155   }
156   return true;
157 }
158
159 std::vector<CZeroconfBrowser::ZeroconfService> CZeroconfBrowserAvahi::doGetFoundServices()
160 {
161   std::vector<CZeroconfBrowser::ZeroconfService> ret;
162   ScopedEventLoopBlock lock ( mp_poll );
163   ret.reserve ( m_discovered_services.size() );
164   for ( tDiscoveredServices::iterator it = m_discovered_services.begin(); it != m_discovered_services.end(); ++it )
165     ret.push_back ( it->first );
166   return ret;
167 }
168
169 bool CZeroconfBrowserAvahi::doResolveService ( CZeroconfBrowser::ZeroconfService& fr_service, double f_timeout )
170 {
171   {
172     //wait for lock on event-loop to schedule resolving
173     ScopedEventLoopBlock lock ( mp_poll );
174     //avahi can only resolve already discovered services, as it needs info from there
175     tDiscoveredServices::const_iterator it = m_discovered_services.find( fr_service );
176     if ( it == m_discovered_services.end() )
177     {
178       CLog::Log ( LOGERROR, "CZeroconfBrowserAvahi::doResolveService called with undiscovered service, resolving is NOT possible" );
179       return false;
180     }
181     //start resolving
182     m_resolving_service = fr_service;
183     m_resolved_event.Reset();
184     if ( !avahi_service_resolver_new ( mp_client, it->second.interface, it->second.protocol,
185       it->first.GetName().c_str(), it->first.GetType().c_str(), it->first.GetDomain().c_str(),
186                                        AVAHI_PROTO_UNSPEC, AvahiLookupFlags ( 0 ), resolveCallback, this ) )
187     {
188       CLog::Log ( LOGERROR, "CZeroconfBrowserAvahi::doResolveService Failed to resolve service '%s': %s\n", it->first.GetName().c_str(),
189                   avahi_strerror ( avahi_client_errno ( mp_client ) ) );
190       return false;
191     }
192   } // end of this block releases lock of eventloop
193
194   //wait for resolve to return or timeout
195   m_resolved_event.WaitMSec(f_timeout*1000);
196   {
197     ScopedEventLoopBlock lock ( mp_poll );
198     fr_service = m_resolving_service;
199     return (!fr_service.GetIP().empty());
200   }
201 }
202
203 void CZeroconfBrowserAvahi::clientCallback ( AvahiClient* fp_client, AvahiClientState f_state, void* fp_data )
204 {
205   CZeroconfBrowserAvahi* p_instance = static_cast<CZeroconfBrowserAvahi*> ( fp_data );
206
207   //store our thread ID and check for shutdown -> check details in destructor
208   p_instance->m_thread_id = pthread_self();
209   if (p_instance->m_shutdown)
210   {
211     avahi_threaded_poll_quit(p_instance->mp_poll);
212     return;
213   }
214
215   switch ( f_state )
216   {
217     case AVAHI_CLIENT_S_RUNNING:
218       {
219         CLog::Log ( LOGDEBUG, "CZeroconfBrowserAvahi::clientCallback: client is up and running" );
220         for ( tBrowserMap::iterator it = p_instance->m_browsers.begin(); it != p_instance->m_browsers.end(); ++it )
221         {
222           assert ( !it->second );
223           it->second = createServiceBrowser ( it->first, fp_client, fp_data );
224         }
225         break;
226       }
227     case AVAHI_CLIENT_FAILURE:
228     {
229       CLog::Log ( LOGINFO, "CZeroconfBrowserAvahi::clientCallback: client failure. avahi-daemon stopped? Recreating client..." );
230       //We were forced to disconnect from server. now free and recreate the client object
231       avahi_client_free ( fp_client );
232       p_instance->mp_client = 0;
233       //freeing the client also frees all groups and browsers, pointers are undefined afterwards, so fix that now
234       for ( tBrowserMap::iterator it = p_instance->m_browsers.begin(); it != p_instance->m_browsers.end(); ++it )
235         it->second = ( AvahiServiceBrowser* ) 0;
236       //clean the list of discovered services and update gui (if someone is interested)
237       p_instance->m_discovered_services.clear();
238       CGUIMessage message ( GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH );
239       message.SetStringParam ( "zeroconf://" );
240       g_windowManager.SendThreadMessage ( message );
241       p_instance->createClient();
242       break;
243     }
244     case AVAHI_CLIENT_S_COLLISION:
245     case AVAHI_CLIENT_S_REGISTERING:
246       //HERE WE SHOULD REMOVE ALL OF OUR SERVICES AND "RESCHEDULE" them for later addition
247       CLog::Log ( LOGDEBUG, "CZeroconfBrowserAvahi::clientCallback: This should not happen" );
248       break;
249
250     case AVAHI_CLIENT_CONNECTING:
251       CLog::Log ( LOGINFO, "CZeroconfBrowserAvahi::clientCallback: avahi server not available. But may become later..." );
252       break;
253   }
254 }
255 void CZeroconfBrowserAvahi::browseCallback (
256   AvahiServiceBrowser *browser, AvahiIfIndex interface, AvahiProtocol protocol, AvahiBrowserEvent event,
257   const char *name, const char *type, const char *domain,
258   AvahiLookupResultFlags flags, void* fp_data )
259 {
260   CZeroconfBrowserAvahi* p_instance = static_cast<CZeroconfBrowserAvahi*> ( fp_data );
261   assert ( browser );
262   bool update_gui = false;
263   /* Called whenever a new services becomes available on the LAN or is removed from the LAN */
264   switch ( event )
265   {
266     case AVAHI_BROWSER_FAILURE:
267       CLog::Log ( LOGERROR, "CZeroconfBrowserAvahi::browseCallback error: %s\n", avahi_strerror ( avahi_client_errno ( avahi_service_browser_get_client ( browser ) ) ) );
268       //TODO
269       return;
270     case AVAHI_BROWSER_NEW:
271       {
272         CLog::Log ( LOGDEBUG, "CZeroconfBrowserAvahi::browseCallback NEW: service '%s' of type '%s' in domain '%s'\n", name, type, domain );
273         //store the service
274         ZeroconfService service(name, type, domain);
275         AvahiSpecificInfos info;
276         info.interface = interface;
277         info.protocol = protocol;
278         p_instance->m_discovered_services.insert ( std::make_pair ( service, info ) );
279         //if this browser already sent the all for now message, we need to update the gui now
280         if( p_instance->m_all_for_now_browsers.find(browser) != p_instance->m_all_for_now_browsers.end() )
281           update_gui = true;
282         break;
283       }
284     case AVAHI_BROWSER_REMOVE:
285       {
286         //remove the service
287         ZeroconfService service(name, type, domain);
288         p_instance->m_discovered_services.erase ( service );
289         CLog::Log ( LOGDEBUG, "CZeroconfBrowserAvahi::browseCallback REMOVE: service '%s' of type '%s' in domain '%s'\n", name, type, domain );
290         //if this browser already sent the all for now message, we need to update the gui now
291         if( p_instance->m_all_for_now_browsers.find(browser) != p_instance->m_all_for_now_browsers.end() )
292           update_gui = true;
293         break;
294       }
295     case AVAHI_BROWSER_CACHE_EXHAUSTED:
296       //do we need that?
297       break;
298     case AVAHI_BROWSER_ALL_FOR_NOW:
299       CLog::Log ( LOGDEBUG, "CZeroconfBrowserAvahi::browseCallback all for now (service = %s)", type);
300       //if this browser already sent the all for now message, we need to update the gui now
301       bool success = p_instance->m_all_for_now_browsers.insert(browser).second;
302       if(!success)
303         CLog::Log ( LOGDEBUG, "CZeroconfBrowserAvahi::browseCallback AVAHI_BROWSER_ALL_FOR_NOW sent twice?!");
304       update_gui = true;
305       break;
306   }
307   if ( update_gui )
308   {
309     CGUIMessage message ( GUI_MSG_NOTIFY_ALL, 0, 0, GUI_MSG_UPDATE_PATH );
310     message.SetStringParam ( "zeroconf://" );
311     g_windowManager.SendThreadMessage ( message );
312     CLog::Log ( LOGDEBUG, "CZeroconfBrowserAvahi::browseCallback sent gui update for path zeroconf://" );
313   }
314 }
315
316 CZeroconfBrowser::ZeroconfService::tTxtRecordMap GetTxtRecords(AvahiStringList *txt)
317 {
318   AvahiStringList *i = NULL;
319   CZeroconfBrowser::ZeroconfService::tTxtRecordMap recordMap;
320   
321   for( i = txt; i; i = i->next )
322   {
323     char *key, *value;
324
325     if( avahi_string_list_get_pair( i, &key, &value, NULL ) < 0 )
326       continue;
327
328     recordMap.insert(
329       std::make_pair(
330         CStdString(key),
331         CStdString(value)
332       )
333     );
334     
335     if( key )
336       avahi_free(key);
337     if( value )
338       avahi_free(value);
339   }
340   return recordMap;
341 }
342
343 void CZeroconfBrowserAvahi::resolveCallback(
344   AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol, AvahiResolverEvent event,
345   const char *name, const char *type, const char *domain, const char *host_name,
346   const AvahiAddress *address, uint16_t port, AvahiStringList *txt, AvahiLookupResultFlags flags, void* userdata )
347 {
348   assert ( r );
349   assert ( userdata );
350   CZeroconfBrowserAvahi* p_instance = static_cast<CZeroconfBrowserAvahi*> ( userdata );
351   switch ( event )
352   {
353     case AVAHI_RESOLVER_FAILURE:
354       CLog::Log ( LOGERROR, "CZeroconfBrowserAvahi::resolveCallback Failed to resolve service '%s' of type '%s' in domain '%s': %s\n", name, type, domain, avahi_strerror ( avahi_client_errno ( avahi_service_resolver_get_client ( r ) ) ) );
355       break;
356     case AVAHI_RESOLVER_FOUND:
357     {
358       char a[AVAHI_ADDRESS_STR_MAX];
359       CLog::Log ( LOGDEBUG, "CZeroconfBrowserAvahi::resolveCallback resolved service '%s' of type '%s' in domain '%s':\n", name, type, domain );
360
361       avahi_address_snprint ( a, sizeof ( a ), address );
362       p_instance->m_resolving_service.SetIP(a);
363       p_instance->m_resolving_service.SetPort(port);
364       //get txt-record list
365       p_instance->m_resolving_service.SetTxtRecords(GetTxtRecords(txt));
366       break;
367     }
368   }
369   avahi_service_resolver_free ( r );
370   p_instance->m_resolved_event.Set();
371 }
372
373 bool CZeroconfBrowserAvahi::createClient()
374 {
375   assert ( mp_poll );
376   if ( mp_client )
377   {
378     avahi_client_free ( mp_client );
379   }
380   mp_client = avahi_client_new ( avahi_threaded_poll_get ( mp_poll ),
381                                  AVAHI_CLIENT_NO_FAIL, &clientCallback, this, 0 );
382   if ( !mp_client )
383   {
384     mp_client = 0;
385     return false;
386   }
387   return true;
388 }
389
390 AvahiServiceBrowser* CZeroconfBrowserAvahi::createServiceBrowser ( const CStdString& fcr_service_type, AvahiClient* fp_client, void* fp_userdata)
391 {
392   assert(fp_client);
393   AvahiServiceBrowser* ret = avahi_service_browser_new ( fp_client, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, fcr_service_type.c_str(),
394                                                          NULL, ( AvahiLookupFlags ) 0, browseCallback, fp_userdata );
395   if ( !ret )
396   {
397     CLog::Log ( LOGERROR, "CZeroconfBrowserAvahi::createServiceBrowser Failed to create service (%s) browser: %s",
398                 avahi_strerror ( avahi_client_errno ( fp_client ) ), fcr_service_type.c_str() );
399   }
400   return ret;
401 }
402
403
404 void CZeroconfBrowserAvahi::shutdownCallback(AvahiTimeout *fp_e, void *fp_data)
405 {
406   CZeroconfBrowserAvahi* p_instance = static_cast<CZeroconfBrowserAvahi*>(fp_data);
407   //should only be called on shutdown
408   if (p_instance->m_shutdown)
409   {
410     avahi_threaded_poll_quit(p_instance->mp_poll);
411   }
412 }
413
414 #endif //HAS_AVAHI