tools: refactor ndn-autoconfig

Hub face creation and prefix registrations are moved into a new
Procedure class. Main function is simplified.

refs #4158

Change-Id: I15b660e3b8a1bde89498a1cb549a87788de46c7a
diff --git a/tests/tools/mock-nfd-mgmt-fixture.hpp b/tests/tools/mock-nfd-mgmt-fixture.hpp
new file mode 100644
index 0000000..4eeab41
--- /dev/null
+++ b/tests/tools/mock-nfd-mgmt-fixture.hpp
@@ -0,0 +1,226 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2014-2017,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef NFD_TESTS_TOOLS_MOCK_NFD_MGMT_FIXTURE_HPP
+#define NFD_TESTS_TOOLS_MOCK_NFD_MGMT_FIXTURE_HPP
+
+#include <ndn-cxx/util/dummy-client-face.hpp>
+
+#include "tests/test-common.hpp"
+#include "tests/identity-management-fixture.hpp"
+
+namespace nfd {
+namespace tools {
+namespace tests {
+
+using namespace nfd::tests;
+using ndn::nfd::ControlParameters;
+
+/** \brief fixture to emulate NFD management
+ */
+class MockNfdMgmtFixture : public IdentityManagementTimeFixture
+{
+protected:
+  MockNfdMgmtFixture()
+    : face(g_io, m_keyChain,
+           {true, false, bind(&MockNfdMgmtFixture::processEventsOverride, this, _1)})
+  {
+    face.onSendInterest.connect([=] (const Interest& interest) {
+      g_io.post([=] {
+        if (processInterest != nullptr) {
+          processInterest(interest);
+        }
+      });
+    });
+  }
+
+protected: // ControlCommand
+  /** \brief check the Interest is a command with specified prefix
+   *  \retval nullopt last Interest is not the expected command
+   *  \return command parameters
+   */
+  static ndn::optional<ControlParameters>
+  parseCommand(const Interest& interest, const Name& expectedPrefix)
+  {
+    if (!expectedPrefix.isPrefixOf(interest.getName())) {
+      return ndn::nullopt;
+    }
+    return ControlParameters(interest.getName().at(expectedPrefix.size()).blockFromValue());
+  }
+
+  /** \brief send successful response to a command Interest
+   */
+  void
+  succeedCommand(const Interest& interest, const ControlParameters& parameters)
+  {
+    this->sendCommandReply(interest, 200, "OK", parameters.wireEncode());
+  }
+
+  /** \brief send failure response to a command Interest
+   */
+  void
+  failCommand(const Interest& interest, uint32_t code, const std::string& text)
+  {
+    this->sendCommandReply(interest, {code, text});
+  }
+
+  /** \brief send failure response to a command Interest
+   */
+  void
+  failCommand(const Interest& interest, uint32_t code, const std::string& text, const ControlParameters& body)
+  {
+    this->sendCommandReply(interest, code, text, body.wireEncode());
+  }
+
+protected: // StatusDataset
+  /** \brief send an empty dataset in reply to StatusDataset request
+   *  \param prefix dataset prefix without version and segment
+   *  \pre Interest for dataset has been expressed, sendDataset has not been invoked
+   */
+  void
+  sendEmptyDataset(const Name& prefix)
+  {
+    this->sendDatasetReply(prefix, nullptr, 0);
+  }
+
+  /** \brief send one WireEncodable in reply to StatusDataset request
+   *  \param prefix dataset prefix without version and segment
+   *  \param payload payload block
+   *  \note payload must fit in one Data
+   *  \pre Interest for dataset has been expressed, sendDataset has not been invoked
+   */
+  template<typename T>
+  void
+  sendDataset(const Name& prefix, const T& payload)
+  {
+    BOOST_CONCEPT_ASSERT((ndn::WireEncodable<T>));
+
+    this->sendDatasetReply(prefix, payload.wireEncode());
+  }
+
+  /** \brief send two WireEncodables in reply to StatusDataset request
+   *  \param prefix dataset prefix without version and segment
+   *  \param payload1 first vector item
+   *  \param payload2 second vector item
+   *  \note all payloads must fit in one Data
+   *  \pre Interest for dataset has been expressed, sendDataset has not been invoked
+   */
+  template<typename T1, typename T2>
+  void
+  sendDataset(const Name& prefix, const T1& payload1, const T2& payload2)
+  {
+    BOOST_CONCEPT_ASSERT((ndn::WireEncodable<T1>));
+    BOOST_CONCEPT_ASSERT((ndn::WireEncodable<T2>));
+
+    ndn::encoding::EncodingBuffer buffer;
+    payload2.wireEncode(buffer);
+    payload1.wireEncode(buffer);
+
+    this->sendDatasetReply(prefix, buffer.buf(), buffer.size());
+  }
+
+private:
+  virtual void
+  processEventsOverride(time::milliseconds timeout)
+  {
+    if (timeout <= time::milliseconds::zero()) {
+      // give enough time to finish execution
+      timeout = time::seconds(30);
+    }
+    this->advanceClocks(time::milliseconds(100), timeout);
+  }
+
+  void
+  sendCommandReply(const Interest& interest, const ndn::nfd::ControlResponse& resp)
+  {
+    auto data = makeData(interest.getName());
+    data->setContent(resp.wireEncode());
+    face.receive(*data);
+  }
+
+  void
+  sendCommandReply(const Interest& interest, uint32_t code, const std::string& text,
+                   const Block& body)
+  {
+    this->sendCommandReply(interest,
+                           ndn::nfd::ControlResponse(code, text).setBody(body));
+  }
+
+  /** \brief send a payload in reply to StatusDataset request
+   *  \param name dataset prefix without version and segment
+   *  \param contentArgs passed to Data::setContent
+   */
+  template<typename ...ContentArgs>
+  void
+  sendDatasetReply(Name name, ContentArgs&&... contentArgs)
+  {
+    name.appendVersion().appendSegment(0);
+
+    // These warnings assist in debugging when nfdc does not receive StatusDataset.
+    // They usually indicate a misspelled prefix or incorrect timing in the test case.
+    if (face.sentInterests.empty()) {
+      BOOST_WARN_MESSAGE(false, "no Interest expressed");
+    }
+    else {
+      BOOST_WARN_MESSAGE(face.sentInterests.back().getName().isPrefixOf(name),
+                         "last Interest " << face.sentInterests.back().getName() <<
+                         " cannot be satisfied by this Data " << name);
+    }
+
+    auto data = make_shared<Data>(name);
+    data->setFinalBlockId(name[-1]);
+    data->setContent(std::forward<ContentArgs>(contentArgs)...);
+    this->signDatasetReply(*data);
+    face.receive(*data);
+  }
+
+  virtual void
+  signDatasetReply(Data& data)
+  {
+    signData(data);
+  }
+
+protected:
+  ndn::util::DummyClientFace face;
+  std::function<void(const Interest&)> processInterest;
+};
+
+} // namespace tests
+} // namespace tools
+} // namespace nfd
+
+/** \brief require the command in \p interest has expected prefix
+ *  \note This must be used in processInterest lambda, and the Interest must be named 'interest'.
+ *  \return ControlParameters, or nullopt if \p interest does match \p expectedPrefix
+ */
+#define MOCK_NFD_MGMT_REQUIRE_COMMAND_IS(expectedPrefix) \
+  [interest] { \
+    auto params = parseCommand(interest, (expectedPrefix)); \
+    BOOST_REQUIRE_MESSAGE(params, "Interest " << interest.getName() << \
+                          " does not match command prefix " << (expectedPrefix)); \
+    return *params; \
+  } ()
+
+#endif // NFD_TESTS_TOOLS_MOCK_NFD_MGMT_FIXTURE_HPP
diff --git a/tests/tools/ndn-autoconfig/procedure.t.cpp b/tests/tools/ndn-autoconfig/procedure.t.cpp
new file mode 100644
index 0000000..27d9dcf
--- /dev/null
+++ b/tests/tools/ndn-autoconfig/procedure.t.cpp
@@ -0,0 +1,286 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2014-2017,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "ndn-autoconfig/procedure.hpp"
+
+#include "../mock-nfd-mgmt-fixture.hpp"
+#include <boost/logic/tribool.hpp>
+
+namespace ndn {
+namespace tools {
+namespace autoconfig {
+namespace tests {
+
+using namespace ::nfd::tests;
+using nfd::ControlParameters;
+
+template<typename ProcedureClass>
+class ProcedureFixture : public ::nfd::tools::tests::MockNfdMgmtFixture
+{
+public:
+  void
+  initialize(const Options& options)
+  {
+    procedure = make_unique<ProcedureClass>(face, m_keyChain);
+    procedure->initialize(options);
+  }
+
+  bool
+  runOnce()
+  {
+    BOOST_ASSERT(procedure != nullptr);
+    boost::logic::tribool result;
+    procedure->onComplete.connectSingleShot([&] (bool result1) { result = result1; });
+    procedure->runOnce();
+    face.processEvents();
+    BOOST_CHECK_MESSAGE(!boost::logic::indeterminate(result), "onComplete is not invoked");
+    return result;
+  }
+
+public:
+  unique_ptr<ProcedureClass> procedure;
+};
+
+class DummyStage : public Stage
+{
+public:
+  /** \param stageName stage name
+   *  \param nCalls pointer to a variable which is incremented each time doStart is invoked
+   *  \param result expected result, nullopt to cause a failued
+   *  \param io io_service to asynchronously post the result
+   */
+  DummyStage(const std::string& stageName, int* nCalls, const optional<FaceUri>& result, boost::asio::io_service& io)
+    : m_stageName(stageName)
+    , m_nCalls(nCalls)
+    , m_result(result)
+    , m_io(io)
+  {
+  }
+
+  const std::string&
+  getName() const override
+  {
+    return m_stageName;
+  }
+
+private:
+  void
+  doStart() override
+  {
+    if (m_nCalls != nullptr) {
+      ++(*m_nCalls);
+    }
+    m_io.post([this] {
+      if (m_result) {
+        this->succeed(*m_result);
+      }
+      else {
+        this->fail("DUMMY-STAGE-FAIL");
+      }
+    });
+  }
+
+private:
+  std::string m_stageName;
+  int* m_nCalls;
+  optional<FaceUri> m_result;
+  boost::asio::io_service& m_io;
+};
+
+/** \brief two-stage Procedure where the first stage succeeds and the second stage fails
+ *
+ *  But the second stage shouldn't be invoked after the first stage succeeds.
+ */
+class ProcedureSuccessFailure : public Procedure
+{
+public:
+  ProcedureSuccessFailure(Face& face, KeyChain& keyChain)
+    : Procedure(face, keyChain)
+    , m_io(face.getIoService())
+  {
+  }
+
+private:
+  void
+  makeStages(const Options& options) override
+  {
+    m_stages.push_back(make_unique<DummyStage>("first", &nCalls1, FaceUri("udp://188.7.60.95"), m_io));
+    m_stages.push_back(make_unique<DummyStage>("second", &nCalls2, nullopt, m_io));
+  }
+
+public:
+  int nCalls1 = 0;
+  int nCalls2 = 0;
+
+private:
+  boost::asio::io_service& m_io;
+};
+
+/** \brief two-stage Procedure where the first stage fails and the second stage succeeds
+ */
+class ProcedureFailureSuccess : public Procedure
+{
+public:
+  ProcedureFailureSuccess(Face& face, KeyChain& keyChain)
+    : Procedure(face, keyChain)
+    , m_io(face.getIoService())
+  {
+  }
+
+private:
+  void
+  makeStages(const Options& options) override
+  {
+    m_stages.push_back(make_unique<DummyStage>("first", &nCalls1, nullopt, m_io));
+    m_stages.push_back(make_unique<DummyStage>("second", &nCalls2, FaceUri("tcp://40.23.174.71"), m_io));
+  }
+
+public:
+  int nCalls1 = 0;
+  int nCalls2 = 0;
+
+private:
+  boost::asio::io_service& m_io;
+};
+
+BOOST_AUTO_TEST_SUITE(NdnAutoconfig)
+BOOST_AUTO_TEST_SUITE(TestProcedure)
+
+BOOST_FIXTURE_TEST_CASE(Normal, ProcedureFixture<ProcedureSuccessFailure>)
+{
+  this->initialize(Options{});
+
+  int nRegisterNdn = 0, nRegisterLocalhopNfd = 0;
+  this->processInterest = [&] (const Interest& interest) {
+    optional<ControlParameters> req = parseCommand(interest, "/localhost/nfd/faces/create");
+    if (req) {
+      BOOST_REQUIRE(req->hasUri());
+      BOOST_CHECK_EQUAL(req->getUri(), "udp4://188.7.60.95:6363");
+
+      ControlParameters resp;
+      resp.setFaceId(1178)
+          .setUri("udp4://188.7.60.95:6363")
+          .setLocalUri("udp4://110.69.164.68:23197")
+          .setFacePersistency(nfd::FacePersistency::FACE_PERSISTENCY_PERSISTENT)
+          .setFlags(0);
+      this->succeedCommand(interest, resp);
+      return;
+    }
+
+    req = parseCommand(interest, "/localhost/nfd/rib/register");
+    if (req) {
+      BOOST_REQUIRE(req->hasFaceId());
+      BOOST_CHECK_EQUAL(req->getFaceId(), 1178);
+      BOOST_REQUIRE(req->hasOrigin());
+      BOOST_CHECK_EQUAL(req->getOrigin(), nfd::ROUTE_ORIGIN_AUTOCONF);
+      BOOST_REQUIRE(req->hasName());
+      if (req->getName() == "/ndn") {
+        ++nRegisterNdn;
+      }
+      else if (req->getName() == "/localhop/nfd") {
+        ++nRegisterLocalhopNfd;
+      }
+      else {
+        BOOST_ERROR("unexpected prefix registration " << req->getName());
+      }
+
+      ControlParameters resp;
+      resp.setName(req->getName())
+          .setFaceId(1178)
+          .setOrigin(nfd::ROUTE_ORIGIN_AUTOCONF)
+          .setCost(1)
+          .setFlags(0);
+      this->succeedCommand(interest, resp);
+      return;
+    }
+
+    BOOST_FAIL("unrecognized command Interest " << interest);
+  };
+
+  BOOST_CHECK_EQUAL(this->runOnce(), true);
+  BOOST_CHECK_EQUAL(procedure->nCalls1, 1);
+  BOOST_CHECK_EQUAL(procedure->nCalls2, 0);
+  BOOST_CHECK_EQUAL(nRegisterNdn, 1);
+  BOOST_CHECK_EQUAL(nRegisterLocalhopNfd, 1);
+}
+
+BOOST_FIXTURE_TEST_CASE(ExistingFace, ProcedureFixture<ProcedureFailureSuccess>)
+{
+  this->initialize(Options{});
+
+  int nRegisterNdn = 0, nRegisterLocalhopNfd = 0;
+  this->processInterest = [&] (const Interest& interest) {
+    optional<ControlParameters> req = parseCommand(interest, "/localhost/nfd/faces/create");
+    if (req) {
+      ControlParameters resp;
+      resp.setFaceId(3146)
+          .setUri("tcp4://40.23.174.71:6363")
+          .setLocalUri("tcp4://34.213.69.67:14445")
+          .setFacePersistency(nfd::FacePersistency::FACE_PERSISTENCY_PERSISTENT)
+          .setFlags(0);
+      this->failCommand(interest, 409, "conflict-409", resp);
+      return;
+    }
+
+    req = parseCommand(interest, "/localhost/nfd/rib/register");
+    if (req) {
+      BOOST_REQUIRE(req->hasName());
+      if (req->getName() == "/ndn") {
+        ++nRegisterNdn;
+      }
+      else if (req->getName() == "/localhop/nfd") {
+        ++nRegisterLocalhopNfd;
+      }
+      else {
+        BOOST_ERROR("unexpected prefix registration " << req->getName());
+      }
+
+      ControlParameters resp;
+      resp.setName(req->getName())
+          .setFaceId(3146)
+          .setOrigin(nfd::ROUTE_ORIGIN_AUTOCONF)
+          .setCost(1)
+          .setFlags(0);
+      this->succeedCommand(interest, resp);
+      return;
+    }
+
+    BOOST_FAIL("unrecognized command Interest " << interest);
+  };
+
+  BOOST_CHECK_EQUAL(this->runOnce(), true);
+  BOOST_CHECK_EQUAL(procedure->nCalls1, 1);
+  BOOST_CHECK_EQUAL(procedure->nCalls2, 1);
+  BOOST_CHECK_EQUAL(nRegisterNdn, 1);
+  BOOST_CHECK_EQUAL(nRegisterLocalhopNfd, 1);
+}
+
+BOOST_AUTO_TEST_SUITE_END() // TestProcedure
+BOOST_AUTO_TEST_SUITE_END() // NdnAutoconfig
+
+} // namespace tests
+} // namespace autoconfig
+} // namespace tools
+} // namespace ndn
diff --git a/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp b/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp
index fd309bf..9841de8 100644
--- a/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp
+++ b/tests/tools/nfdc/mock-nfd-mgmt-fixture.hpp
@@ -1,5 +1,5 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
-/**
+/*
  * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
@@ -26,11 +26,7 @@
 #ifndef NFD_TESTS_TOOLS_NFDC_MOCK_NFD_MGMT_FIXTURE_HPP
 #define NFD_TESTS_TOOLS_NFDC_MOCK_NFD_MGMT_FIXTURE_HPP
 
-#include <ndn-cxx/mgmt/nfd/face-query-filter.hpp>
-#include <ndn-cxx/util/dummy-client-face.hpp>
-
-#include "tests/test-common.hpp"
-#include "tests/identity-management-fixture.hpp"
+#include "../mock-nfd-mgmt-fixture.hpp"
 
 namespace nfd {
 namespace tools {
@@ -38,111 +34,12 @@
 namespace tests {
 
 using namespace nfd::tests;
-using ndn::nfd::ControlParameters;
 
 /** \brief fixture to emulate NFD management
  */
