add stuff
This commit is contained in:
parent
91140888db
commit
1cd4394841
12 changed files with 1702 additions and 18 deletions
19
.gitignore
vendored
19
.gitignore
vendored
|
|
@ -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/
|
||||
|
|
|
|||
95
README.md
95
README.md
|
|
@ -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
132
configure.py
Normal 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
54
etc/init.rc
Normal 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
188
src/config.cpp
Normal 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
107
src/config.hpp
Normal 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
92
src/logger.cpp
Normal 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
27
src/logger.hpp
Normal 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
90
src/main.cpp
Normal 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
512
src/supervisor.cpp
Normal 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
75
src/supervisor.hpp
Normal 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
329
tools/run_vm.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue