SPI device code updater

This code updater is for updating spi flash devices.
It can for example update the host firmware on different server boards
and has following features:

- power down the host before update
- set mux gpios to access spi flash
- (very limited) communication with ME (Management Engine)
- use flashrom to utilize fw with IFD (Intel Flash Descriptor)
- otherwise directly write to the flash chip.

The behavior of this code updater can be configured via EM.

Tested: on Tyan S8030 and Tyan S5549 Board. Steps below.

1. Display the fw inventory

```
curl --silent $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory
```

```
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory",
  "@odata.type": "#SoftwareInventoryCollection.SoftwareInventoryCollection",
  "Members": [
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/717f6f4d"
    },
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_4950"
    },
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/bios_active"
    }
  ],
  "Members@odata.count": 3,
  "Name": "Software Inventory Collection"
}
```

2. Query BIOS version.
The version is "unknown" here since currently there is no interface
enabled via which to query it.

```
curl $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_4950
```

```
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_4950",
  "@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory",
  "Description": "Unknown image",
  "Id": "HostSPIFlash_4950",
  "Name": "Software Inventory",
  "Status": {
    "Health": "Warning",
    "HealthRollup": "OK",
    "State": "Disabled"
  },
  "Updateable": true,
  "Version": "unknown"
}
```

3. Trigger the fw update via redfish.

```
curl -k ${creds} \
	-H "Content-Type:multipart/form-data" \
	-X POST \
	-F UpdateParameters="{\"Targets\":[\"/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_6041\"],\"@Redfish.OperationApplyTime\":\"Immediate\"};type=application/json" \
	-F "UpdateFile=@${fwpath};type=application/octet-stream" \
	https://${bmc}/redfish/v1/UpdateService/update
```

4. Task is returned

```
{
  "@odata.id": "/redfish/v1/TaskService/Tasks/0",
  "@odata.type": "#Task.v1_4_3.Task",
  "Id": "0",
  "TaskState": "Running",
  "TaskStatus": "OK"
}
```

5. Query Task status
```
curl --silent $creds https://$bmc/redfish/v1/TaskService/Tasks/0
```

```
{
  "@odata.id": "/redfish/v1/TaskService/Tasks/0",
  "@odata.type": "#Task.v1_4_3.Task",
  "EndTime": "2025-02-18T14:05:46+00:00",
  "HidePayload": false,
  "Id": "0",
  "Messages": [
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has started.",
      "MessageArgs": [
        "0"
      ],
      "MessageId": "TaskEvent.1.0.TaskStarted",
      "MessageSeverity": "OK",
      "Resolution": "None."
    },
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has changed to progress 10 percent complete.",
      "MessageArgs": [
        "0",
        "10"
      ],
      "MessageId": "TaskEvent.1.0.TaskProgressChanged",
      "MessageSeverity": "OK",
      "Resolution": "None."
    },
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has changed to progress 20 percent complete.",
      "MessageArgs": [
        "0",
        "20"
      ],
      "MessageId": "TaskEvent.1.0.TaskProgressChanged",
      "MessageSeverity": "OK",
      "Resolution": "None."
    },
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has changed to progress 70 percent complete.",
      "MessageArgs": [
        "0",
        "70"
      ],
      "MessageId": "TaskEvent.1.0.TaskProgressChanged",
      "MessageSeverity": "OK",
      "Resolution": "None."
    },
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has changed to progress 100 percent complete.",
      "MessageArgs": [
        "0",
        "100"
      ],
      "MessageId": "TaskEvent.1.0.TaskProgressChanged",
      "MessageSeverity": "OK",
      "Resolution": "None."
    },
    {
      "@odata.type": "#Message.v1_1_1.Message",
      "Message": "The task with Id '0' has completed.",
      "MessageArgs": [
        "0"
      ],
      "MessageId": "TaskEvent.1.0.TaskCompletedOK",
      "MessageSeverity": "OK",
      "Resolution": "None."
    }
  ],
  "Name": "Task 0",
  "Payload": {
    "HttpHeaders": [],
    "HttpOperation": "POST",
    "JsonBody": "null",
    "TargetUri": "/redfish/v1/UpdateService/update"
  },
  "PercentComplete": 90,
  "StartTime": "2025-02-18T14:04:47+00:00",
  "TaskMonitor": "/redfish/v1/TaskService/TaskMonitors/0",
  "TaskState": "Completed",
  "TaskStatus": "OK"
}
```

6. Display the fw inventory with newly updated fw.
```
curl --silent $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory
```

```
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory",
  "@odata.type": "#SoftwareInventoryCollection.SoftwareInventoryCollection",
  "Members": [
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/717f6f4d"
    },
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_8728"
    },
    {
      "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/bios_active"
    }
  ],
  "Members@odata.count": 3,
  "Name": "Software Inventory Collection"
}
```

7. Query the new fw version.

The version is 'mycompversion' since that's what has been set in the
pldm fw update package for testing.

```
curl $creds https://$bmc/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_8728
```

```
{
  "@odata.id": "/redfish/v1/UpdateService/FirmwareInventory/HostSPIFlash_8728",
  "@odata.type": "#SoftwareInventory.v1_1_0.SoftwareInventory",
  "Description": "Unknown image",
  "Id": "HostSPIFlash_8728",
  "Name": "Software Inventory",
  "Status": {
    "Health": "OK",
    "HealthRollup": "OK",
    "State": "Enabled"
  },
  "Updateable": false,
  "Version": "mycompversion"
}
```

