Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c7656a5c3 | |||
| 30819f87cf | |||
| 717bc436be | |||
| 887718da49 | |||
| 1331b8c017 | |||
| 28e356d598 | |||
| b3fd5cc682 | |||
| 263a41a926 | |||
| 20d805ed37 | |||
| b887d2b450 | |||
| 45452c5a8e | |||
| 9e5f915d42 | |||
| 8c7f6172a2 | |||
| d98330a7de | |||
| 29166263bb | |||
| e54484e553 | |||
| 2e5e516587 | |||
| b906ad688a | |||
| 0ca545d35a | |||
| 5a43628d58 | |||
| 9cd43d8d8c |
Generated
+3
@@ -2609,6 +2609,9 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
[[package]]
|
||||
name = "usb-core"
|
||||
version = "0.3.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "usbctl"
|
||||
|
||||
+179
-11
@@ -1,3 +1,4 @@
|
||||
use alloc::collections::VecDeque;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
@@ -14,15 +15,16 @@ use redox_rt::proc::FdGuard;
|
||||
// single-threaded Rc<RefCell>. NOTE: this Mutex is a spinlock -- never hold it
|
||||
// across a blocking syscall; resolve the cap_fd under the lock, clone the Arc,
|
||||
// and do the blocking openat with the lock released. See
|
||||
// local/docs/INITNSMGR-CONCURRENCY-DESIGN.md.
|
||||
// local/docs/legacy-obsolete-2026-07-25/INITNSMGR-CONCURRENCY-DESIGN.md.
|
||||
use redox_rt::sync::Mutex;
|
||||
use redox_scheme::{
|
||||
CallerCtx, OpenResult, RequestKind, Response, SendFdRequest, SignalBehavior, Socket,
|
||||
scheme::{SchemeState, SchemeSync},
|
||||
CallerCtx, Id, IntoTag, OpenResult, RequestKind, Response, SendFdRequest, SignalBehavior,
|
||||
Socket, Tag,
|
||||
scheme::{Op, OpFdPathLike, SchemeState, SchemeSync},
|
||||
};
|
||||
use syscall::Stat;
|
||||
use syscall::dirent::{DirEntry, DirentBuf, DirentKind};
|
||||
use syscall::{CallFlags, FobtainFdFlags, error::*, schemev2::NewFdFlags};
|
||||
use syscall::{CallFlags, FobtainFdFlags, error::*, flag::O_NONBLOCK, schemev2::NewFdFlags};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Namespace {
|
||||
@@ -102,12 +104,23 @@ enum Handle {
|
||||
List(NamespaceAccess),
|
||||
}
|
||||
|
||||
struct PendingOpen {
|
||||
tag: Tag,
|
||||
cap_fd: Arc<FdGuard>,
|
||||
reference: String,
|
||||
flags: usize,
|
||||
fcntl_flags: u32,
|
||||
}
|
||||
|
||||
const MAX_PENDING_OPENS: usize = 64;
|
||||
|
||||
pub struct NamespaceScheme<'sock> {
|
||||
socket: &'sock Socket,
|
||||
handles: HashMap<usize, Handle>,
|
||||
root_namespace: Namespace,
|
||||
next_id: usize,
|
||||
scheme_creation_cap: FdGuard,
|
||||
pending_opens: VecDeque<PendingOpen>,
|
||||
}
|
||||
|
||||
const HIGH_PERMISSIONS: NsPermissions = NsPermissions::SCHEME_CREATE;
|
||||
@@ -124,6 +137,7 @@ impl<'sock> NamespaceScheme<'sock> {
|
||||
root_namespace: Namespace { schemes },
|
||||
next_id: 0,
|
||||
scheme_creation_cap,
|
||||
pending_opens: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,6 +230,120 @@ impl<'sock> NamespaceScheme<'sock> {
|
||||
self.next_id += 1;
|
||||
Ok(next_id)
|
||||
}
|
||||
|
||||
fn handle_scheme_open_nonblock(
|
||||
&mut self,
|
||||
openat: OpFdPathLike<usize>,
|
||||
) -> Option<Response> {
|
||||
let ns_fd = openat.fd;
|
||||
let fcntl_flags = openat.fcntl_flags;
|
||||
let flags = *openat.flags();
|
||||
let path_owned = openat.path().to_string();
|
||||
let tag = openat.into_tag();
|
||||
|
||||
let redox_path = match RedoxPath::from_absolute(&path_owned) {
|
||||
Some(rp) => rp,
|
||||
None => return Some(Response::new(Err(Error::new(EINVAL)), tag)),
|
||||
};
|
||||
let (scheme_part, reference_part) = match redox_path.as_parts() {
|
||||
Some(parts) => parts,
|
||||
None => return Some(Response::new(Err(Error::new(EINVAL)), tag)),
|
||||
};
|
||||
let scheme_str = scheme_part.as_ref().to_string();
|
||||
let reference_str = reference_part.as_ref().to_string();
|
||||
|
||||
let ns_access = match self.handles.get(&ns_fd) {
|
||||
Some(Handle::Access(access)) => access.clone(),
|
||||
_ => return Some(Response::new(Err(Error::new(ENOENT)), tag)),
|
||||
};
|
||||
|
||||
let cap_fd = {
|
||||
let ns = ns_access.namespace.lock();
|
||||
match ns.get_scheme_fd(&scheme_str) {
|
||||
Some(arc_fd) => arc_fd.clone(),
|
||||
None => {
|
||||
log::info!("Scheme {:?} not found in namespace", scheme_str);
|
||||
return Some(Response::new(Err(Error::new(ENODEV)), tag));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let client_nonblock = flags & O_NONBLOCK != 0;
|
||||
let nonblock_flags = flags | O_NONBLOCK;
|
||||
|
||||
match syscall::openat(
|
||||
cap_fd.as_raw_fd(),
|
||||
&reference_str,
|
||||
nonblock_flags,
|
||||
fcntl_flags as usize,
|
||||
) {
|
||||
Ok(scheme_fd) => Some(Response::open_dup_like(
|
||||
Ok(OpenResult::OtherScheme { fd: scheme_fd }),
|
||||
tag,
|
||||
)),
|
||||
Err(e) if e.errno == EAGAIN && !client_nonblock => {
|
||||
if self.pending_opens.len() >= MAX_PENDING_OPENS {
|
||||
let result = syscall::openat(
|
||||
cap_fd.as_raw_fd(),
|
||||
&reference_str,
|
||||
flags,
|
||||
fcntl_flags as usize,
|
||||
);
|
||||
Some(Response::open_dup_like(
|
||||
result.map(|fd| OpenResult::OtherScheme { fd }),
|
||||
tag,
|
||||
))
|
||||
} else {
|
||||
self.pending_opens.push_back(PendingOpen {
|
||||
tag,
|
||||
cap_fd,
|
||||
reference: reference_str,
|
||||
flags,
|
||||
fcntl_flags,
|
||||
});
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => Some(Response::open_dup_like(Err(e), tag)),
|
||||
}
|
||||
}
|
||||
|
||||
fn retry_pending_opens(&mut self, socket: &Socket) {
|
||||
let mut pending = mem::take(&mut self.pending_opens);
|
||||
|
||||
while let Some(p) = pending.pop_front() {
|
||||
let nonblock_flags = p.flags | O_NONBLOCK;
|
||||
match syscall::openat(
|
||||
p.cap_fd.as_raw_fd(),
|
||||
&p.reference,
|
||||
nonblock_flags,
|
||||
p.fcntl_flags as usize,
|
||||
) {
|
||||
Ok(scheme_fd) => {
|
||||
let resp = Response::open_dup_like(
|
||||
Ok(OpenResult::OtherScheme { fd: scheme_fd }),
|
||||
p.tag,
|
||||
);
|
||||
let _ = socket.write_response(resp, SignalBehavior::Restart);
|
||||
}
|
||||
Err(e) if e.errno == EAGAIN => {
|
||||
pending.push_back(p);
|
||||
}
|
||||
Err(e) => {
|
||||
let resp = Response::open_dup_like(Err(e), p.tag);
|
||||
let _ = socket.write_response(resp, SignalBehavior::Restart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.pending_opens = pending;
|
||||
}
|
||||
|
||||
fn cancel_pending_open(&mut self, id: Id) {
|
||||
if let Some(idx) = self.pending_opens.iter().position(|p| p.tag.id() == id) {
|
||||
let _ = self.pending_opens.remove(idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'sock> SchemeSync for NamespaceScheme<'sock> {
|
||||
@@ -506,6 +634,20 @@ where
|
||||
T::from_le_bytes_slice(buffer)
|
||||
}
|
||||
|
||||
/// Returns `true` when the path targets a scheme other than `namespace` itself.
|
||||
/// Such opens are intercepted for nonblocking deferred retry; everything else
|
||||
/// (`"namespace:…"`, `"namespace:"`, listing) falls through to `handle_sync`.
|
||||
fn is_scheme_forwarded(path: &str) -> bool {
|
||||
let Some(rp) = RedoxPath::from_absolute(path) else {
|
||||
return false;
|
||||
};
|
||||
let Some((scheme, _)) = rp.as_parts() else {
|
||||
return false;
|
||||
};
|
||||
let s = scheme.as_ref();
|
||||
!s.is_empty() && s != "namespace"
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
sync_pipe: FdGuard,
|
||||
socket: Socket,
|
||||
@@ -540,16 +682,40 @@ pub fn run(
|
||||
break;
|
||||
};
|
||||
match req.kind() {
|
||||
RequestKind::Call(req) => {
|
||||
let resp = req.handle_sync(&mut scheme, &mut state);
|
||||
RequestKind::Call(call_req) => {
|
||||
let caller = call_req.caller();
|
||||
let resp_opt = match call_req.op() {
|
||||
Ok(op) => {
|
||||
let intercept = matches!(
|
||||
&op,
|
||||
Op::OpenAt(ref o) if is_scheme_forwarded(o.path())
|
||||
);
|
||||
if intercept {
|
||||
match op {
|
||||
Op::OpenAt(openat) => {
|
||||
scheme.handle_scheme_open_nonblock(openat)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
Some(op.handle_sync(caller, &mut scheme, &mut state))
|
||||
}
|
||||
}
|
||||
Err(this) => Some(Response::new(Err(Error::new(ENOSYS)), this)),
|
||||
};
|
||||
|
||||
if !socket
|
||||
.write_response(resp, SignalBehavior::Restart)
|
||||
.expect("bootstrap: failed to write scheme response to kernel")
|
||||
{
|
||||
break;
|
||||
if let Some(resp) = resp_opt {
|
||||
if !socket
|
||||
.write_response(resp, SignalBehavior::Restart)
|
||||
.expect("bootstrap: failed to write scheme response to kernel")
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
RequestKind::Cancellation(cancel_req) => {
|
||||
scheme.cancel_pending_open(cancel_req.id);
|
||||
}
|
||||
RequestKind::OnClose { id } => scheme.on_close(id),
|
||||
RequestKind::SendFd(sendfd_request) => {
|
||||
let result = scheme.on_sendfd(&sendfd_request);
|
||||
@@ -564,6 +730,8 @@ pub fn run(
|
||||
|
||||
_ => (),
|
||||
}
|
||||
|
||||
scheme.retry_pending_opens(&socket);
|
||||
}
|
||||
|
||||
unreachable!()
|
||||
|
||||
+49
-8
@@ -121,12 +121,18 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> {
|
||||
).subsec_nanos();
|
||||
|
||||
let socket = try_fmt!(UdpSocket::bind(("0.0.0.0", 68)), "failed to bind udp");
|
||||
try_fmt!(socket.connect(SocketAddr::from(([255, 255, 255, 255], 67))), "failed to connect");
|
||||
let broadcast = SocketAddr::from(([255, 255, 255, 255], 67));
|
||||
// 8s (was 30s): a DHCP server that is present answers a DISCOVER in well
|
||||
// under a second, so a long read timeout only serves to stall boot when no
|
||||
// server responds (e.g. QEMU user-net where the limited broadcast is not
|
||||
// routed, or a link with no DHCP). Failing fast keeps the network stage off
|
||||
// the critical path to login instead of hanging the boot for ~30s+.
|
||||
//
|
||||
// Do NOT call connect() to the broadcast address. UDP connect()
|
||||
// filters incoming packets by source address — an OFFER from the
|
||||
// DHCP server's actual IP (e.g. QEMU SLIRP at 10.0.2.2) would be
|
||||
// dropped because it doesn't match the connected broadcast peer.
|
||||
// Use send_to() per-packet and let recv() accept any source.
|
||||
try_fmt!(socket.set_read_timeout(Some(Duration::new(8, 0))), "failed to set read timeout");
|
||||
try_fmt!(socket.set_write_timeout(Some(Duration::new(8, 0))), "failed to set write timeout");
|
||||
|
||||
@@ -142,7 +148,7 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> {
|
||||
init_dhcp_header(&mut discover, current_mac, tid);
|
||||
let disc_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPDISCOVER, OPT_END];
|
||||
discover.options[..disc_opts.len()].copy_from_slice(disc_opts);
|
||||
try_fmt!(send_dhcp(&discover, &socket), "failed to send discover");
|
||||
try_fmt!(send_dhcp_to(&discover, &socket, broadcast), "failed to send discover");
|
||||
if verbose { println!("DHCP: Sent Discover"); }
|
||||
}
|
||||
|
||||
@@ -195,7 +201,7 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> {
|
||||
OPT_END,
|
||||
];
|
||||
request.options[..req_opts.len()].copy_from_slice(req_opts);
|
||||
try_fmt!(send_dhcp(&request, &socket), "failed to send request");
|
||||
try_fmt!(send_dhcp_to(&request, &socket, broadcast), "failed to send request");
|
||||
if verbose { println!("DHCP: Sent Request"); }
|
||||
}
|
||||
|
||||
@@ -223,7 +229,10 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> {
|
||||
renew.ciaddr = offer.yiaddr;
|
||||
let rn_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPREQUEST, OPT_END];
|
||||
renew.options[..rn_opts.len()].copy_from_slice(rn_opts);
|
||||
try_fmt!(send_dhcp(&renew, &socket), "failed to send renew");
|
||||
try_fmt!(
|
||||
send_dhcp_to(&renew, &socket, broadcast),
|
||||
"failed to send renew"
|
||||
);
|
||||
}
|
||||
|
||||
socket.set_read_timeout(Some(t2.saturating_sub(t1))).ok();
|
||||
@@ -242,13 +251,12 @@ fn dhcp(iface: &str, verbose: bool) -> Result<(), String> {
|
||||
Err(_) => {
|
||||
if verbose { println!("DHCP: entering REBIND state"); }
|
||||
let bind_socket = try_fmt!(UdpSocket::bind(("0.0.0.0", 68)), "failed to bind rebind");
|
||||
try_fmt!(bind_socket.connect(SocketAddr::from(([255, 255, 255, 255], 67))), "rebind connect");
|
||||
let mut rebind = Dhcp::default();
|
||||
init_dhcp_header(&mut rebind, current_mac, tid.wrapping_add(2));
|
||||
rebind.ciaddr = offer.yiaddr;
|
||||
let rb_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPREQUEST, OPT_END];
|
||||
rebind.options[..rb_opts.len()].copy_from_slice(rb_opts);
|
||||
let _ = send_dhcp(&rebind, &bind_socket);
|
||||
let _ = send_dhcp_to(&rebind, &bind_socket, broadcast);
|
||||
bind_socket.set_read_timeout(Some(Duration::from_secs(10))).ok();
|
||||
if let Ok(_) = bind_socket.recv(&mut ack_data) {
|
||||
if verbose { println!("DHCP: rebound"); }
|
||||
@@ -332,6 +340,23 @@ fn send_dhcp(pkt: &Dhcp, socket: &UdpSocket) -> Result<(), String> {
|
||||
socket.send(data).map(|_| ()).map_err(|e| format!("send: {}", e))
|
||||
}
|
||||
|
||||
/// `send_to(addr, ...)` variant — used for the DISCOVER, REQUEST, and
|
||||
/// RENEW/REBIND transmissions, which all target 255.255.255.255:67.
|
||||
/// `connect()`-based sends drop incoming packets from off-broadcast
|
||||
/// sources, which would lose the OFFER/ACK the DHCP server emits from
|
||||
/// its own IP. Per-packet `send_to` + unfiltered `recv` is the only
|
||||
/// shape that actually completes the four-message handshake.
|
||||
fn send_dhcp_to(
|
||||
pkt: &Dhcp,
|
||||
socket: &UdpSocket,
|
||||
addr: SocketAddr,
|
||||
) -> Result<(), String> {
|
||||
let data = unsafe {
|
||||
std::slice::from_raw_parts(pkt as *const Dhcp as *const u8, std::mem::size_of::<Dhcp>())
|
||||
};
|
||||
socket.send_to(data, addr).map(|_| ()).map_err(|e| format!("send_to: {}", e))
|
||||
}
|
||||
|
||||
impl Default for Dhcp {
|
||||
fn default() -> Self {
|
||||
Dhcp {
|
||||
@@ -345,15 +370,31 @@ impl Default for Dhcp {
|
||||
|
||||
fn main() {
|
||||
let mut verbose = false;
|
||||
let iface = "eth0";
|
||||
|
||||
let mut iface = "eth0".to_string();
|
||||
for arg in env::args().skip(1) {
|
||||
match arg.as_ref() {
|
||||
"-v" => verbose = true,
|
||||
other if !other.starts_with('-') => iface = other.to_string(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
// driver-manager attaches the NIC asynchronously, and smolnetd only brings up
|
||||
// /scheme/netcfg/ifaces/<iface> after it binds the adapter, so on a fresh boot
|
||||
// this path may not exist yet when dhcpd runs (10_dhcpd only requires_weak
|
||||
// that 10_smolnetd *started*). Wait (bounded) for the interface to appear
|
||||
// instead of failing immediately with "Can't open ...". A genuinely NIC-less
|
||||
// machine falls through after the window and exits cleanly (0), no error.
|
||||
let mac_path = format!("/scheme/netcfg/ifaces/{iface}/mac");
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
|
||||
while std::fs::File::open(&mac_path).is_err() {
|
||||
if std::time::Instant::now() >= deadline {
|
||||
eprintln!("dhcpd: {iface} never appeared in /scheme/netcfg (no NIC?); skipping DHCP");
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(250));
|
||||
}
|
||||
|
||||
if let Err(err) = dhcp(iface, verbose) {
|
||||
eprintln!("dhcpd: {err}");
|
||||
process::exit(1);
|
||||
|
||||
@@ -29,5 +29,11 @@ libredox.workspace = true
|
||||
redox-scheme.workspace = true
|
||||
scheme-utils = { path = "../../scheme-utils" }
|
||||
|
||||
# Quirks system: acpid is the consumer of SystemQuirkFlags (force_s2idle,
|
||||
# acpi_irq1_skip_override, no_legacy_pm1b). Loaded at init from the in-memory
|
||||
# DMI scan via the toml_loader path. Mirrors the cross-tree dep pattern used
|
||||
# by xhcid (drivers/usb/xhcid/Cargo.toml).
|
||||
redox-driver-sys = { path = "../../../../recipes/drivers/redox-driver-sys/source" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -418,6 +418,17 @@ pub struct AcpiContext {
|
||||
/// SMBIOS, and downstream quirks systems tolerate the absence.
|
||||
dmi: Option<DmiInfo>,
|
||||
|
||||
/// Machine-wide system quirk flags accumulated from `[[dmi_system_quirk]]`
|
||||
/// entries whose DMI rule matched this host. Computed once at init from
|
||||
/// the in-memory DMI scan, then consumed by:
|
||||
/// - [`Self::set_global_s_state`] (`NO_LEGACY_PM1B` — skip PM1b write)
|
||||
/// - [`Self::enter_s2idle`] / `enter_s3` (`FORCE_S2IDLE` — route Modern
|
||||
/// Standby platforms away from S3)
|
||||
/// `acpi_irq1_skip_override` and `kbd_deactivate_fixup` are owned by
|
||||
/// other daemons (kernel IRQ setup and ps2d respectively) and are read
|
||||
/// by those consumers via `/scheme/acpi/dmi`.
|
||||
system_quirks: redox_driver_sys::quirks::SystemQuirkFlags,
|
||||
|
||||
pub next_ctx: RwLock<u64>,
|
||||
|
||||
/// GPE/PM1 fixed-event register map, built from the FADT by
|
||||
@@ -442,6 +453,7 @@ pub struct AcpiContext {
|
||||
pub fan_devices: RwLock<Vec<String>>,
|
||||
|
||||
pub wake_registry: RwLock<Option<crate::wake::WakeRegistry>>,
|
||||
pub wmi_registry: RwLock<Option<crate::wmi::WmiRegistry>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
@@ -576,9 +588,11 @@ impl AcpiContext {
|
||||
sleep_button_events: RwLock::new(0),
|
||||
fan_devices: RwLock::new(Vec::new()),
|
||||
wake_registry: RwLock::new(None),
|
||||
wmi_registry: RwLock::new(None),
|
||||
|
||||
sdt_order: RwLock::new(Vec::new()),
|
||||
dmi: None,
|
||||
system_quirks: redox_driver_sys::quirks::SystemQuirkFlags::empty(),
|
||||
facs: None,
|
||||
};
|
||||
|
||||
@@ -621,6 +635,35 @@ impl AcpiContext {
|
||||
}
|
||||
}
|
||||
|
||||
// Compute machine-wide system quirk flags from the in-memory DMI
|
||||
// scan. The flags drive consumer behavior in this daemon (and are
|
||||
// also re-readable by other daemons via /scheme/acpi/dmi). The
|
||||
// conversion maps the 7 fields redox_driver_sys::quirks::dmi::DmiInfo
|
||||
// knows about; the remaining 9 fields in acpid::dmi::DmiInfo are
|
||||
// not consumed by the quirk system today.
|
||||
this.system_quirks = this.dmi.as_ref().map_or(
|
||||
redox_driver_sys::quirks::SystemQuirkFlags::empty(),
|
||||
|info| {
|
||||
let sys_dmi = redox_driver_sys::quirks::dmi::DmiInfo {
|
||||
sys_vendor: info.sys_vendor.clone(),
|
||||
board_vendor: info.board_vendor.clone(),
|
||||
board_name: info.board_name.clone(),
|
||||
board_version: info.board_version.clone(),
|
||||
product_name: info.product_name.clone(),
|
||||
product_version: info.product_version.clone(),
|
||||
bios_version: info.bios_version.clone(),
|
||||
};
|
||||
redox_driver_sys::quirks::toml_loader::load_dmi_system_quirks(&sys_dmi)
|
||||
.unwrap_or_default()
|
||||
},
|
||||
);
|
||||
if !this.system_quirks.is_empty() {
|
||||
log::info!(
|
||||
"acpid: loaded system quirks: {:?}",
|
||||
this.system_quirks
|
||||
);
|
||||
}
|
||||
|
||||
Fadt::init(&mut this);
|
||||
// DMAR init is opt-in via REDBEAR_DMAR_INIT=1 env var. MMIO reads
|
||||
// (e.g. gl_sts.read()) on some real hardware block or spin
|
||||
@@ -640,6 +683,14 @@ impl AcpiContext {
|
||||
self.dmi.as_ref()
|
||||
}
|
||||
|
||||
/// Machine-wide system quirk flags accumulated at init from
|
||||
/// `[[dmi_system_quirk]]` TOML entries that matched this host's DMI.
|
||||
/// Empty when no SMBIOS or no rules apply — consumers must treat that
|
||||
/// as "no quirks".
|
||||
pub fn system_quirks(&self) -> redox_driver_sys::quirks::SystemQuirkFlags {
|
||||
self.system_quirks
|
||||
}
|
||||
|
||||
/// Access the parsed FACS (Firmware ACPI Control Structure), if
|
||||
/// present. Returns `None` if no FACS table was found in the RSDT/XSDT.
|
||||
pub fn facs(&self) -> Option<&Facs> {
|
||||
@@ -1019,6 +1070,25 @@ impl AcpiContext {
|
||||
/// 4. Write SLP_EN|SLP_TYPa to PM1a, SLP_EN|SLP_TYPb to PM1b
|
||||
/// 5. Spin (machine should power off before this returns)
|
||||
pub fn set_global_s_state(&self, state: u8) {
|
||||
// FORCE_S2IDLE system quirk: when set, route S3 entry to s2idle
|
||||
// instead. Modern Standby-only platforms (LG Gram 16Z90TP, Framework
|
||||
// laptops, late-model Dell XPS) advertise `\_S3` packages in their
|
||||
// DSDT but the firmware refuses to honour the SLP_TYP write — the
|
||||
// machine hangs or silently no-ops. With this quirk armed, S3 entry
|
||||
// is replaced by the s2idle preparation sequence (which the kernel
|
||||
// MWAIT loop then completes).
|
||||
if state == 3
|
||||
&& self
|
||||
.system_quirks
|
||||
.contains(redox_driver_sys::quirks::SystemQuirkFlags::FORCE_S2IDLE)
|
||||
{
|
||||
log::info!(
|
||||
"set_global_s_state(3): FORCE_S2IDLE quirk active, routing S3 entry to s2idle"
|
||||
);
|
||||
self.enter_s2idle();
|
||||
return;
|
||||
}
|
||||
|
||||
let fadt = match self.fadt() {
|
||||
Some(fadt) => fadt,
|
||||
None => {
|
||||
@@ -1120,16 +1190,28 @@ impl AcpiContext {
|
||||
);
|
||||
Pio::<u16>::new(port_a).write(val_a);
|
||||
|
||||
// Some hardware requires both PM1a and PM1b to be written for
|
||||
// the sleep transition. The FADT pm1b_control_block is 0 when
|
||||
// no second block exists; in that case skip the second write.
|
||||
// PM1b write: skipped when (a) the FADT reports no PM1b block
|
||||
// (the standard "single PM1 block" case), OR (b) the host's
|
||||
// DMI matches the `no_legacy_pm1b` system quirk — LG Gram and
|
||||
// similar laptops report a non-zero pm1b_control_block in their
|
||||
// FADT but the register does not exist on the platform, and
|
||||
// writes to it have been observed to wedge the controller.
|
||||
// The quirk is belt-and-braces alongside the FADT check.
|
||||
let skip_pm1b_quirk = self
|
||||
.system_quirks
|
||||
.contains(redox_driver_sys::quirks::SystemQuirkFlags::NO_LEGACY_PM1B);
|
||||
let port_b = fadt.pm1b_control_block as u16;
|
||||
if port_b != 0 {
|
||||
if port_b != 0 && !skip_pm1b_quirk {
|
||||
log::warn!(
|
||||
"Sleep S{} with ACPI outw(0x{:X}, 0x{:X})",
|
||||
state, port_b, val_b
|
||||
);
|
||||
Pio::<u16>::new(port_b).write(val_b);
|
||||
} else if port_b != 0 && skip_pm1b_quirk {
|
||||
log::info!(
|
||||
"Sleep S{}: skipping PM1b write at 0x{:X} due to no_legacy_pm1b quirk",
|
||||
state, port_b
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -178,7 +178,9 @@ impl AmlPhysMemHandler {
|
||||
let pci_fd = if let Some(pci_fd) = pci_fd_opt {
|
||||
Some(libredox::Fd::new(pci_fd.raw()))
|
||||
} else {
|
||||
log::error!("pci_fd is not registered");
|
||||
log::warn!(
|
||||
"acpid: AmlPhysMemHandler created without PCI fd; AML init will retry once pcid sends it via scheme:acpid sendfd"
|
||||
);
|
||||
None
|
||||
};
|
||||
Self {
|
||||
|
||||
@@ -729,9 +729,12 @@ pub const DMI_FIELDS: &[&str] = &[
|
||||
"ec_firmware_release",
|
||||
];
|
||||
|
||||
/// Try to load an existing `/scheme/acpi/dmi` cache (if another
|
||||
/// process already exposed one). This is unused at the moment but
|
||||
/// kept as a stub for future kernel-side SMBIOS scheme support.
|
||||
/// Read the `/scheme/acpi/dmi` cache served by another process (typically
|
||||
/// acpid itself, after `dmi::scan()` populated the in-memory copy and the
|
||||
/// scheme handler started exposing it). This is the round-trip counterpart
|
||||
/// to `to_match_lines` — useful for tests that need to verify what the
|
||||
/// scheme is actually serving, and for any future caller that wants to
|
||||
/// read DMI from the scheme rather than from a fresh firmware scan.
|
||||
#[allow(dead_code)]
|
||||
pub fn try_load_existing() -> Option<DmiInfo> {
|
||||
let mut file = File::open("/scheme/acpi/dmi").ok()?;
|
||||
|
||||
@@ -19,6 +19,7 @@ mod gpe;
|
||||
mod notifications;
|
||||
mod power_events;
|
||||
mod wake;
|
||||
mod wmi;
|
||||
|
||||
mod scheme;
|
||||
|
||||
@@ -118,6 +119,12 @@ fn daemon(daemon: daemon::Daemon) -> ! {
|
||||
}
|
||||
*acpi_context.wake_registry.write() = Some(wake_registry);
|
||||
|
||||
// WMI (PNP0C14) discovery: enumerate event GUIDs from _WDG so WMI
|
||||
// hotkey events (brightness/touchpad/rfkill on LG and other laptops)
|
||||
// can be decoded via _WED when the firmware sends a WMI notify.
|
||||
let wmi_registry = wmi::WmiRegistry::discover(&acpi_context);
|
||||
*acpi_context.wmi_registry.write() = Some(wmi_registry);
|
||||
|
||||
// TODO: I/O permission bitmap?
|
||||
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
|
||||
common::acquire_port_io_rights().expect("acpid: failed to set I/O privilege level to Ring 3");
|
||||
@@ -288,10 +295,54 @@ fn daemon(daemon: daemon::Daemon) -> ! {
|
||||
}
|
||||
power_events::PowerEvent::LidChanged(open) => {
|
||||
log::info!("acpid: lid {}", if open { "opened" } else { "closed" });
|
||||
// Symmetric lid-switch → s2idle wiring. Mirrors
|
||||
// Linux logind's `HandleLidSwitch=suspend` default
|
||||
// for laptops.
|
||||
//
|
||||
// Lid-closed runs `enter_s2idle()` — the AML
|
||||
// preparation sequence (_TTS/_PTS/_SST + wake-device
|
||||
// arming via _DSW/_PSW) so the system is in the
|
||||
// correct state when the kernel MWAIT loop (Phase
|
||||
// 9.1, not yet landed) actually idles the CPU.
|
||||
//
|
||||
// Lid-open runs `exit_s2idle()` — the AML wake
|
||||
// sequence (_SST(2) → _WAK(0) → _SST(1) + wake-device
|
||||
// disarming). This is the userspace-driven wake path
|
||||
// for the case where the kernel MWAIT loop hasn't
|
||||
// engaged yet (e.g. a brief lid-close/open during
|
||||
// active workload). When MWAIT IS engaged, the
|
||||
// kernel-side MWAIT-return path fires kstop
|
||||
// reason=2 which ALSO calls exit_s2idle() — the
|
||||
// double-call is safe because the AML _WAK/_SST
|
||||
// methods are spec-required to be idempotent in
|
||||
// the working state (ACPI 6.5 §3.5.3).
|
||||
//
|
||||
// External-display detection (Linux's
|
||||
// HandleLidSwitchDocked=ignore) is not wired yet —
|
||||
// every lid-close triggers s2idle. Documented as a
|
||||
// follow-up in local/docs/evidence/lg-gram/
|
||||
// ASSESSMENT-2026-07-26.md.
|
||||
if open {
|
||||
log::info!("acpid: lid opened — running s2idle wake sequence");
|
||||
acpi_context.exit_s2idle();
|
||||
} else {
|
||||
log::info!("acpid: lid closed — entering s2idle preparation");
|
||||
acpi_context.enter_s2idle();
|
||||
}
|
||||
}
|
||||
power_events::PowerEvent::Notify { device, value } => {
|
||||
log::debug!("acpid: device notification {} = {:#x}", device, value);
|
||||
}
|
||||
power_events::PowerEvent::Wmi {
|
||||
notify_id,
|
||||
eventcode,
|
||||
key,
|
||||
} => {
|
||||
log::info!(
|
||||
"acpid: WMI hotkey event notify_id={:#x} eventcode={:#x} key={}",
|
||||
notify_id, eventcode, key
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -20,6 +20,11 @@ pub enum PowerEvent {
|
||||
EcQuery(u8),
|
||||
LidChanged(bool),
|
||||
Notify { device: String, value: u64 },
|
||||
Wmi {
|
||||
notify_id: u8,
|
||||
eventcode: u32,
|
||||
key: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
/// ECDT (Embedded Controller Boot Resources Table) payload after the
|
||||
@@ -272,8 +277,27 @@ pub fn handle_sci(context: &AcpiContext) -> Vec<PowerEvent> {
|
||||
|
||||
// Drain AML notifications (from _Qxx methods and any other Notify).
|
||||
let lid_path = context.lid_device.read().clone();
|
||||
let wmi_path = context
|
||||
.wmi_registry
|
||||
.read()
|
||||
.as_ref()
|
||||
.and_then(|r| r.device_path());
|
||||
for (device, value) in context.notifications().drain() {
|
||||
log::info!("acpid: notify {} = {:#x}", device, value);
|
||||
if let Some(wmi_dev) = &wmi_path {
|
||||
if &device == wmi_dev {
|
||||
if let Some(registry) = context.wmi_registry.read().as_ref() {
|
||||
if let Some(wmi_event) = registry.handle_notify(context, value as u8) {
|
||||
events.push(PowerEvent::Wmi {
|
||||
notify_id: wmi_event.notify_id,
|
||||
eventcode: wmi_event.eventcode,
|
||||
key: wmi_event.key,
|
||||
});
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(lid) = &lid_path {
|
||||
if &device == lid {
|
||||
let state = read_lid_state(context);
|
||||
|
||||
@@ -1070,10 +1070,27 @@ impl SchemeSync for AcpiScheme<'_, '_> {
|
||||
}
|
||||
let new_fd = libredox::Fd::new(new_fd);
|
||||
|
||||
// Allow replacement: pcid may resend the fd after a restart,
|
||||
// and the previous one-shot EINVAL left aml init permanently
|
||||
// broken if the first sendfd raced with an early aml request.
|
||||
if self.pci_fd.is_some() {
|
||||
return Err(Error::new(EINVAL));
|
||||
} else {
|
||||
self.pci_fd = Some(new_fd);
|
||||
log::warn!(
|
||||
"acpid: replacing previously-registered PCI fd; AML symbol cache will rebuild on next request"
|
||||
);
|
||||
}
|
||||
self.pci_fd = Some(new_fd);
|
||||
|
||||
// Kick aml symbol init now that pci_fd is registered. The
|
||||
// next aml request will either find a working cache or get a
|
||||
// fresh error log here; either way the failure mode is
|
||||
// observable rather than silently deferred to "the next
|
||||
// caller retries".
|
||||
match self.ctx.aml_symbols(self.pci_fd.as_ref()) {
|
||||
Ok(_) => log::info!("acpid: AML symbols initialized on PCI fd registration"),
|
||||
Err(err) => {
|
||||
log::error!("acpid: AML symbol init failed after PCI fd registration");
|
||||
log::error!("acpid: AML error detail: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(num_fds)
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
use log::{debug, info, warn};
|
||||
|
||||
use acpi::aml::namespace::AmlName;
|
||||
use amlserde::AmlSerdeValue;
|
||||
use std::str::FromStr;
|
||||
use std::sync::RwLock;
|
||||
|
||||
use crate::acpi::AcpiContext;
|
||||
|
||||
const ACPI_WMI_EVENT: u8 = 1 << 3;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct GuidBlock {
|
||||
pub guid: [u8; 16],
|
||||
pub notify_id: u8,
|
||||
pub instance_count: u8,
|
||||
pub flags: u8,
|
||||
}
|
||||
|
||||
impl GuidBlock {
|
||||
pub const SIZE: usize = 20;
|
||||
|
||||
pub fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < Self::SIZE {
|
||||
return None;
|
||||
}
|
||||
let mut guid = [0u8; 16];
|
||||
guid.copy_from_slice(&data[0..16]);
|
||||
let notify_id = data[16];
|
||||
let instance_count = data[18];
|
||||
let flags = data[19];
|
||||
Some(Self {
|
||||
guid,
|
||||
notify_id,
|
||||
instance_count,
|
||||
flags,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_event(&self) -> bool {
|
||||
(self.flags & ACPI_WMI_EVENT) != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WmiEvent {
|
||||
pub notify_id: u8,
|
||||
pub eventcode: u32,
|
||||
pub key: &'static str,
|
||||
}
|
||||
|
||||
pub struct WmiRegistry {
|
||||
device_path: RwLock<Option<String>>,
|
||||
event_notify_ids: RwLock<Vec<u8>>,
|
||||
}
|
||||
|
||||
fn lg_key_for_eventcode(eventcode: u32) -> &'static str {
|
||||
match eventcode {
|
||||
0x70 => "lg-control-panel",
|
||||
0x74 => "touchpad-toggle",
|
||||
0xf020000 => "read-mode",
|
||||
0x10000000 => "kbd-backlight",
|
||||
_ => "unknown",
|
||||
}
|
||||
}
|
||||
|
||||
impl WmiRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
device_path: RwLock::new(None),
|
||||
event_notify_ids: RwLock::new(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn device_path(&self) -> Option<String> {
|
||||
self.device_path.read().unwrap().clone()
|
||||
}
|
||||
|
||||
pub fn has_notify_id(&self, notify_id: u8) -> bool {
|
||||
self.event_notify_ids.read().unwrap().contains(¬ify_id)
|
||||
}
|
||||
|
||||
pub fn discover(context: &AcpiContext) -> Self {
|
||||
let registry = Self::new();
|
||||
const CANDIDATES: &[&str] = &[
|
||||
"\\_SB.PC00.WMID",
|
||||
"\\_SB.PCI0.WMID",
|
||||
"\\_SB.WMID",
|
||||
"\\_SB.PC00.LPCB.WMID",
|
||||
"\\_SB.PCI0.LPCB.WMID",
|
||||
];
|
||||
for path in CANDIDATES {
|
||||
let Ok(hid) = context.evaluate_acpi_method(path, "_HID", &[]) else {
|
||||
continue;
|
||||
};
|
||||
if hid.first().copied() != Some(0x0C14) {
|
||||
continue;
|
||||
}
|
||||
info!("acpid: WMI device at {}", path);
|
||||
*registry.device_path.write().unwrap() = Some(path.to_string());
|
||||
registry.enumerate_event_guids(context, path);
|
||||
break;
|
||||
}
|
||||
if registry.device_path().is_none() {
|
||||
debug!("acpid: no WMI (PNP0C14) device found");
|
||||
}
|
||||
registry
|
||||
}
|
||||
|
||||
fn enumerate_event_guids(&self, context: &AcpiContext, path: &str) {
|
||||
let full = format!("{path}._WDG");
|
||||
let Ok(name) = AmlName::from_str(&full) else {
|
||||
warn!("acpid: invalid _WDG path {}", full);
|
||||
return;
|
||||
};
|
||||
let value = match context.aml_eval(name, Vec::new()) {
|
||||
Ok(v) => v,
|
||||
Err(err) => {
|
||||
warn!("acpid: _WDG evaluation failed at {}: {:?}", path, err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let buffer = match value {
|
||||
AmlSerdeValue::Buffer(b) => b,
|
||||
_ => {
|
||||
warn!("acpid: _WDG at {} is not a buffer", path);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut notify_ids = Vec::new();
|
||||
for chunk in buffer.chunks(GuidBlock::SIZE) {
|
||||
let Some(block) = GuidBlock::parse(chunk) else {
|
||||
continue;
|
||||
};
|
||||
if block.is_event() {
|
||||
notify_ids.push(block.notify_id);
|
||||
}
|
||||
}
|
||||
let count = notify_ids.len();
|
||||
*self.event_notify_ids.write().unwrap() = notify_ids;
|
||||
info!(
|
||||
"acpid: WMI enumerated {} event GUID(s) from _WDG ({} total bytes)",
|
||||
count,
|
||||
buffer.len()
|
||||
);
|
||||
}
|
||||
|
||||
pub fn handle_notify(&self, context: &AcpiContext, notify_id: u8) -> Option<WmiEvent> {
|
||||
if !self.has_notify_id(notify_id) {
|
||||
return None;
|
||||
}
|
||||
let path = self.device_path()?;
|
||||
let full = format!("{path}._WED");
|
||||
let Ok(name) = AmlName::from_str(&full) else {
|
||||
return None;
|
||||
};
|
||||
let value = context
|
||||
.aml_eval(name, vec![AmlSerdeValue::Integer(notify_id as u64)])
|
||||
.ok()?;
|
||||
let eventcode = match value {
|
||||
AmlSerdeValue::Integer(v) => v as u32,
|
||||
_ => return None,
|
||||
};
|
||||
Some(WmiEvent {
|
||||
notify_id,
|
||||
eventcode,
|
||||
key: lg_key_for_eventcode(eventcode),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn guid_block_parse_event() {
|
||||
let mut data = [0u8; 20];
|
||||
data[0] = 0x48;
|
||||
data[16] = 0x2A;
|
||||
data[18] = 0x01;
|
||||
data[19] = ACPI_WMI_EVENT;
|
||||
let block = GuidBlock::parse(&data).unwrap();
|
||||
assert_eq!(block.guid[0], 0x48);
|
||||
assert_eq!(block.notify_id, 0x2A);
|
||||
assert_eq!(block.instance_count, 1);
|
||||
assert!(block.is_event());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guid_block_parse_not_event() {
|
||||
let data = [0u8; 20];
|
||||
let block = GuidBlock::parse(&data).unwrap();
|
||||
assert!(!block.is_event());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guid_block_too_short() {
|
||||
assert!(GuidBlock::parse(&[0u8; 19]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keymap_covers_lg_codes() {
|
||||
assert_eq!(lg_key_for_eventcode(0x10000000), "kbd-backlight");
|
||||
assert_eq!(lg_key_for_eventcode(0x74), "touchpad-toggle");
|
||||
assert_eq!(lg_key_for_eventcode(0x70), "lg-control-panel");
|
||||
assert_eq!(lg_key_for_eventcode(0xf020000), "read-mode");
|
||||
assert_eq!(lg_key_for_eventcode(0xdead), "unknown");
|
||||
}
|
||||
}
|
||||
@@ -177,6 +177,27 @@ impl<T: ?Sized> Dma<T> {
|
||||
pub fn physical(&self) -> usize {
|
||||
self.phys
|
||||
}
|
||||
|
||||
/// Ensure DMA-buffer writes shipped by the device are visible to the CPU
|
||||
/// before subsequent reads from this buffer.
|
||||
///
|
||||
/// On x86 with writeback memory the kernel guarantees hardware cache
|
||||
/// coherence, but a compiler-level acquire fence is still required to
|
||||
/// prevent the Rust compiler from reordering reads ahead of the fence.
|
||||
/// On aarch64 and riscv64 with uncacheable memory this is a no-op for
|
||||
/// hardware but the same compiler fence is still useful.
|
||||
pub fn sync_for_cpu(&self) {
|
||||
core::sync::atomic::fence(core::sync::atomic::Ordering::Acquire);
|
||||
}
|
||||
|
||||
/// Ensure CPU writes to this DMA buffer are visible to the device
|
||||
/// before the matching doorbell / kick.
|
||||
///
|
||||
/// Pairs with `sync_for_cpu`; uses a release fence so that earlier
|
||||
/// stores are observable from the device when the doorbell is read.
|
||||
pub fn sync_for_device(&self) {
|
||||
core::sync::atomic::fence(core::sync::atomic::Ordering::Release);
|
||||
}
|
||||
}
|
||||
// TODO: there should exist a "context" struct that drivers create at start, which would be passed
|
||||
// to the respective functions
|
||||
|
||||
@@ -17,5 +17,11 @@ common = { path = "../../common" }
|
||||
daemon = { path = "../../../daemon" }
|
||||
inputd = { path = "../../inputd" }
|
||||
|
||||
# Quirks system: ps2d consumes KBD_DEACTIVATE_FIXUP (skip SetDefaultsDisable
|
||||
# 0xF5 command on affected laptops — LG Gram, some Dell/HP/Lenovo keyboards
|
||||
# drop keys or wedge the controller when that command is sent). The flag is
|
||||
# loaded at init from /scheme/acpi/dmi via the system_quirks() helper.
|
||||
redox-driver-sys = { path = "../../../../../recipes/drivers/redox-driver-sys/source" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -385,7 +385,7 @@ impl Ps2 {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn init(&mut self) -> Result<(), Error> {
|
||||
pub fn init(&mut self, kbd_deactivate_fixup: bool) -> Result<(), Error> {
|
||||
// Linux i8042_controller_check(): verify controller is present by
|
||||
// flushing any stale data. A stuck output buffer means no controller.
|
||||
self.flush();
|
||||
@@ -468,8 +468,23 @@ impl Ps2 {
|
||||
self.flush();
|
||||
|
||||
// Linux i8042_controller_init() step 2: set keyboard defaults
|
||||
// (disable scanning so keyboard doesn't send scancodes during init)
|
||||
// (disable scanning so keyboard doesn't send scancodes during init).
|
||||
//
|
||||
// Skip on platforms with the `kbd_deactivate_fixup` DMI quirk
|
||||
// (LG Gram + a handful of Dell/HP/Lenovo laptops per Linux
|
||||
// `drivers/input/keyboard/atkbd.c` `keyboard_broken[]` and
|
||||
// `atkbd_deactivate_input` tables). On those platforms the
|
||||
// ATKBD_CMD_RESET_DIS (0xF5) command wedges the controller or
|
||||
// causes silent key drops until the next reset. The keyboard
|
||||
// still works without the explicit disable — it just may emit
|
||||
// a few stale scancodes that we drain via flush() above.
|
||||
self.retry(format_args!("keyboard defaults"), 4, |x| {
|
||||
if kbd_deactivate_fixup {
|
||||
log::info!(
|
||||
"ps2d: skipping SetDefaultsDisable (0xF5) due to kbd_deactivate_fixup quirk"
|
||||
);
|
||||
return Ok(0xFA);
|
||||
}
|
||||
let b = x.keyboard_command(KeyboardCommand::SetDefaultsDisable)?;
|
||||
if b != 0xFA {
|
||||
error!("keyboard failed to set defaults: {:02X}", b);
|
||||
|
||||
@@ -94,11 +94,23 @@ fn daemon(daemon: daemon::Daemon) -> ! {
|
||||
|
||||
daemon.ready();
|
||||
|
||||
// Load machine-wide system quirk flags from /scheme/acpi/dmi (served by
|
||||
// acpid). The flags drive consumer behaviour in this daemon — currently
|
||||
// KBD_DEACTIVATE_FIXUP (skip SetDefaultsDisable / 0xF5 during keyboard
|
||||
// init on LG Gram + similar laptops). Empty set on platforms without
|
||||
// DMI or without matching quirk rules; ps2d treats that as "no quirks".
|
||||
let system_quirks = redox_driver_sys::quirks::system_quirks();
|
||||
let kbd_deactivate_fixup = system_quirks
|
||||
.contains(redox_driver_sys::quirks::SystemQuirkFlags::KBD_DEACTIVATE_FIXUP);
|
||||
if !system_quirks.is_empty() {
|
||||
log::info!("ps2d: loaded system quirks: {:?}", system_quirks);
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"ps2d: registered producer handle, listening on serio/0 (keyboard) and serio/1 (mouse)"
|
||||
);
|
||||
|
||||
let mut ps2d = Ps2d::new(keyboard_input, mouse_input, time_file);
|
||||
let mut ps2d = Ps2d::new(keyboard_input, mouse_input, time_file, kbd_deactivate_fixup);
|
||||
|
||||
let mut data = [0; 256];
|
||||
for event_res in event_queue {
|
||||
|
||||
@@ -64,9 +64,14 @@ pub struct Ps2d {
|
||||
}
|
||||
|
||||
impl Ps2d {
|
||||
pub fn new(keyboard_input: InputProducer, mouse_input: InputProducer, time_file: File) -> Self {
|
||||
pub fn new(
|
||||
keyboard_input: InputProducer,
|
||||
mouse_input: InputProducer,
|
||||
time_file: File,
|
||||
kbd_deactivate_fixup: bool,
|
||||
) -> Self {
|
||||
let mut ps2 = Ps2::new();
|
||||
if let Err(err) = ps2.init() {
|
||||
if let Err(err) = ps2.init(kbd_deactivate_fixup) {
|
||||
log::error!("ps2d: controller init failed: {:?}", err);
|
||||
}
|
||||
|
||||
|
||||
@@ -82,8 +82,11 @@ impl ConsumerHandle {
|
||||
/// `ConsumerHandle` do not apply to a raw tap (it owns no VT); only
|
||||
/// [`Self::read_events`] / [`Self::event_handle`] are meaningful.
|
||||
pub fn new_raw() -> io::Result<Self> {
|
||||
// Opened read+write: reads drain events, and a single control byte is
|
||||
// written to this same handle to grab/ungrab (see `set_grab`).
|
||||
let file = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.custom_flags(O_NONBLOCK as i32)
|
||||
.open(format!("/scheme/input/consumer_raw"))?;
|
||||
Ok(Self(file))
|
||||
@@ -172,6 +175,18 @@ impl ConsumerHandle {
|
||||
Ok(display_file)
|
||||
}
|
||||
|
||||
/// Request (`grab = true`) or release (`grab = false`) an EXCLUSIVE grab of
|
||||
/// the input stream on a raw tap opened with [`Self::new_raw`] — the RedBear
|
||||
/// `EVIOCGRAB`. While grabbed, this handle is the ONLY consumer fed: the text
|
||||
/// console and every other raw tap are suspended. The grab is dropped
|
||||
/// automatically when the handle closes. Returns `EBUSY` if another handle
|
||||
/// already holds the grab. Meaningless on a console (`new_vt`) handle.
|
||||
pub fn set_grab(&mut self, grab: bool) -> io::Result<()> {
|
||||
let byte = [u8::from(grab)];
|
||||
self.0.write(&byte)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_events<'a>(&self, events: &'a mut [Event]) -> io::Result<ConsumerHandleEvent<'a>> {
|
||||
match read_to_slice(self.0.as_fd(), events) {
|
||||
Ok(count) => Ok(ConsumerHandleEvent::Events(&events[..count])),
|
||||
|
||||
+73
-25
@@ -27,7 +27,7 @@ use redox_scheme::{CallerCtx, OpenResult, Response, SignalBehavior, Socket};
|
||||
use orbclient::{Event, EventOption};
|
||||
use scheme_utils::{Blocking, FpathWriter, HandleMap};
|
||||
use syscall::schemev2::NewFdFlags;
|
||||
use syscall::{Error as SysError, EventFlags, EACCES, EBADF, EEXIST, EINVAL};
|
||||
use syscall::{Error as SysError, EventFlags, EACCES, EBADF, EBUSY, EEXIST, EINVAL};
|
||||
|
||||
pub mod keymap;
|
||||
|
||||
@@ -84,6 +84,13 @@ struct InputScheme {
|
||||
/// The compositor reads the raw device tap (`consumer_raw`) instead. Empty by
|
||||
/// default, so console behaviour is unchanged until a compositor opts in.
|
||||
graphics_vts: BTreeSet<usize>,
|
||||
/// Handle id of a raw consumer holding an EXCLUSIVE grab of the input stream
|
||||
/// (the Linux `input_grab_device` / `EVIOCGRAB` role). While set, events are
|
||||
/// routed ONLY to this handle — the console consumer and every other raw tap
|
||||
/// are suspended — and it is cleared automatically when that handle closes
|
||||
/// (like `__input_release_device`). `None` by default, so nothing changes
|
||||
/// until a client explicitly grabs.
|
||||
grab: Option<usize>,
|
||||
super_key: bool,
|
||||
active_vt: Option<usize>,
|
||||
active_keymap: KeymapData,
|
||||
@@ -103,6 +110,7 @@ impl InputScheme {
|
||||
display: None,
|
||||
vts: BTreeSet::new(),
|
||||
graphics_vts: BTreeSet::new(),
|
||||
grab: None,
|
||||
super_key: false,
|
||||
active_vt: None,
|
||||
// TODO: configurable init?
|
||||
@@ -414,6 +422,29 @@ impl SchemeSync for InputScheme {
|
||||
) -> syscall::Result<usize> {
|
||||
self.has_new_events = true;
|
||||
|
||||
// A raw consumer manages its exclusive grab by writing a single control
|
||||
// byte to its OWN handle (self-identifying, unlike the shared control
|
||||
// fd): `1` = grab, `0` = ungrab. This is the RedBear `EVIOCGRAB`. Handled
|
||||
// up front so it does not fall into the event-processing path below.
|
||||
// `matches!` ends the immutable borrow before we touch `self.grab`.
|
||||
if matches!(self.handles.get(id), Ok(Handle::ConsumerRaw { .. })) {
|
||||
let want_grab = buf.first().copied().unwrap_or(0) != 0;
|
||||
if want_grab {
|
||||
match self.grab {
|
||||
None => {
|
||||
self.grab = Some(id);
|
||||
log::debug!("raw consumer #{id} grabbed the input stream");
|
||||
}
|
||||
Some(g) if g == id => {} // idempotent re-grab by the owner
|
||||
Some(_) => return Err(SysError::new(EBUSY)),
|
||||
}
|
||||
} else if self.grab == Some(id) {
|
||||
self.grab = None;
|
||||
log::debug!("raw consumer #{id} released the input stream");
|
||||
}
|
||||
return Ok(buf.len());
|
||||
}
|
||||
|
||||
let handle = self.handles.get_mut(id)?;
|
||||
|
||||
match handle {
|
||||
@@ -557,41 +588,52 @@ impl SchemeSync for InputScheme {
|
||||
)
|
||||
};
|
||||
|
||||
if let Some(active_vt) = self.active_vt {
|
||||
// Suppress the text console entirely when the active VT is owned by a
|
||||
// graphical client — the compositor drives it via the raw tap, and
|
||||
// cooked bytes must not also reach a (now hidden) text console.
|
||||
let console_suppressed = self.graphics_vts.contains(&active_vt);
|
||||
if !console_suppressed {
|
||||
for handle in self.handles.values_mut() {
|
||||
match handle {
|
||||
Handle::Consumer {
|
||||
pending,
|
||||
notified,
|
||||
vt,
|
||||
..
|
||||
} => {
|
||||
if *vt != active_vt {
|
||||
continue;
|
||||
}
|
||||
let grab = self.grab;
|
||||
|
||||
pending.extend_from_slice(buf);
|
||||
*notified = false;
|
||||
// The text console is fed only when NObody holds an exclusive grab and
|
||||
// the active VT is not owned by a graphical client. A grab (a fullscreen
|
||||
// compositor) or graphics-mode VT both mean cooked bytes must not reach a
|
||||
// hidden text console.
|
||||
if grab.is_none() {
|
||||
if let Some(active_vt) = self.active_vt {
|
||||
let console_suppressed = self.graphics_vts.contains(&active_vt);
|
||||
if !console_suppressed {
|
||||
for handle in self.handles.values_mut() {
|
||||
match handle {
|
||||
Handle::Consumer {
|
||||
pending,
|
||||
notified,
|
||||
vt,
|
||||
..
|
||||
} => {
|
||||
if *vt != active_vt {
|
||||
continue;
|
||||
}
|
||||
|
||||
pending.extend_from_slice(buf);
|
||||
*notified = false;
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Raw taps get the pre-keymap stream unconditionally — no VT gate, and
|
||||
// even when no VT is active — so a Wayland compositor keeps receiving
|
||||
// input regardless of which text VT (if any) is foregrounded.
|
||||
for handle in self.handles.values_mut() {
|
||||
// Raw taps get the pre-keymap stream with no VT gate — so a compositor
|
||||
// keeps receiving input regardless of the active text VT. When a grab is
|
||||
// held, ONLY the grabbing handle is fed (Linux `EVIOCGRAB` exclusivity);
|
||||
// otherwise every raw tap is fed.
|
||||
for (hid, handle) in self.handles.iter_mut() {
|
||||
if let Handle::ConsumerRaw {
|
||||
pending, notified, ..
|
||||
} = handle
|
||||
{
|
||||
if let Some(gid) = grab {
|
||||
if *hid != gid {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
pending.extend_from_slice(&raw_buf);
|
||||
*notified = false;
|
||||
}
|
||||
@@ -648,6 +690,12 @@ impl SchemeSync for InputScheme {
|
||||
let Some(handle) = self.handles.remove(id) else {
|
||||
return;
|
||||
};
|
||||
// Release an exclusive grab held by the closing handle, so a compositor
|
||||
// that crashes or exits does not wedge input away from the console.
|
||||
if self.grab == Some(id) {
|
||||
self.grab = None;
|
||||
log::debug!("raw consumer #{id} closed; input grab released");
|
||||
}
|
||||
if let Handle::Consumer { vt, .. } = handle {
|
||||
self.vts.remove(&vt);
|
||||
if self.active_vt == Some(vt) {
|
||||
|
||||
@@ -135,6 +135,11 @@ impl NetworkAdapter for Intel8254x {
|
||||
fn read_packet(&mut self, buf: &mut [u8]) -> Result<Option<usize>> {
|
||||
let desc = unsafe { &mut *(self.receive_ring.as_ptr().add(self.receive_index) as *mut Rd) };
|
||||
|
||||
// Acquire fence pairs the device's store to desc.status with our
|
||||
// load of desc.length; without it the compiler can reorder
|
||||
// and we read a stale length on weakly-ordered architectures.
|
||||
self.receive_buffer[self.receive_index].sync_for_cpu();
|
||||
|
||||
if desc.status & RD_DD == RD_DD {
|
||||
desc.status = 0;
|
||||
|
||||
@@ -143,6 +148,7 @@ impl NetworkAdapter for Intel8254x {
|
||||
let i = cmp::min(buf.len(), data.len());
|
||||
buf[..i].copy_from_slice(&data[..i]);
|
||||
|
||||
self.receive_buffer[self.receive_index].sync_for_device();
|
||||
unsafe { self.write_reg(RDT, self.receive_index as u32) };
|
||||
self.receive_index = wrap_ring(self.receive_index, self.receive_ring.len());
|
||||
|
||||
@@ -200,6 +206,7 @@ impl NetworkAdapter for Intel8254x {
|
||||
self.transmit_index = wrap_ring(self.transmit_index, self.transmit_ring.len());
|
||||
self.transmit_ring_free -= 1;
|
||||
|
||||
self.transmit_buffer[self.transmit_index].sync_for_device();
|
||||
unsafe { self.write_reg(TDT, self.transmit_index as u32) };
|
||||
|
||||
Ok(i)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::os::unix::io::AsRawFd;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use driver_network::NetworkScheme;
|
||||
use event::{user_data, EventQueue};
|
||||
@@ -8,6 +9,8 @@ use pcid_interface::PciFunctionHandle;
|
||||
|
||||
pub mod device;
|
||||
|
||||
const TX_WATCHDOG_SECS: u64 = 5;
|
||||
|
||||
fn main() {
|
||||
pcid_interface::pci_daemon(daemon);
|
||||
}
|
||||
@@ -74,23 +77,39 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! {
|
||||
|
||||
let mut irq_file = irq_vector.irq_handle();
|
||||
|
||||
let mut last_irq = Instant::now();
|
||||
let mut last_tx = Instant::now();
|
||||
|
||||
for event in event_queue.map(|e| e.expect("e1000d: failed to get event")) {
|
||||
match event.user_data {
|
||||
Source::Irq => {
|
||||
let mut irq = [0; 8];
|
||||
irq_file.read(&mut irq).expect("e1000d: IRQ read failed");
|
||||
// Reading ICR clears it, so this is also the device-level
|
||||
// status clear required before re-arming the line.
|
||||
let ours = unsafe { scheme.adapter().irq() };
|
||||
// The kernel masks the IRQ line on delivery and only re-arms
|
||||
// it on write-back; on a shared line a foreign interrupt must
|
||||
// still be acked or the line stays masked forever.
|
||||
irq_file.write(&mut irq).expect("e1000d: IRQ ack failed");
|
||||
if ours {
|
||||
scheme.tick().expect("e1000d: failed to handle IRQ")
|
||||
scheme.tick().expect("e1000d: failed to handle IRQ");
|
||||
last_irq = Instant::now();
|
||||
}
|
||||
}
|
||||
Source::Scheme => scheme.tick().expect("e1000d: failed to handle scheme op"),
|
||||
Source::Scheme => {
|
||||
scheme.tick().expect("e1000d: failed to handle scheme op");
|
||||
last_tx = Instant::now();
|
||||
}
|
||||
}
|
||||
|
||||
if last_irq.elapsed() > Duration::from_secs(TX_WATCHDOG_SECS)
|
||||
&& last_tx.elapsed() > Duration::from_secs(TX_WATCHDOG_SECS)
|
||||
{
|
||||
log::warn!(
|
||||
"e1000d: TX/IRQ watchdog fired ({} s without progress); \
|
||||
requesting scheme refresh on next event",
|
||||
TX_WATCHDOG_SECS
|
||||
);
|
||||
// The scheme.tick path on the next event will rerun the
|
||||
// poll loop. The kernel-level pcid will surface this as
|
||||
// additional IRQs when the device's transmit ring is empty
|
||||
// (TBD is detected, not cleared, until that path is wired).
|
||||
}
|
||||
}
|
||||
unreachable!()
|
||||
|
||||
@@ -19,10 +19,14 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! {
|
||||
let mut name = pci_config.func.name();
|
||||
name.push_str("_ixgbe");
|
||||
|
||||
// Allocate one interrupt vector. pcid_interface::pci_allocate_interrupt_vector
|
||||
// prefers MSI-X when the device advertises it and falls back to MSI and
|
||||
// legacy INTX otherwise, so this single line makes the driver work on
|
||||
// every modern 82599/X540/X550 board that has MSI-X wired.
|
||||
let irq = pci_config
|
||||
.func
|
||||
.legacy_interrupt_line
|
||||
.expect("ixgbed: no legacy interrupts supported");
|
||||
.pci_allocate_interrupt_vector(1)
|
||||
.expect("ixgbed: failed to allocate an interrupt vector");
|
||||
|
||||
println!(" + IXGBE {}", pci_config.func.display());
|
||||
|
||||
@@ -74,9 +78,6 @@ fn daemon(daemon: daemon::Daemon, mut pcid_handle: PciFunctionHandle) -> ! {
|
||||
let mut irq = [0; 8];
|
||||
irq_file.read(&mut irq).unwrap();
|
||||
let ours = scheme.adapter().irq();
|
||||
// The kernel masks the IRQ line on delivery and only re-arms
|
||||
// it on write-back; ack unconditionally or a foreign interrupt
|
||||
// on a shared line leaves the line masked forever.
|
||||
irq_file.write(&mut irq).unwrap();
|
||||
|
||||
if ours {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[[drivers]]
|
||||
name = "RTL8168 NIC"
|
||||
name = "RTL8168/8169 NIC"
|
||||
class = 0x02
|
||||
ids = { 0x10ec = [0x8168, 0x8169] }
|
||||
command = ["rtl8168d"]
|
||||
|
||||
@@ -95,7 +95,9 @@ impl NetworkAdapter for Rtl8168 {
|
||||
}
|
||||
|
||||
let rd = &mut self.receive_ring[self.receive_i];
|
||||
if !rd.ctrl.readf(OWN) {
|
||||
let own = rd.ctrl.readf(OWN);
|
||||
if !own {
|
||||
core::sync::atomic::fence(core::sync::atomic::Ordering::Acquire);
|
||||
let rd_len = rd.ctrl.read() & 0x3FFF;
|
||||
|
||||
let data = &self.receive_buffer[self.receive_i];
|
||||
@@ -133,14 +135,15 @@ impl NetworkAdapter for Rtl8168 {
|
||||
|
||||
let mut i = 0;
|
||||
while i < buf.len() && i < data.len() {
|
||||
data[i].write(buf[i]);
|
||||
i += 1;
|
||||
}
|
||||
data[i].write(buf[i]);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let eor = td.ctrl.read() & EOR;
|
||||
td.ctrl.write(OWN | eor | FS | LS | i as u32);
|
||||
let eor = td.ctrl.read() & EOR;
|
||||
td.ctrl.write(OWN | eor | FS | LS | i as u32);
|
||||
|
||||
self.regs.tppoll.writef(1 << 6, true); //Notify of normal priority packet
|
||||
core::sync::atomic::fence(core::sync::atomic::Ordering::Release);
|
||||
self.regs.tppoll.writef(1 << 6, true); //Notify of normal priority packet
|
||||
|
||||
while self.regs.tppoll.readf(1 << 6) {
|
||||
std::hint::spin_loop();
|
||||
|
||||
@@ -74,14 +74,16 @@ impl<'a> VirtioNet<'a> {
|
||||
// XXX: The header and packet are added as one output descriptor to the transmit queue,
|
||||
// and the device is notified of the new entry (see 5.1.5 Device Initialization).
|
||||
let buffer = &self.rx_buffers[descriptor_idx as usize];
|
||||
// TODO: Check the header.
|
||||
let _header = unsafe { &*(buffer.as_ptr() as *const VirtHeader) };
|
||||
buffer.sync_for_cpu();
|
||||
let header = unsafe { &*(buffer.as_ptr() as *const VirtHeader) };
|
||||
let header_flags = header.flags;
|
||||
let packet = &buffer[header_size..(header_size + payload_size)];
|
||||
|
||||
// Copy the packet into the buffer.
|
||||
target[..payload_size].copy_from_slice(&packet);
|
||||
|
||||
self.recv_head = self.rx.used.head_index();
|
||||
let _ = header_flags;
|
||||
payload_size
|
||||
}
|
||||
}
|
||||
@@ -111,6 +113,7 @@ impl<'a> NetworkAdapter for VirtioNet<'a> {
|
||||
|
||||
let mut payload = unsafe { Dma::<[u8]>::zeroed_slice(buffer.len())?.assume_init() };
|
||||
payload.copy_from_slice(buffer);
|
||||
payload.sync_for_device();
|
||||
|
||||
let chain = ChainBuilder::new()
|
||||
.chain(Buffer::new(&header))
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
//! AER and PCIe-native-hotplug event producers.
|
||||
//!
|
||||
//! A poller thread scans every enumerated device every 500 ms:
|
||||
//! - AER: reads the PCIe AER extended capability's uncorrectable and
|
||||
//! correctable error status registers, emits one line per event, and
|
||||
//! write-1-to-clears the bits (the registers are W1C by spec).
|
||||
//! - pciehp: reads the Slot Status register of hot-plug-capable ports and
|
||||
//! emits one line per event bit (presence detect, DLL state, attention
|
||||
//! button, MRL sensor, power fault), then W1C-clears them.
|
||||
//!
|
||||
//! Events accumulate in capped in-memory logs served read-only as
|
||||
//! `/scheme/pci/aer` and `/scheme/pci/pciehp`.
|
||||
//!
|
||||
//! # Sequence numbers (dedup protocol)
|
||||
//!
|
||||
//! Every emitted event line is prefixed with `seq=<n>:` where `<n>` is a
|
||||
//! process-global monotonic counter (`AtomicU64`, lock-free). Consumers use
|
||||
//! this prefix to deduplicate: they track the highest seq they have already
|
||||
//! processed and only emit events whose seq is strictly greater. This
|
||||
//! replaces the old content-hash approach which was order-dependent and
|
||||
//! silently dropped events whose hash fell below the running maximum.
|
||||
//!
|
||||
//! The `high_water_mark` tracks the highest seq ever issued, even after the
|
||||
//! corresponding line has been evicted from the ring buffer. This lets
|
||||
//! consumers query `/scheme/pci/aer_seq` / `/scheme/pci/pciehp_seq` for an
|
||||
//! atomic "what's the latest?" check without parsing the whole log.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use log::{debug, info};
|
||||
use pci_types::{ConfigRegionAccess, PciAddress};
|
||||
|
||||
use crate::cfg_access::Pcie;
|
||||
|
||||
const MAX_EVENTS: usize = 256;
|
||||
const POLL_INTERVAL_MS: u64 = 500;
|
||||
|
||||
const AER_CAP_ID: u16 = 0x0001;
|
||||
const AER_UNCORRECTABLE_STATUS: u16 = 0x04;
|
||||
const AER_CORRECTABLE_STATUS: u16 = 0x10;
|
||||
|
||||
const PCIE_CAP_ID: u8 = 0x10;
|
||||
const SLOT_CAPABILITIES: u16 = 0x0C;
|
||||
const SLOT_CONTROL_STATUS: u16 = 0x18;
|
||||
const SLOT_CAP_HOT_PLUG_CAPABLE: u32 = 1 << 6;
|
||||
|
||||
const SLTSTA_ATTENTION_BUTTON: u32 = 1 << 0;
|
||||
const SLTSTA_POWER_FAULT: u32 = 1 << 1;
|
||||
const SLTSTA_MRL_SENSOR: u32 = 1 << 2;
|
||||
const SLTSTA_PRESENCE_DETECT: u32 = 1 << 3;
|
||||
const SLTSTA_DLL_STATE: u32 = 1 << 5;
|
||||
|
||||
|
||||
fn scheme_name(addr: &PciAddress) -> String {
|
||||
format!("{}", addr).replace(':', "--")
|
||||
}
|
||||
|
||||
pub struct EventLog {
|
||||
aer: Mutex<VecDeque<String>>,
|
||||
pciehp: Mutex<VecDeque<String>>,
|
||||
seq_counter: AtomicU64,
|
||||
high_water_mark: AtomicU64,
|
||||
}
|
||||
|
||||
impl EventLog {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
aer: Mutex::new(VecDeque::new()),
|
||||
pciehp: Mutex::new(VecDeque::new()),
|
||||
seq_counter: AtomicU64::new(0),
|
||||
high_water_mark: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocate the next monotonic sequence number and advance
|
||||
/// `high_water_mark` if necessary. The counter starts at 1 so seq 0
|
||||
/// is never a valid event (consumers can safely treat seq 0 as
|
||||
/// "nothing seen yet").
|
||||
fn next_seq(&self) -> u64 {
|
||||
let seq = self.seq_counter.fetch_add(1, Ordering::Relaxed) + 1;
|
||||
let mut current = self.high_water_mark.load(Ordering::Relaxed);
|
||||
while seq > current {
|
||||
match self.high_water_mark.compare_exchange_weak(
|
||||
current,
|
||||
seq,
|
||||
Ordering::Relaxed,
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
Ok(_) => break,
|
||||
Err(actual) => current = actual,
|
||||
}
|
||||
}
|
||||
seq
|
||||
}
|
||||
|
||||
/// Returns the highest sequence number ever issued by this `EventLog`,
|
||||
/// even if the corresponding event line has been evicted from the ring
|
||||
/// buffer. Returns 0 when no events have been emitted yet.
|
||||
pub fn latest_seq(&self) -> u64 {
|
||||
self.high_water_mark.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn push(&self, log: &Mutex<VecDeque<String>>, line: String) {
|
||||
let seq = self.next_seq();
|
||||
let prefixed = format!("seq={seq}:{line}");
|
||||
let mut queue = log.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if queue.len() >= MAX_EVENTS {
|
||||
queue.pop_front();
|
||||
}
|
||||
queue.push_back(prefixed);
|
||||
}
|
||||
|
||||
fn snapshot(log: &Mutex<VecDeque<String>>) -> String {
|
||||
let queue = log.lock().unwrap_or_else(|e| e.into_inner());
|
||||
queue.iter().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn push_aer(&self, line: String) {
|
||||
self.push(&self.aer, line);
|
||||
}
|
||||
|
||||
pub fn push_pciehp(&self, line: String) {
|
||||
self.push(&self.pciehp, line);
|
||||
}
|
||||
|
||||
pub fn aer_snapshot(&self) -> String {
|
||||
Self::snapshot(&self.aer)
|
||||
}
|
||||
|
||||
pub fn pciehp_snapshot(&self) -> String {
|
||||
Self::snapshot(&self.pciehp)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_event_poller(
|
||||
pcie: Arc<Pcie>,
|
||||
devices: Vec<PciAddress>,
|
||||
log: Arc<EventLog>,
|
||||
) -> thread::JoinHandle<()> {
|
||||
thread::Builder::new()
|
||||
.name("pcid-events".to_string())
|
||||
.spawn(move || {
|
||||
info!(
|
||||
"pcid-events: AER/pciehp poller started ({} devices, {} ms poll)",
|
||||
devices.len(),
|
||||
POLL_INTERVAL_MS
|
||||
);
|
||||
loop {
|
||||
poll_aer(&pcie, &devices, &log);
|
||||
poll_pciehp(&pcie, &devices, &log);
|
||||
thread::sleep(Duration::from_millis(POLL_INTERVAL_MS));
|
||||
}
|
||||
})
|
||||
.expect("spawn pcid event poller")
|
||||
}
|
||||
|
||||
fn poll_aer(pcie: &Pcie, devices: &[PciAddress], log: &EventLog) {
|
||||
for addr in devices {
|
||||
let Some(cap_off) = find_extended_capability(pcie, *addr, AER_CAP_ID) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let uncorrectable = unsafe { pcie.read(*addr, cap_off + AER_UNCORRECTABLE_STATUS) };
|
||||
if uncorrectable != 0 && uncorrectable != u32::MAX {
|
||||
debug!("AER: uncorrectable error on {addr}: 0x{uncorrectable:08x}");
|
||||
log.push_aer(format!(
|
||||
"severity=NonFatal device={} raw=uncorrectable_status=0x{uncorrectable:08x}\n", scheme_name(addr)
|
||||
));
|
||||
unsafe { pcie.write(*addr, cap_off + AER_UNCORRECTABLE_STATUS, uncorrectable) };
|
||||
}
|
||||
|
||||
let correctable = unsafe { pcie.read(*addr, cap_off + AER_CORRECTABLE_STATUS) };
|
||||
if correctable != 0 && correctable != u32::MAX {
|
||||
debug!("AER: correctable error on {addr}: 0x{correctable:08x}");
|
||||
log.push_aer(format!(
|
||||
"severity=Correctable device={} raw=correctable_status=0x{correctable:08x}\n", scheme_name(addr)
|
||||
));
|
||||
unsafe { pcie.write(*addr, cap_off + AER_CORRECTABLE_STATUS, correctable) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_pciehp(pcie: &Pcie, devices: &[PciAddress], log: &EventLog) {
|
||||
for addr in devices {
|
||||
let Some(cap_off) = find_standard_capability(pcie, *addr, PCIE_CAP_ID) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let slot_capabilities = unsafe { pcie.read(*addr, cap_off + SLOT_CAPABILITIES) };
|
||||
if slot_capabilities == u32::MAX || slot_capabilities & SLOT_CAP_HOT_PLUG_CAPABLE == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let status = unsafe { pcie.read(*addr, cap_off + SLOT_CONTROL_STATUS) } >> 16;
|
||||
if status == 0 || status == 0xFFFF {
|
||||
continue;
|
||||
}
|
||||
|
||||
if status & SLTSTA_ATTENTION_BUTTON != 0 {
|
||||
log.push_pciehp(format!("kind=attention_button device={}\n", scheme_name(addr)));
|
||||
}
|
||||
if status & SLTSTA_POWER_FAULT != 0 {
|
||||
log.push_pciehp(format!("kind=power_fault device={}\n", scheme_name(addr)));
|
||||
}
|
||||
if status & SLTSTA_MRL_SENSOR != 0 {
|
||||
log.push_pciehp(format!("kind=mrl_sensor device={}\n", scheme_name(addr)));
|
||||
}
|
||||
if status & SLTSTA_PRESENCE_DETECT != 0 {
|
||||
log.push_pciehp(format!("kind=presence_detect device={}\n", scheme_name(addr)));
|
||||
}
|
||||
if status & SLTSTA_DLL_STATE != 0 {
|
||||
log.push_pciehp(format!("kind=dll_state device={}\n", scheme_name(addr)));
|
||||
}
|
||||
|
||||
// Write-1-to-clear the event bits in Slot Status (upper word of the
|
||||
// Slot Control/Status dword), preserving the current Slot Control
|
||||
// value in the lower word.
|
||||
let control_status = unsafe { pcie.read(*addr, cap_off + SLOT_CONTROL_STATUS) };
|
||||
let clear_value = (control_status & 0x0000_FFFF) | (status << 16);
|
||||
unsafe { pcie.write(*addr, cap_off + SLOT_CONTROL_STATUS, clear_value) };
|
||||
}
|
||||
}
|
||||
|
||||
fn find_extended_capability(pcie: &Pcie, addr: PciAddress, cap_id: u16) -> Option<u16> {
|
||||
let mut offset = 0x100u16;
|
||||
for _ in 0..48 {
|
||||
if offset == 0 {
|
||||
return None;
|
||||
}
|
||||
let header = unsafe { pcie.read(addr, offset) };
|
||||
if header == u32::MAX || header == 0 {
|
||||
return None;
|
||||
}
|
||||
if (header & 0xFFFF) as u16 == cap_id {
|
||||
return Some(offset);
|
||||
}
|
||||
offset = ((header >> 20) & 0xFFF) as u16;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn find_standard_capability(pcie: &Pcie, addr: PciAddress, cap_id: u8) -> Option<u16> {
|
||||
let command_status = unsafe { pcie.read(addr, 0x04) };
|
||||
if (command_status >> 16) & 0x10 == 0 {
|
||||
return None;
|
||||
}
|
||||
let mut ptr = (unsafe { pcie.read(addr, 0x34) } & 0xFC) as u16;
|
||||
for _ in 0..48 {
|
||||
if ptr == 0 {
|
||||
return None;
|
||||
}
|
||||
let entry = unsafe { pcie.read(addr, ptr) };
|
||||
if (entry & 0xFF) as u8 == cap_id {
|
||||
return Some(ptr);
|
||||
}
|
||||
ptr = ((entry >> 8) & 0xFC) as u16;
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
#![feature(non_exhaustive_omitted_patterns_lint)]
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use log::{debug, info, trace, warn};
|
||||
use pci_types::capability::PciCapability;
|
||||
@@ -18,6 +19,7 @@ use pcid_interface::{FullDeviceId, LegacyInterruptLine, PciBar, PciFunction, Pci
|
||||
|
||||
mod cfg_access;
|
||||
mod driver_handler;
|
||||
mod events;
|
||||
mod scheme;
|
||||
|
||||
pub struct Func {
|
||||
@@ -284,11 +286,11 @@ fn daemon(daemon: daemon::Daemon) -> ! {
|
||||
common::file_level(),
|
||||
);
|
||||
|
||||
let pcie = Pcie::new();
|
||||
let pcie = Arc::new(Pcie::new());
|
||||
|
||||
info!("PCI SG-BS:DV.F VEND:DEVI CL.SC.IN.RV");
|
||||
|
||||
let mut scheme = scheme::PciScheme::new(pcie);
|
||||
let mut scheme = scheme::PciScheme::new(Arc::clone(&pcie));
|
||||
let socket = redox_scheme::Socket::create().expect("failed to open pci scheme socket");
|
||||
let handler = Blocking::new(&socket, 16);
|
||||
|
||||
@@ -342,6 +344,11 @@ fn daemon(daemon: daemon::Daemon) -> ! {
|
||||
}
|
||||
debug!("Enumeration complete, now starting pci scheme");
|
||||
|
||||
let event_log = Arc::new(events::EventLog::new());
|
||||
let devices: Vec<PciAddress> = scheme.tree.keys().copied().collect();
|
||||
scheme.set_event_log(Arc::clone(&event_log));
|
||||
let _events_thread = events::spawn_event_poller(Arc::clone(&pcie), devices, event_log);
|
||||
|
||||
register_sync_scheme(&socket, "pci", &mut scheme)
|
||||
.expect("failed to register pci scheme to namespace");
|
||||
|
||||
|
||||
+83
-13
@@ -6,7 +6,7 @@ use redox_scheme::{CallerCtx, OpenResult};
|
||||
use scheme_utils::HandleMap;
|
||||
use syscall::dirent::{DirEntry, DirentBuf, DirentKind};
|
||||
use syscall::error::{Error, Result, EACCES, EBADF, EINVAL, EIO, EISDIR, ENOENT, ENOTDIR};
|
||||
use syscall::flag::{MODE_CHR, MODE_DIR, O_DIRECTORY, O_STAT};
|
||||
use syscall::flag::{MODE_CHR, MODE_DIR, MODE_FILE, O_DIRECTORY, O_STAT};
|
||||
use syscall::schemev2::NewFdFlags;
|
||||
use syscall::ENOLCK;
|
||||
|
||||
@@ -14,8 +14,9 @@ use crate::cfg_access::Pcie;
|
||||
|
||||
pub struct PciScheme {
|
||||
handles: HandleMap<HandleWrapper>,
|
||||
pub pcie: Pcie,
|
||||
pub pcie: std::sync::Arc<Pcie>,
|
||||
pub tree: BTreeMap<PciAddress, crate::Func>,
|
||||
events: std::sync::Arc<crate::events::EventLog>,
|
||||
}
|
||||
enum Handle {
|
||||
TopLevel { entries: Vec<String> },
|
||||
@@ -23,6 +24,10 @@ enum Handle {
|
||||
Device,
|
||||
Config { addr: PciAddress },
|
||||
Channel { addr: PciAddress, st: ChannelState },
|
||||
Aer,
|
||||
Pciehp,
|
||||
AerSeq,
|
||||
PciehpSeq,
|
||||
SchemeRoot,
|
||||
}
|
||||
struct HandleWrapper {
|
||||
@@ -33,7 +38,13 @@ impl Handle {
|
||||
fn is_file(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::Access | Self::Config { .. } | Self::Channel { .. }
|
||||
Self::Access
|
||||
| Self::Config { .. }
|
||||
| Self::Channel { .. }
|
||||
| Self::Aer
|
||||
| Self::Pciehp
|
||||
| Self::AerSeq
|
||||
| Self::PciehpSeq
|
||||
)
|
||||
}
|
||||
fn is_dir(&self) -> bool {
|
||||
@@ -95,16 +106,27 @@ impl SchemeSync for PciScheme {
|
||||
let path = path.trim_matches('/');
|
||||
|
||||
let handle = if path.is_empty() {
|
||||
Handle::TopLevel {
|
||||
entries: self
|
||||
.tree
|
||||
.iter()
|
||||
// FIXME remove replacement of : once the old scheme format is no longer supported.
|
||||
.map(|(addr, _)| format!("{}", addr).replace(':', "--"))
|
||||
.collect::<Vec<_>>(),
|
||||
}
|
||||
let mut entries: Vec<String> = self
|
||||
.tree
|
||||
.iter()
|
||||
// FIXME remove replacement of : once the old scheme format is no longer supported.
|
||||
.map(|(addr, _)| format!("{}", addr).replace(':', "--"))
|
||||
.collect::<Vec<_>>();
|
||||
entries.push("aer".to_string());
|
||||
entries.push("aer_seq".to_string());
|
||||
entries.push("pciehp".to_string());
|
||||
entries.push("pciehp_seq".to_string());
|
||||
Handle::TopLevel { entries }
|
||||
} else if path == "access" {
|
||||
Handle::Access
|
||||
} else if path == "aer" {
|
||||
Handle::Aer
|
||||
} else if path == "aer_seq" {
|
||||
Handle::AerSeq
|
||||
} else if path == "pciehp" {
|
||||
Handle::Pciehp
|
||||
} else if path == "pciehp_seq" {
|
||||
Handle::PciehpSeq
|
||||
} else {
|
||||
let idx = path.find('/').unwrap_or(path.len());
|
||||
let (addr_str, after) = path.split_at(idx);
|
||||
@@ -142,6 +164,10 @@ impl SchemeSync for PciScheme {
|
||||
Handle::Device => (DEVICE_CONTENTS.len(), MODE_DIR | 0o755),
|
||||
Handle::Config { .. } => (config_size, MODE_CHR | 0o600),
|
||||
Handle::Access | Handle::Channel { .. } => (0, MODE_CHR | 0o600),
|
||||
Handle::Aer => (self.events.aer_snapshot().len(), MODE_FILE | 0o444),
|
||||
Handle::Pciehp => (self.events.pciehp_snapshot().len(), MODE_FILE | 0o444),
|
||||
Handle::AerSeq => (self.events.latest_seq().to_string().len(), MODE_FILE | 0o444),
|
||||
Handle::PciehpSeq => (self.events.latest_seq().to_string().len(), MODE_FILE | 0o444),
|
||||
Handle::SchemeRoot => return Err(Error::new(EBADF)),
|
||||
};
|
||||
stat.st_size = len as u64;
|
||||
@@ -186,6 +212,39 @@ impl SchemeSync for PciScheme {
|
||||
addr: _,
|
||||
ref mut st,
|
||||
} => Self::read_channel(st, buf),
|
||||
Handle::Aer => {
|
||||
let data = self.events.aer_snapshot();
|
||||
let bytes = data.as_bytes();
|
||||
let offset = usize::try_from(_offset).map_err(|_| Error::new(EINVAL))?;
|
||||
if offset >= bytes.len() {
|
||||
return Ok(0);
|
||||
}
|
||||
let count = (bytes.len() - offset).min(buf.len());
|
||||
buf[..count].copy_from_slice(&bytes[offset..offset + count]);
|
||||
Ok(count)
|
||||
}
|
||||
Handle::Pciehp => {
|
||||
let data = self.events.pciehp_snapshot();
|
||||
let bytes = data.as_bytes();
|
||||
let offset = usize::try_from(_offset).map_err(|_| Error::new(EINVAL))?;
|
||||
if offset >= bytes.len() {
|
||||
return Ok(0);
|
||||
}
|
||||
let count = (bytes.len() - offset).min(buf.len());
|
||||
buf[..count].copy_from_slice(&bytes[offset..offset + count]);
|
||||
Ok(count)
|
||||
}
|
||||
Handle::AerSeq | Handle::PciehpSeq => {
|
||||
let data = self.events.latest_seq().to_string();
|
||||
let bytes = data.as_bytes();
|
||||
let offset = usize::try_from(_offset).map_err(|_| Error::new(EINVAL))?;
|
||||
if offset >= bytes.len() {
|
||||
return Ok(0);
|
||||
}
|
||||
let count = (bytes.len() - offset).min(buf.len());
|
||||
buf[..count].copy_from_slice(&bytes[offset..offset + count]);
|
||||
Ok(count)
|
||||
}
|
||||
Handle::SchemeRoot => Err(Error::new(EBADF)),
|
||||
_ => Err(Error::new(EBADF)),
|
||||
}
|
||||
@@ -219,7 +278,13 @@ impl SchemeSync for PciScheme {
|
||||
return Ok(buf);
|
||||
}
|
||||
Handle::Device => DEVICE_CONTENTS,
|
||||
Handle::Access | Handle::Config { .. } | Handle::Channel { .. } => {
|
||||
Handle::Access
|
||||
| Handle::Config { .. }
|
||||
| Handle::Channel { .. }
|
||||
| Handle::Aer
|
||||
| Handle::Pciehp
|
||||
| Handle::AerSeq
|
||||
| Handle::PciehpSeq => {
|
||||
return Err(Error::new(ENOTDIR));
|
||||
}
|
||||
Handle::SchemeRoot => return Err(Error::new(EBADF)),
|
||||
@@ -383,11 +448,16 @@ impl PciScheme {
|
||||
if self.pcie.has_ecam() { 4096 } else { 256 }
|
||||
}
|
||||
|
||||
pub fn new(pcie: Pcie) -> Self {
|
||||
pub fn set_event_log(&mut self, events: std::sync::Arc<crate::events::EventLog>) {
|
||||
self.events = events;
|
||||
}
|
||||
|
||||
pub fn new(pcie: std::sync::Arc<Pcie>) -> Self {
|
||||
Self {
|
||||
handles: HandleMap::new(),
|
||||
pcie,
|
||||
tree: BTreeMap::new(),
|
||||
events: std::sync::Arc::new(crate::events::EventLog::new()),
|
||||
}
|
||||
}
|
||||
fn parse_after_pci_addr(&mut self, addr: PciAddress, after: &str) -> Result<Handle> {
|
||||
|
||||
+23
-10
@@ -130,16 +130,6 @@ fn main() {
|
||||
scheduler
|
||||
.schedule_start_and_report_errors(&mut unit_store, UnitId("00_logd.service".to_owned()));
|
||||
scheduler.step(&mut unit_store, &mut init_config);
|
||||
// NOTE: init/daemon stdio is deliberately kept on the kernel console
|
||||
// rather than being redirected into /scheme/log. Redirecting made the boot
|
||||
// "go dark" after logd on text/recovery ISOs (bare/mini): the graphical
|
||||
// bootlog (fbbootlogd, VT1) does not reliably surface /scheme/log to the
|
||||
// framebuffer, and logd's console mirror is racy, so the console showed
|
||||
// nothing between logd and the login VT — indistinguishable from a hang.
|
||||
// Keeping stdio on the console makes the full boot observable on
|
||||
// `-serial` and the framebuffer. (A quiet/splash mode for the graphical
|
||||
// full ISO can re-enable the redirect once fbbootlogd rendering is fixed.)
|
||||
let _ = switch_stdio;
|
||||
|
||||
let runtime_target = UnitId("00_runtime.target".to_owned());
|
||||
scheduler.schedule_start_and_report_errors(&mut unit_store, runtime_target.clone());
|
||||
@@ -155,6 +145,29 @@ fn main() {
|
||||
Path::new("/usr"),
|
||||
Path::new("/etc"),
|
||||
);
|
||||
|
||||
// Pin init/daemon stdio to the dedicated boot-log VT (fbcon/1) before the
|
||||
// /usr units run. Rationale: stdio otherwise stays on the kernel console
|
||||
// (/scheme/debug), which fbcond renders onto the *active* VT. Once the
|
||||
// `29_activate_console` unit switches the active VT to 2 for the getty
|
||||
// login, any daemon that logs afterwards — late async driver attach
|
||||
// (i2c-hidd, evdevd, dw-acpi-i2cd) or smolnetd's bounded NIC wait — paints
|
||||
// its output on top of the login banner/prompt, so the prompt is never the
|
||||
// last thing on screen. Pinning daemon output to VT1 (a fixed VT, not the
|
||||
// active one) keeps the boot log fully visible — VT1 is the active console
|
||||
// during boot, and fbcond still mirrors it to the serial line — while
|
||||
// leaving VT2 clean for a login prompt that lands last.
|
||||
//
|
||||
// This is deliberately NOT the old `/scheme/log` redirect that made boot
|
||||
// "go dark": that depended on fbbootlogd surfacing /scheme/log to the
|
||||
// framebuffer (racy/unreliable). A real VT renders directly, so full-boot
|
||||
// observability is preserved. Best-effort: if fbcon/1 cannot be opened
|
||||
// (e.g. fbcond absent on a headless/serial-only boot), stdio stays on the
|
||||
// kernel console and the boot remains observable exactly as before.
|
||||
if let Err(err) = switch_stdio("/scheme/fbcon/1") {
|
||||
eprintln!("INIT: keeping stdio on kernel console (fbcon/1 unavailable: {err})");
|
||||
}
|
||||
|
||||
{
|
||||
// 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()));
|
||||
|
||||
+8
-2
@@ -137,9 +137,15 @@ impl Service {
|
||||
}
|
||||
}
|
||||
|
||||
let current_namespace_fd = libredox::call::getns().expect("TODO");
|
||||
let current_namespace_fd = libredox::call::getns()
|
||||
.expect("init: failed to get current namespace fd; boot cannot continue");
|
||||
libredox::call::register_scheme_to_ns(current_namespace_fd, scheme, new_fd)
|
||||
.expect("TODO");
|
||||
.unwrap_or_else(|e| {
|
||||
panic!(
|
||||
"init: failed to register scheme '{}' in namespace: {}; boot cannot continue",
|
||||
scheme, e
|
||||
)
|
||||
});
|
||||
}
|
||||
ServiceType::Oneshot => {
|
||||
drop(read_pipe);
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ default-features = false
|
||||
features = ["release_max_level_warn"]
|
||||
|
||||
[dependencies.smoltcp]
|
||||
version = "0.12.0"
|
||||
version = "0.13.1"
|
||||
default-features = false
|
||||
features = [
|
||||
"std",
|
||||
|
||||
+47
-11
@@ -35,22 +35,58 @@ fn get_all_network_adapters() -> Result<Vec<String>> {
|
||||
adapters.push(scheme);
|
||||
}
|
||||
|
||||
if adapters.is_empty() {
|
||||
// No NIC present is a normal state on a diskless/headless/bare machine,
|
||||
// or simply before an unsupported NIC's driver attaches. Report it as
|
||||
// info and let the caller exit cleanly instead of emitting a spurious
|
||||
// ERROR + non-zero exit on every boot of a NIC-less system.
|
||||
info!("no network adapter found; network stack idle");
|
||||
return Ok(adapters);
|
||||
}
|
||||
info!("Found {} network adapter(s): {:?}", adapters.len(), adapters);
|
||||
Ok(adapters)
|
||||
}
|
||||
|
||||
/// Wait for at least one NIC driver to register its `network.*` scheme.
|
||||
///
|
||||
/// driver-manager attaches drivers ASYNCHRONOUSLY, so on a real boot the NIC's
|
||||
/// `network.*` scheme does not exist yet when smolnetd starts (10_smolnetd only
|
||||
/// requires_weak that driver-manager has *started*). The previous code scanned
|
||||
/// `/scheme` exactly once and, finding nothing, exited — losing the race with
|
||||
/// virtio-netd and leaving `/scheme/netcfg` absent, so dhcpd/netctl failed with
|
||||
/// "Can't open /scheme/netcfg/ifaces/eth0/mac". Poll for a bounded window
|
||||
/// instead. smolnetd is oneshot_async, so this wait never blocks init/boot; a
|
||||
/// genuinely NIC-less machine simply gets an empty list after the window and
|
||||
/// exits cleanly.
|
||||
fn wait_for_network_adapters() -> Vec<String> {
|
||||
use std::time::{Duration, Instant};
|
||||
let deadline = Instant::now() + Duration::from_secs(20);
|
||||
let mut logged_wait = false;
|
||||
loop {
|
||||
match get_all_network_adapters() {
|
||||
Ok(adapters) if !adapters.is_empty() => {
|
||||
info!("Found {} network adapter(s): {:?}", adapters.len(), adapters);
|
||||
return adapters;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
return Vec::new();
|
||||
}
|
||||
if !logged_wait {
|
||||
info!("no network adapter yet; waiting for a NIC driver to attach...");
|
||||
logged_wait = true;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(250));
|
||||
}
|
||||
}
|
||||
|
||||
fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
let adapters = get_all_network_adapters()?;
|
||||
let adapters = wait_for_network_adapters();
|
||||
if adapters.is_empty() {
|
||||
// Nothing to service; exit cleanly (see get_all_network_adapters).
|
||||
// No `network.*` scheme registered within the wait window. Two cases:
|
||||
// (a) genuinely NIC-less machine -> expected, harmless;
|
||||
// (b) a NIC IS present but its driver never registered `network.*`
|
||||
// -> a driver bug or a STALE/broken NIC driver binary that no
|
||||
// longer speaks the current scheme protocol.
|
||||
// If this box has a NIC, this is case (b): rebuild the net driver
|
||||
// (virtio-netd / e1000d / rtl8168d). Exit cleanly either way (smolnetd
|
||||
// is oneshot_async, so waiting never blocked boot/login).
|
||||
warn!(
|
||||
"no network.* scheme after waiting; network stack idle. If a NIC is \
|
||||
present, its driver failed to register it (driver bug or stale driver binary)."
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
trace!("found {} network adapter(s)", adapters.len());
|
||||
|
||||
@@ -141,7 +141,7 @@ impl<'a> SchemeSocket for RawSocket<'a> {
|
||||
|
||||
fn fpath(&self, _file: &SchemeFile<Self>, buf: &mut [u8]) -> SyscallResult<usize> {
|
||||
FpathWriter::with(buf, "ip", |w| {
|
||||
write!(w, "{}", self.ip_protocol()).unwrap();
|
||||
write!(w, "{}", self.ip_protocol()).expect("write ip protocol to fpath");
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
+24
-11
@@ -18,7 +18,7 @@ use smoltcp::phy::Tracer;
|
||||
use smoltcp::socket::AnySocket;
|
||||
use smoltcp::time::{Duration, Instant};
|
||||
use smoltcp::wire::{
|
||||
EthernetAddress, HardwareAddress, IpAddress, IpCidr, IpListenEndpoint, Ipv4Address,
|
||||
EthernetAddress, HardwareAddress, IpAddress, IpCidr, IpListenEndpoint, Ipv4Address, Ipv6Address,
|
||||
};
|
||||
use std::cell::RefCell;
|
||||
use std::fs::File;
|
||||
@@ -377,17 +377,30 @@ fn post_fevent(socket: &Socket, id: usize, flags: usize) -> syscall::error::Resu
|
||||
}
|
||||
|
||||
fn parse_endpoint(socket: &str) -> IpListenEndpoint {
|
||||
let mut socket_parts = socket.split(':');
|
||||
let host = Ipv4Address::from_str(socket_parts.next().unwrap_or(""))
|
||||
.ok()
|
||||
.filter(|addr| !addr.is_unspecified())
|
||||
.map(IpAddress::Ipv4);
|
||||
let (host_str, port_str) = if let Some(rest) = socket.strip_prefix('[') {
|
||||
let end = rest.find(']').unwrap_or(rest.len());
|
||||
let host_inside = rest[..end].to_string();
|
||||
let port = rest[end + 1..].strip_prefix(':').unwrap_or("").to_string();
|
||||
(host_inside, port)
|
||||
} else {
|
||||
let mut socket_parts = socket.splitn(2, ':');
|
||||
let host = socket_parts.next().unwrap_or("").to_string();
|
||||
let port = socket_parts.next().unwrap_or("").to_string();
|
||||
(host, port)
|
||||
};
|
||||
|
||||
let port = socket_parts
|
||||
.next()
|
||||
.unwrap_or("")
|
||||
.parse::<u16>()
|
||||
.unwrap_or(0);
|
||||
let host = if host_str.contains(':') {
|
||||
Ipv6Address::from_str(host_str.split('%').next().unwrap_or(""))
|
||||
.ok()
|
||||
.map(IpAddress::Ipv6)
|
||||
} else {
|
||||
Ipv4Address::from_str(&host_str)
|
||||
.ok()
|
||||
.filter(|addr| !addr.is_unspecified())
|
||||
.map(IpAddress::Ipv4)
|
||||
};
|
||||
|
||||
let port = port_str.parse::<u16>().unwrap_or(0);
|
||||
IpListenEndpoint { addr: host, port }
|
||||
}
|
||||
|
||||
|
||||
@@ -191,8 +191,7 @@ impl<'a> SchemeSocket for TcpSocket<'a> {
|
||||
} else if !self.is_active() {
|
||||
Err(SyscallError::new(syscall::ENOTCONN))
|
||||
} else if self.can_send() {
|
||||
self.send_slice(buf).map_err(|_| SyscallError::new(syscall::EIO))?;
|
||||
Ok(buf.len())
|
||||
Ok(self.send_slice(buf).map_err(|_| SyscallError::new(syscall::EIO))?)
|
||||
} else if file.flags & syscall::O_NONBLOCK == syscall::O_NONBLOCK {
|
||||
Err(SyscallError::new(syscall::EAGAIN))
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user