Files
netris-nestri/packages/server/src/p2p/p2p_protocol_stream.rs
Kristian Ollikainen 41dca22d9d feat(runner): More runner improvements (#294)
## Description
Whew..

- Steam can now run without namespaces using live-patcher (because
Docker..)
- Improved NVIDIA GPU selection and handling
- Pipeline tests for GPU picking logic
- Optimizations and cleanup all around
- SSH (by default disabled) for easier instance debugging.
- CachyOS' Proton because that works without namespaces (couldn't figure
out how to enable automatically in Steam yet..)
- Package updates and partial removal of futures (libp2p is going to
switch to Tokio in next release hopefully)



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- SSH server can now be enabled within the container for remote access
when configured.
- Added persistent live patching for Steam runtime entrypoints to
improve compatibility with namespace-less applications.
- Enhanced GPU selection with multi-GPU support and PCI bus ID matching
for improved hardware compatibility.
- Improved encoder selection by runtime testing of video encoders for
better reliability.
  - Added WebSocket transport support in peer-to-peer networking.
- Added flexible compositor and application launching with configurable
commands and improved socket handling.

- **Bug Fixes**
- Addressed NVIDIA-specific GStreamer issues by setting new environment
variables.
  - Improved error handling and logging for GPU and encoder selection.
- Fixed process monitoring to handle patcher restarts and added cleanup
logic.
- Added GStreamer cache clearing workaround for Wayland socket failures.

- **Improvements**
- Real-time logging of container processes to standard output and error
for easier monitoring.
- Enhanced process management and reduced CPU usage in protocol handling
loops.
- Updated dependency versions for greater stability and feature support.
  - Improved audio capture defaults and expanded audio pipeline support.
- Enhanced video pipeline setup with conditional handling for different
encoder APIs and DMA-BUF support.
- Refined concurrency and lifecycle management in protocol messaging for
increased robustness.
- Consistent namespace usage and updated crate references across the
codebase.
- Enhanced SSH configuration with key management, port customization,
and startup verification.
  - Improved GPU and video encoder integration in pipeline construction.
- Simplified error handling and consolidated write operations in
protocol streams.
- Removed Ludusavi installation from container image and updated package
installations.

- **Other**
- Minor formatting and style changes for better code readability and
maintainability.
- Docker build context now ignores `.idea` directory to streamline
builds.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
2025-07-07 09:06:48 +03:00

193 lines
6.5 KiB
Rust

