1 /* ovirt viewer console application
2 * Copyright (C) 2008 Red Hat Inc.
3 * Written by Richard W.M. Jones <rjones@redhat.com>
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.
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.
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.
28 #include <glib/gprintf.h>
30 #include <curl/curl.h>
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);
46 /* Messages (main thread -> WUI thread only).
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).
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. */
60 enum message_type type;
65 /* Start the WUI thread. See main() for explanation of the threading model. */
66 static GThread *wui_gthread = NULL;
67 static GThread *main_gthread = NULL;
68 static GAsyncQueue *wui_thread_queue = NULL;
71 start_wui_thread (void)
75 DEBUG ("starting the WUI thread");
77 assert (wui_gthread == NULL);
79 main_gthread = g_thread_self ();
81 /* Create the message queue for main -> WUI thread communications. */
82 wui_thread_queue = g_async_queue_new ();
84 wui_gthread = g_thread_create (wui_thread, wui_thread_queue, TRUE, &error);
86 g_print ("%s\n", error->message);
93 stop_wui_thread (void)
95 DEBUG ("stopping the WUI thread");
97 assert (wui_gthread != NULL);
98 ASSERT_IS_MAIN_THREAD ();
100 /* Send a quit message then wait for the WUI thread to join.
102 * This "nice" shutdown could cause the UI to hang for an
103 * indefinite period (eg. if the WUI thread is engaged in some
104 * long connection or download from the remote server). But
105 * I want to keep it this way for the moment so that we can
106 * diagnose problems with the WUI thread.
108 * (This could be solved with some sort of interruptible
109 * join, but glib doesn't support that AFAICT).
111 wui_thread_send_quit ();
112 (void) g_thread_join (wui_gthread);
113 g_async_queue_unref (wui_thread_queue);
118 assert_is_wui_thread (const char *filename, int lineno)
120 if (g_thread_self () != wui_gthread) {
121 fprintf (stderr, "%s:%d: internal error: this function should only run in the context of the WUI thread\n", filename, lineno);
127 assert_is_main_thread (const char *filename, int lineno)
129 if (g_thread_self () != main_gthread) {
130 fprintf (stderr, "%s:%d: internal error: this function should only run in the context of the main thread\n", filename, lineno);
135 /* Send the quit message to the WUI thread. */
137 wui_thread_send_quit (void)
141 ASSERT_IS_MAIN_THREAD ();
143 msg = g_new (struct message, 1);
145 g_async_queue_push (wui_thread_queue, msg);
148 /* Send the connect message to the WUI thread. */
150 wui_thread_send_connect (const char *uri)
154 ASSERT_IS_MAIN_THREAD ();
156 msg = g_new (struct message, 1);
158 msg->str1 = g_strdup (uri);
159 g_async_queue_push (wui_thread_queue, msg);
162 /* Send the disconnect message to the WUI thread. */
164 wui_thread_send_disconnect (void)
168 ASSERT_IS_MAIN_THREAD ();
170 msg = g_new (struct message, 1);
171 msg->type = DISCONNECT;
172 g_async_queue_push (wui_thread_queue, msg);
175 /* Send the login message to the WUI thread. */
177 wui_thread_send_login (const char *username, const char *password)
181 ASSERT_IS_MAIN_THREAD ();
183 msg = g_new (struct message, 1);
185 msg->str1 = g_strdup (username);
186 msg->str2 = g_strdup (password);
187 g_async_queue_push (wui_thread_queue, msg);
190 /* Send the refresh VM list message to the WUI thread. */
192 wui_thread_send_refresh_vm_list (void)
196 ASSERT_IS_MAIN_THREAD ();
198 msg = g_new (struct message, 1);
199 msg->type = REFRESH_VM_LIST;
200 g_async_queue_push (wui_thread_queue, msg);
203 /* The current state.
205 * For safety, the main thread must lock this before reading, and the
206 * WUI thread must lock this before writing. However the WUI thread
207 * does not need to lock before reading, because no other thread
210 static gboolean connected = FALSE;
211 static gboolean logged_in = FALSE;
212 static gboolean busy = FALSE;
213 static GStaticMutex state_mutex;
215 static void set_connected (gboolean);
216 static void set_logged_in (gboolean);
217 static void set_busy (gboolean);
219 /* The private state of the WUI thread. */
220 static int secs_between_refresh = 60;
221 static CURL *curl = NULL;
222 static char curl_error_buffer[CURL_ERROR_SIZE];
223 static char *uri = NULL;
224 static char *username = NULL;
225 static char *password = NULL;
227 static gboolean process_message (struct message *);
229 /* The WUI thread. See main() above for explanation of
230 * the threading model.
233 wui_thread (gpointer _queue)
235 GAsyncQueue *queue = (GAsyncQueue *) _queue;
236 gboolean quit = FALSE;
241 DEBUG ("WUI thread starting up");
243 ASSERT_IS_WUI_THREAD ();
245 g_async_queue_ref (queue);
247 /* In the thread's loop we check for new instructions from the main
248 * thread and carry them out. Also, if we are connected and logged
249 * in then we periodically recheck the list of VMs.
255 g_get_current_time (&tv);
256 g_time_val_add (&tv, secs_between_refresh * 1000000);
257 _msg = g_async_queue_timed_pop (queue, &tv);
259 _msg = g_async_queue_pop (queue);
263 msg = (struct message *) _msg;
266 DEBUG ("received message with msg->type = %d", msg->type);
267 quit = process_message (msg);
268 /* Don't free any strings in the message - we've saved them. */
271 /* No message, must have got a timeout instead, which means
272 * we are logged in and we should refresh the list of VMs.
273 * Note it's not an error if we temporarily lose contact
280 DEBUG ("WUI thread shutting down cleanly");
282 g_async_queue_unref (queue);
283 g_thread_exit (NULL);
284 return NULL; /* not reached? */
287 /* The WUI thread calls this to safely update the state variables.
288 * This also updates elements in the UI by setting idle callbacks
289 * which are executed in the context of the main thread.
292 set_connected (gboolean new_connected)
294 ASSERT_IS_WUI_THREAD ();
296 g_static_mutex_lock (&state_mutex);
297 connected = new_connected;
298 g_static_mutex_unlock (&state_mutex);
301 g_idle_add (main_connected, NULL);
303 g_idle_add (main_disconnected, NULL);
307 set_logged_in (gboolean new_logged_in)
309 ASSERT_IS_WUI_THREAD ();
311 g_static_mutex_lock (&state_mutex);
312 logged_in = new_logged_in;
313 g_static_mutex_unlock (&state_mutex);
316 g_idle_add (main_logged_in, NULL);
318 g_idle_add (main_logged_out, NULL);
322 set_busy (gboolean new_busy)
324 ASSERT_IS_WUI_THREAD ();
326 g_static_mutex_lock (&state_mutex);
328 g_static_mutex_unlock (&state_mutex);
331 g_idle_add (main_busy, NULL);
333 g_idle_add (main_idle, NULL);
336 /* The main thread should call these functions to get the WUI thread's
340 wui_thread_is_connected (void)
344 ASSERT_IS_MAIN_THREAD ();
346 g_static_mutex_lock (&state_mutex);
348 g_static_mutex_unlock (&state_mutex);
353 wui_thread_is_logged_in (void)
357 ASSERT_IS_MAIN_THREAD ();
359 g_static_mutex_lock (&state_mutex);
361 g_static_mutex_unlock (&state_mutex);
366 wui_thread_is_busy (void)
370 ASSERT_IS_MAIN_THREAD ();
372 g_static_mutex_lock (&state_mutex);
374 g_static_mutex_unlock (&state_mutex);
378 /* Process a message from the main thread. */
380 process_message (struct message *msg)
382 ASSERT_IS_WUI_THREAD ();
386 write_fn_discard_capture_buffer ();
387 if (curl) curl_easy_cleanup (curl);
388 if (uri) g_free (uri);
389 if (username) g_free (username);
390 if (password) g_free (password);
391 set_connected (FALSE);
392 set_logged_in (FALSE);
396 write_fn_discard_capture_buffer ();
397 if (curl) curl_easy_cleanup (curl);
399 if (uri) g_free (uri);
403 set_connected (TRUE);
405 set_connected (FALSE);
406 set_logged_in (FALSE);
411 /* This just forgets the state. REST is connectionless. */
412 write_fn_discard_capture_buffer ();
413 if (curl) curl_easy_cleanup (curl);
415 if (uri) g_free (uri);
417 if (username) g_free (username);
419 if (password) g_free (password);
421 set_connected (FALSE);
422 set_logged_in (FALSE);
426 if (username) g_free (username);
427 username = msg->str1;
428 if (password) g_free (password);
429 password = msg->str2;
431 /* If we're not connected, this message just updates the
432 * username and password. Otherwise if we are connected,
433 * try to login and grab the initial list of VMs.
437 set_logged_in (TRUE);
438 if (refresh_vm_list ())
439 secs_between_refresh = 60;
441 set_logged_in (FALSE);
446 case REFRESH_VM_LIST:
447 if (connected && logged_in) {
449 secs_between_refresh = 60;
454 DEBUG ("unknown message type %d", msg->type);
461 /* Macro for easy handling of CURL errors. */
462 #define CURL_CHECK_ERROR(fn, args) \
464 CURLcode __r = fn args; \
465 if (__r != CURLE_OK) { \
466 fprintf (stderr, "%s: %s\n", #fn, curl_easy_strerror (__r)); \
471 /* Libcurl has a really crufty method for handling HTTP headers and
472 * data. We set these functions as callbacks, because the default
473 * callback functions print the data out to stderr. In order to
474 * capture the data, we have to keep state here.
477 static char *write_fn_buffer = NULL;
478 static ssize_t write_fn_len = -1;
481 write_fn (void *ptr, size_t size, size_t nmemb, void *stream)
483 int bytes = size * nmemb;
486 ASSERT_IS_WUI_THREAD ();
488 if (write_fn_len >= 0) { /* We're capturing. */
489 old_start = write_fn_len;
490 write_fn_len += bytes;
491 write_fn_buffer = g_realloc (write_fn_buffer, write_fn_len);
492 memcpy (write_fn_buffer + old_start, ptr, bytes);
498 /* Start capturing HTTP response data. */
500 write_fn_start_capture (void)
502 ASSERT_IS_WUI_THREAD ();
504 write_fn_discard_capture_buffer ();
505 write_fn_buffer = NULL;
509 /* Finish capture and return the capture buffer. Caller must free. */
511 write_fn_finish_capture (void)
513 char *ret = write_fn_buffer;
515 ASSERT_IS_WUI_THREAD ();
517 write_fn_buffer = NULL;
522 /* Stop capturing and discard the capture buffer, if any. */
524 write_fn_discard_capture_buffer (void)
526 ASSERT_IS_WUI_THREAD ();
528 g_free (write_fn_buffer);
529 write_fn_buffer = NULL;
534 header_fn (void *ptr, size_t size, size_t nmemb, void *stream)
536 int bytes = size * nmemb;
538 ASSERT_IS_WUI_THREAD ();
543 /* Called from the message loop to initialize the CURL handle. */
547 DEBUG ("initializing libcurl");
549 ASSERT_IS_WUI_THREAD ();
551 curl = curl_easy_init ();
552 if (!curl) { /* This is probably quite bad, so abort. */
553 DEBUG ("curl_easy_init failed");
557 CURL_CHECK_ERROR (curl_easy_setopt,
558 (curl, CURLOPT_CAINFO, cainfo));
559 CURL_CHECK_ERROR (curl_easy_setopt,
560 (curl, CURLOPT_SSL_VERIFYHOST, check_cert ? 2 : 0));
561 CURL_CHECK_ERROR (curl_easy_setopt,
562 (curl, CURLOPT_SSL_VERIFYPEER, check_cert ? 1 : 0));
564 CURL_CHECK_ERROR (curl_easy_setopt,
565 (curl, CURLOPT_WRITEFUNCTION, write_fn));
566 CURL_CHECK_ERROR (curl_easy_setopt,
567 (curl, CURLOPT_HEADERFUNCTION, header_fn));
569 /* This enables error messages in curl_easy_perform. */
570 CURL_CHECK_ERROR (curl_easy_setopt,
571 (curl, CURLOPT_ERRORBUFFER, curl_error_buffer));
573 /* This enables cookie handling, using an internal cookiejar. */
574 CURL_CHECK_ERROR (curl_easy_setopt,
575 (curl, CURLOPT_COOKIEFILE, ""));
578 /* Called from the message loop. Try to connect to the current URI.
579 * Returns true on success.
588 DEBUG ("connecting to uri %s", uri);
589 ASSERT_IS_WUI_THREAD ();
591 /* Set the URI for libcurl. */
592 CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_URL, uri));
594 /* Try to fetch the URI. */
595 r = CURL_CHECK_ERROR (curl_easy_perform, (curl));
597 /* Signal an error back to the main thread. */
598 error_str = g_strdup (curl_easy_strerror (r));
599 g_idle_add (main_connection_error, error_str);
603 CURL_CHECK_ERROR (curl_easy_getinfo, (curl, CURLINFO_RESPONSE_CODE, &code));
604 DEBUG ("HTTP return code is %ld", code);
605 if (code != 200 && code != 302 && code != 401) {
606 /* XXX If only glib had g_asprintf. */
607 error_str = g_strdup ("unexpected HTTP return code from server");
608 g_idle_add (main_connection_error, error_str);
615 /* Called from the message loop. Try to login to 'URI/login' with the
616 * current username and password. Returns true on success.
628 DEBUG ("logging in with username %s, password *****", username);
629 ASSERT_IS_WUI_THREAD ();
631 /* Generate the login URI from the base URI. */
632 len = strlen (uri) + 6 + 1;
633 login_uri = g_alloca (len);
634 snprintf (login_uri, len, "%s/login", uri);
636 DEBUG ("login URI %s", login_uri);
638 /* Set the URI for libcurl. */
639 CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_URL, login_uri));
641 /* Construct the username:password for CURL. */
642 len = strlen (username) + strlen (password) + 2;
643 userpwd = g_alloca (len);
644 snprintf (userpwd, len, "%s:%s", username, password);
646 CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_USERPWD, userpwd));
648 /* HTTP Basic authentication is OK since we should be sending
649 * this only over HTTPS.
651 CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC));
653 /* Follow redirects. */
654 CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_FOLLOWLOCATION, (long) 1));
655 CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_MAXREDIRS, (long) 10));
657 /* Try to fetch the URI. */
658 r = CURL_CHECK_ERROR (curl_easy_perform, (curl));
660 /* Signal an error back to the main thread. */
661 error_str = g_strdup (curl_easy_strerror (r));
662 g_idle_add (main_login_error, error_str);
666 CURL_CHECK_ERROR (curl_easy_getinfo, (curl, CURLINFO_RESPONSE_CODE, &code));
667 DEBUG ("HTTP return code is %ld", code);
671 DEBUG ("login was successful");
675 error_str = g_strdup ("server rejected the username or password");
676 g_idle_add (main_login_error, error_str);
680 /* XXX If only glib had g_asprintf. */
681 error_str = g_strdup ("unexpected HTTP return code from server");
682 g_idle_add (main_login_error, error_str);
687 /* Called from the message loop. Refresh the list of VMs. */
689 refresh_vm_list (void)
698 DEBUG ("refreshing list of VMs");
699 ASSERT_IS_WUI_THREAD ();
701 /* Generate the vms URI from the base URI. */
702 len = strlen (uri) + 4 + 1;
703 vms_uri = g_alloca (len);
704 snprintf (vms_uri, len, "%s/vms", uri);
706 DEBUG ("vms URI %s", vms_uri);
708 /* Set the URI for libcurl. */
709 CURL_CHECK_ERROR (curl_easy_setopt, (curl, CURLOPT_URL, vms_uri));
711 /* We want to capture the output, so tell our write function
712 * to place the output into a buffer.
714 write_fn_start_capture ();
716 /* Try to fetch the URI. */
717 r = CURL_CHECK_ERROR (curl_easy_perform, (curl));
719 /* Signal an error back to the main thread. */
720 error_str = g_strdup (curl_easy_strerror (r));
721 g_idle_add (main_login_error, error_str);
725 CURL_CHECK_ERROR (curl_easy_getinfo, (curl, CURLINFO_RESPONSE_CODE, &code));
726 DEBUG ("HTTP return code is %ld", code);
731 /* Hmm - even though we previously logged in, the server is
732 * rejecting our attempts now with an authorization error.
733 * We move to the logged out state.
736 set_logged_in (FALSE);
737 error_str = g_strdup ("server rejected the username or password");
738 g_idle_add (main_login_error, error_str);
742 /* XXX If only glib had g_asprintf. */
743 error_str = g_strdup ("unexpected HTTP return code from server");
744 g_idle_add (main_status_error, error_str);
748 /* If we got here then we appear to have a correct
749 * XML document describing the list of VMs.
751 secs_between_refresh <<= 1;
753 xml = write_fn_finish_capture ();
755 /*DEBUG ("XML from /vms =\n%s", xml);*/
756 parse_vmlist_from_xml (xml);
762 /* Functions to deal with the list of VMs, parsing it from the XML, etc.
764 * A vmlist is a GSList (glib singly-linked list) of vm structures.
765 * The caller must call free_vmlist to free up this list correctly.
769 free_vm (gpointer _vm, gpointer data)
771 struct vm *vm = (struct vm *) _vm;
773 g_free (vm->description);
776 g_free (vm->mac_addr);
781 free_vmlist (GSList *vmlist)
783 g_slist_foreach (vmlist, free_vm, NULL);
784 g_slist_free (vmlist);
788 copy_vm (struct vm *vm)
792 vm2 = g_memdup (vm, sizeof (*vm));
793 vm2->description = g_strdup (vm->description);
794 vm2->state = g_strdup (vm->state);
795 vm2->uuid = g_strdup (vm->uuid);
796 vm2->mac_addr = g_strdup (vm->mac_addr);
801 compare_vm (gconstpointer _vm1, gconstpointer _vm2)
803 const struct vm *vm1 = (const struct vm *) _vm1;
804 const struct vm *vm2 = (const struct vm *) _vm2;
806 return strcmp (vm1->description, vm2->description);
809 static GSList *vmlist = NULL;
810 static gboolean vmlist_valid = FALSE;
811 static GStaticMutex vmlist_mutex;
813 /* Called from the main thread to find out if we have a valid vmlist. */
815 wui_thread_has_valid_vmlist (void)
819 ASSERT_IS_MAIN_THREAD ();
821 g_static_mutex_lock (&vmlist_mutex);
823 g_static_mutex_unlock (&vmlist_mutex);
827 /* Called from the main thread to find return the current vmlist. This
828 * actually returns a deep copy of it that the caller must free.
831 duplicate_and_insert_vm (gpointer _vm, gpointer _ret)
833 struct vm *vm = (struct vm *) _vm;
834 GSList **ret = (GSList **) _ret;
836 *ret = g_slist_prepend (*ret, copy_vm (vm));
840 wui_thread_get_vmlist (GSList **ret)
844 ASSERT_IS_MAIN_THREAD ();
849 g_static_mutex_lock (&vmlist_mutex);
850 if (!vmlist_valid) goto done;
852 g_slist_foreach (vmlist, duplicate_and_insert_vm, ret);
853 *ret = g_slist_sort (*ret, compare_vm);
857 g_static_mutex_unlock (&vmlist_mutex);
861 /* Called from the message loop in the WUI thread, with an XML document
862 * which we turn into a list of VM structures, and update the vmlist
866 parse_vmlist_from_xml (const char *xml)