tools: nfdc face destroy command

refs #3864

Change-Id: I1d070570c14364529c566273eba44b87413942b1
diff --git a/docs/manpages/nfdc-face.rst b/docs/manpages/nfdc-face.rst
index 86935d4..46d0ef5 100644
--- a/docs/manpages/nfdc-face.rst
+++ b/docs/manpages/nfdc-face.rst
@@ -6,7 +6,7 @@
 | nfdc face [list]
 | nfdc face show <FACEID>
 | nfdc face create [remote] <FACEURI> [[persistency] <PERSISTENCY>]
-| nfdc destroy <FACEID|FACEURI>
+| nfdc face destroy <FACEID|FACEURI>
 | nfdc channel [list]
 
 DESCRIPTION
@@ -22,8 +22,7 @@
 
 The **nfdc face create** command creates a unicast UDP or TCP face.
 
-The **nfdc destroy** command destroys an existing face.
-It has no effect if the specified face does not exist.
+The **nfdc face destroy** command destroys an existing face.
 
 The **nfdc channel list** command shows a list of channels.
 Channels are listening sockets that can accept incoming connections and create new faces.
@@ -57,9 +56,11 @@
 
 2: Malformed command line
 
-3: Face not found (**nfdc face show** only)
+3: Face not found (**nfdc face show** and **nfdc face destroy** only)
 
-4: FaceUri canonization failed (**nfdc face create** only)
+4: FaceUri canonization failed (**nfdc face create** and **nfdc face destroy** only)
+
+5: Ambiguous: multiple matching faces are found (**nfdc face destroy** only)
 
 SEE ALSO
 --------
diff --git a/tests/tools/nfdc/face-module.t.cpp b/tests/tools/nfdc/face-module.t.cpp
index 20d5514..8d465e2 100644
--- a/tests/tools/nfdc/face-module.t.cpp
+++ b/tests/tools/nfdc/face-module.t.cpp
@@ -142,6 +142,159 @@
 
 BOOST_AUTO_TEST_SUITE_END() // CreateCommand
 
