PolicyKit: Branch 'wip/js-rule-files'

David Zeuthen david at kemper.freedesktop.org
Mon May 21 11:39:38 PDT 2012


 src/polkitbackend/polkitbackendjsauthority.c       |  446 ++++++++++++++++++++-
 test/data/etc/polkit-1/rules.d/10-testing.rules    |   63 ++
 test/polkitbackend/test-polkitbackendjsauthority.c |   40 +
 3 files changed, 538 insertions(+), 11 deletions(-)

New commits:
commit 28ce5634df0a109d15f2b307e56fbfac92d7c876
Author: David Zeuthen <davidz at redhat.com>
Date:   Mon May 21 14:38:49 2012 -0400

    Add test-cases and 10 second timeout for polkit.spawn()
    
    Signed-off-by: David Zeuthen <davidz at redhat.com>

diff --git a/src/polkitbackend/polkitbackendjsauthority.c b/src/polkitbackend/polkitbackendjsauthority.c
index 7798d45..a7bf50b 100644
--- a/src/polkitbackend/polkitbackendjsauthority.c
+++ b/src/polkitbackend/polkitbackendjsauthority.c
@@ -68,6 +68,18 @@ struct _PolkitBackendJsAuthorityPrivate
   GList *scripts;
 };
 
+static void utils_spawn (const gchar *const  *argv,
+                         guint                timeout_seconds,
+                         GCancellable        *cancellable,
+                         GAsyncReadyCallback  callback,
+                         gpointer             user_data);
+
+gboolean utils_spawn_finish (GAsyncResult   *res,
+                             gint           *out_exit_status,
+                             gchar         **out_standard_output,
+                             gchar         **out_standard_error,
+                             GError        **error);
+
 static void on_dir_monitor_changed (GFileMonitor     *monitor,
                                     GFile            *file,
                                     GFile            *other_file,
@@ -1130,6 +1142,22 @@ get_signal_name (gint signal_number)
   return "UNKNOWN_SIGNAL";
 }
 
