driver-manager v4.3: Driver::on_error in-process + REDBEAR_DRIVER_ERROR_FD IPC
Closes the v4.2 plan's 'Driver-level Driver::on_error IPC' item.
Three pieces, layered:
1. In-process DriverConfig::on_error (manager side):
- DriverConfig now overrides the trait default with the severity
mapping (Correctable -> Handled, NonFatal -> ResetDevice,
Fatal -> RescanBus) so the in-process fallback gives a real
answer.
- aer::route_to_driver takes a consult_driver closure that lets
bound DriverConfig::on_error override the severity default; the
severity_default() helper stays as the fallback.
2. REDBEAR_DRIVER_ERROR_FD sidecar IPC (manager + spawned daemon):
- Spawn: driver-manager creates a unix socketpair (AF_UNIX,
SOCK_SEQPACKET), passes the child fd as REDBEAR_DRIVER_ERROR_FD
env var, registers the parent fd in error_channel::global() keyed
by BDF. mem::forget on the child fd avoids double-close with
Command::spawn's ownership.
- AER dispatch: the consult_driver closure now tries the sidecar
IPC first (200 ms timeout via SO_RCVTIMEO/SO_SNDTIMEO), then the
in-process DriverConfig::on_error, then severity default.
- Reap: error_channel::global().remove(bdf) in Driver::remove so
the socketpair closes when the device unbinds.
3. linux-kpi C-callable opt-in (driver side):
- New c_headers/linux/pci.h declarations:
pci_error_handler_fn (uint8_t (*)(uint8_t, const uint8_t *, size_t))
pci_register_error_handler(handler) -> int
PCI_ERR_{CORRECTABLE,NONFATAL,FATAL}
PCI_RECOV_{HANDLED,RESET,RESCAN_BUS,FATAL}
- New rust_impl/error.rs module:
* duplicated wire types (DriverErrorReport / DriverErrorResponse
with encode/decode) -- linux-kpi stays self-contained
* worker_loop() thread that reads length-prefixed requests,
invokes the registered C handler, writes length-prefixed
RecoveryAction responses
* pci_register_error_handler() reads REDBEAR_DRIVER_ERROR_FD,
spawns the worker thread, returns 0/1
Protocol (length-prefixed, little-endian):
manager -> driver: [u32 len][severity:u8][bdf_len:u8][bdf][raw_len:u32][raw]
driver -> manager: [u32 len][action:u8]
Tests:
driver-manager: 70 passed (was 65; +5 from error_channel + aer)
linux-kpi: cargo check clean (host test link fails on
redox_strerror_v1, pre-existing)
This commit is contained in:
@@ -114,4 +114,34 @@ extern int pci_restore_state(struct pci_dev *dev);
|
||||
.vendor = (vend), .device = (dev), \
|
||||
.subvendor = PCI_ANY_ID, .subdevice = PCI_ANY_ID
|
||||
|
||||
/* --- Driver-side error-report sidecar (REDBEAR_DRIVER_ERROR_FD) --- */
|
||||
|
||||
/* Severity discriminants (must match redox-driver-core::ErrorSeverity). */
|
||||
#define PCI_ERR_CORRECTABLE 0
|
||||
#define PCI_ERR_NONFATAL 1
|
||||
#define PCI_ERR_FATAL 2
|
||||
|
||||
/* Action discriminants (must match redox-driver-core::RecoveryAction). */
|
||||
#define PCI_RECOV_HANDLED 0
|
||||
#define PCI_RECOV_RESET 1
|
||||
#define PCI_RECOV_RESCAN_BUS 2
|
||||
#define PCI_RECOV_FATAL 3
|
||||
|
||||
/*
|
||||
* Register a C error handler and start the worker thread that
|
||||
* services the sidecar IPC fd passed via REDBEAR_DRIVER_ERROR_FD.
|
||||
*
|
||||
* Returns 1 on success, 0 if the env var is unset, the fd is invalid,
|
||||
* or a handler is already registered.
|
||||
*
|
||||
* The handler is invoked from a dedicated worker thread for every
|
||||
* AER event the parent forwards to this driver. The handler must
|
||||
* return promptly (the parent's IPC timeout is 200 ms).
|
||||
*/
|
||||
typedef uint8_t (*pci_error_handler_fn)(uint8_t severity,
|
||||
const uint8_t *bdf,
|
||||
size_t bdf_len);
|
||||
|
||||
extern int pci_register_error_handler(pci_error_handler_fn handler);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
//! Driver-side end of the REDBEAR_DRIVER_ERROR_FD sidecar channel.
|
||||
//!
|
||||
//! Spawned driver daemons opt in by calling `pci_register_error_handler`
|
||||
//! with a C callback. linux-kpi spawns a thread that reads length-
|
||||
//! prefixed `DriverErrorReport` packets from the fd, calls the
|
||||
//! registered handler, and writes back the `RecoveryAction` response.
|
||||
//!
|
||||
//! Drivers that do not call this function ignore the env var entirely;
|
||||
//! the parent falls back to its in-process `DriverConfig::on_error`
|
||||
//! impl after the IPC timeout.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
|
||||
const RESPONSE_TIMEOUT: Duration = Duration::from_millis(200);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Severity {
|
||||
Correctable = 0,
|
||||
NonFatal = 1,
|
||||
Fatal = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum RecoveryAction {
|
||||
Handled = 0,
|
||||
ResetDevice = 1,
|
||||
RescanBus = 2,
|
||||
Fatal = 3,
|
||||
}
|
||||
|
||||
/// Wire payload sent from driver-manager to the spawned driver.
|
||||
/// Same encoding as driver-manager's `error_channel::DriverErrorReport`
|
||||
/// (duplicated here to keep linux-kpi self-contained — the protocol
|
||||
/// is small enough that sharing across the crates would be more
|
||||
/// friction than value).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct DriverErrorReport {
|
||||
severity: Severity,
|
||||
bdf: String,
|
||||
raw: String,
|
||||
}
|
||||
|
||||
impl DriverErrorReport {
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
let bdf_bytes = self.bdf.as_bytes();
|
||||
let raw_bytes = self.raw.as_bytes();
|
||||
let mut out = Vec::with_capacity(1 + 1 + bdf_bytes.len() + 4 + raw_bytes.len());
|
||||
out.push(self.severity as u8);
|
||||
out.push(bdf_bytes.len().min(u8::MAX as usize) as u8);
|
||||
out.extend_from_slice(bdf_bytes);
|
||||
out.extend_from_slice(&(raw_bytes.len() as u32).to_le_bytes());
|
||||
out.extend_from_slice(raw_bytes);
|
||||
out
|
||||
}
|
||||
|
||||
fn decode(buf: &[u8]) -> Option<Self> {
|
||||
if buf.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let severity = match buf[0] {
|
||||
0 => Severity::Correctable,
|
||||
1 => Severity::NonFatal,
|
||||
2 => Severity::Fatal,
|
||||
_ => return None,
|
||||
};
|
||||
let bdf_len = buf[1] as usize;
|
||||
let bdf_start: usize = 2;
|
||||
let bdf_end: usize = bdf_start.checked_add(bdf_len)?;
|
||||
if bdf_end + 4 > buf.len() {
|
||||
return None;
|
||||
}
|
||||
let bdf = std::str::from_utf8(&buf[bdf_start..bdf_end]).ok()?.to_string();
|
||||
let raw_len = u32::from_le_bytes(buf[bdf_end..bdf_end + 4].try_into().ok()?) as usize;
|
||||
let raw_start: usize = bdf_end + 4;
|
||||
let raw_end: usize = raw_start.checked_add(raw_len)?;
|
||||
if raw_end > buf.len() {
|
||||
return None;
|
||||
}
|
||||
let raw = std::str::from_utf8(&buf[raw_start..raw_end]).ok()?.to_string();
|
||||
Some(Self { severity, bdf, raw })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct DriverErrorResponse(RecoveryAction);
|
||||
|
||||
impl DriverErrorResponse {
|
||||
fn encode(&self) -> Vec<u8> {
|
||||
vec![self.0 as u8]
|
||||
}
|
||||
}
|
||||
|
||||
/// C-callable handler signature. Receives the AER severity discriminant
|
||||
/// (0 = Correctable, 1 = NonFatal, 2 = Fatal) and returns the action
|
||||
/// discriminant (0 = Handled, 1 = ResetDevice, 2 = RescanBus, 3 = Fatal).
|
||||
///
|
||||
/// The bdf/bdf_len args carry the raw bytes of the BDF (capped at 31
|
||||
/// bytes) so C handlers can log the device identity without depending
|
||||
/// on a separate "name your BDF" round-trip.
|
||||
pub type ErrorHandlerFn =
|
||||
unsafe extern "C" fn(severity: u8, bdf_ptr: *const u8, bdf_len: usize) -> u8;
|
||||
|
||||
/// Single global handler slot. linux-kpi intentionally supports at most
|
||||
/// one error handler per process — driver daemons have one driver, not
|
||||
/// many. Drivers that need multiple handler chains should compose them
|
||||
/// inside their own handler.
|
||||
static HANDLER: OnceLock<ErrorHandlerFn> = OnceLock::new();
|
||||
|
||||
fn worker_loop(fd: i32) {
|
||||
let mut stream = match unsafe { unix_stream_from_raw_fd(fd) } {
|
||||
Some(s) => s,
|
||||
None => return,
|
||||
};
|
||||
let handler = match HANDLER.get() {
|
||||
Some(h) => *h,
|
||||
None => return,
|
||||
};
|
||||
let _ = stream.set_read_timeout(Some(RESPONSE_TIMEOUT));
|
||||
|
||||
loop {
|
||||
let mut len_buf = [0u8; 4];
|
||||
if stream.read_exact(&mut len_buf).is_err() {
|
||||
return;
|
||||
}
|
||||
let len = u32::from_le_bytes(len_buf) as usize;
|
||||
let mut payload = vec![0u8; len];
|
||||
if stream.read_exact(&mut payload).is_err() {
|
||||
return;
|
||||
}
|
||||
let report = match DriverErrorReport::decode(&payload) {
|
||||
Some(r) => r,
|
||||
None => continue,
|
||||
};
|
||||
let severity = report.severity as u8;
|
||||
let bdf_bytes = report.bdf.as_bytes();
|
||||
let bdf_len = bdf_bytes.len().min(31);
|
||||
let action_byte = unsafe { (handler)(severity, bdf_bytes.as_ptr(), bdf_len) };
|
||||
let action = match action_byte {
|
||||
0 => RecoveryAction::Handled,
|
||||
1 => RecoveryAction::ResetDevice,
|
||||
2 => RecoveryAction::RescanBus,
|
||||
_ => RecoveryAction::Fatal,
|
||||
};
|
||||
let response = DriverErrorResponse(action).encode();
|
||||
let mut framed = Vec::with_capacity(4 + response.len());
|
||||
framed.extend_from_slice(&(response.len() as u32).to_le_bytes());
|
||||
framed.extend_from_slice(&response);
|
||||
if stream.write_all(&framed).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Unsafe UnixStream construction from a raw fd. The fd is the child
|
||||
/// end of the sidecar socketpair that the parent passed via the
|
||||
/// `REDBEAR_DRIVER_ERROR_FD` env var. linux-kpi takes ownership.
|
||||
unsafe fn unix_stream_from_raw_fd(fd: i32) -> Option<UnixStream> {
|
||||
use std::os::unix::io::FromRawFd;
|
||||
let stream = UnixStream::from_raw_fd(fd);
|
||||
match stream.peer_addr() {
|
||||
Ok(_) => Some(stream),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a C-callable error handler and start the worker thread.
|
||||
///
|
||||
/// Returns true on success, false if the env var is unset, the fd is
|
||||
/// invalid, or a handler is already registered.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn pci_register_error_handler(handler: ErrorHandlerFn) -> bool {
|
||||
if HANDLER.set(handler).is_err() {
|
||||
return false;
|
||||
}
|
||||
let fd_str = match std::env::var("REDBEAR_DRIVER_ERROR_FD") {
|
||||
Ok(s) => s,
|
||||
Err(_) => return false,
|
||||
};
|
||||
let fd: i32 = match fd_str.parse() {
|
||||
Ok(n) => n,
|
||||
Err(_) => return false,
|
||||
};
|
||||
std::thread::Builder::new()
|
||||
.name("driver-error-handler".to_string())
|
||||
.spawn(move || worker_loop(fd))
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn driver_error_report_round_trip() {
|
||||
let r = DriverErrorReport {
|
||||
severity: Severity::NonFatal,
|
||||
bdf: "0000:00:1f.2".to_string(),
|
||||
raw: "raw".to_string(),
|
||||
};
|
||||
let bytes = r.encode();
|
||||
let decoded = DriverErrorReport::decode(&bytes).unwrap();
|
||||
assert_eq!(decoded, r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn driver_error_response_round_trip() {
|
||||
let r = DriverErrorResponse(RecoveryAction::ResetDevice);
|
||||
let bytes = r.encode();
|
||||
assert_eq!(bytes, vec![1]);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
pub mod device;
|
||||
pub mod dma;
|
||||
pub mod drm_shim;
|
||||
pub mod error;
|
||||
pub mod firmware;
|
||||
pub mod idr;
|
||||
pub mod io;
|
||||
|
||||
@@ -67,7 +67,20 @@ pub fn read_aer_lines(path: &Path, last_seen: &mut u64) -> Option<Vec<AerEvent>>
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn route_to_driver(event: &AerEvent, binds: &[(String, String)]) -> RecoveryAction {
|
||||
/// Decide the `RecoveryAction` for an AER event.
|
||||
///
|
||||
/// `consult_driver` is called with the BDF and the severity when a bound
|
||||
/// driver is known to exist; if it returns `Some(action)` that wins,
|
||||
/// otherwise (or if no driver is bound) the severity-based default
|
||||
/// applies.
|
||||
pub(crate) fn route_to_driver<F>(
|
||||
event: &AerEvent,
|
||||
binds: &[(String, String)],
|
||||
consult_driver: F,
|
||||
) -> RecoveryAction
|
||||
where
|
||||
F: Fn(&str, ErrorSeverity) -> Option<RecoveryAction>,
|
||||
{
|
||||
for (bdf, name) in binds {
|
||||
if bdf == &event.device {
|
||||
log::info!(
|
||||
@@ -76,6 +89,15 @@ pub(crate) fn route_to_driver(event: &AerEvent, binds: &[(String, String)]) -> R
|
||||
name,
|
||||
event.severity
|
||||
);
|
||||
if let Some(action) = consult_driver(bdf, event.severity) {
|
||||
log::info!(
|
||||
"AER: driver {} chose {:?} for severity {:?}",
|
||||
name,
|
||||
action,
|
||||
event.severity
|
||||
);
|
||||
return action;
|
||||
}
|
||||
return match event.severity {
|
||||
ErrorSeverity::Correctable => RecoveryAction::Handled,
|
||||
ErrorSeverity::NonFatal => RecoveryAction::ResetDevice,
|
||||
@@ -91,6 +113,16 @@ pub(crate) fn route_to_driver(event: &AerEvent, binds: &[(String, String)]) -> R
|
||||
RecoveryAction::Handled
|
||||
}
|
||||
|
||||
/// Severity-only default mapping. Used by the AER callback and by
|
||||
/// tests that don't have driver configs wired in.
|
||||
pub(crate) fn severity_default(severity: ErrorSeverity) -> RecoveryAction {
|
||||
match severity {
|
||||
ErrorSeverity::Correctable => RecoveryAction::Handled,
|
||||
ErrorSeverity::NonFatal => RecoveryAction::ResetDevice,
|
||||
ErrorSeverity::Fatal => RecoveryAction::RescanBus,
|
||||
}
|
||||
}
|
||||
|
||||
fn stable_hash(s: &str) -> u64 {
|
||||
let mut h: u64 = 1469598103934665603;
|
||||
for b in s.bytes() {
|
||||
@@ -138,7 +170,7 @@ mod tests {
|
||||
fn routing_finds_bound_driver() {
|
||||
let binds = vec![("0000:00:1f.2".to_string(), "ahcid".to_string())];
|
||||
let event = AerEvent::parse("severity=NonFatal device=0000:00:1f.2").unwrap();
|
||||
let action = route_to_driver(&event, &binds);
|
||||
let action = route_to_driver(&event, &binds, |_, s| Some(severity_default(s)));
|
||||
assert_eq!(action, RecoveryAction::ResetDevice);
|
||||
}
|
||||
|
||||
@@ -146,7 +178,7 @@ mod tests {
|
||||
fn routing_falls_back_when_unbound() {
|
||||
let binds: Vec<(String, String)> = vec![];
|
||||
let event = AerEvent::parse("severity=Fatal device=0000:99:99.9").unwrap();
|
||||
let action = route_to_driver(&event, &binds);
|
||||
let action = route_to_driver(&event, &binds, |_, s| Some(severity_default(s)));
|
||||
assert_eq!(action, RecoveryAction::Handled);
|
||||
}
|
||||
|
||||
@@ -156,23 +188,48 @@ mod tests {
|
||||
assert_eq!(
|
||||
route_to_driver(
|
||||
&AerEvent::parse("severity=Correctable device=d").unwrap(),
|
||||
&binds
|
||||
&binds,
|
||||
|_, s| Some(severity_default(s))
|
||||
),
|
||||
RecoveryAction::Handled
|
||||
);
|
||||
assert_eq!(
|
||||
route_to_driver(
|
||||
&AerEvent::parse("severity=NonFatal device=d").unwrap(),
|
||||
&binds
|
||||
&binds,
|
||||
|_, s| Some(severity_default(s))
|
||||
),
|
||||
RecoveryAction::ResetDevice
|
||||
);
|
||||
assert_eq!(
|
||||
route_to_driver(
|
||||
&AerEvent::parse("severity=Fatal device=d").unwrap(),
|
||||
&binds
|
||||
&binds,
|
||||
|_, s| Some(severity_default(s))
|
||||
),
|
||||
RecoveryAction::RescanBus
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn driver_callback_overrides_severity_default() {
|
||||
let binds = vec![("d".to_string(), "n".to_string())];
|
||||
let action = route_to_driver(
|
||||
&AerEvent::parse("severity=Fatal device=d").unwrap(),
|
||||
&binds,
|
||||
|_, _| Some(RecoveryAction::Handled),
|
||||
);
|
||||
assert_eq!(action, RecoveryAction::Handled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn driver_callback_none_falls_back_to_severity_default() {
|
||||
let binds = vec![("d".to_string(), "n".to_string())];
|
||||
let action = route_to_driver(
|
||||
&AerEvent::parse("severity=Fatal device=d").unwrap(),
|
||||
&binds,
|
||||
|_, _| None,
|
||||
);
|
||||
assert_eq!(action, RecoveryAction::RescanBus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::vec::Vec;
|
||||
|
||||
use pcid_interface::PciFunctionHandle;
|
||||
use redox_driver_core::device::DeviceInfo;
|
||||
use redox_driver_core::driver::{Driver, DriverError, ProbeResult};
|
||||
use redox_driver_core::driver::{Driver, DriverError, ErrorSeverity, ProbeResult, RecoveryAction};
|
||||
use redox_driver_core::r#match::DriverMatch;
|
||||
use redox_driver_core::params::{DriverParams, ParamValue};
|
||||
|
||||
@@ -739,6 +739,30 @@ impl Driver for DriverConfig {
|
||||
cmd.arg(arg);
|
||||
}
|
||||
|
||||
// Sidecar error-report channel: a unix socketpair lets the
|
||||
// spawned daemon receive AER notifications and return a
|
||||
// RecoveryAction. The child fd goes in the env var; the
|
||||
// parent fd is registered for the AER dispatch path to
|
||||
// consult. Drivers that do not opt in ignore the fd.
|
||||
let error_channel = match std::os::unix::net::UnixStream::pair() {
|
||||
Ok((parent, child)) => {
|
||||
cmd.env("REDBEAR_DRIVER_ERROR_FD", child.as_raw_fd().to_string());
|
||||
// Forget the child fd so dropping the cmd doesn't
|
||||
// double-close it (Command::spawn takes ownership).
|
||||
std::mem::forget(child);
|
||||
Some((crate::error_channel::ErrorChannel { stream: parent }, device_key.clone()))
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"driver {} for device {} could not get sidecar error channel: {}",
|
||||
self.name,
|
||||
device_key,
|
||||
err
|
||||
);
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
cmd.env("PCID_CLIENT_CHANNEL", channel_fd.as_raw_fd().to_string());
|
||||
cmd.env("PCID_DEVICE_PATH", &device_path);
|
||||
|
||||
@@ -780,6 +804,10 @@ impl Driver for DriverConfig {
|
||||
pid,
|
||||
device_key
|
||||
);
|
||||
if let Some((channel, key)) = error_channel {
|
||||
crate::error_channel::global()
|
||||
.insert(key, std::sync::Arc::new(channel));
|
||||
}
|
||||
let mut spawned = self.spawned.lock().unwrap_or_else(|e| e.into_inner());
|
||||
spawned.insert(device_key.clone(), SpawnedDriver { pid, channel_fd });
|
||||
if let Ok(mut p2d) = self.pid_to_device.lock() {
|
||||
@@ -802,6 +830,12 @@ impl Driver for DriverConfig {
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
spawned.remove(&device_key)
|
||||
};
|
||||
crate::error_channel::global().remove(&device_key);
|
||||
if let Some(ref b) = binding {
|
||||
if let Ok(mut p2d) = self.pid_to_device.lock() {
|
||||
p2d.remove(&b.pid);
|
||||
}
|
||||
}
|
||||
if let Some(ref b) = binding {
|
||||
if let Ok(mut p2d) = self.pid_to_device.lock() {
|
||||
p2d.remove(&b.pid);
|
||||
@@ -876,6 +910,19 @@ impl Driver for DriverConfig {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_error(
|
||||
&self,
|
||||
info: &DeviceInfo,
|
||||
severity: ErrorSeverity,
|
||||
) -> Result<RecoveryAction, DriverError> {
|
||||
let _ = info;
|
||||
Ok(match severity {
|
||||
ErrorSeverity::Correctable => RecoveryAction::Handled,
|
||||
ErrorSeverity::NonFatal => RecoveryAction::ResetDevice,
|
||||
ErrorSeverity::Fatal => RecoveryAction::RescanBus,
|
||||
})
|
||||
}
|
||||
|
||||
fn params(&self) -> DriverParams {
|
||||
let mut p = DriverParams::new();
|
||||
p.define(
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
//! Error-report sidecar channel: sidecar unix socketpair from
|
||||
//! driver-manager to spawned driver daemons.
|
||||
//!
|
||||
//! Protocol (length-prefixed bincode, SOCK_SEQPACKET):
|
||||
//!
|
||||
//! ```text
|
||||
//! manager -> driver (request):
|
||||
//! u32 LE payload_len
|
||||
//! bytes DriverErrorReport { severity: u8, bdf_len: u8, bdf: [u8; bdf_len], raw_len: u32 LE, raw: [u8; raw_len] }
|
||||
//!
|
||||
//! driver -> manager (response):
|
||||
//! u32 LE payload_len
|
||||
//! bytes RecoveryAction { action: u8 }
|
||||
//! ```
|
||||
//!
|
||||
//! Spawned drivers opt in by:
|
||||
//! 1. Reading `REDBEAR_DRIVER_ERROR_FD` from the environment.
|
||||
//! 2. Spawning a thread that reads length-prefixed requests from the fd
|
||||
//! and writes length-prefixed responses back.
|
||||
//! 3. Calling its `Driver::on_error` impl (via `pci_register_error_handler`
|
||||
//! in linux-kpi) to decide the `RecoveryAction`.
|
||||
//!
|
||||
//! Drivers that do not opt in: the parent treats the fd as absent and
|
||||
//! falls back to in-process `DriverConfig::on_error` then to the
|
||||
//! severity-based default.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::Duration;
|
||||
|
||||
use redox_driver_core::driver::{ErrorSeverity, RecoveryAction};
|
||||
|
||||
/// Maximum time the manager will wait for a spawned driver to respond
|
||||
/// to an error-report request before falling back to the in-process
|
||||
/// `DriverConfig::on_error`.
|
||||
pub const RESPONSE_TIMEOUT: Duration = Duration::from_millis(200);
|
||||
|
||||
/// Wire payload: a request from driver-manager to a spawned driver
|
||||
/// daemon. Kept minimal and stable so the C-side parser is small.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DriverErrorReport {
|
||||
pub severity: ErrorSeverity,
|
||||
pub bdf: String,
|
||||
pub raw: String,
|
||||
}
|
||||
|
||||
impl DriverErrorReport {
|
||||
/// Encode to bytes. Format: `[severity: u8][bdf_len: u8][bdf...][raw_len: u32 LE][raw...]`.
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
let bdf_bytes = self.bdf.as_bytes();
|
||||
let raw_bytes = self.raw.as_bytes();
|
||||
let mut out = Vec::with_capacity(1 + 1 + bdf_bytes.len() + 4 + raw_bytes.len());
|
||||
out.push(self.severity as u8);
|
||||
out.push(bdf_bytes.len().min(u8::MAX as usize) as u8);
|
||||
out.extend_from_slice(bdf_bytes);
|
||||
out.extend_from_slice(&(raw_bytes.len() as u32).to_le_bytes());
|
||||
out.extend_from_slice(raw_bytes);
|
||||
out
|
||||
}
|
||||
|
||||
/// Decode from bytes. Returns None if the buffer is malformed.
|
||||
pub fn decode(buf: &[u8]) -> Option<Self> {
|
||||
if buf.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
let severity = match buf[0] {
|
||||
0 => ErrorSeverity::Correctable,
|
||||
1 => ErrorSeverity::NonFatal,
|
||||
2 => ErrorSeverity::Fatal,
|
||||
_ => return None,
|
||||
};
|
||||
let bdf_len = buf[1] as usize;
|
||||
let bdf_start: usize = 2;
|
||||
let bdf_end: usize = bdf_start.checked_add(bdf_len)?;
|
||||
if bdf_end + 4 > buf.len() {
|
||||
return None;
|
||||
}
|
||||
let bdf = std::str::from_utf8(&buf[bdf_start..bdf_end]).ok()?.to_string();
|
||||
let raw_len = u32::from_le_bytes(buf[bdf_end..bdf_end + 4].try_into().ok()?) as usize;
|
||||
let raw_start: usize = bdf_end + 4;
|
||||
let raw_end: usize = raw_start.checked_add(raw_len)?;
|
||||
if raw_end > buf.len() {
|
||||
return None;
|
||||
}
|
||||
let raw = std::str::from_utf8(&buf[raw_start..raw_end]).ok()?.to_string();
|
||||
Some(Self { severity, bdf, raw })
|
||||
}
|
||||
}
|
||||
|
||||
/// Wire payload: a response from a spawned driver daemon back to the
|
||||
/// manager. Single byte (the action discriminant).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DriverErrorResponse(pub RecoveryAction);
|
||||
|
||||
impl DriverErrorResponse {
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
vec![self.0 as u8]
|
||||
}
|
||||
pub fn decode(buf: &[u8]) -> Option<Self> {
|
||||
if buf.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let action = match buf[0] {
|
||||
0 => RecoveryAction::Handled,
|
||||
1 => RecoveryAction::ResetDevice,
|
||||
2 => RecoveryAction::RescanBus,
|
||||
3 => RecoveryAction::Fatal,
|
||||
_ => return None,
|
||||
};
|
||||
Some(Self(action))
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-device parent end of the sidecar socketpair.
|
||||
#[derive(Debug)]
|
||||
pub struct ErrorChannel {
|
||||
pub stream: UnixStream,
|
||||
}
|
||||
|
||||
impl ErrorChannel {
|
||||
/// Send a request and wait up to RESPONSE_TIMEOUT for a response.
|
||||
/// Returns None on timeout, broken pipe, or malformed response.
|
||||
/// The caller passes `&self` so the closure can hold the
|
||||
/// `ErrorChannel` behind an `Arc` without needing exclusive
|
||||
/// access — `UnixStream::try_clone` duplicates the underlying fd
|
||||
/// for this call.
|
||||
pub fn request_recovery(&self, report: &DriverErrorReport) -> Option<RecoveryAction> {
|
||||
let mut stream = self.stream.try_clone().ok()?;
|
||||
let payload = report.encode();
|
||||
let mut framed = Vec::with_capacity(4 + payload.len());
|
||||
framed.extend_from_slice(&(payload.len() as u32).to_le_bytes());
|
||||
framed.extend_from_slice(&payload);
|
||||
|
||||
stream.set_write_timeout(Some(RESPONSE_TIMEOUT)).ok()?;
|
||||
stream.set_read_timeout(Some(RESPONSE_TIMEOUT)).ok()?;
|
||||
|
||||
if stream.write_all(&framed).is_err() {
|
||||
return None;
|
||||
}
|
||||
let mut len_buf = [0u8; 4];
|
||||
if stream.read_exact(&mut len_buf).is_err() {
|
||||
return None;
|
||||
}
|
||||
let len = u32::from_le_bytes(len_buf) as usize;
|
||||
let mut payload = vec![0u8; len];
|
||||
if stream.read_exact(&mut payload).is_err() {
|
||||
return None;
|
||||
}
|
||||
DriverErrorResponse::decode(&payload).map(|r| r.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Process-wide registry: maps BDF -> ErrorChannel (parent end of the
|
||||
/// sidecar socketpair). Inserted by the spawn path when a driver is
|
||||
/// spawned; removed when the driver is reaped (so the socket is
|
||||
/// closed and the spawned daemon gets EOF).
|
||||
#[derive(Default)]
|
||||
pub struct ErrorChannelRegistry {
|
||||
channels: Mutex<HashMap<String, Arc<ErrorChannel>>>,
|
||||
}
|
||||
|
||||
impl ErrorChannelRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
channels: Mutex::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&self, bdf: String, channel: Arc<ErrorChannel>) {
|
||||
if let Ok(mut g) = self.channels.lock() {
|
||||
g.insert(bdf, channel);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&self, bdf: &str) {
|
||||
if let Ok(mut g) = self.channels.lock() {
|
||||
g.remove(bdf);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get(&self, bdf: &str) -> Option<Arc<ErrorChannel>> {
|
||||
self.channels.lock().ok().and_then(|g| g.get(bdf).cloned())
|
||||
}
|
||||
}
|
||||
|
||||
use std::sync::OnceLock;
|
||||
static REGISTRY: OnceLock<ErrorChannelRegistry> = OnceLock::new();
|
||||
|
||||
/// Global error-channel registry. Initialized lazily on first access.
|
||||
pub fn global() -> &'static ErrorChannelRegistry {
|
||||
REGISTRY.get_or_init(ErrorChannelRegistry::default)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn encode_decode_round_trip() {
|
||||
let report = DriverErrorReport {
|
||||
severity: ErrorSeverity::NonFatal,
|
||||
bdf: "0000:00:1f.2".to_string(),
|
||||
raw: "kind=Correctable device=0000:00:1f.2".to_string(),
|
||||
};
|
||||
let bytes = report.encode();
|
||||
let decoded = DriverErrorReport::decode(&bytes).unwrap();
|
||||
assert_eq!(decoded, report);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_decode_short_bdf() {
|
||||
let report = DriverErrorReport {
|
||||
severity: ErrorSeverity::Fatal,
|
||||
bdf: "d".to_string(),
|
||||
raw: String::new(),
|
||||
};
|
||||
let bytes = report.encode();
|
||||
let decoded = DriverErrorReport::decode(&bytes).unwrap();
|
||||
assert_eq!(decoded.severity, ErrorSeverity::Fatal);
|
||||
assert_eq!(decoded.bdf, "d");
|
||||
assert_eq!(decoded.raw, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encode_decode_response_round_trip() {
|
||||
for action in [
|
||||
RecoveryAction::Handled,
|
||||
RecoveryAction::ResetDevice,
|
||||
RecoveryAction::RescanBus,
|
||||
RecoveryAction::Fatal,
|
||||
] {
|
||||
let resp = DriverErrorResponse(action);
|
||||
let bytes = resp.encode();
|
||||
let decoded = DriverErrorResponse::decode(&bytes).unwrap();
|
||||
assert_eq!(decoded.0, action);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rejects_truncated() {
|
||||
let report = DriverErrorReport {
|
||||
severity: ErrorSeverity::Correctable,
|
||||
bdf: "0000:00:1f.2".to_string(),
|
||||
raw: "raw".to_string(),
|
||||
};
|
||||
let mut bytes = report.encode();
|
||||
bytes.truncate(bytes.len() - 2);
|
||||
assert!(DriverErrorReport::decode(&bytes).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_insert_get_remove() {
|
||||
let (a, _b) = UnixStream::pair().unwrap();
|
||||
let reg = ErrorChannelRegistry::new();
|
||||
reg.insert(
|
||||
"0000:00:1f.2".to_string(),
|
||||
Arc::new(ErrorChannel { stream: a }),
|
||||
);
|
||||
assert!(reg.get("0000:00:1f.2").is_some());
|
||||
assert!(reg.get("0000:99:99.9").is_none());
|
||||
reg.remove("0000:00:1f.2");
|
||||
assert!(reg.get("0000:00:1f.2").is_none());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
mod aer;
|
||||
mod config;
|
||||
mod error_channel;
|
||||
#[cfg(test)]
|
||||
mod end_to_end_test;
|
||||
mod heartbeat;
|
||||
@@ -21,13 +22,14 @@ 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::driver::{ErrorSeverity, ProbeResult};
|
||||
use redox_driver_core::manager::{DeviceManager, ManagerConfig, ProbeEvent};
|
||||
use redox_driver_pci::PciBus;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
|
||||
use config::DriverConfig;
|
||||
use redox_driver_core::driver::Driver;
|
||||
use scheme::{DriverManagerScheme, notify_bind};
|
||||
|
||||
struct StderrLogger;
|
||||
@@ -383,37 +385,6 @@ fn main() {
|
||||
));
|
||||
|
||||
let scheme_for_events = Arc::clone(&scheme);
|
||||
// Both listener callbacks capture the scheme Arc by move: the bound-pairs
|
||||
// provider closure and the event handler (which calls dispatch_recovery).
|
||||
// Clone one so each closure owns its own Arc (Arc is not Copy).
|
||||
let scheme_for_pairs = Arc::clone(&scheme_for_events);
|
||||
let _events_thread = unified_events::spawn_unified_listener(
|
||||
std::path::PathBuf::from("/scheme/pci/aer"),
|
||||
std::path::PathBuf::from("/scheme/pci/pciehp"),
|
||||
move || scheme_for_pairs.bound_device_pairs(),
|
||||
move |event| match event {
|
||||
unified_events::UnifiedEvent::Aer { event, action } => {
|
||||
if matches!(
|
||||
action,
|
||||
redox_driver_core::driver::RecoveryAction::Fatal
|
||||
) {
|
||||
log::error!(
|
||||
"AER-FATAL: device={} driver-already-dead escalation marker (no auto-dispatch; operator intervention required)",
|
||||
event.device
|
||||
);
|
||||
}
|
||||
if let Some(action_str) =
|
||||
scheme::DriverManagerScheme::recovery_action_str(*action)
|
||||
{
|
||||
#[cfg(target_os = "redox")]
|
||||
scheme_for_events.dispatch_recovery(&event.device, action_str);
|
||||
}
|
||||
}
|
||||
unified_events::UnifiedEvent::Pciehp(e) => {
|
||||
log::info!("pciehp: event kind={} device={}", e.kind.label(), e.device);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
match manager.lock() {
|
||||
Ok(mut mgr) => {
|
||||
@@ -442,6 +413,100 @@ fn main() {
|
||||
registry::register(weak);
|
||||
}
|
||||
|
||||
// Spawn the unified event listener. The closure passed as
|
||||
// `consult_driver` tries the per-device sidecar IPC channel first,
|
||||
// then falls back to the in-process `DriverConfig::on_error` impl
|
||||
// (which gives the severity-default mapping unless a driver
|
||||
// overrides), then to None (severity-only). The listener then
|
||||
// maps the action through `RecoveryAction::Fatal` -> escalation
|
||||
// log marker (in main callback) before calling dispatch_recovery.
|
||||
let scheme_for_events_aer = Arc::clone(&scheme);
|
||||
let _events_thread = unified_events::spawn_unified_listener(
|
||||
std::path::PathBuf::from("/scheme/pci/aer"),
|
||||
std::path::PathBuf::from("/scheme/pci/pciehp"),
|
||||
move || scheme_for_events_aer.bound_device_pairs(),
|
||||
move |bdf, severity| {
|
||||
// 1) Sidecar IPC: ask the spawned driver daemon if it
|
||||
// opted in to error-reporting.
|
||||
let report = error_channel::DriverErrorReport {
|
||||
severity,
|
||||
bdf: bdf.to_string(),
|
||||
raw: String::new(),
|
||||
};
|
||||
if let Some(channel) = error_channel::global().get(bdf) {
|
||||
if let Some(action) = channel.request_recovery(&report) {
|
||||
return Some(action);
|
||||
}
|
||||
}
|
||||
// 2) In-process DriverConfig::on_error: drivers may
|
||||
// override the severity-default via the trait method.
|
||||
let pairs = scheme_for_events.bound_device_pairs();
|
||||
let driver_name = match pairs.iter().find(|(addr, _)| addr == bdf) {
|
||||
Some((_, name)) => name.clone(),
|
||||
None => return None,
|
||||
};
|
||||
let snapshot = registry::snapshot();
|
||||
for weak in snapshot {
|
||||
if let Some(cfg) = weak.upgrade() {
|
||||
if cfg.name() == driver_name {
|
||||
let info = redox_driver_core::device::DeviceInfo {
|
||||
id: redox_driver_core::device::DeviceId {
|
||||
bus: "pci".to_string(),
|
||||
path: bdf.to_string(),
|
||||
},
|
||||
vendor: None,
|
||||
device: None,
|
||||
class: None,
|
||||
subclass: None,
|
||||
prog_if: None,
|
||||
revision: None,
|
||||
subsystem_vendor: None,
|
||||
subsystem_device: None,
|
||||
raw_path: format!("/scheme/pci/{bdf}"),
|
||||
description: None,
|
||||
};
|
||||
match cfg.on_error(&info, severity) {
|
||||
Ok(action) => return Some(action),
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"AER: driver {} on_error returned {:?} for device {} severity {:?}",
|
||||
driver_name,
|
||||
err,
|
||||
bdf,
|
||||
severity
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
},
|
||||
move |event| match event {
|
||||
unified_events::UnifiedEvent::Aer { event, action } => {
|
||||
if matches!(
|
||||
action,
|
||||
redox_driver_core::driver::RecoveryAction::Fatal
|
||||
) {
|
||||
log::error!(
|
||||
"AER-FATAL: device={} driver-already-dead escalation marker (no auto-dispatch; operator intervention required)",
|
||||
event.device
|
||||
);
|
||||
}
|
||||
if let Some(action_str) =
|
||||
scheme::DriverManagerScheme::recovery_action_str(*action)
|
||||
{
|
||||
#[cfg(target_os = "redox")]
|
||||
scheme_for_events.dispatch_recovery(&event.device, action_str);
|
||||
}
|
||||
}
|
||||
unified_events::UnifiedEvent::Pciehp(e) => {
|
||||
log::info!("pciehp: event kind={} device={}", e.kind.label(), e.device);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let _reaper_thread = reaper::spawn_reaper_thread(|pid| {
|
||||
let registry = registry::snapshot();
|
||||
for weak in registry {
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::time::Duration;
|
||||
|
||||
use crate::aer::AerEvent;
|
||||
use crate::pciehp::PciehpEvent;
|
||||
use redox_driver_core::driver::RecoveryAction;
|
||||
use redox_driver_core::driver::{ErrorSeverity, RecoveryAction};
|
||||
|
||||
/// A unified event: either AER (error) or pciehp (hotplug).
|
||||
///
|
||||
@@ -32,25 +32,42 @@ pub enum UnifiedEvent {
|
||||
/// Spawn the unified event listener thread. Polls both files and
|
||||
/// routes events to `handle_event`. AER events are routed through
|
||||
/// `route_to_driver` using the `bind_snapshot` of currently-bound
|
||||
/// devices; the decided recovery action is logged per event.
|
||||
pub fn spawn_unified_listener(
|
||||
/// devices; the `consult_driver` closure is invoked for bound devices
|
||||
/// so the driver's `Driver::on_error` impl can override the
|
||||
/// severity-based default.
|
||||
pub fn spawn_unified_listener<F>(
|
||||
aer_path: std::path::PathBuf,
|
||||
pciehp_path: std::path::PathBuf,
|
||||
bind_snapshot: impl Fn() -> Vec<(String, String)> + Send + 'static,
|
||||
consult_driver: F,
|
||||
handle_event: impl Fn(&UnifiedEvent) + Send + 'static,
|
||||
) -> thread::JoinHandle<()> {
|
||||
) -> thread::JoinHandle<()>
|
||||
where
|
||||
F: Fn(&str, ErrorSeverity) -> Option<RecoveryAction> + Send + 'static,
|
||||
{
|
||||
thread::Builder::new()
|
||||
.name("driver-manager-events".to_string())
|
||||
.spawn(move || run(aer_path, pciehp_path, bind_snapshot, handle_event))
|
||||
.spawn(move || {
|
||||
run(
|
||||
aer_path,
|
||||
pciehp_path,
|
||||
bind_snapshot,
|
||||
consult_driver,
|
||||
handle_event,
|
||||
)
|
||||
})
|
||||
.expect("spawn unified event listener")
|
||||
}
|
||||
|
||||
fn run(
|
||||
fn run<F>(
|
||||
aer_path: std::path::PathBuf,
|
||||
pciehp_path: std::path::PathBuf,
|
||||
bind_snapshot: impl Fn() -> Vec<(String, String)>,
|
||||
consult_driver: F,
|
||||
handle_event: impl Fn(&UnifiedEvent),
|
||||
) {
|
||||
) where
|
||||
F: Fn(&str, ErrorSeverity) -> Option<RecoveryAction>,
|
||||
{
|
||||
log::info!(
|
||||
"events: unified listener started (aer={}, pciehp={})",
|
||||
aer_path.display(),
|
||||
@@ -63,7 +80,7 @@ fn run(
|
||||
if let Some(events) = crate::aer::read_aer_lines(&aer_path, &mut last_seen_aer) {
|
||||
let binds = bind_snapshot();
|
||||
for event in events {
|
||||
let action = crate::aer::route_to_driver(&event, &binds);
|
||||
let action = crate::aer::route_to_driver(&event, &binds, &consult_driver);
|
||||
log::info!(
|
||||
"AER: event device={} severity={:?} action={:?}",
|
||||
event.device,
|
||||
|
||||
Reference in New Issue
Block a user