+BOOST_FIXTURE_TEST_SUITE(DestroyCommand, ExecuteCommandFixture)
+
+BOOST_AUTO_TEST_CASE(NormalByFaceId)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    if (Name("/localhost/nfd/faces/query").isPrefixOf(interest.getName())) {
+      BOOST_CHECK_EQUAL(interest.getName().size(), 5);
+      FaceQueryFilter filter(interest.getName().at(4).blockFromValue());
+      // BOOST_CHECK_EQUAL(filter, FaceQueryFilter().setFaceId(10156));
+      BOOST_CHECK_EQUAL(filter.getFaceId(), 10156);
+
+      FaceStatus faceStatus;
+      faceStatus.setFaceId(10156)
+                .setLocalUri("tcp4://151.26.163.27:22967")
+                .setRemoteUri("tcp4://198.57.27.40:6363")
+                .setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERSISTENT);
+      this->sendDataset(interest.getName(), faceStatus);
+      return;
+    }
+
+    ControlParameters req = MOCK_NFD_MGMT_REQUIRE_LAST_COMMAND_IS("/localhost/nfd/faces/destroy");
+    BOOST_REQUIRE(req.hasFaceId());
+    BOOST_CHECK_EQUAL(req.getFaceId(), 10156);
+
+    ControlParameters resp;
+    resp.setFaceId(10156);
+    this->succeedCommand(resp);
+  };
+
+  this->execute("face destroy 10156");
+  BOOST_CHECK_EQUAL(exitCode, 0);
+  BOOST_CHECK(out.is_equal("face-destroyed id=10156 local=tcp4://151.26.163.27:22967 "
+                           "remote=tcp4://198.57.27.40:6363 persistency=persistent\n"));
+  BOOST_CHECK(err.is_empty());
+}
+
+BOOST_AUTO_TEST_CASE(NormalByFaceUri)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    if (Name("/localhost/nfd/faces/query").isPrefixOf(interest.getName())) {
+      BOOST_CHECK_EQUAL(interest.getName().size(), 5);
+      FaceQueryFilter filter(interest.getName().at(4).blockFromValue());
+      // BOOST_CHECK_EQUAL(filter, FaceQueryFilter().setRemoteUri("tcp4://32.121.182.82:6363"));
+      BOOST_CHECK_EQUAL(filter.getRemoteUri(), "tcp4://32.121.182.82:6363");
+
+      FaceStatus faceStatus;
+      faceStatus.setFaceId(2249)
+                .setLocalUri("tcp4://30.99.87.98:31414")
+                .setRemoteUri("tcp4://32.121.182.82:6363")
+                .setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERSISTENT);
+      this->sendDataset(interest.getName(), faceStatus);
+      return;
+    }
+
+    ControlParameters req = MOCK_NFD_MGMT_REQUIRE_LAST_COMMAND_IS("/localhost/nfd/faces/destroy");
+    BOOST_REQUIRE(req.hasFaceId());
+    BOOST_CHECK_EQUAL(req.getFaceId(), 2249);
+
+    ControlParameters resp;
+    resp.setFaceId(2249);
+    this->succeedCommand(resp);
+  };
+
+  this->execute("face destroy tcp://32.121.182.82");
+  BOOST_CHECK_EQUAL(exitCode, 0);
+  BOOST_CHECK(out.is_equal("face-destroyed id=2249 local=tcp4://30.99.87.98:31414 "
+                           "remote=tcp4://32.121.182.82:6363 persistency=persistent\n"));
+  BOOST_CHECK(err.is_empty());
+}
+
+BOOST_AUTO_TEST_CASE(FaceNotExist)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    BOOST_CHECK(Name("/localhost/nfd/faces/query").isPrefixOf(interest.getName()));
+    this->sendEmptyDataset(interest.getName());
+  };
+
+  this->execute("face destroy 23728");
+  BOOST_CHECK_EQUAL(exitCode, 3);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Face not found\n"));
+}
+
+BOOST_AUTO_TEST_CASE(Ambiguous)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    BOOST_CHECK(Name("/localhost/nfd/faces/query").isPrefixOf(interest.getName()));
+
+    FaceStatus faceStatus1, faceStatus2;
+    faceStatus1.setFaceId(6720)
+               .setLocalUri("udp4://202.83.168.28:56363")
+               .setRemoteUri("udp4://225.131.75.231:56363")
+               .setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERMANENT);
+    faceStatus2.setFaceId(31066)
+               .setLocalUri("udp4://25.90.26.32:56363")
+               .setRemoteUri("udp4://225.131.75.231:56363")
+               .setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERMANENT);
+    this->sendDataset(interest.getName(), faceStatus1, faceStatus2);
+  };
+
+  this->execute("face destroy udp4://225.131.75.231:56363");
+  BOOST_CHECK_EQUAL(exitCode, 5);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Multiple faces match specified remote FaceUri. "
+                           "Re-run the command with a FaceId: "
+                           "6720 (local=udp4://202.83.168.28:56363), "
+                           "31066 (local=udp4://25.90.26.32:56363)\n"));
+}
+
+BOOST_AUTO_TEST_CASE(ErrorCanonization)
+{
+  this->execute("face destroy udp6://32.38.164.64:10445");
+  BOOST_CHECK_EQUAL(exitCode, 4);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Error during remote FaceUri canonization: "
+                           "No endpoints match the specified address selector\n"));
+}
+
+BOOST_AUTO_TEST_CASE(ErrorDataset)
+{
+  this->processInterest = nullptr; // no response to dataset or command
+
+  this->execute("face destroy udp://159.242.33.78");
+  BOOST_CHECK_EQUAL(exitCode, 1);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Error 10060 when querying face: Timeout\n"));
+}
+
+BOOST_AUTO_TEST_CASE(ErrorCommand)
+{
+  this->processInterest = [this] (const Interest& interest) {
+    if (Name("/localhost/nfd/faces/query").isPrefixOf(interest.getName())) {
+      FaceStatus faceStatus;
+      faceStatus.setFaceId(17757)
+                .setLocalUri("tcp4://27.65.24.30:19187")
+                .setRemoteUri("tcp4://70.47.27.77:6363")
+                .setFacePersistency(FacePersistency::FACE_PERSISTENCY_PERSISTENT);
+      this->sendDataset(interest.getName(), faceStatus);
+      return;
+    }
+
+    MOCK_NFD_MGMT_REQUIRE_LAST_COMMAND_IS("/localhost/nfd/faces/destroy");
+    // no response to command
+  };
+
+  this->execute("face destroy 17757");
+  BOOST_CHECK_EQUAL(exitCode, 1);
+  BOOST_CHECK(out.is_empty());
+  BOOST_CHECK(err.is_equal("Error 10060 when destroying face: request timed out\n"));
+}
+
+BOOST_AUTO_TEST_SUITE_END() // DestroyCommand
+
 const std::string STATUS_XML = stripXmlSpaces(R"XML(
   <faces>
     <face>
diff --git a/tools/nfdc/face-module.cpp b/tools/nfdc/face-module.cpp
index 3c087f4..4dc732e 100644
--- a/tools/nfdc/face-module.cpp
+++ b/tools/nfdc/face-module.cpp
@@ -24,6 +24,7 @@
  */
 
 #include "face-module.hpp"
+#include "find-face.hpp"
 #include "format-helpers.hpp"
 
 namespace nfd {
@@ -45,6 +46,12 @@
     .addArg("remote", ArgValueType::FACE_URI, Required::YES, Positional::YES)
     .addArg("persistency", ArgValueType::FACE_PERSISTENCY, Required::NO, Positional::YES);
   parser.addCommand(defFaceCreate, &FaceModule::create);
+
+  CommandDefinition defFaceDestroy("face", "destroy");
+  defFaceDestroy
+    .setTitle("destroy a face")
+    .addArg("face", ArgValueType::FACE_ID_OR_URI, Required::YES, Positional::YES);
+  parser.addCommand(defFaceDestroy, &FaceModule::destroy);
 }
 
 void
@@ -101,6 +108,58 @@
 }
 
 void
