use std::collections::{HashMap, HashSet, VecDeque};
use wtf_string::Wtf16String;
use crate::admission::EnumerationHandle;
use crate::completion::{Completion, EnumerationId, TerminalOutcome};
use crate::error::{BeginFailure, EnumerationError, Win32Error};
use crate::request::EnumerationRequest;
use crate::session::{QuantumOutcome, Receiver, Session};
use crate::testing::named_file;
#[derive(Clone, Copy, Debug)]
pub(crate) enum Quantum {
Idle,
Yielded,
Parked,
Completed,
Cancelled,
Failed,
}
impl Quantum {
fn into_outcome(self) -> QuantumOutcome {
match self {
Quantum::Idle => QuantumOutcome::Idle,
Quantum::Yielded => QuantumOutcome::Yielded,
Quantum::Parked => QuantumOutcome::Parked,
Quantum::Completed => QuantumOutcome::Finished(TerminalOutcome::Completed),
Quantum::Cancelled => QuantumOutcome::Finished(TerminalOutcome::Cancelled),
Quantum::Failed => QuantumOutcome::Finished(TerminalOutcome::Failed(
EnumerationError::DirectoryQuery(Win32Error::from_code(5)),
)),
}
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum Op {
Begin,
BeginRefused(BeginFailure),
Service,
Cancel(usize),
DropHandle(usize),
Detach(usize),
Claim,
Report(Quantum),
RunEngine(Quantum),
Schedule(usize),
OfferEntry(usize, &'static str),
Recv,
DrainReceiver,
DropReceiver,
DropSession,
}
#[derive(Default)]
struct Observed {
entries: Vec<String>,
terminal: Option<&'static str>,
}
pub(crate) struct Model {
session: Option<Session>,
receiver: Option<Receiver>,
handles: Vec<Option<EnumerationHandle>>,
ids: Vec<EnumerationId>,
offered: HashMap<EnumerationId, VecDeque<String>>,
observed: HashMap<EnumerationId, Observed>,
finished: HashSet<EnumerationId>,
refused: usize,
held: Vec<(EnumerationId, crate::engine::EngineState)>,
last_claim: Option<EnumerationId>,
completion_capacity: usize,
}
impl Model {
pub(crate) fn new(submission_capacity: usize, completion_capacity: usize) -> Self {
let (session, receiver) =
Session::new(submission_capacity, completion_capacity).expect("valid bounds");
session.suppress_pool();
receiver.doorbell().expect("an event");
Self {
session: Some(session),
receiver: Some(receiver),
handles: Vec::new(),
ids: Vec::new(),
offered: HashMap::new(),
observed: HashMap::new(),
finished: HashSet::new(),
refused: 0,
held: Vec::new(),
last_claim: None,
completion_capacity,
}
}
pub(crate) fn run(&mut self, script: &[Op]) {
for (index, op) in script.iter().enumerate() {
self.apply(*op);
self.check(index, *op);
}
}
pub(crate) fn registered(&self) -> usize {
self.session.as_ref().map_or(0, Session::enumerations)
}
pub(crate) fn entries(&self, slot: usize) -> Vec<String> {
self.observed
.get(&self.ids[slot])
.map(|observed| observed.entries.clone())
.unwrap_or_default()
}
pub(crate) fn terminal(&self, slot: usize) -> Option<&'static str> {
self.observed
.get(&self.ids[slot])
.and_then(|observed| observed.terminal)
}
pub(crate) fn refused(&self) -> usize {
self.refused
}
pub(crate) fn id(&self, slot: usize) -> EnumerationId {
self.ids[slot]
}
pub(crate) fn claimed(&self) -> Option<EnumerationId> {
self.last_claim
}
pub(crate) fn ready(&self) -> usize {
self.session
.as_ref()
.map_or(0, |session| session.shared.ready())
}
fn session(&self) -> &Session {
self.session.as_ref().expect("the session is still held")
}
fn apply(&mut self, op: Op) {
match op {
Op::Begin => {
let handle = self
.session()
.try_begin(request())
.expect("the script expects room");
self.ids.push(handle.id());
self.handles.push(Some(handle));
}
Op::BeginRefused(expected) => {
let error = self
.session()
.try_begin(request())
.expect_err("the script expects a refusal");
assert_eq!(error.failure(), expected);
}
Op::Service => self.session().shared.drain_submissions(),
Op::Cancel(slot) => {
if let Some(handle) = self.handles[slot].take() {
handle.cancel();
}
}
Op::DropHandle(slot) => {
self.handles[slot] = None;
}
Op::Detach(slot) => {
if let Some(handle) = self.handles[slot].take() {
handle.detach();
}
}
Op::Claim => {
let claimed = self.session().shared.claim_next();
self.last_claim = claimed.as_ref().map(|(enumeration, _)| *enumeration);
if let Some(claim) = claimed {
self.held.push(claim);
}
}
Op::Report(quantum) => {
if let Some((enumeration, engine)) = self.held.pop() {
self.session().shared.report_quantum(
enumeration,
engine,
quantum.into_outcome(),
);
}
}
Op::RunEngine(quantum) => {
self.session().shared.script_quantum(quantum.into_outcome());
self.session().shared.run_engine_quantum();
}
Op::Schedule(slot) => {
let id = self.ids[slot];
self.session().shared.schedule(id);
}
Op::OfferEntry(slot, name) => {
let id = self.ids[slot];
let record = Completion::Entry {
enumeration: id,
entry: named_file(name),
};
match self.session().shared.completions.try_send_entry(record) {
Ok(()) => self
.offered
.entry(id)
.or_default()
.push_back(name.to_string()),
Err(_) => self.refused += 1,
}
}
Op::Recv => {
let record = self
.receiver
.as_ref()
.and_then(|receiver| receiver.try_recv());
if let Some(record) = record {
self.observe(record);
}
}
Op::DrainReceiver => {
while let Some(record) = self
.receiver
.as_ref()
.and_then(|receiver| receiver.try_recv())
{
self.observe(record);
}
}
Op::DropReceiver => {
self.receiver = None;
}
Op::DropSession => {
self.session = None;
}
}
}
fn observe(&mut self, record: Completion) {
let id = record.enumeration();
let observed = self.observed.entry(id).or_default();
match record {
Completion::Entry { entry, .. } => {
assert!(
observed.terminal.is_none(),
"{id} produced an entry after its terminal"
);
observed.entries.push(entry.name().to_string_lossy());
}
Completion::Terminal { outcome, .. } => {
assert!(
observed.terminal.is_none(),
"{id} produced a second terminal"
);
observed.terminal = Some(match outcome {
TerminalOutcome::Completed => "completed",
TerminalOutcome::Cancelled => "cancelled",
TerminalOutcome::Failed(_) => "failed",
});
assert!(self.finished.insert(id), "{id} finished twice");
}
}
}
fn check(&self, index: usize, op: Op) {
let context = format!("after step {index} ({op:?})");
let Some(session) = self.session.as_ref() else {
return;
};
let ring = &session.shared.completions;
let queued = ring.len();
let reserved = ring.reserved();
assert!(
queued + reserved <= self.completion_capacity,
"{context}: {queued} queued plus {reserved} reserved exceeds the bound"
);
assert!(
reserved < self.completion_capacity,
"{context}: reservations took every slot, leaving no room for an entry"
);
if let Ok(handle) = self.receiver.as_ref().map_or_else(
|| Err(std::io::Error::other("no receiver")),
|receiver| {
receiver
.doorbell()
.map(|handle| handle.try_clone_to_owned())
},
) {
let handle = handle.expect("the doorbell can be duplicated");
assert_eq!(
is_signalled(&handle),
ring.is_pending(),
"{context}: the doorbell disagrees with what the receiver can observe"
);
}
for (id, offered) in &self.offered {
let Some(observed) = self.observed.get(id) else {
continue;
};
let expected: Vec<&String> = offered.iter().take(observed.entries.len()).collect();
let actual: Vec<&String> = observed.entries.iter().collect();
assert_eq!(expected, actual, "{context}: {id} delivered out of order");
}
}
}
fn request() -> EnumerationRequest {
EnumerationRequest::new(&Wtf16String::from(r"C:\Windows")).expect("a resolvable path")
}
fn is_signalled(handle: &std::os::windows::io::OwnedHandle) -> bool {
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Foundation::{HANDLE, WAIT_OBJECT_0};
use windows_sys::Win32::System::Threading::WaitForSingleObject;
let result = unsafe { WaitForSingleObject(handle.as_raw_handle() as HANDLE, 0) };
result == WAIT_OBJECT_0
}
#[cfg(test)]
mod tests;