fc894a7f77
GUI desktop app (egui/eframe) that manages the openfut-core and openfut-bridge services with a single window. - Dashboard: start/stop each service independently or together; live status indicators; quick-status for cert and hosts - Logs: real-time stdout/stderr from both processes, colour-coded by level, follow mode and manual clear - Setup: add/remove EA hostname redirects in /etc/hosts (pkexec/sudo); install the bridge TLS cert into the system CA store - Config: all binary paths and env vars editable in-app; persisted to ~/.config/openfut-launcher/config.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
135 lines
3.5 KiB
Rust
135 lines
3.5 KiB
Rust
use std::{
|
|
io::{BufRead, BufReader},
|
|
process::{Child, Command, Stdio},
|
|
sync::{Arc, Mutex},
|
|
thread,
|
|
};
|
|
|
|
use crate::logs::LogBuffer;
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum ServiceStatus {
|
|
Stopped,
|
|
Starting,
|
|
Running,
|
|
Failed(String),
|
|
}
|
|
|
|
impl ServiceStatus {
|
|
pub fn label(&self) -> &str {
|
|
match self {
|
|
ServiceStatus::Stopped => "Stopped",
|
|
ServiceStatus::Starting => "Starting…",
|
|
ServiceStatus::Running => "Running",
|
|
ServiceStatus::Failed(_) => "Failed",
|
|
}
|
|
}
|
|
|
|
pub fn color(&self) -> egui::Color32 {
|
|
match self {
|
|
ServiceStatus::Running => egui::Color32::from_rgb(80, 200, 120),
|
|
ServiceStatus::Starting => egui::Color32::from_rgb(255, 200, 0),
|
|
ServiceStatus::Failed(_) => egui::Color32::from_rgb(220, 60, 60),
|
|
ServiceStatus::Stopped => egui::Color32::from_rgb(150, 150, 150),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct ServiceHandle {
|
|
child: Option<Child>,
|
|
pub status: Arc<Mutex<ServiceStatus>>,
|
|
}
|
|
|
|
impl ServiceHandle {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
child: None,
|
|
status: Arc::new(Mutex::new(ServiceStatus::Stopped)),
|
|
}
|
|
}
|
|
|
|
pub fn start(
|
|
&mut self,
|
|
binary: &str,
|
|
env_pairs: &[(String, String)],
|
|
log_buf: Arc<Mutex<LogBuffer>>,
|
|
) -> anyhow::Result<()> {
|
|
if self.is_running() {
|
|
return Ok(());
|
|
}
|
|
|
|
*self.status.lock().unwrap() = ServiceStatus::Starting;
|
|
|
|
let mut cmd = Command::new(binary);
|
|
for (k, v) in env_pairs {
|
|
cmd.env(k, v);
|
|
}
|
|
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
|
|
|
let mut child = cmd.spawn().map_err(|e| {
|
|
*self.status.lock().unwrap() = ServiceStatus::Failed(e.to_string());
|
|
e
|
|
})?;
|
|
|
|
// Drain stdout
|
|
if let Some(stdout) = child.stdout.take() {
|
|
let buf = Arc::clone(&log_buf);
|
|
let status = Arc::clone(&self.status);
|
|
thread::spawn(move || {
|
|
*status.lock().unwrap() = ServiceStatus::Running;
|
|
for line in BufReader::new(stdout).lines().map_while(Result::ok) {
|
|
buf.lock().unwrap().push(line);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Drain stderr
|
|
if let Some(stderr) = child.stderr.take() {
|
|
let buf = Arc::clone(&log_buf);
|
|
thread::spawn(move || {
|
|
for line in BufReader::new(stderr).lines().map_while(Result::ok) {
|
|
buf.lock().unwrap().push(line);
|
|
}
|
|
});
|
|
}
|
|
|
|
self.child = Some(child);
|
|
Ok(())
|
|
}
|
|
|
|
pub fn stop(&mut self) {
|
|
if let Some(mut child) = self.child.take() {
|
|
let _ = child.kill();
|
|
let _ = child.wait();
|
|
}
|
|
*self.status.lock().unwrap() = ServiceStatus::Stopped;
|
|
}
|
|
|
|
pub fn is_running(&mut self) -> bool {
|
|
if let Some(child) = &mut self.child {
|
|
match child.try_wait() {
|
|
Ok(Some(_)) => {
|
|
// process exited
|
|
self.child = None;
|
|
*self.status.lock().unwrap() = ServiceStatus::Stopped;
|
|
false
|
|
}
|
|
Ok(None) => true,
|
|
Err(_) => false,
|
|
}
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
pub fn status(&self) -> ServiceStatus {
|
|
self.status.lock().unwrap().clone()
|
|
}
|
|
}
|
|
|
|
impl Drop for ServiceHandle {
|
|
fn drop(&mut self) {
|
|
self.stop();
|
|
}
|
|
}
|