#include <stdlib.h>
#include <unistd.h>
#include <gtk/gtk.h>
#include <libxapp/xapp-status-icon.h>
#include <libxapp/xapp-util.h>
#include <glib-unix.h>
#include "sn-watcher-interface.h"
#include "sn-item-interface.h"
#include "sn-item.h"
#define XAPP_TYPE_SN_WATCHER xapp_sn_watcher_get_type ()
G_DECLARE_FINAL_TYPE (XAppSnWatcher, xapp_sn_watcher, XAPP, SN_WATCHER, GtkApplication)
struct _XAppSnWatcher
{
GtkApplication parent_instance;
SnWatcherInterface *skeleton;
GDBusConnection *connection;
GCancellable *cancellable;
guint owner_id;
guint name_listener_id;
GHashTable *items;
gboolean shutdown_pending;
gboolean advertise_host;
};
G_DEFINE_TYPE (XAppSnWatcher, xapp_sn_watcher, GTK_TYPE_APPLICATION)
#define NOTIFICATION_WATCHER_NAME "org.kde.StatusNotifierWatcher"
#define NOTIFICATION_WATCHER_PATH "/StatusNotifierWatcher"
#define STATUS_ICON_MONITOR_PREFIX "org.x.StatusIconMonitor"
#define FDO_DBUS_NAME "org.freedesktop.DBus"
#define FDO_DBUS_PATH "/org/freedesktop/DBus"
#define STATUS_ICON_MONITOR_MATCH "org.x.StatusIconMonitor"
#define APPINDICATOR_PATH_PREFIX "/org/ayatana/NotificationItem/"
GSettings *xapp_settings;
static void continue_startup (XAppSnWatcher *watcher);
static void update_published_items (XAppSnWatcher *watcher);
static void
handle_status_applet_name_owner_appeared (XAppSnWatcher *watcher,
const gchar *name,
const gchar *new_owner)
{
if (g_str_has_prefix (name, STATUS_ICON_MONITOR_PREFIX))
{
if (watcher->shutdown_pending)
{
g_debug ("A monitor appeared on the bus, cancelling shutdown\n");
watcher->shutdown_pending = FALSE;
g_application_hold (G_APPLICATION (watcher));
if (watcher->owner_id == 0)
{
continue_startup (watcher);
return;
}
else
{
sn_watcher_interface_set_is_status_notifier_host_registered (watcher->skeleton,
watcher->advertise_host);
g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (watcher->skeleton));
sn_watcher_interface_emit_status_notifier_host_registered (watcher->skeleton);
}
}
}
}
static void
handle_sn_item_name_owner_lost (XAppSnWatcher *watcher,
const gchar *name,
const gchar *old_owner)
{
GList *keys, *l;
keys = g_hash_table_get_keys (watcher->items);
for (l = keys; l != NULL; l = l->next)
{
const gchar *key = l->data;
if (g_str_has_prefix (key, name))
{
g_debug ("Client %s has exited, removing status icon", key);
g_hash_table_remove (watcher->items, key);
update_published_items (watcher);
break;
}
}
g_list_free (keys);
}
static void
handle_status_applet_name_owner_lost (XAppSnWatcher *watcher,
const gchar *name,
const gchar *old_owner)
{
if (g_str_has_prefix (name, STATUS_ICON_MONITOR_PREFIX))
{
g_debug ("Lost a monitor, checking for any more");
if (xapp_status_icon_any_monitors ())
{
g_debug ("Still have a monitor, continuing");
return;
}
else
{
g_debug ("Lost our last monitor, starting countdown\n");
if (!watcher->shutdown_pending)
{
watcher->shutdown_pending = TRUE;
g_application_release (G_APPLICATION (watcher));
sn_watcher_interface_set_is_status_notifier_host_registered (watcher->skeleton,
FALSE);
g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (watcher->skeleton));
}
}
}
else
{
handle_sn_item_name_owner_lost (watcher, name, old_owner);
}
}
static void
name_owner_changed_signal (GDBusConnection *connection,
const gchar *sender_name,
const gchar *object_path,
const gchar *interface_name,
const gchar *signal_name,
GVariant *parameters,
gpointer user_data)
{
XAppSnWatcher *watcher = XAPP_SN_WATCHER (user_data);
const gchar *name, *old_owner, *new_owner;
g_variant_get (parameters, "(&s&s&s)", &name, &old_owner, &new_owner);
g_debug("XAppSnWatcher: NameOwnerChanged signal received (n: %s, old: %s, new: %s", name, old_owner, new_owner);
if (!name)
{
return;
}
if (g_strcmp0 (new_owner, "") == 0)
{
handle_status_applet_name_owner_lost (watcher, name, old_owner);
}
else
{
handle_status_applet_name_owner_appeared (watcher, name, new_owner);
}
}
static void
add_name_listener (XAppSnWatcher *watcher)
{
g_debug ("XAppSnWatcher: Adding NameOwnerChanged listener for status monitor existence");
watcher->name_listener_id = g_dbus_connection_signal_subscribe (watcher->connection,
FDO_DBUS_NAME,
FDO_DBUS_NAME,
"NameOwnerChanged",
FDO_DBUS_PATH,
NULL,
G_DBUS_SIGNAL_FLAGS_NONE,
name_owner_changed_signal,
watcher,
NULL);
}
static void
on_name_lost (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
XAppSnWatcher *watcher = XAPP_SN_WATCHER (user_data);
g_debug ("Lost StatusNotifierWatcher name (maybe something replaced us), exiting immediately");
g_application_quit (G_APPLICATION (watcher));
}
static void
on_name_acquired (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
XAppSnWatcher *watcher = XAPP_SN_WATCHER (user_data);
g_debug ("Name acquired on dbus");
sn_watcher_interface_set_protocol_version (watcher->skeleton, 0);
sn_watcher_interface_set_is_status_notifier_host_registered (watcher->skeleton,
watcher->advertise_host);
g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (watcher->skeleton));
sn_watcher_interface_emit_status_notifier_host_registered (watcher->skeleton);
}
static gboolean
handle_register_host (SnWatcherInterface *skeleton,
GDBusMethodInvocation *invocation,
const gchar* service,
XAppSnWatcher *watcher)
{
// Nothing to do - we wouldn't be here if there wasn't a host (status applet)
sn_watcher_interface_complete_register_status_notifier_host (skeleton,
invocation);
return TRUE;
}
static void
populate_published_list (const gchar *key,
gpointer item,
GPtrArray *array)
{
g_ptr_array_add (array, g_strdup (key));
}
static void
update_published_items (XAppSnWatcher *watcher)
{
GPtrArray *array;
gpointer as;
array = g_ptr_array_new ();
g_hash_table_foreach (watcher->items, (GHFunc) populate_published_list, array);
g_ptr_array_add (array, NULL);
as = g_ptr_array_free (array, FALSE);
sn_watcher_interface_set_registered_status_notifier_items (watcher->skeleton,
(const gchar * const *) as);
g_strfreev ((gchar **) as);
g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (watcher->skeleton));
}
static gboolean
create_key (const gchar *sender,
const gchar *service,
gchar **key,
gchar **bus_name,
gchar **path)
{
gchar *temp_key, *temp_bname, *temp_path;
temp_key = temp_bname = temp_path = NULL;
*key = *bus_name = *path = NULL;
if (g_str_has_prefix (service, "/"))
{
temp_bname = g_strdup (sender);
temp_path = g_strdup (service);
}
else
{
temp_bname = g_strdup (service);
temp_path = g_strdup ("/StatusNotifierItem");
}
if (!g_dbus_is_name (temp_bname))
{
g_free (temp_bname);
g_free (temp_path);
return FALSE;
}
temp_key = g_strdup_printf ("%s%s", temp_bname, temp_path);
g_debug ("Key: '%s', busname '%s', path '%s'", temp_key, temp_bname, temp_path);
*key = temp_key;
*bus_name = temp_bname;
*path = temp_path;
return TRUE;
}
typedef struct
{
XAppSnWatcher *watcher;
GDBusMethodInvocation *invocation;
gchar *key;
gchar *path;
gchar *bus_name;
gchar *service;
} NewSnProxyData;
static void
free_sn_proxy_data (NewSnProxyData *data)
{
g_free (data->key);
g_free (data->path);
g_free (data->bus_name);
g_free (data->service);
g_object_unref (data->invocation);
g_slice_free (NewSnProxyData, data);
}
static void
sn_item_proxy_new_completed (GObject *source,
GAsyncResult *res,
gpointer user_data)
{
NewSnProxyData *data = (NewSnProxyData *) user_data;
XAppSnWatcher *watcher = data->watcher;
SnItem *item;
gpointer stolen_ptr;
GError *error = NULL;
SnItemInterface *proxy;
proxy = sn_item_interface_proxy_new_finish (res, &error);
g_hash_table_steal_extended (watcher->items,
data->key,
&stolen_ptr,
NULL);
if ((gchar *) stolen_ptr == NULL)
{
g_dbus_method_invocation_return_error (data->invocation,
G_DBUS_ERROR,
G_DBUS_ERROR_FAILED,
"New StatusNotifierItem disappeared before "
"its registration was complete.");
free_sn_proxy_data (data);
g_clear_object (&proxy);
return;
}
if (error != NULL)
{
if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
{
g_debug ("Could not create new status notifier proxy item for item at %s: %s",
data->bus_name, error->message);
}
g_free (stolen_ptr);
free_sn_proxy_data (data);
g_dbus_method_invocation_take_error (data->invocation, error);
return;
}
item = sn_item_new ((GDBusProxy *) proxy,
g_str_has_prefix (data->path, APPINDICATOR_PATH_PREFIX));
g_hash_table_insert (watcher->items,
stolen_ptr,
item);
update_published_items (watcher);
sn_watcher_interface_emit_status_notifier_item_registered (watcher->skeleton,
data->service);
sn_watcher_interface_complete_register_status_notifier_item (watcher->skeleton,
data->invocation);
free_sn_proxy_data (data);
}
static gboolean
handle_register_item (SnWatcherInterface *skeleton,
GDBusMethodInvocation *invocation,
const gchar* service,
XAppSnWatcher *watcher)
{
GError *error;
const gchar *sender;
g_autofree gchar *key = NULL, *bus_name = NULL, *path = NULL;
sender = g_dbus_method_invocation_get_sender (invocation);
if (!create_key (sender, service, &key, &bus_name, &path))
{
error = g_error_new (g_dbus_error_quark (),
G_DBUS_ERROR_INVALID_ARGS,
"Invalid bus name from: %s, %s", service, sender);
g_dbus_method_invocation_return_gerror (invocation, error);
return FALSE;
}
if (!g_hash_table_contains (watcher->items, (const gchar *) key))
{
NewSnProxyData *data;
error = NULL;
// g_debug ("Key: '%s'", key);
data = g_slice_new0 (NewSnProxyData);
data->watcher = watcher;
data->key = g_strdup (key);
data->path = g_strdup (path);
data->bus_name = g_strdup (bus_name);
data->service = g_strdup (service);
data->invocation = g_object_ref (invocation);
/* Save a place in the hash table, some programs re-register frequently,
* and we don't want the same app have two registrations here. */
g_hash_table_insert (watcher->items,
g_strdup (key),
NULL);
sn_item_interface_proxy_new (watcher->connection,
G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES,
bus_name,
path,
watcher->cancellable,
sn_item_proxy_new_completed,
data);
}
else
{
sn_watcher_interface_complete_register_status_notifier_item (watcher->skeleton,
invocation);
}
return TRUE;
}
static gboolean
export_watcher_interface (XAppSnWatcher *watcher)
{
GError *error = NULL;
if (watcher->skeleton) {
return TRUE;
}
watcher->skeleton = sn_watcher_interface_skeleton_new ();
g_debug ("XAppSnWatcher: exporting StatusNotifierWatcher dbus interface to %s", NOTIFICATION_WATCHER_PATH);
g_signal_connect (watcher->skeleton,
"handle-register-status-notifier-item",
G_CALLBACK (handle_register_item),
watcher);
g_signal_connect (watcher->skeleton,
"handle-register-status-notifier-host",
G_CALLBACK (handle_register_host),
watcher);
g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (watcher->skeleton),
watcher->connection,
NOTIFICATION_WATCHER_PATH,
&error);
if (error != NULL) {
g_critical ("XAppSnWatcher: could not export StatusNotifierWatcher interface: %s", error->message);
g_error_free (error);
return FALSE;
}
return TRUE;
}
static gboolean
on_interrupt (XAppSnWatcher *watcher)
{
g_debug ("SIGINT - shutting down immediately");
g_application_quit (G_APPLICATION (watcher));
return FALSE;
}
static void
continue_startup (XAppSnWatcher *watcher)
{
g_debug ("Trying to acquire session bus connection");
g_unix_signal_add (SIGINT, (GSourceFunc) on_interrupt, watcher);
g_application_hold (G_APPLICATION (watcher));
export_watcher_interface (watcher);
watcher->owner_id = g_bus_own_name_on_connection (watcher->connection,
NOTIFICATION_WATCHER_NAME,
G_BUS_NAME_OWNER_FLAGS_REPLACE,
on_name_acquired,
on_name_lost,
watcher,
NULL);
}
static void
unref_proxy (gpointer data)
{
// if g_hash_table_remove is called from handle_sn_item_name_owner_lost
// *before* sn_item_proxy_new_completed is complete, the key will be
// pointing to a NULL value, so avoid trying to free it.
if (data == NULL)
{
return;
}
SnItem *item = SN_ITEM (data);
if (item)
{
g_object_unref (item);
}
}
static void
watcher_startup (GApplication *application)
{
XAppSnWatcher *watcher = (XAppSnWatcher*) application;
GError *error;
G_APPLICATION_CLASS (xapp_sn_watcher_parent_class)->startup (application);
xapp_settings = g_settings_new (STATUS_ICON_SCHEMA);
watcher->items = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free, (GDestroyNotify) unref_proxy);
/* This buys us 30 seconds (gapp timeout) - we'll either be re-held immediately
* because there's a monitor or exit after the 30 seconds. */
g_application_hold (application);
g_application_release (application);
watcher->cancellable = g_cancellable_new ();
error = NULL;
watcher->connection = g_bus_get_sync (G_BUS_TYPE_SESSION,
NULL,
&error);
if (error != NULL)
{
g_critical ("Could not get session bus: %s\n", error->message);
g_application_quit (application);
}
watcher->advertise_host = g_settings_get_boolean (xapp_settings, ADVERTISE_SNH_KEY);
add_name_listener (watcher);
if (xapp_status_icon_any_monitors ())
{
continue_startup (watcher);
}
else
{
g_debug ("No active monitors, exiting in 30s");
watcher->shutdown_pending = TRUE;
}
}
static void
watcher_finalize (GObject *object)
{
G_OBJECT_CLASS (xapp_sn_watcher_parent_class)->finalize (object);
}
static void
watcher_shutdown (GApplication *application)
{
XAppSnWatcher *watcher = (XAppSnWatcher *) application;
g_clear_object (&xapp_settings);
if (watcher->name_listener_id > 0)
{
g_dbus_connection_signal_unsubscribe (watcher->connection, watcher->name_listener_id);
watcher->name_listener_id = 0;
}
update_published_items (watcher);
g_clear_pointer (&watcher->items, g_hash_table_unref);
sn_watcher_interface_set_is_status_notifier_host_registered (watcher->skeleton,
FALSE);
g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (watcher->skeleton));
sn_watcher_interface_emit_status_notifier_host_registered (watcher->skeleton);
if (watcher->owner_id > 0)
{
g_bus_unown_name (watcher->owner_id);
}
if (watcher->skeleton != NULL)
{
g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (watcher->skeleton));
g_clear_object (&watcher->skeleton);
}
g_clear_object (&watcher->connection);
g_clear_object (&watcher->cancellable);
G_APPLICATION_CLASS (xapp_sn_watcher_parent_class)->shutdown (application);
}
static void
watcher_activate (GApplication *application)
{
}
static void
xapp_sn_watcher_init (XAppSnWatcher *watcher)
{
}
static void
xapp_sn_watcher_class_init (XAppSnWatcherClass *class)
{
GApplicationClass *application_class = G_APPLICATION_CLASS (class);
GObjectClass *object_class = G_OBJECT_CLASS (class);
application_class->startup = watcher_startup;
application_class->shutdown = watcher_shutdown;
application_class->activate = watcher_activate;
object_class->finalize = watcher_finalize;
}
XAppSnWatcher *
watcher_new (const gchar *current_desktop)
{
XAppSnWatcher *watcher;
gboolean _register;
g_set_application_name ("xapp-sn-watcher");
// FIXME: xfce-session crashes if we try to register.
_register = g_strcmp0 (current_desktop, "XFCE") != 0;
watcher = g_object_new (xapp_sn_watcher_get_type (),
"application-id", "org.x.StatusNotifierWatcher",
"inactivity-timeout", 30000,
"register-session", _register,
NULL);
return watcher;
}
int
main (int argc, char **argv)
{
XAppSnWatcher *watcher;
gchar **whitelist;
const gchar *current_desktop;
gboolean should_start;
int status;
if (xapp_util_get_session_is_running ())
{
// The session manager tries to restart this immediately in the event of a crash,
// and we need a small delay between instances of xapp-sn-watcher to allow dbus and
// status-notifier apps to cleanly process removal of the old one.
//
// Skip this at startup, as it would cause a delay during login, and there's no
// existing process to replace anyhow.
sleep (2);
}
current_desktop = g_getenv ("XDG_CURRENT_DESKTOP");
xapp_settings = g_settings_new (STATUS_ICON_SCHEMA);
if (g_settings_get_boolean (xapp_settings, DEBUG_KEY))
{
g_setenv ("G_MESSAGES_DEBUG", "all", TRUE);
}
whitelist = g_settings_get_strv (xapp_settings,
VALID_XDG_DESKTOPS_KEY);
should_start = g_strv_contains ((const gchar * const *) whitelist, current_desktop);
g_strfreev (whitelist);
g_clear_object (&xapp_settings);
if (!should_start)
{
g_debug ("XDG_CURRENT_DESKTOP is '%s' - not starting XApp's StatusNotifierWatcher service."
"If you want to change this, add your desktop's name to the dconf org.x.apps.statusicon "
"'status-notifier-enabled-desktops' setting key.", current_desktop);
exit(0);
}
watcher = watcher_new (current_desktop);
status = g_application_run (G_APPLICATION (watcher), argc, argv);
g_object_unref (watcher);
return status;
}