Enregistrements : droits du dossier compatibles Plex (PUID/PGID), code 243 expliqué (#34)

Sous Unraid, l'entrypoint faisait chmod -R 770 sur le dossier
d'enregistrement : Plex (nobody:users) ne lisait plus les vidéos. Rendre
la lecture à Plex retirait l'écriture au conteneur, et le match suivant
a échoué sur « Erreur FFmpeg code 243 » (EACCES), sans log puisque le
.log ne pouvait pas non plus être créé.

- entrypoint : PUID/PGID (Unraid 99/100) et UMASK (002) ; les droits du
  dossier sont ajoutés, plus jamais retirés ; avertissement au démarrage
  si le dossier reste non inscriptible.
- Serveur : vérification d'écriture avant de lancer FFmpeg, motif clair.
- Codes de sortie FFmpeg courants traduits (243, 8, 228…).
- Dialogue des logs : affiche le message d'erreur du serveur.


Claude-Session: https://claude.ai/code/session_018RD1okqQzNYjujGirBHXpu

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
LogiFlowandClaude authored and GitHub committed 2026-10-23 12:09:07 +02:00
1 parent 0f5b087eb7
commit 2f8f09e12f
8 files changed
+124 -11

No files matched your search

+39
View File
@@ -101,6 +101,28 @@ pub fn decide_post_exit_action(
}
}
/// Cause lisible d'un code de sortie FFmpeg, pour le motif d'échec affiché.
///
/// FFmpeg ≥ 7 sort avec son code d'erreur interne (`AVERROR`, négatif)
/// tronqué à un octet : 243 = −13 = `EACCES`. Seules les causes que
/// l'utilisateur peut corriger sont nommées.
pub fn describe_ffmpeg_exit(exit_code: i32) -> Option<&'static str> {
match exit_code {
// −13 EACCES
243 => Some("permission refusée sur le dossier d'enregistrement"),
// −2 ENOENT
254 => Some("fichier ou dossier introuvable"),
// −28 ENOSPC
228 => Some("disque plein"),
// −30 EROFS
226 => Some("dossier d'enregistrement en lecture seule"),
// Erreurs HTTP du fournisseur (AVERROR_HTTP_*)
8 => Some("flux refusé par le fournisseur (erreur HTTP)"),
c if c < 0 => Some("FFmpeg interrompu par un signal"),
_ => None,
}
}
/// Durée de capture (`-t`) : le temps restant jusqu'à la fin programmée,
/// et non la durée théorique du programme — un démarrage tardif (ou une
/// relance après coupure) ne doit pas décaler la fin de l'enregistrement.
@@ -216,6 +238,23 @@ mod tests {
);
}
#[test]
fn causes_de_sortie_ffmpeg() {
assert_eq!(
describe_ffmpeg_exit(243),
Some("permission refusée sur le dossier d'enregistrement")
);
assert_eq!(
describe_ffmpeg_exit(8),
Some("flux refusé par le fournisseur (erreur HTTP)")
);
assert_eq!(
describe_ffmpeg_exit(-9),
Some("FFmpeg interrompu par un signal")
);
assert_eq!(describe_ffmpeg_exit(1), None);
}
#[test]
fn duree_de_capture() {
let n = now();
+40 -3
View File
@@ -8,7 +8,7 @@ use crate::db::AppDatabase;
use crate::models::Recording;
use crate::services::recording_decision::{
OrphanAction, PostExitAction, RecordingAction, capture_duration, decide_orphan_action,
decide_post_exit_action, decide_recording_action, ffmpeg_retry_delay,
decide_post_exit_action, decide_recording_action, describe_ffmpeg_exit, ffmpeg_retry_delay,
};
use crate::util::b64;
use crate::util::log_redactor::redact_url;
@@ -631,6 +631,27 @@ impl RecordingScheduler {
}
}
// Un dossier non inscriptible (droits modifiés côté hôte) ferait
// échouer chaque relance de FFmpeg pendant toute la fenêtre, sans
// même de log : on le signale tout de suite.
if let Err(e) = check_writable(&dir) {
println!(
"[RecordingScheduler] Dossier {} non accessible en écriture ({e}) : \"{}\" refusé",
dir.display(),
recording.title
);
self.db.update_recording_status(
&recording.id,
"failed",
None,
Some(&format!(
"Dossier d'enregistrement non accessible en écriture ({e}). Vérifier les droits du dossier hôte (PUID/PGID)."
)),
false,
);
return Ok(());
}
let file_path = dir
.join(file_name_for(&recording))
.to_string_lossy()
@@ -849,8 +870,11 @@ impl RecordingScheduler {
state.consecutive_failures
};
let delay = ffmpeg_retry_delay(failures);
let cause = describe_ffmpeg_exit(exit_code)
.map(|c| format!(", {c}"))
.unwrap_or_default();
let message = format!(
"FFmpeg s'est arrêté (code {exit_code}) après {}s, avant la fin prévue — relance dans {}s (échec consécutif n°{failures})",
"FFmpeg s'est arrêté (code {exit_code}{cause}) après {}s, avant la fin prévue — relance dans {}s (échec consécutif n°{failures})",
lasted.as_secs(),
delay.as_secs(),
);
@@ -893,7 +917,12 @@ impl RecordingScheduler {
self.finalize(
&active,
"failed",
Some(&format!("Erreur FFmpeg code {exit_code}. Voir logs.")),
Some(&match describe_ffmpeg_exit(exit_code) {
Some(cause) => {
format!("Erreur FFmpeg code {exit_code} : {cause}. Voir logs.")
}
None => format!("Erreur FFmpeg code {exit_code}. Voir logs."),
}),
)
.await;
}
@@ -1234,6 +1263,14 @@ fn file_has_data(path: Option<&str>) -> bool {
.is_some_and(|m| m.is_file() && m.len() > 0)
}
/// Vérifie qu'un fichier peut être créé dans [dir] (fichier sonde supprimé).
fn check_writable(dir: &Path) -> std::io::Result<()> {
let probe = dir.join(format!(".xtremflow-write-test-{}", std::process::id()));
std::fs::File::create(&probe)?;
let _ = std::fs::remove_file(&probe);
Ok(())
}
fn delete_quietly(path: &str) {
let _ = std::fs::remove_file(path);
}