add stuff

This commit is contained in:
Hedy88 2026-08-27 19:08:08 +01:00
commit 1cd4394841
No known key found for this signature in database
12 changed files with 1702 additions and 18 deletions

19
.gitignore vendored
View file

@ -1,51 +1,38 @@
# ---> C++
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Linker files
*.ilk
# Debugger Files
*.pdb
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
*.so.*
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app
# Build directories
build/
Build/
build-*/
# CMake generated files
CMakeFiles/
CMakeCache.txt
cmake_install.cmake
@ -53,18 +40,16 @@ Makefile
install_manifest.txt
compile_commands.json
# Temporary files
__pycache__
*.tmp
*.log
*.bak
*.swp
# vcpkg
vcpkg_installed/
# debug information files
*.dwo
# test output & cache
Testing/
.cache/

View file

@ -1,3 +1,96 @@
# bajia
an init system
an init system (PID 1) for embedded / VM targets, written in C++20 and
configured with a small declarative language inspired by Android's init `.rc`
format.
## Status
- `.rc` config parser (services + on-trigger action blocks)
- Supervisor event loop built on `signalfd` + `epoll`
- service spawn / reap / respawn (per-service restart policy)
- action commands: `start`, `stop`, `restart`, `exec`, `mkdir`, `chmod`,
`chown`, `setenv`, `write`, `symlink`, `mount`, `log`
- logger with a ring buffer that flushes to the console once available
roadmap:
- user/group privilege drop (`user`, `group`, supplementary groups)
- dependency ordering between services
- `SIGCHLD` crash-window limiting (rate-limited restarts)
- property triggers (`property:<k>=<v>`) and `setprop`/`getprop`
- per-service logging to files
- `reboot`/`poweroff` path with ordered unmount
- `SIGHUP` config reload
- SELinux
## building
requires a C++20 compiler and [Ninja](https://ninja-build.org/)
```sh
python3 configure.py # generates build/build.ninja
ninja -C build # produces build/bajia
```
`configure.py` also writes a thin `Makefile` convenience wrapper
(`make`, `make clean`, `make format`, `--asan`, `--debug`).
## running
As a real init, the kernel must launch it as PID 1:
```
init=/path/to/bajia
```
Or by hand against a config (useful for development, may not behave like a
real boot). bajia normally refuses to start unless it is PID 1; pass
`--run-as-user` to override:
```sh
./build/bajia --run-as-user etc/init.rc
```
If no files are given it looks for `/etc/bajia/init.rc`.
## configuration language
See [`etc/init.rc`](etc/init.rc) for a complete example.
### services
```rc
service NAME /path/to/exe [args...]
user root|other # privilege level (drop, planned)
group GROUP [GROUP...]
oneshot # run once and exit, never respawn
disabled # not started by the boot sequence
console # bind stdio to /dev/console
class NAME # grouping (default "default")
respawn never|on-failure|always # restart policy (default always)
crash-threshold N # restarts allowed per window
crash-window SECS
setenv K=V # extra environment (repeatable)
cwd /path
```
### actions
```rc
on TRIGGER
start NAME | stop NAME | restart NAME
exec /cmd args...
mkdir PATH [mode]
chmod PATH mode
chown PATH uid gid
setenv K V
write PATH CONTENT
symlink TARGET LINK
mount SOURCE TARGET FSTYPE
log message
```
boot triggers fire in order: `early-init`, `init`, `boot`. `shutdown` is
reserved (planned wiring to the signal path). property/`service-*` triggers
are on the roadmap.

132
configure.py Normal file
View file

@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""generates a Ninja build file
usage:
python3 configure.py
python3 configure.py --asan # enable address/undefined sanitizers
python3 configure.py --debug # -O0 instead of -O2
"""
import argparse
import os
import shutil
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent
SRC = ROOT / "src"
BUILD = ROOT / "build"
CXX = os.environ.get("CXX", "g++")
CXXFLAGS = ["-std=c++20", "-O2", "-g"]
WARNINGS = ["-Wall", "-Wextra", "-Wpedantic", "-Wshadow", "-Wconversion", "-Wnull-dereference"]
linkflags = []
def discover_sources():
return sorted(
p.name for p in SRC.glob("*.cpp")
if not p.name.endswith("_main.cpp") and not p.name.startswith(".")
)
def write_makefile_convenience():
make = ROOT / "Makefile"
make.write_text(
"""# convenience wrapper around configure.py + ninja.
#
# make -> configure + build
# make configure -> regenerate build.ninja
# make clean -> remove build dir
# make format -> clang-format all sources (if available)
#
.PHONY: all configure clean format
all: configure
\t@ninja -C build
configure:
\t@python3 configure.py
clean:
\t@rm -rf build
format:
\t@command -v clang-format >/dev/null && clang-format -i src/*.cpp src/*.hpp || true
""",
encoding="utf-8",
)
def emit_ninja(cxx, cxxflags, dst):
sources = discover_sources()
objs = ["obj/" + Path(s).stem + ".o" for s in sources]
target = "bajia"
rule_cxx = (
"rule cxx\n"
" command = {cxx} {cxxflags} -MMD -MF $out.d -c $in -o $out\n"
" depfile = $out.d\n"
" deps = gcc\n"
" description = CXX $out\n"
).format(cxx=cxx, cxxflags=" ".join(cxxflags))
rule_link = (
"rule link\n"
" command = {cxx} {ldflags} $in -o $out -lpthread\n"
" description = LINK $out\n"
).format(cxx=cxx, ldflags=" ".join(linkflags))
lines = ["# generated by configure.py -- do not edit", "ninja_required_version = 1.8", ""]
lines.append(rule_cxx)
lines.append(rule_link)
lines.append('build {target}: link {objs}'.format(target=target, objs=" ".join(objs)))
lines.append("")
for o, s in zip(objs, sources):
lines.append('build {o}: cxx {src}/{s}'.format(o=o, src=SRC, s=s))
lines.append("")
lines.append('build all: phony {target}'.format(target=target))
lines.append("default all")
lines.append("")
(BUILD / "obj").mkdir(exist_ok=True)
dst.write_text("\n".join(lines) + "\n", encoding="utf-8")
def parse_args():
p = argparse.ArgumentParser(description="Configure bajia build (generates build.ninja)")
p.add_argument("--asan", action="store_true", help="enable address + UB sanitizers")
p.add_argument("--debug", action="store_true", help="disable -O2, enable -O0")
p.add_argument("--clean", action="store_true", help="remove build dir")
return p.parse_args()
def main():
args = parse_args()
if args.clean:
shutil.rmtree(BUILD, ignore_errors=True)
print("removed build/")
return 0
if not shutil.which("ninja"):
print("error: ninja not found in PATH", file=sys.stderr)
return 1
BUILD.mkdir(exist_ok=True)
cxxflags = list(CXXFLAGS)
global linkflags
linkflags = []
if args.debug:
cxxflags = [f for f in cxxflags if f != "-O2"]
cxxflags += ["-O0"]
if args.asan:
cxxflags += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"]
linkflags += ["-fsanitize=address,undefined"]
cxxflags = [f for f in cxxflags if f not in WARNINGS]
cxxflags = WARNINGS + cxxflags
emit_ninja(CXX, cxxflags, BUILD / "build.ninja")
write_makefile_convenience()
print(f"configured {len(discover_sources())} sources -> build/build.ninja")
print("run: ninja -C build")
return 0
if __name__ == "__main__":
sys.exit(main())

54
etc/init.rc Normal file
View file

@ -0,0 +1,54 @@
# bajia init.rc - example configuration for an embedded/VM system.
#
# constructs:
# service NAME /path/to/exe [args...]
# user|group|oneshot|disabled|console|class|respawn|crash-*|setenv|cwd
#
# on TRIGGER
# start NAME | stop NAME | restart NAME
# exec /cmd args... (run synchronously, wait to finish)
# mkdir PATH [mode]
# chmod PATH mode
# chown PATH uid gid
# setenv K V
# write PATH CONTENT
# symlink TARGET LINK
# mount SOURCE TARGET FSTYPE
# log message...
#
# standard trigger sequence run at boot: early-init, init, boot.
# future: property:<key>=<value>, service-started:<name>.
on early-init
mount proc /proc proc
mount sysfs /sys sysfs
mount devtmpfs /dev devtmpfs
mkdir /dev/pts 0755
mount devpts /dev/pts devpts
mkdir /run 0755
mount tmpfs /run tmpfs
on init
exec /sbin/modprobe virtio_rng
write /proc/sys/kernel/hostname bajia
log **** bajia init on-line ****
on boot
start console
start watchdog
# the console service: bind stdio to /dev/console, always respawn.
service console /sbin/getty -L ttyS0 115200 vt100
class core
console
user root
# a long-lived example daemon. respawning is the default (always).
service watchdog /usr/sbin/watchdog
class core
respawn always
# a one-shot job: runs once, exits, never respawns.
service boot-logo /usr/bin/show-boot-logo
class late
oneshot

188
src/config.cpp Normal file
View file

@ -0,0 +1,188 @@
// config.cpp - parser for the bajia .rc language.
#include "config.hpp"
#include <fstream>
namespace bajia {
namespace {
// tokenize a line honoring double-quoted strings as single tokens.
// both '#' (unquoted) and ';' begin a comment to end of line
std::vector<std::string> tokenize(const std::string& line) {
std::vector<std::string> out;
std::string cur;
bool in_q = false;
bool need_quote_close = false;
for (size_t i = 0; i < line.size(); ++i) {
char c = line[i];
if (in_q) {
if (c == '"') {
in_q = false;
need_quote_close = false;
} else if (c == '\\' && i + 1 < line.size()) {
cur += line[++i];
} else {
cur += c;
}
continue;
}
if (c == '"') {
in_q = true;
need_quote_close = true;
continue;
}
if (c == '#' || c == ';') {
break; // comment to end of line
}
if (std::isspace(static_cast<unsigned char>(c))) {
if (!cur.empty()) {
out.push_back(cur);
cur.clear();
}
continue;
}
cur += c;
}
if (in_q) {
// unterminated quote: best effort, keep what we have.
if (!cur.empty()) out.push_back(cur);
} else if (!cur.empty()) {
out.push_back(cur);
}
(void)need_quote_close;
return out;
}
RespawnPolicy parse_respawn(const std::string& s) {
if (s == "always") return RespawnPolicy::Always;
if (s == "on-failure") return RespawnPolicy::OnFailure;
return RespawnPolicy::Never;
}
} // namespace
Service* Config::find_service(const std::string& name) {
for (auto& s : services) {
if (s.name == name) return &s;
}
return nullptr;
}
const Service* Config::find_service(const std::string& name) const {
for (auto& s : services) {
if (s.name == name) return &s;
}
return nullptr;
}
// public entry point
Config parse_config(const std::vector<std::string>& files) {
Config cfg;
int line = 0;
std::string section_kind; // "service" or "action"
Service* cur_svc = nullptr; // service being configured
Action* cur_act = nullptr; // action being configured
for (const auto& file : files) {
std::ifstream in(file);
if (!in) {
throw std::runtime_error("cannot open config file: " + file);
}
std::string raw;
line = 0;
section_kind.clear();
cur_svc = nullptr;
cur_act = nullptr;
while (std::getline(in, raw)) {
++line;
auto toks = tokenize(raw);
if (toks.empty()) continue;
std::string first = toks[0];
size_t indent = raw.find_first_not_of(" \t");
if (first == "service" && indent == 0) {
if (toks.size() < 3) {
throw std::runtime_error(file + ":" + std::to_string(line) +
": 'service' requires name + executable");
}
Service svc;
svc.name = toks[1];
svc.args.assign(toks.begin() + 2, toks.end());
cfg.services.push_back(std::move(svc));
cur_svc = &cfg.services.back();
cur_act = nullptr;
section_kind = "service";
continue;
}
if (first == "on") {
if (toks.size() < 2) {
throw std::runtime_error(file + ":" + std::to_string(line) +
": 'on' requires a trigger");
}
cfg.actions.push_back(Action{toks[1], {}});
cur_act = &cfg.actions.back();
cur_svc = nullptr;
section_kind = "action";
continue;
}
// service options (must already be inside a service section).
if (section_kind == "service" && cur_svc) {
if (first == "user" && toks.size() >= 2) cur_svc->uid = toks[1];
else if (first == "group" && toks.size() >= 2) {
cur_svc->gid = toks[1];
for (size_t i = 2; i < toks.size(); ++i) cur_svc->groups.push_back(toks[i]);
}
else if (first == "oneshot") cur_svc->oneshot = true;
else if (first == "disabled") cur_svc->disabled = true;
else if (first == "console") cur_svc->console = true;
else if (first == "class" && toks.size() >= 2) cur_svc->service_class = toks[1];
else if (first == "respawn" && toks.size() >= 2)
cur_svc->respawn = parse_respawn(toks[1]);
else if (first == "crash-threshold" && toks.size() >= 2)
cur_svc->crash_threshold = std::stoi(toks[1]);
else if (first == "crash-window" && toks.size() >= 2)
cur_svc->crash_window_secs = std::stoi(toks[1]);
else if (first == "setenv" && toks.size() >= 2) cur_svc->env.push_back(toks[1]);
else if (first == "cwd" && toks.size() >= 2) cur_svc->cwd = toks[1];
continue;
}
// action command (must be inside an action section).
if (section_kind == "action" && cur_act) {
Command cmd;
if (first == "start" && toks.size() >= 2) cmd.kind = Command::Kind::Start;
else if (first == "stop" && toks.size() >= 2) cmd.kind = Command::Kind::Stop;
else if (first == "restart" && toks.size() >= 2) cmd.kind = Command::Kind::Restart;
else if (first == "exec") cmd.kind = Command::Kind::Exec;
else if (first == "mkdir") cmd.kind = Command::Kind::Mkdir;
else if (first == "chmod") cmd.kind = Command::Kind::Chmod;
else if (first == "chown") cmd.kind = Command::Kind::Chown;
else if (first == "setenv") cmd.kind = Command::Kind::Setenv;
else if (first == "write") cmd.kind = Command::Kind::Write;
else if (first == "symlink") cmd.kind = Command::Kind::Symlink;
else if (first == "mount") cmd.kind = Command::Kind::Mount;
else if (first == "log") cmd.kind = Command::Kind::Log;
else {
throw std::runtime_error(file + ":" + std::to_string(line) +
": unknown action command '" + first + "'");
}
cmd.args.assign(toks.begin() + 1, toks.end());
cur_act->commands.push_back(std::move(cmd));
continue;
}
throw std::runtime_error(file + ":" + std::to_string(line) +
": unexpected directive '" + first + "'");
}
}
return cfg;
}
} // namespace bajia

107
src/config.hpp Normal file
View file

@ -0,0 +1,107 @@
// config.hpp - data model for the bajia .rc language.
//
// The language is a small, embedded-oriented dialect inspired by Android init.
// Two top-level constructs:
//
// service NAME /path/to/exec args...
// user root
// group root
// oneshot
// disabled
// class main
// respawn never|on-failure|always
// console
//
// on TRIGGER
// start NAME
// exec /path/to/cmd args...
// mkdir /path mode
// setenv K V
//
// TRIGGER events: early-init, init, boot, shutdown, property:<name>=<value>,
// service-started:<name>, service-stopped:<name>.
#pragma once
#include <cstdint>
#include <map>
#include <optional>
#include <string>
#include <vector>
namespace bajia {
// --------------------------------------------------------------------------
// Service
// --------------------------------------------------------------------------
enum class RespawnPolicy {
Never,
OnFailure,
Always,
};
struct Service {
std::string name;
std::vector<std::string> args; // executable path + arguments
std::string cwd = "/";
std::string uid = "root"; // resolved in supervisor
std::string gid = "root";
std::vector<std::string> groups;
bool oneshot = false; // run once, don't keep alive
bool disabled = false; // not started automatically
bool console = false; // bind stdio to the console
std::string service_class = "default";
RespawnPolicy respawn = RespawnPolicy::Always;
int crash_threshold = 4; // max restarts within window
int crash_window_secs = 30; // before giving up
std::vector<std::string> env; // "K=V" pairs
// Runtime state
int pid = 0;
int exit_code = 0;
bool running = false;
};
// --------------------------------------------------------------------------
// Action: a list of commands to run when a trigger fires.
// --------------------------------------------------------------------------
struct Command {
enum class Kind {
Start,
Stop,
Restart,
Exec, // run a synchronous command to completion
Mkdir,
Chmod,
Chown,
Setenv,
Write,
Symlink,
Mount,
Log,
};
Kind kind;
std::vector<std::string> args; // command-specific arguments
};
struct Action {
std::string trigger; // e.g. "boot", "early-init"
std::vector<Command> commands;
};
// --------------------------------------------------------------------------
// Config: everything parsed from all loaded .rc files.
// --------------------------------------------------------------------------
struct Config {
std::vector<Service> services;
std::vector<Action> actions;
std::string hostname;
Service* find_service(const std::string& name);
const Service* find_service(const std::string& name) const;
};
// Parse a set of .rc files into a Config. Throws std::runtime_error on
// malformed input (reported with file:line context).
Config parse_config(const std::vector<std::string>& files);
} // namespace bajia

92
src/logger.cpp Normal file
View file

@ -0,0 +1,92 @@
// logger.cpp - implementation of the pid-1 logger.
#include "logger.hpp"
#include <algorithm>
#include <array>
#include <ctime>
#include <fcntl.h>
#include <mutex>
#include <unistd.h>
namespace bajia {
namespace {
std::mutex g_lock;
int g_fd = -1; // console fd (>=0 when open)
std::array<std::string, 64> g_ring;
size_t g_ring_pos = 0;
size_t g_ring_count = 0;
LogLevel g_min_level = LogLevel::Info;
const char* level_name(LogLevel l) {
switch (l) {
case LogLevel::Debug: return "DBG";
case LogLevel::Info: return "INF";
case LogLevel::Warn: return "WRN";
case LogLevel::Err: return "ERR";
}
return "???";
}
void write_all(const std::string& s) {
if (g_fd >= 0) {
size_t off = 0;
while (off < s.size()) {
ssize_t n = ::write(g_fd, s.data() + off, s.size() - off);
if (n < 0) break;
off += static_cast<size_t>(n);
}
} else {
::write(STDERR_FILENO, s.data(), s.size());
}
}
std::string timestamp() {
std::time_t t = std::time(nullptr);
std::tm tm{};
localtime_r(&t, &tm);
char buf[32];
std::strftime(buf, sizeof buf, "%H:%M:%S", &tm);
return buf;
}
} // namespace
void log_init(const std::string& console_path, LogLevel min_level) {
g_min_level = min_level;
g_fd = ::open(console_path.c_str(), O_WRONLY | O_NOCTTY | O_CLOEXEC);
if (g_fd >= 0) {
// flush buffered boot messages from the ring.
std::lock_guard<std::mutex> lk(g_lock);
size_t start = (g_ring_count < g_ring.size()) ? 0 : g_ring_pos;
size_t n = std::min(g_ring_count, g_ring.size());
for (size_t i = 0; i < n; ++i) {
write_all(g_ring[(start + i) % g_ring.size()]);
}
g_ring_count = 0;
}
}
void log_set_level(LogLevel level) { g_min_level = level; }
void log_msg(LogLevel level, const std::string& tag, const std::string& msg) {
if (level < g_min_level) return;
std::string line = "[" + timestamp() + "] [" + level_name(level) + "] " + tag + ": " +
msg + "\n";
std::lock_guard<std::mutex> lk(g_lock);
if (g_fd < 0 && g_ring_count < g_ring.size()) {
g_ring[g_ring_pos] = line;
g_ring_pos = (g_ring_pos + 1) % g_ring.size();
++g_ring_count;
// drop oldest to keep ordering if ring ever overflows
if (g_ring_count == g_ring.size()) {
g_ring[g_ring_pos] = line;
g_ring_pos = (g_ring_pos + 1) % g_ring.size();
}
return;
}
write_all(line);
}
} // namespace bajia

27
src/logger.hpp Normal file
View file

@ -0,0 +1,27 @@
// logger.hpp - minimal kernel-log / console logger for PID 1.
//
// Logs go to the console (/dev/console) if it can be opened, otherwise
// fall back to stderr. A ring buffer keeps boot messages in memory until
// the console is ready.
#pragma once
#include <string>
#include <utility>
namespace bajia {
enum class LogLevel { Debug = 0, Info = 1, Warn = 2, Err = 3 };
void log_init(const std::string& console_path = "/dev/console", LogLevel min_level = LogLevel::Info);
void log_set_level(LogLevel level);
void log_msg(LogLevel level, const std::string& tag, const std::string& msg);
template <typename... Args>
void log_info(const std::string& tag, Args&&... args) {
std::string buf;
(buf += ... += std::forward<Args>(args));
log_msg(LogLevel::Info, tag, buf);
}
} // namespace bajia

90
src/main.cpp Normal file
View file

@ -0,0 +1,90 @@
// main.cpp - bajia init system entry point (PID 1).
//
// loads .rc config files and hands control to the Supervisor, which never
// returns. on a real system this binary should be passed to the kernel as
// `init=/path/to/bajia`.
#include "config.hpp"
#include "logger.hpp"
#include "supervisor.hpp"
#include <cstdio>
#include <cstring>
#include <string>
#include <unistd.h>
#include <vector>
using namespace bajia;
namespace {
void usage(const char* argv0) {
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
::fprintf(stderr,
"usage: %s [--run-as-user] [config.rc ...]\n"
" if no files are given, reads /etc/bajia/init.rc if present.\n"
" as PID 1 this usually means the kernel passed init=%s.\n"
" refuses to start unless it is PID 1; --run-as-user overrides\n"
" that check for development runs.\n",
argv0, argv0);
}
} // namespace
int main(int argc, char** argv) {
std::vector<std::string> files;
bool run_as_user = false;
// PID 1 note: the kernel may pass extra args after the init program name.
for (int i = 1; i < argc; ++i) {
if (argv[i][0] == '-') {
if (std::strcmp(argv[i], "-h") == 0 || std::strcmp(argv[i], "--help") == 0) {
usage(argv[0]);
return 0;
}
if (std::strcmp(argv[i], "--run-as-user") == 0) {
run_as_user = true;
continue;
}
usage(argv[0]);
return 2;
}
files.emplace_back(argv[i]);
}
if (::getpid() != 1 && !run_as_user) {
::fprintf(stderr,
"bajia: refusing to run as pid %d (not PID 1). bajia is an "
"init system and would re-fire boot triggers, mount "
"filesystems and spawn services on top of a running system. "
"Launch it via the kernel (init=...), or pass --run-as-user "
"for a development run.\n",
::getpid());
return 1;
}
if (files.empty()) {
// default: prefer the single init.rc, and also load /etc/bajia.d/*.rc.
if (::access("/etc/bajia/init.rc", R_OK) == 0) files.emplace_back("/etc/bajia/init.rc");
if (files.empty()) {
::fprintf(stderr, "bajia: no config given and /etc/bajia/init.rc not found.\n");
return 1;
}
}
Config config;
try {
config = parse_config(files);
} catch (const std::exception& e) {
::fprintf(stderr, "bajia: config error: %s\n", e.what());
return 1;
}
// logger targets the console; falls back to stderr until the console is ready.
log_init("/dev/console", LogLevel::Info);
log_info("init", "loaded ", std::to_string(files.size()), " config file(s), ",
std::to_string(config.services.size()), " services, ",
std::to_string(config.actions.size()), " actions");
Supervisor supervisor(std::move(config));
supervisor.run(); // [[noreturn]]
}

512
src/supervisor.cpp Normal file
View file

@ -0,0 +1,512 @@
// supervisor.cpp - PID 1 supervisor
#include "supervisor.hpp"
#include "logger.hpp"
#include <cstring>
#include <sys/epoll.h>
#include <sys/signalfd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <utility>
#include <cerrno>
#include <chrono>
#include <climits>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <sys/mount.h>
#include <sys/reboot.h>
namespace bajia {
namespace {
constexpr const char* kTag = "supv";
// shutdown timing
constexpr int kStopGraceSecs = 5; // wait this long for a clean SIGTERM exit
constexpr int kKillGraceSecs = 2; // then this long after SIGKILL before giving up
// console fd shared with spawned services flagged `console`.
int g_open_console_fd = -1;
std::string join(const std::vector<std::string>& v, const std::string& sep) {
std::string out;
for (size_t i = 0; i < v.size(); ++i) {
if (i) out += sep;
out += v[i];
}
return out;
}
} // namespace
Supervisor::Supervisor(Config config) : config_(std::move(config)) {}
Supervisor::~Supervisor() {
if (sigfd_ >= 0) ::close(sigfd_);
if (epfd_ >= 0) ::close(epfd_);
}
void Supervisor::setup_signals() {
sigset_t mask;
sigemptyset(&mask);
// block these for ALL threads/processes; signalfd delivers them to us.
sigaddset(&mask, SIGCHLD);
sigaddset(&mask, SIGTERM);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGHUP);
sigaddset(&mask, SIGUSR1);
sigaddset(&mask, SIGQUIT);
if (sigprocmask(SIG_BLOCK, &mask, nullptr) != 0) {
log_info(kTag, "sigprocmask failed: ", std::strerror(errno));
}
sigfd_ = ::signalfd(-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK);
if (sigfd_ < 0) {
log_info(kTag, "signalfd failed: ", std::strerror(errno));
_exit(1);
}
epfd_ = ::epoll_create1(EPOLL_CLOEXEC);
if (epfd_ < 0) {
log_info(kTag, "epoll_create1 failed: ", std::strerror(errno));
_exit(1);
}
struct epoll_event ev {};
ev.events = EPOLLIN;
ev.data.fd = sigfd_;
if (::epoll_ctl(epfd_, EPOLL_CTL_ADD, sigfd_, &ev) != 0) {
log_info(kTag, "epoll_ctl failed: ", std::strerror(errno));
_exit(1);
}
}
void Supervisor::spawn_service(Service& svc, bool missing_ok) {
if (svc.running || svc.pid > 0) {
log_info(kTag, svc.name, " already running");
return;
}
if (svc.args.empty()) {
log_info(kTag, "service ", svc.name, " has no executable");
return;
}
if (::access(svc.args[0].c_str(), X_OK) != 0) {
if (missing_ok) {
log_info(kTag, svc.name, ": executable missing (", svc.args[0], "), ignored");
return;
}
log_info(kTag, svc.name, ": cannot exec ", svc.args[0], ": ", std::strerror(errno));
return;
}
pid_t pid = ::fork();
if (pid < 0) {
log_info(kTag, svc.name, ": fork failed: ", std::strerror(errno));
return;
}
if (pid == 0) {
// child
if (svc.console && g_open_console_fd >= 0) {
::dup2(g_open_console_fd, 0);
::dup2(g_open_console_fd, 1);
::dup2(g_open_console_fd, 2);
}
if (::chdir(svc.cwd.c_str()) != 0) {
_exit(126);
}
// environment: inherit, then apply K=V entries.
std::vector<std::string> kvs = svc.env;
std::vector<char*> envp;
for (char** e = environ; e && *e; ++e) envp.push_back(*e);
for (auto& kv : kvs) envp.push_back(const_cast<char*>(kv.c_str()));
envp.push_back(nullptr);
std::vector<char*> argv;
for (auto& a : svc.args) argv.push_back(const_cast<char*>(a.c_str()));
argv.push_back(nullptr);
// uid/gid handling intentionally left to a drop-privileges pass;
// for a first version we run as-is. Placeholder to avoid unused warn.
(void)svc.uid;
(void)svc.gid;
(void)svc.groups;
::execvpe(argv[0], argv.data(), envp.data());
// exec failed in child
_exit(127);
}
// parent
svc.pid = pid;
svc.running = true;
log_info(kTag, svc.name, " started (pid ", std::to_string(pid), ")");
}
void Supervisor::start_service(const std::string& name) {
Service* svc = config_.find_service(name);
if (!svc) {
log_info(kTag, "start ", name, ": no such service");
return;
}
if (svc->running) return;
spawn_service(*svc, false);
}
void Supervisor::stop_service(const std::string& name, bool kill) {
Service* svc = config_.find_service(name);
if (!svc || !svc->running) return;
if (svc->pid > 0) {
::kill(svc->pid, kill ? SIGKILL : SIGTERM);
}
}
void Supervisor::restart_service(const std::string& name) {
Service* svc = config_.find_service(name);
if (!svc) return;
if (svc->running && svc->pid > 0) {
::kill(svc->pid, SIGTERM);
// It will be respawned by reap logic for non-oneshot services; for
// simplicity, mark for immediate respawn below.
}
// If not running, start now.
if (!svc->running) spawn_service(*svc, false);
}
void Supervisor::reap_children() {
int status;
pid_t pid;
while ((pid = ::waitpid(-1, &status, WNOHANG)) > 0) {
// Find the service this pid belongs to.
bool matched = false;
for (auto& svc : config_.services) {
if (svc.pid == pid) {
matched = true;
int code = WIFEXITED(status) ? WEXITSTATUS(status)
: (WIFSIGNALED(status) ? 128 + WTERMSIG(status) : -1);
svc.exit_code = code;
svc.pid = 0;
svc.running = false;
log_info(kTag, svc.name, " exited with code ", std::to_string(code));
bool success = (code == 0);
if (svc.oneshot) {
// oneshot services terminate on their own; never respawn.
continue;
}
bool should_respawn = false;
switch (svc.respawn) {
case RespawnPolicy::Always: should_respawn = true; break;
case RespawnPolicy::OnFailure: should_respawn = !success; break;
case RespawnPolicy::Never: should_respawn = false; break;
}
if (should_respawn) {
if (shutdown_requested_) break;
spawn_service(svc, true);
}
break;
}
}
(void)matched; // keep matched to avoid unused warning
// for pids not matching a tracked service: they're orphans reparented
// to us; already reaped here, so nothing more to do.
}
}
void Supervisor::run_exec_command(const Command& cmd) {
// synchronously run an action 'exec' and wait for it to complete.
if (cmd.args.empty()) return;
pid_t pid = ::fork();
if (pid < 0) return;
if (pid == 0) {
std::vector<char*> argv;
for (auto& a : cmd.args) argv.push_back(const_cast<char*>(a.c_str()));
argv.push_back(nullptr);
::execvpe(argv[0], argv.data(), environ);
_exit(127);
}
int status;
::waitpid(pid, &status, 0);
log_info(kTag, "action exec ", join(cmd.args, " "), " -> ",
WIFEXITED(status) ? std::to_string(WEXITSTATUS(status)) : "signal");
}
void Supervisor::run_command(Command& cmd) {
using K = Command::Kind;
switch (cmd.kind) {
case K::Start:
if (!cmd.args.empty()) start_service(cmd.args[0]);
break;
case K::Stop:
if (!cmd.args.empty()) stop_service(cmd.args[0], true);
break;
case K::Restart:
if (!cmd.args.empty()) restart_service(cmd.args[0]);
break;
case K::Exec:
run_exec_command(cmd);
break;
case K::Mkdir:
if (cmd.args.size() >= 1) {
mode_t m = cmd.args.size() >= 2 ? static_cast<mode_t>(std::stoul(cmd.args[1], nullptr, 8)) : 0755;
::mkdir(cmd.args[0].c_str(), m);
}
break;
case K::Chmod:
if (cmd.args.size() >= 2) ::chmod(cmd.args[0].c_str(),
static_cast<mode_t>(std::stoul(cmd.args[1], nullptr, 8)));
break;
case K::Chown:
if (cmd.args.size() >= 3) {
uid_t uid = static_cast<uid_t>(std::stoul(cmd.args[1]));
gid_t gid = static_cast<gid_t>(std::stoul(cmd.args[2]));
::chown(cmd.args[0].c_str(), uid, gid);
}
break;
case K::Setenv:
if (cmd.args.size() >= 1) ::setenv(cmd.args[0].c_str(), cmd.args[1].c_str(), 1);
break;
case K::Write: {
if (cmd.args.size() >= 2) {
int fd = ::open(cmd.args[0].c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) {
::write(fd, cmd.args[1].c_str(), cmd.args[1].size());
::close(fd);
}
}
break;
}
case K::Symlink:
if (cmd.args.size() >= 2) ::symlink(cmd.args[0].c_str(), cmd.args[1].c_str());
break;
case K::Mount:
if (cmd.args.size() >= 3) ::mount(cmd.args[0].c_str(), cmd.args[1].c_str(),
cmd.args[2].c_str(), 0, nullptr);
break;
case K::Log:
log_info(kTag, "action: ", join(cmd.args, " "));
break;
}
}
void Supervisor::execute_action(Action& action) {
log_info(kTag, "trigger: ", action.trigger);
for (auto& cmd : action.commands) {
run_command(cmd);
}
}
// The console fd, opened once PID1 realizes it's on a real console. Provided
// so spawn_service can rebind stdio for services flagged `console`.
void Supervisor::open_console() {
if (g_open_console_fd >= 0) return;
g_open_console_fd = ::open("/dev/console", O_RDWR | O_NOCTTY | O_CLOEXEC);
}
void Supervisor::begin_shutdown(ShutdownKind kind) {
if (shutdown_requested_) return; // already winding down
shutdown_requested_ = true;
shutdown_kind_ = kind;
shutdown_state_ = ShutdownState::FiringActions;
log_info(kTag, "shutdown requested: ",
kind == ShutdownKind::Reboot ? "reboot" : "poweroff");
}
bool Supervisor::any_running() const {
for (const auto& svc : config_.services) {
if (svc.running) return true;
}
return false;
}
int Supervisor::shutdown_timeout_ms() const {
// millis until the next state transition, or -1 for "wait forever".
if (shutdown_state_ == ShutdownState::Running) return -1;
const auto now = std::chrono::steady_clock::now();
if (now >= shutdown_deadline_) return 0;
const auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
shutdown_deadline_ - now).count();
return ms >= INT_MAX ? INT_MAX : static_cast<int>(ms);
}
void Supervisor::unmount_filesystems() {
FILE* f = ::fopen("/proc/self/mounts", "r");
if (!f) {
log_info(kTag, "unmount: cannot open /proc/self/mounts: ", std::strerror(errno));
return;
}
std::vector<std::string> mounts; // mountpoints in mount order
char line[4096];
while (::fgets(line, sizeof line, f)) {
std::vector<std::string> toks;
char* save = nullptr;
for (char* p = ::strtok_r(line, " \t\n", &save); p;
p = ::strtok_r(nullptr, " \t\n", &save)) {
toks.emplace_back(p);
}
if (toks.size() >= 2 && toks[1] != "/") mounts.push_back(toks[1]);
}
::fclose(f);
for (auto it = mounts.rbegin(); it != mounts.rend(); ++it) { // deepest last
if (::umount2(it->c_str(), MNT_DETACH) != 0) {
log_info(kTag, "unmount ", *it, ": ", std::strerror(errno));
} else {
log_info(kTag, "unmounted ", *it);
}
}
}
bool Supervisor::advance_shutdown() {
switch (shutdown_state_) {
case ShutdownState::Running:
return false;
case ShutdownState::FiringActions:
log_info(kTag, "firing shutdown actions");
for (auto& action : config_.actions) {
if (action.trigger == "shutdown") execute_action(action);
}
for (auto& svc : config_.services) {
if (svc.running && svc.pid > 0) {
log_info(kTag, "stopping ", svc.name, " (pid ",
std::to_string(svc.pid), ")");
::kill(svc.pid, SIGTERM);
}
}
shutdown_deadline_ = std::chrono::steady_clock::now() +
std::chrono::seconds(kStopGraceSecs);
shutdown_state_ = ShutdownState::StoppingServices;
return false;
case ShutdownState::StoppingServices:
if (!any_running()) {
shutdown_state_ = ShutdownState::Finalizing;
return false;
}
if (std::chrono::steady_clock::now() >= shutdown_deadline_) {
log_info(kTag, "grace elapsed; forcing kill");
for (auto& svc : config_.services) {
if (svc.running && svc.pid > 0) {
log_info(kTag, "killing ", svc.name, " (pid ",
std::to_string(svc.pid), ")");
::kill(svc.pid, SIGKILL);
}
}
shutdown_deadline_ = std::chrono::steady_clock::now() +
std::chrono::seconds(kKillGraceSecs);
shutdown_state_ = ShutdownState::ForcingKill;
}
return false;
case ShutdownState::ForcingKill:
if (!any_running() || std::chrono::steady_clock::now() >= shutdown_deadline_) {
shutdown_state_ = ShutdownState::Finalizing; // give up on the rest
}
return false;
case ShutdownState::Finalizing: {
bool reboot = shutdown_kind_ == ShutdownKind::Reboot;
log_info(kTag, "finalizing: ", reboot ? "reboot" : "poweroff");
::sync();
unmount_filesystems();
::sync();
if (reboot) {
log_info(kTag, "reboot()");
if (::reboot(RB_AUTOBOOT) != 0) {
log_info(kTag, "reboot failed: ", std::strerror(errno));
}
} else {
log_info(kTag, "power off");
if (::reboot(RB_POWER_OFF) != 0) {
log_info(kTag, "poweroff failed: ", std::strerror(errno));
}
}
return true; // nothing left do to; run() will _exit(0)
}
}
return false;
}
void Supervisor::handle_sigchld() {
struct signalfd_siginfo si;
ssize_t n;
while ((n = ::read(sigfd_, &si, sizeof si)) == static_cast<ssize_t>(sizeof si)) {
switch (si.ssi_signo) {
case SIGCHLD:
reap_children();
break;
case SIGTERM:
case SIGINT:
// SIGTERM -> reboot, SIGINT -> poweroff (distinct paths to test).
begin_shutdown(si.ssi_signo == SIGTERM ? ShutdownKind::Reboot
: ShutdownKind::PowerOff);
break;
case SIGQUIT:
log_info(kTag, "SIGQUIT: emergency exit");
_exit(1);
break;
case SIGHUP:
case SIGUSR1:
log_info(kTag, "reload requested (not yet implemented)");
break;
default:
break;
}
}
}
[[noreturn]] void Supervisor::run() {
setup_signals();
open_console();
log_info(kTag, "bajia init starting (pid 1)");
// boot sequence: fire ordered triggers.
for (const char* ev : {"early-init", "init", "boot"}) {
for (auto& action : config_.actions) {
if (action.trigger == ev) execute_action(action);
}
}
struct epoll_event events[8];
for (;;) {
// while shutting down, wake up exactly when the next state transition
// is due; otherwise block indefinitely.
int timeout = shutdown_state_ == ShutdownState::Running ? -1
: shutdown_timeout_ms();
int n = ::epoll_wait(epfd_, events, 8, timeout);
if (n < 0) {
if (errno == EINTR) continue;
log_info(kTag, "epoll_wait: ", std::strerror(errno));
_exit(1);
}
for (int i = 0; i < n; ++i) {
if (events[i].data.fd == sigfd_) {
handle_sigchld();
}
}
reap_children(); // also catch anything before the next event
if (advance_shutdown()) {
log_info(kTag, "shutdown complete");
_exit(0);
}
}
}
} // namespace bajia

75
src/supervisor.hpp Normal file
View file

@ -0,0 +1,75 @@
// supervisor.hpp - PID 1 event loop and service supervisor.
//
// owns the signalfd+epoll loop, spawns/reaps services, fires actions on
// triggers, and never returns (except on explicit termination).
#pragma once
#include "config.hpp"
#include <chrono>
#include <string>
namespace bajia {
// what to do once a shutdown request has completed.
enum class ShutdownKind {
PowerOff,
Reboot,
};
class Supervisor {
public:
explicit Supervisor(Config config);
~Supervisor();
// fire all actions whose trigger matches `trigger`. Queued and run by
// the event loop. `trigger` is one of: early-init, init, boot, shutdown,
// or a custom event name.
void trigger(const std::string& event);
void start_service(const std::string& name);
void stop_service(const std::string& name, bool kill = false);
void restart_service(const std::string& name);
// run until terminated (e.g. by SIGTERM/SIGINT -> calls shutdown()). Does
// not return normally.
[[noreturn]] void run();
private:
// shutdown is a state machine driven from the event loop so that SIGKILL
// grace periods and child reaping keep working while we wind down.
enum class ShutdownState {
Running, // normal operation
FiringActions, // ran `on shutdown` actions, SIGTERM all services
StoppingServices, // waiting for graceful exits (bounded by a deadline)
ForcingKill, // grace elapsed; SIGKILL the stragglers
Finalizing, // unmount + sync + reboot/poweroff
};
Config config_;
int epfd_ = -1;
int sigfd_ = -1;
bool shutdown_requested_ = false;
ShutdownKind shutdown_kind_ = ShutdownKind::PowerOff;
ShutdownState shutdown_state_ = ShutdownState::Running;
std::chrono::steady_clock::time_point shutdown_deadline_{};
void setup_signals();
void open_console();
void spawn_service(Service& svc, bool missing_ok);
void reap_children();
void execute_action(Action& action);
void run_exec_command(const Command& cmd);
void run_command(Command& cmd);
void begin_shutdown(ShutdownKind kind);
// step the shutdown state machine; returns true when the loop should exit.
bool advance_shutdown();
bool any_running() const;
int shutdown_timeout_ms() const;
void unmount_filesystems();
void handle_sigchld();
};
} // namespace bajia

329
tools/run_vm.py Normal file
View file

@ -0,0 +1,329 @@
#!/usr/bin/env python3
"""boot bajia as PID 1 in a QEMU VM using a tiny initramfs.
sets up everything needed for a realistic test run:
- builds bajia (optionally statically linked)
- builds an initramfs with bajia as /init, busybox, and a test init.rc
- runs qemu-system-x86_64 with a GTK display, serial console, and a gdb stub
examples:
python3 tools/run_vm.py --busybox /path/to/busybox-static
python3 tools/run_vm.py --fetch-busybox --nographic
python3 tools/run_vm.py --kernel /boot/vmlinuz-$(uname -r) --gdb
python3 tools/run_vm.py --config my-init.rc
inside the guest: log in as `root` (passwordless by default, or use
--root-password), then `kill -TERM 1` -> reboot path, `kill -INT 1` ->
poweroff path.
bajia is built statically by default: a dynamic binary cannot exec inside the
initramfs (no libc there). Use --no-static only if you ship the libs too.
"""
from __future__ import annotations
import argparse
import glob
import gzip
import os
import shutil
import subprocess
import sys
import tarfile
import tempfile
import urllib.request
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
BUILD = ROOT / "build"
BAJIA = BUILD / "bajia"
DEFAULT_RC = """\
# generated by tools/run_vm.py - minimal init.rc for VM testing.
on early-init
mount proc /proc proc
mount sysfs /sys sysfs
mount devtmpfs /dev devtmpfs
mkdir /dev/pts 0755
mount devpts /dev/pts devpts
mkdir /run 0755
mount tmpfs /run tmpfs
on init
write /proc/sys/kernel/hostname bajia
log **** bajia init on-line ****
on boot
start console-serial
start console-tty1
on shutdown
log shutdown: stopping services
service console-serial /bin/getty -L ttyS0 115200 vt100
console
respawn always
service console-tty1 /bin/getty -L 38400 tty1 vt100
console
respawn always
"""
# source tarballs of busybox (github.com/mirror/busybox); a pinned tag is
# tried first, then the master branch. built statically into $XDG_CACHE_HOME.
BUSYBOX_URLS = [
"https://github.com/mirror/busybox/archive/refs/tags/1_36_1.tar.gz",
"https://github.com/mirror/busybox/archive/refs/heads/master.tar.gz",
]
BUSYBOX_APPLETS = ["sh", "getty", "mount", "sync", "ls", "cat", "kill", "ps",
"poweroff", "reboot", "mkdir", "mknod", "login"]
def run(cmd, **kw) -> subprocess.CompletedProcess:
print("$", " ".join(str(c) for c in cmd))
return subprocess.run(cmd, **kw)
def build_bajia(static: bool) -> Path:
env = dict(os.environ)
if static:
print("building bajia (statically linked)...")
env["CXX"] = env.get("CXX", "g++") + " -static"
r = run(["python3", "configure.py"], env=env)
if r.returncode != 0:
sys.exit("configure.py failed")
r = run(["ninja", "-C", str(BUILD)])
if r.returncode != 0:
sys.exit("ninja build failed")
return BAJIA
def find_kernel() -> Path | None:
p = Path("/boot/vmlinuz-" + os.uname().release)
if p.is_file():
return p
matches = sorted(glob.glob("/boot/vmlinuz-*"))
return Path(matches[-1]) if matches else None
def is_static(path: Path) -> bool:
filetool = shutil.which("file")
if not filetool:
return True # cannot tell; don't nag
out = run([filetool, "-b", str(path)], capture_output=True, text=True)
blob = out.stdout.lower()
return "static" in blob or "statically" in blob
def find_busybox() -> Path | None:
for cand in (os.environ.get("BAJIA_BUSYBOX"), shutil.which("busybox")):
if cand and Path(cand).is_file():
return Path(cand)
return None
def fetch_busybox(cache: Path) -> Path:
"""download busybox source from github.com/mirror/busybox and build it
statically. The resulting binary is cached in `cache`."""
dst = cache / "busybox"
if dst.is_file():
print("using cached busybox:", dst)
return dst
src_dir = cache / "busybox-src"
src_dir.mkdir(parents=True, exist_ok=True)
tree = None
for url in BUSYBOX_URLS:
try:
print("downloading busybox source:", url)
req = urllib.request.Request(url, headers={"User-Agent": "bajia-run-vm"})
with urllib.request.urlopen(req, timeout=120) as resp, open(
cache / "busybox.tar.gz", "wb") as out:
shutil.copyfileobj(resp, out)
print("extracting busybox source...")
with tarfile.open(cache / "busybox.tar.gz", "r:gz") as tf:
tf.extractall(src_dir)
tops = [p for p in src_dir.iterdir() if p.is_dir()]
if len(tops) == 1:
tree = tops[0]
break
print(" unexpected source layout, trying next URL")
except Exception as e: # noqa: BLE001 - try the next URL
print(" failed:", e)
if not tree:
sys.exit("could not fetch busybox source from github.com/mirror/busybox")
env = dict(os.environ)
r = run(["make", "defconfig"], cwd=tree, env=env)
if r.returncode != 0:
sys.exit("busybox defconfig failed")
r = run(["sed", "-i", "s|# CONFIG_STATIC is not set|CONFIG_STATIC=y|",
tree / ".config"])
if r.returncode != 0:
sys.exit("busybox config edit failed")
# CONFIG_TC needs the CBQ uapi structs (tc_cbq_*) that modern kernel
# headers no longer provide; not needed in an initramfs, so drop it.
r = run(["sed", "-i", "s|^CONFIG_TC=y$|# CONFIG_TC is not set|",
tree / ".config"])
if r.returncode != 0:
sys.exit("busybox config edit failed")
r = run(["make", f"-j{os.cpu_count() or 2}", "busybox"], cwd=tree, env=env)
if r.returncode != 0:
sys.exit("busybox build failed; if another applet breaks on your "
"kernel headers, disable it in busybox-src/.config and pass "
"--busybox with a prebuilt static binary instead")
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(tree / "busybox", dst)
dst.chmod(0o755)
print("built busybox:", dst)
return dst
def default_cache_dir() -> Path:
base = Path(os.environ.get("XDG_CACHE_HOME",
str(Path.home() / ".cache")))
return base / "bajia"
def crypt_password(password: str) -> str:
try:
import crypt
return crypt.crypt(password, crypt.mksalt(crypt.METHOD_SHA512))
except (ImportError, AttributeError):
p = subprocess.run(["openssl", "passwd", "-6", password],
capture_output=True, text=True)
if p.returncode != 0:
sys.exit("cannot hash the root password: need python `crypt` or openssl")
return p.stdout.strip()
def root_passwd_line(password: str | None) -> str:
field = crypt_password(password) if password else ""
return f"root:{field}:0:0:root:/:/bin/sh\n"
def build_initramfs(init: Path, busybox: Path, rc_text: str, root_password: str | None,
keep: bool) -> Path:
if not shutil.which("cpio"):
sys.exit("cpio not found (install cpio)")
root = Path(tempfile.mkdtemp(prefix="bajia-root-"))
try:
for sub in ("etc/bajia", "bin", "sbin", "usr/sbin", "usr/bin",
"dev", "proc", "sys", "run", "tmp"):
(root / sub).mkdir(parents=True)
shutil.copy(init, root / "init")
(root / "init").chmod(0o755)
shutil.copy(busybox, root / "bin" / "busybox")
(root / "bin" / "busybox").chmod(0o755)
for applet in BUSYBOX_APPLETS:
(root / "bin" / applet).symlink_to("busybox")
(root / "etc" / "bajia" / "init.rc").write_text(rc_text)
(root / "etc" / "passwd").write_text(root_passwd_line(root_password))
p = run(["bash", "-c",
"cd \"$1\" && find . -print0 | cpio --null -o -H newc",
"bajia-initramfs", str(root)], stdout=subprocess.PIPE)
if p.returncode != 0:
sys.exit("cpio packing failed")
initrd = Path(tempfile.gettempdir()) / "bajia-initrd.cpio.gz"
initrd.write_bytes(gzip.compress(p.stdout))
if keep:
print("initramfs root tree kept at:", root)
return initrd
finally:
if not keep:
shutil.rmtree(root, ignore_errors=True)
def qemu_command(kernel: Path, initrd: Path, args: argparse.Namespace) -> list[str]:
qemu = args.qemu or shutil.which("qemu-system-x86_64") or "qemu-system-x86_64"
display = args.display
if display is None:
display = "gtk" if os.environ.get("DISPLAY") else "none"
cmd = [
qemu,
"-M", args.machine,
"-m", str(args.mem),
"-smp", str(args.smp),
"-kernel", str(kernel),
"-initrd", str(initrd),
"-append",
f"console=tty1 console=ttyS0 rdinit=/init loglevel={args.loglevel}",
"-display", display,
"-serial", "stdio",
]
if args.nographic:
cmd[cmd.index("-display") + 1] = "none"
if args.gdb or args.wait_gdb:
cmd += ["-gdb", "tcp::1234", "-S"] if args.wait_gdb else ["-s"]
cmd += ["-no-reboot", "-no-shutdown"]
return cmd
def main() -> int:
ap = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--kernel", help="kernel bzImage (default: host /boot/vmlinuz-*)")
ap.add_argument("--busybox", help="static busybox binary to bundle")
ap.add_argument("--fetch-busybox", action="store_true",
help="download busybox source (github.com/mirror/busybox), "
"build it static, cache in ~/.cache/bajia")
ap.add_argument("--config", type=Path,
help="use this init.rc instead of the bundled test config")
ap.add_argument("--root-password", default=None,
help="password for the root account in the guest "
"(default: passwordless login)")
ap.add_argument("--no-static", action="store_true",
help="build bajia dynamically linked (won't exec in the "
"initramfs unless you also pack the libs)")
ap.add_argument("--no-build", action="store_true",
help="use the existing build/bajia without rebuilding")
ap.add_argument("--display", choices=["gtk", "sdl", "none"],
help="QEMU display backend (default: gtk if $DISPLAY set)")
ap.add_argument("--nographic", action="store_true",
help="headless; serial console on stdio")
ap.add_argument("--machine", default="q35", help="QEMU machine type")
ap.add_argument("--mem", type=int, default=512, help="RAM in MB")
ap.add_argument("--smp", type=int, default=2, help="virtual CPUs")
ap.add_argument("--loglevel", type=int, default=4,
help="kernel loglevel (4=dmesg, 7=everything)")
ap.add_argument("--qemu", help="qemu binary (default: qemu-system-x86_64)")
ap.add_argument("--gdb", action="store_true", help="expose a gdb stub on :1234")
ap.add_argument("--wait-gdb", action="store_true",
help="pause the machine until a gdb client attaches")
ap.add_argument("--keep-initramfs", action="store_true",
help="don't delete the initramfs staging tree")
args = ap.parse_args()
init = BAJIA if args.no_build else build_bajia(not args.no_static)
if not init.is_file():
sys.exit(f"bajia not built at {init} (drop --no-build)")
if not is_static(init):
print("warning: bajia is dynamically linked; /init will fail to exec "
"inside the initramfs (error -2). Rebuild with --no-static "
"unset (static is the default) or drop --no-build.")
kernel = Path(args.kernel) if args.kernel else find_kernel()
if not kernel or not kernel.is_file():
sys.exit("no kernel found: pass --kernel /path/to/bzImage or install a "
"kernel and use its /boot/vmlinuz-*")
if args.fetch_busybox:
busybox = fetch_busybox(default_cache_dir())
else:
busybox = Path(args.busybox) if args.busybox else find_busybox()
if not busybox or not busybox.is_file():
sys.exit("no busybox found: pass --busybox, --fetch-busybox, or install "
"busybox-static (Debian/Ubuntu: `apt install busybox-static`)")
if not is_static(busybox):
print("warning: busybox looks dynamically linked; a static build is "
"safest inside an initramfs")
rc_text = args.config.read_text() if args.config else DEFAULT_RC
initrd = build_initramfs(init, busybox, rc_text, args.root_password,
keep=args.keep_initramfs)
print("initramfs:", initrd, f"({initrd.stat().st_size / 1024:.0f} KB)")
cmd = qemu_command(kernel, initrd, args)
print("$", " ".join(cmd))
return subprocess.run(cmd).returncode
if __name__ == "__main__":
try:
sys.exit(main())
except KeyboardInterrupt:
sys.exit(130)