Commit 0379aed0 authored by jan.koester's avatar jan.koester
Browse files

sysbus added

parent 864b983d
Loading
Loading
Loading
Loading
+32 −1
Changes for CMakeLists.txt: 32 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -12,12 +12,24 @@ set(SYSLITE_TTY_COUNT 3 CACHE STRING "Number of TTYs to spawn")

configure_file(config.h.in ${CMAKE_BINARY_DIR}/config.h)

# sysbus: the bus protocol and the client API other programs link against
add_library(sysbus STATIC
    src/sysbus/message.cpp
    src/sysbus/client.cpp
)
target_include_directories(sysbus PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)
set_target_properties(sysbus PROPERTIES POSITION_INDEPENDENT_CODE ON)

add_executable(syslite
    src/main.cpp
    src/unitfile.cpp
    src/service.cpp
    src/tty.cpp
    src/network.cpp
    src/busserver.cpp
    src/syslite.cpp
)

@@ -26,9 +38,28 @@ target_include_directories(syslite PRIVATE
    ${CMAKE_BINARY_DIR}
)

target_link_libraries(syslite PRIVATE)
target_link_libraries(syslite PRIVATE sysbus)

# syslitectl, also installed as systemctl
add_executable(syslitectl
    src/syslitectl.cpp
    src/unitfile.cpp
)
target_include_directories(syslitectl PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/src
    ${CMAKE_BINARY_DIR}
)
target_link_libraries(syslitectl PRIVATE sysbus)

add_executable(netdev src/netdev.cpp)

install(TARGETS syslite DESTINATION sbin)
install(TARGETS netdev DESTINATION sbin)
install(TARGETS syslitectl DESTINATION sbin)
install(CODE "execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink syslitectl
    \$ENV{DESTDIR}${CMAKE_INSTALL_PREFIX}/sbin/systemctl)")

install(TARGETS sysbus EXPORT sysbusTargets ARCHIVE DESTINATION lib)
install(DIRECTORY include/sysbus DESTINATION include)
install(EXPORT sysbusTargets NAMESPACE sysbus:: DESTINATION lib/cmake/sysbus
        FILE sysbusConfig.cmake)
+115 −0
Changes for include/sysbus/client.h: 115 added lines, 0 removed lines.
Original line number Diff line number Diff line
#pragma once

// sysbus client API: talks to syslite over SYSBUS_SOCKET (see message.h).
//
//   sysbus::Client bus;
//   if (!bus.connect()) ...bus.lastError()...
//   bus.restartUnit("sshd");
//   for (auto &u : bus.listUnits()) ...
//
// Unit names may be given with or without ".service". Queries (getUnit,
// listUnits, subscribe) are open to every user; changes need root.

#include "sysbus/message.h"

#include <functional>
#include <string>
#include <vector>

namespace sysbus {

// The syslite methods and signals.
namespace method {
constexpr const char *StartUnit = "StartUnit";
constexpr const char *StopUnit = "StopUnit";
constexpr const char *RestartUnit = "RestartUnit";
constexpr const char *EnableUnit = "EnableUnit";
constexpr const char *DisableUnit = "DisableUnit";
constexpr const char *GetUnit = "GetUnit";
constexpr const char *ListUnits = "ListUnits";
constexpr const char *Reload = "Reload";
constexpr const char *Subscribe = "Subscribe";
} // namespace method

namespace signal {
// fields: unit, state, pid, exit_code
constexpr const char *UnitStateChanged = "UnitStateChanged";
} // namespace signal

// Error names.
namespace error {
constexpr const char *UnknownMethod = "sysbus.Error.UnknownMethod";
constexpr const char *UnknownUnit = "sysbus.Error.UnknownUnit";
constexpr const char *AccessDenied = "sysbus.Error.AccessDenied";
constexpr const char *Failed = "sysbus.Error.Failed";
constexpr const char *InvalidArgs = "sysbus.Error.InvalidArgs";
} // namespace error

struct UnitInfo {
    std::string name;         // without ".service"
    std::string description;
    std::string state;        // stopped, starting, running, stopping, failed
    std::string enabled;      // enabled, disabled, static (no [Install] WantedBy=)
    std::string filePath;
    std::vector<std::string> wantedBy;
    long pid = -1;
    int exitCode = 0;
    int restarts = 0;
};

// "sshd.service" -> "sshd"
std::string unitName(const std::string &name);

class Client {
public:
    Client() = default;
    ~Client();
    Client(const Client &) = delete;
    Client &operator=(const Client &) = delete;

    bool connect(const std::string &path = SYSBUS_SOCKET);
    bool connected() const { return _fd >= 0; }
    void close();

    // Sends a method call and waits up to timeoutMs for its reply. Signals
    // arriving meanwhile are queued for waitSignal(). An Error reply is
    // returned as is (type == MessageType::Error); a transport failure
    // yields an Error message named sysbus.Error.Failed.
    Message call(const std::string &member,
                 const std::vector<std::pair<std::string, std::string>> &args = {},
                 int timeoutMs = 90000);

    // Unit control: true on success, otherwise see lastError().
    bool startUnit(const std::string &name);
    bool stopUnit(const std::string &name);
    bool restartUnit(const std::string &name);
    bool enableUnit(const std::string &name);
    bool disableUnit(const std::string &name);
    bool reload();
    bool getUnit(const std::string &name, UnitInfo &out);
    std::vector<UnitInfo> listUnits();

    // Signals: subscribe once, then waitSignal() returns the next one
    // (false on timeout or a closed connection; timeoutMs < 0 waits forever).
    bool subscribe();
    bool waitSignal(Message &out, int timeoutMs = -1);

    const std::string &lastError() const { return _lastError; }
    const std::string &lastErrorName() const { return _lastErrorName; }

private:
    bool readSome(int timeoutMs);
    bool simpleCall(const char *member, const std::string &unit);

    int _fd = -1;
    uint32_t _serial = 0;
    Decoder _decoder;
    std::vector<Message> _pendingSignals;
    std::string _lastError;
    std::string _lastErrorName;
};

// Reads a UnitInfo from the fields of a GetUnit reply / ListUnits entry.
UnitInfo unitFromFields(const std::vector<std::pair<std::string, std::string>> &fields);

} // namespace sysbus
+82 −0
Changes for include/sysbus/message.h: 82 added lines, 0 removed lines.
Original line number Diff line number Diff line
#pragma once

// sysbus -- syslite's system bus (in the spirit of D-Bus, much smaller).
//
// One Unix stream socket (SYSBUS_SOCKET), owned by syslite (PID 1). Clients
// send method calls and get a reply or an error back, matched by serial;
// clients that called Subscribe also get signals (e.g. UnitStateChanged).
//
// Wire format of one message (all integers little endian):
//   "SBUS"  u32 payload length  payload
//   payload: u8 type, u32 serial, u32 reply_serial, str member,
//            u32 field count, then per field: str key, str value
//   str: u32 length + bytes (binary safe, no terminator)
// Fields are an ordered list; a key may repeat (e.g. one "unit" per entry of
// a ListUnits reply).

#include <cstdint>
#include <string>
#include <utility>
#include <vector>

#define SYSBUS_SOCKET "/run/sysbus/system.sock"

namespace sysbus {

enum class MessageType : uint8_t {
    MethodCall = 1,
    MethodReturn = 2,
    Error = 3,
    Signal = 4,
};

// Largest payload either side accepts; anything bigger is a protocol error.
constexpr uint32_t MaxPayload = 1u << 20;

struct Message {
    MessageType type = MessageType::MethodCall;
    uint32_t serial = 0;
    uint32_t replySerial = 0;  // MethodReturn/Error: serial of the call answered
    std::string member;        // method or signal name; error name for Error
    std::vector<std::pair<std::string, std::string>> fields;