-class MockNfdMgmtFixture : public IdentityManagementTimeFixture
+class MockNfdMgmtFixture : public nfd::tools::tests::MockNfdMgmtFixture
 {
 protected:
-  MockNfdMgmtFixture()
-    : face(g_io, m_keyChain,
-           {true, false, bind(&MockNfdMgmtFixture::processEventsOverride, this, _1)})
-  {
-    face.onSendInterest.connect([=] (const Interest& interest) {
-      g_io.post([=] {
-        if (processInterest != nullptr) {
-          processInterest(interest);
-        }
-      });
-    });
-  }
-
-protected: // ControlCommand
-  /** \brief check the Interest is a command with specified prefix
-   *  \retval nullopt last Interest is not the expected command
-   *  \return command parameters
-   */
-  static ndn::optional<ControlParameters>
-  parseCommand(const Interest& interest, const Name& expectedPrefix)
-  {
-    if (!expectedPrefix.isPrefixOf(interest.getName())) {
-      return ndn::nullopt;
-    }
-    return ControlParameters(interest.getName().at(expectedPrefix.size()).blockFromValue());
-  }
-
-  /** \brief send successful response to a command Interest
-   */
-  void
-  succeedCommand(const Interest& interest, const ControlParameters& parameters)
-  {
-    this->sendCommandReply(interest, 200, "OK", parameters.wireEncode());
-  }
-
-  /** \brief send failure response to a command Interest
-   */
-  void
-  failCommand(const Interest& interest, uint32_t code, const std::string& text)
-  {
-    this->sendCommandReply(interest, {code, text});
-  }
-
-  /** \brief send failure response to a command Interest
-   */
-  void
-  failCommand(const Interest& interest, uint32_t code, const std::string& text, const ControlParameters& body)
-  {
-    this->sendCommandReply(interest, code, text, body.wireEncode());
-  }
-
-protected: // StatusDataset
-  /** \brief send an empty dataset in reply to StatusDataset request
-   *  \param prefix dataset prefix without version and segment
-   *  \pre Interest for dataset has been expressed, sendDataset has not been invoked
-   */
-  void
-  sendEmptyDataset(const Name& prefix)
-  {
-    this->sendDatasetReply(prefix, nullptr, 0);
-  }
-
-  /** \brief send one WireEncodable in reply to StatusDataset request
-   *  \param prefix dataset prefix without version and segment
-   *  \param payload payload block
-   *  \note payload must fit in one Data
-   *  \pre Interest for dataset has been expressed, sendDataset has not been invoked
-   */
-  template<typename T>
-  void
-  sendDataset(const Name& prefix, const T& payload)
-  {
-    BOOST_CONCEPT_ASSERT((ndn::WireEncodable<T>));
-
-    this->sendDatasetReply(prefix, payload.wireEncode());
-  }
-
-  /** \brief send two WireEncodables in reply to StatusDataset request
-   *  \param prefix dataset prefix without version and segment
-   *  \param payload1 first vector item
-   *  \param payload2 second vector item
-   *  \note all payloads must fit in one Data
-   *  \pre Interest for dataset has been expressed, sendDataset has not been invoked
-   */
-  template<typename T1, typename T2>
-  void
-  sendDataset(const Name& prefix, const T1& payload1, const T2& payload2)
-  {
-    BOOST_CONCEPT_ASSERT((ndn::WireEncodable<T1>));
-    BOOST_CONCEPT_ASSERT((ndn::WireEncodable<T2>));
-
-    ndn::encoding::EncodingBuffer buffer;
-    payload2.wireEncode(buffer);
-    payload1.wireEncode(buffer);
-
-    this->sendDatasetReply(prefix, buffer.buf(), buffer.size());
-  }
-
   /** \brief respond to specific FaceQuery requests
    *  \retval true the Interest matches one of the defined patterns and is responded
    *  \retval false the Interest is not responded
@@ -201,71 +98,6 @@
 
     return false;
   }
-
-private:
-  virtual void
-  processEventsOverride(time::milliseconds timeout)
-  {
-    if (timeout <= time::milliseconds::zero()) {
-      // give enough time to finish execution
-      timeout = time::seconds(30);
-    }
-    this->advanceClocks(time::milliseconds(100), timeout);
-  }
-
-  void
-  sendCommandReply(const Interest& interest, const ndn::nfd::ControlResponse& resp)
-  {
-    auto data = makeData(interest.getName());
-    data->setContent(resp.wireEncode());
-    face.receive(*data);
-  }
-
-  void
-  sendCommandReply(const Interest& interest, uint32_t code, const std::string& text,
-                   const Block& body)
-  {
-    this->sendCommandReply(interest,
-                           ndn::nfd::ControlResponse(code, text).setBody(body));
-  }
-
-  /** \brief send a payload in reply to StatusDataset request
-   *  \param name dataset prefix without version and segment
-   *  \param contentArgs passed to Data::setContent
-   */
-  template<typename ...ContentArgs>
-  void
-  sendDatasetReply(Name name, ContentArgs&&... contentArgs)
-  {
-    name.appendVersion().appendSegment(0);
-
-    // These warnings assist in debugging when nfdc does not receive StatusDataset.
-    // They usually indicate a misspelled prefix or incorrect timing in the test case.
-    if (face.sentInterests.empty()) {
-      BOOST_WARN_MESSAGE(false, "no Interest expressed");
-    }
-    else {
-      BOOST_WARN_MESSAGE(face.sentInterests.back().getName().isPrefixOf(name),
-                         "last Interest " << face.sentInterests.back().getName() <<
-                         " cannot be satisfied by this Data " << name);
-    }
-
-    auto data = make_shared<Data>(name);
-    data->setFinalBlockId(name[-1]);
-    data->setContent(std::forward<ContentArgs>(contentArgs)...);
-    this->signDatasetReply(*data);
-    face.receive(*data);
-  }
-
-  virtual void
-  signDatasetReply(Data& data)
-  {
-    signData(data);
-  }
-
-protected:
-  ndn::util::DummyClientFace face;
-  std::function<void(const Interest&)> processInterest;
 };
 
 } // namespace tests
