about summary refs log tree commit diff
path: root/src/nm-dispatcher.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/nm-dispatcher.c')
-rw-r--r--src/nm-dispatcher.c603
1 files changed, 488 insertions, 115 deletions
diff --git a/src/nm-dispatcher.c b/src/nm-dispatcher.c
index 12cd0f0a..f955822b 100644
--- a/src/nm-dispatcher.c
+++ b/src/nm-dispatcher.c
@@ -20,17 +20,58 @@
  */
 
 #include <glib.h>
+#include <gio/gio.h>
 #include <string.h>
+#include <errno.h>
 
 #include "nm-dispatcher.h"
-#include "nm-dispatcher-action.h"
+#include "nm-dispatcher-api.h"
 #include "NetworkManagerUtils.h"
 #include "nm-utils.h"
 #include "nm-logging.h"
 #include "nm-dbus-manager.h"
 #include "nm-dbus-glib-types.h"
+#include "nm-glib-compat.h"
 
-static GSList *requests = NULL;
+#define CALL_TIMEOUT (1000 * 60 * 10)  /* 10 minutes for all scripts */
+
+static GHashTable *requests = NULL;
+
+typedef struct {
+	GFileMonitor *monitor;
+	const char *const description;
+	const char *const dir;
+	const guint16 dir_len;
+	char has_scripts;
+} Monitor;
+
+enum {
+	MONITOR_INDEX_DEFAULT,
+	MONITOR_INDEX_PRE_UP,
+	MONITOR_INDEX_PRE_DOWN,
+};
+
+static Monitor monitors[3] = {
+#define MONITORS_INIT_SET(INDEX, USE, SCRIPT_DIR)   [INDEX] = { .dir_len = STRLEN (SCRIPT_DIR), .dir = SCRIPT_DIR, .description = ("" USE), .has_scripts = TRUE }
+	MONITORS_INIT_SET (MONITOR_INDEX_DEFAULT,  "default",  NMD_SCRIPT_DIR_DEFAULT),
+	MONITORS_INIT_SET (MONITOR_INDEX_PRE_UP,   "pre-up",   NMD_SCRIPT_DIR_PRE_UP),
+	MONITORS_INIT_SET (MONITOR_INDEX_PRE_DOWN, "pre-down", NMD_SCRIPT_DIR_PRE_DOWN),
+};
+
+static const Monitor*
+_get_monitor_by_action (DispatcherAction action)
+{
+	switch (action) {
+	case DISPATCHER_ACTION_PRE_UP:
+	case DISPATCHER_ACTION_VPN_PRE_UP:
+		return &monitors[MONITOR_INDEX_PRE_UP];
+	case DISPATCHER_ACTION_PRE_DOWN:
+	case DISPATCHER_ACTION_VPN_PRE_DOWN:
+		return &monitors[MONITOR_INDEX_PRE_DOWN];
+	default:
+		return &monitors[MONITOR_INDEX_DEFAULT];
+	}
+}
 
 static void
 dump_object_to_props (GObject *object, GHashTable *hash)
@@ -131,19 +172,38 @@ fill_vpn_props (NMIP4Config *ip4_config,
 }
 
 typedef struct {
+	DispatcherAction action;
+	guint request_id;
 	DispatcherFunc callback;
 	gpointer user_data;
-	NMDBusManager *dbus_mgr;
+	guint idle_id;
 } DispatchInfo;
 
 static void
 dispatcher_info_free (DispatchInfo *info)
 {
-	requests = g_slist_remove (requests, info);
-	g_object_unref (info->dbus_mgr);
+	if (info->idle_id)
+		g_source_remove (info->idle_id);
 	g_free (info);
 }
 
+static void
+_ensure_requests (void)
+{
+	if (G_UNLIKELY (requests == NULL)) {
+		requests = g_hash_table_new_full (g_direct_hash,
+		                                  g_direct_equal,
+		                                  NULL,
+		                                  (GDestroyNotify) dispatcher_info_free);
+	}
+}
+
+static void
+dispatcher_info_cleanup (DispatchInfo *info)
+{
+	g_hash_table_remove (requests, GUINT_TO_POINTER (info->request_id));
+}
+
 static const char *
 dispatch_result_to_string (DispatchResult result)
 {
@@ -162,98 +222,178 @@ dispatch_result_to_string (DispatchResult result)
 	g_assert_not_reached ();
 }
 
+static gboolean
+validate_element (guint request_id, GValue *val, GType expected_type, guint idx, guint eltnum)
+{
+	if (G_VALUE_TYPE (val) != expected_type) {
+		nm_log_dbg (LOGD_DISPATCH, "(%u) result %d element %d invalid type %s",
+		            request_id, idx, eltnum, G_VALUE_TYPE_NAME (val));
+		return FALSE;
+	}
+	return TRUE;
+}
+
+static void
+dispatcher_results_process (guint request_id, DispatcherAction action, GPtrArray *results)
+{
+	guint i;
+	const Monitor *monitor = _get_monitor_by_action (action);
+
+	g_return_if_fail (results != NULL);
+
+	if (results->len == 0) {
+		nm_log_dbg (LOGD_DISPATCH, "(%u) succeeded but no scripts invoked",
+		            request_id);
+		return;
+	}
+
+	for (i = 0; i < results->len; i++) {
+		GValueArray *item = g_ptr_array_index (results, i);
+		GValue *tmp;
+		const char *script, *err;
+		DispatchResult result;
+		const char *script_validation_msg = "";
+
+		if (item->n_values != 3) {
+			nm_log_dbg (LOGD_DISPATCH, "(%u) unexpected number of items in "
+			            "dispatcher result (got %d, expected 3)",
+			            request_id, item->n_values);
+			continue;
+		}
+
+		/* Script */
+		tmp = g_value_array_get_nth (item, 0);
+		if (!validate_element (request_id, tmp, G_TYPE_STRING, i, 0))
+			continue;
+		script = g_value_get_string (tmp);
+		if (!script) {
+			script_validation_msg = " (path is NULL)";
+			script = "(unknown)";
+		} else if (!strncmp (script, monitor->dir, monitor->dir_len)            /* check: prefixed by script directory */
+		    && script[monitor->dir_len] == '/' && script[monitor->dir_len+1]    /* check: with additional "/?" */
+		    && !strchr (&script[monitor->dir_len+1], '/')) {                    /* check: and no further '/' */
+			/* we expect the script to lie inside monitor->dir. If it does,
+			 * strip the directory name. Otherwise show the full path and a warning. */
+			script += monitor->dir_len + 1;
+		} else
+			script_validation_msg = " (unexpected path)";
+
+
+		/* Result */
+		tmp = g_value_array_get_nth (item, 1);
+		if (!validate_element (request_id, tmp, G_TYPE_UINT, i, 1))
+			continue;
+		result = g_value_get_uint (tmp);
+
+		/* Error */
+		tmp = g_value_array_get_nth (item, 2);
+		if (!validate_element (request_id, tmp, G_TYPE_STRING, i, 2))
+			continue;
+		err = g_value_get_string (tmp);
+
+
+		if (result == DISPATCH_RESULT_SUCCESS) {
+			nm_log_dbg (LOGD_DISPATCH, "(%u) %s succeeded%s",
+			            request_id,
+			            script, script_validation_msg);
+		} else {
+			nm_log_warn (LOGD_DISPATCH, "(%u) %s failed (%s): %s%s",
+			             request_id,
+			             script,
+			             dispatch_result_to_string (result),
+			             err ? err : "", script_validation_msg);
+		}
+	}
+}
+
+static void
+free_results (GPtrArray *results)
+{
+	g_return_if_fail (results != NULL);
+	g_ptr_array_foreach (results, (GFunc) g_value_array_free, NULL);
+	g_ptr_array_free (results, TRUE);
+}
+
 static void
 dispatcher_done_cb (DBusGProxy *proxy, DBusGProxyCall *call, gpointer user_data)
 {
 	DispatchInfo *info = user_data;
 	GError *error = NULL;
 	GPtrArray *results = NULL;
-	guint i;
 
 	if (dbus_g_proxy_end_call (proxy, call, &error,
 	                           DISPATCHER_TYPE_RESULT_ARRAY, &results,
 	                           G_TYPE_INVALID)) {
-		for (i = 0; results && (i < results->len); i++) {
-			GValueArray *item = g_ptr_array_index (results, i);
-			GValue *tmp;
-			const char *script, *err;
-			DispatchResult result;
-
-			if (   (G_VALUE_TYPE (g_value_array_get_nth (item, 0)) == G_TYPE_STRING)
-			    && (G_VALUE_TYPE (g_value_array_get_nth (item, 1)) == G_TYPE_UINT)
-			    && (G_VALUE_TYPE (g_value_array_get_nth (item, 2)) == G_TYPE_STRING)) {
-				/* result */
-				tmp = g_value_array_get_nth (item, 1);
-				result = g_value_get_uint (tmp);
-				if (result != DISPATCH_RESULT_SUCCESS) {
-					/* script */
-					tmp = g_value_array_get_nth (item, 0);
-					script = g_value_get_string (tmp);
-
-					/* error */
-					tmp = g_value_array_get_nth (item, 2);
-					err = g_value_get_string (tmp);
-
-					nm_log_warn (LOGD_CORE, "Dispatcher script %s: %s",
-					             dispatch_result_to_string (result), err);
-				}
-			} else
-				nm_log_dbg (LOGD_CORE, "Dispatcher result element %d invalid type", i);
-
-			g_value_array_free (item);
-		}
-		g_ptr_array_free (results, TRUE);
+		dispatcher_results_process (info->request_id, info->action, results);
+		free_results (results);
 	} else {
 		g_assert (error);
-		nm_log_warn (LOGD_CORE, "Dispatcher failed: (%d) %s", error->code, error->message);
+
+		if (!g_error_matches (error, DBUS_GERROR, DBUS_GERROR_REMOTE_EXCEPTION)) {
+			nm_log_warn (LOGD_DISPATCH, "(%u) failed to call dispatcher scripts: (%s:%d) %s",
+			             info->request_id, g_quark_to_string (error->domain),
+			             error->code, error->message);
+		} else if (!dbus_g_error_has_name (error, "org.freedesktop.systemd1.LoadFailed")) {
+			nm_log_warn (LOGD_DISPATCH, "(%u) failed to call dispatcher scripts: (%s) %s",
+			             info->request_id, dbus_g_error_get_name (error), error->message);
+		} else {
+			nm_log_dbg (LOGD_DISPATCH, "(%u) failed to call dispatcher scripts: (%s) %s",
+			            info->request_id, dbus_g_error_get_name (error), error->message);
+		}
 	}
 
 	if (info->callback)
-		info->callback (info, info->user_data);
+		info->callback (info->request_id, info->user_data);
 
 	g_clear_error (&error);
 	g_object_unref (proxy);
 }
 
