p2p: Add P2PManager class
This class makes it simple to use p2p both as a client and a server.
For now, this feature is hidden behind a flag. The intent is that this
flag can be toggled with the crosh command - for now, only the reading
of the flag is implemented - see chromium:260441 for the remaining
work.
BUG=chromium:260426,chromium:260441
TEST=New unit tests + unit tests pass
Change-Id: If1879f4535c8e7e3af7c6d378e6df4054264b794
Reviewed-on: https://chromium-review.googlesource.com/64824
Reviewed-by: David Zeuthen <zeuthen@chromium.org>
Commit-Queue: David Zeuthen <zeuthen@chromium.org>
Tested-by: David Zeuthen <zeuthen@chromium.org>
diff --git a/SConstruct b/SConstruct
index dc4dd6c..215d4b1 100644
--- a/SConstruct
+++ b/SConstruct
@@ -275,6 +275,7 @@
omaha_request_action.cc
omaha_request_params.cc
omaha_response_handler_action.cc
+ p2p_manager.cc
payload_signer.cc
payload_state.cc
postinstall_runner_action.cc
@@ -324,6 +325,7 @@
omaha_request_action_unittest.cc
omaha_request_params_unittest.cc
omaha_response_handler_action_unittest.cc
+ p2p_manager_unittest.cc
payload_signer_unittest.cc
payload_state_unittest.cc
postinstall_runner_action_unittest.cc
diff --git a/constants.cc b/constants.cc
index 874a004..31454cc 100644
--- a/constants.cc
+++ b/constants.cc
@@ -38,6 +38,7 @@
const char kPrefsManifestMetadataSize[] = "manifest-metadata-size";
const char kPrefsNumReboots[] = "num-reboots";
const char kPrefsNumResponsesSeen[] = "num-responses-seen";
+const char kPrefsP2PEnabled[] = "p2p-enabled";
const char kPrefsPayloadAttemptNumber[] = "payload-attempt-number";
const char kPrefsPreviousVersion[] = "previous-version";
const char kPrefsResumedUpdateFailures[] = "resumed-update-failures";
diff --git a/constants.h b/constants.h
index 7b3dca9..66c12e7 100644
--- a/constants.h
+++ b/constants.h
@@ -41,6 +41,7 @@
extern const char kPrefsManifestMetadataSize[];
extern const char kPrefsNumReboots[];
extern const char kPrefsNumResponsesSeen[];
+extern const char kPrefsP2PEnabled[];
extern const char kPrefsPayloadAttemptNumber[];
extern const char kPrefsPreviousVersion[];
extern const char kPrefsResumedUpdateFailures[];
diff --git a/fake_p2p_manager_configuration.h b/fake_p2p_manager_configuration.h
new file mode 100644
index 0000000..9cc6481
--- /dev/null
+++ b/fake_p2p_manager_configuration.h
@@ -0,0 +1,120 @@
+// Copyright (c) 2013 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 CHROMEOS_PLATFORM_UPDATE_ENGINE_FAKE_P2P_MANAGER_CONFIGURATION_H__
+#define CHROMEOS_PLATFORM_UPDATE_ENGINE_FAKE_P2P_MANAGER_CONFIGURATION_H__
+
+#include "update_engine/p2p_manager.h"
+#include "update_engine/utils.h"
+
+#include <glib.h>
+
+#include <base/logging.h>
+#include <base/stringprintf.h>
+
+namespace chromeos_update_engine {
+
+// Configuration for P2PManager for use in unit tests. Instead of
+// /var/cache/p2p, a temporary directory is used.
+class FakeP2PManagerConfiguration : public P2PManager::Configuration {
+public:
+ FakeP2PManagerConfiguration()
+ : p2p_client_cmdline_format_("p2p-client --get-url=%s --minimum-size=%zu") {
+ EXPECT_TRUE(utils::MakeTempDirectory("/tmp/p2p-tc.XXXXXX", &p2p_dir_));
+ SetInitctlStartCommandLine("initctl start p2p");
+ SetInitctlStopCommandLine("initctl stop p2p");
+ }
+
+ ~FakeP2PManagerConfiguration() {
+ if (p2p_dir_.size() > 0 && !utils::RecursiveUnlinkDir(p2p_dir_)) {
+ PLOG(ERROR) << "Unable to unlink files and directory in " << p2p_dir_;
+ }
+ }
+
+ // P2PManager::Configuration override
+ virtual FilePath GetP2PDir() {
+ return FilePath(p2p_dir_);
+ };
+
+ // P2PManager::Configuration override
+ virtual std::vector<std::string> GetInitctlArgs(bool is_start) {
+ return is_start ? initctl_start_args_ : initctl_stop_args_;
+ }
+
+ // P2PManager::Configuration override
+ virtual std::vector<std::string> GetP2PClientArgs(const std::string &file_id,
+ size_t minimum_size) {
+ std::string formatted_command_line =
+ base::StringPrintf(p2p_client_cmdline_format_.c_str(),
+ file_id.c_str(), minimum_size);
+ return ParseCommandLine(formatted_command_line);
+ }
+
+ // Use |command_line| instead of "initctl start p2p" when attempting
+ // to start the p2p service.
+ void SetInitctlStartCommandLine(const std::string &command_line) {
+ initctl_start_args_ = ParseCommandLine(command_line);
+ }
+
+ // Use |command_line| instead of "initctl stop p2p" when attempting
+ // to stop the p2p service.
+ void SetInitctlStopCommandLine(const std::string &command_line) {
+ initctl_stop_args_ = ParseCommandLine(command_line);
+ }
+
+ // Use |command_line_format| instead of "p2p-client --get-url=%s
+ // --minimum-size=%zu" when attempting to look up a file using
+ // p2p-client(1).
+ //
+ // The passed |command_line_format| argument should be a
+ // printf()-style format string taking two arguments, the first
+ // being the a C string for the p2p file id (e.g. %s) and the second
+ // being a size_t with the minimum_size.
+ void SetP2PClientCommandLine(const std::string &command_line_format) {
+ p2p_client_cmdline_format_ = command_line_format;
+ }
+
+private:
+ // Helper for parsing and splitting |command_line| into an argument
+ // vector in much the same way a shell would except for not
+ // supporting wildcards, globs, operators etc. See
+ // g_shell_parse_argv() for more details. If an error occurs, the
+ // empty vector is returned.
+ std::vector<std::string> ParseCommandLine(const std::string &command_line) {
+ gint argc;
+ gchar **argv;
+ std::vector<std::string> ret;
+
+ if (!g_shell_parse_argv(command_line.c_str(),
+ &argc,
+ &argv,
+ NULL)) {
+ LOG(ERROR) << "Error splitting '" << command_line << "'";
+ return ret;
+ }
+ for (int n = 0; n < argc; n++)
+ ret.push_back(argv[n]);
+ g_strfreev(argv);
+ return ret;
+ }
+
+ // The temporary directory used for p2p.
+ std::string p2p_dir_;
+
+ // Argument vector for starting p2p.
+ std::vector<std::string> initctl_start_args_;
+
+ // Argument vector for stopping p2p.
+ std::vector<std::string> initctl_stop_args_;
+
+ // A printf()-style format string for generating the p2p-client format.
+ // See the SetP2PClientCommandLine() for details.
+ std::string p2p_client_cmdline_format_;
+
+ DISALLOW_COPY_AND_ASSIGN(FakeP2PManagerConfiguration);
+};
+
+} // namespace chromeos_update_engine
+
+#endif // CHROMEOS_PLATFORM_UPDATE_ENGINE_FAKE_P2P_MANAGER_CONFIGURATION_H__
diff --git a/p2p_manager.cc b/p2p_manager.cc
new file mode 100644
index 0000000..e56f739
--- /dev/null
+++ b/p2p_manager.cc
@@ -0,0 +1,743 @@
+// Copyright (c) 2013 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.
+
+// This provides access to timestamps with nano-second resolution in
+// struct stat, See NOTES in stat(2) for details.
+#ifndef _BSD_SOURCE
+#define _BSD_SOURCE
+#endif
+
+#include "update_engine/p2p_manager.h"
+
+#include <attr/xattr.h>
+#include <dirent.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <glib.h>
+#include <linux/falloc.h>
+#include <signal.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/statvfs.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <unistd.h>
+
+#include <map>
+#include <utility>
+#include <vector>
+
+#include <base/file_path.h>
+#include <base/logging.h>
+#include <base/stringprintf.h>
+
+#include "update_engine/utils.h"
+
+using base::FilePath;
+using base::StringPrintf;
+using base::Time;
+using base::TimeDelta;
+using std::map;
+using std::pair;
+using std::string;
+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() {}
+
+ virtual ~ConfigurationImpl() {}
+
+ virtual FilePath GetP2PDir() {
+ return FilePath(kDefaultP2PDir);
+ }
+
+ virtual vector<string> GetInitctlArgs(bool is_start) {
+ vector<string> args;
+ args.push_back("initctl");
+ args.push_back(is_start ? "start" : "stop");
+ args.push_back("p2p");
+ return args;
+ }
+
+ virtual vector<string> GetP2PClientArgs(const string &file_id,
+ size_t minimum_size) {
+ vector<string> args;
+ args.push_back("p2p-client");
+ args.push_back(string("--get-url=") + file_id);
+ args.push_back(StringPrintf("--minimum-size=%zu", minimum_size));
+ return args;
+ }
+
+private:
+ DISALLOW_COPY_AND_ASSIGN(ConfigurationImpl);
+};
+
+// The default P2PManager implementation.
+class P2PManagerImpl : public P2PManager {
+public:
+ P2PManagerImpl(Configuration *configuration,
+ PrefsInterface *prefs,
+ const string& file_extension,
+ const int num_files_to_keep);
+
+ // P2PManager methods.
+ virtual void SetConfiguration(Configuration *configuration);
+ virtual bool IsP2PEnabled();
+ virtual bool EnsureP2PRunning();
+ virtual bool EnsureP2PNotRunning();
+ virtual bool PerformHousekeeping();
+ virtual void LookupUrlForFile(const string& file_id,
+ size_t minimum_size,
+ TimeDelta max_time_to_wait,
+ LookupCallback callback);
+ virtual bool FileShare(const string& file_id,
+ size_t expected_size);
+ virtual FilePath FileGetPath(const string& file_id);
+ virtual ssize_t FileGetSize(const string& file_id);
+ virtual ssize_t FileGetExpectedSize(const string& file_id);
+ virtual bool FileGetVisible(const string& file_id,
+ bool *out_result);
+ virtual bool FileMakeVisible(const string& file_id);
+ virtual int CountSharedFiles();
+
+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);
+
+ // Configuration object.
+ scoped_ptr<Configuration> configuration_;
+
+ // Object for persisted state.
+ PrefsInterface* prefs_;
+
+ // 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_;
+
+ // The string ".p2p".
+ static const char kP2PExtension[];
+
+ // The string ".tmp".
+ static const char kTmpExtension[];
+
+ DISALLOW_COPY_AND_ASSIGN(P2PManagerImpl);
+};
+
+const char P2PManagerImpl::kP2PExtension[] = ".p2p";
+
+const char P2PManagerImpl::kTmpExtension[] = ".tmp";
+
+P2PManagerImpl::P2PManagerImpl(Configuration *configuration,
+ PrefsInterface *prefs,
+ const string& file_extension,
+ const int num_files_to_keep)
+ : prefs_(prefs),
+ file_extension_(file_extension),
+ num_files_to_keep_(num_files_to_keep) {
+ configuration_.reset(configuration != NULL ? configuration :
+ new ConfigurationImpl());
+}
+
+void P2PManagerImpl::SetConfiguration(Configuration *configuration) {
+ configuration_.reset(configuration);
+}
+
+bool P2PManagerImpl::IsP2PEnabled() {
+ // TODO(deymo,zeuthen)(chromium:260441): See the bug for the bigger
+ // picture. For now we just read the state variable so in order for
+ // p2p to work the developer will have to manually create the prefs
+ // file. Once the fix for bug 260441 has been merged, this can be
+ // toggled using the crosh command, as intended.
+ bool enabled = false;
+ if (prefs_ == NULL) {
+ LOG(INFO) << "No prefs object.";
+ } else if (!prefs_->Exists(kPrefsP2PEnabled)) {
+ LOG(INFO) << "The " << kPrefsP2PEnabled << " pref does not exist.";
+ } else if (!prefs_->GetBoolean(kPrefsP2PEnabled, &enabled)) {
+ LOG(ERROR) << "Error getting " << kPrefsP2PEnabled << " pref.";
+ }
+ LOG(INFO) << "Returning value " << enabled << " for whether p2p is enabled.";
+ return enabled;
+}
+
+bool P2PManagerImpl::EnsureP2P(bool should_be_running) {
+ gchar *standard_error = NULL;
+ GError *error = NULL;
+ gint exit_status = 0;
+
+ vector<string> args = configuration_->GetInitctlArgs(should_be_running);
+ scoped_ptr<gchar*, GLibStrvFreeDeleter> argv(
+ utils::StringVectorToGStrv(args));
+ if (!g_spawn_sync(NULL, // working_directory
+ argv.get(),
+ NULL, // envp
+ static_cast<GSpawnFlags>(G_SPAWN_SEARCH_PATH),
+ NULL, NULL, // child_setup, user_data
+ NULL, // standard_output
+ &standard_error,
+ &exit_status,
+ &error)) {
+ LOG(ERROR) << "Error spawning " << utils::StringVectorToString(args)
+ << ": " << utils::GetAndFreeGError(&error);
+ return false;
+ }
+ scoped_ptr<gchar, GLibFreeDeleter> standard_error_deleter(standard_error);
+
+ if (!WIFEXITED(exit_status)) {
+ LOG(ERROR) << "Error spawning '" << utils::StringVectorToString(args)
+ << "': WIFEXITED is false";
+ return false;
+ }
+
+ // If initctl(8) exits normally with exit status 0 ("success"), it
+ // meant that it did what we requested.
+ if (WEXITSTATUS(exit_status) == 0) {
+ return true;
+ }
+
+ // Otherwise, screenscape stderr from initctl(8). Ugh, yes, this is
+ // ugly but since the program lacks verbs/actions such as
+ //
+ // ensure-started (or start-or-return-success-if-already-started)
+ // ensure-stopped (or stop-or-return-success-if-not-running)
+ //
+ // this is what we have to do.
+ //
+ // TODO(zeuthen,chromium:277051): Avoid doing this.
+ const gchar *expected_error_message = should_be_running ?
+ "initctl: Job is already running: p2p\n" :
+ "initctl: Unknown instance \n";
+ if (g_strcmp0(standard_error, expected_error_message) == 0) {
+ return true;
+ }
+
+ return false;
+}
+
+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<FilePath, Time>& a,
+ const pair<FilePath, Time>& 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::PerformHousekeeping() {
+ GDir* dir = NULL;
+ GError* error = NULL;
+ const char* name = NULL;
+ vector<pair<FilePath, Time> > matches;
+
+ // Go through all files in the p2p dir and pick the ones that match
+ // and get their ctime.
+ FilePath p2p_dir = configuration_->GetP2PDir();
+ dir = g_dir_open(p2p_dir.value().c_str(), 0, &error);
+ if (dir == NULL) {
+ LOG(ERROR) << "Error opening directory " << p2p_dir.value() << ": "
+ << utils::GetAndFreeGError(&error);
+ return false;
+ }
+
+ if (num_files_to_keep_ == 0)
+ return true;
+
+ string ext_visible = GetExt(kVisible);
+ string ext_non_visible = GetExt(kNonVisible);
+ while ((name = g_dir_read_name(dir)) != NULL) {
+ if (!(g_str_has_suffix(name, ext_visible.c_str()) ||
+ g_str_has_suffix(name, ext_non_visible.c_str())))
+ continue;
+
+ struct stat statbuf;
+ FilePath file = p2p_dir.Append(name);
+ if (stat(file.value().c_str(), &statbuf) != 0) {
+ PLOG(ERROR) << "Error getting file status for " << file.value();
+ continue;
+ }
+
+ Time time = utils::TimeFromStructTimespec(&statbuf.st_ctim);
+ matches.push_back(std::make_pair(file, time));
+ }
+ g_dir_close(dir);
+
+ // Sort list of matches, newest (biggest time) to oldest (lowest time).
+ std::sort(matches.begin(), matches.end(), MatchCompareFunc);
+
+ // Delete starting at element num_files_to_keep_.
+ vector<pair<FilePath, Time> >::const_iterator i;
+ for (i = matches.begin() + num_files_to_keep_; i < matches.end(); ++i) {
+ const FilePath& file = i->first;
+ LOG(INFO) << "Deleting p2p file " << file.value();
+ if (unlink(file.value().c_str()) != 0) {
+ PLOG(ERROR) << "Error deleting p2p file " << file.value();
+ return false;
+ }
+ }
+
+ return true;
+}
+
+// Helper class for implementing LookupUrlForFile().
+class LookupData {
+public:
+ LookupData(P2PManager::LookupCallback callback)
+ : callback_(callback),
+ pid_(0),
+ stdout_fd_(-1),
+ stdout_channel_source_id_(0),
+ child_watch_source_id_(0),
+ timeout_source_id_(0),
+ reported_(false) {}
+
+ ~LookupData() {
+ if (child_watch_source_id_ != 0)
+ g_source_remove(child_watch_source_id_);
+ if (stdout_channel_source_id_ != 0)
+ g_source_remove(stdout_channel_source_id_);
+ if (timeout_source_id_ != 0)
+ g_source_remove(timeout_source_id_);
+ if (stdout_fd_ != -1)
+ close(stdout_fd_);
+ if (pid_ != 0)
+ kill(pid_, SIGTERM);
+ }
+
+ void InitiateLookup(gchar **argv, 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 from the GLib mainloop (this
+ // guarantee is useful for testing).
+
+ GError *error = NULL;
+ if (!g_spawn_async_with_pipes(NULL, // working_directory
+ argv,
+ NULL, // envp
+ static_cast<GSpawnFlags>(G_SPAWN_SEARCH_PATH |
+ G_SPAWN_DO_NOT_REAP_CHILD),
+ NULL, // child_setup
+ this,
+ &pid_,
+ NULL, // standard_input
+ &stdout_fd_,
+ NULL, // standard_error
+ &error)) {
+ LOG(ERROR) << "Error spawning p2p-client: "
+ << utils::GetAndFreeGError(&error);
+ ReportErrorAndDeleteInIdle();
+ return;
+ }
+
+ GIOChannel* io_channel = g_io_channel_unix_new(stdout_fd_);
+ stdout_channel_source_id_ = g_io_add_watch(
+ io_channel,
+ static_cast<GIOCondition>(G_IO_IN | G_IO_PRI | G_IO_ERR | G_IO_HUP),
+ OnIOChannelActivity, this);
+ CHECK(stdout_channel_source_id_ != 0);
+ g_io_channel_unref(io_channel);
+
+ child_watch_source_id_ = g_child_watch_add(pid_, OnChildWatchActivity,
+ this);
+ CHECK(child_watch_source_id_ != 0);
+
+ if (timeout.ToInternalValue() > 0) {
+ timeout_source_id_ = g_timeout_add(timeout.InMilliseconds(),
+ OnTimeout, this);
+ CHECK(timeout_source_id_ != 0);
+ }
+ }
+
+private:
+ void ReportErrorAndDeleteInIdle() {
+ g_idle_add(static_cast<GSourceFunc>(OnIdleForReportErrorAndDelete), this);
+ }
+
+ static gboolean OnIdleForReportErrorAndDelete(gpointer user_data) {
+ LookupData *lookup_data = reinterpret_cast<LookupData*>(user_data);
+ lookup_data->ReportError();
+ delete lookup_data;
+ return FALSE; // Remove source.
+ }
+
+ void IssueCallback(const string& url) {
+ if (!callback_.is_null())
+ callback_.Run(url);
+ }
+
+ void ReportError() {
+ if (reported_)
+ return;
+ IssueCallback("");
+ reported_ = true;
+ }
+
+ void ReportSuccess() {
+ if (reported_)
+ return;
+
+ string url = stdout_;
+ 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;
+ }
+
+ static gboolean OnIOChannelActivity(GIOChannel *source,
+ GIOCondition condition,
+ gpointer user_data) {
+ LookupData *lookup_data = reinterpret_cast<LookupData*>(user_data);
+ gchar* str = NULL;
+ GError* error = NULL;
+ GIOStatus status = g_io_channel_read_line(source,
+ &str,
+ NULL, // len
+ NULL, // line_terminator
+ &error);
+ if (status != G_IO_STATUS_NORMAL) {
+ // Ignore EOF since we usually get that before SIGCHLD and we
+ // need to examine exit status there.
+ if (status != G_IO_STATUS_EOF) {
+ LOG(ERROR) << "Error reading a line from p2p-client: "
+ << utils::GetAndFreeGError(&error);
+ lookup_data->ReportError();
+ delete lookup_data;
+ }
+ } else {
+ if (str != NULL) {
+ lookup_data->stdout_ += str;
+ g_free(str);
+ }
+ }
+ return TRUE; // Don't remove source.
+ }
+
+ static void OnChildWatchActivity(GPid pid,
+ gint status,
+ gpointer user_data) {
+ LookupData *lookup_data = reinterpret_cast<LookupData*>(user_data);
+
+ if (!WIFEXITED(status)) {
+ LOG(ERROR) << "Child didn't exit normally";
+ lookup_data->ReportError();
+ } else if (WEXITSTATUS(status) != 0) {
+ LOG(INFO) << "Child exited with non-zero exit code "
+ << WEXITSTATUS(status);
+ lookup_data->ReportError();
+ } else {
+ lookup_data->ReportSuccess();
+ }
+ delete lookup_data;
+ }
+
+ static gboolean OnTimeout(gpointer user_data) {
+ LookupData *lookup_data = reinterpret_cast<LookupData*>(user_data);
+ lookup_data->ReportError();
+ delete lookup_data;
+ return TRUE; // Don't remove source.
+ }
+
+ P2PManager::LookupCallback callback_;
+ GPid pid_;
+ gint stdout_fd_;
+ guint stdout_channel_source_id_;
+ guint child_watch_source_id_;
+ guint timeout_source_id_;
+ string stdout_;
+ bool reported_;
+};
+
+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<string> args = configuration_->GetP2PClientArgs(file_id_with_ext,
+ minimum_size);
+ gchar **argv = utils::StringVectorToGStrv(args);
+ lookup_data->InitiateLookup(argv, max_time_to_wait);
+ g_strfreev(argv);
+}
+
+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<size_t>(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<size_t>(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) {
+ // ENOSPC can happen (funky race though, cf. the statvfs() check
+ // above), handle it gracefully, e.g. use logging level INFO.
+ //
+ // NOTE: we *could* handle ENOSYS gracefully (ie. we could
+ // ignore it) but currently we don't because running out of
+ // space later sounds absolutely horrible. Better to fail fast.
+ 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 = StringPrintf("%zu", 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 != NULL)
+ *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;
+
+ struct stat statbuf;
+ if (stat(path.value().c_str(), &statbuf) != 0) {
+ PLOG(ERROR) << "Error getting file status for " << path.value();
+ return -1;
+ }
+
+ return statbuf.st_size;
+}
+
+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 = NULL;
+ long long int val = strtoll(ea_value, &endp, 0);
+ 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() {
+ GDir* dir;
+ GError* error = NULL;
+ const char* name;
+ int num_files = 0;
+
+ FilePath p2p_dir = configuration_->GetP2PDir();
+ dir = g_dir_open(p2p_dir.value().c_str(), 0, &error);
+ if (dir == NULL) {
+ LOG(ERROR) << "Error opening directory " << p2p_dir.value() << ": "
+ << utils::GetAndFreeGError(&error);
+ return -1;
+ }
+
+ string ext_visible = GetExt(kVisible);
+ string ext_non_visible = GetExt(kNonVisible);
+ while ((name = g_dir_read_name(dir)) != NULL) {
+ if (g_str_has_suffix(name, ext_visible.c_str()) ||
+ g_str_has_suffix(name, ext_non_visible.c_str())) {
+ num_files += 1;
+ }
+ }
+ g_dir_close(dir);
+
+ return num_files;
+}
+
+P2PManager* P2PManager::Construct(Configuration *configuration,
+ PrefsInterface *prefs,
+ const string& file_extension,
+ const int num_files_to_keep) {
+ return new P2PManagerImpl(configuration,
+ prefs,
+ file_extension,
+ num_files_to_keep);
+}
+
+} // namespace chromeos_update_engine
diff --git a/p2p_manager.h b/p2p_manager.h
new file mode 100644
index 0000000..a38f648
--- /dev/null
+++ b/p2p_manager.h
@@ -0,0 +1,164 @@
+// Copyright (c) 2013 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 CHROMEOS_PLATFORM_UPDATE_ENGINE_P2P_MANAGER_H__
+#define CHROMEOS_PLATFORM_UPDATE_ENGINE_P2P_MANAGER_H__
+
+#include <string>
+#include <vector>
+
+#include <base/callback.h>
+#include <base/file_path.h>
+#include <base/memory/ref_counted.h>
+#include <base/time.h>
+
+#include "update_engine/prefs_interface.h"
+
+namespace chromeos_update_engine {
+
+// Interface for sharing and discovering files via p2p.
+class P2PManager {
+public:
+ // Interface used for P2PManager implementations. The sole reason
+ // for this interface is unit testing.
+ class Configuration {
+ public:
+ virtual ~Configuration() {}
+
+ // Gets the path to the p2p dir being used, e.g. /var/cache/p2p.
+ virtual base::FilePath GetP2PDir() = 0;
+
+ // Gets the argument vector for starting (if |is_start| is True)
+ // resp. stopping (if |is_start| is False) the p2p service
+ // e.g. ["initctl", "start", "p2p"] or ["initctl", "stop", "p2p"].
+ virtual std::vector<std::string> GetInitctlArgs(bool is_start) = 0;
+
+ // Gets the argument vector for invoking p2p-client, e.g.
+ // "p2p-client --get-url=file_id_we_want --minimum-size=42123".
+ virtual std::vector<std::string> GetP2PClientArgs(
+ const std::string& file_id, size_t minimum_size) = 0;
+ };
+
+ virtual ~P2PManager() {}
+
+ // The type for the callback used in LookupUrlForFile().
+ // If the lookup failed, |url| is empty.
+ typedef base::Callback<void(const std::string& url)> LookupCallback;
+
+ // Returns true if - and only if - P2P should be used on this
+ // device. This value is derived from a variety of sources including
+ // enterprise policy.
+ virtual bool IsP2PEnabled() = 0;
+
+ // Ensures that the p2p subsystem is running (e.g. starts it if it's
+ // not already running) and blocks until this is so. Returns false
+ // if an error occurred.
+ virtual bool EnsureP2PRunning() = 0;
+
+ // Ensures that the p2p subsystem is not running (e.g. stops it if
+ // it's running) and blocks until this is so. Returns false if an
+ // error occurred.
+ virtual bool EnsureP2PNotRunning() = 0;
+
+ // Cleans up files in /var/cache/p2p owned by this application as
+ // per the |file_extension| and |num_files_to_keep| values passed
+ // when the object was constructed. This may be called even if
+ // the p2p subsystem is not running.
+ virtual bool PerformHousekeeping() = 0;
+
+ // Asynchronously finds a peer that serves the file identified by
+ // |file_id|. If |minimum_size| is non-zero, will find a peer that
+ // has at least that many bytes. When the result is ready |callback|
+ // is called from the default GLib mainloop.
+ //
+ // This operation may take a very long time to complete because part
+ // of the p2p protocol involves waiting for the LAN-wide sum of all
+ // num-connections to drop below a given threshold. However, if
+ // |max_time_to_wait| is non-zero, the operation is guaranteed to
+ // not exceed this duration.
+ //
+ // If the file is not available on the LAN (or if mDNS/DNS-SD is
+ // filtered), this is guaranteed to not take longer than 5 seconds.
+ virtual void LookupUrlForFile(const std::string& file_id,
+ size_t minimum_size,
+ base::TimeDelta max_time_to_wait,
+ LookupCallback callback) = 0;
+
+ // Shares a file identified by |file_id| in the directory
+ // /var/cache/p2p. Initially the file will not be visible, that is,
+ // it will have a .tmp extension and not be shared via p2p. Use the
+ // FileMakeVisible() method to change this.
+ //
+ // If you know the final size of the file, pass it in the
+ // |expected_size| parameter. Otherwise pass zero. If non-zero, the
+ // amount of free space in /var/cache/p2p is checked and if there is
+ // less than twice the amount of space available, this method
+ // fails. Additionally, disk space will be reserved via fallocate(2)
+ // and |expected_size| is written to the user.cros-p2p-filesize
+ // xattr of the created file.
+ //
+ // If the file already exists, true is returned. Any on-disk xattr
+ // is not updated.
+ virtual bool FileShare(const std::string& file_id,
+ size_t expected_size) = 0;
+
+ // Gets a fully qualified path for the file identified by |file_id|.
+ // If the file has not been shared already using the FileShare()
+ // method, an empty FilePath is returned - use FilePath::empty() to
+ // find out.
+ virtual base::FilePath FileGetPath(const std::string& file_id) = 0;
+
+ // Gets the actual size of the file identified by |file_id|. This is
+ // equivalent to reading the value of the st_size field of the
+ // struct stat on the file given by FileGetPath(). Returns -1 if an
+ // error occurs.
+ //
+ // For a file just created with FileShare() this will return 0.
+ virtual ssize_t FileGetSize(const std::string& file_id) = 0;
+
+ // Gets the expected size of the file identified by |file_id|. This
+ // is equivalent to reading the value of the user.cros-p2p-filesize
+ // xattr on the file given by FileGetPath(). Returns -1 if an error
+ // occurs.
+ //
+ // For a file just created with FileShare() this will return the
+ // value of the |expected_size| parameter passed to that method.
+ virtual ssize_t FileGetExpectedSize(const std::string& file_id) = 0;
+
+ // Gets whether the file identified by |file_id| is publicly
+ // visible. If |out_result| is not NULL, the result is returned
+ // there. Returns false if an error occurs.
+ virtual bool FileGetVisible(const std::string& file_id,
+ bool *out_result) = 0;
+
+ // Makes the file identified by |file_id| publicly visible
+ // (e.g. removes the .tmp extension). If the file is already
+ // visible, this method does nothing. Returns False if
+ // the method fails or there is no file for |file_id|.
+ virtual bool FileMakeVisible(const std::string& file_id) = 0;
+
+ // Counts the number of shared files used by this application
+ // (cf. the |file_extension parameter|. Returns -1 if an error
+ // occurred.
+ virtual int CountSharedFiles() = 0;
+
+ // Creates a suitable P2PManager instance and initializes the object
+ // so it's ready for use. The |file_extension| parameter is used to
+ // identify your application, use e.g. "cros_au". If
+ // |configuration| is non-NULL, the P2PManager will take ownership
+ // of the Configuration object and use it (hence, it must be
+ // heap-allocated).
+ //
+ // The |num_files_to_keep| parameter specifies how many files to
+ // keep after performing housekeeping (cf. the PerformHousekeeping()
+ // method). If zero is passed, no files will be deleted.
+ static P2PManager* Construct(Configuration *configuration,
+ PrefsInterface *prefs,
+ const std::string& file_extension,
+ const int num_files_to_keep);
+};
+
+} // namespace chromeos_update_engine
+
+#endif // CHROMEOS_PLATFORM_UPDATE_ENGINE_P2P_MANAGER_H__
diff --git a/p2p_manager_unittest.cc b/p2p_manager_unittest.cc
new file mode 100644
index 0000000..e41d1ca
--- /dev/null
+++ b/p2p_manager_unittest.cc
@@ -0,0 +1,370 @@
+// Copyright (c) 2012 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 <glib.h>
+
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <dirent.h>
+#include <unistd.h>
+#include <attr/xattr.h>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/stringprintf.h"
+
+#include "update_engine/p2p_manager.h"
+#include "update_engine/fake_p2p_manager_configuration.h"
+#include "update_engine/prefs.h"
+#include "update_engine/test_utils.h"
+#include "update_engine/utils.h"
+
+using std::string;
+using std::vector;
+using base::TimeDelta;
+
+namespace chromeos_update_engine {
+
+// Test fixture that sets up a testing configuration (with e.g. a
+// temporary p2p dir) for P2PManager and cleans up when the test is
+// done.
+class P2PManagerTest : public testing::Test {
+protected:
+ P2PManagerTest() {}
+ virtual ~P2PManagerTest() {}
+
+ // Derived from testing::Test.
+ virtual void SetUp() {
+ test_conf_ = new FakeP2PManagerConfiguration();
+ }
+ virtual void TearDown() {}
+
+ // The P2PManager::Configuration instance used for testing.
+ FakeP2PManagerConfiguration *test_conf_;
+};
+
+
+// Check that result of IsP2PEnabled() correspond to the
+// kPrefsP2PEnabled state variable.
+TEST_F(P2PManagerTest, P2PEnabled) {
+ string temp_dir;
+ Prefs prefs;
+ EXPECT_TRUE(utils::MakeTempDirectory("/tmp/PayloadStateP2PTests.XXXXXX",
+ &temp_dir));
+ prefs.Init(FilePath(temp_dir));
+
+ scoped_ptr<P2PManager> manager(P2PManager::Construct(test_conf_,
+ &prefs, "cros_au", 3));
+ EXPECT_FALSE(manager->IsP2PEnabled());
+ prefs.SetBoolean(kPrefsP2PEnabled, true);
+ EXPECT_TRUE(manager->IsP2PEnabled());
+ prefs.SetBoolean(kPrefsP2PEnabled, false);
+ EXPECT_FALSE(manager->IsP2PEnabled());
+
+ EXPECT_TRUE(utils::RecursiveUnlinkDir(temp_dir));
+}
+
+// Check that we keep the $N newest files with the .$EXT.p2p extension.
+TEST_F(P2PManagerTest, HouseKeeping) {
+ scoped_ptr<P2PManager> manager(P2PManager::Construct(test_conf_,
+ NULL, "cros_au", 3));
+ EXPECT_EQ(manager->CountSharedFiles(), 0);
+
+ // Generate files both matching our pattern and not matching them.
+ for (int n = 0; n < 5; n++) {
+ EXPECT_EQ(0, System(StringPrintf("touch %s/file_%d.cros_au.p2p",
+ test_conf_->GetP2PDir().value().c_str(),
+ n)));
+
+ EXPECT_EQ(0, System(StringPrintf("touch %s/file_%d.OTHER.p2p",
+ test_conf_->GetP2PDir().value().c_str(),
+ n)));
+
+ // Sleep one micro-second to ensure that the files all have
+ // different timestamps (time resolution for ext4 is one
+ // nano-second so sleeping a single usec is more than enough).
+ g_usleep(1);
+ }
+ // CountSharedFiles() only counts 'cros_au' files.
+ EXPECT_EQ(manager->CountSharedFiles(), 5);
+
+ EXPECT_TRUE(manager->PerformHousekeeping());
+
+ // At this point - after HouseKeeping - we should only have
+ // eight files left.
+ for (int n = 0; n < 5; n++) {
+ string file_name;
+ bool expect;
+
+ expect = (n >= 2);
+ file_name = StringPrintf("%s/file_%d.cros_au.p2p",
+ test_conf_->GetP2PDir().value().c_str(), n);
+ EXPECT_EQ(!!g_file_test(file_name.c_str(), G_FILE_TEST_EXISTS), expect);
+
+ file_name = StringPrintf("%s/file_%d.OTHER.p2p",
+ test_conf_->GetP2PDir().value().c_str(), n);
+ EXPECT_TRUE(g_file_test(file_name.c_str(), G_FILE_TEST_EXISTS));
+ }
+ // CountSharedFiles() only counts 'cros_au' files.
+ EXPECT_EQ(manager->CountSharedFiles(), 3);
+}
+
+static bool CheckP2PFile(const string& p2p_dir, const string& file_name,
+ ssize_t expected_size, ssize_t expected_size_xattr) {
+ string path = p2p_dir + "/" + file_name;
+ struct stat statbuf;
+ char ea_value[64] = { 0 };
+ ssize_t ea_size;
+
+ if (stat(path.c_str(), &statbuf) != 0) {
+ LOG(ERROR) << "File " << path << " does not exist";
+ return false;
+ }
+
+ if (expected_size != 0) {
+ if (statbuf.st_size != expected_size) {
+ LOG(ERROR) << "Expected size " << expected_size
+ << " but size was " << statbuf.st_size;
+ return false;
+ }
+ }
+
+ if (expected_size_xattr == 0) {
+ ea_size = getxattr(path.c_str(), "user.cros-p2p-filesize",
+ &ea_value, sizeof ea_value - 1);
+ if (ea_size == -1 && errno == ENOATTR) {
+ // This is valid behavior as we support files without the xattr set.
+ } else {
+ PLOG(ERROR) << "getxattr() didn't fail with ENOATTR as expected, "
+ << "ea_size=" << ea_size << ", errno=" << errno;
+ return false;
+ }
+ } else {
+ ea_size = getxattr(path.c_str(), "user.cros-p2p-filesize",
+ &ea_value, sizeof ea_value - 1);
+ if (ea_size < 0) {
+ LOG(ERROR) << "Error getting xattr attribute";
+ return false;
+ }
+ char* endp = NULL;
+ long long int val = strtoll(ea_value, &endp, 0);
+ if (endp == NULL || *endp != '\0') {
+ LOG(ERROR) << "Error parsing xattr '" << ea_value
+ << "' as an integer";
+ return false;
+ }
+ if (val != expected_size_xattr) {
+ LOG(ERROR) << "Expected xattr size " << expected_size_xattr
+ << " but size was " << val;
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static bool CreateP2PFile(string p2p_dir, string file_name,
+ size_t size, size_t size_xattr) {
+ string path = p2p_dir + "/" + file_name;
+
+ int fd = open(path.c_str(), O_CREAT|O_RDWR, 0644);
+ if (fd == -1) {
+ PLOG(ERROR) << "Error creating file with path " << path;
+ return false;
+ }
+ if (ftruncate(fd, size) != 0) {
+ PLOG(ERROR) << "Error truncating " << path << " to size " << size;
+ close(fd);
+ return false;
+ }
+
+ if (size_xattr != 0) {
+ string decimal_size = StringPrintf("%zu", size_xattr);
+ if (fsetxattr(fd, "user.cros-p2p-filesize",
+ decimal_size.c_str(), decimal_size.size(), 0) != 0) {
+ PLOG(ERROR) << "Error setting xattr on " << path;
+ close(fd);
+ return false;
+ }
+ }
+
+ close(fd);
+ return true;
+}
+
+// Check that sharing a *new* file works.
+TEST_F(P2PManagerTest, ShareFile) {
+ scoped_ptr<P2PManager> manager(P2PManager::Construct(test_conf_,
+ NULL, "cros_au", 3));
+ EXPECT_TRUE(manager->FileShare("foo", 10 * 1000 * 1000));
+ EXPECT_EQ(manager->FileGetPath("foo"),
+ test_conf_->GetP2PDir().Append("foo.cros_au.p2p.tmp"));
+ EXPECT_TRUE(CheckP2PFile(test_conf_->GetP2PDir().value(),
+ "foo.cros_au.p2p.tmp", 0, 10 * 1000 * 1000));
+
+ // Sharing it again - with the same expected size - should return true
+ EXPECT_TRUE(manager->FileShare("foo", 10 * 1000 * 1000));
+
+ // ... but if we use the wrong size, it should fail
+ EXPECT_FALSE(manager->FileShare("foo", 10 * 1000 * 1000 + 1));
+}
+
+// Check that making a shared file visible, does what is expected.
+TEST_F(P2PManagerTest, MakeFileVisible) {
+ scoped_ptr<P2PManager> manager(P2PManager::Construct(test_conf_,
+ NULL, "cros_au", 3));
+ // First, check that it's not visible.
+ manager->FileShare("foo", 10*1000*1000);
+ EXPECT_EQ(manager->FileGetPath("foo"),
+ test_conf_->GetP2PDir().Append("foo.cros_au.p2p.tmp"));
+ EXPECT_TRUE(CheckP2PFile(test_conf_->GetP2PDir().value(),
+ "foo.cros_au.p2p.tmp", 0, 10 * 1000 * 1000));
+ // Make the file visible and check that it changed its name. Do it
+ // twice to check that FileMakeVisible() is idempotent.
+ for (int n = 0; n < 2; n++) {
+ manager->FileMakeVisible("foo");
+ EXPECT_EQ(manager->FileGetPath("foo"),
+ test_conf_->GetP2PDir().Append("foo.cros_au.p2p"));
+ EXPECT_TRUE(CheckP2PFile(test_conf_->GetP2PDir().value(),
+ "foo.cros_au.p2p", 0, 10 * 1000 * 1000));
+ }
+}
+
+// Check that we return the right values for existing files in P2P_DIR.
+TEST_F(P2PManagerTest, ExistingFiles) {
+ scoped_ptr<P2PManager> manager(P2PManager::Construct(test_conf_,
+ NULL, "cros_au", 3));
+ bool visible;
+
+ // Check that errors are returned if the file does not exist
+ EXPECT_EQ(manager->FileGetPath("foo"), FilePath());
+ EXPECT_EQ(manager->FileGetSize("foo"), -1);
+ EXPECT_EQ(manager->FileGetExpectedSize("foo"), -1);
+ EXPECT_FALSE(manager->FileGetVisible("foo", NULL));
+ // ... then create the file ...
+ EXPECT_TRUE(CreateP2PFile(test_conf_->GetP2PDir().value(),
+ "foo.cros_au.p2p", 42, 43));
+ // ... and then check that the expected values are returned
+ EXPECT_EQ(manager->FileGetPath("foo"),
+ test_conf_->GetP2PDir().Append("foo.cros_au.p2p"));
+ EXPECT_EQ(manager->FileGetSize("foo"), 42);
+ EXPECT_EQ(manager->FileGetExpectedSize("foo"), 43);
+ EXPECT_TRUE(manager->FileGetVisible("foo", &visible));
+ EXPECT_TRUE(visible);
+
+ // One more time, this time with a .tmp variant. First ensure it errors out..
+ EXPECT_EQ(manager->FileGetPath("bar"), FilePath());
+ EXPECT_EQ(manager->FileGetSize("bar"), -1);
+ EXPECT_EQ(manager->FileGetExpectedSize("bar"), -1);
+ EXPECT_FALSE(manager->FileGetVisible("bar", NULL));
+ // ... then create the file ...
+ EXPECT_TRUE(CreateP2PFile(test_conf_->GetP2PDir().value(),
+ "bar.cros_au.p2p.tmp", 44, 45));
+ // ... and then check that the expected values are returned
+ EXPECT_EQ(manager->FileGetPath("bar"),
+ test_conf_->GetP2PDir().Append("bar.cros_au.p2p.tmp"));
+ EXPECT_EQ(manager->FileGetSize("bar"), 44);
+ EXPECT_EQ(manager->FileGetExpectedSize("bar"), 45);
+ EXPECT_TRUE(manager->FileGetVisible("bar", &visible));
+ EXPECT_FALSE(visible);
+}
+
+// This is a little bit ugly but short of mocking a 'p2p' service this
+// will have to do. E.g. we essentially simulate the various
+// behaviours of initctl(8) that we rely on.
+TEST_F(P2PManagerTest, StartP2P) {
+ scoped_ptr<P2PManager> manager(P2PManager::Construct(test_conf_,
+ NULL, "cros_au", 3));
+
+ // Check that we can start the service
+ test_conf_->SetInitctlStartCommandLine("true");
+ EXPECT_TRUE(manager->EnsureP2PRunning());
+ test_conf_->SetInitctlStartCommandLine("false");
+ EXPECT_FALSE(manager->EnsureP2PRunning());
+ test_conf_->SetInitctlStartCommandLine(
+ "sh -c 'echo \"initctl: Job is already running: p2p\" >&2; false'");
+ EXPECT_TRUE(manager->EnsureP2PRunning());
+ test_conf_->SetInitctlStartCommandLine(
+ "sh -c 'echo something else >&2; false'");
+ EXPECT_FALSE(manager->EnsureP2PRunning());
+}
+
+// Same comment as for StartP2P
+TEST_F(P2PManagerTest, StopP2P) {
+ scoped_ptr<P2PManager> manager(P2PManager::Construct(test_conf_,
+ NULL, "cros_au", 3));
+
+ // Check that we can start the service
+ test_conf_->SetInitctlStopCommandLine("true");
+ EXPECT_TRUE(manager->EnsureP2PNotRunning());
+ test_conf_->SetInitctlStopCommandLine("false");
+ EXPECT_FALSE(manager->EnsureP2PNotRunning());
+ test_conf_->SetInitctlStopCommandLine(
+ "sh -c 'echo \"initctl: Unknown instance \" >&2; false'");
+ EXPECT_TRUE(manager->EnsureP2PNotRunning());
+ test_conf_->SetInitctlStopCommandLine(
+ "sh -c 'echo something else >&2; false'");
+ EXPECT_FALSE(manager->EnsureP2PNotRunning());
+}
+
+static void ExpectUrl(const string& expected_url,
+ GMainLoop* loop,
+ const string& url) {
+ EXPECT_EQ(url, expected_url);
+ g_main_loop_quit(loop);
+}
+
+// Like StartP2P, we're mocking the different results that p2p-client
+// can return. It's not pretty but it works.
+TEST_F(P2PManagerTest, LookupURL) {
+ scoped_ptr<P2PManager> manager(P2PManager::Construct(test_conf_,
+ NULL, "cros_au", 3));
+ GMainLoop *loop = g_main_loop_new(NULL, FALSE);
+
+ // Emulate p2p-client returning valid URL with "fooX", 42 and "cros_au"
+ // being propagated in the right places.
+ test_conf_->SetP2PClientCommandLine("echo 'http://1.2.3.4/%s_%zu'");
+ manager->LookupUrlForFile("fooX", 42, TimeDelta(),
+ base::Bind(ExpectUrl,
+ "http://1.2.3.4/fooX.cros_au_42", loop));
+ g_main_loop_run(loop);
+
+ // Emulate p2p-client returning invalid URL.
+ test_conf_->SetP2PClientCommandLine("echo 'not_a_valid_url'");
+ manager->LookupUrlForFile("foobar", 42, TimeDelta(),
+ base::Bind(ExpectUrl, "", loop));
+ g_main_loop_run(loop);
+
+ // Emulate p2p-client conveying failure.
+ test_conf_->SetP2PClientCommandLine("false");
+ manager->LookupUrlForFile("foobar", 42, TimeDelta(),
+ base::Bind(ExpectUrl, "", loop));
+ g_main_loop_run(loop);
+
+ // Emulate p2p-client not existing.
+ test_conf_->SetP2PClientCommandLine("/path/to/non/existent/helper/program");
+ manager->LookupUrlForFile("foobar", 42,
+ TimeDelta(),
+ base::Bind(ExpectUrl, "", loop));
+ g_main_loop_run(loop);
+
+ // Emulate p2p-client crashing.
+ test_conf_->SetP2PClientCommandLine("sh -c 'kill -SEGV $$'");
+ manager->LookupUrlForFile("foobar", 42, TimeDelta(),
+ base::Bind(ExpectUrl, "", loop));
+ g_main_loop_run(loop);
+
+ // Emulate p2p-client exceeding its timeout.
+ test_conf_->SetP2PClientCommandLine("sh -c 'echo http://1.2.3.4/; sleep 2'");
+ manager->LookupUrlForFile("foobar", 42, TimeDelta::FromMilliseconds(500),
+ base::Bind(ExpectUrl, "", loop));
+ g_main_loop_run(loop);
+
+ g_main_loop_unref(loop);
+}
+
+} // namespace chromeos_update_engine
diff --git a/utils.cc b/utils.cc
index 248c665..8b4bffe 100644
--- a/utils.cc
+++ b/utils.cc
@@ -1026,6 +1026,36 @@
return true;
}
+Time TimeFromStructTimespec(struct timespec *ts) {
+ int64 us = static_cast<int64>(ts->tv_sec) * Time::kMicrosecondsPerSecond +
+ static_cast<int64>(ts->tv_nsec) / Time::kNanosecondsPerMicrosecond;
+ return Time::UnixEpoch() + TimeDelta::FromMicroseconds(us);
+}
+
+gchar** StringVectorToGStrv(const vector<string> &vector) {
+ GPtrArray *p = g_ptr_array_new();
+ for (std::vector<string>::const_iterator i = vector.begin();
+ i != vector.end(); ++i) {
+ g_ptr_array_add(p, g_strdup(i->c_str()));
+ }
+ g_ptr_array_add(p, NULL);
+ return reinterpret_cast<gchar**>(g_ptr_array_free(p, FALSE));
+}
+
+string StringVectorToString(const vector<string> &vector) {
+ string str = "[";
+ for (std::vector<string>::const_iterator i = vector.begin();
+ i != vector.end(); ++i) {
+ if (i != vector.begin())
+ str += ", ";
+ str += '"';
+ str += *i;
+ str += '"';
+ }
+ str += "]";
+ return str;
+}
+
} // namespace utils
} // namespace chromeos_update_engine
diff --git a/utils.h b/utils.h
index 412252c..79b6917 100644
--- a/utils.h
+++ b/utils.h
@@ -35,6 +35,20 @@
// boot mode. Returns false if the boot mode is developer.
bool IsNormalBootMode();
+// Converts a struct timespec representing a number of seconds since
+// the Unix epoch to a base::Time. Sub-microsecond time is rounded
+// down.
+base::Time TimeFromStructTimespec(struct timespec *ts);
+
+// Converts a vector of strings to a NULL-terminated array of
+// strings. The resulting array should be freed with g_strfreev()
+// when are you done with it.
+gchar** StringVectorToGStrv(const std::vector<std::string> &vector);
+
+// Formats |vector| as a string of the form ["<elem1>", "<elem2>"].
+// Does no escaping, only use this for presentation in error messages.
+std::string StringVectorToString(const std::vector<std::string> &vector);
+
// Returns the HWID or an empty string on error.
std::string GetHardwareClass();
@@ -470,6 +484,24 @@
DISALLOW_COPY_AND_ASSIGN(ScopedActionCompleter);
};
+// A base::FreeDeleter that frees memory using g_free(). Useful when
+// integrating with GLib since it can be used with scoped_ptr to
+// automatically free memory when going out of scope.
+struct GLibFreeDeleter : public base::FreeDeleter {
+ inline void operator()(void *ptr) const {
+ g_free(reinterpret_cast<gpointer>(ptr));
+ }
+};
+
+// A base::FreeDeleter that frees memory using g_strfreev(). Useful
+// when integrating with GLib since it can be used with scoped_ptr to
+// automatically free memory when going out of scope.
+struct GLibStrvFreeDeleter : public base::FreeDeleter {
+ inline void operator()(void *ptr) const {
+ g_strfreev(reinterpret_cast<gchar**>(ptr));
+ }
+};
+
} // namespace chromeos_update_engine
#define TEST_AND_RETURN_FALSE_ERRNO(_x) \
diff --git a/utils_unittest.cc b/utils_unittest.cc
index 5098912..ae7343d 100644
--- a/utils_unittest.cc
+++ b/utils_unittest.cc
@@ -352,4 +352,21 @@
"2d7h33m20.001s");
}
+TEST(UtilsTest, TimeFromStructTimespecTest) {
+ struct timespec ts;
+
+ // Unix epoch (Thursday 00:00:00 UTC on Jan 1, 1970)
+ ts = (struct timespec) {.tv_sec = 0, .tv_nsec = 0};
+ EXPECT_EQ(base::Time::UnixEpoch(), utils::TimeFromStructTimespec(&ts));
+
+ // 42 ms after the Unix billennium (Sunday 01:46:40 UTC on September 9, 2001)
+ ts = (struct timespec) {.tv_sec = 1000 * 1000 * 1000,
+ .tv_nsec = 42 * 1000 * 1000};
+ base::Time::Exploded exploded = (base::Time::Exploded) {
+ .year = 2001, .month = 9, .day_of_week = 0, .day_of_month = 9,
+ .hour = 1, .minute = 46, .second = 40, .millisecond = 42};
+ EXPECT_EQ(base::Time::FromUTCExploded(exploded),
+ utils::TimeFromStructTimespec(&ts));
+}
+
} // namespace chromeos_update_engine