meta-google: dhcp-done: Adding status report

Previously dhcp-done only sends status code, this one provides the
capability to send status code + status message for better
troubleshooting.
Provide a way to let other process upgrade the status.

Tested: Unit test passed.

Change-Id: I9c689f90502a32b586c41e3491ad47ebc78fcc38
Signed-off-by: Yuxiao Zhang <yuxiaozhang@google.com>
diff --git a/subprojects/dhcp-done/dhcp-done.cpp b/subprojects/dhcp-done/dhcp-done.cpp
index d6c5c96..7ea8761 100644
--- a/subprojects/dhcp-done/dhcp-done.cpp
+++ b/subprojects/dhcp-done/dhcp-done.cpp
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include "file-io.hpp"
+
 #include <sdeventplus/event.hpp>
 #include <sdeventplus/source/io.hpp>
 #include <stdplus/fd/create.hpp>
@@ -23,11 +25,6 @@
 // A privileged port that is reserved for querying BMC DHCP completion.
 // This is well known by the clients querying the status.
 constexpr uint16_t kListenPort = 23;
-enum : uint8_t
-{
-    DONE = 0,
-    POWERCYCLE = 1,
-};
 
 stdplus::ManagedFd createListener()
 {
@@ -43,30 +40,8 @@
     return sock;
 }
 
-int main(int argc, char* argv[])
+int main()
 {
-    if (argc != 2)
-    {
-        stdplus::println(stderr, "Invalid parameter count");
-        return 1;
-    }
-
-    std::vector<uint8_t> data;
-
-    if (argv[1] == "POWERCYCLE"sv)
-    {
-        data.push_back(POWERCYCLE);
-    }
-    else if (argv[1] == "DONE"sv)
-    {
-        data.push_back(DONE);
-    }
-    else
-    {
-        stdplus::println(stderr, "Invalid parameter");
-        return 1;
-    }
-
     try
     {
         auto listener = createListener();
@@ -76,6 +51,20 @@
             [&](sdeventplus::source::IO&, int, uint32_t) {
             while (auto fd = stdplus::fd::accept(listener))
             {
+                std::string data;
+                try
+                {
+                    data = fileRead(statusFile);
+                }
+                catch (const std::exception& e)
+                {
+                    // we don't want to fail the upgrade process, set the status
+                    // to ONGOING
+                    data.push_back(2);
+                    data.append("Failed to read status ");
+                    data.append(e.what());
+                }
+
                 stdplus::fd::sendExact(*fd, data, stdplus::fd::SendFlags(0));
             }
         });
diff --git a/subprojects/dhcp-done/dhcp-done.service.in b/subprojects/dhcp-done/dhcp-done.service.in
new file mode 100644
index 0000000..1c66551
--- /dev/null
+++ b/subprojects/dhcp-done/dhcp-done.service.in
@@ -0,0 +1,9 @@
+[Unit]
+Description=gBMC DHCP Status Daemon
+
+[Service]
+Restart=on-failure
+ExecStart=@@BIN@ dhcp-done
+
+[Install]
+WantedBy=multi-user.target
diff --git a/subprojects/dhcp-done/dhcp-done@.service.in b/subprojects/dhcp-done/dhcp-done@.service.in
deleted file mode 100644
index ece864b..0000000
--- a/subprojects/dhcp-done/dhcp-done@.service.in
+++ /dev/null
@@ -1,6 +0,0 @@
-[Unit]
-Description=gBMC DHCP Complete
-
-[Service]
-Restart=on-failure
-ExecStart=@@BIN@ dhcp-done %I
diff --git a/subprojects/dhcp-done/file-io.cpp b/subprojects/dhcp-done/file-io.cpp
new file mode 100644
index 0000000..321cc6d
--- /dev/null
+++ b/subprojects/dhcp-done/file-io.cpp
@@ -0,0 +1,39 @@
+#include "file-io.hpp"
+
+#include <fcntl.h>
+#include <sys/file.h>
+#include <unistd.h>
+
+#include <stdplus/fd/atomic.hpp>
+#include <stdplus/fd/create.hpp>
+#include <stdplus/fd/fmt.hpp>
+#include <stdplus/fd/managed.hpp>
+#include <stdplus/fd/ops.hpp>
+#include <stdplus/print.hpp>
+
+#include <cerrno>
+#include <fstream>
+#include <iostream>
+#include <string>
+
+using ::stdplus::fd::ManagedFd;
+using std::literals::string_view_literals::operator""sv;
+
+// Function to read contents from a file (with locking)
+std::string fileRead(const fs::path& filename)
+{
+    // Open the file in read mode
+    ManagedFd fd = stdplus::fd::open(std::string(filename).c_str(),
+                                     stdplus::fd::OpenAccess::ReadOnly);
+    return stdplus::fd::readAll<std::string>(fd);
+}
+
+// Function to write contents to a file atomically
+void fileWrite(const fs::path& filename, const std::string& data)
+{
+    stdplus::fd::AtomicWriter writer(filename, 0644);
+    stdplus::fd::FormatBuffer out(writer);
+    out.appends(data);
+    out.flush();
+    writer.commit();
+}
diff --git a/subprojects/dhcp-done/file-io.hpp b/subprojects/dhcp-done/file-io.hpp
new file mode 100644
index 0000000..93458ee
--- /dev/null
+++ b/subprojects/dhcp-done/file-io.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <filesystem>
+#include <string>
+
+namespace fs = std::filesystem;
+constexpr auto statusFile = "/run/dhcp_status";
+
+// Function to read contents from a file
+std::string fileRead(const fs::path& filename);
+
+// Function to write contents to a file atomically
+void fileWrite(const fs::path& filename, const std::string& data);
diff --git a/subprojects/dhcp-done/meson.build b/subprojects/dhcp-done/meson.build
index abc449c..0bc7198 100644
--- a/subprojects/dhcp-done/meson.build
+++ b/subprojects/dhcp-done/meson.build
@@ -30,21 +30,45 @@
 ]
 
 libexecdir = get_option('prefix') / get_option('libexecdir')
