Commit cd0d6853 authored by jan.koester's avatar jan.koester
Browse files

Initial commit: syslite lightweight init system

- Systemd-compatible .service unit file parser
- Service manager (simple/forking/oneshot, restart policies, dependencies)
- TTY manager with getty respawn (toybox getty/login)
- PID 1 main loop: VFS mount, hostname, sysctl, SIGCHLD reaping
- Config via libconfplus (YAML), CLI via libcmdplus
parents
Loading
Loading
Loading
Loading

.gitignore

0 → 100644
+3 −0
Changes for .gitignore: 3 added lines, 0 removed lines.
Original line number Diff line number Diff line
build/
*.o
*.d

CMakeLists.txt

0 → 100644
+30 −0
Changes for CMakeLists.txt: 30 added lines, 0 removed lines.
Original line number Diff line number Diff line
cmake_minimum_required(VERSION 3.18)

project(syslite VERSION 0.1.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(libconfplus REQUIRED)
find_package(libcmdplus REQUIRED)

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

target_include_directories(syslite PRIVATE
    ${CMAKE_CURRENT_SOURCE_DIR}/src
)

target_link_libraries(syslite PRIVATE
    confplus::confplus
    cmdplus::cmdplus
    dl
)

install(TARGETS syslite DESTINATION sbin)
install(FILES data/syslite.yaml DESTINATION etc/syslite)

data/syslite.yaml

0 → 100644
+8 −0
Changes for data/syslite.yaml: 8 added lines, 0 removed lines.
Original line number Diff line number Diff line
SYSLITE:
  HOSTNAME: ""
  UNIT_DIR: "/etc/syslite/services"
  DEFAULT_TARGET: "multi-user.target"

  TTY:
    COUNT: 3
    COMMAND: ""

src/main.cpp

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

#include <cmdplus/cmdplus.h>

#include <iostream>
#include <csignal>

static syslite::Init *g_init = nullptr;

static void sigTermHandler(int) {
    if (g_init) g_init->shutdown();
}

static void sigIntHandler(int) {
    if (g_init) g_init->shutdown();
}

// Ctrl+Alt+Del sends SIGINT to PID 1 on Linux
static void sigRebootHandler(int) {
    if (g_init) g_init->reboot();
}

int main(int argc, char **argv) {
    cmdplus::CmdController &cmd = cmdplus::CmdController::getInstance();
    cmd.registerCmd("help", 'h', false, "", "Show help");
    cmd.registerCmd("config", 'c', true, "", "Config file path (e.g. yaml:/etc/syslite/syslite.yaml)");

    cmd.parseCmd(argc, argv);

    if (cmd.getCmdbyKey("help") && cmd.getCmdbyKey("help")->getFound()) {
        std::cout << "syslite — lightweight Linux init system" << std::endl;
        std::cout << std::endl;
        cmd.printHelp();
        return 0;
    }

    if (!cmd.checkRequired()) {
        // If running as PID 1, use default config path
        if (getpid() != 1) {
            std::cerr << "Usage: syslite -c yaml:/etc/syslite/syslite.yaml" << std::endl;
            return 1;
        }
    }

    syslite::Init init;
    g_init = &init;

    // Set up signal handlers
    struct sigaction sa{};
    sa.sa_handler = sigTermHandler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;
    sigaction(SIGTERM, &sa, nullptr);

    sa.sa_handler = sigIntHandler;
    sigaction(SIGINT, &sa, nullptr);

    // SIGUSR1 for reboot (alternative to Ctrl+Alt+Del)
    sa.sa_handler = sigRebootHandler;
    sigaction(SIGUSR1, &sa, nullptr);

    // Ignore SIGPIPE
    signal(SIGPIPE, SIG_IGN);

    // Load config
    std::string configPath;
    auto *configCmd = cmd.getCmdbyKey("config");
    if (configCmd && configCmd->getFound()) {
        configPath = configCmd->getValue();
    } else {
        configPath = "yaml:/etc/syslite/syslite.yaml";
    }

    init.loadConfig(configPath);

    // Run the init main loop
    return init.run();
}

src/service.cpp

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

#include <iostream>
#include <fstream>
#include <filesystem>
#include <algorithm>
#include <cstring>
#include <csignal>

#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <pwd.h>
#include <grp.h>
#include <fcntl.h>

namespace syslite {

    ServiceManager::ServiceManager() {}
    ServiceManager::~ServiceManager() {}

    int ServiceManager::loadDirectory(const std::string &path) {
        int count = 0;
        if (!std::filesystem::exists(path))
            return 0;

        for (auto &entry : std::filesystem::directory_iterator(path)) {
            if (entry.is_regular_file() || entry.is_symlink()) {
                std::string fname = entry.path().filename().string();
                if (fname.size() > 8 && fname.substr(fname.size() - 8) == ".service") {
                    if (loadUnit(entry.path().string()))
                        ++count;
                }
            }
        }

        std::cout << "[syslite] Loaded " << count << " service(s) from " << path << std::endl;
        return count;
    }

    bool ServiceManager::loadUnit(const std::string &path) {
        UnitFile unit;
        if (!parseUnitFile(path, unit)) {
            std::cerr << "[syslite] Failed to parse: " << path << std::endl;
            return false;
        }

        ServiceEntry entry;
        entry.unit = std::move(unit);
        _services[entry.unit.name] = std::move(entry);
        return true;
    }

    // Resolve dependencies and determine start order for a target
    void ServiceManager::startTarget(const std::string &target) {
        // Collect services that are WantedBy this target
        std::vector<std::string> toStart;
        for (auto &[name, entry] : _services) {
            for (auto &wb : entry.unit.wantedBy) {
                if (wb == target) {
                    toStart.push_back(name);
                    break;
                }
            }
        }

        // Simple dependency-based ordering: start services whose After
        // dependencies have already been started first
        std::vector<std::string> started;
        std::vector<std::string> pending = toStart;
        int maxPasses = static_cast<int>(pending.size()) + 1;

        while (!pending.empty() && maxPasses-- > 0) {
            std::vector<std::string> nextPending;
            for (auto &name : pending) {
                auto *entry = findService(name);
                if (!entry) continue;

                // Check if all After deps are either started or not in our set
                bool depsReady = true;
                for (auto &dep : entry->unit.after) {
                    // Strip .service suffix if present
                    std::string depName = dep;
                    if (depName.size() > 8 && depName.substr(depName.size() - 8) == ".service")
                        depName = depName.substr(0, depName.size() - 8);

                    // If this dep is in our start set and not yet started, wait
                    if (std::find(toStart.begin(), toStart.end(), depName) != toStart.end()) {
                        if (std::find(started.begin(), started.end(), depName) == started.end()) {
                            depsReady = false;
                            break;
                        }
                    }
                }

                if (depsReady) {
                    startService(name);
                    started.push_back(name);
                } else {
                    nextPending.push_back(name);
                }
            }
            pending = nextPending;
        }

        // Start any remaining that couldn't resolve deps
        for (auto &name : pending) {
            std::cerr << "[syslite] Starting " << name << " (unresolved deps)" << std::endl;
            startService(name);
        }

        _startOrder = started;
        for (auto &name : pending)
            _startOrder.push_back(name);
    }

    std::vector<std::string> ServiceManager::buildArgv(const std::string &cmdline) {
        std::vector<std::string> args;
        std::string current;
        bool inQuote = false;
        char quoteChar = 0;

        for (size_t i = 0; i < cmdline.size(); ++i) {
            char c = cmdline[i];

            if (inQuote) {
                if (c == quoteChar) {
                    inQuote = false;
                } else {
                    current += c;
                }
            } else if (c == '"' || c == '\'') {
                inQuote = true;
                quoteChar = c;
            } else if (c == ' ' || c == '\t') {
                if (!current.empty()) {
                    args.push_back(current);
                    current.clear();
                }
            } else {
                current += c;
            }
        }
        if (!current.empty())
            args.push_back(current);

        return args;
    }

    void ServiceManager::resolveEnvironment(std::string &cmd, const ServiceEntry &entry) {
        // Replace $MAINPID with actual PID
        std::string pidStr = std::to_string(entry.pid);
        size_t pos;
        while ((pos = cmd.find("$MAINPID")) != std::string::npos)
            cmd.replace(pos, 8, pidStr);
    }

    void ServiceManager::createRuntimeDirectory(const ServiceEntry &entry) {
        if (entry.unit.runtimeDirectory.empty())
            return;

        std::string dir = "/run/" + entry.unit.runtimeDirectory;
        std::filesystem::create_directories(dir);

        if (!entry.unit.user.empty()) {
            struct passwd *pw = getpwnam(entry.unit.user.c_str());
            struct group *gr = entry.unit.group.empty()
                ? nullptr : getgrnam(entry.unit.group.c_str());
            if (pw) {
                chown(dir.c_str(), pw->pw_uid, gr ? gr->gr_gid : pw->pw_gid);
            }
        }
    }

    void ServiceManager::setupChildProcess(const ServiceEntry &entry) {
        // Set working directory
        if (!entry.unit.workingDirectory.empty()) {
            if (chdir(entry.unit.workingDirectory.c_str()) != 0) {
                std::cerr << "[syslite] chdir failed: " << entry.unit.workingDirectory << std::endl;
            }
        }

        // Load environment file
        if (!entry.unit.environmentFile.empty()) {
            std::string envPath = entry.unit.environmentFile;
            // Strip leading '-' (optional file)
            if (!envPath.empty() && envPath[0] == '-')
                envPath = envPath.substr(1);

            std::ifstream envFile(envPath);
            if (envFile.is_open()) {
                std::string line;
                while (std::getline(envFile, line)) {
                    if (line.empty() || line[0] == '#') continue;
                    putenv(strdup(line.c_str()));
                }
            }
        }

        // Set individual environment variables
        for (auto &[key, val] : entry.unit.environment) {
            setenv(key.c_str(), val.c_str(), 1);
        }

        // Drop privileges
        if (!entry.unit.group.empty()) {
            struct group *gr = getgrnam(entry.unit.group.c_str());
            if (gr) setgid(gr->gr_gid);
        }
        if (!entry.unit.user.empty()) {
            struct passwd *pw = getpwnam(entry.unit.user.c_str());
            if (pw) {
                initgroups(entry.unit.user.c_str(), pw->pw_gid);
                setuid(pw->pw_uid);
            }
        }

        // Reset signals
        signal(SIGCHLD, SIG_DFL);
        signal(SIGTERM, SIG_DFL);
        signal(SIGINT, SIG_DFL);

        // New session for the service
        setsid();
    }

    bool ServiceManager::execService(ServiceEntry &entry) {
        if (entry.unit.execStart.empty()) {
            std::cerr << "[syslite] No ExecStart for " << entry.unit.name << std::endl;
            entry.state = ServiceState::Failed;
            return false;
        }

        createRuntimeDirectory(entry);

        // Run ExecStartPre if set
        if (!entry.unit.execStartPre.empty()) {
            std::string preCmd = entry.unit.execStartPre;
            // Strip leading '-' (ignore failures)
            bool ignoreFailure = false;
            if (!preCmd.empty() && preCmd[0] == '-') {
                ignoreFailure = true;
                preCmd = preCmd.substr(1);
            }

            pid_t prePid = fork();
            if (prePid == 0) {
                setupChildProcess(entry);
                auto args = buildArgv(preCmd);
                std::vector<char*> argv;
                for (auto &a : args)
                    argv.push_back(const_cast<char*>(a.c_str()));
                argv.push_back(nullptr);
                execvp(argv[0], argv.data());
                _exit(127);
            } else if (prePid > 0) {
                int preStatus;
                waitpid(prePid, &preStatus, 0);
                if (WIFEXITED(preStatus) && WEXITSTATUS(preStatus) != 0 && !ignoreFailure) {
                    std::cerr << "[syslite] ExecStartPre failed for " << entry.unit.name << std::endl;
                    entry.state = ServiceState::Failed;
                    return false;
                }
            }
        }

        entry.state = ServiceState::Starting;

        pid_t pid = fork();
        if (pid == 0) {
            // Child
            setupChildProcess(entry);

            auto args = buildArgv(entry.unit.execStart);
            std::vector<char*> argv;
            for (auto &a : args)
                argv.push_back(const_cast<char*>(a.c_str()));
            argv.push_back(nullptr);

            execvp(argv[0], argv.data());
            std::cerr << "[syslite] exec failed: " << entry.unit.execStart
                      << ": " << strerror(errno) << std::endl;
            _exit(127);
        }

        if (pid < 0) {
            std::cerr << "[syslite] fork failed for " << entry.unit.name << std::endl;
            entry.state = ServiceState::Failed;
            return false;
        }

        entry.pid = pid;

        if (entry.unit.type == ServiceType::Forking) {
            // Wait for the forking parent to exit, then read PID file
            int status;
            waitpid(pid, &status, 0);

            if (!entry.unit.pidFile.empty()) {
                std::ifstream pf(entry.unit.pidFile);
                if (pf.is_open()) {
                    pid_t realPid;
                    pf >> realPid;
                    if (realPid > 0)
                        entry.pid = realPid;
                }
            } else {
                // No PID file — we can't reliably track the daemon
                entry.pid = -1;
            }
            entry.state = ServiceState::Running;
        }
        else if (entry.unit.type == ServiceType::Oneshot) {
            // Wait for completion
            int status;
            waitpid(pid, &status, 0);
            entry.exitCode = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
            entry.pid = -1;
            entry.state = (entry.exitCode == 0) ? ServiceState::Stopped : ServiceState::Failed;
        }
        else {
            // Simple — process stays running
            entry.state = ServiceState::Running;
        }

        std::cout << "[syslite] Started " << entry.unit.name
                  << " (pid=" << entry.pid << ")" << std::endl;
        return true;
    }

    bool ServiceManager::startService(const std::string &name) {
        auto it = _services.find(name);
        if (it == _services.end()) {
            std::cerr << "[syslite] Unknown service: " << name << std::endl;
            return false;
        }

        auto &entry = it->second;
        if (entry.state == ServiceState::Running) {
            return true;
        }

        // Start required dependencies first
        for (auto &req : entry.unit.requires) {
            std::string depName = req;
            if (depName.size() > 8 && depName.substr(depName.size() - 8) == ".service")
                depName = depName.substr(0, depName.size() - 8);
            startService(depName);
        }

        std::cout << "[syslite] Starting " << name << "..." << std::endl;
        return execService(entry);
    }

    bool ServiceManager::stopService(const std::string &name) {
        auto it = _services.find(name);
        if (it == _services.end())
            return false;

        auto &entry = it->second;
        if (entry.state != ServiceState::Running)
            return true;

        entry.state = ServiceState::Stopping;
        std::cout << "[syslite] Stopping " << name << "..." << std::endl;

        // ExecStop if defined
        if (!entry.unit.execStop.empty() && entry.pid > 0) {
            std::string stopCmd = entry.unit.execStop;
            resolveEnvironment(stopCmd, entry);

            pid_t stopPid = fork();
            if (stopPid == 0) {
                auto args = buildArgv(stopCmd);
                std::vector<char*> argv;
                for (auto &a : args)
                    argv.push_back(const_cast<char*>(a.c_str()));
                argv.push_back(nullptr);
                execvp(argv[0], argv.data());
                _exit(127);
            } else if (stopPid > 0) {
                int status;
                waitpid(stopPid, &status, 0);
            }
        } else if (entry.pid > 0) {
            // Default: send SIGTERM
            kill(entry.pid, SIGTERM);
        }

        // Wait for process to exit with timeout
        if (entry.pid > 0) {
            int timeoutMs = entry.unit.timeoutStopSec * 1000;
            int waited = 0;
            int step = 100; // 100ms

            while (waited < timeoutMs) {
                int status;
                pid_t ret = waitpid(entry.pid, &status, WNOHANG);
                if (ret > 0) {
                    entry.pid = -1;
                    entry.state = ServiceState::Stopped;
                    std::cout << "[syslite] Stopped " << name << std::endl;
                    return true;
                }
                if (ret < 0) break; // already gone
                usleep(step * 1000);
                waited += step;
            }

            // Timeout: SIGKILL
            if (entry.pid > 0) {
                std::cerr << "[syslite] " << name << " did not stop, sending SIGKILL" << std::endl;
                kill(entry.pid, SIGKILL);
                waitpid(entry.pid, nullptr, 0);
            }
        }

        entry.pid = -1;
        entry.state = ServiceState::Stopped;
        return true;
    }

    void ServiceManager::stopAll() {
        // Stop in reverse start order
        std::vector<std::string> order = _startOrder;
        std::reverse(order.begin(), order.end());

        // Also stop anything not in the start order
        for (auto &[name, entry] : _services) {
            if (entry.state == ServiceState::Running) {
                if (std::find(order.begin(), order.end(), name) == order.end())
                    order.push_back(name);
            }
        }

        for (auto &name : order) {
            stopService(name);
        }
    }

    void ServiceManager::handleChild(pid_t pid, int status) {
        for (auto &[name, entry] : _services) {
            if (entry.pid == pid) {
                entry.exitCode = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
                entry.pid = -1;

                if (entry.state == ServiceState::Stopping) {
                    entry.state = ServiceState::Stopped;
                } else if (entry.state == ServiceState::Running) {
                    if (entry.exitCode == 0) {
                        entry.state = ServiceState::Stopped;
                        std::cout << "[syslite] " << name << " exited normally" << std::endl;
                    } else {
                        entry.state = ServiceState::Failed;
                        std::cerr << "[syslite] " << name << " exited with code "
                                  << entry.exitCode << std::endl;
                    }
                }
                return;
            }
        }
    }

    void ServiceManager::checkRestarts() {
        for (auto &[name, entry] : _services) {
            bool shouldRestart = false;

            if (entry.unit.restart == RestartPolicy::Always &&
                (entry.state == ServiceState::Stopped || entry.state == ServiceState::Failed)) {
                shouldRestart = true;
            }
            else if (entry.unit.restart == RestartPolicy::OnFailure &&
                     entry.state == ServiceState::Failed) {
                shouldRestart = true;
            }

            if (shouldRestart && entry.pid <= 0) {
                ++entry.restartCount;
                std::cout << "[syslite] Restarting " << name
                          << " (attempt " << entry.restartCount << ")" << std::endl;
                execService(entry);
            }
        }
    }

    ServiceEntry *ServiceManager::findService(const std::string &name) {
        auto it = _services.find(name);
        if (it == _services.end()) return nullptr;
        return &it->second;
    }

}
Loading