+FaceModule::destroy(ExecuteContext& ctx)
+{
+  const boost::any faceIdOrUri = ctx.args.at("face");
+
+  FindFace findFace(ctx);
+  FindFace::Code res = FindFace::Code::ERROR;
+  const uint64_t* faceId = boost::any_cast<uint64_t>(&faceIdOrUri);
+  if (faceId != nullptr) {
+    res = findFace.execute(*faceId);
+  }
+  else {
+    res = findFace.execute(boost::any_cast<FaceUri>(faceIdOrUri));
+  }
+
+  ctx.exitCode = static_cast<int>(res);
+  switch (res) {
+    case FindFace::Code::OK:
+      break;
+    case FindFace::Code::ERROR:
+    case FindFace::Code::CANONIZE_ERROR:
+    case FindFace::Code::NOT_FOUND:
+      ctx.err << findFace.getErrorReason() << '\n';
+      return;
+    case FindFace::Code::AMBIGUOUS:
+      ctx.err << "Multiple faces match specified remote FaceUri. Re-run the command with a FaceId:";
+      findFace.printDisambiguation(ctx.err, FindFace::DisambiguationStyle::LOCAL_URI);
+      ctx.err << '\n';
+      return;
+    default:
+      BOOST_ASSERT_MSG(false, "unexpected FindFace result");
+      return;
+  }
+
+  const FaceStatus& face = findFace.getFaceStatus();
+
+  ctx.controller.start<ndn::nfd::FaceDestroyCommand>(
+    ControlParameters().setFaceId(face.getFaceId()),
+    [&] (const ControlParameters& resp) {
+      ctx.out << "face-destroyed ";
+      text::ItemAttributes ia;
+      ctx.out << ia("id") << face.getFaceId()
+              << ia("local") << face.getLocalUri()
+              << ia("remote") << face.getRemoteUri()
+              << ia("persistency") << face.getFacePersistency() << '\n';
+    },
+    ctx.makeCommandFailureHandler("destroying face"),
+    ctx.makeCommandOptions());
+
+  ctx.face.processEvents();
+}
+
+void
 FaceModule::fetchStatus(Controller& controller,
                         const function<void()>& onSuccess,
                         const Controller::DatasetFailCallback& onFailure,
diff --git a/tools/nfdc/face-module.hpp b/tools/nfdc/face-module.hpp
index 1bbc31b..3f70d48 100644
--- a/tools/nfdc/face-module.hpp
+++ b/tools/nfdc/face-module.hpp
@@ -56,6 +56,11 @@
   static void
   create(ExecuteContext& ctx);
 
+  /** \brief the 'face destroy' command
+   */
+  static void
+  destroy(ExecuteContext& ctx);
+
   void
   fetchStatus(Controller& controller,
               const function<void()>& onSuccess,
diff --git a/tools/nfdc/find-face.cpp b/tools/nfdc/find-face.cpp
new file mode 100644
index 0000000..c03e6dc
--- /dev/null
+++ b/tools/nfdc/find-face.cpp
@@ -0,0 +1,149 @@
+/* -*- 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 "find-face.hpp"
+#include "format-helpers.hpp"
+#include <ndn-cxx/util/logger.hpp>
+
+namespace nfd {
+namespace tools {
+namespace nfdc {
+
+NDN_LOG_INIT(nfdc.FindFace);
+
+FindFace::FindFace(ExecuteContext& ctx)
+  : m_ctx(ctx)
+{
+}
+
+FindFace::Code
+FindFace::execute(const FaceUri& faceUri, bool allowMulti)
+{
+  FaceQueryFilter filter;
+  filter.setRemoteUri(faceUri.toString());
+  return this->execute(filter);
+}
+
+FindFace::Code
+FindFace::execute(uint64_t faceId)
+{
+  FaceQueryFilter filter;
+  filter.setFaceId(faceId);
+  return this->execute(filter);
+}
+
+FindFace::Code
+FindFace::execute(const FaceQueryFilter& filter, bool allowMulti)
+{
+  BOOST_ASSERT(m_res == Code::NOT_STARTED);
+  m_res = Code::IN_PROGRESS;
+  m_filter = filter;
+
+  if (m_filter.hasRemoteUri()) {
+    auto remoteUri = this->canonize("remote", FaceUri(m_filter.getRemoteUri()));
+    if (!remoteUri) {
+      m_res = Code::CANONIZE_ERROR;
+      return m_res;
+    }
+    m_filter.setRemoteUri(remoteUri->toString());
+  }
+
+  ///\todo #3864 canonize localUri
+
+  this->query();
+  if (m_res == Code::OK) {
+    if (m_results.size() == 0) {
+      m_res = Code::NOT_FOUND;
+      m_errorReason = "Face not found";
+    }
+    else if (m_results.size() > 1 && !allowMulti) {
+      m_res = Code::AMBIGUOUS;
+      m_errorReason = "Multiple faces match the query";
+    }
+  }
+  return m_res;
+}
+
+ndn::optional<FaceUri>
+FindFace::canonize(const std::string& fieldName, const FaceUri& input)
+{
+  if (!FaceUri::canCanonize(input.getScheme())) {
+    NDN_LOG_DEBUG("Using " << fieldName << '=' << input << " without canonization");
+    return input;
+  }
+
+  ndn::optional<FaceUri> result;
+  input.canonize(
+    [&result] (const FaceUri& canonicalUri) { result = canonicalUri; },
+    [this, fieldName] (const std::string& errorReason) {
+      m_errorReason = "Error during " + fieldName + " FaceUri canonization: " + errorReason;
+    },
+    m_ctx.face.getIoService(), m_ctx.getTimeout());
+  m_ctx.face.processEvents();
+
+  return result;
+}
+
+void
+FindFace::query()
+{
+  m_ctx.controller.fetch<ndn::nfd::FaceQueryDataset>(
+    m_filter,
+    [this] (const std::vector<ndn::nfd::FaceStatus>& result) {
+      m_res = Code::OK;
+      m_results = result;
+    },
+    [this] (uint32_t code, const std::string& reason) {
+      m_res = Code::ERROR;
+      m_errorReason = "Error " + std::to_string(code) + " when querying face: " + reason;
+    },
+    m_ctx.makeCommandOptions());
+  m_ctx.face.processEvents();
+}
+
+const FaceStatus&
+FindFace::getFaceStatus() const
+{
+  BOOST_ASSERT(m_results.size() == 1);
+  return m_results.front();
+}
+
+void
+FindFace::printDisambiguation(std::ostream& os, DisambiguationStyle style) const
+{
+  text::Separator sep(" ", ", ");
+  for (const auto& item : m_results) {
+    os << sep;
+    switch (style) {
+      case DisambiguationStyle::LOCAL_URI:
+        os << item.getFaceId() << " (local=" << item.getLocalUri() << ')';
+        break;
+    }
+  }
+}
+
+} // namespace nfdc
+} // namespace tools
+} // namespace nfd
diff --git a/tools/nfdc/find-face.hpp b/tools/nfdc/find-face.hpp
new file mode 100644
index 0000000..57eb0f0
--- /dev/null
+++ b/tools/nfdc/find-face.hpp
@@ -0,0 +1,137 @@
+/* -*- 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_NFDC_FIND_FACE_HPP
+#define NFD_TOOLS_NFDC_FIND_FACE_HPP
+
+#include "execute-command.hpp"
+
+namespace nfd {
+namespace tools {
+namespace nfdc {
+
+using ndn::nfd::FaceQueryFilter;
+using ndn::nfd::FaceStatus;
+
+/** \brief procedure to find a face
+ */
+class FindFace : noncopyable
+{
+public:
+  enum class Code {
+    OK = 0, ///< found exactly one face, or found multiple faces when allowMulti is true
+    ERROR = 1, ///< unspecified error
+    NOT_FOUND = 3, ///< found zero face
+    CANONIZE_ERROR = 4, ///< error during FaceUri canonization
+    AMBIGUOUS = 5, ///< found multiple faces and allowMulti is false
+    NOT_STARTED = -1, ///< for internal use
+    IN_PROGRESS = -2, ///< for internal use
+  };
+
+  enum class DisambiguationStyle
+  {
+    LOCAL_URI = 1 ///< print FaceId and LocalUri
+  };
+
+  explicit
+  FindFace(ExecuteContext& ctx);
+
+  /** \brief find face by FaceUri
+   *  \pre execute has not been invoked
+   */
+  Code
+  execute(const FaceUri& faceUri, bool allowMulti = false);
+
+  /** \brief find face by FaceId
+   *  \pre execute has not been invoked
+   */
+  Code
+  execute(uint64_t faceId);
+
+  /** \brief find face by FaceQueryFilter
+   *  \pre execute has not been invoked
+   */
+  Code
+  execute(const FaceQueryFilter& filter, bool allowMulti = false);
+
+  /** \return face status for all results
+   */
+  const std::vector<FaceStatus>&
+  getResults() const
+  {
+    return m_results;
+  }
+
+  /** \return a single face status
+   *  \pre getResults().size() == 1
+   */
+  const FaceStatus&
+  getFaceStatus() const;
+
+  uint64_t
+  getFaceId() const
+  {
+    return this->getFaceStatus().getFaceId();
+  }
+
+  const std::string&
+  getErrorReason() const
+  {
+    return m_errorReason;
+  }
+
+  /** \brief print results for disambiguation
+   */
+  void
+  printDisambiguation(std::ostream& os, DisambiguationStyle style) const;
+
+private:
+  /** \brief canonize FaceUri
+   *  \return canonical FaceUri if canonization succeeds, input if canonization is unsupported
+   *  \retval nullopt canonization fails; m_errorReason describes the failure
+   */
+  ndn::optional<FaceUri>
+  canonize(const std::string& fieldName, const FaceUri& input);
+
+  /** \brief retrieve FaceStatus from filter
+   *  \post m_res == Code::OK and m_results is populated if retrieval succeeds
+   *  \post m_res == Code::ERROR and m_errorReason is set if retrieval fails
+   */
+  void
+  query();
+
+private:
+  ExecuteContext& m_ctx;
+  FaceQueryFilter m_filter;
+  Code m_res = Code::NOT_STARTED;
+  std::vector<FaceStatus> m_results;
+  std::string m_errorReason;
+};
+
+} // namespace nfdc
+} // namespace tools
+} // namespace nfd
+
+#endif // NFD_TOOLS_NFDC_FIND_FACE_HPP
diff --git a/wscript b/wscript
index fc5d999..9bb3e15 100644
--- a/wscript
+++ b/wscript
@@ -250,8 +250,6 @@
             install_path="${MANDIR}/",
             VERSION=VERSION)
         bld.symlink_as('${MANDIR}/man1/nfdc-channel.1', 'nfdc-face.1')
-        bld.symlink_as('${MANDIR}/man1/nfdc-create.1', 'nfdc-face.1')
-        bld.symlink_as('${MANDIR}/man1/nfdc-destroy.1', 'nfdc-face.1')
         bld.symlink_as('${MANDIR}/man1/nfdc-fib.1', 'nfdc-route.1')
         bld.symlink_as('${MANDIR}/man1/nfdc-register.1', 'nfdc-route.1')
         bld.symlink_as('${MANDIR}/man1/nfdc-unregister.1', 'nfdc-route.1')