+static const char *action_table[] = {
+	[DISPATCHER_ACTION_HOSTNAME]     = NMD_ACTION_HOSTNAME,
+	[DISPATCHER_ACTION_PRE_UP]       = NMD_ACTION_PRE_UP,
+	[DISPATCHER_ACTION_UP]           = NMD_ACTION_UP,
+	[DISPATCHER_ACTION_PRE_DOWN]     = NMD_ACTION_PRE_DOWN,
+	[DISPATCHER_ACTION_DOWN]         = NMD_ACTION_DOWN,
+	[DISPATCHER_ACTION_VPN_PRE_UP]   = NMD_ACTION_VPN_PRE_UP,
+	[DISPATCHER_ACTION_VPN_UP]       = NMD_ACTION_VPN_UP,
+	[DISPATCHER_ACTION_VPN_PRE_DOWN] = NMD_ACTION_VPN_PRE_DOWN,
+	[DISPATCHER_ACTION_VPN_DOWN]     = NMD_ACTION_VPN_DOWN,
+	[DISPATCHER_ACTION_DHCP4_CHANGE] = NMD_ACTION_DHCP4_CHANGE,
+	[DISPATCHER_ACTION_DHCP6_CHANGE] = NMD_ACTION_DHCP6_CHANGE,
+};
+
 static const char *
 action_to_string (DispatcherAction action)
 {
-	switch (action) {
-	case DISPATCHER_ACTION_HOSTNAME:
-		return "hostname";
-	case DISPATCHER_ACTION_UP:
-		return "up";
-	case DISPATCHER_ACTION_PRE_DOWN:
-		return "pre-down";
-	case DISPATCHER_ACTION_DOWN:
-		return "down";
-	case DISPATCHER_ACTION_VPN_UP:
-		return "vpn-up";
-	case DISPATCHER_ACTION_VPN_PRE_DOWN:
-		return "vpn-pre-down";
-	case DISPATCHER_ACTION_VPN_DOWN:
-		return "vpn-down";
-	case DISPATCHER_ACTION_DHCP4_CHANGE:
-		return "dhcp4-change";
-	case DISPATCHER_ACTION_DHCP6_CHANGE:
-		return "dhcp6-change";
-	default:
-		break;
-	}
-	g_assert_not_reached ();
+	g_assert (action >= 0 && action < G_N_ELEMENTS (action_table));
+	return action_table[action];
+}
+
+static gboolean
+dispatcher_idle_cb (gpointer user_data)
+{
+	DispatchInfo *info = user_data;
+
+	info->idle_id = 0;
+	if (info->callback)
+		info->callback (info->request_id, info->user_data);
+	dispatcher_info_cleanup (info);
+	return G_SOURCE_REMOVE;
 }
 
