1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781
|
<?xml version="1.0"?>
<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
"http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd"
[
<!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'">
]>
<book id="index">
<bookinfo>
<title>GNOME Software Reference Manual</title>
</bookinfo>
<reference id="tutorial">
<title>GNOME Software Plugin Tutorial</title>
<partintro>
<para>
GNOME Software is a software installer designed to be easy to use.
</para>
<section>
<title>Introduction</title>
<para>
At the heart of gnome software the application is just a plugin loader
that has some GTK UI that gets created for various result types.
The idea is we have lots of small plugins that each do one thing and
then pass the result onto the other plugins.
These are ordered by dependencies against each other at runtime and
each one can do things like editing an existing application or adding a
new application to the result set.
This is how we can add support for things like firmware updating,
GNOME Shell web-apps and flatpak bundles without making big
changes all over the source tree.
</para>
<para>
There are broadly 3 types of plugin methods:
</para>
<itemizedlist>
<listitem>
<para><emphasis role="strong">Actions</emphasis>: Do something on a specific GsApp</para>
</listitem>
<listitem>
<para><emphasis role="strong">Refine</emphasis>: Get details about a specific GsApp</para>
</listitem>
<listitem>
<para><emphasis role="strong">Adopt</emphasis>: Can this plugin handle this GsApp</para>
</listitem>
</itemizedlist>
<para>
In general, building things out-of-tree isn't something that I think is
a very good idea; the API and ABI inside gnome-software is still
changing and there's a huge benefit to getting plugins upstream where
they can undergo review and be ported as the API adapts.
I'm also super keen to provide configurability in GSettings for doing
obviously-useful things, the sort of thing Fleet Commander can set for
groups of users.
</para>
<para>
However, now we're shipping gnome-software in enterprise-class distros
we might want to allow customers to ship their own plugins to make
various business-specific changes that don't make sense upstream.
This might involve querying a custom LDAP server and changing the
suggested apps to reflect what groups the user is in, or might involve
showing a whole new class of applications that does not conform to the
Linux-specific <emphasis>application is a desktop-file</emphasis> paradigm.
This is where a plugin makes sense.
</para>
<para>
The plugin needs to create a class derived from <type>GsPlugin</type>,
and define the vfuncs that it needs. The
plugin name is taken automatically from the suffix of the
<filename>.so</filename> file. The type of the plugin is exposed to
gnome-software using <function>gs_plugin_query_type()</function>, which
must be exported from the module.
</para>
<example>
<title>A sample plugin</title>
<programlisting>
/*
* Copyright (C) 2016 Richard Hughes
*/
#include <glib.h>
#include <gnome-software.h>
struct _GsPluginSample {
GsPlugin parent;
/* private data here */
};
G_DEFINE_TYPE (GsPluginSample, gs_plugin_sample, GS_TYPE_PLUGIN)
static void
gs_plugin_sample_init (GsPluginSample *self)
{
GsPlugin *plugin = GS_PLUGIN (self);
gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "appstream");
}
static void
gs_plugin_sample_list_apps_async (GsPlugin *plugin,
GsAppQuery *query,
GsPluginListAppsFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginSample *self = GS_PLUGIN_SAMPLE (plugin);
g_autoptr(GTask) task = NULL;
const gchar * const *keywords;
g_autoptr(GsAppList) list = gs_app_list_new ();
task = gs_plugin_list_apps_data_new_task (plugin, query, flags,
cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_sample_list_apps_async);
if (query == NULL ||
gs_app_query_get_keywords (query) == NULL ||
gs_app_query_get_n_properties_set (query) != 1) {
g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
"Unsupported query");
return;
}
keywords = gs_app_query_get_keywords (query);
for (gsize i = 0; keywords[i] != NULL; i++) {
if (g_str_equal (keywords[i], "fotoshop")) {
g_autoptr(GsApp) app = gs_app_new ("gimp.desktop");
gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
gs_app_list_add (list, app);
}
}
g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref);
}
static GsAppList *
gs_plugin_sample_list_apps_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_pointer (G_TASK (result), error);
}
static void
gs_plugin_sample_class_init (GsPluginSampleClass *klass)
{
GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
plugin_class->list_apps_async = gs_plugin_sample_list_apps_async;
plugin_class->list_apps_finish = gs_plugin_sample_list_apps_finish;
}
GType
gs_plugin_query_type (void)
{
return GS_TYPE_PLUGIN_SAMPLE;
}
</programlisting>
</example>
<para>
We have to define when our plugin is run in reference to other plugins,
in this case, making sure we run before <code>appstream</code>.
As we're such a simple plugin we're relying on another plugin to run
after us to actually make the GsApp <emphasis>complete</emphasis>,
i.e. loading icons and setting a localised long description.
</para>
<para>
In this example we want to show GIMP as a result (from any provider,
e.g. flatpak or a distro package) when the user searches exactly for
<code>fotoshop</code>.
</para>
<para>
We can then build and install the plugin using:
</para>
<informalexample>
<programlisting>
gcc -shared -o libgs_plugin_example.so gs-plugin-example.c -fPIC \
$(pkg-config --libs --cflags gnome-software) \
-DI_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE && \
cp libgs_plugin_example.so $(pkg-config gnome-software --variable=plugindir)
</programlisting>
</informalexample>
<mediaobject id="gs-example-search">
<imageobject>
<imagedata format="PNG" fileref="gs-example-search.png" align="center"/>
</imageobject>
</mediaobject>
</section>
<section>
<title>Distribution Specific Functionality</title>
<para>
Some plugins should only run on specific distributions, for instance
the <code>fedora-pkgdb-collections</code> plugin should only be used on
Fedora systems.
This can be achieved with a simple runtime check using the helper
<code>gs_plugin_check_distro_id()</code> method or the <code>GsOsRelease</code>
object where more complicated rules are required.
</para>
<example>
<title>Self disabling on other distributions</title>
<programlisting>
static void
gs_plugin_sample_init (GsPluginSample *self)
{
GsPlugin *plugin = GS_PLUGIN (self);
if (!gs_plugin_check_distro_id (plugin, "ubuntu")) {
gs_plugin_set_enabled (plugin, FALSE);
return;
}
/* set up private data etc. */
}
</programlisting>
</example>
</section>
<section>
<title>Custom Applications in the Installed List</title>
<para>
Next is returning custom applications in the installed list.
The use case here is a proprietary software distribution method that
installs custom files into your home directory, but you can use your
imagination for how this could be useful.
The example here is all hardcoded, and a true plugin would have to
derive the details about the GsApp, for example reading in an XML
file or YAML config file somewhere.
</para>
<example>
<title>Example showing a custom installed application</title>
<programlisting>
static void
gs_plugin_sample_init (GsPluginSample *self)
{
GsPlugin *plugin = GS_PLUGIN (self);
gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_BEFORE, "icons");
}
static void
gs_plugin_custom_list_apps_async (GsPlugin *plugin,
GsAppQuery *query,
GsPluginListAppsFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_autofree gchar *fn = NULL;
g_autoptr(GsApp) app = NULL;
g_autoptr(GIcon) icon = NULL;
g_autoptr(GsAppList) list = gs_app_list_new ();
g_autoptr(GTask) task = NULL;
task = g_task_new (plugin, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_custom_list_apps_async);
/* We’re only listing installed apps in this example. */
if (query == NULL ||
gs_app_query_get_is_installed (query) != GS_APP_QUERY_TRISTATE_TRUE ||
gs_app_query_get_n_properties_set (query) != 1) {
g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
"Unsupported query");
return;
}
/* check if the app exists */
fn = g_build_filename (g_get_home_dir (), "chiron", NULL);
if (!g_file_test (fn, G_FILE_TEST_EXISTS)) {
g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref);
return;
}
/* the trigger exists, so create a fake app */
app = gs_app_new ("chiron.desktop");
gs_app_set_management_plugin (app, plugin);
gs_app_set_kind (app, AS_COMPONENT_KIND_DESKTOP_APP);
gs_app_set_state (app, GS_APP_STATE_INSTALLED);
gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "Chiron");
gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, "A teaching application");
gs_app_set_description (app, GS_APP_QUALITY_NORMAL,
"Chiron is the name of an application.\n\n"
"It can be used to demo some of our features");
/* these are all optional, but make details page looks better */
gs_app_set_version (app, "1.2.3");
gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, 2 * 1024 * 1024);
gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 3 * 1024 * 1024);
gs_app_set_origin_hostname (app, "http://www.teaching-example.org/");
gs_app_add_category (app, "Game");
gs_app_add_category (app, "ActionGame");
gs_app_set_license (app, GS_APP_QUALITY_NORMAL, "GPL-2.0-or-later and LGPL-2.1-or-later");
/* use a stock icon */
icon = g_themed_icon_new ("input-gaming");
gs_app_add_icon (app, icon);
/* return new app */
gs_app_list_add (list, app);
g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref);
}
static GsAppList *
gs_plugin_custom_list_apps_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_pointer (G_TASK (result), error);
}
</programlisting>
</example>
<para>
This shows a lot of the plugin architecture in action. Some notable points:
</para>
<itemizedlist>
<listitem>
<para>
Setting the management plugin means we can check for this string
when working out if we can handle the install or remove action.
</para>
</listitem>
<listitem>
<para>
Most applications want a kind of <code>AS_COMPONENT_KIND_DESKTOP_APP</code>
to be visible as an application.
</para>
</listitem>
<listitem>
<para>
The origin is where the application originated from — usually
this will be something like <emphasis>Fedora Updates</emphasis>.
</para>
</listitem>
<listitem>
<para>
Setting the license means we don't get the non-free warning —
removing the 3rd party warning can be done using
<code>GS_APP_QUIRK_PROVENANCE</code>
</para>
</listitem>
<listitem>
<para>
The icon will be loaded into a pixbuf of the correct size when
needed by the UI. You must ensure that icons are available at
common sizes. For icons of type <code>GsRemoteIcon</code>, the
<code>icons</code> plugin will download and cache the icon
locally.
</para>
</listitem>
</itemizedlist>
<para>
To show this fake application just compile and install the plugin,
<code>touch ~/chiron</code> and then restart gnome-software.
To avoid restarting <filename>gnome-software</filename> each time a
proper plugin would create a <code>GFileMonitor</code> object to
monitor files.
</para>
<mediaobject id="gs-example-installed">
<imageobject>
<imagedata format="PNG" fileref="gs-example-installed.png" align="center"/>
</imageobject>
</mediaobject>
<para>
By filling in the optional details (which can also be filled in using
<code>refine_async()</code> you can also make the details
page a much more exciting place.
Adding a set of screenshots is left as an exercise to the reader.
</para>
<mediaobject id="gs-example-details">
<imageobject>
<imagedata format="PNG" fileref="gs-example-details.png" align="center"/>
</imageobject>
</mediaobject>
</section>
<section>
<title>Downloading Metadata and Updates</title>
<para>
The plugin loader supports a <code>refresh_metadata_async()</code> vfunc that
is called in various situations.
To ensure plugins have the minimum required metadata on disk it is
called at startup, but with a cache age of <emphasis>infinite</emphasis>.
This basically means the plugin must just ensure that
<emphasis role="strong">any</emphasis> data exists no matter what the age.
</para>
<para>
Usually once per hour, we'll call <code>refresh_metadata_async()</code> but
with the correct cache age set (typically a little over 24 hours) which
allows the plugin to download new metadata or payload files from remote
servers.
The <code>gs_utils_get_file_age()</code> utility helper can help you
work out the cache age of a file, or the plugin can handle it some other
way.
</para>
<para>
For the Flatpak plugin we just make sure the AppStream metadata exists
at startup, which allows us to show search results in the UI.
If the metadata did not exist (e.g. if the user had added a remote
using the command-line without gnome-software running) then we would
show a loading screen with a progress bar before showing the main UI.
On fast connections we should only show that for a couple of seconds,
but it's a good idea to try any avoid that if at all possible in the
plugin.
Once per day the <code>gs_plugin_get_updates()</code> method is called,
and then a <code>GsPluginJobUpdateApps</code> job may be run with the
<code>GS_PLUGIN_JOB_UPDATE_APPS_FLAGS_NO_APPLY</code> flag if the
user has configured automatic updates.
This is where the Flatpak plugin would download any ostree trees (but
not doing the deploy step) so that the applications can be updated live
in the details panel without having to wait for the download to complete.
In a similar way, the fwupd plugin downloads the tiny LVFS metadata with
<code>refresh_metadata_async()</code> and then downloads the large firmware
files themselves when <code>update_apps_async()</code> is called.
</para>
<para>
Note, if the downloading fails it's okay to return <code>FALSE</code>;
the plugin loader continues to run all plugins and just logs an error
to the console. We'll be calling into <code>refresh_metadata_async()</code>
again in only another hour, so there's no need to bother the user.
For actions like <code>gs_plugin_app_install</code> we also do the same
thing, but we also save the error on the GsApp itself so that the UI is
free to handle that how it wants, for instance showing a GtkDialog
window for example.
</para>
<example>
<title>Refresh example</title>
<programlisting>
static void progress_cb (gsize bytes_downloaded,
gsize total_download_size,
gpointer user_data);
static void download_file_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data);
static void
gs_plugin_example_refresh_metadata_async (GsPlugin *plugin,
guint64 cache_age_secs,
GsPluginRefreshMetadataFlags flags,
GCancellable *cancellable,
GError **error)
{
const gchar *metadata_filename = "/var/cache/example/metadata.xml";
const gchar *metadata_url = "https://www.example.com/new.xml";
g_autoptr(GFile) file = g_file_new_for_path (metadata_filename);
g_autoptr(GTask) task = NULL;
g_autoptr(SoupSession) soup_session = NULL;
task = g_task_new (plugin, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_example_refresh_metadata_async);
soup_session = gs_build_soup_session ();
/* is the metadata missing or too old? */
if (gs_utils_get_file_age (file) > cache_age_secs) {
gs_download_file_async (soup_session,
metadata_url,
file,
G_PRIORITY_LOW,
progress_cb,
plugin,
cancellable,
download_file_cb,
g_steal_pointer (&task));
return;
}
g_task_return_boolean (task, TRUE);
}
static void
progress_cb (gsize bytes_downloaded,
gsize total_download_size,
gpointer user_data)
{
g_debug ("Downloaded %zu of %zu bytes", bytes_downloaded, total_download_size);
}
static void
download_file_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
GsPlugin *plugin = GS_PLUGIN (source_object);
g_autoptr(GTask) task = g_steal_pointer (&user_data);
if (!gs_download_file_finish (result, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
} else {
g_debug ("successfully downloaded new metadata");
g_task_return_boolean (task, TRUE);
}
}
static gboolean
gs_plugin_example_refresh_metadata_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
</programlisting>
</example>
</section>
<section>
<title>Adding Application Information Using Refine</title>
<para>
As previous examples have shown it's very easy to add a new
application to the search results, updates list or installed list.
Some plugins don't want to add more applications, but want to modify
existing applications to add more information depending on what is
required by the UI code.
The reason we don't just add everything at once is that for
search-as-you-type to work effectively we need to return results in
less than about 50ms and querying some data can take a long time.
For example, it might take a few hundred ms to work out the download
size for an application when a plugin has to also look at what
dependencies are already installed.
We only need this information once the user has clicked the search
results and when the user is in the details panel, so we can save a
ton of time not working out properties that are not useful.
</para>
<example>
<title>Refine example</title>
<programlisting>
static void
gs_plugin_example_refine_async (GsPlugin *plugin,
GsAppList *list,
GsPluginRefineFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_autoptr(GTask) task = NULL;
task = g_task_new (plugin, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_example_refine_async);
/* not required */
if ((flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) == 0) {
g_task_return_boolean (task, TRUE);
return;
}
for (guint i = 0; i < gs_app_list_length (list); i++) {
GsApp *app = gs_app_list_index (list, i);
/* already set */
if (gs_app_get_license (app) != NULL) {
g_task_return_boolean (task, TRUE);
return;
}
/* FIXME, not just hardcoded! */
if (g_strcmp0 (gs_app_get_id (app, "chiron.desktop") == 0))
gs_app_set_license (app, "GPL-2.0 and LGPL-2.1-or-later");
}
g_task_return_boolean (task, TRUE);
}
static gboolean
gs_plugin_example_refine_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
</programlisting>
</example>
<para>
This is a simple example, but shows what a plugin needs to do.
It first checks if the action is required, in this case
<code>GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE</code>.
This request is more common than you might expect as even the search
results shows a non-free label if the license is unspecified or
non-free.
It then checks if the license is already set, returning with success
if so.
If not, it checks the application ID and hardcodes a license; in the
real world this would be querying a database or parsing an additional
config file.
As mentioned before, if the license value is freely available without
any extra work then it's best just to set this at the same time as
when adding the app with <code>gs_app_list_add()</code>.
Think of refine as <emphasis>adding things that cost time to
calculate only when really required</emphasis>.
</para>
<para>
The UI in gnome-software is quite forgiving for missing data, hiding
sections or labels as required.
Some things are required however, and forgetting to assign an icon or
short description will get the application vetoed so that it's not
displayed at all.
Helpfully, running <code>gnome-software --verbose</code> on the
command line will tell you why an application isn't shown along with
any extra data.
</para>
</section>
<section>
<title>Adopting AppStream Applications</title>
<para>
There's a lot of flexibility in the gnome-software plugin structure;
a plugin can add custom applications and handle things like search and
icon loading in a totally custom way.
Most of the time you don't care about how search is implemented or how
icons are going to be loaded, and you can re-use a lot of the existing
code in the <code>appstream</code> plugin.
To do this you just save an AppStream-format XML file in either
<filename>/usr/share/swcatalog/xml/</filename>,
<filename>/var/cache/swcatalog/xml/</filename> or
<filename>~/.local/share/swcatalog/xml/</filename>.
GNOME Software will immediately notice any new files, or changes to
existing files as it has set up the various inotify watches.
</para>
<para>
This allows plugins to care a lot less about how applications are
going to be shown.
For example, the <code>flatpak</code> plugin downloads AppStream data
for configured remotes during <code>refresh_metadata_async()</code>.
</para>
<para>
The only extra step a plugin providing its own apps needs to do
is to implement the <code>GsPluginClass.adopt_app()</code> virtual function.
This is called when an application does not have a management plugin
set, and allows the plugin to <emphasis>claim</emphasis> the
application for itself so it can handle installation, removal and
updating.
</para>
<para>
Another good example is the <code>fwupd</code> that wants to handle
any firmware we've discovered in the AppStream XML.
This might be shipped by the vendor in a package using Satellite,
or downloaded from the LVFS. It wouldn't be kind to set a management
plugin explicitly in case XFCE or KDE want to handle this in a
different way. This adoption function in this case is trivial:
</para>
<informalexample>
<programlisting>
void
gs_plugin_sample_adopt_app (GsPlugin *plugin, GsApp *app)
{
if (gs_app_get_kind (app) == AS_COMPONENT_KIND_FIRMWARE)
gs_app_set_management_plugin (app, plugin);
}
static void
gs_plugin_sample_class_init (GsPluginSampleClass *klass)
{
GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
plugin_class->adopt_app = gs_plugin_sample_adopt_app;
}
</programlisting>
</informalexample>
</section>
<section>
<title>Using The Plugin Cache</title>
<para>
GNOME Software used to provide a per-process plugin cache,
automatically de-duplicating applications and trying to be smarter
than the plugins themselves.
This involved merging applications created by different plugins and
really didn't work very well.
For versions 3.20 and later we moved to a per-plugin cache which
allows the plugin to control getting and adding applications to the
cache and invalidating it when it made sense.
This seems to work a lot better and is an order of magnitude less
complicated.
Plugins can trivially be ported to using the cache using something
like this:
</para>
<informalexample>
<programlisting>
/* create new object */
id = gs_plugin_flatpak_build_id (inst, xref);
- app = gs_app_new (id);
+ app = gs_plugin_cache_lookup (plugin, id);
+ if (app == NULL) {
+ app = gs_app_new (id);
+ gs_plugin_cache_add (plugin, id, app);
+ }
</programlisting>
</informalexample>
<para>
Using the cache has two main benefits for plugins.
The first is that we avoid creating duplicate GsApp objects for the
same logical thing.
This means we can query the installed list, start installing an
application, then query it again before the install has finished.
The GsApp returned from the second <code>list_apps()</code>
request will be the same GObject, and thus all the signals connecting
up to the UI will still be correct.
This means we don't have to care about <emphasis>migrating</emphasis>
the UI widgets as the object changes and things like progress bars just
magically work.
</para>
<para>
The other benefit is more obvious.
If we know the application state from a previous request we don't have
to query a daemon or do another blocking library call to get it.
This does of course imply that the plugin is properly invalidating
the cache using <code>gs_plugin_cache_invalidate()</code> which it
should do whenever a change is detected.
Whether a plugin uses the cache for this reason is up to the plugin,
but if it does it is up to the plugin to make sure the cache doesn't
get out of sync.
</para>
</section>
</partintro>
</reference>
<reference id="api">
<partintro>
<para>
This documentation is auto-generated.
If you see any issues, please file bugs.
</para>
</partintro>
<title>GNOME Software Plugin API</title>
<xi:include href="xml/gs-app.xml"/>
<xi:include href="xml/gs-app-collation.xml"/>
<xi:include href="xml/gs-app-list.xml"/>
<xi:include href="xml/gs-app-query.xml"/>
<xi:include href="xml/gs-appstream.xml"/>
<xi:include href="xml/gs-category.xml"/>
<xi:include href="xml/gs-category-manager.xml"/>
<xi:include href="xml/gs-debug.xml"/>
<xi:include href="xml/gs-desktop-data.xml"/>
<xi:include href="xml/gs-download-utils.xml"/>
<xi:include href="xml/gs-external-appstream-utils.xml"/>
<xi:include href="xml/gs-fedora-third-party.xml"/>
<xi:include href="xml/gs-icon.xml"/>
<xi:include href="xml/gs-ioprio.xml"/>
<xi:include href="xml/gs-key-colors.xml"/>
<xi:include href="xml/gs-metered.xml"/>
<xi:include href="xml/gs-odrs-provider.xml"/>
<xi:include href="xml/gs-os-release.xml"/>
<xi:include href="xml/gs-plugin.xml"/>
<xi:include href="xml/gs-plugin-event.xml"/>
<xi:include href="xml/gs-plugin-helpers.xml"/>
<xi:include href="xml/gs-plugin-job-list-apps.xml"/>
<xi:include href="xml/gs-plugin-job-list-categories.xml"/>
<xi:include href="xml/gs-plugin-job-list-distro-upgrades.xml"/>
<xi:include href="xml/gs-plugin-job-refine.xml"/>
<xi:include href="xml/gs-plugin-job-refresh-metadata.xml"/>
<xi:include href="xml/gs-plugin-job-install-apps.xml"/>
<xi:include href="xml/gs-plugin-job-uninstall-apps.xml"/>
<xi:include href="xml/gs-plugin-job-update-apps.xml"/>
<xi:include href="xml/gs-plugin-job.xml"/>
<xi:include href="xml/gs-plugin-loader-sync.xml"/>
<xi:include href="xml/gs-plugin-loader.xml"/>
<xi:include href="xml/gs-plugin-types.xml"/>
<xi:include href="xml/gs-plugin-vfuncs.xml"/>
<xi:include href="xml/gs-remote-icon.xml"/>
<xi:include href="xml/gs-test.xml"/>
<xi:include href="xml/gs-worker-thread.xml"/>
<xi:include href="xml/gs-utils.xml"/>
</reference>
</book>
|