added JSON support to select the services

This commit is contained in:
Lukas Forsberg 2025-05-27 21:33:15 +02:00
parent d9d0643dfc
commit 1e9a377c2a
19 changed files with 26060 additions and 139 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
cmake-build-debug
cmake-build-release
build
# JetBrains IDEs
.idea/
!.idea/codeStyles/
!.idea/runConfigurations/
!.idea/inspectionProfiles/
*.iml

View File

@ -81,6 +81,12 @@
"typeinfo": "cpp",
"valarray": "cpp",
"variant": "cpp",
"*.ipp": "cpp"
"*.ipp": "cpp",
"expected": "cpp",
"queue": "cpp",
"stack": "cpp",
"strstream": "cpp",
"complex": "cpp",
"typeindex": "cpp"
}
}

View File

@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.10)
project(CrowHTMX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Allow selection of build type if not set
@ -32,7 +32,21 @@ endforeach()
# Use Crow from system include (installed via yay -S crow + asio)
include_directories(/usr/include)
add_executable(app main.cpp)
add_executable(app src/main.cpp
src/htmx/HtmxTable.cpp
src/htmx/HtmxTable.h
src/systemd.cpp
src/systemd.h
src/json.hpp
src/htmx/HtmxTableRow.cpp
src/htmx/HtmxTableRow.h
src/htmx/HtmxObject.cpp
src/htmx/HtmxObject.h
src/htmx_helper.cpp
src/htmx_helper.h
src/json_settings.cpp
src/json_settings.h
src/json.hpp)
target_link_libraries(app pthread)

136
main.cpp
View File

@ -1,136 +0,0 @@
#include <crow.h>
#include <fstream>
#include <sstream>
#include <cstdlib>
#include <string>
#include <array>
#include <format>
#include <optional>
using namespace std;
string load_file(const string& path) {
ifstream f(path);
stringstream buffer;
buffer << f.rdbuf();
return buffer.str();
}
bool is_service_active(string_view service_name) {
const string cmd = format("systemctl is-active --quiet {}", service_name);
return system(cmd.c_str()) == 0;
}
bool is_service_enabled(string_view service_name) {
const string cmd = format("systemctl is-enabled --quiet {}", service_name);
return system(cmd.c_str()) == 0;
}
void toggle_service(string_view serviceName){
string_view toggle = is_service_active(serviceName) ? "stop" : "start";
const string cmd = format("systemctl {} {}", toggle, serviceName);
system(cmd.c_str());
}
string_view get_button_class(bool isActive) {
return isActive ? "active-button" : "inactive-button";
}
optional<string> get_body_name(const string& body) {
const auto pos = body.find('=');
if (pos == std::string::npos) return {};
const string key = body.substr(0, pos);
string value = body.substr(pos + 1);
if (key != "name") return {};
return value;
}
string create_htmx_button(string_view endpoint, string_view serviceName, string_view text) {
return format(
"<td>\
<button\
hx-post=\"{}\"\
hx-vals='{{\"name\":\"{}\"}}'\
hx-target=\"closest tr\"\
hx-swap=\"outerHTML\">\
{} \
</button> \
</td>", endpoint, serviceName, text);
}
string create_htmx_table_row(string_view serviceName){
const bool isRunning = is_service_active(serviceName);
const bool isEnabled = is_service_active(serviceName);
const auto running = isRunning ? "Running" : "Stopped";
const auto enabled = isEnabled ? "Enabled" : "Disabled";
// create status indicators
string html = format(
"<tr>\
<td>{}</td>\
<td class='{}'>{}</td>\
<td class='{}'>{}</td>",
serviceName, get_button_class(isRunning), running, get_button_class(isEnabled), enabled);
// create buttons
html += create_htmx_button("/toggle-service", serviceName, isRunning ? "Stop" : "Start");
html += create_htmx_button("/enable-service", serviceName, isEnabled ? "Disable" : "Enable");
html += create_htmx_button("/restart-service", serviceName, "Restart");
return html;
}
const array<string, 1>& get_service_names() {
static const array<string, 1> arr = {
"cups.service"
};
return arr;
}
int main() {
crow::SimpleApp app;
CROW_ROUTE(app, "/")([] {
return crow::response(load_file("templates/index.html"));
});
CROW_ROUTE(app, "/static/<string>")([](const std::string& file) {
return crow::response(load_file("static/" + file));
});
CROW_ROUTE(app, "/status")([] {
auto names = get_service_names();
// define the table header
string html = "<table><tr>\
<th>Service</th>\
<th>Status</th>\
<th>State</th>\
</tr>";
// display each service as an entry
for (auto& serviceName : names) {
html += create_htmx_table_row(serviceName);
}
html += " </tr></table>";
return crow::response{html};
});
CROW_ROUTE(app, "/toggle-service").methods(crow::HTTPMethod::Post)([](const crow::request& req) {
auto body = get_body_name(req.body);
if (!body.has_value())
return crow::response(400);
const string& serviceName = body.value();
toggle_service(serviceName);
const string html = create_htmx_table_row(serviceName);
return crow::response{html};
});
app.port(8080).multithreaded().run();
}

11
src/htmx/HtmxObject.cpp Normal file
View File

@ -0,0 +1,11 @@
//
// Created by lukas on 5/11/25.
//
#include "HtmxObject.h"
using namespace std;
const string& HtmxObject::htmx() const {
return html;
}

28
src/htmx/HtmxObject.h Normal file
View File

@ -0,0 +1,28 @@
//
// Created by lukas on 5/11/25.
//
#ifndef HTMXOBJECT_H
#define HTMXOBJECT_H
#include <string>
class HtmxObject {
public:
HtmxObject() = default;
/**
* Get the HTMX representation of the object as a string
* @return htmx string
*/
const std::string& htmx() const;
protected:
std::string html;
};
#endif //HTMXOBJECT_H

17
src/htmx/HtmxTable.cpp Normal file
View File

@ -0,0 +1,17 @@
//
// Created by lukas on 5/11/25.
//
#include <format>
#include "HtmxTable.h"
using namespace std;
void HtmxTable::add_row(const HtmxTableRow& row){
html += row.htmx();
}
void HtmxTable::complete() {
html += "</table>";
}

40
src/htmx/HtmxTable.h Normal file
View File

@ -0,0 +1,40 @@
//
// Created by lukas on 5/11/25.
//
#ifndef HTMXTABLE_H
#define HTMXTABLE_H
#include "HtmxObject.h"
#include "HtmxTableRow.h"
#include "format"
class HtmxTable : public HtmxObject {
public:
template<std::ranges::input_range R>
requires std::convertible_to<std::ranges::range_value_t<R>, std::string_view>
HtmxTable(const R& strings) {
// define the table header
html = "<table><tr>";
for (const auto& s : strings) {
html += format("<th>{}</th>", s);
}
html += "</tr>";
}
/**
* Add a htmx row to the table
*
* @param row
* @return htmx representation of the row
*/
void add_row(const HtmxTableRow& row);
/**
* Complete the table
*/
void complete();
};
#endif //HTMXTABLE_H

35
src/htmx/HtmxTableRow.cpp Normal file
View File

@ -0,0 +1,35 @@
//
// Created by lukas on 5/11/25.
//
#include <format>
#include "HtmxTableRow.h"
using namespace std;
void HtmxTableRow::add_button(string_view endpoint, string_view name, string_view text) {
html += format("<td>\
<button\
hx-post=\"{}\"\
hx-vals='{{\"name\":\"{}\"}}'\
hx-target=\"closest tr\"\
hx-swap=\"outerHTML\">\
{} \
</button> \
</td>", endpoint, name, text);
}
static string_view get_button_class(bool is_active) {
return is_active ? "active-button" : "inactive-button";
}
void HtmxTableRow::add_status_box(std::string_view name, bool is_active) {
html += format("<td class='{}'>{}</td>", get_button_class(is_active), name);
}
void HtmxTableRow::add(string_view text) {
html += format("<td>{}</td>", text);
}
void HtmxTableRow::complete() {
html += "</tr>";
}

28
src/htmx/HtmxTableRow.h Normal file
View File

@ -0,0 +1,28 @@
//
// Created by lukas on 5/11/25.
//
#ifndef HTMXTABLEROW_H
#define HTMXTABLEROW_H
#include <string>
#include "HtmxObject.h"
class HtmxTableRow : public HtmxObject {
public:
void add_status_box(std::string_view name, bool is_active);
void add_button(std::string_view endpoint, std::string_view name, std::string_view text);
void add(std::string_view text);
/**
* Complete the row
*/
void complete();
};
#endif //HTMXTABLEROW_H

59
src/htmx_helper.cpp Normal file
View File

@ -0,0 +1,59 @@
//
// Created by lukas on 5/11/25.
//
#include <array>
#include "systemd.h"
#include "htmx_helper.h"
#include "json_settings.h"
using namespace std;
HtmxTableRow create_service_table_row(string_view service_name, string_view service_id) {
HtmxTableRow row;
const bool isRunning = systemd::is_service_active(service_id);
const bool isEnabled = systemd::is_service_active(service_id);
const auto running = isRunning ? "Running" : "Stopped";
const auto enabled = isEnabled ? "Enabled" : "Disabled";
row.add(service_name);
row.add_status_box(running, isRunning);
row.add_status_box(enabled, isEnabled);
// create buttons
row.add_button("/toggle-service", service_name, isRunning ? "Stop" : "Start");
row.add_button("/restart-service", service_name, "Restart");
row.complete();
return row;
}
HtmxTableRow create_error_table_row(string_view error) {
HtmxTableRow row;
row.add("Error");
row.add(error);
return row;
}
HtmxTable create_service_table() {
constexpr array<string_view, 3> cols = {
"Service", "Status", "State"
};
HtmxTable table(cols);
auto settings = AppSettings::loadAppSettings();
if(settings.has_value()){
AppSettings& data = settings.value();
for (auto& service : data.services) {
table.add_row(create_service_table_row(service.name, service.service));
}
}
else {
table.add_row(create_error_table_row(settings.error()));
}
return table;
}

15
src/htmx_helper.h Normal file
View File

@ -0,0 +1,15 @@
//
// Created by lukas on 5/11/25.
//
#ifndef HTMX_HELPER_H
#define HTMX_HELPER_H
#include <string>
#include "htmx/HtmxTable.h"
HtmxTableRow create_service_table_row(std::string_view service_name, std::string_view service_id);
HtmxTableRow create_error_table_row(std::string_view error);
HtmxTable create_service_table();
#endif //HTMX_HELPER_H

25580
src/json.hpp Normal file

File diff suppressed because it is too large Load Diff

54
src/json_settings.cpp Normal file
View File

@ -0,0 +1,54 @@
#include <fstream>
#include <format>
#include "json.hpp"
#include "json_settings.h"
using namespace std;
using json = nlohmann::json;
expected<AppSettings, string> AppSettings::loadAppSettings() {
constexpr char settings_file[] = "static/settings.json";
ifstream file(settings_file);
if (!file.is_open()) {
return unexpected(format("Failed to open {}",settings_file));
}
// Parse the JSON
json j;
try {
file >> j;
} catch (const json::parse_error& e) {
return unexpected(format("Failed to parse JSON {}",e.what()));
}
if (!j.contains("services") || !j["services"].is_array()) {
return unexpected("JSON does not contain an array called 'services'");
}
AppSettings settings;
for (const auto& item : j["services"]) {
if (item.is_array() && item.size() == 2) {
Service service = {
item[0].get<std::string>(),
item[1].get<std::string>()
};
settings.services.push_back(service);
}
}
if (settings.services.empty()){
return unexpected("'services' array in JSON file is empty");
}
return settings;
}
optional<const std::string&> AppSettings::getId(string_view name){
for (auto& service : services) {
if(service.name == name) {
return service.service;
}
}
return {};
}

22
src/json_settings.h Normal file
View File

@ -0,0 +1,22 @@
#ifndef JSON_SETTINGS_H
#define JSON_SETTINGS_H
#include <vector>
#include <string>
#include <expected>
#include <optional>
struct Service {
std::string name;
std::string service;
};
struct AppSettings {
static std::expected<AppSettings, std::string> loadAppSettings();
std::optional<const std::string&> getId(std::string_view name);
std::vector<Service> services;
};
#endif // JSON_SETTINGS_H

72
src/main.cpp Normal file
View File

@ -0,0 +1,72 @@
#include <crow.h>
#include <fstream>
#include <sstream>
#include <cstdlib>
#include <string>
#include <optional>
#include "json_settings.h"
#include "htmx_helper.h"
#include "systemd.h"
using namespace std;
string load_file(const string& path) {
ifstream f(path);
stringstream buffer;
buffer << f.rdbuf();
return buffer.str();
}
optional<string> get_body_name(const string& body) {
const auto pos = body.find('=');
if (pos == std::string::npos) return {};
const string key = body.substr(0, pos);
string value = body.substr(pos + 1);
if (key != "name") return {};
return value;
}
int main() {
crow::SimpleApp app;
CROW_ROUTE(app, "/")([] {
return crow::response(load_file("templates/index.html"));
});
CROW_ROUTE(app, "/static/<string>")([](const std::string& file) {
return crow::response(load_file("static/" + file));
});
CROW_ROUTE(app, "/status")([] {
auto table = create_service_table();
return crow::response{table.htmx()};
});
CROW_ROUTE(app, "/toggle-service").methods(crow::HTTPMethod::Post)([](const crow::request& req) {
auto body = get_body_name(req.body);
if (!body.has_value())
return crow::response(400);
const string& serviceName = body.value();
auto opt_settings = AppSettings::loadAppSettings();
HtmxTableRow row;
if (opt_settings.has_value()) {
auto& settings = opt_settings.value();
const auto& service_id = settings.getId(serviceName).value_or(serviceName);
systemd::toggle_service(service_id);
row = create_service_table_row(serviceName, service_id);
}
else {
row = create_error_table_row(opt_settings.error());
}
return crow::response{row.htmx()};
});
app.port(8080).multithreaded().run();
}

26
src/systemd.cpp Normal file
View File

@ -0,0 +1,26 @@
//
// Created by lukas on 5/11/25.
//
#include "format"
#include "systemd.h"
using namespace std;
namespace systemd {
bool is_service_active(string_view service_name) {
const string cmd = format("systemctl is-active --quiet {}", service_name);
return system(cmd.c_str()) == 0;
}
bool is_service_enabled(string_view service_name) {
const string cmd = format("systemctl is-enabled --quiet {}", service_name);
return system(cmd.c_str()) == 0;
}
void toggle_service(string_view serviceName){
string_view toggle = is_service_active(serviceName) ? "stop" : "start";
const string cmd = format("systemctl {} {}", toggle, serviceName);
system(cmd.c_str());
}
}

34
src/systemd.h Normal file
View File

@ -0,0 +1,34 @@
//
// Created by lukas on 5/11/25.
//
#ifndef SYSTEMD_H
#define SYSTEMD_H
#include <string>
namespace systemd {
/**
* Check if a service is active
* @param service_name name of the systemd service
* @return
*/
bool is_service_active(std::string_view service_name);
/**
* Check if a service is enabled
* @param service_name name of the systemd service
* @return
*/
bool is_service_enabled(std::string_view service_name);
/**
* Toggle the service on or off dependent on its current state
* @param service_name name of the systemd service
*/
void toggle_service(std::string_view service_name);
}
#endif //SYSTEMD_H

6
static/settings.json Normal file
View File

@ -0,0 +1,6 @@
{
"services": [
["Cups", "cups.service"],
["Waydriod", "waydroid-container.service"]
]
}