Add assertions about which thread we are running in.
[ovirt-viewer.git] / wui_thread.c
index 0db43c8..94631b6 100644 (file)
 
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
 #include <assert.h>
 
 #include <glib.h>
+#include <glib/gprintf.h>
+
+#include <curl/curl.h>
 
 #include "internal.h"
 
 /* Private functions. */
 static gpointer wui_thread (gpointer data);
 static void wui_thread_send_quit (void);
+static void do_curl_init (void);
+static void write_fn_start_capture (void);
+static char *write_fn_finish_capture (void);
+static void write_fn_discard_capture_buffer (void);
 static gboolean do_connect (void);
 static gboolean do_login (void);
-static void refresh_vm_list (void);
+static gboolean refresh_vm_list (void);
+static void parse_vmlist_from_xml (const char *xml);
 
 /* Messages (main thread -> WUI thread only).
  *
@@ -55,6 +64,7 @@ struct message {
 
 /* Start the WUI thread.  See main() for explanation of the threading model. */
 static GThread *wui_gthread = NULL;
+static GThread *main_gthread = NULL;
 static GAsyncQueue *wui_thread_queue = NULL;
 
 void
@@ -64,6 +74,10 @@ start_wui_thread (void)
 
   DEBUG ("starting the WUI thread");
 
+  assert (wui_gthread == NULL);
+
+  main_gthread = g_thread_self ();
+
   /* Create the message queue for main -> WUI thread communications. */
   wui_thread_queue = g_async_queue_new ();
 
