tools: add progress implementation

Signed-off-by: Patrick Venture <venture@google.com>
Change-Id: I9da1674d6cbc688efc7bab0e033788d6ee4694f7
diff --git a/tools/Makefile.am b/tools/Makefile.am
index 086fba8..14d9618 100644
--- a/tools/Makefile.am
+++ b/tools/Makefile.am
@@ -21,7 +21,8 @@
 	lpc.cpp \
 	io.cpp \
 	pci.cpp \
-	p2a.cpp
+	p2a.cpp \
+	progress.cpp
 libupdater_la_LIBADD = $(top_builddir)/libfirmware_common.la
 
 SUBDIRS = . test
diff --git a/tools/bt.cpp b/tools/bt.cpp
index 9e77688..d1e4dc7 100644
--- a/tools/bt.cpp
+++ b/tools/bt.cpp
@@ -16,6 +16,15 @@
         return false;
     }
 
+    std::int64_t fileSize = sys->getSize(input.c_str());
+    if (fileSize == 0)
+    {
+        std::fprintf(stderr, "Zero-length file, or other file access error\n");
+        return false;
+    }
+
+    progress->start(fileSize);
+
     try
     {
         static constexpr int btBufferLen = 50;
@@ -33,6 +42,7 @@
                                                  &readBuffer[bytesRead]);
                 blob->writeBytes(session, offset, buffer);
                 offset += bytesRead;
+                progress->updateProgress(bytesRead);
             }
         } while (bytesRead > 0);
     }
diff --git a/tools/bt.hpp b/tools/bt.hpp
index 729f74d..16605fd 100644
--- a/tools/bt.hpp
+++ b/tools/bt.hpp
@@ -2,6 +2,7 @@
 
 #include "interface.hpp"
 #include "internal/sys.hpp"
+#include "progress.hpp"
 
 #include <ipmiblob/blob_interface.hpp>
 
@@ -11,10 +12,10 @@
 class BtDataHandler : public DataInterface
 {
   public:
-    BtDataHandler(ipmiblob::BlobInterface* blob,
+    BtDataHandler(ipmiblob::BlobInterface* blob, ProgressInterface* progress,
                   const internal::Sys* sys = &internal::sys_impl) :
         blob(blob),
-        sys(sys){};
+        progress(progress), sys(sys){};
 
     bool sendContents(const std::string& input, std::uint16_t session) override;
     ipmi_flash::FirmwareFlags::UpdateFlags supportedType() const override
@@ -24,6 +25,7 @@
 
   private:
     ipmiblob::BlobInterface* blob;
+    ProgressInterface* progress;
     const internal::Sys* sys;
 };
 
diff --git a/tools/interface.hpp b/tools/interface.hpp
index 2c993bf..c613f0e 100644
--- a/tools/interface.hpp
+++ b/tools/interface.hpp
@@ -1,6 +1,7 @@
 #pragma once
 
 #include "flags.hpp"
+#include "progress.hpp"
 
 #include <cstdint>
 #include <string>
diff --git a/tools/lpc.cpp b/tools/lpc.cpp
index 7dbfaf3..3616085 100644
--- a/tools/lpc.cpp
+++ b/tools/lpc.cpp
@@ -103,6 +103,15 @@
         return false;
     }
 
+    std::int64_t fileSize = sys->getSize(input.c_str());
+    if (fileSize == 0)
+    {
+        std::fprintf(stderr, "Zero-length file, or other file access error\n");
+        return false;
+    }
+
+    progress->start(fileSize);
+
     /* For Nuvoton the maximum is 4K */
     auto readBuffer = std::make_unique<std::uint8_t[]>(host_lpc_buf.length);
     if (nullptr == readBuffer)
@@ -140,6 +149,7 @@
                 /* This doesn't return anything on success. */
                 blob->writeBytes(session, offset, chunkBytes);
                 offset += bytesRead;
+                progress->updateProgress(bytesRead);
             }
         } while (bytesRead > 0);
     }
diff --git a/tools/lpc.hpp b/tools/lpc.hpp
index 2c3a723..17367bb 100644
--- a/tools/lpc.hpp
+++ b/tools/lpc.hpp
@@ -3,6 +3,7 @@
 #include "interface.hpp"
 #include "internal/sys.hpp"
 #include "io.hpp"
+#include "progress.hpp"
 
 #include <cstdint>
 #include <ipmiblob/blob_interface.hpp>
@@ -23,9 +24,11 @@
   public:
     LpcDataHandler(ipmiblob::BlobInterface* blob, HostIoInterface* io,
                    std::uint32_t address, std::uint32_t length,
+                   ProgressInterface* progress,
                    const internal::Sys* sys = &internal::sys_impl) :
         blob(blob),
-        io(io), address(address), length(length), sys(sys){};
+        io(io), address(address), length(length), progress(progress),
+        sys(sys){};
 
     bool sendContents(const std::string& input, std::uint16_t session) override;
     ipmi_flash::FirmwareFlags::UpdateFlags supportedType() const override
@@ -38,6 +41,7 @@
     HostIoInterface* io;
     std::uint32_t address;
     std::uint32_t length;
+    ProgressInterface* progress;
     const internal::Sys* sys;
 };
 
diff --git a/tools/main.cpp b/tools/main.cpp
index c9e76c0..746e679 100644
--- a/tools/main.cpp
+++ b/tools/main.cpp
@@ -19,6 +19,7 @@
 #include "lpc.hpp"
 #include "p2a.hpp"
 #include "pci.hpp"
+#include "progress.hpp"
 #include "tool_errors.hpp"
 #include "updater.hpp"
 
@@ -207,13 +208,15 @@
         ipmiblob::BlobHandler blob(std::move(ipmi));
         host_tool::DevMemDevice devmem;
         host_tool::PciUtilImpl pci;
+        host_tool::ProgressStdoutIndicator progress;
 
         std::unique_ptr<host_tool::DataInterface> handler;
 
         /* Input has already been validated in this case. */
         if (interface == IPMIBT)
         {
-            handler = std::make_unique<host_tool::BtDataHandler>(&blob);
+            handler =
+                std::make_unique<host_tool::BtDataHandler>(&blob, &progress);
         }
         else if (interface == IPMILPC)
         {
@@ -223,12 +226,12 @@
                 exit(EXIT_FAILURE);
             }
             handler = std::make_unique<host_tool::LpcDataHandler>(
-                &blob, &devmem, hostAddress, hostLength);
+                &blob, &devmem, hostAddress, hostLength, &progress);
         }
         else if (interface == IPMIPCI)
         {
             handler = std::make_unique<host_tool::P2aDataHandler>(
-                &blob, &devmem, &pci);
+                &blob, &devmem, &pci, &progress);
         }
 
         if (!handler)
diff --git a/tools/p2a.cpp b/tools/p2a.cpp
index 66fc62e..0ac5eb3 100644
--- a/tools/p2a.cpp
+++ b/tools/p2a.cpp
@@ -111,6 +111,15 @@
         return false;
     }
 
+    std::int64_t fileSize = sys->getSize(input.c_str());
+    if (fileSize == 0)
+    {
+        std::fprintf(stderr, "Zero-length file, or other file access error\n");
+        return false;
+    }
+
+    progress->start(fileSize);
+
     const std::uint32_t p2aLength = aspeedP2aOffset;
 
     auto readBuffer = std::make_unique<std::uint8_t[]>(p2aLength);
@@ -152,6 +161,7 @@
                 /* This doesn't return anything on success. */
                 blob->writeBytes(session, offset, chunkBytes);
                 offset += bytesRead;
+                progress->updateProgress(bytesRead);
             }
         } while (bytesRead > 0);
     }
diff --git a/tools/p2a.hpp b/tools/p2a.hpp
index c84f12d..d1a303e 100644
--- a/tools/p2a.hpp
+++ b/tools/p2a.hpp
@@ -4,6 +4,7 @@
 #include "internal/sys.hpp"
 #include "io.hpp"
 #include "pci.hpp"
+#include "progress.hpp"
 
 #include <cstdint>
 #include <ipmiblob/blob_interface.hpp>
@@ -22,10 +23,10 @@
 {
   public:
     P2aDataHandler(ipmiblob::BlobInterface* blob, HostIoInterface* io,
-                   PciUtilInterface* pci,
+                   PciUtilInterface* pci, ProgressInterface* progress,
                    const internal::Sys* sys = &internal::sys_impl) :
         blob(blob),
-        io(io), pci(pci), sys(sys)
+        io(io), pci(pci), progress(progress), sys(sys)
     {
     }
 
@@ -39,6 +40,7 @@
     ipmiblob::BlobInterface* blob;
     HostIoInterface* io;
     PciUtilInterface* pci;
+    ProgressInterface* progress;
     const internal::Sys* sys;
 };
 
diff --git a/tools/progress.cpp b/tools/progress.cpp
new file mode 100644
index 0000000..a395ca3
--- /dev/null
+++ b/tools/progress.cpp
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2019 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "progress.hpp"
+
+#include <cstdio>
+
+namespace host_tool
+{
+
+void ProgressStdoutIndicator::updateProgress(std::int64_t bytes)
+{
+    /* Print progress update. */
+    currentBytes += bytes;
+    std::fprintf(stdout, "\rProgress: %.2f%%",
+                 100.0 * currentBytes / totalBytes);
+    std::fflush(stdout);
+}
+
+void ProgressStdoutIndicator::start(std::int64_t bytes)
+{
+    totalBytes = bytes;
+    currentBytes = 0;
+}
+
+} // namespace host_tool
diff --git a/tools/progress.hpp b/tools/progress.hpp
new file mode 100644
index 0000000..f884d62
--- /dev/null
+++ b/tools/progress.hpp
@@ -0,0 +1,38 @@
+#pragma once
+
+#include <cstdint>
+
+namespace host_tool
+{
+
+class ProgressInterface
+{
+  public:
+    virtual ~ProgressInterface() = default;
+
+    /** Update the progress by X bytes.  This will inform any listening
+     * interfaces (just write to stdout mostly), and tick off as time passed.
+     */
+    virtual void updateProgress(std::int64_t bytes) = 0;
+    virtual void start(std::int64_t bytes) = 0;
+};
+
+/**
+ * @brief A progress indicator that writes to stdout.  It deliberately
+ * overwrites the same line when it's used, so it's advised to not interject
+ * other non-error messages.
+ */
+class ProgressStdoutIndicator : public ProgressInterface
+{
+  public:
+    ProgressStdoutIndicator() = default;
+
+    void updateProgress(std::int64_t bytes) override;
+    void start(std::int64_t bytes) override;
+
+  private:
+    std::int64_t totalBytes = 0;
+    std::int64_t currentBytes = 0;
+};
+
+} // namespace host_tool
diff --git a/tools/test/internal_sys_mock.hpp b/tools/test/internal_sys_mock.hpp
index fc98561..06aa023 100644
--- a/tools/test/internal_sys_mock.hpp
+++ b/tools/test/internal_sys_mock.hpp
@@ -4,6 +4,8 @@
 
 #include <unistd.h>
 
+#include <cstdint>
+
 #include <gmock/gmock.h>
 
 namespace internal
@@ -22,6 +24,7 @@
     MOCK_CONST_METHOD0(getpagesize, int());
     MOCK_CONST_METHOD3(ioctl, int(int, unsigned long, void*));
     MOCK_CONST_METHOD3(poll, int(struct pollfd*, nfds_t, int));
+    MOCK_CONST_METHOD1(getSize, std::int64_t(const char*));
 };
 
 } // namespace internal
diff --git a/tools/test/progress_mock.hpp b/tools/test/progress_mock.hpp
new file mode 100644
index 0000000..80c0af1
--- /dev/null
+++ b/tools/test/progress_mock.hpp
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "progress.hpp"
+
+#include <cstdint>
+
+#include <gmock/gmock.h>
+
+namespace host_tool
+{
+
+class ProgressMock : public ProgressInterface
+{
+  public:
+    MOCK_METHOD1(updateProgress, void(std::int64_t));
+    MOCK_METHOD1(start, void(std::int64_t));
+};
+
+} // namespace host_tool
diff --git a/tools/test/tools_bt_unittest.cpp b/tools/test/tools_bt_unittest.cpp
index 1a364d3..cca0b34 100644
--- a/tools/test/tools_bt_unittest.cpp
+++ b/tools/test/tools_bt_unittest.cpp
@@ -1,5 +1,6 @@
 #include "bt.hpp"
 #include "internal_sys_mock.hpp"