+typedef struct
+{
+  GMainLoop *loop;
+  GAsyncResult *res;
+} SpawnData;
+
+static void
+spawn_cb (GObject       *source_object,
+          GAsyncResult  *res,
+          gpointer       user_data)
+{
+  SpawnData *data = user_data;
+  data->res = g_object_ref (res);
+  g_main_loop_quit (data->loop);
+}
+
 static JSBool
 js_polkit_spawn (JSContext  *cx,
                  uintN       js_argc,
@@ -1145,6 +1173,9 @@ js_polkit_spawn (JSContext  *cx,
   JSString *ret_jsstr;
   jsuint array_len;
   gchar **argv = NULL;
+  GMainContext *context = NULL;
+  GMainLoop *loop = NULL;
+  SpawnData data = {0};
   guint n;
 
   if (!JS_ConvertArguments (cx, js_argc, JS_ARGV (cx, vp), "o", &array_object))
@@ -1172,19 +1203,30 @@ js_polkit_spawn (JSContext  *cx,
       JS_free (cx, s);
     }
 
-  /* TODO: set a timeout */
-  if (!g_spawn_sync (NULL, /* working dir */
-                     argv,
-                     NULL, /* envp */
-                     G_SPAWN_SEARCH_PATH,
-                     NULL, NULL, /* child_setup, user_data */
-                     &standard_output,
-                     &standard_error,
-                     &exit_status,
-                     &error))
+  context = g_main_context_new ();
+  loop = g_main_loop_new (context, FALSE);
+
+  g_main_context_push_thread_default (context);
+
+  data.loop = loop;
+  utils_spawn ((const gchar *const *) argv,
+               10, /* timeout_seconds */
+               NULL, /* cancellable */
+               spawn_cb,
+               &data);
+
+  g_main_loop_run (loop);
+
+  g_main_context_pop_thread_default (context);
+
+  if (!utils_spawn_finish (data.res,
+                           &exit_status,
+                           &standard_output,
+                           &standard_error,
+                           &error))
     {
       JS_ReportError (cx,
-                      "Failed to spawn helper: %s (%s, %d)",
+                      "Error spawning helper: %s (%s, %d)",
                       error->message, g_quark_to_string (error->domain), error->code);
       g_clear_error (&error);
       goto out;
@@ -1223,6 +1265,11 @@ js_polkit_spawn (JSContext  *cx,
   g_strfreev (argv);
   g_free (standard_output);
   g_free (standard_error);
+  g_clear_object (&data.res);
+  if (loop != NULL)
+    g_main_loop_unref (loop);
+  if (context != NULL)
+    g_main_context_unref (context);
   return ret;
 }
 
@@ -1266,3 +1313,380 @@ js_polkit_user_is_in_netgroup (JSContext  *cx,
   return ret;
 }
 
+
+
+/* ---------------------------------------------------------------------------------------------------- */
+
+typedef struct
+{
+  GSimpleAsyncResult *simple; /* borrowed reference */
+  GMainContext *main_context; /* may be NULL */
+
+  GCancellable *cancellable;  /* may be NULL */
+  gulong cancellable_handler_id;
+
+  GPid child_pid;
+  gint child_stdout_fd;
+  gint child_stderr_fd;
+
+  GIOChannel *child_stdout_channel;
+  GIOChannel *child_stderr_channel;
+
+  GSource *child_watch_source;
+  GSource *child_stdout_source;
+  GSource *child_stderr_source;
+
+  guint timeout_seconds;
+  gboolean timed_out;
+  GSource *timeout_source;
+
+  GString *child_stdout;
+  GString *child_stderr;
+
+  gint exit_status;
+} UtilsSpawnData;
+
+static void
+utils_child_watch_from_release_cb (GPid     pid,
+                                   gint     status,
+                                   gpointer user_data)
+{
+}
+
+static void
+utils_spawn_data_free (UtilsSpawnData *data)
+{
+  if (data->timeout_source != NULL)
+    {
+      g_source_destroy (data->timeout_source);
+      data->timeout_source = NULL;
+    }
+
+  /* Nuke the child, if necessary */
+  if (data->child_watch_source != NULL)
+    {
+      g_source_destroy (data->child_watch_source);
+      data->child_watch_source = NULL;
+    }
+
+  if (data->child_pid != 0)
+    {
+      GSource *source;
+      kill (data->child_pid, SIGTERM);
+      /* OK, we need to reap for the child ourselves - we don't want
+       * to use waitpid() because that might block the calling
+       * thread (the child might handle SIGTERM and use several
+       * seconds for cleanup/rollback).
+       *
+       * So we use GChildWatch instead.
+       *
+       * Avoid taking a references to ourselves. but note that we need
+       * to pass the GSource so we can nuke it once handled.
+       */
+      source = g_child_watch_source_new (data->child_pid);
+      g_source_set_callback (source,
+                             (GSourceFunc) utils_child_watch_from_release_cb,
+                             source,
+                             (GDestroyNotify) g_source_destroy);
+      g_source_attach (source, data->main_context);
+      g_source_unref (source);
+      data->child_pid = 0;
+    }
+
+  if (data->child_stdout != NULL)
+    {
+      g_string_free (data->child_stdout, TRUE);
+      data->child_stdout = NULL;
+    }
+
+  if (data->child_stderr != NULL)
+    {
+      g_string_free (data->child_stderr, TRUE);
+      data->child_stderr = NULL;
+    }
+
+  if (data->child_stdout_channel != NULL)
+    {
+      g_io_channel_unref (data->child_stdout_channel);
+      data->child_stdout_channel = NULL;
+    }
+  if (data->child_stderr_channel != NULL)
+    {
+      g_io_channel_unref (data->child_stderr_channel);
+      data->child_stderr_channel = NULL;
+    }
+
+  if (data->child_stdout_source != NULL)
+    {
+      g_source_destroy (data->child_stdout_source);
+      data->child_stdout_source = NULL;
+    }
+  if (data->child_stderr_source != NULL)
+    {
+      g_source_destroy (data->child_stderr_source);
+      data->child_stderr_source = NULL;
+    }
+
+  if (data->child_stdout_fd != -1)
+    {
+      g_warn_if_fail (close (data->child_stdout_fd) == 0);
+      data->child_stdout_fd = -1;
+    }
+  if (data->child_stderr_fd != -1)
+    {
+      g_warn_if_fail (close (data->child_stderr_fd) == 0);
+      data->child_stderr_fd = -1;
+    }
+
+  if (data->cancellable_handler_id > 0)
+    {
+      g_cancellable_disconnect (data->cancellable, data->cancellable_handler_id);
+      data->cancellable_handler_id = 0;
+    }
+
+  if (data->main_context != NULL)
+    g_main_context_unref (data->main_context);
+
+  if (data->cancellable != NULL)
+    g_object_unref (data->cancellable);
+
+  g_slice_free (UtilsSpawnData, data);
+}
+
+/* called in the thread where @cancellable was cancelled */
+static void
+utils_on_cancelled (GCancellable *cancellable,
+                    gpointer      user_data)
+{
+  UtilsSpawnData *data = user_data;
+  GError *error;
+
+  error = NULL;
+  g_warn_if_fail (g_cancellable_set_error_if_cancelled (cancellable, &error));
+  g_simple_async_result_take_error (data->simple, error);
+  g_simple_async_result_complete_in_idle (data->simple);
+  g_object_unref (data->simple);
+}
+
+static gboolean
+utils_read_child_stderr (GIOChannel *channel,
+                         GIOCondition condition,
+                         gpointer user_data)
+{
+  UtilsSpawnData *data = user_data;
+  gchar buf[1024];
+  gsize bytes_read;
+
+  g_io_channel_read_chars (channel, buf, sizeof buf, &bytes_read, NULL);
+  g_string_append_len (data->child_stderr, buf, bytes_read);
+  return TRUE;
+}
+
+static gboolean
+utils_read_child_stdout (GIOChannel *channel,
+                         GIOCondition condition,
+                         gpointer user_data)
+{
+  UtilsSpawnData *data = user_data;
+  gchar buf[1024];
+  gsize bytes_read;
+
+  g_io_channel_read_chars (channel, buf, sizeof buf, &bytes_read, NULL);
+  g_string_append_len (data->child_stdout, buf, bytes_read);
+  return TRUE;
+}
+
+static void
+utils_child_watch_cb (GPid     pid,
+                      gint     status,
+                      gpointer user_data)
+{
+  UtilsSpawnData *data = user_data;
+  gchar *buf;
+  gsize buf_size;
+
+  if (g_io_channel_read_to_end (data->child_stdout_channel, &buf, &buf_size, NULL) == G_IO_STATUS_NORMAL)
+    {
+      g_string_append_len (data->child_stdout, buf, buf_size);
+      g_free (buf);
+    }
+  if (g_io_channel_read_to_end (data->child_stderr_channel, &buf, &buf_size, NULL) == G_IO_STATUS_NORMAL)
+    {
+      g_string_append_len (data->child_stderr, buf, buf_size);
+      g_free (buf);
+    }
+
+  data->exit_status = status;
+
+  /* ok, child watch is history, make sure we don't free it in spawn_data_free() */
+  data->child_pid = 0;
+  data->child_watch_source = NULL;
+
+  /* we're done */
+  g_simple_async_result_complete_in_idle (data->simple);
+  g_object_unref (data->simple);
+}
+
+static gboolean
+utils_timeout_cb (gpointer user_data)
+{
+  UtilsSpawnData *data = user_data;
+
+  data->timed_out = TRUE;
+
+  /* ok, timeout is history, make sure we don't free it in spawn_data_free() */
+  data->timeout_source = NULL;
+
+  /* we're done */
+  g_simple_async_result_complete_in_idle (data->simple);
+  g_object_unref (data->simple);
+
+  return FALSE; /* remove source */
+}
+
+static void
+utils_spawn (const gchar *const  *argv,
+             guint                timeout_seconds,
+             GCancellable        *cancellable,
+             GAsyncReadyCallback  callback,
+             gpointer             user_data)
+{
+  UtilsSpawnData *data;
+  GError *error;
+
+  data = g_slice_new0 (UtilsSpawnData);
+  data->timeout_seconds = timeout_seconds;
+  data->simple = g_simple_async_result_new (NULL,
+                                            callback,
+                                            user_data,
+                                            utils_spawn);
+  data->main_context = g_main_context_get_thread_default ();
+  if (data->main_context != NULL)
+    g_main_context_ref (data->main_context);
+
+  data->cancellable = cancellable != NULL ? g_object_ref (cancellable) : NULL;
+
+  data->child_stdout = g_string_new (NULL);
+  data->child_stderr = g_string_new (NULL);
+  data->child_stdout_fd = -1;
+  data->child_stderr_fd = -1;
+
+  /* the life-cycle of UtilsSpawnData is tied to its GSimpleAsyncResult */
+  g_simple_async_result_set_op_res_gpointer (data->simple, data, (GDestroyNotify) utils_spawn_data_free);
+
+  error = NULL;
+  if (data->cancellable != NULL)
+    {
+      /* could already be cancelled */
+      error = NULL;
+      if (g_cancellable_set_error_if_cancelled (data->cancellable, &error))
+        {
+          g_simple_async_result_take_error (data->simple, error);
+          g_simple_async_result_complete_in_idle (data->simple);
+          g_object_unref (data->simple);
+          goto out;
+        }
+
+      data->cancellable_handler_id = g_cancellable_connect (data->cancellable,
+                                                            G_CALLBACK (utils_on_cancelled),
+                                                            data,
+                                                            NULL);
+    }
+
+  error = NULL;
+  if (!g_spawn_async_with_pipes (NULL, /* working directory */
+                                 (gchar **) argv,
+                                 NULL, /* envp */
+                                 G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD,
+                                 NULL, /* child_setup */
+                                 NULL, /* child_setup's user_data */
+                                 &(data->child_pid),
+                                 NULL, /* gint *stdin_fd */
+                                 &(data->child_stdout_fd),
+                                 &(data->child_stderr_fd),
+                                 &error))
+    {
+      g_prefix_error (&error, "Error spawning: ");
+      g_simple_async_result_take_error (data->simple, error);
+      g_simple_async_result_complete_in_idle (data->simple);
+      g_object_unref (data->simple);
+      goto out;
+    }
+
+  if (timeout_seconds > 0)
+    {
+      data->timeout_source = g_timeout_source_new_seconds (timeout_seconds);
+      g_source_set_priority (data->timeout_source, G_PRIORITY_DEFAULT);
+      g_source_set_callback (data->timeout_source, utils_timeout_cb, data, NULL);
+      g_source_attach (data->timeout_source, data->main_context);
+      g_source_unref (data->timeout_source);
+    }
+
+  data->child_watch_source = g_child_watch_source_new (data->child_pid);
+  g_source_set_callback (data->child_watch_source, (GSourceFunc) utils_child_watch_cb, data, NULL);
+  g_source_attach (data->child_watch_source, data->main_context);
+  g_source_unref (data->child_watch_source);
+
+  data->child_stdout_channel = g_io_channel_unix_new (data->child_stdout_fd);
+  g_io_channel_set_flags (data->child_stdout_channel, G_IO_FLAG_NONBLOCK, NULL);
+  data->child_stdout_source = g_io_create_watch (data->child_stdout_channel, G_IO_IN);
+  g_source_set_callback (data->child_stdout_source, (GSourceFunc) utils_read_child_stdout, data, NULL);
+  g_source_attach (data->child_stdout_source, data->main_context);
+  g_source_unref (data->child_stdout_source);
+
+  data->child_stderr_channel = g_io_channel_unix_new (data->child_stderr_fd);
+  g_io_channel_set_flags (data->child_stderr_channel, G_IO_FLAG_NONBLOCK, NULL);
+  data->child_stderr_source = g_io_create_watch (data->child_stderr_channel, G_IO_IN);
+  g_source_set_callback (data->child_stderr_source, (GSourceFunc) utils_read_child_stderr, data, NULL);
+  g_source_attach (data->child_stderr_source, data->main_context);
+  g_source_unref (data->child_stderr_source);
+
+ out:
+  ;
+}
+
+gboolean
+utils_spawn_finish (GAsyncResult   *res,
+                    gint           *out_exit_status,
+                    gchar         **out_standard_output,
+                    gchar         **out_standard_error,
+                    GError        **error)
+{
+  GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (res);
+  UtilsSpawnData *data;
+  gboolean ret = FALSE;
+
+  g_return_val_if_fail (G_IS_ASYNC_RESULT (res), FALSE);
+  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+  g_warn_if_fail (g_simple_async_result_get_source_tag (simple) == utils_spawn);
+
+  if (g_simple_async_result_propagate_error (simple, error))
+    goto out;
+
+  data = g_simple_async_result_get_op_res_gpointer (simple);
+
+  if (data->timed_out)
+    {
+      g_set_error (error,
+                   G_IO_ERROR,
+                   G_IO_ERROR_TIMED_OUT,
+                   "Timed out after %d seconds",
+                   data->timeout_seconds);
+      goto out;
+    }
+
+  if (out_exit_status != NULL)
+    *out_exit_status = data->exit_status;
+
+  if (out_standard_output != NULL)
+    *out_standard_output = g_strdup (data->child_stdout->str);
+
+  if (out_standard_error != NULL)
+    *out_standard_error = g_strdup (data->child_stderr->str);
+
+  ret = TRUE;
+
+ out:
+  return ret;
+}
diff --git a/test/data/etc/polkit-1/rules.d/10-testing.rules b/test/data/etc/polkit-1/rules.d/10-testing.rules
index 0cad62c..4a35e48 100644
--- a/test/data/etc/polkit-1/rules.d/10-testing.rules
+++ b/test/data/etc/polkit-1/rules.d/10-testing.rules
@@ -71,3 +71,66 @@ polkit.addRule(function(action, subject, details) {
             return "no";
     }
 });
+
+// ---------------------------------------------------------------------
+// spawning
+
+polkit.addRule(function(action, subject, details) {
+    if (action == "net.company.spawning.non_existing_helper") {
+        try {
+            polkit.spawn(["/path/to/non/existing/helper"]);
+            return "no";
+        } catch (error) {
+            return "yes";
+        }
+    }
+});
+
+polkit.addRule(function(action, subject, details) {
+    if (action == "net.company.spawning.successful_helper") {
+        try {
+            polkit.spawn(["/bin/true"]);
+            return "yes";
+        } catch (error) {
+            return "no";
+        }
+    }
+});
+
+polkit.addRule(function(action, subject, details) {
+    if (action == "net.company.spawning.failing_helper") {
+        try {
+            polkit.spawn(["/bin/false"]);
+            return "no";
+        } catch (error) {
+            return "yes";
+        }
+    }
+});
+
+polkit.addRule(function(action, subject, details) {
+    if (action == "net.company.spawning.helper_with_output") {
+        try {
+            var out = polkit.spawn(["echo", "-n", "-e", "Hello\nWorld"]);
+            if (out == "Hello\nWorld")
+                return "yes";
+            else
+                return "no";
+        } catch (error) {
+            return "no";
+        }
+    }
+});
+
+polkit.addRule(function(action, subject, details) {
+    if (action == "net.company.spawning.helper_timeout") {
+        try {
+            polkit.spawn(["sleep", "20"]);
+            return "no";
+        } catch (error) {
+            if (error == "Error: Error spawning helper: Timed out after 10 seconds (g-io-error-quark, 24)")
+                return "yes";
+            return "no";
+        }
+    }
+});
diff --git a/test/polkitbackend/test-polkitbackendjsauthority.c b/test/polkitbackend/test-polkitbackendjsauthority.c
index f81c7fb..948cbc1 100644
--- a/test/polkitbackend/test-polkitbackendjsauthority.c
+++ b/test/polkitbackend/test-polkitbackendjsauthority.c
@@ -23,6 +23,7 @@
 
 #include "glib.h"
 
+#include <locale.h>
 #include <polkit/polkit.h>
 #include <polkitbackend/polkitbackendjsauthority.h>
 #include <polkittesthelper.h>
@@ -246,6 +247,43 @@ static const RulesTestCase rules_test_cases[] = {
     POLKIT_IMPLICIT_AUTHORIZATION_NOT_AUTHORIZED,
     NULL
   },
+
+  /* spawning */
+  {
+    "spawning_non_existing_helper",
+    "net.company.spawning.non_existing_helper",
+    "unix-user:root",
+    POLKIT_IMPLICIT_AUTHORIZATION_AUTHORIZED,
+    NULL
+  },
+  {
+    "spawning_successful_helper",
+    "net.company.spawning.successful_helper",
+    "unix-user:root",
+    POLKIT_IMPLICIT_AUTHORIZATION_AUTHORIZED,
+    NULL
+  },
+  {
+    "spawning_failing_helper",
+    "net.company.spawning.failing_helper",
+    "unix-user:root",
+    POLKIT_IMPLICIT_AUTHORIZATION_AUTHORIZED,
+    NULL
+  },
+  {
+    "spawning_helper_with_output",
+    "net.company.spawning.helper_with_output",
+    "unix-user:root",
+    POLKIT_IMPLICIT_AUTHORIZATION_AUTHORIZED,
+    NULL
+  },
+  {
+    "spawning_helper_timeout",
+    "net.company.spawning.helper_timeout",
+    "unix-user:root",
+    POLKIT_IMPLICIT_AUTHORIZATION_AUTHORIZED,
+    NULL
+  },
 };
 
 /* ---------------------------------------------------------------------------------------------------- */
@@ -310,6 +348,8 @@ main (int argc, char *argv[])
 {
   GIOExtensionPoint *ep;
 
+  setlocale (LC_ALL, "");
+
   g_type_init ();
   g_test_init (&argc, &argv, NULL);
   //polkit_test_redirect_logs ();


More information about the hal-commit mailing list