use crate::p2p::p2p::NestriConnection;
use crate::p2p::p2p_safestream::SafeStream;
use dashmap::DashMap;
use libp2p::StreamProtocol;
use std::sync::Arc;
use tokio::sync::mpsc;
use tokio::time::{self, Duration};
// Cloneable callback type
pub type CallbackInner = dyn Fn(Vec<u8>) + Send + Sync + 'static;
pub struct Callback(Arc<CallbackInner>);
impl Callback {
pub fn new<F>(f: F) -> Self
where
F: Fn(Vec<u8>) + Send + Sync + 'static,
{
Callback(Arc::new(f))
}
pub fn call(&self, data: Vec<u8>) {
self.0(data)
}
}
impl Clone for Callback {
fn clone(&self) -> Self {
Callback(Arc::clone(&self.0))
}
}
impl From<Box<CallbackInner>> for Callback {
fn from(boxed: Box<CallbackInner>) -> Self {
Callback(Arc::from(boxed))
}
}
/// NestriStreamProtocol manages the stream protocol for Nestri connections.
pub struct NestriStreamProtocol {
tx: Option<mpsc::Sender<Vec<u8>>>,
safe_stream: Arc<SafeStream>,
callbacks: Arc<DashMap<String, Callback>>,
read_handle: Option<tokio::task::JoinHandle<()>>,
write_handle: Option<tokio::task::JoinHandle<()>>,
}
impl NestriStreamProtocol {
const NESTRI_PROTOCOL_STREAM_PUSH: StreamProtocol =
StreamProtocol::new("/nestri-relay/stream-push/1.0.0");
pub async fn new(
nestri_connection: NestriConnection,
) -> Result<Self, Box<dyn std::error::Error>> {
let mut nestri_connection = nestri_connection.clone();
let push_stream = match nestri_connection
.control
.open_stream(nestri_connection.peer_id, Self::NESTRI_PROTOCOL_STREAM_PUSH)
.await
{
Ok(stream) => stream,
Err(e) => {
return Err(Box::new(e));
}
};
let mut sp = NestriStreamProtocol {
tx: None,
safe_stream: Arc::new(SafeStream::new(push_stream)),
callbacks: Arc::new(DashMap::new()),
read_handle: None,
write_handle: None,
};
// Use restart method to initialize the read and write loops
sp.restart()?;
Ok(sp)
}
pub fn restart(&mut self) -> Result<(), Box<dyn std::error::Error>> {
// Return if tx and handles are already initialized
if self.tx.is_some() && self.read_handle.is_some() && self.write_handle.is_some() {
tracing::warn!("NestriStreamProtocol is already running, restart skipped");
return Ok(());
}
let (tx, rx) = mpsc::channel(1000);
self.tx = Some(tx);
self.read_handle = Some(self.spawn_read_loop());
self.write_handle = Some(self.spawn_write_loop(rx));
Ok(())
}
fn spawn_read_loop(&self) -> tokio::task::JoinHandle<()> {
let safe_stream = self.safe_stream.clone();
let callbacks = self.callbacks.clone();
tokio::spawn(async move {
loop {
let data = {
match safe_stream.receive_raw().await {
Ok(data) => data,
Err(e) => {
tracing::error!("Error receiving data: {}", e);
break; // Exit the loop on error
}
}
};
match serde_json::from_slice::<crate::messages::MessageBase>(&data) {
Ok(base_message) => {
let response_type = base_message.payload_type;
// With DashMap, we don't need explicit locking
// we just get the callback directly if it exists
if let Some(callback) = callbacks.get(&response_type) {
// Execute the callback
if let Err(e) =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
callback.call(data.clone())
}))
{
tracing::error!(
"Callback for response type '{}' panicked: {:?}",
response_type,
e
);
}
} else {
tracing::warn!(
"No callback registered for response type: {}",
response_type
);
}
}
Err(e) => {
tracing::error!("Failed to decode message: {}", e);
}
}
// Add a small sleep to reduce CPU usage
time::sleep(Duration::from_micros(100)).await;
}
})
}
fn spawn_write_loop(&self, mut rx: mpsc::Receiver<Vec<u8>>) -> tokio::task::JoinHandle<()> {
let safe_stream = self.safe_stream.clone();
tokio::spawn(async move {
loop {
// Wait for a message from the channel
match rx.recv().await {
Some(tx_data) => {
if let Err(e) = safe_stream.send_raw(&tx_data).await {
tracing::error!("Error sending data: {:?}", e);
}
}
None => {
tracing::info!("Receiver closed, exiting write loop");
break;
}
}
// Add a small sleep to reduce CPU usage
time::sleep(Duration::from_micros(100)).await;
}
})
}
pub fn send_message<M: serde::Serialize>(
&self,
message: &M,
) -> Result<(), Box<dyn std::error::Error>> {
let json_data = serde_json::to_vec(message)?;
let Some(tx) = &self.tx else {
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::NotConnected,
if self.read_handle.is_none() && self.write_handle.is_none() {
"NestriStreamProtocol has been shutdown"
} else {
"NestriStreamProtocol is not properly initialized"
},
)));
};
tx.try_send(json_data)?;
Ok(())
}
pub fn register_callback<F>(&self, response_type: &str, callback: F)
where
F: Fn(Vec<u8>) + Send + Sync + 'static,
{
self.callbacks
.insert(response_type.to_string(), Callback::new(callback));
}
}