diff --git a/Cargo.toml b/Cargo.toml index dd8f14c..cb0f4ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = ["engine","subcrates/zen_core"] [profile.dev] + rpath = false panic = "abort" lto = "off" @@ -12,8 +13,9 @@ overflow-checks = false incremental = true codegen-units = 512 + strip = "symbols" -debug-assertions = false +debug-assertions = true [profile.dev.package."*"] opt-level = 0 @@ -22,8 +24,9 @@ overflow-checks = false incremental = true codegen-units = 512 + strip = "symbols" -debug-assertions = false +debug-assertions = true [profile.dev.build-override] opt-level = 0 debug = false @@ -34,3 +37,6 @@ codegen-units = 512 [workspace.dependencies] lazy_static = "1.5.0" parking_lot = "0.12.3" + +[profile.release] +debug-assertions = false diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 230f3b7..82574fa 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -2,11 +2,14 @@ name = "engine" version = "0.1.0" edition = "2024" - +repository = "https://github.com/Zenyx-Engine/Zenyx" [dependencies] anyhow = "1.0.94" +backtrace = "0.3.74" chrono = "0.4.39" colored = "2.2.0" +crashreport = "1.0.1" +dirs-next = "2.0.0" lazy_static.workspace = true log = "0.4.22" @@ -15,3 +18,9 @@ parking_lot.workspace = true regex = "1.11.1" rustyline = { version = "15.0.0", features = ["derive", "rustyline-derive"] } tokio = { version = "1.42.0", features = ["macros", "parking_lot", "rt", "rt-multi-thread"] } + +[profile.dev] +debug-assertions = true + +[profile.release] +debug-assertions = false diff --git a/engine/src/core/ecs/mod.rs b/engine/src/core/ecs/mod.rs new file mode 100644 index 0000000..f810534 --- /dev/null +++ b/engine/src/core/ecs/mod.rs @@ -0,0 +1,3 @@ +// struct ComponentRegistry { +// components +// } diff --git a/engine/src/core/mod.rs b/engine/src/core/mod.rs index a831946..7d8a407 100644 --- a/engine/src/core/mod.rs +++ b/engine/src/core/mod.rs @@ -1,3 +1,6 @@ -pub mod repl; +pub mod ecs; pub mod logger; -pub mod splash; \ No newline at end of file +pub mod panic; +pub mod repl; +pub mod splash; +pub mod workspace; diff --git a/engine/src/core/panic.rs b/engine/src/core/panic.rs new file mode 100644 index 0000000..9a14611 --- /dev/null +++ b/engine/src/core/panic.rs @@ -0,0 +1,152 @@ +use std::fmt::Write as FmtWrite; +use std::mem; + +use backtrace::Backtrace; +use parking_lot::Once; +use regex::Regex; + +static INIT: parking_lot::Once = Once::new(); + +//#[cfg(not(debug_assertions))] +pub fn set_panic_hook() { + use std::io::Write; + + use colored::Colorize; + + use crate::workspace; + INIT.call_once(|| { + let default_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + let log_path = workspace::get_working_dir().unwrap_or_else(|_| { + default_hook(info); + std::process::exit(0); + }); + if !log_path.exists() { + std::fs::create_dir_all(&log_path).unwrap_or_else(|_| { + default_hook(info); + std::process::exit(0); + }) + } + let log_path = log_path.join("panic.log"); + + // human_panic::print_msg::(Some(log_path), &human_panic::Metadata::new("Zenyx", env!("CARGO_PKG_VERSION")) + // .support("https://github.com/Zenyx-Engine/Zenyx/issues") + // .authors("Zenyx community ")).unwrap(); + // // Call the default hook for any additional actions + + let mut file = std::fs::File::create(&log_path).unwrap_or_else(|_| { + default_hook(info); + std::process::exit(0); + }); + writeln!(file, "{}", info.payload_as_str().unwrap_or_else(|| { + default_hook(info); + std::process::exit(0); + })).unwrap_or_else(|_| { + default_hook(info); + std::process::exit(0); + }); + writeln!(file, "{}", render_backtrace().sanitize_path()).unwrap_or_else(|_| { + default_hook(info); + std::process::exit(0); + }); + let panic_msg = format!( +"Zenyx had a problem and crashed. To help us diagnose the problem you can send us a crash report. + +We have generated a report file at \"{}\". Submit an issue or email with the subject of \"Zenyx Crash Report\" and include the report as an attachment. + +To submit the crash report: + +https://github.com/Zenyx-Engine/Zenyx/issues + +We take privacy seriously, and do not perform any automated error collection. In order to improve the software, we rely on people to submit reports. + +Thank you kindly!",log_path.display()); + println!("{}",panic_msg.red().bold()); + println!("\nFor future reference, the error summary is as follows:\n{}",info.payload_as_str().unwrap_or_else(||{ + default_hook(info); + std::process::exit(0); + }).red().bold()); + std::process::exit(0); // There is nothing to be done at this point, it looks cleaner to exit instead of doing a natural panic + })); + }); +} +// THIS SNIPPET IS LICENSED UNDER THE APACHE LICENSE, VERSION 2.0 +// https://github.com/rust-cli/human-panic +// No changes were made to the original snippet +fn render_backtrace() -> String { + //We take padding for address and extra two letters + //to pad after index. + #[allow(unused_qualifications)] // needed for pre-1.80 MSRV + const HEX_WIDTH: usize = mem::size_of::() * 2 + 2; + //Padding for next lines after frame's address + const NEXT_SYMBOL_PADDING: usize = HEX_WIDTH + 6; + + let mut backtrace = String::new(); + + //Here we iterate over backtrace frames + //(each corresponds to function's stack) + //We need to print its address + //and symbol(e.g. function name), + //if it is available + let bt = Backtrace::new(); + let symbols = bt + .frames() + .iter() + .flat_map(|frame| { + let symbols = frame.symbols(); + if symbols.is_empty() { + vec![(frame, None, "".to_owned())] + } else { + symbols + .iter() + .map(|s| { + ( + frame, + Some(s), + s.name() + .map(|n| n.to_string()) + .unwrap_or_else(|| "".to_owned()), + ) + }) + .collect::>() + } + }) + .collect::>(); + let begin_unwind = "rust_begin_unwind"; + let begin_unwind_start = symbols + .iter() + .position(|(_, _, n)| n == begin_unwind) + .unwrap_or(0); + for (entry_idx, (frame, symbol, name)) in symbols.iter().skip(begin_unwind_start).enumerate() { + let ip = frame.ip(); + let _ = writeln!(backtrace, "{entry_idx:4}: {ip:HEX_WIDTH$?} - {name}"); + if let Some(symbol) = symbol { + //See if there is debug information with file name and line + if let (Some(file), Some(line)) = (symbol.filename(), symbol.lineno()) { + let _ = writeln!( + backtrace, + "{:3$}at {}:{}", + "", + file.display(), + line, + NEXT_SYMBOL_PADDING + ); + } + } + } + + backtrace +} + +trait Sanitize { + fn sanitize_path(&self) -> String; +} + +impl Sanitize for str { + fn sanitize_path(&self) -> String { + let username_pattern = r"(?i)(/home/|/Users/|\\Users\\)([^/\\]+)"; + let re = Regex::new(username_pattern).expect("Failed to sanitize path, aborting operation"); + + re.replace_all(self, "${1}").to_string() + } +} diff --git a/engine/src/core/repl/commands.rs b/engine/src/core/repl/commands.rs index 0b1a38e..4708302 100644 --- a/engine/src/core/repl/commands.rs +++ b/engine/src/core/repl/commands.rs @@ -4,9 +4,8 @@ use anyhow::anyhow; use parking_lot::RwLock; use regex::Regex; -use crate::core::repl::handler::COMMAND_MANAGER; - use super::{handler::Command, input::tokenize}; +use crate::core::repl::handler::COMMAND_MANAGER; #[derive(Default)] pub struct HelpCommand; @@ -19,7 +18,7 @@ impl Command for HelpCommand { for (_, command) in manager.get_commands() { println!( "Command: {}\n\tDescription: {}\n\tParameters: {}\n\tHelp: {}\n", - command.get_name(), + command.get_name().to_lowercase(), command.get_description(), command.get_params(), command.get_help() @@ -62,7 +61,9 @@ impl Command for ClearCommand { fn execute(&self, _args: Option>) -> Result<(), anyhow::Error> { println!("Clearing screen..., running command"); let _result = if cfg!(target_os = "windows") { - std::process::Command::new("cmd").args(["/c", "cls"]).spawn() + std::process::Command::new("cmd") + .args(["/c", "cls"]) + .spawn() } else { std::process::Command::new("clear").spawn() }; @@ -97,14 +98,13 @@ impl Command for ExitCommand { fn execute(&self, args: Option>) -> Result<(), anyhow::Error> { match args { Some(args) => { - let exit_code = args[0].parse()?; std::process::exit(exit_code); // Ok(()) - }, + } None => { std::process::exit(0); - }, + } } } @@ -136,27 +136,33 @@ impl Command for ExitCommand { pub struct ExecFile; impl Command for ExecFile { - fn execute(&self, args: Option>) -> Result<(),anyhow::Error> { + fn execute(&self, args: Option>) -> Result<(), anyhow::Error> { match args { Some(args) => { - let file_path = PathBuf::from_str(&args[0])?; if file_path.extension().is_some() && file_path.extension().unwrap() != "zensh" { return Err(anyhow!("Selected file was not a zensh file")); } else { let zscript = fs::read_to_string(file_path)?; if let Ok(command) = eval(zscript) { - println!("{:#?}",command); - for (cmd_name,cmd_args) in command { - COMMAND_MANAGER.read().execute(&cmd_name, cmd_args)? + println!("{:#?}", command); + for (cmd_name, cmd_args) in command { + match COMMAND_MANAGER.read().execute(&cmd_name, cmd_args) { + Ok(_) => (), + Err(e) => { + println!( + "Error executing command returned an error: {}. Aborting script", + e + ); + break; + } + } } } } Ok(()) - }, - None => { - Err(anyhow!("Not enough argumentss")) - }, + } + None => Err(anyhow!("Not enough argumentss")), } } @@ -181,37 +187,6 @@ impl Command for ExecFile { } } -fn eval(input: String) -> Result>)>, anyhow::Error> { - if input.trim().is_empty() { - return Err(anyhow!("Input was empty")); - } - - let pattern = Regex::new(r"[;|\n]").unwrap(); - let commands: Vec<&str> = pattern.split(&input).collect(); - let mut evaluted = vec![]; - - for command in commands { - let command = command.trim(); - if command.is_empty() { - println!("Empty command, skipping."); - continue; - } - - let tokens = tokenize(command); - if tokens.is_empty() { - println!("Empty command, skipping."); - continue; - } - let cmd_name = &tokens[0]; - let args: Option> = if tokens.len() > 1 { - Some(tokens[1..].iter().map(|s| s.to_string()).collect()) - } else { - None - }; - evaluted.push((cmd_name.to_owned(),args)); - } - Ok(evaluted) -} #[derive(Default)] pub struct CounterCommand { @@ -250,4 +225,73 @@ impl Command for CounterCommand { fn get_name(&self) -> String { String::from("count") } -} \ No newline at end of file +} +#[derive(Default)] +pub struct PanicCommmand; +impl Command for PanicCommmand { + fn execute(&self, args: Option>) -> Result<(), anyhow::Error> { + if args.is_some() { + let panic_msg = &args.unwrap()[0]; + panic!("{}", panic_msg) + } + let option: Option = None; + println!("Unwrapping None: {}", option.unwrap()); + panic!("Panic command was called") + } + + fn undo(&self) {} + + fn redo(&self) {} + + fn get_description(&self) -> String { + String::from("causes a panic with your provided message") + } + + fn get_name(&self) -> String { + String::from("panic") + } + + fn get_help(&self) -> String { + String::from("") + } + + fn get_params(&self) -> String { + String::from("optional: panic msg") + } +} + + + + +fn eval(input: String) -> Result>)>, anyhow::Error> { + if input.trim().is_empty() { + return Err(anyhow!("Input was empty")); + } + + let pattern = Regex::new(r"[;|\n]").unwrap(); + let commands: Vec<&str> = pattern.split(&input).collect(); + let mut evaluted = vec![]; + + for command in commands { + let command = command.trim(); + if command.is_empty() { + println!("Empty command, skipping."); + continue; + } + + let tokens = tokenize(command); + if tokens.is_empty() { + println!("Empty command, skipping."); + continue; + } + let cmd_name = &tokens[0]; + let args: Option> = if tokens.len() > 1 { + Some(tokens[1..].iter().map(|s| s.to_string()).collect()) + } else { + None + }; + evaluted.push((cmd_name.to_owned(),args)); + } + Ok(evaluted) +} + diff --git a/engine/src/core/repl/handler.rs b/engine/src/core/repl/handler.rs index 563368d..4b7a340 100644 --- a/engine/src/core/repl/handler.rs +++ b/engine/src/core/repl/handler.rs @@ -29,7 +29,6 @@ macro_rules! alias { }; } - fn hamming_distance(a: &str, b: &str) -> Option { if a.len() != b.len() { return None; @@ -71,7 +70,7 @@ fn check_similarity(target: &str) -> Option { let mut best_match: Option = None; let mut best_distance = usize::MAX; - for (cmd_name,_) in COMMAND_MANAGER.read().get_commands() { + for (cmd_name, _) in COMMAND_MANAGER.read().get_commands() { if let Some(hamming_dist) = hamming_distance(target, cmd_name) { if hamming_dist <= max_hamming_distance && hamming_dist < best_distance { best_distance = hamming_dist; @@ -92,6 +91,7 @@ fn check_similarity(target: &str) -> Option { pub struct CommandManager { pub commands: HashMap>, pub aliases: HashMap, + pub categories: HashMap, } impl CommandManager { @@ -99,53 +99,99 @@ impl CommandManager { CommandManager { commands: HashMap::new(), aliases: HashMap::new(), + categories: HashMap::new(), } } + + pub fn add_category(&mut self, category: Category) { + self.categories.insert(category.name.clone(), category); + } + pub fn get_commands(&self) -> std::collections::hash_map::Iter<'_, String, Box> { self.commands.iter() } - pub fn execute_command(&self,command: &str,args: Option>) -> Result<(),anyhow::Error> { + + pub fn execute_command( + &self, + command: &str, + args: Option>, + ) -> Result<(), anyhow::Error> { if let Some(command) = self.commands.get(command) { command.execute(args)?; Ok(()) } else { - println!("Command '{}' not found.", command); let corrected_cmd = check_similarity(command); if corrected_cmd.is_some() { println!("Command: {} was not found. Did you mean {}?",command.red().bold(),corrected_cmd .expect("A command was editied during execution, something has gone seriously wrong").green().bold().italic()); - return Ok(()); } - Ok(()) + Err(anyhow::anyhow!("Command '{}' not found.", command)) } } - pub fn execute(&self, command: &str,args: Option>) -> Result<(), anyhow::Error> { + pub fn execute(&self, command: &str, args: Option>) -> Result<(), anyhow::Error> { match self.aliases.get(command) { - Some(command) => self.execute(command,args), + Some(command) => self.execute(command, args), // check to see if we are using an alias or the command just doesnt exist None => { - self.execute_command(command,args)?; + self.execute_command(command, args)?; Ok(()) - }, + } } - } pub fn add_command(&mut self, command: Box) { - self.commands.insert(command.get_name().to_lowercase(), command); + self.commands + .insert(command.get_name().to_lowercase(), command); } + + pub fn add_command_with_category(&mut self, command: Box, category: Category) { + if self.categories.contains_key(&category.name) { + let mut cmd_name = command.get_name().to_lowercase(); + cmd_name.insert_str(0, &format!("{}_", &&category.uid.to_lowercase())); + println!("{}", cmd_name); + self.commands.insert(cmd_name, command); + } else { + panic!("Category {} does not exist", category.name); + } + } + pub fn add_alias(&mut self, alias: &str, command: &str) { - self.aliases.insert(alias.to_string(), command.to_string()); + self.aliases.insert( + alias.to_string().to_lowercase(), + command.to_string().to_lowercase(), + ); + } +} +#[derive(Debug, Clone)] +pub struct Category { + // eg: Zenyx -> Z + // eg: core -> cr + // eg: exitcmd -> cr_exit + // eg: echo -> z_echo + pub uid: String, + // eg: Zenyx + pub name: String, + // eg: Zenyx internal commands + pub description: String, +} + +impl Category { + pub fn new(uid: &str, name: &str, description: &str) -> Self { + Self { + uid: uid.to_string(), + name: name.to_string(), + description: description.to_string(), + } } } pub trait Command: Send + Sync { - fn execute(&self, args: Option>) -> Result<(),anyhow::Error>; + fn execute(&self, args: Option>) -> Result<(), anyhow::Error>; fn undo(&self); fn redo(&self); fn get_description(&self) -> String; fn get_name(&self) -> String; fn get_help(&self) -> String; fn get_params(&self) -> String; -} \ No newline at end of file +} diff --git a/engine/src/core/repl/input.rs b/engine/src/core/repl/input.rs index 7a5a52e..33995e9 100644 --- a/engine/src/core/repl/input.rs +++ b/engine/src/core/repl/input.rs @@ -161,10 +161,10 @@ pub fn evaluate_command(input: &str) -> anyhow::Result<()> { } else { None }; - match COMMAND_MANAGER.read().execute(cmd_name, args) { - Ok(_) => continue, - Err(e) => return Err(e) - } + match COMMAND_MANAGER.read().execute(cmd_name, args) { + Ok(_) => continue, + Err(e) => return Err(e), + } } Ok(()) } diff --git a/engine/src/core/repl/mod.rs b/engine/src/core/repl/mod.rs index a5b0613..9e1dd63 100644 --- a/engine/src/core/repl/mod.rs +++ b/engine/src/core/repl/mod.rs @@ -1,12 +1,23 @@ -use commands::{ClearCommand, CounterCommand, ExecFile, ExitCommand, HelpCommand}; +use commands::{ClearCommand, CounterCommand, ExecFile, ExitCommand, HelpCommand, PanicCommmand}; +use handler::{COMMAND_MANAGER, Category}; use crate::commands; pub mod commands; -pub mod input; pub mod handler; - +pub mod input; pub fn setup() { - commands!(HelpCommand,ClearCommand,ExitCommand,ExecFile,CounterCommand); + commands!( + HelpCommand, + ClearCommand, + ExitCommand, + CounterCommand, + PanicCommmand + ); + let cat = Category::new("cr", "Core", "Core commands"); + COMMAND_MANAGER.write().add_category(cat.clone()); + COMMAND_MANAGER + .write() + .add_command_with_category(Box::new(ExecFile), cat.clone()); } diff --git a/engine/src/core/splash.rs b/engine/src/core/splash.rs index 7381a46..94f4b5b 100644 --- a/engine/src/core/splash.rs +++ b/engine/src/core/splash.rs @@ -26,4 +26,4 @@ pub fn print_splash() { ) .bright_yellow() ); -} \ No newline at end of file +} diff --git a/engine/src/core/workspace/mod.rs b/engine/src/core/workspace/mod.rs new file mode 100644 index 0000000..d98bf5a --- /dev/null +++ b/engine/src/core/workspace/mod.rs @@ -0,0 +1,17 @@ +use std::path::PathBuf; + +use anyhow::{Context, Result, anyhow}; + +pub fn get_working_dir() -> Result { + let mut dir = dirs_next::data_dir() + .ok_or(anyhow!("Expected working directory, found: None")) + .context("Could not fetch working dir")?; + dir.push("Zenyx"); + Ok(dir) +} + +pub fn get_data_dir() -> Result { + let mut dir = get_working_dir().context("Failed to obtain working dir")?; + dir.push("data"); + Ok(dir) +} diff --git a/engine/src/main.rs b/engine/src/main.rs index 3beed07..ccd41b9 100644 --- a/engine/src/main.rs +++ b/engine/src/main.rs @@ -1,18 +1,33 @@ -use core::{repl::{handler::COMMAND_MANAGER, input::handle_repl, setup}, splash}; +#![feature(panic_payload_as_str)] +use core::{ + panic::set_panic_hook, + repl::{handler::COMMAND_MANAGER, setup}, + splash, workspace, +}; use anyhow::Ok; - +use colored::Colorize; +use tokio::runtime; pub mod core; -#[tokio::main] -async fn main() -> anyhow::Result<()> { - setup(); - splash::print_splash(); - COMMAND_MANAGER.read().execute("help", None)?; - let t = tokio::spawn(handle_repl()); +fn main() -> anyhow::Result<()> { + if !cfg!(debug_assertions) { + println!("{}", "Debug mode disabled".bright_blue()); + set_panic_hook(); + } + let runtime = runtime::Builder::new_current_thread() + .enable_all() + .build()?; - t.await??; + runtime.block_on(async { + setup(); + splash::print_splash(); + COMMAND_MANAGER.read().execute("help", None)?; + let t = tokio::spawn(core::repl::input::handle_repl()); + t.await??; + Ok(()) + })?; Ok(()) diff --git a/engine/test.zensh b/engine/test.zensh index 5149af1..aa8771e 100644 --- a/engine/test.zensh +++ b/engine/test.zensh @@ -1,7 +1 @@ -count;count;count;count;count;count;count;count;count;count;count;count;count;count;count;count;count;count;count;count;count;count - -count -help - - -exit 102 \ No newline at end of file +cr_exec ./test.zensh diff --git a/main.zensh b/main.zensh new file mode 100644 index 0000000..95eee35 --- /dev/null +++ b/main.zensh @@ -0,0 +1 @@ +exit "test" \ No newline at end of file diff --git a/subcrates/zen_core/Cargo.toml b/subcrates/zen_core/Cargo.toml index 0604d37..d342c70 100644 --- a/subcrates/zen_core/Cargo.toml +++ b/subcrates/zen_core/Cargo.toml @@ -6,3 +6,10 @@ edition = "2024" [dependencies] anyhow = "1.0.94" thiserror = "2.0.8" +parking_lot.workspace = true + +[profile.dev] +debug-assertions = true + +[profile.release] +debug-assertions = false diff --git a/subcrates/zen_core/src/lib.rs b/subcrates/zen_core/src/lib.rs index 9b8cf04..1293081 100644 --- a/subcrates/zen_core/src/lib.rs +++ b/subcrates/zen_core/src/lib.rs @@ -1,9 +1,11 @@ -use thiserror::Error; +use thiserror::Error; + #[derive(Debug,Error)] enum ZError { #[error(transparent)] Unknown(#[from] anyhow::Error) -} \ No newline at end of file +} + diff --git a/test.zensh b/test.zensh deleted file mode 100644 index f7725fc..0000000 --- a/test.zensh +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - -echo "Hello World" -echo "hello world"; hello \ No newline at end of file