Initial Headless Functions v2
This commit is contained in:
parent
615c5c9edd
commit
a41f765211
4 changed files with 90 additions and 36 deletions
|
|
@ -13,7 +13,7 @@ path = "Core/main.rs"
|
|||
[dependencies]
|
||||
axum = "0.7"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-stream = "0.1"
|
||||
tokio-stream = { version = "0.1", features = ["sync"] }
|
||||
tower-http = { version = "0.5", features = ["fs"] }
|
||||
crossterm = "0.27"
|
||||
ratatui = "0.26"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use futures::StreamExt;
|
|||
use chromiumoxide::browser::{Browser, BrowserConfig};
|
||||
use chromiumoxide::cdp::browser_protocol::page::NavigateParams;
|
||||
use chromiumoxide::cdp::browser_protocol::input::InsertTextParams;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::state::{SharedState, HeadlessPhase};
|
||||
use crate::config::read_config;
|
||||
|
|
@ -110,27 +111,33 @@ impl PageHandle {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn run_test(ip: &str) -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("[Headless] Starte Test für {ip}");
|
||||
pub async fn run_test(ip: &str, log: broadcast::Sender<String>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let _ = log.send(format!("[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 cfg = crate::config::read_config(&exe_dir)?;
|
||||
let chrome_exe = std::path::Path::new(&cfg.chrome.path);
|
||||
|
||||
let _ = log.send(format!("[Headless] Chrome: {}", chrome_exe.display()));
|
||||
let manager = HeadlessManager::new(chrome_exe, &cfg.chrome.args).await?;
|
||||
|
||||
let page = manager.new_page().await?;
|
||||
let url_https = format!("https://{ip}/");
|
||||
let url_http = format!("http://{ip}/");
|
||||
|
||||
// Zielseite öffnen
|
||||
let url = format!("https://{ip}/");
|
||||
page.goto(&url).await?;
|
||||
let _ = log.send(format!("[Headless] goto {url_https}"));
|
||||
if page.goto(&url_https).await.is_err() {
|
||||
let _ = log.send(format!("[Headless] HTTPS fehlgeschlagen, versuche HTTP"));
|
||||
let _ = log.send(format!("[Headless] goto {url_http}"));
|
||||
page.goto(&url_http).await?;
|
||||
}
|
||||
|
||||
// kurz warten & Text auslesen
|
||||
let _ = log.send("[Headless] warte auf <h1> …".to_string());
|
||||
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");
|
||||
|
||||
match page.get_text("h1").await? {
|
||||
Some(h1) => { let _ = log.send(format!("[Headless] H1: {h1}")); }
|
||||
None => { let _ = log.send("[Headless] Keine H1 gefunden".to_string()); }
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
|
|
|||
|
|
@ -19,26 +19,39 @@ pub const DEFAULT_INDEX_HTML: &str = r#"
|
|||
<button id="btn-start">Start</button>
|
||||
<p id="status" class="status"></p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<h2>Konsole</h2>
|
||||
<pre id="console" class="console"></pre>
|
||||
</section>
|
||||
</main>
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
|
||||
|
||||
pub const DEFAULT_APP_JS: &str = r#"
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const ip = document.getElementById('ip');
|
||||
const btn = document.getElementById('btn-start');
|
||||
const status = document.getElementById('status');
|
||||
const consoleEl = document.getElementById('console');
|
||||
|
||||
// SSE: Logs anhören
|
||||
const es = new EventSource('/api/logs');
|
||||
es.onmessage = (ev) => {
|
||||
consoleEl.textContent += ev.data + '\n';
|
||||
consoleEl.scrollTop = consoleEl.scrollHeight;
|
||||
};
|
||||
es.onerror = () => {
|
||||
// optional: Reconnect-Info
|
||||
};
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
const val = (ip.value || '').trim();
|
||||
if (!val) {
|
||||
status.textContent = 'Bitte IP eingeben.';
|
||||
return;
|
||||
}
|
||||
|
||||
status.textContent = `Starte Erkennung für ${val} ...`;
|
||||
if (!val) { status.textContent = 'Bitte IP eingeben.'; return; }
|
||||
status.textContent = `Starte…`;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/start', {
|
||||
|
|
@ -46,18 +59,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ip: val })
|
||||
});
|
||||
|
||||
const text = await res.text();
|
||||
status.textContent = text || 'Anfrage gesendet.';
|
||||
status.textContent = text || 'OK';
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
status.textContent = 'Fehler beim Senden der Anfrage.';
|
||||
status.textContent = 'Fehler: ' + (err?.message || err);
|
||||
}
|
||||
});
|
||||
});
|
||||
"#;
|
||||
|
||||
|
||||
|
||||
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;
|
||||
|
|
@ -74,4 +86,8 @@ button { margin-top: 12px; padding: 10px 14px; border-radius: 8px; border: 1px s
|
|||
background: #3056ff; color: white; cursor: pointer; }
|
||||
button:hover { filter: brightness(1.05); }
|
||||
.status { margin-top: 10px; color: #9db4ff; }
|
||||
|
||||
.console { margin: 0; margin-top: 10px; padding: 10px; height: 220px; overflow: auto;
|
||||
background: #0b0f19; border: 1px solid #1f2740; border-radius: 8px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
white-space: pre-wrap; }
|
||||
"#;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
mod assets;
|
||||
|
||||
use std::{
|
||||
env, fs,
|
||||
net::SocketAddr,
|
||||
|
|
@ -7,12 +9,14 @@ use axum::{
|
|||
routing::{get, post},
|
||||
extract::Json,
|
||||
Router,
|
||||
response::Html,
|
||||
response::{Html, sse::{Sse, Event, KeepAlive}},
|
||||
};
|
||||
use tower_http::services::ServeDir;
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio_stream::wrappers::BroadcastStream;
|
||||
use futures::StreamExt;
|
||||
|
||||
mod assets;
|
||||
use assets::{DEFAULT_INDEX_HTML, DEFAULT_APP_JS, DEFAULT_STYLES_CSS};
|
||||
|
||||
use crate::{state::SharedState, headless};
|
||||
|
|
@ -38,26 +42,47 @@ fn rebuild_web_assets(dir: &Path) -> std::io::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct HttpState {
|
||||
shared: SharedState,
|
||||
log_tx: broadcast::Sender<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct StartRequest {
|
||||
ip: String,
|
||||
}
|
||||
|
||||
async fn start_job(Json(req): Json<StartRequest>) -> String {
|
||||
async fn start_job(
|
||||
axum::extract::State(st): axum::extract::State<HttpState>,
|
||||
Json(req): Json<StartRequest>,
|
||||
) {
|
||||
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();
|
||||
let tx_for_task = st.log_tx.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = headless::run_test(&ip_for_task).await {
|
||||
eprintln!("[Headless Error] {}", e);
|
||||
if let Err(e) = headless::run_test(&ip, tx_for_task.clone()).await {
|
||||
let _ = tx_for_task.send(format!("[Headless Error] {e}"));
|
||||
} else {
|
||||
let _ = tx_for_task.send(format!("[Headless] Fertig für {ip}"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
format!("Starte Setup für {}", ip)
|
||||
|
||||
|
||||
async fn stream_logs(
|
||||
axum::extract::State(st): axum::extract::State<HttpState>
|
||||
) -> Sse<impl futures::Stream<Item = Result<Event, std::convert::Infallible>>> {
|
||||
let rx = st.log_tx.subscribe();
|
||||
let stream = BroadcastStream::new(rx)
|
||||
.filter_map(|res| async move { res.ok() })
|
||||
.map(|msg| Ok(Event::default().data(msg)));
|
||||
Sse::new(stream).keep_alive(
|
||||
KeepAlive::new()
|
||||
.interval(std::time::Duration::from_secs(10))
|
||||
.text("💓"),
|
||||
)
|
||||
}
|
||||
|
||||
// Port wird jetzt von außen übergeben
|
||||
|
|
@ -67,6 +92,10 @@ pub async fn start(state: SharedState, port: u16)
|
|||
let dir = exe_dir();
|
||||
rebuild_web_assets(&dir)?;
|
||||
|
||||
// Broadcast-Kanal für Web-Logs
|
||||
let (log_tx, _rx) = broadcast::channel::<String>(200);
|
||||
let http_state = HttpState { shared: state.clone(), log_tx: log_tx.clone() };
|
||||
|
||||
let web_root = dir.join(WEB_DIR);
|
||||
let static_root = web_root.clone();
|
||||
let index_path = web_root.join("index.html");
|
||||
|
|
@ -83,9 +112,11 @@ pub async fn start(state: SharedState, port: u16)
|
|||
}
|
||||
}
|
||||
}))
|
||||
.route("/api/start", post(start_job)) // 👈 Neue Route
|
||||
.route("/api/start", post(start_job)) // Start-Button
|
||||
.route("/api/logs", get(stream_logs)) // SSE-Konsole
|
||||
.nest_service("/static", ServeDir::new(static_root))
|
||||
.route("/healthz", get(|| async { "ok" }));
|
||||
.route("/healthz", get(|| async { "ok" }))
|
||||
.with_state(http_state);
|
||||
|
||||
{
|
||||
let mut s = state.lock().unwrap();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue