Re: [PATCH spice-streaming-agent 2/3] Rework option handling

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 




On 15 Nov 2017, at 13:55, Frediano Ziglio <fziglio@xxxxxxxxxx> wrote:


From: Christophe de Dinechin <dinechin@xxxxxxxxxx>

Several functional objectives:

1. Be able to share some common options, e.g. fps
2. Prepare for the possibility to update options on the fly

Also get rid of the null-terminated C++ vector

Signed-off-by: Christophe de Dinechin <dinechin@xxxxxxxxxx>
---
include/spice-streaming-agent/plugin.hpp | 120
+++++++++++++++++++++++++++----
m4/spice-compile-warnings.m4             |   2 +
src/concrete-agent.cpp                   |  77 ++++++++++++++++----
src/concrete-agent.hpp                   |  30 ++++----
src/mjpeg-fallback.cpp                   |  90 +++++++++++------------
5 files changed, 227 insertions(+), 92 deletions(-)

diff --git a/include/spice-streaming-agent/plugin.hpp
b/include/spice-streaming-agent/plugin.hpp
index 607fabf..41ad11f 100644
--- a/include/spice-streaming-agent/plugin.hpp
+++ b/include/spice-streaming-agent/plugin.hpp
@@ -8,6 +8,11 @@
#define SPICE_STREAMING_AGENT_PLUGIN_HPP

#include <spice/enums.h>
+#include <climits>
+#include <sstream>
+#include <string>
+#include <vector>
+

/*!
 * \file
@@ -20,6 +25,7 @@
namespace SpiceStreamingAgent {

class FrameCapture;
+class Agent;

/*!
 * Plugin version, only using few bits, schema is 0xMMmm
@@ -42,13 +48,23 @@ enum Ranks : unsigned {
};

/*!
- * 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".
+ * Settings that are common to all plugins
 */
-struct ConfigureOption
+struct Settings
{
+    unsigned    framerate       =      30; // Frames per second (1-240)
+    unsigned    quality         =      80; // Normalized in 0-100 (100=high)
+    unsigned    avg_bitrate     = 3000000; // Target average bitrate in bps
+    unsigned    max_bitrate     = 8000000; // Target maximum bitrate
+};
+
+/*!
+ * Command line option
+ */
+struct Option
+{
+    Option(const char *name, const char *value)
+        : name(name), value(value) {}
    const char *name;
    const char *value;
};
@@ -59,6 +75,9 @@ struct ConfigureOption
 * A plugin module can register multiple Plugin interfaces to handle
 * multiple codecs. In this case each Plugin will report data for a
 * specific codec.
+ *
+ * A plugin will typically extend the Settings struct to add its own
+ * settings.

Not clear how and why.

Yes, the other patch series for the plugin did not go through, the originating
email address was rejected by the list server. Working on it :-)


 */
class Plugin
{
@@ -66,7 +85,17 @@ public:
    /*!
     * Allows to free the plugin when not needed
     */
-    virtual ~Plugin() {};
+    virtual ~Plugin() {}
+
+    /*!
+     * Return the name of the plugin, e.g. for command-line usage
information
+     */
+    virtual const char *Name() = 0;
+
+    /*!
+     * Return usage information for the plug-in, e.g. command-line options
+     */
+    virtual const char *Usage() = 0;

