554 lines
19 KiB
Rust
554 lines
19 KiB
Rust
mod config;
|
|
mod exec;
|
|
mod hotplug;
|
|
mod scheme;
|
|
|
|
use std::sync::atomic::{AtomicBool, Ordering};
|
|
use std::sync::{Arc, Mutex};
|
|
use std::thread;
|
|
use std::time::{Duration, Instant};
|
|
use std::{env, fs, process};
|
|
|
|
use redox_driver_core::device::DeviceId;
|
|
use redox_driver_core::driver::ProbeResult;
|
|
use redox_driver_core::manager::{DeviceManager, ManagerConfig, ProbeEvent};
|
|
use redox_driver_pci::PciBus;
|
|
use redox_driver_acpi::AcpiBus;
|
|
use std::fs::OpenOptions;
|
|
use std::io::Write;
|
|
|
|
use config::DriverConfig;
|
|
use scheme::{DriverManagerScheme, notify_bind};
|
|
|
|
/// Global flag set by SIGTERM handler to request graceful shutdown.
|
|
static SHUTDOWN_REQUESTED: AtomicBool = AtomicBool::new(false);
|
|
|
|
extern "C" fn sigterm_handler(_sig: i32) {
|
|
SHUTDOWN_REQUESTED.store(true, Ordering::SeqCst);
|
|
}
|
|
|
|
fn install_sigterm_handler() {
|
|
unsafe {
|
|
libc::signal(libc::SIGTERM, sigterm_handler as *const () as usize);
|
|
}
|
|
}
|
|
|
|
struct StderrLogger;
|
|
|
|
const BOOT_TIMELINE_PATH: &str = "/tmp/redbear-boot-timeline.json";
|
|
|
|
impl log::Log for StderrLogger {
|
|
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
|
metadata.level() <= log::Level::Info
|
|
}
|
|
fn log(&self, record: &log::Record) {
|
|
if self.enabled(record.metadata()) {
|
|
eprintln!("[{}] driver-manager: {}", record.level(), record.args());
|
|
}
|
|
}
|
|
fn flush(&self) {}
|
|
}
|
|
|
|
fn run_enumeration(
|
|
manager: &Arc<Mutex<DeviceManager>>,
|
|
scheme: &DriverManagerScheme,
|
|
initfs: bool,
|
|
) -> (usize, usize) {
|
|
let enum_start = Instant::now();
|
|
let events = match manager.lock() {
|
|
Ok(mut mgr) => mgr.enumerate(),
|
|
Err(err) => {
|
|
log::error!("failed to enumerate devices: manager lock poisoned: {err}");
|
|
return (0, 0);
|
|
}
|
|
};
|
|
let enum_duration = enum_start.elapsed();
|
|
|
|
let mut bound = 0usize;
|
|
let mut deferred = 0usize;
|
|
|
|
for event in &events {
|
|
log_timeline(event);
|
|
match event {
|
|
ProbeEvent::ProbeCompleted {
|
|
device,
|
|
driver_name,
|
|
result,
|
|
} => match result {
|
|
ProbeResult::Bound => {
|
|
log::info!("bound: {} -> {}", device.path, driver_name);
|
|
notify_bound_device(scheme, device, driver_name);
|
|
bound += 1;
|
|
}
|
|
ProbeResult::Deferred { reason } => {
|
|
log::info!("deferred: {} -> {} ({})", device.path, driver_name, reason);
|
|
deferred += 1;
|
|
}
|
|
ProbeResult::Fatal { reason } => {
|
|
log::error!("fatal: {} -> {} ({})", device.path, driver_name, reason);
|
|
}
|
|
_ => {}
|
|
},
|
|
ProbeEvent::BusEnumerated { bus, device_count } => {
|
|
log::info!("bus {} enumerated {} device(s)", bus, device_count);
|
|
}
|
|
ProbeEvent::BusEnumerationFailed { bus, error } => {
|
|
if initfs && *bus == "pci" {
|
|
log::warn!("bus {} enumeration not yet ready (initfs, pcid may still be starting): {:?}", bus, error);
|
|
} else {
|
|
log::error!("bus {} enumeration failed: {:?}", bus, error);
|
|
}
|
|
}
|
|
ProbeEvent::AlreadyBound {
|
|
device,
|
|
driver_name,
|
|
} => {
|
|
log::debug!("already bound: {} -> {}", device.path, driver_name);
|
|
}
|
|
ProbeEvent::NoDriverFound { device } => {
|
|
log::info!("no driver found for {} device {}", device.bus, device.path);
|
|
}
|
|
ProbeEvent::MissingDriver {
|
|
device,
|
|
driver_name,
|
|
} => {
|
|
log::error!(
|
|
"deferred probe lost driver: {} device {} wanted {}",
|
|
device.bus,
|
|
device.path,
|
|
driver_name
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
log::info!(
|
|
"enumeration complete: {} bound, {} deferred ({}ms total)",
|
|
bound,
|
|
deferred,
|
|
enum_duration.as_millis()
|
|
);
|
|
|
|
(bound, deferred)
|
|
}
|
|
|
|
fn notify_bound_device(scheme: &DriverManagerScheme, device: &DeviceId, driver_name: &str) {
|
|
// Notify for both PCI and ACPI devices
|
|
notify_bind(scheme, &device.path, driver_name);
|
|
}
|
|
|
|
fn reset_timeline_log() {
|
|
// Best-effort: truncate or create empty. On scheme filesystems that
|
|
// don't support truncate on existing files, this may fail — that's OK,
|
|
// the append path will handle it.
|
|
match fs::write(BOOT_TIMELINE_PATH, "") {
|
|
Ok(()) => {}
|
|
Err(_) => {
|
|
let _ = fs::remove_file(BOOT_TIMELINE_PATH);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn json_escape(value: &str) -> String {
|
|
let mut escaped = String::with_capacity(value.len());
|
|
for ch in value.chars() {
|
|
match ch {
|
|
'\\' => escaped.push_str("\\\\"),
|
|
'"' => escaped.push_str("\\\""),
|
|
'\n' => escaped.push_str("\\n"),
|
|
'\r' => escaped.push_str("\\r"),
|
|
'\t' => escaped.push_str("\\t"),
|
|
ch if ch.is_control() => escaped.push_str(&format!("\\u{:04x}", ch as u32)),
|
|
ch => escaped.push(ch),
|
|
}
|
|
}
|
|
escaped
|
|
}
|
|
|
|
fn log_timeline(event: &ProbeEvent) {
|
|
let timestamp = std::time::SystemTime::now()
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_millis();
|
|
|
|
let entry = match event {
|
|
ProbeEvent::BusEnumerated { bus, device_count } => format!(
|
|
r#"{{"ts":{},"event":"bus_enumerated","bus":"{}","count":{}}}"#,
|
|
timestamp,
|
|
json_escape(bus),
|
|
device_count
|
|
),
|
|
ProbeEvent::BusEnumerationFailed { bus, error } => format!(
|
|
r#"{{"ts":{},"event":"bus_enumeration_failed","bus":"{}","error":"{}"}}"#,
|
|
timestamp,
|
|
json_escape(bus),
|
|
json_escape(&format!("{error:?}"))
|
|
),
|
|
ProbeEvent::ProbeCompleted {
|
|
device,
|
|
driver_name,
|
|
result,
|
|
} => {
|
|
let status = match result {
|
|
ProbeResult::Bound => "bound",
|
|
ProbeResult::Deferred { .. } => "deferred",
|
|
ProbeResult::Fatal { .. } => "failed",
|
|
ProbeResult::NotSupported => "skipped",
|
|
};
|
|
format!(
|
|
r#"{{"ts":{},"event":"probe","device":"{}","driver":"{}","status":"{}"}}"#,
|
|
timestamp,
|
|
json_escape(&device.path),
|
|
json_escape(driver_name),
|
|
status
|
|
)
|
|
}
|
|
ProbeEvent::NoDriverFound { device } => format!(
|
|
r#"{{"ts":{},"event":"no_driver","bus":"{}","device":"{}"}}"#,
|
|
timestamp,
|
|
json_escape(&device.bus),
|
|
json_escape(&device.path)
|
|
),
|
|
ProbeEvent::AlreadyBound {
|
|
device,
|
|
driver_name,
|
|
} => format!(
|
|
r#"{{"ts":{},"event":"already_bound","bus":"{}","device":"{}","driver":"{}"}}"#,
|
|
timestamp,
|
|
json_escape(&device.bus),
|
|
json_escape(&device.path),
|
|
json_escape(driver_name)
|
|
),
|
|
ProbeEvent::MissingDriver {
|
|
device,
|
|
driver_name,
|
|
} => format!(
|
|
r#"{{"ts":{},"event":"missing_driver","bus":"{}","device":"{}","driver":"{}"}}"#,
|
|
timestamp,
|
|
json_escape(&device.bus),
|
|
json_escape(&device.path),
|
|
json_escape(driver_name)
|
|
),
|
|
};
|
|
|
|
match OpenOptions::new()
|
|
.create(true)
|
|
.append(true)
|
|
.open(BOOT_TIMELINE_PATH)
|
|
{
|
|
Ok(mut file) => {
|
|
if let Err(err) = writeln!(file, "{entry}") {
|
|
// EPIPE or other write errors can occur when /tmp is backed
|
|
// by a scheme that doesn't support append writes, or when the
|
|
// filesystem is not yet fully ready. Log once and suppress
|
|
// all subsequent write errors to avoid log spam.
|
|
static WRITE_ERROR_LOGGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
|
if !WRITE_ERROR_LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
|
|
log::warn!("failed to append boot timeline entry to {BOOT_TIMELINE_PATH}: {err} (suppressing further write errors)");
|
|
}
|
|
}
|
|
}
|
|
Err(err) => {
|
|
// EEXIST (os error 17) can occur when the file already exists
|
|
// but the scheme filesystem doesn't support create+append.
|
|
// EPIPE and other errors occur when /tmp isn't ready.
|
|
// Log once and suppress all subsequent open errors.
|
|
static OPEN_ERROR_LOGGED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
|
|
if !OPEN_ERROR_LOGGED.swap(true, std::sync::atomic::Ordering::Relaxed) {
|
|
log::warn!("failed to open boot timeline log at {BOOT_TIMELINE_PATH}: {err} (suppressing further open errors)");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn run_status() {
|
|
// Print the boot timeline log if it exists.
|
|
match fs::read_to_string(BOOT_TIMELINE_PATH) {
|
|
Ok(content) => {
|
|
if content.trim().is_empty() {
|
|
println!("No boot timeline data found at {}", BOOT_TIMELINE_PATH);
|
|
println!("Driver manager has not completed enumeration yet.");
|
|
return;
|
|
}
|
|
|
|
println!("=== Red Bear OS Driver Manager Status ===");
|
|
println!();
|
|
|
|
let mut bound = 0usize;
|
|
let mut deferred = 0usize;
|
|
let mut failed = 0usize;
|
|
let mut no_driver = 0usize;
|
|
let mut buses = Vec::new();
|
|
|
|
for line in content.lines() {
|
|
if line.trim().is_empty() {
|
|
continue;
|
|
}
|
|
// Parse JSON timeline entries
|
|
if line.contains("\"event\":\"bus_enumerated\"") {
|
|
if let Some(bus) = extract_json_string(line, "bus") {
|
|
if let Some(count) = extract_json_number(line, "count") {
|
|
buses.push((bus, count));
|
|
}
|
|
}
|
|
} else if line.contains("\"status\":\"bound\"") {
|
|
bound += 1;
|
|
} else if line.contains("\"status\":\"deferred\"") {
|
|
deferred += 1;
|
|
} else if line.contains("\"status\":\"failed\"") {
|
|
failed += 1;
|
|
} else if line.contains("\"event\":\"no_driver\"") {
|
|
no_driver += 1;
|
|
}
|
|
}
|
|
|
|
println!("Bus enumeration:");
|
|
for (bus, count) in &buses {
|
|
println!(" {}: {} device(s)", bus, count);
|
|
}
|
|
println!();
|
|
println!("Driver binding:");
|
|
println!(" bound: {}", bound);
|
|
println!(" deferred: {}", deferred);
|
|
println!(" failed: {}", failed);
|
|
println!(" no driver: {}", no_driver);
|
|
println!();
|
|
println!("Timeline log: {}", BOOT_TIMELINE_PATH);
|
|
}
|
|
Err(err) => {
|
|
println!("Cannot read {}: {}", BOOT_TIMELINE_PATH, err);
|
|
println!("Driver manager may not have run yet.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Extract a JSON string value for a given key from a single-line JSON object.
|
|
fn extract_json_string(line: &str, key: &str) -> Option<String> {
|
|
let pattern = format!("\"{}\":\"", key);
|
|
let start = line.find(&pattern)?;
|
|
let value_start = start + pattern.len();
|
|
let end = line[value_start..].find('"')?;
|
|
Some(line[value_start..value_start + end].to_string())
|
|
}
|
|
|
|
/// Extract a JSON number value for a given key from a single-line JSON object.
|
|
fn extract_json_number(line: &str, key: &str) -> Option<usize> {
|
|
let pattern = format!("\"{}\":", key);
|
|
let start = line.find(&pattern)?;
|
|
let value_start = start + pattern.len();
|
|
let rest = &line[value_start..];
|
|
let end = rest.find(|c: char| !c.is_ascii_digit()).unwrap_or(rest.len());
|
|
rest[..end].parse().ok()
|
|
}
|
|
|
|
fn main() {
|
|
log::set_logger(&StderrLogger).ok();
|
|
log::set_max_level(log::LevelFilter::Info);
|
|
|
|
// Install SIGTERM handler for graceful shutdown
|
|
install_sigterm_handler();
|
|
|
|
let args: Vec<String> = env::args().collect();
|
|
let initfs = args.iter().any(|a| a == "--initfs");
|
|
let hotplug_mode = args.iter().any(|a| a == "--hotplug");
|
|
let status_mode = args.iter().any(|a| a == "--status");
|
|
|
|
// --status: print the current device registry from the boot timeline log
|
|
// and exit. This is for diagnostics: "what did driver-manager find?"
|
|
if status_mode {
|
|
run_status();
|
|
return;
|
|
}
|
|
|
|
let config_dir = if initfs {
|
|
"/scheme/initfs/lib/drivers.d"
|
|
} else {
|
|
"/lib/drivers.d"
|
|
};
|
|
|
|
let driver_configs = match DriverConfig::load_all(config_dir) {
|
|
Ok(c) => c,
|
|
Err(e) => {
|
|
log::error!("failed to load driver configs: {}", e);
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
if driver_configs.is_empty() {
|
|
log::warn!("no driver configs found in {}", config_dir);
|
|
process::exit(0);
|
|
}
|
|
|
|
log::info!("loaded {} driver config(s)", driver_configs.len());
|
|
|
|
let manager_config = ManagerConfig {
|
|
max_concurrent_probes: 4,
|
|
deferred_retry_ms: 500,
|
|
async_probe: true,
|
|
};
|
|
|
|
let manager = Arc::new(Mutex::new(DeviceManager::new(manager_config.clone())));
|
|
let scheme = Arc::new(DriverManagerScheme::new());
|
|
|
|
match manager.lock() {
|
|
Ok(mut mgr) => {
|
|
// Register PCI bus first (higher priority — storage, network, GPU).
|
|
// Mirrors Linux's pci_scan_child_bus() via subsys_initcall.
|
|
mgr.register_bus(Box::new(PciBus::new()));
|
|
|
|
// Register ACPI bus for platform/I2C/SPI/GPIO/thermal devices.
|
|
// Mirrors Linux's acpi_bus_scan() which walks the namespace for
|
|
// _HID/_CID/_STA/_CRS. ACPI devices are enumerated from
|
|
// /scheme/acpi/symbols/ which acpid populates from the AML
|
|
// interpreter.
|
|
mgr.register_bus(Box::new(AcpiBus::new()));
|
|
|
|
for dc in &driver_configs {
|
|
mgr.register_driver(Box::new(dc.clone()));
|
|
}
|
|
}
|
|
Err(err) => {
|
|
log::error!("failed to configure driver manager: manager lock poisoned: {err}");
|
|
process::exit(1);
|
|
}
|
|
}
|
|
|
|
let mgr_clone = Arc::clone(&manager);
|
|
let scheme_clone = Arc::clone(&scheme);
|
|
|
|
// Ensure /tmp exists before writing the boot timeline log.
|
|
let _ = std::fs::create_dir_all("/tmp");
|
|
|
|
reset_timeline_log();
|
|
|
|
if manager_config.async_probe {
|
|
let handle = thread::spawn(move || {
|
|
let (bound, deferred) = run_enumeration(&mgr_clone, scheme_clone.as_ref(), initfs);
|
|
log::info!("async enum: {} bound, {} deferred", bound, deferred);
|
|
});
|
|
if handle.join().is_err() {
|
|
log::error!("initial enumeration thread panicked");
|
|
process::exit(1);
|
|
}
|
|
} else {
|
|
let (bound, deferred) = run_enumeration(&manager, scheme.as_ref(), initfs);
|
|
log::info!("enum complete: {} bound, {} deferred", bound, deferred);
|
|
}
|
|
|
|
match scheme::start_scheme_server(Arc::clone(&scheme)) {
|
|
Ok(true) => {
|
|
log::info!("driver-manager: scheme server started successfully");
|
|
}
|
|
Ok(false) => {
|
|
log::warn!("driver-manager: scheme already registered — another instance is active, continuing without scheme server");
|
|
}
|
|
Err(err) => {
|
|
log::error!("{err}");
|
|
process::exit(1);
|
|
}
|
|
}
|
|
|
|
if hotplug_mode {
|
|
log::info!("entering hotplug event loop");
|
|
hotplug::run_hotplug_loop(manager.clone(), scheme.clone(), 2000);
|
|
idle_forever();
|
|
}
|
|
|
|
let max_retries = 3u32;
|
|
for retry in 1..=max_retries {
|
|
if SHUTDOWN_REQUESTED.load(Ordering::SeqCst) {
|
|
log::info!("driver-manager: SIGTERM received during deferred retry, shutting down");
|
|
graceful_shutdown();
|
|
process::exit(0);
|
|
}
|
|
|
|
// Check for crashed drivers during retry loop
|
|
reap_all_drivers(&driver_configs);
|
|
|
|
thread::sleep(Duration::from_millis(500));
|
|
|
|
let retry_events = match manager.lock() {
|
|
Ok(mut mgr) => mgr.retry_deferred(),
|
|
Err(err) => {
|
|
log::error!("failed to retry deferred probes: manager lock poisoned: {err}");
|
|
process::exit(1);
|
|
}
|
|
};
|
|
|
|
let mut remaining = 0;
|
|
let mut newly_bound = 0;
|
|
|
|
for event in &retry_events {
|
|
log_timeline(event);
|
|
if let ProbeEvent::ProbeCompleted {
|
|
device,
|
|
driver_name,
|
|
result,
|
|
} = event
|
|
{
|
|
match result {
|
|
ProbeResult::Bound => {
|
|
newly_bound += 1;
|
|
notify_bound_device(scheme.as_ref(), device, driver_name);
|
|
}
|
|
ProbeResult::Deferred { .. } => remaining += 1,
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
if remaining == 0 {
|
|
log::info!("all deferred resolved after {} retries", retry);
|
|
idle_forever();
|
|
}
|
|
|
|
if newly_bound > 0 {
|
|
log::info!(
|
|
"retry #{}: {} new, {} remaining",
|
|
retry,
|
|
newly_bound,
|
|
remaining
|
|
);
|
|
}
|
|
}
|
|
|
|
log::warn!("deferred probe retry limit reached");
|
|
idle_forever();
|
|
}
|
|
|
|
fn idle_forever() -> ! {
|
|
log::info!("driver-manager: entering persistent idle loop");
|
|
loop {
|
|
thread::sleep(Duration::from_secs(5));
|
|
if SHUTDOWN_REQUESTED.load(Ordering::SeqCst) {
|
|
log::info!("driver-manager: SIGTERM received, performing graceful shutdown");
|
|
graceful_shutdown();
|
|
process::exit(0);
|
|
}
|
|
// Periodically check for exited child drivers
|
|
reap_all_drivers(&[]);
|
|
}
|
|
}
|
|
|
|
/// Poll all driver configs for exited children and log the results.
|
|
fn reap_all_drivers(driver_configs: &[DriverConfig]) {
|
|
for dc in driver_configs {
|
|
let exited = dc.reap_exited_children();
|
|
for (device_key, driver_name, code) in &exited {
|
|
log::warn!(
|
|
"reaped crashed driver: {} for device {} (exit {})",
|
|
driver_name,
|
|
device_key,
|
|
code
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn graceful_shutdown() {
|
|
// The DeviceManager and spawned children are managed by DriverConfig instances
|
|
// which track their child processes. On shutdown, we log and exit cleanly.
|
|
// Child processes will be orphaned but the kernel reaps them.
|
|
log::info!("driver-manager: clean shutdown complete");
|
|
}
|