Added Core Features chroium, config,console_ui,headless,webserver,states

This commit is contained in:
Joey Pillunat 2025-10-24 11:52:34 +02:00
parent 4c6c6d572c
commit c3c60efb81
13 changed files with 688 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
target/
Cargo.lock

24
Cargo.toml Normal file
View file

@ -0,0 +1,24 @@
[package]
name = "aurora_print"
version = "1.0.0"
edition = "2024"
[[bin]]
name = "aurora_core"
path = "Core/main.rs"
[build-dependencies]
[dependencies]
axum = "0.7"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tower-http = { version = "0.5", features = ["fs"] }
crossterm = "0.27"
ratatui = "0.26"
serde = { version = "1", features = ["derive"] }
toml = "0.8"
reqwest = { version = "0.12", features = ["blocking", "rustls-tls"] }
zip = "0.6"
sha2 = "0.10"
pathdiff = "0.2"

86
Core/chromium/download.rs Normal file
View file

@ -0,0 +1,86 @@
use std::{fs, io, path::{Path, PathBuf}};
use reqwest::blocking::Client;
use super::utils::extract_zip; // <-- statt util
#[cfg(target_os = "windows")]
const PLATFORM_DIR: &str = "Win_x64";
#[cfg(target_os = "linux")]
const PLATFORM_DIR: &str = "Linux_x64";
#[cfg(target_os = "macos")]
const PLATFORM_DIR: &str = "Mac";
fn zip_name() -> &'static str {
#[cfg(target_os = "windows")] { "chrome-win.zip" }
#[cfg(target_os = "linux")] { "chrome-linux.zip" }
#[cfg(target_os = "macos")] { "chrome-mac.zip" }
}
fn bin_relative_path() -> &'static str {
#[cfg(target_os = "windows")] { "chrome-win/chrome.exe" }
#[cfg(target_os = "linux")] { "chrome-linux/chrome" }
#[cfg(target_os = "macos")] { "chrome-mac/Chromium.app/Contents/MacOS/Chromium" }
}
fn fetch_latest_revision(client: &Client) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let url = format!(
"https://storage.googleapis.com/chromium-browser-snapshots/{}/LAST_CHANGE",
PLATFORM_DIR
);
let txt = client.get(url).send()?.error_for_status()?.text()?;
Ok(txt.trim().to_string())
}
pub fn find_latest_local(cache_dir: &Path) -> io::Result<Option<PathBuf>> {
let mut best: Option<(u64, PathBuf)> = None;
if !cache_dir.exists() { return Ok(None); }
for entry in fs::read_dir(cache_dir)? {
let entry = entry?;
if !entry.file_type()?.is_dir() { continue; }
let name = entry.file_name().to_string_lossy().to_string();
if let Ok(n) = name.parse::<u64>() {
let bin = entry.path().join(bin_relative_path());
if bin.exists() {
if let Some((best_n, _)) = best {
if n > best_n { best = Some((n, bin)); }
} else {
best = Some((n, bin));
}
}
}
}
Ok(best.map(|(_, p)| p))
}
fn download_to(client: &Client, url: &str, dest: &Path) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let mut resp = client.get(url).send()?.error_for_status()?;
let mut file = fs::File::create(dest)?;
io::copy(&mut resp, &mut file)?;
Ok(())
}
pub fn get_or_install_latest(cache_dir: &Path) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>>
{
fs::create_dir_all(cache_dir)?;
let client = Client::builder().build()?;
let rev = fetch_latest_revision(&client)?;
let rev_dir = cache_dir.join(&rev);
let bin_path = rev_dir.join(bin_relative_path());
if bin_path.exists() {
return Ok(bin_path);
}
let url = format!(
"https://storage.googleapis.com/chromium-browser-snapshots/{}/{}/{}",
PLATFORM_DIR, rev, zip_name()
);
let zip_path = cache_dir.join(format!("chromium-{}.zip", rev));
if !zip_path.exists() {
download_to(&client, &url, &zip_path)?;
}
extract_zip(&zip_path, &rev_dir)?;
Ok(bin_path)
}

78
Core/chromium/mod.rs Normal file
View file

