From ae8bd98df2121996fd1074d5ad523a70bfdf19e2 Mon Sep 17 00:00:00 2001 From: "Maciej S. Szmigiero" Date: Sat, 18 Jun 2022 22:31:24 +0200 Subject: nmea-source: Fix a few issues * Try to reconnect a broken Avahi client each time the source is started so a restarted or late-started Avahi daemon won't cause this source to stop working, * Try to reconnect to a network NMEA service if its connection drops for any reason (they are often provided by mobile phones which are known for having aggressive network connection management to save power), * Don't add duplicate Avahi services to the service candidates list, * Fix a few memory leaks and random bugs. Tested under Valgrind. --- src/gclue-nmea-source.c | 555 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 416 insertions(+), 139 deletions(-) (limited to 'src') diff --git a/src/gclue-nmea-source.c b/src/gclue-nmea-source.c index 0172c9b..ea267f7 100644 --- a/src/gclue-nmea-source.c +++ b/src/gclue-nmea-source.c @@ -39,21 +39,35 @@ #include #include +/* Once we run out of NMEA services to try how long to wait + * until retrying all of them. + * In seconds. + */ +#define SERVICE_UNBREAK_TIME 5 + typedef struct AvahiServiceInfo AvahiServiceInfo; struct _GClueNMEASourcePrivate { GSocketConnection *connection; + GDataInputStream *input_stream; GSocketClient *client; GCancellable *cancellable; + AvahiGLibPoll *glib_poll; + AvahiClient *avahi_client; AvahiServiceInfo *active_service; - /* List of all services but only the most accurate one is used. */ - GList *all_services; + /* List of services to try but only the most accurate one is used. */ + GList *try_services; + + /* List of known-broken services. */ + GList *broken_services; + + guint accuracy_refresh_source, unbreak_timer; }; G_DEFINE_TYPE_WITH_CODE (GClueNMEASource, @@ -67,16 +81,15 @@ static GClueLocationSourceStopResult gclue_nmea_source_stop (GClueLocationSource *source); static void -connect_to_service (GClueNMEASource *source); -static void -disconnect_from_service (GClueNMEASource *source); +try_connect_to_service (GClueNMEASource *source); struct AvahiServiceInfo { char *identifier; char *host_name; + gboolean is_socket; guint16 port; GClueAccuracyLevel accuracy; - guint64 timestamp; + gint64 timestamp_add; }; static void @@ -101,7 +114,7 @@ avahi_service_new (const char *identifier, service->host_name = g_strdup (host_name); service->port = port; service->accuracy = accuracy; - service->timestamp = g_get_real_time () / G_USEC_PER_SEC; + service->timestamp_add = g_get_monotonic_time (); return service; } @@ -124,16 +137,41 @@ compare_avahi_service_by_accuracy_n_time (gconstpointer a, { AvahiServiceInfo *first, *second; gint diff; + gint64 tdiff; first = (AvahiServiceInfo *) a; second = (AvahiServiceInfo *) b; diff = second->accuracy - first->accuracy; + if (diff) + return diff; + + g_assert (first->timestamp_add >= 0); + g_assert (second->timestamp_add >= 0); + tdiff = first->timestamp_add - second->timestamp_add; + if (tdiff < 0) + return -1; + else if (tdiff > 0) + return 1; + else + return 0; +} - if (diff == 0) - return first->timestamp - second->timestamp; +static void +disconnect_from_service (GClueNMEASource *source) +{ + GClueNMEASourcePrivate *priv = source->priv; - return diff; + if (!priv->active_service) + return; + + g_cancellable_cancel (priv->cancellable); + + g_clear_object (&priv->input_stream); + g_clear_object (&priv->connection); + g_clear_object (&priv->client); + g_clear_object (&priv->cancellable); + priv->active_service = NULL; } static gboolean @@ -147,9 +185,9 @@ reconnection_required (GClueNMEASource *source) * 2. a more accurate service than one currently in use, is now * available. */ - return (priv->active_service != NULL && - (priv->all_services == NULL || - priv->active_service != priv->all_services->data)); + return priv->active_service == NULL || + priv->try_services == NULL || + priv->active_service != priv->try_services->data; } static void @@ -159,25 +197,35 @@ reconnect_service (GClueNMEASource *source) return; disconnect_from_service (source); - connect_to_service (source); + try_connect_to_service (source); } -static void -refresh_accuracy_level (GClueNMEASource *source) +static GClueAccuracyLevel get_head_accuracy (GList *list) { - GClueAccuracyLevel new, existing; + AvahiServiceInfo *service; + + if (!list) + return GCLUE_ACCURACY_LEVEL_NONE; + + service = (AvahiServiceInfo *) list->data; + return service->accuracy; +} + +static gboolean +on_refresh_accuracy_level (gpointer user_data) +{ + GClueNMEASource *source = GCLUE_NMEA_SOURCE (user_data); + GClueNMEASourcePrivate *priv = source->priv; + GClueAccuracyLevel new_try, new_broken, new, existing; + + priv->accuracy_refresh_source = 0; existing = gclue_location_source_get_available_accuracy_level (GCLUE_LOCATION_SOURCE (source)); - if (source->priv->all_services != NULL) { - AvahiServiceInfo *service; - - service = (AvahiServiceInfo *) source->priv->all_services->data; - new = service->accuracy; - } else { - new = GCLUE_ACCURACY_LEVEL_NONE; - } + new_try = get_head_accuracy (priv->try_services); + new_broken = get_head_accuracy (priv->broken_services); + new = MAX (new_try, new_broken); if (new != existing) { g_debug ("Available accuracy level from %s: %u", @@ -186,6 +234,109 @@ refresh_accuracy_level (GClueNMEASource *source) "available-accuracy-level", new, NULL); } + + return G_SOURCE_REMOVE; +} + +static void +refresh_accuracy_level (GClueNMEASource *source) +{ + GClueNMEASourcePrivate *priv = source->priv; + + if (priv->accuracy_refresh_source) { + return; + } + + g_debug ("Scheduling accuracy level refresh"); + priv->accuracy_refresh_source = g_idle_add (on_refresh_accuracy_level, + source); +} + +static gboolean +on_service_unbreak_time (gpointer source) +{ + GClueNMEASourcePrivate *priv = GCLUE_NMEA_SOURCE (source)->priv; + + priv->unbreak_timer = 0; + + if (!priv->try_services && priv->broken_services) { + g_debug ("Unbreaking existing services"); + + priv->try_services = priv->broken_services; + priv->broken_services = NULL; + + reconnect_service (source); + } + + return G_SOURCE_REMOVE; +} + +static void +check_unbreak_timer (GClueNMEASource *source) +{ + GClueNMEASourcePrivate *priv = source->priv; + + if (priv->try_services || !priv->broken_services) { + if (priv->unbreak_timer) { + g_debug ("Removing unnecessary unbreaking timer"); + + g_source_remove (priv->unbreak_timer); + priv->unbreak_timer = 0; + } + + return; + } + + if (priv->unbreak_timer) { + return; + } + + g_debug ("Scheduling unbreaking timer"); + priv->unbreak_timer = g_timeout_add_seconds (SERVICE_UNBREAK_TIME, + on_service_unbreak_time, + source); +} + +static void +service_lists_changed (GClueNMEASource *source) +{ + check_unbreak_timer (source); + reconnect_service (source); + refresh_accuracy_level (source); +} + +static gboolean +check_service_exists (GClueNMEASource *source, + const char *name) +{ + GClueNMEASourcePrivate *priv = source->priv; + AvahiServiceInfo *service; + GList *item; + gboolean ret = FALSE; + + /* only `name` is required here */ + service = avahi_service_new (name, + NULL, + 0, + GCLUE_ACCURACY_LEVEL_NONE); + + item = g_list_find_custom (priv->try_services, + service, + compare_avahi_service_by_identifier); + if (item) { + ret = TRUE; + } else { + item = g_list_find_custom (priv->broken_services, + service, + compare_avahi_service_by_identifier); + if (item) { + ret = TRUE; + } + } + + g_clear_pointer (&service, avahi_service_free); + + return ret; } static void @@ -193,6 +344,7 @@ add_new_service (GClueNMEASource *source, const char *name, const char *host_name, uint16_t port, + gboolean is_socket, AvahiStringList *txt) { GClueAccuracyLevel accuracy = GCLUE_ACCURACY_LEVEL_NONE; @@ -203,7 +355,12 @@ add_new_service (GClueNMEASource *source, GEnumClass *enum_class; GEnumValue *enum_value; - if (port == 0) { + if (check_service_exists (source, name)) { + g_debug ("Service %s already exists", name); + return; + } + + if (!txt) { accuracy = GCLUE_ACCURACY_LEVEL_EXACT; goto CREATE_SERVICE; @@ -244,43 +401,72 @@ add_new_service (GClueNMEASource *source, CREATE_SERVICE: service = avahi_service_new (name, host_name, port, accuracy); + service->is_socket = is_socket; - source->priv->all_services = g_list_insert_sorted - (source->priv->all_services, + source->priv->try_services = g_list_insert_sorted + (source->priv->try_services, service, compare_avahi_service_by_accuracy_n_time); - refresh_accuracy_level (source); - reconnect_service (source); + n_services = g_list_length (source->priv->try_services); + g_debug ("No. of _nmea-0183._tcp services %u", n_services); - n_services = g_list_length (source->priv->all_services); + service_lists_changed (source); +} - g_debug ("No. of _nmea-0183._tcp services %u", n_services); +static void +add_new_service_avahi (GClueNMEASource *source, + const char *name, + const char *host_name, + uint16_t port, + AvahiStringList *txt) +{ + add_new_service (source, name, host_name, port, FALSE, txt); } static void -remove_service (GClueNMEASource *source, - AvahiServiceInfo *service) +add_new_service_socket (GClueNMEASource *source, + const char *name, + const char *socket_path) { - guint n_services = 0; + add_new_service (source, name, socket_path, 0, TRUE, NULL); +} - avahi_service_free (service); - source->priv->all_services = g_list_remove - (source->priv->all_services, service); +static void +service_broken (GClueNMEASource *source) +{ + GClueNMEASourcePrivate *priv = source->priv; + AvahiServiceInfo *service = priv->active_service; - n_services = g_list_length (source->priv->all_services); + g_assert (service); - g_debug ("No. of _nmea-0183._tcp services %u", - n_services); + disconnect_from_service (source); - refresh_accuracy_level (source); - reconnect_service (source); + priv->try_services = g_list_remove (priv->try_services, + service); + priv->broken_services = g_list_insert_sorted + (priv->broken_services, + service, + compare_avahi_service_by_accuracy_n_time); + + service_lists_changed (source); +} + +static void +remove_service_from_list (GList **list, + GList *item) +{ + AvahiServiceInfo *service = item->data; + + *list = g_list_delete_link (*list, item); + avahi_service_free (service); } static void remove_service_by_name (GClueNMEASource *source, const char *name) { + GClueNMEASourcePrivate *priv = source->priv; AvahiServiceInfo *service; GList *item; @@ -290,15 +476,31 @@ remove_service_by_name (GClueNMEASource *source, 0, GCLUE_ACCURACY_LEVEL_NONE); - item = g_list_find_custom (source->priv->all_services, + item = g_list_find_custom (priv->try_services, service, compare_avahi_service_by_identifier); - avahi_service_free (service); + if (item) { + if (item->data == priv->active_service) { + g_debug ("Active NMEA service removed, disconnecting."); + disconnect_from_service (source); + } - if (item == NULL) - return; + remove_service_from_list (&priv->try_services, + item); + } else { + item = g_list_find_custom (priv->broken_services, + service, + compare_avahi_service_by_identifier); + if (item) { + g_assert (item->data != priv->active_service); + remove_service_from_list (&priv->broken_services, + item); + } + } - remove_service (source, item->data); + g_clear_pointer (&service, avahi_service_free); + + service_lists_changed (source); } static void @@ -339,15 +541,18 @@ resolve_callback (AvahiServiceResolver *service_resolver, } case AVAHI_RESOLVER_FOUND: - g_debug ("Service %s:%u resolved", + g_debug ("Service '%s' of type '%s' in domain '%s' resolved to %s:%u", + name, + type, + domain, host_name, - port); + (unsigned int)port); - add_new_service (GCLUE_NMEA_SOURCE (user_data), - name, - host_name, - port, - txt); + add_new_service_avahi (GCLUE_NMEA_SOURCE (user_data), + name, + host_name, + port, + txt); break; } @@ -360,12 +565,8 @@ client_callback (AvahiClient *avahi_client, AvahiClientState state, void *user_data) { - GClueNMEASourcePrivate *priv = GCLUE_NMEA_SOURCE (user_data)->priv; - g_return_if_fail (avahi_client != NULL); - priv->avahi_client = avahi_client; - if (state == AVAHI_CLIENT_FAILURE) { const char *errorstr = avahi_strerror (avahi_client_errno (avahi_client)); @@ -514,21 +715,19 @@ on_read_nmea_sentence (GObject *object, do { if (message == NULL) { if (error != NULL) { - if (error->code == G_IO_ERROR_CLOSED) + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + return; + } else if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CLOSED)) { g_debug ("Socket closed."); - else if (error->code != G_IO_ERROR_CANCELLED) + } else { g_warning ("Error when receiving message: %s", error->message); + } } else { g_debug ("Nothing to read"); } - g_object_unref (data_input_stream); - if (source->priv->active_service != NULL) - /* In case service did not advertise it exiting - * or we failed to receive it's notification. - */ - remove_service (source, source->priv->active_service); + service_broken (source); return; } @@ -594,28 +793,34 @@ on_connection_to_location_server (GObject *object, { GClueNMEASource *source = GCLUE_NMEA_SOURCE (user_data); GSocketClient *client = G_SOCKET_CLIENT (object); - GError *error = NULL; - GDataInputStream *data_input_stream; - GInputStream *input_stream; + g_autoptr(GSocketConnection) connection = NULL; + g_autoptr(GError) error = NULL; - source->priv->connection = g_socket_client_connect_to_host_finish + connection = g_socket_client_connect_to_host_finish (client, result, &error); if (error != NULL) { - if (error->code != G_IO_ERROR_CANCELLED) - g_warning ("Failed to connect to NMEA service: %s", error->message); - g_clear_error (&error); + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + return; + } + g_warning ("Failed to connect to NMEA service: %s", error->message); + service_broken (source); return; } - input_stream = g_io_stream_get_input_stream - (G_IO_STREAM (source->priv->connection)); - data_input_stream = g_data_input_stream_new (input_stream); + g_debug ("NMEA service connected."); - g_data_input_stream_read_upto_async (data_input_stream, + g_assert (!source->priv->connection); + source->priv->connection = g_steal_pointer (&connection); + + g_assert (!source->priv->input_stream); + source->priv->input_stream = g_data_input_stream_new + (g_io_stream_get_input_stream (G_IO_STREAM (source->priv->connection))); + + g_data_input_stream_read_upto_async (source->priv->input_stream, NMEA_LINE_END, NMEA_LINE_END_CTR, G_PRIORITY_DEFAULT, @@ -625,24 +830,39 @@ on_connection_to_location_server (GObject *object, } static void -connect_to_service (GClueNMEASource *source) +try_connect_to_service (GClueNMEASource *source) { GClueNMEASourcePrivate *priv = source->priv; - GSocketAddress *addr; - GSocketConnectable *connectable; - if (priv->all_services == NULL) + if (!gclue_location_source_get_active (GCLUE_LOCATION_SOURCE (source))) { + g_warn_if_fail (!priv->active_service); + + return; + } + + if (priv->active_service) return; + if (priv->try_services == NULL) + return; + + g_assert (!priv->cancellable); + priv->cancellable = g_cancellable_new (); + + g_assert (!priv->client); priv->client = g_socket_client_new (); - g_cancellable_reset (priv->cancellable); /* The service with the highest accuracy will be stored in the beginning * of the list. */ - priv->active_service = (AvahiServiceInfo *) priv->all_services->data; + priv->active_service = (AvahiServiceInfo *) priv->try_services->data; - if ( priv->active_service->port != 0 ) + g_debug ("Trying to connect to NMEA %sservice %s:%u.", + priv->active_service->is_socket ? "socket " : "", + priv->active_service->host_name, + (unsigned int) priv->active_service->port); + + if (!priv->active_service->is_socket) { g_socket_client_connect_to_host_async (priv->client, priv->active_service->host_name, @@ -650,52 +870,86 @@ connect_to_service (GClueNMEASource *source) priv->cancellable, on_connection_to_location_server, source); - else { - addr = g_unix_socket_address_new(priv->active_service->host_name); - connectable = G_SOCKET_CONNECTABLE (addr); + } else { + g_autoptr(GSocketAddress) addr = NULL; + + addr = g_unix_socket_address_new (priv->active_service->host_name); g_socket_client_connect_async (priv->client, - connectable, + G_SOCKET_CONNECTABLE (addr), priv->cancellable, on_connection_to_location_server, source); } } -static void -disconnect_from_service (GClueNMEASource *source) +static gboolean +remove_avahi_services_from_list (GClueNMEASource *source, GList **list) { GClueNMEASourcePrivate *priv = source->priv; + gboolean removed_active = FALSE; + GList *l = *list; + + while (l != NULL) { + GList *next = l->next; + AvahiServiceInfo *service = l->data; + + if (!service->is_socket) { + if (service == priv->active_service) { + g_debug ("Active NMEA service was Avahi-provided, disconnecting."); + disconnect_from_service (source); + removed_active = TRUE; + } - g_cancellable_cancel (priv->cancellable); + remove_service_from_list (list, l); + } + + l = next; + } - if (priv->connection != NULL) { - GError *error = NULL; + return removed_active; +} - g_io_stream_close (G_IO_STREAM (priv->connection), - NULL, - &error); - if (error != NULL) - g_warning ("Error in closing socket connection: %s", error->message); +static void +disconnect_avahi_client (GClueNMEASource *source) +{ + GClueNMEASourcePrivate *priv = source->priv; + + remove_avahi_services_from_list (source, &priv->try_services); + if (remove_avahi_services_from_list (source, &priv->broken_services)) { + g_warn_if_reached (); } - g_clear_object (&priv->connection); - g_clear_object (&priv->client); - priv->active_service = NULL; + g_clear_pointer (&priv->avahi_client, avahi_client_free); + + service_lists_changed (source); } static void gclue_nmea_source_finalize (GObject *gnmea) { - GClueNMEASourcePrivate *priv = GCLUE_NMEA_SOURCE (gnmea)->priv; + GClueNMEASource *source = GCLUE_NMEA_SOURCE (gnmea); + GClueNMEASourcePrivate *priv = source->priv; G_OBJECT_CLASS (gclue_nmea_source_parent_class)->finalize (gnmea); - g_clear_object (&priv->connection); - g_clear_object (&priv->client); - g_clear_object (&priv->cancellable); - if (priv->avahi_client) - avahi_client_free (priv->avahi_client); - g_list_free_full (priv->all_services, + disconnect_avahi_client (source); + disconnect_from_service (source); + + if (priv->accuracy_refresh_source) { + g_source_remove (priv->accuracy_refresh_source); + priv->accuracy_refresh_source = 0; + } + + if (priv->unbreak_timer) { + g_source_remove (priv->unbreak_timer); + priv->unbreak_timer = 0; + } + + g_clear_pointer (&priv->glib_poll, avahi_glib_poll_free); + + g_list_free_full (g_steal_pointer (&priv->try_services), + avahi_service_free); + g_list_free_full (g_steal_pointer (&priv->broken_services), avahi_service_free); } @@ -712,41 +966,33 @@ gclue_nmea_source_class_init (GClueNMEASourceClass *klass) } static void -gclue_nmea_source_init (GClueNMEASource *source) +try_connect_avahi_client (GClueNMEASource *source) { - GClueNMEASourcePrivate *priv; AvahiServiceBrowser *service_browser; + GClueNMEASourcePrivate *priv = source->priv; const AvahiPoll *poll_api; - AvahiGLibPoll *glib_poll; - const char *nmea_socket; - GClueConfig *config; int error; - source->priv = gclue_nmea_source_get_instance_private (source); - priv = source->priv; + if (priv->avahi_client) { + AvahiClientState avahi_state; - glib_poll = avahi_glib_poll_new (NULL, G_PRIORITY_DEFAULT); - poll_api = avahi_glib_poll_get (glib_poll); - - priv->cancellable = g_cancellable_new (); - - config = gclue_config_get_singleton (); + avahi_state = avahi_client_get_state (priv->avahi_client); + if (avahi_state != AVAHI_CLIENT_FAILURE) { + return; + } - nmea_socket = gclue_config_get_nmea_socket (config); - if (nmea_socket != NULL) { - add_new_service (source, - "nmea-socket", - nmea_socket, - 0, - NULL); + g_debug ("Avahi client in failure state, trying to reinit."); + disconnect_avahi_client (source); } - avahi_client_new (poll_api, - 0, - client_callback, - source, - &error); + g_assert (priv->glib_poll); + poll_api = avahi_glib_poll_get (priv->glib_poll); + priv->avahi_client = avahi_client_new (poll_api, + 0, + client_callback, + source, + &error); if (priv->avahi_client == NULL) { g_warning ("Failed to connect to avahi service: %s", avahi_strerror (error)); @@ -762,15 +1008,43 @@ gclue_nmea_source_init (GClueNMEASource *source) 0, browse_callback, source); - - if (service_browser == NULL) { const char *errorstr; error = avahi_client_errno (priv->avahi_client); errorstr = avahi_strerror (error); g_warning ("Failed to browse avahi services: %s", errorstr); + goto fail_client; + } + + return; + +fail_client: + disconnect_avahi_client (source); +} + +static void +gclue_nmea_source_init (GClueNMEASource *source) +{ + GClueNMEASourcePrivate *priv; + const char *nmea_socket; + GClueConfig *config; + + source->priv = gclue_nmea_source_get_instance_private (source); + priv = source->priv; + + priv->glib_poll = avahi_glib_poll_new (NULL, G_PRIORITY_DEFAULT); + + config = gclue_config_get_singleton (); + + nmea_socket = gclue_config_get_nmea_socket (config); + if (nmea_socket != NULL) { + add_new_service_socket (source, + "nmea-socket", + nmea_socket); } + + try_connect_avahi_client (source); } /** @@ -790,8 +1064,10 @@ gclue_nmea_source_get_singleton (void) source = g_object_new (GCLUE_TYPE_NMEA_SOURCE, NULL); g_object_add_weak_pointer (G_OBJECT (source), (gpointer) &source); - } else + } else { g_object_ref (source); + try_connect_avahi_client (source); + } return source; } @@ -807,10 +1083,11 @@ gclue_nmea_source_start (GClueLocationSource *source) base_class = GCLUE_LOCATION_SOURCE_CLASS (gclue_nmea_source_parent_class); base_result = base_class->start (source); - if (base_result != GCLUE_LOCATION_SOURCE_START_RESULT_OK) + if (base_result == GCLUE_LOCATION_SOURCE_START_RESULT_FAILED) return base_result; - connect_to_service (GCLUE_NMEA_SOURCE (source)); + try_connect_avahi_client (GCLUE_NMEA_SOURCE (source)); + reconnect_service (GCLUE_NMEA_SOURCE (source)); return base_result; } -- cgit v1.2.1