use std::marker::PhantomData;
use std::time::Duration;
use crate::error::{Error, Result};
use crate::transport::Transport;
use crate::wire::{cmd, ui, Message, ObjectClass, Service};
#[derive(Debug)]
pub struct ReadOnly;
#[derive(Debug)]
pub struct ReadWrite;
pub const DRAIN_CAP: usize = 32;
pub const STALE_SESSION: u32 = 0x12;
pub const WRITE_LIMIT: Duration = Duration::from_secs(10);
pub const READ_LIMIT: Duration = Duration::from_secs(30);
pub struct Session<'t, T: Transport, C = ReadOnly> {
transport: Option<&'t mut T>,
class: ObjectClass,
closed: bool,
device_changed: bool,
read_limit: Duration,
_capability: PhantomData<C>,
}
impl<'t, T: Transport> Session<'t, T, ReadOnly> {
pub async fn open(transport: &'t mut T, class: ObjectClass) -> Result<Self> {
let mut s = Self {
transport: Some(transport),
class,
closed: false,
device_changed: false,
read_limit: READ_LIMIT,
_capability: PhantomData,
};
s.handshake().await?;
let opened = s.open_class(class).await;
let opened = match opened {
Err(Error::DeviceStatus(STALE_SESSION)) => {
if let Err(error) = s.discard_stale_session().await {
s.release().await;
return Err(error);
}
s.open_class(class).await
}
other => other,
};
match opened {
Ok(_) => Ok(s),
Err(e) => {
s.release().await;
Err(match e {
Error::DeviceStatus(status) if status != STALE_SESSION => {
Error::ClassRefused { class, status }
}
other => other,
})
}
}
}
async fn handshake(&mut self) -> Result<()> {
let hello = Message::new(Service::Ui, ui::SUBSYSTEM, ui::HELLO, Vec::new());
if let Err(e) = self.notify(&hello).await {
self.closed = true; return Err(e);
}
if let Err(e) = self.response_to(ui::HELLO).await {
self.release().await;
return Err(e);
}
Ok(())
}
async fn open_class(&mut self, class: ObjectClass) -> Result<()> {
self.request(
Service::Program,
10,
cmd::SESSION_OPEN,
&class.to_raw().to_be_bytes(),
)
.await
.map(|_| ())
}
async fn discard_stale_session(&mut self) -> Result<()> {
let close = Message::new(Service::Program, 10, cmd::SESSION_CLOSE, Vec::new());
self.notify(&close).await?;
let _ = self.read_frame().await?;
Ok(())
}
pub fn allow_destructive_writes(mut self) -> Session<'t, T, ReadWrite> {
let transport = self.transport.take();
let (class, closed, device_changed) = (self.class, self.closed, self.device_changed);
let read_limit = self.read_limit;
self.closed = true;
Session {
transport,
class,
closed,
device_changed,
read_limit,
_capability: PhantomData,
}
}
}
impl<T: Transport, C> Session<'_, T, C> {
pub fn class(&self) -> ObjectClass {
self.class
}
pub fn instrument_changed(&self) -> bool {
self.device_changed
}
pub fn set_read_limit(&mut self, limit: Duration) {
self.read_limit = limit;
}
async fn read_frame(&mut self) -> Result<Option<Message>> {
self.read_frame_with_limit(self.read_limit).await
}
async fn read_frame_with_limit(&mut self, limit: Duration) -> Result<Option<Message>> {
self.read_frame_as(limit, Message::decode_response).await
}
async fn read_frame_as(
&mut self,
limit: Duration,
decode: fn(&[u8]) -> Result<Message>,
) -> Result<Option<Message>> {
let transport = self
.transport
.as_mut()
.ok_or_else(|| Error::Transport("session has no transport".into()))?;
let raw = match transport
.read_timeout(crate::transport::READ_BUFFER, limit)
.await?
{
Some(raw) => raw,
None => return Ok(None),
};
decode(&raw).map(Some)
}
pub async fn probe(
&mut self,
service: Service,
subsystem: u32,
command: u32,
args: &[u8],
limit: Duration,
) -> Result<Option<Message>> {
let response = command.checked_add(1).ok_or_else(|| {
Error::InvalidArgument("command 0xffffffff has no response code".into())
})?;
let req = Message::new(service, subsystem, command, args.to_vec());
self.notify(&req).await?;
let mut drained = 0;
loop {
let Some(resp) = self.read_probe_frame_with_limit(limit).await? else {
return Ok(None);
};
if resp.command == cmd::CHANGED && resp.command != response && drained < DRAIN_CAP {
drained += 1;
self.device_changed = true;
continue;
}
return Ok(Some(resp));
}
}
async fn read_probe_frame_with_limit(&mut self, limit: Duration) -> Result<Option<Message>> {
self.read_frame_as(limit, Message::decode_probe).await
}
pub(crate) async fn request(
&mut self,
service: Service,
subsystem: u32,
command: u32,
args: &[u8],
) -> Result<Message> {
let req = Message::new(service, subsystem, command, args.to_vec());
self.notify(&req).await?;
self.response_to(command).await
}
async fn response_to(&mut self, command: u32) -> Result<Message> {
let expected = command.checked_add(1).ok_or_else(|| {
Error::InvalidArgument("command 0xffffffff has no response code".into())
})?;
let mut drained = 0;
loop {
let resp = match self.read_frame().await {
Ok(Some(resp)) => resp,
Ok(None) => {
self.release().await;
return Err(Error::Transport(format!(
"no reply to command {command:#04x} within the session's read limit"
)));
}
Err(e) => {
self.release().await;
return Err(e);
}
};
if resp.command != expected {
if resp.command == cmd::CHANGED && drained < DRAIN_CAP {
drained += 1;
self.device_changed = true;
continue;
}
self.release().await;
return Err(Error::UnexpectedResponse {
expected,
got: resp.command,
});
}
return match resp.status() {
Some(0) => Ok(resp),
Some(code) => Err(Error::DeviceStatus(code)),
None => {
self.release().await;
Err(Error::Truncated { got: 0, need: 4 })
}
};
}
}
async fn release(&mut self) {
if self.closed {
return;
}
self.closed = true;
let goodbye = Message::new(Service::Ui, ui::SUBSYSTEM, ui::GOODBYE, Vec::new());
if self.notify(&goodbye).await.is_err() {
return;
}
let _ = self.read_frame().await;
}
pub(crate) async fn notify(&mut self, msg: &Message) -> Result<()> {
let transport = self
.transport
.as_mut()
.ok_or_else(|| Error::Transport("session has no transport".into()))?;
let encoded = msg.encode();
if transport.write_timeout(&encoded, WRITE_LIMIT).await? {
Ok(())
} else {
Err(Error::Transport(format!(
"the device did not accept command {:#04x} within {}s: its bulk endpoints \
are stalled, and a power cycle is the only way out — `nord device recover` \
cannot help, because that frame cannot be delivered either",
msg.command,
WRITE_LIMIT.as_secs()
)))
}
}
pub async fn commit(mut self) -> Result<()> {
self.close().await
}
pub(crate) async fn commit_observing_changed(mut self) -> (Result<()>, bool) {
let result = self.close().await;
(result, self.device_changed)
}
async fn close(&mut self) -> Result<()> {
if self.closed {
return Ok(());
}
self.closed = true;
if let Err(e) = self
.request(Service::Program, 10, cmd::SESSION_CLOSE, &[])
.await
{
let _ = self
.request(Service::Ui, ui::SUBSYSTEM, ui::GOODBYE, &[])
.await;
return Err(e);
}
self.request(Service::Ui, ui::SUBSYSTEM, ui::GOODBYE, &[])
.await?;
Ok(())
}
pub async fn commit_with_read_limit(mut self, limit: Duration) -> Result<()> {
self.read_limit = limit;
self.close().await
}
pub fn abort(mut self) {
self.closed = true;
}
}
impl<T: Transport, C> Drop for Session<'_, T, C> {
fn drop(&mut self) {
debug_assert!(
self.closed,
"Session dropped without commit()/abort() — the device may be left \
mid-transaction. Close it explicitly."
);
}
}