@ -0,0 +1,78 @@
use std::{path::{Path, PathBuf}};
use crate::config::{Config, save_config, resolve_path};
use crate::state::{SharedState, ChromiumPhase};
mod download;
pub mod utils;
pub const CACHE_DIR: &str = "runtime/chrome-cache";
pub async fn start_check(cfg: &mut Config, exe_dir: &Path, state: SharedState)
-> Result<PathBuf, Box<dyn std::error::Error>>
{
// Status: Checking
{
let mut s = state.lock().unwrap();
s.chromium = ChromiumPhase::Checking;
}
// ensure_chromium kann Download triggern (wir geben state weiter, um „Downloading“ zu setzen)
match ensure_chromium(cfg, exe_dir, state.clone()).await {
Ok(path) => {
let mut s = state.lock().unwrap();
s.chromium = ChromiumPhase::Ready;
Ok(path)
}
Err(e) => {
let mut s = state.lock().unwrap();
s.chromium = ChromiumPhase::Error(e.to_string());
Err(e)
}
}
}
async fn ensure_chromium(cfg: &mut Config, exe_dir: &Path, state: SharedState)
-> Result<PathBuf, Box<dyn std::error::Error>>
{
// 1) Pfad aus Config
if !cfg.chrome.path.is_empty() {
let p = resolve_path(exe_dir, &cfg.chrome.path);
if p.exists() {
return Ok(p);
}
}
// 2) Lokaler Cache?
if let Some(p) = find_existing_binary(exe_dir) {
cfg.chrome.path = p.to_string_lossy().to_string();
let _ = save_config(exe_dir, cfg);
return Ok(p);
}
// 3) Download erforderlich -> Status: Downloading
{
let mut s = state.lock().unwrap();
s.chromium = ChromiumPhase::Downloading;
}
let cache_dir = exe_dir.join(CACHE_DIR);
let bin_path_result = tokio::task::spawn_blocking(move || {
download::get_or_install_latest(&cache_dir)
}).await;
let bin_path = match bin_path_result {
Ok(Ok(path)) => path,
Ok(Err(e)) => return Err(e as Box<dyn std::error::Error>),
Err(e) => return Err(Box::new(e)),
};
cfg.chrome.path = bin_path.to_string_lossy().to_string();
let _ = save_config(exe_dir, cfg);
Ok(bin_path)
}
fn find_existing_binary(exe_dir: &Path) -> Option<PathBuf> {
let cache = exe_dir.join(CACHE_DIR);
download::find_latest_local(&cache).ok().flatten()
}

42
Core/chromium/utils.rs Normal file
View file

@ -0,0 +1,42 @@
use std::{fs, io::{self}, path::{Path, PathBuf}};
use zip::read::ZipArchive;
pub fn extract_zip(src: &Path, dest: &Path) -> io::Result<()> {
let file = fs::File::open(src)?;
let mut archive = ZipArchive::new(file)?;
fs::create_dir_all(dest)?;
for i in 0..archive.len() {
let mut entry = archive.by_index(i)?;
let outpath = dest.join(sanitize_path(entry.name()));
if entry.is_dir() {
fs::create_dir_all(&outpath)?;
} else {
if let Some(p) = outpath.parent() { fs::create_dir_all(p)?; }
let mut outfile = fs::File::create(&outpath)?;
io::copy(&mut entry, &mut outfile)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Some(mode) = entry.unix_mode() {
fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?;
}
}
}
}
Ok(())
}
fn sanitize_path(name: &str) -> PathBuf {
let path = Path::new(name);
let mut clean = PathBuf::new();
for comp in path.components() {
use std::path::Component::*;
match comp {
Normal(c) => clean.push(c),
CurDir | ParentDir | RootDir | Prefix(_) => { /* skip */ }
}
}
clean
}

19
Core/config/default.rs Normal file
View file

@ -0,0 +1,19 @@
use super::{Config, ServerCfg, ChromeCfg};
pub fn default_config() -> Config {
Config {
server: ServerCfg { port: 8080 },
chrome: ChromeCfg {
path: "".to_string(), // leer => auto-download beim Start
args: vec![
"--headless=new".to_string(),
"--disable-gpu".to_string(),
"--no-sandbox".to_string(),
"--ignore-certificate-errors".to_string(),
"--allow-insecure-localhost".to_string(),
"--disable-features=SSL_ERROR_OVERRIDE_UI".to_string(),
"--test-type".to_string(),
],
},
}
}

57
Core/config/mod.rs Normal file
View file

@ -0,0 +1,57 @@
mod default;
use default::default_config;
use std::{fs, io, path::{Path, PathBuf}};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChromeCfg {
pub path: String, // kann leer sein => auto-download
pub args: Vec<String>, // SSL-Flags etc.
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerCfg {
pub port: u16,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub server: ServerCfg,
pub chrome: ChromeCfg,
}
const CONF_NAME: &str = "server.conf";
pub fn read_config(dir: &Path) -> io::Result<Config> {
let path = dir.join(CONF_NAME);
if !path.exists() {
let def = default_config();
let toml_str = toml::to_string_pretty(&def)
.expect("Default Config serialisieren fehlgeschlagen");
fs::write(&path, toml_str)?;
Ok(def)
} else {
let content = fs::read_to_string(&path)?;
let cfg: Config = toml::from_str(&content)
.unwrap_or_else(|_| {
eprintln!("[Aurora] WARNUNG: Config beschädigt lade Default.");
default_config()
});
Ok(cfg)
}
}
pub fn save_config(dir: &Path, cfg: &Config) -> io::Result<()> {
let path = dir.join(CONF_NAME);
let toml_str = toml::to_string_pretty(cfg)
.expect("Config serialisieren fehlgeschlagen");
fs::write(path, toml_str)
}
/// Hilfsfunktion: macht aus einem (ggf. relativen) Pfad einen absoluten Pfad relativ zur EXE.
pub fn resolve_path(exe_dir: &Path, p: &str) -> PathBuf {
let pb = PathBuf::from(p);
if pb.is_absolute() { pb } else { exe_dir.join(pb) }
}

137
Core/console_ui/mod.rs Normal file
View file

@ -0,0 +1,137 @@
use std::time::{Duration, Instant};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{
backend::CrosstermBackend,
Terminal,
layout::{Constraint, Direction, Layout},
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, Paragraph},
};
use crate::state::{SharedState, ChromiumPhase, HeadlessPhase};
pub fn start_ui(state: SharedState) -> std::io::Result<()> {
enable_raw_mode()?;
let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut last_tick = Instant::now();
let tick_rate = Duration::from_millis(250);
loop {
let snapshot = {
let s = state.lock().unwrap();
s.clone()
};
terminal.draw(|f| {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([
Constraint::Length(4),
Constraint::Min(6),
Constraint::Min(6),
])
.split(f.size());
let header = Block::default()
.title(" Aurora Core — Engines ")
.borders(Borders::ALL);
let web_dot = Span::styled(
"",
Style::default().fg(if snapshot.web_server_active { Color::Green } else { Color::Red })
);
let plug_dot = Span::styled(
"",
Style::default().fg(if snapshot.plugin_engine_active { Color::Green } else { Color::Red })
);
// Chromium Punkt + optionaler Text
let (chrom_dot_color, chrom_text) = match &snapshot.chromium {
ChromiumPhase::Ready => (Color::Green, "".to_string()),
ChromiumPhase::Downloading => (Color::Yellow, "downloading…".to_string()),
ChromiumPhase::Checking => (Color::Yellow, "checking…".to_string()),
ChromiumPhase::Idle => (Color::Red, "".to_string()),
ChromiumPhase::Error(msg) => (Color::Red, format!("error: {}", msg)),
};
let chrom_dot = Span::styled("", Style::default().fg(chrom_dot_color));
// Headless Engine Punkt + optionaler Text
let (head_dot_color, head_text) = match &snapshot.headless {
HeadlessPhase::Ready => (Color::Green, "".to_string()),
HeadlessPhase::Starting => (Color::Yellow, "starting…".to_string()),
HeadlessPhase::Idle => (Color::Red, "".to_string()),
HeadlessPhase::Error(msg) => (Color::Red, format!("error: {}", msg)),
};
let head_dot = Span::styled("", Style::default().fg(head_dot_color));
let mut spans = vec![
Span::raw("Web Server "), web_dot, Span::raw(" "),
Span::raw("Plugin Engine "), plug_dot, Span::raw(" "),
Span::raw("Chromium "), chrom_dot,
];
if !chrom_text.is_empty() {
spans.push(Span::raw(" "));
spans.push(Span::styled(format!("({})", chrom_text), Style::default().fg(Color::Gray)));
}
spans.push(Span::raw(" "));
spans.push(Span::raw("Headless Engine "));
spans.push(head_dot);
if !head_text.is_empty() {
spans.push(Span::raw(" "));
spans.push(Span::styled(format!("({})", head_text), Style::default().fg(Color::Gray)));
}
let header_para = Paragraph::new(Line::from(spans)).block(header);
f.render_widget(header_para, chunks[0]);
// Loaded plugins
let plugin_block = Block::default().title(" Loaded Plugins ").borders(Borders::ALL);
let items: Vec<ListItem> = if snapshot.loaded_plugins.is_empty() {
vec![ListItem::new("— none —")]
} else {
snapshot.loaded_plugins.iter().map(|p| ListItem::new(p.clone())).collect()
};
f.render_widget(List::new(items).block(plugin_block), chunks[1]);
// Running jobs
let jobs_block = Block::default().title(" Active Setups ").borders(Borders::ALL);
let jobs_items: Vec<ListItem> = if snapshot.running_jobs.is_empty() {
vec![ListItem::new("— idle —")]
} else {
snapshot.running_jobs.iter().map(|j| ListItem::new(j.clone())).collect()
};
f.render_widget(List::new(jobs_items).block(jobs_block), chunks[2]);
})?;
let timeout = tick_rate.checked_sub(last_tick.elapsed()).unwrap_or(Duration::from_secs(0));
if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => break,
_ => {}
}
}
}
if last_tick.elapsed() >= tick_rate {
last_tick = Instant::now();
}
}
disable_raw_mode()?;
let mut stdout = std::io::stdout();
execute!(stdout, LeaveAlternateScreen)?;
Ok(())
}

