410 lines
12 KiB
Markdown
410 lines
12 KiB
Markdown
|
|
# Analyse : Rendre rust-cast asynchrone vs autres options
|
||
|
|
|
||
|
|
**Date :** 2025-12-27
|
||
|
|
**Question :** Est-il plus simple d'intégrer rust-cast et le modifier pour le rendre asynchrone ?
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. Analyse de la codebase rust-cast
|
||
|
|
|
||
|
|
### Taille et complexité
|
||
|
|
|
||
|
|
```bash
|
||
|
|
Total : ~5800 lignes de code Rust
|
||
|
|
Structure modulaire :
|
||
|
|
├── src/lib.rs (~570 lignes)
|
||
|
|
├── src/message_manager.rs (~300 lignes)
|
||
|
|
├── src/channels/
|
||
|
|
│ ├── media.rs (~800 lignes)
|
||
|
|
│ ├── receiver.rs (~400 lignes)
|
||
|
|
│ ├── heartbeat.rs (~100 lignes)
|
||
|
|
│ └── connection.rs (~100 lignes)
|
||
|
|
├── src/cast/
|
||
|
|
│ ├── cast_channel.rs (généré par protobuf)
|
||
|
|
│ └── proxies.rs (~500 lignes)
|
||
|
|
└── src/errors.rs, utils.rs (~200 lignes)
|
||
|
|
```
|
||
|
|
|
||
|
|
**Conclusion :** Codebase de taille **modeste et bien structurée**.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. Points bloquants identifiés
|
||
|
|
|
||
|
|
### 2.1 I/O synchrone bloquant
|
||
|
|
|
||
|
|
Tous les I/O passent par `MessageManager<S>` où `S: Read + Write` :
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// message_manager.rs:246-253
|
||
|
|
fn read(&self) -> Result<CastMessage, Error> {
|
||
|
|
let mut buffer: [u8; 4] = [0; 4];
|
||
|
|
let reader = &mut *self.stream.borrow_mut();
|
||
|
|
|
||
|
|
reader.read_exact(&mut buffer)?; // ❌ BLOQUANT
|
||
|
|
let length = utils::read_u32_from_buffer(&buffer)?;
|
||
|
|
|
||
|
|
let mut buffer: Vec<u8> = Vec::with_capacity(length as usize);
|
||
|
|
let mut limited_reader = reader.take(u64::from(length));
|
||
|
|
limited_reader.read_to_end(&mut buffer)?; // ❌ BLOQUANT
|
||
|
|
...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// message_manager.rs:138-141
|
||
|
|
pub fn send(&self, message: CastMessage) -> Result<(), Error> {
|
||
|
|
...
|
||
|
|
let writer = &mut *self.stream.borrow_mut();
|
||
|
|
writer.write_all(&message_length_buffer)?; // ❌ BLOQUANT
|
||
|
|
writer.write_all(&message_content_buffer)?; // ❌ BLOQUANT
|
||
|
|
...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2.2 Connexion TLS
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// lib.rs:125
|
||
|
|
let stream = StreamOwned::new(
|
||
|
|
conn,
|
||
|
|
TcpStream::connect((host.as_ref(), port))? // ❌ BLOQUANT
|
||
|
|
);
|
||
|
|
```
|
||
|
|
|
||
|
|
**Total : 5 points bloquants critiques** (connect, read_exact, read_to_end, 2x write_all)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. Effort pour rendre rust-cast asynchrone
|
||
|
|
|
||
|
|
### 3.1 Modifications requises
|
||
|
|
|
||
|
|
#### A. Remplacer la stack réseau
|
||
|
|
|
||
|
|
**Avant (sync) :**
|
||
|
|
```rust
|
||
|
|
use std::net::TcpStream;
|
||
|
|
use rustls::{ClientConnection, StreamOwned};
|
||
|
|
|
||
|
|
type TlsStream = StreamOwned<ClientConnection, TcpStream>;
|
||
|
|
```
|
||
|
|
|
||
|
|
**Après (async) :**
|
||
|
|
```rust
|
||
|
|
use async_io::Async;
|
||
|
|
use std::net::TcpStream;
|
||
|
|
use async_rustls::{TlsConnector, client::TlsStream};
|
||
|
|
|
||
|
|
// OU avec tokio :
|
||
|
|
use tokio::net::TcpStream;
|
||
|
|
use tokio_rustls::{TlsConnector, client::TlsStream};
|
||
|
|
```
|
||
|
|
|
||
|
|
⚠️ **PROBLÈME :** `rustls::StreamOwned` n'existe pas en version async native. Il faut utiliser :
|
||
|
|
- `async-rustls` (pour async-std/smol)
|
||
|
|
- `tokio-rustls` (pour tokio)
|
||
|
|
|
||
|
|
Ces crates ont une **API différente** de `rustls::StreamOwned`.
|
||
|
|
|
||
|
|
#### B. Modifier `MessageManager`
|
||
|
|
|
||
|
|
```diff
|
||
|
|
- pub struct MessageManager<S> where S: Write + Read {
|
||
|
|
+ pub struct MessageManager<S> where S: AsyncWrite + AsyncRead + Unpin {
|
||
|
|
|
||
|
|
- pub fn send(&self, message: CastMessage) -> Result<(), Error> {
|
||
|
|
+ pub async fn send(&self, message: CastMessage) -> Result<(), Error> {
|
||
|
|
...
|
||
|
|
- writer.write_all(&message_length_buffer)?;
|
||
|
|
+ writer.write_all(&message_length_buffer).await?;
|
||
|
|
}
|
||
|
|
|
||
|
|
- pub fn receive(&self) -> Result<CastMessage, Error> {
|
||
|
|
+ pub async fn receive(&self) -> Result<CastMessage, Error> {
|
||
|
|
...
|
||
|
|
}
|
||
|
|
|
||
|
|
- fn read(&self) -> Result<CastMessage, Error> {
|
||
|
|
+ async fn read(&self) -> Result<CastMessage, Error> {
|
||
|
|
- reader.read_exact(&mut buffer)?;
|
||
|
|
+ reader.read_exact(&mut buffer).await?;
|
||
|
|
- limited_reader.read_to_end(&mut buffer)?;
|
||
|
|
+ limited_reader.read_to_end(&mut buffer).await?;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### C. Propager `async` dans tous les channels
|
||
|
|
|
||
|
|
**Avant :**
|
||
|
|
```rust
|
||
|
|
// channels/media.rs
|
||
|
|
impl<'a, S> MediaChannel<'a, S> where S: Write + Read {
|
||
|
|
pub fn play(&self, ...) -> Result<(), Error> {
|
||
|
|
self.message_manager.send(...)?;
|
||
|
|
self.message_manager.receive_find_map(...)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Après :**
|
||
|
|
```rust
|
||
|
|
impl<'a, S> MediaChannel<'a, S> where S: AsyncWrite + AsyncRead + Unpin {
|
||
|
|
pub async fn play(&self, ...) -> Result<(), Error> {
|
||
|
|
self.message_manager.send(...).await?;
|
||
|
|
self.message_manager.receive_find_map(...).await
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Impact :** TOUS les channels (media, receiver, heartbeat, connection) deviennent `async`.
|
||
|
|
|
||
|
|
#### D. Modifier `CastDevice`
|
||
|
|
|
||
|
|
```diff
|
||
|
|
impl<'a> CastDevice<'a> {
|
||
|
|
- pub fn connect<S>(host: S, port: u16) -> Result<CastDevice<'a>, Error>
|
||
|
|
+ pub async fn connect<S>(host: S, port: u16) -> Result<CastDevice<'a>, Error>
|
||
|
|
{
|
||
|
|
...
|
||
|
|
- let stream = TcpStream::connect((host.as_ref(), port))?;
|
||
|
|
+ let stream = TcpStream::connect((host.as_ref(), port)).await?;
|
||
|
|
...
|
||
|
|
}
|
||
|
|
|
||
|
|
- pub fn receive(&self) -> Result<ChannelMessage, Error> {
|
||
|
|
+ pub async fn receive(&self) -> Result<ChannelMessage, Error> {
|
||
|
|
- let cast_message = self.message_manager.receive()?;
|
||
|
|
+ let cast_message = self.message_manager.receive().await?;
|
||
|
|
...
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3.2 Estimation de l'effort
|
||
|
|
|
||
|
|
| Tâche | Fichiers touchés | Complexité | Temps estimé |
|
||
|
|
|-------|------------------|------------|--------------|
|
||
|
|
| Choisir stack async (smol vs tokio) | - | Faible | 1h |
|
||
|
|
| Migrer vers async-rustls/tokio-rustls | lib.rs | **MOYENNE** | 4-6h |
|
||
|
|
| Rendre MessageManager async | message_manager.rs | **MOYENNE** | 4-6h |
|
||
|
|
| Rendre tous les channels async | 4 fichiers | **MOYENNE-ÉLEVÉE** | 8-12h |
|
||
|
|
| Mettre à jour CastDevice | lib.rs | Moyenne | 2-4h |
|
||
|
|
| Tests et debug | Tous | **ÉLEVÉE** | 8-16h |
|
||
|
|
| **TOTAL** | **~10 fichiers** | **ÉLEVÉE** | **27-45 heures** |
|
||
|
|
|
||
|
|
⚠️ **RISQUES :**
|
||
|
|
- API `async-rustls` différente de `rustls::StreamOwned` → peut nécessiter refactoring profond
|
||
|
|
- Gestion des locks async (`Mutex` → `async_lock::Mutex` ou `tokio::sync::Mutex`)
|
||
|
|
- Bugs subtils liés à la concurrence async
|
||
|
|
- Tests nécessaires pour valider la stabilité
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. Comparaison des 4 options
|
||
|
|
|
||
|
|
### Option 1 : ✅ **Rester avec rust-cast sync et corriger le TLS**
|
||
|
|
|
||
|
|
**Effort :** FAIBLE (2-8 heures)
|
||
|
|
|
||
|
|
**Actions :**
|
||
|
|
- Investiguer les erreurs TLS prématurées
|
||
|
|
- Ajouter retry logic sur les reconnexions
|
||
|
|
- Améliorer la gestion d'erreur dans [chromecast_renderer.rs](pmocontrol/src/chromecast_renderer.rs:86-104)
|
||
|
|
- Peut-être ajuster les timeouts de lecture
|
||
|
|
|
||
|
|
**Avantages :**
|
||
|
|
- ✅ Garde l'API sync compatible avec PMOMusic
|
||
|
|
- ✅ Risque minimal
|
||
|
|
- ✅ Solution rapide
|
||
|
|
|
||
|
|
**Inconvénients :**
|
||
|
|
- ⚠️ Ne résout peut-être pas tous les problèmes TLS
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Option 2 : 🔧 **Forker rust-cast et moderniser le TLS (reste sync)**
|
||
|
|
|
||
|
|
**Effort :** MOYEN (8-16 heures)
|
||
|
|
|
||
|
|
**Actions :**
|
||
|
|
- Forker rust-cast sur GitHub/GitLab
|
||
|
|
- Améliorer la gestion TLS (retry, reconnexion automatique)
|
||
|
|
- Ajouter logs détaillés
|
||
|
|
- Corriger les bugs TLS identifiés
|
||
|
|
- Maintenir un fork privé
|
||
|
|
|
||
|
|
**Avantages :**
|
||
|
|
- ✅ Garde l'API sync
|
||
|
|
- ✅ Contrôle total sur les correctifs
|
||
|
|
- ✅ Peut merger les améliorations de upstream
|
||
|
|
|
||
|
|
**Inconvénients :**
|
||
|
|
- ⚠️ Maintenance du fork à long terme
|
||
|
|
- ⚠️ Doit suivre les mises à jour de rustls
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Option 3 : 🔄 **Rendre rust-cast asynchrone**
|
||
|
|
|
||
|
|
**Effort :** ÉLEVÉ (27-45 heures)
|
||
|
|
|
||
|
|
**Actions :**
|
||
|
|
- Migrer vers async-rustls ou tokio-rustls
|
||
|
|
- Rendre tout le code async (MessageManager, channels, CastDevice)
|
||
|
|
- Adapter PMOMusic pour wrapper les appels async
|
||
|
|
|
||
|
|
**Avantages :**
|
||
|
|
- ✅ Architecture moderne
|
||
|
|
- ✅ Potentiellement meilleure performance pour gérer plusieurs devices
|
||
|
|
- ✅ Résout probablement les problèmes TLS via stack moderne
|
||
|
|
|
||
|
|
**Inconvénients :**
|
||
|
|
- ❌ Effort très élevé
|
||
|
|
- ❌ Risque de bugs subtils
|
||
|
|
- ❌ PMOMusic doit wrapper tous les appels avec `smol::block_on()`
|
||
|
|
- ❌ Overhead de conversion sync→async→sync
|
||
|
|
|
||
|
|
**⚠️ PARADOXE :** Rendre rust-cast async pour ensuite le wrapper en sync dans PMOMusic = **surcharge inutile**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Option 4 : ❌ **Migrer vers cast-sender (déjà async)**
|
||
|
|
|
||
|
|
**Effort :** TRÈS ÉLEVÉ (40-80 heures)
|
||
|
|
|
||
|
|
**Problèmes critiques :**
|
||
|
|
- ❌ API incomplète (pas de get_status, pas de seek)
|
||
|
|
- ❌ Nécessite architecture stateful complexe
|
||
|
|
- ❌ Documentation insuffisante (23%)
|
||
|
|
|
||
|
|
**Voir :** [cast-sender-evaluation.md](cast-sender-evaluation.md)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. Analyse détaillée : Async est-il vraiment utile ?
|
||
|
|
|
||
|
|
### 5.1 Cas d'usage PMOMusic
|
||
|
|
|
||
|
|
**Architecture actuelle :**
|
||
|
|
- 1 thread par Chromecast actif (pour le heartbeat)
|
||
|
|
- Opérations de contrôle (play, pause, volume) : sporadiques
|
||
|
|
- Pas de gestion massive de connexions simultanées
|
||
|
|
|
||
|
|
**Bénéfice de async :**
|
||
|
|
- ❌ **FAIBLE** : PMOMusic n'a pas besoin de gérer 100+ connexions simultanées
|
||
|
|
- ❌ **OVERHEAD** : Wrapping sync→async→sync ajoute de la complexité
|
||
|
|
|
||
|
|
### 5.2 Vraie cause des problèmes TLS ?
|
||
|
|
|
||
|
|
Les problèmes de "fermeture TLS prématurée" sont probablement dus à :
|
||
|
|
- Timeout réseau trop court
|
||
|
|
- Gestion d'erreur insuffisante lors des reconnexions
|
||
|
|
- Bugs spécifiques de certaines versions de rustls
|
||
|
|
|
||
|
|
**Async ne résout PAS directement ces problèmes !**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. Recommandation finale
|
||
|
|
|
||
|
|
### 🏆 **Option recommandée : Option 1 (Corriger rust-cast sync)**
|
||
|
|
|
||
|
|
**Raisons :**
|
||
|
|
|
||
|
|
1. **Effort minimal** : 2-8 heures vs 27-45h pour async
|
||
|
|
2. **Risque minimal** : Garde l'architecture validée
|
||
|
|
3. **Compatibilité** : Pas de changement dans PMOMusic
|
||
|
|
4. **Pragmatique** : Résout le problème réel (TLS) sans over-engineering
|
||
|
|
|
||
|
|
**Plan d'action concret :**
|
||
|
|
|
||
|
|
```rust
|
||
|
|
// Améliorer la fonction connect_to_device
|
||
|
|
fn connect_to_device(host: &str, port: u16) -> Result<CastDevice> {
|
||
|
|
const MAX_RETRIES: u32 = 3;
|
||
|
|
const RETRY_DELAY_MS: u64 = 1000;
|
||
|
|
|
||
|
|
for attempt in 1..=MAX_RETRIES {
|
||
|
|
match try_connect(host, port) {
|
||
|
|
Ok(device) => return Ok(device),
|
||
|
|
Err(e) if attempt < MAX_RETRIES => {
|
||
|
|
tracing::warn!(
|
||
|
|
"Connection attempt {} failed: {}. Retrying in {}ms...",
|
||
|
|
attempt, e, RETRY_DELAY_MS
|
||
|
|
);
|
||
|
|
std::thread::sleep(Duration::from_millis(RETRY_DELAY_MS));
|
||
|
|
}
|
||
|
|
Err(e) => return Err(e),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
unreachable!()
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ajouter timeout configurable pour les read operations
|
||
|
|
// Ajouter meilleure gestion d'erreur dans le heartbeat loop
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 🥈 **Alternative : Option 2 (Fork rust-cast)**
|
||
|
|
|
||
|
|
Si l'Option 1 ne suffit pas après investigation, forker permet :
|
||
|
|
- Corrections TLS plus profondes
|
||
|
|
- Ajout de fonctionnalités manquantes
|
||
|
|
- Contrôle total
|
||
|
|
|
||
|
|
**Pas besoin de rendre async !**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 🚫 **Options déconseillées :**
|
||
|
|
|
||
|
|
- ❌ **Option 3** (Async rust-cast) : Effort 5-10x supérieur pour bénéfice marginal
|
||
|
|
- ❌ **Option 4** (cast-sender) : API incomplète, effort encore plus élevé
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. Conclusion
|
||
|
|
|
||
|
|
**NON, rendre rust-cast asynchrone n'est PAS plus simple.**
|
||
|
|
|
||
|
|
**Comparaison des efforts :**
|
||
|
|
|
||
|
|
| Option | Effort (heures) | Complexité | Risque |
|
||
|
|
|--------|----------------|------------|--------|
|
||
|
|
| 1. Corriger rust-cast sync | 2-8 | Faible | Minimal |
|
||
|
|
| 2. Forker rust-cast | 8-16 | Moyenne | Faible |
|
||
|
|
| 3. **Async rust-cast** | **27-45** | **Élevée** | **Élevé** |
|
||
|
|
| 4. Migrer cast-sender | 40-80 | Très élevée | Très élevé |
|
||
|
|
|
||
|
|
**Le ratio effort/bénéfice de l'option async est défavorable :**
|
||
|
|
- **5-10x plus d'effort** que corriger le code sync
|
||
|
|
- **Bénéfice minimal** pour l'architecture actuelle de PMOMusic
|
||
|
|
- **Risques élevés** de bugs de concurrence async
|
||
|
|
|
||
|
|
**Recommandation :** Commencer par l'**Option 1**, investiguer les vrais problèmes TLS, et envisager l'**Option 2** (fork) uniquement si nécessaire. Éviter absolument l'**Option 3** (async) sauf changement radical d'architecture de PMOMusic.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Annexe : Si vous vouliez quand même faire async...
|
||
|
|
|
||
|
|
### Stack recommandée
|
||
|
|
|
||
|
|
**Pour PMOMusic (déjà avec smol) :**
|
||
|
|
```toml
|
||
|
|
[dependencies]
|
||
|
|
async-io = "2.3"
|
||
|
|
async-rustls = "0.4"
|
||
|
|
futures-lite = "2.1"
|
||
|
|
```
|
||
|
|
|
||
|
|
**Points d'attention :**
|
||
|
|
- Remplacer tous les `Mutex` par `async_lock::Mutex`
|
||
|
|
- Gérer correctement le `Unpin` trait pour les streams
|
||
|
|
- Tester intensivement la gestion des erreurs async
|
||
|
|
- Prévoir 2-3 semaines de développement + tests
|
||
|
|
|
||
|
|
**Mais encore une fois : le jeu n'en vaut pas la chandelle !**
|