mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-23 06:58:10 +02:00
Merge pull request #30 from R0m1k3/claude/project-thread-m5x8ia
Qlik : extraction coupée à 30 s par chromiumoxide (« Request timed out »)
This commit is contained in:
2 files changed
+60
-12
No files matched your search
+46
-12
@@ -67,9 +67,9 @@ async fn lancer() -> anyhow::Result<Navigateur> {
|
|||||||
.arg("ignore-certificate-errors")
|
.arg("ignore-certificate-errors")
|
||||||
.user_data_dir(&profil)
|
.user_data_dir(&profil)
|
||||||
.launch_timeout(Duration::from_secs(60))
|
.launch_timeout(Duration::from_secs(60))
|
||||||
// Une extraction complète tient dans UN `page.evaluate` (plusieurs
|
// Délai des commandes internes du gestionnaire CDP. Il ne couvre PAS
|
||||||
// minutes, parfois plus de 15) : pas de délai par commande CDP, comme
|
// `page.execute`, borné à 30 s par chromiumoxide : les évaluations
|
||||||
// Playwright. Les étapes bornées le sont explicitement (tokio::time::timeout).
|
// longues passent par un relevé périodique (cf. `PageQlik::evaluer`).
|
||||||
.request_timeout(Duration::from_secs(24 * 3600));
|
.request_timeout(Duration::from_secs(24 * 3600));
|
||||||
if let Some(chemin) = chemin_chromium() {
|
if let Some(chemin) = chemin_chromium() {
|
||||||
builder = builder.chrome_executable(chemin);
|
builder = builder.chrome_executable(chemin);
|
||||||
@@ -312,14 +312,52 @@ impl PageQlik {
|
|||||||
/// `page.evaluate(fn, args)` : `source` est le texte d'une fonction JS
|
/// `page.evaluate(fn, args)` : `source` est le texte d'une fonction JS
|
||||||
/// (éventuellement async), appelée avec `args` ; la promesse est attendue et
|
/// (éventuellement async), appelée avec `args` ; la promesse est attendue et
|
||||||
/// le résultat rendu en JSON (`JSON.stringify` côté page, donc NaN → null).
|
/// le résultat rendu en JSON (`JSON.stringify` côté page, donc NaN → null).
|
||||||
|
///
|
||||||
|
/// ⚠️ chromiumoxide abandonne toute commande CDP au bout de 30 s
|
||||||
|
/// (« Request timed out. »), quel que soit `request_timeout` : la constante
|
||||||
|
/// est codée en dur dans `CommandFuture`. Une extraction Qlik dure plusieurs
|
||||||
|
/// minutes : attendre la promesse dans un seul `Runtime.evaluate` la faisait
|
||||||
|
/// échouer à coup sûr. Le script est donc lancé sans être attendu, son
|
||||||
|
/// résultat rangé dans `window`, et relevé par des évaluations courtes.
|
||||||
pub async fn evaluer(&self, source: &str, args: &Value) -> anyhow::Result<Value> {
|
pub async fn evaluer(&self, source: &str, args: &Value) -> anyhow::Result<Value> {
|
||||||
let expression = format!(
|
static EVALUATIONS: AtomicUsize = AtomicUsize::new(0);
|
||||||
"(async () => {{ const __f = (\n{source}\n); const __r = await __f({}); return JSON.stringify(__r === undefined ? null : __r); }})()",
|
let cle = serde_json::to_string(&format!("__cf_eval_{}", EVALUATIONS.fetch_add(1, Ordering::SeqCst)))?;
|
||||||
serde_json::to_string(args)?
|
let lancement = format!(
|
||||||
|
"(() => {{ const __k = {cle}; window[__k] = {{ fini: false }}; \
|
||||||
|
(async () => {{ const __f = (\n{source}\n); const __r = await __f({args}); return JSON.stringify(__r === undefined ? null : __r); }})()\
|
||||||
|
.then((json) => {{ window[__k] = {{ fini: true, json }}; }}, \
|
||||||
|
(e) => {{ window[__k] = {{ fini: true, erreur: (e && e.stack) ? String(e.stack) : String(e) }}; }}); \
|
||||||
|
return true; }})()",
|
||||||
|
args = serde_json::to_string(args)?
|
||||||
);
|
);
|
||||||
|
self.evaluer_court(lancement).await?;
|
||||||
|
|
||||||
|
let releve = format!(
|
||||||
|
"(() => {{ const __k = {cle}; const e = window[__k]; \
|
||||||
|
if (e === undefined) return JSON.stringify({{ perdu: true }}); \
|
||||||
|
if (!e.fini) return null; delete window[__k]; return JSON.stringify(e); }})()"
|
||||||
|
);
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
||||||
|
let Some(Value::String(etat)) = self.evaluer_court(releve.clone()).await? else { continue };
|
||||||
|
let etat: Value = serde_json::from_str(&etat)?;
|
||||||
|
if etat.get("perdu").is_some() {
|
||||||
|
return Err(anyhow!("Execution context was destroyed (page rechargée pendant l'évaluation)"));
|
||||||
|
}
|
||||||
|
if let Some(erreur) = etat.get("erreur").and_then(|e| e.as_str()) {
|
||||||
|
return Err(anyhow!("{erreur}"));
|
||||||
|
}
|
||||||
|
return match etat.get("json").and_then(|j| j.as_str()) {
|
||||||
|
Some(json) => Ok(serde_json::from_str(json)?),
|
||||||
|
None => Ok(Value::Null),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Un `Runtime.evaluate` qui rend la main aussitôt (valeur JSON brute).
|
||||||
|
async fn evaluer_court(&self, expression: String) -> anyhow::Result<Option<Value>> {
|
||||||
let params = EvaluateParams::builder()
|
let params = EvaluateParams::builder()
|
||||||
.expression(expression)
|
.expression(expression)
|
||||||
.await_promise(true)
|
|
||||||
.return_by_value(true)
|
.return_by_value(true)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| anyhow!("[qlik] paramètres d'évaluation : {e}"))?;
|
.map_err(|e| anyhow!("[qlik] paramètres d'évaluation : {e}"))?;
|
||||||
@@ -328,11 +366,7 @@ impl PageQlik {
|
|||||||
let message = ex.exception.as_ref().and_then(|e| e.description.clone()).unwrap_or_else(|| ex.text.clone());
|
let message = ex.exception.as_ref().and_then(|e| e.description.clone()).unwrap_or_else(|| ex.text.clone());
|
||||||
return Err(anyhow!("{message}"));
|
return Err(anyhow!("{message}"));
|
||||||
}
|
}
|
||||||
match &res.result.result.value {
|
Ok(res.result.result.value.clone())
|
||||||
Some(Value::String(s)) => Ok(serde_json::from_str(s)?),
|
|
||||||
Some(v) => Ok(v.clone()),
|
|
||||||
None => Ok(Value::Null),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ferme la page puis le contexte (`ctx.close()`).
|
/// Ferme la page puis le contexte (`ctx.close()`).
|
||||||
|
|||||||
@@ -95,6 +95,20 @@ async fn plomberie_cdp() {
|
|||||||
let err = page.evaluer("() => { throw new Error('boum'); }", &json!(null)).await.unwrap_err().to_string();
|
let err = page.evaluer("() => { throw new Error('boum'); }", &json!(null)).await.unwrap_err().to_string();
|
||||||
assert!(err.contains("boum"), "{err}");
|
assert!(err.contains("boum"), "{err}");
|
||||||
|
|
||||||
|
// Une extraction dure plusieurs minutes : l'évaluation doit dépasser les
|
||||||
|
// 30 s après lesquelles chromiumoxide abandonne une commande CDP.
|
||||||
|
let r = page
|
||||||
|
.evaluer("async ({ ms }) => { await new Promise((r) => setTimeout(r, ms)); return { ok: true, ms }; }", &json!({ "ms": 35_000 }))
|
||||||
|
.await
|
||||||
|
.expect("évaluation de 35 s");
|
||||||
|
assert_eq!(r, json!({ "ok": true, "ms": 35000 }));
|
||||||
|
let err = page
|
||||||
|
.evaluer("async () => { await new Promise((r) => setTimeout(r, 50)); throw new Error('boum asynchrone'); }", &json!(null))
|
||||||
|
.await
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string();
|
||||||
|
assert!(err.contains("boum asynchrone"), "{err}");
|
||||||
|
|
||||||
// Script embarqué de la recherche, avec ses vrais arguments JSON : le
|
// Script embarqué de la recherche, avec ses vrais arguments JSON : le
|
||||||
// websocket Engine (wss) échoue ici faute de Qlik, et le script le dit.
|
// websocket Engine (wss) échoue ici faute de Qlik, et le script le dit.
|
||||||
let args = json!({
|
let args = json!({
|
||||||
|
|||||||
Reference in new issue
Block a user