20
Core/headless/mod.rs Normal file
View file

@ -0,0 +1,20 @@
use crate::state::{SharedState, HeadlessPhase};
/// 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>> {
{
let mut s = state.lock().unwrap();
s.headless = HeadlessPhase::Starting;
}
// Hier später: chromiumoxide Browser-Launch + Healthcheck
// Für jetzt: kurze simulierte Init-Zeit
tokio::time::sleep(std::time::Duration::from_millis(400)).await;
{
let mut s = state.lock().unwrap();
s.headless = HeadlessPhase::Ready;
}
Ok(())
}

View file

@ -0,0 +1,52 @@
mod web_server;
mod state;
mod console_ui;
mod config;
mod chromium;
mod headless;
use state::{SharedState, Status};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Aurora Core gestartet!");
//config Datei Lesen
let exe_dir = std::env::current_exe()?.parent().unwrap().to_path_buf();
let mut cfg = config::read_config(&exe_dir)?;
// Gemeinsamen Status anlegen
let state: SharedState = std::sync::Arc::new(std::sync::Mutex::new(Status {
web_server_active: false,
plugin_engine_active: false,
chromium: Default::default(),
headless: Default::default(),
loaded_plugins: vec![],
running_jobs: vec![],
}));
// UI sofort anzeigen läuft in eigenem Thread
let ui_state = state.clone();
let ui_thread = std::thread::spawn(move || {
// UI blockiert hier bis q/ESC aber nur in DIESEM Thread
let _ = console_ui::start_ui(ui_state);
});
//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();
// (Optional) Nach UI-Ende Webserver-Task abbrechen:
// _server.abort();
Ok(())
}

39
Core/state.rs Normal file
View file

@ -0,0 +1,39 @@
use std::sync::{Arc, Mutex};
#[derive(Clone, Debug)]
pub enum ChromiumPhase {
Idle,
Checking,
Downloading,
Ready,
Error(String),
}
impl Default for ChromiumPhase {
fn default() -> Self { ChromiumPhase::Idle }
}
#[derive(Clone, Debug)]
pub enum HeadlessPhase {
Idle,
Starting,
Ready,
Error(String),
}
impl Default for HeadlessPhase {
fn default() -> Self { HeadlessPhase::Idle }
}
#[derive(Default, Clone)]
pub struct Status {
pub web_server_active: bool,
pub plugin_engine_active: bool,
pub chromium: ChromiumPhase,
pub headless: HeadlessPhase,
pub loaded_plugins: Vec<String>,
pub running_jobs: Vec<String>,
}
// Shared State Handle
pub type SharedState = Arc<Mutex<Status>>;

