From 615c5c9edd3ab9f7bff2a55997d63073dbf39e61 Mon Sep 17 00:00:00 2001 From: Joey Pillunat Date: Fri, 24 Oct 2025 13:01:16 +0200 Subject: [PATCH] Initial Headless Functions --- Cargo.toml | 3 + Core/headless/mod.rs | 123 +++++++++++++++++++++++++++++++++++++- Core/main.rs | 2 + Core/web_server/assets.rs | 18 +++++- Core/web_server/mod.rs | 37 +++++++++++- 5 files changed, 176 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cc1c179..59f0807 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ path = "Core/main.rs" [dependencies] axum = "0.7" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } +tokio-stream = "0.1" tower-http = { version = "0.5", features = ["fs"] } crossterm = "0.27" ratatui = "0.26" @@ -22,3 +23,5 @@ reqwest = { version = "0.12", features = ["blocking", "rustls-tls"] } zip = "0.6" sha2 = "0.10" pathdiff = "0.2" +chromiumoxide = {version = "0.7", features = ["tokio-runtime"] } +futures = "0.3" \ No newline at end of file diff --git a/Core/headless/mod.rs b/Core/headless/mod.rs index f409fff..75091c4 100644 --- a/Core/headless/mod.rs +++ b/Core/headless/mod.rs @@ -1,7 +1,12 @@ -use crate::state::{SharedState, HeadlessPhase}; +use std::{path::Path, time::Duration}; +use futures::StreamExt; +use chromiumoxide::browser::{Browser, BrowserConfig}; +use chromiumoxide::cdp::browser_protocol::page::NavigateParams; +use chromiumoxide::cdp::browser_protocol::input::InsertTextParams; + +use crate::state::{SharedState, HeadlessPhase}; +use crate::config::read_config; -/// Startet die Headless Engine (Stub). -/// Aktuell nur Statuswechsel; chromiumoxide folgt im nächsten Schritt. pub async fn start(state: SharedState) -> Result<(), Box> { { let mut s = state.lock().unwrap(); @@ -18,3 +23,115 @@ pub async fn start(state: SharedState) -> Result<(), Box> } Ok(()) } + +pub struct HeadlessManager { + browser: chromiumoxide::Browser, + _handler_task: tokio::task::JoinHandle<()>, +} + +pub struct PageHandle { + page: chromiumoxide::Page, +} + +impl HeadlessManager { + /// Startet Chromium (für Tests ohne Headless: wir filtern --headless raus) + pub async fn new(chrome_exe: &Path, extra_args: &[String]) -> Result> { + let mut cfg = BrowserConfig::builder(); + cfg = cfg.chrome_executable(chrome_exe); // v0.7 API + + for a in extra_args { + // fürs Testen: Headless-Flag unterdrücken, damit ein Fenster sichtbar ist + if a.starts_with("--headless") { continue; } + cfg = cfg.arg(a); + } + + let (browser, mut handler) = Browser::launch(cfg.build()?).await?; + + // Event-Handler-Loop muss laufen + let handler_task = tokio::spawn(async move { + while let Some(_evt) = handler.next().await { + // hier ggf. Logging + } + }); + + Ok(Self { browser, _handler_task: handler_task }) + } + + pub async fn new_page(&self) -> Result> { + let page = self.browser.new_page("about:blank").await?; + Ok(PageHandle { page }) + } +} + +impl PageHandle { + pub async fn goto(&self, url: &str) -> Result<(), Box> { + self.page.goto(NavigateParams::builder().url(url).build()?).await?; + // manche UIs brauchen einen Moment + self.page.wait_for_navigation().await.ok(); + Ok(()) + } + + /// Wartet auf einen CSS-Selector, true wenn gefunden, false bei Timeout + pub async fn wait_for_selector(&self, selector: &str, timeout_ms: u64) -> Result> { + match tokio::time::timeout( + Duration::from_millis(timeout_ms), + self.page.find_element(selector), + ).await { + Ok(Ok(_elem)) => Ok(true), + _ => Ok(false), + } + } + + /// Liest innerText eines Elements, None falls nicht gefunden/Fehler + pub async fn get_text(&self, selector: &str) -> Result, Box> { + let elem = match self.page.find_element(selector).await { + Ok(e) => e, + Err(_) => return Ok(None), + }; + match elem.inner_text().await { + Ok(opt) => Ok(opt), // opt ist bereits Option + Err(_) => Ok(None), + } + } + + /// Tippt Text in ein (vorher angeklicktes) Eingabefeld + pub async fn type_text(&self, selector: &str, text: &str) -> Result<(), Box> { + let elem = self.page.find_element(selector).await?; + elem.click().await?; + // Fokus ist im Feld – jetzt Text am Stück einfügen + self.page.execute(InsertTextParams::new(text.to_string())).await?; + Ok(()) + } + + pub async fn click(&self, selector: &str) -> Result<(), Box> { + let elem = self.page.find_element(selector).await?; + elem.click().await?; + Ok(()) + } +} + +pub async fn run_test(ip: &str) -> Result<(), Box> { + println!("[Headless] Starte Test für {ip}"); + + // config lesen, um Pfad zu Chrome zu bekommen + let exe_dir = std::env::current_exe()?.parent().unwrap().to_path_buf(); + let cfg = read_config(&exe_dir)?; + let chrome_exe = Path::new(&cfg.chrome.path); + + let manager = HeadlessManager::new(chrome_exe, &cfg.chrome.args).await?; + let page = manager.new_page().await?; + + // Zielseite öffnen + let url = format!("https://{ip}/"); + page.goto(&url).await?; + + // kurz warten & Text auslesen + let _ = page.wait_for_selector("h1", 5000).await?; + if let Some(h1) = page.get_text("h1").await? { + println!("[Headless] H1: {}", h1); + } else { + println!("[Headless] Keine H1 gefunden"); + } + + Ok(()) +} \ No newline at end of file diff --git a/Core/main.rs b/Core/main.rs index 1e285ae..386a317 100644 --- a/Core/main.rs +++ b/Core/main.rs @@ -36,12 +36,14 @@ async fn main() -> Result<(), Box> { //Chromium Check + Download chromium::start_check(&mut cfg, &exe_dir, state.clone()).await?; + // Webserver im Hintergrund starten let _server = web_server::start(state.clone(), cfg.server.port).await?; // Headless Engine starten (Stub → Starting → Ready) headless::start(state.clone()).await?; + // Main-Thread bleibt bis UI beendet wird (q/ESC) let _ = ui_thread.join(); diff --git a/Core/web_server/assets.rs b/Core/web_server/assets.rs index 9c8762b..9e8c349 100644 --- a/Core/web_server/assets.rs +++ b/Core/web_server/assets.rs @@ -37,11 +37,27 @@ document.addEventListener('DOMContentLoaded', () => { status.textContent = 'Bitte IP eingeben.'; return; } - status.textContent = `Erkenne Gerät bei ${val} … (Demo)`; + + status.textContent = `Starte Erkennung für ${val} ...`; + + try { + const res = await fetch('/api/start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ip: val }) + }); + + const text = await res.text(); + status.textContent = text || 'Anfrage gesendet.'; + } catch (err) { + console.error(err); + status.textContent = 'Fehler beim Senden der Anfrage.'; + } }); }); "#; + pub const DEFAULT_STYLES_CSS: &str = r#" * { box-sizing: border-box; } body { margin: 0; font: 16px system-ui, -apple-system, Segoe UI, Roboto, sans-serif; diff --git a/Core/web_server/mod.rs b/Core/web_server/mod.rs index e28573a..d1798b3 100644 --- a/Core/web_server/mod.rs +++ b/Core/web_server/mod.rs @@ -3,13 +3,19 @@ use std::{ net::SocketAddr, path::{Path, PathBuf}, }; -use axum::{routing::get, Router, response::Html}; +use axum::{ + routing::{get, post}, + extract::Json, + Router, + response::Html, +}; use tower_http::services::ServeDir; +use serde::Deserialize; mod assets; use assets::{DEFAULT_INDEX_HTML, DEFAULT_APP_JS, DEFAULT_STYLES_CSS}; -use crate::state::SharedState; // ⬅️ nur noch state importieren +use crate::{state::SharedState, headless}; const WEB_DIR: &str = "web"; @@ -22,7 +28,9 @@ fn exe_dir() -> PathBuf { fn rebuild_web_assets(dir: &Path) -> std::io::Result<()> { let web = dir.join(WEB_DIR); - if web.exists() { fs::remove_dir_all(&web)?; } + if web.exists() { + fs::remove_dir_all(&web)?; + } fs::create_dir_all(&web)?; fs::write(web.join("index.html"), DEFAULT_INDEX_HTML.trim_start())?; fs::write(web.join("app.js"), DEFAULT_APP_JS.trim_start())?; @@ -30,6 +38,28 @@ fn rebuild_web_assets(dir: &Path) -> std::io::Result<()> { Ok(()) } +#[derive(Deserialize)] +struct StartRequest { + ip: String, +} + +async fn start_job(Json(req): Json) -> String { + let ip = req.ip.trim().to_string(); + println!("[WebServer] Starte Headless-Test für {}", ip); + + // Klon für den Task + let ip_for_task = ip.clone(); + + tokio::spawn(async move { + if let Err(e) = headless::run_test(&ip_for_task).await { + eprintln!("[Headless Error] {}", e); + } + }); + + format!("Starte Setup für {}", ip) + +} + // Port wird jetzt von außen übergeben pub async fn start(state: SharedState, port: u16) -> Result, Box> @@ -53,6 +83,7 @@ pub async fn start(state: SharedState, port: u16) } } })) + .route("/api/start", post(start_job)) // 👈 Neue Route .nest_service("/static", ServeDir::new(static_root)) .route("/healthz", get(|| async { "ok" }));