+#include "progress_mock.hpp"
 
 #include <cstring>
 #include <ipmiblob/test/blob_interface_mock.hpp>
@@ -8,6 +9,8 @@
 
 namespace host_tool
 {
+namespace
+{
 
 using ::testing::_;
 using ::testing::ContainerEq;
@@ -23,14 +26,17 @@
      */
     internal::InternalSysMock sysMock;
     ipmiblob::BlobInterfaceMock blobMock;
+    ProgressMock progMock;
 
-    BtDataHandler handler(&blobMock, &sysMock);
+    BtDataHandler handler(&blobMock, &progMock, &sysMock);
     std::string filePath = "/asdf";
     int fd = 1;
     std::uint16_t session = 0xbeef;
     std::vector<std::uint8_t> bytes = {'1', '2', '3', '4'};
+    const int fakeFileSize = 100;
 
     EXPECT_CALL(sysMock, open(Eq(filePath), _)).WillOnce(Return(fd));
+    EXPECT_CALL(sysMock, getSize(Eq(filePath))).WillOnce(Return(fakeFileSize));
     EXPECT_CALL(sysMock, read(fd, NotNull(), _))
         .WillOnce(Invoke([&](int fd, void* buf, std::size_t count) {
             EXPECT_TRUE(count > bytes.size());
@@ -45,4 +51,5 @@
     EXPECT_TRUE(handler.sendContents(filePath, session));
 }
 
+} // namespace
 } // namespace host_tool
diff --git a/tools/test/tools_lpc_unittest.cpp b/tools/test/tools_lpc_unittest.cpp
index 84a81eb..9e417ed 100644
--- a/tools/test/tools_lpc_unittest.cpp
+++ b/tools/test/tools_lpc_unittest.cpp
@@ -1,6 +1,7 @@
 #include "internal_sys_mock.hpp"
 #include "io_mock.hpp"
 #include "lpc.hpp"
+#include "progress_mock.hpp"
 
 #include <cstring>
 #include <ipmiblob/test/blob_interface_mock.hpp>
@@ -9,6 +10,8 @@
 
 namespace host_tool
 {
+namespace
+{
 
 using ::testing::_;
 using ::testing::ContainerEq;
@@ -23,14 +26,17 @@
     internal::InternalSysMock sysMock;
     ipmiblob::BlobInterfaceMock blobMock;
     HostIoInterfaceMock ioMock;
+    ProgressMock progMock;
 
     const std::uint32_t address = 0xfedc1000;
     const std::uint32_t length = 0x1000;
 
-    LpcDataHandler handler(&blobMock, &ioMock, address, length, &sysMock);
+    LpcDataHandler handler(&blobMock, &ioMock, address, length, &progMock,
+                           &sysMock);
     std::uint16_t session = 0xbeef;
     std::string filePath = "/asdf";
     int fileDescriptor = 5;
+    const int fakeFileSize = 100;
 
     LpcRegion host_lpc_buf;
     host_lpc_buf.address = address;
@@ -45,6 +51,8 @@
 
     EXPECT_CALL(sysMock, open(StrEq(filePath.c_str()), _))
         .WillOnce(Return(fileDescriptor));
+    EXPECT_CALL(sysMock, getSize(StrEq(filePath.c_str())))
+        .WillOnce(Return(fakeFileSize));
     EXPECT_CALL(sysMock, read(_, NotNull(), Gt(data.size())))
         .WillOnce(Invoke([&data](int, void* buf, std::size_t) {
             std::memcpy(buf, data.data(), data.size());
@@ -66,4 +74,5 @@
     EXPECT_TRUE(handler.sendContents(filePath, session));
 }
 
+} // namespace
 } // namespace host_tool