Merge branch 'init_rework8' into 'main'

init: Introduce a basic job scheduler

See merge request redox-os/base!215
This commit is contained in:
Jeremy Soller
2026-04-09 15:21:05 -06:00
4 changed files with 211 additions and 154 deletions
+19 -26
View File
@@ -2,7 +2,8 @@
#![feature(never_type)]
use std::io::{self, PipeWriter, Read, Write};
use std::os::fd::{AsRawFd, FromRawFd, RawFd};
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::os::unix::process::CommandExt;
use std::process::Command;
use libredox::Fd;
@@ -20,6 +21,20 @@ unsafe fn get_fd(var: &str) -> RawFd {
fd
}
unsafe fn pass_fd(cmd: &mut Command, env: &str, fd: OwnedFd) {
cmd.env(env, format!("{}", fd.as_raw_fd()));
unsafe {
cmd.pre_exec(move || {
// Pass notify pipe to child
if libc::fcntl(fd.as_raw_fd(), libc::F_SETFD, 0) == -1 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
});
}
}
/// A long running background process that handles requests.
#[must_use = "Daemon::ready must be called"]
pub struct Daemon {
@@ -43,29 +58,16 @@ impl Daemon {
pub fn spawn(mut cmd: Command) {
let (mut read_pipe, write_pipe) = io::pipe().unwrap();
// Pass notify pipe to child
if unsafe { libc::fcntl(write_pipe.as_raw_fd(), libc::F_SETFD, 0) } == -1 {
eprintln!(
"daemon: failed to unset CLOEXEC flag for notify pipe: {}",
io::Error::last_os_error()
);
return;
}
cmd.env("INIT_NOTIFY", format!("{}", write_pipe.as_raw_fd()));
unsafe { pass_fd(&mut cmd, "INIT_NOTIFY", write_pipe.into()) };
if let Err(err) = cmd.spawn() {
eprintln!("daemon: failed to execute {cmd:?}: {err}");
return;
}
drop(write_pipe);
let mut data = [0];
match read_pipe.read_exact(&mut data) {
Ok(()) => {
if data[0] != 0 {
eprintln!("daemon: {cmd:?} failed with {}", data[0]);
}
}
Ok(()) => {}
Err(err) if err.kind() == io::ErrorKind::UnexpectedEof => {
eprintln!("daemon: {cmd:?} exited without notifying readiness");
}
@@ -127,21 +129,12 @@ impl SchemeDaemon {
pub fn spawn(mut cmd: Command, scheme_name: &str) {
let (read_pipe, write_pipe) = io::pipe().unwrap();
// Pass notify pipe to child
if unsafe { libc::fcntl(write_pipe.as_raw_fd(), libc::F_SETFD, 0) } == -1 {
eprintln!(
"daemon: failed to unset CLOEXEC flag for notify pipe: {}",
io::Error::last_os_error()
);
return;
}
cmd.env("INIT_NOTIFY", format!("{}", write_pipe.as_raw_fd()));
unsafe { pass_fd(&mut cmd, "INIT_NOTIFY", write_pipe.into()) };
if let Err(err) = cmd.spawn() {
eprintln!("daemon: failed to execute {cmd:?}: {err}");
return;
}
drop(write_pipe);
let mut new_fd = usize::MAX;
let fd_bytes = unsafe {
+66 -127
View File
@@ -1,15 +1,16 @@
use std::collections::{BTreeMap, VecDeque};
use std::collections::BTreeMap;
use std::env;
use std::ffi::OsString;
use std::io::Result;
use std::path::{Path, PathBuf};
use std::path::Path;
use libredox::flag::{O_RDONLY, O_WRONLY};
use serde::Deserialize;
use crate::scheduler::Scheduler;
use crate::script::Command;
use crate::unit::{UnitId, UnitStore};
use crate::unit::{Unit, UnitId, UnitStore};
mod scheduler;
mod script;
mod service;
mod unit;
@@ -49,83 +50,25 @@ impl InitConfig {
}
}
#[derive(Clone, Deserialize)]
#[serde(deny_unknown_fields)]
struct SwitchRoot {
prefix: PathBuf,
etcdir: PathBuf,
target: Option<UnitId>,
fn switch_root(unit_store: &mut UnitStore, config: &mut InitConfig, prefix: &Path, etcdir: &Path) {
eprintln!(
"init: switchroot to {} {}",
prefix.display(),
etcdir.display()
);
config
.envs
.insert("PATH".to_owned(), prefix.join("bin").into_os_string());
config.envs.insert(
"LD_LIBRARY_PATH".to_owned(),
prefix.join("lib").into_os_string(),
);
unit_store.config_dirs = vec![prefix.join("lib").join("init.d"), etcdir.join("init.d")];
}
impl SwitchRoot {
fn apply(
self,
pending_units: &mut VecDeque<UnitId>,
unit_store: &mut UnitStore,
config: &mut InitConfig,
) {
eprintln!(
"init: switchroot to {} {}",
self.prefix.display(),
self.etcdir.display()
);
config
.envs
.insert("PATH".to_owned(), self.prefix.join("bin").into_os_string());
config.envs.insert(
"LD_LIBRARY_PATH".to_owned(),
self.prefix.join("lib").into_os_string(),
);
unit_store.config_dirs = vec![
self.prefix.join("lib").join("init.d"),
self.etcdir.join("init.d"),
];
let mut errors = vec![];
let loaded_units =
unit_store.load_units(UnitId("00_runtime.target".to_owned()), &mut errors);
pending_units.extend(loaded_units);
if let Some(target) = self.target {
let loaded_units = unit_store.load_units(target, &mut errors);
pending_units.extend(loaded_units);
} else {
let entries = match config::config_for_dirs(&unit_store.config_dirs) {
Ok(entries) => entries,
Err(err) => {
eprintln!(
"init: failed to read configs from {}: {err}",
unit_store
.config_dirs
.iter()
.map(|dir| dir.display().to_string())
.collect::<Vec<_>>()
.join(", ")
);
return;
}
};
for entry in entries {
let loaded_units = unit_store.load_units(
UnitId(entry.file_name().unwrap().to_str().unwrap().to_owned()),
&mut errors,
);
pending_units.extend(loaded_units);
}
}
for error in errors {
eprintln!("init: {error}");
}
}
}
fn run(unit: &UnitId, unit_store: &mut UnitStore, config: &mut InitConfig) -> Result<()> {
let unit = unit_store.unit_mut(unit);
fn run(unit: &mut Unit, config: &mut InitConfig) -> Result<()> {
match &unit.kind {
unit::UnitKind::LegacyScript { script } => {
for cmd in script.0.clone() {
@@ -185,17 +128,21 @@ fn run_command(cmd: Command, config: &mut InitConfig) {
fn main() {
let mut init_config = InitConfig::new();
let mut unit_store = UnitStore::new();
let mut pending_units = VecDeque::new();
let mut scheduler = Scheduler::new();
switch_root(
&mut unit_store,
&mut init_config,
Path::new("/scheme/initfs"),
Path::new("/scheme/initfs/etc"),
);
let runtime_target = UnitId("00_runtime.target".to_owned());
let initfs_target = UnitId("90_initfs.target".to_owned());
scheduler.schedule_start_and_report_errors(&mut unit_store, runtime_target.clone());
unit_store.set_runtime_target(runtime_target);
SwitchRoot {
prefix: Path::new("/scheme/initfs").to_owned(),
etcdir: Path::new("/scheme/initfs/etc").to_owned(),
target: Some(initfs_target.clone()),
}
.apply(&mut pending_units, &mut unit_store, &mut init_config);
scheduler
.schedule_start_and_report_errors(&mut unit_store, UnitId("90_initfs.target".to_owned()));
let mut command = std::process::Command::new("logd");
command.env_clear().envs(&init_config.envs);
@@ -204,50 +151,42 @@ fn main() {
eprintln!("init: failed to switch stdio to '/scheme/log': {err}");
}
'a: while let Some(unit) = pending_units.pop_front() {
if let Some(condition_architecture) = &unit_store.unit(&unit).info.condition_architecture {
if !condition_architecture
.iter()
.any(|arch| arch == std::env::consts::ARCH)
{
continue 'a;
}
}
if let Some(condition_board) = &unit_store.unit(&unit).info.condition_board {
if !condition_board
.iter()
.any(|board| Some(&**board) == option_env!("BOARD"))
{
continue 'a;
}
}
if unit_store.unit(&unit).info.default_dependencies {
if pending_units.contains(&runtime_target) {
pending_units.push_back(unit);
continue 'a;
}
}
for dep in &unit_store.unit(&unit).info.requires_weak {
if pending_units.contains(dep) {
pending_units.push_back(unit);
continue 'a;
}
}
scheduler.step(&mut unit_store, &mut init_config);
if let Err(err) = run(&unit, &mut unit_store, &mut init_config) {
eprintln!("init: failed to run {}: {}", unit.0, err);
}
switch_root(
&mut unit_store,
&mut init_config,
Path::new("/usr"),
Path::new("/etc"),
);
{
// FIXME introduce multi-user.target unit and replace the config dir iteration
// scheduler.schedule_start_and_report_errors(&mut unit_store, UnitId("multi-user.target".to_owned()));
if unit == initfs_target {
SwitchRoot {
prefix: PathBuf::from("/usr"),
etcdir: PathBuf::from("/etc"),
// FIXME make target non-optional once there is a multi-user.target unit
target: None, //UnitId("multi-user.target".to_owned()),
let entries = match config::config_for_dirs(&unit_store.config_dirs) {
Ok(entries) => entries,
Err(err) => {
eprintln!(
"init: failed to read configs from {}: {err}",
unit_store
.config_dirs
.iter()
.map(|dir| dir.display().to_string())
.collect::<Vec<_>>()
.join(", ")
);
return;
}
.apply(&mut pending_units, &mut unit_store, &mut init_config);
};
for entry in entries {
scheduler.schedule_start_and_report_errors(
&mut unit_store,
UnitId(entry.file_name().unwrap().to_str().unwrap().to_owned()),
);
}
}
};
scheduler.step(&mut unit_store, &mut init_config);
libredox::call::setrens(0, 0).expect("init: failed to enter null namespace");
+83
View File
@@ -0,0 +1,83 @@
use std::collections::VecDeque;
use crate::InitConfig;
use crate::unit::{UnitId, UnitStore};
pub struct Scheduler {
pending: VecDeque<Job>,
}
struct Job {
unit: UnitId,
kind: JobKind,
}
enum JobKind {
Start,
}
impl Scheduler {
pub fn new() -> Scheduler {
Scheduler {
pending: VecDeque::new(),
}
}
pub fn schedule_start_and_report_errors(
&mut self,
unit_store: &mut UnitStore,
unit_id: UnitId,
) {
let mut errors = vec![];
self.schedule_start(unit_store, unit_id, &mut errors);
for error in errors {
eprintln!("init: {error}");
}
}
pub fn schedule_start(
&mut self,
unit_store: &mut UnitStore,
unit_id: UnitId,
errors: &mut Vec<String>,
) {
let loaded_units = unit_store.load_units(unit_id.clone(), errors);
for unit_id in loaded_units {
if !unit_store.unit(&unit_id).conditions_met() {
continue;
}
self.pending.push_back(Job {
unit: unit_id,
kind: JobKind::Start,
});
}
}
pub fn step(&mut self, unit_store: &mut UnitStore, init_config: &mut InitConfig) {
'a: loop {
let Some(job) = self.pending.pop_front() else {
return;
};
match job.kind {
JobKind::Start => {
let unit = unit_store.unit_mut(&job.unit);
for dep in &unit.info.requires_weak {
for pending_job in &self.pending {
if &pending_job.unit == dep {
self.pending.push_back(job);
continue 'a;
}
}
}
if let Err(err) = crate::run(unit, init_config) {
eprintln!("init: failed to run {}: {}", job.unit.0, err);
}
}
}
}
}
}
+43 -1
View File
@@ -10,6 +10,7 @@ use crate::service::Service;
pub struct UnitStore {
pub config_dirs: Vec<PathBuf>,
units: BTreeMap<UnitId, Unit>,
runtime_target: Option<UnitId>,
}
impl UnitStore {
@@ -17,9 +18,16 @@ impl UnitStore {
UnitStore {
config_dirs: vec![],
units: BTreeMap::new(),
runtime_target: None,
}
}
pub fn set_runtime_target(&mut self, unit_id: UnitId) {
assert!(self.runtime_target.is_none());
assert!(self.units.contains_key(&unit_id));
self.runtime_target = Some(unit_id);
}
fn load_single_unit(&mut self, unit_id: UnitId, errors: &mut Vec<String>) -> Option<UnitId> {
let (filename, instance) = if let Some((base_service, rest)) = unit_id.0.split_once('@') {
let Some((instance, ext)) = rest.rsplit_once('.') else {
@@ -42,13 +50,25 @@ impl UnitStore {
return None;
};
let unit = match Unit::from_file(unit_id.clone(), &path, instance, errors) {
let mut unit = match Unit::from_file(unit_id.clone(), &path, instance, errors) {
Ok(unit) => unit,
Err(err) => {
errors.push(format!("{}: {err}", path.display()));
return None;
}
};
if unit.info.default_dependencies {
if let Some(runtime_target) = self.runtime_target.clone() {
unit.info.requires_weak.push(runtime_target);
} else {
errors.push(format!(
"{}: dependency of the runtime target must have default dependencies disabled",
path.display(),
));
}
}
self.units.insert(unit_id.clone(), unit);
Some(unit_id)
@@ -213,4 +233,26 @@ impl Unit {
Ok(Unit { id, info, kind })
}
pub fn conditions_met(&self) -> bool {
if let Some(condition_architecture) = &self.info.condition_architecture {
if !condition_architecture
.iter()
.any(|arch| arch == std::env::consts::ARCH)
{
return false;
}
}
if let Some(condition_board) = &self.info.condition_board {
if !condition_board
.iter()
.any(|board| Some(&**board) == option_env!("BOARD"))
{
return false;
}
}
true
}
}