Initial Headless Functions
This commit is contained in:
parent
c3c60efb81
commit
615c5c9edd
5 changed files with 176 additions and 7 deletions
|
|
@ -13,6 +13,7 @@ path = "Core/main.rs"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.7"
|
axum = "0.7"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||||
|
tokio-stream = "0.1"
|
||||||
tower-http = { version = "0.5", features = ["fs"] }
|
tower-http = { version = "0.5", features = ["fs"] }
|
||||||
crossterm = "0.27"
|
crossterm = "0.27"
|
||||||
ratatui = "0.26"
|
ratatui = "0.26"
|
||||||
|
|
@ -22,3 +23,5 @@ reqwest = { version = "0.12", features = ["blocking", "rustls-tls"] }
|
||||||
zip = "0.6"
|
zip = "0.6"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
pathdiff = "0.2"
|
pathdiff = "0.2"
|
||||||
|
chromiumoxide = {version = "0.7", features = ["tokio-runtime"] }
|
||||||
|
futures = "0.3"
|
||||||
|
|
@ -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<dyn std::error::Error>> {
|
pub async fn start(state: SharedState) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
{
|
{
|
||||||
let mut s = state.lock().unwrap();
|
let mut s = state.lock().unwrap();
|
||||||
|
|
@ -18,3 +23,115 @@ pub async fn start(state: SharedState) -> Result<(), Box<dyn std::error::Error>>
|
||||||
}
|
}
|
||||||
Ok(())
|
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<Self, Box<dyn std::error::Error>> {
|
||||||
|
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<PageHandle, Box<dyn std::error::Error>> {
|
||||||
|
let page = self.browser.new_page("about:blank").await?;
|
||||||
|
Ok(PageHandle { page })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PageHandle {
|
||||||
|
pub async fn goto(&self, url: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
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<bool, Box<dyn std::error::Error>> {
|
||||||
|
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<Option<String>, Box<dyn std::error::Error>> {
|
||||||
|
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<String>
|
||||||
|
Err(_) => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tippt Text in ein (vorher angeklicktes) Eingabefeld
|
||||||
|
pub async fn type_text(&self, selector: &str, text: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
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<dyn std::error::Error>> {
|
||||||
|
let elem = self.page.find_element(selector).await?;
|
||||||
|
elem.click().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn run_test(ip: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
|
@ -36,12 +36,14 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
//Chromium Check + Download
|
//Chromium Check + Download
|
||||||
chromium::start_check(&mut cfg, &exe_dir, state.clone()).await?;
|
chromium::start_check(&mut cfg, &exe_dir, state.clone()).await?;
|
||||||
|
|
||||||
|
|
||||||
// Webserver im Hintergrund starten
|
// Webserver im Hintergrund starten
|
||||||
let _server = web_server::start(state.clone(), cfg.server.port).await?;
|
let _server = web_server::start(state.clone(), cfg.server.port).await?;
|
||||||
|
|
||||||
// Headless Engine starten (Stub → Starting → Ready)
|
// Headless Engine starten (Stub → Starting → Ready)
|
||||||
headless::start(state.clone()).await?;
|
headless::start(state.clone()).await?;
|
||||||
|
|
||||||
|
|
||||||
// Main-Thread bleibt bis UI beendet wird (q/ESC)
|
// Main-Thread bleibt bis UI beendet wird (q/ESC)
|
||||||
let _ = ui_thread.join();
|
let _ = ui_thread.join();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,27 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
status.textContent = 'Bitte IP eingeben.';
|
status.textContent = 'Bitte IP eingeben.';
|
||||||
return;
|
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#"
|
pub const DEFAULT_STYLES_CSS: &str = r#"
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
body { margin: 0; font: 16px system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
body { margin: 0; font: 16px system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,19 @@ use std::{
|
||||||
net::SocketAddr,
|
net::SocketAddr,
|
||||||
path::{Path, PathBuf},
|
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 tower_http::services::ServeDir;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
mod assets;
|
mod assets;
|
||||||
use assets::{DEFAULT_INDEX_HTML, DEFAULT_APP_JS, DEFAULT_STYLES_CSS};
|
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";
|
const WEB_DIR: &str = "web";
|
||||||
|
|
||||||
|
|
@ -22,7 +28,9 @@ fn exe_dir() -> PathBuf {
|
||||||
|
|
||||||
fn rebuild_web_assets(dir: &Path) -> std::io::Result<()> {
|
fn rebuild_web_assets(dir: &Path) -> std::io::Result<()> {
|
||||||
let web = dir.join(WEB_DIR);
|
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::create_dir_all(&web)?;
|
||||||
fs::write(web.join("index.html"), DEFAULT_INDEX_HTML.trim_start())?;
|
fs::write(web.join("index.html"), DEFAULT_INDEX_HTML.trim_start())?;
|
||||||
fs::write(web.join("app.js"), DEFAULT_APP_JS.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct StartRequest {
|
||||||
|
ip: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_job(Json(req): Json<StartRequest>) -> 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
|
// Port wird jetzt von außen übergeben
|
||||||
pub async fn start(state: SharedState, port: u16)
|
pub async fn start(state: SharedState, port: u16)
|
||||||
-> Result<tokio::task::JoinHandle<()>, Box<dyn std::error::Error>>
|
-> Result<tokio::task::JoinHandle<()>, Box<dyn std::error::Error>>
|
||||||
|
|
@ -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))
|
.nest_service("/static", ServeDir::new(static_root))
|
||||||
.route("/healthz", get(|| async { "ok" }));
|
.route("/healthz", get(|| async { "ok" }));
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue