poke: code modernization and cleanup

Change-Id: Ia0d5f0b1bc495636f8fac127bec735cc35af66a7
diff --git a/manpages/ndnpeek.rst b/manpages/ndnpeek.rst
index 9468889..0b6683e 100644
--- a/manpages/ndnpeek.rst
+++ b/manpages/ndnpeek.rst
@@ -63,4 +63,4 @@
 
 Send Interest for ``/app1/video`` and print the received payload only::
 
-    ndnpeek -p ndn:/app1/video
+    ndnpeek -p /app1/video
diff --git a/manpages/ndnpoke.rst b/manpages/ndnpoke.rst
index c4c0a47..caa7753 100644
--- a/manpages/ndnpoke.rst
+++ b/manpages/ndnpoke.rst
@@ -1,54 +1,59 @@
 ndnpoke
 =======
 
-Usage
------
+Synopsis
+--------
 
-::
-
-    ndnpoke [-h] [-f] [-D] [-i identity] [-F] [-x freshness] [-w timeout] name
+**ndnpoke** [-h] [-f] [-F] [-x *freshness*] [-i *identity*\|\ -D] [-w *timeout*] [-V] *name*
 
 Description
 -----------
 
-``ndnpoke`` is a simple producer program that reads payload from stdin and publishes it
-as a single NDN Data packet.  The Data packet is published either as a response to the
-incoming Interest for the given ``name``, or forcefully pushed to the local NDN
-forwarder's cache if ``-f`` flag is specified.
-
-The program terminates with return code 0 if Data is sent and with return code 1 when
-timeout occurs.
+:program:`ndnpoke` is a simple producer program that reads a payload from the standard
+input and publishes it as a single Data packet. The Data packet is either sent as a
+response to an incoming Interest matching *name*, or immediately pushed to the local
+NDN forwarder as "unsolicited Data" if the **-f** flag is specified.
 
 Options
 -------
 
-``-h``
-  Print usage and exit.
+``-h, --help``
+  Print help and exit.
 
-``-f``
-  If specified, send Data without waiting for Interest.
+``-f, --force``
+  Send the Data packet without waiting for an incoming Interest.
 
-``-D``
-  If specified, use ``DigestSha256`` signature instead of default ``SignatureSha256WithRsa``.
+``-F, --final``
+  Set the ``FinalBlockId`` to the last component of *name*.
 
-``-i``
-  Use ``identity`` to sign the created Data packet.
+``-x, --freshness <freshness>``
+  Set ``freshness`` (in milliseconds) as the ``FreshnessPeriod``.
 
-``-F``
-  Set ``FinalBlockId`` to the last component of specified name.
+``-i, --identity <identity>``
+  Use ``identity`` to sign the Data packet.
 
-``-x``
-  Set ``FreshnessPeriod`` in milliseconds.
+``-D, --digest``
+  Use ``DigestSha256`` signature type instead of the default ``SignatureSha256WithRsa``.
 
-``-w``
-  Wait at most ``timeout`` milliseconds for the incoming Interest.  If no Interest arrives
-  within the ``timeout``, the Data packet will not be published.
+``-w, --timeout <timeout>``
+  Quit the program after ``timeout`` milliseconds, even if no Interest has been received.
 
+``-V, --version``
+  Print version and exit.
 
-Examples
---------
+Exit Status
+-----------
 
-Create Data packet with content ``hello`` with the name ``ndn:/app/video`` and wait at
-most 3 seconds for the incoming Interest for it::
+0: Success
 
