From 7c0ff8c75ac534bd8953a2b7026f981f07b27fc8 Mon Sep 17 00:00:00 2001 From: apio Date: Sun, 1 Sep 2024 12:40:37 +0200 Subject: [PATCH] libos+init: Add a standard API for config file access --- libos/CMakeLists.txt | 1 + libos/include/os/Config.h | 43 ++++++++++++ libos/src/Config.cpp | 139 ++++++++++++++++++++++++++++++++++++ system/init.cpp | 143 +++++++++----------------------------- 4 files changed, 215 insertions(+), 111 deletions(-) create mode 100644 libos/include/os/Config.h create mode 100644 libos/src/Config.cpp diff --git a/libos/CMakeLists.txt b/libos/CMakeLists.txt index 3ad1f5d3..4043788d 100644 --- a/libos/CMakeLists.txt +++ b/libos/CMakeLists.txt @@ -20,6 +20,7 @@ set(SOURCES src/IPC.cpp src/SharedMemory.cpp src/Timer.cpp + src/Config.cpp ) add_library(os ${SOURCES}) diff --git a/libos/include/os/Config.h b/libos/include/os/Config.h new file mode 100644 index 00000000..a6756935 --- /dev/null +++ b/libos/include/os/Config.h @@ -0,0 +1,43 @@ +/** + * @file Config.h + * @author apio (cloudapio.eu) + * @brief Configuration file parsing. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#pragma once +#include +#include +#include +#include + +namespace os +{ + class ConfigFile + { + public: + static Result> open(os::Path path); + + Option read_string(StringView key); + StringView read_string_or(StringView key, StringView default_value); + + Option read_number(StringView key); + i64 read_number_or(StringView key, i64 default_value); + + Option read_boolean(StringView key); + bool read_boolean_or(StringView key, bool default_value); + + Result write_string(StringView key, StringView value); + Result write_number(StringView key, int value); + Result write_boolean(StringView key, bool value); + + Result sync(); + + private: + os::Path m_path { "" }; + + HashMap m_data; + }; +} diff --git a/libos/src/Config.cpp b/libos/src/Config.cpp new file mode 100644 index 00000000..c2482ce4 --- /dev/null +++ b/libos/src/Config.cpp @@ -0,0 +1,139 @@ +/** + * @file Config.cpp + * @author apio (cloudapio.eu) + * @brief Configuration file parsing. + * + * @copyright Copyright (c) 2024, the Luna authors. + * + */ + +#include +#include + +namespace os +{ + Result> ConfigFile::open(os::Path path) + { + auto config = TRY(make_owned()); + + config->m_path = path; + + auto file = TRY(os::File::open(path, os::File::ReadOnly)); + + while (true) + { + auto line = TRY(file->read_line()); + if (line.is_empty()) break; + + line.trim("\n"); + if (line.is_empty()) continue; + + auto parts = TRY(line.split_once('=')); + if (parts.size() < 2 || parts[0].is_empty() || parts[1].is_empty()) return err(EINVAL); + + auto key = TRY(RefString::from_string(parts[0])); + auto value = TRY(RefString::from_string(parts[1])); + + config->m_data.try_set(key, value); + } + + return config; + } + + Option ConfigFile::read_string(StringView key) + { + auto string = RefString::from_string_view(key).release_value(); + auto* value = m_data.try_get_ref(string); + + if (!value) return {}; + + return value->view(); + } + + StringView ConfigFile::read_string_or(StringView key, StringView default_value) + { + return read_string(key).value_or(default_value); + } + + Option ConfigFile::read_number(StringView key) + { + auto maybe_string = read_string(key); + + if (!maybe_string.has_value()) return {}; + + auto string = maybe_string.value(); + + char* endptr; + + i64 result = strtol(string.chars(), &endptr, 10); + + if (string.chars() == endptr) return {}; + + return result; + } + + i64 ConfigFile::read_number_or(StringView key, i64 default_value) + { + return read_number(key).value_or(default_value); + } + + Option ConfigFile::read_boolean(StringView key) + { + auto maybe_string = read_string(key); + + if (!maybe_string.has_value()) return {}; + + auto string = maybe_string.value(); + + if (string == "true" || string.to_uint().value_or(0) == 1) return true; + if (string == "false" || string.to_uint().value_or(1) == 0) return false; + + return {}; + } + + bool ConfigFile::read_boolean_or(StringView key, bool default_value) + { + return read_boolean(key).value_or(default_value); + } + + Result ConfigFile::write_string(StringView key, StringView value) + { + auto key_string = TRY(RefString::from_string_view(key)); + auto value_string = TRY(RefString::from_string_view(value)); + + auto worked = TRY(m_data.try_set(key_string, value_string)); + if (!worked) + { + m_data.try_remove(key_string); + check(TRY(m_data.try_set(key_string, value_string))); + } + + return {}; + } + + Result ConfigFile::write_number(StringView key, int value) + { + auto value_string = TRY(String::format("%d"_sv, value)); + + return write_string(key, value_string.view()); + } + + Result ConfigFile::write_boolean(StringView key, bool value) + { + StringView value_string = value ? "true" : "false"; + + return write_string(key, value_string); + } + + Result ConfigFile::sync() + { + auto file = TRY(os::File::open(m_path, os::File::WriteOnly)); + + for (auto& pair : m_data) + { + file->write_formatted("%s=%s\n", pair.key.chars(), pair.value.release_value().chars()); + } + + return {}; + } +} diff --git a/system/init.cpp b/system/init.cpp index cf238661..6d8d1bb7 100644 --- a/system/init.cpp +++ b/system/init.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -191,129 +192,49 @@ static Result load_service(const os::Path& path) { do_log("[init] reading service file: %s\n", path.name().chars()); - auto file = TRY(os::File::open(path, os::File::ReadOnly)); + auto file = TRY(os::ConfigFile::open(path)); Service service; - while (true) - { - auto line = TRY(file->read_line()); - if (line.is_empty()) break; - - line.trim("\n"); - if (line.is_empty()) continue; - - auto parts = TRY(line.split_once('=')); - if (parts.size() < 2 || parts[0].is_empty() || parts[1].is_empty()) - { - do_log("[init] file contains invalid line, aborting: '%s'\n", line.chars()); - return {}; - } - - if (parts[0].view() == "Name") - { - service.name = move(parts[1]); - continue; - } - - if (parts[0].view() == "Description") - { - // We let users specify this in the config file, but init doesn't actually use it. - continue; - } - - if (parts[0].view() == "Command") - { - service.command = move(parts[1]); - continue; - } - - if (parts[0].view() == "Restart") - { - if (parts[1].view() == "true" || parts[1].view().to_uint().value_or(0) == 1) - { - service.restart = true; - continue; - } - service.restart = false; - continue; - } - - if (parts[0].view() == "Environment") - { - service.environment = move(parts[1]); - continue; - } - - if (parts[0].view() == "StandardOutput") - { - service.standard_output = move(parts[1]); - continue; - } - - if (parts[0].view() == "StandardError") - { - service.standard_error = move(parts[1]); - continue; - } - - if (parts[0].view() == "StandardInput") - { - service.standard_input = move(parts[1]); - continue; - } - - if (parts[0].view() == "WorkingDirectory") - { - service.working_directory = move(parts[1]); - continue; - } - - if (g_is_system && parts[0].view() == "User") - { - auto* pw = getpwnam(parts[1].chars()); - if (!pw) continue; - service.user = pw->pw_uid; - service.group = pw->pw_gid; - continue; - } - - if (parts[0].view() == "Wait") - { - if (parts[1].view() == "true" || parts[1].view().to_uint().value_or(0) == 1) - { - service.wait = true; - continue; - } - service.wait = false; - continue; - } - - if (parts[0].view() == "WaitUntilReady") - { - if (parts[1].view() == "true" || parts[1].view().to_uint().value_or(0) == 1) - { - service.wait_notify = true; - continue; - } - service.wait_notify = false; - continue; - } - - do_log("[init] skipping unknown entry name %s\n", parts[0].chars()); - } - - if (service.name.is_empty()) + auto name = file->read_string("Name"); + if (!name.has_value()) { do_log("[init] service file is missing 'Name' entry, aborting!\n"); return {}; } + service.name = TRY(String::from_string_view(name.value())); - if (service.command.is_empty()) + auto command = file->read_string("Command"); + if (!command.has_value()) { do_log("[init] service file is missing 'Command' entry, aborting!\n"); return {}; } + service.command = TRY(String::from_string_view(command.value())); + + service.restart = file->read_boolean_or("Restart", false); + service.environment = TRY(String::from_string_view(file->read_string_or("Environment", {}))); + service.standard_output = TRY(String::from_string_view(file->read_string_or("StandardOutput", {}))); + service.standard_error = TRY(String::from_string_view(file->read_string_or("StandardError", {}))); + service.standard_input = TRY(String::from_string_view(file->read_string_or("StandardInput", {}))); + service.working_directory = TRY(String::from_string_view(file->read_string_or("WorkingDirectory", {}))); + + if (g_is_system) + { + auto user = file->read_string("User"); + if (user.has_value()) + { + auto* pw = getpwnam(user.value().chars()); + if (pw) + { + service.user = pw->pw_uid; + service.group = pw->pw_gid; + } + } + } + + service.wait = file->read_boolean_or("Wait", false); + service.wait_notify = file->read_boolean_or("WaitUntilReady", false); do_log("[init] loaded service %s into memory\n", service.name.chars());