61
Core/web_server/assets.rs Normal file
View file

@ -0,0 +1,61 @@
pub const DEFAULT_INDEX_HTML: &str = r#"
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aurora Print UI</title>
<link rel="stylesheet" href="/static/styles.css" />
</head>
<body>
<header>
<h1>Aurora Print</h1>
</header>
<main>
<section class="card">
<h2>Gerät erkennen</h2>
<label for="ip">IP-Adresse</label>
<input id="ip" type="text" placeholder="z.B. 192.168.1.50" />
<button id="btn-start">Start</button>
<p id="status" class="status"></p>
</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');
btn.addEventListener('click', async () => {
const val = (ip.value || '').trim();
if (!val) {
status.textContent = 'Bitte IP eingeben.';
return;
}
status.textContent = `Erkenne Gerät bei ${val} (Demo)`;
});
});
"#;
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;
background: #0b0f19; color: #e6eefc; }
header { padding: 16px 24px; border-bottom: 1px solid #1c2233; }
h1 { margin: 0; font-size: 20px; }
main { max-width: 720px; margin: 32px auto; padding: 0 16px; }
.card { background: #121829; border: 1px solid #1f2740; border-radius: 12px; padding: 16px;
box-shadow: 0 6px 18px rgba(0,0,0,0.25); }
label { display:block; margin: 8px 0 4px; color: #9db4ff; }
input { width: 100%; padding: 10px 12px; border-radius: 8px; border: 1px solid #34406a;
background: #0e1424; color: #e6eefc; }
button { margin-top: 12px; padding: 10px 14px; border-radius: 8px; border: 1px solid #3a5bff;
background: #3056ff; color: white; cursor: pointer; }
button:hover { filter: brightness(1.05); }
.status { margin-top: 10px; color: #9db4ff; }
"#;

71
Core/web_server/mod.rs Normal file
View file

@ -0,0 +1,71 @@
use std::{
env, fs,
net::SocketAddr,
path::{Path, PathBuf},
};
use axum::{routing::get, Router, response::Html};
use tower_http::services::ServeDir;
mod assets;
use assets::{DEFAULT_INDEX_HTML, DEFAULT_APP_JS, DEFAULT_STYLES_CSS};
use crate::state::SharedState; // ⬅️ nur noch state importieren
const WEB_DIR: &str = "web";
fn exe_dir() -> PathBuf {
env::current_exe()
.ok()
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
.unwrap_or_else(|| PathBuf::from("."))
}
fn rebuild_web_assets(dir: &Path) -> std::io::Result<()> {
let web = dir.join(WEB_DIR);
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())?;
fs::write(web.join("styles.css"), DEFAULT_STYLES_CSS.trim_start())?;
Ok(())
}
// Port wird jetzt von außen übergeben
pub async fn start(state: SharedState, port: u16)
-> Result<tokio::task::JoinHandle<()>, Box<dyn std::error::Error>>
{
let dir = exe_dir();
rebuild_web_assets(&dir)?;
let web_root = dir.join(WEB_DIR);
let static_root = web_root.clone();
let index_path = web_root.join("index.html");
let app = Router::new()
.route("/", get({
let index_path = index_path.clone();
move || {
let index_path = index_path.clone();
async move {
let html = fs::read_to_string(&index_path)
.unwrap_or_else(|_| "<h1>index.html fehlt</h1>".to_string());
Html(html)
}
}
}))
.nest_service("/static", ServeDir::new(static_root))
.route("/healthz", get(|| async { "ok" }));
{
let mut s = state.lock().unwrap();
s.web_server_active = true;
}
let addr: SocketAddr = format!("0.0.0.0:{}", port).parse()?;
let listener = tokio::net::TcpListener::bind(addr).await?;
let handle = tokio::spawn(async move {
let _ = axum::serve(listener, app).await;
});
Ok(handle)
}