diff options
author | Marc-André Lureau <marcandre.lureau@redhat.com> | 2015-06-05 17:44:47 +0200 |
---|---|---|
committer | Marc-André Lureau <marcandre.lureau@redhat.com> | 2015-06-08 17:38:58 +0200 |
commit | caf28401cac9ece5e314360f8a3a54479df59727 (patch) | |
tree | 7ada0451442ffa1d9d056500ebd878ad80da3e06 /src | |
parent | 39c315241350dde3741c99fee4fff17b22d2b1fa (diff) | |
download | spice-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')
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_ */ |