/* * Copyright © 2019 Red Hat, Inc * * This program 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 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, see . * * Authors: * Matthias Clasen */ #include "config.h" #include #include #include #include #include "call.h" #include "background.h" #include "background-monitor.h" #include "request.h" #include "permissions.h" #include "xdp-dbus.h" #include "xdp-impl-dbus.h" #include "xdp-utils.h" #include "flatpak-instance.h" /* Implementation notes: * * We store a YES/NO/ASK permission for "run in background". * * There is a portal api for apps to request this permission * ahead of time. The portal also lets apps ask for being * autostarted. * * We determine this condition by getting per-application * state from the compositor, and comparing that list to * the list of running flatpak instances obtained from * $XDG_RUNTIME_DIR/.flatpak/. A thread is comparing * this list every minute, and if it finds an app that * is in the background twice, we take actions: * - if the permission is NO, we kill it * - if the permission is YES or ASK, we notify the user * * We only notify once per running instance to not be * annoying. * * Platform-dependent parts are in the background portal * backend: * - Notifying the user * - Getting compositor state * - Enable or disable autostart */ #define PERMISSION_TABLE "background" #define PERMISSION_ID "background" typedef struct _Background Background; typedef struct _BackgroundClass BackgroundClass; struct _Background { XdpDbusBackgroundSkeleton parent_instance; BackgroundMonitor *monitor; }; struct _BackgroundClass { XdpDbusBackgroundSkeletonClass parent_class; }; static XdpDbusImplAccess *access_impl; static XdpDbusImplBackground *background_impl; static Background *background; static GFileMonitor *instance_monitor; GType background_get_type (void) G_GNUC_CONST; static void background_iface_init (XdpDbusBackgroundIface *iface); G_DEFINE_TYPE_WITH_CODE (Background, background, XDP_DBUS_TYPE_BACKGROUND_SKELETON, G_IMPLEMENT_INTERFACE (XDP_DBUS_TYPE_BACKGROUND, background_iface_init)); typedef enum { BACKGROUND = 0, RUNNING = 1, ACTIVE = 2, } AppState; typedef enum { FORBID = 0, ALLOW = 1, IGNORE = 2 } NotifyResult; typedef enum { AUTOSTART_FLAGS_NONE = 0, AUTOSTART_FLAGS_ACTIVATABLE = 1 << 0, } AutostartFlags; static GVariant * get_all_permissions (void) { g_autoptr(GError) error = NULL; g_autoptr(GVariant) out_perms = NULL; g_autoptr(GVariant) out_data = NULL; if (!xdp_dbus_impl_permission_store_call_lookup_sync (get_permission_store (), PERMISSION_TABLE, PERMISSION_ID, &out_perms, &out_data, NULL, &error)) { g_dbus_error_strip_remote_error (error); g_debug ("No background permissions found: %s", error->message); return NULL; } return g_steal_pointer (&out_perms); } static Permission get_one_permission (const char *app_id, GVariant *perms) { const char **permissions; if (perms == NULL) { g_debug ("No background permissions found"); return PERMISSION_UNSET; } else if (!g_variant_lookup (perms, app_id, "^a&s", &permissions)) { g_debug ("No background permissions stored for: app %s", app_id); return PERMISSION_UNSET; } else if (g_strv_length ((char **)permissions) != 1) { g_autofree char *a = g_strjoinv (" ", (char **)permissions); g_warning ("Wrong background permission format, ignoring (%s)", a); return PERMISSION_UNSET; } g_debug ("permission store: background, app %s -> %s", app_id, permissions[0]); if (strcmp (permissions[0], "yes") == 0) return PERMISSION_YES; else if (strcmp (permissions[0], "no") == 0) return PERMISSION_NO; else if (strcmp (permissions[0], "ask") == 0) return PERMISSION_ASK; else { g_autofree char *a = g_strjoinv (" ", (char **)permissions); g_warning ("Wrong permission format, ignoring (%s)", a); } return PERMISSION_UNSET; } static Permission get_permission (const char *app_id) { g_autoptr(GVariant) perms = NULL; perms = get_all_permissions (); if (perms) return get_one_permission (app_id, perms); return PERMISSION_UNSET; } static void set_permission (const char *app_id, Permission permission) { g_autoptr(GError) error = NULL; const char *permissions[2]; if (permission == PERMISSION_ASK) permissions[0] = "ask"; else if (permission == PERMISSION_YES) permissions[0] = "yes"; else if (permission == PERMISSION_NO) permissions[0] = "no"; else { g_warning ("Wrong permission format, ignoring"); return; } permissions[1] = NULL; if (!xdp_dbus_impl_permission_store_call_set_permission_sync (get_permission_store (), PERMISSION_TABLE, TRUE, PERMISSION_ID, app_id, (const char * const*)permissions, NULL, &error)) { g_dbus_error_strip_remote_error (error); g_warning ("Error updating permission store: %s", error->message); } } /* background monitor */ /* The background monitor is running in a dedicated thread. * * We rely on the RunningApplicationsChanged signal from the backend to get * notified about applications that start or stop having open windows, and on * file monitoring to learn about flatpak instances appearing and disappearing. * * When either of these changes happens, we wake up the background monitor * thread, and it will check the state of applications a few times, with a * few seconds of wait in between. When we find an application in the background * more than once, we check the permissions, and kill or notify if warranted. * * We require an application to be in background state for more than once check * to avoid killing an unlucky application that just happened to start up as we * did our check. */ static GHashTable * get_app_states (void) { g_autoptr(GVariant) apps = NULL; g_autoptr(GHashTable) app_states = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); const char *appid; GVariant *value; g_autoptr(GError) error = NULL; if (!xdp_dbus_impl_background_call_get_app_state_sync (background_impl, &apps, NULL, &error)) { static int warned = 0; if (!warned) { g_warning ("Failed to get application states: %s", error->message); warned = 1; } return NULL; } g_autoptr(GVariantIter) iter = g_variant_iter_new (apps); while (g_variant_iter_loop (iter, "{&sv}", &appid, &value)) { AppState state = g_variant_get_uint32 (value); g_hash_table_insert (app_states, g_strdup (appid), GINT_TO_POINTER (state)); } return g_steal_pointer (&app_states); } static AppState get_one_app_state (const char *app_id, GHashTable *app_states) { return (AppState)GPOINTER_TO_INT (g_hash_table_lookup (app_states, app_id)); } typedef struct { FlatpakInstance *instance; int stamp; AppState state; char *handle; gboolean notified; Permission permission; char *status_message; } InstanceData; static void instance_data_free (gpointer data) { InstanceData *idata = data; g_object_unref (idata->instance); g_free (idata->status_message); g_free (idata->handle); g_free (idata); } /* instance ID -> InstanceData */ static GHashTable *applications; G_LOCK_DEFINE (applications); static void close_notification (const char *handle) { g_dbus_connection_call (g_dbus_proxy_get_connection (G_DBUS_PROXY (background_impl)), g_dbus_proxy_get_name (G_DBUS_PROXY (background_impl)), handle, "org.freedesktop.impl.portal.Request", "Close", NULL, NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL, NULL); } static void remove_outdated_instances (int stamp) { GHashTableIter iter; char *id; InstanceData *data; g_autoptr(GPtrArray) handles = NULL; int i; handles = g_ptr_array_new_with_free_func (g_free); G_LOCK (applications); g_hash_table_iter_init (&iter, applications); while (g_hash_table_iter_next (&iter, (gpointer *)&id, (gpointer *)&data)) { if (data->stamp < stamp) { if (data->handle) g_ptr_array_add (handles, g_strdup (data->handle)); g_hash_table_iter_remove (&iter); } } G_UNLOCK (applications); for (i = 0; i < handles->len; i++) { char *handle = g_ptr_array_index (handles, i); close_notification (handle); } } static void update_background_monitor_properties (void) { GVariantBuilder builder; GHashTableIter iter; InstanceData *data; char *id; g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}")); G_LOCK (applications); g_hash_table_iter_init (&iter, applications); while (g_hash_table_iter_next (&iter, (gpointer *)&id, (gpointer *)&data)) { GVariantBuilder app_builder; const char *app_id; const char *id; if (data->state != BACKGROUND) continue; if (!flatpak_instance_is_running (data->instance)) continue; id = flatpak_instance_get_id (data->instance); app_id = flatpak_instance_get_app (data->instance); g_assert (app_id != NULL); g_variant_builder_init (&app_builder, G_VARIANT_TYPE_VARDICT); g_variant_builder_add (&app_builder, "{sv}", "app_id", g_variant_new_string (app_id)); g_variant_builder_add (&app_builder, "{sv}", "instance", g_variant_new_string (id)); if (data->status_message) g_variant_builder_add (&app_builder, "{sv}", "message", g_variant_new_string (data->status_message)); g_variant_builder_add_value (&builder, g_variant_builder_end (&app_builder)); } G_UNLOCK (applications); xdp_dbus_background_monitor_set_background_apps (XDP_DBUS_BACKGROUND_MONITOR (background->monitor), g_variant_builder_end (&builder)); } static char * flatpak_instance_get_display_name (FlatpakInstance *instance) { const char *app_id = flatpak_instance_get_app (instance); if (app_id[0] != 0) { g_autofree char *desktop_id = NULL; g_autoptr(GAppInfo) info = NULL; desktop_id = g_strconcat (app_id, ".desktop", NULL); info = (GAppInfo*)g_desktop_app_info_new (desktop_id); if (info) return g_strdup (g_app_info_get_display_name (info)); } return g_strdup (app_id); } typedef struct { char *handle; char *app_id; char *id; char *name; Permission perm; pid_t child_pid; } NotificationData; static void notification_data_free (gpointer data) { NotificationData *nd = data; g_free (nd->handle); g_free (nd->app_id); g_free (nd->id); g_free (nd->name); g_free (nd); } static void notify_background_done (GObject *source, GAsyncResult *res, gpointer data) { NotificationData *nd = (NotificationData *)data; g_autoptr(GError) error = NULL; g_autoptr(GVariant) results = NULL; guint response; guint result; InstanceData *idata; if (!xdp_dbus_impl_background_call_notify_background_finish (background_impl, &response, &results, res, &error)) { g_dbus_error_strip_remote_error (error); g_warning ("Error from background backend: %s", error->message); notification_data_free (nd); return; } if (!g_variant_lookup (results, "result", "u", &result)) result = IGNORE; if (result == ALLOW) { g_debug ("Allowing app %s to run in background", nd->app_id); if (nd->perm != PERMISSION_ASK) nd->perm = PERMISSION_YES; } else if (result == FORBID) { g_debug ("Forbid app %s to run in background", nd->app_id); if (nd->perm != PERMISSION_ASK) nd->perm = PERMISSION_NO; g_message ("Terminating app %s (process %d) because the app does not " "have permission to run in the background. You may be able to " "grant this app the permission to run in background in the " "system settings of your desktop environment.", nd->app_id, nd->child_pid); kill (nd->child_pid, SIGKILL); } else if (result == IGNORE) { g_debug ("Allow this instance of %s to run in background without permission changes", nd->app_id); } else g_debug ("Unexpected response from NotifyBackground: %u", result); if (nd->perm != PERMISSION_UNSET) set_permission (nd->app_id, nd->perm); G_LOCK (applications); idata = g_hash_table_lookup (applications, nd->id); if (idata) { g_clear_pointer (&idata->handle, g_free); idata->permission = nd->perm; } G_UNLOCK (applications); notification_data_free (nd); } static void check_background_apps (void) { g_autoptr(GVariant) perms = NULL; g_autoptr(GHashTable) app_states = NULL; g_autoptr(GPtrArray) instances = NULL; int i; static int stamp; g_autoptr(GPtrArray) notifications = NULL; app_states = get_app_states (); if (app_states == NULL) return; g_debug ("Checking background permissions"); perms = get_all_permissions (); instances = flatpak_instance_get_all (); notifications = g_ptr_array_new (); stamp++; G_LOCK (applications); for (i = 0; i < instances->len; i++) { FlatpakInstance *instance = g_ptr_array_index (instances, i); const char *id; const char *app_id; pid_t child_pid; InstanceData *idata; const char *state_names[] = { "background", "running", "active" }; gboolean is_new = FALSE; if (!flatpak_instance_is_running (instance)) continue; id = flatpak_instance_get_id (instance); app_id = flatpak_instance_get_app (instance); child_pid = flatpak_instance_get_child_pid (instance); idata = g_hash_table_lookup (applications, id); if (!app_id) continue; if (!idata) { is_new = TRUE; idata = g_new0 (InstanceData, 1); idata->instance = g_object_ref (instance); g_hash_table_insert (applications, g_strdup (id), idata); } idata->stamp = stamp; idata->state = get_one_app_state (app_id, app_states); g_debug ("App %s is %s", app_id, state_names[idata->state]); idata->permission = get_one_permission (app_id, perms); /* If the app is not in the list yet, add it, * but don't notify yet - this gives apps some * leeway to get their window up. If it is still * in the background next time around, we'll proceed * to the next step. */ if (idata->state != BACKGROUND || idata->notified || is_new) { if (idata->notified) g_debug ("Already notified app %s ...skipping\n", app_id); if (is_new) g_debug ("App %s is new ...skipping\n", app_id); continue; } switch (idata->permission) { case PERMISSION_NO: idata->stamp = 0; g_debug ("Kill app %s (pid %d)", app_id, child_pid); kill (child_pid, SIGKILL); break; case PERMISSION_ASK: case PERMISSION_UNSET: { NotificationData *nd = g_new0 (NotificationData, 1); if (idata->handle) { close_notification (idata->handle); g_free (idata->handle); } idata->handle = g_strdup_printf ("/org/freedesktop/portal/desktop/notify/background%d", stamp); idata->notified = TRUE; nd->handle = g_strdup (idata->handle); nd->name = flatpak_instance_get_display_name (instance); nd->app_id = g_strdup (app_id); nd->id = g_strdup (id); nd->child_pid = child_pid; nd->perm = idata->permission; g_ptr_array_add (notifications, nd); } break; case PERMISSION_YES: default: break; } } G_UNLOCK (applications); for (i = 0; i < notifications->len; i++) { NotificationData *nd = g_ptr_array_index (notifications, i); g_debug ("Notify background for %s", nd->app_id); xdp_dbus_impl_background_call_notify_background (background_impl, nd->handle, nd->app_id, nd->name, NULL, notify_background_done, nd); } remove_outdated_instances (stamp); update_background_monitor_properties (); } static GMainContext *monitor_context; static gpointer background_monitor (gpointer data) { applications = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, instance_data_free); while (TRUE) { g_main_context_iteration (monitor_context, TRUE); /* We check twice, to avoid killing unlucky apps hit at a bad time */ sleep (5); check_background_apps (); sleep (5); check_background_apps (); } g_clear_pointer (&applications, g_hash_table_unref); g_clear_pointer (&monitor_context, g_main_context_unref); return NULL; } static void start_background_monitor (void) { g_autoptr(GThread) thread = NULL; g_debug ("Starting background app monitor"); monitor_context = g_main_context_new (); thread = g_thread_new ("background monitor", background_monitor, NULL); } static void running_apps_changed (gpointer data) { g_debug ("Running app windows changed, wake up monitor thread"); g_main_context_wakeup (monitor_context); } static void instances_changed (gpointer data) { g_debug ("Running instances changed, wake up monitor thread"); g_main_context_wakeup (monitor_context); } static void handle_request_background_in_thread_func (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { Request *request = (Request *)task_data; GVariant *options; const char *app_id; Permission permission; const char *reason = NULL; gboolean autostart_requested = FALSE; gboolean autostart_enabled; gboolean allowed; g_autoptr(GError) error = NULL; const char * const *autostart_exec = { NULL }; AutostartFlags autostart_flags = AUTOSTART_FLAGS_NONE; gboolean activatable = FALSE; g_auto(GStrv) commandline = NULL; REQUEST_AUTOLOCK (request); options = (GVariant *)g_object_get_data (G_OBJECT (request), "options"); g_variant_lookup (options, "reason", "&s", &reason); g_variant_lookup (options, "autostart", "b", &autostart_requested); g_variant_lookup (options, "commandline", "^a&s", &autostart_exec); g_variant_lookup (options, "dbus-activatable", "b", &activatable); if (activatable) autostart_flags |= AUTOSTART_FLAGS_ACTIVATABLE; app_id = xdp_app_info_get_id (request->app_info); if (xdp_app_info_is_host (request->app_info)) permission = PERMISSION_YES; else permission = get_permission (app_id); g_debug ("Handle RequestBackground for '%s'", app_id); if (permission == PERMISSION_ASK) { GVariantBuilder opt_builder; g_autofree char *title = NULL; g_autofree char *subtitle = NULL; g_autofree char *body = NULL; guint32 response = 2; g_autoptr(GVariant) results = NULL; g_autoptr(GError) error = NULL; g_autoptr(GAppInfo) info = NULL; info = xdp_app_info_load_app_info (request->app_info); title = g_strdup_printf (_("Allow %s to run in the background?"), info ? g_app_info_get_display_name (info) : app_id); if (reason) subtitle = g_strdup (reason); else if (autostart_requested) subtitle = g_strdup_printf (_("%s requests to be started automatically and run in the background."), info ? g_app_info_get_display_name (info) : app_id); else subtitle = g_strdup_printf (_("%s requests to run in the background."), info ? g_app_info_get_display_name (info) : app_id); body = g_strdup (_("The ‘run in background’ permission can be changed at any time from the application settings.")); g_debug ("Calling backend for background access for: %s", app_id); g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); g_variant_builder_add (&opt_builder, "{sv}", "deny_label", g_variant_new_string (_("Don't allow"))); g_variant_builder_add (&opt_builder, "{sv}", "grant_label", g_variant_new_string (_("Allow"))); if (!xdp_dbus_impl_access_call_access_dialog_sync (access_impl, request->id, app_id, "", title, subtitle, body, g_variant_builder_end (&opt_builder), &response, &results, NULL, &error)) { g_warning ("AccessDialog call failed: %s", error->message); g_clear_error (&error); } allowed = response == 0; } else { allowed = permission != PERMISSION_NO; if (permission == PERMISSION_UNSET) set_permission (app_id, PERMISSION_YES); } g_debug ("Setting autostart for %s to %s", app_id, allowed && autostart_requested ? "enabled" : "disabled"); autostart_enabled = FALSE; commandline = xdp_app_info_rewrite_commandline (request->app_info, autostart_exec, FALSE /* don't quote escape */); if (commandline == NULL) { g_debug ("Autostart not supported for: %s", app_id); } else if (!xdp_dbus_impl_background_call_enable_autostart_sync (background_impl, app_id, allowed && autostart_requested, (const char * const *)commandline, autostart_flags, &autostart_enabled, NULL, &error)) { g_warning ("EnableAutostart call failed: %s", error->message); g_clear_error (&error); } if (request->exported) { XdgDesktopPortalResponseEnum portal_response; GVariantBuilder results; g_variant_builder_init (&results, G_VARIANT_TYPE_VARDICT); g_variant_builder_add (&results, "{sv}", "background", g_variant_new_boolean (allowed)); g_variant_builder_add (&results, "{sv}", "autostart", g_variant_new_boolean (autostart_enabled)); if (allowed) portal_response = XDG_DESKTOP_PORTAL_RESPONSE_SUCCESS; else portal_response = XDG_DESKTOP_PORTAL_RESPONSE_CANCELLED; xdp_dbus_request_emit_response (XDP_DBUS_REQUEST (request), portal_response, g_variant_builder_end (&results)); request_unexport (request); } } static gboolean validate_reason (const char *key, GVariant *value, GVariant *options, GError **error) { const char *string = g_variant_get_string (value, NULL); if (g_utf8_strlen (string, -1) > 256) { g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, "Not accepting overly long reasons"); return FALSE; } return TRUE; } static gboolean validate_commandline (const char *key, GVariant *value, GVariant *options, GError **error) { gsize length; const char **strv = g_variant_get_strv (value, &length); if (strv[0] == NULL) { g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, "Commandline can't be empty"); return FALSE; } if (g_utf8_strlen (strv[0], -1) > 256) { g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, "Not accepting overly long commandlines"); return FALSE; } if (length > 100) { g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, "Not accepting overly long commandlines"); return FALSE; } return TRUE; } static XdpOptionKey background_options[] = { { "reason", G_VARIANT_TYPE_STRING, validate_reason }, { "autostart", G_VARIANT_TYPE_BOOLEAN, NULL }, { "commandline", G_VARIANT_TYPE_STRING_ARRAY, validate_commandline }, { "dbus-activatable", G_VARIANT_TYPE_BOOLEAN, NULL }, }; static gboolean handle_request_background (XdpDbusBackground *object, GDBusMethodInvocation *invocation, const char *arg_window, GVariant *arg_options) { Request *request = request_from_invocation (invocation); g_autoptr(GError) error = NULL; g_autoptr(XdpDbusImplRequest) impl_request = NULL; g_autoptr(GTask) task = NULL; GVariantBuilder opt_builder; g_autoptr(GVariant) options = NULL; REQUEST_AUTOLOCK (request); g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); if (!xdp_filter_options (arg_options, &opt_builder, background_options, G_N_ELEMENTS (background_options), &error)) { g_dbus_method_invocation_return_gerror (invocation, error); return G_DBUS_METHOD_INVOCATION_HANDLED; } options = g_variant_ref_sink (g_variant_builder_end (&opt_builder)); g_object_set_data_full (G_OBJECT (request), "window", g_strdup (arg_window), g_free); g_object_set_data_full (G_OBJECT (request), "options", g_variant_ref (options), (GDestroyNotify)g_variant_unref); impl_request = xdp_dbus_impl_request_proxy_new_sync (g_dbus_proxy_get_connection (G_DBUS_PROXY (access_impl)), G_DBUS_PROXY_FLAGS_NONE, g_dbus_proxy_get_name (G_DBUS_PROXY (access_impl)), request->id, NULL, &error); if (!impl_request) { g_dbus_method_invocation_return_gerror (invocation, error); return G_DBUS_METHOD_INVOCATION_HANDLED; } request_set_impl_request (request, impl_request); request_export (request, g_dbus_method_invocation_get_connection (invocation)); xdp_dbus_background_complete_request_background (object, invocation, request->id); task = g_task_new (object, NULL, NULL, NULL); g_task_set_task_data (task, g_object_ref (request), g_object_unref); g_task_run_in_thread (task, handle_request_background_in_thread_func); return G_DBUS_METHOD_INVOCATION_HANDLED; } static void set_status_finished_cb (GObject *source_object, GAsyncResult *result, gpointer user_data) { GDBusMethodInvocation *invocation; g_autoptr(GError) error = NULL; g_assert (g_task_is_valid (result, source_object)); invocation = g_task_get_task_data (G_TASK (result)); g_assert (invocation != NULL); if (g_task_propagate_boolean (G_TASK (result), &error)) { xdp_dbus_background_complete_set_status (XDP_DBUS_BACKGROUND (source_object), invocation); } else { g_dbus_method_invocation_return_gerror (invocation, error); } } static void handle_set_status_in_thread_func (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { GDBusMethodInvocation *invocation = task_data; g_autofree char *message = NULL; InstanceData *data; const char *id = NULL; GVariant *options; Call *call; call = call_from_invocation (invocation); id = xdp_app_info_get_instance (call->app_info); options = g_object_get_data (G_OBJECT (invocation), "options"); g_variant_lookup (options, "message", "s", &message); G_LOCK (applications); data = g_hash_table_lookup (applications, id); if (!data) { g_autoptr(GHashTable) app_states = NULL; g_autoptr(GPtrArray) instances = NULL; FlatpakInstance *instance = NULL; instances = flatpak_instance_get_all (); for (guint i = 0; i < instances->len; i++) { FlatpakInstance *aux = g_ptr_array_index (instances, i); if (g_strcmp0 (id, flatpak_instance_get_id (aux)) == 0) { instance = aux; break; } } if (!instance) { G_UNLOCK (applications); g_task_return_new_error (task, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED, "No sandboxed instance of the application found"); return; } app_states = get_app_states (); if (app_states == NULL) { G_UNLOCK (applications); g_task_return_new_error (task, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED, "Could not fetch app state from backend"); return; } data = g_new0 (InstanceData, 1); data->instance = g_object_ref (instance); data->state = get_one_app_state (xdp_app_info_get_id (call->app_info), app_states); g_hash_table_insert (applications, g_strdup (id), data); } g_assert (data != NULL); g_clear_pointer (&data->status_message, g_free); data->status_message = g_steal_pointer (&message); G_UNLOCK (applications); update_background_monitor_properties (); g_task_return_boolean (task, TRUE); } static gboolean validate_message (const char *key, GVariant *value, GVariant *options, GError **error) { const char *string = g_variant_get_string (value, NULL); if (g_utf8_strlen (string, -1) > 96) { g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, "Status message is longer than 96 characters"); return FALSE; } if (strstr (string, "\n")) { g_set_error (error, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_INVALID_ARGUMENT, "Status message must not have newlines"); return FALSE; } return TRUE; } static XdpOptionKey set_status_options[] = { { "message", G_VARIANT_TYPE_STRING, validate_message }, }; static gboolean handle_set_status (XdpDbusBackground *object, GDBusMethodInvocation *invocation, GVariant *arg_options) { g_autoptr(GVariant) options = NULL; g_autoptr(GError) error = NULL; g_autoptr(GTask) task = NULL; GVariantBuilder opt_builder; const char *id = NULL; Call *call; call = call_from_invocation (invocation); g_debug ("Handling SetStatus call from %s", xdp_app_info_get_id (call->app_info)); if (xdp_app_info_is_host (call->app_info)) { g_dbus_method_invocation_return_error (invocation, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_NOT_ALLOWED, "Only sandboxed applications can set background status"); return G_DBUS_METHOD_INVOCATION_HANDLED; } id = xdp_app_info_get_instance (call->app_info); if (!id) { g_dbus_method_invocation_return_error (invocation, XDG_DESKTOP_PORTAL_ERROR, XDG_DESKTOP_PORTAL_ERROR_FAILED, "No sandboxed instance of the application found"); return G_DBUS_METHOD_INVOCATION_HANDLED; } g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT); if (!xdp_filter_options (arg_options, &opt_builder, set_status_options, G_N_ELEMENTS (set_status_options), &error)) { g_dbus_method_invocation_return_gerror (invocation, error); return G_DBUS_METHOD_INVOCATION_HANDLED; } options = g_variant_ref_sink (g_variant_builder_end (&opt_builder)); g_object_set_data_full (G_OBJECT (invocation), "options", g_steal_pointer (&options), (GDestroyNotify) g_variant_unref); task = g_task_new (object, NULL, set_status_finished_cb, NULL); g_task_set_task_data (task, g_object_ref (invocation), g_object_unref); g_task_run_in_thread (task, handle_set_status_in_thread_func); return G_DBUS_METHOD_INVOCATION_HANDLED; } static void background_iface_init (XdpDbusBackgroundIface *iface) { iface->handle_request_background = handle_request_background; iface->handle_set_status = handle_set_status; } static void background_init (Background *background) { xdp_dbus_background_set_version (XDP_DBUS_BACKGROUND (background), 2); } static void background_class_init (BackgroundClass *klass) { } GDBusInterfaceSkeleton * background_create (GDBusConnection *connection, const char *dbus_name_access, const char *dbus_name_background) { g_autofree char *instance_path = NULL; g_autoptr(GFile) instance_dir = NULL; g_autoptr(GError) error = NULL; access_impl = xdp_dbus_impl_access_proxy_new_sync (connection, G_DBUS_PROXY_FLAGS_NONE, dbus_name_access, DESKTOP_PORTAL_OBJECT_PATH, NULL, &error); if (access_impl == NULL) { g_warning ("Failed to create access proxy: %s", error->message); return NULL; } g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (access_impl), G_MAXINT); background_impl = xdp_dbus_impl_background_proxy_new_sync (connection, G_DBUS_PROXY_FLAGS_NONE, dbus_name_background, DESKTOP_PORTAL_OBJECT_PATH, NULL, &error); if (background_impl == NULL) { g_warning ("Failed to create background proxy: %s", error->message); return NULL; } g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (background_impl), G_MAXINT); background = g_object_new (background_get_type (), NULL); background->monitor = background_monitor_new (NULL, &error); if (background->monitor == NULL) { g_warning ("Failed to create background monitor: %s", error->message); return NULL; } start_background_monitor (); g_signal_connect (background_impl, "running-applications-changed", G_CALLBACK (running_apps_changed), NULL); /* FIXME: it would be better if libflatpak had a monitor api for this */ instance_path = g_build_filename (g_get_user_runtime_dir (), ".flatpak", NULL); instance_dir = g_file_new_for_path (instance_path); instance_monitor = g_file_monitor_directory (instance_dir, G_FILE_MONITOR_NONE, NULL, &error); if (!instance_monitor) g_warning ("Failed to create a monitor for %s: %s", instance_path, error->message); else g_signal_connect (instance_monitor, "changed", G_CALLBACK (instances_changed), NULL); return G_DBUS_INTERFACE_SKELETON (background); }