// // Copyright (C) 2013 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. // // This provides access to timestamps with nanosecond resolution in // struct stat, See NOTES in stat(2) for details. #ifndef _DEFAULT_SOURCE #define _DEFAULT_SOURCE #endif #ifndef _BSD_SOURCE #define _BSD_SOURCE #endif #include "update_engine/cros/p2p_manager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "update_engine/common/subprocess.h" #include "update_engine/common/system_state.h" #include "update_engine/common/utils.h" #include "update_engine/update_manager/policy.h" #include "update_engine/update_manager/update_manager.h" using base::Bind; using base::Callback; using base::FilePath; using base::StringPrintf; using base::Time; using base::TimeDelta; using brillo::MessageLoop; using chromeos_update_manager::EvalStatus; using chromeos_update_manager::Policy; using chromeos_update_manager::UpdateManager; using std::pair; using std::string; using std::unique_ptr; using std::vector; namespace chromeos_update_engine { namespace { // The default p2p directory. const char kDefaultP2PDir[] = "/var/cache/p2p"; // The p2p xattr used for conveying the final size of a file - see the // p2p ddoc for details. const char kCrosP2PFileSizeXAttrName[] = "user.cros-p2p-filesize"; } // namespace // The default P2PManager::Configuration implementation. class ConfigurationImpl : public P2PManager::Configuration { public: ConfigurationImpl() {} FilePath GetP2PDir() override { return FilePath(kDefaultP2PDir); } vector GetInitctlArgs(bool is_start) override { vector args; args.push_back("initctl"); args.push_back(is_start ? "start" : "stop"); args.push_back("p2p"); return args; } vector GetP2PClientArgs(const string& file_id, size_t minimum_size) override { vector args; args.push_back("p2p-client"); args.push_back(string("--get-url=") + file_id); args.push_back(StringPrintf("--minimum-size=%" PRIuS, minimum_size)); return args; } private: DISALLOW_COPY_AND_ASSIGN(ConfigurationImpl); }; // The default P2PManager implementation. class P2PManagerImpl : public P2PManager { public: P2PManagerImpl(Configuration* configuration, UpdateManager* update_manager, const string& file_extension, const int num_files_to_keep, const TimeDelta& max_file_age); // P2PManager methods. void SetDevicePolicy(const policy::DevicePolicy* device_policy) override; bool IsP2PEnabled() override; bool EnsureP2PRunning() override; bool EnsureP2PNotRunning() override; bool PerformHousekeeping() override; void LookupUrlForFile(const string& file_id, size_t minimum_size, TimeDelta max_time_to_wait, LookupCallback callback) override; bool FileShare(const string& file_id, size_t expected_size) override; FilePath FileGetPath(const string& file_id) override; ssize_t FileGetSize(const string& file_id) override; ssize_t FileGetExpectedSize(const string& file_id) override; bool FileGetVisible(const string& file_id, bool* out_result) override; bool FileMakeVisible(const string& file_id) override; int CountSharedFiles() override; private: // Enumeration for specifying visibility. enum Visibility { kVisible, kNonVisible }; // Returns "." + |file_extension_| + ".p2p" if |visibility| is // |kVisible|. Returns the same concatenated with ".tmp" otherwise. string GetExt(Visibility visibility); // Gets the on-disk path for |file_id| depending on if the file // is visible or not. FilePath GetPath(const string& file_id, Visibility visibility); // Utility function used by EnsureP2PRunning() and EnsureP2PNotRunning(). bool EnsureP2P(bool should_be_running); // Utility function to delete a file given by |path| and log the // path as well as |reason|. Returns false on failure. bool DeleteP2PFile(const FilePath& path, const string& reason); // Schedules an async request for tracking changes in P2P enabled status. void ScheduleEnabledStatusChange(); // An async callback used by the above. void OnEnabledStatusChange(EvalStatus status, const bool& result); // The device policy being used or null if no policy is being used. const policy::DevicePolicy* device_policy_ = nullptr; // Configuration object. unique_ptr configuration_; // A pointer to the global Update Manager. UpdateManager* update_manager_; // A short string unique to the application (for example "cros_au") // used to mark a file as being owned by a particular application. const string file_extension_; // If non-zero, this number denotes how many files in /var/cache/p2p // owned by the application (cf. |file_extension_|) to keep after // performing housekeeping. const int num_files_to_keep_; // If non-zero, files older than this will not be kept after // performing housekeeping. const TimeDelta max_file_age_; // The string ".p2p". static const char kP2PExtension[]; // The string ".tmp". static const char kTmpExtension[]; // Whether P2P service may be running; initially, we assume it may be. bool may_be_running_ = true; // The current known enabled status of the P2P feature (initialized lazily), // and whether an async status check has been scheduled. bool is_enabled_; bool waiting_for_enabled_status_change_ = false; DISALLOW_COPY_AND_ASSIGN(P2PManagerImpl); }; const char P2PManagerImpl::kP2PExtension[] = ".p2p"; const char P2PManagerImpl::kTmpExtension[] = ".tmp"; P2PManagerImpl::P2PManagerImpl(Configuration* configuration, UpdateManager* update_manager, const string& file_extension, const int num_files_to_keep, const TimeDelta& max_file_age) : update_manager_(update_manager), file_extension_(file_extension), num_files_to_keep_(num_files_to_keep), max_file_age_(max_file_age) { configuration_.reset(configuration != nullptr ? configuration : new ConfigurationImpl()); } void P2PManagerImpl::SetDevicePolicy( const policy::DevicePolicy* device_policy) { device_policy_ = device_policy; } bool P2PManagerImpl::IsP2PEnabled() { if (!waiting_for_enabled_status_change_) { // Get and store an initial value. if (update_manager_->PolicyRequest(&Policy::P2PEnabled, &is_enabled_) == EvalStatus::kFailed) { is_enabled_ = false; LOG(ERROR) << "Querying P2P enabled status failed, disabling."; } // Track future changes (async). ScheduleEnabledStatusChange(); } return is_enabled_; } bool P2PManagerImpl::EnsureP2P(bool should_be_running) { int return_code = 0; string stderr; may_be_running_ = true; // Unless successful, we must be conservative. vector args = configuration_->GetInitctlArgs(should_be_running); if (!Subprocess::SynchronousExec(args, &return_code, nullptr, &stderr)) { LOG(ERROR) << "Error spawning " << utils::StringVectorToString(args); return false; } // If initctl(8) does not exit normally (exit status other than zero), ensure // that the error message is not benign by scanning stderr; this is a // necessity because initctl does not offer actions such as "start if not // running" or "stop if running". // TODO(zeuthen,chromium:277051): Avoid doing this. if (return_code != 0) { const char* expected_error_message = should_be_running ? "initctl: Job is already running: p2p\n" : "initctl: Unknown instance \n"; if (stderr != expected_error_message) return false; } may_be_running_ = should_be_running; // Successful after all. return true; } bool P2PManagerImpl::EnsureP2PRunning() { return EnsureP2P(true); } bool P2PManagerImpl::EnsureP2PNotRunning() { return EnsureP2P(false); } // Returns True if the timestamp in the first pair is greater than the // timestamp in the latter. If used with std::sort() this will yield a // sequence of elements where newer (high timestamps) elements precede // older ones (low timestamps). static bool MatchCompareFunc(const pair& a, const pair& b) { return a.second > b.second; } string P2PManagerImpl::GetExt(Visibility visibility) { string ext = string(".") + file_extension_ + kP2PExtension; switch (visibility) { case kVisible: break; case kNonVisible: ext += kTmpExtension; break; // Don't add a default case to let the compiler warn about newly // added enum values. } return ext; } FilePath P2PManagerImpl::GetPath(const string& file_id, Visibility visibility) { return configuration_->GetP2PDir().Append(file_id + GetExt(visibility)); } bool P2PManagerImpl::DeleteP2PFile(const FilePath& path, const string& reason) { LOG(INFO) << "Deleting p2p file " << path.value() << " (reason: " << reason << ")"; if (unlink(path.value().c_str()) != 0) { PLOG(ERROR) << "Error deleting p2p file " << path.value(); return false; } return true; } bool P2PManagerImpl::PerformHousekeeping() { // Open p2p dir. FilePath p2p_dir = configuration_->GetP2PDir(); const string ext_visible = GetExt(kVisible); const string ext_non_visible = GetExt(kNonVisible); bool deletion_failed = false; vector> matches; base::FileEnumerator dir(p2p_dir, false, base::FileEnumerator::FILES); // Go through all files and collect their mtime. for (FilePath name = dir.Next(); !name.empty(); name = dir.Next()) { if (!(base::EndsWith( name.value(), ext_visible, base::CompareCase::SENSITIVE) || base::EndsWith( name.value(), ext_non_visible, base::CompareCase::SENSITIVE))) { continue; } Time time = dir.GetInfo().GetLastModifiedTime(); // If instructed to keep only files younger than a given age // (|max_file_age_| != 0), delete files satisfying this criteria // right now. Otherwise add it to a list we'll consider for later. if (max_file_age_ != TimeDelta() && SystemState::Get()->clock()->GetWallclockTime() - time > max_file_age_) { if (!DeleteP2PFile(name, "file too old")) deletion_failed = true; } else { matches.push_back(std::make_pair(name, time)); } } // If instructed to only keep N files (|max_files_to_keep_ != 0), // sort list of matches, newest (biggest time) to oldest (lowest // time). Then delete starting at element |num_files_to_keep_|. if (num_files_to_keep_ > 0) { std::sort(matches.begin(), matches.end(), MatchCompareFunc); vector>::const_iterator i; for (i = matches.begin() + num_files_to_keep_; i < matches.end(); ++i) { if (!DeleteP2PFile(i->first, "too many files")) deletion_failed = true; } } return !deletion_failed; } // Helper class for implementing LookupUrlForFile(). class LookupData { public: explicit LookupData(P2PManager::LookupCallback callback) : callback_(callback) {} ~LookupData() { if (timeout_task_ != MessageLoop::kTaskIdNull) MessageLoop::current()->CancelTask(timeout_task_); if (child_pid_) Subprocess::Get().KillExec(child_pid_); } void InitiateLookup(const vector& cmd, TimeDelta timeout) { // NOTE: if we fail early (i.e. in this method), we need to schedule // an idle to report the error. This is because we guarantee that // the callback is always called from the message loop (this // guarantee is useful for testing). // We expect to run just "p2p-client" and find it in the path. child_pid_ = Subprocess::Get().ExecFlags( cmd, Subprocess::kSearchPath, {}, Bind(&LookupData::OnLookupDone, base::Unretained(this))); if (!child_pid_) { LOG(ERROR) << "Error spawning " << utils::StringVectorToString(cmd); ReportErrorAndDeleteInIdle(); return; } if (timeout > TimeDelta()) { timeout_task_ = MessageLoop::current()->PostDelayedTask( FROM_HERE, Bind(&LookupData::OnTimeout, base::Unretained(this)), timeout); } } private: void ReportErrorAndDeleteInIdle() { MessageLoop::current()->PostTask( FROM_HERE, Bind(&LookupData::OnIdleForReportErrorAndDelete, base::Unretained(this))); } void OnIdleForReportErrorAndDelete() { ReportError(); delete this; } void IssueCallback(const string& url) { if (!callback_.is_null()) callback_.Run(url); } void ReportError() { if (reported_) return; IssueCallback(""); reported_ = true; } void ReportSuccess(const string& output) { if (reported_) return; string url = output; size_t newline_pos = url.find('\n'); if (newline_pos != string::npos) url.resize(newline_pos); // Since p2p-client(1) is constructing this URL itself strictly // speaking there's no need to validate it... but, anyway, can't // hurt. if (url.compare(0, 7, "http://") == 0) { IssueCallback(url); } else { LOG(ERROR) << "p2p URL '" << url << "' does not look right. Ignoring."; ReportError(); } reported_ = true; } void OnLookupDone(int return_code, const string& output) { child_pid_ = 0; if (return_code != 0) { LOG(INFO) << "Child exited with non-zero exit code " << return_code; ReportError(); } else { ReportSuccess(output); } delete this; } void OnTimeout() { timeout_task_ = MessageLoop::kTaskIdNull; ReportError(); delete this; } P2PManager::LookupCallback callback_; // The Subprocess tag of the running process. A value of 0 means that the // process is not running. pid_t child_pid_{0}; // The timeout task_id we are waiting on, if any. MessageLoop::TaskId timeout_task_{MessageLoop::kTaskIdNull}; bool reported_{false}; }; void P2PManagerImpl::LookupUrlForFile(const string& file_id, size_t minimum_size, TimeDelta max_time_to_wait, LookupCallback callback) { LookupData* lookup_data = new LookupData(callback); string file_id_with_ext = file_id + "." + file_extension_; vector args = configuration_->GetP2PClientArgs(file_id_with_ext, minimum_size); lookup_data->InitiateLookup(args, max_time_to_wait); } bool P2PManagerImpl::FileShare(const string& file_id, size_t expected_size) { // Check if file already exist. FilePath path = FileGetPath(file_id); if (!path.empty()) { // File exists - double check its expected size though. ssize_t file_expected_size = FileGetExpectedSize(file_id); if (file_expected_size == -1 || static_cast(file_expected_size) != expected_size) { LOG(ERROR) << "Existing p2p file " << path.value() << " with expected_size=" << file_expected_size << " does not match the passed in" << " expected_size=" << expected_size; return false; } return true; } // Before creating the file, bail if statvfs(3) indicates that at // least twice the size is not available in P2P_DIR. struct statvfs statvfsbuf; FilePath p2p_dir = configuration_->GetP2PDir(); if (statvfs(p2p_dir.value().c_str(), &statvfsbuf) != 0) { PLOG(ERROR) << "Error calling statvfs() for dir " << p2p_dir.value(); return false; } size_t free_bytes = static_cast(statvfsbuf.f_bsize) * statvfsbuf.f_bavail; if (free_bytes < 2 * expected_size) { // This can easily happen and is worth reporting. LOG(INFO) << "Refusing to allocate p2p file of " << expected_size << " bytes since the directory " << p2p_dir.value() << " only has " << free_bytes << " bytes available and this is less than twice the" << " requested size."; return false; } // Okie-dokey looks like enough space is available - create the file. path = GetPath(file_id, kNonVisible); int fd = open(path.value().c_str(), O_CREAT | O_RDWR, 0644); if (fd == -1) { PLOG(ERROR) << "Error creating file with path " << path.value(); return false; } ScopedFdCloser fd_closer(&fd); // If the final size is known, allocate the file (e.g. reserve disk // space) and set the user.cros-p2p-filesize xattr. if (expected_size != 0) { if (fallocate(fd, FALLOC_FL_KEEP_SIZE, // Keep file size as 0. 0, expected_size) != 0) { if (errno == ENOSYS || errno == EOPNOTSUPP) { // If the filesystem doesn't support the fallocate, keep // going. This is helpful when running unit tests on build // machines with ancient filesystems and/or OSes. PLOG(WARNING) << "Ignoring fallocate(2) failure"; } else { // ENOSPC can happen (funky race though, cf. the statvfs() check // above), handle it gracefully, e.g. use logging level INFO. PLOG(INFO) << "Error allocating " << expected_size << " bytes for file " << path.value(); if (unlink(path.value().c_str()) != 0) { PLOG(ERROR) << "Error deleting file with path " << path.value(); } return false; } } string decimal_size = std::to_string(expected_size); if (fsetxattr(fd, kCrosP2PFileSizeXAttrName, decimal_size.c_str(), decimal_size.size(), 0) != 0) { PLOG(ERROR) << "Error setting xattr " << path.value(); return false; } } return true; } FilePath P2PManagerImpl::FileGetPath(const string& file_id) { struct stat statbuf; FilePath path; path = GetPath(file_id, kVisible); if (stat(path.value().c_str(), &statbuf) == 0) { return path; } path = GetPath(file_id, kNonVisible); if (stat(path.value().c_str(), &statbuf) == 0) { return path; } path.clear(); return path; } bool P2PManagerImpl::FileGetVisible(const string& file_id, bool* out_result) { FilePath path = FileGetPath(file_id); if (path.empty()) { LOG(ERROR) << "No file for id " << file_id; return false; } if (out_result != nullptr) *out_result = path.MatchesExtension(kP2PExtension); return true; } bool P2PManagerImpl::FileMakeVisible(const string& file_id) { FilePath path = FileGetPath(file_id); if (path.empty()) { LOG(ERROR) << "No file for id " << file_id; return false; } // Already visible? if (path.MatchesExtension(kP2PExtension)) return true; LOG_ASSERT(path.MatchesExtension(kTmpExtension)); FilePath new_path = path.RemoveExtension(); LOG_ASSERT(new_path.MatchesExtension(kP2PExtension)); if (rename(path.value().c_str(), new_path.value().c_str()) != 0) { PLOG(ERROR) << "Error renaming " << path.value() << " to " << new_path.value(); return false; } return true; } ssize_t P2PManagerImpl::FileGetSize(const string& file_id) { FilePath path = FileGetPath(file_id); if (path.empty()) return -1; return utils::FileSize(path.value()); } ssize_t P2PManagerImpl::FileGetExpectedSize(const string& file_id) { FilePath path = FileGetPath(file_id); if (path.empty()) return -1; char ea_value[64] = {0}; ssize_t ea_size; ea_size = getxattr(path.value().c_str(), kCrosP2PFileSizeXAttrName, &ea_value, sizeof(ea_value) - 1); if (ea_size == -1) { PLOG(ERROR) << "Error calling getxattr() on file " << path.value(); return -1; } char* endp = nullptr; long long int val = strtoll(ea_value, &endp, 0); // NOLINT(runtime/int) if (*endp != '\0') { LOG(ERROR) << "Error parsing the value '" << ea_value << "' of the xattr " << kCrosP2PFileSizeXAttrName << " as an integer"; return -1; } return val; } int P2PManagerImpl::CountSharedFiles() { int num_files = 0; FilePath p2p_dir = configuration_->GetP2PDir(); const string ext_visible = GetExt(kVisible); const string ext_non_visible = GetExt(kNonVisible); base::FileEnumerator dir(p2p_dir, false, base::FileEnumerator::FILES); for (FilePath name = dir.Next(); !name.empty(); name = dir.Next()) { if (base::EndsWith( name.value(), ext_visible, base::CompareCase::SENSITIVE) || base::EndsWith( name.value(), ext_non_visible, base::CompareCase::SENSITIVE)) { num_files += 1; } } return num_files; } void P2PManagerImpl::ScheduleEnabledStatusChange() { if (waiting_for_enabled_status_change_) return; Callback callback = Bind(&P2PManagerImpl::OnEnabledStatusChange, base::Unretained(this)); update_manager_->AsyncPolicyRequest( callback, &Policy::P2PEnabledChanged, is_enabled_); waiting_for_enabled_status_change_ = true; } void P2PManagerImpl::OnEnabledStatusChange(EvalStatus status, const bool& result) { waiting_for_enabled_status_change_ = false; if (status == EvalStatus::kSucceeded) { if (result == is_enabled_) { LOG(WARNING) << "P2P enabled status did not change, which means that it " "is permanent; not scheduling further checks."; waiting_for_enabled_status_change_ = true; return; } is_enabled_ = result; // If P2P is running but shouldn't be, make sure it isn't. if (may_be_running_ && !is_enabled_ && !EnsureP2PNotRunning()) { LOG(WARNING) << "Failed to stop P2P service."; } } else { LOG(WARNING) << "P2P enabled tracking failed (possibly timed out); retrying."; } ScheduleEnabledStatusChange(); } P2PManager* P2PManager::Construct(Configuration* configuration, UpdateManager* update_manager, const string& file_extension, const int num_files_to_keep, const TimeDelta& max_file_age) { return new P2PManagerImpl(configuration, update_manager, file_extension, num_files_to_keep, max_file_age); } } // namespace chromeos_update_engine