Daily update.
[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 <curl/curl.h>
31
32 #include "internal.h"
33
34 /* Private functions. */
35 static gpointer wui_thread (gpointer data);
36 static void wui_thread_send_quit (void);
37 static void do_curl_init (void);
38 static void write_fn_start_capture (void);
39 static char *write_fn_finish_capture (void);
40 static void write_fn_discard_capture_buffer (void);
41 static gboolean do_connect (void);
42 static gboolean do_login (void);
43 static gboolean refresh_vm_list (void);
44 static void parse_vmlist_from_xml (const char *xml);
45
46 /* Messages (main thread -> WUI thread only).
47  *
48  * These are passed by reference.  They are allocated by the sender
49  * (ie. the main thread) and freed by the receiver (ie. the WUI thread).
50  */
51 enum message_type {
52   QUIT,                    /* Tell the WUI thread to quit cleanly. */
53   CONNECT,                 /* Tell to connect (just fetch the URI). */
54   DISCONNECT,              /* Tell to disconnect, forget state. */
55   LOGIN,                   /* Tell to login, and pass credentials. */
56   REFRESH_VM_LIST,         /* Tell to refresh the VM list right away. */
57 };
58
59 struct message {
60   enum message_type type;
61   char *str1;
62   char *str2;
63 };
64
65 /* Start the WUI thread.  See main() for explanation of the threading model. */
66 static GThread *wui_gthread = NULL;
67 static GAsyncQueue *wui_thread_queue = NULL;
68
69 void
70 start_wui_thread (void)
71 {
72   GError *error = NULL;
73
74   DEBUG ("starting the WUI thread");
75
76   /* Create the message queue for main -> WUI thread communications. */
77   wui_thread_queue = g_async_queue_new ();
78
79   wui_gthread = g_thread_create (wui_thread, wui_thread_queue, TRUE, &error);
80   if (error) {
81     g_print ("%s\n", error->message);
82     g_error_free (error);
83     exit (1);
84   }
85 }
86
87 void
88 stop_wui_thread (void)
89 {
90   DEBUG ("stopping the WUI thread");
91   assert (wui_gthread);
92
93   /* Send a quit message then wait for the WUI thread to join.
94    *
95    * This "nice" shutdown could cause the UI to hang for an
96    * indefinite period (eg. if the WUI thread is engaged in some
97    * long connection or download from the remote server).  But
98    * I want to keep it this way for the moment so that we can
99    * diagnose problems with the WUI thread.
100    *
101    * (This could be solved with some sort of interruptible
102    * join, but glib doesn't support that AFAICT).
103    */
104   wui_thread_send_quit ();
105   (void) g_thread_join (wui_gthread);
106   g_async_queue_unref (wui_thread_queue);
107   wui_gthread = NULL;
108 }
109
110 /* Send the quit message to the WUI thread. */
111 static void
112 wui_thread_send_quit (void)
113 {
114   struct message *msg;
115
116   msg = g_new (struct message, 1);
117   msg->type = QUIT;
118   g_async_queue_push (wui_thread_queue, msg);
119 }
120
121 /* Send the connect message to the WUI thread. */
122 void
123 wui_thread_send_connect (const char *uri)
124 {
125   struct message *msg;
126
127   msg = g_new (struct message, 1);
128   msg->type = CONNECT;
129   msg->str1 = g_strdup (uri);
130   g_async_queue_push (wui_thread_queue, msg);
131 }
132
133 /* Send the disconnect message to the WUI thread. */
134 void
135 wui_thread_send_disconnect (void)
136 {
137   struct message *msg;
138
139   msg = g_new (struct message, 1);
140   msg->type = DISCONNECT;
141   g_async_queue_push (wui_thread_queue, msg);
142 }
143
144 /* Send the login message to the WUI thread. */
145 void
146 wui_thread_send_login (const char *username, const char *password)
147 {
148   struct message *msg;
149
150   msg = g_new (struct message, 1);
151   msg->type = LOGIN;
152   msg->str1 = g_strdup (username);
153   msg->str2 = g_strdup (password);
154   g_async_queue_push (wui_thread_queue, msg);
155 }
156
157 /* Send the refresh VM list message to the WUI thread. */
158 void
159 wui_thread_send_refresh_vm_list (void)
160 {
161   struct message *msg;
162
163   msg = g_new (struct message, 1);
164   msg->type = REFRESH_VM_LIST;
165   g_async_queue_push (wui_thread_queue, msg);
166 }
167
168 /* The current state.
169  *
170  * For safety, the main thread must lock this before reading, and the
171  * WUI thread must lock this before writing.  However the WUI thread
172  * does not need to lock before reading, because no other thread
173  * can modify it.
174  */
175 static gboolean connected = FALSE;
176 static gboolean logged_in = FALSE;
177 static gboolean busy = FALSE;
178 static GStaticMutex state_mutex;
179
180 static void set_connected (gboolean);
181 static void set_logged_in (gboolean);
182 static void set_busy (gboolean);
183
184 /* The private state of the WUI thread. */
185 static int secs_between_refresh = 60;
186 static CURL *curl = NULL;
187 static char curl_error_buffer[CURL_ERROR_SIZE];
188 static char *uri = NULL;
189 static char *username = NULL;
190 static char *password = NULL;
191
192 static gboolean process_message (struct message *);
193
194 /* The WUI thread.  See main() above for explanation of
195  * the threading model.
196  */
197 static gpointer
198 wui_thread (gpointer _queue)
199 {
200   GAsyncQueue *queue = (GAsyncQueue *) _queue;
201   gboolean quit = FALSE;
202   GTimeVal tv;
203   gpointer _msg;
204   struct message *msg;
205
206   DEBUG ("WUI thread starting up");
207
208   g_async_queue_ref (queue);
209
210   /* In the thread's loop we check for new instructions from the main
211    * thread and carry them out.  Also, if we are connected and logged
212    * in then we periodically recheck the list of VMs.
213    */
214   while (!quit) {
215     set_busy (FALSE);
216
217     if (logged_in) {
218       g_get_current_time (&tv);
219       g_time_val_add (&tv, secs_between_refresh * 1000000);
220       _msg = g_async_queue_timed_pop (queue, &tv);
221     } else
222       _msg = g_async_queue_pop (queue);
223
224     set_busy (TRUE);
225
226     msg = (struct message *) _msg;
227
228     if (msg) {
229       DEBUG ("received message with msg->type = %d", msg->type);
230       quit = process_message (msg);
231       /* Don't free any strings in the message - we've saved them. */
232       g_free (msg);
233     } else {
234       /* No message, must have got a timeout instead, which means
235        * we are logged in and we should refresh the list of VMs.
236        * Note it's not an error if we temporarily lose contact
237        * with the WUI.
238        */
239       refresh_vm_list ();
240     }
241   }
242
243   DEBUG ("WUI thread shutting down cleanly");
244
245   g_async_queue_unref (queue);
246   g_thread_exit (NULL);
247   return NULL; /* not reached? */
248 }
249
250 /* The WUI thread calls this to safely update the state variables.
251  * This also updates elements in the UI by setting idle callbacks
252  * which are executed in the context of the main thread.
253  */
254 static void
255 set_connected (gboolean new_connected)
256 {
257   g_static_mutex_lock (&state_mutex);
258   connected = new_connected;
259   g_static_mutex_unlock (&state_mutex);
260
261   if (connected)
262     g_idle_add (main_connected, NULL);
263   else
264     g_idle_add (main_disconnected, NULL);
265 }
266
267 static void
268 set_logged_in (gboolean new_logged_in)
269 {
270   g_static_mutex_lock (&state_mutex);
271   logged_in = new_logged_in;
272   g_static_mutex_unlock (&state_mutex);
273
274   if (logged_in)
275     g_idle_add (main_logged_in, NULL);
276   else
277     g_idle_add (main_logged_out, NULL);
278 }
279
280 static void
281 set_busy (gboolean new_busy)
282 {
283   g_static_mutex_lock (&state_mutex);
284   busy = new_busy;
285   g_static_mutex_unlock (&state_mutex);
286
287   if (busy)
288     g_idle_add (main_busy, NULL);
289   else
290     g_idle_add (main_idle, NULL);
291 }
292
293 /* The main thread should call these functions to get the WUI thread's
294  * current state.
295  */
296 gboolean
297 wui_thread_is_connected (void)
298 {
299   gboolean ret;
300
301   g_static_mutex_lock (&state_mutex);
302   ret = connected;
303   g_static_mutex_unlock (&state_mutex);
304   return ret;
305 }
306
307 gboolean
308 wui_thread_is_logged_in (void)
309 {
310   gboolean ret;
311
312   g_static_mutex_lock (&state_mutex);
313   ret = logged_in;
314   g_static_mutex_unlock (&state_mutex);
315   return ret;
316 }
317
318 gboolean
319 wui_thread_is_busy (void)
320 {
321   gboolean ret;
322
323   g_static_mutex_lock (&state_mutex);
324   ret = busy;
325   g_static_mutex_unlock (&state_mutex);
326   return ret;
327 }
328
329 /* Process a message from the main thread. */
330 static gboolean
331 process_message (struct message *msg)
332 {
333   switch (msg->type) {
334   case QUIT:
335     write_fn_discard_capture_buffer ();
336     if (curl) curl_easy_cleanup (curl);
337     if (uri) g_free (uri);
338     if (username) g_free (username);
339     if (password) g_free (password);
340     set_connected (FALSE);
341     set_logged_in (FALSE);
342     return 1;
343
344   case CONNECT:
345     write_fn_discard_capture_buffer ();
346     if (curl) curl_easy_cleanup (curl);
347     do_curl_init ();
348     if (uri) g_free (uri);
349     uri = msg->str1;
350
351     if (do_connect ()) {
352       set_connected (TRUE);
353     } else {
354       set_connected (FALSE);
355       set_logged_in (FALSE);
356     }
357     break;
358
359   case DISCONNECT:
360     /* This just forgets the state. REST is connectionless. */
361     write_fn_discard_capture_buffer ();
362     if (curl) curl_easy_cleanup (curl);
363     curl = NULL;
364     if (uri) g_free (uri);
365     uri = NULL;
366     if (username) g_free (username);
367     username = NULL;
368     if (password) g_free (password);
369     password = NULL;
370     set_connected (FALSE);
371     set_logged_in (FALSE);
372     break;
373
374   case LOGIN:
375     if (username) g_free (username);
376     username = msg->str1;
377     if (password) g_free (password);
378     password = msg->str2;
379
380     /* If we're not connected, this message just updates the
381      * username and password.  Otherwise if we are connected,
382      * try to login and grab the initial list of VMs.
383      */
384     if (connected) {
385       if (do_login ()) {
386         set_logged_in (TRUE);
387         if (refresh_vm_list ())
388           secs_between_refresh = 60;
389       } else {
390         set_logged_in (FALSE);
391       }
392     }
393     break;
394
395   case REFRESH_VM_LIST:
396     if (connected && logged_in) {
397       refresh_vm_list ();
398       secs_between_refresh = 60;
399     }
400     break;
401
402   default:
403     DEBUG ("unknown message type %d", msg->type);
404     abort ();
405   }
406
407   return 0;
408 }
409
410 /* Macro for easy handling of CURL errors. */
411 #define CURL_CHECK_ERROR(fn, args)                                      \
412   ({                                                                    \
413     CURLcode __r = fn args;                                             \
414     if (__r != CURLE_OK) {                                              \
415       fprintf (stderr, "%s: %s\n", #fn, curl_easy_strerror (__r));      \
416     }                                                                   \
417     __r;                                                                \
418   })
419
420 /* Libcurl has a really crufty method for handling HTTP headers and
421  * data.  We set these functions as callbacks, because the default
422  * callback functions print the data out to stderr.  In order to
423  * capture the data, we have to keep state here.
424  */
425
426 static char *write_fn_buffer = NULL;
427 static ssize_t write_fn_len = -1;
428
429 static size_t
430 write_fn (void *ptr, size_t size, size_t nmemb, void *stream)
431 {
432   int bytes = size * nmemb;
433   int old_start;
434
435   if (write_fn_len >= 0) {      /* We're capturing. */
436     old_start = write_fn_len;
437     write_fn_len += bytes;
438     write_fn_buffer = g_realloc (write_fn_buffer, write_fn_len);
439     memcpy (write_fn_buffer + old_start, ptr, bytes);
440   }
441
442   return bytes;
443 }
444
445 /* Start capturing HTTP response data. */
446 static void
447 write_fn_start_capture (void)
448 {
449   write_fn_discard_capture_buffer ();
450   write_fn_buffer = NULL;
451   write_fn_len = 0;
452 }
453
454 /* Finish capture and return the capture buffer.  Caller must free. */
455 static char *
456 write_fn_finish_capture (void)
457 {
458   char *ret = write_fn_buffer;
459
460   write_fn_buffer = NULL;
461   write_fn_len = -1;
462   return ret;
463 }
464
465 /* Stop capturing and discard the capture buffer, if any. */
466 static void
467 write_fn_discard_capture_buffer (void)
468 {
469   g_free (write_fn_buffer);
470   write_fn_buffer = NULL;
471   write_fn_len = -1;
472 }
473
474 static size_t
475 header_fn (void *ptr, size_t size, size_t nmemb, void *stream)
476 {
477   int bytes = size * nmemb;
478   return bytes;
479 }
480
481 /* Called from the message loop to initialize the CURL handle. */
482 static void
483 do_curl_init (void)
484 {
485   DEBUG ("initializing libcurl");
486
487   curl = curl_easy_init ();
488   if (!curl) {             /* This is probably quite bad, so abort. */
489     DEBUG ("curl_easy_init failed");
490     abort ();
491   }
492
493   CURL_CHECK_ERROR (curl_easy_setopt,
494                     (curl, CURLOPT_CAINFO, cainfo));
495   CURL_CHECK_ERROR (curl_easy_setopt,
496                     (curl, CURLOPT_SSL_VERIFYHOST, check_cert ? 2 : 0));
497   CURL_CHECK_ERROR (curl_easy_setopt,
498                     (curl, CURLOPT_SSL_VERIFYPEER, check_cert ? 1 : 0));
499
500   CURL_CHECK_ERROR (curl_easy_setopt,
501                     (curl, CURLOPT_WRITEFUNCTION, write_fn));
502   CURL_CHECK_ERROR (curl_easy_setopt,
503                     (curl, CURLOPT_HEADERFUNCTION, header_fn));
504
505   /* This enables error messages in curl_easy_perform. */
506   CURL_CHECK_ERROR (curl_easy_setopt,
507                     (curl, CURLOPT_ERRORBUFFER, curl_error_buffer));
508
509   /* This enables cookie handling, using an internal cookiejar. */
510   CURL_CHECK_ERROR (curl_easy_setopt,
511                     (curl, CURLOPT_COOKIEFILE, ""));
512 }
513
514 /* Called from the message loop.  Try to connect to the current URI.
515  * Returns true on success.
516  */
517 static gboolean
518 do_connect (void)
519 {
520   long code = 0;
521   CURLcode r;
522   char *error_str;
523
524   DEBUG ("connecting to uri %s", uri);
525
526   /* Set the URI for libcurl. */
527   CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_URL, uri));
528
529   /* Try to fetch the URI. */
530   r = CURL_CHECK_ERROR (curl_easy_perform, (curl));
531   if (r != CURLE_OK) {
532     /* Signal an error back to the main thread. */
533     error_str = g_strdup (curl_easy_strerror (r));
534     g_idle_add (main_connection_error, error_str);
535     return FALSE;
536   }
537
538   CURL_CHECK_ERROR (curl_easy_getinfo, (curl, CURLINFO_RESPONSE_CODE, &code));
539   DEBUG ("HTTP return code is %ld", code);
540   if (code != 200 && code != 302 && code != 401) {
541     /* XXX If only glib had g_asprintf. */
542     error_str = g_strdup ("unexpected HTTP return code from server");
543     g_idle_add (main_connection_error, error_str);
544     return FALSE;
545   }
546
547   return TRUE;
548 }
549
550 /* Called from the message loop.  Try to login to 'URI/login' with the
551  * current username and password.  Returns true on success.
552  */
553 static gboolean
554 do_login (void)
555 {
556   int len;
557   char *login_uri;
558   char *userpwd;
559   char *error_str;
560   CURLcode r;
561   long code = 0;
562
563   DEBUG ("logging in with username %s, password *****", username);
564
565   /* Generate the login URI from the base URI. */
566   len = strlen (uri) + 6 + 1;
567   login_uri = g_alloca (len);
568   snprintf (login_uri, len, "%s/login", uri);
569
570   DEBUG ("login URI %s", login_uri);
571
572   /* Set the URI for libcurl. */
573   CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_URL, login_uri));
574
575   /* Construct the username:password for CURL. */
576   len = strlen (username) + strlen (password) + 2;
577   userpwd = g_alloca (len);
578   snprintf (userpwd, len, "%s:%s", username, password);
579
580   CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_USERPWD, userpwd));
581
582   /* HTTP Basic authentication is OK since we should be sending
583    * this only over HTTPS.
584    */
585   CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC));
586
587   /* Follow redirects. */
588   CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_FOLLOWLOCATION, (long) 1));
589   CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_MAXREDIRS, (long) 10));
590
591   /* Try to fetch the URI. */
592   r = CURL_CHECK_ERROR (curl_easy_perform, (curl));
593   if (r != CURLE_OK) {
594     /* Signal an error back to the main thread. */
595     error_str = g_strdup (curl_easy_strerror (r));
596     g_idle_add (main_login_error, error_str);
597     return FALSE;
598   }
599
600   CURL_CHECK_ERROR (curl_easy_getinfo, (curl, CURLINFO_RESPONSE_CODE, &code));
601   DEBUG ("HTTP return code is %ld", code);
602   switch (code)
603     {
604     case 200:
605       DEBUG ("login was successful");
606       return TRUE;
607
608     case 401:
609       error_str = g_strdup ("server rejected the username or password");
610       g_idle_add (main_login_error, error_str);
611       return FALSE;
612
613     default:
614       /* XXX If only glib had g_asprintf. */
615       error_str = g_strdup ("unexpected HTTP return code from server");
616       g_idle_add (main_login_error, error_str);
617       return FALSE;
618     }
619 }
620
621 /* Called from the message loop.  Refresh the list of VMs. */
622 static gboolean
623 refresh_vm_list (void)
624 {
625   int len;
626   char *vms_uri;
627   char *error_str;
628   CURLcode r;
629   long code = 0;
630   char *xml;
631
632   DEBUG ("refreshing list of VMs");
633
634   /* Generate the vms URI from the base URI. */
635   len = strlen (uri) + 4 + 1;
636   vms_uri = g_alloca (len);
637   snprintf (vms_uri, len, "%s/vms", uri);
638
639   DEBUG ("vms URI %s", vms_uri);
640
641   /* Set the URI for libcurl. */
642   CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_URL, vms_uri));
643
644   /* We want to capture the output, so tell our write function
645    * to place the output into a buffer.
646    */
647   write_fn_start_capture ();
648
649   /* Try to fetch the URI. */
650   r = CURL_CHECK_ERROR (curl_easy_perform, (curl));
651   if (r != CURLE_OK) {
652     /* Signal an error back to the main thread. */
653     error_str = g_strdup (curl_easy_strerror (r));
654     g_idle_add (main_login_error, error_str);
655     return FALSE;
656   }
657
658   CURL_CHECK_ERROR (curl_easy_getinfo, (curl, CURLINFO_RESPONSE_CODE, &code));
659   DEBUG ("HTTP return code is %ld", code);
660   switch (code)
661     {
662     case 200: break;
663
664       /* Hmm - even though we previously logged in, the server is
665        * rejecting our attempts now with an authorization error.
666        * We move to the logged out state.
667        */
668     case 401:
669       set_logged_in (FALSE);
670       error_str = g_strdup ("server rejected the username or password");
671       g_idle_add (main_login_error, error_str);
672       return FALSE;
673
674     default:
675       /* XXX If only glib had g_asprintf. */
676       error_str = g_strdup ("unexpected HTTP return code from server");
677       g_idle_add (main_status_error, error_str);
678       return FALSE;
679     }
680
681   /* If we got here then we appear to have a correct
682    * XML document describing the list of VMs.
683    */
684   secs_between_refresh <<= 1;
685
686   xml = write_fn_finish_capture ();
687
688   /*DEBUG ("XML from /vms =\n%s", xml);*/
689   parse_vmlist_from_xml (xml);
690   g_free (xml);
691
692   return TRUE;
693 }
694
695 /* Functions to deal with the list of VMs, parsing it from the XML, etc.
696  *
697  * A vmlist is a GSList (glib singly-linked list) of vm structures.
698  * The caller must call free_vmlist to free up this list correctly.
699  */
700
701 static void
702 free_vm (gpointer _vm, gpointer data)
703 {
704   struct vm *vm = (struct vm *) _vm;
705
706   g_free (vm->description);
707   g_free (vm->state);
708   g_free (vm->uuid);
709   g_free (vm->mac_addr);
710   g_free (vm);
711 }
712
713 void
714 free_vmlist (GSList *vmlist)
715 {
716   g_slist_foreach (vmlist, free_vm, NULL);
717   g_slist_free (vmlist);
718 }
719
720 static struct vm *
721 copy_vm (struct vm *vm)
722 {
723   struct vm *vm2;
724
725   vm2 = g_memdup (vm, sizeof (*vm));
726   vm2->description = g_strdup (vm->description);
727   vm2->state = g_strdup (vm->state);
728   vm2->uuid = g_strdup (vm->uuid);
729   vm2->mac_addr = g_strdup (vm->mac_addr);
730   return vm2;
731 }
732
733 static int
734 compare_vm (gconstpointer _vm1, gconstpointer _vm2)
735 {
736   const struct vm *vm1 = (const struct vm *) _vm1;
737   const struct vm *vm2 = (const struct vm *) _vm2;
738
739   return strcmp (vm1->description, vm2->description);
740 }
741
742 static GSList *vmlist = NULL;
743 static gboolean vmlist_valid = FALSE;
744 static GStaticMutex vmlist_mutex;
745
746 /* Called from the main thread to find out if we have a valid vmlist. */
747 gboolean
748 wui_thread_has_valid_vmlist (void)
749 {
750   gboolean ret;
751
752   g_static_mutex_lock (&vmlist_mutex);
753   ret = vmlist_valid;
754   g_static_mutex_unlock (&vmlist_mutex);
755   return ret;
756 }
757
758 /* Called from the main thread to find return the current vmlist.  This
759  * actually returns a deep copy of it that the caller must free.
760  */
761 static void
762 duplicate_and_insert_vm (gpointer _vm, gpointer _ret)
763 {
764   struct vm *vm = (struct vm *) _vm;
765   GSList **ret = (GSList **) _ret;
766
767   *ret = g_slist_prepend (*ret, copy_vm (vm));
768 }
769
770 gboolean
771 wui_thread_get_vmlist (GSList **ret)
772 {
773   gboolean r;
774
775   r = FALSE;
776   *ret = NULL;
777
778   g_static_mutex_lock (&vmlist_mutex);
779   if (!vmlist_valid) goto done;
780
781   g_slist_foreach (vmlist, duplicate_and_insert_vm, ret);
782   *ret = g_slist_sort (*ret, compare_vm);
783
784   r = TRUE;
785  done:
786   g_static_mutex_unlock (&vmlist_mutex);
787   return r;
788 }
789
790 /* Called from the message loop in the WUI thread, with an XML document
791  * which we turn into a list of VM structures, and update the vmlist
792  * if we can.
793  */
794 static void
795 parse_vmlist_from_xml (const char *xml)
796 {
797   
798
799
800
801
802
803
804 }