buffet: Add DBusCommandDispacher and hook it up to CommandQueue

Added DBusCommandDispacher class that maintains DBusCommandProxy object
for each instance of CommandInstance class added to the CommandQueue.

As soon as a command instance is removed from the queue, D-Bus command
dispatcher removes the corresponding command proxy object from D-Bus.

BUG=chromium:374864
TEST=FEATURES=test emerge-link buffet

Change-Id: Ib7ce7370bd3ee471e22f02b8546675021ff063d7
Reviewed-on: https://chromium-review.googlesource.com/211642
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
Reviewed-by: Christopher Wiley <wiley@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/buffet.gyp b/buffet/buffet.gyp
index f245e5e..0a10afe 100644
--- a/buffet/buffet.gyp
+++ b/buffet/buffet.gyp
@@ -18,6 +18,7 @@
         'commands/command_instance.cc',
         'commands/command_manager.cc',
         'commands/command_queue.cc',
+        'commands/dbus_command_dispatcher.cc',
         'commands/dbus_command_proxy.cc',
         'commands/object_schema.cc',
         'commands/prop_constraints.cc',
@@ -76,6 +77,7 @@
             'commands/command_instance_unittest.cc',
             'commands/command_manager_unittest.cc',
             'commands/command_queue_unittest.cc',
+            'commands/dbus_command_dispatcher_unittest.cc',
             'commands/dbus_command_proxy_unittest.cc',
             'commands/object_schema_unittest.cc',
             'commands/schema_utils_unittest.cc',
diff --git a/buffet/commands/command_manager.cc b/buffet/commands/command_manager.cc
index 3d55906..ae105b8 100644
--- a/buffet/commands/command_manager.cc
+++ b/buffet/commands/command_manager.cc
@@ -8,13 +8,26 @@
 #include <base/files/file_enumerator.h>
 #include <base/json/json_reader.h>
 #include <base/values.h>
+#include <chromeos/dbus/exported_object_manager.h>
 #include <chromeos/errors/error.h>
 #include <chromeos/errors/error_codes.h>
 
 #include "buffet/commands/schema_constants.h"
 
+using chromeos::dbus_utils::ExportedObjectManager;
+
 namespace buffet {
 
+CommandManager::CommandManager() {
+  command_queue_.SetCommandDispachInterface(&command_dispatcher_);
+}
+
+CommandManager::CommandManager(
+    const base::WeakPtr<ExportedObjectManager>& object_manager)
+    : command_dispatcher_(object_manager->GetBus(), object_manager.get()) {
+  command_queue_.SetCommandDispachInterface(&command_dispatcher_);
+}
+
 const CommandDictionary& CommandManager::GetCommandDictionary() const {
   return dictionary_;
 }
@@ -104,4 +117,9 @@
   return std::unique_ptr<const base::DictionaryValue>(dict_value);
 }
 
+std::string CommandManager::AddCommand(
+    std::unique_ptr<CommandInstance> command_instance) {
+  return command_queue_.Add(std::move(command_instance));
+}
+
 }  // namespace buffet
diff --git a/buffet/commands/command_manager.h b/buffet/commands/command_manager.h
index d3f9a34..601ae9a 100644
--- a/buffet/commands/command_manager.h
+++ b/buffet/commands/command_manager.h
@@ -5,21 +5,37 @@
 #ifndef BUFFET_COMMANDS_COMMAND_MANAGER_H_
 #define BUFFET_COMMANDS_COMMAND_MANAGER_H_
 
+#include <memory>
 #include <string>
 
-#include <base/basictypes.h>
 #include <base/files/file_path.h>
+#include <base/macros.h>
+#include <base/memory/weak_ptr.h>
 
 #include "buffet/commands/command_dictionary.h"
