Update the Connect menu with vmlist.
[ovirt-viewer.git] / wui_thread.c
1 /* ovirt viewer console application
2  * Copyright (C) 2008 Red Hat Inc.
3  * Written by Richard W.M. Jones <rjones@redhat.com>
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
18  */
19
20 #include <config.h>
21
22 #include <stdio.h>
23 #include <stdlib.h>
24 #include <string.h>
25 #include <assert.h>
26
27 #include <glib.h>
28 #include <glib/gprintf.h>
29
30 #include <libxml/parser.h>
31
32 #include <curl/curl.h>
33
34 #include "internal.h"
35
36 /* Private functions. */
37 static gpointer wui_thread (gpointer data);
38 static void wui_thread_send_quit (void);
39 static void do_curl_init (void);
40 static void write_fn_start_capture (void);
41 static char *write_fn_finish_capture (void);
42 static void write_fn_discard_capture_buffer (void);
43 static gboolean do_connect (void);
44 static gboolean do_login (void);
45 static gboolean refresh_vm_list (void);
46 static void parse_vmlist_from_xml (const char *xml);
47 static struct vm *parse_vm_from_xml (xmlNodePtr node);
48
49 /* Messages (main thread -> WUI thread only).
50  *
51  * These are passed by reference.  They are allocated by the sender
52  * (ie. the main thread) and freed by the receiver (ie. the WUI thread).
53  */
54 enum message_type {
55   QUIT,                    /* Tell the WUI thread to quit cleanly. */
56   CONNECT,                 /* Tell to connect (just fetch the URI). */
57   DISCONNECT,              /* Tell to disconnect, forget state. */
58   LOGIN,                   /* Tell to login, and pass credentials. */
59   REFRESH_VM_LIST,         /* Tell to refresh the VM list right away. */
60 };
61
62 struct message {
63   enum message_type type;
64   char *str1;
65   char *str2;
66 };
67
68 /* Start the WUI thread.  See main() for explanation of the threading model. */
69 static GThread *wui_gthread = NULL;
70 static GThread *main_gthread = NULL;
71 static GAsyncQueue *wui_thread_queue = NULL;
72
73 void
74 start_wui_thread (void)
75 {
76   GError *error = NULL;
77
78   DEBUG ("starting the WUI thread");
79
80   assert (wui_gthread == NULL);
81
82   main_gthread = g_thread_self ();
83
84   /* Create the message queue for main -> WUI thread communications. */
85   wui_thread_queue = g_async_queue_new ();
86
87   wui_gthread = g_thread_create (wui_thread, wui_thread_queue, TRUE, &error);
88   if (error) {
89     g_print ("%s\n", error->message);
90     g_error_free (error);
91     exit (1);
92   }
93 }
94
95 void
96 stop_wui_thread (void)
97 {
98   DEBUG ("stopping the WUI thread");
99
100   assert (wui_gthread != NULL);
101   ASSERT_IS_MAIN_THREAD ();
102
103   /* Send a quit message then wait for the WUI thread to join.
104    *
105    * This "nice" shutdown could cause the UI to hang for an
106    * indefinite period (eg. if the WUI thread is engaged in some
107    * long connection or download from the remote server).  But
108    * I want to keep it this way for the moment so that we can
109    * diagnose problems with the WUI thread.
110    *
111    * (This could be solved with some sort of interruptible
112    * join, but glib doesn't support that AFAICT).
113    */
114   wui_thread_send_quit ();
115   (void) g_thread_join (wui_gthread);
116   g_async_queue_unref (wui_thread_queue);
117   wui_gthread = NULL;
118 }
119
120 void
121 assert_is_wui_thread (const char *filename, int lineno)
122 {
123   if (g_thread_self () != wui_gthread) {
124     fprintf (stderr, "%s:%d: internal error: this function should only run in the context of the WUI thread\n", filename, lineno);
125     abort ();
126   }
127 }
128
129 void
130 assert_is_main_thread (const char *filename, int lineno)
131 {
132   if (g_thread_self () != main_gthread) {
133     fprintf (stderr, "%s:%d: internal error: this function should only run in the context of the main thread\n", filename, lineno);
134     abort ();
135   }
136 }
137
138 /* Send the quit message to the WUI thread. */
139 static void
140 wui_thread_send_quit (void)
141 {
142   struct message *msg;
143
144   ASSERT_IS_MAIN_THREAD ();
145
146   msg = g_new (struct message, 1);
147   msg->type = QUIT;
148   g_async_queue_push (wui_thread_queue, msg);
149 }
150
151 /* Send the connect message to the WUI thread. */
152 void
153 wui_thread_send_connect (const char *uri)
154 {
155   struct message *msg;
156
157   ASSERT_IS_MAIN_THREAD ();
158
159   msg = g_new (struct message, 1);
160   msg->type = CONNECT;
161   msg->str1 = g_strdup (uri);
162   g_async_queue_push (wui_thread_queue, msg);
163 }
164
165 /* Send the disconnect message to the WUI thread. */
166 void
167 wui_thread_send_disconnect (void)
168 {
169   struct message *msg;
170
171   ASSERT_IS_MAIN_THREAD ();
172
173   msg = g_new (struct message, 1);
174   msg->type = DISCONNECT;
175   g_async_queue_push (wui_thread_queue, msg);
176 }
177
178 /* Send the login message to the WUI thread. */
179 void
180 wui_thread_send_login (const char *username, const char *password)
181 {
182   struct message *msg;
183
184   ASSERT_IS_MAIN_THREAD ();
185
186   msg = g_new (struct message, 1);
187   msg->type = LOGIN;
188   msg->str1 = g_strdup (username);
189   msg->str2 = g_strdup (password);
190   g_async_queue_push (wui_thread_queue, msg);
191 }
192
193 /* Send the refresh VM list message to the WUI thread. */
194 void
195 wui_thread_send_refresh_vm_list (void)
196 {
197   struct message *msg;
198
199   ASSERT_IS_MAIN_THREAD ();
200
201   msg = g_new (struct message, 1);
202   msg->type = REFRESH_VM_LIST;
203   g_async_queue_push (wui_thread_queue, msg);
204 }
205
206 /* The current state.
207  *
208  * For safety, the main thread must lock this before reading, and the
209  * WUI thread must lock this before writing.  However the WUI thread
210  * does not need to lock before reading, because no other thread
211  * can modify it.
212  */
213 static gboolean connected = FALSE;
214 static gboolean logged_in = FALSE;
215 static gboolean busy = FALSE;
216 static GStaticMutex state_mutex;
217
218 static void set_connected (gboolean);
219 static void set_logged_in (gboolean);
220 static void set_busy (gboolean);
221
222 /* The private state of the WUI thread. */
223 static int secs_between_refresh = 60;
224 static CURL *curl = NULL;
225 static char curl_error_buffer[CURL_ERROR_SIZE];
226 static char *uri = NULL;
227 static char *username = NULL;
228 static char *password = NULL;
229
230 static gboolean process_message (struct message *);
231
232 /* The WUI thread.  See main() above for explanation of
233  * the threading model.
234  */
235 static gpointer
236 wui_thread (gpointer _queue)
237 {
238   GAsyncQueue *queue = (GAsyncQueue *) _queue;
239   gboolean quit = FALSE;
240   GTimeVal tv;
241   gpointer _msg;
242   struct message *msg;
243
244   DEBUG ("WUI thread starting up");
245
246   /* This checks wui_gthread global which is actually set in the
247    * main thread.  Of course, it might not be set if the WUI thread
248    * runs first.  Hence we yield for the main thread to run.
249    */
250   g_thread_yield ();
251   ASSERT_IS_WUI_THREAD ();
252
253   g_async_queue_ref (queue);
254
255   /* In the thread's loop we check for new instructions from the main
256    * thread and carry them out.  Also, if we are connected and logged
257    * in then we periodically recheck the list of VMs.
258    */
259   while (!quit) {
260     set_busy (FALSE);
261
262     if (logged_in) {
263       g_get_current_time (&tv);
264       g_time_val_add (&tv, secs_between_refresh * 1000000);
265       _msg = g_async_queue_timed_pop (queue, &tv);
266     } else
267       _msg = g_async_queue_pop (queue);
268
269     set_busy (TRUE);
270
271     msg = (struct message *) _msg;
272
273     if (msg) {
274       DEBUG ("received message with msg->type = %d", msg->type);
275       quit = process_message (msg);
276       /* Don't free any strings in the message - we've saved them. */
277       g_free (msg);
278     } else {
279       /* No message, must have got a timeout instead, which means
280        * we are logged in and we should refresh the list of VMs.
281        * Note it's not an error if we temporarily lose contact
282        * with the WUI.
283        */
284       refresh_vm_list ();
285     }
286   }
287
288   DEBUG ("WUI thread shutting down cleanly");
289
290   g_async_queue_unref (queue);
291   g_thread_exit (NULL);
292   return NULL; /* not reached? */
293 }
294
295 /* The WUI thread calls this to safely update the state variables.
296  * This also updates elements in the UI by setting idle callbacks
297  * which are executed in the context of the main thread.
298  */
299 static void
300 set_connected (gboolean new_connected)
301 {
302   ASSERT_IS_WUI_THREAD ();
303
304   g_static_mutex_lock (&state_mutex);
305   connected = new_connected;
306   g_static_mutex_unlock (&state_mutex);
307
308   if (connected)
309     g_idle_add (main_connected, NULL);
310   else
311     g_idle_add (main_disconnected, NULL);
312 }
313
314 static void
315 set_logged_in (gboolean new_logged_in)
316 {
317   ASSERT_IS_WUI_THREAD ();
318
319   g_static_mutex_lock (&state_mutex);
320   logged_in = new_logged_in;
321   g_static_mutex_unlock (&state_mutex);
322
323   if (logged_in)
324     g_idle_add (main_logged_in, NULL);
325   else
326     g_idle_add (main_logged_out, NULL);
327 }
328
329 static void
330 set_busy (gboolean new_busy)
331 {
332   ASSERT_IS_WUI_THREAD ();
333
334   g_static_mutex_lock (&state_mutex);
335   busy = new_busy;
336   g_static_mutex_unlock (&state_mutex);
337
338   if (busy)
339     g_idle_add (main_busy, NULL);
340   else
341     g_idle_add (main_idle, NULL);
342 }
343
344 /* The main thread should call these functions to get the WUI thread's
345  * current state.
346  */
347 gboolean
348 wui_thread_is_connected (void)
349 {
350   gboolean ret;
351
352   ASSERT_IS_MAIN_THREAD ();
353
354   g_static_mutex_lock (&state_mutex);
355   ret = connected;
356   g_static_mutex_unlock (&state_mutex);
357   return ret;
358 }
359
360 gboolean
361 wui_thread_is_logged_in (void)
362 {
363   gboolean ret;
364
365   ASSERT_IS_MAIN_THREAD ();
366
367   g_static_mutex_lock (&state_mutex);
368   ret = logged_in;
369   g_static_mutex_unlock (&state_mutex);
370   return ret;
371 }
372
373 gboolean
374 wui_thread_is_busy (void)
375 {
376   gboolean ret;
377
378   ASSERT_IS_MAIN_THREAD ();
379
380   g_static_mutex_lock (&state_mutex);
381   ret = busy;
382   g_static_mutex_unlock (&state_mutex);
383   return ret;
384 }
385
386 /* Process a message from the main thread. */
387 static gboolean
388 process_message (struct message *msg)
389 {
390   ASSERT_IS_WUI_THREAD ();
391
392   switch (msg->type) {
393   case QUIT:
394     write_fn_discard_capture_buffer ();
395     if (curl) curl_easy_cleanup (curl);
396     if (uri) g_free (uri);
397     if (username) g_free (username);
398     if (password) g_free (password);
399     set_connected (FALSE);
400     set_logged_in (FALSE);
401     return 1;
402
403   case CONNECT:
404     write_fn_discard_capture_buffer ();
405     if (curl) curl_easy_cleanup (curl);
406     do_curl_init ();
407     if (uri) g_free (uri);
408     uri = msg->str1;
409
410     if (do_connect ()) {
411       set_connected (TRUE);
412     } else {
413       set_connected (FALSE);
414       set_logged_in (FALSE);
415     }
416     break;
417
418   case DISCONNECT:
419     /* This just forgets the state. REST is connectionless. */
420     write_fn_discard_capture_buffer ();
421     if (curl) curl_easy_cleanup (curl);
422     curl = NULL;
423     if (uri) g_free (uri);
424     uri = NULL;
425     if (username) g_free (username);
426     username = NULL;
427     if (password) g_free (password);
428     password = NULL;
429     set_connected (FALSE);
430     set_logged_in (FALSE);
431     break;
432
433   case LOGIN:
434     if (username) g_free (username);
435     username = msg->str1;
436     if (password) g_free (password);
437     password = msg->str2;
438
439     /* If we're not connected, this message just updates the
440      * username and password.  Otherwise if we are connected,
441      * try to login and grab the initial list of VMs.
442      */
443     if (connected) {
444       if (do_login ()) {
445         set_logged_in (TRUE);
446         if (refresh_vm_list ())
447           secs_between_refresh = 60;
448       } else {
449         set_logged_in (FALSE);
450       }
451     }
452     break;
453
454   case REFRESH_VM_LIST:
455     if (connected && logged_in) {
456       refresh_vm_list ();
457       secs_between_refresh = 60;
458     }
459     break;
460
461   default:
462     DEBUG ("unknown message type %d", msg->type);
463     abort ();
464   }
465
466   return 0;
467 }
468
469 /* Macro for easy handling of CURL errors. */
470 #define CURL_CHECK_ERROR(fn, args)                                      \
471   ({                                                                    \
472     CURLcode __r = fn args;                                             \
473     if (__r != CURLE_OK) {                                              \
474       fprintf (stderr, "%s: %s\n", #fn, curl_easy_strerror (__r));      \
475     }                                                                   \
476     __r;                                                                \
477   })
478
479 /* Libcurl has a really crufty method for handling HTTP headers and
480  * data.  We set these functions as callbacks, because the default
481  * callback functions print the data out to stderr.  In order to
482  * capture the data, we have to keep state here.
483  */
484
485 static char *write_fn_buffer = NULL;
486 static ssize_t write_fn_len = -1;
487
488 static size_t
489 write_fn (void *ptr, size_t size, size_t nmemb, void *stream)
490 {
491   int bytes = size * nmemb;
492   int old_start;
493
494   ASSERT_IS_WUI_THREAD ();
495
496   if (write_fn_len >= 0) {      /* We're capturing. */
497     old_start = write_fn_len;
498     write_fn_len += bytes;
499     write_fn_buffer = g_realloc (write_fn_buffer, write_fn_len);
500     memcpy (write_fn_buffer + old_start, ptr, bytes);
501   }
502
503   return bytes;
504 }
505
506 /* Start capturing HTTP response data. */
507 static void
508 write_fn_start_capture (void)
509 {
510   ASSERT_IS_WUI_THREAD ();
511
512   write_fn_discard_capture_buffer ();
513   write_fn_buffer = NULL;
514   write_fn_len = 0;
515 }
516
517 /* Finish capture and return the capture buffer.  Caller must free. */
518 static char *
519 write_fn_finish_capture (void)
520 {
521   char *ret = write_fn_buffer;
522
523   ASSERT_IS_WUI_THREAD ();
524
525   write_fn_buffer = NULL;
526   write_fn_len = -1;
527   return ret;
528 }
529
530 /* Stop capturing and discard the capture buffer, if any. */
531 static void
532 write_fn_discard_capture_buffer (void)
533 {
534   ASSERT_IS_WUI_THREAD ();
535
536   g_free (write_fn_buffer);
537   write_fn_buffer = NULL;
538   write_fn_len = -1;
539 }
540
541 static size_t
542 header_fn (void *ptr, size_t size, size_t nmemb, void *stream)
543 {
544   int bytes = size * nmemb;
545
546   ASSERT_IS_WUI_THREAD ();
547
548   return bytes;
549 }
550
551 /* Called from the message loop to initialize the CURL handle. */
552 static void
553 do_curl_init (void)
554 {
555   DEBUG ("initializing libcurl");
556
557   ASSERT_IS_WUI_THREAD ();
558
559   curl = curl_easy_init ();
560   if (!curl) {             /* This is probably quite bad, so abort. */
561     DEBUG ("curl_easy_init failed");
562     abort ();
563   }
564
565   CURL_CHECK_ERROR (curl_easy_setopt,
566                     (curl, CURLOPT_CAINFO, cainfo));
567   CURL_CHECK_ERROR (curl_easy_setopt,
568                     (curl, CURLOPT_SSL_VERIFYHOST, check_cert ? 2 : 0));
569   CURL_CHECK_ERROR (curl_easy_setopt,
570                     (curl, CURLOPT_SSL_VERIFYPEER, check_cert ? 1 : 0));
571
572   CURL_CHECK_ERROR (curl_easy_setopt,
573                     (curl, CURLOPT_WRITEFUNCTION, write_fn));
574   CURL_CHECK_ERROR (curl_easy_setopt,
575                     (curl, CURLOPT_HEADERFUNCTION, header_fn));
576
577   /* This enables error messages in curl_easy_perform. */
578   CURL_CHECK_ERROR (curl_easy_setopt,
579                     (curl, CURLOPT_ERRORBUFFER, curl_error_buffer));
580
581   /* This enables cookie handling, using an internal cookiejar. */
582   CURL_CHECK_ERROR (curl_easy_setopt,
583                     (curl, CURLOPT_COOKIEFILE, ""));
584 }
585
586 /* Called from the message loop.  Try to connect to the current URI.
587  * Returns true on success.
588  */
589 static gboolean
590 do_connect (void)
591 {
592   long code = 0;
593   CURLcode r;
594   char *error_str;
595
596   DEBUG ("connecting to uri %s", uri);
597   ASSERT_IS_WUI_THREAD ();
598
599   /* Set the URI for libcurl. */
600   CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_URL, uri));
601
602   /* Try to fetch the URI. */
603   r = CURL_CHECK_ERROR (curl_easy_perform, (curl));
604   if (r != CURLE_OK) {
605     /* Signal an error back to the main thread. */
606     error_str = g_strdup (curl_easy_strerror (r));
607     g_idle_add (main_connection_error, error_str);
608     return FALSE;
609   }
610
611   CURL_CHECK_ERROR (curl_easy_getinfo, (curl, CURLINFO_RESPONSE_CODE, &code));
612   DEBUG ("HTTP return code is %ld", code);
613   if (code != 200 && code != 302 && code != 401) {
614     /* XXX If only glib had g_asprintf. */
615     error_str = g_strdup ("unexpected HTTP return code from server");
616     g_idle_add (main_connection_error, error_str);
617     return FALSE;
618   }
619
620   return TRUE;
621 }
622
623 /* Called from the message loop.  Try to login to 'URI/login' with the
624  * current username and password.  Returns true on success.
625  */
626 static gboolean
627 do_login (void)
628 {
629   int len;
630   char *login_uri;
631   char *userpwd;
632   char *error_str;
633   CURLcode r;
634   long code = 0;
635
636   DEBUG ("logging in with username %s, password *****", username);
637   ASSERT_IS_WUI_THREAD ();
638
639   /* Generate the login URI from the base URI. */
640   len = strlen (uri) + 6 + 1;
641   login_uri = g_alloca (len);
642   snprintf (login_uri, len, "%s/login", uri);
643
644   DEBUG ("login URI %s", login_uri);
645
646   /* Set the URI for libcurl. */
647   CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_URL, login_uri));
648
649   /* Construct the username:password for CURL. */
650   len = strlen (username) + strlen (password) + 2;
651   userpwd = g_alloca (len);
652   snprintf (userpwd, len, "%s:%s", username, password);
653
654   CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_USERPWD, userpwd));
655
656   /* HTTP Basic authentication is OK since we should be sending
657    * this only over HTTPS.
658    */
659   CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC));
660
661   /* Follow redirects. */
662   CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_FOLLOWLOCATION, (long) 1));
663   CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_MAXREDIRS, (long) 10));
664
665   /* Try to fetch the URI. */
666   r = CURL_CHECK_ERROR (curl_easy_perform, (curl));
667   if (r != CURLE_OK) {
668     /* Signal an error back to the main thread. */
669     error_str = g_strdup (curl_easy_strerror (r));
670     g_idle_add (main_login_error, error_str);
671     return FALSE;
672   }
673
674   CURL_CHECK_ERROR (curl_easy_getinfo, (curl, CURLINFO_RESPONSE_CODE, &code));
675   DEBUG ("HTTP return code is %ld", code);
676   switch (code)
677     {
678     case 200:
679       DEBUG ("login was successful");
680       return TRUE;
681
682     case 401:
683       error_str = g_strdup ("server rejected the username or password");
684       g_idle_add (main_login_error, error_str);
685       return FALSE;
686
687     default:
688       /* XXX If only glib had g_asprintf. */
689       error_str = g_strdup ("unexpected HTTP return code from server");
690       g_idle_add (main_login_error, error_str);
691       return FALSE;
692     }
693 }
694
695 /* Called from the message loop.  Refresh the list of VMs. */
696 static gboolean
697 refresh_vm_list (void)
698 {
699   int len;
700   char *vms_uri;
701   char *error_str;
702   CURLcode r;
703   long code = 0;
704   char *xml;
705
706   DEBUG ("refreshing list of VMs");
707   ASSERT_IS_WUI_THREAD ();
708
709   /* Generate the vms URI from the base URI. */
710   len = strlen (uri) + 4 + 1;
711   vms_uri = g_alloca (len);
712   snprintf (vms_uri, len, "%s/vms", uri);
713
714   DEBUG ("vms URI %s", vms_uri);
715
716   /* Set the URI for libcurl. */
717   CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_URL, vms_uri));
718
719   /* We want to capture the output, so tell our write function
720    * to place the output into a buffer.
721    */
722   write_fn_start_capture ();
723
724   /* Try to fetch the URI. */
725   r = CURL_CHECK_ERROR (curl_easy_perform, (curl));
726   if (r != CURLE_OK) {
727     /* Signal an error back to the main thread. */
728     error_str = g_strdup (curl_easy_strerror (r));
729     g_idle_add (main_login_error, error_str);
730     return FALSE;
731   }
732
733   CURL_CHECK_ERROR (curl_easy_getinfo, (curl, CURLINFO_RESPONSE_CODE, &code));
734   DEBUG ("HTTP return code is %ld", code);
735   switch (code)
736     {
737     case 200: break;
738
739       /* Hmm - even though we previously logged in, the server is
740        * rejecting our attempts now with an authorization error.
741        * We move to the logged out state.
742        */
743     case 401:
744       set_logged_in (FALSE);
745       error_str = g_strdup ("server rejected the username or password");
746       g_idle_add (main_login_error, error_str);
747       return FALSE;
748
749     default:
750       /* XXX If only glib had g_asprintf. */
751       error_str = g_strdup ("unexpected HTTP return code from server");
752       g_idle_add (main_status_error, error_str);
753       return FALSE;
754     }
755
756   /* If we got here then we appear to have a correct
757    * XML document describing the list of VMs.
758    */
759   secs_between_refresh <<= 1;
760
761   xml = write_fn_finish_capture ();
762
763   parse_vmlist_from_xml (xml);
764   g_free (xml);
765
766   return TRUE;
767 }
768
769 /* Functions to deal with the list of VMs, parsing it from the XML, etc.
770  *
771  * A vmlist is a GSList (glib singly-linked list) of vm structures.
772  * The caller must call free_vmlist to free up this list correctly.
773  */
774
775 static void
776 free_vm (gpointer _vm, gpointer data)
777 {
778   struct vm *vm = (struct vm *) _vm;
779
780   g_free (vm->description);
781   g_free (vm->state);
782   g_free (vm->uuid);
783   g_free (vm->mac_addr);
784   g_free (vm);
785 }
786
787 void
788 free_vmlist (GSList *vmlist)
789 {
790   g_slist_foreach (vmlist, free_vm, NULL);
791   g_slist_free (vmlist);
792 }
793
794 static struct vm *
795 copy_vm (struct vm *vm)
796 {
797   struct vm *vm2;
798
799   vm2 = g_memdup (vm, sizeof (*vm));
800   vm2->description = g_strdup (vm->description);
801   vm2->state = vm->state ? g_strdup (vm->state) : NULL;
802   vm2->uuid = vm->uuid ? g_strdup (vm->uuid) : NULL;
803   vm2->mac_addr = vm->mac_addr ? g_strdup (vm->mac_addr) : NULL;
804   return vm2;
805 }
806
807 static int
808 compare_vm (gconstpointer _vm1, gconstpointer _vm2)
809 {
810   const struct vm *vm1 = (const struct vm *) _vm1;
811   const struct vm *vm2 = (const struct vm *) _vm2;
812
813   return strcmp (vm1->description, vm2->description);
814 }
815
816 static GSList *vmlist = NULL;
817 static gboolean vmlist_valid = FALSE;
818 static GStaticMutex vmlist_mutex;
819
820 /* Called from the main thread to find out if we have a valid vmlist. */
821 gboolean
822 wui_thread_has_valid_vmlist (void)
823 {
824   gboolean ret;
825
826   ASSERT_IS_MAIN_THREAD ();
827
828   g_static_mutex_lock (&vmlist_mutex);
829   ret = vmlist_valid;
830   g_static_mutex_unlock (&vmlist_mutex);
831   return ret;
832 }
833
834 /* Called from the main thread to find return the current vmlist.  This
835  * actually returns a deep copy of it that the caller must free.
836  */
837 static void
838 duplicate_and_insert_vm (gpointer _vm, gpointer _ret)
839 {
840   struct vm *vm = (struct vm *) _vm;
841   GSList **ret = (GSList **) _ret;
842
843   *ret = g_slist_prepend (*ret, copy_vm (vm));
844 }
845
846 gboolean
847 wui_thread_get_vmlist (GSList **ret)
848 {
849   gboolean r;
850
851   ASSERT_IS_MAIN_THREAD ();
852
853   r = FALSE;
854   *ret = NULL;
855
856   g_static_mutex_lock (&vmlist_mutex);
857   if (!vmlist_valid) goto done;
858
859   g_slist_foreach (vmlist, duplicate_and_insert_vm, ret);
860   *ret = g_slist_sort (*ret, compare_vm);
861
862   r = TRUE;
863  done:
864   g_static_mutex_unlock (&vmlist_mutex);
865   return r;
866 }
867
868 /* Called from the message loop in the WUI thread, with an XML document
869  * which we turn into a list of VM structures, and update the vmlist
870  * if we can.
871  */
872 static void
873 parse_vmlist_from_xml (const char *xml)
874 {
875   xmlDocPtr doc = NULL;
876   xmlNodePtr root;
877   xmlNodePtr node;
878   char *error_str;
879   GSList *new_vmlist = NULL;
880   struct vm *vm;
881
882   /*DEBUG ("XML =\n%s", xml);*/
883   ASSERT_IS_WUI_THREAD ();
884
885   /* We don't really expect that we won't be able to parse the XML ... */
886   doc = xmlParseDoc ((const xmlChar *) xml);
887   if (!doc) {
888     DEBUG ("error parsing XML document, xml =\n%s", xml);
889     error_str = g_strdup ("error parsing XML document from remote server");
890     g_idle_add (main_status_error, error_str);
891     goto done;
892   }
893
894   root = xmlDocGetRootElement (doc);
895   if (!root) {
896     DEBUG ("XML document was empty");
897     error_str = g_strdup ("XML document was empty");
898     g_idle_add (main_status_error, error_str);
899     goto done;
900   }
901
902   /* We expect the root element will be either "nil-classes"
903    * or "vms", with the former indicating an empty list of VMs.
904    */
905   if (xmlStrcmp (root->name, (const xmlChar *) "nil-classes") == 0) {
906     g_static_mutex_lock (&vmlist_mutex);
907     vmlist_valid = TRUE;
908     free_vmlist (vmlist);
909     vmlist = NULL;
910     g_static_mutex_unlock (&vmlist_mutex);
911
912     /* Signal to the main UI thread that the list has been updated. */
913     g_idle_add (main_vmlist_updated, NULL);
914
915     goto done;
916   }
917
918   if (xmlStrcmp (root->name, (const xmlChar *) "vms") != 0) {
919     DEBUG ("unexpected root node in XML document, xml =\n%s", xml);
920     error_str = g_strdup ("unexpected root node in XML document");
921     g_idle_add (main_status_error, error_str);
922     goto done;
923   }
924
925   /* The document is <vms> with a list of <vm> elements which
926    * we process in turn.
927    */
928   for (node = root->xmlChildrenNode; node != NULL; node = node->next) {
929     if (xmlStrcmp (node->name, (const xmlChar *) "vm") == 0) {
930       vm = parse_vm_from_xml (node);
931       if (!vm) {
932         error_str = g_strdup ("could not parse <vm> element");
933         g_idle_add (main_status_error, error_str);
934
935         free_vmlist (new_vmlist);
936         goto done;
937       }
938       new_vmlist = g_slist_prepend (new_vmlist, vm);
939     }
940   }
941
942   /* Successfully parsed all the <vm> nodes, so swap the old and new
943    * vmlists.
944    */
945   g_static_mutex_lock (&vmlist_mutex);
946   vmlist_valid = TRUE;
947   free_vmlist (vmlist);
948   vmlist = new_vmlist;
949   g_static_mutex_unlock (&vmlist_mutex);
950
951   /* Signal that the vmlist has been updated. */
952   g_idle_add (main_vmlist_updated, NULL);
953
954  done:
955   /* Free up XML resources used before returning. */
956   if (doc) xmlFreeDoc (doc);
957 }
958
959 static struct vm *
960 parse_vm_from_xml (xmlNodePtr node)
961 {
962   xmlNodePtr p;
963   struct vm vm, *ret;
964   xmlChar *str;
965
966   memset (&vm, 0, sizeof vm);
967   vm.hostid = -1;
968   vm.id = -1;
969   vm.vnc_port = -1;
970   vm.mem_allocated = -1;
971   vm.mem_used = -1;
972   vm.vcpus_allocated = -1;
973   vm.vcpus_used = -1;
974
975   for (p = node->xmlChildrenNode; p != NULL; p = p->next) {
976     if (xmlStrcmp (p->name, (const xmlChar *) "description") == 0) {
977       str = xmlNodeGetContent (p);
978       if (str != NULL) {
979         vm.description = g_strdup ((char *) str);
980         xmlFree (str);
981       }
982     }
983     else if (xmlStrcmp (p->name, (const xmlChar *) "host-id") == 0) {
984       str = xmlNodeGetContent (p);
985       if (str != NULL) {
986         vm.hostid = strtol ((char *) str, NULL, 10);
987         xmlFree (str);
988       }
989     }
990     else if (xmlStrcmp (p->name, (const xmlChar *) "id") == 0) {
991       str = xmlNodeGetContent (p);
992       if (str != NULL) {
993         vm.id = strtol ((char *) str, NULL, 10);
994         xmlFree (str);
995       }
996     }
997     else if (xmlStrcmp (p->name, (const xmlChar *) "memory-allocated") == 0) {
998       str = xmlNodeGetContent (p);
999       if (str != NULL) {
1000         vm.mem_allocated = strtol ((char *) str, NULL, 10);
1001         xmlFree (str);
1002       }
1003     }
1004     else if (xmlStrcmp (p->name, (const xmlChar *) "memory-used") == 0) {
1005       str = xmlNodeGetContent (p);
1006       if (str != NULL) {
1007         vm.mem_used = strtol ((char *) str, NULL, 10);
1008         xmlFree (str);
1009       }
1010     }
1011     else if (xmlStrcmp (p->name, (const xmlChar *) "num-vcpus-allocated") == 0) {
1012       str = xmlNodeGetContent (p);
1013       if (str != NULL) {
1014         vm.vcpus_allocated = strtol ((char *) str, NULL, 10);
1015         xmlFree (str);
1016       }
1017     }
1018     else if (xmlStrcmp (p->name, (const xmlChar *) "num-vcpus-used") == 0) {
1019       str = xmlNodeGetContent (p);
1020       if (str != NULL) {
1021         vm.vcpus_used = strtol ((char *) str, NULL, 10);
1022         xmlFree (str);
1023       }
1024     }
1025     else if (xmlStrcmp (p->name, (const xmlChar *) "state") == 0) {
1026       str = xmlNodeGetContent (p);
1027       if (str != NULL) {
1028         vm.state = g_strdup ((char *) str);
1029         xmlFree (str);
1030       }
1031     }
1032     else if (xmlStrcmp (p->name, (const xmlChar *) "uuid") == 0) {
1033       str = xmlNodeGetContent (p);
1034       if (str != NULL) {
1035         vm.uuid = g_strdup ((char *) str);
1036         xmlFree (str);
1037       }
1038     }
1039     else if (xmlStrcmp (p->name, (const xmlChar *) "vnc-port") == 0) {
1040       str = xmlNodeGetContent (p);
1041       if (str != NULL) {
1042         vm.vnc_port = strtol ((char *) str, NULL, 10);
1043         xmlFree (str);
1044       }
1045     }
1046     else if (xmlStrcmp (p->name, (const xmlChar *) "vnic-mac-addr") == 0) {
1047       str = xmlNodeGetContent (p);
1048       if (str != NULL) {
1049         vm.mac_addr = g_strdup ((char *) str);
1050         xmlFree (str);
1051       }
1052     }
1053   }
1054
1055   /* Make sure we've got the required fields. */
1056   ret = NULL;
1057   if (vm.description == NULL)
1058     DEBUG ("required field \"description\" missing from <vm> structure");
1059   else if (vm.hostid == -1)
1060     DEBUG ("required field \"description\" missing from <vm> structure");
1061   else if (vm.id == -1)
1062     DEBUG ("required field \"description\" missing from <vm> structure");
1063   else if (vm.vnc_port == -1)
1064     DEBUG ("required field \"vnc-port\" missing from <vm> structure");
1065   else
1066     ret = g_memdup (&vm, sizeof vm);
1067
1068   return ret;
1069 }