// Copyright 2013 The Chromium 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 "chromeos/system/statistics_provider.h" #include "base/bind.h" #include "base/command_line.h" #include "base/files/file_path.h" #include "base/files/file_util.h" #include "base/json/json_file_value_serializer.h" #include "base/location.h" #include "base/logging.h" #include "base/macros.h" #include "base/memory/singleton.h" #include "base/path_service.h" #include "base/strings/string_number_conversions.h" #include "base/synchronization/cancellation_flag.h" #include "base/synchronization/waitable_event.h" #include "base/sys_info.h" #include "base/task_runner.h" #include "base/threading/thread_restrictions.h" #include "base/time/time.h" #include "base/values.h" #include "chromeos/app_mode/kiosk_oem_manifest_parser.h" #include "chromeos/chromeos_constants.h" #include "chromeos/chromeos_paths.h" #include "chromeos/chromeos_switches.h" #include "chromeos/system/name_value_pairs_parser.h" namespace chromeos { namespace system { namespace { // Path to the tool used to get system info, and delimiters for the output // format of the tool. const char* kCrosSystemTool[] = { "/usr/bin/crossystem" }; const char kCrosSystemEq[] = "="; const char kCrosSystemDelim[] = "\n"; const char kCrosSystemCommentDelim[] = "#"; const char kCrosSystemUnknownValue[] = "(error)"; const char kHardwareClassCrosSystemKey[] = "hwid"; const char kUnknownHardwareClass[] = "unknown"; // Key/value delimiters of machine hardware info file. machine-info is generated // only for OOBE and enterprise enrollment and may not be present. See // login-manager/init/machine-info.conf. const char kMachineHardwareInfoEq[] = "="; const char kMachineHardwareInfoDelim[] = " \n"; // File to get ECHO coupon info from, and key/value delimiters of // the file. const char kEchoCouponFile[] = "/var/cache/echo/vpd_echo.txt"; const char kEchoCouponEq[] = "="; const char kEchoCouponDelim[] = "\n"; // File to get VPD info from, and key/value delimiters of the file. const char kVpdFile[] = "/var/log/vpd_2.0.txt"; const char kVpdEq[] = "="; const char kVpdDelim[] = "\n"; // File to get regional data from. const char kCrosRegions[] = "/usr/share/misc/cros-regions.json"; // Timeout that we should wait for statistics to get loaded const int kTimeoutSecs = 3; // The location of OEM manifest file used to trigger OOBE flow for kiosk mode. const base::CommandLine::CharType kOemManifestFilePath[] = FILE_PATH_LITERAL("/usr/share/oem/oobe/manifest.json"); // Items in region dictionary. const char kKeyboardsPath[] = "keyboards"; const char kLocalesPath[] = "locales"; const char kTimeZonesPath[] = "time_zones"; // Gets ListValue from given |dictionary| by given |key| and (unless |result| is // nullptr) sets |result| to a string with all list values joined by ','. // Returns true on success. bool JoinListValuesToString(const base::DictionaryValue* dictionary, const std::string key, std::string* result) { const base::ListValue* list = nullptr; if (!dictionary->GetListWithoutPathExpansion(key, &list)) return false; std::string buffer; bool first = true; for (const base::Value* v : *list) { std::string value; if (!v->GetAsString(&value)) return false; if (first) first = false; else buffer += ','; buffer += value; } if (result != nullptr) *result = buffer; return true; } // Gets ListValue from given |dictionary| by given |key| and (unless |result| is // nullptr) sets |result| to the first value as string. Returns true on // success. bool GetFirstListValueAsString(const base::DictionaryValue* dictionary, const std::string key, std::string* result) { const base::ListValue* list = nullptr; if (!dictionary->GetListWithoutPathExpansion(key, &list)) return false; std::string value; if (!list->GetString(0, &value)) return false; if (result != nullptr) *result = value; return true; } bool GetKeyboardLayoutFromRegionalData(const base::DictionaryValue* region_dict, std::string* result) { return JoinListValuesToString(region_dict, kKeyboardsPath, result); } bool GetInitialTimezoneFromRegionalData( const base::DictionaryValue* region_dict, std::string* result) { return GetFirstListValueAsString(region_dict, kTimeZonesPath, result); } bool GetInitialLocaleFromRegionalData(const base::DictionaryValue* region_dict, std::string* result) { return JoinListValuesToString(region_dict, kLocalesPath, result); } } // namespace // Key values for GetMachineStatistic()/GetMachineFlag() calls. const char kActivateDateKey[] = "ActivateDate"; const char kCustomizationIdKey[] = "customization_id"; const char kDevSwitchBootKey[] = "devsw_boot"; const char kDevSwitchBootValueDev[] = "1"; const char kDevSwitchBootValueVerified[] = "0"; const char kFirmwareTypeKey[] = "mainfw_type"; const char kFirmwareTypeValueDeveloper[] = "developer"; const char kFirmwareTypeValueNonchrome[] = "nonchrome"; const char kFirmwareTypeValueNormal[] = "normal"; const char kHardwareClassKey[] = "hardware_class"; const char kOffersCouponCodeKey[] = "ubind_attribute"; const char kOffersGroupCodeKey[] = "gbind_attribute"; const char kRlzBrandCodeKey[] = "rlz_brand_code"; const char kWriteProtectSwitchBootKey[] = "wpsw_boot"; const char kWriteProtectSwitchBootValueOff[] = "0"; const char kWriteProtectSwitchBootValueOn[] = "1"; const char kRegionKey[] = "region"; const char kInitialLocaleKey[] = "initial_locale"; const char kInitialTimezoneKey[] = "initial_timezone"; const char kKeyboardLayoutKey[] = "keyboard_layout"; // OEM specific statistics. Must be prefixed with "oem_". const char kOemCanExitEnterpriseEnrollmentKey[] = "oem_can_exit_enrollment"; const char kOemDeviceRequisitionKey[] = "oem_device_requisition"; const char kOemIsEnterpriseManagedKey[] = "oem_enterprise_managed"; const char kOemKeyboardDrivenOobeKey[] = "oem_keyboard_driven_oobe"; bool HasOemPrefix(const std::string& name) { return name.substr(0, 4) == "oem_"; } // The StatisticsProvider implementation used in production. class StatisticsProviderImpl : public StatisticsProvider { public: // StatisticsProvider implementation: void StartLoadingMachineStatistics( const scoped_refptr& file_task_runner, bool load_oem_manifest) override; bool GetMachineStatistic(const std::string& name, std::string* result) override; bool GetMachineFlag(const std::string& name, bool* result) override; void Shutdown() override; static StatisticsProviderImpl* GetInstance(); protected: typedef std::map MachineFlags; typedef bool (*RegionDataExtractor)(const base::DictionaryValue*, std::string*); friend struct base::DefaultSingletonTraits; StatisticsProviderImpl(); ~StatisticsProviderImpl() override; // Waits up to |kTimeoutSecs| for statistics to be loaded. Returns true if // they were loaded successfully. bool WaitForStatisticsLoaded(); // Loads the machine statistics off of disk. Runs on the file thread. void LoadMachineStatistics(bool load_oem_manifest); // Loads the OEM statistics off of disk. Runs on the file thread. void LoadOemManifestFromFile(const base::FilePath& file); // Loads regional data off of disk. Runs on the file thread. void LoadRegionsFile(const base::FilePath& filename); // Extracts known data from regional_data_; // Returns true on success; bool GetRegionalInformation(const std::string& name, std::string* result) const; // Returns current region dictionary or NULL if not found. const base::DictionaryValue* GetRegionDictionary() const; // Returns extractor from regional_data_extractors_ or nullptr. RegionDataExtractor GetRegionalDataExtractor(const std::string& name) const; bool load_statistics_started_; NameValuePairsParser::NameValueMap machine_info_; MachineFlags machine_flags_; base::CancellationFlag cancellation_flag_; // |on_statistics_loaded_| protects |machine_info_| and |machine_flags_|. base::WaitableEvent on_statistics_loaded_; bool oem_manifest_loaded_; std::string region_; scoped_ptr regional_data_; base::hash_map regional_data_extractors_; private: DISALLOW_COPY_AND_ASSIGN(StatisticsProviderImpl); }; bool StatisticsProviderImpl::WaitForStatisticsLoaded() { CHECK(load_statistics_started_); if (on_statistics_loaded_.IsSignaled()) return true; // Block if the statistics are not loaded yet. Normally this shouldn't // happen except during OOBE. base::Time start_time = base::Time::Now(); base::ThreadRestrictions::ScopedAllowWait allow_wait; on_statistics_loaded_.TimedWait(base::TimeDelta::FromSeconds(kTimeoutSecs)); base::TimeDelta dtime = base::Time::Now() - start_time; if (on_statistics_loaded_.IsSignaled()) { LOG(WARNING) << "Statistics loaded after waiting " << dtime.InMilliseconds() << "ms."; return true; } LOG(ERROR) << "Statistics not loaded after waiting " << dtime.InMilliseconds() << "ms."; return false; } const base::DictionaryValue* StatisticsProviderImpl::GetRegionDictionary() const { const base::DictionaryValue* full_dict = nullptr; if (!regional_data_->GetAsDictionary(&full_dict)) return nullptr; const base::DictionaryValue* region_dict = nullptr; if (!full_dict->GetDictionaryWithoutPathExpansion(region_, ®ion_dict)) return nullptr; return region_dict; } StatisticsProviderImpl::RegionDataExtractor StatisticsProviderImpl::GetRegionalDataExtractor( const std::string& name) const { const auto it = regional_data_extractors_.find(name); if (it == regional_data_extractors_.end()) return nullptr; return it->second; } bool StatisticsProviderImpl::GetRegionalInformation(const std::string& name, std::string* result) const { if (region_.empty() || !regional_data_.get()) return false; const RegionDataExtractor extractor = GetRegionalDataExtractor(name); if (!extractor) return false; const base::DictionaryValue* region_dict = GetRegionDictionary(); if (!region_dict) return false; return extractor(region_dict, result); } bool StatisticsProviderImpl::GetMachineStatistic(const std::string& name, std::string* result) { VLOG(1) << "Machine Statistic requested: " << name; if (!WaitForStatisticsLoaded()) { LOG(ERROR) << "GetMachineStatistic called before load started: " << name; return false; } base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); std::string cros_regions_mode; if (command_line->HasSwitch(chromeos::switches::kCrosRegionsMode)) { cros_regions_mode = command_line->GetSwitchValueASCII(chromeos::switches::kCrosRegionsMode); } // These two modes override existing machine statistics keys. // By default (cros_regions_mode is empty), the same keys are emulated if // they do not exist in machine statistics. if (cros_regions_mode == chromeos::switches::kCrosRegionsModeOverride || cros_regions_mode == chromeos::switches::kCrosRegionsModeHide) { if (GetRegionalInformation(name, result)) return true; } if (cros_regions_mode == chromeos::switches::kCrosRegionsModeHide && GetRegionalDataExtractor(name)) { return false; } NameValuePairsParser::NameValueMap::iterator iter = machine_info_.find(name); if (iter == machine_info_.end()) { if (GetRegionalInformation(name, result)) return true; if (result != nullptr && base::SysInfo::IsRunningOnChromeOS() && (oem_manifest_loaded_ || !HasOemPrefix(name))) { LOG(WARNING) << "Requested statistic not found: " << name; } return false; } if (result != nullptr) *result = iter->second; return true; } bool StatisticsProviderImpl::GetMachineFlag(const std::string& name, bool* result) { VLOG(1) << "Machine Flag requested: " << name; if (!WaitForStatisticsLoaded()) { LOG(ERROR) << "GetMachineFlag called before load started: " << name; return false; } MachineFlags::const_iterator iter = machine_flags_.find(name); if (iter == machine_flags_.end()) { if (result != nullptr && base::SysInfo::IsRunningOnChromeOS() && (oem_manifest_loaded_ || !HasOemPrefix(name))) { LOG(WARNING) << "Requested machine flag not found: " << name; } return false; } if (result != nullptr) *result = iter->second; return true; } void StatisticsProviderImpl::Shutdown() { cancellation_flag_.Set(); // Cancel any pending loads } StatisticsProviderImpl::StatisticsProviderImpl() : load_statistics_started_(false), on_statistics_loaded_(true /* manual_reset */, false /* initially_signaled */), oem_manifest_loaded_(false) { regional_data_extractors_[kInitialLocaleKey] = &GetInitialLocaleFromRegionalData; regional_data_extractors_[kKeyboardLayoutKey] = &GetKeyboardLayoutFromRegionalData; regional_data_extractors_[kInitialTimezoneKey] = &GetInitialTimezoneFromRegionalData; } StatisticsProviderImpl::~StatisticsProviderImpl() { } void StatisticsProviderImpl::StartLoadingMachineStatistics( const scoped_refptr& file_task_runner, bool load_oem_manifest) { CHECK(!load_statistics_started_); load_statistics_started_ = true; VLOG(1) << "Started loading statistics. Load OEM Manifest: " << load_oem_manifest; file_task_runner->PostTask( FROM_HERE, base::Bind(&StatisticsProviderImpl::LoadMachineStatistics, base::Unretained(this), load_oem_manifest)); } void StatisticsProviderImpl::LoadMachineStatistics(bool load_oem_manifest) { // Run from the file task runner. StatisticsProviderImpl is a Singleton<> and // will not be destroyed until after threads have been stopped, so this test // is always safe. if (cancellation_flag_.IsSet()) return; NameValuePairsParser parser(&machine_info_); if (base::SysInfo::IsRunningOnChromeOS()) { // Parse all of the key/value pairs from the crossystem tool. if (!parser.ParseNameValuePairsFromTool(arraysize(kCrosSystemTool), kCrosSystemTool, kCrosSystemEq, kCrosSystemDelim, kCrosSystemCommentDelim)) { LOG(ERROR) << "Errors parsing output from: " << kCrosSystemTool; } } base::FilePath machine_info_path; PathService::Get(chromeos::FILE_MACHINE_INFO, &machine_info_path); if (!base::SysInfo::IsRunningOnChromeOS() && !base::PathExists(machine_info_path)) { // Use time value to create an unique stub serial because clashes of the // same serial for the same domain invalidate earlier enrollments. Persist // to disk to keep it constant across restarts (required for re-enrollment // testing). std::string stub_contents = "\"serial_number\"=\"stub_" + base::Int64ToString(base::Time::Now().ToJavaTime()) + "\"\n"; int bytes_written = base::WriteFile(machine_info_path, stub_contents.c_str(), stub_contents.size()); // static_cast is fine because stub_contents is small. if (bytes_written < static_cast(stub_contents.size())) { LOG(ERROR) << "Error writing machine info stub: " << machine_info_path.value(); } } parser.GetNameValuePairsFromFile(machine_info_path, kMachineHardwareInfoEq, kMachineHardwareInfoDelim); parser.GetNameValuePairsFromFile(base::FilePath(kEchoCouponFile), kEchoCouponEq, kEchoCouponDelim); parser.GetNameValuePairsFromFile(base::FilePath(kVpdFile), kVpdEq, kVpdDelim); // Ensure that the hardware class key is present with the expected // key name, and if it couldn't be retrieved, that the value is "unknown". std::string hardware_class = machine_info_[kHardwareClassCrosSystemKey]; if (hardware_class.empty() || hardware_class == kCrosSystemUnknownValue) machine_info_[kHardwareClassKey] = kUnknownHardwareClass; else machine_info_[kHardwareClassKey] = hardware_class; if (load_oem_manifest) { // If kAppOemManifestFile switch is specified, load OEM Manifest file. base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); if (command_line->HasSwitch(switches::kAppOemManifestFile)) { LoadOemManifestFromFile( command_line->GetSwitchValuePath(switches::kAppOemManifestFile)); } else if (base::SysInfo::IsRunningOnChromeOS()) { LoadOemManifestFromFile(base::FilePath(kOemManifestFilePath)); } } LoadRegionsFile(base::FilePath(kCrosRegions)); // Set region const auto region_iter = machine_info_.find(kRegionKey); if (region_iter != machine_info_.end()) region_ = region_iter->second; else region_ = std::string(); base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); if (command_line->HasSwitch(chromeos::switches::kCrosRegion)) { region_ = command_line->GetSwitchValueASCII(chromeos::switches::kCrosRegion); machine_info_[kRegionKey] = region_; LOG(WARNING) << "CrOS region set to '" << region_ << "'"; } if (regional_data_.get() && !region_.empty() && !GetRegionDictionary()) LOG(ERROR) << "Bad reginal data: '" << region_ << "' << not found."; // Finished loading the statistics. on_statistics_loaded_.Signal(); VLOG(1) << "Finished loading statistics."; } void StatisticsProviderImpl::LoadRegionsFile(const base::FilePath& filename) { JSONFileValueDeserializer regions_file(filename); int regions_error_code = 0; std::string regions_error_message; regional_data_ = regions_file.Deserialize(®ions_error_code, ®ions_error_message); if (!regional_data_.get()) { if (base::SysInfo::IsRunningOnChromeOS()) LOG(ERROR) << "Failed to load regions file '" << filename.value() << "': error='" << regions_error_message << "'"; return; } const base::DictionaryValue* full_dict = nullptr; if (!regional_data_->GetAsDictionary(&full_dict)) { LOG(ERROR) << "Bad regions file '" << filename.value() << "': not a dictionary."; regional_data_.reset(); } } void StatisticsProviderImpl::LoadOemManifestFromFile( const base::FilePath& file) { // Called from LoadMachineStatistics. Check cancellation_flag_ again here. if (cancellation_flag_.IsSet()) return; KioskOemManifestParser::Manifest oem_manifest; if (!KioskOemManifestParser::Load(file, &oem_manifest)) { LOG(WARNING) << "Unable to load OEM Manifest file: " << file.value(); return; } machine_info_[kOemDeviceRequisitionKey] = oem_manifest.device_requisition; machine_flags_[kOemIsEnterpriseManagedKey] = oem_manifest.enterprise_managed; machine_flags_[kOemCanExitEnterpriseEnrollmentKey] = oem_manifest.can_exit_enrollment; machine_flags_[kOemKeyboardDrivenOobeKey] = oem_manifest.keyboard_driven_oobe; oem_manifest_loaded_ = true; VLOG(1) << "Loaded OEM Manifest statistics from " << file.value(); } StatisticsProviderImpl* StatisticsProviderImpl::GetInstance() { return base::Singleton< StatisticsProviderImpl, base::DefaultSingletonTraits>::get(); } bool StatisticsProvider::HasMachineStatistic(const std::string& name) { return GetMachineStatistic(name, nullptr); } static StatisticsProvider* g_test_statistics_provider = NULL; // static StatisticsProvider* StatisticsProvider::GetInstance() { if (g_test_statistics_provider) return g_test_statistics_provider; return StatisticsProviderImpl::GetInstance(); } // static void StatisticsProvider::SetTestProvider(StatisticsProvider* test_provider) { g_test_statistics_provider = test_provider; } } // namespace system } // namespace chromeos