Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 14c1f8af authored by Treehugger Robot's avatar Treehugger Robot Committed by Gerrit Code Review
Browse files

Merge "GD-storage: Add StorageModule"

parents da74161f 369f01ac
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -7,6 +7,7 @@ filegroup {
            "legacy_config_file.cc",
            "legacy_osi_config.cc",
            "mutation_entry.cc",
            "storage_module.cc",
    ],
}

@@ -19,6 +20,7 @@ filegroup {
            "legacy_test.cc",
            "legacy_osi_config.cc",
            "mutation_test.cc",
            "storage_module_test.cc",
    ],
}
+177 −0
Original line number Diff line number Diff line
/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "storage/storage_module.h"

#include <chrono>
#include <ctime>
#include <iomanip>
#include <memory>

#include "common/bind.h"
#include "os/alarm.h"
#include "os/handler.h"
#include "os/system_properties.h"
#include "storage/config_cache.h"
#include "storage/legacy_config_file.h"
#include "storage/mutation.h"

namespace bluetooth {
namespace storage {

using common::ListMap;
using common::LruCache;
using os::Alarm;
using os::Handler;

static const std::string kFactoryResetProperty = "persist.bluetooth.factoryreset";

static const std::string kConfigFilePath = "/data/misc/bluedroid/bt_config.conf";
static const size_t kDefaultTempDeviceCapacity = 10000;
// Save config whenever there is a change, but delay it by this value so that burst config change won't overwhelm disk
static const std::chrono::milliseconds kDefaultConfigSaveDelay = std::chrono::milliseconds(3000);
// Writing a config to disk takes a minimum 10 ms on a decent x86_64 machine, and 20 ms if including backup file
// The config saving delay must be bigger than this value to avoid overwhelming the disk
static const std::chrono::milliseconds kMinConfigSaveDelay = std::chrono::milliseconds(20);

const std::string StorageModule::kInfoSection = "Info";
const std::string StorageModule::kFileSourceProperty = "FileSource";
const std::string StorageModule::kTimeCreatedProperty = "TimeCreated";
const std::string StorageModule::kTimeCreatedFormat = "%Y-%m-%d %H:%M:%S";

const std::string StorageModule::kAdapterSection = "Adapter";

StorageModule::StorageModule(
    std::string config_file_path,
    std::chrono::milliseconds config_save_delay,
    size_t temp_devices_capacity,
    bool is_restricted_mode,
    bool is_single_user_mode)
    : config_file_path_(std::move(config_file_path)),
      config_save_delay_(config_save_delay),
      temp_devices_capacity_(temp_devices_capacity),
      is_restricted_mode_(is_restricted_mode),
      is_single_user_mode_(is_single_user_mode) {
  // e.g. "/data/misc/bluedroid/bt_config.conf" to "/data/misc/bluedroid/bt_config.bak"
  config_backup_path_ = config_file_path_.substr(0, config_file_path_.find_last_of('.')) + ".bak";
  ASSERT_LOG(
      config_save_delay > kMinConfigSaveDelay,
      "Config save delay of %lld ms is not enought, must be at least %lld ms to avoid overwhelming the disk",
      config_save_delay_.count(),
      kMinConfigSaveDelay.count());
};

StorageModule::~StorageModule() {
  std::lock_guard<std::mutex> lock(mutex_);
  pimpl_.reset();
}

const ModuleFactory StorageModule::Factory = ModuleFactory([]() {
  return new StorageModule(kConfigFilePath, kDefaultConfigSaveDelay, kDefaultTempDeviceCapacity, false, false);
});

struct StorageModule::impl {
  explicit impl(Handler* handler, ConfigCache cache) : config_save_alarm_(handler), cache_(std::move(cache)) {}
  Alarm config_save_alarm_;
  ConfigCache cache_;
  bool has_pending_config_save_ = false;
};

Mutation StorageModule::Modify() {
  std::lock_guard<std::mutex> lock(mutex_);
  return Mutation(pimpl_->cache_);
}

ConfigCache* StorageModule::GetConfigCache() {
  std::lock_guard<std::mutex> lock(mutex_);
  return &pimpl_->cache_;
}

void StorageModule::SaveDelayed() {
  std::lock_guard<std::mutex> lock(mutex_);
  if (pimpl_->has_pending_config_save_) {
    return;
  }
  pimpl_->config_save_alarm_.Schedule(
      common::BindOnce(&StorageModule::SaveImmediately, common::Unretained(this)), config_save_delay_);
  pimpl_->has_pending_config_save_ = true;
}

void StorageModule::SaveImmediately() {
  std::lock_guard<std::mutex> lock(mutex_);
  if (pimpl_->has_pending_config_save_) {
    pimpl_->config_save_alarm_.Cancel();
    pimpl_->has_pending_config_save_ = false;
  }
  ASSERT(LegacyConfigFile::FromPath(config_file_path_).Write(pimpl_->cache_));
  ASSERT(LegacyConfigFile::FromPath(config_backup_path_).Write(pimpl_->cache_));
}

void StorageModule::ListDependencies(ModuleList* list) {
  // No dependencies
}

void StorageModule::Start() {
  std::lock_guard<std::mutex> lock(mutex_);
  std::string file_source;
  if (os::GetSystemProperty(kFactoryResetProperty) == "true") {
    LegacyConfigFile::FromPath(config_file_path_).Delete();
    LegacyConfigFile::FromPath(config_backup_path_).Delete();
  }
  auto config = LegacyConfigFile::FromPath(config_file_path_).Read(temp_devices_capacity_);
  if (!config || !config->HasSection(kAdapterSection)) {
    LOG_WARN("cannot load config at %s, using backup at %s.", config_file_path_.c_str(), config_backup_path_.c_str());
    config = LegacyConfigFile::FromPath(config_backup_path_).Read(temp_devices_capacity_);
    file_source = "Backup";
  }
  if (!config || !config->HasSection(kAdapterSection)) {
    LOG_WARN("cannot load backup config at %s; creating new empty ones", config_backup_path_.c_str());
    config.emplace(temp_devices_capacity_);
    file_source = "Empty";
  }
  if (!file_source.empty()) {
    config->SetProperty(kInfoSection, kFileSourceProperty, std::move(file_source));
  }
  // Cleanup temporary pairings if we have left guest mode
  if (!is_restricted_mode_) {
    config->RemoveSectionWithProperty("Restricted");
  }
  // Read or set config file creation timestamp
  auto time_str = config->GetProperty(kInfoSection, kTimeCreatedProperty);
  if (!time_str) {
    std::stringstream ss;
    auto now = std::chrono::system_clock::now();
    auto now_time_t = std::chrono::system_clock::to_time_t(now);
    ss << std::put_time(std::localtime(&now_time_t), kTimeCreatedFormat.c_str());
    config->SetProperty(kInfoSection, kTimeCreatedProperty, ss.str());
  }
  config->SetPersistentConfigChangedCallback([this] { this->SaveDelayed(); });
  // TODO (b/158035889) Migrate metrics module to GD
  pimpl_ = std::make_unique<impl>(GetHandler(), std::move(config.value()));
}

void StorageModule::Stop() {
  SaveImmediately();
  std::lock_guard<std::mutex> lock(mutex_);
  pimpl_.reset();
}

std::string StorageModule::ToString() const {
  return "Storage Module";
}

}  // namespace storage
}  // namespace bluetooth
 No newline at end of file
+92 −0
Original line number Diff line number Diff line
/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#pragma once

#include <array>
#include <chrono>
#include <cstdint>
#include <list>
#include <memory>
#include <mutex>
#include <string>

#include "hci/address.h"
#include "module.h"
#include "storage/config_cache.h"
#include "storage/mutation.h"

namespace bluetooth {

namespace storage {

class StorageModule : public bluetooth::Module {
 public:
  static const std::string kInfoSection;
  static const std::string kFileSourceProperty;
  static const std::string kTimeCreatedProperty;
  static const std::string kTimeCreatedFormat;

  static const std::string kAdapterSection;

  // Create the storage module where:
  // - config_file_path is the path to the config file on disk, a .bak file will be created with the original
  // - config_save_delay is the duration after which to dump config to disk after SaveDelayed() is called
  // - temp_devices_capacity is the number of temporary, typically unpaired devices to hold in a memory based LRU
  // - is_restricted_mode and is_single_user_mode are flags from upper layer
  StorageModule(
      std::string config_file_path,
      std::chrono::milliseconds config_save_delay,
      size_t temp_devices_capacity,
      bool is_restricted_mode,
      bool is_single_user_mode);
  ~StorageModule() override;
  static const ModuleFactory Factory;

  // Modify the underlying config by starting a mutation. All entries in the mutation will be applied atomically when
  // Commit() is called. User should never touch ConfigCache() directly.
  Mutation Modify();