@@ -273,16 +105,4 @@
 } // namespace tools
 } // namespace nfd
 
-/** \brief require the command in \p interest has expected prefix
- *  \note This must be used in processInterest lambda, and the Interest must be named 'interest'.
- *  \return ControlParameters, or nullopt if \p interest does match \p expectedPrefix
- */
-#define MOCK_NFD_MGMT_REQUIRE_COMMAND_IS(expectedPrefix) \
-  [interest] { \
-    auto params = parseCommand(interest, (expectedPrefix)); \
-    BOOST_REQUIRE_MESSAGE(params, "Interest " << interest.getName() << \
-                          " does not match command prefix " << (expectedPrefix)); \
-    return *params; \
-  } ()
-
 #endif // NFD_TESTS_TOOLS_NFDC_MOCK_NFD_MGMT_FIXTURE_HPP
diff --git a/tools/ndn-autoconfig/guess-from-identity-name.cpp b/tools/ndn-autoconfig/guess-from-identity-name.cpp
index c26ec81..8ef112d 100644
--- a/tools/ndn-autoconfig/guess-from-identity-name.cpp
+++ b/tools/ndn-autoconfig/guess-from-identity-name.cpp
@@ -32,14 +32,13 @@
 namespace tools {
 namespace autoconfig {
 
-GuessFromIdentityName::GuessFromIdentityName(Face& face, KeyChain& keyChain,
-                                             const NextStageCallback& nextStageOnFailure)
-  : Stage(face, keyChain, nextStageOnFailure)
+GuessFromIdentityName::GuessFromIdentityName(KeyChain& keyChain)
+  : m_keyChain(keyChain)
 {
 }
 
 void
-GuessFromIdentityName::start()
+GuessFromIdentityName::doStart()
 {
   std::cerr << "Trying default identity name..." << std::endl;
 
@@ -53,11 +52,10 @@
 
   try {
     std::string hubUri = querySrvRr(serverName.str());
-    this->connectToHub(hubUri);
+    this->provideHubFaceUri(hubUri);
   }
   catch (const DnsSrvError& e) {
-    m_nextStageOnFailure(std::string("Failed to find a home router based on the default identity "
-                                     "name (") + e.what() + ")");
+    this->fail(e.what());
   }
 }
 
diff --git a/tools/ndn-autoconfig/guess-from-identity-name.hpp b/tools/ndn-autoconfig/guess-from-identity-name.hpp
index 315e856..9da9093 100644
--- a/tools/ndn-autoconfig/guess-from-identity-name.hpp
+++ b/tools/ndn-autoconfig/guess-from-identity-name.hpp
@@ -27,6 +27,7 @@
 #define NFD_TOOLS_NDN_AUTOCONFIG_GUESS_FROM_IDENTITY_NAME_HPP
 
 #include "stage.hpp"
+#include <ndn-cxx/security/key-chain.hpp>
 
 namespace ndn {
 namespace tools {
@@ -56,14 +57,22 @@
 class GuessFromIdentityName : public Stage
 {
 public:
-  /**
-   * @brief Create stage to guess home router based on the default identity name
-   */
-  GuessFromIdentityName(Face& face, KeyChain& keyChain,
-                        const NextStageCallback& nextStageOnFailure);
+  explicit
+  GuessFromIdentityName(KeyChain& keyChain);
 
+  const std::string&
+  getName() const override
+  {
+    static const std::string STAGE_NAME("guess from identity name");
+    return STAGE_NAME;
+  }
+
+private:
   void
-  start() override;
+  doStart() override;
+
+private:
+  KeyChain& m_keyChain;
 };
 
 } // namespace autoconfig
diff --git a/tools/ndn-autoconfig/guess-from-search-domains.cpp b/tools/ndn-autoconfig/guess-from-search-domains.cpp
index f53c4fc..2f92cc3 100644
--- a/tools/ndn-autoconfig/guess-from-search-domains.cpp
+++ b/tools/ndn-autoconfig/guess-from-search-domains.cpp
@@ -30,24 +30,15 @@
 namespace tools {
 namespace autoconfig {
 
-GuessFromSearchDomains::GuessFromSearchDomains(Face& face, KeyChain& keyChain,
-                                               const NextStageCallback& nextStageOnFailure)
-  : Stage(face, keyChain, nextStageOnFailure)
-{
-}
-
 void
-GuessFromSearchDomains::start()
+GuessFromSearchDomains::doStart()
 {
-  std::cerr << "Trying default suffix DNS query..." << std::endl;
-
   try {
     std::string hubUri = querySrvRrSearch();
-    this->connectToHub(hubUri);
+    this->provideHubFaceUri(hubUri);
   }
   catch (const DnsSrvError& e) {
-    m_nextStageOnFailure(std::string("Failed to find NDN router using default suffix DNS query (") +
-                         e.what() + ")");
+    this->fail(e.what());
   }
 }
 
diff --git a/tools/ndn-autoconfig/guess-from-search-domains.hpp b/tools/ndn-autoconfig/guess-from-search-domains.hpp
index a2670ec..9bd6521 100644
--- a/tools/ndn-autoconfig/guess-from-search-domains.hpp
+++ b/tools/ndn-autoconfig/guess-from-search-domains.hpp
@@ -49,14 +49,16 @@
 class GuessFromSearchDomains : public Stage
 {
 public:
-  /**
-   * @brief Create stage to guess home router based on DNS query with default suffix
-   */
-  GuessFromSearchDomains(Face& face, KeyChain& keyChain,
-                         const NextStageCallback& nextStageOnFailure);
+  const std::string&
+  getName() const override
+  {
+    static const std::string STAGE_NAME("guess from search domains");
+    return STAGE_NAME;
+  }
 
+private:
   void
-  start() override;
+  doStart() override;
 };
 
 } // namespace autoconfig
diff --git a/tools/ndn-autoconfig/main.cpp b/tools/ndn-autoconfig/main.cpp
index db45610..fad4933 100644
--- a/tools/ndn-autoconfig/main.cpp
+++ b/tools/ndn-autoconfig/main.cpp
@@ -1,5 +1,5 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
-/**
+/*
  * Copyright (c) 2014-2017,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
@@ -23,23 +23,20 @@
  * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#include "procedure.hpp"
+#include "core/extended-error-message.hpp"
+#include "core/scheduler.hpp"
 #include "core/version.hpp"
 
-#include "multicast-discovery.hpp"
-#include "guess-from-search-domains.hpp"
-#include "ndn-fch-discovery.hpp"
-#include "guess-from-identity-name.hpp"
-
-#include <ndn-cxx/net/network-monitor.hpp>
-#include <ndn-cxx/util/scheduler.hpp>
-#include <ndn-cxx/util/scheduler-scoped-event-id.hpp>
-
-#include <boost/noncopyable.hpp>
+#include <signal.h>
+#include <string.h>
 #include <boost/program_options/options_description.hpp>
 #include <boost/program_options/parsers.hpp>
 #include <boost/program_options/variables_map.hpp>
-
-namespace po = boost::program_options;
+#include <ndn-cxx/net/network-monitor.hpp>
+#include <ndn-cxx/util/scheduler.hpp>
+#include <ndn-cxx/util/scheduler-scoped-event-id.hpp>
+#include <ndn-cxx/util/time.hpp>
 
 namespace ndn {
 namespace tools {
@@ -47,201 +44,144 @@
 // ndn-autoconfig is an NDN tool not an NFD tool, so it uses ndn::tools::autoconfig namespace.
 // It lives in NFD repository because nfd-start can automatically start ndn-autoconfig in daemon mode.
 
-class NdnAutoconfig : boost::noncopyable
+static const time::nanoseconds DAEMON_INITIAL_DELAY = time::milliseconds(100);
+static const time::nanoseconds DAEMON_UNCONDITIONAL_INTERVAL = time::hours(1);
+static const time::nanoseconds NETMON_DAMPEN_PERIOD = time::seconds(5);
+
+namespace po = boost::program_options;
+
+static void
+usage(std::ostream& os,
+      const po::options_description& optionsDescription,
+      const char* programName)
 {
-public:
-  class Error : public std::runtime_error
-  {
-  public:
-    explicit
-    Error(const std::string& what)
-      : std::runtime_error(what)
-    {
+  os << "Usage:\n"
+     << "  " << programName << " [options]\n"
+     << "\n";
+  os << optionsDescription;
+}
+
+static void
+runDaemon(Procedure& proc)
+{
+  boost::asio::signal_set terminateSignals(proc.getIoService());
+  terminateSignals.add(SIGINT);
+  terminateSignals.add(SIGTERM);
+  terminateSignals.async_wait([&] (const boost::system::error_code& error, int signalNo) {
+    if (error) {
+      return;
     }
+    const char* signalName = ::strsignal(signalNo);
+    std::cerr << "Exit on signal ";
+    if (signalName == nullptr) {
+      std::cerr << signalNo;
+    }
+    else {
+      std::cerr << signalName;
+    }
+    std::cerr << std::endl;
+    proc.getIoService().stop();
+  });
+
+  util::Scheduler sched(proc.getIoService());
+  util::scheduler::ScopedEventId runEvt(sched);
+  auto scheduleRerun = [&] (time::nanoseconds delay) {
+    runEvt = sched.scheduleEvent(delay, [&] { proc.runOnce(); });
   };
 
-  explicit
-  NdnAutoconfig(const std::string& ndnFchUrl, bool isDaemonMode)
-    : m_face(m_io)
-    , m_scheduler(m_io)
-    , m_startStagesEvent(m_scheduler)
-    , m_isDaemonMode(isDaemonMode)
-    , m_terminationSignalSet(m_io)
-    , m_stage1(m_face, m_keyChain,
-               [&] (const std::string& errorMessage) {
-                 std::cerr << "Stage 1 failed: " << errorMessage << std::endl;
-                 m_stage2.start();
-               })
-    , m_stage2(m_face, m_keyChain,
-               [&] (const std::string& errorMessage) {
-                 std::cerr << "Stage 2 failed: " << errorMessage << std::endl;
-                 m_stage3.start();
-               })
-    , m_stage3(m_face, m_keyChain,
-               ndnFchUrl,
-               [&] (const std::string& errorMessage) {
-                 std::cerr << "Stage 3 failed: " << errorMessage << std::endl;
-                 m_stage4.start();
-               })
-    , m_stage4(m_face, m_keyChain,
-               [&] (const std::string& errorMessage) {
-                 std::cerr << "Stage 4 failed: " << errorMessage << std::endl;
-                 if (!m_isDaemonMode)
-                   BOOST_THROW_EXCEPTION(Error("No more stages, automatic discovery failed"));
-                 else
-                   std::cerr << "No more stages, automatic discovery failed" << std::endl;
-               })
-  {
-    if (m_isDaemonMode) {
-      m_networkMonitor.reset(new net::NetworkMonitor(m_io));
-      m_networkMonitor->onNetworkStateChanged.connect([this] {
-          // delay stages, so if multiple events are triggered in short sequence,
-          // only one auto-detection procedure is triggered
-          m_startStagesEvent = m_scheduler.scheduleEvent(time::seconds(5),
-                                                         bind(&NdnAutoconfig::startStages, this));
-        });
-    }
+  proc.onComplete.connect([&] (bool isSuccess) {
+    scheduleRerun(DAEMON_UNCONDITIONAL_INTERVAL);
+  });
 
-    // Delay a little bit
-    m_startStagesEvent = m_scheduler.scheduleEvent(time::milliseconds(100),
-                                                   bind(&NdnAutoconfig::startStages, this));
-  }
+  net::NetworkMonitor netmon(proc.getIoService());
+  netmon.onNetworkStateChanged.connect([&] { scheduleRerun(NETMON_DAMPEN_PERIOD); });
 
-  void
-  run()
-  {
-    if (m_isDaemonMode) {
-      m_terminationSignalSet.add(SIGINT);
-      m_terminationSignalSet.add(SIGTERM);
-      m_terminationSignalSet.async_wait(bind(&NdnAutoconfig::terminate, this, _1, _2));
-    }
-
-    m_io.run();
-  }
-
-  void
-  terminate(const boost::system::error_code& error, int signalNo)
-  {
-    if (error)
-      return;
-
-    m_io.stop();
-  }
-
-  static void
-  usage(std::ostream& os,
-        const po::options_description& optionDescription,
-        const char* programName)
-  {
-    os << "Usage:\n"
-       << "  " << programName << " [options]\n"
-       << "\n";
-    os << optionDescription;
-  }
-
-private:
-  void
-  startStages()
-  {
-    m_stage1.start();
-    if (m_isDaemonMode) {
-      m_startStagesEvent = m_scheduler.scheduleEvent(time::hours(1),
-                                                     bind(&NdnAutoconfig::startStages, this));
-    }
-  }
-
-private:
-  boost::asio::io_service m_io;
-  Face m_face;
-  KeyChain m_keyChain;
-  unique_ptr<net::NetworkMonitor> m_networkMonitor;
-  util::Scheduler m_scheduler;
-  util::scheduler::ScopedEventId m_startStagesEvent;
-  bool m_isDaemonMode;
-  boost::asio::signal_set m_terminationSignalSet;
-
-  MulticastDiscovery m_stage1;
-  GuessFromSearchDomains m_stage2;
-  NdnFchDiscovery m_stage3;
-  GuessFromIdentityName m_stage4;
-};
+  scheduleRerun(DAEMON_INITIAL_DELAY);
+  proc.getIoService().run();
+}
 
 static int
 main(int argc, char** argv)
 {
-  bool isDaemonMode = false;
+  Options options;
+  bool isDaemon = false;
   std::string configFile;
-  std::string ndnFchUrl;
 
-  po::options_description optionDescription("Options");
-  optionDescription.add_options()
-    ("help,h", "produce help message")
-    ("daemon,d", po::bool_switch(&isDaemonMode)->default_value(isDaemonMode),
-     "run in daemon mode, detecting network change events and re-running "
-     "auto-discovery procedure.  In addition, the auto-discovery procedure "
-     "is unconditionally re-run every hour.\n"
+  po::options_description optionsDescription("Options");
+  optionsDescription.add_options()
+    ("help,h", "print this message and exit")
+    ("version,V", "display version and exit")
+    ("daemon,d", po::bool_switch(&isDaemon)->default_value(isDaemon),
+     "run in daemon mode, detecting network change events and re-running auto-discovery procedure. "
+     "In addition, the auto-discovery procedure is unconditionally re-run every hour.\n"
      "NOTE: if connection to NFD fails, the daemon will be terminated.")
-    ("ndn-fch-url", po::value<std::string>(&ndnFchUrl)->default_value("http://ndn-fch.named-data.net"),
+    ("ndn-fch-url", po::value<std::string>(&options.ndnFchUrl)->default_value(options.ndnFchUrl),
      "URL for NDN-FCH (Find Closest Hub) service")
-    ("config,c", po::value<std::string>(&configFile), "configuration file. If `enabled = true` "
-     "is not specified, no actions will be performed.")
-    ("version,V", "show version and exit")
+    ("config,c", po::value<std::string>(&configFile),
+     "configuration file. Exit immediately if `enabled = true` is not specified in config file.")
     ;
 
-  po::variables_map options;
+  po::variables_map vm;
   try {
-    po::store(po::parse_command_line(argc, argv, optionDescription), options);
-    po::notify(options);
+    po::store(po::parse_command_line(argc, argv, optionsDescription), vm);
+    po::notify(vm);
   }
   catch (const std::exception& e) {
-    std::cerr << "ERROR: " << e.what() << "\n" << std::endl;
-    NdnAutoconfig::usage(std::cerr, optionDescription, argv[0]);
-    return 1;
+    std::cerr << "ERROR: " << e.what() << "\n" << "\n\n";
+    usage(std::cerr, optionsDescription, argv[0]);
+    return 2;
   }
 
-  if (options.count("help")) {
-    NdnAutoconfig::usage(std::cout, optionDescription, argv[0]);
+  if (vm.count("help")) {
+    usage(std::cout, optionsDescription, argv[0]);
     return 0;
   }
 
-  if (options.count("version")) {
+  if (vm.count("version")) {
     std::cout << NFD_VERSION_BUILD_STRING << std::endl;
     return 0;
   }
 
-  // Enable (one-shot or daemon mode whenever config file is not specified)
-  bool isEnabled = true;
-
-  po::options_description configFileOptions;
-  configFileOptions.add_options()
-    ("enabled", po::value<bool>(&isEnabled))
-    ;
-
-  if (!configFile.empty()) {
-    isEnabled = false; // Disable by default if config file is specified
+  if (vm.count("config")) {
+    po::options_description configFileOptions;
+    configFileOptions.add_options()
+      ("enabled", po::value<bool>()->default_value(false))
+      ;
     try {
-      po::store(po::parse_config_file<char>(configFile.c_str(), configFileOptions), options);
-      po::notify(options);
+      po::store(po::parse_config_file<char>(configFile.data(), configFileOptions), vm);
+      po::notify(vm);
     }
     catch (const std::exception& e) {
-      std::cerr << "ERROR: " << e.what() << std::endl << std::endl;
-      return 1;
+      std::cerr << "ERROR in config: " << e.what() << "\n\n";
+      return 2;
+    }
+    if (!vm["enabled"].as<bool>()) {
+      // not enabled in config
+      return 0;
     }
   }
 
-  if (!isEnabled) {
-    return 0;
-  }
-
+  int exitCode = 0;
   try {
-    NdnAutoconfig autoConfigInstance(ndnFchUrl, isDaemonMode);
-    autoConfigInstance.run();
+    Face face;
+    KeyChain keyChain;
+    Procedure proc(face, keyChain);
+    proc.initialize(options);
+
+    if (isDaemon) {
+      runDaemon(proc);
+    }
+    else {
+      proc.onComplete.connect([&exitCode] (bool isSuccess) { exitCode = isSuccess ? 0 : 3; });
+      proc.runOnce();
+      face.processEvents();
+    }
   }
-  catch (const std::exception& error) {
-    std::cerr << "ERROR: " << error.what() << std::endl;
+  catch (const std::exception& e) {
+    std::cerr << ::nfd::getExtendedErrorMessage(e) << std::endl;
     return 1;
   }
-  return 0;
+  return exitCode;
 }
 
 } // namespace autoconfig
diff --git a/tools/ndn-autoconfig/multicast-discovery.cpp b/tools/ndn-autoconfig/multicast-discovery.cpp
index 6692bc2..f9b4797 100644
--- a/tools/ndn-autoconfig/multicast-discovery.cpp
+++ b/tools/ndn-autoconfig/multicast-discovery.cpp
@@ -33,46 +33,44 @@
 
 static const Name LOCALHOP_HUB_DISCOVERY_PREFIX = "/localhop/ndn-autoconf/hub";
 
-MulticastDiscovery::MulticastDiscovery(Face& face, KeyChain& keyChain,
-                                       const NextStageCallback& nextStageOnFailure)
-  : Stage(face, keyChain, nextStageOnFailure)
+MulticastDiscovery::MulticastDiscovery(Face& face, nfd::Controller& controller)
+  : m_face(face)
+  , m_controller(controller)
   , m_nRequestedRegs(0)
   , m_nFinishedRegs(0)
 {
 }
 
 void
-MulticastDiscovery::start()
+MulticastDiscovery::doStart()
 {
-  std::cerr << "Trying multicast discovery..." << std::endl;
-
   this->collectMulticastFaces();
 }
 
 void
 MulticastDiscovery::collectMulticastFaces()
 {
-  ndn::nfd::FaceQueryFilter filter;
-  filter.setLinkType(ndn::nfd::LINK_TYPE_MULTI_ACCESS);
-  m_controller.fetch<ndn::nfd::FaceQueryDataset>(
+  nfd::FaceQueryFilter filter;
+  filter.setLinkType(nfd::LINK_TYPE_MULTI_ACCESS);
+  m_controller.fetch<nfd::FaceQueryDataset>(
     filter,
     bind(&MulticastDiscovery::registerHubDiscoveryPrefix, this, _1),
-    bind(m_nextStageOnFailure, _2)
+    bind(&MulticastDiscovery::fail, this, _2)
   );
 }
 
 void
-MulticastDiscovery::registerHubDiscoveryPrefix(const std::vector<ndn::nfd::FaceStatus>& dataset)
+MulticastDiscovery::registerHubDiscoveryPrefix(const std::vector<nfd::FaceStatus>& dataset)
 {
   std::vector<uint64_t> multicastFaces;
   std::transform(dataset.begin(), dataset.end(), std::back_inserter(multicastFaces),
-                 [] (const ndn::nfd::FaceStatus& faceStatus) { return faceStatus.getFaceId(); });
+                 [] (const nfd::FaceStatus& faceStatus) { return faceStatus.getFaceId(); });
 
   if (multicastFaces.empty()) {
-    m_nextStageOnFailure("No multicast faces available, skipping multicast discovery stage");
+    this->fail("No multicast faces available, skipping multicast discovery stage");
   }
   else {
-    ControlParameters parameters;
+    nfd::ControlParameters parameters;
     parameters
       .setName(LOCALHOP_HUB_DISCOVERY_PREFIX)
       .setCost(1)
@@ -83,7 +81,7 @@
 
     for (const auto& face : multicastFaces) {
       parameters.setFaceId(face);
-      m_controller.start<ndn::nfd::RibRegisterCommand>(
+      m_controller.start<nfd::RibRegisterCommand>(
         parameters,
         bind(&MulticastDiscovery::onRegisterSuccess, this),
         bind(&MulticastDiscovery::onRegisterFailure, this, _1));
@@ -102,7 +100,7 @@
 }
 
 void
-MulticastDiscovery::onRegisterFailure(const ControlResponse& response)
+MulticastDiscovery::onRegisterFailure(const nfd::ControlResponse& response)
 {
   std::cerr << "ERROR: " << response.getText() << " (code: " << response.getCode() << ")" << std::endl;
   --m_nRequestedRegs;
@@ -112,8 +110,8 @@
       MulticastDiscovery::setStrategy();
     }
     else {
-      m_nextStageOnFailure("Failed to register " + LOCALHOP_HUB_DISCOVERY_PREFIX.toUri() +
-                           " for all multicast faces, skipping multicast discovery stage");
+      this->fail("Failed to register " + LOCALHOP_HUB_DISCOVERY_PREFIX.toUri() +
+                 " for all multicast faces, skipping multicast discovery stage");
     }
   }
 }
@@ -121,23 +119,23 @@
 void
 MulticastDiscovery::setStrategy()
 {
-  ControlParameters parameters;
+  nfd::ControlParameters parameters;
   parameters
     .setName(LOCALHOP_HUB_DISCOVERY_PREFIX)
     .setStrategy("/localhost/nfd/strategy/multicast");
 
-  m_controller.start<ndn::nfd::StrategyChoiceSetCommand>(
+  m_controller.start<nfd::StrategyChoiceSetCommand>(
     parameters,
     bind(&MulticastDiscovery::requestHubData, this),
     bind(&MulticastDiscovery::onSetStrategyFailure, this, _1));
 }
 
 void
-MulticastDiscovery::onSetStrategyFailure(const ControlResponse& response)
+MulticastDiscovery::onSetStrategyFailure(const nfd::ControlResponse& response)
 {
-  m_nextStageOnFailure("Failed to set multicast strategy for " +
-                       LOCALHOP_HUB_DISCOVERY_PREFIX.toUri() + " namespace (" + response.getText() + "). "
-                       "Skipping multicast discovery stage");
+  this->fail("Failed to set multicast strategy for " +
+             LOCALHOP_HUB_DISCOVERY_PREFIX.toUri() + " namespace (" + response.getText() + "). "
+             "Skipping multicast discovery stage");
 }
 
 void
@@ -149,8 +147,8 @@
 
   m_face.expressInterest(interest,
                          bind(&MulticastDiscovery::onSuccess, this, _2),
-                         bind(m_nextStageOnFailure, "HUB Data not received: nacked"),
-                         bind(m_nextStageOnFailure, "HUB Data not received: timeout"));
+                         bind(&MulticastDiscovery::fail, this, "HUB Data not received: nacked"),
+                         bind(&MulticastDiscovery::fail, this, "HUB Data not received: timeout"));
 }
 
 void
@@ -162,11 +160,10 @@
   // Get Uri
   Block::element_const_iterator blockValue = content.find(tlv::nfd::Uri);
   if (blockValue == content.elements_end()) {
-    m_nextStageOnFailure("Incorrect reply to multicast discovery stage");
+    this->fail("Incorrect reply to multicast discovery stage");
     return;
   }
-  std::string hubUri(reinterpret_cast<const char*>(blockValue->value()), blockValue->value_size());
-  this->connectToHub(hubUri);
+  this->provideHubFaceUri(std::string(reinterpret_cast<const char*>(blockValue->value()), blockValue->value_size()));
 }
 
 } // namespace autoconfig
diff --git a/tools/ndn-autoconfig/multicast-discovery.hpp b/tools/ndn-autoconfig/multicast-discovery.hpp
index 1a10d89..d714306 100644
--- a/tools/ndn-autoconfig/multicast-discovery.hpp
+++ b/tools/ndn-autoconfig/multicast-discovery.hpp
@@ -27,6 +27,9 @@
 #define NFD_TOOLS_NDN_AUTOCONFIG_MULTICAST_DISCOVERY_HPP
 
 #include "stage.hpp"
+#include <ndn-cxx/face.hpp>
+#include <ndn-cxx/mgmt/nfd/controller.hpp>
+#include <ndn-cxx/mgmt/nfd/face-status.hpp>
 
 namespace ndn {
 namespace tools {
@@ -53,29 +56,36 @@
   /**
    * @brief Create multicast discovery stage
    */
-  MulticastDiscovery(Face& face, KeyChain& keyChain, const NextStageCallback& nextStageOnFailure);
+  MulticastDiscovery(Face& face, nfd::Controller& controller);
 
-  void
-  start() override;
+  const std::string&
+  getName() const override
+  {
+    static const std::string STAGE_NAME("multicast discovery");
+    return STAGE_NAME;
+  }
 
 private:
   void
+  doStart() override;
+
+  void
   collectMulticastFaces();
 
   void
-  registerHubDiscoveryPrefix(const std::vector<ndn::nfd::FaceStatus>& dataset);
+  registerHubDiscoveryPrefix(const std::vector<nfd::FaceStatus>& dataset);
 
   void
   onRegisterSuccess();
 
   void
-  onRegisterFailure(const ControlResponse& response);
+  onRegisterFailure(const nfd::ControlResponse& response);
 
   void
   setStrategy();
 
   void
-  onSetStrategyFailure(const ControlResponse& response);
+  onSetStrategyFailure(const nfd::ControlResponse& response);
 
   // Start to look for a hub (NDN hub discovery first stage)
   void
@@ -85,6 +95,8 @@
   onSuccess(const Data& data);
 
 private:
+  Face& m_face;
+  nfd::Controller& m_controller;
   size_t m_nRequestedRegs;
   size_t m_nFinishedRegs;
 };
diff --git a/tools/ndn-autoconfig/ndn-fch-discovery.cpp b/tools/ndn-autoconfig/ndn-fch-discovery.cpp
index 6f043a3..01c169e 100644
--- a/tools/ndn-autoconfig/ndn-fch-discovery.cpp
+++ b/tools/ndn-autoconfig/ndn-fch-discovery.cpp
@@ -132,19 +132,14 @@
   }
 };
 
-NdnFchDiscovery::NdnFchDiscovery(Face& face, KeyChain& keyChain,
-                                 const std::string& url,
-                                 const NextStageCallback& nextStageOnFailure)
-  : Stage(face, keyChain, nextStageOnFailure)
-  , m_url(url)
+NdnFchDiscovery::NdnFchDiscovery(const std::string& url)
+  : m_url(url)
 {
 }
 
 void
-NdnFchDiscovery::start()
+NdnFchDiscovery::doStart()
 {
-  std::cerr << "Trying NDN-FCH service..." << std::endl;
-
   try {
     using namespace boost::asio::ip;
     tcp::iostream requestStream;
@@ -205,10 +200,10 @@
       BOOST_THROW_EXCEPTION(HttpException("NDN-FCH did not return hub host"));
     }
 
-    this->connectToHub("udp://" + hubHost);
+    this->provideHubFaceUri("udp://" + hubHost);
   }
   catch (const std::runtime_error& e) {
-    m_nextStageOnFailure(std::string("Failed to find NDN router using NDN-FCH service (") + e.what() + ")");
+    this->fail(e.what());
   }
 }
 