@@ -79,7 +93,9 @@ void
 stop_wui_thread (void)
 {
   DEBUG ("stopping the WUI thread");
-  assert (wui_gthread);
+
+  assert (wui_gthread != NULL);
+  ASSERT_IS_MAIN_THREAD ();
 
   /* Send a quit message then wait for the WUI thread to join.
    *
@@ -98,12 +114,32 @@ stop_wui_thread (void)
   wui_gthread = NULL;
 }
 
+void
+assert_is_wui_thread (const char *filename, int lineno)
+{
+  if (g_thread_self () != wui_gthread) {
+    fprintf (stderr, "%s:%d: internal error: this function should only run in the context of the WUI thread\n", filename, lineno);
+    abort ();
+  }
+}
+
+void
+assert_is_main_thread (const char *filename, int lineno)
+{
+  if (g_thread_self () != main_gthread) {
+    fprintf (stderr, "%s:%d: internal error: this function should only run in the context of the main thread\n", filename, lineno);
+    abort ();
+  }
+}
+
 /* Send the quit message to the WUI thread. */
 static void
 wui_thread_send_quit (void)
 {
   struct message *msg;
 
+  ASSERT_IS_MAIN_THREAD ();
+
   msg = g_new (struct message, 1);
   msg->type = QUIT;
   g_async_queue_push (wui_thread_queue, msg);
@@ -115,6 +151,8 @@ wui_thread_send_connect (const char *uri)
 {
   struct message *msg;
 
+  ASSERT_IS_MAIN_THREAD ();
+
   msg = g_new (struct message, 1);
   msg->type = CONNECT;
   msg->str1 = g_strdup (uri);
@@ -127,6 +165,8 @@ wui_thread_send_disconnect (void)
 {
   struct message *msg;
 
+  ASSERT_IS_MAIN_THREAD ();
+
   msg = g_new (struct message, 1);
   msg->type = DISCONNECT;
   g_async_queue_push (wui_thread_queue, msg);
@@ -138,6 +178,8 @@ wui_thread_send_login (const char *username, const char *password)
 {
   struct message *msg;
 
+  ASSERT_IS_MAIN_THREAD ();
+
   msg = g_new (struct message, 1);
   msg->type = LOGIN;
   msg->str1 = g_strdup (username);
@@ -151,6 +193,8 @@ wui_thread_send_refresh_vm_list (void)
 {
   struct message *msg;
 
+  ASSERT_IS_MAIN_THREAD ();
+
   msg = g_new (struct message, 1);
   msg->type = REFRESH_VM_LIST;
   g_async_queue_push (wui_thread_queue, msg);
@@ -174,6 +218,8 @@ static void set_busy (gboolean);
 
 /* The private state of the WUI thread. */
 static int secs_between_refresh = 60;
+static CURL *curl = NULL;
+static char curl_error_buffer[CURL_ERROR_SIZE];
 static char *uri = NULL;
 static char *username = NULL;
 static char *password = NULL;
@@ -194,6 +240,8 @@ wui_thread (gpointer _queue)
 
   DEBUG ("WUI thread starting up");
 
+  ASSERT_IS_WUI_THREAD ();
+
   g_async_queue_ref (queue);
 
   /* In the thread's loop we check for new instructions from the main
@@ -215,7 +263,7 @@ wui_thread (gpointer _queue)
     msg = (struct message *) _msg;
 
     if (msg) {
-      DEBUG ("received message %d", msg->type);
+      DEBUG ("received message with msg->type = %d", msg->type);
       quit = process_message (msg);
       /* Don't free any strings in the message - we've saved them. */
       g_free (msg);
@@ -243,25 +291,46 @@ wui_thread (gpointer _queue)
 static void
 set_connected (gboolean new_connected)
 {
+  ASSERT_IS_WUI_THREAD ();
+
   g_static_mutex_lock (&state_mutex);
   connected = new_connected;
   g_static_mutex_unlock (&state_mutex);
+
+  if (connected)
+    g_idle_add (main_connected, NULL);
+  else
+    g_idle_add (main_disconnected, NULL);
 }
 
 static void
 set_logged_in (gboolean new_logged_in)
 {
+  ASSERT_IS_WUI_THREAD ();
+
   g_static_mutex_lock (&state_mutex);
   logged_in = new_logged_in;
   g_static_mutex_unlock (&state_mutex);
+
+  if (logged_in)
+    g_idle_add (main_logged_in, NULL);
+  else
+    g_idle_add (main_logged_out, NULL);
 }
 
 static void
 set_busy (gboolean new_busy)
 {
+  ASSERT_IS_WUI_THREAD ();
+
   g_static_mutex_lock (&state_mutex);
   busy = new_busy;
   g_static_mutex_unlock (&state_mutex);
+
+  if (busy)
+    g_idle_add (main_busy, NULL);
+  else
+    g_idle_add (main_idle, NULL);
 }
 
 /* The main thread should call these functions to get the WUI thread's
@@ -272,6 +341,8 @@ wui_thread_is_connected (void)
 {
   gboolean ret;
 
+  ASSERT_IS_MAIN_THREAD ();
+
   g_static_mutex_lock (&state_mutex);
   ret = connected;
   g_static_mutex_unlock (&state_mutex);
@@ -283,6 +354,8 @@ wui_thread_is_logged_in (void)
 {
   gboolean ret;
 
+  ASSERT_IS_MAIN_THREAD ();
+
   g_static_mutex_lock (&state_mutex);
   ret = logged_in;
   g_static_mutex_unlock (&state_mutex);
@@ -294,6 +367,8 @@ wui_thread_is_busy (void)
 {
   gboolean ret;
 
+  ASSERT_IS_MAIN_THREAD ();
+
   g_static_mutex_lock (&state_mutex);
   ret = busy;
   g_static_mutex_unlock (&state_mutex);
@@ -304,8 +379,12 @@ wui_thread_is_busy (void)
 static gboolean
 process_message (struct message *msg)
 {
+  ASSERT_IS_WUI_THREAD ();
+
   switch (msg->type) {
   case QUIT:
+    write_fn_discard_capture_buffer ();
+    if (curl) curl_easy_cleanup (curl);
     if (uri) g_free (uri);
     if (username) g_free (username);
     if (password) g_free (password);
@@ -314,6 +393,9 @@ process_message (struct message *msg)
     return 1;
 
   case CONNECT:
+    write_fn_discard_capture_buffer ();
+    if (curl) curl_easy_cleanup (curl);
+    do_curl_init ();
     if (uri) g_free (uri);
     uri = msg->str1;
 
@@ -327,9 +409,15 @@ process_message (struct message *msg)
 
   case DISCONNECT:
     /* This just forgets the state. REST is connectionless. */
+    write_fn_discard_capture_buffer ();
+    if (curl) curl_easy_cleanup (curl);
+    curl = NULL;
     if (uri) g_free (uri);
+    uri = NULL;
     if (username) g_free (username);
+    username = NULL;
     if (password) g_free (password);
+    password = NULL;
     set_connected (FALSE);
     set_logged_in (FALSE);
     break;
@@ -340,10 +428,15 @@ process_message (struct message *msg)
     if (password) g_free (password);
     password = msg->str2;
 
+    /* If we're not connected, this message just updates the
+     * username and password.  Otherwise if we are connected,
+     * try to login and grab the initial list of VMs.
+     */
     if (connected) {
       if (do_login ()) {
        set_logged_in (TRUE);
-       refresh_vm_list ();
+       if (refresh_vm_list ())
+         secs_between_refresh = 60;
       } else {
        set_logged_in (FALSE);
       }
@@ -351,8 +444,10 @@ process_message (struct message *msg)
     break;
 
   case REFRESH_VM_LIST:
-    refresh_vm_list ();
-    secs_between_refresh = 60;
+    if (connected && logged_in) {
+      refresh_vm_list ();
+      secs_between_refresh = 60;
+    }
     break;
 
   default:
@@ -363,27 +458,418 @@ process_message (struct message *msg)
   return 0;
 }
 
-/* Try to connect to the current URI.  Returns true on success. */
+/* Macro for easy handling of CURL errors. */
+#define CURL_CHECK_ERROR(fn, args)                                     \
+  ({                                                                   \
+    CURLcode __r = fn args;                                            \
+    if (__r != CURLE_OK) {                                             \
+      fprintf (stderr, "%s: %s\n", #fn, curl_easy_strerror (__r));     \
+    }                                                                  \
+    __r;                                                               \
+  })
+
+/* Libcurl has a really crufty method for handling HTTP headers and
+ * data.  We set these functions as callbacks, because the default
+ * callback functions print the data out to stderr.  In order to
+ * capture the data, we have to keep state here.
+ */
+
+static char *write_fn_buffer = NULL;
+static ssize_t write_fn_len = -1;
+
+static size_t
+write_fn (void *ptr, size_t size, size_t nmemb, void *stream)
+{
+  int bytes = size * nmemb;
+  int old_start;
+
+  ASSERT_IS_WUI_THREAD ();
+
+  if (write_fn_len >= 0) {     /* We're capturing. */
+    old_start = write_fn_len;
+    write_fn_len += bytes;
+    write_fn_buffer = g_realloc (write_fn_buffer, write_fn_len);
+    memcpy (write_fn_buffer + old_start, ptr, bytes);
+  }
+
+  return bytes;
+}
+
+/* Start capturing HTTP response data. */
+static void
+write_fn_start_capture (void)
+{
+  ASSERT_IS_WUI_THREAD ();
+
+  write_fn_discard_capture_buffer ();
+  write_fn_buffer = NULL;
+  write_fn_len = 0;
+}
+
+/* Finish capture and return the capture buffer.  Caller must free. */
+static char *
+write_fn_finish_capture (void)
+{
+  char *ret = write_fn_buffer;
+
+  ASSERT_IS_WUI_THREAD ();
+
+  write_fn_buffer = NULL;
+  write_fn_len = -1;
+  return ret;
+}
+
+/* Stop capturing and discard the capture buffer, if any. */
+static void
+write_fn_discard_capture_buffer (void)
+{
+  ASSERT_IS_WUI_THREAD ();
+
+  g_free (write_fn_buffer);
+  write_fn_buffer = NULL;
+  write_fn_len = -1;
+}
+
+static size_t
+header_fn (void *ptr, size_t size, size_t nmemb, void *stream)
+{
+  int bytes = size * nmemb;
+
+  ASSERT_IS_WUI_THREAD ();
+
+  return bytes;
+}
+
+/* Called from the message loop to initialize the CURL handle. */
+static void
+do_curl_init (void)
+{
+  DEBUG ("initializing libcurl");
+
+  ASSERT_IS_WUI_THREAD ();
+
+  curl = curl_easy_init ();
+  if (!curl) {            /* This is probably quite bad, so abort. */
+    DEBUG ("curl_easy_init failed");
+    abort ();
+  }
+
+  CURL_CHECK_ERROR (curl_easy_setopt,
+                   (curl, CURLOPT_CAINFO, cainfo));
+  CURL_CHECK_ERROR (curl_easy_setopt,
+                   (curl, CURLOPT_SSL_VERIFYHOST, check_cert ? 2 : 0));
+  CURL_CHECK_ERROR (curl_easy_setopt,
+                   (curl, CURLOPT_SSL_VERIFYPEER, check_cert ? 1 : 0));
+
+  CURL_CHECK_ERROR (curl_easy_setopt,
+                   (curl, CURLOPT_WRITEFUNCTION, write_fn));
+  CURL_CHECK_ERROR (curl_easy_setopt,
+                   (curl, CURLOPT_HEADERFUNCTION, header_fn));
+
+  /* This enables error messages in curl_easy_perform. */
+  CURL_CHECK_ERROR (curl_easy_setopt,
+                   (curl, CURLOPT_ERRORBUFFER, curl_error_buffer));
+
+  /* This enables cookie handling, using an internal cookiejar. */
+  CURL_CHECK_ERROR (curl_easy_setopt,
+                   (curl, CURLOPT_COOKIEFILE, ""));
+}
+
+/* Called from the message loop.  Try to connect to the current URI.
+ * Returns true on success.
+ */
 static gboolean
 do_connect (void)
 {
+  long code = 0;
+  CURLcode r;
+  char *error_str;
+
   DEBUG ("connecting to uri %s", uri);
-  return FALSE;
+  ASSERT_IS_WUI_THREAD ();
+
+  /* Set the URI for libcurl. */
+  CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_URL, uri));
+
+  /* Try to fetch the URI. */
+  r = CURL_CHECK_ERROR (curl_easy_perform, (curl));
+  if (r != CURLE_OK) {
+    /* Signal an error back to the main thread. */
+    error_str = g_strdup (curl_easy_strerror (r));
+    g_idle_add (main_connection_error, error_str);
+    return FALSE;
+  }
+
+  CURL_CHECK_ERROR (curl_easy_getinfo, (curl, CURLINFO_RESPONSE_CODE, &code));
+  DEBUG ("HTTP return code is %ld", code);
+  if (code != 200 && code != 302 && code != 401) {
+    /* XXX If only glib had g_asprintf. */
+    error_str = g_strdup ("unexpected HTTP return code from server");
+    g_idle_add (main_connection_error, error_str);
+    return FALSE;
+  }
+
+  return TRUE;
 }
 
-/* Try to login to URI/login with the username and password given.
- * Returns true on success.
+/* Called from the message loop.  Try to login to 'URI/login' with the
+ * current username and password.  Returns true on success.
  */
 static gboolean
 do_login (void)
 {
+  int len;
+  char *login_uri;
+  char *userpwd;
+  char *error_str;
+  CURLcode r;
+  long code = 0;
+
   DEBUG ("logging in with username %s, password *****", username);
-  return FALSE;
+  ASSERT_IS_WUI_THREAD ();
+
+  /* Generate the login URI from the base URI. */
+  len = strlen (uri) + 6 + 1;
+  login_uri = g_alloca (len);
+  snprintf (login_uri, len, "%s/login", uri);
+
+  DEBUG ("login URI %s", login_uri);
+
+  /* Set the URI for libcurl. */
+  CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_URL, login_uri));
+
+  /* Construct the username:password for CURL. */
+  len = strlen (username) + strlen (password) + 2;
+  userpwd = g_alloca (len);
+  snprintf (userpwd, len, "%s:%s", username, password);
+
+  CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_USERPWD, userpwd));
+
+  /* HTTP Basic authentication is OK since we should be sending
+   * this only over HTTPS.
+   */
+  CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC));
+
+  /* Follow redirects. */
+  CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_FOLLOWLOCATION, (long) 1));
+  CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_MAXREDIRS, (long) 10));
+
+  /* Try to fetch the URI. */
+  r = CURL_CHECK_ERROR (curl_easy_perform, (curl));
+  if (r != CURLE_OK) {
+    /* Signal an error back to the main thread. */
+    error_str = g_strdup (curl_easy_strerror (r));
+    g_idle_add (main_login_error, error_str);
+    return FALSE;
+  }
+
+  CURL_CHECK_ERROR (curl_easy_getinfo, (curl, CURLINFO_RESPONSE_CODE, &code));
+  DEBUG ("HTTP return code is %ld", code);
+  switch (code)
+    {
+    case 200:
+      DEBUG ("login was successful");
+      return TRUE;
+
+    case 401:
+      error_str = g_strdup ("server rejected the username or password");
+      g_idle_add (main_login_error, error_str);
+      return FALSE;
+
+    default:
+      /* XXX If only glib had g_asprintf. */
+      error_str = g_strdup ("unexpected HTTP return code from server");
+      g_idle_add (main_login_error, error_str);
+      return FALSE;
+    }
 }
 
