buffet: Add command definition support for GCD command manager Rudimentary framework of classes to represent GCD command manager and list of registered device command definitions/schemas. BUG=chromium:374861 TEST=USE=buffet P2_TEST_FILTER="buffet::*" FEATURES=test emerge-link platform Change-Id: I3cd3d776879e8bd506aecd20df5cd89c65247d35 Reviewed-on: https://chromium-review.googlesource.com/208464 Tested-by: Alex Vakulenko <avakulenko@chromium.org> Reviewed-by: Christopher Wiley <wiley@chromium.org> Reviewed-by: Alex Vakulenko <avakulenko@chromium.org> Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/buffet.gyp b/buffet/buffet.gyp index 0174e5b..2197989 100644 --- a/buffet/buffet.gyp +++ b/buffet/buffet.gyp
@@ -25,6 +25,9 @@ 'sources': [ 'any.cc', 'async_event_sequencer.cc', + 'commands/command_definition.cc', + 'commands/command_dictionary.cc', + 'commands/command_manager.cc', 'commands/object_schema.cc', 'commands/prop_constraints.cc', 'commands/prop_types.cc', @@ -82,6 +85,9 @@ 'any_internal_impl_unittest.cc', 'async_event_sequencer_unittest.cc', 'buffet_testrunner.cc', + 'commands/command_definition_unittest.cc', + 'commands/command_dictionary_unittest.cc', + 'commands/command_manager_unittest.cc', 'commands/object_schema_unittest.cc', 'commands/schema_utils_unittest.cc', 'commands/unittest_utils.cc',
diff --git a/buffet/commands/command_definition.cc b/buffet/commands/command_definition.cc new file mode 100644 index 0000000..f24b22d --- /dev/null +++ b/buffet/commands/command_definition.cc
@@ -0,0 +1,15 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/commands/command_definition.h" + +namespace buffet { + +CommandDefinition::CommandDefinition( + const std::string& category, + const std::shared_ptr<const ObjectSchema>& parameters) + : category_(category), parameters_(parameters) { +} + +} // namespace buffet
diff --git a/buffet/commands/command_definition.h b/buffet/commands/command_definition.h new file mode 100644 index 0000000..36ba554 --- /dev/null +++ b/buffet/commands/command_definition.h
@@ -0,0 +1,42 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_COMMANDS_COMMAND_DEFINITION_H_ +#define BUFFET_COMMANDS_COMMAND_DEFINITION_H_ + +#include <memory> +#include <string> + +#include <base/basictypes.h> + +#include "buffet/commands/object_schema.h" + +namespace buffet { + +// A simple GCD command definition. This class contains the command category +// and a full object schema describing the command parameter types and +// constraints. See comments for CommandDefinitions::LoadCommands for the +// detailed description of what command categories are and what they are used +// for. +class CommandDefinition { + public: + CommandDefinition(const std::string& category, + const std::shared_ptr<const ObjectSchema>& parameters); + + // Gets the category this command belongs to. + const std::string& GetCategory() const { return category_; } + // Gets the object schema for command parameters. + const std::shared_ptr<const ObjectSchema>& GetParameters() const { + return parameters_; + } + + private: + std::string category_; // Cmd category. Could be "powerd" for "base.reboot". + std::shared_ptr<const ObjectSchema> parameters_; // Command parameter def. + DISALLOW_COPY_AND_ASSIGN(CommandDefinition); +}; + +} // namespace buffet + +#endif // BUFFET_COMMANDS_COMMAND_DEFINITION_H_
diff --git a/buffet/commands/command_definition_unittest.cc b/buffet/commands/command_definition_unittest.cc new file mode 100644 index 0000000..76212af --- /dev/null +++ b/buffet/commands/command_definition_unittest.cc
@@ -0,0 +1,14 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <gtest/gtest.h> + +#include "buffet/commands/command_definition.h" + +TEST(CommandDefinition, Test) { + auto params = std::make_shared<buffet::ObjectSchema>(); + buffet::CommandDefinition def("powerd", params); + EXPECT_EQ("powerd", def.GetCategory()); + EXPECT_EQ(params, def.GetParameters()); +}
diff --git a/buffet/commands/command_dictionary.cc b/buffet/commands/command_dictionary.cc new file mode 100644 index 0000000..69f9e2b --- /dev/null +++ b/buffet/commands/command_dictionary.cc
@@ -0,0 +1,125 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/commands/command_dictionary.h" + +#include <base/values.h> + +#include "buffet/commands/command_definition.h" +#include "buffet/commands/schema_constants.h" +#include "buffet/string_utils.h" + +namespace buffet { + +std::vector<std::string> CommandDictionary::GetCommandNamesByCategory( + const std::string& category) const { + std::vector<std::string> names; + for (const auto& pair : definitions_) { + if (pair.second->GetCategory() == category) + names.push_back(pair.first); + } + return names; +} + +bool CommandDictionary::LoadCommands(const base::DictionaryValue& json, + const std::string& category, + ErrorPtr* error) { + std::map<std::string, std::shared_ptr<const CommandDefinition>> new_defs; + + // |json| contains a list of nested objects with the following structure: + // {"<pkg_name>": {"<cmd_name>": {"parameters": {object_schema}}, ...}, ...} + // Iterate over packages + base::DictionaryValue::Iterator package_iter(json); + while (!package_iter.IsAtEnd()) { + std::string package = package_iter.key(); + const base::DictionaryValue* package_value = nullptr; + if (!package_iter.value().GetAsDictionary(&package_value)) { + Error::AddToPrintf(error, commands::errors::kDomain, + commands::errors::kTypeMismatch, + "Expecting an object for package '%s'", + package.c_str()); + return false; + } + // Iterate over command definitions within the current package. + base::DictionaryValue::Iterator command_iter(*package_value); + while (!command_iter.IsAtEnd()) { + std::string command = command_iter.key(); + const base::DictionaryValue* command_value = nullptr; + if (!command_iter.value().GetAsDictionary(&command_value)) { + Error::AddToPrintf(error, commands::errors::kDomain, + commands::errors::kTypeMismatch, + "Expecting an object for command '%s'", + command.c_str()); + return false; + } + // Construct the compound command name as "pkg_name.cmd_name". + std::string command_name = string_utils::Join('.', package, command); + // Get the "parameters" definition of the command and read it into + // an object schema. + const base::DictionaryValue* command_schema_def = nullptr; + if (!command_value->GetDictionaryWithoutPathExpansion( + commands::attributes::kCommand_Parameters, &command_schema_def)) { + Error::AddToPrintf(error, commands::errors::kDomain, + commands::errors::kPropertyMissing, + "Command definition '%s' is missing property '%s'", + command_name.c_str(), + commands::attributes::kCommand_Parameters); + return false; + } + auto command_schema = std::make_shared<ObjectSchema>(); + if (!command_schema->FromJson(command_schema_def, nullptr, error)) { + Error::AddToPrintf(error, commands::errors::kDomain, + commands::errors::kInvalidObjectSchema, + "Invalid definition for command '%s'", + command_name.c_str()); + return false; + } + auto command_def = std::make_shared<CommandDefinition>(category, + command_schema); + new_defs.insert(std::make_pair(command_name, command_def)); + + command_iter.Advance(); + } + package_iter.Advance(); + } + + // Verify that newly loaded command definitions do not override existing + // definitions in another category. This is unlikely, but we don't want to let + // one vendor daemon to define the same commands already handled by another + // daemon on the same device. + for (const auto& pair : new_defs) { + auto iter = definitions_.find(pair.first); + if (iter != definitions_.end()) { + Error::AddToPrintf(error, commands::errors::kDomain, + commands::errors::kDuplicateCommandDef, + "Definition for command '%s' overrides an earlier " + "definition in category '%s'", + pair.first.c_str(), + iter->second->GetCategory().c_str()); + return false; + } + } + + // Now that we successfully loaded all the command definitions, + // remove previous definitions of commands from the same category. + std::vector<std::string> names = GetCommandNamesByCategory(category); + for (const std::string& name : names) + definitions_.erase(name); + + // Insert new definitions into the global map. + definitions_.insert(new_defs.begin(), new_defs.end()); + return true; +} + +const CommandDefinition* CommandDictionary::FindCommand( + const std::string& command_name) const { + auto pair = definitions_.find(command_name); + return (pair != definitions_.end()) ? pair->second.get() : nullptr; +} + +void CommandDictionary::Clear() { + definitions_.clear(); +} + +} // namespace buffet
diff --git a/buffet/commands/command_dictionary.h b/buffet/commands/command_dictionary.h new file mode 100644 index 0000000..11295c9 --- /dev/null +++ b/buffet/commands/command_dictionary.h
@@ -0,0 +1,72 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_COMMANDS_COMMAND_DICTIONARY_H_ +#define BUFFET_COMMANDS_COMMAND_DICTIONARY_H_ + +#include <map> +#include <memory> +#include <string> +#include <vector> + +#include <base/basictypes.h> + +#include "buffet/error.h" + +namespace base { +class Value; +class DictionaryValue; +} // namespace base + +namespace buffet { + +class CommandDefinition; + +// CommandDictionary is a wrapper around a map of command name and the +// corresponding command definition schema. The command name (the key in +// the map) is a compound name in a form of "package_name.command_name", +// where "package_name" is a name of command package such as "base", "printers", +// and others. So the full command name could be "base.reboot", for example. +class CommandDictionary { + public: + CommandDictionary() = default; + + // Gets the list of names of commands that belong to the given category. + std::vector<std::string> GetCommandNamesByCategory( + const std::string& category) const; + + // Loads command definitions from a JSON object. This is done at the daemon + // startup and whenever a device daemon decides to update its command list. + // |json| is a JSON dictionary that describes the complete commands contained + // in a particular |category|. Usually, |categories| are 1:1 with daemons on + // a device. For instance, the power manager daemon might provide a category + // "power_man" that provides the "base.reboot" and "base.shutdown" commands. + // However, nothing prohibits a daemon providing commands in two categories. + // When LoadCommands is called, all previous definitions of commands from the + // same category are removed, effectively replacing all the commands in the + // given category. + // Returns false on failure and |error| provides additional error information + // when provided. + bool LoadCommands(const base::DictionaryValue& json, + const std::string& category, ErrorPtr* error); + // Returns the number of command definitions in the dictionary. + size_t GetSize() const { return definitions_.size(); } + // Checks if the dictionary has no command definitions. + bool IsEmpty() const { return definitions_.empty(); } + // Remove all the command definitions from the dictionary. + void Clear(); + // Finds a definition for the given command. + const CommandDefinition* FindCommand(const std::string& command_name) const; + + private: + using CommandMap = std::map<std::string, + std::shared_ptr<const CommandDefinition>>; + + CommandMap definitions_; // List of all available command definitions. + DISALLOW_COPY_AND_ASSIGN(CommandDictionary); +}; + +} // namespace buffet + +#endif // BUFFET_COMMANDS_COMMAND_DICTIONARY_H_
diff --git a/buffet/commands/command_dictionary_unittest.cc b/buffet/commands/command_dictionary_unittest.cc new file mode 100644 index 0000000..f345019 --- /dev/null +++ b/buffet/commands/command_dictionary_unittest.cc
@@ -0,0 +1,97 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <gtest/gtest.h> + +#include "buffet/commands/command_dictionary.h" +#include "buffet/commands/unittest_utils.h" + +using buffet::unittests::CreateDictionaryValue; + +TEST(CommandDictionary, Empty) { + buffet::CommandDictionary dict; + EXPECT_TRUE(dict.IsEmpty()); + EXPECT_EQ(nullptr, dict.FindCommand("robot.jump")); + EXPECT_TRUE(dict.GetCommandNamesByCategory("robotd").empty()); +} + +TEST(CommandDictionary, LoadCommands) { + auto json = CreateDictionaryValue(R"({ + 'robot': { + 'jump': { + 'parameters': { + 'height': 'integer', + '_jumpType': ['_withAirFlip', '_withSpin', '_withKick'] + } + } + } + })"); + buffet::CommandDictionary dict; + EXPECT_TRUE(dict.LoadCommands(*json, "robotd", nullptr)); + EXPECT_EQ(1, dict.GetSize()); + EXPECT_NE(nullptr, dict.FindCommand("robot.jump")); + json = CreateDictionaryValue(R"({ + 'base': { + 'reboot': { + 'parameters': {'delay': 'integer'} + }, + 'shutdown': { + 'parameters': {} + } + } + })"); + EXPECT_TRUE(dict.LoadCommands(*json, "powerd", nullptr)); + EXPECT_EQ(3, dict.GetSize()); + EXPECT_NE(nullptr, dict.FindCommand("robot.jump")); + EXPECT_NE(nullptr, dict.FindCommand("base.reboot")); + EXPECT_NE(nullptr, dict.FindCommand("base.shutdown")); + EXPECT_EQ(nullptr, dict.FindCommand("foo.bar")); + std::vector<std::string> expected_commands{"base.reboot", "base.shutdown"}; + EXPECT_EQ(expected_commands, dict.GetCommandNamesByCategory("powerd")); +} + +TEST(CommandDictionary, LoadCommands_Failures) { + buffet::CommandDictionary dict; + buffet::ErrorPtr error; + + // Command definition missing 'parameters' property. + auto json = CreateDictionaryValue("{'robot':{'jump':{}}}"); + EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error)); + EXPECT_EQ("parameter_missing", error->GetCode()); + EXPECT_EQ("Command definition 'robot.jump' is missing property 'parameters'", + error->GetMessage()); + error.reset(); + + // Command definition is not an object. + json = CreateDictionaryValue("{'robot':{'jump':0}}"); + EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error)); + EXPECT_EQ("type_mismatch", error->GetCode()); + EXPECT_EQ("Expecting an object for command 'jump'", error->GetMessage()); + error.reset(); + + // Package definition is not an object. + json = CreateDictionaryValue("{'robot':'blah'}"); + EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error)); + EXPECT_EQ("type_mismatch", error->GetCode()); + EXPECT_EQ("Expecting an object for package 'robot'", error->GetMessage()); + error.reset(); + + // Invalid command definition is not an object. + json = CreateDictionaryValue("{'robot':{'jump':{'parameters':{'flip':0}}}}"); + EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error)); + EXPECT_EQ("invalid_object_schema", error->GetCode()); + EXPECT_EQ("Invalid definition for command 'robot.jump'", error->GetMessage()); + EXPECT_NE(nullptr, error->GetInnerError()); // Must have additional info. + error.reset(); + + // Redefine commands in different category. + json = CreateDictionaryValue("{'robot':{'jump':{'parameters':{}}}}"); + dict.Clear(); + dict.LoadCommands(*json, "category1", &error); + EXPECT_FALSE(dict.LoadCommands(*json, "category2", &error)); + EXPECT_EQ("duplicate_command_definition", error->GetCode()); + EXPECT_EQ("Definition for command 'robot.jump' overrides an earlier " + "definition in category 'category1'", error->GetMessage()); + error.reset(); +}
diff --git a/buffet/commands/command_manager.cc b/buffet/commands/command_manager.cc new file mode 100644 index 0000000..fc04b2d --- /dev/null +++ b/buffet/commands/command_manager.cc
@@ -0,0 +1,13 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "buffet/commands/command_manager.h" + +namespace buffet { + +const CommandDictionary& CommandManager::GetCommandDictionary() const { + return dictionary_; +} + +} // namespace buffet
diff --git a/buffet/commands/command_manager.h b/buffet/commands/command_manager.h new file mode 100644 index 0000000..1700712 --- /dev/null +++ b/buffet/commands/command_manager.h
@@ -0,0 +1,31 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef BUFFET_COMMANDS_COMMAND_MANAGER_H_ +#define BUFFET_COMMANDS_COMMAND_MANAGER_H_ + +#include <base/basictypes.h> + +#include "buffet/commands/command_dictionary.h" + +namespace buffet { + +// CommandManager class that will have a list of all the device command +// schemas as well as the live command queue of pending command instances +// dispatched to the device. +class CommandManager { + public: + CommandManager() = default; + + // Get the command definitions for the device. + const CommandDictionary& GetCommandDictionary() const; + + private: + CommandDictionary dictionary_; // Command definitions/schemas. + DISALLOW_COPY_AND_ASSIGN(CommandManager); +}; + +} // namespace buffet + +#endif // BUFFET_COMMANDS_COMMAND_MANAGER_H_
diff --git a/buffet/commands/command_manager_unittest.cc b/buffet/commands/command_manager_unittest.cc new file mode 100644 index 0000000..1a0bfec --- /dev/null +++ b/buffet/commands/command_manager_unittest.cc
@@ -0,0 +1,12 @@ +// Copyright 2014 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <gtest/gtest.h> + +#include "buffet/commands/command_manager.h" + +TEST(CommandManager, Empty) { + buffet::CommandManager manager; + EXPECT_TRUE(manager.GetCommandDictionary().IsEmpty()); +}
diff --git a/buffet/commands/schema_constants.cc b/buffet/commands/schema_constants.cc index d8ec689..6504c5d 100644 --- a/buffet/commands/schema_constants.cc +++ b/buffet/commands/schema_constants.cc
@@ -20,6 +20,7 @@ const char kPropertyMissing[] = "parameter_missing"; const char kUnknownProperty[] = "unexpected_parameter"; const char kInvalidObjectSchema[] = "invalid_object_schema"; +const char kDuplicateCommandDef[] = "duplicate_command_definition"; } // namespace errors namespace attributes { @@ -37,6 +38,8 @@ const char kOneOf_MetaSchema[] = "schema"; const char kObject_Properties[] = "properties"; + +const char kCommand_Parameters[] = "parameters"; } // namespace attributes } // namespace commands
diff --git a/buffet/commands/schema_constants.h b/buffet/commands/schema_constants.h index 087f53b..d709e59 100644 --- a/buffet/commands/schema_constants.h +++ b/buffet/commands/schema_constants.h
@@ -23,6 +23,7 @@ extern const char kPropertyMissing[]; extern const char kUnknownProperty[]; extern const char kInvalidObjectSchema[]; +extern const char kDuplicateCommandDef[]; } // namespace errors namespace attributes { @@ -41,6 +42,8 @@ extern const char kOneOf_MetaSchema[]; extern const char kObject_Properties[]; + +extern const char kCommand_Parameters[]; } // namespace attributes } // namespace commands
diff --git a/buffet/commands/unittest_utils.h b/buffet/commands/unittest_utils.h index e14e4d1..665a2d1 100644 --- a/buffet/commands/unittest_utils.h +++ b/buffet/commands/unittest_utils.h
@@ -16,7 +16,7 @@ // Helper method to create base::Value from a string as a smart pointer. // For ease of definition in C++ code, double-quotes in the source definition // are replaced with apostrophes. - std::unique_ptr<base::Value> CreateValue(const char* json); +std::unique_ptr<base::Value> CreateValue(const char* json); // Helper method to create a JSON dictionary object from a string. std::unique_ptr<base::DictionaryValue> CreateDictionaryValue(const char* json);