summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMarc-André Lureau <marcandre.lureau@redhat.com>2015-06-05 17:44:47 +0200
committerMarc-André Lureau <marcandre.lureau@redhat.com>2015-06-08 17:38:58 +0200
commitcaf28401cac9ece5e314360f8a3a54479df59727 (patch)
tree7ada0451442ffa1d9d056500ebd878ad80da3e06 /src
parent39c315241350dde3741c99fee4fff17b22d2b1fa (diff)
downloadspice-gtk-caf28401cac9ece5e314360f8a3a54479df59727.tar.gz
spice-gtk-caf28401cac9ece5e314360f8a3a54479df59727.tar.xz
spice-gtk-caf28401cac9ece5e314360f8a3a54479df59727.zip
Move gtk/ -> src/
For historical reasons, the code was placed under gtk/ subdirectory. If it was always bugging you, bug no more!
Diffstat (limited to 'src')
-rw-r--r--src/Makefile.am703
-rw-r--r--src/bio-gio.c114
-rw-r--r--src/bio-gio.h30
-rw-r--r--src/channel-base.c284
-rw-r--r--src/channel-cursor.c529
-rw-r--r--src/channel-cursor.h77
-rw-r--r--src/channel-display-mjpeg.c156
-rw-r--r--src/channel-display-priv.h113
-rw-r--r--src/channel-display.c1789
-rw-r--r--src/channel-display.h102
-rw-r--r--src/channel-inputs.c603
-rw-r--r--src/channel-inputs.h89
-rw-r--r--src/channel-main.c2993
-rw-r--r--src/channel-main.h109
-rw-r--r--src/channel-playback-priv.h24
-rw-r--r--src/channel-playback.c496
-rw-r--r--src/channel-playback.h76
-rw-r--r--src/channel-port.c361
-rw-r--r--src/channel-port.h76
-rw-r--r--src/channel-record.c482
-rw-r--r--src/channel-record.h77
-rw-r--r--src/channel-smartcard.c587
-rw-r--r--src/channel-smartcard.h68
-rw-r--r--src/channel-usbredir-priv.h61
-rw-r--r--src/channel-usbredir.c686
-rw-r--r--src/channel-usbredir.h71
-rw-r--r--src/channel-webdav.c613
-rw-r--r--src/channel-webdav.h68
-rw-r--r--src/client_sw_canvas.c20
-rw-r--r--src/client_sw_canvas.h25
-rw-r--r--src/continuation.c102
-rw-r--r--src/continuation.h61
-rw-r--r--src/controller/Makefile.am100
-rw-r--r--src/controller/controller.vala286
-rw-r--r--src/controller/custom.h22
-rw-r--r--src/controller/custom.vapi28
-rw-r--r--src/controller/dump.c118
-rw-r--r--src/controller/foreign-menu.vala197
-rw-r--r--src/controller/gio-windows-2.0.vapi30
-rw-r--r--src/controller/menu.vala108
-rw-r--r--src/controller/namedpipe.c270
-rw-r--r--src/controller/namedpipe.h59
-rw-r--r--src/controller/namedpipeconnection.c245
-rw-r--r--src/controller/namedpipeconnection.h56
-rw-r--r--src/controller/namedpipelistener.c329
-rw-r--r--src/controller/namedpipelistener.h70
-rw-r--r--src/controller/spice-controller-listener.c159
-rw-r--r--src/controller/spice-controller-listener.h47
-rw-r--r--src/controller/spice-foreign-menu-listener.c161
-rw-r--r--src/controller/spice-foreign-menu-listener.h47
-rw-r--r--src/controller/test.c292
-rw-r--r--src/controller/util.vala42
-rw-r--r--src/controller/win32-util.c161
-rw-r--r--src/controller/win32-util.h30
-rw-r--r--src/coroutine.h83
-rw-r--r--src/coroutine_gthread.c170
-rw-r--r--src/coroutine_ucontext.c150
-rw-r--r--src/coroutine_winfibers.c126
-rw-r--r--src/decode-glz-tmpl.c336
-rw-r--r--src/decode-glz.c475
-rw-r--r--src/decode-jpeg.c191
-rw-r--r--src/decode-zlib.c89
-rw-r--r--src/decode.h44
-rw-r--r--src/desktop-integration.c223
-rw-r--r--src/desktop-integration.h64
-rw-r--r--src/gio-coroutine.c275
-rw-r--r--src/gio-coroutine.h66
-rw-r--r--src/giopipe.c484
-rw-r--r--src/giopipe.h29
-rw-r--r--src/glib-compat.c79
-rw-r--r--src/glib-compat.h68
-rw-r--r--src/gtk-compat.h56
-rwxr-xr-xsrc/keymap-gen.pl214
-rw-r--r--src/keymaps.csv490
-rw-r--r--src/map-file139
-rw-r--r--src/smartcard-manager-priv.h37
-rw-r--r--src/smartcard-manager.c737
-rw-r--r--src/smartcard-manager.h80
-rw-r--r--src/spice-audio-priv.h42
-rw-r--r--src/spice-audio.c274
-rw-r--r--src/spice-audio.h109
-rw-r--r--src/spice-channel-cache.h106
-rw-r--r--src/spice-channel-enums.h7
-rw-r--r--src/spice-channel-priv.h203
-rw-r--r--src/spice-channel.c2960
-rw-r--r--src/spice-channel.h131
-rw-r--r--src/spice-client-glib-usb-acl-helper.c372
-rw-r--r--src/spice-client-gtk-manual.defs117
-rw-r--r--src/spice-client-gtk-module.c45
-rw-r--r--src/spice-client-gtk.override171
-rw-r--r--src/spice-client.c27
-rw-r--r--src/spice-client.h86
-rw-r--r--src/spice-cmdline.c98
-rw-r--r--src/spice-cmdline.h29
-rw-r--r--src/spice-common.h36
-rw-r--r--src/spice-glib-sym-file111
-rw-r--r--src/spice-grabsequence.c163
-rw-r--r--src/spice-grabsequence.h61
-rw-r--r--src/spice-gstaudio.c739
-rw-r--r--src/spice-gstaudio.h56
-rw-r--r--src/spice-gtk-session-priv.h34
-rw-r--r--src/spice-gtk-session.c1229
-rw-r--r--src/spice-gtk-session.h65
-rw-r--r--src/spice-gtk-sym-file23
-rw-r--r--src/spice-marshal.txt14
-rw-r--r--src/spice-option.c284
-rw-r--r--src/spice-option.h31
-rw-r--r--src/spice-pulse.c1354
-rw-r--r--src/spice-pulse.h57
-rw-r--r--src/spice-session-priv.h104
-rw-r--r--src/spice-session.c2728
-rw-r--r--src/spice-session.h103
-rw-r--r--src/spice-types.h35
-rw-r--r--src/spice-uri-priv.h30
-rw-r--r--src/spice-uri.c462
-rw-r--r--src/spice-uri.h52
-rw-r--r--src/spice-util-priv.h52
-rw-r--r--src/spice-util.c497
-rw-r--r--src/spice-util.h63
-rw-r--r--src/spice-version.h.in70
-rw-r--r--src/spice-widget-cairo.c160
-rw-r--r--src/spice-widget-priv.h141
-rw-r--r--src/spice-widget-x11.c280
-rw-r--r--src/spice-widget.c2642
-rw-r--r--src/spice-widget.h92
-rw-r--r--src/spicy-screenshot.c196
-rw-r--r--src/spicy-stats.c144
-rw-r--r--src/spicy.c1855
-rw-r--r--src/usb-acl-helper.c299
-rw-r--r--src/usb-acl-helper.h72
-rw-r--r--src/usb-device-manager-priv.h48
-rw-r--r--src/usb-device-manager.c1907
-rw-r--r--src/usb-device-manager.h122
-rw-r--r--src/usb-device-widget.c554
-rw-r--r--src/usb-device-widget.h81
-rw-r--r--src/usbutil.c323
-rw-r--r--src/usbutil.h39
-rw-r--r--src/vmcstream.c535
-rw-r--r--src/vmcstream.h81
-rw-r--r--src/vncdisplaykeymap.c323
-rw-r--r--src/vncdisplaykeymap.h36
-rw-r--r--src/win-usb-clerk.h36
-rw-r--r--src/win-usb-dev.c542
-rw-r--r--src/win-usb-dev.h110
-rw-r--r--src/win-usb-driver-install.c426
-rw-r--r--src/win-usb-driver-install.h106
-rw-r--r--src/wocky-http-proxy.c537
-rw-r--r--src/wocky-http-proxy.h56
148 files changed, 44180 insertions, 0 deletions
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644
index 0000000..25e2255
--- /dev/null
+++ b/src/Makefile.am
@@ -0,0 +1,703 @@
+NULL =
+SUBDIRS =
+
+if WITH_CONTROLLER
+SUBDIRS += controller
+endif
+
+# Avoid need for perl(Text::CSV) by end users
+KEYMAPS = \
+ vncdisplaykeymap_xorgevdev2xtkbd.c \
+ vncdisplaykeymap_xorgkbd2xtkbd.c \
+ vncdisplaykeymap_xorgxquartz2xtkbd.c \
+ vncdisplaykeymap_xorgxwin2xtkbd.c \
+ vncdisplaykeymap_osx2xtkbd.c \
+ vncdisplaykeymap_win322xtkbd.c \
+ vncdisplaykeymap_x112xtkbd.c \
+ $(NULL)
+
+# End users build dependencies can be cleaned
+GLIBGENS = \
+ spice-glib-enums.c \
+ spice-glib-enums.h \
+ spice-marshal.c \
+ spice-marshal.h \
+ spice-widget-enums.c \
+ spice-widget-enums.h \
+ $(NULL)
+
+CLEANFILES = $(GLIBGENS)
+BUILT_SOURCES = $(GLIBGENS) $(KEYMAPS)
+
+EXTRA_DIST = \
+ $(KEYMAPS) \
+ decode-glz-tmpl.c \
+ keymap-gen.pl \
+ keymaps.csv \
+ map-file \
+ spice-glib-sym-file \
+ spice-gtk-sym-file \
+ spice-client-gtk-manual.defs \
+ spice-client-gtk.override \
+ spice-marshal.txt \
+ spice-version.h.in \
+ $(NULL)
+
+DISTCLEANFILES = spice-version.h
+
+bin_PROGRAMS = spicy-stats spicy-screenshot
+if WITH_GTK
+bin_PROGRAMS += spicy
+endif
+if WITH_POLKIT
+acldir = $(ACL_HELPER_DIR)
+acl_PROGRAMS = spice-client-glib-usb-acl-helper
+endif
+
+lib_LTLIBRARIES = libspice-client-glib-2.0.la
+
+if WITH_GTK
+if HAVE_GTK_2
+lib_LTLIBRARIES += libspice-client-gtk-2.0.la
+else
+lib_LTLIBRARIES += libspice-client-gtk-3.0.la
+endif
+endif
+
+if HAVE_LD_VERSION_SCRIPT
+GLIB_SYMBOLS_LDFLAGS = -Wl,--version-script=${srcdir}/map-file
+GLIB_SYMBOLS_FILE = map-file
+GTK_SYMBOLS_LDFLAGS = $(GLIB_SYMBOLS_LDFLAGS)
+GTK_SYMBOLS_FILE = $(GLIB_SYMBOLS_FILE)
+else
+GLIB_SYMBOLS_LDFLAGS = -export-symbols ${srcdir}/spice-glib-sym-file
+GLIB_SYMBOLS_FILE = spice-glib-sym-file
+GTK_SYMBOLS_LDFLAGS = -export-symbols ${srcdir}/spice-gtk-sym-file
+GTK_SYMBOLS_FILE = spice-gtk-sym-file
+endif
+
+KEYMAP_GEN = $(srcdir)/keymap-gen.pl
+
+SPICE_COMMON_CPPFLAGS = \
+ -DG_LOG_DOMAIN=\"GSpice\" \
+ -DSPICE_NO_DEPRECATED \
+ -DSPICE_GTK_LOCALEDIR=\"${SPICE_GTK_LOCALEDIR}\" \
+ -DPNP_IDS=\""$(PNP_IDS)"\" \
+ -DUSB_IDS=\""$(USB_IDS)"\" \
+ -DSPICE_DISABLE_ABORT \
+ -I$(top_srcdir) \
+ $(COMMON_CFLAGS) \
+ $(PIXMAN_CFLAGS) \
+ $(PULSE_CFLAGS) \
+ $(GTK_CFLAGS) \
+ $(CAIRO_CFLAGS) \
+ $(GLIB2_CFLAGS) \
+ $(GIO_CFLAGS) \
+ $(GOBJECT2_CFLAGS) \
+ $(SSL_CFLAGS) \
+ $(SASL_CFLAGS) \
+ $(GST_CFLAGS) \
+ $(SMARTCARD_CFLAGS) \
+ $(USBREDIR_CFLAGS) \
+ $(GUDEV_CFLAGS) \
+ $(SOUP_CFLAGS) \
+ $(PHODAV_CFLAGS) \
+ $(LZ4_CFLAGS) \
+ $(NULL)
+
+AM_CPPFLAGS = \
+ $(SPICE_COMMON_CPPFLAGS) \
+ $(SPICE_CFLAGS) \
+ $(NULL)
+
+# http://www.gnu.org/software/libtool/manual/html_node/Updating-version-info.html
+SPICE_GTK_LDFLAGS_COMMON = \
+ -version-info 4:0:0 \
+ -no-undefined \
+ $(GTK_SYMBOLS_LDFLAGS) \
+ $(NULL)
+
+SPICE_GTK_LIBADD_COMMON = \
+ libspice-client-glib-2.0.la \
+ $(GTK_LIBS) \
+ $(CAIRO_LIBS) \
+ $(XRANDR_LIBS) \
+ $(LIBM) \
+ $(NULL)
+
+SPICE_GTK_SOURCES_COMMON = \
+ glib-compat.h \
+ gtk-compat.h \
+ spice-util.c \
+ spice-util-priv.h \
+ spice-gtk-session.c \
+ spice-gtk-session-priv.h \
+ spice-widget.c \
+ spice-widget-priv.h \
+ vncdisplaykeymap.c \
+ vncdisplaykeymap.h \
+ spice-grabsequence.c \
+ spice-grabsequence.h \
+ desktop-integration.c \
+ desktop-integration.h \
+ usb-device-widget.c \
+ $(NULL)
+
+nodist_SPICE_GTK_SOURCES_COMMON = \
+ spice-widget-enums.c \
+ spice-marshal.c \
+ $(NULL)
+
+if WITH_X11
+SPICE_GTK_SOURCES_COMMON += \
+ spice-widget-x11.c \
+ $(NULL)
+else
+SPICE_GTK_SOURCES_COMMON += \
+ spice-widget-cairo.c \
+ $(NULL)
+endif
+
+if WITH_GTK
+if HAVE_GTK_2
+libspice_client_gtk_2_0_la_DEPEDENCIES = $(GTK_SYMBOLS_FILE)
+libspice_client_gtk_2_0_la_LDFLAGS = $(SPICE_GTK_LDFLAGS_COMMON)
+libspice_client_gtk_2_0_la_LIBADD = $(SPICE_GTK_LIBADD_COMMON)
+libspice_client_gtk_2_0_la_SOURCES = $(SPICE_GTK_SOURCES_COMMON)
+nodist_libspice_client_gtk_2_0_la_SOURCES = $(nodist_SPICE_GTK_SOURCES_COMMON)
+else
+libspice_client_gtk_3_0_la_DEPEDENCIES = $(GTK_SYMBOLS_FILE)
+libspice_client_gtk_3_0_la_LDFLAGS = $(SPICE_GTK_LDFLAGS_COMMON)
+libspice_client_gtk_3_0_la_LIBADD = $(SPICE_GTK_LIBADD_COMMON)
+libspice_client_gtk_3_0_la_SOURCES = $(SPICE_GTK_SOURCES_COMMON)
+nodist_libspice_client_gtk_3_0_la_SOURCES = $(nodist_SPICE_GTK_SOURCES_COMMON)
+endif
+
+libspice_client_gtkincludedir = $(includedir)/spice-client-gtk-$(SPICE_GTK_API_VERSION)
+libspice_client_gtkinclude_HEADERS = \
+ spice-gtk-session.h \
+ spice-widget.h \
+ spice-grabsequence.h \
+ usb-device-widget.h \
+ $(NULL)
+
+nodist_libspice_client_gtkinclude_HEADERS = \
+ spice-widget-enums.h \
+ $(NULL)
+endif
+
+libspice_client_glib_2_0_la_DEPENDENCIES = $(GLIB_SYMBOLS_FILE)
+
+libspice_client_glib_2_0_la_LDFLAGS = \
+ -version-info 13:0:5 \
+ -no-undefined \
+ $(GLIB_SYMBOLS_LDFLAGS) \
+ $(NULL)
+
+libspice_client_glib_2_0_la_LIBADD = \
+ $(top_builddir)/spice-common/common/libspice-common.la \
+ $(top_builddir)/spice-common/common/libspice-common-client.la \
+ $(GLIB2_LIBS) \
+ $(SOUP_LIBS) \
+ $(GIO_LIBS) \
+ $(GOBJECT2_LIBS) \
+ $(JPEG_LIBS) \
+ $(Z_LIBS) \
+ $(LZ4_LIBS) \
+ $(PIXMAN_LIBS) \
+ $(SSL_LIBS) \
+ $(PULSE_LIBS) \
+ $(GST_LIBS) \
+ $(SASL_LIBS) \
+ $(SMARTCARD_LIBS) \
+ $(USBREDIR_LIBS) \
+ $(GUDEV_LIBS) \
+ $(PHODAV_LIBS) \
+ $(NULL)
+
+if WITH_POLKIT
+USB_ACL_HELPER_SRCS = \
+ usb-acl-helper.c \
+ usb-acl-helper.h \
+ $(NULL)
+AM_CPPFLAGS += -DACL_HELPER_PATH="\"$(ACL_HELPER_DIR)\""
+else
+USB_ACL_HELPER_SRCS =
+endif
+
+libspice_client_glib_2_0_la_SOURCES = \
+ bio-gio.c \
+ bio-gio.h \
+ glib-compat.c \
+ glib-compat.h \
+ spice-audio.c \
+ spice-audio-priv.h \
+ spice-common.h \
+ spice-util.c \
+ spice-util-priv.h \
+ spice-option.h \
+ spice-option.c \
+ \
+ spice-client.c \
+ spice-session.c \
+ spice-session-priv.h \
+ spice-channel.c \
+ spice-channel-cache.h \
+ spice-channel-priv.h \
+ coroutine.h \
+ gio-coroutine.c \
+ gio-coroutine.h \
+ \
+ channel-base.c \
+ channel-webdav.c \
+ channel-cursor.c \
+ channel-display.c \
+ channel-display-priv.h \
+ channel-display-mjpeg.c \
+ channel-inputs.c \
+ channel-main.c \
+ channel-playback.c \
+ channel-playback-priv.h \
+ channel-port.c \
+ channel-record.c \
+ channel-smartcard.c \
+ channel-usbredir.c \
+ channel-usbredir-priv.h \
+ smartcard-manager.c \
+ smartcard-manager-priv.h \
+ spice-uri.c \
+ spice-uri-priv.h \
+ usb-device-manager.c \
+ usb-device-manager-priv.h \
+ usbutil.c \
+ usbutil.h \
+ $(USB_ACL_HELPER_SRCS) \
+ vmcstream.c \
+ vmcstream.h \
+ wocky-http-proxy.c \
+ wocky-http-proxy.h \
+ \
+ decode.h \
+ decode-glz.c \
+ decode-jpeg.c \
+ decode-zlib.c \
+ \
+ client_sw_canvas.c \
+ client_sw_canvas.h \
+ $(NULL)
+
+nodist_libspice_client_glib_2_0_la_SOURCES = \
+ spice-glib-enums.c \
+ spice-marshal.c \
+ spice-marshal.h \
+ $(NULL)
+
+libspice_client_glibincludedir = $(includedir)/spice-client-glib-2.0
+libspice_client_glibinclude_HEADERS = \
+ spice-audio.h \
+ spice-client.h \
+ spice-uri.h \
+ spice-types.h \
+ spice-session.h \
+ spice-channel.h \
+ spice-util.h \
+ spice-option.h \
+ spice-version.h \
+ channel-cursor.h \
+ channel-display.h \
+ channel-inputs.h \
+ channel-main.h \
+ channel-playback.h \
+ channel-port.h \
+ channel-record.h \
+ channel-smartcard.h \
+ channel-usbredir.h \
+ channel-webdav.h \
+ usb-device-manager.h \
+ smartcard-manager.h \
+ $(NULL)
+
+nodist_libspice_client_glibinclude_HEADERS = \
+ spice-glib-enums.h \
+ $(NULL)
+
+# file for API compatibility, but we don't want warning during our compilation
+dist_libspice_client_glibinclude_DATA = \
+ spice-channel-enums.h \
+ $(NULL)
+
+if WITH_PULSE
+libspice_client_glib_2_0_la_SOURCES += \
+ spice-pulse.c \
+ spice-pulse.h \
+ $(NULL)
+endif
+
+if WITH_GSTAUDIO
+libspice_client_glib_2_0_la_SOURCES += \
+ spice-gstaudio.c \
+ spice-gstaudio.h \
+ $(NULL)
+endif
+
+if WITH_PHODAV
+libspice_client_glib_2_0_la_SOURCES += \
+ giopipe.c \
+ giopipe.h \
+ $(NULL)
+endif
+
+if WITH_UCONTEXT
+libspice_client_glib_2_0_la_SOURCES += continuation.h continuation.c coroutine_ucontext.c
+endif
+
+if WITH_WINFIBER
+libspice_client_glib_2_0_la_SOURCES += coroutine_winfibers.c
+endif
+
+if WITH_GTHREAD
+libspice_client_glib_2_0_la_SOURCES += coroutine_gthread.c
+libspice_client_glib_2_0_la_LIBADD += $(GTHREAD_LIBS)
+endif
+
+
+WIN_USB_FILES= \
+ win-usb-dev.h \
+ win-usb-dev.c \
+ win-usb-clerk.h \
+ win-usb-driver-install.h \
+ win-usb-driver-install.c \
+ $(NULL)
+
+if OS_WIN32
+if WITH_USBREDIR
+libspice_client_glib_2_0_la_SOURCES += \
+ $(WIN_USB_FILES)
+endif
+libspice_client_glib_2_0_la_LIBADD += -lws2_32 -lgdi32
+endif
+
+spicy_SOURCES = \
+ spicy.c \
+ spice-cmdline.h \
+ spice-cmdline.c \
+ $(NULL)
+
+spicy_LDADD = \
+ libspice-client-gtk-$(SPICE_GTK_API_VERSION).la \
+ libspice-client-glib-2.0.la \
+ $(XRANDR_LIBS) \
+ $(GTHREAD_LIBS) \
+ $(GTK_LIBS) \
+ $(LIBM) \
+ $(NULL)
+
+spicy_CPPFLAGS = \
+ $(AM_CPPFLAGS) \
+ $(XRANDR_CFLAGS) \
+ $(GTHREAD_CFLAGS) \
+ -DSPICE_DISABLE_DEPRECATED \
+ $(NULL)
+
+
+if WITH_POLKIT
+spice_client_glib_usb_acl_helper_SOURCES = \
+ glib-compat.c \
+ glib-compat.h \
+ spice-client-glib-usb-acl-helper.c \
+ $(NULL)
+
+spice_client_glib_usb_acl_helper_LDADD = \
+ $(GLIB2_LIBS) \
+ $(GIO_LIBS) \
+ $(POLKIT_LIBS) \
+ $(ACL_LIBS) \
+ $(PIE_LDFLAGS) \
+ $(NULL)
+
+spice_client_glib_usb_acl_helper_CPPFLAGS = \
+ $(SPICE_CFLAGS) \
+ $(GLIB2_CFLAGS) \
+ $(GIO_CFLAGS) \
+ $(POLKIT_CFLAGS) \
+ $(PIE_CFLAGS) \
+ $(NULL)
+
+install-data-hook:
+ -chown root $(DESTDIR)$(acldir)/spice-client-glib-usb-acl-helper
+ -chmod u+s $(DESTDIR)$(acldir)/spice-client-glib-usb-acl-helper
+
+endif
+
+
+spicy_screenshot_SOURCES = \
+ spicy-screenshot.c \
+ spice-cmdline.h \
+ spice-cmdline.c \
+ $(NULL)
+
+spicy_screenshot_LDADD = \
+ libspice-client-glib-2.0.la \
+ $(GOBJECT2_LIBS) \
+ $(NULL)
+
+spicy_stats_SOURCES = \
+ spicy-stats.c \
+ spice-cmdline.h \
+ spice-cmdline.c \
+ $(NULL)
+
+spicy_stats_LDADD = \
+ libspice-client-glib-2.0.la \
+ $(GOBJECT2_LIBS) \
+ $(NULL)
+
+
+
+$(libspice_client_glib_2_0_la_SOURCES): spice-glib-enums.h spice-marshal.h
+
+if WITH_GTK
+if HAVE_GTK_2
+$(libspice_client_gtk_2_0_la_SOURCES): spice-glib-enums.h spice-widget-enums.h
+else
+$(libspice_client_gtk_3_0_la_SOURCES): spice-glib-enums.h spice-widget-enums.h
+endif
+endif
+
+spice-marshal.c: spice-marshal.h
+spice-glib-enums.c: spice-glib-enums.h
+spice-widget-enums.c: spice-widget-enums.h
+
+spice-marshal.c: spice-marshal.txt
+ $(AM_V_GEN)echo "#include \"config.h\"" > $@ && \
+ echo "#include \"spice-marshal.h\"" > $@ && \
+ glib-genmarshal --body $< >> $@ || (rm -f $@ && exit 1)
+
+spice-marshal.h: spice-marshal.txt
+ $(AM_V_GEN)glib-genmarshal --header $< > $@ || (rm -f $@ && exit 1)
+
+spice-glib-enums.c: spice-channel.h channel-inputs.h spice-session.h
+ $(AM_V_GEN)glib-mkenums --fhead "#include \"config.h\"\n\n" \
+ --fhead "#include <glib-object.h>\n" \
+ --fhead "#include \"spice-glib-enums.h\"\n\n" \
+ --fprod "\n#include \"spice-session.h\"\n" \
+ --fprod "\n#include \"spice-channel.h\"\n" \
+ --fprod "\n#include \"channel-inputs.h\"\n" \
+ --vhead "static const G@Type@Value _@enum_name@_values[] = {" \
+ --vprod " { @VALUENAME@, \"@VALUENAME@\", \"@valuenick@\" }," \
+ --vtail " { 0, NULL, NULL }\n};\n\n" \
+ --vtail "GType\n@enum_name@_get_type (void)\n{\n" \
+ --vtail " static GType type = 0;\n" \
+ --vtail " static volatile gsize type_volatile = 0;\n\n" \
+ --vtail " if (g_once_init_enter(&type_volatile)) {\n" \
+ --vtail " type = g_@type@_register_static (\"@EnumName@\", _@enum_name@_values);\n" \
+ --vtail " g_once_init_leave(&type_volatile, type);\n" \
+ --vtail " }\n\n" \
+ --vtail " return type;\n}\n\n" \
+ $^ > $@
+
+spice-glib-enums.h: spice-channel.h channel-inputs.h spice-session.h
+ $(AM_V_GEN)glib-mkenums --fhead "#ifndef SPICE_GLIB_ENUMS_H\n" \
+ --fhead "#define SPICE_GLIB_ENUMS_H\n\n" \
+ --fhead "G_BEGIN_DECLS\n\n" \
+ --ftail "G_END_DECLS\n\n" \
+ --ftail "#endif /* SPICE_CHANNEL_ENUMS_H */\n" \
+ --eprod "#define SPICE_TYPE_@ENUMSHORT@ @enum_name@_get_type()\n" \
+ --eprod "GType @enum_name@_get_type (void);\n" \
+ $^ > $@
+
+spice-widget-enums.c: spice-widget.h
+ $(AM_V_GEN)glib-mkenums --fhead "#include \"config.h\"\n\n" \
+ --fhead "#include <glib-object.h>\n" \
+ --fhead "#include \"spice-widget-enums.h\"\n\n" \
+ --fprod "\n#include \"spice-widget.h\"\n" \
+ --vhead "static const G@Type@Value _@enum_name@_values[] = {" \
+ --vprod " { @VALUENAME@, \"@VALUENAME@\", \"@valuenick@\" }," \
+ --vtail " { 0, NULL, NULL }\n};\n\n" \
+ --vtail "GType\n@enum_name@_get_type (void)\n{\n" \
+ --vtail " static GType type = 0;\n" \
+ --vtail " static volatile gsize type_volatile = 0;\n\n" \
+ --vtail " if (g_once_init_enter(&type_volatile)) {\n" \
+ --vtail " type = g_@type@_register_static (\"@EnumName@\", _@enum_name@_values);\n" \
+ --vtail " g_once_init_leave(&type_volatile, type);\n" \
+ --vtail " }\n\n" \
+ --vtail " return type;\n}\n\n" \
+ $< > $@
+
+spice-widget-enums.h: spice-widget.h
+ $(AM_V_GEN)glib-mkenums --fhead "#ifndef SPICE_WIDGET_ENUMS_H\n" \
+ --fhead "#define SPICE_WIDGET_ENUMS_H\n\n" \
+ --fhead "G_BEGIN_DECLS\n\n" \
+ --ftail "G_END_DECLS\n\n" \
+ --ftail "#endif /* SPICE_WIDGET_ENUMS_H */\n" \
+ --eprod "#define SPICE_TYPE_@ENUMSHORT@ @enum_name@_get_type()\n" \
+ --eprod "GType @enum_name@_get_type (void);\n" \
+ $< > $@
+
+
+vncdisplaykeymap.c: $(KEYMAPS)
+
+$(KEYMAPS): $(KEYMAP_GEN) keymaps.csv
+
+# Note despite being autogenerated these are not part of CLEANFILES, they
+# are actually a part of EXTRA_DIST to avoid the need for perl(Text::CSV) by
+# end users
+vncdisplaykeymap_xorgevdev2xtkbd.c:
+ $(AM_V_GEN)$(KEYMAP_GEN) $(srcdir)/keymaps.csv xorgevdev xtkbd > $@ || rm $@
+
+vncdisplaykeymap_xorgkbd2xtkbd.c:
+ $(AM_V_GEN)$(KEYMAP_GEN) $(srcdir)/keymaps.csv xorgkbd xtkbd > $@ || rm $@
+
+vncdisplaykeymap_xorgxquartz2xtkbd.c:
+ $(AM_V_GEN)$(KEYMAP_GEN) $(srcdir)/keymaps.csv xorgxquartz xtkbd > $@ || rm $@
+
+vncdisplaykeymap_xorgxwin2xtkbd.c:
+ $(AM_V_GEN)$(KEYMAP_GEN) $(srcdir)/keymaps.csv xorgxwin xtkbd > $@ || rm $@
+
+vncdisplaykeymap_osx2xtkbd.c:
+ $(AM_V_GEN)$(KEYMAP_GEN) $(srcdir)/keymaps.csv osx xtkbd > $@ || rm $@
+
+vncdisplaykeymap_win322xtkbd.c:
+ $(AM_V_GEN)$(KEYMAP_GEN) $(srcdir)/keymaps.csv win32 xtkbd > $@ || rm $@
+
+vncdisplaykeymap_x112xtkbd.c:
+ $(AM_V_GEN)$(KEYMAP_GEN) $(srcdir)/keymaps.csv x11 xtkbd > $@ || rm $@
+
+if WITH_PYTHON
+pyexec_LTLIBRARIES = SpiceClientGtk.la
+
+# workaround for broken parallel install support in automake with LTLIBRARIES
+# http://debbugs.gnu.org/cgi/bugreport.cgi?bug=7328
+install_pyexecLTLIBRARIES = install-pyexecLTLIBRARIES
+$(install_pyexecLTLIBRARIES): install-libLTLIBRARIES
+
+SpiceClientGtk_la_LIBADD = libspice-client-gtk-2.0.la libspice-client-glib-2.0.la $(PYGTK_LIBS)
+SpiceClientGtk_la_CFLAGS = $(GTK_CFLAGS) $(PYTHON_INCLUDES) $(PYGTK_CFLAGS) $(WARN_PYFLAGS)
+SpiceClientGtk_la_LDFLAGS = -module -avoid-version -fPIC
+SpiceClientGtk_la_SOURCES = spice-client-gtk-module.c
+nodist_SpiceClientGtk_la_SOURCES = spice-client-gtk-module.defs.c
+
+CODEGENDIR = `pkg-config --variable=codegendir pygtk-2.0`
+DEFSDIR = `pkg-config --variable=defsdir pygtk-2.0`
+
+spice-client-gtk.defs: $(libspice_client_gtkinclude_HEADERS) $(nodist_libspice_client_gtkinclude_HEADERS) $(libspice_client_glibinclude_HEADERS) $(nodist_libspice_client_glibinclude_HEADERS)
+ $(AM_V_GEN)$(PYTHON) $(CODEGENDIR)/h2def.py \
+ -f $(srcdir)/spice-client-gtk-manual.defs \
+ $^ > $@
+
+spice-client-gtk-module.defs.c: spice-client-gtk.override spice-client-gtk.defs spice-client-gtk-manual.defs
+ @cat spice-client-gtk.defs $(srcdir)/spice-client-gtk-manual.defs > tmp.defs
+ $(AM_V_GEN)pygobject-codegen-2.0 --prefix spice \
+ --register $(DEFSDIR)/gdk-types.defs \
+ --register $(DEFSDIR)/gtk-types.defs \
+ --override $(srcdir)/spice-client-gtk.override \
+ tmp.defs > $@
+ @rm tmp.defs
+
+CLEANFILES += spice-client-gtk-module.defs.c spice-client-gtk.defs
+endif
+
+-include $(INTROSPECTION_MAKEFILE)
+
+if G_IR_SCANNER_SYMBOL_PREFIX
+PREFIX_ARGS = --symbol-prefix=spice --identifier-prefix=Spice
+else
+PREFIX_ARGS = --strip-prefix=Spice
+endif
+
+INTROSPECTION_GIRS =
+INTROSPECTION_SCANNER_ARGS = --warn-all --accept-unprefixed --add-include-path=$(builddir) $(PREFIX_ARGS)
+INTROSPECTION_COMPILER_ARGS = --includedir=$(builddir)
+
+if HAVE_INTROSPECTION
+glib_introspection_files = \
+ $(libspice_client_glibinclude_HEADERS) \
+ $(nodist_libspice_client_glibinclude_HEADERS) \
+ spice-audio.c \
+ spice-client.c \
+ spice-session.c \
+ spice-channel.c \
+ spice-glib-enums.c \
+ spice-option.c \
+ spice-util.c \
+ channel-webdav.c \
+ channel-cursor.c \
+ channel-display.c \
+ channel-inputs.c \
+ channel-main.c \
+ channel-playback.c \
+ channel-port.c \
+ channel-record.c \
+ channel-smartcard.c \
+ channel-usbredir.c \
+ smartcard-manager.c \
+ usb-device-manager.c \
+ $(NULL)
+
+gtk_introspection_files = \
+ $(libspice_client_gtkinclude_HEADERS) \
+ $(nodist_libspice_client_gtkinclude_HEADERS) \
+ spice-gtk-session.c \
+ spice-widget.c \
+ spice-grabsequence.c \
+ usb-device-widget.c \
+ $(NULL)
+
+SpiceClientGLib-2.0.gir: libspice-client-glib-2.0.la
+SpiceClientGLib_2_0_gir_INCLUDES = GObject-2.0 Gio-2.0
+SpiceClientGLib_2_0_gir_CFLAGS = $(SPICE_COMMON_CPPFLAGS)
+SpiceClientGLib_2_0_gir_LIBS = libspice-client-glib-2.0.la
+SpiceClientGLib_2_0_gir_FILES = $(glib_introspection_files)
+SpiceClientGLib_2_0_gir_EXPORT_PACKAGES = spice-client-glib-2.0
+SpiceClientGLib_2_0_gir_SCANNERFLAGS = --c-include="spice-client.h"
+INTROSPECTION_GIRS += SpiceClientGLib-2.0.gir
+
+if WITH_GTK
+if HAVE_GTK_2
+SpiceClientGtk-2.0.gir: libspice-client-gtk-2.0.la SpiceClientGLib-2.0.gir
+SpiceClientGtk_2_0_gir_INCLUDES = GObject-2.0 Gtk-2.0 SpiceClientGLib-2.0
+SpiceClientGtk_2_0_gir_CFLAGS = $(SPICE_COMMON_CPPFLAGS)
+SpiceClientGtk_2_0_gir_LIBS = libspice-client-gtk-2.0.la libspice-client-glib-2.0.la
+SpiceClientGtk_2_0_gir_FILES = $(gtk_introspection_files)
+SpiceClientGtk_2_0_gir_EXPORT_PACKAGES = spice-client-gtk-2.0
+SpiceClientGtk_2_0_gir_SCANNERFLAGS = --c-include="spice-widget.h"
+else
+SpiceClientGtk-3.0.gir: libspice-client-gtk-3.0.la SpiceClientGLib-2.0.gir
+SpiceClientGtk_3_0_gir_INCLUDES = GObject-2.0 Gtk-3.0 SpiceClientGLib-2.0
+SpiceClientGtk_3_0_gir_CFLAGS = $(SPICE_COMMON_CPPFLAGS)
+SpiceClientGtk_3_0_gir_LIBS = libspice-client-gtk-3.0.la libspice-client-glib-2.0.la
+SpiceClientGtk_3_0_gir_FILES = $(gtk_introspection_files)
+SpiceClientGtk_3_0_gir_EXPORT_PACKAGES = spice-client-gtk-3.0
+SpiceClientGtk_3_0_gir_SCANNERFLAGS = --c-include="spice-widget.h"
+endif
+INTROSPECTION_GIRS += SpiceClientGtk-$(SPICE_GTK_API_VERSION).gir
+endif
+
+girdir = $(datadir)/gir-1.0
+gir_DATA = $(INTROSPECTION_GIRS)
+
+typelibsdir = $(libdir)/girepository-1.0
+typelibs_DATA = $(INTROSPECTION_GIRS:.gir=.typelib)
+
+CLEANFILES += $(gir_DATA) $(typelibs_DATA)
+endif
+
+update-map-file: $(libspice_client_gtkinclude_HEADERS) $(nodist_libspice_client_gtkinclude_HEADERS) $(libspice_client_glibinclude_HEADERS) $(nodist_libspice_client_glibinclude_HEADERS)
+ ( echo "SPICEGTK_1 {" ; \
+ echo "global:" ; \
+ ctags -f - -I G_GNUC_CONST --c-kinds=p $^ | awk '/^spice_/ { print $$1 ";" }' | sort ; \
+ echo "local:" ; \
+ echo "*;" ; \
+ echo "};" ) > $(srcdir)/map-file
+
+update-glib-sym-file: $(libspice_client_glibinclude_HEADERS) $(nodist_libspice_client_glibinclude_HEADERS)
+ ( ctags -f - -I G_GNUC_CONST --c-kinds=p $^ | awk '/^spice_/ { print $$1 }' | sort ; \
+ ) > $(srcdir)/spice-glib-sym-file
+
+update-gtk-sym-file: $(libspice_client_gtkinclude_HEADERS) $(nodist_libspice_client_gtkinclude_HEADERS)
+ ( ctags -f - -I G_GNUC_CONST --c-kinds=p $^ | awk '/^spice_/ { print $$1 }' | sort ; \
+ ) > $(srcdir)/spice-gtk-sym-file
+
+update-symbol-files: update-map-file update-glib-sym-file update-gtk-sym-file
+
+-include $(top_srcdir)/git.mk
diff --git a/src/bio-gio.c b/src/bio-gio.c
new file mode 100644
index 0000000..108ac1a
--- /dev/null
+++ b/src/bio-gio.c
@@ -0,0 +1,114 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 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 <string.h>
+#include <glib.h>
+
+#include "spice-util.h"
+#include "bio-gio.h"
+
+typedef struct bio_gsocket_method {
+ BIO_METHOD method;
+ GIOStream *stream;
+} bio_gsocket_method;
+
+#define BIO_GET_GSOCKET(bio) (((bio_gsocket_method*)bio->method)->gsocket)
+#define BIO_GET_ISTREAM(bio) (g_io_stream_get_input_stream(((bio_gsocket_method*)bio->method)->stream))
+#define BIO_GET_OSTREAM(bio) (g_io_stream_get_output_stream(((bio_gsocket_method*)bio->method)->stream))
+
+static int bio_gio_write(BIO *bio, const char *in, int inl)
+{
+ gssize ret;
+ GError *error = NULL;
+
+ ret = g_pollable_output_stream_write_nonblocking(G_POLLABLE_OUTPUT_STREAM(BIO_GET_OSTREAM(bio)),
+ in, inl, NULL, &error);
+ BIO_clear_retry_flags(bio);
+
+ if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
+ BIO_set_retry_write(bio);
+ if (error != NULL) {
+ g_warning("%s", error->message);
+ g_clear_error(&error);
+ }
+
+ return ret;
+}
+
+static int bio_gio_read(BIO *bio, char *out, int outl)
+{
+ gssize ret;
+ GError *error = NULL;
+
+ ret = g_pollable_input_stream_read_nonblocking(G_POLLABLE_INPUT_STREAM(BIO_GET_ISTREAM(bio)),
+ out, outl, NULL, &error);
+ BIO_clear_retry_flags(bio);
+
+ if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK))
+ BIO_set_retry_read(bio);
+ else if (error != NULL)
+ g_warning("%s", error->message);
+
+ g_clear_error(&error);
+
+ return ret;
+}
+
+static int bio_gio_destroy(BIO *bio)
+{
+ if (bio == NULL || bio->method == NULL)
+ return 0;
+
+ SPICE_DEBUG("bio gsocket destroy");
+ g_free(bio->method);
+ bio->method = NULL;;
+
+ return 1;
+}
+
+static int bio_gio_puts(BIO *bio, const char *str)
+{
+ int n, ret;
+
+ n = strlen(str);
+ ret = bio_gio_write(bio, str, n);
+
+ return ret;
+}
+
+G_GNUC_INTERNAL
+BIO* bio_new_giostream(GIOStream *stream)
+{
+ // TODO: make an actual new BIO type, or just switch to GTls already...
+ BIO *bio = BIO_new_socket(-1, BIO_NOCLOSE);
+
+ bio_gsocket_method *bio_method = g_new(bio_gsocket_method, 1);
+ bio_method->method = *bio->method;
+ bio_method->stream = stream;
+
+ bio->method->destroy(bio);
+ bio->method = (BIO_METHOD*)bio_method;
+
+ bio->method->bwrite = bio_gio_write;
+ bio->method->bread = bio_gio_read;
+ bio->method->bputs = bio_gio_puts;
+ bio->method->destroy = bio_gio_destroy;
+
+ return bio;
+}
diff --git a/src/bio-gio.h b/src/bio-gio.h
new file mode 100644
index 0000000..31fd369
--- /dev/null
+++ b/src/bio-gio.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 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 BIO_GIO_H_
+# define BIO_GIO_H_
+
+#include <openssl/bio.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+BIO* bio_new_giostream(GIOStream *stream);
+
+G_END_DECLS
+
+#endif /* !BIO_GIO_H_ */
diff --git a/src/channel-base.c b/src/channel-base.c
new file mode 100644
index 0000000..77d339c
--- /dev/null
+++ b/src/channel-base.c
@@ -0,0 +1,284 @@
+/* -*- 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-session-priv.h"
+#include "spice-channel-priv.h"
+
+/* coroutine context */
+G_GNUC_INTERNAL
+void spice_channel_handle_set_ack(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ SpiceMsgSetAck* ack = spice_msg_in_parsed(in);
+ SpiceMsgOut *out = spice_msg_out_new(channel, SPICE_MSGC_ACK_SYNC);
+ SpiceMsgcAckSync sync = {
+ .generation = ack->generation,
+ };
+
+ c->message_ack_window = c->message_ack_count = ack->window;
+ c->marshallers->msgc_ack_sync(out->marshaller, &sync);
+ spice_msg_out_send_internal(out);
+}
+
+/* coroutine context */
+G_GNUC_INTERNAL
+void spice_channel_handle_ping(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ SpiceMsgPing *ping = spice_msg_in_parsed(in);
+ SpiceMsgOut *pong = spice_msg_out_new(channel, SPICE_MSGC_PONG);
+
+ c->marshallers->msgc_pong(pong->marshaller, ping);
+ spice_msg_out_send_internal(pong);
+}
+
+/* coroutine context */
+G_GNUC_INTERNAL
+void spice_channel_handle_notify(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ static const char* severity_strings[] = {"info", "warn", "error"};
+ static const char* visibility_strings[] = {"!", "!!", "!!!"};
+
+ SpiceMsgNotify *notify = spice_msg_in_parsed(in);
+ const char *severity = "?";
+ const char *visibility = "?";
+ const char *message_str = NULL;
+
+ if (notify->severity <= SPICE_NOTIFY_SEVERITY_ERROR) {
+ severity = severity_strings[notify->severity];
+ }
+ if (notify->visibilty <= SPICE_NOTIFY_VISIBILITY_HIGH) {
+ visibility = visibility_strings[notify->visibilty];
+ }
+
+ if (notify->message_len &&
+ notify->message_len <= in->dpos - sizeof(*notify)) {
+ message_str = (char*)notify->message;
+ }
+
+ CHANNEL_DEBUG(channel, "%s -- %s%s #%u%s%.*s", __FUNCTION__,
+ severity, visibility, notify->what,
+ message_str ? ": " : "", notify->message_len,
+ message_str ? message_str : "");
+}
+
+/* coroutine context */
+G_GNUC_INTERNAL
+void spice_channel_handle_disconnect(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgDisconnect *disconnect = spice_msg_in_parsed(in);
+
+ CHANNEL_DEBUG(channel, "%s: ts: %" PRIu64", reason: %u", __FUNCTION__,
+ disconnect->time_stamp, disconnect->reason);
+}
+
+typedef struct WaitForChannelData
+{
+ SpiceWaitForChannel *wait;
+ SpiceChannel *channel;
+} WaitForChannelData;
+
+/* coroutine and main context */
+static gboolean wait_for_channel(gpointer data)
+{
+ WaitForChannelData *wfc = data;
+ SpiceChannelPrivate *c = wfc->channel->priv;
+ SpiceChannel *wait_channel;
+
+ wait_channel = spice_session_lookup_channel(c->session, wfc->wait->channel_id, wfc->wait->channel_type);
+ g_return_val_if_fail(wait_channel != NULL, TRUE);
+
+ if (wait_channel->priv->last_message_serial >= wfc->wait->message_serial)
+ return TRUE;
+
+ return FALSE;
+}
+
+/* coroutine context */
+G_GNUC_INTERNAL
+void spice_channel_handle_wait_for_channels(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ SpiceMsgWaitForChannels *wfc = spice_msg_in_parsed(in);
+ int i;
+
+ for (i = 0; i < wfc->wait_count; ++i) {
+ WaitForChannelData data = {
+ .wait = wfc->wait_list + i,
+ .channel = channel
+ };
+
+ CHANNEL_DEBUG(channel, "waiting for serial %" PRIu64 " (%d/%d)", data.wait->message_serial, i + 1, wfc->wait_count);
+ if (g_coroutine_condition_wait(&c->coroutine, wait_for_channel, &data))
+ CHANNEL_DEBUG(channel, "waiting for serial %" PRIu64 ", done", data.wait->message_serial);
+ else
+ CHANNEL_DEBUG(channel, "waiting for serial %" PRIu64 ", cancelled", data.wait->message_serial);
+ }
+}
+
+static void
+get_msg_handler(SpiceChannel *channel, SpiceMsgIn *in, gpointer data)
+{
+ SpiceMsgIn **msg = data;
+
+ g_return_if_fail(msg != NULL);
+ g_return_if_fail(*msg == NULL);
+
+ spice_msg_in_ref(in);
+ *msg = in;
+}
+
+/* coroutine context */
+G_GNUC_INTERNAL
+void spice_channel_handle_migrate(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgOut *out;
+ SpiceMsgIn *data = NULL;
+ SpiceMsgMigrate *mig = spice_msg_in_parsed(in);
+ SpiceChannelPrivate *c = channel->priv;
+
+ CHANNEL_DEBUG(channel, "%s: flags %u", __FUNCTION__, mig->flags);
+ if (mig->flags & SPICE_MIGRATE_NEED_FLUSH) {
+ /* if peer version > 1: pushing the mark msg before all other messgages and sending it,
+ * and only it */
+ if (c->peer_hdr.major_version == 1) {
+ /* iterate_write is blocking and flushing all pending write */
+ SPICE_CHANNEL_GET_CLASS(channel)->iterate_write(channel);
+ }
+ out = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_MIGRATE_FLUSH_MARK);
+ spice_msg_out_send_internal(out);
+ }
+ if (mig->flags & SPICE_MIGRATE_NEED_DATA_TRANSFER) {
+ spice_channel_recv_msg(channel, get_msg_handler, &data);
+ if (!data) {
+ g_critical("expected SPICE_MSG_MIGRATE_DATA, got empty message");
+ goto end;
+ } else if (spice_header_get_msg_type(data->header, c->use_mini_header) !=
+ SPICE_MSG_MIGRATE_DATA) {
+ g_critical("expected SPICE_MSG_MIGRATE_DATA, got %d",
+ spice_header_get_msg_type(data->header, c->use_mini_header));
+ goto end;
+ }
+ }
+
+ /* swapping channels sockets */
+ spice_session_channel_migrate(c->session, channel);
+
+ /* pushing the MIGRATE_DATA before all other pending messages */
+ if ((mig->flags & SPICE_MIGRATE_NEED_DATA_TRANSFER) && (data != NULL)) {
+ out = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_MIGRATE_DATA);
+ spice_marshaller_add(out->marshaller, data->data,
+ spice_header_get_msg_size(data->header, c->use_mini_header));
+ spice_msg_out_send_internal(out);
+ }
+
+end:
+ if (data)
+ spice_msg_in_unref(data);
+}
+
+
+static void set_handlers(SpiceChannelClass *klass,
+ const spice_msg_handler* handlers, const int n)
+{
+ int i;
+
+ g_array_set_size(klass->handlers, MAX(klass->handlers->len, n));
+ for (i = 0; i < n; i++) {
+ if (handlers[i])
+ g_array_index(klass->handlers, spice_msg_handler, i) = handlers[i];
+ }
+}
+
+static void spice_channel_add_base_handlers(SpiceChannelClass *klass)
+{
+ static const spice_msg_handler handlers[] = {
+ [ SPICE_MSG_SET_ACK ] = spice_channel_handle_set_ack,
+ [ SPICE_MSG_PING ] = spice_channel_handle_ping,
+ [ SPICE_MSG_NOTIFY ] = spice_channel_handle_notify,
+ [ SPICE_MSG_DISCONNECTING ] = spice_channel_handle_disconnect,
+ [ SPICE_MSG_WAIT_FOR_CHANNELS ] = spice_channel_handle_wait_for_channels,
+ [ SPICE_MSG_MIGRATE ] = spice_channel_handle_migrate,
+ };
+
+ set_handlers(klass, handlers, G_N_ELEMENTS(handlers));
+}
+
+G_GNUC_INTERNAL
+void spice_channel_set_handlers(SpiceChannelClass *klass,
+ const spice_msg_handler* handlers, const int n)
+{
+ /* FIXME: use class private (requires glib 2.24) */
+ g_return_if_fail(klass->handlers == NULL);
+ klass->handlers = g_array_sized_new(FALSE, TRUE, sizeof(spice_msg_handler), n);
+
+ spice_channel_add_base_handlers(klass);
+ set_handlers(klass, handlers, n);
+}
+
+static void
+vmc_write_free_cb(uint8_t *data, void *user_data)
+{
+ GSimpleAsyncResult *result = user_data;
+
+ g_simple_async_result_complete_in_idle(result);
+ g_object_unref(result);
+}
+
+G_GNUC_INTERNAL
+void spice_vmc_write_async(SpiceChannel *self,
+ const void *buffer, gsize count,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SpiceMsgOut *msg;
+ GSimpleAsyncResult *simple;
+
+ simple = g_simple_async_result_new(G_OBJECT(self), callback, user_data,
+ spice_port_write_async);
+ g_simple_async_result_set_op_res_gssize(simple, count);
+
+ msg = spice_msg_out_new(SPICE_CHANNEL(self), SPICE_MSGC_SPICEVMC_DATA);
+ spice_marshaller_add_ref_full(msg->marshaller, (uint8_t*)buffer, count,
+ vmc_write_free_cb, simple);
+ spice_msg_out_send(msg);
+}
+
+G_GNUC_INTERNAL
+gssize spice_vmc_write_finish(SpiceChannel *self,
+ GAsyncResult *result, GError **error)
+{
+ GSimpleAsyncResult *simple;
+
+ g_return_val_if_fail(result != NULL, -1);
+
+ simple = (GSimpleAsyncResult *)result;
+
+ if (g_simple_async_result_propagate_error(simple, error))
+ return -1;
+
+ g_return_val_if_fail(g_simple_async_result_is_valid(result, G_OBJECT(self),
+ spice_port_write_async), -1);
+
+ return g_simple_async_result_get_op_res_gssize(simple);
+}
diff --git a/src/channel-cursor.c b/src/channel-cursor.c
new file mode 100644
index 0000000..e6514a2
--- /dev/null
+++ b/src/channel-cursor.c
@@ -0,0 +1,529 @@
+/* -*- 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-compat.h"
+#include "spice-client.h"
+#include "spice-common.h"
+
+#include "spice-channel-priv.h"
+#include "spice-channel-cache.h"
+#include "spice-marshal.h"
+
+/**
+ * SECTION:channel-cursor
+ * @short_description: update cursor shape and position
+ * @title: Cursor Channel
+ * @section_id:
+ * @see_also: #SpiceChannel, and the GTK widget #SpiceDisplay
+ * @stability: Stable
+ * @include: channel-cursor.h
+ *
+ * The Spice protocol defines a set of messages for controlling cursor
+ * shape and position on the remote display area. The cursor changes
+ * that should be reflected on the display are notified by
+ * signals. See for example #SpiceCursorChannel::cursor-set
+ * #SpiceCursorChannel::cursor-move signals.
+ */
+
+#define SPICE_CURSOR_CHANNEL_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_CURSOR_CHANNEL, SpiceCursorChannelPrivate))
+
+typedef struct display_cursor display_cursor;
+
+struct display_cursor {
+ SpiceCursorHeader hdr;
+ gboolean default_cursor;
+ int refcount;
+ guint32 data[];
+};
+
+struct _SpiceCursorChannelPrivate {
+ display_cache *cursors;
+ gboolean init_done;
+};
+
+enum {
+ SPICE_CURSOR_SET,
+ SPICE_CURSOR_MOVE,
+ SPICE_CURSOR_HIDE,
+ SPICE_CURSOR_RESET,
+
+ SPICE_CURSOR_LAST_SIGNAL,
+};
+
+static guint signals[SPICE_CURSOR_LAST_SIGNAL];
+
+static display_cursor * display_cursor_ref(display_cursor *cursor);
+static void display_cursor_unref(display_cursor *cursor);
+static void channel_set_handlers(SpiceChannelClass *klass);
+
+G_DEFINE_TYPE(SpiceCursorChannel, spice_cursor_channel, SPICE_TYPE_CHANNEL)
+
+/* ------------------------------------------------------------------ */
+
+static void spice_cursor_channel_init(SpiceCursorChannel *channel)
+{
+ SpiceCursorChannelPrivate *c;
+
+ c = channel->priv = SPICE_CURSOR_CHANNEL_GET_PRIVATE(channel);
+
+ c->cursors = cache_new((GDestroyNotify)display_cursor_unref);
+}
+
+static void spice_cursor_channel_finalize(GObject *obj)
+{
+ SpiceCursorChannel *channel = SPICE_CURSOR_CHANNEL(obj);
+ SpiceCursorChannelPrivate *c = channel->priv;
+
+ g_clear_pointer(&c->cursors, cache_unref);
+
+ if (G_OBJECT_CLASS(spice_cursor_channel_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_cursor_channel_parent_class)->finalize(obj);
+}
+
+/* coroutine context */
+static void spice_cursor_channel_reset(SpiceChannel *channel, gboolean migrating)
+{
+ SpiceCursorChannelPrivate *c = SPICE_CURSOR_CHANNEL(channel)->priv;
+
+ cache_clear(c->cursors);
+ c->init_done = FALSE;
+
+ SPICE_CHANNEL_CLASS(spice_cursor_channel_parent_class)->channel_reset(channel, migrating);
+}
+
+static void spice_cursor_channel_class_init(SpiceCursorChannelClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS(klass);
+
+ gobject_class->finalize = spice_cursor_channel_finalize;
+ channel_class->channel_reset = spice_cursor_channel_reset;
+
+ /**
+ * SpiceCursorChannel::cursor-set:
+ * @cursor: the #SpiceCursorChannel that emitted the signal
+ * @width: width of the shape
+ * @height: height of the shape
+ * @hot_x: horizontal offset of the 'hotspot' of the cursor
+ * @hot_y: vertical offset of the 'hotspot' of the cursor
+ * @rgba: 32bits shape data, or %NULL if default cursor. It might
+ * be freed after the signal is emitted, so make sure to copy it
+ * if you need it later!
+ *
+ * The #SpiceCursorChannel::cursor-set signal is emitted to modify
+ * cursor aspect and position on the display area.
+ **/
+ signals[SPICE_CURSOR_SET] =
+ g_signal_new("cursor-set",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceCursorChannelClass, cursor_set),
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__INT_INT_INT_INT_POINTER,
+ G_TYPE_NONE,
+ 5,
+ G_TYPE_INT, G_TYPE_INT,
+ G_TYPE_INT, G_TYPE_INT,
+ G_TYPE_POINTER);
+
+ /**
+ * SpiceCursorChannel::cursor-move:
+ * @cursor: the #SpiceCursorChannel that emitted the signal
+ * @x: x position
+ * @y: y position
+ *
+ * The #SpiceCursorChannel::cursor-move signal is emitted to update
+ * the cursor position on the display area.
+ **/
+ signals[SPICE_CURSOR_MOVE] =
+ g_signal_new("cursor-move",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceCursorChannelClass, cursor_move),
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__INT_INT,
+ G_TYPE_NONE,
+ 2,
+ G_TYPE_INT, G_TYPE_INT);
+
+ /**
+ * SpiceCursorChannel::cursor-hide:
+ * @cursor: the #SpiceCursorChannel that emitted the signal
+ *
+ * The #SpiceCursorChannel::cursor-hide signal is emitted to hide
+ * the cursor/pointer on the display area.
+ **/
+ signals[SPICE_CURSOR_HIDE] =
+ g_signal_new("cursor-hide",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceCursorChannelClass, cursor_hide),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
+
+ /**
+ * SpiceCursorChannel::cursor-reset:
+ * @cursor: the #SpiceCursorChannel that emitted the signal
+ *
+ * The #SpiceCursorChannel::cursor-reset signal is emitted to
+ * reset the cursor to its default context.
+ **/
+ signals[SPICE_CURSOR_RESET] =
+ g_signal_new("cursor-reset",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceCursorChannelClass, cursor_reset),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
+
+ g_type_class_add_private(klass, sizeof(SpiceCursorChannelPrivate));
+ channel_set_handlers(SPICE_CHANNEL_CLASS(klass));
+}
+
+/* ------------------------------------------------------------------ */
+
+#ifdef DEBUG_CURSOR
+static void print_cursor(display_cursor *cursor, const guint8 *data)
+{
+ int x, y, bpl;
+ const guint8 *xor, *and;
+
+ bpl = (cursor->hdr.width + 7) / 8;
+ and = data;
+ xor = and + bpl * cursor->hdr.height;
+
+ printf("data (%d x %d):\n", cursor->hdr.width, cursor->hdr.height);
+ for (y = 0 ; y < cursor->hdr.height; ++y) {
+ for (x = 0 ; x < cursor->hdr.width / 8; x++) {
+ printf("%02X", and[x]);
+ }
+ and += bpl;
+ printf("\n");
+ }
+ printf("xor:\n");
+ for (y = 0 ; y < cursor->hdr.height; ++y) {
+ for (x = 0 ; x < cursor->hdr.width / 8; ++x) {
+ printf("%02X", xor[x]);
+ }
+ xor += bpl;
+ printf("\n");
+ }
+}
+#endif
+
+static void mono_cursor(display_cursor *cursor, const guint8 *data)
+{
+ int bpl = (cursor->hdr.width + 7) / 8;
+ const guint8 *xor, *and;
+ guint8 *dest;
+ dest = (uint8_t *)cursor->data;
+
+#ifdef DEBUG_CURSOR
+ print_cursor(cursor, data);
+#endif
+ and = data;
+ xor = and + bpl * cursor->hdr.height;
+ spice_mono_edge_highlight(cursor->hdr.width, cursor->hdr.height,
+ and, xor, dest);
+}
+
+static guint8 get_pix_mask(const guint8 *data, gint offset, gint pix_index)
+{
+ return data[offset + (pix_index >> 3)] & (0x80 >> (pix_index % 8));
+}
+
+static guint32 get_pix_hack(gint pix_index, gint width)
+{
+ return (((pix_index % width) ^ (pix_index / width)) & 1) ? 0xc0303030 : 0x30505050;
+}
+
+static display_cursor * display_cursor_ref(display_cursor *cursor)
+{
+ g_return_val_if_fail(cursor != NULL, NULL);
+ g_return_val_if_fail(cursor->refcount > 0, NULL);
+
+ cursor->refcount++;
+ return cursor;
+}
+
+static void display_cursor_unref(display_cursor *cursor)
+{
+ g_return_if_fail(cursor != NULL);
+ g_return_if_fail(cursor->refcount > 0);
+
+ cursor->refcount--;
+ if (cursor->refcount == 0)
+ g_free(cursor);
+}
+
+static const char *cursor_type_to_string(int type)
+{
+ switch (type) {
+ case SPICE_CURSOR_TYPE_MONO:
+ return "mono";
+ case SPICE_CURSOR_TYPE_ALPHA:
+ return "alpha";
+ case SPICE_CURSOR_TYPE_COLOR32:
+ return "color32";
+ case SPICE_CURSOR_TYPE_COLOR16:
+ return "color16";
+ case SPICE_CURSOR_TYPE_COLOR4:
+ return "color4";
+ }
+ return "unknown";
+}
+
+static display_cursor *set_cursor(SpiceChannel *channel, SpiceCursor *scursor)
+{
+ SpiceCursorChannelPrivate *c = SPICE_CURSOR_CHANNEL(channel)->priv;
+ SpiceCursorHeader *hdr = &scursor->header;
+ display_cursor *cursor;
+ size_t size;
+ gint i, pix_mask, pix;
+ const guint8* data;
+ guint8 *rgba;
+ guint8 val;
+
+ CHANNEL_DEBUG(channel, "%s: flags %d, size %d", __FUNCTION__,
+ scursor->flags, scursor->data_size);
+
+ if (scursor->flags & SPICE_CURSOR_FLAGS_NONE)
+ return NULL;
+
+ CHANNEL_DEBUG(channel, "%s: type %s(%d), %" PRIx64 ", %dx%d", __FUNCTION__,
+ cursor_type_to_string(hdr->type), hdr->type, hdr->unique,
+ hdr->width, hdr->height);
+
+ if (scursor->flags & SPICE_CURSOR_FLAGS_FROM_CACHE) {
+ cursor = cache_find(c->cursors, hdr->unique);
+ g_return_val_if_fail(cursor != NULL, NULL);
+ return display_cursor_ref(cursor);
+ }
+
+ g_return_val_if_fail(scursor->data_size != 0, NULL);
+
+ size = 4u * hdr->width * hdr->height;
+ cursor = g_malloc0(sizeof(*cursor) + size);
+ cursor->hdr = *hdr;
+ cursor->default_cursor = FALSE;
+ cursor->refcount = 1;
+ data = scursor->data;
+
+ switch (hdr->type) {
+ case SPICE_CURSOR_TYPE_MONO:
+ mono_cursor(cursor, data);
+ break;
+ case SPICE_CURSOR_TYPE_ALPHA:
+ memcpy(cursor->data, data, size);
+ break;
+ case SPICE_CURSOR_TYPE_COLOR32:
+ memcpy(cursor->data, data, size);
+ for (i = 0; i < hdr->width * hdr->height; i++) {
+ pix_mask = get_pix_mask(data, size, i);
+ if (pix_mask && *((guint32*)data + i) == 0xffffff) {
+ cursor->data[i] = get_pix_hack(i, hdr->width);
+ } else {
+ cursor->data[i] |= (pix_mask ? 0 : 0xff000000);
+ }
+ }
+ break;
+ case SPICE_CURSOR_TYPE_COLOR16:
+ for (i = 0; i < hdr->width * hdr->height; i++) {
+ pix_mask = get_pix_mask(data, size, i);
+ pix = *((guint16*)data + i);
+ if (pix_mask && pix == 0x7fff) {
+ cursor->data[i] = get_pix_hack(i, hdr->width);
+ } else {
+ cursor->data[i] |= ((pix & 0x1f) << 3) | ((pix & 0x3e0) << 6) |
+ ((pix & 0x7c00) << 9) | (pix_mask ? 0 : 0xff000000);
+ }
+ }
+ break;
+ case SPICE_CURSOR_TYPE_COLOR4:
+ size = ((unsigned int)(SPICE_ALIGN(hdr->width, 2) / 2)) * hdr->height;
+ for (i = 0; i < hdr->width * hdr->height; i++) {
+ pix_mask = get_pix_mask(data, size + (sizeof(uint32_t) << 4), i);
+ int idx = (i & 1) ? (data[i >> 1] & 0x0f) : ((data[i >> 1] & 0xf0) >> 4);
+ pix = *((uint32_t*)(data + size) + idx);
+ if (pix_mask && pix == 0xffffff) {
+ cursor->data[i] = get_pix_hack(i, hdr->width);
+ } else {
+ cursor->data[i] = pix | (pix_mask ? 0 : 0xff000000);
+ }
+ }
+
+ break;
+ default:
+ g_warning("%s: unimplemented cursor type %d", __FUNCTION__,
+ hdr->type);
+ cursor->default_cursor = TRUE;
+ goto cache_add;
+ }
+
+ rgba = (guint8*)cursor->data;
+ for (i = 0; i < hdr->width * hdr->height; i++) {
+ val = rgba[0];
+ rgba[0] = rgba[2];
+ rgba[2] = val;
+ rgba += 4;
+ }
+
+cache_add:
+ if (scursor->flags & SPICE_CURSOR_FLAGS_CACHE_ME) {
+ cache_add(c->cursors, hdr->unique, display_cursor_ref(cursor));
+ }
+
+ return cursor;
+}
+
+/* coroutine context */
+static void emit_cursor_set(SpiceChannel *channel, display_cursor *cursor)
+{
+ g_return_if_fail(cursor != NULL);
+ g_coroutine_signal_emit(channel, signals[SPICE_CURSOR_SET], 0,
+ cursor->hdr.width, cursor->hdr.height,
+ cursor->hdr.hot_spot_x, cursor->hdr.hot_spot_y,
+ cursor->default_cursor ? NULL : cursor->data);
+}
+
+/* coroutine context */
+static void cursor_handle_init(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgCursorInit *init = spice_msg_in_parsed(in);
+ SpiceCursorChannelPrivate *c = SPICE_CURSOR_CHANNEL(channel)->priv;
+ display_cursor *cursor;
+
+ g_return_if_fail(c->init_done == FALSE);
+
+ cache_clear(c->cursors);
+ cursor = set_cursor(channel, &init->cursor);
+ c->init_done = TRUE;
+ if (cursor)
+ emit_cursor_set(channel, cursor);
+ if (!init->visible || !cursor)
+ g_coroutine_signal_emit(channel, signals[SPICE_CURSOR_HIDE], 0);
+ if (cursor)
+ display_cursor_unref(cursor);
+}
+
+/* coroutine context */
+static void cursor_handle_reset(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceCursorChannelPrivate *c = SPICE_CURSOR_CHANNEL(channel)->priv;
+
+ CHANNEL_DEBUG(channel, "%s, init_done: %d", __FUNCTION__, c->init_done);
+
+ cache_clear(c->cursors);
+ g_coroutine_signal_emit(channel, signals[SPICE_CURSOR_RESET], 0);
+ c->init_done = FALSE;
+}
+
+/* coroutine context */
+static void cursor_handle_set(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgCursorSet *set = spice_msg_in_parsed(in);
+ SpiceCursorChannelPrivate *c = SPICE_CURSOR_CHANNEL(channel)->priv;
+ display_cursor *cursor;
+
+ g_return_if_fail(c->init_done == TRUE);
+
+ cursor = set_cursor(channel, &set->cursor);
+ if (cursor)
+ emit_cursor_set(channel, cursor);
+ else
+ g_coroutine_signal_emit(channel, signals[SPICE_CURSOR_HIDE], 0);
+
+
+ if (cursor)
+ display_cursor_unref(cursor);
+}
+
+/* coroutine context */
+static void cursor_handle_move(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgCursorMove *move = spice_msg_in_parsed(in);
+ SpiceCursorChannelPrivate *c = SPICE_CURSOR_CHANNEL(channel)->priv;
+
+ g_return_if_fail(c->init_done == TRUE);
+
+ g_coroutine_signal_emit(channel, signals[SPICE_CURSOR_MOVE], 0,
+ move->position.x, move->position.y);
+}
+
+/* coroutine context */
+static void cursor_handle_hide(SpiceChannel *channel, SpiceMsgIn *in)
+{
+#ifdef EXTRA_CHECKS
+ SpiceCursorChannelPrivate *c = SPICE_CURSOR_CHANNEL(channel)->priv;
+
+ g_return_if_fail(c->init_done == TRUE);
+#endif
+
+ g_coroutine_signal_emit(channel, signals[SPICE_CURSOR_HIDE], 0);
+}
+
+/* coroutine context */
+static void cursor_handle_trail(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceCursorChannelPrivate *c = SPICE_CURSOR_CHANNEL(channel)->priv;
+
+ g_return_if_fail(c->init_done == TRUE);
+
+ g_warning("%s: TODO", __FUNCTION__);
+}
+
+/* coroutine context */
+static void cursor_handle_inval_one(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceCursorChannelPrivate *c = SPICE_CURSOR_CHANNEL(channel)->priv;
+ SpiceMsgDisplayInvalOne *zap = spice_msg_in_parsed(in);
+
+ g_return_if_fail(c->init_done == TRUE);
+
+ cache_remove(c->cursors, zap->id);
+}
+
+/* coroutine context */
+static void cursor_handle_inval_all(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceCursorChannelPrivate *c = SPICE_CURSOR_CHANNEL(channel)->priv;
+
+ cache_clear(c->cursors);
+}
+
+static void channel_set_handlers(SpiceChannelClass *klass)
+{
+ static const spice_msg_handler handlers[] = {
+ [ SPICE_MSG_CURSOR_INIT ] = cursor_handle_init,
+ [ SPICE_MSG_CURSOR_RESET ] = cursor_handle_reset,
+ [ SPICE_MSG_CURSOR_SET ] = cursor_handle_set,
+ [ SPICE_MSG_CURSOR_MOVE ] = cursor_handle_move,
+ [ SPICE_MSG_CURSOR_HIDE ] = cursor_handle_hide,
+ [ SPICE_MSG_CURSOR_TRAIL ] = cursor_handle_trail,
+ [ SPICE_MSG_CURSOR_INVAL_ONE ] = cursor_handle_inval_one,
+ [ SPICE_MSG_CURSOR_INVAL_ALL ] = cursor_handle_inval_all,
+ };
+
+ spice_channel_set_handlers(klass, handlers, G_N_ELEMENTS(handlers));
+}
diff --git a/src/channel-cursor.h b/src/channel-cursor.h
new file mode 100644
index 0000000..5b5ed47
--- /dev/null
+++ b/src/channel-cursor.h
@@ -0,0 +1,77 @@
+/* -*- 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_CLIENT_CURSOR_CHANNEL_H__
+#define __SPICE_CLIENT_CURSOR_CHANNEL_H__
+
+#include "spice-client.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_CURSOR_CHANNEL (spice_cursor_channel_get_type())
+#define SPICE_CURSOR_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_CURSOR_CHANNEL, SpiceCursorChannel))
+#define SPICE_CURSOR_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_CURSOR_CHANNEL, SpiceCursorChannelClass))
+#define SPICE_IS_CURSOR_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_CURSOR_CHANNEL))
+#define SPICE_IS_CURSOR_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_CURSOR_CHANNEL))
+#define SPICE_CURSOR_CHANNEL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_CURSOR_CHANNEL, SpiceCursorChannelClass))
+
+typedef struct _SpiceCursorChannel SpiceCursorChannel;
+typedef struct _SpiceCursorChannelClass SpiceCursorChannelClass;
+typedef struct _SpiceCursorChannelPrivate SpiceCursorChannelPrivate;
+
+/**
+ * SpiceCursorChannel:
+ *
+ * The #SpiceCursorChannel struct is opaque and should not be accessed directly.
+ */
+struct _SpiceCursorChannel {
+ SpiceChannel parent;
+
+ /*< private >*/
+ SpiceCursorChannelPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+/**
+ * SpiceCursorChannelClass:
+ * @parent_class: Parent class.
+ * @cursor_set: Signal class handler for the #SpiceCursorChannel::cursor-set signal.
+ * @cursor_move: Signal class handler for the #SpiceCursorChannel::cursor-move signal.
+ * @cursor_hide: Signal class handler for the #SpiceCursorChannel::cursor-hide signal.
+ * @cursor_reset: Signal class handler for the #SpiceCursorChannel::cursor-reset signal.
+ *
+ * Class structure for #SpiceCursorChannel.
+ */
+struct _SpiceCursorChannelClass {
+ SpiceChannelClass parent_class;
+
+ /* signals */
+ void (*cursor_set)(SpiceCursorChannel *channel, gint width, gint height,
+ gint hot_x, gint hot_y, gpointer rgba);
+ void (*cursor_move)(SpiceCursorChannel *channel, gint x, gint y);
+ void (*cursor_hide)(SpiceCursorChannel *channel);
+ void (*cursor_reset)(SpiceCursorChannel *channel);
+
+ /*< private >*/
+ /* Do not add fields to this struct */
+};
+
+GType spice_cursor_channel_get_type(void);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_CURSOR_CHANNEL_H__ */
diff --git a/src/channel-display-mjpeg.c b/src/channel-display-mjpeg.c
new file mode 100644
index 0000000..95d5b33
--- /dev/null
+++ b/src/channel-display-mjpeg.c
@@ -0,0 +1,156 @@
+/* -*- 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-channel-priv.h"
+
+#include "channel-display-priv.h"
+
+static void mjpeg_src_init(struct jpeg_decompress_struct *cinfo)
+{
+ display_stream *st = SPICE_CONTAINEROF(cinfo->src, display_stream, mjpeg_src);
+ uint8_t *data;
+
+ cinfo->src->bytes_in_buffer = stream_get_current_frame(st, &data);
+ cinfo->src->next_input_byte = data;
+}
+
+static boolean mjpeg_src_fill(struct jpeg_decompress_struct *cinfo)
+{
+ g_critical("need more input data");
+ return 0;
+}
+
+static void mjpeg_src_skip(struct jpeg_decompress_struct *cinfo,
+ long num_bytes)
+{
+ cinfo->src->next_input_byte += num_bytes;
+}
+
+static void mjpeg_src_term(struct jpeg_decompress_struct *cinfo)
+{
+ /* nothing */
+}
+
+G_GNUC_INTERNAL
+void stream_mjpeg_init(display_stream *st)
+{
+ st->mjpeg_cinfo.err = jpeg_std_error(&st->mjpeg_jerr);
+ jpeg_create_decompress(&st->mjpeg_cinfo);
+
+ st->mjpeg_src.init_source = mjpeg_src_init;
+ st->mjpeg_src.fill_input_buffer = mjpeg_src_fill;
+ st->mjpeg_src.skip_input_data = mjpeg_src_skip;
+ st->mjpeg_src.resync_to_restart = jpeg_resync_to_restart;
+ st->mjpeg_src.term_source = mjpeg_src_term;
+ st->mjpeg_cinfo.src = &st->mjpeg_src;
+}
+
+G_GNUC_INTERNAL
+void stream_mjpeg_data(display_stream *st)
+{
+ gboolean back_compat = st->channel->priv->peer_hdr.major_version == 1;
+ int width;
+ int height;
+ uint8_t *dest;
+ uint8_t *lines[4];
+
+ stream_get_dimensions(st, &width, &height);
+ dest = g_malloc0(width * height * 4);
+
+ g_free(st->out_frame);
+ st->out_frame = dest;
+
+ jpeg_read_header(&st->mjpeg_cinfo, 1);
+#ifdef JCS_EXTENSIONS
+ // requires jpeg-turbo
+ if (back_compat)
+ st->mjpeg_cinfo.out_color_space = JCS_EXT_RGBX;
+ else
+ st->mjpeg_cinfo.out_color_space = JCS_EXT_BGRX;
+#else
+#warning "You should consider building with libjpeg-turbo"
+ st->mjpeg_cinfo.out_color_space = JCS_RGB;
+#endif
+
+#ifndef SPICE_QUALITY
+ st->mjpeg_cinfo.dct_method = JDCT_IFAST;
+ st->mjpeg_cinfo.do_fancy_upsampling = FALSE;
+ st->mjpeg_cinfo.do_block_smoothing = FALSE;
+ st->mjpeg_cinfo.dither_mode = JDITHER_ORDERED;
+#endif
+ // TODO: in theory should check cinfo.output_height match with our height
+ jpeg_start_decompress(&st->mjpeg_cinfo);
+ /* rec_outbuf_height is the recommended size of the output buffer we
+ * pass to libjpeg for optimum performance
+ */
+ if (st->mjpeg_cinfo.rec_outbuf_height > G_N_ELEMENTS(lines)) {
+ jpeg_abort_decompress(&st->mjpeg_cinfo);
+ g_return_if_reached();
+ }
+
+ while (st->mjpeg_cinfo.output_scanline < st->mjpeg_cinfo.output_height) {
+ /* only used when JCS_EXTENSIONS is undefined */
+ G_GNUC_UNUSED unsigned int lines_read;
+
+ for (unsigned int j = 0; j < st->mjpeg_cinfo.rec_outbuf_height; j++) {
+ lines[j] = dest;
+#ifdef JCS_EXTENSIONS
+ dest += 4 * width;
+#else
+ dest += 3 * width;
+#endif
+ }
+ lines_read = jpeg_read_scanlines(&st->mjpeg_cinfo, lines,
+ st->mjpeg_cinfo.rec_outbuf_height);
+#ifndef JCS_EXTENSIONS
+ {
+ uint8_t *s = lines[0];
+ uint32_t *d = (uint32_t *)s;
+
+ if (back_compat) {
+ for (unsigned int j = lines_read * width; j > 0; ) {
+ j -= 1; // reverse order, bad for cache?
+ d[j] = s[j * 3 + 0] |
+ s[j * 3 + 1] << 8 |
+ s[j * 3 + 2] << 16;
+ }
+ } else {
+ for (unsigned int j = lines_read * width; j > 0; ) {
+ j -= 1; // reverse order, bad for cache?
+ d[j] = s[j * 3 + 0] << 16 |
+ s[j * 3 + 1] << 8 |
+ s[j * 3 + 2];
+ }
+ }
+ }
+#endif
+ dest = &st->out_frame[st->mjpeg_cinfo.output_scanline * width * 4];
+ }
+ jpeg_finish_decompress(&st->mjpeg_cinfo);
+}
+
+G_GNUC_INTERNAL
+void stream_mjpeg_cleanup(display_stream *st)
+{
+ jpeg_destroy_decompress(&st->mjpeg_cinfo);
+ g_free(st->out_frame);
+ st->out_frame = NULL;
+}
diff --git a/src/channel-display-priv.h b/src/channel-display-priv.h
new file mode 100644
index 0000000..71f5d17
--- /dev/null
+++ b/src/channel-display-priv.h
@@ -0,0 +1,113 @@
+/* -*- 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 CHANNEL_DISPLAY_PRIV_H_
+# define CHANNEL_DISPLAY_PRIV_H_
+
+#include <pixman.h>
+#ifdef WIN32
+/* We need some hacks to avoid warnings from the jpeg headers */
+#define HAVE_BOOLEAN
+#define XMD_H
+#endif
+#include <jpeglib.h>
+
+#include "common/canvas_utils.h"
+#include "client_sw_canvas.h"
+#include "common/ring.h"
+#include "common/quic.h"
+#include "common/rop3.h"
+
+G_BEGIN_DECLS
+
+
+typedef struct display_surface {
+ guint32 surface_id;
+ bool primary;
+ enum SpiceSurfaceFmt format;
+ int width, height, stride, size;
+ int shmid;
+ uint8_t *data;
+ SpiceCanvas *canvas;
+ SpiceGlzDecoder *glz_decoder;
+ SpiceZlibDecoder *zlib_decoder;
+ SpiceJpegDecoder *jpeg_decoder;
+} display_surface;
+
+typedef struct drops_sequence_stats {
+ uint32_t len;
+ uint32_t start_mm_time;
+ uint32_t duration;
+} drops_sequence_stats;
+
+typedef struct display_stream {
+ SpiceMsgIn *msg_create;
+ SpiceMsgIn *msg_clip;
+ SpiceMsgIn *msg_data;
+
+ /* from messages */
+ display_surface *surface;
+ SpiceClip *clip;
+ QRegion region;
+ int have_region;
+ int codec;
+
+ /* mjpeg decoder */
+ struct jpeg_source_mgr mjpeg_src;
+ struct jpeg_decompress_struct mjpeg_cinfo;
+ struct jpeg_error_mgr mjpeg_jerr;
+
+ uint8_t *out_frame;
+ GQueue *msgq;
+ guint timeout;
+ SpiceChannel *channel;
+
+ /* stats */
+ uint32_t first_frame_mm_time;
+ uint32_t num_drops_on_receive;
+ uint64_t arrive_late_time;
+ uint32_t num_drops_on_playback;
+ uint32_t num_input_frames;
+ drops_sequence_stats cur_drops_seq_stats;
+ GArray *drops_seqs_stats_arr;
+ uint32_t num_drops_seqs;
+
+ uint32_t playback_sync_drops_seq_len;
+
+ /* playback quality report to server */
+ gboolean report_is_active;
+ uint32_t report_id;
+ uint32_t report_max_window;
+ uint32_t report_timeout;
+ uint64_t report_start_time;
+ uint32_t report_start_frame_time;
+ uint32_t report_num_frames;
+ uint32_t report_num_drops;
+ uint32_t report_drops_seq_len;
+} display_stream;
+
+void stream_get_dimensions(display_stream *st, int *width, int *height);
+uint32_t stream_get_current_frame(display_stream *st, uint8_t **data);
+
+/* channel-display-mjpeg.c */
+void stream_mjpeg_init(display_stream *st);
+void stream_mjpeg_data(display_stream *st);
+void stream_mjpeg_cleanup(display_stream *st);
+
+G_END_DECLS
+
+#endif // CHANNEL_DISPLAY_PRIV_H_
diff --git a/src/channel-display.c b/src/channel-display.c
new file mode 100644
index 0000000..efe2259
--- /dev/null
+++ b/src/channel-display.c
@@ -0,0 +1,1789 @@
+/* -*- 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"
+
+#ifdef HAVE_SYS_TYPES_H
+#include <sys/types.h>
+#endif
+
+#ifdef HAVE_SYS_SHM_H
+#include <sys/shm.h>
+#endif
+
+#ifdef HAVE_SYS_IPC_H
+#include <sys/ipc.h>
+#endif
+
+#include "glib-compat.h"
+#include "spice-client.h"
+#include "spice-common.h"
+
+#include "spice-marshal.h"
+#include "spice-channel-priv.h"
+#include "spice-session-priv.h"
+#include "channel-display-priv.h"
+#include "decode.h"
+
+/**
+ * SECTION:channel-display
+ * @short_description: remote display area
+ * @title: Display Channel
+ * @section_id:
+ * @see_also: #SpiceChannel, and the GTK widget #SpiceDisplay
+ * @stability: Stable
+ * @include: channel-display.h
+ *
+ * A class that handles the rendering of the remote display and inform
+ * of its updates.
+ *
+ * The creation of the main graphic buffer is signaled with
+ * #SpiceDisplayChannel::display-primary-create.
+ *
+ * The update of regions is notified by
+ * #SpiceDisplayChannel::display-invalidate signals.
+ */
+
+#define SPICE_DISPLAY_CHANNEL_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_DISPLAY_CHANNEL, SpiceDisplayChannelPrivate))
+
+#define MONITORS_MAX 256
+
+struct _SpiceDisplayChannelPrivate {
+ GHashTable *surfaces;
+ display_surface *primary;
+ display_cache *images;
+ display_cache *palettes;
+ SpiceImageCache image_cache;
+ SpicePaletteCache palette_cache;
+ SpiceImageSurfaces image_surfaces;
+ SpiceGlzDecoderWindow *glz_window;
+ display_stream **streams;
+ int nstreams;
+ gboolean mark;
+ guint mark_false_event_id;
+ GArray *monitors;
+ guint monitors_max;
+ gboolean enable_adaptive_streaming;
+#ifdef G_OS_WIN32
+ HDC dc;
+#endif
+};
+
+G_DEFINE_TYPE(SpiceDisplayChannel, spice_display_channel, SPICE_TYPE_CHANNEL)
+
+/* Properties */
+enum {
+ PROP_0,
+ PROP_WIDTH,
+ PROP_HEIGHT,
+ PROP_MONITORS,
+ PROP_MONITORS_MAX
+};
+
+enum {
+ SPICE_DISPLAY_PRIMARY_CREATE,
+ SPICE_DISPLAY_PRIMARY_DESTROY,
+ SPICE_DISPLAY_INVALIDATE,
+ SPICE_DISPLAY_MARK,
+
+ SPICE_DISPLAY_LAST_SIGNAL,
+};
+
+static guint signals[SPICE_DISPLAY_LAST_SIGNAL];
+
+static void spice_display_channel_up(SpiceChannel *channel);
+static void channel_set_handlers(SpiceChannelClass *klass);
+
+static void clear_surfaces(SpiceChannel *channel, gboolean keep_primary);
+static void clear_streams(SpiceChannel *channel);
+static display_surface *find_surface(SpiceDisplayChannelPrivate *c, guint32 surface_id);
+static gboolean display_stream_render(display_stream *st);
+static void spice_display_channel_reset(SpiceChannel *channel, gboolean migrating);
+static void spice_display_channel_reset_capabilities(SpiceChannel *channel);
+static void destroy_canvas(display_surface *surface);
+static void _msg_in_unref_func(gpointer data, gpointer user_data);
+static void display_session_mm_time_reset_cb(SpiceSession *session, gpointer data);
+
+/* ------------------------------------------------------------------ */
+
+static void spice_display_channel_dispose(GObject *object)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(object)->priv;
+
+ if (c->mark_false_event_id != 0) {
+ g_source_remove(c->mark_false_event_id);
+ c->mark_false_event_id = 0;
+ }
+
+ if (G_OBJECT_CLASS(spice_display_channel_parent_class)->dispose)
+ G_OBJECT_CLASS(spice_display_channel_parent_class)->dispose(object);
+}
+
+static void spice_display_channel_finalize(GObject *object)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(object)->priv;
+
+ g_clear_pointer(&c->monitors, g_array_unref);
+ clear_surfaces(SPICE_CHANNEL(object), FALSE);
+ g_hash_table_unref(c->surfaces);
+ clear_streams(SPICE_CHANNEL(object));
+ g_clear_pointer(&c->palettes, cache_unref);
+
+ if (G_OBJECT_CLASS(spice_display_channel_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_display_channel_parent_class)->finalize(object);
+}
+
+static void spice_display_channel_constructed(GObject *object)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(object)->priv;
+ SpiceSession *s = spice_channel_get_session(SPICE_CHANNEL(object));
+
+ g_return_if_fail(s != NULL);
+ spice_session_get_caches(s, &c->images, &c->glz_window);
+ c->palettes = cache_new(g_free);
+
+ g_return_if_fail(c->glz_window != NULL);
+ g_return_if_fail(c->images != NULL);
+ g_return_if_fail(c->palettes != NULL);
+
+ c->monitors = g_array_new(FALSE, TRUE, sizeof(SpiceDisplayMonitorConfig));
+ spice_g_signal_connect_object(s, "mm-time-reset",
+ G_CALLBACK(display_session_mm_time_reset_cb),
+ SPICE_CHANNEL(object), 0);
+
+
+ if (G_OBJECT_CLASS(spice_display_channel_parent_class)->constructed)
+ G_OBJECT_CLASS(spice_display_channel_parent_class)->constructed(object);
+}
+
+
+static void spice_display_get_property(GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(object)->priv;
+
+ switch (prop_id) {
+ case PROP_WIDTH: {
+ g_value_set_uint(value, c->primary ? c->primary->width : 0);
+ break;
+ }
+ case PROP_HEIGHT: {
+ g_value_set_uint(value, c->primary ? c->primary->height : 0);
+ break;
+ }
+ case PROP_MONITORS: {
+ g_value_set_boxed(value, c->monitors);
+ break;
+ }
+ case PROP_MONITORS_MAX: {
+ g_value_set_uint(value, c->monitors_max);
+ break;
+ }
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_display_set_property(GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (prop_id) {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+/* main or coroutine context */
+static void spice_display_channel_reset(SpiceChannel *channel, gboolean migrating)
+{
+ /* palettes, images, and glz_window are cleared in the session */
+ clear_streams(channel);
+ clear_surfaces(channel, TRUE);
+
+ SPICE_CHANNEL_CLASS(spice_display_channel_parent_class)->channel_reset(channel, migrating);
+}
+
+static void spice_display_channel_class_init(SpiceDisplayChannelClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS(klass);
+
+ gobject_class->finalize = spice_display_channel_finalize;
+ gobject_class->dispose = spice_display_channel_dispose;
+ gobject_class->get_property = spice_display_get_property;
+ gobject_class->set_property = spice_display_set_property;
+ gobject_class->constructed = spice_display_channel_constructed;
+
+ channel_class->channel_up = spice_display_channel_up;
+ channel_class->channel_reset = spice_display_channel_reset;
+ channel_class->channel_reset_capabilities = spice_display_channel_reset_capabilities;
+
+ g_object_class_install_property
+ (gobject_class, PROP_HEIGHT,
+ g_param_spec_uint("height",
+ "Display height",
+ "The primary surface height",
+ 0, G_MAXUINT, 0,
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_WIDTH,
+ g_param_spec_uint("width",
+ "Display width",
+ "The primary surface width",
+ 0, G_MAXUINT, 0,
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceDisplayChannel:monitors:
+ *
+ * Current monitors configuration.
+ *
+ * Since: 0.13
+ */
+ g_object_class_install_property
+ (gobject_class, PROP_MONITORS,
+ g_param_spec_boxed("monitors",
+ "Display monitors",
+ "The monitors configuration",
+ G_TYPE_ARRAY,
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceDisplayChannel:monitors-max:
+ *
+ * The maximum number of monitors the server or guest supports.
+ * May change during client lifetime, for instance guest may
+ * reboot or dynamically adjust this.
+ *
+ * Since: 0.13
+ */
+ g_object_class_install_property
+ (gobject_class, PROP_MONITORS_MAX,
+ g_param_spec_uint("monitors-max",
+ "Max display monitors",
+ "The current maximum number of monitors",
+ 1, MONITORS_MAX, 1,
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceDisplayChannel::display-primary-create:
+ * @display: the #SpiceDisplayChannel that emitted the signal
+ * @format: %SPICE_SURFACE_FMT_32_xRGB or %SPICE_SURFACE_FMT_16_555;
+ * @width: width resolution
+ * @height: height resolution
+ * @stride: the buffer stride ("width" padding)
+ * @shmid: identifier of the shared memory segment associated with
+ * the @imgdata, or -1 if not shm
+ * @imgdata: pointer to surface buffer
+ *
+ * The #SpiceDisplayChannel::display-primary-create signal
+ * provides main display buffer data.
+ **/
+ signals[SPICE_DISPLAY_PRIMARY_CREATE] =
+ g_signal_new("display-primary-create",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceDisplayChannelClass,
+ display_primary_create),
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__INT_INT_INT_INT_INT_POINTER,
+ G_TYPE_NONE,
+ 6,
+ G_TYPE_INT, G_TYPE_INT, G_TYPE_INT,
+ G_TYPE_INT, G_TYPE_INT, G_TYPE_POINTER);
+
+ /**
+ * SpiceDisplayChannel::display-primary-destroy:
+ * @display: the #SpiceDisplayChannel that emitted the signal
+ *
+ * The #SpiceDisplayChannel::display-primary-destroy signal is
+ * emitted when the primary surface is freed and should not be
+ * accessed anymore.
+ **/
+ signals[SPICE_DISPLAY_PRIMARY_DESTROY] =
+ g_signal_new("display-primary-destroy",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceDisplayChannelClass,
+ display_primary_destroy),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
+
+ /**
+ * SpiceDisplayChannel::display-invalidate:
+ * @display: the #SpiceDisplayChannel that emitted the signal
+ * @x: x position
+ * @y: y position
+ * @width: width
+ * @height: height
+ *
+ * The #SpiceDisplayChannel::display-invalidate signal is emitted
+ * when the rectangular region x/y/w/h of the primary buffer is
+ * updated.
+ **/
+ signals[SPICE_DISPLAY_INVALIDATE] =
+ g_signal_new("display-invalidate",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceDisplayChannelClass,
+ display_invalidate),
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__INT_INT_INT_INT,
+ G_TYPE_NONE,
+ 4,
+ G_TYPE_INT, G_TYPE_INT, G_TYPE_INT, G_TYPE_INT);
+
+ /**
+ * SpiceDisplayChannel::display-mark:
+ * @display: the #SpiceDisplayChannel that emitted the signal
+ * @mark: %TRUE when the display mark has been received
+ *
+ * The #SpiceDisplayChannel::display-mark signal is emitted when
+ * the %RED_DISPLAY_MARK command is received, and the display
+ * should be exposed.
+ **/
+ signals[SPICE_DISPLAY_MARK] =
+ g_signal_new("display-mark",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceDisplayChannelClass,
+ display_mark),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__INT,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_INT);
+
+ g_type_class_add_private(klass, sizeof(SpiceDisplayChannelPrivate));
+
+ sw_canvas_init();
+ quic_init();
+ rop3_init();
+ channel_set_handlers(SPICE_CHANNEL_CLASS(klass));
+}
+
+/**
+ * spice_display_get_primary:
+ * @channel:
+ * @surface_id:
+ * @primary:
+ *
+ * Retrieve primary display surface @surface_id.
+ *
+ * Returns: %TRUE if the primary surface was found and its details
+ * collected in @primary.
+ */
+gboolean spice_display_get_primary(SpiceChannel *channel, guint32 surface_id,
+ SpiceDisplayPrimary *primary)
+{
+ g_return_val_if_fail(SPICE_IS_DISPLAY_CHANNEL(channel), FALSE);
+ g_return_val_if_fail(primary != NULL, FALSE);
+
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+ display_surface *surface = find_surface(c, surface_id);
+
+ if (surface == NULL)
+ return FALSE;
+
+ g_return_val_if_fail(surface->primary, FALSE);
+
+ primary->format = surface->format;
+ primary->width = surface->width;
+ primary->height = surface->height;
+ primary->stride = surface->stride;
+ primary->shmid = surface->shmid;
+ primary->data = surface->data;
+ primary->marked = c->mark;
+ CHANNEL_DEBUG(channel, "get primary %p", primary->data);
+
+ return TRUE;
+}
+
+/* ------------------------------------------------------------------ */
+
+static void image_put(SpiceImageCache *cache, uint64_t id, pixman_image_t *image)
+{
+ SpiceDisplayChannelPrivate *c =
+ SPICE_CONTAINEROF(cache, SpiceDisplayChannelPrivate, image_cache);
+
+ cache_add(c->images, id, pixman_image_ref(image));
+}
+
+typedef struct _WaitImageData
+{
+ gboolean lossy;
+ SpiceImageCache *cache;
+ uint64_t id;
+ pixman_image_t *image;
+} WaitImageData;
+
+static gboolean wait_image(gpointer data)
+{
+ gboolean lossy;
+ WaitImageData *wait = data;
+ SpiceDisplayChannelPrivate *c =
+ SPICE_CONTAINEROF(wait->cache, SpiceDisplayChannelPrivate, image_cache);
+ pixman_image_t *image = cache_find_lossy(c->images, wait->id, &lossy);
+
+ if (!image || (lossy && !wait->lossy))
+ return FALSE;
+
+ wait->image = pixman_image_ref(image);
+
+ return TRUE;
+}
+
+static pixman_image_t *image_get(SpiceImageCache *cache, uint64_t id)
+{
+ WaitImageData wait = {
+ .lossy = TRUE,
+ .cache = cache,
+ .id = id,
+ .image = NULL
+ };
+ if (!g_coroutine_condition_wait(g_coroutine_self(), wait_image, &wait))
+ SPICE_DEBUG("wait image got cancelled");
+
+ return wait.image;
+}
+
+static void palette_put(SpicePaletteCache *cache, SpicePalette *palette)
+{
+ SpiceDisplayChannelPrivate *c =
+ SPICE_CONTAINEROF(cache, SpiceDisplayChannelPrivate, palette_cache);
+
+ cache_add(c->palettes, palette->unique,
+ g_memdup(palette, sizeof(SpicePalette) +
+ palette->num_ents * sizeof(palette->ents[0])));
+}
+
+static SpicePalette *palette_get(SpicePaletteCache *cache, uint64_t id)
+{
+ SpiceDisplayChannelPrivate *c =
+ SPICE_CONTAINEROF(cache, SpiceDisplayChannelPrivate, palette_cache);
+
+ /* here the returned pointer is weak, no ref given to caller. it
+ * seems spice canvas usage is exclusively temporary, so it's ok.
+ * palette_release is a noop. */
+ return cache_find(c->palettes, id);
+}
+
+static void palette_remove(SpicePaletteCache *cache, uint64_t id)
+{
+ SpiceDisplayChannelPrivate *c =
+ SPICE_CONTAINEROF(cache, SpiceDisplayChannelPrivate, palette_cache);
+
+ cache_remove(c->palettes, id);
+}
+
+static void palette_release(SpicePaletteCache *cache, SpicePalette *palette)
+{
+ /* there is no refcount of palette, see palette_get() */
+}
+
+static void image_put_lossy(SpiceImageCache *cache, uint64_t id,
+ pixman_image_t *surface)
+{
+ SpiceDisplayChannelPrivate *c =
+ SPICE_CONTAINEROF(cache, SpiceDisplayChannelPrivate, image_cache);
+
+#ifndef NDEBUG
+ g_warn_if_fail(cache_find(c->images, id) == NULL);
+#endif
+
+ cache_add_lossy(c->images, id, pixman_image_ref(surface), TRUE);
+}
+
+static void image_replace_lossy(SpiceImageCache *cache, uint64_t id,
+ pixman_image_t *surface)
+{
+ image_put(cache, id, surface);
+}
+
+static pixman_image_t* image_get_lossless(SpiceImageCache *cache, uint64_t id)
+{
+ WaitImageData wait = {
+ .lossy = FALSE,
+ .cache = cache,
+ .id = id,
+ .image = NULL
+ };
+ if (!g_coroutine_condition_wait(g_coroutine_self(), wait_image, &wait))
+ SPICE_DEBUG("wait lossless got cancelled");
+
+ return wait.image;
+}
+
+static SpiceCanvas *surfaces_get(SpiceImageSurfaces *surfaces,
+ uint32_t surface_id)
+{
+ SpiceDisplayChannelPrivate *c =
+ SPICE_CONTAINEROF(surfaces, SpiceDisplayChannelPrivate, image_surfaces);
+
+ display_surface *s =
+ find_surface(c, surface_id);
+
+ return s ? s->canvas : NULL;
+}
+
+static SpiceImageCacheOps image_cache_ops = {
+ .put = image_put,
+ .get = image_get,
+
+ .put_lossy = image_put_lossy,
+ .replace_lossy = image_replace_lossy,
+ .get_lossless = image_get_lossless,
+};
+
+static SpicePaletteCacheOps palette_cache_ops = {
+ .put = palette_put,
+ .get = palette_get,
+ .release = palette_release,
+};
+
+static SpiceImageSurfacesOps image_surfaces_ops = {
+ .get = surfaces_get
+};
+
+#if defined(G_OS_WIN32)
+static HDC create_compatible_dc(void)
+{
+ HDC dc = CreateCompatibleDC(NULL);
+ if (!dc) {
+ g_warning("create compatible DC failed");
+ }
+ return dc;
+}
+#endif
+
+static void spice_display_channel_reset_capabilities(SpiceChannel *channel)
+{
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_DISPLAY_CAP_SIZED_STREAM);
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_DISPLAY_CAP_MONITORS_CONFIG);
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_DISPLAY_CAP_COMPOSITE);
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_DISPLAY_CAP_A8_SURFACE);
+#ifdef USE_LZ4
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_DISPLAY_CAP_LZ4_COMPRESSION);
+#endif
+ if (SPICE_DISPLAY_CHANNEL(channel)->priv->enable_adaptive_streaming) {
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_DISPLAY_CAP_STREAM_REPORT);
+ }
+}
+
+static void destroy_surface(gpointer data)
+{
+ display_surface *surface = data;
+
+ destroy_canvas(surface);
+ g_slice_free(display_surface, surface);
+}
+
+static void spice_display_channel_init(SpiceDisplayChannel *channel)
+{
+ SpiceDisplayChannelPrivate *c;
+
+ c = channel->priv = SPICE_DISPLAY_CHANNEL_GET_PRIVATE(channel);
+
+ c->surfaces = g_hash_table_new_full(NULL, NULL, NULL, destroy_surface);
+ c->image_cache.ops = &image_cache_ops;
+ c->palette_cache.ops = &palette_cache_ops;
+ c->image_surfaces.ops = &image_surfaces_ops;
+#if defined(G_OS_WIN32)
+ c->dc = create_compatible_dc();
+#endif
+ c->monitors_max = 1;
+
+ if (g_getenv("SPICE_DISABLE_ADAPTIVE_STREAMING")) {
+ SPICE_DEBUG("adaptive video disabled");
+ c->enable_adaptive_streaming = FALSE;
+ } else {
+ c->enable_adaptive_streaming = TRUE;
+ }
+ spice_display_channel_reset_capabilities(SPICE_CHANNEL(channel));
+}
+
+/* ------------------------------------------------------------------ */
+
+static int create_canvas(SpiceChannel *channel, display_surface *surface)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+
+ if (surface->primary) {
+ if (c->primary) {
+ if (c->primary->width == surface->width &&
+ c->primary->height == surface->height) {
+ CHANNEL_DEBUG(channel, "Reusing existing primary surface");
+ return 0;
+ }
+
+ g_coroutine_signal_emit(channel, signals[SPICE_DISPLAY_PRIMARY_DESTROY], 0);
+
+ g_hash_table_remove(c->surfaces, GINT_TO_POINTER(c->primary->surface_id));
+ }
+
+ CHANNEL_DEBUG(channel, "Create primary canvas");
+#if defined(WITH_X11) && defined(HAVE_SYS_SHM_H)
+ surface->shmid = shmget(IPC_PRIVATE, surface->size, IPC_CREAT | 0777);
+ if (surface->shmid >= 0) {
+ surface->data = shmat(surface->shmid, 0, 0);
+ if (surface->data == NULL) {
+ shmctl(surface->shmid, IPC_RMID, 0);
+ surface->shmid = -1;
+ }
+ }
+#else
+ surface->shmid = -1;
+#endif
+ } else {
+ surface->shmid = -1;
+ }
+
+ if (surface->shmid == -1)
+ surface->data = g_malloc0(surface->size);
+
+ g_return_val_if_fail(c->glz_window, 0);
+
+ g_warn_if_fail(surface->canvas == NULL);
+ g_warn_if_fail(surface->glz_decoder == NULL);
+ g_warn_if_fail(surface->zlib_decoder == NULL);
+ g_warn_if_fail(surface->jpeg_decoder == NULL);
+
+ surface->glz_decoder = glz_decoder_new(c->glz_window);
+ surface->zlib_decoder = zlib_decoder_new();
+ surface->jpeg_decoder = jpeg_decoder_new();
+
+ surface->canvas = canvas_create_for_data(surface->width,
+ surface->height,
+ surface->format,
+ surface->data,
+ surface->stride,
+ &c->image_cache,
+ &c->palette_cache,
+ &c->image_surfaces,
+ surface->glz_decoder,
+ surface->jpeg_decoder,
+ surface->zlib_decoder);
+
+ g_return_val_if_fail(surface->canvas != NULL, 0);
+ g_hash_table_insert(c->surfaces, GINT_TO_POINTER(surface->surface_id), surface);
+
+ if (surface->primary) {
+ g_warn_if_fail(c->primary == NULL);
+ c->primary = surface;
+ g_coroutine_signal_emit(channel, signals[SPICE_DISPLAY_PRIMARY_CREATE], 0,
+ surface->format, surface->width, surface->height,
+ surface->stride, surface->shmid, surface->data);
+
+ if (!spice_channel_test_capability(channel, SPICE_DISPLAY_CAP_MONITORS_CONFIG)) {
+ g_array_set_size(c->monitors, 1);
+ SpiceDisplayMonitorConfig *config = &g_array_index(c->monitors, SpiceDisplayMonitorConfig, 0);
+ config->x = config->y = 0;
+ config->width = surface->width;
+ config->height = surface->height;
+ g_coroutine_object_notify(G_OBJECT(channel), "monitors");
+ }
+ }
+
+ return 0;
+}
+
+static void destroy_canvas(display_surface *surface)
+{
+ if (surface == NULL)
+ return;
+
+ glz_decoder_destroy(surface->glz_decoder);
+ zlib_decoder_destroy(surface->zlib_decoder);
+ jpeg_decoder_destroy(surface->jpeg_decoder);
+
+ if (surface->shmid == -1) {
+ g_free(surface->data);
+ }
+#ifdef HAVE_SYS_SHM_H
+ else {
+ shmdt(surface->data);
+ shmctl(surface->shmid, IPC_RMID, 0);
+ }
+#endif
+ surface->shmid = -1;
+ surface->data = NULL;
+
+ surface->canvas->ops->destroy(surface->canvas);
+ surface->canvas = NULL;
+}
+
+static display_surface *find_surface(SpiceDisplayChannelPrivate *c, guint32 surface_id)
+{
+ if (c->primary && c->primary->surface_id == surface_id)
+ return c->primary;
+
+ return g_hash_table_lookup(c->surfaces, GINT_TO_POINTER(surface_id));
+}
+
+/* main or coroutine context */
+static void clear_surfaces(SpiceChannel *channel, gboolean keep_primary)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+ GHashTableIter iter;
+ display_surface *surface;
+
+ if (!keep_primary) {
+ c->primary = NULL;
+ g_coroutine_signal_emit(channel, signals[SPICE_DISPLAY_PRIMARY_DESTROY], 0);
+ }
+
+ g_hash_table_iter_init(&iter, c->surfaces);
+ while (g_hash_table_iter_next(&iter, NULL, (gpointer*)&surface)) {
+
+ if (keep_primary && surface->primary) {
+ CHANNEL_DEBUG(channel, "keeping existing primary surface, migration or reset");
+ continue;
+ }
+
+ g_hash_table_iter_remove(&iter);
+ }
+}
+
+/* coroutine context */
+static void emit_invalidate(SpiceChannel *channel, SpiceRect *bbox)
+{
+ g_coroutine_signal_emit(channel, signals[SPICE_DISPLAY_INVALIDATE], 0,
+ bbox->left, bbox->top,
+ bbox->right - bbox->left,
+ bbox->bottom - bbox->top);
+}
+
+/* ------------------------------------------------------------------ */
+
+/* coroutine context */
+static void spice_display_channel_up(SpiceChannel *channel)
+{
+ SpiceMsgOut *out;
+ SpiceSession *s = spice_channel_get_session(channel);
+ SpiceMsgcDisplayInit init;
+ int cache_size;
+ int glz_window_size;
+
+ g_object_get(s,
+ "cache-size", &cache_size,
+ "glz-window-size", &glz_window_size,
+ NULL);
+ CHANNEL_DEBUG(channel, "%s: cache_size %d, glz_window_size %d (bytes)", __FUNCTION__,
+ cache_size, glz_window_size);
+ init.pixmap_cache_id = 1;
+ init.glz_dictionary_id = 1;
+ init.pixmap_cache_size = cache_size / 4; /* pixels */
+ init.glz_dictionary_window_size = glz_window_size / 4; /* pixels */
+ out = spice_msg_out_new(channel, SPICE_MSGC_DISPLAY_INIT);
+ out->marshallers->msgc_display_init(out->marshaller, &init);
+ spice_msg_out_send_internal(out);
+
+ /* if we are not using monitors config, notify of existence of
+ this monitor */
+ if (channel->priv->channel_id != 0)
+ g_coroutine_object_notify(G_OBJECT(channel), "monitors");
+}
+
+#define DRAW(type) { \
+ display_surface *surface = \
+ find_surface(SPICE_DISPLAY_CHANNEL(channel)->priv, \
+ op->base.surface_id); \
+ g_return_if_fail(surface != NULL); \
+ surface->canvas->ops->draw_##type(surface->canvas, &op->base.box, \
+ &op->base.clip, &op->data); \
+ if (surface->primary) { \
+ emit_invalidate(channel, &op->base.box); \
+ } \
+}
+
+/* coroutine context */
+static void display_handle_mode(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+ SpiceMsgDisplayMode *mode = spice_msg_in_parsed(in);
+ display_surface *surface;
+
+ g_warn_if_fail(c->mark == FALSE);
+
+ surface = g_slice_new0(display_surface);
+ surface->format = mode->bits == 32 ?
+ SPICE_SURFACE_FMT_32_xRGB : SPICE_SURFACE_FMT_16_555;
+ surface->width = mode->x_res;
+ surface->height = mode->y_res;
+ surface->stride = surface->width * 4;
+ surface->size = surface->height * surface->stride;
+ surface->primary = true;
+ create_canvas(channel, surface);
+}
+
+/* coroutine context */
+static void display_handle_mark(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+
+ CHANNEL_DEBUG(channel, "%s", __FUNCTION__);
+ g_return_if_fail(c->primary != NULL);
+#ifdef EXTRA_CHECKS
+ g_warn_if_fail(c->mark == FALSE);
+#endif
+
+ c->mark = TRUE;
+ g_coroutine_signal_emit(channel, signals[SPICE_DISPLAY_MARK], 0, TRUE);
+}
+
+/* coroutine context */
+static void display_handle_reset(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+ display_surface *surface = c->primary;
+
+ CHANNEL_DEBUG(channel, "%s: TODO detach_from_screen", __FUNCTION__);
+
+ if (surface != NULL)
+ surface->canvas->ops->clear(surface->canvas);
+
+ cache_clear(c->palettes);
+
+ c->mark = FALSE;
+ g_coroutine_signal_emit(channel, signals[SPICE_DISPLAY_MARK], 0, FALSE);
+}
+
+/* coroutine context */
+static void display_handle_copy_bits(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgDisplayCopyBits *op = spice_msg_in_parsed(in);
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+ display_surface *surface = find_surface(c, op->base.surface_id);
+
+ g_return_if_fail(surface != NULL);
+ surface->canvas->ops->copy_bits(surface->canvas, &op->base.box,
+ &op->base.clip, &op->src_pos);
+ if (surface->primary) {
+ emit_invalidate(channel, &op->base.box);
+ }
+}
+
+/* coroutine context */
+static void display_handle_inv_list(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+ SpiceResourceList *list = spice_msg_in_parsed(in);
+ int i;
+
+ for (i = 0; i < list->count; i++) {
+ guint64 id = list->resources[i].id;
+
+ switch (list->resources[i].type) {
+ case SPICE_RES_TYPE_PIXMAP:
+ if (!cache_remove(c->images, id))
+ SPICE_DEBUG("fail to remove image %" G_GUINT64_FORMAT, id);
+ break;
+ default:
+ g_return_if_reached();
+ break;
+ }
+ }
+}
+
+/* coroutine context */
+static void display_handle_inv_pixmap_all(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+
+ spice_channel_handle_wait_for_channels(channel, in);
+ cache_clear(c->images);
+}
+
+/* coroutine context */
+static void display_handle_inv_palette(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+ SpiceMsgDisplayInvalOne* op = spice_msg_in_parsed(in);
+
+ palette_remove(&c->palette_cache, op->id);
+}
+
+/* coroutine context */
+static void display_handle_inv_palette_all(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+
+ cache_clear(c->palettes);
+}
+
+/* ------------------------------------------------------------------ */
+
+static void display_update_stream_region(display_stream *st)
+{
+ int i;
+
+ switch (st->clip->type) {
+ case SPICE_CLIP_TYPE_RECTS:
+ region_clear(&st->region);
+ for (i = 0; i < st->clip->rects->num_rects; i++) {
+ region_add(&st->region, &st->clip->rects->rects[i]);
+ }
+ st->have_region = true;
+ break;
+ case SPICE_CLIP_TYPE_NONE:
+ default:
+ st->have_region = false;
+ break;
+ }
+}
+
+/* coroutine context */
+static void display_handle_stream_create(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+ SpiceMsgDisplayStreamCreate *op = spice_msg_in_parsed(in);
+ display_stream *st;
+
+ CHANNEL_DEBUG(channel, "%s: id %d", __FUNCTION__, op->id);
+
+ if (op->id >= c->nstreams) {
+ int n = c->nstreams;
+ if (!c->nstreams) {
+ c->nstreams = 1;
+ }
+ while (op->id >= c->nstreams) {
+ c->nstreams *= 2;
+ }
+ c->streams = realloc(c->streams, c->nstreams * sizeof(c->streams[0]));
+ memset(c->streams + n, 0, (c->nstreams - n) * sizeof(c->streams[0]));
+ }
+ g_return_if_fail(c->streams[op->id] == NULL);
+ c->streams[op->id] = g_new0(display_stream, 1);
+ st = c->streams[op->id];
+
+ st->msg_create = in;
+ spice_msg_in_ref(in);
+ st->clip = &op->clip;
+ st->codec = op->codec_type;
+ st->surface = find_surface(c, op->surface_id);
+ st->msgq = g_queue_new();
+ st->channel = channel;
+ st->drops_seqs_stats_arr = g_array_new(FALSE, FALSE, sizeof(drops_sequence_stats));
+
+ region_init(&st->region);
+ display_update_stream_region(st);
+
+ switch (st->codec) {
+ case SPICE_VIDEO_CODEC_TYPE_MJPEG:
+ stream_mjpeg_init(st);
+ break;
+ }
+}
+
+/* coroutine or main context */
+static gboolean display_stream_schedule(display_stream *st)
+{
+ SpiceSession *session = spice_channel_get_session(st->channel);
+ guint32 time, d;
+ SpiceStreamDataHeader *op;
+ SpiceMsgIn *in;
+
+ SPICE_DEBUG("%s", __FUNCTION__);
+ if (st->timeout || !session)
+ return TRUE;
+
+ time = spice_session_get_mm_time(session);
+ in = g_queue_peek_head(st->msgq);
+
+ if (in == NULL) {
+ return TRUE;
+ }
+
+ op = spice_msg_in_parsed(in);
+ if (time < op->multi_media_time) {
+ d = op->multi_media_time - time;
+ SPICE_DEBUG("scheduling next stream render in %u ms", d);
+ st->timeout = g_timeout_add(d, (GSourceFunc)display_stream_render, st);
+ return TRUE;
+ } else {
+ SPICE_DEBUG("%s: rendering too late by %u ms (ts: %u, mmtime: %u), dropping ",
+ __FUNCTION__, time - op->multi_media_time,
+ op->multi_media_time, time);
+ in = g_queue_pop_head(st->msgq);
+ spice_msg_in_unref(in);
+ st->num_drops_on_playback++;
+ if (g_queue_get_length(st->msgq) == 0)
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static SpiceRect *stream_get_dest(display_stream *st)
+{
+ if (st->msg_data == NULL ||
+ spice_msg_in_type(st->msg_data) != SPICE_MSG_DISPLAY_STREAM_DATA_SIZED) {
+ SpiceMsgDisplayStreamCreate *info = spice_msg_in_parsed(st->msg_create);
+
+ return &info->dest;
+ } else {
+ SpiceMsgDisplayStreamDataSized *op = spice_msg_in_parsed(st->msg_data);
+
+ return &op->dest;
+ }
+
+}
+
+static uint32_t stream_get_flags(display_stream *st)
+{
+ SpiceMsgDisplayStreamCreate *info = spice_msg_in_parsed(st->msg_create);
+
+ return info->flags;
+}
+
+G_GNUC_INTERNAL
+uint32_t stream_get_current_frame(display_stream *st, uint8_t **data)
+{
+ if (st->msg_data == NULL) {
+ *data = NULL;
+ return 0;
+ }
+
+ if (spice_msg_in_type(st->msg_data) == SPICE_MSG_DISPLAY_STREAM_DATA) {
+ SpiceMsgDisplayStreamData *op = spice_msg_in_parsed(st->msg_data);
+
+ *data = op->data;
+ return op->data_size;
+ } else {
+ SpiceMsgDisplayStreamDataSized *op = spice_msg_in_parsed(st->msg_data);
+
+ g_return_val_if_fail(spice_msg_in_type(st->msg_data) ==
+ SPICE_MSG_DISPLAY_STREAM_DATA_SIZED, 0);
+ *data = op->data;
+ return op->data_size;
+ }
+
+}
+
+G_GNUC_INTERNAL
+void stream_get_dimensions(display_stream *st, int *width, int *height)
+{
+ g_return_if_fail(width != NULL);
+ g_return_if_fail(height != NULL);
+
+ if (st->msg_data == NULL ||
+ spice_msg_in_type(st->msg_data) != SPICE_MSG_DISPLAY_STREAM_DATA_SIZED) {
+ SpiceMsgDisplayStreamCreate *info = spice_msg_in_parsed(st->msg_create);
+
+ *width = info->stream_width;
+ *height = info->stream_height;
+ } else {
+ SpiceMsgDisplayStreamDataSized *op = spice_msg_in_parsed(st->msg_data);
+
+ *width = op->width;
+ *height = op->height;
+ }
+}
+
+/* main context */
+static gboolean display_stream_render(display_stream *st)
+{
+ SpiceMsgIn *in;
+
+ st->timeout = 0;
+ do {
+ in = g_queue_pop_head(st->msgq);
+
+ g_return_val_if_fail(in != NULL, FALSE);
+
+ st->msg_data = in;
+ switch (st->codec) {
+ case SPICE_VIDEO_CODEC_TYPE_MJPEG:
+ stream_mjpeg_data(st);
+ break;
+ }
+
+ if (st->out_frame) {
+ int width;
+ int height;
+ SpiceRect *dest;
+ uint8_t *data;
+ int stride;
+
+ stream_get_dimensions(st, &width, &height);
+ dest = stream_get_dest(st);
+
+ data = st->out_frame;
+ stride = width * sizeof(uint32_t);
+ if (!(stream_get_flags(st) & SPICE_STREAM_FLAGS_TOP_DOWN)) {
+ data += stride * (height - 1);
+ stride = -stride;
+ }
+
+ st->surface->canvas->ops->put_image(
+ st->surface->canvas,
+#ifdef G_OS_WIN32
+ SPICE_DISPLAY_CHANNEL(st->channel)->priv->dc,
+#endif
+ dest, data,
+ width, height, stride,
+ st->have_region ? &st->region : NULL);
+
+ if (st->surface->primary)
+ g_signal_emit(st->channel, signals[SPICE_DISPLAY_INVALIDATE], 0,
+ dest->left, dest->top,
+ dest->right - dest->left,
+ dest->bottom - dest->top);
+ }
+
+ st->msg_data = NULL;
+ spice_msg_in_unref(in);
+
+ in = g_queue_peek_head(st->msgq);
+ if (in == NULL)
+ break;
+
+ if (display_stream_schedule(st))
+ return FALSE;
+ } while (1);
+
+ return FALSE;
+}
+/* after a sequence of 3 drops, push a report to the server, even
+ * if the report window is bigger */
+#define STREAM_REPORT_DROP_SEQ_LEN_LIMIT 3
+
+static void display_update_stream_report(SpiceDisplayChannel *channel, uint32_t stream_id,
+ uint32_t frame_time, int32_t latency)
+{
+ display_stream *st = channel->priv->streams[stream_id];
+ guint64 now;
+
+ if (!st->report_is_active) {
+ return;
+ }
+ now = g_get_monotonic_time();
+
+ if (st->report_num_frames == 0) {
+ st->report_start_frame_time = frame_time;
+ st->report_start_time = now;
+ }
+ st->report_num_frames++;
+
+ if (latency < 0) { // drop
+ st->report_num_drops++;
+ st->report_drops_seq_len++;
+ } else {
+ st->report_drops_seq_len = 0;
+ }
+
+ if (st->report_num_frames >= st->report_max_window ||
+ now - st->report_start_time >= st->report_timeout ||
+ st->report_drops_seq_len >= STREAM_REPORT_DROP_SEQ_LEN_LIMIT) {
+ SpiceMsgcDisplayStreamReport report;
+ SpiceSession *session = spice_channel_get_session(SPICE_CHANNEL(channel));
+ SpiceMsgOut *msg;
+
+ report.stream_id = stream_id;
+ report.unique_id = st->report_id;
+ report.start_frame_mm_time = st->report_start_frame_time;
+ report.end_frame_mm_time = frame_time;
+ report.num_frames = st->report_num_frames;
+ report.num_drops = st-> report_num_drops;
+ report.last_frame_delay = latency;
+ if (spice_session_is_playback_active(session)) {
+ report.audio_delay = spice_session_get_playback_latency(session);
+ } else {
+ report.audio_delay = UINT_MAX;
+ }
+
+ msg = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_DISPLAY_STREAM_REPORT);
+ msg->marshallers->msgc_display_stream_report(msg->marshaller, &report);
+ spice_msg_out_send(msg);
+
+ st->report_start_time = 0;
+ st->report_start_frame_time = 0;
+ st->report_num_frames = 0;
+ st->report_num_drops = 0;
+ st->report_drops_seq_len = 0;
+ }
+}
+
+static void display_stream_reset_rendering_timer(display_stream *st)
+{
+ SPICE_DEBUG("%s", __FUNCTION__);
+ if (st->timeout != 0) {
+ g_source_remove(st->timeout);
+ st->timeout = 0;
+ }
+ while (!display_stream_schedule(st)) {
+ }
+}
+
+/*
+ * Migration can occur between 2 spice-servers with different mm-times.
+ * Then, the following cases can happen after migration completes:
+ * (We refer to src/dst-time as the mm-times on the src/dst servers):
+ *
+ * (case 1) Frames with time ~= dst-time arrive to the client before the
+ * playback-channel updates the session's mm-time (i.e., the mm_time
+ * of the session is still based on the src-time).
+ * (a) If src-time < dst-time:
+ * display_stream_schedule schedules the next rendering to
+ * ~(dst-time - src-time) milliseconds from now.
+ * Since we assume monotonic mm_time, display_stream_schedule,
+ * returns immediately when a rendering timeout
+ * has already been set, and doesn't update the timeout,
+ * even after the mm_time is updated.
+ * When src-time << dst-time, a significant video frames loss will occur.
+ * (b) If src-time > dst-time
+ * Frames will be dropped till the mm-time will be updated.
+ * (case 2) mm-time is synced with dst-time, but frames that were in the command
+ * ring during migration still arrive (such frames hold src-time).
+ * (a) If src-time < dst-time
+ * The frames that hold src-time will be dropped, since their
+ * mm_time < session-mm_time. But all the new frames that are generated in
+ * the driver after migration, will be rendered appropriately.
+ * (b) If src-time > dst-time
+ * Similar consequences as in 1 (a)
+ * case 2 is less likely, since at takes at least 20 frames till the dst-server re-identifies
+ * the video stream and starts sending stream data
+ *
+ * display_session_mm_time_reset_cb handles case 1.a, and
+ * display_stream_test_frames_mm_time_reset handles case 2.b
+ */
+
+/* main context */
+static void display_session_mm_time_reset_cb(SpiceSession *session, gpointer data)
+{
+ SpiceChannel *channel = data;
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+ guint i;
+
+ CHANNEL_DEBUG(channel, "%s", __FUNCTION__);
+
+ for (i = 0; i < c->nstreams; i++) {
+ display_stream *st;
+
+ if (c->streams[i] == NULL) {
+ continue;
+ }
+ SPICE_DEBUG("%s: stream-id %d", __FUNCTION__, i);
+ st = c->streams[i];
+ display_stream_reset_rendering_timer(st);
+ }
+}
+
+/* coroutine context */
+static void display_stream_test_frames_mm_time_reset(display_stream *st,
+ SpiceMsgIn *new_frame_msg,
+ guint32 mm_time)
+{
+ SpiceStreamDataHeader *tail_op, *new_op;
+ SpiceMsgIn *tail_msg;
+
+ SPICE_DEBUG("%s", __FUNCTION__);
+ g_return_if_fail(new_frame_msg != NULL);
+ tail_msg = g_queue_peek_tail(st->msgq);
+ if (!tail_msg) {
+ return;
+ }
+ tail_op = spice_msg_in_parsed(tail_msg);
+ new_op = spice_msg_in_parsed(new_frame_msg);
+
+ if (new_op->multi_media_time < tail_op->multi_media_time) {
+ SPICE_DEBUG("new-frame-time < tail-frame-time (%u < %u):"
+ " reseting stream, id %d",
+ new_op->multi_media_time,
+ tail_op->multi_media_time,
+ new_op->id);
+ g_queue_foreach(st->msgq, _msg_in_unref_func, NULL);
+ g_queue_clear(st->msgq);
+ display_stream_reset_rendering_timer(st);
+ }
+}
+
+#define STREAM_PLAYBACK_SYNC_DROP_SEQ_LEN_LIMIT 5
+
+/* coroutine context */
+static void display_handle_stream_data(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+ SpiceStreamDataHeader *op = spice_msg_in_parsed(in);
+ display_stream *st;
+ guint32 mmtime;
+ int32_t latency;
+
+ g_return_if_fail(c != NULL);
+ g_return_if_fail(c->streams != NULL);
+ g_return_if_fail(c->nstreams > op->id);
+
+ st = c->streams[op->id];
+ mmtime = spice_session_get_mm_time(spice_channel_get_session(channel));
+
+ if (spice_msg_in_type(in) == SPICE_MSG_DISPLAY_STREAM_DATA_SIZED) {
+ CHANNEL_DEBUG(channel, "stream %d contains sized data", op->id);
+ }
+
+ if (op->multi_media_time == 0) {
+ g_critical("Received frame with invalid 0 timestamp! perhaps wrong graphic driver?");
+ op->multi_media_time = mmtime + 100; /* workaround... */
+ }
+
+ if (!st->num_input_frames) {
+ st->first_frame_mm_time = op->multi_media_time;
+ }
+ st->num_input_frames++;
+
+ latency = op->multi_media_time - mmtime;
+ if (latency < 0) {
+ CHANNEL_DEBUG(channel, "stream data too late by %u ms (ts: %u, mmtime: %u), dropping",
+ mmtime - op->multi_media_time, op->multi_media_time, mmtime);
+ st->arrive_late_time += mmtime - op->multi_media_time;
+ st->num_drops_on_receive++;
+
+ if (!st->cur_drops_seq_stats.len) {
+ st->cur_drops_seq_stats.start_mm_time = op->multi_media_time;
+ }
+ st->cur_drops_seq_stats.len++;
+ st->playback_sync_drops_seq_len++;
+ } else {
+ CHANNEL_DEBUG(channel, "video latency: %d", latency);
+ spice_msg_in_ref(in);
+ display_stream_test_frames_mm_time_reset(st, in, mmtime);
+ g_queue_push_tail(st->msgq, in);
+ while (!display_stream_schedule(st)) {
+ }
+ if (st->cur_drops_seq_stats.len) {
+ st->cur_drops_seq_stats.duration = op->multi_media_time -
+ st->cur_drops_seq_stats.start_mm_time;
+ g_array_append_val(st->drops_seqs_stats_arr, st->cur_drops_seq_stats);
+ memset(&st->cur_drops_seq_stats, 0, sizeof(st->cur_drops_seq_stats));
+ st->num_drops_seqs++;
+ }
+ st->playback_sync_drops_seq_len = 0;
+ }
+ if (c->enable_adaptive_streaming) {
+ display_update_stream_report(SPICE_DISPLAY_CHANNEL(channel), op->id,
+ op->multi_media_time, latency);
+ if (st->playback_sync_drops_seq_len >= STREAM_PLAYBACK_SYNC_DROP_SEQ_LEN_LIMIT) {
+ spice_session_sync_playback_latency(spice_channel_get_session(channel));
+ st->playback_sync_drops_seq_len = 0;
+ }
+ }
+}
+
+/* coroutine context */
+static void display_handle_stream_clip(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+ SpiceMsgDisplayStreamClip *op = spice_msg_in_parsed(in);
+ display_stream *st;
+
+ g_return_if_fail(c != NULL);
+ g_return_if_fail(c->streams != NULL);
+ g_return_if_fail(c->nstreams > op->id);
+
+ st = c->streams[op->id];
+
+ if (st->msg_clip) {
+ spice_msg_in_unref(st->msg_clip);
+ }
+ spice_msg_in_ref(in);
+ st->msg_clip = in;
+ st->clip = &op->clip;
+ display_update_stream_region(st);
+}
+
+static void _msg_in_unref_func(gpointer data, gpointer user_data)
+{
+ spice_msg_in_unref(data);
+}
+
+static void destroy_stream(SpiceChannel *channel, int id)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+ display_stream *st;
+ guint64 drops_duration_total = 0;
+ guint32 num_out_frames;
+ int i;
+
+ g_return_if_fail(c != NULL);
+ g_return_if_fail(c->streams != NULL);
+ g_return_if_fail(c->nstreams > id);
+
+ st = c->streams[id];
+ if (!st)
+ return;
+
+ num_out_frames = st->num_input_frames - st->num_drops_on_receive - st->num_drops_on_playback;
+ CHANNEL_DEBUG(channel, "%s: id=%d #in-frames=%d out/in=%.2f "
+ "#drops-on-receive=%d avg-late-time(ms)=%.2f "
+ "#drops-on-playback=%d", __FUNCTION__,
+ id,
+ st->num_input_frames,
+ num_out_frames / (double)st->num_input_frames,
+ st->num_drops_on_receive,
+ st->num_drops_on_receive ? st->arrive_late_time / ((double)st->num_drops_on_receive): 0,
+ st->num_drops_on_playback);
+ if (st->num_drops_seqs) {
+ CHANNEL_DEBUG(channel, "%s: #drops-sequences=%u ==>", __FUNCTION__, st->num_drops_seqs);
+ }
+ for (i = 0; i < st->num_drops_seqs; i++) {
+ drops_sequence_stats *stats = &g_array_index(st->drops_seqs_stats_arr,
+ drops_sequence_stats,
+ i);
+ drops_duration_total += stats->duration;
+ CHANNEL_DEBUG(channel, "%s: \t len=%u start-ms=%u duration-ms=%u", __FUNCTION__,
+ stats->len,
+ stats->start_mm_time - st->first_frame_mm_time,
+ stats->duration);
+ }
+ if (st->num_drops_seqs) {
+ CHANNEL_DEBUG(channel, "%s: drops-total-duration=%"G_GUINT64_FORMAT" ==>", __FUNCTION__, drops_duration_total);
+ }
+
+ g_array_free(st->drops_seqs_stats_arr, TRUE);
+
+ switch (st->codec) {
+ case SPICE_VIDEO_CODEC_TYPE_MJPEG:
+ stream_mjpeg_cleanup(st);
+ break;
+ }
+
+ if (st->msg_clip)
+ spice_msg_in_unref(st->msg_clip);
+ spice_msg_in_unref(st->msg_create);
+
+ g_queue_foreach(st->msgq, _msg_in_unref_func, NULL);
+ g_queue_free(st->msgq);
+ if (st->timeout != 0)
+ g_source_remove(st->timeout);
+ g_free(st);
+ c->streams[id] = NULL;
+}
+
+static void clear_streams(SpiceChannel *channel)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+ int i;
+
+ for (i = 0; i < c->nstreams; i++) {
+ destroy_stream(channel, i);
+ }
+ g_free(c->streams);
+ c->streams = NULL;
+ c->nstreams = 0;
+}
+
+/* coroutine context */
+static void display_handle_stream_destroy(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgDisplayStreamDestroy *op = spice_msg_in_parsed(in);
+
+ g_return_if_fail(op != NULL);
+ CHANNEL_DEBUG(channel, "%s: id %d", __FUNCTION__, op->id);
+ destroy_stream(channel, op->id);
+}
+
+/* coroutine context */
+static void display_handle_stream_destroy_all(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ clear_streams(channel);
+}
+
+/* coroutine context */
+static void display_handle_stream_activate_report(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+ SpiceMsgDisplayStreamActivateReport *op = spice_msg_in_parsed(in);
+ display_stream *st;
+
+ g_return_if_fail(c != NULL);
+ g_return_if_fail(c->streams != NULL);
+ g_return_if_fail(c->nstreams > op->stream_id);
+
+ st = c->streams[op->stream_id];
+ g_return_if_fail(st != NULL);
+
+ st->report_is_active = TRUE;
+ st->report_id = op->unique_id;
+ st->report_max_window = op->max_window_size;
+ st->report_timeout = op->timeout_ms * 1000;
+ st->report_start_time = 0;
+ st->report_start_frame_time = 0;
+ st->report_num_frames = 0;
+ st->report_num_drops = 0;
+ st->report_drops_seq_len = 0;
+}
+
+/* ------------------------------------------------------------------ */
+
+/* coroutine context */
+static void display_handle_draw_fill(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgDisplayDrawFill *op = spice_msg_in_parsed(in);
+ DRAW(fill);
+}
+
+/* coroutine context */
+static void display_handle_draw_opaque(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgDisplayDrawOpaque *op = spice_msg_in_parsed(in);
+ DRAW(opaque);
+}
+
+/* coroutine context */
+static void display_handle_draw_copy(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgDisplayDrawCopy *op = spice_msg_in_parsed(in);
+ DRAW(copy);
+}
+
+/* coroutine context */
+static void display_handle_draw_blend(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgDisplayDrawBlend *op = spice_msg_in_parsed(in);
+ DRAW(blend);
+}
+
+/* coroutine context */
+static void display_handle_draw_blackness(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgDisplayDrawBlackness *op = spice_msg_in_parsed(in);
+ DRAW(blackness);
+}
+
+static void display_handle_draw_whiteness(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgDisplayDrawWhiteness *op = spice_msg_in_parsed(in);
+ DRAW(whiteness);
+}
+
+/* coroutine context */
+static void display_handle_draw_invers(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgDisplayDrawInvers *op = spice_msg_in_parsed(in);
+ DRAW(invers);
+}
+
+/* coroutine context */
+static void display_handle_draw_rop3(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgDisplayDrawRop3 *op = spice_msg_in_parsed(in);
+ DRAW(rop3);
+}
+
+/* coroutine context */
+static void display_handle_draw_stroke(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgDisplayDrawStroke *op = spice_msg_in_parsed(in);
+ DRAW(stroke);
+}
+
+/* coroutine context */
+static void display_handle_draw_text(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgDisplayDrawText *op = spice_msg_in_parsed(in);
+ DRAW(text);
+}
+
+/* coroutine context */
+static void display_handle_draw_transparent(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgDisplayDrawTransparent *op = spice_msg_in_parsed(in);
+ DRAW(transparent);
+}
+
+/* coroutine context */
+static void display_handle_draw_alpha_blend(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgDisplayDrawAlphaBlend *op = spice_msg_in_parsed(in);
+ DRAW(alpha_blend);
+}
+
+/* coroutine context */
+static void display_handle_draw_composite(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgDisplayDrawComposite *op = spice_msg_in_parsed(in);
+ DRAW(composite);
+}
+
+/* coroutine context */
+static void display_handle_surface_create(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+ SpiceMsgSurfaceCreate *create = spice_msg_in_parsed(in);
+ display_surface *surface = g_slice_new0(display_surface);
+
+ surface->surface_id = create->surface_id;
+ surface->format = create->format;
+ surface->width = create->width;
+ surface->height = create->height;
+ surface->stride = create->width * 4;
+ surface->size = surface->height * surface->stride;
+
+ if (create->flags & SPICE_SURFACE_FLAGS_PRIMARY) {
+ SPICE_DEBUG("primary flags: %d", create->flags);
+ surface->primary = true;
+ create_canvas(channel, surface);
+ if (c->mark_false_event_id != 0) {
+ g_source_remove(c->mark_false_event_id);
+ c->mark_false_event_id = FALSE;
+ }
+ } else {
+ surface->primary = false;
+ create_canvas(channel, surface);
+ }
+}
+
+static gboolean display_mark_false(gpointer data)
+{
+ SpiceChannel *channel = data;
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+
+ c->mark = FALSE;
+ g_signal_emit(channel, signals[SPICE_DISPLAY_MARK], 0, FALSE);
+
+ c->mark_false_event_id = 0;
+ return FALSE;
+}
+
+/* coroutine context */
+static void display_handle_surface_destroy(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgSurfaceDestroy *destroy = spice_msg_in_parsed(in);
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+ display_surface *surface;
+
+ g_return_if_fail(destroy != NULL);
+
+ surface = find_surface(c, destroy->surface_id);
+ if (surface == NULL) {
+ /* this is not a problem in spicec, it happens as well and returns.. */
+ /* g_warn_if_reached(); */
+ return;
+ }
+ if (surface->primary) {
+ int id = spice_channel_get_channel_id(channel);
+ CHANNEL_DEBUG(channel, "%d: FIXME primary destroy, but is display really disabled?", id);
+ /* this is done with a timeout in spicec as well, it's *ugly* */
+ if (id != 0 && c->mark_false_event_id == 0) {
+ c->mark_false_event_id = g_timeout_add_seconds(1, display_mark_false, channel);
+ }
+ c->primary = NULL;
+ g_coroutine_signal_emit(channel, signals[SPICE_DISPLAY_PRIMARY_DESTROY], 0);
+ }
+
+ g_hash_table_remove(c->surfaces, GINT_TO_POINTER(surface->surface_id));
+}
+
+#define CLAMP_CHECK(x, low, high) (((x) > (high)) ? TRUE : (((x) < (low)) ? TRUE : FALSE))
+
+/* coroutine context */
+static void display_handle_monitors_config(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgDisplayMonitorsConfig *config = spice_msg_in_parsed(in);
+ SpiceDisplayChannelPrivate *c = SPICE_DISPLAY_CHANNEL(channel)->priv;
+ guint i;
+
+ g_return_if_fail(config != NULL);
+ g_return_if_fail(config->count > 0);
+
+ CHANNEL_DEBUG(channel, "monitors config: n: %d/%d", config->count, config->max_allowed);
+
+ c->monitors_max = config->max_allowed;
+ if (CLAMP_CHECK(c->monitors_max, 1, MONITORS_MAX)) {
+ g_warning("MonitorConfig max_allowed is not within permitted range, clamping");
+ c->monitors_max = CLAMP(c->monitors_max, 1, MONITORS_MAX);
+ }
+
+ if (CLAMP_CHECK(config->count, 1, c->monitors_max)) {
+ g_warning("MonitorConfig count is not within permitted range, clamping");
+ config->count = CLAMP(config->count, 1, c->monitors_max);
+ }
+
+ c->monitors = g_array_set_size(c->monitors, config->count);
+
+ for (i = 0; i < config->count; i++) {
+ SpiceDisplayMonitorConfig *mc = &g_array_index(c->monitors, SpiceDisplayMonitorConfig, i);
+ SpiceHead *head = &config->heads[i];
+ CHANNEL_DEBUG(channel, "monitor id: %u, surface id: %u, +%u+%u-%ux%u",
+ head->id, head->surface_id,
+ head->x, head->y, head->width, head->height);
+ mc->id = head->id;
+ mc->surface_id = head->surface_id;
+ mc->x = head->x;
+ mc->y = head->y;
+ mc->width = head->width;
+ mc->height = head->height;
+ }
+
+ g_coroutine_object_notify(G_OBJECT(channel), "monitors");
+}
+
+static void channel_set_handlers(SpiceChannelClass *klass)
+{
+ static const spice_msg_handler handlers[] = {
+ [ SPICE_MSG_DISPLAY_MODE ] = display_handle_mode,
+ [ SPICE_MSG_DISPLAY_MARK ] = display_handle_mark,
+ [ SPICE_MSG_DISPLAY_RESET ] = display_handle_reset,
+ [ SPICE_MSG_DISPLAY_COPY_BITS ] = display_handle_copy_bits,
+ [ SPICE_MSG_DISPLAY_INVAL_LIST ] = display_handle_inv_list,
+ [ SPICE_MSG_DISPLAY_INVAL_ALL_PIXMAPS ] = display_handle_inv_pixmap_all,
+ [ SPICE_MSG_DISPLAY_INVAL_PALETTE ] = display_handle_inv_palette,
+ [ SPICE_MSG_DISPLAY_INVAL_ALL_PALETTES ] = display_handle_inv_palette_all,
+
+ [ SPICE_MSG_DISPLAY_STREAM_CREATE ] = display_handle_stream_create,
+ [ SPICE_MSG_DISPLAY_STREAM_DATA ] = display_handle_stream_data,
+ [ SPICE_MSG_DISPLAY_STREAM_CLIP ] = display_handle_stream_clip,
+ [ SPICE_MSG_DISPLAY_STREAM_DESTROY ] = display_handle_stream_destroy,
+ [ SPICE_MSG_DISPLAY_STREAM_DESTROY_ALL ] = display_handle_stream_destroy_all,
+ [ SPICE_MSG_DISPLAY_STREAM_DATA_SIZED ] = display_handle_stream_data,
+ [ SPICE_MSG_DISPLAY_STREAM_ACTIVATE_REPORT ] = display_handle_stream_activate_report,
+
+ [ SPICE_MSG_DISPLAY_DRAW_FILL ] = display_handle_draw_fill,
+ [ SPICE_MSG_DISPLAY_DRAW_OPAQUE ] = display_handle_draw_opaque,
+ [ SPICE_MSG_DISPLAY_DRAW_COPY ] = display_handle_draw_copy,
+ [ SPICE_MSG_DISPLAY_DRAW_BLEND ] = display_handle_draw_blend,
+ [ SPICE_MSG_DISPLAY_DRAW_BLACKNESS ] = display_handle_draw_blackness,
+ [ SPICE_MSG_DISPLAY_DRAW_WHITENESS ] = display_handle_draw_whiteness,
+ [ SPICE_MSG_DISPLAY_DRAW_INVERS ] = display_handle_draw_invers,
+ [ SPICE_MSG_DISPLAY_DRAW_ROP3 ] = display_handle_draw_rop3,
+ [ SPICE_MSG_DISPLAY_DRAW_STROKE ] = display_handle_draw_stroke,
+ [ SPICE_MSG_DISPLAY_DRAW_TEXT ] = display_handle_draw_text,
+ [ SPICE_MSG_DISPLAY_DRAW_TRANSPARENT ] = display_handle_draw_transparent,
+ [ SPICE_MSG_DISPLAY_DRAW_ALPHA_BLEND ] = display_handle_draw_alpha_blend,
+ [ SPICE_MSG_DISPLAY_DRAW_COMPOSITE ] = display_handle_draw_composite,
+
+ [ SPICE_MSG_DISPLAY_SURFACE_CREATE ] = display_handle_surface_create,
+ [ SPICE_MSG_DISPLAY_SURFACE_DESTROY ] = display_handle_surface_destroy,
+
+ [ SPICE_MSG_DISPLAY_MONITORS_CONFIG ] = display_handle_monitors_config,
+ };
+
+ spice_channel_set_handlers(klass, handlers, G_N_ELEMENTS(handlers));
+}
diff --git a/src/channel-display.h b/src/channel-display.h
new file mode 100644
index 0000000..88e60d9
--- /dev/null
+++ b/src/channel-display.h
@@ -0,0 +1,102 @@
+/* -*- 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_CLIENT_DISPLAY_CHANNEL_H__
+#define __SPICE_CLIENT_DISPLAY_CHANNEL_H__
+
+#include "spice-client.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_DISPLAY_CHANNEL (spice_display_channel_get_type())
+#define SPICE_DISPLAY_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_DISPLAY_CHANNEL, SpiceDisplayChannel))
+#define SPICE_DISPLAY_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_DISPLAY_CHANNEL, SpiceDisplayChannelClass))
+#define SPICE_IS_DISPLAY_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_DISPLAY_CHANNEL))
+#define SPICE_IS_DISPLAY_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_DISPLAY_CHANNEL))
+#define SPICE_DISPLAY_CHANNEL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_DISPLAY_CHANNEL, SpiceDisplayChannelClass))
+
+typedef struct _SpiceDisplayChannel SpiceDisplayChannel;
+typedef struct _SpiceDisplayChannelClass SpiceDisplayChannelClass;
+typedef struct _SpiceDisplayChannelPrivate SpiceDisplayChannelPrivate;
+
+typedef struct _SpiceDisplayMonitorConfig SpiceDisplayMonitorConfig;
+struct _SpiceDisplayMonitorConfig {
+ guint id;
+ guint surface_id;
+ guint x;
+ guint y;
+ guint width;
+ guint height;
+};
+
+typedef struct _SpiceDisplayPrimary SpiceDisplayPrimary;
+struct _SpiceDisplayPrimary {
+ enum SpiceSurfaceFmt format;
+ gint width;
+ gint height;
+ gint stride;
+ gint shmid;
+ guint8 *data;
+ gboolean marked;
+};
+
+/**
+ * SpiceDisplayChannel:
+ *
+ * The #SpiceDisplayChannel struct is opaque and should not be accessed directly.
+ */
+struct _SpiceDisplayChannel {
+ SpiceChannel parent;
+
+ /*< private >*/
+ SpiceDisplayChannelPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+/**
+ * SpiceDisplayChannelClass:
+ * @parent_class: Parent class.
+ * @display_primary_create: Signal class handler for the #SpiceDisplayChannel::display-primary-create signal.
+ * @display_primary_destroy: Signal class handler for the #SpiceDisplayChannel::display-primary-destroy signal.
+ * @display_invalidate: Signal class handler for the #SpiceDisplayChannel::display-invalidate signal.
+ * @display_mark: Signal class handler for the #SpiceDisplayChannel::display-mark signal.
+ *
+ * Class structure for #SpiceDisplayChannel.
+ */
+struct _SpiceDisplayChannelClass {
+ SpiceChannelClass parent_class;
+
+ /* signals */
+ void (*display_primary_create)(SpiceChannel *channel, gint format,
+ gint width, gint height, gint stride,
+ gint shmid, gpointer data);
+ void (*display_primary_destroy)(SpiceChannel *channel);
+ void (*display_invalidate)(SpiceChannel *channel,
+ gint x, gint y, gint w, gint h);
+ void (*display_mark)(SpiceChannel *channel,
+ gboolean mark);
+
+ /*< private >*/
+};
+
+GType spice_display_channel_get_type(void);
+gboolean spice_display_get_primary(SpiceChannel *channel, guint32 surface_id,
+ SpiceDisplayPrimary *primary);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_DISPLAY_CHANNEL_H__ */
diff --git a/src/channel-inputs.c b/src/channel-inputs.c
new file mode 100644
index 0000000..df1ffe1
--- /dev/null
+++ b/src/channel-inputs.c
@@ -0,0 +1,603 @@
+/* -*- 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-channel-priv.h"
+
+/**
+ * SECTION:channel-inputs
+ * @short_description: control the server mouse and keyboard
+ * @title: Inputs Channel
+ * @section_id:
+ * @see_also: #SpiceChannel, and the GTK widget #SpiceDisplay
+ * @stability: Stable
+ * @include: channel-inputs.h
+ *
+ * Spice supports sending keyboard key events and keyboard leds
+ * synchronization. The key events are sent using
+ * spice_inputs_key_press() and spice_inputs_key_release() using
+ * a modified variant of PC XT scancodes.
+ *
+ * Guest keyboard leds state can be manipulated with
+ * spice_inputs_set_key_locks(). When key lock change, a notification
+ * is emitted with #SpiceInputsChannel::inputs-modifiers signal.
+ */
+
+#define SPICE_INPUTS_CHANNEL_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_INPUTS_CHANNEL, SpiceInputsChannelPrivate))
+
+struct _SpiceInputsChannelPrivate {
+ int bs;
+ int dx, dy;
+ unsigned int x, y, dpy;
+ int motion_count;
+ int modifiers;
+ guint32 locks;
+};
+
+G_DEFINE_TYPE(SpiceInputsChannel, spice_inputs_channel, SPICE_TYPE_CHANNEL)
+
+/* Properties */
+enum {
+ PROP_0,
+ PROP_KEY_MODIFIERS,
+};
+
+/* Signals */
+enum {
+ SPICE_INPUTS_MODIFIERS,
+
+ SPICE_INPUTS_LAST_SIGNAL,
+};
+
+static guint signals[SPICE_INPUTS_LAST_SIGNAL];
+
+static void spice_inputs_channel_up(SpiceChannel *channel);
+static void spice_inputs_channel_reset(SpiceChannel *channel, gboolean migrating);
+static void channel_set_handlers(SpiceChannelClass *klass);
+
+/* ------------------------------------------------------------------ */
+
+static void spice_inputs_channel_init(SpiceInputsChannel *channel)
+{
+ channel->priv = SPICE_INPUTS_CHANNEL_GET_PRIVATE(channel);
+}
+
+static void spice_inputs_get_property(GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceInputsChannelPrivate *c = SPICE_INPUTS_CHANNEL(object)->priv;
+
+ switch (prop_id) {
+ case PROP_KEY_MODIFIERS:
+ g_value_set_int(value, c->modifiers);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_inputs_channel_finalize(GObject *obj)
+{
+ if (G_OBJECT_CLASS(spice_inputs_channel_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_inputs_channel_parent_class)->finalize(obj);
+}
+
+static void spice_inputs_channel_class_init(SpiceInputsChannelClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS(klass);
+
+ gobject_class->finalize = spice_inputs_channel_finalize;
+ gobject_class->get_property = spice_inputs_get_property;
+ channel_class->channel_up = spice_inputs_channel_up;
+ channel_class->channel_reset = spice_inputs_channel_reset;
+
+ g_object_class_install_property
+ (gobject_class, PROP_KEY_MODIFIERS,
+ g_param_spec_int("key-modifiers",
+ "Key modifiers",
+ "Guest keyboard lock/led state",
+ 0, INT_MAX, 0,
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_NAME |
+ G_PARAM_STATIC_NICK |
+ G_PARAM_STATIC_BLURB));
+
+ /**
+ * SpiceInputsChannel::inputs-modifier:
+ * @display: the #SpiceInputsChannel that emitted the signal
+ *
+ * The #SpiceInputsChannel::inputs-modifier signal is emitted when
+ * the guest keyboard locks are changed. You can read the current
+ * state from #SpiceInputsChannel:key-modifiers property.
+ **/
+ /* TODO: use notify instead? */
+ signals[SPICE_INPUTS_MODIFIERS] =
+ g_signal_new("inputs-modifiers",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceInputsChannelClass, inputs_modifiers),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
+
+ g_type_class_add_private(klass, sizeof(SpiceInputsChannelPrivate));
+ channel_set_handlers(SPICE_CHANNEL_CLASS(klass));
+}
+
+/* ------------------------------------------------------------------ */
+
+static SpiceMsgOut* mouse_motion(SpiceInputsChannel *channel)
+{
+ SpiceInputsChannelPrivate *c = channel->priv;
+ SpiceMsgcMouseMotion motion;
+ SpiceMsgOut *msg;
+
+ if (!c->dx && !c->dy)
+ return NULL;
+
+ motion.buttons_state = c->bs;
+ motion.dx = c->dx;
+ motion.dy = c->dy;
+ msg = spice_msg_out_new(SPICE_CHANNEL(channel),
+ SPICE_MSGC_INPUTS_MOUSE_MOTION);
+ msg->marshallers->msgc_inputs_mouse_motion(msg->marshaller, &motion);
+
+ c->motion_count++;
+ c->dx = 0;
+ c->dy = 0;
+
+ return msg;
+}
+
+static SpiceMsgOut* mouse_position(SpiceInputsChannel *channel)
+{
+ SpiceInputsChannelPrivate *c = channel->priv;
+ SpiceMsgcMousePosition position;
+ SpiceMsgOut *msg;
+
+ if (c->dpy == -1)
+ return NULL;
+
+ /* CHANNEL_DEBUG(channel, "%s: +%d+%d", __FUNCTION__, c->x, c->y); */
+ position.buttons_state = c->bs;
+ position.x = c->x;
+ position.y = c->y;
+ position.display_id = c->dpy;
+ msg = spice_msg_out_new(SPICE_CHANNEL(channel),
+ SPICE_MSGC_INPUTS_MOUSE_POSITION);
+ msg->marshallers->msgc_inputs_mouse_position(msg->marshaller, &position);
+
+ c->motion_count++;
+ c->dpy = -1;
+
+ return msg;
+}
+
+/* main context */
+static void send_position(SpiceInputsChannel *channel)
+{
+ SpiceMsgOut *msg;
+
+ if (spice_channel_get_read_only(SPICE_CHANNEL(channel)))
+ return;
+
+ msg = mouse_position(channel);
+ if (!msg) /* if no motion */
+ return;
+
+ spice_msg_out_send(msg);
+}
+
+/* main context */
+static void send_motion(SpiceInputsChannel *channel)
+{
+ SpiceMsgOut *msg;
+
+ if (spice_channel_get_read_only(SPICE_CHANNEL(channel)))
+ return;
+
+ msg = mouse_motion(channel);
+ if (!msg) /* if no motion */
+ return;
+
+ spice_msg_out_send(msg);
+}
+
+/* coroutine context */
+static void inputs_handle_init(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceInputsChannelPrivate *c = SPICE_INPUTS_CHANNEL(channel)->priv;
+ SpiceMsgInputsInit *init = spice_msg_in_parsed(in);
+
+ c->modifiers = init->keyboard_modifiers;
+ g_coroutine_signal_emit(channel, signals[SPICE_INPUTS_MODIFIERS], 0);
+}
+
+/* coroutine context */
+static void inputs_handle_modifiers(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceInputsChannelPrivate *c = SPICE_INPUTS_CHANNEL(channel)->priv;
+ SpiceMsgInputsKeyModifiers *modifiers = spice_msg_in_parsed(in);
+
+ c->modifiers = modifiers->modifiers;
+ g_coroutine_signal_emit(channel, signals[SPICE_INPUTS_MODIFIERS], 0);
+}
+
+/* coroutine context */
+static void inputs_handle_ack(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceInputsChannelPrivate *c = SPICE_INPUTS_CHANNEL(channel)->priv;
+ SpiceMsgOut *msg;
+
+ c->motion_count -= SPICE_INPUT_MOTION_ACK_BUNCH;
+
+ msg = mouse_motion(SPICE_INPUTS_CHANNEL(channel));
+ if (msg) { /* if no motion, msg == NULL */
+ spice_msg_out_send_internal(msg);
+ }
+
+ msg = mouse_position(SPICE_INPUTS_CHANNEL(channel));
+ if (msg) {
+ spice_msg_out_send_internal(msg);
+ }
+}
+
+static void channel_set_handlers(SpiceChannelClass *klass)
+{
+ static const spice_msg_handler handlers[] = {
+ [ SPICE_MSG_INPUTS_INIT ] = inputs_handle_init,
+ [ SPICE_MSG_INPUTS_KEY_MODIFIERS ] = inputs_handle_modifiers,
+ [ SPICE_MSG_INPUTS_MOUSE_MOTION_ACK ] = inputs_handle_ack,
+ };
+
+ spice_channel_set_handlers(klass, handlers, G_N_ELEMENTS(handlers));
+}
+
+/**
+ * spice_inputs_motion:
+ * @channel:
+ * @dx: delta X mouse coordinates
+ * @dy: delta Y mouse coordinates
+ * @button_state: SPICE_MOUSE_BUTTON_MASK flags
+ *
+ * Change mouse position (used in SPICE_MOUSE_MODE_CLIENT).
+ **/
+void spice_inputs_motion(SpiceInputsChannel *channel, gint dx, gint dy,
+ gint button_state)
+{
+ SpiceInputsChannelPrivate *c;
+
+ g_return_if_fail(channel != NULL);
+ g_return_if_fail(SPICE_CHANNEL(channel)->priv->state != SPICE_CHANNEL_STATE_UNCONNECTED);
+ if (SPICE_CHANNEL(channel)->priv->state != SPICE_CHANNEL_STATE_READY)
+ return;
+
+ if (dx == 0 && dy == 0)
+ return;
+
+ c = channel->priv;
+ c->bs = button_state;
+ c->dx += dx;
+ c->dy += dy;
+
+ if (c->motion_count < SPICE_INPUT_MOTION_ACK_BUNCH * 2) {
+ send_motion(channel);
+ }
+}
+
+/**
+ * spice_inputs_position:
+ * @channel:
+ * @x: X mouse coordinates
+ * @y: Y mouse coordinates
+ * @display: display channel id
+ * @button_state: SPICE_MOUSE_BUTTON_MASK flags
+ *
+ * Change mouse position (used in SPICE_MOUSE_MODE_CLIENT).
+ **/
+void spice_inputs_position(SpiceInputsChannel *channel, gint x, gint y,
+ gint display, gint button_state)
+{
+ SpiceInputsChannelPrivate *c;
+
+ g_return_if_fail(channel != NULL);
+
+ if (SPICE_CHANNEL(channel)->priv->state != SPICE_CHANNEL_STATE_READY)
+ return;
+
+ c = channel->priv;
+ c->bs = button_state;
+ c->x = x;
+ c->y = y;
+ c->dpy = display;
+
+ if (c->motion_count < SPICE_INPUT_MOTION_ACK_BUNCH * 2) {
+ send_position(channel);
+ } else {
+ CHANNEL_DEBUG(channel, "over SPICE_INPUT_MOTION_ACK_BUNCH * 2, dropping");
+ }
+}
+
+/**
+ * spice_inputs_button_press:
+ * @channel:
+ * @button: a SPICE_MOUSE_BUTTON
+ * @button_state: SPICE_MOUSE_BUTTON_MASK flags
+ *
+ * Press a mouse button.
+ **/
+void spice_inputs_button_press(SpiceInputsChannel *channel, gint button,
+ gint button_state)
+{
+ SpiceInputsChannelPrivate *c;
+ SpiceMsgcMousePress press;
+ SpiceMsgOut *msg;
+
+ g_return_if_fail(channel != NULL);
+
+ if (SPICE_CHANNEL(channel)->priv->state != SPICE_CHANNEL_STATE_READY)
+ return;
+ if (spice_channel_get_read_only(SPICE_CHANNEL(channel)))
+ return;
+
+ c = channel->priv;
+ switch (button) {
+ case SPICE_MOUSE_BUTTON_LEFT:
+ button_state |= SPICE_MOUSE_BUTTON_MASK_LEFT;
+ break;
+ case SPICE_MOUSE_BUTTON_MIDDLE:
+ button_state |= SPICE_MOUSE_BUTTON_MASK_MIDDLE;
+ break;
+ case SPICE_MOUSE_BUTTON_RIGHT:
+ button_state |= SPICE_MOUSE_BUTTON_MASK_RIGHT;
+ break;
+ }
+
+ c->bs = button_state;
+ send_motion(channel);
+ send_position(channel);
+
+ msg = spice_msg_out_new(SPICE_CHANNEL(channel),
+ SPICE_MSGC_INPUTS_MOUSE_PRESS);
+ press.button = button;
+ press.buttons_state = button_state;
+ msg->marshallers->msgc_inputs_mouse_press(msg->marshaller, &press);
+ spice_msg_out_send(msg);
+}
+
+/**
+ * spice_inputs_button_release:
+ * @channel:
+ * @button: a SPICE_MOUSE_BUTTON
+ * @button_state: SPICE_MOUSE_BUTTON_MASK flags
+ *
+ * Release a button.
+ **/
+void spice_inputs_button_release(SpiceInputsChannel *channel, gint button,
+ gint button_state)
+{
+ SpiceInputsChannelPrivate *c;
+ SpiceMsgcMouseRelease release;
+ SpiceMsgOut *msg;
+
+ g_return_if_fail(channel != NULL);
+
+ if (SPICE_CHANNEL(channel)->priv->state != SPICE_CHANNEL_STATE_READY)
+ return;
+ if (spice_channel_get_read_only(SPICE_CHANNEL(channel)))
+ return;
+
+ c = channel->priv;
+ switch (button) {
+ case SPICE_MOUSE_BUTTON_LEFT:
+ button_state &= ~SPICE_MOUSE_BUTTON_MASK_LEFT;
+ break;
+ case SPICE_MOUSE_BUTTON_MIDDLE:
+ button_state &= ~SPICE_MOUSE_BUTTON_MASK_MIDDLE;
+ break;
+ case SPICE_MOUSE_BUTTON_RIGHT:
+ button_state &= ~SPICE_MOUSE_BUTTON_MASK_RIGHT;
+ break;
+ }
+
+ c->bs = button_state;
+ send_motion(channel);
+ send_position(channel);
+
+ msg = spice_msg_out_new(SPICE_CHANNEL(channel),
+ SPICE_MSGC_INPUTS_MOUSE_RELEASE);
+ release.button = button;
+ release.buttons_state = button_state;
+ msg->marshallers->msgc_inputs_mouse_release(msg->marshaller, &release);
+ spice_msg_out_send(msg);
+}
+
+/**
+ * spice_inputs_key_press:
+ * @channel:
+ * @scancode: a PC XT (set 1) key scancode. For scancodes with an %0xe0
+ * prefix, drop the prefix and OR the scancode with %0x100.
+ *
+ * Press a key.
+ **/
+void spice_inputs_key_press(SpiceInputsChannel *channel, guint scancode)
+{
+ SpiceMsgcKeyDown down;
+ SpiceMsgOut *msg;
+
+ g_return_if_fail(channel != NULL);
+ g_return_if_fail(SPICE_CHANNEL(channel)->priv->state != SPICE_CHANNEL_STATE_UNCONNECTED);
+ if (SPICE_CHANNEL(channel)->priv->state != SPICE_CHANNEL_STATE_READY)
+ return;
+ if (spice_channel_get_read_only(SPICE_CHANNEL(channel)))
+ return;
+
+ down.code = spice_make_scancode(scancode, FALSE);
+ msg = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_INPUTS_KEY_DOWN);
+ msg->marshallers->msgc_inputs_key_down(msg->marshaller, &down);
+ spice_msg_out_send(msg);
+}
+
+/**
+ * spice_inputs_key_release:
+ * @channel:
+ * @scancode: a PC XT (set 1) key scancode. For scancodes with an %0xe0
+ * prefix, drop the prefix and OR the scancode with %0x100.
+ *
+ * Release a key.
+ **/
+void spice_inputs_key_release(SpiceInputsChannel *channel, guint scancode)
+{
+ SpiceMsgcKeyUp up;
+ SpiceMsgOut *msg;
+
+ g_return_if_fail(channel != NULL);
+ g_return_if_fail(SPICE_CHANNEL(channel)->priv->state != SPICE_CHANNEL_STATE_UNCONNECTED);
+ if (SPICE_CHANNEL(channel)->priv->state != SPICE_CHANNEL_STATE_READY)
+ return;
+ if (spice_channel_get_read_only(SPICE_CHANNEL(channel)))
+ return;
+
+ up.code = spice_make_scancode(scancode, TRUE);
+ msg = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_INPUTS_KEY_UP);
+ msg->marshallers->msgc_inputs_key_up(msg->marshaller, &up);
+ spice_msg_out_send(msg);
+}
+
+/**
+ * spice_inputs_key_press_and_release:
+ * @channel:
+ * @scancode: a PC XT (set 1) key scancode. For scancodes with an %0xe0
+ * prefix, drop the prefix and OR the scancode with %0x100.
+ *
+ * Press and release a key event atomically (in the same message).
+ *
+ * Since: 0.13
+ **/
+void spice_inputs_key_press_and_release(SpiceInputsChannel *input_channel, guint scancode)
+{
+ SpiceChannel *channel = SPICE_CHANNEL(input_channel);
+
+ g_return_if_fail(channel != NULL);
+ g_return_if_fail(channel->priv->state != SPICE_CHANNEL_STATE_UNCONNECTED);
+
+ if (channel->priv->state != SPICE_CHANNEL_STATE_READY)
+ return;
+ if (spice_channel_get_read_only(channel))
+ return;
+
+ if (spice_channel_test_capability(channel, SPICE_INPUTS_CAP_KEY_SCANCODE)) {
+ SpiceMsgOut *msg;
+ guint16 code;
+ guint8 *buf;
+
+ msg = spice_msg_out_new(channel, SPICE_MSGC_INPUTS_KEY_SCANCODE);
+ if (scancode < 0x100) {
+ buf = (guint8*)spice_marshaller_reserve_space(msg->marshaller, 2);
+ buf[0] = spice_make_scancode(scancode, FALSE);
+ buf[1] = spice_make_scancode(scancode, TRUE);
+ } else {
+ buf = (guint8*)spice_marshaller_reserve_space(msg->marshaller, 4);
+ code = spice_make_scancode(scancode, FALSE);
+ buf[0] = code & 0xff;
+ buf[1] = code >> 8;
+ code = spice_make_scancode(scancode, TRUE);
+ buf[2] = code & 0xff;
+ buf[3] = code >> 8;
+ }
+ spice_msg_out_send(msg);
+ } else {
+ CHANNEL_DEBUG(channel, "The server doesn't support atomic press and release");
+ spice_inputs_key_press(input_channel, scancode);
+ spice_inputs_key_release(input_channel, scancode);
+ }
+}
+
+/* main or coroutine context */
+static SpiceMsgOut* set_key_locks(SpiceInputsChannel *channel, guint locks)
+{
+ SpiceMsgcKeyModifiers modifiers;
+ SpiceMsgOut *msg;
+ SpiceInputsChannelPrivate *ic;
+ SpiceChannelPrivate *c;
+
+ g_return_val_if_fail(SPICE_IS_INPUTS_CHANNEL(channel), NULL);
+
+ ic = channel->priv;
+ c = SPICE_CHANNEL(channel)->priv;
+
+ ic->locks = locks;
+ if (c->state != SPICE_CHANNEL_STATE_READY)
+ return NULL;
+
+ msg = spice_msg_out_new(SPICE_CHANNEL(channel),
+ SPICE_MSGC_INPUTS_KEY_MODIFIERS);
+ modifiers.modifiers = locks;
+ msg->marshallers->msgc_inputs_key_modifiers(msg->marshaller, &modifiers);
+ return msg;
+}
+
+/**
+ * spice_inputs_set_key_locks:
+ * @channel:
+ * @locks: #SpiceInputsLock modifiers flags
+ *
+ * Set the keyboard locks on the guest (Caps, Num, Scroll..)
+ **/
+void spice_inputs_set_key_locks(SpiceInputsChannel *channel, guint locks)
+{
+ SpiceMsgOut *msg;
+
+ if (spice_channel_get_read_only(SPICE_CHANNEL(channel)))
+ return;
+
+ msg = set_key_locks(channel, locks);
+ if (!msg) /* you can set_key_locks() even if the channel is not ready */
+ return;
+
+ spice_msg_out_send(msg); /* main -> coroutine */
+}
+
+/* coroutine context */
+static void spice_inputs_channel_up(SpiceChannel *channel)
+{
+ SpiceInputsChannelPrivate *c = SPICE_INPUTS_CHANNEL(channel)->priv;
+ SpiceMsgOut *msg;
+
+ if (spice_channel_get_read_only(channel))
+ return;
+
+ msg = set_key_locks(SPICE_INPUTS_CHANNEL(channel), c->locks);
+ spice_msg_out_send_internal(msg);
+}
+
+static void spice_inputs_channel_reset(SpiceChannel *channel, gboolean migrating)
+{
+ SpiceInputsChannelPrivate *c = SPICE_INPUTS_CHANNEL(channel)->priv;
+ c->motion_count = 0;
+
+ SPICE_CHANNEL_CLASS(spice_inputs_channel_parent_class)->channel_reset(channel, migrating);
+}
diff --git a/src/channel-inputs.h b/src/channel-inputs.h
new file mode 100644
index 0000000..3179a76
--- /dev/null
+++ b/src/channel-inputs.h
@@ -0,0 +1,89 @@
+/* -*- 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_CLIENT_INPUTS_CHANNEL_H__
+#define __SPICE_CLIENT_INPUTS_CHANNEL_H__
+
+#include "spice-client.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_INPUTS_CHANNEL (spice_inputs_channel_get_type())
+#define SPICE_INPUTS_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_INPUTS_CHANNEL, SpiceInputsChannel))
+#define SPICE_INPUTS_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_INPUTS_CHANNEL, SpiceInputsChannelClass))
+#define SPICE_IS_INPUTS_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_INPUTS_CHANNEL))
+#define SPICE_IS_INPUTS_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_INPUTS_CHANNEL))
+#define SPICE_INPUTS_CHANNEL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_INPUTS_CHANNEL, SpiceInputsChannelClass))
+
+typedef struct _SpiceInputsChannel SpiceInputsChannel;
+typedef struct _SpiceInputsChannelClass SpiceInputsChannelClass;
+typedef struct _SpiceInputsChannelPrivate SpiceInputsChannelPrivate;
+
+typedef enum {
+ SPICE_INPUTS_SCROLL_LOCK = (1 << 0),
+ SPICE_INPUTS_NUM_LOCK = (1 << 1),
+ SPICE_INPUTS_CAPS_LOCK = (1 << 2)
+} SpiceInputsLock;
+
+/**
+ * SpiceInputsChannel:
+ *
+ * The #SpiceInputsChannel struct is opaque and should not be accessed directly.
+ */
+struct _SpiceInputsChannel {
+ SpiceChannel parent;
+
+ /*< private >*/
+ SpiceInputsChannelPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+/**
+ * SpiceInputsChannelClass:
+ * @parent_class: Parent class.
+ * @inputs_modifiers: Signal class handler for the #SpiceInputsChannel::inputs-modifiers signal.
+ *
+ * Class structure for #SpiceInputsChannel.
+ */
+struct _SpiceInputsChannelClass {
+ SpiceChannelClass parent_class;
+
+ /* signals */
+ void (*inputs_modifiers)(SpiceChannel *channel);
+
+ /*< private >*/
+ /* Do not add fields to this struct */
+};
+
+GType spice_inputs_channel_get_type(void);
+
+void spice_inputs_motion(SpiceInputsChannel *channel, gint dx, gint dy,
+ gint button_state);
+void spice_inputs_position(SpiceInputsChannel *channel, gint x, gint y,
+ gint display, gint button_state);
+void spice_inputs_button_press(SpiceInputsChannel *channel, gint button,
+ gint button_state);
+void spice_inputs_button_release(SpiceInputsChannel *channel, gint button,
+ gint button_state);
+void spice_inputs_key_press(SpiceInputsChannel *channel, guint scancode);
+void spice_inputs_key_release(SpiceInputsChannel *channel, guint scancode);
+void spice_inputs_set_key_locks(SpiceInputsChannel *channel, guint locks);
+void spice_inputs_key_press_and_release(SpiceInputsChannel *channel, guint scancode);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_INPUTS_CHANNEL_H__ */
diff --git a/src/channel-main.c b/src/channel-main.c
new file mode 100644
index 0000000..c55d097
--- /dev/null
+++ b/src/channel-main.c
@@ -0,0 +1,2993 @@
+/* -*- 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 <math.h>
+#include <spice/vd_agent.h>
+#include <common/rect.h>
+#include <glib/gstdio.h>
+
+#include "glib-compat.h"
+#include "spice-client.h"
+#include "spice-common.h"
+#include "spice-marshal.h"
+
+#include "spice-util-priv.h"
+#include "spice-channel-priv.h"
+#include "spice-session-priv.h"
+#include "spice-audio-priv.h"
+
+/**
+ * SECTION:channel-main
+ * @short_description: the main Spice channel
+ * @title: Main Channel
+ * @section_id:
+ * @see_also: #SpiceChannel, and the GTK widget #SpiceDisplay
+ * @stability: Stable
+ * @include: channel-main.h
+ *
+ * The main channel is the Spice session control channel. It handles
+ * communication initialization (channels list), migrations, mouse
+ * modes, multimedia time, and agent communication.
+ *
+ *
+ */
+
+#define SPICE_MAIN_CHANNEL_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_MAIN_CHANNEL, SpiceMainChannelPrivate))
+
+#define MAX_DISPLAY 16 /* Note must fit in a guint32, see monitors_align */
+
+typedef struct spice_migrate spice_migrate;
+
+#define FILE_XFER_CHUNK_SIZE (VD_AGENT_MAX_DATA_SIZE * 32)
+typedef struct SpiceFileXferTask {
+ uint32_t id;
+ gboolean pending;
+ GFile *file;
+ SpiceMainChannel *channel;
+ GFileInputStream *file_stream;
+ GFileCopyFlags flags;
+ GCancellable *cancellable;
+ GFileProgressCallback progress_callback;
+ gpointer progress_callback_data;
+ GAsyncReadyCallback callback;
+ gpointer user_data;
+ char buffer[FILE_XFER_CHUNK_SIZE];
+ uint64_t read_bytes;
+ uint64_t file_size;
+ GError *error;
+} SpiceFileXferTask;
+
+struct _SpiceMainChannelPrivate {
+ enum SpiceMouseMode mouse_mode;
+ bool agent_connected;
+ bool agent_caps_received;
+
+ gboolean agent_display_config_sent;
+ guint8 display_color_depth;
+ gboolean display_disable_wallpaper:1;
+ gboolean display_disable_font_smooth:1;
+ gboolean display_disable_animation:1;
+ gboolean disable_display_position:1;
+ gboolean disable_display_align:1;
+
+ int agent_tokens;
+ VDAgentMessage agent_msg; /* partial msg reconstruction */
+ guint8 *agent_msg_data;
+ guint agent_msg_pos;
+ uint8_t agent_msg_size;
+ uint32_t agent_caps[VD_AGENT_CAPS_SIZE];
+ struct {
+ int x;
+ int y;
+ int width;
+ int height;
+ gboolean enabled;
+ gboolean enabled_set;
+ } display[MAX_DISPLAY];
+ gint timer_id;
+ GQueue *agent_msg_queue;
+ GHashTable *file_xfer_tasks;
+ GSList *flushing;
+
+ guint switch_host_delayed_id;
+ guint migrate_delayed_id;
+ spice_migrate *migrate_data;
+ int max_clipboard;
+
+ gboolean agent_volume_playback_sync;
+ gboolean agent_volume_record_sync;
+ GCancellable *cancellable_volume_info;
+};
+
+struct spice_migrate {
+ struct coroutine *from;
+ SpiceMigrationDstInfo *info;
+ SpiceSession *session;
+ guint nchannels;
+ SpiceChannel *src_channel;
+ SpiceChannel *dst_channel;
+ bool do_seamless; /* used as input and output for the seamless migration handshake.
+ input: whether to send to the dest SPICE_MSGC_MAIN_MIGRATE_DST_DO_SEAMLESS
+ output: whether the dest approved seamless migration
+ (SPICE_MSG_MAIN_MIGRATE_DST_SEAMLESS_ACK/NACK)
+ */
+ uint32_t src_mig_version;
+};
+
+G_DEFINE_TYPE(SpiceMainChannel, spice_main_channel, SPICE_TYPE_CHANNEL)
+
+/* Properties */
+enum {
+ PROP_0,
+ PROP_MOUSE_MODE,
+ PROP_AGENT_CONNECTED,
+ PROP_AGENT_CAPS_0,
+ PROP_DISPLAY_DISABLE_WALLPAPER,
+ PROP_DISPLAY_DISABLE_FONT_SMOOTH,
+ PROP_DISPLAY_DISABLE_ANIMATION,
+ PROP_DISPLAY_COLOR_DEPTH,
+ PROP_DISABLE_DISPLAY_POSITION,
+ PROP_DISABLE_DISPLAY_ALIGN,
+ PROP_MAX_CLIPBOARD,
+};
+
+/* Signals */
+enum {
+ SPICE_MAIN_MOUSE_UPDATE,
+ SPICE_MAIN_AGENT_UPDATE,
+ SPICE_MAIN_CLIPBOARD,
+ SPICE_MAIN_CLIPBOARD_GRAB,
+ SPICE_MAIN_CLIPBOARD_REQUEST,
+ SPICE_MAIN_CLIPBOARD_RELEASE,
+ SPICE_MAIN_CLIPBOARD_SELECTION,
+ SPICE_MAIN_CLIPBOARD_SELECTION_GRAB,
+ SPICE_MAIN_CLIPBOARD_SELECTION_REQUEST,
+ SPICE_MAIN_CLIPBOARD_SELECTION_RELEASE,
+ SPICE_MIGRATION_STARTED,
+ SPICE_MAIN_LAST_SIGNAL,
+};
+
+static guint signals[SPICE_MAIN_LAST_SIGNAL];
+
+static void spice_main_handle_msg(SpiceChannel *channel, SpiceMsgIn *msg);
+static void channel_set_handlers(SpiceChannelClass *klass);
+static void agent_send_msg_queue(SpiceMainChannel *channel);
+static void agent_free_msg_queue(SpiceMainChannel *channel);
+static void migrate_channel_event_cb(SpiceChannel *channel, SpiceChannelEvent event,
+ gpointer data);
+static gboolean main_migrate_handshake_done(gpointer data);
+static void spice_main_channel_send_migration_handshake(SpiceChannel *channel);
+static void file_xfer_continue_read(SpiceFileXferTask *task);
+static void file_xfer_completed(SpiceFileXferTask *task, GError *error);
+static void file_xfer_flushed(SpiceMainChannel *channel, gboolean success);
+static void spice_main_set_max_clipboard(SpiceMainChannel *self, gint max);
+static void set_agent_connected(SpiceMainChannel *channel, gboolean connected);
+
+/* ------------------------------------------------------------------ */
+
+static const char *agent_msg_types[] = {
+ [ VD_AGENT_MOUSE_STATE ] = "mouse state",
+ [ VD_AGENT_MONITORS_CONFIG ] = "monitors config",
+ [ VD_AGENT_REPLY ] = "reply",
+ [ VD_AGENT_CLIPBOARD ] = "clipboard",
+ [ VD_AGENT_DISPLAY_CONFIG ] = "display config",
+ [ VD_AGENT_ANNOUNCE_CAPABILITIES ] = "announce caps",
+ [ VD_AGENT_CLIPBOARD_GRAB ] = "clipboard grab",
+ [ VD_AGENT_CLIPBOARD_REQUEST ] = "clipboard request",
+ [ VD_AGENT_CLIPBOARD_RELEASE ] = "clipboard release",
+ [ VD_AGENT_AUDIO_VOLUME_SYNC ] = "volume-sync",
+};
+
+static const char *agent_caps[] = {
+ [ VD_AGENT_CAP_MOUSE_STATE ] = "mouse state",
+ [ VD_AGENT_CAP_MONITORS_CONFIG ] = "monitors config",
+ [ VD_AGENT_CAP_REPLY ] = "reply",
+ [ VD_AGENT_CAP_CLIPBOARD ] = "clipboard (old)",
+ [ VD_AGENT_CAP_DISPLAY_CONFIG ] = "display config",
+ [ VD_AGENT_CAP_CLIPBOARD_BY_DEMAND ] = "clipboard",
+ [ VD_AGENT_CAP_CLIPBOARD_SELECTION ] = "clipboard selection",
+ [ VD_AGENT_CAP_SPARSE_MONITORS_CONFIG ] = "sparse monitors",
+ [ VD_AGENT_CAP_GUEST_LINEEND_LF ] = "line-end lf",
+ [ VD_AGENT_CAP_GUEST_LINEEND_CRLF ] = "line-end crlf",
+ [ VD_AGENT_CAP_MAX_CLIPBOARD ] = "max-clipboard",
+ [ VD_AGENT_CAP_AUDIO_VOLUME_SYNC ] = "volume-sync",
+};
+#define NAME(_a, _i) ((_i) < SPICE_N_ELEMENTS(_a) ? (_a[(_i)] ?: "?") : "?")
+
+/* ------------------------------------------------------------------ */
+
+static gboolean test_agent_cap(SpiceMainChannel *channel, guint32 cap)
+{
+ SpiceMainChannelPrivate *c = channel->priv;
+
+ if (!c->agent_caps_received)
+ return FALSE;
+
+ return VD_AGENT_HAS_CAPABILITY(c->agent_caps, G_N_ELEMENTS(c->agent_caps), cap);
+}
+
+static void spice_main_channel_reset_capabilties(SpiceChannel *channel)
+{
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_MAIN_CAP_SEMI_SEAMLESS_MIGRATE);
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_MAIN_CAP_NAME_AND_UUID);
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_MAIN_CAP_AGENT_CONNECTED_TOKENS);
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_MAIN_CAP_SEAMLESS_MIGRATE);
+}
+
+static void spice_main_channel_init(SpiceMainChannel *channel)
+{
+ SpiceMainChannelPrivate *c;
+
+ c = channel->priv = SPICE_MAIN_CHANNEL_GET_PRIVATE(channel);
+ c->agent_msg_queue = g_queue_new();
+ c->file_xfer_tasks = g_hash_table_new(g_direct_hash, g_direct_equal);
+ c->cancellable_volume_info = g_cancellable_new();
+
+ spice_main_channel_reset_capabilties(SPICE_CHANNEL(channel));
+}
+
+static gint spice_main_get_max_clipboard(SpiceMainChannel *self)
+{
+ g_return_val_if_fail(SPICE_IS_MAIN_CHANNEL(self), 0);
+
+ if (g_getenv("SPICE_MAX_CLIPBOARD"))
+ return atoi(g_getenv("SPICE_MAX_CLIPBOARD"));
+
+ return self->priv->max_clipboard;
+}
+
+static void spice_main_get_property(GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceMainChannel *self = SPICE_MAIN_CHANNEL(object);
+ SpiceMainChannelPrivate *c = self->priv;
+
+ switch (prop_id) {
+ case PROP_MOUSE_MODE:
+ g_value_set_int(value, c->mouse_mode);
+ break;
+ case PROP_AGENT_CONNECTED:
+ g_value_set_boolean(value, c->agent_connected);
+ break;
+ case PROP_AGENT_CAPS_0:
+ g_value_set_int(value, c->agent_caps[0]);
+ break;
+ case PROP_DISPLAY_DISABLE_WALLPAPER:
+ g_value_set_boolean(value, c->display_disable_wallpaper);
+ break;
+ case PROP_DISPLAY_DISABLE_FONT_SMOOTH:
+ g_value_set_boolean(value, c->display_disable_font_smooth);
+ break;
+ case PROP_DISPLAY_DISABLE_ANIMATION:
+ g_value_set_boolean(value, c->display_disable_animation);
+ break;
+ case PROP_DISPLAY_COLOR_DEPTH:
+ g_value_set_uint(value, c->display_color_depth);
+ break;
+ case PROP_DISABLE_DISPLAY_POSITION:
+ g_value_set_boolean(value, c->disable_display_position);
+ break;
+ case PROP_DISABLE_DISPLAY_ALIGN:
+ g_value_set_boolean(value, c->disable_display_align);
+ break;
+ case PROP_MAX_CLIPBOARD:
+ g_value_set_int(value, spice_main_get_max_clipboard(self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_main_set_property(GObject *gobject, guint prop_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ SpiceMainChannel *self = SPICE_MAIN_CHANNEL(gobject);
+ SpiceMainChannelPrivate *c = self->priv;
+
+ switch (prop_id) {
+ case PROP_DISPLAY_DISABLE_WALLPAPER:
+ c->display_disable_wallpaper = g_value_get_boolean(value);
+ break;
+ case PROP_DISPLAY_DISABLE_FONT_SMOOTH:
+ c->display_disable_font_smooth = g_value_get_boolean(value);
+ break;
+ case PROP_DISPLAY_DISABLE_ANIMATION:
+ c->display_disable_animation = g_value_get_boolean(value);
+ break;
+ case PROP_DISPLAY_COLOR_DEPTH: {
+ guint color_depth = g_value_get_uint(value);
+ g_return_if_fail(color_depth % 8 == 0);
+ c->display_color_depth = color_depth;
+ break;
+ }
+ case PROP_DISABLE_DISPLAY_POSITION:
+ c->disable_display_position = g_value_get_boolean(value);
+ break;
+ case PROP_DISABLE_DISPLAY_ALIGN:
+ c->disable_display_align = g_value_get_boolean(value);
+ break;
+ case PROP_MAX_CLIPBOARD:
+ spice_main_set_max_clipboard(self, g_value_get_int(value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_main_channel_dispose(GObject *obj)
+{
+ SpiceMainChannelPrivate *c = SPICE_MAIN_CHANNEL(obj)->priv;
+
+ if (c->timer_id) {
+ g_source_remove(c->timer_id);
+ c->timer_id = 0;
+ }
+
+ if (c->switch_host_delayed_id) {
+ g_source_remove(c->switch_host_delayed_id);
+ c->switch_host_delayed_id = 0;
+ }
+
+ if (c->migrate_delayed_id) {
+ g_source_remove(c->migrate_delayed_id);
+ c->migrate_delayed_id = 0;
+ }
+
+ g_cancellable_cancel(c->cancellable_volume_info);
+ g_clear_object(&c->cancellable_volume_info);
+
+ if (G_OBJECT_CLASS(spice_main_channel_parent_class)->dispose)
+ G_OBJECT_CLASS(spice_main_channel_parent_class)->dispose(obj);
+}
+
+static void spice_main_channel_finalize(GObject *obj)
+{
+ SpiceMainChannelPrivate *c = SPICE_MAIN_CHANNEL(obj)->priv;
+
+ g_free(c->agent_msg_data);
+ agent_free_msg_queue(SPICE_MAIN_CHANNEL(obj));
+ if (c->file_xfer_tasks)
+ g_hash_table_unref(c->file_xfer_tasks);
+
+ if (G_OBJECT_CLASS(spice_main_channel_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_main_channel_parent_class)->finalize(obj);
+}
+
+/* coroutine context */
+static void spice_channel_iterate_write(SpiceChannel *channel)
+{
+ agent_send_msg_queue(SPICE_MAIN_CHANNEL(channel));
+
+ if (SPICE_CHANNEL_CLASS(spice_main_channel_parent_class)->iterate_write)
+ SPICE_CHANNEL_CLASS(spice_main_channel_parent_class)->iterate_write(channel);
+}
+
+/* main or coroutine context */
+static void spice_main_channel_reset_agent(SpiceMainChannel *channel)
+{
+ SpiceMainChannelPrivate *c = channel->priv;
+ GError *error;
+ GList *tasks;
+ GList *l;
+
+ c->agent_connected = FALSE;
+ c->agent_caps_received = FALSE;
+ c->agent_display_config_sent = FALSE;
+ c->agent_msg_pos = 0;
+ g_free(c->agent_msg_data);
+ c->agent_msg_data = NULL;
+ c->agent_msg_size = 0;
+
+ tasks = g_hash_table_get_values(c->file_xfer_tasks);
+ for (l = tasks; l != NULL; l = l->next) {
+ SpiceFileXferTask *task = (SpiceFileXferTask *)l->data;
+
+ error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "Agent connection closed");
+ file_xfer_completed(task, error);
+ }
+ g_list_free(tasks);
+ file_xfer_flushed(channel, FALSE);
+}
+
+/* main or coroutine context */
+static void spice_main_channel_reset(SpiceChannel *channel, gboolean migrating)
+{
+ SpiceMainChannelPrivate *c = SPICE_MAIN_CHANNEL(channel)->priv;
+
+ /* This is not part of reset_agent, since the spice-server expects any
+ pending multi-chunk messages to be completed by the client, even after
+ it has send an agent-disconnected msg as that is what the original
+ spicec did. Also see the TODO in server/reds.c reds_reset_vdp() */
+ c->agent_tokens = 0;
+ agent_free_msg_queue(SPICE_MAIN_CHANNEL(channel));
+ c->agent_msg_queue = g_queue_new();
+
+ c->agent_volume_playback_sync = FALSE;
+ c->agent_volume_record_sync = FALSE;
+
+ set_agent_connected(SPICE_MAIN_CHANNEL(channel), FALSE);
+
+ SPICE_CHANNEL_CLASS(spice_main_channel_parent_class)->channel_reset(channel, migrating);
+}
+
+static void spice_main_constructed(GObject *object)
+{
+ SpiceMainChannel *self = SPICE_MAIN_CHANNEL(object);
+ SpiceMainChannelPrivate *c = self->priv;
+
+ /* update default value */
+ c->max_clipboard = spice_main_get_max_clipboard(self);
+
+ if (G_OBJECT_CLASS(spice_main_channel_parent_class)->constructed)
+ G_OBJECT_CLASS(spice_main_channel_parent_class)->constructed(object);
+}
+
+static void spice_main_channel_class_init(SpiceMainChannelClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS(klass);
+
+ gobject_class->dispose = spice_main_channel_dispose;
+ gobject_class->finalize = spice_main_channel_finalize;
+ gobject_class->get_property = spice_main_get_property;
+ gobject_class->set_property = spice_main_set_property;
+ gobject_class->constructed = spice_main_constructed;
+
+ channel_class->handle_msg = spice_main_handle_msg;
+ channel_class->iterate_write = spice_channel_iterate_write;
+ channel_class->channel_reset = spice_main_channel_reset;
+ channel_class->channel_reset_capabilities = spice_main_channel_reset_capabilties;
+ channel_class->channel_send_migration_handshake = spice_main_channel_send_migration_handshake;
+
+ /**
+ * SpiceMainChannel:mouse-mode:
+ *
+ * Spice protocol specifies two mouse modes, client mode and
+ * server mode. In client mode (%SPICE_MOUSE_MODE_CLIENT), the
+ * affective mouse is the client side mouse: the client sends
+ * mouse position within the display and the server sends mouse
+ * shape messages. In server mode (%SPICE_MOUSE_MODE_SERVER), the
+ * client sends relative mouse movements and the server sends
+ * position and shape commands.
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_MOUSE_MODE,
+ g_param_spec_int("mouse-mode",
+ "Mouse mode",
+ "Mouse mode",
+ 0, INT_MAX, 0,
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_NAME |
+ G_PARAM_STATIC_NICK |
+ G_PARAM_STATIC_BLURB));
+
+ g_object_class_install_property
+ (gobject_class, PROP_AGENT_CONNECTED,
+ g_param_spec_boolean("agent-connected",
+ "Agent connected",
+ "Whether the agent is connected",
+ FALSE,
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_NAME |
+ G_PARAM_STATIC_NICK |
+ G_PARAM_STATIC_BLURB));
+
+ g_object_class_install_property
+ (gobject_class, PROP_AGENT_CAPS_0,
+ g_param_spec_int("agent-caps-0",
+ "Agent caps 0",
+ "Agent capability bits 0 -> 31",
+ 0, INT_MAX, 0,
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_NAME |
+ G_PARAM_STATIC_NICK |
+ G_PARAM_STATIC_BLURB));
+
+ g_object_class_install_property
+ (gobject_class, PROP_DISPLAY_DISABLE_WALLPAPER,
+ g_param_spec_boolean("disable-wallpaper",
+ "Disable guest wallpaper",
+ "Disable guest wallpaper",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_DISPLAY_DISABLE_FONT_SMOOTH,
+ g_param_spec_boolean("disable-font-smooth",
+ "Disable guest font smooth",
+ "Disable guest font smoothing",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_DISPLAY_DISABLE_ANIMATION,
+ g_param_spec_boolean("disable-animation",
+ "Disable guest animations",
+ "Disable guest animations",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_DISABLE_DISPLAY_POSITION,
+ g_param_spec_boolean("disable-display-position",
+ "Disable display position",
+ "Disable using display position when setting monitor config",
+ TRUE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_DISPLAY_COLOR_DEPTH,
+ g_param_spec_uint("color-depth",
+ "Color depth",
+ "Color depth", 0, 32, 0,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceMainChannel:disable-display-align:
+ *
+ * Disable automatic horizontal display position alignment.
+ *
+ * Since: 0.13
+ */
+ g_object_class_install_property
+ (gobject_class, PROP_DISABLE_DISPLAY_ALIGN,
+ g_param_spec_boolean("disable-display-align",
+ "Disable display align",
+ "Disable display position alignment",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceMainChannel:max-clipboard:
+ *
+ * Maximum size of clipboard operations in bytes (default 100MB,
+ * -1 for unlimited size);
+ *
+ * Since: 0.22
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_MAX_CLIPBOARD,
+ g_param_spec_int("max-clipboard",
+ "max clipboard",
+ "Maximum clipboard data size",
+ -1, G_MAXINT, 100 * 1024 * 1024,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /* TODO use notify instead */
+ /**
+ * SpiceMainChannel::main-mouse-update:
+ * @main: the #SpiceMainChannel that emitted the signal
+ *
+ * Notify when the mouse mode has changed.
+ **/
+ signals[SPICE_MAIN_MOUSE_UPDATE] =
+ g_signal_new("main-mouse-update",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceMainChannelClass, mouse_update),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
+
+ /* TODO use notify instead */
+ /**
+ * SpiceMainChannel::main-agent-update:
+ * @main: the #SpiceMainChannel that emitted the signal
+ *
+ * Notify when the %SpiceMainChannel:agent-connected or
+ * %SpiceMainChannel:agent-caps-0 property change.
+ **/
+ signals[SPICE_MAIN_AGENT_UPDATE] =
+ g_signal_new("main-agent-update",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceMainChannelClass, agent_update),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
+ /**
+ * SpiceMainChannel::main-clipboard:
+ * @main: the #SpiceMainChannel that emitted the signal
+ * @type: the VD_AGENT_CLIPBOARD data type
+ * @data: clipboard data
+ * @size: size of @data in bytes
+ *
+ * Provides guest clipboard data requested by spice_main_clipboard_request().
+ *
+ * Deprecated: 0.6: use SpiceMainChannel::main-clipboard-selection instead.
+ **/
+ signals[SPICE_MAIN_CLIPBOARD] =
+ g_signal_new("main-clipboard",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_DEPRECATED,
+ 0,
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__UINT_POINTER_UINT,
+ G_TYPE_NONE,
+ 3,
+ G_TYPE_UINT, G_TYPE_POINTER, G_TYPE_UINT);
+
+ /**
+ * SpiceMainChannel::main-clipboard-selection:
+ * @main: the #SpiceMainChannel that emitted the signal
+ * @selection: a VD_AGENT_CLIPBOARD_SELECTION clipboard
+ * @type: the VD_AGENT_CLIPBOARD data type
+ * @data: clipboard data
+ * @size: size of @data in bytes
+ *
+ * Since: 0.6
+ **/
+ signals[SPICE_MAIN_CLIPBOARD_SELECTION] =
+ g_signal_new("main-clipboard-selection",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__UINT_UINT_POINTER_UINT,
+ G_TYPE_NONE,
+ 4,
+ G_TYPE_UINT, G_TYPE_UINT, G_TYPE_POINTER, G_TYPE_UINT);
+
+ /**
+ * SpiceMainChannel::main-clipboard-grab:
+ * @main: the #SpiceMainChannel that emitted the signal
+ * @types: the VD_AGENT_CLIPBOARD data types
+ * @ntypes: the number of @types
+ *
+ * Inform when clipboard data is available from the guest, and for
+ * which @types.
+ *
+ * Deprecated: 0.6: use SpiceMainChannel::main-clipboard-selection-grab instead.
+ **/
+ signals[SPICE_MAIN_CLIPBOARD_GRAB] =
+ g_signal_new("main-clipboard-grab",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_DEPRECATED,
+ 0,
+ NULL, NULL,
+ g_cclosure_user_marshal_BOOLEAN__POINTER_UINT,
+ G_TYPE_BOOLEAN,
+ 2,
+ G_TYPE_POINTER, G_TYPE_UINT);
+
+ /**
+ * SpiceMainChannel::main-clipboard-selection-grab:
+ * @main: the #SpiceMainChannel that emitted the signal
+ * @selection: a VD_AGENT_CLIPBOARD_SELECTION clipboard
+ * @types: the VD_AGENT_CLIPBOARD data types
+ * @ntypes: the number of @types
+ *
+ * Inform when clipboard data is available from the guest, and for
+ * which @types.
+ *
+ * Since: 0.6
+ **/
+ signals[SPICE_MAIN_CLIPBOARD_SELECTION_GRAB] =
+ g_signal_new("main-clipboard-selection-grab",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ g_cclosure_user_marshal_BOOLEAN__UINT_POINTER_UINT,
+ G_TYPE_BOOLEAN,
+ 3,
+ G_TYPE_UINT, G_TYPE_POINTER, G_TYPE_UINT);
+
+ /**
+ * SpiceMainChannel::main-clipboard-request:
+ * @main: the #SpiceMainChannel that emitted the signal
+ * @types: the VD_AGENT_CLIPBOARD request type
+ *
+ * Return value: %TRUE if the request is successful
+ *
+ * Request clipbard data from the client.
+ *
+ * Deprecated: 0.6: use SpiceMainChannel::main-clipboard-selection-request instead.
+ **/
+ signals[SPICE_MAIN_CLIPBOARD_REQUEST] =
+ g_signal_new("main-clipboard-request",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_DEPRECATED,
+ 0,
+ NULL, NULL,
+ g_cclosure_user_marshal_BOOLEAN__UINT,
+ G_TYPE_BOOLEAN,
+ 1,
+ G_TYPE_UINT);
+
+ /**
+ * SpiceMainChannel::main-clipboard-selection-request:
+ * @main: the #SpiceMainChannel that emitted the signal
+ * @selection: a VD_AGENT_CLIPBOARD_SELECTION clipboard
+ * @types: the VD_AGENT_CLIPBOARD request type
+ *
+ * Return value: %TRUE if the request is successful
+ *
+ * Request clipbard data from the client.
+ *
+ * Since: 0.6
+ **/
+ signals[SPICE_MAIN_CLIPBOARD_SELECTION_REQUEST] =
+ g_signal_new("main-clipboard-selection-request",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ g_cclosure_user_marshal_BOOLEAN__UINT_UINT,
+ G_TYPE_BOOLEAN,
+ 2,
+ G_TYPE_UINT, G_TYPE_UINT);
+
+ /**
+ * SpiceMainChannel::main-clipboard-release:
+ * @main: the #SpiceMainChannel that emitted the signal
+ *
+ * Inform when the clipboard is released from the guest, when no
+ * clipboard data is available from the guest.
+ *
+ * Deprecated: 0.6: use SpiceMainChannel::main-clipboard-selection-release instead.
+ **/
+ signals[SPICE_MAIN_CLIPBOARD_RELEASE] =
+ g_signal_new("main-clipboard-release",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_DEPRECATED,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
+
+ /**
+ * SpiceMainChannel::main-clipboard-selection-release:
+ * @main: the #SpiceMainChannel that emitted the signal
+ * @selection: a VD_AGENT_CLIPBOARD_SELECTION clipboard
+ *
+ * Inform when the clipboard is released from the guest, when no
+ * clipboard data is available from the guest.
+ *
+ * Since: 0.6
+ **/
+ signals[SPICE_MAIN_CLIPBOARD_SELECTION_RELEASE] =
+ g_signal_new("main-clipboard-selection-release",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_UINT);
+
+ /**
+ * SpiceMainChannel::migration-started:
+ * @main: the #SpiceMainChannel that emitted the signal
+ * @session: a migration #SpiceSession
+ *
+ * Inform when migration is starting. Application wishing to make
+ * connections themself can set the #SpiceSession:client-sockets
+ * to @TRUE, then follow #SpiceSession::channel-new creation, and
+ * use spice_channel_open_fd() once the socket is created.
+ *
+ **/
+ signals[SPICE_MIGRATION_STARTED] =
+ g_signal_new("migration-started",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__OBJECT,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_OBJECT);
+
+ g_type_class_add_private(klass, sizeof(SpiceMainChannelPrivate));
+ channel_set_handlers(SPICE_CHANNEL_CLASS(klass));
+}
+
+/* ------------------------------------------------------------------ */
+
+
+static void agent_free_msg_queue(SpiceMainChannel *channel)
+{
+ SpiceMainChannelPrivate *c = channel->priv;
+ SpiceMsgOut *out;
+
+ if (!c->agent_msg_queue)
+ return;
+
+ while (!g_queue_is_empty(c->agent_msg_queue)) {
+ out = g_queue_pop_head(c->agent_msg_queue);
+ spice_msg_out_unref(out);
+ }
+
+ g_queue_free(c->agent_msg_queue);
+ c->agent_msg_queue = NULL;
+}
+
+/* Here, flushing algorithm is stolen from spice-channel.c */
+static void file_xfer_flushed(SpiceMainChannel *channel, gboolean success)
+{
+ SpiceMainChannelPrivate *c = channel->priv;
+ GSList *l;
+
+ for (l = c->flushing; l != NULL; l = l->next) {
+ GSimpleAsyncResult *result = G_SIMPLE_ASYNC_RESULT(l->data);
+ g_simple_async_result_set_op_res_gboolean(result, success);
+ g_simple_async_result_complete_in_idle(result);
+ }
+
+ g_slist_free_full(c->flushing, g_object_unref);
+ c->flushing = NULL;
+}
+
+static void file_xfer_flush_async(SpiceMainChannel *channel, GCancellable *cancellable,
+ GAsyncReadyCallback callback, gpointer user_data)
+{
+ GSimpleAsyncResult *simple;
+ SpiceMainChannelPrivate *c = channel->priv;
+ gboolean was_empty;
+
+ simple = g_simple_async_result_new(G_OBJECT(channel), callback, user_data,
+ file_xfer_flush_async);
+
+ was_empty = g_queue_is_empty(c->agent_msg_queue);
+ if (was_empty) {
+ g_simple_async_result_set_op_res_gboolean(simple, TRUE);
+ g_simple_async_result_complete_in_idle(simple);
+ g_object_unref(simple);
+ return;
+ }
+
+ c->flushing = g_slist_append(c->flushing, simple);
+}
+
+static gboolean file_xfer_flush_finish(SpiceMainChannel *channel, GAsyncResult *result,
+ GError **error)
+{
+ GSimpleAsyncResult *simple = (GSimpleAsyncResult *)result;
+
+ g_return_val_if_fail(g_simple_async_result_is_valid(result,
+ G_OBJECT(channel), file_xfer_flush_async), FALSE);
+
+ if (g_simple_async_result_propagate_error(simple, error)) {
+ return FALSE;
+ }
+
+ CHANNEL_DEBUG(channel, "flushed finished!");
+ return g_simple_async_result_get_op_res_gboolean(simple);
+}
+
+/* coroutine context */
+static void agent_send_msg_queue(SpiceMainChannel *channel)
+{
+ SpiceMainChannelPrivate *c = channel->priv;
+ SpiceMsgOut *out;
+
+ while (c->agent_tokens > 0 &&
+ !g_queue_is_empty(c->agent_msg_queue)) {
+ c->agent_tokens--;
+ out = g_queue_pop_head(c->agent_msg_queue);
+ spice_msg_out_send_internal(out);
+ }
+ if (g_queue_is_empty(c->agent_msg_queue) && c->flushing != NULL) {
+ file_xfer_flushed(channel, TRUE);
+ }
+}
+
+/* any context: the message is not flushed immediately,
+ you can wakeup() the channel coroutine or send_msg_queue()
+
+ expected arguments, pair of data/data_size to send terminated with NULL:
+ agent_msg_queue_many(main, VD_AGENT_...,
+ &foo, sizeof(Foo),
+ data, data_size, NULL);
+*/
+G_GNUC_NULL_TERMINATED
+static void agent_msg_queue_many(SpiceMainChannel *channel, int type, const void *data, ...)
+{
+ va_list args;
+ SpiceMainChannelPrivate *c = channel->priv;
+ SpiceMsgOut *out;
+ VDAgentMessage msg;
+ guint8 *payload;
+ gsize paysize, s, mins, size = 0;
+ const guint8 *d;
+
+ G_STATIC_ASSERT(VD_AGENT_MAX_DATA_SIZE > sizeof(VDAgentMessage));
+
+ va_start(args, data);
+ for (d = data; d != NULL; d = va_arg(args, void*)) {
+ size += va_arg(args, gsize);
+ }
+ va_end(args);
+
+ msg.protocol = VD_AGENT_PROTOCOL;
+ msg.type = type;
+ msg.opaque = 0;
+ msg.size = size;
+
+ paysize = MIN(VD_AGENT_MAX_DATA_SIZE, size + sizeof(VDAgentMessage));
+ out = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_MAIN_AGENT_DATA);
+ payload = spice_marshaller_reserve_space(out->marshaller, paysize);
+ memcpy(payload, &msg, sizeof(VDAgentMessage));
+ payload += sizeof(VDAgentMessage);
+ paysize -= sizeof(VDAgentMessage);
+ if (paysize == 0) {
+ g_queue_push_tail(c->agent_msg_queue, out);
+ out = NULL;
+ }
+
+ va_start(args, data);
+ for (d = data; size > 0; d = va_arg(args, void*)) {
+ s = va_arg(args, gsize);
+ while (s > 0) {
+ if (out == NULL) {
+ paysize = MIN(VD_AGENT_MAX_DATA_SIZE, size);
+ out = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_MAIN_AGENT_DATA);
+ payload = spice_marshaller_reserve_space(out->marshaller, paysize);
+ }
+ mins = MIN(paysize, s);
+ memcpy(payload, d, mins);
+ d += mins;
+ payload += mins;
+ s -= mins;
+ size -= mins;
+ paysize -= mins;
+ if (paysize == 0) {
+ g_queue_push_tail(c->agent_msg_queue, out);
+ out = NULL;
+ }
+ }
+ }
+ va_end(args);
+ g_warn_if_fail(out == NULL);
+}
+
+static int monitors_cmp(const void *p1, const void *p2, gpointer user_data)
+{
+ const VDAgentMonConfig *m1 = p1;
+ const VDAgentMonConfig *m2 = p2;
+ double d1 = sqrt(m1->x * m1->x + m1->y * m1->y);
+ double d2 = sqrt(m2->x * m2->x + m2->y * m2->y);
+ int diff = d1 - d2;
+
+ return diff == 0 ? (char*)p1 - (char*)p2 : diff;
+}
+
+static void monitors_align(VDAgentMonConfig *monitors, int nmonitors)
+{
+ gint i, j, x = 0;
+ guint32 used = 0;
+ VDAgentMonConfig *sorted_monitors;
+
+ if (nmonitors == 0)
+ return;
+
+ /* sort by distance from origin */
+ sorted_monitors = g_memdup(monitors, nmonitors * sizeof(VDAgentMonConfig));
+ g_qsort_with_data(sorted_monitors, nmonitors, sizeof(VDAgentMonConfig), monitors_cmp, NULL);
+
+ /* super-KISS ltr alignment, feel free to improve */
+ for (i = 0; i < nmonitors; i++) {
+ /* Find where this monitor is in the sorted order */
+ for (j = 0; j < nmonitors; j++) {
+ /* Avoid using the same entry twice, this happens with older
+ virt-viewer versions which always set x and y to 0 */
+ if (used & (1 << j))
+ continue;
+ if (memcmp(&monitors[j], &sorted_monitors[i],
+ sizeof(VDAgentMonConfig)) == 0)
+ break;
+ }
+ used |= 1 << j;
+ monitors[j].x = x;
+ monitors[j].y = 0;
+ x += monitors[j].width;
+ if (monitors[j].width || monitors[j].height)
+ SPICE_DEBUG("#%d +%d+%d-%dx%d", j, monitors[j].x, monitors[j].y,
+ monitors[j].width, monitors[j].height);
+ }
+ g_free(sorted_monitors);
+}
+
+
+#define agent_msg_queue(Channel, Type, Size, Data) \
+ agent_msg_queue_many((Channel), (Type), (Data), (Size), NULL)
+
+/**
+ * spice_main_send_monitor_config:
+ * @channel:
+ *
+ * Send monitors configuration previously set with
+ * spice_main_set_display() and spice_main_set_display_enabled()
+ *
+ * Returns: %TRUE on success.
+ **/
+gboolean spice_main_send_monitor_config(SpiceMainChannel *channel)
+{
+ SpiceMainChannelPrivate *c;
+ VDAgentMonitorsConfig *mon;
+ int i, j, monitors;
+ size_t size;
+
+ g_return_val_if_fail(SPICE_IS_MAIN_CHANNEL(channel), FALSE);
+ c = channel->priv;
+ g_return_val_if_fail(c->agent_connected, FALSE);
+
+ if (spice_main_agent_test_capability(channel,
+ VD_AGENT_CAP_SPARSE_MONITORS_CONFIG)) {
+ monitors = SPICE_N_ELEMENTS(c->display);
+ } else {
+ monitors = 0;
+ for (i = 0; i < SPICE_N_ELEMENTS(c->display); i++) {
+ if (c->display[i].enabled)
+ monitors += 1;
+ }
+ }
+
+ size = sizeof(VDAgentMonitorsConfig) + sizeof(VDAgentMonConfig) * monitors;
+ mon = g_malloc0(size);
+
+ mon->num_of_monitors = monitors;
+ if (c->disable_display_position == FALSE ||
+ c->disable_display_align == FALSE)
+ mon->flags |= VD_AGENT_CONFIG_MONITORS_FLAG_USE_POS;
+
+ j = 0;
+ for (i = 0; i < SPICE_N_ELEMENTS(c->display); i++) {
+ if (!c->display[i].enabled) {
+ if (spice_main_agent_test_capability(channel,
+ VD_AGENT_CAP_SPARSE_MONITORS_CONFIG))
+ j++;
+ continue;
+ }
+ mon->monitors[j].depth = c->display_color_depth ? c->display_color_depth : 32;
+ mon->monitors[j].width = c->display[i].width;
+ mon->monitors[j].height = c->display[i].height;
+ mon->monitors[j].x = c->display[i].x;
+ mon->monitors[j].y = c->display[i].y;
+ CHANNEL_DEBUG(channel, "monitor config: #%d %dx%d+%d+%d @ %d bpp", j,
+ mon->monitors[j].width, mon->monitors[j].height,
+ mon->monitors[j].x, mon->monitors[j].y,
+ mon->monitors[j].depth);
+ j++;
+ }
+
+ if (c->disable_display_align == FALSE)
+ monitors_align(mon->monitors, mon->num_of_monitors);
+
+ agent_msg_queue(channel, VD_AGENT_MONITORS_CONFIG, size, mon);
+ g_free(mon);
+
+ spice_channel_wakeup(SPICE_CHANNEL(channel), FALSE);
+ if (c->timer_id != 0) {
+ g_source_remove(c->timer_id);
+ c->timer_id = 0;
+ }
+
+ return TRUE;
+}
+
+static void audio_playback_volume_info_cb(GObject *object, GAsyncResult *res, gpointer user_data)
+{
+ SpiceMainChannel *main_channel = user_data;
+ SpiceSession *session = spice_channel_get_session(SPICE_CHANNEL(main_channel));
+ SpiceAudio *audio = spice_audio_get(session, NULL);
+ VDAgentAudioVolumeSync *avs;
+ guint16 *volume;
+ guint8 nchannels;
+ gboolean mute, ret;
+ gsize array_size;
+ GError *error = NULL;
+
+ ret = spice_audio_get_playback_volume_info_finish(audio, res, &mute, &nchannels,
+ &volume, &error);
+ if (ret == FALSE || volume == NULL || nchannels == 0) {
+ if (error != NULL) {
+ spice_warning("Failed to get playback async volume info: %s", error->message);
+ g_error_free (error);
+ } else {
+ SPICE_DEBUG("Failed to get playback async volume info");
+ }
+ main_channel->priv->agent_volume_playback_sync = FALSE;
+ return;
+ }
+
+ array_size = sizeof(uint16_t) * nchannels;
+ avs = g_malloc0(sizeof(VDAgentAudioVolumeSync) + array_size);
+ avs->is_playback = TRUE;
+ avs->mute = mute;
+ avs->nchannels = nchannels;
+ memcpy(avs->volume, volume, array_size);
+
+ SPICE_DEBUG("%s mute=%s nchannels=%u volume[0]=%u",
+ __func__, spice_yes_no(mute), nchannels, volume[0]);
+ g_free(volume);
+ agent_msg_queue(main_channel, VD_AGENT_AUDIO_VOLUME_SYNC,
+ sizeof(VDAgentAudioVolumeSync) + array_size, avs);
+}
+
+static void agent_sync_audio_playback(SpiceMainChannel *main_channel)
+{
+ SpiceSession *session = spice_channel_get_session(SPICE_CHANNEL(main_channel));
+ SpiceAudio *audio = spice_audio_get(session, NULL);
+ SpiceMainChannelPrivate *c = main_channel->priv;
+
+ if (!test_agent_cap(main_channel, VD_AGENT_CAP_AUDIO_VOLUME_SYNC) ||
+ c->agent_volume_playback_sync == TRUE) {
+ SPICE_DEBUG("%s - is not going to sync audio with guest", __func__);
+ return;
+ }
+ /* only one per connection */
+ g_cancellable_reset(c->cancellable_volume_info);
+ c->agent_volume_playback_sync = TRUE;
+ spice_audio_get_playback_volume_info_async(audio, c->cancellable_volume_info, main_channel,
+ audio_playback_volume_info_cb, main_channel);
+}
+
+static void audio_record_volume_info_cb(GObject *object, GAsyncResult *res, gpointer user_data)
+{
+ SpiceMainChannel *main_channel = user_data;
+ SpiceSession *session = spice_channel_get_session(SPICE_CHANNEL(main_channel));
+ SpiceAudio *audio = spice_audio_get(session, NULL);
+ VDAgentAudioVolumeSync *avs;
+ guint16 *volume;
+ guint8 nchannels;
+ gboolean ret, mute;
+ gsize array_size;
+ GError *error = NULL;
+
+ ret = spice_audio_get_record_volume_info_finish(audio, res, &mute, &nchannels, &volume, &error);
+ if (ret == FALSE || volume == NULL || nchannels == 0) {
+ if (error != NULL) {
+ spice_warning ("Failed to get record async volume info: %s", error->message);
+ g_error_free (error);
+ } else {
+ SPICE_DEBUG("Failed to get record async volume info");
+ }
+ main_channel->priv->agent_volume_record_sync = FALSE;
+ return;
+ }
+
+ array_size = sizeof(uint16_t) * nchannels;
+ avs = g_malloc0(sizeof(VDAgentAudioVolumeSync) + array_size);
+ avs->is_playback = FALSE;
+ avs->mute = mute;
+ avs->nchannels = nchannels;
+ memcpy(avs->volume, volume, array_size);
+
+ SPICE_DEBUG("%s mute=%s nchannels=%u volume[0]=%u",
+ __func__, spice_yes_no(mute), nchannels, volume[0]);
+ g_free(volume);
+ agent_msg_queue(main_channel, VD_AGENT_AUDIO_VOLUME_SYNC,
+ sizeof(VDAgentAudioVolumeSync) + array_size, avs);
+}
+
+static void agent_sync_audio_record(SpiceMainChannel *main_channel)
+{
+ SpiceSession *session = spice_channel_get_session(SPICE_CHANNEL(main_channel));
+ SpiceAudio *audio = spice_audio_get(session, NULL);
+ SpiceMainChannelPrivate *c = main_channel->priv;
+
+ if (!test_agent_cap(main_channel, VD_AGENT_CAP_AUDIO_VOLUME_SYNC) ||
+ c->agent_volume_record_sync == TRUE) {
+ SPICE_DEBUG("%s - is not going to sync audio with guest", __func__);
+ return;
+ }
+ /* only one per connection */
+ g_cancellable_reset(c->cancellable_volume_info);
+ c->agent_volume_record_sync = TRUE;
+ spice_audio_get_record_volume_info_async(audio, c->cancellable_volume_info, main_channel,
+ audio_record_volume_info_cb, main_channel);
+}
+
+/* any context: the message is not flushed immediately,
+ you can wakeup() the channel coroutine or send_msg_queue() */
+static void agent_display_config(SpiceMainChannel *channel)
+{
+ SpiceMainChannelPrivate *c = channel->priv;
+ VDAgentDisplayConfig config = { 0, };
+
+ if (c->display_disable_wallpaper) {
+ config.flags |= VD_AGENT_DISPLAY_CONFIG_FLAG_DISABLE_WALLPAPER;
+ }
+
+ if (c->display_disable_font_smooth) {
+ config.flags |= VD_AGENT_DISPLAY_CONFIG_FLAG_DISABLE_FONT_SMOOTH;
+ }
+
+ if (c->display_disable_animation) {
+ config.flags |= VD_AGENT_DISPLAY_CONFIG_FLAG_DISABLE_ANIMATION;
+ }
+
+ if (c->display_color_depth != 0) {
+ config.flags |= VD_AGENT_DISPLAY_CONFIG_FLAG_SET_COLOR_DEPTH;
+ config.depth = c->display_color_depth;
+ }
+
+ CHANNEL_DEBUG(channel, "display_config: flags: %u, depth: %u", config.flags, config.depth);
+
+ agent_msg_queue(channel, VD_AGENT_DISPLAY_CONFIG, sizeof(VDAgentDisplayConfig), &config);
+}
+
+/* any context: the message is not flushed immediately,
+ you can wakeup() the channel coroutine or send_msg_queue() */
+static void agent_announce_caps(SpiceMainChannel *channel)
+{
+ SpiceMainChannelPrivate *c = channel->priv;
+ VDAgentAnnounceCapabilities *caps;
+ size_t size;
+
+ if (!c->agent_connected)
+ return;
+
+ size = sizeof(VDAgentAnnounceCapabilities) + VD_AGENT_CAPS_BYTES;
+ caps = g_malloc0(size);
+ if (!c->agent_caps_received)
+ caps->request = 1;
+ VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_MOUSE_STATE);
+ VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_MONITORS_CONFIG);
+ VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_REPLY);
+ VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_DISPLAY_CONFIG);
+ VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_CLIPBOARD_BY_DEMAND);
+ VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_CLIPBOARD_SELECTION);
+
+ agent_msg_queue(channel, VD_AGENT_ANNOUNCE_CAPABILITIES, size, caps);
+ g_free(caps);
+}
+
+/* any context: the message is not flushed immediately,
+ you can wakeup() the channel coroutine or send_msg_queue() */
+static void agent_clipboard_grab(SpiceMainChannel *channel, guint selection,
+ guint32 *types, int ntypes)
+{
+ SpiceMainChannelPrivate *c = channel->priv;
+ guint8 *msg;
+ VDAgentClipboardGrab *grab;
+ size_t size;
+ int i;
+
+ if (!c->agent_connected)
+ return;
+
+ g_return_if_fail(test_agent_cap(channel, VD_AGENT_CAP_CLIPBOARD_BY_DEMAND));
+
+ size = sizeof(VDAgentClipboardGrab) + sizeof(uint32_t) * ntypes;
+ if (test_agent_cap(channel, VD_AGENT_CAP_CLIPBOARD_SELECTION)) {
+ size += 4;
+ } else if (selection != VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD) {
+ CHANNEL_DEBUG(channel, "Ignoring clipboard grab");
+ return;
+ }
+
+ msg = g_alloca(size);
+ memset(msg, 0, size);
+
+ grab = (VDAgentClipboardGrab *)msg;
+
+ if (test_agent_cap(channel, VD_AGENT_CAP_CLIPBOARD_SELECTION)) {
+ msg[0] = selection;
+ grab = (VDAgentClipboardGrab *)(msg + 4);
+ }
+
+ for (i = 0; i < ntypes; i++) {
+ grab->types[i] = types[i];
+ }
+
+ agent_msg_queue(channel, VD_AGENT_CLIPBOARD_GRAB, size, msg);
+}
+
+/* any context: the message is not flushed immediately,
+ you can wakeup() the channel coroutine or send_msg_queue() */
+static void agent_clipboard_notify(SpiceMainChannel *self, guint selection,
+ guint32 type, const guchar *data, size_t size)
+{
+ SpiceMainChannelPrivate *c = self->priv;
+ VDAgentClipboard *cb;
+ guint8 *msg;
+ size_t msgsize;
+ gint max_clipboard = spice_main_get_max_clipboard(self);
+
+ g_return_if_fail(c->agent_connected);
+ g_return_if_fail(test_agent_cap(self, VD_AGENT_CAP_CLIPBOARD_BY_DEMAND));
+ g_return_if_fail(max_clipboard == -1 || size < max_clipboard);
+
+ msgsize = sizeof(VDAgentClipboard);
+ if (test_agent_cap(self, VD_AGENT_CAP_CLIPBOARD_SELECTION)) {
+ msgsize += 4;
+ } else if (selection != VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD) {
+ CHANNEL_DEBUG(self, "Ignoring clipboard notify");
+ return;
+ }
+
+ msg = g_alloca(msgsize);
+ memset(msg, 0, msgsize);
+
+ cb = (VDAgentClipboard *)msg;
+
+ if (test_agent_cap(self, VD_AGENT_CAP_CLIPBOARD_SELECTION)) {
+ msg[0] = selection;
+ cb = (VDAgentClipboard *)(msg + 4);
+ }
+
+ cb->type = type;
+ agent_msg_queue_many(self, VD_AGENT_CLIPBOARD, msg, msgsize, data, size, NULL);
+}
+
+/* any context: the message is not flushed immediately,
+ you can wakeup() the channel coroutine or send_msg_queue() */
+static void agent_clipboard_request(SpiceMainChannel *channel, guint selection, guint32 type)
+{
+ SpiceMainChannelPrivate *c = channel->priv;
+ VDAgentClipboardRequest *request;
+ guint8 *msg;
+ size_t msgsize;
+
+ g_return_if_fail(c->agent_connected);
+ g_return_if_fail(test_agent_cap(channel, VD_AGENT_CAP_CLIPBOARD_BY_DEMAND));
+
+ msgsize = sizeof(VDAgentClipboardRequest);
+ if (test_agent_cap(channel, VD_AGENT_CAP_CLIPBOARD_SELECTION)) {
+ msgsize += 4;
+ } else if (selection != VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD) {
+ SPICE_DEBUG("Ignoring clipboard request");
+ return;
+ }
+
+ msg = g_alloca(msgsize);
+ memset(msg, 0, msgsize);
+
+ request = (VDAgentClipboardRequest *)msg;
+
+ if (test_agent_cap(channel, VD_AGENT_CAP_CLIPBOARD_SELECTION)) {
+ msg[0] = selection;
+ request = (VDAgentClipboardRequest *)(msg + 4);
+ }
+
+ request->type = type;
+
+ agent_msg_queue(channel, VD_AGENT_CLIPBOARD_REQUEST, msgsize, msg);
+}
+
+/* any context: the message is not flushed immediately,
+ you can wakeup() the channel coroutine or send_msg_queue() */
+static void agent_clipboard_release(SpiceMainChannel *channel, guint selection)
+{
+ SpiceMainChannelPrivate *c = channel->priv;
+ guint8 msg[4] = { 0, };
+ guint8 msgsize = 0;
+
+ g_return_if_fail(c->agent_connected);
+ g_return_if_fail(test_agent_cap(channel, VD_AGENT_CAP_CLIPBOARD_BY_DEMAND));
+
+ if (test_agent_cap(channel, VD_AGENT_CAP_CLIPBOARD_SELECTION)) {
+ msg[0] = selection;
+ msgsize += 4;
+ } else if (selection != VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD) {
+ SPICE_DEBUG("Ignoring clipboard release");
+ return;
+ }
+
+ agent_msg_queue(channel, VD_AGENT_CLIPBOARD_RELEASE, msgsize, msg);
+}
+
+/* main context*/
+static gboolean timer_set_display(gpointer data)
+{
+ SpiceMainChannel *channel = data;
+ SpiceMainChannelPrivate *c = channel->priv;
+ SpiceSession *session;
+ gint i;
+
+ c->timer_id = 0;
+ if (!c->agent_connected)
+ return FALSE;
+
+ session = spice_channel_get_session(SPICE_CHANNEL(channel));
+
+ /* ensure we have an explicit monitor configuration at least for
+ number of display channels */
+ for (i = 0; i < spice_session_get_n_display_channels(session); i++)
+ if (!c->display[i].enabled_set) {
+ SPICE_DEBUG("Not sending monitors config, missing monitors");
+ return FALSE;
+ }
+
+ spice_main_send_monitor_config(channel);
+
+ return FALSE;
+}
+
+/* any context */
+static void update_display_timer(SpiceMainChannel *channel, guint seconds)
+{
+ SpiceMainChannelPrivate *c = channel->priv;
+
+ if (c->timer_id)
+ g_source_remove(c->timer_id);
+
+ c->timer_id = g_timeout_add_seconds(seconds, timer_set_display, channel);
+}
+
+/* coroutine context */
+static void set_agent_connected(SpiceMainChannel *channel, gboolean connected)
+{
+ SpiceMainChannelPrivate *c = channel->priv;
+
+ SPICE_DEBUG("agent connected: %s", spice_yes_no(connected));
+ if (connected != c->agent_connected) {
+ c->agent_connected = connected;
+ g_coroutine_object_notify(G_OBJECT(channel), "agent-connected");
+ }
+ if (!connected)
+ spice_main_channel_reset_agent(SPICE_MAIN_CHANNEL(channel));
+
+ g_coroutine_signal_emit(channel, signals[SPICE_MAIN_AGENT_UPDATE], 0);
+}
+
+/* coroutine context */
+static void agent_start(SpiceMainChannel *channel)
+{
+ SpiceMainChannelPrivate *c = channel->priv;
+ SpiceMsgcMainAgentStart agent_start = {
+ .num_tokens = ~0,
+ };
+ SpiceMsgOut *out;
+
+ c->agent_volume_playback_sync = FALSE;
+ c->agent_volume_record_sync = FALSE;
+ c->agent_caps_received = false;
+ set_agent_connected(channel, TRUE);
+
+ out = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_MAIN_AGENT_START);
+ out->marshallers->msgc_main_agent_start(out->marshaller, &agent_start);
+ spice_msg_out_send_internal(out);
+
+ if (c->agent_connected) {
+ agent_announce_caps(channel);
+ agent_send_msg_queue(channel);
+ }
+}
+
+/* coroutine context */
+static void agent_stopped(SpiceMainChannel *channel)
+{
+ set_agent_connected(channel, FALSE);
+}
+
+/* coroutine context */
+static void set_mouse_mode(SpiceMainChannel *channel, uint32_t supported, uint32_t current)
+{
+ SpiceMainChannelPrivate *c = channel->priv;
+
+ if (c->mouse_mode != current) {
+ c->mouse_mode = current;
+ g_coroutine_signal_emit(channel, signals[SPICE_MAIN_MOUSE_UPDATE], 0);
+ g_coroutine_object_notify(G_OBJECT(channel), "mouse-mode");
+ }
+
+ /* switch to client mode if possible */
+ if (!spice_channel_get_read_only(SPICE_CHANNEL(channel)) &&
+ supported & SPICE_MOUSE_MODE_CLIENT &&
+ current != SPICE_MOUSE_MODE_CLIENT) {
+ SpiceMsgcMainMouseModeRequest req = {
+ .mode = SPICE_MOUSE_MODE_CLIENT,
+ };
+ SpiceMsgOut *out;
+
+ out = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_MAIN_MOUSE_MODE_REQUEST);
+ out->marshallers->msgc_main_mouse_mode_request(out->marshaller, &req);
+ spice_msg_out_send_internal(out);
+ }
+}
+
+/* coroutine context */
+static void main_handle_init(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMainChannelPrivate *c = SPICE_MAIN_CHANNEL(channel)->priv;
+ SpiceMsgMainInit *init = spice_msg_in_parsed(in);
+ SpiceSession *session;
+ SpiceMsgOut *out;
+
+ session = spice_channel_get_session(channel);
+ spice_session_set_connection_id(session, init->session_id);
+
+ set_mouse_mode(SPICE_MAIN_CHANNEL(channel), init->supported_mouse_modes,
+ init->current_mouse_mode);
+
+ spice_session_set_mm_time(session, init->multi_media_time);
+ spice_session_set_caches_hints(session, init->ram_hint, init->display_channels_hint);
+
+ c->agent_tokens = init->agent_tokens;
+ if (init->agent_connected)
+ agent_start(SPICE_MAIN_CHANNEL(channel));
+
+ if (spice_session_migrate_after_main_init(session))
+ return;
+
+ out = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_MAIN_ATTACH_CHANNELS);
+ spice_msg_out_send_internal(out);
+}
+
+/* coroutine context */
+static void main_handle_name(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgMainName *name = spice_msg_in_parsed(in);
+ SpiceSession *session = spice_channel_get_session(channel);
+
+ SPICE_DEBUG("server name: %s", name->name);
+ spice_session_set_name(session, (const gchar *)name->name);
+}
+
+/* coroutine context */
+static void main_handle_uuid(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgMainUuid *uuid = spice_msg_in_parsed(in);
+ SpiceSession *session = spice_channel_get_session(channel);
+ gchar *uuid_str = spice_uuid_to_string(uuid->uuid);
+
+ SPICE_DEBUG("server uuid: %s", uuid_str);
+ spice_session_set_uuid(session, uuid->uuid);
+
+ g_free(uuid_str);
+}
+
+/* coroutine context */
+static void main_handle_mm_time(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceSession *session;
+ SpiceMsgMainMultiMediaTime *msg = spice_msg_in_parsed(in);
+
+ session = spice_channel_get_session(channel);
+ spice_session_set_mm_time(session, msg->time);
+}
+
+typedef struct channel_new {
+ SpiceSession *session;
+ int type;
+ int id;
+} channel_new_t;
+
+/* main context */
+static gboolean _channel_new(channel_new_t *c)
+{
+ g_return_val_if_fail(c != NULL, FALSE);
+
+ spice_channel_new(c->session, c->type, c->id);
+
+ g_object_unref(c->session);
+ g_free(c);
+
+ return FALSE;
+}
+
+/* coroutine context */
+static void main_handle_channels_list(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgChannels *msg = spice_msg_in_parsed(in);
+ SpiceSession *session;
+ int i;
+
+ session = spice_channel_get_session(channel);
+
+ /* guarantee that uuid is notified before setting up the channels, even if
+ * the server is older and doesn't actually send the uuid */
+ g_coroutine_object_notify(G_OBJECT(session), "uuid");
+
+ for (i = 0; i < msg->num_of_channels; i++) {
+ channel_new_t *c;
+
+ c = g_new(channel_new_t, 1);
+ c->session = g_object_ref(session);
+ c->type = msg->channels[i].type;
+ c->id = msg->channels[i].id;
+ /* no need to explicitely switch to main context, since
+ synchronous call is not needed. */
+ /* no need to track idle, session is refed */
+ g_idle_add((GSourceFunc)_channel_new, c);
+ }
+}
+
+/* coroutine context */
+static void main_handle_mouse_mode(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgMainMouseMode *msg = spice_msg_in_parsed(in);
+ set_mouse_mode(SPICE_MAIN_CHANNEL(channel), msg->supported_modes, msg->current_mode);
+}
+
+/* coroutine context */
+static void main_handle_agent_connected(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ agent_start(SPICE_MAIN_CHANNEL(channel));
+}
+
+/* coroutine context */
+static void main_handle_agent_connected_tokens(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMainChannelPrivate *c = SPICE_MAIN_CHANNEL(channel)->priv;
+ SpiceMsgMainAgentConnectedTokens *msg = spice_msg_in_parsed(in);
+
+ c->agent_tokens = msg->num_tokens;
+ agent_start(SPICE_MAIN_CHANNEL(channel));
+}
+
+/* coroutine context */
+static void main_handle_agent_disconnected(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ agent_stopped(SPICE_MAIN_CHANNEL(channel));
+}
+
+static void file_xfer_task_free(SpiceFileXferTask *task)
+{
+ SpiceMainChannelPrivate *c;
+
+ g_return_if_fail(task != NULL);
+
+ c = task->channel->priv;
+ g_hash_table_remove(c->file_xfer_tasks, GUINT_TO_POINTER(task->id));
+
+ g_clear_object(&task->channel);
+ g_clear_object(&task->file);
+ g_clear_object(&task->file_stream);
+ g_free(task);
+}
+
+/* main context */
+static void file_xfer_close_cb(GObject *object,
+ GAsyncResult *close_res,
+ gpointer user_data)
+{
+ GSimpleAsyncResult *res;
+ SpiceFileXferTask *task;
+ GError *error = NULL;
+
+ task = user_data;
+
+ if (object) {
+ GInputStream *stream = G_INPUT_STREAM(object);
+ g_input_stream_close_finish(stream, close_res, &error);
+ if (error) {
+ /* This error dont need to report to user, just print a log */
+ SPICE_DEBUG("close file error: %s", error->message);
+ g_clear_error(&error);
+ }
+ }
+
+ /* Notify to user that files have been transferred or something error
+ happened. */
+ res = g_simple_async_result_new(G_OBJECT(task->channel),
+ task->callback,
+ task->user_data,
+ spice_main_file_copy_async);
+ if (task->error) {
+ g_simple_async_result_take_error(res, task->error);
+ g_simple_async_result_set_op_res_gboolean(res, FALSE);
+ } else {
+ g_simple_async_result_set_op_res_gboolean(res, TRUE);
+ }
+ g_simple_async_result_complete_in_idle(res);
+ g_object_unref(res);
+
+ file_xfer_task_free(task);
+}
+
+static void file_xfer_data_flushed_cb(GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SpiceFileXferTask *task = user_data;
+ SpiceMainChannel *channel = (SpiceMainChannel *)source_object;
+ GError *error = NULL;
+
+ task->pending = FALSE;
+ file_xfer_flush_finish(channel, res, &error);
+ if (error || task->error) {
+ file_xfer_completed(task, error);
+ return;
+ }
+
+ if (task->progress_callback)
+ task->progress_callback(task->read_bytes, task->file_size,
+ task->progress_callback_data);
+
+ /* Read more data */
+ file_xfer_continue_read(task);
+}
+
+static void file_xfer_queue(SpiceFileXferTask *task, int data_size)
+{
+ VDAgentFileXferDataMessage msg;
+ SpiceMainChannel *channel = SPICE_MAIN_CHANNEL(task->channel);
+
+ msg.id = task->id;
+ msg.size = data_size;
+ agent_msg_queue_many(channel, VD_AGENT_FILE_XFER_DATA,
+ &msg, sizeof(msg),
+ task->buffer, data_size, NULL);
+ spice_channel_wakeup(SPICE_CHANNEL(channel), FALSE);
+}
+
+/* main context */
+static void file_xfer_read_cb(GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SpiceFileXferTask *task = user_data;
+ SpiceMainChannel *channel = task->channel;
+ gssize count;
+ GError *error = NULL;
+
+ task->pending = FALSE;
+ count = g_input_stream_read_finish(G_INPUT_STREAM(task->file_stream),
+ res, &error);
+ /* Check for pending earlier errors */
+ if (task->error) {
+ file_xfer_completed(task, error);
+ return;
+ }
+
+ if (count > 0 || task->file_size == 0) {
+ task->read_bytes += count;
+ file_xfer_queue(task, count);
+ if (count == 0)
+ return;
+ file_xfer_flush_async(channel, task->cancellable,
+ file_xfer_data_flushed_cb, task);
+ task->pending = TRUE;
+ } else if (error) {
+ VDAgentFileXferStatusMessage msg = {
+ .id = task->id,
+ .result = VD_AGENT_FILE_XFER_STATUS_ERROR,
+ };
+ agent_msg_queue_many(task->channel, VD_AGENT_FILE_XFER_STATUS,
+ &msg, sizeof(msg), NULL);
+ spice_channel_wakeup(SPICE_CHANNEL(task->channel), FALSE);
+ file_xfer_completed(task, error);
+ }
+ /* else EOF, do nothing (wait for VD_AGENT_FILE_XFER_STATUS from agent) */
+}
+
+/* coroutine context */
+static void file_xfer_continue_read(SpiceFileXferTask *task)
+{
+ g_input_stream_read_async(G_INPUT_STREAM(task->file_stream),
+ task->buffer,
+ FILE_XFER_CHUNK_SIZE,
+ G_PRIORITY_DEFAULT,
+ task->cancellable,
+ file_xfer_read_cb,
+ task);
+ task->pending = TRUE;
+}
+
+/* coroutine context */
+static void file_xfer_handle_status(SpiceMainChannel *channel,
+ VDAgentFileXferStatusMessage *msg)
+{
+ SpiceMainChannelPrivate *c = channel->priv;
+ SpiceFileXferTask *task;
+ GError *error = NULL;
+
+
+ task = g_hash_table_lookup(c->file_xfer_tasks, GUINT_TO_POINTER(msg->id));
+ if (task == NULL) {
+ SPICE_DEBUG("cannot find task %d", msg->id);
+ return;
+ }
+
+ SPICE_DEBUG("task %d received response %d", msg->id, msg->result);
+
+ switch (msg->result) {
+ case VD_AGENT_FILE_XFER_STATUS_CAN_SEND_DATA:
+ if (task->pending) {
+ error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "transfer received CAN_SEND_DATA in pending state");
+ break;
+ }
+ file_xfer_continue_read(task);
+ return;
+ case VD_AGENT_FILE_XFER_STATUS_CANCELLED:
+ error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "transfer is cancelled by spice agent");
+ break;
+ case VD_AGENT_FILE_XFER_STATUS_ERROR:
+ error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "some errors occurred in the spice agent");
+ break;
+ case VD_AGENT_FILE_XFER_STATUS_SUCCESS:
+ if (task->pending)
+ error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "transfer received success in pending state");
+ break;
+ default:
+ g_warn_if_reached();
+ error = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "unhandled status type: %u", msg->result);
+ break;
+ }
+
+ file_xfer_completed(task, error);
+}
+
+/* any context: the message is not flushed immediately,
+ you can wakeup() the channel coroutine or send_msg_queue() */
+static void agent_max_clipboard(SpiceMainChannel *self)
+{
+ VDAgentMaxClipboard msg = { .max = spice_main_get_max_clipboard(self) };
+
+ if (!test_agent_cap(self, VD_AGENT_CAP_MAX_CLIPBOARD))
+ return;
+
+ agent_msg_queue(self, VD_AGENT_MAX_CLIPBOARD, sizeof(VDAgentMaxClipboard), &msg);
+}
+
+static void spice_main_set_max_clipboard(SpiceMainChannel *self, gint max)
+{
+ SpiceMainChannelPrivate *c;
+
+ g_return_if_fail(SPICE_IS_MAIN_CHANNEL(self));
+ g_return_if_fail(max >= -1);
+
+ c = self->priv;
+ if (max == spice_main_get_max_clipboard(self))
+ return;
+
+ c->max_clipboard = max;
+ agent_max_clipboard(self);
+ spice_channel_wakeup(SPICE_CHANNEL(self), FALSE);
+}
+
+/* coroutine context */
+static void main_agent_handle_msg(SpiceChannel *channel,
+ VDAgentMessage *msg, gpointer payload)
+{
+ SpiceMainChannel *self = SPICE_MAIN_CHANNEL(channel);
+ SpiceMainChannelPrivate *c = self->priv;
+ guint8 selection = VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD;
+
+ g_return_if_fail(msg->protocol == VD_AGENT_PROTOCOL);
+
+ switch (msg->type) {
+ case VD_AGENT_CLIPBOARD_RELEASE:
+ case VD_AGENT_CLIPBOARD_REQUEST:
+ case VD_AGENT_CLIPBOARD_GRAB:
+ case VD_AGENT_CLIPBOARD:
+ if (test_agent_cap(self, VD_AGENT_CAP_CLIPBOARD_SELECTION)) {
+ selection = *((guint8*)payload);
+ payload = ((guint8*)payload) + 4;
+ msg->size -= 4;
+ }
+ break;
+ default:
+ break;
+ }
+
+ switch (msg->type) {
+ case VD_AGENT_ANNOUNCE_CAPABILITIES:
+ {
+ VDAgentAnnounceCapabilities *caps = payload;
+ int i, size;
+
+ size = VD_AGENT_CAPS_SIZE_FROM_MSG_SIZE(msg->size);
+ if (size > VD_AGENT_CAPS_SIZE)
+ size = VD_AGENT_CAPS_SIZE;
+ memset(c->agent_caps, 0, sizeof(c->agent_caps));
+ for (i = 0; i < size * 32; i++) {
+ if (!VD_AGENT_HAS_CAPABILITY(caps->caps, size, i))
+ continue;
+ SPICE_DEBUG("%s: cap: %d (%s)", __FUNCTION__,
+ i, NAME(agent_caps, i));
+ VD_AGENT_SET_CAPABILITY(c->agent_caps, i);
+ }
+ c->agent_caps_received = true;
+ g_coroutine_signal_emit(self, signals[SPICE_MAIN_AGENT_UPDATE], 0);
+ update_display_timer(SPICE_MAIN_CHANNEL(channel), 0);
+
+ if (caps->request)
+ agent_announce_caps(self);
+
+ if (test_agent_cap(self, VD_AGENT_CAP_DISPLAY_CONFIG) &&
+ !c->agent_display_config_sent) {
+ agent_display_config(self);
+ c->agent_display_config_sent = true;
+ }
+
+ agent_sync_audio_playback(self);
+ agent_sync_audio_record(self);
+
+ agent_max_clipboard(self);
+
+ agent_send_msg_queue(self);
+
+ break;
+ }
+ case VD_AGENT_CLIPBOARD:
+ {
+ VDAgentClipboard *cb = payload;
+ g_coroutine_signal_emit(self, signals[SPICE_MAIN_CLIPBOARD_SELECTION], 0, selection,
+ cb->type, cb->data, msg->size - sizeof(VDAgentClipboard));
+
+ if (selection == VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD)
+ g_coroutine_signal_emit(self, signals[SPICE_MAIN_CLIPBOARD], 0,
+ cb->type, cb->data, msg->size - sizeof(VDAgentClipboard));
+ break;
+ }
+ case VD_AGENT_CLIPBOARD_GRAB:
+ {
+ gboolean ret;
+ g_coroutine_signal_emit(self, signals[SPICE_MAIN_CLIPBOARD_SELECTION_GRAB], 0, selection,
+ (guint8*)payload, msg->size / sizeof(uint32_t), &ret);
+ if (selection == VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD)
+ g_coroutine_signal_emit(self, signals[SPICE_MAIN_CLIPBOARD_GRAB], 0,
+ payload, msg->size / sizeof(uint32_t), &ret);
+ break;
+ }
+ case VD_AGENT_CLIPBOARD_REQUEST:
+ {
+ gboolean ret;
+ VDAgentClipboardRequest *req = payload;
+ g_coroutine_signal_emit(self, signals[SPICE_MAIN_CLIPBOARD_SELECTION_REQUEST], 0, selection,
+ req->type, &ret);
+
+ if (selection == VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD)
+ g_coroutine_signal_emit(self, signals[SPICE_MAIN_CLIPBOARD_REQUEST], 0,
+ req->type, &ret);
+ break;
+ }
+ case VD_AGENT_CLIPBOARD_RELEASE:
+ {
+ g_coroutine_signal_emit(self, signals[SPICE_MAIN_CLIPBOARD_SELECTION_RELEASE], 0, selection);
+
+ if (selection == VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD)
+ g_coroutine_signal_emit(self, signals[SPICE_MAIN_CLIPBOARD_RELEASE], 0);
+ break;
+ }
+ case VD_AGENT_REPLY:
+ {
+ VDAgentReply *reply = payload;
+ SPICE_DEBUG("%s: reply: type %d, %s", __FUNCTION__, reply->type,
+ reply->error == VD_AGENT_SUCCESS ? "success" : "error");
+ break;
+ }
+ case VD_AGENT_FILE_XFER_STATUS:
+ file_xfer_handle_status(self, payload);
+ break;
+ default:
+ g_warning("unhandled agent message type: %u (%s), size %u",
+ msg->type, NAME(agent_msg_types, msg->type), msg->size);
+ }
+}
+
+/* coroutine context */
+static void main_handle_agent_data_msg(SpiceChannel* channel, int* msg_size, guchar** msg_pos)
+{
+ SpiceMainChannelPrivate *c = SPICE_MAIN_CHANNEL(channel)->priv;
+ int n;
+
+ if (c->agent_msg_pos < sizeof(VDAgentMessage)) {
+ n = MIN(sizeof(VDAgentMessage) - c->agent_msg_pos, *msg_size);
+ memcpy((uint8_t*)&c->agent_msg + c->agent_msg_pos, *msg_pos, n);
+ c->agent_msg_pos += n;
+ *msg_size -= n;
+ *msg_pos += n;
+ if (c->agent_msg_pos == sizeof(VDAgentMessage)) {
+ SPICE_DEBUG("agent msg start: msg_size=%d, protocol=%d, type=%d",
+ c->agent_msg.size, c->agent_msg.protocol, c->agent_msg.type);
+ g_return_if_fail(c->agent_msg_data == NULL);
+ c->agent_msg_data = g_malloc0(c->agent_msg.size);
+ }
+ }
+
+ if (c->agent_msg_pos >= sizeof(VDAgentMessage)) {
+ n = MIN(sizeof(VDAgentMessage) + c->agent_msg.size - c->agent_msg_pos, *msg_size);
+ memcpy(c->agent_msg_data + c->agent_msg_pos - sizeof(VDAgentMessage), *msg_pos, n);
+ c->agent_msg_pos += n;
+ *msg_size -= n;
+ *msg_pos += n;
+ }
+
+ if (c->agent_msg_pos == sizeof(VDAgentMessage) + c->agent_msg.size) {
+ main_agent_handle_msg(channel, &c->agent_msg, c->agent_msg_data);
+ g_free(c->agent_msg_data);
+ c->agent_msg_data = NULL;
+ c->agent_msg_pos = 0;
+ }
+}
+
+/* coroutine context */
+static void main_handle_agent_data(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMainChannelPrivate *c = SPICE_MAIN_CHANNEL(channel)->priv;
+ guint8 *data;
+ int len;
+
+ g_warn_if_fail(c->agent_connected);
+
+ /* shortcut to avoid extra message allocation & copy if possible */
+ if (c->agent_msg_pos == 0) {
+ VDAgentMessage *msg;
+ guint msg_size;
+
+ msg = spice_msg_in_raw(in, &len);
+ msg_size = msg->size;
+
+ if (msg_size + sizeof(VDAgentMessage) == len) {
+ main_agent_handle_msg(channel, msg, msg->data);
+ return;
+ }
+ }
+
+ data = spice_msg_in_raw(in, &len);
+ while (len > 0) {
+ main_handle_agent_data_msg(channel, &len, &data);
+ }
+}
+
+/* coroutine context */
+static void main_handle_agent_token(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgMainAgentTokens *tokens = spice_msg_in_parsed(in);
+ SpiceMainChannelPrivate *c = SPICE_MAIN_CHANNEL(channel)->priv;
+
+ c->agent_tokens += tokens->num_tokens;
+
+ agent_send_msg_queue(SPICE_MAIN_CHANNEL(channel));
+}
+
+/* main context */
+static void migrate_channel_new_cb(SpiceSession *s, SpiceChannel *channel, gpointer data)
+{
+ g_signal_connect(channel, "channel-event",
+ G_CALLBACK(migrate_channel_event_cb), data);
+}
+
+static SpiceChannel* migrate_channel_connect(spice_migrate *mig, int type, int id)
+{
+ SPICE_DEBUG("migrate_channel_connect %d:%d", type, id);
+
+ SpiceChannel *newc = spice_channel_new(mig->session, type, id);
+ spice_channel_connect(newc);
+ mig->nchannels++;
+
+ return newc;
+}
+
+/* coroutine context */
+static void spice_main_channel_send_migration_handshake(SpiceChannel *channel)
+{
+ SpiceMainChannelPrivate *c = SPICE_MAIN_CHANNEL(channel)->priv;
+
+ if (!spice_channel_test_capability(channel, SPICE_MAIN_CAP_SEAMLESS_MIGRATE)) {
+ c->migrate_data->do_seamless = false;
+ g_idle_add(main_migrate_handshake_done, c->migrate_data);
+ } else {
+ SpiceMsgcMainMigrateDstDoSeamless msg_data;
+ SpiceMsgOut *msg_out;
+
+ msg_data.src_version = c->migrate_data->src_mig_version;
+
+ msg_out = spice_msg_out_new(channel, SPICE_MSGC_MAIN_MIGRATE_DST_DO_SEAMLESS);
+ msg_out->marshallers->msgc_main_migrate_dst_do_seamless(msg_out->marshaller, &msg_data);
+ spice_msg_out_send_internal(msg_out);
+ }
+}
+
+/* main context */
+static void migrate_channel_event_cb(SpiceChannel *channel, SpiceChannelEvent event,
+ gpointer data)
+{
+ spice_migrate *mig = data;
+ SpiceChannelPrivate *c = SPICE_CHANNEL(channel)->priv;
+ SpiceSession *session;
+
+ g_return_if_fail(mig->nchannels > 0);
+ g_signal_handlers_disconnect_by_func(channel, migrate_channel_event_cb, data);
+
+ session = spice_channel_get_session(mig->src_channel);
+
+ switch (event) {
+ case SPICE_CHANNEL_OPENED:
+
+ if (c->channel_type == SPICE_CHANNEL_MAIN) {
+ if (mig->do_seamless) {
+ SpiceMainChannelPrivate *main_priv = SPICE_MAIN_CHANNEL(channel)->priv;
+
+ c->state = SPICE_CHANNEL_STATE_MIGRATION_HANDSHAKE;
+ mig->dst_channel = channel;
+ main_priv->migrate_data = mig;
+ } else {
+ c->state = SPICE_CHANNEL_STATE_MIGRATING;
+ mig->nchannels--;
+ }
+ /* now connect the rest of the channels */
+ GList *channels, *l;
+ l = channels = spice_session_get_channels(session);
+ while (l != NULL) {
+ SpiceChannelPrivate *curc = SPICE_CHANNEL(l->data)->priv;
+ l = l->next;
+ if (curc->channel_type == SPICE_CHANNEL_MAIN)
+ continue;
+ migrate_channel_connect(mig, curc->channel_type, curc->channel_id);
+ }
+ g_list_free(channels);
+ } else {
+ c->state = SPICE_CHANNEL_STATE_MIGRATING;
+ mig->nchannels--;
+ }
+
+ SPICE_DEBUG("migration: channel opened chan:%p, left %d", channel, mig->nchannels);
+ if (mig->nchannels == 0)
+ coroutine_yieldto(mig->from, NULL);
+ break;
+ default:
+ SPICE_DEBUG("error or unhandled channel event during migration: %d", event);
+ /* go back to main channel to report error */
+ coroutine_yieldto(mig->from, NULL);
+ }
+}
+
+/* main context */
+static gboolean main_migrate_handshake_done(gpointer data)
+{
+ spice_migrate *mig = data;
+ SpiceChannelPrivate *c = SPICE_CHANNEL(mig->dst_channel)->priv;
+
+ g_return_val_if_fail(c->channel_type == SPICE_CHANNEL_MAIN, FALSE);
+ g_return_val_if_fail(c->state == SPICE_CHANNEL_STATE_MIGRATION_HANDSHAKE, FALSE);
+
+ c->state = SPICE_CHANNEL_STATE_MIGRATING;
+ mig->nchannels--;
+ if (mig->nchannels == 0)
+ coroutine_yieldto(mig->from, NULL);
+ return FALSE;
+}
+
+#ifdef __GNUC__
+typedef struct __attribute__ ((__packed__)) OldRedMigrationBegin {
+#else
+typedef struct __declspec(align(1)) OldRedMigrationBegin {
+#endif
+ uint16_t port;
+ uint16_t sport;
+ char host[0];
+} OldRedMigrationBegin;
+
+/* main context */
+static gboolean migrate_connect(gpointer data)
+{
+ spice_migrate *mig = data;
+ SpiceChannelPrivate *c;
+ int port, sport;
+ const char *host;
+
+ g_return_val_if_fail(mig != NULL, FALSE);
+ g_return_val_if_fail(mig->info != NULL, FALSE);
+ g_return_val_if_fail(mig->nchannels == 0, FALSE);
+ c = SPICE_CHANNEL(mig->src_channel)->priv;
+ g_return_val_if_fail(c != NULL, FALSE);
+ g_return_val_if_fail(mig->session != NULL, FALSE);
+
+ spice_session_set_migration_state(mig->session, SPICE_SESSION_MIGRATION_CONNECTING);
+
+ if ((c->peer_hdr.major_version == 1) &&
+ (c->peer_hdr.minor_version < 1)) {
+ OldRedMigrationBegin *info = (OldRedMigrationBegin *)mig->info;
+ SPICE_DEBUG("migrate_begin old %s %d %d",
+ info->host, info->port, info->sport);
+ port = info->port;
+ sport = info->sport;
+ host = info->host;
+ } else {
+ SpiceMigrationDstInfo *info = mig->info;
+ SPICE_DEBUG("migrate_begin %d %s %d %d",
+ info->host_size, info->host_data, info->port, info->sport);
+ port = info->port;
+ sport = info->sport;
+ host = (char*)info->host_data;
+
+ if ((c->peer_hdr.major_version == 1) ||
+ (c->peer_hdr.major_version == 2 && c->peer_hdr.minor_version < 1)) {
+ GByteArray *pubkey = g_byte_array_new();
+
+ g_byte_array_append(pubkey, info->pub_key_data, info->pub_key_size);
+ g_object_set(mig->session,
+ "pubkey", pubkey,
+ "verify", SPICE_SESSION_VERIFY_PUBKEY,
+ NULL);
+ g_byte_array_unref(pubkey);
+ } else if (info->cert_subject_size == 0 ||
+ strlen((const char*)info->cert_subject_data) == 0) {
+ /* only verify hostname if no cert subject */
+ g_object_set(mig->session, "verify", SPICE_SESSION_VERIFY_HOSTNAME, NULL);
+ } else {
+ gchar *subject = g_alloca(info->cert_subject_size + 1);
+ strncpy(subject, (const char*)info->cert_subject_data, info->cert_subject_size);
+ subject[info->cert_subject_size] = '\0';
+
+ // session data are already copied
+ g_object_set(mig->session,
+ "cert-subject", subject,
+ "verify", SPICE_SESSION_VERIFY_SUBJECT,
+ NULL);
+ }
+ }
+
+ if (g_getenv("SPICE_MIG_HOST"))
+ host = g_getenv("SPICE_MIG_HOST");
+
+ g_object_set(mig->session, "host", host, NULL);
+ spice_session_set_port(mig->session, port, FALSE);
+ spice_session_set_port(mig->session, sport, TRUE);
+ g_signal_connect(mig->session, "channel-new",
+ G_CALLBACK(migrate_channel_new_cb), mig);
+
+ g_signal_emit(mig->src_channel, signals[SPICE_MIGRATION_STARTED], 0,
+ mig->session);
+
+ /* the migration process is in 2 steps, first the main channel and
+ then the rest of the channels */
+ migrate_channel_connect(mig, SPICE_CHANNEL_MAIN, 0);
+
+ return FALSE;
+}
+
+/* coroutine context */
+static void main_migrate_connect(SpiceChannel *channel,
+ SpiceMigrationDstInfo *dst_info, bool do_seamless,
+ uint32_t src_mig_version)
+{
+ SpiceMainChannelPrivate *main_priv = SPICE_MAIN_CHANNEL(channel)->priv;
+ int reply_type = SPICE_MSGC_MAIN_MIGRATE_CONNECT_ERROR;
+ spice_migrate mig = { 0, };
+ SpiceMsgOut *out;
+ SpiceSession *session;
+
+ mig.src_channel = channel;
+ mig.info = dst_info;
+ mig.from = coroutine_self();
+ mig.do_seamless = do_seamless;
+ mig.src_mig_version = src_mig_version;
+
+ CHANNEL_DEBUG(channel, "migrate connect");
+ session = spice_channel_get_session(channel);
+ mig.session = spice_session_new_from_session(session);
+ if (mig.session == NULL)
+ goto end;
+ if (!spice_session_set_migration_session(session, mig.session))
+ goto end;
+
+ main_priv->migrate_data = &mig;
+
+ /* no need to track idle, call is sync for this coroutine */
+ g_idle_add(migrate_connect, &mig);
+
+ /* switch to main loop and wait for connections */
+ coroutine_yield(NULL);
+
+ if (mig.nchannels != 0) {
+ CHANNEL_DEBUG(channel, "migrate failed: some channels failed to connect");
+ spice_session_abort_migration(session);
+ } else {
+ if (mig.do_seamless) {
+ SPICE_DEBUG("migration (seamless): connections all ok");
+ reply_type = SPICE_MSGC_MAIN_MIGRATE_CONNECTED_SEAMLESS;
+ } else {
+ SPICE_DEBUG("migration (semi-seamless): connections all ok");
+ reply_type = SPICE_MSGC_MAIN_MIGRATE_CONNECTED;
+ }
+ spice_session_start_migrating(spice_channel_get_session(channel),
+ mig.do_seamless);
+ }
+
+end:
+ CHANNEL_DEBUG(channel, "migrate connect reply %d", reply_type);
+ out = spice_msg_out_new(SPICE_CHANNEL(channel), reply_type);
+ spice_msg_out_send(out);
+}
+
+/* coroutine context */
+static void main_handle_migrate_begin(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgMainMigrationBegin *msg = spice_msg_in_parsed(in);
+
+ main_migrate_connect(channel, &msg->dst_info, false, 0);
+}
+
+/* coroutine context */
+static void main_handle_migrate_begin_seamless(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgMainMigrateBeginSeamless *msg = spice_msg_in_parsed(in);
+
+ main_migrate_connect(channel, &msg->dst_info, true, msg->src_mig_version);
+}
+
+static void main_handle_migrate_dst_seamless_ack(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceChannelPrivate *c = SPICE_CHANNEL(channel)->priv;
+ SpiceMainChannelPrivate *main_priv = SPICE_MAIN_CHANNEL(channel)->priv;
+
+ g_return_if_fail(c->state == SPICE_CHANNEL_STATE_MIGRATION_HANDSHAKE);
+ main_priv->migrate_data->do_seamless = true;
+ g_idle_add(main_migrate_handshake_done, main_priv->migrate_data);
+}
+
+static void main_handle_migrate_dst_seamless_nack(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceChannelPrivate *c = SPICE_CHANNEL(channel)->priv;
+ SpiceMainChannelPrivate *main_priv = SPICE_MAIN_CHANNEL(channel)->priv;
+
+ g_return_if_fail(c->state == SPICE_CHANNEL_STATE_MIGRATION_HANDSHAKE);
+ main_priv->migrate_data->do_seamless = false;
+ g_idle_add(main_migrate_handshake_done, main_priv->migrate_data);
+}
+
+/* main context */
+static gboolean migrate_delayed(gpointer data)
+{
+ SpiceChannel *channel = data;
+ SpiceMainChannelPrivate *c = SPICE_MAIN_CHANNEL(channel)->priv;
+
+ g_warn_if_fail(c->migrate_delayed_id != 0);
+ c->migrate_delayed_id = 0;
+
+ spice_session_migrate_end(channel->priv->session);
+
+ return FALSE;
+}
+
+/* coroutine context */
+static void main_handle_migrate_end(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMainChannelPrivate *c = SPICE_MAIN_CHANNEL(channel)->priv;
+
+ SPICE_DEBUG("migrate end");
+
+ g_return_if_fail(c->migrate_delayed_id == 0);
+ g_return_if_fail(spice_channel_test_capability(channel, SPICE_MAIN_CAP_SEMI_SEAMLESS_MIGRATE));
+
+ c->migrate_delayed_id = g_idle_add(migrate_delayed, channel);
+}
+
+/* main context */
+static gboolean switch_host_delayed(gpointer data)
+{
+ SpiceChannel *channel = data;
+ SpiceSession *session;
+ SpiceMainChannelPrivate *c = SPICE_MAIN_CHANNEL(channel)->priv;
+
+ g_warn_if_fail(c->switch_host_delayed_id != 0);
+ c->switch_host_delayed_id = 0;
+
+ session = spice_channel_get_session(channel);
+
+ spice_channel_disconnect(channel, SPICE_CHANNEL_SWITCHING);
+ spice_session_switching_disconnect(session);
+
+ return FALSE;
+}
+
+/* coroutine context */
+static void main_handle_migrate_switch_host(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceMsgMainMigrationSwitchHost *mig = spice_msg_in_parsed(in);
+ SpiceSession *session;
+ char *host = (char *)mig->host_data;
+ char *subject = NULL;
+ SpiceMainChannelPrivate *c = SPICE_MAIN_CHANNEL(channel)->priv;
+
+ g_return_if_fail(host[mig->host_size - 1] == '\0');
+
+ if (mig->cert_subject_size) {
+ subject = (char *)mig->cert_subject_data;
+ g_return_if_fail(subject[mig->cert_subject_size - 1] == '\0');
+ }
+
+ SPICE_DEBUG("migrate_switch %s %d %d %s",
+ host, mig->port, mig->sport, subject);
+
+ if (c->switch_host_delayed_id != 0) {
+ g_warning("Switching host already in progress, aborting it");
+ g_warn_if_fail(g_source_remove(c->switch_host_delayed_id));
+ c->switch_host_delayed_id = 0;
+ }
+
+ session = spice_channel_get_session(channel);
+ spice_session_set_migration_state(session, SPICE_SESSION_MIGRATION_SWITCHING);
+ g_object_set(session,
+ "host", host,
+ "cert-subject", subject,
+ NULL);
+ spice_session_set_port(session, mig->port, FALSE);
+ spice_session_set_port(session, mig->sport, TRUE);
+
+ c->switch_host_delayed_id = g_idle_add(switch_host_delayed, channel);
+}
+
+/* coroutine context */
+static void main_handle_migrate_cancel(SpiceChannel *channel,
+ SpiceMsgIn *in G_GNUC_UNUSED)
+{
+ SpiceSession *session;
+
+ SPICE_DEBUG("migrate_cancel");
+ session = spice_channel_get_session(channel);
+ spice_session_abort_migration(session);
+}
+
+static void channel_set_handlers(SpiceChannelClass *klass)
+{
+ static const spice_msg_handler handlers[] = {
+ [ SPICE_MSG_MAIN_INIT ] = main_handle_init,
+ [ SPICE_MSG_MAIN_NAME ] = main_handle_name,
+ [ SPICE_MSG_MAIN_UUID ] = main_handle_uuid,
+ [ SPICE_MSG_MAIN_CHANNELS_LIST ] = main_handle_channels_list,
+ [ SPICE_MSG_MAIN_MOUSE_MODE ] = main_handle_mouse_mode,
+ [ SPICE_MSG_MAIN_MULTI_MEDIA_TIME ] = main_handle_mm_time,
+
+ [ SPICE_MSG_MAIN_AGENT_CONNECTED ] = main_handle_agent_connected,
+ [ SPICE_MSG_MAIN_AGENT_DISCONNECTED ] = main_handle_agent_disconnected,
+ [ SPICE_MSG_MAIN_AGENT_DATA ] = main_handle_agent_data,
+ [ SPICE_MSG_MAIN_AGENT_TOKEN ] = main_handle_agent_token,
+
+ [ SPICE_MSG_MAIN_MIGRATE_BEGIN ] = main_handle_migrate_begin,
+ [ SPICE_MSG_MAIN_MIGRATE_END ] = main_handle_migrate_end,
+ [ SPICE_MSG_MAIN_MIGRATE_CANCEL ] = main_handle_migrate_cancel,
+ [ SPICE_MSG_MAIN_MIGRATE_SWITCH_HOST ] = main_handle_migrate_switch_host,
+ [ SPICE_MSG_MAIN_AGENT_CONNECTED_TOKENS ] = main_handle_agent_connected_tokens,
+ [ SPICE_MSG_MAIN_MIGRATE_BEGIN_SEAMLESS ] = main_handle_migrate_begin_seamless,
+ [ SPICE_MSG_MAIN_MIGRATE_DST_SEAMLESS_ACK] = main_handle_migrate_dst_seamless_ack,
+ [ SPICE_MSG_MAIN_MIGRATE_DST_SEAMLESS_NACK] = main_handle_migrate_dst_seamless_nack,
+ };
+
+ spice_channel_set_handlers(klass, handlers, G_N_ELEMENTS(handlers));
+}
+
+/* coroutine context */
+static void spice_main_handle_msg(SpiceChannel *channel, SpiceMsgIn *msg)
+{
+ int type = spice_msg_in_type(msg);
+ SpiceChannelClass *parent_class;
+ SpiceChannelPrivate *c = SPICE_CHANNEL(channel)->priv;
+
+ parent_class = SPICE_CHANNEL_CLASS(spice_main_channel_parent_class);
+
+ if (c->state == SPICE_CHANNEL_STATE_MIGRATION_HANDSHAKE) {
+ if (type != SPICE_MSG_MAIN_MIGRATE_DST_SEAMLESS_ACK &&
+ type != SPICE_MSG_MAIN_MIGRATE_DST_SEAMLESS_NACK) {
+ g_critical("unexpected msg (%d)."
+ "Only MIGRATE_DST_SEAMLESS_ACK/NACK are allowed", type);
+ return;
+ }
+ }
+
+ parent_class->handle_msg(channel, msg);
+}
+
+/**
+ * spice_main_agent_test_capability:
+ * @channel:
+ * @cap: an agent capability identifier
+ *
+ * Test capability of a remote agent.
+ *
+ * Returns: %TRUE if @cap (channel kind capability) is available.
+ **/
+gboolean spice_main_agent_test_capability(SpiceMainChannel *channel, guint32 cap)
+{
+ g_return_val_if_fail(SPICE_IS_MAIN_CHANNEL(channel), FALSE);
+
+ return test_agent_cap(channel, cap);
+}
+
+/**
+ * spice_main_update_display:
+ * @channel:
+ * @id: display ID
+ * @x: x position
+ * @y: y position
+ * @width: display width
+ * @height: display height
+ * @update: if %TRUE, update guest resolution after 1sec.
+ *
+ * Update the display @id resolution.
+ *
+ * If @update is %TRUE, the remote configuration will be updated too
+ * after 1 second without further changes. You can send when you want
+ * without delay the new configuration to the remote with
+ * spice_main_send_monitor_config()
+ **/
+void spice_main_update_display(SpiceMainChannel *channel, int id,
+ int x, int y, int width, int height,
+ gboolean update)
+{
+ SpiceMainChannelPrivate *c;
+
+ g_return_if_fail(channel != NULL);
+ g_return_if_fail(SPICE_IS_MAIN_CHANNEL(channel));
+ g_return_if_fail(x >= 0);
+ g_return_if_fail(y >= 0);
+ g_return_if_fail(width >= 0);
+ g_return_if_fail(height >= 0);
+
+ c = SPICE_MAIN_CHANNEL(channel)->priv;
+
+ g_return_if_fail(id < SPICE_N_ELEMENTS(c->display));
+
+ c->display[id].x = x;
+ c->display[id].y = y;
+ c->display[id].width = width;
+ c->display[id].height = height;
+
+ if (update)
+ update_display_timer(channel, 1);
+}
+
+/**
+ * spice_main_set_display:
+ * @channel:
+ * @id: display ID
+ * @x: x position
+ * @y: y position
+ * @width: display width
+ * @height: display height
+ *
+ * Notify the guest of screen resolution change. The notification is
+ * sent 1 second later, if no further changes happen.
+ **/
+void spice_main_set_display(SpiceMainChannel *channel, int id,
+ int x, int y, int width, int height)
+{
+ spice_main_update_display(channel, id, x, y, width, height, TRUE);
+}
+
+/**
+ * spice_main_clipboard_grab:
+ * @channel:
+ * @types: an array of #VD_AGENT_CLIPBOARD types available in the clipboard
+ * @ntypes: the number of @types
+ *
+ * Grab the guest clipboard, with #VD_AGENT_CLIPBOARD @types.
+ *
+ * Deprecated: 0.6: use spice_main_clipboard_selection_grab() instead.
+ **/
+void spice_main_clipboard_grab(SpiceMainChannel *channel, guint32 *types, int ntypes)
+{
+ spice_main_clipboard_selection_grab(channel, VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD, types, ntypes);
+}
+
+/**
+ * spice_main_clipboard_selection_grab:
+ * @channel:
+ * @selection: one of the clipboard #VD_AGENT_CLIPBOARD_SELECTION_*
+ * @types: an array of #VD_AGENT_CLIPBOARD types available in the clipboard
+ * @ntypes: the number of @types
+ *
+ * Grab the guest clipboard, with #VD_AGENT_CLIPBOARD @types.
+ *
+ * Since: 0.6
+ **/
+void spice_main_clipboard_selection_grab(SpiceMainChannel *channel, guint selection,
+ guint32 *types, int ntypes)
+{
+ g_return_if_fail(channel != NULL);
+ g_return_if_fail(SPICE_IS_MAIN_CHANNEL(channel));
+
+ agent_clipboard_grab(channel, selection, types, ntypes);
+ spice_channel_wakeup(SPICE_CHANNEL(channel), FALSE);
+}
+
+/**
+ * spice_main_clipboard_release:
+ * @channel:
+ *
+ * Release the clipboard (for example, when the client loses the
+ * clipboard grab): Inform the guest no clipboard data is available.
+ *
+ * Deprecated: 0.6: use spice_main_clipboard_selection_release() instead.
+ **/
+void spice_main_clipboard_release(SpiceMainChannel *channel)
+{
+ spice_main_clipboard_selection_release(channel, VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD);
+}
+
+/**
+ * spice_main_clipboard_selection_release:
+ * @channel:
+ * @selection: one of the clipboard #VD_AGENT_CLIPBOARD_SELECTION_*
+ *
+ * Release the clipboard (for example, when the client loses the
+ * clipboard grab): Inform the guest no clipboard data is available.
+ *
+ * Since: 0.6
+ **/
+void spice_main_clipboard_selection_release(SpiceMainChannel *channel, guint selection)
+{
+ g_return_if_fail(channel != NULL);
+ g_return_if_fail(SPICE_IS_MAIN_CHANNEL(channel));
+
+ SpiceMainChannelPrivate *c = channel->priv;
+
+ if (!c->agent_connected)
+ return;
+
+ agent_clipboard_release(channel, selection);
+ spice_channel_wakeup(SPICE_CHANNEL(channel), FALSE);
+}
+
+/**
+ * spice_main_clipboard_notify:
+ * @channel:
+ * @type: a #VD_AGENT_CLIPBOARD type
+ * @data: clipboard data
+ * @size: data length in bytes
+ *
+ * Send the clipboard data to the guest.
+ *
+ * Deprecated: 0.6: use spice_main_clipboard_selection_notify() instead.
+ **/
+void spice_main_clipboard_notify(SpiceMainChannel *channel,
+ guint32 type, const guchar *data, size_t size)
+{
+ spice_main_clipboard_selection_notify(channel, VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD,
+ type, data, size);
+}
+
+/**
+ * spice_main_clipboard_selection_notify:
+ * @channel:
+ * @selection: one of the clipboard #VD_AGENT_CLIPBOARD_SELECTION_*
+ * @type: a #VD_AGENT_CLIPBOARD type
+ * @data: clipboard data
+ * @size: data length in bytes
+ *
+ * Send the clipboard data to the guest.
+ *
+ * Since: 0.6
+ **/
+void spice_main_clipboard_selection_notify(SpiceMainChannel *channel, guint selection,
+ guint32 type, const guchar *data, size_t size)
+{
+ g_return_if_fail(channel != NULL);
+ g_return_if_fail(SPICE_IS_MAIN_CHANNEL(channel));
+
+ agent_clipboard_notify(channel, selection, type, data, size);
+ spice_channel_wakeup(SPICE_CHANNEL(channel), FALSE);
+}
+
+/**
+ * spice_main_clipboard_request:
+ * @channel:
+ * @type: a #VD_AGENT_CLIPBOARD type
+ *
+ * Request clipboard data of @type from the guest. The reply is sent
+ * through the #SpiceMainChannel::main-clipboard signal.
+ *
+ * Deprecated: 0.6: use spice_main_clipboard_selection_request() instead.
+ **/
+void spice_main_clipboard_request(SpiceMainChannel *channel, guint32 type)
+{
+ spice_main_clipboard_selection_request(channel, VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD, type);
+}
+
+/**
+ * spice_main_clipboard_selection_request:
+ * @channel:
+ * @selection: one of the clipboard #VD_AGENT_CLIPBOARD_SELECTION_*
+ * @type: a #VD_AGENT_CLIPBOARD type
+ *
+ * Request clipboard data of @type from the guest. The reply is sent
+ * through the #SpiceMainChannel::main-clipboard-selection signal.
+ *
+ * Since: 0.6
+ **/
+void spice_main_clipboard_selection_request(SpiceMainChannel *channel, guint selection, guint32 type)
+{
+ g_return_if_fail(channel != NULL);
+ g_return_if_fail(SPICE_IS_MAIN_CHANNEL(channel));
+
+ agent_clipboard_request(channel, selection, type);
+ spice_channel_wakeup(SPICE_CHANNEL(channel), FALSE);
+}
+
+/**
+ * spice_main_set_display_enabled:
+ * @channel: a #SpiceMainChannel
+ * @id: display ID (if -1: set all displays)
+ * @enabled: wether display @id is enabled
+ *
+ * When sending monitor configuration to agent guest, don't set
+ * display @id, which the agent translates to disabling the display
+ * id. Note: this will take effect next time the monitor
+ * configuration is sent.
+ *
+ * Since: 0.6
+ **/
+void spice_main_set_display_enabled(SpiceMainChannel *channel, int id, gboolean enabled)
+{
+ g_return_if_fail(channel != NULL);
+ g_return_if_fail(SPICE_IS_MAIN_CHANNEL(channel));
+ g_return_if_fail(id >= -1);
+
+ SpiceMainChannelPrivate *c = channel->priv;
+
+ if (id == -1) {
+ gint i;
+ for (i = 0; i < G_N_ELEMENTS(c->display); i++) {
+ c->display[i].enabled = enabled;
+ c->display[i].enabled_set = TRUE;
+ }
+ } else {
+ g_return_if_fail(id < G_N_ELEMENTS(c->display));
+ if (c->display[id].enabled == enabled)
+ return;
+ c->display[id].enabled = enabled;
+ c->display[id].enabled_set = TRUE;
+ }
+
+ update_display_timer(channel, 1);
+}
+
+static void file_xfer_completed(SpiceFileXferTask *task, GError *error)
+{
+ /* In case of multiple errors we only report the first error */
+ if (task->error)
+ g_clear_error(&error);
+ if (error) {
+ SPICE_DEBUG("File %s xfer failed: %s",
+ g_file_get_path(task->file), error->message);
+ task->error = error;
+ }
+
+ if (task->pending)
+ return;
+
+ if (!task->file_stream) {
+ file_xfer_close_cb(NULL, NULL, task);
+ return;
+ }
+
+ g_input_stream_close_async(G_INPUT_STREAM(task->file_stream),
+ G_PRIORITY_DEFAULT,
+ task->cancellable,
+ file_xfer_close_cb,
+ task);
+ task->pending = TRUE;
+}
+
+static void file_xfer_info_async_cb(GObject *obj, GAsyncResult *res, gpointer data)
+{
+ GFileInfo *info;
+ GFile *file = G_FILE(obj);
+ GError *error = NULL;
+ GKeyFile *keyfile = NULL;
+ gchar *basename = NULL;
+ VDAgentFileXferStartMessage msg;
+ gsize /*msg_size*/ data_len;
+ gchar *string;
+ SpiceFileXferTask *task = (SpiceFileXferTask *)data;
+
+ task->pending = FALSE;
+ info = g_file_query_info_finish(file, res, &error);
+ if (error || task->error)
+ goto failed;
+
+ task->file_size =
+ g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_STANDARD_SIZE);
+ keyfile = g_key_file_new();
+
+ /* File name */
+ basename = g_file_get_basename(file);
+ g_key_file_set_string(keyfile, "vdagent-file-xfer", "name", basename);
+ g_free(basename);
+ /* File size */
+ g_key_file_set_uint64(keyfile, "vdagent-file-xfer", "size", task->file_size);
+
+ /* Save keyfile content to memory. TODO: more file attributions
+ need to be sent to guest */
+ string = g_key_file_to_data(keyfile, &data_len, &error);
+ g_key_file_free(keyfile);
+ if (error)
+ goto failed;
+
+ /* Create file-xfer start message */
+ msg.id = task->id;
+ agent_msg_queue_many(task->channel, VD_AGENT_FILE_XFER_START,
+ &msg, sizeof(msg),
+ string, data_len + 1, NULL);
+ g_free(string);
+ spice_channel_wakeup(SPICE_CHANNEL(task->channel), FALSE);
+ return;
+
+failed:
+ file_xfer_completed(task, error);
+}
+
+static void file_xfer_read_async_cb(GObject *obj, GAsyncResult *res, gpointer data)
+{
+ GFile *file = G_FILE(obj);
+ SpiceFileXferTask *task = (SpiceFileXferTask *)data;
+ GError *error = NULL;
+
+ task->pending = FALSE;
+ task->file_stream = g_file_read_finish(file, res, &error);
+ if (error || task->error) {
+ file_xfer_completed(task, error);
+ return;
+ }
+
+ g_file_query_info_async(task->file,
+ G_FILE_ATTRIBUTE_STANDARD_SIZE,
+ G_FILE_QUERY_INFO_NONE,
+ G_PRIORITY_DEFAULT,
+ task->cancellable,
+ file_xfer_info_async_cb,
+ task);
+ task->pending = TRUE;
+}
+
+static void file_xfer_send_start_msg_async(SpiceMainChannel *channel,
+ GFile **files,
+ GFileCopyFlags flags,
+ GCancellable *cancellable,
+ GFileProgressCallback progress_callback,
+ gpointer progress_callback_data,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SpiceMainChannelPrivate *c = channel->priv;
+ SpiceFileXferTask *task;
+ static uint32_t xfer_id; /* Used to identify task id */
+ gint i;
+
+ for (i = 0; files[i] != NULL && !g_cancellable_is_cancelled(cancellable); i++) {
+ task = g_malloc0(sizeof(SpiceFileXferTask));
+ task->id = ++xfer_id;
+ task->channel = g_object_ref(channel);
+ task->file = g_object_ref(files[i]);
+ task->flags = flags;
+ task->cancellable = cancellable;
+ task->progress_callback = progress_callback;
+ task->progress_callback_data = progress_callback_data;
+ task->callback = callback;
+ task->user_data = user_data;
+
+ CHANNEL_DEBUG(task->channel, "Insert a xfer task:%d to task list", task->id);
+ g_hash_table_insert(c->file_xfer_tasks, GUINT_TO_POINTER(task->id), task);
+
+ g_file_read_async(files[i],
+ G_PRIORITY_DEFAULT,
+ cancellable,
+ file_xfer_read_async_cb,
+ task);
+ task->pending = TRUE;
+ }
+}
+
+/**
+ * spice_main_file_copy_async:
+ * @sources: #GFile to be transfer
+ * @flags: set of #GFileCopyFlags
+ * @cancellable: (allow-none): optional #GCancellable object, %NULL to ignore
+ * @progress_callback: (allow-none) (scope call): function to callback with
+ * progress information, or %NULL if progress information is not needed
+ * @progress_callback_data: (closure): user data to pass to @progress_callback
+ * @callback: a #GAsyncReadyCallback to call when the request is satisfied
+ * @user_data: the data to pass to callback function
+ *
+ * Copies the file @sources to guest
+ *
+ * If @cancellable is not %NULL, then the operation can be cancelled by
+ * triggering the cancellable object from another thread. If the operation
+ * was cancelled, the error %G_IO_ERROR_CANCELLED will be returned.
+ *
+ * If @progress_callback is not %NULL, then the operation can be monitored by
+ * setting this to a #GFileProgressCallback function. @progress_callback_data
+ * will be passed to this function. It is guaranteed that this callback will
+ * be called after all data has been transferred with the total number of bytes
+ * copied during the operation.
+ *
+ * When the operation is finished, callback will be called. You can then call
+ * spice_main_file_copy_finish() to get the result of the operation.
+ *
+ **/
+void spice_main_file_copy_async(SpiceMainChannel *channel,
+ GFile **sources,
+ GFileCopyFlags flags,
+ GCancellable *cancellable,
+ GFileProgressCallback progress_callback,
+ gpointer progress_callback_data,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SpiceMainChannelPrivate *c = channel->priv;
+
+ g_return_if_fail(channel != NULL);
+ g_return_if_fail(SPICE_IS_MAIN_CHANNEL(channel));
+ g_return_if_fail(sources != NULL);
+
+ if (!c->agent_connected) {
+ g_simple_async_report_error_in_idle(G_OBJECT(channel),
+ callback,
+ user_data,
+ SPICE_CLIENT_ERROR,
+ SPICE_CLIENT_ERROR_FAILED,
+ "The agent is not connected");
+ return;
+ }
+
+ file_xfer_send_start_msg_async(channel,
+ sources,
+ flags,
+ cancellable,
+ progress_callback,
+ progress_callback_data,
+ callback,
+ user_data);
+}
+
+/**
+ * spice_main_file_copy_finish:
+ * @result: a #GAsyncResult.
+ * @error: a #GError, or %NULL
+ *
+ * Finishes copying the file started with
+ * spice_main_file_copy_async().
+ *
+ * Returns: a %TRUE on success, %FALSE on error.
+ **/
+gboolean spice_main_file_copy_finish(SpiceMainChannel *channel,
+ GAsyncResult *result,
+ GError **error)
+{
+ GSimpleAsyncResult *simple;
+
+ g_return_val_if_fail(SPICE_IS_MAIN_CHANNEL(channel), FALSE);
+ g_return_val_if_fail(g_simple_async_result_is_valid(result,
+ G_OBJECT(channel), spice_main_file_copy_async), FALSE);
+
+ simple = (GSimpleAsyncResult *)result;
+
+ if (g_simple_async_result_propagate_error(simple, error)) {
+ return FALSE;
+ }
+
+ return g_simple_async_result_get_op_res_gboolean(simple);
+}
diff --git a/src/channel-main.h b/src/channel-main.h
new file mode 100644
index 0000000..3e4fc42
--- /dev/null
+++ b/src/channel-main.h
@@ -0,0 +1,109 @@
+/* -*- 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_CLIENT_MAIN_CHANNEL_H__
+#define __SPICE_CLIENT_MAIN_CHANNEL_H__
+
+#include "spice-client.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_MAIN_CHANNEL (spice_main_channel_get_type())
+#define SPICE_MAIN_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_MAIN_CHANNEL, SpiceMainChannel))
+#define SPICE_MAIN_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_MAIN_CHANNEL, SpiceMainChannelClass))
+#define SPICE_IS_MAIN_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_MAIN_CHANNEL))
+#define SPICE_IS_MAIN_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_MAIN_CHANNEL))
+#define SPICE_MAIN_CHANNEL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_MAIN_CHANNEL, SpiceMainChannelClass))
+
+typedef struct _SpiceMainChannel SpiceMainChannel;
+typedef struct _SpiceMainChannelClass SpiceMainChannelClass;
+typedef struct _SpiceMainChannelPrivate SpiceMainChannelPrivate;
+
+/**
+ * SpiceMainChannel:
+ *
+ * The #SpiceMainChannel struct is opaque and should not be accessed directly.
+ */
+struct _SpiceMainChannel {
+ SpiceChannel parent;
+
+ /*< private >*/
+ SpiceMainChannelPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+/**
+ * SpiceMainChannelClass:
+ * @parent_class: Parent class.
+ * @mouse_update: Signal class handler for the #SpiceMainChannel::mouse-update signal.
+ * @agent_update: Signal class handler for the #SpiceMainChannel::agent-update signal.
+ *
+ * Class structure for #SpiceMainChannel.
+ */
+struct _SpiceMainChannelClass {
+ SpiceChannelClass parent_class;
+
+ /* signals */
+ void (*mouse_update)(SpiceChannel *channel);
+ void (*agent_update)(SpiceChannel *channel);
+
+ /*< private >*/
+ /* Do not add fields to this struct */
+};
+
+GType spice_main_channel_get_type(void);
+
+void spice_main_set_display(SpiceMainChannel *channel, int id,
+ int x, int y, int width, int height);
+void spice_main_update_display(SpiceMainChannel *channel, int id,
+ int x, int y, int width, int height, gboolean update);
+void spice_main_set_display_enabled(SpiceMainChannel *channel, int id, gboolean enabled);
+gboolean spice_main_send_monitor_config(SpiceMainChannel *channel);
+
+void spice_main_clipboard_selection_grab(SpiceMainChannel *channel, guint selection, guint32 *types, int ntypes);
+void spice_main_clipboard_selection_release(SpiceMainChannel *channel, guint selection);
+void spice_main_clipboard_selection_notify(SpiceMainChannel *channel, guint selection, guint32 type, const guchar *data, size_t size);
+void spice_main_clipboard_selection_request(SpiceMainChannel *channel, guint selection, guint32 type);
+
+gboolean spice_main_agent_test_capability(SpiceMainChannel *channel, guint32 cap);
+void spice_main_file_copy_async(SpiceMainChannel *channel,
+ GFile **sources,
+ GFileCopyFlags flags,
+ GCancellable *cancellable,
+ GFileProgressCallback progress_callback,
+ gpointer progress_callback_data,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+gboolean spice_main_file_copy_finish(SpiceMainChannel *channel,
+ GAsyncResult *result,
+ GError **error);
+
+#ifndef SPICE_DISABLE_DEPRECATED
+SPICE_DEPRECATED_FOR(spice_main_clipboard_selection_grab)
+void spice_main_clipboard_grab(SpiceMainChannel *channel, guint32 *types, int ntypes);
+SPICE_DEPRECATED_FOR(spice_main_clipboard_selection_release)
+void spice_main_clipboard_release(SpiceMainChannel *channel);
+SPICE_DEPRECATED_FOR(spice_main_clipboard_selection_notify)
+void spice_main_clipboard_notify(SpiceMainChannel *channel, guint32 type, const guchar *data, size_t size);
+SPICE_DEPRECATED_FOR(spice_main_clipboard_selection_request)
+void spice_main_clipboard_request(SpiceMainChannel *channel, guint32 type);
+#endif
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_MAIN_CHANNEL_H__ */
diff --git a/src/channel-playback-priv.h b/src/channel-playback-priv.h
new file mode 100644
index 0000000..aa33d2c
--- /dev/null
+++ b/src/channel-playback-priv.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2013 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_CLIENT_PLAYBACK_CHANNEL_PRIV_H__
+#define __SPICE_CLIENT_PLAYBACK_CHANNEL_PRIV_H__
+
+gboolean spice_playback_channel_is_active(SpicePlaybackChannel *channel);
+guint32 spice_playback_channel_get_latency(SpicePlaybackChannel *channel);
+void spice_playback_channel_sync_latency(SpicePlaybackChannel *channel);
+#endif
diff --git a/src/channel-playback.c b/src/channel-playback.c
new file mode 100644
index 0000000..d8a181e
--- /dev/null
+++ b/src/channel-playback.c
@@ -0,0 +1,496 @@
+/* -*- 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-channel-priv.h"
+#include "spice-session-priv.h"
+
+#include "spice-marshal.h"
+
+#include "common/snd_codec.h"
+
+/**
+ * SECTION:channel-playback
+ * @short_description: audio stream for playback
+ * @title: Playback Channel
+ * @section_id:
+ * @see_also: #SpiceChannel, and #SpiceAudio
+ * @stability: Stable
+ * @include: channel-playback.h
+ *
+ * #SpicePlaybackChannel class handles an audio playback stream. The
+ * audio data is received via #SpicePlaybackChannel::playback-data
+ * signal, and is controlled by the guest with
+ * #SpicePlaybackChannel::playback-stop and
+ * #SpicePlaybackChannel::playback-start signal events.
+ *
+ * Note: You may be interested to let the #SpiceAudio class play and
+ * record audio channels for your application.
+ */
+
+#define SPICE_PLAYBACK_CHANNEL_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_PLAYBACK_CHANNEL, SpicePlaybackChannelPrivate))
+
+struct _SpicePlaybackChannelPrivate {
+ int mode;
+ SndCodec codec;
+ guint32 frame_count;
+ guint32 last_time;
+ guint8 nchannels;
+ guint16 *volume;
+ guint8 mute;
+ gboolean is_active;
+ guint32 latency;
+ guint32 min_latency;
+};
+
+G_DEFINE_TYPE(SpicePlaybackChannel, spice_playback_channel, SPICE_TYPE_CHANNEL)
+
+/* Properties */
+enum {
+ PROP_0,
+ PROP_NCHANNELS,
+ PROP_VOLUME,
+ PROP_MUTE,
+ PROP_MIN_LATENCY,
+};
+
+/* Signals */
+enum {
+ SPICE_PLAYBACK_START,
+ SPICE_PLAYBACK_DATA,
+ SPICE_PLAYBACK_STOP,
+ SPICE_PLAYBACK_GET_DELAY,
+
+ SPICE_PLAYBACK_LAST_SIGNAL,
+};
+
+static guint signals[SPICE_PLAYBACK_LAST_SIGNAL];
+static void channel_set_handlers(SpiceChannelClass *klass);
+
+/* ------------------------------------------------------------------ */
+
+#define SPICE_PLAYBACK_DEFAULT_LATENCY_MS 200
+
+static void spice_playback_channel_reset_capabilities(SpiceChannel *channel)
+{
+ if (!g_getenv("SPICE_DISABLE_CELT"))
+ if (snd_codec_is_capable(SPICE_AUDIO_DATA_MODE_CELT_0_5_1, SND_CODEC_ANY_FREQUENCY))
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_PLAYBACK_CAP_CELT_0_5_1);
+ if (!g_getenv("SPICE_DISABLE_OPUS"))
+ if (snd_codec_is_capable(SPICE_AUDIO_DATA_MODE_OPUS, SND_CODEC_ANY_FREQUENCY))
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_PLAYBACK_CAP_OPUS);
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_PLAYBACK_CAP_VOLUME);
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_PLAYBACK_CAP_LATENCY);
+}
+
+static void spice_playback_channel_init(SpicePlaybackChannel *channel)
+{
+ channel->priv = SPICE_PLAYBACK_CHANNEL_GET_PRIVATE(channel);
+
+ spice_playback_channel_reset_capabilities(SPICE_CHANNEL(channel));
+}
+
+static void spice_playback_channel_finalize(GObject *obj)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(obj)->priv;
+
+ snd_codec_destroy(&c->codec);
+
+ g_free(c->volume);
+ c->volume = NULL;
+
+ if (G_OBJECT_CLASS(spice_playback_channel_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_playback_channel_parent_class)->finalize(obj);
+}
+
+static void spice_playback_channel_get_property(GObject *gobject,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SpicePlaybackChannel *channel = SPICE_PLAYBACK_CHANNEL(gobject);
+ SpicePlaybackChannelPrivate *c = channel->priv;
+
+ switch (prop_id) {
+ case PROP_VOLUME:
+ g_value_set_pointer(value, c->volume);
+ break;
+ case PROP_NCHANNELS:
+ g_value_set_uint(value, c->nchannels);
+ break;
+ case PROP_MUTE:
+ g_value_set_boolean(value, c->mute);
+ break;
+ case PROP_MIN_LATENCY:
+ g_value_set_uint(value, c->min_latency);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_playback_channel_set_property(GObject *gobject,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (prop_id) {
+ case PROP_VOLUME:
+ /* TODO: request guest volume change */
+ break;
+ case PROP_MUTE:
+ /* TODO: request guest mute change */
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+/* main or coroutine context */
+static void spice_playback_channel_reset(SpiceChannel *channel, gboolean migrating)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(channel)->priv;
+
+ snd_codec_destroy(&c->codec);
+ g_coroutine_signal_emit(channel, signals[SPICE_PLAYBACK_STOP], 0);
+ c->is_active = FALSE;
+
+ SPICE_CHANNEL_CLASS(spice_playback_channel_parent_class)->channel_reset(channel, migrating);
+}
+
+static void spice_playback_channel_class_init(SpicePlaybackChannelClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS(klass);
+
+ gobject_class->finalize = spice_playback_channel_finalize;
+ gobject_class->get_property = spice_playback_channel_get_property;
+ gobject_class->set_property = spice_playback_channel_set_property;
+
+ channel_class->channel_reset = spice_playback_channel_reset;
+ channel_class->channel_reset_capabilities = spice_playback_channel_reset_capabilities;
+
+ g_object_class_install_property
+ (gobject_class, PROP_NCHANNELS,
+ g_param_spec_uint("nchannels",
+ "Number of Channels",
+ "Number of Channels",
+ 0, G_MAXUINT8, 2,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_VOLUME,
+ g_param_spec_pointer("volume",
+ "Playback volume",
+ "Playback volume",
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_MUTE,
+ g_param_spec_boolean("mute",
+ "Mute",
+ "Mute",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+ g_object_class_install_property
+ (gobject_class, PROP_MIN_LATENCY,
+ g_param_spec_uint("min-latency",
+ "Playback min buffer size (ms)",
+ "Playback min buffer size (ms)",
+ 0, G_MAXUINT32, SPICE_PLAYBACK_DEFAULT_LATENCY_MS,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+ /**
+ * SpicePlaybackChannel::playback-start:
+ * @channel: the #SpicePlaybackChannel that emitted the signal
+ * @format: a #SPICE_AUDIO_FMT
+ * @channels: number of channels
+ * @rate: audio rate
+ * @latency: minimum playback latency in ms
+ *
+ * Notify when the playback should start, and provide audio format
+ * characteristics.
+ **/
+ signals[SPICE_PLAYBACK_START] =
+ g_signal_new("playback-start",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpicePlaybackChannelClass, playback_start),
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__INT_INT_INT,
+ G_TYPE_NONE,
+ 3,
+ G_TYPE_INT, G_TYPE_INT, G_TYPE_INT);
+
+ /**
+ * SpicePlaybackChannel::playback-data:
+ * @channel: the #SpicePlaybackChannel that emitted the signal
+ * @data: pointer to audio data
+ * @data_size: size in byte of @data
+ *
+ * Provide audio data to be played.
+ **/
+ signals[SPICE_PLAYBACK_DATA] =
+ g_signal_new("playback-data",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpicePlaybackChannelClass, playback_data),
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__POINTER_INT,
+ G_TYPE_NONE,
+ 2,
+ G_TYPE_POINTER, G_TYPE_INT);
+
+ /**
+ * SpicePlaybackChannel::playback-stop:
+ * @channel: the #SpicePlaybackChannel that emitted the signal
+ *
+ * Notify when the playback should stop.
+ **/
+ signals[SPICE_PLAYBACK_STOP] =
+ g_signal_new("playback-stop",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpicePlaybackChannelClass, playback_stop),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
+
+ /**
+ * SpicePlaybackChannel::playback-get-delay:
+ * @channel: the #SpicePlaybackChannel that emitted the signal
+ *
+ * Notify when the current playback delay is requested
+ **/
+ signals[SPICE_PLAYBACK_GET_DELAY] =
+ g_signal_new("playback-get-delay",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
+
+ g_type_class_add_private(klass, sizeof(SpicePlaybackChannelPrivate));
+ channel_set_handlers(SPICE_CHANNEL_CLASS(klass));
+}
+
+/* ------------------------------------------------------------------ */
+
+/* coroutine context */
+static void playback_handle_data(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(channel)->priv;
+ SpiceMsgPlaybackPacket *packet = spice_msg_in_parsed(in);
+
+#ifdef DEBUG
+ CHANNEL_DEBUG(channel, "%s: time %d data %p size %d", __FUNCTION__,
+ packet->time, packet->data, packet->data_size);
+#endif
+
+ if (c->last_time > packet->time)
+ g_warn_if_reached();
+
+ c->last_time = packet->time;
+
+ uint8_t *data = packet->data;
+ int n = packet->data_size;
+ uint8_t pcm[SND_CODEC_MAX_FRAME_SIZE * 2 * 2];
+
+ if (c->mode != SPICE_AUDIO_DATA_MODE_RAW) {
+ n = sizeof(pcm);
+ data = pcm;
+
+ if (snd_codec_decode(c->codec, packet->data, packet->data_size,
+ pcm, &n) != SND_CODEC_OK) {
+ g_warning("snd_codec_decode() error");
+ return;
+ }
+ }
+
+ g_coroutine_signal_emit(channel, signals[SPICE_PLAYBACK_DATA], 0, data, n);
+
+ if ((c->frame_count++ % 100) == 0) {
+ g_coroutine_signal_emit(channel, signals[SPICE_PLAYBACK_GET_DELAY], 0);
+ }
+}
+
+/* coroutine context */
+static void playback_handle_mode(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(channel)->priv;
+ SpiceMsgPlaybackMode *mode = spice_msg_in_parsed(in);
+
+ CHANNEL_DEBUG(channel, "%s: time %d mode %d data %p size %d", __FUNCTION__,
+ mode->time, mode->mode, mode->data, mode->data_size);
+
+ c->mode = mode->mode;
+ switch (c->mode) {
+ case SPICE_AUDIO_DATA_MODE_RAW:
+ case SPICE_AUDIO_DATA_MODE_CELT_0_5_1:
+ case SPICE_AUDIO_DATA_MODE_OPUS:
+ break;
+ default:
+ g_warning("%s: unhandled mode", __FUNCTION__);
+ break;
+ }
+}
+
+/* coroutine context */
+static void playback_handle_start(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(channel)->priv;
+ SpiceMsgPlaybackStart *start = spice_msg_in_parsed(in);
+
+ CHANNEL_DEBUG(channel, "%s: fmt %d channels %d freq %d time %d", __FUNCTION__,
+ start->format, start->channels, start->frequency, start->time);
+
+ c->frame_count = 0;
+ c->last_time = start->time;
+ c->is_active = TRUE;
+ c->min_latency = SPICE_PLAYBACK_DEFAULT_LATENCY_MS;
+ snd_codec_destroy(&c->codec);
+
+ if (c->mode != SPICE_AUDIO_DATA_MODE_RAW) {
+ if (snd_codec_create(&c->codec, c->mode, start->frequency, SND_CODEC_DECODE) != SND_CODEC_OK) {
+ g_warning("create decoder failed");
+ return;
+ }
+ }
+ g_coroutine_signal_emit(channel, signals[SPICE_PLAYBACK_START], 0,
+ start->format, start->channels, start->frequency);
+}
+
+/* coroutine context */
+static void playback_handle_stop(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(channel)->priv;
+
+ g_coroutine_signal_emit(channel, signals[SPICE_PLAYBACK_STOP], 0);
+ c->is_active = FALSE;
+}
+
+/* coroutine context */
+static void playback_handle_set_volume(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(channel)->priv;
+ SpiceMsgAudioVolume *vol = spice_msg_in_parsed(in);
+
+ if (vol->nchannels == 0) {
+ g_warning("spice-server send audio-volume-msg with 0 channels");
+ return;
+ }
+
+ g_free(c->volume);
+ c->nchannels = vol->nchannels;
+ c->volume = g_new(guint16, c->nchannels);
+ memcpy(c->volume, vol->volume, sizeof(guint16) * c->nchannels);
+ g_coroutine_object_notify(G_OBJECT(channel), "volume");
+}
+
+/* coroutine context */
+static void playback_handle_set_mute(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(channel)->priv;
+ SpiceMsgAudioMute *m = spice_msg_in_parsed(in);
+
+ c->mute = m->mute;
+ g_coroutine_object_notify(G_OBJECT(channel), "mute");
+}
+
+/* coroutine context */
+static void playback_handle_set_latency(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePlaybackChannelPrivate *c = SPICE_PLAYBACK_CHANNEL(channel)->priv;
+ SpiceMsgPlaybackLatency *msg = spice_msg_in_parsed(in);
+
+ c->min_latency = msg->latency_ms;
+ SPICE_DEBUG("%s: notify latency update %u", __FUNCTION__, c->min_latency);
+ g_coroutine_object_notify(G_OBJECT(channel), "min-latency");
+}
+
+static void channel_set_handlers(SpiceChannelClass *klass)
+{
+ static const spice_msg_handler handlers[] = {
+ [ SPICE_MSG_PLAYBACK_DATA ] = playback_handle_data,
+ [ SPICE_MSG_PLAYBACK_MODE ] = playback_handle_mode,
+ [ SPICE_MSG_PLAYBACK_START ] = playback_handle_start,
+ [ SPICE_MSG_PLAYBACK_STOP ] = playback_handle_stop,
+ [ SPICE_MSG_PLAYBACK_VOLUME ] = playback_handle_set_volume,
+ [ SPICE_MSG_PLAYBACK_MUTE ] = playback_handle_set_mute,
+ [ SPICE_MSG_PLAYBACK_LATENCY ] = playback_handle_set_latency,
+ };
+
+ spice_channel_set_handlers(klass, handlers, G_N_ELEMENTS(handlers));
+}
+
+void spice_playback_channel_set_delay(SpicePlaybackChannel *channel, guint32 delay_ms)
+{
+ SpicePlaybackChannelPrivate *c;
+ SpiceSession *session;
+
+ g_return_if_fail(SPICE_IS_PLAYBACK_CHANNEL(channel));
+
+ CHANNEL_DEBUG(channel, "playback set_delay %u ms", delay_ms);
+
+ c = channel->priv;
+ c->latency = delay_ms;
+
+ session = spice_channel_get_session(SPICE_CHANNEL(channel));
+ if (session) {
+ spice_session_set_mm_time(session, c->last_time - delay_ms);
+ } else {
+ CHANNEL_DEBUG(channel, "channel detached from session, mm time skipped");
+ }
+}
+
+G_GNUC_INTERNAL
+gboolean spice_playback_channel_is_active(SpicePlaybackChannel *channel)
+{
+ g_return_val_if_fail(SPICE_IS_PLAYBACK_CHANNEL(channel), FALSE);
+ return channel->priv->is_active;
+}
+
+G_GNUC_INTERNAL
+guint32 spice_playback_channel_get_latency(SpicePlaybackChannel *channel)
+{
+ g_return_val_if_fail(SPICE_IS_PLAYBACK_CHANNEL(channel), 0);
+ if (!channel->priv->is_active) {
+ return 0;
+ }
+ return channel->priv->latency;
+}
+
+G_GNUC_INTERNAL
+void spice_playback_channel_sync_latency(SpicePlaybackChannel *channel)
+{
+ g_return_if_fail(SPICE_IS_PLAYBACK_CHANNEL(channel));
+ g_return_if_fail(channel->priv->is_active);
+ SPICE_DEBUG("%s: notify latency update %u", __FUNCTION__, channel->priv->min_latency);
+ g_coroutine_object_notify(G_OBJECT(SPICE_CHANNEL(channel)), "min-latency");
+}
diff --git a/src/channel-playback.h b/src/channel-playback.h
new file mode 100644
index 0000000..9cf68cf
--- /dev/null
+++ b/src/channel-playback.h
@@ -0,0 +1,76 @@
+/* -*- 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_CLIENT_PLAYBACK_CHANNEL_H__
+#define __SPICE_CLIENT_PLAYBACK_CHANNEL_H__
+
+#include "spice-client.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_PLAYBACK_CHANNEL (spice_playback_channel_get_type())
+#define SPICE_PLAYBACK_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_PLAYBACK_CHANNEL, SpicePlaybackChannel))
+#define SPICE_PLAYBACK_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_PLAYBACK_CHANNEL, SpicePlaybackChannelClass))
+#define SPICE_IS_PLAYBACK_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_PLAYBACK_CHANNEL))
+#define SPICE_IS_PLAYBACK_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_PLAYBACK_CHANNEL))
+#define SPICE_PLAYBACK_CHANNEL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_PLAYBACK_CHANNEL, SpicePlaybackChannelClass))
+
+typedef struct _SpicePlaybackChannel SpicePlaybackChannel;
+typedef struct _SpicePlaybackChannelClass SpicePlaybackChannelClass;
+typedef struct _SpicePlaybackChannelPrivate SpicePlaybackChannelPrivate;
+
+/**
+ * SpicePlaybackChannel:
+ *
+ * The #SpicePlaybackChannel struct is opaque and should not be accessed directly.
+ */
+struct _SpicePlaybackChannel {
+ SpiceChannel parent;
+
+ /*< private >*/
+ SpicePlaybackChannelPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+/**
+ * SpicePlaybackChannelClass:
+ * @parent_class: Parent class.
+ * @playback_start: Signal class handler for the #SpicePlaybackChannel::playback-start signal.
+ * @playback_data: Signal class handler for the #SpicePlaybackChannel::playback-data signal.
+ * @playback_stop: Signal class handler for the #SpicePlaybackChannel::playback-stop signal.
+ *
+ * Class structure for #SpicePlaybackChannel.
+ */
+struct _SpicePlaybackChannelClass {
+ SpiceChannelClass parent_class;
+
+ /* signals */
+ void (*playback_start)(SpicePlaybackChannel *channel,
+ gint format, gint channels, gint freq);
+ void (*playback_data)(SpicePlaybackChannel *channel, gpointer *data, gint size);
+ void (*playback_stop)(SpicePlaybackChannel *channel);
+
+ /*< private >*/
+ /* Do not add fields to this struct */
+};
+
+GType spice_playback_channel_get_type(void);
+void spice_playback_channel_set_delay(SpicePlaybackChannel *channel, guint32 delay_ms);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_PLAYBACK_CHANNEL_H__ */
diff --git a/src/channel-port.c b/src/channel-port.c
new file mode 100644
index 0000000..f0b6d1e
--- /dev/null
+++ b/src/channel-port.c
@@ -0,0 +1,361 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 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-channel-priv.h"
+#include "spice-marshal.h"
+#include "glib-compat.h"
+
+/**
+ * SECTION:channel-port
+ * @short_description: private communication channel
+ * @title: Port Channel
+ * @section_id:
+ * @see_also: #SpiceChannel
+ * @stability: Stable
+ * @include: channel-port.h
+ *
+ * A Spice port channel carry arbitrary data between the Spice client
+ * and the Spice server. It may be used to provide additional
+ * services on top of a Spice connection. For example, a channel can
+ * be associated with the qemu monitor for the client to interact
+ * with it, just like any qemu chardev. Or it may be used with
+ * various protocols, such as the Spice Controller.
+ *
+ * A port kind is identified simply by a fqdn, such as
+ * org.qemu.monitor, org.spice.spicy.test or org.ovirt.controller...
+ *
+ * Once connected and initialized, the client may read the name of the
+ * port via SpicePortChannel:port-name.
+
+ * When the other end of the port is ready,
+ * SpicePortChannel:port-opened is set to %TRUE and you can start
+ * receiving data via the signal SpicePortChannel::port-data, or
+ * sending data via spice_port_write_async().
+ *
+ * Since: 0.15
+ */
+
+#define SPICE_PORT_CHANNEL_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_PORT_CHANNEL, SpicePortChannelPrivate))
+
+struct _SpicePortChannelPrivate {
+ gchar *name;
+ gboolean opened;
+};
+
+G_DEFINE_TYPE(SpicePortChannel, spice_port_channel, SPICE_TYPE_CHANNEL)
+
+/* Properties */
+enum {
+ PROP_0,
+ PROP_PORT_NAME,
+ PROP_PORT_OPENED,
+};
+
+/* Signals */
+enum {
+ SPICE_PORT_DATA,
+ SPICE_PORT_EVENT,
+ LAST_SIGNAL,
+};
+
+static guint signals[LAST_SIGNAL];
+static void channel_set_handlers(SpiceChannelClass *klass);
+
+static void spice_port_channel_init(SpicePortChannel *channel)
+{
+ channel->priv = SPICE_PORT_CHANNEL_GET_PRIVATE(channel);
+}
+
+static void spice_port_get_property(GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SpicePortChannelPrivate *c = SPICE_PORT_CHANNEL(object)->priv;
+
+ switch (prop_id) {
+ case PROP_PORT_NAME:
+ g_value_set_string(value, c->name);
+ break;
+ case PROP_PORT_OPENED:
+ g_value_set_boolean(value, c->opened);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_port_channel_finalize(GObject *object)
+{
+ SpicePortChannelPrivate *c = SPICE_PORT_CHANNEL(object)->priv;
+
+ g_free(c->name);
+
+ if (G_OBJECT_CLASS(spice_port_channel_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_port_channel_parent_class)->finalize(object);
+}
+
+static void spice_port_channel_reset(SpiceChannel *channel, gboolean migrating)
+{
+ SpicePortChannelPrivate *c = SPICE_PORT_CHANNEL(channel)->priv;
+
+ g_clear_pointer(&c->name, g_free);
+ c->opened = FALSE;
+
+ SPICE_CHANNEL_CLASS(spice_port_channel_parent_class)->channel_reset(channel, migrating);
+}
+
+static void spice_port_channel_class_init(SpicePortChannelClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS(klass);
+
+ gobject_class->finalize = spice_port_channel_finalize;
+ gobject_class->get_property = spice_port_get_property;
+ channel_class->channel_reset = spice_port_channel_reset;
+
+ g_object_class_install_property
+ (gobject_class, PROP_PORT_NAME,
+ g_param_spec_string("port-name",
+ "Port name",
+ "Port name",
+ NULL,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_PORT_OPENED,
+ g_param_spec_boolean("port-opened",
+ "Port opened",
+ "Port opened",
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpicePort::port-data:
+ * @channel: the channel that emitted the signal
+ * @data: the data received
+ * @size: number of bytes read
+ *
+ * The #SpicePortChannel::port-data signal is emitted when new
+ * port data is received.
+ * Since: 0.15
+ **/
+ signals[SPICE_PORT_DATA] =
+ g_signal_new("port-data",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__POINTER_INT,
+ G_TYPE_NONE,
+ 2,
+ G_TYPE_POINTER, G_TYPE_INT);
+
+
+ /**
+ * SpicePort::port-event:
+ * @channel: the channel that emitted the signal
+ * @event: the event received
+ * @size: number of bytes read
+ *
+ * The #SpicePortChannel::port-event signal is emitted when new
+ * port event is received.
+ * Since: 0.15
+ **/
+ signals[SPICE_PORT_EVENT] =
+ g_signal_new("port-event",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__INT,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_INT);
+
+ g_type_class_add_private(klass, sizeof(SpicePortChannelPrivate));
+ channel_set_handlers(SPICE_CHANNEL_CLASS(klass));
+}
+
+
+/* coroutine context */
+static void port_set_opened(SpicePortChannel *self, gboolean opened)
+{
+ SpicePortChannelPrivate *c = self->priv;
+
+ if (c->opened == opened)
+ return;
+
+ c->opened = opened;
+ g_coroutine_object_notify(G_OBJECT(self), "port-opened");
+}
+
+/* coroutine context */
+static void port_handle_init(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePortChannel *self = SPICE_PORT_CHANNEL(channel);
+ SpicePortChannelPrivate *c = self->priv;
+ SpiceMsgPortInit *init = spice_msg_in_parsed(in);
+
+ CHANNEL_DEBUG(channel, "init: %s %d", init->name, init->opened);
+ g_return_if_fail(init->name != NULL && *init->name != '\0');
+ g_return_if_fail(c->name == NULL);
+
+ c->name = g_strdup((gchar*)init->name);
+
+ port_set_opened(self, init->opened);
+ if (init->opened)
+ g_coroutine_signal_emit(channel, signals[SPICE_PORT_EVENT], 0, SPICE_PORT_EVENT_OPENED);
+
+ g_coroutine_object_notify(G_OBJECT(channel), "port-name");
+}
+
+/* coroutine context */
+static void port_handle_event(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePortChannel *self = SPICE_PORT_CHANNEL(channel);
+ SpiceMsgPortEvent *event = spice_msg_in_parsed(in);
+
+ CHANNEL_DEBUG(channel, "port event: %d", event->event);
+ switch (event->event) {
+ case SPICE_PORT_EVENT_OPENED:
+ port_set_opened(self, true);
+ break;
+ case SPICE_PORT_EVENT_CLOSED:
+ port_set_opened(self, false);
+ break;
+ }
+
+ g_coroutine_signal_emit(channel, signals[SPICE_PORT_EVENT], 0, event->event);
+}
+
+/* coroutine context */
+static void port_handle_msg(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePortChannel *self = SPICE_PORT_CHANNEL(channel);
+ int size;
+ uint8_t *buf;
+
+ buf = spice_msg_in_raw(in, &size);
+ CHANNEL_DEBUG(channel, "port %p got %d %p", channel, size, buf);
+ port_set_opened(self, true);
+ g_coroutine_signal_emit(channel, signals[SPICE_PORT_DATA], 0, buf, size);
+}
+
+/**
+ * spice_port_write_async:
+ * @port: A #SpicePortChannel
+ * @buffer: (array length=count) (element-type guint8): the buffer
+ * containing the data to write
+ * @count: the number of bytes to write
+ * @cancellable: (allow-none): optional GCancellable object, NULL to ignore
+ * @callback: (scope async): callback to call when the request is satisfied
+ * @user_data: (closure): the data to pass to callback function
+ *
+ * Request an asynchronous write of count bytes from @buffer into the
+ * @port. When the operation is finished @callback will be called. You
+ * can then call spice_port_write_finish() to get the result of
+ * the operation.
+ *
+ * Since: 0.15
+ **/
+void spice_port_write_async(SpicePortChannel *self,
+ const void *buffer, gsize count,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SpicePortChannelPrivate *c;
+
+ g_return_if_fail(SPICE_IS_PORT_CHANNEL(self));
+ g_return_if_fail(buffer != NULL);
+ c = self->priv;
+
+ if (!c->opened) {
+ g_simple_async_report_error_in_idle(G_OBJECT(self), callback, user_data,
+ SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "The port is not opened");
+ return;
+ }
+
+ spice_vmc_write_async(SPICE_CHANNEL(self), buffer, count,
+ cancellable, callback, user_data);
+}
+
+/**
+ * spice_port_write_finish:
+ * @port: a #SpicePortChannel
+ * @result: a #GAsyncResult
+ * @error: a #GError location to store the error occurring, or %NULL
+ * to ignore
+ *
+ * Finishes a port write operation.
+ *
+ * Returns: a #gssize containing the number of bytes written to the stream.
+ * Since: 0.15
+ **/
+gssize spice_port_write_finish(SpicePortChannel *self,
+ GAsyncResult *result, GError **error)
+{
+ g_return_val_if_fail(SPICE_IS_PORT_CHANNEL(self), -1);
+
+ return spice_vmc_write_finish(SPICE_CHANNEL(self), result, error);
+}
+
+/**
+ * spice_port_event:
+ * @port: a #SpicePortChannel
+ * @event: a SPICE_PORT_EVENT value
+ *
+ * Send an event to the port.
+ *
+ * Note: The values SPICE_PORT_EVENT_CLOSED and
+ * SPICE_PORT_EVENT_OPENED are managed by the channel connection
+ * state.
+ *
+ * Since: 0.15
+ **/
+void spice_port_event(SpicePortChannel *self, guint8 event)
+{
+ SpiceMsgcPortEvent e;
+ SpiceMsgOut *msg;
+
+ g_return_if_fail(SPICE_IS_PORT_CHANNEL(self));
+ g_return_if_fail(event > SPICE_PORT_EVENT_CLOSED);
+
+ msg = spice_msg_out_new(SPICE_CHANNEL(self), SPICE_MSGC_PORT_EVENT);
+ e.event = event;
+ msg->marshallers->msgc_port_event(msg->marshaller, &e);
+ spice_msg_out_send(msg);
+}
+
+static void channel_set_handlers(SpiceChannelClass *klass)
+{
+ static const spice_msg_handler handlers[] = {
+ [ SPICE_MSG_PORT_INIT ] = port_handle_init,
+ [ SPICE_MSG_PORT_EVENT ] = port_handle_event,
+ [ SPICE_MSG_SPICEVMC_DATA ] = port_handle_msg,
+ };
+
+ spice_channel_set_handlers(klass, handlers, G_N_ELEMENTS(handlers));
+}
diff --git a/src/channel-port.h b/src/channel-port.h
new file mode 100644
index 0000000..08c15dc
--- /dev/null
+++ b/src/channel-port.h
@@ -0,0 +1,76 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 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_CLIENT_PORT_CHANNEL_H__
+#define __SPICE_CLIENT_PORT_CHANNEL_H__
+
+#include <gio/gio.h>
+#include "spice-channel.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_PORT_CHANNEL (spice_port_channel_get_type())
+#define SPICE_PORT_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_PORT_CHANNEL, SpicePortChannel))
+#define SPICE_PORT_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_PORT_CHANNEL, SpicePortChannelClass))
+#define SPICE_IS_PORT_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_PORT_CHANNEL))
+#define SPICE_IS_PORT_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_PORT_CHANNEL))
+#define SPICE_PORT_CHANNEL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_PORT_CHANNEL, SpicePortChannelClass))
+
+typedef struct _SpicePortChannel SpicePortChannel;
+typedef struct _SpicePortChannelClass SpicePortChannelClass;
+typedef struct _SpicePortChannelPrivate SpicePortChannelPrivate;
+
+/**
+ * SpicePortChannel:
+ *
+ * The #SpicePortChannel struct is opaque and should not be accessed directly.
+ */
+struct _SpicePortChannel {
+ SpiceChannel parent;
+
+ /*< private >*/
+ SpicePortChannelPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+/**
+ * SpicePortChannelClass:
+ * @parent_class: Parent class.
+ *
+ * Class structure for #SpicePortChannel.
+ */
+struct _SpicePortChannelClass {
+ SpiceChannelClass parent_class;
+
+ /*< private >*/
+ /* Do not add fields to this struct */
+};
+
+GType spice_port_channel_get_type(void);
+
+void spice_port_write_async(SpicePortChannel *port,
+ const void *buffer, gsize count,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gssize spice_port_write_finish(SpicePortChannel *port,
+ GAsyncResult *result, GError **error);
+void spice_port_event(SpicePortChannel *port, guint8 event);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_PORT_CHANNEL_H__ */
diff --git a/src/channel-record.c b/src/channel-record.c
new file mode 100644
index 0000000..d07d84e
--- /dev/null
+++ b/src/channel-record.c
@@ -0,0 +1,482 @@
+/* -*- 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-channel-priv.h"
+
+#include "spice-marshal.h"
+#include "spice-session-priv.h"
+
+#include "common/snd_codec.h"
+
+/**
+ * SECTION:channel-record
+ * @short_description: audio stream for recording
+ * @title: Record Channel
+ * @section_id:
+ * @see_also: #SpiceChannel, and #SpiceAudio
+ * @stability: Stable
+ * @include: channel-record.h
+ *
+ * #SpiceRecordChannel class handles an audio recording stream. The
+ * audio stream should start when #SpiceRecordChannel::record-start is
+ * emitted and should be stopped when #SpiceRecordChannel::record-stop
+ * is received.
+ *
+ * The audio is sent to the guest by calling spice_record_send_data()
+ * with the recorded PCM data.
+ *
+ * Note: You may be interested to let the #SpiceAudio class play and
+ * record audio channels for your application.
+ */
+
+#define SPICE_RECORD_CHANNEL_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_RECORD_CHANNEL, SpiceRecordChannelPrivate))
+
+struct _SpiceRecordChannelPrivate {
+ int mode;
+ gboolean started;
+ SndCodec codec;
+ gsize frame_bytes;
+ guint8 *last_frame;
+ gsize last_frame_current;
+ guint8 nchannels;
+ guint16 *volume;
+ guint8 mute;
+};
+
+G_DEFINE_TYPE(SpiceRecordChannel, spice_record_channel, SPICE_TYPE_CHANNEL)
+
+/* Properties */
+enum {
+ PROP_0,
+ PROP_NCHANNELS,
+ PROP_VOLUME,
+ PROP_MUTE,
+};
+
+/* Signals */
+enum {
+ SPICE_RECORD_START,
+ SPICE_RECORD_STOP,
+
+ SPICE_RECORD_LAST_SIGNAL,
+};
+
+static guint signals[SPICE_RECORD_LAST_SIGNAL];
+
+static void channel_set_handlers(SpiceChannelClass *klass);
+
+/* ------------------------------------------------------------------ */
+
+static void spice_record_channel_reset_capabilities(SpiceChannel *channel)
+{
+ if (!g_getenv("SPICE_DISABLE_CELT"))
+ if (snd_codec_is_capable(SPICE_AUDIO_DATA_MODE_CELT_0_5_1, SND_CODEC_ANY_FREQUENCY))
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_RECORD_CAP_CELT_0_5_1);
+ if (!g_getenv("SPICE_DISABLE_OPUS"))
+ if (snd_codec_is_capable(SPICE_AUDIO_DATA_MODE_OPUS, SND_CODEC_ANY_FREQUENCY))
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_RECORD_CAP_OPUS);
+ spice_channel_set_capability(SPICE_CHANNEL(channel), SPICE_RECORD_CAP_VOLUME);
+}
+
+static void spice_record_channel_init(SpiceRecordChannel *channel)
+{
+ channel->priv = SPICE_RECORD_CHANNEL_GET_PRIVATE(channel);
+
+ spice_record_channel_reset_capabilities(SPICE_CHANNEL(channel));
+}
+
+static void spice_record_channel_finalize(GObject *obj)
+{
+ SpiceRecordChannelPrivate *c = SPICE_RECORD_CHANNEL(obj)->priv;
+
+ g_free(c->last_frame);
+ c->last_frame = NULL;
+
+ snd_codec_destroy(&c->codec);
+
+ g_free(c->volume);
+ c->volume = NULL;
+
+ if (G_OBJECT_CLASS(spice_record_channel_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_record_channel_parent_class)->finalize(obj);
+}
+
+static void spice_record_channel_get_property(GObject *gobject,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceRecordChannel *channel = SPICE_RECORD_CHANNEL(gobject);
+ SpiceRecordChannelPrivate *c = channel->priv;
+
+ switch (prop_id) {
+ case PROP_VOLUME:
+ g_value_set_pointer(value, c->volume);
+ break;
+ case PROP_NCHANNELS:
+ g_value_set_uint(value, c->nchannels);
+ break;
+ case PROP_MUTE:
+ g_value_set_boolean(value, c->mute);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_record_channel_set_property(GObject *gobject,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (prop_id) {
+ case PROP_VOLUME:
+ /* TODO: request guest volume change */
+ break;
+ case PROP_MUTE:
+ /* TODO: request guest mute change */
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void channel_reset(SpiceChannel *channel, gboolean migrating)
+{
+ SpiceRecordChannelPrivate *c = SPICE_RECORD_CHANNEL(channel)->priv;
+
+ g_free(c->last_frame);
+ c->last_frame = NULL;
+
+ g_coroutine_signal_emit(channel, signals[SPICE_RECORD_STOP], 0);
+ c->started = FALSE;
+
+ snd_codec_destroy(&c->codec);
+
+ SPICE_CHANNEL_CLASS(spice_record_channel_parent_class)->channel_reset(channel, migrating);
+}
+
+static void spice_record_channel_class_init(SpiceRecordChannelClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS(klass);
+
+ gobject_class->finalize = spice_record_channel_finalize;
+ gobject_class->get_property = spice_record_channel_get_property;
+ gobject_class->set_property = spice_record_channel_set_property;
+ channel_class->channel_reset = channel_reset;
+ channel_class->channel_reset_capabilities = spice_record_channel_reset_capabilities;
+
+ g_object_class_install_property
+ (gobject_class, PROP_NCHANNELS,
+ g_param_spec_uint("nchannels",
+ "Number of Channels",
+ "Number of Channels",
+ 0, G_MAXUINT8, 2,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_VOLUME,
+ g_param_spec_pointer("volume",
+ "Playback volume",
+ "",
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_MUTE,
+ g_param_spec_boolean("mute",
+ "Mute",
+ "Mute",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+ /**
+ * SpiceRecordChannel::record-start:
+ * @channel: the #SpiceRecordChannel that emitted the signal
+ * @format: a #SPICE_AUDIO_FMT
+ * @channels: number of channels
+ * @rate: audio rate
+ *
+ * Notify when the recording should start, and provide audio format
+ * characteristics.
+ **/
+ signals[SPICE_RECORD_START] =
+ g_signal_new("record-start",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceRecordChannelClass, record_start),
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__INT_INT_INT,
+ G_TYPE_NONE,
+ 3,
+ G_TYPE_INT, G_TYPE_INT, G_TYPE_INT);
+
+ /**
+ * SpiceRecordChannel::record-stop:
+ * @channel: the #SpiceRecordChannel that emitted the signal
+ *
+ * Notify when the recording should stop.
+ **/
+ signals[SPICE_RECORD_STOP] =
+ g_signal_new("record-stop",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceRecordChannelClass, record_stop),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
+
+ g_type_class_add_private(klass, sizeof(SpiceRecordChannelPrivate));
+ channel_set_handlers(SPICE_CHANNEL_CLASS(klass));
+}
+
+/* main context */
+static void spice_record_mode(SpiceRecordChannel *channel, uint32_t time,
+ uint32_t mode, uint8_t *data, uint32_t data_size)
+{
+ SpiceMsgcRecordMode m = {0, };
+ SpiceMsgOut *msg;
+
+ g_return_if_fail(channel != NULL);
+ if (spice_channel_get_read_only(SPICE_CHANNEL(channel)))
+ return;
+
+ m.mode = mode;
+ m.time = time;
+ m.data = data;
+ m.data_size = data_size;
+
+ msg = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_RECORD_MODE);
+ msg->marshallers->msgc_record_mode(msg->marshaller, &m);
+ spice_msg_out_send(msg);
+}
+
+static int spice_record_desired_mode(SpiceChannel *channel, int frequency)
+{
+ if (!g_getenv("SPICE_DISABLE_OPUS") &&
+ snd_codec_is_capable(SPICE_AUDIO_DATA_MODE_OPUS, frequency) &&
+ spice_channel_test_capability(channel, SPICE_RECORD_CAP_OPUS)) {
+ return SPICE_AUDIO_DATA_MODE_OPUS;
+ } else if (!g_getenv("SPICE_DISABLE_CELT") &&
+ snd_codec_is_capable(SPICE_AUDIO_DATA_MODE_CELT_0_5_1, frequency) &&
+ spice_channel_test_capability(channel, SPICE_RECORD_CAP_CELT_0_5_1)) {
+ return SPICE_AUDIO_DATA_MODE_CELT_0_5_1;
+ } else {
+ return SPICE_AUDIO_DATA_MODE_RAW;
+ }
+}
+
+/* main context */
+static void spice_record_start_mark(SpiceRecordChannel *channel, uint32_t time)
+{
+ SpiceMsgcRecordStartMark m = {0, };
+ SpiceMsgOut *msg;
+
+ g_return_if_fail(channel != NULL);
+ if (spice_channel_get_read_only(SPICE_CHANNEL(channel)))
+ return;
+
+ m.time = time;
+
+ msg = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_RECORD_START_MARK);
+ msg->marshallers->msgc_record_start_mark(msg->marshaller, &m);
+ spice_msg_out_send(msg);
+}
+
+/**
+ * spice_record_send_data:
+ * @channel:
+ * @data: PCM data
+ * @bytes: size of @data
+ * @time: stream timestamp
+ *
+ * Send recorded PCM data to the guest.
+ **/
+void spice_record_send_data(SpiceRecordChannel *channel, gpointer data,
+ gsize bytes, uint32_t time)
+{
+ SpiceRecordChannelPrivate *rc;
+ SpiceMsgcRecordPacket p = {0, };
+
+ g_return_if_fail(SPICE_IS_RECORD_CHANNEL(channel));
+ rc = channel->priv;
+ if (rc->last_frame == NULL) {
+ CHANNEL_DEBUG(channel, "recording didn't start or was reset");
+ return;
+ }
+
+ g_return_if_fail(spice_channel_get_read_only(SPICE_CHANNEL(channel)) == FALSE);
+
+ uint8_t *encode_buf = NULL;
+
+ if (!rc->started) {
+ spice_record_mode(channel, time, rc->mode, NULL, 0);
+ spice_record_start_mark(channel, time);
+ rc->started = TRUE;
+ }
+
+ if (rc->mode != SPICE_AUDIO_DATA_MODE_RAW)
+ encode_buf = g_alloca(SND_CODEC_MAX_COMPRESSED_BYTES);
+
+ p.time = time;
+
+ while (bytes > 0) {
+ gsize n;
+ int frame_size;
+ SpiceMsgOut *msg;
+ uint8_t *frame;
+
+ if (rc->last_frame_current > 0) {
+ /* complete previous frame */
+ n = MIN(bytes, rc->frame_bytes - rc->last_frame_current);
+ memcpy(rc->last_frame + rc->last_frame_current, data, n);
+ rc->last_frame_current += n;
+ if (rc->last_frame_current < rc->frame_bytes)
+ /* if the frame is still incomplete, return */
+ break;
+ frame = rc->last_frame;
+ frame_size = rc->frame_bytes;
+ } else {
+ n = MIN(bytes, rc->frame_bytes);
+ frame_size = n;
+ frame = data;
+ }
+
+ if (rc->last_frame_current == 0 &&
+ n < rc->frame_bytes) {
+ /* start a new frame */
+ memcpy(rc->last_frame, data, n);
+ rc->last_frame_current = n;
+ break;
+ }
+
+ if (rc->mode != SPICE_AUDIO_DATA_MODE_RAW) {
+ int len = SND_CODEC_MAX_COMPRESSED_BYTES;
+ if (snd_codec_encode(rc->codec, frame, frame_size, encode_buf, &len) != SND_CODEC_OK) {
+ g_warning("encode failed");
+ return;
+ }
+ frame = encode_buf;
+ frame_size = len;
+ }
+
+ msg = spice_msg_out_new(SPICE_CHANNEL(channel), SPICE_MSGC_RECORD_DATA);
+ msg->marshallers->msgc_record_data(msg->marshaller, &p);
+ spice_marshaller_add(msg->marshaller, frame, frame_size);
+ spice_msg_out_send(msg);
+
+ if (rc->last_frame_current == rc->frame_bytes)
+ rc->last_frame_current = 0;
+
+ bytes -= n;
+ data = (guint8*)data + n;
+ }
+}
+
+/* ------------------------------------------------------------------ */
+
+/* coroutine context */
+static void record_handle_start(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceRecordChannelPrivate *c = SPICE_RECORD_CHANNEL(channel)->priv;
+ SpiceMsgRecordStart *start = spice_msg_in_parsed(in);
+ int frame_size = SND_CODEC_MAX_FRAME_SIZE;
+
+ c->mode = spice_record_desired_mode(channel, start->frequency);
+
+ CHANNEL_DEBUG(channel, "%s: fmt %d channels %d freq %d", __FUNCTION__,
+ start->format, start->channels, start->frequency);
+
+ g_return_if_fail(start->format == SPICE_AUDIO_FMT_S16);
+
+ snd_codec_destroy(&c->codec);
+
+ if (c->mode != SPICE_AUDIO_DATA_MODE_RAW) {
+ if (snd_codec_create(&c->codec, c->mode, start->frequency, SND_CODEC_ENCODE) != SND_CODEC_OK) {
+ g_warning("Failed to create encoder");
+ return;
+ }
+ frame_size = snd_codec_frame_size(c->codec);
+ }
+
+ g_free(c->last_frame);
+ c->frame_bytes = frame_size * 16 * start->channels / 8;
+ c->last_frame = g_malloc0(c->frame_bytes);
+ c->last_frame_current = 0;
+
+ g_coroutine_signal_emit(channel, signals[SPICE_RECORD_START], 0,
+ start->format, start->channels, start->frequency);
+}
+
+/* coroutine context */
+static void record_handle_stop(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceRecordChannelPrivate *rc = SPICE_RECORD_CHANNEL(channel)->priv;
+
+ g_coroutine_signal_emit(channel, signals[SPICE_RECORD_STOP], 0);
+ rc->started = FALSE;
+}
+
+/* coroutine context */
+static void record_handle_set_volume(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceRecordChannelPrivate *c = SPICE_RECORD_CHANNEL(channel)->priv;
+ SpiceMsgAudioVolume *vol = spice_msg_in_parsed(in);
+
+ if (vol->nchannels == 0) {
+ g_warning("spice-server send audio-volume-msg with 0 channels");
+ return;
+ }
+
+ g_free(c->volume);
+ c->nchannels = vol->nchannels;
+ c->volume = g_new(guint16, c->nchannels);
+ memcpy(c->volume, vol->volume, sizeof(guint16) * c->nchannels);
+ g_coroutine_object_notify(G_OBJECT(channel), "volume");
+}
+
+/* coroutine context */
+static void record_handle_set_mute(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceRecordChannelPrivate *c = SPICE_RECORD_CHANNEL(channel)->priv;
+ SpiceMsgAudioMute *m = spice_msg_in_parsed(in);
+
+ c->mute = m->mute;
+ g_coroutine_object_notify(G_OBJECT(channel), "mute");
+}
+
+static void channel_set_handlers(SpiceChannelClass *klass)
+{
+ static const spice_msg_handler handlers[] = {
+ [ SPICE_MSG_RECORD_START ] = record_handle_start,
+ [ SPICE_MSG_RECORD_STOP ] = record_handle_stop,
+ [ SPICE_MSG_RECORD_VOLUME ] = record_handle_set_volume,
+ [ SPICE_MSG_RECORD_MUTE ] = record_handle_set_mute,
+ };
+
+ spice_channel_set_handlers(klass, handlers, G_N_ELEMENTS(handlers));
+}
diff --git a/src/channel-record.h b/src/channel-record.h
new file mode 100644
index 0000000..20a9ad3
--- /dev/null
+++ b/src/channel-record.h
@@ -0,0 +1,77 @@
+/* -*- 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_CLIENT_RECORD_CHANNEL_H__
+#define __SPICE_CLIENT_RECORD_CHANNEL_H__
+
+#include "spice-client.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_RECORD_CHANNEL (spice_record_channel_get_type())
+#define SPICE_RECORD_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_RECORD_CHANNEL, SpiceRecordChannel))
+#define SPICE_RECORD_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_RECORD_CHANNEL, SpiceRecordChannelClass))
+#define SPICE_IS_RECORD_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_RECORD_CHANNEL))
+#define SPICE_IS_RECORD_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_RECORD_CHANNEL))
+#define SPICE_RECORD_CHANNEL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_RECORD_CHANNEL, SpiceRecordChannelClass))
+
+typedef struct _SpiceRecordChannel SpiceRecordChannel;
+typedef struct _SpiceRecordChannelClass SpiceRecordChannelClass;
+typedef struct _SpiceRecordChannelPrivate SpiceRecordChannelPrivate;
+
+/**
+ * SpiceRecordChannel:
+ *
+ * The #SpiceRecordChannel struct is opaque and should not be accessed directly.
+ */
+struct _SpiceRecordChannel {
+ SpiceChannel parent;
+
+ /*< private >*/
+ SpiceRecordChannelPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+/**
+ * SpiceRecordChannelClass:
+ * @parent_class: Parent class.
+ * @record_start: Signal class handler for the #SpiceRecordChannel::record-start signal.
+ * @record_stop: Signal class handler for the #SpiceRecordChannel::record-stop signal.
+ * @record_data: Unused (deprecated).
+ *
+ * Class structure for #SpiceRecordChannel.
+ */
+struct _SpiceRecordChannelClass {
+ SpiceChannelClass parent_class;
+
+ /* signals */
+ void (*record_start)(SpiceRecordChannel *channel,
+ gint format, gint channels, gint freq);
+ void (*record_data)(SpiceRecordChannel *channel, gpointer *data, gint size);
+ void (*record_stop)(SpiceRecordChannel *channel);
+
+ /*< private >*/
+ /* Do not add fields to this struct */
+};
+
+GType spice_record_channel_get_type(void);
+void spice_record_send_data(SpiceRecordChannel *channel, gpointer data,
+ gsize bytes, guint32 time);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_RECORD_CHANNEL_H__ */
diff --git a/src/channel-smartcard.c b/src/channel-smartcard.c
new file mode 100644
index 0000000..d91c9a0
--- /dev/null
+++ b/src/channel-smartcard.c
@@ -0,0 +1,587 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 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"
+
+#ifdef USE_SMARTCARD
+#include <vreader.h>
+#endif
+
+#include "spice-client.h"
+#include "spice-common.h"
+
+#include "spice-channel-priv.h"
+#include "smartcard-manager.h"
+#include "smartcard-manager-priv.h"
+#include "spice-session-priv.h"
+
+/**
+ * SECTION:channel-smartcard
+ * @short_description: smartcard authentication
+ * @title: Smartcard Channel
+ * @section_id:
+ * @see_also: #SpiceSmartcardManager, #SpiceSession
+ * @stability: API Stable (channel in development)
+ * @include: channel-smartcard.h
+ *
+ * The Spice protocol defines a set of messages to forward smartcard
+ * information from the Spice client to the VM. This channel handles
+ * these messages. While it's mainly focus on smartcard readers and
+ * smartcards, it's also possible to use it with a software smartcard
+ * (ie a set of 3 certificates from the client machine).
+ * This class doesn't provide useful methods, see #SpiceSession properties
+ * for a way to enable/disable this channel, and #SpiceSmartcardManager
+ * if you want to detect smartcard reader hotplug/unplug, and smartcard
+ * insertion/removal.
+ */
+
+#define SPICE_SMARTCARD_CHANNEL_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_SMARTCARD_CHANNEL, SpiceSmartcardChannelPrivate))
+
+struct _SpiceSmartcardChannelMessage {
+#ifdef USE_SMARTCARD
+ VSCMsgType message_type;
+#endif
+ SpiceMsgOut *message;
+};
+typedef struct _SpiceSmartcardChannelMessage SpiceSmartcardChannelMessage;
+
+
+struct _SpiceSmartcardChannelPrivate {
+ /* track readers that have been added but for which we didn't receive
+ * an ack from the spice server yet. We rely on the fact that the
+ * readers in this list are ordered by the time we sent the request to
+ * the server. When we get an ack from the server for a reader addition,
+ * we can pop the 1st entry to get the reader the ack corresponds to. */
+ GList *pending_reader_additions;
+
+ /* used to removals of readers that were not ack'ed yet by the spice
+ * server */
+ GHashTable *pending_reader_removals;
+
+ /* used to track card insertions on readers that were not ack'ed yet
+ * by the spice server */
+ GHashTable *pending_card_insertions;
+
+ /* next commands to be sent to the spice server. This is needed since
+ * we have to wait for a command answer before sending the next one
+ */
+ GQueue *message_queue;
+
+ /* message that is currently being processed by the spice server (ie last
+ * message that was sent to the server)
+ */
+ SpiceSmartcardChannelMessage *in_flight_message;
+};
+
+G_DEFINE_TYPE(SpiceSmartcardChannel, spice_smartcard_channel, SPICE_TYPE_CHANNEL)
+
+enum {
+
+ SPICE_SMARTCARD_LAST_SIGNAL,
+};
+
+static void spice_smartcard_channel_up(SpiceChannel *channel);
+static void handle_smartcard_msg(SpiceChannel *channel, SpiceMsgIn *in);
+static void smartcard_message_free(SpiceSmartcardChannelMessage *message);
+
+/* ------------------------------------------------------------------ */
+#ifdef USE_SMARTCARD
+static void reader_added_cb(SpiceSmartcardManager *manager, VReader *reader,
+ gpointer user_data);
+static void reader_removed_cb(SpiceSmartcardManager *manager, VReader *reader,
+ gpointer user_data);
+static void card_inserted_cb(SpiceSmartcardManager *manager, VReader *reader,
+ gpointer user_data);
+static void card_removed_cb(SpiceSmartcardManager *manager, VReader *reader,
+ gpointer user_data);
+#endif
+
+static void spice_smartcard_channel_init(SpiceSmartcardChannel *channel)
+{
+ SpiceSmartcardChannelPrivate *priv;
+
+ channel->priv = SPICE_SMARTCARD_CHANNEL_GET_PRIVATE(channel);
+ priv = channel->priv;
+ priv->message_queue = g_queue_new();
+
+#ifdef USE_SMARTCARD
+ priv->pending_card_insertions =
+ g_hash_table_new_full(g_direct_hash, g_direct_equal,
+ (GDestroyNotify)vreader_free, NULL);
+ priv->pending_reader_removals =
+ g_hash_table_new_full(g_direct_hash, g_direct_equal,
+ (GDestroyNotify)vreader_free, NULL);
+#endif
+}
+
+static void spice_smartcard_channel_constructed(GObject *object)
+{
+ SpiceSession *s = spice_channel_get_session(SPICE_CHANNEL(object));
+
+ g_return_if_fail(s != NULL);
+
+#ifdef USE_SMARTCARD
+ if (!spice_session_is_for_migration(s)) {
+ SpiceSmartcardChannel *channel = SPICE_SMARTCARD_CHANNEL(object);
+ SpiceSmartcardManager *manager = spice_smartcard_manager_get();
+
+ spice_g_signal_connect_object(G_OBJECT(manager), "reader-added",
+ (GCallback)reader_added_cb, channel, 0);
+ spice_g_signal_connect_object(G_OBJECT(manager), "reader-removed",
+ (GCallback)reader_removed_cb, channel, 0);
+ spice_g_signal_connect_object(G_OBJECT(manager), "card-inserted",
+ (GCallback)card_inserted_cb, channel, 0);
+ spice_g_signal_connect_object(G_OBJECT(manager), "card-removed",
+ (GCallback)card_removed_cb, channel, 0);
+ }
+#endif
+
+ if (G_OBJECT_CLASS(spice_smartcard_channel_parent_class)->constructed)
+ G_OBJECT_CLASS(spice_smartcard_channel_parent_class)->constructed(object);
+
+}
+
+static void spice_smartcard_channel_finalize(GObject *obj)
+{
+ SpiceSmartcardChannel *channel = SPICE_SMARTCARD_CHANNEL(obj);
+ SpiceSmartcardChannelPrivate *c = channel->priv;
+
+ if (c->pending_card_insertions != NULL) {
+ g_hash_table_destroy(c->pending_card_insertions);
+ c->pending_card_insertions = NULL;
+ }
+ if (c->pending_reader_removals != NULL) {
+ g_hash_table_destroy(c->pending_reader_removals);
+ c->pending_reader_removals = NULL;
+ }
+ if (c->message_queue != NULL) {
+ g_queue_foreach(c->message_queue, (GFunc)smartcard_message_free, NULL);
+ g_queue_free(c->message_queue);
+ c->message_queue = NULL;
+ }
+ if (c->in_flight_message != NULL) {
+ smartcard_message_free(c->in_flight_message);
+ c->in_flight_message = NULL;
+ }
+
+ g_list_free(c->pending_reader_additions);
+ c->pending_reader_additions = NULL;
+
+ if (G_OBJECT_CLASS(spice_smartcard_channel_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_smartcard_channel_parent_class)->finalize(obj);
+}
+
+static void spice_smartcard_channel_reset(SpiceChannel *channel, gboolean migrating)
+{
+ SpiceSmartcardChannel *smartcard_channel = SPICE_SMARTCARD_CHANNEL(channel);
+ SpiceSmartcardChannelPrivate *c = smartcard_channel->priv;
+
+ g_hash_table_remove_all(c->pending_card_insertions);
+ g_hash_table_remove_all(c->pending_reader_removals);
+
+ if (c->message_queue != NULL) {
+ g_queue_foreach(c->message_queue, (GFunc)smartcard_message_free, NULL);
+ g_queue_clear(c->message_queue);
+ }
+
+ if (c->in_flight_message != NULL) {
+ smartcard_message_free(c->in_flight_message);
+ c->in_flight_message = NULL;
+ }
+
+ g_list_free(c->pending_reader_additions);
+ c->pending_reader_additions = NULL;
+
+ SPICE_CHANNEL_CLASS(spice_smartcard_channel_parent_class)->channel_reset(channel, migrating);
+}
+
+static void channel_set_handlers(SpiceChannelClass *klass)
+{
+ static const spice_msg_handler handlers[] = {
+ [ SPICE_MSG_SMARTCARD_DATA ] = handle_smartcard_msg,
+ };
+ spice_channel_set_handlers(klass, handlers, G_N_ELEMENTS(handlers));
+}
+
+static void spice_smartcard_channel_class_init(SpiceSmartcardChannelClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS(klass);
+
+ gobject_class->finalize = spice_smartcard_channel_finalize;
+ gobject_class->constructed = spice_smartcard_channel_constructed;
+
+ channel_class->channel_up = spice_smartcard_channel_up;
+ channel_class->channel_reset = spice_smartcard_channel_reset;
+
+ g_type_class_add_private(klass, sizeof(SpiceSmartcardChannelPrivate));
+ channel_set_handlers(SPICE_CHANNEL_CLASS(klass));
+}
+
+/* ------------------------------------------------------------------ */
+/* private api */
+
+static void
+smartcard_message_free(SpiceSmartcardChannelMessage *message)
+{
+ if (message->message)
+ spice_msg_out_unref(message->message);
+ g_slice_free(SpiceSmartcardChannelMessage, message);
+}
+
+#if USE_SMARTCARD
+static gboolean is_attached_to_server(VReader *reader)
+{
+ return (vreader_get_id(reader) != (vreader_id_t)-1);
+}
+
+static gboolean
+spice_channel_has_pending_card_insertion(SpiceSmartcardChannel *channel,
+ VReader *reader)
+{
+ return (g_hash_table_lookup(channel->priv->pending_card_insertions, reader) != NULL);
+}
+
+static void
+spice_channel_queue_card_insertion(SpiceSmartcardChannel *channel,
+ VReader *reader)
+{
+ vreader_reference(reader);
+ g_hash_table_insert(channel->priv->pending_card_insertions,
+ reader, reader);
+}
+
+static void
+spice_channel_drop_pending_card_insertion(SpiceSmartcardChannel *channel,
+ VReader *reader)
+{
+ g_hash_table_remove(channel->priv->pending_card_insertions, reader);
+}
+
+static gboolean
+spice_channel_has_pending_reader_removal(SpiceSmartcardChannel *channel,
+ VReader *reader)
+{
+ return (g_hash_table_lookup(channel->priv->pending_reader_removals, reader) != NULL);
+}
+
+static void
+spice_channel_queue_reader_removal(SpiceSmartcardChannel *channel,
+ VReader *reader)
+{
+ vreader_reference(reader);
+ g_hash_table_insert(channel->priv->pending_reader_removals,
+ reader, reader);
+}
+
+static void
+spice_channel_drop_pending_reader_removal(SpiceSmartcardChannel *channel,
+ VReader *reader)
+{
+ g_hash_table_remove(channel->priv->pending_reader_removals, reader);
+}
+
+static SpiceSmartcardChannelMessage *
+smartcard_message_new(VSCMsgType msg_type, SpiceMsgOut *msg_out)
+{
+ SpiceSmartcardChannelMessage *message;
+
+ message = g_slice_new0(SpiceSmartcardChannelMessage);
+ message->message = msg_out;
+ message->message_type = msg_type;
+
+ return message;
+}
+
+/* Indicates that handling of the message that is currently in flight has
+ * been completed. If needed, sends the next queued command to the server. */
+static void
+smartcard_message_complete_in_flight(SpiceSmartcardChannel *channel)
+{
+ g_return_if_fail(channel->priv->in_flight_message != NULL);
+
+ smartcard_message_free(channel->priv->in_flight_message);
+ channel->priv->in_flight_message = g_queue_pop_head(channel->priv->message_queue);
+ if (channel->priv->in_flight_message != NULL) {
+ spice_msg_out_send(channel->priv->in_flight_message->message);
+ channel->priv->in_flight_message->message = NULL;
+ }
+}
+
+static void smartcard_message_send(SpiceSmartcardChannel *channel,
+ VSCMsgType msg_type,
+ SpiceMsgOut *msg_out, gboolean queue)
+{
+ SpiceSmartcardChannelMessage *message;
+
+ if (spice_channel_get_read_only(SPICE_CHANNEL(channel)))
+ return;
+
+ CHANNEL_DEBUG(channel, "send message %d, %s",
+ msg_type, queue ? "queued" : "now");
+ if (!queue) {
+ spice_msg_out_send(msg_out);
+ return;
+ }
+
+ message = smartcard_message_new(msg_type, msg_out);
+ if (channel->priv->in_flight_message == NULL) {
+ g_return_if_fail(g_queue_is_empty(channel->priv->message_queue));
+ channel->priv->in_flight_message = message;
+ spice_msg_out_send(channel->priv->in_flight_message->message);
+ channel->priv->in_flight_message->message = NULL;
+ } else {
+ g_queue_push_tail(channel->priv->message_queue, message);
+ }
+}
+
+static void
+send_msg_generic_with_data(SpiceSmartcardChannel *channel, VReader *reader,
+ VSCMsgType msg_type,
+ const uint8_t *data, gsize data_len,
+ gboolean serialize_msg)
+{
+ SpiceMsgOut *msg_out;
+ VSCMsgHeader header = {
+ .type = msg_type,
+ .length = data_len
+ };
+
+ if(vreader_get_id(reader) == -1)
+ header.reader_id = VSCARD_UNDEFINED_READER_ID;
+ else
+ header.reader_id = vreader_get_id(reader);
+
+ msg_out = spice_msg_out_new(SPICE_CHANNEL(channel),
+ SPICE_MSGC_SMARTCARD_DATA);
+ msg_out->marshallers->msgc_smartcard_header(msg_out->marshaller, &header);
+ if ((data != NULL) && (data_len != 0)) {
+ spice_marshaller_add(msg_out->marshaller, data, data_len);
+ }
+
+ smartcard_message_send(channel, msg_type, msg_out, serialize_msg);
+}
+
+static void send_msg_generic(SpiceSmartcardChannel *channel, VReader *reader,
+ VSCMsgType msg_type)
+{
+ send_msg_generic_with_data(channel, reader, msg_type, NULL, 0, TRUE);
+}
+
+static void send_msg_atr(SpiceSmartcardChannel *channel, VReader *reader)
+{
+#define MAX_ATR_LEN 40 //this should be defined in libcacard
+ uint8_t atr[MAX_ATR_LEN];
+ int atr_len = MAX_ATR_LEN;
+
+ g_return_if_fail(vreader_get_id(reader) != VSCARD_UNDEFINED_READER_ID);
+ vreader_power_on(reader, atr, &atr_len);
+ send_msg_generic_with_data(channel, reader, VSC_ATR, atr, atr_len, TRUE);
+}
+
+static void reader_added_cb(SpiceSmartcardManager *manager, VReader *reader,
+ gpointer user_data)
+{
+ SpiceSmartcardChannel *channel = SPICE_SMARTCARD_CHANNEL(user_data);
+ const char *reader_name = vreader_get_name(reader);
+
+ if (vreader_get_id(reader) != -1 ||
+ g_list_find(channel->priv->pending_reader_additions, reader))
+ return;
+
+ channel->priv->pending_reader_additions =
+ g_list_append(channel->priv->pending_reader_additions, reader);
+
+ send_msg_generic_with_data(channel, reader, VSC_ReaderAdd,
+ (uint8_t*)reader_name, strlen(reader_name), TRUE);
+}
+
+static void reader_removed_cb(SpiceSmartcardManager *manager, VReader *reader,
+ gpointer user_data)
+{
+ SpiceSmartcardChannel *channel = SPICE_SMARTCARD_CHANNEL(user_data);
+
+ if (is_attached_to_server(reader)) {
+ send_msg_generic(channel, reader, VSC_ReaderRemove);
+ } else {
+ spice_channel_queue_reader_removal(channel, reader);
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* callbacks */
+static void card_inserted_cb(SpiceSmartcardManager *manager, VReader *reader,
+ gpointer user_data)
+{
+ SpiceSmartcardChannel *channel = SPICE_SMARTCARD_CHANNEL(user_data);
+
+ if (is_attached_to_server(reader)) {
+ send_msg_atr(channel, reader);
+ } else {
+ spice_channel_queue_card_insertion(channel, reader);
+ }
+}
+
+static void card_removed_cb(SpiceSmartcardManager *manager, VReader *reader,
+ gpointer user_data)
+{
+ SpiceSmartcardChannel *channel = SPICE_SMARTCARD_CHANNEL(user_data);
+
+ if (is_attached_to_server(reader)) {
+ send_msg_generic(channel, reader, VSC_CardRemove);
+ } else {
+ /* this does nothing when reader has no card insertion pending */
+ spice_channel_drop_pending_card_insertion(channel, reader);
+ }
+}
+#endif /* USE_SMARTCARD */
+
+static void spice_smartcard_channel_up_cb(GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SpiceChannel *channel = SPICE_CHANNEL(user_data);
+#ifdef USE_SMARTCARD
+ SpiceSmartcardManager *manager = spice_smartcard_manager_get();
+ GList *l, *list = NULL;
+#endif
+ GError *error = NULL;
+
+ g_return_if_fail(channel != NULL);
+ g_return_if_fail(SPICE_IS_SESSION(source_object));
+
+ spice_smartcard_manager_init_finish(SPICE_SESSION(source_object),
+ res, &error);
+ if (error) {
+ g_warning("%s", error->message);
+ goto end;
+ }
+
+#ifdef USE_SMARTCARD
+ list = spice_smartcard_manager_get_readers(manager);
+ for (l = list; l != NULL; l = l->next) {
+ VReader *reader = l->data;
+ gboolean has_card = vreader_card_is_present(reader) == VREADER_OK;
+
+ reader_added_cb(manager, reader, channel);
+ if (has_card)
+ card_inserted_cb(manager, reader, channel);
+
+ g_boxed_free(SPICE_TYPE_SMARTCARD_READER, reader);
+ }
+#endif
+
+end:
+#ifdef USE_SMARTCARD
+ g_list_free(list);
+#endif
+ g_clear_error(&error);
+}
+
+static void spice_smartcard_channel_up(SpiceChannel *channel)
+{
+ if (spice_session_is_for_migration(spice_channel_get_session(channel)))
+ return;
+
+ spice_smartcard_manager_init_async(spice_channel_get_session(channel),
+ g_cancellable_new(),
+ spice_smartcard_channel_up_cb,
+ channel);
+}
+
+static void handle_smartcard_msg(SpiceChannel *channel, SpiceMsgIn *in)
+{
+#ifdef USE_SMARTCARD
+ SpiceSmartcardChannel *smartcard_channel = SPICE_SMARTCARD_CHANNEL(channel);
+ SpiceSmartcardChannelPrivate *priv = smartcard_channel->priv;
+ SpiceMsgSmartcard *msg = spice_msg_in_parsed(in);
+ VReader *reader;
+
+ CHANNEL_DEBUG(channel, "handle msg %d", msg->type);
+ switch (msg->type) {
+ case VSC_Error:
+ g_return_if_fail(priv->in_flight_message != NULL);
+ CHANNEL_DEBUG(channel, "in flight %d", priv->in_flight_message->message_type);
+ switch (priv->in_flight_message->message_type) {
+ case VSC_ReaderAdd:
+ g_return_if_fail(priv->pending_reader_additions != NULL);
+ reader = priv->pending_reader_additions->data;
+ g_return_if_fail(reader != NULL);
+ g_return_if_fail(vreader_get_id(reader) == -1);
+ priv->pending_reader_additions =
+ g_list_delete_link(priv->pending_reader_additions,
+ priv->pending_reader_additions);
+ vreader_set_id(reader, msg->reader_id);
+
+ if (spice_channel_has_pending_card_insertion(smartcard_channel, reader)) {
+ send_msg_atr(smartcard_channel, reader);
+ spice_channel_drop_pending_card_insertion(smartcard_channel, reader);
+ }
+
+ if (spice_channel_has_pending_reader_removal(smartcard_channel, reader)) {
+ send_msg_generic(smartcard_channel, reader, VSC_CardRemove);
+ spice_channel_drop_pending_reader_removal(smartcard_channel, reader);
+ }
+ break;
+ case VSC_APDU:
+ case VSC_ATR:
+ case VSC_CardRemove:
+ case VSC_Error:
+ case VSC_ReaderRemove:
+ break;
+ default:
+ g_warning("Unexpected message: %d", priv->in_flight_message->message_type);
+ break;
+ }
+ smartcard_message_complete_in_flight(smartcard_channel);
+
+ break;
+
+ case VSC_APDU:
+ case VSC_Init: {
+ const unsigned int APDU_BUFFER_SIZE = 270;
+ VReaderStatus reader_status;
+ uint8_t data_out[APDU_BUFFER_SIZE + sizeof(uint32_t)];
+ int data_out_len = sizeof(data_out);
+
+ g_return_if_fail(msg->reader_id != VSCARD_UNDEFINED_READER_ID);
+ reader = vreader_get_reader_by_id(msg->reader_id);
+ g_return_if_fail(reader != NULL); //FIXME: add log message
+
+ reader_status = vreader_xfr_bytes(reader,
+ msg->data, msg->length,
+ data_out, &data_out_len);
+ if (reader_status == VREADER_OK) {
+ send_msg_generic_with_data(smartcard_channel,
+ reader, VSC_APDU,
+ data_out, data_out_len, FALSE);
+ } else {
+ uint32_t error_code;
+ error_code = GUINT32_TO_LE(reader_status);
+ send_msg_generic_with_data(smartcard_channel,
+ reader, VSC_Error,
+ (uint8_t*)&error_code,
+ sizeof (error_code), FALSE);
+ }
+ break;
+ }
+ default:
+ g_return_if_reached();
+ }
+#endif
+}
diff --git a/src/channel-smartcard.h b/src/channel-smartcard.h
new file mode 100644
index 0000000..28c8b88
--- /dev/null
+++ b/src/channel-smartcard.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 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/>.
+*/
+#ifndef __SPICE_CLIENT_SMARTCARD_CHANNEL_H__
+#define __SPICE_CLIENT_SMARTCARD_CHANNEL_H__
+
+#include "spice-client.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_SMARTCARD_CHANNEL (spice_smartcard_channel_get_type())
+#define SPICE_SMARTCARD_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_SMARTCARD_CHANNEL, SpiceSmartcardChannel))
+#define SPICE_SMARTCARD_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_SMARTCARD_CHANNEL, SpiceSmartcardChannelClass))
+#define SPICE_IS_SMARTCARD_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_SMARTCARD_CHANNEL))
+#define SPICE_IS_SMARTCARD_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_SMARTCARD_CHANNEL))
+#define SPICE_SMARTCARD_CHANNEL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_SMARTCARD_CHANNEL, SpiceSmartcardChannelClass))
+
+typedef struct _SpiceSmartcardChannel SpiceSmartcardChannel;
+typedef struct _SpiceSmartcardChannelClass SpiceSmartcardChannelClass;
+typedef struct _SpiceSmartcardChannelPrivate SpiceSmartcardChannelPrivate;
+
+/**
+ * SpiceSmartcardChannel:
+ *
+ * The #SpiceSmartcardChannel struct is opaque and should not be accessed directly.
+ */
+struct _SpiceSmartcardChannel {
+ SpiceChannel parent;
+
+ /*< private >*/
+ SpiceSmartcardChannelPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+/**
+ * SpiceSmartcardChannelClass:
+ * @parent_class: Parent class.
+ *
+ * Class structure for #SpiceSmartcardChannel.
+ */
+struct _SpiceSmartcardChannelClass {
+ SpiceChannelClass parent_class;
+
+ /* signals */
+
+ /*< private >*/
+ /* Do not add fields to this struct */
+};
+
+GType spice_smartcard_channel_get_type(void);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_SMARTCARD_CHANNEL_H__ */
diff --git a/src/channel-usbredir-priv.h b/src/channel-usbredir-priv.h
new file mode 100644
index 0000000..2c4c6f7
--- /dev/null
+++ b/src/channel-usbredir-priv.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2011 Red Hat, Inc.
+
+ Red Hat Authors:
+ Hans de Goede <hdegoede@redhat.com>
+
+ 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_CLIENT_USBREDIR_CHANNEL_PRIV_H__
+#define __SPICE_CLIENT_USBREDIR_CHANNEL_PRIV_H__
+
+#include <libusb.h>
+#include <usbredirfilter.h>
+#include "spice-client.h"
+
+G_BEGIN_DECLS
+
+/* Note: this must be called before calling any other functions, and the
+ context should not be destroyed before the last device has been
+ disconnected */
+void spice_usbredir_channel_set_context(SpiceUsbredirChannel *channel,
+ libusb_context *context);
+
+/* Note the context must be set, and the channel must be brought up
+ (through spice_channel_connect()), before calling this. */
+void spice_usbredir_channel_connect_device_async(
+ SpiceUsbredirChannel *channel,
+ libusb_device *device,
+ SpiceUsbDevice *spice_device,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gboolean spice_usbredir_channel_connect_device_finish(
+ SpiceUsbredirChannel *channel,
+ GAsyncResult *res,
+ GError **err);
+
+void spice_usbredir_channel_disconnect_device(SpiceUsbredirChannel *channel);
+
+libusb_device *spice_usbredir_channel_get_device(SpiceUsbredirChannel *channel);
+
+void spice_usbredir_channel_get_guest_filter(
+ SpiceUsbredirChannel *channel,
+ const struct usbredirfilter_rule **rules_ret,
+ int *rules_count_ret);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_USBREDIR_CHANNEL_PRIV_H__ */
diff --git a/src/channel-usbredir.c b/src/channel-usbredir.c
new file mode 100644
index 0000000..292b82f
--- /dev/null
+++ b/src/channel-usbredir.c
@@ -0,0 +1,686 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2010-2012 Red Hat, Inc.
+
+ Red Hat Authors:
+ Hans de Goede <hdegoede@redhat.com>
+ Richard Hughes <rhughes@redhat.com>
+
+ 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"
+
+#ifdef USE_USBREDIR
+#include <glib/gi18n.h>
+#include <usbredirhost.h>
+#if USE_POLKIT
+#include "usb-acl-helper.h"
+#endif
+#include "channel-usbredir-priv.h"
+#include "usb-device-manager-priv.h"
+#include "usbutil.h"
+#endif
+
+#include "spice-client.h"
+#include "spice-common.h"
+
+#include "spice-channel-priv.h"
+#include "glib-compat.h"
+
+/**
+ * SECTION:channel-usbredir
+ * @short_description: usb redirection
+ * @title: USB Redirection Channel
+ * @section_id:
+ * @stability: API Stable (channel in development)
+ * @include: channel-usbredir.h
+ *
+ * The Spice protocol defines a set of messages to redirect USB devices
+ * from the Spice client to the VM. This channel handles these messages.
+ */
+
+#ifdef USE_USBREDIR
+
+#define SPICE_USBREDIR_CHANNEL_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_USBREDIR_CHANNEL, SpiceUsbredirChannelPrivate))
+
+enum SpiceUsbredirChannelState {
+ STATE_DISCONNECTED,
+#if USE_POLKIT
+ STATE_WAITING_FOR_ACL_HELPER,
+#endif
+ STATE_CONNECTED,
+ STATE_DISCONNECTING,
+};
+
+struct _SpiceUsbredirChannelPrivate {
+ libusb_device *device;
+ SpiceUsbDevice *spice_device;
+ libusb_context *context;
+ struct usbredirhost *host;
+ /* To catch usbredirhost error messages and report them as a GError */
+ GError **catch_error;
+ /* Data passed from channel handle msg to the usbredirhost read cb */
+ const uint8_t *read_buf;
+ int read_buf_size;
+ enum SpiceUsbredirChannelState state;
+#if USE_POLKIT
+ GSimpleAsyncResult *result;
+ SpiceUsbAclHelper *acl_helper;
+#endif
+};
+
+static void channel_set_handlers(SpiceChannelClass *klass);
+static void spice_usbredir_channel_up(SpiceChannel *channel);
+static void spice_usbredir_channel_dispose(GObject *obj);
+static void spice_usbredir_channel_finalize(GObject *obj);
+static void usbredir_handle_msg(SpiceChannel *channel, SpiceMsgIn *in);
+
+static void usbredir_log(void *user_data, int level, const char *msg);
+static int usbredir_read_callback(void *user_data, uint8_t *data, int count);
+static int usbredir_write_callback(void *user_data, uint8_t *data, int count);
+static void usbredir_write_flush_callback(void *user_data);
+
+static void *usbredir_alloc_lock(void);
+static void usbredir_lock_lock(void *user_data);
+static void usbredir_unlock_lock(void *user_data);
+static void usbredir_free_lock(void *user_data);
+
+#endif
+
+G_DEFINE_TYPE(SpiceUsbredirChannel, spice_usbredir_channel, SPICE_TYPE_CHANNEL)
+
+/* ------------------------------------------------------------------ */
+
+static void spice_usbredir_channel_init(SpiceUsbredirChannel *channel)
+{
+#ifdef USE_USBREDIR
+ channel->priv = SPICE_USBREDIR_CHANNEL_GET_PRIVATE(channel);
+#endif
+}
+
+#ifdef USE_USBREDIR
+static void spice_usbredir_channel_reset(SpiceChannel *c, gboolean migrating)
+{
+ SpiceUsbredirChannel *channel = SPICE_USBREDIR_CHANNEL(c);
+ SpiceUsbredirChannelPrivate *priv = channel->priv;
+
+ if (priv->host) {
+ if (priv->state == STATE_CONNECTED)
+ spice_usbredir_channel_disconnect_device(channel);
+ usbredirhost_close(priv->host);
+ priv->host = NULL;
+ /* Call set_context to re-create the host */
+ spice_usbredir_channel_set_context(channel, priv->context);
+ }
+ SPICE_CHANNEL_CLASS(spice_usbredir_channel_parent_class)->channel_reset(c, migrating);
+}
+#endif
+
+static void spice_usbredir_channel_class_init(SpiceUsbredirChannelClass *klass)
+{
+#ifdef USE_USBREDIR
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS(klass);
+
+ gobject_class->dispose = spice_usbredir_channel_dispose;
+ gobject_class->finalize = spice_usbredir_channel_finalize;
+ channel_class->channel_up = spice_usbredir_channel_up;
+ channel_class->channel_reset = spice_usbredir_channel_reset;
+
+ g_type_class_add_private(klass, sizeof(SpiceUsbredirChannelPrivate));
+ channel_set_handlers(SPICE_CHANNEL_CLASS(klass));
+#endif
+}
+
+#ifdef USE_USBREDIR
+static void spice_usbredir_channel_dispose(GObject *obj)
+{
+ SpiceUsbredirChannel *channel = SPICE_USBREDIR_CHANNEL(obj);
+
+ spice_usbredir_channel_disconnect_device(channel);
+
+ /* Chain up to the parent class */
+ if (G_OBJECT_CLASS(spice_usbredir_channel_parent_class)->dispose)
+ G_OBJECT_CLASS(spice_usbredir_channel_parent_class)->dispose(obj);
+}
+
+/*
+ * Note we don't unref our device / acl_helper / result references in our
+ * finalize. The reason for this is that depending on our state at dispose
+ * time they are either:
+ * 1) Already unreferenced
+ * 2) Will be unreferenced by the disconnect_device call from dispose
+ * 3) Will be unreferenced by spice_usbredir_channel_open_acl_cb
+ *
+ * Now the last one may seem like an issue, since what will happen if
+ * spice_usbredir_channel_open_acl_cb will run after finalization?
+ *
+ * This will never happens since the GSimpleAsyncResult created before we
+ * get into the STATE_WAITING_FOR_ACL_HELPER takes a reference to its
+ * source object, which is our SpiceUsbredirChannel object, so
+ * the finalize won't hapen until spice_usbredir_channel_open_acl_cb runs,
+ * and unrefs priv->result which will in turn unref ourselve once the
+ * complete_in_idle call it does has completed. And once
+ * spice_usbredir_channel_open_acl_cb has run, all references we hold have
+ * been released even in the 3th scenario.
+ */
+static void spice_usbredir_channel_finalize(GObject *obj)
+{
+ SpiceUsbredirChannel *channel = SPICE_USBREDIR_CHANNEL(obj);
+
+ if (channel->priv->host)
+ usbredirhost_close(channel->priv->host);
+
+ /* Chain up to the parent class */
+ if (G_OBJECT_CLASS(spice_usbredir_channel_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_usbredir_channel_parent_class)->finalize(obj);
+}
+
+static void channel_set_handlers(SpiceChannelClass *klass)
+{
+ static const spice_msg_handler handlers[] = {
+ [ SPICE_MSG_SPICEVMC_DATA ] = usbredir_handle_msg,
+ };
+
+ spice_channel_set_handlers(klass, handlers, G_N_ELEMENTS(handlers));
+}
+
+/* ------------------------------------------------------------------ */
+/* private api */
+
+G_GNUC_INTERNAL
+void spice_usbredir_channel_set_context(SpiceUsbredirChannel *channel,
+ libusb_context *context)
+{
+ SpiceUsbredirChannelPrivate *priv = channel->priv;
+
+ g_return_if_fail(priv->host == NULL);
+
+ priv->context = context;
+ priv->host = usbredirhost_open_full(
+ context, NULL,
+ usbredir_log,
+ usbredir_read_callback,
+ usbredir_write_callback,
+ usbredir_write_flush_callback,
+ usbredir_alloc_lock,
+ usbredir_lock_lock,
+ usbredir_unlock_lock,
+ usbredir_free_lock,
+ channel, PACKAGE_STRING,
+ spice_util_get_debug() ? usbredirparser_debug : usbredirparser_warning,
+ usbredirhost_fl_write_cb_owns_buffer);
+ if (!priv->host)
+ g_error("Out of memory allocating usbredirhost");
+}
+
+static gboolean spice_usbredir_channel_open_device(
+ SpiceUsbredirChannel *channel, GError **err)
+{
+ SpiceUsbredirChannelPrivate *priv = channel->priv;
+ libusb_device_handle *handle = NULL;
+ int rc, status;
+
+ g_return_val_if_fail(priv->state == STATE_DISCONNECTED
+#if USE_POLKIT
+ || priv->state == STATE_WAITING_FOR_ACL_HELPER
+#endif
+ , FALSE);
+
+ rc = libusb_open(priv->device, &handle);
+ if (rc != 0) {
+ g_set_error(err, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "Could not open usb device: %s [%i]",
+ spice_usbutil_libusb_strerror(rc), rc);
+ return FALSE;
+ }
+
+ priv->catch_error = err;
+ status = usbredirhost_set_device(priv->host, handle);
+ priv->catch_error = NULL;
+ if (status != usb_redir_success) {
+ g_return_val_if_fail(err == NULL || *err != NULL, FALSE);
+ return FALSE;
+ }
+
+ if (!spice_usb_device_manager_start_event_listening(
+ spice_usb_device_manager_get(
+ spice_channel_get_session(SPICE_CHANNEL(channel)), NULL),
+ err)) {
+ usbredirhost_set_device(priv->host, NULL);
+ return FALSE;
+ }
+
+ priv->state = STATE_CONNECTED;
+
+ return TRUE;
+}
+
+#if USE_POLKIT
+static void spice_usbredir_channel_open_acl_cb(
+ GObject *gobject, GAsyncResult *acl_res, gpointer user_data)
+{
+ SpiceUsbAclHelper *acl_helper = SPICE_USB_ACL_HELPER(gobject);
+ SpiceUsbredirChannel *channel = SPICE_USBREDIR_CHANNEL(user_data);
+ SpiceUsbredirChannelPrivate *priv = channel->priv;
+ GError *err = NULL;
+
+ g_return_if_fail(acl_helper == priv->acl_helper);
+ g_return_if_fail(priv->state == STATE_WAITING_FOR_ACL_HELPER ||
+ priv->state == STATE_DISCONNECTING);
+
+ spice_usb_acl_helper_open_acl_finish(acl_helper, acl_res, &err);
+ if (!err && priv->state == STATE_DISCONNECTING) {
+ err = g_error_new_literal(G_IO_ERROR, G_IO_ERROR_CANCELLED,
+ "USB redirection channel connect cancelled");
+ }
+ if (!err) {
+ spice_usbredir_channel_open_device(channel, &err);
+ }
+ if (err) {
+ g_simple_async_result_take_error(priv->result, err);
+ libusb_unref_device(priv->device);
+ priv->device = NULL;
+ g_boxed_free(spice_usb_device_get_type(), priv->spice_device);
+ priv->spice_device = NULL;
+ priv->state = STATE_DISCONNECTED;
+ }
+
+ spice_usb_acl_helper_close_acl(priv->acl_helper);
+ g_clear_object(&priv->acl_helper);
+ g_object_set(spice_channel_get_session(SPICE_CHANNEL(channel)),
+ "inhibit-keyboard-grab", FALSE, NULL);
+
+ g_simple_async_result_complete_in_idle(priv->result);
+ g_clear_object(&priv->result);
+}
+#endif
+
+G_GNUC_INTERNAL
+void spice_usbredir_channel_connect_device_async(
+ SpiceUsbredirChannel *channel,
+ libusb_device *device,
+ SpiceUsbDevice *spice_device,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SpiceUsbredirChannelPrivate *priv = channel->priv;
+ GSimpleAsyncResult *result;
+#if ! USE_POLKIT
+ GError *err = NULL;
+#endif
+
+ g_return_if_fail(SPICE_IS_USBREDIR_CHANNEL(channel));
+ g_return_if_fail(device != NULL);
+
+ CHANNEL_DEBUG(channel, "connecting usb channel %p", channel);
+
+ result = g_simple_async_result_new(G_OBJECT(channel), callback, user_data,
+ spice_usbredir_channel_connect_device_async);
+
+ if (!priv->host) {
+ g_simple_async_result_set_error(result,
+ SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "Error libusb context not set");
+ goto done;
+ }
+
+ if (priv->state != STATE_DISCONNECTED) {
+ g_simple_async_result_set_error(result,
+ SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "Error channel is busy");
+ goto done;
+ }
+
+ priv->device = libusb_ref_device(device);
+ priv->spice_device = g_boxed_copy(spice_usb_device_get_type(),
+ spice_device);
+#if USE_POLKIT
+ priv->result = result;
+ priv->state = STATE_WAITING_FOR_ACL_HELPER;
+ priv->acl_helper = spice_usb_acl_helper_new();
+ g_object_set(spice_channel_get_session(SPICE_CHANNEL(channel)),
+ "inhibit-keyboard-grab", TRUE, NULL);
+ spice_usb_acl_helper_open_acl(priv->acl_helper,
+ libusb_get_bus_number(device),
+ libusb_get_device_address(device),
+ cancellable,
+ spice_usbredir_channel_open_acl_cb,
+ channel);
+ return;
+#else
+ if (!spice_usbredir_channel_open_device(channel, &err)) {
+ g_simple_async_result_take_error(result, err);
+ libusb_unref_device(priv->device);
+ priv->device = NULL;
+ g_boxed_free(spice_usb_device_get_type(), priv->spice_device);
+ priv->spice_device = NULL;
+ }
+#endif
+
+done:
+ g_simple_async_result_complete_in_idle(result);
+ g_object_unref(result);
+}
+
+G_GNUC_INTERNAL
+gboolean spice_usbredir_channel_connect_device_finish(
+ SpiceUsbredirChannel *channel,
+ GAsyncResult *res,
+ GError **err)
+{
+ GSimpleAsyncResult *result = G_SIMPLE_ASYNC_RESULT(res);
+
+ g_return_val_if_fail(g_simple_async_result_is_valid(res, G_OBJECT(channel),
+ spice_usbredir_channel_connect_device_async),
+ FALSE);
+
+ if (g_simple_async_result_propagate_error(result, err))
+ return FALSE;
+
+ return TRUE;
+}
+
+G_GNUC_INTERNAL
+void spice_usbredir_channel_disconnect_device(SpiceUsbredirChannel *channel)
+{
+ SpiceUsbredirChannelPrivate *priv = channel->priv;
+
+ CHANNEL_DEBUG(channel, "disconnecting device from usb channel %p", channel);
+
+ switch (priv->state) {
+ case STATE_DISCONNECTED:
+ case STATE_DISCONNECTING:
+ break;
+#if USE_POLKIT
+ case STATE_WAITING_FOR_ACL_HELPER:
+ priv->state = STATE_DISCONNECTING;
+ /* We're still waiting for the acl helper -> cancel it */
+ spice_usb_acl_helper_close_acl(priv->acl_helper);
+ break;
+#endif
+ case STATE_CONNECTED:
+ /*
+ * This sets the usb event thread run condition to FALSE, therefor
+ * it must be done before usbredirhost_set_device NULL, as
+ * usbredirhost_set_device NULL will interrupt the
+ * libusb_handle_events call in the thread.
+ */
+ {
+ SpiceSession *session = spice_channel_get_session(SPICE_CHANNEL(channel));
+ if (session != NULL)
+ spice_usb_device_manager_stop_event_listening(
+ spice_usb_device_manager_get(session, NULL));
+ }
+ /* This also closes the libusb handle we passed from open_device */
+ usbredirhost_set_device(priv->host, NULL);
+ libusb_unref_device(priv->device);
+ priv->device = NULL;
+ g_boxed_free(spice_usb_device_get_type(), priv->spice_device);
+ priv->spice_device = NULL;
+ priv->state = STATE_DISCONNECTED;
+ break;
+ }
+}
+
+G_GNUC_INTERNAL
+libusb_device *spice_usbredir_channel_get_device(SpiceUsbredirChannel *channel)
+{
+ return channel->priv->device;
+}
+
+G_GNUC_INTERNAL
+void spice_usbredir_channel_get_guest_filter(
+ SpiceUsbredirChannel *channel,
+ const struct usbredirfilter_rule **rules_ret,
+ int *rules_count_ret)
+{
+ SpiceUsbredirChannelPrivate *priv = channel->priv;
+
+ g_return_if_fail(priv->host != NULL);
+
+ usbredirhost_get_guest_filter(priv->host, rules_ret, rules_count_ret);
+}
+
+/* ------------------------------------------------------------------ */
+/* callbacks (any context) */
+
+/* Note that this function must be re-entrant safe, as it can get called
+ from both the main thread as well as from the usb event handling thread */
+static void usbredir_write_flush_callback(void *user_data)
+{
+ SpiceUsbredirChannel *channel = SPICE_USBREDIR_CHANNEL(user_data);
+ SpiceUsbredirChannelPrivate *priv = channel->priv;
+
+ if (spice_channel_get_state(SPICE_CHANNEL(channel)) !=
+ SPICE_CHANNEL_STATE_READY)
+ return;
+
+ if (!priv->host)
+ return;
+
+ usbredirhost_write_guest_data(priv->host);
+}
+
+static void usbredir_log(void *user_data, int level, const char *msg)
+{
+ SpiceUsbredirChannel *channel = user_data;
+ SpiceUsbredirChannelPrivate *priv = channel->priv;
+
+ if (priv->catch_error && level == usbredirparser_error) {
+ CHANNEL_DEBUG(channel, "%s", msg);
+ /* Remove "usbredirhost: " prefix from usbredirhost messages */
+ if (strncmp(msg, "usbredirhost: ", 14) == 0)
+ g_set_error_literal(priv->catch_error, SPICE_CLIENT_ERROR,
+ SPICE_CLIENT_ERROR_FAILED, msg + 14);
+ else
+ g_set_error_literal(priv->catch_error, SPICE_CLIENT_ERROR,
+ SPICE_CLIENT_ERROR_FAILED, msg);
+ return;
+ }
+
+ switch (level) {
+ case usbredirparser_error:
+ g_critical("%s", msg); break;
+ case usbredirparser_warning:
+ g_warning("%s", msg); break;
+ default:
+ CHANNEL_DEBUG(channel, "%s", msg); break;
+ }
+}
+
+static int usbredir_read_callback(void *user_data, uint8_t *data, int count)
+{
+ SpiceUsbredirChannel *channel = user_data;
+ SpiceUsbredirChannelPrivate *priv = channel->priv;
+
+ if (priv->read_buf_size < count) {
+ count = priv->read_buf_size;
+ }
+
+ memcpy(data, priv->read_buf, count);
+
+ priv->read_buf_size -= count;
+ if (priv->read_buf_size) {
+ priv->read_buf += count;
+ } else {
+ priv->read_buf = NULL;
+ }
+
+ return count;
+}
+
+static void usbredir_free_write_cb_data(uint8_t *data, void *user_data)
+{
+ SpiceUsbredirChannel *channel = user_data;
+ SpiceUsbredirChannelPrivate *priv = channel->priv;
+
+ usbredirhost_free_write_buffer(priv->host, data);
+}
+
+static int usbredir_write_callback(void *user_data, uint8_t *data, int count)
+{
+ SpiceUsbredirChannel *channel = user_data;
+ SpiceMsgOut *msg_out;
+
+ msg_out = spice_msg_out_new(SPICE_CHANNEL(channel),
+ SPICE_MSGC_SPICEVMC_DATA);
+ spice_marshaller_add_ref_full(msg_out->marshaller, data, count,
+ usbredir_free_write_cb_data, channel);
+ spice_msg_out_send(msg_out);
+
+ return count;
+}
+
+static void *usbredir_alloc_lock(void) {
+#if GLIB_CHECK_VERSION(2,32,0)
+ GMutex *mutex;
+
+ mutex = g_new0(GMutex, 1);
+ g_mutex_init(mutex);
+
+ return mutex;
+#else
+ return g_mutex_new();
+#endif
+}
+
+static void usbredir_lock_lock(void *user_data) {
+ GMutex *mutex = user_data;
+
+ g_mutex_lock(mutex);
+}
+
+static void usbredir_unlock_lock(void *user_data) {
+ GMutex *mutex = user_data;
+
+ g_mutex_unlock(mutex);
+}
+
+static void usbredir_free_lock(void *user_data) {
+ GMutex *mutex = user_data;
+
+#if GLIB_CHECK_VERSION(2,32,0)
+ g_mutex_clear(mutex);
+ g_free(mutex);
+#else
+ g_mutex_free(mutex);
+#endif
+}
+
+/* --------------------------------------------------------------------- */
+
+typedef struct device_error_data {
+ SpiceUsbredirChannel *channel;
+ SpiceUsbDevice *spice_device;
+ GError *error;
+ struct coroutine *caller;
+} device_error_data;
+
+/* main context */
+static gboolean device_error(gpointer user_data)
+{
+ device_error_data *data = user_data;
+ SpiceUsbredirChannel *channel = data->channel;
+ SpiceUsbredirChannelPrivate *priv = channel->priv;
+
+ /* Check that the device has not changed before we manage to run */
+ if (data->spice_device == priv->spice_device) {
+ spice_usbredir_channel_disconnect_device(channel);
+ spice_usb_device_manager_device_error(
+ spice_usb_device_manager_get(
+ spice_channel_get_session(SPICE_CHANNEL(channel)), NULL),
+ data->spice_device, data->error);
+ }
+
+ coroutine_yieldto(data->caller, NULL);
+ return FALSE;
+}
+
+/* --------------------------------------------------------------------- */
+/* coroutine context */
+static void spice_usbredir_channel_up(SpiceChannel *c)
+{
+ SpiceUsbredirChannel *channel = SPICE_USBREDIR_CHANNEL(c);
+ SpiceUsbredirChannelPrivate *priv = channel->priv;
+
+ /* Flush any pending writes */
+ usbredirhost_write_guest_data(priv->host);
+}
+
+static void usbredir_handle_msg(SpiceChannel *c, SpiceMsgIn *in)
+{
+ SpiceUsbredirChannel *channel = SPICE_USBREDIR_CHANNEL(c);
+ SpiceUsbredirChannelPrivate *priv = channel->priv;
+ device_error_data data;
+ int r, size;
+ uint8_t *buf;
+
+ g_return_if_fail(priv->host != NULL);
+
+ /* No recursion allowed! */
+ g_return_if_fail(priv->read_buf == NULL);
+
+ buf = spice_msg_in_raw(in, &size);
+ priv->read_buf = buf;
+ priv->read_buf_size = size;
+
+ r = usbredirhost_read_guest_data(priv->host);
+ if (r != 0) {
+ SpiceUsbDevice *spice_device = priv->spice_device;
+ gchar *desc;
+ GError *err;
+
+ g_return_if_fail(spice_device != NULL);
+
+ desc = spice_usb_device_get_description(spice_device, NULL);
+ switch (r) {
+ case usbredirhost_read_parse_error:
+ err = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ _("usbredir protocol parse error for %s"), desc);
+ break;
+ case usbredirhost_read_device_rejected:
+ err = g_error_new(SPICE_CLIENT_ERROR,
+ SPICE_CLIENT_ERROR_USB_DEVICE_REJECTED,
+ _("%s rejected by host"), desc);
+ break;
+ case usbredirhost_read_device_lost:
+ err = g_error_new(SPICE_CLIENT_ERROR,
+ SPICE_CLIENT_ERROR_USB_DEVICE_LOST,
+ _("%s disconnected (fatal IO error)"), desc);
+ break;
+ default:
+ err = g_error_new(SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ _("Unknown error (%d) for %s"), r, desc);
+ }
+ g_free(desc);
+
+ CHANNEL_DEBUG(c, "%s", err->message);
+
+ data.channel = channel;
+ data.caller = coroutine_self();
+ data.spice_device = g_boxed_copy(spice_usb_device_get_type(), spice_device);
+ data.error = err;
+ g_idle_add(device_error, &data);
+ coroutine_yield(NULL);
+
+ g_boxed_free(spice_usb_device_get_type(), data.spice_device);
+
+ g_error_free(err);
+ }
+}
+
+#endif /* USE_USBREDIR */
diff --git a/src/channel-usbredir.h b/src/channel-usbredir.h
new file mode 100644
index 0000000..0cc4fbf
--- /dev/null
+++ b/src/channel-usbredir.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2011 Red Hat, Inc.
+
+ Red Hat Authors:
+ Hans de Goede <hdegoede@redhat.com>
+
+ 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_CLIENT_USBREDIR_CHANNEL_H__
+#define __SPICE_CLIENT_USBREDIR_CHANNEL_H__
+
+#include "spice-client.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_USBREDIR_CHANNEL (spice_usbredir_channel_get_type())
+#define SPICE_USBREDIR_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_USBREDIR_CHANNEL, SpiceUsbredirChannel))
+#define SPICE_USBREDIR_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_USBREDIR_CHANNEL, SpiceUsbredirChannelClass))
+#define SPICE_IS_USBREDIR_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_USBREDIR_CHANNEL))
+#define SPICE_IS_USBREDIR_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_USBREDIR_CHANNEL))
+#define SPICE_USBREDIR_CHANNEL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_USBREDIR_CHANNEL, SpiceUsbredirChannelClass))
+
+typedef struct _SpiceUsbredirChannel SpiceUsbredirChannel;
+typedef struct _SpiceUsbredirChannelClass SpiceUsbredirChannelClass;
+typedef struct _SpiceUsbredirChannelPrivate SpiceUsbredirChannelPrivate;
+
+/**
+ * SpiceUsbredirChannel:
+ *
+ * The #SpiceUsbredirChannel struct is opaque and should not be accessed directly.
+ */
+struct _SpiceUsbredirChannel {
+ SpiceChannel parent;
+
+ /*< private >*/
+ SpiceUsbredirChannelPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+/**
+ * SpiceUsbredirChannelClass:
+ * @parent_class: Parent class.
+ *
+ * Class structure for #SpiceUsbredirChannel.
+ */
+struct _SpiceUsbredirChannelClass {
+ SpiceChannelClass parent_class;
+
+ /* signals */
+
+ /*< private >*/
+ /* Do not add fields to this struct */
+};
+
+GType spice_usbredir_channel_get_type(void);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_USBREDIR_CHANNEL_H__ */
diff --git a/src/channel-webdav.c b/src/channel-webdav.c
new file mode 100644
index 0000000..bde728e
--- /dev/null
+++ b/src/channel-webdav.c
@@ -0,0 +1,613 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2013 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-channel-priv.h"
+#include "spice-session-priv.h"
+#include "spice-marshal.h"
+#include "glib-compat.h"
+#include "vmcstream.h"
+#include "giopipe.h"
+
+/**
+ * SECTION:channel-webdav
+ * @short_description: exports a directory
+ * @title: WebDAV Channel
+ * @section_id:
+ * @see_also: #SpiceChannel
+ * @stability: Stable
+ * @include: channel-webdav.h
+ *
+ * The "webdav" channel exports a directory to the guest for file
+ * manipulation (read/write/copy etc). The underlying protocol is
+ * implemented using WebDAV (RFC 4918).
+ *
+ * By default, the shared directory is the one associated with GLib
+ * %G_USER_DIRECTORY_PUBLIC_SHARE. You can specify a different
+ * directory with #SpiceSession #SpiceSession:shared-dir property.
+ *
+ * Since: 0.24
+ */
+
+#define SPICE_WEBDAV_CHANNEL_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_WEBDAV_CHANNEL, SpiceWebdavChannelPrivate))
+
+typedef struct _OutputQueue OutputQueue;
+
+struct _SpiceWebdavChannelPrivate {
+ SpiceVmcStream *stream;
+ GCancellable *cancellable;
+ GHashTable *clients;
+ OutputQueue *queue;
+
+ gboolean demuxing;
+ struct _demux {
+ gint64 client;
+ guint16 size;
+ guint8 *buf;
+ } demux;
+};
+
+G_DEFINE_TYPE(SpiceWebdavChannel, spice_webdav_channel, SPICE_TYPE_PORT_CHANNEL)
+
+static void spice_webdav_handle_msg(SpiceChannel *channel, SpiceMsgIn *msg);
+
+struct _OutputQueue {
+ GOutputStream *output;
+ gboolean flushing;
+ guint idle_id;
+ GQueue *queue;
+};
+
+typedef struct _OutputQueueElem {
+ OutputQueue *queue;
+ const guint8 *buf;
+ gsize size;
+ GFunc pushed_cb;
+ gpointer user_data;
+} OutputQueueElem;
+
+static OutputQueue* output_queue_new(GOutputStream *output)
+{
+ OutputQueue *queue = g_new0(OutputQueue, 1);
+
+ queue->output = g_object_ref(output);
+ queue->queue = g_queue_new();
+
+ return queue;
+}
+
+static void output_queue_free(OutputQueue *queue)
+{
+ g_warn_if_fail(g_queue_get_length(queue->queue) == 0);
+ g_warn_if_fail(!queue->flushing);
+
+ g_queue_free_full(queue->queue, g_free);
+ g_clear_object(&queue->output);
+ if (queue->idle_id)
+ g_source_remove(queue->idle_id);
+ g_free(queue);
+}
+
+static gboolean output_queue_idle(gpointer user_data);
+
+static void output_queue_flush_cb(GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GError *error = NULL;
+ OutputQueueElem *e = user_data;
+ OutputQueue *q = e->queue;
+
+ q->flushing = FALSE;
+ g_output_stream_flush_finish(G_OUTPUT_STREAM(source_object),
+ res, &error);
+ if (error)
+ g_warning("error: %s", error->message);
+
+ g_clear_error(&error);
+
+ if (!q->idle_id)
+ q->idle_id = g_idle_add(output_queue_idle, q);
+
+ g_free(e);
+}
+
+static gboolean output_queue_idle(gpointer user_data)
+{
+ OutputQueue *q = user_data;
+ OutputQueueElem *e;
+ GError *error = NULL;
+
+ if (q->flushing) {
+ q->idle_id = 0;
+ return FALSE;
+ }
+
+ e = g_queue_pop_head(q->queue);
+ if (!e) {
+ q->idle_id = 0;
+ return FALSE;
+ }
+
+ if (!g_output_stream_write_all(q->output, e->buf, e->size, NULL, NULL, &error))
+ goto err;
+ else if (e->pushed_cb)
+ e->pushed_cb(q, e->user_data);
+
+ q->flushing = TRUE;
+ g_output_stream_flush_async(q->output, G_PRIORITY_DEFAULT, NULL, output_queue_flush_cb, e);
+
+ return TRUE;
+
+err:
+ g_warning("failed to write to output stream");
+ if (error)
+ g_warning("error: %s", error->message);
+ g_clear_error(&error);
+
+ q->idle_id = 0;
+ return FALSE;
+}
+
+static void output_queue_push(OutputQueue *q, const guint8 *buf, gsize size,
+ GFunc pushed_cb, gpointer user_data)
+{
+ OutputQueueElem *e = g_new(OutputQueueElem, 1);
+
+ e->buf = buf;
+ e->size = size;
+ e->pushed_cb = pushed_cb;
+ e->user_data = user_data;
+ e->queue = q;
+ g_queue_push_tail(q->queue, e);
+
+ if (!q->idle_id && !q->flushing)
+ q->idle_id = g_idle_add(output_queue_idle, q);
+}
+
+typedef struct Client
+{
+ guint refs;
+ SpiceWebdavChannel *self;
+ GIOStream *pipe;
+ gint64 id;
+ GCancellable *cancellable;
+
+ struct _mux {
+ gint64 id;
+ guint16 size;
+ guint8 *buf;
+ } mux;
+} Client;
+
+static void
+client_unref(Client *client)
+{
+ if (--client->refs > 0)
+ return;
+
+ g_free(client->mux.buf);
+
+ g_object_unref(client->pipe);
+ g_object_unref(client->cancellable);
+
+ g_free(client);
+}
+
+static Client *
+client_ref(Client *client)
+{
+ client->refs++;
+ return client;
+}
+
+static void client_start_read(SpiceWebdavChannel *self, Client *client);
+
+static void remove_client(SpiceWebdavChannel *self, Client *client)
+{
+ SpiceWebdavChannelPrivate *c;
+
+ if (g_cancellable_is_cancelled(client->cancellable))
+ return;
+
+ g_cancellable_cancel(client->cancellable);
+
+ c = self->priv;
+ g_hash_table_remove(c->clients, &client->id);
+}
+
+static void mux_pushed_cb(OutputQueue *q, gpointer user_data)
+{
+ Client *client = user_data;
+
+ if (client->mux.size == 0) {
+ remove_client(client->self, client);
+ } else {
+ client_start_read(client->self, client);
+ }
+
+ client_unref(client);
+}
+
+#define MAX_MUX_SIZE G_MAXUINT16
+
+static void server_reply_cb(GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ Client *client = user_data;
+ SpiceWebdavChannel *self = client->self;
+ SpiceWebdavChannelPrivate *c = self->priv;
+ GError *err = NULL;
+ gssize size;
+
+ size = g_input_stream_read_finish(G_INPUT_STREAM(source_object), res, &err);
+ if (err || g_cancellable_is_cancelled(client->cancellable))
+ goto end;
+
+ g_return_if_fail(size <= MAX_MUX_SIZE);
+ g_return_if_fail(size >= 0);
+ client->mux.size = size;
+
+ output_queue_push(c->queue, (guint8 *)&client->mux.id, sizeof(gint64), NULL, NULL);
+ client->mux.size = GUINT16_TO_LE(client->mux.size);
+ output_queue_push(c->queue, (guint8 *)&client->mux.size, sizeof(guint16), NULL, NULL);
+ output_queue_push(c->queue, (guint8 *)client->mux.buf, size, (GFunc)mux_pushed_cb, client);
+
+ return;
+
+end:
+ if (err) {
+ if (!g_cancellable_is_cancelled(client->cancellable))
+ g_warning("read error: %s", err->message);
+ remove_client(self, client);
+ g_clear_error(&err);
+ }
+
+ client_unref(client);
+}
+
+static void client_start_read(SpiceWebdavChannel *self, Client *client)
+{
+ GInputStream *input;
+
+ input = g_io_stream_get_input_stream(G_IO_STREAM(client->pipe));
+ g_input_stream_read_async(input, client->mux.buf, MAX_MUX_SIZE,
+ G_PRIORITY_DEFAULT, client->cancellable, server_reply_cb,
+ client_ref(client));
+}
+
+static void start_demux(SpiceWebdavChannel *self);
+
+static void demux_to_client_finish(SpiceWebdavChannel *self,
+ Client *client, gboolean fail)
+{
+ SpiceWebdavChannelPrivate *c = self->priv;
+
+ if (fail) {
+ remove_client(self, client);
+ }
+
+ c->demuxing = FALSE;
+ start_demux(self);
+}
+
+static void demux_to_client_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+ Client *client = user_data;
+ SpiceWebdavChannelPrivate *c = client->self->priv;
+ GError *error = NULL;
+ gboolean fail;
+ gsize size;
+
+ g_output_stream_write_all_finish(G_OUTPUT_STREAM(source), result, &size, &error);
+
+ if (error) {
+ CHANNEL_DEBUG(client->self, "write failed: %s", error->message);
+ g_clear_error(&error);
+ }
+
+ fail = (size != c->demux.size);
+ g_warn_if_fail(size == c->demux.size);
+ demux_to_client_finish(client->self, client, fail);
+}
+
+static void demux_to_client(SpiceWebdavChannel *self,
+ Client *client)
+{
+ SpiceWebdavChannelPrivate *c = self->priv;
+ gsize size = c->demux.size;
+
+ CHANNEL_DEBUG(self, "pushing %"G_GSIZE_FORMAT" to client %p", size, client);
+
+ if (size > 0) {
+ g_output_stream_write_all_async(g_io_stream_get_output_stream(client->pipe),
+ c->demux.buf, size, G_PRIORITY_DEFAULT,
+ c->cancellable, demux_to_client_cb, client);
+ return;
+ } else {
+ /* Nothing to write */
+ demux_to_client_finish(self, client, FALSE);
+ }
+}
+
+static void start_client(SpiceWebdavChannel *self)
+{
+#ifdef USE_PHODAV
+ SpiceWebdavChannelPrivate *c = self->priv;
+ Client *client;
+ GIOStream *peer = NULL;
+ SpiceSession *session;
+ SoupServer *server;
+ GSocketAddress *addr;
+ GError *error = NULL;
+
+ session = spice_channel_get_session(SPICE_CHANNEL(self));
+ server = phodav_server_get_soup_server(spice_session_get_webdav_server(session));
+
+ CHANNEL_DEBUG(self, "starting client %" G_GINT64_FORMAT, c->demux.client);
+
+ client = g_new0(Client, 1);
+ client->refs = 1;
+ client->id = c->demux.client;
+ client->self = self;
+ client->mux.id = GINT64_TO_LE(client->id);
+ client->mux.buf = g_malloc0(MAX_MUX_SIZE);
+ client->cancellable = g_cancellable_new();
+ spice_make_pipe(&client->pipe, &peer);
+
+ addr = g_inet_socket_address_new_from_string ("127.0.0.1", 0);
+ if (!soup_server_accept_iostream(server, peer, addr, addr, &error))
+ goto fail;
+
+ g_hash_table_insert(c->clients, &client->id, client);
+
+ client_start_read(self, client);
+ demux_to_client(self, client);
+
+ g_clear_object(&addr);
+ return;
+
+fail:
+ if (error)
+ CHANNEL_DEBUG(self, "failed to start client: %s", error->message);
+
+ g_clear_object(&addr);
+ g_clear_object(&peer);
+ g_clear_error(&error);
+ client_unref(client);
+#endif
+}
+
+static void data_read_cb(GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SpiceWebdavChannel *self = user_data;
+ SpiceWebdavChannelPrivate *c;
+ Client *client;
+ GError *error = NULL;
+ gssize size;
+
+ size = spice_vmc_input_stream_read_all_finish(G_INPUT_STREAM(source_object), res, &error);
+ if (error) {
+ g_warning("error: %s", error->message);
+ g_clear_error(&error);
+ return;
+ }
+
+ c = self->priv;
+ g_return_if_fail(size == c->demux.size);
+
+ client = g_hash_table_lookup(c->clients, &c->demux.client);
+
+ if (client)
+ demux_to_client(self, client);
+ else
+ start_client(self);
+}
+
+
+static void size_read_cb(GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SpiceWebdavChannel *self = user_data;
+ SpiceWebdavChannelPrivate *c;
+ GInputStream *istream = G_INPUT_STREAM(source_object);
+ GError *error = NULL;
+ gssize size;
+
+ size = spice_vmc_input_stream_read_all_finish(G_INPUT_STREAM(source_object), res, &error);
+ if (error || size != sizeof(guint16))
+ goto end;
+
+ c = self->priv;
+ c->demux.size = GUINT16_FROM_LE(c->demux.size);
+ spice_vmc_input_stream_read_all_async(istream,
+ c->demux.buf, c->demux.size,
+ G_PRIORITY_DEFAULT, c->cancellable, data_read_cb, self);
+ return;
+
+end:
+ if (error) {
+ g_warning("error: %s", error->message);
+ g_clear_error(&error);
+ }
+}
+
+static void client_read_cb(GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SpiceWebdavChannel *self = user_data;
+ SpiceWebdavChannelPrivate *c = self->priv;
+ GInputStream *istream = G_INPUT_STREAM(source_object);
+ GError *error = NULL;
+ gssize size;
+
+ size = spice_vmc_input_stream_read_all_finish(G_INPUT_STREAM(source_object), res, &error);
+ if (error || size != sizeof(gint64))
+ goto end;
+
+ c->demux.client = GINT64_FROM_LE(c->demux.client);
+ spice_vmc_input_stream_read_all_async(istream,
+ &c->demux.size, sizeof(guint16),
+ G_PRIORITY_DEFAULT, c->cancellable, size_read_cb, self);
+ return;
+
+end:
+ if (error) {
+ g_warning("error: %s", error->message);
+ g_clear_error(&error);
+ }
+}
+
+static void start_demux(SpiceWebdavChannel *self)
+{
+ SpiceWebdavChannelPrivate *c = self->priv;
+ GInputStream *istream = g_io_stream_get_input_stream(G_IO_STREAM(c->stream));
+
+ if (c->demuxing)
+ return;
+
+ c->demuxing = TRUE;
+
+ CHANNEL_DEBUG(self, "start demux");
+ spice_vmc_input_stream_read_all_async(istream, &c->demux.client, sizeof(gint64),
+ G_PRIORITY_DEFAULT, c->cancellable, client_read_cb, self);
+
+}
+
+static void port_event(SpiceWebdavChannel *self, gint event)
+{
+ SpiceWebdavChannelPrivate *c = self->priv;
+
+ CHANNEL_DEBUG(self, "port event:%d", event);
+ if (event == SPICE_PORT_EVENT_OPENED) {
+ g_cancellable_reset(c->cancellable);
+ start_demux(self);
+ } else {
+ g_cancellable_cancel(c->cancellable);
+ c->demuxing = FALSE;
+ g_hash_table_remove_all(c->clients);
+ }
+}
+
+static void client_remove_unref(gpointer data)
+{
+ Client *client = data;
+
+ g_cancellable_cancel(client->cancellable);
+ client_unref(client);
+}
+
+static void spice_webdav_channel_init(SpiceWebdavChannel *channel)
+{
+ SpiceWebdavChannelPrivate *c = SPICE_WEBDAV_CHANNEL_GET_PRIVATE(channel);
+
+ channel->priv = c;
+ c->stream = spice_vmc_stream_new(SPICE_CHANNEL(channel));
+ c->cancellable = g_cancellable_new();
+ c->clients = g_hash_table_new_full(g_int64_hash, g_int64_equal,
+ NULL, client_remove_unref);
+ c->demux.buf = g_malloc0(MAX_MUX_SIZE);
+
+ GOutputStream *ostream = g_io_stream_get_output_stream(G_IO_STREAM(c->stream));
+ c->queue = output_queue_new(ostream);
+}
+
+static void spice_webdav_channel_finalize(GObject *object)
+{
+ SpiceWebdavChannelPrivate *c = SPICE_WEBDAV_CHANNEL(object)->priv;
+
+ g_free(c->demux.buf);
+
+ G_OBJECT_CLASS(spice_webdav_channel_parent_class)->finalize(object);
+}
+
+static void spice_webdav_channel_dispose(GObject *object)
+{
+ SpiceWebdavChannelPrivate *c = SPICE_WEBDAV_CHANNEL(object)->priv;
+
+ g_cancellable_cancel(c->cancellable);
+ g_clear_object(&c->cancellable);
+ g_clear_pointer(&c->queue, output_queue_free);
+ g_clear_object(&c->stream);
+ g_hash_table_unref(c->clients);
+
+ G_OBJECT_CLASS(spice_webdav_channel_parent_class)->dispose(object);
+}
+
+static void spice_webdav_channel_up(SpiceChannel *channel)
+{
+ CHANNEL_DEBUG(channel, "up");
+}
+
+static void spice_webdav_channel_class_init(SpiceWebdavChannelClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS(klass);
+
+ gobject_class->dispose = spice_webdav_channel_dispose;
+ gobject_class->finalize = spice_webdav_channel_finalize;
+ channel_class->handle_msg = spice_webdav_handle_msg;
+ channel_class->channel_up = spice_webdav_channel_up;
+
+ g_signal_override_class_handler("port-event",
+ SPICE_TYPE_WEBDAV_CHANNEL,
+ G_CALLBACK(port_event));
+
+ g_type_class_add_private(klass, sizeof(SpiceWebdavChannelPrivate));
+}
+
+/* coroutine context */
+static void webdav_handle_msg(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceWebdavChannel *self = SPICE_WEBDAV_CHANNEL(channel);
+ SpiceWebdavChannelPrivate *c = self->priv;
+ int size;
+ uint8_t *buf;
+
+ buf = spice_msg_in_raw(in, &size);
+ CHANNEL_DEBUG(channel, "len:%d buf:%p", size, buf);
+
+ spice_vmc_input_stream_co_data(
+ SPICE_VMC_INPUT_STREAM(g_io_stream_get_input_stream(G_IO_STREAM(c->stream))),
+ buf, size);
+}
+
+
+/* coroutine context */
+static void spice_webdav_handle_msg(SpiceChannel *channel, SpiceMsgIn *msg)
+{
+ int type = spice_msg_in_type(msg);
+ SpiceChannelClass *parent_class;
+
+ parent_class = SPICE_CHANNEL_CLASS(spice_webdav_channel_parent_class);
+
+ if (type == SPICE_MSG_SPICEVMC_DATA)
+ webdav_handle_msg(channel, msg);
+ else if (parent_class->handle_msg)
+ parent_class->handle_msg(channel, msg);
+ else
+ g_return_if_reached();
+}
diff --git a/src/channel-webdav.h b/src/channel-webdav.h
new file mode 100644
index 0000000..7940706
--- /dev/null
+++ b/src/channel-webdav.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2013 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_WEBDAV_CHANNEL_H__
+#define __SPICE_WEBDAV_CHANNEL_H__
+
+#include <gio/gio.h>
+#include "spice-client.h"
+#include "channel-port.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_WEBDAV_CHANNEL (spice_webdav_channel_get_type())
+#define SPICE_WEBDAV_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_WEBDAV_CHANNEL, SpiceWebdavChannel))
+#define SPICE_WEBDAV_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_WEBDAV_CHANNEL, SpiceWebdavChannelClass))
+#define SPICE_IS_WEBDAV_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_WEBDAV_CHANNEL))
+#define SPICE_IS_WEBDAV_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_WEBDAV_CHANNEL))
+#define SPICE_WEBDAV_CHANNEL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_WEBDAV_CHANNEL, SpiceWebdavChannelClass))
+
+typedef struct _SpiceWebdavChannel SpiceWebdavChannel;
+typedef struct _SpiceWebdavChannelClass SpiceWebdavChannelClass;
+typedef struct _SpiceWebdavChannelPrivate SpiceWebdavChannelPrivate;
+
+/**
+ * SpiceWebdavChannel:
+ *
+ * The #SpiceWebdavChannel struct is opaque and should not be accessed directly.
+ */
+struct _SpiceWebdavChannel {
+ SpicePortChannel parent;
+
+ /*< private >*/
+ SpiceWebdavChannelPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+/**
+ * SpiceWebdavChannelClass:
+ * @parent_class: Parent class.
+ *
+ * Class structure for #SpiceWebdavChannel.
+ */
+struct _SpiceWebdavChannelClass {
+ SpicePortChannelClass parent_class;
+
+ /*< private >*/
+ /* Do not add fields to this struct */
+};
+
+GType spice_webdav_channel_get_type(void);
+
+G_END_DECLS
+
+#endif /* __SPICE_WEBDAV_CHANNEL_H__ */
diff --git a/src/client_sw_canvas.c b/src/client_sw_canvas.c
new file mode 100644
index 0000000..a69abe0
--- /dev/null
+++ b/src/client_sw_canvas.c
@@ -0,0 +1,20 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2014 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/>.
+*/
+#define SW_CANVAS_CACHE
+
+#include "common/sw_canvas.c"
diff --git a/src/client_sw_canvas.h b/src/client_sw_canvas.h
new file mode 100644
index 0000000..1180c5b
--- /dev/null
+++ b/src/client_sw_canvas.h
@@ -0,0 +1,25 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2014 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_CLIENT_SW_CANVAS_H__
+#define __SPICE_CLIENT_SW_CANVAS_H__
+
+#define SW_CANVAS_CACHE
+
+#include <common/sw_canvas.h>
+
+#endif /* __SPICE_CLIENT_SW_CANVAS_H__ */
diff --git a/src/continuation.c b/src/continuation.c
new file mode 100644
index 0000000..adce858
--- /dev/null
+++ b/src/continuation.c
@@ -0,0 +1,102 @@
+/*
+ * GTK VNC Widget
+ *
+ * Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws>
+ *
+ * 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.0 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+#include "config.h"
+
+/* keep this above system headers, but below config.h */
+#ifdef _FORTIFY_SOURCE
+#undef _FORTIFY_SOURCE
+#endif
+
+#include <errno.h>
+#include <glib.h>
+
+#include "continuation.h"
+
+/*
+ * va_args to makecontext() must be type 'int', so passing
+ * the pointer we need may require several int args. This
+ * union is a quick hack to let us do that
+ */
+union cc_arg {
+ void *p;
+ int i[2];
+};
+
+static void continuation_trampoline(int i0, int i1)
+{
+ union cc_arg arg;
+ struct continuation *cc;
+ arg.i[0] = i0;
+ arg.i[1] = i1;
+ cc = arg.p;
+
+ if (_setjmp(cc->jmp) == 0) {
+ ucontext_t tmp;
+ swapcontext(&tmp, &cc->last);
+ }
+
+ cc->entry(cc);
+}
+
+void cc_init(struct continuation *cc)
+{
+ volatile union cc_arg arg;
+ arg.p = cc;
+ if (getcontext(&cc->uc) == -1)
+ g_error("getcontext() failed: %s", g_strerror(errno));
+ cc->uc.uc_link = &cc->last;
+ cc->uc.uc_stack.ss_sp = cc->stack;
+ cc->uc.uc_stack.ss_size = cc->stack_size;
+ cc->uc.uc_stack.ss_flags = 0;
+
+ makecontext(&cc->uc, (void *)continuation_trampoline, 2, arg.i[0], arg.i[1]);
+ swapcontext(&cc->last, &cc->uc);
+}
+
+int cc_release(struct continuation *cc)
+{
+ if (cc->release)
+ return cc->release(cc);
+
+ return 0;
+}
+
+int cc_swap(struct continuation *from, struct continuation *to)
+{
+ to->exited = 0;
+ if (getcontext(&to->last) == -1)
+ return -1;
+ else if (to->exited == 0)
+ to->exited = 1; // so when coroutine finishes
+ else if (to->exited == 1)
+ return 1; // it ends up here
+
+ if (_setjmp(from->jmp) == 0)
+ _longjmp(to->jmp, 1);
+
+ return 0;
+}
+/*
+ * Local variables:
+ * c-indent-level: 8
+ * c-basic-offset: 8
+ * tab-width: 8
+ * End:
+ */
diff --git a/src/continuation.h b/src/continuation.h
new file mode 100644
index 0000000..675a257
--- /dev/null
+++ b/src/continuation.h
@@ -0,0 +1,61 @@
+/*
+ * GTK VNC Widget
+ *
+ * Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws>
+ *
+ * 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.0 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef _CONTINUATION_H_
+#define _CONTINUATION_H_
+
+#include <stddef.h>
+#include <ucontext.h>
+#include <setjmp.h>
+
+struct continuation
+{
+ char *stack;
+ size_t stack_size;
+ void (*entry)(struct continuation *cc);
+ int (*release)(struct continuation *cc);
+
+ /* private */
+ ucontext_t uc;
+ ucontext_t last;
+ int exited;
+ jmp_buf jmp;
+};
+
+void cc_init(struct continuation *cc);
+
+int cc_release(struct continuation *cc);
+
+/* you can use an uninitialized struct continuation for from if you do not have
+ the current continuation handy. */
+int cc_swap(struct continuation *from, struct continuation *to);
+
+#define offset_of(type, member) ((unsigned long)(&((type *)0)->member))
+#define container_of(obj, type, member) \
+ (type *)(((char *)obj) - offset_of(type, member))
+
+#endif
+/*
+ * Local variables:
+ * c-indent-level: 8
+ * c-basic-offset: 8
+ * tab-width: 8
+ * End:
+ */
diff --git a/src/controller/Makefile.am b/src/controller/Makefile.am
new file mode 100644
index 0000000..00552e8
--- /dev/null
+++ b/src/controller/Makefile.am
@@ -0,0 +1,100 @@
+NULL =
+
+AM_CPPFLAGS = \
+ -DG_LOG_DOMAIN=\"GSpiceController\" \
+ $(GIO_CFLAGS) \
+ $(COMMON_CFLAGS) \
+ $(NULL)
+
+# http://www.gnu.org/software/libtool/manual/html_node/Updating-version-info.html
+AM_LDFLAGS = \
+ -no-undefined \
+ $(GIO_LIBS) \
+ $(NULL)
+
+AM_VALAFLAGS = \
+ --pkg gio-2.0 \
+ --pkg spice-protocol --vapidir=$(top_srcdir)/data \
+ --pkg custom --vapidir=$(srcdir) \
+ -C \
+ $(NULL)
+
+lib_LTLIBRARIES = libspice-controller.la
+noinst_PROGRAMS = test-controller spice-controller-dump
+
+libspice_controller_la_VALASOURCES = \
+ menu.vala \
+ controller.vala \
+ foreign-menu.vala \
+ util.vala \
+ $(NULL)
+
+libspice_controller_la_BUILT_SOURCES = \
+ $(libspice_controller_la_VALASOURCES:.vala=.c) \
+ spice-controller.h \
+ $(NULL)
+
+BUILT_SOURCES = \
+ $(libspice_controller_la_BUILT_SOURCES) \
+ controller.vala.stamp \
+ $(NULL)
+
+libspice_controller_la_SOURCES = \
+ $(libspice_controller_la_BUILT_SOURCES) \
+ custom.h \
+ spice-controller-listener.c \
+ spice-controller-listener.h \
+ spice-foreign-menu-listener.c \
+ spice-foreign-menu-listener.h \
+ $(NULL)
+
+if OS_WIN32
+libspice_controller_la_SOURCES += \
+ namedpipe.c \
+ namedpipe.h \
+ namedpipeconnection.c \
+ namedpipeconnection.h \
+ namedpipelistener.c \
+ namedpipelistener.h \
+ win32-util.c \
+ win32-util.h \
+ $(NULL)
+endif
+libspice_controller_la_LDFLAGS = \
+ $(AM_LDFLAGS) \
+ -version-info 0:0:0 \
+ $(NULL)
+
+libspice_controllerincludedir = $(includedir)/spice-controller
+libspice_controllerinclude_HEADERS = \
+ spice-controller.h
+
+test_controller_SOURCES = test.c
+test_controller_LDADD = libspice-controller.la
+
+spice_controller_dump_SOURCES = dump.c
+spice_controller_dump_LDADD = libspice-controller.la
+
+controller.vala.stamp: $(libspice_controller_la_VALASOURCES) custom.vapi
+ @if test -z "$(VALAC)"; then \
+ echo "" ; \
+ echo " *** Error: missing valac!" ; \
+ echo " *** You must run autogen.sh or configure --enable-vala" ; \
+ echo "" ; \
+ exit 1 ; \
+ fi
+ $(VALA_V)$(VALAC) $(VALAFLAGS) $(AM_VALAFLAGS) \
+ $(addprefix $(srcdir)/,$(libspice_controller_la_VALASOURCES)) \
+ -H spice-controller.h
+ @touch $@
+
+$(libspice_controller_la_BUILT_SOURCES): controller.vala.stamp
+
+EXTRA_DIST = \
+ $(libspice_controller_la_VALASOURCES) \
+ controller.vala.stamp \
+ custom.vapi \
+ gio-windows-2.0.vapi \
+ $(NULL)
+
+-include $(top_srcdir)/git.mk
diff --git a/src/controller/controller.vala b/src/controller/controller.vala
new file mode 100644
index 0000000..84b4527
--- /dev/null
+++ b/src/controller/controller.vala
@@ -0,0 +1,286 @@
+// Copyright (C) 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/>.
+
+using GLib;
+using Custom;
+using Win32;
+using Spice;
+using SpiceProtocol;
+
+namespace SpiceCtrl {
+
+public errordomain Error {
+ VALUE,
+}
+
+public class Controller: Object {
+ public string host { private set; get; }
+ public uint32 port { private set; get; }
+ public uint32 sport { private set; get; }
+ public string password { private set; get; }
+ public SpiceProtocol.Controller.Display display_flags { private set; get; }
+ public string tls_ciphers { private set; get; }
+ public string host_subject { private set; get; }
+ public string ca_file { private set; get; }
+ public string title { private set; get; }
+ public string hotkeys { private set; get; }
+ public string[] secure_channels { private set; get; }
+ public string[] disable_channels { private set; get; }
+ public SpiceCtrl.Menu? menu { private set; get; }
+ public bool enable_smartcard { private set; get; }
+ public bool send_cad { private set; get; }
+ public string[] disable_effects {private set; get; }
+ public uint32 color_depth {private set; get; }
+ public bool enable_usbredir { private set; get; }
+ public bool enable_usb_autoshare { private set; get; }
+ public string usb_filter { private set; get; }
+ public string proxy { private set; get; }
+
+ public signal void do_connect ();
+ public signal void show ();
+ public signal void hide ();
+
+ public signal void client_connected ();
+
+ public void menu_item_click_msg (int32 item_id) {
+ var msg = SpiceProtocol.Controller.MsgValue ();
+ msg.base.size = (uint32)sizeof (SpiceProtocol.Controller.MsgValue);
+ msg.base.id = SpiceProtocol.Controller.MsgId.MENU_ITEM_CLICK;
+ msg.value = item_id;
+ unowned uint8[] p = ((uint8[])(&msg))[0:msg.base.size];
+ send_msg.begin (p);
+ }
+
+ public async bool send_msg (uint8[] p) throws GLib.Error {
+ // vala FIXME: pass Controller.Msg instead
+ // vala doesn't keep reference on the struct in async methods
+ // it copies only base, which is not enough to transmit the whole
+ // message.
+ try {
+ if (excl_connection != null) {
+ yield output_stream_write (excl_connection.output_stream, p);
+ } else {
+ foreach (var c in clients)
+ yield output_stream_write (c.output_stream, p);
+ }
+ } catch (GLib.Error e) {
+ warning (e.message);
+ }
+
+ return true;
+ }
+
+ private GLib.IOStream? excl_connection;
+ private int nclients;
+ List<IOStream> clients;
+
+ private bool handle_message (SpiceProtocol.Controller.Msg* msg) {
+ var v = (SpiceProtocol.Controller.MsgValue*)(msg);
+ var d = (SpiceProtocol.Controller.MsgData*)(msg);
+ unowned string str = (string)(&d.data);
+
+ switch (msg.id) {
+ case SpiceProtocol.Controller.MsgId.HOST:
+ host = str;
+ debug ("got HOST: %s".printf (str));
+ break;
+ case SpiceProtocol.Controller.MsgId.PORT:
+ port = v.value;
+ debug ("got PORT: %u".printf (port));
+ break;
+ case SpiceProtocol.Controller.MsgId.SPORT:
+ sport = v.value;
+ debug ("got SPORT: %u".printf (sport));
+ break;
+ case SpiceProtocol.Controller.MsgId.PASSWORD:
+ password = str;
+ debug ("got PASSWORD");
+ break;
+
+ case SpiceProtocol.Controller.MsgId.SECURE_CHANNELS:
+ secure_channels = str.split(",");
+ debug ("got SECURE_CHANNELS %s".printf (str));
+ break;
+
+ case SpiceProtocol.Controller.MsgId.DISABLE_CHANNELS:
+ disable_channels = str.split(",");
+ debug ("got DISABLE_CHANNELS %s".printf (str));
+ break;
+
+ case SpiceProtocol.Controller.MsgId.TLS_CIPHERS:
+ tls_ciphers = str;
+ debug ("got TLS_CIPHERS %s".printf (str));
+ break;
+ case SpiceProtocol.Controller.MsgId.CA_FILE:
+ ca_file = str;
+ debug ("got CA_FILE %s".printf (str));
+ break;
+ case SpiceProtocol.Controller.MsgId.HOST_SUBJECT:
+ host_subject = str;
+ debug ("got HOST_SUBJECT %s".printf (str));
+ break;
+
+ case SpiceProtocol.Controller.MsgId.FULL_SCREEN:
+ display_flags = (SpiceProtocol.Controller.Display)v.value;
+ debug ("got FULL_SCREEN 0x%x".printf (v.value));
+ break;
+ case SpiceProtocol.Controller.MsgId.SET_TITLE:
+ title = str;
+ debug ("got TITLE %s".printf (str));
+ break;
+ case SpiceProtocol.Controller.MsgId.ENABLE_SMARTCARD:
+ enable_smartcard = (bool)v.value;
+ debug ("got ENABLE_SMARTCARD 0x%x".printf (v.value));
+ break;
+
+ case SpiceProtocol.Controller.MsgId.CREATE_MENU:
+ menu = new SpiceCtrl.Menu.from_string (str);
+ debug ("got CREATE_MENU %s".printf (str));
+ break;
+ case SpiceProtocol.Controller.MsgId.DELETE_MENU:
+ menu = null;
+ debug ("got DELETE_MENU request");
+ break;
+
+ case SpiceProtocol.Controller.MsgId.SEND_CAD:
+ send_cad = (bool)v.value;
+ debug ("got SEND_CAD %u".printf (v.value));
+ break;
+
+ case SpiceProtocol.Controller.MsgId.HOTKEYS:
+ hotkeys = str;
+ debug ("got HOTKEYS %s".printf (str));
+ break;
+
+ case SpiceProtocol.Controller.MsgId.COLOR_DEPTH:
+ color_depth = v.value;
+ debug ("got COLOR_DEPTH %u".printf (v.value));
+ break;
+ case SpiceProtocol.Controller.MsgId.DISABLE_EFFECTS:
+ disable_effects = str.split(",");
+ debug ("got DISABLE_EFFECTS %s".printf (str));
+ break;
+
+ case SpiceProtocol.Controller.MsgId.CONNECT:
+ do_connect ();
+ debug ("got CONNECT request");
+ break;
+ case SpiceProtocol.Controller.MsgId.SHOW:
+ show ();
+ debug ("got SHOW request");
+ break;
+ case SpiceProtocol.Controller.MsgId.HIDE:
+ hide ();
+ debug ("got HIDE request");
+ break;
+ case SpiceProtocol.Controller.MsgId.ENABLE_USB:
+ enable_usbredir = (bool)v.value;
+ debug ("got ENABLE_USB %u".printf (v.value));
+ break;
+ case SpiceProtocol.Controller.MsgId.ENABLE_USB_AUTOSHARE:
+ enable_usb_autoshare = (bool)v.value;
+ debug ("got ENABLE_USB_AUTOSHARE %u".printf (v.value));
+ break;
+ case SpiceProtocol.Controller.MsgId.USB_FILTER:
+ usb_filter = str;
+ debug ("got USB_FILTER %s".printf (str));
+ break;
+ case SpiceProtocol.Controller.MsgId.PROXY:
+ proxy = str;
+ debug ("got PROXY %s".printf (str));
+ break;
+ default:
+ debug ("got unknown msg.id %u".printf (msg.id));
+ warn_if_reached ();
+ return false;
+ }
+ return true;
+ }
+
+ private async void handle_client (IOStream c) throws GLib.Error {
+ var excl = false;
+
+ debug ("new socket client, reading init header");
+
+ var p = new uint8[sizeof(SpiceProtocol.Controller.Init)];
+ var init = (SpiceProtocol.Controller.Init*)p;
+ yield input_stream_read (c.input_stream, p);
+ if (warn_if (init.base.magic != SpiceProtocol.Controller.MAGIC))
+ return;
+ if (warn_if (init.base.version != SpiceProtocol.Controller.VERSION))
+ return;
+ if (warn_if (init.base.size < sizeof (SpiceProtocol.Controller.Init)))
+ return;
+ if (warn_if (init.credentials != 0))
+ return;
+ if (warn_if (excl_connection != null))
+ return;
+
+ excl = (bool)(init.flags & SpiceProtocol.Controller.Flag.EXCLUSIVE);
+ if (excl) {
+ if (nclients > 1) {
+ warning (@"Can't make the client exclusive, there is already $nclients connected clients");
+ return;
+ }
+ excl_connection = c;
+ }
+
+ client_connected ();
+
+ for (;;) {
+ var t = new uint8[sizeof(SpiceProtocol.Controller.Msg)];
+ yield input_stream_read (c.input_stream, t);
+ var msg = (SpiceProtocol.Controller.Msg*)t;
+ debug ("new message " + msg.id.to_string () + "size " + msg.size.to_string ());
+ if (warn_if (msg.size < sizeof (SpiceProtocol.Controller.Msg)))
+ break;
+
+ if (msg.size > sizeof (SpiceProtocol.Controller.Msg)) {
+ t.resize ((int)msg.size);
+ msg = (SpiceProtocol.Controller.Msg*)t;
+ yield input_stream_read (c.input_stream, t[sizeof(SpiceProtocol.Controller.Msg):msg.size]);
+ }
+
+ handle_message (msg);
+ }
+
+ if (excl)
+ excl_connection = null;
+ }
+
+ public Controller() {
+ }
+
+ public async void listen (string? addr = null) throws GLib.Error, SpiceCtrl.Error
+ {
+ var listener = ControllerListener.new_listener (addr);
+
+ for (;;) {
+ var c = yield listener.accept_async ();
+ nclients += 1;
+ clients.append (c);
+ try {
+ yield handle_client (c);
+ } catch (GLib.Error e) {
+ warning (e.message);
+ }
+ c.close ();
+ clients.remove (c);
+ nclients -= 1;
+ }
+ }
+}
+
+} // SpiceCtrl
diff --git a/src/controller/custom.h b/src/controller/custom.h
new file mode 100644
index 0000000..7f849fc
--- /dev/null
+++ b/src/controller/custom.h
@@ -0,0 +1,22 @@
+#ifndef CUSTOM_H_
+#define CUSTOM_H_
+
+#include <glib.h>
+
+static inline gboolean g_warn_if_expr (gboolean condition,
+ const char *pretty_func,
+ const char *expression) {
+ if G_UNLIKELY(condition) {
+ g_log (G_LOG_DOMAIN,
+ G_LOG_LEVEL_CRITICAL,
+ "%s: `%s' condition reached",
+ pretty_func,
+ expression);
+ }
+
+ return condition;
+}
+
+#define g_warn_if(expr) g_warn_if_expr((expr), __PRETTY_FUNCTION__, #expr)
+
+#endif
diff --git a/src/controller/custom.vapi b/src/controller/custom.vapi
new file mode 100644
index 0000000..a12fdec
--- /dev/null
+++ b/src/controller/custom.vapi
@@ -0,0 +1,28 @@
+using GLib;
+
+namespace Custom {
+
+ [CCode (cname = "g_warn_if", cheader_filename = "custom.h")]
+ public bool warn_if(bool condition);
+}
+
+namespace Spice {
+
+ [CCode (cname = "GObject", ref_function = "g_object_ref", unref_function = "g_object_unref", free_function = "")]
+ class ControllerListener {
+ [CCode (cname = "spice_controller_listener_new", cheader_filename = "spice-controller-listener.h")]
+ public static ControllerListener new_listener (string addr) throws GLib.Error;
+
+ [CCode (cname = "spice_controller_listener_accept_async", cheader_filename = "spice-controller-listener.h")]
+ public async unowned GLib.IOStream accept_async (GLib.Cancellable? cancellable = null, out GLib.Object? source_object = null) throws GLib.Error;
+ }
+
+ [CCode (cname = "GObject", ref_function = "g_object_ref", unref_function = "g_object_unref", free_function = "")]
+ class ForeignMenuListener {
+ [CCode (cname = "spice_foreign_menu_listener_new", cheader_filename = "spice-foreign-menu-listener.h")]
+ public static ForeignMenuListener new_listener (string addr) throws GLib.Error;
+
+ [CCode (cname = "spice_foreign_menu_listener_accept_async", cheader_filename = "spice-foreign-menu-listener.h")]
+ public async unowned GLib.IOStream accept_async (GLib.Cancellable? cancellable = null, out GLib.Object? source_object = null) throws GLib.Error;
+ }
+}
diff --git a/src/controller/dump.c b/src/controller/dump.c
new file mode 100644
index 0000000..831a1d7
--- /dev/null
+++ b/src/controller/dump.c
@@ -0,0 +1,118 @@
+/* Copyright (C) 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 <stdio.h>
+#include <stdint.h>
+
+#ifdef WIN32
+#include <windows.h>
+#else
+#include <sys/socket.h>
+#ifdef HAVE_SYS_TYPES_H
+#include <sys/types.h>
+#endif
+#include <sys/un.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#endif
+
+#include "spice-controller.h"
+
+SpiceCtrlController *ctrl = NULL;
+SpiceCtrlForeignMenu *menu = NULL;
+GMainLoop *loop = NULL;
+
+void signaled (GObject *gobject, const gchar *signal_name)
+{
+ g_message ("signaled: %s", signal_name);
+}
+
+void notified (GObject *gobject, GParamSpec *pspec,
+ gpointer user_data)
+{
+ GValue value = { 0, };
+ GValue strvalue = { 0, };
+
+ g_return_if_fail (gobject != NULL);
+ g_return_if_fail (pspec != NULL);
+
+ g_value_init (&value, pspec->value_type);
+ g_value_init (&strvalue, G_TYPE_STRING);
+ g_object_get_property (gobject, pspec->name, &value);
+
+ if (pspec->value_type == G_TYPE_STRV) {
+ gchar** p = (gchar **)g_value_get_boxed (&value);
+ g_message ("notify::%s == ", pspec->name);
+ while (*p)
+ g_message ("%s", *p++);
+ } else if (G_TYPE_IS_OBJECT(pspec->value_type)) {
+ GObject *o = g_value_get_object (&value);
+ g_message ("notify::%s == %s", pspec->name, o ? G_OBJECT_TYPE_NAME (o) : "null");
+ } else {
+ g_value_transform (&value, &strvalue);
+ g_message ("notify::%s = %s", pspec->name, g_value_get_string (&strvalue));
+ }
+
+ g_value_unset (&value);
+ g_value_unset (&strvalue);
+}
+
+void connect_signals (gpointer obj)
+{
+ guint i, n_ids = 0;
+ guint *ids = NULL;
+ GType type = G_OBJECT_TYPE (obj);
+
+ ids = g_signal_list_ids (type, &n_ids);
+ for (i = 0; i < n_ids; i++) {
+ const gchar *name = g_signal_name (ids[i]);
+ g_signal_connect (obj, name, G_CALLBACK (signaled), (gpointer)name);
+ }
+}
+
+int main (int argc, char *argv[])
+{
+#if !GLIB_CHECK_VERSION(2,36,0)
+ g_type_init ();
+#endif
+ loop = g_main_loop_new (NULL, FALSE);
+
+ if (argc > 1 && g_str_equal(argv[1], "--menu")) {
+ menu = spice_ctrl_foreign_menu_new ();
+ g_signal_connect (menu, "notify", G_CALLBACK (notified), NULL);
+ connect_signals (menu);
+
+ spice_ctrl_foreign_menu_listen (menu, NULL, NULL, NULL);
+ } else {
+ ctrl = spice_ctrl_controller_new ();
+ g_signal_connect (ctrl, "notify", G_CALLBACK (notified), NULL);
+ connect_signals (ctrl);
+
+ spice_ctrl_controller_listen (ctrl, NULL, NULL, NULL);
+ }
+
+ g_main_loop_run (loop);
+
+ if (ctrl != NULL)
+ g_object_unref (ctrl);
+ if (menu != NULL)
+ g_object_unref (menu);
+
+ return 0;
+}
diff --git a/src/controller/foreign-menu.vala b/src/controller/foreign-menu.vala
new file mode 100644
index 0000000..005955a
--- /dev/null
+++ b/src/controller/foreign-menu.vala
@@ -0,0 +1,197 @@
+// Copyright (C) 2012 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/>.
+
+using Custom;
+
+namespace SpiceCtrl {
+
+public class ForeignMenu: Object {
+
+ public Menu menu { get; private set; }
+ public string title { get; private set; }
+
+ public signal void client_connected ();
+
+ private int nclients;
+ private List<IOStream> clients;
+
+ public ForeignMenu() {
+ menu = new Menu ();
+ }
+
+ public void menu_item_click_msg (int32 item_id) {
+ debug ("clicked id: %d".printf (item_id));
+
+ var msg = SpiceProtocol.ForeignMenu.Event ();
+ msg.base.size = (uint32)sizeof (SpiceProtocol.ForeignMenu.Event);
+ msg.base.id = SpiceProtocol.ForeignMenu.MsgId.ITEM_EVENT;
+ msg.id = item_id;
+ msg.action = SpiceProtocol.ForeignMenu.EventType.CLICK;
+
+ unowned uint8[] p = ((uint8[])(&msg))[0:msg.base.size];
+ send_msg.begin (p);
+ }
+
+ public void menu_item_checked_msg (int32 item_id, bool checked = true) {
+ debug ("%schecked id: %d".printf (checked ? "" : "un", item_id));
+
+ var msg = SpiceProtocol.ForeignMenu.Event ();
+ msg.base.size = (uint32)sizeof (SpiceProtocol.ForeignMenu.Event);
+ msg.base.id = SpiceProtocol.ForeignMenu.MsgId.ITEM_EVENT;
+ msg.id = item_id;
+ msg.action = checked ?
+ SpiceProtocol.ForeignMenu.EventType.CHECKED :
+ SpiceProtocol.ForeignMenu.EventType.UNCHECKED;
+
+ unowned uint8[] p = ((uint8[])(&msg))[0:msg.base.size];
+ send_msg.begin (p);
+ }
+
+ public void app_activated_msg (bool activated = true) {
+ var msg = SpiceProtocol.ForeignMenu.Msg ();
+ msg.size = (uint32)sizeof (SpiceProtocol.ForeignMenu.Event);
+ msg.id = activated ?
+ SpiceProtocol.ForeignMenu.MsgId.APP_ACTIVATED :
+ SpiceProtocol.ForeignMenu.MsgId.APP_DEACTIVATED;
+
+ unowned uint8[] p = ((uint8[])(&msg))[0:msg.size];
+ send_msg.begin (p);
+ }
+
+ public async bool send_msg (owned uint8[] p) throws GLib.Error {
+ // vala FIXME: pass Controller.Msg instead
+ // vala doesn't keep reference on the struct in async methods
+ // it copies only base, which is not enough to transmit the whole
+ // message.
+ try {
+ foreach (var c in clients) {
+ yield output_stream_write (c.output_stream, p);
+ }
+ } catch (GLib.Error e) {
+ warning (e.message);
+ }
+
+ return true;
+ }
+
+ SpiceProtocol.Controller.MenuFlags get_menu_flags (uint32 type) {
+ SpiceProtocol.Controller.MenuFlags flags = 0;
+
+ if ((SpiceProtocol.ForeignMenu.MenuFlags.CHECKED & type) != 0)
+ flags |= SpiceProtocol.Controller.MenuFlags.CHECKED;
+ if ((SpiceProtocol.ForeignMenu.MenuFlags.DIM & type) != 0)
+ flags |= SpiceProtocol.Controller.MenuFlags.GRAYED;
+
+ return flags;
+ }
+
+ private bool handle_message (SpiceProtocol.ForeignMenu.Msg* msg) {
+ switch (msg.id) {
+ case SpiceProtocol.ForeignMenu.MsgId.SET_TITLE:
+ var t = (SpiceProtocol.ForeignMenu.SetTitle*)(msg);
+ title = t.string;
+ break;
+ case SpiceProtocol.ForeignMenu.MsgId.ADD_ITEM:
+ var i = (SpiceProtocol.ForeignMenu.AddItem*)(msg);
+ debug ("add id:%u type:%u position:%u title:%s", i.id, i.type, i.position, i.string);
+ menu.items.append (new MenuItem ((int)i.id, i.string, get_menu_flags (i.type)));
+ notify_property ("menu");
+ break;
+ case SpiceProtocol.ForeignMenu.MsgId.MODIFY_ITEM:
+ debug ("deprecated: modify item");
+ break;
+ case SpiceProtocol.ForeignMenu.MsgId.REMOVE_ITEM:
+ var i = (SpiceProtocol.ForeignMenu.RmItem*)(msg);
+ debug ("not implemented: remove id:%u".printf (i.id));
+ break;
+ case SpiceProtocol.ForeignMenu.MsgId.CLEAR:
+ menu = new Menu ();
+ break;
+ default:
+ warn_if_reached ();
+ return false;
+ }
+ return true;
+ }
+
+ private async void handle_client (IOStream c) throws GLib.Error {
+ debug ("new socket client, reading init header");
+
+ var p = new uint8[sizeof(SpiceProtocol.ForeignMenu.InitHeader)];
+ var header = (SpiceProtocol.ForeignMenu.InitHeader*)p;
+ yield input_stream_read (c.input_stream, p);
+ if (warn_if (header.magic != SpiceProtocol.ForeignMenu.MAGIC))
+ return;
+ if (warn_if (header.version != SpiceProtocol.ForeignMenu.VERSION))
+ return;
+ if (warn_if (header.size < sizeof (SpiceProtocol.ForeignMenu.Init)))
+ return;
+
+ var cp = new uint8[sizeof(uint64)];
+ yield input_stream_read (c.input_stream, cp);
+ uint64 credentials = *(uint64*)cp;
+ if (warn_if (credentials != 0))
+ return;
+
+ var title_size = header.size - sizeof(SpiceProtocol.ForeignMenu.Init);
+ var title = new uint8[title_size + 1];
+ yield c.input_stream.read_async (title[0:title_size]);
+ this.title = (string)title;
+
+ client_connected ();
+
+ for (;;) {
+ var t = new uint8[sizeof(SpiceProtocol.ForeignMenu.Msg)];
+ yield input_stream_read (c.input_stream, t);
+ var msg = (SpiceProtocol.ForeignMenu.Msg*)t;
+ debug ("new message " + msg.id.to_string () + "size " + msg.size.to_string ());
+
+ if (warn_if (msg.size < sizeof (SpiceProtocol.ForeignMenu.Msg)))
+ break;
+
+ if (msg.size > sizeof (SpiceProtocol.ForeignMenu.Msg)) {
+ t.resize ((int)msg.size);
+ msg = (SpiceProtocol.ForeignMenu.Msg*)t;
+
+ yield input_stream_read (c.input_stream, t[sizeof(SpiceProtocol.ForeignMenu.Msg):msg.size]);
+ }
+
+ handle_message (msg);
+ }
+
+ }
+
+ public async void listen (string? addr = null) throws GLib.Error, SpiceCtrl.Error
+ {
+ var listener = Spice.ForeignMenuListener.new_listener (addr);
+
+ for (;;) {
+ var c = yield listener.accept_async ();
+ nclients += 1;
+ clients.append (c);
+ try {
+ yield handle_client (c);
+ } catch (GLib.Error e) {
+ warning (e.message);
+ }
+ c.close ();
+ clients.remove (c);
+ nclients -= 1;
+ }
+ }
+
+}
+
+} // SpiceCtrl
diff --git a/src/controller/gio-windows-2.0.vapi b/src/controller/gio-windows-2.0.vapi
new file mode 100644
index 0000000..a09cfe8
--- /dev/null
+++ b/src/controller/gio-windows-2.0.vapi
@@ -0,0 +1,30 @@
+/* gio-windows-2.0.vapi generated by vapigen. */
+/* NOT YET UPSTREAM: https://bugzilla.gnome.org/show_bug.cgi?id=650052 */
+
+[CCode (cprefix = "GLib", lower_case_cprefix = "glib_")]
+namespace GLib {
+ [CCode (cheader_filename = "gio/gwin32inputstream.h")]
+ public class Win32InputStream : GLib.InputStream {
+ public weak GLib.InputStream parent_instance;
+ [CCode (cname = "g_win32_input_stream_new", type = "GInputStream*", has_construct_function = false)]
+ public Win32InputStream (void* handle, bool close_handle);
+ [CCode (cname = "g_win32_input_stream_get_close_handle")]
+ public static bool get_close_handle (GLib.Win32InputStream stream);
+ [CCode (cname = "g_win32_input_stream_get_handle")]
+ public static void* get_handle (GLib.Win32InputStream stream);
+ [CCode (cname = "g_win32_input_stream_set_close_handle")]
+ public static void set_close_handle (GLib.Win32InputStream stream, bool close_handle);
+ }
+ [CCode (cheader_filename = "gio/gwin32inputstream.h")]
+ public class Win32OutputStream : GLib.OutputStream {
+ public weak GLib.OutputStream parent_instance;
+ [CCode (cname = "g_win32_output_stream_new", type = "GOutputStream*", has_construct_function = false)]
+ public Win32OutputStream (void* handle, bool close_handle);
+ [CCode (cname = "g_win32_output_stream_get_close_handle")]
+ public static bool get_close_handle (GLib.Win32OutputStream stream);
+ [CCode (cname = "g_win32_output_stream_get_handle")]
+ public static void* get_handle (GLib.Win32OutputStream stream);
+ [CCode (cname = "g_win32_output_stream_set_close_handle")]
+ public static void set_close_handle (GLib.Win32OutputStream stream, bool close_handle);
+ }
+}
diff --git a/src/controller/menu.vala b/src/controller/menu.vala
new file mode 100644
index 0000000..7e8fc16
--- /dev/null
+++ b/src/controller/menu.vala
@@ -0,0 +1,108 @@
+// Copyright (C) 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/>.
+
+using GLib;
+using Custom;
+using SpiceProtocol.Controller;
+
+namespace SpiceCtrl {
+
+public class MenuItem: Object {
+
+ public Menu submenu;
+ public int parent_id;
+ public int id;
+ public string text;
+ public string accel;
+ public SpiceProtocol.Controller.MenuFlags flags;
+
+ public MenuItem (int id, string text, SpiceProtocol.Controller.MenuFlags flags) {
+ this.id = id;
+ this.text = text;
+ this.flags = flags;
+ }
+
+ public MenuItem.from_string (string str) throws SpiceCtrl.Error {
+ var params = str.split (SpiceProtocol.Controller.MENU_PARAM_DELIMITER);
+ if (warn_if (params.length != 5))
+ throw new SpiceCtrl.Error.VALUE(""); /* Vala: why is it mandatory to give a string? */
+ parent_id = int.parse (params[0]);
+ id = int.parse (params[1]);
+ var textaccel = params[2].split ("\t");
+ text = textaccel[0];
+ if (textaccel.length > 1)
+ accel = textaccel[1];
+ flags = (SpiceProtocol.Controller.MenuFlags)int.parse (params[3]);
+
+ submenu = new Menu ();
+ }
+
+ public string to_string () {
+ var sub = submenu.to_string ();
+ var str = @"pid: $parent_id, id: $id, text: \"$text\", flags: $flags";
+ foreach (var l in sub.to_string ().split ("\n")) {
+ if (l == "")
+ continue;
+ str += @"\n $l";
+ }
+ return str;
+ }
+}
+
+public class Menu: Object {
+
+ public List<MenuItem> items;
+
+ public Menu? find_id (int id) {
+ if (id == 0)
+ return this;
+
+ foreach (var item in items) {
+ if (item.id == id)
+ return item.submenu;
+
+ var menu = item.submenu.find_id (id);
+ if (menu != null)
+ return menu;
+ }
+
+ return null;
+ }
+
+ public Menu.from_string (string str) {
+ foreach (var itemstr in str.split (SpiceProtocol.Controller.MENU_ITEM_DELIMITER)) {
+ try {
+ if (itemstr.length == 0)
+ continue;
+ var item = new MenuItem.from_string (itemstr);
+ var parent = find_id (item.parent_id);
+ if (parent == null)
+ throw new SpiceCtrl.Error.VALUE("Invalid parent menu id");
+ parent.items.append (item);
+ } catch (SpiceCtrl.Error e) {
+ warning (e.message);
+ }
+ }
+ }
+
+ public string to_string () {
+ var str = "";
+ foreach (var i in items)
+ str += @"\n$i";
+ return str;
+ }
+}
+
+} // SpiceCtrl
diff --git a/src/controller/namedpipe.c b/src/controller/namedpipe.c
new file mode 100644
index 0000000..5312218
--- /dev/null
+++ b/src/controller/namedpipe.c
@@ -0,0 +1,270 @@
+/*
+ Copyright (C) 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 "namedpipe.h"
+
+#include <windows.h>
+#include <stdio.h>
+#include <conio.h>
+#include <tchar.h>
+
+static void spice_named_pipe_initable_iface_init (GInitableIface *iface);
+static gboolean spice_named_pipe_initable_init (GInitable *initable,
+ GCancellable *cancellable,
+ GError **error);
+
+G_DEFINE_TYPE_WITH_CODE (SpiceNamedPipe, spice_named_pipe, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE,
+ spice_named_pipe_initable_iface_init));
+
+enum
+{
+ PROP_0,
+ PROP_NAME,
+ PROP_HANDLE,
+};
+
+struct _SpiceNamedPipePrivate
+{
+ gchar * name;
+ GError * construct_error;
+ guint inited : 1;
+ HANDLE handle;
+};
+
+static void
+spice_named_pipe_finalize (GObject *object)
+{
+ SpiceNamedPipe *np = SPICE_NAMED_PIPE (object);
+
+ g_clear_error (&np->priv->construct_error);
+
+ g_free (np->priv->name);
+ np->priv->name = NULL;
+
+ if (np->priv->handle)
+ {
+ CloseHandle (np->priv->handle);
+ np->priv->handle = NULL;
+ }
+
+ if (G_OBJECT_CLASS (spice_named_pipe_parent_class)->finalize)
+ G_OBJECT_CLASS (spice_named_pipe_parent_class)->finalize (object);
+}
+
+#define DEFAULT_PIPE_BUF_SIZE 4096
+
+static void
+spice_named_pipe_constructed (GObject *object)
+{
+ SpiceNamedPipe *np = SPICE_NAMED_PIPE (object);
+
+ if (np->priv->handle)
+ /* TODO: find a way to ensure user provided handle is a named
+ pipe, in overlapped mode */
+ goto end;
+
+ np->priv->handle = CreateNamedPipe (np->priv->name,
+ PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
+ PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
+ PIPE_UNLIMITED_INSTANCES,
+ DEFAULT_PIPE_BUF_SIZE, DEFAULT_PIPE_BUF_SIZE,
+ 0, NULL);
+
+ if (np->priv->handle == INVALID_HANDLE_VALUE)
+ {
+ int errsv = GetLastError ();
+ gchar *emsg = g_win32_error_message (errsv);
+
+ g_set_error (&np->priv->construct_error,
+ G_IO_ERROR,
+ g_io_error_from_win32_error (errsv),
+ "Error CreateNamedPipe(): %s",
+ emsg);
+
+ g_free (emsg);
+ return;
+ }
+
+ /* TODO: we could have a client backlog by creating many pipes, the
+ maximum number of outstanding connections.. or we could just let
+ the named_pipe_listener take multiple NamedPipe instances */
+end:
+ g_assert (np->priv->handle != INVALID_HANDLE_VALUE);
+ return;
+}
+
+static void
+spice_named_pipe_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceNamedPipe *np = SPICE_NAMED_PIPE (object);
+
+ switch (prop_id)
+ {
+ case PROP_NAME:
+ g_value_set_string (value, np->priv->name);
+ break;
+ case PROP_HANDLE:
+ g_value_set_pointer (value, np->priv->handle);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+spice_named_pipe_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceNamedPipe *np = SPICE_NAMED_PIPE (object);
+
+ switch (prop_id)
+ {
+ case PROP_NAME:
+ g_free (np->priv->name);
+ np->priv->name = g_value_dup_string (value);
+ break;
+ case PROP_HANDLE:
+ np->priv->handle = g_value_get_pointer (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+spice_named_pipe_class_init (SpiceNamedPipeClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ g_type_class_add_private (klass, sizeof (SpiceNamedPipePrivate));
+
+ gobject_class->set_property = spice_named_pipe_set_property;
+ gobject_class->get_property = spice_named_pipe_get_property;
+ gobject_class->finalize = spice_named_pipe_finalize;
+ gobject_class->constructed = spice_named_pipe_constructed;
+
+ g_object_class_install_property (gobject_class, PROP_NAME,
+ g_param_spec_string ("name",
+ "Pipe Name",
+ "The NamedPipe name",
+ NULL,
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property (gobject_class, PROP_HANDLE,
+ g_param_spec_pointer ("handle",
+ "Pipe handle",
+ "The pipe handle",
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+}
+
+static void
+spice_named_pipe_init (SpiceNamedPipe *np)
+{
+ np->priv = G_TYPE_INSTANCE_GET_PRIVATE (np,
+ SPICE_TYPE_NAMED_PIPE,
+ SpiceNamedPipePrivate);
+}
+
+static gboolean
+spice_named_pipe_initable_init (GInitable *initable,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SpiceNamedPipe *np;
+
+ g_return_val_if_fail (SPICE_IS_NAMED_PIPE (initable), FALSE);
+
+ np = SPICE_NAMED_PIPE (initable);
+
+ if (cancellable != NULL)
+ {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
+ "Cancellable initialization not supported");
+ return FALSE;
+ }
+
+ np->priv->inited = TRUE;
+
+ if (np->priv->construct_error)
+ {
+ if (error)
+ *error = g_error_copy (np->priv->construct_error);
+ return FALSE;
+ }
+
+
+ return TRUE;
+}
+
+static void
+spice_named_pipe_initable_iface_init (GInitableIface *iface)
+{
+ iface->init = spice_named_pipe_initable_init;
+}
+
+SpiceNamedPipe *
+spice_named_pipe_new (const gchar *name, GError **error)
+{
+ return SPICE_NAMED_PIPE (g_initable_new (SPICE_TYPE_NAMED_PIPE,
+ NULL, error,
+ "name", name,
+ NULL));
+}
+
+void *
+spice_named_pipe_get_handle (SpiceNamedPipe *namedpipe)
+{
+ g_return_val_if_fail (SPICE_IS_NAMED_PIPE (namedpipe), NULL);
+
+ return namedpipe->priv->handle;
+}
+
+gboolean
+spice_named_pipe_close (SpiceNamedPipe *np,
+ GError **error)
+{
+ BOOL res;
+
+ g_return_val_if_fail (SPICE_IS_NAMED_PIPE (np), FALSE);
+
+ res = CloseHandle (np->priv->handle);
+ np->priv->handle = NULL;
+ if (!res)
+ {
+ int errsv = GetLastError ();
+ gchar *emsg = g_win32_error_message (errsv);
+
+ g_set_error (error, G_IO_ERROR,
+ g_io_error_from_win32_error (errsv),
+ "Error closing handle: %s",
+ emsg);
+ g_free (emsg);
+ return FALSE;
+ }
+
+ return TRUE;
+}
diff --git a/src/controller/namedpipe.h b/src/controller/namedpipe.h
new file mode 100644
index 0000000..e0e873b
--- /dev/null
+++ b/src/controller/namedpipe.h
@@ -0,0 +1,59 @@
+/*
+ Copyright (C) 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/>.
+*/
+#ifndef __NAMED_PIPE_H__
+#define __NAMED_PIPE_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_NAMED_PIPE (spice_named_pipe_get_type ())
+#define SPICE_NAMED_PIPE(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \
+ SPICE_TYPE_NAMED_PIPE, SpiceNamedPipe))
+#define SPICE_NAMED_PIPE_CLASS(class) (G_TYPE_CHECK_CLASS_CAST ((class), \
+ SPICE_TYPE_NAMED_PIPE, SpiceNamedPipeClass))
+#define SPICE_IS_NAMED_PIPE(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \
+ SPICE_TYPE_NAMED_PIPE))
+#define SPICE_IS_NAMED_PIPE_CLASS(class) (G_TYPE_CHECK_CLASS_TYPE ((class), \
+ SPICE_TYPE_NAMED_PIPE))
+#define SPICE_NAMED_PIPE_GET_CLASS(inst) (G_TYPE_INSTANCE_GET_CLASS ((inst), \
+ SPICE_TYPE_NAMED_PIPE, SpiceNamedPipeClass))
+
+typedef struct _SpiceNamedPipe SpiceNamedPipe;
+typedef struct _SpiceNamedPipePrivate SpiceNamedPipePrivate;
+typedef struct _SpiceNamedPipeClass SpiceNamedPipeClass;
+
+struct _SpiceNamedPipeClass
+{
+ GObjectClass parent_class;
+};
+
+struct _SpiceNamedPipe
+{
+ GObject parent_instance;
+ SpiceNamedPipePrivate *priv;
+};
+
+GType spice_named_pipe_get_type (void) G_GNUC_CONST;
+
+SpiceNamedPipe * spice_named_pipe_new (const gchar *name, GError **error);
+void * spice_named_pipe_get_handle(SpiceNamedPipe *namedpipe);
+gboolean spice_named_pipe_close (SpiceNamedPipe *namedpipe,
+ GError **error);
+G_END_DECLS
+
+#endif /* __NAMED_PIPE_H__ */
diff --git a/src/controller/namedpipeconnection.c b/src/controller/namedpipeconnection.c
new file mode 100644
index 0000000..3173b61
--- /dev/null
+++ b/src/controller/namedpipeconnection.c
@@ -0,0 +1,245 @@
+/*
+ Copyright (C) 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 "namedpipeconnection.h"
+
+#include <windows.h>
+#include <stdio.h>
+#include <conio.h>
+#include <tchar.h>
+
+#include <gio/gwin32inputstream.h>
+#include <gio/gwin32outputstream.h>
+
+G_DEFINE_TYPE (SpiceNamedPipeConnection, spice_named_pipe_connection,
+ G_TYPE_IO_STREAM)
+
+enum
+{
+ PROP_0,
+ PROP_NAMED_PIPE,
+};
+
+struct _SpiceNamedPipeConnectionPrivate
+{
+ GInputStream *input_stream;
+ GOutputStream *output_stream;
+ SpiceNamedPipe *namedpipe;
+ gboolean in_dispose;
+};
+
+static void
+spice_named_pipe_connection_init (SpiceNamedPipeConnection *connection)
+{
+ connection->priv = G_TYPE_INSTANCE_GET_PRIVATE (connection,
+ SPICE_TYPE_NAMED_PIPE_CONNECTION,
+ SpiceNamedPipeConnectionPrivate);
+}
+
+static void
+spice_named_pipe_connection_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceNamedPipeConnection *c = SPICE_NAMED_PIPE_CONNECTION (object);
+
+ switch (prop_id)
+ {
+ case PROP_NAMED_PIPE:
+ g_return_if_fail (c->priv->namedpipe == NULL);
+ g_value_set_object (value, c->priv->namedpipe);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+spice_named_pipe_connection_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceNamedPipeConnection *c = SPICE_NAMED_PIPE_CONNECTION (object);
+
+ switch (prop_id)
+ {
+ case PROP_NAMED_PIPE:
+ c->priv->namedpipe = g_value_get_object (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static GInputStream *
+spice_named_pipe_connection_get_input_stream (GIOStream *io_stream)
+{
+ SpiceNamedPipeConnection *c = SPICE_NAMED_PIPE_CONNECTION (io_stream);
+ HANDLE h = spice_named_pipe_get_handle (c->priv->namedpipe);
+
+ g_return_val_if_fail (h != NULL, NULL);
+
+ if (c->priv->input_stream == NULL)
+ c->priv->input_stream = g_win32_input_stream_new (h, FALSE);
+
+ return c->priv->input_stream;
+}
+
+static GOutputStream *
+spice_named_pipe_connection_get_output_stream (GIOStream *io_stream)
+{
+ SpiceNamedPipeConnection *c = SPICE_NAMED_PIPE_CONNECTION (io_stream);
+ HANDLE h = spice_named_pipe_get_handle (c->priv->namedpipe);
+
+ g_return_val_if_fail (h != NULL, NULL);
+
+ if (c->priv->output_stream == NULL)
+ c->priv->output_stream = g_win32_output_stream_new (h, FALSE);
+
+ return c->priv->output_stream;
+}
+
+static void
+spice_named_pipe_connection_dispose (GObject *object)
+{
+ SpiceNamedPipeConnection *c = SPICE_NAMED_PIPE_CONNECTION (object);
+
+ c->priv->in_dispose = TRUE;
+
+ if (G_OBJECT_CLASS (spice_named_pipe_connection_parent_class)->dispose)
+ G_OBJECT_CLASS (spice_named_pipe_connection_parent_class)->dispose (object);
+
+ c->priv->in_dispose = FALSE;
+}
+
+static void
+spice_named_pipe_connection_finalize (GObject *object)
+{
+ SpiceNamedPipeConnection *c = SPICE_NAMED_PIPE_CONNECTION (object);
+
+ if (c->priv->output_stream)
+ {
+ g_object_unref (c->priv->output_stream);
+ c->priv->output_stream = NULL;
+ }
+
+ if (c->priv->input_stream)
+ {
+ g_object_unref (c->priv->input_stream);
+ c->priv->input_stream = NULL;
+ }
+
+ g_object_unref (c->priv->namedpipe);
+
+ if (G_OBJECT_CLASS (spice_named_pipe_connection_parent_class)->finalize)
+ G_OBJECT_CLASS (spice_named_pipe_connection_parent_class)->finalize (object);
+}
+
+static gboolean
+spice_named_pipe_connection_close (GIOStream *stream,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SpiceNamedPipeConnection *c = SPICE_NAMED_PIPE_CONNECTION (stream);
+
+ if (c->priv->output_stream)
+ g_output_stream_close (c->priv->output_stream, cancellable, NULL);
+ if (c->priv->input_stream)
+ g_input_stream_close (c->priv->input_stream, cancellable, NULL);
+
+ /* Don't close the underlying socket if this is being called
+ * as part of dispose(); when destroying the GSocketConnection,
+ * we only want to close the socket if we're holding the last
+ * reference on it, and in that case it will close itself when
+ * we unref namedpipe in finalize().
+ */
+ if (c->priv->in_dispose)
+ return TRUE;
+
+ return spice_named_pipe_close (c->priv->namedpipe, error);
+}
+
+static void
+spice_named_pipe_connection_close_async (GIOStream *stream,
+ int io_priority,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GSimpleAsyncResult *res;
+ GIOStreamClass *class;
+ GError *error;
+
+ class = G_IO_STREAM_GET_CLASS (stream);
+
+ /* namedpipe close is not blocking, just do it! */
+ error = NULL;
+ if (class->close_fn &&
+ !class->close_fn (stream, cancellable, &error))
+ {
+ g_simple_async_report_take_gerror_in_idle (G_OBJECT (stream),
+ callback, user_data,
+ error);
+ return;
+ }
+
+ res = g_simple_async_result_new (G_OBJECT (stream),
+ callback,
+ user_data,
+ spice_named_pipe_connection_close_async);
+ g_simple_async_result_complete_in_idle (res);
+ g_object_unref (res);
+}
+
+static gboolean
+spice_named_pipe_connection_close_finish (GIOStream *stream,
+ GAsyncResult *result,
+ GError **error)
+{
+ return TRUE;
+}
+
+static void
+spice_named_pipe_connection_class_init (SpiceNamedPipeConnectionClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GIOStreamClass *stream_class = G_IO_STREAM_CLASS (klass);
+
+ g_type_class_add_private (klass, sizeof (SpiceNamedPipeConnectionPrivate));
+
+ gobject_class->set_property = spice_named_pipe_connection_set_property;
+ gobject_class->get_property = spice_named_pipe_connection_get_property;
+ gobject_class->dispose = spice_named_pipe_connection_dispose;
+ gobject_class->finalize = spice_named_pipe_connection_finalize;
+
+ stream_class->get_input_stream = spice_named_pipe_connection_get_input_stream;
+ stream_class->get_output_stream = spice_named_pipe_connection_get_output_stream;
+ stream_class->close_fn = spice_named_pipe_connection_close;
+ stream_class->close_async = spice_named_pipe_connection_close_async;
+ stream_class->close_finish = spice_named_pipe_connection_close_finish;
+
+ g_object_class_install_property (gobject_class, PROP_NAMED_PIPE,
+ g_param_spec_object ("namedpipe",
+ "NamedPipe",
+ "The associated NamedPipe",
+ SPICE_TYPE_NAMED_PIPE,
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+}
diff --git a/src/controller/namedpipeconnection.h b/src/controller/namedpipeconnection.h
new file mode 100644
index 0000000..86f0be6
--- /dev/null
+++ b/src/controller/namedpipeconnection.h
@@ -0,0 +1,56 @@
+/*
+ Copyright (C) 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/>.
+*/
+#ifndef __NAMED_PIPE_CONNECTION_H__
+#define __NAMED_PIPE_CONNECTION_H__
+
+#include <gio/gio.h>
+#include "namedpipe.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_NAMED_PIPE_CONNECTION (spice_named_pipe_connection_get_type ())
+#define SPICE_NAMED_PIPE_CONNECTION(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \
+ SPICE_TYPE_NAMED_PIPE_CONNECTION, SpiceNamedPipeConnection))
+#define SPICE_NAMED_PIPE_CONNECTION_CLASS(class) (G_TYPE_CHECK_CLASS_CAST ((class), \
+ SPICE_TYPE_NAMED_PIPE_CONNECTION, SpiceNamedPipeConnectionClass))
+#define SPICE_IS_NAMED_PIPE_CONNECTION(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \
+ SPICE_TYPE_NAMED_PIPE_CONNECTION))
+#define SPICE_IS_NAMED_PIPE_CONNECTION_CLASS(class) (G_TYPE_CHECK_CLASS_TYPE ((class), \
+ SPICE_TYPE_NAMED_PIPE_CONNECTION))
+#define SPICE_NAMED_PIPE_CONNECTION_GET_CLASS(inst) (G_TYPE_INSTANCE_GET_CLASS ((inst), \
+ SPICE_TYPE_NAMED_PIPE_CONNECTION, SpiceNamedPipeConnectionClass))
+
+typedef struct _SpiceNamedPipeConnection SpiceNamedPipeConnection;
+typedef struct _SpiceNamedPipeConnectionPrivate SpiceNamedPipeConnectionPrivate;
+typedef struct _SpiceNamedPipeConnectionClass SpiceNamedPipeConnectionClass;
+
+struct _SpiceNamedPipeConnectionClass
+{
+ GIOStreamClass parent_class;
+};
+
+struct _SpiceNamedPipeConnection
+{
+ GIOStream parent_instance;
+ SpiceNamedPipeConnectionPrivate *priv;
+};
+
+GType spice_named_pipe_connection_get_type (void) G_GNUC_CONST;
+
+G_END_DECLS
+
+#endif /* __NAMED_PIPE_CONNECTION_H__ */
diff --git a/src/controller/namedpipelistener.c b/src/controller/namedpipelistener.c
new file mode 100644
index 0000000..820c606
--- /dev/null
+++ b/src/controller/namedpipelistener.c
@@ -0,0 +1,329 @@
+/*
+ Copyright (C) 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 "namedpipelistener.h"
+
+#include <windows.h>
+#include <stdio.h>
+#include <conio.h>
+#include <tchar.h>
+
+static GSource *g_win32_handle_source_add (HANDLE handle,
+ GSourceFunc callback,
+ gpointer user_data);
+
+G_DEFINE_TYPE (SpiceNamedPipeListener, spice_named_pipe_listener, G_TYPE_OBJECT);
+
+struct _SpiceNamedPipeListenerPrivate
+{
+ GQueue namedpipes;
+};
+
+static void
+spice_named_pipe_listener_dispose (GObject *object)
+{
+ SpiceNamedPipeListener *listener = SPICE_NAMED_PIPE_LISTENER (object);
+ SpiceNamedPipe *p;
+
+ while ((p = g_queue_pop_head (&listener->priv->namedpipes)) != NULL)
+ g_object_unref (p);
+
+ g_return_if_fail (g_queue_get_length (&listener->priv->namedpipes) == 0);
+ g_queue_clear (&listener->priv->namedpipes);
+
+ if (G_OBJECT_CLASS (spice_named_pipe_listener_parent_class)->dispose)
+ G_OBJECT_CLASS (spice_named_pipe_listener_parent_class)->dispose (object);
+}
+
+static void
+spice_named_pipe_listener_class_init (SpiceNamedPipeListenerClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ g_type_class_add_private (klass, sizeof (SpiceNamedPipeListenerPrivate));
+
+ gobject_class->dispose = spice_named_pipe_listener_dispose;
+}
+
+static void
+spice_named_pipe_listener_init (SpiceNamedPipeListener *listener)
+{
+ listener->priv = G_TYPE_INSTANCE_GET_PRIVATE (listener,
+ SPICE_TYPE_NAMED_PIPE_LISTENER,
+ SpiceNamedPipeListenerPrivate);
+
+ g_queue_init (&listener->priv->namedpipes);
+}
+
+void
+spice_named_pipe_listener_add_named_pipe (SpiceNamedPipeListener *listener,
+ SpiceNamedPipe *namedpipe)
+{
+ g_return_if_fail (SPICE_IS_NAMED_PIPE_LISTENER (listener));
+ g_return_if_fail (SPICE_IS_NAMED_PIPE (namedpipe));
+
+ g_queue_push_head (&listener->priv->namedpipes, g_object_ref (namedpipe));
+}
+
+typedef struct {
+ GCancellable *cancellable;
+ GSource *source;
+ GSimpleAsyncResult *async_result;
+ SpiceNamedPipe *np;
+ OVERLAPPED overlapped;
+} ConnectData;
+
+static void
+connect_cancelled (GCancellable *cancellable,
+ gpointer user_data)
+{
+ ConnectData *c = user_data;
+ GError *error = NULL;
+
+ g_source_destroy (c->source);
+ c->source = NULL;
+
+ g_cancellable_set_error_if_cancelled (cancellable, &error);
+ g_simple_async_result_set_from_error (c->async_result, error);
+ g_error_free (error);
+
+ g_simple_async_result_complete (c->async_result);
+ g_object_unref (c->async_result);
+}
+
+static gboolean
+connect_ready (gpointer user_data)
+{
+ ConnectData *c = user_data;
+ gulong cbret;
+ gboolean success;
+
+ /* Now complete the result (assuming it wasn't already completed) */
+ g_return_val_if_fail (c->async_result != NULL, FALSE);
+
+ success = GetOverlappedResult (c->np, &c->overlapped, &cbret, FALSE);
+ if (!success)
+ {
+ int errsv = GetLastError ();
+ gchar *emsg = g_win32_error_message (errsv);
+
+ g_simple_async_result_set_error (c->async_result,
+ G_IO_ERROR,
+ G_IO_ERROR_INVALID_ARGUMENT,
+ "GetOverlappedResult(): %s %d",
+ emsg, errsv);
+ }
+
+ g_simple_async_result_complete (c->async_result);
+ g_object_unref (c->async_result); /* TODO: that sould free c? */
+
+ return FALSE;
+}
+
+static void
+connect_data_free (gpointer data)
+{
+ ConnectData *c = data;
+
+ if (c->source)
+ {
+ g_source_destroy (c->source);
+ g_source_unref (c->source);
+ c->source = NULL;
+ }
+ if (c->cancellable)
+ {
+ g_signal_handlers_disconnect_by_func (c->cancellable, connect_cancelled, c);
+ g_object_unref (c->cancellable);
+ c->cancellable = NULL;
+ }
+
+ if (c->async_result) /* this is only a weak reference */
+ c->async_result = NULL;
+
+ if (c->overlapped.hEvent != NULL)
+ {
+ CloseHandle (c->overlapped.hEvent);
+ c->overlapped.hEvent = NULL;
+ }
+
+ if (c->np != NULL)
+ {
+ g_object_unref (c->np);
+ c->np = NULL;
+ }
+
+ g_free (c);
+}
+
+void
+spice_named_pipe_listener_accept_async (SpiceNamedPipeListener *listener,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ ConnectData *c;
+ SpiceNamedPipe *namedpipe;
+
+ g_return_if_fail (SPICE_IS_NAMED_PIPE_LISTENER (listener));
+
+ namedpipe = SPICE_NAMED_PIPE (g_queue_pop_head (&listener->priv->namedpipes));
+ /* do not unref, we keep that ref */
+ g_return_if_fail (namedpipe != NULL);
+
+ c = g_new0 (ConnectData, 1);
+ c->np = namedpipe; /* transfer what used to be the avail_namedpipes ref */
+ c->async_result = g_simple_async_result_new (G_OBJECT (listener), callback, user_data,
+ spice_named_pipe_listener_accept_async);
+ c->overlapped.hEvent = CreateEvent (NULL, /* default security attribute */
+ TRUE, /* manual-reset event */
+ TRUE, /* initial state = signaled */
+ NULL); /* unnamed event object */
+ g_simple_async_result_set_op_res_gpointer (c->async_result, c, connect_data_free);
+
+ if (ConnectNamedPipe (spice_named_pipe_get_handle (namedpipe), &c->overlapped) != 0)
+ {
+ /* we shouldn't get there if the listener is in non-blocking */
+ g_warn_if_reached ();
+ }
+
+ switch (GetLastError ())
+ {
+ case ERROR_SUCCESS:
+ case ERROR_IO_PENDING:
+ break;
+ case ERROR_PIPE_CONNECTED:
+ g_simple_async_result_complete_in_idle (c->async_result);
+ g_object_unref (c->async_result);
+ return;
+ default:
+ g_simple_async_report_error_in_idle (G_OBJECT (listener),
+ callback, user_data,
+ G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
+ "ConnectNamedPipe() failed %ld", GetLastError ());
+ g_object_unref (c->async_result);
+ return;
+ }
+
+ c->source = g_win32_handle_source_add (c->overlapped.hEvent,
+ connect_ready, c);
+
+ if (cancellable)
+ {
+ c->cancellable = g_object_ref (cancellable);
+ g_signal_connect (cancellable, "cancelled",
+ G_CALLBACK (connect_cancelled), c);
+ }
+}
+
+SpiceNamedPipeConnection *
+spice_named_pipe_listener_accept_finish (SpiceNamedPipeListener *listener,
+ GAsyncResult *result,
+ GObject **source_object,
+ GError **error)
+{
+ GSimpleAsyncResult *simple;
+ ConnectData *c;
+ SpiceNamedPipeConnection *connection;
+
+ g_return_val_if_fail (SPICE_IS_NAMED_PIPE_LISTENER (listener), NULL);
+ g_return_val_if_fail (G_IS_SIMPLE_ASYNC_RESULT (result), NULL);
+ g_return_val_if_fail (g_simple_async_result_is_valid (result, G_OBJECT (listener),
+ spice_named_pipe_listener_accept_async),
+ NULL);
+
+ simple = G_SIMPLE_ASYNC_RESULT (result);
+ if (g_simple_async_result_propagate_error (simple, error))
+ return NULL;
+
+ c = g_simple_async_result_get_op_res_gpointer (simple);
+
+ connection = g_object_new (SPICE_TYPE_NAMED_PIPE_CONNECTION,
+ "namedpipe", c->np,
+ NULL);
+ return connection;
+}
+
+SpiceNamedPipeListener *
+spice_named_pipe_listener_new (void)
+{
+ return g_object_new (SPICE_TYPE_NAMED_PIPE_LISTENER, NULL);
+}
+
+/* Windows HANDLE GSource - from gio/gwin32resolver.c */
+
+typedef struct {
+ GSource source;
+ GPollFD pollfd;
+} GWin32HandleSource;
+
+static gboolean
+g_win32_handle_source_prepare (GSource *source,
+ gint *timeout)
+{
+ *timeout = -1;
+ return FALSE;
+}
+
+static gboolean
+g_win32_handle_source_check (GSource *source)
+{
+ GWin32HandleSource *hsource = (GWin32HandleSource *)source;
+
+ return hsource->pollfd.revents;
+}
+
+static gboolean
+g_win32_handle_source_dispatch (GSource *source,
+ GSourceFunc callback,
+ gpointer user_data)
+{
+ return (*callback) (user_data);
+}
+
+static void
+g_win32_handle_source_finalize (GSource *source)
+{
+ ;
+}
+
+GSourceFuncs g_win32_handle_source_funcs = {
+ g_win32_handle_source_prepare,
+ g_win32_handle_source_check,
+ g_win32_handle_source_dispatch,
+ g_win32_handle_source_finalize
+};
+
+static GSource *
+g_win32_handle_source_add (HANDLE handle,
+ GSourceFunc callback,
+ gpointer user_data)
+{
+ GWin32HandleSource *hsource;
+ GSource *source;
+
+ source = g_source_new (&g_win32_handle_source_funcs, sizeof (GWin32HandleSource));
+ hsource = (GWin32HandleSource *)source;
+ hsource->pollfd.fd = (gint)handle;
+ hsource->pollfd.events = G_IO_IN;
+ hsource->pollfd.revents = 0;
+ g_source_add_poll (source, &hsource->pollfd);
+
+ g_source_set_callback (source, callback, user_data, NULL);
+ g_source_attach (source, g_main_context_get_thread_default ());
+ return source;
+}
diff --git a/src/controller/namedpipelistener.h b/src/controller/namedpipelistener.h
new file mode 100644
index 0000000..c2dbd0a
--- /dev/null
+++ b/src/controller/namedpipelistener.h
@@ -0,0 +1,70 @@
+/*
+ Copyright (C) 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/>.
+*/
+#ifndef __NAMED_PIPE_LISTENER_H__
+#define __NAMED_PIPE_LISTENER_H__
+
+#include <gio/gio.h>
+
+#include "namedpipe.h"
+#include "namedpipeconnection.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_NAMED_PIPE_LISTENER (spice_named_pipe_listener_get_type ())
+#define SPICE_NAMED_PIPE_LISTENER(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \
+ SPICE_TYPE_NAMED_PIPE_LISTENER, SpiceNamedPipeListener))
+#define SPICE_NAMED_PIPE_LISTENER_CLASS(class) (G_TYPE_CHECK_CLASS_CAST ((class), \
+ SPICE_TYPE_NAMED_PIPE_LISTENER, SpiceNamedPipeListenerClass))
+#define SPICE_IS_NAMED_PIPE_LISTENER(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \
+ SPICE_TYPE_NAMED_PIPE_LISTENER))
+#define SPICE_IS_NAMED_PIPE_LISTENER_CLASS(class) (G_TYPE_CHECK_CLASS_TYPE ((class), \
+ SPICE_TYPE_NAMED_PIPE_LISTENER))
+#define SPICE_NAMED_PIPE_LISTENER_GET_CLASS(inst) (G_TYPE_INSTANCE_GET_CLASS ((inst), \
+ SPICE_TYPE_NAMED_PIPE_LISTENER, SpiceNamedPipeListenerClass))
+
+typedef struct _SpiceNamedPipeListener SpiceNamedPipeListener;
+typedef struct _SpiceNamedPipeListenerPrivate SpiceNamedPipeListenerPrivate;
+typedef struct _SpiceNamedPipeListenerClass SpiceNamedPipeListenerClass;
+
+struct _SpiceNamedPipeListenerClass
+{
+ GObjectClass parent_class;
+};
+
+struct _SpiceNamedPipeListener
+{
+ GObject parent_instance;
+ SpiceNamedPipeListenerPrivate *priv;
+};
+
+GType spice_named_pipe_listener_get_type (void) G_GNUC_CONST;
+
+SpiceNamedPipeListener * spice_named_pipe_listener_new (void);
+void spice_named_pipe_listener_add_named_pipe (SpiceNamedPipeListener *listener,
+ SpiceNamedPipe *namedpipe);
+void spice_named_pipe_listener_accept_async (SpiceNamedPipeListener *listener,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+SpiceNamedPipeConnection * spice_named_pipe_listener_accept_finish (SpiceNamedPipeListener *listener,
+ GAsyncResult *result,
+ GObject **source_object,
+ GError **error);
+
+G_END_DECLS
+
+#endif /* __NAMED_PIPE_LISTENER_H__ */
diff --git a/src/controller/spice-controller-listener.c b/src/controller/spice-controller-listener.c
new file mode 100644
index 0000000..98baf33
--- /dev/null
+++ b/src/controller/spice-controller-listener.c
@@ -0,0 +1,159 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 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 <glib/gstdio.h>
+
+#include "spice-controller-listener.h"
+
+#ifdef G_OS_WIN32
+#include <windows.h>
+#include "namedpipe.h"
+#include "namedpipelistener.h"
+#include "win32-util.h"
+#endif
+
+#ifdef G_OS_UNIX
+#include <gio/gunixsocketaddress.h>
+#endif
+
+/**
+ * SpiceControllerListenerError:
+ * @SPICE_CONTROLLER_LISTENER_ERROR_VALUE: invalid value.
+ *
+ * Possible errors of controller listener related functions.
+ **/
+
+/**
+ * SPICE_CONTROLLER_LISTENER_ERROR:
+ *
+ * The error domain of the controller listener subsystem.
+ **/
+GQuark
+spice_controller_listener_error_quark (void)
+{
+ return g_quark_from_static_string ("spice-controller-listener-error");
+}
+
+GObject*
+spice_controller_listener_new (const gchar *address, GError **error)
+{
+ GObject *listener = NULL;
+ gchar *addr = NULL;
+
+ g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+ addr = g_strdup (address);
+
+#ifdef G_OS_WIN32
+ if (addr == NULL)
+ addr = g_strdup (g_getenv ("SPICE_XPI_NAMEDPIPE"));
+ if (addr == NULL)
+ addr = g_strdup_printf ("\\\\.\\pipe\\SpiceController-%" G_GUINT64_FORMAT, (guint64)GetCurrentProcessId ());
+#else
+ if (addr == NULL)
+ addr = g_strdup (g_getenv ("SPICE_XPI_SOCKET"));
+#endif
+ if (addr == NULL) {
+ g_set_error (error,
+ SPICE_CONTROLLER_LISTENER_ERROR,
+ SPICE_CONTROLLER_LISTENER_ERROR_VALUE,
+#ifdef G_OS_WIN32
+ "Missing namedpipe address"
+#else
+ "Missing socket address"
+#endif
+ );
+ goto end;
+ }
+
+ g_unlink (addr);
+
+#ifdef G_OS_WIN32
+ {
+ SpiceNamedPipe *np;
+
+ listener = G_OBJECT (spice_named_pipe_listener_new ());
+
+ np = spice_win32_user_pipe_new (addr, error);
+ if (!np) {
+ g_object_unref (listener);
+ listener = NULL;
+ goto end;
+ }
+
+ spice_named_pipe_listener_add_named_pipe (SPICE_NAMED_PIPE_LISTENER (listener), np);
+ }
+#else
+ {
+ listener = G_OBJECT (g_socket_listener_new ());
+
+ if (!g_socket_listener_add_address (G_SOCKET_LISTENER (listener),
+ G_SOCKET_ADDRESS (g_unix_socket_address_new (addr)),
+ G_SOCKET_TYPE_STREAM,
+ G_SOCKET_PROTOCOL_DEFAULT,
+ NULL,
+ NULL,
+ error))
+ g_warning ("failed to add address");
+ }
+#endif
+
+end:
+ g_free (addr);
+ return listener;
+}
+
+void
+spice_controller_listener_accept_async (GObject *listener,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail(G_IS_OBJECT(listener));
+
+#ifdef G_OS_WIN32
+ spice_named_pipe_listener_accept_async (SPICE_NAMED_PIPE_LISTENER (listener), cancellable, callback, user_data);
+#else
+ g_socket_listener_accept_async (G_SOCKET_LISTENER (listener), cancellable, callback, user_data);
+#endif
+}
+
+GIOStream*
+spice_controller_listener_accept_finish (GObject *listener,
+ GAsyncResult *result,
+ GObject **source_object,
+ GError **error)
+{
+ g_return_val_if_fail(G_IS_OBJECT(listener), NULL);
+
+#ifdef G_OS_WIN32
+ SpiceNamedPipeConnection *np;
+ np = spice_named_pipe_listener_accept_finish (SPICE_NAMED_PIPE_LISTENER (listener), result, source_object, error);
+ if (np)
+ return G_IO_STREAM (np);
+#else
+ GSocketConnection *socket;
+ socket = g_socket_listener_accept_finish (G_SOCKET_LISTENER (listener), result, source_object, error);
+ if (socket)
+ return G_IO_STREAM (socket);
+#endif
+
+ return NULL;
+}
diff --git a/src/controller/spice-controller-listener.h b/src/controller/spice-controller-listener.h
new file mode 100644
index 0000000..a50bdea
--- /dev/null
+++ b/src/controller/spice-controller-listener.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 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_CONTROLLER_LISTENER_H__
+#define __SPICE_CONTROLLER_LISTENER_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define SPICE_CONTROLLER_LISTENER_ERROR spice_controller_listener_error_quark ()
+GQuark spice_controller_listener_error_quark (void);
+
+typedef enum
+{
+ SPICE_CONTROLLER_LISTENER_ERROR_VALUE /* incorrect value */
+} SpiceControllerListenerError;
+
+
+GObject* spice_controller_listener_new (const gchar *address, GError **error);
+
+void spice_controller_listener_accept_async (GObject *listener,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+GIOStream* spice_controller_listener_accept_finish (GObject *listener,
+ GAsyncResult *result,
+ GObject **source_object,
+ GError **error);
+G_END_DECLS
+
+#endif /* __SPICE_CONTROLLER_LISTENER_H__ */
diff --git a/src/controller/spice-foreign-menu-listener.c b/src/controller/spice-foreign-menu-listener.c
new file mode 100644
index 0000000..5e62606
--- /dev/null
+++ b/src/controller/spice-foreign-menu-listener.c
@@ -0,0 +1,161 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 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 <glib/gstdio.h>
+
+#include "spice-foreign-menu-listener.h"
+
+#ifdef G_OS_WIN32
+#include <windows.h>
+#include "namedpipe.h"
+#include "namedpipelistener.h"
+#include "win32-util.h"
+#endif
+
+#ifdef G_OS_UNIX
+#include <gio/gunixsocketaddress.h>
+#endif
+
+/**
+ * SpiceForeignMenuListenerError:
+ * @SPICE_FOREIGN_MENU_LISTENER_ERROR_VALUE: invalid value.
+ *
+ * Possible errors of foreign menu listener related functions.
+ **/
+
+/**
+ * SPICE_FOREIGN_MENU_LISTENER_ERROR:
+ *
+ * The error domain of the foreign menu listener subsystem.
+ **/
+GQuark
+spice_foreign_menu_listener_error_quark (void)
+{
+ return g_quark_from_static_string ("spice-foreign-menu-listener-error");
+}
+
+GObject*
+spice_foreign_menu_listener_new (const gchar *address, GError **error)
+{
+ GObject *listener = NULL;
+ gchar *addr = NULL;
+
+ g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+ addr = g_strdup (address);
+
+#ifdef G_OS_WIN32
+ if (addr == NULL)
+ addr = g_strdup (g_getenv ("SPICE_FOREIGN_MENU_NAMEDPIPE"));
+ if (addr == NULL)
+ addr = g_strdup_printf ("\\\\.\\pipe\\SpiceForeignMenu-%" G_GUINT64_FORMAT, (guint64)GetCurrentProcessId ());
+#else
+ if (addr == NULL)
+ addr = g_strdup (g_getenv ("SPICE_FOREIGN_MENU_SOCKET"));
+ if (addr == NULL)
+ addr = g_strdup_printf ("/tmp/SpiceForeignMenu-%" G_GUINT64_FORMAT ".uds", (guint64)getpid ());
+#endif
+ if (addr == NULL) {
+ g_set_error (error,
+ SPICE_FOREIGN_MENU_LISTENER_ERROR,
+ SPICE_FOREIGN_MENU_LISTENER_ERROR_VALUE,
+#ifdef G_OS_WIN32
+ "Missing namedpipe address"
+#else
+ "Missing socket address"
+#endif
+ );
+ goto end;
+ }
+
+ g_unlink (addr);
+
+#ifdef G_OS_WIN32
+ {
+ SpiceNamedPipe *np;
+
+ listener = G_OBJECT (spice_named_pipe_listener_new ());
+
+ np = spice_win32_user_pipe_new (addr, error);
+ if (!np) {
+ g_object_unref (listener);
+ listener = NULL;
+ goto end;
+ }
+
+ spice_named_pipe_listener_add_named_pipe (SPICE_NAMED_PIPE_LISTENER (listener), np);
+ }
+#else
+ {
+ listener = G_OBJECT (g_socket_listener_new ());
+
+ if (!g_socket_listener_add_address (G_SOCKET_LISTENER (listener),
+ G_SOCKET_ADDRESS (g_unix_socket_address_new (addr)),
+ G_SOCKET_TYPE_STREAM,
+ G_SOCKET_PROTOCOL_DEFAULT,
+ NULL,
+ NULL,
+ error))
+ g_warning ("failed to add address");
+ }
+#endif
+
+end:
+ g_free (addr);
+ return listener;
+}
+
+void
+spice_foreign_menu_listener_accept_async (GObject *listener,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail(G_IS_OBJECT(listener));
+
+#ifdef G_OS_WIN32
+ spice_named_pipe_listener_accept_async (SPICE_NAMED_PIPE_LISTENER (listener), cancellable, callback, user_data);
+#else
+ g_socket_listener_accept_async (G_SOCKET_LISTENER (listener), cancellable, callback, user_data);
+#endif
+}
+
+GIOStream*
+spice_foreign_menu_listener_accept_finish (GObject *listener,
+ GAsyncResult *result,
+ GObject **source_object,
+ GError **error)
+{
+ g_return_val_if_fail(G_IS_OBJECT(listener), NULL);
+
+#ifdef G_OS_WIN32
+ SpiceNamedPipeConnection *np;
+ np = spice_named_pipe_listener_accept_finish (SPICE_NAMED_PIPE_LISTENER (listener), result, source_object, error);
+ if (np)
+ return G_IO_STREAM (np);
+#else
+ GSocketConnection *socket;
+ socket = g_socket_listener_accept_finish (G_SOCKET_LISTENER (listener), result, source_object, error);
+ if (socket)
+ return G_IO_STREAM (socket);
+#endif
+
+ return NULL;
+}
diff --git a/src/controller/spice-foreign-menu-listener.h b/src/controller/spice-foreign-menu-listener.h
new file mode 100644
index 0000000..1071528
--- /dev/null
+++ b/src/controller/spice-foreign-menu-listener.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 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_FOREIGN_MENU_LISTENER_H__
+#define __SPICE_FOREIGN_MENU_LISTENER_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define SPICE_FOREIGN_MENU_LISTENER_ERROR spice_foreign_menu_listener_error_quark ()
+GQuark spice_foreign_menu_listener_error_quark (void);
+
+typedef enum
+{
+ SPICE_FOREIGN_MENU_LISTENER_ERROR_VALUE /* incorrect value */
+} SpiceForeignMenuListenerError;
+
+
+GObject* spice_foreign_menu_listener_new (const gchar *address, GError **error);
+
+void spice_foreign_menu_listener_accept_async (GObject *listener,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+GIOStream* spice_foreign_menu_listener_accept_finish (GObject *listener,
+ GAsyncResult *result,
+ GObject **source_object,
+ GError **error);
+G_END_DECLS
+
+#endif /* __SPICE_FOREIGN_MENU_LISTENER_H__ */
diff --git a/src/controller/test.c b/src/controller/test.c
new file mode 100644
index 0000000..c08fe21
--- /dev/null
+++ b/src/controller/test.c
@@ -0,0 +1,292 @@
+/* Copyright (C) 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 <stdio.h>
+#include <stdint.h>
+#include <spice/controller_prot.h>
+
+#include "spice-controller.h"
+
+#ifdef WIN32
+#include <windows.h>
+#define PIPE_NAME TEXT("\\\\.\\pipe\\SpiceController-%lu")
+static HANDLE pipe = INVALID_HANDLE_VALUE;
+#else
+
+#include <sys/socket.h>
+#ifdef HAVE_SYS_TYPES_H
+#include <sys/types.h>
+#endif
+#include <sys/un.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+
+#define PIPE_NAME "/tmp/test"
+static int sock = -1;
+
+#endif
+
+#define PIPE_NAME_MAX_LEN 256
+
+void write_to_pipe (const void* data, size_t len)
+{
+#ifdef WIN32
+ DWORD written;
+ if (!WriteFile (pipe, data, len, &written, NULL) || written != len) {
+ printf("Write to pipe failed %u\n", GetLastError());
+ }
+#else
+ if (send (sock, data, len, 0) != len) {
+ printf ("send failed, (%d) %s\n", errno, strerror(errno));
+ }
+#endif
+}
+
+gboolean send_init (void)
+{
+ ControllerInit msg = {
+ { CONTROLLER_MAGIC, CONTROLLER_VERSION, sizeof (msg) },
+ 0,
+ CONTROLLER_FLAG_EXCLUSIVE
+ };
+
+ write_to_pipe(&msg, sizeof (msg));
+ return FALSE;
+}
+
+void send_msg (uint32_t id)
+{
+ ControllerMsg msg = {
+ id, sizeof (msg)
+ };
+
+ write_to_pipe (&msg, sizeof (msg));
+}
+
+void send_value (uint32_t id, uint32_t value)
+{
+ ControllerValue msg = {
+ { id, sizeof(msg) },
+ value
+ };
+
+ write_to_pipe (&msg, sizeof (msg));
+}
+
+void send_data (uint32_t id, uint8_t* data, size_t data_size)
+{
+ size_t size = sizeof (ControllerData) + data_size;
+ ControllerData* msg = (ControllerData*)g_malloc0 (size);
+
+ msg->base.id = id;
+ msg->base.size = (uint32_t)size;
+ memcpy (msg->data, data, data_size);
+ write_to_pipe (msg, size);
+ g_free (msg);
+}
+
+ssize_t read_from_pipe (void* data, size_t size)
+{
+ ssize_t read;
+#ifdef WIN32
+ DWORD bytes;
+ if (!ReadFile (pipe, data, size, &bytes, NULL)) {
+ printf ("Read from pipe failed %u\n", GetLastError());
+ }
+ read = bytes;
+#else
+ read = recv (sock, data, size, 0);
+ if ((read == -1 || read == 0)) {
+ printf ("recv failed, (%d) %s\n", errno, strerror (errno));
+ }
+#endif
+ return read;
+}
+
+#define HOST "localhost"
+#define PORT 5931
+#define SPORT 0
+#define PWD "P@s5w0rd"
+#define SECURE_CHANNELS "main,inputs,playback"
+#define DISABLED_CHANNELS "playback,record"
+#define TITLE "Hello from controller"
+#define HOTKEYS "toggle-fullscreen=shift+f1,release-cursor=shift+f2"
+#define MENU "0\r4864\rS&end Ctrl+Alt+Del\tCtrl+Alt+End\r0\r\n" \
+ "0\r5120\r&Toggle full screen\tShift+F11\r0\r\n" \
+ "0\r1\r&Special keys\r4\r\n" \
+ "1\r5376\r&Send Shift+F11\r0\r\n" \
+ "1\r5632\r&Send Shift+F12\r0\r\n" \
+ "1\r5888\r&Send Ctrl+Alt+End\r0\r\n" \
+ "0\r1\r-\r1\r\n" \
+ "0\r2\rChange CD\r4\r\n" \
+ "2\r3\rNo CDs\r0\r\n" \
+ "2\r4\r[Eject]\r0\r\n" \
+ "0\r5\r-\r1\r\n" \
+ "0\r6\rPlay\r0\r\n" \
+ "0\r7\rSuspend\r0\r\n" \
+ "0\r8\rStop\r0\r\n"
+
+#define TLS_CIPHERS "TLS_C1PHERS"
+#define CA_FILE "C@_FILE"
+#define HOST_SUBJECT "Host_SUBJ3CT"
+
+SpiceCtrlController *ctrl;
+GMainLoop *loop;
+
+void signaled (GObject *gobject, const gchar *signal_name)
+{
+ g_message ("signaled: %s", signal_name);
+ if (g_str_equal (signal_name, "hide")) {
+ spice_ctrl_controller_menu_item_click_msg (ctrl, 42);
+ g_timeout_add (1000, (GSourceFunc)g_main_loop_quit, loop);
+ }
+}
+
+void notified (GObject *gobject, GParamSpec *pspec,
+ gpointer user_data)
+{
+ GValue value = { 0, };
+ GValue strvalue = { 0, };
+
+ g_return_if_fail (gobject != NULL);
+ g_return_if_fail (pspec != NULL);
+
+ g_value_init (&value, pspec->value_type);
+ g_value_init (&strvalue, G_TYPE_STRING);
+ g_object_get_property (gobject, pspec->name, &value);
+
+ if (pspec->value_type == G_TYPE_STRV) {
+ gchar** p = (gchar **)g_value_get_boxed (&value);
+ g_message ("notify::%s == ", pspec->name);
+ while (*p)
+ g_message ("%s", *p++);
+ } else if (G_TYPE_IS_OBJECT(pspec->value_type)) {
+ GObject *o = g_value_get_object (&value);
+ g_message ("notify::%s == %s", pspec->name, o ? G_OBJECT_TYPE_NAME (o) : "null");
+ } else {
+ g_value_transform (&value, &strvalue);
+ g_message ("notify::%s = %s", pspec->name, g_value_get_string (&strvalue));
+ }
+
+ g_value_unset (&value);
+ g_value_unset (&strvalue);
+}
+
+void connect_signals (gpointer obj)
+{
+ guint i, n_ids = 0;
+ guint *ids = NULL;
+ GType type = G_OBJECT_TYPE (obj);
+
+ ids = g_signal_list_ids (type, &n_ids);
+ for (i = 0; i < n_ids; i++) {
+ const gchar *name = g_signal_name (ids[i]);
+ g_signal_connect (obj, name, G_CALLBACK (signaled), (gpointer)name);
+ }
+}
+
+int main (int argc, char *argv[])
+{
+#ifdef WIN32
+ int spicec_pid = (argc > 1 ? atoi (argv[1]) : 0);
+#endif
+ char* host = (argc > 2 ? argv[2] : (char*)HOST);
+ int port = (argc > 3 ? atoi (argv[3]) : PORT);
+ char pipe_name[PIPE_NAME_MAX_LEN];
+ ControllerValue msg;
+ ssize_t read;
+
+#if !GLIB_CHECK_VERSION(2,36,0)
+ g_type_init ();
+#endif
+ ctrl = spice_ctrl_controller_new ();
+ loop = g_main_loop_new (NULL, FALSE);
+ g_signal_connect (ctrl, "notify", G_CALLBACK (notified), NULL);
+ connect_signals (ctrl);
+
+#ifdef WIN32
+ snprintf (pipe_name, PIPE_NAME_MAX_LEN, PIPE_NAME, spicec_pid);
+ spice_ctrl_controller_listen (ctrl, pipe_name, NULL, NULL);
+
+ printf ("Creating Spice controller connection %s\n", pipe_name);
+ pipe = CreateFile (pipe_name, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
+ if (pipe == INVALID_HANDLE_VALUE) {
+ printf ("Could not open pipe %u\n", GetLastError());
+ return -1;
+ }
+#else
+ spice_ctrl_controller_listen (ctrl, PIPE_NAME, NULL, NULL);
+
+ snprintf (pipe_name, PIPE_NAME_MAX_LEN, PIPE_NAME);
+ printf ("Creating a controller connection %s\n", pipe_name);
+ struct sockaddr_un remote;
+ if ((sock = socket (AF_UNIX, SOCK_STREAM, 0)) == -1) {
+ printf ("Could not open socket, (%d) %s\n", errno, strerror(errno));
+ return -1;
+ }
+ remote.sun_family = AF_UNIX;
+ strcpy (remote.sun_path, pipe_name);
+ if (connect (sock, (struct sockaddr *)&remote,
+ strlen (remote.sun_path) + sizeof(remote.sun_family)) == -1) {
+ printf ("Socket connect failed, (%d) %s\n", errno, strerror(errno));
+ close (sock);
+ return -1;
+ }
+#endif
+
+ /* TODO: we rely on socket / pipe buffer... which is lame :) */
+ send_init ();
+
+ send_data (CONTROLLER_HOST, (uint8_t*)host, strlen(host) + 1);
+ send_value (CONTROLLER_PORT, port);
+ send_value (CONTROLLER_SPORT, SPORT);
+ send_data (CONTROLLER_PASSWORD, (uint8_t*)PWD, strlen(PWD) + 1);
+ send_data (CONTROLLER_SECURE_CHANNELS, (uint8_t*)SECURE_CHANNELS, strlen(SECURE_CHANNELS) + 1);
+ send_data (CONTROLLER_DISABLE_CHANNELS, (uint8_t*)DISABLED_CHANNELS, strlen(DISABLED_CHANNELS) + 1);
+ send_data (CONTROLLER_TLS_CIPHERS, (uint8_t*)TLS_CIPHERS, sizeof(TLS_CIPHERS) + 1);
+ send_data (CONTROLLER_CA_FILE, (uint8_t*)CA_FILE, strlen(CA_FILE) + 1);
+ send_data (CONTROLLER_HOST_SUBJECT, (uint8_t*)HOST_SUBJECT, strlen(HOST_SUBJECT) + 1);
+ send_data (CONTROLLER_SET_TITLE, (uint8_t*)TITLE, strlen(TITLE) + 1);
+ send_data (CONTROLLER_HOTKEYS, (uint8_t*)HOTKEYS, strlen(HOTKEYS) + 1);
+ send_data (CONTROLLER_CREATE_MENU, (uint8_t*)MENU, strlen(MENU));
+
+ send_value (CONTROLLER_FULL_SCREEN, /*CONTROLLER_SET_FULL_SCREEN |*/ CONTROLLER_AUTO_DISPLAY_RES);
+
+ send_msg (CONTROLLER_SHOW);
+ send_msg (CONTROLLER_CONNECT);
+ send_msg (CONTROLLER_SHOW);
+ send_msg (CONTROLLER_DELETE_MENU);
+ send_msg (CONTROLLER_HIDE);
+
+ g_main_loop_run (loop);
+
+ while ((read = read_from_pipe (&msg, sizeof(msg))) == sizeof(msg)) {
+ printf ("Received id %u, size %u, value %u\n", msg.base.id, msg.base.size, msg.value);
+ if (msg.value == 42)
+ break;
+ }
+
+#ifdef WIN32
+ CloseHandle (pipe);
+#else
+ close (sock);
+#endif
+ g_object_unref (ctrl);
+ return 0;
+}
diff --git a/src/controller/util.vala b/src/controller/util.vala
new file mode 100644
index 0000000..acd677e
--- /dev/null
+++ b/src/controller/util.vala
@@ -0,0 +1,42 @@
+// Copyright (C) 2012 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/>.
+
+namespace SpiceCtrl {
+
+ public async void input_stream_read (InputStream stream, uint8[] buffer) throws GLib.IOError {
+ var length = buffer.length;
+ ssize_t i = 0;
+
+ while (i < length) {
+ var n = yield stream.read_async (buffer[i:length]);
+ if (n == 0)
+ throw new GLib.IOError.CLOSED ("closed stream") ;
+ i += n;
+ }
+ }
+
+ public async void output_stream_write (OutputStream stream, owned uint8[] buffer) throws GLib.IOError {
+ var length = buffer.length;
+ ssize_t i = 0;
+
+ while (i < length) {
+ var n = yield stream.write_async (buffer[i:length]);
+ if (n == 0)
+ throw new GLib.IOError.CLOSED ("closed stream") ;
+ i += n;
+ }
+ }
+
+}
diff --git a/src/controller/win32-util.c b/src/controller/win32-util.c
new file mode 100644
index 0000000..c3e0400
--- /dev/null
+++ b/src/controller/win32-util.c
@@ -0,0 +1,161 @@
+/*
+ Copyright (C) 2012 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 "win32-util.h"
+#include <windows.h>
+#include <sddl.h>
+#include <aclapi.h>
+
+gboolean
+spice_win32_set_low_integrity (void* handle, GError **error)
+{
+ g_return_val_if_fail (handle != NULL, FALSE);
+ g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+ /* see also http://msdn.microsoft.com/en-us/library/bb625960.aspx */
+ PSECURITY_DESCRIPTOR psd = NULL;
+ PACL psacl = NULL;
+ BOOL sacl_present = FALSE;
+ BOOL sacl_defaulted = FALSE;
+ char *emsg;
+ int errsv;
+ gboolean success = FALSE;
+
+ if (!ConvertStringSecurityDescriptorToSecurityDescriptor ("S:(ML;;NW;;;LW)",
+ SDDL_REVISION_1, &psd, NULL))
+ goto failed;
+
+ if (!GetSecurityDescriptorSacl (psd, &sacl_present, &psacl, &sacl_defaulted))
+ goto failed;
+
+ if (SetSecurityInfo (handle, SE_KERNEL_OBJECT, LABEL_SECURITY_INFORMATION,
+ NULL, NULL, NULL, psacl) != ERROR_SUCCESS)
+ goto failed;
+
+ success = TRUE;
+ goto end;
+
+failed:
+ errsv = GetLastError ();
+ emsg = g_win32_error_message (errsv);
+ g_set_error (error, G_IO_ERROR,
+ g_io_error_from_win32_error (errsv),
+ "Error setting integrity: %s",
+ emsg);
+ g_free (emsg);
+
+end:
+ if (psd != NULL)
+ LocalFree (psd);
+
+ return success;
+}
+
+static gboolean
+get_user_security_attributes (SECURITY_ATTRIBUTES* psa, SECURITY_DESCRIPTOR* psd, PACL* ppdacl)
+{
+ EXPLICIT_ACCESS ea;
+ TRUSTEE trst;
+ DWORD ret = 0;
+
+ ZeroMemory (psa, sizeof (*psa));
+ ZeroMemory (psd, sizeof (*psd));
+ psa->nLength = sizeof (*psa);
+ psa->bInheritHandle = FALSE;
+ psa->lpSecurityDescriptor = psd;
+
+ ZeroMemory (&trst, sizeof (trst));
+ trst.pMultipleTrustee = NULL;
+ trst.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE;
+ trst.TrusteeForm = TRUSTEE_IS_NAME;
+ trst.TrusteeType = TRUSTEE_IS_USER;
+ trst.ptstrName = "CURRENT_USER";
+
+ ZeroMemory (&ea, sizeof (ea));
+ ea.grfAccessPermissions = GENERIC_WRITE | GENERIC_READ;
+ ea.grfAccessMode = SET_ACCESS;
+ ea.grfInheritance = NO_INHERITANCE;
+ ea.Trustee = trst;
+
+ ret = SetEntriesInAcl (1, &ea, NULL, ppdacl);
+ if (ret != ERROR_SUCCESS)
+ return FALSE;
+
+ if (!InitializeSecurityDescriptor (psd, SECURITY_DESCRIPTOR_REVISION))
+ return FALSE;
+
+ if (!SetSecurityDescriptorDacl (psd, TRUE, *ppdacl, FALSE))
+ return FALSE;
+
+ return TRUE;
+}
+
+#define DEFAULT_PIPE_BUF_SIZE 4096
+
+SpiceNamedPipe*
+spice_win32_user_pipe_new (gchar *name, GError **error)
+{
+ SECURITY_ATTRIBUTES sa;
+ SECURITY_DESCRIPTOR sd;
+ PACL dacl = NULL;
+ HANDLE pipe;
+ SpiceNamedPipe *np = NULL;
+
+ g_return_val_if_fail (name != NULL, NULL);
+ g_return_val_if_fail (error != NULL, NULL);
+
+ if (!get_user_security_attributes (&sa, &sd, &dacl))
+ return NULL;
+
+ pipe = CreateNamedPipe (name,
+ PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED |
+ /* FIXME: why is FILE_FLAG_FIRST_PIPE_INSTANCE needed for WRITE_DAC
+ * (apparently needed by SetSecurityInfo). This will prevent
+ * multiple pipe listener....?! */
+ FILE_FLAG_FIRST_PIPE_INSTANCE | WRITE_DAC,
+ PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
+ PIPE_UNLIMITED_INSTANCES,
+ DEFAULT_PIPE_BUF_SIZE, DEFAULT_PIPE_BUF_SIZE,
+ 0, &sa);
+
+ if (pipe == INVALID_HANDLE_VALUE) {
+ int errsv = GetLastError ();
+ gchar *emsg = g_win32_error_message (errsv);
+
+ g_set_error (error,
+ G_IO_ERROR,
+ g_io_error_from_win32_error (errsv),
+ "Error CreateNamedPipe(): %s",
+ emsg);
+
+ g_free (emsg);
+ goto end;
+ }
+
+ /* lower integrity on Vista/Win7+ */
+ if ((LOBYTE (g_win32_get_windows_version()) > 0x05) &&
+ !spice_win32_set_low_integrity (pipe, error))
+ goto end;
+
+ np = SPICE_NAMED_PIPE (g_initable_new (SPICE_TYPE_NAMED_PIPE,
+ NULL, error, "handle", pipe, NULL));
+
+end:
+ LocalFree (dacl);
+
+ return np;
+}
diff --git a/src/controller/win32-util.h b/src/controller/win32-util.h
new file mode 100644
index 0000000..b24ac77
--- /dev/null
+++ b/src/controller/win32-util.h
@@ -0,0 +1,30 @@
+/*
+ Copyright (C) 2012 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 __WIN32_UTIL_H__
+#define __WIN32_UTIL_H__
+
+#include <gio/gio.h>
+#include "namedpipe.h"
+
+G_BEGIN_DECLS
+
+gboolean spice_win32_set_low_integrity (void* handle, GError **error);
+SpiceNamedPipe* spice_win32_user_pipe_new (gchar *name, GError **error);
+
+G_END_DECLS
+
+#endif /* __WIN32_UTIL_H__ */
diff --git a/src/coroutine.h b/src/coroutine.h
new file mode 100644
index 0000000..78dc467
--- /dev/null
+++ b/src/coroutine.h
@@ -0,0 +1,83 @@
+/*
+ * GTK VNC Widget
+ *
+ * Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws>
+ *
+ * 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.0 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef _COROUTINE_H_
+#define _COROUTINE_H_
+
+#include "config.h"
+
+#if WITH_UCONTEXT
+#include "continuation.h"
+#elif WITH_WINFIBER
+#include <windows.h>
+#else
+#include <glib.h>
+#endif
+
+struct coroutine
+{
+ size_t stack_size;
+ void *(*entry)(void *);
+ int (*release)(struct coroutine *);
+
+ /* read-only */
+ int exited;
+
+ /* private */
+ struct coroutine *caller;
+ void *data;
+
+#if WITH_UCONTEXT
+ struct continuation cc;
+#elif WITH_WINFIBER
+ LPVOID fiber;
+ int ret;
+#else
+ GThread *thread;
+ gboolean runnable;
+#endif
+};
+
+void coroutine_init(struct coroutine *co);
+
+int coroutine_release(struct coroutine *co);
+
+void *coroutine_swap(struct coroutine *from, struct coroutine *to, void *arg);
+
+struct coroutine *coroutine_self(void);
+
+void *coroutine_yieldto(struct coroutine *to, void *arg);
+
+void *coroutine_yield(void *arg);
+
+gboolean coroutine_is_main(struct coroutine *co);
+
+static inline gboolean coroutine_self_is_main(void) {
+ return coroutine_self() == NULL || coroutine_is_main(coroutine_self());
+}
+
+#endif
+/*
+ * Local variables:
+ * c-indent-level: 8
+ * c-basic-offset: 8
+ * tab-width: 8
+ * End:
+ */
diff --git a/src/coroutine_gthread.c b/src/coroutine_gthread.c
new file mode 100644
index 0000000..b0098fa
--- /dev/null
+++ b/src/coroutine_gthread.c
@@ -0,0 +1,170 @@
+/*
+ * GTK VNC Widget
+ *
+ * Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws>
+ *
+ * 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.0 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "config.h"
+
+#include "coroutine.h"
+#include <stdio.h>
+#include <stdlib.h>
+
+static GCond *run_cond;
+static GMutex *run_lock;
+static struct coroutine *current;
+static struct coroutine leader;
+
+#if 0
+#define CO_DEBUG(OP) fprintf(stderr, "%s %p %s %d\n", OP, g_thread_self(), __FUNCTION__, __LINE__)
+#else
+#define CO_DEBUG(OP)
+#endif
+
+static void coroutine_system_init(void)
+{
+ if (!g_thread_supported()) {
+ CO_DEBUG("INIT");
+ g_thread_init(NULL);
+ }
+
+
+ run_cond = g_cond_new();
+ run_lock = g_mutex_new();
+ CO_DEBUG("LOCK");
+ g_mutex_lock(run_lock);
+
+ /* The thread that creates the first coroutine is the system coroutine
+ * so let's fill out a structure for it */
+ leader.entry = NULL;
+ leader.release = NULL;
+ leader.stack_size = 0;
+ leader.exited = 0;
+ leader.thread = g_thread_self();
+ leader.runnable = TRUE; /* we're the one running right now */
+ leader.caller = NULL;
+ leader.data = NULL;
+
+ current = &leader;
+}
+
+static gpointer coroutine_thread(gpointer opaque)
+{
+ struct coroutine *co = opaque;
+ CO_DEBUG("LOCK");
+ g_mutex_lock(run_lock);
+ while (!co->runnable) {
+ CO_DEBUG("WAIT");
+ g_cond_wait(run_cond, run_lock);
+ }
+
+ CO_DEBUG("RUNNABLE");
+ current = co;
+ co->caller->data = co->entry(co->data);
+ co->exited = 1;
+
+ co->caller->runnable = TRUE;
+ CO_DEBUG("BROADCAST");
+ g_cond_broadcast(run_cond);
+ CO_DEBUG("UNLOCK");
+ g_mutex_unlock(run_lock);
+
+ return NULL;
+}
+
+void coroutine_init(struct coroutine *co)
+{
+ GError *err = NULL;
+
+ if (run_cond == NULL)
+ coroutine_system_init();
+
+ CO_DEBUG("NEW");
+ co->thread = g_thread_create_full(coroutine_thread, co, co->stack_size,
+ FALSE, TRUE,
+ G_THREAD_PRIORITY_NORMAL,
+ &err);
+ if (err != NULL)
+ g_error("g_thread_create_full() failed: %s", err->message);
+
+ co->exited = 0;
+ co->runnable = FALSE;
+ co->caller = NULL;
+}
+
+int coroutine_release(struct coroutine *co G_GNUC_UNUSED)
+{
+ return 0;
+}
+
+void *coroutine_swap(struct coroutine *from, struct coroutine *to, void *arg)
+{
+ from->runnable = FALSE;
+ to->runnable = TRUE;
+ to->data = arg;
+ to->caller = from;
+ CO_DEBUG("BROADCAST");
+ g_cond_broadcast(run_cond);
+ CO_DEBUG("UNLOCK");
+ g_mutex_unlock(run_lock);
+ CO_DEBUG("LOCK");
+ g_mutex_lock(run_lock);
+ while (!from->runnable) {
+ CO_DEBUG("WAIT");
+ g_cond_wait(run_cond, run_lock);
+ }
+ current = from;
+ to->caller = NULL;
+
+ CO_DEBUG("SWAPPED");
+ return from->data;
+}
+
+struct coroutine *coroutine_self(void)
+{
+ if (run_cond == NULL)
+ coroutine_system_init();
+
+ return current;
+}
+
+void *coroutine_yieldto(struct coroutine *to, void *arg)
+{
+ g_return_val_if_fail(!to->caller, NULL);
+ g_return_val_if_fail(!to->exited, NULL);
+
+ CO_DEBUG("SWAP");
+ return coroutine_swap(coroutine_self(), to, arg);
+}
+
+void *coroutine_yield(void *arg)
+{
+ struct coroutine *to = coroutine_self()->caller;
+ if (!to) {
+ fprintf(stderr, "Co-routine is yielding to no one\n");
+ abort();
+ }
+
+ CO_DEBUG("SWAP");
+ coroutine_self()->caller = NULL;
+ return coroutine_swap(coroutine_self(), to, arg);
+}
+
+gboolean coroutine_is_main(struct coroutine *co)
+{
+ return (co == &leader);
+}
diff --git a/src/coroutine_ucontext.c b/src/coroutine_ucontext.c
new file mode 100644
index 0000000..d709a33
--- /dev/null
+++ b/src/coroutine_ucontext.c
@@ -0,0 +1,150 @@
+/*
+ * GTK VNC Widget
+ *
+ * Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws>
+ *
+ * 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.0 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "config.h"
+#include <glib.h>
+
+#ifdef HAVE_SYS_TYPES_H
+#include <sys/types.h>
+#endif
+#include <sys/mman.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "coroutine.h"
+
+#ifndef MAP_ANONYMOUS
+# define MAP_ANONYMOUS MAP_ANON
+#endif
+
+int coroutine_release(struct coroutine *co)
+{
+ return cc_release(&co->cc);
+}
+
+static int _coroutine_release(struct continuation *cc)
+{
+ struct coroutine *co = container_of(cc, struct coroutine, cc);
+
+ if (co->release) {
+ int ret = co->release(co);
+ if (ret < 0)
+ return ret;
+ }
+
+ munmap(co->cc.stack, co->cc.stack_size);
+
+ co->caller = NULL;
+
+ return 0;
+}
+
+static void coroutine_trampoline(struct continuation *cc)
+{
+ struct coroutine *co = container_of(cc, struct coroutine, cc);
+ co->data = co->entry(co->data);
+}
+
+void coroutine_init(struct coroutine *co)
+{
+ if (co->stack_size == 0)
+ co->stack_size = 16 << 20;
+
+ co->cc.stack_size = co->stack_size;
+ co->cc.stack = mmap(0, co->stack_size,
+ PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS,
+ -1, 0);
+ if (co->cc.stack == MAP_FAILED)
+ g_error("mmap(%" G_GSIZE_FORMAT ") failed: %s",
+ co->stack_size, g_strerror(errno));
+
+ co->cc.entry = coroutine_trampoline;
+ co->cc.release = _coroutine_release;
+ co->exited = 0;
+
+ cc_init(&co->cc);
+}
+
+#if 0
+static __thread struct coroutine leader;
+static __thread struct coroutine *current;
+#else
+static struct coroutine leader;
+static struct coroutine *current;
+#endif
+
+struct coroutine *coroutine_self(void)
+{
+ if (current == NULL)
+ current = &leader;
+ return current;
+}
+
+void *coroutine_swap(struct coroutine *from, struct coroutine *to, void *arg)
+{
+ int ret;
+ to->data = arg;
+ current = to;
+ ret = cc_swap(&from->cc, &to->cc);
+ if (ret == 0)
+ return from->data;
+ else if (ret == 1) {
+ coroutine_release(to);
+ current = from;
+ to->exited = 1;
+ return to->data;
+ }
+
+ return NULL;
+}
+
+void *coroutine_yieldto(struct coroutine *to, void *arg)
+{
+ g_return_val_if_fail(!to->caller, NULL);
+ g_return_val_if_fail(!to->exited, NULL);
+
+ to->caller = coroutine_self();
+ return coroutine_swap(coroutine_self(), to, arg);
+}
+
+void *coroutine_yield(void *arg)
+{
+ struct coroutine *to = coroutine_self()->caller;
+ if (!to) {
+ fprintf(stderr, "Co-routine is yielding to no one\n");
+ abort();
+ }
+ coroutine_self()->caller = NULL;
+ return coroutine_swap(coroutine_self(), to, arg);
+}
+
+gboolean coroutine_is_main(struct coroutine *co)
+{
+ return (co == &leader);
+}
+/*
+ * Local variables:
+ * c-indent-level: 8
+ * c-basic-offset: 8
+ * tab-width: 8
+ * End:
+ */
diff --git a/src/coroutine_winfibers.c b/src/coroutine_winfibers.c
new file mode 100644
index 0000000..a56d33d
--- /dev/null
+++ b/src/coroutine_winfibers.c
@@ -0,0 +1,126 @@
+/*
+ * SpiceGtk coroutine with Windows fibers
+ *
+ * Copyright (C) 2011 Marc-André Lureau <marcandre.lureau@redhat.com>
+ *
+ * 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.0 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "config.h"
+#include <stdio.h>
+#include <glib.h>
+
+#include "coroutine.h"
+
+static struct coroutine leader = { 0, };
+static struct coroutine *current = NULL;
+static struct coroutine *caller = NULL;
+
+int coroutine_release(struct coroutine *co)
+{
+ DeleteFiber(co->fiber);
+ return 0;
+}
+
+static void WINAPI coroutine_trampoline(LPVOID lpParameter)
+{
+ struct coroutine *co = (struct coroutine *)lpParameter;
+
+ co->data = co->entry(co->data);
+
+ if (co->release)
+ co->ret = co->release(co);
+ else
+ co->ret = 0;
+
+ co->caller = NULL;
+
+ // and switch back to caller
+ co->ret = 1;
+ SwitchToFiber(caller->fiber);
+}
+
+void coroutine_init(struct coroutine *co)
+{
+ if (leader.fiber == NULL) {
+ leader.fiber = ConvertThreadToFiber(&leader);
+ if (leader.fiber == NULL)
+ g_error("ConvertThreadToFiber() failed");
+ }
+
+ co->exited = 0;
+ co->fiber = CreateFiber(0, &coroutine_trampoline, co);
+ if (co->fiber == NULL)
+ g_error("CreateFiber() failed");
+
+ co->ret = 0;
+}
+
+struct coroutine *coroutine_self(void)
+{
+ if (current == NULL)
+ current = &leader;
+ return current;
+}
+
+void *coroutine_swap(struct coroutine *from, struct coroutine *to, void *arg)
+{
+ to->data = arg;
+ current = to;
+ caller = from;
+ SwitchToFiber(to->fiber);
+ if (to->ret == 0)
+ return from->data;
+ else if (to->ret == 1) {
+ coroutine_release(to);
+ current = &leader;
+ to->exited = 1;
+ return to->data;
+ }
+
+ return NULL;
+}
+
+void *coroutine_yieldto(struct coroutine *to, void *arg)
+{
+ g_return_val_if_fail(!to->caller, NULL);
+ g_return_val_if_fail(!to->exited, NULL);
+
+ to->caller = coroutine_self();
+ return coroutine_swap(coroutine_self(), to, arg);
+}
+
+void *coroutine_yield(void *arg)
+{
+ struct coroutine *to = coroutine_self()->caller;
+ if (!to) {
+ fprintf(stderr, "Co-routine is yielding to no one\n");
+ abort();
+ }
+ coroutine_self()->caller = NULL;
+ return coroutine_swap(coroutine_self(), to, arg);
+}
+
+gboolean coroutine_is_main(struct coroutine *co)
+{
+ return (co == &leader);
+}
+/*
+ * Local variables:
+ * c-indent-level: 8
+ * c-basic-offset: 8
+ * tab-width: 8
+ * End:
+ */
diff --git a/src/decode-glz-tmpl.c b/src/decode-glz-tmpl.c
new file mode 100644
index 0000000..b337a8b
--- /dev/null
+++ b/src/decode-glz-tmpl.c
@@ -0,0 +1,336 @@
+/*
+ Copyright (C) 2009 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/>.
+*/
+
+// External defines: PLT, RGBX/PLTXX/ALPHA, TO_RGB32.
+// If PLT4/1 and TO_RGB32 are defined, we need CAST_PLT_DISTANCE (
+// because then the number of pixels differ from the units used in the compression)
+
+/*
+ For each output pixel type the following macros are defined:
+ OUT_PIXEL - the output pixel type
+ COPY_PIXEL(p, out) - assigns the pixel to the place pointed by out and
+ increases out. Used in RLE.
+ Need special handling because in alpha we copy only
+ the pad byte.
+ COPY_REF_PIXEL(ref, out) - copies the pixel pointed by ref to the pixel pointed by out.
+ Increases ref and out.
+ COPY_COMP_PIXEL(encoder, out) - copies pixel from the compressed buffer to the decompressed
+ buffer. Increases out.
+*/
+
+#if !defined(LZ_RGB_ALPHA)
+#define COPY_PIXEL(p, out) (*(out++) = p)
+#define COPY_REF_PIXEL(ref, out) (*(out++) = *(ref++))
+#endif
+
+// decompressing plt to plt
+#ifdef LZ_PLT
+#ifndef TO_RGB32
+#define OUT_PIXEL one_byte_pixel_t
+#define FNAME(name) glz_plt_##name
+#define COPY_COMP_PIXEL(in, out) {(out)->a = *(in++); out++;}
+#else // TO_RGB32
+#define OUT_PIXEL rgb32_pixel_t
+#define COPY_PLT_ENTRY(ent, out) {\
+ (out)->b = ent; (out)->g = (ent >> 8); (out)->r = (ent >> 16); (out)->pad = 0;}
+#ifdef PLT8
+#define FNAME(name) glz_plt8_to_rgb32_##name
+#define COPY_COMP_PIXEL(in, out, palette) { \
+ uint32_t rgb = palette->ents[*(in++)]; \
+ COPY_PLT_ENTRY(rgb, out); \
+ out++; \
+}
+#elif defined(PLT4_BE)
+#define FNAME(name) glz_plt4_be_to_rgb32_##name
+#define COPY_COMP_PIXEL(in, out, palette){ \
+ uint8_t byte = *(in++); \
+ uint32_t rgb = palette->ents[((byte >> 4) & 0x0f) % (palette->num_ents)]; \
+ COPY_PLT_ENTRY(rgb, out); \
+ out++; \
+ rgb = palette->ents[(byte & 0x0f) % (palette->num_ents)]; \
+ COPY_PLT_ENTRY(rgb, out); \
+ out++; \
+}
+#define CAST_PLT_DISTANCE(dist) (dist*2)
+#elif defined(PLT4_LE)
+#define FNAME(name) glz_plt4_le_to_rgb32_##name
+#define COPY_COMP_PIXEL(in, out, palette){ \
+ uint8_t byte = *(in++); \
+ uint32_t rgb = palette->ents[(byte & 0x0f) % (palette->num_ents)]; \
+ COPY_PLT_ENTRY(rgb, out); \
+ out++; \
+ rgb = palette->ents[((byte >> 4) & 0x0f) % (palette->num_ents)]; \
+ COPY_PLT_ENTRY(rgb, out); \
+ out++; \
+}
+#define CAST_PLT_DISTANCE(dist) (dist*2)
+#elif defined(PLT1_BE) // TODO store palette entries for direct access
+#define FNAME(name) glz_plt1_be_to_rgb32_##name
+#define COPY_COMP_PIXEL(in, out, palette){ \
+ uint8_t byte = *(in++); \
+ int i; \
+ uint32_t fore = palette->ents[1]; \
+ uint32_t back = palette->ents[0]; \
+ for (i = 7; i >= 0; i--) \
+ { \
+ if ((byte >> i) & 1) { \
+ COPY_PLT_ENTRY(fore, out); \
+ } else { \
+ COPY_PLT_ENTRY(back, out); \
+ } \
+ out++; \
+ } \
+}
+#define CAST_PLT_DISTANCE(dist) (dist*8)
+#elif defined(PLT1_LE)
+#define FNAME(name) glz_plt1_le_to_rgb32_##name
+#define COPY_COMP_PIXEL(in, out, palette){ \
+ uint8_t byte = *(in++); \
+ int i; \
+ uint32_t fore = palette->ents[1]; \
+ uint32_t back = palette->ents[0]; \
+ for (i = 0; i < 8; i++) \
+ { \
+ if ((byte >> i) & 1) { \
+ COPY_PLT_ENTRY(fore, out); \
+ } else { \
+ COPY_PLT_ENTRY(back, out); \
+ } \
+ out++; \
+ } \
+}
+#define CAST_PLT_DISTANCE(dist) (dist*8)
+#endif // PLT Type
+#endif // TO_RGB32
+#endif
+
+#ifdef LZ_RGB16
+#ifndef TO_RGB32
+#define OUT_PIXEL rgb16_pixel_t
+#define FNAME(name) glz_rgb16_##name
+#define COPY_COMP_PIXEL(in, out) {*out = (*(in++)) << 8; *out |= *(in++); out++;}
+#else
+#define OUT_PIXEL rgb32_pixel_t
+#define FNAME(name) glz_rgb16_to_rgb32_##name
+#define COPY_COMP_PIXEL(in, out) {out->r = *(in++); out->b= *(in++); \
+ out->g = (((out->r) << 6) | ((out->b) >> 2)) & ~0x07; \
+ out->g |= (out->g >> 5); \
+ out->r = ((out->r << 1) & ~0x07) | ((out->r >> 4) & 0x07) ; \
+ out->b = (out->b << 3) | ((out->b >> 2) & 0x07); \
+ out->pad = 0; \
+ out++; \
+}
+#endif
+#endif
+
+#ifdef LZ_RGB24
+#define OUT_PIXEL rgb24_pixel_t
+#define FNAME(name) glz_rgb24_##name
+#define COPY_COMP_PIXEL(in, out) { \
+ out->b = *(in++); \
+ out->g = *(in++); \
+ out->r = *(in++); \
+ out++; \
+}
+#endif
+
+#ifdef LZ_RGB32
+#define OUT_PIXEL rgb32_pixel_t
+#define FNAME(name) glz_rgb32_##name
+#define COPY_COMP_PIXEL(in, out) { \
+ out->b = *(in++); \
+ out->g = *(in++); \
+ out->r = *(in++); \
+ out->pad = 0; \
+ out++; \
+}
+#endif
+
+#ifdef LZ_RGB_ALPHA
+#define OUT_PIXEL rgb32_pixel_t
+#define FNAME(name) glz_rgb_alpha_##name
+#define COPY_PIXEL(p, out) {out->pad = p.pad; out++;}
+#define COPY_REF_PIXEL(ref, out) {out->pad = ref->pad; out++; ref++;}
+#define COPY_COMP_PIXEL(in, out) {out->pad = *(in++); out++;}
+#endif
+
+// TODO: separate into routines that decode to dist,len. and to a routine that
+// actually copies the data.
+
+/* returns num of bytes read from in buf.
+ size should be in PIXEL */
+static size_t FNAME(decode)(SpiceGlzDecoderWindow *window,
+ uint8_t* in_buf, uint8_t *out_buf, int size,
+ uint64_t image_id, SpicePalette *plt)
+{
+ uint8_t *ip = in_buf;
+ OUT_PIXEL *out_pix_buf = (OUT_PIXEL *)out_buf;
+ OUT_PIXEL *op = out_pix_buf;
+ OUT_PIXEL *op_limit = out_pix_buf + size;
+
+ uint32_t ctrl = *(ip++);
+ int loop = true;
+
+ do {
+ if (ctrl >= MAX_COPY) { // reference (dictionary/RLE)
+ OUT_PIXEL *ref = op;
+ uint32_t len = ctrl >> 5;
+ uint8_t pixel_flag = (ctrl >> 4) & 0x01;
+ uint32_t pixel_ofs = (ctrl & 0x0f);
+ uint8_t image_flag;
+ uint32_t image_dist;
+
+ /* retrieving the referenced images, the offset of the first pixel,
+ and the match length */
+
+ uint8_t code;
+ //len--; // TODO: why do we do this?
+
+ if (len == 7) { // match length is bigger than 7
+ do {
+ code = *(ip++);
+ len += code;
+ } while (code == 255); // remaining of len
+ }
+ code = *(ip++);
+ pixel_ofs += (code << 4);
+
+ code = *(ip++);
+ image_flag = (code >> 6) & 0x03;
+ if (!pixel_flag) { // short pixel offset
+ int i;
+ image_dist = code & 0x3f;
+ for (i = 0; i < image_flag; i++) {
+ code = *(ip++);
+ image_dist += (code << (6 + (8 * i)));
+ }
+ } else {
+ int i;
+ pixel_flag = (code >> 5) & 0x01;
+ pixel_ofs += (code & 0x1f) << 12;
+ image_dist = 0;
+ for (i = 0; i < image_flag; i++) {
+ code = *(ip++);
+ image_dist += (code << 8 * i);
+ }
+
+
+ if (pixel_flag) { // very long pixel offset
+ code = *(ip++);
+ pixel_ofs += code << 17;
+ }
+ }
+
+#if defined(LZ_PLT) || defined(LZ_RGB_ALPHA)
+ len += 2; // length is biased by 2 (fixing bias)
+#elif defined(LZ_RGB16)
+ len += 1; // length is biased by 1 (fixing bias)
+#endif
+ if (!image_dist) {
+ pixel_ofs += 1; // offset is biased by 1 (fixing bias)
+ }
+
+#if defined(TO_RGB32)
+#if defined(PLT4_BE) || defined(PLT4_LE) || defined(PLT1_BE) || defined(PLT1_LE)
+ pixel_ofs = CAST_PLT_DISTANCE(pixel_ofs);
+ len = CAST_PLT_DISTANCE(len);
+#endif
+#endif
+
+ if (!image_dist) { // reference is inside the same image
+ ref -= pixel_ofs;
+ g_return_val_if_fail(ref + len <= op_limit, 0);
+ g_return_val_if_fail(ref >= out_pix_buf, 0);
+ } else {
+ ref = glz_decoder_window_bits(window, image_id,
+ image_dist, pixel_ofs);
+ }
+
+ g_return_val_if_fail(ref != NULL, 0);
+ g_return_val_if_fail(op + len <= op_limit, 0);
+
+ /* copying the match*/
+
+ if (ref == (op - 1)) { // run (this will never be called in PLT4/1_TO_RGB because the
+ // number of pixel copied is larger then one...
+ /* optimize copy for a run */
+ OUT_PIXEL b = *ref;
+ for (; len; --len) {
+ COPY_PIXEL(b, op);
+ g_return_val_if_fail(op <= op_limit, 0);
+ }
+ } else {
+ for (; len; --len) {
+ COPY_REF_PIXEL(ref, op);
+ g_return_val_if_fail(op <= op_limit, 0);
+ }
+ }
+ } else { // copy
+ ctrl++; // copy count is biased by 1
+#if defined(TO_RGB32) && (defined(PLT4_BE) || defined(PLT4_LE) || defined(PLT1_BE) || \
+ defined(PLT1_LE))
+ g_return_val_if_fail(op + CAST_PLT_DISTANCE(ctrl) <= op_limit, 0);
+#else
+ g_return_val_if_fail(op + ctrl <= op_limit, 0);
+#endif
+
+#if defined(TO_RGB32) && defined(LZ_PLT)
+ g_return_val_if_fail(plt, 0);
+ COPY_COMP_PIXEL(ip, op, plt);
+#else
+ COPY_COMP_PIXEL(ip, op);
+#endif
+ g_return_val_if_fail(op <= op_limit, 0);
+
+ for (--ctrl; ctrl; ctrl--) {
+#if defined(TO_RGB32) && defined(LZ_PLT)
+ g_return_val_if_fail(plt, 0);
+ COPY_COMP_PIXEL(ip, op, plt);
+#else
+ COPY_COMP_PIXEL(ip, op);
+#endif
+ g_return_val_if_fail(op <= op_limit, 0);
+ }
+ } // END REF/COPY
+
+ if (LZ_EXPECT_CONDITIONAL(op < op_limit)) {
+ ctrl = *(ip++);
+ } else {
+ loop = false;
+ }
+ } while (LZ_EXPECT_CONDITIONAL(loop));
+
+ return (ip - in_buf);
+}
+#undef LZ_PLT
+#undef PLT8
+#undef PLT4_BE
+#undef PLT4_LE
+#undef PLT1_BE
+#undef PLT1_LE
+#undef LZ_RGB16
+#undef LZ_RGB24
+#undef LZ_RGB32
+#undef LZ_RGB_ALPHA
+#undef TO_RGB32
+#undef OUT_PIXEL
+#undef FNAME
+#undef COPY_PIXEL
+#undef COPY_REF_PIXEL
+#undef COPY_COMP_PIXEL
+#undef COPY_PLT_ENTRY
+#undef CAST_PLT_DISTANCE
diff --git a/src/decode-glz.c b/src/decode-glz.c
new file mode 100644
index 0000000..34a7185
--- /dev/null
+++ b/src/decode-glz.c
@@ -0,0 +1,475 @@
+/* -*- 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 <stdio.h>
+#include <stdbool.h>
+#include <inttypes.h>
+
+#include <glib.h>
+
+#include "gio-coroutine.h"
+#include "spice-util.h"
+#include "decode.h"
+
+#include "common/canvas_utils.h"
+
+struct glz_image_hdr {
+ uint64_t id;
+ LzImageType type;
+ uint32_t width;
+ uint32_t height;
+ uint32_t gross_pixels;
+ bool top_down;
+ uint32_t win_head_dist;
+};
+
+struct glz_image {
+ struct glz_image_hdr hdr;
+ pixman_image_t *surface;
+ uint8_t *data;
+};
+
+static struct glz_image *glz_image_new(struct glz_image_hdr *hdr,
+ int type, void *opaque)
+{
+ struct glz_image *img;
+
+ g_return_val_if_fail(type == LZ_IMAGE_TYPE_RGB32 || type == LZ_IMAGE_TYPE_RGBA, NULL);
+
+ img = g_new0(struct glz_image, 1);
+ img->hdr = *hdr;
+ img->surface = alloc_lz_image_surface
+ (opaque, type == LZ_IMAGE_TYPE_RGBA ? PIXMAN_a8r8g8b8 : PIXMAN_x8r8g8b8,
+ img->hdr.width, img->hdr.height, img->hdr.gross_pixels, img->hdr.top_down);
+ pixman_image_ref(img->surface);
+ img->data = (uint8_t *)pixman_image_get_data(img->surface);
+ if (!img->hdr.top_down) {
+ img->data = img->data - img->hdr.width * (img->hdr.height - 1) * 4;
+ }
+ return img;
+}
+
+static void glz_image_destroy(struct glz_image *img)
+{
+ if (img == NULL)
+ return;
+
+ pixman_image_unref(img->surface);
+ free(img);
+}
+
+/* ------------------------------------------------------------------ */
+
+#define INIT_IMAGES_CAPACITY 100
+#define WIN_OVERFLOW_FACTOR 1.5
+#define WIN_REALLOC_FACTOR 1.5
+
+struct SpiceGlzDecoderWindow {
+ struct glz_image **images;
+ uint32_t nimages;
+ uint64_t oldest;
+ uint64_t tail_gap;
+};
+
+static void glz_decoder_window_resize(SpiceGlzDecoderWindow *w)
+{
+ struct glz_image **new_images;
+ int i, new_slot;
+
+ SPICE_DEBUG("%s: array resize %d -> %d", __FUNCTION__,
+ w->nimages, w->nimages * 2);
+ new_images = g_new0(struct glz_image*, w->nimages * 2);
+ for (i = 0; i < w->nimages; i++) {
+ if (w->images[i] == NULL) {
+ /*
+ * We can have empty slots when images come in out of order, this
+ * can happen when a vm has multiple displays, since each display
+ * uses its own socket there is no guarantee that images
+ * originating from different displays are received in id order.
+ */
+ continue;
+ }
+ new_slot = w->images[i]->hdr.id % (w->nimages * 2);
+ new_images[new_slot] = w->images[i];
+ }
+ free(w->images);
+ w->images = new_images;
+ w->nimages *= 2;
+}
+
+static void glz_decoder_window_add(SpiceGlzDecoderWindow *w,
+ struct glz_image *img)
+{
+ int slot = img->hdr.id % w->nimages;
+
+ if (w->images[slot]) {
+ /* need more space */
+ glz_decoder_window_resize(w);
+ slot = img->hdr.id % w->nimages;
+ }
+
+ w->images[slot] = img;
+
+ /* close the gap */
+ while (w->tail_gap <= img->hdr.id && w->images[w->tail_gap % w->nimages] != NULL)
+ w->tail_gap++;
+}
+
+struct wait_for_image_data {
+ SpiceGlzDecoderWindow *window;
+ uint64_t id;
+};
+
+static gboolean wait_for_image(gpointer data)
+{
+ struct wait_for_image_data *wait = data;
+ int slot = wait->id % wait->window->nimages;
+ struct glz_image *image = wait->window->images[slot];
+ gboolean ready = image && image->hdr.id == wait->id;
+
+ return ready;
+}
+
+static void *glz_decoder_window_bits(SpiceGlzDecoderWindow *w, uint64_t id,
+ uint32_t dist, uint32_t offset)
+{
+ struct wait_for_image_data data = {
+ .window = w,
+ .id = id - dist,
+ };
+
+ if (!g_coroutine_condition_wait(g_coroutine_self(), wait_for_image, &data))
+ SPICE_DEBUG("wait for image cancelled");
+
+ int slot = (id - dist) % w->nimages;
+
+ g_return_val_if_fail(w->images[slot] != NULL, NULL);
+ g_return_val_if_fail(w->images[slot]->hdr.id == id - dist, NULL);
+ g_return_val_if_fail(w->images[slot]->hdr.gross_pixels >= offset, NULL);
+
+ return w->images[slot]->data + offset * 4;
+}
+
+static void glz_decoder_window_release(SpiceGlzDecoderWindow *w,
+ uint64_t oldest)
+{
+ int slot;
+
+ while (w->oldest < oldest) {
+ slot = w->oldest % w->nimages;
+ glz_image_destroy(w->images[slot]);
+ w->images[slot] = NULL;
+ w->oldest++;
+ }
+}
+
+/* ------------------------------------------------------------------ */
+
+typedef struct GlibGlzDecoder {
+ SpiceGlzDecoder base;
+ uint8_t *in_start;
+ uint8_t *in_now;
+ SpiceGlzDecoderWindow *window;
+ struct glz_image_hdr image;
+} GlibGlzDecoder;
+
+/*
+ * Give hints to the compiler for branch prediction optimization.
+ */
+#if defined(__GNUC__) && (__GNUC__ > 2)
+#define LZ_EXPECT_CONDITIONAL(c) (__builtin_expect((c), 1))
+#define LZ_UNEXPECT_CONDITIONAL(c) (__builtin_expect((c), 0))
+#else
+#define LZ_EXPECT_CONDITIONAL(c) (c)
+#define LZ_UNEXPECT_CONDITIONAL(c) (c)
+#endif
+
+
+#ifdef __GNUC__
+#define ATTR_PACKED __attribute__ ((__packed__))
+#else
+#define ATTR_PACKED
+#pragma pack(push)
+#pragma pack(1)
+#endif
+
+/*
+ * the palette images will be treated as one byte pixels. Their width
+ * should be transformed accordingly.
+ */
+typedef struct ATTR_PACKED one_byte_pixel_t {
+ uint8_t a;
+} one_byte_pixel_t;
+
+typedef struct ATTR_PACKED rgb32_pixel_t {
+ uint8_t b;
+ uint8_t g;
+ uint8_t r;
+ uint8_t pad;
+} rgb32_pixel_t;
+
+typedef struct ATTR_PACKED rgb24_pixel_t {
+ uint8_t b;
+ uint8_t g;
+ uint8_t r;
+} rgb24_pixel_t;
+
+typedef uint16_t rgb16_pixel_t;
+
+#ifndef __GNUC__
+#pragma pack(pop)
+#endif
+
+#undef ATTR_PACKED
+
+#define LZ_PLT
+#include "decode-glz-tmpl.c"
+
+#define LZ_PLT
+#define PLT8
+#define TO_RGB32
+#include "decode-glz-tmpl.c"
+
+#define LZ_PLT
+#define PLT4_BE
+#define TO_RGB32
+#include "decode-glz-tmpl.c"
+
+#define LZ_PLT
+#define PLT4_LE
+#define TO_RGB32
+#include "decode-glz-tmpl.c"
+
+#define LZ_PLT
+#define PLT1_BE
+#define TO_RGB32
+#include "decode-glz-tmpl.c"
+
+#define LZ_PLT
+#define PLT1_LE
+#define TO_RGB32
+#include "decode-glz-tmpl.c"
+
+
+#define LZ_RGB16
+#include "decode-glz-tmpl.c"
+#define LZ_RGB16
+#define TO_RGB32
+#include "decode-glz-tmpl.c"
+
+#define LZ_RGB24
+#include "decode-glz-tmpl.c"
+
+#define LZ_RGB32
+#include "decode-glz-tmpl.c"
+
+#define LZ_RGB_ALPHA
+#include "decode-glz-tmpl.c"
+
+#undef LZ_UNEXPECT_CONDITIONAL
+#undef LZ_EXPECT_CONDITIONAL
+
+typedef size_t (*decode_function)(SpiceGlzDecoderWindow *window,
+ uint8_t* in_buf, uint8_t *out_buf, int size,
+ uint64_t id, SpicePalette *plt);
+
+// ordered according to LZ_IMAGE_TYPE
+const decode_function DECODE_TO_RGB32[] = {
+ NULL,
+ glz_plt1_le_to_rgb32_decode,
+ glz_plt1_be_to_rgb32_decode,
+ glz_plt4_le_to_rgb32_decode,
+ glz_plt4_be_to_rgb32_decode,
+ glz_plt8_to_rgb32_decode,
+ glz_rgb16_to_rgb32_decode,
+ glz_rgb32_decode,
+ glz_rgb32_decode,
+ glz_rgb32_decode
+};
+
+const decode_function DECODE_TO_SAME[] = {
+ NULL,
+ glz_plt_decode,
+ glz_plt_decode,
+ glz_plt_decode,
+ glz_plt_decode,
+ glz_plt_decode,
+ glz_rgb16_decode,
+ glz_rgb24_decode,
+ glz_rgb32_decode,
+ glz_rgb32_decode
+};
+
+static uint32_t decode_32(GlibGlzDecoder *d)
+{
+ uint32_t word = 0;
+ word |= *(d->in_now++);
+ word <<= 8;
+ word |= *(d->in_now++);
+ word <<= 8;
+ word |= *(d->in_now++);
+ word <<= 8;
+ word |= *(d->in_now++);
+ return word;
+}
+
+static uint64_t decode_64(GlibGlzDecoder *d)
+{
+ uint64_t long_word = decode_32(d);
+ long_word <<= 32;
+ long_word |= decode_32(d);
+ return long_word;
+}
+
+static void decode_header(GlibGlzDecoder *d)
+{
+ uint32_t magic;
+ uint32_t version;
+ uint32_t stride;
+ uint8_t tmp;
+
+ magic = decode_32(d);
+ g_return_if_fail(magic == LZ_MAGIC);
+
+ version = decode_32(d);
+ g_return_if_fail(version == LZ_VERSION);
+
+ tmp = *(d->in_now++);
+
+ d->image.type = (LzImageType)(tmp & LZ_IMAGE_TYPE_MASK);
+ d->image.top_down = (tmp >> LZ_IMAGE_TYPE_LOG) ? true : false;
+ d->image.width = decode_32(d);
+ d->image.height = decode_32(d);
+ stride = decode_32(d);
+
+ if (IS_IMAGE_TYPE_PLT[d->image.type]) {
+ d->image.gross_pixels = stride * PLT_PIXELS_PER_BYTE[d->image.type]
+ * d->image.height;
+ } else {
+ d->image.gross_pixels = d->image.width * d->image.height;
+ }
+
+ d->image.id = decode_64(d);
+ d->image.win_head_dist = decode_32(d);
+
+ SPICE_DEBUG("%s: %dx%d, id %" PRId64 ", ref %" PRId64,
+ __FUNCTION__,
+ d->image.width, d->image.height, d->image.id,
+ d->image.id - d->image.win_head_dist);
+}
+
+static void decode(SpiceGlzDecoder *decoder,
+ uint8_t *data, SpicePalette *palette,
+ void *usr_data)
+{
+ GlibGlzDecoder *d = SPICE_CONTAINEROF(decoder, GlibGlzDecoder, base);
+ LzImageType decoded_type;
+ struct glz_image *decoded_image;
+ size_t n_in_bytes_decoded;
+
+ d->in_start = data;
+ d->in_now = data;
+
+ decode_header(d);
+
+ if (d->image.type == LZ_IMAGE_TYPE_RGBA) {
+ decoded_type = LZ_IMAGE_TYPE_RGBA;
+ } else {
+ decoded_type = LZ_IMAGE_TYPE_RGB32;
+ }
+
+ decoded_image = glz_image_new(&d->image, decoded_type, usr_data);
+
+ n_in_bytes_decoded = DECODE_TO_RGB32[d->image.type]
+ (d->window, d->in_now, decoded_image->data,
+ d->image.gross_pixels, d->image.id, palette);
+
+ d->in_now += n_in_bytes_decoded;
+
+ if (d->image.type == LZ_IMAGE_TYPE_RGBA) {
+ glz_rgb_alpha_decode(d->window, d->in_now, decoded_image->data,
+ d->image.gross_pixels, d->image.id, palette);
+ }
+
+ glz_decoder_window_add(d->window, decoded_image);
+
+ { /* release old images from last tail_gap, only if the gap is closed */
+ uint64_t oldest;
+ struct glz_image *image = d->window->images[(d->window->tail_gap - 1) % d->window->nimages];
+
+ g_return_if_fail(image != NULL);
+
+ oldest = image->hdr.id - image->hdr.win_head_dist;
+ glz_decoder_window_release(d->window, oldest);
+ }
+}
+
+/* ------------------------------------------------------------------ */
+
+static SpiceGlzDecoderOps glz_decoder_ops = {
+ .decode = decode,
+};
+
+void glz_decoder_window_clear(SpiceGlzDecoderWindow *w)
+{
+ int i;
+
+ g_return_if_fail(w->nimages == 0 || w->images != NULL);
+
+ for (i = 0; i < w->nimages; i++) {
+ if (w->images[i]) {
+ glz_image_destroy(w->images[i]);
+ }
+ }
+
+ w->nimages = 16;
+ g_free(w->images);
+ w->images = g_new0(struct glz_image*, w->nimages);
+ w->tail_gap = 0;
+}
+
+SpiceGlzDecoderWindow *glz_decoder_window_new(void)
+{
+ SpiceGlzDecoderWindow *w = g_new0(SpiceGlzDecoderWindow, 1);
+ glz_decoder_window_clear(w);
+ return w;
+}
+
+void glz_decoder_window_destroy(SpiceGlzDecoderWindow *w)
+{
+ if (w == NULL)
+ return;
+
+ glz_decoder_window_clear(w);
+ free(w->images);
+ free(w);
+}
+
+SpiceGlzDecoder *glz_decoder_new(SpiceGlzDecoderWindow *w)
+{
+ GlibGlzDecoder *d = g_new0(GlibGlzDecoder, 1);
+ d->base.ops = &glz_decoder_ops;
+ d->window = w;
+ return &d->base;
+}
+
+void glz_decoder_destroy(SpiceGlzDecoder *d)
+{
+ free(d);
+}
diff --git a/src/decode-jpeg.c b/src/decode-jpeg.c
new file mode 100644
index 0000000..697d0de
--- /dev/null
+++ b/src/decode-jpeg.c
@@ -0,0 +1,191 @@
+/* -*- 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 "decode.h"
+
+#ifdef G_OS_WIN32
+/* We need some hacks to avoid warnings from the jpeg headers, ex: */
+/* #define HAVE_BOOLEAN */
+#define XMD_H
+/* #undef FAR */
+/* but they are not compatible: uchar vs int........!@@(#$$??!@! */
+/* fix this with UGLY HACK! */
+/* #define boolean spice_jpeg_boolean */
+/* #define INT32 spice_jpeg_int32 */
+#endif
+
+#include <stdio.h>
+#include <jpeglib.h>
+
+typedef struct GlibJpegDecoder
+{
+ SpiceJpegDecoder base;
+ struct jpeg_decompress_struct _cinfo;
+ struct jpeg_error_mgr _jerr;
+ struct jpeg_source_mgr _jsrc;
+
+ uint8_t* _data;
+ int _data_size;
+ int _width;
+ int _height;
+} GlibJpegDecoder;
+
+static void begin_decode(SpiceJpegDecoder *decoder,
+ uint8_t* data, int data_size,
+ int* out_width, int* out_height)
+{
+ GlibJpegDecoder *d = SPICE_CONTAINEROF(decoder, GlibJpegDecoder, base);
+
+ g_return_if_fail(data != NULL);
+ g_return_if_fail(data_size != 0);
+
+ if (d->_data)
+ jpeg_abort_decompress(&d->_cinfo);
+
+ d->_data = data;
+ d->_data_size = data_size;
+
+ d->_cinfo.src->next_input_byte = d->_data;
+ d->_cinfo.src->bytes_in_buffer = d->_data_size;
+
+ jpeg_read_header(&d->_cinfo, TRUE);
+
+ d->_cinfo.out_color_space = JCS_RGB;
+ d->_width = d->_cinfo.image_width;
+ d->_height = d->_cinfo.image_height;
+
+ *out_width = d->_width;
+ *out_height = d->_height;
+}
+
+/* TODO: move it elsewhere and reuse it in get_pixbuf(), optimize? */
+typedef void (*converter_rgb_t)(uint8_t* src, uint8_t* dest, int width);
+
+static void convert_rgb_to_bgr(uint8_t* src, uint8_t* dest, int width)
+{
+ int x;
+
+ for (x = 0; x < width; x++) {
+ *dest++ = src[2];
+ *dest++ = src[1];
+ *dest++ = src[0];
+ src += 3;
+ }
+}
+
+static void convert_rgb_to_bgrx(uint8_t* src, uint8_t* dest, int width)
+{
+ int x;
+
+ for (x = 0; x < width; x++) {
+ *dest++ = src[2];
+ *dest++ = src[1];
+ *dest++ = src[0];
+ *dest++ = 0;
+ src += 3;
+ }
+}
+
+static void decode(SpiceJpegDecoder *decoder,
+ uint8_t* dest, int stride, int format)
+{
+ GlibJpegDecoder *d = SPICE_CONTAINEROF(decoder, GlibJpegDecoder, base);
+ uint8_t* scan_line = g_alloca(d->_width * 3);
+ converter_rgb_t converter = NULL;
+ int row;
+
+ switch (format) {
+ case SPICE_BITMAP_FMT_24BIT:
+ converter = convert_rgb_to_bgr;
+ break;
+ case SPICE_BITMAP_FMT_32BIT:
+ converter = convert_rgb_to_bgrx;
+ break;
+ default:
+ g_warning("bad bitmap format, %d", format);
+ return;
+ }
+
+ g_return_if_fail(converter != NULL);
+
+ jpeg_start_decompress(&d->_cinfo);
+
+ for (row = 0; row < d->_height; row++) {
+ jpeg_read_scanlines(&d->_cinfo, &scan_line, 1);
+ converter(scan_line, dest, d->_width);
+ dest += stride;
+ }
+
+ jpeg_finish_decompress(&d->_cinfo);
+}
+
+static SpiceJpegDecoderOps jpeg_decoder_ops = {
+ .begin_decode = begin_decode,
+ .decode = decode,
+};
+
+static void jpeg_decoder_init_source(j_decompress_ptr cinfo)
+{
+}
+
+static boolean jpeg_decoder_fill_input_buffer(j_decompress_ptr cinfo)
+{
+ g_warning("no more data for jpeg");
+ return FALSE;
+}
+
+static void jpeg_decoder_skip_input_data(j_decompress_ptr cinfo, long num_bytes)
+{
+ g_return_if_fail(num_bytes < (long)cinfo->src->bytes_in_buffer);
+
+ cinfo->src->next_input_byte += num_bytes;
+ cinfo->src->bytes_in_buffer -= num_bytes;
+}
+
+static void jpeg_decoder_term_source (j_decompress_ptr cinfo)
+{
+ return;
+}
+
+SpiceJpegDecoder *jpeg_decoder_new(void)
+{
+ GlibJpegDecoder *d = g_new0(GlibJpegDecoder, 1);
+
+ d->_cinfo.err = jpeg_std_error(&d->_jerr);
+ jpeg_create_decompress(&d->_cinfo);
+
+ d->_cinfo.src = &d->_jsrc;
+ d->_cinfo.src->init_source = jpeg_decoder_init_source;
+ d->_cinfo.src->fill_input_buffer = jpeg_decoder_fill_input_buffer;
+ d->_cinfo.src->skip_input_data = jpeg_decoder_skip_input_data;
+ d->_cinfo.src->resync_to_restart = jpeg_resync_to_restart;
+ d->_cinfo.src->term_source = jpeg_decoder_term_source;
+
+ d->base.ops = &jpeg_decoder_ops;
+
+ return &d->base;
+}
+
+void jpeg_decoder_destroy(SpiceJpegDecoder *decoder)
+{
+ GlibJpegDecoder *d = SPICE_CONTAINEROF(decoder, GlibJpegDecoder, base);
+
+ jpeg_destroy_decompress(&d->_cinfo);
+ free(d);
+}
diff --git a/src/decode-zlib.c b/src/decode-zlib.c
new file mode 100644
index 0000000..a5325c0
--- /dev/null
+++ b/src/decode-zlib.c
@@ -0,0 +1,89 @@
+/* -*- 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 "decode.h"
+
+#ifndef __GNUC__
+#define ZLIB_WINAPI
+#endif
+
+#include <zlib.h>
+
+typedef struct GlibZlibDecoder
+{
+ SpiceZlibDecoder base;
+ z_stream _z_strm;
+} GlibZlibDecoder;
+
+static void decode(SpiceZlibDecoder *decoder,
+ uint8_t *data, int data_size,
+ uint8_t *dest, int dest_size)
+{
+ GlibZlibDecoder *d = SPICE_CONTAINEROF(decoder, GlibZlibDecoder, base);
+ int z_ret;
+
+ inflateReset(&d->_z_strm);
+ d->_z_strm.next_in = data;
+ d->_z_strm.avail_in = data_size;
+ d->_z_strm.next_out = dest;
+ d->_z_strm.avail_out = dest_size;
+
+ z_ret = inflate(&d->_z_strm, Z_FINISH);
+
+ if (z_ret != Z_STREAM_END) {
+ g_warning("zlib inflate failed, error %d", z_ret);
+ }
+}
+
+static SpiceZlibDecoderOps zlib_decoder_ops = {
+ .decode = decode,
+};
+
+SpiceZlibDecoder *zlib_decoder_new(void)
+{
+ GlibZlibDecoder *d = g_new0(GlibZlibDecoder, 1);
+ int z_ret;
+
+ d->_z_strm.zalloc = Z_NULL;
+ d->_z_strm.zfree = Z_NULL;
+ d->_z_strm.opaque = Z_NULL;
+ d->_z_strm.next_in = Z_NULL;
+ d->_z_strm.avail_in = 0;
+ z_ret = inflateInit(&d->_z_strm);
+ if (z_ret != Z_OK) {
+ g_warning("zlib decoder init failed, error %d", z_ret);
+ goto fail;
+ }
+
+ d->base.ops = &zlib_decoder_ops;
+
+ return &d->base;
+
+fail:
+ free(d);
+ return NULL;
+}
+
+void zlib_decoder_destroy(SpiceZlibDecoder *decoder)
+{
+ GlibZlibDecoder *d = SPICE_CONTAINEROF(decoder, GlibZlibDecoder, base);
+
+ inflateEnd(&d->_z_strm);
+ free(d);
+}
diff --git a/src/decode.h b/src/decode.h
new file mode 100644
index 0000000..b274d67
--- /dev/null
+++ b/src/decode.h
@@ -0,0 +1,44 @@
+/* -*- 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 SPICEGTK_DECODE_H_
+# define SPICEGTK_DECODE_H_
+
+#include <glib.h>
+
+#include "client_sw_canvas.h"
+
+G_BEGIN_DECLS
+
+typedef struct SpiceGlzDecoderWindow SpiceGlzDecoderWindow;
+
+SpiceGlzDecoderWindow *glz_decoder_window_new(void);
+void glz_decoder_window_clear(SpiceGlzDecoderWindow *w);
+void glz_decoder_window_destroy(SpiceGlzDecoderWindow *w);
+
+SpiceGlzDecoder *glz_decoder_new(SpiceGlzDecoderWindow *w);
+void glz_decoder_destroy(SpiceGlzDecoder *d);
+
+SpiceZlibDecoder *zlib_decoder_new(void);
+void zlib_decoder_destroy(SpiceZlibDecoder *d);
+
+SpiceJpegDecoder *jpeg_decoder_new(void);
+void jpeg_decoder_destroy(SpiceJpegDecoder *d);
+
+G_END_DECLS
+
+#endif // SPICEGTK_DECODE_H_
diff --git a/src/desktop-integration.c b/src/desktop-integration.c
new file mode 100644
index 0000000..5868d48
--- /dev/null
+++ b/src/desktop-integration.c
@@ -0,0 +1,223 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 Red Hat, Inc.
+
+ Red Hat Authors:
+ Hans de Goede <hdegoede@redhat.com>
+
+ 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-object.h>
+
+#include "glib-compat.h"
+#include "spice-session-priv.h"
+#include "desktop-integration.h"
+
+#include <glib/gi18n.h>
+
+#define GNOME_SESSION_INHIBIT_AUTOMOUNT 16
+
+/* ------------------------------------------------------------------ */
+/* gobject glue */
+
+#define SPICE_DESKTOP_INTEGRATION_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE ((obj), SPICE_TYPE_DESKTOP_INTEGRATION, SpiceDesktopIntegrationPrivate))
+
+struct _SpiceDesktopIntegrationPrivate {
+#if defined(USE_GDBUS)
+ GDBusProxy *gnome_session_proxy;
+#else
+ GObject *gnome_session_proxy; /* dummy */
+#endif
+ guint gnome_automount_inhibit_cookie;
+};
+
+G_DEFINE_TYPE(SpiceDesktopIntegration, spice_desktop_integration, G_TYPE_OBJECT);
+
+/* ------------------------------------------------------------------ */
+/* Gnome specific code */
+
+static void handle_dbus_call_error(const char *call, GError **_error)
+{
+ GError *error = *_error;
+ const char *message = error->message;
+
+ g_warning("Error calling '%s': %s", call, message);
+ g_clear_error(_error);
+}
+
+static gboolean gnome_integration_init(SpiceDesktopIntegration *self)
+{
+ G_GNUC_UNUSED SpiceDesktopIntegrationPrivate *priv = self->priv;
+ GError *error = NULL;
+ gboolean success = TRUE;
+
+#if defined(USE_GDBUS)
+ gchar *name_owner = NULL;
+ priv->gnome_session_proxy =
+ g_dbus_proxy_new_for_bus_sync(G_BUS_TYPE_SESSION,
+ G_DBUS_PROXY_FLAGS_NONE,
+ NULL,
+ "org.gnome.SessionManager",
+ "/org/gnome/SessionManager",
+ "org.gnome.SessionManager",
+ NULL,
+ &error);
+ if (!error &&
+ (name_owner = g_dbus_proxy_get_name_owner(priv->gnome_session_proxy)) == NULL) {
+ g_clear_object(&priv->gnome_session_proxy);
+ success = FALSE;
+ }
+ g_free(name_owner);
+#else
+ success = FALSE;
+#endif
+
+ if (error) {
+ g_warning("Could not create org.gnome.SessionManager dbus proxy: %s",
+ error->message);
+ g_clear_error(&error);
+ return FALSE;
+ }
+
+ return success;
+}
+
+static void gnome_integration_inhibit_automount(SpiceDesktopIntegration *self)
+{
+ SpiceDesktopIntegrationPrivate *priv = self->priv;
+ GError *error = NULL;
+ G_GNUC_UNUSED const gchar *reason =
+ _("Automounting has been inhibited for USB auto-redirecting");
+
+ if (!priv->gnome_session_proxy)
+ return;
+
+ g_return_if_fail(priv->gnome_automount_inhibit_cookie == 0);
+
+#if defined(USE_GDBUS)
+ GVariant *v = g_dbus_proxy_call_sync(priv->gnome_session_proxy,
+ "Inhibit",
+ g_variant_new("(susu)",
+ g_get_prgname(),
+ 0,
+ reason,
+ GNOME_SESSION_INHIBIT_AUTOMOUNT),
+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
+ if (v)
+ g_variant_get(v, "(u)", &priv->gnome_automount_inhibit_cookie);
+
+ g_clear_pointer(&v, g_variant_unref);
+#endif
+ if (error)
+ handle_dbus_call_error("org.gnome.SessionManager.Inhibit", &error);
+}
+
+static void gnome_integration_uninhibit_automount(SpiceDesktopIntegration *self)
+{
+ SpiceDesktopIntegrationPrivate *priv = self->priv;
+ GError *error = NULL;
+
+ if (!priv->gnome_session_proxy)
+ return;
+
+ /* Cookie is 0 when we failed to inhibit (and when called from dispose) */
+ if (priv->gnome_automount_inhibit_cookie == 0)
+ return;
+
+#if defined(USE_GDBUS)
+ GVariant *v = g_dbus_proxy_call_sync(priv->gnome_session_proxy,
+ "Uninhibit",
+ g_variant_new("(u)",
+ priv->gnome_automount_inhibit_cookie),
+ G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error);
+ g_clear_pointer(&v, g_variant_unref);
+#endif
+ if (error)
+ handle_dbus_call_error("org.gnome.SessionManager.Uninhibit", &error);
+
+ priv->gnome_automount_inhibit_cookie = 0;
+}
+
+static void gnome_integration_dispose(SpiceDesktopIntegration *self)
+{
+ SpiceDesktopIntegrationPrivate *priv = self->priv;
+
+ g_clear_object(&priv->gnome_session_proxy);
+}
+
+/* ------------------------------------------------------------------ */
+/* gobject glue */
+
+static void spice_desktop_integration_init(SpiceDesktopIntegration *self)
+{
+ SpiceDesktopIntegrationPrivate *priv;
+
+ priv = SPICE_DESKTOP_INTEGRATION_GET_PRIVATE(self);
+ self->priv = priv;
+
+ if (!gnome_integration_init(self))
+ g_warning("Warning no automount-inhibiting implementation available");
+}
+
+static void spice_desktop_integration_dispose(GObject *gobject)
+{
+ SpiceDesktopIntegration *self = SPICE_DESKTOP_INTEGRATION(gobject);
+
+ gnome_integration_dispose(self);
+
+ /* Chain up to the parent class */
+ if (G_OBJECT_CLASS(spice_desktop_integration_parent_class)->dispose)
+ G_OBJECT_CLASS(spice_desktop_integration_parent_class)->dispose(gobject);
+}
+
+static void spice_desktop_integration_class_init(SpiceDesktopIntegrationClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->dispose = spice_desktop_integration_dispose;
+
+ g_type_class_add_private(klass, sizeof(SpiceDesktopIntegrationPrivate));
+}
+
+SpiceDesktopIntegration *spice_desktop_integration_get(SpiceSession *session)
+{
+ SpiceDesktopIntegration *self;
+ static GStaticMutex mutex = G_STATIC_MUTEX_INIT;
+
+ g_return_val_if_fail(session != NULL, NULL);
+
+ g_static_mutex_lock(&mutex);
+ self = g_object_get_data(G_OBJECT(session), "spice-desktop");
+ if (self == NULL) {
+ self = g_object_new(SPICE_TYPE_DESKTOP_INTEGRATION, NULL);
+ g_object_set_data_full(G_OBJECT(session), "spice-desktop", self, g_object_unref);
+ }
+ g_static_mutex_unlock(&mutex);
+
+ return self;
+}
+
+void spice_desktop_integration_inhibit_automount(SpiceDesktopIntegration *self)
+{
+ gnome_integration_inhibit_automount(self);
+}
+
+void spice_desktop_integration_uninhibit_automount(SpiceDesktopIntegration *self)
+{
+ gnome_integration_uninhibit_automount(self);
+}
diff --git a/src/desktop-integration.h b/src/desktop-integration.h
new file mode 100644
index 0000000..3716089
--- /dev/null
+++ b/src/desktop-integration.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 Red Hat, Inc.
+
+ Red Hat Authors:
+ Hans de Goede <hdegoede@redhat.com>
+
+ 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_DESKTOP_INTEGRATION_H__
+#define __SPICE_DESKTOP_INTEGRATION_H__
+
+#include "spice-client.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_DESKTOP_INTEGRATION (spice_desktop_integration_get_type ())
+#define SPICE_DESKTOP_INTEGRATION(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SPICE_TYPE_DESKTOP_INTEGRATION, SpiceDesktopIntegration))
+#define SPICE_DESKTOP_INTEGRATION_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SPICE_TYPE_DESKTOP_INTEGRATION, SpiceDesktopIntegrationClass))
+#define SPICE_IS_DESKTOP_INTEGRATION(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SPICE_TYPE_DESKTOP_INTEGRATION))
+#define SPICE_IS_DESKTOP_INTEGRATION_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SPICE_TYPE_DESKTOP_INTEGRATION))
+#define SPICE_DESKTOP_INTEGRATION_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SPICE_TYPE_DESKTOP_INTEGRATION, SpiceDesktopIntegrationClass))
+
+typedef struct _SpiceDesktopIntegration SpiceDesktopIntegration;
+typedef struct _SpiceDesktopIntegrationClass SpiceDesktopIntegrationClass;
+typedef struct _SpiceDesktopIntegrationPrivate SpiceDesktopIntegrationPrivate;
+
+/*
+ * SpiceDesktopIntegration offers helper-functions to do desktop environment
+ * and/or platform specific tasks like disabling automount, disabling the
+ * screen-saver, etc. SpiceDesktopIntegration is for internal spice-gtk usage
+ * only!
+ */
+struct _SpiceDesktopIntegration
+{
+ GObject parent;
+
+ SpiceDesktopIntegrationPrivate *priv;
+};
+
+struct _SpiceDesktopIntegrationClass
+{
+ GObjectClass parent_class;
+};
+
+GType spice_desktop_integration_get_type(void);
+SpiceDesktopIntegration *spice_desktop_integration_get(SpiceSession *session);
+void spice_desktop_integration_inhibit_automount(SpiceDesktopIntegration *self);
+void spice_desktop_integration_uninhibit_automount(SpiceDesktopIntegration *self);
+
+G_END_DECLS
+
+#endif /* __SPICE_DESKTOP_INTEGRATION_H__ */
diff --git a/src/gio-coroutine.c b/src/gio-coroutine.c
new file mode 100644
index 0000000..c866e15
--- /dev/null
+++ b/src/gio-coroutine.c
@@ -0,0 +1,275 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2010 Red Hat, Inc.
+ Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws>
+ Copyright (C) 2009-2010 Daniel P. Berrange <dan@berrange.com>
+
+ 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.0 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, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+#include "config.h"
+
+#include "gio-coroutine.h"
+
+typedef struct _GConditionWaitSource
+{
+ GCoroutine *self;
+ GSource src;
+ GConditionWaitFunc func;
+ gpointer data;
+} GConditionWaitSource;
+
+GCoroutine* g_coroutine_self(void)
+{
+ return (GCoroutine*)coroutine_self();
+}
+
+/* Main loop helper functions */
+static gboolean g_io_wait_helper(GSocket *sock G_GNUC_UNUSED,
+ GIOCondition cond,
+ gpointer data)
+{
+ struct coroutine *to = data;
+ coroutine_yieldto(to, &cond);
+ return FALSE;
+}
+
+GIOCondition g_coroutine_socket_wait(GCoroutine *self,
+ GSocket *sock,
+ GIOCondition cond)
+{
+ GIOCondition *ret, val = 0;
+ GSource *src;
+
+ g_return_val_if_fail(self != NULL, 0);
+ g_return_val_if_fail(self->wait_id == 0, 0);
+ g_return_val_if_fail(sock != NULL, 0);
+
+ src = g_socket_create_source(sock, cond | G_IO_HUP | G_IO_ERR | G_IO_NVAL, NULL);
+ g_source_set_callback(src, (GSourceFunc)g_io_wait_helper, self, NULL);
+ self->wait_id = g_source_attach(src, NULL);
+ ret = coroutine_yield(NULL);
+ g_source_unref(src);
+
+ if (ret != NULL)
+ val = *ret;
+ else
+ g_source_remove(self->wait_id);
+
+ self->wait_id = 0;
+ return val;
+}
+
+void g_coroutine_condition_cancel(GCoroutine *coroutine)
+{
+ g_return_if_fail(coroutine != NULL);
+
+ if (coroutine->condition_id == 0)
+ return;
+
+ g_source_remove(coroutine->condition_id);
+ coroutine->condition_id = 0;
+}
+
+void g_coroutine_wakeup(GCoroutine *coroutine)
+{
+ g_return_if_fail(coroutine != NULL);
+ g_return_if_fail(coroutine != g_coroutine_self());
+
+ if (coroutine->wait_id)
+ coroutine_yieldto(&coroutine->coroutine, NULL);
+}
+
+/*
+ * Call immediately before the main loop does an iteration. Returns
+ * true if the condition we're checking is ready for dispatch
+ */
+static gboolean g_condition_wait_prepare(GSource *src,
+ int *timeout) {
+ GConditionWaitSource *vsrc = (GConditionWaitSource *)src;
+ *timeout = -1;
+ return vsrc->func(vsrc->data);
+}
+
+/*
+ * Call immediately after the main loop does an iteration. Returns
+ * true if the condition we're checking is ready for dispatch
+ */
+static gboolean g_condition_wait_check(GSource *src)
+{
+ GConditionWaitSource *vsrc = (GConditionWaitSource *)src;
+ return vsrc->func(vsrc->data);
+}
+
+static gboolean g_condition_wait_dispatch(GSource *src G_GNUC_UNUSED,
+ GSourceFunc cb,
+ gpointer data) {
+ return cb(data);
+}
+
+GSourceFuncs waitFuncs = {
+ .prepare = g_condition_wait_prepare,
+ .check = g_condition_wait_check,
+ .dispatch = g_condition_wait_dispatch,
+};
+
+static gboolean g_condition_wait_helper(gpointer data)
+{
+ GCoroutine *self = (GCoroutine *)data;
+ coroutine_yieldto(&self->coroutine, NULL);
+ return FALSE;
+}
+
+/*
+ * g_coroutine_condition_wait:
+ * @coroutine: the coroutine to wait on
+ * @func: the condition callback
+ * @data: the user data passed to @func callback
+ *
+ * This function will wait on caller coroutine until @func returns %TRUE.
+ *
+ * @func is called when entering the main loop from the main context (coroutine).
+ *
+ * The condition can be cancelled by calling g_coroutine_wakeup()
+ *
+ * Returns: %TRUE if condition reached, %FALSE if not and cancelled
+ */
+gboolean g_coroutine_condition_wait(GCoroutine *self, GConditionWaitFunc func, gpointer data)
+{
+ GSource *src;
+ GConditionWaitSource *vsrc;
+
+ g_return_val_if_fail(self != NULL, FALSE);
+ g_return_val_if_fail(self->condition_id == 0, FALSE);
+ g_return_val_if_fail(func != NULL, FALSE);
+
+ /* Short-circuit check in case we've got it ahead of time */
+ if (func(data))
+ return TRUE;
+
+ /*
+ * Don't have it, so yield to the main loop, checking the condition
+ * on each iteration of the main loop
+ */
+ src = g_source_new(&waitFuncs, sizeof(GConditionWaitSource));
+ vsrc = (GConditionWaitSource *)src;
+
+ vsrc->func = func;
+ vsrc->data = data;
+ vsrc->self = self;
+
+ self->condition_id = g_source_attach(src, NULL);
+ g_source_set_callback(src, g_condition_wait_helper, self, NULL);
+ coroutine_yield(NULL);
+ g_source_unref(src);
+
+ /* it got woked up / cancelled? */
+ if (self->condition_id == 0)
+ return func(data);
+
+ self->condition_id = 0;
+ return TRUE;
+}
+
+struct signal_data
+{
+ gpointer instance;
+ struct coroutine *caller;
+ guint signal_id;
+ GQuark detail;
+ const gchar *propname;
+ gboolean notified;
+ va_list var_args;
+};
+
+static gboolean emit_main_context(gpointer opaque)
+{
+ struct signal_data *signal = opaque;
+
+ g_signal_emit_valist(signal->instance, signal->signal_id,
+ signal->detail, signal->var_args);
+ signal->notified = TRUE;
+
+ coroutine_yieldto(signal->caller, NULL);
+
+ return FALSE;
+}
+
+void
+g_coroutine_signal_emit(gpointer instance, guint signal_id,
+ GQuark detail, ...)
+{
+ struct signal_data data = {
+ .instance = instance,
+ .signal_id = signal_id,
+ .detail = detail,
+ .caller = coroutine_self(),
+ };
+
+ va_start (data.var_args, detail);
+
+ if (coroutine_self_is_main()) {
+ g_signal_emit_valist(instance, signal_id, detail, data.var_args);
+ } else {
+ g_object_ref(instance);
+ g_idle_add(emit_main_context, &data);
+ coroutine_yield(NULL);
+ g_warn_if_fail(data.notified);
+ g_object_unref(instance);
+ }
+
+ va_end (data.var_args);
+}
+
+
+static gboolean notify_main_context(gpointer opaque)
+{
+ struct signal_data *signal = opaque;
+
+ g_object_notify(signal->instance, signal->propname);
+ signal->notified = TRUE;
+
+ coroutine_yieldto(signal->caller, NULL);
+
+ return FALSE;
+}
+
+/* coroutine -> main context */
+void g_coroutine_object_notify(GObject *object,
+ const gchar *property_name)
+{
+ struct signal_data data;
+
+ if (coroutine_self_is_main()) {
+ g_object_notify(object, property_name);
+ } else {
+
+ data.instance = g_object_ref(object);
+ data.caller = coroutine_self();
+ data.propname = (gpointer)property_name;
+ data.notified = FALSE;
+
+ g_idle_add(notify_main_context, &data);
+
+ /* This switches to the system coroutine context, lets
+ * the idle function run to dispatch the signal, and
+ * finally returns once complete. ie this is synchronous
+ * from the POV of the coroutine despite there being
+ * an idle function involved
+ */
+ coroutine_yield(NULL);
+ g_warn_if_fail(data.notified);
+ g_object_unref(object);
+ }
+}
diff --git a/src/gio-coroutine.h b/src/gio-coroutine.h
new file mode 100644
index 0000000..b3a6d78
--- /dev/null
+++ b/src/gio-coroutine.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2010 Red Hat, Inc.
+ Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws>
+ Copyright (C) 2009-2010 Daniel P. Berrange <dan@berrange.com>
+
+ 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.0 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, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+*/
+#ifndef __GIO_COROUTINE_H__
+#define __GIO_COROUTINE_H__
+
+#include <gio/gio.h>
+#include "coroutine.h"
+
+G_BEGIN_DECLS
+
+typedef struct _GCoroutine GCoroutine;
+
+struct _GCoroutine
+{
+ struct coroutine coroutine;
+ guint wait_id;
+ guint condition_id;
+};
+
+/*
+ * A special GSource impl which allows us to wait on a certain
+ * condition to be satisfied. This is effectively a boolean test
+ * run on each iteration of the main loop. So whenever a file has
+ * new I/O, or a timer occurs, etc we'll do the check. This is
+ * pretty efficient compared to a normal GLib Idle func which has
+ * to busy wait on a timeout, since our condition is only checked
+ * when some other source's state changes
+ */
+typedef gboolean (*GConditionWaitFunc)(gpointer);
+
+typedef void (*GSignalEmitMainFunc)(GObject *object, int signum, gpointer params);
+
+GCoroutine* g_coroutine_self (void);
+void g_coroutine_wakeup (GCoroutine *coroutine);
+GIOCondition g_coroutine_socket_wait (GCoroutine *coroutine,
+ GSocket *sock, GIOCondition cond);
+gboolean g_coroutine_condition_wait (GCoroutine *coroutine,
+ GConditionWaitFunc func, gpointer data);
+void g_coroutine_condition_cancel(GCoroutine *coroutine);
+
+void g_coroutine_signal_emit (gpointer instance, guint signal_id,
+ GQuark detail, ...);
+
+void g_coroutine_object_notify(GObject *object, const gchar *property_name);
+
+G_END_DECLS
+
+#endif /* __GIO_COROUTINE_H__ */
diff --git a/src/giopipe.c b/src/giopipe.c
new file mode 100644
index 0000000..d91c4d9
--- /dev/null
+++ b/src/giopipe.c
@@ -0,0 +1,484 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 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 <string.h>
+#include <errno.h>
+
+#include "giopipe.h"
+
+#define TYPE_PIPE_INPUT_STREAM (pipe_input_stream_get_type ())
+#define PIPE_INPUT_STREAM(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), TYPE_PIPE_INPUT_STREAM, PipeInputStream))
+#define PIPE_INPUT_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), TYPE_PIPE_INPUT_STREAM, PipeInputStreamClass))
+#define IS_PIPE_INPUT_STREAM(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), TYPE_PIPE_INPUT_STREAM))
+#define IS_PIPE_INPUT_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), TYPE_PIPE_INPUT_STREAM))
+#define PIPE_INPUT_STREAM_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), TYPE_PIPE_INPUT_STREAM, PipeInputStreamClass))
+
+typedef struct _PipeInputStreamClass PipeInputStreamClass;
+typedef struct _PipeInputStream PipeInputStream;
+typedef struct _PipeOutputStream PipeOutputStream;
+
+struct _PipeInputStream
+{
+ GInputStream parent_instance;
+
+ PipeOutputStream *peer;
+ gssize read;
+
+ /* GIOstream:closed is protected against pending operations, so we
+ * use an additional close flag to cancel those when the peer is
+ * closing.
+ */
+ gboolean peer_closed;
+ GList *sources;
+};
+
+struct _PipeInputStreamClass
+{
+ GInputStreamClass parent_class;
+};
+
+#define TYPE_PIPE_OUTPUT_STREAM (pipe_output_stream_get_type ())
+#define PIPE_OUTPUT_STREAM(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), TYPE_PIPE_OUTPUT_STREAM, PipeOutputStream))
+#define PIPE_OUTPUT_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), TYPE_PIPE_OUTPUT_STREAM, PipeOutputStreamClass))
+#define IS_PIPE_OUTPUT_STREAM(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), TYPE_PIPE_OUTPUT_STREAM))
+#define IS_PIPE_OUTPUT_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), TYPE_PIPE_OUTPUT_STREAM))
+#define PIPE_OUTPUT_STREAM_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), TYPE_PIPE_OUTPUT_STREAM, PipeOutputStreamClass))
+
+typedef struct _PipeOutputStreamClass PipeOutputStreamClass;
+
+struct _PipeOutputStream
+{
+ GOutputStream parent_instance;
+
+ PipeInputStream *peer;
+ const gchar *buffer;
+ gsize count;
+ gboolean peer_closed;
+ GList *sources;
+};
+
+struct _PipeOutputStreamClass
+{
+ GOutputStreamClass parent_class;
+};
+
+static void pipe_input_stream_pollable_iface_init (GPollableInputStreamInterface *iface);
+static void pipe_input_stream_check_source (PipeInputStream *self);
+static void pipe_output_stream_check_source (PipeOutputStream *self);
+
+G_DEFINE_TYPE_WITH_CODE (PipeInputStream, pipe_input_stream, G_TYPE_INPUT_STREAM,
+ G_IMPLEMENT_INTERFACE (G_TYPE_POLLABLE_INPUT_STREAM,
+ pipe_input_stream_pollable_iface_init))
+
+static gssize
+pipe_input_stream_read (GInputStream *stream,
+ void *buffer,
+ gsize count,
+ GCancellable *cancellable,
+ GError **error)
+{
+ PipeInputStream *self = PIPE_INPUT_STREAM (stream);
+
+ g_return_val_if_fail(count > 0, -1);
+
+ if (g_input_stream_is_closed (stream) || self->peer_closed) {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_CLOSED,
+ "Stream is already closed");
+ return -1;
+ }
+
+ if (!self->peer->buffer) {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK,
+ g_strerror(EAGAIN));
+ return -1;
+ }
+
+ count = MIN(self->peer->count, count);
+ memcpy(buffer, self->peer->buffer, count);
+ self->read = count;
+ self->peer->buffer = NULL;
+
+ //g_debug("read %p :%"G_GSIZE_FORMAT, self->peer, count);
+ /* schedule peer source */
+ pipe_output_stream_check_source(self->peer);
+
+ return count;
+}
+
+static GList *
+set_all_sources_ready (GList *sources)
+{
+ GList *it = sources;
+ while (it != NULL) {
+ GSource *s = it->data;
+ GList *next = it->next;
+
+ if (s == NULL || g_source_is_destroyed(s)) {
+ /* remove */
+ sources = g_list_delete_link(sources, it);
+ g_source_unref(s);
+ } else {
+ /* dispatch */
+ g_source_set_ready_time(s, 0);
+ }
+ it = next;
+ }
+ return sources;
+}
+
+static void
+pipe_input_stream_check_source (PipeInputStream *self)
+{
+ if (g_pollable_input_stream_is_readable(G_POLLABLE_INPUT_STREAM(self)))
+ self->sources = set_all_sources_ready(self->sources);
+}
+
+static gboolean
+pipe_input_stream_close (GInputStream *stream,
+ GCancellable *cancellable,
+ GError **error)
+{
+ PipeInputStream *self;
+
+ self = PIPE_INPUT_STREAM(stream);
+
+ if (self->peer) {
+ /* ignore any pending errors */
+ self->peer->peer_closed = TRUE;
+ g_output_stream_close(G_OUTPUT_STREAM(self->peer), cancellable, NULL);
+ pipe_output_stream_check_source(self->peer);
+ }
+
+ return TRUE;
+}
+
+static void
+pipe_input_stream_close_async (GInputStream *stream,
+ int io_priority,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer data)
+{
+ GTask *task;
+
+ task = g_task_new (stream, cancellable, callback, data);
+
+ /* will always return TRUE */
+ pipe_input_stream_close (stream, cancellable, NULL);
+
+ g_task_return_boolean (task, TRUE);
+ g_object_unref (task);
+}
+
+static gboolean
+pipe_input_stream_close_finish (GInputStream *stream,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (result, stream), FALSE);
+
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+pipe_input_stream_init (PipeInputStream *self)
+{
+ self->read = -1;
+}
+
+static void
+pipe_input_stream_dispose(GObject *object)
+{
+ PipeInputStream *self;
+
+ self = PIPE_INPUT_STREAM(object);
+
+ if (self->peer) {
+ g_object_remove_weak_pointer(G_OBJECT(self->peer), (gpointer*)&self->peer);
+ self->peer = NULL;
+ }
+
+ g_list_free_full (self->sources, (GDestroyNotify) g_source_unref);
+ self->sources = NULL;
+
+ G_OBJECT_CLASS(pipe_input_stream_parent_class)->dispose (object);
+}
+
+static void
+pipe_input_stream_class_init (PipeInputStreamClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GInputStreamClass *istream_class = G_INPUT_STREAM_CLASS (klass);
+
+ istream_class->read_fn = pipe_input_stream_read;
+ istream_class->close_fn = pipe_input_stream_close;
+ istream_class->close_async = pipe_input_stream_close_async;
+ istream_class->close_finish = pipe_input_stream_close_finish;
+
+ gobject_class->dispose = pipe_input_stream_dispose;
+}
+
+static gboolean
+pipe_input_stream_is_readable (GPollableInputStream *stream)
+{
+ PipeInputStream *self = PIPE_INPUT_STREAM (stream);
+ gboolean readable;
+
+ readable = (self->peer && self->peer->buffer && self->read == -1) || self->peer_closed;
+ //g_debug("readable %p %d", self->peer, readable);
+
+ return readable;
+}
+
+static GSource *
+pipe_input_stream_create_source (GPollableInputStream *stream,
+ GCancellable *cancellable)
+{
+ PipeInputStream *self = PIPE_INPUT_STREAM(stream);
+ GSource *pollable_source;
+
+ pollable_source = g_pollable_source_new_full (self, NULL, cancellable);
+ self->sources = g_list_prepend (self->sources, g_source_ref (pollable_source));
+
+ return pollable_source;
+}
+
+static void
+pipe_input_stream_pollable_iface_init (GPollableInputStreamInterface *iface)
+{
+ iface->is_readable = pipe_input_stream_is_readable;
+ iface->create_source = pipe_input_stream_create_source;
+}
+
+static void pipe_output_stream_pollable_iface_init (GPollableOutputStreamInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (PipeOutputStream, pipe_output_stream, G_TYPE_OUTPUT_STREAM,
+ G_IMPLEMENT_INTERFACE (G_TYPE_POLLABLE_OUTPUT_STREAM,
+ pipe_output_stream_pollable_iface_init))
+
+static gssize
+pipe_output_stream_write (GOutputStream *stream,
+ const void *buffer,
+ gsize count,
+ GCancellable *cancellable,
+ GError **error)
+{
+ PipeOutputStream *self = PIPE_OUTPUT_STREAM(stream);
+ PipeInputStream *peer = self->peer;
+
+ //g_debug("write %p :%"G_GSIZE_FORMAT, stream, count);
+ if (g_output_stream_is_closed (stream) || self->peer_closed) {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_CLOSED,
+ "Stream is already closed");
+ return -1;
+ }
+
+ /* this abuses pollable stream, writing sync would likely lead to
+ crashes, since the buffer pointer would become invalid, a
+ generic solution would need a copy..
+ */
+ g_return_val_if_fail(self->buffer == buffer || self->buffer == NULL, -1);
+ self->buffer = buffer;
+ self->count = count;
+
+ pipe_input_stream_check_source(self->peer);
+
+ if (peer->read < 0) {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK,
+ g_strerror (EAGAIN));
+ return -1;
+ }
+
+ g_assert(peer->read <= self->count);
+ count = peer->read;
+
+ self->buffer = NULL;
+ self->count = 0;
+ peer->read = -1;
+
+ return count;
+}
+
+static void
+pipe_output_stream_init (PipeOutputStream *stream)
+{
+}
+
+static void
+pipe_output_stream_dispose(GObject *object)
+{
+ PipeOutputStream *self;
+
+ self = PIPE_OUTPUT_STREAM(object);
+
+ if (self->peer) {
+ g_object_remove_weak_pointer(G_OBJECT(self->peer), (gpointer*)&self->peer);
+ self->peer = NULL;
+ }
+
+ g_list_free_full (self->sources, (GDestroyNotify) g_source_unref);
+ self->sources = NULL;
+
+ G_OBJECT_CLASS(pipe_output_stream_parent_class)->dispose (object);
+}
+
+static void
+pipe_output_stream_check_source (PipeOutputStream *self)
+{
+ if (g_pollable_output_stream_is_writable(G_POLLABLE_OUTPUT_STREAM(self)))
+ self->sources = set_all_sources_ready(self->sources);
+}
+
+static gboolean
+pipe_output_stream_close (GOutputStream *stream,
+ GCancellable *cancellable,
+ GError **error)
+{
+ PipeOutputStream *self;
+
+ self = PIPE_OUTPUT_STREAM(stream);
+
+ if (self->peer) {
+ /* ignore any pending errors */
+ self->peer->peer_closed = TRUE;
+ g_input_stream_close(G_INPUT_STREAM(self->peer), cancellable, NULL);
+ pipe_input_stream_check_source(self->peer);
+ }
+
+ return TRUE;
+}
+
+static void
+pipe_output_stream_close_async (GOutputStream *stream,
+ int io_priority,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer data)
+{
+ GTask *task;
+
+ task = g_task_new (stream, cancellable, callback, data);
+
+ /* will always return TRUE */
+ pipe_output_stream_close (stream, cancellable, NULL);
+
+ g_task_return_boolean (task, TRUE);
+ g_object_unref (task);
+}
+
+static gboolean
+pipe_output_stream_close_finish (GOutputStream *stream,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (g_task_is_valid (result, stream), FALSE);
+
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+
+static void
+pipe_output_stream_class_init (PipeOutputStreamClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GOutputStreamClass *ostream_class = G_OUTPUT_STREAM_CLASS (klass);
+
+ ostream_class->write_fn = pipe_output_stream_write;
+ ostream_class->close_fn = pipe_output_stream_close;
+ ostream_class->close_async = pipe_output_stream_close_async;
+ ostream_class->close_finish = pipe_output_stream_close_finish;
+
+ gobject_class->dispose = pipe_output_stream_dispose;
+}
+
+static gboolean
+pipe_output_stream_is_writable (GPollableOutputStream *stream)
+{
+ PipeOutputStream *self = PIPE_OUTPUT_STREAM(stream);
+ gboolean writable;
+
+ writable = self->buffer == NULL || self->peer->read >= 0;
+ //g_debug("writable %p %d", self, writable);
+
+ return writable;
+}
+
+static GSource *
+pipe_output_stream_create_source (GPollableOutputStream *stream,
+ GCancellable *cancellable)
+{
+ PipeOutputStream *self = PIPE_OUTPUT_STREAM(stream);
+ GSource *pollable_source;
+
+ pollable_source = g_pollable_source_new_full (self, NULL, cancellable);
+ self->sources = g_list_prepend (self->sources, g_source_ref (pollable_source));
+
+ return pollable_source;
+}
+
+static void
+pipe_output_stream_pollable_iface_init (GPollableOutputStreamInterface *iface)
+{
+ iface->is_writable = pipe_output_stream_is_writable;
+ iface->create_source = pipe_output_stream_create_source;
+}
+
+G_GNUC_INTERNAL void
+make_gio_pipe(GInputStream **input, GOutputStream **output)
+{
+ PipeInputStream *in;
+ PipeOutputStream *out;
+
+ g_return_if_fail(input != NULL && *input == NULL);
+ g_return_if_fail(output != NULL && *output == NULL);
+
+ in = g_object_new(TYPE_PIPE_INPUT_STREAM, NULL);
+ out = g_object_new(TYPE_PIPE_OUTPUT_STREAM, NULL);
+
+ out->peer = in;
+ g_object_add_weak_pointer(G_OBJECT(in), (gpointer*)&out->peer);
+
+ in->peer = out;
+ g_object_add_weak_pointer(G_OBJECT(out), (gpointer*)&in->peer);
+
+ *input = G_INPUT_STREAM(in);
+ *output = G_OUTPUT_STREAM(out);
+}
+
+G_GNUC_INTERNAL void
+spice_make_pipe(GIOStream **p1, GIOStream **p2)
+{
+ GInputStream *in1 = NULL, *in2 = NULL;
+ GOutputStream *out1 = NULL, *out2 = NULL;
+
+ g_return_if_fail(p1 != NULL);
+ g_return_if_fail(p2 != NULL);
+ g_return_if_fail(*p1 == NULL);
+ g_return_if_fail(*p2 == NULL);
+
+ make_gio_pipe(&in1, &out2);
+ make_gio_pipe(&in2, &out1);
+
+ *p1 = g_simple_io_stream_new(in1, out1);
+ *p2 = g_simple_io_stream_new(in2, out2);
+
+ g_object_unref(in1);
+ g_object_unref(in2);
+ g_object_unref(out1);
+ g_object_unref(out2);
+}
diff --git a/src/giopipe.h b/src/giopipe.h
new file mode 100644
index 0000000..46c2c9c
--- /dev/null
+++ b/src/giopipe.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 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 __SPICE_GIO_PIPE_H__
+#define __SPICE_GIO_PIPE_H__
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+void spice_make_pipe(GIOStream **p1, GIOStream **p2);
+
+G_END_DECLS
+
+#endif /* __SPICE_GIO_PIPE_H__ */
diff --git a/src/glib-compat.c b/src/glib-compat.c
new file mode 100644
index 0000000..49edf73
--- /dev/null
+++ b/src/glib-compat.c
@@ -0,0 +1,79 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012-2014 Red Hat, Inc.
+ Copyright © 1998-2009 VLC authors and VideoLAN
+
+ 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 <string.h>
+
+#include "glib-compat.h"
+
+#if !GLIB_CHECK_VERSION(2,30,0)
+G_DEFINE_BOXED_TYPE (GMainContext, spice_main_context, g_main_context_ref, g_main_context_unref)
+#endif
+
+
+#if !GLIB_CHECK_VERSION(2,32,0)
+/**
+ * g_queue_free_full:
+ * @queue: a pointer to a #GQueue
+ * @free_func: the function to be called to free each element's data
+ *
+ * Convenience method, which frees all the memory used by a #GQueue,
+ * and calls the specified destroy function on every element's data.
+ *
+ * Since: 2.32
+ */
+void
+g_queue_free_full (GQueue *queue,
+ GDestroyNotify free_func)
+{
+ g_queue_foreach (queue, (GFunc) free_func, NULL);
+ g_queue_free (queue);
+}
+#endif
+
+
+#ifndef HAVE_STRTOK_R
+G_GNUC_INTERNAL
+char *strtok_r(char *s, const char *delim, char **save_ptr)
+{
+ char *token;
+
+ if (s == NULL)
+ s = *save_ptr;
+
+ /* Scan leading delimiters. */
+ s += strspn (s, delim);
+ if (*s == '\0')
+ return NULL;
+
+ /* Find the end of the token. */
+ token = s;
+ s = strpbrk (token, delim);
+ if (s == NULL)
+ /* This token finishes the string. */
+ *save_ptr = strchr (token, '\0');
+ else
+ {
+ /* Terminate the token and make *SAVE_PTR point past it. */
+ *s = '\0';
+ *save_ptr = s + 1;
+ }
+ return token;
+}
+#endif
diff --git a/src/glib-compat.h b/src/glib-compat.h
new file mode 100644
index 0000000..5491fe4
--- /dev/null
+++ b/src/glib-compat.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012-2014 Red Hat, Inc.
+ Copyright © 1998-2009 VLC authors and VideoLAN
+
+ 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 GLIB_COMPAT_H
+#define GLIB_COMPAT_H
+
+#include "config.h"
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+
+#if !GLIB_CHECK_VERSION(2,30,0)
+#define G_TYPE_MAIN_CONTEXT (spice_main_context_get_type ())
+GType spice_main_context_get_type (void) G_GNUC_CONST;
+#endif
+
+#if !GLIB_CHECK_VERSION(2,32,0)
+# define G_SIGNAL_DEPRECATED (1 << 9)
+
+#define G_SOURCE_CONTINUE TRUE
+#define G_SOURCE_REMOVE FALSE
+
+void
+g_queue_free_full (GQueue *queue,
+ GDestroyNotify free_func);
+#endif
+
+#ifndef g_clear_pointer
+#define g_clear_pointer(pp, destroy) \
+ G_STMT_START { \
+ G_STATIC_ASSERT (sizeof *(pp) == sizeof (gpointer)); \
+ /* Only one access, please */ \
+ gpointer *_pp = (gpointer *) (pp); \
+ gpointer _p; \
+ /* This assignment is needed to avoid a gcc warning */ \
+ GDestroyNotify _destroy = (GDestroyNotify) (destroy); \
+ \
+ (void) (0 ? (gpointer) *(pp) : 0); \
+ do \
+ _p = g_atomic_pointer_get (_pp); \
+ while G_UNLIKELY (!g_atomic_pointer_compare_and_exchange (_pp, _p, NULL)); \
+ \
+ if (_p) \
+ _destroy (_p); \
+ } G_STMT_END
+#endif
+
+#ifndef HAVE_STRTOK_R
+char* strtok_r(char *s, const char *delim, char **save_ptr);
+#endif
+
+#endif /* GLIB_COMPAT_H */
diff --git a/src/gtk-compat.h b/src/gtk-compat.h
new file mode 100644
index 0000000..be143b2
--- /dev/null
+++ b/src/gtk-compat.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012-2014 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 GTK_COMPAT_H
+#define GTK_COMPAT_H
+
+#include "config.h"
+
+#include <gtk/gtk.h>
+
+#if !GTK_CHECK_VERSION (2, 91, 0)
+#define GDK_IS_X11_DISPLAY(D) TRUE
+#define gdk_window_get_display(W) gdk_drawable_get_display(GDK_DRAWABLE(W))
+#endif
+
+#if GTK_CHECK_VERSION (2, 91, 0)
+static inline void gdk_drawable_get_size(GdkWindow *w, gint *ww, gint *wh)
+{
+ *ww = gdk_window_get_width(w);
+ *wh = gdk_window_get_height(w);
+}
+#endif
+
+#if !GTK_CHECK_VERSION(2, 20, 0)
+static inline gboolean gtk_widget_get_realized(GtkWidget *widget)
+{
+ g_return_val_if_fail (GTK_IS_WIDGET (widget), FALSE);
+ return GTK_WIDGET_REALIZED(widget);
+}
+#endif
+
+#if !GTK_CHECK_VERSION (3, 0, 0)
+#define cairo_rectangle_int_t GdkRectangle
+#define cairo_region_t GdkRegion
+#define cairo_region_create_rectangle gdk_region_rectangle
+#define cairo_region_subtract_rectangle(_dest,_rect) { GdkRegion *_region = gdk_region_rectangle (_rect); gdk_region_subtract (_dest, _region); gdk_region_destroy (_region); }
+#define cairo_region_destroy gdk_region_destroy
+
+#define gdk_window_get_display(W) gdk_drawable_get_display(GDK_DRAWABLE(W))
+#endif
+
+#endif /* GTK_COMPAT_H */
diff --git a/src/keymap-gen.pl b/src/keymap-gen.pl
new file mode 100755
index 0000000..56953f8
--- /dev/null
+++ b/src/keymap-gen.pl
@@ -0,0 +1,214 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+
+use Text::CSV;
+
+my %names = (
+ linux => [],
+ osx => []
+);
+
+my %namecolumns = (
+ linux => 0,
+ osx => 2,
+ win32 => 10,
+ x11 => 14,
+ );
+
+# Base data sources:
+#
+# linux: Linux: linux/input.h (master set)
+# osx: OS-X: Carbon/HIToolbox/Events.h (manually mapped)
+# atset1: AT Set 1: linux/drivers/input/keyboard/atkbd.c (atkbd_set2_keycode + atkbd_unxlate_table)
+# atset2: AT Set 2: linux/drivers/input/keyboard/atkbd.c (atkbd_set2_keycode)
+# atset3: AT Set 3: linux/drivers/input/keyboard/atkbd.c (atkbd_set3_keycode)
+# xt: XT: linux/drivers/input/keyboard/xt.c (xtkbd_keycode)
+# xtkbd: Linux RAW: linux/drivers/char/keyboard.c (x86_keycodes)
+# usb: USB HID: linux/drivers/hid/usbhid/usbkbd.c (usb_kbd_keycode)
+# win32: Win32: mingw32/winuser.h (manually mapped)
+# xwinxt: XWin XT: xorg-server/hw/xwin/{winkeybd.c,winkeynames.h} (xt + manually transcribed)
+# xkbdxt: XKBD XT: xf86-input-keyboard/src/at_scancode.c
+#(xt + manually transcribed)
+# x11: X11 keysyms: http://cgit.freedesktop.org/xorg/proto/x11proto/plain/keysymdef.h
+#
+# Derived data sources
+#
+# xorgevdev: Xorg + evdev: linux + an offset
+# xorgkbd: Xorg + kbd: xkbdxt + an offset
+# xorgxquartz: Xorg + OS-X: osx + an offset
+# xorgxwin: Xorg + Cygwin: xwinxt + an offset
+# rfb: XT over RFB: xtkbd + special re-encoding of high bit
+
+my @basemaps = qw(linux osx atset1 atset2 atset3 xt xtkbd usb win32 xwinxt xkbdxt x11);
+my @derivedmaps = qw(xorgevdev xorgkbd xorgxquartz xorgxwin rfb);
+my @maps = (@basemaps, @derivedmaps);
+
+my %maps;
+
+foreach my $map (@maps) {
+ $maps{$map} = [ [], [] ];
+}
+my %mapcolumns = (
+ osx => 3,
+ atset1 => 4,
+ atset2 => 5,
+ atset3 => 6,
+ xt => 7,
+ xtkbd => 8,
+ usb => 9,
+ win32 => 11,
+ xwinxt => 12,
+ xkbdxt => 13,
+ x11 => 15
+ );
+
+sub help {
+ my $msg = shift;
+ print $msg;
+ print "\n";
+ print "Valid keymaps are:\n";
+ print "\n";
+ foreach my $name (sort { $a cmp $b } keys %maps) {
+ print " $name\n";
+ }
+ print "\n";
+ exit (1);
+}
+
+if ($#ARGV != 2) {
+ help("syntax: $0 KEYMAPS SRCMAP DSTMAP\n");
+}
+
+my $keymaps = shift @ARGV;
+my $src = shift @ARGV;
+my $dst = shift @ARGV;
+
+help("$src is not a known keymap\n") unless exists $maps{$src};
+help("$dst is not a known keymap\n") unless exists $maps{$dst};
+
+
+open CSV, $keymaps
+ or die "cannot read $keymaps: $!";
+
+my $csv = Text::CSV->new();
+# Discard column headings
+$csv->getline(\*CSV);
+
+my $row;
+while ($row = $csv->getline(\*CSV)) {
+ my $linux = $row->[1];
+
+ $linux = hex($linux) if $linux =~ /0x/;
+
+ my $to = $maps{linux}->[0];
+ my $from = $maps{linux}->[1];
+ $to->[$linux] = $linux;
+ $from->[$linux] = $linux;
+
+ foreach my $name (keys %namecolumns) {
+ my $col = $namecolumns{$name};
+ my $val = $row->[$col];
+
+ $val = "" unless defined $val;
+
+ $names{$name}->[$linux] = $val;
+ }
+
+ foreach my $name (keys %mapcolumns) {
+ my $col = $mapcolumns{$name};
+ my $val = $row->[$col];
+
+ next unless defined $val && $val ne "";
+ $val = hex($val) if $val =~ /0x/;
+
+ $to = $maps{$name}->[0];
+ $from = $maps{$name}->[1];
+ $to->[$linux] = $val;
+ $from->[$val] = $linux;
+ }
+
+ # XXX there are some special cases in kbd to handle
+ # Xorg KBD driver is the Xorg KBD XT codes offset by +8
+ # The XKBD XT codes are the same as normal XT codes
+ # for values <= 83, and completely made up for extended
+ # scancodes :-(
+ ($to, $from) = @{$maps{xorgkbd}};
+ if (defined $maps{xkbdxt}->[0]->[$linux]) {
+ $to->[$linux] = $maps{xkbdxt}->[0]->[$linux] + 8;
+ $from->[$to->[$linux]] = $linux;
+ }
+
+ # Xorg evdev is simply Linux keycodes offset by +8
+ ($to, $from) = @{$maps{xorgevdev}};
+ $to->[$linux] = $linux + 8;
+ $from->[$to->[$linux]] = $linux;
+
+ # Xorg XQuartz is simply OS-X keycodes offset by +8
+ ($to, $from) = @{$maps{xorgxquartz}};
+ if (defined $maps{osx}->[0]->[$linux]) {
+ $to->[$linux] = $maps{osx}->[0]->[$linux] + 8;
+ $from->[$to->[$linux]] = $linux;
+ }
+
+ # RFB keycodes are XT kbd keycodes with a slightly
+ # different encoding of 0xe0 scan codes. RFB uses
+ # the high bit of the first byte, instead of the low
+ # bit of the second byte.
+ ($to, $from) = @{$maps{rfb}};
+ my $xtkbd = $maps{xtkbd}->[0]->[$linux];
+ if (defined $xtkbd) {
+ $to->[$linux] = $xtkbd ? (($xtkbd & 0x100)>>1) | ($xtkbd & 0x7f) : 0;
+ $from->[$to->[$linux]] = $linux;
+ }
+
+ # Xorg Cygwin is the Xorg Cygwin XT codes offset by +8
+ # The Cygwin XT codes are the same as normal XT codes
+ # for values <= 83, and completely made up for extended
+ # scancodes :-(
+ ($to, $from) = @{$maps{xorgxwin}};
+ if (defined $maps{xwinxt}->[0]->[$linux]) {
+ $to->[$linux] = $maps{xwinxt}->[0]->[$linux] + 8;
+ $from->[$to->[$linux]] = $linux;
+ }
+
+# print $linux, "\n";
+}
+
+close CSV;
+
+my $srcmap = $maps{$src}->[1];
+my $dstmap = $maps{$dst}->[0];
+
+printf "static const guint16 keymap_%s2%s[] = {\n", $src, $dst;
+
+for (my $i = 0 ; $i <= $#{$srcmap} ; $i++) {
+ my $linux = $srcmap->[$i] || 0;
+ my $j = $dstmap->[$linux];
+ next unless $linux && $j;
+
+ my $srcname = $names{$src}->[$linux] if exists $names{$src};
+ my $dstname = $names{$dst}->[$linux] if exists $names{$dst};
+ my $vianame = $names{linux}->[$linux] unless $src eq "linux" || $dst eq "linux";
+
+ $srcname = "" unless $srcname;
+ $dstname = "" unless $dstname;
+ $vianame = "" unless $vianame;
+ $srcname = " ($srcname)" if $srcname;
+ $dstname = " ($dstname)" if $dstname;
+ $vianame = " ($vianame)" if $vianame;
+
+ my $comment;
+ if ($src ne "linux" && $dst ne "linux") {
+ $comment = sprintf "%d%s => %d%s via %d%s", $i, $srcname, $j, $dstname, $linux, $vianame;
+ } else {
+ $comment = sprintf "%d%s => %d%s", $i, $srcname, $j, $dstname;
+ }
+
+ my $data = sprintf "[0x%x] = 0x%x,", $i, $j;
+
+ printf " %-20s /* %s */\n", $data, $comment;
+}
+
+print "};\n";
diff --git a/src/keymaps.csv b/src/keymaps.csv
new file mode 100644
index 0000000..9052e3b
--- /dev/null
+++ b/src/keymaps.csv
@@ -0,0 +1,490 @@
+"Linux Name","Linux Keycode","OS-X Name","OS-X Keycode","AT set1 keycode","AT set2 keycode","AT set3 keycode",XT,"XT KBD","USB Keycodes","Win32 Name","Win32 Keycode","Xwin XT","Xfree86 KBD XT","X11 keysym","X11 keycode"
+KEY_RESERVED,0,,,,,,,,,,,,,,
+KEY_ESC,1,Escape,0x35,1,118,8,1,1,41,VK_ESCAPE,0x1b,1,1,XK_Escape,0xff1b
+KEY_1,2,ANSI_1,0x12,2,22,22,2,2,30,VK_1,0x31,2,2,XK_1,0x0031
+KEY_2,3,ANSI_2,0x13,3,30,30,3,3,31,VK_2,0x32,3,3,XK_2,0x0032
+KEY_3,4,ANSI_3,0x14,4,38,38,4,4,32,VK_3,0x33,4,4,XK_3,0x0033
+KEY_4,5,ANSI_4,0x15,5,37,37,5,5,33,VK_4,0x34,5,5,XK_4,0x0034
+KEY_5,6,ANSI_5,0x17,6,46,46,6,6,34,VK_5,0x35,6,6,XK_5,0x0035
+KEY_6,7,ANSI_6,0x16,7,54,54,7,7,35,VK_6,0x36,7,7,XK_6,0x0036
+KEY_7,8,ANSI_7,0x1a,8,61,61,8,8,36,VK_7,0x37,8,8,XK_7,0x0037
+KEY_8,9,ANSI_8,0x1c,9,62,62,9,9,37,VK_8,0x38,9,9,XK_8,0x0038
+KEY_9,10,ANSI_9,0x19,10,70,70,10,10,38,VK_9,0x39,10,10,XK_9,0x0039
+KEY_0,11,ANSI_0,0x1d,11,69,69,11,11,39,VK_0,0x30,11,11,XK_0,0x0030
+KEY_MINUS,12,ANSI_Minus,0x1b,12,78,78,12,12,45,VK_OEM_MINUS,0xbd,12,12,XK_minus,0x002d
+KEY_EQUAL,13,ANSI_Equal,0x18,13,85,85,13,13,46,VK_OEM_PLUS,0xbb,13,13,XK_equal,0x003d
+KEY_BACKSPACE,14,Delete,0x33,14,102,102,14,14,42,VK_BACK,0x08,14,14,XK_BackSpace,0xff08
+KEY_TAB,15,Tab,0x30,15,13,13,15,15,43,VK_TAB,0x09,15,15,XK_Tab,0xff09
+KEY_Q,16,ANSI_Q,0xc,16,21,21,16,16,20,VK_Q,0x51,16,16,XK_Q,0x0051
+KEY_Q,16,ANSI_Q,0xc,16,21,21,16,16,20,VK_Q,0x51,16,16,XK_q,0x0071
+KEY_W,17,ANSI_W,0xd,17,29,29,17,17,26,VK_W,0x57,17,17,XK_W,0x0057
+KEY_W,17,ANSI_W,0xd,17,29,29,17,17,26,VK_W,0x57,17,17,XK_w,0x0077
+KEY_E,18,ANSI_E,0xe,18,36,36,18,18,8,VK_E,0x45,18,18,XK_E,0x0045
+KEY_E,18,ANSI_E,0xe,18,36,36,18,18,8,VK_E,0x45,18,18,XK_e,0x0065
+KEY_R,19,ANSI_R,0xf,19,45,45,19,19,21,VK_R,0x52,19,19,XK_R,0x0052
+KEY_R,19,ANSI_R,0xf,19,45,45,19,19,21,VK_R,0x52,19,19,XK_r,0x0072
+KEY_T,20,ANSI_T,0x11,20,44,44,20,20,23,VK_T,0x54,20,20,XK_T,0x0054
+KEY_T,20,ANSI_T,0x11,20,44,44,20,20,23,VK_T,0x54,20,20,XK_t,0x0074
+KEY_Y,21,ANSI_Y,0x10,21,53,53,21,21,28,VK_Y,0x59,21,21,XK_Y,0x0059
+KEY_Y,21,ANSI_Y,0x10,21,53,53,21,21,28,VK_Y,0x59,21,21,XK_y,0x0079
+KEY_U,22,ANSI_U,0x20,22,60,60,22,22,24,VK_U,0x55,22,22,XK_U,0x0055
+KEY_U,22,ANSI_U,0x20,22,60,60,22,22,24,VK_U,0x55,22,22,XK_u,0x0075
+KEY_I,23,ANSI_I,0x22,23,67,67,23,23,12,VK_I,0x49,23,23,XK_I,0x0049
+KEY_I,23,ANSI_I,0x22,23,67,67,23,23,12,VK_I,0x49,23,23,XK_i,0x0069
+KEY_O,24,ANSI_O,0x1f,24,68,68,24,24,18,VK_O,0x4f,24,24,XK_O,0x004f
+KEY_O,24,ANSI_O,0x1f,24,68,68,24,24,18,VK_O,0x4f,24,24,XK_o,0x006f
+KEY_P,25,ANSI_P,0x23,25,77,77,25,25,19,VK_P,0x50,25,25,XK_P,0x0050
+KEY_P,25,ANSI_P,0x23,25,77,77,25,25,19,VK_P,0x50,25,25,XK_p,0x0070
+KEY_LEFTBRACE,26,ANSI_LeftBracket,0x21,26,84,84,26,26,47,VK_OEM_4,0xdb,26,26,XK_bracketleft,0x005b
+KEY_RIGHTBRACE,27,ANSI_RightBracket,0x1e,27,91,91,27,27,48,VK_OEM_6,0xdd,27,27,XK_bracketright,0x005d
+KEY_ENTER,28,Return,0x24,28,90,90,28,28,40,VK_RETURN,0x0d,28,28,XK_Return,0xff0d
+KEY_LEFTCTRL,29,Control,0x3b,29,20,17,29,29,224,VK_LCONTROL,0xa2,29,29,XK_Control_L,0xffe3
+KEY_LEFTCTRL,29,Control,0x3b,29,20,17,29,29,224,VK_CONTROL,0x11,29,29,XK_Control_L,0xffe3
+KEY_A,30,ANSI_A,0x0,30,28,28,30,30,4,VK_A,0x41,30,30,XK_A,0x0041
+KEY_A,30,ANSI_A,0x0,30,28,28,30,30,4,VK_A,0x41,30,30,XK_a,0x0061
+KEY_S,31,ANSI_S,0x1,31,27,27,31,31,22,VK_S,0x53,31,31,XK_S,0x0053
+KEY_S,31,ANSI_S,0x1,31,27,27,31,31,22,VK_S,0x53,31,31,XK_s,0x0073
+KEY_D,32,ANSI_D,0x2,32,35,35,32,32,7,VK_D,0x44,32,32,XK_D,0x0044
+KEY_D,32,ANSI_D,0x2,32,35,35,32,32,7,VK_D,0x44,32,32,XK_d,0x0064
+KEY_F,33,ANSI_F,0x3,33,43,43,33,33,9,VK_F,0x46,33,33,XK_F,0x0046
+KEY_F,33,ANSI_F,0x3,33,43,43,33,33,9,VK_F,0x46,33,33,XK_f,0x0066
+KEY_G,34,ANSI_G,0x5,34,52,52,34,34,10,VK_G,0x47,34,34,XK_G,0x0047
+KEY_G,34,ANSI_G,0x5,34,52,52,34,34,10,VK_G,0x47,34,34,XK_g,0x0067
+KEY_H,35,ANSI_H,0x4,35,51,51,35,35,11,VK_H,0x48,35,35,XK_H,0x0048
+KEY_H,35,ANSI_H,0x4,35,51,51,35,35,11,VK_H,0x48,35,35,XK_h,0x0068
+KEY_J,36,ANSI_J,0x26,36,59,59,36,36,13,VK_J,0x4a,36,36,XK_J,0x004a
+KEY_J,36,ANSI_J,0x26,36,59,59,36,36,13,VK_J,0x4a,36,36,XK_j,0x006a
+KEY_K,37,ANSI_K,0x28,37,66,66,37,37,14,VK_K,0x4b,37,37,XK_K,0x004b
+KEY_K,37,ANSI_K,0x28,37,66,66,37,37,14,VK_K,0x4b,37,37,XK_K,0x006b
+KEY_L,38,ANSI_L,0x25,38,75,75,38,38,15,VK_L,0x4c,38,38,XK_L,0x004c
+KEY_L,38,ANSI_L,0x25,38,75,75,38,38,15,VK_L,0x4c,38,38,XK_l,0x006c
+KEY_SEMICOLON,39,ANSI_Semicolon,0x29,39,76,76,39,39,51,VK_OEM_1,0xba,39,39,XK_semicolon,0x003b
+KEY_APOSTROPHE,40,ANSI_Quote,0x27,40,82,82,40,40,52,VK_OEM_7,0xde,40,40,XK_apostrophe,0x0027
+KEY_GRAVE,41,ANSI_Grave,0x32,41,14,14,41,41,53,VK_OEM_3,0xc0,41,41,XK_grave,0x0060
+KEY_SHIFT,42,Shift,0x38,42,18,18,42,42,225,VK_SHIFT,0x10,42,42,XK_Shift_L,0xffe1
+KEY_LEFTSHIFT,42,Shift,0x38,42,18,18,42,42,225,VK_LSHIFT,0xa0,42,42,XK_Shift_L,0xffe1
+KEY_BACKSLASH,43,ANSI_Backslash,0x2a,43,93,93,43,43,50,VK_OEM_5,0xdc,43,43,XK_backslash,0x005c
+KEY_Z,44,ANSI_Z,0x6,44,26,26,44,44,29,VK_Z,0x5a,44,44,XK_Z,0x005a
+KEY_Z,44,ANSI_Z,0x6,44,26,26,44,44,29,VK_Z,0x5a,44,44,XK_z,0x007a
+KEY_X,45,ANSI_X,0x7,45,34,34,45,45,27,VK_X,0x58,45,45,XK_X,0x0058
+KEY_X,45,ANSI_X,0x7,45,34,34,45,45,27,VK_X,0x58,45,45,XK_x,0x0078
+KEY_C,46,ANSI_C,0x8,46,33,33,46,46,6,VK_C,0x43,46,46,XK_C,0x0043
+KEY_C,46,ANSI_C,0x8,46,33,33,46,46,6,VK_C,0x43,46,46,XK_c,0x0063
+KEY_V,47,ANSI_V,0x9,47,42,42,47,47,25,VK_V,0x56,47,47,XK_V,0x0056
+KEY_V,47,ANSI_V,0x9,47,42,42,47,47,25,VK_V,0x56,47,47,XK_v,0x0076
+KEY_B,48,ANSI_B,0xb,48,50,50,48,48,5,VK_B,0x42,48,48,XK_B,0x0042
+KEY_B,48,ANSI_B,0xb,48,50,50,48,48,5,VK_B,0x42,48,48,XK_b,0x0062
+KEY_N,49,ANSI_N,0x2d,49,49,49,49,49,17,VK_N,0x4e,49,49,XK_N,0x004e
+KEY_N,49,ANSI_N,0x2d,49,49,49,49,49,17,VK_N,0x4e,49,49,XK_n,0x006e
+KEY_M,50,ANSI_M,0x2e,50,58,58,50,50,16,VK_M,0x4d,50,50,XK_M,0x004d
+KEY_M,50,ANSI_M,0x2e,50,58,58,50,50,16,VK_M,0x4d,50,50,XK_m,0x006d
+KEY_COMMA,51,ANSI_Comma,0x2b,51,65,65,51,51,54,VK_OEM_COMMA,0xbc,51,51,XK_comma,0x002c
+KEY_DOT,52,ANSI_Period,0x2f,52,73,73,52,52,55,VK_OEM_PERIOD,0xbe,52,52,XK_period,0x002e
+KEY_SLASH,53,ANSI_Slash,0x2c,53,74,74,53,53,56,VK_OEM_2,0xbf,53,53,XK_slash,0x002f
+KEY_RIGHTSHIFT,54,RightShift,0x3c,54,89,89,54,54,229,VK_RSHIFT,0xa1,54,54,XK_Shift_R,0xffe2
+KEY_KPASTERISK,55,ANSI_KeypadMultiply,0x43,55,124,126,55,55,85,VK_MULTIPLY,0x6a,55,55,XK_multiply,0x00d7
+KEY_LEFTALT,56,Option,0x3a,56,17,25,56,56,226,VK_LMENU,0xa4,56,56,XK_Alt_L,0xffe9
+KEY_LEFTALT,56,Option,0x3a,56,17,25,56,56,226,VK_MENU,0x12,56,56,XK_Alt_L,0xffe9
+KEY_SPACE,57,Space,0x31,57,41,41,57,57,44,VK_SPACE,0x20,57,57,XK_space,0x0020
+KEY_CAPSLOCK,58,CapsLock,0x39,58,88,20,58,58,57,VK_CAPITAL,0x14,58,58,XK_Caps_Lock,0xffe5
+KEY_F1,59,F1,0x7a,59,5,7,59,59,58,VK_F1,0x70,59,59,XK_F1,0xffbe
+KEY_F2,60,F2,0x78,60,6,15,60,60,59,VK_F2,0x71,60,60,XK_F2,0xffbf
+KEY_F3,61,F3,0x63,61,4,23,61,61,60,VK_F3,0x72,61,61,XK_F3,0xffc0
+KEY_F4,62,F4,0x76,62,12,31,62,62,61,VK_F4,0x73,62,62,XK_F4,0xffc1
+KEY_F5,63,F5,0x60,63,3,39,63,63,62,VK_F5,0x74,63,63,XK_F5,0xffc2
+KEY_F6,64,F6,0x61,64,11,47,64,64,63,VK_F6,0x75,64,64,XK_F6,0xffc3
+KEY_F7,65,F7,0x62,65,259,55,65,65,64,VK_F7,0x76,65,65,XK_F7,0xffc4
+KEY_F8,66,F8,0x64,66,10,63,66,66,65,VK_F8,0x77,66,66,XK_F8,0xffc5
+KEY_F9,67,F9,0x65,67,1,71,67,67,66,VK_F9,0x78,67,67,XK_F9,0xffc6
+KEY_F10,68,F10,0x6d,68,9,79,68,68,67,VK_F10,0x79,68,68,XK_F10,0xffc7
+KEY_NUMLOCK,69,,,69,119,118,69,69,83,VK_NUMLOCK,0x90,69,69,XK_Num_Lock,0xff7f
+KEY_SCROLLLOCK,70,,,70,126,95,70,70,71,VK_SCROLL,0x91,70,70,XK_Scroll_Lock,0xff14
+KEY_KP7,71,ANSI_Keypad7,0x59,71,108,108,71,71,95,VK_NUMPAD7,0x67,71,71,XK_KP_7,0xffb7
+KEY_KP8,72,ANSI_Keypad8,0x5b,72,117,117,72,72,96,VK_NUMPAD8,0x68,72,72,XK_KP_8,0xffb8
+KEY_KP9,73,ANSI_Keypad9,0x5c,73,125,125,73,73,97,VK_NUMPAD9,0x69,73,73,XK_KP_9,0xffb9
+KEY_KPMINUS,74,ANSI_KeypadMinus,0x4e,74,123,132,74,74,86,VK_SUBTRACT,0x6d,74,74,XK_KP_Subtract,0xffad
+KEY_KP4,75,ANSI_Keypad4,0x56,75,107,107,75,75,92,VK_NUMPAD4,0x64,75,75,XK_KP_4,0xffb4
+KEY_KP5,76,ANSI_Keypad5,0x57,76,115,115,76,76,93,VK_NUMPAD5,0x65,76,76,XK_KP_5,0xffb5
+KEY_KP6,77,ANSI_Keypad6,0x58,77,116,116,77,77,94,VK_NUMPAD6,0x66,77,77,XK_KP_6,0xffb6
+KEY_KPPLUS,78,ANSI_KeypadPlus,0x45,78,121,124,78,78,87,VK_ADD,0x6b,78,78,XK_KP_Add,0xffab
+KEY_KP1,79,ANSI_Keypad1,0x53,79,105,105,79,79,89,VK_NUMPAD1,0x61,79,79,XK_KP_1,0xffb1
+KEY_KP2,80,ANSI_Keypad2,0x54,80,114,114,80,80,90,VK_NUMPAD2,0x62,80,80,XK_KP_2,0xffb2
+KEY_KP3,81,ANSI_Keypad3,0x55,81,122,122,81,81,91,VK_NUMPAD3,0x63,81,81,XK_KP_3,0xffb3
+KEY_KP0,82,ANSI_Keypad0,0x52,82,112,112,82,82,98,VK_NUMPAD0,0x60,82,82,XK_KP_0,0xffb0
+KEY_KPDOT,83,ANSI_KeypadDecimal,0x41,83,113,113,83,83,99,VK_DECIMAL,0x6e,83,83,XK_KP_Decimal,0xffae
+,84,,,,,,,84,,,,,
+KEY_ZENKAKUHANKAKU,85,,,118,95,,,118,148,,,,
+KEY_102ND,86,,,86,97,19,,86,100,VK_OEM_102,0xe1,,
+KEY_F11,87,F11,0x67,87,120,86,101,87,68,VK_F11,0x7a,,
+KEY_F12,88,F12,0x6f,88,7,94,102,88,69,VK_F12,0x7b,,
+KEY_RO,89,,,115,81,,,115,135,,,,
+KEY_KATAKANA,90,JIS_Kana????,0x68,120,99,,,120,146,VK_KANA,0x15,,
+KEY_HIRAGANA,91,,,119,98,,,119,147,,,,
+KEY_HENKAN,92,,,121,100,134,,121,138,,,,
+KEY_KATAKANAHIRAGANA,93,,,112,19,135,,112,136,,,0xc8,0xc8
+KEY_MUHENKAN,94,,,123,103,133,,123,139,,,,
+KEY_KPJPCOMMA,95,JIS_KeypadComma,0x5f,92,39,,,92,140,,,,,XK_KP_Separator,0xffac
+KEY_KPENTER,96,ANSI_KeypadEnter,0x4c,,158,121,,284,88,,,0x64,0x64,XK_KP_Enter,0xff8d
+KEY_RIGHTCTRL,97,RightControl,0x3e,,,88,,285,228,VK_RCONTROL,0xa3,0x65,0x65,XK_Control_R,0xffe4
+KEY_KPSLASH,98,ANSI_KeypadDivide,0x4b,,181,119,,309,84,VK_DIVIDE,0x6f,0x68,0x68,XK_KP_Divide,0xffaf
+KEY_SYSRQ,99,,,84,260,87,,84,70,"VK_SNAPSHOT ???",0x2c,0x67,0x67,XK_Sys_Req,0xff15
+KEY_RIGHTALT,100,RightOption,0x3d,,,57,,312,230,VK_RMENU,0xa5,0x69,0x69,XK_Alt_R,0xffea
+KEY_LINEFEED,101,,,,,,,91,,,,,
+KEY_HOME,102,Home,0x73,,224,110,,327,74,VK_HOME,0x24,0x59,0x59,XK_Home,0xff50
+KEY_UP,103,UpArrow,0x7e,,236,99,109,328,82,VK_UP,0x26,0x5a,0x5a,XK_Up,0xff52
+KEY_PAGEUP,104,PageUp,0x74,,201,111,,329,75,VK_PRIOR,0x21,0x5b,0x5b,XK_Page_Up,0xff55
+KEY_LEFT,105,LeftArrow,0x7b,,203,97,111,331,80,VK_LEFT,0x25,0x5c,0x5c,XK_Left,0xff51
+KEY_RIGHT,106,RightArrow,0x7c,,205,106,112,333,79,VK_RIGHT,0x27,0x5e,0x5e,XK_Right,0xff53
+KEY_END,107,End,0x77,,225,101,,335,77,VK_END,0x23,0x5f,0x5f,XK_End,0xff57
+KEY_DOWN,108,DownArrow,0x7d,,254,96,110,336,81,VK_DOWN,0x28,0x60,0x60,XK_Down,0xff54
+KEY_PAGEDOWN,109,PageDown,0x79,,243,109,,337,78,VK_NEXT,0x22,0x61,0x61,XK_Page_Down,0xff56
+KEY_INSERT,110,,,,210,103,107,338,73,VK_INSERT,0x2d,0x62,0x62,XK_Insert,0xff63
+KEY_DELETE,111,ForwardDelete,0x75,,244,100,108,339,76,VK_DELETE,0x2e,0x63,0x63,XK_Delete,0xffff
+KEY_MACRO,112,,,,239,142,,367,,,,,
+KEY_MUTE,113,Mute,0x4a,,251,156,,288,239,VK_VOLUME_MUTE,0xad,,,
+KEY_VOLUMEDOWN,114,VolumeDown,0x49,,,157,,302,238,VK_VOLUME_DOWN,0xae,,
+KEY_VOLUMEUP,115,VolumeUp,0x48,,233,149,,304,237,VK_VOLUME_UP,0xaf,,
+KEY_POWER,116,,,,,,,350,102,,,,
+KEY_KPEQUAL,117,ANSI_KeypadEquals,0x51,89,15,,,89,103,,,0x76,0x76,XK_KP_Equal,0xffbd
+KEY_KPPLUSMINUS,118,,,,206,,,334,,,,,
+KEY_PAUSE,119,,,,198,98,,326,72,VK_PAUSE,0x013,0x66,0x66,XK_Pause,0xff13
+KEY_SCALE,120,,,,,,,267,,,,,
+KEY_KPCOMMA,121,ANSI_KeypadClear????,0x47,126,109,,,126,133,VK_SEPARATOR??,0x6c,,
+KEY_HANGEUL,122,,,,,,,,144,VK_HANGEUL,0x15,,
+KEY_HANJA,123,,,,,,,269,145,VK_HANJA,0x19,,
+KEY_YEN,124,JIS_Yen,0x5d,125,106,,,125,137,,,0x7d,0x7d
+KEY_LEFTMETA,125,Command,0x37,,,139,,347,227,VK_LWIN,0x5b,0x6b,0x6b,XK_Meta_L,0xffe7
+KEY_RIGHTMETA,126,,,,,140,,348,231,VK_RWIN,0x5c,0x6c,0x6c,XK_Meta_R,0xffe8
+KEY_COMPOSE,127,Function,0x3f,,,141,,349,101,VK_APPS,0x5d,0x6d,0x6d
+KEY_STOP,128,,,,,10,,360,243,VK_BROWSER_STOP,0xa9,,
+KEY_AGAIN,129,,,,,11,,261,121,,,,
+KEY_PROPS,130,,,,,12,,262,118,,,,
+KEY_UNDO,131,,,,,16,,263,122,,,,
+KEY_FRONT,132,,,,,,,268,119,,,,
+KEY_COPY,133,,,,,24,,376,124,,,,
+KEY_OPEN,134,,,,,32,,100,116,,,,
+KEY_PASTE,135,,,,,40,,101,125,,,,
+KEY_FIND,136,,,,,48,,321,244,,,,
+KEY_CUT,137,,,,,56,,316,123,,,,
+KEY_HELP,138,,,,,9,,373,117,VK_HELP,0x2f,,,XK_Help,0xff6a
+KEY_MENU,139,,,,,145,,286,,,,,
+KEY_CALC,140,,,,174,163,,289,251,,,,
+KEY_SETUP,141,,,,,,,102,,,,,
+KEY_SLEEP,142,,,,,,,351,248,VK_SLEEP,0x5f,,
+KEY_WAKEUP,143,,,,,,,355,,,,,
+KEY_FILE,144,,,,,,,103,,,,,
+KEY_SENDFILE,145,,,,,,,104,,,,,
+KEY_DELETEFILE,146,,,,,,,105,,,,,
+KEY_XFER,147,,,,,162,,275,,,,,
+KEY_PROG1,148,,,,,160,,287,,,,,
+KEY_PROG2,149,,,,,161,,279,,,,,
+KEY_WWW,150,,,,,,,258,240,,,,
+KEY_MSDOS,151,,,,,,,106,,,,,
+KEY_SCREENLOCK,152,,,,,150,,274,249,,,,
+KEY_DIRECTION,153,,,,,,,107,,,,,
+KEY_CYCLEWINDOWS,154,,,,,155,,294,,,,,
+KEY_MAIL,155,,,,,,,364,,,,,
+KEY_BOOKMARKS,156,,,,,,,358,,,,,
+KEY_COMPUTER,157,,,,,,,363,,,,,
+KEY_BACK,158,,,,,,,362,241,VK_BROWSER_BACK,0xa6,,
+KEY_FORWARD,159,,,,,,,361,242,VK_BROWSER_FORWARD,0xa7,,
+KEY_CLOSECD,160,,,,,154,,291,,,,,
+KEY_EJECTCD,161,,,,,,,108,236,,,,
+KEY_EJECTCLOSECD,162,,,,,,,381,,,,,
+KEY_NEXTSONG,163,,,,241,147,,281,235,VK_MEDIA_NEXT_TRACK,0xb0,,
+KEY_PLAYPAUSE,164,,,,173,,,290,232,VK_MEDIA_PLAY_PAUSE,0xb3,,
+KEY_PREVIOUSSONG,165,,,,250,148,,272,234,VK_MEDIA_PREV_TRACK,0xb1,,
+KEY_STOPCD,166,,,,164,152,,292,233,VK_MEDIA_STOP,0xb2,,
+KEY_RECORD,167,,,,,158,,305,,,,,
+KEY_REWIND,168,,,,,159,,280,,,,,
+KEY_PHONE,169,,,,,,,99,,,,,
+KEY_ISO,170,ISO_Section,0xa,,,,,112,,,,,
+KEY_CONFIG,171,,,,,,,257,,,,,
+KEY_HOMEPAGE,172,,,,178,151,,306,,VK_BROWSER_HOME,0xac,,
+KEY_REFRESH,173,,,,,,,359,250,VK_BROWSER_REFRESH,0xa8,,
+KEY_EXIT,174,,,,,,,113,,,,,
+KEY_MOVE,175,,,,,,,114,,,,,
+KEY_EDIT,176,,,,,,,264,247,,,,
+KEY_SCROLLUP,177,,,,,,,117,245,,,,
+KEY_SCROLLDOWN,178,,,,,,,271,246,,,,
+KEY_KPLEFTPAREN,179,,,,,,,374,182,,,,
+KEY_KPRIGHTPAREN,180,,,,,,,379,183,,,,
+KEY_NEW,181,,,,,,,265,,,,,
+KEY_REDO,182,,,,,,,266,,,,,
+KEY_F13,183,F13,0x69,93,47,127,,93,104,VK_F13,0x7c,0x6e,0x6e
+KEY_F14,184,F14,0x6b,94,55,128,,94,105,VK_F14,0x7d,0x6f,0x6f
+KEY_F15,185,F15,0x71,95,63,129,,95,106,VK_F15,0x7e,0x70,0x70
+KEY_F16,186,F16,0x6a,,,130,,85,107,VK_F16,0x7f,0x71,0x71
+KEY_F17,187,F17,0x40,,,131,,259,108,VK_F17,0x80,0x72,0x72
+KEY_F18,188,F18,0x4f,,,,,375,109,VK_F18,0x81,,
+KEY_F19,189,F19,0x50,,,,,260,110,VK_F19,0x82,,
+KEY_F20,190,F20,0x5a,,,,,90,111,VK_F20,0x83,,
+KEY_F21,191,,,,,,,116,112,VK_F21,0x84,,
+KEY_F22,192,,,,,,,377,113,VK_F22,0x85,,
+KEY_F23,193,,,,,,,109,114,VK_F23,0x86,,
+KEY_F24,194,,,,,,,111,115,VK_F24,0x87,,
+,195,,,,,,,277,,,,,
+,196,,,,,,,278,,,,,
+,197,,,,,,,282,,,,,
+,198,,,,,,,283,,,,,
+,199,,,,,,,295,,,,,
+KEY_PLAYCD,200,,,,,,,296,,,,,
+KEY_PAUSECD,201,,,,,,,297,,,,,
+KEY_PROG3,202,,,,,,,299,,,,,
+KEY_PROG4,203,,,,,,,300,,,,,
+KEY_DASHBOARD,204,,,,,,,301,,,,,
+KEY_SUSPEND,205,,,,,,,293,,,,,
+KEY_CLOSE,206,,,,,,,303,,,,,
+KEY_PLAY,207,,,,,,,307,,VK_PLAY,0xfa,,
+KEY_FASTFORWARD,208,,,,,,,308,,,,,
+KEY_BASSBOOST,209,,,,,,,310,,,,,
+KEY_PRINT,210,,,,,,,313,,VK_PRINT,0x2a,,
+KEY_HP,211,,,,,,,314,,,,,
+KEY_CAMERA,212,,,,,,,315,,,,,
+KEY_SOUND,213,,,,,,,317,,,,,
+KEY_QUESTION,214,,,,,,,318,,,,,
+KEY_EMAIL,215,,,,,,,319,,VK_LAUNCH_MAIL,0xb4,,
+KEY_CHAT,216,,,,,,,320,,,,,
+KEY_SEARCH,217,,,,,,,357,,VK_BROWSER_SEARCH,0xaa,,
+KEY_CONNECT,218,,,,,,,322,,,,,
+KEY_FINANCE,219,,,,,,,323,,,,,
+KEY_SPORT,220,,,,,,,324,,,,,
+KEY_SHOP,221,,,,,,,325,,,,,
+KEY_ALTERASE,222,,,,,,,276,,,,,
+KEY_CANCEL,223,,,,,,,330,,,,,
+KEY_BRIGHTNESSDOWN,224,,,,,,,332,,,,,
+KEY_BRIGHTNESSUP,225,,,,,,,340,,,,,
+KEY_MEDIA,226,,,,,,,365,,,,,
+KEY_SWITCHVIDEOMODE,227,,,,,,,342,,,,,
+KEY_KBDILLUMTOGGLE,228,,,,,,,343,,,,,
+KEY_KBDILLUMDOWN,229,,,,,,,344,,,,,
+KEY_KBDILLUMUP,230,,,,,,,345,,,,,
+KEY_SEND,231,,,,,,,346,,,,,
+KEY_REPLY,232,,,,,,,356,,,,,
+KEY_FORWARDMAIL,233,,,,,,,270,,,,,
+KEY_SAVE,234,,,,,,,341,,,,,
+KEY_DOCUMENTS,235,,,,,,,368,,,,,
+KEY_BATTERY,236,,,,,,,369,,,,,
+KEY_BLUETOOTH,237,,,,,,,370,,,,,
+KEY_WLAN,238,,,,,,,371,,,,,
+KEY_UWB,239,,,,,,,372,,,,,
+KEY_UNKNOWN,240,,,,,,,,,,,,
+KEY_VIDEO_NEXT,241,,,,,,,,,,,,
+KEY_VIDEO_PREV,242,,,,,,,,,,,,
+KEY_BRIGHTNESS_CYCLE,243,,,,,,,,,,,,
+KEY_BRIGHTNESS_ZERO,244,,,,,,,,,,,,
+KEY_DISPLAY_OFF,245,,,,,,,,,,,,
+KEY_WIMAX,246,,,,,,,,,,,,
+,247,,,,,,,,,,,,
+,248,,,,,,,,,,,,
+,249,,,,,,,,,,,,
+,250,,,,,,,,,,,,
+,251,,,,,,,,,,,,
+,252,,,,,,,,,,,,
+,253,,,,,,,,,,,,
+,254,,,,,,,,,,,,
+,255,,,,182,,,,,,,,
+BTN_MISC,0x100,,,,,,,,,,,,
+BTN_0,0x100,,,,,,,,,VK_LBUTTON,0x01,,
+BTN_1,0x101,,,,,,,,,VK_RBUTTON,0x02,,
+BTN_2,0x102,,,,,,,,,VK_MBUTTON,0x04,,
+BTN_3,0x103,,,,,,,,,VK_XBUTTON1,0x05,,
+BTN_4,0x104,,,,,,,,,VK_XBUTTON2,0x06,,
+BTN_5,0x105,,,,,,,,,,,,
+BTN_6,0x106,,,,,,,,,,,,
+BTN_7,0x107,,,,,,,,,,,,
+BTN_8,0x108,,,,,,,,,,,,
+BTN_9,0x109,,,,,,,,,,,,
+BTN_MOUSE,0x110,,,,,,,,,,,,
+BTN_LEFT,0x110,,,,,,,,,,,,
+BTN_RIGHT,0x111,,,,,,,,,,,,
+BTN_MIDDLE,0x112,,,,,,,,,,,,
+BTN_SIDE,0x113,,,,,,,,,,,,
+BTN_EXTRA,0x114,,,,,,,,,,,,
+BTN_FORWARD,0x115,,,,,,,,,,,,
+BTN_BACK,0x116,,,,,,,,,,,,
+BTN_TASK,0x117,,,,,,,,,,,,
+BTN_JOYSTICK,0x120,,,,,,,,,,,,
+BTN_TRIGGER,0x120,,,,,,,,,,,,
+BTN_THUMB,0x121,,,,,,,,,,,,
+BTN_THUMB2,0x122,,,,,,,,,,,,
+BTN_TOP,0x123,,,,,,,,,,,,
+BTN_TOP2,0x124,,,,,,,,,,,,
+BTN_PINKIE,0x125,,,,,,,,,,,,
+BTN_BASE,0x126,,,,,,,,,,,,
+BTN_BASE2,0x127,,,,,,,,,,,,
+BTN_BASE3,0x128,,,,,,,,,,,,
+BTN_BASE4,0x129,,,,,,,,,,,,
+BTN_BASE5,0x12a,,,,,,,,,,,,
+BTN_BASE6,0x12b,,,,,,,,,,,,
+BTN_DEAD,0x12f,,,,,,,,,,,,
+BTN_GAMEPAD,0x130,,,,,,,,,,,,
+BTN_A,0x130,,,,,,,,,,,,
+BTN_B,0x131,,,,,,,,,,,,
+BTN_C,0x132,,,,,,,,,,,,
+BTN_X,0x133,,,,,,,,,,,,
+BTN_Y,0x134,,,,,,,,,,,,
+BTN_Z,0x135,,,,,,,,,,,,
+BTN_TL,0x136,,,,,,,,,,,,
+BTN_TR,0x137,,,,,,,,,,,,
+BTN_TL2,0x138,,,,,,,,,,,,
+BTN_TR2,0x139,,,,,,,,,,,,
+BTN_SELECT,0x13a,,,,,,,,,,,,
+BTN_START,0x13b,,,,,,,,,,,,
+BTN_MODE,0x13c,,,,,,,,,,,,
+BTN_THUMBL,0x13d,,,,,,,,,,,,
+BTN_THUMBR,0x13e,,,,,,,,,,,,
+BTN_DIGI,0x140,,,,,,,,,,,,
+BTN_TOOL_PEN,0x140,,,,,,,,,,,,
+BTN_TOOL_RUBBER,0x141,,,,,,,,,,,,
+BTN_TOOL_BRUSH,0x142,,,,,,,,,,,,
+BTN_TOOL_PENCIL,0x143,,,,,,,,,,,,
+BTN_TOOL_AIRBRUSH,0x144,,,,,,,,,,,,
+BTN_TOOL_FINGER,0x145,,,,,,,,,,,,
+BTN_TOOL_MOUSE,0x146,,,,,,,,,,,,
+BTN_TOOL_LENS,0x147,,,,,,,,,,,,
+BTN_TOUCH,0x14a,,,,,,,,,,,,
+BTN_STYLUS,0x14b,,,,,,,,,,,,
+BTN_STYLUS2,0x14c,,,,,,,,,,,,
+BTN_TOOL_DOUBLETAP,0x14d,,,,,,,,,,,,
+BTN_TOOL_TRIPLETAP,0x14e,,,,,,,,,,,,
+BTN_TOOL_QUADTAP,0x14f,,,,,,,,,,,,
+BTN_WHEEL,0x150,,,,,,,,,,,,
+BTN_GEAR_DOWN,0x150,,,,,,,,,,,,
+BTN_GEAR_UP,0x151,,,,,,,,,,,,
+KEY_OK,0x160,,,,,,,,,,,,
+KEY_SELECT,0x161,,,,,,,,,VK_SELECT,0x29,,,XK_Select,0xff60
+KEY_GOTO,0x162,,,,,,,,,,,,
+KEY_CLEAR,0x163,,,,,,,,,,,,
+KEY_POWER2,0x164,,,,,,,,,,,,
+KEY_OPTION,0x165,,,,,,,,,,,,
+KEY_INFO,0x166,,,,,,,,,,,,
+KEY_TIME,0x167,,,,,,,,,,,,
+KEY_VENDOR,0x168,,,,,,,,,,,,
+KEY_ARCHIVE,0x169,,,,,,,,,,,,
+KEY_PROGRAM,0x16a,,,,,,,,,,,,
+KEY_CHANNEL,0x16b,,,,,,,,,,,,
+KEY_FAVORITES,0x16c,,,,,,,,,VK_BROWSER_FAVOURITES,0xab,,
+KEY_EPG,0x16d,,,,,,,,,,,,
+KEY_PVR,0x16e,,,,,,,,,,,,
+KEY_MHP,0x16f,,,,,,,,,,,,
+KEY_LANGUAGE,0x170,,,,,,,,,,,,
+KEY_TITLE,0x171,,,,,,,,,,,,
+KEY_SUBTITLE,0x172,,,,,,,,,,,,
+KEY_ANGLE,0x173,,,,,,,,,,,,
+KEY_ZOOM,0x174,,,,,,,,,VK_ZOOM,0xfb,,
+KEY_MODE,0x175,,,,,,,,,,,,
+KEY_KEYBOARD,0x176,,,,,,,,,,,,
+KEY_SCREEN,0x177,,,,,,,,,,,,
+KEY_PC,0x178,,,,,,,,,,,,
+KEY_TV,0x179,,,,,,,,,,,,
+KEY_TV2,0x17a,,,,,,,,,,,,
+KEY_VCR,0x17b,,,,,,,,,,,,
+KEY_VCR2,0x17c,,,,,,,,,,,,
+KEY_SAT,0x17d,,,,,,,,,,,,
+KEY_SAT2,0x17e,,,,,,,,,,,,
+KEY_CD,0x17f,,,,,,,,,,,,
+KEY_TAPE,0x180,,,,,,,,,,,,
+KEY_RADIO,0x181,,,,,,,,,,,,
+KEY_TUNER,0x182,,,,,,,,,,,,
+KEY_PLAYER,0x183,,,,,,,,,,,,
+KEY_TEXT,0x184,,,,,,,,,,,,
+KEY_DVD,0x185,,,,,,,,,,,,
+KEY_AUX,0x186,,,,,,,,,,,,
+KEY_MP3,0x187,,,,,,,,,,,,
+KEY_AUDIO,0x188,,,,,,,,,,,,
+KEY_VIDEO,0x189,,,,,,,,,,,,
+KEY_DIRECTORY,0x18a,,,,,,,,,,,,
+KEY_LIST,0x18b,,,,,,,,,,,,
+KEY_MEMO,0x18c,,,,,,,,,,,,
+KEY_CALENDAR,0x18d,,,,,,,,,,,,
+KEY_RED,0x18e,,,,,,,,,,,,
+KEY_GREEN,0x18f,,,,,,,,,,,,
+KEY_YELLOW,0x190,,,,,,,,,,,,
+KEY_BLUE,0x191,,,,,,,,,,,,
+KEY_CHANNELUP,0x192,,,,,,,,,,,,
+KEY_CHANNELDOWN,0x193,,,,,,,,,,,,
+KEY_FIRST,0x194,,,,,,,,,,,,
+KEY_LAST,0x195,,,,,,,,,,,,
+KEY_AB,0x196,,,,,,,,,,,,
+KEY_NEXT,0x197,,,,,,,,,,,,
+KEY_RESTART,0x198,,,,,,,,,,,,
+KEY_SLOW,0x199,,,,,,,,,,,,
+KEY_SHUFFLE,0x19a,,,,,,,,,,,,
+KEY_BREAK,0x19b,,,,,,,,,,,,
+KEY_PREVIOUS,0x19c,,,,,,,,,,,,
+KEY_DIGITS,0x19d,,,,,,,,,,,,
+KEY_TEEN,0x19e,,,,,,,,,,,,
+KEY_TWEN,0x19f,,,,,,,,,,,,
+KEY_VIDEOPHONE,0x1a0,,,,,,,,,,,,
+KEY_GAMES,0x1a1,,,,,,,,,,,,
+KEY_ZOOMIN,0x1a2,,,,,,,,,,,,
+KEY_ZOOMOUT,0x1a3,,,,,,,,,,,,
+KEY_ZOOMRESET,0x1a4,,,,,,,,,,,,
+KEY_WORDPROCESSOR,0x1a5,,,,,,,,,,,,
+KEY_EDITOR,0x1a6,,,,,,,,,,,,
+KEY_SPREADSHEET,0x1a7,,,,,,,,,,,,
+KEY_GRAPHICSEDITOR,0x1a8,,,,,,,,,,,,
+KEY_PRESENTATION,0x1a9,,,,,,,,,,,,
+KEY_DATABASE,0x1aa,,,,,,,,,,,,
+KEY_NEWS,0x1ab,,,,,,,,,,,,
+KEY_VOICEMAIL,0x1ac,,,,,,,,,,,,
+KEY_ADDRESSBOOK,0x1ad,,,,,,,,,,,,
+KEY_MESSENGER,0x1ae,,,,,,,,,,,,
+KEY_DISPLAYTOGGLE,0x1af,,,,,,,,,,,,
+KEY_SPELLCHECK,0x1b0,,,,,,,,,,,,
+KEY_LOGOFF,0x1b1,,,,,,,,,,,,
+KEY_DOLLAR,0x1b2,,,,,,,,,,,,
+KEY_EURO,0x1b3,,,,,,,,,,,,
+KEY_FRAMEBACK,0x1b4,,,,,,,,,,,,
+KEY_FRAMEFORWARD,0x1b5,,,,,,,,,,,,
+KEY_CONTEXT_MENU,0x1b6,,,,,,,,,,,,
+KEY_MEDIA_REPEAT,0x1b7,,,,,,,,,,,,
+KEY_DEL_EOL,0x1c0,,,,,,,,,,,,
+KEY_DEL_EOS,0x1c1,,,,,,,,,,,,
+KEY_INS_LINE,0x1c2,,,,,,,,,,,,
+KEY_DEL_LINE,0x1c3,,,,,,,,,,,,
+KEY_FN,0x1d0,,,,,,,,,,,,
+KEY_FN_ESC,0x1d1,,,,,,,,,,,,
+KEY_FN_F1,0x1d2,,,,,,,,,,,,
+KEY_FN_F2,0x1d3,,,,,,,,,,,,
+KEY_FN_F3,0x1d4,,,,,,,,,,,,
+KEY_FN_F4,0x1d5,,,,,,,,,,,,
+KEY_FN_F5,0x1d6,,,,,,,,,,,,
+KEY_FN_F6,0x1d7,,,,,,,,,,,,
+KEY_FN_F7,0x1d8,,,,,,,,,,,,
+KEY_FN_F8,0x1d9,,,,,,,,,,,,
+KEY_FN_F9,0x1da,,,,,,,,,,,,
+KEY_FN_F10,0x1db,,,,,,,,,,,,
+KEY_FN_F11,0x1dc,,,,,,,,,,,,
+KEY_FN_F12,0x1dd,,,,,,,,,,,,
+KEY_FN_1,0x1de,,,,,,,,,,,,
+KEY_FN_2,0x1df,,,,,,,,,,,,
+KEY_FN_D,0x1e0,,,,,,,,,,,,
+KEY_FN_E,0x1e1,,,,,,,,,,,,
+KEY_FN_F,0x1e2,,,,,,,,,,,,
+KEY_FN_S,0x1e3,,,,,,,,,,,,
+KEY_FN_B,0x1e4,,,,,,,,,,,,
+KEY_BRL_DOT1,0x1f1,,,,,,,,,,,,
+KEY_BRL_DOT2,0x1f2,,,,,,,,,,,,
+KEY_BRL_DOT3,0x1f3,,,,,,,,,,,,
+KEY_BRL_DOT4,0x1f4,,,,,,,,,,,,
+KEY_BRL_DOT5,0x1f5,,,,,,,,,,,,
+KEY_BRL_DOT6,0x1f6,,,,,,,,,,,,
+KEY_BRL_DOT7,0x1f7,,,,,,,,,,,,
+KEY_BRL_DOT8,0x1f8,,,,,,,,,,,,
+KEY_BRL_DOT9,0x1f9,,,,,,,,,,,,
+KEY_BRL_DOT10,0x1fa,,,,,,,,,,,,
+KEY_NUMERIC_0,0x200,,,,,,,,,,,,
+KEY_NUMERIC_1,0x201,,,,,,,,,,,,
+KEY_NUMERIC_2,0x202,,,,,,,,,,,,
+KEY_NUMERIC_3,0x203,,,,,,,,,,,,
+KEY_NUMERIC_4,0x204,,,,,,,,,,,,
+KEY_NUMERIC_5,0x205,,,,,,,,,,,,
+KEY_NUMERIC_6,0x206,,,,,,,,,,,,
+KEY_NUMERIC_7,0x207,,,,,,,,,,,,
+KEY_NUMERIC_8,0x208,,,,,,,,,,,,
+KEY_NUMERIC_9,0x209,,,,,,,,,,,,
+KEY_NUMERIC_STAR,0x20a,,,,,,,,,,,,
+KEY_NUMERIC_POUND,0x20b,,,,,,,,,,,,
+KEY_RFKILL,0x20c,,,,,,,,,,,,
diff --git a/src/map-file b/src/map-file
new file mode 100644
index 0000000..d5a073f
--- /dev/null
+++ b/src/map-file
@@ -0,0 +1,139 @@
+SPICEGTK_1 {
+global:
+spice_audio_get;
+spice_audio_get_type;
+spice_audio_new;
+spice_channel_connect;
+spice_channel_destroy;
+spice_channel_disconnect;
+spice_channel_event_get_type;
+spice_channel_flush_async;
+spice_channel_flush_finish;
+spice_channel_get_error;
+spice_channel_get_type;
+spice_channel_new;
+spice_channel_open_fd;
+spice_channel_set_capability;
+spice_channel_string_to_type;
+spice_channel_test_capability;
+spice_channel_test_common_capability;
+spice_channel_type_to_string;
+spice_client_error_quark;
+spice_cursor_channel_get_type;
+spice_display_channel_get_type;
+spice_display_copy_to_guest;
+spice_display_get_grab_keys;
+spice_display_get_pixbuf;
+spice_display_get_primary;
+spice_display_get_type;
+spice_display_key_event_get_type;
+spice_display_mouse_ungrab;
+spice_display_new;
+spice_display_new_with_monitor;
+spice_display_paste_from_guest;
+spice_display_send_keys;
+spice_display_set_grab_keys;
+spice_get_option_group;
+spice_grab_sequence_as_string;
+spice_grab_sequence_copy;
+spice_grab_sequence_free;
+spice_grab_sequence_get_type;
+spice_grab_sequence_new;
+spice_grab_sequence_new_from_string;
+spice_g_signal_connect_object;
+spice_gtk_session_copy_to_guest;
+spice_gtk_session_get;
+spice_gtk_session_get_type;
+spice_gtk_session_paste_from_guest;
+spice_inputs_button_press;
+spice_inputs_button_release;
+spice_inputs_channel_get_type;
+spice_inputs_key_press;
+spice_inputs_key_press_and_release;
+spice_inputs_key_release;
+spice_inputs_lock_get_type;
+spice_inputs_motion;
+spice_inputs_position;
+spice_inputs_set_key_locks;
+spice_main_agent_test_capability;
+spice_main_channel_get_type;
+spice_main_clipboard_grab;
+spice_main_clipboard_notify;
+spice_main_clipboard_release;
+spice_main_clipboard_request;
+spice_main_clipboard_selection_grab;
+spice_main_clipboard_selection_notify;
+spice_main_clipboard_selection_release;
+spice_main_clipboard_selection_request;
+spice_main_file_copy_async;
+spice_main_file_copy_finish;
+spice_main_send_monitor_config;
+spice_main_set_display;
+spice_main_set_display_enabled;
+spice_main_update_display;
+spice_playback_channel_get_type;
+spice_playback_channel_set_delay;
+spice_port_channel_get_type;
+spice_port_event;
+spice_port_write_async;
+spice_port_write_finish;
+spice_record_channel_get_type;
+spice_record_send_data;
+spice_session_connect;
+spice_session_disconnect;
+spice_session_get_channels;
+spice_session_get_proxy_uri;
+spice_session_get_read_only;
+spice_session_get_type;
+spice_session_has_channel_type;
+spice_session_is_for_migration;
+spice_session_migration_get_type;
+spice_session_new;
+spice_session_open_fd;
+spice_session_verify_get_type;
+spice_set_session_option;
+spice_smartcard_channel_get_type;
+spice_smartcard_manager_get;
+spice_smartcard_manager_get_readers;
+spice_smartcard_manager_get_type;
+spice_smartcard_manager_insert_card;
+spice_smartcard_manager_remove_card;
+spice_smartcard_reader_get_type;
+spice_smartcard_reader_insert_card;
+spice_smartcard_reader_is_software;
+spice_smartcard_reader_remove_card;
+spice_uri_get_hostname;
+spice_uri_get_password;
+spice_uri_get_port;
+spice_uri_get_scheme;
+spice_uri_get_type;
+spice_uri_get_user;
+spice_uri_set_hostname;
+spice_uri_set_password;
+spice_uri_set_port;
+spice_uri_set_scheme;
+spice_uri_set_user;
+spice_uri_to_string;
+spice_usb_device_get_description;
+spice_usb_device_get_libusb_device;
+spice_usb_device_get_type;
+spice_usb_device_manager_can_redirect_device;
+spice_usb_device_manager_connect_device_async;
+spice_usb_device_manager_connect_device_finish;
+spice_usb_device_manager_disconnect_device;
+spice_usb_device_manager_get;
+spice_usb_device_manager_get_devices;
+spice_usb_device_manager_get_devices_with_filter;
+spice_usb_device_manager_get_type;
+spice_usb_device_manager_is_device_connected;
+spice_usb_device_widget_get_type;
+spice_usb_device_widget_new;
+spice_usbredir_channel_get_type;
+spice_util_get_debug;
+spice_util_get_version_string;
+spice_util_set_debug;
+spice_uuid_to_string;
+spice_webdav_channel_get_type;
+local:
+*;
+};
diff --git a/src/smartcard-manager-priv.h b/src/smartcard-manager-priv.h
new file mode 100644
index 0000000..409c1c5
--- /dev/null
+++ b/src/smartcard-manager-priv.h
@@ -0,0 +1,37 @@
+/* -*- 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 __SMARTCARD_MANAGER_PRIV_H__
+#define __SMARTCARD_MANAGER_PRIV_H__
+
+#include "config.h"
+#include <gio/gio.h>
+#include "spice-session.h"
+
+G_BEGIN_DECLS
+
+void spice_smartcard_manager_init_async(SpiceSession *session,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer opaque);
+gboolean spice_smartcard_manager_init_finish(SpiceSession *session,
+ GAsyncResult *result,
+ GError **err);
+
+G_END_DECLS
+
+#endif /* __SMARTCARD_MANAGER_PRIV_H__ */
diff --git a/src/smartcard-manager.c b/src/smartcard-manager.c
new file mode 100644
index 0000000..9e228e9
--- /dev/null
+++ b/src/smartcard-manager.c
@@ -0,0 +1,737 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 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-object.h>
+#include <string.h>
+
+#include "glib-compat.h"
+
+#ifdef USE_SMARTCARD
+#include <vcard_emul.h>
+#include <vevent.h>
+#include <vreader.h>
+#endif
+
+#include "spice-client.h"
+#include "smartcard-manager.h"
+#include "smartcard-manager-priv.h"
+#include "spice-marshal.h"
+
+/**
+ * SECTION:smartcard-manager
+ * @short_description: smartcard management
+ * @title: Spice Smartcard Manager
+ * @section_id:
+ * @see_also:
+ * @stability: Stable
+ * @include: smartcard-manager.h
+ *
+ * #SpiceSmartcardManager monitors smartcard reader plugging/unplugging,
+ * and smartcard insertions/removals. It also provides methods to handle
+ * software smartcards (to emulate a smartcard reader/smartcard on the
+ * guest using 3 certificates available to the client).
+ */
+
+/* ------------------------------------------------------------------ */
+/* gobject glue */
+
+#define SPICE_SMARTCARD_MANAGER_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE ((obj), SPICE_TYPE_SMARTCARD_MANAGER, SpiceSmartcardManagerPrivate))
+
+struct _SpiceSmartcardManagerPrivate {
+ guint monitor_id;
+
+ /* software smartcard reader, the certificates to use for this reader
+ * were given at the channel creation time. This reader has no physical
+ * existence, it's all controlled by explicit software
+ * insertion/removal of cards
+ */
+#ifdef USE_SMARTCARD
+ VReader *software_reader;
+#endif
+};
+
+G_DEFINE_TYPE(SpiceSmartcardManager, spice_smartcard_manager, G_TYPE_OBJECT)
+#ifdef USE_SMARTCARD
+G_DEFINE_BOXED_TYPE(VReader, spice_smartcard_reader, vreader_reference, vreader_free)
+#else
+typedef GObject VReader;
+G_DEFINE_BOXED_TYPE(VReader, spice_smartcard_reader, g_object_ref, g_object_unref)
+#endif
+
+/* Properties */
+enum {
+ PROP_0,
+};
+
+/* Signals */
+enum {
+ SPICE_SMARTCARD_MANAGER_READER_ADDED,
+ SPICE_SMARTCARD_MANAGER_READER_REMOVED,
+ SPICE_SMARTCARD_MANAGER_CARD_INSERTED,
+ SPICE_SMARTCARD_MANAGER_CARD_REMOVED,
+
+ SPICE_SMARTCARD_MANAGER_LAST_SIGNAL,
+};
+
+static guint signals[SPICE_SMARTCARD_MANAGER_LAST_SIGNAL];
+
+#ifdef USE_SMARTCARD
+typedef gboolean (*SmartcardSourceFunc)(VEvent *event, gpointer user_data);
+static gboolean smartcard_monitor_dispatch(VEvent *event, gpointer user_data);
+#endif
+
+/* ------------------------------------------------------------------ */
+
+static void spice_smartcard_manager_init(SpiceSmartcardManager *smartcard_manager)
+{
+ SpiceSmartcardManagerPrivate *priv;
+
+ priv = SPICE_SMARTCARD_MANAGER_GET_PRIVATE(smartcard_manager);
+ smartcard_manager->priv = priv;
+}
+
+static void spice_smartcard_manager_dispose(GObject *gobject)
+{
+ /* Chain up to the parent class */
+ if (G_OBJECT_CLASS(spice_smartcard_manager_parent_class)->dispose)
+ G_OBJECT_CLASS(spice_smartcard_manager_parent_class)->dispose(gobject);
+}
+
+static void spice_smartcard_manager_finalize(GObject *gobject)
+{
+ SpiceSmartcardManager *manager = SPICE_SMARTCARD_MANAGER(gobject);
+ SpiceSmartcardManagerPrivate *priv = manager->priv;
+
+ if (priv->monitor_id != 0) {
+ g_source_remove(priv->monitor_id);
+ priv->monitor_id = 0;
+ }
+
+#ifdef USE_SMARTCARD
+ if (priv->software_reader != NULL) {
+ vreader_free(priv->software_reader);
+ priv->software_reader = NULL;
+ }
+#endif
+
+ /* Chain up to the parent class */
+ if (G_OBJECT_CLASS(spice_smartcard_manager_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_smartcard_manager_parent_class)->finalize(gobject);
+}
+
+static void spice_smartcard_manager_class_init(SpiceSmartcardManagerClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ /**
+ * SpiceSmartcardManager::reader-added:
+ * @manager: the #SpiceSmartcardManager that emitted the signal
+ * @vreader: #VReader boxed object corresponding to the added reader
+ *
+ * The #SpiceSmartcardManager::reader-added signal is emitted whenever
+ * a new smartcard reader (software or hardware) has been plugged in.
+ **/
+ signals[SPICE_SMARTCARD_MANAGER_READER_ADDED] =
+ g_signal_new("reader-added",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceSmartcardManagerClass, reader_added),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__BOXED,
+ G_TYPE_NONE,
+ 1,
+ SPICE_TYPE_SMARTCARD_READER);
+
+ /**
+ * SpiceSmartcardManager::reader-removed:
+ * @manager: the #SpiceSmartcardManager that emitted the signal
+ * @vreader: #VReader boxed object corresponding to the removed reader
+ *
+ * The #SpiceSmartcardManager::reader-removed signal is emitted whenever
+ * a smartcard reader (software or hardware) has been removed.
+ **/
+ signals[SPICE_SMARTCARD_MANAGER_READER_REMOVED] =
+ g_signal_new("reader-removed",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceSmartcardManagerClass, reader_removed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__BOXED,
+ G_TYPE_NONE,
+ 1,
+ SPICE_TYPE_SMARTCARD_READER);
+
+ /**
+ * SpiceSmartcardManager::card-inserted:
+ * @manager: the #SpiceSmartcardManager that emitted the signal
+ * @vreader: #VReader boxed object corresponding to the reader a new
+ * card was inserted in
+ *
+ * The #SpiceSmartcardManager::card-inserted signal is emitted whenever
+ * a smartcard is inserted in a reader
+ **/
+ signals[SPICE_SMARTCARD_MANAGER_CARD_INSERTED] =
+ g_signal_new("card-inserted",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceSmartcardManagerClass, card_inserted),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__BOXED,
+ G_TYPE_NONE,
+ 1,
+ SPICE_TYPE_SMARTCARD_READER);
+
+ /**
+ * SpiceSmartcardManager::card-removed:
+ * @manager: the #SpiceSmartcardManager that emitted the signal
+ * @vreader: #VReader boxed object corresponding to the reader a card
+ * was removed from
+ *
+ * The #SpiceSmartcardManager::card-removed signal is emitted whenever
+ * a smartcard was removed from a reader.
+ **/
+ signals[SPICE_SMARTCARD_MANAGER_CARD_REMOVED] =
+ g_signal_new("card-removed",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceSmartcardManagerClass, card_removed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__BOXED,
+ G_TYPE_NONE,
+ 1,
+ SPICE_TYPE_SMARTCARD_READER);
+ gobject_class->dispose = spice_smartcard_manager_dispose;
+ gobject_class->finalize = spice_smartcard_manager_finalize;
+
+ g_type_class_add_private(klass, sizeof(SpiceSmartcardManagerPrivate));
+}
+
+/* ------------------------------------------------------------------ */
+/* private api */
+
+static SpiceSmartcardManager *spice_smartcard_manager_new(void)
+{
+ return g_object_new(SPICE_TYPE_SMARTCARD_MANAGER, NULL);
+}
+
+/* ------------------------------------------------------------------ */
+/* public api */
+
+/**
+ * spice_smartcard_manager_get:
+ *
+ * #SpiceSmartcardManager is a singleton, use this function to get a pointer
+ * to it. A new SpiceSmartcardManager instance will be created the first
+ * time this function is called
+ *
+ * Returns: (transfer none): a weak reference to the #SpiceSmartcardManager
+ */
+SpiceSmartcardManager *spice_smartcard_manager_get(void)
+{
+ static GOnce manager_singleton_once = G_ONCE_INIT;
+
+ return g_once(&manager_singleton_once,
+ (GThreadFunc)spice_smartcard_manager_new,
+ NULL);
+}
+
+#ifdef USE_SMARTCARD
+static gboolean smartcard_monitor_dispatch(VEvent *event, gpointer user_data)
+{
+ g_return_val_if_fail(event != NULL, TRUE);
+ SpiceSmartcardManager *manager = SPICE_SMARTCARD_MANAGER(user_data);
+
+ switch (event->type) {
+ case VEVENT_READER_INSERT:
+ if (spice_smartcard_reader_is_software((SpiceSmartcardReader*)event->reader)) {
+ g_warn_if_fail(manager->priv->software_reader == NULL);
+ manager->priv->software_reader = vreader_reference(event->reader);
+ }
+ SPICE_DEBUG("smartcard: reader-added");
+ g_signal_emit(G_OBJECT(user_data),
+ signals[SPICE_SMARTCARD_MANAGER_READER_ADDED],
+ 0, event->reader);
+ break;
+
+ case VEVENT_READER_REMOVE:
+ if (spice_smartcard_reader_is_software((SpiceSmartcardReader*)event->reader)) {
+ g_warn_if_fail(manager->priv->software_reader != NULL);
+ vreader_free(manager->priv->software_reader);
+ manager->priv->software_reader = NULL;
+ }
+ SPICE_DEBUG("smartcard: reader-removed");
+ g_signal_emit(G_OBJECT(user_data),
+ signals[SPICE_SMARTCARD_MANAGER_READER_REMOVED],
+ 0, event->reader);
+ break;
+
+ case VEVENT_CARD_INSERT:
+ SPICE_DEBUG("smartcard: card-inserted");
+ g_signal_emit(G_OBJECT(user_data),
+ signals[SPICE_SMARTCARD_MANAGER_CARD_INSERTED],
+ 0, event->reader);
+ break;
+ case VEVENT_CARD_REMOVE:
+ SPICE_DEBUG("smartcard: card-removed");
+ g_signal_emit(G_OBJECT(user_data),
+ signals[SPICE_SMARTCARD_MANAGER_CARD_REMOVED],
+ 0, event->reader);
+ break;
+ case VEVENT_LAST:
+ break;
+ }
+
+ return TRUE;
+}
+
+/* ------------------------------------------------------------------ */
+/* smartcard monitoring GSource */
+struct _SmartcardSource {
+ GSource parent_source;
+ VEvent *pending_event;
+};
+typedef struct _SmartcardSource SmartcardSource;
+
+static gboolean smartcard_source_prepare(GSource *source, gint *timeout)
+{
+ SmartcardSource *smartcard_source = (SmartcardSource *)source;
+
+ if (smartcard_source->pending_event == NULL)
+ smartcard_source->pending_event = vevent_get_next_vevent();
+
+ if (timeout != NULL)
+ *timeout = -1;
+
+ return (smartcard_source->pending_event != NULL);
+}
+
+static gboolean smartcard_source_check(GSource *source)
+{
+ return smartcard_source_prepare(source, NULL);
+}
+
+static gboolean smartcard_source_dispatch(GSource *source,
+ GSourceFunc callback,
+ gpointer user_data)
+{
+ SmartcardSource *smartcard_source = (SmartcardSource *)source;
+ SmartcardSourceFunc smartcard_callback = (SmartcardSourceFunc)callback;
+
+ g_return_val_if_fail(smartcard_source->pending_event != NULL, FALSE);
+
+ if (callback) {
+ gboolean event_consumed;
+ event_consumed = smartcard_callback(smartcard_source->pending_event,
+ user_data);
+ if (event_consumed) {
+ vevent_delete(smartcard_source->pending_event);
+ smartcard_source->pending_event = NULL;
+ }
+ }
+
+ return TRUE;
+}
+
+static void smartcard_source_finalize(GSource *source)
+{
+ SmartcardSource *smartcard_source = (SmartcardSource *)source;
+
+ if (smartcard_source->pending_event) {
+ vevent_delete(smartcard_source->pending_event);
+ smartcard_source->pending_event = NULL;
+ }
+}
+
+static GSource *smartcard_monitor_source_new(void)
+{
+ static GSourceFuncs source_funcs = {
+ .prepare = smartcard_source_prepare,
+ .check = smartcard_source_check,
+ .dispatch = smartcard_source_dispatch,
+ .finalize = smartcard_source_finalize
+ };
+ GSource *source;
+
+ source = g_source_new(&source_funcs, sizeof(SmartcardSource));
+ g_source_set_name(source, "Smartcard event source");
+ return source;
+}
+
+static guint smartcard_monitor_add(SmartcardSourceFunc callback,
+ gpointer user_data)
+{
+ GSource *source;
+ guint id;
+
+ source = smartcard_monitor_source_new();
+ g_source_set_callback(source, (GSourceFunc)callback, user_data, NULL);
+ id = g_source_attach(source, NULL);
+ g_source_unref(source);
+
+ return id;
+}
+
+static void
+spice_smartcard_manager_update_monitor(void)
+{
+ SpiceSmartcardManager *self = spice_smartcard_manager_get();
+ SpiceSmartcardManagerPrivate *priv = self->priv;
+
+ if (priv->monitor_id != 0)
+ return;
+
+ priv->monitor_id = smartcard_monitor_add(smartcard_monitor_dispatch, self);
+}
+
+#define SPICE_SOFTWARE_READER_NAME "Spice Software Smartcard"
+
+typedef struct {
+ SpiceSession *session;
+ GCancellable *cancellable;
+ GError *err;
+} SmartcardManagerInitArgs;
+
+static gboolean smartcard_manager_init(SmartcardManagerInitArgs *args)
+{
+ gchar *emul_args = NULL;
+ VCardEmulOptions *options = NULL;
+ VCardEmulError emul_init_status;
+ gchar *dbname = NULL;
+ GStrv certificates = NULL;
+ gboolean retval = FALSE;
+
+ SPICE_DEBUG("smartcard_manager_init");
+ g_return_val_if_fail(SPICE_IS_SESSION(args->session), FALSE);
+ g_object_get(G_OBJECT(args->session),
+ "smartcard-db", &dbname,
+ "smartcard-certificates", &certificates,
+ NULL);
+
+ if ((certificates == NULL) || (g_strv_length(certificates) != 3))
+ goto init;
+
+ if (dbname) {
+ emul_args = g_strdup_printf("db=\"%s\" use_hw=no "
+ "soft=(,%s,CAC,,%s,%s,%s)",
+ dbname, SPICE_SOFTWARE_READER_NAME,
+ certificates[0], certificates[1],
+ certificates[2]);
+ } else {
+ emul_args = g_strdup_printf("use_hw=no soft=(,%s,CAC,,%s,%s,%s)",
+ SPICE_SOFTWARE_READER_NAME,
+ certificates[0], certificates[1],
+ certificates[2]);
+ }
+
+ options = vcard_emul_options(emul_args);
+ if (options == NULL) {
+ args->err = g_error_new(SPICE_CLIENT_ERROR,
+ SPICE_CLIENT_ERROR_FAILED,
+ "vcard_emul_options() failed!");
+ goto end;
+ }
+
+ if (g_cancellable_set_error_if_cancelled(args->cancellable, &args->err))
+ goto end;
+
+init:
+ SPICE_DEBUG("vcard_emul_init");
+ emul_init_status = vcard_emul_init(options);
+ if ((emul_init_status != VCARD_EMUL_OK)
+ && (emul_init_status != VCARD_EMUL_INIT_ALREADY_INITED)) {
+ args->err = g_error_new(SPICE_CLIENT_ERROR,
+ SPICE_CLIENT_ERROR_FAILED,
+ "Failed to initialize smartcard");
+ goto end;
+ }
+
+ retval = TRUE;
+
+end:
+ SPICE_DEBUG("smartcard_manager_init end: %d", retval);
+ g_free(emul_args);
+ g_free(dbname);
+ g_strfreev(certificates);
+ return retval;
+}
+
+static void smartcard_manager_init_helper(GSimpleAsyncResult *res,
+ GObject *object,
+ GCancellable *cancellable)
+{
+ static GOnce smartcard_manager_once = G_ONCE_INIT;
+ SmartcardManagerInitArgs args;
+
+ args.session = SPICE_SESSION(object);
+ args.cancellable = cancellable;
+ args.err = NULL;
+
+
+ g_once(&smartcard_manager_once,
+ (GThreadFunc)smartcard_manager_init,
+ &args);
+ if (args.err != NULL) {
+ g_simple_async_result_set_from_error(res, args.err);
+ g_error_free(args.err);
+ }
+}
+
+
+G_GNUC_INTERNAL
+void spice_smartcard_manager_init_async(SpiceSession *session,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer opaque)
+{
+ GSimpleAsyncResult *res;
+
+ res = g_simple_async_result_new(G_OBJECT(session),
+ callback,
+ opaque,
+ spice_smartcard_manager_init);
+ g_simple_async_result_run_in_thread(res,
+ smartcard_manager_init_helper,
+ G_PRIORITY_DEFAULT,
+ cancellable);
+ g_object_unref(res);
+}
+
+G_GNUC_INTERNAL
+gboolean spice_smartcard_manager_init_finish(SpiceSession *session,
+ GAsyncResult *result,
+ GError **err)
+{
+ GSimpleAsyncResult *simple;
+
+ g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);
+ g_return_val_if_fail(G_IS_SIMPLE_ASYNC_RESULT(result), FALSE);
+
+ SPICE_DEBUG("smartcard_manager_finish");
+
+ simple = G_SIMPLE_ASYNC_RESULT(result);
+ g_return_val_if_fail(g_simple_async_result_get_source_tag(simple) == spice_smartcard_manager_init, FALSE);
+ if (g_simple_async_result_propagate_error(simple, err))
+ return FALSE;
+
+ spice_smartcard_manager_update_monitor();
+
+ return TRUE;
+}
+
+/**
+ * spice_smartcard_reader_is_software:
+ * @reader: a #SpiceSmartcardReader
+ *
+ * Tests if @reader is a software (emulated) smartcard reader.
+ *
+ * Returns: TRUE if @reader is a software (emulated) smartcard reader,
+ * FALSE otherwise
+ */
+gboolean spice_smartcard_reader_is_software(SpiceSmartcardReader *reader)
+{
+ g_return_val_if_fail(reader != NULL, FALSE);
+ return (strcmp(vreader_get_name((VReader*)reader), SPICE_SOFTWARE_READER_NAME) == 0);
+}
+
+/**
+ * spice_smartcard_reader_insert_card:
+ * @reader: a #SpiceSmartcardReader
+ *
+ * Simulates insertion of a smartcard in the software smartcard reader
+ * @reader. If @reader is not a software smartcard reader, FALSE will be
+ * returned.
+ *
+ * Returns: TRUE if insertion of a card was successfully simulated, FALSE
+ * otherwise
+ */
+gboolean spice_smartcard_reader_insert_card(SpiceSmartcardReader *reader)
+{
+ VCardEmulError status;
+
+ g_return_val_if_fail(spice_smartcard_reader_is_software(reader), FALSE);
+
+ status = vcard_emul_force_card_insert((VReader *)reader);
+
+ return (status == VCARD_EMUL_OK);
+}
+
+/**
+ * spice_smartcard_reader_remove_card:
+ * @reader: a #SpiceSmartcardReader
+ *
+ * Simulates removal of a smartcard from the software smartcard reader
+ * @reader. If @reader is not a software smartcard reader, FALSE will be
+ * returned.
+ *
+ * Returns: TRUE if removal of a card was successfully simulated, FALSE
+ * otherwise
+ */
+gboolean spice_smartcard_reader_remove_card(SpiceSmartcardReader *reader)
+{
+ VCardEmulError status;
+
+ g_return_val_if_fail(spice_smartcard_reader_is_software(reader), FALSE);
+
+ status = vcard_emul_force_card_remove((VReader *)reader);
+
+ return (status == VCARD_EMUL_OK);
+}
+
+/**
+ * spice_smartcard_manager_get_readers:
+ *
+ * manager: a #SpiceSmartcardManager
+ *
+ * Gets the list of smartcard readers that are currently available, they
+ * can be either software (emulated) readers, or hardware ones.
+ *
+ * Returns: (element-type SpiceSmartcardReader) (transfer full): a newly
+ * allocated list of SpiceSmartcardReader instances, or NULL if none were
+ * found. When no longer needed, the list must be freed after unreferencing
+ * its elements with g_boxed_free()
+ *
+ * Since: 0.20
+ */
+GList *spice_smartcard_manager_get_readers(SpiceSmartcardManager *manager)
+{
+
+ GList *readers = NULL;
+ VReaderList *vreader_list;
+ VReaderListEntry *entry;
+
+ vreader_list = vreader_get_reader_list();
+
+ if (vreader_list == NULL)
+ return NULL;
+
+ for (entry = vreader_list_get_first(vreader_list);
+ entry != NULL;
+ entry = vreader_list_get_next(entry)) {
+ VReader *reader;
+
+ reader = vreader_list_get_reader(entry);
+ g_warn_if_fail(reader != NULL);
+ readers = g_list_prepend(readers, vreader_reference(reader));
+ }
+ vreader_list_delete(vreader_list);
+
+ return g_list_reverse(readers);
+}
+
+/**
+ * spice_smartcard_manager_insert_card:
+ * @manager: a #SpiceSmartcardManager
+ *
+ * Simulates the insertion of a smartcard in the guest. Valid certificates
+ * must have been set in #SpiceSession:smartcard-certificates for software
+ * smartcard support to work. At the moment, only one software smartcard
+ * reader is supported, that's why there is no parameter to indicate which
+ * reader to insert the card in.
+ *
+ * Returns: TRUE if smartcard insertion was successfully simulated, FALSE
+ * if this failed, or if software smartcard support isn't enabled.
+ *
+ * Since: 0.20
+ */
+gboolean spice_smartcard_manager_insert_card(SpiceSmartcardManager *manager)
+{
+ SpiceSmartcardReader *reader;
+
+ g_return_val_if_fail (manager->priv->software_reader != NULL, FALSE);
+
+ reader = (SpiceSmartcardReader *)manager->priv->software_reader;
+
+ return spice_smartcard_reader_insert_card(reader);
+}
+
+/**
+ * spice_smartcard_manager_remove_card:
+ * @manager: a #SpiceSmartcardManager
+ *
+ * Simulates the removal of a smartcard in the guest. At the moment, only
+ * one software smartcard reader is supported, that's why there is no
+ * parameter to indicate which reader to insert the card in.
+ *
+ * Returns: TRUE if smartcard removal was successfully simulated, FALSE
+ * if this failed, or if software smartcard support isn't enabled.
+ *
+ * Since: 0.20
+ */
+gboolean spice_smartcard_manager_remove_card(SpiceSmartcardManager *manager)
+{
+ SpiceSmartcardReader *reader;
+
+ g_return_val_if_fail (manager->priv->software_reader != NULL, FALSE);
+
+ reader = (SpiceSmartcardReader *)manager->priv->software_reader;
+
+ return spice_smartcard_reader_remove_card(reader);
+}
+#else
+gboolean spice_smartcard_reader_is_software(SpiceSmartcardReader *reader)
+{
+ return TRUE;
+}
+
+G_GNUC_INTERNAL
+void spice_smartcard_manager_init_async(SpiceSession *session,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer opaque)
+{
+ SPICE_DEBUG("using fake smartcard backend");
+}
+
+G_GNUC_INTERNAL
+gboolean spice_smartcard_manager_init_finish(SpiceSession *session,
+ GAsyncResult *result,
+ GError **err)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);
+
+ return TRUE;
+}
+
+gboolean spice_smartcard_manager_insert_card(SpiceSmartcardManager *manager)
+{
+ return FALSE;
+}
+
+gboolean spice_smartcard_manager_remove_card(SpiceSmartcardManager *manager)
+{
+ return FALSE;
+}
+
+gboolean spice_smartcard_reader_insert_card(SpiceSmartcardReader *reader)
+{
+ return FALSE;
+}
+
+gboolean spice_smartcard_reader_remove_card(SpiceSmartcardReader *reader)
+{
+ return FALSE;
+}
+
+GList *spice_smartcard_manager_get_readers(SpiceSmartcardManager *manager)
+{
+ return NULL;
+}
+
+#endif /* USE_SMARTCARD */
diff --git a/src/smartcard-manager.h b/src/smartcard-manager.h
new file mode 100644
index 0000000..4811083
--- /dev/null
+++ b/src/smartcard-manager.h
@@ -0,0 +1,80 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 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/>.
+*/
+#ifndef __SPICE_SMARTCARD_MANAGER_H__
+#define __SPICE_SMARTCARD_MANAGER_H__
+
+G_BEGIN_DECLS
+
+#include "spice-types.h"
+#include "spice-util.h"
+
+#define SPICE_TYPE_SMARTCARD_MANAGER (spice_smartcard_manager_get_type ())
+#define SPICE_SMARTCARD_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SPICE_TYPE_SMARTCARD_MANAGER, SpiceSmartcardManager))
+#define SPICE_SMARTCARD_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SPICE_TYPE_SMARTCARD_MANAGER, SpiceSmartcardManagerClass))
+#define SPICE_IS_SMARTCARD_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SPICE_TYPE_SMARTCARD_MANAGER))
+#define SPICE_IS_SMARTCARD_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SPICE_TYPE_SMARTCARD_MANAGER))
+#define SPICE_SMARTCARD_MANAGER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SPICE_TYPE_SMARTCARD_MANAGER, SpiceSmartcardManagerClass))
+
+#define SPICE_TYPE_SMARTCARD_READER (spice_smartcard_reader_get_type())
+
+typedef struct _SpiceSmartcardManager SpiceSmartcardManager;
+typedef struct _SpiceSmartcardManagerClass SpiceSmartcardManagerClass;
+typedef struct _SpiceSmartcardManagerPrivate SpiceSmartcardManagerPrivate;
+typedef struct _SpiceSmartcardReader SpiceSmartcardReader;
+
+struct _SpiceSmartcardManager
+{
+ GObject parent;
+
+ /*< private >*/
+ SpiceSmartcardManagerPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+struct _SpiceSmartcardManagerClass
+{
+ GObjectClass parent_class;
+ /*< public >*/
+ /* signals */
+ void (*reader_added)(SpiceSmartcardManager *manager, SpiceSmartcardReader *reader);
+ void (*reader_removed)(SpiceSmartcardManager *manager, SpiceSmartcardReader *reader);
+ void (*card_inserted)(SpiceSmartcardManager *manager, SpiceSmartcardReader *reader);
+ void (*card_removed)(SpiceSmartcardManager *manager, SpiceSmartcardReader *reader );
+
+ /*< private >*/
+ /*
+ * If adding fields to this struct, remove corresponding
+ * amount of padding to avoid changing overall struct size
+ */
+ gchar _spice_reserved[SPICE_RESERVED_PADDING];
+};
+
+GType spice_smartcard_manager_get_type(void);
+GType spice_smartcard_reader_get_type(void);
+
+SpiceSmartcardManager *spice_smartcard_manager_get(void);
+gboolean spice_smartcard_manager_insert_card(SpiceSmartcardManager *manager);
+gboolean spice_smartcard_manager_remove_card(SpiceSmartcardManager *manager);
+gboolean spice_smartcard_reader_is_software(SpiceSmartcardReader *reader);
+gboolean spice_smartcard_reader_insert_card(SpiceSmartcardReader *reader);
+gboolean spice_smartcard_reader_remove_card(SpiceSmartcardReader *reader);
+GList *spice_smartcard_manager_get_readers(SpiceSmartcardManager *manager);
+
+G_END_DECLS
+
+#endif /* __SPICE_SMARTCARD_MANAGER_H__ */
diff --git a/src/spice-audio-priv.h b/src/spice-audio-priv.h
new file mode 100644
index 0000000..f108059
--- /dev/null
+++ b/src/spice-audio-priv.h
@@ -0,0 +1,42 @@
+/* -*- 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_AUDIO_PRIVATE_H__
+#define __SPICE_AUDIO_PRIVATE_H__
+
+#include <glib.h>
+#include <gio/gio.h>
+#include "spice-session.h"
+
+G_BEGIN_DECLS
+
+struct _SpiceAudioPrivate {
+ SpiceSession *session;
+ GMainContext *main_context;
+};
+
+void spice_audio_get_playback_volume_info_async(SpiceAudio *audio, GCancellable *cancellable,
+ SpiceMainChannel *main_channel, GAsyncReadyCallback callback, gpointer user_data);
+gboolean spice_audio_get_playback_volume_info_finish(SpiceAudio *audio, GAsyncResult *res,
+ gboolean *mute, guint8 *nchannels, guint16 **volume, GError **error);
+void spice_audio_get_record_volume_info_async(SpiceAudio *audio, GCancellable *cancellable,
+ SpiceMainChannel *main_channel, GAsyncReadyCallback callback, gpointer user_data);
+gboolean spice_audio_get_record_volume_info_finish(SpiceAudio *audio, GAsyncResult *res,
+ gboolean *mute, guint8 *nchannels, guint16 **volume, GError **error);
+G_END_DECLS
+
+#endif /* __SPICE_AUDIO_PRIVATE_H__ */
diff --git a/src/spice-audio.c b/src/spice-audio.c
new file mode 100644
index 0000000..ce191e1
--- /dev/null
+++ b/src/spice-audio.c
@@ -0,0 +1,274 @@
+/* -*- 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/>.
+*/
+/*
+ * simple audio init dispatcher
+ */
+
+/**
+ * SECTION:spice-audio
+ * @short_description: a helper to play and to record audio channels
+ * @title: Spice Audio
+ * @section_id:
+ * @see_also: #SpiceRecordChannel, and #SpicePlaybackChannel
+ * @stability: Stable
+ * @include: spice-audio.h
+ *
+ * A class that handles the playback and record channels for your
+ * application, and connect them to the default sound system.
+ */
+
+#include "config.h"
+
+#include "spice-client.h"
+#include "spice-common.h"
+
+#include "spice-audio.h"
+#include "spice-session-priv.h"
+#include "spice-channel-priv.h"
+#include "spice-audio-priv.h"
+
+#ifdef WITH_PULSE
+#include "spice-pulse.h"
+#endif
+#if defined(WITH_GSTAUDIO)
+#include "spice-gstaudio.h"
+#endif
+
+#include "glib-compat.h"
+
+#define SPICE_AUDIO_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE ((obj), SPICE_TYPE_AUDIO, SpiceAudioPrivate))
+
+G_DEFINE_ABSTRACT_TYPE(SpiceAudio, spice_audio, G_TYPE_OBJECT)
+
+enum {
+ PROP_0,
+ PROP_SESSION,
+ PROP_MAIN_CONTEXT,
+};
+
+static void spice_audio_finalize(GObject *gobject)
+{
+ SpiceAudio *self = SPICE_AUDIO(gobject);
+ SpiceAudioPrivate *priv = self->priv;
+
+ if (priv->main_context) {
+ g_main_context_unref(priv->main_context);
+ priv->main_context = NULL;
+ }
+
+ if (G_OBJECT_CLASS(spice_audio_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_audio_parent_class)->finalize(gobject);
+}
+
+static void spice_audio_get_property(GObject *gobject,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceAudio *self = SPICE_AUDIO(gobject);
+ SpiceAudioPrivate *priv = self->priv;
+
+ switch (prop_id) {
+ case PROP_SESSION:
+ g_value_set_object(value, priv->session);
+ break;
+ case PROP_MAIN_CONTEXT:
+ g_value_set_boxed(value, priv->main_context);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_audio_set_property(GObject *gobject,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceAudio *self = SPICE_AUDIO(gobject);
+ SpiceAudioPrivate *priv = self->priv;
+
+ switch (prop_id) {
+ case PROP_SESSION:
+ priv->session = g_value_get_object(value);
+ break;
+ case PROP_MAIN_CONTEXT:
+ priv->main_context = g_value_dup_boxed(value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_audio_class_init(SpiceAudioClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GParamSpec *pspec;
+
+ gobject_class->finalize = spice_audio_finalize;
+ gobject_class->get_property = spice_audio_get_property;
+ gobject_class->set_property = spice_audio_set_property;
+
+ /**
+ * SpiceAudio:session:
+ *
+ * #SpiceSession this #SpiceAudio is associated with
+ *
+ **/
+ pspec = g_param_spec_object("session", "Session", "SpiceSession",
+ SPICE_TYPE_SESSION,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ g_object_class_install_property(gobject_class, PROP_SESSION, pspec);
+
+ /**
+ * SpiceAudio:main-context:
+ */
+ pspec = g_param_spec_boxed("main-context", "Main Context",
+ "GMainContext to use for the event source",
+ G_TYPE_MAIN_CONTEXT,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ g_object_class_install_property(gobject_class, PROP_MAIN_CONTEXT, pspec);
+
+ g_type_class_add_private(klass, sizeof(SpiceAudioPrivate));
+}
+
+static void spice_audio_init(SpiceAudio *self)
+{
+ self->priv = SPICE_AUDIO_GET_PRIVATE(self);
+}
+
+static void connect_channel(SpiceAudio *self, SpiceChannel *channel)
+{
+ if (channel->priv->state != SPICE_CHANNEL_STATE_UNCONNECTED)
+ return;
+
+ if (SPICE_AUDIO_GET_CLASS(self)->connect_channel(self, channel))
+ spice_channel_connect(channel);
+}
+
+static void update_audio_channels(SpiceAudio *self, SpiceSession *session)
+{
+ GList *list, *tmp;
+
+ if (!spice_session_get_audio_enabled(session)) {
+ g_debug("FIXME: disconnect audio channels");
+ return;
+ }
+
+ list = spice_session_get_channels(session);
+ for (tmp = g_list_first(list); tmp != NULL; tmp = g_list_next(tmp)) {
+ connect_channel(self, tmp->data);
+ }
+ g_list_free(list);
+}
+
+static void channel_new(SpiceSession *session, SpiceChannel *channel, SpiceAudio *self)
+{
+ connect_channel(self, channel);
+}
+
+static void session_enable_audio(GObject *gobject, GParamSpec *pspec,
+ gpointer user_data)
+{
+ update_audio_channels(SPICE_AUDIO(user_data), SPICE_SESSION(gobject));
+}
+
+void spice_audio_get_playback_volume_info_async(SpiceAudio *audio,
+ GCancellable *cancellable,
+ SpiceMainChannel *main_channel,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SPICE_AUDIO_GET_CLASS(audio)->get_playback_volume_info_async(audio,
+ cancellable, main_channel, callback, user_data);
+}
+
+gboolean spice_audio_get_playback_volume_info_finish(SpiceAudio *audio,
+ GAsyncResult *res,
+ gboolean *mute,
+ guint8 *nchannels,
+ guint16 **volume,
+ GError **error)
+{
+ return SPICE_AUDIO_GET_CLASS(audio)->get_playback_volume_info_finish(audio,
+ res, mute, nchannels, volume, error);
+}
+
+void spice_audio_get_record_volume_info_async(SpiceAudio *audio,
+ GCancellable *cancellable,
+ SpiceMainChannel *main_channel,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SPICE_AUDIO_GET_CLASS(audio)->get_record_volume_info_async(audio,
+ cancellable, main_channel, callback, user_data);
+}
+
+gboolean spice_audio_get_record_volume_info_finish(SpiceAudio *audio,
+ GAsyncResult *res,
+ gboolean *mute,
+ guint8 *nchannels,
+ guint16 **volume,
+ GError **error)
+{
+ return SPICE_AUDIO_GET_CLASS(audio)->get_record_volume_info_finish(audio,
+ res, mute, nchannels, volume, error);
+}
+
+/**
+ * spice_audio_new:
+ * @session: the #SpiceSession to connect to
+ * @context: (allow-none): a #GMainContext to attach to (or %NULL for
+ * default).
+ * @name: (allow-none): a name for the audio channels (or %NULL for
+ * application name).
+ *
+ * Once instantiated, #SpiceAudio will handle the playback and record
+ * channels to stream to your local audio system.
+ *
+ * Returns: a new #SpiceAudio instance or %NULL if no backend or failed.
+ * Deprecated: 0.8: Use spice_audio_get() instead
+ **/
+SpiceAudio *spice_audio_new(SpiceSession *session, GMainContext *context,
+ const char *name)
+{
+ SpiceAudio *self = NULL;
+
+ if (context == NULL)
+ context = g_main_context_default();
+ if (name == NULL)
+ name = g_get_application_name();
+
+#ifdef WITH_PULSE
+ self = SPICE_AUDIO(spice_pulse_new(session, context, name));
+#endif
+#if defined(WITH_GSTAUDIO)
+ self = SPICE_AUDIO(spice_gstaudio_new(session, context, name));
+#endif
+ if (!self)
+ return NULL;
+
+ spice_g_signal_connect_object(session, "notify::enable-audio", G_CALLBACK(session_enable_audio), self, 0);
+ spice_g_signal_connect_object(session, "channel-new", G_CALLBACK(channel_new), self, G_CONNECT_AFTER);
+ update_audio_channels(self, session);
+
+ return self;
+}
diff --git a/src/spice-audio.h b/src/spice-audio.h
new file mode 100644
index 0000000..0bf625b
--- /dev/null
+++ b/src/spice-audio.h
@@ -0,0 +1,109 @@
+/* -*- 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_CLIENT_AUDIO_H__
+#define __SPICE_CLIENT_AUDIO_H__
+
+#include <glib-object.h>
+#include <gio/gio.h>
+#include "spice-util.h"
+#include "spice-session.h"
+#include "channel-main.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_AUDIO spice_audio_get_type()
+
+#define SPICE_AUDIO(obj) \
+ (G_TYPE_CHECK_INSTANCE_CAST ((obj), SPICE_TYPE_AUDIO, SpiceAudio))
+
+#define SPICE_AUDIO_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_CAST ((klass), SPICE_TYPE_AUDIO, SpiceAudioClass))
+
+#define SPICE_IS_AUDIO(obj) \
+ (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SPICE_TYPE_AUDIO))
+
+#define SPICE_IS_AUDIO_CLASS(klass) \
+ (G_TYPE_CHECK_CLASS_TYPE ((klass), SPICE_TYPE_AUDIO))
+
+#define SPICE_AUDIO_GET_CLASS(obj) \
+ (G_TYPE_INSTANCE_GET_CLASS ((obj), SPICE_TYPE_AUDIO, SpiceAudioClass))
+
+typedef struct _SpiceAudio SpiceAudio;
+typedef struct _SpiceAudioClass SpiceAudioClass;
+typedef struct _SpiceAudioPrivate SpiceAudioPrivate;
+
+/**
+ * SpiceAudio:
+ *
+ * The #SpiceAudio struct is opaque and should not be accessed directly.
+ */
+struct _SpiceAudio {
+ GObject parent;
+
+ SpiceAudioPrivate *priv;
+};
+
+/**
+ * SpiceAudioClass:
+ * @parent_class: Parent class.
+ *
+ * Class structure for #SpiceAudio.
+ */
+struct _SpiceAudioClass {
+ GObjectClass parent_class;
+
+ /*< private >*/
+ gboolean (*connect_channel)(SpiceAudio *audio, SpiceChannel *channel);
+ void (*get_playback_volume_info_async)(SpiceAudio *audio,
+ GCancellable *cancellable,
+ SpiceMainChannel *main_channel,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+ gboolean (*get_playback_volume_info_finish)(SpiceAudio *audio,
+ GAsyncResult *res,
+ gboolean *mute,
+ guint8 *nchannels,
+ guint16 **volume,
+ GError **error);
+ void (*get_record_volume_info_async)(SpiceAudio *audio,
+ GCancellable *cancellable,
+ SpiceMainChannel *main_channel,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+ gboolean (*get_record_volume_info_finish)(SpiceAudio *audio,
+ GAsyncResult *res,
+ gboolean *mute,
+ guint8 *nchannels,
+ guint16 **volume,
+ GError **error);
+
+ gchar _spice_reserved[SPICE_RESERVED_PADDING - 4 * sizeof(void *)];
+};
+
+GType spice_audio_get_type(void);
+
+SpiceAudio* spice_audio_get(SpiceSession *session, GMainContext *context);
+
+#ifndef SPICE_DISABLE_DEPRECATED
+SPICE_DEPRECATED_FOR(spice_audio_get)
+SpiceAudio* spice_audio_new(SpiceSession *session, GMainContext *context, const char *name);
+#endif
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_AUDIO_H__ */
diff --git a/src/spice-channel-cache.h b/src/spice-channel-cache.h
new file mode 100644
index 0000000..17775e6
--- /dev/null
+++ b/src/spice-channel-cache.h
@@ -0,0 +1,106 @@
+/* -*- 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_CHANNEL_CACHE_H_
+# define SPICE_CHANNEL_CACHE_H_
+
+#include <inttypes.h> /* For PRIx64 */
+#include "common/mem.h"
+#include "common/ring.h"
+
+G_BEGIN_DECLS
+
+typedef struct display_cache_item {
+ guint64 id;
+ gboolean lossy;
+} display_cache_item;
+
+typedef GHashTable display_cache;
+
+static inline display_cache_item* cache_item_new(guint64 id, gboolean lossy)
+{
+ display_cache_item *self = g_slice_new(display_cache_item);
+ self->id = id;
+ self->lossy = lossy;
+ return self;
+}
+
+static inline void cache_item_free(display_cache_item *self)
+{
+ g_slice_free(display_cache_item, self);
+}
+
+static inline display_cache* cache_new(GDestroyNotify value_destroy)
+{
+ GHashTable* self;
+
+ self = g_hash_table_new_full(g_int64_hash, g_int64_equal,
+ (GDestroyNotify)cache_item_free,
+ value_destroy);
+
+ return self;
+}
+
+static inline gpointer cache_find(display_cache *cache, uint64_t id)
+{
+ return g_hash_table_lookup(cache, &id);
+}
+
+static inline gpointer cache_find_lossy(display_cache *cache, uint64_t id, gboolean *lossy)
+{
+ gpointer value;
+ display_cache_item *item;
+
+ if (!g_hash_table_lookup_extended(cache, &id, (gpointer*)&item, &value))
+ return NULL;
+
+ *lossy = item->lossy;
+
+ return value;
+}
+
+static inline void cache_add_lossy(display_cache *cache, uint64_t id,
+ gpointer value, gboolean lossy)
+{
+ display_cache_item *item = cache_item_new(id, lossy);
+
+ g_hash_table_replace(cache, item, value);
+}
+
+static inline void cache_add(display_cache *cache, uint64_t id, gpointer value)
+{
+ cache_add_lossy(cache, id, value, FALSE);
+}
+
+static inline gboolean cache_remove(display_cache *cache, uint64_t id)
+{
+ return g_hash_table_remove(cache, &id);
+}
+
+static inline void cache_clear(display_cache *cache)
+{
+ g_hash_table_remove_all(cache);
+}
+
+static inline void cache_unref(display_cache *cache)
+{
+ g_hash_table_unref(cache);
+}
+
+G_END_DECLS
+
+#endif // SPICE_CHANNEL_CACHE_H_
diff --git a/src/spice-channel-enums.h b/src/spice-channel-enums.h
new file mode 100644
index 0000000..02df762
--- /dev/null
+++ b/src/spice-channel-enums.h
@@ -0,0 +1,7 @@
+#ifndef SPICE_CHANNEL_ENUMS_H
+#define SPICE_CHANNEL_ENUMS_H
+
+#warning "deprecated: please include spice-glib-enums.h"
+#include "spice-glib-enums.h"
+
+#endif /* SPICE_CHANNEL_ENUMS_H */
diff --git a/src/spice-channel-priv.h b/src/spice-channel-priv.h
new file mode 100644
index 0000000..d70cf86
--- /dev/null
+++ b/src/spice-channel-priv.h
@@ -0,0 +1,203 @@
+/* -*- 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_CLIENT_CHANNEL_PRIV_H__
+#define __SPICE_CLIENT_CHANNEL_PRIV_H__
+
+#include "config.h"
+
+#include <openssl/ssl.h>
+#include <gio/gio.h>
+
+#if HAVE_SASL
+#include <sasl/sasl.h>
+#endif
+
+#include "spice-channel.h"
+#include "spice-util-priv.h"
+#include "coroutine.h"
+#include "gio-coroutine.h"
+
+#include "common/client_marshallers.h"
+#include "common/client_demarshallers.h"
+#include "common/ssl_verify.h"
+
+G_BEGIN_DECLS
+
+#define MAX_SPICE_DATA_HEADER_SIZE sizeof(SpiceDataHeader)
+
+#define CHANNEL_DEBUG(channel, fmt, ...) \
+ SPICE_DEBUG("%s: " fmt, SPICE_CHANNEL(channel)->priv->name, ## __VA_ARGS__)
+
+struct _SpiceMsgOut {
+ int refcount;
+ SpiceChannel *channel;
+ SpiceMessageMarshallers *marshallers;
+ SpiceMarshaller *marshaller;
+ uint8_t *header;
+ gboolean ro_check;
+};
+
+struct _SpiceMsgIn {
+ int refcount;
+ SpiceChannel *channel;
+ uint8_t header[MAX_SPICE_DATA_HEADER_SIZE];
+ uint8_t *data;
+ int dpos;
+ uint8_t *parsed;
+ size_t psize;
+ message_destructor_t pfree;
+ SpiceMsgIn *parent;
+};
+
+enum spice_channel_state {
+ SPICE_CHANNEL_STATE_UNCONNECTED = 0,
+ SPICE_CHANNEL_STATE_RECONNECTING,
+ SPICE_CHANNEL_STATE_CONNECTING,
+ SPICE_CHANNEL_STATE_READY,
+ SPICE_CHANNEL_STATE_SWITCHING,
+ SPICE_CHANNEL_STATE_MIGRATING,
+ SPICE_CHANNEL_STATE_MIGRATION_HANDSHAKE,
+};
+
+struct _SpiceChannelPrivate {
+ /* swapped on migration */
+ SSL_CTX *ctx;
+ SSL *ssl;
+ SpiceOpenSSLVerify *sslverify;
+ GSocket *sock;
+ GSocketConnection *conn;
+ GInputStream *in;
+ GOutputStream *out;
+
+#if HAVE_SASL
+ sasl_conn_t *sasl_conn;
+ const char *sasl_decoded;
+ unsigned int sasl_decoded_length;
+ unsigned int sasl_decoded_offset;
+#endif
+
+ gboolean use_mini_header;
+ uint64_t out_serial;
+ uint64_t in_serial;
+
+ /* not swapped */
+ SpiceSession *session;
+ GCoroutine coroutine;
+ int fd;
+ gboolean has_error;
+ guint connect_delayed_id;
+
+ GQueue xmit_queue;
+ gboolean xmit_queue_blocked;
+ STATIC_MUTEX xmit_queue_lock;
+ guint xmit_queue_wakeup_id;
+
+ char name[16];
+ enum spice_channel_state state;
+ SpiceChannelEvent event;
+
+ spice_parse_channel_func_t parser;
+ SpiceMessageMarshallers *marshallers;
+ guint channel_watch;
+ int tls;
+
+ int channel_id;
+ int channel_type;
+ SpiceLinkHeader link_hdr;
+ SpiceLinkMess link_msg;
+ SpiceLinkHeader peer_hdr;
+ SpiceLinkReply* peer_msg;
+ int peer_pos;
+
+ int message_ack_window;
+ int message_ack_count;
+
+ GArray *caps;
+ GArray *common_caps;
+ GArray *remote_caps;
+ GArray *remote_common_caps;
+
+ gsize total_read_bytes;
+ uint64_t last_message_serial;
+ GSList *flushing;
+
+ gboolean disable_channel_msg;
+ gboolean auth_needs_username_and_password;
+ GError *error;
+};
+
+SpiceMsgIn *spice_msg_in_new(SpiceChannel *channel);
+SpiceMsgIn *spice_msg_in_sub_new(SpiceChannel *channel, SpiceMsgIn *parent,
+ SpiceSubMessage *sub);
+void spice_msg_in_ref(SpiceMsgIn *in);
+void spice_msg_in_unref(SpiceMsgIn *in);
+int spice_msg_in_type(SpiceMsgIn *in);
+void *spice_msg_in_parsed(SpiceMsgIn *in);
+void *spice_msg_in_raw(SpiceMsgIn *in, int *len);
+void spice_msg_in_hexdump(SpiceMsgIn *in);
+
+SpiceMsgOut *spice_msg_out_new(SpiceChannel *channel, int type);
+void spice_msg_out_ref(SpiceMsgOut *out);
+void spice_msg_out_unref(SpiceMsgOut *out);
+void spice_msg_out_send(SpiceMsgOut *out);
+void spice_msg_out_send_internal(SpiceMsgOut *out);
+void spice_msg_out_hexdump(SpiceMsgOut *out, unsigned char *data, int len);
+
+uint16_t spice_header_get_msg_type(uint8_t *header, gboolean is_mini_header);
+uint32_t spice_header_get_msg_size(uint8_t *header, gboolean is_mini_header);
+
+void spice_channel_up(SpiceChannel *channel);
+void spice_channel_wakeup(SpiceChannel *channel, gboolean cancel);
+
+SpiceSession* spice_channel_get_session(SpiceChannel *channel);
+enum spice_channel_state spice_channel_get_state(SpiceChannel *channel);
+
+/* coroutine context */
+typedef void (*handler_msg_in)(SpiceChannel *channel, SpiceMsgIn *msg, gpointer data);
+void spice_channel_recv_msg(SpiceChannel *channel, handler_msg_in handler, gpointer data);
+
+/* channel-base.c */
+void spice_channel_set_handlers(SpiceChannelClass *klass,
+ const spice_msg_handler* handlers, const int n);
+void spice_channel_handle_wait_for_channels(SpiceChannel *channel, SpiceMsgIn *in);
+
+gint spice_channel_get_channel_id(SpiceChannel *channel);
+gint spice_channel_get_channel_type(SpiceChannel *channel);
+void spice_channel_swap(SpiceChannel *channel, SpiceChannel *swap, gboolean swap_msgs);
+gboolean spice_channel_get_read_only(SpiceChannel *channel);
+void spice_channel_reset(SpiceChannel *channel, gboolean migrating);
+
+void spice_caps_set(GArray *caps, guint32 cap, const gchar *desc);
+#define spice_channel_set_common_capability(channel, cap) \
+ spice_caps_set(SPICE_CHANNEL(channel)->priv->common_caps, cap, #cap)
+#define spice_channel_set_capability(channel, cap) \
+ spice_caps_set(SPICE_CHANNEL(channel)->priv->caps, cap, #cap)
+
+gchar *spice_channel_supported_string(void);
+
+void spice_vmc_write_async(SpiceChannel *self,
+ const void *buffer, gsize count,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gssize spice_vmc_write_finish(SpiceChannel *self,
+ GAsyncResult *result, GError **error);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_CHANNEL_PRIV_H__ */
diff --git a/src/spice-channel.c b/src/spice-channel.c
new file mode 100644
index 0000000..4e7d8b7
--- /dev/null
+++ b/src/spice-channel.c
@@ -0,0 +1,2960 @@
+/* -*- 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 "glib-compat.h"
+
+#include "spice-channel-priv.h"
+#include "spice-session-priv.h"
+#include "spice-marshal.h"
+#include "bio-gio.h"
+
+#include <glib/gi18n.h>
+
+#include <openssl/rsa.h>
+#include <openssl/evp.h>
+#include <openssl/x509.h>
+#include <openssl/ssl.h>
+#include <openssl/err.h>
+#include <openssl/x509v3.h>
+#ifdef HAVE_SYS_SOCKET_H
+#include <sys/socket.h>
+#endif
+#ifdef HAVE_NETINET_IN_H
+#include <netinet/in.h>
+#include <netinet/tcp.h> // TCP_NODELAY
+#endif
+#ifdef HAVE_ARPA_INET_H
+#include <arpa/inet.h>
+#endif
+#include <ctype.h>
+
+#include "gio-coroutine.h"
+
+static void spice_channel_handle_msg(SpiceChannel *channel, SpiceMsgIn *msg);
+static void spice_channel_write_msg(SpiceChannel *channel, SpiceMsgOut *out);
+static void spice_channel_send_link(SpiceChannel *channel);
+static void channel_reset(SpiceChannel *channel, gboolean migrating);
+static void spice_channel_reset_capabilities(SpiceChannel *channel);
+static void spice_channel_send_migration_handshake(SpiceChannel *channel);
+static gboolean channel_connect(SpiceChannel *channel, gboolean tls);
+
+/**
+ * SECTION:spice-channel
+ * @short_description: the base channel class
+ * @title: Spice Channel
+ * @section_id:
+ * @see_also: #SpiceSession, #SpiceMainChannel and other channels
+ * @stability: Stable
+ * @include: spice-channel.h
+ *
+ * #SpiceChannel is the base class for the different kind of Spice
+ * channel connections, such as #SpiceMainChannel, or
+ * #SpiceInputsChannel.
+ */
+
+/* ------------------------------------------------------------------ */
+/* gobject glue */
+
+#define SPICE_CHANNEL_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE ((obj), SPICE_TYPE_CHANNEL, SpiceChannelPrivate))
+
+G_DEFINE_TYPE(SpiceChannel, spice_channel, G_TYPE_OBJECT);
+
+/* Properties */
+enum {
+ PROP_0,
+ PROP_SESSION,
+ PROP_CHANNEL_TYPE,
+ PROP_CHANNEL_ID,
+ PROP_TOTAL_READ_BYTES,
+};
+
+/* Signals */
+enum {
+ SPICE_CHANNEL_EVENT,
+ SPICE_CHANNEL_OPEN_FD,
+
+ SPICE_CHANNEL_LAST_SIGNAL,
+};
+
+static guint signals[SPICE_CHANNEL_LAST_SIGNAL];
+
+static void spice_channel_iterate_write(SpiceChannel *channel);
+static void spice_channel_iterate_read(SpiceChannel *channel);
+
+static void spice_channel_init(SpiceChannel *channel)
+{
+ SpiceChannelPrivate *c;
+
+ c = channel->priv = SPICE_CHANNEL_GET_PRIVATE(channel);
+
+ c->out_serial = 1;
+ c->in_serial = 1;
+ c->fd = -1;
+ c->auth_needs_username_and_password = FALSE;
+ strcpy(c->name, "?");
+ c->caps = g_array_new(FALSE, TRUE, sizeof(guint32));
+ c->common_caps = g_array_new(FALSE, TRUE, sizeof(guint32));
+ c->remote_caps = g_array_new(FALSE, TRUE, sizeof(guint32));
+ c->remote_common_caps = g_array_new(FALSE, TRUE, sizeof(guint32));
+ spice_channel_set_common_capability(channel, SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION);
+ spice_channel_set_common_capability(channel, SPICE_COMMON_CAP_MINI_HEADER);
+#if HAVE_SASL
+ spice_channel_set_common_capability(channel, SPICE_COMMON_CAP_AUTH_SASL);
+#endif
+ g_queue_init(&c->xmit_queue);
+ STATIC_MUTEX_INIT(c->xmit_queue_lock);
+}
+
+static void spice_channel_constructed(GObject *gobject)
+{
+ SpiceChannel *channel = SPICE_CHANNEL(gobject);
+ SpiceChannelPrivate *c = channel->priv;
+ const char *desc = spice_channel_type_to_string(c->channel_type);
+
+ snprintf(c->name, sizeof(c->name), "%s-%d:%d",
+ desc, c->channel_type, c->channel_id);
+ CHANNEL_DEBUG(channel, "%s", __FUNCTION__);
+
+ const char *disabled = g_getenv("SPICE_DISABLE_CHANNELS");
+ if (disabled && strstr(disabled, desc))
+ c->disable_channel_msg = TRUE;
+
+ spice_session_channel_new(c->session, channel);
+
+ /* Chain up to the parent class */
+ if (G_OBJECT_CLASS(spice_channel_parent_class)->constructed)
+ G_OBJECT_CLASS(spice_channel_parent_class)->constructed(gobject);
+}
+
+static void spice_channel_dispose(GObject *gobject)
+{
+ SpiceChannel *channel = SPICE_CHANNEL(gobject);
+ SpiceChannelPrivate *c = channel->priv;
+
+ CHANNEL_DEBUG(channel, "%s %p", __FUNCTION__, gobject);
+
+ spice_channel_disconnect(channel, SPICE_CHANNEL_CLOSED);
+
+ if (c->session) {
+ g_object_unref(c->session);
+ c->session = NULL;
+ }
+
+ g_clear_error(&c->error);
+
+ /* Chain up to the parent class */
+ if (G_OBJECT_CLASS(spice_channel_parent_class)->dispose)
+ G_OBJECT_CLASS(spice_channel_parent_class)->dispose(gobject);
+}
+
+static void spice_channel_finalize(GObject *gobject)
+{
+ SpiceChannel *channel = SPICE_CHANNEL(gobject);
+ SpiceChannelPrivate *c = channel->priv;
+
+ CHANNEL_DEBUG(channel, "%s %p", __FUNCTION__, gobject);
+
+ g_idle_remove_by_data(gobject);
+
+ STATIC_MUTEX_CLEAR(c->xmit_queue_lock);
+
+ if (c->caps)
+ g_array_free(c->caps, TRUE);
+
+ if (c->common_caps)
+ g_array_free(c->common_caps, TRUE);
+
+ if (c->remote_caps)
+ g_array_free(c->remote_caps, TRUE);
+
+ if (c->remote_common_caps)
+ g_array_free(c->remote_common_caps, TRUE);
+
+ /* Chain up to the parent class */
+ if (G_OBJECT_CLASS(spice_channel_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_channel_parent_class)->finalize(gobject);
+}
+
+static void spice_channel_get_property(GObject *gobject,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceChannel *channel = SPICE_CHANNEL(gobject);
+ SpiceChannelPrivate *c = channel->priv;
+
+ switch (prop_id) {
+ case PROP_SESSION:
+ g_value_set_object(value, c->session);
+ break;
+ case PROP_CHANNEL_TYPE:
+ g_value_set_int(value, c->channel_type);
+ break;
+ case PROP_CHANNEL_ID:
+ g_value_set_int(value, c->channel_id);
+ break;
+ case PROP_TOTAL_READ_BYTES:
+ g_value_set_ulong(value, c->total_read_bytes);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+G_GNUC_INTERNAL
+gint spice_channel_get_channel_id(SpiceChannel *channel)
+{
+ SpiceChannelPrivate *c = channel->priv;
+
+ g_return_val_if_fail(c != NULL, 0);
+ return c->channel_id;
+}
+
+G_GNUC_INTERNAL
+gint spice_channel_get_channel_type(SpiceChannel *channel)
+{
+ SpiceChannelPrivate *c = channel->priv;
+
+ g_return_val_if_fail(c != NULL, 0);
+ return c->channel_type;
+}
+
+static void spice_channel_set_property(GObject *gobject,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceChannel *channel = SPICE_CHANNEL(gobject);
+ SpiceChannelPrivate *c = channel->priv;
+
+ switch (prop_id) {
+ case PROP_SESSION:
+ c->session = g_value_dup_object(value);
+ break;
+ case PROP_CHANNEL_TYPE:
+ c->channel_type = g_value_get_int(value);
+ break;
+ case PROP_CHANNEL_ID:
+ c->channel_id = g_value_get_int(value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_channel_class_init(SpiceChannelClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ klass->iterate_write = spice_channel_iterate_write;
+ klass->iterate_read = spice_channel_iterate_read;
+ klass->channel_reset = channel_reset;
+
+ gobject_class->constructed = spice_channel_constructed;
+ gobject_class->dispose = spice_channel_dispose;
+ gobject_class->finalize = spice_channel_finalize;
+ gobject_class->get_property = spice_channel_get_property;
+ gobject_class->set_property = spice_channel_set_property;
+ klass->handle_msg = spice_channel_handle_msg;
+
+ g_object_class_install_property
+ (gobject_class, PROP_SESSION,
+ g_param_spec_object("spice-session",
+ "Spice session",
+ "",
+ SPICE_TYPE_SESSION,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_CHANNEL_TYPE,
+ g_param_spec_int("channel-type",
+ "Channel type",
+ "",
+ -1, INT_MAX, -1,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_CHANNEL_ID,
+ g_param_spec_int("channel-id",
+ "Channel ID",
+ "",
+ -1, INT_MAX, -1,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_TOTAL_READ_BYTES,
+ g_param_spec_ulong("total-read-bytes",
+ "Total read bytes",
+ "",
+ 0, G_MAXULONG, 0,
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceChannel::channel-event:
+ * @channel: the channel that emitted the signal
+ * @event: a #SpiceChannelEvent
+ *
+ * The #SpiceChannel::channel-event signal is emitted when the
+ * state of the connection is changed. In case of errors,
+ * spice_channel_get_error() may provide additional informations
+ * on the source of the error.
+ **/
+ signals[SPICE_CHANNEL_EVENT] =
+ g_signal_new("channel-event",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceChannelClass, channel_event),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__ENUM,
+ G_TYPE_NONE,
+ 1,
+ SPICE_TYPE_CHANNEL_EVENT);
+
+ /**
+ * SpiceChannel::open-fd:
+ * @channel: the channel that emitted the signal
+ * @with_tls: wether TLS connection is requested
+ *
+ * The #SpiceChannel::open-fd signal is emitted when a new
+ * connection is requested. This signal is emitted when the
+ * connection is made with spice_session_open_fd().
+ **/
+ signals[SPICE_CHANNEL_OPEN_FD] =
+ g_signal_new("open-fd",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceChannelClass, open_fd),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__INT,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_INT);
+
+ g_type_class_add_private(klass, sizeof(SpiceChannelPrivate));
+
+ SSL_library_init();
+ SSL_load_error_strings();
+}
+
+/* ---------------------------------------------------------------- */
+/* private header api */
+
+static inline void spice_header_set_msg_type(uint8_t *header, gboolean is_mini_header,
+ uint16_t type)
+{
+ if (is_mini_header) {
+ ((SpiceMiniDataHeader *)header)->type = type;
+ } else {
+ ((SpiceDataHeader *)header)->type = type;
+ }
+}
+
+static inline void spice_header_set_msg_size(uint8_t *header, gboolean is_mini_header,
+ uint32_t size)
+{
+ if (is_mini_header) {
+ ((SpiceMiniDataHeader *)header)->size = size;
+ } else {
+ ((SpiceDataHeader *)header)->size = size;
+ }
+}
+
+G_GNUC_INTERNAL
+uint16_t spice_header_get_msg_type(uint8_t *header, gboolean is_mini_header)
+{
+ if (is_mini_header) {
+ return ((SpiceMiniDataHeader *)header)->type;
+ } else {
+ return ((SpiceDataHeader *)header)->type;
+ }
+}
+
+G_GNUC_INTERNAL
+uint32_t spice_header_get_msg_size(uint8_t *header, gboolean is_mini_header)
+{
+ if (is_mini_header) {
+ return ((SpiceMiniDataHeader *)header)->size;
+ } else {
+ return ((SpiceDataHeader *)header)->size;
+ }
+}
+
+static inline int spice_header_get_header_size(gboolean is_mini_header)
+{
+ return is_mini_header ? sizeof(SpiceMiniDataHeader) : sizeof(SpiceDataHeader);
+}
+
+static inline void spice_header_set_msg_serial(uint8_t *header, gboolean is_mini_header,
+ uint64_t serial)
+{
+ if (!is_mini_header) {
+ ((SpiceDataHeader *)header)->serial = serial;
+ }
+}
+
+static inline void spice_header_reset_msg_sub_list(uint8_t *header, gboolean is_mini_header)
+{
+ if (!is_mini_header) {
+ ((SpiceDataHeader *)header)->sub_list = 0;
+ }
+}
+
+static inline uint64_t spice_header_get_in_msg_serial(SpiceMsgIn *in)
+{
+ SpiceChannelPrivate *c = in->channel->priv;
+ uint8_t *header = in->header;
+
+ if (c->use_mini_header) {
+ return c->in_serial;
+ } else {
+ return ((SpiceDataHeader *)header)->serial;
+ }
+}
+
+static inline uint64_t spice_header_get_out_msg_serial(SpiceMsgOut *out)
+{
+ SpiceChannelPrivate *c = out->channel->priv;
+
+ if (c->use_mini_header) {
+ return c->out_serial;
+ } else {
+ return ((SpiceDataHeader *)out->header)->serial;
+ }
+}
+
+static inline uint32_t spice_header_get_msg_sub_list(uint8_t *header, gboolean is_mini_header)
+{
+ if (is_mini_header) {
+ return 0;
+ } else {
+ return ((SpiceDataHeader *)header)->sub_list;
+ }
+}
+
+/* ---------------------------------------------------------------- */
+/* private msg api */
+
+G_GNUC_INTERNAL
+SpiceMsgIn *spice_msg_in_new(SpiceChannel *channel)
+{
+ SpiceMsgIn *in;
+
+ g_return_val_if_fail(channel != NULL, NULL);
+
+ in = g_slice_new0(SpiceMsgIn);
+ in->refcount = 1;
+ in->channel = channel;
+
+ return in;
+}
+
+G_GNUC_INTERNAL
+SpiceMsgIn *spice_msg_in_sub_new(SpiceChannel *channel, SpiceMsgIn *parent,
+ SpiceSubMessage *sub)
+{
+ SpiceMsgIn *in;
+
+ g_return_val_if_fail(channel != NULL, NULL);
+
+ in = spice_msg_in_new(channel);
+ spice_header_set_msg_type(in->header, channel->priv->use_mini_header, sub->type);
+ spice_header_set_msg_size(in->header, channel->priv->use_mini_header, sub->size);
+ in->data = (uint8_t*)(sub+1);
+ in->dpos = sub->size;
+ in->parent = parent;
+ spice_msg_in_ref(parent);
+ return in;
+}
+
+G_GNUC_INTERNAL
+void spice_msg_in_ref(SpiceMsgIn *in)
+{
+ g_return_if_fail(in != NULL);
+
+ in->refcount++;
+}
+
+G_GNUC_INTERNAL
+void spice_msg_in_unref(SpiceMsgIn *in)
+{
+ g_return_if_fail(in != NULL);
+
+ in->refcount--;
+ if (in->refcount > 0)
+ return;
+ if (in->parsed)
+ in->pfree(in->parsed);
+ if (in->parent) {
+ spice_msg_in_unref(in->parent);
+ } else {
+ g_free(in->data);
+ }
+ g_slice_free(SpiceMsgIn, in);
+}
+
+G_GNUC_INTERNAL
+int spice_msg_in_type(SpiceMsgIn *in)
+{
+ g_return_val_if_fail(in != NULL, -1);
+
+ return spice_header_get_msg_type(in->header, in->channel->priv->use_mini_header);
+}
+
+G_GNUC_INTERNAL
+void *spice_msg_in_parsed(SpiceMsgIn *in)
+{
+ g_return_val_if_fail(in != NULL, NULL);
+
+ return in->parsed;
+}
+
+G_GNUC_INTERNAL
+void *spice_msg_in_raw(SpiceMsgIn *in, int *len)
+{
+ g_return_val_if_fail(in != NULL, NULL);
+ g_return_val_if_fail(len != NULL, NULL);
+
+ *len = in->dpos;
+ return in->data;
+}
+
+static void hexdump(const char *prefix, unsigned char *data, int len)
+{
+ int i;
+
+ for (i = 0; i < len; i++) {
+ if (i % 16 == 0)
+ fprintf(stderr, "%s:", prefix);
+ if (i % 4 == 0)
+ fprintf(stderr, " ");
+ fprintf(stderr, " %02x", data[i]);
+ if (i % 16 == 15)
+ fprintf(stderr, "\n");
+ }
+ if (i % 16 != 0)
+ fprintf(stderr, "\n");
+}
+
+G_GNUC_INTERNAL
+void spice_msg_in_hexdump(SpiceMsgIn *in)
+{
+ SpiceChannelPrivate *c = in->channel->priv;
+
+ fprintf(stderr, "--\n<< hdr: %s serial %" PRIu64 " type %d size %d sub-list %d\n",
+ c->name, spice_header_get_in_msg_serial(in),
+ spice_header_get_msg_type(in->header, c->use_mini_header),
+ spice_header_get_msg_size(in->header, c->use_mini_header),
+ spice_header_get_msg_sub_list(in->header, c->use_mini_header));
+ hexdump("<< msg", in->data, in->dpos);
+}
+
+G_GNUC_INTERNAL
+void spice_msg_out_hexdump(SpiceMsgOut *out, unsigned char *data, int len)
+{
+ SpiceChannelPrivate *c = out->channel->priv;
+
+ fprintf(stderr, "--\n>> hdr: %s serial %" PRIu64 " type %d size %d sub-list %d\n",
+ c->name,
+ spice_header_get_out_msg_serial(out),
+ spice_header_get_msg_type(out->header, c->use_mini_header),
+ spice_header_get_msg_size(out->header, c->use_mini_header),
+ spice_header_get_msg_sub_list(out->header, c->use_mini_header));
+ hexdump(">> msg", data, len);
+}
+
+static gboolean msg_check_read_only (int channel_type, int msg_type)
+{
+ if (msg_type < 100) // those are the common messages
+ return FALSE;
+
+ switch (channel_type) {
+ /* messages allowed to be sent in read-only mode */
+ case SPICE_CHANNEL_MAIN:
+ switch (msg_type) {
+ case SPICE_MSGC_MAIN_CLIENT_INFO:
+ case SPICE_MSGC_MAIN_MIGRATE_CONNECTED:
+ case SPICE_MSGC_MAIN_MIGRATE_CONNECT_ERROR:
+ case SPICE_MSGC_MAIN_ATTACH_CHANNELS:
+ case SPICE_MSGC_MAIN_MIGRATE_END:
+ return FALSE;
+ }
+ break;
+ case SPICE_CHANNEL_DISPLAY:
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+G_GNUC_INTERNAL
+SpiceMsgOut *spice_msg_out_new(SpiceChannel *channel, int type)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ SpiceMsgOut *out;
+
+ g_return_val_if_fail(c != NULL, NULL);
+
+ out = g_slice_new0(SpiceMsgOut);
+ out->refcount = 1;
+ out->channel = channel;
+ out->ro_check = msg_check_read_only(c->channel_type, type);
+
+ out->marshallers = c->marshallers;
+ out->marshaller = spice_marshaller_new();
+
+ out->header = spice_marshaller_reserve_space(out->marshaller,
+ spice_header_get_header_size(c->use_mini_header));
+ spice_marshaller_set_base(out->marshaller, spice_header_get_header_size(c->use_mini_header));
+ spice_header_set_msg_type(out->header, c->use_mini_header, type);
+ spice_header_set_msg_serial(out->header, c->use_mini_header, c->out_serial);
+ spice_header_reset_msg_sub_list(out->header, c->use_mini_header);
+
+ c->out_serial++;
+ return out;
+}
+
+G_GNUC_INTERNAL
+void spice_msg_out_ref(SpiceMsgOut *out)
+{
+ g_return_if_fail(out != NULL);
+
+ out->refcount++;
+}
+
+G_GNUC_INTERNAL
+void spice_msg_out_unref(SpiceMsgOut *out)
+{
+ g_return_if_fail(out != NULL);
+
+ out->refcount--;
+ if (out->refcount > 0)
+ return;
+ spice_marshaller_destroy(out->marshaller);
+ g_slice_free(SpiceMsgOut, out);
+}
+
+/* system context */
+static gboolean spice_channel_idle_wakeup(gpointer user_data)
+{
+ SpiceChannel *channel = SPICE_CHANNEL(user_data);
+ SpiceChannelPrivate *c = channel->priv;
+
+ /*
+ * Note:
+ *
+ * - This must be done before the wakeup as that may eventually
+ * call channel_reset() which checks this.
+ * - The lock calls are really necessary, this fixes the following race:
+ * 1) usb-event-thread calls spice_msg_out_send()
+ * 2) spice_msg_out_send calls g_timeout_add_full(...)
+ * 3) we run, set xmit_queue_wakeup_id to 0
+ * 4) spice_msg_out_send stores the result of g_timeout_add_full() in
+ * xmit_queue_wakeup_id, overwriting the 0 we just stored
+ * 5) xmit_queue_wakeup_id now says there is a wakeup pending which is
+ * false
+ */
+ STATIC_MUTEX_LOCK(c->xmit_queue_lock);
+ c->xmit_queue_wakeup_id = 0;
+ STATIC_MUTEX_UNLOCK(c->xmit_queue_lock);
+
+ spice_channel_wakeup(channel, FALSE);
+
+ return FALSE;
+}
+
+/* any context (system/co-routine/usb-event-thread) */
+G_GNUC_INTERNAL
+void spice_msg_out_send(SpiceMsgOut *out)
+{
+ SpiceChannelPrivate *c;
+ gboolean was_empty;
+
+ g_return_if_fail(out != NULL);
+ g_return_if_fail(out->channel != NULL);
+ c = out->channel->priv;
+
+ STATIC_MUTEX_LOCK(c->xmit_queue_lock);
+ if (c->xmit_queue_blocked) {
+ g_warning("message queue is blocked, dropping message");
+ goto end;
+ }
+
+ was_empty = g_queue_is_empty(&c->xmit_queue);
+ g_queue_push_tail(&c->xmit_queue, out);
+
+ /* One wakeup is enough to empty the entire queue -> only do a wakeup
+ if the queue was empty, and there isn't one pending already. */
+ if (was_empty && !c->xmit_queue_wakeup_id) {
+ c->xmit_queue_wakeup_id =
+ /* Use g_timeout_add_full so that can specify the priority */
+ g_timeout_add_full(G_PRIORITY_HIGH, 0,
+ spice_channel_idle_wakeup,
+ out->channel, NULL);
+ }
+
+end:
+ STATIC_MUTEX_UNLOCK(c->xmit_queue_lock);
+}
+
+/* coroutine context */
+G_GNUC_INTERNAL
+void spice_msg_out_send_internal(SpiceMsgOut *out)
+{
+ g_return_if_fail(out != NULL);
+
+ spice_channel_write_msg(out->channel, out);
+}
+
+/*
+ * Write all 'data' of length 'datalen' bytes out to
+ * the wire
+ */
+/* coroutine context */
+static void spice_channel_flush_wire(SpiceChannel *channel,
+ const void *data,
+ size_t datalen)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ const char *ptr = data;
+ size_t offset = 0;
+ GIOCondition cond;
+
+ while (offset < datalen) {
+ gssize ret;
+ GError *error = NULL;
+
+ if (c->has_error) return;
+
+ cond = 0;
+ if (c->tls) {
+ ret = SSL_write(c->ssl, ptr+offset, datalen-offset);
+ if (ret < 0) {
+ ret = SSL_get_error(c->ssl, ret);
+ if (ret == SSL_ERROR_WANT_READ)
+ cond |= G_IO_IN;
+ if (ret == SSL_ERROR_WANT_WRITE)
+ cond |= G_IO_OUT;
+ ret = -1;
+ }
+ } else {
+ ret = g_pollable_output_stream_write_nonblocking(G_POLLABLE_OUTPUT_STREAM(c->out),
+ ptr+offset, datalen-offset, NULL, &error);
+ if (ret < 0) {
+ if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
+ cond = G_IO_OUT;
+ } else {
+ CHANNEL_DEBUG(channel, "Send error %s", error->message);
+ }
+ g_clear_error(&error);
+ ret = -1;
+ }
+ }
+ if (ret == -1) {
+ if (cond != 0) {
+ // TODO: should use g_pollable_input/output_stream_create_source() in 2.28 ?
+ g_coroutine_socket_wait(&c->coroutine, c->sock, cond);
+ continue;
+ } else {
+ CHANNEL_DEBUG(channel, "Closing the channel: spice_channel_flush %d", errno);
+ c->has_error = TRUE;
+ return;
+ }
+ }
+ if (ret == 0) {
+ CHANNEL_DEBUG(channel, "Closing the connection: spice_channel_flush");
+ c->has_error = TRUE;
+ return;
+ }
+ offset += ret;
+ }
+}
+
+#if HAVE_SASL
+/*
+ * Encode all buffered data, write all encrypted data out
+ * to the wire
+ */
+static void spice_channel_flush_sasl(SpiceChannel *channel, const void *data, size_t len)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ const char *output;
+ unsigned int outputlen;
+ int err;
+
+ err = sasl_encode(c->sasl_conn, data, len, &output, &outputlen);
+ if (err != SASL_OK) {
+ g_warning ("Failed to encode SASL data %s",
+ sasl_errstring(err, NULL, NULL));
+ c->has_error = TRUE;
+ return;
+ }
+
+ //CHANNEL_DEBUG(channel, "Flush SASL %d: %p %d", len, output, outputlen);
+ spice_channel_flush_wire(channel, output, outputlen);
+}
+#endif
+
+/* coroutine context */
+static void spice_channel_write(SpiceChannel *channel, const void *data, size_t len)
+{
+#if HAVE_SASL
+ SpiceChannelPrivate *c = channel->priv;
+
+ if (c->sasl_conn)
+ spice_channel_flush_sasl(channel, data, len);
+ else
+#endif
+ spice_channel_flush_wire(channel, data, len);
+}
+
+/* coroutine context */
+static void spice_channel_write_msg(SpiceChannel *channel, SpiceMsgOut *out)
+{
+ uint8_t *data;
+ int free_data;
+ size_t len;
+ uint32_t msg_size;
+
+ g_return_if_fail(channel != NULL);
+ g_return_if_fail(out != NULL);
+ g_return_if_fail(channel == out->channel);
+
+ if (out->ro_check &&
+ spice_channel_get_read_only(channel)) {
+ g_warning("Try to send message while read-only. Please report a bug.");
+ return;
+ }
+
+ msg_size = spice_marshaller_get_total_size(out->marshaller) -
+ spice_header_get_header_size(channel->priv->use_mini_header);
+ spice_header_set_msg_size(out->header, channel->priv->use_mini_header, msg_size);
+ data = spice_marshaller_linearize(out->marshaller, 0, &len, &free_data);
+ /* spice_msg_out_hexdump(out, data, len); */
+ spice_channel_write(channel, data, len);
+
+ if (free_data)
+ g_free(data);
+
+ spice_msg_out_unref(out);
+}
+
+/*
+ * Read at least 1 more byte of data straight off the wire
+ * into the requested buffer.
+ */
+/* coroutine context */
+static int spice_channel_read_wire(SpiceChannel *channel, void *data, size_t len)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ gssize ret;
+ GIOCondition cond;
+
+reread:
+
+ if (c->has_error) return 0; /* has_error is set by disconnect(), return no error */
+
+ cond = 0;
+ if (c->tls) {
+ ret = SSL_read(c->ssl, data, len);
+ if (ret < 0) {
+ ret = SSL_get_error(c->ssl, ret);
+ if (ret == SSL_ERROR_WANT_READ)
+ cond |= G_IO_IN;
+ if (ret == SSL_ERROR_WANT_WRITE)
+ cond |= G_IO_OUT;
+ ret = -1;
+ }
+ } else {
+ GError *error = NULL;
+ ret = g_pollable_input_stream_read_nonblocking(G_POLLABLE_INPUT_STREAM(c->in),
+ data, len, NULL, &error);
+ if (ret < 0) {
+ if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) {
+ cond = G_IO_IN;
+ } else {
+ CHANNEL_DEBUG(channel, "Read error %s", error->message);
+ }
+ g_clear_error(&error);
+ ret = -1;
+ }
+ }
+
+ if (ret == -1) {
+ if (cond != 0) {
+ // TODO: should use g_pollable_input/output_stream_create_source() ?
+ g_coroutine_socket_wait(&c->coroutine, c->sock, cond);
+ goto reread;
+ } else {
+ c->has_error = TRUE;
+ return -errno;
+ }
+ }
+ if (ret == 0) {
+ CHANNEL_DEBUG(channel, "Closing the connection: spice_channel_read() - ret=0");
+ c->has_error = TRUE;
+ return 0;
+ }
+
+ return ret;
+}
+
+#if HAVE_SASL
+/*
+ * Read at least 1 more byte of data out of the SASL decrypted
+ * data buffer, into the internal read buffer
+ */
+static int spice_channel_read_sasl(SpiceChannel *channel, void *data, size_t len)
+{
+ SpiceChannelPrivate *c = channel->priv;
+
+ /* CHANNEL_DEBUG(channel, "Read %lu SASL %p size %d offset %d", len, c->sasl_decoded, */
+ /* c->sasl_decoded_length, c->sasl_decoded_offset); */
+
+ if (c->sasl_decoded == NULL || c->sasl_decoded_length == 0) {
+ char encoded[8192]; /* should stay lower than maxbufsize */
+ int err, ret;
+
+ g_warn_if_fail(c->sasl_decoded_offset == 0);
+
+ ret = spice_channel_read_wire(channel, encoded, sizeof(encoded));
+ if (ret < 0)
+ return ret;
+
+ err = sasl_decode(c->sasl_conn, encoded, ret,
+ &c->sasl_decoded, &c->sasl_decoded_length);
+ if (err != SASL_OK) {
+ g_warning("Failed to decode SASL data %s",
+ sasl_errstring(err, NULL, NULL));
+ c->has_error = TRUE;
+ return -EINVAL;
+ }
+ c->sasl_decoded_offset = 0;
+ }
+
+ if (c->sasl_decoded_length == 0)
+ return 0;
+
+ len = MIN(c->sasl_decoded_length - c->sasl_decoded_offset, len);
+ memcpy(data, c->sasl_decoded + c->sasl_decoded_offset, len);
+ c->sasl_decoded_offset += len;
+
+ if (c->sasl_decoded_offset == c->sasl_decoded_length) {
+ c->sasl_decoded_length = c->sasl_decoded_offset = 0;
+ c->sasl_decoded = NULL;
+ }
+
+ return len;
+}
+#endif
+
+/*
+ * Fill the 'data' buffer up with exactly 'len' bytes worth of data
+ */
+/* coroutine context */
+static int spice_channel_read(SpiceChannel *channel, void *data, size_t length)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ gsize len = length;
+ int ret;
+
+ while (len > 0) {
+ if (c->has_error) return 0; /* has_error is set by disconnect(), return no error */
+
+#if HAVE_SASL
+ if (c->sasl_conn)
+ ret = spice_channel_read_sasl(channel, data, len);
+ else
+#endif
+ ret = spice_channel_read_wire(channel, data, len);
+ if (ret < 0)
+ return ret;
+ g_assert(ret <= len);
+ len -= ret;
+ data = ((char*)data) + ret;
+#if DEBUG
+ if (len > 0)
+ CHANNEL_DEBUG(channel, "still needs %" G_GSIZE_FORMAT, len);
+#endif
+ }
+ c->total_read_bytes += length;
+
+ return length;
+}
+
+/* coroutine context */
+static void spice_channel_send_spice_ticket(SpiceChannel *channel)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ EVP_PKEY *pubkey;
+ int nRSASize;
+ BIO *bioKey;
+ RSA *rsa;
+ char *password;
+ uint8_t *encrypted;
+ int rc;
+
+ bioKey = BIO_new(BIO_s_mem());
+ g_return_if_fail(bioKey != NULL);
+
+ BIO_write(bioKey, c->peer_msg->pub_key, SPICE_TICKET_PUBKEY_BYTES);
+ pubkey = d2i_PUBKEY_bio(bioKey, NULL);
+ g_return_if_fail(pubkey != NULL);
+
+ rsa = pubkey->pkey.rsa;
+ nRSASize = RSA_size(rsa);
+
+ encrypted = g_alloca(nRSASize);
+ /*
+ The use of RSA encryption limit the potential maximum password length.
+ for RSA_PKCS1_OAEP_PADDING it is RSA_size(rsa) - 41.
+ */
+ g_object_get(c->session, "password", &password, NULL);
+ if (password == NULL)
+ password = g_strdup("");
+ rc = RSA_public_encrypt(strlen(password) + 1, (uint8_t*)password,
+ encrypted, rsa, RSA_PKCS1_OAEP_PADDING);
+ g_warn_if_fail(rc > 0);
+
+ spice_channel_write(channel, encrypted, nRSASize);
+ memset(encrypted, 0, nRSASize);
+ EVP_PKEY_free(pubkey);
+ BIO_free(bioKey);
+ g_free(password);
+}
+
+/* coroutine context */
+static void spice_channel_failed_authentication(SpiceChannel *channel)
+{
+ SpiceChannelPrivate *c = channel->priv;
+
+ if (c->auth_needs_username_and_password)
+ g_set_error_literal(&c->error,
+ SPICE_CLIENT_ERROR,
+ SPICE_CLIENT_ERROR_AUTH_NEEDS_PASSWORD_AND_USERNAME,
+ _("Authentication failed: password and username are required"));
+ else
+ g_set_error_literal(&c->error,
+ SPICE_CLIENT_ERROR,
+ SPICE_CLIENT_ERROR_AUTH_NEEDS_PASSWORD,
+ _("Authentication failed: password is required"));
+
+ c->event = SPICE_CHANNEL_ERROR_AUTH;
+
+ c->has_error = TRUE; /* force disconnect */
+}
+
+/* coroutine context */
+static gboolean spice_channel_recv_auth(SpiceChannel *channel)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ uint32_t link_res;
+ int rc;
+
+ rc = spice_channel_read(channel, &link_res, sizeof(link_res));
+ if (rc != sizeof(link_res)) {
+ CHANNEL_DEBUG(channel, "incomplete auth reply (%d/%" G_GSIZE_FORMAT ")",
+ rc, sizeof(link_res));
+ c->event = SPICE_CHANNEL_ERROR_LINK;
+ return FALSE;
+ }
+
+ if (link_res != SPICE_LINK_ERR_OK) {
+ CHANNEL_DEBUG(channel, "link result: reply %d", link_res);
+ spice_channel_failed_authentication(channel);
+ return FALSE;
+ }
+
+ c->state = SPICE_CHANNEL_STATE_READY;
+
+ g_coroutine_signal_emit(channel, signals[SPICE_CHANNEL_EVENT], 0, SPICE_CHANNEL_OPENED);
+
+ if (c->state == SPICE_CHANNEL_STATE_MIGRATION_HANDSHAKE) {
+ spice_channel_send_migration_handshake(channel);
+ }
+
+ if (c->state != SPICE_CHANNEL_STATE_MIGRATING)
+ spice_channel_up(channel);
+
+ return TRUE;
+}
+
+G_GNUC_INTERNAL
+void spice_channel_up(SpiceChannel *channel)
+{
+ SpiceChannelPrivate *c = channel->priv;
+
+ CHANNEL_DEBUG(channel, "channel up, state %d", c->state);
+
+ if (SPICE_CHANNEL_GET_CLASS(channel)->channel_up)
+ SPICE_CHANNEL_GET_CLASS(channel)->channel_up(channel);
+}
+
+/* coroutine context */
+static void spice_channel_send_link(SpiceChannel *channel)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ uint8_t *buffer, *p;
+ int protocol, i;
+
+ c->link_hdr.magic = SPICE_MAGIC;
+ c->link_hdr.size = sizeof(c->link_msg);
+
+ g_object_get(c->session, "protocol", &protocol, NULL);
+ switch (protocol) {
+ case 1: /* protocol 1 == major 1, old 0.4 protocol, last active minor */
+ c->link_hdr.major_version = 1;
+ c->link_hdr.minor_version = 3;
+ c->parser = spice_get_server_channel_parser1(c->channel_type, NULL);
+ c->marshallers = spice_message_marshallers_get1();
+ break;
+ case SPICE_VERSION_MAJOR: /* protocol 2 == current */
+ c->link_hdr.major_version = SPICE_VERSION_MAJOR;
+ c->link_hdr.minor_version = SPICE_VERSION_MINOR;
+ c->parser = spice_get_server_channel_parser(c->channel_type, NULL);
+ c->marshallers = spice_message_marshallers_get();
+ break;
+ default:
+ g_critical("unknown major %d", protocol);
+ return;
+ }
+
+ c->link_msg.connection_id = spice_session_get_connection_id(c->session);
+ c->link_msg.channel_type = c->channel_type;
+ c->link_msg.channel_id = c->channel_id;
+ c->link_msg.caps_offset = sizeof(c->link_msg);
+
+ c->link_msg.num_common_caps = c->common_caps->len;
+ c->link_msg.num_channel_caps = c->caps->len;
+ c->link_hdr.size += (c->link_msg.num_common_caps +
+ c->link_msg.num_channel_caps) * sizeof(uint32_t);
+
+ buffer = g_malloc0(sizeof(c->link_hdr) + c->link_hdr.size);
+ p = buffer;
+
+ memcpy(p, &c->link_hdr, sizeof(c->link_hdr)); p += sizeof(c->link_hdr);
+ memcpy(p, &c->link_msg, sizeof(c->link_msg)); p += sizeof(c->link_msg);
+
+ for (i = 0; i < c->common_caps->len; i++) {
+ *(uint32_t *)p = g_array_index(c->common_caps, uint32_t, i);
+ p += sizeof(uint32_t);
+ }
+ for (i = 0; i < c->caps->len; i++) {
+ *(uint32_t *)p = g_array_index(c->caps, uint32_t, i);
+ p += sizeof(uint32_t);
+ }
+ CHANNEL_DEBUG(channel, "channel type %d id %d num common caps %d num caps %d",
+ c->link_msg.channel_type,
+ c->link_msg.channel_id,
+ c->link_msg.num_common_caps,
+ c->link_msg.num_channel_caps);
+ spice_channel_write(channel, buffer, p - buffer);
+ g_free(buffer);
+}
+
+/* coroutine context */
+static gboolean spice_channel_recv_link_hdr(SpiceChannel *channel)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ int rc;
+
+ rc = spice_channel_read(channel, &c->peer_hdr, sizeof(c->peer_hdr));
+ if (rc != sizeof(c->peer_hdr)) {
+ g_warning("incomplete link header (%d/%" G_GSIZE_FORMAT ")",
+ rc, sizeof(c->peer_hdr));
+ goto error;
+ }
+ if (c->peer_hdr.magic != SPICE_MAGIC) {
+ g_warning("invalid SPICE_MAGIC!");
+ goto error;
+ }
+
+ CHANNEL_DEBUG(channel, "Peer version: %d:%d", c->peer_hdr.major_version, c->peer_hdr.minor_version);
+ if (c->peer_hdr.major_version != c->link_hdr.major_version) {
+ g_warning("major mismatch (got %d, expected %d)",
+ c->peer_hdr.major_version, c->link_hdr.major_version);
+ goto error;
+ }
+
+ c->peer_msg = g_malloc0(c->peer_hdr.size);
+ if (c->peer_msg == NULL) {
+ g_warning("invalid peer header size: %u", c->peer_hdr.size);
+ goto error;
+ }
+
+ return TRUE;
+
+error:
+ /* Windows socket seems to give early CONNRESET errors. The server
+ does not linger when closing the socket if the protocol is
+ incompatible. Try with the oldest protocol in this case: */
+ if (c->link_hdr.major_version != 1) {
+ SPICE_DEBUG("%s: error, switching to protocol 1 (spice 0.4)", c->name);
+ c->state = SPICE_CHANNEL_STATE_RECONNECTING;
+ g_object_set(c->session, "protocol", 1, NULL);
+ return FALSE;
+ }
+
+ c->event = SPICE_CHANNEL_ERROR_LINK;
+ return FALSE;
+}
+
+#if HAVE_SASL
+/*
+ * NB, keep in sync with similar method in spice/server/reds.c
+ */
+static gchar *addr_to_string(GSocketAddress *addr)
+{
+ GInetSocketAddress *iaddr = G_INET_SOCKET_ADDRESS(addr);
+ guint16 port;
+ GInetAddress *host;
+ gchar *hoststr;
+ gchar *ret;
+
+ host = g_inet_socket_address_get_address(iaddr);
+ port = g_inet_socket_address_get_port(iaddr);
+ hoststr = g_inet_address_to_string(host);
+
+ ret = g_strdup_printf("%s;%hu", hoststr, port);
+ g_free(hoststr);
+
+ return ret;
+}
+
+static gboolean
+spice_channel_gather_sasl_credentials(SpiceChannel *channel,
+ sasl_interact_t *interact)
+{
+ SpiceChannelPrivate *c;
+ int ninteract;
+ gboolean ret = TRUE;
+
+ g_return_val_if_fail(channel != NULL, FALSE);
+ g_return_val_if_fail(channel->priv != NULL, FALSE);
+
+ c = channel->priv;
+
+ /* FIXME: we could keep connection open and ask connection details if missing */
+
+ for (ninteract = 0 ; interact[ninteract].id != 0 ; ninteract++) {
+ switch (interact[ninteract].id) {
+ case SASL_CB_AUTHNAME:
+ case SASL_CB_USER:
+ c->auth_needs_username_and_password = TRUE;
+ if (spice_session_get_username(c->session) == NULL)
+ return FALSE;
+
+ interact[ninteract].result = spice_session_get_username(c->session);
+ interact[ninteract].len = strlen(interact[ninteract].result);
+ break;
+
+ case SASL_CB_PASS:
+ if (spice_session_get_password(c->session) == NULL) {
+ /* Even if we reach this point, we have to continue looking for
+ * SASL_CB_AUTHNAME|SASL_CB_USER, otherwise we would return a
+ * wrong error to the applications */
+ ret = FALSE;
+ continue;
+ }
+
+ interact[ninteract].result = spice_session_get_password(c->session);
+ interact[ninteract].len = strlen(interact[ninteract].result);
+ break;
+ }
+ }
+
+ CHANNEL_DEBUG(channel, "Filled SASL interact");
+
+ return ret;
+}
+
+/*
+ *
+ * Init msg from server
+ *
+ * u32 mechlist-length
+ * u8-array mechlist-string
+ *
+ * Start msg to server
+ *
+ * u32 mechname-length
+ * u8-array mechname-string
+ * u32 clientout-length
+ * u8-array clientout-string
+ *
+ * Start msg from server
+ *
+ * u32 serverin-length
+ * u8-array serverin-string
+ * u8 continue
+ *
+ * Step msg to server
+ *
+ * u32 clientout-length
+ * u8-array clientout-string
+ *
+ * Step msg from server
+ *
+ * u32 serverin-length
+ * u8-array serverin-string
+ * u8 continue
+ */
+
+#define SASL_MAX_MECHLIST_LEN 300
+#define SASL_MAX_MECHNAME_LEN 100
+#define SASL_MAX_DATA_LEN (1024 * 1024)
+
+/* Perform the SASL authentication process
+ */
+static gboolean spice_channel_perform_auth_sasl(SpiceChannel *channel)
+{
+ SpiceChannelPrivate *c;
+ sasl_conn_t *saslconn = NULL;
+ sasl_security_properties_t secprops;
+ const char *clientout;
+ char *serverin = NULL;
+ unsigned int clientoutlen;
+ int err;
+ char *localAddr = NULL, *remoteAddr = NULL;
+ const void *val;
+ sasl_ssf_t ssf;
+ static const sasl_callback_t saslcb[] = {
+ { .id = SASL_CB_USER },
+ { .id = SASL_CB_AUTHNAME },
+ { .id = SASL_CB_PASS },
+ { .id = 0 },
+ };
+ sasl_interact_t *interact = NULL;
+ guint32 len;
+ char *mechlist = NULL;
+ const char *mechname;
+ gboolean ret = FALSE;
+ GSocketAddress *addr = NULL;
+ guint8 complete;
+
+ g_return_val_if_fail(channel != NULL, FALSE);
+ g_return_val_if_fail(channel->priv != NULL, FALSE);
+
+ c = channel->priv;
+
+ /* Sets up the SASL library as a whole */
+ err = sasl_client_init(NULL);
+ CHANNEL_DEBUG(channel, "Client initialize SASL authentication %d", err);
+ if (err != SASL_OK) {
+ g_critical("failed to initialize SASL library: %d (%s)",
+ err, sasl_errstring(err, NULL, NULL));
+ goto error;
+ }
+
+ /* Get local address in form IPADDR:PORT */
+ addr = g_socket_get_local_address(c->sock, NULL);
+ if (!addr) {
+ g_critical("failed to get local address");
+ goto error;
+ }
+ if ((g_socket_address_get_family(addr) == G_SOCKET_FAMILY_IPV4 ||
+ g_socket_address_get_family(addr) == G_SOCKET_FAMILY_IPV6) &&
+ (localAddr = addr_to_string(addr)) == NULL)
+ goto error;
+ g_clear_object(&addr);
+
+ /* Get remote address in form IPADDR:PORT */
+ addr = g_socket_get_remote_address(c->sock, NULL);
+ if (!addr) {
+ g_critical("failed to get peer address");
+ goto error;
+ }
+ if ((g_socket_address_get_family(addr) == G_SOCKET_FAMILY_IPV4 ||
+ g_socket_address_get_family(addr) == G_SOCKET_FAMILY_IPV6) &&
+ (remoteAddr = addr_to_string(addr)) == NULL)
+ goto error;
+ g_clear_object(&addr);
+
+ CHANNEL_DEBUG(channel, "Client SASL new host:'%s' local:'%s' remote:'%s'",
+ spice_session_get_host(c->session), localAddr, remoteAddr);
+
+ /* Setup a handle for being a client */
+ err = sasl_client_new("spice",
+ spice_session_get_host(c->session),
+ localAddr,
+ remoteAddr,
+ saslcb,
+ SASL_SUCCESS_DATA,
+ &saslconn);
+
+ if (err != SASL_OK) {
+ g_critical("Failed to create SASL client context: %d (%s)",
+ err, sasl_errstring(err, NULL, NULL));
+ goto error;
+ }
+
+ if (c->ssl) {
+ sasl_ssf_t ssf;
+
+ ssf = SSL_get_cipher_bits(c->ssl, NULL);
+ err = sasl_setprop(saslconn, SASL_SSF_EXTERNAL, &ssf);
+ if (err != SASL_OK) {
+ g_critical("cannot set SASL external SSF %d (%s)",
+ err, sasl_errstring(err, NULL, NULL));
+ goto error;
+ }
+ }
+
+ memset(&secprops, 0, sizeof secprops);
+ /* If we've got TLS, we don't care about SSF */
+ secprops.min_ssf = c->ssl ? 0 : 56; /* Equiv to DES supported by all Kerberos */
+ secprops.max_ssf = c->ssl ? 0 : 100000; /* Very strong ! AES == 256 */
+ secprops.maxbufsize = 100000;
+ /* If we're not TLS, then forbid any anonymous or trivially crackable auth */
+ secprops.security_flags = c->ssl ? 0 :
+ SASL_SEC_NOANONYMOUS | SASL_SEC_NOPLAINTEXT;
+
+ err = sasl_setprop(saslconn, SASL_SEC_PROPS, &secprops);
+ if (err != SASL_OK) {
+ g_critical("cannot set security props %d (%s)",
+ err, sasl_errstring(err, NULL, NULL));
+ goto error;
+ }
+
+ /* Get the supported mechanisms from the server */
+ spice_channel_read(channel, &len, sizeof(len));
+ if (c->has_error)
+ goto error;
+ if (len > SASL_MAX_MECHLIST_LEN) {
+ g_critical("mechlistlen %d too long", len);
+ goto error;
+ }
+
+ mechlist = g_malloc0(len + 1);
+ spice_channel_read(channel, mechlist, len);
+ mechlist[len] = '\0';
+ if (c->has_error) {
+ goto error;
+ }
+
+restart:
+ /* Start the auth negotiation on the client end first */
+ CHANNEL_DEBUG(channel, "Client start negotiation mechlist '%s'", mechlist);
+ err = sasl_client_start(saslconn,
+ mechlist,
+ &interact,
+ &clientout,
+ &clientoutlen,
+ &mechname);
+ if (err != SASL_OK && err != SASL_CONTINUE && err != SASL_INTERACT) {
+ g_critical("Failed to start SASL negotiation: %d (%s)",
+ err, sasl_errdetail(saslconn));
+ goto error;
+ }
+
+ /* Need to gather some credentials from the client */
+ if (err == SASL_INTERACT) {
+ if (!spice_channel_gather_sasl_credentials(channel, interact)) {
+ CHANNEL_DEBUG(channel, "Failed to collect auth credentials");
+ goto error;
+ }
+ goto restart;
+ }
+
+ CHANNEL_DEBUG(channel, "Server start negotiation with mech %s. Data %d bytes %p '%s'",
+ mechname, clientoutlen, clientout, clientout);
+
+ if (clientoutlen > SASL_MAX_DATA_LEN) {
+ g_critical("SASL negotiation data too long: %d bytes",
+ clientoutlen);
+ goto error;
+ }
+
+ /* Send back the chosen mechname */
+ len = strlen(mechname);
+ spice_channel_write(channel, &len, sizeof(guint32));
+ spice_channel_write(channel, mechname, len);
+
+ /* NB, distinction of NULL vs "" is *critical* in SASL */
+ if (clientout) {
+ len = clientoutlen + 1;
+ spice_channel_write(channel, &len, sizeof(guint32));
+ spice_channel_write(channel, clientout, len);
+ } else {
+ len = 0;
+ spice_channel_write(channel, &len, sizeof(guint32));
+ }
+
+ if (c->has_error)
+ goto error;
+
+ CHANNEL_DEBUG(channel, "Getting sever start negotiation reply");
+ /* Read the 'START' message reply from server */
+ spice_channel_read(channel, &len, sizeof(len));
+ if (c->has_error)
+ goto error;
+ if (len > SASL_MAX_DATA_LEN) {
+ g_critical("SASL negotiation data too long: %d bytes",
+ len);
+ goto error;
+ }
+
+ /* NB, distinction of NULL vs "" is *critical* in SASL */
+ if (len > 0) {
+ serverin = g_malloc0(len);
+ spice_channel_read(channel, serverin, len);
+ serverin[len - 1] = '\0';
+ len--;
+ } else {
+ serverin = NULL;
+ }
+ spice_channel_read(channel, &complete, sizeof(guint8));
+ if (c->has_error)
+ goto error;
+
+ CHANNEL_DEBUG(channel, "Client start result complete: %d. Data %d bytes %p '%s'",
+ complete, len, serverin, serverin);
+
+ /* Loop-the-loop...
+ * Even if the server has completed, the client must *always* do at least one step
+ * in this loop to verify the server isn't lying about something. Mutual auth */
+ for (;;) {
+ if (complete && err == SASL_OK)
+ break;
+
+ restep:
+ err = sasl_client_step(saslconn,
+ serverin,
+ len,
+ &interact,
+ &clientout,
+ &clientoutlen);
+ if (err != SASL_OK && err != SASL_CONTINUE && err != SASL_INTERACT) {
+ g_critical("Failed SASL step: %d (%s)",
+ err, sasl_errdetail(saslconn));
+ goto error;
+ }
+
+ /* Need to gather some credentials from the client */
+ if (err == SASL_INTERACT) {
+ if (!spice_channel_gather_sasl_credentials(channel,
+ interact)) {
+ CHANNEL_DEBUG(channel, "%s", "Failed to collect auth credentials");
+ goto error;
+ }
+ goto restep;
+ }
+
+ if (serverin) {
+ g_free(serverin);
+ serverin = NULL;
+ }
+
+ CHANNEL_DEBUG(channel, "Client step result %d. Data %d bytes %p '%s'", err, clientoutlen, clientout, clientout);
+
+ /* Previous server call showed completion & we're now locally complete too */
+ if (complete && err == SASL_OK)
+ break;
+
+ /* Not done, prepare to talk with the server for another iteration */
+
+ /* NB, distinction of NULL vs "" is *critical* in SASL */
+ if (clientout) {
+ len = clientoutlen + 1;
+ spice_channel_write(channel, &len, sizeof(guint32));
+ spice_channel_write(channel, clientout, len);
+ } else {
+ len = 0;
+ spice_channel_write(channel, &len, sizeof(guint32));
+ }
+
+ if (c->has_error)
+ goto error;
+
+ CHANNEL_DEBUG(channel, "Server step with %d bytes %p", clientoutlen, clientout);
+
+ spice_channel_read(channel, &len, sizeof(guint32));
+ if (c->has_error)
+ goto error;
+ if (len > SASL_MAX_DATA_LEN) {
+ g_critical("SASL negotiation data too long: %d bytes", len);
+ goto error;
+ }
+
+ /* NB, distinction of NULL vs "" is *critical* in SASL */
+ if (len) {
+ serverin = g_malloc0(len);
+ spice_channel_read(channel, serverin, len);
+ serverin[len - 1] = '\0';
+ len--;
+ } else {
+ serverin = NULL;
+ }
+
+ spice_channel_read(channel, &complete, sizeof(guint8));
+ if (c->has_error)
+ goto error;
+
+ CHANNEL_DEBUG(channel, "Client step result complete: %d. Data %d bytes %p '%s'",
+ complete, len, serverin, serverin);
+
+ /* This server call shows complete, and earlier client step was OK */
+ if (complete) {
+ g_free(serverin);
+ serverin = NULL;
+ if (err == SASL_CONTINUE) /* something went wrong */
+ goto complete;
+ break;
+ }
+ }
+
+ /* Check for suitable SSF if non-TLS */
+ if (!c->ssl) {
+ err = sasl_getprop(saslconn, SASL_SSF, &val);
+ if (err != SASL_OK) {
+ g_critical("cannot query SASL ssf on connection %d (%s)",
+ err, sasl_errstring(err, NULL, NULL));
+ goto error;
+ }
+ ssf = *(const int *)val;
+ CHANNEL_DEBUG(channel, "SASL SSF value %d", ssf);
+ if (ssf < 56) { /* 56 == DES level, good for Kerberos */
+ g_critical("negotiation SSF %d was not strong enough", ssf);
+ goto error;
+ }
+ }
+
+complete:
+ CHANNEL_DEBUG(channel, "%s", "SASL authentication complete");
+ spice_channel_read(channel, &len, sizeof(len));
+ if (len == SPICE_LINK_ERR_OK) {
+ ret = TRUE;
+ /* This must come *after* check-auth-result, because the former
+ * is defined to be sent unencrypted, and setting saslconn turns
+ * on the SSF layer encryption processing */
+ c->sasl_conn = saslconn;
+ goto cleanup;
+ }
+
+error:
+ if (saslconn)
+ sasl_dispose(&saslconn);
+
+ spice_channel_failed_authentication(channel);
+ ret = FALSE;
+
+cleanup:
+ g_free(localAddr);
+ g_free(remoteAddr);
+ g_free(mechlist);
+ g_free(serverin);
+ g_clear_object(&addr);
+ return ret;
+}
+#endif /* HAVE_SASL */
+
+/* coroutine context */
+static gboolean spice_channel_recv_link_msg(SpiceChannel *channel)
+{
+ SpiceChannelPrivate *c;
+ int rc, num_caps, i;
+ uint32_t *caps;
+
+ g_return_val_if_fail(channel != NULL, FALSE);
+ g_return_val_if_fail(channel->priv != NULL, FALSE);
+
+ c = channel->priv;
+
+ rc = spice_channel_read(channel, (uint8_t*)c->peer_msg + c->peer_pos,
+ c->peer_hdr.size - c->peer_pos);
+ c->peer_pos += rc;
+ if (c->peer_pos != c->peer_hdr.size) {
+ g_critical("%s: %s: incomplete link reply (%d/%d)",
+ c->name, __FUNCTION__, rc, c->peer_hdr.size);
+ goto error;
+ }
+ switch (c->peer_msg->error) {
+ case SPICE_LINK_ERR_OK:
+ /* nothing */
+ break;
+ case SPICE_LINK_ERR_NEED_SECURED:
+ c->state = SPICE_CHANNEL_STATE_RECONNECTING;
+ CHANNEL_DEBUG(channel, "switching to tls");
+ c->tls = TRUE;
+ return FALSE;
+ default:
+ g_warning("%s: %s: unhandled error %d",
+ c->name, __FUNCTION__, c->peer_msg->error);
+ goto error;
+ }
+
+ num_caps = c->peer_msg->num_channel_caps + c->peer_msg->num_common_caps;
+ CHANNEL_DEBUG(channel, "%s: %d caps", __FUNCTION__, num_caps);
+
+ /* see original spice/client code: */
+ /* g_return_if_fail(c->peer_msg + c->peer_msg->caps_offset * sizeof(uint32_t) > c->peer_msg + c->peer_hdr.size); */
+
+ caps = (uint32_t *)((uint8_t *)c->peer_msg + c->peer_msg->caps_offset);
+
+ g_array_set_size(c->remote_common_caps, c->peer_msg->num_common_caps);
+ for (i = 0; i < c->peer_msg->num_common_caps; i++, caps++) {
+ g_array_index(c->remote_common_caps, uint32_t, i) = *caps;
+ CHANNEL_DEBUG(channel, "got common caps %u:0x%X", i, *caps);
+ }
+
+ g_array_set_size(c->remote_caps, c->peer_msg->num_channel_caps);
+ for (i = 0; i < c->peer_msg->num_channel_caps; i++, caps++) {
+ g_array_index(c->remote_caps, uint32_t, i) = *caps;
+ CHANNEL_DEBUG(channel, "got channel caps %u:0x%X", i, *caps);
+ }
+
+ if (!spice_channel_test_common_capability(channel,
+ SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION)) {
+ CHANNEL_DEBUG(channel, "Server supports spice ticket auth only");
+ spice_channel_send_spice_ticket(channel);
+ } else {
+ SpiceLinkAuthMechanism auth = { 0, };
+
+#if HAVE_SASL
+ if (spice_channel_test_common_capability(channel, SPICE_COMMON_CAP_AUTH_SASL)) {
+ CHANNEL_DEBUG(channel, "Choosing SASL mechanism");
+ auth.auth_mechanism = SPICE_COMMON_CAP_AUTH_SASL;
+ spice_channel_write(channel, &auth, sizeof(auth));
+ if (!spice_channel_perform_auth_sasl(channel))
+ return FALSE;
+ } else
+#endif
+ if (spice_channel_test_common_capability(channel, SPICE_COMMON_CAP_AUTH_SPICE)) {
+ auth.auth_mechanism = SPICE_COMMON_CAP_AUTH_SPICE;
+ spice_channel_write(channel, &auth, sizeof(auth));
+ spice_channel_send_spice_ticket(channel);
+ } else {
+ g_warning("No compatible AUTH mechanism");
+ goto error;
+ }
+ }
+ c->use_mini_header = spice_channel_test_common_capability(channel,
+ SPICE_COMMON_CAP_MINI_HEADER);
+ CHANNEL_DEBUG(channel, "use mini header: %d", c->use_mini_header);
+ return TRUE;
+
+error:
+ c->has_error = TRUE;
+ c->event = SPICE_CHANNEL_ERROR_LINK;
+ return FALSE;
+}
+
+/* system context */
+G_GNUC_INTERNAL
+void spice_channel_wakeup(SpiceChannel *channel, gboolean cancel)
+{
+ GCoroutine *c = &channel->priv->coroutine;
+
+ if (cancel)
+ g_coroutine_condition_cancel(c);
+
+ g_coroutine_wakeup(c);
+}
+
+G_GNUC_INTERNAL
+gboolean spice_channel_get_read_only(SpiceChannel *channel)
+{
+ return spice_session_get_read_only(channel->priv->session);
+}
+
+/* coroutine context */
+G_GNUC_INTERNAL
+void spice_channel_recv_msg(SpiceChannel *channel,
+ handler_msg_in msg_handler, gpointer data)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ SpiceMsgIn *in;
+ int msg_size;
+ int msg_type;
+ int sub_list_offset = 0;
+
+ in = spice_msg_in_new(channel);
+
+ /* receive message */
+ spice_channel_read(channel, in->header,
+ spice_header_get_header_size(c->use_mini_header));
+ if (c->has_error)
+ goto end;
+
+ msg_size = spice_header_get_msg_size(in->header, c->use_mini_header);
+ /* FIXME: do not allow others to take ref on in, and use realloc here?
+ * this would avoid malloc/free on each message?
+ */
+ in->data = g_malloc0(msg_size);
+ spice_channel_read(channel, in->data, msg_size);
+ if (c->has_error)
+ goto end;
+ in->dpos = msg_size;
+
+ msg_type = spice_header_get_msg_type(in->header, c->use_mini_header);
+ sub_list_offset = spice_header_get_msg_sub_list(in->header, c->use_mini_header);
+
+ if (msg_type == SPICE_MSG_LIST || sub_list_offset) {
+ SpiceSubMessageList *sub_list;
+ SpiceSubMessage *sub;
+ SpiceMsgIn *sub_in;
+ int i;
+
+ sub_list = (SpiceSubMessageList *)(in->data + sub_list_offset);
+ for (i = 0; i < sub_list->size; i++) {
+ sub = (SpiceSubMessage *)(in->data + sub_list->sub_messages[i]);
+ sub_in = spice_msg_in_sub_new(channel, in, sub);
+ sub_in->parsed = c->parser(sub_in->data, sub_in->data + sub_in->dpos,
+ spice_header_get_msg_type(sub_in->header,
+ c->use_mini_header),
+ c->peer_hdr.minor_version,
+ &sub_in->psize, &sub_in->pfree);
+ if (sub_in->parsed == NULL) {
+ g_critical("failed to parse sub-message: %s type %d",
+ c->name, spice_header_get_msg_type(sub_in->header, c->use_mini_header));
+ goto end;
+ }
+ msg_handler(channel, sub_in, data);
+ spice_msg_in_unref(sub_in);
+ }
+ }
+
+ /* ack message */
+ if (c->message_ack_count) {
+ c->message_ack_count--;
+ if (!c->message_ack_count) {
+ SpiceMsgOut *out = spice_msg_out_new(channel, SPICE_MSGC_ACK);
+ spice_msg_out_send_internal(out);
+ c->message_ack_count = c->message_ack_window;
+ }
+ }
+
+ if (msg_type == SPICE_MSG_LIST) {
+ goto end;
+ }
+
+ /* parse message */
+ in->parsed = c->parser(in->data, in->data + msg_size, msg_type,
+ c->peer_hdr.minor_version, &in->psize, &in->pfree);
+ if (in->parsed == NULL) {
+ g_critical("failed to parse message: %s type %d",
+ c->name, msg_type);
+ goto end;
+ }
+
+ /* process message */
+ /* spice_msg_in_hexdump(in); */
+ msg_handler(channel, in, data);
+
+end:
+ /* If the server uses full header, the serial is not necessarily equal
+ * to c->in_serial (the server can sometimes skip serials) */
+ c->last_message_serial = spice_header_get_in_msg_serial(in);
+ c->in_serial++;
+ spice_msg_in_unref(in);
+}
+
+static const char *to_string[] = {
+ NULL,
+ [ SPICE_CHANNEL_MAIN ] = "main",
+ [ SPICE_CHANNEL_DISPLAY ] = "display",
+ [ SPICE_CHANNEL_INPUTS ] = "inputs",
+ [ SPICE_CHANNEL_CURSOR ] = "cursor",
+ [ SPICE_CHANNEL_PLAYBACK ] = "playback",
+ [ SPICE_CHANNEL_RECORD ] = "record",
+ [ SPICE_CHANNEL_TUNNEL ] = "tunnel",
+ [ SPICE_CHANNEL_SMARTCARD ] = "smartcard",
+ [ SPICE_CHANNEL_USBREDIR ] = "usbredir",
+ [ SPICE_CHANNEL_PORT ] = "port",
+ [ SPICE_CHANNEL_WEBDAV ] = "webdav",
+};
+
+/**
+ * spice_channel_type_to_string:
+ * @type: a channel-type property value
+ *
+ * Convert a channel-type property value to a string.
+ *
+ * Returns: string representation of @type.
+ * Since: 0.20
+ **/
+const gchar* spice_channel_type_to_string(gint type)
+{
+ const char *str = NULL;
+
+ if (type >= 0 && type < G_N_ELEMENTS(to_string)) {
+ str = to_string[type];
+ }
+
+ return str ? str : "unknown channel type";
+}
+
+/**
+ * spice_channel_string_to_type:
+ * @str: a string representation of the channel-type property
+ *
+ * Convert a channel-type property value to a string.
+ *
+ * Returns: the channel-type property value for a @str channel
+ * Since: 0.21
+ **/
+gint spice_channel_string_to_type(const gchar *str)
+{
+ int i;
+
+ g_return_val_if_fail(str != NULL, -1);
+
+ for (i = 0; i < G_N_ELEMENTS(to_string); i++)
+ if (g_strcmp0(str, to_string[i]) == 0)
+ return i;
+
+ return -1;
+}
+
+G_GNUC_INTERNAL
+gchar *spice_channel_supported_string(void)
+{
+ return g_strjoin(", ",
+ spice_channel_type_to_string(SPICE_CHANNEL_MAIN),
+ spice_channel_type_to_string(SPICE_CHANNEL_DISPLAY),
+ spice_channel_type_to_string(SPICE_CHANNEL_INPUTS),
+ spice_channel_type_to_string(SPICE_CHANNEL_CURSOR),
+ spice_channel_type_to_string(SPICE_CHANNEL_PLAYBACK),
+ spice_channel_type_to_string(SPICE_CHANNEL_RECORD),
+#ifdef USE_SMARTCARD
+ spice_channel_type_to_string(SPICE_CHANNEL_SMARTCARD),
+#endif
+#ifdef USE_USBREDIR
+ spice_channel_type_to_string(SPICE_CHANNEL_USBREDIR),
+#endif
+#ifdef USE_PHODAV
+ spice_channel_type_to_string(SPICE_CHANNEL_WEBDAV),
+#endif
+ NULL);
+}
+
+
+/**
+ * spice_channel_new:
+ * @s: the @SpiceSession the channel is linked to
+ * @type: the requested SPICECHANNELPRIVATE type
+ * @id: the channel-id
+ *
+ * Create a new #SpiceChannel of type @type, and channel ID @id.
+ *
+ * Returns: a weak reference to #SpiceChannel, the session owns the reference
+ **/
+SpiceChannel *spice_channel_new(SpiceSession *s, int type, int id)
+{
+ SpiceChannel *channel;
+ GType gtype = 0;
+
+ g_return_val_if_fail(s != NULL, NULL);
+
+ switch (type) {
+ case SPICE_CHANNEL_MAIN:
+ gtype = SPICE_TYPE_MAIN_CHANNEL;
+ break;
+ case SPICE_CHANNEL_DISPLAY:
+ gtype = SPICE_TYPE_DISPLAY_CHANNEL;
+ break;
+ case SPICE_CHANNEL_CURSOR:
+ gtype = SPICE_TYPE_CURSOR_CHANNEL;
+ break;
+ case SPICE_CHANNEL_INPUTS:
+ gtype = SPICE_TYPE_INPUTS_CHANNEL;
+ break;
+ case SPICE_CHANNEL_PLAYBACK:
+ case SPICE_CHANNEL_RECORD: {
+ if (!spice_session_get_audio_enabled(s)) {
+ g_debug("audio channel is disabled, not creating it");
+ return NULL;
+ }
+ gtype = type == SPICE_CHANNEL_RECORD ?
+ SPICE_TYPE_RECORD_CHANNEL : SPICE_TYPE_PLAYBACK_CHANNEL;
+ break;
+ }
+#ifdef USE_SMARTCARD
+ case SPICE_CHANNEL_SMARTCARD: {
+ if (!spice_session_get_smartcard_enabled(s)) {
+ g_debug("smartcard channel is disabled, not creating it");
+ return NULL;
+ }
+ gtype = SPICE_TYPE_SMARTCARD_CHANNEL;
+ break;
+ }
+#endif
+#ifdef USE_USBREDIR
+ case SPICE_CHANNEL_USBREDIR: {
+ if (!spice_session_get_usbredir_enabled(s)) {
+ g_debug("usbredir channel is disabled, not creating it");
+ return NULL;
+ }
+ gtype = SPICE_TYPE_USBREDIR_CHANNEL;
+ break;
+ }
+#endif
+#ifdef USE_PHODAV
+ case SPICE_CHANNEL_WEBDAV: {
+ gtype = SPICE_TYPE_WEBDAV_CHANNEL;
+ break;
+ }
+#endif
+ case SPICE_CHANNEL_PORT:
+ gtype = SPICE_TYPE_PORT_CHANNEL;
+ break;
+ default:
+ g_debug("unsupported channel kind: %s: %d",
+ spice_channel_type_to_string(type), type);
+ return NULL;
+ }
+ channel = SPICE_CHANNEL(g_object_new(gtype,
+ "spice-session", s,
+ "channel-type", type,
+ "channel-id", id,
+ NULL));
+ return channel;
+}
+
+/**
+ * spice_channel_destroy:
+ * @channel:
+ *
+ * Disconnect and unref the @channel.
+ *
+ * Deprecated: 0.27: this function has been deprecated because it is
+ * misleading, the object is not actually destroyed. Instead, it is
+ * recommended to call explicitely spice_channel_disconnect() and
+ * g_object_unref().
+ **/
+void spice_channel_destroy(SpiceChannel *channel)
+{
+ g_return_if_fail(channel != NULL);
+
+ CHANNEL_DEBUG(channel, "channel destroy");
+ spice_channel_disconnect(channel, SPICE_CHANNEL_NONE);
+ g_object_unref(channel);
+}
+
+/* any context */
+static void spice_channel_flushed(SpiceChannel *channel, gboolean success)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ GSList *l;
+
+ for (l = c->flushing; l != NULL; l = l->next) {
+ GSimpleAsyncResult *result = G_SIMPLE_ASYNC_RESULT(l->data);
+ g_simple_async_result_set_op_res_gboolean(result, success);
+ g_simple_async_result_complete_in_idle(result);
+ }
+
+ g_slist_free_full(c->flushing, g_object_unref);
+ c->flushing = NULL;
+}
+
+/* coroutine context */
+static void spice_channel_iterate_write(SpiceChannel *channel)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ SpiceMsgOut *out;
+
+ do {
+ STATIC_MUTEX_LOCK(c->xmit_queue_lock);
+ out = g_queue_pop_head(&c->xmit_queue);
+ STATIC_MUTEX_UNLOCK(c->xmit_queue_lock);
+ if (out)
+ spice_channel_write_msg(channel, out);
+ } while (out);
+
+ spice_channel_flushed(channel, TRUE);
+}
+
+/* coroutine context */
+static void spice_channel_iterate_read(SpiceChannel *channel)
+{
+ SpiceChannelPrivate *c = channel->priv;
+
+ g_coroutine_socket_wait(&c->coroutine, c->sock, G_IO_IN);
+
+ /* treat all incoming data (block on message completion) */
+ while (!c->has_error &&
+ c->state != SPICE_CHANNEL_STATE_MIGRATING &&
+ g_pollable_input_stream_is_readable(G_POLLABLE_INPUT_STREAM(c->in))
+ ) { do
+ spice_channel_recv_msg(channel,
+ (handler_msg_in)SPICE_CHANNEL_GET_CLASS(channel)->handle_msg, NULL);
+#if HAVE_SASL
+ /* flush the sasl buffer too */
+ while (c->sasl_decoded != NULL);
+#else
+ while (FALSE);
+#endif
+ }
+
+}
+
+static gboolean wait_migration(gpointer data)
+{
+ SpiceChannel *channel = SPICE_CHANNEL(data);
+ SpiceChannelPrivate *c = channel->priv;
+
+ if (c->state != SPICE_CHANNEL_STATE_MIGRATING) {
+ CHANNEL_DEBUG(channel, "unfreeze channel");
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/* coroutine context */
+static gboolean spice_channel_iterate(SpiceChannel *channel)
+{
+ SpiceChannelPrivate *c = channel->priv;
+
+ if (c->state == SPICE_CHANNEL_STATE_MIGRATING &&
+ !g_coroutine_condition_wait(&c->coroutine, wait_migration, channel))
+ CHANNEL_DEBUG(channel, "migration wait cancelled");
+
+ /* flush any pending write and read */
+ if (!c->has_error)
+ SPICE_CHANNEL_GET_CLASS(channel)->iterate_write(channel);
+ if (!c->has_error)
+ SPICE_CHANNEL_GET_CLASS(channel)->iterate_read(channel);
+
+ if (c->has_error) {
+ GIOCondition ret;
+
+ if (!c->sock)
+ return FALSE;
+
+ /* We don't want to report an error if the socket was closed gracefully
+ * on the other end (VM shutdown) */
+ ret = g_socket_condition_check(c->sock, G_IO_IN | G_IO_ERR);
+
+ if (ret & G_IO_ERR) {
+ CHANNEL_DEBUG(channel, "channel got error");
+
+ if (c->state > SPICE_CHANNEL_STATE_CONNECTING) {
+ if (c->state == SPICE_CHANNEL_STATE_READY)
+ c->event = SPICE_CHANNEL_ERROR_IO;
+ else
+ c->event = SPICE_CHANNEL_ERROR_LINK;
+ }
+ }
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+/* we use an idle function to allow the coroutine to exit before we actually
+ * unref the object since the coroutine's state is part of the object */
+static gboolean spice_channel_delayed_unref(gpointer data)
+{
+ SpiceChannel *channel = SPICE_CHANNEL(data);
+ SpiceChannelPrivate *c = channel->priv;
+ gboolean was_ready = c->state == SPICE_CHANNEL_STATE_READY;
+
+ CHANNEL_DEBUG(channel, "Delayed unref channel %p", channel);
+
+ g_return_val_if_fail(c->coroutine.coroutine.exited == TRUE, FALSE);
+
+ c->state = SPICE_CHANNEL_STATE_UNCONNECTED;
+
+ if (c->event != SPICE_CHANNEL_NONE) {
+ g_coroutine_signal_emit(channel, signals[SPICE_CHANNEL_EVENT], 0, c->event);
+ c->event = SPICE_CHANNEL_NONE;
+ g_clear_error(&c->error);
+ }
+
+ if (was_ready)
+ g_coroutine_signal_emit(channel, signals[SPICE_CHANNEL_EVENT], 0, SPICE_CHANNEL_CLOSED);
+
+ g_object_unref(G_OBJECT(data));
+
+ return FALSE;
+}
+
+static X509_LOOKUP_METHOD spice_x509_mem_lookup = {
+ "spice_x509_mem_lookup",
+ 0
+};
+
+static int spice_channel_load_ca(SpiceChannel *channel)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ STACK_OF(X509_INFO) *inf;
+ X509_INFO *itmp;
+ X509_LOOKUP *lookup;
+ BIO *in;
+ int i, count = 0;
+ guint8 *ca;
+ guint size;
+ const gchar *ca_file;
+ int rc;
+
+ g_return_val_if_fail(c->ctx != NULL, 0);
+
+ lookup = X509_STORE_add_lookup(c->ctx->cert_store, &spice_x509_mem_lookup);
+ ca_file = spice_session_get_ca_file(c->session);
+ spice_session_get_ca(c->session, &ca, &size);
+
+ CHANNEL_DEBUG(channel, "Load CA, file: %s, data: %p", ca_file, ca);
+ g_warn_if_fail(ca_file || ca);
+
+ if (ca != NULL) {
+ in = BIO_new_mem_buf(ca, size);
+ inf = PEM_X509_INFO_read_bio(in, NULL, NULL, NULL);
+ BIO_free(in);
+
+ for (i = 0; i < sk_X509_INFO_num(inf); i++) {
+ itmp = sk_X509_INFO_value(inf, i);
+ if (itmp->x509) {
+ X509_STORE_add_cert(lookup->store_ctx, itmp->x509);
+ count++;
+ }
+ if (itmp->crl) {
+ X509_STORE_add_crl(lookup->store_ctx, itmp->crl);
+ count++;
+ }
+ }
+
+ sk_X509_INFO_pop_free(inf, X509_INFO_free);
+ }
+
+ if (ca_file != NULL) {
+ rc = SSL_CTX_load_verify_locations(c->ctx, ca_file, NULL);
+ if (rc != 1)
+ g_warning("loading ca certs from %s failed", ca_file);
+ else
+ count++;
+ }
+
+ if (count == 0) {
+ rc = SSL_CTX_set_default_verify_paths(c->ctx);
+ if (rc != 1)
+ g_warning("loading ca certs from default location failed");
+ else
+ count++;
+ }
+
+ return count;
+}
+
+/**
+ * spice_channel_get_error:
+ * @channel:
+ *
+ * Retrieves the #GError currently set on channel, if the #SpiceChannel
+ * is in error state and can provide additional error details.
+ *
+ * Returns: the pointer to the error, or %NULL
+ * Since: 0.24
+ **/
+const GError* spice_channel_get_error(SpiceChannel *self)
+{
+ SpiceChannelPrivate *c;
+
+ g_return_val_if_fail(SPICE_IS_CHANNEL(self), NULL);
+ c = self->priv;
+
+ return c->error;
+}
+
+/* coroutine context */
+static void *spice_channel_coroutine(void *data)
+{
+ SpiceChannel *channel = SPICE_CHANNEL(data);
+ SpiceChannelPrivate *c = channel->priv;
+ guint verify;
+ int rc, delay_val = 1;
+ /* When some other SSL/TLS version becomes obsolete, add it to this
+ * variable. */
+ long ssl_options = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3;
+
+ CHANNEL_DEBUG(channel, "Started background coroutine %p", &c->coroutine);
+
+ if (spice_session_get_client_provided_socket(c->session)) {
+ if (c->fd < 0) {
+ g_critical("fd not provided!");
+ c->event = SPICE_CHANNEL_ERROR_CONNECT;
+ goto cleanup;
+ }
+
+ if (!(c->sock = g_socket_new_from_fd(c->fd, NULL))) {
+ CHANNEL_DEBUG(channel, "Failed to open socket from fd %d", c->fd);
+ c->event = SPICE_CHANNEL_ERROR_CONNECT;
+ goto cleanup;
+ }
+
+ g_socket_set_blocking(c->sock, FALSE);
+ g_socket_set_keepalive(c->sock, TRUE);
+ c->conn = g_socket_connection_factory_create_connection(c->sock);
+ goto connected;
+ }
+
+
+reconnect:
+ c->conn = spice_session_channel_open_host(c->session, channel, &c->tls, &c->error);
+ if (c->conn == NULL) {
+ if (!c->error && !c->tls) {
+ CHANNEL_DEBUG(channel, "trying with TLS port");
+ c->tls = true; /* FIXME: does that really work with provided fd */
+ goto reconnect;
+ } else {
+ CHANNEL_DEBUG(channel, "Connect error");
+ c->event = SPICE_CHANNEL_ERROR_CONNECT;
+ goto cleanup;
+ }
+ }
+ c->sock = g_object_ref(g_socket_connection_get_socket(c->conn));
+
+ if (c->tls) {
+ c->ctx = SSL_CTX_new(SSLv23_method());
+ if (c->ctx == NULL) {
+ g_critical("SSL_CTX_new failed");
+ c->event = SPICE_CHANNEL_ERROR_TLS;
+ goto cleanup;
+ }
+
+ SSL_CTX_set_options(c->ctx, ssl_options);
+
+ verify = spice_session_get_verify(c->session);
+ if (verify &
+ (SPICE_SESSION_VERIFY_SUBJECT | SPICE_SESSION_VERIFY_HOSTNAME)) {
+ rc = spice_channel_load_ca(channel);
+ if (rc == 0) {
+ g_warning("no cert loaded");
+ if (verify & SPICE_SESSION_VERIFY_PUBKEY) {
+ g_warning("only pubkey active");
+ verify = SPICE_SESSION_VERIFY_PUBKEY;
+ } else {
+ c->event = SPICE_CHANNEL_ERROR_TLS;
+ goto cleanup;
+ }
+ }
+ }
+
+ {
+ const gchar *ciphers = spice_session_get_ciphers(c->session);
+ if (ciphers != NULL) {
+ rc = SSL_CTX_set_cipher_list(c->ctx, ciphers);
+ if (rc != 1)
+ g_warning("loading cipher list %s failed", ciphers);
+ }
+ }
+
+ c->ssl = SSL_new(c->ctx);
+ if (c->ssl == NULL) {
+ g_critical("SSL_new failed");
+ c->event = SPICE_CHANNEL_ERROR_TLS;
+ goto cleanup;
+ }
+
+
+ BIO *bio = bio_new_giostream(G_IO_STREAM(c->conn));
+ SSL_set_bio(c->ssl, bio, bio);
+
+ {
+ guint8 *pubkey;
+ guint pubkey_len;
+
+ spice_session_get_pubkey(c->session, &pubkey, &pubkey_len);
+ c->sslverify = spice_openssl_verify_new(c->ssl, verify,
+ spice_session_get_host(c->session),
+ (char*)pubkey, pubkey_len,
+ spice_session_get_cert_subject(c->session));
+ }
+
+ssl_reconnect:
+ rc = SSL_connect(c->ssl);
+ if (rc <= 0) {
+ rc = SSL_get_error(c->ssl, rc);
+ if (rc == SSL_ERROR_WANT_READ || rc == SSL_ERROR_WANT_WRITE) {
+ g_coroutine_socket_wait(&c->coroutine, c->sock, G_IO_OUT|G_IO_ERR|G_IO_HUP);
+ goto ssl_reconnect;
+ } else {
+ g_warning("%s: SSL_connect: %s",
+ c->name, ERR_error_string(rc, NULL));
+ c->event = SPICE_CHANNEL_ERROR_TLS;
+ goto cleanup;
+ }
+ }
+ }
+
+connected:
+ c->has_error = FALSE;
+ c->in = g_io_stream_get_input_stream(G_IO_STREAM(c->conn));
+ c->out = g_io_stream_get_output_stream(G_IO_STREAM(c->conn));
+
+ rc = setsockopt(g_socket_get_fd(c->sock), IPPROTO_TCP, TCP_NODELAY,
+ (const char*)&delay_val, sizeof(delay_val));
+ if ((rc != 0)
+#ifdef ENOTSUP
+ && (errno != ENOTSUP)
+#endif
+ ) {
+ g_warning("%s: could not set sockopt TCP_NODELAY: %s", c->name,
+ strerror(errno));
+ }
+
+ spice_channel_send_link(channel);
+ if (!spice_channel_recv_link_hdr(channel) ||
+ !spice_channel_recv_link_msg(channel) ||
+ !spice_channel_recv_auth(channel))
+ goto cleanup;
+
+ while (spice_channel_iterate(channel))
+ ;
+
+cleanup:
+ CHANNEL_DEBUG(channel, "Coroutine exit %s", c->name);
+
+ spice_channel_reset(channel, FALSE);
+
+ if (c->state == SPICE_CHANNEL_STATE_RECONNECTING ||
+ c->state == SPICE_CHANNEL_STATE_SWITCHING) {
+ g_warn_if_fail(c->event == SPICE_CHANNEL_NONE);
+ channel_connect(channel, c->tls);
+ g_object_unref(channel);
+ } else
+ g_idle_add(spice_channel_delayed_unref, data);
+
+ /* Co-routine exits now - the SpiceChannel object may no longer exist,
+ so don't do anything else now unless you like SEGVs */
+ return NULL;
+}
+
+static gboolean connect_delayed(gpointer data)
+{
+ SpiceChannel *channel = data;
+ SpiceChannelPrivate *c = channel->priv;
+ struct coroutine *co;
+
+ CHANNEL_DEBUG(channel, "Open coroutine starting %p", channel);
+ c->connect_delayed_id = 0;
+
+ co = &c->coroutine.coroutine;
+
+ co->stack_size = 16 << 20; /* 16Mb */
+ co->entry = spice_channel_coroutine;
+ co->release = NULL;
+
+ coroutine_init(co);
+ coroutine_yieldto(co, channel);
+
+ return FALSE;
+}
+
+/* any context */
+static gboolean channel_connect(SpiceChannel *channel, gboolean tls)
+{
+ SpiceChannelPrivate *c = channel->priv;
+
+ g_return_val_if_fail(c != NULL, FALSE);
+
+ if (c->session == NULL || c->channel_type == -1 || c->channel_id == -1) {
+ /* unset properties or unknown channel type */
+ g_warning("%s: channel setup incomplete", __FUNCTION__);
+ return false;
+ }
+
+ c->state = SPICE_CHANNEL_STATE_CONNECTING;
+ c->tls = tls;
+
+ if (spice_session_get_client_provided_socket(c->session)) {
+ if (c->fd == -1) {
+ CHANNEL_DEBUG(channel, "requesting fd");
+ /* FIXME: no way for client to provide fd atm. */
+ /* It could either chain on parent channel.. */
+ /* or register migration channel on parent session, or ? */
+ g_signal_emit(channel, signals[SPICE_CHANNEL_OPEN_FD], 0, c->tls);
+ return true;
+ }
+ }
+
+ c->xmit_queue_blocked = FALSE;
+
+ g_return_val_if_fail(c->sock == NULL, FALSE);
+ g_object_ref(G_OBJECT(channel)); /* Unref'd when co-routine exits */
+
+ /* we connect in idle, to let previous coroutine exit, if present */
+ c->connect_delayed_id = g_idle_add(connect_delayed, channel);
+
+ return true;
+}
+
+/**
+ * spice_channel_connect:
+ * @channel:
+ *
+ * Connect the channel, using #SpiceSession connection informations
+ *
+ * Returns: %TRUE on success.
+ **/
+gboolean spice_channel_connect(SpiceChannel *channel)
+{
+ g_return_val_if_fail(SPICE_IS_CHANNEL(channel), FALSE);
+ SpiceChannelPrivate *c = channel->priv;
+
+ if (c->state >= SPICE_CHANNEL_STATE_CONNECTING)
+ return TRUE;
+
+ g_return_val_if_fail(channel->priv->fd == -1, FALSE);
+
+ return channel_connect(channel, FALSE);
+}
+
+/**
+ * spice_channel_open_fd:
+ * @channel:
+ * @fd: a file descriptor (socket) or -1.
+ * request mechanism
+ *
+ * Connect the channel using @fd socket.
+ *
+ * If @fd is -1, a valid fd will be requested later via the
+ * SpiceChannel::open-fd signal.
+ *
+ * Returns: %TRUE on success.
+ **/
+gboolean spice_channel_open_fd(SpiceChannel *channel, int fd)
+{
+ SpiceChannelPrivate *c;
+
+ g_return_val_if_fail(SPICE_IS_CHANNEL(channel), FALSE);
+ g_return_val_if_fail(channel->priv != NULL, FALSE);
+ g_return_val_if_fail(channel->priv->fd == -1, FALSE);
+ g_return_val_if_fail(fd >= -1, FALSE);
+
+ c = channel->priv;
+ if (c->state > SPICE_CHANNEL_STATE_CONNECTING) {
+ g_warning("Invalid channel_connect state: %d", c->state);
+ return true;
+ }
+
+ c->fd = fd;
+
+ return channel_connect(channel, FALSE);
+}
+
+/* system or coroutine context */
+static void channel_reset(SpiceChannel *channel, gboolean migrating)
+{
+ SpiceChannelPrivate *c = channel->priv;
+
+ CHANNEL_DEBUG(channel, "channel reset");
+ if (c->connect_delayed_id) {
+ g_source_remove(c->connect_delayed_id);
+ c->connect_delayed_id = 0;
+ }
+
+#if HAVE_SASL
+ if (c->sasl_conn) {
+ sasl_dispose(&c->sasl_conn);
+ c->sasl_conn = NULL;
+ c->sasl_decoded_offset = c->sasl_decoded_length = 0;
+ }
+#endif
+
+ spice_openssl_verify_free(c->sslverify);
+ c->sslverify = NULL;
+
+ if (c->ssl) {
+ SSL_free(c->ssl);
+ c->ssl = NULL;
+ }
+
+ if (c->ctx) {
+ SSL_CTX_free(c->ctx);
+ c->ctx = NULL;
+ }
+
+ if (c->conn) {
+ g_object_unref(c->conn);
+ c->conn = NULL;
+ }
+
+ g_clear_object(&c->sock);
+
+ c->fd = -1;
+
+ c->auth_needs_username_and_password = FALSE;
+
+ g_free(c->peer_msg);
+ c->peer_msg = NULL;
+ c->peer_pos = 0;
+
+ STATIC_MUTEX_LOCK(c->xmit_queue_lock);
+ c->xmit_queue_blocked = TRUE; /* Disallow queuing new messages */
+ gboolean was_empty = g_queue_is_empty(&c->xmit_queue);
+ g_queue_foreach(&c->xmit_queue, (GFunc)spice_msg_out_unref, NULL);
+ g_queue_clear(&c->xmit_queue);
+ if (c->xmit_queue_wakeup_id) {
+ g_source_remove(c->xmit_queue_wakeup_id);
+ c->xmit_queue_wakeup_id = 0;
+ }
+ STATIC_MUTEX_UNLOCK(c->xmit_queue_lock);
+ spice_channel_flushed(channel, was_empty);
+
+ g_array_set_size(c->remote_common_caps, 0);
+ g_array_set_size(c->remote_caps, 0);
+ g_array_set_size(c->common_caps, 0);
+ /* Restore our default capabilities in case the channel gets re-used */
+ spice_channel_set_common_capability(channel, SPICE_COMMON_CAP_PROTOCOL_AUTH_SELECTION);
+ spice_channel_set_common_capability(channel, SPICE_COMMON_CAP_MINI_HEADER);
+ spice_channel_reset_capabilities(channel);
+
+ if (c->state == SPICE_CHANNEL_STATE_SWITCHING)
+ spice_session_set_migration_state(spice_channel_get_session(channel),
+ SPICE_SESSION_MIGRATION_NONE);
+}
+
+/* system or coroutine context */
+G_GNUC_INTERNAL
+void spice_channel_reset(SpiceChannel *channel, gboolean migrating)
+{
+ CHANNEL_DEBUG(channel, "reset %s", migrating ? "migrating" : "");
+ SPICE_CHANNEL_GET_CLASS(channel)->channel_reset(channel, migrating);
+}
+
+/**
+ * spice_channel_disconnect:
+ * @channel:
+ * @reason: a channel event emitted on main context (or #SPICE_CHANNEL_NONE)
+ *
+ * Close the socket and reset connection specific data. Finally, emit
+ * @reason #SpiceChannel::channel-event on main context if not
+ * #SPICE_CHANNEL_NONE.
+ **/
+void spice_channel_disconnect(SpiceChannel *channel, SpiceChannelEvent reason)
+{
+ SpiceChannelPrivate *c;
+
+ CHANNEL_DEBUG(channel, "channel disconnect %d", reason);
+
+ g_return_if_fail(SPICE_IS_CHANNEL(channel));
+ g_return_if_fail(channel->priv != NULL);
+
+ c = channel->priv;
+
+ if (c->state == SPICE_CHANNEL_STATE_UNCONNECTED)
+ return;
+
+ if (reason == SPICE_CHANNEL_SWITCHING)
+ c->state = SPICE_CHANNEL_STATE_SWITCHING;
+
+ c->has_error = TRUE; /* break the loop */
+
+ if (c->state == SPICE_CHANNEL_STATE_MIGRATING) {
+ c->state = SPICE_CHANNEL_STATE_READY;
+ } else
+ spice_channel_wakeup(channel, TRUE);
+
+ if (reason != SPICE_CHANNEL_NONE)
+ g_signal_emit(G_OBJECT(channel), signals[SPICE_CHANNEL_EVENT], 0, reason);
+}
+
+static gboolean test_capability(GArray *caps, guint32 cap)
+{
+ guint32 c, word_index = cap / 32;
+ gboolean ret;
+
+ if (caps == NULL)
+ return FALSE;
+
+ if (caps->len < word_index + 1)
+ return FALSE;
+
+ c = g_array_index(caps, guint32, word_index);
+ ret = (c & (1 << (cap % 32))) != 0;
+
+ SPICE_DEBUG("test cap %d in 0x%X: %s", cap, c, ret ? "yes" : "no");
+ return ret;
+}
+
+/**
+ * spice_channel_test_capability:
+ * @channel:
+ * @cap:
+ *
+ * Test availability of remote "channel kind capability".
+ *
+ * Returns: %TRUE if @cap (channel kind capability) is available.
+ **/
+gboolean spice_channel_test_capability(SpiceChannel *self, guint32 cap)
+{
+ SpiceChannelPrivate *c;
+
+ g_return_val_if_fail(SPICE_IS_CHANNEL(self), FALSE);
+
+ c = self->priv;
+ return test_capability(c->remote_caps, cap);
+}
+
+/**
+ * spice_channel_test_common_capability:
+ * @channel:
+ * @cap:
+ *
+ * Test availability of remote "common channel capability".
+ *
+ * Returns: %TRUE if @cap (common channel capability) is available.
+ **/
+gboolean spice_channel_test_common_capability(SpiceChannel *self, guint32 cap)
+{
+ SpiceChannelPrivate *c;
+
+ g_return_val_if_fail(SPICE_IS_CHANNEL(self), FALSE);
+
+ c = self->priv;
+ return test_capability(c->remote_common_caps, cap);
+}
+
+static void set_capability(GArray *caps, guint32 cap)
+{
+ guint word_index = cap / 32;
+
+ g_return_if_fail(caps != NULL);
+
+ if (caps->len <= word_index)
+ g_array_set_size(caps, word_index + 1);
+
+ g_array_index(caps, guint32, word_index) =
+ g_array_index(caps, guint32, word_index) | (1 << (cap % 32));
+}
+
+/**
+ * spice_channel_set_capability:
+ * @channel:
+ * @cap: a capability
+ *
+ * Enable specific channel-kind capability.
+ * Deprecated: 0.13: this function has been removed
+ **/
+#undef spice_channel_set_capability
+void spice_channel_set_capability(SpiceChannel *channel, guint32 cap)
+{
+ SpiceChannelPrivate *c;
+
+ g_return_if_fail(SPICE_IS_CHANNEL(channel));
+
+ c = channel->priv;
+ set_capability(c->caps, cap);
+}
+
+G_GNUC_INTERNAL
+void spice_caps_set(GArray *caps, guint32 cap, const gchar *desc)
+{
+ g_return_if_fail(caps != NULL);
+ g_return_if_fail(desc != NULL);
+
+ if (g_strcmp0(g_getenv(desc), "0") == 0)
+ return;
+
+ set_capability(caps, cap);
+}
+
+G_GNUC_INTERNAL
+SpiceSession* spice_channel_get_session(SpiceChannel *channel)
+{
+ g_return_val_if_fail(SPICE_IS_CHANNEL(channel), NULL);
+
+ return channel->priv->session;
+}
+
+G_GNUC_INTERNAL
+enum spice_channel_state spice_channel_get_state(SpiceChannel *channel)
+{
+ g_return_val_if_fail(SPICE_IS_CHANNEL(channel),
+ SPICE_CHANNEL_STATE_UNCONNECTED);
+
+ return channel->priv->state;
+}
+
+G_GNUC_INTERNAL
+void spice_channel_swap(SpiceChannel *channel, SpiceChannel *swap, gboolean swap_msgs)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ SpiceChannelPrivate *s = swap->priv;
+
+ g_return_if_fail(c != NULL);
+ g_return_if_fail(s != NULL);
+
+ g_return_if_fail(s->session != NULL);
+ g_return_if_fail(s->sock != NULL);
+
+#define SWAP(Field) ({ \
+ typeof (c->Field) Field = c->Field; \
+ c->Field = s->Field; \
+ s->Field = Field; \
+})
+
+ /* TODO: split channel in 2 objects: a controller and a swappable
+ state object */
+ SWAP(sock);
+ SWAP(conn);
+ SWAP(in);
+ SWAP(out);
+ SWAP(ctx);
+ SWAP(ssl);
+ SWAP(sslverify);
+ SWAP(tls);
+ SWAP(use_mini_header);
+ if (swap_msgs) {
+ SWAP(xmit_queue);
+ SWAP(xmit_queue_blocked);
+ SWAP(in_serial);
+ SWAP(out_serial);
+ }
+ SWAP(caps);
+ SWAP(common_caps);
+ SWAP(remote_caps);
+ SWAP(remote_common_caps);
+#if HAVE_SASL
+ SWAP(sasl_conn);
+ SWAP(sasl_decoded);
+ SWAP(sasl_decoded_length);
+ SWAP(sasl_decoded_offset);
+#endif
+}
+
+/* coroutine context */
+static void spice_channel_handle_msg(SpiceChannel *channel, SpiceMsgIn *msg)
+{
+ SpiceChannelClass *klass = SPICE_CHANNEL_GET_CLASS(channel);
+ int type = spice_msg_in_type(msg);
+ spice_msg_handler handler;
+
+ g_return_if_fail(type < klass->handlers->len);
+ if (type > SPICE_MSG_BASE_LAST && channel->priv->disable_channel_msg)
+ return;
+
+ handler = g_array_index(klass->handlers, spice_msg_handler, type);
+ g_return_if_fail(handler != NULL);
+ handler(channel, msg);
+}
+
+static void spice_channel_reset_capabilities(SpiceChannel *channel)
+{
+ SpiceChannelPrivate *c = channel->priv;
+ g_array_set_size(c->caps, 0);
+
+ if (SPICE_CHANNEL_GET_CLASS(channel)->channel_reset_capabilities) {
+ SPICE_CHANNEL_GET_CLASS(channel)->channel_reset_capabilities(channel);
+ }
+}
+
+static void spice_channel_send_migration_handshake(SpiceChannel *channel)
+{
+ SpiceChannelPrivate *c = channel->priv;
+
+ if (SPICE_CHANNEL_GET_CLASS(channel)->channel_send_migration_handshake) {
+ SPICE_CHANNEL_GET_CLASS(channel)->channel_send_migration_handshake(channel);
+ } else {
+ c->state = SPICE_CHANNEL_STATE_MIGRATING;
+ }
+}
+
+/**
+ * spice_channel_flush_async:
+ * @channel: a #SpiceChannel
+ * @cancellable: (allow-none): optional GCancellable object, %NULL to ignore
+ * @callback: (scope async): callback to call when the request is satisfied
+ * @user_data: (closure): the data to pass to callback function
+ *
+ * Forces an asynchronous write of all user-space buffered data for
+ * the given channel.
+ *
+ * When the operation is finished callback will be called. You can
+ * then call spice_channel_flush_finish() to get the result of the
+ * operation.
+ *
+ * Since: 0.15
+ **/
+void spice_channel_flush_async(SpiceChannel *self, GCancellable *cancellable,
+ GAsyncReadyCallback callback, gpointer user_data)
+{
+ GSimpleAsyncResult *simple;
+ SpiceChannelPrivate *c;
+ gboolean was_empty;
+
+ g_return_if_fail(SPICE_IS_CHANNEL(self));
+ c = self->priv;
+
+ if (c->state != SPICE_CHANNEL_STATE_READY) {
+ g_simple_async_report_error_in_idle(G_OBJECT(self), callback, user_data,
+ SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "The channel is not ready yet");
+ return;
+ }
+
+ simple = g_simple_async_result_new(G_OBJECT(self), callback, user_data,
+ spice_channel_flush_async);
+
+ STATIC_MUTEX_LOCK(c->xmit_queue_lock);
+ was_empty = g_queue_is_empty(&c->xmit_queue);
+ STATIC_MUTEX_UNLOCK(c->xmit_queue_lock);
+ if (was_empty) {
+ g_simple_async_result_set_op_res_gboolean(simple, TRUE);
+ g_simple_async_result_complete_in_idle(simple);
+ g_object_unref(simple);
+ return;
+ }
+
+ c->flushing = g_slist_append(c->flushing, simple);
+}
+
+/**
+ * spice_channel_flush_finish:
+ * @channel: a #SpiceChannel
+ * @result: a #GAsyncResult
+ * @error: a #GError location to store the error occurring, or %NULL
+ * to ignore.
+ *
+ * Finishes flushing a channel.
+ *
+ * Returns: %TRUE if flush operation succeeded, %FALSE otherwise.
+ * Since: 0.15
+ **/
+gboolean spice_channel_flush_finish(SpiceChannel *self, GAsyncResult *result,
+ GError **error)
+{
+ GSimpleAsyncResult *simple;
+
+ g_return_val_if_fail(SPICE_IS_CHANNEL(self), FALSE);
+ g_return_val_if_fail(result != NULL, FALSE);
+
+ simple = (GSimpleAsyncResult *)result;
+
+ if (g_simple_async_result_propagate_error(simple, error))
+ return -1;
+
+ g_return_val_if_fail(g_simple_async_result_is_valid(result, G_OBJECT(self),
+ spice_channel_flush_async), FALSE);
+
+ CHANNEL_DEBUG(self, "flushed finished!");
+ return g_simple_async_result_get_op_res_gboolean(simple);
+}
diff --git a/src/spice-channel.h b/src/spice-channel.h
new file mode 100644
index 0000000..7f132f6
--- /dev/null
+++ b/src/spice-channel.h
@@ -0,0 +1,131 @@
+/* -*- 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_CLIENT_CHANNEL_H__
+#define __SPICE_CLIENT_CHANNEL_H__
+
+G_BEGIN_DECLS
+
+#include <gio/gio.h>
+#include "spice-types.h"
+#include "spice-glib-enums.h"
+#include "spice-util.h"
+
+#define SPICE_TYPE_CHANNEL (spice_channel_get_type ())
+#define SPICE_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SPICE_TYPE_CHANNEL, SpiceChannel))
+#define SPICE_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SPICE_TYPE_CHANNEL, SpiceChannelClass))
+#define SPICE_IS_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SPICE_TYPE_CHANNEL))
+#define SPICE_IS_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SPICE_TYPE_CHANNEL))
+#define SPICE_CHANNEL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SPICE_TYPE_CHANNEL, SpiceChannelClass))
+
+typedef struct _SpiceMsgIn SpiceMsgIn;
+typedef struct _SpiceMsgOut SpiceMsgOut;
+
+/**
+ * SpiceChannelEvent:
+ * @SPICE_CHANNEL_NONE: no event, or ignored event
+ * @SPICE_CHANNEL_OPENED: connection is authentified and ready
+ * @SPICE_CHANNEL_CLOSED: connection is closed normally (sent if channel was ready)
+ * @SPICE_CHANNEL_ERROR_CONNECT: connection error
+ * @SPICE_CHANNEL_ERROR_TLS: SSL error
+ * @SPICE_CHANNEL_ERROR_LINK: error during link process
+ * @SPICE_CHANNEL_ERROR_AUTH: authentication error
+ * @SPICE_CHANNEL_ERROR_IO: IO error
+ *
+ * An event, emitted by #SpiceChannel::channel-event signal.
+ **/
+typedef enum
+{
+ SPICE_CHANNEL_NONE = 0,
+ SPICE_CHANNEL_OPENED = 10,
+ SPICE_CHANNEL_SWITCHING,
+ SPICE_CHANNEL_CLOSED,
+ SPICE_CHANNEL_ERROR_CONNECT = 20,
+ SPICE_CHANNEL_ERROR_TLS,
+ SPICE_CHANNEL_ERROR_LINK,
+ SPICE_CHANNEL_ERROR_AUTH,
+ SPICE_CHANNEL_ERROR_IO,
+} SpiceChannelEvent;
+
+struct _SpiceChannel
+{
+ GObject parent;
+ SpiceChannelPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+struct _SpiceChannelClass
+{
+ GObjectClass parent_class;
+
+ /*< public >*/
+ /* signals, main context */
+ void (*channel_event)(SpiceChannel *channel, SpiceChannelEvent event);
+ void (*open_fd)(SpiceChannel *channel, int with_tls);
+
+ /*< private >*/
+ /* virtual methods, coroutine context */
+ void (*handle_msg)(SpiceChannel *channel, SpiceMsgIn *msg);
+ void (*channel_up)(SpiceChannel *channel);
+ void (*iterate_write)(SpiceChannel *channel);
+ void (*iterate_read)(SpiceChannel *channel);
+
+ /*< private >*/
+ /* virtual method, any context */
+ gpointer deprecated;
+ void (*channel_reset)(SpiceChannel *channel, gboolean migrating);
+ void (*channel_reset_capabilities)(SpiceChannel *channel);
+
+ /*< private >*/
+ /* virtual methods, coroutine context */
+ void (*channel_send_migration_handshake)(SpiceChannel *channel);
+
+ GArray *handlers;
+ /*
+ * If adding fields to this struct, remove corresponding
+ * amount of padding to avoid changing overall struct size
+ */
+ gchar _spice_reserved[SPICE_RESERVED_PADDING - 2 * sizeof(void *)];
+};
+
+GType spice_channel_get_type(void);
+
+typedef void (*spice_msg_handler)(SpiceChannel *channel, SpiceMsgIn *in);
+
+SpiceChannel *spice_channel_new(SpiceSession *s, int type, int id);
+gboolean spice_channel_connect(SpiceChannel *channel);
+gboolean spice_channel_open_fd(SpiceChannel *channel, int fd);
+void spice_channel_disconnect(SpiceChannel *channel, SpiceChannelEvent reason);
+gboolean spice_channel_test_capability(SpiceChannel *channel, guint32 cap);
+gboolean spice_channel_test_common_capability(SpiceChannel *channel, guint32 cap);
+void spice_channel_flush_async(SpiceChannel *channel, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data);
+gboolean spice_channel_flush_finish(SpiceChannel *channel, GAsyncResult *result, GError **error);
+#ifndef SPICE_DISABLE_DEPRECATED
+SPICE_DEPRECATED
+void spice_channel_set_capability(SpiceChannel *channel, guint32 cap);
+SPICE_DEPRECATED
+void spice_channel_destroy(SpiceChannel *channel);
+#endif
+
+const gchar* spice_channel_type_to_string(gint type);
+gint spice_channel_string_to_type(const gchar *str);
+
+const GError* spice_channel_get_error(SpiceChannel *channel);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_CHANNEL_H__ */
diff --git a/src/spice-client-glib-usb-acl-helper.c b/src/spice-client-glib-usb-acl-helper.c
new file mode 100644
index 0000000..bc09776
--- /dev/null
+++ b/src/spice-client-glib-usb-acl-helper.c
@@ -0,0 +1,372 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2011,2012 Red Hat, Inc.
+ Copyright (C) 2009 Kay Sievers <kay.sievers@vrfy.org>
+
+ Red Hat Authors:
+ Hans de Goede <hdegoede@redhat.com>
+
+ 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, see <http://www.gnu.org/licenses/>.
+*/
+
+#include "config.h"
+
+#include <ctype.h>
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <gio/gunixinputstream.h>
+#include <polkit/polkit.h>
+#include <acl/libacl.h>
+
+#include "glib-compat.h"
+
+#define FATAL_ERROR(...) \
+ do { \
+ /* We print the error both to stdout, for the app invoking us and \
+ stderr for the end user */ \
+ fprintf(stdout, "Error " __VA_ARGS__); \
+ fprintf(stderr, "spice-client-glib-usb-helper: Error " __VA_ARGS__); \
+ exit_status = 1; \
+ cleanup(); \
+ } while (0)
+
+#define ERROR(...) \
+ do { \
+ fprintf(stdout, __VA_ARGS__); \
+ cleanup(); \
+ } while (0)
+
+enum state {
+ STATE_WAITING_FOR_BUS_N_DEV,
+ STATE_WAITING_FOR_POL_KIT,
+ STATE_WAITING_FOR_STDIN_EOF,
+};
+
+static enum state state = STATE_WAITING_FOR_BUS_N_DEV;
+static int exit_status;
+static int busnum, devnum;
+static char path[PATH_MAX];
+static GMainLoop *loop;
+static GDataInputStream *stdin_stream;
+static GCancellable *polkit_cancellable;
+static PolkitSubject *subject;
+static PolkitAuthority *authority;
+
+/*
+ * This function is a copy of the same function in udev, written by Kay
+ * Sievers, you can find it in udev in extras/udev-acl/udev-acl.c
+ */
+static int set_facl(const char* filename, uid_t uid, int add)
+{
+ int get;
+ acl_t acl;
+ acl_entry_t entry = NULL;
+ acl_entry_t e;
+ acl_permset_t permset;
+ int ret;
+
+ /* don't touch ACLs for root */
+ if (uid == 0)
+ return 0;
+
+ /* read current record */
+ acl = acl_get_file(filename, ACL_TYPE_ACCESS);
+ if (!acl)
+ return -1;
+
+ /* locate ACL_USER entry for uid */
+ get = acl_get_entry(acl, ACL_FIRST_ENTRY, &e);
+ while (get == 1) {
+ acl_tag_t t;
+
+ acl_get_tag_type(e, &t);
+ if (t == ACL_USER) {
+ uid_t *u;
+
+ u = (uid_t*)acl_get_qualifier(e);
+ if (u == NULL) {
+ ret = -1;
+ goto out;
+ }
+ if (*u == uid) {
+ entry = e;
+ acl_free(u);
+ break;
+ }
+ acl_free(u);
+ }
+
+ get = acl_get_entry(acl, ACL_NEXT_ENTRY, &e);
+ }
+
+ /* remove ACL_USER entry for uid */
+ if (!add) {
+ if (entry == NULL) {
+ ret = 0;
+ goto out;
+ }
+ acl_delete_entry(acl, entry);
+ goto update;
+ }
+
+ /* create ACL_USER entry for uid */
+ if (entry == NULL) {
+ ret = acl_create_entry(&acl, &entry);
+ if (ret != 0)
+ goto out;
+ acl_set_tag_type(entry, ACL_USER);
+ acl_set_qualifier(entry, &uid);
+ }
+
+ /* add permissions for uid */
+ acl_get_permset(entry, &permset);
+ acl_add_perm(permset, ACL_READ|ACL_WRITE);
+update:
+ /* update record */
+ acl_calc_mask(&acl);
+ ret = acl_set_file(filename, ACL_TYPE_ACCESS, acl);
+ if (ret != 0)
+ goto out;
+out:
+ acl_free(acl);
+ return ret;
+}
+
+static void cleanup(void)
+{
+ if (polkit_cancellable)
+ g_cancellable_cancel(polkit_cancellable);
+
+ if (state == STATE_WAITING_FOR_STDIN_EOF)
+ set_facl(path, getuid(), 0);
+
+ if (loop)
+ g_main_loop_quit(loop);
+}
+
+/* Not available in polkit < 0.101 */
+#if !HAVE_POLKIT_AUTHORIZATION_RESULT_GET_DISMISSED
+static gboolean
+polkit_authorization_result_get_dismissed(PolkitAuthorizationResult *result)
+{
+ gboolean ret;
+ PolkitDetails *details;
+
+ g_return_val_if_fail(POLKIT_IS_AUTHORIZATION_RESULT(result), FALSE);
+
+ ret = FALSE;
+ details = polkit_authorization_result_get_details(result);
+ if (details != NULL && polkit_details_lookup(details, "polkit.dismissed"))
+ ret = TRUE;
+
+ return ret;
+}
+#endif
+
+static void check_authorization_cb(PolkitAuthority *authority,
+ GAsyncResult *res, gpointer data)
+{
+ PolkitAuthorizationResult *result;
+ GError *err = NULL;
+ struct stat stat_buf;
+
+ g_clear_object(&polkit_cancellable);
+
+ result = polkit_authority_check_authorization_finish(authority, res, &err);
+ if (err) {
+ FATAL_ERROR("PoliciKit error: %s\n", err->message);
+ g_error_free(err);
+ return;
+ }
+
+ if (polkit_authorization_result_get_dismissed(result)) {
+ ERROR("CANCELED\n");
+ return;
+ }
+
+ if (!polkit_authorization_result_get_is_authorized(result)) {
+ ERROR("Not authorized\n");
+ return;
+ }
+
+ snprintf(path, PATH_MAX, "/dev/bus/usb/%03d/%03d", busnum, devnum);
+
+ if (stat(path, &stat_buf) != 0) {
+ FATAL_ERROR("statting %s: %s\n", path, strerror(errno));
+ return;
+ }
+ if (!S_ISCHR(stat_buf.st_mode)) {
+ FATAL_ERROR("%s is not a character device\n", path);
+ return;
+ }
+
+ if (set_facl(path, getuid(), 1)) {
+ FATAL_ERROR("setting facl: %s\n", strerror(errno));
+ return;
+ }
+
+ fprintf(stdout, "SUCCESS\n");
+ fflush(stdout);
+ state = STATE_WAITING_FOR_STDIN_EOF;
+}
+
+static void stdin_read_complete(GObject *src, GAsyncResult *res, gpointer data)
+{
+ char *s, *ep;
+ GError *err = NULL;
+ gsize len;
+
+ s = g_data_input_stream_read_line_finish(G_DATA_INPUT_STREAM(src), res,
+ &len, &err);
+ if (!s) {
+ if (err) {
+ FATAL_ERROR("Reading from stdin: %s\n", err->message);
+ g_error_free(err);
+ return;
+ }
+
+ switch (state) {
+ case STATE_WAITING_FOR_BUS_N_DEV:
+ FATAL_ERROR("EOF while waiting for bus and device num\n");
+ break;
+ case STATE_WAITING_FOR_POL_KIT:
+ ERROR("Cancelled while waiting for authorization\n");
+ break;
+ case STATE_WAITING_FOR_STDIN_EOF:
+ cleanup();
+ break;
+ }
+ return;
+ }
+
+ switch (state) {
+ case STATE_WAITING_FOR_BUS_N_DEV:
+ busnum = strtol(s, &ep, 10);
+ if (!isspace(*ep)) {
+ FATAL_ERROR("Invalid busnum / devnum: %s\n", s);
+ break;
+ }
+ devnum = strtol(ep, &ep, 10);
+ if (*ep != '\0') {
+ FATAL_ERROR("Invalid busnum / devnum: %s\n", s);
+ break;
+ }
+
+ /*
+ * The set_facl() call is a no-op for root, so no need to ask PolKit
+ * and then if ok call set_facl(), when called by a root process.
+ */
+ if (getuid() != 0) {
+ polkit_cancellable = g_cancellable_new();
+ polkit_authority_check_authorization(
+ authority, subject, "org.spice-space.lowlevelusbaccess", NULL,
+ POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION,
+ polkit_cancellable,
+ (GAsyncReadyCallback)check_authorization_cb, NULL);
+ state = STATE_WAITING_FOR_POL_KIT;
+ } else {
+ fprintf(stdout, "SUCCESS\n");
+ fflush(stdout);
+ state = STATE_WAITING_FOR_STDIN_EOF;
+ }
+
+ g_data_input_stream_read_line_async(stdin_stream, G_PRIORITY_DEFAULT,
+ NULL, stdin_read_complete, NULL);
+ break;
+ default:
+ FATAL_ERROR("Unexpected extra input in state %d: %s\n", state, s);
+ }
+ g_free(s);
+}
+
+/* Fix for polkit 0.97 and later */
+#if !HAVE_POLKIT_AUTHORITY_GET_SYNC
+static PolkitAuthority *
+polkit_authority_get_sync (GCancellable *cancellable, GError **error)
+{
+ PolkitAuthority *authority;
+
+ authority = polkit_authority_get ();
+ if (!authority)
+ g_set_error (error, 0, 0, "failed to get the PolicyKit authority");
+
+ return authority;
+}
+#endif
+
+#ifndef HAVE_CLEARENV
+extern char **environ;
+
+static int
+clearenv (void)
+{
+ if (environ != NULL)
+ environ[0] = NULL;
+ return 0;
+}
+#endif
+
+int main(void)
+{
+ pid_t parent_pid;
+ GInputStream *stdin_unix_stream;
+
+ /* Nuke the environment to get a well-known and sanitized
+ * environment to avoid attacks via e.g. the DBUS_SYSTEM_BUS_ADDRESS
+ * environment variable and similar.
+ */
+ if (clearenv () != 0) {
+ FATAL_ERROR("Error clearing environment: %s\n", g_strerror (errno));
+ return 1;
+ }
+
+#if !GLIB_CHECK_VERSION(2,36,0)
+ g_type_init();
+#endif
+
+ loop = g_main_loop_new(NULL, FALSE);
+
+ authority = polkit_authority_get_sync(NULL, NULL);
+ parent_pid = getppid ();
+ if (parent_pid == 1) {
+ FATAL_ERROR("Parent process was reaped by init(1)\n");
+ return 1;
+ }
+ /* Do what pkexec does */
+ subject = polkit_unix_process_new_for_owner(parent_pid, 0, getuid ());
+
+ stdin_unix_stream = g_unix_input_stream_new(STDIN_FILENO, 0);
+ stdin_stream = g_data_input_stream_new(stdin_unix_stream);
+ g_data_input_stream_set_newline_type(stdin_stream,
+ G_DATA_STREAM_NEWLINE_TYPE_LF);
+ g_clear_object(&stdin_unix_stream);
+ g_data_input_stream_read_line_async(stdin_stream, G_PRIORITY_DEFAULT, NULL,
+ stdin_read_complete, NULL);
+
+ g_main_loop_run(loop);
+
+ if (polkit_cancellable)
+ g_clear_object(&polkit_cancellable);
+ g_object_unref(stdin_stream);
+ g_object_unref(authority);
+ g_object_unref(subject);
+ g_main_loop_unref(loop);
+
+ return exit_status;
+}
diff --git a/src/spice-client-gtk-manual.defs b/src/spice-client-gtk-manual.defs
new file mode 100644
index 0000000..9631b74
--- /dev/null
+++ b/src/spice-client-gtk-manual.defs
@@ -0,0 +1,117 @@
+(define-method set_display
+ (of-object "SpiceMainChannel")
+ (c-name "spice_main_set_display")
+ (return-type "none")
+ (parameters
+ '("int" "id")
+ '("int" "x")
+ '("int" "y")
+ '("int" "width")
+ '("int" "height")
+ )
+)
+
+(define-method clipboard_grab
+ (of-object "SpiceMainChannel")
+ (c-name "spice_main_clipboard_grab")
+ (return-type "none")
+ (parameters
+ '("int*" "types")
+ '("int" "ntypes")
+ )
+)
+
+(define-method clipboard_release
+ (of-object "SpiceMainChannel")
+ (c-name "spice_main_clipboard_release")
+ (return-type "none")
+)
+
+(define-method motion
+ (of-object "SpiceInputsChannel")
+ (c-name "spice_inputs_motion")
+ (return-type "none")
+ (parameters
+ '("gint" "dx")
+ '("gint" "dy")
+ '("gint" "button_state")
+ )
+)
+
+(define-method position
+ (of-object "SpiceInputsChannel")
+ (c-name "spice_inputs_position")
+ (return-type "none")
+ (parameters
+ '("gint" "x")
+ '("gint" "y")
+ '("gint" "display")
+ '("gint" "button_state")
+ )
+)
+
+(define-method button_press
+ (of-object "SpiceInputsChannel")
+ (c-name "spice_inputs_button_press")
+ (return-type "none")
+ (parameters
+ '("gint" "button")
+ '("gint" "button_state")
+ )
+)
+
+(define-method button_release
+ (of-object "SpiceInputsChannel")
+ (c-name "spice_inputs_button_release")
+ (return-type "none")
+ (parameters
+ '("gint" "button")
+ '("gint" "button_state")
+ )
+)
+
+(define-method key_press
+ (of-object "SpiceInputsChannel")
+ (c-name "spice_inputs_key_press")
+ (return-type "none")
+ (parameters
+ '("guint" "keyval")
+ )
+)
+
+(define-method key_release
+ (of-object "SpiceInputsChannel")
+ (c-name "spice_inputs_key_release")
+ (return-type "none")
+ (parameters
+ '("guint" "keyval")
+ )
+)
+
+(define-method set_key_locks
+ (of-object "SpiceInputsChannel")
+ (c-name "spice_inputs_set_key_locks")
+ (return-type "none")
+ (parameters
+ '("guint" "locks")
+ )
+)
+
+(define-enum ClientError
+ (in-module "Spice")
+ (c-name "SpiceClientError")
+ (values
+ '("failed" "SPICE_CLIENT_ERROR_FAILED")
+ )
+)
+
+(define-function spice_audio_new
+ (c-name "spice_audio_new")
+ (is-constructor-of "SpiceAudio")
+ (return-type "SpiceAudio*")
+ (parameters
+ '("SpiceSession*" "session")
+ '("GMainContext*" "context")
+ '("const-char*" "name")
+ )
+)
diff --git a/src/spice-client-gtk-module.c b/src/spice-client-gtk-module.c
new file mode 100644
index 0000000..b82f1e3
--- /dev/null
+++ b/src/spice-client-gtk-module.c
@@ -0,0 +1,45 @@
+/* -*- 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 <pygobject.h>
+
+void spice_register_classes (PyObject *d);
+void spice_add_constants(PyObject *module, const gchar *strip_prefix);
+extern PyMethodDef spice_functions[];
+
+DL_EXPORT(void) initSpiceClientGtk(void)
+{
+ PyObject *m, *d;
+
+ init_pygobject();
+
+ m = Py_InitModule("SpiceClientGtk", spice_functions);
+ if (PyErr_Occurred())
+ Py_FatalError("can't init module");
+
+ d = PyModule_GetDict(m);
+ if (PyErr_Occurred())
+ Py_FatalError("can't get dict");
+
+ spice_register_classes(d);
+ spice_add_constants(m, "SPICE_");
+
+ if (PyErr_Occurred()) {
+ Py_FatalError("can't initialise module SpiceClientGtk");
+ }
+}
diff --git a/src/spice-client-gtk.override b/src/spice-client-gtk.override
new file mode 100644
index 0000000..41aeee3
--- /dev/null
+++ b/src/spice-client-gtk.override
@@ -0,0 +1,171 @@
+%%
+headers
+#include <Python.h>
+#include "pygobject.h"
+#include "spice-common.h"
+#include "spice-widget.h"
+#include "spice-gtk-session.h"
+#include "spice-audio.h"
+#include "usb-device-widget.h"
+%%
+modulename spice_client_gtk
+%%
+import gobject.GObject as PyGObject_Type
+import gtk.DrawingArea as PyGtkDrawingArea_Type
+import gtk.Widget as PyGtkWidget_Type
+import gtk.VBox as PyGtkVBox_Type
+%%
+ignore-glob
+ *_get_type
+%%
+%%
+override spice_display_send_keys kwargs
+static PyObject*
+_wrap_spice_display_send_keys(PyGObject *self,
+ PyObject *args, PyObject *kwargs)
+{
+ static char *kwlist[] = {"keys", "kind", NULL};
+ PyObject *keyList;
+ int kind = SPICE_DISPLAY_KEY_EVENT_CLICK;
+ int i, len;
+ guint *keys;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs,
+ "O|I:SpiceDisplay.send_keys", kwlist,
+ &keyList, &kind))
+ return NULL;
+
+ if (!PyList_Check(keyList))
+ return NULL;
+
+ len = PyList_Size(keyList);
+ keys = g_malloc0(sizeof(guint)*len);
+
+ for (i = 0 ; i < len ; i++) {
+ PyObject *val;
+ char *sym;
+ val = PyList_GetItem(keyList, i);
+ sym = PyString_AsString(val);
+ if (!sym) {
+ g_free(keys);
+ return NULL;
+ }
+ keys[i] = gdk_keyval_from_name(sym);
+ }
+
+ spice_display_send_keys(SPICE_DISPLAY(self->obj), keys, len, kind);
+ g_free(keys);
+
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+%%
+override spice_display_get_grab_keys kwargs
+static PyObject*
+_wrap_spice_display_get_grab_keys(PyGObject *self,
+ PyObject *args, PyObject *kwargs)
+{
+ SpiceGrabSequence *seq;
+ PyObject *keyList;
+ int i;
+
+ seq = spice_display_get_grab_keys(SPICE_DISPLAY(self->obj));
+
+ keyList = PyList_New(0);
+ for (i = 0 ; i < seq->nkeysyms ; i++)
+ PyList_Append(keyList, PyInt_FromLong(seq->keysyms[i]));
+
+ return keyList;
+}
+%%
+override spice_display_set_grab_keys kwargs
+static PyObject*
+_wrap_spice_display_set_grab_keys(PyGObject *self,
+ PyObject *args, PyObject *kwargs)
+{
+ static char *kwlist[] = {"keys", NULL};
+ PyObject *keyList;
+ int i;
+ guint nkeysyms;
+ guint *keysyms;
+ SpiceGrabSequence *seq;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs,
+ "O|I:SpiceDisplay.set_grab_keys", kwlist,
+ &keyList))
+ return NULL;
+
+ if (!PyList_Check(keyList))
+ return NULL;
+
+ nkeysyms = PyList_Size(keyList);
+ keysyms = g_new0(guint, nkeysyms);
+
+ for (i = 0 ; i < nkeysyms ; i++) {
+ PyObject *val = PyList_GetItem(keyList, i);
+ keysyms[i] = (guint)PyInt_AsLong(val);
+ }
+
+ seq = spice_grab_sequence_new(nkeysyms, keysyms);
+ g_free(keysyms);
+
+ spice_display_set_grab_keys(SPICE_DISPLAY(self->obj), seq);
+
+ spice_grab_sequence_free(seq);
+
+ Py_INCREF(Py_None);
+ return Py_None;
+}
+%%
+override spice_session_get_channels
+static PyObject*
+_wrap_spice_session_get_channels(PyGObject *self,
+ PyObject *args, PyObject *kwargs)
+{
+ PyObject *py_list;
+ GList *list, *tmp;
+ PyObject *chann;
+
+ list = spice_session_get_channels(SPICE_SESSION(self->obj));
+
+ if ((py_list = PyList_New(0)) == NULL) {
+ return NULL;
+ }
+ for (tmp = list; tmp != NULL; tmp = tmp->next) {
+ chann = pygobject_new(G_OBJECT(tmp->data));
+ if (chann == NULL) {
+ Py_DECREF(py_list);
+ return NULL;
+ }
+ PyList_Append(py_list, chann);
+ Py_DECREF(chann);
+ }
+ return py_list;
+}
+%%
+override spice_audio_new
+static int
+_wrap_spice_audio_new(PyGObject *self,
+ PyObject *args, PyObject *kwargs)
+{
+ static char *kwlist[] = {"session", "context", "name", NULL};
+ PyGObject *session = NULL;
+ PyObject *py_context = NULL;
+ char *name = NULL;
+
+ if (!PyArg_ParseTupleAndKeywords(args, kwargs,
+ "O!|Os:SpiceAudio", kwlist,
+ &PySpiceSession_Type, &session,
+ &py_context, &name))
+ return -1;
+
+ self->obj = (GObject *)spice_audio_new(SPICE_SESSION(session->obj), NULL, NULL);
+
+ if (!self->obj) {
+ PyErr_SetString(PyExc_RuntimeError, "could not create SpiceAudio object");
+ return -1;
+ }
+ pygobject_register_wrapper((PyObject *)self);
+ return 0;
+
+}
diff --git a/src/spice-client.c b/src/spice-client.c
new file mode 100644
index 0000000..5fd511f
--- /dev/null
+++ b/src/spice-client.c
@@ -0,0 +1,27 @@
+/* -*- 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.h>
+
+#include "spice-client.h"
+
+GQuark spice_client_error_quark(void)
+{
+ return g_quark_from_static_string("spice-client-error-quark");
+}
diff --git a/src/spice-client.h b/src/spice-client.h
new file mode 100644
index 0000000..e4e1763
--- /dev/null
+++ b/src/spice-client.h
@@ -0,0 +1,86 @@
+/* -*- 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_CLIENT_CLIENT_H__
+#define __SPICE_CLIENT_CLIENT_H__
+
+/* glib */
+#include <glib.h>
+#include <glib-object.h>
+
+/* spice-protocol */
+#include <spice/enums.h>
+#include <spice/protocol.h>
+
+/* spice/gtk */
+#include "spice-types.h"
+#include "spice-session.h"
+#include "spice-channel.h"
+#include "spice-option.h"
+#include "spice-uri.h"
+#include "spice-version.h"
+
+#include "channel-main.h"
+#include "channel-display.h"
+#include "channel-cursor.h"
+#include "channel-inputs.h"
+#include "channel-playback.h"
+#include "channel-record.h"
+#include "channel-smartcard.h"
+#include "channel-usbredir.h"
+#include "channel-port.h"
+#include "channel-webdav.h"
+
+#include "smartcard-manager.h"
+#include "usb-device-manager.h"
+#include "spice-audio.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_CLIENT_ERROR spice_client_error_quark()
+
+/**
+ * SpiceClientError:
+ * @SPICE_CLIENT_ERROR_FAILED: generic error code
+ * @SPICE_CLIENT_ERROR_USB_DEVICE_REJECTED: device redirection rejected by host
+ * @SPICE_CLIENT_ERROR_USB_DEVICE_LOST: device disconnected (fatal IO error)
+ * @SPICE_CLIENT_ERROR_AUTH_NEEDS_PASSWORD: password is required
+ * @SPICE_CLIENT_ERROR_AUTH_NEEDS_PASSWORD_AND_USERNAME: password and username are required
+ * @SPICE_CLIENT_ERROR_USB_SERVICE: USB service error
+ *
+ * Error codes returned by spice-client API.
+ */
+typedef enum
+{
+ SPICE_CLIENT_ERROR_FAILED,
+ SPICE_CLIENT_ERROR_USB_DEVICE_REJECTED,
+ SPICE_CLIENT_ERROR_USB_DEVICE_LOST,
+ SPICE_CLIENT_ERROR_AUTH_NEEDS_PASSWORD,
+ SPICE_CLIENT_ERROR_AUTH_NEEDS_PASSWORD_AND_USERNAME,
+ SPICE_CLIENT_ERROR_USB_SERVICE,
+} SpiceClientError;
+
+#ifndef SPICE_DISABLE_DEPRECATED
+#define SPICE_CLIENT_USB_DEVICE_REJECTED SPICE_CLIENT_ERROR_USB_DEVICE_REJECTED
+#define SPICE_CLIENT_USB_DEVICE_LOST SPICE_CLIENT_ERROR_USB_DEVICE_LOST
+#endif
+
+GQuark spice_client_error_quark(void);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_CLIENT_H__ */
diff --git a/src/spice-cmdline.c b/src/spice-cmdline.c
new file mode 100644
index 0000000..8619b57
--- /dev/null
+++ b/src/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.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/src/spice-cmdline.h b/src/spice-cmdline.h
new file mode 100644
index 0000000..11a8086
--- /dev/null
+++ b/src/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/src/spice-common.h b/src/spice-common.h
new file mode 100644
index 0000000..8554f4c
--- /dev/null
+++ b/src/spice-common.h
@@ -0,0 +1,36 @@
+/* -*- 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_COMMON_H_
+# define SPICE_COMMON_H_
+
+/* system */
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <inttypes.h>
+
+#include "common/mem.h"
+#include "common/messages.h"
+#include "common/marshaller.h"
+
+#include "spice-util.h"
+
+#endif // SPICE_COMMON_H_
diff --git a/src/spice-glib-sym-file b/src/spice-glib-sym-file
new file mode 100644
index 0000000..3a8da93
--- /dev/null
+++ b/src/spice-glib-sym-file
@@ -0,0 +1,111 @@
+spice_audio_get
+spice_audio_get_type
+spice_audio_new
+spice_channel_connect
+spice_channel_destroy
+spice_channel_disconnect
+spice_channel_event_get_type
+spice_channel_flush_async
+spice_channel_flush_finish
+spice_channel_get_error
+spice_channel_get_type
+spice_channel_new
+spice_channel_open_fd
+spice_channel_set_capability
+spice_channel_string_to_type
+spice_channel_test_capability
+spice_channel_test_common_capability
+spice_channel_type_to_string
+spice_client_error_quark
+spice_cursor_channel_get_type
+spice_display_channel_get_type
+spice_display_get_primary
+spice_get_option_group
+spice_g_signal_connect_object
+spice_inputs_button_press
+spice_inputs_button_release
+spice_inputs_channel_get_type
+spice_inputs_key_press
+spice_inputs_key_press_and_release
+spice_inputs_key_release
+spice_inputs_lock_get_type
+spice_inputs_motion
+spice_inputs_position
+spice_inputs_set_key_locks
+spice_main_agent_test_capability
+spice_main_channel_get_type
+spice_main_clipboard_grab
+spice_main_clipboard_notify
+spice_main_clipboard_release
+spice_main_clipboard_request
+spice_main_clipboard_selection_grab
+spice_main_clipboard_selection_notify
+spice_main_clipboard_selection_release
+spice_main_clipboard_selection_request
+spice_main_file_copy_async
+spice_main_file_copy_finish
+spice_main_send_monitor_config
+spice_main_set_display
+spice_main_set_display_enabled
+spice_main_update_display
+spice_playback_channel_get_type
+spice_playback_channel_set_delay
+spice_port_channel_get_type
+spice_port_event
+spice_port_write_async
+spice_port_write_finish
+spice_record_channel_get_type
+spice_record_send_data
+spice_session_connect
+spice_session_disconnect
+spice_session_get_channels
+spice_session_get_proxy_uri
+spice_session_get_read_only
+spice_session_get_type
+spice_session_has_channel_type
+spice_session_is_for_migration
+spice_session_migration_get_type
+spice_session_new
+spice_session_open_fd
+spice_session_verify_get_type
+spice_set_session_option
+spice_smartcard_channel_get_type
+spice_smartcard_manager_get
+spice_smartcard_manager_get_readers
+spice_smartcard_manager_get_type
+spice_smartcard_manager_insert_card
+spice_smartcard_manager_remove_card
+spice_smartcard_reader_get_type
+spice_smartcard_reader_insert_card
+spice_smartcard_reader_is_software
+spice_smartcard_reader_remove_card
+spice_uri_get_hostname
+spice_uri_get_password
+spice_uri_get_port
+spice_uri_get_scheme
+spice_uri_get_type
+spice_uri_get_user
+spice_uri_set_hostname
+spice_uri_set_password
+spice_uri_set_port
+spice_uri_set_scheme
+spice_uri_set_user
+spice_uri_to_string
+spice_usb_device_get_description
+spice_usb_device_get_libusb_device
+spice_usb_device_get_type
+spice_usb_device_manager_can_redirect_device
+spice_usb_device_manager_connect_device_async
+spice_usb_device_manager_connect_device_finish
+spice_usb_device_manager_disconnect_device
+spice_usb_device_manager_get
+spice_usb_device_manager_get_devices
+spice_usb_device_manager_get_devices_with_filter
+spice_usb_device_manager_get_type
+spice_usb_device_manager_is_device_connected
+spice_usbredir_channel_get_type
+spice_util_get_debug
+spice_util_get_version_string
+spice_util_set_debug
+spice_uuid_to_string
+spice_webdav_channel_get_type
diff --git a/src/spice-grabsequence.c b/src/spice-grabsequence.c
new file mode 100644
index 0000000..39adfb0
--- /dev/null
+++ b/src/spice-grabsequence.c
@@ -0,0 +1,163 @@
+/*
+ * GTK VNC Widget
+ *
+ * Copyright (C) 2010 Daniel P. Berrange <dan@berrange.com>
+ *
+ * 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.0 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <gdk/gdk.h>
+
+#include "spice-grabsequence.h"
+
+GType spice_grab_sequence_get_type(void)
+{
+ static GType grab_sequence_type = 0;
+ static volatile gsize grab_sequence_type_volatile;
+
+ if (g_once_init_enter(&grab_sequence_type_volatile)) {
+ grab_sequence_type = g_boxed_type_register_static
+ ("SpiceGrabSequence",
+ (GBoxedCopyFunc)spice_grab_sequence_copy,
+ (GBoxedFreeFunc)spice_grab_sequence_free);
+ g_once_init_leave(&grab_sequence_type_volatile,
+ grab_sequence_type);
+ }
+
+ return grab_sequence_type;
+}
+
+
+/**
+ * spice_grab_sequence_new:
+ * @nkeysyms: length of @keysyms
+ * @keysyms: (array length=nkeysyms): the keysym values
+ *
+ * Creates a new grab sequence from a list of keysym values
+ *
+ * Returns: (transfer full): a new grab sequence object
+ */
+SpiceGrabSequence *spice_grab_sequence_new(guint nkeysyms, guint *keysyms)
+{
+ SpiceGrabSequence *sequence;
+
+ sequence = g_slice_new0(SpiceGrabSequence);
+ sequence->nkeysyms = nkeysyms;
+ sequence->keysyms = g_new0(guint, nkeysyms);
+ memcpy(sequence->keysyms, keysyms, sizeof(guint)*nkeysyms);
+
+ return sequence;
+}
+
+
+/**
+ * spice_grab_sequence_new_from_string:
+ * @str: a string of '+' seperated key names (ex: "Control_L+Alt_L")
+ *
+ * Returns: a new #SpiceGrabSequence.
+ **/
+SpiceGrabSequence *spice_grab_sequence_new_from_string(const gchar *str)
+{
+ gchar **keysymstr;
+ int i;
+ SpiceGrabSequence *sequence;
+
+ sequence = g_slice_new0(SpiceGrabSequence);
+
+ keysymstr = g_strsplit(str, "+", 5);
+
+ sequence->nkeysyms = 0;
+ while (keysymstr[sequence->nkeysyms])
+ sequence->nkeysyms++;
+
+ sequence->keysyms = g_new0(guint, sequence->nkeysyms);
+ for (i = 0 ; i < sequence->nkeysyms ; i++) {
+ sequence->keysyms[i] =
+ (guint)gdk_keyval_from_name(keysymstr[i]);
+ if (sequence->keysyms[i] == 0) {
+ g_critical("Invalid key: %s", keysymstr[i]);
+ }
+ }
+ g_strfreev(keysymstr);
+
+ return sequence;
+
+}
+
+
+/**
+ * spice_grab_sequence_copy:
+ * @sequence: sequence to copy
+ *
+ * Returns: (transfer full): a copy of @sequence
+ **/
+SpiceGrabSequence *spice_grab_sequence_copy(SpiceGrabSequence *srcSequence)
+{
+ SpiceGrabSequence *sequence;
+
+ sequence = g_slice_dup(SpiceGrabSequence, srcSequence);
+ sequence->keysyms = g_new0(guint, srcSequence->nkeysyms);
+ memcpy(sequence->keysyms, srcSequence->keysyms,
+ sizeof(guint) * sequence->nkeysyms);
+
+ return sequence;
+}
+
+
+/**
+ * spice_grab_sequence_free:
+ * @sequence:
+ *
+ * Free @sequence.
+ **/
+void spice_grab_sequence_free(SpiceGrabSequence *sequence)
+{
+ g_free(sequence->keysyms);
+ g_slice_free(SpiceGrabSequence, sequence);
+}
+
+
+/**
+ * spice_grab_sequence_as_string:
+ * @sequence:
+ *
+ * Returns: (transfer full): a newly allocated string representing the key sequence
+ **/
+gchar *spice_grab_sequence_as_string(SpiceGrabSequence *sequence)
+{
+ GString *str = g_string_new("");
+ int i;
+
+ for (i = 0 ; i < sequence->nkeysyms ; i++) {
+ if (i > 0)
+ g_string_append_c(str, '+');
+ g_string_append(str, gdk_keyval_name(sequence->keysyms[i]));
+ }
+
+ return g_string_free(str, FALSE);
+
+}
+
+
+/*
+ * Local variables:
+ * c-indent-level: 8
+ * c-basic-offset: 8
+ * tab-width: 8
+ * End:
+ */
diff --git a/src/spice-grabsequence.h b/src/spice-grabsequence.h
new file mode 100644
index 0000000..fe58fc1
--- /dev/null
+++ b/src/spice-grabsequence.h
@@ -0,0 +1,61 @@
+/*
+ * GTK VNC Widget
+ *
+ * Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws>
+ * Copyright (C) 2009-2010 Daniel P. Berrange <dan@berrange.com>
+ *
+ * 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.0 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef SPICE_GRAB_SEQUENCE_H
+#define SPICE_GRAB_SEQUENCE_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_GRAB_SEQUENCE (spice_grab_sequence_get_type ())
+
+typedef struct _SpiceGrabSequence SpiceGrabSequence;
+
+struct _SpiceGrabSequence {
+ /*< private >*/
+ guint nkeysyms;
+ guint *keysyms;
+
+ /* Do not add fields to this struct */
+};
+
+GType spice_grab_sequence_get_type(void);
+
+SpiceGrabSequence *spice_grab_sequence_new(guint nkeysyms, guint *keysyms);
+SpiceGrabSequence *spice_grab_sequence_new_from_string(const gchar *str);
+SpiceGrabSequence *spice_grab_sequence_copy(SpiceGrabSequence *sequence);
+void spice_grab_sequence_free(SpiceGrabSequence *sequence);
+gchar *spice_grab_sequence_as_string(SpiceGrabSequence *sequence);
+
+
+G_END_DECLS
+
+#endif /* SPICE_GRAB_SEQUENCE_H */
+
+/*
+ * Local variables:
+ * c-indent-level: 8
+ * c-basic-offset: 8
+ * tab-width: 8
+ * End:
+ */
diff --git a/src/spice-gstaudio.c b/src/spice-gstaudio.c
new file mode 100644
index 0000000..1623421
--- /dev/null
+++ b/src/spice-gstaudio.c
@@ -0,0 +1,739 @@
+/* -*- 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 <gst/gst.h>
+#include <gst/app/gstappsrc.h>
+#include <gst/app/gstappsink.h>
+#include <gst/audio/streamvolume.h>
+
+#include "spice-gstaudio.h"
+#include "spice-common.h"
+#include "spice-session.h"
+#include "spice-util.h"
+
+#define SPICE_GSTAUDIO_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_GSTAUDIO, SpiceGstaudioPrivate))
+
+G_DEFINE_TYPE(SpiceGstaudio, spice_gstaudio, SPICE_TYPE_AUDIO)
+
+struct stream {
+ GstElement *pipe;
+ GstElement *src;
+ GstElement *sink;
+ guint rate;
+ guint channels;
+};
+
+struct _SpiceGstaudioPrivate {
+ SpiceChannel *pchannel;
+ SpiceChannel *rchannel;
+ struct stream playback;
+ struct stream record;
+ guint mmtime_id;
+};
+
+static gboolean connect_channel(SpiceAudio *audio, SpiceChannel *channel);
+static void channel_weak_notified(gpointer data, GObject *where_the_object_was);
+static void spice_gstaudio_get_playback_volume_info_async(SpiceAudio *audio,
+ GCancellable *cancellable, SpiceMainChannel *main_channel,
+ GAsyncReadyCallback callback, gpointer user_data);
+static gboolean spice_gstaudio_get_playback_volume_info_finish(SpiceAudio *audio,
+ GAsyncResult *res, gboolean *mute, guint8 *nchannels, guint16 **volume, GError **error);
+static void spice_gstaudio_get_record_volume_info_async(SpiceAudio *audio,
+ GCancellable *cancellable, SpiceMainChannel *main_channel,
+ GAsyncReadyCallback callback, gpointer user_data);
+static gboolean spice_gstaudio_get_record_volume_info_finish(SpiceAudio *audio,
+ GAsyncResult *res, gboolean *mute, guint8 *nchannels, guint16 **volume, GError **error);
+
+static void spice_gstaudio_finalize(GObject *obj)
+{
+ G_OBJECT_CLASS(spice_gstaudio_parent_class)->finalize(obj);
+}
+
+void stream_dispose(struct stream *s)
+{
+ if (s->pipe) {
+ gst_element_set_state(s->pipe, GST_STATE_NULL);
+ gst_object_unref(s->pipe);
+ s->pipe = NULL;
+ }
+
+ if (s->src) {
+ gst_object_unref(s->src);
+ s->src = NULL;
+ }
+
+ if (s->sink) {
+ gst_object_unref(s->sink);
+ s->sink = NULL;
+ }
+}
+
+static void spice_gstaudio_dispose(GObject *obj)
+{
+ SpiceGstaudio *gstaudio = SPICE_GSTAUDIO(obj);
+ SpiceGstaudioPrivate *p;
+ SPICE_DEBUG("%s", __FUNCTION__);
+ p = gstaudio->priv;
+
+ stream_dispose(&p->playback);
+ stream_dispose(&p->record);
+
+ if (p->pchannel)
+ g_object_weak_unref(G_OBJECT(p->pchannel), channel_weak_notified, gstaudio);
+ p->pchannel = NULL;
+
+ if (p->rchannel)
+ g_object_weak_unref(G_OBJECT(p->rchannel), channel_weak_notified, gstaudio);
+ p->rchannel = NULL;
+
+ if (G_OBJECT_CLASS(spice_gstaudio_parent_class)->dispose)
+ G_OBJECT_CLASS(spice_gstaudio_parent_class)->dispose(obj);
+}
+
+static void spice_gstaudio_init(SpiceGstaudio *gstaudio)
+{
+ gstaudio->priv = SPICE_GSTAUDIO_GET_PRIVATE(gstaudio);
+}
+
+static void spice_gstaudio_class_init(SpiceGstaudioClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ SpiceAudioClass *audio_class = SPICE_AUDIO_CLASS(klass);
+
+ audio_class->connect_channel = connect_channel;
+ audio_class->get_playback_volume_info_async = spice_gstaudio_get_playback_volume_info_async;
+ audio_class->get_playback_volume_info_finish = spice_gstaudio_get_playback_volume_info_finish;
+ audio_class->get_record_volume_info_async = spice_gstaudio_get_record_volume_info_async;
+ audio_class->get_record_volume_info_finish = spice_gstaudio_get_record_volume_info_finish;
+
+ gobject_class->finalize = spice_gstaudio_finalize;
+ gobject_class->dispose = spice_gstaudio_dispose;
+
+ g_type_class_add_private(klass, sizeof(SpiceGstaudioPrivate));
+}
+
+static GstFlowReturn record_new_buffer(GstAppSink *appsink, gpointer data)
+{
+ SpiceGstaudio *gstaudio = data;
+ SpiceGstaudioPrivate *p = gstaudio->priv;
+ GstMessage *msg;
+
+ g_return_val_if_fail(p != NULL, GST_FLOW_ERROR);
+
+ msg = gst_message_new_application(GST_OBJECT(p->record.pipe),
+ gst_structure_new_empty ("new-sample"));
+ gst_element_post_message(p->record.pipe, msg);
+ return GST_FLOW_OK;
+}
+
+static void record_stop(SpiceGstaudio *gstaudio)
+{
+ SpiceGstaudioPrivate *p = gstaudio->priv;
+
+ SPICE_DEBUG("%s", __FUNCTION__);
+ if (p->record.pipe)
+ gst_element_set_state(p->record.pipe, GST_STATE_READY);
+}
+
+static gboolean record_bus_cb(GstBus *bus, GstMessage *msg, gpointer data)
+{
+ SpiceGstaudio *gstaudio = data;
+ SpiceGstaudioPrivate *p = gstaudio->priv;
+
+ g_return_val_if_fail(p != NULL, FALSE);
+
+ switch (GST_MESSAGE_TYPE(msg)) {
+ case GST_MESSAGE_APPLICATION: {
+ GstSample *s;
+ GstBuffer *buffer;
+ GstMapInfo mapping;
+
+ s = gst_app_sink_pull_sample(GST_APP_SINK(p->record.sink));
+ if (!s) {
+ if (!gst_app_sink_is_eos(GST_APP_SINK(p->record.sink)))
+ g_warning("eos not reached, but can't pull new sample");
+ return TRUE;
+ }
+
+ buffer = gst_sample_get_buffer(s);
+ if (!buffer) {
+ if (!gst_app_sink_is_eos(GST_APP_SINK(p->record.sink)))
+ g_warning("eos not reached, but can't pull new buffer");
+ return TRUE;
+ }
+ if (!gst_buffer_map(buffer, &mapping, GST_MAP_READ)) {
+ return TRUE;
+ }
+
+ spice_record_send_data(SPICE_RECORD_CHANNEL(p->rchannel),
+ /* FIXME: server side doesn't care about ts?
+ what is the unit? ms apparently */
+ mapping.data, mapping.size, 0);
+ gst_buffer_unmap(buffer, &mapping);
+ gst_sample_unref(s);
+ break;
+ }
+ default:
+ break;
+ }
+
+ return TRUE;
+}
+
+static void record_start(SpiceRecordChannel *channel, gint format, gint channels,
+ gint frequency, gpointer data)
+{
+ SpiceGstaudio *gstaudio = data;
+ SpiceGstaudioPrivate *p = gstaudio->priv;
+
+ g_return_if_fail(p != NULL);
+ g_return_if_fail(format == SPICE_AUDIO_FMT_S16);
+
+ if (p->record.pipe &&
+ (p->record.rate != frequency ||
+ p->record.channels != channels)) {
+ record_stop(gstaudio);
+ gst_object_unref(p->record.pipe);
+ p->record.pipe = NULL;
+ }
+
+ if (!p->record.pipe) {
+ GError *error = NULL;
+ GstBus *bus;
+ gchar *audio_caps =
+ g_strdup_printf("audio/x-raw,format=\"S16LE\",channels=%d,rate=%d,"
+ "layout=interleaved", channels, frequency);
+ gchar *pipeline =
+ g_strdup_printf("autoaudiosrc name=audiosrc ! queue ! audioconvert ! audioresample ! "
+ "appsink caps=\"%s\" name=appsink", audio_caps);
+
+ p->record.pipe = gst_parse_launch(pipeline, &error);
+ if (error != NULL) {
+ g_warning("Failed to create pipeline: %s", error->message);
+ goto cleanup;
+ }
+
+ bus = gst_pipeline_get_bus(GST_PIPELINE(p->record.pipe));
+ gst_bus_add_watch(bus, record_bus_cb, data);
+ gst_object_unref(GST_OBJECT(bus));
+
+ p->record.src = gst_bin_get_by_name(GST_BIN(p->record.pipe), "audiosrc");
+ p->record.sink = gst_bin_get_by_name(GST_BIN(p->record.pipe), "appsink");
+ p->record.rate = frequency;
+ p->record.channels = channels;
+
+ gst_app_sink_set_emit_signals(GST_APP_SINK(p->record.sink), TRUE);
+ spice_g_signal_connect_object(p->record.sink, "new-sample",
+ G_CALLBACK(record_new_buffer), gstaudio, 0);
+
+cleanup:
+ if (error != NULL && p->record.pipe != NULL) {
+ gst_object_unref(p->record.pipe);
+ p->record.pipe = NULL;
+ }
+ g_clear_error(&error);
+ g_free(audio_caps);
+ g_free(pipeline);
+ }
+
+ if (p->record.pipe)
+ gst_element_set_state(p->record.pipe, GST_STATE_PLAYING);
+}
+
+static void playback_stop(SpiceGstaudio *gstaudio)
+{
+ SpiceGstaudioPrivate *p = gstaudio->priv;
+
+ if (p->playback.pipe)
+ gst_element_set_state(p->playback.pipe, GST_STATE_READY);
+ if (p->mmtime_id != 0) {
+ g_source_remove(p->mmtime_id);
+ p->mmtime_id = 0;
+ }
+}
+
+static gboolean update_mmtime_timeout_cb(gpointer data)
+{
+ SpiceGstaudio *gstaudio = data;
+ SpiceGstaudioPrivate *p = gstaudio->priv;
+ GstQuery *q;
+
+ q = gst_query_new_latency();
+ if (gst_element_query(p->playback.pipe, q)) {
+ gboolean live;
+ GstClockTime minlat, maxlat;
+ gst_query_parse_latency(q, &live, &minlat, &maxlat);
+ SPICE_DEBUG("got min latency %" GST_TIME_FORMAT ", max latency %"
+ GST_TIME_FORMAT ", live %d", GST_TIME_ARGS (minlat),
+ GST_TIME_ARGS (maxlat), live);
+ spice_playback_channel_set_delay(SPICE_PLAYBACK_CHANNEL(p->pchannel), GST_TIME_AS_MSECONDS(minlat));
+ }
+ gst_query_unref (q);
+
+ return TRUE;
+}
+
+static void playback_start(SpicePlaybackChannel *channel, gint format, gint channels,
+ gint frequency, gpointer data)
+{
+ SpiceGstaudio *gstaudio = data;
+ SpiceGstaudioPrivate *p = gstaudio->priv;
+
+ g_return_if_fail(p != NULL);
+ g_return_if_fail(format == SPICE_AUDIO_FMT_S16);
+
+ if (p->playback.pipe &&
+ (p->playback.rate != frequency ||
+ p->playback.channels != channels)) {
+ playback_stop(gstaudio);
+ gst_object_unref(p->playback.pipe);
+ p->playback.pipe = NULL;
+ }
+
+ if (!p->playback.pipe) {
+ GError *error = NULL;
+ gchar *audio_caps =
+ g_strdup_printf("audio/x-raw,format=\"S16LE\",channels=%d,rate=%d,"
+ "layout=interleaved", channels, frequency);
+ gchar *pipeline = g_strdup (g_getenv("SPICE_GST_AUDIOSINK"));
+ if (pipeline == NULL)
+ pipeline = g_strdup_printf("appsrc is-live=1 do-timestamp=0 caps=\"%s\" name=\"appsrc\" ! queue ! "
+ "audioconvert ! audioresample ! autoaudiosink name=\"audiosink\"", audio_caps);
+ SPICE_DEBUG("audio pipeline: %s", pipeline);
+ p->playback.pipe = gst_parse_launch(pipeline, &error);
+ if (error != NULL) {
+ g_warning("Failed to create pipeline: %s", error->message);
+ goto cleanup;
+ }
+ p->playback.src = gst_bin_get_by_name(GST_BIN(p->playback.pipe), "appsrc");
+ p->playback.sink = gst_bin_get_by_name(GST_BIN(p->playback.pipe), "audiosink");
+ p->playback.rate = frequency;
+ p->playback.channels = channels;
+
+cleanup:
+ if (error != NULL && p->playback.pipe != NULL) {
+ gst_object_unref(p->playback.pipe);
+ p->playback.pipe = NULL;
+ }
+ g_clear_error(&error);
+ g_free(audio_caps);
+ g_free(pipeline);
+ }
+
+ if (p->playback.pipe)
+ gst_element_set_state(p->playback.pipe, GST_STATE_PLAYING);
+
+ if (p->mmtime_id == 0) {
+ update_mmtime_timeout_cb(gstaudio);
+ p->mmtime_id = g_timeout_add_seconds(1, update_mmtime_timeout_cb, gstaudio);
+ }
+}
+
+static void playback_data(SpicePlaybackChannel *channel,
+ gpointer *audio, gint size,
+ gpointer data)
+{
+ SpiceGstaudio *gstaudio = data;
+ SpiceGstaudioPrivate *p = gstaudio->priv;
+ GstBuffer *buf;
+
+ g_return_if_fail(p != NULL);
+
+ audio = g_memdup(audio, size); /* TODO: try to avoid memory copy */
+ buf = gst_buffer_new_wrapped(audio, size);
+ gst_app_src_push_buffer(GST_APP_SRC(p->playback.src), buf);
+}
+
+#define VOLUME_NORMAL 65535
+
+static void playback_volume_changed(GObject *object, GParamSpec *pspec, gpointer data)
+{
+ SpiceGstaudio *gstaudio = data;
+ GstElement *e;
+ guint16 *volume;
+ guint nchannels;
+ SpiceGstaudioPrivate *p = gstaudio->priv;
+ gdouble vol;
+
+ if (!p->playback.sink)
+ return;
+
+ g_object_get(object,
+ "volume", &volume,
+ "nchannels", &nchannels,
+ NULL);
+
+ g_return_if_fail(nchannels > 0);
+
+ vol = 1.0 * volume[0] / VOLUME_NORMAL;
+ SPICE_DEBUG("playback volume changed to %u (%0.2f)", volume[0], 100*vol);
+
+ if (GST_IS_BIN(p->playback.sink))
+ e = gst_bin_get_by_interface(GST_BIN(p->playback.sink), GST_TYPE_STREAM_VOLUME);
+ else
+ e = g_object_ref(p->playback.sink);
+
+ if (GST_IS_STREAM_VOLUME(e))
+ gst_stream_volume_set_volume(GST_STREAM_VOLUME(e), GST_STREAM_VOLUME_FORMAT_CUBIC, vol);
+ else
+ g_object_set(e, "volume", vol, NULL);
+
+ g_object_unref(e);
+}
+
+static void playback_mute_changed(GObject *object, GParamSpec *pspec, gpointer data)
+{
+ SpiceGstaudio *gstaudio = data;
+ SpiceGstaudioPrivate *p = gstaudio->priv;
+ GstElement *e;
+ gboolean mute;
+
+ if (!p->playback.sink)
+ return;
+
+ g_object_get(object, "mute", &mute, NULL);
+ SPICE_DEBUG("playback mute changed to %u", mute);
+
+ if (GST_IS_BIN(p->playback.sink))
+ e = gst_bin_get_by_interface(GST_BIN(p->playback.sink), GST_TYPE_STREAM_VOLUME);
+ else
+ e = g_object_ref(p->playback.sink);
+
+ if (GST_IS_STREAM_VOLUME(e))
+ gst_stream_volume_set_mute(GST_STREAM_VOLUME(e), mute);
+
+ g_object_unref(e);
+}
+
+static void record_volume_changed(GObject *object, GParamSpec *pspec, gpointer data)
+{
+ SpiceGstaudio *gstaudio = data;
+ SpiceGstaudioPrivate *p = gstaudio->priv;
+ GstElement *e;
+ guint16 *volume;
+ guint nchannels;
+ gdouble vol;
+
+ if (!p->record.src)
+ return;
+
+ g_object_get(object,
+ "volume", &volume,
+ "nchannels", &nchannels,
+ NULL);
+
+ g_return_if_fail(nchannels > 0);
+
+ vol = 1.0 * volume[0] / VOLUME_NORMAL;
+ SPICE_DEBUG("record volume changed to %u (%0.2f)", volume[0], 100*vol);
+
+ /* TODO directsoundsrc doesn't support IDirectSoundBuffer_SetVolume */
+ /* TODO pulsesrc doesn't support volume property, it's all coming! */
+
+ if (GST_IS_BIN(p->record.src))
+ e = gst_bin_get_by_interface(GST_BIN(p->record.src), GST_TYPE_STREAM_VOLUME);
+ else
+ e = g_object_ref(p->record.src);
+
+ if (GST_IS_STREAM_VOLUME(e))
+ gst_stream_volume_set_volume(GST_STREAM_VOLUME(e), GST_STREAM_VOLUME_FORMAT_CUBIC, vol);
+ else
+ g_warning("gst lacks volume capabilities on src (TODO)");
+
+ g_object_unref(e);
+}
+
+static void record_mute_changed(GObject *object, GParamSpec *pspec, gpointer data)
+{
+ SpiceGstaudio *gstaudio = data;
+ SpiceGstaudioPrivate *p = gstaudio->priv;
+ GstElement *e;
+ gboolean mute;
+
+ if (!p->record.src)
+ return;
+
+ g_object_get(object, "mute", &mute, NULL);
+ SPICE_DEBUG("record mute changed to %u", mute);
+
+ if (GST_IS_BIN(p->record.src))
+ e = gst_bin_get_by_interface(GST_BIN(p->record.src), GST_TYPE_STREAM_VOLUME);
+ else
+ e = g_object_ref(p->record.src);
+
+ if (GST_IS_STREAM_VOLUME (e))
+ gst_stream_volume_set_mute(GST_STREAM_VOLUME(e), mute);
+ else
+ g_warning("gst lacks mute capabilities on src: %d (TODO)", mute);
+
+ g_object_unref(e);
+}
+
+static void
+channel_weak_notified(gpointer data,
+ GObject *where_the_object_was)
+{
+ SpiceGstaudio *gstaudio = SPICE_GSTAUDIO(data);
+ SpiceGstaudioPrivate *p = gstaudio->priv;
+
+ if (where_the_object_was == (GObject *)p->pchannel) {
+ SPICE_DEBUG("playback closed");
+ playback_stop(gstaudio);
+ p->pchannel = NULL;
+ } else if (where_the_object_was == (GObject *)p->rchannel) {
+ SPICE_DEBUG("record closed");
+ record_stop(gstaudio);
+ p->rchannel = NULL;
+ }
+}
+
+static gboolean connect_channel(SpiceAudio *audio, SpiceChannel *channel)
+{
+ SpiceGstaudio *gstaudio = SPICE_GSTAUDIO(audio);
+ SpiceGstaudioPrivate *p = gstaudio->priv;
+
+ if (SPICE_IS_PLAYBACK_CHANNEL(channel)) {
+ g_return_val_if_fail(p->pchannel == NULL, FALSE);
+
+ p->pchannel = channel;
+ g_object_weak_ref(G_OBJECT(p->pchannel), channel_weak_notified, audio);
+ spice_g_signal_connect_object(channel, "playback-start",
+ G_CALLBACK(playback_start), gstaudio, 0);
+ spice_g_signal_connect_object(channel, "playback-data",
+ G_CALLBACK(playback_data), gstaudio, 0);
+ spice_g_signal_connect_object(channel, "playback-stop",
+ G_CALLBACK(playback_stop), gstaudio, G_CONNECT_SWAPPED);
+ spice_g_signal_connect_object(channel, "notify::volume",
+ G_CALLBACK(playback_volume_changed), gstaudio, 0);
+ spice_g_signal_connect_object(channel, "notify::mute",
+ G_CALLBACK(playback_mute_changed), gstaudio, 0);
+
+ return TRUE;
+ }
+
+ if (SPICE_IS_RECORD_CHANNEL(channel)) {
+ g_return_val_if_fail(p->rchannel == NULL, FALSE);
+
+ p->rchannel = channel;
+ g_object_weak_ref(G_OBJECT(p->rchannel), channel_weak_notified, audio);
+ spice_g_signal_connect_object(channel, "record-start",
+ G_CALLBACK(record_start), gstaudio, 0);
+ spice_g_signal_connect_object(channel, "record-stop",
+ G_CALLBACK(record_stop), gstaudio, G_CONNECT_SWAPPED);
+ spice_g_signal_connect_object(channel, "notify::volume",
+ G_CALLBACK(record_volume_changed), gstaudio, 0);
+ spice_g_signal_connect_object(channel, "notify::mute",
+ G_CALLBACK(record_mute_changed), gstaudio, 0);
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+SpiceGstaudio *spice_gstaudio_new(SpiceSession *session, GMainContext *context,
+ const char *name)
+{
+ SpiceGstaudio *gstaudio;
+
+ gst_init(NULL, NULL);
+ gstaudio = g_object_new(SPICE_TYPE_GSTAUDIO,
+ "session", session,
+ "main-context", context,
+ NULL);
+
+ return gstaudio;
+}
+
+static void spice_gstaudio_get_playback_volume_info_async(SpiceAudio *audio,
+ GCancellable *cancellable,
+ SpiceMainChannel *main_channel,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GSimpleAsyncResult *simple;
+
+ simple = g_simple_async_result_new(G_OBJECT(audio),
+ callback,
+ user_data,
+ spice_gstaudio_get_playback_volume_info_async);
+ g_simple_async_result_set_check_cancellable (simple, cancellable);
+
+ g_simple_async_result_set_op_res_gboolean(simple, TRUE);
+ g_simple_async_result_complete_in_idle(simple);
+}
+
+static gboolean spice_gstaudio_get_playback_volume_info_finish(SpiceAudio *audio,
+ GAsyncResult *res,
+ gboolean *mute,
+ guint8 *nchannels,
+ guint16 **volume,
+ GError **error)
+{
+ SpiceGstaudioPrivate *p = SPICE_GSTAUDIO(audio)->priv;
+ GstElement *e;
+ gboolean lmute;
+ gdouble vol;
+ gboolean fake_channel = FALSE;
+ GSimpleAsyncResult *simple = (GSimpleAsyncResult *) res;
+
+ g_return_val_if_fail(g_simple_async_result_is_valid(res,
+ G_OBJECT(audio), spice_gstaudio_get_playback_volume_info_async), FALSE);
+
+ if (g_simple_async_result_propagate_error(simple, error)) {
+ return FALSE;
+ }
+
+ if (p->playback.sink == NULL || p->playback.channels == 0) {
+ SPICE_DEBUG("PlaybackChannel not created yet, force start");
+ /* In order to get system volume, we start the pipeline */
+ playback_start(NULL, SPICE_AUDIO_FMT_S16, 2, 48000, audio);
+ fake_channel = TRUE;
+ }
+
+ if (GST_IS_BIN(p->playback.sink))
+ e = gst_bin_get_by_interface(GST_BIN(p->playback.sink), GST_TYPE_STREAM_VOLUME);
+ else
+ e = g_object_ref(p->playback.sink);
+
+ if (GST_IS_STREAM_VOLUME(e)) {
+ vol = gst_stream_volume_get_volume(GST_STREAM_VOLUME(e), GST_STREAM_VOLUME_FORMAT_CUBIC);
+ lmute = gst_stream_volume_get_mute(GST_STREAM_VOLUME(e));
+ } else {
+ g_object_get(e,
+ "volume", &vol,
+ "mute", &lmute, NULL);
+ }
+ g_object_unref(e);
+
+ if (fake_channel) {
+ SPICE_DEBUG("Stop faked PlaybackChannel");
+ playback_stop(SPICE_GSTAUDIO(audio));
+ }
+
+ if (mute != NULL) {
+ *mute = lmute;
+ }
+
+ if (nchannels != NULL) {
+ *nchannels = p->playback.channels;
+ }
+
+ if (volume != NULL) {
+ gint i;
+ *volume = g_new(guint16, p->playback.channels);
+ for (i = 0; i < p->playback.channels; i++) {
+ (*volume)[i] = (guint16) (vol * VOLUME_NORMAL);
+ SPICE_DEBUG("(playback) volume at %d is %u (%0.2f%%)", i, (*volume)[i], 100*vol);
+ }
+ }
+
+ return g_simple_async_result_get_op_res_gboolean(simple);
+}
+
+static void spice_gstaudio_get_record_volume_info_async(SpiceAudio *audio,
+ GCancellable *cancellable,
+ SpiceMainChannel *main_channel,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GSimpleAsyncResult *simple;
+
+ simple = g_simple_async_result_new(G_OBJECT(audio),
+ callback,
+ user_data,
+ spice_gstaudio_get_record_volume_info_async);
+ g_simple_async_result_set_check_cancellable (simple, cancellable);
+
+ g_simple_async_result_set_op_res_gboolean(simple, TRUE);
+ g_simple_async_result_complete_in_idle(simple);
+}
+
+static gboolean spice_gstaudio_get_record_volume_info_finish(SpiceAudio *audio,
+ GAsyncResult *res,
+ gboolean *mute,
+ guint8 *nchannels,
+ guint16 **volume,
+ GError **error)
+{
+ SpiceGstaudioPrivate *p = SPICE_GSTAUDIO(audio)->priv;
+ GstElement *e;
+ gboolean lmute;
+ gdouble vol;
+ gboolean fake_channel = FALSE;
+ GSimpleAsyncResult *simple = (GSimpleAsyncResult *) res;
+
+ g_return_val_if_fail(g_simple_async_result_is_valid(res,
+ G_OBJECT(audio), spice_gstaudio_get_record_volume_info_async), FALSE);
+
+ if (g_simple_async_result_propagate_error(simple, error)) {
+ /* set out args that should have new alloc'ed memory to NULL */
+ if (volume != NULL) {
+ *volume = NULL;
+ }
+ return FALSE;
+ }
+
+ if (p->record.src == NULL || p->record.channels == 0) {
+ SPICE_DEBUG("RecordChannel not created yet, force start");
+ /* In order to get system volume, we start the pipeline */
+ record_start(NULL, SPICE_AUDIO_FMT_S16, 2, 48000, audio);
+ fake_channel = TRUE;
+ }
+
+ if (GST_IS_BIN(p->record.src))
+ e = gst_bin_get_by_interface(GST_BIN(p->record.src), GST_TYPE_STREAM_VOLUME);
+ else
+ e = g_object_ref(p->record.src);
+
+ if (GST_IS_STREAM_VOLUME(e)) {
+ vol = gst_stream_volume_get_volume(GST_STREAM_VOLUME(e), GST_STREAM_VOLUME_FORMAT_CUBIC);
+ lmute = gst_stream_volume_get_mute(GST_STREAM_VOLUME(e));
+ } else {
+ g_object_get(e,
+ "volume", &vol,
+ "mute", &lmute, NULL);
+ }
+ g_object_unref(e);
+
+ if (fake_channel) {
+ SPICE_DEBUG("Stop faked RecordChannel");
+ record_stop(SPICE_GSTAUDIO(audio));
+ }
+
+ if (mute != NULL) {
+ *mute = lmute;
+ }
+
+ if (nchannels != NULL) {
+ *nchannels = p->record.channels;
+ }
+
+ if (volume != NULL) {
+ gint i;
+ *volume = g_new(guint16, p->record.channels);
+ for (i = 0; i < p->record.channels; i++) {
+ (*volume)[i] = (guint16) (vol * VOLUME_NORMAL);
+ SPICE_DEBUG("(record) volume at %d is %u (%0.2f%%)", i, (*volume)[i], 100*vol);
+ }
+ }
+
+ return g_simple_async_result_get_op_res_gboolean(simple);
+}
diff --git a/src/spice-gstaudio.h b/src/spice-gstaudio.h
new file mode 100644
index 0000000..b605f1c
--- /dev/null
+++ b/src/spice-gstaudio.h
@@ -0,0 +1,56 @@
+/* -*- 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_CLIENT_GSTAUDIO_H__
+#define __SPICE_CLIENT_GSTAUDIO_H__
+
+#include "spice-client.h"
+#include "spice-audio.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_GSTAUDIO (spice_gstaudio_get_type())
+#define SPICE_GSTAUDIO(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_GSTAUDIO, SpiceGstaudio))
+#define SPICE_GSTAUDIO_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_GSTAUDIO, SpiceGstaudioClass))
+#define SPICE_IS_GSTAUDIO(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_GSTAUDIO))
+#define SPICE_IS_GSTAUDIO_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_GSTAUDIO))
+#define SPICE_GSTAUDIO_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_GSTAUDIO, SpiceGstaudioClass))
+
+
+typedef struct _SpiceGstaudio SpiceGstaudio;
+typedef struct _SpiceGstaudioClass SpiceGstaudioClass;
+typedef struct _SpiceGstaudioPrivate SpiceGstaudioPrivate;
+
+struct _SpiceGstaudio {
+ SpiceAudio parent;
+ SpiceGstaudioPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+struct _SpiceGstaudioClass {
+ SpiceAudioClass parent_class;
+ /* Do not add fields to this struct */
+};
+
+GType spice_gstaudio_get_type(void);
+
+SpiceGstaudio *spice_gstaudio_new(SpiceSession *session,
+ GMainContext *context, const char *name);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_GSTAUDIO_H__ */
diff --git a/src/spice-gtk-session-priv.h b/src/spice-gtk-session-priv.h
new file mode 100644
index 0000000..91304b2
--- /dev/null
+++ b/src/spice-gtk-session-priv.h
@@ -0,0 +1,34 @@
+/* -*- 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/>.
+*/
+#ifndef __SPICE_CLIENT_GTK_SESSION_PRIV_H__
+#define __SPICE_CLIENT_GTK_SESSION_PRIV_H__
+
+#include "spice-gtk-session.h"
+
+G_BEGIN_DECLS
+
+void spice_gtk_session_request_auto_usbredir(SpiceGtkSession *self,
+ gboolean state);
+gboolean spice_gtk_session_get_read_only(SpiceGtkSession *self);
+void spice_gtk_session_sync_keyboard_modifiers(SpiceGtkSession *self);
+void spice_gtk_session_set_pointer_grabbed(SpiceGtkSession *self, gboolean grabbed);
+gboolean spice_gtk_session_get_pointer_grabbed(SpiceGtkSession *self);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_GTK_SESSION_PRIV_H__ */
diff --git a/src/spice-gtk-session.c b/src/spice-gtk-session.c
new file mode 100644
index 0000000..0937434
--- /dev/null
+++ b/src/spice-gtk-session.c
@@ -0,0 +1,1229 @@
+/* -*- 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>
+
+#if HAVE_X11_XKBLIB_H
+#include <X11/XKBlib.h>
+#include <gdk/gdkx.h>
+#endif
+#ifdef GDK_WINDOWING_X11
+#include <X11/Xlib.h>
+#include <gdk/gdkx.h>
+#endif
+#ifdef G_OS_WIN32
+#include <windows.h>
+#include <gdk/gdkwin32.h>
+#ifndef MAPVK_VK_TO_VSC /* may be undefined in older mingw-headers */
+#define MAPVK_VK_TO_VSC 0
+#endif
+#endif
+
+#include <gtk/gtk.h>
+#include <spice/vd_agent.h>
+#include "desktop-integration.h"
+#include "gtk-compat.h"
+#include "spice-common.h"
+#include "spice-gtk-session.h"
+#include "spice-gtk-session-priv.h"
+#include "spice-session-priv.h"
+#include "spice-util-priv.h"
+#include "spice-channel-priv.h"
+
+#define CLIPBOARD_LAST (VD_AGENT_CLIPBOARD_SELECTION_SECONDARY + 1)
+
+struct _SpiceGtkSessionPrivate {
+ SpiceSession *session;
+ /* Clipboard related */
+ gboolean auto_clipboard_enable;
+ SpiceMainChannel *main;
+ GtkClipboard *clipboard;
+ GtkClipboard *clipboard_primary;
+ GtkTargetEntry *clip_targets[CLIPBOARD_LAST];
+ guint nclip_targets[CLIPBOARD_LAST];
+ gboolean clip_hasdata[CLIPBOARD_LAST];
+ gboolean clip_grabbed[CLIPBOARD_LAST];
+ gboolean clipboard_by_guest[CLIPBOARD_LAST];
+ /* auto-usbredir related */
+ gboolean auto_usbredir_enable;
+ int auto_usbredir_reqs;
+ gboolean pointer_grabbed;
+};
+
+/**
+ * SECTION:spice-gtk-session
+ * @short_description: handles GTK connection details
+ * @title: Spice GTK Session
+ * @section_id:
+ * @see_also: #SpiceSession, and the GTK widget #SpiceDisplay
+ * @stability: Stable
+ * @include: spice-gtk-session.h
+ *
+ * The #SpiceGtkSession class is the spice-client-gtk counter part of
+ * #SpiceSession. It contains functionality which should be handled per
+ * session rather then per #SpiceDisplay (one session can have multiple
+ * displays), but which cannot live in #SpiceSession as it depends on
+ * GTK. For example the clipboard functionality.
+ *
+ * There should always be a 1:1 relation between #SpiceGtkSession objects
+ * and #SpiceSession objects. Therefor there is no spice_gtk_session_new,
+ * instead there is spice_gtk_session_get() which ensures this 1:1 relation.
+ *
+ * Client and guest clipboards will be shared automatically if
+ * #SpiceGtkSession:auto-clipboard is set to #TRUE. Alternatively, you
+ * can send / receive clipboard data from client to guest with
+ * spice_gtk_session_copy_to_guest() / spice_gtk_session_paste_from_guest().
+ */
+
+/* ------------------------------------------------------------------ */
+/* Prototypes for private functions */
+static void clipboard_owner_change(GtkClipboard *clipboard,
+ GdkEventOwnerChange *event,
+ gpointer user_data);
+static void channel_new(SpiceSession *session, SpiceChannel *channel,
+ gpointer user_data);
+static void channel_destroy(SpiceSession *session, SpiceChannel *channel,
+ gpointer user_data);
+static gboolean read_only(SpiceGtkSession *self);
+
+/* ------------------------------------------------------------------ */
+/* gobject glue */
+
+#define SPICE_GTK_SESSION_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE ((obj), SPICE_TYPE_GTK_SESSION, SpiceGtkSessionPrivate))
+
+G_DEFINE_TYPE (SpiceGtkSession, spice_gtk_session, G_TYPE_OBJECT);
+
+/* Properties */
+enum {
+ PROP_0,
+ PROP_SESSION,
+ PROP_AUTO_CLIPBOARD,
+ PROP_AUTO_USBREDIR,
+ PROP_POINTER_GRABBED,
+};
+
+static guint32 get_keyboard_lock_modifiers(void)
+{
+ guint32 modifiers = 0;
+#if GTK_CHECK_VERSION(3,18,0)
+ GdkKeymap *keyboard = gdk_keymap_get_default();
+
+ if (gdk_keymap_get_caps_lock_state(keyboard)) {
+ modifiers |= SPICE_INPUTS_CAPS_LOCK;
+ }
+
+ if (gdk_keymap_get_num_lock_state(keyboard)) {
+ modifiers |= SPICE_INPUTS_NUM_LOCK;
+ }
+
+ if (gdk_keymap_get_scroll_lock_state(keyboard)) {
+ modifiers |= SPICE_INPUTS_SCROLL_LOCK;
+ }
+#else
+#if HAVE_X11_XKBLIB_H
+ Display *x_display = NULL;
+ XKeyboardState keyboard_state;
+
+ GdkScreen *screen = gdk_screen_get_default();
+ if (!GDK_IS_X11_DISPLAY(gdk_screen_get_display(screen))) {
+ SPICE_DEBUG("FIXME: gtk backend is not X11");
+ return 0;
+ }
+
+ x_display = GDK_SCREEN_XDISPLAY(screen);
+ XGetKeyboardControl(x_display, &keyboard_state);
+
+ if (keyboard_state.led_mask & 0x01) {
+ modifiers |= SPICE_INPUTS_CAPS_LOCK;
+ }
+ if (keyboard_state.led_mask & 0x02) {
+ modifiers |= SPICE_INPUTS_NUM_LOCK;
+ }
+ if (keyboard_state.led_mask & 0x04) {
+ modifiers |= SPICE_INPUTS_SCROLL_LOCK;
+ }
+#elif defined(G_OS_WIN32)
+ if (GetKeyState(VK_CAPITAL) & 1) {
+ modifiers |= SPICE_INPUTS_CAPS_LOCK;
+ }
+ if (GetKeyState(VK_NUMLOCK) & 1) {
+ modifiers |= SPICE_INPUTS_NUM_LOCK;
+ }
+ if (GetKeyState(VK_SCROLL) & 1) {
+ modifiers |= SPICE_INPUTS_SCROLL_LOCK;
+ }
+#else
+ g_warning("get_keyboard_lock_modifiers not implemented");
+#endif // HAVE_X11_XKBLIB_H
+#endif // GTK_CHECK_VERSION(3,18,0)
+ return modifiers;
+}
+
+static void spice_gtk_session_sync_keyboard_modifiers_for_channel(SpiceGtkSession *self,
+ SpiceInputsChannel* inputs,
+ gboolean force)
+{
+ gint guest_modifiers = 0, client_modifiers = 0;
+
+ g_return_if_fail(SPICE_IS_INPUTS_CHANNEL(inputs));
+
+ g_object_get(inputs, "key-modifiers", &guest_modifiers, NULL);
+ client_modifiers = get_keyboard_lock_modifiers();
+
+ if (force || client_modifiers != guest_modifiers) {
+ CHANNEL_DEBUG(inputs, "client_modifiers:0x%x, guest_modifiers:0x%x",
+ client_modifiers, guest_modifiers);
+ spice_inputs_set_key_locks(inputs, client_modifiers);
+ }
+}
+
+static void keymap_modifiers_changed(GdkKeymap *keymap, gpointer data)
+{
+ SpiceGtkSession *self = data;
+
+ spice_gtk_session_sync_keyboard_modifiers(self);
+}
+
+static void guest_modifiers_changed(SpiceInputsChannel *inputs, gpointer data)
+{
+ SpiceGtkSession *self = data;
+
+ spice_gtk_session_sync_keyboard_modifiers_for_channel(self, inputs, FALSE);
+}
+
+static void spice_gtk_session_init(SpiceGtkSession *self)
+{
+ SpiceGtkSessionPrivate *s;
+ GdkKeymap *keymap = gdk_keymap_get_default();
+
+ s = self->priv = SPICE_GTK_SESSION_GET_PRIVATE(self);
+
+ s->clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD);
+ g_signal_connect(G_OBJECT(s->clipboard), "owner-change",
+ G_CALLBACK(clipboard_owner_change), self);
+ s->clipboard_primary = gtk_clipboard_get(GDK_SELECTION_PRIMARY);
+ g_signal_connect(G_OBJECT(s->clipboard_primary), "owner-change",
+ G_CALLBACK(clipboard_owner_change), self);
+ spice_g_signal_connect_object(keymap, "state-changed",
+ G_CALLBACK(keymap_modifiers_changed), self, 0);
+}
+
+static GObject *
+spice_gtk_session_constructor(GType gtype,
+ guint n_properties,
+ GObjectConstructParam *properties)
+{
+ GObject *obj;
+ SpiceGtkSession *self;
+ SpiceGtkSessionPrivate *s;
+ GList *list;
+ GList *it;
+
+ {
+ /* Always chain up to the parent constructor */
+ GObjectClass *parent_class;
+ parent_class = G_OBJECT_CLASS(spice_gtk_session_parent_class);
+ obj = parent_class->constructor(gtype, n_properties, properties);
+ }
+
+ self = SPICE_GTK_SESSION(obj);
+ s = self->priv;
+ if (!s->session)
+ g_error("SpiceGtKSession constructed without a session");
+
+ g_signal_connect(s->session, "channel-new",
+ G_CALLBACK(channel_new), self);
+ g_signal_connect(s->session, "channel-destroy",
+ G_CALLBACK(channel_destroy), self);
+ list = spice_session_get_channels(s->session);
+ for (it = g_list_first(list); it != NULL; it = g_list_next(it)) {
+ channel_new(s->session, it->data, (gpointer*)self);
+ }
+ g_list_free(list);
+
+ return obj;
+}
+
+static void spice_gtk_session_dispose(GObject *gobject)
+{
+ SpiceGtkSession *self = SPICE_GTK_SESSION(gobject);
+ SpiceGtkSessionPrivate *s = self->priv;
+
+ /* release stuff */
+ if (s->clipboard) {
+ g_signal_handlers_disconnect_by_func(s->clipboard,
+ G_CALLBACK(clipboard_owner_change), self);
+ s->clipboard = NULL;
+ }
+
+ if (s->clipboard_primary) {
+ g_signal_handlers_disconnect_by_func(s->clipboard_primary,
+ G_CALLBACK(clipboard_owner_change), self);
+ s->clipboard_primary = NULL;
+ }
+
+ if (s->session) {
+ g_signal_handlers_disconnect_by_func(s->session,
+ G_CALLBACK(channel_new),
+ self);
+ g_signal_handlers_disconnect_by_func(s->session,
+ G_CALLBACK(channel_destroy),
+ self);
+ s->session = NULL;
+ }
+
+ /* Chain up to the parent class */
+ if (G_OBJECT_CLASS(spice_gtk_session_parent_class)->dispose)
+ G_OBJECT_CLASS(spice_gtk_session_parent_class)->dispose(gobject);
+}
+
+static void spice_gtk_session_finalize(GObject *gobject)
+{
+ SpiceGtkSession *self = SPICE_GTK_SESSION(gobject);
+ SpiceGtkSessionPrivate *s = self->priv;
+ int i;
+
+ /* release stuff */
+ for (i = 0; i < CLIPBOARD_LAST; ++i) {
+ g_free(s->clip_targets[i]);
+ s->clip_targets[i] = NULL;
+ }
+
+ /* Chain up to the parent class */
+ if (G_OBJECT_CLASS(spice_gtk_session_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_gtk_session_parent_class)->finalize(gobject);
+}
+
+static void spice_gtk_session_get_property(GObject *gobject,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceGtkSession *self = SPICE_GTK_SESSION(gobject);
+ SpiceGtkSessionPrivate *s = self->priv;
+
+ switch (prop_id) {
+ case PROP_SESSION:
+ g_value_set_object(value, s->session);
+ break;
+ case PROP_AUTO_CLIPBOARD:
+ g_value_set_boolean(value, s->auto_clipboard_enable);
+ break;
+ case PROP_AUTO_USBREDIR:
+ g_value_set_boolean(value, s->auto_usbredir_enable);
+ break;
+ case PROP_POINTER_GRABBED:
+ g_value_set_boolean(value, s->pointer_grabbed);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_gtk_session_set_property(GObject *gobject,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceGtkSession *self = SPICE_GTK_SESSION(gobject);
+ SpiceGtkSessionPrivate *s = self->priv;
+
+ switch (prop_id) {
+ case PROP_SESSION:
+ s->session = g_value_get_object(value);
+ break;
+ case PROP_AUTO_CLIPBOARD:
+ s->auto_clipboard_enable = g_value_get_boolean(value);
+ break;
+ case PROP_AUTO_USBREDIR: {
+ SpiceDesktopIntegration *desktop_int;
+ gboolean orig_value = s->auto_usbredir_enable;
+
+ s->auto_usbredir_enable = g_value_get_boolean(value);
+ if (s->auto_usbredir_enable == orig_value)
+ break;
+
+ if (s->auto_usbredir_reqs) {
+ SpiceUsbDeviceManager *manager =
+ spice_usb_device_manager_get(s->session, NULL);
+
+ if (!manager)
+ break;
+
+ g_object_set(manager, "auto-connect", s->auto_usbredir_enable,
+ NULL);
+
+ desktop_int = spice_desktop_integration_get(s->session);
+ if (s->auto_usbredir_enable)
+ spice_desktop_integration_inhibit_automount(desktop_int);
+ else
+ spice_desktop_integration_uninhibit_automount(desktop_int);
+ }
+ break;
+ }
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_gtk_session_class_init(SpiceGtkSessionClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+
+ gobject_class->constructor = spice_gtk_session_constructor;
+ gobject_class->dispose = spice_gtk_session_dispose;
+ gobject_class->finalize = spice_gtk_session_finalize;
+ gobject_class->get_property = spice_gtk_session_get_property;
+ gobject_class->set_property = spice_gtk_session_set_property;
+
+ /**
+ * SpiceGtkSession:session:
+ *
+ * #SpiceSession this #SpiceGtkSession is associated with
+ *
+ * Since: 0.8
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_SESSION,
+ g_param_spec_object("session",
+ "Session",
+ "SpiceSession",
+ SPICE_TYPE_SESSION,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT_ONLY |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceGtkSession:auto-clipboard:
+ *
+ * When this is true the clipboard gets automatically shared between host
+ * and guest.
+ *
+ * Since: 0.8
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_AUTO_CLIPBOARD,
+ g_param_spec_boolean("auto-clipboard",
+ "Auto clipboard",
+ "Automatically relay clipboard changes between "
+ "host and guest.",
+ TRUE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceGtkSession:auto-usbredir:
+ *
+ * Automatically redirect newly plugged in USB devices. Note the auto
+ * redirection only happens when a #SpiceDisplay associated with the
+ * session had keyboard focus.
+ *
+ * Since: 0.8
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_AUTO_USBREDIR,
+ g_param_spec_boolean("auto-usbredir",
+ "Auto USB Redirection",
+ "Automatically redirect newly plugged in USB"
+ "Devices to the guest.",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceGtkSession:pointer-grabbed:
+ *
+ * Returns %TRUE if the pointer is currently grabbed by this session.
+ *
+ * Since: 0.27
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_POINTER_GRABBED,
+ g_param_spec_boolean("pointer-grabbed",
+ "Pointer grabbed",
+ "Whether the pointer is grabbed",
+ FALSE,
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_type_class_add_private(klass, sizeof(SpiceGtkSessionPrivate));
+}
+
+/* ---------------------------------------------------------------- */
+/* private functions (clipboard related) */
+
+static GtkClipboard* get_clipboard_from_selection(SpiceGtkSessionPrivate *s,
+ guint selection)
+{
+ if (selection == VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD) {
+ return s->clipboard;
+ } else if (selection == VD_AGENT_CLIPBOARD_SELECTION_PRIMARY) {
+ return s->clipboard_primary;
+ } else {
+ g_warning("Unhandled clipboard selection: %d", selection);
+ return NULL;
+ }
+}
+
+static gint get_selection_from_clipboard(SpiceGtkSessionPrivate *s,
+ GtkClipboard* cb)
+{
+ if (cb == s->clipboard) {
+ return VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD;
+ } else if (cb == s->clipboard_primary) {
+ return VD_AGENT_CLIPBOARD_SELECTION_PRIMARY;
+ } else {
+ g_warning("Unhandled clipboard");
+ return -1;
+ }
+}
+
+static const struct {
+ const char *xatom;
+ uint32_t vdagent;
+} atom2agent[] = {
+ {
+ .vdagent = VD_AGENT_CLIPBOARD_UTF8_TEXT,
+ .xatom = "UTF8_STRING",
+ },{
+ .vdagent = VD_AGENT_CLIPBOARD_UTF8_TEXT,
+ .xatom = "text/plain;charset=utf-8"
+ },{
+ .vdagent = VD_AGENT_CLIPBOARD_UTF8_TEXT,
+ .xatom = "STRING"
+ },{
+ .vdagent = VD_AGENT_CLIPBOARD_UTF8_TEXT,
+ .xatom = "TEXT"
+ },{
+ .vdagent = VD_AGENT_CLIPBOARD_UTF8_TEXT,
+ .xatom = "text/plain"
+ },{
+ .vdagent = VD_AGENT_CLIPBOARD_IMAGE_PNG,
+ .xatom = "image/png"
+ },{
+ .vdagent = VD_AGENT_CLIPBOARD_IMAGE_BMP,
+ .xatom = "image/bmp"
+ },{
+ .vdagent = VD_AGENT_CLIPBOARD_IMAGE_BMP,
+ .xatom = "image/x-bmp"
+ },{
+ .vdagent = VD_AGENT_CLIPBOARD_IMAGE_BMP,
+ .xatom = "image/x-MS-bmp"
+ },{
+ .vdagent = VD_AGENT_CLIPBOARD_IMAGE_BMP,
+ .xatom = "image/x-win-bitmap"
+ },{
+ .vdagent = VD_AGENT_CLIPBOARD_IMAGE_TIFF,
+ .xatom = "image/tiff"
+ },{
+ .vdagent = VD_AGENT_CLIPBOARD_IMAGE_JPG,
+ .xatom = "image/jpeg"
+ }
+};
+
+typedef struct _WeakRef {
+ GObject *object;
+} WeakRef;
+
+static void weak_notify_cb(WeakRef *weakref, GObject *object)
+{
+ weakref->object = NULL;
+}
+
+static WeakRef* weak_ref(GObject *object)
+{
+ WeakRef *weakref = g_new(WeakRef, 1);
+
+ g_object_weak_ref(object, (GWeakNotify)weak_notify_cb, weakref);
+ weakref->object = object;
+
+ return weakref;
+}
+
+static void weak_unref(WeakRef* weakref)
+{
+ if (weakref->object)
+ g_object_weak_unref(weakref->object, (GWeakNotify)weak_notify_cb, weakref);
+
+ g_free(weakref);
+}
+
+static void clipboard_get_targets(GtkClipboard *clipboard,
+ GdkAtom *atoms,
+ gint n_atoms,
+ gpointer user_data)
+{
+ WeakRef *weakref = user_data;
+ SpiceGtkSession *self = (SpiceGtkSession*)weakref->object;
+ weak_unref(weakref);
+
+ if (self == NULL)
+ return;
+
+ g_return_if_fail(SPICE_IS_GTK_SESSION(self));
+
+ SpiceGtkSessionPrivate *s = self->priv;
+ guint32 types[SPICE_N_ELEMENTS(atom2agent)];
+ char *name;
+ int a, m, t;
+ int selection;
+
+ if (s->main == NULL)
+ return;
+
+ selection = get_selection_from_clipboard(s, clipboard);
+ g_return_if_fail(selection != -1);
+
+ SPICE_DEBUG("%s:", __FUNCTION__);
+ if (spice_util_get_debug()) {
+ for (a = 0; a < n_atoms; a++) {
+ name = gdk_atom_name(atoms[a]);
+ SPICE_DEBUG(" \"%s\"", name);
+ g_free(name);
+ }
+ }
+
+ memset(types, 0, sizeof(types));
+ for (a = 0; a < n_atoms; a++) {
+ name = gdk_atom_name(atoms[a]);
+ for (m = 0; m < SPICE_N_ELEMENTS(atom2agent); m++) {
+ if (strcasecmp(name, atom2agent[m].xatom) != 0) {
+ continue;
+ }
+ /* found match */
+ for (t = 0; t < SPICE_N_ELEMENTS(atom2agent); t++) {
+ if (types[t] == atom2agent[m].vdagent) {
+ /* type already in list */
+ break;
+ }
+ if (types[t] == 0) {
+ /* add type to empty slot */
+ types[t] = atom2agent[m].vdagent;
+ break;
+ }
+ }
+ break;
+ }
+ g_free(name);
+ }
+ for (t = 0; t < SPICE_N_ELEMENTS(atom2agent); t++) {
+ if (types[t] == 0) {
+ break;
+ }
+ }
+ if (!s->clip_grabbed[selection] && t > 0) {
+ s->clip_grabbed[selection] = TRUE;
+
+ if (spice_main_agent_test_capability(s->main, VD_AGENT_CAP_CLIPBOARD_BY_DEMAND))
+ spice_main_clipboard_selection_grab(s->main, selection, types, t);
+ /* Sending a grab causes the agent to do an impicit release */
+ s->nclip_targets[selection] = 0;
+ }
+}
+
+static void clipboard_owner_change(GtkClipboard *clipboard,
+ GdkEventOwnerChange *event,
+ gpointer user_data)
+{
+ g_return_if_fail(SPICE_IS_GTK_SESSION(user_data));
+
+ SpiceGtkSession *self = user_data;
+ SpiceGtkSessionPrivate *s = self->priv;
+ int selection;
+
+ selection = get_selection_from_clipboard(s, clipboard);
+ g_return_if_fail(selection != -1);
+
+ if (s->main == NULL)
+ return;
+
+ if (s->clip_grabbed[selection]) {
+ s->clip_grabbed[selection] = FALSE;
+ if (spice_main_agent_test_capability(s->main, VD_AGENT_CAP_CLIPBOARD_BY_DEMAND))
+ spice_main_clipboard_selection_release(s->main, selection);
+ }
+
+ switch (event->reason) {
+ case GDK_OWNER_CHANGE_NEW_OWNER:
+ if (gtk_clipboard_get_owner(clipboard) == G_OBJECT(self))
+ break;
+
+ s->clipboard_by_guest[selection] = FALSE;
+ s->clip_hasdata[selection] = TRUE;
+ if (s->auto_clipboard_enable && !read_only(self))
+ gtk_clipboard_request_targets(clipboard, clipboard_get_targets,
+ weak_ref(G_OBJECT(self)));
+ break;
+ default:
+ s->clip_hasdata[selection] = FALSE;
+ break;
+ }
+}
+
+typedef struct
+{
+ SpiceGtkSession *self;
+ GMainLoop *loop;
+ GtkSelectionData *selection_data;
+ guint info;
+ guint selection;
+} RunInfo;
+
+static void clipboard_got_from_guest(SpiceMainChannel *main, guint selection,
+ guint type, const guchar *data, guint size,
+ gpointer user_data)
+{
+ RunInfo *ri = user_data;
+ SpiceGtkSessionPrivate *s = ri->self->priv;
+ gchar *conv = NULL;
+
+ g_return_if_fail(selection == ri->selection);
+
+ SPICE_DEBUG("clipboard got data");
+
+ if (atom2agent[ri->info].vdagent == VD_AGENT_CLIPBOARD_UTF8_TEXT) {
+ /* on windows, gtk+ would already convert to LF endings, but
+ not on unix */
+ if (spice_main_agent_test_capability(s->main, VD_AGENT_CAP_GUEST_LINEEND_CRLF)) {
+ GError *err = NULL;
+
+ conv = spice_dos2unix((gchar*)data, size, &err);
+ if (err) {
+ g_warning("Failed to convert text line ending: %s", err->message);
+ g_clear_error(&err);
+ goto end;
+ }
+
+ size = strlen(conv);
+ }
+
+ gtk_selection_data_set_text(ri->selection_data, conv ?: (gchar*)data, size);
+ } else {
+ gtk_selection_data_set(ri->selection_data,
+ gdk_atom_intern_static_string(atom2agent[ri->info].xatom),
+ 8, data, size);
+ }
+
+end:
+ if (g_main_loop_is_running (ri->loop))
+ g_main_loop_quit (ri->loop);
+
+ g_free(conv);
+}
+
+static void clipboard_agent_connected(RunInfo *ri)
+{
+ g_warning("agent status changed, cancel clipboard request");
+
+ if (g_main_loop_is_running(ri->loop))
+ g_main_loop_quit(ri->loop);
+}
+
+static void clipboard_get(GtkClipboard *clipboard,
+ GtkSelectionData *selection_data,
+ guint info, gpointer user_data)
+{
+ g_return_if_fail(SPICE_IS_GTK_SESSION(user_data));
+
+ RunInfo ri = { NULL, };
+ SpiceGtkSession *self = user_data;
+ SpiceGtkSessionPrivate *s = self->priv;
+ gboolean agent_connected = FALSE;
+ gulong clipboard_handler;
+ gulong agent_handler;
+ int selection;
+
+ SPICE_DEBUG("clipboard get");
+
+ selection = get_selection_from_clipboard(s, clipboard);
+ g_return_if_fail(selection != -1);
+ g_return_if_fail(info < SPICE_N_ELEMENTS(atom2agent));
+ g_return_if_fail(s->main != NULL);
+
+ ri.selection_data = selection_data;
+ ri.info = info;
+ ri.loop = g_main_loop_new(NULL, FALSE);
+ ri.selection = selection;
+ ri.self = self;
+
+ clipboard_handler = g_signal_connect(s->main, "main-clipboard-selection",
+ G_CALLBACK(clipboard_got_from_guest),
+ &ri);
+ agent_handler = g_signal_connect_swapped(s->main, "notify::agent-connected",
+ G_CALLBACK(clipboard_agent_connected),
+ &ri);
+
+ spice_main_clipboard_selection_request(s->main, selection,
+ atom2agent[info].vdagent);
+
+
+ g_object_get(s->main, "agent-connected", &agent_connected, NULL);
+ if (!agent_connected) {
+ SPICE_DEBUG("canceled clipboard_get, before running loop");
+ goto cleanup;
+ }
+
+ /* apparently, this is needed to avoid dead-lock, from
+ gtk_dialog_run */
+ gdk_threads_leave();
+ g_main_loop_run(ri.loop);
+ gdk_threads_enter();
+
+cleanup:
+ g_main_loop_unref(ri.loop);
+ ri.loop = NULL;
+ g_signal_handler_disconnect(s->main, clipboard_handler);
+ g_signal_handler_disconnect(s->main, agent_handler);
+}
+
+static void clipboard_clear(GtkClipboard *clipboard, gpointer user_data)
+{
+ SPICE_DEBUG("clipboard_clear");
+ /* We watch for clipboard ownership changes and act on those, so we
+ don't need to do anything here */
+}
+
+static gboolean clipboard_grab(SpiceMainChannel *main, guint selection,
+ guint32* types, guint32 ntypes,
+ gpointer user_data)
+{
+ g_return_val_if_fail(SPICE_IS_GTK_SESSION(user_data), FALSE);
+
+ SpiceGtkSession *self = user_data;
+ SpiceGtkSessionPrivate *s = self->priv;
+ GtkTargetEntry targets[SPICE_N_ELEMENTS(atom2agent)];
+ gboolean target_selected[SPICE_N_ELEMENTS(atom2agent)] = { FALSE, };
+ gboolean found;
+ GtkClipboard* cb;
+ int m, n, i;
+
+ cb = get_clipboard_from_selection(s, selection);
+ g_return_val_if_fail(cb != NULL, FALSE);
+
+ i = 0;
+ for (n = 0; n < ntypes; ++n) {
+ found = FALSE;
+ for (m = 0; m < SPICE_N_ELEMENTS(atom2agent); m++) {
+ if (atom2agent[m].vdagent == types[n] && !target_selected[m]) {
+ found = TRUE;
+ g_return_val_if_fail(i < SPICE_N_ELEMENTS(atom2agent), FALSE);
+ targets[i].target = (gchar*)atom2agent[m].xatom;
+ targets[i].info = m;
+ target_selected[m] = TRUE;
+ i += 1;
+ }
+ }
+ if (!found) {
+ g_warning("clipboard: couldn't find a matching type for: %d",
+ types[n]);
+ }
+ }
+
+ g_free(s->clip_targets[selection]);
+ s->nclip_targets[selection] = i;
+ s->clip_targets[selection] = g_memdup(targets, sizeof(GtkTargetEntry) * i);
+ /* Receiving a grab implies we've released our own grab */
+ s->clip_grabbed[selection] = FALSE;
+
+ if (read_only(self) ||
+ !s->auto_clipboard_enable ||
+ s->nclip_targets[selection] == 0)
+ goto skip_grab_clipboard;
+
+ if (!gtk_clipboard_set_with_owner(cb, targets, i,
+ clipboard_get, clipboard_clear, G_OBJECT(self))) {
+ g_warning("clipboard grab failed");
+ return FALSE;
+ }
+ s->clipboard_by_guest[selection] = TRUE;
+ s->clip_hasdata[selection] = FALSE;
+
+skip_grab_clipboard:
+ return TRUE;
+}
+
+static gboolean check_clipboard_size_limits(SpiceGtkSession *session,
+ gint clipboard_len)
+{
+ int max_clipboard;
+
+ g_object_get(session->priv->main, "max-clipboard", &max_clipboard, NULL);
+ if (max_clipboard != -1 && clipboard_len > max_clipboard) {
+ g_warning("discarded clipboard of size %d (max: %d)",
+ clipboard_len, max_clipboard);
+ return FALSE;
+ } else if (clipboard_len <= 0) {
+ SPICE_DEBUG("discarding empty clipboard");
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void clipboard_received_cb(GtkClipboard *clipboard,
+ GtkSelectionData *selection_data,
+ gpointer user_data)
+{
+ WeakRef *weakref = user_data;
+ SpiceGtkSession *self = (SpiceGtkSession*)weakref->object;
+ weak_unref(weakref);
+
+ if (self == NULL)
+ return;
+
+ g_return_if_fail(SPICE_IS_GTK_SESSION(self));
+
+ SpiceGtkSessionPrivate *s = self->priv;
+ gint len = 0, m;
+ guint32 type = VD_AGENT_CLIPBOARD_NONE;
+ gchar* name;
+ GdkAtom atom;
+ int selection;
+
+ selection = get_selection_from_clipboard(s, clipboard);
+ g_return_if_fail(selection != -1);
+
+ len = gtk_selection_data_get_length(selection_data);
+ if (!check_clipboard_size_limits(self, len)) {
+ return;
+ } else {
+ atom = gtk_selection_data_get_data_type(selection_data);
+ name = gdk_atom_name(atom);
+ for (m = 0; m < SPICE_N_ELEMENTS(atom2agent); m++) {
+ if (strcasecmp(name, atom2agent[m].xatom) == 0) {
+ break;
+ }
+ }
+
+ if (m >= SPICE_N_ELEMENTS(atom2agent)) {
+ g_warning("clipboard_received for unsupported type: %s", name);
+ } else {
+ type = atom2agent[m].vdagent;
+ }
+
+ g_free(name);
+ }
+
+ const guchar *data = gtk_selection_data_get_data(selection_data);
+ gpointer conv = NULL;
+
+ /* gtk+ internal utf8 newline is always LF, even on windows */
+ if (type == VD_AGENT_CLIPBOARD_UTF8_TEXT) {
+ if (spice_main_agent_test_capability(s->main, VD_AGENT_CAP_GUEST_LINEEND_CRLF)) {
+ GError *err = NULL;
+
+ conv = spice_unix2dos((gchar*)data, len, &err);
+ if (err) {
+ g_warning("Failed to convert text line ending: %s", err->message);
+ g_clear_error(&err);
+ return;
+ }
+
+ len = strlen(conv);
+ } else {
+ /* On Windows, with some versions of gtk+, GtkSelectionData::length
+ * will include the final '\0'. When a string with this trailing '\0'
+ * is pasted in some linux applications, it will be pasted as <NIL> or
+ * as an invisible character, which is unwanted. Ensure the length we
+ * send to the agent does not include any trailing '\0'
+ * This is gtk+ bug https://bugzilla.gnome.org/show_bug.cgi?id=734670
+ */
+ len = strlen((const char *)data);
+ }
+ if (!check_clipboard_size_limits(self, len)) {
+ g_free(conv);
+ return;
+ }
+ }
+
+ spice_main_clipboard_selection_notify(s->main, selection, type,
+ conv ?: data, len);
+ g_free(conv);
+}
+
+static gboolean clipboard_request(SpiceMainChannel *main, guint selection,
+ guint type, gpointer user_data)
+{
+ g_return_val_if_fail(SPICE_IS_GTK_SESSION(user_data), FALSE);
+
+ SpiceGtkSession *self = user_data;
+ SpiceGtkSessionPrivate *s = self->priv;
+ GdkAtom atom;
+ GtkClipboard* cb;
+ int m;
+
+ g_return_val_if_fail(s->clipboard_by_guest[selection] == FALSE, FALSE);
+ g_return_val_if_fail(s->clip_grabbed[selection], FALSE);
+
+ if (read_only(self))
+ return FALSE;
+
+ cb = get_clipboard_from_selection(s, selection);
+ g_return_val_if_fail(cb != NULL, FALSE);
+
+ for (m = 0; m < SPICE_N_ELEMENTS(atom2agent); m++) {
+ if (atom2agent[m].vdagent == type)
+ break;
+ }
+
+ g_return_val_if_fail(m < SPICE_N_ELEMENTS(atom2agent), FALSE);
+
+ atom = gdk_atom_intern_static_string(atom2agent[m].xatom);
+ gtk_clipboard_request_contents(cb, atom, clipboard_received_cb,
+ weak_ref(G_OBJECT(self)));
+
+ return TRUE;
+}
+
+static void clipboard_release(SpiceMainChannel *main, guint selection,
+ gpointer user_data)
+{
+ g_return_if_fail(SPICE_IS_GTK_SESSION(user_data));
+
+ SpiceGtkSession *self = user_data;
+ SpiceGtkSessionPrivate *s = self->priv;
+ GtkClipboard* clipboard = get_clipboard_from_selection(s, selection);
+
+ if (!clipboard)
+ return;
+
+ s->nclip_targets[selection] = 0;
+
+ if (!s->clipboard_by_guest[selection])
+ return;
+ gtk_clipboard_clear(clipboard);
+ s->clipboard_by_guest[selection] = FALSE;
+}
+
+static void channel_new(SpiceSession *session, SpiceChannel *channel,
+ gpointer user_data)
+{
+ g_return_if_fail(SPICE_IS_GTK_SESSION(user_data));
+
+ SpiceGtkSession *self = user_data;
+ SpiceGtkSessionPrivate *s = self->priv;
+
+ if (SPICE_IS_MAIN_CHANNEL(channel)) {
+ SPICE_DEBUG("Changing main channel from %p to %p", s->main, channel);
+ s->main = SPICE_MAIN_CHANNEL(channel);
+ g_signal_connect(channel, "main-clipboard-selection-grab",
+ G_CALLBACK(clipboard_grab), self);
+ g_signal_connect(channel, "main-clipboard-selection-request",
+ G_CALLBACK(clipboard_request), self);
+ g_signal_connect(channel, "main-clipboard-selection-release",
+ G_CALLBACK(clipboard_release), self);
+ }
+ if (SPICE_IS_INPUTS_CHANNEL(channel)) {
+ spice_g_signal_connect_object(channel, "inputs-modifiers",
+ G_CALLBACK(guest_modifiers_changed), self, 0);
+ spice_gtk_session_sync_keyboard_modifiers_for_channel(self, SPICE_INPUTS_CHANNEL(channel), TRUE);
+ }
+}
+
+static void channel_destroy(SpiceSession *session, SpiceChannel *channel,
+ gpointer user_data)
+{
+ g_return_if_fail(SPICE_IS_GTK_SESSION(user_data));
+
+ SpiceGtkSession *self = user_data;
+ SpiceGtkSessionPrivate *s = self->priv;
+ guint i;
+
+ if (SPICE_IS_MAIN_CHANNEL(channel) && SPICE_MAIN_CHANNEL(channel) == s->main) {
+ s->main = NULL;
+ for (i = 0; i < CLIPBOARD_LAST; ++i) {
+ if (s->clipboard_by_guest[i]) {
+ GtkClipboard *cb = get_clipboard_from_selection(s, i);
+ if (cb)
+ gtk_clipboard_clear(cb);
+ s->clipboard_by_guest[i] = FALSE;
+ }
+ s->clip_grabbed[i] = FALSE;
+ s->nclip_targets[i] = 0;
+ }
+ }
+}
+
+static gboolean read_only(SpiceGtkSession *self)
+{
+ return spice_session_get_read_only(self->priv->session);
+}
+
+/* ---------------------------------------------------------------- */
+/* private functions (usbredir related) */
+G_GNUC_INTERNAL
+void spice_gtk_session_request_auto_usbredir(SpiceGtkSession *self,
+ gboolean state)
+{
+ g_return_if_fail(SPICE_IS_GTK_SESSION(self));
+
+ SpiceGtkSessionPrivate *s = self->priv;
+ SpiceDesktopIntegration *desktop_int;
+ SpiceUsbDeviceManager *manager;
+
+ if (state) {
+ s->auto_usbredir_reqs++;
+ if (s->auto_usbredir_reqs != 1)
+ return;
+ } else {
+ g_return_if_fail(s->auto_usbredir_reqs > 0);
+ s->auto_usbredir_reqs--;
+ if (s->auto_usbredir_reqs != 0)
+ return;
+ }
+
+ if (!s->auto_usbredir_enable)
+ return;
+
+ manager = spice_usb_device_manager_get(s->session, NULL);
+ if (!manager)
+ return;
+
+ g_object_set(manager, "auto-connect", state, NULL);
+
+ desktop_int = spice_desktop_integration_get(s->session);
+ if (state)
+ spice_desktop_integration_inhibit_automount(desktop_int);
+ else
+ spice_desktop_integration_uninhibit_automount(desktop_int);
+}
+
+/* ------------------------------------------------------------------ */
+/* public functions */
+
+/**
+ * spice_gtk_session_get:
+ * @session: #SpiceSession for which to get the #SpiceGtkSession
+ *
+ * Gets the #SpiceGtkSession associated with the passed in #SpiceSession.
+ * A new #SpiceGtkSession instance will be created the first time this
+ * function is called for a certain #SpiceSession.
+ *
+ * Note that this function returns a weak reference, which should not be used
+ * after the #SpiceSession itself has been unref-ed by the caller.
+ *
+ * Returns: (transfer none): a weak reference to the #SpiceGtkSession associated with the passed in #SpiceSession
+ *
+ * Since 0.8
+ **/
+SpiceGtkSession *spice_gtk_session_get(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+
+ SpiceGtkSession *self;
+ static GStaticMutex mutex = G_STATIC_MUTEX_INIT;
+
+ g_static_mutex_lock(&mutex);
+ self = g_object_get_data(G_OBJECT(session), "spice-gtk-session");
+ if (self == NULL) {
+ self = g_object_new(SPICE_TYPE_GTK_SESSION, "session", session, NULL);
+ g_object_set_data_full(G_OBJECT(session), "spice-gtk-session", self, g_object_unref);
+ }
+ g_static_mutex_unlock(&mutex);
+
+ return SPICE_GTK_SESSION(self);
+}
+
+/**
+ * spice_gtk_session_copy_to_guest:
+ * @self:
+ *
+ * Copy client-side clipboard to guest clipboard.
+ *
+ * Since 0.8
+ **/
+void spice_gtk_session_copy_to_guest(SpiceGtkSession *self)
+{
+ g_return_if_fail(SPICE_IS_GTK_SESSION(self));
+ g_return_if_fail(read_only(self) == FALSE);
+
+ SpiceGtkSessionPrivate *s = self->priv;
+ int selection = VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD;
+
+ if (s->clip_hasdata[selection] && !s->clip_grabbed[selection]) {
+ gtk_clipboard_request_targets(s->clipboard, clipboard_get_targets,
+ weak_ref(G_OBJECT(self)));
+ }
+}
+
+/**
+ * spice_gtk_session_paste_from_guest:
+ * @self:
+ *
+ * Copy guest clipboard to client-side clipboard.
+ *
+ * Since 0.8
+ **/
+void spice_gtk_session_paste_from_guest(SpiceGtkSession *self)
+{
+ g_return_if_fail(SPICE_IS_GTK_SESSION(self));
+ g_return_if_fail(read_only(self) == FALSE);
+
+ SpiceGtkSessionPrivate *s = self->priv;
+ int selection = VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD;
+
+ if (s->nclip_targets[selection] == 0) {
+ g_warning("Guest clipboard is not available.");
+ return;
+ }
+
+ if (!gtk_clipboard_set_with_owner(s->clipboard, s->clip_targets[selection], s->nclip_targets[selection],
+ clipboard_get, clipboard_clear, G_OBJECT(self))) {
+ g_warning("Clipboard grab failed");
+ return;
+ }
+ s->clipboard_by_guest[selection] = TRUE;
+ s->clip_hasdata[selection] = FALSE;
+}
+
+G_GNUC_INTERNAL
+void spice_gtk_session_sync_keyboard_modifiers(SpiceGtkSession *self)
+{
+ GList *l = NULL, *channels = spice_session_get_channels(self->priv->session);
+
+ for (l = channels; l != NULL; l = l->next) {
+ if (SPICE_IS_INPUTS_CHANNEL(l->data)) {
+ SpiceInputsChannel *inputs = SPICE_INPUTS_CHANNEL(l->data);
+ spice_gtk_session_sync_keyboard_modifiers_for_channel(self, inputs, TRUE);
+ }
+ }
+ g_list_free(channels);
+}
+
+G_GNUC_INTERNAL
+void spice_gtk_session_set_pointer_grabbed(SpiceGtkSession *self, gboolean grabbed)
+{
+ g_return_if_fail(SPICE_IS_GTK_SESSION(self));
+
+ self->priv->pointer_grabbed = grabbed;
+ g_object_notify(G_OBJECT(self), "pointer-grabbed");
+}
+
+G_GNUC_INTERNAL
+gboolean spice_gtk_session_get_pointer_grabbed(SpiceGtkSession *self)
+{
+ g_return_val_if_fail(SPICE_IS_GTK_SESSION(self), FALSE);
+
+ return self->priv->pointer_grabbed;
+}
diff --git a/src/spice-gtk-session.h b/src/spice-gtk-session.h
new file mode 100644
index 0000000..3b4eac6
--- /dev/null
+++ b/src/spice-gtk-session.h
@@ -0,0 +1,65 @@
+/* -*- 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/>.
+*/
+#ifndef __SPICE_CLIENT_GTK_SESSION_H__
+#define __SPICE_CLIENT_GTK_SESSION_H__
+
+#include "spice-client.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_GTK_SESSION (spice_gtk_session_get_type ())
+#define SPICE_GTK_SESSION(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SPICE_TYPE_GTK_SESSION, SpiceGtkSession))
+#define SPICE_GTK_SESSION_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SPICE_TYPE_GTK_SESSION, SpiceGtkSessionClass))
+#define SPICE_IS_GTK_SESSION(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SPICE_TYPE_GTK_SESSION))
+#define SPICE_IS_GTK_SESSION_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SPICE_TYPE_GTK_SESSION))
+#define SPICE_GTK_SESSION_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SPICE_TYPE_GTK_SESSION, SpiceGtkSessionClass))
+
+typedef struct _SpiceGtkSession SpiceGtkSession;
+typedef struct _SpiceGtkSessionClass SpiceGtkSessionClass;
+typedef struct _SpiceGtkSessionPrivate SpiceGtkSessionPrivate;
+
+struct _SpiceGtkSession
+{
+ GObject parent;
+ SpiceGtkSessionPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+struct _SpiceGtkSessionClass
+{
+ GObjectClass parent_class;
+
+ /* signals */
+
+ /*< private >*/
+ /*
+ * If adding fields to this struct, remove corresponding
+ * amount of padding to avoid changing overall struct size
+ */
+ gchar _spice_reserved[SPICE_RESERVED_PADDING];
+};
+
+GType spice_gtk_session_get_type(void);
+
+SpiceGtkSession *spice_gtk_session_get(SpiceSession *session);
+void spice_gtk_session_copy_to_guest(SpiceGtkSession *self);
+void spice_gtk_session_paste_from_guest(SpiceGtkSession *self);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_GTK_SESSION_H__ */
diff --git a/src/spice-gtk-sym-file b/src/spice-gtk-sym-file
new file mode 100644
index 0000000..1574e07
--- /dev/null
+++ b/src/spice-gtk-sym-file
@@ -0,0 +1,23 @@
+spice_display_copy_to_guest
+spice_display_get_grab_keys
+spice_display_get_pixbuf
+spice_display_get_type
+spice_display_key_event_get_type
+spice_display_mouse_ungrab
+spice_display_new
+spice_display_new_with_monitor
+spice_display_paste_from_guest
+spice_display_send_keys
+spice_display_set_grab_keys
+spice_grab_sequence_as_string
+spice_grab_sequence_copy
+spice_grab_sequence_free
+spice_grab_sequence_get_type
+spice_grab_sequence_new
+spice_grab_sequence_new_from_string
+spice_gtk_session_copy_to_guest
+spice_gtk_session_get
+spice_gtk_session_get_type
+spice_gtk_session_paste_from_guest
+spice_usb_device_widget_get_type
+spice_usb_device_widget_new
diff --git a/src/spice-marshal.txt b/src/spice-marshal.txt
new file mode 100644
index 0000000..9c76054
--- /dev/null
+++ b/src/spice-marshal.txt
@@ -0,0 +1,14 @@
+VOID:INT,INT
+VOID:INT,INT,INT
+VOID:INT,INT,INT,INT
+VOID:INT,INT,INT,INT,POINTER
+VOID:INT,INT,INT,INT,INT,POINTER
+VOID:POINTER,INT
+BOOLEAN:POINTER,UINT
+BOOLEAN:UINT
+VOID:UINT,POINTER,UINT
+VOID:UINT,UINT,POINTER,UINT
+BOOLEAN:UINT,POINTER,UINT
+BOOLEAN:UINT,UINT
+VOID:OBJECT,OBJECT
+VOID:BOXED,BOXED
diff --git a/src/spice-option.c b/src/spice-option.c
new file mode 100644
index 0000000..958e03c
--- /dev/null
+++ b/src/spice-option.c
@@ -0,0 +1,284 @@
+/* -*- 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 <stdlib.h>
+#include <glib-object.h>
+#include <glib/gi18n.h>
+#include "glib-compat.h"
+#include "spice-session.h"
+#include "spice-util.h"
+#include "spice-channel-priv.h"
+#include "usb-device-manager.h"
+
+static GStrv disable_effects = NULL;
+static gint color_depth = 0;
+static char *ca_file = NULL;
+static char *host_subject = NULL;
+static char *smartcard_db = NULL;
+static char *smartcard_certificates = NULL;
+static char *usbredir_auto_redirect_filter = NULL;
+static char *usbredir_redirect_on_connect = NULL;
+static gboolean smartcard = FALSE;
+static gboolean disable_audio = FALSE;
+static gboolean disable_usbredir = FALSE;
+static gint cache_size = 0;
+static gint glz_window_size = 0;
+static gchar *secure_channels = NULL;
+static gchar *shared_dir = NULL;
+
+G_GNUC_NORETURN
+static void option_version(void)
+{
+ g_print(PACKAGE_STRING "\n");
+ exit(0);
+}
+
+static gboolean option_debug(void)
+{
+ spice_util_set_debug(TRUE);
+ return TRUE;
+}
+
+static gboolean parse_color_depth(const gchar *option_name, const gchar *value,
+ gpointer data, GError **error)
+{
+ unsigned long parsed_depth;
+ char *end;
+
+ if (option_name == NULL) {
+ g_set_error(error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("missing color depth, must be 16 or 32"));
+ return FALSE;
+ }
+
+ parsed_depth = strtoul(value, &end, 0);
+ if (*end != '\0')
+ goto error;
+
+ if ((parsed_depth != 16) && (parsed_depth != 32))
+ goto error;
+
+ color_depth = parsed_depth;
+
+ return TRUE;
+
+error:
+ g_set_error(error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED, _("invalid color depth (%s), must be 16 or 32"), value);
+ return FALSE;
+}
+
+static gboolean parse_disable_effects(const gchar *option_name, const gchar *value,
+ gpointer data, GError **error)
+{
+ GStrv it;
+
+ disable_effects = g_strsplit(value, ",", -1);
+ for (it = disable_effects; *it != NULL; it++) {
+ if ((g_strcmp0(*it, "wallpaper") != 0)
+ && (g_strcmp0(*it, "font-smooth") != 0)
+ && (g_strcmp0(*it, "animation") != 0)
+ && (g_strcmp0(*it, "all") != 0)) {
+ /* Translators: do not translate 'wallpaper', 'font-smooth',
+ * 'animation', 'all' as the user must use these values with the
+ * --spice-disable-effects command line option
+ */
+ g_set_error(error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED,
+ _("invalid effect name (%s), must be 'wallpaper', 'font-smooth', 'animation' or 'all'"), *it);
+ g_strfreev(disable_effects);
+ disable_effects = NULL;
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+static gboolean parse_secure_channels(const gchar *option_name, const gchar *value,
+ gpointer data, GError **error)
+{
+ gint i;
+ gchar **channels = g_strsplit(value, ",", -1);
+
+ g_return_val_if_fail(channels != NULL, FALSE);
+
+ for (i = 0; channels[i]; i++) {
+ if (g_strcmp0(channels[i], "all") == 0)
+ continue;
+
+ if (spice_channel_string_to_type(channels[i]) == -1) {
+ gchar *supported = spice_channel_supported_string();
+ g_set_error(error, G_OPTION_ERROR, G_OPTION_ERROR_FAILED,
+ _("invalid channel name (%s), valid names: all, %s"),
+ channels[i], supported);
+ g_free(supported);
+ return FALSE;
+ }
+ }
+
+ g_strfreev(channels);
+
+ secure_channels = g_strdup(value);
+
+ return TRUE;
+}
+
+
+static gboolean parse_usbredir_filter(const gchar *option_name,
+ const gchar *value,
+ gpointer data, GError **error)
+
+{
+ g_warning("--spice-usbredir-filter is deprecated, please use --spice-usbredir-auto-redirect-filter instead");
+ g_free(usbredir_auto_redirect_filter);
+ usbredir_auto_redirect_filter = g_strdup(value);
+ return TRUE;
+}
+
+
+/**
+ * spice_get_option_group: (skip)
+ *
+ * Returns: (transfer full): a #GOptionGroup for the commandline
+ * arguments specific to Spice. You have to call
+ * spice_set_session_option() after to set the options on a
+ * #SpiceSession.
+ **/
+GOptionGroup* spice_get_option_group(void)
+{
+ const GOptionEntry entries[] = {
+ { "spice-secure-channels", '\0', 0, G_OPTION_ARG_CALLBACK, parse_secure_channels,
+ N_("Force the specified channels to be secured"), "<main,display,inputs,...,all>" },
+ { "spice-disable-effects", '\0', 0, G_OPTION_ARG_CALLBACK, parse_disable_effects,
+ N_("Disable guest display effects"), "<wallpaper,font-smooth,animation,all>" },
+ { "spice-color-depth", '\0', 0, G_OPTION_ARG_CALLBACK, parse_color_depth,
+ N_("Guest display color depth"), "<16,32>" },
+ { "spice-ca-file", '\0', 0, G_OPTION_ARG_FILENAME, &ca_file,
+ N_("Truststore file for secure connections"), N_("<file>") },
+ { "spice-host-subject", '\0', 0, G_OPTION_ARG_STRING, &host_subject,
+ N_("Subject of the host certificate (field=value pairs separated by commas)"), N_("<host-subject>") },
+ { "spice-disable-audio", '\0', 0, G_OPTION_ARG_NONE, &disable_audio,
+ N_("Disable audio support"), NULL },
+ { "spice-smartcard", '\0', 0, G_OPTION_ARG_NONE, &smartcard,
+ N_("Enable smartcard support"), NULL },
+ { "spice-smartcard-certificates", '\0', 0, G_OPTION_ARG_STRING, &smartcard_certificates,
+ N_("Certificates to use for software smartcards (field=values separated by commas)"), N_("<certificates>") },
+ { "spice-smartcard-db", '\0', 0, G_OPTION_ARG_STRING, &smartcard_db,
+ N_("Path to the local certificate database to use for software smartcard certificates"), N_("<certificate-db>") },
+ { "spice-disable-usbredir", '\0', 0, G_OPTION_ARG_NONE, &disable_usbredir,
+ N_("Disable USB redirection support"), NULL },
+ /* Backward compats version of spice-usbredir-auto-redirect-filter */
+ { "spice-usbredir-filter", '\0', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_CALLBACK, parse_usbredir_filter,
+ NULL, NULL },
+ { "spice-usbredir-auto-redirect-filter", '\0', 0, G_OPTION_ARG_STRING, &usbredir_auto_redirect_filter,
+ N_("Filter selecting USB devices to be auto-redirected when plugged in"), N_("<filter-string>") },
+ { "spice-usbredir-redirect-on-connect", '\0', 0, G_OPTION_ARG_STRING, &usbredir_redirect_on_connect,
+ N_("Filter selecting USB devices to redirect on connect"), N_("<filter-string>") },
+ { "spice-cache-size", '\0', 0, G_OPTION_ARG_INT, &cache_size,
+ N_("Image cache size"), N_("<bytes>") },
+ { "spice-glz-window-size", '\0', 0, G_OPTION_ARG_INT, &glz_window_size,
+ N_("Glz compression history size"), N_("<bytes>") },
+ { "spice-shared-dir", '\0', 0, G_OPTION_ARG_FILENAME, &shared_dir,
+ N_("Shared directory"), N_("<dir>") },
+
+ { "spice-debug", '\0', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, option_debug,
+ N_("Enable Spice-GTK debugging"), NULL },
+ { "spice-gtk-version", '\0', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, option_version,
+ N_("Display Spice-GTK version information"), NULL },
+ { NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL }
+ };
+ GOptionGroup *grp;
+
+ grp = g_option_group_new("spice", _("Spice Options:"), _("Show Spice Options"), NULL, NULL);
+ g_option_group_add_entries(grp, entries);
+
+ return grp;
+}
+
+/**
+ * spice_set_session_option:
+ * @session: a #SpiceSession to set option upon
+ *
+ * Set various properties on @session, according to the commandline
+ * arguments given to spice_get_option_group() option group.
+ **/
+void spice_set_session_option(SpiceSession *session)
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+
+ if (ca_file == NULL) {
+ const char *homedir = g_getenv("HOME");
+ if (!homedir)
+ homedir = g_get_home_dir();
+ ca_file = g_build_filename(homedir, ".spicec", "spice_truststore.pem", NULL);
+ if (!g_file_test(ca_file, G_FILE_TEST_IS_REGULAR))
+ g_clear_pointer(&ca_file, g_free);
+ }
+
+ if (disable_effects) {
+ g_object_set(session, "disable-effects", disable_effects, NULL);
+ }
+
+ if (secure_channels) {
+ GStrv channels;
+ channels = g_strsplit(secure_channels, ",", -1);
+ if (channels)
+ g_object_set(session, "secure-channels", channels, NULL);
+ g_strfreev(channels);
+ }
+
+ if (color_depth)
+ g_object_set(session, "color-depth", color_depth, NULL);
+ if (ca_file)
+ g_object_set(session, "ca-file", ca_file, NULL);
+ if (host_subject)
+ g_object_set(session, "cert-subject", host_subject, NULL);
+ if (smartcard) {
+ g_object_set(session, "enable-smartcard", smartcard, NULL);
+ if (smartcard_certificates) {
+ GStrv certs_strv;
+ certs_strv = g_strsplit(smartcard_certificates, ",", -1);
+ if (certs_strv)
+ g_object_set(session, "smartcard-certificates", certs_strv, NULL);
+ g_strfreev(certs_strv);
+ }
+ if (smartcard_db)
+ g_object_set(session, "smartcard-db", smartcard_db, NULL);
+ }
+ if (usbredir_auto_redirect_filter) {
+ SpiceUsbDeviceManager *m = spice_usb_device_manager_get(session, NULL);
+ if (m)
+ g_object_set(m, "auto-connect-filter",
+ usbredir_auto_redirect_filter, NULL);
+ }
+ if (usbredir_redirect_on_connect) {
+ SpiceUsbDeviceManager *m = spice_usb_device_manager_get(session, NULL);
+ if (m)
+ g_object_set(m, "redirect-on-connect",
+ usbredir_redirect_on_connect, NULL);
+ }
+ if (disable_usbredir)
+ g_object_set(session, "enable-usbredir", FALSE, NULL);
+ if (disable_audio)
+ g_object_set(session, "enable-audio", FALSE, NULL);
+ if (cache_size)
+ g_object_set(session, "cache-size", cache_size, NULL);
+ if (glz_window_size)
+ g_object_set(session, "glz-window-size", glz_window_size, NULL);
+ if (shared_dir)
+ g_object_set(session, "shared-dir", shared_dir, NULL);
+}
diff --git a/src/spice-option.h b/src/spice-option.h
new file mode 100644
index 0000000..ce24f65
--- /dev/null
+++ b/src/spice-option.h
@@ -0,0 +1,31 @@
+/* -*- 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_OPTION_H
+#define SPICE_OPTION_H
+
+#include <glib.h>
+#include "spice-session.h"
+
+G_BEGIN_DECLS
+
+GOptionGroup* spice_get_option_group(void);
+void spice_set_session_option(SpiceSession *session);
+
+G_END_DECLS
+
+#endif /* SPICE_OPTION_H */
diff --git a/src/spice-pulse.c b/src/spice-pulse.c
new file mode 100644
index 0000000..22db893
--- /dev/null
+++ b/src/spice-pulse.c
@@ -0,0 +1,1354 @@
+/* -*- 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-pulse.h"
+#include "spice-common.h"
+#include "spice-session-priv.h"
+#include "spice-channel-priv.h"
+#include "spice-util-priv.h"
+#include "glib-compat.h"
+
+#include <pulse/glib-mainloop.h>
+#include <pulse/pulseaudio.h>
+#include <pulse/ext-stream-restore.h>
+
+#define SPICE_PULSE_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_PULSE, SpicePulsePrivate))
+
+struct async_task {
+ SpicePulse *pulse;
+ SpiceMainChannel *main_channel;
+ GSimpleAsyncResult *res;
+ GAsyncReadyCallback callback;
+ gpointer user_data;
+ gboolean is_playback;
+ pa_operation *pa_op;
+ gulong cancel_id;
+ GCancellable *cancellable;
+};
+
+struct stream {
+ pa_sample_spec spec;
+ pa_stream *stream;
+ int state;
+ pa_operation *uncork_op;
+ pa_operation *cork_op;
+ gboolean started;
+ guint num_underflow;
+ gboolean info_updated;
+ gchar *name;
+ pa_ext_stream_restore_info info;
+};
+
+struct _SpicePulsePrivate {
+ SpiceChannel *pchannel;
+ SpiceChannel *rchannel;
+
+ pa_glib_mainloop *mainloop;
+ pa_context *context;
+ int state;
+ struct stream playback;
+ struct stream record;
+ guint last_delay;
+ guint target_delay;
+ struct async_task *pending_restore_task;
+ GList *results;
+};
+
+G_DEFINE_TYPE(SpicePulse, spice_pulse, SPICE_TYPE_AUDIO)
+
+static const char *stream_state_names[] = {
+ [ PA_STREAM_UNCONNECTED ] = "unconnected",
+ [ PA_STREAM_CREATING ] = "creating",
+ [ PA_STREAM_READY ] = "ready",
+ [ PA_STREAM_FAILED ] = "failed",
+ [ PA_STREAM_TERMINATED ] = "terminated",
+};
+
+static const char *context_state_names[] = {
+ [ PA_CONTEXT_UNCONNECTED ] = "unconnected",
+ [ PA_CONTEXT_CONNECTING ] = "connecting",
+ [ PA_CONTEXT_AUTHORIZING ] = "authorizing",
+ [ PA_CONTEXT_SETTING_NAME ] = "setting_name",
+ [ PA_CONTEXT_READY ] = "ready",
+ [ PA_CONTEXT_FAILED ] = "failed",
+ [ PA_CONTEXT_TERMINATED ] = "terminated",
+};
+#define STATE_NAME(array, state) \
+ ((state < G_N_ELEMENTS(array)) ? array[state] : NULL)
+
+static void stream_stop(SpicePulse *pulse, struct stream *s);
+static gboolean connect_channel(SpiceAudio *audio, SpiceChannel *channel);
+static void channel_weak_notified(gpointer data, GObject *where_the_object_was);
+static void spice_pulse_get_playback_volume_info_async(SpiceAudio *audio, GCancellable *cancellable,
+ SpiceMainChannel *main_channel, GAsyncReadyCallback callback, gpointer user_data);
+static gboolean spice_pulse_get_playback_volume_info_finish(SpiceAudio *audio, GAsyncResult *res,
+ gboolean *mute, guint8 *nchannels, guint16 **volume, GError **error);
+static void spice_pulse_get_record_volume_info_async(SpiceAudio *audio, GCancellable *cancellable,
+ SpiceMainChannel *main_channel, GAsyncReadyCallback callback, gpointer user_data);
+static gboolean spice_pulse_get_record_volume_info_finish(SpiceAudio *audio,GAsyncResult *res,
+ gboolean *mute, guint8 *nchannels, guint16 **volume, GError **error);
+static void stream_restore_read_cb(pa_context *context,
+ const pa_ext_stream_restore_info *info, int eol, void *userdata);
+static void spice_pulse_complete_async_task(struct async_task *task, const gchar *err_msg);
+static void spice_pulse_complete_all_async_tasks(SpicePulse *pulse, const gchar *err_msg);
+
+static void spice_pulse_finalize(GObject *obj)
+{
+ SpicePulse *pulse = SPICE_PULSE(obj);
+ SpicePulsePrivate *p;
+
+ p = pulse->priv;
+
+ if (p->context != NULL)
+ pa_context_unref(p->context);
+
+ if (p->mainloop != NULL)
+ pa_glib_mainloop_free(p->mainloop);
+
+ G_OBJECT_CLASS(spice_pulse_parent_class)->finalize(obj);
+}
+
+static void spice_pulse_dispose(GObject *obj)
+{
+ SpicePulse *pulse = SPICE_PULSE(obj);
+ SpicePulsePrivate *p;
+
+ SPICE_DEBUG("%s", __FUNCTION__);
+ p = pulse->priv;
+
+ if (p->playback.uncork_op)
+ pa_operation_unref(p->playback.uncork_op);
+ p->playback.uncork_op = NULL;
+
+ if (p->playback.cork_op)
+ pa_operation_unref(p->playback.cork_op);
+ p->playback.cork_op = NULL;
+
+ if (p->record.uncork_op)
+ pa_operation_unref(p->record.uncork_op);
+ p->record.uncork_op = NULL;
+
+ if (p->record.cork_op)
+ pa_operation_unref(p->record.cork_op);
+ p->record.cork_op = NULL;
+
+ if (p->results != NULL)
+ spice_pulse_complete_all_async_tasks(pulse, "PulseAudio is being dispose");
+
+ g_clear_pointer(&p->playback.name, g_free);
+ g_clear_pointer(&p->record.name, g_free);
+
+ if (p->pchannel)
+ g_object_weak_unref(G_OBJECT(p->pchannel), channel_weak_notified, pulse);
+ p->pchannel = NULL;
+
+ if (p->rchannel)
+ g_object_weak_unref(G_OBJECT(p->rchannel), channel_weak_notified, pulse);
+ p->rchannel = NULL;
+
+ G_OBJECT_CLASS(spice_pulse_parent_class)->dispose(obj);
+}
+
+static void spice_pulse_init(SpicePulse *pulse)
+{
+ pulse->priv = SPICE_PULSE_GET_PRIVATE(pulse);
+}
+
+static void spice_pulse_class_init(SpicePulseClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ SpiceAudioClass *audio_class = SPICE_AUDIO_CLASS(klass);
+
+ audio_class->connect_channel = connect_channel;
+ audio_class->get_playback_volume_info_async = spice_pulse_get_playback_volume_info_async;
+ audio_class->get_playback_volume_info_finish = spice_pulse_get_playback_volume_info_finish;
+ audio_class->get_record_volume_info_async = spice_pulse_get_record_volume_info_async;
+ audio_class->get_record_volume_info_finish = spice_pulse_get_record_volume_info_finish;
+
+ gobject_class->finalize = spice_pulse_finalize;
+ gobject_class->dispose = spice_pulse_dispose;
+
+ g_type_class_add_private(klass, sizeof(SpicePulsePrivate));
+}
+
+/* ------------------------------------------------------------------ */
+static void pulse_uncork_cb(pa_stream *pastream, int success, void *data)
+{
+ struct stream *s = data;
+
+ if (!success)
+ g_warning("pulseaudio uncork operation failed");
+
+ pa_operation_unref(s->uncork_op);
+ s->uncork_op = NULL;
+}
+
+static void stream_uncork(SpicePulse *pulse, struct stream *s)
+{
+ SpicePulsePrivate *p = pulse->priv;
+ pa_operation *o = NULL;
+
+ g_return_if_fail(s->stream);
+
+ if (s->cork_op) {
+ pa_operation_cancel(s->cork_op);
+ pa_operation_unref(s->cork_op);
+ s->cork_op = NULL;
+ }
+
+ if (pa_stream_is_corked(s->stream) && !s->uncork_op) {
+ if (!(o = pa_stream_cork(s->stream, 0, pulse_uncork_cb, s))) {
+ g_warning("pa_stream_uncork() failed: %s",
+ pa_strerror(pa_context_errno(p->context)));
+ }
+ s->uncork_op = o;
+ }
+}
+
+static void pulse_flush_cb(pa_stream *pastream, int success, void *data)
+{
+ struct stream *s = data;
+
+ if (!success)
+ g_warning("pulseaudio flush operation failed");
+
+ pa_operation_unref(s->cork_op);
+ s->cork_op = NULL;
+}
+
+static void pulse_cork_flush_cb(pa_stream *pastream, int success, void *data)
+{
+ struct stream *s = data;
+
+ if (!success)
+ g_warning("pulseaudio cork operation failed");
+
+ pa_operation_unref(s->cork_op);
+
+ if (!(s->cork_op = pa_stream_flush(s->stream, pulse_flush_cb, s))) {
+ g_warning("pa_stream_flush() failed");
+ }
+}
+
+static void pulse_cork_cb(pa_stream *pastream, int success, void *data)
+{
+ struct stream *s = data;
+
+ SPICE_DEBUG("%s: cork started", __FUNCTION__);
+ if (!success)
+ g_warning("pulseaudio cork operation failed");
+
+ pa_operation_unref(s->cork_op);
+ s->cork_op = NULL;
+}
+
+static void stream_cork(SpicePulse *pulse, struct stream *s, gboolean with_flush)
+{
+ SpicePulsePrivate *p = pulse->priv;
+ pa_operation *o = NULL;
+
+ if (s->uncork_op) {
+ pa_operation_cancel(s->uncork_op);
+ pa_operation_unref(s->uncork_op);
+ s->uncork_op = NULL;
+ }
+
+ if (!pa_stream_is_corked(s->stream) && !s->cork_op) {
+ if (!(o = pa_stream_cork(s->stream, 1,
+ with_flush ? pulse_cork_flush_cb :
+ pulse_cork_cb,
+ s))) {
+ g_warning("pa_stream_cork() failed: %s",
+ pa_strerror(pa_context_errno(p->context)));
+ }
+ s->cork_op = o;
+ }
+}
+
+static void stream_stop(SpicePulse *pulse, struct stream *s)
+{
+ SpicePulsePrivate *p = pulse->priv;
+
+ if (pa_stream_disconnect(s->stream) < 0) {
+ g_warning("pa_stream_disconnect() failed: %s",
+ pa_strerror(pa_context_errno(p->context)));
+ }
+ pa_stream_unref(s->stream);
+ s->stream = NULL;
+}
+
+static void stream_state_callback(pa_stream *s, void *userdata)
+{
+ SpicePulse *pulse = userdata;
+ SpicePulsePrivate *p;
+
+ p = pulse->priv;
+
+ g_return_if_fail(p != NULL);
+ g_return_if_fail(s != NULL);
+
+ switch (pa_stream_get_state(s)) {
+ case PA_STREAM_CREATING:
+ case PA_STREAM_TERMINATED:
+ case PA_STREAM_READY:
+ break;
+ case PA_STREAM_FAILED:
+ default:
+ g_warning("Stream error: %s", pa_strerror(pa_context_errno(pa_stream_get_context(s))));
+ }
+}
+
+static void stream_underflow_cb(pa_stream *s, void *userdata)
+{
+ SpicePulse *pulse = userdata;
+ SpicePulsePrivate *p;
+
+ SPICE_DEBUG("PA stream underflow!!");
+
+ p = pulse->priv;
+ g_return_if_fail(p != NULL);
+ p->playback.num_underflow++;
+#ifdef PULSE_ADJUST_LATENCY
+ const pa_buffer_attr *buffer_attr;
+ pa_buffer_attr new_buffer_attr;
+ pa_operation *op;
+
+ buffer_attr = pa_stream_get_buffer_attr(s);
+ g_return_if_fail(buffer_attr != NULL);
+
+ new_buffer_attr = *buffer_attr;
+ new_buffer_attr.tlength *= 2;
+ new_buffer_attr.minreq *= 2;
+ op = pa_stream_set_buffer_attr(s, &new_buffer_attr, NULL, NULL);
+ pa_operation_unref(op);
+#endif
+}
+
+static void stream_update_latency_callback(pa_stream *s, void *userdata)
+{
+ SpicePulse *pulse = userdata;
+ pa_usec_t usec;
+ int negative = 0;
+ SpicePulsePrivate *p;
+
+ p = pulse->priv;
+
+ g_return_if_fail(s != NULL);
+ g_return_if_fail(p != NULL);
+
+ if (!p->playback.stream || !p->playback.started)
+ return;
+
+ if (pa_stream_get_latency(s, &usec, &negative) < 0) {
+ g_warning("Failed to get latency: %s", pa_strerror(pa_context_errno(p->context)));
+ return;
+ }
+
+ g_return_if_fail(negative == FALSE);
+ p->last_delay = usec / PA_USEC_PER_MSEC;
+ spice_playback_channel_set_delay(SPICE_PLAYBACK_CHANNEL(p->pchannel), usec / 1000);
+ if (pa_stream_is_corked(p->playback.stream)) {
+ if (p->last_delay >= p->target_delay) {
+ SPICE_DEBUG("%s: uncork playback. delay %u target %u", __FUNCTION__, p->last_delay, p->target_delay);
+ stream_uncork(pulse, &p->playback);
+ } else {
+ SPICE_DEBUG("%s: still corked. delay %u target %u", __FUNCTION__, p->last_delay, p->target_delay);
+ }
+ }
+}
+
+static void create_playback(SpicePulse *pulse)
+{
+ SpicePulsePrivate *p = pulse->priv;
+ pa_stream_flags_t flags;
+ pa_buffer_attr buffer_attr = { 0, };
+
+ g_return_if_fail(p != NULL);
+ g_return_if_fail(p->context != NULL);
+ g_return_if_fail(p->playback.stream == NULL);
+ g_return_if_fail(pa_context_get_state(p->context) == PA_CONTEXT_READY);
+
+ p->playback.state = PA_STREAM_READY;
+ p->playback.stream = pa_stream_new(p->context, "playback",
+ &p->playback.spec, NULL);
+ pa_stream_set_state_callback(p->playback.stream, stream_state_callback, pulse);
+ pa_stream_set_underflow_callback(p->playback.stream, stream_underflow_cb, pulse);
+ pa_stream_set_latency_update_callback(p->playback.stream, stream_update_latency_callback, pulse);
+
+ buffer_attr.maxlength = -1;
+ buffer_attr.tlength = pa_usec_to_bytes(p->target_delay * PA_USEC_PER_MSEC, &p->playback.spec);
+ buffer_attr.prebuf = -1;
+ buffer_attr.minreq = -1;
+ flags = PA_STREAM_ADJUST_LATENCY | PA_STREAM_AUTO_TIMING_UPDATE;
+
+ if (pa_stream_connect_playback(p->playback.stream,
+ NULL, &buffer_attr, flags, NULL, NULL) < 0) {
+ g_warning("pa_stream_connect_playback() failed: %s",
+ pa_strerror(pa_context_errno(p->context)));
+ }
+}
+
+static void playback_start(SpicePlaybackChannel *channel, gint format, gint channels,
+ gint frequency, gpointer data)
+{
+ SpicePulse *pulse = data;
+ SpicePulsePrivate *p = pulse->priv;
+ pa_context_state_t state;
+ guint latency;
+
+ g_return_if_fail(p != NULL);
+
+ p->playback.started = TRUE;
+ p->playback.num_underflow = 0;
+ g_object_get(p->pchannel, "min-latency", &latency, NULL);
+
+ if (p->playback.stream &&
+ (p->playback.spec.rate != frequency ||
+ p->playback.spec.channels != channels ||
+ p->target_delay != latency)) {
+ stream_stop(pulse, &p->playback);
+ }
+
+ g_return_if_fail(format == SPICE_AUDIO_FMT_S16);
+ p->playback.spec.format = PA_SAMPLE_S16LE;
+ p->playback.spec.rate = frequency;
+ p->playback.spec.channels = channels;
+ p->target_delay = latency;
+ p->last_delay = 0;
+
+ state = pa_context_get_state(p->context);
+ switch (state) {
+ case PA_CONTEXT_READY:
+ if (p->state != state) {
+ SPICE_DEBUG("%s: pulse context ready", __FUNCTION__);
+ }
+ if (p->playback.stream == NULL) {
+ create_playback(pulse);
+ } else
+ stream_uncork(pulse, &p->playback);
+ break;
+ default:
+ if (p->state != state) {
+ SPICE_DEBUG("%s: pulse context not ready (%s)",
+ __FUNCTION__, STATE_NAME(context_state_names, state));
+ }
+ break;
+ }
+ p->state = state;
+}
+
+static void playback_data(SpicePlaybackChannel *channel,
+ gpointer *audio, gint size,
+ gpointer data)
+{
+ SpicePulse *pulse = data;
+ SpicePulsePrivate *p = pulse->priv;
+ pa_stream_state_t state;
+
+ if (!p->playback.stream)
+ return;
+
+ state = pa_stream_get_state(p->playback.stream);
+ switch (state) {
+ case PA_STREAM_CREATING:
+ SPICE_DEBUG("stream creating, dropping data");
+ break;
+ case PA_STREAM_READY:
+ if (p->playback.state != state) {
+ SPICE_DEBUG("%s: pulse playback stream ready", __FUNCTION__);
+ }
+ if (pa_stream_write(p->playback.stream, audio, size, NULL, 0, PA_SEEK_RELATIVE) < 0) {
+ g_warning("pa_stream_write() failed: %s",
+ pa_strerror(pa_context_errno(p->context)));
+ }
+ break;
+ default:
+ if (p->playback.state != state) {
+ SPICE_DEBUG("%s: pulse playback stream not ready (%s)",
+ __FUNCTION__, STATE_NAME(stream_state_names, state));
+ }
+ break;
+ }
+ p->playback.state = state;
+}
+
+static void playback_stop(SpicePulse *pulse)
+{
+ SpicePulsePrivate *p = pulse->priv;
+
+ SPICE_DEBUG("%s: #underflow %u", __FUNCTION__, p->playback.num_underflow);
+
+ p->playback.started = FALSE;
+ if (!p->playback.stream)
+ return;
+
+ stream_cork(pulse, &p->playback, TRUE);
+}
+
+static void stream_read_callback(pa_stream *s, size_t length, void *data)
+{
+ SpicePulse *pulse = data;
+ SpicePulsePrivate *p = pulse->priv;
+
+ g_return_if_fail(p != NULL);
+
+ while (pa_stream_readable_size(s) > 0) {
+ const void *snddata;
+
+ if (pa_stream_peek(s, &snddata, &length) < 0) {
+ g_warning("pa_stream_peek() failed: %s",
+ pa_strerror(pa_context_errno(p->context)));
+ return;
+ }
+
+ g_return_if_fail(snddata);
+ g_return_if_fail(length > 0);
+
+ if (p->rchannel != NULL)
+ spice_record_send_data(SPICE_RECORD_CHANNEL(p->rchannel),
+ /* FIXME: server side doesn't care about ts?
+ what is the unit? ms apparently */
+ (gpointer)snddata, length, 0);
+
+ if (pa_stream_drop(s) < 0) {
+ g_warning("pa_stream_drop() failed: %s",
+ pa_strerror(pa_context_errno(p->context)));
+ return;
+ }
+ }
+}
+
+static void create_record(SpicePulse *pulse)
+{
+ SpicePulsePrivate *p = pulse->priv;
+ pa_buffer_attr buffer_attr = { 0, };
+ pa_stream_flags_t flags;
+
+ g_return_if_fail(p != NULL);
+ g_return_if_fail(p->context != NULL);
+ g_return_if_fail(p->record.stream == NULL);
+ g_return_if_fail(pa_context_get_state(p->context) == PA_CONTEXT_READY);
+
+ p->record.state = PA_STREAM_READY;
+ p->record.stream = pa_stream_new(p->context, "record",
+ &p->record.spec, NULL);
+ pa_stream_set_read_callback(p->record.stream, stream_read_callback, pulse);
+ pa_stream_set_state_callback(p->record.stream, stream_state_callback, pulse);
+
+ /* FIXME: we might want customizable latency */
+ buffer_attr.maxlength = -1;
+ buffer_attr.prebuf = -1;
+ buffer_attr.fragsize = buffer_attr.tlength = pa_usec_to_bytes(20 * PA_USEC_PER_MSEC, &p->record.spec);
+ buffer_attr.minreq = (uint32_t) -1;
+ flags = PA_STREAM_ADJUST_LATENCY;
+
+ if (pa_stream_connect_record(p->record.stream, NULL, &buffer_attr, flags) < 0) {
+ g_warning("pa_stream_connect_record() failed: %s",
+ pa_strerror(pa_context_errno(p->context)));
+ }
+}
+
+static void record_start(SpiceRecordChannel *channel, gint format, gint channels,
+ gint frequency, gpointer data)
+{
+ SpicePulse *pulse = data;
+ SpicePulsePrivate *p = pulse->priv;
+ pa_context_state_t state;
+
+ p->record.started = TRUE;
+
+ if (p->record.stream &&
+ (p->record.spec.rate != frequency ||
+ p->record.spec.channels != channels)) {
+ stream_stop(pulse, &p->record);
+ }
+
+ g_return_if_fail(format == SPICE_AUDIO_FMT_S16);
+ p->record.spec.format = PA_SAMPLE_S16LE;
+ p->record.spec.rate = frequency;
+ p->record.spec.channels = channels;
+
+ state = pa_context_get_state(p->context);
+ switch (state) {
+ case PA_CONTEXT_READY:
+ if (p->state != state) {
+ SPICE_DEBUG("%s: pulse context ready", __FUNCTION__);
+ }
+ if (p->record.stream == NULL) {
+ create_record(pulse);
+ } else
+ stream_uncork(pulse, &p->record);
+ break;
+ default:
+ if (p->state != state) {
+ g_warning("%s: pulse context not ready (%s)",
+ __FUNCTION__, STATE_NAME(context_state_names, state));
+ }
+ break;
+ }
+ p->state = state;
+}
+
+static void record_stop(SpicePulse *pulse)
+{
+ SpicePulsePrivate *p = pulse->priv;
+
+ SPICE_DEBUG("%s", __FUNCTION__);
+
+ p->record.started = FALSE;
+ if (!p->record.stream)
+ return;
+
+ stream_stop(pulse, &p->record);
+}
+
+static void playback_volume_changed(GObject *object, GParamSpec *pspec, gpointer data)
+{
+ SpicePulse *pulse = data;
+ SpicePulsePrivate *p = pulse->priv;
+ guint16 *volume;
+ guint nchannels;
+ pa_operation *op;
+ pa_cvolume v;
+ guint i;
+
+ g_object_get(object,
+ "volume", &volume,
+ "nchannels", &nchannels,
+ NULL);
+
+ pa_cvolume_init(&v);
+ v.channels = p->playback.spec.channels;
+ for (i = 0; i < nchannels; ++i) {
+ v.values[i] = (PA_VOLUME_NORM - PA_VOLUME_MUTED) * volume[i] / G_MAXUINT16;
+ SPICE_DEBUG("playback volume changed %u", v.values[i]);
+ }
+
+ if (!p->playback.stream ||
+ pa_stream_get_index(p->playback.stream) == PA_INVALID_INDEX)
+ return;
+
+ op = pa_context_set_sink_input_volume(p->context,
+ pa_stream_get_index(p->playback.stream),
+ &v, NULL, NULL);
+ if (!op)
+ g_warning("set_sink_input_volume() failed: %s",
+ pa_strerror(pa_context_errno(p->context)));
+ else
+ pa_operation_unref(op);
+}
+
+static void playback_mute_changed(GObject *object, GParamSpec *pspec, gpointer data)
+{
+ SpicePulse *pulse = data;
+ SpicePulsePrivate *p = pulse->priv;
+ gboolean mute;
+ pa_operation *op;
+
+ g_object_get(object, "mute", &mute, NULL);
+ SPICE_DEBUG("playback mute changed %u", mute);
+
+ if (!p->playback.stream ||
+ pa_stream_get_index(p->playback.stream) == PA_INVALID_INDEX)
+ return;
+
+ op = pa_context_set_sink_input_mute(p->context,
+ pa_stream_get_index(p->playback.stream),
+ mute, NULL, NULL);
+ if (!op)
+ g_warning("set_sink_input_mute() failed: %s",
+ pa_strerror(pa_context_errno(p->context)));
+ else
+ pa_operation_unref(op);
+}
+
+static void playback_min_latency_changed(GObject *object, GParamSpec *pspec, gpointer data)
+{
+
+ SpicePulse *pulse = data;
+ SpicePulsePrivate *p = pulse->priv;
+ guint min_latency;
+
+ g_object_get(object, "min-latency", &min_latency, NULL);
+ p->target_delay = min_latency;
+
+ if (p->last_delay < p->target_delay) {
+ spice_debug("%s: corking", __FUNCTION__);
+ if (p->playback.stream)
+ stream_cork(pulse, &p->playback, FALSE);
+ } else {
+ spice_debug("%s: not corking. The current delay satisfies the requirement", __FUNCTION__);
+ }
+}
+
+static void record_mute_changed(GObject *object, GParamSpec *pspec, gpointer data)
+{
+ SpicePulse *pulse = data;
+ SpicePulsePrivate *p = pulse->priv;
+ gboolean mute;
+ pa_operation *op;
+
+ g_object_get(object, "mute", &mute, NULL);
+ SPICE_DEBUG("record mute changed %u", mute);
+
+ if (!p->record.stream ||
+ pa_stream_get_device_index(p->record.stream) == PA_INVALID_INDEX)
+ return;
+
+#if PA_CHECK_VERSION(1,0,0)
+ op = pa_context_set_source_output_mute(p->context,
+ pa_stream_get_index(p->record.stream),
+#else
+ op = pa_context_set_source_mute_by_index(p->context,
+ pa_stream_get_device_index(p->record.stream),
+#endif
+ mute, NULL, NULL);
+ if (!op)
+ g_warning("set_source_output_mute() failed: %s",
+ pa_strerror(pa_context_errno(p->context)));
+ else
+ pa_operation_unref(op);
+}
+
+static void record_volume_changed(GObject *object, GParamSpec *pspec, gpointer data)
+{
+ SpicePulse *pulse = data;
+ SpicePulsePrivate *p = pulse->priv;
+ guint16 *volume;
+ guint nchannels;
+ pa_operation *op;
+ pa_cvolume v;
+ guint i;
+
+ g_object_get(object,
+ "volume", &volume,
+ "nchannels", &nchannels,
+ NULL);
+
+ pa_cvolume_init(&v);
+ v.channels = p->record.spec.channels;
+ for (i = 0; i < nchannels; ++i) {
+ v.values[i] = (PA_VOLUME_NORM - PA_VOLUME_MUTED) * volume[i] / G_MAXUINT16;
+ SPICE_DEBUG("record volume changed %u", v.values[i]);
+ }
+
+ if (!p->record.stream ||
+ pa_stream_get_device_index(p->record.stream) == PA_INVALID_INDEX)
+ return;
+
+#if PA_CHECK_VERSION(1,0,0)
+ op = pa_context_set_source_output_volume(p->context,
+ pa_stream_get_index(p->record.stream),
+#else
+ op = pa_context_set_source_volume_by_index(p->context,
+ pa_stream_get_device_index(p->record.stream),
+#endif
+ &v, NULL, NULL);
+ if (!op)
+ g_warning("set_source_output_volume() failed: %s",
+ pa_strerror(pa_context_errno(p->context)));
+ else
+ pa_operation_unref(op);
+}
+
+static void
+channel_weak_notified(gpointer data,
+ GObject *where_the_object_was)
+{
+ SpicePulse *pulse = SPICE_PULSE(data);
+ SpicePulsePrivate *p = pulse->priv;
+
+ if (where_the_object_was == (GObject *)p->pchannel) {
+ SPICE_DEBUG("playback closed");
+ playback_stop(pulse);
+ p->pchannel = NULL;
+ } else if (where_the_object_was == (GObject *)p->rchannel) {
+ SPICE_DEBUG("record closed");
+ record_stop(pulse);
+ p->rchannel = NULL;
+ }
+}
+
+static gboolean connect_channel(SpiceAudio *audio, SpiceChannel *channel)
+{
+ SpicePulse *pulse = SPICE_PULSE(audio);
+ SpicePulsePrivate *p = pulse->priv;
+
+ if (SPICE_IS_PLAYBACK_CHANNEL(channel)) {
+ g_return_val_if_fail(p->pchannel == NULL, FALSE);
+
+ p->pchannel = channel;
+ g_object_weak_ref(G_OBJECT(p->pchannel), channel_weak_notified, audio);
+ spice_g_signal_connect_object(channel, "playback-start",
+ G_CALLBACK(playback_start), pulse, 0);
+ spice_g_signal_connect_object(channel, "playback-data",
+ G_CALLBACK(playback_data), pulse, 0);
+ spice_g_signal_connect_object(channel, "playback-stop",
+ G_CALLBACK(playback_stop), pulse, G_CONNECT_SWAPPED);
+ spice_g_signal_connect_object(channel, "notify::volume",
+ G_CALLBACK(playback_volume_changed), pulse, 0);
+ spice_g_signal_connect_object(channel, "notify::mute",
+ G_CALLBACK(playback_mute_changed), pulse, 0);
+ spice_g_signal_connect_object(channel, "notify::min-latency",
+ G_CALLBACK(playback_min_latency_changed), pulse, 0);
+
+ return TRUE;
+ }
+
+ if (SPICE_IS_RECORD_CHANNEL(channel)) {
+ g_return_val_if_fail(p->rchannel == NULL, FALSE);
+
+ p->rchannel = channel;
+ g_object_weak_ref(G_OBJECT(p->rchannel), channel_weak_notified, audio);
+ spice_g_signal_connect_object(channel, "record-start",
+ G_CALLBACK(record_start), pulse, 0);
+ spice_g_signal_connect_object(channel, "record-stop",
+ G_CALLBACK(record_stop), pulse, G_CONNECT_SWAPPED);
+ spice_g_signal_connect_object(channel, "notify::volume",
+ G_CALLBACK(record_volume_changed), pulse, 0);
+ spice_g_signal_connect_object(channel, "notify::mute",
+ G_CALLBACK(record_mute_changed), pulse, 0);
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void context_state_callback(pa_context *c, void *userdata)
+{
+ SpicePulse *pulse = userdata;
+ SpicePulsePrivate *p;
+
+ p = pulse->priv;
+
+ g_return_if_fail(p != NULL);
+ g_return_if_fail(c != NULL);
+ switch (pa_context_get_state(c)) {
+ case PA_CONTEXT_CONNECTING:
+ case PA_CONTEXT_AUTHORIZING:
+ case PA_CONTEXT_SETTING_NAME:
+ case PA_CONTEXT_UNCONNECTED:
+ break;
+
+ case PA_CONTEXT_READY: {
+ if (!p->record.stream && p->record.started)
+ create_record(SPICE_PULSE(userdata));
+
+ if (!p->playback.stream && p->playback.started)
+ create_playback(SPICE_PULSE(userdata));
+
+ if (p->pending_restore_task != NULL &&
+ p->pending_restore_task->pa_op == NULL) {
+ pa_operation *op = pa_ext_stream_restore_read(p->context,
+ stream_restore_read_cb,
+ pulse);
+ if (!op)
+ goto context_fail;
+ p->pending_restore_task->pa_op = op;
+ }
+ break;
+ }
+
+ case PA_CONTEXT_FAILED:
+ g_warning("PulseAudio context failed %s",
+ pa_strerror(pa_context_errno(p->context)));
+ goto context_fail;
+
+ case PA_CONTEXT_TERMINATED:
+ default:
+ SPICE_DEBUG("PulseAudio context terminated");
+ goto context_fail;
+ }
+
+ return;
+
+context_fail:
+ if (p->pending_restore_task != NULL) {
+ const gchar *errmsg = pa_strerror(pa_context_errno(p->context));
+ errmsg = (errmsg != NULL) ? errmsg : "PulseAudio context terminated";
+ spice_pulse_complete_all_async_tasks(pulse, errmsg);
+ }
+}
+
+SpicePulse *spice_pulse_new(SpiceSession *session, GMainContext *context,
+ const char *name)
+{
+ SpicePulse *pulse;
+ SpicePulsePrivate *p;
+
+ pulse = g_object_new(SPICE_TYPE_PULSE,
+ "session", session,
+ "main-context", context,
+ NULL);
+ p = pulse->priv;
+
+ p->mainloop = pa_glib_mainloop_new(context);
+ p->state = PA_CONTEXT_READY;
+ p->context = pa_context_new(pa_glib_mainloop_get_api(p->mainloop), name);
+ pa_context_set_state_callback(p->context, context_state_callback, pulse);
+ if (pa_context_connect(p->context, NULL, 0, NULL) < 0) {
+ g_warning("pa_context_connect() failed: %s",
+ pa_strerror(pa_context_errno(p->context)));
+ goto error;
+ }
+
+ p->playback.name = g_strconcat("sink-input-by-application-name:",
+ g_get_application_name(), NULL);
+ p->record.name = g_strconcat("source-output-by-application-name:",
+ g_get_application_name(), NULL);
+ return pulse;
+
+error:
+ g_object_unref(pulse);
+ return NULL;
+}
+
+static gboolean free_async_task(gpointer user_data)
+{
+ struct async_task *task = user_data;
+
+ if (task == NULL)
+ return G_SOURCE_REMOVE;
+
+ if (task->pa_op != NULL) {
+ pa_operation_cancel(task->pa_op);
+ pa_operation_unref(task->pa_op);
+ task->pa_op = NULL;
+ }
+
+ if (task->pulse) {
+ if (task->pulse->priv->pending_restore_task == task) {
+ task->pulse->priv->pending_restore_task = NULL;
+ }
+ g_object_unref(task->pulse);
+ }
+
+ if (task->res)
+ g_object_unref(task->res);
+
+ if (task->main_channel)
+ g_object_unref(task->main_channel);
+
+ if (task->pa_op != NULL)
+ pa_operation_unref(task->pa_op);
+
+ if (task->cancel_id != 0) {
+ g_cancellable_disconnect(task->cancellable, task->cancel_id);
+ g_clear_object(&task->cancellable);
+ }
+
+ g_free(task);
+ return G_SOURCE_REMOVE;
+}
+
+static void cancel_task(GCancellable *cancellable, gpointer user_data)
+{
+ struct async_task *task = user_data;
+ g_return_if_fail(task != NULL);
+
+#if GLIB_CHECK_VERSION(2,40,0)
+ free_async_task(task);
+#else
+ /* This must be done now otherwise pulseaudio may return to a
+ * cancelled task operation before free_async_task is called */
+ if (task->pa_op != NULL) {
+ pa_operation_cancel(task->pa_op);
+ pa_operation_unref(task->pa_op);
+ task->pa_op = NULL;
+ }
+
+ /* Clear the pending_restore_task reference to avoid triggering a
+ * pa_operation when context state is in READY state */
+ if (task->pulse->priv->pending_restore_task == task) {
+ task->pulse->priv->pending_restore_task = NULL;
+ }
+
+#if !GLIB_CHECK_VERSION(2,32,0)
+ /* g_simple_async_result_set_check_cancellable is not present. Set an error
+ * in the GSimpleAsyncResult in case of _finish functions is called */
+ g_simple_async_result_set_error(task->res,
+ SPICE_CLIENT_ERROR,
+ SPICE_CLIENT_ERROR_FAILED,
+ "Operation was cancelled");
+#endif
+ /* FIXME: https://bugzilla.gnome.org/show_bug.cgi?id=705395
+ * Free the memory in idle */
+ g_idle_add(free_async_task, task);
+#endif
+}
+
+static void complete_task(SpicePulse *pulse, struct async_task *task, const gchar *err_msg)
+{
+ SpicePulsePrivate *p = pulse->priv;
+
+ /* If we do have any err_msg, we failed */
+ if (err_msg != NULL) {
+ g_simple_async_result_set_op_res_gboolean(task->res, FALSE);
+ g_simple_async_result_set_error(task->res,
+ SPICE_CLIENT_ERROR,
+ SPICE_CLIENT_ERROR_FAILED,
+ "restore-info failed due %s",
+ err_msg);
+ /* Volume-info does not change if stream is not found */
+ } else if ((task->is_playback == TRUE && p->playback.info_updated == FALSE) ||
+ (task->is_playback == FALSE && p->record.info_updated == FALSE)) {
+ g_simple_async_result_set_op_res_gboolean(task->res, FALSE);
+ g_simple_async_result_set_error(task->res,
+ SPICE_CLIENT_ERROR,
+ SPICE_CLIENT_ERROR_FAILED,
+ "Stream not found by pulse");
+ } else {
+ g_simple_async_result_set_op_res_gboolean(task->res, TRUE);
+ }
+
+ /* As all async calls to PulseAudio are done with glib mainloop, it is
+ * safe to complete the operation synchronously here. */
+ g_simple_async_result_complete(task->res);
+}
+
+static void spice_pulse_complete_async_task(struct async_task *task, const gchar *err_msg)
+{
+ SpicePulsePrivate *p;
+
+ g_return_if_fail(task != NULL);
+ p = task->pulse->priv;
+
+ complete_task(task->pulse, task, err_msg);
+ if (p->results != NULL) {
+ p->results = g_list_remove(p->results, task);
+ SPICE_DEBUG("Number of async task is %d", g_list_length(p->results));
+ }
+ free_async_task(task);
+}
+
+static void spice_pulse_complete_all_async_tasks(SpicePulse *pulse, const gchar *err_msg)
+{
+ SpicePulsePrivate *p;
+ GList *it;
+
+ g_return_if_fail(pulse != NULL);
+ p = pulse->priv;
+
+ /* Complete all tasks in list */
+ for(it = p->results; it != NULL; it = it->next) {
+ struct async_task *task = it->data;
+ complete_task(pulse, task, err_msg);
+ free_async_task(task);
+ }
+ g_list_free(p->results);
+ p->results = NULL;
+ SPICE_DEBUG("All async tasks completed");
+}
+
+static void stream_restore_read_cb(pa_context *context,
+ const pa_ext_stream_restore_info *info,
+ int eol,
+ void *userdata)
+{
+ SpicePulsePrivate *p = SPICE_PULSE(userdata)->priv;
+ struct stream *pstream = NULL;
+
+ if (eol ||
+ (p->playback.info_updated == TRUE &&
+ p->record.info_updated == TRUE)) {
+ /* We only have one pa_operation running the stream-restore-info
+ * which retrieves volume-info from both Playback and Record channels;
+ * We can complete all async tasks now that this operation ended.
+ * (or we already have the volume-info we want)
+ * Note: the following function cancel the current pa_operation */
+ spice_pulse_complete_all_async_tasks(SPICE_PULSE(userdata), NULL);
+ return;
+ }
+
+ if (g_strcmp0(info->name, p->playback.name) == 0) {
+ pstream = &p->playback;
+ } else if (g_strcmp0(info->name, p->record.name) == 0) {
+ pstream = &p->record;
+ } else {
+ /* This is not the stream you are looking for. */
+ return;
+ }
+
+ if (info->channel_map.channels == 0) {
+ SPICE_DEBUG("Number of channels stored is zero. Ignore. (%s)", info->name);
+ return;
+ }
+
+ pstream->info_updated = TRUE;
+ pstream->info.name = pstream->name;
+ pstream->info.mute = info->mute;
+ pstream->info.channel_map = info->channel_map;
+ pstream->info.volume = info->volume;
+}
+
+#if PA_CHECK_VERSION(1,0,0)
+static void source_output_info_cb(pa_context *context,
+ const pa_source_output_info *info,
+ int eol,
+ void *userdata)
+#else
+static void source_info_cb(pa_context *context,
+ const pa_source_info *info,
+ int eol,
+ void *userdata)
+#endif
+{
+ struct async_task *task = userdata;
+ SpicePulsePrivate *p = task->pulse->priv;
+ struct stream *pstream = &p->record;
+
+ if (eol) {
+ spice_pulse_complete_async_task(task, NULL);
+ return;
+ }
+
+ pstream->info_updated = TRUE;
+ pstream->info.name = pstream->name;
+ pstream->info.mute = info->mute;
+ pstream->info.channel_map = info->channel_map;
+ pstream->info.volume = info->volume;
+}
+
+static void sink_input_info_cb(pa_context *context,
+ const pa_sink_input_info *info,
+ int eol,
+ void *userdata)
+{
+ struct async_task *task = userdata;
+ SpicePulsePrivate *p = task->pulse->priv;
+ struct stream *pstream = &p->playback;
+
+ if (eol) {
+ spice_pulse_complete_async_task(task, NULL);
+ return;
+ }
+
+ pstream->info_updated = TRUE;
+ pstream->info.name = pstream->name;
+ pstream->info.mute = info->mute;
+ pstream->info.channel_map = info->channel_map;
+ pstream->info.volume = info->volume;
+}
+
+/* to avoid code duplication */
+static void pulse_stream_restore_info_async(gboolean is_playback,
+ SpiceAudio *audio,
+ GCancellable *cancellable,
+ SpiceMainChannel *main_channel,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SpicePulsePrivate *p = SPICE_PULSE(audio)->priv;
+ GSimpleAsyncResult *simple;
+ struct async_task *task = g_malloc0(sizeof(struct async_task));
+ pa_operation *op = NULL;
+
+ simple = g_simple_async_result_new(G_OBJECT(audio),
+ callback,
+ user_data,
+ pulse_stream_restore_info_async);
+#if GLIB_CHECK_VERSION(2,32,0)
+ g_simple_async_result_set_check_cancellable (simple, cancellable);
+#endif
+
+ task->res = simple;
+ task->pulse = g_object_ref(audio);
+ task->callback = callback;
+ task->user_data = user_data;
+ task->is_playback = is_playback;
+ task->main_channel = g_object_ref(main_channel);
+ task->pa_op = NULL;
+
+ if (cancellable) {
+ task->cancellable = g_object_ref(cancellable);
+ task->cancel_id = g_cancellable_connect(cancellable, G_CALLBACK(cancel_task), task, NULL);
+ }
+
+ /* If Playback/Record stream is created we use pulse API to get volume-info
+ * from those streams directly. If the stream is not created, retrieve last
+ * volume/mute values from Pulse database using the application name;
+ * If we already have retrieved volume-info from Pulse database then it is
+ * safe to return the volume-info we already have in <stream>info */
+
+ if (is_playback == TRUE &&
+ p->playback.stream != NULL &&
+ pa_stream_get_index(p->playback.stream) != PA_INVALID_INDEX) {
+ SPICE_DEBUG("Playback stream is created - get-sink-input-info");
+ p->playback.info_updated = FALSE;
+ op = pa_context_get_sink_input_info(p->context,
+ pa_stream_get_index(p->playback.stream),
+ sink_input_info_cb,
+ task);
+ if (!op)
+ goto fail;
+ task->pa_op = op;
+
+ } else if (is_playback == FALSE &&
+ p->record.stream != NULL &&
+ pa_stream_get_index(p->record.stream) != PA_INVALID_INDEX) {
+ SPICE_DEBUG("Record stream is created - get-source-output-info");
+ p->record.info_updated = FALSE;
+#if PA_CHECK_VERSION(1,0,0)
+ op = pa_context_get_source_output_info(p->context,
+ pa_stream_get_index(p->record.stream),
+ source_output_info_cb,
+ task);
+#else
+ op = pa_context_get_source_info_by_index(p->context,
+ pa_stream_get_device_index(p->record.stream),
+ source_info_cb,
+ task);
+#endif
+ if (!op)
+ goto fail;
+ task->pa_op = op;
+
+ } else {
+ if (p->playback.info.name != NULL ||
+ p->record.info.name != NULL) {
+ /* If the pstream->info.name is set then we already have updated
+ * volume information. We can complete the request now */
+ SPICE_DEBUG("Return the volume-information we already have");
+ spice_pulse_complete_async_task(task, NULL);
+ return;
+ }
+
+ if (p->results == NULL) {
+ SPICE_DEBUG("Streams are not created - ext-stream-restore");
+ p->playback.info_updated = FALSE;
+ p->record.info_updated = FALSE;
+
+ if (pa_context_get_state(p->context) == PA_CONTEXT_READY) {
+ /* Restore value from pulse db */
+ op = pa_ext_stream_restore_read(p->context, stream_restore_read_cb, audio);
+ if (!op)
+ goto fail;
+ task->pa_op = op;
+ } else {
+ /* It is possible that we want to get volume-info before the
+ * context is in READY state. In this case, we wait for the
+ * context state change to READY. */
+ p->pending_restore_task = task;
+ }
+ }
+ }
+
+ p->results = g_list_append(p->results, task);
+ SPICE_DEBUG ("Number of async task is %d", g_list_length(p->results));
+ return;
+
+fail:
+ if (!op) {
+ g_simple_async_report_error_in_idle(G_OBJECT(audio),
+ callback,
+ user_data,
+ SPICE_CLIENT_ERROR,
+ SPICE_CLIENT_ERROR_FAILED,
+ "Volume-Info failed: %s",
+ pa_strerror(pa_context_errno(p->context)));
+ free_async_task(task);
+ }
+}
+
+/* to avoid code duplication */
+static gboolean pulse_stream_restore_info_finish(gboolean is_playback,
+ SpiceAudio *audio,
+ GAsyncResult *res,
+ gboolean *mute,
+ guint8 *nchannels,
+ guint16 **volume,
+ GError **error)
+{
+ SpicePulsePrivate *p = SPICE_PULSE(audio)->priv;
+ struct stream *pstream = (is_playback) ? &p->playback : &p->record;
+ GSimpleAsyncResult *simple = (GSimpleAsyncResult *) res;
+
+ g_return_val_if_fail(g_simple_async_result_is_valid(res,
+ G_OBJECT(audio), pulse_stream_restore_info_async), FALSE);
+
+ if (g_simple_async_result_propagate_error(simple, error)) {
+ /* set out args that should have new alloc'ed memory to NULL */
+ if (volume != NULL) {
+ *volume = NULL;
+ }
+ return FALSE;
+ }
+
+ if (mute != NULL) {
+ *mute = (pstream->info.mute) ? TRUE : FALSE;
+ }
+
+ if (nchannels != NULL) {
+ *nchannels = pstream->info.channel_map.channels;
+ }
+
+ if (volume != NULL) {
+ gint i;
+ *volume = g_new(guint16, pstream->info.channel_map.channels);
+ for (i = 0; i < pstream->info.channel_map.channels; i++) {
+ (*volume)[i] = MIN(pstream->info.volume.values[i], G_MAXUINT16);
+ SPICE_DEBUG("(%s) volume at channel %d is %u",
+ (is_playback) ? "playback" : "record", i, (*volume)[i]);
+ }
+ }
+
+ return g_simple_async_result_get_op_res_gboolean(simple);
+}
+
+static void spice_pulse_get_playback_volume_info_async(SpiceAudio *audio,
+ GCancellable *cancellable,
+ SpiceMainChannel *main_channel,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ pulse_stream_restore_info_async(TRUE, audio, cancellable, main_channel, callback, user_data);
+}
+
+static gboolean spice_pulse_get_playback_volume_info_finish(SpiceAudio *audio,
+ GAsyncResult *res,
+ gboolean *mute,
+ guint8 *nchannels,
+ guint16 **volume,
+ GError **error)
+{
+ return pulse_stream_restore_info_finish(TRUE, audio, res, mute,
+ nchannels, volume, error);
+}
+
+static void spice_pulse_get_record_volume_info_async(SpiceAudio *audio,
+ GCancellable *cancellable,
+ SpiceMainChannel *main_channel,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ pulse_stream_restore_info_async(FALSE, audio, cancellable, main_channel, callback, user_data);
+}
+
+static gboolean spice_pulse_get_record_volume_info_finish(SpiceAudio *audio,
+ GAsyncResult *res,
+ gboolean *mute,
+ guint8 *nchannels,
+ guint16 **volume,
+ GError **error)
+{
+ return pulse_stream_restore_info_finish(FALSE, audio, res, mute,
+ nchannels, volume, error);
+}
diff --git a/src/spice-pulse.h b/src/spice-pulse.h
new file mode 100644
index 0000000..819647e
--- /dev/null
+++ b/src/spice-pulse.h
@@ -0,0 +1,57 @@
+/* -*- 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_CLIENT_PULSE_H__
+#define __SPICE_CLIENT_PULSE_H__
+
+#include "spice-client.h"
+#include "spice-audio.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_PULSE (spice_pulse_get_type())
+#define SPICE_PULSE(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_PULSE, SpicePulse))
+#define SPICE_PULSE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_PULSE, SpicePulseClass))
+#define SPICE_IS_PULSE(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_PULSE))
+#define SPICE_IS_PULSE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_PULSE))
+#define SPICE_PULSE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_PULSE, SpicePulseClass))
+
+
+typedef struct _SpicePulse SpicePulse;
+typedef struct _SpicePulseClass SpicePulseClass;
+typedef struct _SpicePulsePrivate SpicePulsePrivate;
+
+struct _SpicePulse {
+ SpiceAudio parent;
+ SpicePulsePrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+struct _SpicePulseClass {
+ SpiceAudioClass parent_class;
+ /* Do not add fields to this struct */
+};
+
+GType spice_pulse_get_type(void);
+
+SpicePulse *spice_pulse_new(SpiceSession *session,
+ GMainContext *context,
+ const char *name);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_PULSE_H__ */
diff --git a/src/spice-session-priv.h b/src/spice-session-priv.h
new file mode 100644
index 0000000..049973a
--- /dev/null
+++ b/src/spice-session-priv.h
@@ -0,0 +1,104 @@
+/* -*- 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_CLIENT_SESSION_PRIV_H__
+#define __SPICE_CLIENT_SESSION_PRIV_H__
+
+#include "config.h"
+
+#include <glib.h>
+#include <gio/gio.h>
+
+#ifdef USE_PHODAV
+#include <libphodav/phodav.h>
+#else
+typedef struct _PhodavServer PhodavServer;
+#endif
+
+#include "desktop-integration.h"
+#include "spice-session.h"
+#include "spice-gtk-session.h"
+#include "spice-channel-cache.h"
+#include "decode.h"
+
+G_BEGIN_DECLS
+
+#define WEBDAV_MAGIC_SIZE 16
+
+SpiceSession *spice_session_new_from_session(SpiceSession *session);
+
+void spice_session_set_connection_id(SpiceSession *session, int id);
+int spice_session_get_connection_id(SpiceSession *session);
+gboolean spice_session_get_client_provided_socket(SpiceSession *session);
+
+GSocketConnection* spice_session_channel_open_host(SpiceSession *session, SpiceChannel *channel,
+ gboolean *use_tls, GError **error);
+void spice_session_channel_new(SpiceSession *session, SpiceChannel *channel);
+void spice_session_channel_migrate(SpiceSession *session, SpiceChannel *channel);
+
+void spice_session_set_mm_time(SpiceSession *session, guint32 time);
+guint32 spice_session_get_mm_time(SpiceSession *session);
+
+void spice_session_switching_disconnect(SpiceSession *session);
+void spice_session_start_migrating(SpiceSession *session,
+ gboolean full_migration);
+void spice_session_abort_migration(SpiceSession *session);
+void spice_session_set_migration_state(SpiceSession *session, SpiceSessionMigration state);
+
+void spice_session_set_port(SpiceSession *session, int port, gboolean tls);
+void spice_session_get_pubkey(SpiceSession *session, guint8 **pubkey, guint *size);
+guint spice_session_get_verify(SpiceSession *session);
+const gchar* spice_session_get_username(SpiceSession *session);
+const gchar* spice_session_get_password(SpiceSession *session);
+const gchar* spice_session_get_host(SpiceSession *session);
+const gchar* spice_session_get_cert_subject(SpiceSession *session);
+const gchar* spice_session_get_ciphers(SpiceSession *session);
+const gchar* spice_session_get_ca_file(SpiceSession *session);
+void spice_session_get_ca(SpiceSession *session, guint8 **ca, guint *size);
+
+void spice_session_set_caches_hints(SpiceSession *session,
+ uint32_t pci_ram_size,
+ uint32_t n_display_channels);
+void spice_session_get_caches(SpiceSession *session,
+ display_cache **images,
+ SpiceGlzDecoderWindow **glz_window);
+void spice_session_palettes_clear(SpiceSession *session);
+void spice_session_images_clear(SpiceSession *session);
+void spice_session_migrate_end(SpiceSession *session);
+gboolean spice_session_migrate_after_main_init(SpiceSession *session);
+SpiceChannel* spice_session_lookup_channel(SpiceSession *session, gint id, gint type);
+void spice_session_set_uuid(SpiceSession *session, guint8 uuid[16]);
+void spice_session_set_name(SpiceSession *session, const gchar *name);
+gboolean spice_session_is_playback_active(SpiceSession *session);
+guint32 spice_session_get_playback_latency(SpiceSession *session);
+void spice_session_sync_playback_latency(SpiceSession *session);
+const gchar* spice_session_get_shared_dir(SpiceSession *session);
+void spice_session_set_shared_dir(SpiceSession *session, const gchar *dir);
+gboolean spice_session_get_audio_enabled(SpiceSession *session);
+gboolean spice_session_get_smartcard_enabled(SpiceSession *session);
+gboolean spice_session_get_usbredir_enabled(SpiceSession *session);
+
+const guint8* spice_session_get_webdav_magic(SpiceSession *session);
+PhodavServer *spice_session_get_webdav_server(SpiceSession *session);
+PhodavServer* channel_webdav_server_new(SpiceSession *session);
+guint spice_session_get_n_display_channels(SpiceSession *session);
+void spice_session_set_main_channel(SpiceSession *session, SpiceChannel *channel);
+gboolean spice_session_set_migration_session(SpiceSession *session, SpiceSession *mig_session);
+SpiceAudio *spice_audio_get(SpiceSession *session, GMainContext *context);
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_SESSION_PRIV_H__ */
diff --git a/src/spice-session.c b/src/spice-session.c
new file mode 100644
index 0000000..778d82a
--- /dev/null
+++ b/src/spice-session.c
@@ -0,0 +1,2728 @@
+/* -*- 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 <gio/gio.h>
+#include <glib.h>
+#ifdef G_OS_UNIX
+#include <gio/gunixsocketaddress.h>
+#endif
+#include "common/ring.h"
+
+#include "spice-client.h"
+#include "spice-common.h"
+#include "spice-channel-priv.h"
+#include "spice-util-priv.h"
+#include "spice-session-priv.h"
+#include "gio-coroutine.h"
+#include "glib-compat.h"
+#include "wocky-http-proxy.h"
+#include "spice-uri-priv.h"
+#include "channel-playback-priv.h"
+#include "spice-audio.h"
+
+struct channel {
+ SpiceChannel *channel;
+ RingItem link;
+};
+
+#define IMAGES_CACHE_SIZE_DEFAULT (1024 * 1024 * 80)
+#define MIN_GLZ_WINDOW_SIZE_DEFAULT (1024 * 1024 * 12)
+#define MAX_GLZ_WINDOW_SIZE_DEFAULT MIN((LZ_MAX_WINDOW_SIZE * 4), 1024 * 1024 * 64)
+
+struct _SpiceSessionPrivate {
+ char *host;
+ char *unix_path;
+ char *port;
+ char *tls_port;
+ char *username;
+ char *password;
+ char *ca_file;
+ char *ciphers;
+ GByteArray *pubkey;
+ GByteArray *ca;
+ char *cert_subject;
+ guint verify;
+ gboolean read_only;
+ SpiceURI *proxy;
+ gchar *shared_dir;
+ gboolean share_dir_ro;
+
+ /* whether to enable audio */
+ gboolean audio;
+
+ /* whether to enable smartcard event forwarding to the server */
+ gboolean smartcard;
+
+ /* list of certificates to use for the software smartcard reader if
+ * enabled. For now, it has to contain exactly 3 certificates for
+ * the software reader to be functional
+ */
+ GStrv smartcard_certificates;
+
+ /* path to the local certificate database to use to lookup the
+ * certificates stored in 'certificates'. If NULL, libcacard will
+ * fallback to using a default database.
+ */
+ char * smartcard_db;
+
+ /* whether to enable USB redirection */
+ gboolean usbredir;
+
+ /* Set when a usbredir channel has requested the keyboard grab to be
+ temporarily released (because it is going to invoke policykit) */
+ gboolean inhibit_keyboard_grab;
+
+ GStrv disable_effects;
+ GStrv secure_channels;
+ gint color_depth;
+
+ int connection_id;
+ int protocol;
+ SpiceChannel *cmain; /* weak reference */
+ Ring channels;
+ guint32 mm_time;
+ gboolean client_provided_sockets;
+ guint64 mm_time_at_clock;
+ SpiceSession *migration;
+ GList *migration_left;
+ SpiceSessionMigration migration_state;
+ gboolean full_migration; /* seamless migration indicator */
+ guint disconnecting;
+ gboolean migrate_wait_init;
+ guint after_main_init;
+ gboolean for_migration;
+
+ display_cache *images;
+ display_cache *palettes;
+ SpiceGlzDecoderWindow *glz_window;
+ int images_cache_size;
+ int glz_window_size;
+ uint32_t pci_ram_size;
+ uint32_t n_display_channels;
+ guint8 uuid[16];
+ gchar *name;
+
+ /* associated objects */
+ SpiceAudio *audio_manager;
+ SpiceUsbDeviceManager *usb_manager;
+ SpicePlaybackChannel *playback_channel;
+ PhodavServer *webdav;
+};
+
+
+/**
+ * SECTION:spice-session
+ * @short_description: handles connection details, and active channels
+ * @title: Spice Session
+ * @section_id:
+ * @see_also: #SpiceChannel, and the GTK widget #SpiceDisplay
+ * @stability: Stable
+ * @include: spice-session.h
+ *
+ * The #SpiceSession class handles all the #SpiceChannel connections.
+ * It's also the class that contains connections informations, such as
+ * #SpiceSession:host and #SpiceSession:port.
+ *
+ * You can simply set the property #SpiceSession:uri to something like
+ * "spice://127.0.0.1?port=5930" to configure your connection details.
+ *
+ * You may want to connect to #SpiceSession::channel-new signal, to be
+ * informed of the availability of channels and to interact with
+ * them.
+ *
+ * For example, when the #SpiceInputsChannel is available and get the
+ * event #SPICE_CHANNEL_OPENED, you can send key events with
+ * spice_inputs_key_press(). When the #SpiceMainChannel is available,
+ * you can start sharing the clipboard... .
+ *
+ *
+ * Once #SpiceSession properties set, you can call
+ * spice_session_connect() to start connecting and communicating with
+ * a Spice server.
+ */
+
+/* ------------------------------------------------------------------ */
+/* gobject glue */
+
+#define SPICE_SESSION_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE ((obj), SPICE_TYPE_SESSION, SpiceSessionPrivate))
+
+G_DEFINE_TYPE (SpiceSession, spice_session, G_TYPE_OBJECT);
+
+/* Properties */
+enum {
+ PROP_0,
+ PROP_HOST,
+ PROP_PORT,
+ PROP_TLS_PORT,
+ PROP_PASSWORD,
+ PROP_CA_FILE,
+ PROP_CIPHERS,
+ PROP_IPV4,
+ PROP_IPV6,
+ PROP_PROTOCOL,
+ PROP_URI,
+ PROP_CLIENT_SOCKETS,
+ PROP_PUBKEY,
+ PROP_CERT_SUBJECT,
+ PROP_VERIFY,
+ PROP_MIGRATION_STATE,
+ PROP_AUDIO,
+ PROP_SMARTCARD,
+ PROP_SMARTCARD_CERTIFICATES,
+ PROP_SMARTCARD_DB,
+ PROP_USBREDIR,
+ PROP_INHIBIT_KEYBOARD_GRAB,
+ PROP_DISABLE_EFFECTS,
+ PROP_COLOR_DEPTH,
+ PROP_READ_ONLY,
+ PROP_CACHE_SIZE,
+ PROP_GLZ_WINDOW_SIZE,
+ PROP_UUID,
+ PROP_NAME,
+ PROP_CA,
+ PROP_PROXY,
+ PROP_SECURE_CHANNELS,
+ PROP_SHARED_DIR,
+ PROP_SHARE_DIR_RO,
+ PROP_USERNAME,
+ PROP_UNIX_PATH,
+};
+
+/* signals */
+enum {
+ SPICE_SESSION_CHANNEL_NEW,
+ SPICE_SESSION_CHANNEL_DESTROY,
+ SPICE_SESSION_MM_TIME_RESET,
+ SPICE_SESSION_LAST_SIGNAL,
+};
+
+static guint signals[SPICE_SESSION_LAST_SIGNAL];
+
+static void spice_session_channel_destroy(SpiceSession *session, SpiceChannel *channel);
+
+static void update_proxy(SpiceSession *self, const gchar *str)
+{
+ SpiceSessionPrivate *s = self->priv;
+ SpiceURI *proxy = NULL;
+ GError *error = NULL;
+
+ if (str == NULL)
+ str = g_getenv("SPICE_PROXY");
+ if (str == NULL || *str == 0) {
+ g_clear_object(&s->proxy);
+ return;
+ }
+
+ proxy = spice_uri_new();
+ if (!spice_uri_parse(proxy, str, &error))
+ g_clear_object(&proxy);
+ if (error) {
+ g_warning("%s", error->message);
+ g_clear_error(&error);
+ }
+
+ if (proxy != NULL) {
+ g_clear_object(&s->proxy);
+ s->proxy = proxy;
+ }
+}
+
+static void spice_session_init(SpiceSession *session)
+{
+ SpiceSessionPrivate *s;
+ gchar *channels;
+
+ SPICE_DEBUG("New session (compiled from package " PACKAGE_STRING ")");
+ s = session->priv = SPICE_SESSION_GET_PRIVATE(session);
+
+ channels = spice_channel_supported_string();
+ SPICE_DEBUG("Supported channels: %s", channels);
+ g_free(channels);
+
+ ring_init(&s->channels);
+ s->images = cache_new((GDestroyNotify)pixman_image_unref);
+ s->glz_window = glz_decoder_window_new();
+ update_proxy(session, NULL);
+}
+
+static void
+session_disconnect(SpiceSession *self, gboolean keep_main)
+{
+ SpiceSessionPrivate *s;
+ struct channel *item;
+ RingItem *ring, *next;
+
+ s = self->priv;
+
+ for (ring = ring_get_head(&s->channels); ring != NULL; ring = next) {
+ next = ring_next(&s->channels, ring);
+ item = SPICE_CONTAINEROF(ring, struct channel, link);
+
+ if (keep_main && item->channel == s->cmain) {
+ spice_channel_disconnect(item->channel, SPICE_CHANNEL_NONE);
+ } else {
+ spice_session_channel_destroy(self, item->channel);
+ }
+ }
+
+ s->connection_id = 0;
+
+ g_free(s->name);
+ s->name = NULL;
+ memset(s->uuid, 0, sizeof(s->uuid));
+
+ spice_session_abort_migration(self);
+}
+
+static void
+spice_session_dispose(GObject *gobject)
+{
+ SpiceSession *session = SPICE_SESSION(gobject);
+ SpiceSessionPrivate *s = session->priv;
+
+ SPICE_DEBUG("session dispose");
+
+ session_disconnect(session, FALSE);
+
+ g_warn_if_fail(s->migration == NULL);
+ g_warn_if_fail(s->migration_left == NULL);
+ g_warn_if_fail(s->after_main_init == 0);
+ g_warn_if_fail(s->disconnecting == 0);
+
+ g_clear_object(&s->audio_manager);
+ g_clear_object(&s->usb_manager);
+ g_clear_object(&s->proxy);
+ g_clear_object(&s->webdav);
+
+ /* Chain up to the parent class */
+ if (G_OBJECT_CLASS(spice_session_parent_class)->dispose)
+ G_OBJECT_CLASS(spice_session_parent_class)->dispose(gobject);
+}
+
+static void
+spice_session_finalize(GObject *gobject)
+{
+ SpiceSession *session = SPICE_SESSION(gobject);
+ SpiceSessionPrivate *s = session->priv;
+
+ /* release stuff */
+ g_free(s->unix_path);
+ g_free(s->host);
+ g_free(s->port);
+ g_free(s->tls_port);
+ g_free(s->username);
+ g_free(s->password);
+ g_free(s->ca_file);
+ g_free(s->ciphers);
+ g_free(s->cert_subject);
+ g_strfreev(s->smartcard_certificates);
+ g_free(s->smartcard_db);
+ g_strfreev(s->disable_effects);
+ g_strfreev(s->secure_channels);
+ g_free(s->shared_dir);
+
+ g_clear_pointer(&s->images, cache_unref);
+ glz_decoder_window_destroy(s->glz_window);
+
+ g_clear_pointer(&s->pubkey, g_byte_array_unref);
+ g_clear_pointer(&s->ca, g_byte_array_unref);
+
+ /* Chain up to the parent class */
+ if (G_OBJECT_CLASS(spice_session_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_session_parent_class)->finalize(gobject);
+}
+
+#define URI_SCHEME_SPICE "spice://"
+#define URI_SCHEME_SPICE_UNIX "spice+unix://"
+#define URI_QUERY_START ";?"
+#define URI_QUERY_SEP ";&"
+
+static gchar* spice_uri_create(SpiceSession *session)
+{
+ SpiceSessionPrivate *s = session->priv;
+
+ if (s->unix_path != NULL) {
+ return g_strdup_printf(URI_SCHEME_SPICE_UNIX "%s", s->unix_path);
+ } else if (s->host != NULL) {
+ g_return_val_if_fail(s->port != NULL || s->tls_port != NULL, NULL);
+
+ GString *str = g_string_new(URI_SCHEME_SPICE);
+
+ g_string_append(str, s->host);
+ g_string_append(str, "?");
+ if (s->port != NULL) {
+ g_string_append_printf(str, "port=%s&", s->port);
+ }
+ if (s->tls_port != NULL) {
+ g_string_append_printf(str, "tls-port=%s", s->tls_port);
+ }
+ return g_string_free(str, FALSE);
+ }
+
+ g_return_val_if_reached(NULL);
+}
+
+static int spice_parse_uri(SpiceSession *session, const char *original_uri)
+{
+ SpiceSessionPrivate *s = session->priv;
+ gchar *host = NULL, *port = NULL, *tls_port = NULL, *uri = NULL, *username = NULL, *password = NULL;
+ gchar *path = NULL;
+ gchar *unescaped_path = NULL;
+ gchar *authority = NULL;
+ gchar *query = NULL;
+ gchar *tmp = NULL;
+
+ g_return_val_if_fail(original_uri != NULL, -1);
+
+ uri = g_strdup(original_uri);
+
+ if (g_str_has_prefix(uri, URI_SCHEME_SPICE_UNIX)) {
+ path = uri + strlen(URI_SCHEME_SPICE_UNIX);
+ goto end;
+ }
+
+ /* Break up the URI into its various parts, scheme, authority,
+ * path (ignored) and query
+ */
+ if (!g_str_has_prefix(uri, URI_SCHEME_SPICE)) {
+ g_warning("Expected a URI scheme of '%s' in URI '%s'",
+ URI_SCHEME_SPICE, uri);
+ goto fail;
+ }
+ authority = uri + strlen(URI_SCHEME_SPICE);
+
+ tmp = strchr(authority, '@');
+ if (tmp) {
+ tmp[0] = '\0';
+ username = g_uri_unescape_string(authority, NULL);
+ authority = ++tmp;
+ tmp = NULL;
+ }
+
+ path = strchr(authority, '/');
+ if (path) {
+ path[0] = '\0';
+ path++;
+ }
+
+ if (path) {
+ size_t prefix = strcspn(path, URI_QUERY_START);
+ query = path + prefix;
+ } else {
+ size_t prefix = strcspn(authority, URI_QUERY_START);
+ query = authority + prefix;
+ }
+
+ if (query && query[0]) {
+ query[0] = '\0';
+ query++;
+ }
+
+ /* Now process the individual parts */
+
+ if (authority[0] == '[') {
+ tmp = strchr(authority, ']');
+ if (!tmp) {
+ g_warning("Missing closing ']' in authority for URI '%s'", uri);
+ goto fail;
+ }
+ tmp[0] = '\0';
+ tmp++;
+ host = g_strdup(authority + 1);
+ if (tmp[0] == ':')
+ port = g_strdup(tmp + 1);
+ } else {
+ tmp = strchr(authority, ':');
+ if (tmp) {
+ *tmp = '\0';
+ tmp++;
+ port = g_strdup(tmp);
+ }
+ host = g_uri_unescape_string(authority, NULL);
+ }
+
+ if (path && !(g_str_equal(path, "") ||
+ g_str_equal(path, "/"))) {
+ g_warning("Unexpected path data '%s' for URI '%s'", path, uri);
+ /* don't fail, just ignore */
+ }
+ unescaped_path = g_uri_unescape_string(path, NULL);
+ path = NULL;
+
+ while (query && query[0] != '\0') {
+ gchar key[32], value[128];
+ gchar **target_key;
+
+ int len;
+ if (sscanf(query, "%31[-a-zA-Z0-9]=%n", key, &len) != 1) {
+ spice_warning("Failed to parse key in URI '%s'", query);
+ goto fail;
+ }
+
+ query += len;
+ if (*query == '\0') {
+ spice_warning ("key '%s' without value", key);
+ break;
+ } else if (*query == ';' || *query == '&') {
+ /* another argument */
+ query++;
+ continue;
+ }
+
+ if (sscanf(query, "%127[^;&]%n", value, &len) != 1) {
+ spice_warning("Failed to parse value of key '%s' in URI '%s'", key, query);
+ goto fail;
+ }
+
+ query += len;
+ if (*query)
+ query++;
+
+ target_key = NULL;
+ if (g_str_equal(key, "port")) {
+ target_key = &port;
+ } else if (g_str_equal(key, "tls-port")) {
+ target_key = &tls_port;
+ } else if (g_str_equal(key, "password")) {
+ target_key = &password;
+ g_warning("password may be visible in process listings");
+ } else {
+ g_warning("unknown key in spice URI parsing: '%s'", key);
+ goto fail;
+ }
+ if (target_key) {
+ if (*target_key) {
+ g_warning("Double set of '%s' in URI '%s'", key, uri);
+ goto fail;
+ }
+ *target_key = g_uri_unescape_string(value, NULL);
+ }
+ }
+
+ if (port == NULL && tls_port == NULL) {
+ g_warning("Missing port or tls-port in spice URI '%s'", uri);
+ goto fail;
+ }
+
+end:
+ /* parsed ok -> apply */
+ g_free(uri);
+ g_free(unescaped_path);
+ g_free(s->unix_path);
+ g_free(s->host);
+ g_free(s->port);
+ g_free(s->tls_port);
+ g_free(s->username);
+ g_free(s->password);
+ s->unix_path = g_strdup(path);
+ s->host = host;
+ s->port = port;
+ s->tls_port = tls_port;
+ s->username = username;
+ s->password = password;
+ return 0;
+
+fail:
+ g_free(uri);
+ g_free(unescaped_path);
+ g_free(host);
+ g_free(port);
+ g_free(tls_port);
+ g_free(username);
+ g_free(password);
+ return -1;
+}
+
+static void spice_session_get_property(GObject *gobject,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceSession *session = SPICE_SESSION(gobject);
+ SpiceSessionPrivate *s = session->priv;
+
+ switch (prop_id) {
+ case PROP_HOST:
+ g_value_set_string(value, s->host);
+ break;
+ case PROP_UNIX_PATH:
+ g_value_set_string(value, s->unix_path);
+ break;
+ case PROP_PORT:
+ g_value_set_string(value, s->port);
+ break;
+ case PROP_TLS_PORT:
+ g_value_set_string(value, s->tls_port);
+ break;
+ case PROP_USERNAME:
+ g_value_set_string(value, s->username);
+ break;
+ case PROP_PASSWORD:
+ g_value_set_string(value, s->password);
+ break;
+ case PROP_CA_FILE:
+ g_value_set_string(value, s->ca_file);
+ break;
+ case PROP_CIPHERS:
+ g_value_set_string(value, s->ciphers);
+ break;
+ case PROP_PROTOCOL:
+ g_value_set_int(value, s->protocol);
+ break;
+ case PROP_URI:
+ g_value_take_string(value, spice_uri_create(session));
+ break;
+ case PROP_CLIENT_SOCKETS:
+ g_value_set_boolean(value, s->client_provided_sockets);
+ break;
+ case PROP_PUBKEY:
+ g_value_set_boxed(value, s->pubkey);
+ break;
+ case PROP_CA:
+ g_value_set_boxed(value, s->ca);
+ break;
+ case PROP_CERT_SUBJECT:
+ g_value_set_string(value, s->cert_subject);
+ break;
+ case PROP_VERIFY:
+ g_value_set_flags(value, s->verify);
+ break;
+ case PROP_MIGRATION_STATE:
+ g_value_set_enum(value, s->migration_state);
+ break;
+ case PROP_SMARTCARD:
+ g_value_set_boolean(value, s->smartcard);
+ break;
+ case PROP_SMARTCARD_CERTIFICATES:
+ g_value_set_boxed(value, s->smartcard_certificates);
+ break;
+ case PROP_SMARTCARD_DB:
+ g_value_set_string(value, s->smartcard_db);
+ break;
+ case PROP_USBREDIR:
+ g_value_set_boolean(value, s->usbredir);
+ break;
+ case PROP_INHIBIT_KEYBOARD_GRAB:
+ g_value_set_boolean(value, s->inhibit_keyboard_grab);
+ break;
+ case PROP_DISABLE_EFFECTS:
+ g_value_set_boxed(value, s->disable_effects);
+ break;
+ case PROP_SECURE_CHANNELS:
+ g_value_set_boxed(value, s->secure_channels);
+ break;
+ case PROP_COLOR_DEPTH:
+ g_value_set_int(value, s->color_depth);
+ break;
+ case PROP_AUDIO:
+ g_value_set_boolean(value, s->audio);
+ break;
+ case PROP_READ_ONLY:
+ g_value_set_boolean(value, s->read_only);
+ break;
+ case PROP_CACHE_SIZE:
+ g_value_set_int(value, s->images_cache_size);
+ break;
+ case PROP_GLZ_WINDOW_SIZE:
+ g_value_set_int(value, s->glz_window_size);
+ break;
+ case PROP_NAME:
+ g_value_set_string(value, s->name);
+ break;
+ case PROP_UUID:
+ g_value_set_pointer(value, s->uuid);
+ break;
+ case PROP_PROXY:
+ g_value_take_string(value, spice_uri_to_string(s->proxy));
+ break;
+ case PROP_SHARED_DIR:
+ g_value_set_string(value, spice_session_get_shared_dir(session));
+ break;
+ case PROP_SHARE_DIR_RO:
+ g_value_set_boolean(value, s->share_dir_ro);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_session_set_property(GObject *gobject,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceSession *session = SPICE_SESSION(gobject);
+ SpiceSessionPrivate *s = session->priv;
+ const char *str;
+
+ switch (prop_id) {
+ case PROP_HOST:
+ g_free(s->host);
+ s->host = g_value_dup_string(value);
+ break;
+ case PROP_UNIX_PATH:
+ g_free(s->unix_path);
+ s->unix_path = g_value_dup_string(value);
+ break;
+ case PROP_PORT:
+ g_free(s->port);
+ s->port = g_value_dup_string(value);
+ break;
+ case PROP_TLS_PORT:
+ g_free(s->tls_port);
+ s->tls_port = g_value_dup_string(value);
+ break;
+ case PROP_USERNAME:
+ g_free(s->username);
+ s->username = g_value_dup_string(value);
+ break;
+ case PROP_PASSWORD:
+ g_free(s->password);
+ s->password = g_value_dup_string(value);
+ break;
+ case PROP_CA_FILE:
+ g_free(s->ca_file);
+ s->ca_file = g_value_dup_string(value);
+ break;
+ case PROP_CIPHERS:
+ g_free(s->ciphers);
+ s->ciphers = g_value_dup_string(value);
+ break;
+ case PROP_PROTOCOL:
+ s->protocol = g_value_get_int(value);
+ break;
+ case PROP_URI:
+ str = g_value_get_string(value);
+ if (str != NULL)
+ spice_parse_uri(session, str);
+ break;
+ case PROP_CLIENT_SOCKETS:
+ s->client_provided_sockets = g_value_get_boolean(value);
+ break;
+ case PROP_PUBKEY:
+ if (s->pubkey)
+ g_byte_array_unref(s->pubkey);
+ s->pubkey = g_value_dup_boxed(value);
+ if (s->pubkey)
+ s->verify |= SPICE_SESSION_VERIFY_PUBKEY;
+ else
+ s->verify &= ~SPICE_SESSION_VERIFY_PUBKEY;
+ break;
+ case PROP_CERT_SUBJECT:
+ g_free(s->cert_subject);
+ s->cert_subject = g_value_dup_string(value);
+ if (s->cert_subject)
+ s->verify |= SPICE_SESSION_VERIFY_SUBJECT;
+ else
+ s->verify &= ~SPICE_SESSION_VERIFY_SUBJECT;
+ break;
+ case PROP_VERIFY:
+ s->verify = g_value_get_flags(value);
+ break;
+ case PROP_MIGRATION_STATE:
+ s->migration_state = g_value_get_enum(value);
+ break;
+ case PROP_SMARTCARD:
+ s->smartcard = g_value_get_boolean(value);
+ break;
+ case PROP_SMARTCARD_CERTIFICATES:
+ g_strfreev(s->smartcard_certificates);
+ s->smartcard_certificates = g_value_dup_boxed(value);
+ break;
+ case PROP_SMARTCARD_DB:
+ g_free(s->smartcard_db);
+ s->smartcard_db = g_value_dup_string(value);
+ break;
+ case PROP_USBREDIR:
+ s->usbredir = g_value_get_boolean(value);
+ break;
+ case PROP_INHIBIT_KEYBOARD_GRAB:
+ s->inhibit_keyboard_grab = g_value_get_boolean(value);
+ break;
+ case PROP_DISABLE_EFFECTS:
+ g_strfreev(s->disable_effects);
+ s->disable_effects = g_value_dup_boxed(value);
+ break;
+ case PROP_SECURE_CHANNELS:
+ g_strfreev(s->secure_channels);
+ s->secure_channels = g_value_dup_boxed(value);
+ break;
+ case PROP_COLOR_DEPTH:
+ s->color_depth = g_value_get_int(value);
+ break;
+ case PROP_AUDIO:
+ s->audio = g_value_get_boolean(value);
+ break;
+ case PROP_READ_ONLY:
+ s->read_only = g_value_get_boolean(value);
+ g_coroutine_object_notify(gobject, "read-only");
+ break;
+ case PROP_CACHE_SIZE:
+ s->images_cache_size = g_value_get_int(value);
+ break;
+ case PROP_GLZ_WINDOW_SIZE:
+ s->glz_window_size = g_value_get_int(value);
+ break;
+ case PROP_CA:
+ g_clear_pointer(&s->ca, g_byte_array_unref);
+ s->ca = g_value_dup_boxed(value);
+ break;
+ case PROP_PROXY:
+ update_proxy(session, g_value_get_string(value));
+ break;
+ case PROP_SHARED_DIR:
+ spice_session_set_shared_dir(session, g_value_get_string(value));
+ break;
+ case PROP_SHARE_DIR_RO:
+ s->share_dir_ro = g_value_get_boolean(value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_session_class_init(SpiceSessionClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+
+ _wocky_http_proxy_get_type();
+ _wocky_https_proxy_get_type();
+
+ gobject_class->dispose = spice_session_dispose;
+ gobject_class->finalize = spice_session_finalize;
+ gobject_class->get_property = spice_session_get_property;
+ gobject_class->set_property = spice_session_set_property;
+
+ /**
+ * SpiceSession:host:
+ *
+ * URL of the SPICE host to connect to
+ *
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_HOST,
+ g_param_spec_string("host",
+ "Host",
+ "Remote host",
+ "localhost",
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:unix-path:
+ *
+ * Path of the Unix socket to connect to
+ *
+ * Since: 0.28
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_UNIX_PATH,
+ g_param_spec_string("unix-path",
+ "Unix path",
+ "Unix path",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:port:
+ *
+ * Port to connect to for unencrypted sessions
+ *
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_PORT,
+ g_param_spec_string("port",
+ "Port",
+ "Remote port (plaintext)",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:tls-port:
+ *
+ * Port to connect to for TLS sessions
+ *
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_TLS_PORT,
+ g_param_spec_string("tls-port",
+ "TLS port",
+ "Remote port (encrypted)",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:username:
+ *
+ * Username to use
+ *
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_USERNAME,
+ g_param_spec_string("username",
+ "Username",
+ "Username used for SASL connections",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:password:
+ *
+ * TLS password to use
+ *
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_PASSWORD,
+ g_param_spec_string("password",
+ "Password",
+ "",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:ca-file:
+ *
+ * File holding the CA certificates for the host the client is
+ * connecting to
+ *
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_CA_FILE,
+ g_param_spec_string("ca-file",
+ "CA file",
+ "File holding the CA certificates",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:ciphers:
+ *
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_CIPHERS,
+ g_param_spec_string("ciphers",
+ "Ciphers",
+ "SSL cipher list",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:protocol:
+ *
+ * Version of the SPICE protocol to use
+ *
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_PROTOCOL,
+ g_param_spec_int("protocol",
+ "Protocol",
+ "Spice protocol major version",
+ 1, 2, 2,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:uri:
+ *
+ * URI of the SPICE host to connect to. The URI is of the form
+ * spice://hostname?port=XXX or spice://hostname?tls_port=XXX
+ *
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_URI,
+ g_param_spec_string("uri",
+ "URI",
+ "Spice connection URI",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:client-sockets:
+ *
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_CLIENT_SOCKETS,
+ g_param_spec_boolean("client-sockets",
+ "Client sockets",
+ "Sockets are provided by the client",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:pubkey:
+ *
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_PUBKEY,
+ g_param_spec_boxed("pubkey",
+ "Pub Key",
+ "Public key to check",
+ G_TYPE_BYTE_ARRAY,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:cert-subject:
+ *
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_CERT_SUBJECT,
+ g_param_spec_string("cert-subject",
+ "Cert Subject",
+ "Certificate subject to check",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:verify:
+ *
+ * #SpiceSessionVerify bit field indicating which parts of the peer
+ * certificate should be checked
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_VERIFY,
+ g_param_spec_flags("verify",
+ "Verify",
+ "Certificate verification parameters",
+ SPICE_TYPE_SESSION_VERIFY,
+ SPICE_SESSION_VERIFY_HOSTNAME,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:migration-state:
+ *
+ * #SpiceSessionMigration bit field indicating if a migration is in
+ * progress
+ *
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_MIGRATION_STATE,
+ g_param_spec_enum("migration-state",
+ "Migration state",
+ "Migration state",
+ SPICE_TYPE_SESSION_MIGRATION,
+ SPICE_SESSION_MIGRATION_NONE,
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:disable-effects:
+ *
+ * A string array of effects to disable. The settings will
+ * be applied on new display channels. The following effets can be
+ * disabled "wallpaper", "font-smooth", "animation", and "all",
+ * which will disable all the effects. If NULL, don't apply changes.
+ *
+ * Since: 0.7
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_DISABLE_EFFECTS,
+ g_param_spec_boxed ("disable-effects",
+ "Disable effects",
+ "Comma-separated effects to disable",
+ G_TYPE_STRV,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:color-depth:
+ *
+ * Display color depth to set on new display channels. If 0, don't set.
+ *
+ * Since: 0.7
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_COLOR_DEPTH,
+ g_param_spec_int("color-depth",
+ "Color depth",
+ "Display channel color depth",
+ 0, 32, 0,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:enable-smartcard:
+ *
+ * If set to TRUE, the smartcard channel will be enabled and smartcard
+ * events will be forwarded to the guest
+ *
+ * Since: 0.7
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_SMARTCARD,
+ g_param_spec_boolean("enable-smartcard",
+ "Enable smartcard event forwarding",
+ "Forward smartcard events to the SPICE server",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:enable-audio:
+ *
+ * If set to TRUE, the audio channels will be enabled for
+ * playback and recording.
+ *
+ * Since: 0.8
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_AUDIO,
+ g_param_spec_boolean("enable-audio",
+ "Enable audio channels",
+ "Enable audio channels",
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:smartcard-certificates:
+ *
+ * This property is used when one wants to simulate a smartcard with no
+ * hardware smartcard reader. If it's set to a NULL-terminated string
+ * array containing the names of 3 valid certificates, these will be
+ * used to simulate a smartcard in the guest
+ * See also spice_smartcard_manager_insert_card()
+ *
+ * Since: 0.7
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_SMARTCARD_CERTIFICATES,
+ g_param_spec_boxed("smartcard-certificates",
+ "Smartcard certificates",
+ "Smartcard certificates for software-based smartcards",
+ G_TYPE_STRV,
+ G_PARAM_READABLE |
+ G_PARAM_WRITABLE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:smartcard-db:
+ *
+ * Path to the NSS certificate database containing the certificates to
+ * use to simulate a software smartcard
+ *
+ * Since: 0.7
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_SMARTCARD_DB,
+ g_param_spec_string("smartcard-db",
+ "Smartcard certificate database",
+ "Path to the database for smartcard certificates",
+ NULL,
+ G_PARAM_READABLE |
+ G_PARAM_WRITABLE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:enable-usbredir:
+ *
+ * If set to TRUE, the usbredir channel will be enabled and USB devices
+ * can be redirected to the guest
+ *
+ * Since: 0.8
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_USBREDIR,
+ g_param_spec_boolean("enable-usbredir",
+ "Enable USB device redirection",
+ "Forward USB devices to the SPICE server",
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession::inhibit-keyboard-grab:
+ *
+ * This boolean is set by the usbredir channel to indicate to #SpiceDisplay
+ * that the keyboard grab should be temporarily released, because it is
+ * going to invoke policykit. It will get reset when the usbredir channel
+ * is done with polickit.
+ *
+ * Since: 0.8
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_INHIBIT_KEYBOARD_GRAB,
+ g_param_spec_boolean("inhibit-keyboard-grab",
+ "Inhibit Keyboard Grab",
+ "Request that SpiceDisplays don't grab the keyboard",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:ca:
+ *
+ * CA certificates in PEM format. The text data can contain
+ * several CA certificates identified by:
+ *
+ * -----BEGIN CERTIFICATE-----
+ * ... (CA certificate in base64 encoding) ...
+ * -----END CERTIFICATE-----
+ *
+ * Since: 0.15
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_CA,
+ g_param_spec_boxed("ca",
+ "CA",
+ "The CA certificates data",
+ G_TYPE_BYTE_ARRAY,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:secure-channels:
+ *
+ * A string array of channel types to be secured.
+ *
+ * Since: 0.20
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_SECURE_CHANNELS,
+ g_param_spec_boxed ("secure-channels",
+ "Secure channels",
+ "Array of channel type to secure",
+ G_TYPE_STRV,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+
+ /**
+ * SpiceSession::channel-new:
+ * @session: the session that emitted the signal
+ * @channel: the new #SpiceChannel
+ *
+ * The #SpiceSession::channel-new signal is emitted each time a #SpiceChannel is created.
+ **/
+ signals[SPICE_SESSION_CHANNEL_NEW] =
+ g_signal_new("channel-new",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceSessionClass, channel_new),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__OBJECT,
+ G_TYPE_NONE,
+ 1,
+ SPICE_TYPE_CHANNEL);
+
+ /**
+ * SpiceSession::channel-destroy:
+ * @session: the session that emitted the signal
+ * @channel: the destroyed #SpiceChannel
+ *
+ * The #SpiceSession::channel-destroy signal is emitted each time a #SpiceChannel is destroyed.
+ **/
+ signals[SPICE_SESSION_CHANNEL_DESTROY] =
+ g_signal_new("channel-destroy",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceSessionClass, channel_destroy),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__OBJECT,
+ G_TYPE_NONE,
+ 1,
+ SPICE_TYPE_CHANNEL);
+
+ /**
+ * SpiceSession::mm-time-reset:
+ * @session: the session that emitted the signal
+ *
+ * The #SpiceSession::mm-time-reset is emitted when we identify discontinuity in mm-time
+ *
+ * Since 0.20
+ **/
+ signals[SPICE_SESSION_MM_TIME_RESET] =
+ g_signal_new("mm-time-reset",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ 0, NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
+
+ /**
+ * SpiceSession:read-only:
+ *
+ * Whether this connection is read-only mode.
+ *
+ * Since: 0.8
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_READ_ONLY,
+ g_param_spec_boolean("read-only", "Read-only",
+ "Whether this connection is read-only mode",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:cache-size:
+ *
+ * Images cache size. If 0, don't set.
+ *
+ * Since: 0.9
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_CACHE_SIZE,
+ g_param_spec_int("cache-size",
+ "Cache size",
+ "Images cache size (bytes)",
+ 0, G_MAXINT, 0,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:glz-window-size:
+ *
+ * Glz window size. If 0, don't set.
+ *
+ * Since: 0.9
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_GLZ_WINDOW_SIZE,
+ g_param_spec_int("glz-window-size",
+ "Glz window size",
+ "Glz window size (bytes)",
+ 0, LZ_MAX_WINDOW_SIZE * 4, 0,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:name:
+ *
+ * Spice server name.
+ *
+ * Since: 0.11
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_NAME,
+ g_param_spec_string("name",
+ "Name",
+ "Spice server name",
+ NULL,
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:uuid:
+ *
+ * Spice server uuid.
+ *
+ * Since: 0.11
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_UUID,
+ g_param_spec_pointer("uuid",
+ "UUID",
+ "Spice server uuid",
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:proxy:
+ *
+ * URI to the proxy server to use when doing network connection.
+ * of the form <![CDATA[ [protocol://]<host>[:port] ]]>
+ *
+ * Since: 0.17
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_PROXY,
+ g_param_spec_string("proxy",
+ "Proxy",
+ "The proxy server",
+ NULL,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:shared-dir:
+ *
+ * Location of the shared directory
+ *
+ * Since: 0.24
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_SHARED_DIR,
+ g_param_spec_string("shared-dir",
+ "Shared directory",
+ "Shared directory",
+ g_get_user_special_dir(G_USER_DIRECTORY_PUBLIC_SHARE),
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceSession:share-dir-ro:
+ *
+ * Whether to share the directory read-only.
+ *
+ * Since: 0.28
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_SHARE_DIR_RO,
+ g_param_spec_boolean("share-dir-ro",
+ "Share directory read-only",
+ "Share directory read-only",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ g_type_class_add_private(klass, sizeof(SpiceSessionPrivate));
+}
+
+/* ------------------------------------------------------------------ */
+/* public functions */
+
+/**
+ * spice_session_new:
+ *
+ * Creates a new Spice session.
+ *
+ * Returns: a new #SpiceSession
+ **/
+SpiceSession *spice_session_new(void)
+{
+ return SPICE_SESSION(g_object_new(SPICE_TYPE_SESSION, NULL));
+}
+
+G_GNUC_INTERNAL
+SpiceSession *spice_session_new_from_session(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+
+ SpiceSessionPrivate *s = session->priv;
+ SpiceSession *copy;
+ SpiceSessionPrivate *c;
+
+ if (s->client_provided_sockets) {
+ g_warning("migration with client provided fd is not supported yet");
+ return NULL;
+ }
+
+ copy = SPICE_SESSION(g_object_new(SPICE_TYPE_SESSION,
+ "host", NULL,
+ "ca-file", NULL,
+ NULL));
+ c = copy->priv;
+ g_clear_object(&c->proxy);
+
+ g_warn_if_fail(c->host == NULL);
+ g_warn_if_fail(c->unix_path == NULL);
+ g_warn_if_fail(c->tls_port == NULL);
+ g_warn_if_fail(c->username == NULL);
+ g_warn_if_fail(c->password == NULL);
+ g_warn_if_fail(c->ca_file == NULL);
+ g_warn_if_fail(c->ciphers == NULL);
+ g_warn_if_fail(c->cert_subject == NULL);
+ g_warn_if_fail(c->pubkey == NULL);
+ g_warn_if_fail(c->pubkey == NULL);
+ g_warn_if_fail(c->proxy == NULL);
+
+ g_object_get(session,
+ "host", &c->host,
+ "unix-path", &c->unix_path,
+ "tls-port", &c->tls_port,
+ "username", &c->username,
+ "password", &c->password,
+ "ca-file", &c->ca_file,
+ "ciphers", &c->ciphers,
+ "cert-subject", &c->cert_subject,
+ "pubkey", &c->pubkey,
+ "verify", &c->verify,
+ "smartcard-certificates", &c->smartcard_certificates,
+ "smartcard-db", &c->smartcard_db,
+ "enable-smartcard", &c->smartcard,
+ "enable-audio", &c->audio,
+ "enable-usbredir", &c->usbredir,
+ "ca", &c->ca,
+ NULL);
+
+ c->client_provided_sockets = s->client_provided_sockets;
+ c->protocol = s->protocol;
+ c->connection_id = s->connection_id;
+ if (s->proxy)
+ c->proxy = g_object_ref(s->proxy);
+
+ return copy;
+}
+
+/**
+ * spice_session_connect:
+ * @session:
+ *
+ * Open the session using the #SpiceSession:host and
+ * #SpiceSession:port.
+ *
+ * Returns: %FALSE if the connection failed.
+ **/
+gboolean spice_session_connect(SpiceSession *session)
+{
+ SpiceSessionPrivate *s;
+
+ g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);
+
+ s = session->priv;
+ g_return_val_if_fail(!s->disconnecting, FALSE);
+
+ session_disconnect(session, TRUE);
+
+ s->client_provided_sockets = FALSE;
+
+ if (s->cmain == NULL)
+ s->cmain = spice_channel_new(session, SPICE_CHANNEL_MAIN, 0);
+
+ glz_decoder_window_clear(s->glz_window);
+ return spice_channel_connect(s->cmain);
+}
+
+/**
+ * spice_session_open_fd:
+ * @session:
+ * @fd: a file descriptor (socket) or -1
+ *
+ * Open the session using the provided @fd socket file
+ * descriptor. This is useful if you create the fd yourself, for
+ * example to setup a SSH tunnel.
+ *
+ * Note however that additional sockets will be needed by all the channels
+ * created for @session so users of this API should hook into
+ * SpiceChannel::open-fd signal for each channel they are interested in, and
+ * create and pass a new socket to the channel using #spice_channel_open_fd, in
+ * the signal callback.
+ *
+ * If @fd is -1, a valid fd will be requested later via the
+ * SpiceChannel::open-fd signal. Typically, you would want to just pass -1 as
+ * @fd this call since you will have to hook to SpiceChannel::open-fd signal
+ * anyway.
+ *
+ * Returns:
+ **/
+gboolean spice_session_open_fd(SpiceSession *session, int fd)
+{
+ SpiceSessionPrivate *s;
+
+ g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);
+ g_return_val_if_fail(fd >= -1, FALSE);
+
+ s = session->priv;
+ g_return_val_if_fail(!s->disconnecting, FALSE);
+
+ session_disconnect(session, TRUE);
+
+ s->client_provided_sockets = TRUE;
+
+ if (s->cmain == NULL)
+ s->cmain = spice_channel_new(session, SPICE_CHANNEL_MAIN, 0);
+
+ glz_decoder_window_clear(s->glz_window);
+ return spice_channel_open_fd(s->cmain, fd);
+}
+
+G_GNUC_INTERNAL
+gboolean spice_session_get_client_provided_socket(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);
+
+ SpiceSessionPrivate *s = session->priv;
+
+ return s->client_provided_sockets;
+}
+
+static void cache_clear_all(SpiceSession *self)
+{
+ SpiceSessionPrivate *s = self->priv;
+
+ cache_clear(s->images);
+ glz_decoder_window_clear(s->glz_window);
+}
+
+G_GNUC_INTERNAL
+void spice_session_switching_disconnect(SpiceSession *self)
+{
+ g_return_if_fail(SPICE_IS_SESSION(self));
+
+ SpiceSessionPrivate *s = self->priv;
+ struct channel *item;
+ RingItem *ring, *next;
+
+ g_return_if_fail(s->cmain != NULL);
+
+ /* disconnect/destroy all but main channel */
+
+ for (ring = ring_get_head(&s->channels); ring != NULL; ring = next) {
+ next = ring_next(&s->channels, ring);
+ item = SPICE_CONTAINEROF(ring, struct channel, link);
+
+ if (item->channel == s->cmain)
+ continue;
+ spice_session_channel_destroy(self, item->channel);
+ }
+
+ g_warn_if_fail(!ring_is_empty(&s->channels)); /* ring_get_length() == 1 */
+
+ cache_clear_all(self);
+ s->connection_id = 0;
+}
+
+#define SWAP_STR(x, y) G_STMT_START { \
+ const gchar *tmp; \
+ const gchar *a = x; \
+ const gchar *b = y; \
+ tmp = a; \
+ a = b; \
+ b = tmp; \
+} G_STMT_END
+
+G_GNUC_INTERNAL
+void spice_session_start_migrating(SpiceSession *session,
+ gboolean full_migration)
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+
+ SpiceSessionPrivate *s = session->priv;
+ SpiceSessionPrivate *m;
+
+ g_return_if_fail(s->migration != NULL);
+ m = s->migration->priv;
+ g_return_if_fail(m->migration_state == SPICE_SESSION_MIGRATION_CONNECTING);
+
+
+ s->full_migration = full_migration;
+ spice_session_set_migration_state(session, SPICE_SESSION_MIGRATION_MIGRATING);
+
+ /* swapping connection details happens after MIGRATION_CONNECTING state */
+ SWAP_STR(s->host, m->host);
+ SWAP_STR(s->port, m->port);
+ SWAP_STR(s->tls_port, m->tls_port);
+ SWAP_STR(s->unix_path, m->unix_path);
+
+ g_warn_if_fail(ring_get_length(&s->channels) == ring_get_length(&m->channels));
+
+ SPICE_DEBUG("migration channels left:%d (in migration:%d)",
+ ring_get_length(&s->channels), ring_get_length(&m->channels));
+ s->migration_left = spice_session_get_channels(session);
+}
+#undef SWAP_STR
+
+G_GNUC_INTERNAL
+SpiceChannel* spice_session_lookup_channel(SpiceSession *session, gint id, gint type)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+
+ RingItem *ring, *next;
+ SpiceSessionPrivate *s = session->priv;
+ struct channel *c;
+
+ for (ring = ring_get_head(&s->channels);
+ ring != NULL; ring = next) {
+ next = ring_next(&s->channels, ring);
+ c = SPICE_CONTAINEROF(ring, struct channel, link);
+ if (c == NULL || c->channel == NULL) {
+ g_warn_if_reached();
+ continue;
+ }
+
+ if (id == spice_channel_get_channel_id(c->channel) &&
+ type == spice_channel_get_channel_type(c->channel))
+ break;
+ }
+ g_return_val_if_fail(ring != NULL, NULL);
+
+ return c->channel;
+}
+
+G_GNUC_INTERNAL
+void spice_session_abort_migration(SpiceSession *session)
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+
+ SpiceSessionPrivate *s = session->priv;
+ RingItem *ring, *next;
+ struct channel *c;
+
+ if (s->migration == NULL) {
+ SPICE_DEBUG("no migration in progress");
+ return;
+ }
+
+ SPICE_DEBUG("migration: abort");
+ if (s->migration_state != SPICE_SESSION_MIGRATION_MIGRATING)
+ goto end;
+
+ for (ring = ring_get_head(&s->channels);
+ ring != NULL; ring = next) {
+ next = ring_next(&s->channels, ring);
+ c = SPICE_CONTAINEROF(ring, struct channel, link);
+
+ if (g_list_find(s->migration_left, c->channel))
+ continue;
+
+ spice_channel_swap(c->channel,
+ spice_session_lookup_channel(s->migration,
+ spice_channel_get_channel_id(c->channel),
+ spice_channel_get_channel_type(c->channel)),
+ !s->full_migration);
+ }
+
+end:
+ g_list_free(s->migration_left);
+ s->migration_left = NULL;
+ session_disconnect(s->migration, FALSE);
+ g_object_unref(s->migration);
+ s->migration = NULL;
+
+ s->migrate_wait_init = FALSE;
+ if (s->after_main_init) {
+ g_source_remove(s->after_main_init);
+ s->after_main_init = 0;
+ }
+
+ spice_session_set_migration_state(session, SPICE_SESSION_MIGRATION_NONE);
+}
+
+G_GNUC_INTERNAL
+void spice_session_channel_migrate(SpiceSession *session, SpiceChannel *channel)
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+
+ SpiceSessionPrivate *s = session->priv;
+ SpiceChannel *c;
+ gint id, type;
+
+ g_return_if_fail(s->migration != NULL);
+ g_return_if_fail(SPICE_IS_CHANNEL(channel));
+
+ id = spice_channel_get_channel_id(channel);
+ type = spice_channel_get_channel_type(channel);
+ CHANNEL_DEBUG(channel, "migrating channel id:%d type:%d", id, type);
+
+ c = spice_session_lookup_channel(s->migration, id, type);
+ g_return_if_fail(c != NULL);
+
+ if (!g_queue_is_empty(&c->priv->xmit_queue) && s->full_migration) {
+ CHANNEL_DEBUG(channel, "mig channel xmit queue is not empty. type %s", c->priv->name);
+ }
+ spice_channel_swap(channel, c, !s->full_migration);
+ s->migration_left = g_list_remove(s->migration_left, channel);
+
+ if (g_list_length(s->migration_left) == 0) {
+ CHANNEL_DEBUG(channel, "migration: all channel migrated, success");
+ session_disconnect(s->migration, FALSE);
+ g_object_unref(s->migration);
+ s->migration = NULL;
+ spice_session_set_migration_state(session, SPICE_SESSION_MIGRATION_NONE);
+ }
+}
+
+/* main context */
+static gboolean after_main_init(gpointer data)
+{
+ SpiceSession *self = data;
+ SpiceSessionPrivate *s = self->priv;
+ GList *l;
+
+ for (l = s->migration_left; l != NULL; ) {
+ SpiceChannel *channel = l->data;
+ l = l->next;
+
+ spice_session_channel_migrate(self, channel);
+ channel->priv->state = SPICE_CHANNEL_STATE_READY;
+ spice_channel_up(channel);
+ }
+
+ s->after_main_init = 0;
+ return FALSE;
+}
+
+/* coroutine context */
+G_GNUC_INTERNAL
+gboolean spice_session_migrate_after_main_init(SpiceSession *self)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(self), FALSE);
+
+ SpiceSessionPrivate *s = self->priv;
+
+ if (!s->migrate_wait_init)
+ return FALSE;
+
+ g_return_val_if_fail(g_list_length(s->migration_left) != 0, FALSE);
+ g_return_val_if_fail(s->after_main_init == 0, FALSE);
+
+ s->migrate_wait_init = FALSE;
+ s->after_main_init = g_idle_add(after_main_init, self);
+
+ return TRUE;
+}
+
+/* main context */
+G_GNUC_INTERNAL
+void spice_session_migrate_end(SpiceSession *self)
+{
+ g_return_if_fail(SPICE_IS_SESSION(self));
+
+ SpiceSessionPrivate *s = self->priv;
+ SpiceMsgOut *out;
+ GList *l;
+
+ g_return_if_fail(s->migration);
+ g_return_if_fail(s->migration->priv->cmain);
+ g_return_if_fail(g_list_length(s->migration_left) != 0);
+
+ /* disconnect and reset all channels */
+ for (l = s->migration_left; l != NULL; ) {
+ SpiceChannel *channel = l->data;
+ l = l->next;
+
+ if (!SPICE_IS_MAIN_CHANNEL(channel)) {
+ /* freeze other channels */
+ channel->priv->state = SPICE_CHANNEL_STATE_MIGRATING;
+ }
+
+ /* reset for migration, disconnect */
+ spice_channel_reset(channel, TRUE);
+
+ if (SPICE_IS_MAIN_CHANNEL(channel)) {
+ /* migrate main to target, so we can start talking */
+ spice_session_channel_migrate(self, channel);
+ }
+ }
+
+ cache_clear_all(self);
+
+ /* send MIGRATE_END to target */
+ out = spice_msg_out_new(s->cmain, SPICE_MSGC_MAIN_MIGRATE_END);
+ spice_msg_out_send(out);
+
+ /* now wait after main init for the rest of channels migration */
+ s->migrate_wait_init = TRUE;
+}
+
+/**
+ * spice_session_get_read_only:
+ * @session: a #SpiceSession
+ *
+ * Returns: wether the @session is in read-only mode.
+ **/
+gboolean spice_session_get_read_only(SpiceSession *self)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(self), FALSE);
+
+ return self->priv->read_only;
+}
+
+static gboolean session_disconnect_idle(SpiceSession *self)
+{
+ SpiceSessionPrivate *s = self->priv;
+
+ session_disconnect(self, FALSE);
+ s->disconnecting = 0;
+
+ g_object_unref(self);
+
+ return FALSE;
+}
+
+/**
+ * spice_session_disconnect:
+ * @session:
+ *
+ * Disconnect the @session, and destroy all channels.
+ **/
+void spice_session_disconnect(SpiceSession *session)
+{
+ SpiceSessionPrivate *s;
+
+ g_return_if_fail(SPICE_IS_SESSION(session));
+
+ s = session->priv;
+
+ SPICE_DEBUG("session: disconnecting %d", s->disconnecting);
+ if (s->disconnecting != 0)
+ return;
+
+ g_object_ref(session);
+ s->disconnecting = g_idle_add((GSourceFunc)session_disconnect_idle, session);
+}
+
+/**
+ * spice_session_get_channels:
+ * @session: a #SpiceSession
+ *
+ * Get the list of current channels associated with this @session.
+ *
+ * Returns: (element-type SpiceChannel) (transfer container): a #GList
+ * of unowned #SpiceChannel channels.
+ **/
+GList *spice_session_get_channels(SpiceSession *session)
+{
+ SpiceSessionPrivate *s;
+ struct channel *item;
+ GList *list = NULL;
+ RingItem *ring;
+
+ g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+ g_return_val_if_fail(session->priv != NULL, NULL);
+
+ s = session->priv;
+
+ for (ring = ring_get_head(&s->channels);
+ ring != NULL;
+ ring = ring_next(&s->channels, ring)) {
+ item = SPICE_CONTAINEROF(ring, struct channel, link);
+ list = g_list_append(list, item->channel);
+ }
+ return list;
+}
+
+/**
+ * spice_session_has_channel_type:
+ * @session: a #SpiceSession
+ *
+ * See if there is a @type channel in the channels associated with this
+ * @session.
+ *
+ * Returns: TRUE if a @type channel is available otherwise FALSE.
+ **/
+gboolean spice_session_has_channel_type(SpiceSession *session, gint type)
+{
+ SpiceSessionPrivate *s;
+ struct channel *item;
+ RingItem *ring;
+
+ g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);
+ g_return_val_if_fail(session->priv != NULL, FALSE);
+
+ s = session->priv;
+
+ for (ring = ring_get_head(&s->channels);
+ ring != NULL;
+ ring = ring_next(&s->channels, ring)) {
+ item = SPICE_CONTAINEROF(ring, struct channel, link);
+ if (spice_channel_get_channel_type(item->channel) == type) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+}
+
+/* ------------------------------------------------------------------ */
+/* private functions */
+
+typedef struct spice_open_host spice_open_host;
+
+struct spice_open_host {
+ struct coroutine *from;
+ SpiceSession *session;
+ SpiceChannel *channel;
+ SpiceURI *proxy;
+ int port;
+ GCancellable *cancellable;
+ GError *error;
+ GSocketConnection *connection;
+ GSocketClient *client;
+};
+
+static void socket_client_connect_ready(GObject *source_object, GAsyncResult *result,
+ gpointer data)
+{
+ GSocketClient *client = G_SOCKET_CLIENT(source_object);
+ spice_open_host *open_host = data;
+ GSocketConnection *connection = NULL;
+
+ CHANNEL_DEBUG(open_host->channel, "connect ready");
+ connection = g_socket_client_connect_finish(client, result, &open_host->error);
+ if (connection == NULL) {
+ g_warn_if_fail(open_host->error != NULL);
+ goto end;
+ }
+
+ open_host->connection = connection;
+
+end:
+ coroutine_yieldto(open_host->from, NULL);
+}
+
+/* main context */
+static void open_host_connectable_connect(spice_open_host *open_host, GSocketConnectable *connectable)
+{
+ CHANNEL_DEBUG(open_host->channel, "connecting %p...", open_host);
+
+ g_socket_client_connect_async(open_host->client, connectable,
+ open_host->cancellable,
+ socket_client_connect_ready, open_host);
+}
+
+/* main context */
+static void proxy_lookup_ready(GObject *source_object, GAsyncResult *result,
+ gpointer data)
+{
+ spice_open_host *open_host = data;
+ SpiceSession *session = open_host->session;
+ SpiceSessionPrivate *s = session->priv;
+ GList *addresses = NULL, *it;
+ GSocketAddress *address;
+
+ SPICE_DEBUG("proxy lookup ready");
+ addresses = g_resolver_lookup_by_name_finish(G_RESOLVER(source_object),
+ result, &open_host->error);
+ if (addresses == NULL || open_host->error) {
+ g_prefix_error(&open_host->error, "SPICE proxy: ");
+ coroutine_yieldto(open_host->from, NULL);
+ return;
+ }
+
+ for (it = addresses; it != NULL; it = it->next) {
+ address = g_proxy_address_new(G_INET_ADDRESS(it->data),
+ spice_uri_get_port(open_host->proxy),
+ spice_uri_get_scheme(open_host->proxy),
+ s->host, open_host->port,
+ spice_uri_get_user(open_host->proxy),
+ spice_uri_get_password(open_host->proxy));
+ if (address != NULL)
+ break;
+ }
+
+ open_host_connectable_connect(open_host, G_SOCKET_CONNECTABLE(address));
+ g_resolver_free_addresses(addresses);
+ g_object_unref(address);
+}
+
+/* main context */
+static gboolean open_host_idle_cb(gpointer data)
+{
+ spice_open_host *open_host = data;
+ SpiceSessionPrivate *s;
+
+ g_return_val_if_fail(open_host != NULL, FALSE);
+ g_return_val_if_fail(open_host->connection == NULL, FALSE);
+
+ if (spice_channel_get_session(open_host->channel) != open_host->session)
+ return FALSE;
+
+ s = open_host->session->priv;
+ open_host->proxy = s->proxy;
+ if (open_host->error != NULL) {
+ coroutine_yieldto(open_host->from, NULL);
+ return FALSE;
+ }
+
+ if (open_host->proxy) {
+ g_resolver_lookup_by_name_async(g_resolver_get_default(),
+ spice_uri_get_hostname(open_host->proxy),
+ open_host->cancellable,
+ proxy_lookup_ready, open_host);
+ } else {
+ GSocketConnectable *address = NULL;
+
+ if (s->unix_path) {
+ SPICE_DEBUG("open unix path %s", s->unix_path);
+#ifdef G_OS_UNIX
+ address = G_SOCKET_CONNECTABLE(g_unix_socket_address_new(s->unix_path));
+#else
+ g_set_error_literal(&open_host->error, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "Unix path unsupported on this platform");
+#endif
+ } else {
+ SPICE_DEBUG("open host %s:%d", s->host, open_host->port);
+ address = g_network_address_new(s->host, open_host->port);
+ }
+
+ if (address == NULL || open_host->error != NULL) {
+ coroutine_yieldto(open_host->from, NULL);
+ return FALSE;
+ }
+
+ open_host_connectable_connect(open_host, address);
+ g_object_unref(address);
+ }
+
+ if (open_host->proxy != NULL) {
+ gchar *str = spice_uri_to_string(open_host->proxy);
+ SPICE_DEBUG("(with proxy %s)", str);
+ g_free(str);
+ }
+
+ return FALSE;
+}
+
+#define SOCKET_TIMEOUT 10
+
+/* coroutine context */
+G_GNUC_INTERNAL
+GSocketConnection* spice_session_channel_open_host(SpiceSession *session, SpiceChannel *channel,
+ gboolean *use_tls, GError **error)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+
+ SpiceSessionPrivate *s = session->priv;
+ SpiceChannelPrivate *c = channel->priv;
+ spice_open_host open_host = { 0, };
+ gchar *port, *endptr;
+
+ // FIXME: make open_host() cancellable
+ open_host.from = coroutine_self();
+ open_host.session = session;
+ open_host.channel = channel;
+
+ const char *name = spice_channel_type_to_string(c->channel_type);
+ if (spice_strv_contains(s->secure_channels, "all") ||
+ spice_strv_contains(s->secure_channels, name))
+ *use_tls = TRUE;
+
+ if (s->unix_path) {
+ if (*use_tls) {
+ CHANNEL_DEBUG(channel, "No TLS for Unix sockets");
+ return NULL;
+ }
+ } else {
+ port = *use_tls ? s->tls_port : s->port;
+ if (port == NULL) {
+ g_debug("Missing port value, not attempting %s connection.",
+ *use_tls?"TLS":"unencrypted");
+ return NULL;
+ }
+
+ open_host.port = strtol(port, &endptr, 10);
+ if (*port == '\0' || *endptr != '\0' ||
+ open_host.port <= 0 || open_host.port > G_MAXUINT16) {
+ g_warning("Invalid port value %s", port);
+ return NULL;
+ }
+ }
+ if (*use_tls) {
+ CHANNEL_DEBUG(channel, "Using TLS, port %d", open_host.port);
+ } else {
+ CHANNEL_DEBUG(channel, "Using plain text, port %d", open_host.port);
+ }
+
+ open_host.client = g_socket_client_new();
+ g_socket_client_set_enable_proxy(open_host.client, s->proxy != NULL);
+ g_socket_client_set_timeout(open_host.client, SOCKET_TIMEOUT);
+
+ g_idle_add(open_host_idle_cb, &open_host);
+ /* switch to main loop and wait for connection */
+ coroutine_yield(NULL);
+
+ if (open_host.error != NULL) {
+ CHANNEL_DEBUG(channel, "open host: %s", open_host.error->message);
+ g_propagate_error(error, open_host.error);
+ } else if (open_host.connection != NULL) {
+ GSocket *socket;
+ socket = g_socket_connection_get_socket(open_host.connection);
+ g_socket_set_timeout(socket, 0);
+ g_socket_set_blocking(socket, FALSE);
+ g_socket_set_keepalive(socket, TRUE);
+ }
+
+ g_clear_object(&open_host.client);
+ return open_host.connection;
+}
+
+
+G_GNUC_INTERNAL
+void spice_session_channel_new(SpiceSession *session, SpiceChannel *channel)
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+ g_return_if_fail(SPICE_IS_CHANNEL(channel));
+
+ SpiceSessionPrivate *s = session->priv;
+ struct channel *item;
+
+
+ item = g_new0(struct channel, 1);
+ item->channel = channel;
+ ring_add(&s->channels, &item->link);
+
+ if (SPICE_IS_MAIN_CHANNEL(channel)) {
+ gboolean all = spice_strv_contains(s->disable_effects, "all");
+
+ g_object_set(channel,
+ "disable-wallpaper", all || spice_strv_contains(s->disable_effects, "wallpaper"),
+ "disable-font-smooth", all || spice_strv_contains(s->disable_effects, "font-smooth"),
+ "disable-animation", all || spice_strv_contains(s->disable_effects, "animation"),
+ NULL);
+ if (s->color_depth != 0)
+ g_object_set(channel, "color-depth", s->color_depth, NULL);
+
+ CHANNEL_DEBUG(channel, "new main channel, switching");
+ s->cmain = channel;
+ } else if (SPICE_IS_PLAYBACK_CHANNEL(channel)) {
+ g_warn_if_fail(s->playback_channel == NULL);
+ s->playback_channel = SPICE_PLAYBACK_CHANNEL(channel);
+ }
+
+ g_signal_emit(session, signals[SPICE_SESSION_CHANNEL_NEW], 0, channel);
+}
+
+static void spice_session_channel_destroy(SpiceSession *session, SpiceChannel *channel)
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+ g_return_if_fail(SPICE_IS_CHANNEL(channel));
+
+ SpiceSessionPrivate *s = session->priv;
+ struct channel *item = NULL;
+ RingItem *ring;
+
+ if (s->migration_left)
+ s->migration_left = g_list_remove(s->migration_left, channel);
+
+ for (ring = ring_get_head(&s->channels); ring != NULL;
+ ring = ring_next(&s->channels, ring)) {
+ item = SPICE_CONTAINEROF(ring, struct channel, link);
+ if (item->channel == channel)
+ break;
+ }
+
+ g_return_if_fail(ring != NULL);
+
+ if (channel == s->cmain) {
+ CHANNEL_DEBUG(channel, "the session lost the main channel");
+ s->cmain = NULL;
+ }
+
+ ring_remove(&item->link);
+ free(item);
+
+ g_signal_emit(session, signals[SPICE_SESSION_CHANNEL_DESTROY], 0, channel);
+
+ g_clear_object(&channel->priv->session);
+ spice_channel_disconnect(channel, SPICE_CHANNEL_NONE);
+ g_object_unref(channel);
+}
+
+G_GNUC_INTERNAL
+void spice_session_set_connection_id(SpiceSession *session, int id)
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+
+ SpiceSessionPrivate *s = session->priv;
+
+ s->connection_id = id;
+}
+
+G_GNUC_INTERNAL
+int spice_session_get_connection_id(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), -1);
+
+ SpiceSessionPrivate *s = session->priv;
+
+ return s->connection_id;
+}
+
+G_GNUC_INTERNAL
+guint32 spice_session_get_mm_time(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), 0);
+
+ SpiceSessionPrivate *s = session->priv;
+
+ /* FIXME: we may want to estimate the drift of clocks, and well,
+ do something better than this trivial approach */
+ return s->mm_time + (g_get_monotonic_time() - s->mm_time_at_clock) / 1000;
+}
+
+#define MM_TIME_DIFF_RESET_THRESH 500 // 0.5 sec
+
+G_GNUC_INTERNAL
+void spice_session_set_mm_time(SpiceSession *session, guint32 time)
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+
+ SpiceSessionPrivate *s = session->priv;
+ guint32 old_time;
+
+ old_time = spice_session_get_mm_time(session);
+
+ s->mm_time = time;
+ s->mm_time_at_clock = g_get_monotonic_time();
+ SPICE_DEBUG("set mm time: %u", spice_session_get_mm_time(session));
+ if (time > old_time + MM_TIME_DIFF_RESET_THRESH ||
+ time < old_time) {
+ SPICE_DEBUG("%s: mm-time-reset, old %u, new %u", __FUNCTION__, old_time, s->mm_time);
+ g_coroutine_signal_emit(session, signals[SPICE_SESSION_MM_TIME_RESET], 0);
+ }
+}
+
+G_GNUC_INTERNAL
+void spice_session_set_port(SpiceSession *session, int port, gboolean tls)
+{
+ const char *prop = tls ? "tls-port" : "port";
+ char *tmp;
+
+ g_return_if_fail(SPICE_IS_SESSION(session));
+
+ /* old spicec client doesn't accept port == 0, see Migrate::start */
+ tmp = port > 0 ? g_strdup_printf("%d", port) : NULL;
+ g_object_set(session, prop, tmp, NULL);
+ g_free(tmp);
+}
+
+G_GNUC_INTERNAL
+void spice_session_get_pubkey(SpiceSession *session, guint8 **pubkey, guint *size)
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+ g_return_if_fail(pubkey != NULL);
+ g_return_if_fail(size != NULL);
+
+ SpiceSessionPrivate *s = session->priv;
+
+ *pubkey = s->pubkey ? s->pubkey->data : NULL;
+ *size = s->pubkey ? s->pubkey->len : 0;
+}
+
+G_GNUC_INTERNAL
+void spice_session_get_ca(SpiceSession *session, guint8 **ca, guint *size)
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+ g_return_if_fail(ca != NULL);
+ g_return_if_fail(size != NULL);
+
+ SpiceSessionPrivate *s = session->priv;
+
+ *ca = s->ca ? s->ca->data : NULL;
+ *size = s->ca ? s->ca->len : 0;
+}
+
+G_GNUC_INTERNAL
+guint spice_session_get_verify(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), 0);
+
+ SpiceSessionPrivate *s = session->priv;
+
+ return s->verify;
+}
+
+G_GNUC_INTERNAL
+void spice_session_set_migration_state(SpiceSession *session, SpiceSessionMigration state)
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+
+ SpiceSessionPrivate *s = session->priv;
+
+ if (state == SPICE_SESSION_MIGRATION_CONNECTING)
+ s->for_migration = true;
+
+ s->migration_state = state;
+ g_coroutine_object_notify(G_OBJECT(session), "migration-state");
+}
+
+G_GNUC_INTERNAL
+const gchar* spice_session_get_username(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+
+ SpiceSessionPrivate *s = session->priv;
+
+ return s->username;
+}
+
+G_GNUC_INTERNAL
+const gchar* spice_session_get_password(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+
+ SpiceSessionPrivate *s = session->priv;
+
+ return s->password;
+}
+
+G_GNUC_INTERNAL
+const gchar* spice_session_get_host(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+
+ SpiceSessionPrivate *s = session->priv;
+
+ return s->host;
+}
+
+G_GNUC_INTERNAL
+const gchar* spice_session_get_cert_subject(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+
+ SpiceSessionPrivate *s = session->priv;
+
+ return s->cert_subject;
+}
+
+G_GNUC_INTERNAL
+const gchar* spice_session_get_ciphers(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+
+ SpiceSessionPrivate *s = session->priv;
+
+ return s->ciphers;
+}
+
+G_GNUC_INTERNAL
+const gchar* spice_session_get_ca_file(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+
+ SpiceSessionPrivate *s = session->priv;
+
+ return s->ca_file;
+}
+
+G_GNUC_INTERNAL
+void spice_session_get_caches(SpiceSession *session,
+ display_cache **images,
+ SpiceGlzDecoderWindow **glz_window)
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+
+ SpiceSessionPrivate *s = session->priv;
+
+ if (images)
+ *images = s->images;
+ if (glz_window)
+ *glz_window = s->glz_window;
+}
+
+G_GNUC_INTERNAL
+void spice_session_set_caches_hints(SpiceSession *session,
+ uint32_t pci_ram_size,
+ uint32_t n_display_channels)
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+
+ SpiceSessionPrivate *s = session->priv;
+
+ s->pci_ram_size = pci_ram_size;
+ s->n_display_channels = n_display_channels;
+
+ /* TODO: when setting cache and window size, we should consider the client's
+ * available memory and the number of display channels */
+ if (s->images_cache_size == 0) {
+ s->images_cache_size = IMAGES_CACHE_SIZE_DEFAULT;
+ }
+
+ if (s->glz_window_size == 0) {
+ s->glz_window_size = MIN(MAX_GLZ_WINDOW_SIZE_DEFAULT, pci_ram_size / 2);
+ s->glz_window_size = MAX(MIN_GLZ_WINDOW_SIZE_DEFAULT, s->glz_window_size);
+ }
+}
+
+G_GNUC_INTERNAL
+guint spice_session_get_n_display_channels(SpiceSession *session)
+{
+ g_return_val_if_fail(session != NULL, 0);
+
+ return session->priv->n_display_channels;
+}
+
+G_GNUC_INTERNAL
+void spice_session_set_uuid(SpiceSession *session, guint8 uuid[16])
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+
+ SpiceSessionPrivate *s = session->priv;
+
+ memcpy(s->uuid, uuid, sizeof(s->uuid));
+
+ g_coroutine_object_notify(G_OBJECT(session), "uuid");
+}
+
+G_GNUC_INTERNAL
+void spice_session_set_name(SpiceSession *session, const gchar *name)
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+
+ SpiceSessionPrivate *s = session->priv;
+
+ g_free(s->name);
+ s->name = g_strdup(name);
+
+ g_coroutine_object_notify(G_OBJECT(session), "name");
+}
+
+G_GNUC_INTERNAL
+void spice_session_sync_playback_latency(SpiceSession *session)
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+
+ SpiceSessionPrivate *s = session->priv;
+
+ if (s->playback_channel &&
+ spice_playback_channel_is_active(s->playback_channel)) {
+ spice_playback_channel_sync_latency(s->playback_channel);
+ } else {
+ SPICE_DEBUG("%s: not implemented when there isn't audio playback", __FUNCTION__);
+ }
+}
+
+G_GNUC_INTERNAL
+gboolean spice_session_is_playback_active(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);
+
+ SpiceSessionPrivate *s = session->priv;
+
+ return (s->playback_channel &&
+ spice_playback_channel_is_active(s->playback_channel));
+}
+
+G_GNUC_INTERNAL
+guint32 spice_session_get_playback_latency(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), 0);
+
+ SpiceSessionPrivate *s = session->priv;
+
+ if (s->playback_channel &&
+ spice_playback_channel_is_active(s->playback_channel)) {
+ return spice_playback_channel_get_latency(s->playback_channel);
+ } else {
+ SPICE_DEBUG("%s: not implemented when there isn't audio playback", __FUNCTION__);
+ return 0;
+ }
+}
+
+G_GNUC_INTERNAL
+const gchar* spice_session_get_shared_dir(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+
+ SpiceSessionPrivate *s = session->priv;
+
+ return s->shared_dir;
+}
+
+G_GNUC_INTERNAL
+void spice_session_set_shared_dir(SpiceSession *session, const gchar *dir)
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+
+ SpiceSessionPrivate *s = session->priv;
+
+ g_free(s->shared_dir);
+ s->shared_dir = g_strdup(dir);
+}
+
+/**
+ * spice_session_get_proxy_uri:
+ * @session: a #SpiceSession
+ *
+ * Returns: (transfer none): the session proxy #SpiceURI or %NULL.
+ * Since: 0.24
+ **/
+SpiceURI *spice_session_get_proxy_uri(SpiceSession *session)
+{
+ SpiceSessionPrivate *s;
+
+ g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+ g_return_val_if_fail(session->priv != NULL, NULL);
+
+ s = session->priv;
+
+ return s->proxy;
+}
+
+/**
+ * spice_audio_get:
+ * @session: the #SpiceSession to connect to
+ * @context: (allow-none): a #GMainContext to attach to (or %NULL for default).
+ *
+ * Gets the #SpiceAudio associated with the passed in #SpiceSession.
+ * A new #SpiceAudio instance will be created the first time this
+ * function is called for a certain #SpiceSession.
+ *
+ * Note that this function returns a weak reference, which should not be used
+ * after the #SpiceSession itself has been unref-ed by the caller.
+ *
+ * Returns: (transfer none): a weak reference to a #SpiceAudio
+ * instance or %NULL if failed.
+ **/
+SpiceAudio *spice_audio_get(SpiceSession *session, GMainContext *context)
+{
+ static GStaticMutex mutex = G_STATIC_MUTEX_INIT;
+ SpiceAudio *self;
+
+ g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+
+ g_static_mutex_lock(&mutex);
+ self = session->priv->audio_manager;
+ if (self == NULL) {
+ self = spice_audio_new(session, context, NULL);
+ session->priv->audio_manager = self;
+ }
+ g_static_mutex_unlock(&mutex);
+
+ return self;
+}
+
+/**
+ * spice_usb_device_manager_get:
+ * @session: #SpiceSession for which to get the #SpiceUsbDeviceManager
+ *
+ * Gets the #SpiceUsbDeviceManager associated with the passed in #SpiceSession.
+ * A new #SpiceUsbDeviceManager instance will be created the first time this
+ * function is called for a certain #SpiceSession.
+ *
+ * Note that this function returns a weak reference, which should not be used
+ * after the #SpiceSession itself has been unref-ed by the caller.
+ *
+ * Returns: (transfer none): a weak reference to the #SpiceUsbDeviceManager associated with the passed in #SpiceSession
+ */
+SpiceUsbDeviceManager *spice_usb_device_manager_get(SpiceSession *session,
+ GError **err)
+{
+ SpiceUsbDeviceManager *self;
+ static GStaticMutex mutex = G_STATIC_MUTEX_INIT;
+
+ g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+ g_return_val_if_fail(err == NULL || *err == NULL, NULL);
+
+ g_static_mutex_lock(&mutex);
+ self = session->priv->usb_manager;
+ if (self == NULL) {
+ self = g_initable_new(SPICE_TYPE_USB_DEVICE_MANAGER, NULL, err,
+ "session", session, NULL);
+ session->priv->usb_manager = self;
+ }
+ g_static_mutex_unlock(&mutex);
+
+ return self;
+}
+
+G_GNUC_INTERNAL
+gboolean spice_session_get_audio_enabled(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);
+
+ return session->priv->audio;
+}
+
+G_GNUC_INTERNAL
+gboolean spice_session_get_usbredir_enabled(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);
+
+ return session->priv->usbredir;
+}
+
+G_GNUC_INTERNAL
+gboolean spice_session_get_smartcard_enabled(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);
+
+ return session->priv->smartcard;
+}
+
+G_GNUC_INTERNAL
+PhodavServer* spice_session_get_webdav_server(SpiceSession *session)
+{
+ SpiceSessionPrivate *priv;
+
+ g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+ priv = session->priv;
+
+#ifdef USE_PHODAV
+ static GMutex mutex;
+
+ const gchar *shared_dir = spice_session_get_shared_dir(session);
+ if (shared_dir == NULL) {
+ g_debug("No shared dir set, not creating webdav server");
+ return NULL;
+ }
+
+ g_mutex_lock(&mutex);
+
+ if (priv->webdav)
+ goto end;
+
+ priv->webdav = phodav_server_new(shared_dir);
+ g_object_bind_property(session, "share-dir-ro",
+ priv->webdav, "read-only",
+ G_BINDING_SYNC_CREATE|G_BINDING_BIDIRECTIONAL);
+ g_object_bind_property(session, "shared-dir",
+ priv->webdav, "root",
+ G_BINDING_SYNC_CREATE|G_BINDING_BIDIRECTIONAL);
+
+end:
+ g_mutex_unlock(&mutex);
+#endif
+
+ return priv->webdav;
+}
+
+/**
+ * spice_session_is_for_migration:
+ * @session: a Spice session
+ *
+ * During seamless migration, channels may be created to establish a
+ * connection with the target, but they are temporary and should only
+ * handle migration steps. In order to avoid other interactions with
+ * the client, channels should check this value.
+ *
+ * Returns: %TRUE if the session is a copy created during migration
+ * Since: 0.27
+ **/
+gboolean spice_session_is_for_migration(SpiceSession *session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);
+
+ return session->priv->for_migration;
+}
+
+G_GNUC_INTERNAL
+void spice_session_set_main_channel(SpiceSession *session, SpiceChannel *channel)
+{
+ g_return_if_fail(SPICE_IS_SESSION(session));
+ g_return_if_fail(SPICE_IS_CHANNEL(channel));
+ g_return_if_fail(session->priv->cmain == NULL);
+
+ session->priv->cmain = channel;
+}
+
+G_GNUC_INTERNAL
+gboolean spice_session_set_migration_session(SpiceSession *session, SpiceSession *mig_session)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);
+ g_return_val_if_fail(SPICE_IS_SESSION(mig_session), FALSE);
+ g_return_val_if_fail(session->priv->migration == NULL, FALSE);
+
+ session->priv->migration = mig_session;
+
+ return TRUE;
+}
diff --git a/src/spice-session.h b/src/spice-session.h
new file mode 100644
index 0000000..750af29
--- /dev/null
+++ b/src/spice-session.h
@@ -0,0 +1,103 @@
+/* -*- 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_CLIENT_SESSION_H__
+#define __SPICE_CLIENT_SESSION_H__
+
+#include <glib-object.h>
+#include "spice-types.h"
+#include "spice-uri.h"
+#include "spice-glib-enums.h"
+#include "spice-util.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_SESSION (spice_session_get_type ())
+#define SPICE_SESSION(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SPICE_TYPE_SESSION, SpiceSession))
+#define SPICE_SESSION_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SPICE_TYPE_SESSION, SpiceSessionClass))
+#define SPICE_IS_SESSION(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SPICE_TYPE_SESSION))
+#define SPICE_IS_SESSION_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SPICE_TYPE_SESSION))
+#define SPICE_SESSION_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SPICE_TYPE_SESSION, SpiceSessionClass))
+
+/**
+ * SpiceSessionVerify:
+ * @SPICE_SESSION_VERIFY_PUBKEY: verify certificate public key matching
+ * @SPICE_SESSION_VERIFY_HOSTNAME: verify certificate hostname matching
+ * @SPICE_SESSION_VERIFY_SUBJECT: verify certificate subject matching
+ *
+ * Peer certificate verification parameters flags.
+ **/
+typedef enum {
+ SPICE_SESSION_VERIFY_PUBKEY = (1 << 0),
+ SPICE_SESSION_VERIFY_HOSTNAME = (1 << 1),
+ SPICE_SESSION_VERIFY_SUBJECT = (1 << 2),
+} SpiceSessionVerify;
+
+/**
+ * SpiceSessionMigration:
+ * @SPICE_SESSION_MIGRATION_NONE: no migration going on
+ * @SPICE_SESSION_MIGRATION_SWITCHING: the session is switching host (destroy and reconnect)
+ * @SPICE_SESSION_MIGRATION_MIGRATING: the session is migrating seamlessly (reconnect)
+ * @SPICE_SESSION_MIGRATION_CONNECTING: the migration is connecting to destination (Since: 0.27)
+ *
+ * Session migration state.
+ **/
+typedef enum {
+ SPICE_SESSION_MIGRATION_NONE,
+ SPICE_SESSION_MIGRATION_SWITCHING,
+ SPICE_SESSION_MIGRATION_MIGRATING,
+ SPICE_SESSION_MIGRATION_CONNECTING,
+} SpiceSessionMigration;
+
+struct _SpiceSession
+{
+ GObject parent;
+ SpiceSessionPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+struct _SpiceSessionClass
+{
+ GObjectClass parent_class;
+
+ /* signals */
+ void (*channel_new)(SpiceSession *session, SpiceChannel *channel);
+ void (*channel_destroy)(SpiceSession *session, SpiceChannel *channel);
+
+ /*< private >*/
+ /*
+ * If adding fields to this struct, remove corresponding
+ * amount of padding to avoid changing overall struct size
+ */
+ gchar _spice_reserved[SPICE_RESERVED_PADDING];
+};
+
+GType spice_session_get_type(void);
+
+SpiceSession *spice_session_new(void);
+gboolean spice_session_connect(SpiceSession *session);
+gboolean spice_session_open_fd(SpiceSession *session, int fd);
+void spice_session_disconnect(SpiceSession *session);
+GList *spice_session_get_channels(SpiceSession *session);
+gboolean spice_session_has_channel_type(SpiceSession *session, gint type);
+gboolean spice_session_get_read_only(SpiceSession *session);
+SpiceURI *spice_session_get_proxy_uri(SpiceSession *session);
+gboolean spice_session_is_for_migration(SpiceSession *session);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_SESSION_H__ */
diff --git a/src/spice-types.h b/src/spice-types.h
new file mode 100644
index 0000000..f149094
--- /dev/null
+++ b/src/spice-types.h
@@ -0,0 +1,35 @@
+/* -*- 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_CLIENT_TYPES_H__
+#define __SPICE_CLIENT_TYPES_H__
+
+G_BEGIN_DECLS
+
+/* SpiceSession */
+typedef struct _SpiceSession SpiceSession;
+typedef struct _SpiceSessionClass SpiceSessionClass;
+typedef struct _SpiceSessionPrivate SpiceSessionPrivate;
+
+/* SpiceChannel */
+typedef struct _SpiceChannel SpiceChannel;
+typedef struct _SpiceChannelClass SpiceChannelClass;
+typedef struct _SpiceChannelPrivate SpiceChannelPrivate;
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_TYPES_H__ */
diff --git a/src/spice-uri-priv.h b/src/spice-uri-priv.h
new file mode 100644
index 0000000..54351de
--- /dev/null
+++ b/src/spice-uri-priv.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 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_URI_PRIV_H__
+#define __SPICE_URI_PRIV_H__
+
+#include "spice-uri.h"
+
+G_BEGIN_DECLS
+
+SpiceURI* spice_uri_new(void);
+gboolean spice_uri_parse(SpiceURI* self, const gchar* uri, GError** error);
+
+G_END_DECLS
+
+#endif /* __SPICE_URI_PRIV_H__ */
diff --git a/src/spice-uri.c b/src/spice-uri.c
new file mode 100644
index 0000000..82aefdb
--- /dev/null
+++ b/src/spice-uri.c
@@ -0,0 +1,462 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 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 <stdlib.h>
+#include <string.h>
+
+#include "glib-compat.h"
+#include "spice-client.h"
+#include "spice-uri.h"
+
+/**
+ * SECTION:spice-uri
+ * @short_description: URIs handling
+ * @title: SpiceURI
+ * @section_id:
+ * @stability: Stable
+ * @include: spice-uri.h
+ *
+ * A SpiceURI represents a (parsed) URI.
+ * Since: 0.24
+ */
+
+struct _SpiceURI {
+ GObject parent_instance;
+ gchar *scheme;
+ gchar *hostname;
+ guint port;
+ gchar *user;
+ gchar *password;
+};
+
+struct _SpiceURIClass {
+ GObjectClass parent_class;
+};
+
+G_DEFINE_TYPE(SpiceURI, spice_uri, G_TYPE_OBJECT);
+
+enum {
+ SPICE_URI_DUMMY_PROPERTY,
+ SPICE_URI_SCHEME,
+ SPICE_URI_USER,
+ SPICE_URI_PASSWORD,
+ SPICE_URI_HOSTNAME,
+ SPICE_URI_PORT
+};
+
+G_GNUC_INTERNAL
+SpiceURI* spice_uri_new(void)
+{
+ SpiceURI * self = NULL;
+ self = (SpiceURI*)g_object_new(SPICE_TYPE_URI, NULL);
+ return self;
+}
+
+G_GNUC_INTERNAL
+gboolean spice_uri_parse(SpiceURI *self, const gchar *_uri, GError **error)
+{
+ gchar *dup, *uri;
+ gboolean success = FALSE;
+ size_t len;
+
+ g_return_val_if_fail(self != NULL, FALSE);
+ g_return_val_if_fail(_uri != NULL, FALSE);
+
+ uri = dup = g_strdup(_uri);
+ /* FIXME: use GUri when it is ready... only support http atm */
+ /* the code is voluntarily not parsing thoroughly the uri */
+ if (g_ascii_strncasecmp("http://", uri, 7) == 0) {
+ uri += 7;
+ spice_uri_set_scheme(self, "http");
+ spice_uri_set_port(self, 3128);
+ } else if (g_ascii_strncasecmp("https://", uri, 8) == 0) {
+ uri += 8;
+ spice_uri_set_scheme(self, "https");
+ spice_uri_set_port(self, 3129);
+ } else {
+ spice_uri_set_scheme(self, "http");
+ spice_uri_set_port(self, 3128);
+ }
+ /* remove trailing slash */
+ len = strlen(uri);
+ for (; len > 0; len--)
+ if (uri[len-1] == '/')
+ uri[len-1] = '\0';
+ else
+ break;
+
+
+ /* yes, that parser is bad, we need GUri... */
+ if (strstr(uri, "@")) {
+ gchar *saveptr = NULL, *saveptr2 = NULL;
+ gchar *next = strstr(uri, "@") + 1;
+ gchar *auth = strtok_r(uri, "@", &saveptr);
+ const gchar *user = strtok_r(auth, ":", &saveptr2);
+ const gchar *pass = strtok_r(NULL, ":", &saveptr2);
+ spice_uri_set_user(self, user);
+ spice_uri_set_password(self, pass);
+ uri = next;
+ }
+
+ /* max 2 parts, host:port */
+ gchar **uriv = g_strsplit(uri, ":", 2);
+ const gchar *uri_port = NULL;
+
+ if (uriv[0] == NULL || strlen(uriv[0]) == 0) {
+ g_set_error(error, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "Invalid hostname in uri address");
+ goto end;
+ }
+
+ spice_uri_set_hostname(self, uriv[0]);
+ if (uriv[0] != NULL)
+ uri_port = uriv[1];
+
+ if (uri_port != NULL) {
+ char *endptr;
+ guint port = strtoul(uri_port, &endptr, 10);
+ if (*endptr != '\0') {
+ g_set_error(error, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "Invalid uri port: %s", uri_port);
+ goto end;
+ }
+ spice_uri_set_port(self, port);
+ }
+
+ success = TRUE;
+
+end:
+ g_free(dup);
+ g_strfreev(uriv);
+ return success;
+}
+
+/**
+ * spice_uri_get_scheme:
+ * @uri: a #SpiceURI
+ *
+ * Gets @uri's scheme.
+ *
+ * Returns: @uri's scheme.
+ * Since: 0.24
+ **/
+const gchar* spice_uri_get_scheme(SpiceURI *self)
+{
+ g_return_val_if_fail(SPICE_IS_URI(self), NULL);
+ return self->scheme;
+}
+
+/**
+ * spice_uri_set_scheme:
+ * @uri: a #SpiceURI
+ * @scheme: the scheme
+ *
+ * Sets @uri's scheme to @scheme.
+ * Since: 0.24
+ **/
+void spice_uri_set_scheme(SpiceURI *self, const gchar *scheme)
+{
+ g_return_if_fail(SPICE_IS_URI(self));
+
+ g_free(self->scheme);
+ self->scheme = g_strdup(scheme);
+ g_object_notify((GObject *)self, "scheme");
+}
+
+/**
+ * spice_uri_get_hostname:
+ * @uri: a #SpiceURI
+ *
+ * Gets @uri's hostname.
+ *
+ * Returns: @uri's hostname.
+ * Since: 0.24
+ **/
+const gchar* spice_uri_get_hostname(SpiceURI *self)
+{
+ g_return_val_if_fail(SPICE_IS_URI(self), NULL);
+ return self->hostname;
+}
+
+
+/**
+ * spice_uri_set_hostname:
+ * @uri: a #SpiceURI
+ * @hostname: the hostname
+ *
+ * Sets @uri's hostname to @hostname.
+ * Since: 0.24
+ **/
+void spice_uri_set_hostname(SpiceURI *self, const gchar *hostname)
+{
+ g_return_if_fail(SPICE_IS_URI(self));
+
+ g_free(self->hostname);
+ self->hostname = g_strdup(hostname);
+ g_object_notify((GObject *)self, "hostname");
+}
+
+/**
+ * spice_uri_get_port:
+ * @uri: a #SpiceURI
+ *
+ * Gets @uri's port.
+ *
+ * Returns: @uri's port.
+ * Since: 0.24
+ **/
+guint spice_uri_get_port(SpiceURI *self)
+{
+ g_return_val_if_fail(SPICE_IS_URI(self), 0);
+ return self->port;
+}
+
+/**
+ * spice_uri_set_port:
+ * @uri: a #SpiceURI
+ * @port: the port
+ *
+ * Sets @uri's port to @port.
+ * Since: 0.24
+ **/
+void spice_uri_set_port(SpiceURI *self, guint port)
+{
+ g_return_if_fail(SPICE_IS_URI(self));
+ self->port = port;
+ g_object_notify((GObject *)self, "port");
+}
+
+static void spice_uri_get_property(GObject *object, guint property_id,
+ GValue *value, GParamSpec *pspec)
+{
+ SpiceURI *self;
+ self = G_TYPE_CHECK_INSTANCE_CAST(object, SPICE_TYPE_URI, SpiceURI);
+
+ switch (property_id) {
+ case SPICE_URI_SCHEME:
+ g_value_set_string(value, spice_uri_get_scheme(self));
+ break;
+ case SPICE_URI_HOSTNAME:
+ g_value_set_string(value, spice_uri_get_hostname(self));
+ break;
+ case SPICE_URI_PORT:
+ g_value_set_uint(value, spice_uri_get_port(self));
+ break;
+ case SPICE_URI_USER:
+ g_value_set_string(value, spice_uri_get_user(self));
+ break;
+ case SPICE_URI_PASSWORD:
+ g_value_set_string(value, spice_uri_get_password(self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+
+static void spice_uri_set_property(GObject *object, guint property_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ SpiceURI * self;
+ self = G_TYPE_CHECK_INSTANCE_CAST(object, SPICE_TYPE_URI, SpiceURI);
+
+ switch (property_id) {
+ case SPICE_URI_SCHEME:
+ spice_uri_set_scheme(self, g_value_get_string(value));
+ break;
+ case SPICE_URI_HOSTNAME:
+ spice_uri_set_hostname(self, g_value_get_string(value));
+ break;
+ case SPICE_URI_USER:
+ spice_uri_set_user(self, g_value_get_string(value));
+ break;
+ case SPICE_URI_PASSWORD:
+ spice_uri_set_password(self, g_value_get_string(value));
+ break;
+ case SPICE_URI_PORT:
+ spice_uri_set_port(self, g_value_get_uint(value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void spice_uri_finalize(GObject* obj)
+{
+ SpiceURI *self;
+
+ self = G_TYPE_CHECK_INSTANCE_CAST(obj, SPICE_TYPE_URI, SpiceURI);
+ g_free(self->scheme);
+ g_free(self->hostname);
+ g_free(self->user);
+ g_free(self->password);
+
+ G_OBJECT_CLASS (spice_uri_parent_class)->finalize (obj);
+}
+
+static void spice_uri_init (SpiceURI *self)
+{
+}
+
+
+static void spice_uri_class_init(SpiceURIClass *klass)
+{
+ spice_uri_parent_class = g_type_class_peek_parent (klass);
+
+ G_OBJECT_CLASS (klass)->get_property = spice_uri_get_property;
+ G_OBJECT_CLASS (klass)->set_property = spice_uri_set_property;
+ G_OBJECT_CLASS (klass)->finalize = spice_uri_finalize;
+
+ g_object_class_install_property(G_OBJECT_CLASS (klass),
+ SPICE_URI_SCHEME,
+ g_param_spec_string ("scheme",
+ "scheme",
+ "scheme",
+ NULL,
+ G_PARAM_STATIC_STRINGS |
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(G_OBJECT_CLASS (klass),
+ SPICE_URI_HOSTNAME,
+ g_param_spec_string ("hostname",
+ "hostname",
+ "hostname",
+ NULL,
+ G_PARAM_STATIC_STRINGS |
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(G_OBJECT_CLASS (klass),
+ SPICE_URI_PORT,
+ g_param_spec_uint ("port",
+ "port",
+ "port",
+ 0, G_MAXUINT, 0,
+ G_PARAM_STATIC_STRINGS |
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(G_OBJECT_CLASS (klass),
+ SPICE_URI_USER,
+ g_param_spec_string ("user",
+ "user",
+ "user",
+ NULL,
+ G_PARAM_STATIC_STRINGS |
+ G_PARAM_READWRITE));
+
+ g_object_class_install_property(G_OBJECT_CLASS (klass),
+ SPICE_URI_PASSWORD,
+ g_param_spec_string ("password",
+ "password",
+ "password",
+ NULL,
+ G_PARAM_STATIC_STRINGS |
+ G_PARAM_READWRITE));
+}
+
+/**
+ * spice_uri_to_string:
+ * @uri: a #SpiceURI
+ *
+ * Returns a string representing @uri.
+ *
+ * Returns: a string representing @uri, which the caller must free.
+ * Since: 0.24
+ **/
+gchar* spice_uri_to_string(SpiceURI* self)
+{
+ g_return_val_if_fail(SPICE_IS_URI(self), NULL);
+
+ if (self->scheme == NULL || self->hostname == NULL)
+ return NULL;
+
+ if (self->user || self->password)
+ return g_strdup_printf("%s://%s:%s@%s:%u",
+ self->scheme,
+ self->user, self->password,
+ self->hostname, self->port);
+ else
+ return g_strdup_printf("%s://%s:%u",
+ self->scheme, self->hostname, self->port);
+}
+
+/**
+ * spice_uri_get_user:
+ * @uri: a #SpiceURI
+ *
+ * Gets @uri's user.
+ *
+ * Returns: @uri's user.
+ * Since: 0.24
+ **/
+const gchar* spice_uri_get_user(SpiceURI *self)
+{
+ g_return_val_if_fail(SPICE_IS_URI(self), NULL);
+ return self->user;
+}
+
+/**
+ * spice_uri_set_user:
+ * @uri: a #SpiceURI
+ * @user: the user, or %NULL.
+ *
+ * Sets @uri's user to @user.
+ * Since: 0.24
+ **/
+void spice_uri_set_user(SpiceURI *self, const gchar *user)
+{
+ g_return_if_fail(SPICE_IS_URI(self));
+
+ g_free(self->user);
+ self->user = g_strdup(user);
+ g_object_notify((GObject *)self, "user");
+}
+
+/**
+ * spice_uri_get_password:
+ * @uri: a #SpiceURI
+ *
+ * Gets @uri's password.
+ *
+ * Returns: @uri's password.
+ * Since: 0.24
+ **/
+const gchar* spice_uri_get_password(SpiceURI *self)
+{
+ g_return_val_if_fail(SPICE_IS_URI(self), NULL);
+ return self->password;
+}
+
+/**
+ * spice_uri_set_password:
+ * @uri: a #SpiceURI
+ * @password: the password, or %NULL.
+ *
+ * Sets @uri's password to @password.
+ * Since: 0.24
+ **/
+void spice_uri_set_password(SpiceURI *self, const gchar *password)
+{
+ g_return_if_fail(SPICE_IS_URI(self));
+
+ g_free(self->password);
+ self->password = g_strdup(password);
+ g_object_notify((GObject *)self, "password");
+}
diff --git a/src/spice-uri.h b/src/spice-uri.h
new file mode 100644
index 0000000..9e8d590
--- /dev/null
+++ b/src/spice-uri.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 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_URI_H__
+#define __SPICE_URI_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_URI (spice_uri_get_type ())
+#define SPICE_URI(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SPICE_TYPE_URI, SpiceURI))
+#define SPICE_URI_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SPICE_TYPE_URI, SpiceURIClass))
+#define SPICE_IS_URI(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SPICE_TYPE_URI))
+#define SPICE_IS_URI_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SPICE_TYPE_URI))
+#define SPICE_URI_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SPICE_TYPE_URI, SpiceURIClass))
+
+typedef struct _SpiceURI SpiceURI;
+typedef struct _SpiceURIClass SpiceURIClass;
+typedef struct _SpiceURIPrivate SpiceURIPrivate;
+
+GType spice_uri_get_type(void) G_GNUC_CONST;
+
+const gchar* spice_uri_get_scheme(SpiceURI* uri);
+void spice_uri_set_scheme(SpiceURI* uri, const gchar* scheme);
+const gchar* spice_uri_get_hostname(SpiceURI* uri);
+void spice_uri_set_hostname(SpiceURI* uri, const gchar* hostname);
+guint spice_uri_get_port(SpiceURI* uri);
+void spice_uri_set_port(SpiceURI* uri, guint port);
+gchar *spice_uri_to_string(SpiceURI* uri);
+const gchar* spice_uri_get_user(SpiceURI* uri);
+void spice_uri_set_user(SpiceURI* uri, const gchar* user);
+const gchar* spice_uri_get_password(SpiceURI* uri);
+void spice_uri_set_password(SpiceURI* uri, const gchar* password);
+
+G_END_DECLS
+
+#endif /* __SPICE_URI_H__ */
diff --git a/src/spice-util-priv.h b/src/spice-util-priv.h
new file mode 100644
index 0000000..c0ea8d9
--- /dev/null
+++ b/src/spice-util-priv.h
@@ -0,0 +1,52 @@
+/* -*- 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_UTIL_PRIV_H
+#define SPICE_UTIL_PRIV_H
+
+#include <glib.h>
+#include "spice-util.h"
+
+G_BEGIN_DECLS
+
+#define UUID_FMT "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x"
+
+gboolean spice_strv_contains(const GStrv strv, const gchar *str);
+const gchar* spice_yes_no(gboolean value);
+guint16 spice_make_scancode(guint scancode, gboolean release);
+gchar* spice_unix2dos(const gchar *str, gssize len, GError **error);
+gchar* spice_dos2unix(const gchar *str, gssize len, GError **error);
+void spice_mono_edge_highlight(unsigned width, unsigned hight,
+ const guint8 *and, const guint8 *xor, guint8 *dest);
+
+#if GLIB_CHECK_VERSION(2,32,0)
+#define STATIC_MUTEX GMutex
+#define STATIC_MUTEX_INIT(m) g_mutex_init(&(m))
+#define STATIC_MUTEX_CLEAR(m) g_mutex_clear(&(m))
+#define STATIC_MUTEX_LOCK(m) g_mutex_lock(&(m))
+#define STATIC_MUTEX_UNLOCK(m) g_mutex_unlock(&(m))
+#else
+#define STATIC_MUTEX GStaticMutex
+#define STATIC_MUTEX_INIT(m) g_static_mutex_init(&(m))
+#define STATIC_MUTEX_CLEAR(m) g_static_mutex_free(&(m))
+#define STATIC_MUTEX_LOCK(m) g_static_mutex_lock(&(m))
+#define STATIC_MUTEX_UNLOCK(m) g_static_mutex_unlock(&(m))
+#endif
+
+G_END_DECLS
+
+#endif /* SPICE_UTIL_PRIV_H */
diff --git a/src/spice-util.c b/src/spice-util.c
new file mode 100644
index 0000000..bec237b
--- /dev/null
+++ b/src/spice-util.c
@@ -0,0 +1,497 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2010 Red Hat, Inc.
+ Copyright © 2006-2010 Collabora Ltd. <http://www.collabora.co.uk/>
+
+ 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 <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <glib.h>
+#include <glib-object.h>
+#include "spice-util-priv.h"
+#include "spice-util.h"
+#include "spice-util-priv.h"
+
+/**
+ * SECTION:spice-util
+ * @short_description: version and debugging functions
+ * @title: Utilities
+ * @section_id:
+ * @stability: Stable
+ * @include: spice-util.h
+ *
+ * Various functions for debugging and informational purposes.
+ */
+
+static GOnce debug_once = G_ONCE_INIT;
+
+static void spice_util_enable_debug_messages(void)
+{
+#if GLIB_CHECK_VERSION(2, 31, 0)
+ const gchar *doms = g_getenv("G_MESSAGES_DEBUG");
+ if (!doms) {
+ g_setenv("G_MESSAGES_DEBUG", G_LOG_DOMAIN, 1);
+ } else if (g_str_equal(doms, "all")) {
+ return;
+ } else if (!strstr(doms, G_LOG_DOMAIN)) {
+ gchar *newdoms = g_strdup_printf("%s %s", doms, G_LOG_DOMAIN);
+ g_setenv("G_MESSAGES_DEBUG", newdoms, 1);
+ g_free(newdoms);
+ }
+#endif
+}
+
+/**
+ * spice_util_set_debug:
+ * @enabled: %TRUE or %FALSE
+ *
+ * Enable or disable Spice-GTK debugging messages.
+ **/
+void spice_util_set_debug(gboolean enabled)
+{
+ /* Make sure debug_once has been initialised
+ * with the value of SPICE_DEBUG already, otherwise
+ * spice_util_get_debug() may overwrite the value
+ * that was just set using spice_util_set_debug()
+ */
+ spice_util_get_debug();
+
+ if (enabled) {
+ spice_util_enable_debug_messages();
+ }
+
+ debug_once.retval = GINT_TO_POINTER(enabled);
+}
+
+static gpointer getenv_debug(gpointer data)
+{
+ gboolean debug;
+
+ debug = (g_getenv("SPICE_DEBUG") != NULL);
+ if (debug)
+ spice_util_enable_debug_messages();
+
+ return GINT_TO_POINTER(debug);
+}
+
+gboolean spice_util_get_debug(void)
+{
+ g_once(&debug_once, getenv_debug, NULL);
+
+ return GPOINTER_TO_INT(debug_once.retval);
+}
+
+/**
+ * spice_util_get_version_string:
+ *
+ * Returns: Spice-GTK version as a const string.
+ **/
+const gchar *spice_util_get_version_string(void)
+{
+ return VERSION;
+}
+
+G_GNUC_INTERNAL
+gboolean spice_strv_contains(const GStrv strv, const gchar *str)
+{
+ int i;
+
+ if (strv == NULL)
+ return FALSE;
+
+ for (i = 0; strv[i] != NULL; i++)
+ if (g_str_equal(strv[i], str))
+ return TRUE;
+
+ return FALSE;
+}
+
+/**
+ * spice_uuid_to_string:
+ * @uuid: UUID byte array
+ *
+ * Creates a string representation of @uuid, of the form
+ * "06e023d5-86d8-420e-8103-383e4566087a"
+ *
+ * Returns: A string that should be freed with g_free().
+ * Since: 0.22
+ **/
+gchar* spice_uuid_to_string(const guint8 uuid[16])
+{
+ return g_strdup_printf(UUID_FMT, uuid[0], uuid[1],
+ uuid[2], uuid[3], uuid[4], uuid[5],
+ uuid[6], uuid[7], uuid[8], uuid[9],
+ uuid[10], uuid[11], uuid[12], uuid[13],
+ uuid[14], uuid[15]);
+}
+
+typedef struct {
+ GObject *instance;
+ GObject *observer;
+ GClosure *closure;
+ gulong handler_id;
+} WeakHandlerCtx;
+
+static WeakHandlerCtx *
+whc_new (GObject *instance,
+ GObject *observer)
+{
+ WeakHandlerCtx *ctx = g_slice_new0 (WeakHandlerCtx);
+
+ ctx->instance = instance;
+ ctx->observer = observer;
+
+ return ctx;
+}
+
+static void
+whc_free (WeakHandlerCtx *ctx)
+{
+ g_slice_free (WeakHandlerCtx, ctx);
+}
+
+static void observer_destroyed_cb (gpointer, GObject *);
+static void closure_invalidated_cb (gpointer, GClosure *);
+
+/*
+ * If signal handlers are removed before the object is destroyed, this
+ * callback will never get triggered.
+ */
+static void
+instance_destroyed_cb (gpointer ctx_,
+ GObject *where_the_instance_was)
+{
+ WeakHandlerCtx *ctx = ctx_;
+
+ /* No need to disconnect the signal here, the instance has gone away. */
+ g_object_weak_unref (ctx->observer, observer_destroyed_cb, ctx);
+ g_closure_remove_invalidate_notifier (ctx->closure, ctx,
+ closure_invalidated_cb);
+ whc_free (ctx);
+}
+
+/* Triggered when the observer is destroyed. */
+static void
+observer_destroyed_cb (gpointer ctx_,
+ GObject *where_the_observer_was)
+{
+ WeakHandlerCtx *ctx = ctx_;
+
+ g_closure_remove_invalidate_notifier (ctx->closure, ctx,
+ closure_invalidated_cb);
+ g_signal_handler_disconnect (ctx->instance, ctx->handler_id);
+ g_object_weak_unref (ctx->instance, instance_destroyed_cb, ctx);
+ whc_free (ctx);
+}
+
+/* Triggered when either object is destroyed or the handler is disconnected. */
+static void
+closure_invalidated_cb (gpointer ctx_,
+ GClosure *where_the_closure_was)
+{
+ WeakHandlerCtx *ctx = ctx_;
+
+ g_object_weak_unref (ctx->instance, instance_destroyed_cb, ctx);
+ g_object_weak_unref (ctx->observer, observer_destroyed_cb, ctx);
+ whc_free (ctx);
+}
+
+/* Copied from tp_g_signal_connect_object. See documentation. */
+/**
+ * spice_g_signal_connect_object: (skip)
+ * @instance: the instance to connect to.
+ * @detailed_signal: a string of the form "signal-name::detail".
+ * @c_handler: the #GCallback to connect.
+ * @gobject: the object to pass as data to @c_handler.
+ * @connect_flags: a combination of #GConnectFlags.
+ *
+ * Similar to g_signal_connect_object() but will delete connection
+ * when any of the objects is destroyed.
+ *
+ * Returns: the handler id.
+ */
+gulong spice_g_signal_connect_object (gpointer instance,
+ const gchar *detailed_signal,
+ GCallback c_handler,
+ gpointer gobject,
+ GConnectFlags connect_flags)
+{
+ GObject *instance_obj = G_OBJECT (instance);
+ WeakHandlerCtx *ctx = whc_new (instance_obj, gobject);
+
+ g_return_val_if_fail (G_TYPE_CHECK_INSTANCE (instance), 0);
+ g_return_val_if_fail (detailed_signal != NULL, 0);
+ g_return_val_if_fail (c_handler != NULL, 0);
+ g_return_val_if_fail (G_IS_OBJECT (gobject), 0);
+ g_return_val_if_fail (
+ (connect_flags & ~(G_CONNECT_AFTER|G_CONNECT_SWAPPED)) == 0, 0);
+
+ if (connect_flags & G_CONNECT_SWAPPED)
+ ctx->closure = g_cclosure_new_object_swap (c_handler, gobject);
+ else
+ ctx->closure = g_cclosure_new_object (c_handler, gobject);
+
+ ctx->handler_id = g_signal_connect_closure (instance, detailed_signal,
+ ctx->closure, (connect_flags & G_CONNECT_AFTER) ? TRUE : FALSE);
+
+ g_object_weak_ref (instance_obj, instance_destroyed_cb, ctx);
+ g_object_weak_ref (gobject, observer_destroyed_cb, ctx);
+ g_closure_add_invalidate_notifier (ctx->closure, ctx,
+ closure_invalidated_cb);
+
+ return ctx->handler_id;
+}
+
+G_GNUC_INTERNAL
+const gchar* spice_yes_no(gboolean value)
+{
+ return value ? "yes" : "no";
+}
+
+G_GNUC_INTERNAL
+guint16 spice_make_scancode(guint scancode, gboolean release)
+{
+ SPICE_DEBUG("%s: %s scancode %d",
+ __FUNCTION__, release ? "release" : "", scancode);
+
+ if (release) {
+ if (scancode < 0x100)
+ return scancode | 0x80;
+ else
+ return 0x80e0 | ((scancode - 0x100) << 8);
+ } else {
+ if (scancode < 0x100)
+ return scancode;
+ else
+ return 0xe0 | ((scancode - 0x100) << 8);
+ }
+
+ g_return_val_if_reached(0);
+}
+
+typedef enum {
+ NEWLINE_TYPE_LF,
+ NEWLINE_TYPE_CR_LF
+} NewlineType;
+
+static gssize get_line(const gchar *str, gsize len,
+ NewlineType type, gsize *nl_len,
+ GError **error)
+{
+ const gchar *p, *endl;
+ gsize nl = 0;
+
+ endl = (type == NEWLINE_TYPE_CR_LF) ? "\r\n" : "\n";
+ p = g_strstr_len(str, len, endl);
+ if (p) {
+ len = p - str;
+ nl = strlen(endl);
+ }
+
+ *nl_len = nl;
+ return len;
+}
+
+
+static gchar* spice_convert_newlines(const gchar *str, gssize len,
+ NewlineType from,
+ NewlineType to,
+ GError **error)
+{
+ GError *err = NULL;
+ gssize length;
+ gsize nl;
+ GString *output;
+ gboolean free_segment = FALSE;
+ gint i;
+
+ g_return_val_if_fail(str != NULL, NULL);
+ g_return_val_if_fail(len >= -1, NULL);
+ g_return_val_if_fail(error == NULL || *error == NULL, NULL);
+ /* only 2 supported combinations */
+ g_return_val_if_fail((from == NEWLINE_TYPE_LF &&
+ to == NEWLINE_TYPE_CR_LF) ||
+ (from == NEWLINE_TYPE_CR_LF &&
+ to == NEWLINE_TYPE_LF), NULL);
+
+ if (len == -1)
+ len = strlen(str);
+ /* sometime we get \0 terminated strings, skip that, or it fails
+ to utf8 validate line with \0 end */
+ else if (len > 0 && str[len-1] == 0)
+ len -= 1;
+
+ /* allocate worst case, if it's small enough, we don't care much,
+ * if it's big, malloc will put us in mmap'd region, and we can
+ * over allocate.
+ */
+ output = g_string_sized_new(len * 2 + 1);
+
+ for (i = 0; i < len; i += length + nl) {
+ length = get_line(str + i, len - i, from, &nl, &err);
+ if (length < 0)
+ break;
+
+ g_string_append_len(output, str + i, length);
+
+ if (nl) {
+ /* let's not double \r if it's already in the line */
+ if (to == NEWLINE_TYPE_CR_LF &&
+ output->str[output->len - 1] != '\r')
+ g_string_append_c(output, '\r');
+
+ g_string_append_c(output, '\n');
+ }
+ }
+
+ if (err) {
+ g_propagate_error(error, err);
+ free_segment = TRUE;
+ }
+
+ return g_string_free(output, free_segment);
+}
+
+G_GNUC_INTERNAL
+gchar* spice_dos2unix(const gchar *str, gssize len, GError **error)
+{
+ return spice_convert_newlines(str, len,
+ NEWLINE_TYPE_CR_LF,
+ NEWLINE_TYPE_LF,
+ error);
+}
+
+G_GNUC_INTERNAL
+gchar* spice_unix2dos(const gchar *str, gssize len, GError **error)
+{
+ return spice_convert_newlines(str, len,
+ NEWLINE_TYPE_LF,
+ NEWLINE_TYPE_CR_LF,
+ error);
+}
+
+static bool buf_is_ones(unsigned size, const guint8 *data)
+{
+ int i;
+
+ for (i = 0 ; i < size; ++i) {
+ if (data[i] != 0xff) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool is_edge_helper(const guint8 *xor, int bpl, int x, int y)
+{
+ return (xor[bpl * y + (x / 8)] & (0x80 >> (x % 8))) > 0;
+}
+
+static bool is_edge(unsigned width, unsigned height, const guint8 *xor, int bpl, int x, int y)
+{
+ if (x == 0 || x == width -1 || y == 0 || y == height - 1) {
+ return 0;
+ }
+#define P(x, y) is_edge_helper(xor, bpl, x, y)
+ return !P(x, y) && (P(x - 1, y + 1) || P(x, y + 1) || P(x + 1, y + 1) ||
+ P(x - 1, y) || P(x + 1, y) ||
+ P(x - 1, y - 1) || P(x, y - 1) || P(x + 1, y - 1));
+#undef P
+}
+
+/* Mono cursors have two places, "and" and "xor". If a bit is 1 in both, it
+ * means invertion of the corresponding pixel in the display. Since X11 (and
+ * gdk) doesn't do invertion, instead we do edge detection and turn the
+ * sorrounding edge pixels black, and the invert-me pixels white. To
+ * illustrate:
+ *
+ * and xor dest RGB (1=0xffffff, 0=0x000000)
+ *
+ * dest alpha (1=0xff, 0=0x00)
+ *
+ * 11111 00000 00000 00000
+ * 11111 00000 00000 01110
+ * 11111 00100 => 00100 01110
+ * 11111 00100 00100 01110
+ * 11111 00000 00000 01110
+ * 11111 00000 00000 00000
+ *
+ * See tests/util.c for more tests
+ *
+ * Notes:
+ * Assumes width >= 8 (i.e. bytes per line is at least 1)
+ * Assumes edges are not on the boundary (first/last line/column) for simplicity
+ *
+ */
+G_GNUC_INTERNAL
+void spice_mono_edge_highlight(unsigned width, unsigned height,
+ const guint8 *and, const guint8 *xor, guint8 *dest)
+{
+ int bpl = (width + 7) / 8;
+ bool and_ones = buf_is_ones(height * bpl, and);
+ int x, y, bit;
+ const guint8 *xor_base = xor;
+
+ for (y = 0; y < height; y++) {
+ bit = 0x80;
+ for (x = 0; x < width; x++, dest += 4) {
+ if (is_edge(width, height, xor_base, bpl, x, y) && and_ones) {
+ dest[0] = 0x00;
+ dest[1] = 0x00;
+ dest[2] = 0x00;
+ dest[3] = 0xff;
+ goto next_bit;
+ }
+ if (and[x/8] & bit) {
+ if (xor[x/8] & bit) {
+ dest[0] = 0xff;
+ dest[1] = 0xff;
+ dest[2] = 0xff;
+ dest[3] = 0xff;
+ } else {
+ /* unchanged -> transparent */
+ dest[0] = 0x00;
+ dest[1] = 0x00;
+ dest[2] = 0x00;
+ dest[3] = 0x00;
+ }
+ } else {
+ if (xor[x/8] & bit) {
+ /* set -> white */
+ dest[0] = 0xff;
+ dest[1] = 0xff;
+ dest[2] = 0xff;
+ dest[3] = 0xff;
+ } else {
+ /* clear -> black */
+ dest[0] = 0x00;
+ dest[1] = 0x00;
+ dest[2] = 0x00;
+ dest[3] = 0xff;
+ }
+ }
+ next_bit:
+ bit >>= 1;
+ if (bit == 0) {
+ bit = 0x80;
+ }
+ }
+ and += bpl;
+ xor += bpl;
+ }
+}
diff --git a/src/spice-util.h b/src/spice-util.h
new file mode 100644
index 0000000..3f429a0
--- /dev/null
+++ b/src/spice-util.h
@@ -0,0 +1,63 @@
+/* -*- 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_UTIL_H
+#define SPICE_UTIL_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+void spice_util_set_debug(gboolean enabled);
+gboolean spice_util_get_debug(void);
+const gchar *spice_util_get_version_string(void);
+gulong spice_g_signal_connect_object(gpointer instance,
+ const gchar *detailed_signal,
+ GCallback c_handler,
+ gpointer gobject,
+ GConnectFlags connect_flags);
+gchar* spice_uuid_to_string(const guint8 uuid[16]);
+
+#define SPICE_DEBUG(fmt, ...) \
+ do { \
+ if (G_UNLIKELY(spice_util_get_debug())) \
+ g_debug(G_STRLOC " " fmt, ## __VA_ARGS__); \
+ } while (0)
+
+#define SPICE_RESERVED_PADDING (10 * sizeof(void*))
+
+/* need to be in a public header, glib-compat.h is private */
+#ifndef SPICE_GNUC_DEPRECATED_FOR
+#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 5)
+#define SPICE_GNUC_DEPRECATED_FOR(f) \
+ __attribute__((deprecated("Use " #f " instead")))
+#else
+#define SPICE_GNUC_DEPRECATED_FOR(f) G_GNUC_DEPRECATED
+#endif /* __GNUC__ */
+#endif
+
+#ifndef SPICE_NO_DEPRECATED
+#define SPICE_DEPRECATED_FOR(f) SPICE_GNUC_DEPRECATED_FOR(f)
+#define SPICE_DEPRECATED G_GNUC_DEPRECATED
+#else
+#define SPICE_DEPRECATED_FOR(f)
+#define SPICE_DEPRECATED
+#endif
+
+G_END_DECLS
+
+#endif /* SPICE_UTIL_H */
diff --git a/src/spice-version.h.in b/src/spice-version.h.in
new file mode 100644
index 0000000..4276a23
--- /dev/null
+++ b/src/spice-version.h.in
@@ -0,0 +1,70 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2014 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_VERSION_H__
+#define __SPICE_VERSION_H__
+
+/**
+ * SECTION:spice-version
+ * @short_description: Spice-Gtk version checking
+ *
+ * Spice-Gtk provides macros to check the version of the library
+ * at compile-time
+ */
+
+/**
+ * SPICE_GTK_MAJOR_VERSION:
+ *
+ * Spice-Gtk major version component (e.g. 1 if version is 1.2.3)
+ * Since: 0.24
+ */
+#define SPICE_GTK_MAJOR_VERSION (@SPICE_GTK_MAJOR_VERSION@)
+
+/**
+ * SPICE_GTK_MINOR_VERSION:
+ *
+ * Spice-Gtk minor version component (e.g. 2 if version is 1.2.3)
+ * Since: 0.24
+ */
+#define SPICE_GTK_MINOR_VERSION (@SPICE_GTK_MINOR_VERSION@)
+
+/**
+ * SPICE_GTK_MICRO_VERSION:
+ *
+ * Spice-Gtk micro version component (e.g. 3 if version is 1.2.3)
+ * Since: 0.24
+ */
+#define SPICE_GTK_MICRO_VERSION (@SPICE_GTK_MICRO_VERSION@)
+
+/**
+ * SPICE_GTK_CHECK_VERSION:
+ * @major: required major version
+ * @minor: required minor version
+ * @micro: required micro version
+ *
+ * Compile-time version checking. Evaluates to %TRUE if the version
+ * of Spice-Gtk is greater than the required one.
+ * Since: 0.24
+ */
+#define SPICE_GTK_CHECK_VERSION(major, minor, micro) \
+ (SPICE_GTK_MAJOR_VERSION > (major) || \
+ (SPICE_GTK_MAJOR_VERSION == (major) && SPICE_GTK_MINOR_VERSION > (minor)) || \
+ (SPICE_GTK_MAJOR_VERSION == (major) && SPICE_GTK_MINOR_VERSION == (minor) && \
+ SPICE_GTK_MICRO_VERSION >= (micro)))
+
+
+#endif /* __SPICE_VERSION_H__ */
diff --git a/src/spice-widget-cairo.c b/src/spice-widget-cairo.c
new file mode 100644
index 0000000..96af076
--- /dev/null
+++ b/src/spice-widget-cairo.c
@@ -0,0 +1,160 @@
+/* -*- 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 "gtk-compat.h"
+#include "spice-widget.h"
+#include "spice-widget-priv.h"
+#include "spice-gtk-session-priv.h"
+
+
+G_GNUC_INTERNAL
+int spicex_image_create(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+
+ if (d->ximage != NULL)
+ return 0;
+
+ if (d->format == SPICE_SURFACE_FMT_16_555 ||
+ d->format == SPICE_SURFACE_FMT_16_565) {
+ d->convert = TRUE;
+ d->data = g_malloc0(d->area.width * d->area.height * 4);
+
+ d->ximage = cairo_image_surface_create_for_data
+ (d->data, CAIRO_FORMAT_RGB24, d->area.width, d->area.height, d->area.width * 4);
+
+ } else {
+ d->convert = FALSE;
+
+ d->ximage = cairo_image_surface_create_for_data
+ (d->data, CAIRO_FORMAT_RGB24, d->width, d->height, d->stride);
+ }
+
+ return 0;
+}
+
+G_GNUC_INTERNAL
+void spicex_image_destroy(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+
+ if (d->ximage) {
+ cairo_surface_destroy(d->ximage);
+ d->ximage = NULL;
+ }
+ if (d->convert && d->data) {
+ g_free(d->data);
+ d->data = NULL;
+ }
+ d->convert = FALSE;
+}
+
+G_GNUC_INTERNAL
+void spicex_draw_event(SpiceDisplay *display, cairo_t *cr)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ cairo_rectangle_int_t rect;
+ cairo_region_t *region;
+ double s;
+ int x, y;
+ int ww, wh;
+ int w, h;
+
+ spice_display_get_scaling(display, &s, &x, &y, &w, &h);
+
+ gdk_drawable_get_size(gtk_widget_get_window(GTK_WIDGET(display)), &ww, &wh);
+
+ /* We need to paint the bg color around the image */
+ rect.x = 0;
+ rect.y = 0;
+ rect.width = ww;
+ rect.height = wh;
+ region = cairo_region_create_rectangle(&rect);
+
+ /* Optionally cut out the inner area where the pixmap
+ will be drawn. This avoids 'flashing' since we're
+ not double-buffering. */
+ if (d->ximage) {
+ rect.x = x;
+ rect.y = y;
+ rect.width = w;
+ rect.height = h;
+ cairo_region_subtract_rectangle(region, &rect);
+ }
+
+ gdk_cairo_region (cr, region);
+ cairo_region_destroy (region);
+
+ /* Need to set a real solid color, because the default is usually
+ transparent these days, and non-double buffered windows can't
+ render transparently */
+ cairo_set_source_rgb (cr, 0, 0, 0);
+ cairo_fill(cr);
+
+ /* Draw the display */
+ if (d->ximage) {
+ cairo_translate(cr, x, y);
+ cairo_rectangle(cr, 0, 0, w, h);
+ cairo_scale(cr, s, s);
+ if (!d->convert)
+ cairo_translate(cr, -d->area.x, -d->area.y);
+ cairo_set_source_surface(cr, d->ximage, 0, 0);
+ cairo_fill(cr);
+
+ if (d->mouse_mode == SPICE_MOUSE_MODE_SERVER &&
+ d->mouse_guest_x != -1 && d->mouse_guest_y != -1 &&
+ !d->show_cursor &&
+ spice_gtk_session_get_pointer_grabbed(d->gtk_session)) {
+ GdkPixbuf *image = d->mouse_pixbuf;
+ if (image != NULL) {
+ gdk_cairo_set_source_pixbuf(cr, image,
+ d->mouse_guest_x - d->mouse_hotspot.x,
+ d->mouse_guest_y - d->mouse_hotspot.y);
+ cairo_paint(cr);
+ }
+ }
+ }
+}
+
+#if ! GTK_CHECK_VERSION (2, 91, 0)
+G_GNUC_INTERNAL
+void spicex_expose_event(SpiceDisplay *display, GdkEventExpose *expose)
+{
+ cairo_t *cr;
+
+ cr = gdk_cairo_create(gtk_widget_get_window(GTK_WIDGET(display)));
+ cairo_rectangle(cr,
+ expose->area.x,
+ expose->area.y,
+ expose->area.width,
+ expose->area.height);
+ cairo_clip(cr);
+
+ spicex_draw_event(display, cr);
+
+ cairo_destroy(cr);
+}
+#endif
+
+G_GNUC_INTERNAL
+gboolean spicex_is_scaled(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ return d->allow_scaling;
+}
diff --git a/src/spice-widget-priv.h b/src/spice-widget-priv.h
new file mode 100644
index 0000000..0e1f661
--- /dev/null
+++ b/src/spice-widget-priv.h
@@ -0,0 +1,141 @@
+/* -*- 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_WIDGET_PRIV_H__
+#define __SPICE_WIDGET_PRIV_H__
+
+G_BEGIN_DECLS
+
+#include "config.h"
+
+#ifdef WITH_X11
+#include <X11/Xlib.h>
+#include <X11/extensions/XShm.h>
+#include <gdk/gdkx.h>
+#endif
+
+#ifdef WIN32
+#include <windows.h>
+#endif
+
+#include "spice-widget.h"
+#include "spice-common.h"
+#include "spice-gtk-session.h"
+
+#define SPICE_DISPLAY_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_DISPLAY, SpiceDisplayPrivate))
+
+struct _SpiceDisplayPrivate {
+ gint channel_id;
+ gint monitor_id;
+
+ /* options */
+ bool keyboard_grab_enable;
+ gboolean keyboard_grab_inhibit;
+ bool mouse_grab_enable;
+ bool resize_guest_enable;
+
+ /* state */
+ gboolean ready;
+ gboolean monitor_ready;
+ enum SpiceSurfaceFmt format;
+ gint width, height, stride;
+ gint shmid;
+ gpointer data_origin; /* the original display image data */
+ gpointer data; /* converted if necessary to 32 bits */
+
+ GdkRectangle area;
+ /* window border */
+ gint ww, wh, mx, my;
+
+ bool convert;
+ bool have_mitshm;
+ gboolean allow_scaling;
+ gboolean only_downscale;
+ gboolean disable_inputs;
+
+ /* TODO: make a display object instead? */
+#ifdef WITH_X11
+ Display *dpy;
+ XVisualInfo *vi;
+ XImage *ximage;
+ XShmSegmentInfo *shminfo;
+ GC gc;
+#else
+ cairo_surface_t *ximage;
+#endif
+
+ SpiceSession *session;
+ SpiceGtkSession *gtk_session;
+ SpiceMainChannel *main;
+ SpiceChannel *display;
+ SpiceCursorChannel *cursor;
+ SpiceInputsChannel *inputs;
+ SpiceSmartcardChannel *smartcard;
+
+ enum SpiceMouseMode mouse_mode;
+ int mouse_grab_active;
+ bool mouse_have_pointer;
+ GdkCursor *mouse_cursor;
+ GdkPixbuf *mouse_pixbuf;
+ GdkPoint mouse_hotspot;
+ GdkCursor *show_cursor;
+ int mouse_last_x;
+ int mouse_last_y;
+ int mouse_guest_x;
+ int mouse_guest_y;
+
+ bool keyboard_grab_active;
+ bool keyboard_have_focus;
+
+ const guint16 *keycode_map;
+ size_t keycode_maplen;
+ uint32_t key_state[512 / 32];
+ int key_delayed_scancode;
+ guint key_delayed_id;
+ SpiceGrabSequence *grabseq; /* the configured key sequence */
+ gboolean *activeseq; /* the currently pressed keys */
+ gboolean seq_pressed;
+ gboolean keyboard_grab_released;
+ gint mark;
+#ifdef WIN32
+ HHOOK keyboard_hook;
+ int win_mouse[3];
+ int win_mouse_speed;
+#endif
+ guint keypress_delay;
+ gint zoom_level;
+#ifdef GDK_WINDOWING_X11
+ int x11_accel_numerator;
+ int x11_accel_denominator;
+ int x11_threshold;
+#endif
+};
+
+int spicex_image_create (SpiceDisplay *display);
+void spicex_image_destroy (SpiceDisplay *display);
+#if GTK_CHECK_VERSION (2, 91, 0)
+void spicex_draw_event (SpiceDisplay *display, cairo_t *cr);
+#else
+void spicex_expose_event (SpiceDisplay *display, GdkEventExpose *ev);
+#endif
+gboolean spicex_is_scaled (SpiceDisplay *display);
+void spice_display_get_scaling (SpiceDisplay *display, double *s, int *x, int *y, int *w, int *h);
+
+G_END_DECLS
+
+#endif
diff --git a/src/spice-widget-x11.c b/src/spice-widget-x11.c
new file mode 100644
index 0000000..3f2ce94
--- /dev/null
+++ b/src/spice-widget-x11.c
@@ -0,0 +1,280 @@
+/* -*- 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-widget.h"
+#include "spice-widget-priv.h"
+
+#ifdef HAVE_SYS_SHM_H
+#include <sys/shm.h>
+#endif
+
+#ifdef HAVE_SYS_IPC_H
+#include <sys/ipc.h>
+#endif
+
+static bool no_mitshm;
+
+static struct format_table {
+ enum SpiceSurfaceFmt spice;
+ XVisualInfo xvisual;
+} format_table[] = {
+ {
+ .spice = SPICE_SURFACE_FMT_32_ARGB, /* FIXME: is that correct xvisual? */
+ .xvisual = {
+ .depth = 24,
+ .red_mask = 0xff0000,
+ .green_mask = 0x00ff00,
+ .blue_mask = 0x0000ff,
+ },
+ },{
+ .spice = SPICE_SURFACE_FMT_32_xRGB,
+ .xvisual = {
+ .depth = 24,
+ .red_mask = 0xff0000,
+ .green_mask = 0x00ff00,
+ .blue_mask = 0x0000ff,
+ },
+ },{
+ .spice = SPICE_SURFACE_FMT_16_555,
+ .xvisual = {
+ .depth = 16,
+ .red_mask = 0x7c00,
+ .green_mask = 0x03e0,
+ .blue_mask = 0x001f,
+ },
+ },{
+ .spice = SPICE_SURFACE_FMT_16_565,
+ .xvisual = {
+ .depth = 16,
+ .red_mask = 0xf800,
+ .green_mask = 0x07e0,
+ .blue_mask = 0x001f,
+ },
+ }
+};
+
+static XVisualInfo *get_visual_for_format(GtkWidget *widget, enum SpiceSurfaceFmt format)
+{
+ GdkDrawable *drawable = gtk_widget_get_window(widget);
+ GdkDisplay *display = gdk_drawable_get_display(drawable);
+ GdkScreen *screen = gdk_drawable_get_screen(drawable);
+ XVisualInfo template;
+ int found, i;
+ XVisualInfo *vi;
+
+ for (i = 0; i < SPICE_N_ELEMENTS(format_table); i++) {
+ if (format == format_table[i].spice)
+ break;
+ }
+ if (i == SPICE_N_ELEMENTS(format_table)) {
+ g_warn_if_reached();
+ return NULL;
+ }
+
+ template = format_table[i].xvisual;
+ template.screen = gdk_x11_screen_get_screen_number(screen);
+ vi = XGetVisualInfo(gdk_x11_display_get_xdisplay(display),
+ VisualScreenMask | VisualDepthMask |
+ VisualRedMaskMask | VisualGreenMaskMask | VisualBlueMaskMask,
+ &template, &found);
+ return vi;
+}
+
+static XVisualInfo *get_visual_default(GtkWidget *widget)
+{
+ GdkDrawable *drawable = gtk_widget_get_window(widget);
+ GdkDisplay *display = gdk_drawable_get_display(drawable);
+ GdkScreen *screen = gdk_drawable_get_screen(drawable);
+ XVisualInfo template;
+ int found;
+
+ template.screen = gdk_x11_screen_get_screen_number(screen);
+ return XGetVisualInfo(gdk_x11_display_get_xdisplay(display),
+ VisualScreenMask,
+ &template, &found);
+}
+
+static int catch_no_mitshm(Display * dpy, XErrorEvent * event)
+{
+ no_mitshm = true;
+ return 0;
+}
+
+G_GNUC_INTERNAL
+int spicex_image_create(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+
+ if (d->ximage != NULL)
+ return 0;
+
+ GdkDrawable *window = gtk_widget_get_window(GTK_WIDGET(display));
+ GdkDisplay *gtkdpy = gdk_drawable_get_display(window);
+ void *old_handler = NULL;
+ XGCValues gcval = {
+ .foreground = 0,
+ .background = 0,
+ };
+
+ d->dpy = gdk_x11_display_get_xdisplay(gtkdpy);
+ d->convert = false;
+ d->vi = get_visual_for_format(GTK_WIDGET(display), d->format);
+ if (d->vi == NULL) {
+ d->convert = true;
+ d->vi = get_visual_default(GTK_WIDGET(display));
+ d->vi = get_visual_for_format(GTK_WIDGET(display), SPICE_SURFACE_FMT_32_xRGB);
+ g_return_val_if_fail(d->vi != NULL, 1);
+ }
+ if (d->convert) {
+ d->data = g_malloc0(d->height * d->stride); /* pixels are 32 bits */
+ }
+
+ d->gc = XCreateGC(d->dpy, gdk_x11_drawable_get_xid(window),
+ GCForeground | GCBackground, &gcval);
+
+ if (d->convert) /* do not use shm when doing color format conversion */
+ goto xcreate;
+
+ if (d->have_mitshm && d->shmid != -1) {
+ if (!XShmQueryExtension(d->dpy)) {
+ goto shm_fail;
+ }
+ no_mitshm = false;
+ old_handler = XSetErrorHandler(catch_no_mitshm);
+ d->shminfo = g_new0(XShmSegmentInfo, 1);
+ d->ximage = XShmCreateImage(d->dpy, d->vi->visual, d->vi->depth,
+ ZPixmap, d->data, d->shminfo, d->width, d->height);
+ if (d->ximage == NULL)
+ goto shm_fail;
+ d->shminfo->shmaddr = d->data;
+ d->shminfo->shmid = d->shmid;
+ d->shminfo->readOnly = false;
+ XShmAttach(d->dpy, d->shminfo);
+ XSync(d->dpy, False);
+ shmctl(d->shmid, IPC_RMID, 0);
+ if (no_mitshm)
+ goto shm_fail;
+ XSetErrorHandler(old_handler);
+ return 0;
+ }
+
+ shm_fail:
+ d->have_mitshm = false;
+ g_free(d->shminfo);
+ d->shminfo = NULL;
+ if (old_handler)
+ XSetErrorHandler(old_handler);
+ xcreate:
+ d->ximage = XCreateImage(d->dpy, d->vi->visual, d->vi->depth, ZPixmap, 0,
+ d->data, d->width, d->height, 32, d->stride);
+ return 0;
+}
+
+G_GNUC_INTERNAL
+void spicex_image_destroy(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+
+ if (d->ximage) {
+ /* avoid XDestroy to free shared memory, owned and freed by
+ channel-display itself */
+ if (d->ximage->data == d->data_origin)
+ d->ximage->data = NULL;
+ XDestroyImage(d->ximage);
+ d->ximage = NULL;
+ if (d->convert)
+ d->data = 0;
+ }
+ if (d->shminfo) {
+ XShmDetach(d->dpy, d->shminfo);
+ free(d->shminfo);
+ d->shminfo = NULL;
+ }
+ if (d->gc) {
+ XFreeGC(d->dpy, d->gc);
+ d->gc = NULL;
+ }
+ if (d->convert && d->data) {
+ g_free(d->data);
+ d->data = NULL;
+ }
+}
+
+G_GNUC_INTERNAL
+void spicex_expose_event(SpiceDisplay *display, GdkEventExpose *expose)
+{
+ GdkDrawable *window = gtk_widget_get_window(GTK_WIDGET(display));
+ SpiceDisplayPrivate *d = display->priv;
+ int x, y, w, h;
+
+ spice_display_get_scaling(display, NULL, &x, &y, &w, &h);
+
+ if (expose->area.x >= x &&
+ expose->area.y >= y &&
+ expose->area.x + expose->area.width <= x + w &&
+ expose->area.y + expose->area.height <= y + h) {
+ /* area is completely inside the guest screen -- blit it */
+ if (d->have_mitshm && d->shminfo) {
+ XShmPutImage(d->dpy, gdk_x11_drawable_get_xid(window),
+ d->gc, d->ximage,
+ d->area.x + expose->area.x - x, d->area.y + expose->area.y - y,
+ expose->area.x, expose->area.y,
+ expose->area.width, expose->area.height,
+ true);
+ } else {
+ XPutImage(d->dpy, gdk_x11_drawable_get_xid(window),
+ d->gc, d->ximage,
+ d->area.x + expose->area.x - x, d->area.y + expose->area.y - y,
+ expose->area.x, expose->area.y,
+ expose->area.width, expose->area.height);
+ }
+ } else {
+ /* complete window update */
+ if (d->ww > d->area.width || d->wh > d->area.height) {
+ int x1 = x;
+ int x2 = x + w;
+ int y1 = y;
+ int y2 = y + h;
+ XFillRectangle(d->dpy, gdk_x11_drawable_get_xid(window),
+ d->gc, 0, 0, x1, d->wh);
+ XFillRectangle(d->dpy, gdk_x11_drawable_get_xid(window),
+ d->gc, x2, 0, d->ww - x2, d->wh);
+ XFillRectangle(d->dpy, gdk_x11_drawable_get_xid(window),
+ d->gc, 0, 0, d->ww, y1);
+ XFillRectangle(d->dpy, gdk_x11_drawable_get_xid(window),
+ d->gc, 0, y2, d->ww, d->wh - y2);
+ }
+ if (d->have_mitshm && d->shminfo) {
+ XShmPutImage(d->dpy, gdk_x11_drawable_get_xid(window),
+ d->gc, d->ximage,
+ d->area.x, d->area.y, x, y, w, h,
+ true);
+ } else {
+ XPutImage(d->dpy, gdk_x11_drawable_get_xid(window),
+ d->gc, d->ximage,
+ d->area.x, d->area.y, x, y, w, h);
+ }
+ }
+}
+
+G_GNUC_INTERNAL
+gboolean spicex_is_scaled(SpiceDisplay *display)
+{
+ return FALSE; /* backend doesn't support scaling yet */
+}
diff --git a/src/spice-widget.c b/src/spice-widget.c
new file mode 100644
index 0000000..b9c4972
--- /dev/null
+++ b/src/spice-widget.c
@@ -0,0 +1,2642 @@
+/* -*- 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 <math.h>
+#include <glib.h>
+
+#if HAVE_X11_XKBLIB_H
+#include <X11/XKBlib.h>
+#include <gdk/gdkx.h>
+#endif
+#ifdef GDK_WINDOWING_X11
+#include <X11/Xlib.h>
+#include <gdk/gdkx.h>
+#endif
+#ifdef G_OS_WIN32
+#include <windows.h>
+#include <gdk/gdkwin32.h>
+#ifndef MAPVK_VK_TO_VSC /* may be undefined in older mingw-headers */
+#define MAPVK_VK_TO_VSC 0
+#endif
+#endif
+
+#include "spice-widget.h"
+#include "spice-widget-priv.h"
+#include "spice-gtk-session-priv.h"
+#include "vncdisplaykeymap.h"
+
+#include "glib-compat.h"
+#include "gtk-compat.h"
+
+/* Some compatibility defines to let us build on both Gtk2 and Gtk3 */
+
+/**
+ * SECTION:spice-widget
+ * @short_description: a GTK display widget
+ * @title: Spice Display
+ * @section_id:
+ * @stability: Stable
+ * @include: spice-widget.h
+ *
+ * A GTK widget that displays a SPICE server. It sends keyboard/mouse
+ * events and can also share clipboard...
+ *
+ * Arbitrary key events can be sent thanks to spice_display_send_keys().
+ *
+ * The widget will optionally grab the keyboard and the mouse when
+ * focused if the properties #SpiceDisplay:grab-keyboard and
+ * #SpiceDisplay:grab-mouse are #TRUE respectively. It can be
+ * ungrabbed with spice_display_mouse_ungrab(), and by setting a key
+ * combination with spice_display_set_grab_keys().
+ *
+ * Finally, spice_display_get_pixbuf() will take a screenshot of the
+ * current display and return an #GdkPixbuf (that you can then easily
+ * save to disk).
+ */
+
+G_DEFINE_TYPE(SpiceDisplay, spice_display, GTK_TYPE_DRAWING_AREA)
+
+/* Properties */
+enum {
+ PROP_0,
+ PROP_SESSION,
+ PROP_CHANNEL_ID,
+ PROP_KEYBOARD_GRAB,
+ PROP_MOUSE_GRAB,
+ PROP_RESIZE_GUEST,
+ PROP_AUTO_CLIPBOARD,
+ PROP_SCALING,
+ PROP_ONLY_DOWNSCALE,
+ PROP_DISABLE_INPUTS,
+ PROP_ZOOM_LEVEL,
+ PROP_MONITOR_ID,
+ PROP_KEYPRESS_DELAY,
+ PROP_READY
+};
+
+/* Signals */
+enum {
+ SPICE_DISPLAY_MOUSE_GRAB,
+ SPICE_DISPLAY_KEYBOARD_GRAB,
+ SPICE_DISPLAY_GRAB_KEY_PRESSED,
+ SPICE_DISPLAY_LAST_SIGNAL,
+};
+
+static guint signals[SPICE_DISPLAY_LAST_SIGNAL];
+
+#ifdef G_OS_WIN32
+static HWND win32_window = NULL;
+#endif
+
+static void update_keyboard_grab(SpiceDisplay *display);
+static void try_keyboard_grab(SpiceDisplay *display);
+static void try_keyboard_ungrab(SpiceDisplay *display);
+static void update_mouse_grab(SpiceDisplay *display);
+static void try_mouse_grab(SpiceDisplay *display);
+static void try_mouse_ungrab(SpiceDisplay *display);
+static void recalc_geometry(GtkWidget *widget);
+static void channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data);
+static void channel_destroy(SpiceSession *s, SpiceChannel *channel, gpointer data);
+static void cursor_invalidate(SpiceDisplay *display);
+static void update_area(SpiceDisplay *display, gint x, gint y, gint width, gint height);
+static void release_keys(SpiceDisplay *display);
+
+/* ---------------------------------------------------------------- */
+
+static void spice_display_get_property(GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceDisplay *display = SPICE_DISPLAY(object);
+ SpiceDisplayPrivate *d = display->priv;
+ gboolean boolean;
+
+ switch (prop_id) {
+ case PROP_SESSION:
+ g_value_set_object(value, d->session);
+ break;
+ case PROP_CHANNEL_ID:
+ g_value_set_int(value, d->channel_id);
+ break;
+ case PROP_MONITOR_ID:
+ g_value_set_int(value, d->monitor_id);
+ break;
+ case PROP_KEYBOARD_GRAB:
+ g_value_set_boolean(value, d->keyboard_grab_enable);
+ break;
+ case PROP_MOUSE_GRAB:
+ g_value_set_boolean(value, d->mouse_grab_enable);
+ break;
+ case PROP_RESIZE_GUEST:
+ g_value_set_boolean(value, d->resize_guest_enable);
+ break;
+ case PROP_AUTO_CLIPBOARD:
+ g_object_get(d->gtk_session, "auto-clipboard", &boolean, NULL);
+ g_value_set_boolean(value, boolean);
+ break;
+ case PROP_SCALING:
+ g_value_set_boolean(value, d->allow_scaling);
+ break;
+ case PROP_ONLY_DOWNSCALE:
+ g_value_set_boolean(value, d->only_downscale);
+ break;
+ case PROP_DISABLE_INPUTS:
+ g_value_set_boolean(value, d->disable_inputs);
+ break;
+ case PROP_ZOOM_LEVEL:
+ g_value_set_int(value, d->zoom_level);
+ break;
+ case PROP_READY:
+ g_value_set_boolean(value, d->ready);
+ break;
+ case PROP_KEYPRESS_DELAY:
+ g_value_set_uint(value, d->keypress_delay);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void scaling_updated(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(display));
+
+ recalc_geometry(GTK_WIDGET(display));
+ if (d->ximage && window) { /* if not yet shown */
+ gtk_widget_queue_draw(GTK_WIDGET(display));
+ }
+}
+
+static void update_size_request(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ gint reqwidth, reqheight;
+
+ if (d->resize_guest_enable) {
+ reqwidth = 640;
+ reqheight = 480;
+ } else {
+ reqwidth = d->area.width;
+ reqheight = d->area.height;
+ }
+
+ gtk_widget_set_size_request(GTK_WIDGET(display), reqwidth, reqheight);
+ recalc_geometry(GTK_WIDGET(display));
+}
+
+static void update_keyboard_focus(SpiceDisplay *display, gboolean state)
+{
+ SpiceDisplayPrivate *d = display->priv;
+
+ d->keyboard_have_focus = state;
+
+ /* keyboard grab gets inhibited by usb-device-manager when it is
+ in the process of redirecting a usb-device (as this may show a
+ policykit dialog). Making autoredir/automount setting changes while
+ this is happening is not a good idea! */
+ if (d->keyboard_grab_inhibit)
+ return;
+
+ spice_gtk_session_request_auto_usbredir(d->gtk_session, state);
+}
+
+static void update_ready(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ gboolean ready;
+
+ ready = d->mark != 0 && d->monitor_ready;
+
+ if (d->ready == ready)
+ return;
+
+ if (ready && gtk_widget_get_window(GTK_WIDGET(display)))
+ gtk_widget_queue_draw(GTK_WIDGET(display));
+
+ d->ready = ready;
+ g_object_notify(G_OBJECT(display), "ready");
+}
+
+static void set_monitor_ready(SpiceDisplay *self, gboolean ready)
+{
+ SpiceDisplayPrivate *d = self->priv;
+
+ d->monitor_ready = ready;
+ update_ready(self);
+}
+
+static gint get_display_id(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+
+ /* supported monitor_id only with display channel #0 */
+ if (d->channel_id == 0 && d->monitor_id >= 0)
+ return d->monitor_id;
+
+ g_return_val_if_fail(d->monitor_id <= 0, -1);
+
+ return d->channel_id;
+}
+
+static void update_monitor_area(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ SpiceDisplayMonitorConfig *cfg, *c = NULL;
+ GArray *monitors = NULL;
+ int i;
+
+ SPICE_DEBUG("update monitor area %d:%d", d->channel_id, d->monitor_id);
+ if (d->monitor_id < 0)
+ goto whole;
+
+ g_object_get(d->display, "monitors", &monitors, NULL);
+ for (i = 0; monitors != NULL && i < monitors->len; i++) {
+ cfg = &g_array_index(monitors, SpiceDisplayMonitorConfig, i);
+ if (cfg->id == d->monitor_id) {
+ c = cfg;
+ break;
+ }
+ }
+ if (c == NULL) {
+ SPICE_DEBUG("update monitor: no monitor %d", d->monitor_id);
+ set_monitor_ready(display, false);
+ if (spice_channel_test_capability(d->display, SPICE_DISPLAY_CAP_MONITORS_CONFIG)) {
+ SPICE_DEBUG("waiting until MonitorsConfig is received");
+ g_clear_pointer(&monitors, g_array_unref);
+ return;
+ }
+ goto whole;
+ }
+
+ if (c->surface_id != 0) {
+ g_warning("FIXME: only support monitor config with primary surface 0, "
+ "but given config surface %d", c->surface_id);
+ goto whole;
+ }
+
+ if (!d->resize_guest_enable)
+ spice_main_update_display(d->main, get_display_id(display),
+ c->x, c->y, c->width, c->height, FALSE);
+
+ update_area(display, c->x, c->y, c->width, c->height);
+ g_clear_pointer(&monitors, g_array_unref);
+ return;
+
+whole:
+ g_clear_pointer(&monitors, g_array_unref);
+ /* by display whole surface */
+ update_area(display, 0, 0, d->width, d->height);
+ set_monitor_ready(display, true);
+}
+
+static void spice_display_set_property(GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceDisplay *display = SPICE_DISPLAY(object);
+ SpiceDisplayPrivate *d = display->priv;
+
+ switch (prop_id) {
+ case PROP_SESSION:
+ g_warn_if_fail(d->session == NULL);
+ d->session = g_value_dup_object(value);
+ d->gtk_session = spice_gtk_session_get(d->session);
+ spice_g_signal_connect_object(d->gtk_session, "notify::pointer-grabbed",
+ G_CALLBACK(cursor_invalidate), object,
+ G_CONNECT_SWAPPED);
+ break;
+ case PROP_CHANNEL_ID:
+ d->channel_id = g_value_get_int(value);
+ break;
+ case PROP_MONITOR_ID:
+ d->monitor_id = g_value_get_int(value);
+ if (d->display) /* if constructed */
+ update_monitor_area(display);
+ break;
+ case PROP_KEYBOARD_GRAB:
+ d->keyboard_grab_enable = g_value_get_boolean(value);
+ update_keyboard_grab(display);
+ break;
+ case PROP_MOUSE_GRAB:
+ d->mouse_grab_enable = g_value_get_boolean(value);
+ update_mouse_grab(display);
+ break;
+ case PROP_RESIZE_GUEST:
+ d->resize_guest_enable = g_value_get_boolean(value);
+ update_size_request(display);
+ break;
+ case PROP_SCALING:
+ d->allow_scaling = g_value_get_boolean(value);
+ scaling_updated(display);
+ break;
+ case PROP_ONLY_DOWNSCALE:
+ d->only_downscale = g_value_get_boolean(value);
+ scaling_updated(display);
+ break;
+ case PROP_AUTO_CLIPBOARD:
+ g_object_set(d->gtk_session, "auto-clipboard",
+ g_value_get_boolean(value), NULL);
+ break;
+ case PROP_DISABLE_INPUTS:
+ d->disable_inputs = g_value_get_boolean(value);
+ gtk_widget_set_can_focus(GTK_WIDGET(display), !d->disable_inputs);
+ update_keyboard_grab(display);
+ update_mouse_grab(display);
+ break;
+ case PROP_ZOOM_LEVEL:
+ d->zoom_level = g_value_get_int(value);
+ scaling_updated(display);
+ break;
+ case PROP_KEYPRESS_DELAY:
+ d->keypress_delay = g_value_get_uint(value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void gtk_session_property_changed(GObject *gobject,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ SpiceDisplay *display = user_data;
+
+ g_object_notify(G_OBJECT(display), g_param_spec_get_name(pspec));
+}
+
+static void session_inhibit_keyboard_grab_changed(GObject *gobject,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ SpiceDisplay *display = user_data;
+ SpiceDisplayPrivate *d = display->priv;
+
+ g_object_get(d->session, "inhibit-keyboard-grab",
+ &d->keyboard_grab_inhibit, NULL);
+ update_keyboard_grab(display);
+ update_mouse_grab(display);
+}
+
+static void spice_display_dispose(GObject *obj)
+{
+ SpiceDisplay *display = SPICE_DISPLAY(obj);
+ SpiceDisplayPrivate *d = display->priv;
+
+ SPICE_DEBUG("spice display dispose");
+
+ spicex_image_destroy(display);
+ g_clear_object(&d->session);
+ d->gtk_session = NULL;
+
+ if (d->key_delayed_id) {
+ g_source_remove(d->key_delayed_id);
+ d->key_delayed_id = 0;
+ }
+
+ G_OBJECT_CLASS(spice_display_parent_class)->dispose(obj);
+}
+
+static void spice_display_finalize(GObject *obj)
+{
+ SpiceDisplay *display = SPICE_DISPLAY(obj);
+ SpiceDisplayPrivate *d = display->priv;
+
+ SPICE_DEBUG("Finalize spice display");
+
+ if (d->grabseq) {
+ spice_grab_sequence_free(d->grabseq);
+ d->grabseq = NULL;
+ }
+ g_free(d->activeseq);
+ d->activeseq = NULL;
+
+ if (d->show_cursor) {
+ gdk_cursor_unref(d->show_cursor);
+ d->show_cursor = NULL;
+ }
+
+ if (d->mouse_cursor) {
+ gdk_cursor_unref(d->mouse_cursor);
+ d->mouse_cursor = NULL;
+ }
+
+ if (d->mouse_pixbuf) {
+ g_object_unref(d->mouse_pixbuf);
+ d->mouse_pixbuf = NULL;
+ }
+
+ G_OBJECT_CLASS(spice_display_parent_class)->finalize(obj);
+}
+
+static GdkCursor* get_blank_cursor(void)
+{
+ if (g_getenv("SPICE_DEBUG_CURSOR"))
+ return gdk_cursor_new(GDK_DOT);
+
+ return gdk_cursor_new(GDK_BLANK_CURSOR);
+}
+
+static gboolean grab_broken(SpiceDisplay *self, GdkEventGrabBroken *event,
+ gpointer user_data G_GNUC_UNUSED)
+{
+ SPICE_DEBUG("%s (implicit: %d, keyboard: %d)", __FUNCTION__,
+ event->implicit, event->keyboard);
+
+ if (event->keyboard) {
+ try_keyboard_ungrab(self);
+ release_keys(self);
+ }
+
+ /* always release mouse when grab broken, this could be more
+ generally placed in keyboard_ungrab(), but one might worry of
+ breaking someone else code. */
+ try_mouse_ungrab(self);
+
+ return false;
+}
+
+static void drag_data_received_callback(SpiceDisplay *self,
+ GdkDragContext *drag_context,
+ gint x,
+ gint y,
+ GtkSelectionData *data,
+ guint info,
+ guint time,
+ gpointer *user_data)
+{
+ const guchar *buf;
+ gchar **file_urls;
+ int n_files;
+ SpiceDisplayPrivate *d = self->priv;
+ int i = 0;
+ GFile **files;
+
+ /* We get a buf like:
+ * file:///root/a.txt\r\nfile:///root/b.txt\r\n
+ */
+ SPICE_DEBUG("%s: drag a file", __FUNCTION__);
+ buf = gtk_selection_data_get_data(data);
+ g_return_if_fail(buf != NULL);
+
+ file_urls = g_uri_list_extract_uris((const gchar*)buf);
+ n_files = g_strv_length(file_urls);
+ files = g_new0(GFile*, n_files + 1);
+ for (i = 0; i < n_files; i++) {
+ files[i] = g_file_new_for_uri(file_urls[i]);
+ }
+ g_strfreev(file_urls);
+
+ spice_main_file_copy_async(d->main, files, 0, NULL, NULL,
+ NULL, NULL, NULL);
+ for (i = 0; i < n_files; i++) {
+ g_object_unref(files[i]);
+ }
+ g_free(files);
+
+ gtk_drag_finish(drag_context, TRUE, FALSE, time);
+}
+
+static void grab_notify(SpiceDisplay *display, gboolean was_grabbed)
+{
+ SPICE_DEBUG("grab notify %d", was_grabbed);
+
+ if (was_grabbed == FALSE)
+ release_keys(display);
+}
+
+static void spice_display_init(SpiceDisplay *display)
+{
+ GtkWidget *widget = GTK_WIDGET(display);
+ SpiceDisplayPrivate *d;
+ GtkTargetEntry targets = { "text/uri-list", 0, 0 };
+
+ d = display->priv = SPICE_DISPLAY_GET_PRIVATE(display);
+
+ g_signal_connect(display, "grab-broken-event", G_CALLBACK(grab_broken), NULL);
+ g_signal_connect(display, "grab-notify", G_CALLBACK(grab_notify), NULL);
+
+ gtk_drag_dest_set(widget, GTK_DEST_DEFAULT_ALL, &targets, 1, GDK_ACTION_COPY);
+ g_signal_connect(display, "drag-data-received",
+ G_CALLBACK(drag_data_received_callback), NULL);
+
+ gtk_widget_add_events(widget,
+ GDK_STRUCTURE_MASK |
+ GDK_POINTER_MOTION_MASK |
+ GDK_BUTTON_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_BUTTON_MOTION_MASK |
+ GDK_ENTER_NOTIFY_MASK |
+ GDK_LEAVE_NOTIFY_MASK |
+ GDK_KEY_PRESS_MASK |
+ GDK_SCROLL_MASK);
+#ifdef WITH_X11
+ gtk_widget_set_double_buffered(widget, false);
+#else
+ gtk_widget_set_double_buffered(widget, true);
+#endif
+ gtk_widget_set_can_focus(widget, true);
+ gtk_widget_set_has_window(widget, true);
+ d->grabseq = spice_grab_sequence_new_from_string("Control_L+Alt_L");
+ d->activeseq = g_new0(gboolean, d->grabseq->nkeysyms);
+
+ d->mouse_cursor = get_blank_cursor();
+ d->have_mitshm = true;
+}
+
+static GObject *
+spice_display_constructor(GType gtype,
+ guint n_properties,
+ GObjectConstructParam *properties)
+{
+ GObject *obj;
+ SpiceDisplay *display;
+ SpiceDisplayPrivate *d;
+ GList *list;
+ GList *it;
+
+ {
+ /* Always chain up to the parent constructor */
+ GObjectClass *parent_class;
+ parent_class = G_OBJECT_CLASS(spice_display_parent_class);
+ obj = parent_class->constructor(gtype, n_properties, properties);
+ }
+
+ display = SPICE_DISPLAY(obj);
+ d = display->priv;
+
+ if (!d->session)
+ g_error("SpiceDisplay constructed without a session");
+
+ spice_g_signal_connect_object(d->session, "channel-new",
+ G_CALLBACK(channel_new), display, 0);
+ spice_g_signal_connect_object(d->session, "channel-destroy",
+ G_CALLBACK(channel_destroy), display, 0);
+ list = spice_session_get_channels(d->session);
+ for (it = g_list_first(list); it != NULL; it = g_list_next(it)) {
+ if (SPICE_IS_MAIN_CHANNEL(it->data)) {
+ channel_new(d->session, it->data, (gpointer*)display);
+ break;
+ }
+ }
+ for (it = g_list_first(list); it != NULL; it = g_list_next(it)) {
+ if (!SPICE_IS_MAIN_CHANNEL(it->data))
+ channel_new(d->session, it->data, (gpointer*)display);
+ }
+ g_list_free(list);
+
+ spice_g_signal_connect_object(d->gtk_session, "notify::auto-clipboard",
+ G_CALLBACK(gtk_session_property_changed), display, 0);
+
+ spice_g_signal_connect_object(d->session, "notify::inhibit-keyboard-grab",
+ G_CALLBACK(session_inhibit_keyboard_grab_changed),
+ display, 0);
+
+ return obj;
+}
+
+/**
+ * spice_display_set_grab_keys:
+ * @display: the display widget
+ * @seq: (transfer none): key sequence
+ *
+ * Set the key combination to grab/ungrab the keyboard. The default is
+ * "Control L + Alt L".
+ **/
+void spice_display_set_grab_keys(SpiceDisplay *display, SpiceGrabSequence *seq)
+{
+ SpiceDisplayPrivate *d;
+
+ g_return_if_fail(SPICE_IS_DISPLAY(display));
+
+ d = display->priv;
+ g_return_if_fail(d != NULL);
+
+ if (d->grabseq) {
+ spice_grab_sequence_free(d->grabseq);
+ }
+ if (seq)
+ d->grabseq = spice_grab_sequence_copy(seq);
+ else
+ d->grabseq = spice_grab_sequence_new_from_string("Control_L+Alt_L");
+ g_free(d->activeseq);
+ d->activeseq = g_new0(gboolean, d->grabseq->nkeysyms);
+}
+
+#ifdef G_OS_WIN32
+static LRESULT CALLBACK keyboard_hook_cb(int code, WPARAM wparam, LPARAM lparam)
+{
+ if (win32_window && code == HC_ACTION && wparam != WM_KEYUP) {
+ KBDLLHOOKSTRUCT *hooked = (KBDLLHOOKSTRUCT*)lparam;
+ DWORD dwmsg = (hooked->flags << 24) | (hooked->scanCode << 16) | 1;
+
+ if (hooked->vkCode == VK_NUMLOCK || hooked->vkCode == VK_RSHIFT) {
+ dwmsg &= ~(1 << 24);
+ SendMessage(win32_window, wparam, hooked->vkCode, dwmsg);
+ }
+ switch (hooked->vkCode) {
+ case VK_CAPITAL:
+ case VK_SCROLL:
+ case VK_NUMLOCK:
+ case VK_LSHIFT:
+ case VK_RSHIFT:
+ case VK_RCONTROL:
+ case VK_LMENU:
+ case VK_RMENU:
+ break;
+ case VK_LCONTROL:
+ /* When pressing AltGr, an extra VK_LCONTROL with a special
+ * scancode with bit 9 set is sent. Let's ignore the extra
+ * VK_LCONTROL, as that will make AltGr misbehave. */
+ if (hooked->scanCode & 0x200)
+ return 1;
+ break;
+ default:
+ SendMessage(win32_window, wparam, hooked->vkCode, dwmsg);
+ return 1;
+ }
+ }
+ return CallNextHookEx(NULL, code, wparam, lparam);
+}
+#endif
+
+/**
+ * spice_display_get_grab_keys:
+ * @display: the display widget
+ *
+ * Returns: (transfer none): the current grab key combination.
+ **/
+SpiceGrabSequence *spice_display_get_grab_keys(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d;
+
+ g_return_val_if_fail(SPICE_IS_DISPLAY(display), NULL);
+
+ d = display->priv;
+ g_return_val_if_fail(d != NULL, NULL);
+
+ return d->grabseq;
+}
+
+static void try_keyboard_grab(SpiceDisplay *display)
+{
+ GtkWidget *widget = GTK_WIDGET(display);
+ SpiceDisplayPrivate *d = display->priv;
+ GdkGrabStatus status;
+
+ if (g_getenv("SPICE_NOGRAB"))
+ return;
+ if (d->disable_inputs)
+ return;
+
+ if (d->keyboard_grab_inhibit)
+ return;
+ if (!d->keyboard_grab_enable)
+ return;
+ if (d->keyboard_grab_active)
+ return;
+ if (!d->keyboard_have_focus)
+ return;
+ if (!d->mouse_have_pointer)
+ return;
+ if (d->keyboard_grab_released)
+ return;
+
+ g_return_if_fail(gtk_widget_is_focus(widget));
+
+ SPICE_DEBUG("grab keyboard");
+ gtk_widget_grab_focus(widget);
+
+#ifdef G_OS_WIN32
+ if (d->keyboard_hook == NULL)
+ d->keyboard_hook = SetWindowsHookEx(WH_KEYBOARD_LL, keyboard_hook_cb,
+ GetModuleHandle(NULL), 0);
+ g_warn_if_fail(d->keyboard_hook != NULL);
+#endif
+ status = gdk_keyboard_grab(gtk_widget_get_window(widget), FALSE,
+ GDK_CURRENT_TIME);
+ if (status != GDK_GRAB_SUCCESS) {
+ g_warning("keyboard grab failed %d", status);
+ d->keyboard_grab_active = false;
+ } else {
+ d->keyboard_grab_active = true;
+ g_signal_emit(widget, signals[SPICE_DISPLAY_KEYBOARD_GRAB], 0, true);
+ }
+}
+
+static void try_keyboard_ungrab(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ GtkWidget *widget = GTK_WIDGET(display);
+
+ if (!d->keyboard_grab_active)
+ return;
+
+ SPICE_DEBUG("ungrab keyboard");
+ gdk_keyboard_ungrab(GDK_CURRENT_TIME);
+#ifdef G_OS_WIN32
+ if (d->keyboard_hook != NULL) {
+ UnhookWindowsHookEx(d->keyboard_hook);
+ d->keyboard_hook = NULL;
+ }
+#endif
+ d->keyboard_grab_active = false;
+ g_signal_emit(widget, signals[SPICE_DISPLAY_KEYBOARD_GRAB], 0, false);
+}
+
+static void update_keyboard_grab(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+
+ if (d->keyboard_grab_enable &&
+ !d->keyboard_grab_inhibit &&
+ !d->disable_inputs)
+ try_keyboard_grab(display);
+ else
+ try_keyboard_ungrab(display);
+}
+
+static void set_mouse_accel(SpiceDisplay *display, gboolean enabled)
+{
+ SpiceDisplayPrivate *d = display->priv;
+
+#if defined GDK_WINDOWING_X11
+ GdkWindow *w = GDK_WINDOW(gtk_widget_get_window(GTK_WIDGET(display)));
+
+ if (!GDK_IS_X11_DISPLAY(gdk_window_get_display(w))) {
+ SPICE_DEBUG("FIXME: gtk backend is not X11");
+ return;
+ }
+
+ Display *x_display = GDK_WINDOW_XDISPLAY(w);
+ if (enabled) {
+ /* restore mouse acceleration */
+ XChangePointerControl(x_display, True, True,
+ d->x11_accel_numerator, d->x11_accel_denominator, d->x11_threshold);
+ } else {
+ XGetPointerControl(x_display,
+ &d->x11_accel_numerator, &d->x11_accel_denominator, &d->x11_threshold);
+ /* set mouse acceleration to default */
+ XChangePointerControl(x_display, True, True, -1, -1, -1);
+ SPICE_DEBUG("disabled X11 mouse motion %d %d %d",
+ d->x11_accel_numerator, d->x11_accel_denominator, d->x11_threshold);
+ }
+#elif defined GDK_WINDOWING_WIN32
+ if (enabled) {
+ g_return_if_fail(SystemParametersInfo(SPI_SETMOUSE, 0, &d->win_mouse, 0));
+ g_return_if_fail(SystemParametersInfo(SPI_SETMOUSESPEED, 0, (PVOID)(INT_PTR)d->win_mouse_speed, 0));
+ } else {
+ int accel[3] = { 0, 0, 0 }; // disabled
+ g_return_if_fail(SystemParametersInfo(SPI_GETMOUSE, 0, &d->win_mouse, 0));
+ g_return_if_fail(SystemParametersInfo(SPI_GETMOUSESPEED, 0, &d->win_mouse_speed, 0));
+ g_return_if_fail(SystemParametersInfo(SPI_SETMOUSE, 0, &accel, SPIF_SENDCHANGE));
+ g_return_if_fail(SystemParametersInfo(SPI_SETMOUSESPEED, 0, (PVOID)10, SPIF_SENDCHANGE)); // default
+ }
+#else
+ g_warning("Mouse acceleration code missing for your platform");
+#endif
+}
+
+#ifdef G_OS_WIN32
+static gboolean win32_clip_cursor(void)
+{
+ RECT window, workarea, rect;
+ HMONITOR monitor;
+ MONITORINFO mi = { 0, };
+
+ g_return_val_if_fail(win32_window != NULL, FALSE);
+
+ if (!GetWindowRect(win32_window, &window))
+ goto error;
+
+ monitor = MonitorFromRect(&window, MONITOR_DEFAULTTONEAREST);
+ g_return_val_if_fail(monitor != NULL, false);
+
+ mi.cbSize = sizeof(mi);
+ if (!GetMonitorInfo(monitor, &mi))
+ goto error;
+ workarea = mi.rcWork;
+
+ if (!IntersectRect(&rect, &window, &workarea)) {
+ g_critical("error clipping cursor");
+ return false;
+ }
+
+ SPICE_DEBUG("clip rect %ld %ld %ld %ld\n",
+ rect.left, rect.right, rect.top, rect.bottom);
+
+ if (!ClipCursor(&rect))
+ goto error;
+
+ return true;
+
+error:
+ {
+ DWORD errval = GetLastError();
+ gchar *errstr = g_win32_error_message(errval);
+ g_warning("failed to clip cursor (%ld) %s", errval, errstr);
+ }
+
+ return false;
+}
+#endif
+
+static GdkGrabStatus do_pointer_grab(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ GdkWindow *window = GDK_WINDOW(gtk_widget_get_window(GTK_WIDGET(display)));
+ GdkGrabStatus status = GDK_GRAB_BROKEN;
+ GdkCursor *blank = get_blank_cursor();
+
+ if (!gtk_widget_get_realized(GTK_WIDGET(display)))
+ goto end;
+
+#ifdef G_OS_WIN32
+ if (!win32_clip_cursor())
+ goto end;
+#endif
+
+ try_keyboard_grab(display);
+ /*
+ * from gtk-vnc:
+ * For relative mouse to work correctly when grabbed we need to
+ * allow the pointer to move anywhere on the local desktop, so
+ * use NULL for the 'confine_to' argument. Furthermore we need
+ * the coords to be reported to our VNC window, regardless of
+ * what window the pointer is actally over, so use 'FALSE' for
+ * 'owner_events' parameter
+ */
+ status = gdk_pointer_grab(window, FALSE,
+ GDK_POINTER_MOTION_MASK |
+ GDK_BUTTON_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_BUTTON_MOTION_MASK |
+ GDK_SCROLL_MASK,
+ NULL,
+ blank,
+ GDK_CURRENT_TIME);
+ if (status != GDK_GRAB_SUCCESS) {
+ d->mouse_grab_active = false;
+ g_warning("pointer grab failed %d", status);
+ } else {
+ d->mouse_grab_active = true;
+ g_signal_emit(display, signals[SPICE_DISPLAY_MOUSE_GRAB], 0, true);
+ spice_gtk_session_set_pointer_grabbed(d->gtk_session, true);
+ set_mouse_accel(display, FALSE);
+ }
+
+end:
+ gdk_cursor_unref(blank);
+ return status;
+}
+
+static void update_mouse_pointer(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ GdkWindow *window = GDK_WINDOW(gtk_widget_get_window(GTK_WIDGET(display)));
+
+ if (!window)
+ return;
+
+ switch (d->mouse_mode) {
+ case SPICE_MOUSE_MODE_CLIENT:
+ if (gdk_window_get_cursor(window) != d->mouse_cursor)
+ gdk_window_set_cursor(window, d->mouse_cursor);
+ break;
+ case SPICE_MOUSE_MODE_SERVER:
+ if (gdk_window_get_cursor(window) != NULL)
+ gdk_window_set_cursor(window, NULL);
+ break;
+ default:
+ g_warn_if_reached();
+ break;
+ }
+}
+
+static void try_mouse_grab(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+
+ if (g_getenv("SPICE_NOGRAB"))
+ return;
+ if (d->disable_inputs)
+ return;
+
+ if (!d->mouse_have_pointer)
+ return;
+ if (!d->keyboard_have_focus)
+ return;
+
+ if (!d->mouse_grab_enable)
+ return;
+ if (d->mouse_mode != SPICE_MOUSE_MODE_SERVER)
+ return;
+ if (d->mouse_grab_active)
+ return;
+
+ if (do_pointer_grab(display) != GDK_GRAB_SUCCESS)
+ return;
+
+ d->mouse_last_x = -1;
+ d->mouse_last_y = -1;
+}
+
+static void mouse_wrap(SpiceDisplay *display, GdkEventMotion *motion)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ gint xr, yr;
+
+#ifdef G_OS_WIN32
+ RECT clip;
+ g_return_if_fail(GetClipCursor(&clip));
+ xr = clip.left + (clip.right - clip.left) / 2;
+ yr = clip.top + (clip.bottom - clip.top) / 2;
+ /* the clip rectangle has no offset, so we can't use gdk_wrap_pointer */
+ SetCursorPos(xr, yr);
+ d->mouse_last_x = -1;
+ d->mouse_last_y = -1;
+#else
+ GdkScreen *screen = gtk_widget_get_screen(GTK_WIDGET(display));
+ xr = gdk_screen_get_width(screen) / 2;
+ yr = gdk_screen_get_height(screen) / 2;
+
+ if (xr != (gint)motion->x_root || yr != (gint)motion->y_root) {
+ /* FIXME: we try our best to ignore that next pointer move event.. */
+ gdk_display_sync(gdk_screen_get_display(screen));
+
+ gdk_display_warp_pointer(gtk_widget_get_display(GTK_WIDGET(display)),
+ screen, xr, yr);
+ d->mouse_last_x = -1;
+ d->mouse_last_y = -1;
+ }
+#endif
+
+}
+
+static void try_mouse_ungrab(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ double s;
+ int x, y;
+
+ if (!d->mouse_grab_active)
+ return;
+
+ gdk_pointer_ungrab(GDK_CURRENT_TIME);
+ gtk_grab_remove(GTK_WIDGET(display));
+#ifdef G_OS_WIN32
+ ClipCursor(NULL);
+#endif
+ set_mouse_accel(display, TRUE);
+
+ d->mouse_grab_active = false;
+
+ spice_display_get_scaling(display, &s, &x, &y, NULL, NULL);
+
+ gdk_window_get_root_coords(gtk_widget_get_window(GTK_WIDGET(display)),
+ x + d->mouse_guest_x * s,
+ y + d->mouse_guest_y * s,
+ &x, &y);
+
+ gdk_display_warp_pointer(gtk_widget_get_display(GTK_WIDGET(display)),
+ gtk_widget_get_screen(GTK_WIDGET(display)),
+ x, y);
+
+ g_signal_emit(display, signals[SPICE_DISPLAY_MOUSE_GRAB], 0, false);
+ spice_gtk_session_set_pointer_grabbed(d->gtk_session, false);
+}
+
+static void update_mouse_grab(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+
+ if (d->mouse_grab_enable &&
+ !d->keyboard_grab_inhibit &&
+ !d->disable_inputs)
+ try_mouse_grab(display);
+ else
+ try_mouse_ungrab(display);
+}
+
+static void recalc_geometry(GtkWidget *widget)
+{
+ SpiceDisplay *display = SPICE_DISPLAY(widget);
+ SpiceDisplayPrivate *d = display->priv;
+ gdouble zoom = 1.0;
+
+ if (spicex_is_scaled(display))
+ zoom = (gdouble)d->zoom_level / 100;
+
+ SPICE_DEBUG("recalc geom monitor: %d:%d, guest +%d+%d:%dx%d, window %dx%d, zoom %g",
+ d->channel_id, d->monitor_id, d->area.x, d->area.y, d->area.width, d->area.height,
+ d->ww, d->wh, zoom);
+
+ if (d->resize_guest_enable)
+ spice_main_set_display(d->main, get_display_id(display),
+ d->area.x, d->area.y, d->ww / zoom, d->wh / zoom);
+}
+
+/* ---------------------------------------------------------------- */
+
+#define CONVERT_0565_TO_0888(s) \
+ (((((s) << 3) & 0xf8) | (((s) >> 2) & 0x7)) | \
+ ((((s) << 5) & 0xfc00) | (((s) >> 1) & 0x300)) | \
+ ((((s) << 8) & 0xf80000) | (((s) << 3) & 0x70000)))
+
+#define CONVERT_0565_TO_8888(s) (CONVERT_0565_TO_0888(s) | 0xff000000)
+
+#define CONVERT_0555_TO_0888(s) \
+ (((((s) & 0x001f) << 3) | (((s) & 0x001c) >> 2)) | \
+ ((((s) & 0x03e0) << 6) | (((s) & 0x0380) << 1)) | \
+ ((((s) & 0x7c00) << 9) | ((((s) & 0x7000)) << 4)))
+
+#define CONVERT_0555_TO_8888(s) (CONVERT_0555_TO_0888(s) | 0xff000000)
+
+static gboolean do_color_convert(SpiceDisplay *display, GdkRectangle *r)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ guint32 *dest = d->data;
+ guint16 *src = d->data_origin;
+ gint x, y;
+
+ g_return_val_if_fail(r != NULL, false);
+ g_return_val_if_fail(d->format == SPICE_SURFACE_FMT_16_555 ||
+ d->format == SPICE_SURFACE_FMT_16_565, false);
+
+ src += (d->stride / 2) * r->y + r->x;
+ dest += d->area.width * (r->y - d->area.y) + (r->x - d->area.x);
+
+ if (d->format == SPICE_SURFACE_FMT_16_555) {
+ for (y = 0; y < r->height; y++) {
+ for (x = 0; x < r->width; x++) {
+ dest[x] = CONVERT_0555_TO_0888(src[x]);
+ }
+
+ dest += d->area.width;
+ src += d->stride / 2;
+ }
+ } else if (d->format == SPICE_SURFACE_FMT_16_565) {
+ for (y = 0; y < r->height; y++) {
+ for (x = 0; x < r->width; x++) {
+ dest[x] = CONVERT_0565_TO_0888(src[x]);
+ }
+
+ dest += d->area.width;
+ src += d->stride / 2;
+ }
+ }
+
+ return true;
+}
+
+
+#if GTK_CHECK_VERSION (2, 91, 0)
+static gboolean draw_event(GtkWidget *widget, cairo_t *cr)
+{
+ SpiceDisplay *display = SPICE_DISPLAY(widget);
+ SpiceDisplayPrivate *d = display->priv;
+ g_return_val_if_fail(d != NULL, false);
+
+ if (d->mark == 0 || d->data == NULL ||
+ d->area.width == 0 || d->area.height == 0)
+ return false;
+ g_return_val_if_fail(d->ximage != NULL, false);
+
+ spicex_draw_event(display, cr);
+ update_mouse_pointer(display);
+
+ return true;
+}
+#else
+static gboolean expose_event(GtkWidget *widget, GdkEventExpose *expose)
+{
+ SpiceDisplay *display = SPICE_DISPLAY(widget);
+ SpiceDisplayPrivate *d = display->priv;
+ g_return_val_if_fail(d != NULL, false);
+
+ if (d->mark == 0 || d->data == NULL ||
+ d->area.width == 0 || d->area.height == 0)
+ return false;
+ g_return_val_if_fail(d->ximage != NULL, false);
+
+ spicex_expose_event(display, expose);
+ update_mouse_pointer(display);
+
+ return true;
+}
+#endif
+
+/* ---------------------------------------------------------------- */
+typedef enum {
+ SEND_KEY_PRESS,
+ SEND_KEY_RELEASE,
+} SendKeyType;
+
+static void key_press_and_release(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+
+ if (d->key_delayed_scancode == 0)
+ return;
+
+ spice_inputs_key_press_and_release(d->inputs, d->key_delayed_scancode);
+ d->key_delayed_scancode = 0;
+
+ if (d->key_delayed_id) {
+ g_source_remove(d->key_delayed_id);
+ d->key_delayed_id = 0;
+ }
+}
+
+static gboolean key_press_delayed(gpointer data)
+{
+ SpiceDisplay *display = data;
+ SpiceDisplayPrivate *d = display->priv;
+
+ if (d->key_delayed_scancode == 0)
+ return FALSE;
+
+ spice_inputs_key_press(d->inputs, d->key_delayed_scancode);
+ d->key_delayed_scancode = 0;
+
+ if (d->key_delayed_id) {
+ g_source_remove(d->key_delayed_id);
+ d->key_delayed_id = 0;
+ }
+
+ return FALSE;
+}
+
+static void send_key(SpiceDisplay *display, int scancode, SendKeyType type, gboolean press_delayed)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ uint32_t i, b, m;
+
+ g_return_if_fail(scancode != 0);
+
+ if (!d->inputs)
+ return;
+
+ if (d->disable_inputs)
+ return;
+
+ i = scancode / 32;
+ b = scancode % 32;
+ m = (1 << b);
+ g_return_if_fail(i < SPICE_N_ELEMENTS(d->key_state));
+
+ switch (type) {
+ case SEND_KEY_PRESS:
+ /* ensure delayed key is pressed before any new input event */
+ key_press_delayed(display);
+
+ if (press_delayed &&
+ d->keypress_delay != 0 &&
+ !(d->key_state[i] & m)) {
+ g_warn_if_fail(d->key_delayed_id == 0);
+ d->key_delayed_id = g_timeout_add(d->keypress_delay, key_press_delayed, display);
+ d->key_delayed_scancode = scancode;
+ } else
+ spice_inputs_key_press(d->inputs, scancode);
+
+ d->key_state[i] |= m;
+ break;
+
+ case SEND_KEY_RELEASE:
+ if (!(d->key_state[i] & m))
+ break;
+
+ if (d->key_delayed_scancode == scancode)
+ key_press_and_release(display);
+ else {
+ /* ensure delayed key is pressed before other key are released */
+ key_press_delayed(display);
+ spice_inputs_key_release(d->inputs, scancode);
+ }
+
+ d->key_state[i] &= ~m;
+ break;
+
+ default:
+ g_warn_if_reached();
+ }
+}
+
+static void release_keys(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ uint32_t i, b;
+
+ SPICE_DEBUG("%s", __FUNCTION__);
+ for (i = 0; i < SPICE_N_ELEMENTS(d->key_state); i++) {
+ if (!d->key_state[i]) {
+ continue;
+ }
+ for (b = 0; b < 32; b++) {
+ unsigned int scancode = i * 32 + b;
+ if (scancode != 0) {
+ send_key(display, scancode, SEND_KEY_RELEASE, FALSE);
+ }
+ }
+ }
+}
+
+static gboolean check_for_grab_key(SpiceDisplay *display, int type, int keyval,
+ int check_type, int reset_type)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ int i;
+
+ if (!d->grabseq->nkeysyms)
+ return FALSE;
+
+ if (type == check_type) {
+ /* Record the new key */
+ for (i = 0 ; i < d->grabseq->nkeysyms ; i++)
+ if (d->grabseq->keysyms[i] == keyval)
+ d->activeseq[i] = TRUE;
+
+ /* Return if any key is missing */
+ for (i = 0 ; i < d->grabseq->nkeysyms ; i++)
+ if (d->activeseq[i] == FALSE)
+ return FALSE;
+
+ /* resets the whole grab sequence on success */
+ memset(d->activeseq, 0, sizeof(gboolean) * d->grabseq->nkeysyms);
+ return TRUE;
+ } else if (type == reset_type) {
+ /* reset key event type resets the whole grab sequence */
+ memset(d->activeseq, 0, sizeof(gboolean) * d->grabseq->nkeysyms);
+ d->seq_pressed = FALSE;
+ return FALSE;
+ } else
+ g_warn_if_reached();
+
+ return FALSE;
+}
+
+static gboolean check_for_grab_key_pressed(SpiceDisplay *display, int type, int keyval)
+{
+ return check_for_grab_key(display, type, keyval, GDK_KEY_PRESS, GDK_KEY_RELEASE);
+}
+
+static gboolean check_for_grab_key_released(SpiceDisplay *display, int type, int keyval)
+{
+ return check_for_grab_key(display, type, keyval, GDK_KEY_RELEASE, GDK_KEY_PRESS);
+}
+
+static void update_display(SpiceDisplay *display)
+{
+#ifdef G_OS_WIN32
+ win32_window = display ? GDK_WINDOW_HWND(gtk_widget_get_window(GTK_WIDGET(display))) : NULL;
+#endif
+}
+
+static gboolean key_event(GtkWidget *widget, GdkEventKey *key)
+{
+ SpiceDisplay *display = SPICE_DISPLAY(widget);
+ SpiceDisplayPrivate *d = display->priv;
+ int scancode;
+
+#ifdef G_OS_WIN32
+ /* on windows, we ought to ignore the reserved key event? */
+ if (key->hardware_keycode == 0xff)
+ return false;
+
+ if (!d->keyboard_grab_active) {
+ if (key->hardware_keycode == VK_LWIN ||
+ key->hardware_keycode == VK_RWIN ||
+ key->hardware_keycode == VK_APPS)
+ return false;
+ }
+
+#endif
+ SPICE_DEBUG("%s %s: keycode: %d state: %d group %d modifier %d",
+ __FUNCTION__, key->type == GDK_KEY_PRESS ? "press" : "release",
+ key->hardware_keycode, key->state, key->group, key->is_modifier);
+
+ if (!d->seq_pressed && check_for_grab_key_pressed(display, key->type, key->keyval)) {
+ g_signal_emit(widget, signals[SPICE_DISPLAY_GRAB_KEY_PRESSED], 0);
+
+ if (d->mouse_mode == SPICE_MOUSE_MODE_SERVER) {
+ if (d->mouse_grab_active)
+ try_mouse_ungrab(display);
+ else
+ try_mouse_grab(display);
+ }
+ d->seq_pressed = TRUE;
+ } else if (d->seq_pressed && check_for_grab_key_released(display, key->type, key->keyval)) {
+ release_keys(display);
+ if (!d->keyboard_grab_released) {
+ d->keyboard_grab_released = TRUE;
+ try_keyboard_ungrab(display);
+ } else {
+ d->keyboard_grab_released = FALSE;
+ try_keyboard_grab(display);
+ }
+ d->seq_pressed = FALSE;
+ }
+
+ if (!d->inputs)
+ return true;
+
+ scancode = vnc_display_keymap_gdk2xtkbd(d->keycode_map, d->keycode_maplen,
+ key->hardware_keycode);
+#ifdef G_OS_WIN32
+ /* MapVirtualKey doesn't return scancode with needed higher byte */
+ scancode = MapVirtualKey(key->hardware_keycode, MAPVK_VK_TO_VSC) |
+ (scancode & 0xff00);
+#endif
+
+ switch (key->type) {
+ case GDK_KEY_PRESS:
+ send_key(display, scancode, SEND_KEY_PRESS, !key->is_modifier);
+ break;
+ case GDK_KEY_RELEASE:
+ send_key(display, scancode, SEND_KEY_RELEASE, !key->is_modifier);
+ break;
+ default:
+ g_warn_if_reached();
+ break;
+ }
+
+ return true;
+}
+
+static guint get_scancode_from_keyval(SpiceDisplay *display, guint keyval)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ guint keycode = 0;
+ GdkKeymapKey *keys = NULL;
+ gint n_keys = 0;
+
+ if (gdk_keymap_get_entries_for_keyval(gdk_keymap_get_default(),
+ keyval, &keys, &n_keys)) {
+ /* FIXME what about levels? */
+ keycode = keys[0].keycode;
+ g_free(keys);
+ } else {
+ g_warning("could not lookup keyval %u, please report a bug", keyval);
+ return 0;
+ }
+
+ return vnc_display_keymap_gdk2xtkbd(d->keycode_map, d->keycode_maplen, keycode);
+}
+
+
+/**
+ * spice_display_send_keys:
+ * @display: The #SpiceDisplay
+ * @keyvals: (array length=nkeyvals): Keyval array
+ * @nkeyvals: Length of keyvals
+ * @kind: #SpiceDisplayKeyEvent action
+ *
+ * Send keyval press/release events to the display.
+ *
+ **/
+void spice_display_send_keys(SpiceDisplay *display, const guint *keyvals,
+ int nkeyvals, SpiceDisplayKeyEvent kind)
+{
+ int i;
+
+ g_return_if_fail(SPICE_IS_DISPLAY(display));
+ g_return_if_fail(keyvals != NULL);
+
+ SPICE_DEBUG("%s", __FUNCTION__);
+
+ if (kind & SPICE_DISPLAY_KEY_EVENT_PRESS) {
+ for (i = 0 ; i < nkeyvals ; i++)
+ send_key(display, get_scancode_from_keyval(display, keyvals[i]), SEND_KEY_PRESS, FALSE);
+ }
+
+ if (kind & SPICE_DISPLAY_KEY_EVENT_RELEASE) {
+ for (i = (nkeyvals-1) ; i >= 0 ; i--)
+ send_key(display, get_scancode_from_keyval(display, keyvals[i]), SEND_KEY_RELEASE, FALSE);
+ }
+}
+
+static gboolean enter_event(GtkWidget *widget, GdkEventCrossing *crossing G_GNUC_UNUSED)
+{
+ SpiceDisplay *display = SPICE_DISPLAY(widget);
+ SpiceDisplayPrivate *d = display->priv;
+
+ SPICE_DEBUG("%s", __FUNCTION__);
+
+ d->mouse_have_pointer = true;
+ try_keyboard_grab(display);
+ update_display(display);
+
+ return true;
+}
+
+static gboolean leave_event(GtkWidget *widget, GdkEventCrossing *crossing G_GNUC_UNUSED)
+{
+ SpiceDisplay *display = SPICE_DISPLAY(widget);
+ SpiceDisplayPrivate *d = display->priv;
+
+ SPICE_DEBUG("%s", __FUNCTION__);
+
+ if (d->mouse_grab_active)
+ return true;
+
+ d->mouse_have_pointer = false;
+ try_keyboard_ungrab(display);
+
+ return true;
+}
+
+static gboolean focus_in_event(GtkWidget *widget, GdkEventFocus *focus G_GNUC_UNUSED)
+{
+ SpiceDisplay *display = SPICE_DISPLAY(widget);
+ SpiceDisplayPrivate *d = display->priv;
+
+ SPICE_DEBUG("%s", __FUNCTION__);
+
+ /*
+ * Ignore focus in when we already have the focus
+ * (this happens when doing an ungrab from the leave_event callback).
+ */
+ if (d->keyboard_have_focus)
+ return true;
+
+ release_keys(display);
+ if (!d->disable_inputs)
+ spice_gtk_session_sync_keyboard_modifiers(d->gtk_session);
+ if (d->keyboard_grab_released)
+ memset(d->activeseq, 0, sizeof(gboolean) * d->grabseq->nkeysyms);
+ update_keyboard_focus(display, true);
+ try_keyboard_grab(display);
+
+ if (gtk_widget_get_realized(widget))
+ update_display(display);
+
+ return true;
+}
+
+static gboolean focus_out_event(GtkWidget *widget, GdkEventFocus *focus G_GNUC_UNUSED)
+{
+ SpiceDisplay *display = SPICE_DISPLAY(widget);
+ SpiceDisplayPrivate *d = display->priv;
+
+ SPICE_DEBUG("%s", __FUNCTION__);
+ update_display(NULL);
+
+ /*
+ * Ignore focus out after a keyboard grab
+ * (this happens when doing the grab from the enter_event callback).
+ */
+ if (d->keyboard_grab_active)
+ return true;
+
+ release_keys(display);
+ update_keyboard_focus(display, false);
+
+ return true;
+}
+
+static int button_gdk_to_spice(int gdk)
+{
+ static const int map[] = {
+ [ 1 ] = SPICE_MOUSE_BUTTON_LEFT,
+ [ 2 ] = SPICE_MOUSE_BUTTON_MIDDLE,
+ [ 3 ] = SPICE_MOUSE_BUTTON_RIGHT,
+ [ 4 ] = SPICE_MOUSE_BUTTON_UP,
+ [ 5 ] = SPICE_MOUSE_BUTTON_DOWN,
+ };
+
+ if (gdk < SPICE_N_ELEMENTS(map)) {
+ return map [ gdk ];
+ }
+ return 0;
+}
+
+static int button_mask_gdk_to_spice(int gdk)
+{
+ int spice = 0;
+
+ if (gdk & GDK_BUTTON1_MASK)
+ spice |= SPICE_MOUSE_BUTTON_MASK_LEFT;
+ if (gdk & GDK_BUTTON2_MASK)
+ spice |= SPICE_MOUSE_BUTTON_MASK_MIDDLE;
+ if (gdk & GDK_BUTTON3_MASK)
+ spice |= SPICE_MOUSE_BUTTON_MASK_RIGHT;
+ return spice;
+}
+
+G_GNUC_INTERNAL
+void spicex_transform_input (SpiceDisplay *display,
+ double window_x, double window_y,
+ int *input_x, int *input_y)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ int display_x, display_y, display_w, display_h;
+ double is;
+
+ spice_display_get_scaling(display, NULL,
+ &display_x, &display_y,
+ &display_w, &display_h);
+
+ /* For input we need a different scaling factor in order to
+ be able to reach the full width of a display. For instance, consider
+ a display of 100 pixels showing in a window 10 pixels wide. The normal
+ scaling factor here would be 100/10==10, but if you then take the largest
+ possible window coordinate, i.e. 9 and multiply by 10 you get 90, not 99,
+ which is the max display coord.
+
+ If you want to be able to reach the last pixel in the window you need
+ max_window_x * input_scale == max_display_x, which is
+ (window_width - 1) * input_scale == (display_width - 1)
+
+ Note, this is the inverse of s (i.e. s ~= 1/is) as we're converting the
+ coordinates in the inverse direction (window -> display) as the fb size
+ (display -> window).
+ */
+ is = (double)(d->area.width-1) / (double)(display_w-1);
+
+ window_x -= display_x;
+ window_y -= display_y;
+
+ *input_x = floor (window_x * is);
+ *input_y = floor (window_y * is);
+}
+
+static gboolean motion_event(GtkWidget *widget, GdkEventMotion *motion)
+{
+ SpiceDisplay *display = SPICE_DISPLAY(widget);
+ SpiceDisplayPrivate *d = display->priv;
+ int x, y;
+
+ if (!d->inputs)
+ return true;
+ if (d->disable_inputs)
+ return true;
+
+ d->seq_pressed = FALSE;
+
+ if (d->keyboard_grab_released && d->keyboard_have_focus) {
+ d->keyboard_grab_released = FALSE;
+ release_keys(display);
+ try_keyboard_grab(display);
+ }
+
+ spicex_transform_input (display, motion->x, motion->y, &x, &y);
+
+ switch (d->mouse_mode) {
+ case SPICE_MOUSE_MODE_CLIENT:
+ if (x >= 0 && x < d->area.width &&
+ y >= 0 && y < d->area.height) {
+ spice_inputs_position(d->inputs, x, y, get_display_id(display),
+ button_mask_gdk_to_spice(motion->state));
+ }
+ break;
+ case SPICE_MOUSE_MODE_SERVER:
+ if (d->mouse_grab_active) {
+ gint dx = d->mouse_last_x != -1 ? x - d->mouse_last_x : 0;
+ gint dy = d->mouse_last_y != -1 ? y - d->mouse_last_y : 0;
+
+ spice_inputs_motion(d->inputs, dx, dy,
+ button_mask_gdk_to_spice(motion->state));
+
+ d->mouse_last_x = x;
+ d->mouse_last_y = y;
+ if (dx != 0 || dy != 0)
+ mouse_wrap(display, motion);
+ }
+ break;
+ default:
+ g_warn_if_reached();
+ break;
+ }
+ return true;
+}
+
+static gboolean scroll_event(GtkWidget *widget, GdkEventScroll *scroll)
+{
+ int button;
+ SpiceDisplay *display = SPICE_DISPLAY(widget);
+ SpiceDisplayPrivate *d = display->priv;
+
+ SPICE_DEBUG("%s", __FUNCTION__);
+
+ if (!d->inputs)
+ return true;
+ if (d->disable_inputs)
+ return true;
+
+ if (scroll->direction == GDK_SCROLL_UP)
+ button = SPICE_MOUSE_BUTTON_UP;
+ else if (scroll->direction == GDK_SCROLL_DOWN)
+ button = SPICE_MOUSE_BUTTON_DOWN;
+ else {
+ SPICE_DEBUG("unsupported scroll direction");
+ return true;
+ }
+
+ spice_inputs_button_press(d->inputs, button,
+ button_mask_gdk_to_spice(scroll->state));
+ spice_inputs_button_release(d->inputs, button,
+ button_mask_gdk_to_spice(scroll->state));
+ return true;
+}
+
+static gboolean button_event(GtkWidget *widget, GdkEventButton *button)
+{
+ SpiceDisplay *display = SPICE_DISPLAY(widget);
+ SpiceDisplayPrivate *d = display->priv;
+ int x, y;
+
+ SPICE_DEBUG("%s %s: button %d, state 0x%x", __FUNCTION__,
+ button->type == GDK_BUTTON_PRESS ? "press" : "release",
+ button->button, button->state);
+
+ if (d->disable_inputs)
+ return true;
+
+ spicex_transform_input (display, button->x, button->y, &x, &y);
+ if ((x < 0 || x >= d->area.width ||
+ y < 0 || y >= d->area.height) &&
+ d->mouse_mode == SPICE_MOUSE_MODE_CLIENT) {
+ /* rule out clicks in outside region */
+ return true;
+ }
+
+ gtk_widget_grab_focus(widget);
+ if (d->mouse_mode == SPICE_MOUSE_MODE_SERVER) {
+ if (!d->mouse_grab_active) {
+ try_mouse_grab(display);
+ return true;
+ }
+ } else
+ /* allow to drag and drop between windows/displays:
+
+ By default, X (and other window system) do a pointer grab
+ when you press a button, so that the release event is
+ received by the same window regardless of where the pointer
+ is. Here, we change that behaviour, so that you can press
+ and release in two differents displays. This is only
+ supported in client mouse mode.
+
+ FIXME: should be multiple widget grab, but how?
+ or should know the position of the other widgets?
+ */
+ gdk_pointer_ungrab(GDK_CURRENT_TIME);
+
+ if (!d->inputs)
+ return true;
+
+ switch (button->type) {
+ case GDK_BUTTON_PRESS:
+ spice_inputs_button_press(d->inputs,
+ button_gdk_to_spice(button->button),
+ button_mask_gdk_to_spice(button->state));
+ break;
+ case GDK_BUTTON_RELEASE:
+ spice_inputs_button_release(d->inputs,
+ button_gdk_to_spice(button->button),
+ button_mask_gdk_to_spice(button->state));
+ break;
+ default:
+ break;
+ }
+ return true;
+}
+
+static gboolean configure_event(GtkWidget *widget, GdkEventConfigure *conf)
+{
+ SpiceDisplay *display = SPICE_DISPLAY(widget);
+ SpiceDisplayPrivate *d = display->priv;
+
+ if (conf->width == d->ww && conf->height == d->wh &&
+ conf->x == d->mx && conf->y == d->my) {
+ return true;
+ }
+
+ if (conf->width != d->ww || conf->height != d->wh) {
+ d->ww = conf->width;
+ d->wh = conf->height;
+ recalc_geometry(widget);
+ }
+
+ d->mx = conf->x;
+ d->my = conf->y;
+
+#ifdef G_OS_WIN32
+ if (d->mouse_grab_active) {
+ try_mouse_ungrab(display);
+ try_mouse_grab(display);
+ }
+#endif
+
+ return true;
+}
+
+static void update_image(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+
+ spicex_image_create(display);
+ if (d->convert)
+ do_color_convert(display, &d->area);
+}
+
+static void realize(GtkWidget *widget)
+{
+ SpiceDisplay *display = SPICE_DISPLAY(widget);
+ SpiceDisplayPrivate *d = display->priv;
+
+ GTK_WIDGET_CLASS(spice_display_parent_class)->realize(widget);
+
+ d->keycode_map =
+ vnc_display_keymap_gdk2xtkbd_table(gtk_widget_get_window(widget),
+ &d->keycode_maplen);
+ update_image(display);
+}
+
+static void unrealize(GtkWidget *widget)
+{
+ spicex_image_destroy(SPICE_DISPLAY(widget));
+
+ GTK_WIDGET_CLASS(spice_display_parent_class)->unrealize(widget);
+}
+
+
+/* ---------------------------------------------------------------- */
+
+static void spice_display_class_init(SpiceDisplayClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ GtkWidgetClass *gtkwidget_class = GTK_WIDGET_CLASS(klass);
+
+#if GTK_CHECK_VERSION (2, 91, 0)
+ gtkwidget_class->draw = draw_event;
+#else
+ gtkwidget_class->expose_event = expose_event;
+#endif
+ gtkwidget_class->key_press_event = key_event;
+ gtkwidget_class->key_release_event = key_event;
+ gtkwidget_class->enter_notify_event = enter_event;
+ gtkwidget_class->leave_notify_event = leave_event;
+ gtkwidget_class->focus_in_event = focus_in_event;
+ gtkwidget_class->focus_out_event = focus_out_event;
+ gtkwidget_class->motion_notify_event = motion_event;
+ gtkwidget_class->button_press_event = button_event;
+ gtkwidget_class->button_release_event = button_event;
+ gtkwidget_class->configure_event = configure_event;
+ gtkwidget_class->scroll_event = scroll_event;
+ gtkwidget_class->realize = realize;
+ gtkwidget_class->unrealize = unrealize;
+
+ gobject_class->constructor = spice_display_constructor;
+ gobject_class->dispose = spice_display_dispose;
+ gobject_class->finalize = spice_display_finalize;
+ gobject_class->get_property = spice_display_get_property;
+ gobject_class->set_property = spice_display_set_property;
+
+ /**
+ * SpiceDisplay:session:
+ *
+ * #SpiceSession for this #SpiceDisplay
+ *
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_SESSION,
+ g_param_spec_object("session",
+ "Session",
+ "SpiceSession",
+ SPICE_TYPE_SESSION,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceDisplay:channel-id:
+ *
+ * channel-id for this #SpiceDisplay
+ *
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_CHANNEL_ID,
+ g_param_spec_int("channel-id",
+ "Channel ID",
+ "Channel ID for this display",
+ 0, 255, 0,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_KEYBOARD_GRAB,
+ g_param_spec_boolean("grab-keyboard",
+ "Grab Keyboard",
+ "Whether we should grab the keyboard.",
+ TRUE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_MOUSE_GRAB,
+ g_param_spec_boolean("grab-mouse",
+ "Grab Mouse",
+ "Whether we should grab the mouse.",
+ TRUE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_RESIZE_GUEST,
+ g_param_spec_boolean("resize-guest",
+ "Resize guest",
+ "Try to adapt guest display on window resize. "
+ "Requires guest cooperation.",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceDisplay:ready:
+ *
+ * Indicate whether the display is ready to be shown. It takes
+ * into account several conditions, such as the channel display
+ * "mark" state, whether the monitor area is visible..
+ *
+ * Since: 0.13
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_READY,
+ g_param_spec_boolean("ready",
+ "Ready",
+ "Ready to display",
+ FALSE,
+ G_PARAM_READABLE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceDisplay:auto-clipboard:
+ *
+ * When this is true the clipboard gets automatically shared between host
+ * and guest.
+ *
+ * Deprecated: 0.8: Use SpiceGtkSession:auto-clipboard property instead
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_AUTO_CLIPBOARD,
+ g_param_spec_boolean("auto-clipboard",
+ "Auto clipboard",
+ "Automatically relay clipboard changes between "
+ "host and guest.",
+ TRUE,
+ G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS |
+ G_PARAM_DEPRECATED));
+
+ g_object_class_install_property
+ (gobject_class, PROP_SCALING,
+ g_param_spec_boolean("scaling", "Scaling",
+ "Whether we should use scaling",
+ TRUE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceDisplay:only-downscale:
+ *
+ * If scaling, only scale down, never up.
+ *
+ * Since: 0.14
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_ONLY_DOWNSCALE,
+ g_param_spec_boolean("only-downscale", "Only Downscale",
+ "If scaling, only scale down, never up",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceDisplay:keypress-delay:
+ *
+ * Delay in ms of non-modifiers key press events. If the key is
+ * released before this delay, a single press & release event is
+ * sent to the server. If the key is pressed longer than the
+ * keypress-delay, the server will receive the delayed press
+ * event, and a following release event when the key is released.
+ *
+ * Since: 0.13
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_KEYPRESS_DELAY,
+ g_param_spec_uint("keypress-delay", "Keypress delay",
+ "Keypress delay",
+ 0, G_MAXUINT, 100,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceDisplay:disable-inputs:
+ *
+ * Disable all keyboard & mouse inputs.
+ *
+ * Since: 0.8
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_DISABLE_INPUTS,
+ g_param_spec_boolean("disable-inputs", "Disable inputs",
+ "Whether inputs should be disabled",
+ FALSE,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+
+ /**
+ * SpiceDisplay:zoom-level:
+ *
+ * Zoom level in percentage, from 10 to 400. Default to 100.
+ * (this option is only supported with cairo backend when scaling
+ * is enabled)
+ *
+ * Since: 0.10
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_ZOOM_LEVEL,
+ g_param_spec_int("zoom-level", "Zoom Level",
+ "Zoom Level",
+ 10, 400, 100,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceDisplay:monitor-id:
+ *
+ * Select monitor from #SpiceDisplay to show.
+ * The value -1 means the whole display is shown.
+ * By default, the monitor 0 is selected.
+ *
+ * Since: 0.13
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_MONITOR_ID,
+ g_param_spec_int("monitor-id",
+ "Monitor ID",
+ "Select monitor ID",
+ -1, G_MAXINT, 0,
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceDisplay::mouse-grab:
+ * @display: the #SpiceDisplay that emitted the signal
+ * @status: 1 if grabbed, 0 otherwise.
+ *
+ * Notify when the mouse grab is active or not.
+ **/
+ signals[SPICE_DISPLAY_MOUSE_GRAB] =
+ g_signal_new("mouse-grab",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceDisplayClass, mouse_grab),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__INT,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_INT);
+
+ /**
+ * SpiceDisplay::keyboard-grab:
+ * @display: the #SpiceDisplay that emitted the signal
+ * @status: 1 if grabbed, 0 otherwise.
+ *
+ * Notify when the keyboard grab is active or not.
+ **/
+ signals[SPICE_DISPLAY_KEYBOARD_GRAB] =
+ g_signal_new("keyboard-grab",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceDisplayClass, keyboard_grab),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__INT,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_INT);
+
+ /**
+ * SpiceDisplay::grab-keys-pressed:
+ * @display: the #SpiceDisplay that emitted the signal
+ *
+ * Notify when the grab keys have been pressed
+ **/
+ signals[SPICE_DISPLAY_GRAB_KEY_PRESSED] =
+ g_signal_new("grab-keys-pressed",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceDisplayClass, keyboard_grab),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE,
+ 0);
+
+ g_type_class_add_private(klass, sizeof(SpiceDisplayPrivate));
+}
+
+/* ---------------------------------------------------------------- */
+
+#define SPICE_GDK_BUTTONS_MASK \
+ (GDK_BUTTON1_MASK|GDK_BUTTON2_MASK|GDK_BUTTON3_MASK|GDK_BUTTON4_MASK|GDK_BUTTON5_MASK)
+
+static void update_mouse_mode(SpiceChannel *channel, gpointer data)
+{
+ SpiceDisplay *display = data;
+ SpiceDisplayPrivate *d = display->priv;
+ GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(display));
+
+ g_object_get(channel, "mouse-mode", &d->mouse_mode, NULL);
+ SPICE_DEBUG("mouse mode %d", d->mouse_mode);
+
+ switch (d->mouse_mode) {
+ case SPICE_MOUSE_MODE_CLIENT:
+ try_mouse_ungrab(display);
+ break;
+ case SPICE_MOUSE_MODE_SERVER:
+ d->mouse_guest_x = -1;
+ d->mouse_guest_y = -1;
+
+ if (window != NULL) {
+ GdkModifierType modifiers;
+ gdk_window_get_pointer(window, NULL, NULL, &modifiers);
+
+ if (modifiers & SPICE_GDK_BUTTONS_MASK)
+ try_mouse_grab(display);
+ }
+ break;
+ default:
+ g_warn_if_reached();
+ }
+
+ update_mouse_pointer(display);
+}
+
+static void update_area(SpiceDisplay *display,
+ gint x, gint y, gint width, gint height)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ GdkRectangle primary = {
+ .x = 0,
+ .y = 0,
+ .width = d->width,
+ .height = d->height
+ };
+ GdkRectangle area = {
+ .x = x,
+ .y = y,
+ .width = width,
+ .height = height
+ };
+
+ SPICE_DEBUG("update area, primary: %dx%d, area: +%d+%d %dx%d", d->width, d->height, area.x, area.y, area.width, area.height);
+
+ if (!gdk_rectangle_intersect(&primary, &area, &area)) {
+ SPICE_DEBUG("The monitor area is not intersecting primary surface");
+ memset(&d->area, '\0', sizeof(d->area));
+ set_monitor_ready(display, false);
+ return;
+ }
+
+ spicex_image_destroy(display);
+ d->area = area;
+ if (gtk_widget_get_realized(GTK_WIDGET(display)))
+ update_image(display);
+
+ update_size_request(display);
+
+ set_monitor_ready(display, true);
+}
+
+static void primary_create(SpiceChannel *channel, gint format,
+ gint width, gint height, gint stride,
+ gint shmid, gpointer imgdata, gpointer data)
+{
+ SpiceDisplay *display = data;
+ SpiceDisplayPrivate *d = display->priv;
+
+ d->format = format;
+ d->stride = stride;
+ d->shmid = shmid;
+ d->width = width;
+ d->height = height;
+ d->data_origin = d->data = imgdata;
+
+ update_monitor_area(display);
+}
+
+static void primary_destroy(SpiceChannel *channel, gpointer data)
+{
+ SpiceDisplay *display = SPICE_DISPLAY(data);
+ SpiceDisplayPrivate *d = display->priv;
+
+ spicex_image_destroy(display);
+ d->width = 0;
+ d->height = 0;
+ d->stride = 0;
+ d->shmid = 0;
+ d->data = NULL;
+ d->data_origin = NULL;
+ set_monitor_ready(display, false);
+}
+
+static void invalidate(SpiceChannel *channel,
+ gint x, gint y, gint w, gint h, gpointer data)
+{
+ SpiceDisplay *display = data;
+ SpiceDisplayPrivate *d = display->priv;
+ int display_x, display_y;
+ int x1, y1, x2, y2;
+ double s;
+ GdkRectangle rect = {
+ .x = x,
+ .y = y,
+ .width = w,
+ .height = h
+ };
+
+ if (!gtk_widget_get_window(GTK_WIDGET(display)))
+ return;
+
+ if (!gdk_rectangle_intersect(&rect, &d->area, &rect))
+ return;
+
+ if (d->convert)
+ do_color_convert(display, &rect);
+
+ spice_display_get_scaling(display, &s,
+ &display_x, &display_y,
+ NULL, NULL);
+
+ x1 = floor ((rect.x - d->area.x) * s);
+ y1 = floor ((rect.y - d->area.y) * s);
+ x2 = ceil ((rect.x - d->area.x + rect.width) * s);
+ y2 = ceil ((rect.y - d->area.y + rect.height) * s);
+
+ gtk_widget_queue_draw_area(GTK_WIDGET(display),
+ display_x + x1, display_y + y1,
+ x2 - x1, y2-y1);
+}
+
+static void mark(SpiceDisplay *display, gint mark)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ g_return_if_fail(d != NULL);
+
+ SPICE_DEBUG("widget mark: %d, %d:%d %p", mark, d->channel_id, d->monitor_id, display);
+ d->mark = mark;
+ update_ready(display);
+}
+
+static void cursor_set(SpiceCursorChannel *channel,
+ gint width, gint height, gint hot_x, gint hot_y,
+ gpointer rgba, gpointer data)
+{
+ SpiceDisplay *display = data;
+ SpiceDisplayPrivate *d = display->priv;
+ GdkCursor *cursor = NULL;
+
+ cursor_invalidate(display);
+
+ if (d->mouse_pixbuf) {
+ g_object_unref(d->mouse_pixbuf);
+ d->mouse_pixbuf = NULL;
+ }
+
+ if (rgba != NULL) {
+ d->mouse_pixbuf = gdk_pixbuf_new_from_data(g_memdup(rgba, width * height * 4),
+ GDK_COLORSPACE_RGB,
+ TRUE, 8,
+ width,
+ height,
+ width * 4,
+ (GdkPixbufDestroyNotify)g_free, NULL);
+ d->mouse_hotspot.x = hot_x;
+ d->mouse_hotspot.y = hot_y;
+ cursor = gdk_cursor_new_from_pixbuf(gtk_widget_get_display(GTK_WIDGET(display)),
+ d->mouse_pixbuf, hot_x, hot_y);
+ } else
+ g_warn_if_reached();
+
+ if (d->show_cursor) {
+ /* unhide */
+ gdk_cursor_unref(d->show_cursor);
+ d->show_cursor = NULL;
+ if (d->mouse_mode == SPICE_MOUSE_MODE_SERVER) {
+ /* keep a hidden cursor, will be shown in cursor_move() */
+ d->show_cursor = cursor;
+ return;
+ }
+ }
+
+ gdk_cursor_unref(d->mouse_cursor);
+ d->mouse_cursor = cursor;
+
+ update_mouse_pointer(display);
+ cursor_invalidate(display);
+}
+
+static void cursor_hide(SpiceCursorChannel *channel, gpointer data)
+{
+ SpiceDisplay *display = data;
+ SpiceDisplayPrivate *d = display->priv;
+
+ if (d->show_cursor != NULL) /* then we are already hidden */
+ return;
+
+ cursor_invalidate(display);
+ d->show_cursor = d->mouse_cursor;
+ d->mouse_cursor = get_blank_cursor();
+ update_mouse_pointer(display);
+}
+
+G_GNUC_INTERNAL
+void spice_display_get_scaling(SpiceDisplay *display,
+ double *s_out,
+ int *x_out, int *y_out,
+ int *w_out, int *h_out)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ int fbw = d->area.width, fbh = d->area.height;
+ int ww, wh;
+ int x, y, w, h;
+ double s;
+
+ if (gtk_widget_get_realized (GTK_WIDGET(display)))
+ gdk_drawable_get_size(gtk_widget_get_window(GTK_WIDGET(display)), &ww, &wh);
+ else {
+ ww = fbw;
+ wh = fbh;
+ }
+
+ if (!spicex_is_scaled(display)) {
+ s = 1.0;
+ x = 0;
+ y = 0;
+ if (ww > d->area.width)
+ x = (ww - d->area.width) / 2;
+ if (wh > d->area.height)
+ y = (wh - d->area.height) / 2;
+ w = fbw;
+ h = fbh;
+ } else {
+ s = MIN ((double)ww / (double)fbw, (double)wh / (double)fbh);
+
+ if (d->only_downscale && s >= 1.0)
+ s = 1.0;
+
+ /* Round to int size */
+ w = floor (fbw * s + 0.5);
+ h = floor (fbh * s + 0.5);
+
+ /* Center the display */
+ x = (ww - w) / 2;
+ y = (wh - h) / 2;
+ }
+
+ if (s_out)
+ *s_out = s;
+ if (w_out)
+ *w_out = w;
+ if (h_out)
+ *h_out = h;
+ if (x_out)
+ *x_out = x;
+ if (y_out)
+ *y_out = y;
+}
+
+static void cursor_invalidate(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d = display->priv;
+ double s;
+ int x, y;
+
+ if (!gtk_widget_get_realized (GTK_WIDGET(display)))
+ return;
+
+ if (d->mouse_pixbuf == NULL)
+ return;
+
+ if (!d->ready || !d->monitor_ready)
+ return;
+
+ spice_display_get_scaling(display, &s, &x, &y, NULL, NULL);
+
+ gtk_widget_queue_draw_area(GTK_WIDGET(display),
+ floor ((d->mouse_guest_x - d->mouse_hotspot.x - d->area.x) * s) + x,
+ floor ((d->mouse_guest_y - d->mouse_hotspot.y - d->area.y) * s) + y,
+ ceil (gdk_pixbuf_get_width(d->mouse_pixbuf) * s),
+ ceil (gdk_pixbuf_get_height(d->mouse_pixbuf) * s));
+}
+
+static void cursor_move(SpiceCursorChannel *channel, gint x, gint y, gpointer data)
+{
+ SpiceDisplay *display = data;
+ SpiceDisplayPrivate *d = display->priv;
+
+ cursor_invalidate(display);
+
+ d->mouse_guest_x = x;
+ d->mouse_guest_y = y;
+
+ cursor_invalidate(display);
+
+ /* apparently we have to restore cursor when "cursor_move" */
+ if (d->show_cursor != NULL) {
+ gdk_cursor_unref(d->mouse_cursor);
+ d->mouse_cursor = d->show_cursor;
+ d->show_cursor = NULL;
+ update_mouse_pointer(display);
+ }
+}
+
+static void cursor_reset(SpiceCursorChannel *channel, gpointer data)
+{
+ SpiceDisplay *display = data;
+ GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(display));
+
+ if (!window) {
+ SPICE_DEBUG("%s: no window, returning", __FUNCTION__);
+ return;
+ }
+
+ SPICE_DEBUG("%s", __FUNCTION__);
+ gdk_window_set_cursor(window, NULL);
+}
+
+static void channel_new(SpiceSession *s, SpiceChannel *channel, gpointer data)
+{
+ SpiceDisplay *display = data;
+ SpiceDisplayPrivate *d = display->priv;
+ int id;
+
+ g_object_get(channel, "channel-id", &id, NULL);
+ if (SPICE_IS_MAIN_CHANNEL(channel)) {
+ d->main = SPICE_MAIN_CHANNEL(channel);
+ spice_g_signal_connect_object(channel, "main-mouse-update",
+ G_CALLBACK(update_mouse_mode), display, 0);
+ update_mouse_mode(channel, display);
+ return;
+ }
+
+ if (SPICE_IS_DISPLAY_CHANNEL(channel)) {
+ SpiceDisplayPrimary primary;
+ if (id != d->channel_id)
+ return;
+ d->display = channel;
+ spice_g_signal_connect_object(channel, "display-primary-create",
+ G_CALLBACK(primary_create), display, 0);
+ spice_g_signal_connect_object(channel, "display-primary-destroy",
+ G_CALLBACK(primary_destroy), display, 0);
+ spice_g_signal_connect_object(channel, "display-invalidate",
+ G_CALLBACK(invalidate), display, 0);
+ spice_g_signal_connect_object(channel, "display-mark",
+ G_CALLBACK(mark), display, G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+ spice_g_signal_connect_object(channel, "notify::monitors",
+ G_CALLBACK(update_monitor_area), display, G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+ if (spice_display_get_primary(channel, 0, &primary)) {
+ primary_create(channel, primary.format, primary.width, primary.height,
+ primary.stride, primary.shmid, primary.data, display);
+ mark(display, primary.marked);
+ }
+ spice_channel_connect(channel);
+ spice_main_set_display_enabled(d->main, get_display_id(display), TRUE);
+ return;
+ }
+
+ if (SPICE_IS_CURSOR_CHANNEL(channel)) {
+ if (id != d->channel_id)
+ return;
+ d->cursor = SPICE_CURSOR_CHANNEL(channel);
+ spice_g_signal_connect_object(channel, "cursor-set",
+ G_CALLBACK(cursor_set), display, 0);
+ spice_g_signal_connect_object(channel, "cursor-move",
+ G_CALLBACK(cursor_move), display, 0);
+ spice_g_signal_connect_object(channel, "cursor-hide",
+ G_CALLBACK(cursor_hide), display, 0);
+ spice_g_signal_connect_object(channel, "cursor-reset",
+ G_CALLBACK(cursor_reset), display, 0);
+ spice_channel_connect(channel);
+ return;
+ }
+
+ if (SPICE_IS_INPUTS_CHANNEL(channel)) {
+ d->inputs = SPICE_INPUTS_CHANNEL(channel);
+ spice_channel_connect(channel);
+ return;
+ }
+
+#ifdef USE_SMARTCARD
+ if (SPICE_IS_SMARTCARD_CHANNEL(channel)) {
+ d->smartcard = SPICE_SMARTCARD_CHANNEL(channel);
+ spice_channel_connect(channel);
+ return;
+ }
+#endif
+
+ return;
+}
+
+static void channel_destroy(SpiceSession *s, SpiceChannel *channel, gpointer data)
+{
+ SpiceDisplay *display = data;
+ SpiceDisplayPrivate *d = display->priv;
+ int id;
+
+ g_object_get(channel, "channel-id", &id, NULL);
+ SPICE_DEBUG("channel_destroy %d", id);
+
+ if (SPICE_IS_MAIN_CHANNEL(channel)) {
+ d->main = NULL;
+ return;
+ }
+
+ if (SPICE_IS_DISPLAY_CHANNEL(channel)) {
+ if (id != d->channel_id)
+ return;
+ primary_destroy(d->display, display);
+ d->display = NULL;
+ return;
+ }
+
+ if (SPICE_IS_CURSOR_CHANNEL(channel)) {
+ if (id != d->channel_id)
+ return;
+ d->cursor = NULL;
+ return;
+ }
+
+ if (SPICE_IS_INPUTS_CHANNEL(channel)) {
+ d->inputs = NULL;
+ return;
+ }
+
+#ifdef USE_SMARTCARD
+ if (SPICE_IS_SMARTCARD_CHANNEL(channel)) {
+ d->smartcard = NULL;
+ return;
+ }
+#endif
+
+ return;
+}
+
+/**
+ * spice_display_new:
+ * @session: a #SpiceSession
+ * @channel_id: the display channel ID to associate with #SpiceDisplay
+ *
+ * Returns: a new #SpiceDisplay widget.
+ **/
+SpiceDisplay *spice_display_new(SpiceSession *session, int id)
+{
+ return g_object_new(SPICE_TYPE_DISPLAY, "session", session,
+ "channel-id", id, NULL);
+}
+
+/**
+ * spice_display_new_with_monitor:
+ * @session: a #SpiceSession
+ * @channel_id: the display channel ID to associate with #SpiceDisplay
+ * @monitor_id: the monitor id within the display channel
+ *
+ * Since: 0.13
+ * Returns: a new #SpiceDisplay widget.
+ **/
+SpiceDisplay* spice_display_new_with_monitor(SpiceSession *session, gint channel_id, gint monitor_id)
+{
+ return g_object_new(SPICE_TYPE_DISPLAY,
+ "session", session,
+ "channel-id", channel_id,
+ "monitor-id", monitor_id,
+ NULL);
+}
+
+/**
+ * spice_display_mouse_ungrab:
+ * @display:
+ *
+ * Ungrab the mouse.
+ **/
+void spice_display_mouse_ungrab(SpiceDisplay *display)
+{
+ g_return_if_fail(SPICE_IS_DISPLAY(display));
+
+ try_mouse_ungrab(display);
+}
+
+/**
+ * spice_display_copy_to_guest:
+ * @display:
+ *
+ * Copy client-side clipboard to guest clipboard.
+ *
+ * Deprecated: 0.8: Use spice_gtk_session_copy_to_guest() instead
+ **/
+void spice_display_copy_to_guest(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d;
+
+ g_return_if_fail(SPICE_IS_DISPLAY(display));
+
+ d = display->priv;
+
+ g_return_if_fail(d->gtk_session != NULL);
+
+ spice_gtk_session_copy_to_guest(d->gtk_session);
+}
+
+/**
+ * spice_display_paste_from_guest:
+ * @display:
+ *
+ * Copy guest clipboard to client-side clipboard.
+ *
+ * Deprecated: 0.8: Use spice_gtk_session_paste_from_guest() instead
+ **/
+void spice_display_paste_from_guest(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d;
+
+ g_return_if_fail(SPICE_IS_DISPLAY(display));
+
+ d = display->priv;
+
+ g_return_if_fail(d->gtk_session != NULL);
+
+ spice_gtk_session_paste_from_guest(d->gtk_session);
+}
+
+/**
+ * spice_display_get_pixbuf:
+ * @display:
+ *
+ * Take a screenshot of the display.
+ *
+ * Returns: (transfer full): a #GdkPixbuf with the screenshot image buffer
+ **/
+GdkPixbuf *spice_display_get_pixbuf(SpiceDisplay *display)
+{
+ SpiceDisplayPrivate *d;
+ GdkPixbuf *pixbuf;
+ int x, y;
+ guchar *src, *data, *dest;
+
+ g_return_val_if_fail(SPICE_IS_DISPLAY(display), NULL);
+
+ d = display->priv;
+
+ g_return_val_if_fail(d != NULL, NULL);
+ /* TODO: ensure d->data has been exposed? */
+ g_return_val_if_fail(d->data != NULL, NULL);
+
+ data = g_malloc0(d->area.width * d->area.height * 3);
+ src = d->data;
+ dest = data;
+
+ src += d->area.y * d->stride + d->area.x * 4;
+ for (y = 0; y < d->area.height; ++y) {
+ for (x = 0; x < d->area.width; ++x) {
+ dest[0] = src[x * 4 + 2];
+ dest[1] = src[x * 4 + 1];
+ dest[2] = src[x * 4 + 0];
+ dest += 3;
+ }
+ src += d->stride;
+ }
+
+ pixbuf = gdk_pixbuf_new_from_data(data, GDK_COLORSPACE_RGB, false,
+ 8, d->area.width, d->area.height, d->area.width * 3,
+ (GdkPixbufDestroyNotify)g_free, NULL);
+ return pixbuf;
+}
diff --git a/src/spice-widget.h b/src/spice-widget.h
new file mode 100644
index 0000000..d239ed2
--- /dev/null
+++ b/src/spice-widget.h
@@ -0,0 +1,92 @@
+/* -*- 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_CLIENT_WIDGET_H__
+#define __SPICE_CLIENT_WIDGET_H__
+
+#include "spice-client.h"
+
+#include <gtk/gtk.h>
+#include "spice-grabsequence.h"
+#include "spice-widget-enums.h"
+#include "spice-util.h"
+#include "spice-gtk-session.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_DISPLAY (spice_display_get_type())
+#define SPICE_DISPLAY(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_DISPLAY, SpiceDisplay))
+#define SPICE_DISPLAY_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_DISPLAY, SpiceDisplayClass))
+#define SPICE_IS_DISPLAY(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_DISPLAY))
+#define SPICE_IS_DISPLAY_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_DISPLAY))
+#define SPICE_DISPLAY_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_DISPLAY, SpiceDisplayClass))
+
+
+typedef struct _SpiceDisplay SpiceDisplay;
+typedef struct _SpiceDisplayClass SpiceDisplayClass;
+typedef struct _SpiceDisplayPrivate SpiceDisplayPrivate;
+
+struct _SpiceDisplay {
+ GtkDrawingArea parent;
+ SpiceDisplayPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+struct _SpiceDisplayClass {
+ GtkDrawingAreaClass parent_class;
+
+ /* signals */
+ void (*mouse_grab)(SpiceChannel *channel, gint grabbed);
+ void (*keyboard_grab)(SpiceChannel *channel, gint grabbed);
+
+ /*< private >*/
+ /*
+ * If adding fields to this struct, remove corresponding
+ * amount of padding to avoid changing overall struct size
+ */
+ gchar _spice_reserved[SPICE_RESERVED_PADDING];
+};
+
+typedef enum
+{
+ SPICE_DISPLAY_KEY_EVENT_PRESS = 1,
+ SPICE_DISPLAY_KEY_EVENT_RELEASE = 2,
+ SPICE_DISPLAY_KEY_EVENT_CLICK = 3,
+} SpiceDisplayKeyEvent;
+
+GType spice_display_get_type(void);
+
+SpiceDisplay* spice_display_new(SpiceSession *session, int channel_id);
+SpiceDisplay* spice_display_new_with_monitor(SpiceSession *session, gint channel_id, gint monitor_id);
+
+void spice_display_mouse_ungrab(SpiceDisplay *display);
+void spice_display_set_grab_keys(SpiceDisplay *display, SpiceGrabSequence *seq);
+SpiceGrabSequence *spice_display_get_grab_keys(SpiceDisplay *display);
+void spice_display_send_keys(SpiceDisplay *display, const guint *keyvals,
+ int nkeyvals, SpiceDisplayKeyEvent kind);
+GdkPixbuf *spice_display_get_pixbuf(SpiceDisplay *display);
+
+#ifndef SPICE_DISABLE_DEPRECATED
+SPICE_DEPRECATED_FOR(spice_gtk_session_copy_to_guest)
+void spice_display_copy_to_guest(SpiceDisplay *display);
+SPICE_DEPRECATED_FOR(spice_gtk_session_paste_from_guest)
+void spice_display_paste_from_guest(SpiceDisplay *display);
+#endif
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_WIDGET_H__ */
diff --git a/src/spicy-screenshot.c b/src/spicy-screenshot.c
new file mode 100644
index 0000000..e7835bf
--- /dev/null
+++ b/src/spicy-screenshot.c
@@ -0,0 +1,196 @@
+/* -*- 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.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) {
+ fputc(p[2], fp);
+ fputc(p[1], fp);
+ fputc(p[0], fp);
+ 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 %d\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: %d", 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 = N_("Output file name (default spicy-screenshot.ppm)"),
+ .arg_description = N_("<filename>"),
+ },
+ {
+ .long_name = "version",
+ .arg = G_OPTION_ARG_NONE,
+ .arg_data = &version,
+ .description = N_("Display version and quit"),
+ },
+ {
+ /* end of list */
+ }
+};
+
+int main(int argc, char *argv[])
+{
+ GError *error = NULL;
+ GOptionContext *context;
+
+ bindtextdomain(GETTEXT_PACKAGE, SPICE_GTK_LOCALEDIR);
+ bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+ textdomain(GETTEXT_PACKAGE);
+
+ /* 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);
+ }
+
+#if !GLIB_CHECK_VERSION(2,36,0)
+ g_type_init();
+#endif
+ 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/src/spicy-stats.c b/src/spicy-stats.c
new file mode 100644
index 0000000..c98148d
--- /dev/null
+++ b/src/spicy-stats.c
@@ -0,0 +1,144 @@
+/* -*- 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.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: %d", 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 = N_("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);
+
+ bindtextdomain(GETTEXT_PACKAGE, SPICE_GTK_LOCALEDIR);
+ bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+ textdomain(GETTEXT_PACKAGE);
+
+ /* 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);
+ }
+
+#if !GLIB_CHECK_VERSION(2,36,0)
+ g_type_init();
+#endif
+ 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/src/spicy.c b/src/spicy.c
new file mode 100644
index 0000000..9cd6ee5
--- /dev/null
+++ b/src/spicy.c
@@ -0,0 +1,1855 @@
+/* -*- 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/gi18n.h>
+
+#include <sys/stat.h>
+#ifdef HAVE_TERMIOS_H
+#include <termios.h>
+#endif
+
+#ifdef USE_SMARTCARD
+#include <vreader.h>
+#include "smartcard-manager.h"
+#endif
+
+#include "glib-compat.h"
+#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"
+
+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;
+};
+
+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;
+};
+
+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,
+ GTK_STOCK_OK,
+ GTK_RESPONSE_ACCEPT,
+ GTK_STOCK_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 struct {
+ const char *text;
+ const char *prop;
+ GtkWidget *entry;
+} connect_entries[] = {
+ { .text = N_("Hostname"), .prop = "host" },
+ { .text = N_("Port"), .prop = "port" },
+ { .text = N_("TLS Port"), .prop = "tls-port" },
+};
+
+#ifndef G_OS_WIN32
+static void recent_selection_changed_dialog_cb(GtkRecentChooser *chooser, gpointer data)
+{
+ GtkRecentInfo *info;
+ gchar *txt = NULL;
+ const gchar *uri;
+ SpiceSession *session = data;
+
+ 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);
+
+ g_object_get(session, "host", &txt, NULL);
+ gtk_entry_set_text(GTK_ENTRY(connect_entries[0].entry), txt ? txt : "");
+ g_free(txt);
+
+ g_object_get(session, "port", &txt, NULL);
+ gtk_entry_set_text(GTK_ENTRY(connect_entries[1].entry), txt ? txt : "");
+ g_free(txt);
+
+ g_object_get(session, "tls-port", &txt, NULL);
+ gtk_entry_set_text(GTK_ENTRY(connect_entries[2].entry), txt ? txt : "");
+ g_free(txt);
+
+ gtk_recent_info_unref(info);
+}
+
+static void recent_item_activated_dialog_cb(GtkRecentChooser *chooser, gpointer data)
+{
+ gtk_dialog_response (GTK_DIALOG (data), GTK_RESPONSE_ACCEPT);
+}
+#endif
+
+static int connect_dialog(SpiceSession *session)
+{
+ GtkWidget *dialog, *area, *label;
+ GtkTable *table;
+ int i, retval;
+
+ /* Create the widgets */
+ dialog = gtk_dialog_new_with_buttons(_("Connect to SPICE"),
+ NULL,
+ GTK_DIALOG_DESTROY_WITH_PARENT,
+ GTK_STOCK_CANCEL,
+ GTK_RESPONSE_REJECT,
+ GTK_STOCK_CONNECT,
+ GTK_RESPONSE_ACCEPT,
+ NULL);
+ gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
+ area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
+ table = GTK_TABLE(gtk_table_new(3, 2, 0));
+ gtk_box_pack_start(GTK_BOX(area), GTK_WIDGET(table), TRUE, TRUE, 0);
+ gtk_table_set_row_spacings(table, 5);
+ gtk_table_set_col_spacings(table, 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_table_attach_defaults(table, label, 0, 1, i, i+1);
+ connect_entries[i].entry = GTK_WIDGET(gtk_entry_new());
+ gtk_table_attach_defaults(table, connect_entries[i].entry, 1, 2, i, i+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);
+ }
+ }
+
+ label = gtk_label_new("Recent connections:");
+ gtk_box_pack_start(GTK_BOX(area), label, TRUE, TRUE, 0);
+ gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
+#ifndef G_OS_WIN32
+ 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(GTK_BOX(area), 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(recent, "item-activated",
+ G_CALLBACK(recent_item_activated_dialog_cb), dialog);
+#endif
+ /* show and wait for response */
+ gtk_widget_show_all(dialog);
+ if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
+ for (i = 0; i < SPICE_N_ELEMENTS(connect_entries); i++) {
+ const gchar *txt;
+ txt = gtk_entry_get_text(GTK_ENTRY(connect_entries[i].entry));
+ g_object_set(session, connect_entries[i].prop, txt, NULL);
+ }
+ retval = 0;
+ } else
+ retval = -1;
+ 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
+
+#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,
+ GTK_STOCK_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", GTK_STOCK_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 = "HelpMenu",
+ .label = "_Help",
+ },{
+
+ /* File menu */
+ .name = "Connect",
+ .stock_id = GTK_STOCK_CONNECT,
+ .label = N_("_Connect ..."),
+ .callback = G_CALLBACK(menu_cb_connect),
+ },{
+ .name = "Close",
+ .stock_id = GTK_STOCK_CLOSE,
+ .label = N_("_Close"),
+ .callback = G_CALLBACK(menu_cb_close),
+ .accelerator = "", /* none (disable default "<control>W") */
+ },{
+
+ /* Edit menu */
+ .name = "CopyToGuest",
+ .stock_id = GTK_STOCK_COPY,
+ .label = N_("_Copy to guest"),
+ .callback = G_CALLBACK(menu_cb_copy),
+ .accelerator = "", /* none (disable default "<control>C") */
+ },{
+ .name = "PasteFromGuest",
+ .stock_id = GTK_STOCK_PASTE,
+ .label = N_("_Paste from guest"),
+ .callback = G_CALLBACK(menu_cb_paste),
+ .accelerator = "", /* none (disable default "<control>V") */
+ },{
+
+ /* View menu */
+ .name = "Fullscreen",
+ .stock_id = GTK_STOCK_FULLSCREEN,
+ .label = N_("_Fullscreen"),
+ .callback = G_CALLBACK(menu_cb_fullscreen),
+ .accelerator = "<shift>F11",
+ },{
+#ifdef USE_SMARTCARD
+ .name = "InsertSmartcard",
+ .label = N_("_Insert Smartcard"),
+ .callback = G_CALLBACK(menu_cb_insert_smartcard),
+ .accelerator = "<shift>F8",
+ },{
+ .name = "RemoveSmartcard",
+ .label = N_("_Remove Smartcard"),
+ .callback = G_CALLBACK(menu_cb_remove_smartcard),
+ .accelerator = "<shift>F9",
+ },{
+#endif
+
+#ifdef USE_USBREDIR
+ .name = "SelectUsbDevices",
+ .label = N_("_Select USB Devices for redirection"),
+ .callback = G_CALLBACK(menu_cb_select_usb_devices),
+ .accelerator = "<shift>F10",
+ },{
+#endif
+
+ /* Help menu */
+ .name = "About",
+ .stock_id = GTK_STOCK_ABOUT,
+ .label = N_("_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",
+};
+
+static const GtkToggleActionEntry tentries[] = {
+ {
+ .name = "grab-keyboard",
+ .label = N_("Grab keyboard when active and focused"),
+ .callback = G_CALLBACK(menu_cb_bool_prop),
+ },{
+ .name = "grab-mouse",
+ .label = N_("Grab mouse in server mode (no tabled/vdagent)"),
+ .callback = G_CALLBACK(menu_cb_bool_prop),
+ },{
+ .name = "resize-guest",
+ .label = N_("Resize guest to match window size"),
+ .callback = G_CALLBACK(menu_cb_bool_prop),
+ },{
+ .name = "scaling",
+ .label = N_("Scale display"),
+ .callback = G_CALLBACK(menu_cb_bool_prop),
+ },{
+ .name = "disable-inputs",
+ .label = N_("Disable inputs"),
+ .callback = G_CALLBACK(menu_cb_bool_prop),
+ },{
+ .name = "auto-clipboard",
+ .label = N_("Automagic clipboard sharing between host and guest"),
+ .callback = G_CALLBACK(menu_cb_bool_prop),
+ },{
+ .name = "auto-usbredir",
+ .label = N_("Auto redirect newly plugged in USB devices"),
+ .callback = G_CALLBACK(menu_cb_bool_prop),
+ },{
+ .name = "Statusbar",
+ .label = N_("Statusbar"),
+ .callback = G_CALLBACK(menu_cb_statusbar),
+ },{
+ .name = "Toolbar",
+ .label = N_("Toolbar"),
+ .callback = G_CALLBACK(menu_cb_toolbar),
+ }
+};
+
+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='resize-guest'/>\n"
+" <menuitem action='scaling'/>\n"
+" <menuitem action='disable-inputs'/>\n"
+" <menuitem action='auto-clipboard'/>\n"
+" <menuitem action='auto-usbredir'/>\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;
+}
+
+#ifndef G_OS_WIN32
+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);
+}
+#endif
+
+static gboolean configure_event_cb(GtkWidget *widget,
+ GdkEventConfigure *event,
+ gpointer data)
+{
+ gboolean resize_guest;
+ SpiceWindow *win = data;
+
+ g_return_val_if_fail(win != NULL, FALSE);
+ g_return_val_if_fail(win->conn != NULL, FALSE);
+
+ g_object_get(win->spice, "resize-guest", &resize_guest, NULL);
+ if (resize_guest && win->conn->agent_connected)
+ return FALSE;
+
+ return FALSE;
+}
+
+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_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");
+
+#ifndef G_OS_WIN32
+ 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);
+#endif
+
+ /* spice display */
+ win->spice = GTK_WIDGET(spice_display_new_with_monitor(conn->session, id, monitor_id));
+ g_signal_connect(win->spice, "configure-event", G_CALLBACK(configure_event_cb), win);
+ 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 */
+#if GTK_CHECK_VERSION(3,0,0)
+ win->statusbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 1);
+#else
+ win->statusbar = gtk_hbox_new(FALSE, 1);
+#endif
+
+ 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 */
+#if GTK_CHECK_VERSION(3,0,0)
+ vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 1);
+#else
+ vbox = gtk_vbox_new(FALSE, 1);
+#endif
+ 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);
+ }
+
+ rc = connect_dialog(conn->session);
+ if (rc == 0) {
+ 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: %d", 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 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) {
+ 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);
+ }
+}
+
+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);
+ 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);
+ }
+
+ 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);
+ 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 = N_("Open in full screen mode"),
+ },{
+ .long_name = "version",
+ .arg = G_OPTION_ARG_NONE,
+ .arg_data = &version,
+ .description = N_("Display version and quit"),
+ },{
+ .long_name = "title",
+ .arg = G_OPTION_ARG_STRING,
+ .arg_data = &spicy_title,
+ .description = N_("Set the window title"),
+ .arg_description = N_("<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
+ static struct termios saved_tios;
+ struct termios tios;
+
+ if (reset)
+ tios = saved_tios;
+ else {
+ tcgetattr(stdinfd, &tios);
+ saved_tios = tios;
+ 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;
+
+#if !GLIB_CHECK_VERSION(2,31,18)
+ g_thread_init(NULL);
+#endif
+ bindtextdomain(GETTEXT_PACKAGE, SPICE_GTK_LOCALEDIR);
+ bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
+ textdomain(GETTEXT_PACKAGE);
+
+ 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);
+ }
+
+#if !GLIB_CHECK_VERSION(2,36,0)
+ g_type_init();
+#endif
+ 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) {
+ int ret = connect_dialog(conn->session);
+ if (ret != 0) {
+ exit(0);
+ }
+ }
+ g_free(host);
+ g_free(port);
+ g_free(tls_port);
+ g_free(unix_path);
+
+ watch_stdin();
+
+ 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;
+}
diff --git a/src/usb-acl-helper.c b/src/usb-acl-helper.c
new file mode 100644
index 0000000..6a49627
--- /dev/null
+++ b/src/usb-acl-helper.c
@@ -0,0 +1,299 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2011 Red Hat, Inc.
+
+ Red Hat Authors:
+ Hans de Goede <hdegoede@redhat.com>
+
+ 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 <errno.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "usb-acl-helper.h"
+#include "glib-compat.h"
+
+/* ------------------------------------------------------------------ */
+/* gobject glue */
+
+#define SPICE_USB_ACL_HELPER_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE ((obj), SPICE_TYPE_USB_ACL_HELPER, SpiceUsbAclHelperPrivate))
+
+struct _SpiceUsbAclHelperPrivate {
+ GSimpleAsyncResult *result;
+ GIOChannel *in_ch;
+ GIOChannel *out_ch;
+ GCancellable *cancellable;
+ gulong cancellable_id;
+};
+
+G_DEFINE_TYPE(SpiceUsbAclHelper, spice_usb_acl_helper, G_TYPE_OBJECT);
+
+static void spice_usb_acl_helper_init(SpiceUsbAclHelper *self)
+{
+ self->priv = SPICE_USB_ACL_HELPER_GET_PRIVATE(self);
+}
+
+static void spice_usb_acl_helper_cleanup(SpiceUsbAclHelper *self)
+{
+ SpiceUsbAclHelperPrivate *priv = self->priv;
+
+ g_cancellable_disconnect(priv->cancellable, priv->cancellable_id);
+ priv->cancellable = NULL;
+ priv->cancellable_id = 0;
+
+ g_clear_object(&priv->result);
+
+ if (priv->in_ch) {
+ g_io_channel_unref(priv->in_ch);
+ priv->in_ch = NULL;
+ }
+
+ if (priv->out_ch) {
+ g_io_channel_unref(priv->out_ch);
+ priv->out_ch = NULL;
+ }
+}
+
+static void spice_usb_acl_helper_finalize(GObject *gobject)
+{
+ spice_usb_acl_helper_cleanup(SPICE_USB_ACL_HELPER(gobject));
+
+ if (G_OBJECT_CLASS(spice_usb_acl_helper_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_usb_acl_helper_parent_class)->finalize(gobject);
+}
+
+static void spice_usb_acl_helper_class_init(SpiceUsbAclHelperClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->finalize = spice_usb_acl_helper_finalize;
+
+ g_type_class_add_private(klass, sizeof(SpiceUsbAclHelperPrivate));
+}
+
+/* ------------------------------------------------------------------ */
+/* callbacks */
+
+static void async_result_set_cancelled(GSimpleAsyncResult *result)
+{
+ g_simple_async_result_set_error(result,
+ G_IO_ERROR, G_IO_ERROR_CANCELLED,
+ "Setting USB device node ACL cancelled");
+}
+
+static gboolean cb_out_watch(GIOChannel *channel,
+ GIOCondition cond,
+ gpointer *user_data)
+{
+ SpiceUsbAclHelper *self = SPICE_USB_ACL_HELPER(user_data);
+ SpiceUsbAclHelperPrivate *priv = self->priv;
+ gboolean success = FALSE;
+ GError *err = NULL;
+ GIOStatus status;
+ gchar *string;
+ gsize size;
+
+ /* Check that we've not been cancelled */
+ if (priv->result == NULL)
+ goto done;
+
+ g_return_val_if_fail(channel == priv->out_ch, FALSE);
+
+ status = g_io_channel_read_line(priv->out_ch, &string, &size, NULL, &err);
+ switch (status) {
+ case G_IO_STATUS_NORMAL:
+ string[strlen(string) - 1] = 0;
+ if (!strcmp(string, "SUCCESS")) {
+ success = TRUE;
+ } else if (!strcmp(string, "CANCELED")) {
+ async_result_set_cancelled(priv->result);
+ } else {
+ g_simple_async_result_set_error(priv->result,
+ SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "Error setting USB device node ACL: '%s'",
+ string);
+ }
+ g_free(string);
+ break;
+ case G_IO_STATUS_ERROR:
+ g_simple_async_result_take_error(priv->result, err);
+ break;
+ case G_IO_STATUS_EOF:
+ g_simple_async_result_set_error(priv->result,
+ SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "Unexpected EOF reading from acl helper stdout");
+ break;
+ case G_IO_STATUS_AGAIN:
+ return TRUE; /* Wait for more input */
+ }
+
+ g_cancellable_disconnect(priv->cancellable, priv->cancellable_id);
+ priv->cancellable = NULL;
+ priv->cancellable_id = 0;
+
+ g_simple_async_result_complete_in_idle(priv->result);
+ g_clear_object(&priv->result);
+
+ if (!success)
+ spice_usb_acl_helper_cleanup(self);
+
+done:
+ g_object_unref(self);
+ return FALSE;
+}
+
+static void cancelled_cb(GCancellable *cancellable, gpointer user_data)
+{
+ SpiceUsbAclHelper *self = SPICE_USB_ACL_HELPER(user_data);
+
+ spice_usb_acl_helper_close_acl(self);
+}
+
+static void helper_child_watch_cb(GPid pid, gint status, gpointer user_data)
+{
+ /* Nothing to do, but we need the child watch to avoid zombies */
+}
+
+/* ------------------------------------------------------------------ */
+/* private api */
+
+G_GNUC_INTERNAL
+SpiceUsbAclHelper *spice_usb_acl_helper_new(void)
+{
+ GObject *obj;
+
+ obj = g_object_new(SPICE_TYPE_USB_ACL_HELPER, NULL);
+
+ return SPICE_USB_ACL_HELPER(obj);
+}
+
+G_GNUC_INTERNAL
+void spice_usb_acl_helper_open_acl(SpiceUsbAclHelper *self,
+ gint busnum, gint devnum,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_return_if_fail(SPICE_IS_USB_ACL_HELPER(self));
+
+ SpiceUsbAclHelperPrivate *priv = self->priv;
+ GSimpleAsyncResult *result;
+ GError *err = NULL;
+ GIOStatus status;
+ GPid helper_pid;
+ gsize bytes_written;
+ gchar *argv[] = { (char*) ACL_HELPER_PATH"/spice-client-glib-usb-acl-helper", NULL };
+ gint in, out;
+ gchar buf[128];
+
+ result = g_simple_async_result_new(G_OBJECT(self), callback, user_data,
+ spice_usb_acl_helper_open_acl);
+
+ if (priv->out_ch) {
+ g_simple_async_result_set_error(result,
+ SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "Error acl-helper already has an acl open");
+ goto done;
+ }
+
+ if (g_cancellable_set_error_if_cancelled(cancellable, &err)) {
+ g_simple_async_result_take_error(result, err);
+ goto done;
+ }
+
+ if (!g_spawn_async_with_pipes(NULL, argv, NULL,
+ G_SPAWN_DO_NOT_REAP_CHILD | G_SPAWN_SEARCH_PATH,
+ NULL, NULL, &helper_pid, &in, &out, NULL, &err)) {
+ g_simple_async_result_take_error(result, err);
+ goto done;
+ }
+ g_child_watch_add(helper_pid, helper_child_watch_cb, NULL);
+
+ priv->in_ch = g_io_channel_unix_new(in);
+ g_io_channel_set_close_on_unref(priv->in_ch, TRUE);
+
+ priv->out_ch = g_io_channel_unix_new(out);
+ g_io_channel_set_close_on_unref(priv->out_ch, TRUE);
+ status = g_io_channel_set_flags(priv->out_ch, G_IO_FLAG_NONBLOCK, &err);
+ if (status != G_IO_STATUS_NORMAL) {
+ g_simple_async_result_take_error(result, err);
+ goto done;
+ }
+
+ snprintf(buf, sizeof(buf), "%d %d\n", busnum, devnum);
+ status = g_io_channel_write_chars(priv->in_ch, buf, -1,
+ &bytes_written, &err);
+ if (status != G_IO_STATUS_NORMAL) {
+ g_simple_async_result_take_error(result, err);
+ goto done;
+ }
+ status = g_io_channel_flush(priv->in_ch, &err);
+ if (status != G_IO_STATUS_NORMAL) {
+ g_simple_async_result_take_error(result, err);
+ goto done;
+ }
+
+ priv->result = result;
+ if (cancellable) {
+ priv->cancellable = cancellable;
+ priv->cancellable_id = g_cancellable_connect(cancellable,
+ G_CALLBACK(cancelled_cb),
+ self, NULL);
+ }
+ g_io_add_watch(priv->out_ch, G_IO_IN|G_IO_HUP,
+ (GIOFunc)cb_out_watch, g_object_ref(self));
+ return;
+
+done:
+ spice_usb_acl_helper_cleanup(self);
+ g_simple_async_result_complete_in_idle(result);
+ g_object_unref(result);
+}
+
+G_GNUC_INTERNAL
+gboolean spice_usb_acl_helper_open_acl_finish(
+ SpiceUsbAclHelper *self, GAsyncResult *res, GError **err)
+{
+ GSimpleAsyncResult *result = G_SIMPLE_ASYNC_RESULT(res);
+
+ g_return_val_if_fail(g_simple_async_result_is_valid(res, G_OBJECT(self),
+ spice_usb_acl_helper_open_acl),
+ FALSE);
+
+ if (g_simple_async_result_propagate_error(result, err))
+ return FALSE;
+
+ return TRUE;
+}
+
+G_GNUC_INTERNAL
+void spice_usb_acl_helper_close_acl(SpiceUsbAclHelper *self)
+{
+ g_return_if_fail(SPICE_IS_USB_ACL_HELPER(self));
+
+ SpiceUsbAclHelperPrivate *priv = self->priv;
+
+ /* If the acl open has not completed yet report it as cancelled */
+ if (priv->result) {
+ async_result_set_cancelled(priv->result);
+ g_simple_async_result_complete_in_idle(priv->result);
+ }
+
+ spice_usb_acl_helper_cleanup(self);
+}
diff --git a/src/usb-acl-helper.h b/src/usb-acl-helper.h
new file mode 100644
index 0000000..2d41b68
--- /dev/null
+++ b/src/usb-acl-helper.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2011 Red Hat, Inc.
+
+ Red Hat Authors:
+ Hans de Goede <hdegoede@redhat.com>
+
+ 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_USB_ACL_HELPER_H__
+#define __SPICE_USB_ACL_HELPER_H__
+
+#include "spice-client.h"
+#include <gio/gio.h>
+
+/* Note the entire usb-acl-helper class is private to spice-client-glib !! */
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_USB_ACL_HELPER (spice_usb_acl_helper_get_type ())
+#define SPICE_USB_ACL_HELPER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SPICE_TYPE_USB_ACL_HELPER, SpiceUsbAclHelper))
+#define SPICE_USB_ACL_HELPER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SPICE_TYPE_USB_ACL_HELPER, SpiceUsbAclHelperClass))
+#define SPICE_IS_USB_ACL_HELPER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SPICE_TYPE_USB_ACL_HELPER))
+#define SPICE_IS_USB_ACL_HELPER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SPICE_TYPE_USB_ACL_HELPER))
+#define SPICE_USB_ACL_HELPER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SPICE_TYPE_USB_ACL_HELPER, SpiceUsbAclHelperClass))
+
+typedef struct _SpiceUsbAclHelper SpiceUsbAclHelper;
+typedef struct _SpiceUsbAclHelperClass SpiceUsbAclHelperClass;
+typedef struct _SpiceUsbAclHelperPrivate SpiceUsbAclHelperPrivate;
+
+struct _SpiceUsbAclHelper
+{
+ GObject parent;
+
+ /*< private >*/
+ SpiceUsbAclHelperPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+struct _SpiceUsbAclHelperClass
+{
+ GObjectClass parent_class;
+};
+
+GType spice_usb_acl_helper_get_type(void);
+
+SpiceUsbAclHelper *spice_usb_acl_helper_new(void);
+
+void spice_usb_acl_helper_open_acl(SpiceUsbAclHelper *self,
+ gint busnum, gint devnum,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gboolean spice_usb_acl_helper_open_acl_finish(
+ SpiceUsbAclHelper *self, GAsyncResult *res, GError **err);
+
+void spice_usb_acl_helper_close_acl(SpiceUsbAclHelper *self);
+
+G_END_DECLS
+
+#endif /* __SPICE_USB_ACL_HELPER_H__ */
diff --git a/src/usb-device-manager-priv.h b/src/usb-device-manager-priv.h
new file mode 100644
index 0000000..b6fa9c9
--- /dev/null
+++ b/src/usb-device-manager-priv.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2011,2012 Red Hat, Inc.
+
+ Red Hat Authors:
+ Hans de Goede <hdegoede@redhat.com>
+
+ 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_USB_DEVICE_MANAGER_PRIV_H__
+#define __SPICE_USB_DEVICE_MANAGER_PRIV_H__
+
+#include "usb-device-manager.h"
+
+G_BEGIN_DECLS
+
+gboolean spice_usb_device_manager_start_event_listening(
+ SpiceUsbDeviceManager *manager, GError **err);
+
+void spice_usb_device_manager_stop_event_listening(
+ SpiceUsbDeviceManager *manager);
+
+#ifdef USE_USBREDIR
+#include <libusb.h>
+void spice_usb_device_manager_device_error(
+ SpiceUsbDeviceManager *manager, SpiceUsbDevice *device, GError *err);
+
+guint8 spice_usb_device_get_busnum(const SpiceUsbDevice *device);
+guint8 spice_usb_device_get_devaddr(const SpiceUsbDevice *device);
+guint16 spice_usb_device_get_vid(const SpiceUsbDevice *device);
+guint16 spice_usb_device_get_pid(const SpiceUsbDevice *device);
+
+#endif
+
+G_END_DECLS
+
+#endif /* __SPICE_USB_DEVICE_MANAGER_PRIV_H__ */
diff --git a/src/usb-device-manager.c b/src/usb-device-manager.c
new file mode 100644
index 0000000..12ad4ba
--- /dev/null
+++ b/src/usb-device-manager.c
@@ -0,0 +1,1907 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2011, 2012 Red Hat, Inc.
+
+ Red Hat Authors:
+ Hans de Goede <hdegoede@redhat.com>
+
+ 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-object.h>
+
+#include "glib-compat.h"
+
+#ifdef USE_USBREDIR
+#include <errno.h>
+#include <libusb.h>
+
+#if defined(USE_GUDEV)
+#include <gudev/gudev.h>
+#elif defined(G_OS_WIN32)
+#include "win-usb-dev.h"
+#include "win-usb-driver-install.h"
+#define USE_GUDEV /* win-usb-dev.h provides a fake gudev interface */
+#elif !defined USE_LIBUSB_HOTPLUG
+#error "Expecting one of USE_GUDEV or USE_LIBUSB_HOTPLUG to be defined"
+#endif
+
+#include "channel-usbredir-priv.h"
+#include "usbredirhost.h"
+#include "usbutil.h"
+#endif
+
+#include "spice-session-priv.h"
+#include "spice-client.h"
+#include "spice-marshal.h"
+#include "usb-device-manager-priv.h"
+
+#include <glib/gi18n.h>
+
+#ifndef G_OS_WIN32 /* Linux -- device id is bus.addr */
+#define DEV_ID_FMT "at %d.%d"
+#else /* Windows -- device id is vid:pid */
+#define DEV_ID_FMT "0x%04x:0x%04x"
+#endif
+
+/**
+ * SECTION:usb-device-manager
+ * @short_description: USB device management
+ * @title: Spice USB Manager
+ * @section_id:
+ * @see_also:
+ * @stability: Stable
+ * @include: usb-device-manager.h
+ *
+ * #SpiceUsbDeviceManager monitors USB redirection channels and USB
+ * devices plugging/unplugging. If #SpiceUsbDeviceManager:auto-connect
+ * is set to %TRUE, it will automatically connect newly plugged USB
+ * devices to available channels.
+ *
+ * There should always be a 1:1 relation between #SpiceUsbDeviceManager objects
+ * and #SpiceSession objects. Therefor there is no
+ * spice_usb_device_manager_new, instead there is
+ * spice_usb_device_manager_get() which ensures this 1:1 relation.
+ */
+
+/* ------------------------------------------------------------------ */
+/* gobject glue */
+
+#define SPICE_USB_DEVICE_MANAGER_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE ((obj), SPICE_TYPE_USB_DEVICE_MANAGER, SpiceUsbDeviceManagerPrivate))
+
+enum {
+ PROP_0,
+ PROP_SESSION,
+ PROP_AUTO_CONNECT,
+ PROP_AUTO_CONNECT_FILTER,
+ PROP_REDIRECT_ON_CONNECT,
+};
+
+enum
+{
+ DEVICE_ADDED,
+ DEVICE_REMOVED,
+ AUTO_CONNECT_FAILED,
+ DEVICE_ERROR,
+ LAST_SIGNAL,
+};
+
+struct _SpiceUsbDeviceManagerPrivate {
+ SpiceSession *session;
+ gboolean auto_connect;
+ gchar *auto_connect_filter;
+ gchar *redirect_on_connect;
+#ifdef USE_USBREDIR
+ libusb_context *context;
+ int event_listeners;
+ GThread *event_thread;
+ gboolean event_thread_run;
+ struct usbredirfilter_rule *auto_conn_filter_rules;
+ struct usbredirfilter_rule *redirect_on_connect_rules;
+ int auto_conn_filter_rules_count;
+ int redirect_on_connect_rules_count;
+#ifdef USE_GUDEV
+ GUdevClient *udev;
+ libusb_device **coldplug_list; /* Avoid needless reprobing during init */
+#else
+ libusb_hotplug_callback_handle hp_handle;
+#endif
+#ifdef G_OS_WIN32
+ SpiceWinUsbDriver *installer;
+#endif
+#endif
+ GPtrArray *devices;
+ GPtrArray *channels;
+};
+
+enum {
+ SPICE_USB_DEVICE_STATE_NONE = 0, /* this is also DISCONNECTED */
+ SPICE_USB_DEVICE_STATE_CONNECTING,
+ SPICE_USB_DEVICE_STATE_CONNECTED,
+ SPICE_USB_DEVICE_STATE_DISCONNECTING,
+ SPICE_USB_DEVICE_STATE_INSTALLING,
+ SPICE_USB_DEVICE_STATE_UNINSTALLING,
+ SPICE_USB_DEVICE_STATE_INSTALLED,
+ SPICE_USB_DEVICE_STATE_MAX
+};
+
+#ifdef USE_USBREDIR
+
+typedef struct _SpiceUsbDeviceInfo {
+ guint8 busnum;
+ guint8 devaddr;
+ guint16 vid;
+ guint16 pid;
+#ifdef G_OS_WIN32
+ guint8 state;
+#else
+ libusb_device *libdev;
+#endif
+ gint ref;
+} SpiceUsbDeviceInfo;
+
+
+static void channel_new(SpiceSession *session, SpiceChannel *channel,
+ gpointer user_data);
+static void channel_destroy(SpiceSession *session, SpiceChannel *channel,
+ gpointer user_data);
+#ifdef USE_GUDEV
+static void spice_usb_device_manager_uevent_cb(GUdevClient *client,
+ const gchar *action,
+ GUdevDevice *udevice,
+ gpointer user_data);
+static void spice_usb_device_manager_add_udev(SpiceUsbDeviceManager *self,
+ GUdevDevice *udev);
+#else
+static int spice_usb_device_manager_hotplug_cb(libusb_context *ctx,
+ libusb_device *device,
+ libusb_hotplug_event event,
+ void *data);
+#endif
+static void spice_usb_device_manager_check_redir_on_connect(
+ SpiceUsbDeviceManager *self, SpiceChannel *channel);
+
+static SpiceUsbDeviceInfo *spice_usb_device_new(libusb_device *libdev);
+static SpiceUsbDevice *spice_usb_device_ref(SpiceUsbDevice *device);
+static void spice_usb_device_unref(SpiceUsbDevice *device);
+
+#ifdef G_OS_WIN32
+static guint8 spice_usb_device_get_state(SpiceUsbDevice *device);
+static void spice_usb_device_set_state(SpiceUsbDevice *device, guint8 s);
+#endif
+
+static gboolean spice_usb_device_equal_libdev(SpiceUsbDevice *device,
+ libusb_device *libdev);
+static libusb_device *
+spice_usb_device_manager_device_to_libdev(SpiceUsbDeviceManager *self,
+ SpiceUsbDevice *device);
+
+static void
+_spice_usb_device_manager_connect_device_async(SpiceUsbDeviceManager *self,
+ SpiceUsbDevice *device,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+G_DEFINE_BOXED_TYPE(SpiceUsbDevice, spice_usb_device,
+ (GBoxedCopyFunc)spice_usb_device_ref,
+ (GBoxedFreeFunc)spice_usb_device_unref)
+
+#else
+G_DEFINE_BOXED_TYPE(SpiceUsbDevice, spice_usb_device, g_object_ref, g_object_unref)
+#endif
+
+static void spice_usb_device_manager_initable_iface_init(GInitableIface *iface);
+
+static guint signals[LAST_SIGNAL] = { 0, };
+
+G_DEFINE_TYPE_WITH_CODE(SpiceUsbDeviceManager, spice_usb_device_manager, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, spice_usb_device_manager_initable_iface_init));
+
+static void spice_usb_device_manager_init(SpiceUsbDeviceManager *self)
+{
+ SpiceUsbDeviceManagerPrivate *priv;
+
+ priv = SPICE_USB_DEVICE_MANAGER_GET_PRIVATE(self);
+ self->priv = priv;
+
+ priv->channels = g_ptr_array_new();
+#ifdef USE_USBREDIR
+ priv->devices = g_ptr_array_new_with_free_func((GDestroyNotify)
+ spice_usb_device_unref);
+#endif
+}
+
+static gboolean spice_usb_device_manager_initable_init(GInitable *initable,
+ GCancellable *cancellable,
+ GError **err)
+{
+#ifdef USE_USBREDIR
+ SpiceUsbDeviceManager *self = SPICE_USB_DEVICE_MANAGER(initable);
+ SpiceUsbDeviceManagerPrivate *priv = self->priv;
+ GList *list;
+ GList *it;
+ int rc;
+#ifdef USE_GUDEV
+ const gchar *const subsystems[] = {"usb", NULL};
+#endif
+
+#ifdef G_OS_WIN32
+ priv->installer = spice_win_usb_driver_new(err);
+ if (!priv->installer) {
+ SPICE_DEBUG("failed to initialize winusb driver");
+ return FALSE;
+ }
+#endif
+
+ /* Initialize libusb */
+ rc = libusb_init(&priv->context);
+ if (rc < 0) {
+ const char *desc = spice_usbutil_libusb_strerror(rc);
+ g_warning("Error initializing USB support: %s [%i]", desc, rc);
+ g_set_error(err, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "Error initializing USB support: %s [%i]", desc, rc);
+ return FALSE;
+ }
+
+ /* Start listening for usb devices plug / unplug */
+#ifdef USE_GUDEV
+ priv->udev = g_udev_client_new(subsystems);
+ g_signal_connect(G_OBJECT(priv->udev), "uevent",
+ G_CALLBACK(spice_usb_device_manager_uevent_cb), self);
+ /* Do coldplug (detection of already connected devices) */
+ libusb_get_device_list(priv->context, &priv->coldplug_list);
+ list = g_udev_client_query_by_subsystem(priv->udev, "usb");
+ for (it = g_list_first(list); it; it = g_list_next(it)) {
+ spice_usb_device_manager_add_udev(self, it->data);
+ g_object_unref(it->data);
+ }
+ g_list_free(list);
+ libusb_free_device_list(priv->coldplug_list, 1);
+ priv->coldplug_list = NULL;
+#else
+ rc = libusb_hotplug_register_callback(priv->context,
+ LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT,
+ LIBUSB_HOTPLUG_ENUMERATE, LIBUSB_HOTPLUG_MATCH_ANY,
+ LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY,
+ spice_usb_device_manager_hotplug_cb, self, &priv->hp_handle);
+ if (rc < 0) {
+ const char *desc = spice_usbutil_libusb_strerror(rc);
+ g_warning("Error initializing USB hotplug support: %s [%i]", desc, rc);
+ g_set_error(err, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "Error initializing USB hotplug support: %s [%i]", desc, rc);
+ return FALSE;
+ }
+ spice_usb_device_manager_start_event_listening(self, NULL);
+#endif
+
+ /* Start listening for usb channels connect/disconnect */
+ spice_g_signal_connect_object(priv->session, "channel-new", G_CALLBACK(channel_new), self, G_CONNECT_AFTER);
+ g_signal_connect(priv->session, "channel-destroy",
+ G_CALLBACK(channel_destroy), self);
+ list = spice_session_get_channels(priv->session);
+ for (it = g_list_first(list); it != NULL; it = g_list_next(it)) {
+ channel_new(priv->session, it->data, (gpointer*)self);
+ }
+ g_list_free(list);
+
+ return TRUE;
+#else
+ g_set_error_literal(err, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ _("USB redirection support not compiled in"));
+ return FALSE;
+#endif
+}
+
+static void spice_usb_device_manager_dispose(GObject *gobject)
+{
+#ifdef USE_USBREDIR
+ SpiceUsbDeviceManager *self = SPICE_USB_DEVICE_MANAGER(gobject);
+ SpiceUsbDeviceManagerPrivate *priv = self->priv;
+
+#ifdef USE_LIBUSB_HOTPLUG
+ if (priv->hp_handle) {
+ spice_usb_device_manager_stop_event_listening(self);
+ /* This also wakes up the libusb_handle_events() in the event_thread */
+ libusb_hotplug_deregister_callback(priv->context, priv->hp_handle);
+ priv->hp_handle = 0;
+ }
+#endif
+ if (priv->event_thread && !priv->event_thread_run) {
+ g_thread_join(priv->event_thread);
+ priv->event_thread = NULL;
+ }
+#endif
+
+ /* Chain up to the parent class */
+ if (G_OBJECT_CLASS(spice_usb_device_manager_parent_class)->dispose)
+ G_OBJECT_CLASS(spice_usb_device_manager_parent_class)->dispose(gobject);
+}
+
+static void spice_usb_device_manager_finalize(GObject *gobject)
+{
+ SpiceUsbDeviceManager *self = SPICE_USB_DEVICE_MANAGER(gobject);
+ SpiceUsbDeviceManagerPrivate *priv = self->priv;
+
+ g_ptr_array_unref(priv->channels);
+ if (priv->devices)
+ g_ptr_array_unref(priv->devices);
+
+#ifdef USE_USBREDIR
+#ifdef USE_GUDEV
+ g_clear_object(&priv->udev);
+#endif
+ g_return_if_fail(priv->event_thread == NULL);
+ if (priv->context)
+ libusb_exit(priv->context);
+ free(priv->auto_conn_filter_rules);
+ free(priv->redirect_on_connect_rules);
+#ifdef G_OS_WIN32
+ if (priv->installer)
+ g_object_unref(priv->installer);
+#endif
+#endif
+
+ g_free(priv->auto_connect_filter);
+ g_free(priv->redirect_on_connect);
+
+ /* Chain up to the parent class */
+ if (G_OBJECT_CLASS(spice_usb_device_manager_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_usb_device_manager_parent_class)->finalize(gobject);
+}
+
+static void spice_usb_device_manager_initable_iface_init(GInitableIface *iface)
+{
+ iface->init = spice_usb_device_manager_initable_init;
+}
+
+static void spice_usb_device_manager_get_property(GObject *gobject,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceUsbDeviceManager *self = SPICE_USB_DEVICE_MANAGER(gobject);
+ SpiceUsbDeviceManagerPrivate *priv = self->priv;
+
+ switch (prop_id) {
+ case PROP_SESSION:
+ g_value_set_object(value, priv->session);
+ break;
+ case PROP_AUTO_CONNECT:
+ g_value_set_boolean(value, priv->auto_connect);
+ break;
+ case PROP_AUTO_CONNECT_FILTER:
+ g_value_set_string(value, priv->auto_connect_filter);
+ break;
+ case PROP_REDIRECT_ON_CONNECT:
+ g_value_set_string(value, priv->redirect_on_connect);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_usb_device_manager_set_property(GObject *gobject,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceUsbDeviceManager *self = SPICE_USB_DEVICE_MANAGER(gobject);
+ SpiceUsbDeviceManagerPrivate *priv = self->priv;
+
+ switch (prop_id) {
+ case PROP_SESSION:
+ priv->session = g_value_get_object(value);
+ break;
+ case PROP_AUTO_CONNECT:
+ priv->auto_connect = g_value_get_boolean(value);
+ break;
+ case PROP_AUTO_CONNECT_FILTER: {
+ const gchar *filter = g_value_get_string(value);
+#ifdef USE_USBREDIR
+ struct usbredirfilter_rule *rules;
+ int r, count;
+
+ r = usbredirfilter_string_to_rules(filter, ",", "|", &rules, &count);
+ if (r) {
+ if (r == -ENOMEM)
+ g_error("Failed to allocate memory for auto-connect-filter");
+ g_warning("Error parsing auto-connect-filter string, keeping old filter");
+ break;
+ }
+
+ free(priv->auto_conn_filter_rules);
+ priv->auto_conn_filter_rules = rules;
+ priv->auto_conn_filter_rules_count = count;
+#endif
+ g_free(priv->auto_connect_filter);
+ priv->auto_connect_filter = g_strdup(filter);
+ break;
+ }
+ case PROP_REDIRECT_ON_CONNECT: {
+ const gchar *filter = g_value_get_string(value);
+#ifdef USE_USBREDIR
+ struct usbredirfilter_rule *rules = NULL;
+ int r = 0, count = 0;
+
+ if (filter)
+ r = usbredirfilter_string_to_rules(filter, ",", "|",
+ &rules, &count);
+ if (r) {
+ if (r == -ENOMEM)
+ g_error("Failed to allocate memory for redirect-on-connect");
+ g_warning("Error parsing redirect-on-connect string, keeping old filter");
+ break;
+ }
+
+ free(priv->redirect_on_connect_rules);
+ priv->redirect_on_connect_rules = rules;
+ priv->redirect_on_connect_rules_count = count;
+#endif
+ g_free(priv->redirect_on_connect);
+ priv->redirect_on_connect = g_strdup(filter);
+ break;
+ }
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_usb_device_manager_class_init(SpiceUsbDeviceManagerClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+ GParamSpec *pspec;
+
+ gobject_class->dispose = spice_usb_device_manager_dispose;
+ gobject_class->finalize = spice_usb_device_manager_finalize;
+ gobject_class->get_property = spice_usb_device_manager_get_property;
+ gobject_class->set_property = spice_usb_device_manager_set_property;
+
+ /**
+ * SpiceUsbDeviceManager:session:
+ *
+ * #SpiceSession this #SpiceUsbDeviceManager is associated with
+ *
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_SESSION,
+ g_param_spec_object("session",
+ "Session",
+ "SpiceSession",
+ SPICE_TYPE_SESSION,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpiceUsbDeviceManager:auto-connect:
+ *
+ * Set this to TRUE to automatically redirect newly plugged in device.
+ *
+ * Note when #SpiceGtkSession's auto-usbredir property is TRUE, this
+ * property is controlled by #SpiceGtkSession.
+ */
+ pspec = g_param_spec_boolean("auto-connect", "Auto Connect",
+ "Auto connect plugged in USB devices",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ g_object_class_install_property(gobject_class, PROP_AUTO_CONNECT, pspec);
+
+ /**
+ * SpiceUsbDeviceManager:auto-connect-filter:
+ *
+ * Set a string specifying a filter to use to determine which USB devices
+ * to autoconnect when plugged in, a filter consists of one or more rules.
+ * Where each rule has the form of:
+ *
+ * @class,@vendor,@product,@version,@allow
+ *
+ * Use -1 for @class/@vendor/@product/@version to accept any value.
+ *
+ * And the rules themselves are concatenated like this:
+ *
+ * @rule1|@rule2|@rule3
+ *
+ * The default setting filters out HID (class 0x03) USB devices from auto
+ * connect and auto connects anything else. Note the explicit allow rule at
+ * the end, this is necessary since by default all devices without a
+ * matching filter rule will not auto-connect.
+ *
+ * Filter strings in this format can be easily created with the RHEV-M
+ * USB filter editor tool.
+ */
+ pspec = g_param_spec_string("auto-connect-filter", "Auto Connect Filter ",
+ "Filter determining which USB devices to auto connect",
+ "0x03,-1,-1,-1,0|-1,-1,-1,-1,1",
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);
+ g_object_class_install_property(gobject_class, PROP_AUTO_CONNECT_FILTER,
+ pspec);
+
+ /**
+ * SpiceUsbDeviceManager:redirect-on-connect:
+ *
+ * Set a string specifying a filter selecting USB devices to automatically
+ * redirect after a Spice connection has been established.
+ *
+ * See #SpiceUsbDeviceManager:auto-connect-filter for the filter string
+ * format.
+ */
+ pspec = g_param_spec_string("redirect-on-connect", "Redirect on connect",
+ "Filter selecting USB devices to redirect on connect", NULL,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+ g_object_class_install_property(gobject_class, PROP_REDIRECT_ON_CONNECT,
+ pspec);
+
+ /**
+ * SpiceUsbDeviceManager::device-added:
+ * @manager: the #SpiceUsbDeviceManager that emitted the signal
+ * @device: #SpiceUsbDevice boxed object corresponding to the added device
+ *
+ * The #SpiceUsbDeviceManager::device-added signal is emitted whenever
+ * a new USB device has been plugged in.
+ **/
+ signals[DEVICE_ADDED] =
+ g_signal_new("device-added",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceUsbDeviceManagerClass, device_added),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__BOXED,
+ G_TYPE_NONE,
+ 1,
+ SPICE_TYPE_USB_DEVICE);
+
+ /**
+ * SpiceUsbDeviceManager::device-removed:
+ * @manager: the #SpiceUsbDeviceManager that emitted the signal
+ * @device: #SpiceUsbDevice boxed object corresponding to the removed device
+ *
+ * The #SpiceUsbDeviceManager::device-removed signal is emitted whenever
+ * an USB device has been removed.
+ **/
+ signals[DEVICE_REMOVED] =
+ g_signal_new("device-removed",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceUsbDeviceManagerClass, device_removed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__BOXED,
+ G_TYPE_NONE,
+ 1,
+ SPICE_TYPE_USB_DEVICE);
+
+ /**
+ * SpiceUsbDeviceManager::auto-connect-failed:
+ * @manager: the #SpiceUsbDeviceManager that emitted the signal
+ * @device: #SpiceUsbDevice boxed object corresponding to the device which failed to auto connect
+ * @error: #GError describing the reason why the autoconnect failed
+ *
+ * The #SpiceUsbDeviceManager::auto-connect-failed signal is emitted
+ * whenever the auto-connect property is true, and a newly plugged in
+ * device could not be auto-connected.
+ **/
+ signals[AUTO_CONNECT_FAILED] =
+ g_signal_new("auto-connect-failed",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceUsbDeviceManagerClass, auto_connect_failed),
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__BOXED_BOXED,
+ G_TYPE_NONE,
+ 2,
+ SPICE_TYPE_USB_DEVICE,
+ G_TYPE_ERROR);
+
+ /**
+ * SpiceUsbDeviceManager::device-error:
+ * @manager: #SpiceUsbDeviceManager that emitted the signal
+ * @device: #SpiceUsbDevice boxed object corresponding to the device which has an error
+ * @error: #GError describing the error
+ *
+ * The #SpiceUsbDeviceManager::device-error signal is emitted whenever an
+ * error happens which causes a device to no longer be available to the
+ * guest.
+ **/
+ signals[DEVICE_ERROR] =
+ g_signal_new("device-error",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceUsbDeviceManagerClass, device_error),
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__BOXED_BOXED,
+ G_TYPE_NONE,
+ 2,
+ SPICE_TYPE_USB_DEVICE,
+ G_TYPE_ERROR);
+
+ g_type_class_add_private(klass, sizeof(SpiceUsbDeviceManagerPrivate));
+}
+
+#ifdef USE_USBREDIR
+
+/* ------------------------------------------------------------------ */
+/* gudev / libusb Helper functions */
+
+#ifdef USE_GUDEV
+static gboolean spice_usb_device_manager_get_udev_bus_n_address(
+ GUdevDevice *udev, int *bus, int *address)
+{
+ const gchar *bus_str, *address_str;
+
+ *bus = *address = 0;
+
+#ifndef G_OS_WIN32
+ bus_str = g_udev_device_get_property(udev, "BUSNUM");
+ address_str = g_udev_device_get_property(udev, "DEVNUM");
+#else /* Windows -- request vid:pid instead */
+ bus_str = g_udev_device_get_property(udev, "VID");
+ address_str = g_udev_device_get_property(udev, "PID");
+#endif
+ if (bus_str)
+ *bus = atoi(bus_str);
+ if (address_str)
+ *address = atoi(address_str);
+
+ return *bus && *address;
+}
+#endif
+
+static gboolean spice_usb_device_manager_get_device_descriptor(
+ libusb_device *libdev,
+ struct libusb_device_descriptor *desc)
+{
+ int errcode;
+ const gchar *errstr;
+
+ g_return_val_if_fail(libdev != NULL, FALSE);
+ g_return_val_if_fail(desc != NULL, FALSE);
+
+ errcode = libusb_get_device_descriptor(libdev, desc);
+ if (errcode < 0) {
+ int bus, addr;
+
+ bus = libusb_get_bus_number(libdev);
+ addr = libusb_get_device_address(libdev);
+ errstr = spice_usbutil_libusb_strerror(errcode);
+ g_warning("cannot get device descriptor for (%p) %d.%d -- %s(%d)",
+ libdev, bus, addr, errstr, errcode);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+
+/**
+ * spice_usb_device_get_libusb_device:
+ * @device: #SpiceUsbDevice to get the descriptor information of
+ *
+ * Returns: (transfer none): the %libusb_device associated to %SpiceUsbDevice.
+ *
+ * Since: 0.27
+ **/
+gconstpointer
+spice_usb_device_get_libusb_device(const SpiceUsbDevice *device G_GNUC_UNUSED)
+{
+#ifdef USE_USBREDIR
+#ifndef G_OS_WIN32
+ const SpiceUsbDeviceInfo *info = (const SpiceUsbDeviceInfo *)device;
+
+ g_return_val_if_fail(info != NULL, FALSE);
+
+ return info->libdev;
+#endif
+#endif
+ return NULL;
+}
+
+static gboolean spice_usb_device_manager_get_libdev_vid_pid(
+ libusb_device *libdev, int *vid, int *pid)
+{
+ struct libusb_device_descriptor desc;
+
+ g_return_val_if_fail(libdev != NULL, FALSE);
+ g_return_val_if_fail(vid != NULL, FALSE);
+ g_return_val_if_fail(pid != NULL, FALSE);
+
+ *vid = *pid = 0;
+
+ if (!spice_usb_device_manager_get_device_descriptor(libdev, &desc)) {
+ return FALSE;
+ }
+ *vid = desc.idVendor;
+ *pid = desc.idProduct;
+
+ return TRUE;
+}
+
+/* ------------------------------------------------------------------ */
+/* callbacks */
+
+static void channel_new(SpiceSession *session, SpiceChannel *channel,
+ gpointer user_data)
+{
+ SpiceUsbDeviceManager *self = user_data;
+
+ if (!SPICE_IS_USBREDIR_CHANNEL(channel))
+ return;
+
+ spice_usbredir_channel_set_context(SPICE_USBREDIR_CHANNEL(channel),
+ self->priv->context);
+ spice_channel_connect(channel);
+ g_ptr_array_add(self->priv->channels, channel);
+
+ spice_usb_device_manager_check_redir_on_connect(self, channel);
+
+ /*
+ * add a reference to ourself, to make sure the libusb context is
+ * alive as long as the channel is.
+ * TODO: moving to gusb could help here too.
+ */
+ g_object_ref(self);
+ g_object_weak_ref(G_OBJECT(channel), (GWeakNotify)g_object_unref, self);
+}
+
+static void channel_destroy(SpiceSession *session, SpiceChannel *channel,
+ gpointer user_data)
+{
+ SpiceUsbDeviceManager *self = user_data;
+
+ if (!SPICE_IS_USBREDIR_CHANNEL(channel))
+ return;
+
+ g_ptr_array_remove(self->priv->channels, channel);
+}
+
+static void spice_usb_device_manager_auto_connect_cb(GObject *gobject,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SpiceUsbDeviceManager *self = SPICE_USB_DEVICE_MANAGER(gobject);
+ SpiceUsbDevice *device = user_data;
+ GError *err = NULL;
+
+ spice_usb_device_manager_connect_device_finish(self, res, &err);
+ if (err) {
+ gchar *desc = spice_usb_device_get_description(device, NULL);
+ g_prefix_error(&err, "Could not auto-redirect %s: ", desc);
+ g_free(desc);
+
+ SPICE_DEBUG("%s", err->message);
+ g_signal_emit(self, signals[AUTO_CONNECT_FAILED], 0, device, err);
+ g_error_free(err);
+ }
+ spice_usb_device_unref(device);
+}
+
+#ifndef G_OS_WIN32 /* match functions for Linux -- match by bus.addr */
+static gboolean
+spice_usb_device_manager_device_match(SpiceUsbDevice *device,
+ const int bus, const int address)
+{
+ return (spice_usb_device_get_busnum(device) == bus &&
+ spice_usb_device_get_devaddr(device) == address);
+}
+
+#ifdef USE_GUDEV
+static gboolean
+spice_usb_device_manager_libdev_match(libusb_device *libdev,
+ const int bus, const int address)
+{
+ return (libusb_get_bus_number(libdev) == bus &&
+ libusb_get_device_address(libdev) == address);
+}
+#endif
+
+#else /* Win32 -- match functions for Windows -- match by vid:pid */
+static gboolean
+spice_usb_device_manager_device_match(SpiceUsbDevice *device,
+ const int vid, const int pid)
+{
+ return (spice_usb_device_get_vid(device) == vid &&
+ spice_usb_device_get_pid(device) == pid);
+}
+
+static gboolean
+spice_usb_device_manager_libdev_match(libusb_device *libdev,
+ const int vid, const int pid)
+{
+ int vid2, pid2;
+
+ if (!spice_usb_device_manager_get_libdev_vid_pid(libdev, &vid2, &pid2)) {
+ return FALSE;
+ }
+ return (vid == vid2 && pid == pid2);
+}
+#endif /* of Win32 -- match functions */
+
+static SpiceUsbDevice*
+spice_usb_device_manager_find_device(SpiceUsbDeviceManager *self,
+ const int bus, const int address)
+{
+ SpiceUsbDeviceManagerPrivate *priv = self->priv;
+ SpiceUsbDevice *curr, *device = NULL;
+ guint i;
+
+ for (i = 0; i < priv->devices->len; i++) {
+ curr = g_ptr_array_index(priv->devices, i);
+ if (spice_usb_device_manager_device_match(curr, bus, address)) {
+ device = curr;
+ break;
+ }
+ }
+ return device;
+}
+
+static void spice_usb_device_manager_add_dev(SpiceUsbDeviceManager *self,
+ libusb_device *libdev)
+{
+ SpiceUsbDeviceManagerPrivate *priv = self->priv;
+ struct libusb_device_descriptor desc;
+ SpiceUsbDevice *device;
+
+ if (!spice_usb_device_manager_get_device_descriptor(libdev, &desc))
+ return;
+
+ /* Skip hubs */
+ if (desc.bDeviceClass == LIBUSB_CLASS_HUB)
+ return;
+
+ device = (SpiceUsbDevice*)spice_usb_device_new(libdev);
+ if (!device)
+ return;
+
+ g_ptr_array_add(priv->devices, device);
+
+ if (priv->auto_connect) {
+ gboolean can_redirect, auto_ok;
+
+ can_redirect = spice_usb_device_manager_can_redirect_device(
+ self, device, NULL);
+
+ auto_ok = usbredirhost_check_device_filter(
+ priv->auto_conn_filter_rules,
+ priv->auto_conn_filter_rules_count,
+ libdev, 0) == 0;
+
+ if (can_redirect && auto_ok)
+ spice_usb_device_manager_connect_device_async(self,
+ device, NULL,
+ spice_usb_device_manager_auto_connect_cb,
+ spice_usb_device_ref(device));
+ }
+
+ SPICE_DEBUG("device added %p", device);
+ g_signal_emit(self, signals[DEVICE_ADDED], 0, device);
+}
+
+static void spice_usb_device_manager_remove_dev(SpiceUsbDeviceManager *self,
+ int bus, int address)
+{
+ SpiceUsbDeviceManagerPrivate *priv = self->priv;
+ SpiceUsbDevice *device;
+
+ device = spice_usb_device_manager_find_device(self, bus, address);
+ if (!device) {
+ g_warning("Could not find USB device to remove " DEV_ID_FMT,
+ bus, address);
+ return;
+ }
+
+#ifdef G_OS_WIN32
+ const guint8 state = spice_usb_device_get_state(device);
+ if ((state == SPICE_USB_DEVICE_STATE_INSTALLING) ||
+ (state == SPICE_USB_DEVICE_STATE_UNINSTALLING)) {
+ SPICE_DEBUG("skipping " DEV_ID_FMT ". It is un/installing its driver",
+ bus, address);
+ return;
+ }
+#endif
+
+ spice_usb_device_manager_disconnect_device(self, device);
+
+ SPICE_DEBUG("device removed %p", device);
+ spice_usb_device_ref(device);
+ g_ptr_array_remove(priv->devices, device);
+ g_signal_emit(self, signals[DEVICE_REMOVED], 0, device);
+ spice_usb_device_unref(device);
+}
+
+#ifdef USE_GUDEV
+static void spice_usb_device_manager_add_udev(SpiceUsbDeviceManager *self,
+ GUdevDevice *udev)
+{
+ SpiceUsbDeviceManagerPrivate *priv = self->priv;
+ libusb_device *libdev = NULL, **dev_list = NULL;
+ SpiceUsbDevice *device;
+ const gchar *devtype;
+ int i, bus, address;
+
+ devtype = g_udev_device_get_property(udev, "DEVTYPE");
+ /* Check if this is a usb device (and not an interface) */
+ if (!devtype || strcmp(devtype, "usb_device"))
+ return;
+
+ if (!spice_usb_device_manager_get_udev_bus_n_address(udev, &bus, &address)) {
+ g_warning("USB device without bus number or device address");
+ return;
+ }
+
+ device = spice_usb_device_manager_find_device(self, bus, address);
+ if (device) {
+ SPICE_DEBUG("USB device 0x%04x:0x%04x at %d.%d already exists, ignored",
+ spice_usb_device_get_vid(device),
+ spice_usb_device_get_pid(device),
+ spice_usb_device_get_busnum(device),
+ spice_usb_device_get_devaddr(device));
+ return;
+ }
+
+ if (priv->coldplug_list)
+ dev_list = priv->coldplug_list;
+ else
+ libusb_get_device_list(priv->context, &dev_list);
+
+ for (i = 0; dev_list && dev_list[i]; i++) {
+ if (spice_usb_device_manager_libdev_match(dev_list[i], bus, address)) {
+ libdev = dev_list[i];
+ break;
+ }
+ }
+
+ if (libdev)
+ spice_usb_device_manager_add_dev(self, libdev);
+ else
+ g_warning("Could not find USB device to add " DEV_ID_FMT,
+ bus, address);
+
+ if (!priv->coldplug_list)
+ libusb_free_device_list(dev_list, 1);
+}
+
+static void spice_usb_device_manager_remove_udev(SpiceUsbDeviceManager *self,
+ GUdevDevice *udev)
+{
+ int bus, address;
+
+ if (!spice_usb_device_manager_get_udev_bus_n_address(udev, &bus, &address))
+ return;
+
+ spice_usb_device_manager_remove_dev(self, bus, address);
+}
+
+static void spice_usb_device_manager_uevent_cb(GUdevClient *client,
+ const gchar *action,
+ GUdevDevice *udevice,
+ gpointer user_data)
+{
+ SpiceUsbDeviceManager *self = SPICE_USB_DEVICE_MANAGER(user_data);
+
+ if (g_str_equal(action, "add"))
+ spice_usb_device_manager_add_udev(self, udevice);
+ else if (g_str_equal (action, "remove"))
+ spice_usb_device_manager_remove_udev(self, udevice);
+}
+#else
+struct hotplug_idle_cb_args {
+ SpiceUsbDeviceManager *self;
+ libusb_device *device;
+ libusb_hotplug_event event;
+};
+
+static gboolean spice_usb_device_manager_hotplug_idle_cb(gpointer user_data)
+{
+ struct hotplug_idle_cb_args *args = user_data;
+ SpiceUsbDeviceManager *self = SPICE_USB_DEVICE_MANAGER(args->self);
+
+ switch (args->event) {
+ case LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED:
+ spice_usb_device_manager_add_dev(self, args->device);
+ break;
+ case LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT:
+ spice_usb_device_manager_remove_dev(self,
+ libusb_get_bus_number(args->device),
+ libusb_get_device_address(args->device));
+ break;
+ }
+ libusb_unref_device(args->device);
+ g_object_unref(self);
+ g_free(args);
+ return FALSE;
+}
+
+/* Can be called from both the main-thread as well as the event_thread */
+static int spice_usb_device_manager_hotplug_cb(libusb_context *ctx,
+ libusb_device *device,
+ libusb_hotplug_event event,
+ void *user_data)
+{
+ SpiceUsbDeviceManager *self = SPICE_USB_DEVICE_MANAGER(user_data);
+ struct hotplug_idle_cb_args *args = g_malloc0(sizeof(*args));
+
+ args->self = g_object_ref(self);
+ args->device = libusb_ref_device(device);
+ args->event = event;
+ g_idle_add(spice_usb_device_manager_hotplug_idle_cb, args);
+ return 0;
+}
+#endif
+
+static void spice_usb_device_manager_channel_connect_cb(
+ GObject *gobject, GAsyncResult *channel_res, gpointer user_data)
+{
+ SpiceUsbredirChannel *channel = SPICE_USBREDIR_CHANNEL(gobject);
+ GSimpleAsyncResult *result = G_SIMPLE_ASYNC_RESULT(user_data);
+ GError *err = NULL;
+
+ spice_usbredir_channel_connect_device_finish(channel, channel_res, &err);
+ if (err) {
+ g_simple_async_result_take_error(result, err);
+ }
+ g_simple_async_result_complete(result);
+ g_object_unref(result);
+}
+
+#ifdef G_OS_WIN32
+
+typedef struct _UsbInstallCbInfo {
+ SpiceUsbDeviceManager *manager;
+ SpiceUsbDevice *device;
+ SpiceWinUsbDriver *installer;
+ GCancellable *cancellable;
+ GAsyncReadyCallback callback;
+ gpointer user_data;
+} UsbInstallCbInfo;
+
+/**
+ * spice_usb_device_manager_drv_install_cb:
+ * @gobject: #SpiceWinUsbDriver in charge of installing the driver
+ * @res: #GAsyncResult of async win usb driver installation
+ * @user_data: #SpiceUsbDeviceManager requested the installation
+ *
+ * Called when an Windows libusb driver installation completed.
+ *
+ * If the driver installation was successful, continue with USB
+ * device redirection
+ *
+ * Always call _spice_usb_device_manager_connect_device_async.
+ * When installation fails, libusb_open fails too, but cleanup would be better.
+ */
+static void spice_usb_device_manager_drv_install_cb(GObject *gobject,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SpiceUsbDeviceManager *self;
+ SpiceWinUsbDriver *installer;
+ GError *err = NULL;
+ SpiceUsbDevice *device;
+ UsbInstallCbInfo *cbinfo;
+ GCancellable *cancellable;
+ GAsyncReadyCallback callback;
+
+ g_return_if_fail(user_data != NULL);
+
+ cbinfo = user_data;
+ self = cbinfo->manager;
+ device = cbinfo->device;
+ installer = cbinfo->installer;
+ cancellable = cbinfo->cancellable;
+ callback = cbinfo->callback;
+ user_data = cbinfo->user_data;
+
+ g_free(cbinfo);
+
+ g_return_if_fail(SPICE_IS_USB_DEVICE_MANAGER(self));
+ g_return_if_fail(SPICE_IS_WIN_USB_DRIVER(installer));
+ g_return_if_fail(device!= NULL);
+
+ SPICE_DEBUG("Win USB driver install finished");
+
+ if (!spice_win_usb_driver_install_finish(installer, res, &err)) {
+ g_warning("win usb driver install failed -- %s", err->message);
+ g_error_free(err);
+ }
+
+ spice_usb_device_set_state(device, SPICE_USB_DEVICE_STATE_INSTALLED);
+
+ /* device is already ref'ed */
+ _spice_usb_device_manager_connect_device_async(self,
+ device,
+ cancellable,
+ callback,
+ user_data);
+
+ spice_usb_device_unref(device);
+}
+
+static void spice_usb_device_manager_drv_uninstall_cb(GObject *gobject,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ UsbInstallCbInfo *cbinfo = user_data;
+ SpiceUsbDeviceManager *self = cbinfo->manager;
+ GError *err = NULL;
+
+ SPICE_DEBUG("Win USB driver uninstall finished");
+ g_return_if_fail(SPICE_IS_USB_DEVICE_MANAGER(self));
+
+ if (!spice_win_usb_driver_uninstall_finish(cbinfo->installer, res, &err)) {
+ g_warning("win usb driver uninstall failed -- %s", err->message);
+ g_clear_error(&err);
+ }
+
+ spice_usb_device_set_state(cbinfo->device, SPICE_USB_DEVICE_STATE_NONE);
+
+ spice_usb_device_unref(cbinfo->device);
+ g_free(cbinfo);
+}
+
+#endif
+
+/* ------------------------------------------------------------------ */
+/* private api */
+
+static gpointer spice_usb_device_manager_usb_ev_thread(gpointer user_data)
+{
+ SpiceUsbDeviceManager *self = SPICE_USB_DEVICE_MANAGER(user_data);
+ SpiceUsbDeviceManagerPrivate *priv = self->priv;
+ int rc;
+
+ while (priv->event_thread_run) {
+ rc = libusb_handle_events(priv->context);
+ if (rc && rc != LIBUSB_ERROR_INTERRUPTED) {
+ const char *desc = spice_usbutil_libusb_strerror(rc);
+ g_warning("Error handling USB events: %s [%i]", desc, rc);
+ break;
+ }
+ }
+
+ return NULL;
+}
+
+gboolean spice_usb_device_manager_start_event_listening(
+ SpiceUsbDeviceManager *self, GError **err)
+{
+ SpiceUsbDeviceManagerPrivate *priv = self->priv;
+
+ g_return_val_if_fail(err == NULL || *err == NULL, FALSE);
+
+ priv->event_listeners++;
+ if (priv->event_listeners > 1)
+ return TRUE;
+
+ /* We don't join the thread when we stop event listening, as the
+ libusb_handle_events call in the thread won't exit until the
+ libusb_close call for the device is made from usbredirhost_close. */
+ if (priv->event_thread) {
+ g_thread_join(priv->event_thread);
+ priv->event_thread = NULL;
+ }
+ priv->event_thread_run = TRUE;
+#if GLIB_CHECK_VERSION(2,31,19)
+ priv->event_thread = g_thread_new("usb_ev_thread",
+ spice_usb_device_manager_usb_ev_thread,
+ self);
+#else
+ priv->event_thread = g_thread_create(spice_usb_device_manager_usb_ev_thread,
+ self, TRUE, err);
+#endif
+ return priv->event_thread != NULL;
+}
+
+void spice_usb_device_manager_stop_event_listening(
+ SpiceUsbDeviceManager *self)
+{
+ SpiceUsbDeviceManagerPrivate *priv = self->priv;
+
+ g_return_if_fail(priv->event_listeners > 0);
+
+ priv->event_listeners--;
+ if (priv->event_listeners == 0)
+ priv->event_thread_run = FALSE;
+}
+
+static void spice_usb_device_manager_check_redir_on_connect(
+ SpiceUsbDeviceManager *self, SpiceChannel *channel)
+{
+ SpiceUsbDeviceManagerPrivate *priv = self->priv;
+ GSimpleAsyncResult *result;
+ SpiceUsbDevice *device;
+ libusb_device *libdev;
+ guint i;
+
+ if (priv->redirect_on_connect == NULL)
+ return;
+
+ for (i = 0; i < priv->devices->len; i++) {
+ device = g_ptr_array_index(priv->devices, i);
+
+ if (spice_usb_device_manager_is_device_connected(self, device))
+ continue;
+
+ libdev = spice_usb_device_manager_device_to_libdev(self, device);
+#ifdef G_OS_WIN32
+ if (libdev == NULL)
+ continue;
+#endif
+ if (usbredirhost_check_device_filter(
+ priv->redirect_on_connect_rules,
+ priv->redirect_on_connect_rules_count,
+ libdev, 0) == 0) {
+ /* Note: re-uses spice_usb_device_manager_connect_device_async's
+ completion handling code! */
+ result = g_simple_async_result_new(G_OBJECT(self),
+ spice_usb_device_manager_auto_connect_cb,
+ spice_usb_device_ref(device),
+ spice_usb_device_manager_connect_device_async);
+ spice_usbredir_channel_connect_device_async(
+ SPICE_USBREDIR_CHANNEL(channel),
+ libdev, device, NULL,
+ spice_usb_device_manager_channel_connect_cb,
+ result);
+ libusb_unref_device(libdev);
+ return; /* We've taken the channel! */
+ }
+
+ libusb_unref_device(libdev);
+ }
+}
+
+void spice_usb_device_manager_device_error(
+ SpiceUsbDeviceManager *self, SpiceUsbDevice *device, GError *err)
+{
+ g_return_if_fail(SPICE_IS_USB_DEVICE_MANAGER(self));
+ g_return_if_fail(device != NULL);
+
+ g_signal_emit(self, signals[DEVICE_ERROR], 0, device, err);
+}
+#endif
+
+static SpiceUsbredirChannel *spice_usb_device_manager_get_channel_for_dev(
+ SpiceUsbDeviceManager *manager, SpiceUsbDevice *device)
+{
+#ifdef USE_USBREDIR
+ SpiceUsbDeviceManagerPrivate *priv = manager->priv;
+ guint i;
+
+ for (i = 0; i < priv->channels->len; i++) {
+ SpiceUsbredirChannel *channel = g_ptr_array_index(priv->channels, i);
+ libusb_device *libdev = spice_usbredir_channel_get_device(channel);
+ if (spice_usb_device_equal_libdev(device, libdev))
+ return channel;
+ }
+#endif
+ return NULL;
+}
+
+/* ------------------------------------------------------------------ */
+/* public api */
+
+/**
+ * spice_usb_device_manager_get_devices_with_filter:
+ * @manager: the #SpiceUsbDeviceManager manager
+ * @filter: (allow-none): filter string for selecting which devices to return,
+ * see #SpiceUsbDeviceManager:auto-connect-filter for the f ilter
+ * string format
+ *
+ * Returns: (element-type SpiceUsbDevice) (transfer full): a
+ * %GPtrArray array of %SpiceUsbDevice
+ *
+ * Since: 0.20
+ */
+GPtrArray* spice_usb_device_manager_get_devices_with_filter(
+ SpiceUsbDeviceManager *self, const gchar *filter)
+{
+ GPtrArray *devices_copy = NULL;
+
+ g_return_val_if_fail(SPICE_IS_USB_DEVICE_MANAGER(self), NULL);
+
+#ifdef USE_USBREDIR
+ SpiceUsbDeviceManagerPrivate *priv = self->priv;
+ struct usbredirfilter_rule *rules = NULL;;
+ int r, count = 0;
+ guint i;
+
+ if (filter) {
+ r = usbredirfilter_string_to_rules(filter, ",", "|", &rules, &count);
+ if (r) {
+ if (r == -ENOMEM)
+ g_error("Failed to allocate memory for filter");
+ g_warning("Error parsing filter, ignoring");
+ rules = NULL;
+ count = 0;
+ }
+ }
+
+ devices_copy = g_ptr_array_new_with_free_func((GDestroyNotify)
+ spice_usb_device_unref);
+ for (i = 0; i < priv->devices->len; i++) {
+ SpiceUsbDevice *device = g_ptr_array_index(priv->devices, i);
+
+ if (rules) {
+ libusb_device *libdev =
+ spice_usb_device_manager_device_to_libdev(self, device);
+#ifdef G_OS_WIN32
+ if (libdev == NULL)
+ continue;
+#endif
+ if (usbredirhost_check_device_filter(rules, count, libdev, 0) != 0)
+ continue;
+ }
+ g_ptr_array_add(devices_copy, spice_usb_device_ref(device));
+ }
+
+ free(rules);
+#endif
+
+ return devices_copy;
+}
+
+/**
+ * spice_usb_device_manager_get_devices:
+ * @manager: the #SpiceUsbDeviceManager manager
+ *
+ * Returns: (element-type SpiceUsbDevice) (transfer full): a %GPtrArray array of %SpiceUsbDevice
+ */
+GPtrArray* spice_usb_device_manager_get_devices(SpiceUsbDeviceManager *self)
+{
+ return spice_usb_device_manager_get_devices_with_filter(self, NULL);
+}
+
+/**
+ * spice_usb_device_manager_is_device_connected:
+ * @manager: the #SpiceUsbDeviceManager manager
+ * @device: a #SpiceUsbDevice
+ *
+ * Returns: %TRUE if @device has an associated USB redirection channel
+ */
+gboolean spice_usb_device_manager_is_device_connected(SpiceUsbDeviceManager *self,
+ SpiceUsbDevice *device)
+{
+ g_return_val_if_fail(SPICE_IS_USB_DEVICE_MANAGER(self), FALSE);
+ g_return_val_if_fail(device != NULL, FALSE);
+
+ return !!spice_usb_device_manager_get_channel_for_dev(self, device);
+}
+
+/**
+ * spice_usb_device_manager_connect_device_async:
+ * @manager: the #SpiceUsbDeviceManager manager
+ * @device: a #SpiceUsbDevice to redirect
+ * @cancellable: a #GCancellable or NULL
+ * @callback: a #GAsyncReadyCallback to call when the request is satisfied
+ * @user_data: data to pass to callback
+ */
+static void
+_spice_usb_device_manager_connect_device_async(SpiceUsbDeviceManager *self,
+ SpiceUsbDevice *device,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GSimpleAsyncResult *result;
+
+ g_return_if_fail(SPICE_IS_USB_DEVICE_MANAGER(self));
+ g_return_if_fail(device != NULL);
+
+ SPICE_DEBUG("connecting device %p", device);
+
+ result = g_simple_async_result_new(G_OBJECT(self), callback, user_data,
+ spice_usb_device_manager_connect_device_async);
+
+#ifdef USE_USBREDIR
+ SpiceUsbDeviceManagerPrivate *priv = self->priv;
+ libusb_device *libdev;
+ guint i;
+
+ if (spice_usb_device_manager_is_device_connected(self, device)) {
+ g_simple_async_result_set_error(result,
+ SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "Cannot connect an already connected usb device");
+ goto done;
+ }
+
+ for (i = 0; i < priv->channels->len; i++) {
+ SpiceUsbredirChannel *channel = g_ptr_array_index(priv->channels, i);
+
+ if (spice_usbredir_channel_get_device(channel))
+ continue; /* Skip already used channels */
+
+ libdev = spice_usb_device_manager_device_to_libdev(self, device);
+#ifdef G_OS_WIN32
+ if (libdev == NULL) {
+ /* Most likely, the device was plugged out at driver installation
+ * time, and its remove-device event was ignored.
+ * So remove the device now
+ */
+ SPICE_DEBUG("libdev does not exist for %p -- removing", device);
+ spice_usb_device_ref(device);
+ g_ptr_array_remove(priv->devices, device);
+ g_signal_emit(self, signals[DEVICE_REMOVED], 0, device);
+ spice_usb_device_unref(device);
+ g_simple_async_result_set_error(result,
+ SPICE_CLIENT_ERROR,
+ SPICE_CLIENT_ERROR_FAILED,
+ _("Device was not found"));
+ goto done;
+ }
+#endif
+ spice_usbredir_channel_connect_device_async(channel,
+ libdev,
+ device,
+ cancellable,
+ spice_usb_device_manager_channel_connect_cb,
+ result);
+ libusb_unref_device(libdev);
+ return;
+ }
+#endif
+
+ g_simple_async_result_set_error(result,
+ SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ _("No free USB channel"));
+#ifdef USE_USBREDIR
+done:
+#endif
+ g_simple_async_result_complete_in_idle(result);
+ g_object_unref(result);
+}
+
+
+void spice_usb_device_manager_connect_device_async(SpiceUsbDeviceManager *self,
+ SpiceUsbDevice *device,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+
+#if defined(USE_USBREDIR) && defined(G_OS_WIN32)
+ SpiceWinUsbDriver *installer;
+ UsbInstallCbInfo *cbinfo;
+
+ g_return_if_fail(self->priv->installer);
+
+ spice_usb_device_set_state(device, SPICE_USB_DEVICE_STATE_INSTALLING);
+
+ installer = self->priv->installer;
+ cbinfo = g_new0(UsbInstallCbInfo, 1);
+ cbinfo->manager = self;
+ cbinfo->device = spice_usb_device_ref(device);
+ cbinfo->installer = installer;
+ cbinfo->cancellable = cancellable;
+ cbinfo->callback = callback;
+ cbinfo->user_data = user_data;
+
+ spice_win_usb_driver_install_async(installer, device, cancellable,
+ spice_usb_device_manager_drv_install_cb,
+ cbinfo);
+#else
+ _spice_usb_device_manager_connect_device_async(self,
+ device,
+ cancellable,
+ callback,
+ user_data);
+#endif
+}
+
+gboolean spice_usb_device_manager_connect_device_finish(
+ SpiceUsbDeviceManager *self, GAsyncResult *res, GError **err)
+{
+ GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT(res);
+
+ g_return_val_if_fail(g_simple_async_result_is_valid(res, G_OBJECT(self),
+ spice_usb_device_manager_connect_device_async),
+ FALSE);
+
+ if (g_simple_async_result_propagate_error(simple, err))
+ return FALSE;
+
+ return TRUE;
+}
+
+/**
+ * spice_usb_device_manager_disconnect_device:
+ * @manager: the #SpiceUsbDeviceManager manager
+ * @device: a #SpiceUsbDevice to disconnect
+ *
+ * Returns: %TRUE if @device has an associated USB redirection channel
+ */
+void spice_usb_device_manager_disconnect_device(SpiceUsbDeviceManager *self,
+ SpiceUsbDevice *device)
+{
+ g_return_if_fail(SPICE_IS_USB_DEVICE_MANAGER(self));
+ g_return_if_fail(device != NULL);
+
+ SPICE_DEBUG("disconnecting device %p", device);
+
+#ifdef USE_USBREDIR
+ SpiceUsbredirChannel *channel;
+
+ channel = spice_usb_device_manager_get_channel_for_dev(self, device);
+ if (channel)
+ spice_usbredir_channel_disconnect_device(channel);
+
+#ifdef G_OS_WIN32
+ SpiceWinUsbDriver *installer;
+ UsbInstallCbInfo *cbinfo;
+ guint8 state;
+
+ g_warn_if_fail(device != NULL);
+ g_return_if_fail(self->priv->installer);
+
+ state = spice_usb_device_get_state(device);
+ if ((state != SPICE_USB_DEVICE_STATE_INSTALLED) &&
+ (state != SPICE_USB_DEVICE_STATE_CONNECTED)) {
+ return;
+ }
+
+ spice_usb_device_set_state(device, SPICE_USB_DEVICE_STATE_UNINSTALLING);
+
+ installer = self->priv->installer;
+ cbinfo = g_new0(UsbInstallCbInfo, 1);
+ cbinfo->manager = self;
+ cbinfo->device = spice_usb_device_ref(device);
+ cbinfo->installer = installer;
+
+ spice_win_usb_driver_uninstall_async(installer, device, NULL,
+ spice_usb_device_manager_drv_uninstall_cb,
+ cbinfo);
+#endif
+
+#endif
+}
+
+gboolean
+spice_usb_device_manager_can_redirect_device(SpiceUsbDeviceManager *self,
+ SpiceUsbDevice *device,
+ GError **err)
+{
+#ifdef USE_USBREDIR
+ const struct usbredirfilter_rule *guest_filter_rules = NULL;
+ SpiceUsbDeviceManagerPrivate *priv = self->priv;
+ int i, guest_filter_rules_count;
+
+ g_return_val_if_fail(SPICE_IS_USB_DEVICE_MANAGER(self), FALSE);
+ g_return_val_if_fail(device != NULL, FALSE);
+ g_return_val_if_fail(err == NULL || *err == NULL, FALSE);
+
+ if (!spice_session_get_usbredir_enabled(priv->session)) {
+ g_set_error_literal(err, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ _("USB redirection is disabled"));
+ return FALSE;
+ }
+
+ if (!priv->channels->len) {
+ g_set_error_literal(err, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ _("The connected VM is not configured for USB redirection"));
+ return FALSE;
+ }
+
+ /* Skip the other checks for already connected devices */
+ if (spice_usb_device_manager_is_device_connected(self, device))
+ return TRUE;
+
+ /* We assume all channels have the same filter, so we just take the
+ filter from the first channel */
+ spice_usbredir_channel_get_guest_filter(
+ g_ptr_array_index(priv->channels, 0),
+ &guest_filter_rules, &guest_filter_rules_count);
+
+ if (guest_filter_rules) {
+ gboolean filter_ok;
+ libusb_device *libdev;
+
+ libdev = spice_usb_device_manager_device_to_libdev(self, device);
+#ifdef G_OS_WIN32
+ if (libdev == NULL) {
+ g_set_error_literal(err, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ _("Some USB devices were not found"));
+ return FALSE;
+ }
+#endif
+ filter_ok = (usbredirhost_check_device_filter(
+ guest_filter_rules, guest_filter_rules_count,
+ libdev, 0) == 0);
+ libusb_unref_device(libdev);
+ if (!filter_ok) {
+ g_set_error_literal(err, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ _("Some USB devices are blocked by host policy"));
+ return FALSE;
+ }
+ }
+
+ /* Check if there are free channels */
+ for (i = 0; i < priv->channels->len; i++) {
+ SpiceUsbredirChannel *channel = g_ptr_array_index(priv->channels, i);
+
+ if (!spice_usbredir_channel_get_device(channel))
+ break;
+ }
+ if (i == priv->channels->len) {
+ g_set_error_literal(err, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ _("There are no free USB channels"));
+ return FALSE;
+ }
+
+ return TRUE;
+#else
+ g_set_error_literal(err, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ _("USB redirection support not compiled in"));
+ return FALSE;
+#endif
+}
+
+/**
+ * spice_usb_device_get_description:
+ * @device: #SpiceUsbDevice to get the description of
+ * @format: (allow-none): an optional printf() format string with
+ * positional parameters
+ *
+ * Get a string describing the device which is suitable as a description of
+ * the device for the end user. The returned string should be freed with
+ * g_free() when no longer needed.
+ *
+ * The @format positional parameters are the following:
+ * - '%%1$s' manufacturer
+ * - '%%2$s' product
+ * - '%%3$s' descriptor (a [vendor_id:product_id] string)
+ * - '%%4$d' bus
+ * - '%%5$d' address
+ *
+ * (the default format string is "%%s %%s %%s at %%d-%%d")
+ *
+ * Returns: a newly-allocated string holding the description, or %NULL if failed
+ */
+gchar *spice_usb_device_get_description(SpiceUsbDevice *device, const gchar *format)
+{
+#ifdef USE_USBREDIR
+ int bus, address, vid, pid;
+ gchar *description, *descriptor, *manufacturer = NULL, *product = NULL;
+
+ g_return_val_if_fail(device != NULL, NULL);
+
+ bus = spice_usb_device_get_busnum(device);
+ address = spice_usb_device_get_devaddr(device);
+ vid = spice_usb_device_get_vid(device);
+ pid = spice_usb_device_get_pid(device);
+
+ if ((vid > 0) && (pid > 0)) {
+ descriptor = g_strdup_printf("[%04x:%04x]", vid, pid);
+ } else {
+ descriptor = g_strdup("");
+ }
+
+ spice_usb_util_get_device_strings(bus, address, vid, pid,
+ &manufacturer, &product);
+
+ if (!format)
+ format = _("%s %s %s at %d-%d");
+
+ description = g_strdup_printf(format, manufacturer, product, descriptor, bus, address);
+
+ g_free(manufacturer);
+ g_free(descriptor);
+ g_free(product);
+
+ return description;
+#else
+ return NULL;
+#endif
+}
+
+
+
+#ifdef USE_USBREDIR
+/*
+ * SpiceUsbDeviceInfo
+ */
+static SpiceUsbDeviceInfo *spice_usb_device_new(libusb_device *libdev)
+{
+ SpiceUsbDeviceInfo *info;
+ int vid, pid;
+ guint8 bus, addr;
+
+ g_return_val_if_fail(libdev != NULL, NULL);
+
+ bus = libusb_get_bus_number(libdev);
+ addr = libusb_get_device_address(libdev);
+
+ if (!spice_usb_device_manager_get_libdev_vid_pid(libdev, &vid, &pid)) {
+ return NULL;
+ }
+
+ info = g_new0(SpiceUsbDeviceInfo, 1);
+
+ info->busnum = bus;
+ info->devaddr = addr;
+ info->vid = vid;
+ info->pid = pid;
+ info->ref = 1;
+#ifndef G_OS_WIN32
+ info->libdev = libusb_ref_device(libdev);
+#endif
+
+ return info;
+}
+
+guint8 spice_usb_device_get_busnum(const SpiceUsbDevice *device)
+{
+ const SpiceUsbDeviceInfo *info = (const SpiceUsbDeviceInfo *)device;
+
+ g_return_val_if_fail(info != NULL, 0);
+
+ return info->busnum;
+}
+
+guint8 spice_usb_device_get_devaddr(const SpiceUsbDevice *device)
+{
+ const SpiceUsbDeviceInfo *info = (const SpiceUsbDeviceInfo *)device;
+
+ g_return_val_if_fail(info != NULL, 0);
+
+ return info->devaddr;
+}
+
+guint16 spice_usb_device_get_vid(const SpiceUsbDevice *device)
+{
+ const SpiceUsbDeviceInfo *info = (const SpiceUsbDeviceInfo *)device;
+
+ g_return_val_if_fail(info != NULL, 0);
+
+ return info->vid;
+}
+
+guint16 spice_usb_device_get_pid(const SpiceUsbDevice *device)
+{
+ const SpiceUsbDeviceInfo *info = (const SpiceUsbDeviceInfo *)device;
+
+ g_return_val_if_fail(info != NULL, 0);
+
+ return info->pid;
+}
+
+#ifdef G_OS_WIN32
+void spice_usb_device_set_state(SpiceUsbDevice *device, guint8 state)
+{
+ SpiceUsbDeviceInfo *info = (SpiceUsbDeviceInfo *)device;
+
+ g_return_if_fail(info != NULL);
+
+ info->state = state;
+}
+
+guint8 spice_usb_device_get_state(SpiceUsbDevice *device)
+{
+ SpiceUsbDeviceInfo *info = (SpiceUsbDeviceInfo *)device;
+
+ g_return_val_if_fail(info != NULL, 0);
+
+ return info->state;
+}
+#endif
+
+static SpiceUsbDevice *spice_usb_device_ref(SpiceUsbDevice *device)
+{
+ SpiceUsbDeviceInfo *info = (SpiceUsbDeviceInfo *)device;
+
+ g_return_val_if_fail(info != NULL, NULL);
+ g_atomic_int_inc(&info->ref);
+ return device;
+}
+
+static void spice_usb_device_unref(SpiceUsbDevice *device)
+{
+ gboolean ref_count_is_0;
+
+ SpiceUsbDeviceInfo *info = (SpiceUsbDeviceInfo *)device;
+
+ g_return_if_fail(info != NULL);
+
+ ref_count_is_0 = g_atomic_int_dec_and_test(&info->ref);
+ if (ref_count_is_0) {
+#ifndef G_OS_WIN32
+ libusb_unref_device(info->libdev);
+#endif
+ g_free(info);
+ }
+}
+
+#ifndef G_OS_WIN32 /* Linux -- directly compare libdev */
+static gboolean
+spice_usb_device_equal_libdev(SpiceUsbDevice *device,
+ libusb_device *libdev)
+{
+ SpiceUsbDeviceInfo *info = (SpiceUsbDeviceInfo *)device;
+
+ if ((device == NULL) || (libdev == NULL))
+ return FALSE;
+
+ return info->libdev == libdev;
+}
+#else /* Windows -- compare vid:pid of device and libdev */
+static gboolean
+spice_usb_device_equal_libdev(SpiceUsbDevice *device,
+ libusb_device *libdev)
+{
+ int vid1, vid2, pid1, pid2;
+
+ if ((device == NULL) || (libdev == NULL))
+ return FALSE;
+
+ vid1 = spice_usb_device_get_vid(device);
+ pid1 = spice_usb_device_get_pid(device);
+
+ if (!spice_usb_device_manager_get_libdev_vid_pid(libdev, &vid2, &pid2)) {
+ return FALSE;
+ }
+
+ return ((vid1 == vid2) && (pid1 == pid2));
+}
+#endif
+
+/*
+ * Caller must libusb_unref_device the libusb_device returned by this function.
+ * Returns a libusb_device, or NULL upon failure
+ */
+static libusb_device *
+spice_usb_device_manager_device_to_libdev(SpiceUsbDeviceManager *self,
+ SpiceUsbDevice *device)
+{
+#ifdef G_OS_WIN32
+ /*
+ * On win32 we need to do this the hard and slow way, by asking libusb to
+ * re-enumerate all devices and then finding a matching device.
+ * We cannot cache the libusb_device like we do under Linux since the
+ * driver swap we do under windows invalidates the cached libdev.
+ */
+
+ libusb_device *d, **devlist;
+ int bus, addr;
+ int i;
+
+ g_return_val_if_fail(SPICE_IS_USB_DEVICE_MANAGER(self), NULL);
+ g_return_val_if_fail(device != NULL, NULL);
+ g_return_val_if_fail(self->priv != NULL, NULL);
+ g_return_val_if_fail(self->priv->context != NULL, NULL);
+
+ /* On windows we match by vid / pid, since the address may change */
+ bus = spice_usb_device_get_vid(device);
+ addr = spice_usb_device_get_pid(device);
+
+ libusb_get_device_list(self->priv->context, &devlist);
+ if (!devlist)
+ return NULL;
+
+ for (i = 0; (d = devlist[i]) != NULL; i++) {
+ if (spice_usb_device_manager_libdev_match(d, bus, addr)) {
+ libusb_ref_device(d);
+ break;
+ }
+ }
+
+ libusb_free_device_list(devlist, 1);
+
+ return d;
+
+#else
+ /* Simply return a ref to the cached libdev */
+ SpiceUsbDeviceInfo *info = (SpiceUsbDeviceInfo *)device;
+
+ return libusb_ref_device(info->libdev);
+#endif
+}
+#endif /* USE_USBREDIR */
diff --git a/src/usb-device-manager.h b/src/usb-device-manager.h
new file mode 100644
index 0000000..5b4cfbe
--- /dev/null
+++ b/src/usb-device-manager.h
@@ -0,0 +1,122 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2011, 2012 Red Hat, Inc.
+
+ Red Hat Authors:
+ Hans de Goede <hdegoede@redhat.com>
+
+ 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_USB_DEVICE_MANAGER_H__
+#define __SPICE_USB_DEVICE_MANAGER_H__
+
+#include "spice-client.h"
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_USB_DEVICE_MANAGER (spice_usb_device_manager_get_type ())
+#define SPICE_USB_DEVICE_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SPICE_TYPE_USB_DEVICE_MANAGER, SpiceUsbDeviceManager))
+#define SPICE_USB_DEVICE_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SPICE_TYPE_USB_DEVICE_MANAGER, SpiceUsbDeviceManagerClass))
+#define SPICE_IS_USB_DEVICE_MANAGER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SPICE_TYPE_USB_DEVICE_MANAGER))
+#define SPICE_IS_USB_DEVICE_MANAGER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SPICE_TYPE_USB_DEVICE_MANAGER))
+#define SPICE_USB_DEVICE_MANAGER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SPICE_TYPE_USB_DEVICE_MANAGER, SpiceUsbDeviceManagerClass))
+
+#define SPICE_TYPE_USB_DEVICE (spice_usb_device_get_type())
+
+typedef struct _SpiceUsbDeviceManager SpiceUsbDeviceManager;
+typedef struct _SpiceUsbDeviceManagerClass SpiceUsbDeviceManagerClass;
+typedef struct _SpiceUsbDeviceManagerPrivate SpiceUsbDeviceManagerPrivate;
+
+typedef struct _SpiceUsbDevice SpiceUsbDevice;
+
+/**
+ * SpiceUsbDeviceManager:
+ *
+ * The #SpiceUsbDeviceManager struct is opaque and should not be accessed directly.
+ */
+struct _SpiceUsbDeviceManager
+{
+ GObject parent;
+
+ /*< private >*/
+ SpiceUsbDeviceManagerPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+/**
+ * SpiceUsbDeviceManagerClass:
+ * @parent_class: Parent class.
+ * @device_added: Signal class handler for the #SpiceUsbDeviceManager::device-added signal.
+ * @device_removed: Signal class handler for the #SpiceUsbDeviceManager::device-removed signal.
+ * @auto_connect_failed: Signal class handler for the #SpiceUsbDeviceManager::auto-connect-failed signal.
+ *
+ * Class structure for #SpiceUsbDeviceManager.
+ */
+struct _SpiceUsbDeviceManagerClass
+{
+ GObjectClass parent_class;
+
+ /* signals */
+ void (*device_added) (SpiceUsbDeviceManager *manager,
+ SpiceUsbDevice *device);
+ void (*device_removed) (SpiceUsbDeviceManager *manager,
+ SpiceUsbDevice *device);
+ void (*auto_connect_failed) (SpiceUsbDeviceManager *manager,
+ SpiceUsbDevice *device, GError *error);
+ void (*device_error) (SpiceUsbDeviceManager *manager,
+ SpiceUsbDevice *device, GError *error);
+ /*< private >*/
+ /*
+ * If adding fields to this struct, remove corresponding
+ * amount of padding to avoid changing overall struct size
+ */
+ gchar _spice_reserved[SPICE_RESERVED_PADDING];
+};
+
+GType spice_usb_device_get_type(void);
+GType spice_usb_device_manager_get_type(void);
+
+gchar *spice_usb_device_get_description(SpiceUsbDevice *device, const gchar *format);
+gconstpointer spice_usb_device_get_libusb_device(const SpiceUsbDevice *device);
+
+SpiceUsbDeviceManager *spice_usb_device_manager_get(SpiceSession *session,
+ GError **err);
+
+GPtrArray *spice_usb_device_manager_get_devices(SpiceUsbDeviceManager *manager);
+GPtrArray* spice_usb_device_manager_get_devices_with_filter(
+ SpiceUsbDeviceManager *manager, const gchar *filter);
+
+gboolean spice_usb_device_manager_is_device_connected(SpiceUsbDeviceManager *manager,
+ SpiceUsbDevice *device);
+void spice_usb_device_manager_connect_device_async(
+ SpiceUsbDeviceManager *manager,
+ SpiceUsbDevice *device,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gboolean spice_usb_device_manager_connect_device_finish(
+ SpiceUsbDeviceManager *self, GAsyncResult *res, GError **err);
+
+void spice_usb_device_manager_disconnect_device(SpiceUsbDeviceManager *manager,
+ SpiceUsbDevice *device);
+
+gboolean
+spice_usb_device_manager_can_redirect_device(SpiceUsbDeviceManager *self,
+ SpiceUsbDevice *device,
+ GError **err);
+
+G_END_DECLS
+
+#endif /* __SPICE_USB_DEVICE_MANAGER_H__ */
diff --git a/src/usb-device-widget.c b/src/usb-device-widget.c
new file mode 100644
index 0000000..1ec30e3
--- /dev/null
+++ b/src/usb-device-widget.c
@@ -0,0 +1,554 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 Red Hat, Inc.
+
+ Red Hat Authors:
+ Hans de Goede <hdegoede@redhat.com>
+
+ 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.h>
+#include "glib-compat.h"
+#include "spice-client.h"
+#include "spice-marshal.h"
+#include "usb-device-widget.h"
+
+/**
+ * SECTION:usb-device-widget
+ * @short_description: USB device selection widget
+ * @title: Spice USB device selection widget
+ * @section_id:
+ * @see_also:
+ * @stability: Stable
+ * @include: usb-device-widget.h
+ *
+ * #SpiceUsbDeviceWidget is a gtk widget which apps can use to easily
+ * add an UI to select USB devices to redirect (or unredirect).
+ */
+
+/* ------------------------------------------------------------------ */
+/* Prototypes for callbacks */
+static void device_added_cb(SpiceUsbDeviceManager *manager,
+ SpiceUsbDevice *device, gpointer user_data);
+static void device_removed_cb(SpiceUsbDeviceManager *manager,
+ SpiceUsbDevice *device, gpointer user_data);
+static void device_error_cb(SpiceUsbDeviceManager *manager,
+ SpiceUsbDevice *device, GError *err, gpointer user_data);
+static gboolean spice_usb_device_widget_update_status(gpointer user_data);
+
+/* ------------------------------------------------------------------ */
+/* gobject glue */
+
+#define SPICE_USB_DEVICE_WIDGET_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_USB_DEVICE_WIDGET, \
+ SpiceUsbDeviceWidgetPrivate))
+
+enum {
+ PROP_0,
+ PROP_SESSION,
+ PROP_DEVICE_FORMAT_STRING,
+};
+
+enum {
+ CONNECT_FAILED,
+ LAST_SIGNAL,
+};
+
+struct _SpiceUsbDeviceWidgetPrivate {
+ SpiceSession *session;
+ gchar *device_format_string;
+ SpiceUsbDeviceManager *manager;
+ GtkWidget *info_bar;
+ gchar *err_msg;
+ gsize device_count;
+};
+
+static guint signals[LAST_SIGNAL] = { 0, };
+
+#if GTK_CHECK_VERSION(3,0,0)
+G_DEFINE_TYPE(SpiceUsbDeviceWidget, spice_usb_device_widget, GTK_TYPE_BOX);
+#else
+G_DEFINE_TYPE(SpiceUsbDeviceWidget, spice_usb_device_widget, GTK_TYPE_VBOX);
+#endif
+
+
+static void spice_usb_device_widget_get_property(GObject *gobject,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(gobject);
+ SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+
+ switch (prop_id) {
+ case PROP_SESSION:
+ g_value_set_object(value, priv->session);
+ break;
+ case PROP_DEVICE_FORMAT_STRING:
+ g_value_set_string(value, priv->device_format_string);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_usb_device_widget_set_property(GObject *gobject,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(gobject);
+ SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+
+ switch (prop_id) {
+ case PROP_SESSION:
+ priv->session = g_value_dup_object(value);
+ break;
+ case PROP_DEVICE_FORMAT_STRING:
+ priv->device_format_string = g_value_dup_string(value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_usb_device_widget_hide_info_bar(SpiceUsbDeviceWidget *self)
+{
+ SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+
+ if (priv->info_bar) {
+ gtk_widget_destroy(priv->info_bar);
+ priv->info_bar = NULL;
+ }
+}
+
+static void
+spice_usb_device_widget_show_info_bar(SpiceUsbDeviceWidget *self,
+ const gchar *message,
+ GtkMessageType message_type,
+ const gchar *stock_icon_id)
+{
+ SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+ GtkWidget *info_bar, *content_area, *hbox, *widget;
+
+ spice_usb_device_widget_hide_info_bar(self);
+
+ info_bar = gtk_info_bar_new();
+ gtk_info_bar_set_message_type(GTK_INFO_BAR(info_bar), message_type);
+
+ content_area = gtk_info_bar_get_content_area(GTK_INFO_BAR(info_bar));
+#if GTK_CHECK_VERSION(3,0,0)
+ hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
+#else
+ hbox = gtk_hbox_new(FALSE, 12);
+#endif
+ gtk_container_add(GTK_CONTAINER(content_area), hbox);
+
+ widget = gtk_image_new_from_stock(stock_icon_id,
+ GTK_ICON_SIZE_SMALL_TOOLBAR);
+ gtk_box_pack_start(GTK_BOX(hbox), widget, FALSE, FALSE, 0);
+
+ widget = gtk_label_new(message);
+ gtk_box_pack_start(GTK_BOX(hbox), widget, TRUE, TRUE, 0);
+
+ priv->info_bar = gtk_alignment_new(0.0, 0.0, 1.0, 0.0);
+ gtk_alignment_set_padding(GTK_ALIGNMENT(priv->info_bar), 0, 0, 12, 0);
+ gtk_container_add(GTK_CONTAINER(priv->info_bar), info_bar);
+ gtk_box_pack_start(GTK_BOX(self), priv->info_bar, FALSE, FALSE, 0);
+ gtk_widget_show_all(priv->info_bar);
+}
+
+static GObject *spice_usb_device_widget_constructor(
+ GType gtype, guint n_properties, GObjectConstructParam *properties)
+{
+ GObject *obj;
+ SpiceUsbDeviceWidget *self;
+ SpiceUsbDeviceWidgetPrivate *priv;
+ GPtrArray *devices = NULL;
+ GError *err = NULL;
+ GtkWidget *label;
+ gchar *str;
+ int i;
+
+ {
+ /* Always chain up to the parent constructor */
+ GObjectClass *parent_class;
+ parent_class = G_OBJECT_CLASS(spice_usb_device_widget_parent_class);
+ obj = parent_class->constructor(gtype, n_properties, properties);
+ }
+
+ self = SPICE_USB_DEVICE_WIDGET(obj);
+ priv = self->priv;
+ if (!priv->session)
+ g_error("SpiceUsbDeviceWidget constructed without a session");
+
+ label = gtk_label_new(NULL);
+ str = g_strdup_printf("<b>%s</b>", _("Select USB devices to redirect"));
+ gtk_label_set_markup(GTK_LABEL (label), str);
+ g_free(str);
+ gtk_misc_set_alignment(GTK_MISC(label), 0.0, 0.5);
+ gtk_box_pack_start(GTK_BOX(self), label, FALSE, FALSE, 0);
+
+ priv->manager = spice_usb_device_manager_get(priv->session, &err);
+ if (err) {
+ spice_usb_device_widget_show_info_bar(self, err->message,
+ GTK_MESSAGE_WARNING,
+ GTK_STOCK_DIALOG_WARNING);
+ g_clear_error(&err);
+ return obj;
+ }
+
+ g_signal_connect(priv->manager, "device-added",
+ G_CALLBACK(device_added_cb), self);
+ g_signal_connect(priv->manager, "device-removed",
+ G_CALLBACK(device_removed_cb), self);
+ g_signal_connect(priv->manager, "device-error",
+ G_CALLBACK(device_error_cb), self);
+
+ devices = spice_usb_device_manager_get_devices(priv->manager);
+ if (!devices)
+ goto end;
+
+ for (i = 0; i < devices->len; i++)
+ device_added_cb(NULL, g_ptr_array_index(devices, i), self);
+
+ g_ptr_array_unref(devices);
+
+end:
+ spice_usb_device_widget_update_status(self);
+
+ return obj;
+}
+
+static void spice_usb_device_widget_finalize(GObject *object)
+{
+ SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(object);
+ SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+
+ if (priv->manager) {
+ g_signal_handlers_disconnect_by_func(priv->manager,
+ device_added_cb, self);
+ g_signal_handlers_disconnect_by_func(priv->manager,
+ device_removed_cb, self);
+ g_signal_handlers_disconnect_by_func(priv->manager,
+ device_error_cb, self);
+ }
+ g_object_unref(priv->session);
+ g_free(priv->device_format_string);
+
+ if (G_OBJECT_CLASS(spice_usb_device_widget_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_usb_device_widget_parent_class)->finalize(object);
+}
+
+static void spice_usb_device_widget_class_init(
+ SpiceUsbDeviceWidgetClass *klass)
+{
+ GObjectClass *gobject_class = (GObjectClass *)klass;
+ GParamSpec *pspec;
+
+ g_type_class_add_private (klass, sizeof (SpiceUsbDeviceWidgetPrivate));
+
+ gobject_class->constructor = spice_usb_device_widget_constructor;
+ gobject_class->finalize = spice_usb_device_widget_finalize;
+ gobject_class->get_property = spice_usb_device_widget_get_property;
+ gobject_class->set_property = spice_usb_device_widget_set_property;
+
+ /**
+ * SpiceUsbDeviceWidget:session:
+ *
+ * #SpiceSession this #SpiceUsbDeviceWidget is associated with
+ *
+ **/
+ pspec = g_param_spec_object("session",
+ "Session",
+ "SpiceSession",
+ SPICE_TYPE_SESSION,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS);
+ g_object_class_install_property(gobject_class, PROP_SESSION, pspec);
+
+ /**
+ * SpiceUsbDeviceWidget:device-format-string:
+ *
+ * Format string to pass to spice_usb_device_get_description() for getting
+ * the device USB descriptions.
+ */
+ pspec = g_param_spec_string("device-format-string",
+ "Device format string",
+ "Format string for device description",
+ NULL,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE |
+ G_PARAM_STATIC_STRINGS);
+ g_object_class_install_property(gobject_class, PROP_DEVICE_FORMAT_STRING,
+ pspec);
+
+ /**
+ * SpiceUsbDeviceWidget::connect-failed:
+ * @widget: The #SpiceUsbDeviceWidget that emitted the signal
+ * @device: #SpiceUsbDevice boxed object corresponding to the added device
+ * @error: #GError describing the reason why the connect failed
+ *
+ * The #SpiceUsbDeviceWidget::connect-failed signal is emitted whenever
+ * the user has requested for a device to be redirected and this has
+ * failed.
+ **/
+ signals[CONNECT_FAILED] =
+ g_signal_new("connect-failed",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(SpiceUsbDeviceWidgetClass, connect_failed),
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__BOXED_BOXED,
+ G_TYPE_NONE,
+ 2,
+ SPICE_TYPE_USB_DEVICE,
+ G_TYPE_ERROR);
+}
+
+static void spice_usb_device_widget_init(SpiceUsbDeviceWidget *self)
+{
+ self->priv = SPICE_USB_DEVICE_WIDGET_GET_PRIVATE(self);
+}
+
+/* ------------------------------------------------------------------ */
+/* public api */
+
+/**
+ * spice_usb_device_widget_new:
+ * @session: #SpiceSession for which to widget will control USB redirection
+ * @device_format_string: (allow-none): String passed to
+ * spice_usb_device_get_description()
+ *
+ * Returns: a new #SpiceUsbDeviceWidget instance
+ */
+GtkWidget *spice_usb_device_widget_new(SpiceSession *session,
+ const gchar *device_format_string)
+{
+ return g_object_new(SPICE_TYPE_USB_DEVICE_WIDGET,
+ "orientation", GTK_ORIENTATION_VERTICAL,
+ "session", session,
+ "device-format-string", device_format_string,
+ "spacing", 6,
+ NULL);
+}
+
+/* ------------------------------------------------------------------ */
+/* callbacks */
+
+static SpiceUsbDevice *get_usb_device(GtkWidget *widget)
+{
+ if (!GTK_IS_ALIGNMENT(widget))
+ return NULL;
+
+ widget = gtk_bin_get_child(GTK_BIN(widget));
+ return g_object_get_data(G_OBJECT(widget), "usb-device");
+}
+
+static void check_can_redirect(GtkWidget *widget, gpointer user_data)
+{
+ SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+ SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+ SpiceUsbDevice *device;
+ gboolean can_redirect;
+ GError *err = NULL;
+
+ device = get_usb_device(widget);
+ if (!device)
+ return; /* Non device widget, ie the info_bar */
+
+ priv->device_count++;
+ can_redirect = spice_usb_device_manager_can_redirect_device(priv->manager,
+ device, &err);
+ gtk_widget_set_sensitive(widget, can_redirect);
+
+ /* If we cannot redirect this device, append the error message to
+ err_msg, but only if it is *not* already there! */
+ if (!can_redirect) {
+ if (priv->err_msg) {
+ if (!strstr(priv->err_msg, err->message)) {
+ gchar *old_err_msg = priv->err_msg;
+
+ priv->err_msg = g_strdup_printf("%s\n%s", priv->err_msg,
+ err->message);
+ g_free(old_err_msg);
+ }
+ } else {
+ priv->err_msg = g_strdup(err->message);
+ }
+ }
+
+ g_clear_error(&err);
+}
+
+static gboolean spice_usb_device_widget_update_status(gpointer user_data)
+{
+ SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+ SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+
+ priv->device_count = 0;
+ gtk_container_foreach(GTK_CONTAINER(self), check_can_redirect, self);
+
+ if (priv->err_msg) {
+ spice_usb_device_widget_show_info_bar(self, priv->err_msg,
+ GTK_MESSAGE_INFO,
+ GTK_STOCK_DIALOG_WARNING);
+ g_free(priv->err_msg);
+ priv->err_msg = NULL;
+ } else {
+ spice_usb_device_widget_hide_info_bar(self);
+ }
+
+ if (priv->device_count == 0)
+ spice_usb_device_widget_show_info_bar(self, _("No USB devices detected"),
+ GTK_MESSAGE_INFO,
+ GTK_STOCK_DIALOG_INFO);
+ return FALSE;
+}
+
+typedef struct _connect_cb_data {
+ GtkWidget *check;
+ SpiceUsbDeviceWidget *self;
+} connect_cb_data;
+
+static void connect_cb(GObject *gobject, GAsyncResult *res, gpointer user_data)
+{
+ SpiceUsbDeviceManager *manager = SPICE_USB_DEVICE_MANAGER(gobject);
+ connect_cb_data *data = user_data;
+ SpiceUsbDeviceWidget *self = data->self;
+ SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+ SpiceUsbDevice *device;
+ GError *err = NULL;
+ gchar *desc;
+
+ spice_usb_device_manager_connect_device_finish(manager, res, &err);
+ if (err) {
+ device = g_object_get_data(G_OBJECT(data->check), "usb-device");
+ desc = spice_usb_device_get_description(device,
+ priv->device_format_string);
+ g_prefix_error(&err, "Could not redirect %s: ", desc);
+ g_free(desc);
+
+ SPICE_DEBUG("%s", err->message);
+ g_signal_emit(self, signals[CONNECT_FAILED], 0, device, err);
+ g_error_free(err);
+
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(data->check), FALSE);
+ spice_usb_device_widget_update_status(self);
+ }
+
+ g_object_unref(data->check);
+ g_object_unref(data->self);
+ g_free(data);
+}
+
+static void checkbox_clicked_cb(GtkWidget *check, gpointer user_data)
+{
+ SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+ SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+ SpiceUsbDevice *device;
+
+ device = g_object_get_data(G_OBJECT(check), "usb-device");
+
+ if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(check))) {
+ connect_cb_data *data = g_new(connect_cb_data, 1);
+ data->check = g_object_ref(check);
+ data->self = g_object_ref(self);
+ spice_usb_device_manager_connect_device_async(priv->manager,
+ device,
+ NULL,
+ connect_cb,
+ data);
+ } else {
+ spice_usb_device_manager_disconnect_device(priv->manager,
+ device);
+ }
+ spice_usb_device_widget_update_status(self);
+}
+
+static void checkbox_usb_device_destroy_notify(gpointer data)
+{
+ g_boxed_free(spice_usb_device_get_type(), data);
+}
+
+static void device_added_cb(SpiceUsbDeviceManager *manager,
+ SpiceUsbDevice *device, gpointer user_data)
+{
+ SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+ SpiceUsbDeviceWidgetPrivate *priv = self->priv;
+ GtkWidget *align, *check;
+ gchar *desc;
+
+ desc = spice_usb_device_get_description(device,
+ priv->device_format_string);
+ check = gtk_check_button_new_with_label(desc);
+ g_free(desc);
+
+ if (spice_usb_device_manager_is_device_connected(priv->manager,
+ device))
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), TRUE);
+
+ g_object_set_data_full(
+ G_OBJECT(check), "usb-device",
+ g_boxed_copy(spice_usb_device_get_type(), device),
+ checkbox_usb_device_destroy_notify);
+ g_signal_connect(G_OBJECT(check), "clicked",
+ G_CALLBACK(checkbox_clicked_cb), self);
+
+ align = gtk_alignment_new(0, 0, 0, 0);
+ gtk_alignment_set_padding(GTK_ALIGNMENT(align), 0, 0, 12, 0);
+ gtk_container_add(GTK_CONTAINER(align), check);
+ gtk_box_pack_end(GTK_BOX(self), align, FALSE, FALSE, 0);
+ spice_usb_device_widget_update_status(self);
+ gtk_widget_show_all(align);
+}
+
+static void destroy_widget_by_usb_device(GtkWidget *widget, gpointer user_data)
+{
+ if (get_usb_device(widget) == user_data)
+ gtk_widget_destroy(widget);
+}
+
+static void device_removed_cb(SpiceUsbDeviceManager *manager,
+ SpiceUsbDevice *device, gpointer user_data)
+{
+ SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+
+ gtk_container_foreach(GTK_CONTAINER(self),
+ destroy_widget_by_usb_device, device);
+
+ spice_usb_device_widget_update_status(self);
+}
+
+static void set_inactive_by_usb_device(GtkWidget *widget, gpointer user_data)
+{
+ if (get_usb_device(widget) == user_data) {
+ GtkWidget *check = gtk_bin_get_child(GTK_BIN(widget));
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check), FALSE);
+ }
+}
+
+static void device_error_cb(SpiceUsbDeviceManager *manager,
+ SpiceUsbDevice *device, GError *err, gpointer user_data)
+{
+ SpiceUsbDeviceWidget *self = SPICE_USB_DEVICE_WIDGET(user_data);
+
+ gtk_container_foreach(GTK_CONTAINER(self),
+ set_inactive_by_usb_device, device);
+
+ spice_usb_device_widget_update_status(self);
+}
diff --git a/src/usb-device-widget.h b/src/usb-device-widget.h
new file mode 100644
index 0000000..b68cc6b
--- /dev/null
+++ b/src/usb-device-widget.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 Red Hat, Inc.
+
+ Red Hat Authors:
+ Hans de Goede <hdegoede@redhat.com>
+
+ 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_USB_DEVICE_WIDGET_H__
+#define __SPICE_USB_DEVICE_WIDGET_H__
+
+#include <gtk/gtk.h>
+#include "spice-client.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_USB_DEVICE_WIDGET (spice_usb_device_widget_get_type ())
+#define SPICE_USB_DEVICE_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SPICE_TYPE_USB_DEVICE_WIDGET, SpiceUsbDeviceWidget))
+#define SPICE_USB_DEVICE_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SPICE_TYPE_USB_DEVICE_WIDGET, SpiceUsbDeviceWidgetClass))
+#define SPICE_IS_USB_DEVICE_WIDGET(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SPICE_TYPE_USB_DEVICE_WIDGET))
+#define SPICE_IS_USB_DEVICE_WIDGET_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SPICE_TYPE_USB_DEVICE_WIDGET))
+#define SPICE_USB_DEVICE_WIDGET_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SPICE_TYPE_USB_DEVICE_WIDGET, SpiceUsbDeviceWidgetClass))
+
+typedef struct _SpiceUsbDeviceWidget SpiceUsbDeviceWidget;
+typedef struct _SpiceUsbDeviceWidgetClass SpiceUsbDeviceWidgetClass;
+typedef struct _SpiceUsbDeviceWidgetPrivate SpiceUsbDeviceWidgetPrivate;
+
+/**
+ * SpiceUsbDeviceWidget:
+ *
+ * The #SpiceUsbDeviceWidget struct is opaque and should not be accessed directly.
+ */
+struct _SpiceUsbDeviceWidget
+{
+ GtkVBox parent;
+
+ /*< private >*/
+ SpiceUsbDeviceWidgetPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+/**
+ * SpiceUsbDeviceWidgetClass:
+ * @connect_failed: Signal class handler for the #SpiceUsbDeviceWidget::connect-failed signal.
+ *
+ * Class structure for #SpiceUsbDeviceWidget.
+ */
+struct _SpiceUsbDeviceWidgetClass
+{
+ GtkVBoxClass parent_class;
+
+ /* signals */
+ void (*connect_failed) (SpiceUsbDeviceWidget *widget,
+ SpiceUsbDevice *device, GError *error);
+ /*< private >*/
+ /*
+ * If adding fields to this struct, remove corresponding
+ * amount of padding to avoid changing overall struct size
+ */
+ gchar _spice_reserved[SPICE_RESERVED_PADDING];
+};
+
+GType spice_usb_device_widget_get_type(void);
+GtkWidget *spice_usb_device_widget_new(SpiceSession *session,
+ const gchar *device_format_string);
+
+G_END_DECLS
+
+#endif /* __SPICE_USB_DEVICE_WIDGET_H__ */
diff --git a/src/usbutil.c b/src/usbutil.c
new file mode 100644
index 0000000..16d757b
--- /dev/null
+++ b/src/usbutil.c
@@ -0,0 +1,323 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 Red Hat, Inc.
+
+ Red Hat Authors:
+ Hans de Goede <hdegoede@redhat.com>
+
+ 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-object.h>
+#include <glib/gi18n.h>
+#include <ctype.h>
+#include <stdlib.h>
+
+#include "glib-compat.h"
+
+#ifdef USE_USBREDIR
+#ifdef __linux__
+#include <stdio.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#endif
+#include "usbutil.h"
+#include "spice-util-priv.h"
+
+#define VENDOR_NAME_LEN (122 - sizeof(void *))
+#define PRODUCT_NAME_LEN 126
+
+typedef struct _usb_product_info {
+ guint16 product_id;
+ char name[PRODUCT_NAME_LEN];
+} usb_product_info;
+
+typedef struct _usb_vendor_info {
+ usb_product_info *product_info;
+ int product_count;
+ guint16 vendor_id;
+ char name[VENDOR_NAME_LEN];
+} usb_vendor_info;
+
+static GStaticMutex usbids_load_mutex = G_STATIC_MUTEX_INIT;
+static int usbids_vendor_count = 0; /* < 0: failed, 0: empty, > 0: loaded */
+static usb_vendor_info *usbids_vendor_info = NULL;
+
+G_GNUC_INTERNAL
+const char *spice_usbutil_libusb_strerror(enum libusb_error error_code)
+{
+ switch (error_code) {
+ case LIBUSB_SUCCESS:
+ return "Success";
+ case LIBUSB_ERROR_IO:
+ return "Input/output error";
+ case LIBUSB_ERROR_INVALID_PARAM:
+ return "Invalid parameter";
+ case LIBUSB_ERROR_ACCESS:
+ return "Access denied (insufficient permissions)";
+ case LIBUSB_ERROR_NO_DEVICE:
+ return "No such device (it may have been disconnected)";
+ case LIBUSB_ERROR_NOT_FOUND:
+ return "Entity not found";
+ case LIBUSB_ERROR_BUSY:
+ return "Resource busy";
+ case LIBUSB_ERROR_TIMEOUT:
+ return "Operation timed out";
+ case LIBUSB_ERROR_OVERFLOW:
+ return "Overflow";
+ case LIBUSB_ERROR_PIPE:
+ return "Pipe error";
+ case LIBUSB_ERROR_INTERRUPTED:
+ return "System call interrupted (perhaps due to signal)";
+ case LIBUSB_ERROR_NO_MEM:
+ return "Insufficient memory";
+ case LIBUSB_ERROR_NOT_SUPPORTED:
+ return "Operation not supported or unimplemented on this platform";
+ case LIBUSB_ERROR_OTHER:
+ return "Other error";
+ }
+ return "Unknown error";
+}
+
+#ifdef __linux__
+/* <Sigh> libusb does not allow getting the manufacturer and product strings
+ without opening the device, so grab them directly from sysfs */
+static gchar *spice_usbutil_get_sysfs_attribute(int bus, int address,
+ const char *attribute)
+{
+ struct stat stat_buf;
+ char filename[256];
+ gchar *contents;
+
+ snprintf(filename, sizeof(filename), "/dev/bus/usb/%03d/%03d",
+ bus, address);
+ if (stat(filename, &stat_buf) != 0)
+ return NULL;
+
+ snprintf(filename, sizeof(filename), "/sys/dev/char/%d:%d/%s",
+ major(stat_buf.st_rdev), minor(stat_buf.st_rdev), attribute);
+ if (!g_file_get_contents(filename, &contents, NULL, NULL))
+ return NULL;
+
+ /* Remove the newline at the end */
+ contents[strlen(contents) - 1] = '\0';
+
+ return contents;
+}
+#endif
+
+static gboolean spice_usbutil_parse_usbids(gchar *path)
+{
+ gchar *contents, *line, **lines;
+ usb_product_info *product_info;
+ int i, j, id, product_count = 0;
+
+ usbids_vendor_count = 0;
+ if (!g_file_get_contents(path, &contents, NULL, NULL)) {
+ usbids_vendor_count = -1;
+ return FALSE;
+ }
+
+ lines = g_strsplit(contents, "\n", -1);
+
+ for (i = 0; lines[i]; i++) {
+ if (!isxdigit(lines[i][0]) || !isxdigit(lines[i][1]))
+ continue;
+
+ for (j = 1; lines[i + j] &&
+ (lines[i + j][0] == '\t' ||
+ lines[i + j][0] == '#' ||
+ lines[i + j][0] == '\0'); j++) {
+ if (lines[i + j][0] == '\t' && isxdigit(lines[i + j][1]))
+ product_count++;
+ }
+ i += j - 1;
+
+ usbids_vendor_count++;
+ }
+
+ usbids_vendor_info = g_new(usb_vendor_info, usbids_vendor_count);
+ product_info = g_new(usb_product_info, product_count);
+
+ usbids_vendor_count = 0;
+ for (i = 0; lines[i]; i++) {
+ line = lines[i];
+
+ if (!isxdigit(line[0]) || !isxdigit(line[1]))
+ continue;
+
+ id = strtoul(line, &line, 16);
+ while (isspace(line[0]))
+ line++;
+
+ usbids_vendor_info[usbids_vendor_count].vendor_id = id;
+ snprintf(usbids_vendor_info[usbids_vendor_count].name,
+ VENDOR_NAME_LEN, "%s", line);
+
+ product_count = 0;
+ for (j = 1; lines[i + j] &&
+ (lines[i + j][0] == '\t' ||
+ lines[i + j][0] == '#' ||
+ lines[i + j][0] == '\0'); j++) {
+ line = lines[i + j];
+
+ if (line[0] != '\t' || !isxdigit(line[1]))
+ continue;
+
+ id = strtoul(line + 1, &line, 16);
+ while (isspace(line[0]))
+ line++;
+ product_info[product_count].product_id = id;
+ snprintf(product_info[product_count].name,
+ PRODUCT_NAME_LEN, "%s", line);
+
+ product_count++;
+ }
+ i += j - 1;
+
+ usbids_vendor_info[usbids_vendor_count].product_count = product_count;
+ usbids_vendor_info[usbids_vendor_count].product_info = product_info;
+ product_info += product_count;
+ usbids_vendor_count++;
+ }
+
+ g_strfreev(lines);
+ g_free(contents);
+
+#if 0 /* Testing only */
+ for (i = 0; i < usbids_vendor_count; i++) {
+ printf("%04x %s\n", usbids_vendor_info[i].vendor_id,
+ usbids_vendor_info[i].name);
+ product_info = usbids_vendor_info[i].product_info;
+ for (j = 0; j < usbids_vendor_info[i].product_count; j++) {
+ printf("\t%04x %s\n", product_info[j].product_id,
+ product_info[j].name);
+ }
+ }
+#endif
+
+ return TRUE;
+}
+
+static gboolean spice_usbutil_load_usbids(void)
+{
+ gboolean success = FALSE;
+
+ g_static_mutex_lock(&usbids_load_mutex);
+ if (usbids_vendor_count) {
+ success = usbids_vendor_count > 0;
+ goto leave;
+ }
+
+#ifdef WITH_USBIDS
+ success = spice_usbutil_parse_usbids(USB_IDS);
+#else
+ {
+ const gchar * const *dirs = g_get_system_data_dirs();
+ gchar *path = NULL;
+ int i;
+
+ for (i = 0; dirs[i]; ++i) {
+ path = g_build_filename(dirs[i], "hwdata", "usb.ids", NULL);
+ success = spice_usbutil_parse_usbids(path);
+ SPICE_DEBUG("loading %s success: %s", path, spice_yes_no(success));
+ g_free(path);
+
+ if (success)
+ goto leave;
+ }
+ }
+#endif
+
+leave:
+ g_static_mutex_unlock(&usbids_load_mutex);
+ return success;
+}
+
+G_GNUC_INTERNAL
+void spice_usb_util_get_device_strings(int bus, int address,
+ int vendor_id, int product_id,
+ gchar **manufacturer, gchar **product)
+{
+ usb_product_info *product_info;
+ int i, j;
+
+ g_return_if_fail(manufacturer != NULL);
+ g_return_if_fail(product != NULL);
+
+ *manufacturer = NULL;
+ *product = NULL;
+
+#if __linux__
+ *manufacturer = spice_usbutil_get_sysfs_attribute(bus, address, "manufacturer");
+ *product = spice_usbutil_get_sysfs_attribute(bus, address, "product");
+#endif
+
+ if ((!*manufacturer || !*product) &&
+ spice_usbutil_load_usbids()) {
+
+ for (i = 0; i < usbids_vendor_count; i++) {
+ if ((int)usbids_vendor_info[i].vendor_id != vendor_id)
+ continue;
+
+ if (!*manufacturer && usbids_vendor_info[i].name[0])
+ *manufacturer = g_strdup(usbids_vendor_info[i].name);
+
+ product_info = usbids_vendor_info[i].product_info;
+ for (j = 0; j < usbids_vendor_info[i].product_count; j++) {
+ if ((int)product_info[j].product_id != product_id)
+ continue;
+
+ if (!*product && product_info[j].name[0])
+ *product = g_strdup(product_info[j].name);
+
+ break;
+ }
+ break;
+ }
+ }
+
+ if (!*manufacturer)
+ *manufacturer = g_strdup(_("USB"));
+ if (!*product)
+ *product = g_strdup(_("Device"));
+
+ /* Some devices have unwanted whitespace in their strings */
+ g_strstrip(*manufacturer);
+ g_strstrip(*product);
+
+ /* Some devices repeat the manufacturer at the beginning of product */
+ if (g_str_has_prefix(*product, *manufacturer) &&
+ strlen(*product) > strlen(*manufacturer)) {
+ gchar *tmp = g_strdup(*product + strlen(*manufacturer));
+ g_free(*product);
+ *product = tmp;
+ g_strstrip(*product);
+ }
+}
+
+#endif
+
+#ifdef USBUTIL_TEST
+int main()
+{
+ if (spice_usbutil_load_usbids())
+ exit(0);
+
+ exit(1);
+}
+#endif
diff --git a/src/usbutil.h b/src/usbutil.h
new file mode 100644
index 0000000..de5e92a
--- /dev/null
+++ b/src/usbutil.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 Red Hat, Inc.
+
+ Red Hat Authors:
+ Hans de Goede <hdegoede@redhat.com>
+
+ 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_USBUTIL_H__
+#define __SPICE_USBUTIL_H__
+
+#include <glib.h>
+
+#ifdef USE_USBREDIR
+#include <libusb.h>
+
+G_BEGIN_DECLS
+
+const char *spice_usbutil_libusb_strerror(enum libusb_error error_code);
+void spice_usb_util_get_device_strings(int bus, int address,
+ int vendor_id, int product_id,
+ gchar **manufacturer, gchar **product);
+
+G_END_DECLS
+
+#endif /* USE_USBREDIR */
+#endif /* __SPICE_USBUTIL_H__ */
diff --git a/src/vmcstream.c b/src/vmcstream.c
new file mode 100644
index 0000000..483dd5a
--- /dev/null
+++ b/src/vmcstream.c
@@ -0,0 +1,535 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2013 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 <string.h>
+
+#include "vmcstream.h"
+#include "spice-channel-priv.h"
+#include "gio-coroutine.h"
+#include "glib-compat.h"
+
+struct _SpiceVmcInputStream
+{
+ GInputStream parent_instance;
+ GSimpleAsyncResult *result;
+ struct coroutine *coroutine;
+
+ SpiceChannel *channel;
+ gboolean all;
+ guint8 *buffer;
+ gsize count;
+ gsize pos;
+
+ GCancellable *cancellable;
+ gulong cancel_id;
+};
+
+struct _SpiceVmcInputStreamClass
+{
+ GInputStreamClass parent_class;
+};
+
+static gssize spice_vmc_input_stream_read (GInputStream *stream,
+ void *buffer,
+ gsize count,
+ GCancellable *cancellable,
+ GError **error);
+static void spice_vmc_input_stream_read_async (GInputStream *stream,
+ void *buffer,
+ gsize count,
+ int io_priority,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+static gssize spice_vmc_input_stream_read_finish (GInputStream *stream,
+ GAsyncResult *result,
+ GError **error);
+static gssize spice_vmc_input_stream_skip (GInputStream *stream,
+ gsize count,
+ GCancellable *cancellable,
+ GError **error);
+static gboolean spice_vmc_input_stream_close (GInputStream *stream,
+ GCancellable *cancellable,
+ GError **error);
+
+G_DEFINE_TYPE(SpiceVmcInputStream, spice_vmc_input_stream, G_TYPE_INPUT_STREAM)
+
+
+static void
+spice_vmc_input_stream_class_init(SpiceVmcInputStreamClass *klass)
+{
+ GInputStreamClass *istream_class;
+
+ istream_class = G_INPUT_STREAM_CLASS(klass);
+ istream_class->read_fn = spice_vmc_input_stream_read;
+ istream_class->read_async = spice_vmc_input_stream_read_async;
+ istream_class->read_finish = spice_vmc_input_stream_read_finish;
+ istream_class->skip = spice_vmc_input_stream_skip;
+ istream_class->close_fn = spice_vmc_input_stream_close;
+}
+
+static void
+spice_vmc_input_stream_init(SpiceVmcInputStream *self)
+{
+}
+
+static SpiceVmcInputStream *
+spice_vmc_input_stream_new(void)
+{
+ SpiceVmcInputStream *self;
+
+ self = g_object_new(SPICE_TYPE_VMC_INPUT_STREAM, NULL);
+
+ return self;
+}
+
+/* coroutine */
+/**
+ * Feed a SpiceVmc stream with new data from a coroutine
+ *
+ * The other end will be waiting on read_async() until data is fed
+ * here.
+ */
+G_GNUC_INTERNAL void
+spice_vmc_input_stream_co_data(SpiceVmcInputStream *self,
+ const gpointer d, gsize size)
+{
+ guint8 *data = d;
+
+ g_return_if_fail(SPICE_IS_VMC_INPUT_STREAM(self));
+ g_return_if_fail(self->coroutine == NULL);
+
+ self->coroutine = coroutine_self();
+
+ while (size > 0) {
+ SPICE_DEBUG("spicevmc co_data %p", self->result);
+ if (!self->result)
+ coroutine_yield(NULL);
+
+ g_return_if_fail(self->result != NULL);
+
+ gsize min = MIN(self->count, size);
+ memcpy(self->buffer, data, min);
+
+ size -= min;
+ data += min;
+
+ SPICE_DEBUG("spicevmc co_data complete: %" G_GSIZE_FORMAT
+ "/%" G_GSIZE_FORMAT, min, self->count);
+
+ self->pos += min;
+ self->buffer += min;
+
+ if (self->all && min > 0 && self->pos != self->count)
+ continue;
+
+ g_simple_async_result_set_op_res_gssize(self->result, self->pos);
+
+ g_simple_async_result_complete_in_idle(self->result);
+ g_clear_object(&self->result);
+ if (self->cancellable) {
+ g_cancellable_disconnect(self->cancellable, self->cancel_id);
+ g_clear_object(&self->cancellable);
+ }
+ }
+
+ self->coroutine = NULL;
+}
+
+static void
+read_cancelled(GCancellable *cancellable,
+ gpointer user_data)
+{
+ SpiceVmcInputStream *self = SPICE_VMC_INPUT_STREAM(user_data);
+
+ SPICE_DEBUG("read cancelled, %p", self->result);
+ g_simple_async_result_set_error(self->result,
+ G_IO_ERROR, G_IO_ERROR_CANCELLED,
+ "read cancelled");
+ g_simple_async_result_complete_in_idle(self->result);
+
+ g_clear_object(&self->result);
+
+ /* See FIXME */
+ /* if (self->cancellable) { */
+ /* g_cancellable_disconnect(self->cancellable, self->cancel_id); */
+ /* g_clear_object(&self->cancellable); */
+ /* } */
+}
+
+G_GNUC_INTERNAL void
+spice_vmc_input_stream_read_all_async(GInputStream *stream,
+ void *buffer,
+ gsize count,
+ int io_priority,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SpiceVmcInputStream *self = SPICE_VMC_INPUT_STREAM(stream);
+ GSimpleAsyncResult *result;
+
+ /* no concurrent read permitted by ginputstream */
+ g_return_if_fail(self->result == NULL);
+ g_return_if_fail(self->cancellable == NULL);
+ self->all = TRUE;
+ self->buffer = buffer;
+ self->count = count;
+ self->pos = 0;
+ result = g_simple_async_result_new(G_OBJECT(self),
+ callback,
+ user_data,
+ spice_vmc_input_stream_read_async);
+ self->result = result;
+ self->cancellable = g_object_ref(cancellable);
+ if (cancellable)
+ self->cancel_id =
+ g_cancellable_connect(cancellable, G_CALLBACK(read_cancelled), self, NULL);
+
+ if (self->coroutine)
+ coroutine_yieldto(self->coroutine, NULL);
+}
+
+G_GNUC_INTERNAL gssize
+spice_vmc_input_stream_read_all_finish(GInputStream *stream,
+ GAsyncResult *result,
+ GError **error)
+{
+ GSimpleAsyncResult *simple;
+ SpiceVmcInputStream *self = SPICE_VMC_INPUT_STREAM(stream);
+
+ g_return_val_if_fail(g_simple_async_result_is_valid(result,
+ G_OBJECT(self),
+ spice_vmc_input_stream_read_async),
+ -1);
+
+ simple = (GSimpleAsyncResult *)result;
+
+ /* FIXME: calling _finish() is required. Disconnecting in
+ read_cancelled() causes a deadlock. #705395 */
+ if (self->cancellable) {
+ g_cancellable_disconnect(self->cancellable, self->cancel_id);
+ g_clear_object(&self->cancellable);
+ }
+
+ if (g_simple_async_result_propagate_error(simple, error))
+ return -1;
+
+ return g_simple_async_result_get_op_res_gssize(simple);
+}
+
+static void
+spice_vmc_input_stream_read_async(GInputStream *stream,
+ void *buffer,
+ gsize count,
+ int io_priority,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SpiceVmcInputStream *self = SPICE_VMC_INPUT_STREAM(stream);
+ GSimpleAsyncResult *result;
+
+ /* no concurrent read permitted by ginputstream */
+ g_return_if_fail(self->result == NULL);
+ g_return_if_fail(self->cancellable == NULL);
+ self->all = FALSE;
+ self->buffer = buffer;
+ self->count = count;
+ self->pos = 0;
+ result = g_simple_async_result_new(G_OBJECT(self),
+ callback,
+ user_data,
+ spice_vmc_input_stream_read_async);
+ self->result = result;
+ self->cancellable = g_object_ref(cancellable);
+ if (cancellable)
+ self->cancel_id =
+ g_cancellable_connect(cancellable, G_CALLBACK(read_cancelled), self, NULL);
+
+ if (self->coroutine)
+ coroutine_yieldto(self->coroutine, NULL);
+}
+
+static gssize
+spice_vmc_input_stream_read_finish(GInputStream *stream,
+ GAsyncResult *result,
+ GError **error)
+{
+ GSimpleAsyncResult *simple;
+ SpiceVmcInputStream *self = SPICE_VMC_INPUT_STREAM(stream);
+
+ g_return_val_if_fail(g_simple_async_result_is_valid(result,
+ G_OBJECT(self),
+ spice_vmc_input_stream_read_async),
+ -1);
+
+ simple = (GSimpleAsyncResult *)result;
+
+ /* FIXME: calling _finish() is required. Disconnecting in
+ read_cancelled() causes a deadlock. #705395 */
+ if (self->cancellable) {
+ g_cancellable_disconnect(self->cancellable, self->cancel_id);
+ g_clear_object(&self->cancellable);
+ }
+
+ if (g_simple_async_result_propagate_error(simple, error))
+ return -1;
+
+ return g_simple_async_result_get_op_res_gssize(simple);
+}
+
+static gssize
+spice_vmc_input_stream_read(GInputStream *stream,
+ void *buffer,
+ gsize count,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_reached(-1);
+}
+
+static gssize
+spice_vmc_input_stream_skip(GInputStream *stream,
+ gsize count,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_return_val_if_reached(-1);
+}
+
+static gboolean
+spice_vmc_input_stream_close(GInputStream *stream,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SPICE_DEBUG("fake close");
+ return TRUE;
+}
+
+/* OUTPUT */
+
+struct _SpiceVmcOutputStream
+{
+ GOutputStream parent_instance;
+
+ SpiceChannel *channel; /* weak */
+};
+
+struct _SpiceVmcOutputStreamClass
+{
+ GOutputStreamClass parent_class;
+};
+
+static gssize spice_vmc_output_stream_write_fn (GOutputStream *stream,
+ const void *buffer,
+ gsize count,
+ GCancellable *cancellable,
+ GError **error);
+static gssize spice_vmc_output_stream_write_finish (GOutputStream *stream,
+ GAsyncResult *result,
+ GError **error);
+static void spice_vmc_output_stream_write_async (GOutputStream *stream,
+ const void *buffer,
+ gsize count,
+ int io_priority,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+
+G_DEFINE_TYPE(SpiceVmcOutputStream, spice_vmc_output_stream, G_TYPE_OUTPUT_STREAM)
+
+
+static void
+spice_vmc_output_stream_class_init(SpiceVmcOutputStreamClass *klass)
+{
+ GOutputStreamClass *ostream_class;
+
+ ostream_class = G_OUTPUT_STREAM_CLASS(klass);
+ ostream_class->write_fn = spice_vmc_output_stream_write_fn;
+ ostream_class->write_async = spice_vmc_output_stream_write_async;
+ ostream_class->write_finish = spice_vmc_output_stream_write_finish;
+}
+
+static void
+spice_vmc_output_stream_init(SpiceVmcOutputStream *self)
+{
+}
+
+static SpiceVmcOutputStream *
+spice_vmc_output_stream_new(SpiceChannel *channel)
+{
+ SpiceVmcOutputStream *self;
+
+ self = g_object_new(SPICE_TYPE_VMC_OUTPUT_STREAM, NULL);
+ self->channel = channel;
+
+ return self;
+}
+
+static gssize
+spice_vmc_output_stream_write_fn(GOutputStream *stream,
+ const void *buffer,
+ gsize count,
+ GCancellable *cancellable,
+ GError **error)
+{
+ SpiceVmcOutputStream *self = SPICE_VMC_OUTPUT_STREAM(stream);
+ SpiceMsgOut *msg_out;
+
+ msg_out = spice_msg_out_new(SPICE_CHANNEL(self->channel),
+ SPICE_MSGC_SPICEVMC_DATA);
+ spice_marshaller_add(msg_out->marshaller, buffer, count);
+ spice_msg_out_send(msg_out);
+
+ return count;
+}
+
+static gssize
+spice_vmc_output_stream_write_finish(GOutputStream *stream,
+ GAsyncResult *simple,
+ GError **error)
+{
+ SpiceVmcOutputStream *self = SPICE_VMC_OUTPUT_STREAM(stream);
+ GSimpleAsyncResult *res =
+ g_simple_async_result_get_op_res_gpointer(G_SIMPLE_ASYNC_RESULT(simple));
+
+ SPICE_DEBUG("spicevmc write finish");
+ return spice_vmc_write_finish(self->channel, G_ASYNC_RESULT(res), error);
+}
+
+static void
+write_cb(GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GSimpleAsyncResult *simple = user_data;
+
+ g_simple_async_result_set_op_res_gpointer(simple, res, NULL);
+
+ g_simple_async_result_complete(simple);
+ g_object_unref(simple);
+}
+
+static void
+spice_vmc_output_stream_write_async(GOutputStream *stream,
+ const void *buffer,
+ gsize count,
+ int io_priority,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SpiceVmcOutputStream *self = SPICE_VMC_OUTPUT_STREAM(stream);
+ GSimpleAsyncResult *simple;
+
+ SPICE_DEBUG("spicevmc write async");
+ /* an AsyncResult to forward async op to channel */
+ simple = g_simple_async_result_new(G_OBJECT(self), callback, user_data,
+ spice_vmc_output_stream_write_async);
+
+ spice_vmc_write_async(self->channel, buffer, count,
+ cancellable, write_cb,
+ simple);
+}
+
+/* STREAM */
+
+struct _SpiceVmcStream
+{
+ GIOStream parent_instance;
+
+ SpiceChannel *channel; /* weak */
+ SpiceVmcInputStream *in;
+ SpiceVmcOutputStream *out;
+};
+
+struct _SpiceVmcStreamClass
+{
+ GIOStreamClass parent_class;
+};
+
+static void spice_vmc_stream_finalize (GObject *object);
+static GInputStream * spice_vmc_stream_get_input_stream (GIOStream *stream);
+static GOutputStream * spice_vmc_stream_get_output_stream (GIOStream *stream);
+
+G_DEFINE_TYPE(SpiceVmcStream, spice_vmc_stream, G_TYPE_IO_STREAM)
+
+static void
+spice_vmc_stream_class_init(SpiceVmcStreamClass *klass)
+{
+ GObjectClass *object_class;
+ GIOStreamClass *iostream_class;
+
+ object_class = G_OBJECT_CLASS(klass);
+ object_class->finalize = spice_vmc_stream_finalize;
+
+ iostream_class = G_IO_STREAM_CLASS(klass);
+ iostream_class->get_input_stream = spice_vmc_stream_get_input_stream;
+ iostream_class->get_output_stream = spice_vmc_stream_get_output_stream;
+}
+
+static void
+spice_vmc_stream_finalize(GObject *object)
+{
+ SpiceVmcStream *self = SPICE_VMC_STREAM(object);
+
+ g_clear_object(&self->in);
+ g_clear_object(&self->out);
+
+ G_OBJECT_CLASS(spice_vmc_stream_parent_class)->finalize(object);
+}
+
+static void
+spice_vmc_stream_init(SpiceVmcStream *self)
+{
+}
+
+G_GNUC_INTERNAL SpiceVmcStream *
+spice_vmc_stream_new(SpiceChannel *channel)
+{
+ SpiceVmcStream *self;
+
+ self = g_object_new(SPICE_TYPE_VMC_STREAM, NULL);
+ self->channel = channel;
+
+ return self;
+}
+
+static GInputStream *
+spice_vmc_stream_get_input_stream(GIOStream *stream)
+{
+ SpiceVmcStream *self = SPICE_VMC_STREAM(stream);
+
+ if (!self->in)
+ self->in = spice_vmc_input_stream_new();
+
+ return G_INPUT_STREAM(self->in);
+}
+
+static GOutputStream *
+spice_vmc_stream_get_output_stream(GIOStream *stream)
+{
+ SpiceVmcStream *self = SPICE_VMC_STREAM(stream);
+
+ if (!self->out)
+ self->out = spice_vmc_output_stream_new(self->channel);
+
+ return G_OUTPUT_STREAM(self->out);
+}
diff --git a/src/vmcstream.h b/src/vmcstream.h
new file mode 100644
index 0000000..1316b77
--- /dev/null
+++ b/src/vmcstream.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2013 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_VMC_STREAM_H__
+#define __SPICE_VMC_STREAM_H__
+
+#include <gio/gio.h>
+
+#include "spice-types.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_VMC_INPUT_STREAM (spice_vmc_input_stream_get_type ())
+#define SPICE_VMC_INPUT_STREAM(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SPICE_TYPE_VMC_INPUT_STREAM, SpiceVmcInputStream))
+#define SPICE_VMC_INPUT_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), SPICE_TYPE_VMC_INPUT_STREAM, SpiceVmcInputStreamClass))
+#define SPICE_IS_VMC_INPUT_STREAM(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SPICE_TYPE_VMC_INPUT_STREAM))
+#define SPICE_IS_VMC_INPUT_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SPICE_TYPE_VMC_INPUT_STREAM))
+#define SPICE_VMC_INPUT_STREAM_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SPICE_TYPE_VMC_INPUT_STREAM, SpiceVmcInputStreamClass))
+
+typedef struct _SpiceVmcInputStreamClass SpiceVmcInputStreamClass;
+typedef struct _SpiceVmcInputStream SpiceVmcInputStream;
+
+GType spice_vmc_input_stream_get_type (void) G_GNUC_CONST;
+void spice_vmc_input_stream_co_data (SpiceVmcInputStream *input,
+ const gpointer data,
+ gsize size);
+
+void spice_vmc_input_stream_read_all_async(GInputStream *stream,
+ void *buffer,
+ gsize count,
+ int io_priority,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gssize spice_vmc_input_stream_read_all_finish(GInputStream *stream,
+ GAsyncResult *result,
+ GError **error);
+
+
+#define SPICE_TYPE_VMC_OUTPUT_STREAM (spice_vmc_output_stream_get_type ())
+#define SPICE_VMC_OUTPUT_STREAM(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SPICE_TYPE_VMC_OUTPUT_STREAM, SpiceVmcOutputStream))
+#define SPICE_VMC_OUTPUT_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), SPICE_TYPE_VMC_OUTPUT_STREAM, SpiceVmcOutputStreamClass))
+#define SPICE_IS_VMC_OUTPUT_STREAM(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SPICE_TYPE_VMC_OUTPUT_STREAM))
+#define SPICE_IS_VMC_OUTPUT_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SPICE_TYPE_VMC_OUTPUT_STREAM))
+#define SPICE_VMC_OUTPUT_STREAM_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SPICE_TYPE_VMC_OUTPUT_STREAM, SpiceVmcOutputStreamClass))
+
+typedef struct _SpiceVmcOutputStreamClass SpiceVmcOutputStreamClass;
+typedef struct _SpiceVmcOutputStream SpiceVmcOutputStream;
+
+GType spice_vmc_output_stream_get_type (void) G_GNUC_CONST;
+
+#define SPICE_TYPE_VMC_STREAM (spice_vmc_stream_get_type ())
+#define SPICE_VMC_STREAM(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), SPICE_TYPE_VMC_STREAM, SpiceVmcStream))
+#define SPICE_VMC_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), SPICE_TYPE_VMC_STREAM, SpiceVmcStreamClass))
+#define SPICE_IS_VMC_STREAM(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), SPICE_TYPE_VMC_STREAM))
+#define SPICE_IS_VMC_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), SPICE_TYPE_VMC_STREAM))
+#define SPICE_VMC_STREAM_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), SPICE_TYPE_VMC_STREAM, SpiceVmcStreamClass))
+
+typedef struct _SpiceVmcStreamClass SpiceVmcStreamClass;
+typedef struct _SpiceVmcStream SpiceVmcStream;
+
+GType spice_vmc_stream_get_type (void) G_GNUC_CONST;
+SpiceVmcStream* spice_vmc_stream_new (SpiceChannel *channel);
+
+G_END_DECLS
+
+#endif /* __SPICE_VMC_STREAM_H__ */
diff --git a/src/vncdisplaykeymap.c b/src/vncdisplaykeymap.c
new file mode 100644
index 0000000..6bf351f
--- /dev/null
+++ b/src/vncdisplaykeymap.c
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2008 Anthony Liguori <anthony@codemonkey.ws>
+ * Copyright (C) 2009-2010 Daniel P. Berrange <dan@berrange.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ */
+#include "config.h"
+
+#include <gtk/gtk.h>
+#include <gdk/gdk.h>
+#include <gdk/gdkkeysyms.h>
+#include "gtk-compat.h"
+#include "vncdisplaykeymap.h"
+
+#include "spice-util.h"
+
+#undef G_LOG_DOMAIN
+#define G_LOG_DOMAIN "vnc-keymap"
+#define VNC_DEBUG(message) SPICE_DEBUG(message);
+
+/*
+ * This table is taken from QEMU x_keymap.c, under the terms:
+ *
+ * Copyright (c) 2003 Fabrice Bellard
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+
+/* Compatability code to allow build on Gtk2 and Gtk3 */
+#ifndef GDK_Tab
+#define GDK_Tab GDK_KEY_Tab
+#endif
+
+/* keycode translation for sending ISO_Left_Send
+ * to vncserver
+ */
+static struct {
+ GdkKeymapKey *keys;
+ gint n_keys;
+ guint keyval;
+} untranslated_keys[] = {{NULL, 0, GDK_Tab}};
+
+static unsigned int ref_count_for_untranslated_keys = 0;
+
+#ifdef GDK_WINDOWING_WAYLAND
+#include <gdk/gdkwayland.h>
+#endif
+
+#ifdef GDK_WINDOWING_BROADWAY
+#include <gdk/gdkbroadway.h>
+#endif
+
+#if defined(GDK_WINDOWING_X11) || defined(GDK_WINDOWING_WAYLAND)
+/* Xorg Linux + evdev (offset evdev keycodes) */
+#include "vncdisplaykeymap_xorgevdev2xtkbd.c"
+#endif
+
+#ifdef GDK_WINDOWING_X11
+#include <gdk/gdkx.h>
+#include <X11/XKBlib.h>
+#include <stdbool.h>
+#include <string.h>
+
+/* Xorg Linux + kbd (offset + mangled XT keycodes) */
+#include "vncdisplaykeymap_xorgkbd2xtkbd.c"
+/* Xorg OS-X aka XQuartz (offset OS-X keycodes) */
+#include "vncdisplaykeymap_xorgxquartz2xtkbd.c"
+/* Xorg Cygwin aka XWin (offset + mangled XT keycodes) */
+#include "vncdisplaykeymap_xorgxwin2xtkbd.c"
+
+/* Gtk2 compat */
+#ifndef GDK_IS_X11_WINDOW
+#define GDK_IS_X11_WINDOW(win) (win == win)
+#endif
+#endif
+
+#ifdef GDK_WINDOWING_WIN32
+/* Win32 native virtual keycodes */
+#include "vncdisplaykeymap_win322xtkbd.c"
+
+/* Gtk2 compat */
+#ifndef GDK_IS_WIN32_WINDOW
+#define GDK_IS_WIN32_WINDOW(win) (win == win)
+#endif
+#endif
+
+#ifdef GDK_WINDOWING_QUARTZ
+/* OS-X native keycodes */
+#include "vncdisplaykeymap_osx2xtkbd.c"
+
+/* Gtk2 compat */
+#ifndef GDK_IS_QUARTZ_WINDOW
+#define GDK_IS_QUARTZ_WINDOW(win) (win == win)
+#endif
+#endif
+
+#ifdef GDK_WINDOWING_BROADWAY
+/* X11 keysyms */
+#include "vncdisplaykeymap_x112xtkbd.c"
+
+/* Gtk2 compat */
+#ifndef GDK_IS_BROADWAY_WINDOW
+#define GDK_IS_BROADWAY_WINDOW(win) (win == win)
+#endif
+
+#endif
+
+#ifdef GDK_WINDOWING_X11
+
+#define STRPREFIX(a,b) (strncmp((a),(b),strlen((b))) == 0)
+
+static gboolean check_for_xwin(GdkDisplay *dpy)
+{
+ char *vendor = ServerVendor(gdk_x11_display_get_xdisplay(dpy));
+
+ if (strstr(vendor, "Cygwin/X"))
+ return TRUE;
+
+ return FALSE;
+}
+
+static gboolean check_for_xquartz(GdkDisplay *dpy)
+{
+ int nextensions;
+ int i;
+ gboolean match = FALSE;
+ char **extensions = XListExtensions(gdk_x11_display_get_xdisplay(dpy),
+ &nextensions);
+ for (i = 0 ; extensions != NULL && i < nextensions ; i++) {
+ if (strcmp(extensions[i], "Apple-WM") == 0 ||
+ strcmp(extensions[i], "Apple-DRI") == 0)
+ match = TRUE;
+ }
+ if (extensions)
+ XFreeExtensionList(extensions);
+
+ return match;
+}
+#endif
+
+const guint16 *vnc_display_keymap_gdk2xtkbd_table(GdkWindow *window,
+ size_t *maplen)
+{
+#ifdef GDK_WINDOWING_X11
+ if (GDK_IS_X11_WINDOW(window)) {
+ XkbDescPtr desc;
+ const gchar *keycodes = NULL;
+ GdkDisplay *dpy = gdk_window_get_display(window);
+
+ /* There is no easy way to determine what X11 server
+ * and platform & keyboard driver is in use. Thus we
+ * do best guess heuristics.
+ *
+ * This will need more work for people with other
+ * X servers..... patches welcomed.
+ */
+
+ Display *xdisplay = gdk_x11_display_get_xdisplay(dpy);
+ desc = XkbGetMap(xdisplay,
+ XkbGBN_AllComponentsMask,
+ XkbUseCoreKbd);
+ if (desc) {
+ if (XkbGetNames(xdisplay, XkbKeycodesNameMask, desc) == Success) {
+ keycodes = gdk_x11_get_xatom_name(desc->names->keycodes);
+ if (!keycodes)
+ g_warning("could not lookup keycode name");
+ }
+ XkbFreeKeyboard(desc, XkbGBN_AllComponentsMask, True);
+ }
+
+ if (check_for_xwin(dpy)) {
+ VNC_DEBUG("Using xwin keycode mapping");
+ *maplen = G_N_ELEMENTS(keymap_xorgxwin2xtkbd);
+ return keymap_xorgxwin2xtkbd;
+ } else if (check_for_xquartz(dpy)) {
+ VNC_DEBUG("Using xquartz keycode mapping");
+ *maplen = G_N_ELEMENTS(keymap_xorgxquartz2xtkbd);
+ return keymap_xorgxquartz2xtkbd;
+ } else if (keycodes && STRPREFIX(keycodes, "evdev")) {
+ VNC_DEBUG("Using evdev keycode mapping");
+ *maplen = G_N_ELEMENTS(keymap_xorgevdev2xtkbd);
+ return keymap_xorgevdev2xtkbd;
+ } else if (keycodes && STRPREFIX(keycodes, "xfree86")) {
+ VNC_DEBUG("Using xfree86 keycode mapping");
+ *maplen = G_N_ELEMENTS(keymap_xorgkbd2xtkbd);
+ return keymap_xorgkbd2xtkbd;
+ } else {
+ g_warning("Unknown keycode mapping '%s'.\n"
+ "Please report to gtk-vnc-list@gnome.org\n"
+ "including the following information:\n"
+ "\n"
+ " - Operating system\n"
+ " - GDK build\n"
+ " - X11 Server\n"
+ " - xprop -root\n"
+ " - xdpyinfo\n",
+ keycodes);
+ return NULL;
+ }
+ }
+#endif
+
+#ifdef GDK_WINDOWING_WIN32
+ if (GDK_IS_WIN32_WINDOW(window)) {
+ VNC_DEBUG("Using Win32 virtual keycode mapping");
+ *maplen = G_N_ELEMENTS(keymap_win322xtkbd);
+ return keymap_win322xtkbd;
+ }
+#endif
+
+#ifdef GDK_WINDOWING_QUARTZ
+ if (GDK_IS_QUARTZ_WINDOW(window)) {
+ VNC_DEBUG("Using OS-X virtual keycode mapping");
+ *maplen = G_N_ELEMENTS(keymap_osx2xtkbd);
+ return keymap_osx2xtkbd;
+ }
+#endif
+
+#ifdef GDK_WINDOWING_WAYLAND
+ if (GDK_IS_WAYLAND_WINDOW(window)) {
+ VNC_DEBUG("Using Wayland Xorg/evdev virtual keycode mapping");
+ *maplen = G_N_ELEMENTS(keymap_xorgevdev2xtkbd);
+ return keymap_xorgevdev2xtkbd;
+ }
+#endif
+
+#ifdef GDK_WINDOWING_BROADWAY
+ if (GDK_IS_BROADWAY_WINDOW(window)) {
+ g_warning("experimental: using broadway, x11 virtual keysym mapping - with very limited support. See also https://bugzilla.gnome.org/show_bug.cgi?id=700105");
+
+ *maplen = G_N_ELEMENTS(keymap_x112xtkbd);
+ return keymap_x112xtkbd;
+ }
+#endif
+
+ g_warning("Unsupported GDK Windowing platform.\n"
+ "Disabling extended keycode tables.\n"
+ "Please report to gtk-vnc-list@gnome.org\n"
+ "including the following information:\n"
+ "\n"
+ " - Operating system\n"
+ " - GDK Windowing system build\n");
+ return NULL;
+}
+
+guint16 vnc_display_keymap_gdk2xtkbd(const guint16 *keycode_map,
+ size_t keycode_maplen,
+ guint16 keycode)
+{
+ if (!keycode_map)
+ return 0;
+ if (keycode >= keycode_maplen)
+ return 0;
+ return keycode_map[keycode];
+}
+
+/* Set the keymap entries */
+void vnc_display_keyval_set_entries(void)
+{
+ size_t i;
+ if (ref_count_for_untranslated_keys == 0)
+ for (i = 0; i < sizeof(untranslated_keys) / sizeof(untranslated_keys[0]); i++)
+ gdk_keymap_get_entries_for_keyval(gdk_keymap_get_default(),
+ untranslated_keys[i].keyval,
+ &untranslated_keys[i].keys,
+ &untranslated_keys[i].n_keys);
+ ref_count_for_untranslated_keys++;
+}
+
+/* Free the keymap entries */
+void vnc_display_keyval_free_entries(void)
+{
+ size_t i;
+
+ if (ref_count_for_untranslated_keys == 0)
+ return;
+
+ ref_count_for_untranslated_keys--;
+ if (ref_count_for_untranslated_keys == 0)
+ for (i = 0; i < sizeof(untranslated_keys) / sizeof(untranslated_keys[0]); i++)
+ g_free(untranslated_keys[i].keys);
+
+}
+
+/* Get the keyval from the keycode without the level. */
+guint vnc_display_keyval_from_keycode(guint keycode, guint keyval)
+{
+ size_t i;
+ for (i = 0; i < sizeof(untranslated_keys) / sizeof(untranslated_keys[0]); i++) {
+ if (keycode == untranslated_keys[i].keys[0].keycode) {
+ return untranslated_keys[i].keyval;
+ }
+ }
+
+ return keyval;
+}
+/*
+ * Local variables:
+ * c-indent-level: 8
+ * c-basic-offset: 8
+ * tab-width: 8
+ * End:
+ */
diff --git a/src/vncdisplaykeymap.h b/src/vncdisplaykeymap.h
new file mode 100644
index 0000000..3ec55d5
--- /dev/null
+++ b/src/vncdisplaykeymap.h
@@ -0,0 +1,36 @@
+/*
+ * GTK VNC Widget
+ *
+ * Copyright (C) 2006 Anthony Liguori <anthony@codemonkey.ws>
+ * Copyright (C) 2009-2010 Daniel P. Berrange <dan@berrange.com>
+ *
+ * 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.0 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef VNC_DISPLAY_KEYMAP_H
+#define VNC_DISPLAY_KEYMAP_H
+
+#include <glib.h>
+
+const guint16 *vnc_display_keymap_gdk2xtkbd_table(GdkWindow *window,
+ size_t *maplen);
+guint16 vnc_display_keymap_gdk2xtkbd(const guint16 *keycode_map,
+ size_t keycode_maplen,
+ guint16 keycode);
+void vnc_display_keyval_set_entries(void);
+void vnc_display_keyval_free_entries(void);
+guint vnc_display_keyval_from_keycode(guint keycode, guint keyval);
+
+#endif /* VNC_DISPLAY_KEYMAP_H */
diff --git a/src/win-usb-clerk.h b/src/win-usb-clerk.h
new file mode 100644
index 0000000..24da3b4
--- /dev/null
+++ b/src/win-usb-clerk.h
@@ -0,0 +1,36 @@
+#ifndef _H_USBCLERK
+#define _H_USBCLERK
+
+#include <windows.h>
+
+#define USB_CLERK_PIPE_NAME TEXT("\\\\.\\pipe\\usbclerkpipe")
+#define USB_CLERK_MAGIC 0xDADA
+#define USB_CLERK_VERSION 0x0003
+
+typedef struct USBClerkHeader {
+ UINT16 magic;
+ UINT16 version;
+ UINT16 type;
+ UINT16 size;
+} USBClerkHeader;
+
+enum {
+ USB_CLERK_DRIVER_INSTALL = 1,
+ USB_CLERK_DRIVER_REMOVE,
+ USB_CLERK_REPLY,
+ USB_CLERK_DRIVER_SESSION_INSTALL,
+ USB_CLERK_END_MESSAGE,
+};
+
+typedef struct USBClerkDriverOp {
+ USBClerkHeader hdr;
+ UINT16 vid;
+ UINT16 pid;
+} USBClerkDriverOp;
+
+typedef struct USBClerkReply {
+ USBClerkHeader hdr;
+ UINT32 status;
+} USBClerkReply;
+
+#endif
diff --git a/src/win-usb-dev.c b/src/win-usb-dev.c
new file mode 100644
index 0000000..1e4b2d6
--- /dev/null
+++ b/src/win-usb-dev.c
@@ -0,0 +1,542 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 Red Hat, Inc.
+
+ Red Hat Authors:
+ Arnon Gilboa <agilboa@redhat.com>
+ Uri Lublin <uril@redhat.com>
+
+ 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 <windows.h>
+#include <libusb.h>
+#include "win-usb-dev.h"
+#include "spice-marshal.h"
+#include "spice-util.h"
+#include "usbutil.h"
+
+#define G_UDEV_CLIENT_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), G_UDEV_TYPE_CLIENT, GUdevClientPrivate))
+
+struct _GUdevClientPrivate {
+ libusb_context *ctx;
+ gssize udev_list_size;
+ GList *udev_list;
+ HWND hwnd;
+};
+
+#define G_UDEV_CLIENT_WINCLASS_NAME TEXT("G_UDEV_CLIENT")
+
+static void g_udev_client_initable_iface_init(GInitableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE(GUdevClient, g_udev_client, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE(G_TYPE_INITABLE, g_udev_client_initable_iface_init));
+
+
+typedef struct _GUdevDeviceInfo GUdevDeviceInfo;
+
+struct _GUdevDeviceInfo {
+ guint16 bus;
+ guint16 addr;
+ guint16 vid;
+ guint16 pid;
+ guint16 class;
+ gchar sclass[4];
+ gchar sbus[4];
+ gchar saddr[4];
+ gchar svid[8];
+ gchar spid[8];
+};
+
+struct _GUdevDevicePrivate
+{
+ /* FixMe: move above fields to this structure and access them directly */
+ GUdevDeviceInfo *udevinfo;
+};
+
+G_DEFINE_TYPE(GUdevDevice, g_udev_device, G_TYPE_OBJECT)
+
+
+enum
+{
+ UEVENT_SIGNAL,
+ LAST_SIGNAL,
+};
+
+static guint signals[LAST_SIGNAL] = { 0, };
+static GUdevClient *singleton = NULL;
+
+static GUdevDevice *g_udev_device_new(GUdevDeviceInfo *udevinfo);
+static LRESULT CALLBACK wnd_proc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam);
+static gboolean get_usb_dev_info(libusb_device *dev, GUdevDeviceInfo *udevinfo);
+
+//uncomment to debug gudev device lists.
+//#define DEBUG_GUDEV_DEVICE_LISTS
+
+#ifdef DEBUG_GUDEV_DEVICE_LISTS
+static void g_udev_device_print_list(GList *l, const gchar *msg);
+#else
+static void g_udev_device_print_list(GList *l, const gchar *msg) {}
+#endif
+static void g_udev_device_print(GUdevDevice *udev, const gchar *msg);
+
+static gboolean g_udev_skip_search(GUdevDevice *udev);
+
+GQuark g_udev_client_error_quark(void)
+{
+ return g_quark_from_static_string("win-gudev-client-error-quark");
+}
+
+GUdevClient *g_udev_client_new(const gchar* const *subsystems)
+{
+ if (!singleton) {
+ singleton = g_initable_new(G_UDEV_TYPE_CLIENT, NULL, NULL, NULL);
+ return singleton;
+ } else {
+ return g_object_ref(singleton);
+ }
+}
+
+
+/*
+ * devs [in,out] an empty devs list in, full devs list out
+ * Returns: number-of-devices, or a negative value on error.
+ */
+static ssize_t
+g_udev_client_list_devices(GUdevClient *self, GList **devs,
+ GError **err, const gchar *name)
+{
+ gssize rc;
+ libusb_device **lusb_list, **dev;
+ GUdevClientPrivate *priv;
+ GUdevDeviceInfo *udevinfo;
+ GUdevDevice *udevice;
+ ssize_t n;
+
+ g_return_val_if_fail(G_UDEV_IS_CLIENT(self), -1);
+ g_return_val_if_fail(devs != NULL, -2);
+
+ priv = self->priv;
+
+ g_return_val_if_fail(self->priv->ctx != NULL, -3);
+
+ rc = libusb_get_device_list(priv->ctx, &lusb_list);
+ if (rc < 0) {
+ const char *errstr = spice_usbutil_libusb_strerror(rc);
+ g_warning("%s: libusb_get_device_list failed", name);
+ g_set_error(err, G_UDEV_CLIENT_ERROR, G_UDEV_CLIENT_LIBUSB_FAILED,
+ "%s: Error getting device list from libusb: %s [%"G_GSSIZE_FORMAT"]",
+ name, errstr, rc);
+ return -4;
+ }
+
+ n = 0;
+ for (dev = lusb_list; *dev; dev++) {
+ udevinfo = g_new0(GUdevDeviceInfo, 1);
+ get_usb_dev_info(*dev, udevinfo);
+ udevice = g_udev_device_new(udevinfo);
+ if (g_udev_skip_search(udevice)) {
+ g_object_unref(udevice);
+ continue;
+ }
+ *devs = g_list_prepend(*devs, udevice);
+ n++;
+ }
+ libusb_free_device_list(lusb_list, 1);
+
+ return n;
+}
+
+static void g_udev_client_free_device_list(GList **devs)
+{
+ g_return_if_fail(devs != NULL);
+ if (*devs) {
+ g_list_free_full(*devs, g_object_unref);
+ *devs = NULL;
+ }
+}
+
+
+static gboolean
+g_udev_client_initable_init(GInitable *initable, GCancellable *cancellable,
+ GError **err)
+{
+ GUdevClient *self;
+ GUdevClientPrivate *priv;
+ WNDCLASS wcls;
+ int rc;
+
+ g_return_val_if_fail(G_UDEV_IS_CLIENT(initable), FALSE);
+ g_return_val_if_fail(cancellable == NULL, FALSE);
+
+ self = G_UDEV_CLIENT(initable);
+ priv = self->priv;
+
+ rc = libusb_init(&priv->ctx);
+ if (rc < 0) {
+ const char *errstr = spice_usbutil_libusb_strerror(rc);
+ g_warning("Error initializing USB support: %s [%i]", errstr, rc);
+ g_set_error(err, G_UDEV_CLIENT_ERROR, G_UDEV_CLIENT_LIBUSB_FAILED,
+ "Error initializing USB support: %s [%i]", errstr, rc);
+ return FALSE;
+ }
+
+ /* get initial device list */
+ priv->udev_list_size = g_udev_client_list_devices(self, &priv->udev_list,
+ err, __FUNCTION__);
+ if (priv->udev_list_size < 0) {
+ goto g_udev_client_init_failed;
+ }
+
+ g_udev_device_print_list(priv->udev_list, "init: first list is: ");
+
+ /* create a hidden window */
+ memset(&wcls, 0, sizeof(wcls));
+ wcls.lpfnWndProc = wnd_proc;
+ wcls.lpszClassName = G_UDEV_CLIENT_WINCLASS_NAME;
+ if (!RegisterClass(&wcls)) {
+ DWORD e = GetLastError();
+ g_warning("RegisterClass failed , %ld", (long)e);
+ g_set_error(err, G_UDEV_CLIENT_ERROR, G_UDEV_CLIENT_WINAPI_FAILED,
+ "RegisterClass failed: %ld", (long)e);
+ goto g_udev_client_init_failed;
+ }
+ priv->hwnd = CreateWindow(G_UDEV_CLIENT_WINCLASS_NAME,
+ NULL, 0, 0, 0, 0, 0, NULL, NULL, NULL, NULL);
+ if (!priv->hwnd) {
+ DWORD e = GetLastError();
+ g_warning("CreateWindow failed: %ld", (long)e);
+ g_set_error(err, G_UDEV_CLIENT_ERROR, G_UDEV_CLIENT_LIBUSB_FAILED,
+ "CreateWindow failed: %ld", (long)e);
+ goto g_udev_client_init_failed_unreg;
+ }
+
+ return TRUE;
+
+ g_udev_client_init_failed_unreg:
+ UnregisterClass(G_UDEV_CLIENT_WINCLASS_NAME, NULL);
+ g_udev_client_init_failed:
+ libusb_exit(priv->ctx);
+ priv->ctx = NULL;
+
+ return FALSE;
+}
+
+static void g_udev_client_initable_iface_init(GInitableIface *iface)
+{
+ iface->init = g_udev_client_initable_init;
+}
+
+GList *g_udev_client_query_by_subsystem(GUdevClient *self, const gchar *subsystem)
+{
+ GList *l = g_list_copy(self->priv->udev_list);
+ g_list_foreach(l, (GFunc)g_object_ref, NULL);
+ return l;
+}
+
+static void g_udev_client_init(GUdevClient *self)
+{
+ self->priv = G_UDEV_CLIENT_GET_PRIVATE(self);
+}
+
+static void g_udev_client_finalize(GObject *gobject)
+{
+ GUdevClient *self = G_UDEV_CLIENT(gobject);
+ GUdevClientPrivate *priv = self->priv;
+
+ singleton = NULL;
+ DestroyWindow(priv->hwnd);
+ UnregisterClass(G_UDEV_CLIENT_WINCLASS_NAME, NULL);
+ g_udev_client_free_device_list(&priv->udev_list);
+
+ /* free libusb context initializing by libusb_init() */
+ g_warn_if_fail(priv->ctx != NULL);
+ libusb_exit(priv->ctx);
+
+ /* Chain up to the parent class */
+ if (G_OBJECT_CLASS(g_udev_client_parent_class)->finalize)
+ G_OBJECT_CLASS(g_udev_client_parent_class)->finalize(gobject);
+}
+
+static void g_udev_client_class_init(GUdevClientClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+
+ gobject_class->finalize = g_udev_client_finalize;
+
+ signals[UEVENT_SIGNAL] =
+ g_signal_new("uevent",
+ G_OBJECT_CLASS_TYPE(klass),
+ G_SIGNAL_RUN_FIRST,
+ G_STRUCT_OFFSET(GUdevClientClass, uevent),
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__BOXED_BOXED,
+ G_TYPE_NONE,
+ 2,
+ G_TYPE_STRING,
+ G_UDEV_TYPE_DEVICE);
+
+ g_type_class_add_private(klass, sizeof(GUdevClientPrivate));
+}
+
+static gboolean get_usb_dev_info(libusb_device *dev, GUdevDeviceInfo *udevinfo)
+{
+ struct libusb_device_descriptor desc;
+
+ g_return_val_if_fail(dev, FALSE);
+ g_return_val_if_fail(udevinfo, FALSE);
+
+ if (libusb_get_device_descriptor(dev, &desc) < 0) {
+ g_warning("cannot get device descriptor %p", dev);
+ return FALSE;
+ }
+
+ udevinfo->bus = libusb_get_bus_number(dev);
+ udevinfo->addr = libusb_get_device_address(dev);
+ udevinfo->class = desc.bDeviceClass;
+ udevinfo->vid = desc.idVendor;
+ udevinfo->pid = desc.idProduct;
+ snprintf(udevinfo->sclass, sizeof(udevinfo->sclass), "%d", udevinfo->class);
+ snprintf(udevinfo->sbus, sizeof(udevinfo->sbus), "%d", udevinfo->bus);
+ snprintf(udevinfo->saddr, sizeof(udevinfo->saddr), "%d", udevinfo->addr);
+ snprintf(udevinfo->svid, sizeof(udevinfo->svid), "%d", udevinfo->vid);
+ snprintf(udevinfo->spid, sizeof(udevinfo->spid), "%d", udevinfo->pid);
+ return TRUE;
+}
+
+/* Only vid:pid are compared */
+static gboolean gudev_devices_are_equal(GUdevDevice *a, GUdevDevice *b)
+{
+ GUdevDeviceInfo *ai, *bi;
+ gboolean same_vid;
+ gboolean same_pid;
+
+ ai = a->priv->udevinfo;
+ bi = b->priv->udevinfo;
+
+ same_vid = (ai->vid == bi->vid);
+ same_pid = (ai->pid == bi->pid);
+
+ return (same_pid && same_vid);
+}
+
+
+/* Assumes each event stands for a single device change (at most) */
+static void handle_dev_change(GUdevClient *self)
+{
+ GUdevClientPrivate *priv = self->priv;
+ GUdevDevice *changed_dev = NULL;
+ ssize_t dev_count;
+ int is_dev_change;
+ GError *err = NULL;
+ GList *now_devs = NULL;
+ GList *llist, *slist; /* long-list and short-list*/
+ GList *lit, *sit; /* iterators for long-list and short-list */
+ GUdevDevice *ldev, *sdev; /* devices on long-list and short-list */
+
+ dev_count = g_udev_client_list_devices(self, &now_devs, &err,
+ __FUNCTION__);
+ g_return_if_fail(dev_count >= 0);
+
+ SPICE_DEBUG("number of current devices %"G_GSSIZE_FORMAT
+ ", I know about %"G_GSSIZE_FORMAT" devices",
+ dev_count, priv->udev_list_size);
+
+ is_dev_change = dev_count - priv->udev_list_size;
+ if (is_dev_change == 0) {
+ g_udev_client_free_device_list(&now_devs);
+ return;
+ }
+
+ if (is_dev_change > 0) {
+ llist = now_devs;
+ slist = priv->udev_list;
+ } else {
+ llist = priv->udev_list;
+ slist = now_devs;
+ }
+
+ g_udev_device_print_list(llist, "handle_dev_change: long list:");
+ g_udev_device_print_list(slist, "handle_dev_change: short list:");
+
+ /* Go over the longer list */
+ for (lit = g_list_first(llist); lit != NULL; lit=g_list_next(lit)) {
+ ldev = lit->data;
+ /* Look for dev in the shorther list */
+ for (sit = g_list_first(slist); sit != NULL; sit=g_list_next(sit)) {
+ sdev = sit->data;
+ if (gudev_devices_are_equal(ldev, sdev))
+ break;
+ }
+ if (sit == NULL) {
+ /* Found a device which appears only in the longer list */
+ changed_dev = ldev;
+ break;
+ }
+ }
+
+ if (!changed_dev) {
+ g_warning("couldn't find any device change");
+ goto leave;
+ }
+
+ if (is_dev_change > 0) {
+ g_udev_device_print(changed_dev, ">>> USB device inserted");
+ g_signal_emit(self, signals[UEVENT_SIGNAL], 0, "add", changed_dev);
+ } else {
+ g_udev_device_print(changed_dev, "<<< USB device removed");
+ g_signal_emit(self, signals[UEVENT_SIGNAL], 0, "remove", changed_dev);
+ }
+
+leave:
+ /* keep most recent info: free previous list, and keep current list */
+ g_udev_client_free_device_list(&priv->udev_list);
+ priv->udev_list = now_devs;
+ priv->udev_list_size = dev_count;
+}
+
+static LRESULT CALLBACK wnd_proc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
+{
+ /* Only DBT_DEVNODES_CHANGED recieved */
+ if (message == WM_DEVICECHANGE) {
+ handle_dev_change(singleton);
+ }
+ return DefWindowProc(hwnd, message, wparam, lparam);
+}
+
+/*** GUdevDevice ***/
+
+static void g_udev_device_finalize(GObject *object)
+{
+ GUdevDevice *device = G_UDEV_DEVICE(object);
+
+ g_free(device->priv->udevinfo);
+ if (G_OBJECT_CLASS(g_udev_device_parent_class)->finalize != NULL)
+ (* G_OBJECT_CLASS(g_udev_device_parent_class)->finalize)(object);
+}
+
+static void g_udev_device_class_init(GUdevDeviceClass *klass)
+{
+ GObjectClass *gobject_class = (GObjectClass *) klass;
+
+ gobject_class->finalize = g_udev_device_finalize;
+ g_type_class_add_private (klass, sizeof(GUdevDevicePrivate));
+}
+
+static void g_udev_device_init(GUdevDevice *device)
+{
+ device->priv = G_TYPE_INSTANCE_GET_PRIVATE(device, G_UDEV_TYPE_DEVICE, GUdevDevicePrivate);
+}
+
+static GUdevDevice *g_udev_device_new(GUdevDeviceInfo *udevinfo)
+{
+ GUdevDevice *device;
+
+ g_return_val_if_fail(udevinfo != NULL, NULL);
+
+ device = G_UDEV_DEVICE(g_object_new(G_UDEV_TYPE_DEVICE, NULL));
+ device->priv->udevinfo = udevinfo;
+ return device;
+}
+
+const gchar *g_udev_device_get_property(GUdevDevice *udev, const gchar *property)
+{
+ GUdevDeviceInfo* udevinfo;
+
+ g_return_val_if_fail(G_UDEV_DEVICE(udev), NULL);
+ g_return_val_if_fail(property != NULL, NULL);
+
+ udevinfo = udev->priv->udevinfo;
+ g_return_val_if_fail(udevinfo != NULL, NULL);
+
+ if (g_strcmp0(property, "BUSNUM") == 0) {
+ return udevinfo->sbus;
+ } else if (g_strcmp0(property, "DEVNUM") == 0) {
+ return udevinfo->saddr;
+ } else if (g_strcmp0(property, "DEVTYPE") == 0) {
+ return "usb_device";
+ } else if (g_strcmp0(property, "VID") == 0) {
+ return udevinfo->svid;
+ } else if (g_strcmp0(property, "PID") == 0) {
+ return udevinfo->spid;
+ }
+
+ g_warn_if_reached();
+ return NULL;
+}
+
+const gchar *g_udev_device_get_sysfs_attr(GUdevDevice *udev, const gchar *attr)
+{
+ GUdevDeviceInfo* udevinfo;
+
+ g_return_val_if_fail(G_UDEV_DEVICE(udev), NULL);
+ g_return_val_if_fail(attr != NULL, NULL);
+
+ udevinfo = udev->priv->udevinfo;
+ g_return_val_if_fail(udevinfo != NULL, NULL);
+
+
+ if (g_strcmp0(attr, "bDeviceClass") == 0) {
+ return udevinfo->sclass;
+ }
+ g_warn_if_reached();
+ return NULL;
+}
+
+#ifdef DEBUG_GUDEV_DEVICE_LISTS
+static void g_udev_device_print_list(GList *l, const gchar *msg)
+{
+ GList *it;
+
+ for (it = g_list_first(l); it != NULL; it=g_list_next(it)) {
+ g_udev_device_print(it->data, msg);
+ }
+}
+#endif
+
+static void g_udev_device_print(GUdevDevice *udev, const gchar *msg)
+{
+ GUdevDeviceInfo* udevinfo;
+
+ g_return_if_fail(G_UDEV_DEVICE(udev));
+
+ udevinfo = udev->priv->udevinfo;
+ g_return_if_fail(udevinfo != NULL);
+
+ SPICE_DEBUG("%s: %d.%d 0x%04x:0x%04x class %d", msg,
+ udevinfo->bus, udevinfo->addr,
+ udevinfo->vid, udevinfo->pid, udevinfo->class);
+}
+
+static gboolean g_udev_skip_search(GUdevDevice *udev)
+{
+ GUdevDeviceInfo* udevinfo;
+ gboolean skip;
+
+ g_return_val_if_fail(G_UDEV_DEVICE(udev), FALSE);
+
+ udevinfo = udev->priv->udevinfo;
+ g_return_val_if_fail(udevinfo != NULL, FALSE);
+
+ skip = ((udevinfo->addr == 0xff) || /* root hub (HCD) */
+#if defined(LIBUSBX_API_VERSION) && (LIBUSBX_API_VERSION >= 0x010000FF)
+ (udevinfo->addr == 1) || /* root hub addr for libusbx >= 1.0.13 */
+#endif
+ (udevinfo->class == LIBUSB_CLASS_HUB) || /* hub*/
+ (udevinfo->addr == 0)); /* bad address */
+ return skip;
+}
diff --git a/src/win-usb-dev.h b/src/win-usb-dev.h
new file mode 100644
index 0000000..b5c4fce
--- /dev/null
+++ b/src/win-usb-dev.h
@@ -0,0 +1,110 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 Red Hat, Inc.
+
+ Red Hat Authors:
+ Arnon Gilboa <agilboa@redhat.com>
+ Uri Lublin <uril@redhat.com>
+
+ 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 __WIN_USB_DEV_H__
+#define __WIN_USB_DEV_H__
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+/* GUdevDevice */
+
+#define G_UDEV_TYPE_DEVICE (g_udev_device_get_type())
+#define G_UDEV_DEVICE(o) (G_TYPE_CHECK_INSTANCE_CAST((o), G_UDEV_TYPE_DEVICE, GUdevDevice))
+#define G_UDEV_DEVICE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), G_UDEV_TYPE_DEVICE, GUdevDeviceClass))
+#define G_UDEV_IS_DEVICE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), G_UDEV_TYPE_DEVICE))
+#define G_UDEV_IS_DEVICE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE((k), G_UDEV_TYPE_DEVICE))
+#define G_UDEV_DEVICE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS((o), G_UDEV_TYPE_DEVICE, GUdevDeviceClass))
+
+typedef struct _GUdevDevice GUdevDevice;
+typedef struct _GUdevDeviceClass GUdevDeviceClass;
+typedef struct _GUdevDevicePrivate GUdevDevicePrivate;
+
+struct _GUdevDevice
+{
+ GObject parent;
+ GUdevDevicePrivate *priv;
+};
+
+struct _GUdevDeviceClass
+{
+ GObjectClass parent_class;
+};
+
+/* GUdevClient */
+
+#define G_UDEV_TYPE_CLIENT (g_udev_client_get_type())
+#define G_UDEV_CLIENT(o) (G_TYPE_CHECK_INSTANCE_CAST((o), G_UDEV_TYPE_CLIENT, GUdevClient))
+#define G_UDEV_CLIENT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), G_UDEV_TYPE_CLIENT, GUdevClientClass))
+#define G_UDEV_IS_CLIENT(o) (G_TYPE_CHECK_INSTANCE_TYPE((o), G_UDEV_TYPE_CLIENT))
+#define G_UDEV_IS_CLIENT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE((k), G_UDEV_TYPE_CLIENT))
+#define G_UDEV_CLIENT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS((o), G_UDEV_TYPE_CLIENT, GUdevClientClass))
+
+typedef struct _GUdevClient GUdevClient;
+typedef struct _GUdevClientClass GUdevClientClass;
+typedef struct _GUdevClientPrivate GUdevClientPrivate;
+
+struct _GUdevClient
+{
+ GObject parent;
+
+ GUdevClientPrivate *priv;
+};
+
+struct _GUdevClientClass
+{
+ GObjectClass parent_class;
+
+ /* signals */
+ void (*uevent)(GUdevClient *client, const gchar *action, GUdevDevice *device);
+};
+
+GType g_udev_client_get_type(void) G_GNUC_CONST;
+GUdevClient *g_udev_client_new(const gchar* const *subsystems);
+GList *g_udev_client_query_by_subsystem(GUdevClient *client, const gchar *subsystem);
+
+GType g_udev_device_get_type(void) G_GNUC_CONST;
+const gchar *g_udev_device_get_property(GUdevDevice *udev, const gchar *property);
+const gchar *g_udev_device_get_sysfs_attr(GUdevDevice *udev, const gchar *attr);
+
+GQuark g_udev_client_error_quark(void);
+#define G_UDEV_CLIENT_ERROR g_udev_client_error_quark()
+
+/**
+ * GUdevClientError:
+ * @G_UDEV_CLIENT_ERROR_FAILED: generic error code
+ * @G_UDEV_CLIENT_LIBUSB_FAILED: a libusb call failed
+ * @G_UDEV_CLIENT_WINAPI_FAILED: a winapi call failed
+ *
+ * Error codes returned by spice-client API.
+ */
+typedef enum
+{
+ G_UDEV_CLIENT_ERROR_FAILED = 1,
+ G_UDEV_CLIENT_LIBUSB_FAILED,
+ G_UDEV_CLIENT_WINAPI_FAILED
+} GUdevClientError;
+
+
+G_END_DECLS
+
+#endif /* __WIN_USB_DEV_H__ */
diff --git a/src/win-usb-driver-install.c b/src/win-usb-driver-install.c
new file mode 100644
index 0000000..54e9b14
--- /dev/null
+++ b/src/win-usb-driver-install.c
@@ -0,0 +1,426 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2011 Red Hat, Inc.
+
+ Red Hat Authors:
+ Uri Lublin <uril@redhat.com>
+
+ 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/>.
+*/
+
+/*
+ * Some notes:
+ * Each installer (instance) opens a named-pipe to talk with win-usb-clerk.
+ * Each installer (instance) requests driver installation for a single device.
+ */
+
+#include "config.h"
+
+#include <windows.h>
+#include <gio/gio.h>
+#include <gio/gwin32inputstream.h>
+#include <gio/gwin32outputstream.h>
+#include "spice-util.h"
+#include "win-usb-clerk.h"
+#include "win-usb-driver-install.h"
+#include "usb-device-manager-priv.h"
+
+/* ------------------------------------------------------------------ */
+/* gobject glue */
+
+#define SPICE_WIN_USB_DRIVER_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE ((obj), SPICE_TYPE_WIN_USB_DRIVER, SpiceWinUsbDriverPrivate))
+
+struct _SpiceWinUsbDriverPrivate {
+ USBClerkReply reply;
+ GSimpleAsyncResult *result;
+ GCancellable *cancellable;
+ HANDLE handle;
+ SpiceUsbDevice *device;
+};
+
+
+static void spice_win_usb_driver_initable_iface_init(GInitableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE(SpiceWinUsbDriver, spice_win_usb_driver, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, spice_win_usb_driver_initable_iface_init));
+
+static void spice_win_usb_driver_init(SpiceWinUsbDriver *self)
+{
+ self->priv = SPICE_WIN_USB_DRIVER_GET_PRIVATE(self);
+}
+
+static gboolean spice_win_usb_driver_initable_init(GInitable *initable,
+ GCancellable *cancellable,
+ GError **err)
+{
+ SpiceWinUsbDriver *self = SPICE_WIN_USB_DRIVER(initable);
+ SpiceWinUsbDriverPrivate *priv = self->priv;
+
+ SPICE_DEBUG("win-usb-driver-install: connecting to usbclerk named pipe");
+ priv->handle = CreateFile(USB_CLERK_PIPE_NAME,
+ GENERIC_READ | GENERIC_WRITE,
+ 0, NULL,
+ OPEN_EXISTING,
+ FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
+ NULL);
+ if (priv->handle == INVALID_HANDLE_VALUE) {
+ DWORD errval = GetLastError();
+ gchar *errstr = g_win32_error_message(errval);
+ g_set_error(err, SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_USB_SERVICE,
+ "Failed to create service named pipe (%ld) %s", errval, errstr);
+ g_free(errstr);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void spice_win_usb_driver_finalize(GObject *gobject)
+{
+ SpiceWinUsbDriver *self = SPICE_WIN_USB_DRIVER(gobject);
+ SpiceWinUsbDriverPrivate *priv = self->priv;
+
+ if (priv->handle)
+ CloseHandle(priv->handle);
+
+ g_clear_object(&priv->result);
+
+ if (G_OBJECT_CLASS(spice_win_usb_driver_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_win_usb_driver_parent_class)->finalize(gobject);
+}
+
+static void spice_win_usb_driver_class_init(SpiceWinUsbDriverClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->finalize = spice_win_usb_driver_finalize;
+
+ g_type_class_add_private(klass, sizeof(SpiceWinUsbDriverPrivate));
+}
+
+static void spice_win_usb_driver_initable_iface_init(GInitableIface *iface)
+{
+ iface->init = spice_win_usb_driver_initable_init;
+}
+
+/* ------------------------------------------------------------------ */
+/* callbacks */
+
+void win_usb_driver_handle_reply_cb(GObject *gobject,
+ GAsyncResult *read_res,
+ gpointer user_data)
+{
+ SpiceWinUsbDriver *self;
+ SpiceWinUsbDriverPrivate *priv;
+
+ GInputStream *istream;
+ GError *err = NULL;
+ gssize bytes;
+
+ g_return_if_fail(SPICE_IS_WIN_USB_DRIVER(user_data));
+ self = SPICE_WIN_USB_DRIVER(user_data);
+ priv = self->priv;
+ istream = G_INPUT_STREAM(gobject);
+
+ bytes = g_input_stream_read_finish(istream, read_res, &err);
+
+ SPICE_DEBUG("Finished reading reply-msg from usbclerk: bytes=%ld "
+ "err_exist?=%d", (long)bytes, err!=NULL);
+
+ g_warn_if_fail(g_input_stream_close(istream, NULL, NULL));
+ g_clear_object(&istream);
+
+ if (err) {
+ g_warning("failed to read reply from usbclerk (%s)", err->message);
+ g_simple_async_result_take_error(priv->result, err);
+ goto failed_reply;
+ }
+
+ if (bytes == 0) {
+ g_warning("unexpected EOF from usbclerk");
+ g_simple_async_result_set_error(priv->result,
+ SPICE_WIN_USB_DRIVER_ERROR,
+ SPICE_WIN_USB_DRIVER_ERROR_FAILED,
+ "unexpected EOF from usbclerk");
+ goto failed_reply;
+ }
+
+ if (bytes != sizeof(priv->reply)) {
+ g_warning("usbclerk size mismatch: read %"G_GSSIZE_FORMAT" bytes,expected "
+ "%"G_GSSIZE_FORMAT" (header %"G_GSSIZE_FORMAT", size in header %d)",
+ bytes, sizeof(priv->reply), sizeof(priv->reply.hdr), priv->reply.hdr.size);
+ /* For now just warn, do not fail */
+ }
+
+ if (priv->reply.hdr.magic != USB_CLERK_MAGIC) {
+ g_warning("usbclerk magic mismatch: mine=0x%04x server=0x%04x",
+ USB_CLERK_MAGIC, priv->reply.hdr.magic);
+ g_simple_async_result_set_error(priv->result,
+ SPICE_WIN_USB_DRIVER_ERROR,
+ SPICE_WIN_USB_DRIVER_ERROR_MESSAGE,
+ "usbclerk magic mismatch");
+ goto failed_reply;
+ }
+
+ if (priv->reply.hdr.version != USB_CLERK_VERSION) {
+ g_warning("usbclerk version mismatch: mine=0x%04x server=0x%04x",
+ USB_CLERK_VERSION, priv->reply.hdr.version);
+ g_simple_async_result_set_error(priv->result,
+ SPICE_WIN_USB_DRIVER_ERROR,
+ SPICE_WIN_USB_DRIVER_ERROR_MESSAGE,
+ "usbclerk version mismatch");
+ }
+
+ if (priv->reply.hdr.type != USB_CLERK_REPLY) {
+ g_warning("usbclerk message with unexpected type %d",
+ priv->reply.hdr.type);
+ g_simple_async_result_set_error(priv->result,
+ SPICE_WIN_USB_DRIVER_ERROR,
+ SPICE_WIN_USB_DRIVER_ERROR_MESSAGE,
+ "usbclerk message with unexpected type");
+ goto failed_reply;
+ }
+
+ if (priv->reply.hdr.size != bytes) {
+ g_warning("usbclerk message size mismatch: read %"G_GSSIZE_FORMAT" bytes hdr.size=%d",
+ bytes, priv->reply.hdr.size);
+ g_simple_async_result_set_error(priv->result,
+ SPICE_WIN_USB_DRIVER_ERROR,
+ SPICE_WIN_USB_DRIVER_ERROR_MESSAGE,
+ "usbclerk message with unexpected size");
+ goto failed_reply;
+ }
+
+ if (priv->reply.status == 0) {
+ g_simple_async_result_set_error(priv->result,
+ SPICE_WIN_USB_DRIVER_ERROR,
+ SPICE_WIN_USB_DRIVER_ERROR_MESSAGE,
+ "usbclerk error reply");
+ goto failed_reply;
+ }
+
+ failed_reply:
+ g_simple_async_result_complete_in_idle(priv->result);
+ g_clear_object(&priv->result);
+}
+
+/* ------------------------------------------------------------------ */
+/* helper functions */
+
+static
+gboolean spice_win_usb_driver_send_request(SpiceWinUsbDriver *self, guint16 op,
+ guint16 vid, guint16 pid, GError **err)
+{
+ USBClerkDriverOp req;
+ GOutputStream *ostream;
+ SpiceWinUsbDriverPrivate *priv;
+ gsize bytes;
+ gboolean ret;
+
+ SPICE_DEBUG("sending a request to usbclerk service (op=%d vid=0x%04x pid=0x%04x",
+ op, vid, pid);
+
+ g_return_val_if_fail(SPICE_IS_WIN_USB_DRIVER(self), FALSE);
+ priv = self->priv;
+
+ memset(&req, 0, sizeof(req));
+ req.hdr.magic = USB_CLERK_MAGIC;
+ req.hdr.version = USB_CLERK_VERSION;
+ req.hdr.type = op;
+ req.hdr.size = sizeof(req);
+ req.vid = vid;
+ req.pid = pid;
+
+ ostream = g_win32_output_stream_new(priv->handle, FALSE);
+
+ ret = g_output_stream_write_all(ostream, &req, sizeof(req), &bytes, NULL, err);
+ g_warn_if_fail(g_output_stream_close(ostream, NULL, NULL));
+ g_object_unref(ostream);
+ SPICE_DEBUG("write_all request returned %d written bytes %"G_GSIZE_FORMAT
+ " expecting %"G_GSIZE_FORMAT,
+ ret, bytes, sizeof(req));
+ return ret;
+}
+
+static
+void spice_win_usb_driver_read_reply_async(SpiceWinUsbDriver *self)
+{
+ SpiceWinUsbDriverPrivate *priv;
+ GInputStream *istream;
+
+ g_return_if_fail(SPICE_IS_WIN_USB_DRIVER(self));
+ priv = self->priv;
+
+ SPICE_DEBUG("waiting for a reply from usbclerk");
+
+ istream = g_win32_input_stream_new(priv->handle, FALSE);
+
+ g_input_stream_read_async(istream, &priv->reply, sizeof(priv->reply),
+ G_PRIORITY_DEFAULT, priv->cancellable,
+ win_usb_driver_handle_reply_cb, self);
+}
+
+
+/* ------------------------------------------------------------------ */
+/* private api */
+
+
+G_GNUC_INTERNAL
+SpiceWinUsbDriver *spice_win_usb_driver_new(GError **err)
+{
+ GObject *self;
+
+ g_return_val_if_fail(err == NULL || *err == NULL, FALSE);
+
+ self = g_initable_new(SPICE_TYPE_WIN_USB_DRIVER, NULL, err, NULL);
+
+ return SPICE_WIN_USB_DRIVER(self);
+}
+
+static
+void spice_win_usb_driver_op(SpiceWinUsbDriver *self,
+ SpiceUsbDevice *device,
+ guint16 op_type,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ guint16 vid, pid;
+ GError *err = NULL;
+ GSimpleAsyncResult *result;
+ SpiceWinUsbDriverPrivate *priv;
+
+ g_return_if_fail(SPICE_IS_WIN_USB_DRIVER(self));
+ g_return_if_fail(device != NULL);
+
+ priv = self->priv;
+
+ result = g_simple_async_result_new(G_OBJECT(self), callback, user_data,
+ spice_win_usb_driver_op);
+
+ if (priv->result) { /* allow one install/uninstall request at a time */
+ g_warning("Another request exists -- try later");
+ g_simple_async_result_set_error(result,
+ SPICE_WIN_USB_DRIVER_ERROR, SPICE_WIN_USB_DRIVER_ERROR_FAILED,
+ "Another request exists -- try later");
+ goto failed_request;
+ }
+
+
+ vid = spice_usb_device_get_vid(device);
+ pid = spice_usb_device_get_pid(device);
+
+ if (!spice_win_usb_driver_send_request(self, op_type,
+ vid, pid, &err)) {
+ g_warning("failed to send a request to usbclerk %s", err->message);
+ g_simple_async_result_take_error(result, err);
+ goto failed_request;
+ }
+
+ /* set up for async read */
+ priv->result = result;
+ priv->device = device;
+ priv->cancellable = cancellable;
+
+ spice_win_usb_driver_read_reply_async(self);
+
+ return;
+
+ failed_request:
+ g_simple_async_result_complete_in_idle(result);
+ g_clear_object(&result);
+}
+
+/**
+ * Returns: currently returns 0 (failure) and 1 (success)
+ * possibly later we'll add error-codes
+ */
+static gboolean
+spice_win_usb_driver_op_finish(SpiceWinUsbDriver *self,
+ GAsyncResult *res, GError **err)
+{
+ GSimpleAsyncResult *result = G_SIMPLE_ASYNC_RESULT(res);
+
+ g_return_val_if_fail(SPICE_IS_WIN_USB_DRIVER(self), 0);
+ g_return_val_if_fail(g_simple_async_result_is_valid(res, G_OBJECT(self),
+ spice_win_usb_driver_op),
+ FALSE);
+ if (g_simple_async_result_propagate_error(result, err))
+ return FALSE;
+
+ return TRUE;
+}
+
+/**
+ * spice_win_usb_driver_install_async:
+ * Start libusb driver installation for @device
+ *
+ * A new NamedPipe is created for each request.
+ *
+ * Returns: TRUE if a request was sent to usbclerk
+ * FALSE upon failure to send a request.
+ */
+G_GNUC_INTERNAL
+void spice_win_usb_driver_install_async(SpiceWinUsbDriver *self,
+ SpiceUsbDevice *device,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SPICE_DEBUG("Win usb driver installation started");
+
+ spice_win_usb_driver_op(self, device, USB_CLERK_DRIVER_SESSION_INSTALL,
+ cancellable, callback, user_data);
+}
+
+G_GNUC_INTERNAL
+void spice_win_usb_driver_uninstall_async(SpiceWinUsbDriver *self,
+ SpiceUsbDevice *device,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SPICE_DEBUG("Win usb driver uninstall operation started");
+
+ spice_win_usb_driver_op(self, device, USB_CLERK_DRIVER_REMOVE, cancellable,
+ callback, user_data);
+}
+
+G_GNUC_INTERNAL
+gboolean spice_win_usb_driver_install_finish(SpiceWinUsbDriver *self,
+ GAsyncResult *res, GError **err)
+{
+ return spice_win_usb_driver_op_finish(self, res, err);
+}
+
+G_GNUC_INTERNAL
+gboolean spice_win_usb_driver_uninstall_finish(SpiceWinUsbDriver *self,
+ GAsyncResult *res, GError **err)
+{
+ return spice_win_usb_driver_op_finish(self, res, err);
+}
+
+G_GNUC_INTERNAL
+SpiceUsbDevice *spice_win_usb_driver_get_device(SpiceWinUsbDriver *self)
+{
+ g_return_val_if_fail(SPICE_IS_WIN_USB_DRIVER(self), 0);
+
+ return self->priv->device;
+}
+
+GQuark spice_win_usb_driver_error_quark(void)
+{
+ return g_quark_from_static_string("spice-win-usb-driver-error-quark");
+}
diff --git a/src/win-usb-driver-install.h b/src/win-usb-driver-install.h
new file mode 100644
index 0000000..f9afedc
--- /dev/null
+++ b/src/win-usb-driver-install.h
@@ -0,0 +1,106 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2011 Red Hat, Inc.
+
+ Red Hat Authors:
+ Uri Lublin <uril@redhat.com>
+
+ 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_WIN_USB_DRIVER_H
+#define SPICE_WIN_USB_DRIVER_H
+
+#include "usb-device-manager.h"
+
+G_BEGIN_DECLS
+
+GQuark win_usb_driver_error_quark(void);
+
+
+#define SPICE_TYPE_WIN_USB_DRIVER (spice_win_usb_driver_get_type ())
+#define SPICE_WIN_USB_DRIVER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), \
+ SPICE_TYPE_WIN_USB_DRIVER, SpiceWinUsbDriver))
+#define SPICE_IS_WIN_USB_DRIVER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), \
+ SPICE_TYPE_WIN_USB_DRIVER))
+#define SPICE_WIN_USB_DRIVER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), \
+ SPICE_TYPE_WIN_USB_DRIVER, SpiceWinUsbDriverClass))
+#define SPICE_IS_WIN_USB_DRIVER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass),\
+ SPICE_TYPE_WIN_USB_DRIVER))
+#define SPICE_WIN_USB_DRIVER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj),\
+ SPICE_TYPE_WIN_USB_DRIVER, SpiceWinUsbDriverClass))
+
+typedef struct _SpiceWinUsbDriver SpiceWinUsbDriver;
+typedef struct _SpiceWinUsbDriverClass SpiceWinUsbDriverClass;
+typedef struct _SpiceWinUsbDriverPrivate SpiceWinUsbDriverPrivate;
+
+struct _SpiceWinUsbDriver
+{
+ GObject parent;
+
+ /*< private >*/
+ SpiceWinUsbDriverPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+struct _SpiceWinUsbDriverClass
+{
+ GObjectClass parent_class;
+};
+
+GType spice_win_usb_driver_get_type(void);
+
+SpiceWinUsbDriver *spice_win_usb_driver_new(GError **err);
+
+
+void spice_win_usb_driver_install_async(SpiceWinUsbDriver *self,
+ SpiceUsbDevice *device,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gboolean spice_win_usb_driver_install_finish(SpiceWinUsbDriver *self,
+ GAsyncResult *res, GError **err);
+
+void spice_win_usb_driver_uninstall_async(SpiceWinUsbDriver *self,
+ SpiceUsbDevice *device,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gboolean spice_win_usb_driver_uninstall_finish(SpiceWinUsbDriver *self,
+ GAsyncResult *res, GError **err);
+
+
+
+SpiceUsbDevice *spice_win_usb_driver_get_device(SpiceWinUsbDriver *self);
+
+#define SPICE_WIN_USB_DRIVER_ERROR spice_win_usb_driver_error_quark()
+
+/**
+ * SpiceWinUsbDriverError:
+ * @SPICE_WIN_USB_DRIVER_ERROR_FAILED: generic error code
+ * @SPICE_WIN_USB_DRIVER_ERROR_MESSAGE: bad message read from clerk
+ *
+ * Error codes returned by spice-client API.
+ */
+typedef enum
+{
+ SPICE_WIN_USB_DRIVER_ERROR_FAILED,
+ SPICE_WIN_USB_DRIVER_ERROR_MESSAGE,
+} SpiceWinUsbDriverError;
+
+GQuark spice_win_usb_driver_error_quark(void);
+
+G_END_DECLS
+
+#endif /* SPICE_WIN_USB_DRIVER_H */
diff --git a/src/wocky-http-proxy.c b/src/wocky-http-proxy.c
new file mode 100644
index 0000000..ce23b0e
--- /dev/null
+++ b/src/wocky-http-proxy.c
@@ -0,0 +1,537 @@
+ /* wocky-http-proxy.c: Source for WockyHttpProxy
+ *
+ * Copyright (C) 2010 Collabora, Ltd.
+ * Copyright (C) 2014 Red Hat, Inc.
+ * @author Nicolas Dufresne <nicolas.dufresne@collabora.co.uk>
+ * @author Marc-André Lureau <marcandre.lureau@redhat.com>
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include "config.h"
+
+#include "glib-compat.h"
+#include "wocky-http-proxy.h"
+
+#include <string.h>
+#include <stdlib.h>
+
+
+struct _WockyHttpProxy
+{
+ GObject parent;
+};
+
+struct _WockyHttpProxyClass
+{
+ GObjectClass parent_class;
+};
+
+static void wocky_http_proxy_iface_init (GProxyInterface *proxy_iface);
+
+#define wocky_http_proxy_get_type _wocky_http_proxy_get_type
+G_DEFINE_TYPE_WITH_CODE (WockyHttpProxy, wocky_http_proxy, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (G_TYPE_PROXY,
+ wocky_http_proxy_iface_init)
+ g_io_extension_point_set_required_type (
+ g_io_extension_point_register (G_PROXY_EXTENSION_POINT_NAME),
+ G_TYPE_PROXY);
+ g_io_extension_point_implement (G_PROXY_EXTENSION_POINT_NAME,
+ g_define_type_id, "http", 0))
+
+static void
+wocky_http_proxy_init (WockyHttpProxy *proxy)
+{
+}
+
+#define HTTP_END_MARKER "\r\n\r\n"
+
+static gchar *
+create_request (GProxyAddress *proxy_address, gboolean *has_cred)
+{
+ const gchar *hostname;
+ gint port;
+ const gchar *username;
+ const gchar *password;
+ GString *request;
+ gchar *ascii_hostname;
+
+ if (has_cred)
+ *has_cred = FALSE;
+
+ hostname = g_proxy_address_get_destination_hostname (proxy_address);
+ port = g_proxy_address_get_destination_port (proxy_address);
+ username = g_proxy_address_get_username (proxy_address);
+ password = g_proxy_address_get_password (proxy_address);
+
+ request = g_string_new (NULL);
+
+ ascii_hostname = g_hostname_to_ascii (hostname);
+ g_string_append_printf (request,
+ "CONNECT %s:%i HTTP/1.0\r\n"
+ "Host: %s:%i\r\n"
+ "Proxy-Connection: keep-alive\r\n"
+ "User-Agent: GLib/%i.%i\r\n",
+ ascii_hostname, port,
+ ascii_hostname, port,
+ GLIB_MAJOR_VERSION, GLIB_MINOR_VERSION);
+ g_free (ascii_hostname);
+
+ if (username != NULL && password != NULL)
+ {
+ gchar *cred;
+ gchar *base64_cred;
+
+ if (has_cred)
+ *has_cred = TRUE;
+
+ cred = g_strdup_printf ("%s:%s", username, password);
+ base64_cred = g_base64_encode ((guchar *) cred, strlen (cred));
+ g_free (cred);
+ g_string_append_printf (request,
+ "Proxy-Authorization: Basic %s\r\n",
+ base64_cred);
+ g_free (base64_cred);
+ }
+
+ g_string_append (request, "\r\n");
+
+ return g_string_free (request, FALSE);
+}
+
+static gboolean
+check_reply (const gchar *buffer, gboolean has_cred, GError **error)
+{
+ gint err_code;
+ const gchar *ptr = buffer + 7;
+
+ if (strncmp (buffer, "HTTP/1.", 7) != 0
+ || (*ptr != '0' && *ptr != '1'))
+ {
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_PROXY_FAILED,
+ "Bad HTTP proxy reply");
+ return FALSE;
+ }
+
+ ptr++;
+ while (*ptr == ' ') ptr++;
+
+ err_code = atoi (ptr);
+
+ if (err_code < 200 || err_code >= 300)
+ {
+ const gchar *msg_start;
+ gchar *msg;
+
+ while (g_ascii_isdigit (*ptr))
+ ptr++;
+
+ while (*ptr == ' ')
+ ptr++;
+
+ msg_start = ptr;
+
+ ptr = strchr (msg_start, '\r');
+
+ if (ptr == NULL)
+ ptr = strchr (msg_start, '\0');
+
+ msg = g_strndup (msg_start, ptr - msg_start);
+
+ if (err_code == 407)
+ {
+ if (has_cred)
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_PROXY_AUTH_FAILED,
+ "HTTP proxy authentication failed");
+ else
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_PROXY_NEED_AUTH,
+ "HTTP proxy authentication required");
+ }
+ else if (msg[0] == '\0')
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_PROXY_FAILED,
+ "Connection failed due to broken HTTP reply");
+ else
+ g_set_error (error, G_IO_ERROR, G_IO_ERROR_PROXY_FAILED,
+ "HTTP proxy connection failed: %i %s",
+ err_code, msg);
+
+ g_free (msg);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static GIOStream *
+wocky_http_proxy_connect (GProxy *proxy,
+ GIOStream *io_stream,
+ GProxyAddress *proxy_address,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GInputStream *in;
+ GOutputStream *out;
+ GDataInputStream *data_in = NULL;
+ gchar *buffer = NULL;
+ gboolean has_cred;
+ GIOStream *tlsconn = NULL;
+
+ if (WOCKY_IS_HTTPS_PROXY (proxy))
+ {
+ tlsconn = g_tls_client_connection_new (io_stream,
+ G_SOCKET_CONNECTABLE(proxy_address),
+ error);
+ if (!tlsconn)
+ goto error;
+
+ GTlsCertificateFlags tls_validation_flags = G_TLS_CERTIFICATE_VALIDATE_ALL;
+#ifdef DEBUG
+ tls_validation_flags &= ~(G_TLS_CERTIFICATE_UNKNOWN_CA | G_TLS_CERTIFICATE_BAD_IDENTITY);
+#endif
+ g_tls_client_connection_set_validation_flags (G_TLS_CLIENT_CONNECTION (tlsconn),
+ tls_validation_flags);
+ if (!g_tls_connection_handshake (G_TLS_CONNECTION (tlsconn), cancellable, error))
+ goto error;
+
+ io_stream = tlsconn;
+ }
+
+ in = g_io_stream_get_input_stream (io_stream);
+ out = g_io_stream_get_output_stream (io_stream);
+
+ data_in = g_data_input_stream_new (in);
+ g_filter_input_stream_set_close_base_stream (G_FILTER_INPUT_STREAM (data_in),
+ FALSE);
+
+ buffer = create_request (proxy_address, &has_cred);
+ if (!g_output_stream_write_all (out, buffer, strlen (buffer), NULL,
+ cancellable, error))
+ goto error;
+
+ g_free (buffer);
+ buffer = g_data_input_stream_read_until (data_in, HTTP_END_MARKER, NULL,
+ cancellable, error);
+ g_object_unref (data_in);
+ data_in = NULL;
+
+ if (buffer == NULL)
+ {
+ if (error && (*error == NULL))
+ g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_PROXY_FAILED,
+ "HTTP proxy server closed connection unexpectedly.");
+ goto error;
+ }
+
+ if (!check_reply (buffer, has_cred, error))
+ goto error;
+
+ g_free (buffer);
+
+ g_object_ref (io_stream);
+ g_clear_object (&tlsconn);
+
+ return io_stream;
+
+error:
+ g_clear_object (&tlsconn);
+ g_clear_object (&data_in);
+ g_free (buffer);
+ return NULL;
+}
+
+
+typedef struct
+{
+ GSimpleAsyncResult *simple;
+ GIOStream *io_stream;
+ gchar *buffer;
+ gssize length;
+ gssize offset;
+ GDataInputStream *data_in;
+ gboolean has_cred;
+ GCancellable *cancellable;
+} ConnectAsyncData;
+
+static void request_write_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data);
+static void reply_read_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data);
+
+static void
+free_connect_data (ConnectAsyncData *data)
+{
+ if (data->io_stream != NULL)
+ g_object_unref (data->io_stream);
+
+ g_free (data->buffer);
+
+ if (data->data_in != NULL)
+ g_object_unref (data->data_in);
+
+ if (data->cancellable != NULL)
+ g_object_unref (data->cancellable);
+
+ g_slice_free (ConnectAsyncData, data);
+}
+
+static void
+complete_async_from_error (ConnectAsyncData *data, GError *error)
+{
+ GSimpleAsyncResult *simple = data->simple;
+
+ if (error == NULL)
+ g_set_error_literal (&error, G_IO_ERROR, G_IO_ERROR_PROXY_FAILED,
+ "HTTP proxy server closed connection unexpectedly.");
+
+ g_simple_async_result_set_from_error (data->simple, error);
+ g_error_free (error);
+ g_simple_async_result_set_op_res_gpointer (simple, NULL, NULL);
+ g_simple_async_result_complete (simple);
+ g_object_unref (simple);
+}
+
+static void
+do_write (GAsyncReadyCallback callback, ConnectAsyncData *data)
+{
+ GOutputStream *out;
+ out = g_io_stream_get_output_stream (data->io_stream);
+ g_output_stream_write_async (out,
+ data->buffer + data->offset,
+ data->length - data->offset,
+ G_PRIORITY_DEFAULT, data->cancellable,
+ callback, data);
+}
+
+static void
+stream_connected (ConnectAsyncData *data,
+ GIOStream *io_stream)
+{
+ GInputStream *in;
+
+ data->io_stream = g_object_ref (io_stream);
+ in = g_io_stream_get_input_stream (io_stream);
+ data->data_in = g_data_input_stream_new (in);
+ g_filter_input_stream_set_close_base_stream (G_FILTER_INPUT_STREAM (data->data_in),
+ FALSE);
+
+ do_write (request_write_cb, data);
+}
+
+static void
+handshake_completed (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GTlsConnection *conn = G_TLS_CONNECTION (source_object);
+ ConnectAsyncData *data = user_data;
+ GError *error = NULL;
+
+ if (!g_tls_connection_handshake_finish (conn, res, &error))
+ {
+ complete_async_from_error (data, error);
+ return;
+ }
+
+ stream_connected (data, G_IO_STREAM (conn));
+}
+
+static void
+wocky_http_proxy_connect_async (GProxy *proxy,
+ GIOStream *io_stream,
+ GProxyAddress *proxy_address,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GSimpleAsyncResult *simple;
+ ConnectAsyncData *data;
+
+ simple = g_simple_async_result_new (G_OBJECT (proxy),
+ callback, user_data,
+ wocky_http_proxy_connect_async);
+
+ data = g_slice_new0 (ConnectAsyncData);
+ if (cancellable != NULL)
+ data->cancellable = g_object_ref (cancellable);
+ data->simple = simple;
+
+ data->buffer = create_request (proxy_address, &data->has_cred);
+ data->length = strlen (data->buffer);
+ data->offset = 0;
+
+ g_simple_async_result_set_op_res_gpointer (simple, data,
+ (GDestroyNotify) free_connect_data);
+
+ if (WOCKY_IS_HTTPS_PROXY (proxy))
+ {
+ GError *error = NULL;
+ GIOStream *tlsconn;
+
+ tlsconn = g_tls_client_connection_new (io_stream,
+ G_SOCKET_CONNECTABLE(proxy_address),
+ &error);
+ if (!tlsconn)
+ {
+ complete_async_from_error (data, error);
+ return;
+ }
+
+ g_return_if_fail (tlsconn != NULL);
+
+ GTlsCertificateFlags tls_validation_flags = G_TLS_CERTIFICATE_VALIDATE_ALL;
+#ifdef DEBUG
+ tls_validation_flags &= ~(G_TLS_CERTIFICATE_UNKNOWN_CA | G_TLS_CERTIFICATE_BAD_IDENTITY);
+#endif
+ g_tls_client_connection_set_validation_flags (G_TLS_CLIENT_CONNECTION (tlsconn),
+ tls_validation_flags);
+ g_tls_connection_handshake_async (G_TLS_CONNECTION (tlsconn),
+ G_PRIORITY_DEFAULT, cancellable,
+ handshake_completed, data);
+ }
+ else
+ {
+ stream_connected (data, io_stream);
+ }
+}
+
+static void
+request_write_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GError *error = NULL;
+ ConnectAsyncData *data = user_data;
+ gssize written;
+
+ written = g_output_stream_write_finish (G_OUTPUT_STREAM (source),
+ res, &error);
+ if (written < 0)
+ {
+ complete_async_from_error (data, error);
+ return;
+ }
+
+ data->offset += written;
+
+ if (data->offset == data->length)
+ {
+ g_free (data->buffer);
+ data->buffer = NULL;
+
+ g_data_input_stream_read_until_async (data->data_in,
+ HTTP_END_MARKER,
+ G_PRIORITY_DEFAULT,
+ data->cancellable,
+ reply_read_cb, data);
+
+ }
+ else
+ {
+ do_write (request_write_cb, data);
+ }
+}
+
+static void
+reply_read_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GError *error = NULL;
+ ConnectAsyncData *data = user_data;
+
+ data->buffer = g_data_input_stream_read_until_finish (data->data_in,
+ res, NULL, &error);
+
+ if (data->buffer == NULL)
+ {
+ complete_async_from_error (data, error);
+ return;
+ }
+
+ if (!check_reply (data->buffer, data->has_cred, &error))
+ {
+ complete_async_from_error (data, error);
+ return;
+ }
+
+ g_simple_async_result_complete (data->simple);
+ g_object_unref (data->simple);
+}
+
+static GIOStream *
+wocky_http_proxy_connect_finish (GProxy *proxy,
+ GAsyncResult *result,
+ GError **error)
+{
+ GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (result);
+ ConnectAsyncData *data = g_simple_async_result_get_op_res_gpointer (simple);
+
+ if (g_simple_async_result_propagate_error (simple, error))
+ return NULL;
+
+ return g_object_ref (data->io_stream);
+}
+
+static gboolean
+wocky_http_proxy_supports_hostname (GProxy *proxy)
+{
+ return TRUE;
+}
+
+static void
+wocky_http_proxy_class_init (WockyHttpProxyClass *class)
+{
+}
+
+static void
+wocky_http_proxy_iface_init (GProxyInterface *proxy_iface)
+{
+ proxy_iface->connect = wocky_http_proxy_connect;
+ proxy_iface->connect_async = wocky_http_proxy_connect_async;
+ proxy_iface->connect_finish = wocky_http_proxy_connect_finish;
+ proxy_iface->supports_hostname = wocky_http_proxy_supports_hostname;
+}
+
+struct _WockyHttpsProxy
+{
+ WockyHttpProxy parent;
+};
+
+struct _WockyHttpsProxyClass
+{
+ WockyHttpProxyClass parent_class;
+};
+
+#define wocky_https_proxy_get_type _wocky_https_proxy_get_type
+G_DEFINE_TYPE_WITH_CODE (WockyHttpsProxy, wocky_https_proxy, WOCKY_TYPE_HTTP_PROXY,
+ G_IMPLEMENT_INTERFACE (G_TYPE_PROXY,
+ wocky_http_proxy_iface_init)
+ g_io_extension_point_set_required_type (
+ g_io_extension_point_register (G_PROXY_EXTENSION_POINT_NAME),
+ G_TYPE_PROXY);
+ g_io_extension_point_implement (G_PROXY_EXTENSION_POINT_NAME,
+ g_define_type_id, "https", 0))
+
+static void
+wocky_https_proxy_init (WockyHttpsProxy *proxy)
+{
+}
+
+static void
+wocky_https_proxy_class_init (WockyHttpsProxyClass *class)
+{
+}
diff --git a/src/wocky-http-proxy.h b/src/wocky-http-proxy.h
new file mode 100644
index 0000000..9484b51
--- /dev/null
+++ b/src/wocky-http-proxy.h
@@ -0,0 +1,56 @@
+ /* wocky-http-proxy.h: Header for WockyHttpProxy
+ *
+ * Copyright (C) 2010 Collabora, Ltd.
+ * @author Nicolas Dufresne <nicolas.dufresne@collabora.co.uk>
+ *
+ *
+ * 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+#ifndef _WOCKY_HTTP_PROXY_H_
+#define _WOCKY_HTTP_PROXY_H_
+
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define WOCKY_TYPE_HTTP_PROXY (_wocky_http_proxy_get_type ())
+#define WOCKY_HTTP_PROXY(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), WOCKY_TYPE_HTTP_PROXY, WockyHttpProxy))
+#define WOCKY_HTTP_PROXY_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), WOCKY_TYPE_HTTP_PROXY, WockyHttpProxyClass))
+#define WOCKY_IS_HTTP_PROXY(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), WOCKY_TYPE_HTTP_PROXY))
+#define WOCKY_IS_HTTP_PROXY_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), WOCKY_TYPE_HTTP_PROXY))
+#define WOCKY_HTTP_PROXY_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), WOCKY_TYPE_HTTP_PROXY, WockyHttpProxyClass))
+
+typedef struct _WockyHttpProxy WockyHttpProxy;
+typedef struct _WockyHttpProxyClass WockyHttpProxyClass;
+
+GType _wocky_http_proxy_get_type (void);
+
+#if GLIB_CHECK_VERSION(2, 28, 0)
+#define WOCKY_TYPE_HTTPS_PROXY (_wocky_https_proxy_get_type ())
+#define WOCKY_HTTPS_PROXY(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), WOCKY_TYPE_HTTPS_PROXY, WockyHttpsProxy))
+#define WOCKY_HTTPS_PROXY_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), WOCKY_TYPE_HTTPS_PROXY, WockyHttpsProxyClass))
+#define WOCKY_IS_HTTPS_PROXY(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), WOCKY_TYPE_HTTPS_PROXY))
+#define WOCKY_IS_HTTPS_PROXY_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), WOCKY_TYPE_HTTPS_PROXY))
+#define WOCKY_HTTPS_PROXY_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), WOCKY_TYPE_HTTPS_PROXY, WockyHttpsProxyClass))
+
+typedef struct _WockyHttpsProxy WockyHttpsProxy;
+typedef struct _WockyHttpsProxyClass WockyHttpsProxyClass;
+
+GType _wocky_https_proxy_get_type (void);
+#endif
+
+G_END_DECLS
+
+#endif /* _WOCKY_HTTP_PROXY_H_ */