summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorFrediano Ziglio <fziglio@redhat.com>2017-10-20 11:28:15 +0100
committerFrediano Ziglio <fziglio@redhat.com>2017-10-20 11:28:15 +0100
commita8fc6814b656221a6d640b4675e990c3e74a2403 (patch)
treefb8c7f19ceff0a0435539093fd8bb01d485fdeb6 /src
Initial public commit
Diffstat (limited to 'src')
-rw-r--r--src/.gitignore2
-rw-r--r--src/Makefile.am59
-rw-r--r--src/concrete-agent.cpp122
-rw-r--r--src/concrete-agent.hpp46
-rw-r--r--src/hexdump.c33
-rw-r--r--src/hexdump.h21
-rw-r--r--src/jpeg.cpp91
-rw-r--r--src/jpeg.hpp14
-rw-r--r--src/mjpeg-fallback.cpp223
-rw-r--r--src/spice-streaming-agent.cpp529
-rw-r--r--src/static-plugin.cpp23
-rw-r--r--src/static-plugin.hpp35
-rw-r--r--src/unittests/.gitignore1
-rw-r--r--src/unittests/Makefile.am35
-rwxr-xr-xsrc/unittests/hexdump.sh15
-rw-r--r--src/unittests/hexdump1.in0
-rw-r--r--src/unittests/hexdump1.out1
-rw-r--r--src/unittests/hexdump2.in1
-rw-r--r--src/unittests/hexdump2.out2
-rw-r--r--src/unittests/hexdump3.inbin0 -> 123 bytes
-rw-r--r--src/unittests/hexdump3.out9
-rw-r--r--src/unittests/test-hexdump.c20
22 files changed, 1282 insertions, 0 deletions
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
new file mode 100644
index 0000000..5f55f17
--- /dev/null
+++ b/src/unittests/hexdump3.in
Binary files differ
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;
+}