use std::fmt::Display;
use std::sync::RwLock;
use std::time::Duration;
use chirograph::{ResumeReply, ResumeRequest};
use graphshell_endpoint::{
IntentSink, PresentationSource, ProjectionCatalog, ProjectionNoticeSource, ProjectionSource,
};
use notochord::{AdmittedSession, RevocationLedger};
use tokio::io::{AsyncRead, AsyncWrite};
use crate::lifecycle::SessionAuthority;
use crate::session_loop::{SessionLoopError, SessionSummary, serve_admitted_session_with};
pub async fn serve_admitted_session_notifying<E, S, F, N>(
session: &mut AdmittedSession<S>,
authority: &SessionAuthority,
revocations: &RwLock<RevocationLedger>,
endpoint: &mut E,
resume: &mut F,
now_ms: N,
poll_interval: Duration,
) -> Result<SessionSummary, SessionLoopError>
where
E: ProjectionCatalog
+ ProjectionSource
+ PresentationSource
+ IntentSink
+ ProjectionNoticeSource,
<E as ProjectionSource>::Error: Display,
<E as PresentationSource>::Error: Display,
<E as IntentSink>::Error: Display,
<E as ProjectionNoticeSource>::Error: Display,
S: AsyncRead + AsyncWrite + Unpin,
F: FnMut(&mut E, ResumeRequest) -> Result<ResumeReply, String>,
N: Fn() -> u64,
{
serve_admitted_session_with(
session,
authority,
revocations,
endpoint,
resume,
now_ms,
Some((poll_interval, |endpoint: &mut E| {
endpoint.poll_notice().map_err(|error| error.to_string())
})),
)
.await
}
#[cfg(test)]
mod tests {
use super::*;
use crate::admission::{CONNECT_ACTION, GRAPHSHELL_DOMAIN, PROJECTION_SERVICE};
use crate::network_carrier::{CarrierRuntime, NetworkCarrier};
use crate::session_loop::SessionEnd;
use chirograph::{
CapabilityProfile, Carrier, CarrierNotice, CarrierRequestBody, CarrierResponseBody,
EndpointDescriptor, IntentInvocation, IntentResult, ProjectionRequest, ProjectionSession,
ProjectionSnapshot, ProtocolVersion, ResourceRequest, ResourceResponse, Revision,
SceneEpoch, SessionOpen,
};
use notochord::{
AdmittedPrincipal, CarrierKind, NetworkId, ProfileRef, RequestedAction, SessionClaims,
SessionFacts, TrafficClass,
};
use personae::delegation::{
CapabilityScope, DelegationCertificate, DelegationParent, SignedDelegationCertificate,
};
use personae::{IdentityProvider, InMemoryProvider};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::io::DuplexStream;
use tokio::runtime::Runtime;
const NOW_MS: u64 = 50;
const EXPIRY_MS: u64 = 100;
const ROOT_AUTHORITY: [u8; 32] = [7; 32];
const NETWORK: [u8; 32] = [3; 32];
fn owner() -> InMemoryProvider {
InMemoryProvider::from_seed([1; 32])
}
fn viewer() -> InMemoryProvider {
InMemoryProvider::from_seed([4; 32])
}
fn grant() -> SignedDelegationCertificate {
SignedDelegationCertificate::issue(
&owner(),
DelegationCertificate::new(
DelegationParent::Root(ROOT_AUTHORITY),
owner().master_public_key().to_bytes(),
viewer().master_public_key().to_bytes(),
CapabilityScope {
domain: GRAPHSHELL_DOMAIN.into(),
resource: NETWORK.to_vec(),
path_prefix: PROJECTION_SERVICE.into(),
actions: [CONNECT_ACTION.to_string()].into_iter().collect(),
},
5,
10,
Some(EXPIRY_MS),
1,
[1; 32],
),
)
.expect("issue certificate")
}
fn principal() -> AdmittedPrincipal {
AdmittedPrincipal {
subject: viewer().master_public_key().to_bytes(),
class: TrafficClass::Interactive,
session_id: [21; 32],
action: RequestedAction {
domain: GRAPHSHELL_DOMAIN.to_string(),
path: PROJECTION_SERVICE.to_string(),
action: CONNECT_ACTION.to_string(),
},
}
}
fn admitted(stream: DuplexStream) -> AdmittedSession<DuplexStream> {
AdmittedSession {
stream,
principal: principal(),
claims: SessionClaims {
wire_version: 1,
network: NetworkId(NETWORK),
profile: ProfileRef {
id: "mere.base".into(),
revision: 1,
},
action: principal().action,
class: TrafficClass::Interactive,
subject: viewer().master_public_key().to_bytes(),
delegations: vec![grant()],
},
facts: SessionFacts::new(b"mere/graphshell/v1", CarrierKind::P2panda),
limits: Default::default(),
}
}
fn notice(revision: u64) -> CarrierNotice {
CarrierNotice {
session: ProjectionSession("fixture:scene".into()),
epoch: SceneEpoch(3),
revision: Revision(revision),
}
}
struct RingingEndpoint {
pending: Vec<CarrierNotice>,
polls: Arc<AtomicUsize>,
}
#[derive(Debug)]
struct StubError;
impl Display for StubError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "stub endpoint")
}
}
impl ProjectionCatalog for RingingEndpoint {
fn describe(&self) -> EndpointDescriptor {
EndpointDescriptor {
label: "ringing".into(),
projections: Vec::new(),
}
}
}
impl ProjectionSource for RingingEndpoint {
type Error = StubError;
fn snapshot(&mut self, _: ProjectionRequest) -> Result<ProjectionSnapshot, StubError> {
Err(StubError)
}
}
impl PresentationSource for RingingEndpoint {
type Error = StubError;
fn resource(&mut self, _: ResourceRequest) -> Result<ResourceResponse, StubError> {
Err(StubError)
}
}
impl IntentSink for RingingEndpoint {
type Error = StubError;
fn invoke(&mut self, _: IntentInvocation) -> Result<IntentResult, StubError> {
Ok(IntentResult::Accepted)
}
}
impl ProjectionNoticeSource for RingingEndpoint {
type Error = StubError;
fn poll_notice(&mut self) -> Result<Option<CarrierNotice>, StubError> {
self.polls.fetch_add(1, Ordering::SeqCst);
Ok(if self.pending.is_empty() {
None
} else {
Some(self.pending.remove(0))
})
}
}
fn open_body() -> CarrierRequestBody {
CarrierRequestBody::Open(Box::new(SessionOpen {
version: ProtocolVersion { major: 1, minor: 0 },
capabilities: CapabilityProfile::default(),
}))
}
fn with_served<T>(
pending: Vec<CarrierNotice>,
client: impl FnOnce(&mut NetworkCarrier<DuplexStream>) -> T + Send + 'static,
) -> (T, SessionSummary, usize)
where
T: Send + 'static,
{
with_served_every(Duration::from_millis(10), pending, client)
}
fn with_served_every<T>(
poll_interval: Duration,
pending: Vec<CarrierNotice>,
client: impl FnOnce(&mut NetworkCarrier<DuplexStream>) -> T + Send + 'static,
) -> (T, SessionSummary, usize)
where
T: Send + 'static,
{
let runtime = Runtime::new().unwrap();
let (near, far) = tokio::io::duplex(64 * 1024);
let polls = Arc::new(AtomicUsize::new(0));
let counted = Arc::clone(&polls);
let served = runtime.spawn(async move {
let mut session = admitted(far);
let mut endpoint = RingingEndpoint {
pending,
polls: counted,
};
let mut resume =
|_: &mut RingingEndpoint, _: ResumeRequest| Err("no resume".to_string());
serve_admitted_session_notifying(
&mut session,
&SessionAuthority::retain(principal(), vec![grant()]),
&RwLock::new(RevocationLedger::new()),
&mut endpoint,
&mut resume,
|| NOW_MS,
poll_interval,
)
.await
.unwrap()
});
let mut carrier =
NetworkCarrier::over(near, CarrierRuntime::borrowed(runtime.handle().clone()));
let outcome = client(&mut carrier);
let summary = runtime.block_on(served).unwrap();
(outcome, summary, polls.load(Ordering::SeqCst))
}
#[test]
fn a_quiet_peer_still_hears_the_bell() {
let (heard, summary, _) = with_served(vec![notice(2)], |carrier| {
let heard = carrier.wait_for_notice().unwrap();
carrier.request(CarrierRequestBody::Close).unwrap();
heard
});
assert_eq!(heard, notice(2));
assert_eq!(summary.end, SessionEnd::Closed);
}
#[test]
fn a_bell_rung_between_requests_arrives_without_waiting_for_the_interval() {
let (notices, summary, _) = with_served(vec![notice(4)], |carrier| {
carrier.request(open_body()).unwrap();
carrier.request(CarrierRequestBody::Close).unwrap();
let mut seen = Vec::new();
while let Some(notice) = carrier.take_notice() {
seen.push(notice);
}
seen
});
assert_eq!(notices, vec![notice(4)]);
assert_eq!(summary.end, SessionEnd::Closed);
}
#[test]
fn the_endpoint_is_asked_even_while_no_request_is_in_flight() {
let (_, summary, polls) = with_served(Vec::new(), |carrier| {
std::thread::sleep(Duration::from_millis(60));
carrier.request(CarrierRequestBody::Close).unwrap()
});
assert!(
polls > 1,
"the loop polls between reads, not only per request: {polls}"
);
assert_eq!(summary.end, SessionEnd::Closed);
}
#[test]
fn every_pending_notice_is_flushed_in_one_wake_rather_than_one_per_interval() {
let started = std::time::Instant::now();
let (heard, _, _) = with_served_every(
Duration::from_secs(5),
vec![notice(2), notice(3), notice(4)],
|carrier| {
let mut seen = Vec::new();
for _ in 0..3 {
seen.push(carrier.wait_for_notice().unwrap());
}
carrier.request(CarrierRequestBody::Close).unwrap();
seen
},
);
assert_eq!(heard, vec![notice(2), notice(3), notice(4)]);
assert!(
started.elapsed() < Duration::from_secs(2),
"all three arrived on one wake, well inside a single interval: {:?}",
started.elapsed()
);
}
#[test]
fn a_response_still_reaches_the_client_that_asked_for_it() {
let (opened, summary, _) = with_served(vec![notice(2)], |carrier| {
let opened = carrier.request(open_body()).unwrap();
carrier.request(CarrierRequestBody::Close).unwrap();
opened
});
match opened {
CarrierResponseBody::Opened(opened) => assert_eq!(opened.descriptor.label, "ringing"),
other => panic!("expected an opened session, got {other:?}"),
}
assert_eq!(summary.answered, 2);
}
}