    /*!
     * Request an object for getting frames.
@@ -89,6 +118,19 @@ public:
     * Get video codec used to encode last frame
     */
    virtual SpiceVideoCodecType VideoCodecType() const=0;
+
+    /*!
+     * Return the concrete Settings for this plugin
+     */
+    virtual Settings &  PluginSettings() = 0;
+
+    /*!
+     * Apply a given option to the plugin settings.
+     * \return true if the option is valid, false if it is not recognized
+     * If there is an error applying the settings, set \arg error.
+     * If this returns false, agent will try StandardOption.

The agent knows its options so does not need to call ApplyOption to
know if is a standard option or not.

I decided to do it the other way round, so that a plugin can do something
special with a standard option, e.g. special limits on bitrate.
Let me clarify one point. I agree the options interface lacks some feature. Changing
them dynamically will be necessary.
On the other hand the interface you are proposing is adding a lot of constrains which
can be hard to bend in the future. As said having a fixed Settings structure is hard to
extend. More that this having a method returning a reference to it force the Plugin
to have a field containing it making it a strong ABI. A workaround would be to
have a base class for it and allocate from the Agent. But I don't thinkthis will get so far.
It looks that your interface is forcing both Agent and Plugin to handle the Settings
structure. This looks messy to me, the responsibilities should not be shared that
easily. I don't really understand why an option should be really shared.
The agent should propose a value, the Plugin can accept and use, ignore or use
a more sensible data. For instance if I say to a Plugin "I want 9600 bps" the Plugin
should fallback to some more sensible value. I don't see the reason for the Agent to
know the internal value (beside debugging purposes).
The ApplyOption API push each option from the Agent to the Plugin. It looks
like a property settings in some way. But what happens if some values are correlated?
Let suppose I have (not in this case) a width and height options and the object needs
to resize some internal memory based on width and height. Should the resize happens
changing width or height? Or should the object just cache the values, mark as changed
and resize the memory lazily? Would not be better to pass all options I want to change?

Why not using exceptions instead of a value passed as reference?

I was concerned about exceptions requiring RTTI information to be
shared between the agent and the plugin, which I thought might lead
to linking issues at dlopen time (much like if you leave the Plugin base
class have non-pure virtual methods).

Experimentally, that does not appear to be an issue, so I will do
that in the next iteration.
On Unix is a good idea to have the exceptions classes not with restricted
visibility. On Windows C++ is in the system ABI and this is supported by
design.
(BTW, you might think that having implemented the first “efficient”
exception handling mechanism, I would naturally be a big proponent
and user of exceptions. But it’s actually the opposite. I know how
expensive this stuff is, and how much it still hinders optimizations
for practical reasons. That probably tends to color my preferences
towards not using exception unless it’s really in some very slow
or infrequent execution path.)



+     */
+    virtual bool ApplyOption(const Option &o, std::string &error) = 0;
};

/*!
@@ -113,24 +155,74 @@ public:
     * Check if a given plugin version is compatible with this agent
     * \return true is compatible
     */
-    virtual bool PluginVersionIsCompatible(unsigned pluginVersion) const=0;
+    virtual bool PluginVersionIsCompatible(unsigned pluginVersion) const =
0;

    /*!
-     * Register a plugin in the system.
+     * Register a plugin in the system, which becomes the owner and should
delete it.
     */
-    virtual void Register(Plugin& plugin)=0;
+    virtual void Register(Plugin *plugin)=0;


We should add a style document file.

The reason I made that change is not style, it is documenting that the target
takes ownership of it and deletes it (through the shared_ptr vector).

If you use a reference, it encourages the caller to pass a stack-based plugin,
expecting that the agent would make a copy or something, e.g. the following
would seem to be a natural usage:

MyPlugin plugin(…);
agent->Register(plugin);

not the rather clumsy:
shared_ptr<MyPlugin> plugin = new MyPlugin(…);
agent->Register(*plugin);

So I added a comment stating that the plugin has shared ownership of the pointer
(instead of stating that the pointer will be deleted). Now the natural usage is
the most natural way to dynamically allocate the plugin:

agent->Register(new MyPlugin(…));



    /*!
-     * Get options array.
-     * Array is terminated with {nullptr, nullptr}.
-     * Never nullptr.
-     * \todo passing options to entry point instead?
+     * Apply the standard options to the given settings (base Settings
class)
     */
-    virtual const ConfigureOption* Options() const=0;
+    virtual bool StandardOption(Settings &, const Option &, std::string
&error) = 0;
+

Is not clear this. Is the plugin telling the agent it changed some settings?
Or telling the agent to update the setting structure?
What the result mean?

It’s a historical leftover. Initially, the plugin was telling the agent, but then
I decided it was too dangerous to leave that to the agent, and forgot to remove
the function. I will remove it.


};

typedef bool PluginInitFunc(SpiceStreamingAgent::Agent* agent);

+/*!
+ * Helper to get integer values from command-line options
+ */
+
+static inline int IntOption(const Option &opt, std::string &error,
+                            int min=INT_MIN, int max=INT_MAX, bool
sizeSuffixOK = false)

Why not Agent::ParseIntOption or something similar?

I hesitated between the two. That was forcing the plugin to call back into
the agent.

One thing I hesitated to do was to add a field to the Plugin:

Agent *agent;

I don’t like globals much. If that’s OK with you, I’d switch to having
I don't see any global... and no reason to add a field to Plugin either.
the option parsing exposed as an agent feature. That also reduces
the number of standard headers needed for the interface.
Definitely better.


+{
+    std::string name = opt.name;
+    std::string value = opt.value;
+    std::ostringstream err;
+    std::istringstream input(value);
+    int result = 0;
+    input >> result;
+
+    if (!input.fail() && !input.eof() && sizeSuffixOK) {
+        std::string suffix;
+        input >> suffix;
+        bool ok = false;
+        if (!input.fail() && suffix.length() == 1) {
+            switch (suffix[0]) {
+            case 'b': case 'B': ok = true;                      break;
+            case 'k': case 'K': ok = true; result *= 1000;      break;
+            case 'm': case 'M': ok = true; result *= 1000000;   break;
+            default:                                            break;
+            }
+        }
+        if (!ok) {
+            err << "Unknown number suffix " << suffix
+                << " for " << name << "\n";
+            error = err.str();
+        }
+    }
+
+    if (input.fail()) {
+        err << "Value " << value << " for " << name << " is not a number\n";
+        error = err.str();
+    }
+    if (!input.eof()) {
+        err << "Value " << value << " for " << name << " does not end like
an integer\n";
+        error = err.str();
+    }
+    if (result < min || result > max) {
+        err << "The value " << value << " for " << name
+            << " is out of range (must be in " << min << ".." << max <<
")\n";
+        error = err.str();        // May actually combine an earlier error
+        result = (min + max) / 2; // Give a value acceptable by caller
+    }
+
+    return result;
+}
+
+
}

#ifndef SPICE_STREAMING_AGENT_PROGRAM
diff --git a/m4/spice-compile-warnings.m4 b/m4/spice-compile-warnings.m4
index 66d7179..9e8bb6e 100644
--- a/m4/spice-compile-warnings.m4
+++ b/m4/spice-compile-warnings.m4
@@ -86,6 +86,8 @@ AC_DEFUN([SPICE_COMPILE_WARNINGS],[
    dontwarn="$dontwarn -Wpointer-sign"
    dontwarn="$dontwarn -Wpointer-to-int-cast"
    dontwarn="$dontwarn -Wstrict-prototypes"
+    dontwarn="$dontwarn -Wsuggest-final-types"
+    dontwarn="$dontwarn -Wsuggest-final-methods"

    # We want to enable these, but need to sort out the
    # decl mess with  gtk/generated_*.c
diff --git a/src/concrete-agent.cpp b/src/concrete-agent.cpp
index 192054a..59f11b2 100644
--- a/src/concrete-agent.cpp
+++ b/src/concrete-agent.cpp
@@ -9,6 +9,7 @@
#include <syslog.h>
#include <glob.h>
#include <dlfcn.h>
+#include <mutex>

#include "concrete-agent.hpp"
#include "static-plugin.hpp"
@@ -28,7 +29,11 @@ static inline unsigned MinorVersion(unsigned version)

ConcreteAgent::ConcreteAgent()
{
-    options.push_back(ConcreteConfigureOption(nullptr, nullptr));
+}
+
+void ConcreteAgent::Register(Plugin *plugin)
+{
+    plugins.push_back(shared_ptr<Plugin>(plugin));
}

bool ConcreteAgent::PluginVersionIsCompatible(unsigned pluginVersion) const
@@ -38,22 +43,34 @@ bool ConcreteAgent::PluginVersionIsCompatible(unsigned
pluginVersion) const
        MinorVersion(version) >= MinorVersion(pluginVersion);
}

-void ConcreteAgent::Register(Plugin& plugin)
+bool ConcreteAgent::StandardOption(Settings &settings,
+                                   const Option &option,
+                                   string &error)
{
-    plugins.push_back(shared_ptr<Plugin>(&plugin));
-}
+    string name = option.name;
+    if (name == "framerate" || name == "fps") {
+        settings.framerate = IntOption(option, error, 1, 240);
+        return true;
+    }
+    if (name == "quality") {
+        settings.quality = IntOption(option, error, 0, 100);
+        return true;
+    }
+    if (name == "avg_bitrate" || name == "bitrate") {
+        settings.avg_bitrate = IntOption(option, error, 10000, 32000000,
true);
+        return true;
+    }
+    if (name == "max_bitrate") {
+        settings.max_bitrate = IntOption(option, error, 10000, 32000000,
true);
+        return true;
+    }

-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]);
+    return false;               // Unrecognized option
}

void ConcreteAgent::AddOption(const char *name, const char *value)
{
-    // insert before the last {nullptr, nullptr} value
-    options.insert(--options.end(), ConcreteConfigureOption(name, value));
+    options.push_back(Option(name, value));
}

void ConcreteAgent::LoadPlugins(const char *directory)
@@ -81,7 +98,8 @@ 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);
+        syslog(LOG_ERR, "error loading plugin %s: %s",
+               plugin_filename, dlerror());
        return;
    }

@@ -103,7 +121,7 @@ FrameCapture *ConcreteAgent::GetBestFrameCapture()
    vector<pair<unsigned, shared_ptr<Plugin>>> sorted_plugins;

    // sort plugins base on ranking, reverse order
-    for (const auto& plugin: plugins) {
+    for (auto& plugin: plugins) {
        sorted_plugins.push_back(make_pair(plugin->Rank(), plugin));
    }
    sort(sorted_plugins.rbegin(), sorted_plugins.rend());
@@ -113,6 +131,7 @@ FrameCapture *ConcreteAgent::GetBestFrameCapture()
        if (plugin.first == DontUse) {
            break;
        }
+        ApplyOptions(plugin.second.get());
        FrameCapture *capture = plugin.second->CreateCapture();
        if (capture) {
            return capture;
@@ -120,3 +139,35 @@ FrameCapture *ConcreteAgent::GetBestFrameCapture()
    }
    return nullptr;
}
+
+void ConcreteAgent::ApplyOptions(Plugin *plugin)
+{
+    bool usage = false;
+    for (const auto &opt : options) {
+        string error;
+        bool known = plugin->ApplyOption(opt, error);
+        if (!known) {
+            Settings &settings = plugin->PluginSettings();
+            known = StandardOption(settings, opt, error);

Can't the agent check for standard before?

As explained above, the idea is that the agent might have special
ways to deal with standard options, e.g. restrictions on the range.
I the agent restrict the range should craft a better option value and pass to the
Plugin. If the Plugin has a better value should just use.

How the plugin knows the agent changed the settings?

Does it care? Right now, I am not testing nor reporting if the setting was modified.
So, the Agent can change internal settings of the Plugin and the Plugin does not need
to know?

And why StandardOption is public if called only by the agent?

As written above, leftover from code evolution, I’ll remove it.


+        }
+        if (!known) {
+            syslog(LOG_ERR, "Option %s not recognized by plugin %s",
+                   opt.name, plugin->Name());
+            usage = true;
+        }
+        if (error != "") {
+            syslog(LOG_ERR, "Plugin %s did not accept setting %s=%s: %s",
+                   plugin->Name(), opt.name, opt.value, error.c_str());
+            usage = true;
+        }
+    }
+    if (usage)
+    {
+        static std:: once_flag once;
+        std::call_once(once, [plugin]()
+        {
+            const char *usage = plugin->Usage();
+            syslog(LOG_ERR, "Usage: for plugin %s: %s", plugin->Name(),
usage);
+        });

This way the error/warning depends on the order of the plugins.

Hmm. You are right. But if a given plugin receives some incorrect input
repeatedly, I don’t want to flood the syslog. We already have the error
message that tells what option failed.
But is neither an error nor a warning.
If pluginA supports optionA and pluginB supports optionB to avoid these message
I cannot use neither optionA nor optionB that is you basically cannot have options
for the plugins but only standard options.
Unless you want to have specific plugin options, in this case you can say that if
optionX for pluginX is not supported by pluginX you give an error/warning
while optionX for pluginX is ignored by pluginY.

Thinking about it, I think showing usage information here is wrong,
I’ll remove it. The order of the messages is the order of the plugins
which is not consistent, but that’s another debate.


+    }
+}
diff --git a/src/concrete-agent.hpp b/src/concrete-agent.hpp
index 828368b..b3d4e06 100644
--- a/src/concrete-agent.hpp
+++ b/src/concrete-agent.hpp
@@ -12,33 +12,33 @@

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();
+
+public:
+    // Implementation of the Agent class virtual methods
    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 Register(Plugin *plugin) override;
+    bool PluginVersionIsCompatible(unsigned pluginVersion) const override;
+    bool StandardOption(Settings &settings,
+                        const Option &option,
+                        std::string &error) override;
+
+public:
+    // Interface used by the main agent program
    void AddOption(const char *name, const char *value);
+    void LoadPlugins(const char *directory);
+    void ApplyOptions(Plugin *plugin);

Why this is public?
Is the program dealing directly with plugins? Should not.

You are right.
Mumble... maybe we should have some documentation on responsibility.


    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;
+    std::vector<Option> options;
};

}
diff --git a/src/mjpeg-fallback.cpp b/src/mjpeg-fallback.cpp
index f41e68a..37df01a 100644
--- a/src/mjpeg-fallback.cpp
+++ b/src/mjpeg-fallback.cpp
@@ -41,16 +41,11 @@ static inline uint64_t get_time()
}

namespace {
-struct MjpegSettings
-{
-    int fps;
-    int quality;
-};

class MjpegFrameCapture final: public FrameCapture
{
public:
-    MjpegFrameCapture(const MjpegSettings &settings);
+    MjpegFrameCapture(Settings &settings);
    ~MjpegFrameCapture();
    FrameInfo CaptureFrame() override;
    void Reset() override;
@@ -58,8 +53,8 @@ public:
        return SPICE_VIDEO_CODEC_TYPE_MJPEG;
    }
private:
-    MjpegSettings settings;
    Display *dpy;
+    Settings &settings;

    vector<uint8_t> frame;

@@ -72,19 +67,20 @@ private:
class MjpegPlugin final: public Plugin
{
public:
+    virtual const char *Name() override;
+    virtual const char *Usage() override;
    FrameCapture *CreateCapture() override;
    unsigned Rank() override;
-    void ParseOptions(const ConfigureOption *options);
-    SpiceVideoCodecType VideoCodecType() const {
-        return SPICE_VIDEO_CODEC_TYPE_MJPEG;
-    }
+    SpiceVideoCodecType VideoCodecType() const override;
+    Settings &PluginSettings() override;
+    bool ApplyOption(const Option &o, string &error) override;
private:
-    MjpegSettings settings = { 10, 80 };
+    Settings settings;
};
}

-MjpegFrameCapture::MjpegFrameCapture(const MjpegSettings& settings):
-    settings(settings)
+MjpegFrameCapture::MjpegFrameCapture(Settings &settings)
+    : settings(settings)
{
    dpy = XOpenDisplay(NULL);
    if (!dpy)
@@ -111,7 +107,7 @@ FrameInfo MjpegFrameCapture::CaptureFrame()
    if (last_time == 0) {
        last_time = now;
    } else {
-        const uint64_t delta = 1000000000u / settings.fps;
+        const uint64_t delta = 1000000000u / settings.framerate;
        if (now >= last_time + delta) {
            last_time = now;
        } else {
@@ -148,13 +144,13 @@ FrameInfo MjpegFrameCapture::CaptureFrame()

    int format = ZPixmap;
    // TODO handle errors
-    XImage *image = XGetImage(dpy, win, win_info.x, win_info.y,
+    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);
+    write_JPEG_file(frame, settings.quality,
+                    (uint8_t*) image->data, image->width, image->height);

    image->f.destroy_image(image);

@@ -166,6 +162,18 @@ FrameInfo MjpegFrameCapture::CaptureFrame()
    return info;
}

+const char *MjpegPlugin::Name()
+{
+    return "MJPEG";
+}
+
+const char *MjpegPlugin::Usage()
+{
+    return
+        "quality        = [0-100]  Set picture quality\n"
+        "framerate      = [1-240]  Set number of frames per second\n";
+}
+
FrameCapture *MjpegPlugin::CreateCapture()
{
    return new MjpegFrameCapture(settings);
@@ -176,33 +184,20 @@ unsigned MjpegPlugin::Rank()
    return FallBackMin;
}

-void MjpegPlugin::ParseOptions(const ConfigureOption *options)
+SpiceVideoCodecType MjpegPlugin::VideoCodecType() const
{
-#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);
-            }
-        }
-    }
+    return SPICE_VIDEO_CODEC_TYPE_MJPEG;
+}
+
+Settings &MjpegPlugin::PluginSettings()
+{
+    return settings;
+}
+
+bool MjpegPlugin::ApplyOption(const Option &o, string &error)
+{
+    // This plugin only relies on base options
+    return false;
}

static bool
@@ -211,12 +206,7 @@ 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());
-
+    agent->Register(new MjpegPlugin);
    return true;
}
Frediano

_______________________________________________
Spice-devel mailing list
Spice-devel@xxxxxxxxxxxxxxxxxxxxx
https://lists.freedesktop.org/mailman/listinfo/spice-devel

[Index of Archives]     [Linux ARM Kernel]     [Linux ARM]     [Linux Omap]     [Fedora ARM]     [IETF Annouce]     [Security]     [Bugtraq]     [Linux]     [Linux OMAP]     [Linux MIPS]     [ECOS]     [Asterisk Internet PBX]     [Linux API]     [Monitors]