Change-Id: I27803b7fded71af2364c2f55fad841a410603dec
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
diff --git a/common/include/NotifyWatch.hpp b/common/include/NotifyWatch.hpp
new file mode 100644
index 0000000..8b5c795
--- /dev/null
+++ b/common/include/NotifyWatch.hpp
@@ -0,0 +1,121 @@
+#pragma once
+#include <sys/inotify.h>
+#include <unistd.h>
+
+#include <sdbusplus/async/context.hpp>
+#include <sdbusplus/async/fdio.hpp>
+#include <sdbusplus/async/task.hpp>
+
+#include <array>
+#include <cerrno>
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <filesystem>
+#include <memory>
+#include <span>
+#include <string>
+#include <system_error>
+namespace phosphor::notify::watch
+{
+namespace fs = std::filesystem;
+template <typename Instance>
+class NotifyWatch
+{
+  public:
+    NotifyWatch() = delete;
+    NotifyWatch(const NotifyWatch&) = delete;
+    NotifyWatch& operator=(const NotifyWatch&) = delete;
+    NotifyWatch(NotifyWatch&&) = delete;
+    NotifyWatch& operator=(NotifyWatch&&) = delete;
+
+    explicit NotifyWatch(sdbusplus::async::context& ctx,
+                         const std::string& dir) : notifyCtx(ctx)
+    {
+        std::error_code ec = {};
+        fs::path dirPath(dir);
+        if (!fs::create_directories(dirPath, ec))
+        {
+            if (ec)
+            {
+                throw std::system_error(ec,
+                                        "Failed to create directory " + dir);
+            }
+        }
+        fd = inotify_init1(IN_NONBLOCK);
+        if (-1 == fd)
+        {
+            throw std::system_error(errno, std::system_category(),
+                                    "inotify_init1 failed");
+        }
+        wd = inotify_add_watch(fd, dir.c_str(), IN_CLOSE_WRITE);
+        if (-1 == wd)
+        {
+            close(fd);
+            throw std::system_error(errno, std::system_category(),
+                                    "inotify_add_watch failed");
+        }
+        fdioInstance = std::make_unique<sdbusplus::async::fdio>(ctx, fd);
+    }
+    ~NotifyWatch()
+    {
+        if (-1 != fd)
+        {
+            if (-1 != wd)
+            {
+                inotify_rm_watch(fd, wd);
+            }
+            close(fd);
+        }
+    }
+    sdbusplus::async::task<> readNotifyAsync()
+    {
+        co_await fdioInstance->next();
+        constexpr size_t maxBytes = 1024;
+        std::array<uint8_t, maxBytes> buffer{};
+        auto bytes = read(fd, buffer.data(), maxBytes);
+        if (0 > bytes)
+        {
+            throw std::system_error(errno, std::system_category(),
+                                    "Failed to read notify event");
+        }
+        auto offset = 0;
+        while (offset < bytes)
+        {
+            // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast)
+            std::span<uint32_t> mask{
+                reinterpret_cast<uint32_t*>(
+                    buffer.data() + offset + offsetof(inotify_event, mask)),
+                1};
+            std::span<uint32_t> len{
+                reinterpret_cast<uint32_t*>(
+                    buffer.data() + offset + offsetof(inotify_event, len)),
+                1};
+            // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast)
+            if (((mask[0] & IN_CLOSE_WRITE) != 0U) &&
+                ((mask[0] & IN_ISDIR) == 0U))
+            {
+                // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast)
+                std::span<char> name{
+                    reinterpret_cast<char*>(
+                        buffer.data() + offset + offsetof(inotify_event, name)),
+                    len[0]};
+                // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast)
+                co_await static_cast<Instance*>(this)->processUpdate(
+                    std::string(name.begin(), name.end()));
+            }
+            offset += offsetof(inotify_event, name) + len[0];
+        }
+        if (!notifyCtx.stop_requested())
+        {
+            notifyCtx.spawn(readNotifyAsync());
+        }
+    }
+
+  private:
+    sdbusplus::async::context& notifyCtx;
+    int wd = -1;
+    int fd = -1;
+    std::unique_ptr<sdbusplus::async::fdio> fdioInstance;
+};
+} // namespace phosphor::notify::watch
diff --git a/common/include/software.hpp b/common/include/software.hpp
index d3e8ffc..849e959 100644
--- a/common/include/software.hpp
+++ b/common/include/software.hpp
@@ -98,6 +98,8 @@
     std::unique_ptr<SoftwareActivationProgress> softwareActivationProgress =
         nullptr;
 
+    static long int getRandomId();
+
   protected:
     // @returns        a random software id (swid) for that device
     static std::string getRandomSoftwareId(device::Device& parent);
diff --git a/common/src/software.cpp b/common/src/software.cpp
index 31b2b75..504a9ae 100644
--- a/common/src/software.cpp
+++ b/common/src/software.cpp
@@ -55,7 +55,7 @@
           "OBJPATH", objPath);
 };
 
-static long int getRandomId()
+long int Software::getRandomId()
 {
     struct timespec ts;
     clock_gettime(CLOCK_REALTIME, &ts);