-/* Refresh the list of VMs. */
-static void
+/* Called from the message loop.  Refresh the list of VMs. */
+static gboolean
 refresh_vm_list (void)
 {
+  int len;
+  char *vms_uri;
+  char *error_str;
+  CURLcode r;
+  long code = 0;
+  char *xml;
+
   DEBUG ("refreshing list of VMs");
+  ASSERT_IS_WUI_THREAD ();
+
+  /* Generate the vms URI from the base URI. */
+  len = strlen (uri) + 4 + 1;
+  vms_uri = g_alloca (len);
+  snprintf (vms_uri, len, "%s/vms", uri);
+
+  DEBUG ("vms URI %s", vms_uri);
+
+  /* Set the URI for libcurl. */
+  CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_URL, vms_uri));
+
+  /* We want to capture the output, so tell our write function
+   * to place the output into a buffer.
+   */
+  write_fn_start_capture ();
+
+  /* Try to fetch the URI. */
+  r = CURL_CHECK_ERROR (curl_easy_perform, (curl));
+  if (r != CURLE_OK) {
+    /* Signal an error back to the main thread. */
+    error_str = g_strdup (curl_easy_strerror (r));
+    g_idle_add (main_login_error, error_str);
+    return FALSE;
+  }
+
+  CURL_CHECK_ERROR (curl_easy_getinfo, (curl, CURLINFO_RESPONSE_CODE, &code));
+  DEBUG ("HTTP return code is %ld", code);
+  switch (code)
+    {
+    case 200: break;
+
+      /* Hmm - even though we previously logged in, the server is
+       * rejecting our attempts now with an authorization error.
+       * We move to the logged out state.
+       */
+    case 401:
+      set_logged_in (FALSE);
+      error_str = g_strdup ("server rejected the username or password");
+      g_idle_add (main_login_error, error_str);
+      return FALSE;
+
+    default:
+      /* XXX If only glib had g_asprintf. */
+      error_str = g_strdup ("unexpected HTTP return code from server");
+      g_idle_add (main_status_error, error_str);
+      return FALSE;
+    }
+
+  /* If we got here then we appear to have a correct
+   * XML document describing the list of VMs.
+   */
+  secs_between_refresh <<= 1;
+
+  xml = write_fn_finish_capture ();
+
+  /*DEBUG ("XML from /vms =\n%s", xml);*/
+  parse_vmlist_from_xml (xml);
+  g_free (xml);
+
+  return TRUE;
+}
+
+/* Functions to deal with the list of VMs, parsing it from the XML, etc.
+ *
+ * A vmlist is a GSList (glib singly-linked list) of vm structures.
+ * The caller must call free_vmlist to free up this list correctly.
+ */
+
+static void
+free_vm (gpointer _vm, gpointer data)
+{
+  struct vm *vm = (struct vm *) _vm;
+
+  g_free (vm->description);
+  g_free (vm->state);
+  g_free (vm->uuid);
+  g_free (vm->mac_addr);
+  g_free (vm);
+}
+
+void
+free_vmlist (GSList *vmlist)
+{
+  g_slist_foreach (vmlist, free_vm, NULL);
+  g_slist_free (vmlist);
+}
+
+static struct vm *
+copy_vm (struct vm *vm)
+{
+  struct vm *vm2;
+
+  vm2 = g_memdup (vm, sizeof (*vm));
+  vm2->description = g_strdup (vm->description);
+  vm2->state = g_strdup (vm->state);
+  vm2->uuid = g_strdup (vm->uuid);
+  vm2->mac_addr = g_strdup (vm->mac_addr);
+  return vm2;
+}
+
+static int
+compare_vm (gconstpointer _vm1, gconstpointer _vm2)
+{
+  const struct vm *vm1 = (const struct vm *) _vm1;
+  const struct vm *vm2 = (const struct vm *) _vm2;
+
+  return strcmp (vm1->description, vm2->description);
+}
+
+static GSList *vmlist = NULL;
+static gboolean vmlist_valid = FALSE;
+static GStaticMutex vmlist_mutex;
+
+/* Called from the main thread to find out if we have a valid vmlist. */
+gboolean
+wui_thread_has_valid_vmlist (void)
+{
+  gboolean ret;
+
+  ASSERT_IS_MAIN_THREAD ();
+
+  g_static_mutex_lock (&vmlist_mutex);
+  ret = vmlist_valid;
+  g_static_mutex_unlock (&vmlist_mutex);
+  return ret;
+}
+
+/* Called from the main thread to find return the current vmlist.  This
+ * actually returns a deep copy of it that the caller must free.
+ */
+static void
+duplicate_and_insert_vm (gpointer _vm, gpointer _ret)
+{
+  struct vm *vm = (struct vm *) _vm;
+  GSList **ret = (GSList **) _ret;
+
+  *ret = g_slist_prepend (*ret, copy_vm (vm));
+}
+
+gboolean
+wui_thread_get_vmlist (GSList **ret)
+{
+  gboolean r;
+
+  ASSERT_IS_MAIN_THREAD ();
+
+  r = FALSE;
+  *ret = NULL;
+
+  g_static_mutex_lock (&vmlist_mutex);
+  if (!vmlist_valid) goto done;
+
+  g_slist_foreach (vmlist, duplicate_and_insert_vm, ret);
+  *ret = g_slist_sort (*ret, compare_vm);
+
+  r = TRUE;
+ done:
+  g_static_mutex_unlock (&vmlist_mutex);
+  return r;
+}
+
+/* Called from the message loop in the WUI thread, with an XML document
+ * which we turn into a list of VM structures, and update the vmlist
+ * if we can.
+ */
+static void
+parse_vmlist_from_xml (const char *xml)
+{
+  
+
+
+
+
+
+
 }