-static gconstpointer
+static gboolean
 _dispatcher_call (DispatcherAction action,
+                  gboolean blocking,
                   NMConnection *connection,
                   NMDevice *device,
                   const char *vpn_iface,
                   NMIP4Config *vpn_ip4_config,
                   NMIP6Config *vpn_ip6_config,
                   DispatcherFunc callback,
-                  gpointer user_data)
+                  gpointer user_data,
+                  guint *out_call_id)
 {
-	NMDBusManager *dbus_mgr;
 	DBusGProxy *proxy;
 	DBusGConnection *g_connection;
 	GHashTable *connection_hash;
@@ -265,26 +405,66 @@ _dispatcher_call (DispatcherAction action,
 	GHashTable *device_dhcp6_props;
 	GHashTable *vpn_ip4_props;
 	GHashTable *vpn_ip6_props;
-	DBusGProxyCall *call;
-	DispatchInfo *info;
+	DispatchInfo *info = NULL;
+	gboolean success = FALSE;
+	GError *error = NULL;
+	static guint request_counter = 0;
+	guint reqid = ++request_counter;
+
+	/* Wrapping protection */
+	if (G_UNLIKELY (!reqid))
+		reqid = ++request_counter;
+
+	g_assert (!blocking || (!callback && !user_data));
+
+	_ensure_requests ();
 
 	/* All actions except 'hostname' require a device */
-	if (action != DISPATCHER_ACTION_HOSTNAME)
-		g_return_val_if_fail (NM_IS_DEVICE (device), NULL);
+	if (action == DISPATCHER_ACTION_HOSTNAME) {
+		nm_log_dbg (LOGD_DISPATCH, "(%u) dispatching action '%s'%s",
+		            reqid, action_to_string (action),
+		            blocking
+		                ? " (blocking)"
+		                : (callback ? " (with callback)" : ""));
+	} else {
+		g_return_val_if_fail (NM_IS_DEVICE (device), FALSE);
+
+		nm_log_dbg (LOGD_DISPATCH, "(%u) (%s) dispatching action '%s'%s",
+		            reqid,
+		            vpn_iface ? vpn_iface : nm_device_get_iface (device),
+		            action_to_string (action),
+		            blocking
+		                ? " (blocking)"
+		                : (callback ? " (with callback)" : ""));
+	}
+
 	/* VPN actions require at least an IPv4 config (for now) */
 	if (action == DISPATCHER_ACTION_VPN_UP)
-		g_return_val_if_fail (vpn_ip4_config != NULL, NULL);
+		g_return_val_if_fail (vpn_ip4_config != NULL, FALSE);
+
+	if (!_get_monitor_by_action(action)->has_scripts) {
+		if (blocking == FALSE && (out_call_id || callback)) {
+			info = g_malloc0 (sizeof (*info));
+			info->action = action;
+			info->request_id = reqid;
+			info->callback = callback;
+			info->user_data = user_data;
+			info->idle_id = g_idle_add (dispatcher_idle_cb, info);
+			nm_log_dbg (LOGD_DISPATCH, "(%u) simulate request; no scripts in %s",  reqid, _get_monitor_by_action(action)->dir);
+		} else
+			nm_log_dbg (LOGD_DISPATCH, "(%u) ignoring request; no scripts in %s", reqid, _get_monitor_by_action(action)->dir);
+		success = TRUE;
+		goto done;
+	}
 
-	dbus_mgr = nm_dbus_manager_get ();
-	g_connection = nm_dbus_manager_get_connection (dbus_mgr);
+	g_connection = nm_dbus_manager_get_connection (nm_dbus_manager_get ());
 	proxy = dbus_g_proxy_new_for_name (g_connection,
 	                                   NM_DISPATCHER_DBUS_SERVICE,
 	                                   NM_DISPATCHER_DBUS_PATH,
 	                                   NM_DISPATCHER_DBUS_IFACE);
 	if (!proxy) {
-		nm_log_err (LOGD_CORE, "could not get dispatcher proxy!");
-		g_object_unref (dbus_mgr);
-		return NULL;
+		nm_log_err (LOGD_DISPATCH, "(%u) could not get dispatcher proxy!", reqid);
+		return FALSE;
 	}
 
 	if (connection) {
@@ -315,33 +495,66 @@ _dispatcher_call (DispatcherAction action,
 		                   device_ip6_props,
 		                   device_dhcp4_props,
 		                   device_dhcp6_props);
-		if (vpn_iface)
+		if (vpn_ip4_config || vpn_ip6_config)
 			fill_vpn_props (vpn_ip4_config, vpn_ip6_config, vpn_ip4_props, vpn_ip6_props);
 	}
 
-	info = g_malloc0 (sizeof (*info));
-	info->callback = callback;
-	info->user_data = user_data;
-	info->dbus_mgr = dbus_mgr;
-
 	/* Send the action to the dispatcher */
-	call = dbus_g_proxy_begin_call_with_timeout (proxy, "Action",
-	                                             dispatcher_done_cb,
-	                                             info,
-	                                             (GDestroyNotify) dispatcher_info_free,
-	                                             15000,
-	                                             G_TYPE_STRING, action_to_string (action),
-	                                             DBUS_TYPE_G_MAP_OF_MAP_OF_VARIANT, connection_hash,
-	                                             DBUS_TYPE_G_MAP_OF_VARIANT, connection_props,
-	                                             DBUS_TYPE_G_MAP_OF_VARIANT, device_props,
-	                                             DBUS_TYPE_G_MAP_OF_VARIANT, device_ip4_props,
-	                                             DBUS_TYPE_G_MAP_OF_VARIANT, device_ip6_props,
-	                                             DBUS_TYPE_G_MAP_OF_VARIANT, device_dhcp4_props,
-	                                             DBUS_TYPE_G_MAP_OF_VARIANT, device_dhcp6_props,
-	                                             G_TYPE_STRING, vpn_iface ? vpn_iface : "",
-	                                             DBUS_TYPE_G_MAP_OF_VARIANT, vpn_ip4_props,
-	                                             DBUS_TYPE_G_MAP_OF_VARIANT, vpn_ip6_props,
-	                                             G_TYPE_INVALID);
+	if (blocking) {
+		GPtrArray *results = NULL;
+
+		success = dbus_g_proxy_call_with_timeout (proxy, "Action",
+		                                          CALL_TIMEOUT,
+		                                          &error,
+		                                          G_TYPE_STRING, action_to_string (action),
+		                                          DBUS_TYPE_G_MAP_OF_MAP_OF_VARIANT, connection_hash,
+		                                          DBUS_TYPE_G_MAP_OF_VARIANT, connection_props,
+		                                          DBUS_TYPE_G_MAP_OF_VARIANT, device_props,
+		                                          DBUS_TYPE_G_MAP_OF_VARIANT, device_ip4_props,
+		                                          DBUS_TYPE_G_MAP_OF_VARIANT, device_ip6_props,
+		                                          DBUS_TYPE_G_MAP_OF_VARIANT, device_dhcp4_props,
+		                                          DBUS_TYPE_G_MAP_OF_VARIANT, device_dhcp6_props,
+		                                          G_TYPE_STRING, vpn_iface ? vpn_iface : "",
+		                                          DBUS_TYPE_G_MAP_OF_VARIANT, vpn_ip4_props,
+		                                          DBUS_TYPE_G_MAP_OF_VARIANT, vpn_ip6_props,
+		                                          G_TYPE_BOOLEAN, nm_logging_enabled (LOGL_DEBUG, LOGD_DISPATCH),
+		                                          G_TYPE_INVALID,
+		                                          DISPATCHER_TYPE_RESULT_ARRAY, &results,
+		                                          G_TYPE_INVALID);
+		if (success) {
+			dispatcher_results_process (reqid, action, results);
+			free_results (results);
+		} else {
+			nm_log_warn (LOGD_DISPATCH, "(%u) failed: (%d) %s", reqid, error->code, error->message);
+			g_error_free (error);
+		}
+	} else {
+		info = g_malloc0 (sizeof (*info));
+		info->action = action;
+		info->request_id = reqid;
+		info->callback = callback;
+		info->user_data = user_data;
+		dbus_g_proxy_begin_call_with_timeout (proxy, "Action",
+		                                      dispatcher_done_cb,
+		                                      info,
+		                                      (GDestroyNotify) dispatcher_info_cleanup,
+		                                      CALL_TIMEOUT,
+		                                      G_TYPE_STRING, action_to_string (action),
+		                                      DBUS_TYPE_G_MAP_OF_MAP_OF_VARIANT, connection_hash,
+		                                      DBUS_TYPE_G_MAP_OF_VARIANT, connection_props,
+		                                      DBUS_TYPE_G_MAP_OF_VARIANT, device_props,
+		                                      DBUS_TYPE_G_MAP_OF_VARIANT, device_ip4_props,
+		                                      DBUS_TYPE_G_MAP_OF_VARIANT, device_ip6_props,
+		                                      DBUS_TYPE_G_MAP_OF_VARIANT, device_dhcp4_props,
+		                                      DBUS_TYPE_G_MAP_OF_VARIANT, device_dhcp6_props,
+		                                      G_TYPE_STRING, vpn_iface ? vpn_iface : "",
+		                                      DBUS_TYPE_G_MAP_OF_VARIANT, vpn_ip4_props,
+		                                      DBUS_TYPE_G_MAP_OF_VARIANT, vpn_ip6_props,
+		                                      G_TYPE_BOOLEAN, nm_logging_enabled (LOGL_DEBUG, LOGD_DISPATCH),
+		                                      G_TYPE_INVALID);
+		success = TRUE;
+	}
+
 	g_hash_table_destroy (connection_hash);
 	g_hash_table_destroy (connection_props);
 	g_hash_table_destroy (device_props);
@@ -352,45 +565,205 @@ _dispatcher_call (DispatcherAction action,
 	g_hash_table_destroy (vpn_ip4_props);
 	g_hash_table_destroy (vpn_ip6_props);
 
-	/* Track the request in case of cancelation */
-	requests = g_slist_append (requests, info);
+done:
+	if (success && info) {
+		/* Track the request in case of cancelation */
+		g_hash_table_insert (requests, GUINT_TO_POINTER (info->request_id), info);
+		if (out_call_id)
+			*out_call_id = info->request_id;
+	} else if (out_call_id)
+		*out_call_id = 0;
 
-	return info;
+	return success;
 }
 
-gconstpointer
+/**
+ * nm_dispatcher_call:
+ * @action: the %DispatcherAction
+ * @connection: the #NMConnection the action applies to
+ * @device: the #NMDevice the action applies to
+ * @callback: a caller-supplied callback to execute when done
+ * @user_data: caller-supplied pointer passed to @callback
+ * @out_call_id: on success, a call identifier which can be passed to
+ * nm_dispatcher_call_cancel()
+ *
+ * This method always invokes the dispatcher action asynchronously.  To ignore
+ * the result, pass %NULL to @callback.
+ *
+ * Returns: %TRUE if the action was dispatched, %FALSE on failure
+ */
+gboolean
 nm_dispatcher_call (DispatcherAction action,
                     NMConnection *connection,
                     NMDevice *device,
                     DispatcherFunc callback,
-                    gpointer user_data)
+                    gpointer user_data,
+                    guint *out_call_id)
+{
+	return _dispatcher_call (action, FALSE, connection, device, NULL, NULL,
+	                         NULL, callback, user_data, out_call_id);
+}
+
+/**
+ * nm_dispatcher_call_sync():
+ * @action: the %DispatcherAction
+ * @connection: the #NMConnection the action applies to
+ * @device: the #NMDevice the action applies to
+ *
+ * This method always invokes the dispatcher action synchronously and it may
+ * take a long time to return.
+ *
+ * Returns: %TRUE if the action was dispatched, %FALSE on failure
+ */
+gboolean
+nm_dispatcher_call_sync (DispatcherAction action,
+                         NMConnection *connection,
+                         NMDevice *device)
 {
-	return _dispatcher_call (action, connection, device, NULL, NULL, NULL, callback, user_data);
+	return _dispatcher_call (action, TRUE, connection, device, NULL, NULL,
+	                         NULL, NULL, NULL, NULL);
 }
 
-gconstpointer
+/**
+ * nm_dispatcher_call_vpn():
+ * @action: the %DispatcherAction
+ * @connection: the #NMConnection the action applies to
+ * @parent_device: the parent #NMDevice of the VPN connection
+ * @vpn_iface: the IP interface of the VPN tunnel, if any
+ * @vpn_ip4_config: the #NMIP4Config of the VPN connection
+ * @vpn_ip6_config: the #NMIP6Config of the VPN connection
+ * @callback: a caller-supplied callback to execute when done
+ * @user_data: caller-supplied pointer passed to @callback
+ * @out_call_id: on success, a call identifier which can be passed to
+ * nm_dispatcher_call_cancel()
+ *
+ * This method always invokes the dispatcher action asynchronously.  To ignore
+ * the result, pass %NULL to @callback.
+ *
+ * Returns: %TRUE if the action was dispatched, %FALSE on failure
+ */
+gboolean
 nm_dispatcher_call_vpn (DispatcherAction action,
                         NMConnection *connection,
-                        NMDevice *device,
+                        NMDevice *parent_device,
                         const char *vpn_iface,
                         NMIP4Config *vpn_ip4_config,
                         NMIP6Config *vpn_ip6_config,
                         DispatcherFunc callback,
-                        gpointer user_data)
+                        gpointer user_data,
+                        guint *out_call_id)
 {
-	return _dispatcher_call (action, connection, device, vpn_iface, vpn_ip4_config, vpn_ip6_config, callback, user_data);
+	return _dispatcher_call (action, FALSE, connection, parent_device, vpn_iface,
+	                         vpn_ip4_config, vpn_ip6_config, callback, user_data, out_call_id);
+}
+
+/**
+ * nm_dispatcher_call_vpn_sync():
+ * @action: the %DispatcherAction
+ * @connection: the #NMConnection the action applies to
+ * @parent_device: the parent #NMDevice of the VPN connection
+ * @vpn_iface: the IP interface of the VPN tunnel, if any
+ * @vpn_ip4_config: the #NMIP4Config of the VPN connection
+ * @vpn_ip6_config: the #NMIP6Config of the VPN connection
+ *
+ * This method always invokes the dispatcher action synchronously and it may
+ * take a long time to return.
+ *
+ * Returns: %TRUE if the action was dispatched, %FALSE on failure
+ */
+gboolean
+nm_dispatcher_call_vpn_sync (DispatcherAction action,
+                             NMConnection *connection,
+                             NMDevice *parent_device,
+                             const char *vpn_iface,
+                             NMIP4Config *vpn_ip4_config,
+                             NMIP6Config *vpn_ip6_config)
+{
+	return _dispatcher_call (action, TRUE, connection, parent_device, vpn_iface,
+	                         vpn_ip4_config, vpn_ip6_config, NULL, NULL, NULL);
 }
 
 void