-    echo "Hello" | build/bin/ndnpoke -w 3000 ndn:/app/video
+1: An unspecified error occurred
+
+2: Malformed command line
+
+Example
+-------
+
+Create a Data packet with content ``hello`` and name ``/app/video`` and wait at
+most 3 seconds for a matching Interest to arrive::
+
+    echo "hello" | ndnpoke -w 3000 /app/video
diff --git a/tests/peek/ndnpoke.t.cpp b/tests/peek/ndnpoke.t.cpp
index f569f03..3f9a59b 100644
--- a/tests/peek/ndnpoke.t.cpp
+++ b/tests/peek/ndnpoke.t.cpp
@@ -1,6 +1,6 @@
 /* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
 /*
- * Copyright (c) 2014-2018,  Regents of the University of California,
+ * Copyright (c) 2014-2019,  Regents of the University of California,
  *                           Arizona Board of Regents,
  *                           Colorado State University,
  *                           University Pierre & Marie Curie, Sorbonne University,
@@ -26,6 +26,7 @@
 #include "tools/peek/ndnpoke/ndnpoke.hpp"
 
 #include "tests/test-common.hpp"
+#include "tests/identity-management-fixture.hpp"
 
 #include <ndn-cxx/util/dummy-client-face.hpp>
 
@@ -35,37 +36,33 @@
 
 using namespace ndn::tests;
 
-class NdnPokeFixture : public UnitTestTimeFixture
+class NdnPokeFixture : public IdentityManagementTimeFixture
 {
 protected:
   NdnPokeFixture()
-    : face(io, keyChain, {true, true})
   {
-    keyChain.createIdentity("/test-id");
-
-    inData << "Hello world!";
+    m_keyChain.createIdentity("/test-id");
   }
 
   void
-  initialize(const PokeOptions& opts)
+  initialize(const PokeOptions& opts = makeDefaultOptions())
   {
-    poke = make_unique<NdnPoke>(face, keyChain, inData, opts);
+    poke = make_unique<NdnPoke>(face, m_keyChain, payload, opts);
   }
 
   static PokeOptions
   makeDefaultOptions()
   {
     PokeOptions opt;
-    opt.prefixName = "/poke/test";
+    opt.name = "/poke/test";
     return opt;
   }
 
 protected:
   boost::asio::io_service io;
-  ndn::util::DummyClientFace face;
-  KeyChain keyChain;
+  ndn::util::DummyClientFace face{io, m_keyChain, {true, true}};
+  std::stringstream payload{"Hello, world!\n"};
   unique_ptr<NdnPoke> poke;
-  std::stringstream inData;
 };
 
 BOOST_AUTO_TEST_SUITE(Peek)
@@ -73,11 +70,9 @@
 
 BOOST_AUTO_TEST_CASE(Basic)
 {
-  auto options = makeDefaultOptions();
-  initialize(options);
+  initialize();
 
   poke->start();
-
   this->advanceClocks(io, 1_ms, 10);
 
   // Check for prefix registration
@@ -88,11 +83,13 @@
   this->advanceClocks(io, 1_ms, 10);
   io.run();
 
-  BOOST_CHECK(poke->wasDataSent());
+  BOOST_CHECK(poke->didSendData());
   BOOST_REQUIRE_EQUAL(face.sentData.size(), 1);
   BOOST_CHECK_EQUAL(face.sentData.back().getName(), "/poke/test");
   BOOST_CHECK(!face.sentData.back().getFinalBlock());
   BOOST_CHECK_EQUAL(face.sentData.back().getFreshnessPeriod(), 0_ms);
+  BOOST_CHECK_EQUAL(face.sentData.back().getContentType(), tlv::ContentType_Blob);
+  BOOST_CHECK_EQUAL(face.sentData.back().getContent(), "150E48656C6C6F2C20776F726C64210A"_block);
   BOOST_CHECK_EQUAL(face.sentData.back().getSignature().getType(), tlv::SignatureSha256WithEcdsa);
 
   // Check for prefix unregistration
@@ -100,45 +97,20 @@
   BOOST_CHECK_EQUAL(face.sentInterests.back().getName().getPrefix(4), "/localhost/nfd/rib/unregister");
 }
 
-BOOST_AUTO_TEST_CASE(FinalBlockId)
-{
-  auto options = makeDefaultOptions();
-  options.prefixName = "/poke/test/123";
-  options.wantLastAsFinalBlockId = true;
-  initialize(options);
-
-  poke->start();
-
-  this->advanceClocks(io, 1_ms, 10);
-
-  face.receive(*makeInterest("/poke/test/123"));
-  this->advanceClocks(io, 1_ms, 10);
-  io.run();
-
-  BOOST_CHECK(poke->wasDataSent());
-  BOOST_REQUIRE_EQUAL(face.sentData.size(), 1);
-  BOOST_CHECK_EQUAL(face.sentData.back().getName(), "/poke/test/123");
-  BOOST_REQUIRE(face.sentData.back().getFinalBlock());
-  BOOST_CHECK_EQUAL(*(face.sentData.back().getFinalBlock()), name::Component("123"));
-  BOOST_CHECK_EQUAL(face.sentData.back().getFreshnessPeriod(), 0_ms);
-  BOOST_CHECK_EQUAL(face.sentData.back().getSignature().getType(), tlv::SignatureSha256WithEcdsa);
-}
-
 BOOST_AUTO_TEST_CASE(FreshnessPeriod)
 {
   auto options = makeDefaultOptions();
-  options.freshnessPeriod = make_optional<time::milliseconds>(1_s);
+  options.freshnessPeriod = 1_s;
   initialize(options);
 
   poke->start();
-
   this->advanceClocks(io, 1_ms, 10);
 
   face.receive(*makeInterest("/poke/test"));
   this->advanceClocks(io, 1_ms, 10);
   io.run();
 
-  BOOST_CHECK(poke->wasDataSent());
+  BOOST_CHECK(poke->didSendData());
   BOOST_REQUIRE_EQUAL(face.sentData.size(), 1);
   BOOST_CHECK_EQUAL(face.sentData.back().getName(), "/poke/test");
   BOOST_CHECK(!face.sentData.back().getFinalBlock());
@@ -146,6 +118,29 @@
   BOOST_CHECK_EQUAL(face.sentData.back().getSignature().getType(), tlv::SignatureSha256WithEcdsa);
 }
 
+BOOST_AUTO_TEST_CASE(FinalBlockId)
+{
+  auto options = makeDefaultOptions();
+  options.name = "/poke/test/123";
+  options.wantFinalBlockId = true;
+  initialize(options);
+
+  poke->start();
+  this->advanceClocks(io, 1_ms, 10);
+
+  face.receive(*makeInterest(options.name));
+  this->advanceClocks(io, 1_ms, 10);
+  io.run();
+
+  BOOST_CHECK(poke->didSendData());
+  BOOST_REQUIRE_EQUAL(face.sentData.size(), 1);
+  BOOST_CHECK_EQUAL(face.sentData.back().getName(), options.name);
+  BOOST_REQUIRE(face.sentData.back().getFinalBlock());
+  BOOST_CHECK_EQUAL(*(face.sentData.back().getFinalBlock()), name::Component("123"));
+  BOOST_CHECK_EQUAL(face.sentData.back().getFreshnessPeriod(), 0_ms);
+  BOOST_CHECK_EQUAL(face.sentData.back().getSignature().getType(), tlv::SignatureSha256WithEcdsa);
+}
+
 BOOST_AUTO_TEST_CASE(DigestSha256)
 {
   auto options = makeDefaultOptions();
@@ -153,14 +148,13 @@
   initialize(options);
 
   poke->start();
-
   this->advanceClocks(io, 1_ms, 10);
 
   face.receive(*makeInterest("/poke/test"));
   this->advanceClocks(io, 1_ms, 10);
   io.run();
 
-  BOOST_CHECK(poke->wasDataSent());
+  BOOST_CHECK(poke->didSendData());
   BOOST_REQUIRE_EQUAL(face.sentData.size(), 1);
   BOOST_CHECK_EQUAL(face.sentData.back().getName(), "/poke/test");
   BOOST_CHECK(!face.sentData.back().getFinalBlock());
@@ -175,10 +169,9 @@
   initialize(options);
 
   poke->start();
-
   this->advanceClocks(io, 1_ms, 10);
 
-  BOOST_CHECK(poke->wasDataSent());
+  BOOST_CHECK(poke->didSendData());
   BOOST_REQUIRE_EQUAL(face.sentData.size(), 1);
   BOOST_CHECK_EQUAL(face.sentData.back().getName(), "/poke/test");
   BOOST_CHECK(!face.sentData.back().getFinalBlock());
@@ -186,24 +179,42 @@
   BOOST_CHECK_EQUAL(face.sentData.back().getSignature().getType(), tlv::SignatureSha256WithEcdsa);
 }
 
-BOOST_AUTO_TEST_CASE(ExceedMaxPacketSize)
+BOOST_AUTO_TEST_CASE(Timeout)
 {
-  for (size_t i = 0; i < MAX_NDN_PACKET_SIZE; i++) {
-    inData << "A";
-  }
-
   auto options = makeDefaultOptions();
+  options.timeout = 4_s;
   initialize(options);
 
   poke->start();
+  this->advanceClocks(io, 1_ms, 10);
 
+  // Check for prefix registration
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 1);
+  BOOST_CHECK_EQUAL(face.sentInterests.front().getName().getPrefix(4), "/localhost/nfd/rib/register");
+
+  this->advanceClocks(io, 1_s, 4);
+
+  BOOST_CHECK(!poke->didSendData());
+  BOOST_CHECK_EQUAL(face.sentData.size(), 0);
+
+  // Check for prefix unregistration
+  BOOST_REQUIRE_EQUAL(face.sentInterests.size(), 2); // One for registration, one for unregistration
+  BOOST_CHECK_EQUAL(face.sentInterests.back().getName().getPrefix(4), "/localhost/nfd/rib/unregister");
+}
+
+BOOST_AUTO_TEST_CASE(OversizedPacket)
+{
+  payload << std::string(MAX_NDN_PACKET_SIZE, 'A');
+  initialize();
+
+  poke->start();
   this->advanceClocks(io, 1_ms, 10);
 
   face.receive(*makeInterest("/poke/test"));
   BOOST_CHECK_THROW(face.processEvents(), Face::OversizedPacketError);
 
-  // We can't check wasDataSent() correctly here because it will be set to true, even if put failed
-  // due to the packet being oversized.
+  // No point in checking didSendData() here. The exception is thrown from processEvents(),
+  // not from put(), so didSendData() is still set to true even though no packets are sent.
   BOOST_CHECK_EQUAL(face.sentData.size(), 0);
 }
 
diff --git a/tools/peek/README.md b/tools/peek/README.md
index 5c6fb35..79e9832 100644
--- a/tools/peek/README.md
+++ b/tools/peek/README.md
@@ -1,14 +1,14 @@
 # ndnpeek and ndnpoke
 
-**ndnpeek** and **ndnpoke** are a pair of programs to request and make available for retrieval of a single Data packet.
+**ndnpeek** and **ndnpoke** are a pair of programs to respectively request and serve a single Data packet.
 
 * **ndnpeek** is a consumer program that sends one Interest and expects one Data.
 * **ndnpoke** is a producer program that serves one Data in response to an Interest.
 
 Usage example:
 
-1. start NFD on local machine
-2. execute `echo 'HELLO WORLD' | ndnpoke ndn:/localhost/demo/hello`
-3. on another console, execute `ndnpeek -p ndn:/localhost/demo/hello`
+1. start NFD
+2. execute `echo 'HELLO WORLD' | ndnpoke /localhost/demo/hello`
+3. in another terminal, execute `ndnpeek -p /localhost/demo/hello`
 
-For more information, consult manpages of these programs.
+For more information, consult the manpages of these programs.
diff --git a/tools/peek/ndnpeek/main.cpp b/tools/peek/ndnpeek/main.cpp
index 1586387..f763b96 100644
--- a/tools/peek/ndnpeek/main.cpp
+++ b/tools/peek/ndnpeek/main.cpp
@@ -39,7 +39,7 @@
 static void
 usage(std::ostream& os, const std::string& program, const po::options_description& options)
 {
-  os << "Usage: " << program << " [options] ndn:/name\n"
+  os << "Usage: " << program << " [options] /name\n"
      << "\n"
      << "Fetch one data item matching the specified name and write it to the standard output.\n"
      << options;
diff --git a/tools/peek/ndnpoke/main.cpp b/tools/peek/ndnpoke/main.cpp
index 2086a2b..33d91ea 100644
--- a/tools/peek/ndnpoke/main.cpp
+++ b/tools/peek/ndnpoke/main.cpp
@@ -28,47 +28,40 @@
 #include "ndnpoke.hpp"
 #include "core/version.hpp"
 
-#include <ndn-cxx/util/io.hpp>
-
-#include <sstream>
-
 namespace ndn {
 namespace peek {
 
 namespace po = boost::program_options;
 
 static void
-usage(std::ostream& os, const po::options_description& options)
+usage(std::ostream& os, const std::string& program, const po::options_description& options)
 {
-  os << "Usage: ndnpoke [options] ndn:/name\n"
-        "\n"
-        "Reads payload from stdin and sends it to the local NDN forwarder as a single Data packet\n"
-        "\n"
+  os << "Usage: " << program << " [options] /name\n"
+     << "\n"
+     << "Reads a payload from the standard input and sends it as a single Data packet.\n"
      << options;
 }
 
 static int
 main(int argc, char* argv[])
 {
+  std::string progName(argv[0]);
   PokeOptions options;
-  bool wantDigestSha256;
+  bool wantDigestSha256 = false;
 
   po::options_description visibleOptDesc;
   visibleOptDesc.add_options()
-    ("help,h", "print help and exit")
-    ("version,V", "print version and exit")
-    ("force,f", po::bool_switch(&options.wantForceData),
-        "for, send Data without waiting for Interest")
-    ("digest,D", po::bool_switch(&wantDigestSha256),
-        "use DigestSha256 signing method instead of SignatureSha256WithRsa")
-    ("identity,i", po::value<std::string>(),
-        "set identity to be used for signing")
-    ("final,F", po::bool_switch(&options.wantLastAsFinalBlockId),
-        "set FinalBlockId to the last component of Name")
-    ("freshness,x", po::value<int>(),
-        "set FreshnessPeriod in milliseconds")
-    ("timeout,w", po::value<int>(),
-        "set Timeout in milliseconds")
+    ("help,h",      "print help and exit")
+    ("force,f",     po::bool_switch(&options.wantForceData),
+                    "send the Data packet without waiting for an incoming Interest")
+    ("final,F",     po::bool_switch(&options.wantFinalBlockId),
+                    "set FinalBlockId to the last component of the Data name")
+    ("freshness,x", po::value<int>(), "set FreshnessPeriod (in milliseconds)")
+    ("identity,i",  po::value<std::string>(), "use the specified identity for signing")
+    ("digest,D",    po::bool_switch(&wantDigestSha256),
+                    "use DigestSha256 signing method instead of SignatureSha256WithRsa")
+    ("timeout,w",   po::value<int>(), "set timeout (in milliseconds)")
+    ("version,V",   "print version and exit")
   ;
 
   po::options_description hiddenOptDesc;
@@ -91,11 +84,8 @@
     return 2;
   }
 
-  // We store timeout here, instead of PokeOptions, because processEvents is called outside the NdnPoke class
-  time::milliseconds timeout = 10_s;
-
   if (vm.count("help") > 0) {
-    usage(std::cout, visibleOptDesc);
+    usage(std::cout, progName, visibleOptDesc);
     return 0;
   }
 
@@ -104,78 +94,75 @@
     return 0;
   }
 
-  if (vm.count("name") > 0) {
-    options.prefixName = vm["name"].as<std::string>();
-  }
-  else {
-    std::cerr << "ERROR: Data name is missing" << std::endl;
-    usage(std::cerr, visibleOptDesc);
+  if (vm.count("name") == 0) {
+    std::cerr << "ERROR: missing name\n\n";
+    usage(std::cerr, progName, visibleOptDesc);
     return 2;
   }
 
+  try {
+    options.name = vm["name"].as<std::string>();
+  }
+  catch (const Name::Error& e) {
+    std::cerr << "ERROR: invalid name: " << e.what() << std::endl;
+    return 2;
+  }
+
+  if (options.name.empty()) {
+    std::cerr << "ERROR: name cannot have zero components" << std::endl;
+    return 2;
+  }
+
+  if (vm.count("freshness") > 0) {
+    if (vm["freshness"].as<int>() < 0) {
+      std::cerr << "ERROR: freshness cannot be negative" << std::endl;
+      return 2;
+    }
+    options.freshnessPeriod = time::milliseconds(vm["freshness"].as<int>());
+  }
+
+  if (vm.count("identity") > 0) {
+    if (wantDigestSha256) {
+      std::cerr << "ERROR: conflicting '--digest' and '--identity' options specified" << std::endl;
+      return 2;
+    }
+    try {
+      options.signingInfo.setSigningIdentity(vm["identity"].as<std::string>());
+    }
+    catch (const Name::Error& e) {
+      std::cerr << "ERROR: invalid identity name: " << e.what() << std::endl;
+      return 2;
+    }
+  }
+
   if (wantDigestSha256) {
     options.signingInfo.setSha256Signing();
   }
 
-  if (vm.count("identity") > 0) {
-    if (wantDigestSha256) {
-      std::cerr << "ERROR: Signing identity cannot be specified when using DigestSha256 signing method" << std::endl;
-      usage(std::cerr, visibleOptDesc);
-      return 2;
-    }
-    options.signingInfo.setSigningIdentity(vm["identity"].as<std::string>());
-  }
-
-  if (vm.count("final") > 0) {
-    if (!options.prefixName.empty()) {
-      options.wantLastAsFinalBlockId = true;
-    }
-    else {
-      std::cerr << "The provided Name must have 1 or more components to be used with FinalBlockId option" << std::endl;
-      usage(std::cerr, visibleOptDesc);
-      return 1;
-    }
-  }
-
-  if (vm.count("freshness") > 0) {
-    if (vm["freshness"].as<int>() >= 0) {
-      options.freshnessPeriod = time::milliseconds(vm["freshness"].as<int>());
-    }
-    else {
-      std::cerr << "ERROR: FreshnessPeriod must be a non-negative integer" << std::endl;
-      usage(std::cerr, visibleOptDesc);
-      return 2;
-    }
-  }
-
   if (vm.count("timeout") > 0) {
-    if (vm["timeout"].as<int>() > 0) {
-      timeout = time::milliseconds(vm["timeout"].as<int>());
-    }
-    else {
-      std::cerr << "ERROR: Timeout must a positive integer" << std::endl;
-      usage(std::cerr, visibleOptDesc);
+    if (options.wantForceData) {
+      std::cerr << "ERROR: conflicting '--force' and '--timeout' options specified" << std::endl;
       return 2;
     }
+    if (vm["timeout"].as<int>() < 0) {
+      std::cerr << "ERROR: timeout cannot be negative" << std::endl;
+      return 2;
+    }
+    options.timeout = time::milliseconds(vm["timeout"].as<int>());
   }
 
-  boost::asio::io_service io;
-  Face face(io);
-  KeyChain keyChain;
-  NdnPoke program(face, keyChain, std::cin, options);
   try {
+    Face face;
+    KeyChain keyChain;
+    NdnPoke program(face, keyChain, std::cin, options);
+
     program.start();
-    face.processEvents(timeout);
+    face.processEvents();
+
+    return program.didSendData() ? 0 : 1;
   }
   catch (const std::exception& e) {
-    std::cerr << "ERROR: " << e.what() << "\n" << std::endl;
-    return 1;
-  }
-
-  if (program.wasDataSent()) {
-    return 0;
-  }
-  else {
+    std::cerr << "ERROR: " << e.what() << std::endl;
     return 1;
   }
 }
diff --git a/tools/peek/ndnpoke/ndnpoke.cpp b/tools/peek/ndnpoke/ndnpoke.cpp
index 40ec247..2934404 100644
--- a/tools/peek/ndnpoke/ndnpoke.cpp
+++ b/tools/peek/ndnpoke/ndnpoke.cpp
@@ -27,79 +27,66 @@
 
 #include "ndnpoke.hpp"
 
-#include <ndn-cxx/security/signing-helpers.hpp>
-
-#include <sstream>
+#include <ndn-cxx/encoding/buffer-stream.hpp>
 
 namespace ndn {
 namespace peek {
 
-NdnPoke::NdnPoke(Face& face, KeyChain& keyChain, std::istream& inStream, const PokeOptions& options)
-  : m_face(face)
+NdnPoke::NdnPoke(Face& face, KeyChain& keyChain, std::istream& input, const PokeOptions& options)
+  : m_options(options)
+  , m_face(face)
   , m_keyChain(keyChain)
-  , m_inStream(inStream)
-  , m_options(options)
-  , m_wasDataSent(false)
+  , m_input(input)
+  , m_scheduler(m_face.getIoService())
 {
 }
 
 void
 NdnPoke::start()
 {
-  shared_ptr<Data> dataPacket = createDataPacket();
+  auto data = createData();
+
   if (m_options.wantForceData) {
-    m_face.put(*dataPacket);
-    m_wasDataSent = true;
+    m_face.put(*data);
+    m_didSendData = true;
+    return;
   }
-  else {
-    m_registeredPrefix = m_face.setInterestFilter(m_options.prefixName,
-                                                  bind(&NdnPoke::onInterest, this, _1, _2, dataPacket),
-                                                  nullptr,
-                                                  bind(&NdnPoke::onRegisterFailed, this, _1, _2));
-  }
+
+  m_registeredPrefix = m_face.setInterestFilter(m_options.name,
+    [this, data] (auto&&...) {
+      m_timeoutEvent.cancel();
+      m_face.put(*data);
+      m_didSendData = true;
+      m_registeredPrefix.cancel();
+    },
+    [this] (auto&&) {
+      m_timeoutEvent = m_scheduler.schedule(m_options.timeout, [this] {
+        m_registeredPrefix.cancel();
+      });
+    },
+    [] (auto&&, const auto& reason) {
+      std::cerr << "Prefix registration failure (" << reason << ")\n";
+    });
 }
 
 shared_ptr<Data>
-NdnPoke::createDataPacket()
+NdnPoke::createData() const
 {
-  auto dataPacket = make_shared<Data>(m_options.prefixName);
-
-  std::stringstream payloadStream;
-  payloadStream << m_inStream.rdbuf();
-  std::string payload = payloadStream.str();
-  dataPacket->setContent(reinterpret_cast<const uint8_t*>(payload.c_str()), payload.length());
-
+  auto data = make_shared<Data>(m_options.name);
   if (m_options.freshnessPeriod) {
-    dataPacket->setFreshnessPeriod(*m_options.freshnessPeriod);
+    data->setFreshnessPeriod(*m_options.freshnessPeriod);
+  }
+  if (m_options.wantFinalBlockId) {
+    data->setFinalBlock(m_options.name.at(-1));
   }
 
-  if (m_options.wantLastAsFinalBlockId) {
-    dataPacket->setFinalBlock(m_options.prefixName.get(-1));
-  }
+  OBufferStream os;
+  os << m_input.rdbuf();
+  data->setContent(os.buf());
 
-  m_keyChain.sign(*dataPacket, m_options.signingInfo);
+  m_keyChain.sign(*data, m_options.signingInfo);
 
-  return dataPacket;
-}
-
-void
-NdnPoke::onInterest(const Name& name, const Interest& interest, const shared_ptr<Data>& data)
-{
-  try {
-    m_face.put(*data);
-    m_wasDataSent = true;
-  }
-  catch (const Face::OversizedPacketError& e) {
-    std::cerr << "Data exceeded maximum packet size" << std::endl;
-  }
-
-  m_registeredPrefix.cancel();
-}
-
-void
-NdnPoke::onRegisterFailed(const Name& prefix, const std::string& reason)
-{
-  std::cerr << "Prefix Registration Failure. Reason = " << reason << std::endl;
+  return data;
 }
 
 } // namespace peek
diff --git a/tools/peek/ndnpoke/ndnpoke.hpp b/tools/peek/ndnpoke/ndnpoke.hpp
index fdad5bb..af5ad34 100644
--- a/tools/peek/ndnpoke/ndnpoke.hpp
+++ b/tools/peek/ndnpoke/ndnpoke.hpp
@@ -30,6 +30,8 @@
 
 #include "core/common.hpp"
 
+#include <ndn-cxx/util/scheduler.hpp>
+
 namespace ndn {
 namespace peek {
 
@@ -38,45 +40,44 @@
  */
 struct PokeOptions
 {
-  Name prefixName;
-  bool wantForceData = false;
+  // Data construction options
+  Name name;
+  optional<time::milliseconds> freshnessPeriod;
+  bool wantFinalBlockId = false;
   security::SigningInfo signingInfo;
-  bool wantLastAsFinalBlockId = false;
-  optional<time::milliseconds> freshnessPeriod = {};
+
+  // program behavior options
+  bool wantForceData = false;
+  time::milliseconds timeout = 10_s;
 };
 
 class NdnPoke : boost::noncopyable
 {
 public:
-  NdnPoke(Face& face, KeyChain& keyChain, std::istream& inStream, const PokeOptions& options);
+  NdnPoke(Face& face, KeyChain& keyChain, std::istream& input, const PokeOptions& options);
 
   void
   start();
 
   bool
-  wasDataSent() const
+  didSendData() const
   {
-    return m_wasDataSent;
+    return m_didSendData;
   }
 
 private:
   shared_ptr<Data>
-  createDataPacket();
-
-  void
-  onInterest(const Name& name, const Interest& interest, const shared_ptr<Data>& data);
-
-  void
-  onRegisterFailed(const Name& prefix, const std::string& reason);
+  createData() const;
 
 private:
+  const PokeOptions m_options;
   Face& m_face;
   KeyChain& m_keyChain;
-  std::istream& m_inStream;
-  const PokeOptions& m_options;
-
-  RegisteredPrefixHandle m_registeredPrefix;
-  bool m_wasDataSent;
+  std::istream& m_input;
+  Scheduler m_scheduler;
+  ScopedRegisteredPrefixHandle m_registeredPrefix;
+  scheduler::ScopedEventId m_timeoutEvent;
+  bool m_didSendData = false;
 };
 
 } // namespace peek