From b01a7425208531b3ca634dc1cb413f9741facafa Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Mon, 13 May 2019 01:18:13 +0000 Subject: [PATCH] Implement a background monitor Implement a facility that monitors running flatpaks, and notifies the user if they run in the background. Add a permission table to store users responses persistently. The platform-dependent parts of this (getting the state of running applications, and notifying the user) are handled by a portal backend. There is currently no application API. --- data/Makefile.am.inc | 1 + ...org.freedesktop.impl.portal.Background.xml | 79 +++ src/Makefile.am.inc | 5 + src/background-monitor.c | 505 ++++++++++++++++++ src/background-monitor.h | 26 + src/xdg-desktop-portal.c | 5 + 6 files changed, 621 insertions(+) create mode 100644 data/org.freedesktop.impl.portal.Background.xml create mode 100644 src/background-monitor.c create mode 100644 src/background-monitor.h diff --git a/data/Makefile.am.inc b/data/Makefile.am.inc index bfb7fc6a6..1c0c4fa23 100644 --- a/data/Makefile.am.inc +++ b/data/Makefile.am.inc @@ -36,4 +36,5 @@ dist_introspection_DATA = \ data/org.freedesktop.impl.portal.RemoteDesktop.xml \ data/org.freedesktop.impl.portal.Settings.xml \ data/org.freedesktop.impl.portal.Lockdown.xml \ + data/org.freedesktop.impl.portal.Background.xml \ $(NULL) diff --git a/data/org.freedesktop.impl.portal.Background.xml b/data/org.freedesktop.impl.portal.Background.xml new file mode 100644 index 000000000..3ee8bf771 --- /dev/null +++ b/data/org.freedesktop.impl.portal.Background.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Makefile.am.inc b/src/Makefile.am.inc index e05fcf825..beb963d7e 100644 --- a/src/Makefile.am.inc +++ b/src/Makefile.am.inc @@ -55,6 +55,7 @@ PORTAL_IMPL_IFACE_FILES =\ data/org.freedesktop.impl.portal.RemoteDesktop.xml \ data/org.freedesktop.impl.portal.Settings.xml \ data/org.freedesktop.impl.portal.Lockdown.xml \ + data/org.freedesktop.impl.portal.Background.xml \ $(NULL) $(xdp_dbus_built_sources) : $(PORTAL_IFACE_FILES) @@ -150,6 +151,8 @@ xdg_desktop_portal_SOURCES = \ src/fc-monitor.h \ src/background.c \ src/background.h \ + src/background-monitor.c \ + src/background-monitor.h \ $(NULL) if HAVE_PIPEWIRE @@ -174,6 +177,7 @@ xdg_desktop_portal_LDADD = \ $(BASE_LIBS) \ $(PIPEWIRE_LIBS) \ $(GEOCLUE_LIBS) \ + $(FLATPAK_LIBS) \ $(NULL) xdg_desktop_portal_CFLAGS = \ -DPKGDATADIR=\"$(pkgdatadir)\" \ @@ -182,6 +186,7 @@ xdg_desktop_portal_CFLAGS = \ $(BASE_CFLAGS) \ $(PIPEWIRE_CFLAGS) \ $(GEOCLUE_CFLAGS) \ + $(FLATPAK_CFLAGS) \ -I$(srcdir)/src \ -I$(builddir)/src \ $(NULL) diff --git a/src/background-monitor.c b/src/background-monitor.c new file mode 100644 index 000000000..4e54bb33a --- /dev/null +++ b/src/background-monitor.c @@ -0,0 +1,505 @@ +/* + * 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.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, see . + * + * Authors: + * Matthias Clasen + */ + +#include "config.h" + +#include +#include +#include + +#include +#include "background-monitor.h" +#include "permissions.h" +#include "xdp-impl-dbus.h" +#include "xdp-utils.h" + +#define PERMISSION_TABLE "background" +#define PERMISSION_ID "background" + +typedef enum { UNSET, NO, YES, ASK } Permission; + +static XdpImplBackground *background = NULL; + +typedef enum { BACKGROUND, RUNNING, ACTIVE } AppState; + +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_impl_background_call_get_app_state_sync (background, &apps, NULL, &error)) + { + g_warning ("Failed to get application states: %s", error->message); + 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_app_state (const char *app_id, + GHashTable *app_states) +{ + return (AppState)GPOINTER_TO_INT (g_hash_table_lookup (app_states, app_id)); +} + +static GVariant * +get_permissions (void) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) out_perms = NULL; + g_autoptr(GVariant) out_data = NULL; + + if (!xdp_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_permission (const char *app_id, + GVariant *perms) +{ + const char **permissions; + + if (perms == NULL) + { + g_debug ("No background permissions found"); + + return UNSET; + } + else if (!g_variant_lookup (perms, app_id, "^a&s", &permissions)) + { + g_debug ("No background permissions stored for: app %s", app_id); + + return 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 UNSET; + } + g_debug ("permission store: background, app %s -> %s", app_id, permissions[0]); + + if (strcmp (permissions[0], "yes") == 0) + return YES; + else if (strcmp (permissions[0], "no") == 0) + return NO; + else if (strcmp (permissions[0], "ask") == 0) + return ASK; + else + { + g_autofree char *a = g_strjoinv (" ", (char **)permissions); + g_warning ("Wrong permission format, ignoring (%s)", a); + } + + return UNSET; +} + +static void +set_permission (const char *app_id, + Permission permission) +{ + g_autoptr(GError) error = NULL; + const char *permissions[2]; + + if (permission == ASK) + permissions[0] = "ask"; + else if (permission == YES) + permissions[0] = "yes"; + else if (permission == NO) + permissions[0] = "no"; + else + { + g_warning ("Wrong permission format, ignoring"); + return; + } + permissions[1] = NULL; + + if (!xdp_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); + } +} + +static GHashTable *applications; +G_LOCK_DEFINE_STATIC (applications); + +static void +close_notification (const char *handle) +{ + g_dbus_connection_call (g_dbus_proxy_get_connection (G_DBUS_PROXY (background)), + g_dbus_proxy_get_name (G_DBUS_PROXY (background)), + handle, + "org.freedesktop.impl.portal.Request", + "Close", + NULL, + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + NULL, NULL, NULL); +} + +static void +remove_outdated_applications (GPtrArray *apps) +{ + int j; + GHashTableIter iter; + char *app_id; + char *handle; + g_autoptr(GPtrArray) handles = NULL; + + 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 *)&app_id, (gpointer *)&handle)) + { + gboolean found = FALSE; + for (j = 0; j < apps->len && !found; j++) + { + FlatpakInstance *app = g_ptr_array_index (apps, j); + found = g_strcmp0 (app_id, flatpak_instance_get_app (app)); + } + if (!found) + { + g_hash_table_iter_remove (&iter); + if (handle) + g_ptr_array_add (handles, g_strdup (handle)); + } + } + G_UNLOCK (applications); + + for (j = 0; j < handles->len; j++) + { + const char *handle = g_ptr_array_index (handles, j); + close_notification (handle); + } +} + +static gboolean +lookup_background_app (const char *app_id, + char **value) +{ + gboolean res; + char *orig_key; + char *orig_val; + + G_LOCK (applications); + res = g_hash_table_lookup_extended (applications, app_id, (gpointer *)&orig_key, (gpointer *)&orig_val); + if (res) + *value = g_strdup (orig_val); + G_UNLOCK (applications); + + return res; +} + +static void +add_background_app (const char *app_id, + const char *handle) +{ + G_LOCK (applications); + g_hash_table_insert (applications, g_strdup (app_id), g_strdup (handle)); + G_UNLOCK (applications); +} + +static void +remove_background_app (const char *app_id) +{ + g_autofree char *handle = NULL; + + G_LOCK (applications); + handle = g_strdup (g_hash_table_lookup (applications, app_id)); + g_hash_table_remove (applications, app_id); + G_UNLOCK (applications); + + if (handle) + close_notification (handle); +} + +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); +} + +static FlatpakInstance * +find_instance (const char *app_id) +{ + g_autoptr(GPtrArray) instances = NULL; + int i; + + instances = flatpak_instance_get_all (); + for (i = 0; i < instances->len; i++) + { + FlatpakInstance *inst = g_ptr_array_index (instances, i); + + if (g_str_equal (flatpak_instance_get_app (inst), app_id)) + return g_object_ref (inst); + } + + return NULL; +} + +static void +kill_app (const char *app_id) +{ + g_autoptr(FlatpakInstance) instance = NULL; + + g_debug ("Killing app %s", app_id); + + instance = find_instance (app_id); + + if (instance) + kill (flatpak_instance_get_child_pid (instance), SIGKILL); +} + +typedef struct { + char *app_id; + Permission perm; +} DoneData; + +static void +done_data_free (gpointer data) +{ + DoneData *ddata = data; + + g_free (ddata->app_id); + g_free (ddata); +} + +static void +notify_background_done (GObject *source, + GAsyncResult *res, + gpointer data) +{ + DoneData *ddata = (DoneData *)data; + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) results = NULL; + guint response; + gboolean allow; + + if (!xdp_impl_background_call_notify_background_finish (background, + &response, + &results, + res, + &error)) + { + g_warning ("Error from background backend: %s", error->message); + done_data_free (ddata); + return; + } + + g_variant_lookup (results, "allow", "b", &allow); + + if (allow) + { + g_debug ("Allowing app %s to run in background", ddata->app_id); + if (ddata->perm != ASK) + set_permission (ddata->app_id, YES); + } + else + { + g_debug ("Forbid app %s to run in background", ddata->app_id); + + if (ddata->perm != ASK) + set_permission (ddata->app_id, ASK); + kill_app (ddata->app_id); + } + + add_background_app (ddata->app_id, NULL); + done_data_free (ddata); +} + +static void +send_notification (FlatpakInstance *instance, + Permission permission) +{ + DoneData *ddata; + g_autofree char *name = flatpak_instance_get_display_name (instance); + g_autofree char *handle = NULL; + static int count; + + ddata = g_new (DoneData, 1); + ddata->app_id = g_strdup (flatpak_instance_get_app (instance)); + ddata->perm = permission; + + g_debug ("Notify background for %s", ddata->app_id); + + handle = g_strdup_printf ("/org/freedesktop/portal/desktop/notify/background%d", count++); + + add_background_app (ddata->app_id, handle); + + xdp_impl_background_call_notify_background (background, + handle, + ddata->app_id, + name, + NULL, + notify_background_done, + ddata); +} + +static void +check_background_apps (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + g_autoptr(GVariant) perms = NULL; + g_autoptr(GHashTable) app_states = NULL; + g_autoptr(GPtrArray) apps = NULL; + int i; + + app_states = get_app_states (); + + if (app_states == NULL) + return; + + g_debug ("Checking background permissions"); + perms = get_permissions (); + apps = flatpak_instance_get_all (); + + remove_outdated_applications (apps); + + for (i = 0; i < apps->len; i++) + { + FlatpakInstance *instance = g_ptr_array_index (apps, i); + const char *app_id = flatpak_instance_get_app (instance); + Permission permission; + AppState state; + const char *state_names[] = { "background", "running", "active" }; + g_autofree char *handle = NULL; + + if (!flatpak_instance_is_running (instance)) + continue; + + state = get_app_state (app_id, app_states); + g_debug ("App %s is %s", app_id, state_names[state]); + + if (state != BACKGROUND) + { + remove_background_app (app_id); + continue; + } + + if (!lookup_background_app (app_id, &handle)) + { + add_background_app (app_id, NULL); + continue; + } + + if (handle) + continue; /* already notified */ + + permission = get_permission (app_id, perms); + if (permission == NO) + { + pid_t pid = flatpak_instance_get_child_pid (instance); + g_debug ("Killing app %s (child pid %u)", app_id, pid); + kill (pid, SIGKILL); + } + else if (permission == ASK || permission == UNSET) + { + send_notification (instance, permission); + } + } +} + +static gboolean +enforce_background_permissions (gpointer data) +{ + g_autoptr(GTask) task = NULL; + + task = g_task_new (NULL, NULL, NULL, NULL); + g_task_run_in_thread (task, check_background_apps); + + return G_SOURCE_CONTINUE; +} + +void +start_background_monitor (GDBusConnection *connection, + const char *dbus_name) +{ + g_autoptr(GError) error = NULL; + + background = xdp_impl_background_proxy_new_sync (connection, + G_DBUS_PROXY_FLAGS_NONE, + dbus_name, + DESKTOP_PORTAL_OBJECT_PATH, + NULL, &error); + if (background == NULL) + { + g_debug ("No background impl: %s", error->message); + return; + } + + g_dbus_proxy_set_default_timeout (G_DBUS_PROXY (background), G_MAXINT); + + init_permission_store (connection); + + applications = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + g_debug ("Starting background app monitor"); + g_timeout_add_seconds (30, enforce_background_permissions, NULL); + enforce_background_permissions (NULL); +} diff --git a/src/background-monitor.h b/src/background-monitor.h new file mode 100644 index 000000000..7b733a002 --- /dev/null +++ b/src/background-monitor.h @@ -0,0 +1,26 @@ +/* + * 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.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, see . + * + * Authors: + * Matthias Clasen + */ +#ifndef __BACKGROUND_MONITOR_H__ +#define __BACKGROUND_MONITOR_H__ + +void start_background_monitor (GDBusConnection *connection, + const char *dbus_name); + +#endif diff --git a/src/xdg-desktop-portal.c b/src/xdg-desktop-portal.c index 9a1745062..1795a671b 100644 --- a/src/xdg-desktop-portal.c +++ b/src/xdg-desktop-portal.c @@ -51,6 +51,7 @@ #include "location.h" #include "settings.h" #include "background.h" +#include "background-monitor.h" static GMainLoop *loop = NULL; @@ -469,6 +470,10 @@ on_bus_acquired (GDBusConnection *connection, export_portal_implementation (connection, remote_desktop_create (connection, implementation->dbus_name)); #endif + + implementation = find_portal_implementation ("org.freedesktop.impl.portal.Background"); + if (implementation != NULL) + start_background_monitor (connection, implementation->dbus_name); } static void