-nm_dispatcher_call_cancel (gconstpointer call)
+nm_dispatcher_call_cancel (guint call_id)
 {
-	/* 'call' is really a DispatchInfo pointer, just opaque to callers.
-	 * Look it up in our requests list, but don't access it directly before
-	 * we've made sure it's a valid request,since it may have long since been
-	 * freed.  Canceling just means the callback doesn't get called, so set
-	 * the DispatcherInfo's callback to NULL.
+	DispatchInfo *info;
+
+	_ensure_requests ();
+
+	/* Canceling just means the callback doesn't get called, so set the
+	 * DispatcherInfo's callback to NULL.
 	 */
-	if (g_slist_find (requests, call))
-		((DispatchInfo *) call)->callback = NULL;
+	info = g_hash_table_lookup (requests, GUINT_TO_POINTER (call_id));
+	g_return_if_fail (info);
+
+	if (info && info->callback) {
+		nm_log_dbg (LOGD_DISPATCH, "(%u) cancelling dispatcher callback action",
+		            call_id);
+		info->callback = NULL;
+	}
+}
+
+static void
+dispatcher_dir_changed (GFileMonitor *monitor,
+                        GFile *file,
+                        GFile *other_file,
+                        GFileMonitorEvent event_type,
+                        Monitor *item)
+{
+	const char *name;
+	char *full_name;
+	GDir *dir;
+	GError *error = NULL;
+
+	dir = g_dir_open (item->dir, 0, &error);
+	if (dir) {
+		int errsv = 0;
+
+		item->has_scripts = FALSE;
+		errno = 0;
+		while (!item->has_scripts
+		    && (name = g_dir_read_name (dir))) {
+			full_name = g_build_filename (item->dir, name, NULL);
+			item->has_scripts = g_file_test (full_name, G_FILE_TEST_IS_EXECUTABLE);
+			g_free (full_name);
+		}
+		errsv = errno;
+		g_dir_close (dir);
+		if (item->has_scripts)
+			nm_log_dbg (LOGD_DISPATCH, "dispatcher: %s script directory '%s' has scripts", item->description, item->dir);
+		else if (errsv == 0)
+			nm_log_dbg (LOGD_DISPATCH, "dispatcher: %s script directory '%s' has no scripts", item->description, item->dir);
+		else {
+			nm_log_dbg (LOGD_DISPATCH, "dispatcher: %s script directory '%s' error reading (%s)", item->description, item->dir, strerror (errsv));
+			item->has_scripts = TRUE;
+		}
+	} else {
+		if (g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) {
+			nm_log_dbg (LOGD_DISPATCH, "dispatcher: %s script directory '%s' does not exist", item->description, item->dir);
+			item->has_scripts = FALSE;
+		} else {
+			nm_log_dbg (LOGD_DISPATCH, "dispatcher: %s script directory '%s' error (%s)", item->description, item->dir, error->message);
+			item->has_scripts = TRUE;
+		}
+		g_error_free (error);
+	}
+
+}
+
+void
+nm_dispatcher_init (void)
+{
+	GFile *file;
+	guint i;
+
+	for (i = 0; i < G_N_ELEMENTS (monitors); i++) {
+		file = g_file_new_for_path (monitors[i].dir);
+		monitors[i].monitor = g_file_monitor_directory (file, G_FILE_MONITOR_NONE, NULL, NULL);
+		if (monitors[i].monitor) {
+			g_signal_connect (monitors[i].monitor, "changed", G_CALLBACK (dispatcher_dir_changed), &monitors[i]);
+			dispatcher_dir_changed (monitors[i].monitor, file, NULL, 0, &monitors[i]);
+		}
+		g_object_unref (file);
+	}
 }