/* GStreamer base utils library plugin install support for applications * Copyright (C) 2007 Tim-Philipp Müller * Copyright (C) 2006 Ryan Lortie * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library 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. */ /** * SECTION:gstpbutilsinstallplugins * @title: Install-plugins * @short_description: Missing plugin installation support for applications * * ## Overview * * Using this API, applications can request the installation of missing * GStreamer plugins. These may be missing decoders/demuxers or * encoders/muxers for a certain format, sources or sinks for a certain URI * protocol (e.g. 'http'), or certain elements known by their element * factory name ('audioresample'). * * Whether plugin installation is supported or not depends on the operating * system and/or distribution in question. The vendor of the operating * system needs to make sure the necessary hooks and mechanisms are in * place for plugin installation to work. See below for more detailed * information. * * From the application perspective, plugin installation is usually * triggered either * * - when the application itself has found that it wants or needs to * install a certain element * - when the application has been notified by an element (such as * playbin or decodebin) that one or more plugins are missing *and* the * application has decided that it wants to install one or more of * those missing plugins * * The install functions in this section all take one or more 'detail * strings'. These detail strings contain information about the type of * plugin that needs to be installed (decoder, encoder, source, sink, or * named element), and some additional information such GStreamer version * used and a human-readable description of the component to install for * user dialogs. * * Applications should not concern themselves with the composition of the * string itself. They should regard the string as if it was a shared * secret between GStreamer and the plugin installer application. * * Detail strings can be obtained using the function * gst_missing_plugin_message_get_installer_detail() on a * missing-plugin message. Such a message will either have been found by * the application on a pipeline's #GstBus, or the application will have * created it itself using gst_missing_element_message_new(), * gst_missing_decoder_message_new(), * gst_missing_encoder_message_new(), * gst_missing_uri_sink_message_new(), or * gst_missing_uri_source_message_new(). * * For each GStreamer element/plugin/component that should be installed, * the application needs one of those 'installer detail' string mentioned * in the previous section. This string can be obtained, as already * mentioned above, from a missing-plugin message using the function * gst_missing_plugin_message_get_installer_detail(). The * missing-plugin message is either posted by another element and then * found on the bus by the application, or the application has created it * itself as described above. * * The application will then call gst_install_plugins_async(), passing a * NULL-terminated array of installer detail strings, and a function that * should be called when the installation of the plugins has finished * (successfully or not). Optionally, a #GstInstallPluginsContext created * with gst_install_plugins_context_new() may be passed as well. This * way additional optional arguments like the application window's XID can * be passed to the external installer application. * * gst_install_plugins_async() will return almost immediately, with the * return code indicating whether plugin installation was started or not. * If the necessary hooks for plugin installation are in place and an * external installer application has in fact been called, the passed in * function will be called with a result code as soon as the external * installer has finished. If the result code indicates that new plugins * have been installed, the application will want to call * gst_update_registry() so the run-time plugin registry is updated and * the new plugins are made available to the application. * * > A Gtk/GLib main loop must be running in order for the result function * > to be called when the external installer has finished. If this is not * > the case, make sure to regularly call in your code: * > * > g_main_context_iteration (NULL,FALSE); * * ## 1. Installer hook * * When GStreamer applications initiate plugin installation via * gst_install_plugins_async() or gst_install_plugins_sync(), a * pre-defined helper application will be called. * * The exact path of the helper application to be called is set at compile * time, usually by the `./configure` script based on the install prefix. * For a normal package build into the `/usr` prefix, this will usually * default to `/usr/libexec/gst-install-plugins-helper` or * `/usr/lib/gst-install-plugins-helper`. * * Vendors/distros who want to support GStreamer plugin installation should * either provide such a helper script/application or use the `./configure` * option `--with-install-plugins-helper=/path/to/installer` to make * GStreamer call an installer of their own directly. * * It is strongly recommended that vendors provide a small helper * application as interlocutor to the real installer though, even more so * if command line argument munging is required to transform the command * line arguments passed by GStreamer to the helper application into * arguments that are understood by the real installer. * * The helper application path defined at compile time can be overridden at * runtime by setting the GST_INSTALL_PLUGINS_HELPER environment * variable. This can be useful for testing/debugging purposes. * * ## 2. Arguments passed to the install helper * * GStreamer will pass the following arguments to the install helper (this * is in addition to the path of the executable itself, which is by * convention argv[0]): * * - none to many optional arguments in the form of `--foo-bar=val`. * Example: `--transient-for=XID` where XID is the X Window ID of the * main window of the calling application (so the installer can make * itself transient to that window). Unknown optional arguments should * be ignored by the installer. * * - one 'installer detail string' argument for each plugin to be * installed; these strings will have a `gstreamer` prefix; the exact * format of the detail string is explained below * * ## 3. Detail string describing the missing plugin * * The string is in UTF-8 encoding and is made up of several fields, * separated by '|' characters (but neither the first nor the last * character is a '|'). The fields are: * * - plugin system identifier, ie. "gstreamer" * This identifier determines the format of the rest of the detail * string. Automatic plugin installers should not process detail * strings with unknown identifiers. This allows other plugin-based * libraries to use the same mechanism for their automatic plugin * installation needs, or for the format to be changed should it turn * out to be insufficient. * - plugin system version, e.g. "1.0" * This is required so that when there is GStreamer-2.0 at some point * in future, the different major versions can still co-exist and use * the same plugin install mechanism in the same way. * - application identifier, e.g. "totem" * This may also be in the form of "pid/12345" if the program name * can't be obtained for some reason. * - human-readable localised description of the required component, e.g. * "Vorbis audio decoder" * - identifier string for the required component (see below for details * about how to map this to the package/plugin that needs installing), * e.g. * - urisource-$(PROTOCOL_REQUIRED), e.g. urisource-http or * urisource-mms * - element-$(ELEMENT_REQUIRED), e.g. element-videoconvert * - decoder-$(CAPS_REQUIRED), e.g. (do read below for more * details!): * - decoder-audio/x-vorbis * - decoder-application/ogg * - decoder-audio/mpeg, mpegversion=(int)4 * - decoder-video/mpeg, systemstream=(boolean)true, * mpegversion=(int)2 * - encoder-$(CAPS_REQUIRED), e.g. encoder-audio/x-vorbis * - optional further fields not yet specified * * An entire ID string might then look like this, for example: ` * gstreamer|1.0|totem|Vorbis audio decoder|decoder-audio/x-vorbis` * * Plugin installers parsing this ID string should expect further fields * also separated by '|' symbols and either ignore them, warn the user, or * error out when encountering them. * * Those unfamiliar with the GStreamer 'caps' system should note a few * things about the caps string used in the above decoder/encoder case: * * - the first part ("video/mpeg") of the caps string is a GStreamer * media type and *not* a MIME type. Wherever possible, the GStreamer * media type will be the same as the corresponding MIME type, but * often it is not. * - a caps string may or may not have additional comma-separated fields * of various types (as seen in the examples above) * - the caps string of a 'required' component (as above) will always * have fields with fixed values, whereas an introspected string (see * below) may have fields with non-fixed values. Compare for example: * - `audio/mpeg, mpegversion=(int)4` vs. * `audio/mpeg, mpegversion=(int){2, 4}` * - `video/mpeg, mpegversion=(int)2` vs. * `video/mpeg, systemstream=(boolean){ true, false}, mpegversion=(int)[1, 2]` * * ## 4. Exit codes the installer should return * * The installer should return one of the following exit codes when it * exits: * * - 0 if all of the requested plugins could be installed * (#GST_INSTALL_PLUGINS_SUCCESS) * - 1 if no appropriate installation candidate for any of the requested * plugins could be found. Only return this if nothing has been * installed (#GST_INSTALL_PLUGINS_NOT_FOUND) * - 2 if an error occurred during the installation. The application will * assume that the user will already have seen an error message by the * installer in this case and will usually not show another one * (#GST_INSTALL_PLUGINS_ERROR) * - 3 if some of the requested plugins could be installed, but not all * (#GST_INSTALL_PLUGINS_PARTIAL_SUCCESS) * - 4 if the user aborted the installation * (#GST_INSTALL_PLUGINS_USER_ABORT) * * ## 5. How to map the required detail string to packages * * It is up to the vendor to find mechanism to map required components from * the detail string to the actual packages/plugins to install. This could * be a hardcoded list of mappings, for example, or be part of the * packaging system metadata. * * GStreamer plugin files can be introspected for this information. The * `gst-inspect` utility has a special command line option that will output * information similar to what is required. For example ` * $ gst-inspect-1.0 --print-plugin-auto-install-info /path/to/libgstvorbis.so * should output something along the lines of * `decoder-audio/x-vorbis`, `element-vorbisdec` `element-vorbisenc` * `element-vorbisparse`, `element-vorbistag`, `encoder-audio/x-vorbis` * * Note that in the encoder and decoder case the introspected caps can be * more complex with additional fields, e.g. * `audio/mpeg,mpegversion=(int){2,4}`, so they will not always exactly * match the caps wanted by the application. It is up to the installer to * deal with this (either by doing proper caps intersection using the * GStreamer #GstCaps API, or by only taking into account the media type). * * Another potential source of problems are plugins such as ladspa or * libvisual where the list of elements depends on the installed * ladspa/libvisual plugins at the time. This is also up to the * distribution to handle (but usually not relevant for playback * applications). */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "install-plugins.h" #include #ifdef HAVE_SYS_TYPES_H #include #endif #ifdef HAVE_SYS_WAIT_H #include #endif #include /* best effort to make things compile and possibly even work on win32 */ #ifndef WEXITSTATUS # define WEXITSTATUS(status) ((((guint)(status)) & 0xff00) >> 8) #endif #ifndef WIFEXITED # define WIFEXITED(status) ((((guint)(status)) & 0x7f) == 0) #endif static gboolean install_in_progress; /* FALSE */ /* private struct */ struct _GstInstallPluginsContext { gchar *confirm_search; gchar *desktop_id; gchar *startup_notification_id; guint xid; }; /** * gst_install_plugins_context_set_confirm_search: * @ctx: a #GstInstallPluginsContext * @confirm_search: whether to ask for confirmation before searching for plugins * * This function is used to tell the external installer process whether it * should ask for confirmation or not before searching for missing plugins. * * If set, this option will be passed to the installer via a * --interaction=[show-confirm-search|hide-confirm-search] command line option. * * Since: 1.6 */ void gst_install_plugins_context_set_confirm_search (GstInstallPluginsContext * ctx, gboolean confirm_search) { g_return_if_fail (ctx != NULL); if (confirm_search) ctx->confirm_search = g_strdup ("show-confirm-search"); else ctx->confirm_search = g_strdup ("hide-confirm-search"); } /** * gst_install_plugins_context_set_desktop_id: * @ctx: a #GstInstallPluginsContext * @desktop_id: the desktop file ID of the calling application * * This function is used to pass the calling application's desktop file ID to * the external installer process. * * A desktop file ID is the basename of the desktop file, including the * .desktop extension. * * If set, the desktop file ID will be passed to the installer via a * --desktop-id= command line option. * * Since: 1.6 */ void gst_install_plugins_context_set_desktop_id (GstInstallPluginsContext * ctx, const gchar * desktop_id) { g_return_if_fail (ctx != NULL); ctx->desktop_id = g_strdup (desktop_id); } /** * gst_install_plugins_context_set_startup_notification_id: * @ctx: a #GstInstallPluginsContext * @startup_id: the startup notification ID * * Sets the startup notification ID for the launched process. * * This is typically used to to pass the current X11 event timestamp to the * external installer process. * * Startup notification IDs are defined in the * [FreeDesktop.Org Startup Notifications standard](http://standards.freedesktop.org/startup-notification-spec/startup-notification-latest.txt). * * If set, the ID will be passed to the installer via a * --startup-notification-id= command line option. * * GTK+/GNOME applications should be able to create a startup notification ID * like this: * |[ * timestamp = gtk_get_current_event_time (); * startup_id = g_strdup_printf ("_TIME%u", timestamp); * ... * ]| * * Since: 1.6 */ void gst_install_plugins_context_set_startup_notification_id (GstInstallPluginsContext * ctx, const gchar * startup_id) { g_return_if_fail (ctx != NULL); ctx->startup_notification_id = g_strdup (startup_id); } /** * gst_install_plugins_context_set_xid: * @ctx: a #GstInstallPluginsContext * @xid: the XWindow ID (XID) of the top-level application * * This function is for X11-based applications (such as most Gtk/Qt * applications on linux/unix) only. You can use it to tell the external * installer the XID of your main application window. That way the installer * can make its own window transient to your application window during the * installation. * * If set, the XID will be passed to the installer via a --transient-for=XID * command line option. * * Gtk+/Gnome application should be able to obtain the XID of the top-level * window like this: * |[ * ##include <gtk/gtk.h> * ##ifdef GDK_WINDOWING_X11 * ##include <gdk/gdkx.h> * ##endif * ... * ##ifdef GDK_WINDOWING_X11 * xid = GDK_WINDOW_XWINDOW (GTK_WIDGET (application_window)->window); * ##endif * ... * ]| * */ void gst_install_plugins_context_set_xid (GstInstallPluginsContext * ctx, guint xid) { g_return_if_fail (ctx != NULL); ctx->xid = xid; } /** * gst_install_plugins_context_new: * * Creates a new #GstInstallPluginsContext. * * Returns: a new #GstInstallPluginsContext. Free with * gst_install_plugins_context_free() when no longer needed */ GstInstallPluginsContext * gst_install_plugins_context_new (void) { return g_new0 (GstInstallPluginsContext, 1); } /** * gst_install_plugins_context_free: * @ctx: a #GstInstallPluginsContext * * Frees a #GstInstallPluginsContext. */ void gst_install_plugins_context_free (GstInstallPluginsContext * ctx) { g_return_if_fail (ctx != NULL); g_free (ctx->confirm_search); g_free (ctx->desktop_id); g_free (ctx->startup_notification_id); g_free (ctx); } GstInstallPluginsContext * gst_install_plugins_context_copy (GstInstallPluginsContext * ctx) { GstInstallPluginsContext *ret; ret = gst_install_plugins_context_new (); ret->confirm_search = g_strdup (ctx->confirm_search); ret->desktop_id = g_strdup (ctx->desktop_id); ret->startup_notification_id = g_strdup (ctx->startup_notification_id); ret->xid = ctx->xid; return ret; } G_DEFINE_BOXED_TYPE (GstInstallPluginsContext, gst_install_plugins_context, (GBoxedCopyFunc) gst_install_plugins_context_copy, (GBoxedFreeFunc) gst_install_plugins_context_free); static const gchar * gst_install_plugins_get_helper (void) { const gchar *helper; helper = g_getenv ("GST_INSTALL_PLUGINS_HELPER"); if (helper == NULL) helper = GST_INSTALL_PLUGINS_HELPER; GST_LOG ("Using plugin install helper '%s'", helper); return helper; } static gboolean ptr_array_contains_string (GPtrArray * arr, const gchar * s) { gint i; for (i = 0; i < arr->len; ++i) { if (strcmp ((const char *) g_ptr_array_index (arr, i), s) == 0) return TRUE; } return FALSE; } static gboolean gst_install_plugins_spawn_child (const gchar * const *details, GstInstallPluginsContext * ctx, GPid * child_pid, gint * exit_status) { GPtrArray *arr; gboolean ret; GError *err = NULL; gchar **argv; arr = g_ptr_array_new_with_free_func (g_free); /* argv[0] = helper path */ g_ptr_array_add (arr, g_strdup (gst_install_plugins_get_helper ())); /* add any additional command line args from the context */ if (ctx != NULL && ctx->confirm_search) { g_ptr_array_add (arr, g_strdup_printf ("--interaction=%s", ctx->confirm_search)); } if (ctx != NULL && ctx->desktop_id != NULL) { g_ptr_array_add (arr, g_strdup_printf ("--desktop-id=%s", ctx->desktop_id)); } if (ctx != NULL && ctx->startup_notification_id != NULL) { g_ptr_array_add (arr, g_strdup_printf ("--startup-notification-id=%s", ctx->startup_notification_id)); } if (ctx != NULL && ctx->xid != 0) { g_ptr_array_add (arr, g_strdup_printf ("--transient-for=%u", ctx->xid)); } /* finally, add the detail strings, but without duplicates */ while (details != NULL && details[0] != NULL) { if (!ptr_array_contains_string (arr, details[0])) g_ptr_array_add (arr, g_strdup (details[0])); ++details; } /* and NULL-terminate */ g_ptr_array_add (arr, NULL); argv = (gchar **) arr->pdata; if (child_pid == NULL && exit_status != NULL) { install_in_progress = TRUE; ret = g_spawn_sync (NULL, argv, NULL, (GSpawnFlags) 0, NULL, NULL, NULL, NULL, exit_status, &err); install_in_progress = FALSE; } else if (child_pid != NULL && exit_status == NULL) { install_in_progress = TRUE; ret = g_spawn_async (NULL, argv, NULL, G_SPAWN_DO_NOT_REAP_CHILD, NULL, NULL, child_pid, &err); } else { g_return_val_if_reached (FALSE); } if (!ret) { GST_ERROR ("Error spawning plugin install helper: %s", err->message); g_error_free (err); } g_ptr_array_unref (arr); return ret; } static GstInstallPluginsReturn gst_install_plugins_return_from_status (gint status) { GstInstallPluginsReturn ret; /* did we exit cleanly? */ if (!WIFEXITED (status)) { ret = GST_INSTALL_PLUGINS_CRASHED; } else { ret = (GstInstallPluginsReturn) WEXITSTATUS (status); /* did the helper return an invalid status code? */ if (((guint) ret) >= GST_INSTALL_PLUGINS_STARTED_OK && ret != GST_INSTALL_PLUGINS_INTERNAL_FAILURE) { ret = GST_INSTALL_PLUGINS_INVALID; } } GST_LOG ("plugin installer exited with status 0x%04x = %s", status, gst_install_plugins_return_get_name (ret)); return ret; } typedef struct { GstInstallPluginsResultFunc func; gpointer user_data; } GstInstallPluginsAsyncHelper; static void gst_install_plugins_installer_exited (GPid pid, gint status, gpointer data) { GstInstallPluginsAsyncHelper *helper; GstInstallPluginsReturn ret; install_in_progress = FALSE; helper = (GstInstallPluginsAsyncHelper *) data; ret = gst_install_plugins_return_from_status (status); GST_LOG ("calling plugin install result function %p", helper->func); helper->func (ret, helper->user_data); g_free (helper); } /** * gst_install_plugins_async: * @details: (array zero-terminated=1) (transfer none): NULL-terminated array * of installer string details (see below) * @ctx: (allow-none): a #GstInstallPluginsContext, or NULL * @func: (scope async): the function to call when the installer program returns * @user_data: (closure): the user data to pass to @func when called, or NULL * * Requests plugin installation without blocking. Once the plugins have been * installed or installation has failed, @func will be called with the result * of the installation and your provided @user_data pointer. * * This function requires a running GLib/Gtk main loop. If you are not * running a GLib/Gtk main loop, make sure to regularly call * g_main_context_iteration(NULL,FALSE). * * The installer strings that make up @detail are typically obtained by * calling gst_missing_plugin_message_get_installer_detail() on missing-plugin * messages that have been caught on a pipeline's bus or created by the * application via the provided API, such as gst_missing_element_message_new(). * * It is possible to request the installation of multiple missing plugins in * one go (as might be required if there is a demuxer for a certain format * installed but no suitable video decoder and no suitable audio decoder). * * Returns: result code whether an external installer could be started */ GstInstallPluginsReturn gst_install_plugins_async (const gchar * const *details, GstInstallPluginsContext * ctx, GstInstallPluginsResultFunc func, gpointer user_data) { GstInstallPluginsAsyncHelper *helper; GPid pid; g_return_val_if_fail (details != NULL, GST_INSTALL_PLUGINS_INTERNAL_FAILURE); g_return_val_if_fail (func != NULL, GST_INSTALL_PLUGINS_INTERNAL_FAILURE); if (install_in_progress) return GST_INSTALL_PLUGINS_INSTALL_IN_PROGRESS; /* if we can't access our helper, don't bother */ if (!g_file_test (gst_install_plugins_get_helper (), G_FILE_TEST_IS_EXECUTABLE)) return GST_INSTALL_PLUGINS_HELPER_MISSING; if (!gst_install_plugins_spawn_child (details, ctx, &pid, NULL)) return GST_INSTALL_PLUGINS_INTERNAL_FAILURE; helper = g_new (GstInstallPluginsAsyncHelper, 1); helper->func = func; helper->user_data = user_data; g_child_watch_add (pid, gst_install_plugins_installer_exited, helper); return GST_INSTALL_PLUGINS_STARTED_OK; } /** * gst_install_plugins_sync: * @details: (array zero-terminated=1) (transfer none): NULL-terminated array * of installer string details * @ctx: (allow-none): a #GstInstallPluginsContext, or NULL * * Requests plugin installation and block until the plugins have been * installed or installation has failed. * * This function should almost never be used, it only exists for cases where * a non-GLib main loop is running and the user wants to run it in a separate * thread and marshal the result back asynchronously into the main thread * using the other non-GLib main loop. You should almost always use * gst_install_plugins_async() instead of this function. * * Returns: the result of the installation. */ GstInstallPluginsReturn gst_install_plugins_sync (const gchar * const *details, GstInstallPluginsContext * ctx) { gint status; g_return_val_if_fail (details != NULL, GST_INSTALL_PLUGINS_INTERNAL_FAILURE); if (install_in_progress) return GST_INSTALL_PLUGINS_INSTALL_IN_PROGRESS; /* if we can't access our helper, don't bother */ if (!g_file_test (gst_install_plugins_get_helper (), G_FILE_TEST_IS_EXECUTABLE)) return GST_INSTALL_PLUGINS_HELPER_MISSING; if (!gst_install_plugins_spawn_child (details, ctx, NULL, &status)) return GST_INSTALL_PLUGINS_INTERNAL_FAILURE; return gst_install_plugins_return_from_status (status); } /** * gst_install_plugins_return_get_name: * @ret: the return status code * * Convenience function to return the descriptive string associated * with a status code. This function returns English strings and * should not be used for user messages. It is here only to assist * in debugging. * * Returns: a descriptive string for the status code in @ret */ const gchar * gst_install_plugins_return_get_name (GstInstallPluginsReturn ret) { switch (ret) { case GST_INSTALL_PLUGINS_SUCCESS: return "success"; case GST_INSTALL_PLUGINS_NOT_FOUND: return "not-found"; case GST_INSTALL_PLUGINS_ERROR: return "install-error"; case GST_INSTALL_PLUGINS_CRASHED: return "installer-exit-unclean"; case GST_INSTALL_PLUGINS_PARTIAL_SUCCESS: return "partial-success"; case GST_INSTALL_PLUGINS_USER_ABORT: return "user-abort"; case GST_INSTALL_PLUGINS_STARTED_OK: return "started-ok"; case GST_INSTALL_PLUGINS_INTERNAL_FAILURE: return "internal-failure"; case GST_INSTALL_PLUGINS_HELPER_MISSING: return "helper-missing"; case GST_INSTALL_PLUGINS_INSTALL_IN_PROGRESS: return "install-in-progress"; case GST_INSTALL_PLUGINS_INVALID: return "invalid"; default: break; } return "(UNKNOWN)"; } /** * gst_install_plugins_installation_in_progress: * * Checks whether plugin installation (initiated by this application only) * is currently in progress. * * Returns: TRUE if plugin installation is in progress, otherwise FALSE */ gboolean gst_install_plugins_installation_in_progress (void) { return install_in_progress; } /** * gst_install_plugins_supported: * * Checks whether plugin installation is likely to be supported by the * current environment. This currently only checks whether the helper script * that is to be provided by the distribution or operating system vendor * exists. * * Returns: TRUE if plugin installation is likely to be supported. */ gboolean gst_install_plugins_supported (void) { return g_file_test (gst_install_plugins_get_helper (), G_FILE_TEST_IS_EXECUTABLE); }