collapsible_if, let-chains, derivable_impls, clone_on_copy, useless_vec, approx_constant, io_other_error, single_match, collapsible_match, unnecessary_map_or, manual_is_multiple_of, result_large_err, too_many_arguments, upper_case_acronyms Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
703 lines
21 KiB
Rust
703 lines
21 KiB
Rust
use std::sync::{
|
|
Arc,
|
|
atomic::{AtomicBool, Ordering},
|
|
};
|
|
use std::time::Duration;
|
|
use std::{future, io};
|
|
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|
use tokio::select;
|
|
use tokio::sync::{Mutex, broadcast, mpsc};
|
|
use tokio::task::JoinHandle;
|
|
use tracing::{debug, error, info, warn};
|
|
|
|
use xserial_core::pipeline::{MultiPipeline, Pipeline};
|
|
use xserial_core::transport::Connection;
|
|
|
|
use crate::cmd::SessionCmd;
|
|
use crate::config::SessionConfig;
|
|
use crate::event::DecodedEntry;
|
|
use crate::history::RingBuffer;
|
|
|
|
pub type SessionId = u64;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum SessionEvent {
|
|
Connected(SessionId),
|
|
Disconnected(SessionId),
|
|
Data(SessionId, DecodedEntry),
|
|
Error(SessionId, String),
|
|
Closed(SessionId),
|
|
}
|
|
|
|
struct SessionHandleInner {
|
|
id: SessionId,
|
|
cmd_tx: mpsc::Sender<SessionCmd>,
|
|
event_tx: broadcast::Sender<SessionEvent>,
|
|
task: Mutex<Option<JoinHandle<()>>>,
|
|
close_requested: AtomicBool,
|
|
}
|
|
|
|
impl SessionHandleInner {
|
|
fn new(
|
|
id: SessionId,
|
|
cmd_tx: mpsc::Sender<SessionCmd>,
|
|
event_tx: broadcast::Sender<SessionEvent>,
|
|
task: JoinHandle<()>,
|
|
) -> Self {
|
|
Self {
|
|
id,
|
|
cmd_tx,
|
|
event_tx,
|
|
task: Mutex::new(Some(task)),
|
|
close_requested: AtomicBool::new(false),
|
|
}
|
|
}
|
|
|
|
async fn request_close(&self) -> Result<(), String> {
|
|
self.close_requested.store(true, Ordering::SeqCst);
|
|
self.cmd_tx
|
|
.send(SessionCmd::Close)
|
|
.await
|
|
.map_err(|_| String::from("session closed"))
|
|
}
|
|
|
|
fn request_close_nonblocking(&self) {
|
|
self.close_requested.store(true, Ordering::SeqCst);
|
|
let _ = self.cmd_tx.try_send(SessionCmd::Close);
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct SessionHandle {
|
|
pub id: SessionId,
|
|
inner: Arc<SessionHandleInner>,
|
|
}
|
|
|
|
impl SessionHandle {
|
|
fn new(inner: Arc<SessionHandleInner>) -> Self {
|
|
Self {
|
|
id: inner.id,
|
|
inner,
|
|
}
|
|
}
|
|
|
|
pub fn id(&self) -> SessionId {
|
|
self.id
|
|
}
|
|
|
|
pub async fn send(&self, data: Vec<u8>) -> Result<(), String> {
|
|
self.inner
|
|
.cmd_tx
|
|
.send(SessionCmd::Send(data))
|
|
.await
|
|
.map_err(|_| String::from("session closed"))
|
|
}
|
|
|
|
pub async fn close(&self) -> Result<(), String> {
|
|
self.inner.request_close().await
|
|
}
|
|
|
|
pub async fn reconfigure(&self, config: SessionConfig) -> Result<(), String> {
|
|
self.inner
|
|
.cmd_tx
|
|
.send(SessionCmd::Reconfigure(config))
|
|
.await
|
|
.map_err(|_| String::from("session closed"))
|
|
}
|
|
|
|
pub async fn connect(&self) -> Result<(), String> {
|
|
self.inner
|
|
.cmd_tx
|
|
.send(SessionCmd::Connect)
|
|
.await
|
|
.map_err(|_| String::from("session closed"))
|
|
}
|
|
|
|
pub async fn disconnect(&self) -> Result<(), String> {
|
|
self.inner
|
|
.cmd_tx
|
|
.send(SessionCmd::Disconnect)
|
|
.await
|
|
.map_err(|_| String::from("session closed"))
|
|
}
|
|
|
|
pub async fn reconnect(&self) -> Result<(), String> {
|
|
self.inner
|
|
.cmd_tx
|
|
.send(SessionCmd::Reconnect)
|
|
.await
|
|
.map_err(|_| String::from("session closed"))
|
|
}
|
|
|
|
pub async fn set_auto_reconnect(&self, enabled: bool) -> Result<(), String> {
|
|
self.inner
|
|
.cmd_tx
|
|
.send(SessionCmd::SetAutoReconnect(enabled))
|
|
.await
|
|
.map_err(|_| String::from("session closed"))
|
|
}
|
|
|
|
pub async fn set_dtr(&self, state: bool) -> Result<(), String> {
|
|
self.inner
|
|
.cmd_tx
|
|
.send(SessionCmd::SetDtr(state))
|
|
.await
|
|
.map_err(|_| String::from("session closed"))
|
|
}
|
|
|
|
pub async fn set_rts(&self, state: bool) -> Result<(), String> {
|
|
self.inner
|
|
.cmd_tx
|
|
.send(SessionCmd::SetRts(state))
|
|
.await
|
|
.map_err(|_| String::from("session closed"))
|
|
}
|
|
|
|
pub async fn read(&self, timeout_ms: u64) -> Option<DecodedEntry> {
|
|
let mut rx = self.inner.event_tx.subscribe();
|
|
let deadline = Duration::from_millis(timeout_ms);
|
|
tokio::time::timeout(deadline, async {
|
|
loop {
|
|
match rx.recv().await {
|
|
Ok(SessionEvent::Data(_, entry)) => return Some(entry),
|
|
Ok(SessionEvent::Error(_, _) | SessionEvent::Closed(_)) => return None,
|
|
Ok(SessionEvent::Connected(_) | SessionEvent::Disconnected(_)) => {}
|
|
Err(broadcast::error::RecvError::Lagged(_)) => continue,
|
|
Err(broadcast::error::RecvError::Closed) => return None,
|
|
}
|
|
}
|
|
})
|
|
.await
|
|
.ok()
|
|
.flatten()
|
|
}
|
|
|
|
pub fn subscribe(&self) -> broadcast::Receiver<SessionEvent> {
|
|
self.inner.event_tx.subscribe()
|
|
}
|
|
|
|
pub(crate) async fn join(&self) {
|
|
let task = {
|
|
let mut slot = self.inner.task.lock().await;
|
|
slot.take()
|
|
};
|
|
if let Some(task) = task {
|
|
let _ = task.await;
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Drop for SessionHandle {
|
|
fn drop(&mut self) {
|
|
if Arc::strong_count(&self.inner) == 1 {
|
|
self.inner.request_close_nonblocking();
|
|
if let Ok(mut task) = self.inner.task.try_lock()
|
|
&& let Some(task) = task.take()
|
|
{
|
|
task.abort();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct Session {
|
|
id: SessionId,
|
|
config: SessionConfig,
|
|
conn: Option<Connection>,
|
|
pipeline: MultiPipeline,
|
|
pipeline_build_error: Option<String>,
|
|
history: RingBuffer<DecodedEntry>,
|
|
cmd_rx: mpsc::Receiver<SessionCmd>,
|
|
event_tx: broadcast::Sender<SessionEvent>,
|
|
close_requested: bool,
|
|
desired_connected: bool,
|
|
}
|
|
|
|
impl Session {
|
|
pub fn spawn(id: SessionId, config: SessionConfig) -> SessionHandle {
|
|
let (cmd_tx, cmd_rx) = mpsc::channel(32);
|
|
let (event_tx, _) = broadcast::channel(256);
|
|
let (pipeline, pipeline_build_error) = match Self::build_pipeline(&config) {
|
|
Ok(pipeline) => (pipeline, None),
|
|
Err(err) => (
|
|
MultiPipeline::new(),
|
|
Some(format!("pipeline build failed: {err}")),
|
|
),
|
|
};
|
|
let mut session = Self {
|
|
id,
|
|
pipeline,
|
|
pipeline_build_error,
|
|
history: RingBuffer::new(config.history_limit),
|
|
config,
|
|
conn: None,
|
|
cmd_rx,
|
|
event_tx: event_tx.clone(),
|
|
close_requested: false,
|
|
desired_connected: true,
|
|
};
|
|
let task = tokio::spawn(async move { session.run().await });
|
|
let inner = Arc::new(SessionHandleInner::new(id, cmd_tx, event_tx, task));
|
|
SessionHandle::new(inner)
|
|
}
|
|
|
|
fn build_pipeline(config: &SessionConfig) -> crate::Result<MultiPipeline> {
|
|
let mut pipelines = MultiPipeline::new();
|
|
for pipeline in &config.pipelines {
|
|
pipelines.add(Pipeline::new(
|
|
pipeline.name.clone(),
|
|
pipeline.framer.clone().build()?,
|
|
pipeline.decoder.clone().build()?,
|
|
));
|
|
}
|
|
Ok(pipelines)
|
|
}
|
|
|
|
async fn run(&mut self) {
|
|
info!(session_id = self.id, "session started");
|
|
|
|
if let Some(message) = self.pipeline_build_error.take() {
|
|
self.emit_error(message);
|
|
self.desired_connected = false;
|
|
}
|
|
|
|
if self.desired_connected
|
|
&& let Err(err) = self.connect().await
|
|
{
|
|
warn!(session_id = self.id, error = %err, "initial connect failed");
|
|
self.emit_error(format!("connect failed: {err}"));
|
|
}
|
|
|
|
loop {
|
|
let reconnect_timer = self.reconnect_timer();
|
|
select! {
|
|
cmd = self.cmd_rx.recv() => {
|
|
if !self.handle_command(cmd).await {
|
|
break;
|
|
}
|
|
}
|
|
read = try_read(&mut self.conn, &mut self.pipeline) => {
|
|
if !self.handle_read_result(read).await {
|
|
break;
|
|
}
|
|
}
|
|
_ = reconnect_timer => {
|
|
if let Err(err) = self.connect().await {
|
|
self.emit_error(format!("connect failed: {err}"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.disconnect(false).await;
|
|
self.emit_closed();
|
|
info!(session_id = self.id, "session ended");
|
|
}
|
|
|
|
async fn handle_command(&mut self, cmd: Option<SessionCmd>) -> bool {
|
|
match cmd {
|
|
Some(SessionCmd::Close) => {
|
|
self.close_requested = true;
|
|
self.desired_connected = false;
|
|
false
|
|
}
|
|
Some(SessionCmd::Connect) => {
|
|
self.desired_connected = true;
|
|
if self.conn.is_none()
|
|
&& let Err(err) = self.connect().await
|
|
{
|
|
self.emit_error(format!("connect failed: {err}"));
|
|
}
|
|
true
|
|
}
|
|
Some(SessionCmd::Disconnect) => {
|
|
self.desired_connected = false;
|
|
self.disconnect(true).await;
|
|
true
|
|
}
|
|
Some(SessionCmd::Reconnect) => {
|
|
self.desired_connected = true;
|
|
self.disconnect(true).await;
|
|
if let Err(err) = self.connect().await {
|
|
self.emit_error(format!("reconnect: {err}"));
|
|
}
|
|
true
|
|
}
|
|
Some(SessionCmd::SetAutoReconnect(enabled)) => {
|
|
self.config.auto_reconnect = enabled;
|
|
true
|
|
}
|
|
Some(SessionCmd::Send(data)) => {
|
|
self.handle_send(data).await;
|
|
true
|
|
}
|
|
Some(SessionCmd::Reconfigure(config)) => {
|
|
self.handle_reconfigure(config).await;
|
|
true
|
|
}
|
|
Some(SessionCmd::SetDtr(state)) => {
|
|
self.handle_set_dtr(state);
|
|
true
|
|
}
|
|
Some(SessionCmd::SetRts(state)) => {
|
|
self.handle_set_rts(state);
|
|
true
|
|
}
|
|
None => false,
|
|
}
|
|
}
|
|
|
|
async fn handle_send(&mut self, data: Vec<u8>) {
|
|
debug!(session_id = self.id, len = data.len(), "sending data");
|
|
match self.conn.as_mut() {
|
|
Some(conn) => {
|
|
if let Err(err) = conn.write_all(&data).await {
|
|
self.emit_error(format!("write: {err}"));
|
|
}
|
|
}
|
|
None => self.emit_error(String::from("write: session not connected")),
|
|
}
|
|
}
|
|
|
|
async fn handle_reconfigure(&mut self, config: SessionConfig) {
|
|
info!(session_id = self.id, "reconfiguring session");
|
|
let pipeline = match Self::build_pipeline(&config) {
|
|
Ok(pipeline) => pipeline,
|
|
Err(err) => {
|
|
self.emit_error(format!("pipeline build failed: {err}"));
|
|
return;
|
|
}
|
|
};
|
|
self.disconnect(true).await;
|
|
self.config = config;
|
|
self.pipeline = pipeline;
|
|
self.history = RingBuffer::new(self.config.history_limit);
|
|
|
|
if let Err(err) = self.connect().await {
|
|
self.emit_error(format!("reconnect: {err}"));
|
|
}
|
|
}
|
|
|
|
fn handle_set_dtr(&mut self, state: bool) {
|
|
match self.conn.as_mut() {
|
|
Some(conn) => {
|
|
if let Err(err) = conn.set_dtr(state) {
|
|
self.emit_error(format!("set_dtr: {err}"));
|
|
}
|
|
}
|
|
None => self.emit_error(String::from("set_dtr: session not connected")),
|
|
}
|
|
}
|
|
|
|
fn handle_set_rts(&mut self, state: bool) {
|
|
match self.conn.as_mut() {
|
|
Some(conn) => {
|
|
if let Err(err) = conn.set_rts(state) {
|
|
self.emit_error(format!("set_rts: {err}"));
|
|
}
|
|
}
|
|
None => self.emit_error(String::from("set_rts: session not connected")),
|
|
}
|
|
}
|
|
|
|
async fn handle_read_result(
|
|
&mut self,
|
|
result: Result<Vec<DecodedEntry>, std::io::Error>,
|
|
) -> bool {
|
|
match result {
|
|
Ok(entries) => {
|
|
for entry in entries {
|
|
self.history.push(entry.clone());
|
|
let _ = self.event_tx.send(SessionEvent::Data(self.id, entry));
|
|
}
|
|
true
|
|
}
|
|
Err(err) => {
|
|
error!(session_id = self.id, error = %err, "read error");
|
|
self.emit_error(err.to_string());
|
|
self.disconnect(true).await;
|
|
true
|
|
}
|
|
}
|
|
}
|
|
|
|
fn reconnect_timer(&self) -> impl std::future::Future<Output = ()> + Send + 'static {
|
|
let should_retry = self.conn.is_none()
|
|
&& self.desired_connected
|
|
&& self.config.auto_reconnect
|
|
&& !self.close_requested;
|
|
async move {
|
|
if should_retry {
|
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
} else {
|
|
future::pending::<()>().await;
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn connect(&mut self) -> Result<(), io::Error> {
|
|
if self.conn.is_some() {
|
|
return Ok(());
|
|
}
|
|
let transport = self.config.transport.clone();
|
|
debug!(session_id = self.id, ?transport, "connecting session");
|
|
let mut conn = Connection::new(transport);
|
|
conn.connect().await.map_err(to_io_error)?;
|
|
self.conn = Some(conn);
|
|
let _ = self.event_tx.send(SessionEvent::Connected(self.id));
|
|
info!(session_id = self.id, "session connected");
|
|
Ok(())
|
|
}
|
|
|
|
async fn disconnect(&mut self, emit_disconnected: bool) {
|
|
if let Some(mut conn) = self.conn.take() {
|
|
let _ = conn.disconnect().await;
|
|
if emit_disconnected {
|
|
let _ = self.event_tx.send(SessionEvent::Disconnected(self.id));
|
|
}
|
|
info!(session_id = self.id, "session disconnected");
|
|
}
|
|
self.pipeline.reset();
|
|
}
|
|
|
|
fn emit_error(&self, message: String) {
|
|
let _ = self.event_tx.send(SessionEvent::Error(self.id, message));
|
|
}
|
|
|
|
fn emit_closed(&self) {
|
|
let _ = self.event_tx.send(SessionEvent::Closed(self.id));
|
|
}
|
|
}
|
|
|
|
async fn try_read(
|
|
conn: &mut Option<Connection>,
|
|
pipeline: &mut MultiPipeline,
|
|
) -> Result<Vec<DecodedEntry>, io::Error> {
|
|
if conn.is_none() {
|
|
future::pending::<()>().await;
|
|
unreachable!("pending future should never resolve");
|
|
}
|
|
|
|
let conn = conn.as_mut().expect("connection presence checked");
|
|
|
|
let mut buf = [0u8; 4096];
|
|
match conn.read(&mut buf).await {
|
|
Ok(0) => Err(io::Error::new(
|
|
io::ErrorKind::UnexpectedEof,
|
|
"connection closed",
|
|
)),
|
|
Ok(n) => Ok(pipeline
|
|
.feed(&buf[..n])
|
|
.into_iter()
|
|
.map(|result| DecodedEntry {
|
|
pipeline_name: result.pipeline_name,
|
|
data: result.data,
|
|
})
|
|
.collect()),
|
|
Err(err) => Err(err),
|
|
}
|
|
}
|
|
|
|
fn to_io_error(err: xserial_core::error::Error) -> std::io::Error {
|
|
io::Error::other(err.to_string())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::config::{DecoderConfig, FramerConfig, PipelineConfig};
|
|
|
|
fn tcp_config() -> SessionConfig {
|
|
SessionConfig {
|
|
transport: xserial_core::transport::TransportConfig::Tcp {
|
|
addr: "127.0.0.1:1".into(),
|
|
},
|
|
pipelines: vec![PipelineConfig {
|
|
name: "text".into(),
|
|
framer: FramerConfig::Line {
|
|
strip_cr: true,
|
|
max_line_len: 65536,
|
|
},
|
|
decoder: DecoderConfig::Text {
|
|
encoding: xserial_core::protocol::text::TextEncoding::Utf8,
|
|
},
|
|
}],
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn session_id_is_u64() {
|
|
let _id: SessionId = 42;
|
|
}
|
|
|
|
#[test]
|
|
fn session_event_debug() {
|
|
let e1 = SessionEvent::Connected(1);
|
|
let e2 = SessionEvent::Disconnected(1);
|
|
let e3 = SessionEvent::Closed(1);
|
|
let e4 = SessionEvent::Error(1, "oops".into());
|
|
let _ = format!("{:?}", e1);
|
|
let _ = format!("{:?}", e2);
|
|
let _ = format!("{:?}", e3);
|
|
let _ = format!("{:?}", e4);
|
|
}
|
|
|
|
#[test]
|
|
fn session_event_clone() {
|
|
let e1 = SessionEvent::Connected(1);
|
|
let e2 = e1.clone();
|
|
match (e1, e2) {
|
|
(SessionEvent::Connected(a), SessionEvent::Connected(b)) => assert_eq!(a, b),
|
|
_ => panic!(),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn spawn_and_close_session() {
|
|
let handle = Session::spawn(1, tcp_config());
|
|
let mut rx = handle.subscribe();
|
|
let event = tokio::time::timeout(Duration::from_secs(5), rx.recv())
|
|
.await
|
|
.expect("timeout");
|
|
if let Ok(event) = event {
|
|
assert!(matches!(
|
|
event,
|
|
SessionEvent::Error(_, _) | SessionEvent::Connected(_)
|
|
));
|
|
}
|
|
let _ = handle.close().await;
|
|
handle.join().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn handle_send_to_dead_session() {
|
|
let handle = Session::spawn(2, tcp_config());
|
|
tokio::time::sleep(Duration::from_millis(200)).await;
|
|
let _ = handle.send(b"data".to_vec()).await;
|
|
let _ = handle.close().await;
|
|
handle.join().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn handle_close_twice_is_idempotent() {
|
|
let handle = Session::spawn(3, tcp_config());
|
|
let _ = handle.close().await;
|
|
let _ = handle.close().await;
|
|
handle.join().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn handle_read_after_close() {
|
|
let handle = Session::spawn(4, tcp_config());
|
|
let _ = handle.close().await;
|
|
let result = handle.read(100).await;
|
|
assert!(result.is_none());
|
|
handle.join().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn handle_read_timeout_returns_none() {
|
|
let handle = Session::spawn(5, tcp_config());
|
|
let result = handle.read(100).await;
|
|
assert!(result.is_none());
|
|
let _ = handle.close().await;
|
|
handle.join().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn handle_subscribe_receives_events() {
|
|
let handle = Session::spawn(6, tcp_config());
|
|
let mut rx = handle.subscribe();
|
|
let event = tokio::time::timeout(Duration::from_secs(5), rx.recv())
|
|
.await
|
|
.expect("timeout");
|
|
if let Ok(event) = event {
|
|
assert!(matches!(
|
|
event,
|
|
SessionEvent::Error(_, _) | SessionEvent::Connected(_)
|
|
));
|
|
}
|
|
let _ = handle.close().await;
|
|
handle.join().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn handle_id_matches() {
|
|
let handle = Session::spawn(99, tcp_config());
|
|
assert_eq!(handle.id(), 99);
|
|
let _ = handle.close().await;
|
|
handle.join().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn handle_reconfigure_triggers_reconnect() {
|
|
let handle = Session::spawn(7, tcp_config());
|
|
let new_cfg = tcp_config();
|
|
let _ = handle.reconfigure(new_cfg).await;
|
|
let _ = handle.close().await;
|
|
handle.join().await;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn try_read_without_connection_stays_pending() {
|
|
let mut conn = None;
|
|
let mut pipeline = MultiPipeline::new();
|
|
|
|
let result = tokio::time::timeout(
|
|
Duration::from_millis(50),
|
|
try_read(&mut conn, &mut pipeline),
|
|
)
|
|
.await;
|
|
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn handle_drop_closes_session() {
|
|
let handle = Session::spawn(8, tcp_config());
|
|
let mut rx = handle.subscribe();
|
|
drop(handle);
|
|
let mut closed = false;
|
|
while let Ok(Ok(event)) = tokio::time::timeout(Duration::from_millis(200), rx.recv()).await
|
|
{
|
|
if matches!(event, SessionEvent::Closed(_)) {
|
|
closed = true;
|
|
break;
|
|
}
|
|
}
|
|
let _ = closed;
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn multiple_subscribers_all_receive() {
|
|
let handle = Session::spawn(9, tcp_config());
|
|
let mut rx1 = handle.subscribe();
|
|
let mut rx2 = handle.subscribe();
|
|
|
|
let e1 = tokio::time::timeout(Duration::from_secs(5), rx1.recv())
|
|
.await
|
|
.unwrap();
|
|
if let Ok(e1) = e1 {
|
|
assert!(matches!(
|
|
e1,
|
|
SessionEvent::Connected(9) | SessionEvent::Error(9, _)
|
|
));
|
|
}
|
|
|
|
let e2 = tokio::time::timeout(Duration::from_secs(1), rx2.recv())
|
|
.await
|
|
.unwrap();
|
|
if let Ok(e2) = e2 {
|
|
assert!(matches!(
|
|
e2,
|
|
SessionEvent::Connected(9) | SessionEvent::Error(9, _)
|
|
));
|
|
}
|
|
|
|
let _ = handle.close().await;
|
|
handle.join().await;
|
|
}
|
|
}
|