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 /src |
Initial public commit
Diffstat (limited to 'src')
-rw-r--r-- | src/.gitignore | 2 | ||||
-rw-r--r-- | src/Makefile.am | 59 | ||||
-rw-r--r-- | src/concrete-agent.cpp | 122 | ||||
-rw-r--r-- | src/concrete-agent.hpp | 46 | ||||
-rw-r--r-- | src/hexdump.c | 33 | ||||
-rw-r--r-- | src/hexdump.h | 21 | ||||
-rw-r--r-- | src/jpeg.cpp | 91 | ||||
-rw-r--r-- | src/jpeg.hpp | 14 | ||||
-rw-r--r-- | src/mjpeg-fallback.cpp | 223 | ||||
-rw-r--r-- | src/spice-streaming-agent.cpp | 529 | ||||
-rw-r--r-- | src/static-plugin.cpp | 23 | ||||
-rw-r--r-- | src/static-plugin.hpp | 35 | ||||
-rw-r--r-- | src/unittests/.gitignore | 1 | ||||
-rw-r--r-- | src/unittests/Makefile.am | 35 | ||||
-rwxr-xr-x | src/unittests/hexdump.sh | 15 | ||||
-rw-r--r-- | src/unittests/hexdump1.in | 0 | ||||
-rw-r--r-- | src/unittests/hexdump1.out | 1 | ||||
-rw-r--r-- | src/unittests/hexdump2.in | 1 | ||||
-rw-r--r-- | src/unittests/hexdump2.out | 2 | ||||
-rw-r--r-- | src/unittests/hexdump3.in | bin | 0 -> 123 bytes | |||
-rw-r--r-- | src/unittests/hexdump3.out | 9 | ||||
-rw-r--r-- | src/unittests/test-hexdump.c | 20 |
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 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; +} |