diff --git a/tools/ndn-autoconfig/ndn-fch-discovery.hpp b/tools/ndn-autoconfig/ndn-fch-discovery.hpp
index 2b8de07..000568d 100644
--- a/tools/ndn-autoconfig/ndn-fch-discovery.hpp
+++ b/tools/ndn-autoconfig/ndn-fch-discovery.hpp
@@ -43,12 +43,19 @@
   /**
    * @brief Create stage to discover NDN hub using NDN-FCH protocol
    */
-  NdnFchDiscovery(Face& face, KeyChain& keyChain,
-                  const std::string& url,
-                  const NextStageCallback& nextStageOnFailure);
+  explicit
+  NdnFchDiscovery(const std::string& url);
 
+  const std::string&
+  getName() const override
+  {
+    static const std::string STAGE_NAME("NDN-FCH");
+    return STAGE_NAME;
+  }
+
+private:
   void
-  start() override;
+  doStart() override;
 
 private:
   std::string m_url;
diff --git a/tools/ndn-autoconfig/procedure.cpp b/tools/ndn-autoconfig/procedure.cpp
new file mode 100644
index 0000000..042ac17
--- /dev/null
+++ b/tools/ndn-autoconfig/procedure.cpp
@@ -0,0 +1,143 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2014-2017,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "procedure.hpp"
+#include "guess-from-identity-name.hpp"
+#include "guess-from-search-domains.hpp"
+#include "multicast-discovery.hpp"
+#include "ndn-fch-discovery.hpp"
+
+namespace ndn {
+namespace tools {
+namespace autoconfig {
+
+using nfd::ControlParameters;
+using nfd::ControlResponse;
+
+static const time::nanoseconds FACEURI_CANONIZE_TIMEOUT = time::seconds(4);
+static const std::vector<Name> HUB_PREFIXES{"/ndn", "/localhop/nfd"};
+static const nfd::RouteOrigin HUB_ROUTE_ORIGIN = nfd::ROUTE_ORIGIN_AUTOCONF;
+static const uint64_t HUB_ROUTE_COST = 100;
+
+Procedure::Procedure(Face& face, KeyChain& keyChain)
+  : m_face(face)
+  , m_keyChain(keyChain)
+  , m_controller(face, keyChain)
+{
+}
+
+void
+Procedure::initialize(const Options& options)
+{
+  BOOST_ASSERT(m_stages.empty());
+  this->makeStages(options);
+  BOOST_ASSERT(!m_stages.empty());
+
+  for (size_t i = 0; i < m_stages.size(); ++i) {
+    m_stages[i]->onSuccess.connect(bind(&Procedure::connect, this, _1));
+    if (i + 1 < m_stages.size()) {
+      m_stages[i]->onFailure.connect([=] (const std::string&) { m_stages[i + 1]->start(); });
+    }
+    else {
+      m_stages[i]->onFailure.connect([=] (const std::string&) { this->onComplete(false); });
+    }
+  }
+}
+
+void
+Procedure::makeStages(const Options& options)
+{
+  m_stages.push_back(make_unique<MulticastDiscovery>(m_face, m_controller));
+  m_stages.push_back(make_unique<GuessFromSearchDomains>());
+  m_stages.push_back(make_unique<NdnFchDiscovery>(options.ndnFchUrl));
+  m_stages.push_back(make_unique<GuessFromIdentityName>(m_keyChain));
+}
+
+void
+Procedure::runOnce()
+{
+  BOOST_ASSERT(!m_stages.empty());
+  m_stages.front()->start();
+}
+
+void
+Procedure::connect(const FaceUri& hubFaceUri)
+{
+  hubFaceUri.canonize(
+    [this] (const FaceUri& canonicalUri) {
+      m_controller.start<nfd::FaceCreateCommand>(
+        ControlParameters().setUri(canonicalUri.toString()),
+        [this] (const ControlParameters& params) {
+          std::cerr << "Connected to HUB " << params.getUri() << std::endl;
+          this->registerPrefixes(params.getFaceId());
+        },
+        [this, canonicalUri] (const ControlResponse& resp) {
+          if (resp.getCode() == 409) {
+            ControlParameters params(resp.getBody());
+            std::cerr << "Already connected to HUB " << params.getUri() << std::endl;
+            this->registerPrefixes(params.getFaceId());
+          }
+          else {
+            std::cerr << "Failed to connect to HUB " << canonicalUri << ": "
+                      << resp.getText() << " (" << resp.getCode() << ")" << std::endl;
+            this->onComplete(false);
+          }
+        });
+    },
+    [this] (const std::string& reason) {
+      std::cerr << "Failed to canonize HUB FaceUri: " << reason << std::endl;
+      this->onComplete(false);
+    },
+    m_face.getIoService(), FACEURI_CANONIZE_TIMEOUT);
+}
+
+void
+Procedure::registerPrefixes(uint64_t hubFaceId, size_t index)
+{
+  if (index >= HUB_PREFIXES.size()) {
+    this->onComplete(true);
+    return;
+  }
+
+  m_controller.start<nfd::RibRegisterCommand>(
+    ControlParameters()
+      .setName(HUB_PREFIXES[index])
+      .setFaceId(hubFaceId)
+      .setOrigin(HUB_ROUTE_ORIGIN)
+      .setCost(HUB_ROUTE_COST),
+    [=] (const ControlParameters&) {
+      std::cerr << "Registered prefix " << HUB_PREFIXES[index] << std::endl;
+      this->registerPrefixes(hubFaceId, index + 1);
+    },
+    [=] (const ControlResponse& resp) {
+      std::cerr << "Failed to register " << HUB_PREFIXES[index] << ": "
+                << resp.getText() << " (" << resp.getCode() << ")" << std::endl;
+      this->onComplete(false);
+    });
+}
+
+} // namespace autoconfig
+} // namespace tools
+} // namespace ndn
diff --git a/tools/ndn-autoconfig/procedure.hpp b/tools/ndn-autoconfig/procedure.hpp
new file mode 100644
index 0000000..0f65b89
--- /dev/null
+++ b/tools/ndn-autoconfig/procedure.hpp
@@ -0,0 +1,92 @@
+/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
+/*
+ * Copyright (c) 2014-2017,  Regents of the University of California,
+ *                           Arizona Board of Regents,
+ *                           Colorado State University,
+ *                           University Pierre & Marie Curie, Sorbonne University,
+ *                           Washington University in St. Louis,
+ *                           Beijing Institute of Technology,
+ *                           The University of Memphis.
+ *
+ * This file is part of NFD (Named Data Networking Forwarding Daemon).
+ * See AUTHORS.md for complete list of NFD authors and contributors.
+ *
+ * NFD is free software: you can redistribute it and/or modify it under the terms
+ * of the GNU General Public License as published by the Free Software Foundation,
+ * either version 3 of the License, or (at your option) any later version.
+ *
+ * NFD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+ * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
+ * PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * NFD, e.g., in COPYING.md file.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef NFD_TOOLS_NDN_AUTOCONFIG_PROCEDURE_HPP
+#define NFD_TOOLS_NDN_AUTOCONFIG_PROCEDURE_HPP
+
+#include "stage.hpp"
+#include <ndn-cxx/face.hpp>
+#include <ndn-cxx/mgmt/nfd/controller.hpp>
+#include <ndn-cxx/security/key-chain.hpp>
+
+namespace ndn {
+namespace tools {
+namespace autoconfig {
+
+struct Options
+{
+  std::string ndnFchUrl = "http://ndn-fch.named-data.net"; ///< HTTP base URL of NDN-FCH service
+};
+
+class Procedure : noncopyable
+{
+public:
+  Procedure(Face& face, KeyChain& keyChain);
+
+  void
+  initialize(const Options& options);
+
+  /** \brief run HUB discovery procedure once
+   */
+  void
+  runOnce();
+
+  boost::asio::io_service&
+  getIoService()
+  {
+    return m_face.getIoService();
+  }
+
+private:
+  VIRTUAL_WITH_TESTS void
+  makeStages(const Options& options);
+
+  void
+  connect(const FaceUri& hubFaceUri);
+
+  void
+  registerPrefixes(uint64_t hubFaceId, size_t index = 0);
+
+public:
+  /** \brief signal when procedure completes
+   *
+   *  Argument indicates whether the procedure succeeds (true) or fails (false).
+   */
+  util::Signal<Procedure, bool> onComplete;
+
+PROTECTED_WITH_TESTS_ELSE_PRIVATE:
+  std::vector<unique_ptr<Stage>> m_stages;
+
+private:
+  Face& m_face;
+  KeyChain& m_keyChain;
+  nfd::Controller m_controller;
+};
+
+} // namespace autoconfig
+} // namespace tools
+} // namespace ndn
+
+#endif // NFD_TOOLS_NDN_AUTOCONFIG_PROCEDURE_HPP
diff --git a/tools/ndn-autoconfig/stage.cpp b/tools/ndn-autoconfig/stage.cpp
index dbad283..2e1e048 100644
--- a/tools/ndn-autoconfig/stage.cpp
+++ b/tools/ndn-autoconfig/stage.cpp
@@ -29,106 +29,46 @@
 namespace tools {
 namespace autoconfig {
 
-Stage::Stage(Face& face, KeyChain& keyChain, const NextStageCallback& nextStageOnFailure)
-  : m_face(face)
-  , m_keyChain(keyChain)
-  , m_controller(face, keyChain)
-  , m_nextStageOnFailure(nextStageOnFailure)
-{
-}
-
 void
-Stage::connectToHub(const std::string& uri)
+Stage::start()
 {
-  FaceUri faceUri(uri);
-  std::cerr << "About to connect to: " << uri << std::endl;
-
-  faceUri.canonize(bind(&Stage::onCanonizeSuccess, this, _1),
-                   bind(&Stage::onCanonizeFailure, this, _1),
-                   m_face.getIoService(), time::seconds(4));
-
-}
-
-
-void
-Stage::onCanonizeSuccess(const FaceUri& canonicalUri)
-{
-  m_controller.start<ndn::nfd::FaceCreateCommand>(
-    ControlParameters().setUri(canonicalUri.toString()),
-    bind(&Stage::onHubConnectSuccess, this, _1),
-    bind(&Stage::onHubConnectError, this, _1));
-}
-
-void
-Stage::onCanonizeFailure(const std::string& reason)
-{
-  BOOST_THROW_EXCEPTION(Error("FaceUri canonization failed: " + reason));
-}
-
-void
-Stage::onHubConnectSuccess(const ControlParameters& resp)
-{
-  std::cerr << "Successfully created face: " << resp << std::endl;
-
-  registerAutoConfigNames(resp.getFaceId());
-}
-
-void
-Stage::onHubConnectError(const ControlResponse& response)
-{
-  // If face exists, continue proceeding with the existing face
-  if (response.getCode() == 409) {
-    std::cerr << "Face exists. Proceeding with existing face: " << ControlParameters(response.getBody()) << std::endl;
-
-    registerAutoConfigNames(ControlParameters(response.getBody()).getFaceId());
+  if (m_isInProgress) {
+    BOOST_THROW_EXCEPTION(Error("Cannot start a stage when it's in progress"));
   }
-  // Otherwise, report the failure and throw out exception
+  m_isInProgress = true;
+
+  std::cerr << "Starting " << this->getName() << " stage" << std::endl;
+  this->doStart();
+}
+
+void
+Stage::provideHubFaceUri(const std::string& s)
+{
+  FaceUri u;
+  if (u.parse(s)) {
+    this->succeed(u);
+  }
   else {
-    std::ostringstream os;
-    os << "Failed to create face: " << response.getText() << " (code: " << response.getCode() << ")";
-    BOOST_THROW_EXCEPTION(Error(os.str()));
+    this->fail("Cannot parse FaceUri: " + s);
   }
 }
 
 void
-Stage::registerAutoConfigNames(uint64_t faceId)
+Stage::succeed(const FaceUri& hubFaceUri)
 {
-  static const Name TESTBED_PREFIX = "/ndn";
-  registerPrefix(TESTBED_PREFIX, faceId);
-
-  static const Name LOCALHOP_NFD_PREFIX = "/localhop/nfd";
-  registerPrefix(LOCALHOP_NFD_PREFIX, faceId);
+  std::cerr << "Stage " << this->getName() << " succeeded with " << hubFaceUri << std::endl;
+  this->onSuccess(hubFaceUri);
+  m_isInProgress = false;
 }
 
 void
-Stage::registerPrefix(const Name& prefix, uint64_t faceId)
+Stage::fail(const std::string& msg)
 {
-  // Register a prefix in RIB
-  m_controller.start<ndn::nfd::RibRegisterCommand>(
-    ControlParameters()
-      .setName(prefix)
-      .setFaceId(faceId)
-      .setOrigin(ndn::nfd::ROUTE_ORIGIN_AUTOCONF)
-      .setCost(100)
-      .setExpirationPeriod(time::milliseconds::max()),
-    bind(&Stage::onPrefixRegistrationSuccess, this, _1),
-    bind(&Stage::onPrefixRegistrationError, this, _1));
+  std::cerr << "Stage " << this->getName() << " failed: " << msg << std::endl;
+  this->onFailure(msg);
+  m_isInProgress = false;
 }
 
-void
-Stage::onPrefixRegistrationSuccess(const ControlParameters& commandSuccessResult)
-{
-  std::cerr << "Successful in name registration: " << commandSuccessResult << std::endl;
-}
-
-void
-Stage::onPrefixRegistrationError(const ControlResponse& response)
-{
-  BOOST_THROW_EXCEPTION(Error("Failed in name registration, " + response.getText() +
-                              " (code: " + to_string(response.getCode()) + ")"));
-}
-
-
 } // namespace autoconfig
 } // namespace tools
 } // namespace ndn