 protected:
  void ListDependencies(ModuleList* list) override;
  void Start() override;
  void Stop() override;
  std::string ToString() const override;

  // For shim layer only
  ConfigCache* GetConfigCache();
  // Normally, underlying config will be saved at most 3 seconds after the first config change in a series of changes
  // This method triggers the delayed saving automatically, the delay is equal to |config_save_delay_|
  void SaveDelayed();
  // In some cases, one may want to save the config immediately to disk. Call this method with caution as it runs
  // immediately on the calling thread
  void SaveImmediately();

 private:
  struct impl;
  mutable std::mutex mutex_;
  std::unique_ptr<impl> pimpl_;
  std::string config_file_path_;
  std::string config_backup_path_;
  std::chrono::milliseconds config_save_delay_;
  size_t temp_devices_capacity_;
  bool is_restricted_mode_;
  bool is_single_user_mode_;

  DISALLOW_COPY_AND_ASSIGN(StorageModule);
};

}  // namespace storage
}  // namespace bluetooth
+253 −0
Original line number Diff line number Diff line
/*
 * Copyright 2020 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "storage/storage_module.h"

#include <gmock/gmock.h>
#include <gtest/gtest.h>

#include <chrono>
#include <cstdio>
#include <ctime>
#include <filesystem>
#include <iomanip>
#include <optional>
#include <thread>

#include "module.h"
#include "os/files.h"
#include "storage/config_cache.h"
#include "storage/legacy_config_file.h"

namespace testing {

using bluetooth::TestModuleRegistry;
using bluetooth::storage::ConfigCache;
using bluetooth::storage::LegacyConfigFile;
using bluetooth::storage::StorageModule;

static const std::chrono::milliseconds kTestConfigSaveDelay = std::chrono::milliseconds(100);

static std::optional<std::chrono::system_clock::time_point> ParseTimestamp(
    const std::string& timestamp, const std::string& format) {
  std::istringstream ss(timestamp);
  // 1. Parse to time_t from timestamp that may not contain day light saving information
  std::tm no_dst_tm = {};
  ss >> std::get_time(&no_dst_tm, format.c_str());
  if (ss.fail()) {
    return std::nullopt;
  }
  // 2. Make a copy of the parsed result so that we can set tm_isdst bit later
  auto dst_tm = no_dst_tm;
  auto no_dst_time_t = std::mktime(&no_dst_tm);
  if (no_dst_time_t == -1) {
    return std::nullopt;
  }
  // 3. Convert time_t to tm again, but let system decide if day light saving should be set at that date and time
  auto dst_tm_only = std::localtime(&no_dst_time_t);
  // 4. Set the correct tm_isdst bit
  dst_tm.tm_isdst = dst_tm_only->tm_isdst;
  auto dst_time_t = std::mktime(&dst_tm);
  if (dst_time_t == -1) {
    return std::nullopt;
  }
  // 5. Parse is to time point
  return std::chrono::system_clock::from_time_t(dst_time_t);
}

class TestStorageModule : public StorageModule {
 public:
  TestStorageModule(
      std::string config_file_path,
      std::chrono::milliseconds config_save_delay,
      size_t temp_devices_capacity,
      bool is_restricted_mode,
      bool is_single_user_mode)
      : StorageModule(
            std::move(config_file_path),
            config_save_delay,
            temp_devices_capacity,
            is_restricted_mode,
            is_single_user_mode) {}

  ConfigCache* GetConfigCachePublic() {
    return StorageModule::GetConfigCache();
  }

  void SaveImmediatelyPublic() {
    StorageModule::SaveImmediately();
  }
};

class StorageModuleTest : public Test {
 protected:
  void SetUp() override {
    temp_dir_ = std::filesystem::temp_directory_path();
    temp_config_ = temp_dir_ / "temp_config.txt";
    temp_backup_config_ = temp_dir_ / "temp_config.bak";
    DeleteConfigFiles();
    EXPECT_FALSE(std::filesystem::exists(temp_config_));
    EXPECT_FALSE(std::filesystem::exists(temp_backup_config_));
  }

  void TearDown() override {
    DeleteConfigFiles();
  }

  void DeleteConfigFiles() {
    if (std::filesystem::exists(temp_config_)) {
      EXPECT_TRUE(std::filesystem::remove(temp_config_));
    }
    if (std::filesystem::exists(temp_backup_config_)) {
      EXPECT_TRUE(std::filesystem::remove(temp_backup_config_));
    }
  }

  std::filesystem::path temp_dir_;
  std::filesystem::path temp_config_;
  std::filesystem::path temp_backup_config_;
};

TEST_F(StorageModuleTest, empty_config_no_op_test) {
  // Actual test
  auto time_before = std::chrono::system_clock::now();
  auto* storage = new TestStorageModule(temp_config_.string(), kTestConfigSaveDelay, 10, false, false);
  TestModuleRegistry test_registry;
  test_registry.InjectTestModule(&StorageModule::Factory, storage);
  test_registry.StopAll();
  auto time_after = std::chrono::system_clock::now();

  // Verify states after test
  EXPECT_TRUE(std::filesystem::exists(temp_config_));

  // Verify config after test
  auto config = LegacyConfigFile::FromPath(temp_config_.string()).Read(10);
  EXPECT_TRUE(config);
  EXPECT_TRUE(config->HasSection(StorageModule::kInfoSection));
  EXPECT_THAT(
      config->GetProperty(StorageModule::kInfoSection, StorageModule::kFileSourceProperty), Optional(StrEq("Empty")));

  // Verify file creation timestamp falls between time_before and time_after
  auto timestamp = config->GetProperty(StorageModule::kInfoSection, StorageModule::kTimeCreatedProperty);
  EXPECT_TRUE(timestamp);
  auto file_time = ParseTimestamp(*timestamp, StorageModule::kTimeCreatedFormat);
  EXPECT_TRUE(file_time);
  EXPECT_GE(std::chrono::duration_cast<std::chrono::seconds>(time_after - *file_time).count(), 0);
  EXPECT_GE(std::chrono::duration_cast<std::chrono::seconds>(*file_time - time_before).count(), 0);
}

static const std::string kReadTestConfig =
    "[Info]\n"
    "FileSource = Empty\n"
    "TimeCreated = 2020-05-20 01:20:56\n"
    "\n"
    "[Metrics]\n"
    "Salt256Bit = 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\n"
    "\n"
    "[Adapter]\n"
    "Address = 01:02:03:ab:cd:ef\n"
    "LE_LOCAL_KEY_IRK = fedcba0987654321fedcba0987654321\n"
    "LE_LOCAL_KEY_IR = fedcba0987654321fedcba0987654322\n"
    "LE_LOCAL_KEY_DHK = fedcba0987654321fedcba0987654323\n"
    "LE_LOCAL_KEY_ER = fedcba0987654321fedcba0987654324\n"
    "ScanMode = 2\n"
    "DiscoveryTimeout = 120\n"
    "\n"
    "[01:02:03:ab:cd:ea]\n"
    "name = hello world\n"
    "LinkKey = fedcba0987654321fedcba0987654328\n"
    "\n";

TEST_F(StorageModuleTest, read_existing_config_test) {
  EXPECT_TRUE(bluetooth::os::WriteToFile(temp_config_.string(), kReadTestConfig));
  // Actual test

  // Set up
  auto* storage = new TestStorageModule(temp_config_.string(), kTestConfigSaveDelay, 10, false, false);
  TestModuleRegistry test_registry;
  test_registry.InjectTestModule(&StorageModule::Factory, storage);

  // Test
  EXPECT_NE(storage->GetConfigCachePublic(), nullptr);
  EXPECT_TRUE(storage->GetConfigCachePublic()->HasSection("Metrics"));
  EXPECT_THAT(storage->GetConfigCachePublic()->GetPersistentDevices(), ElementsAre("01:02:03:ab:cd:ea"));
  EXPECT_THAT(
      storage->GetConfigCachePublic()->GetProperty(StorageModule::kAdapterSection, "Address"),
      Optional(StrEq("01:02:03:ab:cd:ef")));

  // Tear down
  test_registry.StopAll();

  // Verify states after test
  EXPECT_TRUE(std::filesystem::exists(temp_config_));

  // Verify config after test
  auto config = bluetooth::os::ReadSmallFile(temp_config_.string());
  EXPECT_TRUE(config);
  EXPECT_EQ(*config, kReadTestConfig);
}

TEST_F(StorageModuleTest, save_config_test) {
  // Prepare config file
  EXPECT_TRUE(bluetooth::os::WriteToFile(temp_config_.string(), kReadTestConfig));

  // Set up
  auto* storage = new TestStorageModule(temp_config_.string(), kTestConfigSaveDelay, 10, false, false);
  TestModuleRegistry test_registry;
  test_registry.InjectTestModule(&StorageModule::Factory, storage);

  // Test
  EXPECT_NE(storage->GetConfigCachePublic(), nullptr);

  // Change a property
  EXPECT_THAT(
      storage->GetConfigCachePublic()->GetProperty("01:02:03:ab:cd:ea", "name"), Optional(StrEq("hello world")));
  storage->GetConfigCachePublic()->SetProperty("01:02:03:ab:cd:ea", "name", "foo");
  EXPECT_THAT(storage->GetConfigCachePublic()->GetProperty("01:02:03:ab:cd:ea", "name"), Optional(StrEq("foo")));
  std::this_thread::sleep_for(kTestConfigSaveDelay * 1.5);
  auto config = LegacyConfigFile::FromPath(temp_config_.string()).Read(10);
  EXPECT_TRUE(config);
  EXPECT_THAT(config->GetProperty("01:02:03:ab:cd:ea", "name"), Optional(StrEq("foo")));

  // Remove a property
  storage->GetConfigCachePublic()->RemoveProperty("01:02:03:ab:cd:ea", "name");
  std::this_thread::sleep_for(kTestConfigSaveDelay * 1.5);
  config = LegacyConfigFile::FromPath(temp_config_.string()).Read(10);
  EXPECT_TRUE(config);
  EXPECT_FALSE(config->HasProperty("01:02:03:ab:cd:ea", "name"));

  // Remove a section
  storage->GetConfigCachePublic()->RemoveSection("01:02:03:ab:cd:ea");
  std::this_thread::sleep_for(kTestConfigSaveDelay * 1.5);
  config = LegacyConfigFile::FromPath(temp_config_.string()).Read(10);
  EXPECT_TRUE(config);
  EXPECT_FALSE(config->HasSection("01:02:03:ab:cd:ea"));

  // Add a section and save immediately
  storage->GetConfigCachePublic()->SetProperty("01:02:03:ab:cd:eb", "LinkKey", "123456");
  storage->SaveImmediatelyPublic();
  config = LegacyConfigFile::FromPath(temp_config_.string()).Read(10);
  EXPECT_TRUE(config);
  EXPECT_TRUE(config->HasSection("01:02:03:ab:cd:eb"));

  // Tear down
  test_registry.StopAll();

  // Verify states after test
  EXPECT_TRUE(std::filesystem::exists(temp_config_));
}

}  // namespace testing
 No newline at end of file