diff options
author | Victor Toso <me@victortoso.com> | 2017-01-10 16:36:48 +0100 |
---|---|---|
committer | Victor Toso <me@victortoso.com> | 2017-01-23 09:30:47 +0100 |
commit | 47e37e7c9db5f70a0ade527fe6045ba742cec8fa (patch) | |
tree | 4e8903dbfab002ad892385cf92b777a13a4d4f89 /tools | |
parent | 0517c9d6c4da58d5e8b76cef7fada58141859443 (diff) |
Move spicy tools to its own folder
So we can have the tools and the libraries in different folders.
In the src/Makefile.am I've only removed the lines related to the
tools but not all lines were copied into tools/Makefile.am as we
don't really need them. Other lines were adjusted to have the paths
correctly;
Signed-off-by: Victor Toso <victortoso@redhat.com>
Acked-by: Christophe Fergeau <cfergeau@redhat.com>
Diffstat (limited to 'tools')
-rw-r--r-- | tools/Makefile.am | 69 | ||||
-rw-r--r-- | tools/spice-cmdline.c | 98 | ||||
-rw-r--r-- | tools/spice-cmdline.h | 29 | ||||
-rw-r--r-- | tools/spicy-connect.c | 248 | ||||
-rw-r--r-- | tools/spicy-connect.h | 26 | ||||
-rw-r--r-- | tools/spicy-screenshot.c | 194 | ||||
-rw-r--r-- | tools/spicy-stats.c | 136 | ||||
-rw-r--r-- | tools/spicy.c | 1938 |
8 files changed, 2738 insertions, 0 deletions
diff --git a/tools/Makefile.am b/tools/Makefile.am new file mode 100644 index 0000000..0bdb3c5 --- /dev/null +++ b/tools/Makefile.am @@ -0,0 +1,69 @@ +bin_PROGRAMS = spicy-stats spicy-screenshot + +TOOLS_CPPFLAGS = \ + -DSPICE_COMPILATION \ + -I$(top_builddir)/src \ + -I$(top_srcdir) \ + -I$(top_srcdir)/src \ + $(COMMON_CFLAGS) \ + $(GLIB2_CFLAGS) \ + $(GIO_CFLAGS) \ + $(SMARTCARD_CFLAGS) \ + $(SPICE_CFLAGS) \ + $(NULL) + +if WITH_GTK +bin_PROGRAMS += spicy +TOOLS_CPPFLAGS += $(GTK_CFLAGS) +endif + +spicy_SOURCES = \ + spicy.c \ + spicy-connect.h \ + spicy-connect.c \ + spice-cmdline.h \ + spice-cmdline.c \ + $(NULL) + +spicy_LDADD = \ + $(top_builddir)/src/libspice-client-gtk-3.0.la \ + $(top_builddir)/src/libspice-client-glib-2.0.la \ + $(NULL) + +# FIXME: GtkAction and lots of GtkUIManager APIs are deprecated +spicy_CPPFLAGS = \ + $(TOOLS_CPPFLAGS) \ + -DSPICE_DISABLE_DEPRECATED \ + -Wno-deprecated-declarations \ + $(NULL) + +spicy_screenshot_SOURCES = \ + spicy-screenshot.c \ + spice-cmdline.h \ + spice-cmdline.c \ + $(NULL) + +spicy_screenshot_LDADD = \ + $(top_builddir)/src/libspice-client-glib-2.0.la \ + $(GOBJECT2_LIBS) \ + $(NULL) + +spicy_screenshot_CPPFLAGS = \ + $(TOOLS_CPPFLAGS) \ + $(NULL) + +spicy_stats_SOURCES = \ + spicy-stats.c \ + spice-cmdline.h \ + spice-cmdline.c \ + $(NULL) + +spicy_stats_LDADD = \ + $(top_builddir)/src/libspice-client-glib-2.0.la \ + $(NULL) + +spicy_stats_CPPFLAGS = \ + $(TOOLS_CPPFLAGS) \ + $(NULL) + +-include $(top_srcdir)/git.mk diff --git a/tools/spice-cmdline.c b/tools/spice-cmdline.c new file mode 100644 index 0000000..4b6f4c2 --- /dev/null +++ b/tools/spice-cmdline.c @@ -0,0 +1,98 @@ +/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + Copyright (C) 2010 Red Hat, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see <http://www.gnu.org/licenses/>. +*/ +#include "config.h" +#include <glib/gi18n-lib.h> + +#include "spice-client.h" +#include "spice-common.h" +#include "spice-cmdline.h" + +static char *host; +static char *port; +static char *tls_port; +static char *password; +static char *uri; + +static GOptionEntry spice_entries[] = { + { + .long_name = "uri", + .arg = G_OPTION_ARG_STRING, + .arg_data = &uri, + .description = N_("Spice server uri"), + .arg_description = N_("<uri>"), + },{ + .long_name = "host", + .short_name = 'h', + .arg = G_OPTION_ARG_STRING, + .arg_data = &host, + .description = N_("Spice server address"), + .arg_description = N_("<host>"), + },{ + .long_name = "port", + .short_name = 'p', + .arg = G_OPTION_ARG_STRING, + .arg_data = &port, + .description = N_("Spice server port"), + .arg_description = N_("<port>"), + },{ + .long_name = "secure-port", + .short_name = 's', + .arg = G_OPTION_ARG_STRING, + .arg_data = &tls_port, + .description = N_("Spice server secure port"), + .arg_description = N_("<port>"), + },{ + .long_name = "password", + .short_name = 'w', + .arg = G_OPTION_ARG_STRING, + .arg_data = &password, + .description = N_("Server password"), + .arg_description = N_("<password>"), + },{ + /* end of list */ + } +}; + +GOptionGroup *spice_cmdline_get_option_group(void) +{ + GOptionGroup *grp; + + grp = g_option_group_new("spice", + _("Spice connection options:"), + _("Show Spice options"), + NULL, NULL); + g_option_group_add_entries(grp, spice_entries); + + return grp; +} + +void spice_cmdline_session_setup(SpiceSession *session) +{ + g_return_if_fail(SPICE_IS_SESSION(session)); + + if (uri) + g_object_set(session, "uri", uri, NULL); + if (host) + g_object_set(session, "host", host, NULL); + if (port) + g_object_set(session, "port", port, NULL); + if (tls_port) + g_object_set(session, "tls-port", tls_port, NULL); + if (password) + g_object_set(session, "password", password, NULL); +} diff --git a/tools/spice-cmdline.h b/tools/spice-cmdline.h new file mode 100644 index 0000000..11a8086 --- /dev/null +++ b/tools/spice-cmdline.h @@ -0,0 +1,29 @@ +/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + Copyright (C) 2010 Red Hat, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef SPICE_CMDLINE_H_ +# define SPICE_CMDLINE_H_ + +G_BEGIN_DECLS + +GOptionGroup *spice_cmdline_get_option_group(void); +void spice_cmdline_session_setup(SpiceSession *session); + +G_END_DECLS + +#endif // SPICE_CMDLINE_H_ diff --git a/tools/spicy-connect.c b/tools/spicy-connect.c new file mode 100644 index 0000000..39555a6 --- /dev/null +++ b/tools/spicy-connect.c @@ -0,0 +1,248 @@ +/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + Copyright (C) 2010-2015 Red Hat, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see <http://www.gnu.org/licenses/>. +*/ + +#include <gtk/gtk.h> +#include <gdk/gdkkeysyms.h> +#include "spice-common.h" +#include "spicy-connect.h" + +typedef struct +{ + gboolean connecting; + GMainLoop *loop; + SpiceSession *session; +} ConnectionInfo; + +static struct { + const char *text; + const char *prop; + GtkWidget *entry; +} connect_entries[] = { + { .text = "Hostname", .prop = "host" }, + { .text = "Port", .prop = "port" }, + { .text = "TLS Port", .prop = "tls-port" }, +}; + +static gboolean can_connect(void) +{ + if ((gtk_entry_get_text_length(GTK_ENTRY(connect_entries[0].entry)) > 0) && + ((gtk_entry_get_text_length(GTK_ENTRY(connect_entries[1].entry)) > 0) || + (gtk_entry_get_text_length(GTK_ENTRY(connect_entries[2].entry)) > 0))) + return TRUE; + + return FALSE; +} + +static void set_connection_info(SpiceSession *session) +{ + const gchar *txt; + int i; + + for (i = 0; i < SPICE_N_ELEMENTS(connect_entries); i++) { + txt = gtk_entry_get_text(GTK_ENTRY(connect_entries[i].entry)); + g_object_set(session, connect_entries[i].prop, txt, NULL); + } +} + +static gboolean close_cb(gpointer data) +{ + ConnectionInfo *info = data; + info->connecting = FALSE; + if (g_main_loop_is_running(info->loop)) + g_main_loop_quit(info->loop); + + return TRUE; +} + +static void entry_changed_cb(GtkEditable* entry, gpointer data) +{ + GtkButton *connect_button = data; + gtk_widget_set_sensitive(GTK_WIDGET(connect_button), can_connect()); +} + +static gboolean entry_focus_in_cb(GtkWidget *widget, GdkEvent *event, gpointer data) +{ + GtkRecentChooser *recent = GTK_RECENT_CHOOSER(data); + gtk_recent_chooser_unselect_all(recent); + return TRUE; +} + +static gboolean key_pressed_cb(GtkWidget *widget, GdkEvent *event, gpointer data) +{ + gboolean tst; + if (event->type == GDK_KEY_PRESS) { + switch (event->key.keyval) { + case GDK_KEY_Escape: + g_signal_emit_by_name(GTK_WIDGET(data), "delete-event", NULL, &tst); + return TRUE; + default: + return FALSE; + } + } + + return FALSE; +} + +static void recent_selection_changed_dialog_cb(GtkRecentChooser *chooser, gpointer data) +{ + GtkRecentInfo *info; + gchar *txt = NULL; + const gchar *uri; + SpiceSession *session = data; + int i; + + info = gtk_recent_chooser_get_current_item(chooser); + if (info == NULL) + return; + + uri = gtk_recent_info_get_uri(info); + g_return_if_fail(uri != NULL); + + g_object_set(session, "uri", uri, NULL); + + for (i = 0; i < SPICE_N_ELEMENTS(connect_entries); i++) { + g_object_get(session, connect_entries[i].prop, &txt, NULL); + gtk_entry_set_text(GTK_ENTRY(connect_entries[i].entry), txt ? txt : ""); + g_free(txt); + } + + gtk_recent_info_unref(info); +} + +static void connect_cb(gpointer data) +{ + ConnectionInfo *info = data; + if (can_connect()) + { + info->connecting = TRUE; + set_connection_info(info->session); + if (g_main_loop_is_running(info->loop)) + g_main_loop_quit(info->loop); + } +} + +gboolean spicy_connect_dialog(SpiceSession *session) +{ + GtkWidget *connect_button, *cancel_button, *label; + GtkBox *main_box, *recent_box, *button_box; + GtkWindow *window; + GtkGrid *grid; + int i; + + ConnectionInfo info = { + FALSE, + NULL, + session + }; + + /* Create the widgets */ + window = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL)); + gtk_window_set_title(window, "Connect to SPICE"); + gtk_window_set_resizable(window, FALSE); + gtk_container_set_border_width(GTK_CONTAINER(window), 5); + + main_box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(main_box)); + + grid = GTK_GRID(gtk_grid_new()); + gtk_box_pack_start(main_box, GTK_WIDGET(grid), FALSE, TRUE, 0); + gtk_container_set_border_width(GTK_CONTAINER(grid), 5); + gtk_grid_set_row_spacing(grid, 5); + gtk_grid_set_column_spacing(grid, 5); + + for (i = 0; i < SPICE_N_ELEMENTS(connect_entries); i++) { + gchar *txt; + label = gtk_label_new(connect_entries[i].text); + gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5); + gtk_grid_attach(grid, label, 0, i, 1, 1); + connect_entries[i].entry = GTK_WIDGET(gtk_entry_new()); + gtk_grid_attach(grid, connect_entries[i].entry, 1, i, 1, 1); + g_object_get(session, connect_entries[i].prop, &txt, NULL); + SPICE_DEBUG("%s: #%i [%s]: \"%s\"", + __FUNCTION__, i, connect_entries[i].prop, txt); + if (txt) { + gtk_entry_set_text(GTK_ENTRY(connect_entries[i].entry), txt); + g_free(txt); + } + } + + recent_box = GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL, 0)); + gtk_box_pack_start(main_box, GTK_WIDGET(recent_box), TRUE, TRUE, 0); + gtk_container_set_border_width(GTK_CONTAINER(recent_box), 5); + + label = gtk_label_new("Recent connections:"); + gtk_box_pack_start(recent_box, label, FALSE, TRUE, 0); + gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5); + + button_box = GTK_BOX(gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL)); + gtk_button_box_set_layout(GTK_BUTTON_BOX(button_box), GTK_BUTTONBOX_END); + gtk_box_set_spacing(button_box, 5); + gtk_container_set_border_width(GTK_CONTAINER(button_box), 5); + connect_button = gtk_button_new_with_label("Connect"); + cancel_button = gtk_button_new_with_label("Cancel"); + gtk_box_pack_start(button_box, cancel_button, FALSE, TRUE, 0); + gtk_box_pack_start(button_box, connect_button, FALSE, TRUE, 1); + + gtk_box_pack_start(main_box, GTK_WIDGET(button_box), FALSE, TRUE, 0); + + gtk_widget_set_sensitive(GTK_WIDGET(connect_button), can_connect()); + + g_signal_connect(window, "key-press-event", + G_CALLBACK(key_pressed_cb), window); + g_signal_connect_swapped(window, "delete-event", + G_CALLBACK(close_cb), &info); + g_signal_connect_swapped(connect_button, "clicked", + G_CALLBACK(connect_cb), &info); + g_signal_connect_swapped(cancel_button, "clicked", + G_CALLBACK(close_cb), &info); + + GtkRecentFilter *rfilter; + GtkWidget *recent; + + recent = GTK_WIDGET(gtk_recent_chooser_widget_new()); + gtk_recent_chooser_set_show_icons(GTK_RECENT_CHOOSER(recent), FALSE); + gtk_box_pack_start(recent_box, recent, TRUE, TRUE, 0); + + rfilter = gtk_recent_filter_new(); + gtk_recent_filter_add_mime_type(rfilter, "application/x-spice"); + gtk_recent_chooser_set_filter(GTK_RECENT_CHOOSER(recent), rfilter); + gtk_recent_chooser_set_local_only(GTK_RECENT_CHOOSER(recent), FALSE); + g_signal_connect(recent, "selection-changed", + G_CALLBACK(recent_selection_changed_dialog_cb), session); + g_signal_connect_swapped(recent, "item-activated", + G_CALLBACK(connect_cb), &info); + + for (i = 0; i < SPICE_N_ELEMENTS(connect_entries); i++) { + g_signal_connect_swapped(connect_entries[i].entry, "activate", + G_CALLBACK(connect_cb), &info); + g_signal_connect(connect_entries[i].entry, "changed", + G_CALLBACK(entry_changed_cb), connect_button); + g_signal_connect(connect_entries[i].entry, "focus-in-event", + G_CALLBACK(entry_focus_in_cb), recent); + } + + /* show and wait for response */ + gtk_widget_show_all(GTK_WIDGET(window)); + + info.loop = g_main_loop_new(NULL, FALSE); + g_main_loop_run(info.loop); + + gtk_widget_destroy(GTK_WIDGET(window)); + + return info.connecting; +} diff --git a/tools/spicy-connect.h b/tools/spicy-connect.h new file mode 100644 index 0000000..56b2d80 --- /dev/null +++ b/tools/spicy-connect.h @@ -0,0 +1,26 @@ +/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + Copyright (C) 2010-2015 Red Hat, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef SPICY_CONNECT_H +#define SPICY_CONNECT_H + +#include "spice-widget.h" + +gboolean spicy_connect_dialog(SpiceSession *session); + +#endif diff --git a/tools/spicy-screenshot.c b/tools/spicy-screenshot.c new file mode 100644 index 0000000..68f9335 --- /dev/null +++ b/tools/spicy-screenshot.c @@ -0,0 +1,194 @@ +/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + Copyright (C) 2010 Red Hat, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see <http://www.gnu.org/licenses/>. +*/ +#include "config.h" + +#include "spice-client.h" +#include "spice-common.h" +#include "spice-cmdline.h" + +/* config */ +static const char *outf = "spicy-screenshot.ppm"; +static gboolean version = FALSE; + +/* state */ +static SpiceSession *session; +static GMainLoop *mainloop; + +enum SpiceSurfaceFmt d_format; +gint d_width, d_height, d_stride; +gpointer d_data; + +/* ------------------------------------------------------------------ */ + +static void primary_create(SpiceChannel *channel, gint format, + gint width, gint height, gint stride, + gint shmid, gpointer imgdata, gpointer data) +{ + SPICE_DEBUG("%s: %dx%d, format %d", __FUNCTION__, width, height, format); + d_format = format; + d_width = width; + d_height = height; + d_stride = stride; + d_data = imgdata; +} + +static int write_ppm_32(void) +{ + FILE *fp; + uint8_t *p; + int n; + + fp = fopen(outf,"w"); + if (NULL == fp) { + fprintf(stderr, "%s: can't open %s: %s\n", g_get_prgname(), outf, strerror(errno)); + return -1; + } + fprintf(fp, "P6\n%d %d\n255\n", + d_width, d_height); + n = d_width * d_height; + p = d_data; + while (n > 0) { +#ifdef WORDS_BIGENDIAN + fputc(p[1], fp); + fputc(p[2], fp); + fputc(p[3], fp); +#else + fputc(p[2], fp); + fputc(p[1], fp); + fputc(p[0], fp); +#endif + p += 4; + n--; + } + fclose(fp); + return 0; +} + +static void invalidate(SpiceChannel *channel, + gint x, gint y, gint w, gint h, gpointer *data) +{ + int rc; + + switch (d_format) { + case SPICE_SURFACE_FMT_32_xRGB: + rc = write_ppm_32(); + break; + default: + fprintf(stderr, "unsupported spice surface format %u\n", d_format); + rc = -1; + break; + } + if (rc == 0) + fprintf(stderr, "wrote screen shot to %s\n", outf); + g_main_loop_quit(mainloop); +} + +static void main_channel_event(SpiceChannel *channel, SpiceChannelEvent event, + gpointer data) +{ + switch (event) { + case SPICE_CHANNEL_OPENED: + break; + default: + g_warning("main channel event: %u", event); + g_main_loop_quit(mainloop); + } +} + +static void channel_new(SpiceSession *s, SpiceChannel *channel, gpointer *data) +{ + int id; + + if (SPICE_IS_MAIN_CHANNEL(channel)) { + g_signal_connect(channel, "channel-event", + G_CALLBACK(main_channel_event), data); + return; + } + + if (!SPICE_IS_DISPLAY_CHANNEL(channel)) + return; + + g_object_get(channel, "channel-id", &id, NULL); + if (id != 0) + return; + + g_signal_connect(channel, "display-primary-create", + G_CALLBACK(primary_create), NULL); + g_signal_connect(channel, "display-invalidate", + G_CALLBACK(invalidate), NULL); + spice_channel_connect(channel); +} + +/* ------------------------------------------------------------------ */ + +static GOptionEntry app_entries[] = { + { + .long_name = "out-file", + .short_name = 'o', + .arg = G_OPTION_ARG_FILENAME, + .arg_data = &outf, + .description = "Output file name (default spicy-screenshot.ppm)", + .arg_description = "<filename>", + }, + { + .long_name = "version", + .arg = G_OPTION_ARG_NONE, + .arg_data = &version, + .description = "Display version and quit", + }, + { + /* end of list */ + } +}; + +int main(int argc, char *argv[]) +{ + GError *error = NULL; + GOptionContext *context; + + /* parse opts */ + context = g_option_context_new(" - make screen shots"); + g_option_context_set_summary(context, "A Spice server client to take screenshots in ppm format."); + g_option_context_set_description(context, "Report bugs to " PACKAGE_BUGREPORT "."); + g_option_context_set_main_group(context, spice_cmdline_get_option_group()); + g_option_context_add_main_entries(context, app_entries, NULL); + if (!g_option_context_parse (context, &argc, &argv, &error)) { + g_print("option parsing failed: %s\n", error->message); + exit(1); + } + + if (version) { + g_print("%s " PACKAGE_VERSION "\n", g_get_prgname()); + exit(0); + } + + mainloop = g_main_loop_new(NULL, false); + + session = spice_session_new(); + g_signal_connect(session, "channel-new", + G_CALLBACK(channel_new), NULL); + spice_cmdline_session_setup(session); + + if (!spice_session_connect(session)) { + fprintf(stderr, "spice_session_connect failed\n"); + exit(1); + } + + g_main_loop_run(mainloop); + return 0; +} diff --git a/tools/spicy-stats.c b/tools/spicy-stats.c new file mode 100644 index 0000000..8ca4cc1 --- /dev/null +++ b/tools/spicy-stats.c @@ -0,0 +1,136 @@ +/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + Copyright (C) 2010 Red Hat, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see <http://www.gnu.org/licenses/>. +*/ +#include "config.h" + +#include "spice-client.h" +#include "spice-common.h" +#include "spice-cmdline.h" + +/* config */ +static gboolean version = FALSE; + +/* state */ +static SpiceSession *session; +static GMainLoop *mainloop; + +/* ------------------------------------------------------------------ */ +static void main_channel_event(SpiceChannel *channel, SpiceChannelEvent event, + gpointer data) +{ + switch (event) { + case SPICE_CHANNEL_OPENED: + break; + default: + g_warning("main channel event: %u", event); + g_main_loop_quit(mainloop); + } +} + +static void channel_new(SpiceSession *s, SpiceChannel *channel, gpointer *data) +{ + int id; + + if (SPICE_IS_MAIN_CHANNEL(channel)) { + SPICE_DEBUG("new main channel"); + g_signal_connect(channel, "channel-event", + G_CALLBACK(main_channel_event), data); + } + + if (SPICE_IS_DISPLAY_CHANNEL(channel)) { + g_object_get(channel, "channel-id", &id, NULL); + if (id != 0) + return; + } + + spice_channel_connect(channel); +} + +/* ------------------------------------------------------------------ */ + +static GOptionEntry app_entries[] = { + { + .long_name = "version", + .arg = G_OPTION_ARG_NONE, + .arg_data = &version, + .description = "Display version and quit", + }, + { + /* end of list */ + } +}; + +static void +signal_handler(int signum) +{ + g_main_loop_quit(mainloop); +} + +int main(int argc, char *argv[]) +{ + GError *error = NULL; + GOptionContext *context; + + signal(SIGINT, signal_handler); + + /* parse opts */ + context = g_option_context_new(NULL); + g_option_context_set_summary(context, "A Spice client used for testing and measurements."); + g_option_context_set_description(context, "Report bugs to " PACKAGE_BUGREPORT "."); + g_option_context_set_main_group(context, spice_cmdline_get_option_group()); + g_option_context_add_main_entries(context, app_entries, NULL); + if (!g_option_context_parse (context, &argc, &argv, &error)) { + g_print("option parsing failed: %s\n", error->message); + exit(1); + } + + if (version) { + g_print("spicy-stats " PACKAGE_VERSION "\n"); + exit(0); + } + + mainloop = g_main_loop_new(NULL, false); + + session = spice_session_new(); + g_signal_connect(session, "channel-new", + G_CALLBACK(channel_new), NULL); + spice_cmdline_session_setup(session); + + if (!spice_session_connect(session)) { + fprintf(stderr, "spice_session_connect failed\n"); + exit(1); + } + + g_main_loop_run(mainloop); + { + GList *iter, *list = spice_session_get_channels(session); + gulong total_read_bytes; + gint channel_type; + printf("total bytes read:\n"); + for (iter = list ; iter ; iter = iter->next) { + g_object_get(iter->data, + "total-read-bytes", &total_read_bytes, + "channel-type", &channel_type, + NULL); + printf("%s: %lu\n", + spice_channel_type_to_string(channel_type), + total_read_bytes); + } + g_list_free(list); + } + return 0; +} diff --git a/tools/spicy.c b/tools/spicy.c new file mode 100644 index 0000000..c502428 --- /dev/null +++ b/tools/spicy.c @@ -0,0 +1,1938 @@ +/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */ +/* + Copyright (C) 2010-2011 Red Hat, Inc. + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see <http://www.gnu.org/licenses/>. +*/ + +#include "config.h" +#include <glib.h> + +#include <sys/stat.h> +#ifdef HAVE_TERMIOS_H +#include <termios.h> +#endif + +#ifdef USE_SMARTCARD_012 +#include <vreader.h> +#endif + +#include "spice-widget.h" +#include "spice-gtk-session.h" +#include "spice-audio.h" +#include "spice-common.h" +#include "spice-cmdline.h" +#include "spice-option.h" +#include "usb-device-widget.h" + +#include "spicy-connect.h" + +typedef struct spice_connection spice_connection; + +enum { + STATE_SCROLL_LOCK, + STATE_CAPS_LOCK, + STATE_NUM_LOCK, + STATE_MAX, +}; + +#define SPICE_TYPE_WINDOW (spice_window_get_type ()) +#define SPICE_WINDOW(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SPICE_TYPE_WINDOW, SpiceWindow)) +#define SPICE_IS_WINDOW(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SPICE_TYPE_WINDOW)) +#define SPICE_WINDOW_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SPICE_TYPE_WINDOW, SpiceWindowClass)) +#define SPICE_IS_WINDOW_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SPICE_TYPE_WINDOW)) +#define SPICE_WINDOW_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SPICE_TYPE_WINDOW, SpiceWindowClass)) + +typedef struct _SpiceWindow SpiceWindow; +typedef struct _SpiceWindowClass SpiceWindowClass; + +struct _SpiceWindow { + GObject object; + spice_connection *conn; + gint id; + gint monitor_id; + GtkWidget *toplevel, *spice; + GtkWidget *menubar, *toolbar; + GtkWidget *ritem, *rmenu; + GtkWidget *statusbar, *status, *st[STATE_MAX]; + GtkActionGroup *ag; + GtkUIManager *ui; + bool fullscreen; + bool mouse_grabbed; + SpiceChannel *display_channel; +#ifdef G_OS_WIN32 + gint win_x; + gint win_y; +#endif + bool enable_accels_save; + bool enable_mnemonics_save; +}; + +struct _SpiceWindowClass +{ + GObjectClass parent_class; +}; + +static GType spice_window_get_type(void); + +G_DEFINE_TYPE (SpiceWindow, spice_window, G_TYPE_OBJECT); + +#define CHANNELID_MAX 4 +#define MONITORID_MAX 4 + + +// FIXME: turn this into an object, get rid of fixed wins array, use +// signals to replace the various callback that iterate over wins array +struct spice_connection { + SpiceSession *session; + SpiceGtkSession *gtk_session; + SpiceMainChannel *main; + SpiceWindow *wins[CHANNELID_MAX * MONITORID_MAX]; + SpiceAudio *audio; + const char *mouse_state; + const char *agent_state; + gboolean agent_connected; + int channels; + int disconnecting; + + /* key: SpiceFileTransferTask, value: TransferTaskWidgets */ + GHashTable *transfers; + GtkWidget *transfer_dialog; +}; + +static spice_connection *connection_new(void); +static void connection_connect(spice_connection *conn); +static void connection_disconnect(spice_connection *conn); +static void connection_destroy(spice_connection *conn); +static void usb_connect_failed(GObject *object, + SpiceUsbDevice *device, + GError *error, + gpointer data); +static gboolean is_gtk_session_property(const gchar *property); +static void del_window(spice_connection *conn, SpiceWindow *win); + +/* options */ +static gboolean fullscreen = false; +static gboolean version = false; +static char *spicy_title = NULL; +/* globals */ +static GMainLoop *mainloop = NULL; +static int connections = 0; +static GKeyFile *keyfile = NULL; +static SpicePortChannel*stdin_port = NULL; + +/* ------------------------------------------------------------------ */ + +static int ask_user(GtkWidget *parent, char *title, char *message, + char *dest, int dlen, int hide) +{ + GtkWidget *dialog, *area, *label, *entry; + const char *txt; + int retval; + + /* Create the widgets */ + dialog = gtk_dialog_new_with_buttons(title, + parent ? GTK_WINDOW(parent) : NULL, + GTK_DIALOG_DESTROY_WITH_PARENT, + "_OK", + GTK_RESPONSE_ACCEPT, + "_Cancel", + GTK_RESPONSE_REJECT, + NULL); + gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); + area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + + label = gtk_label_new(message); + gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5); + gtk_box_pack_start(GTK_BOX(area), label, FALSE, FALSE, 5); + + entry = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(entry), dest); + gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); + if (hide) + gtk_entry_set_visibility(GTK_ENTRY(entry), FALSE); + gtk_box_pack_start(GTK_BOX(area), entry, FALSE, FALSE, 5); + + /* show and wait for response */ + gtk_widget_show_all(dialog); + switch (gtk_dialog_run(GTK_DIALOG(dialog))) { + case GTK_RESPONSE_ACCEPT: + txt = gtk_entry_get_text(GTK_ENTRY(entry)); + snprintf(dest, dlen, "%s", txt); + retval = 0; + break; + default: + retval = -1; + break; + } + gtk_widget_destroy(dialog); + return retval; +} + +static void update_status_window(SpiceWindow *win) +{ + gchar *status; + + if (win == NULL) + return; + + if (win->mouse_grabbed) { + SpiceGrabSequence *sequence = spice_display_get_grab_keys(SPICE_DISPLAY(win->spice)); + gchar *seq = spice_grab_sequence_as_string(sequence); + status = g_strdup_printf("Use %s to ungrab mouse.", seq); + g_free(seq); + } else { + status = g_strdup_printf("mouse: %s, agent: %s", + win->conn->mouse_state, win->conn->agent_state); + } + + gtk_label_set_text(GTK_LABEL(win->status), status); + g_free(status); +} + +static void update_status(struct spice_connection *conn) +{ + int i; + + for (i = 0; i < SPICE_N_ELEMENTS(conn->wins); i++) { + if (conn->wins[i] == NULL) + continue; + update_status_window(conn->wins[i]); + } +} + +static const char *spice_edit_properties[] = { + "CopyToGuest", + "PasteFromGuest", +}; + +static void update_edit_menu_window(SpiceWindow *win) +{ + int i; + GtkAction *toggle; + + if (win == NULL) { + return; + } + + /* Make "CopyToGuest" and "PasteFromGuest" insensitive if spice + * agent is not connected */ + for (i = 0; i < G_N_ELEMENTS(spice_edit_properties); i++) { + toggle = gtk_action_group_get_action(win->ag, spice_edit_properties[i]); + if (toggle) { + gtk_action_set_sensitive(toggle, win->conn->agent_connected); + } + } +} + +static void update_edit_menu(struct spice_connection *conn) +{ + int i; + + for (i = 0; i < SPICE_N_ELEMENTS(conn->wins); i++) { + if (conn->wins[i]) { + update_edit_menu_window(conn->wins[i]); + } + } +} + +static void menu_cb_connect(GtkAction *action, void *data) +{ + struct spice_connection *conn; + + conn = connection_new(); + connection_connect(conn); +} + +static void menu_cb_close(GtkAction *action, void *data) +{ + SpiceWindow *win = data; + + connection_disconnect(win->conn); +} + +static void menu_cb_copy(GtkAction *action, void *data) +{ + SpiceWindow *win = data; + + spice_gtk_session_copy_to_guest(win->conn->gtk_session); +} + +static void menu_cb_paste(GtkAction *action, void *data) +{ + SpiceWindow *win = data; + + spice_gtk_session_paste_from_guest(win->conn->gtk_session); +} + +static void window_set_fullscreen(SpiceWindow *win, gboolean fs) +{ + if (fs) { +#ifdef G_OS_WIN32 + gtk_window_get_position(GTK_WINDOW(win->toplevel), &win->win_x, &win->win_y); +#endif + gtk_window_fullscreen(GTK_WINDOW(win->toplevel)); + } else { + gtk_window_unfullscreen(GTK_WINDOW(win->toplevel)); +#ifdef G_OS_WIN32 + gtk_window_move(GTK_WINDOW(win->toplevel), win->win_x, win->win_y); +#endif + } +} + +static void menu_cb_fullscreen(GtkAction *action, void *data) +{ + SpiceWindow *win = data; + + window_set_fullscreen(win, !win->fullscreen); +} + +#ifdef USE_SMARTCARD +static void enable_smartcard_actions(SpiceWindow *win, VReader *reader, + gboolean can_insert, gboolean can_remove) +{ + GtkAction *action; + + if ((reader != NULL) && (!spice_smartcard_reader_is_software((SpiceSmartcardReader*)reader))) + { + /* Having menu actions to insert/remove smartcards only makes sense + * for software smartcard readers, don't do anything when the event + * we received was for a "real" smartcard reader. + */ + return; + } + action = gtk_action_group_get_action(win->ag, "InsertSmartcard"); + g_return_if_fail(action != NULL); + gtk_action_set_sensitive(action, can_insert); + action = gtk_action_group_get_action(win->ag, "RemoveSmartcard"); + g_return_if_fail(action != NULL); + gtk_action_set_sensitive(action, can_remove); +} + + +static void reader_added_cb(SpiceSmartcardManager *manager, VReader *reader, + gpointer user_data) +{ + enable_smartcard_actions(user_data, reader, TRUE, FALSE); +} + +static void reader_removed_cb(SpiceSmartcardManager *manager, VReader *reader, + gpointer user_data) +{ + enable_smartcard_actions(user_data, reader, FALSE, FALSE); +} + +static void card_inserted_cb(SpiceSmartcardManager *manager, VReader *reader, + gpointer user_data) +{ + enable_smartcard_actions(user_data, reader, FALSE, TRUE); +} + +static void card_removed_cb(SpiceSmartcardManager *manager, VReader *reader, + gpointer user_data) +{ + enable_smartcard_actions(user_data, reader, TRUE, FALSE); +} + +static void menu_cb_insert_smartcard(GtkAction *action, void *data) +{ + spice_smartcard_manager_insert_card(spice_smartcard_manager_get()); +} + +static void menu_cb_remove_smartcard(GtkAction *action, void *data) +{ + spice_smartcard_manager_remove_card(spice_smartcard_manager_get()); +} +#endif + +static void menu_cb_mouse_mode(GtkAction *action, void *data) +{ + SpiceWindow *win = data; + SpiceMainChannel *cmain = win->conn->main; + int mode; + + g_object_get(cmain, "mouse-mode", &mode, NULL); + if (mode == SPICE_MOUSE_MODE_CLIENT) + mode = SPICE_MOUSE_MODE_SERVER; + else + mode = SPICE_MOUSE_MODE_CLIENT; + + spice_main_request_mouse_mode(cmain, mode); +} + +#ifdef USE_USBREDIR +static void remove_cb(GtkContainer *container, GtkWidget *widget, void *data) +{ + gtk_window_resize(GTK_WINDOW(data), 1, 1); +} + +static void menu_cb_select_usb_devices(GtkAction *action, void *data) +{ + GtkWidget *dialog, *area, *usb_device_widget; + SpiceWindow *win = data; + + /* Create the widgets */ + dialog = gtk_dialog_new_with_buttons( + "Select USB devices for redirection", + GTK_WINDOW(win->toplevel), + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + "_Close", GTK_RESPONSE_ACCEPT, + NULL); + gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT); + gtk_container_set_border_width(GTK_CONTAINER(dialog), 12); + gtk_box_set_spacing(GTK_BOX(gtk_bin_get_child(GTK_BIN(dialog))), 12); + + area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + + usb_device_widget = spice_usb_device_widget_new(win->conn->session, + NULL); /* default format */ + g_signal_connect(usb_device_widget, "connect-failed", + G_CALLBACK(usb_connect_failed), NULL); + gtk_box_pack_start(GTK_BOX(area), usb_device_widget, TRUE, TRUE, 0); + + /* This shrinks the dialog when USB devices are unplugged */ + g_signal_connect(usb_device_widget, "remove", + G_CALLBACK(remove_cb), dialog); + + /* show and run */ + gtk_widget_show_all(dialog); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} +#endif + +static void menu_cb_bool_prop(GtkToggleAction *action, gpointer data) +{ + SpiceWindow *win = data; + gboolean state = gtk_toggle_action_get_active(action); + const char *name; + gpointer object; + + name = gtk_action_get_name(GTK_ACTION(action)); + SPICE_DEBUG("%s: %s = %s", __FUNCTION__, name, state ? "yes" : "no"); + + g_key_file_set_boolean(keyfile, "general", name, state); + + if (is_gtk_session_property(name)) { + object = win->conn->gtk_session; + } else { + object = win->spice; + } + g_object_set(object, name, state, NULL); +} + +static void menu_cb_conn_bool_prop_changed(GObject *gobject, + GParamSpec *pspec, + gpointer user_data) +{ + SpiceWindow *win = user_data; + const gchar *property = g_param_spec_get_name(pspec); + GtkAction *toggle; + gboolean state; + + toggle = gtk_action_group_get_action(win->ag, property); + g_object_get(win->conn->gtk_session, property, &state, NULL); + gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(toggle), state); +} + +static void menu_cb_toolbar(GtkToggleAction *action, gpointer data) +{ + SpiceWindow *win = data; + gboolean state = gtk_toggle_action_get_active(action); + + gtk_widget_set_visible(win->toolbar, state); + g_key_file_set_boolean(keyfile, "ui", "toolbar", state); +} + +static void menu_cb_statusbar(GtkToggleAction *action, gpointer data) +{ + SpiceWindow *win = data; + gboolean state = gtk_toggle_action_get_active(action); + + gtk_widget_set_visible(win->statusbar, state); + g_key_file_set_boolean(keyfile, "ui", "statusbar", state); +} + +static void menu_cb_about(GtkAction *action, void *data) +{ + char *comments = "gtk test client app for the\n" + "spice remote desktop protocol"; + static const char *copyright = "(c) 2010 Red Hat"; + static const char *website = "http://www.spice-space.org"; + static const char *authors[] = { "Gerd Hoffmann <kraxel@redhat.com>", + "Marc-André Lureau <marcandre.lureau@redhat.com>", + NULL }; + SpiceWindow *win = data; + + gtk_show_about_dialog(GTK_WINDOW(win->toplevel), + "authors", authors, + "comments", comments, + "copyright", copyright, + "logo-icon-name", "help-about", + "website", website, + "version", PACKAGE_VERSION, + "license", "LGPLv2.1", + NULL); +} + +static gboolean delete_cb(GtkWidget *widget, GdkEvent *event, gpointer data) +{ + SpiceWindow *win = data; + + if (win->monitor_id == 0) + connection_disconnect(win->conn); + else + del_window(win->conn, win); + + return true; +} + +static gboolean window_state_cb(GtkWidget *widget, GdkEventWindowState *event, + gpointer data) +{ + SpiceWindow *win = data; + if (event->changed_mask & GDK_WINDOW_STATE_FULLSCREEN) { + win->fullscreen = event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN; + if (win->fullscreen) { + gtk_widget_hide(win->menubar); + gtk_widget_hide(win->toolbar); + gtk_widget_hide(win->statusbar); + gtk_widget_grab_focus(win->spice); + } else { + gboolean state; + GtkAction *toggle; + + gtk_widget_show(win->menubar); + toggle = gtk_action_group_get_action(win->ag, "Toolbar"); + state = gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(toggle)); + gtk_widget_set_visible(win->toolbar, state); + toggle = gtk_action_group_get_action(win->ag, "Statusbar"); + state = gtk_toggle_action_get_active(GTK_TOGGLE_ACTION(toggle)); + gtk_widget_set_visible(win->statusbar, state); + } + } + return TRUE; +} + +static void grab_keys_pressed_cb(GtkWidget *widget, gpointer data) +{ + SpiceWindow *win = data; + + /* since mnemonics are disabled, we leave fullscreen when + ungrabbing mouse. Perhaps we should have a different handling + of fullscreen key, or simply use a UI, like vinagre */ + window_set_fullscreen(win, FALSE); +} + +static void mouse_grab_cb(GtkWidget *widget, gint grabbed, gpointer data) +{ + SpiceWindow *win = data; + + win->mouse_grabbed = grabbed; + update_status(win->conn); +} + +static void keyboard_grab_cb(GtkWidget *widget, gint grabbed, gpointer data) +{ + SpiceWindow *win = data; + GtkSettings *settings = gtk_widget_get_settings (widget); + + if (grabbed) { + /* disable mnemonics & accels */ + g_object_get(settings, + "gtk-enable-accels", &win->enable_accels_save, + "gtk-enable-mnemonics", &win->enable_mnemonics_save, + NULL); + g_object_set(settings, + "gtk-enable-accels", FALSE, + "gtk-enable-mnemonics", FALSE, + NULL); + } else { + g_object_set(settings, + "gtk-enable-accels", win->enable_accels_save, + "gtk-enable-mnemonics", win->enable_mnemonics_save, + NULL); + } +} + +static void restore_configuration(SpiceWindow *win) +{ + gboolean state; + gchar *str; + gchar **keys = NULL; + gsize nkeys, i; + GError *error = NULL; + gpointer object; + + keys = g_key_file_get_keys(keyfile, "general", &nkeys, &error); + if (error != NULL) { + if (error->code != G_KEY_FILE_ERROR_GROUP_NOT_FOUND) + g_warning("Failed to read configuration file keys: %s", error->message); + g_clear_error(&error); + return; + } + + if (nkeys > 0) + g_return_if_fail(keys != NULL); + + for (i = 0; i < nkeys; ++i) { + if (g_str_equal(keys[i], "grab-sequence")) + continue; + state = g_key_file_get_boolean(keyfile, "general", keys[i], &error); + if (error != NULL) { + g_clear_error(&error); + continue; + } + + if (is_gtk_session_property(keys[i])) { + object = win->conn->gtk_session; + } else { + object = win->spice; + } + g_object_set(object, keys[i], state, NULL); + } + + g_strfreev(keys); + + str = g_key_file_get_string(keyfile, "general", "grab-sequence", &error); + if (error == NULL) { + SpiceGrabSequence *seq = spice_grab_sequence_new_from_string(str); + spice_display_set_grab_keys(SPICE_DISPLAY(win->spice), seq); + spice_grab_sequence_free(seq); + g_free(str); + } + g_clear_error(&error); + + + state = g_key_file_get_boolean(keyfile, "ui", "toolbar", &error); + if (error == NULL) + gtk_widget_set_visible(win->toolbar, state); + g_clear_error(&error); + + state = g_key_file_get_boolean(keyfile, "ui", "statusbar", &error); + if (error == NULL) + gtk_widget_set_visible(win->statusbar, state); + g_clear_error(&error); +} + +/* ------------------------------------------------------------------ */ + +static const GtkActionEntry entries[] = { + { + .name = "FileMenu", + .label = "_File", + },{ + .name = "FileRecentMenu", + .label = "_Recent", + },{ + .name = "EditMenu", + .label = "_Edit", + },{ + .name = "ViewMenu", + .label = "_View", + },{ + .name = "InputMenu", + .label = "_Input", + },{ + .name = "OptionMenu", + .label = "_Options", + },{ + .name = "CompressionMenu", + .label = "_Preferred image compression", + },{ + .name = "HelpMenu", + .label = "_Help", + },{ + + /* File menu */ + .name = "Connect", + .stock_id = "_Connect", + .label = "_Connect ...", + .callback = G_CALLBACK(menu_cb_connect), + },{ + .name = "Close", + .stock_id = "window-close", + .label = "_Close", + .callback = G_CALLBACK(menu_cb_close), + .accelerator = "", /* none (disable default "<control>W") */ + },{ + + /* Edit menu */ + .name = "CopyToGuest", + .stock_id = "edit-copy", + .label = "_Copy to guest", + .callback = G_CALLBACK(menu_cb_copy), + .accelerator = "", /* none (disable default "<control>C") */ + },{ + .name = "PasteFromGuest", + .stock_id = "edit-paste", + .label = "_Paste from guest", + .callback = G_CALLBACK(menu_cb_paste), + .accelerator = "", /* none (disable default "<control>V") */ + },{ + + /* View menu */ + .name = "Fullscreen", + .stock_id = "view-fullscreen", + .label = "_Fullscreen", + .callback = G_CALLBACK(menu_cb_fullscreen), + .accelerator = "<shift>F11", + },{ +#ifdef USE_SMARTCARD + .name = "InsertSmartcard", + .label = "_Insert Smartcard", + .callback = G_CALLBACK(menu_cb_insert_smartcard), + .accelerator = "<shift>F8", + },{ + .name = "RemoveSmartcard", + .label = "_Remove Smartcard", + .callback = G_CALLBACK(menu_cb_remove_smartcard), + .accelerator = "<shift>F9", + },{ +#endif + +#ifdef USE_USBREDIR + .name = "SelectUsbDevices", + .label = "_Select USB Devices for redirection", + .callback = G_CALLBACK(menu_cb_select_usb_devices), + .accelerator = "<shift>F10", + },{ +#endif + + .name = "MouseMode", + .label = "Toggle _mouse mode", + .callback = G_CALLBACK(menu_cb_mouse_mode), + .accelerator = "<shift>F7", + + },{ + /* Help menu */ + .name = "About", + .stock_id = "help-about", + .label = "_About ...", + .callback = G_CALLBACK(menu_cb_about), + } +}; + +static const char *spice_display_properties[] = { + "grab-keyboard", + "grab-mouse", + "resize-guest", + "scaling", + "disable-inputs", +}; + +static const char *spice_gtk_session_properties[] = { + "auto-clipboard", + "auto-usbredir", + "sync-modifiers", +}; + +static const GtkToggleActionEntry tentries[] = { + { + .name = "grab-keyboard", + .label = "Grab keyboard when active and focused", + .callback = G_CALLBACK(menu_cb_bool_prop), + },{ + .name = "grab-mouse", + .label = "Grab mouse in server mode (no tablet/vdagent)", + .callback = G_CALLBACK(menu_cb_bool_prop), + },{ + .name = "resize-guest", + .label = "Resize guest to match window size", + .callback = G_CALLBACK(menu_cb_bool_prop), + },{ + .name = "scaling", + .label = "Scale display", + .callback = G_CALLBACK(menu_cb_bool_prop), + },{ + .name = "disable-inputs", + .label = "Disable inputs", + .callback = G_CALLBACK(menu_cb_bool_prop), + },{ + .name = "sync-modifiers", + .label = "Sync modifiers", + .callback = G_CALLBACK(menu_cb_bool_prop), + },{ + .name = "auto-clipboard", + .label = "Automatic clipboard sharing between host and guest", + .callback = G_CALLBACK(menu_cb_bool_prop), + },{ + .name = "auto-usbredir", + .label = "Auto redirect newly plugged in USB devices", + .callback = G_CALLBACK(menu_cb_bool_prop), + },{ + .name = "Statusbar", + .label = "Statusbar", + .callback = G_CALLBACK(menu_cb_statusbar), + },{ + .name = "Toolbar", + .label = "Toolbar", + .callback = G_CALLBACK(menu_cb_toolbar), + } +}; + +static const GtkRadioActionEntry compression_entries[] = { + { + .name = "auto-glz", + .label = "auto-glz", + .value = SPICE_IMAGE_COMPRESSION_AUTO_GLZ, + },{ + .name = "auto-lz", + .label = "auto-lz", + .value = SPICE_IMAGE_COMPRESSION_AUTO_LZ, + },{ + .name = "quic", + .label = "quic", + .value = SPICE_IMAGE_COMPRESSION_QUIC, + },{ + .name = "glz", + .label = "glz", + .value = SPICE_IMAGE_COMPRESSION_GLZ, + },{ + .name = "lz", + .label = "lz", + .value = SPICE_IMAGE_COMPRESSION_LZ, + },{ +#ifdef USE_LZ4 + .name = "lz4", + .label = "lz4", + .value = SPICE_IMAGE_COMPRESSION_LZ4, + },{ +#endif + .name = "off", + .label = "off", + .value = SPICE_IMAGE_COMPRESSION_OFF, + } +}; + +static char ui_xml[] = +"<ui>\n" +" <menubar action='MainMenu'>\n" +" <menu action='FileMenu'>\n" +" <menuitem action='Connect'/>\n" +" <menu action='FileRecentMenu'/>\n" +" <separator/>\n" +" <menuitem action='Close'/>\n" +" </menu>\n" +" <menu action='EditMenu'>\n" +" <menuitem action='CopyToGuest'/>\n" +" <menuitem action='PasteFromGuest'/>\n" +" </menu>\n" +" <menu action='ViewMenu'>\n" +" <menuitem action='Fullscreen'/>\n" +" <menuitem action='Toolbar'/>\n" +" <menuitem action='Statusbar'/>\n" +" </menu>\n" +" <menu action='InputMenu'>\n" +#ifdef USE_SMARTCARD +" <menuitem action='InsertSmartcard'/>\n" +" <menuitem action='RemoveSmartcard'/>\n" +#endif +#ifdef USE_USBREDIR +" <menuitem action='SelectUsbDevices'/>\n" +#endif +" </menu>\n" +" <menu action='OptionMenu'>\n" +" <menuitem action='grab-keyboard'/>\n" +" <menuitem action='grab-mouse'/>\n" +" <menuitem action='MouseMode'/>\n" +" <menuitem action='resize-guest'/>\n" +" <menuitem action='scaling'/>\n" +" <menuitem action='disable-inputs'/>\n" +" <menuitem action='sync-modifiers'/>\n" +" <menuitem action='auto-clipboard'/>\n" +" <menuitem action='auto-usbredir'/>\n" +" <menu action='CompressionMenu'>\n" +" <menuitem action='auto-glz'/>\n" +" <menuitem action='auto-lz'/>\n" +" <menuitem action='quic'/>\n" +" <menuitem action='glz'/>\n" +" <menuitem action='lz'/>\n" +#ifdef USE_LZ4 +" <menuitem action='lz4'/>\n" +#endif +" <menuitem action='off'/>\n" +" </menu>\n" +" </menu>\n" +" <menu action='HelpMenu'>\n" +" <menuitem action='About'/>\n" +" </menu>\n" +" </menubar>\n" +" <toolbar action='ToolBar'>\n" +" <toolitem action='Close'/>\n" +" <separator/>\n" +" <toolitem action='CopyToGuest'/>\n" +" <toolitem action='PasteFromGuest'/>\n" +" <separator/>\n" +" <toolitem action='Fullscreen'/>\n" +" </toolbar>\n" +"</ui>\n"; + +static gboolean is_gtk_session_property(const gchar *property) +{ + int i; + + for (i = 0; i < G_N_ELEMENTS(spice_gtk_session_properties); i++) { + if (!strcmp(spice_gtk_session_properties[i], property)) { + return TRUE; + } + } + return FALSE; +} + +static void recent_item_activated_cb(GtkRecentChooser *chooser, gpointer data) +{ + GtkRecentInfo *info; + struct spice_connection *conn; + const char *uri; + + info = gtk_recent_chooser_get_current_item(chooser); + + uri = gtk_recent_info_get_uri(info); + g_return_if_fail(uri != NULL); + + conn = connection_new(); + g_object_set(conn->session, "uri", uri, NULL); + gtk_recent_info_unref(info); + connection_connect(conn); +} + +static void compression_cb(GtkRadioAction *action G_GNUC_UNUSED, + GtkRadioAction *current, + gpointer user_data) +{ + spice_display_change_preferred_compression(SPICE_CHANNEL(user_data), + gtk_radio_action_get_current_value(current)); +} + +static void +spice_window_class_init (SpiceWindowClass *klass) +{ +} + +static void +spice_window_init (SpiceWindow *self) +{ +} + +static SpiceWindow *create_spice_window(spice_connection *conn, SpiceChannel *channel, int id, gint monitor_id) +{ + char title[32]; + SpiceWindow *win; + GtkAction *toggle; + gboolean state; + GtkWidget *vbox, *frame; + GError *err = NULL; + int i; + SpiceGrabSequence *seq; + + win = g_object_new(SPICE_TYPE_WINDOW, NULL); + win->id = id; + win->monitor_id = monitor_id; + win->conn = conn; + win->display_channel = channel; + + /* toplevel */ + win->toplevel = gtk_window_new(GTK_WINDOW_TOPLEVEL); + if (spicy_title == NULL) { + snprintf(title, sizeof(title), "spice display %d:%d", id, monitor_id); + } else { + snprintf(title, sizeof(title), "%s", spicy_title); + } + + gtk_window_set_title(GTK_WINDOW(win->toplevel), title); + g_signal_connect(G_OBJECT(win->toplevel), "window-state-event", + G_CALLBACK(window_state_cb), win); + g_signal_connect(G_OBJECT(win->toplevel), "delete-event", + G_CALLBACK(delete_cb), win); + + /* menu + toolbar */ + win->ui = gtk_ui_manager_new(); + win->ag = gtk_action_group_new("MenuActions"); + gtk_action_group_add_actions(win->ag, entries, G_N_ELEMENTS(entries), win); + gtk_action_group_add_toggle_actions(win->ag, tentries, + G_N_ELEMENTS(tentries), win); + gtk_action_group_add_radio_actions(win->ag, compression_entries, + G_N_ELEMENTS(compression_entries), -1, + G_CALLBACK(compression_cb), win->display_channel); + if (!spice_channel_test_capability(win->display_channel, SPICE_DISPLAY_CAP_PREF_COMPRESSION)) { + GtkAction *compression_menu_action = gtk_action_group_get_action(win->ag, "CompressionMenu"); + gtk_action_set_sensitive(compression_menu_action, FALSE); + } + gtk_ui_manager_insert_action_group(win->ui, win->ag, 0); + gtk_window_add_accel_group(GTK_WINDOW(win->toplevel), + gtk_ui_manager_get_accel_group(win->ui)); + + err = NULL; + if (!gtk_ui_manager_add_ui_from_string(win->ui, ui_xml, -1, &err)) { + g_warning("building menus failed: %s", err->message); + g_error_free(err); + exit(1); + } + win->menubar = gtk_ui_manager_get_widget(win->ui, "/MainMenu"); + win->toolbar = gtk_ui_manager_get_widget(win->ui, "/ToolBar"); + + /* recent menu */ + win->ritem = gtk_ui_manager_get_widget + (win->ui, "/MainMenu/FileMenu/FileRecentMenu"); + + GtkRecentFilter *rfilter; + + win->rmenu = gtk_recent_chooser_menu_new(); + gtk_recent_chooser_set_show_icons(GTK_RECENT_CHOOSER(win->rmenu), FALSE); + rfilter = gtk_recent_filter_new(); + gtk_recent_filter_add_mime_type(rfilter, "application/x-spice"); + gtk_recent_chooser_add_filter(GTK_RECENT_CHOOSER(win->rmenu), rfilter); + gtk_recent_chooser_set_local_only(GTK_RECENT_CHOOSER(win->rmenu), FALSE); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(win->ritem), win->rmenu); + g_signal_connect(win->rmenu, "item-activated", + G_CALLBACK(recent_item_activated_cb), win); + + /* spice display */ + win->spice = GTK_WIDGET(spice_display_new_with_monitor(conn->session, id, monitor_id)); + seq = spice_grab_sequence_new_from_string("Shift_L+F12"); + spice_display_set_grab_keys(SPICE_DISPLAY(win->spice), seq); + spice_grab_sequence_free(seq); + + g_signal_connect(G_OBJECT(win->spice), "mouse-grab", + G_CALLBACK(mouse_grab_cb), win); + g_signal_connect(G_OBJECT(win->spice), "keyboard-grab", + G_CALLBACK(keyboard_grab_cb), win); + g_signal_connect(G_OBJECT(win->spice), "grab-keys-pressed", + G_CALLBACK(grab_keys_pressed_cb), win); + + /* status line */ + win->statusbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 1); + + win->status = gtk_label_new("status line"); + gtk_misc_set_alignment(GTK_MISC(win->status), 0, 0.5); + gtk_misc_set_padding(GTK_MISC(win->status), 3, 1); + update_status_window(win); + + frame = gtk_frame_new(NULL); + gtk_box_pack_start(GTK_BOX(win->statusbar), frame, TRUE, TRUE, 0); + gtk_container_add(GTK_CONTAINER(frame), win->status); + + for (i = 0; i < STATE_MAX; i++) { + win->st[i] = gtk_label_new("?"); + gtk_label_set_width_chars(GTK_LABEL(win->st[i]), 5); + frame = gtk_frame_new(NULL); + gtk_box_pack_end(GTK_BOX(win->statusbar), frame, FALSE, FALSE, 0); + gtk_container_add(GTK_CONTAINER(frame), win->st[i]); + } + + /* Make a vbox and put stuff in */ + vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 1); + gtk_container_set_border_width(GTK_CONTAINER(vbox), 0); + gtk_container_add(GTK_CONTAINER(win->toplevel), vbox); + gtk_box_pack_start(GTK_BOX(vbox), win->menubar, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), win->toolbar, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), win->spice, TRUE, TRUE, 0); + gtk_box_pack_end(GTK_BOX(vbox), win->statusbar, FALSE, TRUE, 0); + + /* show window */ + if (fullscreen) + gtk_window_fullscreen(GTK_WINDOW(win->toplevel)); + + gtk_widget_show_all(vbox); + restore_configuration(win); + + /* init toggle actions */ + for (i = 0; i < G_N_ELEMENTS(spice_display_properties); i++) { + toggle = gtk_action_group_get_action(win->ag, + spice_display_properties[i]); + g_object_get(win->spice, spice_display_properties[i], &state, NULL); + gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(toggle), state); + } + + for (i = 0; i < G_N_ELEMENTS(spice_gtk_session_properties); i++) { + char notify[64]; + + toggle = gtk_action_group_get_action(win->ag, + spice_gtk_session_properties[i]); + g_object_get(win->conn->gtk_session, spice_gtk_session_properties[i], + &state, NULL); + gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(toggle), state); + + snprintf(notify, sizeof(notify), "notify::%s", + spice_gtk_session_properties[i]); + spice_g_signal_connect_object(win->conn->gtk_session, notify, + G_CALLBACK(menu_cb_conn_bool_prop_changed), + win, 0); + } + + update_edit_menu_window(win); + + toggle = gtk_action_group_get_action(win->ag, "Toolbar"); + state = gtk_widget_get_visible(win->toolbar); + gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(toggle), state); + + toggle = gtk_action_group_get_action(win->ag, "Statusbar"); + state = gtk_widget_get_visible(win->statusbar); + gtk_toggle_action_set_active(GTK_TOGGLE_ACTION(toggle), state); + +#ifdef USE_SMARTCARD + gboolean smartcard; + + enable_smartcard_actions(win, NULL, FALSE, FALSE); + g_object_get(G_OBJECT(conn->session), + "enable-smartcard", &smartcard, + NULL); + if (smartcard) { + g_signal_connect(G_OBJECT(spice_smartcard_manager_get()), "reader-added", + (GCallback)reader_added_cb, win); + g_signal_connect(G_OBJECT(spice_smartcard_manager_get()), "reader-removed", + (GCallback)reader_removed_cb, win); + g_signal_connect(G_OBJECT(spice_smartcard_manager_get()), "card-inserted", + (GCallback)card_inserted_cb, win); + g_signal_connect(G_OBJECT(spice_smartcard_manager_get()), "card-removed", + (GCallback)card_removed_cb, win); + } +#endif + +#ifndef USE_USBREDIR + GtkAction *usbredir = gtk_action_group_get_action(win->ag, "auto-usbredir"); + gtk_action_set_visible(usbredir, FALSE); +#endif + + gtk_widget_grab_focus(win->spice); + + return win; +} + +static void destroy_spice_window(SpiceWindow *win) +{ + if (win == NULL) + return; + + SPICE_DEBUG("destroy window (#%d:%d)", win->id, win->monitor_id); + g_object_unref(win->ag); + g_object_unref(win->ui); + gtk_widget_destroy(win->toplevel); + g_object_unref(win); +} + +/* ------------------------------------------------------------------ */ + +static void recent_add(SpiceSession *session) +{ + GtkRecentManager *recent; + GtkRecentData meta = { + .mime_type = (char*)"application/x-spice", + .app_name = (char*)"spicy", + .app_exec = (char*)"spicy --uri=%u", + }; + char *uri; + + g_object_get(session, "uri", &uri, NULL); + SPICE_DEBUG("%s: %s", __FUNCTION__, uri); + + recent = gtk_recent_manager_get_default(); + if (g_str_has_prefix(uri, "spice://")) + meta.display_name = uri + 8; + else if (g_str_has_prefix(uri, "spice+unix://")) + meta.display_name = uri + 13; + else + g_return_if_reached(); + + if (!gtk_recent_manager_add_full(recent, uri, &meta)) + g_warning("Recent item couldn't be added successfully"); + + g_free(uri); +} + +static void main_channel_event(SpiceChannel *channel, SpiceChannelEvent event, + gpointer data) +{ + const GError *error = NULL; + spice_connection *conn = data; + char password[64]; + int rc; + + switch (event) { + case SPICE_CHANNEL_OPENED: + g_message("main channel: opened"); + recent_add(conn->session); + break; + case SPICE_CHANNEL_SWITCHING: + g_message("main channel: switching host"); + break; + case SPICE_CHANNEL_CLOSED: + /* this event is only sent if the channel was succesfully opened before */ + g_message("main channel: closed"); + connection_disconnect(conn); + break; + case SPICE_CHANNEL_ERROR_IO: + connection_disconnect(conn); + break; + case SPICE_CHANNEL_ERROR_TLS: + case SPICE_CHANNEL_ERROR_LINK: + case SPICE_CHANNEL_ERROR_CONNECT: + error = spice_channel_get_error(channel); + g_message("main channel: failed to connect"); + if (error) { + g_message("channel error: %s", error->message); + } + + if (spicy_connect_dialog(conn->session)) { + connection_connect(conn); + } else { + connection_disconnect(conn); + } + break; + case SPICE_CHANNEL_ERROR_AUTH: + g_warning("main channel: auth failure (wrong password?)"); + strcpy(password, ""); + /* FIXME i18 */ + rc = ask_user(NULL, "Authentication", + "Please enter the spice server password", + password, sizeof(password), true); + if (rc == 0) { + g_object_set(conn->session, "password", password, NULL); + connection_connect(conn); + } else { + connection_disconnect(conn); + } + break; + default: + /* TODO: more sophisticated error handling */ + g_warning("unknown main channel event: %u", event); + /* connection_disconnect(conn); */ + break; + } +} + +static void main_mouse_update(SpiceChannel *channel, gpointer data) +{ + spice_connection *conn = data; + gint mode; + + g_object_get(channel, "mouse-mode", &mode, NULL); + switch (mode) { + case SPICE_MOUSE_MODE_SERVER: + conn->mouse_state = "server"; + break; + case SPICE_MOUSE_MODE_CLIENT: + conn->mouse_state = "client"; + break; + default: + conn->mouse_state = "?"; + break; + } + update_status(conn); +} + +static void main_agent_update(SpiceChannel *channel, gpointer data) +{ + spice_connection *conn = data; + + g_object_get(channel, "agent-connected", &conn->agent_connected, NULL); + conn->agent_state = conn->agent_connected ? "yes" : "no"; + update_status(conn); + update_edit_menu(conn); +} + +static void inputs_modifiers(SpiceChannel *channel, gpointer data) +{ + spice_connection *conn = data; + int m, i; + + g_object_get(channel, "key-modifiers", &m, NULL); + for (i = 0; i < SPICE_N_ELEMENTS(conn->wins); i++) { + if (conn->wins[i] == NULL) + continue; + + gtk_label_set_text(GTK_LABEL(conn->wins[i]->st[STATE_SCROLL_LOCK]), + m & SPICE_KEYBOARD_MODIFIER_FLAGS_SCROLL_LOCK ? "SCROLL" : ""); + gtk_label_set_text(GTK_LABEL(conn->wins[i]->st[STATE_CAPS_LOCK]), + m & SPICE_KEYBOARD_MODIFIER_FLAGS_CAPS_LOCK ? "CAPS" : ""); + gtk_label_set_text(GTK_LABEL(conn->wins[i]->st[STATE_NUM_LOCK]), + m & SPICE_KEYBOARD_MODIFIER_FLAGS_NUM_LOCK ? "NUM" : ""); + } +} + +static void display_mark(SpiceChannel *channel, gint mark, SpiceWindow *win) +{ + g_return_if_fail(win != NULL); + g_return_if_fail(win->toplevel != NULL); + + if (mark == TRUE) { + gtk_widget_show(win->toplevel); + } else { + gtk_widget_hide(win->toplevel); + } +} + +static void update_auto_usbredir_sensitive(spice_connection *conn) +{ +#ifdef USE_USBREDIR + int i; + GtkAction *ac; + gboolean sensitive; + + sensitive = spice_session_has_channel_type(conn->session, + SPICE_CHANNEL_USBREDIR); + for (i = 0; i < SPICE_N_ELEMENTS(conn->wins); i++) { + if (conn->wins[i] == NULL) + continue; + ac = gtk_action_group_get_action(conn->wins[i]->ag, "auto-usbredir"); + gtk_action_set_sensitive(ac, sensitive); + } +#endif +} + +static SpiceWindow* get_window(spice_connection *conn, int channel_id, int monitor_id) +{ + g_return_val_if_fail(channel_id < CHANNELID_MAX, NULL); + g_return_val_if_fail(monitor_id < MONITORID_MAX, NULL); + + return conn->wins[channel_id * CHANNELID_MAX + monitor_id]; +} + +static void add_window(spice_connection *conn, SpiceWindow *win) +{ + g_return_if_fail(win != NULL); + g_return_if_fail(win->id < CHANNELID_MAX); + g_return_if_fail(win->monitor_id < MONITORID_MAX); + g_return_if_fail(conn->wins[win->id * CHANNELID_MAX + win->monitor_id] == NULL); + + SPICE_DEBUG("add display monitor %d:%d", win->id, win->monitor_id); + conn->wins[win->id * CHANNELID_MAX + win->monitor_id] = win; +} + +static void del_window(spice_connection *conn, SpiceWindow *win) +{ + if (win == NULL) + return; + + g_return_if_fail(win->id < CHANNELID_MAX); + g_return_if_fail(win->monitor_id < MONITORID_MAX); + + g_debug("del display monitor %d:%d", win->id, win->monitor_id); + conn->wins[win->id * CHANNELID_MAX + win->monitor_id] = NULL; + if (win->id > 0) + spice_main_set_display_enabled(conn->main, win->id, FALSE); + else + spice_main_set_display_enabled(conn->main, win->monitor_id, FALSE); + spice_main_send_monitor_config(conn->main); + + destroy_spice_window(win); +} + +static void display_monitors(SpiceChannel *display, GParamSpec *pspec, + spice_connection *conn) +{ + GArray *monitors = NULL; + int id; + guint i; + + g_object_get(display, + "channel-id", &id, + "monitors", &monitors, + NULL); + g_return_if_fail(monitors != NULL); + + for (i = 0; i < monitors->len; i++) { + SpiceWindow *w; + + if (!get_window(conn, id, i)) { + w = create_spice_window(conn, display, id, i); + add_window(conn, w); + spice_g_signal_connect_object(display, "display-mark", + G_CALLBACK(display_mark), w, 0); + gtk_widget_show(w->toplevel); + update_auto_usbredir_sensitive(conn); + } + } + + for (; i < MONITORID_MAX; i++) + del_window(conn, get_window(conn, id, i)); + + g_clear_pointer(&monitors, g_array_unref); +} + +static void port_write_cb(GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + SpicePortChannel *port = SPICE_PORT_CHANNEL(source_object); + GError *error = NULL; + + spice_port_write_finish(port, res, &error); + if (error != NULL) + g_warning("%s", error->message); + g_clear_error(&error); +} + +static void port_flushed_cb(GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + SpiceChannel *channel = SPICE_CHANNEL(source_object); + GError *error = NULL; + + spice_channel_flush_finish(channel, res, &error); + if (error != NULL) + g_warning("%s", error->message); + g_clear_error(&error); + + spice_channel_disconnect(channel, SPICE_CHANNEL_CLOSED); +} + +static gboolean input_cb(GIOChannel *gin, GIOCondition condition, gpointer data) +{ + char buf[4096]; + gsize bytes_read; + GIOStatus status; + + if (!(condition & G_IO_IN)) + return FALSE; + + status = g_io_channel_read_chars(gin, buf, sizeof(buf), &bytes_read, NULL); + if (status != G_IO_STATUS_NORMAL) + return FALSE; + + if (stdin_port != NULL) + spice_port_write_async(stdin_port, buf, bytes_read, NULL, port_write_cb, NULL); + + return TRUE; +} + +static void watch_stdin(void); + +static void port_opened(SpiceChannel *channel, GParamSpec *pspec, + spice_connection *conn) +{ + SpicePortChannel *port = SPICE_PORT_CHANNEL(channel); + gchar *name = NULL; + gboolean opened = FALSE; + + g_object_get(channel, + "port-name", &name, + "port-opened", &opened, + NULL); + + g_printerr("port %p %s: %s\n", channel, name, opened ? "opened" : "closed"); + + if (opened) { + /* only send a break event and disconnect */ + if (g_strcmp0(name, "org.spice.spicy.break") == 0) { + spice_port_event(port, SPICE_PORT_EVENT_BREAK); + spice_channel_flush_async(channel, NULL, port_flushed_cb, conn); + } + + /* handle the first spicy port and connect it to stdin/out */ + if (g_strcmp0(name, "org.spice.spicy") == 0 && stdin_port == NULL) { + watch_stdin(); + stdin_port = port; + } + } else { + if (port == stdin_port) + stdin_port = NULL; + } + + g_free(name); +} + +static void port_data(SpicePortChannel *port, + gpointer data, int size, spice_connection *conn) +{ + int r; + + if (port != stdin_port) + return; + + r = write(fileno(stdout), data, size); + if (r != size) { + g_warning("port write failed result %d/%d errno %d", r, size, errno); + } +} + +typedef struct { + GtkWidget *vbox; + GtkWidget *hbox; + GtkWidget *progress; + GtkWidget *label; + GtkWidget *cancel; +} TransferTaskWidgets; + +static void transfer_update_progress(GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + spice_connection *conn = user_data; + TransferTaskWidgets *widgets = g_hash_table_lookup(conn->transfers, object); + g_return_if_fail(widgets); + gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(widgets->progress), + spice_file_transfer_task_get_progress(SPICE_FILE_TRANSFER_TASK(object))); +} + +static void transfer_task_finished(SpiceFileTransferTask *task, GError *error, spice_connection *conn) +{ + if (error) + g_warning("%s", error->message); + g_hash_table_remove(conn->transfers, task); + if (!g_hash_table_size(conn->transfers)) + gtk_widget_hide(conn->transfer_dialog); +} + +static void dialog_response_cb(GtkDialog *dialog, + gint response_id, + gpointer user_data) +{ + spice_connection *conn = user_data; + g_print("Reponse: %i\n", response_id); + + if (response_id == GTK_RESPONSE_CANCEL) { + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init(&iter, conn->transfers); + while (g_hash_table_iter_next(&iter, &key, &value)) { + SpiceFileTransferTask *task = key; + spice_file_transfer_task_cancel(task); + } + } +} + +static void +task_cancel_cb(GtkButton *button, + gpointer user_data) +{ + SpiceFileTransferTask *task = SPICE_FILE_TRANSFER_TASK(user_data); + spice_file_transfer_task_cancel(task); +} + +static TransferTaskWidgets * +transfer_task_widgets_new(SpiceFileTransferTask *task) +{ + char *filename; + TransferTaskWidgets *widgets = g_new0(TransferTaskWidgets, 1); + + widgets->vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + widgets->hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + widgets->cancel = gtk_button_new_with_label("Cancel"); + + widgets->progress = gtk_progress_bar_new(); + filename = spice_file_transfer_task_get_filename(task); + widgets->label = gtk_label_new(filename); + g_free(filename); + + gtk_widget_set_halign(widgets->label, GTK_ALIGN_START); + gtk_widget_set_valign(widgets->label, GTK_ALIGN_BASELINE); + gtk_widget_set_valign(widgets->progress, GTK_ALIGN_CENTER); + gtk_widget_set_hexpand(widgets->progress, TRUE); + gtk_widget_set_valign(widgets->cancel, GTK_ALIGN_CENTER); + gtk_widget_set_hexpand(widgets->progress, FALSE); + + gtk_box_pack_start(GTK_BOX(widgets->hbox), widgets->progress, + TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(widgets->hbox), widgets->cancel, + FALSE, TRUE, 0); + + gtk_box_pack_start(GTK_BOX(widgets->vbox), widgets->label, + TRUE, TRUE, 0); + gtk_box_pack_start(GTK_BOX(widgets->vbox), widgets->hbox, + TRUE, TRUE, 0); + + g_signal_connect(widgets->cancel, "clicked", + G_CALLBACK(task_cancel_cb), task); + + gtk_widget_show_all(widgets->vbox); + + return widgets; +} + +static void +transfer_task_widgets_free(TransferTaskWidgets *widgets) +{ + /* child widgets will be destroyed automatically */ + gtk_widget_destroy(widgets->vbox); + g_free(widgets); +} + +static void spice_connection_add_task(spice_connection *conn, SpiceFileTransferTask *task) +{ + TransferTaskWidgets *widgets; + GtkWidget *content = NULL; + + g_signal_connect(task, "notify::progress", + G_CALLBACK(transfer_update_progress), conn); + g_signal_connect(task, "finished", + G_CALLBACK(transfer_task_finished), conn); + if (!conn->transfer_dialog) { + conn->transfer_dialog = gtk_dialog_new_with_buttons("File Transfers", + GTK_WINDOW(conn->wins[0]->toplevel), 0, + "Cancel", GTK_RESPONSE_CANCEL, NULL); + gtk_dialog_set_default_response(GTK_DIALOG(conn->transfer_dialog), + GTK_RESPONSE_CANCEL); + gtk_window_set_resizable(GTK_WINDOW(conn->transfer_dialog), FALSE); + g_signal_connect(conn->transfer_dialog, "response", + G_CALLBACK(dialog_response_cb), conn); + } + gtk_widget_show(conn->transfer_dialog); + content = gtk_dialog_get_content_area(GTK_DIALOG(conn->transfer_dialog)); + gtk_container_set_border_width(GTK_CONTAINER(content), 12); + + widgets = transfer_task_widgets_new(task); + g_hash_table_insert(conn->transfers, g_object_ref(task), widgets); + gtk_box_pack_start(GTK_BOX(content), + widgets->vbox, TRUE, TRUE, 6); +} + +static void new_file_transfer(SpiceMainChannel *main, SpiceFileTransferTask *task, gpointer user_data) +{ + spice_connection *conn = user_data; + g_debug("new file transfer task"); + spice_connection_add_task(conn, task); +} + +static void channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data) +{ + spice_connection *conn = data; + int id; + + g_object_get(channel, "channel-id", &id, NULL); + conn->channels++; + SPICE_DEBUG("new channel (#%d)", id); + + if (SPICE_IS_MAIN_CHANNEL(channel)) { + SPICE_DEBUG("new main channel"); + conn->main = SPICE_MAIN_CHANNEL(channel); + g_signal_connect(channel, "channel-event", + G_CALLBACK(main_channel_event), conn); + g_signal_connect(channel, "main-mouse-update", + G_CALLBACK(main_mouse_update), conn); + g_signal_connect(channel, "main-agent-update", + G_CALLBACK(main_agent_update), conn); + g_signal_connect(channel, "new-file-transfer", + G_CALLBACK(new_file_transfer), conn); + main_mouse_update(channel, conn); + main_agent_update(channel, conn); + } + + if (SPICE_IS_DISPLAY_CHANNEL(channel)) { + if (id >= SPICE_N_ELEMENTS(conn->wins)) + return; + if (conn->wins[id] != NULL) + return; + SPICE_DEBUG("new display channel (#%d)", id); + g_signal_connect(channel, "notify::monitors", + G_CALLBACK(display_monitors), conn); + spice_channel_connect(channel); + } + + if (SPICE_IS_INPUTS_CHANNEL(channel)) { + SPICE_DEBUG("new inputs channel"); + g_signal_connect(channel, "inputs-modifiers", + G_CALLBACK(inputs_modifiers), conn); + } + + if (SPICE_IS_PLAYBACK_CHANNEL(channel)) { + SPICE_DEBUG("new audio channel"); + conn->audio = spice_audio_get(s, NULL); + } + + if (SPICE_IS_USBREDIR_CHANNEL(channel)) { + update_auto_usbredir_sensitive(conn); + } + + if (SPICE_IS_PORT_CHANNEL(channel)) { + g_signal_connect(channel, "notify::port-opened", + G_CALLBACK(port_opened), conn); + g_signal_connect(channel, "port-data", + G_CALLBACK(port_data), conn); + spice_channel_connect(channel); + } +} + +static void channel_destroy(SpiceSession *s, SpiceChannel *channel, gpointer data) +{ + spice_connection *conn = data; + int id; + + g_object_get(channel, "channel-id", &id, NULL); + if (SPICE_IS_MAIN_CHANNEL(channel)) { + SPICE_DEBUG("zap main channel"); + conn->main = NULL; + } + + if (SPICE_IS_DISPLAY_CHANNEL(channel)) { + if (id >= SPICE_N_ELEMENTS(conn->wins)) + return; + SPICE_DEBUG("zap display channel (#%d)", id); + /* FIXME destroy widget only */ + } + + if (SPICE_IS_PLAYBACK_CHANNEL(channel)) { + SPICE_DEBUG("zap audio channel"); + } + + if (SPICE_IS_USBREDIR_CHANNEL(channel)) { + update_auto_usbredir_sensitive(conn); + } + + if (SPICE_IS_PORT_CHANNEL(channel)) { + if (SPICE_PORT_CHANNEL(channel) == stdin_port) + stdin_port = NULL; + } + + conn->channels--; + if (conn->channels > 0) { + return; + } + + connection_destroy(conn); +} + +static void migration_state(GObject *session, + GParamSpec *pspec, gpointer data) +{ + SpiceSessionMigration mig; + + g_object_get(session, "migration-state", &mig, NULL); + if (mig == SPICE_SESSION_MIGRATION_SWITCHING) + g_message("migrating session"); +} + +static spice_connection *connection_new(void) +{ + spice_connection *conn; + SpiceUsbDeviceManager *manager; + + conn = g_new0(spice_connection, 1); + conn->session = spice_session_new(); + conn->gtk_session = spice_gtk_session_get(conn->session); + g_signal_connect(conn->session, "channel-new", + G_CALLBACK(channel_new), conn); + g_signal_connect(conn->session, "channel-destroy", + G_CALLBACK(channel_destroy), conn); + g_signal_connect(conn->session, "notify::migration-state", + G_CALLBACK(migration_state), conn); + + manager = spice_usb_device_manager_get(conn->session, NULL); + if (manager) { + g_signal_connect(manager, "auto-connect-failed", + G_CALLBACK(usb_connect_failed), NULL); + g_signal_connect(manager, "device-error", + G_CALLBACK(usb_connect_failed), NULL); + } + + conn->transfers = g_hash_table_new_full(g_direct_hash, g_direct_equal, + g_object_unref, + (GDestroyNotify)transfer_task_widgets_free); + connections++; + SPICE_DEBUG("%s (%d)", __FUNCTION__, connections); + return conn; +} + +static void connection_connect(spice_connection *conn) +{ + conn->disconnecting = false; + spice_session_connect(conn->session); +} + +static void connection_disconnect(spice_connection *conn) +{ + if (conn->disconnecting) + return; + conn->disconnecting = true; + spice_session_disconnect(conn->session); +} + +static void connection_destroy(spice_connection *conn) +{ + g_object_unref(conn->session); + g_hash_table_unref(conn->transfers); + free(conn); + + connections--; + SPICE_DEBUG("%s (%d)", __FUNCTION__, connections); + if (connections > 0) { + return; + } + + g_main_loop_quit(mainloop); +} + +/* ------------------------------------------------------------------ */ + +static GOptionEntry cmd_entries[] = { + { + .long_name = "full-screen", + .short_name = 'f', + .arg = G_OPTION_ARG_NONE, + .arg_data = &fullscreen, + .description = "Open in full screen mode", + },{ + .long_name = "version", + .arg = G_OPTION_ARG_NONE, + .arg_data = &version, + .description = "Display version and quit", + },{ + .long_name = "title", + .arg = G_OPTION_ARG_STRING, + .arg_data = &spicy_title, + .description = "Set the window title", + .arg_description = "<title>", + },{ + /* end of list */ + } +}; + +static void usb_connect_failed(GObject *object, + SpiceUsbDevice *device, + GError *error, + gpointer data) +{ + GtkWidget *dialog; + + if (error->domain == G_IO_ERROR && error->code == G_IO_ERROR_CANCELLED) + return; + + dialog = gtk_message_dialog_new(NULL, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, + GTK_BUTTONS_CLOSE, + "USB redirection error"); + gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog), + "%s", error->message); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + +static void setup_terminal(gboolean reset) +{ + int stdinfd = fileno(stdin); + + if (!isatty(stdinfd)) + return; + +#ifdef HAVE_TERMIOS_H + struct termios tios; + static struct termios saved_tios; + static bool saved = false; + + if (reset) { + if (!saved) + return; + tios = saved_tios; + } else { + tcgetattr(stdinfd, &tios); + saved_tios = tios; + saved = true; + tios.c_lflag &= ~(ICANON | ECHO); + } + + tcsetattr(stdinfd, TCSANOW, &tios); +#endif +} + +static void watch_stdin(void) +{ + int stdinfd = fileno(stdin); + GIOChannel *gin; + + setup_terminal(false); + gin = g_io_channel_unix_new(stdinfd); + g_io_channel_set_flags(gin, G_IO_FLAG_NONBLOCK, NULL); + g_io_add_watch(gin, G_IO_IN|G_IO_ERR|G_IO_HUP|G_IO_NVAL, input_cb, NULL); +} + +int main(int argc, char *argv[]) +{ + GError *error = NULL; + GOptionContext *context; + spice_connection *conn; + gchar *conf_file, *conf; + char *host = NULL, *port = NULL, *tls_port = NULL, *unix_path = NULL; + + keyfile = g_key_file_new(); + + int mode = S_IRWXU; + conf_file = g_build_filename(g_get_user_config_dir(), "spicy", NULL); + if (g_mkdir_with_parents(conf_file, mode) == -1) + SPICE_DEBUG("failed to create config directory"); + g_free(conf_file); + + conf_file = g_build_filename(g_get_user_config_dir(), "spicy", "settings", NULL); + if (!g_key_file_load_from_file(keyfile, conf_file, + G_KEY_FILE_KEEP_COMMENTS|G_KEY_FILE_KEEP_TRANSLATIONS, &error)) { + SPICE_DEBUG("Couldn't load configuration: %s", error->message); + g_clear_error(&error); + } + + /* parse opts */ + gtk_init(&argc, &argv); + context = g_option_context_new("- spice client test application"); + g_option_context_set_summary(context, "Gtk+ test client to connect to Spice servers."); + g_option_context_set_description(context, "Report bugs to " PACKAGE_BUGREPORT "."); + g_option_context_add_group(context, spice_get_option_group()); + g_option_context_set_main_group(context, spice_cmdline_get_option_group()); + g_option_context_add_main_entries(context, cmd_entries, NULL); + g_option_context_add_group(context, gtk_get_option_group(TRUE)); + if (!g_option_context_parse (context, &argc, &argv, &error)) { + g_print("option parsing failed: %s\n", error->message); + exit(1); + } + g_option_context_free(context); + + if (version) { + g_print("spicy " PACKAGE_VERSION "\n"); + exit(0); + } + + mainloop = g_main_loop_new(NULL, false); + + conn = connection_new(); + spice_set_session_option(conn->session); + spice_cmdline_session_setup(conn->session); + + g_object_get(conn->session, + "unix-path", &unix_path, + "host", &host, + "port", &port, + "tls-port", &tls_port, + NULL); + /* If user doesn't provide hostname and port, show the dialog window + instead of connecting to server automatically */ + if ((host == NULL || (port == NULL && tls_port == NULL)) && unix_path == NULL) { + if (!spicy_connect_dialog(conn->session)) { + exit(0); + } + } + g_free(host); + g_free(port); + g_free(tls_port); + g_free(unix_path); + + connection_connect(conn); + if (connections > 0) + g_main_loop_run(mainloop); + g_main_loop_unref(mainloop); + + if ((conf = g_key_file_to_data(keyfile, NULL, &error)) == NULL || + !g_file_set_contents(conf_file, conf, -1, &error)) { + SPICE_DEBUG("Couldn't save configuration: %s", error->message); + g_error_free(error); + error = NULL; + } + + g_free(conf_file); + g_free(conf); + g_key_file_free(keyfile); + + g_free(spicy_title); + + setup_terminal(true); + return 0; +} |