Add multi-host support

This refactoring includes:
- added multi-host mode support;
- added support for graceful shutdown of the service;
- added support to flush the log buffer as it fills;
- D-Bus service xyz.openbmc_project.HostLogger replaced with SIGUSR1
  signal handler;
- self diagnostic messages now registered via phosphor-logging;
- added unit tests;
- build system migrated from autotools to meson;
- source code aligned with OpenBMC conventions.

Change-Id: If6c1dfde278af685d8563450543a6587a282c7e4
Signed-off-by: Artem Senichev <a.senichev@yadro.com>
diff --git a/test/config_test.cpp b/test/config_test.cpp
new file mode 100644
index 0000000..15fbc4b
--- /dev/null
+++ b/test/config_test.cpp
@@ -0,0 +1,97 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "config.hpp"
+
+#include <gtest/gtest.h>
+
+// Names of environment variables
+static const char* SOCKET_ID = "SOCKET_ID";
+static const char* BUF_MAXSIZE = "BUF_MAXSIZE";
+static const char* BUF_MAXTIME = "BUF_MAXTIME";
+static const char* FLUSH_FULL = "FLUSH_FULL";
+static const char* HOST_STATE = "HOST_STATE";
+static const char* OUT_DIR = "OUT_DIR";
+static const char* MAX_FILES = "MAX_FILES";
+
+/**
+ * @class ConfigTest
+ * @brief Configuration tests.
+ */
+class ConfigTest : public ::testing::Test
+{
+  protected:
+    void SetUp() override
+    {
+        resetEnv();
+    }
+
+    void TearDown() override
+    {
+        resetEnv();
+    }
+
+    /** @brief Reset environment variables. */
+    void resetEnv() const
+    {
+        unsetenv(SOCKET_ID);
+        unsetenv(BUF_MAXSIZE);
+        unsetenv(BUF_MAXTIME);
+        unsetenv(FLUSH_FULL);
+        unsetenv(HOST_STATE);
+        unsetenv(OUT_DIR);
+        unsetenv(MAX_FILES);
+    }
+};
+
+TEST_F(ConfigTest, Defaults)
+{
+    Config cfg;
+    EXPECT_STREQ(cfg.socketId, "");
+    EXPECT_EQ(cfg.bufMaxSize, 3000);
+    EXPECT_EQ(cfg.bufMaxTime, 0);
+    EXPECT_EQ(cfg.bufFlushFull, false);
+    EXPECT_STREQ(cfg.hostState, "/xyz/openbmc_project/state/host0");
+    EXPECT_STREQ(cfg.outDir, "/var/lib/obmc/hostlogs");
+    EXPECT_EQ(cfg.maxFiles, 10);
+}
+
+TEST_F(ConfigTest, Load)
+{
+    setenv(SOCKET_ID, "id123", 1);
+    setenv(BUF_MAXSIZE, "1234", 1);
+    setenv(BUF_MAXTIME, "4321", 1);
+    setenv(FLUSH_FULL, "true", 1);
+    setenv(HOST_STATE, "host123", 1);
+    setenv(OUT_DIR, "path123", 1);
+    setenv(MAX_FILES, "1122", 1);
+
+    Config cfg;
+    EXPECT_STREQ(cfg.socketId, "id123");
+    EXPECT_EQ(cfg.bufMaxSize, 1234);
+    EXPECT_EQ(cfg.bufMaxTime, 4321);
+    EXPECT_EQ(cfg.bufFlushFull, true);
+    EXPECT_STREQ(cfg.hostState, "host123");
+    EXPECT_STREQ(cfg.outDir, "path123");
+    EXPECT_EQ(cfg.maxFiles, 1122);
+}
+
+TEST_F(ConfigTest, InvalidNumeric)
+{
+    setenv(BUF_MAXSIZE, "-1234", 1);
+    ASSERT_THROW(Config(), std::invalid_argument);
+}
+
+TEST_F(ConfigTest, InvalidBoolean)
+{
+    setenv(FLUSH_FULL, "invalid", 1);
+    ASSERT_THROW(Config(), std::invalid_argument);
+}
+
+TEST_F(ConfigTest, InvalidConfig)
+{
+    setenv(BUF_MAXSIZE, "0", 1);
+    setenv(BUF_MAXTIME, "0", 1);
+    setenv(FLUSH_FULL, "true", 1);
+    ASSERT_THROW(Config(), std::invalid_argument);
+}
diff --git a/test/file_storage_test.cpp b/test/file_storage_test.cpp
new file mode 100644
index 0000000..ba74bd4
--- /dev/null
+++ b/test/file_storage_test.cpp
@@ -0,0 +1,92 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "file_storage.hpp"
+
+#include <fstream>
+
+#include <gtest/gtest.h>
+
+namespace fs = std::filesystem;
+
+/**
+ * @class FileStorageTest
+ * @brief Persistent file storage tests.
+ */
+class FileStorageTest : public ::testing::Test
+{
+  protected:
+    void SetUp() override
+    {
+        fs::remove_all(logPath);
+    }
+
+    void TearDown() override
+    {
+        fs::remove_all(logPath);
+    }
+
+    const fs::path logPath =
+        fs::temp_directory_path() / "file_storage_test_out";
+};
+
+TEST_F(FileStorageTest, InvalidPath)
+{
+    ASSERT_THROW(FileStorage("", "", 0), std::invalid_argument);
+    ASSERT_THROW(FileStorage("relative/path", "", 0), std::invalid_argument);
+    ASSERT_THROW(FileStorage("/noaccess", "", 0), fs::filesystem_error);
+}
+
+TEST_F(FileStorageTest, Save)
+{
+    const char* data = "test message\n";
+    LogBuffer buf(0, 0);
+    buf.append(data, strlen(data));
+
+    FileStorage fs(logPath, "", 0);
+    fs.save(buf);
+
+    const auto itBegin = fs::recursive_directory_iterator(logPath);
+    const auto itEnd = fs::recursive_directory_iterator{};
+    ASSERT_EQ(std::distance(itBegin, itEnd), 1);
+
+    const fs::path file = *fs::directory_iterator(logPath);
+    EXPECT_NE(fs::file_size(file), 0);
+}
+
+TEST_F(FileStorageTest, Rotation)
+{
+    const size_t limit = 5;
+    const std::string prefix = "host123";
+
+    const char* data = "test message\n";
+    LogBuffer buf(0, 0);
+    buf.append(data, strlen(data));
+
+    FileStorage fs(logPath, prefix, limit);
+    for (size_t i = 0; i < limit + 3; ++i)
+    {
+        fs.save(buf);
+    }
+
+    // Dir and other files that can not be removed
+    const fs::path dir = logPath / (prefix + "_11111111_222222.log.gz");
+    const fs::path files[] = {logPath / "short",
+                              logPath / (prefix + "_11111111_222222.bad.ext"),
+                              logPath / (prefix + "x_11111111_222222.log.gz")};
+    fs::create_directory(dir);
+    for (const auto& i : files)
+    {
+        std::ofstream dummy(i);
+    }
+
+    const auto itBegin = fs::recursive_directory_iterator(logPath);
+    const auto itEnd = fs::recursive_directory_iterator{};
+    EXPECT_EQ(std::distance(itBegin, itEnd),
+              limit + 1 /*dir*/ + sizeof(files) / sizeof(files[0]));
+    EXPECT_TRUE(fs::exists(dir));
+    for (const auto& i : files)
+    {
+        EXPECT_TRUE(fs::exists(i));
+    }
+}
diff --git a/test/host_console_test.cpp b/test/host_console_test.cpp
new file mode 100644
index 0000000..1581725
--- /dev/null
+++ b/test/host_console_test.cpp
@@ -0,0 +1,88 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "host_console.hpp"
+
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#include <gtest/gtest.h>
+
+static constexpr char socketPath[] = "\0obmc-console";
+
+/**
+ * @class HostConsoleTest
+ * @brief Persistent file storage tests.
+ */
+class HostConsoleTest : public ::testing::Test
+{
+  protected:
+    void startServer(const char* socketId)
+    {
+        // Start server
+        serverSocket = socket(AF_UNIX, SOCK_STREAM, 0);
+        ASSERT_NE(serverSocket, -1);
+        std::string path(socketPath, socketPath + sizeof(socketPath) - 1);
+        if (*socketId)
+        {
+            path += '.';
+            path += socketId;
+        }
+        sockaddr_un sa;
+        sa.sun_family = AF_UNIX;
+        memcpy(&sa.sun_path, path.c_str(), path.length());
+        const socklen_t len = sizeof(sa) - sizeof(sa.sun_path) + path.length();
+        ASSERT_NE(
+            bind(serverSocket, reinterpret_cast<const sockaddr*>(&sa), len),
+            -1);
+        ASSERT_NE(listen(serverSocket, 1), -1);
+    }
+
+    void TearDown() override
+    {
+        // Stop server
+        if (serverSocket != -1)
+        {
+            close(serverSocket);
+        }
+    }
+
+    int serverSocket = -1;
+};
+
+TEST_F(HostConsoleTest, SingleHost)
+{
+    const char* socketId = "";
+    startServer(socketId);
+
+    HostConsole con(socketId);
+    con.connect();
+
+    const int clientSocket = accept(serverSocket, nullptr, nullptr);
+    EXPECT_NE(clientSocket, -1);
+    close(clientSocket);
+}
+
+TEST_F(HostConsoleTest, MultiHost)
+{
+    const char* socketId = "host123";
+    startServer(socketId);
+
+    HostConsole con(socketId);
+    con.connect();
+
+    const int clientSocket = accept(serverSocket, nullptr, nullptr);
+    EXPECT_NE(clientSocket, -1);
+
+    const char* data = "test data";
+    const size_t len = strlen(data);
+    EXPECT_EQ(send(clientSocket, data, len, 0), len);
+
+    char buf[64];
+    memset(buf, 0, sizeof(buf));
+    EXPECT_EQ(con.read(buf, sizeof(buf)), len);
+    EXPECT_STREQ(buf, data);
+    EXPECT_EQ(con.read(buf, sizeof(buf)), 0);
+
+    close(clientSocket);
+}
diff --git a/test/log_buffer_test.cpp b/test/log_buffer_test.cpp
new file mode 100644
index 0000000..d7638be
--- /dev/null
+++ b/test/log_buffer_test.cpp
@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "log_buffer.hpp"
+
+#include <gtest/gtest.h>
+
+TEST(LogBufferTest, Append)
+{
+    const std::string msg = "Test message";
+
+    LogBuffer buf(0, 0);
+
+    buf.append(msg.data(), msg.length());
+    ASSERT_EQ(std::distance(buf.begin(), buf.end()), 1);
+    EXPECT_EQ(buf.begin()->text, msg);
+    EXPECT_NE(buf.begin()->timeStamp, 0);
+
+    // must be merged with previous message
+    const std::string append = "Append";
+    buf.append(append.data(), append.length());
+    ASSERT_EQ(std::distance(buf.begin(), buf.end()), 1);
+    EXPECT_EQ(buf.begin()->text, msg + append);
+
+    // end of line, we still have 1 message
+    buf.append("\n", 1);
+    ASSERT_EQ(std::distance(buf.begin(), buf.end()), 1);
+
+    // second message
+    buf.append(append.data(), append.length());
+    ASSERT_EQ(std::distance(buf.begin(), buf.end()), 2);
+    EXPECT_EQ((++buf.begin())->text, append);
+}
+
+TEST(LogBufferTest, AppendEol)
+{
+    LogBuffer buf(0, 0);
+
+    buf.append("\r\r\r\r", 4);
+    EXPECT_EQ(std::distance(buf.begin(), buf.end()), 4);
+
+    buf.clear();
+    buf.append("\n\n\n\n", 4);
+    EXPECT_EQ(std::distance(buf.begin(), buf.end()), 4);
+
+    buf.clear();
+    buf.append("\r\n\r\n", 4);
+    EXPECT_EQ(std::distance(buf.begin(), buf.end()), 2);
+
+    buf.clear();
+    buf.append("\n\r\n\r", 4);
+    EXPECT_EQ(std::distance(buf.begin(), buf.end()), 2);
+
+    buf.clear();
+    buf.append("\r\r\r\n\n\n", 6);
+    EXPECT_EQ(std::distance(buf.begin(), buf.end()), 5);
+}
+
+TEST(LogBufferTest, Clear)
+{
+    const std::string msg = "Test message";
+
+    LogBuffer buf(0, 0);
+    buf.append(msg.data(), msg.length());
+    EXPECT_FALSE(buf.empty());
+    buf.clear();
+    EXPECT_TRUE(buf.empty());
+}
+
+TEST(LogBufferTest, SizeLimit)
+{
+    const size_t limit = 5;
+    const std::string msg = "Test message\n";
+
+    LogBuffer buf(limit, 0);
+    for (size_t i = 0; i < limit + 3; ++i)
+    {
+        buf.append(msg.data(), msg.length());
+    }
+    EXPECT_EQ(std::distance(buf.begin(), buf.end()), limit);
+}
+
+TEST(LogBufferTest, FullHandler)
+{
+    const size_t limit = 5;
+    const std::string msg = "Test message\n";
+
+    size_t count = 0;
+
+    LogBuffer buf(limit, 0);
+    buf.setFullHandler([&count, &buf]() {
+        ++count;
+        buf.clear();
+    });
+    for (size_t i = 0; i < limit + 3; ++i)
+    {
+        buf.append(msg.data(), msg.length());
+    }
+    EXPECT_EQ(count, 1);
+    EXPECT_EQ(std::distance(buf.begin(), buf.end()), 2);
+}
diff --git a/test/meson.build b/test/meson.build
new file mode 100644
index 0000000..1a10b6f
--- /dev/null
+++ b/test/meson.build
@@ -0,0 +1,26 @@
+# Rules for building tests
+
+test(
+  'hostlogger',
+  executable(
+    'hostlogger_test',
+    [
+      'config_test.cpp',
+      'file_storage_test.cpp',
+      'host_console_test.cpp',
+      'log_buffer_test.cpp',
+      'zlib_file_test.cpp',
+      '../src/config.cpp',
+      '../src/file_storage.cpp',
+      '../src/host_console.cpp',
+      '../src/log_buffer.cpp',
+      '../src/zlib_exception.cpp',
+      '../src/zlib_file.cpp',
+    ],
+    dependencies: [
+      dependency('gtest', main: true, disabler: true, required: build_tests),
+      dependency('zlib'),
+    ],
+    include_directories: '../src',
+  )
+)
diff --git a/test/zlib_file_test.cpp b/test/zlib_file_test.cpp
new file mode 100644
index 0000000..ef49a6f
--- /dev/null
+++ b/test/zlib_file_test.cpp
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: Apache-2.0
+// Copyright (C) 2020 YADRO
+
+#include "zlib_exception.hpp"
+#include "zlib_file.hpp"
+
+#include <gtest/gtest.h>
+
+TEST(ZlibFileTest, Exception)
+{
+    ASSERT_THROW(ZlibFile("invalid/path"), ZlibException);
+}
+
+TEST(ZlibFileTest, Write)
+{
+    const std::string msg = "Test message";
+    time_t currTime;
+    time(&currTime);
+    tm localTime;
+    localtime_r(&currTime, &localTime);
+
+    const std::string path = "/tmp/zlib_file_test.out";
+    ZlibFile file(path);
+    file.write(localTime, msg);
+    file.close();
+
+    char expect[64];
+    const int len = snprintf(expect, sizeof(expect), "[ %02i:%02i:%02i ] %s\n",
+                             localTime.tm_hour, localTime.tm_min,
+                             localTime.tm_sec, msg.c_str());
+
+    gzFile fd = gzopen(path.c_str(), "r");
+    ASSERT_TRUE(fd);
+    char buf[64];
+    memset(buf, 0, sizeof(buf));
+    EXPECT_EQ(gzread(fd, buf, sizeof(buf)), len);
+    EXPECT_STREQ(buf, expect);
+    EXPECT_EQ(gzclose(fd), 0);
+
+    unlink(path.c_str());
+}