    Message() = default;
    Message(MessageType t, std::string m) : type(t), member(std::move(m)) {}

    Message &add(const std::string &key, const std::string &value) {
        fields.emplace_back(key, value);
        return *this;
    }
    Message &add(const std::string &key, long long value) { return add(key, std::to_string(value)); }

    // First value for key, or def.
    std::string get(const std::string &key, const std::string &def = "") const;
    // Every value for key, in order.
    std::vector<std::string> getAll(const std::string &key) const;
    bool has(const std::string &key) const;

    // Error replies carry their text in "message".
    static Message error(const Message &call, const std::string &name, const std::string &text);
    static Message reply(const Message &call);

    std::string encode() const;
};

// Incremental decoder for a byte stream: feed() what was read, then take
// complete messages with next(). A malformed stream sets failed().
class Decoder {
public:
    void feed(const char *data, size_t len) { _buf.append(data, len); }
    // true and a message if one is complete; false if more bytes are needed
    bool next(Message &out);
    bool failed() const { return _failed; }

private:
    std::string _buf;
    bool _failed = false;
};

// Writes all of a message to a blocking fd. false on error.
bool sendMessage(int fd, const Message &msg);

} // namespace sysbus

src/busserver.cpp

0 → 100644
+146 −0
Changes for src/busserver.cpp: 146 added lines, 0 removed lines.
Original line number Diff line number Diff line
#include "busserver.h"

#include <cerrno>
#include <cstring>
#include <iostream>
#include <vector>

#include <fcntl.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <unistd.h>

namespace syslite {

BusServer::~BusServer() { close(); }

bool BusServer::open(const std::string &path, Handler handler) {
    close();
    _handler = std::move(handler);

    // the socket's directory: world-searchable, so unprivileged clients can
    // query; changes are refused per call (SO_PEERCRED) instead
    std::string dir = path.substr(0, path.rfind('/'));
    if (!dir.empty()) {
        ::mkdir(dir.c_str(), 0755);
        ::chmod(dir.c_str(), 0755);
    }
    ::unlink(path.c_str());

    int fd = ::socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
    if (fd < 0) {
        std::cerr << "[sysbus] socket: " << std::strerror(errno) << std::endl;
        return false;
    }
    sockaddr_un addr{};
    addr.sun_family = AF_UNIX;
    if (path.size() >= sizeof(addr.sun_path)) {
        ::close(fd);
        return false;
    }
    std::memcpy(addr.sun_path, path.c_str(), path.size() + 1);
    if (::bind(fd, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) < 0 || ::listen(fd, 16) < 0) {
        std::cerr << "[sysbus] cannot listen on " << path << ": " << std::strerror(errno) << std::endl;
        ::close(fd);
        return false;
    }
    ::chmod(path.c_str(), 0666);
    _listenFd = fd;
    _path = path;
    std::cout << "[sysbus] Listening on " << path << std::endl;
    return true;
}

void BusServer::close() {
    for (auto &c : _clients) ::close(c.first);
    _clients.clear();
    if (_listenFd >= 0) {
        ::close(_listenFd);
        ::unlink(_path.c_str());
    }
    _listenFd = -1;
}

void BusServer::poll(int timeoutMs) {
    if (_listenFd < 0) {
        if (timeoutMs > 0) ::usleep(static_cast<useconds_t>(timeoutMs) * 1000);
        return;
    }
    std::vector<pollfd> fds;
    fds.push_back({_listenFd, POLLIN, 0});
    for (auto &c : _clients) fds.push_back({c.first, POLLIN, 0});

    int r = ::poll(fds.data(), fds.size(), timeoutMs);
    if (r <= 0) return;  // timeout, or EINTR from SIGCHLD & co.

    if (fds[0].revents & POLLIN) accept();
    for (size_t i = 1; i < fds.size(); ++i)
        if (fds[i].revents & (POLLIN | POLLHUP | POLLERR)) serve(fds[i].fd);
}

void BusServer::accept() {
    for (;;) {
        // non-blocking: a client that stops reading gets dropped (send
        // fails with EAGAIN) instead of stalling PID 1
        int fd = ::accept4(_listenFd, nullptr, nullptr, SOCK_NONBLOCK | SOCK_CLOEXEC);
        if (fd < 0) return;  // EAGAIN: all accepted
        if (_clients.size() >= MaxClients) {
            ::close(fd);
            continue;
        }
        Client c;
        ucred cred{};
        socklen_t len = sizeof(cred);
        if (::getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) == 0) {
            c.peer.pid = cred.pid;
            c.peer.uid = cred.uid;
        }
        _clients.emplace(fd, std::move(c));
    }
}

void BusServer::serve(int fd) {
    auto it = _clients.find(fd);
    if (it == _clients.end()) return;
    char buf[4096];
    ssize_t n = ::read(fd, buf, sizeof(buf));
    if (n <= 0) {
        if (n < 0 && (errno == EINTR || errno == EAGAIN)) return;
        drop(fd);
        return;
    }
    Client &c = it->second;
    c.decoder.feed(buf, static_cast<size_t>(n));

    sysbus::Message msg;
    while (c.decoder.next(msg)) {
        if (msg.type != sysbus::MessageType::MethodCall) continue;  // clients only call
        sysbus::Message reply = _handler(msg, c.peer);
        if (!sysbus::sendMessage(fd, reply)) {
            drop(fd);
            return;
        }
    }
    if (c.decoder.failed()) {
        std::cerr << "[sysbus] malformed message from pid " << c.peer.pid << ", disconnecting" << std::endl;
        drop(fd);
    }
}

void BusServer::drop(int fd) {
    ::close(fd);
    _clients.erase(fd);
}

void BusServer::broadcast(sysbus::Message signal) {
    signal.type = sysbus::MessageType::Signal;
    signal.serial = ++_signalSerial;
    std::vector<int> dead;
    for (auto &c : _clients)
        if (c.second.peer.subscribed && !sysbus::sendMessage(c.first, signal)) dead.push_back(c.first);
    for (int fd : dead) drop(fd);
}

} // namespace syslite

src/busserver.h

0 → 100644
+58 −0
Changes for src/busserver.h: 58 added lines, 0 removed lines.
Original line number Diff line number Diff line
#pragma once

// syslite's end of sysbus (see include/sysbus/message.h): accepts clients on
// SYSBUS_SOCKET, hands each complete method call to a handler and sends its
// reply back, and broadcasts signals to subscribed clients. Single-threaded:
// poll() is called from syslite's main loop.

#include "sysbus/message.h"

#include <functional>
#include <map>
#include <string>
#include <sys/types.h>

namespace syslite {

struct BusPeer {
    pid_t pid = -1;
    uid_t uid = static_cast<uid_t>(-1);
    bool subscribed = false;
};

class BusServer {
public:
    // Returns the reply (MethodReturn or Error) for a method call.
    using Handler = std::function<sysbus::Message(const sysbus::Message &call, BusPeer &peer)>;

    ~BusServer();

    bool open(const std::string &path, Handler handler);
    void close();

    // Waits up to timeoutMs for activity, then serves what arrived.
    void poll(int timeoutMs);

    // Sends a signal to every subscribed client.
    void broadcast(sysbus::Message signal);

private:
    struct Client {
        BusPeer peer;
        sysbus::Decoder decoder;
    };

    static constexpr size_t MaxClients = 64;

    void accept();
    void serve(int fd);
    void drop(int fd);

    int _listenFd = -1;
    std::string _path;
    Handler _handler;
    std::map<int, Client> _clients;
    uint32_t _signalSerial = 0;
};

} // namespace syslite
Loading