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]
|
[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"
|
tokio-stream = { version = "0.1", features = ["sync"] }
|
||||||
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"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ use futures::StreamExt;
|
||||||
use chromiumoxide::browser::{Browser, BrowserConfig};
|
use chromiumoxide::browser::{Browser, BrowserConfig};
|
||||||
use chromiumoxide::cdp::browser_protocol::page::NavigateParams;
|
use chromiumoxide::cdp::browser_protocol::page::NavigateParams;
|
||||||
use chromiumoxide::cdp::browser_protocol::input::InsertTextParams;
|
use chromiumoxide::cdp::browser_protocol::input::InsertTextParams;
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
|
||||||
use crate::state::{SharedState, HeadlessPhase};
|
use crate::state::{SharedState, HeadlessPhase};
|
||||||
use crate::config::read_config;
|
use crate::config::read_config;
|
||||||
|
|
@ -110,27 +111,33 @@ impl PageHandle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_test(ip: &str) -> Result<(), Box<dyn std::error::Error>> {
|
pub async fn run_test(ip: &str, log: broadcast::Sender<String>) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
println!("[Headless] Starte Test für {ip}");
|
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 exe_dir = std::env::current_exe()?.parent().unwrap().to_path_buf();
|
||||||
let cfg = read_config(&exe_dir)?;
|
let cfg = crate::config::read_config(&exe_dir)?;
|
||||||
let chrome_exe = Path::new(&cfg.chrome.path);
|
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 manager = HeadlessManager::new(chrome_exe, &cfg.chrome.args).await?;
|
||||||
|
|
||||||
let page = manager.new_page().await?;
|
let page = manager.new_page().await?;
|
||||||
|
let url_https = format!("https://{ip}/");
|
||||||
|
let url_http = format!("http://{ip}/");
|
||||||
|
|
||||||
// Zielseite öffnen
|
let _ = log.send(format!("[Headless] goto {url_https}"));
|
||||||
let url = format!("https://{ip}/");
|
if page.goto(&url_https).await.is_err() {
|
||||||
page.goto(&url).await?;
|
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?;
|
let _ = page.wait_for_selector("h1", 5000).await?;
|
||||||
if let Some(h1) = page.get_text("h1").await? {
|
|
||||||
println!("[Headless] H1: {}", h1);
|
match page.get_text("h1").await? {
|
||||||
} else {
|
Some(h1) => { let _ = log.send(format!("[Headless] H1: {h1}")); }
|
||||||
println!("[Headless] Keine H1 gefunden");
|
None => { let _ = log.send("[Headless] Keine H1 gefunden".to_string()); }
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -19,26 +19,39 @@ pub const DEFAULT_INDEX_HTML: &str = r#"
|
||||||
<button id="btn-start">Start</button>
|
<button id="btn-start">Start</button>
|
||||||
<p id="status" class="status"></p>
|
<p id="status" class="status"></p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<h2>Konsole</h2>
|
||||||
|
<pre id="console" class="console"></pre>
|
||||||
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<script src="/static/app.js"></script>
|
<script src="/static/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
|
||||||
pub const DEFAULT_APP_JS: &str = r#"
|
pub const DEFAULT_APP_JS: &str = r#"
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const ip = document.getElementById('ip');
|
const ip = document.getElementById('ip');
|
||||||
const btn = document.getElementById('btn-start');
|
const btn = document.getElementById('btn-start');
|
||||||
const status = document.getElementById('status');
|
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 () => {
|
btn.addEventListener('click', async () => {
|
||||||
const val = (ip.value || '').trim();
|
const val = (ip.value || '').trim();
|
||||||
if (!val) {
|
if (!val) { status.textContent = 'Bitte IP eingeben.'; return; }
|
||||||
status.textContent = 'Bitte IP eingeben.';
|
status.textContent = `Starte…`;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
status.textContent = `Starte Erkennung für ${val} ...`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/start', {
|
const res = await fetch('/api/start', {
|
||||||
|
|
@ -46,18 +59,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ ip: val })
|
body: JSON.stringify({ ip: val })
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
status.textContent = text || 'Anfrage gesendet.';
|
status.textContent = text || 'OK';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
status.textContent = 'Fehler: ' + (err?.message || 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;
|
||||||
|
|
@ -74,4 +86,8 @@ button { margin-top: 12px; padding: 10px 14px; border-radius: 8px; border: 1px s
|
||||||
background: #3056ff; color: white; cursor: pointer; }
|
background: #3056ff; color: white; cursor: pointer; }
|
||||||
button:hover { filter: brightness(1.05); }
|
button:hover { filter: brightness(1.05); }
|
||||||
.status { margin-top: 10px; color: #9db4ff; }
|
.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::{
|
use std::{
|
||||||
env, fs,
|
env, fs,
|
||||||
net::SocketAddr,
|
net::SocketAddr,
|
||||||
|
|
@ -7,12 +9,14 @@ use axum::{
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
extract::Json,
|
extract::Json,
|
||||||
Router,
|
Router,
|
||||||
response::Html,
|
response::{Html, sse::{Sse, Event, KeepAlive}},
|
||||||
};
|
};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use serde::Deserialize;
|
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 assets::{DEFAULT_INDEX_HTML, DEFAULT_APP_JS, DEFAULT_STYLES_CSS};
|
||||||
|
|
||||||
use crate::{state::SharedState, headless};
|
use crate::{state::SharedState, headless};
|
||||||
|
|
@ -38,26 +42,47 @@ fn rebuild_web_assets(dir: &Path) -> std::io::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct HttpState {
|
||||||
|
shared: SharedState,
|
||||||
|
log_tx: broadcast::Sender<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct StartRequest {
|
struct StartRequest {
|
||||||
ip: String,
|
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();
|
let ip = req.ip.trim().to_string();
|
||||||
println!("[WebServer] Starte Headless-Test für {}", ip);
|
let tx_for_task = st.log_tx.clone();
|
||||||
|
|
||||||
// Klon für den Task
|
|
||||||
let ip_for_task = ip.clone();
|
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = headless::run_test(&ip_for_task).await {
|
if let Err(e) = headless::run_test(&ip, tx_for_task.clone()).await {
|
||||||
eprintln!("[Headless Error] {}", e);
|
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
|
// Port wird jetzt von außen übergeben
|
||||||
|
|
@ -67,6 +92,10 @@ pub async fn start(state: SharedState, port: u16)
|
||||||
let dir = exe_dir();
|
let dir = exe_dir();
|
||||||
rebuild_web_assets(&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 web_root = dir.join(WEB_DIR);
|
||||||
let static_root = web_root.clone();
|
let static_root = web_root.clone();
|
||||||
let index_path = web_root.join("index.html");
|
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))
|
.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();
|
let mut s = state.lock().unwrap();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue