[Spice-devel] [PATCH spice-streaming-agent v2 2/2] Rework option handling

Frediano Ziglio fziglio at redhat.com
Thu Nov 16 15:17:28 UTC 2017


> 
> From: Christophe de Dinechin <dinechin at redhat.com>
> 
> The overall objective is to pave the way for features that will likely
> become important, notably the ability to dynamically adjust quality of
> service to respond to network conditions or client load issues.
> 
> Consequently, this patch cleans up the option-related aspects of the
> plugin ABI and API as follows:
> 
> 1. It adds standard settings that are shared across plugins, e.g. framerate.
>    This will enable future QoS adjustments to be applied by the agent
>    in a consistent way, irrespective of the actual capture plugin.
> 
> 2. It makes the interface for option handling dynamic, so that the agent
>    can in the future update settings without having to do expensive
>    operations such as searching for pre-existing options.
> 
> 3. It exposes a consistent way to deal witih settings values, so that

typo

>    error messages and acceptable values are consistent across plugins.
>    The current interfaces only deals with integer values, possibly with
>    a range of acceptable values, and possibly with a K/M suffix, e.g.
>    to accept max_bitrate=100k.
> 

Was thinking about this unit thing. Maybe we can use an enum making possible
to add additional units in the future?

> 4. It delegates usage information to the plugins, so that each plugin
>    can document its specific options in a consistent way.
> 
> 5. It cleans up some confusing or surprising aspects of the interace,
>    notably null-terminated option vectors and the ownership of
>    dynamically loaded plugins.
> 
> Signed-off-by: Christophe de Dinechin <dinechin at redhat.com>

Maybe you should split the patch instead of having multiple points
inside the comment? I would personally starts with some commit that
can be merged without waiting, like adding a Name() method.

> ---
>  include/spice-streaming-agent/plugin.hpp |  94 +++++++++++++++++---
>  m4/spice-compile-warnings.m4             |   2 +
>  src/concrete-agent.cpp                   | 143
>  ++++++++++++++++++++++++++++---
>  src/concrete-agent.hpp                   |  34 ++++----
>  src/mjpeg-fallback.cpp                   |  89 +++++++++----------
>  src/spice-streaming-agent.cpp            |  23 +++--
>  6 files changed, 284 insertions(+), 101 deletions(-)
> 
> diff --git a/include/spice-streaming-agent/plugin.hpp
> b/include/spice-streaming-agent/plugin.hpp
> index 607fabf..6202d2c 100644
> --- a/include/spice-streaming-agent/plugin.hpp
> +++ b/include/spice-streaming-agent/plugin.hpp
> @@ -8,6 +8,10 @@
>  #define SPICE_STREAMING_AGENT_PLUGIN_HPP
>  
>  #include <spice/enums.h>
> +#include <stdexcept>
> +#include <vector>
> +#include <climits>
> +#include <cstdio>
>  
>  /*!
>   * \file
> @@ -20,6 +24,7 @@
>  namespace SpiceStreamingAgent {
>  
>  class FrameCapture;
> +class Agent;
>  
>  /*!
>   * Plugin version, only using few bits, schema is 0xMMmm
> @@ -42,31 +47,75 @@ 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;
>  };
>  
>  /*!
> + * Error applying an option
> + */
> +class OptionError : public std::runtime_error
> +{
> +public:
> +    OptionError(const std::string &what): std::runtime_error(what) {}
> +};
> +
> +/*!
> + * Usage information
> + */
> +struct UsageInfo
> +{
> +    const char *name;
> +    const char *range;
> +    const char *description;
> +};
> +
> +/*!
>   * Interface a plugin should implement and register to the Agent.
>   *
>   * A plugin module can register multiple Plugin interfaces to handle
>   * multiple codecs. In this case each Plugin will report data for a
>   * specific codec.
> + *
> + * A plugin will typically extend the Settings struct to add its own
> + * settings.
>   */
>  class Plugin
>  {
>  public:
> +    Plugin(Agent *agent): agent(agent) {}
> +
>      /*!
>       * 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
> +     * Usage is returned as a NULL-terminated array of UsageInfo
> +     */
> +    virtual const UsageInfo *Usage() = 0;
>  
>      /*!
>       * Request an object for getting frames.
> @@ -89,6 +138,22 @@ 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 process standard options itself.
> +     */
> +    virtual bool ApplyOption(const Option &o) throw(OptionError) = 0;
> +
> +protected:
> +    Agent *agent;
>  };
>  
>  /*!
> @@ -113,20 +178,23 @@ 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.
> +     * The agent then has shared ownership of the plugin, meaning that
> +     * the plugin will be deleted when the agent is deleted unless another
> +     * shared_ptr instance still holds it at that time.
>       */
> -    virtual void Register(Plugin& plugin)=0;
> +    virtual void Register(Plugin *plugin)=0;
>  
>      /*!
> -     * Get options array.
> -     * Array is terminated with {nullptr, nullptr}.
> -     * Never nullptr.
> -     * \todo passing options to entry point instead?
> +     * Helper to get integer values from command-line options
>       */
> -    virtual const ConfigureOption* Options() const=0;
> +    virtual int IntOption(const Option &opt,
> +                          int min = INT_MIN, int max = INT_MAX,
> +                          bool sizeSuffixOK = false) const
> +        throw(OptionError) = 0;
>  };
>  
>  typedef bool PluginInitFunc(SpiceStreamingAgent::Agent* agent);
> 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 51b4504..5c93e42 100644
> --- a/src/concrete-agent.cpp
> +++ b/src/concrete-agent.cpp
> @@ -9,6 +9,10 @@
>  #include <syslog.h>
>  #include <glob.h>
>  #include <dlfcn.h>
> +#include <mutex>
> +#include <climits>
> +#include <sstream>
> +#include <string>
>  
>  #include "concrete-agent.hpp"
>  #include "static-plugin.hpp"
> @@ -28,7 +32,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
> @@ -41,22 +49,83 @@ bool ConcreteAgent::PluginVersionIsCompatible(unsigned
> pluginVersion) const
>          MinorVersion(version) >= MinorVersion(pluginVersion);
>  }
>  
> -void ConcreteAgent::Register(Plugin& plugin)
> +int ConcreteAgent::IntOption(const Option &opt,
> +                             int min, int max, bool sizeSuffixOK) const
> +    throw(OptionError)
>  {
> -    plugins.push_back(shared_ptr<Plugin>(&plugin));
> +    std::string name = opt.name;
> +    std::string value = opt.value;
> +    std::istringstream input(value);
> +    std::ostringstream err;
> +
> +    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";
> +            throw OptionError(err.str());
> +        }
> +    }
> +
> +    if (input.fail()) {
> +        err << "Value " << value << " for " << name
> +            << " is not a number\n";
> +        throw OptionError(err.str());
> +    }
> +    if (!input.eof()) {
> +        err << "Value " << value << " for " << name
> +            << " does not end like an integer\n";
> +        throw OptionError(err.str());
> +    }
> +    if (result < min || result > max) {
> +        err << "The value " << value << " for " << name
> +            << " is out of range (must be in " << min << ".." << max <<
> ")\n";
> +        throw OptionError(err.str());
> +    }
> +
> +    return result;
>  }
>  
> -const ConfigureOption* ConcreteAgent::Options() const
> +bool ConcreteAgent::StandardOption(Settings &settings, const Option &option)
> +    throw (OptionError)
>  {
> -    static_assert(sizeof(ConcreteConfigureOption) ==
> sizeof(ConfigureOption),
> -                  "ConcreteConfigureOption should be binary compatible with
> ConfigureOption");
> -    return static_cast<const ConfigureOption*>(&options[0]);
> +    string name = option.name;
> +    if (name == "framerate" || name == "fps") {
> +        settings.framerate = IntOption(option, 1, 240);
> +        return true;
> +    }
> +    if (name == "quality") {
> +        settings.quality = IntOption(option, 0, 100);
> +        return true;
> +    }
> +    if (name == "avg_bitrate" || name == "bitrate") {
> +        settings.avg_bitrate = IntOption(option, 10000, 32000000, true);
> +        return true;
> +    }
> +    if (name == "max_bitrate") {
> +        settings.max_bitrate = IntOption(option, 10000, 32000000, true);
> +        return true;
> +    }
> +
> +    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)
> @@ -84,7 +153,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;
>      }
>  
> @@ -106,7 +176,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());
> @@ -116,6 +186,7 @@ FrameCapture *ConcreteAgent::GetBestFrameCapture()
>          if (plugin.first == DontUse) {
>              break;
>          }
> +        ApplyOptions(plugin.second.get());
>          FrameCapture *capture = plugin.second->CreateCapture();
>          if (capture) {
>              return capture;
> @@ -123,3 +194,53 @@ FrameCapture *ConcreteAgent::GetBestFrameCapture()
>      }
>      return nullptr;
>  }
> +
> +void ConcreteAgent::PluginsUsage()
> +{
> +    static const UsageInfo standardSettingsInfo[] =
> +    {
> +        { "framerate",  "[1-240]",      "Number of frames per second" },
> +        { "quality",    "[1-100]",      "Normalized quality, 0=low, 100 =
> high" },
> +        { "avg_bitrate","[1-10000000]", "Average bits per second for stream"
> },
> +        { "max_bitrate","[1-10000000]", "Maximum bits per second for stream"
> }
> +    };
> +    printf("\nsettings shared by all plugins:\n");
> +    for (const UsageInfo &info : standardSettingsInfo) {
> +        printf("%16s = %-16s : %s\n",
> +               info.name, info.range, info.description);
> +    }
> +
> +    for (auto &plugin: plugins) {
> +        const UsageInfo *info = plugin->Usage();
> +        if (info) {
> +            printf("\nsettings for %s:\n", plugin->Name());
> +            for (info = info; info->name; info++) {
> +                printf("%16s = %-16s : %s\n",
> +                       info->name, info->range, info->description);
> +            }
> +        }
> +    }
> +}
> +
> +void ConcreteAgent::ApplyOptions(Plugin *plugin)
> +{
> +    for (const auto &opt : options) {
> +        try
> +        {
> +            bool known = plugin->ApplyOption(opt);
> +            if (!known) {
> +                Settings &settings = plugin->PluginSettings();
> +                known = StandardOption(settings, opt);
> +            }
> +            if (!known) {
> +                syslog(LOG_ERR, "Option %s not recognized by plugin %s",
> +                       opt.name, plugin->Name());
> +            }
> +        }
> +        catch (OptionError &err)
> +        {
> +            syslog(LOG_ERR, "Plugin %s did not accept setting %s=%s: %s",
> +                   plugin->Name(), opt.name, opt.value, err.what());
> +        }
> +    }
> +}
> diff --git a/src/concrete-agent.hpp b/src/concrete-agent.hpp
> index 828368b..6852b51 100644
> --- a/src/concrete-agent.hpp
> +++ b/src/concrete-agent.hpp
> @@ -12,33 +12,37 @@
>  
>  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;
> +    int IntOption(const Option &opt,
> +                  int min = INT_MIN, int max = INT_MAX,
> +                  bool sizeSuffixOK = false) const
> +        throw(OptionError) override;
> +
> +public:
> +    // Interface used by the main agent program
>      void AddOption(const char *name, const char *value);
> +    void LoadPlugins(const char *directory);
>      FrameCapture *GetBestFrameCapture();
> -    bool PluginVersionIsCompatible(unsigned pluginVersion) const override;
> +    bool StandardOption(Settings &settings, const Option &option)
> +        throw (OptionError);
> +    void PluginsUsage();
> +
>  private:
>      void LoadPlugin(const char *plugin_filename);
> +    void ApplyOptions(Plugin *plugin);
>      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..6bd8bf5 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,21 @@ private:
>  class MjpegPlugin final: public Plugin
>  {
>  public:
> +    MjpegPlugin(Agent *agent): Plugin(agent) {}
> +    virtual const char *Name() override;
> +    virtual const UsageInfo *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) throw(OptionError) 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 +108,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 +145,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 +163,16 @@ FrameInfo MjpegFrameCapture::CaptureFrame()
>      return info;
>  }
>  
> +const char *MjpegPlugin::Name()
> +{
> +    return "MJPEG";
> +}
> +
> +const UsageInfo *MjpegPlugin::Usage()
> +{
> +    return NULL;
> +}
> +
>  FrameCapture *MjpegPlugin::CreateCapture()
>  {
>      return new MjpegFrameCapture(settings);
> @@ -176,33 +183,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) throw(OptionError)
> +{
> +    // This plugin only relies solely on base options
> +    return false;
>  }
>  
>  static bool
> @@ -211,12 +205,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(agent));
>      return true;
>  }
>  
> diff --git a/src/spice-streaming-agent.cpp b/src/spice-streaming-agent.cpp
> index d5984bc..71a36e1 100644
> --- a/src/spice-streaming-agent.cpp
> +++ b/src/spice-streaming-agent.cpp
> @@ -269,18 +269,16 @@ static void register_interrupts(void)
>  
>  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\tframerate = 1-100 (check 10,20,30,40,50,60)\n");
> -    printf("\n");
> -    printf("\t-h or --help     -- print this help message\n");
> -
> +    printf("usage: %s <options>\n"
> +           "options are:\n"
> +           "\t-p portname  -- virtio-serial port to use\n"
> +           "\t-i accept commands from stdin\n"
> +           "\t-l file -- log frames to file\n"
> +           "\t--log-binary -- log binary frames (following -l)\n"
> +           "\t-d -- enable debug logs\n"
> +           "\t-c variable=value -- change settings (see below)\n"
> +           "\t-h or --help     -- print this help message\n", progname);
> +    agent.PluginsUsage();
>      exit(1);
>  }
>  
> @@ -474,6 +472,7 @@ int main(int argc, char* argv[])
>              setlogmask(logmask);
>              break;
>  	case 'h':
> +            agent.LoadPlugins(PLUGINSDIR);
>  	    usage(argv[0]);
>  	    break;
>  	}

Frediano


More information about the Spice-devel mailing list