mirror of
https://gitlab.uni-freiburg.de/opensourcevdi/virt-viewer
synced 2025-12-27 06:23:42 +00:00
By default when connecting with VNC, an existing viewer will be kicked off. This adds a --shared / -s flag which tells the server we are willing the share the session with other clients. This is wired up for VNC only. https://bugzilla.redhat.com/show_bug.cgi?id=1060425 Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>
1183 lines
36 KiB
C
1183 lines
36 KiB
C
/*
|
|
* Virt Viewer: A virtual machine console viewer
|
|
*
|
|
* Copyright (C) 2007-2012 Red Hat, Inc.
|
|
* Copyright (C) 2009-2012 Daniel P. Berrange
|
|
* Copyright (C) 2010 Marc-André Lureau
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
*
|
|
* Author: Daniel P. Berrange <berrange@redhat.com>
|
|
*/
|
|
|
|
#include <config.h>
|
|
|
|
#include <gdk/gdkkeysyms.h>
|
|
#include <gtk/gtk.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <locale.h>
|
|
#include <gio/gio.h>
|
|
#include <glib/gprintf.h>
|
|
#include <glib/gi18n.h>
|
|
|
|
#include <libvirt/libvirt.h>
|
|
#include <libvirt/virterror.h>
|
|
#include <libvirt-glib/libvirt-glib.h>
|
|
#include <libxml/xpath.h>
|
|
#include <libxml/uri.h>
|
|
|
|
#if defined(HAVE_SOCKETPAIR)
|
|
#include <sys/socket.h>
|
|
#endif
|
|
|
|
#include "virt-viewer.h"
|
|
#include "virt-viewer-app.h"
|
|
#include "virt-viewer-vm-connection.h"
|
|
#include "virt-viewer-auth.h"
|
|
#include "virt-viewer-util.h"
|
|
|
|
#ifdef HAVE_SPICE_GTK
|
|
#include "virt-viewer-session-spice.h"
|
|
#endif
|
|
|
|
struct _VirtViewerPrivate {
|
|
char *uri;
|
|
virConnectPtr conn;
|
|
virDomainPtr dom;
|
|
char *domkey;
|
|
gboolean waitvm;
|
|
gboolean reconnect;
|
|
gboolean auth_cancelled;
|
|
gint domain_event;
|
|
guint reconnect_poll; /* source id */
|
|
};
|
|
|
|
G_DEFINE_TYPE_WITH_PRIVATE (VirtViewer, virt_viewer, VIRT_VIEWER_TYPE_APP)
|
|
|
|
static gboolean virt_viewer_initial_connect(VirtViewerApp *self, GError **error);
|
|
static gboolean virt_viewer_open_connection(VirtViewerApp *self, int *fd);
|
|
static void virt_viewer_deactivated(VirtViewerApp *self, gboolean connect_error);
|
|
static gboolean virt_viewer_start(VirtViewerApp *self, GError **error);
|
|
static void virt_viewer_dispose (GObject *object);
|
|
static int virt_viewer_connect(VirtViewerApp *app, GError **error);
|
|
|
|
static gchar **opt_args = NULL;
|
|
static gchar *opt_uri = NULL;
|
|
static gboolean opt_direct = FALSE;
|
|
static gboolean opt_attach = FALSE;
|
|
static gboolean opt_waitvm = FALSE;
|
|
static gboolean opt_reconnect = FALSE;
|
|
static gboolean opt_shared = FALSE;
|
|
|
|
typedef enum {
|
|
DOMAIN_SELECTION_ID = (1 << 0),
|
|
DOMAIN_SELECTION_UUID = (1 << 1),
|
|
DOMAIN_SELECTION_NAME = (1 << 2),
|
|
DOMAIN_SELECTION_DEFAULT = DOMAIN_SELECTION_ID | DOMAIN_SELECTION_UUID | DOMAIN_SELECTION_NAME,
|
|
} DomainSelection;
|
|
|
|
static const gchar* domain_selection_to_opt[] = {
|
|
[DOMAIN_SELECTION_ID] = "--id",
|
|
[DOMAIN_SELECTION_UUID] = "--uuid",
|
|
[DOMAIN_SELECTION_NAME] = "--domain-name",
|
|
};
|
|
|
|
static DomainSelection domain_selection_type = DOMAIN_SELECTION_DEFAULT;
|
|
|
|
static gboolean
|
|
opt_domain_selection_cb(const gchar *option_name,
|
|
const gchar *value G_GNUC_UNUSED,
|
|
gpointer data G_GNUC_UNUSED,
|
|
GError **error)
|
|
{
|
|
guint i;
|
|
if (domain_selection_type != DOMAIN_SELECTION_DEFAULT) {
|
|
g_set_error(error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED,
|
|
"selection type has been already set");
|
|
return FALSE;
|
|
}
|
|
|
|
for (i = DOMAIN_SELECTION_ID; i < G_N_ELEMENTS(domain_selection_to_opt); i++) {
|
|
if (g_strcmp0(option_name, domain_selection_to_opt[i]) == 0) {
|
|
domain_selection_type = i;
|
|
return TRUE;
|
|
}
|
|
}
|
|
|
|
g_assert_not_reached();
|
|
return FALSE;
|
|
}
|
|
|
|
static void
|
|
virt_viewer_add_option_entries(VirtViewerApp *self, GOptionContext *context, GOptionGroup *group)
|
|
{
|
|
static const GOptionEntry options[] = {
|
|
{ "direct", 'd', 0, G_OPTION_ARG_NONE, &opt_direct,
|
|
N_("Direct connection with no automatic tunnels"), NULL },
|
|
{ "attach", 'a', 0, G_OPTION_ARG_NONE, &opt_attach,
|
|
N_("Attach to the local display using libvirt"), NULL },
|
|
{ "connect", 'c', 0, G_OPTION_ARG_STRING, &opt_uri,
|
|
N_("Connect to hypervisor"), "URI"},
|
|
{ "wait", 'w', 0, G_OPTION_ARG_NONE, &opt_waitvm,
|
|
N_("Wait for domain to start"), NULL },
|
|
{ "reconnect", 'r', 0, G_OPTION_ARG_NONE, &opt_reconnect,
|
|
N_("Reconnect to domain upon restart"), NULL },
|
|
{ "domain-name", '\0', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_domain_selection_cb,
|
|
N_("Select the virtual machine only by its name"), NULL },
|
|
{ "id", '\0', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_domain_selection_cb,
|
|
N_("Select the virtual machine only by its ID"), NULL },
|
|
{ "uuid", '\0', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, opt_domain_selection_cb,
|
|
N_("Select the virtual machine only by its UUID"), NULL },
|
|
{ "shared", 's', 0, G_OPTION_ARG_NONE, &opt_shared,
|
|
N_("Share client session"), NULL },
|
|
{ G_OPTION_REMAINING, '\0', 0, G_OPTION_ARG_STRING_ARRAY, &opt_args,
|
|
NULL, "-- ID|UUID|DOMAIN-NAME" },
|
|
{ NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL }
|
|
};
|
|
|
|
VIRT_VIEWER_APP_CLASS(virt_viewer_parent_class)->add_option_entries(self, context, group);
|
|
g_option_context_set_summary(context, _("Virtual machine graphical console"));
|
|
g_option_group_add_entries(group, options);
|
|
}
|
|
|
|
static gboolean
|
|
virt_viewer_local_command_line (GApplication *gapp,
|
|
gchar ***args,
|
|
int *status)
|
|
{
|
|
gboolean ret = FALSE;
|
|
VirtViewer *self = VIRT_VIEWER(gapp);
|
|
VirtViewerApp *app = VIRT_VIEWER_APP(gapp);
|
|
|
|
ret = G_APPLICATION_CLASS(virt_viewer_parent_class)->local_command_line(gapp, args, status);
|
|
if (ret)
|
|
goto end;
|
|
|
|
if (opt_args) {
|
|
if (g_strv_length(opt_args) != 1) {
|
|
g_printerr(_("\nUsage: %s [OPTIONS] [ID|UUID|DOMAIN-NAME]\n\n"), PACKAGE);
|
|
ret = TRUE;
|
|
*status = 1;
|
|
goto end;
|
|
}
|
|
|
|
self->priv->domkey = g_strdup(opt_args[0]);
|
|
}
|
|
|
|
|
|
if (opt_waitvm || domain_selection_type != DOMAIN_SELECTION_DEFAULT) {
|
|
if (!self->priv->domkey) {
|
|
g_printerr(_("\nNo ID|UUID|DOMAIN-NAME was specified for '%s'\n\n"),
|
|
opt_waitvm ? "--wait" : domain_selection_to_opt[domain_selection_type]);
|
|
ret = TRUE;
|
|
*status = 1;
|
|
goto end;
|
|
}
|
|
|
|
self->priv->waitvm = opt_waitvm;
|
|
}
|
|
|
|
virt_viewer_app_set_direct(app, opt_direct);
|
|
virt_viewer_app_set_attach(app, opt_attach);
|
|
virt_viewer_app_set_shared(app, opt_shared);
|
|
self->priv->reconnect = opt_reconnect;
|
|
self->priv->uri = g_strdup(opt_uri);
|
|
|
|
end:
|
|
if (ret && *status)
|
|
g_printerr(_("Run '%s --help' to see a full list of available command line options\n"), g_get_prgname());
|
|
|
|
g_strfreev(opt_args);
|
|
g_free(opt_uri);
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
virt_viewer_class_init (VirtViewerClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
VirtViewerAppClass *app_class = VIRT_VIEWER_APP_CLASS (klass);
|
|
GApplicationClass *g_app_class = G_APPLICATION_CLASS(klass);
|
|
|
|
object_class->dispose = virt_viewer_dispose;
|
|
|
|
app_class->initial_connect = virt_viewer_initial_connect;
|
|
app_class->deactivated = virt_viewer_deactivated;
|
|
app_class->open_connection = virt_viewer_open_connection;
|
|
app_class->start = virt_viewer_start;
|
|
app_class->add_option_entries = virt_viewer_add_option_entries;
|
|
|
|
g_app_class->local_command_line = virt_viewer_local_command_line;
|
|
}
|
|
|
|
static void
|
|
virt_viewer_init(VirtViewer *self)
|
|
{
|
|
self->priv = virt_viewer_get_instance_private(self);
|
|
self->priv->domain_event = -1;
|
|
}
|
|
|
|
static gboolean
|
|
virt_viewer_connect_timer(void *opaque)
|
|
{
|
|
VirtViewer *self = VIRT_VIEWER(opaque);
|
|
VirtViewerApp *app = VIRT_VIEWER_APP(self);
|
|
|
|
g_debug("Connect timer fired");
|
|
|
|
if (!virt_viewer_app_is_active(app) &&
|
|
!virt_viewer_app_initial_connect(app, NULL))
|
|
g_application_quit(G_APPLICATION(app));
|
|
|
|
if (virt_viewer_app_is_active(app)) {
|
|
self->priv->reconnect_poll = 0;
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
virt_viewer_start_reconnect_poll(VirtViewer *self)
|
|
{
|
|
VirtViewerPrivate *priv = self->priv;
|
|
|
|
g_debug("reconnect_poll: %u", priv->reconnect_poll);
|
|
|
|
if (priv->reconnect_poll != 0)
|
|
return;
|
|
|
|
priv->reconnect_poll = g_timeout_add(500, virt_viewer_connect_timer, self);
|
|
}
|
|
|
|
static void
|
|
virt_viewer_stop_reconnect_poll(VirtViewer *self)
|
|
{
|
|
VirtViewerPrivate *priv = self->priv;
|
|
|
|
g_debug("reconnect_poll: %u", priv->reconnect_poll);
|
|
|
|
if (priv->reconnect_poll == 0)
|
|
return;
|
|
|
|
g_source_remove(priv->reconnect_poll);
|
|
priv->reconnect_poll = 0;
|
|
}
|
|
|
|
static void
|
|
virt_viewer_deactivated(VirtViewerApp *app, gboolean connect_error)
|
|
{
|
|
VirtViewer *self = VIRT_VIEWER(app);
|
|
VirtViewerPrivate *priv = self->priv;
|
|
|
|
if (priv->dom) {
|
|
virDomainFree(priv->dom);
|
|
priv->dom = NULL;
|
|
}
|
|
|
|
if (priv->reconnect && !virt_viewer_app_get_session_cancelled(app)) {
|
|
if (priv->domain_event < 0) {
|
|
g_debug("No domain events, falling back to polling");
|
|
virt_viewer_start_reconnect_poll(self);
|
|
}
|
|
|
|
virt_viewer_app_show_status(app, _("Waiting for guest domain to re-start"));
|
|
virt_viewer_app_trace(app, "Guest %s display has disconnected, waiting to reconnect", priv->domkey);
|
|
virt_viewer_app_set_menus_sensitive(app, FALSE);
|
|
} else {
|
|
VIRT_VIEWER_APP_CLASS(virt_viewer_parent_class)->deactivated(app, connect_error);
|
|
}
|
|
}
|
|
|
|
static int
|
|
virt_viewer_parse_uuid(const char *name,
|
|
unsigned char *uuid)
|
|
{
|
|
int i;
|
|
|
|
const char *cur = name;
|
|
for (i = 0;i < 16;) {
|
|
uuid[i] = 0;
|
|
if (*cur == 0)
|
|
return -1;
|
|
if ((*cur == '-') || (*cur == ' ')) {
|
|
cur++;
|
|
continue;
|
|
}
|
|
if ((*cur >= '0') && (*cur <= '9'))
|
|
uuid[i] = *cur - '0';
|
|
else if ((*cur >= 'a') && (*cur <= 'f'))
|
|
uuid[i] = *cur - 'a' + 10;
|
|
else if ((*cur >= 'A') && (*cur <= 'F'))
|
|
uuid[i] = *cur - 'A' + 10;
|
|
else
|
|
return -1;
|
|
uuid[i] *= 16;
|
|
cur++;
|
|
if (*cur == 0)
|
|
return -1;
|
|
if ((*cur >= '0') && (*cur <= '9'))
|
|
uuid[i] += *cur - '0';
|
|
else if ((*cur >= 'a') && (*cur <= 'f'))
|
|
uuid[i] += *cur - 'a' + 10;
|
|
else if ((*cur >= 'A') && (*cur <= 'F'))
|
|
uuid[i] += *cur - 'A' + 10;
|
|
else
|
|
return -1;
|
|
i++;
|
|
cur++;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static virDomainPtr
|
|
virt_viewer_lookup_domain(VirtViewer *self)
|
|
{
|
|
char *end;
|
|
VirtViewerPrivate *priv = self->priv;
|
|
virDomainPtr dom = NULL;
|
|
|
|
if (priv->domkey == NULL) {
|
|
return NULL;
|
|
}
|
|
|
|
if (domain_selection_type & DOMAIN_SELECTION_ID) {
|
|
long int id = strtol(priv->domkey, &end, 10);
|
|
if (id >= 0 && end && !*end) {
|
|
dom = virDomainLookupByID(priv->conn, id);
|
|
}
|
|
}
|
|
|
|
if (domain_selection_type & DOMAIN_SELECTION_UUID) {
|
|
unsigned char uuid[16];
|
|
if (dom == NULL && virt_viewer_parse_uuid(priv->domkey, uuid) == 0) {
|
|
dom = virDomainLookupByUUID(priv->conn, uuid);
|
|
}
|
|
}
|
|
|
|
if (domain_selection_type & DOMAIN_SELECTION_NAME) {
|
|
if (dom == NULL) {
|
|
dom = virDomainLookupByName(priv->conn, priv->domkey);
|
|
}
|
|
}
|
|
|
|
return dom;
|
|
}
|
|
|
|
static int
|
|
virt_viewer_matches_domain(VirtViewer *self,
|
|
virDomainPtr dom)
|
|
{
|
|
char *end;
|
|
const char *name;
|
|
VirtViewerPrivate *priv = self->priv;
|
|
int id = strtol(priv->domkey, &end, 10);
|
|
unsigned char wantuuid[16];
|
|
unsigned char domuuid[16];
|
|
|
|
if (id >= 0 && end && !*end) {
|
|
if (virDomainGetID(dom) == id)
|
|
return 1;
|
|
}
|
|
if (virt_viewer_parse_uuid(priv->domkey, wantuuid) == 0) {
|
|
virDomainGetUUID(dom, domuuid);
|
|
if (memcmp(wantuuid, domuuid, VIR_UUID_BUFLEN) == 0)
|
|
return 1;
|
|
}
|
|
|
|
name = virDomainGetName(dom);
|
|
if (strcmp(name, priv->domkey) == 0)
|
|
return 1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static char *
|
|
virt_viewer_extract_xpath_string(const gchar *xmldesc,
|
|
const gchar *xpath)
|
|
{
|
|
xmlDocPtr xml = NULL;
|
|
xmlParserCtxtPtr pctxt = NULL;
|
|
xmlXPathContextPtr ctxt = NULL;
|
|
xmlXPathObjectPtr obj = NULL;
|
|
char *port = NULL;
|
|
|
|
pctxt = xmlNewParserCtxt();
|
|
if (!pctxt || !pctxt->sax)
|
|
goto error;
|
|
|
|
xml = xmlCtxtReadDoc(pctxt, (const xmlChar *)xmldesc, "domain.xml", NULL,
|
|
XML_PARSE_NOENT | XML_PARSE_NONET |
|
|
XML_PARSE_NOWARNING);
|
|
if (!xml)
|
|
goto error;
|
|
|
|
ctxt = xmlXPathNewContext(xml);
|
|
if (!ctxt)
|
|
goto error;
|
|
|
|
obj = xmlXPathEval((const xmlChar *)xpath, ctxt);
|
|
if (!obj || obj->type != XPATH_STRING || !obj->stringval || !obj->stringval[0])
|
|
goto error;
|
|
if (!strcmp((const char*)obj->stringval, "-1"))
|
|
goto error;
|
|
|
|
port = g_strdup((const char*)obj->stringval);
|
|
xmlXPathFreeObject(obj);
|
|
obj = NULL;
|
|
|
|
error:
|
|
xmlXPathFreeObject(obj);
|
|
xmlXPathFreeContext(ctxt);
|
|
xmlFreeDoc(xml);
|
|
xmlFreeParserCtxt(pctxt);
|
|
return port;
|
|
}
|
|
|
|
|
|
static gboolean
|
|
virt_viewer_replace_host(const gchar *host)
|
|
{
|
|
GInetAddress *addr;
|
|
gboolean ret;
|
|
|
|
if (!host)
|
|
return TRUE;
|
|
|
|
addr = g_inet_address_new_from_string(host);
|
|
|
|
if (!addr) /* Parsing error means it was probably a hostname */
|
|
return FALSE;
|
|
|
|
ret = g_inet_address_get_is_any(addr);
|
|
g_object_unref(addr);
|
|
|
|
return ret;
|
|
}
|
|
|
|
|
|
static gboolean
|
|
virt_viewer_is_loopback(const char *host)
|
|
{
|
|
GInetAddress *addr = NULL;
|
|
gboolean is_loopback = FALSE;
|
|
|
|
g_return_val_if_fail(host != NULL, FALSE);
|
|
|
|
addr = g_inet_address_new_from_string(host);
|
|
if (!addr) /* Parsing error means it was probably a hostname */
|
|
return (strcmp(host, "localhost") == 0);
|
|
|
|
is_loopback = g_inet_address_get_is_loopback(addr);
|
|
g_object_unref(addr);
|
|
|
|
return is_loopback;
|
|
}
|
|
|
|
|
|
static gboolean
|
|
virt_viewer_is_reachable(const gchar *host,
|
|
const char *transport,
|
|
const char *transport_host,
|
|
gboolean direct)
|
|
{
|
|
gboolean host_is_loopback;
|
|
gboolean transport_is_loopback;
|
|
|
|
if (!host)
|
|
return FALSE;
|
|
|
|
if (!transport)
|
|
return TRUE;
|
|
|
|
if (strcmp(transport, "ssh") == 0 && !direct)
|
|
return TRUE;
|
|
|
|
if (strcmp(transport, "unix") == 0)
|
|
return TRUE;
|
|
|
|
host_is_loopback = virt_viewer_is_loopback(host);
|
|
transport_is_loopback = virt_viewer_is_loopback(transport_host);
|
|
|
|
if (transport_is_loopback && host_is_loopback)
|
|
return TRUE;
|
|
else
|
|
return !host_is_loopback;
|
|
}
|
|
|
|
|
|
static gboolean
|
|
virt_viewer_extract_connect_info(VirtViewer *self,
|
|
virDomainPtr dom,
|
|
GError **error)
|
|
{
|
|
char *type = NULL;
|
|
char *xpath = NULL;
|
|
gboolean retval = FALSE;
|
|
char *xmldesc = virDomainGetXMLDesc(dom, 0);
|
|
VirtViewerPrivate *priv = self->priv;
|
|
VirtViewerApp *app = VIRT_VIEWER_APP(self);
|
|
gchar *gport = NULL;
|
|
gchar *gtlsport = NULL;
|
|
gchar *ghost = NULL;
|
|
gchar *unixsock = NULL;
|
|
gchar *host = NULL;
|
|
gchar *transport = NULL;
|
|
gchar *user = NULL;
|
|
gint port = 0;
|
|
gchar *uri = NULL;
|
|
gboolean direct = virt_viewer_app_get_direct(app);
|
|
|
|
virt_viewer_app_free_connect_info(app);
|
|
|
|
if ((type = virt_viewer_extract_xpath_string(xmldesc, "string(/domain/devices/graphics/@type)")) == NULL) {
|
|
g_set_error(error,
|
|
VIRT_VIEWER_ERROR, VIRT_VIEWER_ERROR_FAILED,
|
|
_("Cannot determine the graphic type for the guest %s"), priv->domkey);
|
|
|
|
goto cleanup;
|
|
}
|
|
|
|
if (!virt_viewer_app_create_session(app, type, error))
|
|
goto cleanup;
|
|
|
|
xpath = g_strdup_printf("string(/domain/devices/graphics[@type='%s']/@port)", type);
|
|
gport = virt_viewer_extract_xpath_string(xmldesc, xpath);
|
|
g_free(xpath);
|
|
if (g_str_equal(type, "spice")) {
|
|
xpath = g_strdup_printf("string(/domain/devices/graphics[@type='%s']/@tlsPort)", type);
|
|
gtlsport = virt_viewer_extract_xpath_string(xmldesc, xpath);
|
|
g_free(xpath);
|
|
}
|
|
|
|
if (gport || gtlsport) {
|
|
xpath = g_strdup_printf("string(/domain/devices/graphics[@type='%s']/listen/@address)", type);
|
|
ghost = virt_viewer_extract_xpath_string(xmldesc, xpath);
|
|
if (ghost == NULL) { /* try old xml format - listen attribute in the graphics node */
|
|
g_free(xpath);
|
|
xpath = g_strdup_printf("string(/domain/devices/graphics[@type='%s']/@listen)", type);
|
|
ghost = virt_viewer_extract_xpath_string(xmldesc, xpath);
|
|
}
|
|
} else {
|
|
xpath = g_strdup_printf("string(/domain/devices/graphics[@type='%s']/listen/@socket)", type);
|
|
unixsock = virt_viewer_extract_xpath_string(xmldesc, xpath);
|
|
if (unixsock == NULL) { /* try old xml format - socket attribute in the graphics node */
|
|
g_free(xpath);
|
|
xpath = g_strdup_printf("string(/domain/devices/graphics[@type='%s']/@socket)", type);
|
|
unixsock = virt_viewer_extract_xpath_string(xmldesc, xpath);
|
|
}
|
|
}
|
|
|
|
if (ghost && gport) {
|
|
g_debug("Guest graphics address is %s:%s", ghost, gport);
|
|
} else if (unixsock) {
|
|
g_debug("Guest graphics address is %s", unixsock);
|
|
} else {
|
|
g_debug("Using direct libvirt connection");
|
|
retval = TRUE;
|
|
goto cleanup;
|
|
}
|
|
|
|
uri = virConnectGetURI(priv->conn);
|
|
if (virt_viewer_util_extract_host(uri, NULL, &host, &transport, &user, &port) < 0) {
|
|
g_set_error(error,
|
|
VIRT_VIEWER_ERROR, VIRT_VIEWER_ERROR_FAILED,
|
|
_("Cannot determine the host for the guest %s"), priv->domkey);
|
|
|
|
goto cleanup;
|
|
}
|
|
|
|
/* If the XML listen attribute shows a wildcard address, we need to
|
|
* throw that away since you obviously can't 'connect(2)' to that
|
|
* from a remote host. Instead we fallback to the hostname used in
|
|
* the libvirt URI. This isn't perfect but it is better than nothing.
|
|
* If the transport is SSH, fallback to localhost as the connection
|
|
* will be made from the remote end of the ssh connection.
|
|
*/
|
|
if (virt_viewer_replace_host(ghost)) {
|
|
gchar *replacement_host = NULL;
|
|
if ((g_strcmp0(transport, "ssh") == 0) && !direct) {
|
|
replacement_host = g_strdup("localhost");
|
|
} else {
|
|
replacement_host = g_strdup(host);
|
|
}
|
|
g_debug("Guest graphics listen '%s' is NULL or a wildcard, replacing with '%s'",
|
|
ghost ? ghost : "", replacement_host);
|
|
g_free(ghost);
|
|
ghost = replacement_host;
|
|
}
|
|
|
|
if (!virt_viewer_is_reachable(ghost, transport, host, direct)) {
|
|
g_set_error(error,
|
|
VIRT_VIEWER_ERROR, VIRT_VIEWER_ERROR_FAILED,
|
|
_("Guest '%s' is not reachable"), priv->domkey);
|
|
|
|
g_debug("graphics listen '%s' is not reachable from this machine",
|
|
ghost ? ghost : "");
|
|
|
|
goto cleanup;
|
|
}
|
|
|
|
virt_viewer_app_set_connect_info(app, host, ghost, gport, gtlsport,transport, unixsock, user, port, NULL);
|
|
|
|
retval = TRUE;
|
|
|
|
cleanup:
|
|
g_free(gport);
|
|
g_free(gtlsport);
|
|
g_free(ghost);
|
|
g_free(unixsock);
|
|
g_free(host);
|
|
g_free(transport);
|
|
g_free(user);
|
|
g_free(type);
|
|
g_free(xpath);
|
|
g_free(xmldesc);
|
|
g_free(uri);
|
|
return retval;
|
|
}
|
|
|
|
static gboolean
|
|
virt_viewer_update_display(VirtViewer *self, virDomainPtr dom, GError **error)
|
|
{
|
|
VirtViewerPrivate *priv = self->priv;
|
|
VirtViewerApp *app = VIRT_VIEWER_APP(self);
|
|
|
|
if (priv->dom)
|
|
virDomainFree(priv->dom);
|
|
priv->dom = dom;
|
|
virDomainRef(priv->dom);
|
|
|
|
virt_viewer_app_trace(app, "Guest %s is running, determining display",
|
|
priv->domkey);
|
|
|
|
if (virt_viewer_app_has_session(app))
|
|
return TRUE;
|
|
|
|
return virt_viewer_extract_connect_info(self, dom, error);
|
|
}
|
|
|
|
static gboolean
|
|
virt_viewer_open_connection(VirtViewerApp *self G_GNUC_UNUSED, int *fd)
|
|
{
|
|
VirtViewer *viewer = VIRT_VIEWER(self);
|
|
VirtViewerPrivate *priv = viewer->priv;
|
|
virErrorPtr err;
|
|
#if defined(HAVE_SOCKETPAIR)
|
|
int pair[2];
|
|
#endif
|
|
*fd = -1;
|
|
|
|
if (!priv->dom)
|
|
return TRUE;
|
|
|
|
if ((*fd = virDomainOpenGraphicsFD(priv->dom, 0,
|
|
VIR_DOMAIN_OPEN_GRAPHICS_SKIPAUTH)) >= 0)
|
|
return TRUE;
|
|
|
|
err = virGetLastError();
|
|
if (err && err->code != VIR_ERR_NO_SUPPORT) {
|
|
g_debug("Error %s", err->message ? err->message : "Unknown");
|
|
return TRUE;
|
|
}
|
|
|
|
#if defined(HAVE_SOCKETPAIR)
|
|
if (socketpair(PF_UNIX, SOCK_STREAM, 0, pair) < 0)
|
|
return FALSE;
|
|
|
|
if (virDomainOpenGraphics(priv->dom, 0, pair[0],
|
|
VIR_DOMAIN_OPEN_GRAPHICS_SKIPAUTH) < 0) {
|
|
err = virGetLastError();
|
|
g_debug("Error %s", err && err->message ? err->message : "Unknown");
|
|
close(pair[0]);
|
|
close(pair[1]);
|
|
return TRUE;
|
|
}
|
|
close(pair[0]);
|
|
*fd = pair[1];
|
|
#endif
|
|
return TRUE;
|
|
}
|
|
|
|
static int
|
|
virt_viewer_domain_event(virConnectPtr conn G_GNUC_UNUSED,
|
|
virDomainPtr dom,
|
|
int event,
|
|
int detail G_GNUC_UNUSED,
|
|
void *opaque)
|
|
{
|
|
VirtViewer *self = opaque;
|
|
VirtViewerApp *app = VIRT_VIEWER_APP(self);
|
|
VirtViewerSession *session;
|
|
GError *error = NULL;
|
|
|
|
g_debug("Got domain event %d %d", event, detail);
|
|
|
|
if (!virt_viewer_matches_domain(self, dom))
|
|
return 0;
|
|
|
|
switch (event) {
|
|
case VIR_DOMAIN_EVENT_STOPPED:
|
|
session = virt_viewer_app_get_session(app);
|
|
#ifdef HAVE_SPICE_GTK
|
|
/* do not disconnect due to migration */
|
|
if (detail == VIR_DOMAIN_EVENT_STOPPED_MIGRATED &&
|
|
VIRT_VIEWER_IS_SESSION_SPICE(session)) {
|
|
break;
|
|
}
|
|
#endif
|
|
if (session != NULL)
|
|
virt_viewer_session_close(session);
|
|
break;
|
|
|
|
case VIR_DOMAIN_EVENT_STARTED:
|
|
virt_viewer_update_display(self, dom, &error);
|
|
if (error) {
|
|
virt_viewer_app_simple_message_dialog(app, error->message);
|
|
g_clear_error(&error);
|
|
}
|
|
|
|
virt_viewer_app_activate(app, &error);
|
|
if (error) {
|
|
/* we may want to consolidate error reporting in
|
|
app_activate() instead */
|
|
g_warning("%s", error->message);
|
|
g_clear_error(&error);
|
|
}
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void
|
|
virt_viewer_conn_event(virConnectPtr conn G_GNUC_UNUSED,
|
|
int reason,
|
|
void *opaque)
|
|
{
|
|
VirtViewer *self = opaque;
|
|
VirtViewerPrivate *priv = self->priv;
|
|
|
|
g_debug("Got connection event %d", reason);
|
|
|
|
virConnectClose(priv->conn);
|
|
priv->conn = NULL;
|
|
|
|
virt_viewer_start_reconnect_poll(self);
|
|
}
|
|
|
|
static void
|
|
virt_viewer_dispose (GObject *object)
|
|
{
|
|
VirtViewer *self = VIRT_VIEWER(object);
|
|
VirtViewerPrivate *priv = self->priv;
|
|
|
|
if (priv->conn) {
|
|
if (priv->domain_event >= 0) {
|
|
virConnectDomainEventDeregisterAny(priv->conn,
|
|
priv->domain_event);
|
|
priv->domain_event = -1;
|
|
}
|
|
virConnectUnregisterCloseCallback(priv->conn,
|
|
virt_viewer_conn_event);
|
|
virConnectClose(priv->conn);
|
|
priv->conn = NULL;
|
|
}
|
|
if (priv->dom) {
|
|
virDomainFree(priv->dom);
|
|
priv->dom = NULL;
|
|
}
|
|
g_free(priv->uri);
|
|
priv->uri = NULL;
|
|
g_free(priv->domkey);
|
|
priv->domkey = NULL;
|
|
G_OBJECT_CLASS(virt_viewer_parent_class)->dispose (object);
|
|
}
|
|
|
|
static virDomainPtr
|
|
choose_vm(GtkWindow *main_window,
|
|
char **vm_name,
|
|
virConnectPtr conn,
|
|
GError **error)
|
|
{
|
|
GtkListStore *model;
|
|
GtkTreeIter iter;
|
|
virDomainPtr *domains, dom = NULL;
|
|
int i, vms_running;
|
|
unsigned int flags = VIR_CONNECT_LIST_DOMAINS_RUNNING;
|
|
|
|
g_return_val_if_fail(vm_name != NULL, NULL);
|
|
free(*vm_name);
|
|
|
|
/* UI name , key , tooltip */
|
|
model = gtk_list_store_new(3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
|
|
|
|
vms_running = virConnectListAllDomains(conn, &domains, flags);
|
|
for (i = 0; i < vms_running; i++) {
|
|
const char *name = virDomainGetName(domains[i]);
|
|
char *title = virDomainGetMetadata(domains[i], VIR_DOMAIN_METADATA_TITLE, NULL, 0);
|
|
char *description = virDomainGetMetadata(domains[i], VIR_DOMAIN_METADATA_DESCRIPTION, NULL, 0);
|
|
gtk_list_store_append(model, &iter);
|
|
gtk_list_store_set(model, &iter, 0, title ? title : name, 1, name, -1);
|
|
if (description)
|
|
gtk_list_store_set(model, &iter, 2, description, -1);
|
|
virDomainFree(domains[i]);
|
|
free(title);
|
|
free(description);
|
|
}
|
|
free(domains);
|
|
|
|
*vm_name = virt_viewer_vm_connection_choose_name_dialog(main_window,
|
|
GTK_TREE_MODEL(model),
|
|
error);
|
|
g_object_unref(G_OBJECT(model));
|
|
if (*vm_name == NULL)
|
|
return NULL;
|
|
|
|
dom = virDomainLookupByName(conn, *vm_name);
|
|
if (dom == NULL) {
|
|
virErrorPtr err = virGetLastError();
|
|
g_set_error_literal(error,
|
|
VIRT_VIEWER_ERROR, VIRT_VIEWER_ERROR_FAILED,
|
|
err && err->message ? err->message : "unknown libvirt error");
|
|
} else if (virDomainGetState(dom, &i, NULL, 0) < 0 || i != VIR_DOMAIN_RUNNING) {
|
|
g_set_error(error,
|
|
VIRT_VIEWER_ERROR, VIRT_VIEWER_ERROR_FAILED,
|
|
_("Virtual machine %s is not running"), *vm_name);
|
|
virDomainFree(dom);
|
|
dom = NULL;
|
|
}
|
|
|
|
return dom;
|
|
}
|
|
|
|
static gboolean
|
|
virt_viewer_initial_connect(VirtViewerApp *app, GError **error)
|
|
{
|
|
virDomainPtr dom = NULL;
|
|
virDomainInfo info;
|
|
gboolean ret = FALSE;
|
|
VirtViewer *self = VIRT_VIEWER(app);
|
|
VirtViewerPrivate *priv = self->priv;
|
|
char uuid_string[VIR_UUID_STRING_BUFLEN];
|
|
const char *guest_name;
|
|
char *title;
|
|
GError *err = NULL;
|
|
|
|
g_debug("initial connect");
|
|
|
|
if (!priv->conn &&
|
|
virt_viewer_connect(app, &err) < 0) {
|
|
virt_viewer_app_show_status(app, _("Waiting for libvirt to start"));
|
|
goto wait;
|
|
}
|
|
|
|
virt_viewer_app_show_status(app, _("Finding guest domain"));
|
|
dom = virt_viewer_lookup_domain(self);
|
|
if (!dom) {
|
|
if (priv->waitvm) {
|
|
virt_viewer_app_show_status(app, _("Waiting for guest domain to be created"));
|
|
goto wait;
|
|
} else {
|
|
VirtViewerWindow *main_window = virt_viewer_app_get_main_window(app);
|
|
if (priv->domkey != NULL)
|
|
g_debug("Cannot find guest %s", priv->domkey);
|
|
dom = choose_vm(virt_viewer_window_get_window(main_window),
|
|
&priv->domkey,
|
|
priv->conn,
|
|
&err);
|
|
if (dom == NULL) {
|
|
goto cleanup;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (virDomainGetUUIDString(dom, uuid_string) < 0) {
|
|
g_debug("Couldn't get uuid from libvirt");
|
|
} else {
|
|
g_object_set(app, "uuid", uuid_string, NULL);
|
|
}
|
|
guest_name = virDomainGetName(dom);
|
|
if (guest_name != NULL) {
|
|
g_object_set(app, "guest-name", guest_name, NULL);
|
|
}
|
|
|
|
title = virDomainGetMetadata(dom, VIR_DOMAIN_METADATA_TITLE, NULL, 0);
|
|
if (title != NULL) {
|
|
g_object_set(app, "title", title, NULL);
|
|
free(title);
|
|
}
|
|
|
|
virt_viewer_app_show_status(app, _("Checking guest domain status"));
|
|
if (virDomainGetInfo(dom, &info) < 0) {
|
|
g_set_error_literal(&err, VIRT_VIEWER_ERROR, VIRT_VIEWER_ERROR_FAILED,
|
|
_("Cannot get guest state"));
|
|
g_debug("%s", err->message);
|
|
goto cleanup;
|
|
}
|
|
|
|
if (info.state == VIR_DOMAIN_SHUTOFF) {
|
|
virt_viewer_app_show_status(app, _("Waiting for guest domain to start"));
|
|
goto wait;
|
|
}
|
|
|
|
if (!virt_viewer_update_display(self, dom, &err))
|
|
goto cleanup;
|
|
|
|
ret = VIRT_VIEWER_APP_CLASS(virt_viewer_parent_class)->initial_connect(app, &err);
|
|
if (ret || err)
|
|
goto cleanup;
|
|
|
|
wait:
|
|
virt_viewer_app_trace(app, "Guest %s has not activated its display yet, waiting "
|
|
"for it to start", priv->domkey);
|
|
ret = TRUE;
|
|
|
|
cleanup:
|
|
if (err != NULL)
|
|
g_propagate_error(error, err);
|
|
if (dom)
|
|
virDomainFree(dom);
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
virt_viewer_error_func (void *data G_GNUC_UNUSED,
|
|
virErrorPtr error G_GNUC_UNUSED)
|
|
{
|
|
/* nothing */
|
|
}
|
|
|
|
|
|
|
|
static int
|
|
virt_viewer_auth_libvirt_credentials(virConnectCredentialPtr cred,
|
|
unsigned int ncred,
|
|
void *cbdata)
|
|
{
|
|
char **username = NULL, **password = NULL;
|
|
VirtViewer *app = cbdata;
|
|
VirtViewerPrivate *priv = app->priv;
|
|
int i;
|
|
int ret = 0;
|
|
|
|
g_debug("Got libvirt credential request for %u credential(s)", ncred);
|
|
|
|
for (i = 0 ; i < ncred ; i++) {
|
|
switch (cred[i].type) {
|
|
case VIR_CRED_USERNAME:
|
|
case VIR_CRED_AUTHNAME:
|
|
username = &cred[i].result;
|
|
break;
|
|
case VIR_CRED_PASSPHRASE:
|
|
password = &cred[i].result;
|
|
break;
|
|
default:
|
|
g_debug("Unsupported libvirt credential %d", cred[i].type);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
if (username || password) {
|
|
VirtViewerWindow *vwin = virt_viewer_app_get_main_window(VIRT_VIEWER_APP(app));
|
|
GtkWindow *win = virt_viewer_window_get_window(vwin);
|
|
|
|
if (username && (*username == NULL || **username == '\0'))
|
|
*username = g_strdup(g_get_user_name());
|
|
|
|
priv->auth_cancelled = !virt_viewer_auth_collect_credentials(win,
|
|
"libvirt",
|
|
app->priv->uri,
|
|
username, password);
|
|
if (priv->auth_cancelled) {
|
|
ret = -1;
|
|
goto cleanup;
|
|
}
|
|
}
|
|
|
|
static const char * const cred_type_to_str[] = {
|
|
[VIR_CRED_USERNAME] = "Identity to act as",
|
|
[VIR_CRED_AUTHNAME] = "Identify to authorize as",
|
|
[VIR_CRED_PASSPHRASE] = "Passphrase secret",
|
|
};
|
|
|
|
for (i = 0 ; i < ncred ; i++) {
|
|
switch (cred[i].type) {
|
|
case VIR_CRED_AUTHNAME:
|
|
case VIR_CRED_USERNAME:
|
|
case VIR_CRED_PASSPHRASE:
|
|
if (cred[i].result)
|
|
cred[i].resultlen = strlen(cred[i].result);
|
|
else
|
|
cred[i].resultlen = 0;
|
|
g_debug("Got %s '%s' %d",
|
|
cred_type_to_str[cred[i].type],
|
|
/* hide password */
|
|
(cred[i].type == VIR_CRED_PASSPHRASE) ? "*****" : cred[i].result,
|
|
cred[i].type);
|
|
break;
|
|
}
|
|
}
|
|
|
|
cleanup:
|
|
g_debug("Return %d", ret);
|
|
return ret;
|
|
}
|
|
|
|
static gchar *
|
|
virt_viewer_get_error_message_from_vir_error(VirtViewer *self,
|
|
virErrorPtr error)
|
|
{
|
|
VirtViewerPrivate *priv = self->priv;
|
|
const gchar *error_details = NULL;
|
|
gchar *detailed_error_message = NULL;
|
|
gchar *error_message = g_strdup_printf(_("Unable to connect to libvirt with URI: %s."),
|
|
priv->uri ? priv->uri : _("[none]"));
|
|
|
|
g_debug("Error: %s", error->message);
|
|
|
|
/* For now we are only treating authentication errors. */
|
|
switch (error->code) {
|
|
case VIR_ERR_AUTH_FAILED:
|
|
error_details = _("Authentication failed.");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (error_details != NULL) {
|
|
detailed_error_message = g_strdup_printf("%s\n%s", error_message, error_details);
|
|
g_free(error_message);
|
|
return detailed_error_message;
|
|
}
|
|
|
|
return error_message;
|
|
}
|
|
|
|
static int
|
|
virt_viewer_connect(VirtViewerApp *app, GError **err)
|
|
{
|
|
VirtViewer *self = VIRT_VIEWER(app);
|
|
VirtViewerPrivate *priv = self->priv;
|
|
int cred_types[] =
|
|
{ VIR_CRED_AUTHNAME, VIR_CRED_PASSPHRASE };
|
|
virConnectAuth auth_libvirt = {
|
|
.credtype = cred_types,
|
|
.ncredtype = G_N_ELEMENTS(cred_types),
|
|
.cb = virt_viewer_auth_libvirt_credentials,
|
|
.cbdata = app,
|
|
};
|
|
int oflags = 0;
|
|
GError *error = NULL;
|
|
|
|
if (!virt_viewer_app_get_attach(app))
|
|
oflags |= VIR_CONNECT_RO;
|
|
|
|
g_debug("connecting ...");
|
|
|
|
virt_viewer_app_trace(app, "Opening connection to libvirt with URI %s",
|
|
priv->uri ? priv->uri : "<null>");
|
|
priv->conn = virConnectOpenAuth(priv->uri,
|
|
//virConnectAuthPtrDefault,
|
|
&auth_libvirt,
|
|
oflags);
|
|
if (!priv->conn) {
|
|
if (!priv->auth_cancelled) {
|
|
gchar *error_message = virt_viewer_get_error_message_from_vir_error(self, virGetLastError());
|
|
g_set_error_literal(&error,
|
|
VIRT_VIEWER_ERROR, VIRT_VIEWER_ERROR_FAILED,
|
|
error_message);
|
|
|
|
g_free(error_message);
|
|
} else {
|
|
g_set_error_literal(&error,
|
|
VIRT_VIEWER_ERROR, VIRT_VIEWER_ERROR_CANCELLED,
|
|
_("Authentication was cancelled"));
|
|
}
|
|
g_propagate_error(err, error);
|
|
return -1;
|
|
}
|
|
|
|
if (!virt_viewer_app_initial_connect(app, &error)) {
|
|
g_propagate_prefixed_error(err, error, _("Failed to connect: "));
|
|
return -1;
|
|
}
|
|
|
|
priv->domain_event = virConnectDomainEventRegisterAny(priv->conn,
|
|
priv->dom,
|
|
VIR_DOMAIN_EVENT_ID_LIFECYCLE,
|
|
VIR_DOMAIN_EVENT_CALLBACK(virt_viewer_domain_event),
|
|
self,
|
|
NULL);
|
|
if (priv->domain_event < 0 &&
|
|
!virt_viewer_app_is_active(app)) {
|
|
g_debug("No domain events, falling back to polling");
|
|
virt_viewer_start_reconnect_poll(self);
|
|
} else {
|
|
/* we may be polling if we lost the libvirt connection and are trying
|
|
* to reconnect */
|
|
virt_viewer_stop_reconnect_poll(self);
|
|
}
|
|
|
|
if (virConnectRegisterCloseCallback(priv->conn,
|
|
virt_viewer_conn_event,
|
|
self,
|
|
NULL) < 0) {
|
|
g_debug("Unable to register close callback on libvirt connection");
|
|
}
|
|
|
|
if (virConnectSetKeepAlive(priv->conn, 5, 3) < 0) {
|
|
g_debug("Unable to set keep alive");
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static gboolean
|
|
virt_viewer_start(VirtViewerApp *app, GError **error)
|
|
{
|
|
gvir_event_register();
|
|
|
|
virSetErrorFunc(NULL, virt_viewer_error_func);
|
|
|
|
if (virt_viewer_connect(app, error) < 0)
|
|
return FALSE;
|
|
|
|
return VIRT_VIEWER_APP_CLASS(virt_viewer_parent_class)->start(app, error);
|
|
}
|
|
|
|
VirtViewer *
|
|
virt_viewer_new(void)
|
|
{
|
|
return g_object_new(VIRT_VIEWER_TYPE,
|
|
"application-id", "org.virt-manager.virt-viewer",
|
|
"flags", G_APPLICATION_NON_UNIQUE,
|
|
NULL);
|
|
}
|
|
|
|
/*
|
|
* Local variables:
|
|
* c-indent-level: 4
|
|
* c-basic-offset: 4
|
|
* indent-tabs-mode: nil
|
|
* End:
|
|
*/
|