summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml81
-rw-r--r--COPYING510
-rw-r--r--HACKING91
-rw-r--r--NEWS903
-rw-r--r--README71
-rw-r--r--bin/completion/dconf44
-rw-r--r--bin/dconf.c1214
-rw-r--r--bin/meson.build23
-rw-r--r--client/dconf-client.c697
-rw-r--r--client/dconf-client.h88
-rw-r--r--client/dconf.deps1
-rw-r--r--client/dconf.h28
-rw-r--r--client/dconf.vapi70
-rw-r--r--client/meson.build79
-rw-r--r--common/dconf-changeset.c845
-rw-r--r--common/dconf-changeset.h75
-rw-r--r--common/dconf-enums.h42
-rw-r--r--common/dconf-error.c52
-rw-r--r--common/dconf-paths.c253
-rw-r--r--common/dconf-paths.h40
-rw-r--r--common/meson.build46
-rw-r--r--dconf.doap50
-rw-r--r--docs/dconf-docs.xml61
-rw-r--r--docs/dconf-overview.xml214
-rw-r--r--docs/dconf-sections.txt66
-rw-r--r--docs/dconf-service.xml63
-rw-r--r--docs/dconf-tool.xml224
-rw-r--r--docs/dconf.types1
-rw-r--r--docs/meson.build53
-rw-r--r--engine/dconf-engine-mockable.c39
-rw-r--r--engine/dconf-engine-mockable.h30
-rw-r--r--engine/dconf-engine-profile.c330
-rw-r--r--engine/dconf-engine-profile.h30
-rw-r--r--engine/dconf-engine-source-file.c77
-rw-r--r--engine/dconf-engine-source-private.h31
-rw-r--r--engine/dconf-engine-source-service.c95
-rw-r--r--engine/dconf-engine-source-system.c81
-rw-r--r--engine/dconf-engine-source-user.c96
-rw-r--r--engine/dconf-engine-source.c141
-rw-r--r--engine/dconf-engine-source.h66
-rw-r--r--engine/dconf-engine.c1496
-rw-r--r--engine/dconf-engine.h167
-rw-r--r--engine/meson.build46
-rw-r--r--gdbus/dconf-gdbus-filter.c310
-rw-r--r--gdbus/dconf-gdbus-thread.c385
-rw-r--r--gdbus/meson.build27
-rwxr-xr-xgsettings/abicheck.sh4
-rw-r--r--gsettings/dconfsettingsbackend.c269
-rw-r--r--gsettings/meson.build32
-rw-r--r--gvdb/README12
-rw-r--r--gvdb/gvdb-builder.c (renamed from gvdb-builder.c)0
-rw-r--r--gvdb/gvdb-builder.h (renamed from gvdb-builder.h)0
-rw-r--r--gvdb/gvdb-format.h (renamed from gvdb-format.h)0
-rw-r--r--gvdb/gvdb-reader.c (renamed from gvdb-reader.c)0
-rw-r--r--gvdb/gvdb-reader.h (renamed from gvdb-reader.h)0
-rw-r--r--gvdb/gvdb.doap (renamed from gvdb.doap)0
-rw-r--r--gvdb/meson.build27
-rw-r--r--meson.build85
-rw-r--r--meson_options.txt4
-rw-r--r--meson_post_install.py9
-rw-r--r--service/ca.desrt.dconf.service.in3
-rw-r--r--service/ca.desrt.dconf.xml23
-rw-r--r--service/dconf-blame.c190
-rw-r--r--service/dconf-blame.h39
-rw-r--r--service/dconf-gvdb-utils.c219
-rw-r--r--service/dconf-gvdb-utils.h33
-rw-r--r--service/dconf-keyfile-writer.c529
-rw-r--r--service/dconf-service.c346
-rw-r--r--service/dconf-service.h34
-rw-r--r--service/dconf-shm-writer.c45
-rw-r--r--service/dconf-writer.c428
-rw-r--r--service/dconf-writer.h90
-rw-r--r--service/main.c35
-rw-r--r--service/meson.build63
-rw-r--r--shm/dconf-shm-mockable.c40
-rw-r--r--shm/dconf-shm-mockable.h31
-rw-r--r--shm/dconf-shm.c155
-rw-r--r--shm/dconf-shm.h38
-rw-r--r--shm/meson.build33
-rw-r--r--tests/changeset.c589
-rw-r--r--tests/client.c246
-rw-r--r--tests/dbus.c544
-rw-r--r--tests/dconf-mock-dbus.c99
-rw-r--r--tests/dconf-mock-gvdb.c211
-rw-r--r--tests/dconf-mock-shm.c124
-rw-r--r--tests/dconf-mock.h37
-rw-r--r--tests/engine.c2100
-rw-r--r--tests/gvdb.c438
-rw-r--r--tests/gvdbs/empty_gvdbbin0 -> 32 bytes
-rw-r--r--tests/gvdbs/example_gvdbbin0 -> 246 bytes
-rw-r--r--tests/gvdbs/example_gvdb.big-endianbin0 -> 246 bytes
-rw-r--r--tests/gvdbs/file_empty0
-rw-r--r--tests/gvdbs/file_too_small1
-rw-r--r--tests/gvdbs/invalid_header1
-rw-r--r--tests/gvdbs/nested_gvdbbin0 -> 371 bytes
-rw-r--r--tests/meson.build61
-rw-r--r--tests/paths.c98
-rw-r--r--tests/profile/broken-profile6
-rw-r--r--tests/profile/colourful13
-rw-r--r--tests/profile/dos2
-rw-r--r--tests/profile/empty-profile0
-rw-r--r--tests/profile/gdm2
-rw-r--r--tests/profile/many-sources10
-rw-r--r--tests/profile/no-newline-longline1
-rw-r--r--tests/profile/test-profile1
-rw-r--r--tests/profile/will-never-exist1
-rw-r--r--tests/shm.c175
-rwxr-xr-xtests/test-dconf.py817
-rw-r--r--tests/tmpdir.c53
-rw-r--r--tests/tmpdir.h9
-rw-r--r--tests/writer.c233
-rwxr-xr-xtrim-lcov.py53
112 files changed, 18173 insertions, 0 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..6b6cebc
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,81 @@
+image: debian:unstable
+
+before_script:
+ - apt update -qq
+ - apt install -y -qq build-essential meson pkg-config gtk-doc-tools
+ libxml2-utils gobject-introspection dbus dbus-x11
+ libdbus-1-dev libgirepository1.0-dev libglib2.0-dev
+ bash-completion lcov valac
+ - export LANG=C.UTF-8
+
+stages:
+ - build
+ - test
+ - deploy
+
+variables:
+ MESON_TEST_TIMEOUT_MULTIPLIER: 2
+
+build-job:
+ stage: build
+ script:
+ - meson -Db_coverage=true -Dman=true -Dgtk_doc=true --buildtype debug --werror _build .
+ - ninja -C _build all dconf-doc
+ except:
+ - tags
+ artifacts:
+ when: on_failure
+ name: "dconf-_${CI_COMMIT_REF_NAME}"
+ paths:
+ - "_build/meson-logs"
+
+test:
+ stage: test
+ script:
+ - meson _build . -Db_coverage=true -Dman=true -Dgtk_doc=true
+ - 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"
+ - 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"
+ coverage: '/^\s+lines\.+:\s+([\d.]+\%)\s+/'
+ except:
+ - tags
+ artifacts:
+ when: always
+ name: "dconf-_${CI_COMMIT_REF_NAME}"
+ paths:
+ - "_build/meson-logs"
+ - "_coverage"
+
+# FIXME: Run gtkdoc-check when we can. See:
+# https://github.com/mesonbuild/meson/issues/3580
+
+dist-job:
+ stage: build
+ only:
+ - tags
+ script:
+ - meson --buildtype release _build .
+ - ninja -C _build dist
+ artifacts:
+ paths:
+ - "_build/meson-dist/dconf-*.tar.xz"
+
+pages:
+ stage: deploy
+ only:
+ - master
+ script:
+ - meson -Db_coverage=true -Dgtk_doc=true _build .
+ - 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"
+ - 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"
+ - mkdir -p public/
+ - mv _build/docs/html/ public/docs/
+ - mv _coverage/ public/coverage/
+ artifacts:
+ paths:
+ - public \ No newline at end of file
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..2d2d780
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,510 @@
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations
+below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it
+becomes a de-facto standard. To achieve this, non-free programs must
+be allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control
+compilation and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at least
+ three years, to give the same user the materials specified in
+ Subsection 6a, above, for a charge no more than the cost of
+ performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply, and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License
+may add an explicit geographical distribution limitation excluding those
+countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms
+of the ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library.
+It is safest to attach them to the start of each source file to most
+effectively convey the exclusion of warranty; and each file should
+have at least the "copyright" line and a pointer to where the full
+notice is found.
+
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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.1 of the License, 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, write to the Free Software
+ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or
+your school, if any, to sign a "copyright disclaimer" for the library,
+if necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James
+ Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/HACKING b/HACKING
new file mode 100644
index 0000000..d071e03
--- /dev/null
+++ b/HACKING
@@ -0,0 +1,91 @@
+dconf is split into a large number of small modules. This is required
+because of the client/server architecture as well as the wide variety of
+situations that dconf is used in on the client side.
+
+gvdb/:
+
+ This code implements the GVDB file format used for the on-disk
+ database. GVDB is shared with a number of other users and lives in a
+ separate 'gvdb' module on git.gnome.org.
+
+ Changes should never be made to this directory. Instead, they should
+ be made against the 'gvdb' module and merged using git.
+
+ The code is split into a reader and a writer (builder).
+
+ This directory produces two libraries: libgvdb.a and libgvdb-shared.a.
+ They are exactly the same, except that libgvdb-shared.a was compiled
+ with -fPIC.
+
+common/:
+
+ Sources in this directory are used in both the dconf-service and
+ client-side library implementations.
+
+ This directory produces two libraries: libdconf-common.a and
+ libdconf-common-shared.a. They are exactly the same, except that
+ libdconf-common-shared.a was compiled with -fPIC.
+
+engine/:
+
+ This directory contains most of the core logic for implementing the
+ client-side of dconf.
+
+ The engine code requires (but does not contain) glue code for speaking
+ to D-Bus. All users of the engine must therefore include a module
+ that implements this glue.
+
+ The engine also requires gvdb.
+
+ This directory produces libdconf-engine.a and its -shared variant.
+
+gdbus/:
+
+ This directory contains the glue code for dconf over GDBus.
+
+ There are two implementations of this code: a threaded approach and an
+ approach based on GDBus filter functions. The threaded one is in use
+ by default, but both are built for testing purposes.
+
+ This directory produces a library for each backend:
+ libdconf-gdbus-thread.a and libdconf-gdbus-filter.a, plus their
+ -shared variants.
+
+client/:
+
+ This is the standard GObject client-side library used for direct access to
+ dconf. It uses the GDBus glue from the gdbus/ directory above.
+
+ This directory produces the libdconf.so shared library as well as
+ libdconf-client.a which is used for testing.
+
+gsettings/:
+
+ This is the GSettings backend for dconf. It also uses GDBus.
+
+ This directory produces the libdconfsettings.so GIOModule.
+
+dbus-1/:
+
+ This directory contains a client-side library based on libdbus-1. It
+ also contains the D-Bus glue code for libdbus-1 (since it is the only
+ client-side library that is using it).
+
+ This directory produces the libdconf-dbus-1.so shared library.
+
+ It also produces libdconf-libdbus-1.a (containing the D-Bus glue) for
+ testing purposes, and its -shared variant.
+
+bin/:
+
+ This is the 'dconf' commandline tool. It uses the library from
+ client/ above.
+
+editor/:
+
+ This is the graphical dconf-editor. It also uses the client/ library.
+
+service/:
+
+ This is the dconf-service required for any client side library to do
+ writes.
diff --git a/NEWS b/NEWS
new file mode 100644
index 0000000..8efda53
--- /dev/null
+++ b/NEWS
@@ -0,0 +1,903 @@
+Changes in dconf 0.32.0
+=======================
+
+ - No changes since 0.31.92
+
+Changes in dconf 0.31.92
+========================
+
+ - bin: Add an option to ignore changes to locked keys during load
+ (Tomasz Miąsko; #1; !43)
+
+ - tests: Use more concise key and value (Tomasz Miąsko; #1; !43)
+
+ - tests: shm: fix pwrite wrapper with -D_FILE_OFFSET_BITS=64 (Ben Wolsieffer; !37)
+
+ - tests: replace usage of dlsym with separate modules containing functions that need to be mocked out
+ (Daniel Playfair Cal; !37)
+
+ - tests: Avoid using real system bus during tests
+ (Tomasz Miąsko; #51; !45)
+
+Changes in dconf 0.31.2
+=======================
+
+ - Bin: rewrite dconf utility in C. Updates are no longer conditional on
+ the mtime of the database directory and files. Help information is shown
+ on erroneous usage, but not otherwise. (Tomasz Miąsko; !39)
+
+ - build: Make dconf client vapi file installation optional, and thus the
+ Vala dependency optional (still built by default) (Tomasz Miąsko; !40)
+
+ - bin: Add a new database directory argument for the update command
+ (Tomasz Miąsko, Takao Fujiwara; !39, !41)
+
+Changes in dconf 0.31.1
+=======================
+
+ - Engine: Fix memory leak when subscribing to a path. (Guido Günther; !25)
+
+ - CI: Always store test artifacts so we always get code coverage results.
+ (Philip Withnall; !32)
+
+ - Sort output of list and dump commands. (Tomasz Miąsko; !31)
+
+ - Tests: Add integration tests for dconf and dconf-service running within
+ a separate D-Bus session and clean XDG_RUNTIME_DIR and XDG_CONFIG_HOME.
+ (Tomasz Miąsko; !31)
+
+ - Engine: Coalesce pending writes into a single changeset.
+ (Tomasz Miąsko; !30)
+
+ - Consistently validate the number of arguments.
+ Add optional directory argument for update command.
+ (Tomasz Miąsko; !33)
+
+ - Tests: Add further integration tests (Tomasz Miąsko; !33)
+
+ - Check mtimes of files when updating databases. (Marek Kasik; !27; #11)
+
+ - Indicate update failure with non-zero exit code. Consistently handle
+ invalid configuration in update. (Tomasz Miąsko; !34; #42)
+
+ - Replace Bugzilla by Gitlab URL in DOAP file. (Andre Klapper)
+
+ - Tests: Add test for key paths locked in system databases
+ (Tomasz Miąsko; !35)
+
+Changes in dconf 0.30.0
+=======================
+
+None.
+
+Changes in dconf 0.29.2
+=======================
+
+ - Service: When corrupt GVDB files are found, they are now
+ transparently backed up and replaced with an empty database.
+ (Philip Withnall, !8)
+ - Replace all hard-coded /etc path with sysconfdir. This is useful for
+ JHBuild environments and systems that don't want to use /etc/dconf.
+ (Ting-Wei Lan; !21, #739229)
+ - Engine: Change overflow thresholds in subscription counts from
+ GMAXUINT32 to GMAXUINT (Daniel Playfair Cal; !20)
+ - Change meson flag used to build Gtk-Doc from enable-gtk-doc to
+ gtk_doc (Daniel Playfair Cal; !19)
+ - Declare libdconf_service as a dependency to fix the build in
+ massively parallelised environments (Emmanuele Bassi; !22)
+
+Changes in dconf 0.29.1
+=======================
+
+ - Engine: track in progress watch handles to avoid spurious changed
+ signals for the root path. Subscription requests are no longer sent
+ if the engine is already subscribed to the given path. In the case
+ that some value changes while a subscription request is in progress,
+ a changed signal is only sent for the path being subscribed to
+ instead of the root path. (Daniel Playfair Cal; !1, !5, #790640)
+
+ - Engine: fix deadlock which occured when using the engine from libsoup
+ within flatpak by extending the existing workaround for Glib !541, aka
+ #674885. GSocket and various other GObject types are now also
+ initialised in the main thread. (Owen Taylor; !15)
+
+ - Add transfer annotations to the GTK-Doc strings for functions in the
+ dconf_changeset_* and dconf_client_* namespaces (Xavier Claessens,
+ Philip Withnall; !9, #758903)
+
+ - Update GVDB subtree from GVDB master, containing mostly documentation
+ improvements (Philip Withnall; !17)
+
+ - dconf-update.vala: correct error message grammar (Kenyon Ralph; !6)
+
+ - Various meson related improvements (Iñigo Martínez; !11)
+
+ - Add GitLab CI (Philip Withnall; !10)
+
+ - Service: Port from the deprecated g_type_class_add_private() to
+ G_ADD_PRIVATE() (Philip Withnall; !7)
+
+ - Add Daniel Playfair Cal and Philip Withnall as maintainers (Philip
+ Withnall)
+
+
+Changes in dconf 0.28.0
+=======================
+
+ - Update README
+
+Changes in dconf 0.27.1
+=======================
+
+ - Port to meson build system (#784910)
+
+Changes in dconf 0.26.1
+========================
+
+ - Work around a deadlock in GObject type initialization (#674885)
+
+Changes in dconf 0.26.0
+========================
+
+None.
+
+Changes in dconf 0.25.1
+========================
+
+ - the libdbus-1 backend has been removed. dconf now always uses GDBus.
+
+ - support has been added for system administration frameworks to set up
+ the dconf profile via a file placed in the XDG_RUNTIME_DIR or in
+ /run/dconf/. In the case of the file in /run/dconf/, it is not
+ possible to modify the proile, even via the DCONF_PROFILE environment
+ variable, which makes it slightly more difficult to evade lockdown.
+
+ - directory resets are now implemented properly in DConfChangeset which
+ means that reading a subkey through a changeset that contains a reset
+ for a parent directory of that key will return TRUE with a NULL value
+
+ - a new API dconf_client_read_full() has been added which allows
+ reading the user value, the default value, or querying what the
+ effective value would be if a changeset were to be applied.
+
+ - a new API has been added for listing the locks that are in effect:
+ dconf_client_list_locks()
+
+ - DConfClient has a new "writability-changed" signal
+
+ - support for reading default values and listing locks have been added
+ to the dconf commandline tool
+
+ - support for g_autoptr() has been added for DConfClient and
+ DConfChangeset
+
+ - the handling of reading of default values via GSettings has been made
+ more efficient. More major changes to the GSettings backend are
+ expected in the near future.
+
+Changes in dconf 0.24.0
+========================
+
+The version number was increased and a new entry was added to the NEWS.
+
+Changes in dconf 0.23.2
+========================
+
+ - remove dconf-editor manpage (accidentally missed during the split)
+
+ - fix whitespace issues in 'dconf --help'
+
+Changes in dconf 0.23.1
+========================
+
+ - dconf-editor is now in a separate package
+
+ - portability improvements
+
+Changes in dconf 0.22.0
+========================
+
+ - fix handling of floating point keys in editor
+
+ - update appdata for renamed desktop file
+
+ - minor doap changes
+
+Translations:
+ French
+ Indonesian
+ Lithuanian
+ Hungarian
+ Catalan (Valencian)
+ Korean
+ Traditional Chinese
+ Spanish
+ Brazilian Portuguese
+ Galician translations
+ Catalan
+ Basque language
+ Danish
+ Norwegian bokmål
+ Greek
+ Czech
+ Slovenian
+ Swedish
+ Polish
+ Latvian
+ Hebrew
+ Russian
+ German
+ Assamese
+ Serbian
+
+Changes in dconf 0.21.0
+========================
+
+ - editor desktop file renamed to ca.desrt.dconf-editor to take advantage of
+ D-Bus activation
+
+ - prevent the service from being released more than once if we receive
+ multiple signals (which caused a hang)
+
+Translations:
+ Indonesian
+ Greek
+ Swedish
+
+Changes in dconf 0.20.0
+========================
+
+Czech translation updated.
+
+Changes in dconf 0.19.92
+=========================
+
+ - depend on automake 1.11.2
+
+ - stop using ACLOCAL_FLAGS
+
+ - depend on released version of Vala (0.18.0)
+
+Changes in dconf 0.19.91
+=========================
+
+ - fix an unlikely failure in the fuzz testing of gvdb
+
+ - fix a thread safety issue with file-db
+
+Changes in dconf 0.19.90
+=========================
+
+ - dconf compile: always write little endian
+
+ - file-db: don't install match rules on no bus (fixes gdbus assertion)
+
+ - update dconf(1) manpage for 'dconf compile'
+
+ - fix 'make clean' on FreeBSD
+
+ - editor: provide appdata
+
+Translations updates:
+ Ukrainian translation
+ Aragonese translation
+ Chinese
+
+Changes in dconf 0.19.3
+========================
+
+ - quite a lot of test coverage improvements
+
+ - Add a proper DCONF_ERROR error domain
+
+ - suppress GLib deprecation warnings during build
+
+ - engine: issue warnings about missing files only once per source
+
+ - engine: grok the WritabilityNotify signal from D-Bus
+
+ - gsettings: handle writability changes correctly
+
+ - engine: assorted fixes for issues found during testing
+
+ - portability: only link to -ldl if it is required
+
+ - add support for 'file-db' to profiles: this is an absolute path to a
+ immutable dconf database file
+
+ - add support for finding profiles in XDG_DATA_DIRS if they are not in
+ /etc
+
+ - add 'dconf compile' command for building dconf databases from
+ keyfiles in arbitrary locations (like from the build system of a
+ project that may want to install a file-db)
+
+ - editor: add a section separator to the app menu for consistency
+
+Changes in dconf 0.19.2
+========================
+
+ - Add support for the new GSettingsBackend.read_user_value() API
+
+ - bump GLib version depend accordingly (2.39.1)
+
+Translations updates:
+ Greek
+ Catalan (Valencian)
+ Simplified Chinese
+ Persian
+
+Changes in dconf 0.18.0
+========================
+
+Translations updates:
+ Basque
+ Danish
+ Japanese
+ Korean
+ Portuguese
+ Punjabi
+
+Changes in dconf 0.17.1
+========================
+
+Documentation updates, including better coverage of profiles.
+
+Translations updates:
+ Assamese
+ Belarusian
+ Catalan
+ Dutch
+ Estonian
+ Finnish
+ French
+ German
+ Hungarian
+ Indonesian
+ Italian
+ Korean
+ Latvian
+ Polish
+ Russian
+ Serbian
+ Tajik
+ Thai
+ Traditional Chinese
+
+Changes in dconf 0.17.0
+========================
+
+Fix a semi-serious thread safety issue:
+
+ https://bugzilla.gnome.org/show_bug.cgi?id=703073
+
+Some improvements to the editor:
+
+ - fix some compile warnings
+ - use gresource
+ - cancel searches on escape
+
+Translation updates:
+ Brazilian Portuguese
+ Czech
+ Esperanto
+ Friulian
+ Galician
+ German
+ Hebrew
+ Indonesian
+ Lithuanian
+ Malayalam
+ Norwegian bokmål
+ Slovak
+ Slovenian
+ Spanish
+ Tajik
+
+Changes in dconf 0.16.0
+========================
+
+This release one small fix and many translation updates.
+
+ - Set G_LOG_DOMAIN so that 'dconf' shows in GLib log messages
+
+Translation updates:
+ Assamese
+ Basque
+ Belarusian
+ Brazilian Portuguese
+ Catalan
+ Czech
+ Danish
+ Estonian
+ Finnish
+ French
+ German
+ Greek
+ Hindi
+ Korean
+ Latvian
+ Persian
+ Portuguese
+ Punjabi
+ Russian
+ Tadjik
+ Tamil
+ Uyghur
+ Valencian
+
+Changes in dconf 0.15.3
+========================
+
+This release more or less completes the rewriting of dconf-service.
+Support for backends are now fully-realised (and we have a couple of
+them as proof). Many small bugs (some with serious symptoms) have been
+fixed vs. the last release.
+
+ - fix problem related to empty changesets (caused by empty 'dconf
+ load', for example). These are now handled properly on the server
+ side but also forbidden in the client libraries.
+
+ - new icons: hicolor icons have been updated and a HighContrast set
+ added
+
+ - loosen some assertions on the order of messages being returned from
+ the service. D-Bus violates this order in the case that the service
+ crashes and that's no reason to take the client with it as well.
+
+ - improve autogen.sh non-bash compatibility
+
+ - add some backends to the dconf-service (that were 'considered' as per
+ the last NEWS). The first backend is a null backend that allows for
+ a temporary database tied to the lifetime of the session, possibly
+ useful for testing.
+
+ - also add a new 'keyfile' backend for the service. This is primarily
+ intended to be used with NFS (where use of mmap is not safe)
+
+ - add a mkdir() in the case of an empty home directory where
+ ~/.config/dconf does not already exist
+
+ - fix crashes in the service caused by failures to write (including
+ failures caused by the above issue being overlooked)
+
+ - work around an issue with a testcase hanging and failing due to a
+ GLib change to the order in which testcases are run
+
+ - running autoreconf on tarballs should now work
+
+Translations updated:
+ Aragonese
+ Friulian
+ Galician
+ Greek
+ Hebrew
+ Hungarian
+ Italian
+ Lithuanian
+ Norwegian bokmål
+ Polish
+ Serbian
+ Slovak
+ Slovenian
+ Spanish
+ Thai
+ Uyghur
+
+Bugs closed:
+ 595579 support NFS
+ 663961 Cannot build without a git checkout
+ 673834 dconf commandline tool doesn't initialise the locale
+ 686998 add keywords to the desktop file
+ 687120 gnome-ostree-3.8 triggers expected && oc->change == expected assertion
+ 687310 dconf-editor should install a highcontrast app icon
+ 689136 Crash in dconf_writer_real_end(): change is NULL (when $XDG_CONFIG_HOME/dconf does not exist)
+ 690316 writer: Fix typo causing segfaults on service shutdown
+ 690477 Can't build with gnome-apps-3.6 module set
+ 691013 Crash on missing ~/.config/dconf and clear pointer
+ 692144 needs a nicer icon
+ 692186 build: autogen.sh uses /bin/sh but depends on advanced shell redirection shorthands
+
+Changes in dconf 0.15.2
+========================
+
+ - update to the new GVDB API (GBytes based) and refactor a bit
+
+ - add a new type of "database mode" DConfChangeset and use it
+
+ - the service has been substantially rewritten
+ - using gdbus-codegen
+ - cache the file contents instead of read/modify/write each time
+ - new DConfWriter abstraction allows considering support for backends
+ - uses only POSIX IO (read and write syscalls) for database access
+ - workaround mmap() bug on OpenBSD
+
+ - editor:
+ - Fix search order of schema dirs
+ - Translate summary and description
+
+Translations:
+
+ - Indonesian
+ - Slovak
+
+Changes in dconf 0.15.1
+========================
+
+ - add --disable-man
+
+ - fix reading default values in GSettings backend
+
+ - editor:
+ - support schema overrides
+ - search improvements (search directories, values, descriptions)
+ - correctly get text from schema descriptions
+ - only write window state settings on exit and support fullscreen state
+
+ - dconf_rebuilder_get_parent: don't leak parent_name
+
+Translations:
+
+ - Arabic
+ - Belarusian
+ - Bengali
+ - Brazilian Portuguese
+ - British English
+ - Bulgarian
+ - Catalan
+ - Catalan (Valencian)
+ - Czech
+ - Danish
+ - Estonian
+ - Finnish
+ - French
+ - German
+ - Hebrew
+ - Hindi
+ - Hungarian
+ - Indonesian
+ - Italian
+ - Japanese
+ - Korean
+ - Latvian
+ - Lithuanian
+ - Malayalam
+ - Marathi
+ - Norwegian bokmål
+ - Persian
+ - Polish
+ - Portuguese
+ - Russian
+ - Simplified Chinese
+ - Slovenian
+ - Spanish
+ - Swedish
+ - Tamil
+ - Telugu
+ - Thai
+ - Vietnamese
+
+Changes in dconf 0.13.90
+=========================
+
+The only change in this release is the addition of many translations for
+dconf-editor:
+
+ - Traditional Chinese
+ - Serbian
+ - Ukrainian
+ - Punjabi
+ - Greek
+ - Galician
+ - Spanish
+ - Assamese
+ - Polish
+ - Hungarian
+
+Changes in dconf 0.13.5
+========================
+
+ - the service no longer attempts to migrate the primary dconf database from
+ its (years) old path ~/.config/dconf to the new ~/.config/dconf/user
+
+ - fixes for profile file parsing problems
+
+ - fix obscure race in GDBus handling code that could result in
+ out-of-order message delivery
+
+ - editor:
+ - clean up some messages and mark strings for translations
+ - enable NLS
+ - add Esperanto translation
+
+ - add manpages for dconf(7), dconf(1), dconf-service and dconf-editor
+
+Changes in dconf 0.13.4 (since 0.13.0)
+=======================================
+
+ - extensively refactored -- watch for bugs
+
+ - now depends on GLib 2.33.3 for g_slist_copy_deep() and
+ g_clear_pointer() with workaround for GCC
+
+ - API of libdconf has changed; soname bumped to libdconf.so.1
+
+ - installed header layout has changed
+
+ - there are changes to the API used between the client and the service.
+ New clients will only talk with the new service (so make sure to
+ restart it). Old clients (ie: already-running programs) will
+ continue to be able to talk to the new service version.
+
+ - there is a now a fairly decent test suite but it requires a full
+ D-Bus environment to run in, so it may make sense to avoid 'make
+ check' on builders for the time being
+
+Changes in dconf 0.13.0
+=======================
+
+ - now requiring Vala 0.18 (ie: at least 0.17.0)
+
+ - editor: use GtkApplication and GMenu
+ - editor: support searching for keys
+
+ - dconf cli: call setlocale() on startup
+
+Changes in dconf 0.11.7
+=======================
+
+ - engine: don't leak the lock table if a system database changes while
+ a program is running
+
+ - dbus-1: call g_slist_remove_link instead of g_slist_remove
+
+ - editor: set 'wrap' property on default value label
+
+ - dconf update:
+
+ - code cleanup, better error handling
+ - read keyfiles in a defined order (later files take precedence)
+ - don't issue warnings if a key is defined in multiple files
+
+Changes in dconf 0.11.6
+=======================
+
+ - new DCONF_BLAME mode for debugging spurious dconf writes at login
+
+ - stop hardcoding the assumption that there will be exactly one user
+ database followed by zero or more system databases
+
+ - allow DCONF_PROFILE to specify a profile file by absolute path
+
+ - dbus-1: fix multiple-key change notifications
+
+ - autogen.sh can now be called from out of tree
+
+Changes in dconf 0.11.5
+=======================
+
+ - do not install non-standard icon sizes
+
+ - fix a bug with getting default values when no profile is in use
+
+ - some general code cleanups on the client side
+
+ - improve Makefile compatibility: avoid using 'echo -e'
+
+Changes in dconf 0.11.3
+=======================
+
+ - works with (and only with) Vala 0.15.1
+
+ - we now have an icon for dconf-editor
+
+Changes in dconf 0.11.2
+=======================
+
+ - many bugfixes and improvements to the editor, most notably porting to
+ GtkGrid to avoid the GtkTable layout bug that was causing size to be
+ incorrectly allocated
+
+ - fix a crasher due to invalid string index of -1
+
+Changes in dconf 0.10.0
+=======================
+
+The version number was increased and a new entry was added to the NEWS.
+
+Changes in dconf 0.9.1
+======================
+
+ - give a g_warning() on failure to communicate with service
+
+ - remove unworking 'set lock' call from dconf API and commandline tool
+
+ - add code to exit gracefully on receipt of SIGINT, SIGHUP, SIGTERM
+
+ - remove "service function" logic; always use the XDG runtime directory
+
+Changes in dconf 0.9.0
+======================
+
+There has been an extremely minor incompatible change in the D-Bus API
+of dconf this release. From a practical standpoint, this change will
+have no effect. However, it serves as a reminder that the dconf D-Bus
+API is private and can change from version to version (and will likely
+change in the future).
+
+As such, it is appropriate for those packaging dconf to kill all running
+instances of dconf ('killall dconf-service') as part of their postinst
+for the package. It will be dbus-activated again on the next use.
+
+ - support loading/storing of maybe types in dconf
+
+ - remove NFS detection hackery and rely on XDG runtime dir
+
+ - add proper support for change notification to DConfClient
+
+ - commandline tool improvements
+
+ - reset: reset keys or entire subpaths
+
+ - dump: dump entire subpaths to keyfile format
+
+ - load: load them back again (maybe at a different path)
+
+ - watch: actually works now
+
+ - editor improvements
+
+ - keys now change in editor when changed from outside
+
+ - support for flags
+
+ - show dconf-editor in applications list
+
+ - work around incompatible Vala bindings changes with an #if
+
+ - don't install the bash completion script as executable
+
+ - fix a warning caused by reusing a GError variable
+
+ - other small fixes
+
+
+Changes in dconf 0.7.5
+======================
+
+This release corrects a serious flaw in the previous release: crashing
+if the database did not already exist.
+
+It also contains many fixes and improvements to the dconf-editor,
+including use of GSettings to store the window geometry.
+
+This is the final release before 0.8.0 which will become the first
+release in a new stable series. Feature development will continue on
+'master' toward 0.9 past that point.
+
+Changes in dconf 0.7.4
+======================
+
+Changes in this version:
+
+ - #648949: multithreading issue fixed (which actually affects all
+ GSettings-using programs since dconf is used from a helper thread in
+ that case)
+
+ - dconf commandline tool is vastly more friendly now
+
+ - no more aborting on unrecognised arguments
+
+ - proper help
+
+ - bash completion support
+
+ - support for sysadmin lockdown
+
+ - the editor now properly reads installed enum xml files
+
+Changes in dconf 0.7.3
+======================
+
+This release consists almost entirely of fixes made by Robert to
+dconf-editor. A few other trivial build fixes are included as well
+(bumping library version dependencies to match reality, etc).
+
+Changes in dconf 0.7.2
+======================
+
+This is entirely a cleanup/fixes release. Some fixes here to make the
+increasingly-strict toolchain happy, and also some fixes for some
+crashers in the GSettings backend and service.
+
+ - remove some unused variables (new GCC gives a warning: #640566, another)
+ - add a mutex to fix multi-threading issue (#640611)
+ - don't crash if we have no D-Bus
+ - clean up symbol exports
+ - fix a crash in the service when using 'reset'
+ - drop old linker options that were for libtool
+
+Changes in dconf 0.7.1
+======================
+
+The last release contained a few problems that caused build failures on
+some strict linkers. Those should be fixed now.
+
+Changes in dconf 0.7
+====================
+
+ - new library to use dconf with libdbus-1
+ - quite a lot of improvements and bug-fixes in dconf-editor, thanks to
+ Robert Ancell
+ - some bug fixes in the GSettings backend (crashers caused by use if
+ custom dconf profiles)
+ - some FreeBSD build fixes
+ - increased Vala dependency to 0.11.4 (required for dconf-editor fixes)
+
+Changes in dconf 0.6
+====================
+
+ - Rewrite a lot of the GSettings backend to reduce GDBus abuse. We use
+ our own worker thread now instead of trying to hijack GDBus's.
+ - disable gobject-introspection support for now
+ - drop support for GTK2 in dconf-editor
+ - Add a new torture-test case
+ - Increase dbus timeout to 2 minutes (in case the service is heavily loaded)
+ - Fix several memory leaks and other bugs
+
+Changes in dconf 0.5.1
+======================
+
+ - Adjust to GDBus API changes
+ - Send correct object path in Notify on WriteMany
+ - Use printf() and exit() instead of g_error() to avoid too many crash
+ reports for now
+ - Require gobject-introspection 0.9.5
+ - Require vala 0.9.5
+ - Make dconf-editor optional
+ - Drop libgee requirement for dconf-editor
+ - Tweak shared library installation to make ldconfig happy
+ - Bump .gir version to dconf-1.0
+ - Fix introspection build with recent gobject-introspection
+ - Minor bug fixes
+
+Changes in dconf 0.5
+=====================
+
+ - Include a dconf-editor
+ - drop libtool
+ - allow compiling without gobject-introspection
+ - autotools/build fixups
+ - repair some broken use of tags
+ - many updates for glib API changes
+ - fix a crasher in the service
+ - prefer 'automake-1.11' if it is in the path
+ - add support for layering (ie: for system defaults)
+ - add support for multiple writers in one service
+ - add a shared memory status region to indicate if the gvdb is stale
+ this prevents dconf from having to reopen the file each time
+ - support keyfile-maintained system settings (via 'dconf update')
+ - port client library and commandline tool to vala
+ - client library no longer has unimplemented calls
+ (except for write_many_async, due to bugs in valac)
+ - gtk-doc is now complete for the client library
+ - install our own vapi
+ - support 'reset' in the GSettingsBackend
+
+Changes in dconf 0.4
+=====================
+
+ - fix crashes when the dconf database doesn't yet exist
+ - add some incomplete gtk-doc
+ - use new GVDB (note: dconf file format has incompatibly changed)
+ - implement GSettings sync()
+ - use string tags instead of sequence numbers since it was impossible
+ to have universally unique sequence numbers
+ - theoretical support for sharing dconf databases between machines with
+ different byte orders
+ - fix bug where first write was not successful when auto-starting
+ service
+ - FreeBSD build fixes
+ - client API cleanups
+ - GObject introspection support
+ - enable automake silent rules by default for tarball builds
diff --git a/README b/README
new file mode 100644
index 0000000..d606d46
--- /dev/null
+++ b/README
@@ -0,0 +1,71 @@
+dconf is a simple key/value storage system that is heavily optimised for
+reading. This makes it an ideal system for storing user preferences
+(which are read 1000s of times for each time the user changes one). It
+was created with this usecase in mind.
+
+All preferences are stored in a single large binary file. Layering of
+preferences is possible using multiple files (ie: for site defaults).
+Lock-down is also supported. The binary file for the defaults can
+optionally be compiled from a set of plain text keyfiles.
+
+dconf has a partial client/server architecture. It uses D-Bus. The
+server is only involved in writes (and is not activated in the user
+session until the user modifies a preference). The service is
+stateless and can exit freely at any time (and is therefore robust
+against crashes). The list of paths that each process is watching is
+stored within the D-Bus daemon itself (as D-Bus signal match rules).
+
+Reads are performed by direct access (via mmap) to the on-disk database
+which is essentially a hashtable. For this reason, dconf reads
+typically involve zero system calls and are comparable to a hashtable
+lookup in terms of speed. Practically speaking, in simple non-layered
+setups, dconf is less than 10 times slower than GHashTable.
+
+Writes are not optimised at all. On some file systems, dconf-service
+will call fsync() for every write, which can introduce a latency of up
+to 100ms. This latency is hidden by the client libraries through a
+clever "fast" mechanism that records the outstanding changes locally (so
+they can be read back immediately) until the service signals that a
+write has completed.
+
+dconf mostly targets Free Software operating systems. It will
+theoretically run on Mac OS but there isn't much point to that (since
+Mac OS applications want to store preferences in plist files). It is
+not possible to use dconf on Windows because of the inability to rename
+over a file that's still in use (which is what the dconf-service does on
+every write).
+
+The dconf API is not particularly friendly. Because of this and the
+lack of portability, you almost certainly want to use some sort of
+wrapper API around it. The wrapper API used by Gtk+ and GNOME
+applications is GSettings, which is included as part of GLib. GSettings
+has backends for Windows (using the registry) and Mac OS (using property
+lists) as well as its dconf backend and is the proper API to use for
+graphical applications.
+
+dconf itself attempts to maintain a rather low profile with respect to
+dependencies. For the most part, there is only a dependency on GLib.
+
+With the exception of the bin/ directory, dconf is written in C using
+libglib. This is a very strong dependency due to the fact that dconf's
+type system is GVariant.
+
+The dconf-service has a dependency on libgio, as do the client libraries
+that make use of GDBus (and the utilities that make use of those
+libraries).
+
+The standard client library is libdconf (in client/). If you can't use
+GSettings then you should probably want to use this next.
+
+There is also a libdbus-1 based library. It does not depend on libgio,
+but still depends on libglib. It is not recommended to use this library
+unless you have a legacy dependency on libdbus-1 (such as in Qt
+applications).
+
+Installing dconf follows the typical meson dance:
+
+ meson builddir
+ ninja -C builddir
+ ninja -C builddir install
+
+If you plan to contribute to dconf, please see the HACKING file.
diff --git a/bin/completion/dconf b/bin/completion/dconf
new file mode 100644
index 0000000..22353b7
--- /dev/null
+++ b/bin/completion/dconf
@@ -0,0 +1,44 @@
+
+# Check for bash
+[ -z "$BASH_VERSION" ] && return
+
+####################################################################################################
+
+__dconf() {
+ local choices
+
+ case "${COMP_CWORD}" in
+ 1)
+ choices=$'help \nread \nlist \nwrite \nreset \nupdate \nlock \nunlock \nwatch \ndump \nload '
+ ;;
+
+ 2)
+ case "${COMP_WORDS[1]}" in
+ help)
+ choices=$'help \nread \nlist \nwrite \nreset \nupdate \nlock \nunlock \nwatch \ndump \nload '
+ ;;
+ list|dump|load)
+ choices="$(dconf _complete / "${COMP_WORDS[2]}")"
+ ;;
+ read|list|write|lock|unlock|watch|reset)
+ choices="$(dconf _complete '' "${COMP_WORDS[2]}")"
+ ;;
+ esac
+ ;;
+
+ 3)
+ case "${COMP_WORDS[1]} ${COMP_WORDS[2]}" in
+ reset\ -f)
+ choices="$(dconf _complete '' "${COMP_WORDS[3]}")"
+ ;;
+ esac
+ ;;
+ esac
+
+ local IFS=$'\n'
+ COMPREPLY=($(compgen -W "${choices}" "${COMP_WORDS[$COMP_CWORD]}"))
+}
+
+####################################################################################################
+
+complete -o nospace -F __dconf dconf
diff --git a/bin/dconf.c b/bin/dconf.c
new file mode 100644
index 0000000..c91f82a
--- /dev/null
+++ b/bin/dconf.c
@@ -0,0 +1,1214 @@
+/*
+ * Copyright © 2010, 2011 Codethink Limited
+ * Copyright © 2011 Canonical Limited
+ * Copyright © 2018 Tomasz Miąsko
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ * Tomasz Miąsko
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <glib.h>
+#include <glib/gprintf.h>
+#include <glib/gstdio.h>
+#include <locale.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "client/dconf-client.h"
+#include "common/dconf-enums.h"
+#include "common/dconf-paths.h"
+#include "gvdb/gvdb-builder.h"
+#include "gvdb/gvdb-reader.h"
+
+static gboolean dconf_help (const gchar **argv, GError **error);
+
+static gboolean
+option_error_propagate (GError **dst, GError **src)
+{
+ g_assert (src != NULL && *src != NULL);
+
+ (*src)->domain = G_OPTION_ERROR;
+ (*src)->code = G_OPTION_ERROR_FAILED;
+ g_propagate_error (dst, g_steal_pointer (src));
+
+ return FALSE;
+}
+
+static gboolean
+option_error_set (GError **error, const char *message)
+{
+ g_assert (error != NULL);
+ g_assert (message != NULL);
+
+ g_set_error_literal (error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, message);
+
+ return FALSE;
+}
+
+static gboolean
+dconf_read (const gchar **argv,
+ GError **error)
+{
+ gint index = 0;
+ const gchar *key;
+ DConfReadFlags flags = DCONF_READ_FLAGS_NONE;
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(DConfClient) client = NULL;
+ g_autoptr(GVariant) result = NULL;
+
+ if (argv[index] != NULL && strcmp (argv[index], "-d") == 0)
+ {
+ flags = DCONF_READ_DEFAULT_VALUE;
+ index += 1;
+ }
+
+ key = argv[index];
+ if (!dconf_is_key (key, &local_error))
+ return option_error_propagate (error, &local_error);
+
+ index += 1;
+
+ if (argv[index] != NULL)
+ return option_error_set (error, "too many arguments");
+
+ client = dconf_client_new ();
+ result = dconf_client_read_full (client, key, flags, NULL);
+
+ if (result != NULL)
+ {
+ g_autofree gchar *s = g_variant_print (result, TRUE);
+ g_printf ("%s\n", s);
+ }
+
+ return TRUE;
+}
+
+static gint
+string_compare (const void *a,
+ const void *b)
+{
+ return strcmp (*(const gchar **)a, *(const gchar **)b);
+}
+
+static gint
+string_rcompare (const void *a,
+ const void *b)
+{
+ return -strcmp (*(const gchar **)a, *(const gchar **)b);
+}
+
+static gboolean
+dconf_list (const gchar **argv,
+ GError **error)
+{
+ const char *dir;
+ gint length;
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(DConfClient) client = NULL;
+ g_auto(GStrv) items = NULL;
+
+ dir = argv[0];
+ if (!dconf_is_dir (dir, &local_error))
+ return option_error_propagate (error, &local_error);
+
+ client = dconf_client_new ();
+ items = dconf_client_list (client, dir, &length);
+ qsort (items, length, sizeof (items[0]), string_compare);
+
+ for (char **item = items; *item; ++item)
+ g_printf ("%s\n", *item);
+
+ return TRUE;
+}
+
+static gboolean
+dconf_list_locks (const gchar **argv,
+ GError **error)
+{
+ const char *dir;
+ gint length;
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(DConfClient) client = NULL;
+ g_auto(GStrv) items = NULL;
+
+ dir = argv[0];
+ if (!dconf_is_dir (dir, &local_error))
+ return option_error_propagate (error, &local_error);
+
+ if (argv[1] != NULL)
+ return option_error_set (error, "too many arguments");
+
+ client = dconf_client_new ();
+ items = dconf_client_list_locks (client, dir, &length);
+ qsort (items, length, sizeof (items[0]), string_compare);
+
+ for (char **item = items; *item; ++item)
+ g_printf ("%s\n", *item);
+
+ return TRUE;
+}
+
+static gboolean
+dconf_write (const gchar **argv,
+ GError **error)
+{
+ const char *key;
+ const char *value_str;
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(GVariant) value = NULL;
+ g_autoptr(DConfClient) client = NULL;
+
+ key = argv[0];
+ if (!dconf_is_key (key, &local_error))
+ return option_error_propagate (error, &local_error);
+
+ value_str = argv[1];
+ if (value_str == NULL)
+ return option_error_set (error, "value not specified");
+
+ value = g_variant_parse (NULL, value_str, NULL, NULL, &local_error);
+ if (value == NULL)
+ return option_error_propagate (error, &local_error);
+
+ if (argv[2] != NULL)
+ return option_error_set (error, "too many arguments");
+
+ client = dconf_client_new ();
+ return dconf_client_write_sync (client, key, value, NULL, NULL, error);
+}
+
+static gboolean
+dconf_reset (const gchar **argv,
+ GError **error)
+{
+ gboolean force = FALSE;
+ gint index = 0;
+ const gchar *path;
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(DConfClient) client = NULL;
+
+ if (argv[index] != NULL && strcmp (argv[index], "-f") == 0)
+ {
+ index += 1;
+ force = TRUE;
+ }
+
+ path = argv[index];
+ if (!dconf_is_path (path, &local_error))
+ return option_error_propagate (error, &local_error);
+
+ index += 1;
+
+ if (dconf_is_dir (path, NULL) && !force)
+ return option_error_set (error, "-f must be given to (recursively) reset entire directories");
+
+ if (argv[index] != NULL)
+ return option_error_set (error, "too many arguments");
+
+ client = dconf_client_new ();
+ return dconf_client_write_sync (client, path, NULL, NULL, NULL, error);
+}
+
+static void
+show_path (DConfClient *client, const gchar *path)
+{
+ if (dconf_is_key (path, NULL))
+ {
+ g_autoptr(GVariant) value = NULL;
+ g_autofree gchar *value_str = NULL;
+
+ value = dconf_client_read (client, path);
+
+ if (value != NULL)
+ value_str = g_variant_print (value, TRUE);
+
+ g_printf (" %s\n", value_str != NULL ? value_str : "unset");
+ }
+}
+
+static void
+watch_function (DConfClient *client,
+ const gchar *path,
+ const gchar **items,
+ const gchar *tag,
+ gpointer user_data)
+{
+ for (const gchar **item = items; *item; ++item)
+ {
+ g_autofree gchar *full = NULL;
+
+ full = g_strconcat (path, *item, NULL);
+ g_printf ("%s\n", full);
+ show_path (client, full);
+ }
+
+ g_printf ("\n");
+ fflush (stdout);
+}
+
+static gboolean
+dconf_watch (const char **argv,
+ GError **error)
+{
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(DConfClient) client = NULL;
+ g_autoptr(GMainLoop) loop = NULL;
+ const gchar *path;
+
+ path = argv[0];
+ if (!dconf_is_path (path, &local_error))
+ return option_error_propagate (error, &local_error);
+
+ if (argv[1] != NULL)
+ return option_error_set (error, "too many arguments");
+
+ client = dconf_client_new ();
+ g_signal_connect (client, "changed", G_CALLBACK (watch_function), NULL);
+ dconf_client_watch_sync (client, path);
+
+ loop = g_main_loop_new (NULL, FALSE);
+ g_main_loop_run (loop);
+
+ return TRUE;
+}
+
+static gboolean
+dconf_blame (const char **argv,
+ GError **error)
+{
+ g_autoptr(GVariant) reply = NULL;
+ g_autoptr(GVariant) child = NULL;
+ g_autoptr(GDBusConnection) connection = NULL;
+
+ if (argv[0] != NULL)
+ return option_error_set (error, "too many arguments");
+
+ connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, error);
+ if (connection == NULL)
+ return FALSE;
+
+ reply = g_dbus_connection_call_sync (connection, "ca.desrt.dconf",
+ "/ca/desrt/dconf",
+ "ca.desrt.dconf.ServiceInfo",
+ "Blame", NULL, G_VARIANT_TYPE ("(s)"),
+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, error);
+ if (reply == NULL)
+ return FALSE;
+
+ child = g_variant_get_child_value (reply, 0);
+ g_printf ("%s", g_variant_get_string (child, NULL));
+
+ return TRUE;
+}
+
+/**
+ * Returns a parent dir that contains given path.
+ */
+static gchar *
+path_get_parent (const char *path)
+{
+ g_return_val_if_fail (path != NULL, NULL);
+ g_return_val_if_fail (strcmp (path, "/") != 0, NULL);
+
+ gsize last = 0;
+
+ /* Find the position of the last slash, other than the trailing one. */
+ for (gsize i = 0; path[i + 1] != '\0'; ++i)
+ if (path[i] == '/')
+ last = i;
+
+ return strndup (path, last + 1);
+}
+
+static gboolean
+dconf_complete (const gchar **argv,
+ GError **error)
+{
+ const gchar *suffix;
+ const gchar *path;
+
+ suffix = argv[0];
+ if (suffix == NULL)
+ return option_error_set (error, "suffix not specified");
+
+ path = argv[1];
+ if (path == NULL)
+ return option_error_set (error, "path not specified");
+
+ if (argv[2] != NULL)
+ return option_error_set (error, "too many arguments");
+
+ if (g_str_equal (path, ""))
+ {
+ g_printf ("/\n");
+ return TRUE;
+ }
+
+ if (path[0] == '/')
+ {
+ gint length;
+ g_autoptr(DConfClient) client = NULL;
+ g_autofree gchar *dir = NULL;
+ g_auto(GStrv) items = NULL;
+
+ if (g_str_has_suffix (path, "/"))
+ dir = g_strdup (path);
+ else
+ dir = path_get_parent (path);
+
+ client = dconf_client_new ();
+ items = dconf_client_list (client, dir, &length);
+ qsort (items, length, sizeof (items[0]), string_compare);
+
+ for (gchar **item = items; *item; ++item)
+ {
+ g_autofree gchar *full_item = NULL;
+
+ full_item = g_strconcat (dir, *item, NULL);
+ if (g_str_has_prefix (full_item, path) &&
+ g_str_has_suffix (*item, suffix))
+ {
+ g_printf ("%s%s\n", full_item,
+ g_str_has_suffix (full_item, "/") ? "" : " ");
+ }
+ }
+ }
+
+ return TRUE;
+}
+
+/**
+ * Comparison function for paths that orders keys before dirs.
+ */
+static gint
+path_compare (const void *a,
+ const void *b)
+{
+ const gchar *as = *(const gchar **)a;
+ const gchar *bs = *(const gchar **)b;
+
+ const gboolean a_is_dir = !!g_str_has_suffix (as, "/");
+ const gboolean b_is_dir = !!g_str_has_suffix (bs, "/");
+
+ if (a_is_dir != b_is_dir)
+ return a_is_dir - b_is_dir;
+ else
+ return strcmp (as, bs);
+}
+
+/**
+ * add_to_keyfile:
+ * @dir_src: a dconf source dir
+ * @dir_dst: a key-file destination dir
+ *
+ * Copy directory contents from dconf to key-file.
+ **/
+static void
+add_to_keyfile (GKeyFile *kf,
+ DConfClient *client,
+ const gchar *dir_src,
+ const gchar *dir_dst)
+{
+ g_autofree gchar *group = NULL;
+ g_auto(GStrv) items = NULL;
+ gint length;
+ gsize n;
+
+ /* Key-file group names are formed by removing initial and trailing slash
+ * from dir name, with the singular exception of root dir whose group name
+ * is just "/". */
+
+ n = strlen (dir_dst);
+ g_assert (n >= 1 && dir_dst[n - 1] == '/');
+
+ if (g_str_equal (dir_dst, "/"))
+ group = g_strdup ("/");
+ else
+ group = g_strndup (dir_dst + 1, n - 2);
+
+ items = dconf_client_list (client, dir_src, &length);
+ qsort (items, length, sizeof (items[0]), path_compare);
+
+ for (gchar **item = items; *item; ++item)
+ {
+ g_autofree gchar *path = g_strconcat (dir_src, *item, NULL);
+
+ if (g_str_has_suffix (*item, "/"))
+ {
+ g_autofree gchar *subdir = g_strconcat (dir_dst, *item, NULL);
+ add_to_keyfile (kf, client, path, subdir);
+ }
+ else
+ {
+ g_autoptr(GVariant) value = dconf_client_read (client, path);
+ if (value != NULL)
+ {
+ g_autofree gchar *value_str = g_variant_print (value, TRUE);
+ g_key_file_set_value (kf, group, *item, value_str);
+ }
+ }
+ }
+}
+
+static gboolean
+dconf_dump (const gchar **argv,
+ GError **error)
+{
+ const gchar *dir;
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(GKeyFile) kf = NULL;
+ g_autoptr(DConfClient) client = NULL;
+ g_autofree gchar *data = NULL;
+
+ dir = argv[0];
+ if (!dconf_is_dir (dir, &local_error))
+ return option_error_propagate (error, &local_error);
+
+ if (argv[1] != NULL)
+ return option_error_set (error, "too many arguments");
+
+ kf = g_key_file_new ();
+ client = dconf_client_new ();
+
+ add_to_keyfile (kf, client, dir, "/");
+
+ data = g_key_file_to_data (kf, NULL, NULL);
+ g_printf ("%s", data);
+
+ return TRUE;
+}
+
+static GKeyFile *
+keyfile_from_stdin (GError **error)
+{
+ g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+ char buffer[1024];
+ g_autoptr(GString) s = NULL;
+ g_autoptr(GKeyFile) kf = NULL;
+
+ s = g_string_new (NULL);
+ while (fgets (buffer, sizeof (buffer), stdin) != NULL)
+ g_string_append (s, buffer);
+
+ kf = g_key_file_new ();
+ if (!g_key_file_load_from_data (kf, s->str, s->len, G_KEY_FILE_NONE, error))
+ return FALSE;
+
+ return g_steal_pointer (&kf);
+}
+
+typedef void (*KeyFileForeachFunc) (const gchar *path,
+ GVariant *value,
+ gpointer user_data);
+
+static gboolean
+keyfile_foreach (GKeyFile *kf,
+ const gchar *dir,
+ KeyFileForeachFunc func,
+ gpointer user_data,
+ GError **error)
+{
+ g_auto(GStrv) groups = NULL;
+
+ groups = g_key_file_get_groups (kf, NULL);
+
+ for (gchar **group = groups; *group; ++group)
+ {
+ g_auto(GStrv) keys = NULL;
+
+ keys = g_key_file_get_keys (kf, *group, NULL, NULL);
+
+ for (gchar **key = keys; *key; ++key)
+ {
+ g_autoptr(GString) s = NULL;
+ g_autofree gchar *value_str = NULL;
+ g_autoptr(GVariant) value = NULL;
+
+ /* Reconstruct dconf key path from the current dir,
+ * key-file group name and key-file key. */
+ s = g_string_new (dir);
+ if (strcmp (*group, "/") != 0)
+ {
+ g_string_append (s, *group);
+ g_string_append (s, "/");
+ }
+ g_string_append (s, *key);
+
+ if (!dconf_is_key (s->str, error))
+ {
+ g_prefix_error (error, "[%s]: %s: invalid path: ",
+ *group, *key);
+ return FALSE;
+ }
+
+ value_str = g_key_file_get_value (kf, *group, *key, NULL);
+ g_assert (value_str != NULL);
+
+ value = g_variant_parse (NULL, value_str, NULL, NULL, error);
+ if (value == NULL)
+ {
+ g_prefix_error (error, "[%s]: %s: invalid value: %s: ",
+ *group, *key, value_str);
+ return FALSE;
+ }
+
+ func (s->str, value, user_data);
+ }
+ }
+
+ return TRUE;
+}
+
+typedef struct {
+ DConfClient *client;
+ DConfChangeset *changeset;
+ gboolean force;
+} LoadContext;
+
+static void
+changeset_set (const gchar *path,
+ GVariant *value,
+ gpointer user_data)
+{
+ LoadContext *ctx = user_data;
+
+ /* When force option is used, ignore changes made to non-writeable keys to
+ * avoid rejecting the whole changeset.
+ */
+ if (ctx->force && !dconf_client_is_writable (ctx->client, path))
+ {
+ g_fprintf (stderr, "warning: ignored non-writable key '%s'\n", path);
+ return;
+ }
+
+ dconf_changeset_set (ctx->changeset, path, value);
+}
+
+static gboolean
+dconf_load (const gchar **argv,
+ GError **error)
+{
+ const gchar *dir;
+ gint index = 0;
+ gboolean force = FALSE;
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(GKeyFile) kf = NULL;
+ g_autoptr(DConfChangeset) changeset = NULL;
+ g_autoptr (DConfClient) client = NULL;
+
+ if (argv[index] != NULL && strcmp (argv[index], "-f") == 0)
+ {
+ force = TRUE;
+ index += 1;
+ }
+
+ dir = argv[index];
+ if (!dconf_is_dir (dir, &local_error))
+ return option_error_propagate (error, &local_error);
+
+ index += 1;
+
+ if (argv[index] != NULL)
+ return option_error_set (error, "too many arguments");
+
+ kf = keyfile_from_stdin (error);
+ if (kf == NULL)
+ return FALSE;
+
+ client = dconf_client_new ();
+ changeset = dconf_changeset_new ();
+
+ LoadContext ctx = { client, changeset, force };
+ if (!keyfile_foreach (kf, dir, changeset_set, &ctx, error))
+ return FALSE;
+
+ return dconf_client_change_sync (client, changeset, NULL, NULL, error);
+}
+
+static GPtrArray *
+list_directory (const gchar *dirname,
+ mode_t ftype,
+ GError **error)
+{
+ const gchar *name;
+ g_autoptr(GDir) dir = NULL;
+ g_autoptr(GPtrArray) files = NULL;
+
+ dir = g_dir_open (dirname, 0, error);
+ if (dir == NULL)
+ return NULL;
+
+ files = g_ptr_array_new_full (0, g_free);
+
+ while ((name = g_dir_read_name (dir)) != NULL)
+ {
+ GStatBuf buf;
+ g_autofree gchar *filename = NULL;
+
+ /* Ignore swap files like .swp etc. */
+ if (g_str_has_prefix (name, "."))
+ continue;
+
+ filename = g_build_filename (dirname, name, NULL);
+
+ if (g_stat (filename, &buf) < 0)
+ {
+ gint saved_errno = errno;
+ g_debug ("ignoring file %s: %s",
+ filename, g_strerror (saved_errno));
+ continue;
+ }
+
+ if ((buf.st_mode & S_IFMT) != ftype)
+ continue;
+
+ g_ptr_array_add (files, g_steal_pointer (&filename));
+ }
+
+ return g_steal_pointer (&files);
+}
+
+static GHashTable *
+read_locks_directory (const gchar *dirname,
+ GError **error)
+{
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(GPtrArray) files = NULL;
+ g_autoptr(GHashTable) table = NULL;
+
+ files = list_directory (dirname, S_IFREG, &local_error);
+ if (files == NULL)
+ {
+ /* If locks directory is missing, there are just no locks... */
+ if (!g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+ g_propagate_error (error, g_steal_pointer (&local_error));
+ return NULL;
+ }
+
+ table = gvdb_hash_table_new (NULL, NULL);
+
+ for (guint i = 0; i != files->len; ++i)
+ {
+ const gchar *filename;
+ g_autofree gchar *contents = NULL;
+ g_auto(GStrv) lines = NULL;
+ gsize length;
+
+ filename = g_ptr_array_index (files, i);
+
+ if (!g_file_get_contents (filename, &contents, &length, error))
+ return NULL;
+
+ lines = g_strsplit (contents, "\n", 0);
+ for (gchar **line = lines; *line; ++line)
+ {
+ if (g_str_has_prefix (*line, "/"))
+ gvdb_hash_table_insert_string (table, *line, "");
+ }
+ }
+
+ return g_steal_pointer (&table);
+}
+
+static GvdbItem *
+table_get_parent (GHashTable *table,
+ const gchar *name)
+{
+ GvdbItem *parent = NULL;
+ g_autofree gchar *dir = NULL;
+
+ dir = path_get_parent (name);
+ parent = g_hash_table_lookup (table, dir);
+
+ if (parent == NULL)
+ {
+ parent = gvdb_hash_table_insert (table, dir);
+ gvdb_item_set_parent (parent, table_get_parent (table, dir));
+ }
+
+ return parent;
+}
+
+
+static void
+table_insert (const gchar *path,
+ GVariant *value,
+ gpointer user_data)
+{
+ GHashTable *table = user_data;
+ GvdbItem *item;
+
+ /* See FILES-PRECEDENCE 2 */
+ if (g_hash_table_lookup (table, path) != NULL)
+ return;
+
+ item = gvdb_hash_table_insert (table, path);
+ gvdb_item_set_parent (item, table_get_parent (table, path));
+ gvdb_item_set_value (item, value);
+}
+
+static GHashTable *
+read_directory (const gchar *dir,
+ GError **error)
+{
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(GHashTable) table = NULL;
+ g_autoptr(GPtrArray) files = NULL;
+ g_autofree gchar *locks_dir = NULL;
+ GHashTable *locks_table = NULL;
+
+ table = gvdb_hash_table_new (NULL, NULL);
+ gvdb_hash_table_insert (table, "/");
+
+ files = list_directory (dir, S_IFREG, error);
+ if (files == NULL)
+ return NULL;
+
+ /* FILES-PRECEDENCE: When a path is found in multiple files, value from the
+ * file lexicographically latest takes precedence. This is achieved by 1)
+ * processing files in reversed lexicographical order, 2) not overwriting
+ * existing paths.
+ */
+ g_ptr_array_sort (files, string_rcompare);
+
+ for (guint i = 0; i != files->len; ++i)
+ {
+ const gchar *filename;
+ g_autoptr(GKeyFile) kf = NULL;
+
+ filename = g_ptr_array_index (files, i);
+ kf = g_key_file_new ();
+
+ g_debug ("loading key-file: %s", filename);
+
+ if (!g_key_file_load_from_file (kf, filename, G_KEY_FILE_NONE, error))
+ {
+ g_autofree gchar *display_name = g_filename_display_basename (filename);
+ g_prefix_error (error, "%s: ", display_name);
+ return FALSE;
+ }
+
+ if (!keyfile_foreach (kf, "/", table_insert, table, error))
+ {
+ g_autofree gchar *display_name = g_filename_display_basename (filename);
+ g_prefix_error (error, "%s: ", display_name);
+ return FALSE;
+ }
+ }
+
+ locks_dir = g_build_filename (dir, "locks", NULL);
+ locks_table = read_locks_directory (locks_dir, &local_error);
+ if (local_error != NULL)
+ {
+ g_propagate_error (error, g_steal_pointer (&local_error));
+ return FALSE;
+ }
+
+ if (locks_table != NULL)
+ {
+ GvdbItem *item;
+
+ item = gvdb_hash_table_insert (table, ".locks");
+ gvdb_item_set_hash_table (item, locks_table);
+ }
+
+ return g_steal_pointer (&table);
+}
+
+static gboolean
+update_directory (const gchar *dir,
+ GError **error)
+{
+ gint fd = -1;
+ g_autofree gchar *filename = NULL;
+ g_autoptr(GHashTable) table = NULL;
+ g_autoptr(GDBusConnection) bus = NULL;
+
+ g_assert (g_str_has_suffix (dir, ".d"));
+ filename = strndup (dir, strlen (dir) - 2);
+
+ table = read_directory (dir, error);
+ if (table == NULL)
+ return FALSE;
+
+ fd = open (filename, O_WRONLY);
+ if (fd < 0 && errno != ENOENT)
+ {
+ gint saved_errno = errno;
+ g_autofree gchar *display_name = g_filename_display_name (filename);
+
+ g_fprintf (stderr, "warning: Failed to open '%s': for replacement: %s\n",
+ display_name, g_strerror (saved_errno));
+ }
+
+ if (!gvdb_table_write_contents (table, filename, FALSE, error))
+ {
+ if (fd >= 0)
+ close (fd);
+ return FALSE;
+ }
+
+ if (fd >= 0)
+ {
+ /* Mark previous database as invalid. */
+ write (fd, "\0\0\0\0\0\0\0\0", 8);
+ close (fd);
+ }
+
+ bus = g_bus_get_sync (G_BUS_TYPE_SYSTEM, NULL, NULL);
+
+ if (bus != NULL)
+ {
+ g_autofree gchar *object_name = NULL;
+ g_autofree gchar *object_path = NULL;
+
+ object_name = g_path_get_basename (filename);
+ object_path = g_strconcat ("/ca/desrt/dconf/Writer/", object_name, NULL);
+
+ /* Ignore all D-Bus errors. */
+ g_dbus_connection_emit_signal (bus, NULL, object_path,
+ "ca.desrt.dconf.Writer",
+ "WritabilityNotify",
+ g_variant_new ("(s)", "/"),
+ NULL);
+ g_dbus_connection_flush_sync (bus, NULL, NULL);
+ }
+
+ return TRUE;
+}
+
+static gboolean
+update_all (const gchar *dirname,
+ GError **error)
+{
+ gboolean failed = FALSE;
+ g_autoptr(GPtrArray) files = NULL;
+
+ files = list_directory (dirname, S_IFDIR, error);
+ if (files == NULL)
+ return FALSE;
+
+ for (guint i = 0; i != files->len; ++i)
+ {
+ const gchar *name;
+ g_autoptr(GError) local_error = NULL;
+
+ name = g_ptr_array_index (files, i);
+ if (!g_str_has_suffix (name, ".d"))
+ continue;
+
+ if (!update_directory (name, &local_error))
+ {
+ g_autofree gchar *display_name = g_filename_display_name (name);
+ g_fprintf (stderr, "%s: %s\n",
+ display_name, local_error->message);
+ failed = TRUE;
+ }
+ }
+
+ if (failed)
+ {
+ g_set_error_literal (error, DCONF_ERROR, DCONF_ERROR_FAILED,
+ "failed to update at least one of the databases");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+dconf_compile (const gchar **argv,
+ GError **error)
+{
+ gboolean byteswap;
+ const gchar *output;
+ const gchar *dir;
+ g_autoptr(GHashTable) table = NULL;
+
+ output = argv[0];
+ if (output == NULL)
+ return option_error_set (error, "output file not specified");
+
+ dir = argv[1];
+ if (dir == NULL)
+ return option_error_set (error, "keyfile .d directory not specified");
+
+ if (argv[2] != NULL)
+ return option_error_set (error, "too many arguments");
+
+ table = read_directory (dir, error);
+ if (table == NULL)
+ return FALSE;
+
+ /* We always write the result of "dconf compile" as little endian so
+ * that it can be installed in /usr/share */
+ byteswap = (G_BYTE_ORDER == G_BIG_ENDIAN);
+ return gvdb_table_write_contents (table, output, byteswap, error);
+}
+
+static gchar *
+get_system_db_path ()
+{
+ return g_build_filename (SYSCONFDIR, "dconf", "db", NULL);
+}
+
+static gboolean
+dconf_update (const gchar **argv,
+ GError **error)
+{
+ gint index = 0;
+ g_autofree gchar *dir = NULL;
+
+ if (argv[index] != NULL)
+ {
+ dir = g_strdup (argv[0]);
+ index += 1;
+ }
+ else
+ dir = get_system_db_path ();
+
+ if (argv[index] != NULL)
+ return option_error_set (error, "too many arguments");
+
+ return update_all (dir, error);
+}
+
+typedef struct {
+ const char *name;
+ gboolean (*func)(const gchar **, GError **);
+ const char *description;
+ const char *synopsis;
+} Command;
+
+static const Command commands[] = {
+ {
+ "help", dconf_help,
+ "Print help", " COMMAND "
+ },
+ {
+ "read", dconf_read,
+ "Read the value of a key. -d to read default values.",
+ " [-d] KEY "
+ },
+ {
+ "list", dconf_list,
+ "List the sub-keys and sub-dirs of a dir",
+ " DIR "
+ },
+ {
+ "list-locks", dconf_list_locks,
+ "List the locks under a dir",
+ " DIR "
+ },
+ {
+ "write", dconf_write,
+ "Write a new value to a key",
+ " KEY VALUE "
+ },
+ {
+ "reset", dconf_reset,
+ "Reset a key or dir. -f is required for dirs.",
+ " [-f] PATH "
+ },
+ {
+ "compile", dconf_compile,
+ "Compile a binary database from keyfiles",
+ " OUTPUT KEYFILEDIR "
+ },
+ {
+ "update", dconf_update,
+ "Update the system dconf databases",
+ " [DBDIR] "
+ },
+ {
+ "watch", dconf_watch,
+ "Watch a path for key changes",
+ " PATH "
+ },
+ {
+ "dump", dconf_dump,
+ "Dump an entire subpath to stdout",
+ " DIR "
+ },
+ {
+ "load", dconf_load,
+ "Populate a subpath from stdin. -f ignore locked keys.",
+ " [-f] DIR "
+ },
+ {
+ "blame", dconf_blame,
+ "",
+ ""
+ },
+ {
+ "_complete", dconf_complete,
+ "",
+ " SUFFIX PATH "
+ },
+ {},
+};
+
+static const gchar usage[] =
+ "Usage:\n"
+ " dconf COMMAND [ARGS...]\n"
+ "\n"
+ "Commands:\n"
+ " help Show this information\n"
+ " read Read the value of a key\n"
+ " list List the contents of a dir\n"
+ " write Change the value of a key\n"
+ " reset Reset the value of a key or dir\n"
+ " compile Compile a binary database from keyfiles\n"
+ " update Update the system databases\n"
+ " watch Watch a path for changes\n"
+ " dump Dump an entire subpath to stdout\n"
+ " load Populate a subpath from stdin\n"
+ "\n"
+ "Use 'dconf help COMMAND' to get detailed help.\n"
+ "\n";
+
+static const Command *
+command_with_name (const gchar *name)
+{
+ const Command *cmd;
+
+ for (cmd = commands; cmd->name != NULL; ++cmd)
+ if (g_strcmp0 (cmd->name, name) == 0)
+ return cmd;
+
+ return NULL;
+}
+
+static void
+command_show_help (const Command *cmd,
+ FILE *file)
+{
+ g_autoptr(GString) s = g_string_sized_new (1024);
+
+ if (cmd == NULL)
+ {
+ g_string_append (s, usage);
+ }
+ else
+ {
+ /* Generate command specific usage help text. */
+
+ g_string_append (s, "Usage:\n");
+ g_string_append_printf (s, " dconf %s%s\n\n", cmd->name, cmd->synopsis);
+
+ if (!g_str_equal (cmd->description, ""))
+ g_string_append_printf (s, "%s\n\n", cmd->description);
+
+ if (!g_str_equal (cmd->synopsis, ""))
+ {
+ g_string_append (s, "Arguments:\n");
+
+ if (strstr (cmd->synopsis, " COMMAND ") != NULL)
+ g_string_append (s, " COMMAND "
+ "The (optional) command to explain\n");
+
+ if (strstr (cmd->synopsis, " PATH ") != NULL)
+ g_string_append (s, " PATH Either a KEY or DIR\n");
+
+ if (strstr (cmd->synopsis, " PATH ") != NULL ||
+ strstr (cmd->synopsis, " KEY ") != NULL)
+ g_string_append (s, " KEY A key path (starting, but not ending with '/')\n");
+
+ if (strstr (cmd->synopsis, " PATH ") != NULL ||
+ strstr (cmd->synopsis, " DIR ") != NULL)
+ g_string_append (s, " DIR A directory path (starting and ending with '/')\n");
+
+ if (strstr (cmd->synopsis, " VALUE ") != NULL)
+ g_string_append (s, " VALUE The value to write (in GVariant format)\n");
+
+ if (strstr (cmd->synopsis, " OUTPUT ") != NULL)
+ g_string_append (s, " OUTPUT The filename of the (binary) output\n");
+
+ if (strstr (cmd->synopsis, " KEYFILEDIR ") != NULL)
+ g_string_append (s, " KEYFILEDIR The path to the .d directory containing keyfiles\n");
+
+ if (strstr (cmd->synopsis, " SUFFIX ") != NULL)
+ g_string_append (s, " SUFFIX An empty string '' or '/'.\n");
+
+ if (strstr (cmd->synopsis, " [DBDIR] ") != NULL)
+ {
+ g_autofree gchar *path = get_system_db_path ();
+ g_string_append_printf (s, " DBDIR The databases directory. Default: %s\n", path);
+ }
+
+ g_string_append (s, "\n");
+ }
+ }
+
+ g_fprintf (file, "%s", s->str);
+}
+
+static gboolean
+dconf_help (const gchar **argv, GError **error)
+{
+ const gchar *name = *argv;
+ command_show_help (command_with_name (name), stdout);
+ return TRUE;
+}
+
+int
+main (int argc, const char **argv)
+{
+ const Command *cmd;
+ g_autoptr(GError) error = NULL;
+
+ setlocale (LC_ALL, "");
+ g_set_prgname (argv[0]);
+
+ if (argc <= 1)
+ {
+ g_fprintf (stderr, "error: no command specified\n\n");
+ command_show_help (NULL, stderr);
+ return 2;
+ }
+
+ cmd = command_with_name (argv[1]);
+ if (cmd == NULL)
+ {
+ g_fprintf (stderr, "error: unknown command %s\n\n", argv[1]);
+ command_show_help (cmd, stderr);
+ return 2;
+ }
+
+ if (cmd->func (argv + 2, &error))
+ return 0;
+
+ g_assert (error != NULL);
+
+ /* Invalid arguments passed, show usage on stderr. */
+ if (error->domain == G_OPTION_ERROR)
+ {
+ g_fprintf (stderr, "error: %s\n\n", error->message);
+ command_show_help (cmd, stderr);
+ return 2;
+ }
+ else
+ {
+ g_fprintf (stderr, "error: %s\n", error->message);
+ return 1;
+ }
+}
diff --git a/bin/meson.build b/bin/meson.build
new file mode 100644
index 0000000..e2c620a
--- /dev/null
+++ b/bin/meson.build
@@ -0,0 +1,23 @@
+sources = gvdb_builder + files(
+ 'dconf.c',
+)
+
+bin_deps = [
+ libdconf_common_dep,
+ libdconf_dep,
+]
+
+dconf = executable(
+ 'dconf',
+ sources,
+ include_directories: top_inc,
+ dependencies: bin_deps,
+ install: true,
+)
+
+if enable_bash_completion
+ install_data(
+ 'completion/dconf',
+ install_dir: completions_dir,
+ )
+endif
diff --git a/client/dconf-client.c b/client/dconf-client.c
new file mode 100644
index 0000000..c315693
--- /dev/null
+++ b/client/dconf-client.c
@@ -0,0 +1,697 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-client.h"
+
+#include "../engine/dconf-engine.h"
+#include "../common/dconf-paths.h"
+#include <glib-object.h>
+
+/**
+ * SECTION:client
+ * @title: DConfClient
+ * @short_description: Direct read and write access to dconf, based on GDBus
+ *
+ * This is the primary client interface to dconf.
+ *
+ * It allows applications to directly read from and write to the dconf
+ * database. Applications can subscribe to change notifications.
+ *
+ * Most applications probably don't want to access dconf directly and
+ * would be better off using something like #GSettings.
+ *
+ * Please note that the API of libdconf is not stable in any way. It
+ * has changed in incompatible ways in the past and there will be
+ * further changes in the future.
+ **/
+
+/**
+ * DConfClient:
+ *
+ * The main object for interacting with dconf. This is a #GObject, so
+ * you should manage it with g_object_ref() and g_object_unref().
+ **/
+struct _DConfClient
+{
+ GObject parent_instance;
+
+ DConfEngine *engine;
+ GMainContext *context;
+};
+
+G_DEFINE_TYPE (DConfClient, dconf_client, G_TYPE_OBJECT)
+
+enum
+{
+ SIGNAL_CHANGED,
+ SIGNAL_WRITABILITY_CHANGED,
+ N_SIGNALS
+};
+static guint dconf_client_signals[N_SIGNALS];
+
+static void
+dconf_client_finalize (GObject *object)
+{
+ DConfClient *client = DCONF_CLIENT (object);
+
+ dconf_engine_unref (client->engine);
+ g_main_context_unref (client->context);
+
+ G_OBJECT_CLASS (dconf_client_parent_class)
+ ->finalize (object);
+}
+
+static void
+dconf_client_init (DConfClient *client)
+{
+}
+
+static void
+dconf_client_class_init (DConfClientClass *class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+ object_class->finalize = dconf_client_finalize;
+
+ /**
+ * DConfClient::changed:
+ * @client: the #DConfClient reporting the change
+ * @prefix: the prefix under which the changes happened
+ * @changes: the list of paths that were changed, relative to @prefix
+ * @tag: the tag for the change, if it originated from the service
+ *
+ * This signal is emitted when the #DConfClient has a possible change
+ * to report. The signal is an indication that a change may have
+ * occurred; it's possible that the keys will still have the same value
+ * as before.
+ *
+ * To ensure that you receive notification about changes to paths that
+ * you are interested in you must call dconf_client_watch_fast() or
+ * dconf_client_watch_sync(). You may still receive notifications for
+ * paths that you did not explicitly watch.
+ *
+ * @prefix will be an absolute dconf path; see dconf_is_path().
+ * @changes is a %NULL-terminated array of dconf rel paths; see
+ * dconf_is_rel_path().
+ *
+ * @tag is an opaque tag string, or %NULL. The only thing you should
+ * do with @tag is to compare it to tag values returned by
+ * dconf_client_write_sync() or dconf_client_change_sync().
+ *
+ * The number of changes being reported is equal to the length of
+ * @changes. Appending each item in @changes to @prefix will give the
+ * absolute path of each changed item.
+ *
+ * If a single key has changed then @prefix will be equal to the key
+ * and @changes will contain a single item: the empty string.
+ *
+ * If a single dir has changed (indicating that any key under the dir
+ * may have changed) then @prefix will be equal to the dir and
+ * @changes will contain a single empty string.
+ *
+ * If more than one change is being reported then @changes will have
+ * more than one item.
+ **/
+ dconf_client_signals[SIGNAL_CHANGED] = g_signal_new ("changed", DCONF_TYPE_CLIENT, G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, NULL, G_TYPE_NONE, 3,
+ G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE,
+ G_TYPE_STRV | G_SIGNAL_TYPE_STATIC_SCOPE,
+ G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+
+ /**
+ * DConfClient::writability-changed:
+ * @client: the #DConfClient reporting the change
+ * @path: the dir or key that changed
+ *
+ * Signal emitted when writability for a key (or all keys in a dir) changes.
+ * It will be immediately followed by #DConfClient::changed signal for
+ * the path.
+ */
+ dconf_client_signals[SIGNAL_WRITABILITY_CHANGED] = g_signal_new ("writability-changed", DCONF_TYPE_CLIENT,
+ G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 1,
+ G_TYPE_STRING | G_SIGNAL_TYPE_STATIC_SCOPE);
+}
+
+typedef struct
+{
+ DConfClient *client;
+ gchar *prefix;
+ gchar **changes;
+ gchar *tag;
+ gboolean is_writability;
+} DConfClientChange;
+
+static gboolean
+dconf_client_dispatch_change_signal (gpointer user_data)
+{
+ DConfClientChange *change = user_data;
+
+ if (change->is_writability)
+ {
+ /* We know that the engine does it this way... */
+ g_assert (change->changes[0][0] == '\0' && change->changes[1] == NULL);
+
+ g_signal_emit (change->client,
+ dconf_client_signals[SIGNAL_WRITABILITY_CHANGED], 0,
+ change->prefix);
+ }
+
+ g_signal_emit (change->client, dconf_client_signals[SIGNAL_CHANGED], 0,
+ change->prefix, change->changes, change->tag);
+
+ g_object_unref (change->client);
+ g_free (change->prefix);
+ g_strfreev (change->changes);
+ g_free (change->tag);
+ g_slice_free (DConfClientChange, change);
+
+ return G_SOURCE_REMOVE;
+}
+
+void
+dconf_engine_change_notify (DConfEngine *engine,
+ const gchar *prefix,
+ const gchar * const *changes,
+ const gchar * tag,
+ gboolean is_writability,
+ gpointer origin_tag,
+ gpointer user_data)
+{
+ GWeakRef *weak_ref = user_data;
+ DConfClientChange *change;
+ DConfClient *client;
+
+ client = g_weak_ref_get (weak_ref);
+
+ if (client == NULL)
+ return;
+
+ g_return_if_fail (DCONF_IS_CLIENT (client));
+
+ change = g_slice_new (DConfClientChange);
+ change->client = client;
+ change->prefix = g_strdup (prefix);
+ change->changes = g_strdupv ((gchar **) changes);
+ change->tag = g_strdup (tag);
+ change->is_writability = is_writability;
+
+ g_main_context_invoke (client->context, dconf_client_dispatch_change_signal, change);
+}
+
+static void
+dconf_client_free_weak_ref (gpointer data)
+{
+ GWeakRef *weak_ref = data;
+
+ g_weak_ref_clear (weak_ref);
+ g_slice_free (GWeakRef, weak_ref);
+}
+
+/**
+ * dconf_client_new:
+ *
+ * Creates a new #DConfClient.
+ *
+ * Returns: (transfer full): a new #DConfClient
+ **/
+DConfClient *
+dconf_client_new (void)
+{
+ DConfClient *client;
+ GWeakRef *weak_ref;
+
+ client = g_object_new (DCONF_TYPE_CLIENT, NULL);
+ weak_ref = g_slice_new (GWeakRef);
+ g_weak_ref_init (weak_ref, client);
+ client->engine = dconf_engine_new (NULL, weak_ref, dconf_client_free_weak_ref);
+ client->context = g_main_context_ref_thread_default ();
+
+ return client;
+}
+
+/**
+ * dconf_client_read:
+ * @client: a #DConfClient
+ * @key: the key to read the value of
+ *
+ * Reads the current value of @key.
+ *
+ * If @key exists, its value is returned. Otherwise, %NULL is returned.
+ *
+ * If there are outstanding "fast" changes in progress they may affect
+ * the result of this call.
+ *
+ * Returns: (transfer full) (nullable): a #GVariant, or %NULL
+ **/
+GVariant *
+dconf_client_read (DConfClient *client,
+ const gchar *key)
+{
+ g_return_val_if_fail (DCONF_IS_CLIENT (client), NULL);
+
+ return dconf_engine_read (client->engine, DCONF_READ_FLAGS_NONE, NULL, key);
+}
+
+/**
+ * DConfReadFlags:
+ * @DCONF_READ_FLAGS_NONE: no flags
+ * @DCONF_READ_DEFAULT_VALUE: read the default value, ignoring any
+ * values in writable databases or any queued changes. This is
+ * effectively equivalent to asking what value would be read after a
+ * reset was written for the key in question.
+ * @DCONF_READ_USER_VALUE: read the user value, ignoring any system
+ * databases, including ignoring locks. It is even possible to read
+ * "invisible" values in the user database in this way, which would
+ * have normally been ignored because of locks.
+ *
+ * Since: 0.26
+ */
+
+/**
+ * dconf_client_read_full:
+ * @client: a #DConfClient
+ * @key: the key to read the default value of
+ * @flags: #DConfReadFlags
+ * @read_through: a #GQueue of #DConfChangeset
+ *
+ * Reads the current value of @key.
+ *
+ * If @flags contains %DCONF_READ_USER_VALUE then only the user value
+ * will be read. Locks are ignored, which means that it is possible to
+ * use this API to read "invisible" user values which are hidden by
+ * system locks.
+ *
+ * If @flags contains %DCONF_READ_DEFAULT_VALUE then only non-user
+ * values will be read. The result will be exactly equivalent to the
+ * value that would be read if the current value of the key were to be
+ * reset.
+ *
+ * Flags may not contain both %DCONF_READ_USER_VALUE and
+ * %DCONF_READ_DEFAULT_VALUE.
+ *
+ * If @read_through is non-%NULL, %DCONF_READ_DEFAULT_VALUE is not
+ * given then @read_through is checked for the key in question, subject
+ * to the restriction that the key in question is writable. This
+ * effectively answers the question of "what would happen if these
+ * changes were committed".
+ *
+ * If there are outstanding "fast" changes in progress they may affect
+ * the result of this call.
+ *
+ * If @flags is %DCONF_READ_FLAGS_NONE and @read_through is %NULL then
+ * this call is exactly equivalent to dconf_client_read().
+ *
+ * Returns: (transfer full) (nullable): a #GVariant, or %NULL
+ *
+ * Since: 0.26
+ */
+GVariant *
+dconf_client_read_full (DConfClient *client,
+ const gchar *key,
+ DConfReadFlags flags,
+ const GQueue *read_through)
+{
+ g_return_val_if_fail (DCONF_IS_CLIENT (client), NULL);
+
+ return dconf_engine_read (client->engine, flags, read_through, key);
+}
+
+/**
+ * dconf_client_list:
+ * @client: a #DConfClient
+ * @dir: the dir to list the contents of
+ * @length: the length of the returned list
+ *
+ * Gets the list of all dirs and keys immediately under @dir.
+ *
+ * If @length is non-%NULL then it will be set to the length of the
+ * returned array. In any case, the array is %NULL-terminated.
+ *
+ * IF there are outstanding "fast" changes in progress then this call
+ * may return inaccurate results with respect to those outstanding
+ * changes.
+ *
+ * Returns: (transfer full) (not nullable): an array of strings, never %NULL.
+ **/
+gchar **
+dconf_client_list (DConfClient *client,
+ const gchar *dir,
+ gint *length)
+{
+ g_return_val_if_fail (DCONF_IS_CLIENT (client), NULL);
+
+ return dconf_engine_list (client->engine, dir, length);
+}
+
+/**
+ * dconf_client_list_locks:
+ * @client: a #DConfClient
+ * @dir: the dir to limit results to
+ * @length: the length of the returned list.
+ *
+ * Lists all locks under @dir in effect for @client.
+ *
+ * If no locks are in effect, an empty list is returned. If no keys are
+ * writable at all then a list containing @dir is returned.
+ *
+ * The returned list will be %NULL-terminated.
+ *
+ * Returns: (transfer full) (not nullable): an array of strings, never %NULL.
+ *
+ * Since: 0.26
+ */
+gchar **
+dconf_client_list_locks (DConfClient *client,
+ const gchar *dir,
+ gint *length)
+{
+ g_return_val_if_fail (DCONF_IS_CLIENT (client), NULL);
+ g_return_val_if_fail (dconf_is_dir (dir, NULL), NULL);
+
+ return dconf_engine_list_locks (client->engine, dir, length);
+}
+
+/**
+ * dconf_client_is_writable:
+ * @client: a #DConfClient
+ * @key: the key to check for writability
+ *
+ * Checks if @key is writable (ie: the key has no locks).
+ *
+ * This call does not verify that writing to the key will actually be
+ * successful. It only checks that the database is writable and that
+ * there are no locks affecting @key. Other issues (such as a full disk
+ * or an inability to connect to the bus and start the service) may
+ * cause the write to fail.
+ *
+ * Returns: %TRUE if @key is writable
+ **/
+gboolean
+dconf_client_is_writable (DConfClient *client,
+ const gchar *key)
+{
+ g_return_val_if_fail (DCONF_IS_CLIENT (client), FALSE);
+
+ return dconf_engine_is_writable (client->engine, key);
+}
+
+/**
+ * dconf_client_write_fast:
+ * @client: a #DConfClient
+ * @key: the key to write to
+ * @value: a #GVariant, the value to write. If it has a floating reference it's
+ * consumed.
+ * @error: a pointer to a %NULL #GError, or %NULL
+ *
+ * Writes @value to the given @key, or reset @key to its default value.
+ *
+ * If @value is %NULL then @key is reset to its default value (which may
+ * be completely unset), otherwise @value becomes the new value.
+ *
+ * This call merely queues up the write and returns immediately, without
+ * blocking. The only errors that can be detected or reported at this
+ * point are attempts to write to read-only keys. If the application
+ * exits immediately after this function returns then the queued call
+ * may never be sent; see dconf_client_sync().
+ *
+ * A local copy of the written value is kept so that calls to
+ * dconf_client_read() that occur before the service actually makes the
+ * change will return the new value.
+ *
+ * If the write is queued then a change signal will be directly emitted.
+ * If this function is being called from the main context of @client
+ * then the signal is emitted before this function returns; otherwise it
+ * is scheduled on the main context.
+ *
+ * Returns: %TRUE if the write was queued
+ **/
+gboolean
+dconf_client_write_fast (DConfClient *client,
+ const gchar *key,
+ GVariant *value,
+ GError **error)
+{
+ DConfChangeset *changeset;
+ gboolean success;
+
+ g_return_val_if_fail (DCONF_IS_CLIENT (client), FALSE);
+
+ changeset = dconf_changeset_new_write (key, value);
+ success = dconf_engine_change_fast (client->engine, changeset, NULL, error);
+ dconf_changeset_unref (changeset);
+
+ return success;
+}
+
+/**
+ * dconf_client_write_sync:
+ * @client: a #DConfClient
+ * @key: the key to write to
+ * @value: a #GVariant, the value to write. If it has a floating reference it's
+ * consumed.
+ * @tag: (out) (optional) (not nullable) (transfer full): the tag from this write
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a pointer to a %NULL #GError, or %NULL
+ *
+ * Write @value to the given @key, or reset @key to its default value.
+ *
+ * If @value is %NULL then @key is reset to its default value (which may
+ * be completely unset), otherwise @value becomes the new value.
+ *
+ * This call blocks until the write is complete. This call will
+ * therefore detect and report all cases of failure. If the modified
+ * key is currently being watched then a signal will be emitted from the
+ * main context of @client (once the signal arrives from the service).
+ *
+ * If @tag is non-%NULL then it is set to the unique tag associated with
+ * this write. This is the same tag that will appear in the following
+ * change signal.
+ *
+ * Returns: %TRUE on success, else %FALSE with @error set
+ **/
+gboolean
+dconf_client_write_sync (DConfClient *client,
+ const gchar *key,
+ GVariant *value,
+ gchar **tag,
+ GCancellable *cancellable,
+ GError **error)
+{
+ DConfChangeset *changeset;
+ gboolean success;
+
+ g_return_val_if_fail (DCONF_IS_CLIENT (client), FALSE);
+
+ changeset = dconf_changeset_new_write (key, value);
+ success = dconf_engine_change_sync (client->engine, changeset, tag, error);
+ dconf_changeset_unref (changeset);
+
+ return success;
+}
+
+/**
+ * dconf_client_change_fast:
+ * @client: a #DConfClient
+ * @changeset: the changeset describing the requested change
+ * @error: a pointer to a %NULL #GError, or %NULL
+ *
+ * Performs the change operation described by @changeset.
+ *
+ * Once @changeset is passed to this call it can no longer be modified.
+ *
+ * This call merely queues up the write and returns immediately, without
+ * blocking. The only errors that can be detected or reported at this
+ * point are attempts to write to read-only keys. If the application
+ * exits immediately after this function returns then the queued call
+ * may never be sent; see dconf_client_sync().
+ *
+ * A local copy of the written value is kept so that calls to
+ * dconf_client_read() that occur before the service actually makes the
+ * change will return the new value.
+ *
+ * If the write is queued then a change signal will be directly emitted.
+ * If this function is being called from the main context of @client
+ * then the signal is emitted before this function returns; otherwise it
+ * is scheduled on the main context.
+ *
+ * Returns: %TRUE if the requested changed was queued
+ **/
+gboolean
+dconf_client_change_fast (DConfClient *client,
+ DConfChangeset *changeset,
+ GError **error)
+{
+ g_return_val_if_fail (DCONF_IS_CLIENT (client), FALSE);
+
+ return dconf_engine_change_fast (client->engine, changeset, NULL, error);
+}
+
+/**
+ * dconf_client_change_sync:
+ * @client: a #DConfClient
+ * @changeset: the changeset describing the requested change
+ * @tag: (out) (optional) (not nullable) (transfer full): the tag from this write
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a pointer to a %NULL #GError, or %NULL
+ *
+ * Performs the change operation described by @changeset.
+ *
+ * Once @changeset is passed to this call it can no longer be modified.
+ *
+ * This call blocks until the change is complete. This call will
+ * therefore detect and report all cases of failure. If any of the
+ * modified keys are currently being watched then a signal will be
+ * emitted from the main context of @client (once the signal arrives
+ * from the service).
+ *
+ * If @tag is non-%NULL then it is set to the unique tag associated with
+ * this change. This is the same tag that will appear in the following
+ * change signal. If @changeset makes no changes then @tag may be
+ * non-unique (eg: the empty string may be used for empty changesets).
+ *
+ * Returns: %TRUE on success, else %FALSE with @error set
+ **/
+gboolean
+dconf_client_change_sync (DConfClient *client,
+ DConfChangeset *changeset,
+ gchar **tag,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_fail (DCONF_IS_CLIENT (client), FALSE);
+
+ return dconf_engine_change_sync (client->engine, changeset, tag, error);
+}
+
+/**
+ * dconf_client_watch_fast:
+ * @client: a #DConfClient
+ * @path: a path to watch
+ *
+ * Requests change notifications for @path.
+ *
+ * If @path is a key then the single key is monitored. If @path is a
+ * dir then all keys under the dir are monitored.
+ *
+ * This function queues the watch request with D-Bus and returns
+ * immediately. There is a very slim chance that the dconf database
+ * could change before the watch is actually established. If that is
+ * the case then a synthetic change signal will be emitted.
+ *
+ * Errors are silently ignored.
+ **/
+void
+dconf_client_watch_fast (DConfClient *client,
+ const gchar *path)
+{
+ g_return_if_fail (DCONF_IS_CLIENT (client));
+
+ dconf_engine_watch_fast (client->engine, path);
+}
+
+/**
+ * dconf_client_watch_sync:
+ * @client: a #DConfClient
+ * @path: a path to watch
+ *
+ * Requests change notifications for @path.
+ *
+ * If @path is a key then the single key is monitored. If @path is a
+ * dir then all keys under the dir are monitored.
+ *
+ * This function submits each of the various watch requests that are
+ * required to monitor a key and waits until each of them returns. By
+ * the time this function returns, the watch has been established.
+ *
+ * Errors are silently ignored.
+ **/
+void
+dconf_client_watch_sync (DConfClient *client,
+ const gchar *path)
+{
+ g_return_if_fail (DCONF_IS_CLIENT (client));
+
+ dconf_engine_watch_sync (client->engine, path);
+}
+
+/**
+ * dconf_client_unwatch_fast:
+ * @client: a #DConfClient
+ * @path: a path previously watched
+ *
+ * Cancels the effect of a previous call to dconf_client_watch_fast().
+ *
+ * This call returns immediately.
+ *
+ * It is still possible that change signals are received after this call
+ * had returned (watching guarantees notification of changes, but
+ * unwatching does not guarantee no notifications).
+ **/
+void
+dconf_client_unwatch_fast (DConfClient *client,
+ const gchar *path)
+{
+ g_return_if_fail (DCONF_IS_CLIENT (client));
+
+ dconf_engine_unwatch_fast (client->engine, path);
+}
+
+/**
+ * dconf_client_unwatch_sync:
+ * @client: a #DConfClient
+ * @path: a path previously watched
+ *
+ * Cancels the effect of a previous call to dconf_client_watch_sync().
+ *
+ * This function submits each of the various unwatch requests and waits
+ * until each of them returns. It is still possible that change signals
+ * are received after this call has returned (watching guarantees
+ * notification of changes, but unwatching does not guarantee no
+ * notifications).
+ **/
+void
+dconf_client_unwatch_sync (DConfClient *client,
+ const gchar *path)
+{
+ g_return_if_fail (DCONF_IS_CLIENT (client));
+
+ dconf_engine_unwatch_sync (client->engine, path);
+}
+
+/**
+ * dconf_client_sync:
+ * @client: a #DConfClient
+ *
+ * Blocks until all outstanding "fast" change or write operations have
+ * been submitted to the service.
+ *
+ * Applications should generally call this before exiting on any
+ * #DConfClient that they wrote to.
+ **/
+void
+dconf_client_sync (DConfClient *client)
+{
+ g_return_if_fail (DCONF_IS_CLIENT (client));
+
+ dconf_engine_sync (client->engine);
+}
diff --git a/client/dconf-client.h b/client/dconf-client.h
new file mode 100644
index 0000000..94c304a
--- /dev/null
+++ b/client/dconf-client.h
@@ -0,0 +1,88 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __dconf_client_h__
+#define __dconf_client_h__
+
+#include <gio/gio.h>
+#include "../common/dconf-changeset.h"
+#include "../common/dconf-enums.h"
+
+G_BEGIN_DECLS
+
+#define DCONF_TYPE_CLIENT (dconf_client_get_type ())
+G_DECLARE_FINAL_TYPE(DConfClient, dconf_client, DCONF, CLIENT, GObject)
+
+DConfClient * dconf_client_new (void);
+
+GVariant * dconf_client_read (DConfClient *client,
+ const gchar *key);
+
+GVariant * dconf_client_read_full (DConfClient *client,
+ const gchar *key,
+ DConfReadFlags flags,
+ const GQueue *read_through);
+
+gchar ** dconf_client_list (DConfClient *client,
+ const gchar *dir,
+ gint *length);
+
+gchar ** dconf_client_list_locks (DConfClient *client,
+ const gchar *dir,
+ gint *length);
+
+gboolean dconf_client_is_writable (DConfClient *client,
+ const gchar *key);
+
+gboolean dconf_client_write_fast (DConfClient *client,
+ const gchar *key,
+ GVariant *value,
+ GError **error);
+gboolean dconf_client_write_sync (DConfClient *client,
+ const gchar *key,
+ GVariant *value,
+ gchar **tag,
+ GCancellable *cancellable,
+ GError **error);
+
+gboolean dconf_client_change_fast (DConfClient *client,
+ DConfChangeset *changeset,
+ GError **error);
+gboolean dconf_client_change_sync (DConfClient *client,
+ DConfChangeset *changeset,
+ gchar **tag,
+ GCancellable *cancellable,
+ GError **error);
+
+void dconf_client_watch_fast (DConfClient *client,
+ const gchar *path);
+void dconf_client_watch_sync (DConfClient *client,
+ const gchar *path);
+
+void dconf_client_unwatch_fast (DConfClient *client,
+ const gchar *path);
+void dconf_client_unwatch_sync (DConfClient *client,
+ const gchar *path);
+
+void dconf_client_sync (DConfClient *client);
+
+
+G_END_DECLS
+
+#endif /* __dconf_client_h__ */
diff --git a/client/dconf.deps b/client/dconf.deps
new file mode 100644
index 0000000..cd10dfd
--- /dev/null
+++ b/client/dconf.deps
@@ -0,0 +1 @@
+gio-2.0
diff --git a/client/dconf.h b/client/dconf.h
new file mode 100644
index 0000000..77bda7c
--- /dev/null
+++ b/client/dconf.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __dconf_h__
+#define __dconf_h__
+
+#include <common/dconf-enums.h>
+#include <common/dconf-paths.h>
+#include <common/dconf-changeset.h>
+#include <client/dconf-client.h>
+
+#endif /* __dconf_h__ */
diff --git a/client/dconf.vapi b/client/dconf.vapi
new file mode 100644
index 0000000..62c2e65
--- /dev/null
+++ b/client/dconf.vapi
@@ -0,0 +1,70 @@
+/* dconf.vapi generated by valac 0.17.1.35-814b, do not modify. */
+
+namespace DConf {
+ [CCode (cheader_filename = "dconf.h", cprefix="DCONF_READ_")]
+ public enum ReadFlags {
+ [CCode (cname="DCONF_READ_FLAGS_NONE")]
+ NONE,
+ DEFAULT_VALUE,
+ USER_VALUE
+ }
+
+ [CCode (cheader_filename = "dconf.h")]
+ public class Client : GLib.Object {
+ public signal void changed (string prefix, [CCode (array_length = false, array_null_terminated = true)] string[] changes, string? tag);
+
+ public Client ();
+ public GLib.Variant? read (string key);
+ public GLib.Variant? read_full (string key, ReadFlags flags, GLib.Queue<Changeset>? read_through);
+ public string[] list (string dir);
+ public string[] list_locks (string dir);
+ public bool is_writable (string key);
+ public void write_fast (string path, GLib.Variant? value) throws GLib.Error;
+ public void write_sync (string path, GLib.Variant? value, out string tag = null, GLib.Cancellable? cancellable = null) throws GLib.Error;
+ public void change_fast (Changeset changeset) throws GLib.Error;
+ public void change_sync (Changeset changeset, out string tag = null, GLib.Cancellable? cancellable = null) throws GLib.Error;
+ public void watch_fast (string path);
+ public void unwatch_fast (string path);
+ public void watch_sync (string path);
+ public void unwatch_sync (string path);
+ }
+
+ [Compact]
+ [CCode (ref_function = "dconf_changeset_ref", unref_function = "dconf_changeset_unref")]
+ public class Changeset {
+ public delegate bool Predicate (string path, GLib.Variant? value);
+ public Changeset ();
+ public Changeset.write (string path, GLib.Variant? value);
+ public void set (string path, GLib.Variant? value);
+ public bool get (string path, out GLib.Variant? value);
+ public bool is_similar_to (Changeset other);
+ public bool all (Predicate predicate);
+ public GLib.Variant serialise ();
+ public static Changeset deserialise (GLib.Variant serialised);
+ }
+
+ [CCode (cheader_filename = "dconf.h")]
+ public static bool is_dir (string str, GLib.Error* error = null);
+ [CCode (cheader_filename = "dconf.h")]
+ public static bool is_key (string str, GLib.Error* error = null);
+ [CCode (cheader_filename = "dconf.h")]
+ public static bool is_path (string str, GLib.Error* error = null);
+ [CCode (cheader_filename = "dconf.h")]
+ public static bool is_rel_dir (string str, GLib.Error* error = null);
+ [CCode (cheader_filename = "dconf.h")]
+ public static bool is_rel_key (string str, GLib.Error* error = null);
+ [CCode (cheader_filename = "dconf.h")]
+ public static bool is_rel_path (string str, GLib.Error* error = null);
+ [CCode (cheader_filename = "dconf.h", cname = "dconf_is_dir")]
+ public static bool verify_dir (string str) throws GLib.Error;
+ [CCode (cheader_filename = "dconf.h", cname = "dconf_is_key")]
+ public static bool verify_key (string str) throws GLib.Error;
+ [CCode (cheader_filename = "dconf.h", cname = "dconf_is_path")]
+ public static bool verify_path (string str) throws GLib.Error;
+ [CCode (cheader_filename = "dconf.h", cname = "dconf_is_rel_dir")]
+ public static bool verify_rel_dir (string str) throws GLib.Error;
+ [CCode (cheader_filename = "dconf.h", cname = "dconf_is_rel_key")]
+ public static bool verify_rel_key (string str) throws GLib.Error;
+ [CCode (cheader_filename = "dconf.h", cname = "dconf_is_rel_path")]
+ public static bool verify_rel_path (string str) throws GLib.Error;
+}
diff --git a/client/meson.build b/client/meson.build
new file mode 100644
index 0000000..f3b7122
--- /dev/null
+++ b/client/meson.build
@@ -0,0 +1,79 @@
+client_inc = include_directories('.')
+
+install_headers(
+ 'dconf.h',
+ subdir: 'dconf',
+)
+
+install_headers(
+ 'dconf-client.h',
+ subdir: join_paths('dconf', 'client'),
+)
+
+sources = files('dconf-client.c')
+
+deps = [
+ libdconf_common_hidden_dep,
+ libdconf_gdbus_thread_dep,
+]
+
+libdconf_client = static_library(
+ 'dconf-client',
+ sources: sources,
+ include_directories: top_inc,
+ dependencies: libdconf_gdbus_thread_dep,
+ c_args: dconf_c_args,
+ pic: true,
+)
+
+libdconf_client_dep = declare_dependency(
+ dependencies: gio_dep,
+ link_whole: libdconf_client,
+)
+
+libdconf = shared_library(
+ 'dconf',
+ sources: sources,
+ version: libversion,
+ soversion: soversion,
+ include_directories: top_inc,
+ dependencies: deps,
+ c_args: dconf_c_args,
+ install: true,
+)
+
+libdconf_dep = declare_dependency(
+ include_directories: client_inc,
+ dependencies: gio_dep,
+ link_with: libdconf,
+)
+
+pkg.generate(
+ libdconf,
+ description: 'dconf client library',
+ subdirs: 'dconf',
+ requires: 'gio-2.0 ' + gio_req_version,
+ variables: 'exec_prefix=${prefix}',
+)
+
+if get_option('vapi')
+ vapigen_dep = dependency('vapigen')
+
+ vapi_data = files(
+ 'dconf.vapi',
+ 'dconf.deps',
+ )
+
+ vapi_dir = vapigen_dep.get_pkgconfig_variable(
+ 'vapidir',
+ define_variable: ['datadir', dconf_datadir]
+ )
+
+ install_data(
+ vapi_data,
+ install_dir: vapi_dir,
+ )
+endif
+
+
+
diff --git a/common/dconf-changeset.c b/common/dconf-changeset.c
new file mode 100644
index 0000000..c80c88c
--- /dev/null
+++ b/common/dconf-changeset.c
@@ -0,0 +1,845 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-changeset.h"
+#include "dconf-paths.h"
+
+#include <string.h>
+#include <stdlib.h>
+
+/**
+ * SECTION:changeset
+ * @title: DConfChangeset
+ * @Short_description: A set of changes to a dconf database
+ *
+ * #DConfChangeset represents a set of changes that can be made to a
+ * dconf database. Currently supported operations are writing new
+ * values to keys and resetting keys and dirs.
+ *
+ * Create the changeset with dconf_changeset_new() and populate it with
+ * dconf_changeset_set(). Submit it to dconf with
+ * dconf_client_change_fast() or dconf_client_change_sync().
+ * dconf_changeset_new_write() is a convenience constructor for the
+ * common case of writing or resetting a single value.
+ **/
+
+/**
+ * DConfChangeset:
+ *
+ * This is a reference counted opaque structure type. It is not a
+ * #GObject.
+ *
+ * Use dconf_changeset_ref() and dconf_changeset_unref() to manipulate
+ * references.
+ **/
+
+struct _DConfChangeset
+{
+ GHashTable *table;
+ GHashTable *dir_resets;
+ guint is_database : 1;
+ guint is_sealed : 1;
+ gint ref_count;
+
+ gchar *prefix;
+ const gchar **paths;
+ GVariant **values;
+};
+
+static void
+unref_gvariant0 (gpointer data)
+{
+ if (data)
+ g_variant_unref (data);
+}
+
+/**
+ * dconf_changeset_new:
+ *
+ * Creates a new, empty, #DConfChangeset.
+ *
+ * Returns: (transfer full): the new #DConfChangeset.
+ **/
+DConfChangeset *
+dconf_changeset_new (void)
+{
+ DConfChangeset *changeset;
+
+ changeset = g_slice_new0 (DConfChangeset);
+ changeset->table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, unref_gvariant0);
+ changeset->ref_count = 1;
+
+ return changeset;
+}
+
+/**
+ * dconf_changeset_new_database:
+ * @copy_of: (nullable): a #DConfChangeset to copy
+ *
+ * Creates a new #DConfChangeset in "database" mode, possibly
+ * initialising it with the values of another changeset.
+ *
+ * In a certain sense it's possible to imagine that a #DConfChangeset
+ * could express the contents of an entire dconf database -- the
+ * contents are the database are what you would have if you applied the
+ * changeset to an empty database. One thing that fails to map in this
+ * analogy are reset operations -- if we start with an empty database
+ * then reset operations are meaningless.
+ *
+ * A "database" mode changeset is therefore a changeset which is
+ * incapable of containing reset operations.
+ *
+ * It is not permitted to use a database-mode changeset for most
+ * operations (such as the @change argument to dconf_changeset_change()
+ * or the @changeset argument to #DConfClient APIs).
+ *
+ * If @copy_of is non-%NULL then its contents will be copied into the
+ * created changeset. @copy_of must be a database-mode changeset.
+ *
+ * Returns: (transfer full): a new #DConfChangeset in "database" mode
+ *
+ * Since: 0.16
+ */
+DConfChangeset *
+dconf_changeset_new_database (DConfChangeset *copy_of)
+{
+ DConfChangeset *changeset;
+
+ g_return_val_if_fail (copy_of == NULL || copy_of->is_database, NULL);
+
+ changeset = dconf_changeset_new ();
+ changeset->is_database = TRUE;
+
+ if (copy_of)
+ {
+ GHashTableIter iter;
+ gpointer key, value;
+
+ g_hash_table_iter_init (&iter, copy_of->table);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ g_hash_table_insert (changeset->table, g_strdup (key), g_variant_ref (value));
+ }
+
+ return changeset;
+}
+
+/**
+ * dconf_changeset_unref:
+ * @changeset: a #DConfChangeset
+ *
+ * Releases a #DConfChangeset reference.
+ **/
+void
+dconf_changeset_unref (DConfChangeset *changeset)
+{
+ if (g_atomic_int_dec_and_test (&changeset->ref_count))
+ {
+ g_free (changeset->prefix);
+ g_free (changeset->paths);
+ g_free (changeset->values);
+
+ g_hash_table_unref (changeset->table);
+
+ if (changeset->dir_resets)
+ g_hash_table_unref (changeset->dir_resets);
+
+ g_slice_free (DConfChangeset, changeset);
+ }
+}
+
+/**
+ * dconf_changeset_ref:
+ * @changeset: a #DConfChangeset
+ *
+ * Increases the reference count on @changeset
+ *
+ * Returns: (transfer full): @changeset
+ **/
+DConfChangeset *
+dconf_changeset_ref (DConfChangeset *changeset)
+{
+ g_atomic_int_inc (&changeset->ref_count);
+
+ return changeset;
+}
+
+static void
+dconf_changeset_record_dir_reset (DConfChangeset *changeset,
+ const gchar *dir)
+{
+ g_return_if_fail (dconf_is_dir (dir, NULL));
+ g_return_if_fail (!changeset->is_database);
+ g_return_if_fail (!changeset->is_sealed);
+
+ if (!changeset->dir_resets)
+ changeset->dir_resets = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+ g_hash_table_insert (changeset->table, g_strdup (dir), NULL);
+ g_hash_table_add (changeset->dir_resets, g_strdup (dir));
+}
+
+/**
+ * dconf_changeset_set:
+ * @changeset: a #DConfChangeset
+ * @path: a path to modify
+ * @value: (nullable): the value for the key, or %NULL to reset. If it has a
+ * floating reference it's consumed.
+ *
+ * Adds an operation to modify @path to a #DConfChangeset.
+ *
+ * @path may either be a key or a dir. If it is a key then @value may
+ * be a #GVariant, or %NULL (to set or reset the key).
+ *
+ * If @path is a dir then this must be a reset operation: @value must be
+ * %NULL. It is not permitted to assign a #GVariant value to a dir.
+ **/
+void
+dconf_changeset_set (DConfChangeset *changeset,
+ const gchar *path,
+ GVariant *value)
+{
+ g_return_if_fail (!changeset->is_sealed);
+ g_return_if_fail (dconf_is_path (path, NULL));
+
+ /* Check if we are performing a path reset */
+ if (g_str_has_suffix (path, "/"))
+ {
+ GHashTableIter iter;
+ gpointer key;
+
+ g_return_if_fail (value == NULL);
+
+ /* When we reset a path we must also reset all keys within that
+ * path.
+ */
+ g_hash_table_iter_init (&iter, changeset->table);
+ while (g_hash_table_iter_next (&iter, &key, NULL))
+ if (g_str_has_prefix (key, path))
+ g_hash_table_iter_remove (&iter);
+
+ /* If this is a non-database then record the reset itself. */
+ if (!changeset->is_database)
+ dconf_changeset_record_dir_reset (changeset, path);
+ }
+
+ /* ...or a value reset */
+ else if (value == NULL)
+ {
+ /* If we're a non-database, record the reset explicitly.
+ * Otherwise, just reset whatever may be there already.
+ */
+ if (!changeset->is_database)
+ g_hash_table_insert (changeset->table, g_strdup (path), NULL);
+ else
+ g_hash_table_remove (changeset->table, path);
+ }
+
+ /* ...or a normal write. */
+ else
+ g_hash_table_insert (changeset->table, g_strdup (path), g_variant_ref_sink (value));
+}
+
+/**
+ * dconf_changeset_get:
+ * @changeset: a #DConfChangeset
+ * @key: the key to check
+ * @value: (transfer full) (optional) (nullable): a return location for the value, or %NULL
+ *
+ * Checks if a #DConfChangeset has an outstanding request to change
+ * the value of the given @key.
+ *
+ * If the change doesn't involve @key then %FALSE is returned and the
+ * @value is unmodified.
+ *
+ * If the change modifies @key then @value is set either to the value
+ * for that key, or %NULL in the case that the key is being reset by the
+ * request.
+ *
+ * Returns: %TRUE if the key is being modified by the change
+ */
+gboolean
+dconf_changeset_get (DConfChangeset *changeset,
+ const gchar *key,
+ GVariant **value)
+{
+ gpointer tmp;
+
+ if (!g_hash_table_lookup_extended (changeset->table, key, NULL, &tmp))
+ {
+ /* Did not find an exact match, so check for dir resets */
+ if (changeset->dir_resets)
+ {
+ GHashTableIter iter;
+ gpointer dir;
+
+ g_hash_table_iter_init (&iter, changeset->dir_resets);
+ while (g_hash_table_iter_next (&iter, &dir, NULL))
+ if (g_str_has_prefix (key, dir))
+ {
+ if (value)
+ *value = NULL;
+
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+ }
+
+ if (value)
+ *value = tmp ? g_variant_ref (tmp) : NULL;
+
+ return TRUE;
+}
+
+/**
+ * dconf_changeset_is_similar_to:
+ * @changeset: a #DConfChangeset
+ * @other: another #DConfChangeset
+ *
+ * Checks if @changeset is similar to @other.
+ *
+ * Two changes are considered similar if they write to the exact same
+ * set of keys. The values written are not considered.
+ *
+ * This check is used to prevent building up a queue of repeated writes
+ * of the same keys. This is often seen when an application writes to a
+ * key on every move of a slider or an application window.
+ *
+ * Strictly speaking, a write resettings all of "/a/" after a write
+ * containing "/a/b" could cause the later to be removed from the queue,
+ * but this situation is difficult to detect and is expected to be
+ * extremely rare.
+ *
+ * Returns: %TRUE if the changes are similar
+ **/
+gboolean
+dconf_changeset_is_similar_to (DConfChangeset *changeset,
+ DConfChangeset *other)
+{
+ GHashTableIter iter;
+ gpointer key;
+
+ if (g_hash_table_size (changeset->table) != g_hash_table_size (other->table))
+ return FALSE;
+
+ g_hash_table_iter_init (&iter, changeset->table);
+ while (g_hash_table_iter_next (&iter, &key, NULL))
+ if (!g_hash_table_contains (other->table, key))
+ return FALSE;
+
+ return TRUE;
+}
+
+/**
+ * DConfChangesetPredicate:
+ * @path: a path, as per dconf_is_path()
+ * @value: (nullable): a #GVariant, or %NULL
+ * @user_data: user data pointer
+ *
+ * Callback function type for predicates over items in a
+ * #DConfChangeset.
+ *
+ * Use with dconf_changeset_all().
+ *
+ * Returns: %TRUE if the predicate is met for the given @path and @value
+ **/
+
+/**
+ * dconf_changeset_all:
+ * @changeset: a #DConfChangeset
+ * @predicate: a #DConfChangesetPredicate
+ * @user_data: user data to pass to @predicate
+ *
+ * Checks if all changes in the changeset satisfy @predicate.
+ *
+ * @predicate is called on each item in the changeset, in turn, until it
+ * returns %FALSE.
+ *
+ * If @predicate returns %FALSE for any item, this function returns
+ * %FALSE. If not (including the case of no items) then this function
+ * returns %TRUE.
+ *
+ * Returns: %TRUE if all items in @changeset satisfy @predicate
+ */
+gboolean
+dconf_changeset_all (DConfChangeset *changeset,
+ DConfChangesetPredicate predicate,
+ gpointer user_data)
+{
+ GHashTableIter iter;
+ gpointer key, value;
+
+ g_hash_table_iter_init (&iter, changeset->table);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ if (!(* predicate) (key, value, user_data))
+ return FALSE;
+
+ return TRUE;
+}
+
+static gint
+dconf_changeset_string_ptr_compare (gconstpointer a_p,
+ gconstpointer b_p)
+{
+ const gchar * const *a = a_p;
+ const gchar * const *b = b_p;
+
+ return strcmp (*a, *b);
+}
+
+/**
+ * dconf_changeset_seal:
+ * @changeset: a #DConfChangeset
+ *
+ * Seals @changeset.
+ *
+ * When a #DConfChangeset is first created, it is mutable and
+ * non-threadsafe. Once the changeset is populated with the required
+ * changes, it can be shared between multiple threads, but only by
+ * making it immutable by "sealing" it.
+ *
+ * After the changeset is sealed, you cannot call dconf_changeset_set()
+ * or any other functions that would modify it. It is safe, however, to
+ * share it between multiple threads.
+ *
+ * All changesets are unsealed on creation, including those that are
+ * made by copying changesets that are sealed.
+ * dconf_changeset_describe() will implicitly seal a changeset.
+ *
+ * This function is idempotent.
+ *
+ * Since: 0.18
+ **/
+void
+dconf_changeset_seal (DConfChangeset *changeset)
+{
+ gsize prefix_length;
+ gint n_items;
+
+ if (changeset->is_sealed)
+ return;
+
+ changeset->is_sealed = TRUE;
+
+ /* This function used to be called dconf_changeset_build_description()
+ * because that's basically what sealing is...
+ */
+
+ n_items = g_hash_table_size (changeset->table);
+
+ /* If there are no items then what is there to describe? */
+ if (n_items == 0)
+ return;
+
+ /* We do three separate passes. This might take a bit longer than
+ * doing it all at once but it keeps the complexity down.
+ *
+ * First, we iterate the table in order to determine the common
+ * prefix.
+ *
+ * Next, we iterate the table again to pull the strings out excluding
+ * the leading prefix.
+ *
+ * We sort the list of paths at this point because the writer
+ * requires a sorted list in order to ensure that dir resets come
+ * before writes to keys in that dir.
+ *
+ * Finally, we iterate over the sorted list and use the normal
+ * hashtable lookup in order to populate the values array in the same
+ * order.
+ *
+ * Doing it this way avoids the complication of trying to sort two
+ * arrays (keys and values) at the same time.
+ */
+
+ /* Pass 1: determine the common prefix. */
+ {
+ GHashTableIter iter;
+ const gchar *first;
+ gboolean have_one;
+ gpointer key;
+
+ g_hash_table_iter_init (&iter, changeset->table);
+
+ /* We checked above that we have at least one item. */
+ have_one = g_hash_table_iter_next (&iter, &key, NULL);
+ g_assert (have_one);
+
+ prefix_length = strlen (key);
+ first = key;
+
+ /* Consider the remaining items to find the common prefix */
+ while (g_hash_table_iter_next (&iter, &key, NULL))
+ {
+ const gchar *this = key;
+ gint i;
+
+ for (i = 0; i < prefix_length; i++)
+ if (first[i] != this[i])
+ {
+ prefix_length = i;
+ break;
+ }
+ }
+
+ /* We must surely always have a common prefix of '/' */
+ g_assert (prefix_length > 0);
+ g_assert (first[0] == '/');
+
+ /* We may find that "/a/ab" and "/a/ac" have a common prefix of
+ * "/a/a" but really we want to trim that back to "/a/".
+ *
+ * If there is only one item, leave it alone.
+ */
+ if (n_items > 1)
+ {
+ while (first[prefix_length - 1] != '/')
+ prefix_length--;
+ }
+
+ changeset->prefix = g_strndup (first, prefix_length);
+ }
+
+ /* Pass 2: collect the list of keys, dropping the prefix */
+ {
+ GHashTableIter iter;
+ gpointer key;
+ gint i = 0;
+
+ changeset->paths = g_new (const gchar *, n_items + 1);
+
+ g_hash_table_iter_init (&iter, changeset->table);
+ while (g_hash_table_iter_next (&iter, &key, NULL))
+ {
+ const gchar *path = key;
+
+ changeset->paths[i++] = path + prefix_length;
+ }
+ changeset->paths[i] = NULL;
+ g_assert (i == n_items);
+
+ /* Sort the list of keys */
+ qsort (changeset->paths, n_items, sizeof (const gchar *), dconf_changeset_string_ptr_compare);
+ }
+
+ /* Pass 3: collect the list of values */
+ {
+ gint i;
+
+ changeset->values = g_new (GVariant *, n_items);
+
+ for (i = 0; i < n_items; i++)
+ /* We dropped the prefix when collecting the array.
+ * Bring it back temporarily, for the lookup.
+ */
+ changeset->values[i] = g_hash_table_lookup (changeset->table, changeset->paths[i] - prefix_length);
+ }
+}
+
+/**
+ * dconf_changeset_describe:
+ * @changeset: a #DConfChangeset
+ * @prefix: (transfer none) (optional) (out): the prefix under which changes have been requested
+ * @paths: (transfer none) (optional) (out): the list of paths changed, relative to @prefix
+ * @values: (transfer none) (optional) (out): the list of values changed
+ *
+ * Describes @changeset.
+ *
+ * @prefix and @paths are presented in the same way as they are for the
+ * DConfClient::changed signal. @values is an array of the same length
+ * as @paths. For each key described by an element in @paths, @values
+ * will contain either a #GVariant (the requested new value of that key)
+ * or %NULL (to reset a reset).
+ *
+ * The @paths array is returned in an order such that dir will always
+ * come before keys contained within those dirs.
+ *
+ * If @changeset is not already sealed then this call will implicitly
+ * seal it. See dconf_changeset_seal().
+ *
+ * Returns: the number of changes (the length of @changes and @values).
+ **/
+guint
+dconf_changeset_describe (DConfChangeset *changeset,
+ const gchar **prefix,
+ const gchar * const **paths,
+ GVariant * const **values)
+{
+ gint n_items;
+
+ n_items = g_hash_table_size (changeset->table);
+
+ dconf_changeset_seal (changeset);
+
+ if (prefix)
+ *prefix = changeset->prefix;
+
+ if (paths)
+ *paths = changeset->paths;
+
+ if (values)
+ *values = changeset->values;
+
+ return n_items;
+}
+
+/**
+ * dconf_changeset_serialise:
+ * @changeset: a #DConfChangeset
+ *
+ * Serialises a #DConfChangeset.
+ *
+ * The returned value has no particular format and should only be passed
+ * to dconf_changeset_deserialise().
+ *
+ * Returns: (transfer full): a floating #GVariant
+ **/
+GVariant *
+dconf_changeset_serialise (DConfChangeset *changeset)
+{
+ GVariantBuilder builder;
+ GHashTableIter iter;
+ gpointer key, value;
+
+ g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{smv}"));
+
+ g_hash_table_iter_init (&iter, changeset->table);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ g_variant_builder_add (&builder, "{smv}", key, value);
+
+ return g_variant_builder_end (&builder);
+}
+
+/**
+ * dconf_changeset_deserialise:
+ * @serialised: (transfer none): a #GVariant from dconf_changeset_serialise()
+ *
+ * Creates a #DConfChangeset according to a serialised description
+ * returned from an earlier call to dconf_changeset_serialise().
+ *
+ * @serialised has no particular format -- you should only pass a value
+ * that resulted from an earlier serialise operation.
+ *
+ * This call never fails, even if @serialised is not in the correct
+ * format. Improperly-formatted parts are simply ignored.
+ *
+ * Returns: (transfer full): a new #DConfChangeset
+ **/
+DConfChangeset *
+dconf_changeset_deserialise (GVariant *serialised)
+{
+ DConfChangeset *changeset;
+ GVariantIter iter;
+ const gchar *key;
+ GVariant *value;
+
+ changeset = dconf_changeset_new ();
+ g_variant_iter_init (&iter, serialised);
+ while (g_variant_iter_loop (&iter, "{&smv}", &key, &value))
+ {
+ /* If value is NULL: we may be resetting a key or a dir (a path).
+ * If value is non-NULL: we must be setting a key.
+ *
+ * ie: it is not possible to set a value to a directory.
+ *
+ * If we get an invalid case, just fall through and ignore it.
+ */
+ if (dconf_is_key (key, NULL))
+ g_hash_table_insert (changeset->table, g_strdup (key), value ? g_variant_ref (value) : NULL);
+
+ else if (dconf_is_dir (key, NULL) && value == NULL)
+ dconf_changeset_record_dir_reset (changeset, key);
+ }
+
+ return changeset;
+}
+
+/**
+ * dconf_changeset_new_write:
+ * @path: a dconf path
+ * @value: (nullable): a #GVariant, or %NULL. If it has a floating reference it's
+ * consumed.
+ *
+ * Creates a new #DConfChangeset with one change. This is equivalent to
+ * calling dconf_changeset_new() and then dconf_changeset_set() with
+ * @path and @value.
+ *
+ * Returns: a new #DConfChangeset
+ **/
+DConfChangeset *
+dconf_changeset_new_write (const gchar *path,
+ GVariant *value)
+{
+ DConfChangeset *changeset;
+
+ changeset = dconf_changeset_new ();
+ dconf_changeset_set (changeset, path, value);
+
+ return changeset;
+}
+
+/**
+ * dconf_changeset_is_empty:
+ * @changeset: a #DConfChangeset
+ *
+ * Checks if @changeset is empty (ie: contains no changes).
+ *
+ * Returns: %TRUE if @changeset is empty
+ **/
+gboolean
+dconf_changeset_is_empty (DConfChangeset *changeset)
+{
+ return !g_hash_table_size (changeset->table);
+}
+
+/**
+ * dconf_changeset_change:
+ * @changeset: a #DConfChangeset (to be changed)
+ * @changes: the changes to make to @changeset
+ *
+ * Applies @changes to @changeset.
+ *
+ * If @changeset is a normal changeset then reset requests in @changes
+ * will be allied to @changeset and then copied down into it. In this
+ * case the two changesets are effectively being merged.
+ *
+ * If @changeset is in database mode then the reset operations in
+ * @changes will simply be applied to @changeset.
+ *
+ * Since: 0.16
+ **/
+void
+dconf_changeset_change (DConfChangeset *changeset,
+ DConfChangeset *changes)
+{
+ gsize prefix_len;
+ gint i;
+
+ g_return_if_fail (!changeset->is_sealed);
+
+ /* Handling resets is a little bit tricky...
+ *
+ * Consider the case that we have @changeset containing a key /a/b and
+ * @changes containing a reset request for /a/ and a set request for
+ * /a/c.
+ *
+ * It's clear that at the end of this all, we should have only /a/c
+ * but in order for that to be the case, we need to make sure that we
+ * process the reset of /a/ before we process the set of /a/c.
+ *
+ * The easiest way to do this is to visit the strings in sorted order.
+ * That removes the possibility of iterating over the hash table, but
+ * dconf_changeset_build_description() makes the list in the order we
+ * need so just call it and then iterate over the result.
+ */
+
+ if (!dconf_changeset_describe (changes, NULL, NULL, NULL))
+ return;
+
+ prefix_len = strlen (changes->prefix);
+ for (i = 0; changes->paths[i]; i++)
+ {
+ const gchar *path;
+ GVariant *value;
+
+ /* The changes->paths are just pointers into the keys of the
+ * hashtable, fast-forwarded past the prefix. Rewind a bit.
+ */
+ path = changes->paths[i] - prefix_len;
+ value = changes->values[i];
+
+ dconf_changeset_set (changeset, path, value);
+ }
+}
+
+/**
+ * dconf_changeset_diff:
+ * @from: a database mode changeset
+ * @to: a database mode changeset
+ *
+ * Compares to database-mode changesets and produces a changeset that
+ * describes their differences.
+ *
+ * If there is no difference, %NULL is returned.
+ *
+ * Applying the returned changeset to @from using
+ * dconf_changeset_change() will result in the two changesets being
+ * equal.
+ *
+ * Returns: (transfer full) (nullable): the changes, or %NULL
+ *
+ * Since: 0.16
+ */
+DConfChangeset *
+dconf_changeset_diff (DConfChangeset *from,
+ DConfChangeset *to)
+{
+ DConfChangeset *changeset = NULL;
+ GHashTableIter iter;
+ gpointer key, val;
+
+ g_return_val_if_fail (from->is_database, NULL);
+ g_return_val_if_fail (to->is_database, NULL);
+
+ /* We make no attempt to do dir resets, but we could...
+ *
+ * For now, we just reset each key individually.
+ *
+ * We create our list of changes in two steps:
+ *
+ * - iterate the 'to' changeset and note any keys that do not have
+ * the same value in the 'from' changeset
+ *
+ * - iterate the 'from' changeset and note any keys not present in
+ * the 'to' changeset, recording resets for them
+ *
+ * This will cover all changes.
+ *
+ * Note: because 'from' and 'to' are database changesets we don't have
+ * to worry about seeing NULL values or dirs.
+ */
+ g_hash_table_iter_init (&iter, to->table);
+ while (g_hash_table_iter_next (&iter, &key, &val))
+ {
+ GVariant *from_val = g_hash_table_lookup (from->table, key);
+
+ if (from_val == NULL || !g_variant_equal (val, from_val))
+ {
+ if (!changeset)
+ changeset = dconf_changeset_new ();
+
+ dconf_changeset_set (changeset, key, val);
+ }
+ }
+
+ g_hash_table_iter_init (&iter, from->table);
+ while (g_hash_table_iter_next (&iter, &key, &val))
+ if (!g_hash_table_lookup (to->table, key))
+ {
+ if (!changeset)
+ changeset = dconf_changeset_new ();
+
+ dconf_changeset_set (changeset, key, NULL);
+ }
+
+ return changeset;
+}
diff --git a/common/dconf-changeset.h b/common/dconf-changeset.h
new file mode 100644
index 0000000..b0ce450
--- /dev/null
+++ b/common/dconf-changeset.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __dconf_changeset_h__
+#define __dconf_changeset_h__
+
+#include <glib.h>
+
+typedef struct _DConfChangeset DConfChangeset;
+
+typedef gboolean (* DConfChangesetPredicate) (const gchar *path,
+ GVariant *value,
+ gpointer user_data);
+
+DConfChangeset * dconf_changeset_new (void);
+DConfChangeset * dconf_changeset_new_database (DConfChangeset *copy_of);
+
+DConfChangeset * dconf_changeset_new_write (const gchar *path,
+ GVariant *value);
+
+DConfChangeset * dconf_changeset_ref (DConfChangeset *changeset);
+void dconf_changeset_unref (DConfChangeset *changeset);
+
+gboolean dconf_changeset_is_empty (DConfChangeset *changeset);
+
+void dconf_changeset_set (DConfChangeset *changeset,
+ const gchar *path,
+ GVariant *value);
+
+gboolean dconf_changeset_get (DConfChangeset *changeset,
+ const gchar *key,
+ GVariant **value);
+
+gboolean dconf_changeset_is_similar_to (DConfChangeset *changeset,
+ DConfChangeset *other);
+
+gboolean dconf_changeset_all (DConfChangeset *changeset,
+ DConfChangesetPredicate predicate,
+ gpointer user_data);
+
+guint dconf_changeset_describe (DConfChangeset *changeset,
+ const gchar **prefix,
+ const gchar * const **paths,
+ GVariant * const **values);
+
+GVariant * dconf_changeset_serialise (DConfChangeset *changeset);
+DConfChangeset * dconf_changeset_deserialise (GVariant *serialised);
+
+void dconf_changeset_change (DConfChangeset *changeset,
+ DConfChangeset *changes);
+
+DConfChangeset * dconf_changeset_diff (DConfChangeset *from,
+ DConfChangeset *to);
+
+void dconf_changeset_seal (DConfChangeset *changeset);
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(DConfChangeset, dconf_changeset_unref)
+
+#endif /* __dconf_changeset_h__ */
diff --git a/common/dconf-enums.h b/common/dconf-enums.h
new file mode 100644
index 0000000..2f10d1a
--- /dev/null
+++ b/common/dconf-enums.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright © 2013 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __dconf_error_h__
+#define __dconf_error_h__
+
+#include <glib.h>
+
+#define DCONF_ERROR (dconf_error_quark ())
+GQuark dconf_error_quark (void);
+
+typedef enum
+{
+ DCONF_ERROR_FAILED,
+ DCONF_ERROR_PATH,
+ DCONF_ERROR_NOT_WRITABLE
+} DConfError;
+
+typedef enum
+{
+ DCONF_READ_FLAGS_NONE = 0,
+ DCONF_READ_DEFAULT_VALUE = (1u << 0),
+ DCONF_READ_USER_VALUE = (1u << 1)
+} DConfReadFlags;
+
+#endif /* __dconf_error_h__ */
diff --git a/common/dconf-error.c b/common/dconf-error.c
new file mode 100644
index 0000000..6339397
--- /dev/null
+++ b/common/dconf-error.c
@@ -0,0 +1,52 @@
+/*
+ * Copyright © 2013 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-enums.h"
+
+/**
+ * SECTION:error
+ * @title: DConfError
+ * @short_description: GError error codes
+ *
+ * These are the error codes that can be returned from dconf APIs.
+ **/
+
+/**
+ * DCONF_ERROR:
+ *
+ * The error domain of DConf.
+ *
+ * Since: 0.20
+ **/
+
+/**
+ * DConfError:
+ * @DCONF_ERROR_FAILED: generic error
+ * @DCONF_ERROR_PATH: the path given for the operation was a valid path
+ * or was not of the expected type (dir vs. key)
+ * @DCONF_ERROR_NOT_WRITABLE: the given key was not writable
+ *
+ * Possible errors from DConf functions.
+ *
+ * Since: 0.20
+ **/
+
+G_DEFINE_QUARK (dconf_error, dconf_error)
diff --git a/common/dconf-paths.c b/common/dconf-paths.c
new file mode 100644
index 0000000..047429d
--- /dev/null
+++ b/common/dconf-paths.c
@@ -0,0 +1,253 @@
+/*
+ * Copyright © 2008-2009 Ryan Lortie
+ * Copyright © 2010 Codethink Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-paths.h"
+
+#include "dconf-enums.h"
+
+/**
+ * SECTION:paths
+ * @title: dconf Paths
+ * @short_description: utility functions to validate dconf paths
+ *
+ * Various places in the dconf API speak of "paths", "keys", "dirs" and
+ * relative versions of each of these. This file contains functions to
+ * check if a given string is a valid member of each of these classes
+ * and to report errors when a string is not.
+ *
+ * See each function in this section for a precise description of what
+ * makes a string a valid member of a given class.
+ **/
+
+#define vars gchar c, l
+
+#define nonnull \
+ if (string == NULL) { \
+ g_set_error (error, DCONF_ERROR, DCONF_ERROR_PATH, \
+ "%s not specified", type); \
+ return FALSE; \
+ }
+
+
+#define absolute \
+ if ((l = *string++) != '/') \
+ { \
+ g_set_error (error, DCONF_ERROR, DCONF_ERROR_PATH, \
+ "dconf %s must begin with a slash", type); \
+ return FALSE; \
+ }
+
+#define relative \
+ if (*string == '/') \
+ { \
+ g_set_error (error, DCONF_ERROR, DCONF_ERROR_PATH, \
+ "dconf %s must not begin with a slash", type); \
+ return FALSE; \
+ } \
+ l = '/'
+
+#define no_double_slash \
+ while ((c = *string++)) \
+ { \
+ if (c == '/' && l == '/') \
+ { \
+ g_set_error (error, DCONF_ERROR, DCONF_ERROR_PATH, \
+ "dconf %s must not contain two " \
+ "consecutive slashes", type); \
+ return FALSE; \
+ } \
+ l = c; \
+ } \
+
+#define path \
+ return TRUE
+
+#define key \
+ if (l == '/') \
+ { \
+ g_set_error (error, DCONF_ERROR, DCONF_ERROR_PATH, \
+ "dconf %s must not end with a slash", type); \
+ return FALSE; \
+ } \
+ return TRUE
+
+#define dir \
+ if (l != '/') \
+ { \
+ g_set_error (error, DCONF_ERROR, DCONF_ERROR_PATH, \
+ "dconf %s must end with a slash", type); \
+ return FALSE; \
+ } \
+ return TRUE
+
+
+
+/**
+ * dconf_is_path:
+ * @string: a string
+ * @error: a pointer to a #GError, or %NULL, set when %FALSE is returned
+ *
+ * Checks if @string is a valid dconf path. dconf keys must start with
+ * '/' and not contain '//'.
+ *
+ * A dconf path may be either a key or a dir. See dconf_is_key() and
+ * dconf_is_dir() for examples of each.
+ *
+ * Returns: %TRUE if @string is a path
+ **/
+gboolean
+dconf_is_path (const gchar *string,
+ GError **error)
+{
+#define type "path"
+ vars; nonnull; absolute; no_double_slash; path;
+#undef type
+}
+
+/**
+ * dconf_is_key:
+ * @string: a string
+ * @error: a pointer to a #GError, or %NULL, set when %FALSE is returned
+ *
+ * Checks if @string is a valid dconf key. dconf keys must start with
+ * '/', not contain '//' and not end with '/'.
+ *
+ * A dconf key is the potential location of a single value within the
+ * database.
+ *
+ * "/a", "/a/b" and "/a/b/c" are examples of keys. "", "/", "a", "a/b",
+ * "//a/b", "/a//b", and "/a/" are examples of strings that are not
+ * keys.
+ *
+ * Returns: %TRUE if @string is a key
+ **/
+gboolean
+dconf_is_key (const gchar *string,
+ GError **error)
+{
+#define type "key"
+ vars; nonnull; absolute; no_double_slash; key;
+#undef type
+}
+
+/**
+ * dconf_is_dir:
+ * @string: a string
+ * @error: a pointer to a #GError, or %NULL, set when %FALSE is returned
+ *
+ * Checks if @string is a valid dconf dir. dconf dirs must start and
+ * end with '/' and not contain '//'.
+ *
+ * A dconf dir refers to a subtree of the database that can contain
+ * other dirs or keys. If @string is a dir, then it will be a prefix of
+ * any key or dir contained within it.
+ *
+ * "/", "/a/" and "/a/b/" are examples of dirs. "", "a/", "a/b/",
+ * "//a/b/", "/a//b/" and "/a" are examples of strings that are not
+ * dirs.
+ *
+ * Returns: %TRUE if @string is a dir
+ **/
+gboolean
+dconf_is_dir (const gchar *string,
+ GError **error)
+{
+#define type "dir"
+ vars; nonnull; absolute; no_double_slash; dir;
+#undef type
+}
+
+/**
+ * dconf_is_rel_path:
+ * @string: a string
+ * @error: a pointer to a #GError, or %NULL, set when %FALSE is returned
+ *
+ * Checks if @string is a valid dconf relative path. A relative path is
+ * a string that, when concatenated to a dir, forms a valid dconf path.
+ * This means that a rel must not start with a '/' or contain '//'.
+ *
+ * A dconf rel may be either a relative key or a relative dir. See
+ * dconf_is_rel_key() and dconf_is_rel_dir() for examples of each.
+ *
+ * Returns: %TRUE if @string is a relative path
+ **/
+gboolean
+dconf_is_rel_path (const gchar *string,
+ GError **error)
+{
+#define type "relative path"
+ vars; nonnull; relative; no_double_slash; path;
+#undef type
+}
+
+
+/**
+ * dconf_is_rel_key:
+ * @string: a string
+ * @error: a pointer to a #GError, or %NULL, set when %FALSE is returned
+ *
+ * Checks if @string is a valid dconf relative key. A relative key is a
+ * string that, when concatenated to a dir, forms a valid dconf key.
+ * This means that a relative key must not start or end with a '/' or
+ * contain '//'.
+ *
+ * "a", "a/b" and "a/b/c" are examples of relative keys. "", "/", "/a",
+ * "/a/b", "//a/b", "/a//b", and "a/" are examples of strings that are
+ * not relative keys.
+ *
+ * Returns: %TRUE if @string is a relative key
+ **/
+gboolean
+dconf_is_rel_key (const gchar *string,
+ GError **error)
+{
+#define type "relative key"
+ vars; nonnull; relative; no_double_slash; key;
+#undef type
+}
+
+/**
+ * dconf_is_rel_dir:
+ * @string: a string
+ * @error: a pointer to a #GError, or %NULL, set when %FALSE is returned
+ *
+ * Checks if @string is a valid dconf relative dir. A relative dir is a
+ * string that, when appended to a dir, forms a valid dconf dir. This
+ * means that a relative dir must not start with a '/' or contain '//'
+ * and must end with a '/' except in the case that it is the empty
+ * string (in which case the path specified by appending the rel to a
+ * directory is the original directory).
+ *
+ * "", "a/" and "a/b/" are examples of relative dirs. "/", "/a/",
+ * "/a/b/", "//a/b/", "a//b/" and "a" are examples of strings that are
+ * not relative dirs.
+ *
+ * Returns: %TRUE if @string is a relative dir
+ **/
+gboolean
+dconf_is_rel_dir (const gchar *string,
+ GError **error)
+{
+#define type "relative dir"
+ vars; nonnull; relative; no_double_slash; dir;
+#undef type
+}
diff --git a/common/dconf-paths.h b/common/dconf-paths.h
new file mode 100644
index 0000000..9449fa5
--- /dev/null
+++ b/common/dconf-paths.h
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2008-2009 Ryan Lortie
+ * Copyright © 2010 Codethink Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __dconf_paths_h__
+#define __dconf_paths_h__
+
+#include <glib.h>
+
+gboolean dconf_is_path (const gchar *string,
+ GError **error);
+gboolean dconf_is_key (const gchar *string,
+ GError **error);
+gboolean dconf_is_dir (const gchar *string,
+ GError **error);
+
+gboolean dconf_is_rel_path (const gchar *string,
+ GError **error);
+gboolean dconf_is_rel_key (const gchar *string,
+ GError **error);
+gboolean dconf_is_rel_dir (const gchar *string,
+ GError **error);
+
+#endif /* __dconf_paths_h__ */
diff --git a/common/meson.build b/common/meson.build
new file mode 100644
index 0000000..58e0fa8
--- /dev/null
+++ b/common/meson.build
@@ -0,0 +1,46 @@
+common_inc = include_directories('.')
+
+headers = files(
+ 'dconf-changeset.h',
+ 'dconf-enums.h',
+ 'dconf-paths.h',
+)
+
+install_headers(
+ headers,
+ subdir: join_paths('dconf', 'common'),
+)
+
+sources = files(
+ 'dconf-changeset.c',
+ 'dconf-error.c',
+ 'dconf-paths.c',
+)
+
+libdconf_common = static_library(
+ 'dconf-common',
+ sources: sources,
+ include_directories: top_inc,
+ dependencies: glib_dep,
+ c_args: dconf_c_args,
+ pic: true,
+)
+
+libdconf_common_dep = declare_dependency(
+ dependencies: glib_dep,
+ link_whole: libdconf_common,
+)
+
+libdconf_common_hidden = static_library(
+ 'dconf-common-hidden',
+ sources: sources,
+ include_directories: top_inc,
+ dependencies: glib_dep,
+ c_args: dconf_c_args + cc.get_supported_arguments('-fvisibility=hidden'),
+ pic: true,
+)
+
+libdconf_common_hidden_dep = declare_dependency(
+ dependencies: glib_dep,
+ link_with: libdconf_common_hidden,
+)
diff --git a/dconf.doap b/dconf.doap
new file mode 100644
index 0000000..6b6a1d8
--- /dev/null
+++ b/dconf.doap
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<Project xmlns="http://usefulinc.com/ns/doap#"
+ xmlns:foaf="http://xmlns.com/foaf/0.1/"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:gnome="http://api.gnome.org/doap-extensions#">
+
+ <name xml:lang="en">dconf</name>
+ <shortdesc xml:lang="en">Configuration database system</shortdesc>
+ <description xml:lang="en">dconf is a simple key/value storage system ideal for storing user preferences</description>
+ <homepage rdf:resource="https://wiki.gnome.org/Projects/dconf"/>
+ <download-page rdf:resource="http://download.gnome.org/sources/dconf/"/>
+ <bug-database rdf:resource="https://gitlab.gnome.org/GNOME/dconf/issues/"/>
+ <category rdf:resource="http://api.gnome.org/doap-extensions#core" />
+ <programming-language>C</programming-language>
+
+ <maintainer>
+ <foaf:Person>
+ <foaf:name>Ryan Lortie</foaf:name>
+ <foaf:mbox rdf:resource="mailto:desrt@desrt.ca"/>
+ <gnome:userid>desrt</gnome:userid>
+ </foaf:Person>
+ </maintainer>
+
+ <maintainer>
+ <foaf:Person>
+ <foaf:name>Daniel Playfair Cal</foaf:name>
+ <foaf:mbox rdf:resource="mailto:daniel.playfair.cal@gmail.com"/>
+ <gnome:userid>danielplayfaircal</gnome:userid>
+ </foaf:Person>
+ </maintainer>
+
+ <maintainer>
+ <foaf:Person>
+ <foaf:name>Marek Kašík</foaf:name>
+ <foaf:mbox rdf:resource="mailto:mkasik@redhat.com"/>
+ <gnome:userid>mkasik</gnome:userid>
+ </foaf:Person>
+ </maintainer>
+
+ <maintainer>
+ <foaf:Person>
+ <foaf:name>Philip Withnall</foaf:name>
+ <foaf:mbox rdf:resource="mailto:philip@tecnocode.co.uk"/>
+ <foaf:mbox rdf:resource="mailto:withnall@endlessm.com"/>
+ <gnome:userid>pwithnall</gnome:userid>
+ </foaf:Person>
+ </maintainer>
+
+</Project>
diff --git a/docs/dconf-docs.xml b/docs/dconf-docs.xml
new file mode 100644
index 0000000..324cdac
--- /dev/null
+++ b/docs/dconf-docs.xml
@@ -0,0 +1,61 @@
+<?xml version='1.0'?>
+
+<book id='index' xmlns:xi='http://www.w3.org/2001/XInclude'>
+ <bookinfo>
+ <title>dconf Reference Manual</title>
+ <releaseinfo>
+ The latest version of this documentation can be found on-line at
+ <ulink role='online-location' url='http://library.gnome.org/devel/dconf/unstable/'>http://library.gnome.org/devel/dconf/unstable/</ulink>.
+ </releaseinfo>
+ </bookinfo>
+
+ <xi:include href="dconf-overview.xml"/>
+
+ <chapter>
+ <title>DConf Client API</title>
+ <xi:include href='xml/error.xml'/>
+ <xi:include href='xml/paths.xml'/>
+ <xi:include href='xml/changeset.xml'/>
+ <xi:include href='xml/client.xml'/>
+ </chapter>
+
+ <chapter id='object-tree'>
+ <title>Object Hierarchy</title>
+ <xi:include href='xml/tree_index.sgml'/>
+ <xi:include href='xml/object_index.sgml'/>
+ </chapter>
+
+ <chapter id='programs'>
+ <title>Programs</title>
+ <xi:include href='dconf-service.xml'/>
+ <xi:include href='dconf-tool.xml'/>
+ </chapter>
+
+ <index id='api-index-full'>
+ <title>API Index</title>
+ <xi:include href='xml/api-index-full.xml'><xi:fallback /></xi:include>
+ <xi:include href='xml/api-index-deprecated.xml'><xi:fallback /></xi:include>
+ </index>
+
+ <index id='api-index-0.16' role='0.16'>
+ <title>Index of new symbols in 0.16</title>
+ <xi:include href='xml/api-index-0.16.xml'/>
+ </index>
+
+ <index id='api-index-0.18' role='0.18'>
+ <title>Index of new symbols in 0.18</title>
+ <xi:include href='xml/api-index-0.18.xml'/>
+ </index>
+
+ <index id='api-index-0.20' role='0.20'>
+ <title>Index of new symbols in 0.20</title>
+ <xi:include href='xml/api-index-0.20.xml'/>
+ </index>
+
+ <index id='api-index-0.26' role='0.26'>
+ <title>Index of new symbols in 0.26</title>
+ <xi:include href='xml/api-index-0.26.xml'/>
+ </index>
+
+ <xi:include href='xml/annotation-glossary.xml'/>
+</book>
diff --git a/docs/dconf-overview.xml b/docs/dconf-overview.xml
new file mode 100644
index 0000000..b245137
--- /dev/null
+++ b/docs/dconf-overview.xml
@@ -0,0 +1,214 @@
+<?xml version='1.0'?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+ "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+
+<refentry id="dconf-overview">
+
+ <refentryinfo>
+ <title>dconf Overview</title>
+ <productname>dconf</productname>
+
+ <authorgroup>
+ <author>
+ <contrib>Developer</contrib>
+ <firstname>Ryan</firstname>
+ <surname>Lortie</surname>
+ <email>desrt@desrt.ca</email>
+ </author>
+ </authorgroup>
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>dconf</refentrytitle>
+ <manvolnum>7</manvolnum>
+ <refmiscinfo class="manual">Conventions and miscellaneous</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>dconf</refname>
+ <refpurpose>A configuration system</refpurpose>
+ </refnamediv>
+
+ <refsect1>
+ <title>Description</title>
+
+ <para>
+ dconf is a simple key/value storage system that is heavily optimised for reading. This makes it an ideal
+ system for storing user preferences (which are read 1000s of times for each time the user changes one).
+ It was created with this usecase in mind.
+ </para>
+ <para>
+ All preferences are stored in a single large binary file. Layering of preferences is possible using
+ multiple files (ie: for site defaults). Lock-down is also supported. The binary file for the defaults
+ can optionally be compiled from a set of plain text keyfiles.
+ </para>
+ <para>
+ dconf has a partial client/server architecture. It uses D-Bus. The server is only involved in writes
+ (and is not activated in the user session until the user modifies a preference). The service is stateless
+ and can exit freely at any time (and is therefore robust against crashes). The list of paths that each
+ process is watching is stored within the D-Bus daemon itself (as D-Bus signal match rules).
+ </para>
+ <para>
+ Reads are performed by direct access (via mmap) to the on-disk database which is essentially a hashtable.
+ For this reason, dconf reads typically involve zero system calls and are comparable to a hashtable lookup
+ in terms of speed. Practically speaking, in simple non-layered setups, dconf is less than 10 times slower
+ than GHashTable.
+ </para>
+ <para>
+ Writes are assumed only to happen in response to explicit user interaction (like clicking on a checkbox in
+ a preferences dialog) and are therefore not optimised at all. On some file systems, dconf-service will
+ call fsync() for every write, which can introduce a latency of up to 100ms. This latency is hidden by the
+ client libraries through a clever "fast" mechanism that records the outstanding changes locally (so they
+ 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.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>Profiles</title>
+
+ <para>
+ A profile is a list of configuration databases that dconf consults to find the value for a key. The user's personal
+ database always takes the highest priority, followed by the system databases in the order prescribed by the profile.
+ </para>
+
+ <para>
+ On startup, dconf consults the <envar>DCONF_PROFILE</envar> environment variable. If set, dconf will attempt to open
+ the named profile, aborting if that fails. If the environment variable is not set, it will attempt to open the profile
+ named "user" and if that fails, it will fall back to an internal hard-wired configuration. dconf stores its profiles
+ in text files. <envar>DCONF_PROFILE</envar> can specify a relative path to a file in <filename>/etc/dconf/profile/</filename>,
+ or an absolute path (such as in a user's home directory). The profile name can only use alphanumeric characters or '_'.
+ </para>
+
+ <para>
+ A profile file might look like the following:
+ <screen>
+user-db:user
+system-db:local
+system-db:site
+ </screen>
+ </para>
+
+ <para>
+ Each line in a profile specifies one dconf database. The first line indicates the database used to write changes, and the
+ remaining lines indicate read-only databases. (The first line should specify a user-db or service-db, so that users can actually
+ make configuration changes.)
+ </para>
+
+ <para>
+ A "user-db" line specifies a user database. These databases are found in <filename><envar>$XDG_CONFIG_HOME</envar>/dconf/</filename>.
+ The name of the file to open in that directory is exactly as it is written in the profile. This file is expected to be in the binary
+ dconf database format. Note that <envar>XDG_CONFIG_HOME</envar> cannot be set/modified per terminal or session, because then the writer
+ and reader would be working on different DBs (the writer is started by DBus and cannot see that variable).
+ </para>
+
+ <para>
+ A "service-db" line instructs dconf to place the binary database file for the user database in <envar>XDG_RUNTIME_DIR</envar>.
+ Since this location is not persistent, the rest of the line instructs dconf how to store the database persistently. A typical
+ line is <literal>service-db:keyfile/user</literal>, which tells dconf to synchronize the binary database with a plain text
+ keyfile in <filename><envar>$XDG_CONFIG_HOME</envar>/dconf/user.txt</filename>. The synchronization is bi-directional.
+ </para>
+
+ <para>
+ A "system-db" line specifies a system database. These databases are found in <filename>/etc/dconf/db/</filename>. Again, the name of
+ the file to open in that directory is exactly as it is written in the profile and the file is expected to be in the dconf database
+ format.
+ </para>
+
+ <para>
+ If the <envar>DCONF_PROFILE</envar> environment variable is unset and the "user" profile can not be opened, then the effect is as if
+ the profile was specified by this file:
+ <screen>
+user-db:user
+ </screen>
+ That is, the user's personal database is consulted and there are no system settings.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>Key Files</title>
+
+ <para>
+ To facilitate system configuration with a text editor, dconf can populate databases from plain text keyfiles. For any given system
+ database, keyfiles can be placed into the <filename>/etc/dconf/db/<replaceable>database</replaceable>.d/</filename> directory. The
+ keyfiles contain groups of settings as follows:
+ </para>
+ <screen>
+# Some useful default settings for our site
+
+[system/proxy/http]
+host='172.16.0.1'
+enabled=true
+
+[org/gnome/desktop/background]
+picture-uri='file:///usr/local/rupert-corp/company-wallpaper.jpeg'
+ </screen>
+ <para>
+ After changing keyfiles, the database needs to be updated with the
+ <citerefentry><refentrytitle>dconf</refentrytitle><manvolnum>1</manvolnum></citerefentry> tool.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>Locks</title>
+
+ <para>
+ System databases can contain 'locks' for keys. If a lock for a particular key or subpath is installed into a database
+ then no database listed above that one in the profile will be able to modify any of the affected settings. This can be
+ used to enforce mandatory settings.
+ </para>
+
+ <para>
+ To add locks to a database, place text files in the <filename>/etc/dconf/db/<replaceable>database</replaceable>.d/locks</filename>
+ directory, where <replaceable>database</replaceable> is the name of a system database, as specified in the profile. The files
+ contain list of keys to lock, on per line. Lines starting with a # are ignored. Here is an example:
+ </para>
+ <screen>
+# prevent changes to the company wallpaper
+/org/gnome/desktop/background/picture-uri
+ </screen>
+ <para>
+ After changing locks, the database needs to be updated with the
+ <citerefentry><refentrytitle>dconf</refentrytitle><manvolnum>1</manvolnum></citerefentry> tool.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>Portability</title>
+
+ <para>
+ dconf mostly targets Free Software operating systems. It will theoretically run on Mac OS but there isn't
+ much point to that (since Mac OS applications want to store preferences in plist files). It is not
+ possible to use dconf on Windows because of the inability to rename over a file that's still in use (which
+ is what the dconf-service does on every write).
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>API stability</title>
+
+ <para>
+ The dconf API is not particularly friendly, and is not guaranteed to be stable. Because of this and the
+ lack of portability, you almost certainly want to use some sort of wrapper API around it. The wrapper API
+ used by GTK+ and GNOME applications is
+ <ulink url="http://developer.gnome.org/gio/stable/GSettings.html">GSettings</ulink>, which is included as
+ part of GLib. GSettings has backends for Windows (using the registry) and Mac OS (using property lists) as
+ well as its dconf backend and is the proper API to use for graphical applications.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>See Also</title>
+ <para>
+ <citerefentry><refentrytitle>dconf-service</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+ <citerefentry><refentrytitle>dconf-editor</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+ <citerefentry><refentrytitle>dconf</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+ <ulink url="http://developer.gnome.org/gio/stable/GSettings.html">GSettings</ulink>
+ </para>
+ </refsect1>
+</refentry>
diff --git a/docs/dconf-sections.txt b/docs/dconf-sections.txt
new file mode 100644
index 0000000..878c6b9
--- /dev/null
+++ b/docs/dconf-sections.txt
@@ -0,0 +1,66 @@
+<SECTION>
+<FILE>error</FILE>
+DConfError
+DCONF_ERROR
+<SUBSECTION Standard>
+dconf_error_quark
+</SECTION>
+
+<SECTION>
+<FILE>client</FILE>
+DConfClient
+dconf_client_new
+dconf_client_read
+DConfReadFlags
+dconf_client_read_full
+dconf_client_list
+dconf_client_list_locks
+dconf_client_is_writable
+dconf_client_write_fast
+dconf_client_write_sync
+dconf_client_change_fast
+dconf_client_change_sync
+dconf_client_watch_fast
+dconf_client_watch_sync
+dconf_client_unwatch_fast
+dconf_client_unwatch_sync
+dconf_client_sync
+<SUBSECTION Standard>
+DConfClientClass
+DCONF_CLIENT
+DCONF_IS_CLIENT
+DCONF_TYPE_CLIENT
+dconf_client_get_type
+</SECTION>
+
+<SECTION>
+<FILE>paths</FILE>
+dconf_is_dir
+dconf_is_key
+dconf_is_path
+dconf_is_rel_path
+dconf_is_rel_dir
+dconf_is_rel_key
+</SECTION>
+
+<SECTION>
+<FILE>changeset</FILE>
+DConfChangeset
+DConfChangesetPredicate
+dconf_changeset_all
+dconf_changeset_change
+dconf_changeset_describe
+dconf_changeset_deserialise
+dconf_changeset_diff
+dconf_changeset_get
+dconf_changeset_is_empty
+dconf_changeset_is_similar_to
+dconf_changeset_new
+dconf_changeset_new_database
+dconf_changeset_new_write
+dconf_changeset_ref
+dconf_changeset_serialise
+dconf_changeset_set
+dconf_changeset_unref
+dconf_changeset_seal
+</SECTION>
diff --git a/docs/dconf-service.xml b/docs/dconf-service.xml
new file mode 100644
index 0000000..507c14d
--- /dev/null
+++ b/docs/dconf-service.xml
@@ -0,0 +1,63 @@
+<?xml version='1.0'?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+ "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+
+<refentry id="dconf-service">
+ <refentryinfo>
+ <title>dconf-service</title>
+ <productname>dconf</productname>
+
+ <authorgroup>
+ <author>
+ <contrib>Developer</contrib>
+ <firstname>Ryan</firstname>
+ <surname>Lortie</surname>
+ <email>desrt@desrt.ca</email>
+ </author>
+ </authorgroup>
+
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>dconf-service</refentrytitle>
+ <manvolnum>1</manvolnum>
+ <refmiscinfo class="manual">User Commands</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>dconf-service</refname>
+ <refpurpose>D-Bus service for writes to the dconf database</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <cmdsynopsis>
+ <command>dconf-service</command>
+ </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+ <title>Description</title>
+
+ <para>
+ The <command>dconf-service</command> program provides the <emphasis>ca.desrt.dconf</emphasis> name on the
+ session or system bus. Users or administrators should never need to start the service, as it will be
+ automatically started by
+ <citerefentry><refentrytitle>dbus-daemon</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+ whenever an application tries to write settings.
+ </para>
+
+ <para>
+ Reading values from the dconf database does not involve the service; it is only needed for writes. The
+ service is stateless and can exit freely at any time (and is therefore robust against crashes).
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>See Also</title>
+ <para>
+ <citerefentry><refentrytitle>dconf</refentrytitle><manvolnum>7</manvolnum></citerefentry>,
+ <citerefentry><refentrytitle>dbus-daemon</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+ <citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+ </para>
+ </refsect1>
+</refentry>
diff --git a/docs/dconf-tool.xml b/docs/dconf-tool.xml
new file mode 100644
index 0000000..e5f8c24
--- /dev/null
+++ b/docs/dconf-tool.xml
@@ -0,0 +1,224 @@
+<?xml version='1.0'?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+ "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd">
+
+<refentry id="dconf-tool">
+ <refentryinfo>
+ <title>dconf</title>
+ <productname>dconf</productname>
+
+ <authorgroup>
+ <author>
+ <contrib>Developer</contrib>
+ <firstname>Ryan</firstname>
+ <surname>Lortie</surname>
+ <email>desrt@desrt.ca</email>
+ </author>
+ </authorgroup>
+
+ </refentryinfo>
+
+ <refmeta>
+ <refentrytitle>dconf</refentrytitle>
+ <manvolnum>1</manvolnum>
+ <refmiscinfo class="manual">User Commands</refmiscinfo>
+ </refmeta>
+
+ <refnamediv>
+ <refname>dconf</refname>
+ <refpurpose>Simple tool for manipulating a dconf database</refpurpose>
+ </refnamediv>
+
+ <refsynopsisdiv>
+ <cmdsynopsis>
+ <command>dconf</command>
+ <arg choice="plain">read</arg>
+ <arg choice="opt">-d</arg>
+ <arg choice="plain"><replaceable>KEY</replaceable></arg>
+ </cmdsynopsis>
+ <cmdsynopsis>
+ <command>dconf</command>
+ <arg choice="plain">list</arg>
+ <arg choice="plain"><replaceable>DIR</replaceable></arg>
+ </cmdsynopsis>
+ <cmdsynopsis>
+ <command>dconf</command>
+ <arg choice="plain">write</arg>
+ <arg choice="plain"><replaceable>KEY</replaceable></arg>
+ <arg choice="plain"><replaceable>VALUE</replaceable></arg>
+ </cmdsynopsis>
+ <cmdsynopsis>
+ <command>dconf</command>
+ <arg choice="plain">reset</arg>
+ <arg choice="opt">-f</arg>
+ <arg choice="plain"><replaceable>PATH</replaceable></arg>
+ </cmdsynopsis>
+ <cmdsynopsis>
+ <command>dconf</command>
+ <arg choice="plain">compile</arg>
+ <arg choice="plain"><replaceable>OUTPUT</replaceable></arg>
+ <arg choice="plain"><replaceable>KEYFILEDIR</replaceable></arg>
+ </cmdsynopsis>
+ <cmdsynopsis>
+ <command>dconf</command>
+ <arg choice="plain">update</arg>
+ <arg choice="opt"><replaceable>DBDIR</replaceable></arg>
+ </cmdsynopsis>
+ <cmdsynopsis>
+ <command>dconf</command>
+ <arg choice="plain">watch</arg>
+ <arg choice="plain"><replaceable>PATH</replaceable></arg>
+ </cmdsynopsis>
+ <cmdsynopsis>
+ <command>dconf</command>
+ <arg choice="plain">dump</arg>
+ <arg choice="plain"><replaceable>DIR</replaceable></arg>
+ </cmdsynopsis>
+ <cmdsynopsis>
+ <command>dconf</command>
+ <arg choice="plain">load</arg>
+ <arg choice="opt">-f</arg>
+ <arg choice="plain"><replaceable>DIR</replaceable></arg>
+ </cmdsynopsis>
+ <cmdsynopsis>
+ <command>dconf</command>
+ <arg choice="plain">help</arg>
+ <arg choice="opt"><replaceable>COMMAND</replaceable></arg>
+ </cmdsynopsis>
+ </refsynopsisdiv>
+
+ <refsect1>
+ <title>Description</title>
+
+ <para>
+ The <command>dconf</command> program can perform various operations on a dconf database, such as reading
+ or writing individual values or entire directories. This tool operates on dconf directly, without using
+ gsettings schema information. Therefore, it cannot perform type and consistency checks on values. The
+ <citerefentry><refentrytitle>gsettings</refentrytitle><manvolnum>1</manvolnum></citerefentry> utility is
+ an alternative if such checks are needed.
+ </para>
+
+ <para>
+ The <replaceable>DIR</replaceable> arguments must be directory paths (starting and ending with '/'), the
+ <replaceable>KEY</replaceable> arguments must be key paths (starting, but not ending with '/') and the
+ <replaceable>PATH</replaceable> arguments can be either directory or key paths.
+ </para>
+
+ <para>
+ The <replaceable>OUTPUT</replaceable> argument must the location to write a (binary) dconf database to and the
+ <replaceable>KEYFILEDIR</replaceable> argument must be a .d directory containing keyfiles.
+ </para>
+
+ <para>
+ <replaceable>VALUE</replaceable> arguments must be in GVariant format, so e.g. a string must include
+ explicit quotes: "'foo'". This format is also used when printing out values.
+ </para>
+
+ <para>
+ Note that dconf needs a D-Bus session bus connection to write changes to the dconf database.
+ </para>
+ </refsect1>
+
+ <refsect1>
+ <title>Commands</title>
+
+ <variablelist>
+
+ <varlistentry>
+ <term><option>read</option></term>
+
+ <listitem><para>Read the value of a key.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>list</option></term>
+
+ <listitem><para>List the sub-keys and sub-directories of a directory.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>write</option></term>
+
+ <listitem><para>Write a new value to a key.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>reset</option></term>
+
+ <listitem><para>Reset a key or an entire directory. For directories, <option>-f</option> must be specified.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>compile</option></term>
+
+ <listitem>
+ <para>Compile a binary database from keyfiles.</para>
+ <para>
+ The result is always in little-endian byte order, so it can be safely installed in 'share'. If it
+ is used on a big endian machine, dconf will automatically byteswap the contents on read.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>update</option></term>
+
+ <listitem><para>Update the system dconf databases.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>watch</option></term>
+
+ <listitem><para>Watch a key or directory for changes.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>dump</option></term>
+
+ <listitem><para>Dump an entire subpath to stdout. The output is in a keyfile-like format, with values in GVariant syntax.</para></listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>load</option></term>
+
+ <listitem>
+ <para>
+ Populate a subpath from stdin. The expected format is the same as produced by <option>dump</option>.
+ Attempting to change non-writable keys cancels the load command.
+ To ignore changes to non-writable keys instead, use <option>-f</option>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry>
+ <term><option>help</option></term>
+
+ <listitem><para>Display help and exit. If <replaceable>COMMAND</replaceable> is given, display help for this command.</para></listitem>
+ </varlistentry>
+
+ </variablelist>
+ </refsect1>
+
+ <refsect1>
+ <title>Environment</title>
+
+ <variablelist>
+ <varlistentry>
+ <term><envar>DCONF_PROFILE</envar></term>
+ <listitem><para>
+ This environment variable determines which dconf <firstterm>profile</firstterm> to use. See
+ <citerefentry><refentrytitle>dconf</refentrytitle><manvolnum>7</manvolnum></citerefentry>.
+ </para></listitem>
+ </varlistentry>
+ </variablelist>
+ </refsect1>
+
+ <refsect1>
+ <title>See Also</title>
+ <para>
+ <citerefentry><refentrytitle>dconf</refentrytitle><manvolnum>7</manvolnum></citerefentry>,
+ <citerefentry><refentrytitle>gsettings</refentrytitle><manvolnum>1</manvolnum></citerefentry>,
+ <citerefentry><refentrytitle>dconf-editor</refentrytitle><manvolnum>1</manvolnum></citerefentry>
+ </para>
+ </refsect1>
+</refentry>
diff --git a/docs/dconf.types b/docs/dconf.types
new file mode 100644
index 0000000..36554ba
--- /dev/null
+++ b/docs/dconf.types
@@ -0,0 +1 @@
+dconf_client_get_type
diff --git a/docs/meson.build b/docs/meson.build
new file mode 100644
index 0000000..d510464
--- /dev/null
+++ b/docs/meson.build
@@ -0,0 +1,53 @@
+if get_option('gtk_doc')
+ gnome.gtkdoc(
+ 'dconf',
+ main_xml: 'dconf-docs.xml',
+ src_dir: [
+ common_inc,
+ client_inc
+ ],
+ dependencies: libdconf_dep,
+ scan_args: '--rebuild-types',
+ gobject_typesfile: 'dconf.types',
+ install: true,
+ install_dir: join_paths(dconf_prefix, gnome.gtkdoc_html_dir('dconf')),
+ )
+endif
+
+if get_option('man')
+ xsltproc = find_program('xsltproc', required: false)
+ assert(xsltproc.found(), 'xsltproc is required for man generation')
+
+ xsltproc_cmd = [
+ xsltproc,
+ '--output', '@OUTPUT@',
+ '--nonet',
+ '--stringparam', 'man.output.quietly', '1',
+ '--stringparam', 'funcsynopsis.style', 'ansi',
+ '--stringparam', 'man.th.extra1.suppress', '1',
+ '--stringparam', 'man.authors.section.enabled', '0',
+ '--stringparam', 'man.copyright.section.enabled', '0',
+ 'http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl',
+ '@INPUT@',
+ ]
+
+ mans = [
+ ['dconf-service.xml', 'dconf-service', '1'],
+ ['dconf-tool.xml', 'dconf', '1'],
+ ['dconf-overview.xml', 'dconf', '7'],
+ ]
+
+ foreach man: mans
+ output = '@0@.@1@'.format(man[1], man[2])
+ man_dir = 'man' + man[2]
+
+ custom_target(
+ output,
+ input: man[0],
+ output: output,
+ command: xsltproc_cmd,
+ install: true,
+ install_dir: join_paths(dconf_mandir, man_dir),
+ )
+ endforeach
+endif
diff --git a/engine/dconf-engine-mockable.c b/engine/dconf-engine-mockable.c
new file mode 100644
index 0000000..dce2f43
--- /dev/null
+++ b/engine/dconf-engine-mockable.c
@@ -0,0 +1,39 @@
+/*
+ * Copyright © 2019 Daniel Playfair Cal
+ *
+ * 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/>.
+ *
+ * Author: Daniel Playfair Cal <daniel.playfair.cal@gmail.com>
+ */
+
+/**
+ * This module contains the production implementations of methods used in
+ * dconf_shm that need to be mocked out for tests.
+ *
+ * In some cases, external methods are wrapped with a different name. This is
+ * done so that it is not necessary to redefine the external functions in
+ * unit tests in order to mock them out, and therefore easy to also call the
+ * non mocked versions in tests if necessary.
+ */
+
+#include "config.h"
+
+#include "dconf-engine-mockable.h"
+
+
+FILE *
+dconf_engine_fopen (const char *pathname, const char *mode)
+{
+ return fopen (pathname, mode);
+}
diff --git a/engine/dconf-engine-mockable.h b/engine/dconf-engine-mockable.h
new file mode 100644
index 0000000..091f6d3
--- /dev/null
+++ b/engine/dconf-engine-mockable.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright © 2019 Daniel Playfair Cal
+ *
+ * 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/>.
+ *
+ * Author: Daniel Playfair Cal <daniel.playfair.cal@gmail.com>
+ */
+
+#ifndef __dconf_engine_mockable_h__
+#define __dconf_engine_mockable_h__
+
+#include <gio/gio.h>
+#include <stdio.h>
+
+G_GNUC_INTERNAL
+FILE *dconf_engine_fopen (const char *pathname,
+ const char *mode);
+
+#endif
diff --git a/engine/dconf-engine-profile.c b/engine/dconf-engine-profile.c
new file mode 100644
index 0000000..6474248
--- /dev/null
+++ b/engine/dconf-engine-profile.c
@@ -0,0 +1,330 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-engine-profile.h"
+#include "dconf-engine-mockable.h"
+
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+
+#include "dconf-engine-source.h"
+
+#define MANDATORY_DIR "/run/dconf/user/" /* + getuid () */
+#define RUNTIME_PROFILE /* XDG_RUNTIME_DIR + */ "/dconf/profile"
+
+/* This comment attempts to document the exact semantics of
+ * profile-loading.
+ *
+ * In no situation should the result of profile loading be an abort.
+ * There must be a defined outcome for all possible situations.
+ * Warnings may be issued to stderr, however.
+ *
+ * The first step is to determine what profile is to be used. If a
+ * profile is explicitly specified by the API then it has the top
+ * priority. Otherwise, if the DCONF_PROFILE environment variable is
+ * set, it takes next priority.
+ *
+ * In both of those cases, if the named profile starts with a slash
+ * character then it is taken to be an absolute pathname. If it does
+ * not start with a slash then it is assumed to specify a profile file
+ * relative to /etc/dconf/profile/ or
+ * <ulink url='http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html'>XDG_DATA_DIRS</ulink>/dconf/profile/,
+ * taking the file in /etc in preference (ie: DCONF_PROFILE=test looks
+ * first for profile file /etc/dconf/profile/test, falling back to
+ * /usr/local/share/dconf/profile/test, then to
+ * /usr/share/dconf/profile/test).
+ *
+ * If opening the profile file fails then the null profile is used.
+ * This is a profile that contains zero sources. All keys will be
+ * unwritable and all reads will return NULL.
+ *
+ * In the case that no explicit profile was given and DCONF_PROFILE is
+ * unset, dconf attempts to open and use a profile called "user" (ie:
+ * /etc/dconf/profile/user or XDG_DATA_DIRS/dconf/profile/user). If
+ * that fails then the fallback is to act as if the profile file existed
+ * and contained a single line: "user-db:user".
+ *
+ * Note that the fallback case for a missing profile file is different
+ * in the case where a profile was explicitly specified (either by the
+ * API or the environment) and the case where one was not.
+ *
+ * Once a profile file is opened, each line is treated as a possible
+ * source. Comments and empty lines are ignored.
+ *
+ * All valid source specification lines need to start with 'user-db:',
+ * 'system-db:', 'service-db:' or 'file-db:'. If a line doesn't start
+ * with one of these then it gets ignored. If all the lines in the file
+ * get ignored then the result is effectively the null profile.
+ *
+ * If the first source is a "user-db:" or "service-db:" then the
+ * resulting profile will be writable. No profile starting with a
+ * "system-db:" or "file-db:" source can ever be writable.
+ *
+ * Note: even if the source fails to initialise (due to a missing file,
+ * for example) it will remain in the source list. This could have a
+ * performance cost: in the case of a system-db, for example, dconf will
+ * check if the file has come into existence on every read.
+ */
+
+static DConfEngineSource **
+dconf_engine_null_profile (gint *n_sources)
+{
+ *n_sources = 0;
+
+ return NULL;
+}
+
+static DConfEngineSource **
+dconf_engine_default_profile (gint *n_sources)
+{
+ DConfEngineSource **sources;
+
+ sources = g_new (DConfEngineSource *, 1);
+ sources[0] = dconf_engine_source_new_default ();
+ *n_sources = 1;
+
+ return sources;
+}
+
+static DConfEngineSource *
+dconf_engine_profile_handle_line (gchar *line)
+{
+ DConfEngineSource *source;
+ gchar *end;
+
+ /* remove whitespace at the front */
+ while (g_ascii_isspace (*line))
+ line++;
+
+ /* find the end of the line (or start of comments) */
+ end = line + strcspn (line, "#\n");
+
+ /* remove whitespace at the end */
+ while (end > line && g_ascii_isspace (end[-1]))
+ end--;
+
+ /* if we're left with nothing, return NULL */
+ if (line == end)
+ return NULL;
+
+ *end = '\0';
+
+ source = dconf_engine_source_new (line);
+
+ if (source == NULL)
+ g_warning ("unknown dconf database description: %s", line);
+
+ return source;
+}
+
+static DConfEngineSource **
+dconf_engine_read_profile_file (FILE *file,
+ gint *n_sources)
+{
+ DConfEngineSource **sources;
+ gchar line[80];
+ gint n = 0, a;
+
+ sources = g_new (DConfEngineSource *, (a = 4));
+
+ while (fgets (line, sizeof line, file))
+ {
+ DConfEngineSource *source;
+
+ /* The input file has long lines. */
+ if G_UNLIKELY (!strchr (line, '\n'))
+ {
+ GString *long_line;
+
+ long_line = g_string_new (line);
+ while (fgets (line, sizeof line, file))
+ {
+ g_string_append (long_line, line);
+ if (strchr (line, '\n'))
+ break;
+ }
+
+ source = dconf_engine_profile_handle_line (long_line->str);
+ g_string_free (long_line, TRUE);
+ }
+
+ else
+ source = dconf_engine_profile_handle_line (line);
+
+ if (source != NULL)
+ {
+ if (n == a)
+ sources = g_renew (DConfEngineSource *, sources, a *= 2);
+
+ sources[n++] = source;
+ }
+ }
+
+ *n_sources = n;
+
+ return g_realloc_n (sources, n, sizeof (DConfEngineSource *));
+}
+
+/* Find a profile file with the name given in 'profile' and open it. */
+static FILE *
+dconf_engine_open_profile_file (const gchar *profile)
+{
+ const gchar * const *xdg_data_dirs;
+ const gchar *prefix = SYSCONFDIR;
+ FILE *fp;
+
+ xdg_data_dirs = g_get_system_data_dirs ();
+
+ /* First time through, we check SYSCONFDIR, then we check XDG_DATA_DIRS,
+ * in order. We stop looking as soon as we successfully open a file
+ * or in the case that we run out of XDG_DATA_DIRS.
+ *
+ * If we hit an error other than ENOENT then we warn about that and
+ * exit immediately. We should only attempt fallback in the case that
+ * the file in the higher-precedence directory is non-existent.
+ */
+ do
+ {
+ gchar *filename;
+
+ filename = g_build_filename (prefix, "dconf/profile", profile, NULL);
+ fp = dconf_engine_fopen (filename, "r");
+
+ /* If it wasn't ENOENT then we don't want to continue on to check
+ * other paths. Fail immediately.
+ */
+ if (fp == NULL && errno != ENOENT)
+ {
+ g_warning ("Unable to open %s: %s", filename, g_strerror (errno));
+ g_free (filename);
+ return NULL;
+ }
+
+ g_free (filename);
+ }
+ while (fp == NULL && (prefix = *xdg_data_dirs++));
+
+ /* If we didn't find it, this could be NULL. That's OK. */
+ return fp;
+}
+
+static FILE *
+dconf_engine_open_mandatory_profile (void)
+{
+ gchar path[20 + sizeof MANDATORY_DIR];
+ gint mdlen = strlen (MANDATORY_DIR);
+
+ memcpy (path, MANDATORY_DIR, mdlen);
+ snprintf (path + mdlen, 20, "%u", (guint) getuid ());
+
+ return dconf_engine_fopen (path, "r");
+}
+
+static FILE *
+dconf_engine_open_runtime_profile (void)
+{
+ const gchar *runtime_dir;
+ gchar *path;
+ gint rdlen;
+
+ runtime_dir = g_get_user_runtime_dir ();
+ rdlen = strlen (runtime_dir);
+
+ path = g_alloca (rdlen + sizeof RUNTIME_PROFILE);
+ memcpy (path, runtime_dir, rdlen);
+ memcpy (path + rdlen, RUNTIME_PROFILE, sizeof RUNTIME_PROFILE);
+
+ return dconf_engine_fopen (path, "r");
+}
+
+DConfEngineSource **
+dconf_engine_profile_open (const gchar *profile,
+ gint *n_sources)
+{
+ DConfEngineSource **sources;
+ FILE *file = NULL;
+
+ /* We must consider a few different possibilities for the dconf
+ * profile file. We proceed until we have either
+ *
+ * a) a profile name; or
+ *
+ * b) a profile file is open
+ *
+ * If we get a profile name, even if the file is missing, we will use
+ * that name rather than falling back to another possibility. In this
+ * case, we will issue a warning.
+ *
+ * Therefore, at each step, we ensure that there is no profile name or
+ * file yet open before checking the next possibility.
+ *
+ * Note that @profile is an argument to this function, so we will end
+ * up trying none of the five possibilities if that is given.
+ */
+
+ /* 1. Mandatory profile */
+ if (profile == NULL)
+ file = dconf_engine_open_mandatory_profile ();
+
+ /* 2. Environment variable */
+ if (profile == NULL && file == NULL)
+ profile = g_getenv ("DCONF_PROFILE");
+
+ /* 3. Runtime profile */
+ if (profile == NULL && file == NULL)
+ file = dconf_engine_open_runtime_profile ();
+
+ /* 4. User profile */
+ if (profile == NULL && file == NULL)
+ file = dconf_engine_open_profile_file ("user");
+
+ /* 5. Default profile */
+ if (profile == NULL && file == NULL)
+ return dconf_engine_default_profile (n_sources);
+
+ /* 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.
+ */
+ if (profile != NULL)
+ {
+ g_assert (file == NULL);
+
+ if (profile[0] != '/')
+ file = dconf_engine_open_profile_file (profile);
+ else
+ file = dconf_engine_fopen (profile, "r");
+ }
+
+ if (file != NULL)
+ {
+ sources = dconf_engine_read_profile_file (file, n_sources);
+ fclose (file);
+ }
+ else
+ {
+ g_warning ("unable to open named profile (%s): using the null configuration.", profile);
+ sources = dconf_engine_null_profile (n_sources);
+ }
+
+ return sources;
+}
diff --git a/engine/dconf-engine-profile.h b/engine/dconf-engine-profile.h
new file mode 100644
index 0000000..4a40383
--- /dev/null
+++ b/engine/dconf-engine-profile.h
@@ -0,0 +1,30 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __dconf_engine_profile_h__
+#define __dconf_engine_profile_h__
+
+#include "dconf-engine-source.h"
+
+G_GNUC_INTERNAL
+DConfEngineSource ** dconf_engine_profile_open (const gchar *profile,
+ gint *n_sources);
+
+#endif
diff --git a/engine/dconf-engine-source-file.c b/engine/dconf-engine-source-file.c
new file mode 100644
index 0000000..d5a7284
--- /dev/null
+++ b/engine/dconf-engine-source-file.c
@@ -0,0 +1,77 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-engine-source-private.h"
+
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <errno.h>
+
+static void
+dconf_engine_source_file_init (DConfEngineSource *source)
+{
+ source->bus_type = G_BUS_TYPE_NONE;
+ source->bus_name = NULL;
+ source->object_path = NULL;
+}
+
+static gboolean
+dconf_engine_source_file_needs_reopen (DConfEngineSource *source)
+{
+ return !source->values;
+}
+
+static GvdbTable *
+dconf_engine_source_file_reopen (DConfEngineSource *source)
+{
+ GError *error = NULL;
+ GvdbTable *table;
+
+ table = gvdb_table_new (source->name, FALSE, &error);
+
+ if (table == NULL)
+ {
+ if (!source->did_warn)
+ {
+ g_warning ("unable to open file '%s': %s; expect degraded performance", source->name, error->message);
+ source->did_warn = TRUE;
+ }
+
+ g_error_free (error);
+ }
+
+ return table;
+}
+
+static void
+dconf_engine_source_file_finalize (DConfEngineSource *source)
+{
+}
+
+G_GNUC_INTERNAL
+const DConfEngineSourceVTable dconf_engine_source_file_vtable = {
+ .instance_size = sizeof (DConfEngineSource),
+ .init = dconf_engine_source_file_init,
+ .finalize = dconf_engine_source_file_finalize,
+ .needs_reopen = dconf_engine_source_file_needs_reopen,
+ .reopen = dconf_engine_source_file_reopen
+};
diff --git a/engine/dconf-engine-source-private.h b/engine/dconf-engine-source-private.h
new file mode 100644
index 0000000..4f68935
--- /dev/null
+++ b/engine/dconf-engine-source-private.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __dconf_engine_source_private_h__
+#define __dconf_engine_source_private_h__
+
+#include "dconf-engine-source.h"
+
+G_GNUC_INTERNAL extern const DConfEngineSourceVTable dconf_engine_source_file_vtable;
+G_GNUC_INTERNAL extern const DConfEngineSourceVTable dconf_engine_source_user_vtable;
+G_GNUC_INTERNAL extern const DConfEngineSourceVTable dconf_engine_source_service_vtable;
+G_GNUC_INTERNAL extern const DConfEngineSourceVTable dconf_engine_source_system_vtable;
+
+#endif /* __dconf_engine_source_private_h__ */
diff --git a/engine/dconf-engine-source-service.c b/engine/dconf-engine-source-service.c
new file mode 100644
index 0000000..f92f4ae
--- /dev/null
+++ b/engine/dconf-engine-source-service.c
@@ -0,0 +1,95 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-engine-source-private.h"
+
+#include "dconf-engine.h"
+#include <sys/mman.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+static void
+dconf_engine_source_service_init (DConfEngineSource *source)
+{
+ source->bus_type = G_BUS_TYPE_SESSION;
+ source->bus_name = g_strdup ("ca.desrt.dconf");
+ source->object_path = g_strdup_printf ("/ca/desrt/dconf/%s", source->name);
+ source->writable = TRUE;
+}
+
+static gboolean
+dconf_engine_source_service_needs_reopen (DConfEngineSource *source)
+{
+ return !source->values || !gvdb_table_is_valid (source->values);
+}
+
+static GvdbTable *
+dconf_engine_source_service_reopen (DConfEngineSource *source)
+{
+ GError *error = NULL;
+ GvdbTable *table;
+ gchar *filename;
+
+ filename = g_build_filename (g_get_user_runtime_dir (), "dconf-service", source->name, NULL);
+
+ table = gvdb_table_new (filename, FALSE, NULL);
+
+ if (table == NULL)
+ {
+ /* If the file does not exist, kick the service to have it created. */
+ dconf_engine_dbus_call_sync_func (source->bus_type, source->bus_name, source->object_path,
+ "ca.desrt.dconf.Writer", "Init", g_variant_new ("()"), NULL, NULL);
+
+ /* try again */
+ table = gvdb_table_new (filename, FALSE, &error);
+
+ if (table == NULL)
+ {
+ if (!source->did_warn)
+ {
+ g_warning ("unable to open file '%s': %s; expect degraded performance", filename, error->message);
+ source->did_warn = TRUE;
+ }
+
+ g_error_free (error);
+ }
+ }
+
+ g_free (filename);
+
+ return table;
+}
+
+static void
+dconf_engine_source_service_finalize (DConfEngineSource *source)
+{
+}
+
+G_GNUC_INTERNAL
+const DConfEngineSourceVTable dconf_engine_source_service_vtable = {
+ .instance_size = sizeof (DConfEngineSource),
+ .init = dconf_engine_source_service_init,
+ .finalize = dconf_engine_source_service_finalize,
+ .needs_reopen = dconf_engine_source_service_needs_reopen,
+ .reopen = dconf_engine_source_service_reopen
+};
diff --git a/engine/dconf-engine-source-system.c b/engine/dconf-engine-source-system.c
new file mode 100644
index 0000000..0d7c2d8
--- /dev/null
+++ b/engine/dconf-engine-source-system.c
@@ -0,0 +1,81 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-engine-source-private.h"
+
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <errno.h>
+
+static void
+dconf_engine_source_system_init (DConfEngineSource *source)
+{
+ source->bus_type = G_BUS_TYPE_SYSTEM;
+ source->bus_name = g_strdup ("ca.desrt.dconf");
+ source->object_path = g_strdup_printf ("/ca/desrt/dconf/Writer/%s", source->name);
+}
+
+static gboolean
+dconf_engine_source_system_needs_reopen (DConfEngineSource *source)
+{
+ return !source->values || !gvdb_table_is_valid (source->values);
+}
+
+static GvdbTable *
+dconf_engine_source_system_reopen (DConfEngineSource *source)
+{
+ GError *error = NULL;
+ GvdbTable *table;
+ gchar *filename;
+
+ filename = g_build_filename (SYSCONFDIR "/dconf/db", source->name, NULL);
+ table = gvdb_table_new (filename, FALSE, &error);
+
+ if (table == NULL)
+ {
+ if (!source->did_warn)
+ {
+ g_warning ("unable to open file '%s': %s; expect degraded performance", filename, error->message);
+ source->did_warn = TRUE;
+ }
+
+ g_error_free (error);
+ }
+
+ g_free (filename);
+
+ return table;
+}
+
+static void
+dconf_engine_source_system_finalize (DConfEngineSource *source)
+{
+}
+
+G_GNUC_INTERNAL
+const DConfEngineSourceVTable dconf_engine_source_system_vtable = {
+ .instance_size = sizeof (DConfEngineSource),
+ .init = dconf_engine_source_system_init,
+ .finalize = dconf_engine_source_system_finalize,
+ .needs_reopen = dconf_engine_source_system_needs_reopen,
+ .reopen = dconf_engine_source_system_reopen
+};
diff --git a/engine/dconf-engine-source-user.c b/engine/dconf-engine-source-user.c
new file mode 100644
index 0000000..1657875
--- /dev/null
+++ b/engine/dconf-engine-source-user.c
@@ -0,0 +1,96 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-engine-source-private.h"
+
+#include "../shm/dconf-shm.h"
+#include <sys/mman.h>
+#include <fcntl.h>
+#include <errno.h>
+
+typedef struct
+{
+ DConfEngineSource source;
+
+ guint8 *shm;
+} DConfEngineSourceUser;
+
+static GvdbTable *
+dconf_engine_source_user_open_gvdb (const gchar *name)
+{
+ GvdbTable *table;
+ gchar *filename;
+
+ /* This can fail in the normal case of the user not having any
+ * settings. That's OK and it shouldn't be considered as an error.
+ */
+ filename = g_build_filename (g_get_user_config_dir (), "dconf", name, NULL);
+ table = gvdb_table_new (filename, FALSE, NULL);
+ g_free (filename);
+
+ return table;
+}
+
+static void
+dconf_engine_source_user_init (DConfEngineSource *source)
+{
+ source->bus_type = G_BUS_TYPE_SESSION;
+ source->bus_name = g_strdup ("ca.desrt.dconf");
+ source->object_path = g_strdup_printf ("/ca/desrt/dconf/Writer/%s", source->name);
+ source->writable = TRUE;
+}
+
+static gboolean
+dconf_engine_source_user_needs_reopen (DConfEngineSource *source)
+{
+ DConfEngineSourceUser *user_source = (DConfEngineSourceUser *) source;
+
+ return dconf_shm_is_flagged (user_source->shm);
+}
+
+static GvdbTable *
+dconf_engine_source_user_reopen (DConfEngineSource *source)
+{
+ DConfEngineSourceUser *user_source = (DConfEngineSourceUser *) source;
+
+ dconf_shm_close (user_source->shm);
+ user_source->shm = dconf_shm_open (source->name);
+
+ return dconf_engine_source_user_open_gvdb (source->name);
+}
+
+static void
+dconf_engine_source_user_finalize (DConfEngineSource *source)
+{
+ DConfEngineSourceUser *user_source = (DConfEngineSourceUser *) source;
+
+ dconf_shm_close (user_source->shm);
+}
+
+G_GNUC_INTERNAL
+const DConfEngineSourceVTable dconf_engine_source_user_vtable = {
+ .instance_size = sizeof (DConfEngineSourceUser),
+ .init = dconf_engine_source_user_init,
+ .finalize = dconf_engine_source_user_finalize,
+ .needs_reopen = dconf_engine_source_user_needs_reopen,
+ .reopen = dconf_engine_source_user_reopen
+};
diff --git a/engine/dconf-engine-source.c b/engine/dconf-engine-source.c
new file mode 100644
index 0000000..019a59c
--- /dev/null
+++ b/engine/dconf-engine-source.c
@@ -0,0 +1,141 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-engine-source-private.h"
+
+#include <string.h>
+
+void
+dconf_engine_source_free (DConfEngineSource *source)
+{
+ if (source->values)
+ gvdb_table_free (source->values);
+
+ if (source->locks)
+ gvdb_table_free (source->locks);
+
+ source->vtable->finalize (source);
+ g_free (source->bus_name);
+ g_free (source->object_path);
+ g_free (source->name);
+ g_free (source);
+}
+
+gboolean
+dconf_engine_source_refresh (DConfEngineSource *source)
+{
+ if (source->vtable->needs_reopen (source))
+ {
+ gboolean was_open;
+ gboolean is_open;
+
+ /* Record if we had a gvdb before or not. */
+ was_open = source->values != NULL;
+
+ g_clear_pointer (&source->values, gvdb_table_free);
+ g_clear_pointer (&source->locks, gvdb_table_free);
+
+ source->values = source->vtable->reopen (source);
+ if (source->values)
+ source->locks = gvdb_table_get_table (source->values, ".locks");
+
+ /* Check if we ended up with a gvdb. */
+ is_open = source->values != NULL;
+
+ /* Only return TRUE in the case that we either had a database
+ * before or ended up with one after. In the case that we just go
+ * from NULL to NULL, return FALSE.
+ */
+ return was_open || is_open;
+ }
+
+ return FALSE;
+}
+
+DConfEngineSource *
+dconf_engine_source_new (const gchar *description)
+{
+ const DConfEngineSourceVTable *vtable;
+ DConfEngineSource *source;
+ const gchar *colon;
+
+ /* Source descriptions are of the form
+ *
+ * type:name
+ *
+ * Where type must currently be one of "user-db" or "system-db".
+ *
+ * We first find the colon.
+ */
+ colon = strchr (description, ':');
+
+ /* Ensure that we have a colon and that a database name follows it. */
+ if (colon == NULL || colon[1] == '\0')
+ return NULL;
+
+ /* Check if the part before the colon is "user-db"... */
+ if ((colon == description + 7) && memcmp (description, "user-db", 7) == 0)
+ vtable = &dconf_engine_source_user_vtable;
+
+ /* ...or "service-db" */
+ else if ((colon == description + 10) && memcmp (description, "service-db", 10) == 0)
+ vtable = &dconf_engine_source_service_vtable;
+
+ /* ...or "system-db" */
+ else if ((colon == description + 9) && memcmp (description, "system-db", 9) == 0)
+ vtable = &dconf_engine_source_system_vtable;
+
+ /* ...or "file-db" */
+ else if ((colon == description + 7) && memcmp (description, "file-db", 7) == 0)
+ vtable = &dconf_engine_source_file_vtable;
+
+ /* If it's not any of those, we have failed. */
+ else
+ return NULL;
+
+ /* We have had a successful parse.
+ *
+ * - either user-db: or system-db:
+ * - non-NULL and non-empty database name
+ *
+ * Create the source.
+ */
+ source = g_malloc0 (vtable->instance_size);
+ source->vtable = vtable;
+ source->name = g_strdup (colon + 1);
+ source->vtable->init (source);
+
+ return source;
+}
+
+DConfEngineSource *
+dconf_engine_source_new_default (void)
+{
+ DConfEngineSource *source;
+
+ source = g_malloc0 (dconf_engine_source_user_vtable.instance_size);
+ source->vtable = &dconf_engine_source_user_vtable;
+ source->name = g_strdup ("user");
+ source->vtable->init (source);
+
+ return source;
+}
diff --git a/engine/dconf-engine-source.h b/engine/dconf-engine-source.h
new file mode 100644
index 0000000..802f09d
--- /dev/null
+++ b/engine/dconf-engine-source.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __dconf_engine_source_h__
+#define __dconf_engine_source_h__
+
+#include "../gvdb/gvdb-reader.h"
+#include <gio/gio.h>
+
+typedef struct _DConfEngineSourceVTable DConfEngineSourceVTable;
+typedef struct _DConfEngineSource DConfEngineSource;
+
+struct _DConfEngineSourceVTable
+{
+ gsize instance_size;
+
+ void (* init) (DConfEngineSource *source);
+ void (* finalize) (DConfEngineSource *source);
+ gboolean (* needs_reopen) (DConfEngineSource *source);
+ GvdbTable * (* reopen) (DConfEngineSource *source);
+};
+
+struct _DConfEngineSource
+{
+ const DConfEngineSourceVTable *vtable;
+
+ GvdbTable *values;
+ GvdbTable *locks;
+ GBusType bus_type;
+ gboolean writable;
+ gboolean did_warn;
+ gchar *bus_name;
+ gchar *object_path;
+ gchar *name;
+};
+
+G_GNUC_INTERNAL
+void dconf_engine_source_free (DConfEngineSource *source);
+
+G_GNUC_INTERNAL
+gboolean dconf_engine_source_refresh (DConfEngineSource *source);
+
+G_GNUC_INTERNAL
+DConfEngineSource * dconf_engine_source_new (const gchar *name);
+
+G_GNUC_INTERNAL
+DConfEngineSource * dconf_engine_source_new_default (void);
+
+#endif /* __dconf_engine_source_h__ */
diff --git a/engine/dconf-engine.c b/engine/dconf-engine.c
new file mode 100644
index 0000000..18b8aa5
--- /dev/null
+++ b/engine/dconf-engine.c
@@ -0,0 +1,1496 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#define _XOPEN_SOURCE 600
+#include "dconf-engine.h"
+
+#include "../common/dconf-enums.h"
+#include "../common/dconf-paths.h"
+#include "../gvdb/gvdb-reader.h"
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/mman.h>
+
+#include "dconf-engine-profile.h"
+
+/* The engine has zero or more sources.
+ *
+ * If it has zero sources then things are very uninteresting. Nothing
+ * is writable, nothing will ever be written and reads will always
+ * return NULL.
+ *
+ * There are two interesting cases when there is a non-zero number of
+ * sources. Writing only ever occurs to the first source, if at all.
+ * Non-first sources are never writable.
+ *
+ * The first source may or may not be writable. In the usual case the
+ * first source is the one in the user's home directory and is writable,
+ * but it may be that the profile was setup for read-only access to
+ * system sources only.
+ *
+ * In the case that the first source is not writable (and therefore
+ * there are no writable sources), is_writable() will always return
+ * FALSE and no writes will ever be performed.
+ *
+ * It's possible to request changes in three ways:
+ *
+ * - synchronous: the D-Bus message is immediately sent to the
+ * dconf service and we block until we receive the reply. The change
+ * signal will follow soon thereafter (when we receive the signal on
+ * D-Bus).
+ *
+ * - asynchronous: typical asynchronous operation: we send the request
+ * and return immediately, notifying using a callback when the
+ * request is completed (and the new value is in the database). The
+ * change signal follows in the same way as with synchronous.
+ *
+ * - fast: we record the value locally and signal the change, returning
+ * immediately, as if the value is already in the database (from the
+ * viewpoint of the local process). We keep note of the new value
+ * locally until the service has confirmed that the write was
+ * successful. If the write fails, we emit a change signal. From
+ * the view of the program it looks like the value was successfully
+ * changed but then quickly changed back again by some external
+ * agent.
+ *
+ * In fast mode if we were to immediately put all requests "in flight",
+ * then we could end up in a situation where the service is kept
+ * (needlessly) busy rewriting the database over and over again after a
+ * sequence of fast changes on the client side.
+ *
+ * To avoid the issue we limit the number of in-flight requests to one.
+ * If a request is already in flight, subsequent changes are merged into
+ * a single aggregated pending change to be submitted as the next write
+ * after the in-flight request completes.
+ *
+ * NB: I tell a lie. Async is not supported yet.
+ *
+ * Notes about threading:
+ *
+ * The engine is oblivious to threads and main contexts.
+ *
+ * What this means is that the engine has no interaction with GMainLoop
+ * and will not schedule idles or anything of the sort. All calls made
+ * by the engine to the client library will be made in response to
+ * incoming method calls, from the same thread as the incoming call.
+ *
+ * If dconf_engine_call_handle_reply() or
+ * dconf_engine_handle_dbus_signal() are called from 'exotic' threads
+ * (as will often be the case) then the resulting calls to
+ * dconf_engine_change_notify() will come from the same thread. That's
+ * left for the client library to deal with.
+ *
+ * All that said, the engine is completely threadsafe. The client
+ * library can call any method from any thread at any time -- as long as
+ * it is willing to deal with receiving the change notifies in those
+ * threads.
+ *
+ * Thread-safety is implemented using three locks.
+ *
+ * The first lock (sources_lock) protects the sources. Although the
+ * sources are only ever read from, it is necessary to lock them because
+ * it is not safe to read during a refresh (when the source is being
+ * closed and reopened). Accordingly, sources_lock need only be
+ * acquired when accessing the parts of the sources that are subject to
+ * change as a result of refreshes; the static parts (like bus type,
+ * object path, etc) can be accessed without holding the lock. The
+ * 'sources' array itself (and 'n_sources') are set at construction and
+ * never change after that.
+ *
+ * The second lock (queue_lock) protects the queue (represented with two
+ * fields pending and in_flight) used to implement the "fast" writes
+ * described above.
+ *
+ * The third lock (subscription_count_lock) protects the two hash tables
+ * that are used to keep track of the number of subscriptions held by
+ * the client library to each path.
+ *
+ * If sources_lock and queue_lock are held at the same time then then
+ * sources_lock must have been acquired first.
+ *
+ * subscription_count_lock is never held at the same time as
+ * sources_lock or queue_lock
+ */
+
+static GSList *dconf_engine_global_list;
+static GMutex dconf_engine_global_lock;
+
+struct _DConfEngine
+{
+ gpointer user_data; /* Set at construct time */
+ GDestroyNotify free_func;
+ gint ref_count;
+
+ GMutex sources_lock; /* This lock is for the sources (ie: refreshing) and state. */
+ guint64 state; /* Counter that changes every time a source is refreshed. */
+ DConfEngineSource **sources; /* Array never changes, but each source changes internally. */
+ gint n_sources;
+
+ GMutex queue_lock; /* This lock is for pending, in_flight, queue_cond */
+ GCond queue_cond; /* Signalled when there are neither in-flight nor pending changes. */
+ DConfChangeset *pending; /* Yet to be sent on the wire. */
+ DConfChangeset *in_flight; /* Already sent but awaiting response. */
+
+ gchar *last_handled; /* reply tag from last item in in_flight */
+
+ /**
+ * establishing and active, are hash tables storing the number
+ * of subscriptions to each path in the two possible states
+ */
+ /* This lock ensures that transactions involving subscription counts are atomic */
+ GMutex subscription_count_lock;
+ /* active on the client side, but awaiting confirmation from the writer */
+ GHashTable *establishing;
+ /* active on the client side, and with a D-Bus match rule established */
+ GHashTable *active;
+};
+
+/* When taking the sources lock we check if any of the databases have
+ * had updates.
+ *
+ * Anything that is accessing the database (even only reading) needs to
+ * be holding the lock (since refreshes could be happening in another
+ * thread), so this makes sense.
+ *
+ * We could probably optimise this to avoid checking some databases in
+ * certain cases (ie: we do not need to check the user's database when
+ * we are only interested in checking writability) but this works well
+ * enough for now and is less prone to errors.
+ *
+ * We could probably change to a reader/writer situation that is only
+ * holding the write lock when actually making changes during a refresh
+ * but the engine is probably only ever really in use by two threads at
+ * a given time (main thread doing reads, DBus worker thread clearing
+ * the queue) so it seems unlikely that lock contention will become an
+ * issue.
+ *
+ * If it does, we can revisit this...
+ */
+static void
+dconf_engine_acquire_sources (DConfEngine *engine)
+{
+ gint i;
+
+ g_mutex_lock (&engine->sources_lock);
+
+ for (i = 0; i < engine->n_sources; i++)
+ if (dconf_engine_source_refresh (engine->sources[i]))
+ engine->state++;
+}
+
+static void
+dconf_engine_release_sources (DConfEngine *engine)
+{
+ g_mutex_unlock (&engine->sources_lock);
+}
+
+static void
+dconf_engine_lock_queue (DConfEngine *engine)
+{
+ g_mutex_lock (&engine->queue_lock);
+}
+
+static void
+dconf_engine_unlock_queue (DConfEngine *engine)
+{
+ g_mutex_unlock (&engine->queue_lock);
+}
+
+/**
+ * Adds the count of subscriptions to @path in @from_table to the
+ * corresponding count in @to_table, creating it if it did not exist.
+ * Removes the count from @from_table.
+ */
+static void
+dconf_engine_move_subscriptions (GHashTable *from_counts,
+ GHashTable *to_counts,
+ const gchar *path)
+{
+ guint from_count = GPOINTER_TO_UINT (g_hash_table_lookup (from_counts, path));
+ guint old_to_count = GPOINTER_TO_UINT (g_hash_table_lookup (to_counts, path));
+ // Detect overflows
+ g_assert (old_to_count <= G_MAXUINT - from_count);
+ guint new_to_count = old_to_count + from_count;
+ if (from_count != 0)
+ {
+ g_hash_table_remove (from_counts, path);
+ g_hash_table_replace (to_counts,
+ g_strdup (path),
+ GUINT_TO_POINTER (new_to_count));
+ }
+}
+
+/**
+ * Increments the reference count for the subscription to @path, or sets
+ * it to 1 if it didn’t previously exist.
+ * Returns the new reference count.
+ */
+static guint
+dconf_engine_inc_subscriptions (GHashTable *counts,
+ const gchar *path)
+{
+ guint old_count = GPOINTER_TO_UINT (g_hash_table_lookup (counts, path));
+ // Detect overflows
+ g_assert (old_count < G_MAXUINT);
+ guint new_count = old_count + 1;
+ g_hash_table_replace (counts, g_strdup (path), GUINT_TO_POINTER (new_count));
+ return new_count;
+}
+
+/**
+ * Decrements the reference count for the subscription to @path, or
+ * removes it if the new value is 0. The count must exist and be greater
+ * than 0.
+ * Returns the new reference count, or 0 if it does not exist.
+ */
+static guint
+dconf_engine_dec_subscriptions (GHashTable *counts,
+ const gchar *path)
+{
+ guint old_count = GPOINTER_TO_UINT (g_hash_table_lookup (counts, path));
+ g_assert (old_count > 0);
+ guint new_count = old_count - 1;
+ if (new_count == 0)
+ g_hash_table_remove (counts, path);
+ else
+ g_hash_table_replace (counts, g_strdup (path), GUINT_TO_POINTER (new_count));
+ return new_count;
+}
+
+/**
+ * Returns the reference count for the subscription to @path, or 0 if it
+ * does not exist.
+ */
+static guint
+dconf_engine_count_subscriptions (GHashTable *counts,
+ const gchar *path)
+{
+ return GPOINTER_TO_UINT (g_hash_table_lookup (counts, path));
+}
+
+/**
+ * Acquires the subscription counts lock, which must be held when
+ * reading or writing to the subscription counts.
+ */
+static void
+dconf_engine_lock_subscription_counts (DConfEngine *engine)
+{
+ g_mutex_lock (&engine->subscription_count_lock);
+}
+
+/**
+ * Releases the subscription counts lock
+ */
+static void
+dconf_engine_unlock_subscription_counts (DConfEngine *engine)
+{
+ g_mutex_unlock (&engine->subscription_count_lock);
+}
+
+DConfEngine *
+dconf_engine_new (const gchar *profile,
+ gpointer user_data,
+ GDestroyNotify free_func)
+{
+ DConfEngine *engine;
+
+ engine = g_slice_new0 (DConfEngine);
+ engine->user_data = user_data;
+ engine->free_func = free_func;
+ engine->ref_count = 1;
+
+ g_mutex_init (&engine->sources_lock);
+ g_mutex_init (&engine->queue_lock);
+ g_cond_init (&engine->queue_cond);
+
+ engine->sources = dconf_engine_profile_open (profile, &engine->n_sources);
+
+ g_mutex_lock (&dconf_engine_global_lock);
+ dconf_engine_global_list = g_slist_prepend (dconf_engine_global_list, engine);
+ g_mutex_unlock (&dconf_engine_global_lock);
+
+ g_mutex_init (&engine->subscription_count_lock);
+ engine->establishing = g_hash_table_new_full (g_str_hash,
+ g_str_equal,
+ g_free,
+ NULL);
+ engine->active = g_hash_table_new_full (g_str_hash,
+ g_str_equal,
+ g_free,
+ NULL);
+
+ return engine;
+}
+
+void
+dconf_engine_unref (DConfEngine *engine)
+{
+ gint ref_count;
+
+ again:
+ ref_count = engine->ref_count;
+
+ if (ref_count == 1)
+ {
+ gint i;
+
+ /* We are about to drop the last reference, but there is a chance
+ * that a signal may be happening at this very moment, causing the
+ * engine to gain another reference (due to its position in the
+ * global engine list).
+ *
+ * Acquiring the lock here means that either we will remove this
+ * engine from the list first or we will notice the reference
+ * count has increased (and skip the free).
+ */
+ g_mutex_lock (&dconf_engine_global_lock);
+ if (engine->ref_count != 1)
+ {
+ g_mutex_unlock (&dconf_engine_global_lock);
+ goto again;
+ }
+ dconf_engine_global_list = g_slist_remove (dconf_engine_global_list, engine);
+ g_mutex_unlock (&dconf_engine_global_lock);
+
+ g_mutex_clear (&engine->sources_lock);
+ g_mutex_clear (&engine->queue_lock);
+ g_cond_clear (&engine->queue_cond);
+
+ g_free (engine->last_handled);
+
+ g_clear_pointer (&engine->pending, dconf_changeset_unref);
+ g_clear_pointer (&engine->in_flight, dconf_changeset_unref);
+
+ for (i = 0; i < engine->n_sources; i++)
+ dconf_engine_source_free (engine->sources[i]);
+
+ g_free (engine->sources);
+
+ g_hash_table_unref (engine->establishing);
+ g_hash_table_unref (engine->active);
+
+ g_mutex_clear (&engine->subscription_count_lock);
+
+ if (engine->free_func)
+ engine->free_func (engine->user_data);
+
+ g_slice_free (DConfEngine, engine);
+ }
+
+ else if (!g_atomic_int_compare_and_exchange (&engine->ref_count, ref_count, ref_count - 1))
+ goto again;
+}
+
+static DConfEngine *
+dconf_engine_ref (DConfEngine *engine)
+{
+ g_atomic_int_inc (&engine->ref_count);
+
+ return engine;
+}
+
+guint64
+dconf_engine_get_state (DConfEngine *engine)
+{
+ guint64 state;
+
+ dconf_engine_acquire_sources (engine);
+ state = engine->state;
+ dconf_engine_release_sources (engine);
+
+ return state;
+}
+
+static gboolean
+dconf_engine_is_writable_internal (DConfEngine *engine,
+ const gchar *key)
+{
+ gint i;
+
+ /* We must check several things:
+ *
+ * - we have at least one source
+ *
+ * - the first source is writable
+ *
+ * - the key is not locked in a non-writable (ie: non-first) source
+ */
+ if (engine->n_sources == 0)
+ return FALSE;
+
+ if (engine->sources[0]->writable == FALSE)
+ return FALSE;
+
+ /* Ignore locks in the first source.
+ *
+ * Either it is writable and therefore ignoring locks is the right
+ * thing to do, or it's non-writable and we caught that case above.
+ */
+ for (i = 1; i < engine->n_sources; i++)
+ if (engine->sources[i]->locks && gvdb_table_has_value (engine->sources[i]->locks, key))
+ return FALSE;
+
+ return TRUE;
+}
+
+gboolean
+dconf_engine_is_writable (DConfEngine *engine,
+ const gchar *key)
+{
+ gboolean writable;
+
+ dconf_engine_acquire_sources (engine);
+ writable = dconf_engine_is_writable_internal (engine, key);
+ dconf_engine_release_sources (engine);
+
+ return writable;
+}
+
+gchar **
+dconf_engine_list_locks (DConfEngine *engine,
+ const gchar *path,
+ gint *length)
+{
+ gchar **strv;
+
+ if (dconf_is_dir (path, NULL))
+ {
+ GHashTable *set;
+
+ set = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+ dconf_engine_acquire_sources (engine);
+
+ if (engine->n_sources > 0 && engine->sources[0]->writable)
+ {
+ gint i, j;
+
+ for (i = 1; i < engine->n_sources; i++)
+ {
+ if (engine->sources[i]->locks)
+ {
+ strv = gvdb_table_get_names (engine->sources[i]->locks, NULL);
+
+ for (j = 0; strv[j]; j++)
+ {
+ /* It is not currently possible to lock dirs, so we
+ * don't (yet) have to check the other direction.
+ */
+ if (g_str_has_prefix (strv[j], path))
+ g_hash_table_add (set, strv[j]);
+ else
+ g_free (strv[j]);
+ }
+
+ g_free (strv);
+ }
+ }
+ }
+ else
+ g_hash_table_add (set, g_strdup (path));
+
+ dconf_engine_release_sources (engine);
+
+ strv = (gchar **) g_hash_table_get_keys_as_array (set, (guint *) length);
+ g_hash_table_steal_all (set);
+ g_hash_table_unref (set);
+ }
+ else
+ {
+ if (dconf_engine_is_writable (engine, path))
+ {
+ strv = g_new0 (gchar *, 0 + 1);
+ }
+ else
+ {
+ strv = g_new0 (gchar *, 1 + 1);
+ strv[0] = g_strdup (path);
+ }
+ }
+
+ return strv;
+}
+
+static gboolean
+dconf_engine_find_key_in_queue (const GQueue *queue,
+ const gchar *key,
+ GVariant **value)
+{
+ GList *node;
+
+ /* Tail to head... */
+ for (node = queue->tail; node; node = node->prev)
+ if (dconf_changeset_get (node->data, key, value))
+ return TRUE;
+
+ return FALSE;
+}
+
+GVariant *
+dconf_engine_read (DConfEngine *engine,
+ DConfReadFlags flags,
+ const GQueue *read_through,
+ const gchar *key)
+{
+ GVariant *value = NULL;
+ gint lock_level = 0;
+ gint i;
+
+ dconf_engine_acquire_sources (engine);
+
+ /* There are a number of situations that this function has to deal
+ * with and they interact in unusual ways. We attempt to write the
+ * rules for all cases here:
+ *
+ * With respect to the steady-state condition with no locks:
+ *
+ * This is the case where there are no changes queued, no
+ * read_through and no locks.
+ *
+ * The value returned is the one from the lowest-index source that
+ * contains that value.
+ *
+ * With respect to locks:
+ *
+ * If a lock is present (except in source #0 where it is ignored)
+ * then we will only return a value found in the source where the
+ * lock was present, or a higher-index source (following the normal
+ * rule that sources with lower indexes take priority).
+ *
+ * This statement includes read_through and queued changes. If a
+ * lock is found, we will ignore those.
+ *
+ * With respect to flags:
+ *
+ * If DCONF_READ_USER_VALUE is given then we completely ignore all
+ * locks, returning the user value all the time, even if it is not
+ * visible (because of a lock). This includes any pending value
+ * that is in the read_through or pending queues.
+ *
+ * If DCONF_READ_DEFAULT_VALUE is given then we skip the writable
+ * database and the queues (including read_through, which is
+ * meaningless in this case) and skip directly to the non-writable
+ * databases. This is defined as the value that the user would see
+ * if they were to have just done a reset for that key.
+ *
+ * With respect to read_through and queued changed:
+ *
+ * We only consider read_through and queued changes in the event
+ * that we have a writable source. This will possibly cause us to
+ * ignore read_through and will have no real effect on the queues
+ * (since they will be empty anyway if we have no writable source).
+ *
+ * We only consider read_through and queued changes in the event
+ * that we have not found any locks.
+ *
+ * If there is a non-NULL value found in read_through or the queued
+ * changes then we will return that value.
+ *
+ * If there is a NULL value (ie: a reset) found in read_through or
+ * the queued changes then we will only ignore any value found in
+ * the first source (which must be writable, or else we would not
+ * have been considering read_through and the queues). This is
+ * consistent with the fact that a reset will unset any value found
+ * in this source but will not affect values found in lower sources.
+ *
+ * Put another way: if a non-writable source contains a value for a
+ * particular key then it is impossible for this function to return
+ * NULL.
+ *
+ * We implement the above rules as follows. We have three state
+ * tracking variables:
+ *
+ * - lock_level: records if and where we found a lock
+ *
+ * - found_key: records if we found the key in any queue
+ *
+ * - value: records the value of the found key (NULL for resets)
+ *
+ * We take these steps:
+ *
+ * 1. check for lockdown. If we find a lock then we prevent any
+ * other sources (including read_through and pending/in-flight)
+ * from affecting the value of the key.
+ *
+ * We record the result of this in the lock_level variable. Zero
+ * means that no locks were found. Non-zero means that a lock was
+ * found in the source with the index given by the variable.
+ *
+ * 2. check the uncommitted changes in the read_through list as the
+ * highest priority. This is only done if we have a writable
+ * source and no locks were found.
+ *
+ * If we found an entry in the read_through then we set
+ * 'found_key' to TRUE and set 'value' to the value that we found
+ * (which will be NULL in the case of finding a reset request).
+ *
+ * 3. check our pending and in-flight "fast" changes (in that order).
+ * This is only done if we have a writable source and no locks
+ * were found. It is also only done if we did not find the key in
+ * the read_through.
+ *
+ * 4. check the first source, if there is one.
+ *
+ * This is only done if 'found_key' is FALSE. If 'found_key' is
+ * TRUE then it means that the first database was writable and we
+ * either found a value that will replace it (value != NULL) or
+ * found a pending reset (value == NULL) that will unset it.
+ *
+ * We only actually do this step if we have a writable first
+ * source and no locks found, otherwise we just let step 5 do all
+ * the checking.
+ *
+ * 5. check the remaining sources.
+ *
+ * We do this until we have value != NULL. Even if found_key was
+ * TRUE, the reset that was requested will not have affected the
+ * lower-level databases.
+ */
+
+ /* Step 1. Check for locks.
+ *
+ * Note: i > 0 (strictly). Ignore locks for source #0.
+ */
+ if (~flags & DCONF_READ_USER_VALUE)
+ for (i = engine->n_sources - 1; i > 0; i--)
+ if (engine->sources[i]->locks && gvdb_table_has_value (engine->sources[i]->locks, key))
+ {
+ lock_level = i;
+ break;
+ }
+
+ /* Only do steps 2 to 4 if we have no locks and we have a writable source. */
+ if (!lock_level && engine->n_sources != 0 && engine->sources[0]->writable)
+ {
+ gboolean found_key = FALSE;
+
+ /* If the user has requested the default value only, then ensure
+ * that we "find" a NULL value here. This is equivalent to the
+ * user having reset the key, which is the definition of this
+ * flag.
+ */
+ if (flags & DCONF_READ_DEFAULT_VALUE)
+ found_key = TRUE;
+
+ /* Step 2. Check read_through. */
+ if (!found_key && read_through)
+ found_key = dconf_engine_find_key_in_queue (read_through, key, &value);
+
+ /* Step 3. Check queued changes if we didn't find it in read_through.
+ *
+ * NB: We may want to optimise this to avoid taking the lock in
+ * the case that we know both queues are empty.
+ */
+ if (!found_key)
+ {
+ dconf_engine_lock_queue (engine);
+
+ /* Check the pending first because those were submitted
+ * more recently.
+ */
+ if (engine->pending != NULL)
+ found_key = dconf_changeset_get (engine->pending, key, &value);
+
+ if (!found_key && engine->in_flight != NULL)
+ found_key = dconf_changeset_get (engine->in_flight, key, &value);
+
+ dconf_engine_unlock_queue (engine);
+ }
+
+ /* Step 4. Check the first source. */
+ if (!found_key && engine->sources[0]->values)
+ value = gvdb_table_get_value (engine->sources[0]->values, key);
+
+ /* We already checked source #0 (or ignored it, as appropriate).
+ *
+ * Abuse the lock_level variable to get step 5 to skip this one.
+ */
+ lock_level = 1;
+ }
+
+ /* Step 5. Check the remaining sources, until value != NULL. */
+ if (~flags & DCONF_READ_USER_VALUE)
+ for (i = lock_level; value == NULL && i < engine->n_sources; i++)
+ {
+ if (engine->sources[i]->values == NULL)
+ continue;
+
+ if ((value = gvdb_table_get_value (engine->sources[i]->values, key)))
+ break;
+ }
+
+ dconf_engine_release_sources (engine);
+
+ return value;
+}
+
+gchar **
+dconf_engine_list (DConfEngine *engine,
+ const gchar *dir,
+ gint *length)
+{
+ GHashTable *results;
+ GHashTableIter iter;
+ gchar **list;
+ gint n_items;
+ gpointer key;
+ gint i;
+
+ /* This function is unreliable in the presence of pending changes.
+ * Here's why:
+ *
+ * Consider the case that we list("/a/") and a pending request has a
+ * reset request recorded for "/a/b/c". The question of if "b/"
+ * should appear in the output rests on if "/a/b/d" also exists.
+ *
+ * Put another way: If "/a/b/c" is the only key in "/a/b/" then
+ * resetting it would mean that "/a/b/" stops existing (and we should
+ * not include it in the output). If there are others keys then it
+ * will continue to exist and we should include it.
+ *
+ * Instead of trying to sort this out, we just ignore the pending
+ * requests and report what the on-disk file says.
+ */
+
+ results = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+ dconf_engine_acquire_sources (engine);
+
+ for (i = 0; i < engine->n_sources; i++)
+ {
+ gchar **partial_list;
+ gint j;
+
+ if (engine->sources[i]->values == NULL)
+ continue;
+
+ partial_list = gvdb_table_list (engine->sources[i]->values, dir);
+
+ if (partial_list != NULL)
+ {
+ for (j = 0; partial_list[j]; j++)
+ /* Steal the keys from the list. */
+ g_hash_table_add (results, partial_list[j]);
+
+ /* Free only the list. */
+ g_free (partial_list);
+ }
+ }
+
+ dconf_engine_release_sources (engine);
+
+ n_items = g_hash_table_size (results);
+ list = g_new (gchar *, n_items + 1);
+
+ i = 0;
+ g_hash_table_iter_init (&iter, results);
+ while (g_hash_table_iter_next (&iter, &key, NULL))
+ {
+ g_hash_table_iter_steal (&iter);
+ list[i++] = key;
+ }
+ list[i] = NULL;
+ g_assert_cmpint (i, ==, n_items);
+
+ if (length)
+ *length = n_items;
+
+ g_hash_table_unref (results);
+
+ return list;
+}
+
+typedef void (* DConfEngineCallHandleCallback) (DConfEngine *engine,
+ gpointer handle,
+ GVariant *parameter,
+ const GError *error);
+
+struct _DConfEngineCallHandle
+{
+ DConfEngine *engine;
+ DConfEngineCallHandleCallback callback;
+ const GVariantType *expected_reply;
+};
+
+static gpointer
+dconf_engine_call_handle_new (DConfEngine *engine,
+ DConfEngineCallHandleCallback callback,
+ const GVariantType *expected_reply,
+ gsize size)
+{
+ DConfEngineCallHandle *handle;
+
+ g_assert (engine != NULL);
+ g_assert (callback != NULL);
+ g_assert (size >= sizeof (DConfEngineCallHandle));
+
+ handle = g_malloc0 (size);
+ handle->engine = dconf_engine_ref (engine);
+ handle->callback = callback;
+ handle->expected_reply = expected_reply;
+
+ return handle;
+}
+
+const GVariantType *
+dconf_engine_call_handle_get_expected_type (DConfEngineCallHandle *handle)
+{
+ if (handle)
+ return handle->expected_reply;
+ else
+ return NULL;
+}
+
+void
+dconf_engine_call_handle_reply (DConfEngineCallHandle *handle,
+ GVariant *parameter,
+ const GError *error)
+{
+ if (handle == NULL)
+ return;
+
+ (* handle->callback) (handle->engine, handle, parameter, error);
+}
+
+static void
+dconf_engine_call_handle_free (DConfEngineCallHandle *handle)
+{
+ dconf_engine_unref (handle->engine);
+ g_free (handle);
+}
+
+/* returns floating */
+static GVariant *
+dconf_engine_make_match_rule (DConfEngineSource *source,
+ const gchar *path)
+{
+ GVariant *params;
+ gchar *rule;
+
+ rule = g_strdup_printf ("type='signal',"
+ "interface='ca.desrt.dconf.Writer',"
+ "path='%s',"
+ "arg0path='%s'",
+ source->object_path,
+ path);
+
+ params = g_variant_new ("(s)", rule);
+
+ g_free (rule);
+
+ return params;
+}
+
+typedef struct
+{
+ DConfEngineCallHandle handle;
+
+ guint64 state;
+ gint pending;
+ gchar *path;
+} OutstandingWatch;
+
+static void
+dconf_engine_watch_established (DConfEngine *engine,
+ gpointer handle,
+ GVariant *reply,
+ const GError *error)
+{
+ OutstandingWatch *ow = handle;
+
+ /* ignore errors */
+
+ if (--ow->pending)
+ /* more on the way... */
+ return;
+
+ if (ow->state != dconf_engine_get_state (engine))
+ {
+ const gchar * const changes[] = { "", NULL };
+
+ /* Our recorded state does not match the current state. Something
+ * must have changed while our watch requests were on the wire.
+ *
+ * We don't know what changed, so we can just say that potentially
+ * everything under the path being watched changed. This case is
+ * very rare, anyway...
+ */
+ g_debug ("SHM invalidated while establishing subscription to %s - signalling change", ow->path);
+ dconf_engine_change_notify (engine, ow->path, changes, NULL, FALSE, NULL, engine->user_data);
+ }
+
+ dconf_engine_lock_subscription_counts (engine);
+ guint num_establishing = dconf_engine_count_subscriptions (engine->establishing,
+ ow->path);
+ g_debug ("watch_established: \"%s\" (establishing: %d)", ow->path, num_establishing);
+ if (num_establishing > 0)
+ // Subscription(s): establishing -> active
+ dconf_engine_move_subscriptions (engine->establishing,
+ engine->active,
+ ow->path);
+
+ dconf_engine_unlock_subscription_counts (engine);
+ g_clear_pointer (&ow->path, g_free);
+ dconf_engine_call_handle_free (handle);
+}
+
+void
+dconf_engine_watch_fast (DConfEngine *engine,
+ const gchar *path)
+{
+ dconf_engine_lock_subscription_counts (engine);
+ guint num_establishing = dconf_engine_count_subscriptions (engine->establishing, path);
+ guint num_active = dconf_engine_count_subscriptions (engine->active, path);
+ g_debug ("watch_fast: \"%s\" (establishing: %d, active: %d)", path, num_establishing, num_active);
+ if (num_active > 0)
+ // Subscription: inactive -> active
+ dconf_engine_inc_subscriptions (engine->active, path);
+ else
+ // Subscription: inactive -> establishing
+ num_establishing = dconf_engine_inc_subscriptions (engine->establishing,
+ path);
+ dconf_engine_unlock_subscription_counts (engine);
+ if (num_establishing > 1 || num_active > 0)
+ return;
+
+ OutstandingWatch *ow;
+ gint i;
+
+ if (engine->n_sources == 0)
+ return;
+
+ /* It's possible (although rare) that the dconf database could change
+ * while our match rule is on the wire.
+ *
+ * Since we returned immediately (suggesting to the user that the
+ * watch was already established) we could have a race.
+ *
+ * To deal with this, we use the current state counter to ensure that nothing
+ * changes while the watch requests are on the wire.
+ */
+ ow = dconf_engine_call_handle_new (engine, dconf_engine_watch_established,
+ G_VARIANT_TYPE_UNIT, sizeof (OutstandingWatch));
+ ow->state = dconf_engine_get_state (engine);
+ ow->path = g_strdup (path);
+
+ /* We start getting async calls returned as soon as we start dispatching them,
+ * so we must not touch the 'ow' struct after we send the first one.
+ */
+ for (i = 0; i < engine->n_sources; i++)
+ if (engine->sources[i]->bus_type)
+ ow->pending++;
+
+ for (i = 0; i < engine->n_sources; i++)
+ if (engine->sources[i]->bus_type)
+ dconf_engine_dbus_call_async_func (engine->sources[i]->bus_type, "org.freedesktop.DBus",
+ "/org/freedesktop/DBus", "org.freedesktop.DBus", "AddMatch",
+ dconf_engine_make_match_rule (engine->sources[i], path),
+ &ow->handle, NULL);
+}
+
+void
+dconf_engine_unwatch_fast (DConfEngine *engine,
+ const gchar *path)
+{
+ dconf_engine_lock_subscription_counts (engine);
+ guint num_active = dconf_engine_count_subscriptions (engine->active, path);
+ guint num_establishing = dconf_engine_count_subscriptions (engine->establishing, path);
+ gint i;
+ g_debug ("unwatch_fast: \"%s\" (active: %d, establishing: %d)", path, num_active, num_establishing);
+
+ // Client code cannot unsubscribe if it is not subscribed
+ g_assert (num_active > 0 || num_establishing > 0);
+ if (num_active == 0)
+ // Subscription: establishing -> inactive
+ num_establishing = dconf_engine_dec_subscriptions (engine->establishing, path);
+ else
+ // Subscription: active -> inactive
+ num_active = dconf_engine_dec_subscriptions (engine->active, path);
+
+ dconf_engine_unlock_subscription_counts (engine);
+ if (num_active > 0 || num_establishing > 0)
+ return;
+
+ for (i = 0; i < engine->n_sources; i++)
+ if (engine->sources[i]->bus_type)
+ dconf_engine_dbus_call_async_func (engine->sources[i]->bus_type, "org.freedesktop.DBus",
+ "/org/freedesktop/DBus", "org.freedesktop.DBus", "RemoveMatch",
+ dconf_engine_make_match_rule (engine->sources[i], path), NULL, NULL);
+}
+
+static void
+dconf_engine_handle_match_rule_sync (DConfEngine *engine,
+ const gchar *method_name,
+ const gchar *path)
+{
+ gint i;
+
+ /* We need not hold any locks here because we are only touching static
+ * things: the number of sources, and static properties of each source
+ * itself.
+ *
+ * This function silently ignores all errors.
+ */
+
+ for (i = 0; i < engine->n_sources; i++)
+ {
+ GVariant *result;
+
+ if (!engine->sources[i]->bus_type)
+ continue;
+
+ result = dconf_engine_dbus_call_sync_func (engine->sources[i]->bus_type, "org.freedesktop.DBus",
+ "/org/freedesktop/DBus", "org.freedesktop.DBus", method_name,
+ dconf_engine_make_match_rule (engine->sources[i], path),
+ G_VARIANT_TYPE_UNIT, NULL);
+
+ if (result)
+ g_variant_unref (result);
+ }
+}
+
+void
+dconf_engine_watch_sync (DConfEngine *engine,
+ const gchar *path)
+{
+ dconf_engine_lock_subscription_counts (engine);
+ guint num_active = dconf_engine_inc_subscriptions (engine->active, path);
+ dconf_engine_unlock_subscription_counts (engine);
+ g_debug ("watch_sync: \"%s\" (active: %d)", path, num_active - 1);
+ if (num_active == 1)
+ dconf_engine_handle_match_rule_sync (engine, "AddMatch", path);
+}
+
+void
+dconf_engine_unwatch_sync (DConfEngine *engine,
+ const gchar *path)
+{
+ dconf_engine_lock_subscription_counts (engine);
+ guint num_active = dconf_engine_dec_subscriptions (engine->active, path);
+ dconf_engine_unlock_subscription_counts (engine);
+ g_debug ("unwatch_sync: \"%s\" (active: %d)", path, num_active + 1);
+ if (num_active == 0)
+ dconf_engine_handle_match_rule_sync (engine, "RemoveMatch", path);
+}
+
+typedef struct
+{
+ DConfEngineCallHandle handle;
+
+ DConfChangeset *change;
+} OutstandingChange;
+
+static GVariant *
+dconf_engine_prepare_change (DConfEngine *engine,
+ DConfChangeset *change)
+{
+ GVariant *serialised;
+
+ serialised = dconf_changeset_serialise (change);
+
+ return g_variant_new_from_data (G_VARIANT_TYPE ("(ay)"),
+ g_variant_get_data (serialised), g_variant_get_size (serialised), TRUE,
+ (GDestroyNotify) g_variant_unref, g_variant_ref_sink (serialised));
+}
+
+/* This function promotes the pending changeset to become the in-flight
+ * changeset by sending the appropriate D-Bus message.
+ *
+ * Of course, this is only possible when there is a pending changeset
+ * and no changeset is in-flight already. For this reason, this function
+ * gets called in two situations:
+ *
+ * - when there is a new pending changeset (due to an API call)
+ *
+ * - when in-flight changeset had been delivered (due to a D-Bus
+ * reply having been received)
+ */
+static void dconf_engine_manage_queue (DConfEngine *engine);
+
+static void
+dconf_engine_emit_changes (DConfEngine *engine,
+ DConfChangeset *changeset,
+ gpointer origin_tag)
+{
+ const gchar *prefix;
+ const gchar * const *changes;
+
+ if (dconf_changeset_describe (changeset, &prefix, &changes, NULL))
+ dconf_engine_change_notify (engine, prefix, changes, NULL, FALSE, origin_tag, engine->user_data);
+}
+
+static void
+dconf_engine_change_completed (DConfEngine *engine,
+ gpointer handle,
+ GVariant *reply,
+ const GError *error)
+{
+ OutstandingChange *oc = handle;
+ DConfChangeset *expected;
+
+ dconf_engine_lock_queue (engine);
+
+ expected = g_steal_pointer (&engine->in_flight);
+ g_assert (expected && oc->change == expected);
+
+ /* Another request could be sent now. Check for pending changes. */
+ dconf_engine_manage_queue (engine);
+ dconf_engine_unlock_queue (engine);
+
+ /* Deal with the reply we got. */
+ if (reply)
+ {
+ /* The write worked.
+ *
+ * We already sent a change notification for this item when we
+ * added it to the pending queue and we don't want to send another
+ * one again. At the same time, it's very likely that we're just
+ * about to receive a change signal from the service.
+ *
+ * The tag sent as part of the reply to the Change call will be
+ * the same tag as on the change notification signal. Record that
+ * tag so that we can ignore the signal when it comes.
+ *
+ * last_handled is only ever touched from the worker thread
+ */
+ g_free (engine->last_handled);
+ g_variant_get (reply, "(s)", &engine->last_handled);
+ }
+
+ if (error)
+ {
+ /* Some kind of unexpected failure occurred while attempting to
+ * commit the change.
+ *
+ * There's not much we can do here except to drop our local copy
+ * of the change (and notify that it is gone) and print the error
+ * message as a warning.
+ */
+ g_warning ("failed to commit changes to dconf: %s", error->message);
+ dconf_engine_emit_changes (engine, oc->change, NULL);
+ }
+
+ dconf_changeset_unref (oc->change);
+ dconf_engine_call_handle_free (handle);
+}
+
+static void
+dconf_engine_manage_queue (DConfEngine *engine)
+{
+ if (engine->pending != NULL && engine->in_flight == NULL)
+ {
+ OutstandingChange *oc;
+ GVariant *parameters;
+
+ oc = dconf_engine_call_handle_new (engine, dconf_engine_change_completed,
+ G_VARIANT_TYPE ("(s)"), sizeof (OutstandingChange));
+
+ oc->change = engine->in_flight = g_steal_pointer (&engine->pending);
+
+ parameters = dconf_engine_prepare_change (engine, oc->change);
+
+ dconf_engine_dbus_call_async_func (engine->sources[0]->bus_type,
+ engine->sources[0]->bus_name,
+ engine->sources[0]->object_path,
+ "ca.desrt.dconf.Writer", "Change",
+ parameters, &oc->handle, NULL);
+ }
+
+ if (engine->in_flight == NULL)
+ {
+ /* The in-flight queue should not be empty if we have changes
+ * pending...
+ */
+ g_assert (engine->pending == NULL);
+
+ g_cond_broadcast (&engine->queue_cond);
+ }
+}
+
+static gboolean
+dconf_engine_is_writable_changeset_predicate (const gchar *key,
+ GVariant *value,
+ gpointer user_data)
+{
+ DConfEngine *engine = user_data;
+
+ /* Resets absolutely always succeed -- even in the case that there is
+ * not even a writable database.
+ */
+ return value == NULL || dconf_engine_is_writable_internal (engine, key);
+}
+
+static gboolean
+dconf_engine_changeset_changes_only_writable_keys (DConfEngine *engine,
+ DConfChangeset *changeset,
+ GError **error)
+{
+ gboolean success = TRUE;
+
+ dconf_engine_acquire_sources (engine);
+
+ if (!dconf_changeset_all (changeset, dconf_engine_is_writable_changeset_predicate, engine))
+ {
+ g_set_error_literal (error, DCONF_ERROR, DCONF_ERROR_NOT_WRITABLE,
+ "The operation attempted to modify one or more non-writable keys");
+ success = FALSE;
+ }
+
+ dconf_engine_release_sources (engine);
+
+ return success;
+}
+
+gboolean
+dconf_engine_change_fast (DConfEngine *engine,
+ DConfChangeset *changeset,
+ gpointer origin_tag,
+ GError **error)
+{
+ g_debug ("change_fast");
+ if (dconf_changeset_is_empty (changeset))
+ return TRUE;
+
+ if (!dconf_engine_changeset_changes_only_writable_keys (engine, changeset, error))
+ return FALSE;
+
+ dconf_changeset_seal (changeset);
+
+ dconf_engine_lock_queue (engine);
+
+ /* The pending changeset is kept unsealed so that it can be modified
+ * by later calls to this functions. It wouldn't be a good idea to
+ * repurpose the incoming changeset for this role, so create a new
+ * one if necessary. */
+ if (engine->pending == NULL)
+ engine->pending = dconf_changeset_new ();
+
+ dconf_changeset_change (engine->pending, changeset);
+
+ /* There might be no in-flight request yet, so we try to manage the
+ * queue right away in order to try to promote pending changes there
+ * (which causes the D-Bus message to actually be sent). */
+ dconf_engine_manage_queue (engine);
+
+ dconf_engine_unlock_queue (engine);
+
+ /* Emit the signal after dropping the lock to avoid deadlock on re-entry. */
+ dconf_engine_emit_changes (engine, changeset, origin_tag);
+
+ return TRUE;
+}
+
+gboolean
+dconf_engine_change_sync (DConfEngine *engine,
+ DConfChangeset *changeset,
+ gchar **tag,
+ GError **error)
+{
+ GVariant *reply;
+ g_debug ("change_sync");
+
+ if (dconf_changeset_is_empty (changeset))
+ {
+ if (tag)
+ *tag = g_strdup ("");
+
+ return TRUE;
+ }
+
+ if (!dconf_engine_changeset_changes_only_writable_keys (engine, changeset, error))
+ return FALSE;
+
+ dconf_changeset_seal (changeset);
+
+ /* we know that we have at least one source because we checked writability */
+ reply = dconf_engine_dbus_call_sync_func (engine->sources[0]->bus_type,
+ engine->sources[0]->bus_name,
+ engine->sources[0]->object_path,
+ "ca.desrt.dconf.Writer", "Change",
+ dconf_engine_prepare_change (engine, changeset),
+ G_VARIANT_TYPE ("(s)"), error);
+
+ if (reply == NULL)
+ return FALSE;
+
+ /* g_variant_get() is okay with NULL tag */
+ g_variant_get (reply, "(s)", tag);
+ g_variant_unref (reply);
+
+ return TRUE;
+}
+
+static gboolean
+dconf_engine_is_interested_in_signal (DConfEngine *engine,
+ GBusType bus_type,
+ const gchar *sender,
+ const gchar *path)
+{
+ gint i;
+
+ for (i = 0; i < engine->n_sources; i++)
+ {
+ DConfEngineSource *source = engine->sources[i];
+
+ if (source->bus_type == bus_type && g_str_equal (source->object_path, path))
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+void
+dconf_engine_handle_dbus_signal (GBusType type,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *member,
+ GVariant *body)
+{
+ if (g_str_equal (member, "Notify"))
+ {
+ const gchar *prefix;
+ const gchar **changes;
+ const gchar *tag;
+ GSList *engines;
+
+ if (!g_variant_is_of_type (body, G_VARIANT_TYPE ("(sass)")))
+ return;
+
+ g_variant_get (body, "(&s^a&s&s)", &prefix, &changes, &tag);
+
+ /* Reject junk */
+ if (changes[0] == NULL)
+ /* No changes? Do nothing. */
+ goto junk;
+
+ if (dconf_is_key (prefix, NULL))
+ {
+ /* If the prefix is a key then the changes must be ['']. */
+ if (changes[0][0] || changes[1])
+ goto junk;
+ }
+ else if (dconf_is_dir (prefix, NULL))
+ {
+ /* If the prefix is a dir then we can have changes within that
+ * dir, but they must be rel paths.
+ *
+ * ie:
+ *
+ * ('/a/', ['b', 'c/']) == ['/a/b', '/a/c/']
+ */
+ gint i;
+
+ for (i = 0; changes[i]; i++)
+ if (!dconf_is_rel_path (changes[i], NULL))
+ goto junk;
+ }
+ else
+ /* Not a key or a dir? */
+ goto junk;
+
+ g_mutex_lock (&dconf_engine_global_lock);
+ engines = g_slist_copy_deep (dconf_engine_global_list, (GCopyFunc) dconf_engine_ref, NULL);
+ g_mutex_unlock (&dconf_engine_global_lock);
+
+ while (engines)
+ {
+ DConfEngine *engine = engines->data;
+
+ /* It's possible that this incoming change notify is for a
+ * change that we already announced to the client when we
+ * placed it in the queue.
+ *
+ * Check last_handled to determine if we should ignore it.
+ */
+ if (!engine->last_handled || !g_str_equal (engine->last_handled, tag))
+ if (dconf_engine_is_interested_in_signal (engine, type, sender, object_path))
+ dconf_engine_change_notify (engine, prefix, changes, tag, FALSE, NULL, engine->user_data);
+
+ engines = g_slist_delete_link (engines, engines);
+
+ dconf_engine_unref (engine);
+ }
+
+junk:
+ g_free (changes);
+ }
+
+ else if (g_str_equal (member, "WritabilityNotify"))
+ {
+ const gchar *empty_str_list[] = { "", NULL };
+ const gchar *path;
+ GSList *engines;
+
+ if (!g_variant_is_of_type (body, G_VARIANT_TYPE ("(s)")))
+ return;
+
+ g_variant_get (body, "(&s)", &path);
+
+ /* Rejecting junk here is relatively straightforward */
+ if (!dconf_is_path (path, NULL))
+ return;
+
+ g_mutex_lock (&dconf_engine_global_lock);
+ engines = g_slist_copy_deep (dconf_engine_global_list, (GCopyFunc) dconf_engine_ref, NULL);
+ g_mutex_unlock (&dconf_engine_global_lock);
+
+ while (engines)
+ {
+ DConfEngine *engine = engines->data;
+
+ if (dconf_engine_is_interested_in_signal (engine, type, sender, object_path))
+ dconf_engine_change_notify (engine, path, empty_str_list, "", TRUE, NULL, engine->user_data);
+
+ engines = g_slist_delete_link (engines, engines);
+
+ dconf_engine_unref (engine);
+ }
+ }
+}
+
+gboolean
+dconf_engine_has_outstanding (DConfEngine *engine)
+{
+ gboolean has;
+
+ /* The in-flight will never be empty unless the pending is
+ * also empty, so we only really need to check one of them...
+ */
+ dconf_engine_lock_queue (engine);
+ has = engine->in_flight != NULL;
+ dconf_engine_unlock_queue (engine);
+
+ return has;
+}
+
+void
+dconf_engine_sync (DConfEngine *engine)
+{
+ g_debug ("sync");
+ dconf_engine_lock_queue (engine);
+ while (engine->in_flight != NULL)
+ g_cond_wait (&engine->queue_cond, &engine->queue_lock);
+ dconf_engine_unlock_queue (engine);
+}
diff --git a/engine/dconf-engine.h b/engine/dconf-engine.h
new file mode 100644
index 0000000..2485423
--- /dev/null
+++ b/engine/dconf-engine.h
@@ -0,0 +1,167 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __dconf_engine_h__
+#define __dconf_engine_h__
+
+#include "../common/dconf-changeset.h"
+#include "../common/dconf-enums.h"
+
+#include <gio/gio.h>
+
+typedef struct _DConfEngine DConfEngine;
+
+typedef struct _DConfEngineCallHandle DConfEngineCallHandle;
+
+/* These functions need to be implemented by the client library */
+G_GNUC_INTERNAL
+void dconf_engine_dbus_init_for_testing (void);
+
+/* Sends a D-Bus message.
+ *
+ * When the reply comes back, the client library should call
+ * dconf_engine_handle_dbus_reply with the given user_data.
+ *
+ * This is called with the engine lock held. Re-entering the engine
+ * from this function will cause a deadlock.
+ */
+G_GNUC_INTERNAL
+gboolean dconf_engine_dbus_call_async_func (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ DConfEngineCallHandle *handle,
+ GError **error);
+
+/* Sends a D-Bus message, synchronously.
+ *
+ * The lock is never held when calling this function (for the sake of
+ * not blocking requests in other threads) but you should have no reason
+ * to re-enter, so don't.
+ */
+G_GNUC_INTERNAL
+GVariant * dconf_engine_dbus_call_sync_func (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ const GVariantType *expected_type,
+ GError **error);
+
+/* Notifies that a change occured.
+ *
+ * The engine lock is never held when calling this function so it is
+ * safe to run user callbacks or emit signals from this function.
+ */
+G_GNUC_INTERNAL
+void dconf_engine_change_notify (DConfEngine *engine,
+ const gchar *prefix,
+ const gchar * const *changes,
+ const gchar *tag,
+ gboolean is_writability,
+ gpointer origin_tag,
+ gpointer user_data);
+
+/* These functions are implemented by the engine */
+G_GNUC_INTERNAL
+const GVariantType * dconf_engine_call_handle_get_expected_type (DConfEngineCallHandle *handle);
+G_GNUC_INTERNAL
+void dconf_engine_call_handle_reply (DConfEngineCallHandle *handle,
+ GVariant *parameters,
+ const GError *error);
+
+G_GNUC_INTERNAL
+void dconf_engine_handle_dbus_signal (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *signal_name,
+ GVariant *parameters);
+
+G_GNUC_INTERNAL
+DConfEngine * dconf_engine_new (const gchar *profile,
+ gpointer user_data,
+ GDestroyNotify free_func);
+
+G_GNUC_INTERNAL
+void dconf_engine_unref (DConfEngine *engine);
+
+/* Read API: always handled immediately */
+G_GNUC_INTERNAL
+guint64 dconf_engine_get_state (DConfEngine *engine);
+
+G_GNUC_INTERNAL
+gboolean dconf_engine_is_writable (DConfEngine *engine,
+ const gchar *key);
+
+G_GNUC_INTERNAL
+gchar ** dconf_engine_list_locks (DConfEngine *engine,
+ const gchar *path,
+ gint *length);
+
+G_GNUC_INTERNAL
+GVariant * dconf_engine_read (DConfEngine *engine,
+ DConfReadFlags flags,
+ const GQueue *read_through,
+ const gchar *key);
+
+G_GNUC_INTERNAL
+gchar ** dconf_engine_list (DConfEngine *engine,
+ const gchar *dir,
+ gint *length);
+
+/* "Fast" API: all calls return immediately and look like they succeeded (from a local viewpoint) */
+G_GNUC_INTERNAL
+void dconf_engine_watch_fast (DConfEngine *engine,
+ const gchar *path);
+
+G_GNUC_INTERNAL
+void dconf_engine_unwatch_fast (DConfEngine *engine,
+ const gchar *path);
+
+G_GNUC_INTERNAL
+gboolean dconf_engine_change_fast (DConfEngine *engine,
+ DConfChangeset *changeset,
+ gpointer origin_tag,
+ GError **error);
+
+/* Synchronous API: all calls block until completed */
+G_GNUC_INTERNAL
+void dconf_engine_watch_sync (DConfEngine *engine,
+ const gchar *path);
+
+G_GNUC_INTERNAL
+void dconf_engine_unwatch_sync (DConfEngine *engine,
+ const gchar *path);
+
+G_GNUC_INTERNAL
+gboolean dconf_engine_change_sync (DConfEngine *engine,
+ DConfChangeset *changeset,
+ gchar **tag,
+ GError **error);
+G_GNUC_INTERNAL
+gboolean dconf_engine_has_outstanding (DConfEngine *engine);
+G_GNUC_INTERNAL
+void dconf_engine_sync (DConfEngine *engine);
+
+/* Asynchronous API: not implemented yet (and maybe never?) */
+
+#endif /* __dconf_engine_h__ */
diff --git a/engine/meson.build b/engine/meson.build
new file mode 100644
index 0000000..ca46b60
--- /dev/null
+++ b/engine/meson.build
@@ -0,0 +1,46 @@
+testable_sources = files(
+ 'dconf-engine.c',
+ 'dconf-engine-profile.c',
+ 'dconf-engine-source.c',
+ 'dconf-engine-source-file.c',
+ 'dconf-engine-source-user.c',
+ 'dconf-engine-source-service.c',
+ 'dconf-engine-source-system.c',
+)
+
+sources = testable_sources + files(
+ 'dconf-engine-mockable.c',
+)
+
+engine_deps = [
+ libdconf_common_dep,
+ libgvdb_dep,
+]
+
+libdconf_engine = static_library(
+ 'dconf-engine',
+ sources: sources,
+ include_directories: top_inc,
+ dependencies: engine_deps + [libdconf_shm_dep],
+ c_args: dconf_c_args,
+ pic: true,
+)
+
+libdconf_engine_dep = declare_dependency(
+ dependencies: engine_deps,
+ link_with: libdconf_engine,
+)
+
+libdconf_engine_test = static_library(
+ 'dconf-engine-test',
+ sources: testable_sources,
+ include_directories: top_inc,
+ dependencies: engine_deps + [libdconf_shm_dep],
+ c_args: dconf_c_args,
+ pic: true,
+)
+
+libdconf_engine_test_dep = declare_dependency(
+ dependencies: engine_deps,
+ link_with: libdconf_engine_test,
+)
diff --git a/gdbus/dconf-gdbus-filter.c b/gdbus/dconf-gdbus-filter.c
new file mode 100644
index 0000000..79b2dd7
--- /dev/null
+++ b/gdbus/dconf-gdbus-filter.c
@@ -0,0 +1,310 @@
+#include "../engine/dconf-engine.h"
+
+
+
+
+typedef struct
+{
+ gpointer data; /* either GDBusConnection or GError */
+ guint is_error;
+ guint waiting_for_serial;
+ GQueue queue;
+} ConnectionState;
+
+typedef struct
+{
+ guint32 serial;
+ DConfEngineCallHandle *handle;
+} DConfGDBusCall;
+
+static ConnectionState connections[3];
+static GMutex dconf_gdbus_lock;
+
+static GBusType
+connection_state_get_bus_type (ConnectionState *state)
+{
+ return state - connections;
+}
+
+static gboolean
+connection_state_ensure_success (ConnectionState *state,
+ GError **error)
+{
+ if (state->is_error)
+ {
+ if (error)
+ *error = g_error_copy (state->data);
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static GDBusConnection *
+connection_state_get_connection (ConnectionState *state)
+{
+ g_assert (!state->is_error);
+
+ return state->data;
+}
+
+/* This function can be slow (as compared to the one below). */
+static void
+dconf_gdbus_handle_reply (ConnectionState *state,
+ GDBusMessage *message)
+{
+ DConfEngineCallHandle *handle;
+ GError *error = NULL;
+ GVariant *body;
+
+ g_mutex_lock (&dconf_gdbus_lock);
+ {
+ DConfGDBusCall *call;
+
+ call = g_queue_pop_head (&state->queue);
+ g_assert_cmpuint (g_dbus_message_get_reply_serial (message), ==, call->serial);
+ handle = call->handle;
+
+ g_slice_free (DConfGDBusCall, call);
+
+ call = g_queue_peek_head (&state->queue);
+ if (call)
+ g_atomic_int_set (&state->waiting_for_serial, call->serial);
+ else
+ g_atomic_int_set (&state->waiting_for_serial, -1);
+ }
+ g_mutex_unlock (&dconf_gdbus_lock);
+
+ body = g_dbus_message_get_body (message);
+
+ if (g_dbus_message_get_message_type (message) == G_DBUS_MESSAGE_TYPE_ERROR)
+ {
+ const GVariantType *first_child_type;
+ const gchar *error_message = NULL;
+
+ first_child_type = g_variant_type_first (g_variant_get_type (body));
+
+ if (g_variant_type_equal (first_child_type, G_VARIANT_TYPE_STRING))
+ g_variant_get_child (body, 0, "&s", &error_message);
+
+ error = g_dbus_error_new_for_dbus_error (g_dbus_message_get_error_name (message), error_message);
+ body = NULL;
+ }
+
+ dconf_engine_call_handle_reply (handle, body, error);
+
+ if (error)
+ g_error_free (error);
+}
+
+/* We optimise for this function being super-efficient since it gets run
+ * on every single D-Bus message in or out.
+ *
+ * We want to bail out as quickly as possible in the case that this
+ * message does not interest us. That means we should not hold locks or
+ * anything like that.
+ *
+ * In the case that this message _does_ interest us (which should be
+ * rare) we can take a lot more time.
+ */
+static GDBusMessage *
+dconf_gdbus_filter_function (GDBusConnection *connection,
+ GDBusMessage *message,
+ gboolean incoming,
+ gpointer user_data)
+{
+ ConnectionState *state = user_data;
+
+ if (incoming)
+ {
+ switch (g_dbus_message_get_message_type (message))
+ {
+ case G_DBUS_MESSAGE_TYPE_SIGNAL:
+ {
+ const gchar *interface;
+
+ interface = g_dbus_message_get_interface (message);
+ if (interface && g_str_equal (interface, "ca.desrt.dconf.Writer"))
+ dconf_engine_handle_dbus_signal (connection_state_get_bus_type (state),
+ g_dbus_message_get_sender (message),
+ g_dbus_message_get_path (message),
+ g_dbus_message_get_member (message),
+ g_dbus_message_get_body (message));
+
+ /* Others could theoretically be interested in this... */
+ }
+ break;
+
+ case G_DBUS_MESSAGE_TYPE_METHOD_RETURN:
+ case G_DBUS_MESSAGE_TYPE_ERROR:
+ if G_UNLIKELY (g_dbus_message_get_reply_serial (message) == g_atomic_int_get (&state->waiting_for_serial))
+ {
+ /* This is definitely for us. */
+ dconf_gdbus_handle_reply (state, message);
+
+ /* Nobody else should be interested in it. */
+ g_clear_object (&message);
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ return message;
+}
+
+static ConnectionState *
+dconf_gdbus_get_connection_state (GBusType bus_type,
+ GError **error)
+{
+ ConnectionState *state;
+
+ g_assert (bus_type < G_N_ELEMENTS (connections));
+
+ state = &connections[bus_type];
+
+ if (g_once_init_enter (&state->data))
+ {
+ GDBusConnection *connection;
+ GError *error = NULL;
+ gpointer result;
+
+ /* This will only block the first time...
+ *
+ * Optimising this away is probably not worth the effort.
+ */
+ connection = g_bus_get_sync (bus_type, NULL, &error);
+
+ if (connection)
+ {
+ g_dbus_connection_add_filter (connection, dconf_gdbus_filter_function, state, NULL);
+ result = connection;
+ state->is_error = FALSE;
+ }
+ else
+ {
+ result = error;
+ state->is_error = TRUE;
+ }
+
+ g_once_init_leave (&state->data, result);
+ }
+
+ if (!connection_state_ensure_success (state, error))
+ return FALSE;
+
+ return state;
+}
+
+gboolean
+dconf_engine_dbus_call_async_func (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ DConfEngineCallHandle *handle,
+ GError **error)
+{
+ ConnectionState *state;
+ GDBusMessage *message;
+ DConfGDBusCall *call;
+ gboolean success;
+
+ state = dconf_gdbus_get_connection_state (bus_type, error);
+
+ if (state == NULL)
+ {
+ g_variant_unref (g_variant_ref_sink (parameters));
+ return FALSE;
+ }
+
+ message = g_dbus_message_new_method_call (bus_name, object_path, interface_name, method_name);
+ g_dbus_message_set_body (message, parameters);
+
+ g_mutex_lock (&dconf_gdbus_lock);
+ {
+ volatile guint *serial_ptr;
+ guint my_serial;
+
+ /* We need to set the serial in call->serial. Sometimes we also
+ * need to set it in state->waiting_for_serial (in the case that no
+ * other items are queued yet).
+ *
+ * g_dbus_connection_send_message() only has one out_serial parameter
+ * so we can only set one of them atomically. If needed, we elect
+ * to set the waiting_for_serial because that is the one that is
+ * accessed from the filter function without holding the lock.
+ *
+ * The serial number in the call structure is only accessed after the
+ * lock is acquired which allows us to take our time setting it (for
+ * as long as we're still holding the lock).
+ *
+ * In the case that waiting_for_serial should not be set we just use
+ * a local variable and use that to fill call->serial.
+ *
+ * Also: the queue itself isn't accessed until after the lock is
+ * taken, so we can delay adding the call to the queue until we know
+ * that the sending of the message was successful.
+ */
+
+ if (g_queue_is_empty (&state->queue))
+ serial_ptr = &state->waiting_for_serial;
+ else
+ serial_ptr = &my_serial;
+
+ success = g_dbus_connection_send_message (connection_state_get_connection (state), message,
+ G_DBUS_SEND_MESSAGE_FLAGS_NONE, serial_ptr, error);
+
+ if (success)
+ {
+ call = g_slice_new (DConfGDBusCall);
+
+ call->handle = handle;
+ call->serial = *serial_ptr;
+
+ g_queue_push_tail (&state->queue, call);
+ }
+ }
+ g_mutex_unlock (&dconf_gdbus_lock);
+
+ g_object_unref (message);
+
+ return success;
+}
+
+GVariant *
+dconf_engine_dbus_call_sync_func (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ const GVariantType *reply_type,
+ GError **error)
+{
+ ConnectionState *state;
+
+ state = dconf_gdbus_get_connection_state (bus_type, error);
+
+ if (state == NULL)
+ {
+ g_variant_unref (g_variant_ref_sink (parameters));
+
+ return NULL;
+ }
+
+ return g_dbus_connection_call_sync (connection_state_get_connection (state),
+ bus_name, object_path, interface_name, method_name, parameters, reply_type,
+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, error);
+}
+
+#ifndef PIC
+void
+dconf_engine_dbus_init_for_testing (void)
+{
+}
+#endif
diff --git a/gdbus/dconf-gdbus-thread.c b/gdbus/dconf-gdbus-thread.c
new file mode 100644
index 0000000..8b8f048
--- /dev/null
+++ b/gdbus/dconf-gdbus-thread.c
@@ -0,0 +1,385 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "../engine/dconf-engine.h"
+
+/* We interact with GDBus using a worker thread just for dconf.
+ *
+ * We want to have a worker thread that's not the main thread for one
+ * main reason: we don't want to have all of our incoming signals and
+ * method call replies being delivered via the default main context
+ * (which may blocked or simply not running at all).
+ *
+ * The only question is if we should have our own thread or share the
+ * GDBus worker thread. This file takes the approach that we should
+ * have our own thread. See "dconf-gdbus-filter.c" for an approach that
+ * shares the worker thread with GDBus.
+ *
+ * We gain at least one advantage here that we cannot gain any other way
+ * (including sharing a worker thread with GDBus): fast startup.
+ *
+ * The first thing that happens when GSettings comes online is a D-Bus
+ * call to establish a watch. We have to bring up the GDBusConnection.
+ * There are two ways to do that: sync and async.
+ *
+ * We can't do either of those in GDBus's worker thread (since it
+ * doesn't exist yet). We can't do async in the main thread because the
+ * user may not be running the mainloop (as is the case for the
+ * commandline tool, for example).
+ *
+ * That leaves only one option: synchronous initialisation in the main
+ * thread. That's what the "dconf-gdbus-filter" variant of this code
+ * does, and it's slower because of it.
+ *
+ * If we have our own worker thread then we can punt synchronous
+ * initialisation of the bus to it and return immediately.
+ *
+ * We also gain the advantage that the dconf worker thread and the GDBus
+ * worker thread can both be doing work at the same time. This
+ * advantage is probably quite marginal (and is likely outweighed by the
+ * cost of all the punting around of messages between threads).
+ */
+
+typedef struct
+{
+ GBusType bus_type;
+ const gchar *bus_name;
+ const gchar *object_path;
+ const gchar *interface_name;
+ const gchar *method_name;
+ GVariant *parameters;
+ const GVariantType *expected_type;
+ DConfEngineCallHandle *handle;
+} DConfGDBusCall;
+
+static gpointer
+dconf_gdbus_worker_thread (gpointer user_data)
+{
+ GMainContext *context = user_data;
+
+ g_main_context_push_thread_default (context);
+
+ for (;;)
+ g_main_context_iteration (context, TRUE);
+
+ /* srsly, gcc? */
+ return NULL;
+}
+
+static GMainContext *
+dconf_gdbus_get_worker_context (void)
+{
+ static GMainContext *worker_context;
+
+ if (g_once_init_enter (&worker_context))
+ {
+ GMainContext *context;
+
+ /* Work around https://bugzilla.gnome.org/show_bug.cgi?id=674885
+ *
+ * This set of types is the same as the set in
+ * glib/gio/gdbusprivate.c:ensure_required_types(). That workaround
+ * is ineffective for us since we're already in the worker thread when
+ * we call g_bus_get_sync() and ensure_required_types() runs. So we do
+ * something similar here before launching the worker thread. Calling
+ * g_bus_get_sync() here would also be possible, but potentially would
+ * cause significant startup latency for every dconf user.
+ */
+ g_type_ensure (G_TYPE_TASK);
+ g_type_ensure (G_TYPE_MEMORY_INPUT_STREAM);
+ g_type_ensure (G_TYPE_DBUS_CONNECTION_FLAGS);
+ g_type_ensure (G_TYPE_DBUS_CAPABILITY_FLAGS);
+ g_type_ensure (G_TYPE_DBUS_AUTH_OBSERVER);
+ g_type_ensure (G_TYPE_DBUS_CONNECTION);
+ g_type_ensure (G_TYPE_DBUS_PROXY);
+ g_type_ensure (G_TYPE_SOCKET_FAMILY);
+ g_type_ensure (G_TYPE_SOCKET_TYPE);
+ g_type_ensure (G_TYPE_SOCKET_PROTOCOL);
+ g_type_ensure (G_TYPE_SOCKET_ADDRESS);
+ g_type_ensure (G_TYPE_SOCKET);
+
+ context = g_main_context_new ();
+ g_thread_new ("dconf worker", dconf_gdbus_worker_thread, context);
+ g_once_init_leave (&worker_context, context);
+ }
+
+ return worker_context;
+}
+
+static void
+dconf_gdbus_signal_handler (GDBusConnection *connection,
+ const gchar *sender_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *signal_name,
+ GVariant *parameters,
+ gpointer user_data)
+{
+ GBusType bus_type = GPOINTER_TO_INT (user_data);
+
+ dconf_engine_handle_dbus_signal (bus_type, sender_name, object_path, signal_name, parameters);
+}
+
+/* The code to create and initialise the GDBusConnection for a
+ * particular bus type is more complicated than it should be.
+ *
+ * The complication comes from the fact that we must call
+ * g_dbus_connection_signal_subscribe() from the thread in which the
+ * signal handler will run (which in our case is the worker thread).
+ * g_main_context_push_thread_default() attempts to acquire the context,
+ * preventing us from temporarily pushing the worker's context just for
+ * the sake of setting up the subscription.
+ *
+ * We therefore always create the bus connection from the worker thread.
+ * For requests that are already in the worker thread this is a pretty
+ * simple affair.
+ *
+ * For requests in other threads (ie: synchronous calls) we have to poke
+ * the worker to instantiate the bus for us (if it doesn't already
+ * exist). We do that by using g_main_context_invoke() to schedule a
+ * dummy request in the worker and then we wait on a GCond until we see
+ * that the bus has been created.
+ *
+ * An attempt to get a particular bus can go one of two ways:
+ *
+ * - success: we end up with a GDBusConnection.
+ *
+ * - failure: we end up with a GError.
+ *
+ * One way or another we put the result in dconf_gdbus_get_bus_data[] so
+ * that we only have one pointer value to check. We know what type of
+ * result it is by dconf_gdbus_get_bus_is_error[].
+ */
+
+static GMutex dconf_gdbus_get_bus_lock;
+static GCond dconf_gdbus_get_bus_cond;
+static gpointer dconf_gdbus_get_bus_data[5];
+static gboolean dconf_gdbus_get_bus_is_error[5];
+
+static GDBusConnection *
+dconf_gdbus_get_bus_common (GBusType bus_type,
+ const GError **error)
+{
+ if (dconf_gdbus_get_bus_is_error[bus_type])
+ {
+ if (error)
+ *error = dconf_gdbus_get_bus_data[bus_type];
+
+ return NULL;
+ }
+
+ return dconf_gdbus_get_bus_data[bus_type];
+}
+
+static GDBusConnection *
+dconf_gdbus_get_bus_in_worker (GBusType bus_type,
+ const GError **error)
+{
+ g_assert_cmpint (bus_type, <, G_N_ELEMENTS (dconf_gdbus_get_bus_data));
+
+ /* We're in the worker thread and only the worker thread can ever set
+ * this variable so there is no need to take a lock.
+ */
+ if (dconf_gdbus_get_bus_data[bus_type] == NULL)
+ {
+ GDBusConnection *connection;
+ GError *error = NULL;
+ gpointer result;
+
+ connection = g_bus_get_sync (bus_type, NULL, &error);
+
+ if (connection)
+ {
+ g_dbus_connection_signal_subscribe (connection, NULL, "ca.desrt.dconf.Writer",
+ NULL, NULL, NULL, G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
+ dconf_gdbus_signal_handler, GINT_TO_POINTER (bus_type), NULL);
+ dconf_gdbus_get_bus_is_error[bus_type] = FALSE;
+ result = connection;
+ }
+ else
+ {
+ dconf_gdbus_get_bus_is_error[bus_type] = TRUE;
+ result = error;
+ }
+
+ g_assert (result != NULL);
+
+ /* It's possible that another thread was waiting for us to do
+ * this on its behalf. Wake it up.
+ *
+ * The other thread cannot actually wake up until we release the
+ * mutex below so we have a guarantee that this CPU will have
+ * flushed all outstanding writes. The other CPU has to acquire
+ * the lock so it cannot have done any out-of-order reads either.
+ */
+ g_mutex_lock (&dconf_gdbus_get_bus_lock);
+ dconf_gdbus_get_bus_data[bus_type] = result;
+ g_cond_broadcast (&dconf_gdbus_get_bus_cond);
+ g_mutex_unlock (&dconf_gdbus_get_bus_lock);
+ }
+
+ return dconf_gdbus_get_bus_common (bus_type, error);
+}
+
+static void
+dconf_gdbus_method_call_done (GObject *source,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GDBusConnection *connection = G_DBUS_CONNECTION (source);
+ DConfEngineCallHandle *handle = user_data;
+ GError *error = NULL;
+ GVariant *reply;
+
+ reply = g_dbus_connection_call_finish (connection, result, &error);
+ dconf_engine_call_handle_reply (handle, reply, error);
+ g_clear_pointer (&reply, g_variant_unref);
+ g_clear_error (&error);
+}
+
+static gboolean
+dconf_gdbus_method_call (gpointer user_data)
+{
+ DConfGDBusCall *call = user_data;
+ GDBusConnection *connection;
+ const GError *error = NULL;
+
+ connection = dconf_gdbus_get_bus_in_worker (call->bus_type, &error);
+
+ if (connection)
+ g_dbus_connection_call (connection, call->bus_name, call->object_path, call->interface_name,
+ call->method_name, call->parameters, call->expected_type, G_DBUS_CALL_FLAGS_NONE,
+ -1, NULL, dconf_gdbus_method_call_done, call->handle);
+
+ else
+ dconf_engine_call_handle_reply (call->handle, NULL, error);
+
+ g_variant_unref (call->parameters);
+ g_slice_free (DConfGDBusCall, call);
+
+ return FALSE;
+}
+
+gboolean
+dconf_engine_dbus_call_async_func (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ DConfEngineCallHandle *handle,
+ GError **error)
+{
+ DConfGDBusCall *call;
+ GSource *source;
+
+ call = g_slice_new (DConfGDBusCall);
+ call->bus_type = bus_type;
+ call->bus_name = bus_name;
+ call->object_path = object_path;
+ call->interface_name = interface_name;
+ call->method_name = method_name;
+ call->parameters = g_variant_ref_sink (parameters);
+ call->expected_type = dconf_engine_call_handle_get_expected_type (handle);
+ call->handle = handle;
+
+ source = g_idle_source_new ();
+ g_source_set_callback (source, dconf_gdbus_method_call, call, NULL);
+ g_source_attach (source, dconf_gdbus_get_worker_context ());
+ g_source_unref (source);
+
+ return TRUE;
+}
+
+/* Dummy function to force the bus into existence in the worker. */
+static gboolean
+dconf_gdbus_summon_bus (gpointer user_data)
+{
+ GBusType bus_type = GPOINTER_TO_INT (user_data);
+
+ dconf_gdbus_get_bus_in_worker (bus_type, NULL);
+
+ return G_SOURCE_REMOVE;
+}
+
+static GDBusConnection *
+dconf_gdbus_get_bus_for_sync (GBusType bus_type,
+ const GError **error)
+{
+ g_assert_cmpint (bus_type, <, G_N_ELEMENTS (dconf_gdbus_get_bus_data));
+
+ /* I'm not 100% sure we have to lock as much as we do here, but let's
+ * play it safe.
+ *
+ * This codepath is only hit on synchronous calls anyway. You're
+ * probably not doing those if you care a lot about performance.
+ */
+ g_mutex_lock (&dconf_gdbus_get_bus_lock);
+ if (dconf_gdbus_get_bus_data[bus_type] == NULL)
+ {
+ g_main_context_invoke (dconf_gdbus_get_worker_context (),
+ dconf_gdbus_summon_bus,
+ GINT_TO_POINTER (bus_type));
+
+ while (dconf_gdbus_get_bus_data[bus_type] == NULL)
+ g_cond_wait (&dconf_gdbus_get_bus_cond, &dconf_gdbus_get_bus_lock);
+ }
+ g_mutex_unlock (&dconf_gdbus_get_bus_lock);
+
+ return dconf_gdbus_get_bus_common (bus_type, error);
+}
+
+GVariant *
+dconf_engine_dbus_call_sync_func (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ const GVariantType *reply_type,
+ GError **error)
+{
+ const GError *inner_error = NULL;
+ GDBusConnection *connection;
+
+ connection = dconf_gdbus_get_bus_for_sync (bus_type, &inner_error);
+
+ if (connection == NULL)
+ {
+ g_variant_unref (g_variant_ref_sink (parameters));
+
+ if (error)
+ *error = g_error_copy (inner_error);
+
+ return NULL;
+ }
+
+ return g_dbus_connection_call_sync (connection, bus_name, object_path, interface_name, method_name,
+ parameters, reply_type, G_DBUS_CALL_FLAGS_NONE, -1, NULL, error);
+}
+
+#ifndef PIC
+void
+dconf_engine_dbus_init_for_testing (void)
+{
+}
+#endif
diff --git a/gdbus/meson.build b/gdbus/meson.build
new file mode 100644
index 0000000..4fbf3ec
--- /dev/null
+++ b/gdbus/meson.build
@@ -0,0 +1,27 @@
+libdconf_gdbus_thread = static_library(
+ 'dconf-gdbus-thread',
+ sources: 'dconf-gdbus-thread.c',
+ include_directories: top_inc,
+ dependencies: libdconf_engine_dep,
+ c_args: dconf_c_args,
+ pic: true,
+)
+
+libdconf_gdbus_thread_dep = declare_dependency(
+ dependencies: libdconf_engine_dep,
+ link_with: libdconf_gdbus_thread,
+)
+
+libdconf_gdbus_filter = static_library(
+ 'dconf-gdbus-filter',
+ sources: 'dconf-gdbus-filter.c',
+ include_directories: top_inc,
+ dependencies: libdconf_engine_dep,
+ c_args: dconf_c_args,
+ pic: true,
+)
+
+libdconf_gdbus_filter_dep = declare_dependency(
+ dependencies: libdconf_engine_dep,
+ link_with: libdconf_gdbus_filter,
+)
diff --git a/gsettings/abicheck.sh b/gsettings/abicheck.sh
new file mode 100755
index 0000000..c8b072b
--- /dev/null
+++ b/gsettings/abicheck.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+${NM:-nm} --dynamic --defined-only $GSETTINGS_LIB > public-abi
+test "`cat public-abi | cut -f 3 -d ' ' | grep -v ^_ | grep -v ^g_io_module | wc -l`" -eq 0 && rm public-abi
diff --git a/gsettings/dconfsettingsbackend.c b/gsettings/dconfsettingsbackend.c
new file mode 100644
index 0000000..6c8179b
--- /dev/null
+++ b/gsettings/dconfsettingsbackend.c
@@ -0,0 +1,269 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#define G_SETTINGS_ENABLE_BACKEND
+#include <gio/gsettingsbackend.h>
+#include "../engine/dconf-engine.h"
+#include <gio/gio.h>
+
+#include <string.h>
+
+typedef GSettingsBackendClass DConfSettingsBackendClass;
+
+typedef struct
+{
+ GSettingsBackend backend;
+ DConfEngine *engine;
+} DConfSettingsBackend;
+
+static GType dconf_settings_backend_get_type (void);
+G_DEFINE_TYPE (DConfSettingsBackend, dconf_settings_backend, G_TYPE_SETTINGS_BACKEND)
+
+static GVariant *
+dconf_settings_backend_read (GSettingsBackend *backend,
+ const gchar *key,
+ const GVariantType *expected_type,
+ gboolean default_value)
+{
+ DConfSettingsBackend *dcsb = (DConfSettingsBackend *) backend;
+
+ return dconf_engine_read (dcsb->engine,
+ default_value ? DCONF_READ_DEFAULT_VALUE : 0,
+ NULL, key);
+}
+
+static GVariant *
+dconf_settings_backend_read_user_value (GSettingsBackend *backend,
+ const gchar *key,
+ const GVariantType *expected_type)
+{
+ DConfSettingsBackend *dcsb = (DConfSettingsBackend *) backend;
+
+ return dconf_engine_read (dcsb->engine, DCONF_READ_USER_VALUE, NULL, key);
+}
+
+static gboolean
+dconf_settings_backend_write (GSettingsBackend *backend,
+ const gchar *key,
+ GVariant *value,
+ gpointer origin_tag)
+{
+ DConfSettingsBackend *dcsb = (DConfSettingsBackend *) backend;
+ DConfChangeset *change;
+ gboolean success;
+
+ change = dconf_changeset_new ();
+ dconf_changeset_set (change, key, value);
+
+ success = dconf_engine_change_fast (dcsb->engine, change, origin_tag, NULL);
+ dconf_changeset_unref (change);
+
+ return success;
+}
+
+static gboolean
+dconf_settings_backend_add_to_changeset (gpointer key,
+ gpointer value,
+ gpointer data)
+{
+ dconf_changeset_set (data, key, value);
+
+ return FALSE;
+}
+
+static gboolean
+dconf_settings_backend_write_tree (GSettingsBackend *backend,
+ GTree *tree,
+ gpointer origin_tag)
+{
+ DConfSettingsBackend *dcsb = (DConfSettingsBackend *) backend;
+ DConfChangeset *change;
+ gboolean success;
+
+ if (g_tree_nnodes (tree) == 0)
+ return TRUE;
+
+ change = dconf_changeset_new ();
+ g_tree_foreach (tree, dconf_settings_backend_add_to_changeset, change);
+ success = dconf_engine_change_fast (dcsb->engine, change, origin_tag, NULL);
+ dconf_changeset_unref (change);
+
+ return success;
+}
+
+static void
+dconf_settings_backend_reset (GSettingsBackend *backend,
+ const gchar *key,
+ gpointer origin_tag)
+{
+ dconf_settings_backend_write (backend, key, NULL, origin_tag);
+}
+
+static gboolean
+dconf_settings_backend_get_writable (GSettingsBackend *backend,
+ const gchar *name)
+{
+ DConfSettingsBackend *dcsb = (DConfSettingsBackend *) backend;
+
+ return dconf_engine_is_writable (dcsb->engine, name);
+}
+
+static void
+dconf_settings_backend_subscribe (GSettingsBackend *backend,
+ const gchar *name)
+{
+ DConfSettingsBackend *dcsb = (DConfSettingsBackend *) backend;
+
+ dconf_engine_watch_fast (dcsb->engine, name);
+}
+
+static void
+dconf_settings_backend_unsubscribe (GSettingsBackend *backend,
+ const gchar *name)
+{
+ DConfSettingsBackend *dcsb = (DConfSettingsBackend *) backend;
+
+ dconf_engine_unwatch_fast (dcsb->engine, name);
+}
+
+static void
+dconf_settings_backend_sync (GSettingsBackend *backend)
+{
+ DConfSettingsBackend *dcsb = (DConfSettingsBackend *) backend;
+
+ dconf_engine_sync (dcsb->engine);
+}
+
+static void
+dconf_settings_backend_free_weak_ref (gpointer data)
+{
+ GWeakRef *weak_ref = data;
+
+ g_weak_ref_clear (weak_ref);
+ g_slice_free (GWeakRef, weak_ref);
+}
+
+static void
+dconf_settings_backend_init (DConfSettingsBackend *dcsb)
+{
+ GWeakRef *weak_ref;
+
+ weak_ref = g_slice_new (GWeakRef);
+ g_weak_ref_init (weak_ref, dcsb);
+ dcsb->engine = dconf_engine_new (NULL, weak_ref, dconf_settings_backend_free_weak_ref);
+}
+
+static void
+dconf_settings_backend_finalize (GObject *object)
+{
+ DConfSettingsBackend *dcsb = (DConfSettingsBackend *) object;
+
+ dconf_engine_unref (dcsb->engine);
+
+ G_OBJECT_CLASS (dconf_settings_backend_parent_class)
+ ->finalize (object);
+}
+
+static void
+dconf_settings_backend_class_init (GSettingsBackendClass *class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+ object_class->finalize = dconf_settings_backend_finalize;
+
+ class->read = dconf_settings_backend_read;
+ class->read_user_value = dconf_settings_backend_read_user_value;
+ class->write = dconf_settings_backend_write;
+ class->write_tree = dconf_settings_backend_write_tree;
+ class->reset = dconf_settings_backend_reset;
+ class->get_writable = dconf_settings_backend_get_writable;
+ class->subscribe = dconf_settings_backend_subscribe;
+ class->unsubscribe = dconf_settings_backend_unsubscribe;
+ class->sync = dconf_settings_backend_sync;
+}
+
+void
+g_io_module_load (GIOModule *module)
+{
+ g_type_module_use (G_TYPE_MODULE (module));
+ g_io_extension_point_implement (G_SETTINGS_BACKEND_EXTENSION_POINT_NAME,
+ dconf_settings_backend_get_type (),
+ "dconf", 100);
+}
+
+void
+g_io_module_unload (GIOModule *module)
+{
+ g_assert_not_reached ();
+}
+
+gchar **
+g_io_module_query (void)
+{
+ return g_strsplit (G_SETTINGS_BACKEND_EXTENSION_POINT_NAME, "!", 0);
+}
+
+void
+dconf_engine_change_notify (DConfEngine *engine,
+ const gchar *prefix,
+ const gchar * const *changes,
+ const gchar *tag,
+ gboolean is_writability,
+ gpointer origin_tag,
+ gpointer user_data)
+{
+ GWeakRef *weak_ref = user_data;
+ DConfSettingsBackend *dcsb;
+ g_debug ("change_notify: %s", prefix);
+
+ dcsb = g_weak_ref_get (weak_ref);
+
+ if (dcsb == NULL)
+ return;
+
+ if (changes[0] == NULL)
+ return;
+
+ if (is_writability)
+ {
+ /* We know that the engine does it this way... */
+ g_assert (changes[0][0] == '\0' && changes[1] == NULL);
+
+ if (g_str_has_suffix (prefix, "/"))
+ g_settings_backend_path_writable_changed (G_SETTINGS_BACKEND (dcsb), prefix);
+ else
+ g_settings_backend_writable_changed (G_SETTINGS_BACKEND (dcsb), prefix);
+ }
+
+ /* We send the normal change notification even in the event that this
+ * was a writability notification because adding/removing a lock could
+ * impact the value that gets read.
+ */
+ if (changes[1] == NULL)
+ {
+ if (g_str_has_suffix (prefix, "/"))
+ g_settings_backend_path_changed (G_SETTINGS_BACKEND (dcsb), prefix, origin_tag);
+ else
+ g_settings_backend_changed (G_SETTINGS_BACKEND (dcsb), prefix, origin_tag);
+ }
+ else
+ g_settings_backend_keys_changed (G_SETTINGS_BACKEND (dcsb), prefix, changes, origin_tag);
+}
diff --git a/gsettings/meson.build b/gsettings/meson.build
new file mode 100644
index 0000000..a28892d
--- /dev/null
+++ b/gsettings/meson.build
@@ -0,0 +1,32 @@
+# We use the libraries directly, as the dependency objects use
+# link_whole; this avoids the gsettings backend module exposing
+# symbols other than g_io_module_*
+backend_deps = [
+ libdconf_common_hidden,
+ libdconf_gdbus_thread,
+]
+
+libdconf_settings = shared_library(
+ 'dconfsettings',
+ sources: 'dconfsettingsbackend.c',
+ include_directories: top_inc,
+ link_with: backend_deps,
+ dependencies: gio_dep,
+ c_args: dconf_c_args,
+ install: true,
+ install_dir: gio_module_dir,
+)
+
+envs = test_env + [
+ 'G_TEST_SRCDIR=' + meson.current_source_dir(),
+ 'G_TEST_BUILDDIR=' + meson.current_build_dir(),
+ 'GSETTINGS_LIB=' + libdconf_settings.full_path(),
+]
+
+unit_test = 'abicheck'
+
+test(
+ unit_test,
+ find_program(unit_test + '.sh'),
+ env: envs,
+)
diff --git a/gvdb/README b/gvdb/README
new file mode 100644
index 0000000..4dbd697
--- /dev/null
+++ b/gvdb/README
@@ -0,0 +1,12 @@
+DO NOT MODIFY ANY FILE IN THIS DIRECTORY
+
+(except maybe the meson.build)
+
+This directory is the result of a git subtree merge with the 'gvdb'
+module on gitlab.gnome.org. Please apply fixes to the 'gvdb' module and
+perform a git merge:
+
+git remote add gvdb git@gitlab.gnome.org:GNOME/gvdb.git
+git remote update gvdb
+cd gvdb/
+git merge gvdb/master \ No newline at end of file
diff --git a/gvdb-builder.c b/gvdb/gvdb-builder.c
index 2383e60..2383e60 100644
--- a/gvdb-builder.c
+++ b/gvdb/gvdb-builder.c
diff --git a/gvdb-builder.h b/gvdb/gvdb-builder.h
index b4815f0..b4815f0 100644
--- a/gvdb-builder.h
+++ b/gvdb/gvdb-builder.h
diff --git a/gvdb-format.h b/gvdb/gvdb-format.h
index ed6adab..ed6adab 100644
--- a/gvdb-format.h
+++ b/gvdb/gvdb-format.h
diff --git a/gvdb-reader.c b/gvdb/gvdb-reader.c
index 9509388..9509388 100644
--- a/gvdb-reader.c
+++ b/gvdb/gvdb-reader.c
diff --git a/gvdb-reader.h b/gvdb/gvdb-reader.h
index 79a97d3..79a97d3 100644
--- a/gvdb-reader.h
+++ b/gvdb/gvdb-reader.h
diff --git a/gvdb.doap b/gvdb/gvdb.doap
index 8c5f3e8..8c5f3e8 100644
--- a/gvdb.doap
+++ b/gvdb/gvdb.doap
diff --git a/gvdb/meson.build b/gvdb/meson.build
new file mode 100644
index 0000000..1a1aba8
--- /dev/null
+++ b/gvdb/meson.build
@@ -0,0 +1,27 @@
+gvdb_builder = files('gvdb-builder.c')
+
+sources = gvdb_builder + files('gvdb-reader.c')
+
+gvdb_deps = [
+ gio_dep,
+ glib_dep,
+]
+
+cflags = [
+ '-DG_LOG_DOMAIN="gvdb (via dconf)"',
+ '-DG_LOG_USE_STRUCTURED=1',
+]
+
+libgvdb = static_library(
+ 'gvdb',
+ sources: sources,
+ include_directories: top_inc,
+ dependencies: gvdb_deps,
+ c_args: cflags,
+ pic: true,
+)
+
+libgvdb_dep = declare_dependency(
+ dependencies: gvdb_deps,
+ link_with: libgvdb,
+)
diff --git a/meson.build b/meson.build
new file mode 100644
index 0000000..9ba40e1
--- /dev/null
+++ b/meson.build
@@ -0,0 +1,85 @@
+project(
+ 'dconf', ['c'],
+ version: '0.33.1',
+ license: 'LGPL2.1+',
+ meson_version: '>= 0.47.0',
+)
+
+dconf_prefix = get_option('prefix')
+dconf_datadir = join_paths(dconf_prefix, get_option('datadir'))
+dconf_libdir = join_paths(dconf_prefix, get_option('libdir'))
+dconf_libexecdir = join_paths(dconf_prefix, get_option('libexecdir'))
+dconf_mandir = join_paths(dconf_prefix, get_option('mandir'))
+dconf_sysconfdir = join_paths(dconf_prefix, get_option('sysconfdir'))
+
+dconf_namespace = 'ca.desrt.dconf'
+
+soversion = 1
+current = 0
+revision = 0
+libversion = '@0@.@1@.@2@'.format(soversion, current, revision)
+
+cc = meson.get_compiler('c')
+
+# compiler flags
+common_flags = ['-DSYSCONFDIR="@0@"'.format(dconf_sysconfdir)]
+
+if get_option('buildtype').contains('debug')
+ common_flags += cc.get_supported_arguments([
+ '-fno-common',
+ '-Wmissing-prototypes',
+ '-Wwrite-strings',
+ ])
+endif
+
+add_project_arguments(common_flags, language: 'c')
+
+dconf_c_args = [
+ '-DG_LOG_DOMAIN="dconf"',
+ '-DG_LOG_USE_STRUCTURED=1',
+]
+
+gio_req_version = '>= 2.25.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.44.0')
+
+gio_module_dir = gio_dep.get_pkgconfig_variable('giomoduledir', define_variable: ['libdir', dconf_libdir])
+dbus_session_service_dir = dependency('dbus-1').get_pkgconfig_variable('session_bus_services_dir', define_variable: ['datadir', dconf_datadir])
+
+enable_bash_completion = get_option('bash_completion')
+if enable_bash_completion
+ # FIXME: the `.pc` file is wrong because `completionsdir` should be relative to `datadir`, not `prefix`
+ completions_dir = dependency('bash-completion').get_pkgconfig_variable('completionsdir', define_variable: ['prefix', dconf_prefix])
+endif
+
+configure_file(
+ output: 'config.h',
+ configuration: configuration_data(),
+)
+
+test_env = [
+ 'G_DEBUG=gc-friendly,fatal-warnings',
+ 'MALLOC_CHECK_=2',
+ 'LC_ALL=C.UTF-8',
+]
+
+gnome = import('gnome')
+pkg = import('pkgconfig')
+
+top_inc = include_directories('.')
+
+subdir('shm')
+subdir('gvdb')
+subdir('common')
+subdir('engine')
+subdir('service')
+subdir('gdbus')
+subdir('gsettings')
+subdir('client')
+subdir('bin')
+subdir('docs')
+subdir('tests')
+
+meson.add_install_script('meson_post_install.py', gio_module_dir)
diff --git a/meson_options.txt b/meson_options.txt
new file mode 100644
index 0000000..a2794ce
--- /dev/null
+++ b/meson_options.txt
@@ -0,0 +1,4 @@
+option('bash_completion', type: 'boolean', value: true, description: 'install bash completion files')
+option('man', type: 'boolean', value: true, description: 'generate man pages')
+option('gtk_doc', type: 'boolean', value: false, description: 'use gtk-doc to build documentation')
+option('vapi', type: 'boolean', value: true, description: 'install dconf client vapi')
diff --git a/meson_post_install.py b/meson_post_install.py
new file mode 100644
index 0000000..3082d42
--- /dev/null
+++ b/meson_post_install.py
@@ -0,0 +1,9 @@
+#!/usr/bin/env python3
+
+import os
+import subprocess
+import sys
+
+if not os.environ.get('DESTDIR'):
+ print('GIO module cache creation...')
+ subprocess.call(['gio-querymodules', sys.argv[1]])
diff --git a/service/ca.desrt.dconf.service.in b/service/ca.desrt.dconf.service.in
new file mode 100644
index 0000000..369948a
--- /dev/null
+++ b/service/ca.desrt.dconf.service.in
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=ca.desrt.dconf
+Exec=@libexecdir@/dconf-service
diff --git a/service/ca.desrt.dconf.xml b/service/ca.desrt.dconf.xml
new file mode 100644
index 0000000..3273d5d
--- /dev/null
+++ b/service/ca.desrt.dconf.xml
@@ -0,0 +1,23 @@
+<node>
+ <interface name='ca.desrt.dconf.Writer'>
+ <method name='Init'/>
+ <method name='Change'>
+ <arg name='blob' direction='in' type='ay'>
+ <annotation name='org.gtk.GDBus.C.ForceGVariant' value='1'/>
+ </arg>
+ <arg name='tag' direction='out' type='s'/>
+ </method>
+ <signal name='Notify'>
+ <annotation name='org.gtk.GDBus.C.Name' value='NotifySignal'/>
+ <arg name='prefix' direction='out' type='s'/>
+ <arg name='changes' direction='out' type='as'/>
+ <arg name='tag' direction='out' type='s'/>
+ </signal>
+ </interface>
+
+ <interface name='ca.desrt.dconf.ServiceInfo'>
+ <method name='Blame'>
+ <arg name='blame' direction='out' type='s'/>
+ </method>
+ </interface>
+</node>
diff --git a/service/dconf-blame.c b/service/dconf-blame.c
new file mode 100644
index 0000000..5084ef1
--- /dev/null
+++ b/service/dconf-blame.c
@@ -0,0 +1,190 @@
+/*
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-blame.h"
+
+#include "dconf-generated.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <fcntl.h>
+
+typedef DConfDBusServiceInfoSkeletonClass DConfBlameClass;
+struct _DConfBlame
+{
+ DConfDBusServiceInfoSkeleton parent_instance;
+
+ GString *blame_info;
+};
+
+static void dconf_blame_iface_init (DConfDBusServiceInfoIface *iface);
+G_DEFINE_TYPE_WITH_CODE (DConfBlame, dconf_blame, DCONF_DBUS_TYPE_SERVICE_INFO_SKELETON,
+ G_IMPLEMENT_INTERFACE (DCONF_DBUS_TYPE_SERVICE_INFO, dconf_blame_iface_init))
+
+#include "../common/dconf-changeset.h"
+#include "dconf-writer.h"
+
+void
+dconf_blame_record (GDBusMethodInvocation *invocation)
+{
+ DConfBlame *blame = dconf_blame_get ();
+ GError *error = NULL;
+ GVariant *parameters;
+ GVariant *reply;
+ GString *info;
+
+ if (!blame)
+ return;
+
+ if (blame->blame_info->len)
+ g_string_append (blame->blame_info, "\n====================================================================\n");
+
+ info = blame->blame_info;
+
+ g_string_append_printf (info, "Sender: %s\n", g_dbus_method_invocation_get_sender (invocation));
+ g_string_append_printf (info, "Object path: %s\n", g_dbus_method_invocation_get_object_path (invocation));
+ g_string_append_printf (info, "Method: %s\n", g_dbus_method_invocation_get_method_name (invocation));
+
+ if ((parameters = g_dbus_method_invocation_get_parameters (invocation)))
+ {
+ gchar *tmp;
+
+ tmp = g_variant_print (parameters, FALSE);
+ g_string_append_printf (info, "Parameters: %s\n", tmp);
+ g_free (tmp);
+ }
+
+ reply = g_dbus_connection_call_sync (g_dbus_method_invocation_get_connection (invocation),
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus",
+ "GetConnectionUnixProcessID",
+ g_variant_new ("(s)", g_dbus_method_invocation_get_sender (invocation)),
+ G_VARIANT_TYPE ("(u)"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
+
+ if (reply != NULL)
+ {
+ guint pid;
+
+ g_variant_get (reply, "(u)", &pid);
+ g_string_append_printf (info, "PID: %u\n", pid);
+ g_variant_unref (reply);
+ }
+ else
+ {
+ g_string_append_printf (info, "Unable to acquire PID: %s\n", error->message);
+ g_error_free (error);
+ }
+
+ {
+ const gchar * const ps_fx[] = { "ps", "fx", NULL };
+ gchar *result_out;
+ gchar *result_err;
+ gint status;
+
+ if (g_spawn_sync (NULL, (gchar **) ps_fx, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
+ &result_out, &result_err, &status, &error))
+ {
+ g_string_append (info, "\n=== Process table from time of call follows ('ps fx') ===\n");
+ g_string_append (info, result_out);
+ g_string_append (info, result_err);
+ g_string_append_printf (info, "\nps exit status: %u\n", status);
+ }
+ else
+ {
+ g_string_append_printf (info, "\nUnable to spawn 'ps fx': %s\n", error->message);
+ g_error_free (error);
+ }
+ }
+}
+
+static gboolean
+dconf_blame_handle_blame (DConfDBusServiceInfo *info,
+ GDBusMethodInvocation *invocation)
+{
+ DConfBlame *blame = DCONF_BLAME (info);
+
+ dconf_blame_record (invocation);
+
+ g_dbus_method_invocation_return_value (invocation, g_variant_new ("(s)", blame->blame_info->str));
+
+ return TRUE;
+}
+
+static void
+dconf_blame_init (DConfBlame *blame)
+{
+ blame->blame_info = g_string_new (NULL);
+}
+
+static void
+dconf_blame_class_init (DConfBlameClass *class)
+{
+}
+
+static void
+dconf_blame_iface_init (DConfDBusServiceInfoIface *iface)
+{
+ iface->handle_blame = dconf_blame_handle_blame;
+}
+
+static gboolean
+dconf_blame_enabled (void)
+{
+ gint fd;
+
+ if (getenv ("DCONF_BLAME"))
+ return TRUE;
+
+ fd = open ("/proc/cmdline", O_RDONLY);
+ if (fd != -1)
+ {
+ gchar buffer[1024];
+ gssize s;
+
+ s = read (fd, buffer, sizeof buffer - 1);
+ close (fd);
+
+ if (0 < s && s < sizeof buffer)
+ {
+ buffer[s] = '\0';
+ if (strstr (buffer, "DCONF_BLAME"))
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+DConfBlame *
+dconf_blame_get (void)
+{
+ static DConfBlame *blame;
+ static gboolean checked;
+
+ if (!checked)
+ {
+ if (dconf_blame_enabled ())
+ blame = g_object_new (DCONF_TYPE_BLAME, NULL);
+
+ checked = TRUE;
+ }
+
+ return blame;
+}
diff --git a/service/dconf-blame.h b/service/dconf-blame.h
new file mode 100644
index 0000000..5910272
--- /dev/null
+++ b/service/dconf-blame.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __dconf_blame_h__
+#define __dconf_blame_h__
+
+typedef struct _DConfBlame DConfBlame;
+
+#include <gio/gio.h>
+
+#define DCONF_TYPE_BLAME (dconf_blame_get_type ())
+#define DCONF_BLAME(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \
+ DCONF_TYPE_BLAME, DConfBlame))
+#define DCONF_IS_BLAME(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \
+ DCONF_TYPE_BLAME))
+#define DCONF_BLAME_GET_CLASS(inst) (G_TYPE_INSTANCE_GET_CLASS ((inst), \
+ DCONF_TYPE_BLAME, DConfBlameClass))
+
+GType dconf_blame_get_type (void);
+DConfBlame *dconf_blame_get (void);
+void dconf_blame_record (GDBusMethodInvocation *invocation);
+
+#endif /* __dconf_blame_h__ */
diff --git a/service/dconf-gvdb-utils.c b/service/dconf-gvdb-utils.c
new file mode 100644
index 0000000..93a4719
--- /dev/null
+++ b/service/dconf-gvdb-utils.c
@@ -0,0 +1,219 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-gvdb-utils.h"
+
+#include "../common/dconf-paths.h"
+#include "../gvdb/gvdb-builder.h"
+#include "../gvdb/gvdb-reader.h"
+
+#include <errno.h>
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <string.h>
+
+DConfChangeset *
+dconf_gvdb_utils_read_and_back_up_file (const gchar *filename,
+ gboolean *file_missing,
+ GError **error)
+{
+ DConfChangeset *database;
+ GError *my_error = NULL;
+ GvdbTable *table = NULL;
+ gchar *contents;
+ gsize size;
+
+ if (g_file_get_contents (filename, &contents, &size, &my_error))
+ {
+ GBytes *bytes;
+
+ bytes = g_bytes_new_take (contents, size);
+ table = gvdb_table_new_from_bytes (bytes, FALSE, &my_error);
+ g_bytes_unref (bytes);
+ }
+
+ /* It is perfectly fine if the file does not exist -- then it's
+ * just empty.
+ */
+ if (g_error_matches (my_error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+ g_clear_error (&my_error);
+
+ /* Otherwise, we should report errors to prevent ourselves from
+ * overwriting the database in other situations...
+ */
+ if (g_error_matches (my_error, G_FILE_ERROR, G_FILE_ERROR_INVAL))
+ {
+ /* Move the database to a backup file, warn and continue with a new
+ * database. The alternative is erroring out and exiting the daemon,
+ * which leaves the user’s session essentially unusable.
+ *
+ * The code to find an unused backup filename is racy, but this is an
+ * error handling path. Who cares. */
+ g_autofree gchar *backup_filename = NULL;
+ guint i;
+
+ for (i = 0;
+ i < G_MAXUINT &&
+ (backup_filename == NULL || g_file_test (backup_filename, G_FILE_TEST_EXISTS));
+ i++)
+ {
+ g_free (backup_filename);
+ backup_filename = g_strdup_printf ("%s~%u", filename, i);
+ }
+
+ if (g_rename (filename, backup_filename) != 0)
+ g_warning ("Error renaming corrupt database from ‘%s’ to ‘%s’: %s",
+ filename, backup_filename, g_strerror (errno));
+ else
+ g_warning ("Database ‘%s’ was corrupt: moved it to ‘%s’ and created an empty replacement",
+ filename, backup_filename);
+
+ g_clear_error (&my_error);
+ }
+ else if (my_error)
+ {
+ g_propagate_prefixed_error (error, my_error, "Cannot open dconf database: ");
+ return NULL;
+ }
+
+ /* Only allocate once we know we are in a non-error situation */
+ database = dconf_changeset_new_database (NULL);
+
+ /* Fill the table up with the initial state */
+ if (table != NULL)
+ {
+ gchar **names;
+ gint n_names;
+ gint i;
+
+ names = gvdb_table_get_names (table, &n_names);
+ for (i = 0; i < n_names; i++)
+ {
+ if (dconf_is_key (names[i], NULL))
+ {
+ GVariant *value;
+
+ value = gvdb_table_get_value (table, names[i]);
+
+ if (value != NULL)
+ {
+ dconf_changeset_set (database, names[i], value);
+ g_variant_unref (value);
+ }
+ }
+
+ g_free (names[i]);
+ }
+
+ gvdb_table_free (table);
+ g_free (names);
+ }
+
+ if (file_missing)
+ *file_missing = (table == NULL);
+
+ return database;
+}
+
+static GvdbItem *
+dconf_gvdb_utils_get_parent (GHashTable *table,
+ const gchar *key)
+{
+ GvdbItem *grandparent, *parent;
+ gchar *parent_name;
+ gint len;
+
+ if (g_str_equal (key, "/"))
+ return NULL;
+
+ len = strlen (key);
+ if (key[len - 1] == '/')
+ len--;
+
+ while (key[len - 1] != '/')
+ len--;
+
+ parent_name = g_strndup (key, len);
+ parent = g_hash_table_lookup (table, parent_name);
+
+ if (parent == NULL)
+ {
+ parent = gvdb_hash_table_insert (table, parent_name);
+
+ grandparent = dconf_gvdb_utils_get_parent (table, parent_name);
+
+ if (grandparent != NULL)
+ gvdb_item_set_parent (parent, grandparent);
+ }
+
+ g_free (parent_name);
+
+ return parent;
+}
+
+static gboolean
+dconf_gvdb_utils_add_key (const gchar *path,
+ GVariant *value,
+ gpointer user_data)
+{
+ GHashTable *gvdb = user_data;
+ GvdbItem *item;
+
+ g_assert (g_hash_table_lookup (gvdb, path) == NULL);
+ item = gvdb_hash_table_insert (gvdb, path);
+ gvdb_item_set_parent (item, dconf_gvdb_utils_get_parent (gvdb, path));
+ gvdb_item_set_value (item, value);
+
+ return TRUE;
+}
+
+gboolean
+dconf_gvdb_utils_write_file (const gchar *filename,
+ DConfChangeset *database,
+ GError **error)
+{
+ GHashTable *gvdb;
+ gboolean success;
+
+ gvdb = gvdb_hash_table_new (NULL, NULL);
+ dconf_changeset_all (database, dconf_gvdb_utils_add_key, gvdb);
+ success = gvdb_table_write_contents (gvdb, filename, FALSE, error);
+
+ if (!success)
+ {
+ gchar *dirname;
+
+ /* Maybe it failed because the directory doesn't exist. Try
+ * again, after mkdir().
+ */
+ dirname = g_path_get_dirname (filename);
+ g_mkdir_with_parents (dirname, 0700);
+ g_free (dirname);
+
+ g_clear_error (error);
+ success = gvdb_table_write_contents (gvdb, filename, FALSE, error);
+ }
+
+ g_hash_table_unref (gvdb);
+
+ return success;
+}
diff --git a/service/dconf-gvdb-utils.h b/service/dconf-gvdb-utils.h
new file mode 100644
index 0000000..7076781
--- /dev/null
+++ b/service/dconf-gvdb-utils.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __dconf_gvdb_utils_h__
+#define __dconf_gvdb_utils_h__
+
+#include "../common/dconf-changeset.h"
+
+DConfChangeset * dconf_gvdb_utils_read_and_back_up_file (const gchar *filename,
+ gboolean *file_missing,
+ GError **error);
+gboolean dconf_gvdb_utils_write_file (const gchar *filename,
+ DConfChangeset *database,
+ GError **error);
+
+#endif /* __dconf_gvdb_utils_h__ */
diff --git a/service/dconf-keyfile-writer.c b/service/dconf-keyfile-writer.c
new file mode 100644
index 0000000..f4951bb
--- /dev/null
+++ b/service/dconf-keyfile-writer.c
@@ -0,0 +1,529 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-writer.h"
+
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+
+typedef DConfWriterClass DConfKeyfileWriterClass;
+
+typedef struct
+{
+ DConfWriter parent_instance;
+ gchar *filename;
+ gchar *lock_filename;
+ gint lock_fd;
+ GFileMonitor *monitor;
+ guint scheduled_update;
+ gchar *contents;
+ GKeyFile *keyfile;
+} DConfKeyfileWriter;
+
+G_DEFINE_TYPE (DConfKeyfileWriter, dconf_keyfile_writer, DCONF_TYPE_WRITER)
+
+static DConfChangeset *
+dconf_keyfile_to_changeset (GKeyFile *keyfile,
+ const gchar *filename_fyi)
+{
+ DConfChangeset *changeset;
+ gchar **groups;
+ gint i;
+
+ changeset = dconf_changeset_new_database (NULL);
+
+ groups = g_key_file_get_groups (keyfile, NULL);
+ for (i = 0; groups[i]; i++)
+ {
+ const gchar *group = groups[i];
+ gchar *key_prefix;
+ gchar **keys;
+ gint j;
+
+ /* Special case the [/] group to be able to contain keys at the
+ * root (/a, /b, etc.). All others must not start or end with a
+ * slash (ie: group [x/y] contains keys such as /x/y/z).
+ */
+ if (!g_str_equal (group, "/"))
+ {
+ if (g_str_has_prefix (group, "/") || g_str_has_suffix (group, "/") || strstr (group, "//"))
+ {
+ g_warning ("%s: ignoring invalid group name: %s\n", filename_fyi, group);
+ continue;
+ }
+
+ key_prefix = g_strconcat ("/", group, "/", NULL);
+ }
+ else
+ key_prefix = g_strdup ("/");
+
+ keys = g_key_file_get_keys (keyfile, group, NULL, NULL);
+ g_assert (keys != NULL);
+
+ for (j = 0; keys[j]; j++)
+ {
+ const gchar *key = keys[j];
+ GError *error = NULL;
+ gchar *value_str;
+ GVariant *value;
+ gchar *path;
+
+ if (strchr (key, '/'))
+ {
+ g_warning ("%s: [%s]: ignoring invalid key name: %s\n", filename_fyi, group, key);
+ continue;
+ }
+
+ value_str = g_key_file_get_value (keyfile, group, key, NULL);
+ g_assert (value_str != NULL);
+
+ value = g_variant_parse (NULL, value_str, NULL, NULL, &error);
+ g_free (value_str);
+
+ if (value == NULL)
+ {
+ g_warning ("%s: [%s]: %s: skipping invalid value: %s (%s)\n",
+ filename_fyi, group, key, value_str, error->message);
+ g_error_free (error);
+ continue;
+ }
+
+ path = g_strconcat (key_prefix, key, NULL);
+ dconf_changeset_set (changeset, path, value);
+ g_variant_unref (value);
+ g_free (path);
+ }
+
+ g_free (key_prefix);
+ g_strfreev (keys);
+ }
+
+ g_strfreev (groups);
+
+ return changeset;
+}
+
+static void
+dconf_keyfile_writer_list (GHashTable *set)
+{
+ const gchar *name;
+ gchar *dirname;
+ GDir *dir;
+
+ dirname = g_build_filename (g_get_user_config_dir (), "dconf", NULL);
+ dir = g_dir_open (dirname, 0, NULL);
+
+ if (!dir)
+ return;
+
+ while ((name = g_dir_read_name (dir)))
+ {
+ const gchar *dottxt;
+
+ dottxt = strstr (name, ".txt");
+
+ if (dottxt && dottxt[4] == '\0')
+ g_hash_table_add (set, g_strndup (name, dottxt - name));
+ }
+
+ g_dir_close (dir);
+}
+
+static gboolean dconf_keyfile_update (gpointer user_data);
+
+static void
+dconf_keyfile_changed (GFileMonitor *monitor,
+ GFile *file,
+ GFile *other_file,
+ GFileMonitorEvent event_type,
+ gpointer user_data)
+{
+ DConfKeyfileWriter *kfw = user_data;
+
+ if (event_type == G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT ||
+ event_type == G_FILE_MONITOR_EVENT_CREATED)
+ {
+ if (!kfw->scheduled_update)
+ kfw->scheduled_update = g_idle_add (dconf_keyfile_update, kfw);
+ }
+}
+
+static gboolean
+dconf_keyfile_writer_begin (DConfWriter *writer,
+ GError **error)
+{
+ DConfKeyfileWriter *kfw = (DConfKeyfileWriter *) writer;
+ GError *local_error = NULL;
+ DConfChangeset *contents;
+ DConfChangeset *changes;
+
+ if (kfw->filename == NULL)
+ {
+ gchar *filename_base;
+ GFile *file;
+
+ filename_base = g_build_filename (g_get_user_config_dir (), "dconf", dconf_writer_get_name (writer), NULL);
+ kfw->filename = g_strconcat (filename_base, ".txt", NULL);
+ kfw->lock_filename = g_strconcat (kfw->filename, "-lock", NULL);
+ g_free (filename_base);
+
+ /* See https://bugzilla.gnome.org/show_bug.cgi?id=691618 */
+ file = g_vfs_get_file_for_path (g_vfs_get_local (), kfw->filename);
+ kfw->monitor = g_file_monitor_file (file, G_FILE_MONITOR_NONE, NULL, NULL);
+ g_object_unref (file);
+
+ g_signal_connect (kfw->monitor, "changed", G_CALLBACK (dconf_keyfile_changed), kfw);
+ }
+
+ g_clear_pointer (&kfw->contents, g_free);
+
+ kfw->lock_fd = open (kfw->lock_filename, O_RDWR | O_CREAT, 0666);
+ 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. */
+ }
+
+ if (!g_file_get_contents (kfw->filename, &kfw->contents, NULL, &local_error))
+ {
+ if (!g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT))
+ {
+ g_propagate_error (error, local_error);
+ return FALSE;
+ }
+
+ g_clear_error (&local_error);
+ }
+
+ kfw->keyfile = g_key_file_new ();
+
+ if (kfw->contents)
+ {
+ if (!g_key_file_load_from_data (kfw->keyfile, kfw->contents, -1, G_KEY_FILE_KEEP_COMMENTS, &local_error))
+ {
+ g_clear_pointer (&kfw->keyfile, g_key_file_free);
+ g_clear_pointer (&kfw->contents, g_free);
+ g_propagate_error (error, local_error);
+ return FALSE;
+ }
+ }
+
+ if (!DCONF_WRITER_CLASS (dconf_keyfile_writer_parent_class)->begin (writer, error))
+ {
+ g_clear_pointer (&kfw->keyfile, g_key_file_free);
+ return FALSE;
+ }
+
+ /* Diff the keyfile to the current contents of the database and apply
+ * any changes that we notice.
+ *
+ * This will catch both the case of people outside of the service
+ * making changes to the file and also the case of starting for the
+ * first time.
+ */
+ contents = dconf_keyfile_to_changeset (kfw->keyfile, kfw->filename);
+ changes = dconf_writer_diff (writer, contents);
+
+ if (changes)
+ {
+ DCONF_WRITER_CLASS (dconf_keyfile_writer_parent_class)->change (writer, changes, "");
+ dconf_changeset_unref (changes);
+ }
+
+ dconf_changeset_unref (contents);
+
+ return TRUE;
+}
+
+static void
+dconf_keyfile_writer_change (DConfWriter *writer,
+ DConfChangeset *changeset,
+ const gchar *tag)
+{
+ DConfKeyfileWriter *kfw = (DConfKeyfileWriter *) writer;
+ const gchar *prefix;
+ const gchar * const *paths;
+ GVariant * const *values;
+ guint n, i;
+
+ DCONF_WRITER_CLASS (dconf_keyfile_writer_parent_class)->change (writer, changeset, tag);
+
+ n = dconf_changeset_describe (changeset, &prefix, &paths, &values);
+
+ for (i = 0; i < n; i++)
+ {
+ gchar *path = g_strconcat (prefix, paths[i], NULL);
+ GVariant *value = values[i];
+
+ if (g_str_equal (path, "/"))
+ {
+ g_assert (value == NULL);
+
+ /* This is a request to reset everything.
+ *
+ * Easiest way to do this:
+ */
+ g_key_file_free (kfw->keyfile);
+ kfw->keyfile = g_key_file_new ();
+ }
+ else if (g_str_has_suffix (path, "/"))
+ {
+ gchar *group_to_remove;
+ gchar **groups;
+ gint i;
+
+ g_assert (value == NULL);
+
+ /* Time to do a path reset.
+ *
+ * We must reset the group for the path plus any "subgroups".
+ *
+ * We dealt with the case of "/" above, so we know we have
+ * something with at least a separate leading and trailing slash,
+ * with the group name in the middle.
+ */
+ group_to_remove = g_strndup (path + 1, strlen (path) - 2);
+ g_key_file_remove_group (kfw->keyfile, group_to_remove, NULL);
+ g_free (group_to_remove);
+
+ /* Now the rest...
+ *
+ * For this case we check if the group is prefixed by the path
+ * given to us, including the trailing slash (but not the leading
+ * one). That means a reset on "/a/" (group "[a]") will match
+ * group "[a/b]" but not will not match group "[another]".
+ */
+ groups = g_key_file_get_groups (kfw->keyfile, NULL);
+ for (i = 0; groups[i]; i++)
+ if (g_str_has_prefix (groups[i], path + 1)) /* remove only leading slash */
+ g_key_file_remove_group (kfw->keyfile, groups[i], NULL);
+ g_strfreev (groups);
+ }
+ else
+ {
+ /* A simple set or reset of a single key. */
+ const gchar *last_slash;
+ gchar *group;
+ gchar *key;
+
+ last_slash = strrchr (path, '/');
+
+ /* If the last slash is the first one then the group will be the
+ * special case: [/]. Otherwise we remove the leading and
+ * trailing slashes.
+ */
+ if (last_slash != path)
+ group = g_strndup (path + 1, last_slash - (path + 1));
+ else
+ group = g_strdup ("/");
+
+ /* Key is the non-empty part following the last slash (we know
+ * that it's non-empty because we dealt with strings ending with
+ * '/' above).
+ */
+ key = g_strdup (last_slash + 1);
+
+ if (value != NULL)
+ {
+ gchar *printed;
+
+ printed = g_variant_print (value, TRUE);
+ g_key_file_set_value (kfw->keyfile, group, key, printed);
+ g_free (printed);
+ }
+ else
+ g_key_file_remove_key (kfw->keyfile, group, key, NULL);
+
+ g_free (group);
+ g_free (key);
+ }
+
+ g_free (path);
+ }
+}
+
+static gboolean
+dconf_keyfile_writer_commit (DConfWriter *writer,
+ GError **error)
+{
+ DConfKeyfileWriter *kfw = (DConfKeyfileWriter *) writer;
+
+ /* Pretty simple. Write the keyfile. */
+ {
+ gchar *data;
+ gsize size;
+
+ /* docs say: "Note that this function never reports an error" */
+ data = g_key_file_to_data (kfw->keyfile, &size, NULL);
+
+ /* don't write it again if nothing changed */
+ if (!kfw->contents || !g_str_equal (kfw->contents, data))
+ {
+ if (!g_file_set_contents (kfw->filename, data, size, error))
+ {
+ gchar *dirname;
+
+ /* Maybe it failed because the directory doesn't exist. Try
+ * again, after mkdir().
+ */
+ dirname = g_path_get_dirname (kfw->filename);
+ g_mkdir_with_parents (dirname, 0777);
+ g_free (dirname);
+
+ g_clear_error (error);
+ if (!g_file_set_contents (kfw->filename, data, size, error))
+ {
+ g_free (data);
+ return FALSE;
+ }
+ }
+ }
+
+ g_free (data);
+ }
+
+ /* Failing to update the shm file after writing the keyfile is
+ * unlikely to occur. It can only happen if the runtime dir hits
+ * quota.
+ *
+ * If it does happen, we're in a bit of a bad spot because the on-disk
+ * keyfile is now out-of-sync with the contents of the shm file. We
+ * fail the write because the apps will see the old values in the shm
+ * file.
+ *
+ * Meanwhile we keep the on-disk keyfile as-is. The next time we open
+ * it we will notice that it's not in sync with the shm file and we'll
+ * try to merge the two as if the changes were made by an outsider.
+ * Eventually that may succeed... If it doesn't, what can we do?
+ */
+ return DCONF_WRITER_CLASS (dconf_keyfile_writer_parent_class)->commit (writer, error);
+}
+
+static void
+dconf_keyfile_writer_end (DConfWriter *writer)
+{
+ DConfKeyfileWriter *kfw = (DConfKeyfileWriter *) writer;
+
+ DCONF_WRITER_CLASS (dconf_keyfile_writer_parent_class)->end (writer);
+
+ g_clear_pointer (&kfw->keyfile, g_key_file_free);
+ g_clear_pointer (&kfw->contents, g_free);
+ close (kfw->lock_fd);
+ kfw->lock_fd = -1;
+}
+
+static gboolean
+dconf_keyfile_update (gpointer user_data)
+{
+ DConfKeyfileWriter *kfw = user_data;
+
+ if (dconf_keyfile_writer_begin (DCONF_WRITER (kfw), NULL))
+ {
+ dconf_keyfile_writer_commit (DCONF_WRITER (kfw), NULL);
+ dconf_keyfile_writer_end (DCONF_WRITER (kfw));
+ }
+
+ kfw->scheduled_update = 0;
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+dconf_keyfile_writer_finalize (GObject *object)
+{
+ DConfKeyfileWriter *kfw = (DConfKeyfileWriter *) object;
+
+ if (kfw->scheduled_update)
+ g_source_remove (kfw->scheduled_update);
+
+ g_clear_object (&kfw->monitor);
+ g_free (kfw->lock_filename);
+ g_free (kfw->filename);
+
+ G_OBJECT_CLASS (dconf_keyfile_writer_parent_class)->finalize (object);
+}
+
+static void
+dconf_keyfile_writer_init (DConfKeyfileWriter *kfw)
+{
+ dconf_writer_set_basepath (DCONF_WRITER (kfw), "keyfile");
+
+ kfw->lock_fd = -1;
+}
+
+static void
+dconf_keyfile_writer_class_init (DConfWriterClass *class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+ object_class->finalize = dconf_keyfile_writer_finalize;
+
+ class->list = dconf_keyfile_writer_list;
+ class->begin = dconf_keyfile_writer_begin;
+ class->change = dconf_keyfile_writer_change;
+ class->commit = dconf_keyfile_writer_commit;
+ class->end = dconf_keyfile_writer_end;
+}
diff --git a/service/dconf-service.c b/service/dconf-service.c
new file mode 100644
index 0000000..9127472
--- /dev/null
+++ b/service/dconf-service.c
@@ -0,0 +1,346 @@
+/*
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-service.h"
+
+#include "dconf-generated.h"
+#include "dconf-writer.h"
+#include "dconf-blame.h"
+
+#include <glib-unix.h>
+#include <string.h>
+#include <fcntl.h>
+
+typedef GApplicationClass DConfServiceClass;
+typedef struct
+{
+ GApplication parent_instance;
+
+ GIOExtensionPoint *extension_point;
+
+ DConfBlame *blame;
+ GHashTable *writers;
+ GArray *subtree_ids;
+
+ gboolean released;
+} DConfService;
+
+G_DEFINE_TYPE (DConfService, dconf_service, G_TYPE_APPLICATION)
+
+static gboolean
+dconf_service_signalled (gpointer user_data)
+{
+ DConfService *service = user_data;
+
+ if (!service->released)
+ g_application_release (G_APPLICATION (service));
+
+ service->released = TRUE;
+
+ return G_SOURCE_REMOVE;
+}
+
+static gchar **
+string_set_free (GHashTable *set)
+{
+ GHashTableIter iter;
+ gchar **result;
+ gint n_items;
+ gpointer key;
+ gint i = 0;
+
+ n_items = g_hash_table_size (set);
+ result = g_new (gchar *, n_items + 1);
+
+ g_hash_table_iter_init (&iter, set);
+ while (g_hash_table_iter_next (&iter, &key, NULL))
+ {
+ result[i++] = key;
+ g_hash_table_iter_steal (&iter);
+ }
+ result[i] = NULL;
+
+ g_assert_cmpint (n_items, ==, i);
+ g_hash_table_unref (set);
+
+ return result;
+}
+
+static GHashTable *
+string_set_new (void)
+{
+ return g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+}
+
+static void
+string_set_add (GHashTable *set,
+ const gchar *string)
+{
+ g_hash_table_add (set, g_strdup (string));
+}
+
+static GType
+dconf_service_find_writer_type (DConfService *service,
+ const gchar *object_path,
+ GHashTable **writers)
+{
+ GIOExtension *extension;
+ const gchar *path;
+ GHashTable *table;
+
+ path = object_path + strlen ("/ca/desrt/dconf");
+ g_assert (*path == '/');
+ path++;
+
+ extension = g_io_extension_point_get_extension_by_name (service->extension_point, path);
+ g_assert (extension != NULL);
+
+ table = g_hash_table_lookup (service->writers, path);
+ if (table == NULL)
+ {
+ table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+ g_hash_table_insert (service->writers, g_strdup (path), table);
+ }
+
+ *writers = table;
+
+ return g_io_extension_get_type (extension);
+}
+
+static gchar **
+dconf_service_subtree_enumerate (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ gpointer user_data)
+{
+ DConfService *service = user_data;
+ GHashTableIter iter;
+ GHashTable *writers;
+ GType writer_type;
+ GHashTable *set;
+ gpointer key;
+
+ set = string_set_new ();
+ writer_type = dconf_service_find_writer_type (service, object_path, &writers);
+ g_hash_table_iter_init (&iter, writers);
+ while (g_hash_table_iter_next (&iter, &key, NULL))
+ string_set_add (set, key);
+
+ dconf_writer_list (writer_type, set);
+
+ return string_set_free (set);
+}
+
+static GDBusInterfaceInfo **
+dconf_service_subtree_introspect (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *node,
+ gpointer user_data)
+{
+ GDBusInterfaceInfo **result;
+
+ if (node == NULL)
+ return NULL;
+
+ result = g_new (GDBusInterfaceInfo *, 2);
+ result[0] = dconf_dbus_writer_interface_info ();
+ result[1] = NULL;
+
+ return result;
+}
+
+static gpointer
+dconf_service_get_writer (DConfService *service,
+ GDBusConnection *connection,
+ const gchar *base_path,
+ const gchar *name)
+{
+ GDBusInterfaceSkeleton *writer;
+ GHashTable *writers;
+ GType writer_type;
+
+ writer_type = dconf_service_find_writer_type (service, base_path, &writers);
+
+ writer = g_hash_table_lookup (writers, name);
+
+ if (writer == NULL)
+ {
+ GError *error = NULL;
+ gchar *object_path;
+
+ writer = dconf_writer_new (writer_type, name);
+ g_hash_table_insert (writers, g_strdup (name), writer);
+ object_path = g_strjoin ("/", base_path, name, NULL);
+ g_dbus_interface_skeleton_export (writer, connection, object_path, &error);
+ g_assert_no_error (error);
+ g_free (object_path);
+ }
+
+ return writer;
+}
+
+static const GDBusInterfaceVTable *
+dconf_service_subtree_dispatch (GDBusConnection *connection,
+ const gchar *sender,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *node,
+ gpointer *out_user_data,
+ gpointer user_data)
+{
+ DConfService *service = user_data;
+
+ g_assert_cmpstr (interface_name, ==, "ca.desrt.dconf.Writer");
+ g_assert (node != NULL);
+
+ *out_user_data = dconf_service_get_writer (service, connection, object_path, node);
+
+ return g_dbus_interface_skeleton_get_vtable (*out_user_data);
+}
+
+static gboolean
+dconf_service_dbus_register (GApplication *application,
+ GDBusConnection *connection,
+ const gchar *object_path,
+ GError **error)
+{
+ const GDBusSubtreeVTable subtree_vtable = {
+ dconf_service_subtree_enumerate,
+ dconf_service_subtree_introspect,
+ dconf_service_subtree_dispatch
+ };
+ DConfService *service = DCONF_SERVICE (application);
+ GError *local_error = NULL;
+ GList *node;
+ guint id;
+
+ service->extension_point = g_io_extension_point_register ("dconf-backend");
+ g_io_extension_point_set_required_type (service->extension_point, DCONF_TYPE_WRITER);
+ 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);
+
+ service->blame = dconf_blame_get ();
+ if (service->blame)
+ {
+ g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (service->blame),
+ connection, object_path, &local_error);
+ g_assert_no_error (local_error);
+ }
+
+ for (node = g_io_extension_point_get_extensions (service->extension_point); node; node = node->next)
+ {
+ gchar *path;
+
+ path = g_strconcat ("/ca/desrt/dconf/", g_io_extension_get_name (node->data), NULL);
+ id = g_dbus_connection_register_subtree (connection, path, &subtree_vtable,
+ G_DBUS_SUBTREE_FLAGS_DISPATCH_TO_UNENUMERATED_NODES,
+ g_object_ref (service), g_object_unref, &local_error);
+ g_assert_no_error (local_error);
+ g_array_append_vals (service->subtree_ids, &id, 1);
+ g_free (path);
+ }
+
+ return TRUE;
+}
+
+static void
+dconf_service_dbus_unregister (GApplication *application,
+ GDBusConnection *connection,
+ const gchar *object_path)
+{
+ DConfService *service = DCONF_SERVICE (application);
+ gint i;
+
+ if (service->blame)
+ {
+ g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (service->blame));
+ g_object_unref (service->blame);
+ service->blame = NULL;
+ }
+
+ for (i = 0; i < service->subtree_ids->len; i++)
+ g_dbus_connection_unregister_subtree (connection, g_array_index (service->subtree_ids, guint, i));
+ g_array_set_size (service->subtree_ids, 0);
+}
+
+static void
+dconf_service_startup (GApplication *application)
+{
+ DConfService *service = DCONF_SERVICE (application);
+
+ G_APPLICATION_CLASS (dconf_service_parent_class)
+ ->startup (application);
+
+ g_unix_signal_add (SIGTERM, dconf_service_signalled, service);
+ g_unix_signal_add (SIGINT, dconf_service_signalled, service);
+ g_unix_signal_add (SIGHUP, dconf_service_signalled, service);
+
+ g_application_hold (application);
+}
+
+static void
+dconf_service_shutdown (GApplication *application)
+{
+ G_APPLICATION_CLASS (dconf_service_parent_class)
+ ->shutdown (application);
+}
+
+static void
+dconf_service_init (DConfService *service)
+{
+ service->writers = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref);
+ service->subtree_ids = g_array_new (FALSE, TRUE, sizeof (guint));
+}
+
+static void
+dconf_service_finalize (GObject *object)
+{
+ DConfService *service = (DConfService *) object;
+
+ g_assert_cmpint (service->subtree_ids->len, ==, 0);
+ g_array_free (service->subtree_ids, TRUE);
+
+ G_OBJECT_CLASS (dconf_service_parent_class)->finalize (object);
+}
+
+static void
+dconf_service_class_init (GApplicationClass *class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+ object_class->finalize = dconf_service_finalize;
+
+ class->dbus_register = dconf_service_dbus_register;
+ class->dbus_unregister = dconf_service_dbus_unregister;
+ class->startup = dconf_service_startup;
+ class->shutdown = dconf_service_shutdown;
+}
+
+GApplication *
+dconf_service_new (void)
+{
+ return g_object_new (DCONF_TYPE_SERVICE,
+ "application-id", "ca.desrt.dconf",
+ "flags", G_APPLICATION_IS_SERVICE,
+ NULL);
+}
diff --git a/service/dconf-service.h b/service/dconf-service.h
new file mode 100644
index 0000000..36e573f
--- /dev/null
+++ b/service/dconf-service.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __dconf_service_h__
+#define __dconf_service_h__
+
+#include <gio/gio.h>
+
+#define DCONF_TYPE_SERVICE (dconf_service_get_type ())
+#define DCONF_SERVICE(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \
+ DCONF_TYPE_SERVICE, DConfService))
+#define DCONF_IS_SERVICE(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \
+ DCONF_TYPE_SERVICE))
+
+GType dconf_service_get_type (void);
+GApplication * dconf_service_new (void);
+
+#endif /* __dconf_service_h__ */
diff --git a/service/dconf-shm-writer.c b/service/dconf-shm-writer.c
new file mode 100644
index 0000000..db3dfcd
--- /dev/null
+++ b/service/dconf-shm-writer.c
@@ -0,0 +1,45 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-writer.h"
+
+typedef DConfWriterClass DConfShmWriterClass;
+typedef DConfWriter DConfShmWriter;
+
+G_DEFINE_TYPE (DConfShmWriter, dconf_shm_writer, DCONF_TYPE_WRITER)
+
+static void
+dconf_shm_writer_list (GHashTable *set)
+{
+}
+
+static void
+dconf_shm_writer_init (DConfWriter *writer)
+{
+ dconf_writer_set_basepath (writer, "shm");
+}
+
+static void
+dconf_shm_writer_class_init (DConfWriterClass *class)
+{
+ class->list = dconf_shm_writer_list;
+}
diff --git a/service/dconf-writer.c b/service/dconf-writer.c
new file mode 100644
index 0000000..26f66dd
--- /dev/null
+++ b/service/dconf-writer.c
@@ -0,0 +1,428 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-writer.h"
+
+#include "../shm/dconf-shm.h"
+#include "dconf-gvdb-utils.h"
+#include "dconf-generated.h"
+#include "dconf-blame.h"
+
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <stdio.h>
+
+struct _DConfWriterPrivate
+{
+ gchar *filename;
+ gboolean native;
+ gchar *basepath;
+ gchar *name;
+ guint64 tag;
+ gboolean need_write;
+
+ DConfChangeset *uncommited_values;
+ DConfChangeset *commited_values;
+
+ GQueue uncommited_changes;
+ GQueue commited_changes;
+};
+
+typedef struct
+{
+ DConfChangeset *changeset;
+ gchar *tag;
+} TaggedChange;
+
+static void dconf_writer_iface_init (DConfDBusWriterIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (DConfWriter, dconf_writer, DCONF_DBUS_TYPE_WRITER_SKELETON,
+ G_ADD_PRIVATE (DConfWriter)
+ G_IMPLEMENT_INTERFACE (DCONF_DBUS_TYPE_WRITER, dconf_writer_iface_init))
+
+static void
+dconf_writer_real_list (GHashTable *set)
+{
+ const gchar *name;
+ gchar *dirname;
+ GDir *dir;
+
+ dirname = g_build_filename (g_get_user_config_dir (), "dconf", NULL);
+ dir = g_dir_open (dirname, 0, NULL);
+
+ if (!dir)
+ return;
+
+ while ((name = g_dir_read_name (dir)))
+ {
+ if (!strchr (name, '.'))
+ g_hash_table_add (set, g_strdup (name));
+ }
+
+ g_dir_close (dir);
+}
+
+static gchar *
+dconf_writer_get_tag (DConfWriter *writer)
+{
+ GDBusConnection *connection;
+
+ connection = g_dbus_interface_skeleton_get_connection (G_DBUS_INTERFACE_SKELETON (writer));
+
+ return g_strdup_printf ("%s:%s:%" G_GUINT64_FORMAT,
+ g_dbus_connection_get_unique_name (connection),
+ writer->priv->name, writer->priv->tag++);
+}
+
+static gboolean
+dconf_writer_real_begin (DConfWriter *writer,
+ GError **error)
+{
+ /* If this is the first time, populate the value table with the
+ * existing values.
+ */
+ if (writer->priv->commited_values == NULL)
+ {
+ gboolean missing;
+
+ writer->priv->commited_values = dconf_gvdb_utils_read_and_back_up_file (writer->priv->filename, &missing, error);
+
+ if (!writer->priv->commited_values)
+ return FALSE;
+
+ /* If this is a non-native writer and the file doesn't exist, we
+ * will need to write it on commit so that the client can open it.
+ */
+ if (missing && !writer->priv->native)
+ writer->priv->need_write = TRUE;
+ }
+
+ writer->priv->uncommited_values = dconf_changeset_new_database (writer->priv->commited_values);
+
+ return TRUE;
+}
+
+static void
+dconf_writer_real_change (DConfWriter *writer,
+ DConfChangeset *changeset,
+ const gchar *tag)
+{
+ g_return_if_fail (writer->priv->uncommited_values != NULL);
+
+ dconf_changeset_change (writer->priv->uncommited_values, changeset);
+
+ if (tag)
+ {
+ TaggedChange *change;
+
+ change = g_slice_new (TaggedChange);
+ change->changeset = dconf_changeset_ref (changeset);
+ change->tag = g_strdup (tag);
+
+ g_queue_push_tail (&writer->priv->uncommited_changes, change);
+ }
+
+ writer->priv->need_write = TRUE;
+}
+
+static gboolean
+dconf_writer_real_commit (DConfWriter *writer,
+ GError **error)
+{
+ gint invalidate_fd = -1;
+
+ if (!writer->priv->need_write)
+ {
+ g_assert (g_queue_is_empty (&writer->priv->uncommited_changes));
+ g_assert (g_queue_is_empty (&writer->priv->commited_changes));
+ dconf_changeset_unref (writer->priv->uncommited_values);
+ writer->priv->uncommited_values = NULL;
+
+ return TRUE;
+ }
+
+ if (!writer->priv->native)
+ /* If it fails, it doesn't matter... */
+ invalidate_fd = open (writer->priv->filename, O_WRONLY);
+
+ if (!dconf_gvdb_utils_write_file (writer->priv->filename, writer->priv->uncommited_values, error))
+ return FALSE;
+
+ if (writer->priv->native)
+ dconf_shm_flag (writer->priv->name);
+
+ if (invalidate_fd != -1)
+ {
+ write (invalidate_fd, "\0\0\0\0\0\0\0\0", 8);
+ close (invalidate_fd);
+ }
+
+ if (writer->priv->commited_values)
+ dconf_changeset_unref (writer->priv->commited_values);
+ writer->priv->commited_values = writer->priv->uncommited_values;
+ writer->priv->uncommited_values = NULL;
+
+ {
+ GQueue empty_queue = G_QUEUE_INIT;
+
+ g_assert (g_queue_is_empty (&writer->priv->commited_changes));
+ writer->priv->commited_changes = writer->priv->uncommited_changes;
+ writer->priv->uncommited_changes = empty_queue;
+ }
+
+ return TRUE;
+}
+
+static void
+dconf_writer_real_end (DConfWriter *writer)
+{
+ while (!g_queue_is_empty (&writer->priv->uncommited_changes))
+ {
+ TaggedChange *change = g_queue_pop_head (&writer->priv->uncommited_changes);
+ dconf_changeset_unref (change->changeset);
+ g_free (change->tag);
+ g_slice_free (TaggedChange, change);
+ }
+
+ while (!g_queue_is_empty (&writer->priv->commited_changes))
+ {
+ TaggedChange *change = g_queue_pop_head (&writer->priv->commited_changes);
+ const gchar *prefix;
+ const gchar * const *paths;
+ guint n;
+
+ n = dconf_changeset_describe (change->changeset, &prefix, &paths, NULL);
+ g_assert (n != 0);
+ dconf_dbus_writer_emit_notify_signal (DCONF_DBUS_WRITER (writer), prefix, paths, change->tag);
+ dconf_changeset_unref (change->changeset);
+ g_free (change->tag);
+ g_slice_free (TaggedChange, change);
+ }
+
+ g_clear_pointer (&writer->priv->uncommited_values, dconf_changeset_unref);
+}
+
+static gboolean
+dconf_writer_begin (DConfWriter *writer,
+ GError **error)
+{
+ return DCONF_WRITER_GET_CLASS (writer)->begin (writer, error);
+}
+
+static void
+dconf_writer_change (DConfWriter *writer,
+ DConfChangeset *changeset,
+ const gchar *tag)
+{
+ DCONF_WRITER_GET_CLASS (writer)->change (writer, changeset, tag);
+}
+
+static gboolean
+dconf_writer_commit (DConfWriter *writer,
+ GError **error)
+{
+ return DCONF_WRITER_GET_CLASS (writer)->commit (writer, error);
+}
+
+static void
+dconf_writer_end (DConfWriter *writer)
+{
+ return DCONF_WRITER_GET_CLASS (writer)->end (writer);
+}
+
+static void
+dconf_writer_complete_invocation (DConfDBusWriter *dbus_writer,
+ GDBusMethodInvocation *invocation,
+ GVariant *result,
+ GError *error)
+{
+ if (error)
+ {
+ g_dbus_method_invocation_return_gerror (invocation, error);
+ g_error_free (error);
+ }
+ else
+ g_dbus_method_invocation_return_value (invocation, result);
+}
+
+static gboolean
+dconf_writer_handle_init (DConfDBusWriter *dbus_writer,
+ GDBusMethodInvocation *invocation)
+{
+ DConfWriter *writer = DCONF_WRITER (dbus_writer);
+ GError *error = NULL;
+
+ dconf_blame_record (invocation);
+
+ if (dconf_writer_begin (writer, &error))
+ dconf_writer_commit (writer, &error);
+
+ dconf_writer_complete_invocation (dbus_writer, invocation, NULL, error);
+ dconf_writer_end (writer);
+
+ return TRUE;
+}
+
+static gboolean
+dconf_writer_handle_change (DConfDBusWriter *dbus_writer,
+ GDBusMethodInvocation *invocation,
+ GVariant *blob)
+{
+ DConfWriter *writer = DCONF_WRITER (dbus_writer);
+ DConfChangeset *changeset;
+ GError *error = NULL;
+ GVariant *tmp, *args, *result = NULL;
+ gchar *tag;
+
+ dconf_blame_record (invocation);
+
+ tmp = g_variant_new_from_data (G_VARIANT_TYPE ("a{smv}"),
+ g_variant_get_data (blob), g_variant_get_size (blob), FALSE,
+ (GDestroyNotify) g_variant_unref, g_variant_ref (blob));
+ g_variant_ref_sink (tmp);
+ args = g_variant_get_normal_form (tmp);
+ g_variant_unref (tmp);
+
+ changeset = dconf_changeset_deserialise (args);
+ g_variant_unref (args);
+
+ tag = dconf_writer_get_tag (writer);
+
+ /* Don't bother with empty changesets... */
+ if (dconf_changeset_describe (changeset, NULL, NULL, NULL))
+ {
+ if (!dconf_writer_begin (writer, &error))
+ goto out;
+
+ dconf_writer_change (writer, changeset, tag);
+
+ if (!dconf_writer_commit (writer, &error))
+ goto out;
+ }
+
+out:
+ if (!error)
+ result = g_variant_new ("(s)", tag);
+
+ dconf_changeset_unref (changeset);
+ g_free (tag);
+
+ dconf_writer_complete_invocation (dbus_writer, invocation, result, error);
+ dconf_writer_end (writer);
+
+ return TRUE;
+}
+
+static void
+dconf_writer_iface_init (DConfDBusWriterIface *iface)
+{
+ iface->handle_init = dconf_writer_handle_init;
+ iface->handle_change = dconf_writer_handle_change;
+}
+
+static void
+dconf_writer_init (DConfWriter *writer)
+{
+ writer->priv = dconf_writer_get_instance_private (writer);
+ writer->priv->basepath = g_build_filename (g_get_user_config_dir (), "dconf", NULL);
+ writer->priv->native = TRUE;
+}
+
+static void
+dconf_writer_set_property (GObject *object, guint prop_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ DConfWriter *writer = DCONF_WRITER (object);
+
+ g_assert_cmpint (prop_id, ==, 1);
+
+ g_assert (!writer->priv->name);
+ writer->priv->name = g_value_dup_string (value);
+
+ writer->priv->filename = g_build_filename (writer->priv->basepath, writer->priv->name, NULL);
+}
+
+static void
+dconf_writer_class_init (DConfWriterClass *class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (class);
+
+ object_class->set_property = dconf_writer_set_property;
+
+ class->begin = dconf_writer_real_begin;
+ class->change = dconf_writer_real_change;
+ class->commit = dconf_writer_real_commit;
+ class->end = dconf_writer_real_end;
+ class->list = dconf_writer_real_list;
+
+ g_object_class_install_property (object_class, 1,
+ g_param_spec_string ("name", "name", "name", NULL,
+ G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_WRITABLE));
+}
+
+void
+dconf_writer_set_basepath (DConfWriter *writer,
+ const gchar *name)
+{
+ g_free (writer->priv->basepath);
+ writer->priv->basepath = g_build_filename (g_get_user_runtime_dir (), "dconf-service", name, NULL);
+ writer->priv->native = FALSE;
+}
+
+DConfChangeset *
+dconf_writer_diff (DConfWriter *writer,
+ DConfChangeset *changeset)
+{
+ return dconf_changeset_diff (writer->priv->uncommited_values, changeset);
+}
+
+const gchar *
+dconf_writer_get_name (DConfWriter *writer)
+{
+ return writer->priv->name;
+}
+
+void
+dconf_writer_list (GType type,
+ GHashTable *set)
+{
+ DConfWriterClass *class;
+
+ g_return_if_fail (g_type_is_a (type, DCONF_TYPE_WRITER));
+
+ class = g_type_class_ref (type);
+ class->list (set);
+ g_type_class_unref (class);
+}
+
+GDBusInterfaceSkeleton *
+dconf_writer_new (GType type,
+ const gchar *name)
+{
+ g_return_val_if_fail (g_type_is_a (type, DCONF_TYPE_WRITER), NULL);
+
+ return g_object_new (type, "name", name, NULL);
+}
diff --git a/service/dconf-writer.h b/service/dconf-writer.h
new file mode 100644
index 0000000..17360c9
--- /dev/null
+++ b/service/dconf-writer.h
@@ -0,0 +1,90 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __dconf_writer_h__
+#define __dconf_writer_h__
+
+#include <glib.h>
+#include <gio/gio.h>
+#include <gobject/gobject.h>
+
+#include "../common/dconf-changeset.h"
+#include "dconf-generated.h"
+
+#define DCONF_TYPE_WRITER (dconf_writer_get_type ())
+#define DCONF_WRITER(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \
+ DCONF_TYPE_WRITER, DConfWriter))
+#define DCONF_WRITER_CLASS(class) (G_TYPE_CHECK_CLASS_CAST ((class), \
+ DCONF_TYPE_WRITER, DConfWriterClass))
+#define DCONF_IS_WRITER(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \
+ DCONF_TYPE_WRITER))
+#define DCONF_IS_WRITER_CLASS(class) (G_TYPE_CHECK_CLASS_TYPE ((class), \
+ DCONF_TYPE_WRITER))
+#define DCONF_WRITER_GET_CLASS(inst) (G_TYPE_INSTANCE_GET_CLASS ((inst), \
+ DCONF_TYPE_WRITER, DConfWriterClass))
+
+typedef struct _DConfWriterPrivate DConfWriterPrivate;
+typedef struct _DConfWriterClass DConfWriterClass;
+typedef struct _DConfWriter DConfWriter;
+
+struct _DConfWriterClass
+{
+ DConfDBusWriterSkeletonClass parent_instance;
+
+ /* static methods */
+ void (* list) (GHashTable *set);
+
+ /* instance methods */
+ gboolean (* begin) (DConfWriter *writer,
+ GError **error);
+ void (* change) (DConfWriter *writer,
+ DConfChangeset *changeset,
+ const gchar *tag);
+ gboolean (* commit) (DConfWriter *writer,
+ GError **error);
+ void (* end) (DConfWriter *writer);
+};
+
+struct _DConfWriter
+{
+ DConfDBusWriterSkeleton parent_instance;
+ DConfWriterPrivate *priv;
+};
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (DConfWriter, g_object_unref)
+
+GType dconf_writer_get_type (void);
+
+void dconf_writer_set_basepath (DConfWriter *writer,
+ const gchar *name);
+DConfChangeset * dconf_writer_diff (DConfWriter *writer,
+ DConfChangeset *changeset);
+const gchar * dconf_writer_get_name (DConfWriter *writer);
+
+void dconf_writer_list (GType type,
+ GHashTable *set);
+GDBusInterfaceSkeleton *dconf_writer_new (GType type,
+ const gchar *name);
+
+#define DCONF_TYPE_SHM_WRITER (dconf_shm_writer_get_type ())
+GType dconf_shm_writer_get_type (void);
+#define DCONF_TYPE_KEYFILE_WRITER (dconf_keyfile_writer_get_type ())
+GType dconf_keyfile_writer_get_type (void);
+
+#endif /* __dconf_writer_h__ */
diff --git a/service/main.c b/service/main.c
new file mode 100644
index 0000000..7824dd0
--- /dev/null
+++ b/service/main.c
@@ -0,0 +1,35 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-service.h"
+
+int
+main (int argc, char **argv)
+{
+ GApplication *app;
+ gint status;
+
+ app = dconf_service_new ();
+ status = g_application_run (app, argc, argv);
+ g_object_unref (app);
+
+ return status;
+}
diff --git a/service/meson.build b/service/meson.build
new file mode 100644
index 0000000..d92b982
--- /dev/null
+++ b/service/meson.build
@@ -0,0 +1,63 @@
+service_conf = configuration_data()
+service_conf.set('libexecdir', dconf_libexecdir)
+
+service = dconf_namespace + '.service'
+
+configure_file(
+ input: service + '.in',
+ output: service,
+ configuration: service_conf,
+ install: true,
+ install_dir: dbus_session_service_dir,
+)
+
+lib_sources = [
+ 'dconf-blame.c',
+ 'dconf-gvdb-utils.c',
+ 'dconf-keyfile-writer.c',
+ 'dconf-service.c',
+ 'dconf-shm-writer.c',
+ 'dconf-writer.c',
+]
+sources = [
+ 'main.c',
+]
+
+dconf_generated = gnome.gdbus_codegen(
+ 'dconf-generated',
+ dconf_namespace + '.xml',
+ interface_prefix: dconf_namespace + '.',
+ namespace: 'DConfDBus',
+)
+
+lib_sources += dconf_generated
+
+libdconf_service = static_library(
+ 'dconf-service',
+ sources: lib_sources,
+ include_directories: top_inc,
+ c_args: dconf_c_args,
+ dependencies: gio_unix_dep,
+ link_with: [
+ libdconf_common,
+ libdconf_shm,
+ libgvdb,
+ ],
+)
+
+libdconf_service_dep = declare_dependency(
+ link_with: libdconf_service,
+ dependencies: gio_unix_dep,
+ sources: dconf_generated,
+)
+
+dconf_service = executable(
+ 'dconf-service',
+ sources,
+ include_directories: top_inc,
+ c_args: dconf_c_args,
+ dependencies: gio_unix_dep,
+ link_with: libdconf_service,
+ install: true,
+ install_dir: dconf_libexecdir,
+)
diff --git a/shm/dconf-shm-mockable.c b/shm/dconf-shm-mockable.c
new file mode 100644
index 0000000..6adf7d5
--- /dev/null
+++ b/shm/dconf-shm-mockable.c
@@ -0,0 +1,40 @@
+/*
+ * Copyright © 2019 Daniel Playfair Cal
+ *
+ * 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/>.
+ *
+ * Author: Daniel Playfair Cal <daniel.playfair.cal@gmail.com>
+ */
+
+/**
+ * This module contains the production implementations of methods used in
+ * dconf_shm that need to be mocked out for tests.
+ *
+ * In some cases, external methods are wrapped with a different name. This is
+ * done so that it is not necessary to redefine the external functions in
+ * unit tests in order to mock them out, and therefore easy to also call the
+ * non mocked versions in tests if necessary.
+ */
+
+#include "config.h"
+
+#include "dconf-shm-mockable.h"
+
+#include <unistd.h>
+
+ssize_t
+dconf_shm_pwrite (int fd, const void *buf, size_t count, off_t offset)
+{
+ return pwrite (fd, buf, count, offset);
+}
diff --git a/shm/dconf-shm-mockable.h b/shm/dconf-shm-mockable.h
new file mode 100644
index 0000000..98ff33f
--- /dev/null
+++ b/shm/dconf-shm-mockable.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright © 2019 Daniel Playfair Cal
+ *
+ * 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/>.
+ *
+ * Author: Daniel Playfair Cal <daniel.playfair.cal@gmail.com>
+ */
+
+#ifndef __dconf_shm_mockable_h__
+#define __dconf_shm_mockable_h__
+
+#include <glib.h>
+
+G_GNUC_INTERNAL
+ssize_t dconf_shm_pwrite (int fd,
+ const void *buf,
+ size_t count,
+ off_t offset);
+
+#endif /* __dconf_shm_mockable_h__ */
diff --git a/shm/dconf-shm.c b/shm/dconf-shm.c
new file mode 100644
index 0000000..dbde759
--- /dev/null
+++ b/shm/dconf-shm.c
@@ -0,0 +1,155 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "dconf-shm.h"
+#include "dconf-shm-mockable.h"
+
+#include <sys/mman.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+static gchar *
+dconf_shm_get_shmdir (void)
+{
+ static gchar *shmdir;
+
+ if (g_once_init_enter (&shmdir))
+ g_once_init_leave (&shmdir, g_build_filename (g_get_user_runtime_dir (), "dconf", NULL));
+
+ return shmdir;
+}
+
+void
+dconf_shm_close (guint8 *shm)
+{
+ if (shm)
+ munmap (shm, 1);
+}
+
+guint8 *
+dconf_shm_open (const gchar *name)
+{
+ const gchar *shmdir;
+ gchar *filename;
+ void *memory;
+ gint fd;
+
+ shmdir = dconf_shm_get_shmdir ();
+ filename = g_build_filename (shmdir, name, NULL);
+ memory = NULL;
+ fd = -1;
+
+ if (g_mkdir_with_parents (shmdir, 0700) != 0)
+ {
+ g_critical ("unable to create directory '%s': %s. dconf will not work properly.", shmdir, g_strerror (errno));
+ goto out;
+ }
+
+ fd = open (filename, O_RDWR | O_CREAT, 0600);
+ if (fd == -1)
+ {
+ g_critical ("unable to create file '%s': %s. dconf will not work properly.", filename, g_strerror (errno));
+ goto out;
+ }
+
+ /* ftruncate(fd, 1) is not sufficient because it does not actually
+ * ensure that the space is available (which could give a SIGBUS
+ * later).
+ *
+ * posix_fallocate() is also problematic because it is implemented in
+ * a racy way in the libc if unavailable for a particular filesystem
+ * (as is the case for tmpfs, which is where we probably are).
+ *
+ * By writing to the second byte in the file we ensure we don't
+ * overwrite the first byte (which is the one we care about).
+ */
+ if (dconf_shm_pwrite (fd, "", 1, 1) != 1)
+ {
+ g_critical ("failed to allocate file '%s': %s. dconf will not work properly.", filename, g_strerror (errno));
+ goto out;
+ }
+
+ memory = mmap (NULL, 1, PROT_READ, MAP_SHARED, fd, 0);
+ g_assert (memory != MAP_FAILED);
+ g_assert (memory != NULL);
+
+ out:
+ g_free (filename);
+ close (fd);
+
+ return memory;
+}
+
+void
+dconf_shm_flag (const gchar *name)
+{
+ const gchar *shmdir;
+ gchar *filename;
+ gint fd;
+
+ shmdir = dconf_shm_get_shmdir ();
+ filename = g_build_filename (shmdir, name, NULL);
+
+ /* We need O_RDWR for PROT_WRITE.
+ *
+ * This is probably due to the fact that some architectures can't make
+ * write-only mappings (so they end up being readable as well).
+ */
+ fd = open (filename, O_RDWR);
+ if (fd >= 0)
+ {
+ /* In theory we could have opened the file after a client created
+ * it but before they called pwrite(). Do the pwrite() ourselves
+ * to make sure (so we don't get SIGBUS in a moment).
+ *
+ * If this fails then it will probably fail for the client too.
+ * If it doesn't then there's not really much we can do...
+ */
+ if (dconf_shm_pwrite (fd, "", 1, 1) == 1)
+ {
+ guint8 *shm;
+
+ /* It would have been easier for us to do write(fd, "\1", 1);
+ * but this causes problems on kernels (ie: OpenBSD) that
+ * don't sync up their filesystem cache with mmap()ed regions.
+ *
+ * Using mmap() works everywhere.
+ *
+ * See https://bugzilla.gnome.org/show_bug.cgi?id=687334 about
+ * why we need to have PROT_READ even though we only write.
+ */
+ shm = mmap (NULL, 1, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
+ g_assert (shm != MAP_FAILED);
+
+ *shm = 1;
+
+ munmap (shm, 1);
+ }
+
+ close (fd);
+
+ unlink (filename);
+ }
+
+ g_free (filename);
+}
diff --git a/shm/dconf-shm.h b/shm/dconf-shm.h
new file mode 100644
index 0000000..2c57297
--- /dev/null
+++ b/shm/dconf-shm.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright © 2010 Codethink Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#ifndef __dconf_shm_h__
+#define __dconf_shm_h__
+
+#include <glib.h>
+
+G_GNUC_INTERNAL
+guint8 * dconf_shm_open (const gchar *name);
+G_GNUC_INTERNAL
+void dconf_shm_close (guint8 *shm);
+G_GNUC_INTERNAL
+void dconf_shm_flag (const gchar *name);
+
+static inline gboolean
+dconf_shm_is_flagged (const guint8 *shm)
+{
+ return shm == NULL || *shm != 0;
+}
+
+#endif /* __dconf_shm_h__ */
diff --git a/shm/meson.build b/shm/meson.build
new file mode 100644
index 0000000..07f77d0
--- /dev/null
+++ b/shm/meson.build
@@ -0,0 +1,33 @@
+sources = files(
+ 'dconf-shm.c',
+ 'dconf-shm-mockable.c',
+)
+
+libdconf_shm = static_library(
+ 'dconf-shm',
+ sources: sources,
+ include_directories: top_inc,
+ dependencies: glib_dep,
+ c_args: dconf_c_args,
+ pic: true,
+)
+
+libdconf_shm_dep = declare_dependency(
+ dependencies: glib_dep,
+ link_with: libdconf_shm,
+)
+
+
+libdconf_shm_test = static_library(
+ 'dconf-shm-test',
+ sources: 'dconf-shm.c',
+ include_directories: top_inc,
+ dependencies: glib_dep,
+ c_args: dconf_c_args,
+ pic: true,
+)
+
+libdconf_shm_test_dep = declare_dependency(
+ dependencies: glib_dep,
+ link_with: libdconf_shm,
+)
diff --git a/tests/changeset.c b/tests/changeset.c
new file mode 100644
index 0000000..5f046df
--- /dev/null
+++ b/tests/changeset.c
@@ -0,0 +1,589 @@
+#include "../common/dconf-changeset.h"
+
+static gboolean
+should_not_run (const gchar *key,
+ GVariant *value,
+ gpointer user_data)
+{
+ g_assert_not_reached ();
+}
+
+static gboolean
+is_null (const gchar *key,
+ GVariant *value,
+ gpointer user_data)
+{
+ return value == NULL;
+}
+
+static gboolean
+is_not_null (const gchar *key,
+ GVariant *value,
+ gpointer user_data)
+{
+ return value != NULL;
+}
+
+static void
+test_basic (void)
+{
+ DConfChangeset *changeset;
+ gboolean result;
+ GVariant *value;
+ gint n_items;
+
+ changeset = dconf_changeset_new ();
+ dconf_changeset_ref (changeset);
+ dconf_changeset_all (changeset, should_not_run, NULL);
+ n_items = dconf_changeset_describe (changeset, NULL, NULL, NULL);
+ g_assert_cmpint (n_items, ==, 0);
+ dconf_changeset_unref (changeset);
+ dconf_changeset_unref (changeset);
+
+ changeset = dconf_changeset_new_write ("/value/a", NULL);
+ result = dconf_changeset_all (changeset, is_null, NULL);
+ g_assert (result);
+ result = dconf_changeset_all (changeset, is_not_null, NULL);
+ g_assert (!result);
+
+ result = dconf_changeset_get (changeset, "/value/a", &value);
+ g_assert (result);
+ g_assert (value == NULL);
+
+ result = dconf_changeset_get (changeset, "/value/b", &value);
+ g_assert (!result);
+
+ dconf_changeset_set (changeset, "/value/b", g_variant_new_int32 (123));
+ result = dconf_changeset_all (changeset, is_null, NULL);
+ g_assert (!result);
+ result = dconf_changeset_all (changeset, is_not_null, NULL);
+ g_assert (!result);
+
+ result = dconf_changeset_get (changeset, "/value/a", &value);
+ g_assert (result);
+ g_assert (value == NULL);
+
+ result = dconf_changeset_get (changeset, "/value/b", &value);
+ g_assert (result);
+ g_assert_cmpint (g_variant_get_int32 (value), ==, 123);
+ g_variant_unref (value);
+
+ dconf_changeset_set (changeset, "/value/a", g_variant_new_string ("a string"));
+ result = dconf_changeset_all (changeset, is_null, NULL);
+ g_assert (!result);
+ result = dconf_changeset_all (changeset, is_not_null, NULL);
+ g_assert (result);
+
+ result = dconf_changeset_get (changeset, "/value/a", &value);
+ g_assert (result);
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "a string");
+ g_variant_unref (value);
+
+ result = dconf_changeset_get (changeset, "/value/b", &value);
+ g_assert (result);
+ g_assert_cmpint (g_variant_get_int32 (value), ==, 123);
+ g_variant_unref (value);
+
+ dconf_changeset_unref (changeset);
+}
+
+static void
+test_similarity (void)
+{
+ DConfChangeset *a, *b;
+
+ a = dconf_changeset_new ();
+ b = dconf_changeset_new ();
+
+ g_assert (dconf_changeset_is_similar_to (a, b));
+ g_assert (dconf_changeset_is_similar_to (b, a));
+
+ dconf_changeset_set (a, "/value/a", g_variant_new_int32 (0));
+ g_assert (!dconf_changeset_is_similar_to (a, b));
+ g_assert (!dconf_changeset_is_similar_to (b, a));
+
+ /* different values for the same key are still the same */
+ dconf_changeset_set (b, "/value/a", g_variant_new_int32 (1));
+ g_assert (dconf_changeset_is_similar_to (a, b));
+ g_assert (dconf_changeset_is_similar_to (b, a));
+
+ /* make sure even a NULL is counted as different */
+ dconf_changeset_set (a, "/value/b", NULL);
+ g_assert (!dconf_changeset_is_similar_to (a, b));
+ g_assert (!dconf_changeset_is_similar_to (b, a));
+
+ dconf_changeset_set (b, "/value/b", NULL);
+ g_assert (dconf_changeset_is_similar_to (a, b));
+ g_assert (dconf_changeset_is_similar_to (b, a));
+
+ /* different types are still the same */
+ dconf_changeset_set (b, "/value/a", g_variant_new_uint32 (222));
+ g_assert (dconf_changeset_is_similar_to (a, b));
+ g_assert (dconf_changeset_is_similar_to (b, a));
+
+ dconf_changeset_set (a, "/value/c", NULL);
+ dconf_changeset_set (b, "/value/d", NULL);
+ g_assert (!dconf_changeset_is_similar_to (a, b));
+ g_assert (!dconf_changeset_is_similar_to (b, a));
+
+ dconf_changeset_unref (a);
+ dconf_changeset_unref (b);
+}
+
+static void
+test_describe (void)
+{
+ DConfChangeset *changeset;
+ const gchar * const *keys;
+ GVariant * const *values;
+ const gchar *prefix;
+ gint n_items;
+ gint i;
+
+ /* test zero items */
+ changeset = dconf_changeset_new ();
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, 0);
+ dconf_changeset_unref (changeset);
+
+ /* test one NULL item */
+ changeset = dconf_changeset_new_write ("/value/a", NULL);
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, 1);
+ g_assert_cmpstr (prefix, ==, "/value/a");
+ g_assert_cmpstr (keys[0], ==, "");
+ g_assert (keys[1] == NULL);
+ g_assert (values[0] == NULL);
+
+
+ /* Check again */
+ prefix = NULL;
+ keys = NULL;
+ values = NULL;
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, 1);
+ g_assert_cmpstr (prefix, ==, "/value/a");
+ g_assert_cmpstr (keys[0], ==, "");
+ g_assert (keys[1] == NULL);
+ g_assert (values[0] == NULL);
+ dconf_changeset_unref (changeset);
+
+ /* test one non-NULL item */
+ changeset = dconf_changeset_new_write ("/value/a", g_variant_new_int32 (55));
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, 1);
+ g_assert_cmpstr (prefix, ==, "/value/a");
+ g_assert_cmpstr (keys[0], ==, "");
+ g_assert (keys[1] == NULL);
+ g_assert_cmpint (g_variant_get_int32 (values[0]), ==, 55);
+ dconf_changeset_unref (changeset);
+
+ /* test many items */
+ changeset = dconf_changeset_new ();
+ for (i = 0; i < 100; i++)
+ {
+ gchar key[80];
+
+ g_snprintf (key, sizeof key, "/test/value/%2d", i);
+
+ dconf_changeset_set (changeset, key, g_variant_new_int32 (i));
+ }
+
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, i);
+ g_assert_cmpstr (prefix, ==, "/test/value/");
+ for (i = 0; i < 100; i++)
+ {
+ gchar key[80];
+
+ g_snprintf (key, sizeof key, "%2d", i);
+
+ g_assert_cmpstr (keys[i], ==, key);
+ g_assert_cmpint (g_variant_get_int32 (values[i]), ==, i);
+ }
+ g_assert (keys[n_items] == NULL);
+ dconf_changeset_unref (changeset);
+
+ /* test many items with common names */
+ changeset = dconf_changeset_new ();
+ for (i = 0; i < 100; i++)
+ {
+ gchar key[80];
+
+ g_snprintf (key, sizeof key, "/test/value/aaa%02d", i);
+
+ dconf_changeset_set (changeset, key, g_variant_new_int32 (i));
+ }
+
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, i);
+ g_assert_cmpstr (prefix, ==, "/test/value/");
+ for (i = 0; i < 100; i++)
+ {
+ gchar key[80];
+
+ g_snprintf (key, sizeof key, "aaa%02d", i);
+
+ g_assert_cmpstr (keys[i], ==, key);
+ g_assert_cmpint (g_variant_get_int32 (values[i]), ==, i);
+ }
+ g_assert (keys[n_items] == NULL);
+ dconf_changeset_unref (changeset);
+
+ /* test several values in different directories */
+ changeset = dconf_changeset_new ();
+ dconf_changeset_set (changeset, "/value/reset/", NULL);
+ dconf_changeset_set (changeset, "/value/int/a", g_variant_new_int32 (123));
+ dconf_changeset_set (changeset, "/value/string", g_variant_new_string ("bar"));
+ dconf_changeset_set (changeset, "/value/string/a", g_variant_new_string ("foo"));
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, 4);
+ g_assert_cmpstr (prefix, ==, "/value/");
+ g_assert_cmpstr (keys[0], ==, "int/a");
+ g_assert_cmpint (g_variant_get_int32 (values[0]), ==, 123);
+ g_assert_cmpstr (keys[1], ==, "reset/");
+ g_assert (values[1] == NULL);
+ g_assert_cmpstr (keys[2], ==, "string");
+ g_assert_cmpstr (g_variant_get_string (values[2], NULL), ==, "bar");
+ g_assert_cmpstr (keys[3], ==, "string/a");
+ g_assert_cmpstr (g_variant_get_string (values[3], NULL), ==, "foo");
+ g_assert (keys[4] == NULL);
+ dconf_changeset_unref (changeset);
+
+ /* test a couple of values in very different directories */
+ changeset = dconf_changeset_new_write ("/a/deep/directory/", NULL);
+ dconf_changeset_set (changeset, "/another/deep/directory/", NULL);
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, 2);
+ g_assert_cmpstr (prefix, ==, "/");
+ g_assert_cmpstr (keys[0], ==, "a/deep/directory/");
+ g_assert_cmpstr (keys[1], ==, "another/deep/directory/");
+ g_assert (keys[2] == NULL);
+ g_assert (values[0] == NULL);
+ g_assert (values[1] == NULL);
+ dconf_changeset_unref (changeset);
+
+ /* one more similar case, but with the first letter different */
+ changeset = dconf_changeset_new_write ("/deep/directory/", NULL);
+ dconf_changeset_set (changeset, "/another/deep/directory/", NULL);
+ n_items = dconf_changeset_describe (changeset, &prefix, &keys, &values);
+ g_assert_cmpint (n_items, ==, 2);
+ g_assert_cmpstr (prefix, ==, "/");
+ g_assert_cmpstr (keys[0], ==, "another/deep/directory/");
+ g_assert_cmpstr (keys[1], ==, "deep/directory/");
+ g_assert (keys[2] == NULL);
+ g_assert (values[0] == NULL);
+ g_assert (values[1] == NULL);
+ dconf_changeset_unref (changeset);
+}
+
+static void
+test_reset (void)
+{
+ DConfChangeset *changeset;
+ GVariant *value;
+
+ changeset = dconf_changeset_new ();
+ g_assert (!dconf_changeset_get (changeset, "/value/a", NULL));
+ g_assert (!dconf_changeset_get (changeset, "/value/a", &value));
+ /* value was not set */
+
+ /* set a value */
+ dconf_changeset_set (changeset, "/value/a", g_variant_new_boolean (TRUE));
+ g_assert (dconf_changeset_get (changeset, "/value/a", NULL));
+ g_assert (dconf_changeset_get (changeset, "/value/a", &value));
+ g_assert (value != NULL);
+ g_variant_unref (value);
+
+ /* record the reset */
+ dconf_changeset_set (changeset, "/value/", NULL);
+ g_assert (dconf_changeset_get (changeset, "/value/a", NULL));
+ g_assert (dconf_changeset_get (changeset, "/value/a", &value));
+ g_assert (value == NULL);
+
+ /* write it back */
+ dconf_changeset_set (changeset, "/value/a", g_variant_new_boolean (TRUE));
+ g_assert (dconf_changeset_get (changeset, "/value/a", NULL));
+ g_assert (dconf_changeset_get (changeset, "/value/a", &value));
+ g_assert (value != NULL);
+ g_variant_unref (value);
+
+ /* reset again */
+ dconf_changeset_set (changeset, "/value/", NULL);
+ g_assert (dconf_changeset_get (changeset, "/value/a", NULL));
+ g_assert (dconf_changeset_get (changeset, "/value/a", &value));
+ g_assert (value == NULL);
+
+ /* write again */
+ dconf_changeset_set (changeset, "/value/a", g_variant_new_boolean (TRUE));
+ g_assert (dconf_changeset_get (changeset, "/value/a", NULL));
+ g_assert (dconf_changeset_get (changeset, "/value/a", &value));
+ g_assert (value != NULL);
+ g_variant_unref (value);
+
+ /* reset a different way */
+ dconf_changeset_set (changeset, "/value/a", NULL);
+ g_assert (dconf_changeset_get (changeset, "/value/a", NULL));
+ g_assert (dconf_changeset_get (changeset, "/value/a", &value));
+ g_assert (value == NULL);
+
+ /* write last time */
+ dconf_changeset_set (changeset, "/value/a", g_variant_new_boolean (TRUE));
+ g_assert (dconf_changeset_get (changeset, "/value/a", NULL));
+ g_assert (dconf_changeset_get (changeset, "/value/a", &value));
+ g_assert (value != NULL);
+ g_variant_unref (value);
+
+ dconf_changeset_unref (changeset);
+}
+
+static gboolean
+has_same_value (const gchar *key,
+ GVariant *value,
+ gpointer user_data)
+{
+ DConfChangeset *other = user_data;
+ GVariant *other_value;
+ gboolean success;
+
+ success = dconf_changeset_get (other, key, &other_value);
+ g_assert (success);
+
+ if (value == NULL)
+ g_assert (other_value == NULL);
+ else
+ {
+ g_assert (g_variant_equal (value, other_value));
+ g_variant_unref (other_value);
+ }
+
+ return TRUE;
+}
+
+static void
+test_serialisation (DConfChangeset *changes)
+{
+ GVariant *serialised;
+ DConfChangeset *copy;
+
+ serialised = dconf_changeset_serialise (changes);
+ copy = dconf_changeset_deserialise (serialised);
+ g_variant_unref (serialised);
+
+ g_assert (dconf_changeset_is_similar_to (copy, changes));
+ g_assert (dconf_changeset_is_similar_to (changes, copy));
+ g_assert (dconf_changeset_all (copy, has_same_value, changes));
+ g_assert (dconf_changeset_all (changes, has_same_value, copy));
+
+ dconf_changeset_unref (copy);
+}
+
+static void
+test_serialiser (void)
+{
+ DConfChangeset *changeset;
+
+ changeset = dconf_changeset_new ();
+ test_serialisation (changeset);
+
+ dconf_changeset_set (changeset, "/some/value", g_variant_new_int32 (333));
+ test_serialisation (changeset);
+
+ dconf_changeset_set (changeset, "/other/value", NULL);
+ test_serialisation (changeset);
+
+ dconf_changeset_set (changeset, "/other/value", g_variant_new_int32 (55));
+ test_serialisation (changeset);
+
+ dconf_changeset_set (changeset, "/other/", NULL);
+ test_serialisation (changeset);
+
+ dconf_changeset_set (changeset, "/", NULL);
+ test_serialisation (changeset);
+
+ dconf_changeset_unref (changeset);
+}
+
+static void
+test_change (void)
+{
+ DConfChangeset *deltaa, *deltab;
+ DConfChangeset *dba, *dbb;
+
+ dba = dconf_changeset_new_database (NULL);
+ dbb = dconf_changeset_new_database (dba);
+ g_assert (dconf_changeset_is_empty (dbb));
+ dconf_changeset_unref (dbb);
+
+ deltaa = dconf_changeset_new ();
+ dconf_changeset_change (dba, deltaa);
+ g_assert (dconf_changeset_is_empty (dba));
+ dconf_changeset_unref (deltaa);
+
+ deltaa = dconf_changeset_new_write ("/some/value", NULL);
+ dconf_changeset_change (dba, deltaa);
+ g_assert (dconf_changeset_is_empty (dba));
+ dconf_changeset_unref (deltaa);
+
+ deltaa = dconf_changeset_new ();
+ deltab = dconf_changeset_new_write ("/some/value", g_variant_new_int32 (123));
+ dconf_changeset_change (deltaa, deltab);
+ g_assert (!dconf_changeset_is_empty (deltaa));
+ dconf_changeset_change (dba, deltab);
+ g_assert (!dconf_changeset_is_empty (dba));
+ dconf_changeset_unref (deltaa);
+ dconf_changeset_unref (deltab);
+
+ deltaa = dconf_changeset_new ();
+ deltab = dconf_changeset_new_write ("/other/value", g_variant_new_int32 (123));
+ dconf_changeset_change (deltaa, deltab);
+ g_assert (!dconf_changeset_is_empty (deltaa));
+ dconf_changeset_unref (deltab);
+ deltab = dconf_changeset_new_write ("/other/", NULL);
+ dconf_changeset_change (deltaa, deltab);
+ g_assert (!dconf_changeset_is_empty (deltaa));
+ dconf_changeset_change (dba, deltaa);
+ g_assert (!dconf_changeset_is_empty (dba));
+
+ dbb = dconf_changeset_new_database (dba);
+ g_assert (!dconf_changeset_is_empty (dbb));
+
+ dconf_changeset_set (dba, "/some/", NULL);
+
+ dconf_changeset_set (dba, "/other/value", g_variant_new_int32 (123));
+ g_assert (!dconf_changeset_is_empty (dba));
+ dconf_changeset_change (dba, deltaa);
+ g_assert (dconf_changeset_is_empty (dba));
+ g_assert (!dconf_changeset_is_empty (dbb));
+
+ dconf_changeset_unref (deltaa);
+ dconf_changeset_unref (deltab);
+ dconf_changeset_unref (dbb);
+ dconf_changeset_unref (dba);
+}
+
+static void
+assert_diff_change_invariant (DConfChangeset *from,
+ DConfChangeset *to)
+{
+ DConfChangeset *copy;
+ DConfChangeset *diff;
+
+ /* Verify this promise from the docs:
+ *
+ * Applying the returned changeset to @from using
+ * dconf_changeset_change() will result in the two changesets being
+ * equal.
+ */
+
+ copy = dconf_changeset_new_database (from);
+ diff = dconf_changeset_diff (from, to);
+ if (diff)
+ {
+ dconf_changeset_change (copy, diff);
+ dconf_changeset_unref (diff);
+ }
+
+ /* Make sure they are now equal */
+ diff = dconf_changeset_diff (copy, to);
+ g_assert (diff == NULL);
+
+ /* Why not try it the other way too? */
+ diff = dconf_changeset_diff (to, copy);
+ g_assert (diff == NULL);
+
+ dconf_changeset_unref (copy);
+}
+
+static gchar *
+create_random_key (void)
+{
+ GString *key;
+ gint i, n;
+
+ key = g_string_new (NULL);
+ n = g_test_rand_int_range (1, 5);
+ for (i = 0; i < n; i++)
+ {
+ gint j;
+
+ g_string_append_c (key, '/');
+ for (j = 0; j < 5; j++)
+ g_string_append_c (key, g_test_rand_int_range ('a', 'z' + 1));
+ }
+
+ return g_string_free (key, FALSE);
+}
+
+static GVariant *
+create_random_value (void)
+{
+ return g_variant_new_take_string (create_random_key ());
+}
+
+static DConfChangeset *
+create_random_db (void)
+{
+ DConfChangeset *set;
+ gint i, n;
+
+ set = dconf_changeset_new_database (NULL);
+ n = g_test_rand_int_range (0, 20);
+ for (i = 0; i < n; i++)
+ {
+ GVariant *value = create_random_value ();
+ gchar *key = create_random_key ();
+
+ dconf_changeset_set (set, key, value);
+ g_free (key);
+ }
+
+ return set;
+}
+
+static void
+test_diff (void)
+{
+ DConfChangeset *a, *b;
+ gint i;
+
+ /* Check diff between two empties */
+ a = dconf_changeset_new_database (NULL);
+ b = dconf_changeset_new_database (NULL);
+ assert_diff_change_invariant (a, b);
+ dconf_changeset_unref (a);
+ dconf_changeset_unref (b);
+
+ /* Check diff between two non-empties that are equal */
+ a = create_random_db ();
+ b = dconf_changeset_new_database (a);
+ assert_diff_change_invariant (a, b);
+ dconf_changeset_unref (a);
+ dconf_changeset_unref (b);
+
+ /* Check diff between two random databases that are probably unequal */
+ for (i = 0; i < 1000; i++)
+ {
+ a = create_random_db ();
+ b = create_random_db ();
+ assert_diff_change_invariant (a, b);
+ dconf_changeset_unref (a);
+ dconf_changeset_unref (b);
+ }
+}
+
+int
+main (int argc, char **argv)
+{
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add_func ("/changeset/basic", test_basic);
+ g_test_add_func ("/changeset/similarity", test_similarity);
+ g_test_add_func ("/changeset/describe", test_describe);
+ g_test_add_func ("/changeset/reset", test_reset);
+ g_test_add_func ("/changeset/serialiser", test_serialiser);
+ g_test_add_func ("/changeset/change", test_change);
+ g_test_add_func ("/changeset/diff", test_diff);
+
+ return g_test_run ();
+}
diff --git a/tests/client.c b/tests/client.c
new file mode 100644
index 0000000..1773ed1
--- /dev/null
+++ b/tests/client.c
@@ -0,0 +1,246 @@
+#define _DEFAULT_SOURCE
+#include "../client/dconf-client.h"
+#include "../engine/dconf-engine.h"
+#include "dconf-mock.h"
+#include <string.h>
+#include <stdlib.h>
+
+static GThread *main_thread;
+
+static void
+test_lifecycle (void)
+{
+ DConfClient *client;
+ GWeakRef weak;
+
+ client = dconf_client_new ();
+ g_weak_ref_init (&weak, client);
+ g_object_unref (client);
+
+ g_assert (g_weak_ref_get (&weak) == NULL);
+ g_weak_ref_clear (&weak);
+}
+
+static gboolean changed_was_called;
+
+static void
+changed (DConfClient *client,
+ const gchar *prefix,
+ const gchar * const *changes,
+ const gchar *tag,
+ gpointer user_data)
+{
+ g_assert (g_thread_self () == main_thread);
+
+ changed_was_called = TRUE;
+}
+
+static void
+check_and_free (GVariant *to_check,
+ GVariant *expected)
+{
+ if (expected)
+ {
+ g_variant_ref_sink (expected);
+ g_assert (to_check);
+
+ g_assert (g_variant_equal (to_check, expected));
+ g_variant_unref (to_check);
+ g_variant_unref (expected);
+ }
+ else
+ g_assert (to_check == NULL);
+}
+
+static void
+queue_up_100_writes (DConfClient *client)
+{
+ gint i;
+
+ /* We send 100 writes, letting them pile up.
+ * At no time should there be more than one write on the wire.
+ */
+ for (i = 0; i < 100; i++)
+ {
+ changed_was_called = FALSE;
+ dconf_client_write_fast (client, "/test/value", g_variant_new_int32 (i), NULL);
+ g_assert (changed_was_called);
+
+ /* We should always see the most recently written value. */
+ check_and_free (dconf_client_read (client, "/test/value"), g_variant_new_int32 (i));
+ check_and_free (dconf_client_read_full (client, "/test/value", DCONF_READ_DEFAULT_VALUE, NULL), NULL);
+ }
+
+ g_assert_cmpint (g_queue_get_length (&dconf_mock_dbus_outstanding_call_handles), ==, 1);
+}
+
+static void
+fail_one_call (void)
+{
+ DConfEngineCallHandle *handle;
+ GError *error;
+
+ error = g_error_new_literal (G_FILE_ERROR, G_FILE_ERROR_NOENT, "--expected error from testcase--");
+ handle = g_queue_pop_head (&dconf_mock_dbus_outstanding_call_handles);
+ dconf_engine_call_handle_reply (handle, NULL, error);
+ g_error_free (error);
+}
+
+static GLogWriterOutput
+log_writer_cb (GLogLevelFlags log_level,
+ const GLogField *fields,
+ gsize n_fields,
+ gpointer user_data)
+{
+ gsize i;
+
+ for (i = 0; i < n_fields; i++)
+ {
+ if (g_strcmp0 (fields[i].key, "MESSAGE") == 0 &&
+ strstr (fields[i].value, "--expected error from testcase--"))
+ return G_LOG_WRITER_HANDLED;
+ }
+
+ return G_LOG_WRITER_UNHANDLED;
+}
+
+static void
+test_fast (void)
+{
+ DConfClient *client;
+
+ g_log_set_writer_func (log_writer_cb, NULL, NULL);
+
+ client = dconf_client_new ();
+ g_signal_connect (client, "changed", G_CALLBACK (changed), NULL);
+
+ queue_up_100_writes (client);
+
+ /* Start indicating that the writes failed.
+ *
+ * Because of the pending-merge logic, we should only have had to fail two calls.
+ *
+ * Each time, we should see a change notify.
+ */
+
+ g_assert_cmpint (g_queue_get_length (&dconf_mock_dbus_outstanding_call_handles), == , 1);
+
+ changed_was_called = FALSE;
+ fail_one_call ();
+ g_assert (changed_was_called);
+
+ /* For the first failure, we should continue to see the most recently written value (99) */
+ check_and_free (dconf_client_read (client, "/test/value"), g_variant_new_int32 (99));
+ check_and_free (dconf_client_read_full (client, "/test/value", DCONF_READ_DEFAULT_VALUE, NULL), NULL);
+
+ g_assert_cmpint (g_queue_get_length (&dconf_mock_dbus_outstanding_call_handles), == , 1);
+
+ changed_was_called = FALSE;
+ fail_one_call ();
+ g_assert (changed_was_called);
+
+ /* Should read back now as NULL */
+ check_and_free (dconf_client_read (client, "/test/value"), NULL);
+ check_and_free (dconf_client_read_full (client, "/test/value", DCONF_READ_DEFAULT_VALUE, NULL), NULL);
+
+ g_assert_cmpint (g_queue_get_length (&dconf_mock_dbus_outstanding_call_handles), == , 0);
+
+ /* Cleanup */
+ g_signal_handlers_disconnect_by_func (client, changed, NULL);
+ g_object_unref (client);
+}
+
+static gboolean changed_a, changed_b, changed_c;
+
+static void
+coalesce_changed (DConfClient *client,
+ const gchar *prefix,
+ const gchar * const *changes,
+ const gchar *tag,
+ gpointer user_data)
+{
+ changed_a = g_str_equal (prefix, "/test/a") || g_strv_contains (changes, "a");
+ changed_b = g_str_equal (prefix, "/test/b") || g_strv_contains (changes, "b");
+ changed_c = g_str_equal (prefix, "/test/c") || g_strv_contains (changes, "c");
+}
+
+static void
+test_coalesce (void)
+{
+ gint i, a, b, c;
+ gboolean should_change_a, should_change_b, should_change_c;
+ g_autoptr(DConfClient) client = NULL;
+
+ gint changes[][3] = {
+ {1, 0, 0},
+ {1, 1, 1},
+ {0, 1, 1},
+ {0, 0, 1},
+ {0, 0, 0},
+ {1, 0, 0},
+ {1, 0, 0},
+ };
+
+ client = dconf_client_new ();
+ g_signal_connect (client, "changed", G_CALLBACK (coalesce_changed), NULL);
+
+ a = b = c = 0;
+
+ for (i = 0; i != G_N_ELEMENTS (changes); ++i)
+ {
+ g_autoptr(DConfChangeset) changeset = NULL;
+
+ should_change_a = changes[i][0];
+ should_change_b = changes[i][1];
+ should_change_c = changes[i][2];
+
+ changeset = dconf_changeset_new ();
+
+ if (should_change_a)
+ dconf_changeset_set (changeset, "/test/a", g_variant_new_int32 (++a));
+ if (should_change_b)
+ dconf_changeset_set (changeset, "/test/b", g_variant_new_int32 (++b));
+ if (should_change_c)
+ dconf_changeset_set (changeset, "/test/c", g_variant_new_int32 (++c));
+
+ changed_a = changed_b = changed_c = FALSE;
+
+ g_assert_true (dconf_client_change_fast (client, changeset, NULL));
+
+ /* Notifications should be only about keys we have just written. */
+ g_assert_cmpint (should_change_a, ==, changed_a);
+ g_assert_cmpint (should_change_b, ==, changed_b);
+ g_assert_cmpint (should_change_c, ==, changed_c);
+
+ /* We should see value from the most recent write or NULL if we haven't written it yet. */
+ check_and_free (dconf_client_read (client, "/test/a"), a == 0 ? NULL : g_variant_new_int32 (a));
+ check_and_free (dconf_client_read (client, "/test/b"), b == 0 ? NULL : g_variant_new_int32 (b));
+ check_and_free (dconf_client_read (client, "/test/c"), c == 0 ? NULL : g_variant_new_int32 (c));
+ }
+
+ dconf_mock_dbus_async_reply (g_variant_new ("(s)", "1"), NULL);
+ dconf_mock_dbus_async_reply (g_variant_new ("(s)", "2"), NULL);
+
+ /* There should be no more requests since all but first have been
+ * coalesced together. */
+ dconf_mock_dbus_assert_no_async ();
+
+ /* Cleanup */
+ g_signal_handlers_disconnect_by_func (client, changed, NULL);
+}
+
+int
+main (int argc, char **argv)
+{
+ setenv ("DCONF_PROFILE", SRCDIR "/profile/will-never-exist", TRUE);
+
+ main_thread = g_thread_self ();
+
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add_func ("/client/lifecycle", test_lifecycle);
+ g_test_add_func ("/client/basic-fast", test_fast);
+ g_test_add_func ("/client/coalesce", test_coalesce);
+
+ return g_test_run ();
+}
diff --git a/tests/dbus.c b/tests/dbus.c
new file mode 100644
index 0000000..032cb04
--- /dev/null
+++ b/tests/dbus.c
@@ -0,0 +1,544 @@
+#include <string.h>
+#include <glib.h>
+#include <stdlib.h>
+
+/* Test the DBus communicaton code.
+ */
+
+#include "../engine/dconf-engine.h"
+
+static gboolean okay_in_main;
+static GThread *main_thread;
+static GThread *dbus_thread;
+static GQueue async_call_success_queue;
+static GQueue async_call_error_queue;
+static GMutex async_call_queue_lock;
+static gboolean signal_was_received;
+
+static void
+wait_for_queue_to_empty (GQueue *queue)
+{
+ okay_in_main = TRUE;
+
+ while (TRUE)
+ {
+ gboolean is_empty;
+
+ g_mutex_lock (&async_call_queue_lock);
+ is_empty = g_queue_is_empty (queue);
+ g_mutex_unlock (&async_call_queue_lock);
+
+ if (is_empty)
+ return;
+
+ g_main_context_iteration (NULL, TRUE);
+ }
+
+ okay_in_main = FALSE;
+}
+
+static gboolean
+just_wake (gpointer user_data)
+{
+ return G_SOURCE_REMOVE;
+}
+
+static void
+signal_if_queue_is_empty (GQueue *queue)
+{
+ gboolean is_empty;
+
+ g_mutex_lock (&async_call_queue_lock);
+ is_empty = g_queue_is_empty (queue);
+ g_mutex_unlock (&async_call_queue_lock);
+
+ if (is_empty)
+ g_idle_add (just_wake, NULL);
+}
+
+const GVariantType *
+dconf_engine_call_handle_get_expected_type (DConfEngineCallHandle *handle)
+{
+ return (GVariantType *) handle;
+}
+
+void
+dconf_engine_call_handle_reply (DConfEngineCallHandle *handle,
+ GVariant *parameters,
+ const GError *error)
+{
+ DConfEngineCallHandle *expected_handle;
+
+ /* Ensure that messages are never delivered in the main thread except
+ * by way of a mainloop (ie: not during sync calls).
+ *
+ * It's okay if they are delivered in another thread at the same time
+ * as a sync call is happening in the main thread, though...
+ */
+ g_assert (g_thread_self () != main_thread || okay_in_main);
+
+ /* Make sure that we only ever receive D-Bus calls from a single
+ * thread.
+ */
+ if (!dbus_thread)
+ dbus_thread = g_thread_self ();
+ g_assert (g_thread_self () == dbus_thread);
+
+ /* This is the passing case. */
+ if (parameters != NULL)
+ {
+ g_mutex_lock (&async_call_queue_lock);
+ g_assert (g_queue_is_empty (&async_call_error_queue));
+ expected_handle = g_queue_pop_head (&async_call_success_queue);
+ g_mutex_unlock (&async_call_queue_lock);
+
+ g_assert (parameters != NULL);
+ g_assert (error == NULL);
+ g_assert (g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(s)")));
+
+ g_assert (expected_handle == handle);
+ g_variant_type_free ((GVariantType *) handle);
+
+ signal_if_queue_is_empty (&async_call_success_queue);
+ }
+ else
+ {
+ g_mutex_lock (&async_call_queue_lock);
+ g_assert (g_queue_is_empty (&async_call_success_queue));
+ expected_handle = g_queue_pop_head (&async_call_error_queue);
+ g_mutex_unlock (&async_call_queue_lock);
+
+ g_assert (parameters == NULL);
+ g_assert (error != NULL);
+
+ g_assert (expected_handle == handle);
+ g_variant_type_free ((GVariantType *) handle);
+
+ signal_if_queue_is_empty (&async_call_error_queue);
+ }
+}
+
+void
+dconf_engine_handle_dbus_signal (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *signal_name,
+ GVariant *parameters)
+{
+ g_assert (g_thread_self () != main_thread || okay_in_main);
+
+ if (!dbus_thread)
+ dbus_thread = g_thread_self ();
+ g_assert (g_thread_self () == dbus_thread);
+
+ if (g_str_equal (signal_name, "TestSignal"))
+ {
+ GVariant *expected;
+
+ expected = g_variant_parse (NULL, "('1', ['2', '3'])", NULL, NULL, NULL);
+ g_assert (g_variant_equal (parameters, expected));
+ g_variant_unref (expected);
+
+ signal_was_received = TRUE;
+ g_idle_add (just_wake, NULL);
+ }
+}
+
+static void
+test_creation_error_sync_with_error (void)
+{
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ /* Sync with 'error' */
+ if (g_test_subprocess ())
+ {
+ GError *error = NULL;
+ GVariant *reply;
+
+ g_setenv ("DBUS_SESSION_BUS_ADDRESS", "some nonsense", 1);
+
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("()"), G_VARIANT_TYPE ("(as)"), &error);
+
+ g_assert (reply == NULL);
+ g_assert (error != NULL);
+ g_assert (strstr (error->message, "some nonsense"));
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+}
+
+static void
+test_creation_error_sync_without_error (void)
+{
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ /* Sync without 'error' */
+ if (g_test_subprocess ())
+ {
+ GVariant *reply;
+
+ g_setenv ("DBUS_SESSION_BUS_ADDRESS", "some nonsense", 1);
+
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("()"), G_VARIANT_TYPE ("(as)"), NULL);
+
+ g_assert (reply == NULL);
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+}
+
+static void
+test_creation_error_async (void)
+{
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ /* Async */
+ if (g_test_subprocess ())
+ {
+ DConfEngineCallHandle *handle;
+ GError *error = NULL;
+ gboolean success;
+
+ g_setenv ("DBUS_SESSION_BUS_ADDRESS", "some nonsense", 1);
+
+ handle = (gpointer) g_variant_type_new ("(s)");
+ g_mutex_lock (&async_call_queue_lock);
+ g_queue_push_tail (&async_call_error_queue, handle);
+ g_mutex_unlock (&async_call_queue_lock);
+
+ success = dconf_engine_dbus_call_async_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("()"), handle, &error);
+
+ /* This could either fail immediately or asynchronously, depending
+ * on how the backend is setup.
+ */
+ if (success)
+ {
+ g_assert_no_error (error);
+
+ wait_for_queue_to_empty (&async_call_error_queue);
+ }
+ else
+ g_assert (error != NULL);
+
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+}
+
+static void
+test_sync_call_success (void)
+{
+ GError *error = NULL;
+ gchar *session_id;
+ gchar *system_id;
+ GVariant *reply;
+
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "ListNames",
+ g_variant_new ("()"), G_VARIANT_TYPE ("(as)"), &error);
+
+ g_assert_no_error (error);
+ g_assert (reply != NULL);
+ g_assert (g_variant_is_of_type (reply, G_VARIANT_TYPE ("(as)")));
+ g_variant_unref (reply);
+
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("()"), G_VARIANT_TYPE ("(s)"), &error);
+
+ g_assert_no_error (error);
+ g_assert (reply != NULL);
+ g_assert (g_variant_is_of_type (reply, G_VARIANT_TYPE ("(s)")));
+ g_variant_get (reply, "(s)", &session_id);
+ g_variant_unref (reply);
+
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SYSTEM,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("()"), G_VARIANT_TYPE ("(s)"), &error);
+
+ g_assert_no_error (error);
+ g_assert (reply != NULL);
+ g_assert (g_variant_is_of_type (reply, G_VARIANT_TYPE ("(s)")));
+ g_variant_get (reply, "(s)", &system_id);
+ g_variant_unref (reply);
+
+ /* Make sure we actually saw two separate buses */
+ g_assert_cmpstr (session_id, !=, system_id);
+ g_free (session_id);
+ g_free (system_id);
+}
+
+static void
+test_sync_call_error (void)
+{
+ GError *error = NULL;
+ GVariant *reply;
+
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ /* Test receiving errors from the other side */
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("(s)", ""), G_VARIANT_TYPE_UNIT, &error);
+ g_assert (reply == NULL);
+ g_assert (error != NULL);
+ g_assert (strstr (error->message, "org.freedesktop.DBus.Error.InvalidArgs"));
+ g_clear_error (&error);
+
+ /* Test with 'ay' to make sure transmitting that works as well */
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("(ay)", NULL), G_VARIANT_TYPE_UNIT, &error);
+ g_assert (reply == NULL);
+ g_assert (error != NULL);
+ g_assert (strstr (error->message, "org.freedesktop.DBus.Error.InvalidArgs"));
+ g_clear_error (&error);
+
+ /* Test reply type errors */
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("()"), G_VARIANT_TYPE ("(u)"), &error);
+ g_assert (reply == NULL);
+ g_assert (error != NULL);
+ g_assert (strstr (error->message, " type "));
+ g_clear_error (&error);
+
+ /* Test two oddities:
+ *
+ * - first, the dbus-1 backend can't handle return types other than
+ * 's' and 'as', so we do a method call that will get something
+ * else in order that we can check that the failure is treated
+ * properly
+ *
+ * - next, we want to make sure that the filter function for
+ * gdbus-filter doesn't block incoming method calls
+ */
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "RequestName",
+ g_variant_new_parsed ("('ca.desrt.dconf.testsuite', uint32 0)"),
+ G_VARIANT_TYPE ("(u)"), &error);
+ if (reply != NULL)
+ {
+ guint s;
+
+ /* It worked, so we must be on gdbus... */
+ g_assert_no_error (error);
+
+ g_variant_get (reply, "(u)", &s);
+ g_assert_cmpuint (s, ==, 1);
+ g_variant_unref (reply);
+
+ /* Ping ourselves... */
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "ca.desrt.dconf.testsuite", "/", "org.freedesktop.DBus.Peer",
+ "Ping", g_variant_new ("()"), G_VARIANT_TYPE_UNIT, &error);
+ g_assert (reply != NULL);
+ g_assert_no_error (error);
+ g_variant_unref (reply);
+ }
+ else
+ {
+ /* Else, we're on dbus1...
+ *
+ * Check that the error was emitted correctly.
+ */
+ g_assert_cmpstr (error->message, ==, "unable to handle message type '(u)'");
+ g_clear_error (&error);
+ }
+}
+
+static void
+test_async_call_success (void)
+{
+ gint i;
+
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ for (i = 0; i < 1000; i++)
+ {
+ DConfEngineCallHandle *handle;
+ GError *error = NULL;
+ gboolean success;
+
+ handle = (gpointer) g_variant_type_new ("(s)");
+ g_mutex_lock (&async_call_queue_lock);
+ g_queue_push_tail (&async_call_success_queue, handle);
+ g_mutex_unlock (&async_call_queue_lock);
+
+ success = dconf_engine_dbus_call_async_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("()"), handle, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ }
+
+ wait_for_queue_to_empty (&async_call_success_queue);
+}
+
+static void
+test_async_call_error (void)
+{
+ DConfEngineCallHandle *handle;
+ GError *error = NULL;
+ gboolean success;
+
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ handle = (gpointer) g_variant_type_new ("(s)");
+
+ g_mutex_lock (&async_call_queue_lock);
+ g_queue_push_tail (&async_call_error_queue, handle);
+ g_mutex_unlock (&async_call_queue_lock);
+
+ success = dconf_engine_dbus_call_async_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("(s)", ""), handle, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ wait_for_queue_to_empty (&async_call_error_queue);
+}
+
+static void
+test_sync_during_async (void)
+{
+ DConfEngineCallHandle *handle;
+ GError *error = NULL;
+ gboolean success;
+ GVariant *reply;
+
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ handle = (gpointer) g_variant_type_new ("(s)");
+ g_mutex_lock (&async_call_queue_lock);
+ g_queue_push_tail (&async_call_success_queue, handle);
+ g_mutex_unlock (&async_call_queue_lock);
+
+ success = dconf_engine_dbus_call_async_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "GetId",
+ g_variant_new ("()"), handle, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "ListNames",
+ g_variant_new ("()"), G_VARIANT_TYPE ("(as)"), &error);
+ g_assert_no_error (error);
+ g_assert (reply != NULL);
+ g_variant_unref (reply);
+
+ wait_for_queue_to_empty (&async_call_success_queue);
+}
+
+static gboolean
+did_not_receive_signal (gpointer user_data)
+{
+ g_assert_not_reached ();
+}
+
+static void
+test_signal_receipt (void)
+{
+ GError *error = NULL;
+ GVariant *reply;
+ gint status;
+ guint id;
+
+ if (g_getenv ("DISPLAY") == NULL || g_strcmp0 (g_getenv ("DISPLAY"), "") == 0)
+ {
+ g_test_skip ("FIXME: D-Bus tests do not work on CI at the moment");
+ return;
+ }
+
+ reply = dconf_engine_dbus_call_sync_func (G_BUS_TYPE_SESSION,
+ "org.freedesktop.DBus", "/", "org.freedesktop.DBus", "AddMatch",
+ g_variant_new ("(s)", "type='signal',interface='ca.desrt.dconf.Writer'"),
+ G_VARIANT_TYPE_UNIT, &error);
+ g_assert_no_error (error);
+ g_assert (reply != NULL);
+ g_variant_unref (reply);
+
+ status = system ("gdbus emit --session "
+ "--object-path /ca/desrt/dconf/Writer/testcase "
+ "--signal ca.desrt.dconf.Writer.TestSignal "
+ "\"'1'\" \"['2', '3']\"");
+ g_assert_cmpint (status, ==, 0);
+
+ id = g_timeout_add (30000, did_not_receive_signal, NULL);
+ while (!signal_was_received)
+ g_main_context_iteration (NULL, FALSE);
+ g_source_remove (id);
+}
+
+int
+main (int argc, char **argv)
+{
+ g_test_init (&argc, &argv, NULL);
+
+ main_thread = g_thread_self ();
+
+ dconf_engine_dbus_init_for_testing ();
+
+ /* test_creation_error absolutely must come first */
+ if (!g_str_equal (DBUS_BACKEND, "/libdbus-1"))
+ {
+ g_test_add_func (DBUS_BACKEND "/creation/error/sync-with-error", test_creation_error_sync_with_error);
+ g_test_add_func (DBUS_BACKEND "/creation/error/sync-without-error", test_creation_error_sync_without_error);
+ g_test_add_func (DBUS_BACKEND "/creation/error/async", test_creation_error_async);
+ }
+
+ g_test_add_func (DBUS_BACKEND "/sync-call/success", test_sync_call_success);
+ g_test_add_func (DBUS_BACKEND "/sync-call/error", test_sync_call_error);
+ g_test_add_func (DBUS_BACKEND "/async-call/success", test_async_call_success);
+ g_test_add_func (DBUS_BACKEND "/async-call/error", test_async_call_error);
+ g_test_add_func (DBUS_BACKEND "/sync-call/during-async", test_sync_during_async);
+ g_test_add_func (DBUS_BACKEND "/signal/receipt", test_signal_receipt);
+
+ return g_test_run ();
+}
diff --git a/tests/dconf-mock-dbus.c b/tests/dconf-mock-dbus.c
new file mode 100644
index 0000000..2bafa30
--- /dev/null
+++ b/tests/dconf-mock-dbus.c
@@ -0,0 +1,99 @@
+/*
+ * Copyright © 2012 Canonical Limited
+ *
+ * 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/>.
+ *
+ * Author: Ryan Lortie <desrt@desrt.ca>
+ */
+
+#include "../engine/dconf-engine.h"
+#include "dconf-mock.h"
+
+GQueue dconf_mock_dbus_outstanding_call_handles;
+
+gboolean
+dconf_engine_dbus_call_async_func (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ DConfEngineCallHandle *handle,
+ GError **error)
+{
+ g_variant_ref_sink (parameters);
+ g_variant_unref (parameters);
+
+ g_queue_push_tail (&dconf_mock_dbus_outstanding_call_handles, handle);
+
+ return TRUE;
+}
+
+void
+dconf_mock_dbus_async_reply (GVariant *reply,
+ GError *error)
+{
+ DConfEngineCallHandle *handle;
+
+ g_assert (!g_queue_is_empty (&dconf_mock_dbus_outstanding_call_handles));
+ handle = g_queue_pop_head (&dconf_mock_dbus_outstanding_call_handles);
+
+ if (reply)
+ {
+ const GVariantType *expected_type;
+
+ expected_type = dconf_engine_call_handle_get_expected_type (handle);
+ g_assert (expected_type == NULL || g_variant_is_of_type (reply, expected_type));
+ g_variant_ref_sink (reply);
+ }
+
+ dconf_engine_call_handle_reply (handle, reply, error);
+
+ if (reply)
+ g_variant_unref (reply);
+}
+
+void
+dconf_mock_dbus_assert_no_async (void)
+{
+ g_assert (g_queue_is_empty (&dconf_mock_dbus_outstanding_call_handles));
+}
+
+DConfMockDBusSyncCallHandler dconf_mock_dbus_sync_call_handler;
+
+GVariant *
+dconf_engine_dbus_call_sync_func (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ const GVariantType *reply_type,
+ GError **error)
+{
+ GVariant *reply;
+
+ g_assert (dconf_mock_dbus_sync_call_handler != NULL);
+
+ g_variant_ref_sink (parameters);
+
+ reply = (* dconf_mock_dbus_sync_call_handler) (bus_type, bus_name, object_path, interface_name,
+ method_name, parameters, reply_type, error);
+
+ g_variant_unref (parameters);
+
+ g_assert (reply != NULL || (error == NULL || *error != NULL));
+
+ return reply ? g_variant_take_ref (reply) : NULL;
+}
diff --git a/tests/dconf-mock-gvdb.c b/tests/dconf-mock-gvdb.c
new file mode 100644
index 0000000..4f58de1
--- /dev/null
+++ b/tests/dconf-mock-gvdb.c
@@ -0,0 +1,211 @@
+#include "../gvdb/gvdb-reader.h"
+#include "dconf-mock.h"
+
+/* The global dconf_mock_gvdb_tables hashtable is modified all the time
+ * so we need to hold the lock while we access it.
+ *
+ * The hashtables contained within it are never modified, however. They
+ * can be safely accessed without a lock.
+ */
+
+static GHashTable *dconf_mock_gvdb_tables;
+static GMutex dconf_mock_gvdb_lock;
+
+typedef struct
+{
+ GVariant *value;
+ GvdbTable *table;
+} DConfMockGvdbItem;
+
+struct _GvdbTable
+{
+ GHashTable *table;
+ gboolean is_valid;
+ gboolean top_level;
+ gint ref_count;
+};
+
+static void
+dconf_mock_gvdb_item_free (gpointer data)
+{
+ DConfMockGvdbItem *item = data;
+
+ if (item->value)
+ g_variant_unref (item->value);
+
+ if (item->table)
+ gvdb_table_free (item->table);
+
+ g_slice_free (DConfMockGvdbItem, item);
+}
+
+static void
+dconf_mock_gvdb_init (void)
+{
+ if (dconf_mock_gvdb_tables == NULL)
+ dconf_mock_gvdb_tables = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) gvdb_table_free);
+}
+
+GvdbTable *
+dconf_mock_gvdb_table_new (void)
+{
+ GvdbTable *table;
+
+ table = g_slice_new (GvdbTable);
+ table->table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, dconf_mock_gvdb_item_free);
+ table->ref_count = 1;
+ table->is_valid = TRUE;
+
+ return table;
+}
+
+void
+dconf_mock_gvdb_table_insert (GvdbTable *table,
+ const gchar *name,
+ GVariant *value,
+ GvdbTable *subtable)
+{
+ DConfMockGvdbItem *item;
+
+ g_assert (value == NULL || subtable == NULL);
+
+ if (subtable)
+ subtable->top_level = FALSE;
+
+ item = g_slice_new (DConfMockGvdbItem);
+ item->value = value ? g_variant_ref_sink (value) : NULL;
+ item->table = subtable;
+
+ g_hash_table_insert (table->table, g_strdup (name), item);
+}
+
+void
+dconf_mock_gvdb_install (const gchar *filename,
+ GvdbTable *table)
+{
+ g_mutex_lock (&dconf_mock_gvdb_lock);
+ dconf_mock_gvdb_init ();
+
+ if (table)
+ {
+ table->top_level = TRUE;
+ g_hash_table_insert (dconf_mock_gvdb_tables, g_strdup (filename), table);
+ }
+ else
+ g_hash_table_remove (dconf_mock_gvdb_tables, filename);
+
+ g_mutex_unlock (&dconf_mock_gvdb_lock);
+}
+
+void
+gvdb_table_free (GvdbTable *table)
+{
+ if (g_atomic_int_dec_and_test (&table->ref_count))
+ {
+ g_hash_table_unref (table->table);
+ g_slice_free (GvdbTable, table);
+ }
+}
+
+GvdbTable *
+dconf_mock_gvdb_table_ref (GvdbTable *table)
+{
+ g_atomic_int_inc (&table->ref_count);
+
+ return table;
+}
+
+GvdbTable *
+gvdb_table_get_table (GvdbTable *table,
+ const gchar *key)
+{
+ DConfMockGvdbItem *item;
+ GvdbTable *subtable;
+
+ item = g_hash_table_lookup (table->table, key);
+
+ if (item && item->table)
+ subtable = dconf_mock_gvdb_table_ref (item->table);
+ else
+ subtable = NULL;
+
+ return subtable;
+}
+
+gboolean
+gvdb_table_has_value (GvdbTable *table,
+ const gchar *key)
+{
+ DConfMockGvdbItem *item;
+
+ item = g_hash_table_lookup (table->table, key);
+
+ return item && item->value;
+}
+
+GVariant *
+gvdb_table_get_value (GvdbTable *table,
+ const gchar *key)
+{
+ DConfMockGvdbItem *item;
+
+ item = g_hash_table_lookup (table->table, key);
+
+ return (item && item->value) ? g_variant_ref (item->value) : NULL;
+}
+
+gchar **
+gvdb_table_list (GvdbTable *table,
+ const gchar *key)
+{
+ const gchar * const result[] = { "value", NULL };
+
+ g_assert_cmpstr (key, ==, "/");
+
+ if (!gvdb_table_has_value (table, "/value"))
+ return NULL;
+
+ return g_strdupv ((gchar **) result);
+}
+
+gchar **
+gvdb_table_get_names (GvdbTable *table,
+ gint *length)
+{
+ if (length)
+ *length = 0;
+
+ return g_new0 (gchar *, 0 + 1);
+}
+
+GvdbTable *
+gvdb_table_new (const gchar *filename,
+ gboolean trusted,
+ GError **error)
+{
+ GvdbTable *table;
+
+ g_mutex_lock (&dconf_mock_gvdb_lock);
+ dconf_mock_gvdb_init ();
+ table = g_hash_table_lookup (dconf_mock_gvdb_tables, filename);
+ if (table)
+ dconf_mock_gvdb_table_ref (table);
+ g_mutex_unlock (&dconf_mock_gvdb_lock);
+
+ if (table == NULL)
+ g_set_error (error, G_FILE_ERROR, G_FILE_ERROR_NOENT, "this gvdb does not exist");
+
+ return table;
+}
+
+gboolean
+gvdb_table_is_valid (GvdbTable *table)
+{
+ return table->is_valid;
+}
+
+void
+dconf_mock_gvdb_table_invalidate (GvdbTable *table)
+{
+ table->is_valid = FALSE;
+}
diff --git a/tests/dconf-mock-shm.c b/tests/dconf-mock-shm.c
new file mode 100644
index 0000000..588667e
--- /dev/null
+++ b/tests/dconf-mock-shm.c
@@ -0,0 +1,124 @@
+#include "../shm/dconf-shm.h"
+
+#include "dconf-mock.h"
+
+typedef struct
+{
+ guint8 flagged;
+ gint ref_count;
+} DConfMockShm;
+
+static GHashTable *dconf_mock_shm_table;
+static GMutex dconf_mock_shm_lock;
+static GString *dconf_mock_shm_log;
+
+static void
+dconf_mock_shm_unref (gpointer data)
+{
+ DConfMockShm *shm = data;
+
+ if (g_atomic_int_dec_and_test (&shm->ref_count))
+ g_slice_free (DConfMockShm, shm);
+}
+
+static DConfMockShm *
+dconf_mock_shm_ref (DConfMockShm *shm)
+{
+ g_atomic_int_inc (&shm->ref_count);
+
+ return shm;
+}
+
+guint8 *
+dconf_shm_open (const gchar *name)
+{
+ DConfMockShm *shm;
+
+ g_mutex_lock (&dconf_mock_shm_lock);
+
+ if G_UNLIKELY (dconf_mock_shm_table == NULL)
+ {
+ dconf_mock_shm_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, dconf_mock_shm_unref);
+ dconf_mock_shm_log = g_string_new (NULL);
+ }
+
+ shm = g_hash_table_lookup (dconf_mock_shm_table, name);
+ if (shm == NULL)
+ {
+ shm = g_slice_new0 (DConfMockShm);
+ g_hash_table_insert (dconf_mock_shm_table, g_strdup (name), dconf_mock_shm_ref (shm));
+ }
+
+ /* before unlocking... */
+ dconf_mock_shm_ref (shm);
+
+ g_string_append_printf (dconf_mock_shm_log, "open %s;", name);
+
+ g_mutex_unlock (&dconf_mock_shm_lock);
+
+ return &shm->flagged;
+}
+
+void
+dconf_shm_close (guint8 *shm)
+{
+ if (shm)
+ {
+ g_mutex_lock (&dconf_mock_shm_lock);
+ g_string_append (dconf_mock_shm_log, "close;");
+ g_mutex_unlock (&dconf_mock_shm_lock);
+
+ dconf_mock_shm_unref (shm);
+ }
+}
+
+gint
+dconf_mock_shm_flag (const gchar *name)
+{
+ DConfMockShm *shm;
+ gint count = 0;
+
+ g_mutex_lock (&dconf_mock_shm_lock);
+ shm = g_hash_table_lookup (dconf_mock_shm_table, name);
+ if (shm)
+ {
+ shm->flagged = 1;
+ count = shm->ref_count;
+ g_hash_table_remove (dconf_mock_shm_table, name);
+ }
+ g_mutex_unlock (&dconf_mock_shm_lock);
+
+ return count;
+}
+
+void
+dconf_mock_shm_reset (void)
+{
+ g_mutex_lock (&dconf_mock_shm_lock);
+ if (dconf_mock_shm_table != NULL)
+ {
+ GHashTableIter iter;
+ gpointer value;
+
+ g_hash_table_iter_init (&iter, dconf_mock_shm_table);
+ while (g_hash_table_iter_next (&iter, NULL, &value))
+ {
+ DConfMockShm *shm = value;
+
+ g_assert_cmpint (shm->ref_count, ==, 1);
+ g_hash_table_iter_remove (&iter);
+ }
+
+ g_string_truncate (dconf_mock_shm_log, 0);
+ }
+ g_mutex_unlock (&dconf_mock_shm_lock);
+}
+
+void
+dconf_mock_shm_assert_log (const gchar *expected_log)
+{
+ g_mutex_lock (&dconf_mock_shm_lock);
+ g_assert_cmpstr (dconf_mock_shm_log->str, ==, expected_log);
+ g_string_truncate (dconf_mock_shm_log, 0);
+ g_mutex_unlock (&dconf_mock_shm_lock);
+}
diff --git a/tests/dconf-mock.h b/tests/dconf-mock.h
new file mode 100644
index 0000000..b3bdcba
--- /dev/null
+++ b/tests/dconf-mock.h
@@ -0,0 +1,37 @@
+#ifndef __dconf_mock_h__
+#define __dconf_mock_h__
+
+#include "../gvdb/gvdb-reader.h"
+#include <gio/gio.h>
+
+typedef GVariant * (* DConfMockDBusSyncCallHandler) (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ const GVariantType *expected_type,
+ GError **error);
+
+extern DConfMockDBusSyncCallHandler dconf_mock_dbus_sync_call_handler;
+extern GQueue dconf_mock_dbus_outstanding_call_handles;
+
+void dconf_mock_dbus_async_reply (GVariant *reply,
+ GError *error);
+void dconf_mock_dbus_assert_no_async (void);
+
+void dconf_mock_shm_reset (void);
+gint dconf_mock_shm_flag (const gchar *name);
+void dconf_mock_shm_assert_log (const gchar *expected_log);
+
+GvdbTable * dconf_mock_gvdb_table_new (void);
+void dconf_mock_gvdb_table_insert (GvdbTable *table,
+ const gchar *name,
+ GVariant *value,
+ GvdbTable *subtable);
+void dconf_mock_gvdb_table_invalidate (GvdbTable *table);
+void dconf_mock_gvdb_install (const gchar *filename,
+ GvdbTable *table);
+GvdbTable * dconf_mock_gvdb_table_ref (GvdbTable *table);
+
+#endif
diff --git a/tests/engine.c b/tests/engine.c
new file mode 100644
index 0000000..fd2a348
--- /dev/null
+++ b/tests/engine.c
@@ -0,0 +1,2100 @@
+#define _GNU_SOURCE
+
+#include "../engine/dconf-engine.h"
+#include "../engine/dconf-engine-profile.h"
+#include "../engine/dconf-engine-mockable.h"
+#include "../common/dconf-enums.h"
+#include "dconf-mock.h"
+
+#include <glib/gstdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <dlfcn.h>
+#include <math.h>
+
+/* Interpose to catch fopen(SYSCONFDIR "/dconf/profile/user") */
+static const gchar *filename_to_replace;
+static const gchar *filename_to_replace_it_with;
+
+FILE *
+dconf_engine_fopen (const char *filename,
+ const char *mode)
+{
+
+ if (filename_to_replace && g_str_equal (filename, filename_to_replace))
+ {
+ /* Crash if this file was unexpectedly opened */
+ g_assert (filename_to_replace_it_with != NULL);
+ filename = filename_to_replace_it_with;
+ }
+
+ return fopen (filename, mode);
+}
+
+static void assert_no_messages (void);
+static void assert_pop_message (const gchar *expected_domain,
+ GLogLevelFlags expected_log_level,
+ const gchar *expected_message_fragment);
+static void assert_maybe_pop_message (const gchar *expected_domain,
+ GLogLevelFlags expected_log_level,
+ const gchar *expected_message_fragment);
+
+static GThread *main_thread;
+static GString *change_log;
+
+void
+dconf_engine_change_notify (DConfEngine *engine,
+ const gchar *prefix,
+ const gchar * const *changes,
+ const gchar *tag,
+ gboolean is_writability,
+ gpointer origin_tag,
+ gpointer user_data)
+{
+ gchar *joined;
+
+ if (!change_log)
+ return;
+
+ if (is_writability)
+ g_string_append (change_log, "w:");
+
+ joined = g_strjoinv (",", (gchar **) changes);
+ g_string_append_printf (change_log, "%s:%d:%s:%s;",
+ prefix, g_strv_length ((gchar **) changes), joined,
+ tag ? tag : "nil");
+ g_free (joined);
+}
+
+static void
+verify_and_free (DConfEngineSource **sources,
+ gint n_sources,
+ const gchar * const *expected_names,
+ gint n_expected)
+{
+ gint i;
+
+ g_assert_cmpint (n_sources, ==, n_expected);
+
+ g_assert ((sources == NULL) == (n_sources == 0));
+
+ for (i = 0; i < n_sources; i++)
+ {
+ g_assert_cmpstr (sources[i]->name, ==, expected_names[i]);
+ dconf_engine_source_free (sources[i]);
+ }
+
+ g_free (sources);
+}
+
+static void
+test_five_times (const gchar *filename,
+ gint n_expected,
+ ...)
+{
+ const gchar **expected_names;
+ DConfEngineSource **sources;
+ gint n_sources;
+ va_list ap;
+ gint i;
+
+ expected_names = g_new (const gchar *, n_expected);
+ va_start (ap, n_expected);
+ for (i = 0; i < n_expected; i++)
+ expected_names[i] = va_arg (ap, const gchar *);
+ va_end (ap);
+
+ /* first try by supplying the profile filename via the API */
+ g_assert (g_getenv ("DCONF_PROFILE") == NULL);
+ g_assert (filename_to_replace == NULL);
+ sources = dconf_engine_profile_open (filename, &n_sources);
+ verify_and_free (sources, n_sources, expected_names, n_expected);
+
+ /* next try supplying it via the environment */
+ g_setenv ("DCONF_PROFILE", filename, TRUE);
+ g_assert (filename_to_replace == NULL);
+ sources = dconf_engine_profile_open (NULL, &n_sources);
+ verify_and_free (sources, n_sources, expected_names, n_expected);
+ g_unsetenv ("DCONF_PROFILE");
+
+ /* next try supplying a profile name via API and intercepting fopen */
+ filename_to_replace = SYSCONFDIR "/dconf/profile/myprofile";
+ filename_to_replace_it_with = filename;
+ g_assert (g_getenv ("DCONF_PROFILE") == NULL);
+ sources = dconf_engine_profile_open ("myprofile", &n_sources);
+ verify_and_free (sources, n_sources, expected_names, n_expected);
+ filename_to_replace = NULL;
+
+ /* next try the same, via the environment */
+ g_setenv ("DCONF_PROFILE", "myprofile", TRUE);
+ filename_to_replace = SYSCONFDIR "/dconf/profile/myprofile";
+ filename_to_replace_it_with = filename;
+ sources = dconf_engine_profile_open (NULL, &n_sources);
+ verify_and_free (sources, n_sources, expected_names, n_expected);
+ g_unsetenv ("DCONF_PROFILE");
+ filename_to_replace = NULL;
+
+ /* next try to have dconf pick it up as the default user profile */
+ filename_to_replace = SYSCONFDIR "/dconf/profile/user";
+ filename_to_replace_it_with = filename;
+ g_assert (g_getenv ("DCONF_PROFILE") == NULL);
+ sources = dconf_engine_profile_open (NULL, &n_sources);
+ verify_and_free (sources, n_sources, expected_names, n_expected);
+ filename_to_replace = NULL;
+
+ filename_to_replace_it_with = NULL;
+ g_free (expected_names);
+}
+
+typedef struct
+{
+ const gchar *profile_path;
+ const gchar *expected_stderr_pattern;
+} ProfileParserOpenData;
+
+static void
+test_profile_parser_errors (gconstpointer test_data)
+{
+ const ProfileParserOpenData *data = test_data;
+ DConfEngineSource **sources;
+ gint n_sources;
+
+ if (g_test_subprocess ())
+ {
+ g_log_set_always_fatal (G_LOG_LEVEL_ERROR);
+
+ sources = dconf_engine_profile_open (data->profile_path, &n_sources);
+ g_assert_cmpint (n_sources, ==, 0);
+ g_assert (sources == NULL);
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+ g_test_trap_assert_stderr (data->expected_stderr_pattern);
+}
+
+static void
+test_profile_parser (void)
+{
+ DConfEngineSource **sources;
+ gint n_sources;
+
+ test_five_times (SRCDIR "/profile/empty-profile", 0);
+ test_five_times (SRCDIR "/profile/test-profile", 1, "test");
+ test_five_times (SRCDIR "/profile/colourful", 4,
+ "user",
+ "other",
+ "verylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongname",
+ "nonewline");
+ test_five_times (SRCDIR "/profile/dos", 2, "user", "site");
+ test_five_times (SRCDIR "/profile/no-newline-longline", 0);
+ test_five_times (SRCDIR "/profile/many-sources", 10,
+ "user", "local", "room", "floor", "building",
+ "site", "region", "division", "country", "global");
+
+ /* finally, test that we get the default profile if the user profile
+ * file cannot be located and we do not specify another profile.
+ */
+ filename_to_replace = SYSCONFDIR "/dconf/profile/user";
+ filename_to_replace_it_with = SRCDIR "/profile/this-file-does-not-exist";
+ g_assert (g_getenv ("DCONF_PROFILE") == NULL);
+ sources = dconf_engine_profile_open (NULL, &n_sources);
+ filename_to_replace = NULL;
+ g_assert_cmpint (n_sources, ==, 1);
+ g_assert_cmpstr (sources[0]->name, ==, "user");
+ dconf_engine_source_free (sources[0]);
+ g_free (sources);
+
+ dconf_mock_shm_reset ();
+}
+
+static gpointer
+test_signal_threadsafety_worker (gpointer user_data)
+{
+ gint *finished = user_data;
+ gint i;
+
+ for (i = 0; i < 20000; i++)
+ {
+ DConfEngine *engine;
+
+ engine = dconf_engine_new (NULL, NULL, NULL);
+ dconf_engine_unref (engine);
+ }
+
+ g_atomic_int_inc (finished);
+
+ return NULL;
+}
+
+static void
+test_signal_threadsafety (void)
+{
+#define N_WORKERS 4
+ GVariant *parameters;
+ gint finished = 0;
+ gint i;
+
+ parameters = g_variant_new_parsed ("('/test/key', [''], 'tag')");
+ g_variant_ref_sink (parameters);
+
+ for (i = 0; i < N_WORKERS; i++)
+ g_thread_unref (g_thread_new ("testcase worker", test_signal_threadsafety_worker, &finished));
+
+ while (g_atomic_int_get (&finished) < N_WORKERS)
+ dconf_engine_handle_dbus_signal (G_BUS_TYPE_SESSION,
+ ":1.2.3",
+ "/ca/desrt/dconf/Writer/user",
+ "Notify", parameters);
+ g_variant_unref (parameters);
+
+ dconf_mock_shm_reset ();
+}
+
+static void
+test_user_source (void)
+{
+ DConfEngineSource *source;
+ GvdbTable *table;
+ GvdbTable *locks;
+ gboolean reopened;
+
+ /* Create the source from a clean slate */
+ source = dconf_engine_source_new ("user-db:user");
+ g_assert (source != NULL);
+ g_assert (source->values == NULL);
+ g_assert (source->locks == NULL);
+
+ /* Refresh it the first time.
+ * This should cause it to open the shm.
+ * FALSE should be returned because there is no database file.
+ */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+ dconf_mock_shm_assert_log ("open user;");
+
+ /* Try to refresh it. There must be no IO at this point. */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+ dconf_mock_shm_assert_log ("");
+
+ /* Add a real database. */
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_table_insert (table, "/values/int32", g_variant_new_int32 (123456), NULL);
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", table);
+
+ /* Try to refresh it again.
+ * Because we didn't flag the change there must still be no IO.
+ */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+ g_assert (source->values == NULL);
+ g_assert (source->locks == NULL);
+ dconf_mock_shm_assert_log ("");
+
+ /* Now flag it and reopen. */
+ dconf_mock_shm_flag ("user");
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values != NULL);
+ g_assert (source->locks == NULL);
+ g_assert (gvdb_table_has_value (source->values, "/values/int32"));
+ dconf_mock_shm_assert_log ("close;open user;");
+
+ /* Do it again -- should get the same result, after some IO */
+ dconf_mock_shm_flag ("user");
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values != NULL);
+ g_assert (source->locks == NULL);
+ dconf_mock_shm_assert_log ("close;open user;");
+
+ /* "Delete" the gvdb and make sure dconf notices after a flag */
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", NULL);
+ dconf_mock_shm_flag ("user");
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values == NULL);
+ g_assert (source->locks == NULL);
+ dconf_mock_shm_assert_log ("close;open user;");
+
+ /* Add a gvdb with a lock */
+ table = dconf_mock_gvdb_table_new ();
+ locks = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_table_insert (table, "/values/int32", g_variant_new_int32 (123456), NULL);
+ dconf_mock_gvdb_table_insert (locks, "/values/int32", g_variant_new_boolean (TRUE), NULL);
+ dconf_mock_gvdb_table_insert (table, ".locks", NULL, locks);
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", table);
+
+ /* Reopen and check if we have the lock */
+ dconf_mock_shm_flag ("user");
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values != NULL);
+ g_assert (source->locks != NULL);
+ g_assert (gvdb_table_has_value (source->values, "/values/int32"));
+ g_assert (gvdb_table_has_value (source->locks, "/values/int32"));
+ dconf_mock_shm_assert_log ("close;open user;");
+
+ /* Reopen one last time */
+ dconf_mock_shm_flag ("user");
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values != NULL);
+ g_assert (source->locks != NULL);
+ dconf_mock_shm_assert_log ("close;open user;");
+
+ dconf_engine_source_free (source);
+ dconf_mock_shm_assert_log ("close;");
+
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", NULL);
+ dconf_mock_shm_reset ();
+}
+
+static void
+test_file_source (void)
+{
+ DConfEngineSource *source;
+ gboolean reopened;
+ GvdbTable *table;
+ GVariant *value;
+
+ source = dconf_engine_source_new ("file-db:/path/to/db");
+ g_assert (source != NULL);
+ g_assert (source->values == NULL);
+ g_assert (source->locks == NULL);
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (source->values == NULL);
+ g_assert (source->locks == NULL);
+ dconf_engine_source_free (source);
+
+ assert_pop_message ("dconf", G_LOG_LEVEL_WARNING, "unable to open file '/path/to/db'");
+
+ source = dconf_engine_source_new ("file-db:/path/to/db");
+ g_assert (source != NULL);
+ g_assert (source->values == NULL);
+ g_assert (source->locks == NULL);
+
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_table_insert (table, "/value", g_variant_new_string ("first file"), NULL);
+ dconf_mock_gvdb_install ("/path/to/db", table);
+
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values);
+ g_assert (source->locks == NULL);
+ value = gvdb_table_get_value (source->values, "/value");
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "first file");
+ g_variant_unref (value);
+
+ /* Of course this should do nothing... */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+
+ /* Invalidate and replace */
+ dconf_mock_gvdb_table_invalidate (table);
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_table_insert (table, "/value", g_variant_new_string ("second file"), NULL);
+ dconf_mock_gvdb_install ("/path/to/db", table);
+
+ /* Even when invalidated, this should still do nothing... */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+ value = gvdb_table_get_value (source->values, "/value");
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "first file");
+ g_variant_unref (value);
+
+ dconf_mock_gvdb_install ("/path/to/db", NULL);
+ dconf_engine_source_free (source);
+}
+
+
+static gboolean service_db_created;
+static GvdbTable *service_db_table;
+
+static GVariant *
+handle_service_request (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ const GVariantType *expected_type,
+ GError **error)
+{
+ g_assert_cmpstr (bus_name, ==, "ca.desrt.dconf");
+ g_assert_cmpstr (interface_name, ==, "ca.desrt.dconf.Writer");
+ g_assert_cmpstr (method_name, ==, "Init");
+ g_assert_cmpstr (g_variant_get_type_string (parameters), ==, "()");
+
+ if (g_str_equal (object_path, "/ca/desrt/dconf/shm/nil"))
+ {
+ service_db_table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_table_insert (service_db_table, "/values/int32", g_variant_new_int32 (123456), NULL);
+ dconf_mock_gvdb_install ("/RUNTIME/dconf-service/shm/nil", service_db_table);
+
+ /* Make sure this only happens the first time... */
+ g_assert (!service_db_created);
+ service_db_created = TRUE;
+
+ return g_variant_new ("()");
+ }
+ else
+ {
+ g_set_error_literal (error, G_FILE_ERROR, G_FILE_ERROR_NOENT, "Unknown DB type");
+ return NULL;
+ }
+}
+
+static void
+test_service_source (void)
+{
+ DConfEngineSource *source;
+ gboolean reopened;
+
+ /* Make sure we deal with errors from the service sensibly */
+ if (g_test_subprocess ())
+ {
+ g_log_set_always_fatal (G_LOG_LEVEL_ERROR);
+
+ source = dconf_engine_source_new ("service-db:unknown/nil");
+ dconf_mock_dbus_sync_call_handler = handle_service_request;
+ g_assert (source != NULL);
+ g_assert (source->values == NULL);
+ g_assert (source->locks == NULL);
+ reopened = dconf_engine_source_refresh (source);
+
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+ g_test_trap_assert_stderr ("*WARNING*: unable to open file*unknown/nil*expect degraded performance*");
+
+ /* Set up one that will work */
+ source = dconf_engine_source_new ("service-db:shm/nil");
+ g_assert (source != NULL);
+ g_assert (source->values == NULL);
+ g_assert (source->locks == NULL);
+
+ /* Refresh it the first time.
+ *
+ * This should cause the service to be asked to create it.
+ *
+ * This should return TRUE because we just opened it.
+ */
+ dconf_mock_dbus_sync_call_handler = handle_service_request;
+ reopened = dconf_engine_source_refresh (source);
+ dconf_mock_dbus_sync_call_handler = NULL;
+ g_assert (service_db_created);
+ g_assert (reopened);
+
+ /* After that, a refresh should be a no-op. */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+
+ /* Close it and reopen it, ensuring that we don't hit the service
+ * again (because the file already exists).
+ *
+ * Note: dconf_mock_dbus_sync_call_handler = NULL, so D-Bus calls will
+ * assert.
+ */
+ dconf_engine_source_free (source);
+ source = dconf_engine_source_new ("service-db:shm/nil");
+ g_assert (source != NULL);
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+
+ /* Make sure it has the content we expect to see */
+ g_assert (gvdb_table_has_value (source->values, "/values/int32"));
+
+ /* Now invalidate it and replace it with an empty one */
+ dconf_mock_gvdb_table_invalidate (service_db_table);
+ service_db_table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install ("/RUNTIME/dconf-service/shm/nil", service_db_table);
+
+ /* Now reopening should get the new one */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+
+ /* ...and we should find it to be empty */
+ g_assert (!gvdb_table_has_value (source->values, "/values/int32"));
+
+ /* We're done. */
+ dconf_engine_source_free (source);
+
+ /* This should not have done any shm... */
+ dconf_mock_shm_assert_log ("");
+
+ dconf_mock_gvdb_install ("/RUNTIME/dconf-service/shm/nil", NULL);
+ service_db_table = NULL;
+}
+
+static void
+test_system_source (void)
+{
+ DConfEngineSource *source;
+ GvdbTable *first_table;
+ GvdbTable *next_table;
+ gboolean reopened;
+
+ source = dconf_engine_source_new ("system-db:site");
+ g_assert (source != NULL);
+
+ /* Check to see that we get the warning about the missing file. */
+ if (g_test_subprocess ())
+ {
+ g_log_set_always_fatal (G_LOG_LEVEL_ERROR);
+
+ /* Failing to open should return FALSE from refresh */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+ g_assert (source->values == NULL);
+
+ /* Attempt the reopen to make sure we don't get two warnings.
+ * We should see FALSE again since we go from NULL to NULL.
+ */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+
+ /* Create the file after the fact and make sure it opens properly */
+ first_table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", first_table);
+
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values != NULL);
+
+ dconf_engine_source_free (source);
+
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+ /* Check that we only saw the warning, but only one time. */
+ g_test_trap_assert_stderr ("*this gvdb does not exist; expect degraded performance*");
+ g_test_trap_assert_stderr_unmatched ("*degraded*degraded*");
+
+ /* Create the file before the first refresh attempt */
+ first_table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", first_table);
+ /* Hang on to a copy for ourselves for below... */
+ dconf_mock_gvdb_table_ref (first_table);
+
+ /* See that we get the database. */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values == first_table);
+
+ /* Do a refresh, make sure there is no change. */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+ g_assert (source->values == first_table);
+
+ /* Replace the table on "disk" but don't invalidate the old one */
+ next_table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", next_table);
+
+ /* Make sure the old table remains open (ie: no IO performed) */
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+ g_assert (source->values == first_table);
+
+ /* Now mark the first table invalid and reopen */
+ dconf_mock_gvdb_table_invalidate (first_table);
+ gvdb_table_free (first_table);
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (reopened);
+ g_assert (source->values == next_table);
+
+ /* Remove the file entirely and do the same thing */
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", NULL);
+ reopened = dconf_engine_source_refresh (source);
+ g_assert (!reopened);
+
+ dconf_engine_source_free (source);
+}
+
+static void
+invalidate_state (guint n_sources,
+ guint source_types,
+ gpointer *state)
+{
+ gint i;
+
+ for (i = 0; i < n_sources; i++)
+ if (source_types & (1u << i))
+ {
+ if (state[i])
+ {
+ dconf_mock_gvdb_table_invalidate (state[i]);
+ gvdb_table_free (state[i]);
+ }
+ }
+ else
+ {
+ dconf_mock_shm_flag (state[i]);
+ g_free (state[i]);
+ }
+}
+
+static void
+setup_state (guint n_sources,
+ guint source_types,
+ guint database_state,
+ gpointer *state)
+{
+ gint i;
+
+ for (i = 0; i < n_sources; i++)
+ {
+ guint contents = database_state % 7;
+ GvdbTable *table = NULL;
+ gchar *filename;
+
+ if (contents)
+ {
+ table = dconf_mock_gvdb_table_new ();
+
+ /* Even numbers get the value setup */
+ if ((contents & 1) == 0)
+ dconf_mock_gvdb_table_insert (table, "/value", g_variant_new_uint32 (i), NULL);
+
+ /* Numbers above 2 get the locks table */
+ if (contents > 2)
+ {
+ GvdbTable *locks;
+
+ locks = dconf_mock_gvdb_table_new ();
+
+ /* Numbers above 4 get the lock set */
+ if (contents > 4)
+ dconf_mock_gvdb_table_insert (locks, "/value", g_variant_new_boolean (TRUE), NULL);
+
+ dconf_mock_gvdb_table_insert (table, ".locks", NULL, locks);
+ }
+ }
+
+ if (source_types & (1u << i))
+ {
+ if (state)
+ {
+ if (table)
+ state[i] = dconf_mock_gvdb_table_ref (table);
+ else
+ state[i] = NULL;
+ }
+
+ filename = g_strdup_printf (SYSCONFDIR "/dconf/db/db%d", i);
+ }
+ else
+ {
+ if (state)
+ state[i] = g_strdup_printf ("db%d", i);
+
+ filename = g_strdup_printf ("/HOME/.config/dconf/db%d", i);
+ }
+
+ dconf_mock_gvdb_install (filename, table);
+ g_free (filename);
+
+ database_state /= 7;
+ }
+}
+
+static void
+create_profile (const gchar *filename,
+ guint n_sources,
+ guint source_types)
+{
+ GError *error = NULL;
+ GString *profile;
+ gint i;
+
+ profile = g_string_new (NULL);
+ for (i = 0; i < n_sources; i++)
+ if (source_types & (1u << i))
+ g_string_append_printf (profile, "system-db:db%d\n", i);
+ else
+ g_string_append_printf (profile, "user-db:db%d\n", i);
+ g_file_set_contents (filename, profile->str, profile->len, &error);
+ g_assert_no_error (error);
+ g_string_free (profile, TRUE);
+}
+
+static GQueue read_through_queues[12];
+
+static void
+check_read (DConfEngine *engine,
+ guint n_sources,
+ guint source_types,
+ guint database_state)
+{
+ gboolean any_values = FALSE;
+ gboolean any_locks = FALSE;
+ guint first_contents;
+ gint underlying = -1;
+ gint expected = -1;
+ gboolean writable;
+ GVariant *value;
+ gchar **list;
+ guint i;
+ gint n;
+
+ /* The value we expect to read is number of the first source that has
+ * the value set (ie: odd digit in database_state) up to the lowest
+ * level lock.
+ *
+ * We go over each database. If 'expected' has not yet been set and
+ * we find that we should have a value in this database, we set it.
+ * If we find that we should have a lock in this database, we unset
+ * any previous values (since they should not have been written).
+ *
+ * We intentionally code this loop in a different way than the one in
+ * dconf itself is currently implemented...
+ *
+ * We also take note of if we saw any locks and cross-check that with
+ * dconf_engine_is_writable(). We check if we saw and values at all
+ * and cross-check that with dconf_engine_list() (which ignores
+ * locks).
+ */
+ first_contents = database_state % 7;
+ for (i = 0; i < n_sources; i++)
+ {
+ guint contents = database_state % 7;
+
+ /* A lock here should prevent higher reads */
+ if (contents > 4)
+ {
+ /* Locks in the first database don't count... */
+ if (i != 0)
+ any_locks = TRUE;
+ expected = -1;
+ }
+
+ /* A value here should be read */
+ if (contents && !(contents & 1))
+ {
+ if (i != 0 && underlying == -1)
+ underlying = i;
+
+ if (expected == -1)
+ {
+ any_values = TRUE;
+ expected = i;
+ }
+ }
+
+ database_state /= 7;
+ }
+
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "/value");
+
+ if (expected != -1)
+ {
+ g_assert (g_variant_is_of_type (value, G_VARIANT_TYPE_UINT32));
+ g_assert_cmpint (g_variant_get_uint32 (value), ==, expected);
+ g_variant_unref (value);
+ }
+ else
+ g_assert (value == NULL);
+
+ /* We are writable if the first database is a user database and we
+ * didn't encounter any locks...
+ */
+ writable = dconf_engine_is_writable (engine, "/value");
+ g_assert_cmpint (writable, ==, n_sources && !(source_types & 1) && !any_locks);
+
+ /* Check various read-through scenarios. Read-through should only be
+ * effective if the database is writable.
+ */
+ for (i = 0; i < G_N_ELEMENTS (read_through_queues); i++)
+ {
+ gint our_expected = expected;
+
+ if (writable)
+ {
+ /* If writable, see what our changeset did.
+ *
+ * 0: nothing
+ * 1: reset value (should see underlying value)
+ * 2: set value to 123
+ */
+ if ((i % 3) == 1)
+ our_expected = underlying;
+ else if ((i % 3) == 2)
+ our_expected = 123;
+ }
+
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, &read_through_queues[i], "/value");
+
+ if (our_expected != -1)
+ {
+ g_assert (g_variant_is_of_type (value, G_VARIANT_TYPE_UINT32));
+ g_assert_cmpint (g_variant_get_uint32 (value), ==, our_expected);
+ g_variant_unref (value);
+ }
+ else
+ g_assert (value == NULL);
+ }
+
+ /* Check listing */
+ g_strfreev (dconf_engine_list (engine, "/", &n));
+ list = dconf_engine_list (engine, "/", NULL);
+ g_assert_cmpint (g_strv_length (list), ==, n);
+ if (any_values)
+ {
+ g_assert_cmpstr (list[0], ==, "value");
+ g_assert (list[1] == NULL);
+ }
+ else
+ g_assert (list[0] == NULL);
+ g_strfreev (list);
+
+ /* Check the user value.
+ *
+ * This should be set only in the case that the first database is a
+ * user database (ie: writable) and the contents of that database are
+ * set (ie: 2, 4 or 6). See the table in the comment below.
+ *
+ * Note: we do not consider locks.
+ */
+ value = dconf_engine_read (engine, DCONF_READ_USER_VALUE, NULL, "/value");
+ if (value)
+ {
+ g_assert (first_contents && !(first_contents & 1) && !(source_types & 1));
+ g_assert (g_variant_is_of_type (value, G_VARIANT_TYPE_UINT32));
+ g_assert_cmpint (g_variant_get_uint32 (value), ==, 0);
+ g_variant_unref (value);
+ }
+ else
+ {
+ /* Three possibilities for failure:
+ * - first db did not exist
+ * - value was missing from first db
+ * - first DB was system-db
+ */
+ g_assert (!first_contents || (first_contents & 1) || (source_types & 1));
+ }
+
+ /* Check read_through vs. user-value */
+ for (i = 0; i < G_N_ELEMENTS (read_through_queues); i++)
+ {
+ /* It is only possible here to see one of three possibilities:
+ *
+ * - NULL
+ * - 0 (value from user's DB)
+ * - 123 (value from queue)
+ *
+ * We see these values regardless of writability. We do however
+ * ensure that we have a writable database as the first one.
+ */
+ value = dconf_engine_read (engine, DCONF_READ_USER_VALUE, &read_through_queues[i], "/value");
+
+ /* If we have no first source, or the first source is non-user
+ * than we should always do nothing (since we can't queue changes
+ * against a system db or one that doesn't exist).
+ */
+ if (n_sources == 0 || (source_types & 1) || (i % 3) == 0)
+ {
+ /* Changeset did nothing, so it should be same as above. */
+ if (value)
+ {
+ g_assert (first_contents && !(first_contents & 1) && !(source_types & 1));
+ g_assert (g_variant_is_of_type (value, G_VARIANT_TYPE_UINT32));
+ g_assert_cmpint (g_variant_get_uint32 (value), ==, 0);
+ }
+ else
+ g_assert (!first_contents || (first_contents & 1) || (source_types & 1));
+ }
+ else if ((i % 3) == 1)
+ {
+ /* Changeset did a reset, so we should always see NULL */
+ g_assert (value == NULL);
+ }
+ else if ((i % 3) == 2)
+ {
+ /* Changeset set a value, so we should see it */
+ g_assert_cmpint (g_variant_get_uint32 (value), ==, 123);
+ }
+
+ if (value)
+ g_variant_unref (value);
+ }
+}
+
+static void
+test_read (void)
+{
+#define MAX_N_SOURCES 2
+ gpointer state[MAX_N_SOURCES];
+ gchar *profile_filename;
+ GError *error = NULL;
+ DConfEngine *engine;
+ guint i, j, k;
+ guint n;
+
+ /* This test throws a lot of messages about missing databases.
+ * Capture and ignore them.
+ */
+
+ /* Our test strategy is as follows:
+ *
+ * We only test a single key name. It is assumed that gvdb is working
+ * properly already so we are only interested in interactions between
+ * multiple databases for a given key name.
+ *
+ * The outermost loop is over 'n'. This is how many sources are in
+ * our test. We test 0 to 3 (which should be enough to cover all
+ * 'interesting' possibilities). 4 takes too long to run (2*7*7 ~=
+ * 100 times as long as 3).
+ *
+ * The next loop is over 'i'. This goes from 0 to 2^n - 1, with each
+ * bit deciding the type of source of the i-th element
+ *
+ * 0: user
+ *
+ * 1: system
+ *
+ * The next loop is over 'j'. This goes from 0 to 7^n - 1, with each
+ * base-7 digit deciding the state of the database file associated
+ * with the i-th source:
+ *
+ * j file has value has ".locks" has lock
+ * ----------------------------------------------------
+ * 0 0 - - -
+ * 1 1 0 0 -
+ * 2 1 1 0 -
+ * 3 1 0 1 0
+ * 4 1 1 1 0
+ * 5 1 0 1 1
+ * 6 1 1 1 1
+ *
+ * Where 'file' is if the database file exists, 'has value' is if a
+ * value exists at '/value' within the file, 'has ".locks"' is if
+ * there is a ".locks" subtable and 'has lock' is if there is a lock
+ * for '/value' within that table.
+ *
+ * Finally, we loop over 'k' as a state to transition to ('k' works
+ * the same way as 'j').
+ *
+ * Once we know 'n' and 'i', we can write a profile file.
+ *
+ * Once we know 'j' we can setup the initial state, create the engine
+ * and check that we got the expected value. Then we transition to
+ * state 'k' and make sure everything still works as expected.
+ *
+ * Since we want to test all j->k transitions, we do the initial setup
+ * of the engine (according to j) inside of the 'k' loop, since we
+ * need to test all possible transitions from 'j'.
+ *
+ * We additionally test the effect of read-through queues in 4
+ * situations:
+ *
+ * - NULL: no queue
+ * - 0: queue with no effect
+ * - 1: queue that resets the value
+ * - 2: queue that sets the value to 123
+ *
+ * For the cases (0, 1, 2) we can have multiple types of queue that
+ * achieve the desired effect. We can put more than 3 items in
+ * read_through_queues -- the expected behaviour is dictated by the
+ * value of (i % 3) where i is the array index.
+ */
+ {
+ /* We use a scheme to set up each queue. Again, we assume that
+ * GHashTable is working OK, so we only bother having "/value" as a
+ * changeset item (or not).
+ *
+ * We have an array of strings, each string defining the
+ * configuration of one queue. In each string, each character
+ * represents the contents of a changeset within the queue, in
+ * order.
+ *
+ * ' ' - empty changeset
+ * 's' - set value to 123
+ * 'r' - reset value
+ * 'x' - set value to 321
+ */
+ const gchar *queue_configs[] = {
+ "", "r", "s",
+ " ", "rr", "ss",
+ " ", "rs", "sr",
+ " ", "rx", "sx"
+ };
+ gint i;
+
+ G_STATIC_ASSERT (G_N_ELEMENTS (queue_configs) == G_N_ELEMENTS (read_through_queues));
+ for (i = 0; i < G_N_ELEMENTS (read_through_queues); i++)
+ {
+ const gchar *conf = queue_configs[i];
+ gint j;
+
+ for (j = 0; conf[j]; j++)
+ {
+ DConfChangeset *changeset;
+
+ changeset = dconf_changeset_new ();
+
+ switch (conf[j])
+ {
+ case ' ':
+ break;
+ case 'r':
+ dconf_changeset_set (changeset, "/value", NULL);
+ break;
+ case 's':
+ dconf_changeset_set (changeset, "/value", g_variant_new_uint32 (123));
+ break;
+ case 'x':
+ dconf_changeset_set (changeset, "/value", g_variant_new_uint32 (321));
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ g_queue_push_head (&read_through_queues[i], changeset);
+ }
+ }
+ }
+
+ /* We need a place to put the profile files we use for this test */
+ close (g_file_open_tmp ("dconf-testcase.XXXXXX", &profile_filename, &error));
+ g_assert_no_error (error);
+
+ for (n = 0; n <= MAX_N_SOURCES; n++)
+ for (i = 0; i < pow (2, n); i++)
+ {
+ gint n_possible_states = pow (7, n);
+
+ /* Step 1: write out the profile file */
+ create_profile (profile_filename, n, i);
+
+ for (j = 0; j < n_possible_states; j++)
+ for (k = 0; k < n_possible_states; k++)
+ {
+ guint64 old_state, new_state;
+
+ /* Step 2: setup the state */
+ setup_state (n, i, j, (j != k) ? state : NULL);
+
+ /* Step 3: create the engine */
+ engine = dconf_engine_new (profile_filename, NULL, NULL);
+
+ /* Step 4: read, and check result */
+ check_read (engine, n, i, j);
+ old_state = dconf_engine_get_state (engine);
+
+ /* Step 5: change to the new state */
+ if (j != k)
+ {
+ setup_state (n, i, k, NULL);
+ invalidate_state (n, i, state);
+ }
+
+ /* Step 6: read, and check result */
+ check_read (engine, n, i, k);
+ new_state = dconf_engine_get_state (engine);
+
+ g_assert ((j == k) == (new_state == old_state));
+
+ /* Clean up */
+ setup_state (n, i, 0, NULL);
+ dconf_engine_unref (engine);
+
+ assert_maybe_pop_message ("dconf", G_LOG_LEVEL_WARNING,
+ "unable to open file '" SYSCONFDIR "/dconf/db");
+ }
+ }
+
+ /* Clean up the tempfile we were using... */
+ g_unlink (profile_filename);
+ g_free (profile_filename);
+ dconf_mock_shm_reset ();
+
+ assert_no_messages ();
+}
+
+static void
+test_watch_fast (void)
+{
+ DConfEngine *engine;
+ GvdbTable *table;
+ GVariant *triv;
+ guint64 a, b, c;
+
+ change_log = g_string_new (NULL);
+
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", table);
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", table);
+
+ triv = g_variant_ref_sink (g_variant_new ("()"));
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+ /* Check that establishing a watch works properly in the normal case.
+ */
+ a = dconf_engine_get_state (engine);
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ /* watches do not count as outstanding changes */
+ g_assert (!dconf_engine_has_outstanding (engine));
+ dconf_engine_sync (engine);
+ b = dconf_engine_get_state (engine);
+ g_assert_cmpuint (a, ==, b);
+ /* both AddMatch results come back before shm is flagged */
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_assert_no_async ();
+ dconf_mock_shm_flag ("user");
+ b = dconf_engine_get_state (engine);
+ g_assert_cmpuint (a, !=, b);
+ g_assert_cmpstr (change_log->str, ==, "");
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_assert_no_async ();
+
+ /* Establish a watch and fail the race. */
+ a = dconf_engine_get_state (engine);
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ g_assert (!dconf_engine_has_outstanding (engine));
+ dconf_engine_sync (engine);
+ b = dconf_engine_get_state (engine);
+ g_assert_cmpuint (a, ==, b);
+ /* one AddMatch result comes back -after- shm is flagged */
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_shm_flag ("user");
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_assert_no_async ();
+ b = dconf_engine_get_state (engine);
+ g_assert_cmpuint (a, !=, b);
+ g_assert_cmpstr (change_log->str, ==, "/a/b/c:1::nil;");
+ /* Try to establish a watch again for the same path */
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ g_assert (!dconf_engine_has_outstanding (engine));
+ dconf_engine_sync (engine);
+ c = dconf_engine_get_state (engine);
+ g_assert_cmpuint (b, ==, c);
+ /* The watch result was not sent, because the path was already watched */
+ dconf_mock_dbus_assert_no_async ();
+ c = dconf_engine_get_state (engine);
+ g_assert_cmpuint (b, ==, c);
+ /* Since the path was already being watched,
+ * do not expect a second false change notification */
+ g_assert_cmpstr (change_log->str, ==, "/a/b/c:1::nil;");
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ /* nothing was done, because there is still a subscription left */
+ dconf_mock_dbus_assert_no_async ();
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_assert_no_async ();
+
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", NULL);
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", NULL);
+ dconf_engine_unref (engine);
+ g_string_free (change_log, TRUE);
+ change_log = NULL;
+ g_variant_unref (triv);
+}
+
+static void
+test_watch_fast_simultaneous_subscriptions (void)
+{
+ /**
+ * Test that creating multiple subscriptions to the same path
+ * simultaneously (before receiving replies from D-Bus) only results in
+ * a single D-Bus match rule, and that it is removed at the right time.
+ */
+ DConfEngine *engine;
+ GvdbTable *table;
+ GVariant *triv;
+
+ /* Set up */
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", table);
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", table);
+
+ triv = g_variant_ref_sink (g_variant_new ("()"));
+
+ change_log = g_string_new (NULL);
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+
+ /* Subscribe to the same path 3 times. Both AddMatch results succeed
+ * (one for each source). There is only one for each source*path.
+ */
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ dconf_engine_watch_fast (engine, "/a/b/c");
+
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_assert_no_async ();
+
+ /* Unsubscribe twice, after the AddMatch succeeds. There is still one
+ * active subscription, so no RemoveMatch request is sent. */
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+
+ dconf_mock_dbus_assert_no_async ();
+
+ /* Unsubscribe once more. The number of active subscriptions falls to 0
+ * and the D-Bus match rule is removed */
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_assert_no_async ();
+
+ /* The shm was not flagged at any point - so no change notifications
+ * should not have been sent */
+ g_assert_cmpstr (change_log->str, ==, "");
+
+ /* Clean up */
+ dconf_engine_unref (engine);
+ g_string_free (change_log, TRUE);
+ change_log = NULL;
+ g_variant_unref (triv);
+}
+
+static void
+test_watch_fast_successive_subscriptions (void)
+{
+ /**
+ * Test that subscribing to the same path multiple times successively
+ * (after waiting for any expected replies from D-Bus) results in only
+ * a single D-Bus match rule being created, and that it is created and
+ * destroyed at the right times.
+ */
+ DConfEngine *engine;
+ GvdbTable *table;
+ GVariant *triv;
+
+ /* Set up */
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", table);
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", table);
+
+ triv = g_variant_ref_sink (g_variant_new ("()"));
+
+ change_log = g_string_new (NULL);
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+ /* Subscribe to a path, and simulate a change to the database while the
+ * AddMatch request is in progress */
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ dconf_mock_shm_flag ("user");
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+
+ /* When the AddMatch requests succeeds, expect a change notification
+ * for the path */
+ dconf_mock_dbus_assert_no_async ();
+ g_assert_cmpstr (change_log->str, ==, "/a/b/c:1::nil;");
+
+ /* Subscribe to a path twice again, and simulate a change to the
+ * database */
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ dconf_mock_shm_flag ("user");
+
+ /* There was already a match rule in place, so there should be no D-Bus
+ * requests and no change notifications */
+ dconf_mock_dbus_assert_no_async ();
+ g_assert_cmpstr (change_log->str, ==, "/a/b/c:1::nil;");
+
+ /* Unsubscribe */
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ dconf_mock_dbus_assert_no_async ();
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_assert_no_async ();
+
+
+ /* Clean up */
+ dconf_engine_unref (engine);
+ g_string_free (change_log, TRUE);
+ change_log = NULL;
+ g_variant_unref (triv);
+}
+
+static void
+test_watch_fast_short_lived_subscriptions (void)
+{
+ /**
+ * Test that subscribing and then immediately unsubscribing (without
+ * waiting for replies from D-Bus) multiple times to the same path
+ * correctly triggers D-Bus requests and change notifications in cases
+ * where the D-Bus match rule was not in place when the database was
+ * changed.
+ */
+ DConfEngine *engine;
+ GvdbTable *table;
+ GVariant *triv;
+
+ /* Set up */
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install ("/HOME/.config/dconf/user", table);
+ table = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", table);
+
+ triv = g_variant_ref_sink (g_variant_new ("()"));
+
+ change_log = g_string_new (NULL);
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+ /* Subscribe to a path twice, and simulate a change to the database
+ * while the AddMatch request is in progress */
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ dconf_engine_watch_fast (engine, "/a/b/c");
+ dconf_mock_shm_flag ("user");
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ dconf_engine_unwatch_fast (engine, "/a/b/c");
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_async_reply (triv, NULL);
+ dconf_mock_dbus_assert_no_async ();
+
+ /* When the AddMatch requests succeed, expect a change notification
+ * to have been sent for the path, even though the client has since
+ * unsubscribed. */
+ g_assert_cmpstr (change_log->str, ==, "/a/b/c:1::nil;");
+
+
+ /* Clean up */
+ dconf_engine_unref (engine);
+ g_string_free (change_log, TRUE);
+ change_log = NULL;
+ g_variant_unref (triv);
+}
+
+static const gchar *match_request_type;
+static gboolean got_match_request[5];
+
+static GVariant *
+handle_match_request (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ const GVariantType *expected_type,
+ GError **error)
+{
+ const gchar *match_rule;
+
+ g_assert_cmpstr (bus_name, ==, "org.freedesktop.DBus");
+ /* any object path works... */
+ g_assert_cmpstr (interface_name, ==, "org.freedesktop.DBus");
+ g_assert_cmpstr (method_name, ==, match_request_type);
+ g_assert_cmpstr (g_variant_get_type_string (parameters), ==, "(s)");
+ g_variant_get (parameters, "(&s)", &match_rule);
+ g_assert (strstr (match_rule, "arg0path='/a/b/c'"));
+ g_assert (!got_match_request[bus_type]);
+ got_match_request[bus_type] = TRUE;
+
+ return g_variant_new ("()");
+}
+
+static void
+test_watch_sync (void)
+{
+ DConfEngine *engine;
+
+ dconf_mock_dbus_sync_call_handler = handle_match_request;
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+ match_request_type = "AddMatch";
+
+ /* A match rule should be added when the first subscription is established */
+ dconf_engine_watch_sync (engine, "/a/b/c");
+ g_assert (got_match_request[G_BUS_TYPE_SESSION]);
+ g_assert (got_match_request[G_BUS_TYPE_SYSTEM]);
+ got_match_request[G_BUS_TYPE_SESSION] = FALSE;
+ got_match_request[G_BUS_TYPE_SYSTEM] = FALSE;
+
+ /* The match rule is now already in place, so more are not needed */
+ dconf_engine_watch_sync (engine, "/a/b/c");
+ g_assert_false (got_match_request[G_BUS_TYPE_SESSION]);
+ g_assert_false (got_match_request[G_BUS_TYPE_SYSTEM]);
+
+ dconf_engine_watch_sync (engine, "/a/b/c");
+ g_assert_false (got_match_request[G_BUS_TYPE_SESSION]);
+ g_assert_false (got_match_request[G_BUS_TYPE_SYSTEM]);
+
+ match_request_type = "RemoveMatch";
+
+ /* There are 3 subscriptions, so removing 2 should not remove
+ * the match rule */
+ dconf_engine_unwatch_sync (engine, "/a/b/c");
+ g_assert_false (got_match_request[G_BUS_TYPE_SESSION]);
+ g_assert_false (got_match_request[G_BUS_TYPE_SYSTEM]);
+
+ dconf_engine_unwatch_sync (engine, "/a/b/c");
+ g_assert_false (got_match_request[G_BUS_TYPE_SESSION]);
+ g_assert_false (got_match_request[G_BUS_TYPE_SYSTEM]);
+
+ /* The match rule should be removed when the last subscription is
+ * removed */
+ dconf_engine_unwatch_sync (engine, "/a/b/c");
+ g_assert (got_match_request[G_BUS_TYPE_SESSION]);
+ g_assert (got_match_request[G_BUS_TYPE_SYSTEM]);
+ got_match_request[G_BUS_TYPE_SESSION] = FALSE;
+ got_match_request[G_BUS_TYPE_SYSTEM] = FALSE;
+
+ dconf_engine_unref (engine);
+
+ dconf_mock_dbus_sync_call_handler = NULL;
+ match_request_type = NULL;
+}
+
+static void
+test_change_fast (void)
+{
+ DConfChangeset *empty, *good_write, *bad_write, *very_good_write, *slightly_bad_write;
+ GvdbTable *table, *locks;
+ DConfEngine *engine;
+ gboolean success;
+ GError *error = NULL;
+ GVariant *value;
+
+ change_log = g_string_new (NULL);
+
+ table = dconf_mock_gvdb_table_new ();
+ locks = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_table_insert (locks, "/locked", g_variant_new_boolean (TRUE), NULL);
+ dconf_mock_gvdb_table_insert (table, ".locks", NULL, locks);
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", table);
+
+ empty = dconf_changeset_new ();
+ good_write = dconf_changeset_new_write ("/value", g_variant_new_string ("value"));
+ bad_write = dconf_changeset_new_write ("/locked", g_variant_new_string ("value"));
+ very_good_write = dconf_changeset_new_write ("/value", g_variant_new_string ("value"));
+ dconf_changeset_set (very_good_write, "/to-reset", NULL);
+ slightly_bad_write = dconf_changeset_new_write ("/locked", g_variant_new_string ("value"));
+ dconf_changeset_set (slightly_bad_write, "/to-reset", NULL);
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+ success = dconf_engine_change_fast (engine, empty, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ success = dconf_engine_change_fast (engine, empty, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ success = dconf_engine_change_fast (engine, bad_write, NULL, &error);
+ g_assert_error (error, DCONF_ERROR, DCONF_ERROR_NOT_WRITABLE);
+ g_clear_error (&error);
+ g_assert (!success);
+
+ success = dconf_engine_change_fast (engine, slightly_bad_write, NULL, &error);
+ g_assert_error (error, DCONF_ERROR, DCONF_ERROR_NOT_WRITABLE);
+ g_clear_error (&error);
+ g_assert (!success);
+
+ /* Up to now, no D-Bus traffic should have been sent at all because we
+ * only had trivial and non-writable attempts.
+ *
+ * Now try some working cases
+ */
+ dconf_mock_dbus_assert_no_async ();
+ g_assert_cmpstr (change_log->str, ==, "");
+
+ success = dconf_engine_change_fast (engine, good_write, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ /* That should have emitted a synthetic change event */
+ g_assert_cmpstr (change_log->str, ==, "/value:1::nil;");
+ g_string_set_size (change_log, 0);
+
+ /* Verify that the value is set */
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "/value");
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "value");
+ g_variant_unref (value);
+
+ /* Fail the attempted write. This should cause a warning and a change. */
+ error = g_error_new_literal (G_FILE_ERROR, G_FILE_ERROR_NOENT, "something failed");
+ dconf_mock_dbus_async_reply (NULL, error);
+ g_clear_error (&error);
+ g_assert_cmpstr (change_log->str, ==, "/value:1::nil;");
+ g_string_set_size (change_log, 0);
+
+ assert_pop_message ("dconf", G_LOG_LEVEL_WARNING, "failed to commit changes to dconf: something failed");
+
+ /* Verify that the value became unset due to the failure */
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "value");
+ g_assert (value == NULL);
+
+ /* Now try a successful write */
+ dconf_mock_dbus_assert_no_async ();
+ g_assert_cmpstr (change_log->str, ==, "");
+
+ success = dconf_engine_change_fast (engine, good_write, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ /* That should have emitted a synthetic change event */
+ g_assert_cmpstr (change_log->str, ==, "/value:1::nil;");
+ g_string_set_size (change_log, 0);
+
+ /* Verify that the value is set */
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "/value");
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "value");
+ g_variant_unref (value);
+
+ /* ACK the write. */
+ error = g_error_new_literal (G_FILE_ERROR, G_FILE_ERROR_NOENT, "something failed");
+ dconf_mock_dbus_async_reply (g_variant_new ("(s)", "tag"), NULL);
+ g_clear_error (&error);
+ /* No change this time, since we already did it. */
+ g_assert_cmpstr (change_log->str, ==, "");
+
+ /* Verify that the value became unset due to the in-flight queue
+ * clearing... */
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "value");
+ g_assert (value == NULL);
+
+ /* Do that all again for a changeset with more than one item */
+ dconf_mock_dbus_assert_no_async ();
+ g_assert_cmpstr (change_log->str, ==, "");
+ success = dconf_engine_change_fast (engine, very_good_write, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpstr (change_log->str, ==, "/:2:to-reset,value:nil;");
+ g_string_set_size (change_log, 0);
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "/value");
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "value");
+ g_variant_unref (value);
+ error = g_error_new_literal (G_FILE_ERROR, G_FILE_ERROR_NOENT, "something failed");
+ dconf_mock_dbus_async_reply (NULL, error);
+ g_clear_error (&error);
+ g_assert_cmpstr (change_log->str, ==, "/:2:to-reset,value:nil;");
+ g_string_set_size (change_log, 0);
+ assert_pop_message ("dconf", G_LOG_LEVEL_WARNING, "failed to commit changes to dconf: something failed");
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "value");
+ g_assert (value == NULL);
+ dconf_mock_dbus_assert_no_async ();
+ g_assert_cmpstr (change_log->str, ==, "");
+ success = dconf_engine_change_fast (engine, very_good_write, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpstr (change_log->str, ==, "/:2:to-reset,value:nil;");
+ g_string_set_size (change_log, 0);
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "/value");
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "value");
+ g_variant_unref (value);
+ error = g_error_new_literal (G_FILE_ERROR, G_FILE_ERROR_NOENT, "something failed");
+ dconf_mock_dbus_async_reply (g_variant_new ("(s)", "tag"), NULL);
+ g_clear_error (&error);
+ g_assert_cmpstr (change_log->str, ==, "");
+ value = dconf_engine_read (engine, DCONF_READ_FLAGS_NONE, NULL, "value");
+ g_assert (value == NULL);
+
+ dconf_engine_unref (engine);
+
+ dconf_changeset_unref (empty);
+ dconf_changeset_unref (good_write);
+ dconf_changeset_unref (very_good_write);
+ dconf_changeset_unref (bad_write);
+ dconf_changeset_unref (slightly_bad_write);
+ g_string_free (change_log, TRUE);
+ change_log = NULL;
+}
+
+static GError *change_sync_error;
+static GVariant *change_sync_result;
+
+static GVariant *
+handle_write_request (GBusType bus_type,
+ const gchar *bus_name,
+ const gchar *object_path,
+ const gchar *interface_name,
+ const gchar *method_name,
+ GVariant *parameters,
+ const GVariantType *expected_type,
+ GError **error)
+{
+ g_assert_cmpstr (bus_name, ==, "ca.desrt.dconf");
+ g_assert_cmpstr (interface_name, ==, "ca.desrt.dconf.Writer");
+
+ /* Assume that the engine can format the method call properly, but
+ * test that it can properly handle weird replies.
+ */
+
+ *error = change_sync_error;
+ return change_sync_result;
+}
+
+
+static void
+test_change_sync (void)
+{
+ DConfChangeset *empty, *good_write, *bad_write, *very_good_write, *slightly_bad_write;
+ GvdbTable *table, *locks;
+ DConfEngine *engine;
+ gboolean success;
+ GError *error = NULL;
+ gchar *tag;
+
+ table = dconf_mock_gvdb_table_new ();
+ locks = dconf_mock_gvdb_table_new ();
+ dconf_mock_gvdb_table_insert (locks, "/locked", g_variant_new_boolean (TRUE), NULL);
+ dconf_mock_gvdb_table_insert (table, ".locks", NULL, locks);
+ dconf_mock_gvdb_install (SYSCONFDIR "/dconf/db/site", table);
+
+ empty = dconf_changeset_new ();
+ good_write = dconf_changeset_new_write ("/value", g_variant_new_string ("value"));
+ bad_write = dconf_changeset_new_write ("/locked", g_variant_new_string ("value"));
+ very_good_write = dconf_changeset_new_write ("/value", g_variant_new_string ("value"));
+ dconf_changeset_set (very_good_write, "/to-reset", NULL);
+ slightly_bad_write = dconf_changeset_new_write ("/locked", g_variant_new_string ("value"));
+ dconf_changeset_set (slightly_bad_write, "/to-reset", NULL);
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+ success = dconf_engine_change_sync (engine, empty, &tag, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_free (tag);
+
+ success = dconf_engine_change_sync (engine, empty, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ success = dconf_engine_change_sync (engine, bad_write, &tag, &error);
+ g_assert_error (error, DCONF_ERROR, DCONF_ERROR_NOT_WRITABLE);
+ g_clear_error (&error);
+ g_assert (!success);
+
+ success = dconf_engine_change_sync (engine, slightly_bad_write, NULL, &error);
+ g_assert_error (error, DCONF_ERROR, DCONF_ERROR_NOT_WRITABLE);
+ g_clear_error (&error);
+ g_assert (!success);
+
+ /* Up to now, no D-Bus traffic should have been sent at all because we
+ * only had trivial and non-writable attempts.
+ *
+ * Now try some working cases
+ */
+ dconf_mock_dbus_sync_call_handler = handle_write_request;
+ change_sync_result = g_variant_new ("(s)", "mytag");
+
+ success = dconf_engine_change_sync (engine, good_write, &tag, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ g_assert_cmpstr (tag, ==, "mytag");
+ g_free (tag);
+ change_sync_result = NULL;
+
+ change_sync_error = g_error_new_literal (G_FILE_ERROR, G_FILE_ERROR_NOENT, "something failed");
+ success = dconf_engine_change_sync (engine, very_good_write, &tag, &error);
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_NOENT);
+ g_assert (!success);
+ g_clear_error (&error);
+ change_sync_error = NULL;
+
+ dconf_changeset_unref (empty);
+ dconf_changeset_unref (good_write);
+ dconf_changeset_unref (very_good_write);
+ dconf_changeset_unref (bad_write);
+ dconf_changeset_unref (slightly_bad_write);
+ dconf_engine_unref (engine);
+}
+
+static void
+send_signal (GBusType type,
+ const gchar *name,
+ const gchar *path,
+ const gchar *signame,
+ const gchar *args)
+{
+ GVariant *value;
+
+ value = g_variant_ref_sink (g_variant_new_parsed (args));
+ dconf_engine_handle_dbus_signal (type, name, path, signame, value);
+ g_variant_unref (value);
+}
+
+static void
+test_signals (void)
+{
+ DConfEngine *engine;
+
+ change_log = g_string_new (NULL);
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+ /* Throw some non-sense at it to make sure it gets rejected */
+
+ /* Invalid signal name */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "UnNotify", "('/', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/site", "UnNotify", "('/', [''], 'tag')");
+ g_assert_cmpstr (change_log->str, ==, "");
+ /* Bad path */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/use", "Notify", "('/', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/use", "WritabilityNotify", "('/',)");
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/sit", "Notify", "('/', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/sit", "WritabilityNotify", "('/',)");
+ g_assert_cmpstr (change_log->str, ==, "");
+ /* Wrong signature for signal */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/',)");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('/', [''], '')");
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/site", "Notify", "('/',)");
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/site", "WritabilityNotify", "('/', [''], '')");
+ g_assert_cmpstr (change_log->str, ==, "");
+ /* Signal delivered on wrong bus type */
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/site", "Notify", "('/', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('/',)");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/site", "WritabilityNotify", "('/',)");
+ g_assert_cmpstr (change_log->str, ==, "");
+ /* Empty changeset */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/a', @as [], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/a/', @as [], 'tag')");
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/site", "Notify", "('/a', @as [], 'tag')");
+ send_signal (G_BUS_TYPE_SYSTEM, ":1.123", "/ca/desrt/dconf/Writer/site", "Notify", "('/a/', @as [], 'tag')");
+ /* Try to notify on some invalid paths to make sure they get properly
+ * rejected by the engine and not passed onto the user...
+ */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('a', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('a/', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/b//a/', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/b//a', [''], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('',)");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('a',)");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('a/',)");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('/b//a/',)");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('/b//a',)");
+ g_assert_cmpstr (change_log->str, ==, "");
+ /* Invalid gluing of segments: '/a' + 'b' != '/ab' */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/a', ['b'], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/a', ['b', 'c'], 'tag')");
+ g_assert_cmpstr (change_log->str, ==, "");
+ /* Also: '/a' + '/b' != '/a/b' */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/a', ['/b'], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/a', ['', '/b'], 'tag')");
+ g_assert_cmpstr (change_log->str, ==, "");
+ /* Invalid (non-relative) changes */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/', ['/'], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/', ['/a'], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/', ['a', '/a'], 'tag')");
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify", "('/', ['a', 'a//b'], 'tag')");
+ g_assert_cmpstr (change_log->str, ==, "");
+
+ /* Now try some real cases */
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify",
+ "('/', [''], 'tag')");
+ g_assert_cmpstr (change_log->str, ==, "/:1::tag;");
+ g_string_set_size (change_log, 0);
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify",
+ "('/one/key', [''], 'tag')");
+ g_assert_cmpstr (change_log->str, ==, "/one/key:1::tag;");
+ g_string_set_size (change_log, 0);
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify",
+ "('/two/', ['keys', 'here'], 'tag')");
+ g_assert_cmpstr (change_log->str, ==, "/two/:2:keys,here:tag;");
+ g_string_set_size (change_log, 0);
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "Notify",
+ "('/some/path/', ['a', 'b/', 'c/d'], 'tag')");
+ g_assert_cmpstr (change_log->str, ==, "/some/path/:3:a,b/,c/d:tag;");
+ g_string_set_size (change_log, 0);
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('/other/key',)");
+ g_assert_cmpstr (change_log->str, ==, "w:/other/key:1::;");
+ g_string_set_size (change_log, 0);
+ send_signal (G_BUS_TYPE_SESSION, ":1.123", "/ca/desrt/dconf/Writer/user", "WritabilityNotify", "('/other/dir/',)");
+ g_assert_cmpstr (change_log->str, ==, "w:/other/dir/:1::;");
+ g_string_set_size (change_log, 0);
+
+ dconf_engine_unref (engine);
+}
+
+static gboolean it_is_good_to_be_done;
+
+static gpointer
+waiter_thread (gpointer user_data)
+{
+ DConfEngine *engine = user_data;
+
+ dconf_engine_sync (engine);
+
+ g_assert (g_atomic_int_get (&it_is_good_to_be_done));
+
+ return NULL;
+}
+
+static void
+test_sync (void)
+{
+ GThread *waiter_threads[5];
+ DConfChangeset *change;
+ DConfEngine *engine;
+ GError *error = NULL;
+ gboolean success;
+ gint i;
+
+ engine = dconf_engine_new (SRCDIR "/profile/dos", NULL, NULL);
+
+ /* Make sure a waiter thread returns straight away if nothing is
+ * outstanding.
+ */
+ g_atomic_int_set (&it_is_good_to_be_done, TRUE);
+ g_thread_join (g_thread_new ("waiter", waiter_thread, engine));
+ g_atomic_int_set (&it_is_good_to_be_done, FALSE);
+
+ /* The write will try to check the system-db for a lock. That will
+ * fail because it doesn't exist...
+ */
+ change = dconf_changeset_new_write ("/value", g_variant_new_boolean (TRUE));
+ success = dconf_engine_change_fast (engine, change, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+
+ assert_pop_message ("dconf", G_LOG_LEVEL_WARNING, "unable to open file");
+
+ /* Spin up some waiters */
+ for (i = 0; i < G_N_ELEMENTS (waiter_threads); i++)
+ waiter_threads[i] = g_thread_new ("test waiter", waiter_thread, engine);
+ g_usleep(100 * G_TIME_SPAN_MILLISECOND);
+ /* Release them by completing the pending async call */
+ g_atomic_int_set (&it_is_good_to_be_done, TRUE);
+ dconf_mock_dbus_async_reply (g_variant_new ("(s)", "tag"), NULL);
+ /* Make sure they all quit by joining them */
+ for (i = 0; i < G_N_ELEMENTS (waiter_threads); i++)
+ g_thread_join (waiter_threads[i]);
+ g_atomic_int_set (&it_is_good_to_be_done, FALSE);
+
+ /* Do the same again, but with a failure as a result */
+ success = dconf_engine_change_fast (engine, change, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ for (i = 0; i < G_N_ELEMENTS (waiter_threads); i++)
+ waiter_threads[i] = g_thread_new ("test waiter", waiter_thread, engine);
+ g_usleep(100 * G_TIME_SPAN_MILLISECOND);
+ error = g_error_new_literal (G_FILE_ERROR, G_FILE_ERROR_NOENT, "some error");
+ g_atomic_int_set (&it_is_good_to_be_done, TRUE);
+ dconf_mock_dbus_async_reply (NULL, error);
+ g_clear_error (&error);
+
+ assert_pop_message ("dconf", G_LOG_LEVEL_WARNING, "failed to commit changes to dconf: some error");
+
+ /* Make sure they all quit by joining them */
+ for (i = 0; i < G_N_ELEMENTS (waiter_threads); i++)
+ g_thread_join (waiter_threads[i]);
+ g_atomic_int_set (&it_is_good_to_be_done, FALSE);
+
+ /* Now put two changes in the queue and make sure we have to reply to
+ * both of them before the waiters finish.
+ */
+ success = dconf_engine_change_fast (engine, change, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ success = dconf_engine_change_fast (engine, change, NULL, &error);
+ g_assert_no_error (error);
+ g_assert (success);
+ for (i = 0; i < G_N_ELEMENTS (waiter_threads); i++)
+ waiter_threads[i] = g_thread_new ("test waiter", waiter_thread, engine);
+ g_usleep(100 * G_TIME_SPAN_MILLISECOND);
+ dconf_mock_dbus_async_reply (g_variant_new ("(s)", "tag1"), NULL);
+ /* Still should not have quit yet... wait a bit to let the waiters try
+ * to shoot themselves in their collective feet...
+ */
+ g_usleep(100 * G_TIME_SPAN_MILLISECOND);
+ /* Will be OK after the second reply */
+ g_atomic_int_set (&it_is_good_to_be_done, TRUE);
+ dconf_mock_dbus_async_reply (g_variant_new ("(s)", "tag2"), NULL);
+ /* Make sure they all quit by joining them */
+ for (i = 0; i < G_N_ELEMENTS (waiter_threads); i++)
+ g_thread_join (waiter_threads[i]);
+ g_atomic_int_set (&it_is_good_to_be_done, FALSE);
+
+ dconf_changeset_unref (change);
+ dconf_engine_unref (engine);
+ dconf_mock_shm_reset ();
+}
+
+/* Log handling. */
+typedef struct
+{
+ GLogLevelFlags log_level;
+ GLogField *fields;
+ gsize n_fields;
+} LogMessage;
+
+static void
+log_message_clear (LogMessage *message)
+{
+ gsize i;
+
+ for (i = 0; i < message->n_fields; i++)
+ {
+ g_free ((gpointer) message->fields[i].key);
+ g_free ((gpointer) message->fields[i].value);
+ }
+}
+
+static GArray *logged_messages = NULL;
+
+static GLogWriterOutput
+log_writer_cb (GLogLevelFlags log_level,
+ const GLogField *fields,
+ gsize n_fields,
+ gpointer user_data)
+{
+ LogMessage *message;
+ gsize i;
+
+ /* If we’re running as a subprocess, the parent process is going to be
+ * checking our stderr, so just behave normally. */
+ if (g_test_subprocess ())
+ return g_log_writer_default (log_level, fields, n_fields, user_data);
+
+ /* We only care about dconf messages and non-debug messages. */
+ if (log_level == G_LOG_LEVEL_DEBUG)
+ return G_LOG_WRITER_HANDLED;
+
+ for (i = 0; i < n_fields; i++)
+ {
+ if (g_strcmp0 (fields[i].key, "GLIB_DOMAIN") == 0 &&
+ g_strcmp0 (fields[i].value, "dconf") != 0)
+ return G_LOG_WRITER_HANDLED;
+ }
+
+ /* Append the message to the queue. */
+ g_array_set_size (logged_messages, logged_messages->len + 1);
+ message = &g_array_index (logged_messages, LogMessage, logged_messages->len - 1);
+
+ message->log_level = log_level;
+ message->fields = g_new0 (GLogField, n_fields);
+ message->n_fields = n_fields;
+
+ for (i = 0; i < n_fields; i++)
+ {
+ gsize length = (fields[i].length < 0) ? strlen (fields[i].value) + 1 : fields[i].length;
+ message->fields[i].key = g_strdup (fields[i].key);
+ message->fields[i].value = g_malloc0 (length);
+ memcpy ((gpointer) message->fields[i].value, fields[i].value, length);
+ message->fields[i].length = fields[i].length;
+ }
+
+ return G_LOG_WRITER_HANDLED;
+}
+
+/* Assert there are no logged messages in the queue. */
+static void
+assert_no_messages (void)
+{
+ g_assert_cmpuint (logged_messages->len, ==, 0);
+}
+
+/* Assert there is at least one logged message in the queue, and the oldest
+ * logged message matches the given expectations. If so, pop it from the queue;
+ * if not, abort. */
+static void
+assert_pop_message (const gchar *expected_domain,
+ GLogLevelFlags expected_log_level,
+ const gchar *expected_message_fragment)
+{
+ const LogMessage *logged_message;
+ gsize i;
+ const gchar *message = NULL, *domain = NULL;
+
+ g_assert_cmpuint (logged_messages->len, >, 0);
+ logged_message = &g_array_index (logged_messages, LogMessage, 0);
+
+ for (i = 0; i < logged_message->n_fields; i++)
+ {
+ if (g_strcmp0 (logged_message->fields[i].key, "MESSAGE") == 0)
+ message = logged_message->fields[i].value;
+ else if (g_strcmp0 (logged_message->fields[i].key, "GLIB_DOMAIN") == 0)
+ domain = logged_message->fields[i].value;
+ }
+
+ g_assert_cmpstr (domain, ==, expected_domain);
+ g_assert_cmpuint (logged_message->log_level, ==, expected_log_level);
+ g_assert_cmpstr (strstr (message, expected_message_fragment), !=, NULL);
+
+ g_array_remove_index (logged_messages, 0);
+}
+
+/* If there is at least one logged message in the queue, act like
+ * assert_pop_message(). Otherwise, if the queue is empty, return. */
+static void
+assert_maybe_pop_message (const gchar *expected_domain,
+ GLogLevelFlags expected_log_level,
+ const gchar *expected_message_fragment)
+{
+ if (logged_messages->len == 0)
+ return;
+
+ assert_pop_message (expected_domain, expected_log_level, expected_message_fragment);
+}
+
+int
+main (int argc, char **argv)
+{
+ const ProfileParserOpenData profile_parser0 =
+ { SRCDIR "/profile/this-file-does-not-exist", "*WARNING*: unable to open named profile*" };
+ const ProfileParserOpenData profile_parser1 =
+ { SRCDIR "/profile/broken-profile", "*WARNING*: unknown dconf database*unknown dconf database*" };
+ const ProfileParserOpenData profile_parser2 =
+ { SRCDIR "/profile/gdm", "*WARNING*: unknown dconf database*unknown dconf database*" };
+ int retval;
+
+ g_setenv ("XDG_RUNTIME_DIR", "/RUNTIME/", TRUE);
+ g_setenv ("XDG_CONFIG_HOME", "/HOME/.config", TRUE);
+ g_unsetenv ("DCONF_PROFILE");
+
+ logged_messages = g_array_new (FALSE, FALSE, sizeof (LogMessage));
+ g_array_set_clear_func (logged_messages, (GDestroyNotify) log_message_clear);
+ g_log_set_writer_func (log_writer_cb, NULL, NULL);
+
+ main_thread = g_thread_self ();
+
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add_data_func ("/engine/profile-parser/errors/0", &profile_parser0, test_profile_parser_errors);
+ g_test_add_data_func ("/engine/profile-parser/errors/1", &profile_parser1, test_profile_parser_errors);
+ g_test_add_data_func ("/engine/profile-parser/errors/2", &profile_parser2, test_profile_parser_errors);
+ g_test_add_func ("/engine/profile-parser", test_profile_parser);
+ g_test_add_func ("/engine/signal-threadsafety", test_signal_threadsafety);
+ g_test_add_func ("/engine/sources/user", test_user_source);
+ g_test_add_func ("/engine/sources/system", test_system_source);
+ g_test_add_func ("/engine/sources/file", test_file_source);
+ g_test_add_func ("/engine/sources/service", test_service_source);
+ g_test_add_func ("/engine/read", test_read);
+ g_test_add_func ("/engine/watch/fast", test_watch_fast);
+ g_test_add_func ("/engine/watch/fast/simultaneous", test_watch_fast_simultaneous_subscriptions);
+ g_test_add_func ("/engine/watch/fast/successive", test_watch_fast_successive_subscriptions);
+ g_test_add_func ("/engine/watch/fast/short_lived", test_watch_fast_short_lived_subscriptions);
+ g_test_add_func ("/engine/watch/sync", test_watch_sync);
+ g_test_add_func ("/engine/change/fast", test_change_fast);
+ g_test_add_func ("/engine/change/sync", test_change_sync);
+ g_test_add_func ("/engine/signals", test_signals);
+ g_test_add_func ("/engine/sync", test_sync);
+
+ retval = g_test_run ();
+
+ assert_no_messages ();
+ g_array_unref (logged_messages);
+
+ return retval;
+}
diff --git a/tests/gvdb.c b/tests/gvdb.c
new file mode 100644
index 0000000..d054067
--- /dev/null
+++ b/tests/gvdb.c
@@ -0,0 +1,438 @@
+#include <glib.h>
+#include "../gvdb/gvdb-reader.h"
+
+static void
+test_reader_open_error (void)
+{
+ GError *error = NULL;
+ GvdbTable *table;
+
+ table = gvdb_table_new (SRCDIR "/gvdbs/does_not_exist", TRUE, &error);
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_NOENT);
+ g_assert (table == NULL);
+ g_clear_error (&error);
+
+ table = gvdb_table_new (SRCDIR "/gvdbs/file_empty", TRUE, &error);
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL);
+ g_assert (table == NULL);
+ g_clear_error (&error);
+
+ table = gvdb_table_new (SRCDIR "/gvdbs/invalid_header", TRUE, &error);
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL);
+ g_assert (table == NULL);
+ g_clear_error (&error);
+
+ table = gvdb_table_new (SRCDIR "/gvdbs/file_too_small", TRUE, &error);
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL);
+ g_assert (table == NULL);
+ g_clear_error (&error);
+}
+
+static void
+test_reader_empty (void)
+{
+ const gchar * strings[] = { "", "value", "/value", ".", NULL};
+ GError *error = NULL;
+ GvdbTable *table;
+ gchar **names;
+ gint n_names;
+ gint i;
+
+ table = gvdb_table_new (SRCDIR "/gvdbs/empty_gvdb", TRUE, &error);
+ g_assert_no_error (error);
+ g_assert (table != NULL);
+
+ g_assert (gvdb_table_is_valid (table));
+
+ names = gvdb_table_get_names (table, &n_names);
+ g_assert_cmpint (n_names, ==, 0);
+ g_assert_cmpint (g_strv_length (names), ==, 0);
+ g_strfreev (names);
+
+ names = gvdb_table_get_names (table, NULL);
+ g_assert_cmpint (g_strv_length (names), ==, 0);
+ g_strfreev (names);
+
+ for (i = 0; strings[i]; i++)
+ {
+ const gchar *key = strings[i];
+ GvdbTable *sub;
+ GVariant *val;
+ gboolean has;
+ gchar **list;
+
+ sub = gvdb_table_get_table (table, key);
+ g_assert (sub == NULL);
+
+ has = gvdb_table_has_value (table, key);
+ g_assert (!has);
+
+ val = gvdb_table_get_value (table, key);
+ g_assert (val == NULL);
+
+ val = gvdb_table_get_raw_value (table, key);
+ g_assert (val == NULL);
+
+ list = gvdb_table_list (table, key);
+ g_assert (list == NULL);
+ }
+
+ gvdb_table_free (table);
+}
+
+static void
+verify_table (GvdbTable *table)
+{
+ GVariant *value;
+ gchar **list;
+ gint n_names;
+ gboolean has;
+
+ /* We could not normally expect these to be in a particular order but
+ * we are using a specific test file that we know to be layed out this
+ * way...
+ *
+ * It's pure luck that they happened to be layed out in this nice way.
+ */
+ list = gvdb_table_get_names (table, &n_names);
+ g_assert_cmpint (n_names, ==, g_strv_length (list));
+ g_assert_cmpint (n_names, ==, 5);
+ g_assert_cmpstr (list[0], ==, "/");
+ g_assert_cmpstr (list[1], ==, "/values/");
+ g_assert_cmpstr (list[2], ==, "/values/boolean");
+ g_assert_cmpstr (list[3], ==, "/values/string");
+ g_assert_cmpstr (list[4], ==, "/values/int32");
+ g_strfreev (list);
+
+ list = gvdb_table_list (table, "/");
+ g_assert (list != NULL);
+ g_assert_cmpint (g_strv_length (list), ==, 1);
+ g_assert_cmpstr (list[0], ==, "values/");
+ g_strfreev (list);
+
+ list = gvdb_table_list (table, "/values/");
+ g_assert (list != NULL);
+ g_assert_cmpint (g_strv_length (list), ==, 3);
+ g_assert_cmpstr (list[0], ==, "boolean");
+ g_assert_cmpstr (list[1], ==, "int32");
+ g_assert_cmpstr (list[2], ==, "string");
+ g_strfreev (list);
+
+ /* A directory is not a value */
+ has = gvdb_table_has_value (table, "/");
+ g_assert (!has);
+ has = gvdb_table_has_value (table, "/values/");
+ g_assert (!has);
+
+ has = gvdb_table_has_value (table, "/int32");
+ g_assert (!has);
+ has = gvdb_table_has_value (table, "values/int32");
+ g_assert (!has);
+ has = gvdb_table_has_value (table, "/values/int32");
+ g_assert (has);
+
+ value = gvdb_table_get_value (table, "/");
+ g_assert (value == NULL);
+ value = gvdb_table_get_value (table, "/values/");
+ g_assert (value == NULL);
+ value = gvdb_table_get_value (table, "/int32");
+ g_assert (value == NULL);
+ value = gvdb_table_get_value (table, "values/int32");
+ g_assert (value == NULL);
+
+ value = gvdb_table_get_value (table, "/values/boolean");
+ g_assert (value != NULL && g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN));
+ g_assert (g_variant_get_boolean (value));
+ g_variant_unref (value);
+
+ value = gvdb_table_get_raw_value (table, "/values/boolean");
+ g_assert (value != NULL && g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN));
+ g_assert (g_variant_get_boolean (value));
+ g_variant_unref (value);
+
+ value = gvdb_table_get_value (table, "/values/int32");
+ g_assert (value != NULL && g_variant_is_of_type (value, G_VARIANT_TYPE_INT32));
+ g_assert_cmpint (g_variant_get_int32 (value), ==, 0x44332211);
+ g_variant_unref (value);
+
+ value = gvdb_table_get_value (table, "/values/string");
+ g_assert (value != NULL && g_variant_is_of_type (value, G_VARIANT_TYPE_STRING));
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "a string");
+ g_variant_unref (value);
+
+ value = gvdb_table_get_raw_value (table, "/values/string");
+ g_assert (value != NULL && g_variant_is_of_type (value, G_VARIANT_TYPE_STRING));
+ g_assert_cmpstr (g_variant_get_string (value, NULL), ==, "a string");
+ g_variant_unref (value);
+}
+
+static void
+test_reader_values (void)
+{
+ GError *error = NULL;
+ GvdbTable *table;
+
+ table = gvdb_table_new (SRCDIR "/gvdbs/example_gvdb", TRUE, &error);
+ g_assert_no_error (error);
+ verify_table (table);
+
+#if G_BYTE_ORDER == G_BIG_ENDIAN
+ {
+ GVariant *value;
+
+ value = gvdb_table_get_raw_value (table, "/values/int32");
+ g_assert (value != NULL && g_variant_is_of_type (value, G_VARIANT_TYPE_INT32));
+ g_assert_cmpint (g_variant_get_int32 (value), ==, 0x11223344);
+ g_variant_unref (value);
+ }
+#endif
+
+ gvdb_table_free (table);
+}
+
+static void
+test_reader_values_bigendian (void)
+{
+ GError *error = NULL;
+ GvdbTable *table;
+
+ table = gvdb_table_new (SRCDIR "/gvdbs/example_gvdb.big-endian", TRUE, &error);
+ g_assert_no_error (error);
+ verify_table (table);
+
+#if G_BYTE_ORDER == G_LITTLE_ENDIAN
+ {
+ GVariant *value;
+
+ value = gvdb_table_get_raw_value (table, "/values/int32");
+ g_assert (value != NULL && g_variant_is_of_type (value, G_VARIANT_TYPE_INT32));
+ g_assert_cmpint (g_variant_get_int32 (value), ==, 0x11223344);
+ g_variant_unref (value);
+ }
+#endif
+
+ gvdb_table_free (table);
+}
+
+static void
+test_nested (void)
+{
+ GError *error = NULL;
+ GvdbTable *table;
+ GvdbTable *locks;
+ gchar **names;
+ gint n_names;
+ gboolean has;
+
+ table = gvdb_table_new (SRCDIR "/gvdbs/nested_gvdb", TRUE, &error);
+ g_assert_no_error (error);
+
+ /* Note the more-random ordering here compared with above. */
+ names = gvdb_table_get_names (table, &n_names);
+ g_assert_cmpint (n_names, ==, g_strv_length (names));
+ g_assert_cmpint (n_names, ==, 6);
+ g_assert_cmpstr (names[0], ==, "/values/boolean");
+ g_assert_cmpstr (names[1], ==, "/");
+ g_assert_cmpstr (names[2], ==, "/values/int32");
+ g_assert_cmpstr (names[3], ==, ".locks");
+ g_assert_cmpstr (names[4], ==, "/values/");
+ g_assert_cmpstr (names[5], ==, "/values/string");
+ g_strfreev (names);
+
+ locks = gvdb_table_get_table (table, "/");
+ g_assert (locks == NULL);
+ locks = gvdb_table_get_table (table, "/values/");
+ g_assert (locks == NULL);
+ locks = gvdb_table_get_table (table, "/values/int32");
+ g_assert (locks == NULL);
+
+ locks = gvdb_table_get_table (table, ".locks");
+ g_assert (locks != NULL);
+
+ has = gvdb_table_has_value (locks, "/first/lck");
+ g_assert (!has);
+
+ has = gvdb_table_has_value (locks, "/first/lock");
+ g_assert (has);
+
+ has = gvdb_table_has_value (locks, "/second");
+ g_assert (has);
+
+ gvdb_table_free (table);
+ gvdb_table_free (locks);
+}
+
+/* This function exercises the API against @table but does not do any
+ * asserts on unexpected values (although it will assert on inconsistent
+ * values returned by the API).
+ */
+static void
+inspect_carefully (GvdbTable *table,
+ gint level)
+{
+ const gchar * key_names[] = {
+ "/", "/values/", "/int32", "values/int32",
+ "/values/int32", "/values/boolean", "/values/string",
+ ".locks", "/first/lock", "/second", NULL
+ };
+ gint found_items;
+ gchar **names;
+ gint n_names;
+ gint i;
+
+ if (level > 100)
+ return;
+
+ found_items = 0;
+ for (i = 0; key_names[i]; i++)
+ {
+ const gchar *key = key_names[i];
+ GvdbTable *subtable;
+ GVariant *value;
+ gchar **list;
+ gboolean has;
+
+ has = gvdb_table_has_value (table, key);
+
+ list = gvdb_table_list (table, key);
+ g_assert (!has || list == NULL);
+ if (list)
+ {
+ gchar *joined = g_strjoinv (",", list);
+ g_strfreev (list);
+ g_free (joined);
+ found_items++;
+ }
+
+ value = gvdb_table_get_value (table, key);
+ g_assert_cmpint (value != NULL, ==, has);
+ if (value)
+ {
+ gchar *printed = g_variant_print (value, FALSE);
+ g_variant_unref (value);
+ g_free (printed);
+ found_items++;
+ }
+
+ value = gvdb_table_get_raw_value (table, key);
+ g_assert_cmpint (value != NULL, ==, has);
+ if (value)
+ {
+ gchar *printed = g_variant_print (value, FALSE);
+ g_variant_unref (value);
+ g_free (printed);
+ }
+
+ subtable = gvdb_table_get_table (table, key);
+ g_assert (!has || subtable == NULL);
+ if (subtable)
+ {
+ inspect_carefully (subtable, level + 1);
+ gvdb_table_free (subtable);
+ found_items++;
+ }
+ }
+
+ names = gvdb_table_get_names (table, &n_names);
+ g_assert_cmpint (n_names, ==, g_strv_length (names));
+ g_assert_cmpint (found_items, <=, n_names);
+ g_free (g_strjoinv (" ", names));
+ g_strfreev (names);
+}
+
+static void
+test_corrupted (gconstpointer user_data)
+{
+ gint percentage = GPOINTER_TO_INT (user_data);
+ GError *error = NULL;
+ GMappedFile *mapped;
+
+ mapped = g_mapped_file_new (SRCDIR "/gvdbs/nested_gvdb", FALSE, &error);
+ g_assert_no_error (error);
+ g_assert (mapped);
+
+ if (percentage)
+ {
+ GvdbTable *table;
+ const gchar *orig;
+ gsize length;
+ gchar *copy;
+ gint i;
+
+ orig = g_mapped_file_get_contents (mapped);
+ length = g_mapped_file_get_length (mapped);
+ copy = g_memdup (orig, length);
+
+ for (i = 0; i < 10000; i++)
+ {
+ GBytes *bytes;
+ gint j;
+
+ /* Make a broken copy, but leave the signature intact so that
+ * we don't get too many boring trivial failures.
+ */
+ for (j = 8; j < length; j++)
+ if (g_test_rand_int_range (0, 100) < percentage)
+ copy[j] = g_test_rand_int_range (0, 256);
+ else
+ copy[j] = orig[j];
+
+ bytes = g_bytes_new_static (copy, length);
+ table = gvdb_table_new_from_bytes (bytes, FALSE, &error);
+ g_bytes_unref (bytes);
+
+ /* If we damaged the header, it may not open */
+ if (table)
+ {
+ inspect_carefully (table, 0);
+ gvdb_table_free (table);
+ }
+ else
+ {
+ g_assert_error (error, G_FILE_ERROR, G_FILE_ERROR_INVAL);
+ g_clear_error (&error);
+ }
+ }
+
+ g_free (copy);
+ }
+ else
+ {
+ GvdbTable *table;
+ GBytes *bytes;
+
+ bytes = g_mapped_file_get_bytes (mapped);
+ table = gvdb_table_new_from_bytes (bytes, FALSE, &error);
+ g_bytes_unref (bytes);
+
+ g_assert_no_error (error);
+ g_assert (table);
+
+ inspect_carefully (table, 0);
+ gvdb_table_free (table);
+ }
+
+ g_mapped_file_unref (mapped);
+}
+
+int
+main (int argc, char **argv)
+{
+ gint i;
+
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add_func ("/gvdb/reader/open-error", test_reader_open_error);
+ g_test_add_func ("/gvdb/reader/empty", test_reader_empty);
+ g_test_add_func ("/gvdb/reader/values", test_reader_values);
+ g_test_add_func ("/gvdb/reader/values/big-endian", test_reader_values_bigendian);
+ g_test_add_func ("/gvdb/reader/nested", test_nested);
+ for (i = 0; i < 20; i++)
+ {
+ gchar test_name[80];
+ g_snprintf (test_name, sizeof test_name, "/gvdb/reader/corrupted/%d%%", i);
+ g_test_add_data_func (test_name, GINT_TO_POINTER (i), test_corrupted);
+ }
+
+ return g_test_run ();
+}
diff --git a/tests/gvdbs/empty_gvdb b/tests/gvdbs/empty_gvdb
new file mode 100644
index 0000000..c700bdb
--- /dev/null
+++ b/tests/gvdbs/empty_gvdb
Binary files differ
diff --git a/tests/gvdbs/example_gvdb b/tests/gvdbs/example_gvdb
new file mode 100644
index 0000000..73b098e
--- /dev/null
+++ b/tests/gvdbs/example_gvdb
Binary files differ
diff --git a/tests/gvdbs/example_gvdb.big-endian b/tests/gvdbs/example_gvdb.big-endian
new file mode 100644
index 0000000..c729546
--- /dev/null
+++ b/tests/gvdbs/example_gvdb.big-endian
Binary files differ
diff --git a/tests/gvdbs/file_empty b/tests/gvdbs/file_empty
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/gvdbs/file_empty
diff --git a/tests/gvdbs/file_too_small b/tests/gvdbs/file_too_small
new file mode 100644
index 0000000..b54857d
--- /dev/null
+++ b/tests/gvdbs/file_too_small
@@ -0,0 +1 @@
+GVariant
diff --git a/tests/gvdbs/invalid_header b/tests/gvdbs/invalid_header
new file mode 100644
index 0000000..25335ec
--- /dev/null
+++ b/tests/gvdbs/invalid_header
@@ -0,0 +1 @@
+GVARIANT________________ \ No newline at end of file
diff --git a/tests/gvdbs/nested_gvdb b/tests/gvdbs/nested_gvdb
new file mode 100644
index 0000000..b593e38
--- /dev/null
+++ b/tests/gvdbs/nested_gvdb
Binary files differ
diff --git a/tests/meson.build b/tests/meson.build
new file mode 100644
index 0000000..8aa5837
--- /dev/null
+++ b/tests/meson.build
@@ -0,0 +1,61 @@
+sources = files(
+ 'dconf-mock-dbus.c',
+ 'dconf-mock-gvdb.c',
+ 'dconf-mock-shm.c',
+)
+
+libdconf_mock = static_library(
+ 'dconf-mock',
+ sources: sources,
+ dependencies: glib_dep,
+)
+
+envs = test_env + [
+ 'G_TEST_SRCDIR=' + meson.current_source_dir(),
+ 'G_TEST_BUILDDIR=' + meson.current_build_dir(),
+]
+
+test_dir = meson.current_source_dir()
+
+dl_dep = cc.find_library('dl', required: false)
+m_dep = cc.find_library('m')
+
+unit_tests = [
+ # [name, sources, c_args, dependencies, link_with]
+ ['paths', 'paths.c', [], libdconf_common_dep, []],
+ ['changeset', 'changeset.c', [], libdconf_common_dep, []],
+ ['shm', ['shm.c', 'tmpdir.c'], [], [dl_dep, libdconf_common_dep, libdconf_shm_test_dep], []],
+ ['gvdb', 'gvdb.c', '-DSRCDIR="@0@"'.format(test_dir), libgvdb_dep, []],
+ ['gdbus-thread', 'dbus.c', '-DDBUS_BACKEND="/gdbus/thread"', libdconf_gdbus_thread_dep, []],
+ ['gdbus-filter', 'dbus.c', '-DDBUS_BACKEND="/gdbus/filter"', 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]],
+]
+
+foreach unit_test: unit_tests
+ exe = executable(
+ unit_test[0],
+ unit_test[1],
+ c_args: unit_test[2],
+ dependencies: unit_test[3],
+ link_with: unit_test[4],
+ include_directories: [top_inc, include_directories('../service')],
+ )
+
+ test(unit_test[0], exe, is_parallel: false, env: envs)
+endforeach
+
+python3 = find_program('python3', required: false)
+dbus_daemon = find_program('dbus-daemon', required: false)
+
+if python3.found() and dbus_daemon.found()
+ test_dconf_py = find_program('test-dconf.py')
+ test(
+ 'dconf',
+ test_dconf_py,
+ args: [dconf.full_path(), dconf_service.full_path()]
+ )
+else
+ message('Skipping dconf test because python3 or dbus-daemon is not available')
+endif
diff --git a/tests/paths.c b/tests/paths.c
new file mode 100644
index 0000000..5774333
--- /dev/null
+++ b/tests/paths.c
@@ -0,0 +1,98 @@
+#include "../common/dconf-paths.h"
+
+static void
+test_paths (void)
+{
+ struct test_case {
+ const gchar *string;
+ guint flags;
+ } cases[] = {
+
+#define invalid 0
+#define path 001
+#define key 002 | path
+#define dir 004 | path
+#define rel 010
+#define relkey 020 | rel
+#define reldir 040 | rel
+
+ { NULL, invalid },
+ { "", reldir },
+ { "/", dir },
+
+ { "/key", key },
+ { "/path/", dir },
+ { "/path/key", key },
+ { "/path/path/", dir },
+ { "/a/b", key },
+ { "/a/b/", dir },
+
+ { "//key", invalid },
+ { "//path/", invalid },
+ { "//path/key", invalid },
+ { "//path/path/", invalid },
+ { "//a/b", invalid },
+ { "//a/b/", invalid },
+
+ { "/key", key },
+ { "/path//", invalid },
+ { "/path/key", key },
+ { "/path/path//", invalid },
+ { "/a/b", key },
+ { "/a/b//", invalid },
+
+ { "/key", key },
+ { "/path/", dir },
+ { "/path//key", invalid },
+ { "/path//path/", invalid },
+ { "/a//b", invalid },
+ { "/a//b/", invalid },
+
+ { "key", relkey },
+ { "path/", reldir },
+ { "path/key", relkey },
+ { "path/path/", reldir },
+ { "a/b", relkey },
+ { "a/b/", reldir },
+
+ { "key", relkey },
+ { "path//", invalid },
+ { "path/key", relkey },
+ { "path/path//", invalid },
+ { "a/b", relkey },
+ { "a/b//", invalid },
+
+ { "key", relkey },
+ { "path/", reldir },
+ { "path//key", invalid },
+ { "path//path/", invalid },
+ { "a//b", invalid },
+ { "a//b/", invalid }
+ };
+ gint i;
+
+ for (i = 0; i < G_N_ELEMENTS (cases); i++)
+ {
+ const gchar *string = cases[i].string;
+ guint flags;
+
+ flags = (dconf_is_path (string, NULL) ? 001 : 000) |
+ (dconf_is_key (string, NULL) ? 002 : 000) |
+ (dconf_is_dir (string, NULL) ? 004 : 000) |
+ (dconf_is_rel_path (string, NULL) ? 010 : 000) |
+ (dconf_is_rel_key (string, NULL) ? 020 : 000) |
+ (dconf_is_rel_dir (string, NULL) ? 040 : 000);
+
+ g_assert_cmphex (flags, ==, cases[i].flags);
+ }
+}
+
+int
+main (int argc, char **argv)
+{
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add_func ("/paths", test_paths);
+
+ return g_test_run ();
+}
diff --git a/tests/profile/broken-profile b/tests/profile/broken-profile
new file mode 100644
index 0000000..6bdcb24
--- /dev/null
+++ b/tests/profile/broken-profile
@@ -0,0 +1,6 @@
+a b c d e
+f g h i j
+bad-db:foo
+user-db:
+user-dd:xyz
+system-dd:wer
diff --git a/tests/profile/colourful b/tests/profile/colourful
new file mode 100644
index 0000000..5a69bb5
--- /dev/null
+++ b/tests/profile/colourful
@@ -0,0 +1,13 @@
+# this is an interesting dconf profile file
+ # it shows some of the possible weird things you can do
+
+ user-db:user # comments can be like this
+
+# we can have comments that are extremely long. we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.
+
+ system-db:other # we can have comments that are extremely long. we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.we can have comments that are extremely long.
+
+system-db:verylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongnameverylongname
+
+# there is no newline after this last line
+system-db:nonewline \ No newline at end of file
diff --git a/tests/profile/dos b/tests/profile/dos
new file mode 100644
index 0000000..cc38458
--- /dev/null
+++ b/tests/profile/dos
@@ -0,0 +1,2 @@
+user-db:user
+system-db:site
diff --git a/tests/profile/empty-profile b/tests/profile/empty-profile
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/profile/empty-profile
diff --git a/tests/profile/gdm b/tests/profile/gdm
new file mode 100644
index 0000000..d5a90e5
--- /dev/null
+++ b/tests/profile/gdm
@@ -0,0 +1,2 @@
+user
+gdm
diff --git a/tests/profile/many-sources b/tests/profile/many-sources
new file mode 100644
index 0000000..15fe5ca
--- /dev/null
+++ b/tests/profile/many-sources
@@ -0,0 +1,10 @@
+user-db:user
+system-db:local
+system-db:room
+system-db:floor
+system-db:building
+system-db:site
+system-db:region
+system-db:division
+system-db:country
+system-db:global
diff --git a/tests/profile/no-newline-longline b/tests/profile/no-newline-longline
new file mode 100644
index 0000000..93193aa
--- /dev/null
+++ b/tests/profile/no-newline-longline
@@ -0,0 +1 @@
+# this long line has no newline at the end. this long line has no newline at the end. this long line has no newline at the end. this long line has no newline at the end. this long line has no newline at the end. this long line has no newline at the end. this long line has no newline at the end. this long line has no newline at the end. \ No newline at end of file
diff --git a/tests/profile/test-profile b/tests/profile/test-profile
new file mode 100644
index 0000000..3a511c2
--- /dev/null
+++ b/tests/profile/test-profile
@@ -0,0 +1 @@
+user-db:test
diff --git a/tests/profile/will-never-exist b/tests/profile/will-never-exist
new file mode 100644
index 0000000..bcff7a6
--- /dev/null
+++ b/tests/profile/will-never-exist
@@ -0,0 +1 @@
+user-db:will-never-exist
diff --git a/tests/shm.c b/tests/shm.c
new file mode 100644
index 0000000..69d683f
--- /dev/null
+++ b/tests/shm.c
@@ -0,0 +1,175 @@
+#define _GNU_SOURCE
+
+#include "../common/dconf-paths.h"
+#include <glib/gstdio.h>
+#include <sys/stat.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <dlfcn.h>
+
+#include "../shm/dconf-shm.h"
+#include "../shm/dconf-shm-mockable.h"
+#include "tmpdir.h"
+
+static void
+test_mkdir_fail (void)
+{
+ guint8 *shm;
+
+ if (g_test_subprocess ())
+ {
+ gchar *evil;
+ gint fd;
+
+ g_log_set_always_fatal (G_LOG_LEVEL_ERROR);
+
+ evil = g_build_filename (g_get_user_runtime_dir (), "dconf", NULL);
+ fd = open (evil, O_WRONLY | O_CREAT, 0600);
+ close (fd);
+
+ shm = dconf_shm_open ("foo");
+ g_assert (shm == NULL);
+
+ g_unlink (evil);
+ g_free (evil);
+
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+ g_test_trap_assert_stderr ("*unable to create directory*");
+}
+
+static void
+test_close_null (void)
+{
+ dconf_shm_close (NULL);
+}
+
+static void
+test_open_and_flag (void)
+{
+ guint8 *shm;
+
+ shm = dconf_shm_open ("foo");
+ g_assert (shm != NULL);
+ g_assert (!dconf_shm_is_flagged (shm));
+ dconf_shm_flag ("foo");
+ g_assert (dconf_shm_is_flagged (shm));
+ dconf_shm_close (shm);
+}
+
+static void
+test_invalid_name (void)
+{
+ if (g_test_subprocess ())
+ {
+ guint8 *shm;
+
+ g_log_set_always_fatal (G_LOG_LEVEL_ERROR);
+
+ shm = dconf_shm_open ("foo/bar");
+ g_assert (shm == NULL);
+ g_assert (dconf_shm_is_flagged (shm));
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+ g_test_trap_assert_stderr ("*unable to create*foo/bar*");
+}
+
+static void
+test_flag_nonexistent (void)
+{
+ dconf_shm_flag ("does-not-exist");
+}
+
+static gboolean should_fail_pwrite;
+/* interpose */
+ssize_t
+dconf_shm_pwrite (int fd, const void *buf, size_t count, off_t offset)
+{
+ if (should_fail_pwrite)
+ {
+ errno = ENOSPC;
+ return -1;
+ }
+
+ return pwrite (fd, buf, count, offset);
+}
+
+static void
+test_out_of_space_open (void)
+{
+ if (g_test_subprocess ())
+ {
+ guint8 *shm;
+
+ g_log_set_always_fatal (G_LOG_LEVEL_ERROR);
+ should_fail_pwrite = TRUE;
+
+ shm = dconf_shm_open ("foo");
+ g_assert (shm == NULL);
+ g_assert (dconf_shm_is_flagged (shm));
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+ g_test_trap_assert_stderr ("*failed to allocate*foo*");
+}
+
+static void
+test_out_of_space_flag (void)
+{
+ if (g_test_subprocess ())
+ {
+ g_log_set_always_fatal (G_LOG_LEVEL_ERROR);
+ should_fail_pwrite = TRUE;
+
+ dconf_shm_flag ("foo");
+ return;
+ }
+
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_passed ();
+}
+
+int
+main (int argc, char **argv)
+{
+ gchar *temp;
+ gint status;
+
+ temp = dconf_test_create_tmpdir ();
+
+ g_setenv ("XDG_RUNTIME_DIR", temp, TRUE);
+ /* This currently works, but it is possible that one day GLib will
+ * read the XDG_RUNTIME_DIR variable (and cache its value) as a
+ * side-effect of the dconf_test_create_tmpdir() call above.
+ *
+ * This assert will quickly uncover the problem in that case...
+ */
+ g_assert_cmpstr (g_get_user_runtime_dir (), ==, temp);
+
+ g_test_init (&argc, &argv, NULL);
+
+ g_test_add_func ("/shm/mkdir-fail", test_mkdir_fail);
+ g_test_add_func ("/shm/close-null", test_close_null);
+ g_test_add_func ("/shm/open-and-flag", test_open_and_flag);
+ g_test_add_func ("/shm/invalid-name", test_invalid_name);
+ g_test_add_func ("/shm/flag-nonexistent", test_flag_nonexistent);
+ g_test_add_func ("/shm/out-of-space-open", test_out_of_space_open);
+ g_test_add_func ("/shm/out-of-space-flag", test_out_of_space_flag);
+
+ status = g_test_run ();
+
+ dconf_test_remove_tmpdir (temp);
+ g_free (temp);
+
+ return status;
+}
diff --git a/tests/test-dconf.py b/tests/test-dconf.py
new file mode 100755
index 0000000..6cd80a8
--- /dev/null
+++ b/tests/test-dconf.py
@@ -0,0 +1,817 @@
+#!/usr/bin/env python3
+#
+# Copyright © 2018 Tomasz Miąsko
+#
+# 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/>.
+
+import mmap
+import os
+import subprocess
+import sys
+import tempfile
+import time
+import unittest
+
+from textwrap import dedent
+
+DAEMON_CONFIG = '''
+<!DOCTYPE busconfig PUBLIC
+ "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<busconfig>
+ <type>session</type>
+ <keep_umask/>
+ <listen>unix:tmpdir=/tmp</listen>
+ <servicedir>{servicedir}</servicedir>
+ <auth>EXTERNAL</auth>
+ <policy context="default">
+ <allow send_destination="*" eavesdrop="true"/>
+ <allow eavesdrop="true"/>
+ <allow own="*"/>
+ </policy>
+</busconfig>
+'''
+
+SERVICE_CONFIG = '''\
+[D-BUS Service]
+Name={name}
+Exec={exec}
+'''
+
+
+def dconf(*args, **kwargs):
+ argv = [dconf_exe]
+ argv.extend(args)
+
+ # Setup convenient defaults:
+ kwargs.setdefault('check', True)
+ kwargs.setdefault('stdout', subprocess.PIPE)
+ kwargs.setdefault('universal_newlines', True)
+
+ return subprocess.run(argv, **kwargs)
+
+
+def dconf_read(key, **kwargs):
+ return dconf('read', key, **kwargs).stdout.rstrip('\n')
+
+
+def dconf_write(key, value):
+ dconf('write', key, value)
+
+
+def dconf_list(key):
+ return dconf('list', key).stdout.splitlines()
+
+def dconf_locks(key, **kwargs):
+ lines = dconf('list-locks', key, **kwargs).stdout.splitlines()
+ lines.sort()
+ return lines
+
+def dconf_complete(suffix, prefix):
+ lines = dconf('_complete', suffix, prefix).stdout.splitlines()
+ lines.sort()
+ return lines
+
+
+def dconf_watch(path):
+ args = [dconf_exe, 'watch', path]
+ return subprocess.Popen(args,
+ stdout=subprocess.PIPE,
+ universal_newlines=True)
+
+
+class DBusTest(unittest.TestCase):
+
+ def setUp(self):
+ self.temporary_dir = tempfile.TemporaryDirectory()
+
+ self.runtime_dir = os.path.join(self.temporary_dir.name, 'run')
+ self.config_home = os.path.join(self.temporary_dir.name, 'config')
+ self.dbus_dir = os.path.join(self.temporary_dir.name, 'dbus-1')
+
+ os.mkdir(self.runtime_dir, mode=0o700)
+ os.mkdir(self.config_home, mode=0o700)
+ os.mkdir(self.dbus_dir, mode=0o700)
+ os.mkdir(os.path.join(self.config_home, 'dconf'))
+
+ os.environ['DCONF_BLAME'] = ''
+ os.environ['XDG_RUNTIME_DIR'] = self.runtime_dir
+ os.environ['XDG_CONFIG_HOME'] = self.config_home
+
+ # Prepare dbus-daemon config.
+ dbus_daemon_config = os.path.join(self.dbus_dir, 'session.conf')
+ with open(dbus_daemon_config, 'w') as file:
+ file.write(DAEMON_CONFIG.format(servicedir=self.dbus_dir))
+
+ # Prepare service config.
+ name = 'ca.desrt.dconf'
+ path = os.path.join(self.dbus_dir, '{}.service'.format(name))
+ with open(path, 'w') as file:
+ config = SERVICE_CONFIG.format(name=name, exec=dconf_service_exe)
+ file.write(config)
+
+ # Pipe where daemon will write its address.
+ read_fd, write_fd = os.pipe2(0)
+
+ args = ['dbus-daemon',
+ '--config-file={}'.format(dbus_daemon_config),
+ '--nofork',
+ '--print-address={}'.format(write_fd)]
+
+ # Start daemon
+ self.dbus_daemon_process = subprocess.Popen(args, pass_fds=[write_fd])
+
+ # Close our writing end of pipe. Daemon closes its own writing end of
+ # pipe after printing address, so subsequent reads shouldn't block.
+ os.close(write_fd)
+
+ with os.fdopen(read_fd) as f:
+ dbus_address = f.read().rstrip()
+
+ # Prepare environment
+ os.environ['DBUS_SESSION_BUS_ADDRESS'] = dbus_address
+
+ def tearDown(self):
+ # Terminate dbus-daemon.
+ p = self.dbus_daemon_process
+ try:
+ p.terminate()
+ p.wait(timeout=0.5)
+ except subprocess.TimeoutExpired:
+ p.kill()
+ p.wait()
+
+ self.temporary_dir.cleanup()
+
+ def test_invalid_usage(self):
+ """Invalid dconf usage results in non-zero exit code and help message.
+ """
+ cases = [
+ # No command:
+ [],
+
+ # Invalid command:
+ ['no-such-command'],
+
+ # Too many arguments:
+ ['blame', 'a'],
+
+ # Missing arguments:
+ ['compile'],
+ ['compile', 'output'],
+ # Too many arguments:
+ ['compile', 'output', 'dir1', 'dir2'],
+
+ # Missing arguments:
+ ['_complete'],
+ ['_complete', ''],
+ # Too many arguments:
+ ['_complete', '', '/', '/'],
+
+ # Missing argument:
+ ['dump'],
+ # Dir is required:
+ ['dump', '/key'],
+ # Too many arguments:
+ ['dump', '/a/', '/b/'],
+
+ # Missing argument:
+ ['list'],
+ # Dir is required:
+ ['list', '/foo/bar'],
+ # Too many arguments:
+ ['list', '/foo', '/bar'],
+
+ # Missing argument:
+ ['list-locks'],
+ # Dir is required:
+ ['list-locks', '/key'],
+ # Too many arguments:
+ ['list-locks', '/a/', '/b/'],
+
+ # Missing argument:
+ ['load'],
+ # Dir is required:
+ ['load', '/key'],
+ # Too many arguments:
+ ['load', '/a/', '/b/'],
+
+ # Missing argument:
+ ['read'],
+ # Key is required:
+ ['read', '/dir/'],
+ # Too many arguments:
+ ['read', '/a', '/b'],
+ ['read', '-d', '/a', '/b'],
+
+ # Missing arguments:
+ ['reset'],
+ # Invalid path:
+ ['reset', 'test/test'],
+ # Too many arguments:
+ ['reset', '/test', '/test'],
+ ['reset', '-f', '/', '/'],
+
+ # Missing arguments:
+ ['watch'],
+ # Invalid path:
+ ['watch', 'foo'],
+ # Too many arguments:
+ ['watch', '/a', '/b'],
+
+ # Missing arguments:
+ ['write'],
+ ['write', '/key'],
+ # Invalid value:
+ ['write', '/key', 'not-a-gvariant-value'],
+ # Too many arguments:
+ ['write', '/key', '1', '2'],
+
+ # Too many arguments:
+ ['update', 'a', 'b'],
+ ]
+
+ for args in cases:
+ with self.subTest(args=args):
+ with self.assertRaises(subprocess.CalledProcessError) as cm:
+ dconf(*args, stderr=subprocess.PIPE)
+ self.assertRegex(cm.exception.stderr, 'Usage:')
+
+ def test_help(self):
+ """Help show usage information on stdout and exits with success."""
+
+ stdout = dconf('help', 'write').stdout
+ self.assertRegex(stdout, 'dconf write KEY VALUE')
+
+ stdout = dconf('help', 'help').stdout
+ self.assertRegex(stdout, 'dconf help COMMAND')
+
+ def test_read_nonexisiting(self):
+ """Reading missing key produces no output. """
+
+ self.assertEqual('', dconf_read('/key'))
+
+ def test_write_read(self):
+ """Read returns previously written value."""
+
+ dconf('write', '/key', '0')
+ self.assertEqual('0', dconf_read('/key'))
+
+ dconf('write', '/key', '"hello there"')
+ self.assertEqual("'hello there'", dconf_read('/key'))
+
+ def test_list(self):
+ """List returns a list of names inside given directory.
+
+ Results include both keys and subdirectories.
+ """
+
+ dconf('write', '/org/gnome/app/fullscreen', 'true')
+ dconf('write', '/org/gnome/terminal/profile', '"default"')
+ dconf('write', '/key', '42')
+
+ self.assertEqual(['key', 'org/'], dconf_list('/'))
+
+ self.assertEqual(['gnome/'], dconf_list('/org/'))
+
+ def test_list_missing(self):
+ """List can be used successfully with non existing directories. """
+
+ self.assertEqual([], dconf_list('/no-existing/directory/'))
+
+ def test_reset_key(self):
+ """Reset can be used to reset a value of a single key."""
+
+ dconf('write', '/app/width', '1024')
+ dconf('write', '/app/height', '768')
+ dconf('write', '/app/fullscreen', 'true')
+
+ # Sanity check.
+ self.assertEqual(['fullscreen', 'height', 'width'], dconf_list('/app/'))
+
+ # Reset one key after another:
+ dconf('reset', '/app/fullscreen')
+ self.assertEqual(['height', 'width'], dconf_list('/app/'))
+ dconf('reset', '/app/width')
+ self.assertEqual(['height'], dconf_list('/app/'))
+ dconf('reset', '/app/height')
+ self.assertEqual([], dconf_list('/app/'))
+
+ def test_reset_dir(self):
+ """Reseting whole directory is possible with -f option.
+
+ It is an error not to use -f when resetting a dir.
+ """
+
+ dconf('write', '/app/a', '1')
+ dconf('write', '/app/b', '2')
+ dconf('write', '/app/c/d', '3')
+ dconf('write', '/x', '4')
+ dconf('write', '/y/z', '5')
+
+ with self.assertRaises(subprocess.CalledProcessError) as cm:
+ dconf('reset', '/app/', stderr=subprocess.PIPE)
+ self.assertRegex(cm.exception.stderr, '-f must be given')
+ self.assertRegex(cm.exception.stderr, 'Usage:')
+
+ # Nothing should be removed just yet.
+ self.assertTrue(['a', 'b', 'c'], dconf_list('/app/'))
+
+ # Try again with -f.
+ dconf('reset', '-f', '/app/')
+
+ # /app/ should be gone now:
+ self.assertEqual(['x', 'y/'], dconf_list('/'))
+
+ def test_watch(self):
+ """Watch reports changes made using write command.
+
+ Only changes made inside given subdirectory should be reported.
+ """
+
+ watch_root = dconf_watch('/')
+ watch_org = dconf_watch('/org/')
+
+ # Arbitrary delay to give "dconf watch" time to set-up the watch.
+ # In the case this turns out to be problematic, dconf tool could be
+ # changed to produce debug message after `dconf_client_watch_sync`,
+ # so that we could synchronize on its output.
+
+ time.sleep(0.2)
+
+ dconf('write', '/com/a', '1')
+ dconf('write', '/org/b', '2')
+ dconf('write', '/organ/c', '3')
+ dconf('write', '/c', '4')
+
+ # Again, give "dconf watch" some time to pick-up changes.
+
+ time.sleep(0.2)
+
+ watch_root.terminate()
+ watch_org.terminate()
+
+ watch_root.wait()
+ watch_org.wait()
+
+ # Watch for '/' should capture all writes.
+ expected = '''\
+ /com/a
+ 1
+
+ /org/b
+ 2
+
+ /organ/c
+ 3
+
+ /c
+ 4
+
+ '''
+ self.assertEqual(dedent(expected), watch_root.stdout.read())
+
+ # Watch for '/org/' should capture only a subset of all writes:
+ expected = '''\
+ /org/b
+ 2
+
+ '''
+ self.assertEqual(dedent(expected), watch_org.stdout.read())
+
+ def test_dump_load(self):
+ """Checks that output produced with dump can be used with load and
+ vice versa.
+ """
+ keyfile = dedent('''\
+ [/]
+ password='secret'
+
+ [org/editor]
+ window-fullscreen=true
+ window-size=(1024, 768)
+
+ [org/editor/language/c-sharp]
+ tab-width=8
+
+ [org/editor/language/c]
+ tab-width=2
+ ''')
+
+ # Load and dump is identity.
+ dconf('load', '/', input=keyfile)
+ self.assertEqual(dconf('dump', '/').stdout, keyfile)
+
+ # Copy /org/ directory to /com/.
+ keyfile = dconf('dump', '/org/').stdout
+ dconf('load', '/com/', input=keyfile)
+
+ # Verify that /org/ and /com/ are now exactly the same.
+ keyfile_org = dconf('dump', '/org/').stdout
+ keyfile_com = dconf('dump', '/com/').stdout
+ self.assertEqual(keyfile_org, keyfile_com)
+
+ def test_complete(self):
+ """Tests _complete command used internally to implement bash completion.
+
+ Runs completion queries after loading a sample database from key-file.
+ """
+
+ keyfile = dedent('''\
+ [org]
+ calamity=false
+
+ [org/calculator]
+ window-position=(0, 0)
+
+ [org/calendar]
+ window-position=(0, 0)
+
+ [org/history]
+ file0='/tmp/a'
+ file1='/tmp/b'
+ file2='/tmp/c'
+ ''')
+
+ dconf('load', '/', input=keyfile)
+
+ # Empty string is completed to '/'.
+ completions = dconf_complete('', '')
+ self.assertEqual(completions, ['/'])
+ completions = dconf_complete('/', '')
+ self.assertEqual(completions, ['/'])
+
+ # Invalid paths don't return any completions.
+ completions = dconf_complete('', 'foo/')
+ self.assertEqual(completions, [])
+ completions = dconf_complete('/', 'foo/')
+ self.assertEqual(completions, [])
+
+ # Key completions include trailing whitespace,
+ # directory completions do not.
+ completions = dconf_complete('', '/org/')
+ self.assertEqual(completions,
+ ['/org/calamity ',
+ '/org/calculator/',
+ '/org/calendar/',
+ '/org/history/'])
+
+ # Only matches with given prefix are returned.
+ completions = dconf_complete('', '/org/cal')
+ self.assertEqual(completions,
+ ['/org/calamity ',
+ '/org/calculator/',
+ '/org/calendar/'])
+
+ # Only matches with given suffix are returned.
+ completions = dconf_complete('/', '/org/cal')
+ self.assertEqual(completions,
+ ['/org/calculator/',
+ '/org/calendar/'])
+
+ def test_compile_precedence(self):
+ """Compile processes key-files in reverse lexicographical order.
+
+ When key occurs in multiple files, the value from file processed first
+ is preferred.
+
+ Test that by preparing four key-files each with a different value for
+ '/org/file'. Compiling it directly into user database, and performing
+ read to check which value had been selected.
+ """
+ # Prepare key file database directory.
+ user_d = os.path.join(self.temporary_dir.name, 'user.d')
+ os.mkdir(user_d, mode=0o700)
+
+ def write_config_d(name):
+ keyfile = dedent('''
+ [org]
+ file = {name}
+ '''.format(name=name))
+
+ with open(os.path.join(user_d, name), 'w') as file:
+ file.write(keyfile)
+
+ write_config_d('00')
+ write_config_d('25')
+ write_config_d('50')
+ write_config_d('99')
+
+ # Compile directly into user configuration file.
+ dconf('compile',
+ os.path.join(self.config_home, 'dconf', 'user'),
+ user_d)
+
+ # Lexicographically last value should win:
+ self.assertEqual(dconf_read('/org/file'), '99')
+
+ @unittest.expectedFailure
+ def test_redundant_disk_writes(self):
+ """Redundant disk writes are avoided.
+
+ When write or reset operations don't modify actual contents of the
+ database, the database file shouldn't be needlessly rewritten. Check
+ mtime after each redundant operation to verify that.
+ """
+
+ config = os.path.join(self.config_home, 'dconf', 'user')
+
+ def move_time_back(path):
+ """Moves file mtime 60 seconds back and returns its new value.
+
+ Used to avoid false positives during comparison checks in the case
+ that mtime is stored with low precision.
+ """
+ atime = os.path.getatime(config)
+ mtime = os.path.getmtime(config)
+
+ os.utime(config, times=(atime, mtime - 60))
+
+ return os.path.getmtime(config)
+
+ # Activate service to trigger initial database write.
+ dconf_write('/prime', '5')
+
+ # Sanity check that database is rewritten when necessary.
+ saved_mtime = move_time_back(config)
+ dconf_write('/prime', '13')
+ self.assertLess(saved_mtime, os.path.getmtime(config))
+
+ # Write the same value as one already in the database.
+ saved_mtime = move_time_back(config)
+ dconf('write', '/prime', '13')
+ self.assertEqual(saved_mtime, os.path.getmtime(config))
+
+ # Reset not directory which is not present in the database.
+ saved_mtime = move_time_back(config)
+ dconf('reset', '-f', '/non-existing/directory/')
+ self.assertEqual(saved_mtime, os.path.getmtime(config))
+
+ def test_compile_dotfiles(self):
+ """Compile ignores files starting with a dot."""
+
+ user_d = os.path.join(self.temporary_dir.name, 'user.d')
+ os.mkdir(user_d)
+
+ a_conf = dedent('''\
+ [math]
+ a=42
+ ''')
+
+ a_conf_swp = dedent('''\
+ [math]
+ b=13
+ ''')
+
+ with open(os.path.join(user_d, 'a.conf'), 'w') as file:
+ file.write(a_conf)
+
+ with open(os.path.join(user_d, '.a.conf.swp'), 'w') as file:
+ file.write(a_conf_swp)
+
+ dconf('compile',
+ os.path.join(self.config_home, 'dconf', 'user'),
+ user_d)
+
+ self.assertEqual(a_conf, dconf('dump', '/').stdout)
+
+ def test_database_invalidation(self):
+ """Update invalidates previous database by overwriting the header with
+ null bytes.
+ """
+
+ db = os.path.join(self.temporary_dir.name, 'db')
+ local = os.path.join(db, 'local')
+ local_d = os.path.join(db, 'local.d')
+
+ os.makedirs(local_d)
+
+ with open(os.path.join(local_d, 'local.conf'), 'w') as file:
+ file.write(dedent('''\
+ [org/gnome/desktop/background]
+ picture-uri = 'file:///usr/share/backgrounds/gnome/ColdWarm.jpg'
+ '''))
+
+ # Compile database for the first time.
+ dconf('update', db)
+
+ with open(local, 'rb') as file:
+ with mmap.mmap(file.fileno(), 8, mmap.MAP_SHARED, prot=mmap.PROT_READ) as mm:
+ # Sanity check that database is valid.
+ self.assertNotEqual(b'\0'*8, mm[:8])
+
+ dconf('update', db)
+
+ # Now database should be marked as invalid.
+ self.assertEqual(b'\0'*8, mm[:8])
+
+ def test_update_failure(self):
+ """Update should skip invalid configuration directory and continue with
+ others. Failure to update one of databases should be indicated with
+ non-zero exit code.
+
+ Regression test for issue #42.
+ """
+
+ # A few different scenarios when loading data from key-file:
+ valid_key_file = '[org]\na = 1'
+
+ invalid_key_file = "<html>This isn't a key-file nor valid HTML."
+
+ invalid_group_name = dedent('''\
+ [org//no/me]
+ a = 2
+ ''')
+
+ invalid_key_name = dedent('''\
+ [org/gnome]
+ b// = 2
+ ''')
+
+ invalid_value = dedent('''\
+ [org/gnome]
+ c = 2x2
+ ''')
+
+ db = os.path.join(self.temporary_dir.name, 'db')
+
+ # Database name, valid, content
+ cases = [('site_aa', True, valid_key_file),
+ ('site_bb', False, invalid_key_file),
+ ('site_cc', False, invalid_group_name),
+ ('site_dd', False, invalid_key_name),
+ ('site_ee', False, invalid_value),
+ ('site_ff', True, valid_key_file)]
+
+ for (name, is_valid, content) in cases:
+ conf_dir = os.path.join(db, '{}.d'.format(name))
+ conf_file = os.path.join(conf_dir, '{}.conf'.format(name))
+
+ os.makedirs(conf_dir)
+
+ with open(conf_file, 'w') as file:
+ file.write(content)
+
+ # Return code should indicate failure.
+ with self.assertRaises(subprocess.CalledProcessError) as cm:
+ dconf('update', db, stderr=subprocess.PIPE)
+
+ for (name, is_valid, content) in cases:
+ path = os.path.join(db, name)
+ if is_valid:
+ # This one was valid so db should be written successfully.
+ self.assertTrue(os.path.exists(path))
+ self.assertNotRegex(cm.exception.stderr, name)
+ else:
+ # This one was broken so we shouldn't create corresponding db.
+ self.assertFalse(os.path.exists(path))
+ self.assertRegex(cm.exception.stderr, name)
+
+ def test_locks(self):
+ """Key paths can be locked in system databases.
+
+ - Update configures locks based on files found in "locks" subdirectory.
+ - Locks can be listed with list-locks command.
+ - Locks are enforced during write.
+ - Load can ignore changes to locked keys using -f option.
+ """
+
+ db = os.path.join(self.temporary_dir.name, 'db')
+ profile = os.path.join(self.temporary_dir.name, 'profile')
+ site = os.path.join(db, 'site')
+ site_d = os.path.join(db, 'site.d')
+ site_locks = os.path.join(db, site_d, 'locks')
+
+ os.makedirs(site_locks)
+
+ # For meaningful test of locks we need two sources, first of which
+ # should be writable. We will use user-db and file-db.
+ with open(profile, 'w') as file:
+ file.write(dedent('''\
+ user-db:user
+ file-db:{}
+ '''.format(site)))
+
+ # Environment to use for all dconf client invocations.
+ env = dict(os.environ)
+ env['DCONF_PROFILE'] = profile
+
+ # Default settings
+ with open(os.path.join(site_d, '10-site-defaults'), 'w') as file:
+ file.write(dedent('''\
+ # Some useful default settings for our site
+ [system/proxy/http]
+ host='172.16.0.1'
+ enabled=true
+
+ [org/gnome/desktop]
+ background='company-wallpaper.jpeg'
+ '''))
+
+ # Lock proxy settings.
+ with open(os.path.join(site_locks, '10-proxy-lock'), 'w') as file:
+ file.write(dedent('''\
+ # Prevent changes to proxy
+ /system/proxy/http/host
+ /system/proxy/http/enabled
+ /system/proxy/ftp/host
+ /system/proxy/ftp/enabled
+ '''))
+
+ # Compile site configuration.
+ dconf('update', db)
+
+ # Test list-locks:
+ self.assertEqual(['/system/proxy/ftp/enabled',
+ '/system/proxy/ftp/host',
+ '/system/proxy/http/enabled',
+ '/system/proxy/http/host'],
+ dconf_locks('/', env=env))
+
+ self.assertEqual(['/system/proxy/http/enabled',
+ '/system/proxy/http/host'],
+ dconf_locks('/system/proxy/http/', env=env))
+
+ self.assertEqual([],
+ dconf_locks('/org/gnome/', env=env))
+
+ # Changing unlocked defaults is fine.
+ dconf('write', '/org/gnome/desktop/background',
+ '"ColdWarm.jpg"', env=env)
+
+ # It is an error to change locked keys.
+ with self.assertRaises(subprocess.CalledProcessError) as cm:
+ dconf('write', '/system/proxy/http/enabled', 'false',
+ env=env, stderr=subprocess.PIPE)
+ self.assertRegex(cm.exception.stderr, 'non-writable keys')
+
+ keyfile = dedent('''\
+ [system/proxy/http]
+ enabled=false
+ [org/gnome/desktop]
+ background='Winter.png'
+ ''')
+
+ # Load fails to apply changes if some key is locked ...
+ with self.assertRaises(subprocess.CalledProcessError) as cm:
+ dconf('load', '/', input=keyfile, env=env, stderr=subprocess.PIPE)
+ self.assertRegex(cm.exception.stderr, 'non-writable keys')
+ self.assertEqual('true', dconf_read('/system/proxy/http/enabled', env=env))
+ self.assertEqual("'ColdWarm.jpg'", dconf_read('/org/gnome/desktop/background', env=env))
+
+ # ..., unless invoked with -f option, then it changes unlocked keys.
+ stderr = dconf('load', '-f', '/', input=keyfile, env=env, stderr=subprocess.PIPE).stderr
+ self.assertRegex(stderr, 'ignored non-writable key')
+ self.assertEqual('true', dconf_read('/system/proxy/http/enabled', env=env))
+ self.assertEqual("'Winter.png'", dconf_read('/org/gnome/desktop/background', env=env))
+
+ def test_dconf_blame(self):
+ """Blame returns recorded information about write operations.
+
+ Recorded information include sender bus name, sender process id and
+ object path the write operations was invoked on.
+ """
+
+ p = subprocess.Popen([dconf_exe, 'write', '/prime', '307'])
+ p.wait()
+
+ blame = dconf('blame').stdout
+ print(blame)
+
+ self.assertRegex(blame, 'Sender: ')
+ self.assertRegex(blame, 'PID: {}'.format(p.pid))
+ self.assertRegex(blame, 'Object path: /ca/desrt/dconf/Writer/user')
+
+if __name__ == '__main__':
+ # Make sure we don't pick up mandatory profile.
+ mandatory_profile = '/run/dconf/user/{}'.format(os.getuid())
+ assert not os.path.isfile(mandatory_profile)
+
+ # Avoid profile sourced from environment or system data dirs.
+ os.environ.pop('DCONF_PROFILE', None)
+ os.environ.pop('XDG_DATA_DIRS', None)
+ # Avoid interfering with external message buses.
+ os.environ['DBUS_SYSTEM_BUS_ADDRESS'] = ''
+ os.environ['DBUS_SESSION_BUS_ADDRESS'] = ''
+
+ if len(sys.argv) < 3:
+ message = 'Usage: {} path-to-dconf path-to-dconf-service'.format(
+ sys.argv[0])
+ raise RuntimeError(message)
+
+ dconf_exe, dconf_service_exe = sys.argv[1:3]
+ del sys.argv[1:3]
+
+ # Run tests!
+ unittest.main()
diff --git a/tests/tmpdir.c b/tests/tmpdir.c
new file mode 100644
index 0000000..b8c8745
--- /dev/null
+++ b/tests/tmpdir.c
@@ -0,0 +1,53 @@
+#include "tmpdir.h"
+
+#include <glib/gstdio.h>
+#include "../common/dconf-paths.h"
+#include <string.h>
+
+gchar *
+dconf_test_create_tmpdir (void)
+{
+ GError *error = NULL;
+ gchar *temp;
+
+ temp = g_dir_make_tmp ("dconf-testcase.XXXXXX", &error);
+ g_assert_no_error (error);
+ g_assert (temp != NULL);
+
+ return temp;
+}
+
+static void
+rm_rf (const gchar *file)
+{
+ GDir *dir;
+
+ dir = g_dir_open (file, 0, NULL);
+ if (dir)
+ {
+ const gchar *basename;
+
+ while ((basename = g_dir_read_name (dir)))
+ {
+ gchar *fullname;
+
+ fullname = g_build_filename (file, basename, NULL);
+ rm_rf (fullname);
+ g_free (fullname);
+ }
+
+ g_dir_close (dir);
+ g_rmdir (file);
+ }
+
+ else
+ /* excess paranoia -- only unlink if we're really really sure */
+ if (strstr (file, "/dconf-testcase") && !strstr (file, ".."))
+ g_unlink (file);
+}
+
+void
+dconf_test_remove_tmpdir (const gchar *tmpdir)
+{
+ rm_rf (tmpdir);
+}
diff --git a/tests/tmpdir.h b/tests/tmpdir.h
new file mode 100644
index 0000000..6e9dae8
--- /dev/null
+++ b/tests/tmpdir.h
@@ -0,0 +1,9 @@
+#ifndef __dconf_tmpdir_h__
+#define __dconf_tmpdir_h__
+
+#include <glib.h>
+
+gchar *dconf_test_create_tmpdir (void);
+void dconf_test_remove_tmpdir (const gchar *tmpdir);
+
+#endif /* __dconf_tmpdir_h__ */
diff --git a/tests/writer.c b/tests/writer.c
new file mode 100644
index 0000000..955ba91
--- /dev/null
+++ b/tests/writer.c
@@ -0,0 +1,233 @@
+/*
+ * Copyright © 2018 Endless Mobile, Inc
+ *
+ * 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/>.
+ *
+ * Author: Philip Withnall <withnall@endlessm.com>
+ */
+
+#include <glib.h>
+#include <glib/gstdio.h>
+#include <locale.h>
+
+#include "service/dconf-generated.h"
+#include "service/dconf-writer.h"
+
+static guint n_warnings = 0;
+
+static GLogWriterOutput
+log_writer_cb (GLogLevelFlags log_level,
+ const GLogField *fields,
+ gsize n_fields,
+ gpointer user_data)
+{
+ if (log_level & G_LOG_LEVEL_WARNING)
+ n_warnings++;
+
+ return G_LOG_WRITER_HANDLED;
+}
+
+static void
+assert_n_warnings (guint expected_n_warnings)
+{
+ g_assert_cmpuint (n_warnings, ==, expected_n_warnings);
+ n_warnings = 0;
+}
+
+typedef struct
+{
+ gchar *dconf_dir; /* (owned) */
+} Fixture;
+
+gchar *config_dir = NULL;
+
+static void
+set_up (Fixture *fixture,
+ gconstpointer test_data)
+{
+ fixture->dconf_dir = g_build_filename (config_dir, "dconf", NULL);
+ g_assert_cmpint (g_mkdir (fixture->dconf_dir, 0755), ==, 0);
+
+ g_test_message ("Using dconf directory: %s", fixture->dconf_dir);
+}
+
+static void
+tear_down (Fixture *fixture,
+ gconstpointer test_data)
+{
+ g_assert_cmpint (g_rmdir (fixture->dconf_dir), ==, 0);
+ g_clear_pointer (&fixture->dconf_dir, g_free);
+
+ assert_n_warnings (0);
+}
+
+/* Test basic initialisation of a #DConfWriter. This is essentially a smoketest. */
+static void
+test_writer_basic (Fixture *fixture,
+ gconstpointer test_data)
+{
+ g_autoptr(DConfWriter) writer = NULL;
+
+ writer = DCONF_WRITER (dconf_writer_new (DCONF_TYPE_WRITER, "some-name"));
+ g_assert_nonnull (writer);
+
+ g_assert_cmpstr (dconf_writer_get_name (writer), ==, "some-name");
+}
+
+/* Test that beginning a write operation when no database exists succeeds. Note
+ * that the database will not actually be created until some changes are made
+ * and the write is committed. */
+static void
+test_writer_begin_missing (Fixture *fixture,
+ gconstpointer test_data)
+{
+ g_autoptr(DConfWriter) writer = NULL;
+ DConfWriterClass *writer_class;
+ gboolean retval;
+ g_autoptr(GError) local_error = NULL;
+ g_autofree gchar *db_filename = g_build_filename (fixture->dconf_dir, "missing", NULL);
+
+ /* Check the database doesn’t exist. */
+ g_assert_false (g_file_test (db_filename, G_FILE_TEST_EXISTS));
+
+ /* Create a writer. */
+ writer = DCONF_WRITER (dconf_writer_new (DCONF_TYPE_WRITER, "missing"));
+ g_assert_nonnull (writer);
+
+ writer_class = DCONF_WRITER_GET_CLASS (writer);
+ retval = writer_class->begin (writer, &local_error);
+ g_assert_no_error (local_error);
+ g_assert_true (retval);
+}
+
+/* Test that beginning a write operation when a corrupt or empty database exists
+ * will take a backup of the database and then succeed. Note that a new empty
+ * database will not actually be created until some changes are made and the
+ * write is committed. */
+typedef struct
+{
+ const gchar *corrupt_db_contents;
+ guint n_existing_backups;
+} BeginCorruptFileData;
+
+static void
+test_writer_begin_corrupt_file (Fixture *fixture,
+ gconstpointer test_data)
+{
+ const BeginCorruptFileData *data = test_data;
+ g_autoptr(DConfWriter) writer = NULL;
+ DConfWriterClass *writer_class;
+ gboolean retval;
+ g_autoptr(GError) local_error = NULL;
+ g_autofree gchar *db_filename = g_build_filename (fixture->dconf_dir, "corrupt", NULL);
+ g_autofree gchar *new_db_filename_backup = NULL;
+ g_autofree gchar *backup_file_contents = NULL;
+ gsize backup_file_contents_len = 0;
+ guint i;
+
+ /* Create a corrupt database. */
+ g_file_set_contents (db_filename, data->corrupt_db_contents, -1, &local_error);
+ g_assert_no_error (local_error);
+
+ /* Create any existing backups, to test we don’t overwrite them. */
+ for (i = 0; i < data->n_existing_backups; i++)
+ {
+ g_autofree gchar *db_filename_backup = g_strdup_printf ("%s~%u", db_filename, i);
+ g_file_set_contents (db_filename_backup, "backup", -1, &local_error);
+ g_assert_no_error (local_error);
+ }
+
+ new_db_filename_backup = g_strdup_printf ("%s~%u", db_filename, data->n_existing_backups);
+
+ /* Create a writer. */
+ writer = DCONF_WRITER (dconf_writer_new (DCONF_TYPE_WRITER, "corrupt"));
+ g_assert_nonnull (writer);
+
+ writer_class = DCONF_WRITER_GET_CLASS (writer);
+ retval = writer_class->begin (writer, &local_error);
+ g_assert_no_error (local_error);
+ g_assert_true (retval);
+
+ /* The writer should have printed a warning about the corrupt database. */
+ assert_n_warnings (1);
+
+ /* Check a backup file has been created and has the right content. */
+ g_file_get_contents (new_db_filename_backup, &backup_file_contents,
+ &backup_file_contents_len, &local_error);
+ g_assert_no_error (local_error);
+ g_assert_cmpstr (backup_file_contents, ==, data->corrupt_db_contents);
+ g_assert_cmpuint (backup_file_contents_len, ==, strlen (data->corrupt_db_contents));
+
+ /* Clean up. */
+ g_assert_cmpint (g_unlink (new_db_filename_backup), ==, 0);
+
+ for (i = 0; i < data->n_existing_backups; i++)
+ {
+ g_autofree gchar *db_filename_backup = g_strdup_printf ("%s~%u", db_filename, i);
+ g_assert_cmpint (g_unlink (db_filename_backup), ==, 0);
+ }
+}
+
+int
+main (int argc, char **argv)
+{
+ g_autoptr(GError) local_error = NULL;
+ int retval;
+ const BeginCorruptFileData empty_data = { "", 0 };
+ const BeginCorruptFileData corrupt_file_data0 = {
+ "secretly not a valid GVDB database 😧", 0
+ };
+ const BeginCorruptFileData corrupt_file_data1 = {
+ "secretly not a valid GVDB database 😧", 1
+ };
+ const BeginCorruptFileData corrupt_file_data2 = {
+ "secretly not a valid GVDB database 😧", 2
+ };
+
+ setlocale (LC_ALL, "");
+
+ g_test_init (&argc, &argv, NULL);
+
+ /* Set up a fake $XDG_CONFIG_HOME. We can’t do this in the fixture, as
+ * g_get_user_config_dir() caches its return value. */
+ config_dir = g_dir_make_tmp ("dconf-test-writer_XXXXXX", &local_error);
+ g_assert_no_error (local_error);
+ g_assert_true (g_setenv ("XDG_CONFIG_HOME", config_dir, TRUE));
+ g_test_message ("Using config directory: %s", config_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,
+ test_writer_begin_missing, tear_down);
+ g_test_add ("/writer/begin/empty", Fixture, &empty_data, set_up,
+ test_writer_begin_corrupt_file, tear_down);
+ g_test_add ("/writer/begin/corrupt-file/0", Fixture, &corrupt_file_data0, set_up,
+ test_writer_begin_corrupt_file, tear_down);
+ g_test_add ("/writer/begin/corrupt-file/1", Fixture, &corrupt_file_data1, set_up,
+ test_writer_begin_corrupt_file, tear_down);
+ g_test_add ("/writer/begin/corrupt-file/2", Fixture, &corrupt_file_data2, set_up,
+ test_writer_begin_corrupt_file, tear_down);
+
+ retval = g_test_run ();
+
+ /* Clean up the config dir. */
+ g_unsetenv ("XDG_CONFIG_HOME");
+ g_assert_cmpint (g_rmdir (config_dir), ==, 0);
+ g_clear_pointer (&config_dir, g_free);
+
+ return retval;
+}
diff --git a/trim-lcov.py b/trim-lcov.py
new file mode 100755
index 0000000..c1709c3
--- /dev/null
+++ b/trim-lcov.py
@@ -0,0 +1,53 @@
+#!/usr/bin/python
+
+# This script removes branch and/or line coverage data for lines that
+# contain a particular substring.
+#
+# In the interest of "fairness" it removes all branch or coverage data
+# when a match is found -- not just negative data. It is therefore
+# likely that running this script will actually reduce the total number
+# of lines and branches that are marked as covered (in absolute terms).
+#
+# This script intentionally avoids checking for errors. Any exceptions
+# will trigger make to fail.
+#
+# Author: Ryan Lortie <desrt@desrt.ca>
+
+import sys
+
+line_suppress = ['g_assert_not_reached']
+branch_suppress = ['g_assert', 'g_return_if_fail', 'g_return_val_if_fail', 'G_DEFINE_TYPE']
+
+def check_suppress(suppressions, source, data):
+ line, _, rest = data.partition(',')
+ line = int(line) - 1
+
+ assert line < len(source)
+
+ for suppression in suppressions:
+ if suppression in source[line]:
+ return True
+
+ return False
+
+source = []
+for line in sys.stdin:
+ line = line[:-1]
+
+ keyword, _, rest = line.partition(':')
+
+ # Source file
+ if keyword == 'SF':
+ source = file(rest).readlines()
+
+ # Branch coverage data
+ elif keyword == 'BRDA':
+ if check_suppress(branch_suppress, source, rest):
+ continue
+
+ # Line coverage data
+ elif keyword == 'DA':
+ if check_suppress(line_suppress, source, rest):
+ continue
+
+ print line