Added Chrome Job Profiling and Handling
This commit is contained in:
parent
5b99621044
commit
e4e454960e
5 changed files with 90 additions and 40 deletions
|
|
@ -4,6 +4,7 @@ use crate::state::{SharedState, ChromiumPhase};
|
||||||
|
|
||||||
mod download;
|
mod download;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
pub mod profiles;
|
||||||
|
|
||||||
pub const CACHE_DIR: &str = "runtime/chrome-cache";
|
pub const CACHE_DIR: &str = "runtime/chrome-cache";
|
||||||
|
|
||||||
|
|
|
||||||
21
Core/chromium/profiles.rs
Normal file
21
Core/chromium/profiles.rs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
use std::{fs, io, path::{Path, PathBuf}};
|
||||||
|
|
||||||
|
pub fn job_profiles_root(exe_dir: &Path) -> PathBuf {
|
||||||
|
exe_dir.join("runtime").join("chrome-profiles")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_job_profile(exe_dir: &Path, job_id: &str) -> io::Result<PathBuf> {
|
||||||
|
let root = job_profiles_root(exe_dir);
|
||||||
|
fs::create_dir_all(&root)?;
|
||||||
|
let p = root.join(format!("job-{}", job_id));
|
||||||
|
fs::create_dir_all(&p)?;
|
||||||
|
Ok(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Baut die Job-spezifischen Chrome-Args: Basis + user-data-dir + auto RDP-Port
|
||||||
|
pub fn build_job_args(base: &[String], profile_dir: &Path) -> Vec<String> {
|
||||||
|
let mut args = base.to_vec();
|
||||||
|
args.push(format!("--user-data-dir={}", profile_dir.display()));
|
||||||
|
args.push("--remote-debugging-port=0".to_string());
|
||||||
|
args
|
||||||
|
}
|
||||||
|
|
@ -62,6 +62,7 @@ impl HeadlessManager {
|
||||||
let page = self.browser.new_page("about:blank").await?;
|
let page = self.browser.new_page("about:blank").await?;
|
||||||
Ok(PageHandle { page })
|
Ok(PageHandle { page })
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PageHandle {
|
impl PageHandle {
|
||||||
|
|
@ -198,48 +199,60 @@ impl PageHandle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_test(ip: &str, log: broadcast::Sender<String>) -> Result<(), Box<dyn std::error::Error>> {
|
/// Startet einen Setup-Job mit eigenem Chrome-Profilverzeichnis.
|
||||||
let _ = log.send(format!("[Headless] Starte Test für {ip}"));
|
/// `profile_dir` zeigt auf z. B. runtime/chrome-profiles/job-<id>
|
||||||
|
pub async fn run_test(
|
||||||
|
ip: &str,
|
||||||
|
log: broadcast::Sender<String>,
|
||||||
|
profile_dir: &Path,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
log.send(format!("[Headless] Starte Test für {ip}")).ok();
|
||||||
|
|
||||||
|
// Config + Chrome-Binärdatei laden
|
||||||
let exe_dir = std::env::current_exe()?.parent().unwrap().to_path_buf();
|
let exe_dir = std::env::current_exe()?.parent().unwrap().to_path_buf();
|
||||||
let cfg = crate::config::read_config(&exe_dir)?;
|
let cfg = crate::config::read_config(&exe_dir)?;
|
||||||
let chrome_exe = std::path::Path::new(&cfg.chrome.path);
|
let chrome_exe = std::path::Path::new(&cfg.chrome.path);
|
||||||
|
log.send(format!("[Headless] Chrome: {}", chrome_exe.display())).ok();
|
||||||
|
|
||||||
let _ = log.send(format!("[Headless] Chrome: {}", chrome_exe.display()));
|
// Basis-Args aus Config + Job-spezifische Isolation
|
||||||
let manager = HeadlessManager::new(chrome_exe, &cfg.chrome.args).await?;
|
let mut args = cfg.chrome.args.clone();
|
||||||
|
args.push(format!("--user-data-dir={}", profile_dir.display()));
|
||||||
|
args.push("--remote-debugging-port=0".to_string());
|
||||||
|
|
||||||
|
// Browser starten (mit Job-Args) und neue Seite öffnen
|
||||||
|
let manager = crate::headless::HeadlessManager::new(chrome_exe, &args).await?;
|
||||||
let page = manager.new_page().await?;
|
let page = manager.new_page().await?;
|
||||||
|
|
||||||
page.goto("https://192.168.178.240/hp/device/SignIn/Index").await?;
|
// --- Login ---
|
||||||
|
page.goto(&format!("https://{}/hp/device/SignIn/Index", ip)).await?;
|
||||||
page.wait_for_selector("#PasswordTextBox", 5000).await?;
|
page.wait_for_selector("#PasswordTextBox", 5000).await?;
|
||||||
let _ = log.send("PasswordTextBox gefunden".to_string());
|
log.send("PasswordTextBox gefunden".to_string()).ok();
|
||||||
|
|
||||||
page.type_text("#PasswordTextBox", "Pa55w.rt").await?;
|
page.type_text("#PasswordTextBox", "Pa55w.rt").await?;
|
||||||
let _ = log.send("Passwort erfolgreich eingetragen".to_string());
|
log.send("Passwort erfolgreich eingetragen".to_string()).ok();
|
||||||
|
|
||||||
page.wait_for_selector("#signInOk", 5000).await?;
|
page.wait_for_selector("#signInOk", 5000).await?;
|
||||||
let _ = log.send("Login-Button gefunden, klicke …".to_string());
|
log.send("Login-Button gefunden, klicke …".to_string()).ok();
|
||||||
|
|
||||||
page.click_and_wait_navigation("#signInOk", 15_000).await?;
|
page.click_and_wait_navigation("#signInOk", 15_000).await?;
|
||||||
let _ = log.send("Login erfolgreich, warte auf Menü".to_string());
|
log.send("Login erfolgreich, warte auf Menü".to_string()).ok();
|
||||||
page.wait_for_dom_quiet(8_000).await?;
|
page.wait_for_dom_quiet(8_000).await?;
|
||||||
|
|
||||||
|
// --- Sleep Schedule setzen ---
|
||||||
|
page.goto(&format!("https://{}/hp/device/SleepSchedule/Index", ip)).await?;
|
||||||
|
|
||||||
page.goto("https://192.168.178.240/hp/device/SleepSchedule/Index").await?;
|
|
||||||
page.wait_for_selector("#SleepDelayTimeLimit", 5000).await?;
|
page.wait_for_selector("#SleepDelayTimeLimit", 5000).await?;
|
||||||
let _ = log.send("Eingabefeld 'SleepDelayTimeLimit' gefunden".to_string());
|
log.send("Eingabefeld 'SleepDelayTimeLimit' gefunden".to_string()).ok();
|
||||||
|
|
||||||
page.set_input_value_by_id("SleepDelayTimeLimit", "15").await?;
|
page.set_input_value_by_id("SleepDelayTimeLimit", "15").await?;
|
||||||
let _ = log.send("SleepDelayTimeLimit = 15 eingetragen".to_string());
|
log.send("SleepDelayTimeLimit = 15 eingetragen".to_string()).ok();
|
||||||
|
|
||||||
page.wait_for_selector("#FormButtonSubmit", 5000).await?;
|
page.wait_for_selector("#FormButtonSubmit", 5000).await?;
|
||||||
let _ = log.send("Submit-Button gefunden, sende Formular …".to_string());
|
log.send("Submit-Button gefunden, sende Formular …".to_string()).ok();
|
||||||
|
|
||||||
page.click_and_wait_navigation("#FormButtonSubmit", 15_000).await?;
|
page.click_and_wait_navigation("#FormButtonSubmit", 15_000).await?;
|
||||||
let _ = log.send("Formular erfolgreich gesendet ✅".to_string());
|
log.send("Formular erfolgreich gesendet ✅".to_string()).ok();
|
||||||
page.wait_for_dom_quiet(8_000).await?;
|
page.wait_for_dom_quiet(8_000).await?;
|
||||||
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ use axum::{
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
|
use axum::Json as AxumJson;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::{sync::{broadcast, Mutex}};
|
use tokio::{sync::{broadcast, Mutex}};
|
||||||
|
|
@ -22,9 +23,12 @@ use tokio_stream::wrappers::BroadcastStream;
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use assets::{DEFAULT_APP_JS, DEFAULT_INDEX_HTML, DEFAULT_STYLES_CSS};
|
use crate::chromium::profiles;
|
||||||
use crate::{headless, state::SharedState};
|
use crate::{headless, state::SharedState};
|
||||||
|
|
||||||
|
use assets::{DEFAULT_APP_JS, DEFAULT_INDEX_HTML, DEFAULT_STYLES_CSS};
|
||||||
|
|
||||||
|
|
||||||
const WEB_DIR: &str = "web";
|
const WEB_DIR: &str = "web";
|
||||||
|
|
||||||
fn exe_dir() -> PathBuf {
|
fn exe_dir() -> PathBuf {
|
||||||
|
|
@ -65,41 +69,57 @@ struct StartResponse {
|
||||||
job_id: String,
|
job_id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct LogsQuery {
|
||||||
|
job: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async fn start_job(
|
async fn start_job(
|
||||||
axum::extract::State(st): axum::extract::State<HttpState>,
|
axum::extract::State(st): axum::extract::State<HttpState>,
|
||||||
Json(req): Json<StartRequest>,
|
Json(req): Json<StartRequest>,
|
||||||
) -> axum::Json<StartResponse> {
|
) -> AxumJson<StartResponse> {
|
||||||
use axum::Json as AxumJson;
|
|
||||||
|
|
||||||
let ip = req.ip.trim().to_string();
|
let ip = req.ip.trim().to_string();
|
||||||
|
|
||||||
// neuen Job anlegen
|
// Neuen Job anlegen
|
||||||
let job_id = uuid::Uuid::new_v4().to_string();
|
let job_id = uuid::Uuid::new_v4().to_string();
|
||||||
let (tx, _rx) = tokio::sync::broadcast::channel::<String>(200);
|
let (tx, _rx) = tokio::sync::broadcast::channel::<String>(200);
|
||||||
|
|
||||||
{
|
{
|
||||||
// in Registry eintragen
|
|
||||||
let mut map = st.jobs.lock().await;
|
let mut map = st.jobs.lock().await;
|
||||||
map.insert(job_id.clone(), tx.clone());
|
map.insert(job_id.clone(), tx.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Klone für den Task
|
// Profilordner erzeugen (unter <exe>/runtime/chrome-profiles/job-<id>)
|
||||||
|
let exe_dir = exe_dir(); // du hast die Funktion schon
|
||||||
|
let profile_dir = profiles::create_job_profile(&exe_dir, &job_id)
|
||||||
|
.expect("profile dir create failed");
|
||||||
|
|
||||||
let job_id_for_task = job_id.clone();
|
let job_id_for_task = job_id.clone();
|
||||||
let jobs_map = st.jobs.clone();
|
let jobs_map = st.jobs.clone();
|
||||||
|
|
||||||
|
// Optional: kleiner Delay, damit der Client Zeit hat den SSE-Stream zu verbinden
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
sleep(Duration::from_millis(500)).await;
|
use tokio::time::{sleep, Duration};
|
||||||
tx.send(format!("[Job {job_id_for_task}] Starte Setup für {ip}")).ok();
|
sleep(Duration::from_millis(250)).await;
|
||||||
|
|
||||||
// WICHTIG: Ergebnis NICHT in Variable halten, sondern sofort matchen,
|
tx.send(format!("[Job {job_id_for_task}] starte Setup für {ip}")).ok();
|
||||||
// damit kein non-Send Error über das spätere .await (Mutex) "lebt".
|
|
||||||
if let Err(e) = crate::headless::run_test(&ip, tx.clone()).await {
|
// --- Wichtig: Ergebnis sofort in String-Nachricht umwandeln,
|
||||||
tx.send(format!("[Job {job_id_for_task}] ❌ Fehler: {e}")).ok();
|
// damit kein `Box<dyn Error>` über ein späteres .await "lebt".
|
||||||
} else {
|
let result_msg = match crate::headless::run_test(&ip, tx.clone(), &profile_dir).await {
|
||||||
tx.send(format!("[Job {job_id_for_task}] ✅ Fertig")).ok();
|
Ok(_) => format!("[Job {job_id_for_task}] ✅ fertig"),
|
||||||
|
Err(e) => format!("[Job {job_id_for_task}] ❌ fehler: {e}"),
|
||||||
|
};
|
||||||
|
tx.send(result_msg).ok();
|
||||||
|
// --- ab hier existiert kein non-Send Error mehr
|
||||||
|
|
||||||
|
// Cleanup: Profilordner löschen (optional)
|
||||||
|
sleep(Duration::from_millis(250)).await;
|
||||||
|
if let Err(e) = std::fs::remove_dir_all(&profile_dir) {
|
||||||
|
tx.send(format!("[Job {job_id_for_task}] ⚠ cleanup fehlgeschlagen: {e}")).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erst NACH dem match den Mutex awaiten -> der Error ist schon gedroppt
|
// Job deregistrieren
|
||||||
let mut m = jobs_map.lock().await;
|
let mut m = jobs_map.lock().await;
|
||||||
m.remove(&job_id_for_task);
|
m.remove(&job_id_for_task);
|
||||||
});
|
});
|
||||||
|
|
@ -107,11 +127,6 @@ async fn start_job(
|
||||||
AxumJson(StartResponse { job_id })
|
AxumJson(StartResponse { job_id })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct LogsQuery {
|
|
||||||
job: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn stream_logs(
|
async fn stream_logs(
|
||||||
AxumState(st): AxumState<HttpState>,
|
AxumState(st): AxumState<HttpState>,
|
||||||
Query(q): Query<LogsQuery>,
|
Query(q): Query<LogsQuery>,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue