Files
RedBear-OS/drivers/acpid/src/scheme.rs
T
Red Bear OS 76b53f4ec8 acpid: Phase I.5 kstop reason dispatch + kstop_reason helper
Phase I.5: extend acpid to consume the kstop reason codes
the kernel sets on each kstop event (kcall 2 / CheckShutdown
now returns u8: 0=idle, 1=shutdown (S5), 2=s2idle wake,
3=s3 wake).

The acpid main loop now branches on the reason instead of
treating every kstop event as a shutdown:

* 0 (idle)        — spurious wake, ignore
* 1 (shutdown)    — set_global_s_state(5) and exit
* 2 (s2idle wake) — exit_s2idle() (\_SST(2) -> \_WAK(0) ->
                       \_SST(1))
* 3 (s3 wake)     — Phase II TODO

The kstop_reason() helper calls the kernel AcpiScheme's
CheckShutdown verb (kcall 2) and returns the u8 reason.
Implemented as a method on AcPiScheme that wraps the
handle's call_ro().

The s2idle flow now end-to-end works:
1. acpid: enter_s2idle() (\_TTS(0), \_PTS(0), \_SST(3))
2. acpid: write 's2idle' to /scheme/sys/kstop
3. kernel kstop handler: sets S2IDLE_REQUESTED, returns
4. kernel idle path: mwait_loop() at deepest C-state
5. SCI breaks MWAIT
6. kernel mwait_loop post-handler:
   s2idle_request_clear() + s2idle_signal_wake()
   (KSTOP_FLAG=2, event signaled)
7. acpid: kstop_reason() returns 2
8. acpid: exit_s2idle() (\_SST(2) -> \_WAK(0) -> \_SST(1))
9. loop back to step 4

Hardware-agnostic: the s2idle state machine is identical
for any platform with Modern Standby (Dell, HP, Lenovo,
LG Gram, etc.). Only the wake source (SCI, GPIO, RTC, ...)
varies per OEM.

The libredox + kcall path uses the upstream redox_syscall
0.8.1's CheckShutdown verb (kcall 2 returns a usize). The
s2idle-specific EnterS2Idle/ExitS2Idle AcPiVerb variants
(Phase J work) are kept in local/sources/syscall/ but
NOT used in this commit because the [patch.crates-io]
chain is not yet wired up (Phase J deferred to avoid the
libredox cross-version type identity issue).
2026-07-01 09:10:12 +03:00

717 lines
25 KiB
Rust

use acpi::aml::namespace::AmlName;
use amlserde::aml_serde_name::to_aml_format;
use amlserde::AmlSerdeValue;
use core::str;
use libredox::Fd;
use parking_lot::RwLockReadGuard;
use redox_scheme::scheme::SchemeSync;
use redox_scheme::{CallerCtx, OpenResult, SendFdRequest, Socket};
use syscall::flag::CallFlags;
use syscall::flag::AcpiVerb;
use ron::de::SpannedError;
use scheme_utils::HandleMap;
use std::convert::{TryFrom, TryInto};
use std::str::FromStr;
use syscall::dirent::{DirEntry, DirentBuf, DirentKind};
use syscall::schemev2::NewFdFlags;
use syscall::FobtainFdFlags;
use syscall::data::Stat;
use syscall::error::{Error, Result};
use syscall::error::{EACCES, EBADF, EBADFD, EINVAL, EIO, EISDIR, ENOENT, ENOTDIR};
use syscall::flag::{MODE_DIR, MODE_FILE};
use syscall::flag::{O_ACCMODE, O_DIRECTORY, O_RDONLY, O_STAT, O_SYMLINK};
use syscall::{EOVERFLOW, EPERM};
use crate::acpi::{AcpiContext, AmlSymbols, SdtSignature};
use crate::dmi::DMI_FIELDS;
pub struct AcpiScheme<'acpi, 'sock> {
ctx: &'acpi AcpiContext,
handles: HandleMap<Handle<'acpi>>,
pci_fd: Option<Fd>,
socket: &'sock Socket,
/// Phase I.5: the kstop handle fd. Stored so the main loop
/// can call `kstop_reason` (kcall 2) to query the kernel
/// for the reason of the most recent kstop event.
kstop_fd: Option<Fd>,
}
struct Handle<'a> {
kind: HandleKind<'a>,
stat: bool,
allowed_to_eval: bool,
}
enum HandleKind<'a> {
TopLevel,
Tables,
Table(SdtSignature),
Symbols(RwLockReadGuard<'a, AmlSymbols>),
Symbol { name: String, description: String },
SchemeRoot,
RegisterPci,
/// `/scheme/acpi/thermal` -- entries are children of `\_TZ` from
/// the AML namespace (e.g. `\_TZ.TZ0`). On systems without
/// thermal zones (headless QEMU, desktops) the directory
/// listing is empty.
Thermal,
/// `/scheme/acpi/power` -- entries are PowerResource objects in
/// the AML namespace. On laptops these are AC adapters and
/// battery controllers. On desktops and QEMU the listing is
/// empty.
Power,
/// `/scheme/acpi/dmi` -- key=value text dump of the SMBIOS identity
/// fields (consumed by `redox-driver-sys` quirks loader).
Dmi,
/// `/scheme/acpi/dmi/<field>` -- a single SMBIOS field as a text
/// file (consumed by `i2c-hidd` for probe-failure quirks).
DmiField(String),
/// `/scheme/acpi/processor` -- entries are children of `\_PR` from
/// the AML namespace (e.g. `CPU0`, `CPU1`). On systems without
/// ACPI processor objects (headless QEMU, very old firmware) the
/// directory listing is empty.
Processor,
/// `/scheme/acpi/processor/<cpu>/<file>` -- per-CPU ACPI data:
/// `pss` (P-state frequencies), `psd` (P-state dependencies),
/// `cst` (C-state table). On QEMU these are typically empty.
/// On the LG Gram 2025 / Arrow Lake-H the firmware provides
/// full _PSS / _PSD / _CST objects that the HWP-aware cpufreqd
/// uses to set initial P-states and detect C-state support.
ProcFile { cpu: u32, kind: ProcFileKind },
DmiDir,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ProcFileKind {
Pss,
Psd,
Cst,
Cpc,
}
impl HandleKind<'_> {
fn is_dir(&self) -> bool {
match self {
Self::TopLevel => true,
Self::Tables => true,
Self::Table(_) => false,
Self::Symbols(_) => true,
Self::Symbol { .. } => false,
Self::SchemeRoot => false,
Self::RegisterPci => false,
Self::Thermal | Self::Power => true,
Self::Dmi => true,
Self::DmiField(_) => false,
}
}
fn len(&self, acpi_ctx: &AcpiContext) -> Result<usize> {
Ok(match self {
// Files
Self::Table(signature) => acpi_ctx
.sdt_from_signature(signature)
.ok_or(Error::new(EBADFD))?
.length(),
Self::Symbol { description, .. } => description.len(),
// /scheme/acpi/dmi is a key=value text file (redox-driver-sys
// reads it via fs::read_to_string). The size depends on how
// many fields are populated.
Self::Dmi => acpi_ctx
.dmi_info()
.map(|info| info.to_match_lines().len())
.unwrap_or(0),
Self::DmiField(field) => dmi_field_contents(acpi_ctx.dmi_info(), field)
.map(|s| s.len())
.unwrap_or(0),
// Directories
Self::TopLevel | Self::Symbols(_) | Self::Tables => 0,
Self::Thermal | Self::Power => 0,
Self::SchemeRoot | Self::RegisterPci => return Err(Error::new(EBADF)),
})
}
}
impl<'acpi, 'sock> AcpiScheme<'acpi, 'sock> {
pub fn new(ctx: &'acpi AcpiContext, socket: &'sock Socket) -> Self {
Self {
ctx,
handles: HandleMap::new(),
pci_fd: None,
socket,
kstop_fd: None,
}
}
/// Phase I.5: register the kstop handle fd. Called by the
/// main loop right after opening the kstop handle.
pub fn set_kstop_fd(&mut self, fd: Fd) {
self.kstop_fd = Some(fd);
}
/// Phase I.5: query the kernel for the kstop reason via
/// the CheckShutdown AcpiVerb (kcall 2). Returns the u8
/// reason: 0=idle, 1=shutdown (S5), 2=s2idle wake,
/// 3=s3 wake. The kernel re-arms the kstop handle's
/// EVENT_READ after each event; acpid's main loop calls
/// this once per event to decide what AML sequence to run.
///
/// Mirrors Linux 7.1 `acpi_s2idle_wake` returning the
/// wake reason in `drivers/acpi/sleep.c:758`. The
/// `kcall 2` is the `AcpiVerb::CheckShutdown` enum
/// variant in the syscall crate.
///
/// Hardware-agnostic: the reason codes are platform-
/// independent; only the wake source (SCI, GPIO, RTC,
/// ...) varies per OEM.
pub fn kstop_reason(&mut self) -> syscall::Result<u64> {
let handle = self.kstop_fd.ok_or(syscall::error::Error::new(syscall::error::EBADF))?;
let mut payload = [0u8; 8];
let verb = AcpiVerb::CheckShutdown as u64;
let result = handle.call_ro(&mut payload, CallFlags::empty(), &[verb])?;
Ok(u64::from_ne_bytes(payload))
}
}
fn parse_hex_digit(hex: u8) -> Option<u8> {
let hex = hex.to_ascii_lowercase();
if hex >= b'a' && hex <= b'f' {
Some(hex - b'a' + 10)
} else if hex >= b'0' && hex <= b'9' {
Some(hex - b'0')
} else {
None
}
}
fn parse_hex_2digit(hex: &[u8]) -> Option<u8> {
parse_hex_digit(hex[0])
.and_then(|most_significant| Some((most_significant << 4) | parse_hex_digit(hex[1])?))
}
fn parse_oem_id(hex: [u8; 12]) -> Option<[u8; 6]> {
Some([
parse_hex_2digit(&hex[0..2])?,
parse_hex_2digit(&hex[2..4])?,
parse_hex_2digit(&hex[4..6])?,
parse_hex_2digit(&hex[6..8])?,
parse_hex_2digit(&hex[8..10])?,
parse_hex_2digit(&hex[10..12])?,
])
}
fn parse_oem_table_id(hex: [u8; 16]) -> Option<[u8; 8]> {
Some([
parse_hex_2digit(&hex[0..2])?,
parse_hex_2digit(&hex[2..4])?,
parse_hex_2digit(&hex[4..6])?,
parse_hex_2digit(&hex[6..8])?,
parse_hex_2digit(&hex[8..10])?,
parse_hex_2digit(&hex[10..12])?,
parse_hex_2digit(&hex[12..14])?,
parse_hex_2digit(&hex[14..16])?,
])
}
/// Look up the contents of `/scheme/acpi/dmi/<field>` for the given
/// field name. Returns `None` when DMI data is not present (no SMBIOS)
/// or when the field name is unknown. The returned `String` is what
/// userspace will read from the file -- a single text line with no
/// trailing newline so that callers can `read_to_string` and `trim`.
fn dmi_field_contents(
info: Option<&crate::dmi::DmiInfo>,
field: &str,
) -> Option<String> {
crate::dmi::read_field(info, field)
}
fn parse_table(table: &[u8]) -> Option<SdtSignature> {
let signature_part = table.get(..4)?;
let first_hyphen = table.get(4)?;
let oem_id_part = table.get(5..17)?;
let second_hyphen = table.get(17)?;
let oem_table_part = table.get(18..34)?;
if *first_hyphen != b'-' {
return None;
}
if *second_hyphen != b'-' {
return None;
}
if table.len() > 34 {
return None;
}
Some(SdtSignature {
signature: <[u8; 4]>::try_from(signature_part)
.expect("expected 4-byte slice to be convertible into [u8; 4]"),
oem_id: {
let hex = <[u8; 12]>::try_from(oem_id_part)
.expect("expected 12-byte slice to be convertible into [u8; 12]");
parse_oem_id(hex)?
},
oem_table_id: {
let hex = <[u8; 16]>::try_from(oem_table_part)
.expect("expected 16-byte slice to be convertible into [u8; 16]");
parse_oem_table_id(hex)?
},
})
}
impl SchemeSync for AcpiScheme<'_, '_> {
fn scheme_root(&mut self) -> Result<usize> {
Ok(self.handles.insert(Handle {
stat: false,
kind: HandleKind::SchemeRoot,
allowed_to_eval: false,
}))
}
fn openat(
&mut self,
dirfd: usize,
path: &str,
flags: usize,
_fcntl_flags: u32,
ctx: &CallerCtx,
) -> Result<OpenResult> {
let handle = self.handles.get(dirfd)?;
let path = path.trim_start_matches('/');
let flag_stat = flags & O_STAT == O_STAT;
let flag_dir = flags & O_DIRECTORY == O_DIRECTORY;
let kind = match handle.kind {
HandleKind::SchemeRoot => {
// TODO: arrayvec
let components = {
let mut v = arrayvec::ArrayVec::<&str, 4>::new();
let it = path.split('/');
for component in it.take(4) {
v.push(component);
}
v
};
match &*components {
[""] => HandleKind::TopLevel,
["register_pci"] => HandleKind::RegisterPci,
["tables"] => HandleKind::Tables,
["thermal"] => HandleKind::Thermal,
["power"] => HandleKind::Power,
["dmi"] => HandleKind::Dmi,
["processor"] => HandleKind::Processor,
["tables", table] => {
let signature = parse_table(table.as_bytes()).ok_or(Error::new(ENOENT))?;
HandleKind::Table(signature)
}
["symbols"] => {
if let Ok(aml_symbols) = self.ctx.aml_symbols(self.pci_fd.as_ref()) {
HandleKind::Symbols(aml_symbols)
} else {
return Err(Error::new(EIO));
}
}
["symbols", symbol] => {
if let Some(description) = self.ctx.aml_lookup(symbol) {
HandleKind::Symbol {
name: (*symbol).to_owned(),
description,
}
} else {
return Err(Error::new(ENOENT));
}
}
["dmi", field] => {
// Reject unknown fields explicitly so consumers
// see ENOENT rather than reading an empty file.
// When SMBIOS is absent, we still serve a
// well-defined file with empty contents (so
// i2c-hidd's `Err(NotFound)` branch is the only
// way to tell the difference between "missing
// field" and "no SMBIOS").
if DMI_FIELDS.iter().any(|f| *f == *field) {
HandleKind::DmiField((*field).to_owned())
} else {
return Err(Error::new(ENOENT));
}
}
["processor", cpu_str, file] => {
// /scheme/acpi/processor/<cpu>/{pss,psd,cst,cpc}
let cpu: u32 = cpu_str.parse().map_err(|_| Error::new(EINVAL))?;
let kind = match *file {
"pss" => ProcFileKind::Pss,
"psd" => ProcFileKind::Psd,
"cst" => ProcFileKind::Cst,
"cpc" => ProcFileKind::Cpc,
_ => return Err(Error::new(ENOENT)),
};
HandleKind::ProcFile { cpu, kind }
}
_ => return Err(Error::new(ENOENT)),
}
}
HandleKind::Symbols(ref aml_symbols) => {
if let Some(description) = aml_symbols.lookup(path) {
HandleKind::Symbol {
name: (*path).to_owned(),
description,
}
} else {
return Err(Error::new(ENOENT));
}
}
_ => return Err(Error::new(EACCES)),
};
if kind.is_dir() && !flag_dir && !flag_stat {
return Err(Error::new(EISDIR));
} else if !kind.is_dir() && flag_dir && !flag_stat {
return Err(Error::new(ENOTDIR));
}
let allowed_to_eval = if flags & O_ACCMODE == O_RDONLY || flag_stat {
false
} else if ctx.uid == 0 {
true
} else {
return Err(Error::new(EINVAL));
};
if flags & O_SYMLINK == O_SYMLINK && !flag_stat {
return Err(Error::new(EINVAL));
}
let fd = self.handles.insert(Handle {
stat: flag_stat,
kind,
allowed_to_eval,
});
Ok(OpenResult::ThisScheme {
number: fd,
flags: NewFdFlags::POSITIONED,
})
}
fn fstat(&mut self, id: usize, stat: &mut Stat, _ctx: &CallerCtx) -> Result<()> {
let handle = self.handles.get(id)?;
stat.st_size = handle
.kind
.len(self.ctx)?
.try_into()
.unwrap_or(u64::max_value());
if handle.kind.is_dir() {
stat.st_mode = MODE_DIR;
} else {
stat.st_mode = MODE_FILE;
}
Ok(())
}
fn read(
&mut self,
id: usize,
buf: &mut [u8],
offset: u64,
_fcntl: u32,
_ctx: &CallerCtx,
) -> Result<usize> {
let offset: usize = offset.try_into().map_err(|_| Error::new(EINVAL))?;
let handle = self.handles.get_mut(id)?;
if handle.stat {
return Err(Error::new(EBADF));
}
// Build an owned buffer for DMI handles so the borrow does not
// escape the match arm scope.
let dmi_buf;
let proc_buf;
let src_buf: &[u8] = match &handle.kind {
HandleKind::Table(ref signature) => self
.ctx
.sdt_from_signature(signature)
.ok_or(Error::new(EBADFD))?
.as_slice(),
HandleKind::Symbol { description, .. } => description.as_bytes(),
HandleKind::Dmi => {
dmi_buf = self
.ctx
.dmi_info()
.map(|info| info.to_match_lines())
.unwrap_or_default();
dmi_buf.as_bytes()
}
HandleKind::DmiField(ref field) => {
dmi_buf = dmi_field_contents(self.ctx.dmi_info(), field)
.unwrap_or_default();
dmi_buf.as_bytes()
}
HandleKind::Processor | HandleKind::DmiDir | HandleKind::Thermal | HandleKind::Power | HandleKind::Symbols(_) | HandleKind::RegisterPci | HandleKind::TopLevel | HandleKind::SchemeRoot => {
return Err(Error::new(EISDIR));
}
HandleKind::ProcFile { .. } => {
// Per-CPU _PSS / _PSD / _CST / _CPC text export. The
// full AML→text conversion is a Phase G follow-up; for
// now, return a placeholder line so consumers
// (cpufreqd, redbear-power) can detect the path is
// present and report "no data" without getting ENOENT.
proc_buf = b"# ACPI processor data not yet populated\n".to_vec();
proc_buf.as_slice()
}
};
let offset = std::cmp::min(src_buf.len(), offset);
let src_buf = &src_buf[offset..];
let to_copy = std::cmp::min(src_buf.len(), buf.len());
buf[..to_copy].copy_from_slice(&src_buf[..to_copy]);
Ok(to_copy)
}
fn getdents<'buf>(
&mut self,
id: usize,
mut buf: DirentBuf<&'buf mut [u8]>,
opaque_offset: u64,
) -> Result<DirentBuf<&'buf mut [u8]>> {
let handle = self.handles.get_mut(id)?;
match &handle.kind {
HandleKind::TopLevel => {
const TOPLEVEL_ENTRIES: &[&str] = &[
"tables", "symbols", "thermal", "power", "dmi", "processor",
];
for (idx, name) in TOPLEVEL_ENTRIES
.iter()
.enumerate()
.skip(opaque_offset as usize)
{
buf.entry(DirEntry {
inode: 0,
next_opaque_id: idx as u64 + 1,
name,
kind: DirentKind::Directory,
})?;
}
}
HandleKind::Symbols(aml_symbols) => {
for (idx, (symbol_name, _value)) in aml_symbols
.symbols_cache()
.iter()
.enumerate()
.skip(opaque_offset as usize)
{
buf.entry(DirEntry {
inode: 0,
next_opaque_id: idx as u64 + 1,
name: symbol_name.as_str(),
kind: DirentKind::Regular,
})?;
}
}
HandleKind::Tables => {
for (idx, table) in self
.ctx
.tables()
.iter()
.enumerate()
.skip(opaque_offset as usize)
{
let utf8_or_eio = |bytes| str::from_utf8(bytes).map_err(|_| Error::new(EIO));
let mut name = String::new();
name.push_str(utf8_or_eio(&table.signature[..])?);
name.push('-');
for byte in table.oem_id.iter() {
std::fmt::write(&mut name, format_args!("{:>02X}", byte)).unwrap();
}
name.push('-');
for byte in table.oem_table_id.iter() {
std::fmt::write(&mut name, format_args!("{:>02X}", byte)).unwrap();
}
buf.entry(DirEntry {
inode: 0,
next_opaque_id: idx as u64 + 1,
name: &name,
kind: DirentKind::Regular,
})?;
}
}
HandleKind::Thermal => {
// Enumerate \_TZ.<zone> entries from the AML namespace.
// Returns Ok with no entries on systems with no zones
// (headless QEMU, desktops) so consumers see an
// empty-but-existing directory.
let zones = self.ctx.thermal_zones();
for (idx, zone) in zones.iter().enumerate().skip(opaque_offset as usize) {
buf.entry(DirEntry {
inode: 0,
next_opaque_id: idx as u64 + 1,
name: zone.as_str(),
kind: DirentKind::Directory,
})?;
}
}
HandleKind::Processor => {
// Enumerate \_PR.<cpu> entries from the AML namespace.
// Returns Ok with no entries on systems with no
// processors (headless QEMU with no DSDT) so consumers
// see an empty-but-existing directory.
let cpus = self.ctx.cpu_names();
for (idx, cpu_name) in cpus.iter().enumerate().skip(opaque_offset as usize) {
buf.entry(DirEntry {
inode: 0,
next_opaque_id: idx as u64 + 1,
name: cpu_name.as_str(),
kind: DirentKind::Directory,
})?;
}
}
HandleKind::Power => {
// Enumerate PowerResource entries. On real laptops these
// are AC adapters and battery controllers; on desktops
// and QEMU the list is empty.
let adapters = self.ctx.power_adapters();
for (idx, adapter) in adapters.iter().enumerate().skip(opaque_offset as usize) {
buf.entry(DirEntry {
inode: 0,
next_opaque_id: idx as u64 + 1,
name: adapter.as_str(),
kind: DirentKind::Directory,
})?;
}
}
HandleKind::Dmi => {
// Consumers should `read_to_string("/scheme/acpi/dmi")`
// rather than iterating, but we still surface the field
list so that ls /scheme/acpi/dmi/ produces a useful
diagnostic on a live system. We always list the same
set of fields regardless of whether SMBIOS data is
present -- empty entries just produce empty reads.
for (idx, field) in DMI_FIELDS
.iter()
.enumerate()
.skip(opaque_offset as usize)
{
buf.entry(DirEntry {
inode: 0,
next_opaque_id: idx as u64 + 1,
name: field,
kind: DirentKind::Regular,
})?;
}
}
HandleKind::ProcFile { .. } | HandleKind::DmiDir => {
// No children; reads/writes go through the
// HandleKind match in kread/kwriteoff.
}
}
_ => return Err(Error::new(EIO)),
}
Ok(buf)
}
fn call(
&mut self,
id: usize,
payload: &mut [u8],
_metadata: &[u64],
_ctx: &CallerCtx,
) -> Result<usize> {
let handle = self.handles.get_mut(id)?;
if !handle.allowed_to_eval {
return Err(Error::new(EPERM));
}
let Ok(args): Result<Vec<AmlSerdeValue>, SpannedError> = ron::de::from_bytes(payload)
else {
return Err(Error::new(EINVAL));
};
let HandleKind::Symbol { name, .. } = &handle.kind else {
return Err(Error::new(EBADF));
};
let Ok(aml_name) = AmlName::from_str(&to_aml_format(name)) else {
log::error!("Failed to convert symbol name: \"{name}\" to aml name!");
return Err(Error::new(EBADF));
};
let Ok(result) = self.ctx.aml_eval(aml_name, args) else {
return Err(Error::new(EINVAL));
};
let Ok(serialized_result) = ron::ser::to_string(&result) else {
log::error!("Failed to serialize aml result!");
return Err(Error::new(EINVAL));
};
let byte_result = serialized_result.as_bytes();
let result_len = byte_result.len();
if result_len > payload.len() {
return Err(Error::new(EOVERFLOW));
}
payload[..result_len].copy_from_slice(byte_result);
Ok(result_len)
}
fn on_sendfd(&mut self, sendfd_request: &SendFdRequest) -> Result<usize> {
let id = sendfd_request.id();
let num_fds = sendfd_request.num_fds();
let handle = self.handles.get(id)?;
if !matches!(handle.kind, HandleKind::RegisterPci) {
return Err(Error::new(EACCES));
}
if num_fds == 0 {
return Ok(0);
}
if num_fds > 1 {
return Err(Error::new(EINVAL));
}
let mut new_fd = usize::MAX;
if let Err(e) = sendfd_request.obtain_fd(
&self.socket,
FobtainFdFlags::UPPER_TBL,
std::slice::from_mut(&mut new_fd),
) {
return Err(e);
}
let new_fd = libredox::Fd::new(new_fd);
if self.pci_fd.is_some() {
return Err(Error::new(EINVAL));
} else {
self.pci_fd = Some(new_fd);
}
Ok(num_fds)
}
fn on_close(&mut self, id: usize) {
self.handles.remove(id);
}
}