Misc settings (#137)

* Initial bug fixes

* fix compile error on non-mac

* Fix even more bugs

* Fix more

* fix more

* fix build

* fix build

* Search fixes

* Fix small instance ui

* working basic

* fix javaw issue

* removed zip

* working functions

* merge fixes

* fixed loadintg bar bug

* menu fix

* wait for settings to sync

* safety expanded and for loading bars

* swtiching to windows

* minimize

* default landing page

* test link registry

* url redirection

* fix formatting

* .mrpack windows

* working mrpack reader

* changed to one layer deep

* working .mrpack + command handling for both opening and existing process

* forge version numbers

* working mac opening mrpack

* reverted changes

* prettier/fmt

* missed debug statement

* improvements + refactoring

* renamed things to fit plugin

* fixed bugs

* removed println

* overrides dont include mrpack

* merge

* fixes

* fixes

* fixed deletion

* merge errors

* force sync before export

* removed testing

* missed line

* removed console log

* mac error reverted

* incoreclty named helper

* additional fixes

* added removed merges

* fixed mislabled invokes

* mac

* added to new register method

* comments, cleanup

* mac clippy change

* review changes

* minor changes

* moved create pack

* removed playground compilation bug

* fixed linux bug; other add ons

* fixed review commets

* cicd fix

* mistaken import for prod

* cicd fix

---------

Co-authored-by: Jai A <jaiagr+gpg@pm.me>
This commit is contained in:
Wyatt Verchere
2023-06-30 08:11:32 -07:00
committed by GitHub
parent f52e777379
commit 91d3bf825d
34 changed files with 1258 additions and 157 deletions

View File

@@ -0,0 +1,72 @@
use std::path::PathBuf;
use crate::event::{
emit::{emit_command, emit_warning},
CommandPayload,
};
/// Handles external functions (such as through URL deep linkage)
/// Link is extracted value (link) in somewhat URL format, such as
/// subdomain1/subdomain2
/// (Does not include modrinth://)
pub async fn handle_url(sublink: &str) -> crate::Result<CommandPayload> {
Ok(match sublink.split_once('/') {
// /mod/{id} - Installs a mod of mod id
Some(("mod", id)) => CommandPayload::InstallMod { id: id.to_string() },
// /version/{id} - Installs a specific version of id
Some(("version", id)) => {
CommandPayload::InstallVersion { id: id.to_string() }
}
// /modpack/{id} - Installs a modpack of modpack id
Some(("modpack", id)) => {
CommandPayload::InstallModpack { id: id.to_string() }
}
_ => {
emit_warning(&format!(
"Invalid command, unrecognized path: {sublink}"
))
.await?;
return Err(crate::ErrorKind::InputError(format!(
"Invalid command, unrecognized path: {sublink}"
))
.into());
}
})
}
pub async fn parse_command(
command_string: &str,
) -> crate::Result<CommandPayload> {
tracing::debug!("Parsing command: {}", &command_string);
// modrinth://some-command
// This occurs when following a web redirect link
if let Some(sublink) = command_string.strip_prefix("modrinth://") {
Ok(handle_url(sublink).await?)
} else {
// We assume anything else is a filepath to an .mrpack file
let path = PathBuf::from(command_string);
let path = path.canonicalize()?;
if let Some(ext) = path.extension() {
if ext == "mrpack" {
return Ok(CommandPayload::RunMRPack { path });
}
}
emit_warning(&format!(
"Invalid command, unrecognized filetype: {}",
path.display()
))
.await?;
Err(crate::ErrorKind::InputError(format!(
"Invalid command, unrecognized filetype: {}",
path.display()
))
.into())
}
}
pub async fn parse_and_emit_command(command_string: &str) -> crate::Result<()> {
let command = parse_command(command_string).await?;
emit_command(command).await?;
Ok(())
}

View File

@@ -1,5 +1,6 @@
//! API for interacting with Theseus
pub mod auth;
pub mod handler;
pub mod jre;
pub mod logs;
pub mod metadata;
@@ -7,6 +8,7 @@ pub mod pack;
pub mod process;
pub mod profile;
pub mod profile_create;
pub mod safety;
pub mod settings;
pub mod tags;
@@ -22,6 +24,7 @@ pub mod prelude {
pub use crate::{
auth::{self, Credentials},
data::*,
event::CommandPayload,
jre, metadata, pack, process,
profile::{self, Profile},
profile_create, settings,

View File

@@ -0,0 +1,5 @@
use crate::state::{ProcessType, SafeProcesses};
pub async fn check_safe_loading_bars() -> crate::Result<bool> {
SafeProcesses::is_complete(ProcessType::LoadingBar).await
}

View File

@@ -1,4 +1,5 @@
//! Theseus profile management interface
pub use crate::{
state::{
Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize,

View File

@@ -90,6 +90,10 @@ pub enum ErrorKind {
#[error("Error: {0}")]
OtherError(String),
#[cfg(feature = "tauri")]
#[error("Tauri error: {0}")]
TauriError(#[from] tauri::Error),
}
#[derive(Debug)]

View File

@@ -1,11 +1,13 @@
use super::LoadingBarId;
use crate::event::{
EventError, LoadingBar, LoadingBarType, ProcessPayloadType,
ProfilePayloadType,
use crate::{
event::{
CommandPayload, EventError, LoadingBar, LoadingBarType,
ProcessPayloadType, ProfilePayloadType,
},
state::{ProcessType, SafeProcesses},
};
use futures::prelude::*;
use std::path::PathBuf;
use tracing::warn;
#[cfg(feature = "tauri")]
use crate::event::{
@@ -42,15 +44,29 @@ const CLI_PROGRESS_BAR_TOTAL: u64 = 1000;
}
*/
// Initialize a loading bar for use in emit_loading
// This will generate a LoadingBarId, which is used to refer to the loading bar uniquely.
// total is the total amount of work to be done- all emissions will be considered a fraction of this value (should be 1 or 100 for simplicity)
// title is the title of the loading bar
/// Initialize a loading bar for use in emit_loading
/// This will generate a LoadingBarId, which is used to refer to the loading bar uniquely.
/// total is the total amount of work to be done- all emissions will be considered a fraction of this value (should be 1 or 100 for simplicity)
/// title is the title of the loading bar
/// The app will wait for this loading bar to finish before exiting, as it is considered safe.
#[theseus_macros::debug_pin]
pub async fn init_loading(
bar_type: LoadingBarType,
total: f64,
title: &str,
) -> crate::Result<LoadingBarId> {
let key = init_loading_unsafe(bar_type, total, title).await?;
SafeProcesses::add_uuid(ProcessType::LoadingBar, key.0).await?;
Ok(key)
}
/// An unsafe loading bar can be created without adding it to the SafeProcesses list,
/// meaning that the app won't ask to wait for it to finish before exiting.
#[theseus_macros::debug_pin]
pub async fn init_loading_unsafe(
bar_type: LoadingBarType,
total: f64,
title: &str,
) -> crate::Result<LoadingBarId> {
let event_state = crate::EventState::get().await?;
let key = LoadingBarId(Uuid::new_v4());
@@ -76,8 +92,6 @@ pub async fn init_loading(
).unwrap()
.progress_chars("#>-"),
);
//pb.set_message(title);
pb
},
},
@@ -215,7 +229,25 @@ pub async fn emit_warning(message: &str) -> crate::Result<()> {
)
.map_err(EventError::from)?;
}
warn!("{}", message);
tracing::warn!("{}", message);
Ok(())
}
// emit_command(CommandPayload::Something { something })
// ie: installing a pack, opening an .mrpack, etc
// Generally used for url deep links and file opens that we we want to handle in the frontend
#[allow(dead_code)]
#[allow(unused_variables)]
pub async fn emit_command(command: CommandPayload) -> crate::Result<()> {
tracing::debug!("Command: {}", serde_json::to_string(&command)?);
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get().await?;
event_state
.app
.emit_all("command", command)
.map_err(EventError::from)?;
}
Ok(())
}

View File

@@ -5,6 +5,8 @@ use tokio::sync::OnceCell;
use tokio::sync::RwLock;
use uuid::Uuid;
use crate::state::SafeProcesses;
pub mod emit;
// Global event state
@@ -48,6 +50,12 @@ impl EventState {
Ok(EVENT_STATE.get().ok_or(EventError::NotInitialized)?.clone())
}
// Initialization requires no app handle in non-tauri mode, so we can just use the same function
#[cfg(not(feature = "tauri"))]
pub async fn get() -> crate::Result<Arc<Self>> {
Self::init().await
}
// Values provided should not be used directly, as they are clones and are not guaranteed to be up-to-date
pub async fn list_progress_bars() -> crate::Result<HashMap<Uuid, LoadingBar>>
{
@@ -62,10 +70,11 @@ impl EventState {
Ok(display_list)
}
// Initialization requires no app handle in non-tauri mode, so we can just use the same function
#[cfg(not(feature = "tauri"))]
pub async fn get() -> crate::Result<Arc<Self>> {
Self::init().await
#[cfg(feature = "tauri")]
pub async fn get_main_window() -> crate::Result<Option<tauri::Window>> {
use tauri::Manager;
let value = Self::get().await?;
Ok(value.app.get_window("main"))
}
}
@@ -91,8 +100,6 @@ pub struct LoadingBarId(Uuid);
impl Drop for LoadingBarId {
fn drop(&mut self) {
let loader_uuid = self.0;
let _event = LoadingBarType::StateInit;
let _message = "finished".to_string();
tokio::spawn(async move {
if let Ok(event_state) = EventState::get().await {
let mut bars = event_state.loading_bars.write().await;
@@ -132,6 +139,11 @@ impl Drop for LoadingBarId {
#[cfg(not(any(feature = "tauri", feature = "cli")))]
bars.remove(&loader_uuid);
}
let _ = SafeProcesses::complete(
crate::state::ProcessType::LoadingBar,
loader_uuid,
)
.await;
});
}
}
@@ -184,6 +196,24 @@ pub struct WarningPayload {
pub message: String,
}
#[derive(Serialize, Clone)]
#[serde(tag = "event")]
pub enum CommandPayload {
InstallMod {
id: String,
},
InstallVersion {
id: String,
},
InstallModpack {
id: String,
},
RunMRPack {
// run or install .mrpack
path: PathBuf,
},
}
#[derive(Serialize, Clone)]
pub struct ProcessPayload {
pub uuid: Uuid, // processes in state are going to be identified by UUIDs, as they might change to different processes

View File

@@ -4,6 +4,7 @@ use crate::event::{LoadingBarId, LoadingBarType};
use crate::jre::{JAVA_17_KEY, JAVA_18PLUS_KEY, JAVA_8_KEY};
use crate::prelude::JavaVersion;
use crate::state::ProfileInstallStage;
use crate::EventState;
use crate::{
process,
state::{self as st, MinecraftChild},
@@ -471,6 +472,18 @@ pub async fn launch_minecraft(
"{MINECRAFT_UUID}".to_string(),
);
// If in tauri, and the 'minimize on launch' setting is enabled, minimize the window
#[cfg(feature = "tauri")]
{
let window = EventState::get_main_window().await?;
if let Some(window) = window {
let settings = state.settings.read().await;
if settings.hide_on_process {
window.minimize()?;
}
}
}
// Create Minecraft child by inserting it into the state
// This also spawns the process and prepares the subsequent processes
let mut state_children = state.children.write().await;

View File

@@ -15,9 +15,11 @@ mod config;
mod error;
mod event;
mod launcher;
mod logger;
mod state;
pub use api::*;
pub use error::*;
pub use event::{EventState, LoadingBar, LoadingBarType};
pub use logger::start_logger;
pub use state::State;

75
theseus/src/logger.rs Normal file
View File

@@ -0,0 +1,75 @@
/*
tracing is set basd on the environment variable RUST_LOG=xxx, depending on the amount of logs to show
ERROR > WARN > INFO > DEBUG > TRACE
eg. RUST_LOG=info will show info, warn, and error logs
RUST_LOG="theseus=trace" will show *all* messages but from theseus only (and not dependencies using similar crates)
RUST_LOG="theseus=trace" will show *all* messages but from theseus only (and not dependencies using similar crates)
Error messages returned to Tauri will display as traced error logs if they return an error.
This will also include an attached span trace if the error is from a tracing error, and the level is set to info, debug, or trace
on unix:
RUST_LOG="theseus=trace" {run command}
The default is theseus=show, meaning only logs from theseus will be displayed, and at the info or higher level.
*/
use tracing_appender::non_blocking::WorkerGuard;
// Handling for the live development logging
// This will log to the console, and will not log to a file
#[cfg(debug_assertions)]
pub fn start_logger() -> Option<WorkerGuard> {
use tracing_subscriber::prelude::*;
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("theseus=info"));
let subscriber = tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(filter)
.with(tracing_error::ErrorLayer::default());
tracing::subscriber::set_global_default(subscriber)
.expect("setting default subscriber failed");
None
}
// Handling for the live production logging
// This will log to a file in the logs directory, and will not show any logs in the console
#[cfg(not(debug_assertions))]
pub fn start_logger() -> Option<WorkerGuard> {
use crate::prelude::DirectoryInfo;
use tracing_appender::rolling::{RollingFileAppender, Rotation};
use tracing_subscriber::fmt::time::ChronoLocal;
use tracing_subscriber::prelude::*;
// Initialize and get logs directory path
let path = if let Some(dir) = DirectoryInfo::init().ok() {
dir.launcher_logs_dir()
} else {
eprintln!("Could not create logger.");
return None;
};
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("theseus=info"));
let file_appender =
RollingFileAppender::new(Rotation::DAILY, path, "theseus.log");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let subscriber = tracing_subscriber::registry()
.with(
tracing_subscriber::fmt::layer()
.with_writer(non_blocking)
.with_ansi(false) // disable ANSI escape codes
.with_timer(ChronoLocal::rfc3339()),
)
.with(filter)
.with(tracing_error::ErrorLayer::default());
tracing::subscriber::set_global_default(subscriber)
.expect("Setting default subscriber failed");
Some(guard)
}

View File

@@ -12,6 +12,7 @@ use tracing::error;
use crate::event::emit::emit_process;
use crate::event::ProcessPayloadType;
use crate::EventState;
use tokio::task::JoinHandle;
use uuid::Uuid;
@@ -129,6 +130,16 @@ impl Children {
break;
}
}
// If in tauri, window should show itself again after process exists if it was hidden
#[cfg(feature = "tauri")]
{
let window = EventState::get_main_window().await?;
if let Some(window) = window {
window.unminimize()?;
}
}
if !mc_exit_status.success() {
emit_process(
uuid,

View File

@@ -1,6 +1,6 @@
//! Theseus directory information
use std::fs;
use std::path::PathBuf;
use tokio::fs;
#[derive(Debug)]
pub struct DirectoryInfo {
@@ -11,7 +11,7 @@ pub struct DirectoryInfo {
impl DirectoryInfo {
/// Get all paths needed for Theseus to operate properly
#[tracing::instrument]
pub async fn init() -> crate::Result<Self> {
pub fn init() -> crate::Result<Self> {
// Working directory
let working_dir = std::env::current_dir().map_err(|err| {
crate::ErrorKind::FSError(format!(
@@ -26,7 +26,7 @@ impl DirectoryInfo {
"Could not find valid config dir".to_string(),
))?;
fs::create_dir_all(&config_dir).await.map_err(|err| {
fs::create_dir_all(&config_dir).map_err(|err| {
crate::ErrorKind::FSError(format!(
"Error creating Theseus config directory: {err}"
))
@@ -130,6 +130,11 @@ impl DirectoryInfo {
.join("modrinth_logs")
}
#[inline]
pub fn launcher_logs_dir(&self) -> PathBuf {
self.config_dir.join("launcher_logs")
}
/// Get the file containing the global database
#[inline]
pub fn database_file(&self) -> PathBuf {

View File

@@ -1,8 +1,7 @@
//! Theseus state management system
use crate::event::emit::emit_loading;
use crate::event::emit::{emit_loading, init_loading_unsafe};
use std::path::PathBuf;
use crate::event::emit::init_loading;
use crate::event::LoadingBarType;
use crate::loading_join;
@@ -46,6 +45,9 @@ pub use self::tags::*;
mod java_globals;
pub use self::java_globals::*;
mod safe_processes;
pub use self::safe_processes::*;
// Global state
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
pub struct State {
@@ -75,6 +77,8 @@ pub struct State {
pub(crate) users: RwLock<Users>,
/// Launcher tags
pub(crate) tags: RwLock<Tags>,
/// Launcher processes that should be safely exited on shutdown
pub(crate) safety_processes: RwLock<SafeProcesses>,
/// File watcher debouncer
pub(crate) file_watcher: RwLock<Debouncer<RecommendedWatcher>>,
@@ -88,7 +92,7 @@ impl State {
LAUNCHER_STATE
.get_or_try_init(|| {
async {
let loading_bar = init_loading(
let loading_bar = init_loading_unsafe(
LoadingBarType::StateInit,
100.0,
"Initializing launcher",
@@ -97,7 +101,7 @@ impl State {
let mut file_watcher = init_watcher().await?;
let directories = DirectoryInfo::init().await?;
let directories = DirectoryInfo::init()?;
emit_loading(&loading_bar, 10.0, None).await?;
// Settings
@@ -132,6 +136,7 @@ impl State {
let children = Children::new();
let auth_flow = AuthTask::new();
let safety_processes = SafeProcesses::new();
emit_loading(&loading_bar, 10.0, None).await?;
Ok(Arc::new(Self {
@@ -151,6 +156,7 @@ impl State {
children: RwLock::new(children),
auth_flow: RwLock::new(auth_flow),
tags: RwLock::new(tags),
safety_processes: RwLock::new(safety_processes),
file_watcher: RwLock::new(file_watcher),
}))
}

View File

@@ -0,0 +1,69 @@
use uuid::Uuid;
use crate::State;
// We implement a store for safe loading bars such that we can wait for them to complete
// We create this store separately from the loading bars themselves, because this may be extended as needed
pub struct SafeProcesses {
pub loading_bars: Vec<Uuid>,
}
#[derive(Debug, Copy, Clone)]
pub enum ProcessType {
LoadingBar,
// Potentially other types of processes (ie: IO operations?)
}
impl SafeProcesses {
// init
pub fn new() -> Self {
Self {
loading_bars: Vec::new(),
}
}
// Adds a new running safe process to the list by uuid
pub async fn add_uuid(
r#type: ProcessType,
uuid: Uuid,
) -> crate::Result<Uuid> {
let state = State::get().await?;
let mut safe_processes = state.safety_processes.write().await;
match r#type {
ProcessType::LoadingBar => {
safe_processes.loading_bars.push(uuid);
}
}
Ok(uuid)
}
// Mark a safe process as finishing
pub async fn complete(
r#type: ProcessType,
uuid: Uuid,
) -> crate::Result<()> {
let state = State::get().await?;
let mut safe_processes = state.safety_processes.write().await;
match r#type {
ProcessType::LoadingBar => {
safe_processes.loading_bars.retain(|x| *x != uuid);
}
}
Ok(())
}
// Check if there are any pending safe processes of a given type
pub async fn is_complete(r#type: ProcessType) -> crate::Result<bool> {
let state = State::get().await?;
let safe_processes = state.safety_processes.read().await;
match r#type {
ProcessType::LoadingBar => {
if safe_processes.loading_bars.is_empty() {
return Ok(true);
}
}
}
Ok(false)
}
}

View File

@@ -30,6 +30,10 @@ pub struct Settings {
pub version: u32,
pub collapsed_navigation: bool,
#[serde(default)]
pub hide_on_process: bool,
#[serde(default)]
pub default_page: DefaultPage,
#[serde(default)]
pub developer_mode: bool,
#[serde(default)]
pub opt_out_analytics: bool,
@@ -54,6 +58,8 @@ impl Default for Settings {
max_concurrent_writes: 10,
version: CURRENT_FORMAT_VERSION,
collapsed_navigation: false,
hide_on_process: false,
default_page: DefaultPage::Home,
developer_mode: false,
opt_out_analytics: false,
advanced_rendering: true,
@@ -126,7 +132,8 @@ impl Settings {
"Error saving settings to file: {err}"
))
.as_error()
})
})?;
Ok(())
}
}
@@ -172,3 +179,16 @@ pub struct Hooks {
#[serde(skip_serializing_if = "Option::is_none")]
pub post_exit: Option<String>,
}
/// Opening window to start with
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub enum DefaultPage {
Home,
Library,
}
impl Default for DefaultPage {
fn default() -> Self {
Self::Home
}
}