refactor(backend): abstraire le sink de stream hors de tauri::ipc::Channel (#13)
Lot B2 du chantier server/client mode : le cœur backend émet désormais ses flux via une abstraction de sink agnostique, sans dépendre directement de tauri::ipc::Channel, préalable au futur serveur web + PTY WebSocket. - crates/backend : abstraction de sink (stream.rs) câblée dans lib.rs. - crates/app-tauri : implémentation Tauri du sink (stream.rs) et adaptation des surfaces lib.rs, pty.rs, chat.rs. - Nettoyage clippy des 2 warnings B2. Validé : cargo check --workspace vert, tests backend/app-tauri verts, cœur agnostique Tauri, clippy B2 propre. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -1,27 +1,27 @@
|
||||
//! Generic **PTY ↔ Tauri Channel** bridge infrastructure.
|
||||
//! Generic **PTY ↔ outbound stream sink** bridge infrastructure.
|
||||
//!
|
||||
//! ARCHITECTURE §2 decides that high-frequency PTY byte streams travel over
|
||||
//! per-session [`tauri::ipc::Channel`]s rather than global events, for
|
||||
//! throughput and isolation. This module provides the transport-side plumbing
|
||||
//! that L3 will plug a real `PtyPort` into; here there is **no real PTY** yet —
|
||||
//! only the registry + the abstraction that routes byte chunks to the right
|
||||
//! frontend channel.
|
||||
//! ARCHITECTURE §2 decides that high-frequency PTY byte streams travel over a
|
||||
//! per-session stream channel rather than global events, for throughput and
|
||||
//! isolation. This module provides the desktop-facing plumbing that adapts the
|
||||
//! shared backend sink abstraction to Tauri IPC.
|
||||
//!
|
||||
//! Design:
|
||||
//! - The frontend opens a terminal and passes a [`tauri::ipc::Channel`] for that
|
||||
//! session. The backend registers it in [`PtyBridge`] keyed by `SessionId`.
|
||||
//! - Whatever produces output (the PTY adapter in L3) calls
|
||||
//! [`PtyBridge::send_output`], which forwards the bytes on the matching
|
||||
//! channel. Bytes are sent as-is; the frontend xterm wrapper consumes them.
|
||||
//! session. The adapter wraps it as a sink and registers it in [`PtyBridge`].
|
||||
//! - Whatever produces output calls [`PtyBridge::send_output`], which forwards
|
||||
//! the bytes through the current sink. Bytes are sent as-is; the frontend
|
||||
//! xterm wrapper consumes them.
|
||||
//! - [`PtyBridge::unregister`] tears the channel down on terminal close.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
use backend::stream::OutputBridge;
|
||||
use tauri::ipc::Channel;
|
||||
|
||||
use domain::ids::SessionId;
|
||||
|
||||
use crate::stream::TauriChannelSink;
|
||||
|
||||
/// A chunk of PTY output bytes destined for a specific session's channel.
|
||||
///
|
||||
/// Sent as a raw byte vector; serde encodes it for the IPC channel. Kept as a
|
||||
@ -29,16 +29,12 @@ use domain::ids::SessionId;
|
||||
/// backpressure handling) without touching call sites.
|
||||
pub type PtyChunk = Vec<u8>;
|
||||
|
||||
/// Registry mapping live terminal sessions to their output [`Channel`].
|
||||
/// Registry mapping live terminal sessions to their output sink.
|
||||
///
|
||||
/// Thread-safe; cloned `Arc<PtyBridge>` is held in [`crate::state::AppState`].
|
||||
#[derive(Default)]
|
||||
pub struct PtyBridge {
|
||||
/// Per session: a monotonically-increasing **generation** plus the current
|
||||
/// output channel. The generation distinguishes successive (re-)attaches so a
|
||||
/// superseded pump thread can't tear down the channel of the attach that
|
||||
/// replaced it (see [`PtyBridge::unregister_if`]).
|
||||
channels: Mutex<HashMap<SessionId, (u64, Channel<PtyChunk>)>>,
|
||||
inner: OutputBridge<SessionId, PtyChunk>,
|
||||
}
|
||||
|
||||
impl PtyBridge {
|
||||
@ -46,7 +42,7 @@ impl PtyBridge {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
channels: Mutex::new(HashMap::new()),
|
||||
inner: OutputBridge::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,20 +51,13 @@ impl PtyBridge {
|
||||
/// generation, so the caller's pump thread can later tear down *only its own*
|
||||
/// registration via [`unregister_if`](Self::unregister_if).
|
||||
pub fn register(&self, session: SessionId, channel: Channel<PtyChunk>) -> u64 {
|
||||
if let Ok(mut map) = self.channels.lock() {
|
||||
let gen = map.get(&session).map_or(0, |(g, _)| g.wrapping_add(1));
|
||||
map.insert(session, (gen, channel));
|
||||
gen
|
||||
} else {
|
||||
0
|
||||
}
|
||||
self.inner
|
||||
.register(session, Arc::new(TauriChannelSink::new(channel)))
|
||||
}
|
||||
|
||||
/// Removes a session's channel unconditionally (terminal explicitly closed).
|
||||
pub fn unregister(&self, session: &SessionId) {
|
||||
if let Ok(mut map) = self.channels.lock() {
|
||||
map.remove(session);
|
||||
}
|
||||
self.inner.unregister(session);
|
||||
}
|
||||
|
||||
/// Removes a session's channel **only if** `gen` is still the current
|
||||
@ -76,11 +65,7 @@ impl PtyBridge {
|
||||
/// session has since been re-attached (newer generation), this is a no-op, so
|
||||
/// the dying thread never unregisters the live channel that superseded it.
|
||||
pub fn unregister_if(&self, session: &SessionId, gen: u64) {
|
||||
if let Ok(mut map) = self.channels.lock() {
|
||||
if matches!(map.get(session), Some((g, _)) if *g == gen) {
|
||||
map.remove(session);
|
||||
}
|
||||
}
|
||||
self.inner.unregister_if(session, gen);
|
||||
}
|
||||
|
||||
/// Forwards a chunk of output bytes to a session's channel.
|
||||
@ -89,18 +74,12 @@ impl PtyBridge {
|
||||
/// registered for the session (e.g. already closed). In L3 the PTY adapter's
|
||||
/// output stream drives this.
|
||||
pub fn send_output(&self, session: &SessionId, chunk: PtyChunk) -> bool {
|
||||
let Ok(map) = self.channels.lock() else {
|
||||
return false;
|
||||
};
|
||||
match map.get(session) {
|
||||
Some((_, channel)) => channel.send(chunk).is_ok(),
|
||||
None => false,
|
||||
}
|
||||
self.inner.send_output(session, chunk)
|
||||
}
|
||||
|
||||
/// Number of currently-registered sessions (handy for tests/diagnostics).
|
||||
#[must_use]
|
||||
pub fn active_sessions(&self) -> usize {
|
||||
self.channels.lock().map(|m| m.len()).unwrap_or(0)
|
||||
self.inner.active_sessions()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user