/* bz-content-provider.c * * Copyright 2025 Adam Masciola * * 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 3 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, see . * * SPDX-License-Identifier: GPL-3.0-or-later */ #define G_LOG_DOMAIN "BAZAAR::CONTENT-PROVIDER" #include "config.h" #include #include #include "bz-content-provider.h" #include "env.h" #include "io.h" #include "util.h" struct _BzContentProvider { GObject parent_instance; GListModel *input_files; BzParser *parser; GListStore *input_mirror; GHashTable *input_tracking; GListStore *outputs; GtkFlattenListModel *impl_model; }; static void list_model_iface_init (GListModelInterface *iface); G_DEFINE_FINAL_TYPE_WITH_CODE ( BzContentProvider, bz_content_provider, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_model_iface_init)) enum { PROP_0, PROP_INPUT_FILES, PROP_PARSER, PROP_HAS_INPUTS, LAST_PROP }; static GParamSpec *props[LAST_PROP] = { 0 }; BZ_DEFINE_DATA ( input_init, InputInit, { GFile *file; }, BZ_RELEASE_DATA (file, g_object_unref)) static DexFuture * input_init_fiber (InputInitData *data); BZ_DEFINE_DATA ( input_load, InputLoad, { GFile *file; BzParser *parser; }, BZ_RELEASE_DATA (file, g_object_unref); BZ_RELEASE_DATA (parser, g_object_unref)) static DexFuture * input_load_fiber (InputLoadData *data); BZ_DEFINE_DATA ( input_tracking, InputTracking, { GMutex mutex; GWeakRef self; char *path; GFileMonitor *monitor; GListStore *output; DexFuture *init; DexFuture *task; }, g_mutex_clear (&self->mutex); g_weak_ref_clear (&self->self); BZ_RELEASE_DATA (path, g_free); BZ_RELEASE_DATA (monitor, g_object_unref); BZ_RELEASE_DATA (output, g_object_unref); BZ_RELEASE_DATA (init, dex_unref); BZ_RELEASE_DATA (task, dex_unref)) static DexFuture * input_init_finally (DexFuture *future, InputTrackingData *data); static DexFuture * input_load_finally (DexFuture *future, InputTrackingData *data); static void impl_model_changed (BzContentProvider *self, guint position, guint removed, guint added, GListModel *impl_model); static void input_files_changed (BzContentProvider *self, guint position, guint removed, guint added, GListModel *input_files); static void input_file_changed_on_disk (InputTrackingData *data, GFile *file, GFile *other_file, GFileMonitorEvent event_type, GFileMonitor *monitor); static gboolean commence_reload (InputTrackingData *data); static void bz_content_provider_dispose (GObject *object) { BzContentProvider *self = BZ_CONTENT_PROVIDER (object); g_clear_object (&self->input_files); g_clear_object (&self->parser); g_clear_object (&self->input_mirror); g_clear_pointer (&self->input_tracking, g_hash_table_unref); g_clear_object (&self->outputs); g_clear_object (&self->impl_model); G_OBJECT_CLASS (bz_content_provider_parent_class)->dispose (object); } static void bz_content_provider_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { BzContentProvider *self = BZ_CONTENT_PROVIDER (object); switch (prop_id) { case PROP_INPUT_FILES: g_value_set_object (value, bz_content_provider_get_input_files (self)); break; case PROP_PARSER: g_value_set_object (value, bz_content_provider_get_parser (self)); break; case PROP_HAS_INPUTS: g_value_set_boolean (value, bz_content_provider_get_has_inputs (self)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void bz_content_provider_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { BzContentProvider *self = BZ_CONTENT_PROVIDER (object); switch (prop_id) { case PROP_INPUT_FILES: bz_content_provider_set_input_files (self, g_value_get_object (value)); break; case PROP_PARSER: bz_content_provider_set_parser (self, g_value_get_object (value)); break; case PROP_HAS_INPUTS: default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void bz_content_provider_class_init (BzContentProviderClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->set_property = bz_content_provider_set_property; object_class->get_property = bz_content_provider_get_property; object_class->dispose = bz_content_provider_dispose; props[PROP_INPUT_FILES] = g_param_spec_object ( "input-files", NULL, NULL, G_TYPE_LIST_MODEL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); props[PROP_PARSER] = g_param_spec_object ( "parser", NULL, NULL, BZ_TYPE_PARSER, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); props[PROP_HAS_INPUTS] = g_param_spec_boolean ( "has-inputs", NULL, NULL, FALSE, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); g_object_class_install_properties (object_class, LAST_PROP, props); } static void bz_content_provider_init (BzContentProvider *self) { self->input_mirror = g_list_store_new (G_TYPE_FILE); self->input_tracking = g_hash_table_new_full ( g_direct_hash, g_direct_equal, g_object_unref, input_tracking_data_unref); self->outputs = g_list_store_new (G_TYPE_LIST_MODEL); self->impl_model = gtk_flatten_list_model_new (g_object_ref (G_LIST_MODEL (self->outputs))); g_signal_connect_swapped ( self->impl_model, "items-changed", G_CALLBACK (impl_model_changed), self); } static GType list_model_get_item_type (GListModel *list) { return G_TYPE_OBJECT; } static guint list_model_get_n_items (GListModel *list) { BzContentProvider *self = BZ_CONTENT_PROVIDER (list); return g_list_model_get_n_items (G_LIST_MODEL (self->impl_model)); } static gpointer list_model_get_item (GListModel *list, guint position) { BzContentProvider *self = BZ_CONTENT_PROVIDER (list); return g_list_model_get_item (G_LIST_MODEL (self->impl_model), position); } static void list_model_iface_init (GListModelInterface *iface) { iface->get_item_type = list_model_get_item_type; iface->get_n_items = list_model_get_n_items; iface->get_item = list_model_get_item; } BzContentProvider * bz_content_provider_new (void) { return g_object_new (BZ_TYPE_CONTENT_PROVIDER, NULL); } void bz_content_provider_set_input_files (BzContentProvider *self, GListModel *input_files) { guint old_length = 0; g_return_if_fail (BZ_IS_CONTENT_PROVIDER (self)); g_return_if_fail (input_files == NULL || G_IS_LIST_MODEL (input_files)); if (self->input_files != NULL) { old_length = g_list_model_get_n_items (self->input_files); g_signal_handlers_disconnect_by_func ( self->input_files, input_files_changed, self); } g_clear_object (&self->input_files); g_hash_table_remove_all (self->input_tracking); g_list_store_remove_all (self->input_mirror); g_list_store_remove_all (self->outputs); if (input_files != NULL) { self->input_files = g_object_ref (input_files); g_signal_connect_swapped ( input_files, "items-changed", G_CALLBACK (input_files_changed), self); input_files_changed ( self, 0, old_length, g_list_model_get_n_items (input_files), input_files); } g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INPUT_FILES]); } GListModel * bz_content_provider_get_input_files (BzContentProvider *self) { g_return_val_if_fail (BZ_IS_CONTENT_PROVIDER (self), NULL); return self->input_files; } void bz_content_provider_set_parser (BzContentProvider *self, BzParser *parser) { GHashTableIter iter = { 0 }; g_return_if_fail (BZ_IS_CONTENT_PROVIDER (self)); g_return_if_fail (parser == NULL || BZ_IS_PARSER (parser)); g_clear_object (&self->parser); if (parser != NULL) self->parser = g_object_ref (parser); g_hash_table_iter_init (&iter, self->input_tracking); for (;;) { GFile *file = NULL; InputTrackingData *data = NULL; if (!g_hash_table_iter_next ( &iter, (gpointer *) &file, (gpointer *) &data)) break; commence_reload (data); } g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PARSER]); } BzParser * bz_content_provider_get_parser (BzContentProvider *self) { g_return_val_if_fail (BZ_IS_CONTENT_PROVIDER (self), NULL); return self->parser; } gboolean bz_content_provider_get_has_inputs (BzContentProvider *self) { g_return_val_if_fail (BZ_IS_CONTENT_PROVIDER (self), FALSE); return self->input_files != NULL && g_list_model_get_n_items (self->input_files) > 0 && g_list_model_get_n_items (G_LIST_MODEL (self)) > 0; } static void impl_model_changed (BzContentProvider *self, guint position, guint removed, guint added, GListModel *impl_model) { g_list_model_items_changed ( G_LIST_MODEL (self), position, removed, added); } static void input_files_changed (BzContentProvider *self, guint position, guint removed, guint added, GListModel *input_files) { gboolean emit_has_inputs = FALSE; g_autofree GFile **additions = NULL; g_autofree GListStore **new_outputs = NULL; emit_has_inputs = g_list_model_get_n_items (input_files) == 0 || g_list_model_get_n_items (G_LIST_MODEL (self->input_mirror)) == 0; if (removed > 0) { for (guint i = 0; i < removed; i++) { g_autoptr (GFile) removal = NULL; InputTrackingData *data = NULL; removal = g_list_model_get_item ( G_LIST_MODEL (self->input_mirror), position + i); data = g_hash_table_lookup (self->input_tracking, removal); g_assert (data != NULL); g_mutex_lock (&data->mutex); dex_clear (&data->task); g_mutex_unlock (&data->mutex); g_hash_table_remove (self->input_tracking, removal); } } if (added > 0) { additions = g_malloc0_n (added, sizeof (*additions)); for (guint i = 0; i < added; i++) additions[i] = g_list_model_get_item ( G_LIST_MODEL (self->input_files), position + i); new_outputs = g_malloc0_n (added, sizeof (*new_outputs)); for (guint i = 0; i < added; i++) new_outputs[i] = g_list_store_new (G_TYPE_OBJECT); } g_list_store_splice (self->input_mirror, position, removed, (gpointer *) additions, added); g_list_store_splice (self->outputs, position, removed, (gpointer *) new_outputs, added); for (guint i = 0; i < added; i++) { g_autoptr (InputInitData) init_data = NULL; g_autoptr (InputTrackingData) tracking_data = NULL; g_autoptr (DexFuture) future = NULL; init_data = input_init_data_new (); init_data->file = g_object_ref (additions[i]); future = dex_limiter_run ( bz_get_io_limiter (), bz_get_io_scheduler (), bz_get_dex_stack_size (), (DexFiberFunc) input_init_fiber, input_init_data_ref (init_data), input_init_data_unref); tracking_data = input_tracking_data_new (); g_mutex_init (&tracking_data->mutex); g_weak_ref_init (&tracking_data->self, self); tracking_data->path = g_file_get_path (additions[i]); tracking_data->output = g_steal_pointer (&new_outputs[i]); g_mutex_lock (&tracking_data->mutex); future = dex_future_finally ( future, (DexFutureCallback) input_init_finally, input_tracking_data_ref (tracking_data), input_tracking_data_unref); tracking_data->init = g_steal_pointer (&future); g_mutex_unlock (&tracking_data->mutex); g_hash_table_replace ( self->input_tracking, g_steal_pointer (&additions[i]), input_tracking_data_ref (tracking_data)); } if (emit_has_inputs) g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HAS_INPUTS]); } static void input_file_changed_on_disk (InputTrackingData *data, GFile *file, GFile *other_file, GFileMonitorEvent event_type, GFileMonitor *monitor) { if (event_type == G_FILE_MONITOR_EVENT_CHANGED || event_type == G_FILE_MONITOR_EVENT_CREATED || event_type == G_FILE_MONITOR_EVENT_DELETED) g_idle_add_full ( G_PRIORITY_DEFAULT, (GSourceFunc) commence_reload, input_tracking_data_ref (data), input_tracking_data_unref); } static DexFuture * input_init_fiber (InputInitData *data) { GFile *file = data->file; g_autoptr (GError) local_error = NULL; g_autoptr (GFileMonitor) monitor = NULL; monitor = g_file_monitor_file ( file, G_FILE_MONITOR_NONE, NULL, &local_error); if (monitor == NULL) return dex_future_new_for_error (g_steal_pointer (&local_error)); return dex_future_new_for_object (monitor); } static DexFuture * input_init_finally (DexFuture *future, InputTrackingData *data) { g_autoptr (BzContentProvider) self = NULL; g_autoptr (GError) local_error = NULL; g_autoptr (GMutexLocker) locker = NULL; const GValue *value = NULL; locker = g_mutex_locker_new (&data->mutex); dex_clear (&data->init); bz_weak_get_or_return_reject (self, &data->self); g_list_store_remove_all (data->output); value = dex_future_get_value (future, &local_error); if (value != NULL) { data->monitor = g_value_dup_object (value); g_signal_connect_swapped ( data->monitor, "changed", G_CALLBACK (input_file_changed_on_disk), data); g_clear_pointer (&locker, g_mutex_locker_free); commence_reload (data); } else g_warning ("Could not init object watch at path %s: %s", data->path, local_error->message); return NULL; } static DexFuture * input_load_fiber (InputLoadData *data) { GFile *file = data->file; BzParser *parser = data->parser; g_autoptr (GError) local_error = NULL; g_autoptr (GBytes) bytes = NULL; g_autoptr (GHashTable) parse_results = NULL; GObject *object = NULL; bytes = dex_await_boxed (dex_file_load_contents_bytes (file), &local_error); if (bytes == NULL) return dex_future_new_for_error (g_steal_pointer (&local_error)); parse_results = bz_parser_process_bytes (parser, bytes, &local_error); if (parse_results == NULL) return dex_future_new_for_error (g_steal_pointer (&local_error)); object = g_value_get_object (g_hash_table_lookup (parse_results, "/")); if (object == NULL) return dex_future_new_reject (G_IO_ERROR, G_IO_ERROR_UNKNOWN, "Parser returned invalid results"); return dex_future_new_for_object (object); } static DexFuture * input_load_finally (DexFuture *future, InputTrackingData *data) { g_autoptr (BzContentProvider) self = NULL; g_autoptr (GError) local_error = NULL; g_autoptr (GMutexLocker) locker = NULL; const GValue *value = NULL; locker = g_mutex_locker_new (&data->mutex); g_list_store_remove_all (data->output); bz_weak_get_or_return_reject (self, &data->self); value = dex_future_get_value (future, &local_error); if (value != NULL) g_list_store_append (data->output, g_value_get_object (value)); else if (local_error->domain != G_IO_ERROR) g_warning ("Could not load object at path %s: %s", data->path, local_error->message); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HAS_INPUTS]); return NULL; } static gboolean commence_reload (InputTrackingData *data) { g_autoptr (BzContentProvider) self = NULL; g_autoptr (GMutexLocker) locker = NULL; g_autoptr (InputLoadData) load_data = NULL; g_autoptr (DexFuture) future = NULL; locker = g_mutex_locker_new (&data->mutex); dex_clear (&data->task); self = g_weak_ref_get (&data->self); if (self == NULL) goto done; if (self->parser == NULL) goto done; load_data = input_load_data_new (); load_data->file = g_file_new_for_path (data->path); load_data->parser = g_object_ref (self->parser); future = dex_limiter_run ( bz_get_io_limiter (), bz_get_io_scheduler (), bz_get_dex_stack_size (), (DexFiberFunc) input_load_fiber, input_load_data_ref (load_data), input_load_data_unref); future = dex_future_finally ( future, (DexFutureCallback) input_load_finally, input_tracking_data_ref (data), input_tracking_data_unref); data->task = g_steal_pointer (&future); done: return G_SOURCE_REMOVE; }