use colored::Colorize;
use dusa_collection_utils::core::errors::Errors;
use dusa_collection_utils::core::logger::{set_log_level, LogLevel};
use dusa_collection_utils::core::types::pathtype::PathType;
use dusa_collection_utils::core::types::stringy::Stringy;
use dusa_collection_utils::core::version::SoftwareVersion;
use serde::{Deserialize, Serialize};
use std::{fmt, fs};
use crate::aggregator::{Metrics, Status};
use crate::config::AppConfig;
use crate::encryption::{simple_decrypt, simple_encrypt};
use crate::git_actions::GitServer;
use crate::timestamp::{current_timestamp, format_unix_timestamp};
use dusa_collection_utils::core::errors::ErrorArrayItem;
use dusa_collection_utils::log;
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
pub struct AppState {
pub name: String,
pub version: SoftwareVersion,
pub data: String,
pub status: Status,
pub pid: u32,
pub last_updated: u64,
pub stared_at: u64,
pub event_counter: u32,
pub error_log: Vec<ErrorArrayItem>,
pub config: AppConfig,
pub system_application: bool,
pub stdout: Vec<(u64, String)>,
pub stderr: Vec<(u64, String)>,
}
impl fmt::Display for AppState {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let version = &self.version;
writeln!(f, "{}:", "AppState".bold().underline().cyan())?;
writeln!(f, " {}: {}", "Data".bold().green(), self.data)?;
writeln!(
f,
" {}: {}",
"Seconds Since Update".bold().yellow(),
format_unix_timestamp(self.last_updated)
)?;
writeln!(
f,
" {}: {}",
"Event Counter".bold().magenta(),
self.event_counter
)?;
writeln!(f, " {}:", "Error Log".bold().red())?;
if self.error_log.is_empty() {
writeln!(f, " {}", "No errors".italic().dimmed())?;
} else {
for (i, error) in self.error_log.iter().enumerate() {
writeln!(
f,
" {}: {:#?} - {}",
format!("Error {}", i + 1).bold().yellow(),
error.err_type,
error.err_mesg
)?;
}
}
writeln!(f, " {}:", "Config".bold().purple())?;
writeln!(
f,
" {}: {}",
"App Name".bold().cyan(),
self.config.app_name
)?;
writeln!(
f,
" {}: {}",
"Software Version".bold().cyan(),
version.application
)?;
writeln!(
f,
" {}: {}",
"Library Version".bold().cyan(),
version.library
)?;
writeln!(
f,
" {}: {}",
"Ram Limit".bold().cyan(),
self.config.max_ram_usage
)?;
writeln!(f, " {}: {}", "PID".bold().purple(), self.pid,)?;
writeln!(
f,
" {}: {}",
"Cpu time limit".bold().cyan(),
self.config.max_cpu_usage
)?;
writeln!(
f,
" {}: {}",
"Environment".bold().cyan(),
self.config.environment
)?;
writeln!(
f,
" {}: {}",
"Debug Mode".bold().cyan(),
if self.config.debug_mode {
"Enabled".bold().green()
} else {
"Disabled".bold().red()
}
)?;
if let Some(git) = &self.config.git {
writeln!(f, " {}:", "Git Configuration".bold().purple())?;
writeln!(
f,
" {}: {}",
"Default Server".bold().cyan(),
match &git.default_server {
GitServer::GitHub => "GitHub".bold(),
GitServer::GitLab => "GitLab".bold(),
GitServer::Custom(url) => format!("Custom ({})", url).bold(),
}
)?;
writeln!(
f,
" {}: {}",
"Credentials File".bold().cyan(),
git.credentials_file
)?;
} else {
writeln!(f, " {}", "Git Configuration: None".italic().dimmed())?;
}
if let Some(database) = &self.config.database {
writeln!(f, " {}:", "Database Configuration".bold().purple())?;
writeln!(f, " {}: {}", "URL".bold().cyan(), database.url)?;
writeln!(
f,
" {}: {}",
"Connection Pool Size".bold().cyan(),
database.pool_size
)?;
} else {
writeln!(
f,
" {}",
"Database Configuration: None".italic().dimmed()
)?;
}
if let Some(aggregator) = &self.config.aggregator {
writeln!(f, " {}:", "Aggregator Configuration".bold().purple())?;
writeln!(
f,
" {}: {}",
"Path".bold().cyan(),
aggregator.socket_path
)?;
} else {
writeln!(
f,
" {}",
"Aggregator Configuration: None".italic().dimmed()
)?;
}
writeln!(f, "Status: {}", &self.status)?;
writeln!(
f,
"Standart Out Configured: {}",
if !&self.stdout.is_empty() {
"YES".green().bold()
} else {
"NO".red().bold()
}
)?;
writeln!(
f,
"Standart Error Configured: {}",
if !&self.stderr.is_empty() {
"YES".green().bold()
} else {
"NO".red().bold()
}
)?;
writeln!(f, "\nStandart Out: {}", {
if !&self.stdout.is_empty() {
let mut data = String::new();
self.stdout.iter().for_each(|entry| {
data.push_str(&format!("{}\n", entry.1));
});
data
} else {
"None".to_string()
}
})?;
writeln!(f, "Standart Err: {}", {
if !&self.stderr.is_empty() {
let mut data = String::new();
self.stderr.iter().for_each(|entry| {
data.push_str(&format!("{}\n", entry.1));
});
data
} else {
"None".to_string()
}
})?;
Ok(())
}
}
pub struct StatePersistence;
impl StatePersistence {
pub fn get_state_path(config: &AppConfig) -> PathType {
PathType::Content(format!("/opt/artisan/tmp/.{}.state", config.app_name))
}
pub async fn save_state(
state: &AppState,
path: &PathType,
) -> Result<(), Box<dyn std::error::Error>> {
let toml_str: Stringy = toml::to_string(state)?.into();
let state_data = simple_encrypt(toml_str.as_bytes()).map_err(|e| {
std::io::Error::new(std::io::ErrorKind::InvalidData, e.err_mesg.to_string())
})?;
fs::write(path, state_data.to_string())?;
Ok(())
}
pub async fn load_state(path: &PathType) -> Result<AppState, Box<dyn std::error::Error>> {
let encrypted_content: Stringy = fs::read_to_string(path)?.into();
let content = simple_decrypt(encrypted_content.as_bytes()).map_err(|_| {
std::io::Error::new(std::io::ErrorKind::InvalidData, "Decryption failed")
})?;
let cipher_string = String::from_utf8(content).map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Failed to convert to string",
)
})?;
let state: AppState = toml::from_str(&cipher_string)?;
Ok(state)
}
}
pub async fn update_state(state: &mut AppState, path: &PathType, _metrics: Option<Metrics>) {
state.last_updated = current_timestamp();
state.event_counter += 1;
if let Err(err) = StatePersistence::save_state(state, path).await {
log!(LogLevel::Error, "Failed to save state: {}", err);
state.error_log.push(ErrorArrayItem::new(
Errors::GeneralError,
format!("{}", err),
));
}
log!(LogLevel::Trace, "State Updated");
}
pub async fn wind_down_state(state: &mut AppState, state_path: &PathType) {
state.data = String::from("Terminated");
state.status = Status::Stopping;
state.error_log.push(ErrorArrayItem::new(
Errors::GeneralError,
"Wind down requested - check logs".to_owned(),
));
update_state(state, &state_path, None).await;
}
pub async fn log_error(state: &mut AppState, error: ErrorArrayItem, path: &PathType) {
log!(LogLevel::Error, "{}", error);
state.error_log.push(error);
state.status = Status::Warning;
update_state(state, path, None).await;
}
pub fn debug_log_set(state: &AppState) {
log!(LogLevel::Trace, "Updating log level");
if state.config.debug_mode {
set_log_level(LogLevel::Debug);
}
}