Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package dconf for openSUSE:Factory checked in at 2026-10-30 18:08:49 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/dconf (Old) and /work/SRC/openSUSE:Factory/.dconf.new.1255 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "dconf" Fri Oct 30 18:08:49 2026 rev:58 rq:1385505 version:51.0 Changes: -------- --- /work/SRC/openSUSE:Factory/dconf/dconf.changes 2026-03-06 18:17:27.949349555 +0100 +++ /work/SRC/openSUSE:Factory/.dconf.new.1255/dconf.changes 2026-10-30 18:11:30.705286703 +0100 @@ -1,0 +2,27 @@ +Wed Sep 30 17:12:07 UTC 2026 - Dominique Leuenberger <[email protected]> + +- Update version dependencies according to meson.build. + +------------------------------------------------------------------- +Mon Sep 14 15:48:08 UTC 2026 - Dominique Leuenberger <[email protected]> + +- Update to version 51.0: + + No changes since 51.rc. + +------------------------------------------------------------------- +Wed Sep 2 06:56:26 UTC 2026 - Bjørn Lie <[email protected]> + +- Update to version 51.rc: + + Update overview regarding safe writer. + +------------------------------------------------------------------- +Thu Aug 6 13:58:41 UTC 2026 - Bjørn Lie <[email protected]> + +- Update to version 51.beta: + + Implement safe GVDB writer and use it by default for homes on + NFS + + docs: + - Describe DBDIR argument in man page + - Fix a typo + +------------------------------------------------------------------- Old: ---- dconf-0.49.0.tar.xz New: ---- _scmsync.obsinfo _service build.specials.obscpio dconf-51.0.tar.xz dconf.obsinfo ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ dconf.spec ++++++ --- /var/tmp/diff_new_pack.PHn4Ht/_old 2026-10-30 18:11:32.277352514 +0100 +++ /var/tmp/diff_new_pack.PHn4Ht/_new 2026-10-30 18:11:32.280352639 +0100 @@ -17,13 +17,13 @@ Name: dconf -Version: 0.49.0 +Version: 51.0 Release: 0 Summary: Key-based configuration system License: LGPL-2.1-or-later Group: System/Libraries URL: https://live.gnome.org/dconf -Source0: https://download.gnome.org/sources/dconf/0.49/%{name}-%{version}.tar.xz +Source0: %{name}-%{version}.tar.xz # PATCH-FIX-UPSTREAM 0001-gvdb-Restore-permissions-on-changed-files.patch bsc#971074 bgo#758066 bsc#1203344 [email protected] -- Restore permissions on files changed by dconf update. Patch0: 0001-gvdb-Restore-permissions-on-changed-files.patch BuildRequires: docbook-xsl-stylesheets @@ -34,8 +34,8 @@ BuildRequires: xsltproc BuildRequires: pkgconfig(bash-completion) BuildRequires: pkgconfig(dbus-1) -BuildRequires: pkgconfig(gio-unix-2.0) -BuildRequires: pkgconfig(glib-2.0) >= 2.44.0 +BuildRequires: pkgconfig(gio-unix-2.0) >= 2.25.7 +BuildRequires: pkgconfig(glib-2.0) >= 2.76 BuildRequires: pkgconfig(gobject-introspection-1.0) # dconf provides a dbus service, but has no dependency on dbus in any way # (because it uses gdbus), so we need an explicit Requires ++++++ _scmsync.obsinfo ++++++ mtime: 1792250026 commit: e63c31ae907b35de58de8f6d8bc1d23c840e24c1672f5b5847387066a8346f28 url: https://src.opensuse.org/GNOME/dconf revision: e63c31ae907b35de58de8f6d8bc1d23c840e24c1672f5b5847387066a8346f28 trackingbranch: factory projectscmsync: https://src.opensuse.org/GNOME/_ObsPrj ++++++ _service ++++++ <?xml version="1.0"?> <services> <service name="obs_scm" mode="manual"> <param name="scm">git</param> <param name="url">https://gitlab.gnome.org/GNOME/dconf.git</param> <param name="revision">51.0</param> <param name="versionformat">@PARENT_TAG@+@TAG_OFFSET@</param> <param name="versionrewrite-pattern">(.*)\+0</param> <param name="versionrewrite-replacement">\1</param> <!-- <param name="changesgenerate">enable</param> --> </service> <service name="tar" mode="manual"/> <service name="recompress" mode="manual"> <param name="file">*.tar</param> <param name="compression">xz</param> </service> <service name="set_version" mode="manual"/> </services> ++++++ build.specials.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/.gitignore new/.gitignore --- old/.gitignore 1970-01-01 01:00:00.000000000 +0100 +++ new/.gitignore 2026-10-17 17:13:46.000000000 +0200 @@ -0,0 +1,6 @@ +*.obscpio +*.osc +_build.* +.pbuild +*.patches~ +osc-collab.* ++++++ dconf-0.49.0.tar.xz -> dconf-51.0.tar.xz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dconf-0.49.0/.gitlab-ci.yml new/dconf-51.0/.gitlab-ci.yml --- old/dconf-0.49.0/.gitlab-ci.yml 2025-09-15 17:53:57.000000000 +0200 +++ new/dconf-51.0/.gitlab-ci.yml 2026-09-14 17:37:59.000000000 +0200 @@ -31,10 +31,10 @@ - ninja -C _build all dconf-doc - mkdir -p _coverage - - lcov --rc lcov_branch_coverage=1 --directory _build --capture --initial --output-file "_coverage/${CI_JOB_NAME}-baseline.lcov" + - lcov --rc branch_coverage=1 --directory _build --capture --initial --output-file "_coverage/${CI_JOB_NAME}-baseline.lcov" - meson test -C _build --timeout-multiplier ${MESON_TEST_TIMEOUT_MULTIPLIER} - - lcov --rc lcov_branch_coverage=1 --directory _build --capture --output-file "_coverage/${CI_JOB_NAME}.lcov" + - lcov --rc branch_coverage=1 --directory _build --capture --output-file "_coverage/${CI_JOB_NAME}.lcov" coverage: '/^\s+lines\.+:\s+([\d.]+\%)\s+/' except: - tags @@ -66,10 +66,10 @@ - ninja -C _build all dconf-doc - mkdir -p _coverage - - lcov --rc lcov_branch_coverage=1 --directory _build --capture --initial --output-file "_coverage/${CI_JOB_NAME}-baseline.lcov" + - lcov --rc branch_coverage=1 --directory _build --capture --initial --output-file "_coverage/${CI_JOB_NAME}-baseline.lcov" - meson test -C _build --timeout-multiplier ${MESON_TEST_TIMEOUT_MULTIPLIER} - - lcov --rc lcov_branch_coverage=1 --directory _build --capture --output-file "_coverage/${CI_JOB_NAME}.lcov" + - lcov --rc branch_coverage=1 --directory _build --capture --output-file "_coverage/${CI_JOB_NAME}.lcov" - mkdir -p public/ - mv _build/docs/html/ public/docs/ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dconf-0.49.0/NEWS new/dconf-51.0/NEWS --- old/dconf-0.49.0/NEWS 2025-09-15 17:53:57.000000000 +0200 +++ new/dconf-51.0/NEWS 2026-09-14 17:37:59.000000000 +0200 @@ -1,3 +1,22 @@ +Changes in dconf 51.0 +======================= + + - No changes since 51.rc + +Changes in dconf 51.rc +======================= + + - Update overview regarding safe writer (Marek Kasik, !97) + + +Changes in dconf 51.beta +======================= + + - Implement safe GVDB writer and use it by default for homes on NFS (Marek Kasik, Ray Strode, !88) + - docs: Describe DBDIR argument in man page (Marek Kasik, !94) + - docs: Fix a typo (Marek Kasik) + + Changes in dconf 0.49.0 ======================= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dconf-0.49.0/common/dconf-gvdb-utils.c new/dconf-51.0/common/dconf-gvdb-utils.c --- old/dconf-0.49.0/common/dconf-gvdb-utils.c 2025-09-15 17:53:57.000000000 +0200 +++ new/dconf-51.0/common/dconf-gvdb-utils.c 2026-09-14 17:37:59.000000000 +0200 @@ -62,6 +62,132 @@ return database; } +#include <fcntl.h> +#include <unistd.h> + +#define DCONF_GVDB_UTILS_ESTALE_RETRY_TIMEOUT_USEC (G_USEC_PER_SEC) +#define DCONF_GVDB_UTILS_ESTALE_INITIAL_DELAY_USEC 5000 +#define DCONF_GVDB_UTILS_ESTALE_MAX_DELAY_USEC 100000 + + +static gboolean +dconf_gvdb_utils_set_file_error (const gchar *filename, + gint saved_errno, + GError **error) +{ + g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (saved_errno), + "%s: %s", filename, g_strerror (saved_errno)); + + return FALSE; +} + +static gboolean +dconf_gvdb_utils_read_file_once (const gchar *filename, + gchar **contents, + gsize *size, + gint *saved_errno, + GError **error) +{ + gint fd; + + *contents = NULL; + *size = 0; + *saved_errno = 0; + + do + fd = g_open (filename, O_RDONLY | O_CLOEXEC, 0); + while (fd == -1 && errno == EINTR); + + if (fd == -1) + { + *saved_errno = errno; + + return dconf_gvdb_utils_set_file_error (filename, *saved_errno, error); + } + else + { + GString *data; + guint8 buffer[4096]; + + data = g_string_new (NULL); + + for (;;) + { + gssize bytes_read; + + bytes_read = read (fd, buffer, sizeof buffer); + if (bytes_read > 0) + { + g_string_append_len (data, (const gchar *) buffer, bytes_read); + } + else if (bytes_read == 0) + { + break; + } + else if (errno != EINTR) + { + *saved_errno = errno; + close (fd); + g_string_free (data, TRUE); + + return dconf_gvdb_utils_set_file_error (filename, *saved_errno, error); + } + } + + close (fd); + + *size = data->len; + *contents = g_string_free_and_steal (data); + + return TRUE; + } +} + +static gboolean +dconf_gvdb_utils_read_file (const gchar *filename, + gchar **contents, + gsize *size, + GError **error) +{ + GError *local_error = NULL; + gint saved_errno = 0; + gint64 estale_retry_deadline = 0; + guint estale_retry_delay = DCONF_GVDB_UTILS_ESTALE_INITIAL_DELAY_USEC; + + while (!dconf_gvdb_utils_read_file_once (filename, contents, size, + &saved_errno, &local_error)) + { +#ifdef ESTALE + if (saved_errno == ESTALE) + { + gint64 now = g_get_monotonic_time (); + + if (estale_retry_deadline == 0) + estale_retry_deadline = now + DCONF_GVDB_UTILS_ESTALE_RETRY_TIMEOUT_USEC; + + if (now < estale_retry_deadline) + { + gint64 remaining_usec = estale_retry_deadline - now; + guint delay_usec = MIN (estale_retry_delay, remaining_usec); + + g_clear_error (&local_error); + g_usleep (delay_usec); + + estale_retry_delay = MIN (estale_retry_delay * 2, + DCONF_GVDB_UTILS_ESTALE_MAX_DELAY_USEC); + continue; + } + } +#endif + + g_propagate_error (error, local_error); + + return FALSE; + } + + return TRUE; +} + DConfChangeset * dconf_gvdb_utils_read_and_back_up_file (const gchar *filename, gboolean *file_missing, @@ -73,7 +199,7 @@ gchar *contents; gsize size; - if (g_file_get_contents (filename, &contents, &size, &my_error)) + if (dconf_gvdb_utils_read_file (filename, &contents, &size, &my_error)) { GBytes *bytes; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dconf-0.49.0/docs/dconf-overview.xml new/dconf-51.0/docs/dconf-overview.xml --- old/dconf-0.49.0/docs/dconf-overview.xml 2025-09-15 17:53:57.000000000 +0200 +++ new/dconf-51.0/docs/dconf-overview.xml 2026-09-14 17:37:59.000000000 +0200 @@ -62,10 +62,9 @@ can be read back immediately) until the service signals that a write has completed. </para> <para> - The binary database format that dconf uses by default is not suitable for use on NFS, where mmap does not - work well. To handle this common use case, dconf can be configured to place its binary database in - <envar>XDG_RUNTIME_DIR</envar> (which is guaranteed to be local, but non-persistent) and synchronize it - with a plain text keyfile in the users home directory. + To overcome mmap issues when the binary database is on NFS, dconf places a binary database in + <envar>XDG_RUNTIME_DIR</envar> (which is guaranteed to be local, but non-persistent) and synchronizes it + automatically with the one in the user's home directory. </para> </refsect1> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dconf-0.49.0/docs/dconf-tool.xml new/dconf-51.0/docs/dconf-tool.xml --- old/dconf-0.49.0/docs/dconf-tool.xml 2025-09-15 17:53:57.000000000 +0200 +++ new/dconf-51.0/docs/dconf-tool.xml 2026-09-14 17:37:59.000000000 +0200 @@ -105,7 +105,7 @@ </para> <para> - The <replaceable>OUTPUT</replaceable> argument must the location to write a (binary) dconf database to and the + The <replaceable>OUTPUT</replaceable> argument must be the location to write a (binary) dconf database to and the <replaceable>KEYFILEDIR</replaceable> argument must be a .d directory containing keyfiles. </para> @@ -115,6 +115,11 @@ </para> <para> + The optional <replaceable>DBDIR</replaceable> argument is a directory containing the system dconf databases to be updated. + The default directory is typically <filename>/etc/dconf/db/</filename> and users need sufficient privileges to update it. + </para> + + <para> Note that dconf needs a D-Bus session bus connection to write changes to the dconf database. </para> </refsect1> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dconf-0.49.0/engine/dconf-engine-profile.c new/dconf-51.0/engine/dconf-engine-profile.c --- old/dconf-0.49.0/engine/dconf-engine-profile.c 2025-09-15 17:53:57.000000000 +0200 +++ new/dconf-51.0/engine/dconf-engine-profile.c 2026-09-14 17:37:59.000000000 +0200 @@ -28,6 +28,7 @@ #include <errno.h> #include "dconf-engine-source.h" +#include "dconf-engine-source-private.h" #define MANDATORY_DIR "/run/dconf/user/" /* + getuid () */ #define RUNTIME_PROFILE /* XDG_RUNTIME_DIR + */ "/dconf/profile" @@ -137,6 +138,30 @@ return source; } +static gboolean +file_is_on_nfs (const gchar *filename) +{ + GFileInfo *file_info; + gboolean result = FALSE; + GFile *file; + + file = g_file_new_for_path (filename); + file_info = g_file_query_filesystem_info (file, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE, NULL, NULL); /* ASYNC ? */ + if (file_info != NULL) + { + const gchar *filesystem_type = g_file_info_get_attribute_string (file_info, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE); + if (g_strcmp0 (filesystem_type, "nfs") == 0 || + g_strcmp0 (filesystem_type, "nfs4") == 0) + { + result = TRUE; + } + g_object_unref (file_info); + } + g_clear_object (&file); + + return result; +} + static DConfEngineSource ** dconf_engine_read_profile_file (FILE *file, gint *n_sources) @@ -145,7 +170,7 @@ gchar line[80]; gint n = 0, a; - sources = g_new (DConfEngineSource *, (a = 4)); + sources = g_new0 (DConfEngineSource *, (a = 4)); while (fgets (line, sizeof line, file)) { @@ -257,6 +282,12 @@ return dconf_engine_fopen (path, "r"); } +static inline gboolean +engine_source_is_user_db (DConfEngineSource *source) +{ + return source->vtable == &dconf_engine_source_user_vtable; +} + DConfEngineSource ** dconf_engine_profile_open (const gchar *profile, gint *n_sources) @@ -300,7 +331,10 @@ /* 5. Default profile */ if (profile == NULL && file == NULL) - return dconf_engine_default_profile (n_sources); + { + sources = dconf_engine_default_profile (n_sources); + goto out; + } /* At this point either we have a profile name or file open, but never * both. If it's a profile name, we try to open it. @@ -326,5 +360,34 @@ sources = dconf_engine_null_profile (n_sources); } +out: + /* Switch to "safe" backend if user config directory is on NFS. + This avoids the need to use "keyfile" backend which was recommended + for such situation before. + */ + if (sources != NULL && sources[0] != NULL) + { + if (engine_source_is_user_db (sources[0])) + { + if (G_UNLIKELY (file_is_on_nfs (g_get_user_config_dir ()))) + { + DConfEngineSource *old_source = sources[0]; + + if (old_source->name != NULL) + { + gchar *name = g_strdup_printf ("service-db:safe/%s", old_source->name); + sources[0] = dconf_engine_source_new (name); + g_free (name); + } + else + { + sources[0] = dconf_engine_source_new ("service-db:safe/user"); + } + + dconf_engine_source_free (old_source); + } + } + } + return sources; } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dconf-0.49.0/meson.build new/dconf-51.0/meson.build --- old/dconf-0.49.0/meson.build 2025-09-15 17:53:57.000000000 +0200 +++ new/dconf-51.0/meson.build 2026-09-14 17:37:59.000000000 +0200 @@ -1,6 +1,6 @@ project( 'dconf', ['c'], - version: '0.49.0', + version: '51.0', license: 'LGPL2.1+', meson_version: '>= 0.60.0', ) @@ -43,7 +43,7 @@ gio_dep = dependency('gio-2.0', version: gio_req_version) gio_unix_dep = dependency('gio-unix-2.0', version: gio_req_version) -glib_dep = dependency('glib-2.0', version: '>= 2.67.4') +glib_dep = dependency('glib-2.0', version: '>= 2.76') gio_module_dir = gio_dep.get_variable(pkgconfig: 'giomoduledir', pkgconfig_define: ['libdir', dconf_libdir]) dbus_session_service_dir = dependency('dbus-1').get_variable(pkgconfig: 'session_bus_services_dir', pkgconfig_define: ['datadir', dconf_datadir]) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dconf-0.49.0/service/dconf-keyfile-writer.c new/dconf-51.0/service/dconf-keyfile-writer.c --- old/dconf-0.49.0/service/dconf-keyfile-writer.c 2025-09-15 17:53:57.000000000 +0200 +++ new/dconf-51.0/service/dconf-keyfile-writer.c 2026-09-14 17:37:59.000000000 +0200 @@ -21,6 +21,7 @@ #include "config.h" #include "dconf-writer.h" +#include "dconf-writer-common.h" #include <string.h> #include <unistd.h> @@ -198,54 +199,9 @@ g_clear_pointer (&kfw->contents, g_free); - kfw->lock_fd = open (kfw->lock_filename, O_RDWR | O_CREAT, 0666); + kfw->lock_fd = dconf_lock_file_lock (kfw->lock_filename, error); if (kfw->lock_fd == -1) - { - gchar *dirname; - - /* Maybe it failed because the directory doesn't exist. Try - * again, after mkdir(). - */ - dirname = g_path_get_dirname (kfw->lock_filename); - g_mkdir_with_parents (dirname, 0700); - g_free (dirname); - - kfw->lock_fd = open (kfw->lock_filename, O_RDWR | O_CREAT, 0666); - if (kfw->lock_fd == -1) - { - gint saved_errno = errno; - - g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (saved_errno), - "%s: %s", kfw->lock_filename, g_strerror (saved_errno)); - return FALSE; - } - } - - while (TRUE) - { - struct flock lock; - - lock.l_type = F_WRLCK; - lock.l_whence = 0; - lock.l_start = 0; - lock.l_len = 0; /* lock all bytes */ - - if (fcntl (kfw->lock_fd, F_SETLKW, &lock) == 0) - break; - - if (errno != EINTR) - { - gint saved_errno = errno; - - g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (saved_errno), - "%s: unable to fcntl(F_SETLKW): %s", kfw->lock_filename, g_strerror (saved_errno)); - close (kfw->lock_fd); - kfw->lock_fd = -1; - return FALSE; - } - - /* it was EINTR. loop again. */ - } + return FALSE; if (!g_file_get_contents (kfw->filename, &kfw->contents, NULL, &local_error)) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dconf-0.49.0/service/dconf-safe-writer.c new/dconf-51.0/service/dconf-safe-writer.c --- old/dconf-0.49.0/service/dconf-safe-writer.c 1970-01-01 01:00:00.000000000 +0100 +++ new/dconf-51.0/service/dconf-safe-writer.c 2026-09-14 17:37:59.000000000 +0200 @@ -0,0 +1,286 @@ +/* + * Copyright © 2025 Red Hat + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the licence, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <http://www.gnu.org/licenses/>. + * + * Authors: Marek Kasik <[email protected]> + * + * This file is based on DconfWriter and DconfKeyfileWriter. + * This backend should be safe to run on homes shared over NFS + * while maintaining compatibility of stored data as it uses + * GVDB format. + * + */ + +#include "config.h" + +#include "dconf-writer.h" +#include "dconf-writer-common.h" + +#include "../common/dconf-gvdb-utils.h" + +#include <glib/gstdio.h> +#include <string.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> + +typedef DConfWriterClass DConfSafeWriterClass; + +typedef struct +{ + DConfWriter parent_instance; + gchar *filename; + gchar *lock_filename; + gint lock_fd; + GFileMonitor *monitor; + guint scheduled_update; + + DConfChangeset *uncommitted_values; + DConfChangeset *committed_values; + gboolean committed_values_missing; +} DConfSafeWriter; + +G_DEFINE_TYPE (DConfSafeWriter, dconf_safe_writer, DCONF_TYPE_WRITER) + +static gboolean dconf_safe_update (gpointer user_data); + +static void +dconf_safe_changed (GFileMonitor *monitor, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + gpointer user_data) +{ + DConfSafeWriter *sw = user_data; + + if (event_type == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT || + event_type == G_FILE_MONITOR_EVENT_CREATED) + { + if (!sw->scheduled_update) + sw->scheduled_update = g_idle_add (dconf_safe_update, sw); + } +} + +/* Acquire lock and check GVDB for changes against committed values. */ +static gboolean +dconf_safe_writer_begin (DConfWriter *writer, + GError **error) +{ + DConfSafeWriter *sw = (DConfSafeWriter *) writer; + DConfChangeset *changes; + gboolean missing; + + if (sw->filename == NULL) + { + GFile *file; + gchar *lock_base_filename; + + sw->filename = g_build_filename (g_get_user_config_dir (), "dconf", dconf_writer_get_name (writer), NULL); + lock_base_filename = g_strconcat (".", dconf_writer_get_name (writer), ".lock", NULL); + sw->lock_filename = g_build_filename (g_get_user_config_dir (), "dconf", lock_base_filename, NULL); + g_free (lock_base_filename); + + /* See https://bugzilla.gnome.org/show_bug.cgi?id=691618 */ + file = g_vfs_get_file_for_path (g_vfs_get_local (), sw->filename); + sw->monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE, NULL, error); + g_object_unref (file); + if (sw->monitor == NULL) + { + g_clear_pointer (&sw->filename, g_free); + g_clear_pointer (&sw->lock_filename, g_free); + + return FALSE; + } + + /* There has to be an initial change to begin watching for the "changed" signal! */ + g_signal_connect (sw->monitor, "changed", G_CALLBACK (dconf_safe_changed), sw); + } + + sw->lock_fd = dconf_lock_file_lock (sw->lock_filename, error); + if (sw->lock_fd == -1) + return FALSE; + + g_clear_pointer (&sw->committed_values, dconf_changeset_unref); + sw->committed_values = dconf_gvdb_utils_read_and_back_up_file (sw->filename, &missing, error); + if (sw->committed_values == NULL) + return FALSE; + sw->committed_values_missing = missing; + + if (!DCONF_WRITER_CLASS (dconf_safe_writer_parent_class)->begin (writer, error)) + return FALSE; + + changes = dconf_writer_diff (writer, sw->committed_values); + if (changes != NULL) + { + DCONF_WRITER_CLASS (dconf_safe_writer_parent_class)->change (writer, changes, ""); + dconf_changeset_unref (changes); + } + + sw->uncommitted_values = dconf_changeset_new_database (sw->committed_values); + + return TRUE; +} + +/* Add the change to uncommitted changes. */ +static void +dconf_safe_writer_change (DConfWriter *writer, + DConfChangeset *changeset, + const gchar *tag) +{ + DConfSafeWriter *sw = (DConfSafeWriter *) writer; + DConfChangeset *effective_changeset = dconf_changeset_filter_changes (sw->uncommitted_values, changeset); + + DCONF_WRITER_CLASS (dconf_safe_writer_parent_class)->change (writer, changeset, tag); + + if (effective_changeset != NULL) + { + dconf_changeset_unref (effective_changeset); + dconf_changeset_change (sw->uncommitted_values, changeset); + } +} + +static gboolean +dconf_safe_writer_rollback (DConfSafeWriter *sw, + GError **error) +{ + if (sw->committed_values_missing) + { + if (g_unlink (sw->filename) != 0 && errno != ENOENT) + { + gint saved_errno = errno; + + g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (saved_errno), + "%s: %s", sw->filename, g_strerror (saved_errno)); + return FALSE; + } + + return TRUE; + } + + return dconf_gvdb_utils_write_file (sw->filename, sw->committed_values, error); +} + +/* Store the uncommitted changes to the GVDB files. */ +static gboolean +dconf_safe_writer_commit (DConfWriter *writer, + GError **error) +{ + DConfSafeWriter *sw = (DConfSafeWriter *) writer; + g_autoptr(DConfChangeset) effective_changeset = NULL; + gboolean wrote_user_database = FALSE; + + effective_changeset = dconf_changeset_diff (sw->committed_values, sw->uncommitted_values); + + /* Save the changes to the file in user config directory. */ + if (effective_changeset != NULL) + { + if (!dconf_gvdb_utils_write_file (sw->filename, sw->uncommitted_values, error)) + return FALSE; + + wrote_user_database = TRUE; + } + + /* The runtime database update is the visible commit point for this process: + * the parent writer only emits notifications after this succeeds. If it + * fails, undo the user database write so the failed operation is not + * persisted on disk. + */ + if (!DCONF_WRITER_CLASS (dconf_safe_writer_parent_class)->commit (writer, error)) + { + if (wrote_user_database) + { + g_autoptr(GError) rollback_error = NULL; + + if (!dconf_safe_writer_rollback (sw, &rollback_error)) + g_warning ("Error rolling back database %s after failed write: %s", + sw->filename, rollback_error->message); + } + + return FALSE; + } + + g_clear_pointer (&sw->committed_values, dconf_changeset_unref); + sw->committed_values = sw->uncommitted_values; + sw->uncommitted_values = NULL; + if (wrote_user_database) + sw->committed_values_missing = FALSE; + + return TRUE; +} + +static void +dconf_safe_writer_end (DConfWriter *writer) +{ + DConfSafeWriter *sw = (DConfSafeWriter *) writer; + + DCONF_WRITER_CLASS (dconf_safe_writer_parent_class)->end (writer); + + g_clear_pointer (&sw->uncommitted_values, dconf_changeset_unref); + close (sw->lock_fd); + sw->lock_fd = -1; +} + +static gboolean +dconf_safe_update (gpointer user_data) +{ + DConfSafeWriter *sw = user_data; + + if (dconf_safe_writer_begin (DCONF_WRITER (sw), NULL)) + dconf_safe_writer_commit (DCONF_WRITER (sw), NULL); + dconf_safe_writer_end (DCONF_WRITER (sw)); + + sw->scheduled_update = 0; + + return G_SOURCE_REMOVE; +} + +static void +dconf_safe_writer_finalize (GObject *object) +{ + DConfSafeWriter *sw = (DConfSafeWriter *) object; + + if (sw->scheduled_update) + g_source_remove (sw->scheduled_update); + + g_clear_object (&sw->monitor); + g_free (sw->lock_filename); + if (sw->lock_fd >= 0) + close (sw->lock_fd); + g_free (sw->filename); + g_clear_pointer (&sw->committed_values, dconf_changeset_unref); + + G_OBJECT_CLASS (dconf_safe_writer_parent_class)->finalize (object); +} + +static void +dconf_safe_writer_init (DConfSafeWriter *sw) +{ + dconf_writer_set_basepath (DCONF_WRITER (sw), "safe"); + + sw->lock_fd = -1; +} + +static void +dconf_safe_writer_class_init (DConfWriterClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->finalize = dconf_safe_writer_finalize; + + class->begin = dconf_safe_writer_begin; + class->change = dconf_safe_writer_change; + class->commit = dconf_safe_writer_commit; + class->end = dconf_safe_writer_end; +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dconf-0.49.0/service/dconf-service.c new/dconf-51.0/service/dconf-service.c --- old/dconf-0.49.0/service/dconf-service.c 2025-09-15 17:53:57.000000000 +0200 +++ new/dconf-51.0/service/dconf-service.c 2026-09-14 17:37:59.000000000 +0200 @@ -238,6 +238,7 @@ g_io_extension_point_implement ("dconf-backend", DCONF_TYPE_WRITER, "Writer", 0); g_io_extension_point_implement ("dconf-backend", DCONF_TYPE_KEYFILE_WRITER, "keyfile", 0); g_io_extension_point_implement ("dconf-backend", DCONF_TYPE_SHM_WRITER, "shm", 0); + g_io_extension_point_implement ("dconf-backend", DCONF_TYPE_SAFE_WRITER, "safe", 0); service->blame = dconf_blame_get (); if (service->blame) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dconf-0.49.0/service/dconf-writer-common.c new/dconf-51.0/service/dconf-writer-common.c --- old/dconf-0.49.0/service/dconf-writer-common.c 1970-01-01 01:00:00.000000000 +0100 +++ new/dconf-51.0/service/dconf-writer-common.c 2026-09-14 17:37:59.000000000 +0200 @@ -0,0 +1,83 @@ +/* + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the licence, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#include "config.h" + +#include "dconf-writer-common.h" + +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <gio/gio.h> + +gint +dconf_lock_file_lock (const gchar *lock_filename, + GError **error) +{ + gint lock_fd; + + lock_fd = open (lock_filename, O_RDWR | O_CREAT, 0666); + if (lock_fd == -1) + { + gchar *dirname; + + /* Maybe it failed because the directory doesn't exist. Try + * again, after mkdir(). + */ + dirname = g_path_get_dirname (lock_filename); + g_mkdir_with_parents (dirname, 0700); + g_free (dirname); + + lock_fd = open (lock_filename, O_RDWR | O_CREAT, 0666); + if (lock_fd == -1) + { + gint saved_errno = errno; + + g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (saved_errno), + "%s: %s", lock_filename, g_strerror (saved_errno)); + + return lock_fd; + } + } + + while (TRUE) + { + struct flock lock = { 0 }; + + lock.l_type = F_WRLCK; + lock.l_whence = 0; + lock.l_start = 0; + lock.l_len = 0; /* lock all bytes */ + + if (fcntl (lock_fd, F_SETLKW, &lock) == 0) + break; + + if (errno != EINTR) + { + gint saved_errno = errno; + + g_set_error (error, G_FILE_ERROR, g_file_error_from_errno (saved_errno), + "%s: unable to fcntl(F_SETLKW): %s", lock_filename, g_strerror (saved_errno)); + close (lock_fd); + lock_fd = -1; + return lock_fd; + } + + /* it was EINTR. loop again. */ + } + + return lock_fd; +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dconf-0.49.0/service/dconf-writer-common.h new/dconf-51.0/service/dconf-writer-common.h --- old/dconf-0.49.0/service/dconf-writer-common.h 1970-01-01 01:00:00.000000000 +0100 +++ new/dconf-51.0/service/dconf-writer-common.h 2026-09-14 17:37:59.000000000 +0200 @@ -0,0 +1,26 @@ +/* + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the licence, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, see <http://www.gnu.org/licenses/>. + * + */ + +#ifndef __dconf_writer_common_h__ +#define __dconf_writer_common_h__ + +#include <glib.h> + +gint +dconf_lock_file_lock (const gchar *lock_filename, + GError **error); + +#endif /* __dconf_writer_common_h__ */ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dconf-0.49.0/service/dconf-writer.h new/dconf-51.0/service/dconf-writer.h --- old/dconf-0.49.0/service/dconf-writer.h 2025-09-15 17:53:57.000000000 +0200 +++ new/dconf-51.0/service/dconf-writer.h 2026-09-14 17:37:59.000000000 +0200 @@ -86,5 +86,7 @@ GType dconf_shm_writer_get_type (void); #define DCONF_TYPE_KEYFILE_WRITER (dconf_keyfile_writer_get_type ()) GType dconf_keyfile_writer_get_type (void); +#define DCONF_TYPE_SAFE_WRITER (dconf_safe_writer_get_type ()) +GType dconf_safe_writer_get_type (void); #endif /* __dconf_writer_h__ */ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dconf-0.49.0/service/meson.build new/dconf-51.0/service/meson.build --- old/dconf-0.49.0/service/meson.build 2025-09-15 17:53:57.000000000 +0200 +++ new/dconf-51.0/service/meson.build 2026-09-14 17:37:59.000000000 +0200 @@ -22,9 +22,11 @@ lib_sources = [ 'dconf-blame.c', 'dconf-keyfile-writer.c', + 'dconf-safe-writer.c', 'dconf-service.c', 'dconf-shm-writer.c', 'dconf-writer.c', + 'dconf-writer-common.c', ] sources = [ 'main.c', diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dconf-0.49.0/tests/meson.build new/dconf-51.0/tests/meson.build --- old/dconf-0.49.0/tests/meson.build 2025-09-15 17:53:57.000000000 +0200 +++ new/dconf-51.0/tests/meson.build 2026-09-14 17:37:59.000000000 +0200 @@ -10,6 +10,12 @@ dependencies: [glib_dep, libgvdb_dep], ) +libdconf_mock_no_gvdb = static_library( + 'dconf-mock-no-gvdb', + sources: files('dconf-mock-dbus.c', 'dconf-mock-shm.c'), + dependencies: [glib_dep, libgvdb_dep], +) + envs = test_env + [ 'G_TEST_SRCDIR=' + meson.current_source_dir(), 'G_TEST_BUILDDIR=' + meson.current_build_dir(), @@ -32,7 +38,7 @@ ['gdbus-filter-leak', 'dbus-leak.c', '-DDBUS_BACKEND="/gdbus/filter"', [libdconf_client_dep, libdconf_gdbus_filter_dep], []], ['engine', 'engine.c', '-DSRCDIR="@0@"'.format(test_dir), [dl_dep, libdconf_engine_test_dep, m_dep], libdconf_mock], ['client', 'client.c', '-DSRCDIR="@0@"'.format(test_dir), [libdconf_client_dep, libdconf_engine_dep], libdconf_mock], - ['writer', 'writer.c', '-DSRCDIR="@0@"'.format(test_dir), [glib_dep, dl_dep, m_dep, libdconf_service_dep], [libdconf_mock]], + ['writer', 'writer.c', '-DSRCDIR="@0@"'.format(test_dir), [glib_dep, dl_dep, m_dep, libdconf_service_dep], [libdconf_mock_no_gvdb]], ] foreach unit_test: unit_tests diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dconf-0.49.0/tests/writer.c new/dconf-51.0/tests/writer.c --- old/dconf-0.49.0/tests/writer.c 2025-09-15 17:53:57.000000000 +0200 +++ new/dconf-51.0/tests/writer.c 2026-09-14 17:37:59.000000000 +0200 @@ -23,8 +23,10 @@ #include "service/dconf-generated.h" #include "service/dconf-writer.h" +#include "common/dconf-gvdb-utils.h" static guint n_warnings = 0; +static gchar *runtime_dir = NULL; static GLogWriterOutput log_writer_cb (GLogLevelFlags log_level, @@ -378,6 +380,141 @@ g_assert_cmpint (g_unlink (db_filename), ==, 0); } +static gchar * +safe_runtime_db_filename (const gchar *db_name) +{ + return g_build_filename (runtime_dir, "dconf-service", "safe", db_name, NULL); +} + +static DConfChangeset * +read_db (const gchar *filename) +{ + g_autoptr(GError) error = NULL; + gboolean missing = FALSE; + DConfChangeset *db; + + db = dconf_gvdb_utils_read_and_back_up_file (filename, &missing, &error); + g_assert_no_error (error); + g_assert_false (missing); + g_assert_nonnull (db); + + return db; +} + +static void +test_safe_writer_persists_add_and_delete (Fixture *fixture, + gconstpointer test_data) +{ + const gchar *db_name = "safe-add-delete"; + g_autoptr(DConfWriter) writer = NULL; + DConfWriterClass *writer_class; + g_autoptr(GError) error = NULL; + g_autofree gchar *db_filename = g_build_filename (fixture->dconf_dir, db_name, NULL); + + writer = DCONF_WRITER (dconf_writer_new (DCONF_TYPE_SAFE_WRITER, db_name)); + writer_class = DCONF_WRITER_GET_CLASS (writer); + + g_assert_true (writer_class->begin (writer, &error)); + g_assert_no_error (error); + + g_autoptr(DConfChangeset) add = dconf_changeset_new (); + dconf_changeset_set (add, "/key", g_variant_new ("(s)", "value")); + writer_class->change (writer, add, NULL); + + g_assert_true (writer_class->commit (writer, &error)); + g_assert_no_error (error); + writer_class->end (writer); + + { + g_autoptr(DConfChangeset) db = read_db (db_filename); + g_autoptr(GVariant) value = NULL; + + g_assert_true (dconf_changeset_get (db, "/key", &value)); + g_assert_true (g_variant_equal (value, g_variant_new ("(s)", "value"))); + } + + g_assert_true (writer_class->begin (writer, &error)); + g_assert_no_error (error); + + g_autoptr(DConfChangeset) reset = dconf_changeset_new (); + dconf_changeset_set (reset, "/key", NULL); + writer_class->change (writer, reset, NULL); + + g_assert_true (writer_class->commit (writer, &error)); + g_assert_no_error (error); + writer_class->end (writer); + + { + g_autoptr(DConfChangeset) db = read_db (db_filename); + g_assert_false (dconf_changeset_get (db, "/key", NULL)); + } + + g_unlink (db_filename); + g_autofree gchar *lock = g_build_filename (fixture->dconf_dir, ".safe-add-delete.lock", NULL); + g_unlink (lock); + g_autofree gchar *rt = safe_runtime_db_filename (db_name); + g_unlink (rt); + g_autofree gchar *rt_safe_dir = g_build_filename (runtime_dir, "dconf-service", "safe", NULL); + g_rmdir (rt_safe_dir); + g_autofree gchar *rt_service_dir = g_build_filename (runtime_dir, "dconf-service", NULL); + g_rmdir (rt_service_dir); +} + +static void +test_safe_writer_rolls_back_persistent_db_on_runtime_commit_failure (Fixture *fixture, + gconstpointer test_data) +{ + const gchar *db_name = "safe-rollback"; + g_autoptr(DConfWriter) writer = NULL; + DConfWriterClass *writer_class; + g_autoptr(GError) error = NULL; + g_autofree gchar *db_filename = g_build_filename (fixture->dconf_dir, db_name, NULL); + g_autofree gchar *runtime_db_filename = safe_runtime_db_filename (db_name); + + g_autoptr(DConfChangeset) old_db = dconf_changeset_new_database (NULL); + dconf_changeset_set (old_db, "/key", g_variant_new ("(s)", "old")); + g_assert_true (dconf_gvdb_utils_write_file (db_filename, old_db, &error)); + g_assert_no_error (error); + + writer = DCONF_WRITER (dconf_writer_new (DCONF_TYPE_SAFE_WRITER, db_name)); + writer_class = DCONF_WRITER_GET_CLASS (writer); + + g_assert_true (writer_class->begin (writer, &error)); + g_assert_no_error (error); + + g_autoptr(DConfChangeset) change = dconf_changeset_new (); + dconf_changeset_set (change, "/key", g_variant_new ("(s)", "new")); + writer_class->change (writer, change, NULL); + + /* Make the runtime DB path not writable as a file target after begin(), + * so the persistent write succeeds and the parent/runtime commit fails. + */ + g_assert_cmpint (g_mkdir_with_parents (runtime_db_filename, 0700), ==, 0); + + g_assert_false (writer_class->commit (writer, &error)); + g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_ISDIR); + g_clear_error (&error); + + writer_class->end (writer); + + { + g_autoptr(DConfChangeset) db = read_db (db_filename); + g_autoptr(GVariant) value = NULL; + + g_assert_true (dconf_changeset_get (db, "/key", &value)); + g_assert_true (g_variant_equal (value, g_variant_new ("(s)", "old"))); + } + + g_unlink (db_filename); + g_autofree gchar *lock = g_build_filename (fixture->dconf_dir, ".safe-rollback.lock", NULL); + g_unlink (lock); + g_rmdir (runtime_db_filename); + g_autofree gchar *rt_safe_dir = g_build_filename (runtime_dir, "dconf-service", "safe", NULL); + g_rmdir (rt_safe_dir); + g_autofree gchar *rt_service_dir = g_build_filename (runtime_dir, "dconf-service", NULL); + g_rmdir (rt_service_dir); +} + int main (int argc, char **argv) { @@ -405,9 +542,13 @@ g_assert_true (g_setenv ("XDG_CONFIG_HOME", config_dir, TRUE)); g_test_message ("Using config directory: %s", config_dir); + runtime_dir = g_dir_make_tmp ("dconf-test-writer-runtime_XXXXXX", &local_error); + g_assert_no_error (local_error); + g_assert_true (g_setenv ("XDG_RUNTIME_DIR", runtime_dir, TRUE)); + g_test_message ("Using runtime directory: %s", runtime_dir); + /* Log handling so we don’t abort on the first g_warning(). */ g_log_set_writer_func (log_writer_cb, NULL, NULL); - g_test_add ("/writer/basic", Fixture, NULL, set_up, test_writer_basic, tear_down); g_test_add ("/writer/begin/missing", Fixture, NULL, set_up, @@ -426,6 +567,10 @@ test_writer_commit_empty_changes, tear_down); g_test_add ("/writer/commit/redundant_change/2", Fixture, NULL, set_up, test_writer_commit_real_changes, tear_down); + g_test_add ("/safe-writer/commit/persistence", Fixture, NULL, set_up, + test_safe_writer_persists_add_and_delete, tear_down); + g_test_add ("/safe-writer/commit/rollback", Fixture, NULL, set_up, + test_safe_writer_rolls_back_persistent_db_on_runtime_commit_failure, tear_down); retval = g_test_run (); @@ -434,5 +579,10 @@ g_assert_cmpint (g_rmdir (config_dir), ==, 0); g_clear_pointer (&config_dir, g_free); + /* Clean up the runtime dir. */ + g_unsetenv ("XDG_RUNTIME_DIR"); + g_assert_cmpint (g_rmdir (runtime_dir), ==, 0); + g_clear_pointer (&runtime_dir, g_free); + return retval; } ++++++ dconf.obsinfo ++++++ name: dconf version: 51.0 mtime: 1789400279 commit: 264e6598863fc1d9ad6edb4c58d86f82ea669927