+#include "buffet/commands/command_queue.h"
+#include "buffet/commands/dbus_command_dispatcher.h"
+
+namespace chromeos {
+namespace dbus_utils {
+class ExportedObjectManager;
+}  // namespace dbus_utils
+}  // namespace chromeos
+
 
 namespace buffet {
 
+class CommandInstance;
+
 // 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 {
+class CommandManager final {
  public:
-  CommandManager() = default;
+  CommandManager();
+  explicit CommandManager(
+      const base::WeakPtr<chromeos::dbus_utils::ExportedObjectManager>&
+          object_manager);
 
   // Get the command definitions for the device.
   const CommandDictionary& GetCommandDictionary() const;
@@ -59,6 +75,9 @@
   // the current device.
   void Startup();
 
+  // Adds a new command to the command queue. Returns command ID.
+  std::string AddCommand(std::unique_ptr<CommandInstance> command_instance);
+
  private:
   // Helper function to load a JSON file that is expected to be
   // an object/dictionary. In case of error, returns empty unique ptr and fills
@@ -68,6 +87,8 @@
 
   CommandDictionary base_dictionary_;  // Base/std command definitions/schemas.
   CommandDictionary dictionary_;  // Command definitions/schemas.
+  CommandQueue command_queue_;
+  DBusCommandDispacher command_dispatcher_;
 
   DISALLOW_COPY_AND_ASSIGN(CommandManager);
 };
diff --git a/buffet/commands/dbus_command_dispatcher.cc b/buffet/commands/dbus_command_dispatcher.cc
new file mode 100644
index 0000000..e6041d2
--- /dev/null
+++ b/buffet/commands/dbus_command_dispatcher.cc
@@ -0,0 +1,52 @@
+// 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/dbus_command_dispatcher.h"
+
+#include <chromeos/dbus/exported_object_manager.h>
+
+#include "buffet/commands/command_instance.h"
+
+using chromeos::dbus_utils::AsyncEventSequencer;
+using chromeos::dbus_utils::ExportedObjectManager;
+
+namespace buffet {
+
+DBusCommandDispacher::DBusCommandDispacher(
+    const scoped_refptr<dbus::Bus>& bus,
+    ExportedObjectManager* object_manager)
+    : bus_(bus) {
+  if (object_manager)
+    object_manager_ = object_manager->AsWeakPtr();
+}
+
+void DBusCommandDispacher::OnCommandAdded(CommandInstance* command_instance) {
+  auto proxy = CreateDBusCommandProxy(command_instance);
+  proxy->RegisterAsync(AsyncEventSequencer::GetDefaultCompletionAction());
+  command_instance->SetProxy(proxy.get());
+
+  auto pair = std::make_pair(command_instance, std::move(proxy));
+  CHECK(command_map_.insert(std::move(pair)).second)
+      << "The command instance is already in the dispatcher command map";
+}
+
+void DBusCommandDispacher::OnCommandRemoved(CommandInstance* command_instance) {
+  command_instance->SetProxy(nullptr);
+  CHECK_GT(command_map_.erase(command_instance), 0u)
+      << "The command instance is not in the dispatcher command map";
+}
+
+DBusCommandProxy* DBusCommandDispacher::FindProxy(
+    CommandInstance* command_instance) const {
+  auto p = command_map_.find(command_instance);
+  return p != command_map_.end() ? p->second.get() : nullptr;
+}
+
+std::unique_ptr<DBusCommandProxy> DBusCommandDispacher::CreateDBusCommandProxy(
+      CommandInstance* command_instance) const {
+  return std::unique_ptr<DBusCommandProxy>(
+      new DBusCommandProxy(object_manager_.get(), bus_, command_instance));
+}
+
+}  // namespace buffet
diff --git a/buffet/commands/dbus_command_dispatcher.h b/buffet/commands/dbus_command_dispatcher.h
new file mode 100644
index 0000000..94f938b
--- /dev/null
+++ b/buffet/commands/dbus_command_dispatcher.h
@@ -0,0 +1,69 @@
+// 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_DBUS_COMMAND_DISPATCHER_H_
+#define BUFFET_COMMANDS_DBUS_COMMAND_DISPATCHER_H_
+
+#include <map>
+#include <string>
+
+#include <base/macros.h>
+#include <base/memory/weak_ptr.h>
+#include <dbus/bus.h>
+
+#include "buffet/commands/command_dispatch_interface.h"
+#include "buffet/commands/dbus_command_proxy.h"
+
+namespace chromeos {
+namespace dbus_utils {
+class ExportedObjectManager;
+}  // namespace dbus_utils
+}  // namespace chromeos
+
+namespace buffet {
+
+// Implements D-Bus dispatch of commands. When OnCommandAdded is called over
+// CommandDispachInterface, DBusCommandDispacher creates an instance of
+// DBusCommandProxy object and advertises it through ExportedObjectManager on
+// D-Bus. Command handling processes can watch the new D-Bus object appear
+// and communicate with it to update the command handling progress.
+// Once command is handled, DBusCommandProxy::Done() is called and the command
+// is removed from the command queue and D-Bus ExportedObjectManager.
+class DBusCommandDispacher : public CommandDispachInterface {
+ public:
+  DBusCommandDispacher(
+      const scoped_refptr<dbus::Bus>& bus,
+      chromeos::dbus_utils::ExportedObjectManager* object_manager = nullptr);
+  virtual ~DBusCommandDispacher() = default;
+
+  // CommandDispachInterface overrides. Called by CommandQueue.
+  void OnCommandAdded(CommandInstance* command_instance) override;
+  void OnCommandRemoved(CommandInstance* command_instance) override;
+
+  // Finds a D-Bus command proxy for the given command instance.
+  // Returns nullptr if the proxy does not exist.
+  DBusCommandProxy* FindProxy(CommandInstance* command_instance) const;
+
+ protected:
+  virtual std::unique_ptr<DBusCommandProxy> CreateDBusCommandProxy(
+      CommandInstance* command_instance) const;
+
+ private:
+  scoped_refptr<dbus::Bus> bus_;
+  base::WeakPtr<chromeos::dbus_utils::ExportedObjectManager> object_manager_;
+  // This is the map that tracks relationship between CommandInstance and
+  // corresponding DBusCommandProxy objects.
+  std::map<CommandInstance*, std::unique_ptr<DBusCommandProxy>> command_map_;
+
+  // Default constructor is used in special circumstances such as for testing.
+  DBusCommandDispacher() = default;
+
+  friend class DBusCommandDispacherTest;
+  friend class CommandManager;
+  DISALLOW_COPY_AND_ASSIGN(DBusCommandDispacher);
+};
+
+}  // namespace buffet
+
+#endif  // BUFFET_COMMANDS_DBUS_COMMAND_DISPATCHER_H_
diff --git a/buffet/commands/dbus_command_dispatcher_unittest.cc b/buffet/commands/dbus_command_dispatcher_unittest.cc
new file mode 100644
index 0000000..dd52871
--- /dev/null
+++ b/buffet/commands/dbus_command_dispatcher_unittest.cc
@@ -0,0 +1,196 @@
+// 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 <memory>
+#include <string>
+
+#include <chromeos/dbus/exported_object_manager.h>
+#include <dbus/mock_bus.h>
+#include <dbus/mock_exported_object.h>
+#include <dbus/object_manager.h>
+#include <dbus/object_path.h>
+#include <gtest/gtest.h>
+
+#include "buffet/commands/command_dictionary.h"
+#include "buffet/commands/command_queue.h"
+#include "buffet/commands/dbus_command_dispatcher.h"
+#include "buffet/commands/unittest_utils.h"
+#include "buffet/dbus_constants.h"
+
+using buffet::unittests::CreateDictionaryValue;
+using chromeos::dbus_utils::AsyncEventSequencer;
+using testing::AnyNumber;
+using testing::InSequence;
+using testing::Invoke;
+using testing::Return;
+using testing::_;
+
+namespace buffet {
+
+namespace {
+
+const char kCommandCategory[] = "test_category";
+
+}  // anonymous namespace
+
+class DBusCommandDispacherTest : public testing::Test {
+ public:
+  void SetUp() override {
+    const dbus::ObjectPath kExportedObjectManagerPath("/test/om_path");
+    std::string cmd_path = dbus_constants::kCommandServicePathPrefix;
+    cmd_path += "1";
+    const dbus::ObjectPath kCmdObjPath(cmd_path);
+
+    dbus::Bus::Options options;
+    options.bus_type = dbus::Bus::SYSTEM;
+    bus_ = new dbus::MockBus(options);
+    // By default, don't worry about threading assertions.
+    EXPECT_CALL(*bus_, AssertOnOriginThread()).Times(AnyNumber());
+    EXPECT_CALL(*bus_, AssertOnDBusThread()).Times(AnyNumber());
+    // Use a mock exported object manager.
+    mock_exported_object_manager_ = new dbus::MockExportedObject(
+        bus_.get(), kExportedObjectManagerPath);
+    EXPECT_CALL(*bus_, GetExportedObject(kExportedObjectManagerPath))
+        .WillRepeatedly(Return(mock_exported_object_manager_.get()));
+    EXPECT_CALL(*mock_exported_object_manager_,
+                ExportMethod(_, _, _, _)).Times(AnyNumber());
+    om_.reset(new chromeos::dbus_utils::ExportedObjectManager(
+        bus_.get(), kExportedObjectManagerPath));
+    om_->RegisterAsync(AsyncEventSequencer::GetDefaultCompletionAction());
+    command_dispatcher_.reset(
+        new DBusCommandDispacher(om_->GetBus(), om_.get()));
+    command_queue_.SetCommandDispachInterface(command_dispatcher_.get());
+    // Use a mock exported object for command proxy.
+    mock_exported_command_proxy_ = new dbus::MockExportedObject(
+        bus_.get(), kCmdObjPath);
+    EXPECT_CALL(*bus_, GetExportedObject(kCmdObjPath))
+        .WillRepeatedly(Return(mock_exported_command_proxy_.get()));
+    EXPECT_CALL(*mock_exported_command_proxy_, ExportMethod(_, _, _, _))
+        .WillRepeatedly(Invoke(MockExportMethod));
+
+    auto json = CreateDictionaryValue(R"({
+      'base': {
+        'reboot': {
+          'parameters': {'delay': 'integer'}
+        },
+        'shutdown': {
+          'parameters': {}
+        }
+      }
+    })");
+    CHECK(dictionary_.LoadCommands(*json, kCommandCategory, nullptr, nullptr))
+        << "Failed to load command dictionary";
+  }
+
+  void TearDown() override {
+    EXPECT_CALL(*mock_exported_object_manager_, Unregister()).Times(1);
+    om_.reset();
+    bus_ = nullptr;
+  }
+
+  static void MockExportMethod(
+      const std::string& interface_name,
+      const std::string& method_name,
+      dbus::ExportedObject::MethodCallCallback method_call_callback,
+      dbus::ExportedObject::OnExportedCallback on_exported_callback) {
+    on_exported_callback.Run(interface_name, method_name, true);
+  }
+
+
+  std::string AddNewCommand(const std::string& json) {
+    auto command_instance = CommandInstance::FromJson(
+        CreateDictionaryValue(json.c_str()).get(), dictionary_, nullptr);
+    // Two interfaces are added - Command and Properties.
+    EXPECT_CALL(*mock_exported_object_manager_, SendSignal(_)).Times(2);
+    return command_instance ?
+        command_queue_.Add(std::move(command_instance)) : std::string();
+  }
+
+  void FinishCommand(DBusCommandProxy* proxy) {
+    proxy->HandleDone(nullptr);
+  }
+
+  void SetProgress(DBusCommandProxy* proxy, int progress) {
+    proxy->HandleSetProgress(nullptr, progress);
+  }
+
+
+  scoped_refptr<dbus::MockBus> bus_;
+  scoped_refptr<dbus::MockExportedObject> mock_exported_object_manager_;
+  scoped_refptr<dbus::MockExportedObject> mock_exported_command_proxy_;
+  std::unique_ptr<chromeos::dbus_utils::ExportedObjectManager> om_;
+  CommandDictionary dictionary_;
+  CommandQueue command_queue_;
+  std::unique_ptr<DBusCommandDispacher> command_dispatcher_;
+};
+
+TEST_F(DBusCommandDispacherTest, Test_Command_Base_Shutdown) {
+  std::string id = AddNewCommand("{'name':'base.shutdown'}");
+  EXPECT_EQ("1", id);
+  CommandInstance* command_instance = command_queue_.Find(id);
+  ASSERT_NE(nullptr, command_instance);
+  DBusCommandProxy* command_proxy =
+      command_dispatcher_->FindProxy(command_instance);
+  ASSERT_NE(nullptr, command_proxy);
+  EXPECT_EQ(CommandInstance::kStatusQueued, command_instance->GetStatus());
+
+  // Two properties are set, Progress = 50%, Status = "inProgress"
+  EXPECT_CALL(*mock_exported_command_proxy_, SendSignal(_)).Times(2);
+  SetProgress(command_proxy, 50);
+  EXPECT_EQ(CommandInstance::kStatusInProgress, command_instance->GetStatus());
+  EXPECT_EQ(50, command_instance->GetProgress());
+
+  // Command must be removed from the queue and proxy destroyed after calling
+  // FinishCommand().
+  // Two properties are set, Progress = 100%, Status = "done"
+  EXPECT_CALL(*mock_exported_command_proxy_, SendSignal(_)).Times(2);
+  // D-Bus command proxy is going away.
+  EXPECT_CALL(*mock_exported_command_proxy_, Unregister()).Times(1);
+  // Two interfaces are being removed on the D-Bus command object.
+  EXPECT_CALL(*mock_exported_object_manager_, SendSignal(_)).Times(2);
+  FinishCommand(command_proxy);
+
+  EXPECT_EQ(nullptr,
+            command_dispatcher_->FindProxy(command_instance));
+  EXPECT_EQ(nullptr, command_queue_.Find(id));
+}
+
+TEST_F(DBusCommandDispacherTest, Test_Command_Base_Reboot) {
+  std::string id = AddNewCommand(R"({
+    'name': 'base.reboot',
+    'parameters': {
+      'delay': 20
+    }
+  })");
+  EXPECT_EQ("1", id);
+  CommandInstance* command_instance = command_queue_.Find(id);
+  ASSERT_NE(nullptr, command_instance);
+  DBusCommandProxy* command_proxy =
+      command_dispatcher_->FindProxy(command_instance);
+  ASSERT_NE(nullptr, command_proxy);
+  EXPECT_EQ(CommandInstance::kStatusQueued, command_instance->GetStatus());
+
+  // Two properties are set, Progress = 50%, Status = "inProgress"
+  EXPECT_CALL(*mock_exported_command_proxy_, SendSignal(_)).Times(2);
+  SetProgress(command_proxy, 50);
+  EXPECT_EQ(CommandInstance::kStatusInProgress, command_instance->GetStatus());
+  EXPECT_EQ(50, command_instance->GetProgress());
+
+  // Command must be removed from the queue and proxy destroyed after calling
+  // FinishCommand().
+  // Two properties are set, Progress = 100%, Status = "done"
+  EXPECT_CALL(*mock_exported_command_proxy_, SendSignal(_)).Times(2);
+  // D-Bus command proxy is going away.
+  EXPECT_CALL(*mock_exported_command_proxy_, Unregister()).Times(1);
+  // Two interfaces are being removed on the D-Bus command object.
+  EXPECT_CALL(*mock_exported_object_manager_, SendSignal(_)).Times(2);
+  FinishCommand(command_proxy);
+
+  EXPECT_EQ(nullptr,
+            command_dispatcher_->FindProxy(command_instance));
+  EXPECT_EQ(nullptr, command_queue_.Find(id));
+}
+
+
+}  // namespace buffet
diff --git a/buffet/manager.cc b/buffet/manager.cc
index ac693f1..7c3cbd5 100644
--- a/buffet/manager.cc
+++ b/buffet/manager.cc
@@ -56,7 +56,8 @@
   //              the properties interface.
   state_.SetValue("{}");
   dbus_object_.RegisterAsync(cb);
-  command_manager_ = std::make_shared<CommandManager>();
+  command_manager_ =
+      std::make_shared<CommandManager>(dbus_object_.GetObjectManager());
   command_manager_->Startup();
   device_info_ = std::unique_ptr<DeviceRegistrationInfo>(
       new DeviceRegistrationInfo(command_manager_));