Almost there...
git-svn-id: svn://chrome-svn/chromeos/trunk@24 06c00378-0e64-4dae-be16-12b19f9950a1
diff --git a/SConstruct b/SConstruct
new file mode 100644
index 0000000..d4c97ed
--- /dev/null
+++ b/SConstruct
@@ -0,0 +1,42 @@
+# Copyright (c) 2009 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.
+
+env = Environment()
+env['CCFLAGS'] = '-g -fno-exceptions -Wall -Werror -D_FILE_OFFSET_BITS=64 ' + \
+ '-I/usr/include/libxml2'
+env['LIBS'] = Split('curl glog gtest ssl xml2 z')
+env['CPPPATH'] = ['..']
+env.ParseConfig('pkg-config --cflags --libs glib-2.0')
+
+if ARGUMENTS.get('debug', 0):
+ env['CCFLAGS'] += ' -fprofile-arcs -ftest-coverage'
+ env['LIBS'] += ['gcov']
+
+sources = Split("""action_processor.cc
+ decompressing_file_writer.cc
+ download_action.cc
+ libcurl_http_fetcher.cc
+ omaha_hash_calculator.cc
+ update_check_action.cc""")
+main = ['main.cc']
+
+unittest_sources = Split("""action_unittest.cc
+ action_pipe_unittest.cc
+ action_processor_unittest.cc
+ decompressing_file_writer_unittest.cc
+ download_action_unittest.cc
+ file_writer_unittest.cc
+ http_fetcher_unittest.cc
+ mock_http_fetcher.cc
+ omaha_hash_calculator_unittest.cc
+ test_utils.cc
+ update_check_action_unittest.cc""")
+unittest_main = ['testrunner.cc']
+
+env.Program('update_engine', sources + main)
+unittest_cmd = env.Program('update_engine_unittests',
+ sources + unittest_sources + unittest_main)
+
+Clean(unittest_cmd, Glob('*.gcda') + Glob('*.gcno') + Glob('*.gcov') +
+ Split('html app.info'))
diff --git a/action.h b/action.h
new file mode 100644
index 0000000..e973066
--- /dev/null
+++ b/action.h
@@ -0,0 +1,199 @@
+// Copyright (c) 2009 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 UPDATE_ENGINE_ACTION_H__
+#define UPDATE_ENGINE_ACTION_H__
+
+#include <stdio.h>
+#include <tr1/memory>
+#include <iostream>
+
+#include "base/basictypes.h"
+#include <glog/logging.h>
+
+#include "update_engine/action_processor.h"
+
+// The structure of these classes (Action, ActionPipe, ActionProcessor, etc.)
+// is based on the KSAction* classes from the Google Update Engine code at
+// http://code.google.com/p/update-engine/ . The author of this file sends
+// a big thanks to that team for their high quality design, implementation,
+// and documentation.
+//
+// Readers may want to consult this wiki page from the Update Engine site:
+// http://code.google.com/p/update-engine/wiki/ActionProcessor
+// Although it's referring to the Objective-C KSAction* classes, much
+// applies here as well.
+//
+// How it works:
+//
+// First off, there is only one thread and all I/O should be asynchronous.
+// A glib main loop blocks whenever there is no work to be done. This happens
+// where there is no CPU work to be done and no I/O ready to transfer in or
+// out. Two kinds of events can wake up the main loop: timer alarm or file
+// descriptors. If either of these happens, glib finds out the owner of what
+// fired and calls the appropriate code to handle it. As such, all the code
+// in the Action* classes and the code that is calls is non-blocking.
+//
+// An ActionProcessor contains a queue of Actions to perform. When
+// ActionProcessor::StartProcessing() is called, it executes the first action.
+// Each action tells the processor when it has completed, which causes the
+// Processor to execute the next action. ActionProcessor may have a delegate
+// (an object of type ActionProcessorDelegate). If it does, the delegate
+// is called to be notified of events as they happen.
+//
+// ActionPipe classes
+//
+// See action_pipe.h
+//
+// ActionTraits
+//
+// We need to use an extra class ActionTraits. ActionTraits is a simple
+// templated class that contains only two typedefs: OutputObjectType and
+// InputObjectType. Each action class also has two typedefs of the same name
+// that are of the same type. So, to get the input/output types of, e.g., the
+// DownloadAction class, we look at the type of
+// DownloadAction::InputObjectType.
+//
+// Each concrete Action class derives from Action<T>. This means that during
+// template instatiation of Action<T>, T is declared but not defined, which
+// means that T::InputObjectType (and OutputObjectType) is not defined.
+// However, the traits class is constructed in such a way that it will be
+// template instatiated first, so Action<T> *can* find the types it needs by
+// consulting ActionTraits<T>::InputObjectType (and OutputObjectType).
+// This is why the ActionTraits classes are needed.
+
+using std::tr1::shared_ptr;
+
+namespace chromeos_update_engine {
+
+// It is handy to have a non-templated base class of all Actions.
+class AbstractAction {
+ public:
+ AbstractAction() : processor_(NULL) {}
+
+ // Begin performing the action. Since this code is asynchronous, when this
+ // method returns, it means only that the action has started, not necessarily
+ // completed. However, it's acceptable for this method to perform the
+ // action synchronously; Action authors should understand the implications
+ // of synchronously performing, though, because this is a single-threaded
+ // app, the entire process will be blocked while the action performs.
+ //
+ // When the action is complete, it must call
+ // ActionProcessor::ActionComplete(this); to notify the processor that it's
+ // done.
+ virtual void PerformAction() = 0;
+
+ // Called by the ActionProcessor to tell this Action which processor
+ // it belongs to.
+ void SetProcessor(ActionProcessor* processor) {
+ if (processor)
+ CHECK(!processor_);
+ else
+ CHECK(processor_);
+ processor_ = processor;
+ }
+
+ // Returns true iff the action is the current action of its ActionProcessor.
+ bool IsRunning() const {
+ if (!processor_)
+ return false;
+ return processor_->current_action() == this;
+ }
+
+ // Called on asynchronous actions if canceled. Actions may implement if
+ // there's any cleanup to do. There is no need to call
+ // ActionProcessor::ActionComplete() because the processor knows this
+ // action is terminating.
+ // Only the ActionProcessor should call this.
+ virtual void TerminateProcessing() {};
+
+ // These methods are useful for debugging. TODO(adlr): consider using
+ // std::type_info for this?
+ // Type() returns a string of the Action type. I.e., for DownloadAction,
+ // Type() would return "DownloadAction".
+ virtual string Type() const = 0;
+
+ protected:
+ // A weak pointer to the processor that owns this Action.
+ ActionProcessor* processor_;
+};
+
+// Forward declare a couple classes we use.
+template<typename T>
+class ActionPipe;
+template<typename T>
+class ActionTraits;
+
+template<typename SubClass>
+class Action : public AbstractAction {
+ public:
+ virtual ~Action() {
+ LOG(INFO) << "Action died";
+ }
+
+ // Attaches an input pipe to this Action. This is optional; an Action
+ // doesn't need to have an input pipe. The input pipe must be of the type
+ // of object that this class expects.
+ // This is generally called by ActionPipe::Bond()
+ void set_in_pipe(
+ // this type is a fancy way of saying: a shared_ptr to an
+ // ActionPipe<InputObjectType>.
+ const shared_ptr<ActionPipe<
+ typename ActionTraits<SubClass>::InputObjectType> >&
+ in_pipe) {
+ in_pipe_ = in_pipe;
+ }
+
+ // Attaches an output pipe to this Action. This is optional; an Action
+ // doesn't need to have an output pipe. The output pipe must be of the type
+ // of object that this class expects.
+ // This is generally called by ActionPipe::Bond()
+ void set_out_pipe(
+ // this type is a fancy way of saying: a shared_ptr to an
+ // ActionPipe<OutputObjectType>.
+ const shared_ptr<ActionPipe<
+ typename ActionTraits<SubClass>::OutputObjectType> >&
+ out_pipe) {
+ out_pipe_ = out_pipe;
+ }
+ protected:
+ // Returns true iff there is an associated input pipe. If there's an input
+ // pipe, there's an input object, but it may have been constructed with the
+ // default ctor if the previous action didn't call SetOutputObject().
+ bool HasInputObject() const { return in_pipe_.get(); }
+
+ // returns a const reference to the object in the input pipe.
+ const typename ActionTraits<SubClass>::InputObjectType& GetInputObject()
+ const {
+ CHECK(HasInputObject());
+ return in_pipe_->contents();
+ }
+
+ // Returns true iff there's an output pipe.
+ bool HasOutputPipe() const {
+ return out_pipe_.get();
+ }
+
+ // Copies the object passed into the output pipe. It will be accessible to
+ // the next Action via that action's input pipe (which is the same as this
+ // Action's output pipe).
+ void SetOutputObject(
+ const typename ActionTraits<SubClass>::OutputObjectType& out_obj) {
+ CHECK(HasOutputPipe());
+ out_pipe_->set_contents(out_obj);
+ }
+
+ // We use a shared_ptr to the pipe. shared_ptr objects destroy what they
+ // point to when the last such shared_ptr object dies. We consider the
+ // Actions on either end of a pipe to "own" the pipe. When the last Action
+ // of the two dies, the ActionPipe will die, too.
+ shared_ptr<ActionPipe<typename ActionTraits<SubClass>::InputObjectType> >
+ in_pipe_;
+ shared_ptr<ActionPipe<typename ActionTraits<SubClass>::OutputObjectType> >
+ out_pipe_;
+};
+
+}; // namespace chromeos_update_engine
+
+#endif // UPDATE_ENGINE_ACTION_H__
diff --git a/action_pipe.h b/action_pipe.h
new file mode 100644
index 0000000..19926ed
--- /dev/null
+++ b/action_pipe.h
@@ -0,0 +1,94 @@
+// Copyright (c) 2009 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 UPDATE_ENGINE_PIPE_H__
+#define UPDATE_ENGINE_PIPE_H__
+
+#include <stdio.h>
+#include <iostream>
+#include <map>
+#include <string>
+#include <tr1/memory>
+#include <glog/logging.h>
+#include "base/basictypes.h"
+
+// The structure of these classes (Action, ActionPipe, ActionProcessor, etc.)
+// is based on the KSAction* classes from the Google Update Engine code at
+// http://code.google.com/p/update-engine/ . The author of this file sends
+// a big thanks to that team for their high quality design, implementation,
+// and documentation.
+
+// This class serves as a temporary holding area for an object passed out
+// from one Action and into another Action. It's templated so that it may
+// contain any type of object that an Action outputs/inputs. Actions
+// cannot be bonded (i.e., connected with a pipe) if their output/input
+// object types differ (a compiler error will result).
+//
+// An ActionPipe is generally created with the Bond() method and owned by
+// the two Action objects. a shared_ptr is used so that when the last Action
+// pointing to an ActionPipe dies, the ActionPipe dies, too.
+
+using std::map;
+using std::string;
+using std::tr1::shared_ptr;
+
+namespace chromeos_update_engine {
+
+// Used by Actions an InputObjectType or OutputObjectType to specify that
+// for that type, no object is taken/given.
+class NoneType {};
+
+template<typename T>
+class Action;
+
+template<typename ObjectType>
+class ActionPipe {
+ public:
+ virtual ~ActionPipe() {
+ LOG(INFO) << "ActionPipe died";
+ }
+
+ // This should be called by an Action on its input pipe.
+ // Returns a reference to the stored object.
+ const ObjectType& contents() const { return contents_; }
+
+ // This should be called by an Action on its output pipe.
+ // Stores a copy of the passed object in this pipe.
+ void set_contents(const ObjectType& contents) { contents_ = contents; }
+
+ // Bonds two Actions together with a new ActionPipe. The ActionPipe is
+ // jointly owned by the two Actions and will be automatically destroyed
+ // when the last Action is destroyed.
+ template<typename FromAction, typename ToAction>
+ static void Bond(FromAction* from, ToAction* to) {
+ shared_ptr<ActionPipe<ObjectType> > pipe(new ActionPipe<ObjectType>);
+ from->set_out_pipe(pipe);
+
+ to->set_in_pipe(pipe); // If you get an error on this line, then
+ // it most likely means that the From object's OutputObjectType is
+ // different from the To object's InputObjectType.
+ }
+
+ private:
+ ObjectType contents_;
+
+ // The ctor is private. This is because this class should construct itself
+ // via the static Bond() method.
+ ActionPipe() {}
+ DISALLOW_COPY_AND_ASSIGN(ActionPipe);
+};
+
+// Utility function
+template<typename FromAction, typename ToAction>
+void BondActions(FromAction* from, ToAction* to) {
+ // TODO(adlr): find something like this that the compiler accepts:
+ // COMPILE_ASSERT(typeof(typename FromAction::OutputObjectType) ==
+ // typeof(typename ToAction::InputObjectType),
+ // FromAction_OutputObjectType_doesnt_match_ToAction_InputObjectType);
+ ActionPipe<typename FromAction::OutputObjectType>::Bond(from, to);
+}
+
+} // namespace chromeos_update_engine
+
+#endif // UPDATE_ENGINE_PIPE_H__
diff --git a/action_pipe_unittest.cc b/action_pipe_unittest.cc
new file mode 100644
index 0000000..28cf248
--- /dev/null
+++ b/action_pipe_unittest.cc
@@ -0,0 +1,43 @@
+// Copyright (c) 2009 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 <gtest/gtest.h>
+#include "update_engine/action.h"
+#include "update_engine/action_pipe.h"
+
+namespace chromeos_update_engine {
+
+using chromeos_update_engine::ActionPipe;
+
+class ActionPipeTestAction;
+
+template<>
+class ActionTraits<ActionPipeTestAction> {
+ public:
+ typedef string OutputObjectType;
+ typedef string InputObjectType;
+};
+
+// This is a simple Action class for testing.
+struct ActionPipeTestAction : public Action<ActionPipeTestAction> {
+ typedef string InputObjectType;
+ typedef string OutputObjectType;
+ ActionPipe<string>* in_pipe() { return in_pipe_.get(); }
+ ActionPipe<string>* out_pipe() { return out_pipe_.get(); }
+ void PerformAction() {}
+ string Type() const { return "ActionPipeTestAction"; }
+};
+
+class ActionPipeTest : public ::testing::Test { };
+
+// This test creates two simple Actions and sends a message via an ActionPipe
+// from one to the other.
+TEST(ActionPipeTest, SimpleTest) {
+ ActionPipeTestAction a, b;
+ BondActions(&a, &b);
+ a.out_pipe()->set_contents("foo");
+ EXPECT_EQ("foo", b.in_pipe()->contents());
+}
+
+} // namespace chromeos_update_engine
diff --git a/action_processor.cc b/action_processor.cc
new file mode 100644
index 0000000..623e3d2
--- /dev/null
+++ b/action_processor.cc
@@ -0,0 +1,75 @@
+// Copyright (c) 2009 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 "update_engine/action_processor.h"
+#include "update_engine/action.h"
+
+namespace chromeos_update_engine {
+
+ActionProcessor::ActionProcessor()
+ : current_action_(NULL), delegate_(NULL) {}
+
+ActionProcessor::~ActionProcessor() {
+ if (IsRunning()) {
+ StopProcessing();
+ }
+ for (std::deque<AbstractAction*>::iterator it = actions_.begin();
+ it != actions_.end(); ++it) {
+ (*it)->SetProcessor(NULL);
+ }
+}
+
+void ActionProcessor::EnqueueAction(AbstractAction* action) {
+ actions_.push_back(action);
+ action->SetProcessor(this);
+}
+
+void ActionProcessor::StartProcessing() {
+ CHECK(!IsRunning());
+ if (!actions_.empty()) {
+ current_action_ = actions_.front();
+ LOG(INFO) << "ActionProcessor::StartProcessing: "
+ << current_action_->Type();
+ actions_.pop_front();
+ current_action_->PerformAction();
+ }
+}
+
+void ActionProcessor::StopProcessing() {
+ CHECK(IsRunning());
+ CHECK(current_action_);
+ current_action_->TerminateProcessing();
+ CHECK(current_action_);
+ current_action_->SetProcessor(NULL);
+ LOG(INFO) << "ActionProcessor::StopProcessing: aborted "
+ << current_action_->Type();
+ current_action_ = NULL;
+ if (delegate_)
+ delegate_->ProcessingStopped(this);
+}
+
+void ActionProcessor::ActionComplete(const AbstractAction* actionptr,
+ bool success) {
+ CHECK_EQ(actionptr, current_action_);
+ if (delegate_)
+ delegate_->ActionCompleted(this, actionptr, success);
+ string old_type = current_action_->Type();
+ current_action_->SetProcessor(NULL);
+ current_action_ = NULL;
+ if (actions_.empty()) {
+ LOG(INFO) << "ActionProcessor::ActionComplete: finished last action of"
+ " type " << old_type;
+ if (delegate_) {
+ delegate_->ProcessingDone(this);
+ }
+ return;
+ }
+ current_action_ = actions_.front();
+ actions_.pop_front();
+ LOG(INFO) << "ActionProcessor::ActionComplete: finished " << old_type
+ << ", starting " << current_action_->Type();
+ current_action_->PerformAction();
+}
+
+} // namespace chromeos_update_engine
diff --git a/action_processor.h b/action_processor.h
new file mode 100644
index 0000000..d478477
--- /dev/null
+++ b/action_processor.h
@@ -0,0 +1,97 @@
+// Copyright (c) 2009 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 UPDATE_ENGINE_ACTION_PROCESSOR_H__
+#define UPDATE_ENGINE_ACTION_PROCESSOR_H__
+
+#include <deque>
+
+#include "base/basictypes.h"
+
+// The structure of these classes (Action, ActionPipe, ActionProcessor, etc.)
+// is based on the KSAction* classes from the Google Update Engine code at
+// http://code.google.com/p/update-engine/ . The author of this file sends
+// a big thanks to that team for their high quality design, implementation,
+// and documentation.
+
+// See action.h for an overview of this class and other other Action* classes.
+
+// An ActionProcessor keeps a queue of Actions and processes them in order.
+
+namespace chromeos_update_engine {
+
+class AbstractAction;
+class ActionProcessorDelegate;
+
+class ActionProcessor {
+ public:
+ ActionProcessor();
+
+ ~ActionProcessor();
+
+ // Starts processing the first Action in the queue. If there's a delegate,
+ // when all processing is complete, ProcessingDone() will be called on the
+ // delegate.
+ void StartProcessing();
+
+ // Aborts processing. If an Action is running, it will have
+ // TerminateProcessing() called on it. The Action that was running
+ // will be lost and must be re-enqueued if this Processor is to use it.
+ void StopProcessing();
+
+ // Returns true iff an Action is currently processing.
+ bool IsRunning() const { return NULL != current_action_; }
+
+ // Adds another Action to the end of the queue.
+ void EnqueueAction(AbstractAction* action);
+
+ // Sets the current delegate. Set to NULL to remove a delegate.
+ void set_delegate(ActionProcessorDelegate *delegate) {
+ delegate_ = delegate;
+ }
+
+ // Returns a pointer to the current Action that's processing.
+ AbstractAction* current_action() const {
+ return current_action_;
+ }
+
+ // Called by an action to notify processor that it's done. Caller passes self.
+ void ActionComplete(const AbstractAction* actionptr, bool success);
+
+ private:
+ // Actions that have not yet begun processing, in the order in which
+ // they'll be processed.
+ std::deque<AbstractAction*> actions_;
+
+ // A pointer to the currrently processing Action, if any.
+ AbstractAction* current_action_;
+
+ // A pointer to the delegate, or NULL if none.
+ ActionProcessorDelegate *delegate_;
+ DISALLOW_COPY_AND_ASSIGN(ActionProcessor);
+};
+
+// A delegate object can be used to be notified of events that happen
+// in an ActionProcessor. An instance of this class can be passed to an
+// ActionProcessor to register itself.
+class ActionProcessorDelegate {
+ public:
+ // Called when all processing in an ActionProcessor has completed. A pointer
+ // to the ActionProcessor is passed.
+ virtual void ProcessingDone(const ActionProcessor* processor) {}
+
+ // Called when processing has stopped. Does not mean that all Actions have
+ // completed. If/when all Actions complete, ProcessingDone() will be called.
+ virtual void ProcessingStopped(const ActionProcessor* processor) {}
+
+ // Called whenever an action has finished processing, either successfully
+ // or otherwise.
+ virtual void ActionCompleted(const ActionProcessor* processor,
+ const AbstractAction* action,
+ bool success) {}
+};
+
+} // namespace chromeos_update_engine
+
+#endif // UPDATE_ENGINE_ACTION_PROCESSOR_H__
diff --git a/action_processor_unittest.cc b/action_processor_unittest.cc
new file mode 100644
index 0000000..2a80bc3
--- /dev/null
+++ b/action_processor_unittest.cc
@@ -0,0 +1,168 @@
+// Copyright (c) 2009 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 <gtest/gtest.h>
+#include "update_engine/action.h"
+#include "update_engine/action_processor.h"
+
+namespace chromeos_update_engine {
+
+using chromeos_update_engine::ActionPipe;
+
+class ActionProcessorTestAction;
+
+template<>
+class ActionTraits<ActionProcessorTestAction> {
+ public:
+ typedef string OutputObjectType;
+ typedef string InputObjectType;
+};
+
+// This is a simple Action class for testing.
+struct ActionProcessorTestAction : public Action<ActionProcessorTestAction> {
+ typedef string InputObjectType;
+ typedef string OutputObjectType;
+ ActionPipe<string>* in_pipe() { return in_pipe_.get(); }
+ ActionPipe<string>* out_pipe() { return out_pipe_.get(); }
+ ActionProcessor* processor() { return processor_; }
+ void PerformAction() {}
+ void CompleteAction() {
+ ASSERT_TRUE(processor());
+ processor()->ActionComplete(this, true);
+ }
+ string Type() const { return "ActionProcessorTestAction"; }
+};
+
+class ActionProcessorTest : public ::testing::Test { };
+
+// This test creates two simple Actions and sends a message via an ActionPipe
+// from one to the other.
+TEST(ActionProcessorTest, SimpleTest) {
+ ActionProcessorTestAction action;
+ ActionProcessor action_processor;
+ EXPECT_FALSE(action_processor.IsRunning());
+ action_processor.EnqueueAction(&action);
+ EXPECT_FALSE(action_processor.IsRunning());
+ EXPECT_FALSE(action.IsRunning());
+ action_processor.StartProcessing();
+ EXPECT_TRUE(action_processor.IsRunning());
+ EXPECT_TRUE(action.IsRunning());
+ EXPECT_EQ(action_processor.current_action(), &action);
+ action.CompleteAction();
+ EXPECT_FALSE(action_processor.IsRunning());
+ EXPECT_FALSE(action.IsRunning());
+}
+
+namespace {
+class MyActionProcessorDelegate : public ActionProcessorDelegate {
+ public:
+ explicit MyActionProcessorDelegate(const ActionProcessor* processor)
+ : processor_(processor), processing_done_called_(false) {}
+
+ virtual void ProcessingDone(const ActionProcessor* processor) {
+ EXPECT_EQ(processor_, processor);
+ EXPECT_FALSE(processing_done_called_);
+ processing_done_called_ = true;
+ }
+ virtual void ProcessingStopped(const ActionProcessor* processor) {
+ EXPECT_EQ(processor_, processor);
+ EXPECT_FALSE(processing_stopped_called_);
+ processing_stopped_called_ = true;
+ }
+ virtual void ActionCompleted(const ActionProcessor* processor,
+ const AbstractAction* action,
+ bool success) {
+ EXPECT_EQ(processor_, processor);
+ EXPECT_FALSE(action_completed_called_);
+ action_completed_called_ = true;
+ action_completed_success_ = success;
+ }
+
+ const ActionProcessor* processor_;
+ bool processing_done_called_;
+ bool processing_stopped_called_;
+ bool action_completed_called_;
+ bool action_completed_success_;
+};
+} // namespace {}
+
+TEST(ActionProcessorTest, DelegateTest) {
+ ActionProcessorTestAction action;
+ ActionProcessor action_processor;
+ MyActionProcessorDelegate delegate(&action_processor);
+ action_processor.set_delegate(&delegate);
+
+ action_processor.EnqueueAction(&action);
+ action_processor.StartProcessing();
+ action.CompleteAction();
+ action_processor.set_delegate(NULL);
+ EXPECT_TRUE(delegate.processing_done_called_);
+ EXPECT_TRUE(delegate.action_completed_called_);
+}
+
+TEST(ActionProcessorTest, StopProcessingTest) {
+ ActionProcessorTestAction action;
+ ActionProcessor action_processor;
+ MyActionProcessorDelegate delegate(&action_processor);
+ action_processor.set_delegate(&delegate);
+
+ action_processor.EnqueueAction(&action);
+ action_processor.StartProcessing();
+ action_processor.StopProcessing();
+ action_processor.set_delegate(NULL);
+ EXPECT_TRUE(delegate.processing_stopped_called_);
+ EXPECT_FALSE(delegate.action_completed_called_);
+ EXPECT_FALSE(action_processor.IsRunning());
+ EXPECT_EQ(NULL, action_processor.current_action());
+}
+
+TEST(ActionProcessorTest, ChainActionsTest) {
+ ActionProcessorTestAction action1, action2;
+ ActionProcessor action_processor;
+ action_processor.EnqueueAction(&action1);
+ action_processor.EnqueueAction(&action2);
+ action_processor.StartProcessing();
+ EXPECT_EQ(&action1, action_processor.current_action());
+ EXPECT_TRUE(action_processor.IsRunning());
+ action1.CompleteAction();
+ EXPECT_EQ(&action2, action_processor.current_action());
+ EXPECT_TRUE(action_processor.IsRunning());
+ action2.CompleteAction();
+ EXPECT_EQ(NULL, action_processor.current_action());
+ EXPECT_FALSE(action_processor.IsRunning());
+}
+
+TEST(ActionProcessorTest, DtorTest) {
+ ActionProcessorTestAction action1, action2;
+ {
+ ActionProcessor action_processor;
+ action_processor.EnqueueAction(&action1);
+ action_processor.EnqueueAction(&action2);
+ action_processor.StartProcessing();
+ }
+ EXPECT_EQ(NULL, action1.processor());
+ EXPECT_FALSE(action1.IsRunning());
+ EXPECT_EQ(NULL, action2.processor());
+ EXPECT_FALSE(action2.IsRunning());
+}
+
+TEST(ActionProcessorTest, DefaultDelegateTest) {
+ // Just make sure it doesn't crash
+ ActionProcessorTestAction action;
+ ActionProcessor action_processor;
+ ActionProcessorDelegate delegate;
+ action_processor.set_delegate(&delegate);
+
+ action_processor.EnqueueAction(&action);
+ action_processor.StartProcessing();
+ action.CompleteAction();
+
+ action_processor.EnqueueAction(&action);
+ action_processor.StartProcessing();
+ action_processor.StopProcessing();
+
+ action_processor.set_delegate(NULL);
+}
+
+} // namespace chromeos_update_engine
diff --git a/action_unittest.cc b/action_unittest.cc
new file mode 100644
index 0000000..371f84d
--- /dev/null
+++ b/action_unittest.cc
@@ -0,0 +1,59 @@
+// Copyright (c) 2009 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 <gtest/gtest.h>
+#include "update_engine/action.h"
+#include "update_engine/action_processor.h"
+
+namespace chromeos_update_engine {
+
+using chromeos_update_engine::ActionPipe;
+
+class ActionTestAction;
+
+template<>
+class ActionTraits<ActionTestAction> {
+ public:
+ typedef string OutputObjectType;
+ typedef string InputObjectType;
+};
+
+// This is a simple Action class for testing.
+struct ActionTestAction : public Action<ActionTestAction> {
+ typedef string InputObjectType;
+ typedef string OutputObjectType;
+ ActionPipe<string>* in_pipe() { return in_pipe_.get(); }
+ ActionPipe<string>* out_pipe() { return out_pipe_.get(); }
+ ActionProcessor* processor() { return processor_; }
+ void PerformAction() {}
+ void CompleteAction() {
+ ASSERT_TRUE(processor());
+ processor()->ActionComplete(this, true);
+ }
+ string Type() const { return "ActionTestAction"; }
+};
+
+class ActionTest : public ::testing::Test { };
+
+// This test creates two simple Actions and sends a message via an ActionPipe
+// from one to the other.
+TEST(ActionTest, SimpleTest) {
+ ActionTestAction action;
+
+ EXPECT_FALSE(action.in_pipe());
+ EXPECT_FALSE(action.out_pipe());
+ EXPECT_FALSE(action.processor());
+ EXPECT_FALSE(action.IsRunning());
+
+ ActionProcessor action_processor;
+ action_processor.EnqueueAction(&action);
+ EXPECT_EQ(&action_processor, action.processor());
+
+ action_processor.StartProcessing();
+ EXPECT_TRUE(action.IsRunning());
+ action.CompleteAction();
+ EXPECT_FALSE(action.IsRunning());
+}
+
+} // namespace chromeos_update_engine
diff --git a/decompressing_file_writer.cc b/decompressing_file_writer.cc
new file mode 100644
index 0000000..15bd4fd
--- /dev/null
+++ b/decompressing_file_writer.cc
@@ -0,0 +1,77 @@
+// Copyright (c) 2009 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 "update_engine/decompressing_file_writer.h"
+
+namespace chromeos_update_engine {
+
+// typedef struct z_stream_s {
+// Bytef *next_in; /* next input byte */
+// uInt avail_in; /* number of bytes available at next_in */
+// uLong total_in; /* total nb of input bytes read so far */
+//
+// Bytef *next_out; /* next output byte should be put there */
+// uInt avail_out; /* remaining free space at next_out */
+// uLong total_out; /* total nb of bytes output so far */
+//
+// char *msg; /* last error message, NULL if no error */
+// struct internal_state FAR *state; /* not visible by applications */
+//
+// alloc_func zalloc; /* used to allocate the internal state */
+// free_func zfree; /* used to free the internal state */
+// voidpf opaque; /* private data object passed to zalloc and zfree */
+//
+// int data_type; /* best guess about the data type: binary or text */
+// uLong adler; /* adler32 value of the uncompressed data */
+// uLong reserved; /* reserved for future use */
+// } z_stream;
+
+int GzipDecompressingFileWriter::Write(const void* bytes, size_t count) {
+ // Steps:
+ // put the data on next_in
+ // call inflate until it returns nothing, each time writing what we get
+ // check that next_in has no data left.
+
+ // It seems that zlib can keep internal buffers in the stream object,
+ // so not all data we get fed to us this time will necessarily
+ // be written out this time (in decompressed form).
+
+ CHECK_EQ(0, stream_.avail_in);
+ char buf[1024];
+
+ buffer_.reserve(count);
+ buffer_.clear();
+ CHECK_GE(buffer_.capacity(), count);
+ const char* char_bytes = reinterpret_cast<const char*>(bytes);
+ buffer_.insert(buffer_.end(), char_bytes, char_bytes + count);
+
+ stream_.next_in = reinterpret_cast<Bytef*>(&buffer_[0]);
+ stream_.avail_in = count;
+ int retcode = Z_OK;
+
+ while (Z_OK == retcode) {
+ stream_.next_out = reinterpret_cast<Bytef*>(buf);
+ stream_.avail_out = sizeof(buf);
+ int retcode = inflate(&stream_, Z_NO_FLUSH);
+ // check for Z_STREAM_END, Z_OK, or Z_BUF_ERROR (which is non-fatal)
+ if (Z_STREAM_END != retcode && Z_OK != retcode && Z_BUF_ERROR != retcode) {
+ LOG(ERROR) << "zlib inflate() error:" << retcode;
+ if (stream_.msg)
+ LOG(ERROR) << "message:" << stream_.msg;
+ return 0;
+ }
+ int count_received = sizeof(buf) - stream_.avail_out;
+ if (count_received > 0) {
+ next_->Write(buf, count_received);
+ } else {
+ // Inflate returned no data; we're done for now. Make sure no
+ // input data remain.
+ CHECK_EQ(0, stream_.avail_in);
+ break;
+ }
+ }
+ return count;
+}
+
+} // namespace chromeos_update_engine
diff --git a/decompressing_file_writer.h b/decompressing_file_writer.h
new file mode 100644
index 0000000..975ca3c
--- /dev/null
+++ b/decompressing_file_writer.h
@@ -0,0 +1,60 @@
+// Copyright (c) 2009 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 UPDATE_ENGINE_DECOMPRESSING_FILE_WRITER_H__
+#define UPDATE_ENGINE_DECOMPRESSING_FILE_WRITER_H__
+
+#include <zlib.h>
+#include "base/basictypes.h"
+#include "update_engine/file_writer.h"
+
+// GzipDecompressingFileWriter is an implementation of FileWriter. It will
+// gzip decompress all data that is passed via Write() onto another FileWriter,
+// which is responsible for actually writing the data out. Calls to
+// Open and Close are passed through to the other FileWriter.
+
+namespace chromeos_update_engine {
+
+class GzipDecompressingFileWriter : public FileWriter {
+ public:
+ explicit GzipDecompressingFileWriter(FileWriter* next) : next_(next) {
+ memset(&stream_, 0, sizeof(stream_));
+ CHECK_EQ(inflateInit2(&stream_, 16 + MAX_WBITS), Z_OK);
+ }
+ virtual ~GzipDecompressingFileWriter() {
+ inflateEnd(&stream_);
+ }
+
+ virtual int Open(const char* path, int flags, mode_t mode) {
+ return next_->Open(path, flags, mode);
+ }
+
+ // Write() calls into zlib to decompress the bytes passed in. It will not
+ // necessarily write all the decompressed data associated with this set of
+ // passed-in compressed data during this call, however for a valid gzip
+ // stream, after the entire stream has been written to this object,
+ // the entire decompressed stream will have been written to the
+ // underlying FileWriter.
+ virtual int Write(const void* bytes, size_t count);
+
+ virtual int Close() {
+ return next_->Close();
+ }
+ private:
+ // The FileWriter that we write all uncompressed data to
+ FileWriter* next_;
+
+ // The zlib state
+ z_stream stream_;
+
+ // This is for an optimization in Write(). We can keep this buffer around
+ // in our class to avoid repeated calls to malloc().
+ std::vector<char> buffer_;
+
+ DISALLOW_COPY_AND_ASSIGN(GzipDecompressingFileWriter);
+};
+
+} // namespace chromeos_update_engine
+
+#endif // UPDATE_ENGINE_DECOMPRESSING_FILE_WRITER_H__
diff --git a/decompressing_file_writer_unittest.cc b/decompressing_file_writer_unittest.cc
new file mode 100644
index 0000000..1d689e7
--- /dev/null
+++ b/decompressing_file_writer_unittest.cc
@@ -0,0 +1,107 @@
+// Copyright (c) 2009 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 <string.h>
+#include <unistd.h>
+#include <string>
+#include <vector>
+#include <gtest/gtest.h>
+#include "update_engine/decompressing_file_writer.h"
+#include "update_engine/mock_file_writer.h"
+#include "update_engine/test_utils.h"
+
+using std::string;
+using std::vector;
+
+namespace chromeos_update_engine {
+
+class GzipDecompressingFileWriterTest : public ::testing::Test { };
+
+TEST(GzipDecompressingFileWriterTest, SimpleTest) {
+ MockFileWriter mock_file_writer;
+ GzipDecompressingFileWriter decompressing_file_writer(&mock_file_writer);
+
+ // Here is the shell magic to include binary file in C source:
+ // hexdump -v -e '" " 12/1 "0x%02x, " "\n"' $FILENAME
+ // | sed -e '$s/0x ,//g' -e 's/^/ /g' | awk
+ // 'BEGIN { print "unsigned char file[] = {" } END { print "};" } { print }'
+
+ // uncompressed, contains just 3 bytes: "hi\n"
+ unsigned char hi_txt_gz[] = {
+ 0x1f, 0x8b, 0x08, 0x08, 0x62, 0xf5, 0x8a, 0x4a,
+ 0x02, 0x03, 0x68, 0x69, 0x2e, 0x74, 0x78, 0x74,
+ 0x00, 0xcb, 0xc8, 0xe4, 0x02, 0x00, 0x7a, 0x7a,
+ 0x6f, 0xed, 0x03, 0x00, 0x00, 0x00,
+ };
+ char hi[] = "hi\n";
+ vector<char> hi_vector(hi, hi + strlen(hi));
+
+ const string path("unused");
+ ASSERT_EQ(0, decompressing_file_writer.Open(
+ path.c_str(), O_CREAT | O_LARGEFILE | O_TRUNC | O_WRONLY, 0644));
+ ASSERT_EQ(sizeof(hi_txt_gz),
+ decompressing_file_writer.Write(hi_txt_gz, sizeof(hi_txt_gz)));
+ ASSERT_EQ(hi_vector.size(), mock_file_writer.bytes().size());
+ for (unsigned int i = 0; i < hi_vector.size(); i++) {
+ EXPECT_EQ(hi_vector[i], mock_file_writer.bytes()[i]) << "i = " << i;
+ }
+}
+
+TEST(GzipDecompressingFileWriterTest, IllegalStreamTest) {
+ MockFileWriter mock_file_writer;
+ GzipDecompressingFileWriter decompressing_file_writer(&mock_file_writer);
+
+ const string path("unused");
+ ASSERT_EQ(0, decompressing_file_writer.Open(
+ path.c_str(), O_CREAT | O_LARGEFILE | O_TRUNC | O_WRONLY, 0644));
+ EXPECT_EQ(0, decompressing_file_writer.Write("\0\0\0\0\0\0\0\0", 8));
+ EXPECT_EQ(0, mock_file_writer.bytes().size());
+}
+
+TEST(GzipDecompressingFileWriterTest, LargeTest) {
+ const string kPath("/tmp/GzipDecompressingFileWriterTest");
+ const string kPathgz(kPath + ".gz");
+ // First, generate some data, say 10 megs:
+ DirectFileWriter uncompressed_file;
+ const char* k10bytes = "0123456789";
+ const unsigned int k10bytesSize = 10;
+ const unsigned int kUncompressedFileSize = strlen(k10bytes) * 1024 * 1024;
+ uncompressed_file.Open(kPath.c_str(),
+ O_CREAT | O_LARGEFILE | O_TRUNC | O_WRONLY, 0644);
+ for (unsigned int i = 0; i < kUncompressedFileSize / k10bytesSize; i++) {
+ ASSERT_EQ(k10bytesSize, uncompressed_file.Write("0123456789", 10));
+ }
+ uncompressed_file.Close();
+
+ // compress the file
+ system((string("cat ") + kPath + " | gzip > " + kPathgz).c_str());
+
+ // Now read the compressed file and put it into a DecompressingFileWriter
+ MockFileWriter mock_file_writer;
+ GzipDecompressingFileWriter decompressing_file_writer(&mock_file_writer);
+
+ const string path("unused");
+ ASSERT_EQ(0, decompressing_file_writer.Open(
+ path.c_str(), O_CREAT | O_LARGEFILE | O_TRUNC | O_WRONLY, 0644));
+
+ // Open compressed file for reading:
+ int fd_in = open(kPathgz.c_str(), O_LARGEFILE | O_RDONLY, 0);
+ ASSERT_GE(fd_in, 0);
+ char buf[100];
+ int sz;
+ while ((sz = read(fd_in, buf, sizeof(buf))) > 0) {
+ decompressing_file_writer.Write(buf, sz);
+ }
+ close(fd_in);
+ decompressing_file_writer.Close();
+
+ ASSERT_EQ(kUncompressedFileSize, mock_file_writer.bytes().size());
+ for (unsigned int i = 0; i < kUncompressedFileSize; i++) {
+ ASSERT_EQ(mock_file_writer.bytes()[i], '0' + (i % 10)) << "i = " << i;
+ }
+ unlink(kPath.c_str());
+ unlink(kPathgz.c_str());
+}
+
+} // namespace chromeos_update_engine
diff --git a/download_action.cc b/download_action.cc
new file mode 100644
index 0000000..3a039d1
--- /dev/null
+++ b/download_action.cc
@@ -0,0 +1,93 @@
+// Copyright (c) 2009 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 "update_engine/action_pipe.h"
+#include "update_engine/download_action.h"
+
+namespace chromeos_update_engine {
+
+DownloadAction::DownloadAction(const std::string& url,
+ const std::string& output_path,
+ off_t size, const std::string& hash,
+ const bool should_decompress,
+ HttpFetcher* http_fetcher)
+ : size_(size),
+ url_(url),
+ output_path_(output_path),
+ hash_(hash),
+ should_decompress_(should_decompress),
+ writer_(NULL),
+ http_fetcher_(http_fetcher) {}
+
+DownloadAction::~DownloadAction() {}
+
+void DownloadAction::PerformAction() {
+ http_fetcher_->set_delegate(this);
+ CHECK(!writer_);
+ direct_file_writer_.reset(new DirectFileWriter);
+
+ if (should_decompress_) {
+ decompressing_file_writer_.reset(
+ new GzipDecompressingFileWriter(direct_file_writer_.get()));
+ writer_ = decompressing_file_writer_.get();
+ } else {
+ writer_ = direct_file_writer_.get();
+ }
+
+ int rc = writer_->Open(output_path_.c_str(),
+ O_TRUNC | O_WRONLY | O_CREAT | O_LARGEFILE, 0644);
+ if (rc < 0) {
+ LOG(ERROR) << "Unable to open output file " << output_path_;
+ // report error to processor
+ processor_->ActionComplete(this, false);
+ return;
+ }
+ http_fetcher_->BeginTransfer(url_);
+}
+
+void DownloadAction::TerminateProcessing() {
+ CHECK(writer_);
+ CHECK_EQ(writer_->Close(), 0);
+ writer_ = NULL;
+ http_fetcher_->TerminateTransfer();
+}
+
+void DownloadAction::ReceivedBytes(HttpFetcher *fetcher,
+ const char* bytes,
+ int length) {
+ int bytes_written = 0;
+ do {
+ CHECK_GT(length, bytes_written);
+ int rc = writer_->Write(bytes + bytes_written, length - bytes_written);
+ // TODO(adlr): handle write error
+ CHECK_GE(rc, 0);
+ bytes_written += rc;
+ } while (length != bytes_written);
+ omaha_hash_calculator_.Update(bytes, length);
+}
+
+void DownloadAction::TransferComplete(HttpFetcher *fetcher, bool successful) {
+ if (writer_) {
+ CHECK_EQ(0, writer_->Close()) << errno;
+ writer_ = NULL;
+ }
+ if (successful) {
+ // Make sure hash is correct
+ omaha_hash_calculator_.Finalize();
+ if (omaha_hash_calculator_.hash() != hash_) {
+ LOG(INFO) << "Download of " << url_ << " failed. Expect hash "
+ << hash_ << " but got hash " << omaha_hash_calculator_.hash();
+ successful = false;
+ }
+ }
+
+ // Write the path to the output pipe if we're successful
+ if (successful && HasOutputPipe())
+ SetOutputObject(output_path_);
+ processor_->ActionComplete(this, successful);
+}
+
+}; // namespace {}
diff --git a/download_action.h b/download_action.h
new file mode 100644
index 0000000..9abbdcf
--- /dev/null
+++ b/download_action.h
@@ -0,0 +1,110 @@
+// Copyright (c) 2009 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 UPDATE_ENGINE_DOWNLOAD_ACTION_H__
+#define UPDATE_ENGINE_DOWNLOAD_ACTION_H__
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include <map>
+#include <string>
+
+#include <curl/curl.h>
+
+#include "base/scoped_ptr.h"
+#include "update_engine/action.h"
+#include "update_engine/decompressing_file_writer.h"
+#include "update_engine/file_writer.h"
+#include "update_engine/http_fetcher.h"
+#include "update_engine/omaha_hash_calculator.h"
+
+// The Download Action downloads a requested url to a specified path on disk.
+// It takes no input object, but if successful, passes the output path
+// to the output pipe.
+
+using std::map;
+using std::string;
+
+namespace chromeos_update_engine {
+
+class DownloadAction;
+class NoneType;
+
+template<>
+class ActionTraits<DownloadAction> {
+ public:
+ // Does not take an object for input
+ typedef NoneType InputObjectType;
+ // On success, puts the output path on output
+ typedef std::string OutputObjectType;
+};
+
+class DownloadAction : public Action<DownloadAction>,
+ public HttpFetcherDelegate {
+ public:
+ // Takes ownership of the passed in HttpFetcher. Useful for testing.
+ // A good calling pattern is:
+ // DownloadAction(..., new WhateverHttpFetcher);
+ DownloadAction(const std::string& url, const std::string& output_path,
+ off_t size, const std::string& hash,
+ const bool should_decompress,
+ HttpFetcher* http_fetcher);
+ virtual ~DownloadAction();
+ typedef ActionTraits<DownloadAction>::InputObjectType InputObjectType;
+ typedef ActionTraits<DownloadAction>::OutputObjectType OutputObjectType;
+ void PerformAction();
+ void TerminateProcessing();
+
+ // Debugging/logging
+ std::string Type() const { return "DownloadAction"; }
+
+ // Delegate methods (see http_fetcher.h)
+ virtual void ReceivedBytes(HttpFetcher *fetcher,
+ const char* bytes, int length);
+ virtual void TransferComplete(HttpFetcher *fetcher, bool successful);
+
+ private:
+ // Expected size of the file (will be used for progress info)
+ const size_t size_;
+
+ // URL to download
+ const std::string url_;
+
+ // Path to save URL to
+ const std::string output_path_;
+
+ // Expected hash of the file. The hash must match for this action to
+ // succeed.
+ const std::string hash_;
+
+ // Whether the caller requested that we decompress the downloaded data.
+ const bool should_decompress_;
+
+ // The FileWriter that downloaded data should be written to. It will
+ // either point to *decompressing_file_writer_ or *direct_file_writer_.
+ FileWriter* writer_;
+
+ // If non-null, a FileWriter used for gzip decompressing downloaded data
+ scoped_ptr<GzipDecompressingFileWriter> decompressing_file_writer_;
+
+ // Used to write out the downloaded file
+ scoped_ptr<DirectFileWriter> direct_file_writer_;
+
+ // pointer to the HttpFetcher that does the http work
+ scoped_ptr<HttpFetcher> http_fetcher_;
+
+ // Used to find the hash of the bytes downloaded
+ OmahaHashCalculator omaha_hash_calculator_;
+ DISALLOW_COPY_AND_ASSIGN(DownloadAction);
+};
+
+// We want to be sure that we're compiled with large file support on linux,
+// just in case we find ourselves downloading large images.
+COMPILE_ASSERT(8 == sizeof(off_t), off_t_not_64_bit);
+
+} // namespace chromeos_update_engine
+
+#endif // UPDATE_ENGINE_DOWNLOAD_ACTION_H__
diff --git a/download_action_unittest.cc b/download_action_unittest.cc
new file mode 100644
index 0000000..3dfc005
--- /dev/null
+++ b/download_action_unittest.cc
@@ -0,0 +1,264 @@
+// Copyright (c) 2009 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 <string>
+#include <vector>
+#include <glib.h>
+#include <gtest/gtest.h>
+#include "update_engine/action_pipe.h"
+#include "update_engine/download_action.h"
+#include "update_engine/mock_http_fetcher.h"
+#include "update_engine/omaha_hash_calculator.h"
+#include "update_engine/test_utils.h"
+
+namespace chromeos_update_engine {
+
+using std::string;
+using std::vector;
+
+class DownloadActionTest : public ::testing::Test { };
+
+namespace {
+class DownloadActionTestProcessorDelegate : public ActionProcessorDelegate {
+ public:
+ DownloadActionTestProcessorDelegate()
+ : loop_(NULL), processing_done_called_(false) {}
+ virtual ~DownloadActionTestProcessorDelegate() {
+ EXPECT_TRUE(processing_done_called_);
+ }
+ virtual void ProcessingDone(const ActionProcessor* processor) {
+ ASSERT_TRUE(loop_);
+ g_main_loop_quit(loop_);
+ vector<char> found_data = ReadFile(path_);
+ ASSERT_EQ(expected_data_.size(), found_data.size());
+ for (unsigned i = 0; i < expected_data_.size(); i++) {
+ EXPECT_EQ(expected_data_[i], found_data[i]);
+ }
+ processing_done_called_ = true;
+ }
+
+ virtual void ActionCompleted(const ActionProcessor* processor,
+ const AbstractAction* action,
+ bool success) {
+ // make sure actions always succeed
+ EXPECT_TRUE(success);
+ }
+
+ GMainLoop *loop_;
+ string path_;
+ vector<char> expected_data_;
+ bool processing_done_called_;
+};
+
+struct EntryPointArgs {
+ const vector<char> *data;
+ GMainLoop *loop;
+ ActionProcessor *processor;
+};
+
+gboolean StartProcessorInRunLoop(gpointer data) {
+ ActionProcessor *processor = reinterpret_cast<ActionProcessor*>(data);
+ processor->StartProcessing();
+ return FALSE;
+}
+
+void TestWithData(const vector<char>& data, bool compress) {
+ vector<char> use_data;
+ if (compress) {
+ use_data = GzipCompressData(data);
+ } else {
+ use_data = data;
+ }
+
+ GMainLoop *loop = g_main_loop_new(g_main_context_default(), FALSE);
+
+ // TODO(adlr): see if we need a different file for build bots
+ const string path("/tmp/DownloadActionTest");
+ // takes ownership of passed in HttpFetcher
+ DownloadAction download_action("", path, 0,
+ OmahaHashCalculator::OmahaHashOfData(use_data),
+ compress, new MockHttpFetcher(&use_data[0],
+ use_data.size()));
+ DownloadActionTestProcessorDelegate delegate;
+ delegate.loop_ = loop;
+ delegate.expected_data_ = data;
+ delegate.path_ = path;
+ ActionProcessor processor;
+ processor.set_delegate(&delegate);
+ processor.EnqueueAction(&download_action);
+
+ g_timeout_add(0, &StartProcessorInRunLoop, &processor);
+ g_main_loop_run(loop);
+ g_main_loop_unref(loop);
+
+ // remove temp file; don't care if there are errors here
+ unlink(path.c_str());
+}
+} // namespace {}
+
+TEST(DownloadActionTest, SimpleTest) {
+ vector<char> small;
+ const char* foo = "foo";
+ small.insert(small.end(), foo, foo + strlen(foo));
+ TestWithData(small, false);
+ TestWithData(small, true);
+}
+
+TEST(DownloadActionTest, LargeTest) {
+ vector<char> big(5 * kMockHttpFetcherChunkSize);
+ char c = '0';
+ for (unsigned int i = 0; i < big.size(); i++) {
+ big[i] = c;
+ if ('9' == c)
+ c = '0';
+ else
+ c++;
+ }
+ TestWithData(big, false);
+ TestWithData(big, true);
+}
+
+namespace {
+class TerminateEarlyTestProcessorDelegate : public ActionProcessorDelegate {
+ public:
+ void ProcessingStopped(const ActionProcessor* processor) {
+ ASSERT_TRUE(loop_);
+ g_main_loop_quit(loop_);
+ }
+ GMainLoop *loop_;
+};
+
+gboolean TerminateEarlyTestStarter(gpointer data) {
+ ActionProcessor *processor = reinterpret_cast<ActionProcessor*>(data);
+ processor->StartProcessing();
+ CHECK(processor->IsRunning());
+ processor->StopProcessing();
+ return FALSE;
+}
+
+} // namespace {}
+
+TEST(DownloadActionTest, TerminateEarlyTest) {
+ GMainLoop *loop = g_main_loop_new(g_main_context_default(), FALSE);
+
+ vector<char> data(kMockHttpFetcherChunkSize + kMockHttpFetcherChunkSize / 2);
+ memset(&data[0], 0, data.size());
+
+ const string path("/tmp/DownloadActionTest");
+ {
+ // takes ownership of passed in HttpFetcher
+ DownloadAction download_action("", path, 0, "", false,
+ new MockHttpFetcher(&data[0], data.size()));
+ TerminateEarlyTestProcessorDelegate delegate;
+ delegate.loop_ = loop;
+ ActionProcessor processor;
+ processor.set_delegate(&delegate);
+ processor.EnqueueAction(&download_action);
+
+ g_timeout_add(0, &TerminateEarlyTestStarter, &processor);
+ g_main_loop_run(loop);
+ g_main_loop_unref(loop);
+ }
+
+ // 1 or 0 chunks should have come through
+ const off_t resulting_file_size(FileSize(path));
+ if (resulting_file_size != 0)
+ EXPECT_EQ(kMockHttpFetcherChunkSize, resulting_file_size);
+}
+
+class DownloadActionTestAction;
+
+template<>
+class ActionTraits<DownloadActionTestAction> {
+ public:
+ typedef string OutputObjectType;
+ typedef string InputObjectType;
+};
+
+// This is a simple Action class for testing.
+struct DownloadActionTestAction : public Action<DownloadActionTestAction> {
+ DownloadActionTestAction() : did_run_(false) {}
+ typedef string InputObjectType;
+ typedef string OutputObjectType;
+ ActionPipe<string>* in_pipe() { return in_pipe_.get(); }
+ ActionPipe<string>* out_pipe() { return out_pipe_.get(); }
+ ActionProcessor* processor() { return processor_; }
+ void PerformAction() {
+ did_run_ = true;
+ ASSERT_TRUE(HasInputObject());
+ EXPECT_EQ(expected_input_object_, GetInputObject());
+ ASSERT_TRUE(processor());
+ processor()->ActionComplete(this, true);
+ }
+ string Type() const { return "DownloadActionTestAction"; }
+ string expected_input_object_;
+ bool did_run_;
+};
+
+namespace {
+// This class is an ActionProcessorDelegate that simply terminates the
+// run loop when the ActionProcessor has completed processing. It's used
+// only by the test PassObjectOutTest.
+class PassObjectOutTestProcessorDelegate : public ActionProcessorDelegate {
+ public:
+ void ProcessingDone(const ActionProcessor* processor) {
+ ASSERT_TRUE(loop_);
+ g_main_loop_quit(loop_);
+ }
+ GMainLoop *loop_;
+};
+
+gboolean PassObjectOutTestStarter(gpointer data) {
+ ActionProcessor *processor = reinterpret_cast<ActionProcessor*>(data);
+ processor->StartProcessing();
+ return FALSE;
+}
+}
+
+TEST(DownloadActionTest, PassObjectOutTest) {
+ GMainLoop *loop = g_main_loop_new(g_main_context_default(), FALSE);
+
+ const string path("/tmp/DownloadActionTest");
+
+ // takes ownership of passed in HttpFetcher
+ DownloadAction download_action("", path, 0,
+ OmahaHashCalculator::OmahaHashOfString("x"),
+ false, new MockHttpFetcher("x", 1));
+
+ DownloadActionTestAction test_action;
+ test_action.expected_input_object_ = path;
+ BondActions(&download_action, &test_action);
+
+ ActionProcessor processor;
+ PassObjectOutTestProcessorDelegate delegate;
+ delegate.loop_ = loop;
+ processor.set_delegate(&delegate);
+ processor.EnqueueAction(&download_action);
+ processor.EnqueueAction(&test_action);
+
+ g_timeout_add(0, &PassObjectOutTestStarter, &processor);
+ g_main_loop_run(loop);
+ g_main_loop_unref(loop);
+
+ EXPECT_EQ(true, test_action.did_run_);
+}
+
+TEST(DownloadActionTest, BadOutFileTest) {
+ GMainLoop *loop = g_main_loop_new(g_main_context_default(), FALSE);
+
+ const string path("/fake/path/that/cant/be/created/because/of/missing/dirs");
+
+ // takes ownership of passed in HttpFetcher
+ DownloadAction download_action("", path, 0, "", false,
+ new MockHttpFetcher("x", 1));
+
+ ActionProcessor processor;
+ processor.EnqueueAction(&download_action);
+ processor.StartProcessing();
+ ASSERT_FALSE(processor.IsRunning());
+
+ g_main_loop_unref(loop);
+}
+
+} // namespace chromeos_update_engine
diff --git a/file_writer.h b/file_writer.h
new file mode 100644
index 0000000..223cd6e
--- /dev/null
+++ b/file_writer.h
@@ -0,0 +1,79 @@
+// Copyright (c) 2009 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 UPDATE_ENGINE_FILE_WRITER_H__
+#define UPDATE_ENGINE_FILE_WRITER_H__
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include "glog/logging.h"
+
+// FileWriter is a class that is used to (synchronously, for now) write to
+// a file. This file is a thin wrapper around open/write/close system calls,
+// but provides and interface that can be customized by subclasses that wish
+// to filter the data.
+
+namespace chromeos_update_engine {
+
+class FileWriter {
+ public:
+ virtual ~FileWriter() {}
+
+ // Wrapper around open. Returns 0 on success or -errno on error.
+ virtual int Open(const char* path, int flags, mode_t mode) = 0;
+
+ // Wrapper around write. Returns bytes written on success or
+ // -errno on error.
+ virtual int Write(const void* bytes, size_t count) = 0;
+
+ // Wrapper around close. Returns 0 on success or -errno on error.
+ virtual int Close() = 0;
+};
+
+// Direct file writer is probably the simplest FileWriter implementation.
+// It calls the system calls directly.
+
+class DirectFileWriter : public FileWriter {
+ public:
+ DirectFileWriter() : fd_(-1) {}
+ virtual ~DirectFileWriter() {}
+
+ virtual int Open(const char* path, int flags, mode_t mode) {
+ CHECK_EQ(-1, fd_);
+ fd_ = open(path, flags, mode);
+ if (fd_ < 0)
+ return -errno;
+ return 0;
+ }
+
+ virtual int Write(const void* bytes, size_t count) {
+ CHECK_GE(fd_, 0);
+ ssize_t rc = write(fd_, bytes, count);
+ if (rc < 0)
+ return -errno;
+ return rc;
+ }
+
+ virtual int Close() {
+ CHECK_GE(fd_, 0);
+ int rc = close(fd_);
+
+ // This can be any negative number that's not -1. This way, this FileWriter
+ // won't be used again for another file.
+ fd_ = -2;
+
+ if (rc < 0)
+ return -errno;
+ return rc;
+ }
+
+ private:
+ int fd_;
+};
+
+} // namespace chromeos_update_engine
+
+#endif // UPDATE_ENGINE_FILE_WRITER_H__
\ No newline at end of file
diff --git a/file_writer_unittest.cc b/file_writer_unittest.cc
new file mode 100644
index 0000000..ed8bcaa
--- /dev/null
+++ b/file_writer_unittest.cc
@@ -0,0 +1,52 @@
+// Copyright (c) 2009 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 <string.h>
+#include <unistd.h>
+#include <string>
+#include <vector>
+#include <gtest/gtest.h>
+#include "update_engine/file_writer.h"
+#include "update_engine/test_utils.h"
+
+using std::string;
+using std::vector;
+
+namespace chromeos_update_engine {
+
+class FileWriterTest : public ::testing::Test { };
+
+TEST(FileWriterTest, SimpleTest) {
+ DirectFileWriter file_writer;
+ const string path("/tmp/FileWriterTest");
+ ASSERT_EQ(0, file_writer.Open(path.c_str(),
+ O_CREAT | O_LARGEFILE | O_TRUNC | O_WRONLY,
+ 0644));
+ ASSERT_EQ(4, file_writer.Write("test", 4));
+ vector<char> actual_data = ReadFile(path);
+
+ EXPECT_FALSE(memcmp("test", &actual_data[0], actual_data.size()));
+ EXPECT_EQ(0, file_writer.Close());
+ unlink(path.c_str());
+}
+
+TEST(FileWriterTest, ErrorTest) {
+ DirectFileWriter file_writer;
+ const string path("/tmp/ENOENT/FileWriterTest");
+ EXPECT_EQ(-ENOENT, file_writer.Open(path.c_str(),
+ O_CREAT | O_LARGEFILE | O_TRUNC, 0644));
+}
+
+TEST(FileWriterTest, WriteErrorTest) {
+ DirectFileWriter file_writer;
+ const string path("/tmp/FileWriterTest");
+ EXPECT_EQ(0, file_writer.Open(path.c_str(),
+ O_CREAT | O_LARGEFILE | O_TRUNC | O_RDONLY,
+ 0644));
+ EXPECT_EQ(-EBADF, file_writer.Write("x", 1));
+ EXPECT_EQ(0, file_writer.Close());
+}
+
+
+} // namespace chromeos_update_engine
diff --git a/gen_coverage_html.sh b/gen_coverage_html.sh
new file mode 100755
index 0000000..7ccc5d8
--- /dev/null
+++ b/gen_coverage_html.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+# Copyright (c) 2009 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.
+
+set -ex
+
+scons debug=1 -c
+scons debug=1
+lcov -d . --zerocounters
+./update_engine_unittests
+lcov --directory . --capture --output-file app.info
+
+# some versions of genhtml support the --no-function-coverage argument,
+# which we want. The problem w/ function coverage is that every template
+# instantiation of a method counts as a different method, so if we
+# instantiate a method twice, once for testing and once for prod, the method
+# is tested, but it shows only 50% function coverage b/c it thinks we didn't
+# test the prod version.
+genhtml --no-function-coverage -o html ./app.info || genhtml -o html ./app.info
diff --git a/http_fetcher.h b/http_fetcher.h
new file mode 100644
index 0000000..6e26906
--- /dev/null
+++ b/http_fetcher.h
@@ -0,0 +1,90 @@
+// Copyright (c) 2009 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 UPDATE_ENGINE_HTTP_FETCHER_H__
+#define UPDATE_ENGINE_HTTP_FETCHER_H__
+
+#include <string>
+#include <glib.h>
+#include "base/basictypes.h"
+
+// This class is a simple wrapper around an HTTP library (libcurl). We can
+// easily mock out this interface for testing.
+
+// Implementations of this class should use asynchronous i/o. They can access
+// the glib main loop to request callbacks when timers or file descriptors
+// change.
+
+namespace chromeos_update_engine {
+
+class HttpFetcherDelegate;
+
+class HttpFetcher {
+ public:
+ HttpFetcher() : delegate_(NULL) {}
+ virtual ~HttpFetcher() {}
+ void set_delegate(HttpFetcherDelegate* delegate) {
+ delegate_ = delegate;
+ }
+ HttpFetcherDelegate* delegate() const {
+ return delegate_;
+ }
+
+ // Optional: Post data to the server. The HttpFetcher should make a copy
+ // of this data and upload it via HTTP POST during the transfer.
+ void SetPostData(const void* data, size_t size) {
+ post_data_set_ = true;
+ post_data_.clear();
+ const char *char_data = reinterpret_cast<const char*>(data);
+ post_data_.insert(post_data_.end(), char_data, char_data + size);
+ }
+
+ // Begins the transfer to the specified URL.
+ virtual void BeginTransfer(const std::string& url) = 0;
+
+ // Aborts the transfer. TransferComplete() will not be called on the
+ // delegate.
+ virtual void TerminateTransfer() = 0;
+
+ // If data is coming in too quickly, you can call Pause() to pause the
+ // transfer. The delegate will not have ReceivedBytes() called while
+ // an HttpFetcher is paused.
+ virtual void Pause() = 0;
+
+ // Used to unpause an HttpFetcher and let the bytes stream in again.
+ // If a delegate is set, ReceivedBytes() may be called on it before
+ // Unpause() returns
+ virtual void Unpause() = 0;
+
+ protected:
+ // The URL we're actively fetching from
+ std::string url_;
+
+ // POST data for the transfer, and whether or not it was ever set
+ bool post_data_set_;
+ std::vector<char> post_data_;
+
+ // The delegate; may be NULL.
+ HttpFetcherDelegate* delegate_;
+ private:
+ DISALLOW_COPY_AND_ASSIGN(HttpFetcher);
+};
+
+// Interface for delegates
+class HttpFetcherDelegate {
+ public:
+ // Called every time bytes are received, even if they are automatically
+ // delivered to an output file.
+ virtual void ReceivedBytes(HttpFetcher* fetcher,
+ const char* bytes,
+ int length) = 0;
+
+ // Called when the transfer has completed successfully or been somehow
+ // aborted.
+ virtual void TransferComplete(HttpFetcher* fetcher, bool successful) = 0;
+};
+
+} // namespace chromeos_update_engine
+
+#endif // UPDATE_ENGINE_HTTP_FETCHER_H__
diff --git a/http_fetcher_unittest.cc b/http_fetcher_unittest.cc
new file mode 100644
index 0000000..92edf69
--- /dev/null
+++ b/http_fetcher_unittest.cc
@@ -0,0 +1,277 @@
+// Copyright (c) 2009 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 <unistd.h>
+#include <base/scoped_ptr.h>
+#include <glib.h>
+#include <glog/logging.h>
+#include <gtest/gtest.h>
+#include "update_engine/libcurl_http_fetcher.h"
+#include "update_engine/mock_http_fetcher.h"
+
+namespace chromeos_update_engine {
+
+namespace {
+// WARNING, if you update this, you must also update test_http_server.py
+const char* const kServerPort = "8080";
+string LocalServerUrlForPath(const string& path) {
+ return string("http://127.0.0.1:") + kServerPort + path;
+}
+}
+
+template <typename T>
+class HttpFetcherTest : public ::testing::Test {
+ public:
+ HttpFetcher* NewLargeFetcher() = 0;
+ HttpFetcher* NewSmallFetcher() = 0;
+ string BigUrl() const = 0;
+ string SmallUrl() const = 0;
+};
+
+class NullHttpServer {
+ public:
+ NullHttpServer() : started_(true) {}
+ ~NullHttpServer() {}
+ bool started_;
+};
+
+
+template <>
+class HttpFetcherTest<MockHttpFetcher> : public ::testing::Test {
+ public:
+ HttpFetcher* NewLargeFetcher() {
+ vector<char> big_data(1000000);
+ return new MockHttpFetcher(big_data.data(), big_data.size());
+ }
+ HttpFetcher* NewSmallFetcher() {
+ return new MockHttpFetcher("x", 1);
+ }
+ string BigUrl() const {
+ return "unused://unused";
+ }
+ string SmallUrl() const {
+ return "unused://unused";
+ }
+ typedef NullHttpServer HttpServer;
+};
+
+class PythonHttpServer {
+ public:
+ PythonHttpServer() {
+ char *argv[2] = {strdup("./test_http_server.py"), NULL};
+ GError *err;
+ started_ = false;
+ if (!g_spawn_async(NULL,
+ argv,
+ NULL,
+ G_SPAWN_DO_NOT_REAP_CHILD,
+ NULL,
+ NULL,
+ &pid_,
+ &err)) {
+ return;
+ }
+ int rc = 1;
+ while (0 != rc) {
+
+ rc = system((string("wget --output-document=/dev/null ") +
+ LocalServerUrlForPath("/test")).c_str());
+ usleep(10 * 1000); // 10 ms
+ }
+ started_ = true;
+ free(argv[0]);
+ return;
+ }
+ ~PythonHttpServer() {
+ if (!started_)
+ return;
+ // request that the server exit itself
+ system((string("wget --output-document=/dev/null ") +
+ LocalServerUrlForPath("/quitquitquit")).c_str());
+ waitpid(pid_, NULL, 0);
+ }
+ GPid pid_;
+ bool started_;
+};
+
+template <>
+class HttpFetcherTest<LibcurlHttpFetcher> : public ::testing::Test {
+ public:
+ HttpFetcher* NewLargeFetcher() {
+ LibcurlHttpFetcher *ret = new LibcurlHttpFetcher;
+ ret->set_idle_ms(1); // speeds up test execution
+ return ret;
+ }
+ HttpFetcher* NewSmallFetcher() {
+ return NewLargeFetcher();
+ }
+ string BigUrl() const {
+ return LocalServerUrlForPath("/big");
+ }
+ string SmallUrl() const {
+ return LocalServerUrlForPath("/foo");
+ }
+ typedef PythonHttpServer HttpServer;
+};
+
+typedef ::testing::Types<LibcurlHttpFetcher, MockHttpFetcher>
+ HttpFetcherTestTypes;
+TYPED_TEST_CASE(HttpFetcherTest, HttpFetcherTestTypes);
+
+namespace {
+class HttpFetcherTestDelegate : public HttpFetcherDelegate {
+public:
+ virtual void ReceivedBytes(HttpFetcher* fetcher,
+ const char* bytes, int length) {
+ char str[length + 1];
+ memset(str, 0, length + 1);
+ memcpy(str, bytes, length);
+ }
+ virtual void TransferComplete(HttpFetcher* fetcher, bool successful) {
+ g_main_loop_quit(loop_);
+ }
+ GMainLoop* loop_;
+};
+} // namespace {}
+
+TYPED_TEST(HttpFetcherTest, SimpleTest) {
+ GMainLoop *loop = g_main_loop_new(g_main_context_default(), FALSE);
+ {
+ HttpFetcherTestDelegate delegate;
+ delegate.loop_ = loop;
+ scoped_ptr<HttpFetcher> fetcher(this->NewSmallFetcher());
+ fetcher->set_delegate(&delegate);
+
+ typename TestFixture::HttpServer server;
+ ASSERT_TRUE(server.started_);
+
+ fetcher->BeginTransfer(this->SmallUrl());
+ g_main_loop_run(loop);
+ }
+ g_main_loop_unref(loop);
+}
+
+namespace {
+class PausingHttpFetcherTestDelegate : public HttpFetcherDelegate {
+ public:
+ virtual void ReceivedBytes(HttpFetcher* fetcher,
+ const char* bytes, int length) {
+ char str[length + 1];
+ LOG(INFO) << "got " << length << " bytes";
+ memset(str, 0, length + 1);
+ memcpy(str, bytes, length);
+ CHECK(!paused_);
+ paused_ = true;
+ fetcher->Pause();
+ LOG(INFO) << "calling pause";
+ }
+ virtual void TransferComplete(HttpFetcher* fetcher, bool successful) {
+ g_main_loop_quit(loop_);
+ }
+ void Unpause() {
+ CHECK(paused_);
+ paused_ = false;
+ fetcher_->Unpause();
+ LOG(INFO) << "calling unpause";
+ }
+ bool paused_;
+ HttpFetcher* fetcher_;
+ GMainLoop* loop_;
+};
+
+gboolean UnpausingTimeoutCallback(gpointer data) {
+ PausingHttpFetcherTestDelegate *delegate =
+ reinterpret_cast<PausingHttpFetcherTestDelegate*>(data);
+ if (delegate->paused_)
+ delegate->Unpause();
+ return TRUE;
+}
+} // namespace {}
+
+TYPED_TEST(HttpFetcherTest, PauseTest) {
+ GMainLoop *loop = g_main_loop_new(g_main_context_default(), FALSE);
+ {
+ PausingHttpFetcherTestDelegate delegate;
+ scoped_ptr<HttpFetcher> fetcher(this->NewLargeFetcher());
+ delegate.paused_ = false;
+ delegate.loop_ = loop;
+ delegate.fetcher_ = fetcher.get();
+ fetcher->set_delegate(&delegate);
+
+ typename TestFixture::HttpServer server;
+ ASSERT_TRUE(server.started_);
+ GSource* timeout_source_;
+ timeout_source_ = g_timeout_source_new(0); // ms
+ g_source_set_callback(timeout_source_, UnpausingTimeoutCallback, &delegate,
+ NULL);
+ g_source_attach(timeout_source_, NULL);
+ fetcher->BeginTransfer(this->BigUrl());
+
+ g_main_loop_run(loop);
+ g_source_destroy(timeout_source_);
+ }
+ g_main_loop_unref(loop);
+}
+
+namespace {
+class AbortingHttpFetcherTestDelegate : public HttpFetcherDelegate {
+ public:
+ virtual void ReceivedBytes(HttpFetcher* fetcher,
+ const char* bytes, int length) {}
+ virtual void TransferComplete(HttpFetcher* fetcher, bool successful) {
+ CHECK(false); // We should never get here
+ g_main_loop_quit(loop_);
+ }
+ void TerminateTransfer() {
+ CHECK(once_);
+ once_ = false;
+ fetcher_->TerminateTransfer();
+ }
+ void EndLoop() {
+ g_main_loop_quit(loop_);
+ }
+ bool once_;
+ HttpFetcher* fetcher_;
+ GMainLoop* loop_;
+};
+
+gboolean AbortingTimeoutCallback(gpointer data) {
+ AbortingHttpFetcherTestDelegate *delegate =
+ reinterpret_cast<AbortingHttpFetcherTestDelegate*>(data);
+ if (delegate->once_) {
+ delegate->TerminateTransfer();
+ return TRUE;
+ } else {
+ delegate->EndLoop();
+ return FALSE;
+ }
+}
+} // namespace {}
+
+TYPED_TEST(HttpFetcherTest, AbortTest) {
+ GMainLoop *loop = g_main_loop_new(g_main_context_default(), FALSE);
+ {
+ AbortingHttpFetcherTestDelegate delegate;
+ scoped_ptr<HttpFetcher> fetcher(this->NewLargeFetcher());
+ delegate.once_ = true;
+ delegate.loop_ = loop;
+ delegate.fetcher_ = fetcher.get();
+ fetcher->set_delegate(&delegate);
+
+ typename TestFixture::HttpServer server;
+ ASSERT_TRUE(server.started_);
+ GSource* timeout_source_;
+ timeout_source_ = g_timeout_source_new(0); // ms
+ g_source_set_callback(timeout_source_, AbortingTimeoutCallback, &delegate,
+ NULL);
+ g_source_attach(timeout_source_, NULL);
+ fetcher->BeginTransfer(this->BigUrl());
+
+ g_main_loop_run(loop);
+ g_source_destroy(timeout_source_);
+ }
+ g_main_loop_unref(loop);
+}
+
+} // namespace chromeos_update_engine
diff --git a/libcurl_http_fetcher.cc b/libcurl_http_fetcher.cc
new file mode 100644
index 0000000..ab48694
--- /dev/null
+++ b/libcurl_http_fetcher.cc
@@ -0,0 +1,226 @@
+// Copyright (c) 2009 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 "glog/logging.h"
+#include "update_engine/libcurl_http_fetcher.h"
+
+// This is a concrete implementation of HttpFetcher that uses libcurl to do the
+// http work.
+
+namespace chromeos_update_engine {
+
+LibcurlHttpFetcher::~LibcurlHttpFetcher() {
+ CleanUp();
+}
+
+// Begins the transfer, which must not have already been started.
+void LibcurlHttpFetcher::BeginTransfer(const std::string& url) {
+ CHECK(!transfer_in_progress_);
+ url_ = url;
+ curl_multi_handle_ = curl_multi_init();
+ CHECK(curl_multi_handle_);
+
+ curl_handle_ = curl_easy_init();
+ CHECK(curl_handle_);
+
+ if (post_data_set_) {
+ CHECK_EQ(CURLE_OK, curl_easy_setopt(curl_handle_, CURLOPT_POST, 1));
+ CHECK_EQ(CURLE_OK, curl_easy_setopt(curl_handle_, CURLOPT_POSTFIELDS,
+ &post_data_[0]));
+ CHECK_EQ(CURLE_OK, curl_easy_setopt(curl_handle_, CURLOPT_POSTFIELDSIZE,
+ post_data_.size()));
+ }
+
+ CHECK_EQ(CURLE_OK, curl_easy_setopt(curl_handle_, CURLOPT_WRITEDATA, this));
+ CHECK_EQ(CURLE_OK, curl_easy_setopt(curl_handle_, CURLOPT_WRITEFUNCTION,
+ StaticLibcurlWrite));
+ CHECK_EQ(CURLE_OK, curl_easy_setopt(curl_handle_, CURLOPT_URL, url_.c_str()));
+ CHECK_EQ(CURLM_OK, curl_multi_add_handle(curl_multi_handle_, curl_handle_));
+ transfer_in_progress_ = true;
+ CurlPerformOnce();
+}
+
+void LibcurlHttpFetcher::TerminateTransfer() {
+ CleanUp();
+}
+
+// TODO(adlr): detect network failures
+void LibcurlHttpFetcher::CurlPerformOnce() {
+ CHECK(transfer_in_progress_);
+ int running_handles = 0;
+ CURLMcode retcode = CURLM_CALL_MULTI_PERFORM;
+
+ // libcurl may request that we immediately call curl_multi_perform after it
+ // returns, so we do. libcurl promises that curl_multi_perform will not block.
+ while (CURLM_CALL_MULTI_PERFORM == retcode) {
+ retcode = curl_multi_perform(curl_multi_handle_, &running_handles);
+ }
+ if (0 == running_handles) {
+ // we're done!
+ CleanUp();
+ if (delegate_)
+ delegate_->TransferComplete(this, true); // success
+ } else {
+ // set up callback
+ SetupMainloopSources();
+ }
+}
+
+size_t LibcurlHttpFetcher::LibcurlWrite(void *ptr, size_t size, size_t nmemb) {
+ if (delegate_)
+ delegate_->ReceivedBytes(this, reinterpret_cast<char*>(ptr), size * nmemb);
+ return size * nmemb;
+}
+
+void LibcurlHttpFetcher::Pause() {
+ CHECK(curl_handle_);
+ CHECK(transfer_in_progress_);
+ CHECK_EQ(CURLE_OK, curl_easy_pause(curl_handle_, CURLPAUSE_ALL));
+}
+
+void LibcurlHttpFetcher::Unpause() {
+ CHECK(curl_handle_);
+ CHECK(transfer_in_progress_);
+ CHECK_EQ(CURLE_OK, curl_easy_pause(curl_handle_, CURLPAUSE_CONT));
+}
+
+// This method sets up callbacks with the glib main loop.
+void LibcurlHttpFetcher::SetupMainloopSources() {
+ fd_set fd_read;
+ fd_set fd_write;
+ fd_set fd_exec;
+
+ FD_ZERO(&fd_read);
+ FD_ZERO(&fd_write);
+ FD_ZERO(&fd_exec);
+
+ int fd_max = 0;
+
+ // Ask libcurl for the set of file descriptors we should track on its
+ // behalf.
+ CHECK_EQ(CURLM_OK, curl_multi_fdset(curl_multi_handle_, &fd_read, &fd_write,
+ &fd_exec, &fd_max));
+
+ // We should iterate through all file descriptors up to libcurl's fd_max or
+ // the highest one we're tracking, whichever is larger
+ if (!io_channels_.empty())
+ fd_max = max(fd_max, io_channels_.rbegin()->first);
+
+ // For each fd, if we're not tracking it, track it. If we are tracking it,
+ // but libcurl doesn't care about it anymore, stop tracking it.
+ // After this loop, there should be exactly as many GIOChannel objects
+ // in io_channels_ as there are fds that we're tracking.
+ for (int i = 0; i <= fd_max; i++) {
+ if (!(FD_ISSET(i, &fd_read) || FD_ISSET(i, &fd_write) ||
+ FD_ISSET(i, &fd_exec))) {
+ // if we have an outstanding io_channel, remove it
+ if (io_channels_.find(i) != io_channels_.end()) {
+ g_source_remove(io_channels_[i].second);
+ g_io_channel_unref(io_channels_[i].first);
+ io_channels_.erase(io_channels_.find(i));
+ }
+ continue;
+ }
+ // If we are already tracking this fd, continue.
+ if (io_channels_.find(i) != io_channels_.end())
+ continue;
+
+ // We must track a new fd
+ GIOChannel *io_channel = g_io_channel_unix_new(i);
+ guint tag = g_io_add_watch(
+ io_channel,
+ static_cast<GIOCondition>(G_IO_IN | G_IO_OUT | G_IO_PRI |
+ G_IO_ERR | G_IO_HUP),
+ &StaticFDCallback,
+ this);
+ io_channels_[i] = make_pair(io_channel, tag);
+ }
+
+ // Wet up a timeout callback for libcurl
+ long ms = 0;
+ CHECK_EQ(CURLM_OK, curl_multi_timeout(curl_multi_handle_, &ms));
+ if (ms < 0) {
+ // From http://curl.haxx.se/libcurl/c/curl_multi_timeout.html:
+ // if libcurl returns a -1 timeout here, it just means that libcurl
+ // currently has no stored timeout value. You must not wait too long
+ // (more than a few seconds perhaps) before you call
+ // curl_multi_perform() again.
+ ms = idle_ms_;
+ }
+ if (timeout_source_) {
+ g_source_destroy(timeout_source_);
+ timeout_source_ = NULL;
+ }
+ timeout_source_ = g_timeout_source_new(ms);
+ CHECK(timeout_source_);
+ g_source_set_callback(timeout_source_, StaticTimeoutCallback, this,
+ NULL);
+ g_source_attach(timeout_source_, NULL);
+}
+
+bool LibcurlHttpFetcher::FDCallback(GIOChannel *source,
+ GIOCondition condition) {
+ // Figure out which source it was; hopefully there aren't too many b/c
+ // this is a linear scan of our channels
+ bool found_in_set = false;
+ for (IOChannels::iterator it = io_channels_.begin();
+ it != io_channels_.end(); ++it) {
+ if (it->second.first == source) {
+ // We will return false from this method, meaning that we shouldn't keep
+ // this g_io_channel around. So we remove it now from our collection of
+ // g_io_channels so that the other code in this class doens't mess with
+ // this (doomed) GIOChannel.
+ // TODO(adlr): optimize by seeing if we should reuse this GIOChannel
+ g_source_remove(it->second.second);
+ g_io_channel_unref(it->second.first);
+ io_channels_.erase(it);
+ found_in_set = true;
+ break;
+ }
+ }
+ CHECK(found_in_set);
+ CurlPerformOnce();
+ return false;
+}
+
+bool LibcurlHttpFetcher::TimeoutCallback() {
+ // Since we will return false from this function, which tells glib to
+ // destroy the timeout callback, we must NULL it out here. This way, when
+ // setting up callback sources again, we won't try to delete this (doomed)
+ // timeout callback then.
+ // TODO(adlr): optimize by checking if we can keep this timeout callback.
+ timeout_source_ = NULL;
+ CurlPerformOnce();
+ return false;
+}
+
+void LibcurlHttpFetcher::CleanUp() {
+ if (timeout_source_) {
+ g_source_destroy(timeout_source_);
+ timeout_source_ = NULL;
+ }
+
+ for (IOChannels::iterator it = io_channels_.begin();
+ it != io_channels_.end(); ++it) {
+ g_source_remove(it->second.second);
+ g_io_channel_unref(it->second.first);
+ }
+ io_channels_.clear();
+
+ if (curl_handle_) {
+ if (curl_multi_handle_) {
+ CHECK_EQ(CURLM_OK,
+ curl_multi_remove_handle(curl_multi_handle_, curl_handle_));
+ }
+ curl_easy_cleanup(curl_handle_);
+ curl_handle_ = NULL;
+ }
+ if (curl_multi_handle_) {
+ CHECK_EQ(CURLM_OK, curl_multi_cleanup(curl_multi_handle_));
+ curl_multi_handle_ = NULL;
+ }
+ transfer_in_progress_ = false;
+}
+
+} // namespace chromeos_update_engine
diff --git a/libcurl_http_fetcher.h b/libcurl_http_fetcher.h
new file mode 100644
index 0000000..0ca31b6
--- /dev/null
+++ b/libcurl_http_fetcher.h
@@ -0,0 +1,118 @@
+// Copyright (c) 2009 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 UPDATE_ENGINE_LIBCURL_HTTP_FETCHER_H__
+#define UPDATE_ENGINE_LIBCURL_HTTP_FETCHER_H__
+
+#include <map>
+#include <string>
+#include <curl/curl.h>
+#include <glib.h>
+#include "base/basictypes.h"
+#include "glog/logging.h"
+#include "update_engine/http_fetcher.h"
+
+// This is a concrete implementation of HttpFetcher that uses libcurl to do the
+// http work.
+
+namespace chromeos_update_engine {
+
+class LibcurlHttpFetcher : public HttpFetcher {
+ public:
+ LibcurlHttpFetcher()
+ : curl_multi_handle_(NULL), curl_handle_(NULL),
+ timeout_source_(NULL), transfer_in_progress_(false),
+ idle_ms_(1000) {}
+
+ // Cleans up all internal state. Does not notify delegate
+ ~LibcurlHttpFetcher();
+
+ // Begins the transfer if it hasn't already begun.
+ virtual void BeginTransfer(const std::string& url);
+
+ // If the transfer is in progress, aborts the transfer early.
+ // The transfer cannot be resumed.
+ virtual void TerminateTransfer();
+
+ // Suspend the transfer by calling curl_easy_pause(CURLPAUSE_ALL).
+ virtual void Pause();
+
+ // Resume the transfer by calling curl_easy_pause(CURLPAUSE_CONT).
+ virtual void Unpause();
+
+ // Libcurl sometimes asks to be called back after some time while
+ // leaving that time unspecified. In that case, we pick a reasonable
+ // default of one second, but it can be overridden here. This is
+ // primarily useful for testing.
+ // From http://curl.haxx.se/libcurl/c/curl_multi_timeout.html:
+ // if libcurl returns a -1 timeout here, it just means that libcurl
+ // currently has no stored timeout value. You must not wait too long
+ // (more than a few seconds perhaps) before you call
+ // curl_multi_perform() again.
+ void set_idle_ms(long ms) {
+ idle_ms_ = ms;
+ }
+ private:
+
+ // These two methods are for glib main loop callbacks. They are called
+ // when either a file descriptor is ready for work or when a timer
+ // has fired. The static versions are shims for libcurl which has a C API.
+ bool FDCallback(GIOChannel *source, GIOCondition condition);
+ static gboolean StaticFDCallback(GIOChannel *source,
+ GIOCondition condition,
+ gpointer data) {
+ return reinterpret_cast<LibcurlHttpFetcher*>(data)->FDCallback(source,
+ condition);
+ }
+ bool TimeoutCallback();
+ static gboolean StaticTimeoutCallback(gpointer data) {
+ return reinterpret_cast<LibcurlHttpFetcher*>(data)->TimeoutCallback();
+ }
+
+ // Calls into curl_multi_perform to let libcurl do its work. Returns after
+ // curl_multi_perform is finished, which may actually be after more than
+ // one call to curl_multi_perform. This method will set up the glib run
+ // loop with sources for future work that libcurl will do.
+ // This method will not block.
+ void CurlPerformOnce();
+
+ // Sets up glib main loop sources as needed by libcurl. This is generally
+ // the file descriptor of the socket and a timer in case nothing happens
+ // on the fds.
+ void SetupMainloopSources();
+
+ // Callback called by libcurl when new data has arrived on the transfer
+ size_t LibcurlWrite(void *ptr, size_t size, size_t nmemb);
+ static size_t StaticLibcurlWrite(void *ptr, size_t size,
+ size_t nmemb, void *stream) {
+ return reinterpret_cast<LibcurlHttpFetcher*>(stream)->
+ LibcurlWrite(ptr, size, nmemb);
+ }
+
+ // Cleans up the following if they are non-null:
+ // curl(m) handles, io_channels_, timeout_source_.
+ void CleanUp();
+
+ // Handles for the libcurl library
+ CURLM *curl_multi_handle_;
+ CURL *curl_handle_;
+
+ // a list of all file descriptors that we're waiting on from the
+ // glib main loop
+ typedef std::map<int, std::pair<GIOChannel*, guint> > IOChannels;
+ IOChannels io_channels_;
+
+ // if non-NULL, a timer we're waiting on. glib main loop will call us back
+ // when it fires.
+ GSource* timeout_source_;
+
+ bool transfer_in_progress_;
+
+ long idle_ms_;
+ DISALLOW_COPY_AND_ASSIGN(LibcurlHttpFetcher);
+};
+
+} // namespace chromeos_update_engine
+
+#endif // UPDATE_ENGINE_LIBCURL_HTTP_FETCHER_H__
diff --git a/main.cc b/main.cc
new file mode 100644
index 0000000..21add3b
--- /dev/null
+++ b/main.cc
@@ -0,0 +1,35 @@
+// Copyright (c) 2009 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 <stdlib.h>
+#include <stdio.h>
+#include <iostream>
+#include <glib.h>
+#include <glog/logging.h>
+
+#include <libxml/parser.h>
+#include <libxml/xpath.h>
+#include <libxml/xpathInternals.h>
+
+// This code runs inside the main loop
+gboolean EntryPoint(gpointer data) {
+ GMainLoop *loop = reinterpret_cast<GMainLoop*>(data);
+ LOG(INFO) << "Chrome OS Update Engine beginning";
+ g_main_loop_quit(loop);
+ return FALSE;
+}
+
+int main(int argc, char** argv) {
+ xmlDocPtr doc = xmlNewDoc((const xmlChar*)"1.0");
+ CHECK(doc);
+ CHECK_EQ(argc, 2);
+ printf("enc: [%s]\n", xmlEncodeEntitiesReentrant(doc, (const xmlChar*)argv[1]));
+ return 0;
+
+
+ GMainLoop *loop = g_main_loop_new(g_main_context_default(), FALSE);
+ g_timeout_add(0, &EntryPoint, loop);
+ g_main_loop_run(loop);
+ return 0;
+}
diff --git a/mock_file_writer.h b/mock_file_writer.h
new file mode 100644
index 0000000..e6d1612
--- /dev/null
+++ b/mock_file_writer.h
@@ -0,0 +1,59 @@
+// Copyright (c) 2009 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 UPDATE_ENGINE_MOCK_FILE_WRITER_H__
+#define UPDATE_ENGINE_MOCK_FILE_WRITER_H__
+
+#include "base/basictypes.h"
+#include "update_engine/file_writer.h"
+
+// MockFileWriter is an implementation of FileWriter. It will succeed
+// calls to Open(), Close(), but not do any work. All calls to Write()
+// will append the passed data to an internal vector.
+
+namespace chromeos_update_engine {
+
+class MockFileWriter : public FileWriter {
+ public:
+ MockFileWriter() : was_opened_(false), was_closed_(false) {}
+
+ virtual int Open(const char* path, int flags, mode_t mode) {
+ CHECK(!was_opened_);
+ CHECK(!was_closed_);
+ was_opened_ = true;
+ return 0;
+ }
+
+ virtual int Write(const void* bytes, size_t count) {
+ CHECK(was_opened_);
+ CHECK(!was_closed_);
+ const char* char_bytes = reinterpret_cast<const char*>(bytes);
+ bytes_.insert(bytes_.end(), char_bytes, char_bytes + count);
+ return count;
+ }
+
+ virtual int Close() {
+ CHECK(was_opened_);
+ CHECK(!was_closed_);
+ was_closed_ = true;
+ return 0;
+ }
+
+ const std::vector<char>& bytes() {
+ return bytes_;
+ }
+ private:
+ // The internal store of all bytes that have been written
+ std::vector<char> bytes_;
+
+ // These are just to ensure FileWriter methods are called properly.
+ bool was_opened_;
+ bool was_closed_;
+
+ DISALLOW_COPY_AND_ASSIGN(MockFileWriter);
+};
+
+} // namespace chromeos_update_engine
+
+#endif // UPDATE_ENGINE_MOCK_FILE_WRITER_H__
diff --git a/mock_http_fetcher.cc b/mock_http_fetcher.cc
new file mode 100644
index 0000000..0c4209a
--- /dev/null
+++ b/mock_http_fetcher.cc
@@ -0,0 +1,99 @@
+// Copyright (c) 2009 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 "glog/logging.h"
+#include "update_engine/mock_http_fetcher.h"
+
+// This is a mac implementation of HttpFetcher which is useful for testing.
+
+namespace chromeos_update_engine {
+
+MockHttpFetcher::~MockHttpFetcher() {
+ CHECK(!timeout_source_) << "Call TerminateTransfer() before dtor.";
+}
+
+void MockHttpFetcher::BeginTransfer(const std::string& url) {
+ if (sent_size_ < data_.size())
+ SendData(true);
+}
+
+// Returns false on one condition: If timeout_source_ was already set
+// and it needs to be deleted by the caller. If timeout_source_ is NULL
+// when this function is called, this function will always return true.
+bool MockHttpFetcher::SendData(bool skip_delivery) {
+ CHECK_LT(sent_size_, data_.size());
+ if (!skip_delivery) {
+ const size_t chunk_size = min(kMockHttpFetcherChunkSize,
+ data_.size() - sent_size_);
+ CHECK(delegate_);
+ delegate_->ReceivedBytes(this, &data_[sent_size_], chunk_size);
+ sent_size_ += chunk_size;
+ CHECK_LE(sent_size_, data_.size());
+ if (sent_size_ == data_.size()) {
+ // We've sent all the data. notify of success
+ delegate_->TransferComplete(this, true);
+ }
+ }
+
+ if (paused_) {
+ // If we're paused, we should return true if timeout_source_ is set,
+ // since we need the caller to delete it.
+ return timeout_source_;
+ }
+
+ if (timeout_source_) {
+ // we still need a timeout if there's more data to send
+ return sent_size_ < data_.size();
+ } else if (sent_size_ < data_.size()) {
+ // we don't have a timeout source and we need one
+ timeout_source_ = g_timeout_source_new(10);
+ CHECK(timeout_source_);
+ g_source_set_callback(timeout_source_, StaticTimeoutCallback, this,
+ NULL);
+ timout_tag_ = g_source_attach(timeout_source_, NULL);
+ }
+ return true;
+}
+
+bool MockHttpFetcher::TimeoutCallback() {
+ CHECK(!paused_);
+ bool ret = SendData(false);
+ if (false == ret) {
+ timeout_source_ = NULL;
+ }
+ return ret;
+}
+
+// If the transfer is in progress, aborts the transfer early.
+// The transfer cannot be resumed.
+void MockHttpFetcher::TerminateTransfer() {
+ sent_size_ = data_.size();
+ // kill any timeout
+ if (timeout_source_) {
+ g_source_remove(timout_tag_);
+ g_source_destroy(timeout_source_);
+ timeout_source_ = NULL;
+ }
+}
+
+void MockHttpFetcher::Pause() {
+ CHECK(!paused_);
+ paused_ = true;
+ if (timeout_source_) {
+ g_source_remove(timout_tag_);
+ g_source_destroy(timeout_source_);
+ timeout_source_ = NULL;
+ }
+
+}
+
+void MockHttpFetcher::Unpause() {
+ CHECK(paused_) << "You must pause before unpause.";
+ paused_ = false;
+ if (sent_size_ < data_.size()) {
+ SendData(false);
+ }
+}
+
+} // namespace chromeos_update_engine
diff --git a/mock_http_fetcher.h b/mock_http_fetcher.h
new file mode 100644
index 0000000..3dc0645
--- /dev/null
+++ b/mock_http_fetcher.h
@@ -0,0 +1,94 @@
+// Copyright (c) 2009 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 UPDATE_ENGINE_MOCK_HTTP_FETCHER_H__
+#define UPDATE_ENGINE_MOCK_HTTP_FETCHER_H__
+
+#include <vector>
+#include <glib.h>
+#include <glog/logging.h>
+#include "update_engine/http_fetcher.h"
+
+// This is a mock implementation of HttpFetcher which is useful for testing.
+// All data must be passed into the ctor. When started, MockHttpFetcher will
+// deliver the data in chunks of size kMockHttpFetcherChunkSize. To simulate
+// a network failure, you can call FailTransfer().
+
+namespace chromeos_update_engine {
+
+// MockHttpFetcher will send a chunk of data down in each call to BeginTransfer
+// and Unpause. For the other chunks of data, a callback is put on the run
+// loop and when that's called, another chunk is sent down.
+const size_t kMockHttpFetcherChunkSize(65536);
+
+class MockHttpFetcher : public HttpFetcher {
+ public:
+ // The data passed in here is copied and then passed to the delegate after
+ // the transfer begins.
+ MockHttpFetcher(const char* data, size_t size)
+ : sent_size_(0), timeout_source_(NULL), timout_tag_(0), paused_(false) {
+ data_.insert(data_.end(), data, data + size);
+ LOG(INFO) << "timeout_source_ = " << (int)timeout_source_;
+ }
+
+ // Cleans up all internal state. Does not notify delegate
+ ~MockHttpFetcher();
+
+ // Begins the transfer if it hasn't already begun.
+ virtual void BeginTransfer(const std::string& url);
+
+ // If the transfer is in progress, aborts the transfer early.
+ // The transfer cannot be resumed.
+ virtual void TerminateTransfer();
+
+ // Suspend the mock transfer.
+ virtual void Pause();
+
+ // Resume the mock transfer.
+ virtual void Unpause();
+
+ // Fail the transfer. This simulates a network failure.
+ void FailTransfer();
+
+ const std::vector<char>& post_data() const {
+ return post_data_;
+ }
+
+ private:
+ // Sends data to the delegate and sets up a glib timeout callback if needed.
+ // There must be a delegate and there must be data to send. If there is
+ // already a timeout callback, and it should be deleted by the caller,
+ // this will return false; otherwise true is returned.
+ // If skip_delivery is true, no bytes will be delivered, but the callbacks
+ // still still be set if needed
+ bool SendData(bool skip_delivery);
+
+ // Callback for when our glib main loop callback is called
+ bool TimeoutCallback();
+ static gboolean StaticTimeoutCallback(gpointer data) {
+ return reinterpret_cast<MockHttpFetcher*>(data)->TimeoutCallback();
+ }
+
+ // A full copy of the data we'll return to the delegate
+ std::vector<char> data_;
+
+ // The number of bytes we've sent so far
+ size_t sent_size_;
+
+ // The glib main loop timeout source. After each chunk of data sent, we
+ // time out for 0s just to make sure that run loop services other clients.
+ GSource* timeout_source_;
+
+ // ID of the timeout source, valid only if timeout_source_ != NULL
+ guint timout_tag_;
+
+ // True iff the fetcher is paused.
+ bool paused_;
+
+ DISALLOW_COPY_AND_ASSIGN(MockHttpFetcher);
+};
+
+} // namespace chromeos_update_engine
+
+#endif // UPDATE_ENGINE_MOCK_HTTP_FETCHER_H__
diff --git a/omaha_hash_calculator.cc b/omaha_hash_calculator.cc
new file mode 100644
index 0000000..41dcdee
--- /dev/null
+++ b/omaha_hash_calculator.cc
@@ -0,0 +1,67 @@
+// Copyright (c) 2009 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 <openssl/bio.h>
+#include <openssl/buffer.h>
+#include <openssl/evp.h>
+#include "glog/logging.h"
+#include "update_engine/omaha_hash_calculator.h"
+
+namespace chromeos_update_engine {
+
+OmahaHashCalculator::OmahaHashCalculator() {
+ CHECK_EQ(1, SHA1_Init(&ctx_));
+}
+
+// Update is called with all of the data that should be hashed in order.
+// Mostly just passes the data through to OpenSSL's SHA1_Update()
+void OmahaHashCalculator::Update(const char* data, size_t length) {
+ CHECK(hash_.empty()) << "Can't Update after hash is finalized";
+ COMPILE_ASSERT(sizeof(size_t) <= sizeof(unsigned long),
+ length_param_may_be_truncated_in_SHA1_Update);
+ CHECK_EQ(1, SHA1_Update(&ctx_, data, length));
+}
+
+// Call Finalize() when all data has been passed in. This mostly just
+// calls OpenSSL's SHA1_Final() and then base64 encodes the hash.
+void OmahaHashCalculator::Finalize() {
+ CHECK(hash_.empty()) << "Don't call Finalize() twice";
+ unsigned char md[SHA_DIGEST_LENGTH];
+ CHECK_EQ(1, SHA1_Final(md, &ctx_));
+
+ // Convert md to base64 encoding and store it in hash_
+ BIO *b64 = BIO_new(BIO_f_base64());
+ CHECK(b64);
+ BIO *bmem = BIO_new(BIO_s_mem());
+ CHECK(bmem);
+ b64 = BIO_push(b64, bmem);
+ CHECK_EQ(sizeof(md), BIO_write(b64, md, sizeof(md)));
+ CHECK_EQ(1, BIO_flush(b64));
+
+ BUF_MEM *bptr = NULL;
+ BIO_get_mem_ptr(b64, &bptr);
+ hash_.assign(bptr->data, bptr->length - 1);
+
+ BIO_free_all(b64);
+}
+
+std::string OmahaHashCalculator::OmahaHashOfBytes(
+ const void* data, size_t length) {
+ OmahaHashCalculator calc;
+ calc.Update(reinterpret_cast<const char*>(data), length);
+ calc.Finalize();
+ return calc.hash();
+}
+
+std::string OmahaHashCalculator::OmahaHashOfString(
+ const std::string& str) {
+ return OmahaHashOfBytes(str.data(), str.size());
+}
+
+std::string OmahaHashCalculator::OmahaHashOfData(
+ const std::vector<char>& data) {
+ return OmahaHashOfBytes(&data[0], data.size());
+}
+
+} // namespace chromeos_update_engine
diff --git a/omaha_hash_calculator.h b/omaha_hash_calculator.h
new file mode 100644
index 0000000..28602a3
--- /dev/null
+++ b/omaha_hash_calculator.h
@@ -0,0 +1,56 @@
+// Copyright (c) 2009 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 UPDATE_ENGINE_OMAHA_HASH_CALCULATOR_H__
+#define UPDATE_ENGINE_OMAHA_HASH_CALCULATOR_H__
+
+#include "base/basictypes.h"
+#include <string>
+#include <openssl/sha.h>
+
+// Omaha uses base64 encoded SHA-1 as the hash. This class provides a simple
+// wrapper around OpenSSL providing such a formatted hash of data passed in.
+// The methods of this class must be called in a very specific order:
+// First the ctor (of course), then 0 or more calls to Update(), then
+// Finalize(), then 0 or more calls to hash().
+
+namespace chromeos_update_engine {
+
+class OmahaHashCalculator {
+ public:
+ OmahaHashCalculator();
+
+ // Update is called with all of the data that should be hashed in order.
+ // Update will read |length| bytes of |data|
+ void Update(const char* data, size_t length);
+
+ // Call Finalize() when all data has been passed in. This method tells
+ // OpenSSl that no more data will come in and base64 encodes the resulting
+ // hash.
+ void Finalize();
+
+ // Gets the hash. Finalize() must have been called.
+ const std::string& hash() const {
+ CHECK(!hash_.empty()) << "Call Finalize() first";
+ return hash_;
+ }
+
+ // Used by tests
+ static std::string OmahaHashOfBytes(const void* data, size_t length);
+ static std::string OmahaHashOfString(const std::string& str);
+ static std::string OmahaHashOfData(const std::vector<char>& data);
+
+ private:
+ // If non-empty, the final base64 encoded hash. Will only be set to
+ // non-empty when Finalize is called.
+ std::string hash_;
+
+ // The hash state used by OpenSSL
+ SHA_CTX ctx_;
+ DISALLOW_COPY_AND_ASSIGN(OmahaHashCalculator);
+};
+
+} // namespace chromeos_update_engine
+
+#endif // UPDATE_ENGINE_OMAHA_HASH_CALCULATOR_H__
\ No newline at end of file
diff --git a/omaha_hash_calculator_unittest.cc b/omaha_hash_calculator_unittest.cc
new file mode 100644
index 0000000..0ee3b80
--- /dev/null
+++ b/omaha_hash_calculator_unittest.cc
@@ -0,0 +1,66 @@
+// Copyright (c) 2009 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 <math.h>
+#include <unistd.h>
+#include <glib.h>
+#include <gtest/gtest.h>
+#include "update_engine/libcurl_http_fetcher.h"
+#include "update_engine/omaha_hash_calculator.h"
+
+namespace chromeos_update_engine {
+
+class OmahaHashCalculatorTest : public ::testing::Test { };
+
+TEST(OmahaHashCalculatorTest, SimpleTest) {
+ OmahaHashCalculator calc;
+ calc.Update("hi", 2);
+ calc.Finalize();
+ // Generated by running this on a linux shell:
+ // $ echo -n hi | openssl sha1 -binary | openssl base64
+ EXPECT_EQ("witfkXg0JglCjW9RssWvTAveakI=", calc.hash());
+}
+
+TEST(OmahaHashCalculatorTest, MultiUpdateTest) {
+ OmahaHashCalculator calc;
+ calc.Update("h", 1);
+ calc.Update("i", 1);
+ calc.Finalize();
+ // Generated by running this on a linux shell:
+ // $ echo -n hi | openssl sha1 -binary | openssl base64
+ EXPECT_EQ("witfkXg0JglCjW9RssWvTAveakI=", calc.hash());
+}
+
+TEST(OmahaHashCalculatorTest, BigTest) {
+ OmahaHashCalculator calc;
+
+ for (int i = 0; i < 1000000; i++) {
+ char buf[8];
+ ASSERT_EQ(0 == i ? 1 : static_cast<int>(floorf(logf(i) / logf(10))) + 1,
+ snprintf(buf, sizeof(buf), "%d", i)) << " i = " << i;
+ calc.Update(buf, strlen(buf));
+ }
+ calc.Finalize();
+
+ // Hash constant generated by running this on a linux shell:
+ // $ C=0
+ // $ while [ $C -lt 1000000 ]; do
+ // echo -n $C
+ // let C=C+1
+ // done | openssl sha1 -binary | openssl base64
+ EXPECT_EQ("qdNsMeRqzoEUu5/ABi+MGRli87s=", calc.hash());
+}
+
+TEST(OmahaHashCalculatorTest, AbortTest) {
+ // Just make sure we don't crash and valgrind doesn't detect memory leaks
+ {
+ OmahaHashCalculator calc;
+ }
+ {
+ OmahaHashCalculator calc;
+ calc.Update("h", 1);
+ }
+}
+
+} // namespace chromeos_update_engine
diff --git a/test_http_server.py b/test_http_server.py
new file mode 100755
index 0000000..404e0e0
--- /dev/null
+++ b/test_http_server.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2009 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 is a simple HTTP server that's used by the
+# libcurl_http_fetcher_unittest, though it could be used by others. In
+# general, you can fork off this server, repeatedly request /test or
+# some URL until that URL succeeds; then you know the server is
+# running. The url /big returns 100,000 bytes of predictable data. The
+# url /quitquitquit causes the server to exit.
+
+import SimpleHTTPServer, BaseHTTPServer, httplib
+
+class TestHttpRequestHandler (SimpleHTTPServer.SimpleHTTPRequestHandler):
+ def do_GET(self):
+ # Exit the server
+ if self.path == '/quitquitquit':
+ self.server.stop = True
+
+ # Write 100,000 bytes out
+ if self.path == '/big':
+ self.send_response(200, 'OK')
+ self.send_header('Content-type', 'text/html')
+ self.end_headers()
+ for i in range(0, 10000):
+ try:
+ self.wfile.write('abcdefghij'); # 10 characters
+ except IOError:
+ return
+ return
+
+ # Everything else
+ self.send_response(200, 'OK')
+ self.send_header('Content-type', 'text/html')
+ self.end_headers()
+ self.wfile.write('unhandled path')
+
+class TestHttpServer (BaseHTTPServer.HTTPServer):
+ def serve_forever(self):
+ self.stop = False
+ while not self.stop:
+ self.handle_request()
+
+def main():
+ # TODO(adlr): Choose a port that works with build bots and report it to
+ # caller.
+ # WARNING, if you update this, you must also update http_fetcher_unittest.cc
+ port = 8080
+ server = TestHttpServer(('', 8080), TestHttpRequestHandler)
+ server.serve_forever()
+
+if __name__ == '__main__':
+ main()
diff --git a/test_utils.cc b/test_utils.cc
new file mode 100644
index 0000000..2791d44
--- /dev/null
+++ b/test_utils.cc
@@ -0,0 +1,73 @@
+// Copyright (c) 2009 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 <sys/stat.h>
+#include <sys/types.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string>
+#include <vector>
+#include "update_engine/test_utils.h"
+#include "glog/logging.h"
+
+#include "update_engine/file_writer.h"
+
+using std::string;
+using std::vector;
+
+namespace chromeos_update_engine {
+
+vector<char> ReadFile(const string& path) {
+ vector<char> ret;
+ FILE* fp = fopen(path.c_str(), "r");
+ if (!fp)
+ return ret;
+ const size_t kChunkSize(1024);
+ size_t read_size;
+ do {
+ char buf[kChunkSize];
+ read_size = fread(buf, 1, kChunkSize, fp);
+ ret.insert(ret.end(), buf, buf + read_size);
+ } while (read_size == kChunkSize);
+ fclose(fp);
+ return ret;
+}
+
+bool WriteFile(const std::string& path, const std::vector<char>& data) {
+ DirectFileWriter writer;
+ if (0 != writer.Open(path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644)) {
+ return false;
+ }
+ if (static_cast<int>(data.size()) != writer.Write(&data[0], data.size())) {
+ writer.Close();
+ return false;
+ }
+ writer.Close();
+ return true;
+}
+
+off_t FileSize(const string& path) {
+ struct stat stbuf;
+ int rc = stat(path.c_str(), &stbuf);
+ CHECK_EQ(0, rc);
+ if (rc < 0)
+ return rc;
+ return stbuf.st_size;
+}
+
+std::vector<char> GzipCompressData(const std::vector<char>& data) {
+ const char fname[] = "/tmp/GzipCompressDataTemp";
+ if (!WriteFile(fname, data)) {
+ system((string("rm ") + fname).c_str());
+ return vector<char>();
+ }
+ system((string("cat ") + fname + "|gzip>" + fname + ".gz").c_str());
+ system((string("rm ") + fname).c_str());
+ vector<char> ret = ReadFile(string(fname) + ".gz");
+ system((string("rm ") + fname + ".gz").c_str());
+ return ret;
+}
+
+} // namespace chromeos_update_engine
diff --git a/test_utils.h b/test_utils.h
new file mode 100644
index 0000000..0c12e13
--- /dev/null
+++ b/test_utils.h
@@ -0,0 +1,33 @@
+// Copyright (c) 2009 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 UPDATE_ENGINE_TEST_UTILS_H__
+#define UPDATE_ENGINE_TEST_UTILS_H__
+
+#include <vector>
+#include <string>
+
+// These are some handy functions for unittests.
+
+namespace chromeos_update_engine {
+
+// Returns the entire contents of the file at path. If the file doesn't
+// exist or error occurrs, an empty vector is returned.
+std::vector<char> ReadFile(const std::string& path);
+
+// Writes the data passed to path. The file at path will be overwritten if it
+// exists. Returns true on success, false otherwise.
+bool WriteFile(const std::string& path, const std::vector<char>& data);
+
+// Returns the size of the file at path. If the file doesn't exist or some
+// error occurrs, -1 is returned.
+off_t FileSize(const std::string& path);
+
+// Gzip compresses the data passed using the gzip command line program.
+// Returns compressed data back.
+std::vector<char> GzipCompressData(const std::vector<char>& data);
+
+} // namespace chromeos_update_engine
+
+#endif // UPDATE_ENGINE_TEST_UTILS_H__
\ No newline at end of file
diff --git a/testrunner.cc b/testrunner.cc
new file mode 100644
index 0000000..8931902
--- /dev/null
+++ b/testrunner.cc
@@ -0,0 +1,12 @@
+// Copyright (c) 2009 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.
+
+// based on pam_google_testrunner.cc
+
+#include <gtest/gtest.h>
+
+int main(int argc, char **argv) {
+ ::testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
diff --git a/update_check_action.cc b/update_check_action.cc
new file mode 100644
index 0000000..a4942a6
--- /dev/null
+++ b/update_check_action.cc
@@ -0,0 +1,281 @@
+// Copyright (c) 2009 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 "update_engine/update_check_action.h"
+#include <sstream>
+
+#include <libxml/parser.h>
+#include <libxml/xpath.h>
+#include <libxml/xpathInternals.h>
+
+#include "update_engine/action_pipe.h"
+
+using std::string;
+
+namespace chromeos_update_engine {
+
+const char* const UpdateCheckParams::kAppId(
+ "87efface-864d-49a5-9bb3-4b050a7c227a");
+const char* const UpdateCheckParams::kOsPlatform("Chrome OS");
+const char* const UpdateCheckParams::kOsVersion("Indy");
+
+namespace {
+
+const string kGupdateVersion("ChromeOSUpdateEngine-0.1.0.0");
+
+// This is handy for passing strings into libxml2
+#define ConstXMLStr(x) (reinterpret_cast<const xmlChar*>(x))
+
+// These are for scoped_ptr_malloc, which is like scoped_ptr, but allows
+// a custom free() function to be specified.
+class ScopedPtrXmlDocFree {
+ public:
+ inline void operator()(void* x) const {
+ xmlFreeDoc(reinterpret_cast<xmlDoc*>(x));
+ }
+};
+class ScopedPtrXmlFree {
+ public:
+ inline void operator()(void* x) const {
+ xmlFree(x);
+ }
+};
+class ScopedPtrXmlXPathObjectFree {
+ public:
+ inline void operator()(void* x) const {
+ xmlXPathFreeObject(reinterpret_cast<xmlXPathObject*>(x));
+ }
+};
+class ScopedPtrXmlXPathContextFree {
+ public:
+ inline void operator()(void* x) const {
+ xmlXPathFreeContext(reinterpret_cast<xmlXPathContext*>(x));
+ }
+};
+
+// Returns a properly formatted omaha request for an update check
+string FormatRequest(const UpdateCheckParams& params) {
+ return string("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
+ "<o:gupdate xmlns:o=\"http://www.google.com/update2/request\" "
+ "version=\"" + XmlEncode(kGupdateVersion) + "\" protocol=\"2.0\" "
+ "machineid=\"") + XmlEncode(params.machine_id) + "\" ismachine=\"1\" "
+ "userid=\"" + XmlEncode(params.user_id) + "\">\n"
+ " <o:os version=\"" + XmlEncode(params.os_version) + "\" platform=\"" +
+ XmlEncode(params.os_platform) + "\" sp=\"" +
+ XmlEncode(params.os_sp) + "\"></o:os>\n"
+ " <o:app appid=\"" + XmlEncode(params.app_id) + "\" version=\"" +
+ XmlEncode(params.app_version) + "\" "
+ "lang=\"" + XmlEncode(params.app_lang) + "\" track=\"" +
+ XmlEncode(params.app_track) + "\">\n"
+ " <o:ping active=\"0\"></o:ping>\n"
+ " <o:updatecheck></o:updatecheck>\n"
+ " </o:app>\n"
+ "</o:gupdate>\n";
+}
+} // namespace {}
+
+// Encodes XML entities in a given string with libxml2. input must be
+// UTF-8 formatted. Output will be UTF-8 formatted.
+string XmlEncode(const string& input) {
+// // TODO(adlr): if allocating a new xmlDoc each time is taking up too much
+// // cpu, considering creating one and caching it.
+// scoped_ptr_malloc<xmlDoc, ScopedPtrXmlDocFree> xml_doc(
+// xmlNewDoc(ConstXMLStr("1.0")));
+// if (!xml_doc.get()) {
+// LOG(ERROR) << "Unable to create xmlDoc";
+// return "";
+// }
+ scoped_ptr_malloc<xmlChar, ScopedPtrXmlFree> str(
+ xmlEncodeEntitiesReentrant(NULL, ConstXMLStr(input.c_str())));
+ return string(reinterpret_cast<const char *>(str.get()));
+}
+
+UpdateCheckAction::UpdateCheckAction(const UpdateCheckParams& params,
+ HttpFetcher* http_fetcher)
+ : params_(params), http_fetcher_(http_fetcher) {}
+
+UpdateCheckAction::~UpdateCheckAction() {}
+
+void UpdateCheckAction::PerformAction() {
+ http_fetcher_->set_delegate(this);
+ string request_post(FormatRequest(params_));
+ http_fetcher_->SetPostData(request_post.data(), request_post.size());
+ http_fetcher_->BeginTransfer("https://tools.google.com/service/update2");
+}
+
+void UpdateCheckAction::TerminateProcessing() {
+ http_fetcher_->TerminateTransfer();
+}
+
+// We just store the response in the buffer. Once we've received all bytes,
+// we'll look in the buffer and decide what to do.
+void UpdateCheckAction::ReceivedBytes(HttpFetcher *fetcher,
+ const char* bytes,
+ int length) {
+ response_buffer_.reserve(response_buffer_.size() + length);
+ response_buffer_.insert(response_buffer_.end(), bytes, bytes + length);
+}
+
+namespace {
+// A little object to call ActionComplete on the ActionProcessor when
+// it's destructed.
+class ScopedActionCompleter {
+ public:
+ explicit ScopedActionCompleter(ActionProcessor* processor,
+ AbstractAction* action)
+ : processor_(processor), action_(action), success_(false) {}
+ ~ScopedActionCompleter() {
+ processor_->ActionComplete(action_, success_);
+ }
+ void set_success(bool success) {
+ success_ = success;
+ }
+ private:
+ ActionProcessor* processor_;
+ AbstractAction* action_;
+ bool success_;
+ DISALLOW_COPY_AND_ASSIGN(ScopedActionCompleter);
+};
+
+// If non-NULL response, caller is responsible for calling xmlXPathFreeObject()
+// on the returned object.
+// This code is roughly based on the libxml tutorial at:
+// http://xmlsoft.org/tutorial/apd.html
+xmlXPathObject* GetNodeSet(xmlDoc* doc, const xmlChar* xpath,
+ const xmlChar* ns, const xmlChar* ns_url) {
+ xmlXPathObject* result = NULL;
+
+ scoped_ptr_malloc<xmlXPathContext, ScopedPtrXmlXPathContextFree> context(
+ xmlXPathNewContext(doc));
+ if (!context.get()) {
+ LOG(ERROR) << "xmlXPathNewContext() returned NULL";
+ return NULL;
+ }
+ if (xmlXPathRegisterNs(context.get(), ns, ns_url) < 0) {
+ LOG(ERROR) << "xmlXPathRegisterNs() returned error";
+ return NULL;
+ }
+
+ result = xmlXPathEvalExpression(xpath, context.get());
+
+ if (result == NULL) {
+ LOG(ERROR) << "xmlXPathEvalExpression returned error";
+ return NULL;
+ }
+ if(xmlXPathNodeSetIsEmpty(result->nodesetval)){
+ LOG(INFO) << "xpath not found in doc";
+ xmlXPathFreeObject(result);
+ return NULL;
+ }
+ return result;
+}
+
+// Returns the string value of a named attribute on a node, or empty string
+// if no such node exists. If the attribute exists and has a value of
+// empty string, there's no way to distinguish that from the attribute
+// not existing.
+string XmlGetProperty(xmlNode* node, const char* name) {
+ if (!xmlHasProp(node, ConstXMLStr(name)))
+ return "";
+ scoped_ptr_malloc<xmlChar, ScopedPtrXmlFree> str(
+ xmlGetProp(node, ConstXMLStr(name)));
+ string ret(reinterpret_cast<const char *>(str.get()));
+ return ret;
+}
+
+// Parses a 64 bit base-10 int from a string and returns it. Returns 0
+// on error. If the string contains "0", that's indistinguishable from
+// error.
+off_t ParseInt(const string& str) {
+ off_t ret = 0;
+
+ int rc = sscanf(str.c_str(), "%lld", &ret);
+ if (rc < 1) {
+ // failure
+ return 0;
+ }
+ return ret;
+}
+} // namespace {}
+
+// If the transfer was successful, this uses libxml2 to parse the response
+// and fill in the appropriate fields of the output object. Also, notifies
+// the processor that we're done.
+void UpdateCheckAction::TransferComplete(HttpFetcher *fetcher,
+ bool successful) {
+ ScopedActionCompleter completer(processor_, this);
+ if (!successful)
+ return;
+ if (!HasOutputPipe()) {
+ // Just set success to whether or not the http transfer succeeded,
+ // which must be true at this point in the code.
+ completer.set_success(true);
+ return;
+ }
+
+ // parse our response and fill the fields in the output object
+ scoped_ptr_malloc<xmlDoc, ScopedPtrXmlDocFree> doc(
+ xmlParseMemory(&response_buffer_[0], response_buffer_.size()));
+ if (!doc.get()) {
+ LOG(ERROR) << "Omaha response not valid XML";
+ return;
+ }
+
+ static const char* kNamespace("x");
+ static const char* kUpdatecheckNodeXpath("/x:gupdate/x:app/x:updatecheck");
+ static const char* kNsUrl("http://www.google.com/update2/response");
+
+ scoped_ptr_malloc<xmlXPathObject, ScopedPtrXmlXPathObjectFree>
+ xpath_nodeset(GetNodeSet(doc.get(),
+ ConstXMLStr(kUpdatecheckNodeXpath),
+ ConstXMLStr(kNamespace),
+ ConstXMLStr(kNsUrl)));
+ if (!xpath_nodeset.get()) {
+ return;
+ }
+ xmlNodeSet* nodeset = xpath_nodeset->nodesetval;
+ CHECK(nodeset) << "XPath missing NodeSet";
+ CHECK_GE(nodeset->nodeNr, 1);
+
+ xmlNode* updatecheck_node = nodeset->nodeTab[0];
+ // get status
+ if (!xmlHasProp(updatecheck_node, ConstXMLStr("status"))) {
+ LOG(ERROR) << "Response missing status";
+ return;
+ }
+
+ const string status(XmlGetProperty(updatecheck_node, "status"));
+ UpdateCheckResponse output_object;
+ if (status == "noupdate") {
+ LOG(INFO) << "No update.";
+ output_object.update_exists = false;
+ SetOutputObject(output_object);
+ completer.set_success(true);
+ return;
+ }
+
+ if (status != "ok") {
+ LOG(ERROR) << "Unknown status: " << status;
+ return;
+ }
+
+ // In best-effort fashion, fetch the rest of the expected attributes
+ // from the updatecheck node, then return the object
+ output_object.update_exists = true;
+ completer.set_success(true);
+
+ output_object.display_version =
+ XmlGetProperty(updatecheck_node, "DisplayVersion");
+ output_object.codebase = XmlGetProperty(updatecheck_node, "codebase");
+ output_object.more_info_url = XmlGetProperty(updatecheck_node, "MoreInfo");
+ output_object.hash = XmlGetProperty(updatecheck_node, "hash");
+ output_object.size = ParseInt(XmlGetProperty(updatecheck_node, "size"));
+ output_object.needs_admin =
+ XmlGetProperty(updatecheck_node, "needsadmin") == "true";
+ output_object.prompt = XmlGetProperty(updatecheck_node, "Prompt") == "true";
+ SetOutputObject(output_object);
+ return;
+}
+
+}; // namespace chromeos_update_engine
diff --git a/update_check_action.h b/update_check_action.h
new file mode 100644
index 0000000..cf6d512
--- /dev/null
+++ b/update_check_action.h
@@ -0,0 +1,142 @@
+// Copyright (c) 2009 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 UPDATE_ENGINE_UPDATE_CHECK_ACTION_H__
+#define UPDATE_ENGINE_UPDATE_CHECK_ACTION_H__
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+
+#include <string>
+
+#include <curl/curl.h>
+
+#include "base/scoped_ptr.h"
+#include "action.h"
+#include "http_fetcher.h"
+
+// The Update Check action makes an update check request to Omaha and
+// can output the response on the output ActionPipe.
+
+using std::string;
+
+namespace chromeos_update_engine {
+
+// Encodes XML entities in a given string with libxml2. input must be
+// UTF-8 formatted. Output will be UTF-8 formatted.
+std::string XmlEncode(const std::string& input);
+
+// This struct encapsulates the data Omaha gets for the update check.
+// These strings in this struct should not be XML escaped.
+struct UpdateCheckParams {
+ UpdateCheckParams()
+ : os_platform(kOsPlatform), os_version(kOsVersion), app_id(kAppId) {}
+ UpdateCheckParams(const std::string& in_machine_id,
+ const std::string& in_user_id,
+ const std::string& in_os_platform,
+ const std::string& in_os_version,
+ const std::string& in_os_sp,
+ const std::string& in_app_id,
+ const std::string& in_app_version,
+ const std::string& in_app_lang,
+ const std::string& in_app_track)
+ : machine_id(in_machine_id),
+ user_id(in_user_id),
+ os_platform(in_os_platform),
+ os_version(in_os_version),
+ os_sp(in_os_sp),
+ app_id(in_app_id),
+ app_version(in_app_version),
+ app_lang(in_app_lang),
+ app_track(in_app_track) {}
+
+ std::string machine_id;
+ std::string user_id;
+ std::string os_platform;
+ std::string os_version;
+ std::string os_sp;
+ std::string app_id;
+ std::string app_version;
+ std::string app_lang;
+ std::string app_track;
+
+ // Suggested defaults
+ static const char* const kAppId;
+ static const char* const kOsPlatform;
+ static const char* const kOsVersion;
+};
+
+// This struct encapsulates the data Omaha returns for the update check.
+// These strings in this struct are not XML escaped.
+struct UpdateCheckResponse {
+ UpdateCheckResponse()
+ : update_exists(false), size(0), needs_admin(false), prompt(false) {}
+ // True iff there is an update to be downloaded.
+ bool update_exists;
+
+ // These are only valid if update_exists is true:
+ std::string display_version;
+ std::string codebase;
+ std::string more_info_url;
+ std::string hash;
+ off_t size;
+ bool needs_admin;
+ bool prompt;
+};
+COMPILE_ASSERT(sizeof(off_t) == 8, off_t_not_64bit);
+
+class UpdateCheckAction;
+class NoneType;
+
+template<>
+class ActionTraits<UpdateCheckAction> {
+ public:
+ // Does not take an object for input
+ typedef NoneType InputObjectType;
+ // On success, puts the output path on output
+ typedef UpdateCheckResponse OutputObjectType;
+};
+
+class UpdateCheckAction : public Action<UpdateCheckAction>,
+ public HttpFetcherDelegate {
+ public:
+ // The ctor takes in all the parameters that will be used for
+ // making the request to Omaha. For some of them we have constants
+ // that should be used.
+ // Takes ownership of the passed in HttpFetcher. Useful for testing.
+ // A good calling pattern is:
+ // UpdateCheckAction(..., new WhateverHttpFetcher);
+ UpdateCheckAction(const UpdateCheckParams& params,
+ HttpFetcher* http_fetcher);
+ virtual ~UpdateCheckAction();
+ typedef ActionTraits<UpdateCheckAction>::InputObjectType InputObjectType;
+ typedef ActionTraits<UpdateCheckAction>::OutputObjectType OutputObjectType;
+ void PerformAction();
+ void TerminateProcessing();
+
+ // Debugging/logging
+ std::string Type() const { return "UpdateCheckAction"; }
+
+ // Delegate methods (see http_fetcher.h)
+ virtual void ReceivedBytes(HttpFetcher *fetcher,
+ const char* bytes, int length);
+ virtual void TransferComplete(HttpFetcher *fetcher, bool successful);
+
+ private:
+ // These are data that are passed in the request to the Omaha server
+ UpdateCheckParams params_;
+
+ // pointer to the HttpFetcher that does the http work
+ scoped_ptr<HttpFetcher> http_fetcher_;
+
+ // Stores the response from the omaha server
+ std::vector<char> response_buffer_;
+
+ DISALLOW_COPY_AND_ASSIGN(UpdateCheckAction);
+};
+
+} // namespace chromeos_update_engine
+
+#endif // UPDATE_ENGINE_UPDATE_CHECK_ACTION_H__
diff --git a/update_check_action_unittest.cc b/update_check_action_unittest.cc
new file mode 100644
index 0000000..9b8436c
--- /dev/null
+++ b/update_check_action_unittest.cc
@@ -0,0 +1,492 @@
+// Copyright (c) 2009 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 <string>
+#include <glib.h>
+#include <gtest/gtest.h>
+#include "update_engine/action_pipe.h"
+#include "update_engine/update_check_action.h"
+#include "update_engine/mock_http_fetcher.h"
+#include "update_engine/omaha_hash_calculator.h"
+#include "update_engine/test_utils.h"
+
+namespace chromeos_update_engine {
+
+using std::string;
+
+class UpdateCheckActionTest : public ::testing::Test { };
+
+namespace {
+string GetNoUpdateResponse(const string& app_id) {
+ return string(
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?><gupdate "
+ "xmlns=\"http://www.google.com/update2/response\" protocol=\"2.0\"><app "
+ "appid=\"") + app_id + "\" status=\"ok\"><ping "
+ "status=\"ok\"/><updatecheck status=\"noupdate\"/></app></gupdate>";
+}
+
+string GetUpdateResponse(const string& app_id,
+ const string& display_version,
+ const string& more_info_url,
+ const string& prompt,
+ const string& codebase,
+ const string& hash,
+ const string& needsadmin,
+ const string& size) {
+ return string("<?xml version=\"1.0\" encoding=\"UTF-8\"?><gupdate "
+ "xmlns=\"http://www.google.com/update2/response\" protocol=\"2.0\"><app "
+ "appid=\"") + app_id + "\" status=\"ok\"><ping "
+ "status=\"ok\"/><updatecheck DisplayVersion=\"" + display_version + "\" "
+ "MoreInfo=\"" + more_info_url + "\" Prompt=\"" + prompt + "\" "
+ "codebase=\"" + codebase + "\" "
+ "hash=\"" + hash + "\" needsadmin=\"" + needsadmin + "\" "
+ "size=\"" + size + "\" status=\"ok\"/></app></gupdate>";
+}
+
+class UpdateCheckActionTestProcessorDelegate : public ActionProcessorDelegate {
+ public:
+ UpdateCheckActionTestProcessorDelegate()
+ : loop_(NULL),
+ expected_success_(true) {}
+ virtual ~UpdateCheckActionTestProcessorDelegate() {
+ }
+ virtual void ProcessingDone(const ActionProcessor* processor) {
+ ASSERT_TRUE(loop_);
+ g_main_loop_quit(loop_);
+ }
+
+ virtual void ActionCompleted(const ActionProcessor* processor,
+ const AbstractAction* action,
+ bool success) {
+ // make sure actions always succeed
+ if (action->Type() == "UpdateCheckAction")
+ EXPECT_EQ(expected_success_, success);
+ else
+ EXPECT_TRUE(success);
+ }
+ GMainLoop *loop_;
+ bool expected_success_;
+};
+
+gboolean StartProcessorInRunLoop(gpointer data) {
+ ActionProcessor *processor = reinterpret_cast<ActionProcessor*>(data);
+ processor->StartProcessing();
+ return FALSE;
+}
+
+} // namespace {}
+
+class OutputObjectCollectorAction;
+
+template<>
+class ActionTraits<OutputObjectCollectorAction> {
+ public:
+ // Does not take an object for input
+ typedef UpdateCheckResponse InputObjectType;
+ // On success, puts the output path on output
+ typedef NoneType OutputObjectType;
+};
+
+class OutputObjectCollectorAction : public Action<OutputObjectCollectorAction> {
+ public:
+ void PerformAction() {
+ // copy input object
+ has_input_object_ = HasInputObject();
+ if (has_input_object_)
+ update_check_response_ = GetInputObject();
+ processor_->ActionComplete(this, true);
+ }
+ // Should never be called
+ void TerminateProcessing() {
+ CHECK(false);
+ }
+ // Debugging/logging
+ std::string Type() const { return "OutputObjectCollectorAction"; }
+ bool has_input_object_;
+ UpdateCheckResponse update_check_response_;
+};
+
+// returns true iff an output response was obtained from the
+// UpdateCheckAction. out_response may be NULL.
+// out_post_data may be null; if non-null, the post-data received by the
+// mock HttpFetcher is returned.
+bool TestUpdateCheckAction(const UpdateCheckParams& params,
+ const string& http_response,
+ bool expected_success,
+ UpdateCheckResponse* out_response,
+ vector<char> *out_post_data) {
+ GMainLoop *loop = g_main_loop_new(g_main_context_default(), FALSE);
+ MockHttpFetcher *fetcher = new MockHttpFetcher(http_response.data(),
+ http_response.size());
+
+ UpdateCheckAction action(params, fetcher); // takes ownership of fetcher
+ UpdateCheckActionTestProcessorDelegate delegate;
+ delegate.loop_ = loop;
+ delegate.expected_success_ = expected_success;
+ ActionProcessor processor;
+ processor.set_delegate(&delegate);
+ processor.EnqueueAction(&action);
+
+ OutputObjectCollectorAction collector_action;
+
+ BondActions(&action, &collector_action);
+ processor.EnqueueAction(&collector_action);
+
+ g_timeout_add(0, &StartProcessorInRunLoop, &processor);
+ g_main_loop_run(loop);
+ g_main_loop_unref(loop);
+ if (collector_action.has_input_object_ && out_response)
+ *out_response = collector_action.update_check_response_;
+ if (out_post_data)
+ *out_post_data = fetcher->post_data();
+ return collector_action.has_input_object_;
+}
+
+TEST(UpdateCheckActionTest, NoUpdateTest) {
+ UpdateCheckParams params("", // machine_id
+ "", // user_id
+ UpdateCheckParams::kOsPlatform,
+ UpdateCheckParams::kOsVersion,
+ "", // os_sp
+ UpdateCheckParams::kAppId,
+ "0.1.0.0",
+ "en-US",
+ "unittest");
+ UpdateCheckResponse response;
+ ASSERT_TRUE(
+ TestUpdateCheckAction(params,
+ GetNoUpdateResponse(UpdateCheckParams::kAppId),
+ true,
+ &response,
+ NULL));
+ EXPECT_FALSE(response.update_exists);
+}
+
+TEST(UpdateCheckActionTest, ValidUpdateTest) {
+ UpdateCheckParams params("machine_id",
+ "user_id",
+ UpdateCheckParams::kOsPlatform,
+ UpdateCheckParams::kOsVersion,
+ "service_pack",
+ UpdateCheckParams::kAppId,
+ "0.1.0.0",
+ "en-US",
+ "unittest_track");
+ UpdateCheckResponse response;
+ ASSERT_TRUE(
+ TestUpdateCheckAction(params,
+ GetUpdateResponse(UpdateCheckParams::kAppId,
+ "1.2.3.4", // version
+ "http://more/info",
+ "true", // prompt
+ "http://code/base", // dl url
+ "HASH1234=", // checksum
+ "false", // needs admin
+ "123"), // size
+ true,
+ &response,
+ NULL));
+ EXPECT_TRUE(response.update_exists);
+ EXPECT_EQ("1.2.3.4", response.display_version);
+ EXPECT_EQ("http://code/base", response.codebase);
+ EXPECT_EQ("http://more/info", response.more_info_url);
+ EXPECT_EQ("HASH1234=", response.hash);
+ EXPECT_EQ(123, response.size);
+ EXPECT_FALSE(response.needs_admin);
+ EXPECT_TRUE(response.prompt);
+}
+
+TEST(UpdateCheckActionTest, NoOutputPipeTest) {
+ UpdateCheckParams params("", // machine_id
+ "", // usr_id
+ UpdateCheckParams::kOsPlatform,
+ UpdateCheckParams::kOsVersion,
+ "", // os_sp
+ UpdateCheckParams::kAppId,
+ "0.1.0.0",
+ "en-US",
+ "unittest");
+ const string http_response(GetNoUpdateResponse(UpdateCheckParams::kAppId));
+
+ GMainLoop *loop = g_main_loop_new(g_main_context_default(), FALSE);
+
+ UpdateCheckAction action(params,
+ new MockHttpFetcher(http_response.data(),
+ http_response.size()));
+ UpdateCheckActionTestProcessorDelegate delegate;
+ delegate.loop_ = loop;
+ ActionProcessor processor;
+ processor.set_delegate(&delegate);
+ processor.EnqueueAction(&action);
+
+ g_timeout_add(0, &StartProcessorInRunLoop, &processor);
+ g_main_loop_run(loop);
+ g_main_loop_unref(loop);
+ EXPECT_FALSE(processor.IsRunning());
+}
+
+TEST(UpdateCheckActionTest, InvalidXmlTest) {
+ UpdateCheckParams params("machine_id",
+ "user_id",
+ UpdateCheckParams::kOsPlatform,
+ UpdateCheckParams::kOsVersion,
+ "service_pack",
+ UpdateCheckParams::kAppId,
+ "0.1.0.0",
+ "en-US",
+ "unittest_track");
+ UpdateCheckResponse response;
+ ASSERT_TRUE(
+ TestUpdateCheckAction(params,
+ "invalid xml>",
+ false,
+ &response,
+ NULL));
+ EXPECT_FALSE(response.update_exists);
+}
+
+TEST(UpdateCheckActionTest, MissingStatusTest) {
+ UpdateCheckParams params("machine_id",
+ "user_id",
+ UpdateCheckParams::kOsPlatform,
+ UpdateCheckParams::kOsVersion,
+ "service_pack",
+ UpdateCheckParams::kAppId,
+ "0.1.0.0",
+ "en-US",
+ "unittest_track");
+ UpdateCheckResponse response;
+ ASSERT_TRUE(TestUpdateCheckAction(
+ params,
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?><gupdate "
+ "xmlns=\"http://www.google.com/update2/response\" protocol=\"2.0\"><app "
+ "appid=\"foo\" status=\"ok\"><ping "
+ "status=\"ok\"/><updatecheck/></app></gupdate>",
+ false,
+ &response,
+ NULL));
+ EXPECT_FALSE(response.update_exists);
+}
+
+TEST(UpdateCheckActionTest, InvalidStatusTest) {
+ UpdateCheckParams params("machine_id",
+ "user_id",
+ UpdateCheckParams::kOsPlatform,
+ UpdateCheckParams::kOsVersion,
+ "service_pack",
+ UpdateCheckParams::kAppId,
+ "0.1.0.0",
+ "en-US",
+ "unittest_track");
+ UpdateCheckResponse response;
+ ASSERT_TRUE(TestUpdateCheckAction(
+ params,
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?><gupdate "
+ "xmlns=\"http://www.google.com/update2/response\" protocol=\"2.0\"><app "
+ "appid=\"foo\" status=\"ok\"><ping "
+ "status=\"ok\"/><updatecheck status=\"foo\"/></app></gupdate>",
+ false,
+ &response,
+ NULL));
+ EXPECT_FALSE(response.update_exists);
+}
+
+TEST(UpdateCheckActionTest, MissingNodesetTest) {
+ UpdateCheckParams params("machine_id",
+ "user_id",
+ UpdateCheckParams::kOsPlatform,
+ UpdateCheckParams::kOsVersion,
+ "service_pack",
+ UpdateCheckParams::kAppId,
+ "0.1.0.0",
+ "en-US",
+ "unittest_track");
+ UpdateCheckResponse response;
+ ASSERT_TRUE(TestUpdateCheckAction(
+ params,
+ "<?xml version=\"1.0\" encoding=\"UTF-8\"?><gupdate "
+ "xmlns=\"http://www.google.com/update2/response\" protocol=\"2.0\"><app "
+ "appid=\"foo\" status=\"ok\"><ping "
+ "status=\"ok\"/></app></gupdate>",
+ false,
+ &response,
+ NULL));
+ EXPECT_FALSE(response.update_exists);
+}
+
+TEST(UpdateCheckActionTest, MissingFieldTest) {
+ UpdateCheckParams params("machine_id",
+ "user_id",
+ UpdateCheckParams::kOsPlatform,
+ UpdateCheckParams::kOsVersion,
+ "service_pack",
+ UpdateCheckParams::kAppId,
+ "0.1.0.0",
+ "en-US",
+ "unittest_track");
+ UpdateCheckResponse response;
+ ASSERT_TRUE(TestUpdateCheckAction(params,
+ string("<?xml version=\"1.0\" encoding=\"UTF-8\"?><gupdate "
+ "xmlns=\"http://www.google.com/update2/response\" "
+ "protocol=\"2.0\"><app appid=\"") +
+ UpdateCheckParams::kAppId + "\" status=\"ok\"><ping "
+ "status=\"ok\"/><updatecheck DisplayVersion=\"1.2.3.4\" "
+ "Prompt=\"false\" "
+ "codebase=\"http://code/base\" "
+ "hash=\"HASH1234=\" needsadmin=\"true\" "
+ "size=\"123\" status=\"ok\"/></app></gupdate>",
+ true,
+ &response,
+ NULL));
+ EXPECT_TRUE(response.update_exists);
+ EXPECT_EQ("1.2.3.4", response.display_version);
+ EXPECT_EQ("http://code/base", response.codebase);
+ EXPECT_EQ("", response.more_info_url);
+ EXPECT_EQ("HASH1234=", response.hash);
+ EXPECT_EQ(123, response.size);
+ EXPECT_TRUE(response.needs_admin);
+ EXPECT_FALSE(response.prompt);
+}
+
+namespace {
+class TerminateEarlyTestProcessorDelegate : public ActionProcessorDelegate {
+ public:
+ void ProcessingStopped(const ActionProcessor* processor) {
+ ASSERT_TRUE(loop_);
+ g_main_loop_quit(loop_);
+ }
+ GMainLoop *loop_;
+};
+
+gboolean TerminateTransferTestStarter(gpointer data) {
+ ActionProcessor *processor = reinterpret_cast<ActionProcessor*>(data);
+ processor->StartProcessing();
+ CHECK(processor->IsRunning());
+ processor->StopProcessing();
+ return FALSE;
+}
+} // namespace {}
+
+TEST(UpdateCheckActionTest, TerminateTransferTest) {
+ UpdateCheckParams params("", // machine_id
+ "", // usr_id
+ UpdateCheckParams::kOsPlatform,
+ UpdateCheckParams::kOsVersion,
+ "", // os_sp
+ UpdateCheckParams::kAppId,
+ "0.1.0.0",
+ "en-US",
+ "unittest");
+ string http_response("doesn't matter");
+ GMainLoop *loop = g_main_loop_new(g_main_context_default(), FALSE);
+
+ UpdateCheckAction action(params,
+ new MockHttpFetcher(http_response.data(),
+ http_response.size()));
+ TerminateEarlyTestProcessorDelegate delegate;
+ delegate.loop_ = loop;
+ ActionProcessor processor;
+ processor.set_delegate(&delegate);
+ processor.EnqueueAction(&action);
+
+ g_timeout_add(0, &TerminateTransferTestStarter, &processor);
+ g_main_loop_run(loop);
+ g_main_loop_unref(loop);
+}
+
+TEST(UpdateCheckActionTest, XmlEncodeTest) {
+ EXPECT_EQ("ab", XmlEncode("ab"));
+ EXPECT_EQ("a<b", XmlEncode("a<b"));
+ EXPECT_EQ("foo-Ω", XmlEncode("foo-\xce\xa9"));
+ EXPECT_EQ("<&>", XmlEncode("<&>"));
+ EXPECT_EQ("&lt;&amp;&gt;", XmlEncode("<&>"));
+
+ vector<char> post_data;
+
+ // Make sure XML Encode is being called on the params
+ UpdateCheckParams params("testthemachine<id",
+ "testtheuser_id<",
+ UpdateCheckParams::kOsPlatform,
+ UpdateCheckParams::kOsVersion,
+ "testtheservice_pack>",
+ UpdateCheckParams::kAppId,
+ "0.1.0.0",
+ "en-US",
+ "unittest_track");
+ UpdateCheckResponse response;
+ ASSERT_TRUE(
+ TestUpdateCheckAction(params,
+ "invalid xml>",
+ false,
+ &response,
+ &post_data));
+ // convert post_data to string
+ string post_str(&post_data[0], post_data.size());
+ EXPECT_NE(post_str.find("testthemachine<id"), string::npos);
+ EXPECT_EQ(post_str.find("testthemachine<id"), string::npos);
+ EXPECT_NE(post_str.find("testtheuser_id&lt;"), string::npos);
+ EXPECT_EQ(post_str.find("testtheuser_id<"), string::npos);
+ EXPECT_NE(post_str.find("testtheservice_pack>"), string::npos);
+ EXPECT_EQ(post_str.find("testtheservice_pack>"), string::npos);
+}
+
+TEST(UpdateCheckActionTest, XmlDecodeTest) {
+ UpdateCheckParams params("machine_id",
+ "user_id",
+ UpdateCheckParams::kOsPlatform,
+ UpdateCheckParams::kOsVersion,
+ "service_pack",
+ UpdateCheckParams::kAppId,
+ "0.1.0.0",
+ "en-US",
+ "unittest_track");
+ UpdateCheckResponse response;
+ ASSERT_TRUE(
+ TestUpdateCheckAction(params,
+ GetUpdateResponse(UpdateCheckParams::kAppId,
+ "1.2.3.4", // version
+ "testthe<url", // more info
+ "true", // prompt
+ "testthe&codebase", // dl url
+ "HASH1234=", // checksum
+ "false", // needs admin
+ "123"), // size
+ true,
+ &response,
+ NULL));
+
+ EXPECT_EQ(response.more_info_url, "testthe<url");
+ EXPECT_EQ(response.codebase, "testthe&codebase");
+}
+
+TEST(UpdateCheckActionTest, ParseIntTest) {
+ UpdateCheckParams params("machine_id",
+ "user_id",
+ UpdateCheckParams::kOsPlatform,
+ UpdateCheckParams::kOsVersion,
+ "service_pack",
+ UpdateCheckParams::kAppId,
+ "0.1.0.0",
+ "en-US",
+ "unittest_track");
+ UpdateCheckResponse response;
+ ASSERT_TRUE(
+ TestUpdateCheckAction(params,
+ GetUpdateResponse(UpdateCheckParams::kAppId,
+ "1.2.3.4", // version
+ "theurl", // more info
+ "true", // prompt
+ "thecodebase", // dl url
+ "HASH1234=", // checksum
+ "false", // needs admin
+ // overflows int32:
+ "123123123123123"), // size
+ true,
+ &response,
+ NULL));
+
+ EXPECT_EQ(response.size, 123123123123123ll);
+}
+
+} // namespace chromeos_update_engine