Added Core Features chroium, config,console_ui,headless,webserver,states
This commit is contained in:
parent
4c6c6d572c
commit
c3c60efb81
13 changed files with 688 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
target/
|
||||
Cargo.lock
|
||||
24
Cargo.toml
Normal file
24
Cargo.toml
Normal 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
86
Core/chromium/download.rs
Normal 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
78
Core/chromium/mod.rs
Normal 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
42
Core/chromium/utils.rs
Normal 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
19
Core/config/default.rs
Normal 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
57
Core/config/mod.rs
Normal 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
137
Core/console_ui/mod.rs
Normal 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
20
Core/headless/mod.rs
Normal 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(())
|
||||
}
|
||||
52
Core/main.rs
52
Core/main.rs
|
|
@ -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
39
Core/state.rs
Normal 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
61
Core/web_server/assets.rs
Normal 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
71
Core/web_server/mod.rs
Normal 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)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue