diff options
author | Frediano Ziglio <fziglio@redhat.com> | 2017-10-20 11:28:15 +0100 |
---|---|---|
committer | Frediano Ziglio <fziglio@redhat.com> | 2017-10-20 11:28:15 +0100 |
commit | a8fc6814b656221a6d640b4675e990c3e74a2403 (patch) | |
tree | fb8c7f19ceff0a0435539093fd8bb01d485fdeb6 |
Initial public commit
44 files changed, 2454 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..601cc9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +*~ +*.o +*.la +*.lo +*.tar.bz2 +*.tar.gz +.*.sw? +.deps +.dirstamp +.libs +Makefile +Makefile.in +/aclocal.m4 +/autom4te.cache +/build-aux/ +/config.h +/config.h.in +/config.log +/config.status +/configure +/INSTALL +/stamp-h1 +/spice-streaming-agent.spec +/spice-streaming-agent.pc +/data/spice-streaming.desktop +/libtool +/m4/libtool.m4 +/m4/ltoptions.m4 +/m4/ltsugar.m4 +/m4/ltversion.m4 +/m4/lt~obsolete.m4 @@ -0,0 +1 @@ +TO BE DEFINED diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,9 @@ +A Proprietary Red Hat License + +Copyright Red Hat Inc. + +This software follows a GridSDK sample program by NVidia. +Please see LICENSE_NVIDIA for GridSDK. + +Exact License is TBD. + diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..9224646 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,23 @@ +NULL = +SUBDIRS = \ + include/spice-streaming-agent \ + src \ + $(NULL) + +# this will start the program for each user session +xdgautostartdir = $(sysconfdir)/xdg/autostart +xdgautostart_DATA = $(top_srcdir)/data/spice-streaming.desktop + +# this will start the program for the login session +gdmautostartdir = $(datadir)/gdm/greeter/autostart +gdmautostart_DATA = $(top_srcdir)/data/spice-streaming.desktop + +pkgconfigdir = $(datadir)/pkgconfig +pkgconfig_DATA = spice-streaming-agent.pc + +EXTRA_DIST = \ + spice-streaming-agent.spec \ + spice-streaming-agent.pc \ + LICENSE \ + data/spice-streaming.desktop \ + $(NULL) @@ -0,0 +1,52 @@ +Introduction +============ + +The SPICE Streaming Agent is a guest-side daemon which captures the +guest X.Org output, encodes it to H.264 using hardware encoding (NVIDIA-only at +the moment), and forwards the resulting stream to the host to be sent through +SPICE. + + +Virtual Machine Configuration +============================= + +In order to set up streaming, qemu needs to expose a +`com.redhat.stream.0` virtio port, associated with a +corresponding Spice port. + +Using virt-manager +------------------ + +In the hardware details, click on "Add Hardware", then select +"Channel". Add a "Spice port" device type with the +"com.redhat.stream.0" name. You also need to set "Channel" to +"com.redhat.stream.0" + + +Using libvirt +------------- + +[source,xml] +<devices> + <channel type='spiceport'> + <source channel='com.redhat.stream.0'/> + <target type='virtio' name='com.redhat.stream.0'/> + </channel> +</devices> + +Using QEMU +---------- + +[source,sh] +-device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel1,id=channel1,name=com.redhat.stream.0 -chardev spiceport,name=com.redhat.stream.0,id=charchannel1 + + +NVIDIA-specific Configuration +============================= + +In order to use NVIDIA hardware-accelerated encoding, you will need to +configure the virtual machine to use mdev/vfio. An NVIDIA card has to be listed +in lspci output on the guest. + +The guest also needs to have the NVIDIA proprietary drivers installed and in +use. diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..c3467cb --- /dev/null +++ b/configure.ac @@ -0,0 +1,109 @@ +AC_PREREQ([2.57]) + +AC_INIT(spice-streaming-agent, 0.1, + [spice-devel@lists.freedesktop.org]) + +AM_CONFIG_HEADER([config.h]) +AC_CONFIG_MACRO_DIR([m4]) +AC_CONFIG_AUX_DIR([build-aux]) + +AM_INIT_AUTOMAKE([dist-bzip2 no-dist-gzip subdir-objects]) +AM_MAINTAINER_MODE + +m4_ifdef([AM_SILENT_RULES],[AM_SILENT_RULES([yes])]) + +AC_PROG_CC +AC_PROG_CC_C99 +if test x"$ac_cv_prog_cc_c99" = xno; then + AC_MSG_ERROR([C99 compiler is required.]) +fi +AC_PROG_CXX +AX_CXX_COMPILE_STDCXX_11 +AC_PROG_INSTALL +AC_CANONICAL_HOST +LT_INIT([disable-static]) +AM_PROG_CC_C_O +AC_C_BIGENDIAN +PKG_PROG_PKG_CONFIG + +dnl ========================================================================= +dnl Check deps + +SPICE_PROTOCOL_MIN_VER=0.12.14 +PKG_CHECK_MODULES([SPICE_PROTOCOL], [spice-protocol >= $SPICE_PROTOCOL_MIN_VER]) +AC_SUBST([SPICE_PROTOCOL_MIN_VER]) +SAVE_CFLAGS="$CFLAGS" +CFLAGS="$SPICE_PROTOCOL_CFLAGS $CFLAGS" +AC_CHECK_HEADER([spice/stream-device.h],,[AC_MSG_ERROR([Could not locate spice-protocol stream-device.h header])]) +CFLAGS="$SAVE_CFLAGS" + +PKG_CHECK_MODULES(X11, x11) +PKG_CHECK_MODULES(XFIXES, xfixes) + +AC_CHECK_LIB(jpeg, jpeg_destroy_decompress, + AC_MSG_CHECKING([for jpeglib.h]) + AC_TRY_CPP( +[#include <stdio.h> +#undef PACKAGE +#undef VERSION +#undef HAVE_STDLIB_H +#include <jpeglib.h>], + JPEG_LIBS='-ljpeg' + AC_MSG_RESULT($jpeg_ok), + AC_MSG_ERROR([jpeglib.h not found])), + AC_MSG_ERROR([libjpeg not found])) +AC_SUBST(JPEG_LIBS) + +dnl =========================================================================== +dnl check compiler flags + +SPICE_COMPILE_WARNINGS +LIBVIRT_LINKER_RELRO +LIBVIRT_LINKER_NO_INDIRECT + +AC_SUBST(WARN_CFLAGS) +AC_SUBST(WARN_CXXFLAGS) + +dnl ========================================================================= +dnl -fvisibility stuff + +have_gcc4=no +AC_MSG_CHECKING(for -fvisibility) +AC_COMPILE_IFELSE([AC_LANG_SOURCE([ +#if defined(__GNUC__) && (__GNUC__ >= 4) +#else +error Need GCC 4.0 for visibility +#endif +int main () { return 0; } +])], have_gcc4=yes) + +if test "x$have_gcc4" = "xyes" && test ! "$os_win32" = "yes" ; then + VISIBILITY_HIDDEN_CFLAGS="-fvisibility=hidden" +fi +AC_MSG_RESULT($have_gcc4) +AC_SUBST(VISIBILITY_HIDDEN_CFLAGS) + +AC_DEFINE_DIR([BINDIR], [bindir], [Where data are placed to.]) + +AC_OUTPUT([ +spice-streaming-agent.spec +data/spice-streaming.desktop +Makefile +src/Makefile +src/unittests/Makefile +include/spice-streaming-agent/Makefile +spice-streaming-agent.pc +]) + +dnl ========================================================================== +AC_MSG_NOTICE([ + + SPICE streaming agent $VERSION + ============================== + + prefix: ${prefix} + C compiler: ${CC} + C++ compiler: ${CXX} + + Now type 'make' to build $PACKAGE +]) diff --git a/data/90-spice-guest-streaming.rules b/data/90-spice-guest-streaming.rules new file mode 100644 index 0000000..6cedd5c --- /dev/null +++ b/data/90-spice-guest-streaming.rules @@ -0,0 +1,2 @@ +ACTION=="add", SUBSYSTEM=="virtio-ports", ENV{DEVLINKS}=="/dev/virtio-ports/com.redhat.stream.0", MODE="0666" + diff --git a/data/spice-streaming.desktop.in b/data/spice-streaming.desktop.in new file mode 100644 index 0000000..dcb30a3 --- /dev/null +++ b/data/spice-streaming.desktop.in @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=SPICE Streaming Agent +Comment=Agent for Streaming the framebuffer to Spice +Exec=@BINDIR@/spice-streaming-agent +#TryExec=/usr/bin/spice-streaming-agent +Terminal=false +Type=Application +Categories= +X-GNOME-Autostart-Phase=Application diff --git a/include/spice-streaming-agent/Makefile.am b/include/spice-streaming-agent/Makefile.am new file mode 100644 index 0000000..844f791 --- /dev/null +++ b/include/spice-streaming-agent/Makefile.am @@ -0,0 +1,7 @@ +NULL = +public_includedir = $(includedir)/spice-streaming-agent +public_include_HEADERS = \ + frame-capture.hpp \ + plugin.hpp \ + $(NULL) + diff --git a/include/spice-streaming-agent/frame-capture.hpp b/include/spice-streaming-agent/frame-capture.hpp new file mode 100644 index 0000000..f226e99 --- /dev/null +++ b/include/spice-streaming-agent/frame-capture.hpp @@ -0,0 +1,62 @@ +/* Common interface for all streaming / capture cards + * used by SPICE streaming-agent. + * + * \copyright + * Copyright 2016-2017 Red Hat Inc. All rights reserved. + */ +#ifndef SPICE_STREAMING_AGENT_FRAME_CAPTURE_HPP +#define SPICE_STREAMING_AGENT_FRAME_CAPTURE_HPP +#include <cstdio> + +#include <spice/enums.h> + +namespace SpiceStreamingAgent { + +struct FrameSize +{ + unsigned width; + unsigned height; +}; + +struct FrameInfo +{ + FrameSize size; + /*! Memory buffer, valid till next frame is read */ + const void *buffer; + size_t buffer_size; + /*! Start of a new stream */ + bool stream_start; +}; + +/*! + * Pure base class implementing the frame capture + */ +class FrameCapture +{ +public: + virtual ~FrameCapture()=default; + + /*! Grab a frame + * This function will wait for next frame. + * Capture is started if needed. + */ + virtual FrameInfo CaptureFrame()=0; + + /*! Reset capturing + * This will reset to beginning state + */ + virtual void Reset()=0; + + /*! + * Get video codec used to encode last frame + */ + virtual SpiceVideoCodecType VideoCodecType() const=0; +protected: + FrameCapture()=default; + FrameCapture(const FrameCapture&)=delete; + void operator=(const FrameCapture&)=delete; +}; + +} + +#endif // SPICE_STREAMING_AGENT_FRAME_CAPTURE_HPP diff --git a/include/spice-streaming-agent/plugin.hpp b/include/spice-streaming-agent/plugin.hpp new file mode 100644 index 0000000..727cb3b --- /dev/null +++ b/include/spice-streaming-agent/plugin.hpp @@ -0,0 +1,152 @@ +/* Plugin interface for all streaming / capture cards + * used by SPICE streaming-agent. + * + * \copyright + * Copyright 2017 Red Hat Inc. All rights reserved. + */ +#ifndef SPICE_STREAMING_AGENT_PLUGIN_HPP +#define SPICE_STREAMING_AGENT_PLUGIN_HPP + +#include <spice/enums.h> + +/*! + * \file + * \brief Plugin interface + * + * Each module loaded by the agent should implement one or more + * Plugins and register them. + */ + +namespace SpiceStreamingAgent { + +class FrameCapture; + +/*! + * Plugin version, only using few bits, schema is 0xMMmm + * where MM is major and mm is the minor, can be easily expanded + * using more bits in the future + */ +enum Constants : unsigned { PluginVersion = 0x100u }; + +enum Ranks : unsigned { + /// this plugin should not be used + DontUse = 0, + /// use plugin only as a fallback + FallBackMin = 1, + /// plugin supports encoding in software + SoftwareMin = 0x40000000, + /// plugin supports encoding in hardware + HardwareMin = 0x80000000, + /// plugin provides access to specific card hardware not only for compression + SpecificHardwareMin = 0xC0000000 +}; + +/*! + * Configuration option. + * An array of these will be passed to the plugin. + * Simply a couple name and value passed as string. + * For instance "framerate" and "20". + */ +struct ConfigureOption +{ + const char *name; + const char *value; +}; + +/*! + * Interface a plugin should implement and register to the Agent. + * + * A plugin module can register multiple Plugin interfaces to handle + * multiple codecs. In this case each Plugin will report data for a + * specific codec. + */ +class Plugin +{ +public: + /*! + * Allows to free the plugin when not needed + */ + virtual ~Plugin() {}; + + /*! + * Request an object for getting frames. + * Plugin should return proper object or nullptr if not possible + * to initialize. + * Plugin can also raise std::runtime_error which will be logged. + */ + virtual FrameCapture *CreateCapture()=0; + + /*! + * Request to rank the plugin. + * See Ranks enumeration for details on ranges. + * \return Ranks::DontUse if not possible to use the plugin, this + * is necessary as the condition for capturing frames can change + * from the time the plugin decided to register and now. + */ + virtual unsigned Rank()=0; + + /*! + * Get video codec used to encode last frame + */ + virtual SpiceVideoCodecType VideoCodecType() const=0; +}; + +/*! + * Interface the plugin should use to interact with the agent. + * The agent will pass it to the entry point. + * Exporting functions from an executable in Windows OS is not easy + * and a standard way to do it so better to implement the interface + * that way for compatibility. + */ +class Agent +{ +public: + /*! + * Get agent version. + * Plugin should check the version for compatibility before doing + * everything. + * \return version specified like PluginVersion + */ + virtual unsigned Version() const=0; + + /*! + * Check if a given plugin version is compatible with this agent + * \return true is compatible + */ + virtual bool PluginVersionIsCompatible(unsigned pluginVersion) const=0; + + /*! + * Register a plugin in the system. + */ + virtual void Register(Plugin& plugin)=0; + + /*! + * Get options array. + * Array is terminated with {nullptr, nullptr}. + * Never nullptr. + * \todo passing options to entry point instead? + */ + virtual const ConfigureOption* Options() const=0; +}; + +typedef bool PluginInitFunc(SpiceStreamingAgent::Agent* agent); + +} + +#ifndef SPICE_STREAMING_AGENT_PROGRAM +/*! + * Plugin main entry point. + * Plugins should check if the version of the agent is compatible. + * If is compatible should register itself to the agent and return + * true. + * If is not compatible can decide to stay in memory or not returning + * true (do not unload) or false (safe to unload). This is necessary + * if the plugin uses some library which are not safe to be unloaded. + * This public interface is also designed to avoid exporting data from + * the plugin which could be a problem in some systems. + * \return true if plugin should stay loaded, false otherwise + */ +extern "C" SpiceStreamingAgent::PluginInitFunc spice_streaming_agent_plugin_init; +#endif + +#endif // SPICE_STREAMING_AGENT_PLUGIN_HPP diff --git a/m4/ac_define_dir.m4 b/m4/ac_define_dir.m4 new file mode 100644 index 0000000..e15cea2 --- /dev/null +++ b/m4/ac_define_dir.m4 @@ -0,0 +1,34 @@ +dnl @synopsis AC_DEFINE_DIR(VARNAME, DIR [, DESCRIPTION]) +dnl +dnl This macro sets VARNAME to the expansion of the DIR variable, +dnl taking care of fixing up ${prefix} and such. +dnl +dnl VARNAME is then offered as both an output variable and a C +dnl preprocessor symbol. +dnl +dnl Example: +dnl +dnl AC_DEFINE_DIR([DATADIR], [datadir], [Where data are placed to.]) +dnl +dnl @category Misc +dnl @author Stepan Kasal <kasal@ucw.cz> +dnl @author Andreas Schwab <schwab@suse.de> +dnl @author Guido U. Draheim <guidod@gmx.de> +dnl @author Alexandre Oliva +dnl @version 2006-10-13 +dnl @license AllPermissive + +AC_DEFUN([AC_DEFINE_DIR], [ + prefix_NONE= + exec_prefix_NONE= + test "x$prefix" = xNONE && prefix_NONE=yes && prefix=$ac_default_prefix + test "x$exec_prefix" = xNONE && exec_prefix_NONE=yes && exec_prefix=$prefix +dnl In Autoconf 2.60, ${datadir} refers to ${datarootdir}, which in turn +dnl refers to ${prefix}. Thus we have to use `eval' twice. + eval ac_define_dir="\"[$]$2\"" + eval ac_define_dir="\"$ac_define_dir\"" + AC_SUBST($1, "$ac_define_dir") + AC_DEFINE_UNQUOTED($1, "$ac_define_dir", [$3]) + test "$prefix_NONE" && prefix=NONE + test "$exec_prefix_NONE" && exec_prefix=NONE +]) diff --git a/m4/manywarnings.m4 b/m4/manywarnings.m4 new file mode 100644 index 0000000..4f701f4 --- /dev/null +++ b/m4/manywarnings.m4 @@ -0,0 +1,276 @@ +# manywarnings.m4 serial 8 +dnl Copyright (C) 2008-2016 Free Software Foundation, Inc. +dnl This file is free software; the Free Software Foundation +dnl gives unlimited permission to copy and/or distribute it, +dnl with or without modifications, as long as this notice is preserved. + +dnl From Simon Josefsson + +# gl_MANYWARN_COMPLEMENT(OUTVAR, LISTVAR, REMOVEVAR) +# -------------------------------------------------- +# Copy LISTVAR to OUTVAR except for the entries in REMOVEVAR. +# Elements separated by whitespace. In set logic terms, the function +# does OUTVAR = LISTVAR \ REMOVEVAR. +AC_DEFUN([gl_MANYWARN_COMPLEMENT], +[ + gl_warn_set= + set x $2; shift + for gl_warn_item + do + case " $3 " in + *" $gl_warn_item "*) + ;; + *) + gl_warn_set="$gl_warn_set $gl_warn_item" + ;; + esac + done + $1=$gl_warn_set +]) + +# gl_MANYWARN_ALL_GCC(VARIABLE) +# ----------------------------- +# Add all documented GCC warning parameters to variable VARIABLE. +# Note that you need to test them using gl_WARN_ADD if you want to +# make sure your gcc understands it. +AC_DEFUN([gl_MANYWARN_ALL_GCC], +[ + dnl First, check for some issues that only occur when combining multiple + dnl gcc warning categories. + AC_REQUIRE([AC_PROG_CC]) + if test -n "$GCC"; then + + dnl Check if -W -Werror -Wno-missing-field-initializers is supported + dnl with the current $CC $CFLAGS $CPPFLAGS. + AC_MSG_CHECKING([whether -Wno-missing-field-initializers is supported]) + AC_CACHE_VAL([gl_cv_cc_nomfi_supported], [ + gl_save_CFLAGS="$CFLAGS" + CFLAGS="$CFLAGS -W -Werror -Wno-missing-field-initializers" + AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM([[]], [[]])], + [gl_cv_cc_nomfi_supported=yes], + [gl_cv_cc_nomfi_supported=no]) + CFLAGS="$gl_save_CFLAGS"]) + AC_MSG_RESULT([$gl_cv_cc_nomfi_supported]) + + if test "$gl_cv_cc_nomfi_supported" = yes; then + dnl Now check whether -Wno-missing-field-initializers is needed + dnl for the { 0, } construct. + AC_MSG_CHECKING([whether -Wno-missing-field-initializers is needed]) + AC_CACHE_VAL([gl_cv_cc_nomfi_needed], [ + gl_save_CFLAGS="$CFLAGS" + CFLAGS="$CFLAGS -W -Werror" + AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM( + [[int f (void) + { + typedef struct { int a; int b; } s_t; + s_t s1 = { 0, }; + return s1.b; + } + ]], + [[]])], + [gl_cv_cc_nomfi_needed=no], + [gl_cv_cc_nomfi_needed=yes]) + CFLAGS="$gl_save_CFLAGS" + ]) + AC_MSG_RESULT([$gl_cv_cc_nomfi_needed]) + fi + + dnl Next, check if -Werror -Wuninitialized is useful with the + dnl user's choice of $CFLAGS; some versions of gcc warn that it + dnl has no effect if -O is not also used + AC_MSG_CHECKING([whether -Wuninitialized is supported]) + AC_CACHE_VAL([gl_cv_cc_uninitialized_supported], [ + gl_save_CFLAGS="$CFLAGS" + CFLAGS="$CFLAGS -Werror -Wuninitialized" + AC_COMPILE_IFELSE( + [AC_LANG_PROGRAM([[]], [[]])], + [gl_cv_cc_uninitialized_supported=yes], + [gl_cv_cc_uninitialized_supported=no]) + CFLAGS="$gl_save_CFLAGS"]) + AC_MSG_RESULT([$gl_cv_cc_uninitialized_supported]) + + fi + + # List all gcc warning categories. + # To compare this list to your installed GCC's, run this Bash command: + # + # comm -3 \ + # <(sed -n 's/^ *\(-[^ ]*\) .*/\1/p' manywarnings.m4 | sort) \ + # <(gcc --help=warnings | sed -n 's/^ \(-[^ ]*\) .*/\1/p' | sort | + # grep -v -x -f <( + # awk '/^[^#]/ {print $1}' ../build-aux/gcc-warning.spec)) + + gl_manywarn_set= + for gl_manywarn_item in \ + -fno-common \ + -W \ + -Wabi \ + -Waddress \ + -Waggressive-loop-optimizations \ + -Wall \ + -Wattributes \ + -Wbad-function-cast \ + -Wbool-compare \ + -Wbuiltin-macro-redefined \ + -Wcast-align \ + -Wchar-subscripts \ + -Wchkp \ + -Wclobbered \ + -Wcomment \ + -Wcomments \ + -Wcoverage-mismatch \ + -Wcpp \ + -Wdate-time \ + -Wdeprecated \ + -Wdeprecated-declarations \ + -Wdesignated-init \ + -Wdisabled-optimization \ + -Wdiscarded-array-qualifiers \ + -Wdiscarded-qualifiers \ + -Wdiv-by-zero \ + -Wdouble-promotion \ + -Wduplicated-cond \ + -Wempty-body \ + -Wendif-labels \ + -Wenum-compare \ + -Wextra \ + -Wformat-contains-nul \ + -Wformat-extra-args \ + -Wformat-nonliteral \ + -Wformat-security \ + -Wformat-signedness \ + -Wformat-y2k \ + -Wformat-zero-length \ + -Wframe-address \ + -Wfree-nonheap-object \ + -Whsa \ + -Wignored-attributes \ + -Wignored-qualifiers \ + -Wimplicit \ + -Wimplicit-function-declaration \ + -Wimplicit-int \ + -Wincompatible-pointer-types \ + -Winit-self \ + -Winline \ + -Wint-conversion \ + -Wint-to-pointer-cast \ + -Winvalid-memory-model \ + -Winvalid-pch \ + -Wjump-misses-init \ + -Wlogical-not-parentheses \ + -Wlogical-op \ + -Wmain \ + -Wmaybe-uninitialized \ + -Wmemset-transposed-args \ + -Wmisleading-indentation \ + -Wmissing-braces \ + -Wmissing-declarations \ + -Wmissing-field-initializers \ + -Wmissing-include-dirs \ + -Wmissing-parameter-type \ + -Wmissing-prototypes \ + -Wmultichar \ + -Wnarrowing \ + -Wnested-externs \ + -Wnonnull \ + -Wnonnull-compare \ + -Wnull-dereference \ + -Wodr \ + -Wold-style-declaration \ + -Wold-style-definition \ + -Wopenmp-simd \ + -Woverflow \ + -Woverlength-strings \ + -Woverride-init \ + -Wpacked \ + -Wpacked-bitfield-compat \ + -Wparentheses \ + -Wpointer-arith \ + -Wpointer-sign \ + -Wpointer-to-int-cast \ + -Wpragmas \ + -Wreturn-local-addr \ + -Wreturn-type \ + -Wscalar-storage-order \ + -Wsequence-point \ + -Wshadow \ + -Wshift-count-negative \ + -Wshift-count-overflow \ + -Wshift-negative-value \ + -Wsizeof-array-argument \ + -Wsizeof-pointer-memaccess \ + -Wstack-protector \ + -Wstrict-aliasing \ + -Wstrict-overflow \ + -Wstrict-prototypes \ + -Wsuggest-attribute=const \ + -Wsuggest-attribute=format \ + -Wsuggest-attribute=noreturn \ + -Wsuggest-attribute=pure \ + -Wsuggest-final-methods \ + -Wsuggest-final-types \ + -Wswitch \ + -Wswitch-bool \ + -Wswitch-default \ + -Wsync-nand \ + -Wsystem-headers \ + -Wtautological-compare \ + -Wtrampolines \ + -Wtrigraphs \ + -Wtype-limits \ + -Wuninitialized \ + -Wunknown-pragmas \ + -Wunsafe-loop-optimizations \ + -Wunused \ + -Wunused-but-set-parameter \ + -Wunused-but-set-variable \ + -Wunused-function \ + -Wunused-label \ + -Wunused-local-typedefs \ + -Wunused-macros \ + -Wunused-parameter \ + -Wunused-result \ + -Wunused-value \ + -Wunused-variable \ + -Wvarargs \ + -Wvariadic-macros \ + -Wvector-operation-performance \ + -Wvla \ + -Wvolatile-register-var \ + -Wwrite-strings \ + \ + ; do + gl_manywarn_set="$gl_manywarn_set $gl_manywarn_item" + done + + # gcc --help=warnings outputs an unusual form for these options; list + # them here so that the above 'comm' command doesn't report a false match. + gl_manywarn_set="$gl_manywarn_set -Warray-bounds=2" + gl_manywarn_set="$gl_manywarn_set -Wnormalized=nfc" + gl_manywarn_set="$gl_manywarn_set -Wshift-overflow=2" + gl_manywarn_set="$gl_manywarn_set -Wunused-const-variable=2" + + # These are needed for older GCC versions. + if test -n "$GCC"; then + case `($CC --version) 2>/dev/null` in + 'gcc (GCC) '[[0-3]].* | \ + 'gcc (GCC) '4.[[0-7]].*) + gl_manywarn_set="$gl_manywarn_set -fdiagnostics-show-option" + gl_manywarn_set="$gl_manywarn_set -funit-at-a-time" + ;; + esac + fi + + # Disable specific options as needed. + if test "$gl_cv_cc_nomfi_needed" = yes; then + gl_manywarn_set="$gl_manywarn_set -Wno-missing-field-initializers" + fi + + if test "$gl_cv_cc_uninitialized_supported" = no; then + gl_manywarn_set="$gl_manywarn_set -Wno-uninitialized" + fi + + $1=$gl_manywarn_set +]) diff --git a/m4/spice-compile-warnings.m4 b/m4/spice-compile-warnings.m4 new file mode 100644 index 0000000..66d7179 --- /dev/null +++ b/m4/spice-compile-warnings.m4 @@ -0,0 +1,183 @@ +dnl +dnl Enable all known GCC compiler warnings, except for those +dnl we can't yet cope with +dnl +AC_DEFUN([SPICE_COMPILE_WARNINGS],[ + dnl ****************************** + dnl More compiler warnings + dnl ****************************** + + AC_ARG_ENABLE([werror], + AS_HELP_STRING([--enable-werror], [Use -Werror (if supported)]), + [set_werror="$enableval"], + [if test -d $srcdir/.git; then + is_git_version=true + set_werror=yes + else + set_werror=no + fi]) + + # List of warnings that are not relevant / wanted + + # Don't care about C++ compiler compat + dontwarn="$dontwarn -Wc++-compat" + dontwarn="$dontwarn -Wabi" + dontwarn="$dontwarn -Wdeprecated" + # For older gcc versions, -Wenum-compare is "C++ and Objective-C++ only" + # For newer gcc versions, -Wenum-compare is "enabled by -Wall" + dontwarn="$dontwarn -Wenum-compare" + # Don't care about ancient C standard compat + dontwarn="$dontwarn -Wtraditional" + # Don't care about ancient C standard compat + dontwarn="$dontwarn -Wtraditional-conversion" + # Ignore warnings in /usr/include + dontwarn="$dontwarn -Wsystem-headers" + # Happy for compiler to add struct padding + dontwarn="$dontwarn -Wpadded" + # GCC very confused with -O2 + dontwarn="$dontwarn -Wunreachable-code" + + + dontwarn="$dontwarn -Wconversion" + dontwarn="$dontwarn -Wsign-conversion" + dontwarn="$dontwarn -Wvla" + dontwarn="$dontwarn -Wundef" + dontwarn="$dontwarn -Wcast-qual" + dontwarn="$dontwarn -Wlong-long" + dontwarn="$dontwarn -Wswitch-default" + dontwarn="$dontwarn -Wswitch-enum" + dontwarn="$dontwarn -Wstrict-overflow" + dontwarn="$dontwarn -Wunsafe-loop-optimizations" + dontwarn="$dontwarn -Wformat-nonliteral" + dontwarn="$dontwarn -Wfloat-equal" + dontwarn="$dontwarn -Wdeclaration-after-statement" + dontwarn="$dontwarn -Wcast-qual" + dontwarn="$dontwarn -Wconversion" + dontwarn="$dontwarn -Wsign-conversion" + dontwarn="$dontwarn -Wpacked" + dontwarn="$dontwarn -Wunused-macros" + dontwarn="$dontwarn -Woverlength-strings" + dontwarn="$dontwarn -Wstack-protector" + dontwarn="$dontwarn -Winline" + dontwarn="$dontwarn -Wbad-function-cast" + dontwarn="$dontwarn -Wshadow" + dontwarn="$dontwarn -Wformat-signedness" + dontwarn="$dontwarn -Wnull-dereference" + + # This causes an error to be detected in glib headers + dontwarn="$dontwarn -Wshift-overflow=2" + + # Stuff that C++ won't allow. Turn them back on later + dontwarn="$dontwarn -Wdesignated-init" + dontwarn="$dontwarn -Wdiscarded-array-qualifiers" + dontwarn="$dontwarn -Wdiscarded-qualifiers" + dontwarn="$dontwarn -Wimplicit-function-declaration" + dontwarn="$dontwarn -Wimplicit-int" + dontwarn="$dontwarn -Wimplicit" + dontwarn="$dontwarn -Wincompatible-pointer-types" + dontwarn="$dontwarn -Wint-conversion" + dontwarn="$dontwarn -Wjump-misses-init" + dontwarn="$dontwarn -Wmissing-parameter-type" + dontwarn="$dontwarn -Wmissing-prototypes" + dontwarn="$dontwarn -Wnested-externs" + dontwarn="$dontwarn -Wold-style-declaration" + dontwarn="$dontwarn -Wold-style-definition" + dontwarn="$dontwarn -Woverride-init" + dontwarn="$dontwarn -Wpointer-sign" + dontwarn="$dontwarn -Wpointer-to-int-cast" + dontwarn="$dontwarn -Wstrict-prototypes" + + # We want to enable these, but need to sort out the + # decl mess with gtk/generated_*.c + dontwarn="$dontwarn -Wmissing-declarations" + + # Get all possible GCC warnings + gl_MANYWARN_ALL_GCC([maybewarn]) + + # Remove the ones we don't want, blacklisted earlier + gl_MANYWARN_COMPLEMENT([wantwarn], [$maybewarn], [$dontwarn]) + + # Check for $CC support of each warning + for w in $wantwarn; do + gl_WARN_ADD([$w]) + done + + # GNULIB uses '-W' (aka -Wextra) which includes a bunch of stuff. + # Unfortunately, this means you can't simply use '-Wsign-compare' + # with gl_MANYWARN_COMPLEMENT + # So we have -W enabled, and then have to explicitly turn off... + gl_WARN_ADD([-Wno-sign-compare]) + gl_WARN_ADD([-Wno-unused-parameter]) + # We can't enable this due to horrible spice_usb_device_get_description + # signature + gl_WARN_ADD([-Wno-format-nonliteral]) + + # This should be < 1024 really. pixman_utils is the blackspot + # preventing lower usage + gl_WARN_ADD([-Wframe-larger-than=20460]) + + # Use improved glibc headers + AH_VERBATIM([FORTIFY_SOURCE], + [/* Enable compile-time and run-time bounds-checking, and some warnings. */ +#if !defined _FORTIFY_SOURCE && defined __OPTIMIZE__ && __OPTIMIZE__ +# define _FORTIFY_SOURCE 2 +#endif +]) + + # Extra special flags + dnl -fstack-protector stuff passes gl_WARN_ADD with gcc + dnl on Mingw32, but fails when actually used + case $host in + *-*-linux*) + dnl Fedora only uses -fstack-protector, but doesn't seem to + dnl be great overhead in adding -fstack-protector-all instead + dnl gl_WARN_ADD([-fstack-protector]) + gl_WARN_ADD([-fstack-protector-all]) + gl_WARN_ADD([--param=ssp-buffer-size=4]) + ;; + esac + gl_WARN_ADD([-fexceptions]) + gl_WARN_ADD([-fasynchronous-unwind-tables]) + gl_WARN_ADD([-fdiagnostics-show-option]) + gl_WARN_ADD([-funit-at-a-time]) + + # Need -fipa-pure-const in order to make -Wsuggest-attribute=pure + # fire even without -O. + gl_WARN_ADD([-fipa-pure-const]) + + # We should eventually enable this, but right now there are at + # least 75 functions triggering warnings. + gl_WARN_ADD([-Wno-suggest-attribute=pure]) + gl_WARN_ADD([-Wno-suggest-attribute=const]) + + if test "$set_werror" = "yes" + then + gl_WARN_ADD([-Werror]) + fi + WARN_CXXFLAGS=$WARN_CFLAGS + AC_SUBST([WARN_CXXFLAGS]) + + # These are C-only warnings + gl_WARN_ADD([-Wdesignated-init]) + gl_WARN_ADD([-Wdiscarded-array-qualifiers]) + gl_WARN_ADD([-Wdiscarded-qualifiers]) + gl_WARN_ADD([-Wimplicit-function-declaration]) + gl_WARN_ADD([-Wimplicit-int]) + gl_WARN_ADD([-Wimplicit]) + gl_WARN_ADD([-Wincompatible-pointer-types]) + gl_WARN_ADD([-Wint-conversion]) + gl_WARN_ADD([-Wjump-misses-init]) + gl_WARN_ADD([-Wmissing-parameter-type]) + gl_WARN_ADD([-Wmissing-prototypes]) + gl_WARN_ADD([-Wnested-externs]) + gl_WARN_ADD([-Wold-style-declaration]) + gl_WARN_ADD([-Wold-style-definition]) + gl_WARN_ADD([-Woverride-init]) + gl_WARN_ADD([-Wpointer-sign]) + gl_WARN_ADD([-Wpointer-to-int-cast]) + gl_WARN_ADD([-Wstrict-prototypes]) + + WARN_LDFLAGS=$WARN_CFLAGS + AC_SUBST([WARN_CFLAGS]) + AC_SUBST([WARN_LDFLAGS]) +]) diff --git a/m4/virt-linker-no-indirect.m4 b/m4/virt-linker-no-indirect.m4 new file mode 100644 index 0000000..b344f70 --- /dev/null +++ b/m4/virt-linker-no-indirect.m4 @@ -0,0 +1,32 @@ +dnl +dnl Check for --no-copy-dt-needed-entries +dnl +dnl Copyright (C) 2013 Guido Günther <agx@sigxcpu.org> +dnl +dnl This library is free software; you can redistribute it and/or +dnl modify it under the terms of the GNU Lesser General Public +dnl License as published by the Free Software Foundation; either +dnl version 2.1 of the License, or (at your option) any later version. +dnl +dnl This library is distributed in the hope that it will be useful, +dnl but WITHOUT ANY WARRANTY; without even the implied warranty of +dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +dnl Lesser General Public License for more details. +dnl +dnl You should have received a copy of the GNU Lesser General Public +dnl License along with this library. If not, see +dnl <http://www.gnu.org/licenses/>. +dnl + +AC_DEFUN([LIBVIRT_LINKER_NO_INDIRECT],[ + AC_MSG_CHECKING([for how to avoid indirect lib deps]) + + NO_INDIRECT_LDFLAGS= + case `$LD --help 2>&1` in + *"--no-copy-dt-needed-entries"*) + NO_INDIRECT_LDFLAGS="-Wl,--no-copy-dt-needed-entries" ;; + esac + AC_SUBST([NO_INDIRECT_LDFLAGS]) + + AC_MSG_RESULT([$NO_INDIRECT_LDFLAGS]) +]) diff --git a/m4/virt-linker-relro.m4 b/m4/virt-linker-relro.m4 new file mode 100644 index 0000000..079a095 --- /dev/null +++ b/m4/virt-linker-relro.m4 @@ -0,0 +1,35 @@ +dnl +dnl Check for -z now and -z relro linker flags +dnl +dnl Copyright (C) 2013 Red Hat, Inc. +dnl +dnl This library is free software; you can redistribute it and/or +dnl modify it under the terms of the GNU Lesser General Public +dnl License as published by the Free Software Foundation; either +dnl version 2.1 of the License, or (at your option) any later version. +dnl +dnl This library is distributed in the hope that it will be useful, +dnl but WITHOUT ANY WARRANTY; without even the implied warranty of +dnl MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +dnl Lesser General Public License for more details. +dnl +dnl You should have received a copy of the GNU Lesser General Public +dnl License along with this library. If not, see +dnl <http://www.gnu.org/licenses/>. +dnl + +AC_DEFUN([LIBVIRT_LINKER_RELRO],[ + AC_MSG_CHECKING([for how to force completely read-only GOT table]) + + RELRO_LDFLAGS= + ld_help=`$LD --help 2>&1` + case $ld_help in + *"-z relro"*) RELRO_LDFLAGS="-Wl,-z -Wl,relro" ;; + esac + case $ld_help in + *"-z now"*) RELRO_LDFLAGS="$RELRO_LDFLAGS -Wl,-z -Wl,now" ;; + esac + AC_SUBST([RELRO_LDFLAGS]) + + AC_MSG_RESULT([$RELRO_LDFLAGS]) +]) diff --git a/m4/warnings.m4 b/m4/warnings.m4 new file mode 100644 index 0000000..e3d239b --- /dev/null +++ b/m4/warnings.m4 @@ -0,0 +1,79 @@ +# warnings.m4 serial 11 +dnl Copyright (C) 2008-2013 Free Software Foundation, Inc. +dnl This file is free software; the Free Software Foundation +dnl gives unlimited permission to copy and/or distribute it, +dnl with or without modifications, as long as this notice is preserved. + +dnl From Simon Josefsson + +# gl_AS_VAR_APPEND(VAR, VALUE) +# ---------------------------- +# Provide the functionality of AS_VAR_APPEND if Autoconf does not have it. +m4_ifdef([AS_VAR_APPEND], +[m4_copy([AS_VAR_APPEND], [gl_AS_VAR_APPEND])], +[m4_define([gl_AS_VAR_APPEND], +[AS_VAR_SET([$1], [AS_VAR_GET([$1])$2])])]) + + +# gl_COMPILER_OPTION_IF(OPTION, [IF-SUPPORTED], [IF-NOT-SUPPORTED], +# [PROGRAM = AC_LANG_PROGRAM()]) +# ----------------------------------------------------------------- +# Check if the compiler supports OPTION when compiling PROGRAM. +# +# FIXME: gl_Warn must be used unquoted until we can assume Autoconf +# 2.64 or newer. +AC_DEFUN([gl_COMPILER_OPTION_IF], +[AS_VAR_PUSHDEF([gl_Warn], [gl_cv_warn_[]_AC_LANG_ABBREV[]_$1])dnl +AS_VAR_PUSHDEF([gl_Flags], [_AC_LANG_PREFIX[]FLAGS])dnl +AS_LITERAL_IF([$1], + [m4_pushdef([gl_Positive], m4_bpatsubst([$1], [^-Wno-], [-W]))], + [gl_positive="$1" +case $gl_positive in + -Wno-*) gl_positive=-W`expr "X$gl_positive" : 'X-Wno-\(.*\)'` ;; +esac +m4_pushdef([gl_Positive], [$gl_positive])])dnl +AC_CACHE_CHECK([whether _AC_LANG compiler handles $1], m4_defn([gl_Warn]), [ + gl_save_compiler_FLAGS="$gl_Flags" + gl_AS_VAR_APPEND(m4_defn([gl_Flags]), + [" $gl_unknown_warnings_are_errors ]m4_defn([gl_Positive])["]) + AC_LINK_IFELSE([m4_default([$4], [AC_LANG_PROGRAM([])])], + [AS_VAR_SET(gl_Warn, [yes])], + [AS_VAR_SET(gl_Warn, [no])]) + gl_Flags="$gl_save_compiler_FLAGS" +]) +AS_VAR_IF(gl_Warn, [yes], [$2], [$3]) +m4_popdef([gl_Positive])dnl +AS_VAR_POPDEF([gl_Flags])dnl +AS_VAR_POPDEF([gl_Warn])dnl +]) + +# gl_UNKNOWN_WARNINGS_ARE_ERRORS +# ------------------------------ +# Clang doesn't complain about unknown warning options unless one also +# specifies -Wunknown-warning-option -Werror. Detect this. +AC_DEFUN([gl_UNKNOWN_WARNINGS_ARE_ERRORS], +[gl_COMPILER_OPTION_IF([-Werror -Wunknown-warning-option], + [gl_unknown_warnings_are_errors='-Wunknown-warning-option -Werror'], + [gl_unknown_warnings_are_errors=])]) + +# gl_WARN_ADD(OPTION, [VARIABLE = WARN_CFLAGS], +# [PROGRAM = AC_LANG_PROGRAM()]) +# --------------------------------------------- +# Adds parameter to WARN_CFLAGS if the compiler supports it when +# compiling PROGRAM. For example, gl_WARN_ADD([-Wparentheses]). +# +# If VARIABLE is a variable name, AC_SUBST it. +AC_DEFUN([gl_WARN_ADD], +[AC_REQUIRE([gl_UNKNOWN_WARNINGS_ARE_ERRORS]) +gl_COMPILER_OPTION_IF([$1], + [gl_AS_VAR_APPEND(m4_if([$2], [], [[WARN_CFLAGS]], [[$2]]), [" $1"])], + [], + [$3]) +m4_ifval([$2], + [AS_LITERAL_IF([$2], [AC_SUBST([$2])])], + [AC_SUBST([WARN_CFLAGS])])dnl +]) + +# Local Variables: +# mode: autoconf +# End: diff --git a/spice-streaming-agent.pc.in b/spice-streaming-agent.pc.in new file mode 100644 index 0000000..74128d3 --- /dev/null +++ b/spice-streaming-agent.pc.in @@ -0,0 +1,12 @@ +prefix=@prefix@ +includedir=@includedir@ +exec_prefix=@exec_prefix@ +plugindir=@libdir@/@PACKAGE@/plugins + +Name: spice-streaming-agent +Description: SPICE streaming agent headers +Version: @VERSION@ +Requires: spice-protocol >= @SPICE_PROTOCOL_MIN_VER@ + +Libs: +Cflags: -I${includedir} diff --git a/spice-streaming-agent.spec.in b/spice-streaming-agent.spec.in new file mode 100644 index 0000000..3aff328 --- /dev/null +++ b/spice-streaming-agent.spec.in @@ -0,0 +1,64 @@ +Name: spice-streaming-agent +Version: @VERSION@ +Release: 1%{?dist} +Summary: SPICE streaming agent +Group: Applications/System +License: Proprietary +URL: https://www.redhat.com +Source0: %{name}-%{version}.tar.bz2 +BuildRequires: spice-protocol >= @SPICE_PROTOCOL_MIN_VER@ +BuildRequires: libX11-devel libXfixes-devel +BuildRequires: libjpeg-turbo-devel +# we need /usr/sbin/semanage program which is available on different +# packages depending on distribution +Requires(post): /usr/sbin/semanage +Requires(postun): /usr/sbin/semanage + +%description +An agent, running on a guest, sending video streams of the X display to a remote client (over Spice) + +%package devel +Requires: spice-protocol >= @SPICE_PROTOCOL_MIN_VER@ +Summary: SPICE streaming agent development files + +%description devel +This package contains necessary header files to build SPICE streaming +agent plugins. + +%prep +%setup -q + +%build +%configure +make %{?_smp_mflags} V=1 + +%install +make install DESTDIR=$RPM_BUILD_ROOT V=1 +if test -d "$RPM_BUILD_ROOT/%{_libdir}/%{name}/plugins"; then + find $RPM_BUILD_ROOT/%{_libdir}/%{name}/plugins -name '*.la' -delete +fi + +%post +semanage fcontext -a -t xserver_exec_t %{_bindir}/spice-streaming-agent 2>/dev/null || : +restorecon %{_bindir}/spice-streaming-agent || : + +%postun +if [ $1 -eq 0 ] ; then # final removal +semanage fcontext -d -t xserver_exec_t %{_bindir}/spice-streaming-agent 2>/dev/null || : +fi + + +%files +%doc COPYING ChangeLog README LICENSE +%{_bindir}/spice-streaming-agent +%{_sysconfdir}/xdg/autostart/spice-streaming.desktop +%{_datadir}/gdm/greeter/autostart/spice-streaming.desktop + +%files devel +%defattr(-,root,root,-) +%{_includedir} +%{_datadir}/pkgconfig + +%changelog +* Wed Aug 16 2017 Frediano Ziglio <fziglio@redhat.com> +- Initial package diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..f6e5531 --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,2 @@ +/spice-streaming-agent +/libstreaming-utils.a diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..8d5c5bd --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,59 @@ +# Makefile configuration for SPICE streaming agent +# +# \copyright +# Copyright 2016-2017 Red Hat Inc. All rights reserved. + +NULL = +SUBDIRS = . unittests + +AM_CPPFLAGS = \ + -DSPICE_STREAMING_AGENT_PROGRAM \ + -I$(top_srcdir)/include \ + -DPLUGINSDIR=\"$(pkglibdir)/plugins\" \ + $(SPICE_PROTOCOL_CFLAGS) \ + $(X11_CFLAGS) \ + $(XFIXES_CFLAGS) \ + $(NULL) + +AM_CFLAGS = \ + $(VISIBILITY_HIDDEN_CFLAGS) \ + $(WARN_CFLAGS) \ + $(NULL) + +AM_CXXFLAGS = \ + $(VISIBILITY_HIDDEN_CFLAGS) \ + $(WARN_CXXFLAGS) \ + $(NULL) + +bin_PROGRAMS = spice-streaming-agent +noinst_LIBRARIES = libstreaming-utils.a + +libstreaming_utils_a_SOURCES = \ + hexdump.c \ + hexdump.h \ + $(NULL) + +spice_streaming_agent_LDFLAGS = \ + $(RELRO_LDFLAGS) \ + $(NO_INDIRECT_LDFLAGS) \ + $(NULL) + +spice_streaming_agent_LDADD = \ + -ldl \ + -lpthread \ + libstreaming-utils.a \ + $(X11_LIBS) \ + $(XFIXES_LIBS) \ + $(JPEG_LIBS) \ + $(NULL) + +spice_streaming_agent_SOURCES = \ + spice-streaming-agent.cpp \ + static-plugin.cpp \ + static-plugin.hpp \ + concrete-agent.cpp \ + concrete-agent.hpp \ + mjpeg-fallback.cpp \ + jpeg.cpp \ + jpeg.hpp \ + $(NULL) diff --git a/src/concrete-agent.cpp b/src/concrete-agent.cpp new file mode 100644 index 0000000..192054a --- /dev/null +++ b/src/concrete-agent.cpp @@ -0,0 +1,122 @@ +/* Implementation of the agent + * + * \copyright + * Copyright 2017 Red Hat Inc. All rights reserved. + */ + +#include <config.h> +#include <algorithm> +#include <syslog.h> +#include <glob.h> +#include <dlfcn.h> + +#include "concrete-agent.hpp" +#include "static-plugin.hpp" + +using namespace std; +using namespace SpiceStreamingAgent; + +static inline unsigned MajorVersion(unsigned version) +{ + return version >> 8; +} + +static inline unsigned MinorVersion(unsigned version) +{ + return version & 0xffu; +} + +ConcreteAgent::ConcreteAgent() +{ + options.push_back(ConcreteConfigureOption(nullptr, nullptr)); +} + +bool ConcreteAgent::PluginVersionIsCompatible(unsigned pluginVersion) const +{ + unsigned version = Version(); + return MajorVersion(version) == MajorVersion(pluginVersion) && + MinorVersion(version) >= MinorVersion(pluginVersion); +} + +void ConcreteAgent::Register(Plugin& plugin) +{ + plugins.push_back(shared_ptr<Plugin>(&plugin)); +} + +const ConfigureOption* ConcreteAgent::Options() const +{ + static_assert(sizeof(ConcreteConfigureOption) == sizeof(ConfigureOption), + "ConcreteConfigureOption should be binary compatible with ConfigureOption"); + return static_cast<const ConfigureOption*>(&options[0]); +} + +void ConcreteAgent::AddOption(const char *name, const char *value) +{ + // insert before the last {nullptr, nullptr} value + options.insert(--options.end(), ConcreteConfigureOption(name, value)); +} + +void ConcreteAgent::LoadPlugins(const char *directory) +{ + StaticPlugin::InitAll(*this); + + string pattern = string(directory) + "/*.so"; + glob_t globbuf; + + int glob_result = glob(pattern.c_str(), 0, NULL, &globbuf); + if (glob_result == GLOB_NOMATCH) + return; + if (glob_result != 0) { + syslog(LOG_ERR, "glob FAILED with %d", glob_result); + return; + } + + for (size_t n = 0; n < globbuf.gl_pathc; ++n) { + LoadPlugin(globbuf.gl_pathv[n]); + } + globfree(&globbuf); +} + +void ConcreteAgent::LoadPlugin(const char *plugin_filename) +{ + void *dl = dlopen(plugin_filename, RTLD_LOCAL|RTLD_NOW); + if (!dl) { + syslog(LOG_ERR, "error loading plugin %s", plugin_filename); + return; + } + + try { + PluginInitFunc* init_func = + (PluginInitFunc *) dlsym(dl, "spice_streaming_agent_plugin_init"); + if (!init_func || !init_func(this)) { + dlclose(dl); + } + } + catch (std::runtime_error &err) { + syslog(LOG_ERR, "%s", err.what()); + dlclose(dl); + } +} + +FrameCapture *ConcreteAgent::GetBestFrameCapture() +{ + vector<pair<unsigned, shared_ptr<Plugin>>> sorted_plugins; + + // sort plugins base on ranking, reverse order + for (const auto& plugin: plugins) { + sorted_plugins.push_back(make_pair(plugin->Rank(), plugin)); + } + sort(sorted_plugins.rbegin(), sorted_plugins.rend()); + + // return first not null + for (const auto& plugin: sorted_plugins) { + if (plugin.first == DontUse) { + break; + } + FrameCapture *capture = plugin.second->CreateCapture(); + if (capture) { + return capture; + } + } + return nullptr; +} diff --git a/src/concrete-agent.hpp b/src/concrete-agent.hpp new file mode 100644 index 0000000..828368b --- /dev/null +++ b/src/concrete-agent.hpp @@ -0,0 +1,46 @@ +/* Agent implementation + * + * \copyright + * Copyright 2017 Red Hat Inc. All rights reserved. + */ +#ifndef SPICE_STREAMING_AGENT_CONCRETE_AGENT_HPP +#define SPICE_STREAMING_AGENT_CONCRETE_AGENT_HPP + +#include <vector> +#include <memory> +#include <spice-streaming-agent/plugin.hpp> + +namespace SpiceStreamingAgent { + +struct ConcreteConfigureOption: ConfigureOption +{ + ConcreteConfigureOption(const char *name, const char *value) + { + this->name = name; + this->value = value; + } +}; + +class ConcreteAgent final : public Agent +{ +public: + ConcreteAgent(); + unsigned Version() const override { + return PluginVersion; + } + void Register(Plugin& plugin) override; + const ConfigureOption* Options() const override; + void LoadPlugins(const char *directory); + // pointer must remain valid + void AddOption(const char *name, const char *value); + FrameCapture *GetBestFrameCapture(); + bool PluginVersionIsCompatible(unsigned pluginVersion) const override; +private: + void LoadPlugin(const char *plugin_filename); + std::vector<std::shared_ptr<Plugin>> plugins; + std::vector<ConcreteConfigureOption> options; +}; + +} + +#endif // SPICE_STREAMING_AGENT_CONCRETE_AGENT_HPP diff --git a/src/hexdump.c b/src/hexdump.c new file mode 100644 index 0000000..7654396 --- /dev/null +++ b/src/hexdump.c @@ -0,0 +1,33 @@ +/* Hex dump utility + * + * \copyright + * Copyright 2016-2017 Red Hat Inc. All rights reserved. + */ +#include <config.h> +#include <stdint.h> +#include <ctype.h> + +#include "hexdump.h" + +void hexdump(const void *ptr, size_t size, FILE *f_out) +{ + const uint8_t *buffer = (const uint8_t *) ptr; + unsigned long sum = 0; + + for (size_t n = 0; n < size;) { + int i; + enum { BYTES_PER_LINE = 16 }; + char s[BYTES_PER_LINE + 1], hexstring[BYTES_PER_LINE * 3 + 1]; + + fprintf(f_out, "%04X ", (unsigned) n); + for (i = 0; n < size && i < BYTES_PER_LINE; i++, n++) { + uint8_t c = buffer[n]; + sum += c; + sprintf(hexstring + i * 3, "%02X ", c); + s[i] = isprint(c) ? c : '.'; + } + s[i] = '\0'; + fprintf(f_out, "%*s\t%s\n", (int) (-3 * BYTES_PER_LINE), hexstring, s); + } + fprintf(f_out, "sum = %lu\n", sum); +} diff --git a/src/hexdump.h b/src/hexdump.h new file mode 100644 index 0000000..b8542a2 --- /dev/null +++ b/src/hexdump.h @@ -0,0 +1,21 @@ +/* Hex dump utility + * + * \copyright + * Copyright 2016-2017 Red Hat Inc. All rights reserved. + */ +#ifndef RH_STREAMING_AGENT_HEXDUMP_H_ +#define RH_STREAMING_AGENT_HEXDUMP_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include <stdio.h> + +void hexdump(const void *buffer, size_t size, FILE *f_out); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/jpeg.cpp b/src/jpeg.cpp new file mode 100644 index 0000000..ceee359 --- /dev/null +++ b/src/jpeg.cpp @@ -0,0 +1,91 @@ +/* Jpeg functions + * + * \copyright + * Copyright 2017 Red Hat Inc. All rights reserved. + */ +#include <config.h> +#include <stdio.h> +#include <stdint.h> +#include <ctype.h> +#include <jpeglib.h> +#include <setjmp.h> + +#include "jpeg.hpp" + +struct JpegBuffer: public jpeg_destination_mgr +{ + JpegBuffer(std::vector<uint8_t>& buffer); + ~JpegBuffer(); + + std::vector<uint8_t>& buffer; +}; + +static boolean buf_empty_output_buffer(j_compress_ptr cinfo) +{ + JpegBuffer *buf = (JpegBuffer *) cinfo->dest; + size_t size = buf->next_output_byte - &buf->buffer[0]; + buf->buffer.resize(buf->buffer.capacity() * 2); + buf->next_output_byte = &buf->buffer[0] + size; + buf->free_in_buffer = buf->buffer.size() - size; + return TRUE; +} + +static void dummy_destination(j_compress_ptr cinfo) +{ +} + +JpegBuffer::JpegBuffer(std::vector<uint8_t>& buffer): + buffer(buffer) +{ + if (buffer.capacity() < 32 * 1024) { + buffer.resize(32 * 1024); + } else { + buffer.resize(buffer.capacity()); + } + next_output_byte = &buffer[0]; + free_in_buffer = buffer.size(); + init_destination = dummy_destination; + empty_output_buffer = buf_empty_output_buffer; + term_destination = dummy_destination; +} + +JpegBuffer::~JpegBuffer() +{ + buffer.resize(next_output_byte - &buffer[0]); +} + +/* from https://github.com/LuaDist/libjpeg/blob/master/example.c */ +void write_JPEG_file(std::vector<uint8_t>& buffer, int quality, uint8_t *data, unsigned width, unsigned height) +{ + struct jpeg_compress_struct cinfo; + struct jpeg_error_mgr jerr; + JSAMPROW row_pointer[1]; + int row_stride; + + cinfo.err = jpeg_std_error(&jerr); + jpeg_create_compress(&cinfo); + + JpegBuffer buf(buffer); + cinfo.dest = &buf; + + cinfo.image_width = width; + cinfo.image_height = height; + cinfo.input_components = 4; + cinfo.in_color_space = JCS_EXT_BGRX; + jpeg_set_defaults(&cinfo); + jpeg_set_quality(&cinfo, quality, TRUE); + + jpeg_start_compress(&cinfo, TRUE); + + row_stride = width * 4; + + while (cinfo.next_scanline < cinfo.image_height) { + row_pointer[0] = &data[cinfo.next_scanline * row_stride]; + // TODO check error + (void) jpeg_write_scanlines(&cinfo, row_pointer, 1); + } + + jpeg_finish_compress(&cinfo); + + jpeg_destroy_compress(&cinfo); +} diff --git a/src/jpeg.hpp b/src/jpeg.hpp new file mode 100644 index 0000000..dd59405 --- /dev/null +++ b/src/jpeg.hpp @@ -0,0 +1,14 @@ +/* Jpeg functions + * + * \copyright + * Copyright 2017 Red Hat Inc. All rights reserved. + */ +#ifndef RH_STREAMING_AGENT_JPEG_HPP_ +#define RH_STREAMING_AGENT_JPEG_HPP_ + +#include <stdio.h> +#include <vector> + +void write_JPEG_file(std::vector<uint8_t>& buffer, int quality, uint8_t *data, unsigned width, unsigned height); + +#endif diff --git a/src/mjpeg-fallback.cpp b/src/mjpeg-fallback.cpp new file mode 100644 index 0000000..f41e68a --- /dev/null +++ b/src/mjpeg-fallback.cpp @@ -0,0 +1,223 @@ +/* Plugin implementation for Mjpeg + * + * \copyright + * Copyright 2017 Red Hat Inc. All rights reserved. + */ + +#include <config.h> +#include <cstring> +#include <exception> +#include <stdexcept> +#include <sstream> +#include <memory> +#include <syslog.h> +#include <X11/Xlib.h> + +#include <spice-streaming-agent/plugin.hpp> +#include <spice-streaming-agent/frame-capture.hpp> + +#include "static-plugin.hpp" +#include "jpeg.hpp" + +using namespace std; +using namespace SpiceStreamingAgent; + +#define ERROR(args) do { \ + std::ostringstream _s; \ + _s << args; \ + throw std::runtime_error(_s.str()); \ +} while(0) + +#define FBC_ERROR(function) \ + ERROR(function " failed(" << fbcStatus << "): " << pFn.nvFBCGetLastErrorStr(fbcHandle)) + +static inline uint64_t get_time() +{ + timespec now; + + clock_gettime(CLOCK_MONOTONIC, &now); + + return (uint64_t)now.tv_sec * 10000000000u + (uint64_t)now.tv_nsec; +} + +namespace { +struct MjpegSettings +{ + int fps; + int quality; +}; + +class MjpegFrameCapture final: public FrameCapture +{ +public: + MjpegFrameCapture(const MjpegSettings &settings); + ~MjpegFrameCapture(); + FrameInfo CaptureFrame() override; + void Reset() override; + SpiceVideoCodecType VideoCodecType() const { + return SPICE_VIDEO_CODEC_TYPE_MJPEG; + } +private: + MjpegSettings settings; + Display *dpy; + + vector<uint8_t> frame; + + // last frame sizes + uint32_t last_width = ~0u, last_height = ~0u; + // last time before capture + uint64_t last_time = 0; +}; + +class MjpegPlugin final: public Plugin +{ +public: + FrameCapture *CreateCapture() override; + unsigned Rank() override; + void ParseOptions(const ConfigureOption *options); + SpiceVideoCodecType VideoCodecType() const { + return SPICE_VIDEO_CODEC_TYPE_MJPEG; + } +private: + MjpegSettings settings = { 10, 80 }; +}; +} + +MjpegFrameCapture::MjpegFrameCapture(const MjpegSettings& settings): + settings(settings) +{ + dpy = XOpenDisplay(NULL); + if (!dpy) + ERROR("Unable to initialize X11"); +} + +MjpegFrameCapture::~MjpegFrameCapture() +{ + XCloseDisplay(dpy); +} + +void MjpegFrameCapture::Reset() +{ + frame.clear(); + last_width = last_height = ~0u; +} + +FrameInfo MjpegFrameCapture::CaptureFrame() +{ + FrameInfo info; + + // reduce speed considering FPS + auto now = get_time(); + if (last_time == 0) { + last_time = now; + } else { + const uint64_t delta = 1000000000u / settings.fps; + if (now >= last_time + delta) { + last_time = now; + } else { + uint64_t wait_time = last_time + delta - now; + // mathematically wait_time must be less than a second as + // delta would be 1 seconds only for FPS == 1 but in this + // side of the if now < last_time + delta + // but is also true that now > last_time so + // last_time + delta > now > last_time so + // 1s >= delta > now - last_time > 0 so + // wait_time = delta - (now - last_time) < delta <= 1s + timespec delay = { 0, (long) wait_time }; + nanosleep(&delay, NULL); + last_time += delta; + } + } + + int screen = XDefaultScreen(dpy); + + Window win = RootWindow(dpy, screen); + + XWindowAttributes win_info; + XGetWindowAttributes(dpy, win, &win_info); + + bool is_first = false; + if (win_info.width != last_width || win_info.height != last_height) { + last_width = win_info.width; + last_height = win_info.height; + is_first = true; + } + + info.size.width = win_info.width; + info.size.height = win_info.height; + + int format = ZPixmap; + // TODO handle errors + XImage *image = XGetImage(dpy, win, win_info.x, win_info.y, + win_info.width, win_info.height, AllPlanes, format); + + // TODO handle errors + // TODO multiple formats (only 32 bit) + write_JPEG_file(frame, settings.quality, (uint8_t*) image->data, + image->width, image->height); + + image->f.destroy_image(image); + + info.buffer = &frame[0]; + info.buffer_size = frame.size(); + + info.stream_start = is_first; + + return info; +} + +FrameCapture *MjpegPlugin::CreateCapture() +{ + return new MjpegFrameCapture(settings); +} + +unsigned MjpegPlugin::Rank() +{ + return FallBackMin; +} + +void MjpegPlugin::ParseOptions(const ConfigureOption *options) +{ +#define arg_error(...) syslog(LOG_ERR, ## __VA_ARGS__); + + for (; options->name; ++options) { + const char *name = options->name; + const char *value = options->value; + + if (strcmp(name, "framerate") == 0) { + int val = atoi(value); + if (val > 0) { + settings.fps = val; + } + else { + arg_error("wrong framerate arg %s\n", value); + } + } + if (strcmp(name, "mjpeg.quality") == 0) { + int val = atoi(value); + if (val > 0) { + settings.quality = val; + } + else { + arg_error("wrong mjpeg.quality arg %s\n", value); + } + } + } +} + +static bool +mjpeg_plugin_init(Agent* agent) +{ + if (agent->Version() != PluginVersion) + return false; + + std::unique_ptr<MjpegPlugin> plugin(new MjpegPlugin()); + + plugin->ParseOptions(agent->Options()); + + agent->Register(*plugin.release()); + + return true; +} + +static StaticPlugin mjpeg_plugin(mjpeg_plugin_init); diff --git a/src/spice-streaming-agent.cpp b/src/spice-streaming-agent.cpp new file mode 100644 index 0000000..ed7ddb9 --- /dev/null +++ b/src/spice-streaming-agent.cpp @@ -0,0 +1,529 @@ +/* An implementation of a SPICE streaming agent + * + * \copyright + * Copyright 2016-2017 Red Hat Inc. All rights reserved. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <stdint.h> +#include <string.h> +#include <getopt.h> +#include <unistd.h> +#include <errno.h> +#include <fcntl.h> +#include <sys/time.h> +#include <poll.h> +#include <syslog.h> +#include <signal.h> +#include <exception> +#include <stdexcept> +#include <memory> +#include <mutex> +#include <thread> +#include <vector> +#include <X11/Xlib.h> +#include <X11/extensions/Xfixes.h> + +#include <spice/stream-device.h> +#include <spice/enums.h> + +#include <spice-streaming-agent/frame-capture.hpp> +#include <spice-streaming-agent/plugin.hpp> + +#include "hexdump.h" +#include "concrete-agent.hpp" + +using namespace std; +using namespace SpiceStreamingAgent; + +static ConcreteAgent agent; + +typedef struct { + StreamDevHeader hdr; + StreamMsgFormat msg; +} SpiceStreamFormatMessage; + +typedef struct { + StreamDevHeader hdr; + StreamMsgData msg; +} SpiceStreamDataMessage; + +static int streaming_requested; +static bool quit; +static int streamfd = -1; +static bool stdin_ok; +static int log_binary = 0; +static std::mutex stream_mtx; + +static int have_something_to_read(int *pfd, int timeout) +{ + int nfds; + struct pollfd pollfds[2] = { + {streamfd, POLLIN, 0}, + {0, POLLIN, 0} + }; + *pfd = -1; + nfds = (stdin_ok ? 2 : 1); + if (poll(pollfds, nfds, timeout) < 0) { + syslog(LOG_ERR, "poll FAILED\n"); + return -1; + } + if (pollfds[0].revents == POLLIN) { + *pfd = streamfd; + } + if (pollfds[1].revents == POLLIN) { + *pfd = 0; + } + return *pfd != -1; +} + +static int read_command_from_stdin(void) +{ + char buffer[64], *p, *save = NULL; + + p = fgets(buffer, sizeof(buffer), stdin); + if (p == NULL) { + syslog(LOG_ERR, "Failed to read from stdin\n"); + return -1; + } + const char *cmd = strtok_r(buffer, " \t\n\r", &save); + if (!cmd) + return 1; + if (strcmp(cmd, "quit") == 0) { + quit = true; + } else if (strcmp(cmd, "start") == 0) { + streaming_requested = 1; + } else if (strcmp(cmd, "stop") == 0) { + streaming_requested = 0; + } else { + syslog(LOG_WARNING, "unknown command %s\n", cmd); + } + return 1; +} + +static int read_command_from_device(void) +{ + StreamDevHeader hdr; + uint8_t msg[64]; + int n; + + std::lock_guard<std::mutex> stream_guard(stream_mtx); + n = read(streamfd, &hdr, sizeof(hdr)); + if (n != sizeof(hdr)) { + syslog(LOG_WARNING, + "read command from device FAILED -- read %d expected %lu\n", + n, sizeof(hdr)); + return -1; + } + if (hdr.protocol_version != STREAM_DEVICE_PROTOCOL) { + syslog(LOG_WARNING, "BAD VERSION %d (expected is %d)\n", hdr.protocol_version, + STREAM_DEVICE_PROTOCOL); + return 0; // return -1; -- fail over this ? + } + if (hdr.type != STREAM_TYPE_START_STOP) { + syslog(LOG_WARNING, "UNKNOWN msg of type %d\n", hdr.type); + return 0; // return -1; + } + if (hdr.size >= sizeof(msg)) { + syslog(LOG_WARNING, + "msg size (%d) is too long (longer than %lu)\n", + hdr.size, sizeof(msg)); + return 0; // return -1; + } + n = read(streamfd, &msg, hdr.size); + if (n != hdr.size) { + syslog(LOG_WARNING, + "read command from device FAILED -- read %d expected %d\n", + n, hdr.size); + return -1; + } + streaming_requested = msg[0]; /* num_codecs */ + syslog(LOG_INFO, "GOT START_STOP message -- request to %s streaming\n", + streaming_requested ? "START" : "STOP"); + return 1; +} + +static int read_command(int blocking) +{ + int fd, n=1; + int timeout = blocking?-1:0; + while ( !quit ) { + if (!have_something_to_read(&fd, timeout)) { + if (!blocking) { + return 0; + } + sleep(1); + continue; + } + if (fd) { + n = read_command_from_device(); + } else { + n = read_command_from_stdin(); + } + break; + } + return n; +} + +static size_t +write_all(int fd, const void *buf, const size_t len) +{ + size_t written = 0; + while (written < len) { + int l = write(fd, (const char *) buf + written, len - written); + if (l < 0 && errno == EINTR) { + continue; + } + if (l < 0) { + syslog(LOG_ERR, "write failed - %m"); + return l; + } + written += l; + } + syslog(LOG_DEBUG, "write_all -- %u bytes written\n", (unsigned)written); + return written; +} + + +static int spice_stream_send_format(unsigned w, unsigned h, unsigned c) +{ + + SpiceStreamFormatMessage msg; + const size_t msgsize = sizeof(msg); + const size_t hdrsize = sizeof(msg.hdr); + memset(&msg, 0, msgsize); + msg.hdr.protocol_version = STREAM_DEVICE_PROTOCOL; + msg.hdr.type = STREAM_TYPE_FORMAT; + msg.hdr.size = msgsize - hdrsize; /* includes only the body? */ + msg.msg.width = w; + msg.msg.height = h; + msg.msg.codec = c; + syslog(LOG_DEBUG, "writing format\n"); + std::lock_guard<std::mutex> stream_guard(stream_mtx); + if (write_all(streamfd, &msg, msgsize) != msgsize) { + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +static int spice_stream_send_frame(const void *buf, const unsigned size) +{ + SpiceStreamDataMessage msg; + const size_t msgsize = sizeof(msg); + ssize_t n; + + memset(&msg, 0, msgsize); + msg.hdr.protocol_version = STREAM_DEVICE_PROTOCOL; + msg.hdr.type = STREAM_TYPE_DATA; + msg.hdr.size = size; /* includes only the body? */ + std::lock_guard<std::mutex> stream_guard(stream_mtx); + n = write_all(streamfd, &msg, msgsize); + syslog(LOG_DEBUG, + "wrote %ld bytes of header of data msg with frame of size %u bytes\n", + n, msg.hdr.size); + if (n != msgsize) { + syslog(LOG_WARNING, "write_all header: wrote %ld expected %lu\n", + n, msgsize); + return EXIT_FAILURE; + } + n = write_all(streamfd, buf, size); + syslog(LOG_DEBUG, "wrote data msg body of size %ld\n", n); + if (n != size) { + syslog(LOG_WARNING, "write_all header: wrote %ld expected %u\n", + n, size); + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +/* returns current time in micro-seconds */ +static uint64_t get_time(void) +{ + struct timeval now; + + gettimeofday(&now, NULL); + + return (uint64_t)now.tv_sec * 1000000 + (uint64_t)now.tv_usec; + +} + +static void handle_interrupt(int intr) +{ + syslog(LOG_INFO, "Got signal %d, exiting", intr); + quit = true; +} + +static void register_interrupts(void) +{ + struct sigaction sa; + + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = handle_interrupt; + if ((sigaction(SIGINT, &sa, NULL) != 0) && + (sigaction(SIGTERM, &sa, NULL) != 0)) { + syslog(LOG_WARNING, "failed to register signal handler %m"); + } +} + +static void usage(const char *progname) +{ + printf("usage: %s <options>\n", progname); + printf("options are:\n"); + printf("\t-p portname -- virtio-serial port to use\n"); + printf("\t-i accept commands from stdin\n"); + printf("\t-l file -- log frames to file\n"); + printf("\t--log-binary -- log binary frames (following -l)\n"); + printf("\t-d -- enable debug logs\n"); + printf("\t-c variable=value -- change settings\n"); + printf("\t\tprofile = [0, 1, 66, 77, 100, 244]\n"); + printf("\t\tratecontrol = constqp/vbr/cbr/2passq/2passf/2passi\n"); + printf("\t\tdwqp = 0-51\n"); + printf("\t\tframerate = 1-100 (check 10,20,30,40,50,60)\n"); + printf("\n"); + printf("\t-h or --help -- print this help message\n"); + + exit(1); +} + +static void send_cursor(const XFixesCursorImage &image) +{ + if (image.width >= 1024 || image.height >= 1024) + return; + + size_t cursor_size = + sizeof(StreamDevHeader) + sizeof(StreamMsgCursorSet) + + image.width * image.height * sizeof(uint32_t); + std::unique_ptr<uint8_t[]> msg(new uint8_t[cursor_size]); + + StreamDevHeader &dev_hdr(*reinterpret_cast<StreamDevHeader*>(msg.get())); + memset(&dev_hdr, 0, sizeof(dev_hdr)); + dev_hdr.protocol_version = STREAM_DEVICE_PROTOCOL; + dev_hdr.type = STREAM_TYPE_CURSOR_SET; + dev_hdr.size = cursor_size - sizeof(StreamDevHeader); + + StreamMsgCursorSet &cursor_msg(*reinterpret_cast<StreamMsgCursorSet *>(msg.get() + sizeof(StreamDevHeader))); + memset(&cursor_msg, 0, sizeof(cursor_msg)); + + cursor_msg.type = SPICE_CURSOR_TYPE_ALPHA; + cursor_msg.width = image.width; + cursor_msg.height = image.height; + cursor_msg.hot_spot_x = image.xhot; + cursor_msg.hot_spot_y = image.yhot; + + uint32_t *pixels = reinterpret_cast<uint32_t *>(cursor_msg.data); + for (unsigned i = 0; i < image.width * image.height; ++i) + pixels[i] = image.pixels[i]; + + std::lock_guard<std::mutex> stream_guard(stream_mtx); + write_all(streamfd, msg.get(), cursor_size); +} + +static void cursor_changes(Display *display, int event_base) +{ + unsigned long last_serial = 0; + + while (1) { + XEvent event; + XNextEvent(display, &event); + if (event.type != event_base + 1) + continue; + + XFixesCursorImage *cursor = XFixesGetCursorImage(display); + if (!cursor) + continue; + + if (cursor->cursor_serial == last_serial) + continue; + + last_serial = cursor->cursor_serial; + send_cursor(*cursor); + } +} + +static void +do_capture(const char *streamport, FILE *f_log) +{ + std::unique_ptr<FrameCapture> capture(agent.GetBestFrameCapture()); + if (!capture) + throw std::runtime_error("cannot find a suitable capture system"); + + streamfd = open(streamport, O_RDWR); + if (streamfd < 0) + // TODO was syslog(LOG_ERR, "Failed to open %s: %s\n", streamport, strerror(errno)); + throw std::runtime_error("failed to open streaming device"); + + unsigned int frame_count = 0; + while (! quit) { + while (!quit && !streaming_requested) { + if (read_command(1) < 0) { + syslog(LOG_ERR, "FAILED to read command\n"); + goto done; + } + } + + syslog(LOG_INFO, "streaming starts now\n"); + uint64_t time_last = 0; + + while (!quit && streaming_requested) { + if (++frame_count % 100 == 0) { + syslog(LOG_DEBUG, "SENT %d frames\n", frame_count); + } + uint64_t time_before = get_time(); + + FrameInfo frame = capture->CaptureFrame(); + + uint64_t time_after = get_time(); + syslog(LOG_DEBUG, + "got a frame -- size is %zu (%lu ms) (%lu ms from last frame)(%lu us)\n", + frame.buffer_size, (time_after - time_before)/1000, + (time_after - time_last)/1000, + (time_before - time_last)); + time_last = time_after; + + if (frame.stream_start) { + unsigned width, height; + unsigned char codec; + + width = frame.size.width; + height = frame.size.height; + codec = capture->VideoCodecType(); + + syslog(LOG_DEBUG, "wXh %uX%u codec=%u\n", width, height, codec); + + if (spice_stream_send_format(width, height, codec) == EXIT_FAILURE) + throw std::runtime_error("FAILED to send format message"); + } + if (f_log) { + if (log_binary) { + fwrite(frame.buffer, frame.buffer_size, 1, f_log); + } else { + fprintf(f_log, "%lu: Frame of %zu bytes:\n", get_time(), frame.buffer_size); + hexdump(frame.buffer, frame.buffer_size, f_log); + } + } + if (spice_stream_send_frame(frame.buffer, frame.buffer_size) == EXIT_FAILURE) { + syslog(LOG_ERR, "FAILED to send a frame\n"); + break; + } + //usleep(1); + if (read_command(0) < 0) { + syslog(LOG_ERR, "FAILED to read command\n"); + goto done; + } + if (!streaming_requested) { + capture->Reset(); + } + } + } + +done: + if (streamfd >= 0) { + close(streamfd); + streamfd = -1; + } +} + +#define arg_error(...) syslog(LOG_ERR, ## __VA_ARGS__); + +int main(int argc, char* argv[]) +{ + const char *streamport = "/dev/virtio-ports/com.redhat.stream.0"; + char opt; + const char *log_filename = NULL; + int logmask = LOG_UPTO(LOG_WARNING); + struct option long_options[] = { + { "log-binary", no_argument, &log_binary, 1}, + { "help", no_argument, NULL, 'h'}, + { 0, 0, 0, 0} + }; + + if (isatty(fileno(stderr)) && isatty(fileno(stdin))) { + stdin_ok = true; + } + + openlog("spice-streaming-agent", stdin_ok? (LOG_PERROR|LOG_PID) : LOG_PID, LOG_USER); + setlogmask(logmask); + + while ((opt = getopt_long(argc, argv, "hip:c:l:d", long_options, NULL)) != -1) { + switch (opt) { + case 0: + /* Handle long options if needed */ + break; + case 'i': + stdin_ok = true; + openlog("spice-streaming-agent", LOG_PERROR|LOG_PID, LOG_USER); + break; + case 'p': + streamport = optarg; + break; + case 'c': { + char *p = strchr(optarg, '='); + if (p == NULL) { + arg_error("wrong 'c' argument %s\n", optarg); + usage(argv[0]); + } + *p++ = '\0'; + agent.AddOption(optarg, p); + break; + } + case 'l': + log_filename = optarg; + break; + case 'd': + logmask = LOG_UPTO(LOG_DEBUG); + setlogmask(logmask); + break; + case 'h': + usage(argv[0]); + break; + } + } + + agent.LoadPlugins(PLUGINSDIR); + + register_interrupts(); + + FILE *f_log = NULL; + if (log_filename) { + f_log = fopen(log_filename, "wb"); + if (!f_log) { + syslog(LOG_ERR, "Failed to open log file '%s': %s\n", + log_filename, strerror(errno)); + return EXIT_FAILURE; + } + } + + Display *display = XOpenDisplay(NULL); + if (display == NULL) { + syslog(LOG_ERR, "failed to open display\n"); + return EXIT_FAILURE; + } + int event_base, error_base; + if (!XFixesQueryExtension(display, &event_base, &error_base)) { + syslog(LOG_ERR, "XFixesQueryExtension failed\n"); + return EXIT_FAILURE; + } + Window rootwindow = DefaultRootWindow(display); + XFixesSelectCursorInput(display, rootwindow, XFixesDisplayCursorNotifyMask); + + std::thread cursor_th(cursor_changes, display, event_base); + cursor_th.detach(); + + int ret = EXIT_SUCCESS; + try { + do_capture(streamport, f_log); + } + catch (std::runtime_error &err) { + syslog(LOG_ERR, "%s\n", err.what()); + ret = EXIT_FAILURE; + } + + if (f_log) { + fclose(f_log); + f_log = NULL; + } + closelog(); + return ret; +} + diff --git a/src/static-plugin.cpp b/src/static-plugin.cpp new file mode 100644 index 0000000..d5feb22 --- /dev/null +++ b/src/static-plugin.cpp @@ -0,0 +1,23 @@ +/* Utility to manage registration of plugins compiled statically + * + * \copyright + * Copyright 2017 Red Hat Inc. All rights reserved. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif + +#include <stdlib.h> +#include "static-plugin.hpp" + +using namespace SpiceStreamingAgent; + +const StaticPlugin *StaticPlugin::list = nullptr; + +void StaticPlugin::InitAll(Agent& agent) +{ + for (const StaticPlugin* plugin = list; plugin; plugin = plugin->next) { + plugin->init_func(&agent); + } +} diff --git a/src/static-plugin.hpp b/src/static-plugin.hpp new file mode 100644 index 0000000..5436b41 --- /dev/null +++ b/src/static-plugin.hpp @@ -0,0 +1,35 @@ +/* Utility to manage registration of plugins compiled statically + * + * \copyright + * Copyright 2017 Red Hat Inc. All rights reserved. + */ +#ifndef SPICE_STREAMING_AGENT_STATIC_PLUGIN_HPP +#define SPICE_STREAMING_AGENT_STATIC_PLUGIN_HPP + +#include <spice-streaming-agent/plugin.hpp> + +namespace SpiceStreamingAgent { + +class StaticPlugin final { +public: + StaticPlugin(PluginInitFunc init_func): + next(list), + init_func(init_func) + { + list = this; + } + static void InitAll(Agent& agent); +private: + // this should be instantiated statically + void *operator new(size_t s); + void *operator new[](size_t s); + + const StaticPlugin *const next; + const PluginInitFunc* const init_func; + + static const StaticPlugin *list; +}; + +} + +#endif // SPICE_STREAMING_AGENT_STATIC_PLUGIN_HPP diff --git a/src/unittests/.gitignore b/src/unittests/.gitignore new file mode 100644 index 0000000..36548a1 --- /dev/null +++ b/src/unittests/.gitignore @@ -0,0 +1 @@ +/test-hexdump diff --git a/src/unittests/Makefile.am b/src/unittests/Makefile.am new file mode 100644 index 0000000..0dc2328 --- /dev/null +++ b/src/unittests/Makefile.am @@ -0,0 +1,35 @@ +NULL = + +AM_CPPFLAGS = \ + -DRH_TOP_SRCDIR=\"$(abs_top_srcdir)\" \ + -I$(top_srcdir)/src \ + -I$(top_srcdir)/src/unittests \ + $(SPICE_PROTOCOL_CFLAGS) \ + $(NULL) + +AM_CFLAGS = \ + $(VISIBILITY_HIDDEN_CFLAGS) \ + $(WARN_CFLAGS) \ + $(NULL) + +check_PROGRAMS = \ + test-hexdump \ + $(NULL) + +TESTS = \ + hexdump.sh \ + $(NULL) + +noinst_PROGRAMS = \ + $(check_PROGRAMS) \ + $(NULL) + +test_hexdump_SOURCES = \ + test-hexdump.c \ + $(NULL) + +test_hexdump_LDADD = \ + ../libstreaming-utils.a \ + $(NULL) + +EXTRA_DIST = hexdump.sh hexdump*.in hexdump*.out diff --git a/src/unittests/hexdump.sh b/src/unittests/hexdump.sh new file mode 100755 index 0000000..602b501 --- /dev/null +++ b/src/unittests/hexdump.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -e + +# avoid weird language handling which could affect +# ascii part of the dump +export LANG=C + +for f in hexdump*.in; do + out=`echo $f | sed 's,\.in,.out,'` + rm -f $out.test + ./test-hexdump $out.test < $f + cmp $out.test $out + rm -f $out.test +done diff --git a/src/unittests/hexdump1.in b/src/unittests/hexdump1.in new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/src/unittests/hexdump1.in diff --git a/src/unittests/hexdump1.out b/src/unittests/hexdump1.out new file mode 100644 index 0000000..00335b3 --- /dev/null +++ b/src/unittests/hexdump1.out @@ -0,0 +1 @@ +sum = 0 diff --git a/src/unittests/hexdump2.in b/src/unittests/hexdump2.in new file mode 100644 index 0000000..887ae93 --- /dev/null +++ b/src/unittests/hexdump2.in @@ -0,0 +1 @@ +ciao diff --git a/src/unittests/hexdump2.out b/src/unittests/hexdump2.out new file mode 100644 index 0000000..5db593c --- /dev/null +++ b/src/unittests/hexdump2.out @@ -0,0 +1,2 @@ +0000 63 69 61 6F 0A ciao. +sum = 422 diff --git a/src/unittests/hexdump3.in b/src/unittests/hexdump3.in Binary files differnew file mode 100644 index 0000000..5f55f17 --- /dev/null +++ b/src/unittests/hexdump3.in diff --git a/src/unittests/hexdump3.out b/src/unittests/hexdump3.out new file mode 100644 index 0000000..27ba4ff --- /dev/null +++ b/src/unittests/hexdump3.out @@ -0,0 +1,9 @@ +0000 7F 45 4C 46 02 01 01 00 00 00 00 00 00 00 00 00 .ELF............ +0010 02 00 3E 00 01 00 00 00 20 09 40 00 00 00 00 00 ..>..... .@..... +0020 40 00 00 00 00 00 00 00 80 68 00 00 00 00 00 00 @........h...... +0030 00 00 00 00 40 00 38 00 09 00 40 00 25 00 22 00 ....@.8...@.%.". +0040 06 00 00 00 05 00 00 00 40 00 00 00 00 00 00 00 ........@....... +0050 40 00 40 00 00 00 00 00 40 00 40 00 00 00 00 00 @.@.....@.@..... +0060 F8 01 00 00 00 00 00 00 F8 01 00 00 00 00 00 00 ................ +0070 08 00 00 00 00 00 00 00 03 00 00 ........... +sum = 1916 diff --git a/src/unittests/test-hexdump.c b/src/unittests/test-hexdump.c new file mode 100644 index 0000000..275fbc0 --- /dev/null +++ b/src/unittests/test-hexdump.c @@ -0,0 +1,20 @@ +#undef NDEBUG +#include <stdio.h> +#include <assert.h> +#include <unistd.h> + +#include "hexdump.h" + +static char buffer[64 * 1024]; + +int main(int argc, const char **argv) +{ + assert(argc >= 2); + size_t s = fread(buffer, 1, sizeof(buffer), stdin); + assert(feof(stdin) && !ferror(stdin) && s <= sizeof(buffer)); + FILE *f = fopen(argv[1], "w"); + assert(f); + hexdump(buffer, s, f); + fclose(f); + return 0; +} |