diff --git a/tools/ndn-autoconfig/stage.hpp b/tools/ndn-autoconfig/stage.hpp
index 2c63280..015153d 100644
--- a/tools/ndn-autoconfig/stage.hpp
+++ b/tools/ndn-autoconfig/stage.hpp
@@ -27,22 +27,14 @@
 #define NFD_TOOLS_NDN_AUTOCONFIG_STAGE_HPP
 
 #include "core/common.hpp"
-
-#include <ndn-cxx/face.hpp>
-#include <ndn-cxx/mgmt/nfd/controller.hpp>
-#include <ndn-cxx/mgmt/nfd/face-status.hpp>
 #include <ndn-cxx/net/face-uri.hpp>
-#include <ndn-cxx/security/key-chain.hpp>
+#include <ndn-cxx/util/signal.hpp>
 
 namespace ndn {
 namespace tools {
 namespace autoconfig {
 
-using ndn::nfd::ControlParameters;
-using ndn::nfd::ControlResponse;
-
-/**
- * @brief Base class for discovery stages
+/** \brief a discovery stage
  */
 class Stage : boost::noncopyable
 {
@@ -57,63 +49,51 @@
     }
   };
 
-  /**
-   * @brief Callback to be called when the stage fails
-   */
-  typedef std::function<void(const std::string&)> NextStageCallback;
+  virtual ~Stage() = default;
 
-  /**
-   * @brief Start the stage
+  /** \brief get stage name
+   *  \return stage name as a phrase, typically starting with lower case
    */
-  virtual void
-  start() = 0;
+  virtual const std::string&
+  getName() const = 0;
 
-protected:
-  /**
-   * @brief Initialize variables and create Controller instance
-   * @param face Face to be used for all operations (e.g., will send registration commands)
-   * @param keyChain KeyChain object
-   * @param nextStageOnFailure Callback to be called after the stage failed
-   */
-  Stage(Face& face, KeyChain& keyChain, const NextStageCallback& nextStageOnFailure);
-
-  /**
-   * @brief Attempt to connect to local hub using the \p uri FaceUri
-   * @throw Error when failed to establish the tunnel
+  /** \brief start running this stage
+   *  \throw Error stage is already running
    */
   void
-  connectToHub(const std::string& uri);
+  start();
+
+protected:
+  /** \brief parse HUB FaceUri from string and declare success
+   */
+  void
+  provideHubFaceUri(const std::string& s);
+
+  void
+  succeed(const FaceUri& hubFaceUri);
+
+  void
+  fail(const std::string& msg);
 
 private:
-  void
-  onCanonizeSuccess(const FaceUri& canonicalUri);
+  virtual void
+  doStart() = 0;
 
-  void
-  onCanonizeFailure(const std::string& reason);
+public:
+  /** \brief signal when a HUB FaceUri is found
+   *
+   *  Argument is HUB FaceUri, may not be canonical.
+   */
+  util::Signal<Stage, FaceUri> onSuccess;
 
-  void
-  onHubConnectSuccess(const ControlParameters& resp);
+  /** \brief signal when discovery fails
+   *
+   *  Argument is error message.
+   */
+  util::Signal<Stage, std::string> onFailure;
 
-  void
-  onHubConnectError(const ControlResponse& response);
-
-  void
-  registerAutoConfigNames(uint64_t faceId);
-
-  void
-  registerPrefix(const Name& prefix, uint64_t faceId);
-
-  void
-  onPrefixRegistrationSuccess(const ControlParameters& commandSuccessResult);
-
-  void
-  onPrefixRegistrationError(const ControlResponse& response);
-
-protected:
-  Face& m_face;
-  KeyChain& m_keyChain;
-  ndn::nfd::Controller m_controller;
-  NextStageCallback m_nextStageOnFailure;
+private:
+  bool m_isInProgress = false;
 };
 
 } // namespace autoconfig