2 * Copyright (C) 2006, 2007, 2008 OpenedHand Ltd.
4 * Author: Jorn Baayen <jorn@openedhand.com>
6 * This library is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Library General Public
8 * License as published by the Free Software Foundation; either
9 * version 2 of the License, or (at your option) any later version.
11 * This library is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Library General Public License for more details.
16 * You should have received a copy of the GNU Library General Public
17 * License along with this library; if not, write to the
18 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
19 * Boston, MA 02111-1307, USA.
23 * SECTION:gssdp-resource-browser
24 * @short_description: Class handling resource discovery.
26 * #GSSDPResourceBrowser handles resource discovery. After creating a browser
27 * and activating it, the ::resource-available and ::resource-unavailable
28 * signals will be emitted whenever the availability of a resource matching the
29 * specified discovery target changes. A discovery request is sent out
30 * automatically when activating the browser.
34 #include <libsoup/soup.h>
38 #include "gssdp-resource-browser.h"
39 #include "gssdp-client-private.h"
40 #include "gssdp-protocol.h"
41 #include "gssdp-marshal.h"
43 #define MAX_DISCOVERY_MESSAGES 3
44 #define DISCOVERY_FREQUENCY 500 /* 500 ms */
46 G_DEFINE_TYPE (GSSDPResourceBrowser,
47 gssdp_resource_browser,
50 struct _GSSDPResourceBrowserPrivate {
60 gulong message_received_id;
62 GHashTable *resources;
82 static guint signals[LAST_SIGNAL];
85 GSSDPResourceBrowser *resource_browser;
90 /* Function prototypes */
92 gssdp_resource_browser_set_client (GSSDPResourceBrowser *resource_browser,
95 message_received_cb (GSSDPClient *client,
98 _GSSDPMessageType type,
99 SoupMessageHeaders *headers,
102 resource_free (gpointer data);
104 clear_cache (GSSDPResourceBrowser *resource_browser);
106 send_discovery_request (GSSDPResourceBrowser *resource_browser);
108 discovery_timeout (gpointer data);
110 start_discovery (GSSDPResourceBrowser *resource_browser);
112 stop_discovery (GSSDPResourceBrowser *resource_browser);
115 gssdp_resource_browser_init (GSSDPResourceBrowser *resource_browser)
117 resource_browser->priv = G_TYPE_INSTANCE_GET_PRIVATE
119 GSSDP_TYPE_RESOURCE_BROWSER,
120 GSSDPResourceBrowserPrivate);
122 resource_browser->priv->mx = SSDP_DEFAULT_MX;
124 resource_browser->priv->resources =
125 g_hash_table_new_full (g_str_hash,
132 gssdp_resource_browser_get_property (GObject *object,
137 GSSDPResourceBrowser *resource_browser;
139 resource_browser = GSSDP_RESOURCE_BROWSER (object);
141 switch (property_id) {
145 gssdp_resource_browser_get_client (resource_browser));
150 gssdp_resource_browser_get_target (resource_browser));
155 gssdp_resource_browser_get_mx (resource_browser));
160 gssdp_resource_browser_get_active (resource_browser));
163 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
169 gssdp_resource_browser_set_property (GObject *object,
174 GSSDPResourceBrowser *resource_browser;
176 resource_browser = GSSDP_RESOURCE_BROWSER (object);
178 switch (property_id) {
180 gssdp_resource_browser_set_client (resource_browser,
181 g_value_get_object (value));
184 gssdp_resource_browser_set_target (resource_browser,
185 g_value_get_string (value));
188 gssdp_resource_browser_set_mx (resource_browser,
189 g_value_get_uint (value));
192 gssdp_resource_browser_set_active (resource_browser,
193 g_value_get_boolean (value));
196 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
202 gssdp_resource_browser_dispose (GObject *object)
204 GSSDPResourceBrowser *resource_browser;
206 resource_browser = GSSDP_RESOURCE_BROWSER (object);
208 if (resource_browser->priv->client) {
209 if (g_signal_handler_is_connected
210 (resource_browser->priv->client,
211 resource_browser->priv->message_received_id)) {
212 g_signal_handler_disconnect
213 (resource_browser->priv->client,
214 resource_browser->priv->message_received_id);
217 stop_discovery (resource_browser);
219 g_object_unref (resource_browser->priv->client);
220 resource_browser->priv->client = NULL;
223 clear_cache (resource_browser);
225 G_OBJECT_CLASS (gssdp_resource_browser_parent_class)->dispose (object);
229 gssdp_resource_browser_finalize (GObject *object)
231 GSSDPResourceBrowser *resource_browser;
233 resource_browser = GSSDP_RESOURCE_BROWSER (object);
235 if (resource_browser->priv->target_regex)
236 g_regex_unref (resource_browser->priv->target_regex);
238 g_free (resource_browser->priv->target);
240 g_hash_table_destroy (resource_browser->priv->resources);
242 G_OBJECT_CLASS (gssdp_resource_browser_parent_class)->finalize (object);
246 gssdp_resource_browser_class_init (GSSDPResourceBrowserClass *klass)
248 GObjectClass *object_class;
250 object_class = G_OBJECT_CLASS (klass);
252 object_class->set_property = gssdp_resource_browser_set_property;
253 object_class->get_property = gssdp_resource_browser_get_property;
254 object_class->dispose = gssdp_resource_browser_dispose;
255 object_class->finalize = gssdp_resource_browser_finalize;
257 g_type_class_add_private (klass, sizeof (GSSDPResourceBrowserPrivate));
260 * GSSDPResourceBrowser:client
262 * The #GSSDPClient to use.
264 g_object_class_install_property
270 "The associated client.",
272 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
273 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
274 G_PARAM_STATIC_BLURB));
277 * GSSDPResourceBrowser:target
279 * The discovery target.
281 g_object_class_install_property
287 "The discovery target.",
290 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
291 G_PARAM_STATIC_BLURB));
294 * GSSDPResourceBrowser:mx
296 * The maximum number of seconds in which to request other parties
299 g_object_class_install_property
305 "The maximum number of seconds in which to request "
306 "other parties to respond.",
311 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
312 G_PARAM_STATIC_BLURB));
315 * GSSDPResourceBrowser:active
317 * Whether this browser is active or not.
319 g_object_class_install_property
325 "TRUE if the resource browser is active.",
328 G_PARAM_STATIC_NAME | G_PARAM_STATIC_NICK |
329 G_PARAM_STATIC_BLURB));
332 * GSSDPResourceBrowser::resource-available
333 * @resource_browser: The #GSSDPResourceBrowser that received the
335 * @usn: The USN of the discovered resource
336 * @locations: (type GList*) (transfer none) (element-type utf8): A #GList of strings describing the locations of the
337 * discovered resource.
339 * The ::resource-available signal is emitted whenever a new resource
340 * has become available.
342 signals[RESOURCE_AVAILABLE] =
343 g_signal_new ("resource-available",
344 GSSDP_TYPE_RESOURCE_BROWSER,
346 G_STRUCT_OFFSET (GSSDPResourceBrowserClass,
349 gssdp_marshal_VOID__STRING_POINTER,
356 * GSSDPResourceBrowser::resource-unavailable
357 * @resource_browser: The #GSSDPResourceBrowser that received the
359 * @usn: The USN of the resource
361 * The ::resource-unavailable signal is emitted whenever a resource
362 * is not available any more.
364 signals[RESOURCE_UNAVAILABLE] =
365 g_signal_new ("resource-unavailable",
366 GSSDP_TYPE_RESOURCE_BROWSER,
368 G_STRUCT_OFFSET (GSSDPResourceBrowserClass,
369 resource_unavailable),
371 gssdp_marshal_VOID__STRING,
378 * gssdp_resource_browser_new
379 * @client: The #GSSDPClient to associate with
381 * Return value: A new #GSSDPResourceBrowser object.
383 GSSDPResourceBrowser *
384 gssdp_resource_browser_new (GSSDPClient *client,
387 return g_object_new (GSSDP_TYPE_RESOURCE_BROWSER,
394 * Sets the #GSSDPClient @resource_browser is associated with to @client
397 gssdp_resource_browser_set_client (GSSDPResourceBrowser *resource_browser,
400 g_return_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser));
401 g_return_if_fail (GSSDP_IS_CLIENT (client));
403 resource_browser->priv->client = g_object_ref (client);
405 resource_browser->priv->message_received_id =
406 g_signal_connect_object (resource_browser->priv->client,
408 G_CALLBACK (message_received_cb),
412 g_object_notify (G_OBJECT (resource_browser), "client");
416 * gssdp_resource_browser_get_client
417 * @resource_browser: A #GSSDPResourceBrowser
419 * Returns: (transfer none): The #GSSDPClient @resource_browser is associated with.
422 gssdp_resource_browser_get_client (GSSDPResourceBrowser *resource_browser)
424 g_return_val_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser),
427 return resource_browser->priv->client;
431 * gssdp_resource_browser_set_target
432 * @resource_browser: A #GSSDPResourceBrowser
433 * @target: The browser target
435 * Sets the browser target of @resource_browser to @target.
438 gssdp_resource_browser_set_target (GSSDPResourceBrowser *resource_browser,
443 char *version_pattern;
446 g_return_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser));
447 g_return_if_fail (target != NULL);
448 g_return_if_fail (!resource_browser->priv->active);
450 g_free (resource_browser->priv->target);
451 resource_browser->priv->target = g_strdup (target);
453 if (resource_browser->priv->target_regex)
454 g_regex_unref (resource_browser->priv->target_regex);
456 version_pattern = "[0-9]+";
457 /* Make sure we have enough room for version pattern */
458 pattern = g_strndup (target,
459 strlen (target) + strlen (version_pattern));
461 version = g_strrstr (pattern, ":");
462 if (version != NULL &&
463 (g_strstr_len (pattern, -1, "uuid:") != pattern ||
464 version != g_strstr_len (pattern, -1, ":")) &&
465 g_regex_match_simple (version_pattern,
467 G_REGEX_MATCH_ANCHORED,
469 strcpy (version + 1, version_pattern);
473 resource_browser->priv->target_regex = g_regex_new (pattern,
478 g_warning ("Error compiling regular expression '%s': %s",
482 g_error_free (error);
486 g_object_notify (G_OBJECT (resource_browser), "target");
490 * gssdp_resource_browser_get_target
491 * @resource_browser: A #GSSDPResourceBrowser
493 * Return value: The browser target.
496 gssdp_resource_browser_get_target (GSSDPResourceBrowser *resource_browser)
498 g_return_val_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser),
501 return resource_browser->priv->target;
505 * gssdp_resource_browser_set_mx
506 * @resource_browser: A #GSSDPResourceBrowser
507 * @mx: The to be used MX value
509 * Sets the used MX value of @resource_browser to @mx.
512 gssdp_resource_browser_set_mx (GSSDPResourceBrowser *resource_browser,
515 g_return_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser));
517 if (resource_browser->priv->mx == mx)
520 resource_browser->priv->mx = mx;
522 g_object_notify (G_OBJECT (resource_browser), "mx");
526 * gssdp_resource_browser_get_mx
527 * @resource_browser: A #GSSDPResourceBrowser
529 * Return value: The used MX value.
532 gssdp_resource_browser_get_mx (GSSDPResourceBrowser *resource_browser)
534 g_return_val_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser), 0);
536 return resource_browser->priv->mx;
540 * gssdp_resource_browser_set_active
541 * @resource_browser: A #GSSDPResourceBrowser
542 * @active: TRUE to activate @resource_browser
544 * (De)activates @resource_browser.
547 gssdp_resource_browser_set_active (GSSDPResourceBrowser *resource_browser,
550 g_return_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser));
552 if (resource_browser->priv->active == active)
555 resource_browser->priv->active = active;
558 start_discovery (resource_browser);
560 stop_discovery (resource_browser);
562 clear_cache (resource_browser);
565 g_object_notify (G_OBJECT (resource_browser), "active");
569 * gssdp_resource_browser_get_active
570 * @resource_browser: A #GSSDPResourceBrowser
572 * Return value: TRUE if @resource_browser is active.
575 gssdp_resource_browser_get_active (GSSDPResourceBrowser *resource_browser)
577 g_return_val_if_fail (GSSDP_IS_RESOURCE_BROWSER (resource_browser), 0);
579 return resource_browser->priv->active;
583 * Resource expired: Remove
586 resource_expire (gpointer user_data)
588 GSSDPResourceBrowser *resource_browser;
592 resource = user_data;
593 resource_browser = resource->resource_browser;
595 /* Steal the USN pointer from the resource as we need it for the signal
599 resource->usn = NULL;
601 g_hash_table_remove (resource->resource_browser->priv->resources, usn);
603 g_signal_emit (resource_browser,
604 signals[RESOURCE_UNAVAILABLE],
613 resource_available (GSSDPResourceBrowser *resource_browser,
614 SoupMessageHeaders *headers)
623 usn = soup_message_headers_get_one (headers, "USN");
625 return; /* No USN specified */
627 /* Get from cache, if possible */
628 resource = g_hash_table_lookup (resource_browser->priv->resources, usn);
630 /* Remove old timeout */
631 g_source_destroy (resource->timeout_src);
635 /* Create new Resource data structure */
636 resource = g_slice_new (Resource);
638 resource->resource_browser = resource_browser;
639 resource->usn = g_strdup (usn);
641 g_hash_table_insert (resource_browser->priv->resources,
648 /* Calculate new timeout */
649 header = soup_message_headers_get_one (headers, "Cache-Control");
656 for (list = soup_header_parse_list (header);
659 res = sscanf (list->data,
667 g_warning ("Invalid 'Cache-Control' header. Assuming "
668 "default max-age of %d.\n"
670 SSDP_DEFAULT_MAX_AGE,
673 timeout = SSDP_DEFAULT_MAX_AGE;
676 soup_header_free_list (list);
680 expires = soup_message_headers_get_one (headers, "Expires");
682 SoupDate *soup_exp_time;
683 time_t exp_time, cur_time;
685 soup_exp_time = soup_date_new_from_string (expires);
686 exp_time = soup_date_to_time_t (soup_exp_time);
687 soup_date_free (soup_exp_time);
689 cur_time = time (NULL);
691 if (exp_time > cur_time)
692 timeout = exp_time - cur_time;
694 g_warning ("Invalid 'Expires' header. Assuming "
695 "default max-age of %d.\n"
697 SSDP_DEFAULT_MAX_AGE,
700 timeout = SSDP_DEFAULT_MAX_AGE;
703 g_warning ("No 'Cache-Control' nor any 'Expires' "
704 "header was specified. Assuming default "
705 "max-age of %d.", SSDP_DEFAULT_MAX_AGE);
707 timeout = SSDP_DEFAULT_MAX_AGE;
711 resource->timeout_src = g_timeout_source_new_seconds (timeout);
712 g_source_set_callback (resource->timeout_src,
716 g_source_attach (resource->timeout_src,
717 g_main_context_get_thread_default ());
719 g_source_unref (resource->timeout_src);
721 /* Only continue with signal emission if this resource was not
726 /* Build list of locations */
729 header = soup_message_headers_get_one (headers, "Location");
731 locations = g_list_append (locations, g_strdup (header));
733 header = soup_message_headers_get_one (headers, "AL");
735 /* Parse AL header. The format is:
737 const char *start, *end;
741 while ((start = strchr (start, '<'))) {
743 if (!start || !*start)
746 end = strchr (start, '>');
750 uri = g_strndup (start, end - start);
751 locations = g_list_append (locations, uri);
758 g_signal_emit (resource_browser,
759 signals[RESOURCE_AVAILABLE],
766 g_free (locations->data);
768 locations = g_list_delete_link (locations, locations);
773 resource_unavailable (GSSDPResourceBrowser *resource_browser,
774 SoupMessageHeaders *headers)
778 usn = soup_message_headers_get_one (headers, "USN");
780 return; /* No USN specified */
782 /* Only process if we were cached */
783 if (!g_hash_table_lookup (resource_browser->priv->resources, usn))
786 g_hash_table_remove (resource_browser->priv->resources, usn);
788 g_signal_emit (resource_browser,
789 signals[RESOURCE_UNAVAILABLE],
795 check_target_compat (GSSDPResourceBrowser *resource_browser,
798 return strcmp (resource_browser->priv->target,
799 GSSDP_ALL_RESOURCES) == 0 ||
800 g_regex_match (resource_browser->priv->target_regex,
807 received_discovery_response (GSSDPResourceBrowser *resource_browser,
808 SoupMessageHeaders *headers)
812 st = soup_message_headers_get_one (headers, "ST");
814 return; /* No target specified */
816 if (!check_target_compat (resource_browser, st))
817 return; /* Target doesn't match */
819 resource_available (resource_browser, headers);
823 received_announcement (GSSDPResourceBrowser *resource_browser,
824 SoupMessageHeaders *headers)
828 header = soup_message_headers_get_one (headers, "NT");
830 return; /* No target specified */
832 if (!check_target_compat (resource_browser, header))
833 return; /* Target doesn't match */
835 header = soup_message_headers_get_one (headers, "NTS");
837 return; /* No announcement type specified */
839 /* Check announcement type */
842 strlen (SSDP_ALIVE_NTS)) == 0)
843 resource_available (resource_browser, headers);
844 else if (strncmp (header,
846 strlen (SSDP_BYEBYE_NTS)) == 0)
847 resource_unavailable (resource_browser, headers);
854 message_received_cb (GSSDPClient *client,
857 _GSSDPMessageType type,
858 SoupMessageHeaders *headers,
861 GSSDPResourceBrowser *resource_browser;
863 resource_browser = GSSDP_RESOURCE_BROWSER (user_data);
865 if (!resource_browser->priv->active)
869 case _GSSDP_DISCOVERY_RESPONSE:
870 received_discovery_response (resource_browser, headers);
872 case _GSSDP_ANNOUNCEMENT:
873 received_announcement (resource_browser, headers);
881 * Free a Resource structure and its contained data
884 resource_free (gpointer data)
890 g_free (resource->usn);
892 g_source_destroy (resource->timeout_src);
894 g_slice_free (Resource, resource);
898 clear_cache_helper (gpointer key, gpointer value, gpointer data)
904 g_signal_emit (resource->resource_browser,
905 signals[RESOURCE_UNAVAILABLE],
913 * Clears the cached resources hash
916 clear_cache (GSSDPResourceBrowser *resource_browser)
919 g_hash_table_foreach_remove (resource_browser->priv->resources,
924 /* Sends discovery request */
926 send_discovery_request (GSSDPResourceBrowser *resource_browser)
930 message = g_strdup_printf (SSDP_DISCOVERY_REQUEST,
931 resource_browser->priv->target,
932 resource_browser->priv->mx,
933 g_get_application_name () ?: "");
935 _gssdp_client_send_message (resource_browser->priv->client,
939 _GSSDP_DISCOVERY_REQUEST);
945 discovery_timeout (gpointer data)
947 GSSDPResourceBrowser *resource_browser;
949 resource_browser = GSSDP_RESOURCE_BROWSER (data);
951 send_discovery_request (resource_browser);
953 resource_browser->priv->num_discovery += 1;
955 if (resource_browser->priv->num_discovery >= MAX_DISCOVERY_MESSAGES) {
956 resource_browser->priv->timeout_src = NULL;
957 resource_browser->priv->num_discovery = 0;
964 /* Starts sending discovery requests */
966 start_discovery (GSSDPResourceBrowser *resource_browser)
969 send_discovery_request (resource_browser);
971 /* And schedule the rest for later */
972 resource_browser->priv->num_discovery = 1;
973 resource_browser->priv->timeout_src =
974 g_timeout_source_new (DISCOVERY_FREQUENCY);
975 g_source_set_callback (resource_browser->priv->timeout_src,
977 resource_browser, NULL);
979 g_source_attach (resource_browser->priv->timeout_src,
980 g_main_context_get_thread_default ());
982 g_source_unref (resource_browser->priv->timeout_src);
985 /* Stops the sending of discovery messages */
987 stop_discovery (GSSDPResourceBrowser *resource_browser)
989 if (resource_browser->priv->timeout_src) {
990 g_source_destroy (resource_browser->priv->timeout_src);
991 resource_browser->priv->timeout_src = NULL;
992 resource_browser->priv->num_discovery = 0;