+bindir = get_option('prefix') / get_option('bindir')
+
+fileio_lib = static_library(
+  'fileio',
+  [
+    'file-io.cpp',
+  ],
+  implicit_include_directories: false)
 
 executable(
   'dhcp-done',
   'dhcp-done.cpp',
   implicit_include_directories: false,
   dependencies: deps,
+  link_with : fileio_lib,
   install: true,
   install_dir: libexecdir)
 
+executable(
+  'update-dhcp-status',
+  'update-dhcp-status.cpp',
+  implicit_include_directories: false,
+  dependencies: deps,
+  link_with : fileio_lib,
+  install: true,
+  install_dir: bindir)
+
 systemd = dependency('systemd')
 systemunitdir = systemd.get_variable('systemdsystemunitdir')
 
 configure_file(
   configuration: {'BIN': libexecdir / 'dhcp-done'},
-  input: 'dhcp-done@.service.in',
-  output: 'dhcp-done@.service',
+  input: 'dhcp-done.service.in',
+  output: 'dhcp-done.service',
   install_mode: 'rw-r--r--',
   install_dir: systemunitdir)
+
+build_tests = get_option('tests')
+
+#if not build_tests.disabled()
+subdir('test')
+#endif
diff --git a/subprojects/dhcp-done/meson.options b/subprojects/dhcp-done/meson.options
new file mode 100644
index 0000000..0fc2767
--- /dev/null
+++ b/subprojects/dhcp-done/meson.options
@@ -0,0 +1 @@
+option('tests', type: 'feature', description: 'Build tests')
diff --git a/subprojects/dhcp-done/test/fileio_test.cpp b/subprojects/dhcp-done/test/fileio_test.cpp
new file mode 100644
index 0000000..8a79cf2
--- /dev/null
+++ b/subprojects/dhcp-done/test/fileio_test.cpp
@@ -0,0 +1,34 @@
+#include "../file-io.hpp"
+
+#include <stdio.h>
+#include <sys/file.h>
+#include <unistd.h>
+
+#include <fstream>
+#include <iostream>
+#include <string>
+
+#include <gtest/gtest.h>
+
+TEST(TestFileIO, TestFileReadWrite)
+{
+    std::string testFile = "./tmp_test_file";
+
+    std::string testStatus, testStatusUpdated;
+
+    testStatus.push_back(2);
+    testStatus.append("image downloading in progress");
+
+    testStatusUpdated.push_back(0);
+    testStatusUpdated.append("finished netboot");
+
+    fileWrite(testFile, testStatus);
+
+    EXPECT_TRUE(testStatus == fileRead(testFile));
+
+    fileWrite(testFile, testStatusUpdated);
+
+    EXPECT_TRUE(testStatusUpdated == fileRead(testFile));
+
+    remove(testFile.c_str());
+}
diff --git a/subprojects/dhcp-done/test/meson.build b/subprojects/dhcp-done/test/meson.build
new file mode 100644
index 0000000..de5354f
--- /dev/null
+++ b/subprojects/dhcp-done/test/meson.build
@@ -0,0 +1,39 @@
+# Copyright 2024 Google LLC
+#
+# 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.
+#
+gtest = dependency('gtest', main: true, disabler: true, required: false)
+if not gtest.found()
+  gtest_proj = import('cmake').subproject(
+    'googletest',
+    cmake_options: [
+      '-DCMAKE_CXX_FLAGS=-Wno-pedantic',
+    ],
+    required: false)
+  if gtest_proj.found()
+    gtest = declare_dependency(
+      dependencies: [
+        dependency('threads'),
+        gtest_proj.dependency('gtest'),
+        gtest_proj.dependency('gtest_main'),
+      ])
+  else
+    assert(not build_tests.allowed(), 'Googletest is required')
+  endif
+endif
+
+test('fileio_test', executable('fileio_test',
+  ['fileio_test.cpp'],
+  implicit_include_directories: false,
+  dependencies: [gtest, dependency('stdplus')],
+  link_with : fileio_lib))
diff --git a/subprojects/dhcp-done/update-dhcp-status.cpp b/subprojects/dhcp-done/update-dhcp-status.cpp
new file mode 100644
index 0000000..b23706e
--- /dev/null
+++ b/subprojects/dhcp-done/update-dhcp-status.cpp
@@ -0,0 +1,63 @@
+#include "file-io.hpp"
+
+#include <stdplus/print.hpp>
+
+#include <cstring>
+#include <iostream>
+
+static void printUsage()
+{
+    stdplus::println(stderr, "Usage: update_dhcp_status <state> <message>");
+    stdplus::println(stderr,
+                     "<state> is one of 'DONE', 'POWERCYCLE' or 'ONGOING'");
+}
+
+static int genStatusCode(char* state)
+{
+    if (std::strcmp(state, "DONE") == 0)
+    {
+        return 0;
+    }
+    else if (std::strcmp(state, "POWERCYCLE") == 0)
+    {
+        return 1;
+    }
+    else if (std::strcmp(state, "ONGOING") == 0)
+    {
+        return 2;
+    }
+
+    return -1;
+}
+
+int main(int argc, char* argv[])
+{
+    if (argc != 3)
+    {
+        printUsage();
+        return 1;
+    }
+
+    int statusCode = genStatusCode(argv[1]);
+
+    if (statusCode == -1)
+    {
+        printUsage();
+        return 1;
+    }
+
+    try
+    {
+        std::string status;
+        status.push_back(statusCode);
+        status.append(argv[2]);
+        fileWrite(statusFile, status);
+    }
+    catch (const std::exception& e)
+    {
+        stdplus::println(stderr, "Failed to update status file {}", e.what());
+        return 1;
+    }
+
+    return 0;
+}