BareGit

Configure Unix socket listening

- Add unix_socket and octal socket_permission configuration fields.
- Apply Unix socket settings when constructing the HTTP server.
- Document and test the new listener configuration.
- Keep Arch packaging aligned with the runtime socket directory.
Author: MetroWind <chris.corsair@gmail.com>
Date: Sat Sep 19 12:06:29 2026 -0700
Commit: 5f2af5c277cbb983aba60932ce28fd0fd846ef6b

Changes

diff --git a/config.example.yaml b/config.example.yaml
index f7ad897..60d5f16 100644
--- a/config.example.yaml
+++ b/config.example.yaml
@@ -1,8 +1,10 @@
 # Copy this file to a local path and run:
-#   status_tracker ./config.yaml
+#   probius ./config.yaml
 
 worker_count: 4
 database_path: status-tracker.sqlite
+# unix_socket: /run/probius/probius.sock
+# socket_permission: 0660
 
 # Groups are a sequence. Service IDs are mapping keys and must be unique.
 groups:
diff --git a/packages/arch/PKGBUILD b/packages/arch/PKGBUILD
index 4bff02c..9b2e85c 100644
--- a/packages/arch/PKGBUILD
+++ b/packages/arch/PKGBUILD
@@ -22,9 +22,9 @@ source=(
 )
 sha256sums=(
     'SKIP'
-    '84b07e2cdc9673bd6a933eb40b92e29249cef13ec0f682bf049f94c5f779db63'
+    'c16a0f6151f0c64681339ac131e07bde8707ab994ba5707d541c6fb1501cca0c'
     '0f57b093e9e0412f2bfaced764c27929a581e6cdaa2842c904701337959712ff'
-    'a5ba1c774bf5531250e37d6e3e83aacbeb02e6afb3ecfe80b7820f43c210d22e'
+    'c486ce4ddedb590db2ed7406e112b70d1ad55628f9c6b049286e2ab144661d72'
 )
 
 pkgver()
diff --git a/packages/arch/probius.service b/packages/arch/probius.service
index 79bb259..6fa67df 100644
--- a/packages/arch/probius.service
+++ b/packages/arch/probius.service
@@ -7,6 +7,7 @@ Wants=network-online.target
 User=probius
 Group=probius
 ExecStart=/usr/bin/probius /etc/probius.yaml
+RuntimeDirectory=probius
 StateDirectory=probius
 Restart=on-failure
 RestartSec=5s
diff --git a/packages/arch/probius.yaml b/packages/arch/probius.yaml
index f0abef2..9aa9f3e 100644
--- a/packages/arch/probius.yaml
+++ b/packages/arch/probius.yaml
@@ -1,3 +1,5 @@
 worker_count: 4
 database_path: /var/lib/probius/probius.sqlite
+# unix_socket: /run/probius/probius.sock
+# socket_permission: 0660
 groups: []
diff --git a/src/configuration.cpp b/src/configuration.cpp
index fd82131..00a5c71 100644
--- a/src/configuration.cpp
+++ b/src/configuration.cpp
@@ -128,6 +128,38 @@ mw::E<std::optional<Integer>> optionalInteger(
     return std::optional<Integer>{result};
 }
 
+mw::E<std::optional<unsigned int>> optionalSocketPermission(
+    Node node, std::string_view key, std::string_view context)
+{
+    auto value = child(node, key);
+    if(!value.readable())
+    {
+        return std::optional<unsigned int>{};
+    }
+    if(!value.has_val())
+    {
+        return std::unexpected(configError(
+            context, std::string("invalid socket permission ") +
+                         std::string(key)));
+    }
+    std::string_view text(value.val().begin(), value.val().size());
+    if(text.starts_with("0o") || text.starts_with("0O"))
+    {
+        text.remove_prefix(2);
+    }
+    unsigned int result = 0;
+    const auto parsed = std::from_chars(
+        text.begin(), text.end(), result, 8);
+    if(text.empty() || parsed.ec != std::errc{} ||
+       parsed.ptr != text.end() || result > 0777)
+    {
+        return std::unexpected(configError(
+            context, std::string(key) +
+                         " must be an octal mode from 000 through 777"));
+    }
+    return std::optional<unsigned int>{result};
+}
+
 std::string lower(std::string value)
 {
     std::transform(value.begin(), value.end(), value.begin(),
@@ -315,6 +347,20 @@ mw::E<Configuration> parseTree(ryml::Tree& tree)
         return std::unexpected(configError(
             "configuration", "database_path must not be empty"));
     }
+    ASSIGN_OR_RETURN(auto unix_socket,
+                     optionalString(root, "unix_socket", "configuration"));
+    if(unix_socket && unix_socket->find('\0') != std::string::npos)
+    {
+        return std::unexpected(configError(
+            "configuration", "unix_socket contains a null character"));
+    }
+    if(unix_socket)
+    {
+        result.unix_socket = std::move(*unix_socket);
+    }
+    ASSIGN_OR_RETURN(result.socket_permission,
+                     optionalSocketPermission(
+                         root, "socket_permission", "configuration"));
     auto groups = child(root, "groups");
     if(!groups.readable() || !groups.is_seq())
     {
diff --git a/src/configuration.h b/src/configuration.h
index 32a9dac..9fe9f9c 100644
--- a/src/configuration.h
+++ b/src/configuration.h
@@ -2,6 +2,7 @@
 
 #include <cstddef>
 #include <filesystem>
+#include <optional>
 #include <string>
 #include <vector>
 
@@ -27,6 +28,10 @@ struct Configuration
     std::string database_path;
     /// Service groups in configuration order.
     std::vector<ServiceGroupConfig> groups;
+    /// Unix-domain socket path; empty selects the default TCP listener.
+    std::string unix_socket;
+    /// Permission bits applied to the Unix-domain socket, when configured.
+    std::optional<unsigned int> socket_permission;
 
     /// Load and validate a YAML configuration file.
     static mw::E<Configuration> fromYaml(
diff --git a/src/main.cpp b/src/main.cpp
index b51004d..e57a346 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,5 +1,6 @@
 #include <filesystem>
 #include <iostream>
+#include <string>
 #include <string_view>
 #include <thread>
 #include <utility>
@@ -13,7 +14,7 @@ namespace
 
 void printUsage(std::ostream& output)
 {
-    output << "Usage: status_tracker FILE\n";
+    output << "Usage: probius FILE\n";
 }
 
 }
@@ -50,7 +51,17 @@ int main(int argc, char** argv)
         std::cerr << scheduler.error().msg() << '\n';
         return 1;
     }
-    App app(mw::IPSocketInfo{"127.0.0.1", 8080},
+    mw::HTTPServer::ListenAddress listen =
+        mw::IPSocketInfo{"127.0.0.1", 8080};
+    std::string listen_description = "http://127.0.0.1:8080";
+    if(!configuration->unix_socket.empty())
+    {
+        mw::SocketFileInfo socket(configuration->unix_socket);
+        socket.permission = configuration->socket_permission;
+        listen = std::move(socket);
+        listen_description = "Unix socket " + configuration->unix_socket;
+    }
+    App app(listen,
             std::move(*configuration), **data_source);
     auto started = app.start();
     if(!started)
@@ -68,7 +79,7 @@ int main(int argc, char** argv)
                 app.stop();
             }
         });
-    std::cout << "Status Tracker listening on http://127.0.0.1:8080\n"
+    std::cout << "Probius listening on " << listen_description << '\n'
               << std::flush;
     app.wait();
     scheduler_thread.request_stop();
diff --git a/tests/configuration_test.cpp b/tests/configuration_test.cpp
index 6853e0c..f2f8b84 100644
--- a/tests/configuration_test.cpp
+++ b/tests/configuration_test.cpp
@@ -36,6 +36,8 @@ TEST(Configuration, LoadsGroupedProtocolSettings)
     TemporaryConfig file(
         "worker_count: 4\n"
         "database_path: tracker.sqlite\n"
+        "unix_socket: /run/probius.sock\n"
+        "socket_permission: 0660\n"
         "groups:\n"
         "  - name: Public services\n"
         "    services:\n"
@@ -86,6 +88,9 @@ TEST(Configuration, LoadsGroupedProtocolSettings)
         << (configuration ? "" : configuration.error().msg());
     EXPECT_EQ(configuration->worker_count, 4);
     EXPECT_EQ(configuration->database_path, "tracker.sqlite");
+    EXPECT_EQ(configuration->unix_socket, "/run/probius.sock");
+    ASSERT_TRUE(configuration->socket_permission);
+    EXPECT_EQ(*configuration->socket_permission, 0660);
     ASSERT_EQ(configuration->groups.size(), 1);
     ASSERT_EQ(configuration->groups.front().services.size(), 4);
 
@@ -132,7 +137,17 @@ TEST(Configuration, RejectsInvalidValues)
         "        name: x\n"
         "        description: x\n"
         "        interval: {value: 1, unit: minute}\n"
-        "        endpoint: {protocol: TCP, host: x, port: 0}\n"
+        "        endpoint: {protocol: TCP, host: x, port: 0}\n",
+        "worker_count: 1\n"
+        "database_path: db\n"
+        "unix_socket: /run/probius.sock\n"
+        "socket_permission: 0668\n"
+        "groups: []\n",
+        "worker_count: 1\n"
+        "database_path: db\n"
+        "unix_socket: /run/probius.sock\n"
+        "socket_permission: 01000\n"
+        "groups: []\n"
     };
     for(const auto& content : contents)
     {