String settings hooks (#82)

* added theme; env change

* began refactoring

* added process hook

* now singular string for each hook

* fixed splitting by comma to by space

* profile_create function updated

* prettier

* added jre validator

* restructured so that it doesnt look like a vec

* fixed merge issue

* snake case

* resolved merge issues + added process events

* clippy, fmt

* removed unnecssary func
This commit is contained in:
Wyatt Verchere
2023-04-17 12:40:27 -07:00
committed by GitHub
parent b120b5cfa8
commit 9f40640ed8
23 changed files with 473 additions and 210 deletions

79
Cargo.lock generated
View File

@@ -2110,6 +2110,17 @@ dependencies = [
"objc_exception",
]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]]
name = "objc_exception"
version = "0.1.2"
@@ -2733,6 +2744,30 @@ dependencies = [
"winreg 0.10.1",
]
[[package]]
name = "rfd"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea"
dependencies = [
"block",
"dispatch",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"lazy_static",
"log",
"objc",
"objc-foundation",
"objc_id",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows 0.37.0",
]
[[package]]
name = "rustc-demangle"
version = "0.1.22"
@@ -3344,6 +3379,7 @@ dependencies = [
"rand 0.8.5",
"raw-window-handle",
"regex",
"rfd",
"semver",
"serde",
"serde_json",
@@ -4304,6 +4340,19 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647"
dependencies = [
"windows_aarch64_msvc 0.37.0",
"windows_i686_gnu 0.37.0",
"windows_i686_msvc 0.37.0",
"windows_x86_64_gnu 0.37.0",
"windows_x86_64_msvc 0.37.0",
]
[[package]]
name = "windows"
version = "0.39.0"
@@ -4413,6 +4462,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_msvc"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a"
[[package]]
name = "windows_aarch64_msvc"
version = "0.39.0"
@@ -4425,6 +4480,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_i686_gnu"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1"
[[package]]
name = "windows_i686_gnu"
version = "0.39.0"
@@ -4437,6 +4498,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_msvc"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c"
[[package]]
name = "windows_i686_msvc"
version = "0.39.0"
@@ -4449,6 +4516,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_x86_64_gnu"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.39.0"
@@ -4467,6 +4540,12 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_msvc"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.39.0"

View File

@@ -1,4 +1,6 @@
//! Authentication flow interface
use std::path::PathBuf;
use crate::{
launcher::download,
prelude::Profile,
@@ -139,3 +141,8 @@ pub async fn validate_globals() -> crate::Result<bool> {
let settings = state.settings.read().await;
Ok(settings.java_globals.is_all_valid())
}
// Validates JRE at a given at a given path
pub async fn check_jre(path: PathBuf) -> crate::Result<Option<JavaVersion>> {
Ok(jre::check_java_at_filepath(&path))
}

View File

@@ -11,7 +11,7 @@ pub mod tags;
pub mod data {
pub use crate::state::{
DirectoryInfo, Hooks, JavaSettings, MemorySettings, ModLoader,
ProfileMetadata, Settings, WindowSize,
ProfileMetadata, Settings, Theme, WindowSize,
};
}

View File

@@ -229,8 +229,8 @@ async fn install_pack(
.await?;
let loading_bar = init_loading(
LoadingBarType::PackDownload {
pack_name ,
LoadingBarType::PackDownload {
pack_name,
pack_id: project_id,
pack_version: version_id,
},

View File

@@ -1,6 +1,8 @@
//! Theseus process management interface
use std::path::{Path, PathBuf};
use uuid::Uuid;
use crate::state::MinecraftChild;
pub use crate::{
state::{
@@ -9,31 +11,33 @@ pub use crate::{
State,
};
// Gets whether a child process stored in the state by PID has finished
// Gets whether a child process stored in the state by UUID has finished
#[tracing::instrument]
pub async fn has_finished_by_pid(pid: u32) -> crate::Result<bool> {
Ok(get_exit_status_by_pid(pid).await?.is_some())
pub async fn has_finished_by_uuid(uuid: &Uuid) -> crate::Result<bool> {
Ok(get_exit_status_by_uuid(uuid).await?.is_some())
}
// Gets the exit status of a child process stored in the state by PID
// Gets the exit status of a child process stored in the state by UUID
#[tracing::instrument]
pub async fn get_exit_status_by_pid(pid: u32) -> crate::Result<Option<i32>> {
pub async fn get_exit_status_by_uuid(
uuid: &Uuid,
) -> crate::Result<Option<i32>> {
let state = State::get().await?;
let children = state.children.read().await;
Ok(children.exit_status(&pid).await?.and_then(|f| f.code()))
Ok(children.exit_status(uuid).await?.and_then(|f| f.code()))
}
// Gets the PID of each stored process in the state
// Gets the UUID of each stored process in the state
#[tracing::instrument]
pub async fn get_all_pids() -> crate::Result<Vec<u32>> {
pub async fn get_all_uuids() -> crate::Result<Vec<Uuid>> {
let state = State::get().await?;
let children = state.children.read().await;
Ok(children.keys())
}
// Gets the PID of each *running* stored process in the state
// Gets the UUID of each *running* stored process in the state
#[tracing::instrument]
pub async fn get_all_running_pids() -> crate::Result<Vec<u32>> {
pub async fn get_all_running_uuids() -> crate::Result<Vec<Uuid>> {
let state = State::get().await?;
let children = state.children.read().await;
children.running_keys().await
@@ -55,62 +59,62 @@ pub async fn get_all_running_profiles() -> crate::Result<Vec<Profile>> {
children.running_profiles().await
}
// Gets the PID of each stored process in the state by profile path
// Gets the UUID of each stored process in the state by profile path
#[tracing::instrument]
pub async fn get_pids_by_profile_path(
pub async fn get_uuids_by_profile_path(
profile_path: &Path,
) -> crate::Result<Vec<u32>> {
) -> crate::Result<Vec<Uuid>> {
let state = State::get().await?;
let children = state.children.read().await;
children.running_keys_with_profile(profile_path).await
}
// Gets stdout of a child process stored in the state by PID, as a string
// Gets stdout of a child process stored in the state by UUID, as a string
#[tracing::instrument]
pub async fn get_stdout_by_pid(pid: u32) -> crate::Result<String> {
pub async fn get_stdout_by_uuid(uuid: &Uuid) -> crate::Result<String> {
let state = State::get().await?;
// Get stdout from child
let children = state.children.read().await;
// Extract child or return crate::Error
if let Some(child) = children.get(&pid) {
if let Some(child) = children.get(uuid) {
let child = child.read().await;
Ok(child.stdout.get_output().await?)
} else {
Err(crate::ErrorKind::LauncherError(format!(
"No child process with PID {}",
pid
"No child process by UUID {}",
uuid
))
.as_error())
}
}
// Gets stderr of a child process stored in the state by PID, as a string
// Gets stderr of a child process stored in the state by UUID, as a string
#[tracing::instrument]
pub async fn get_stderr_by_pid(pid: u32) -> crate::Result<String> {
pub async fn get_stderr_by_uuid(uuid: &Uuid) -> crate::Result<String> {
let state = State::get().await?;
// Get stdout from child
let children = state.children.read().await;
// Extract child or return crate::Error
if let Some(child) = children.get(&pid) {
if let Some(child) = children.get(uuid) {
let child = child.read().await;
Ok(child.stderr.get_output().await?)
} else {
Err(crate::ErrorKind::LauncherError(format!(
"No child process with PID {}",
pid
"No child process with UUID {}",
uuid
))
.as_error())
}
}
// Kill a child process stored in the state by PID, as a string
// Kill a child process stored in the state by UUID, as a string
#[tracing::instrument]
pub async fn kill_by_pid(pid: u32) -> crate::Result<()> {
pub async fn kill_by_uuid(uuid: &Uuid) -> crate::Result<()> {
let state = State::get().await?;
let children = state.children.read().await;
if let Some(mchild) = children.get(&pid) {
if let Some(mchild) = children.get(uuid) {
let mut mchild = mchild.write().await;
kill(&mut mchild).await
} else {
@@ -119,13 +123,13 @@ pub async fn kill_by_pid(pid: u32) -> crate::Result<()> {
}
}
// Wait for a child process stored in the state by PID
// Wait for a child process stored in the state by UUID
#[tracing::instrument]
pub async fn wait_for_by_pid(pid: u32) -> crate::Result<()> {
pub async fn wait_for_by_uuid(uuid: &Uuid) -> crate::Result<()> {
let state = State::get().await?;
let children = state.children.read().await;
// No error returned for already killed process
if let Some(mchild) = children.get(&pid) {
if let Some(mchild) = children.get(uuid) {
let mut mchild = mchild.write().await;
wait_for(&mut mchild).await
} else {
@@ -137,18 +141,30 @@ pub async fn wait_for_by_pid(pid: u32) -> crate::Result<()> {
// Kill a running child process directly, and wait for it to be killed
#[tracing::instrument]
pub async fn kill(running: &mut MinecraftChild) -> crate::Result<()> {
running.child.kill().await?;
running.current_child.write().await.kill().await?;
wait_for(running).await
}
// Await on the completion of a child process directly
#[tracing::instrument]
pub async fn wait_for(running: &mut MinecraftChild) -> crate::Result<()> {
let result = running.child.wait().await.map_err(|err| {
crate::ErrorKind::LauncherError(format!(
"Error running minecraft: {err}"
))
})?;
// We do not wait on the Child directly, but wait on the thread manager.
// This way we can still run all cleanup hook functions that happen after.
let result = running
.manager
.take()
.ok_or_else(|| {
crate::ErrorKind::LauncherError(format!(
"Process manager already completed or missing for process {}",
running.uuid
))
})?
.await?
.map_err(|err| {
crate::ErrorKind::LauncherError(format!(
"Error running minecraft: {err}"
))
})?;
match result.success() {
false => Err(crate::ErrorKind::LauncherError(format!(

View File

@@ -256,7 +256,7 @@ pub async fn run_credentials(
.await?;
let pre_launch_hooks =
&profile.hooks.as_ref().unwrap_or(&settings.hooks).pre_launch;
for hook in pre_launch_hooks.iter() {
if let Some(hook) = pre_launch_hooks {
// TODO: hook parameters
let mut cmd = hook.split(' ');
if let Some(command) = cmd.next() {
@@ -336,6 +336,23 @@ pub async fn run_credentials(
let env_args = &settings.custom_env_args;
// Post post exit hooks
let post_exit_hook =
&profile.hooks.as_ref().unwrap_or(&settings.hooks).post_exit;
let post_exit_hook = if let Some(hook) = post_exit_hook {
let mut cmd = hook.split(' ');
if let Some(command) = cmd.next() {
let mut command = Command::new(command);
command.args(&cmd.collect::<Vec<&str>>()).current_dir(path);
Some(command)
} else {
None
}
} else {
None
};
let mc_process = crate::launcher::launch_minecraft(
&profile.metadata.game_version,
&profile.metadata.loader_version,
@@ -347,20 +364,10 @@ pub async fn run_credentials(
&memory,
&resolution,
credentials,
post_exit_hook,
&profile,
)
.await?;
// Insert child into state
let mut state_children = state.children.write().await;
let pid = mc_process.id().ok_or_else(|| {
crate::ErrorKind::LauncherError(
"Process failed to stay open.".to_string(),
)
})?;
let mchild_arc = state_children
.insert_process(pid, path.to_path_buf(), mc_process)
.await?;
Ok(mchild_arc)
Ok(mc_process)
}

View File

@@ -129,7 +129,7 @@ pub async fn emit_warning(message: &str) -> crate::Result<()> {
Ok(())
}
// emit_process(pid, event, message)
// emit_process(uuid, pid, event, message)
#[allow(unused_variables)]
pub async fn emit_process(
uuid: uuid::Uuid,

View File

@@ -93,7 +93,7 @@ pub enum LoadingBarType {
pack_name: String,
pack_id: Option<String>,
pack_version: Option<String>,
},
},
MinecraftDownload {
profile_uuid: Uuid,
profile_name: String,
@@ -121,11 +121,11 @@ pub struct ProcessPayload {
pub event: ProcessPayloadType,
pub message: String,
}
#[derive(Serialize, Clone)]
#[derive(Serialize, Clone, Debug)]
pub enum ProcessPayloadType {
Launched,
// Finishing, // TODO: process restructing incoming, currently this is never emitted
// Finished, // TODO: process restructing incoming, currently this is never emitted
Updated, // eg: if the MinecraftChild changes to its post-command process instead of the Minecraft process
Finished,
}
#[derive(Serialize, Clone)]

View File

@@ -33,7 +33,7 @@ pub async fn download_minecraft(
LoadingBarType::MinecraftDownload {
// If we are downloading minecraft for a profile, provide its name and uuid
profile_name: profile.metadata.name.clone(),
profile_uuid: profile.uuid,
profile_uuid: profile.uuid,
},
100.0,
"Downloading Minecraft...",

View File

@@ -1,10 +1,13 @@
//! Logic for launching Minecraft
use crate::{process, state as st};
use crate::{
process,
state::{self as st, MinecraftChild},
};
use daedalus as d;
use dunce::canonicalize;
use st::Profile;
use std::{path::Path, process::Stdio};
use tokio::process::{Child, Command};
use std::{path::Path, process::Stdio, sync::Arc};
use tokio::process::Command;
mod args;
@@ -58,8 +61,9 @@ pub async fn launch_minecraft(
memory: &st::MemorySettings,
resolution: &st::WindowSize,
credentials: &auth::Credentials,
post_exit_hook: Option<Command>,
profile: &Profile, // optional ref to Profile for event tracking
) -> crate::Result<Child> {
) -> crate::Result<Arc<tokio::sync::RwLock<MinecraftChild>>> {
let state = st::State::get().await?;
let instance_path = &canonicalize(instance_path)?;
@@ -182,10 +186,10 @@ pub async fn launch_minecraft(
// Check if profile has a running profile, and reject running the command if it does
// Done late so a quick double call doesn't launch two instances
let existing_processes =
process::get_pids_by_profile_path(instance_path).await?;
if let Some(pid) = existing_processes.first() {
process::get_uuids_by_profile_path(instance_path).await?;
if let Some(uuid) = existing_processes.first() {
return Err(crate::ErrorKind::LauncherError(format!(
"Profile {} is already running at PID: {pid}",
"Profile {} is already running at UUID: {uuid}",
instance_path.display()
))
.as_error());
@@ -233,12 +237,15 @@ pub async fn launch_minecraft(
.stdout(Stdio::piped())
.stderr(Stdio::piped());
command.spawn().map_err(|err| {
crate::ErrorKind::LauncherError(format!(
"Error running Minecraft (minecraft-{} @ {}): {err}",
&version.id,
instance_path.display()
))
.as_error()
})
// 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;
state_children
.insert_process(
uuid::Uuid::new_v4(),
instance_path.to_path_buf(),
command,
post_exit_hook,
)
.await
}

View File

@@ -1,25 +1,29 @@
use super::Profile;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use std::{collections::HashMap, sync::Arc};
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::process::Child;
use tokio::process::Command;
use tokio::process::{ChildStderr, ChildStdout};
use tokio::sync::RwLock;
use crate::event::emit::emit_process;
use crate::event::ProcessPayloadType;
use super::Profile;
use tokio::task::JoinHandle;
use uuid::Uuid;
// Child processes (instances of Minecraft)
// A wrapper over a Hashmap connecting PID -> MinecraftChild
pub struct Children(HashMap<u32, Arc<RwLock<MinecraftChild>>>);
pub struct Children(HashMap<Uuid, Arc<RwLock<MinecraftChild>>>);
// Minecraft Child, bundles together the PID, the actual Child, and the easily queryable stdout and stderr streams
#[derive(Debug)]
pub struct MinecraftChild {
pub uuid: uuid::Uuid,
pub pid: u32,
pub uuid: Uuid,
pub profile_path: PathBuf, //todo: make UUID when profiles are recognized by UUID
pub child: tokio::process::Child,
pub manager: Option<JoinHandle<crate::Result<ExitStatus>>>, // None when future has completed and been handled
pub current_child: Arc<RwLock<Child>>,
pub stdout: SharedOutput,
pub stderr: SharedOutput,
}
@@ -29,16 +33,18 @@ impl Children {
Children(HashMap::new())
}
// Inserts a child process to keep track of, and returns a reference to the container struct MinecraftChild
// Runs the command in process, inserts a child process to keep track of, and returns a reference to the container struct MinecraftChild
// The threads for stdout and stderr are spawned here
// Unlike a Hashmap's 'insert', this directly returns the reference to the Child rather than any previously stored Child that may exist
// Unlike a Hashmap's 'insert', this directly returns the reference to the MinecraftChild rather than any previously stored MinecraftChild that may exist
pub async fn insert_process(
&mut self,
pid: u32,
uuid: Uuid,
profile_path: PathBuf,
mut child: tokio::process::Child,
mut mc_command: Command,
post_command: Option<Command>, // Command to run after minecraft.
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
let uuid = uuid::Uuid::new_v4();
// Takes the first element of the commands vector and spawns it
let mut child = mc_command.spawn()?;
// Create std watcher threads for stdout and stderr
let stdout = SharedOutput::new();
@@ -55,11 +61,25 @@ impl Children {
let stderr_clone = stderr.clone();
tokio::spawn(async move {
if let Err(e) = stderr_clone.read_stderr(child_stderr).await {
eprintln!("Stderr thread died with error: {}", e);
eprintln!("Stderr process died with error: {}", e);
}
});
}
// Slots child into manager
let pid = child.id().ok_or_else(|| {
crate::ErrorKind::LauncherError(
"Process immediately failed, could not get PID".to_string(),
)
})?;
let current_child = Arc::new(RwLock::new(child));
let manager = Some(tokio::spawn(Self::sequential_process_manager(
uuid,
post_command,
pid,
current_child.clone(),
)));
emit_process(
uuid,
pid,
@@ -71,24 +91,88 @@ impl Children {
// Create MinecraftChild
let mchild = MinecraftChild {
uuid,
pid,
profile_path,
child,
current_child,
stdout,
stderr,
manager,
};
let mchild = Arc::new(RwLock::new(mchild));
self.0.insert(pid, mchild.clone());
self.0.insert(uuid, mchild.clone());
Ok(mchild)
}
// Spawns a new child process and inserts it into the hashmap
// Also, as the process ends, it spawns the follow-up process if it exists
// By convention, ExitStatus is last command's exit status, and we exit on the first non-zero exit status
async fn sequential_process_manager(
uuid: Uuid,
post_command: Option<Command>,
mut current_pid: u32,
current_child: Arc<RwLock<Child>>,
) -> crate::Result<ExitStatus> {
let current_child = current_child.clone();
// Wait on current Minecraft Child
let mut mc_exit_status;
loop {
if let Some(t) = current_child.write().await.try_wait()? {
mc_exit_status = t;
break;
}
}
if !mc_exit_status.success() {
return Ok(mc_exit_status); // Err for a non-zero exit is handled in helper
}
// If a post-command exist, switch to it and wait on it
if let Some(mut m_command) = post_command {
{
let mut current_child = current_child.write().await;
let new_child = m_command.spawn()?;
current_pid = new_child.id().ok_or_else(|| {
crate::ErrorKind::LauncherError(
"Process immediately failed, could not get PID"
.to_string(),
)
})?;
*current_child = new_child;
}
emit_process(
uuid,
current_pid,
ProcessPayloadType::Updated,
"Completed Minecraft, switching to post-commands",
)
.await?;
loop {
if let Some(t) = current_child.write().await.try_wait()? {
mc_exit_status = t;
break;
}
}
}
emit_process(
uuid,
current_pid,
ProcessPayloadType::Finished,
"Exited process",
)
.await?;
Ok(mc_exit_status)
}
// Returns a ref to the child
pub fn get(&self, pid: &u32) -> Option<Arc<RwLock<MinecraftChild>>> {
self.0.get(pid).cloned()
pub fn get(&self, uuid: &Uuid) -> Option<Arc<RwLock<MinecraftChild>>> {
self.0.get(uuid).cloned()
}
// Gets all PID keys
pub fn keys(&self) -> Vec<u32> {
pub fn keys(&self) -> Vec<Uuid> {
self.0.keys().cloned().collect()
}
@@ -96,25 +180,25 @@ impl Children {
// Returns None if the child is still running
pub async fn exit_status(
&self,
pid: &u32,
uuid: &Uuid,
) -> crate::Result<Option<std::process::ExitStatus>> {
if let Some(child) = self.get(pid) {
let child = child.clone();
let mut child = child.write().await;
Ok(child.child.try_wait()?)
if let Some(child) = self.get(uuid) {
let child = child.write().await;
let status = child.current_child.write().await.try_wait()?;
Ok(status)
} else {
Ok(None)
}
}
// Gets all PID keys of running children
pub async fn running_keys(&self) -> crate::Result<Vec<u32>> {
pub async fn running_keys(&self) -> crate::Result<Vec<Uuid>> {
let mut keys = Vec::new();
for key in self.keys() {
if let Some(child) = self.get(&key) {
let child = child.clone();
let mut child = child.write().await;
if child.child.try_wait()?.is_none() {
let child = child.write().await;
if child.current_child.write().await.try_wait()?.is_none() {
keys.push(key);
}
}
@@ -126,7 +210,7 @@ impl Children {
pub async fn running_keys_with_profile(
&self,
profile_path: &Path,
) -> crate::Result<Vec<u32>> {
) -> crate::Result<Vec<Uuid>> {
let running_keys = self.running_keys().await?;
let mut keys = Vec::new();
for key in running_keys {
@@ -147,8 +231,8 @@ impl Children {
for key in self.keys() {
if let Some(child) = self.get(&key) {
let child = child.clone();
let mut child = child.write().await;
if child.child.try_wait()?.is_none() {
let child = child.write().await;
if child.current_child.write().await.try_wait()?.is_none() {
profiles.push(child.profile_path.clone());
}
}
@@ -163,8 +247,8 @@ impl Children {
for key in self.keys() {
if let Some(child) = self.get(&key) {
let child = child.clone();
let mut child = child.write().await;
if child.child.try_wait()?.is_none() {
let child = child.write().await;
if child.current_child.write().await.try_wait()?.is_none() {
if let Some(prof) =
crate::api::profile::get(&child.profile_path.clone())
.await?

View File

@@ -1,6 +1,6 @@
//! Theseus settings file
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, path::Path};
use std::path::Path;
use tokio::fs;
use super::JavaGlobals;
@@ -13,6 +13,7 @@ const CURRENT_FORMAT_VERSION: u32 = 1;
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(default)]
pub struct Settings {
pub theme: Theme,
pub memory: MemorySettings,
pub game_resolution: WindowSize,
pub custom_java_args: Vec<String>,
@@ -27,6 +28,7 @@ pub struct Settings {
impl Default for Settings {
fn default() -> Self {
Self {
theme: Theme::Dark,
memory: MemorySettings::default(),
game_resolution: WindowSize::default(),
custom_java_args: Vec::new(),
@@ -74,6 +76,15 @@ impl Settings {
}
}
/// Theseus theme
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Theme {
Dark,
Light,
Oled,
}
/// Minecraft memory settings
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub struct MemorySettings {
@@ -105,10 +116,10 @@ impl Default for WindowSize {
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
#[serde(default)]
pub struct Hooks {
#[serde(skip_serializing_if = "HashSet::is_empty")]
pub pre_launch: HashSet<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pre_launch: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub wrapper: Option<String>,
#[serde(skip_serializing_if = "HashSet::is_empty")]
pub post_exit: HashSet<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub post_exit: Option<String>,
}

View File

@@ -19,7 +19,7 @@ theseus = { path = "../../theseus", features = ["tauri"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.2", features = ["protocol-asset", "window-close", "window-create"] }
tauri = { version = "1.2", features = ["protocol-asset", "window-close", "window-create", "dialog"] }
tokio = { version = "1", features = ["full"] }
thiserror = "1.0"
tokio-stream = { version = "0.1", features = ["fs"] }

View File

@@ -1,4 +1,4 @@
use std::path::Path;
use std::path::{Path, PathBuf};
use crate::api::Result;
use theseus::prelude::JavaVersion;
@@ -60,3 +60,10 @@ pub async fn jre_get_optimal_jre_key_by_path(path: &Path) -> Result<String> {
pub async fn jre_validate_globals() -> Result<bool> {
Ok(jre::validate_globals().await?)
}
// Validates JRE at a given path
// Returns None if the path is not a valid JRE
#[tauri::command]
pub async fn jre_get_jre(path: PathBuf) -> Result<Option<JavaVersion>> {
jre::check_jre(path).await.map_err(|e| e.into())
}

View File

@@ -33,6 +33,9 @@ pub enum TheseusSerializableError {
#[error("No profile found at {0}")]
NoProfileFound(String),
#[error("Improperly formatted environment variables: {0}")]
BadEnvVars(String),
}
// Generic implementation of From<T> for ErrorTypeA
@@ -74,4 +77,5 @@ impl_serialize! {
Theseus,
IO,
NoProfileFound,
BadEnvVars,
}

View File

@@ -2,37 +2,40 @@ use std::path::{Path, PathBuf};
use crate::api::Result;
use theseus::prelude::*;
use uuid::Uuid;
// Checks if a process has finished by process PID
// Checks if a process has finished by process UUID
#[tauri::command]
pub async fn process_has_finished_by_pid(pid: u32) -> Result<bool> {
Ok(process::has_finished_by_pid(pid).await?)
pub async fn process_has_finished_by_uuid(uuid: Uuid) -> Result<bool> {
Ok(process::has_finished_by_uuid(&uuid).await?)
}
// Gets process exit status by process PID
// Gets process exit status by process UUID
#[tauri::command]
pub async fn process_get_exit_status_by_pid(pid: u32) -> Result<Option<i32>> {
Ok(process::get_exit_status_by_pid(pid).await?)
pub async fn process_get_exit_status_by_uuid(
uuid: Uuid,
) -> Result<Option<i32>> {
Ok(process::get_exit_status_by_uuid(&uuid).await?)
}
// Gets all process PIDs
// Gets all process UUIDs
#[tauri::command]
pub async fn process_get_all_pids() -> Result<Vec<u32>> {
Ok(process::get_all_pids().await?)
pub async fn process_get_all_uuids() -> Result<Vec<Uuid>> {
Ok(process::get_all_uuids().await?)
}
// Gets all running process PIDs
// Gets all running process UUIDs
#[tauri::command]
pub async fn process_get_all_running_pids() -> Result<Vec<u32>> {
Ok(process::get_all_running_pids().await?)
pub async fn process_get_all_running_uuids() -> Result<Vec<Uuid>> {
Ok(process::get_all_running_uuids().await?)
}
// Gets all process PIDs by profile path
// Gets all process UUIDs by profile path
#[tauri::command]
pub async fn process_get_pids_by_profile_path(
pub async fn process_get_uuids_by_profile_path(
profile_path: &Path,
) -> Result<Vec<u32>> {
Ok(process::get_pids_by_profile_path(profile_path).await?)
) -> Result<Vec<Uuid>> {
Ok(process::get_uuids_by_profile_path(profile_path).await?)
}
// Gets the Profile paths of each *running* stored process in the state
@@ -47,26 +50,26 @@ pub async fn process_get_all_running_profiles() -> Result<Vec<Profile>> {
Ok(process::get_all_running_profiles().await?)
}
// Gets process stderr by process PID
// Gets process stderr by process UUID
#[tauri::command]
pub async fn process_get_stderr_by_pid(pid: u32) -> Result<String> {
Ok(process::get_stderr_by_pid(pid).await?)
pub async fn process_get_stderr_by_uuid(uuid: Uuid) -> Result<String> {
Ok(process::get_stderr_by_uuid(&uuid).await?)
}
// Gets process stdout by process PID
// Gets process stdout by process UUID
#[tauri::command]
pub async fn process_get_stdout_by_pid(pid: u32) -> Result<String> {
Ok(process::get_stdout_by_pid(pid).await?)
pub async fn process_get_stdout_by_uuid(uuid: Uuid) -> Result<String> {
Ok(process::get_stdout_by_uuid(&uuid).await?)
}
// Kill a process by process PID
// Kill a process by process UUID
#[tauri::command]
pub async fn process_kill_by_pid(pid: u32) -> Result<()> {
Ok(process::kill_by_pid(pid).await?)
pub async fn process_kill_by_uuid(uuid: Uuid) -> Result<()> {
Ok(process::kill_by_uuid(&uuid).await?)
}
// Wait for a process to finish by process PID
// Wait for a process to finish by process UUID
#[tauri::command]
pub async fn process_wait_for_by_pid(pid: u32) -> Result<()> {
Ok(process::wait_for_by_pid(pid).await?)
pub async fn process_wait_for_by_uuid(uuid: Uuid) -> Result<()> {
Ok(process::wait_for_by_uuid(&uuid).await?)
}

View File

@@ -1,6 +1,7 @@
use crate::api::Result;
use std::path::{Path, PathBuf};
use theseus::prelude::*;
use uuid::Uuid;
// Remove a profile
// invoke('profile_add_path',path)
@@ -73,18 +74,14 @@ pub async fn profile_remove_project(
Ok(())
}
// Run minecraft using a profile using the default credentials
// Returns a u32 representing the PID, which can be used to poll
// Returns the UUID, which can be used to poll
// for the actual Child in the state.
// invoke('profile_run', path)
#[tauri::command]
pub async fn profile_run(path: &Path) -> Result<u32> {
let proc_lock = profile::run(path).await?;
let pid = proc_lock.read().await.child.id().ok_or_else(|| {
theseus::Error::from(theseus::ErrorKind::LauncherError(
"Process failed to stay open.".to_string(),
))
})?;
Ok(pid)
pub async fn profile_run(path: &Path) -> Result<Uuid> {
let minecraft_child = profile::run(path).await?;
let uuid = minecraft_child.read().await.uuid;
Ok(uuid)
}
// Run Minecraft using a profile using the default credentials, and wait for the result
@@ -97,21 +94,17 @@ pub async fn profile_run_wait(path: &Path) -> Result<()> {
}
// Run Minecraft using a profile using chosen credentials
// Returns a u32 representing the PID, which can be used to poll
// Returns the UUID, which can be used to poll
// for the actual Child in the state.
// invoke('profile_run_credentials', {path, credentials})')
#[tauri::command]
pub async fn profile_run_credentials(
path: &Path,
credentials: Credentials,
) -> Result<u32> {
let proc_lock = profile::run_credentials(path, &credentials).await?;
let pid = proc_lock.read().await.child.id().ok_or_else(|| {
theseus::Error::from(theseus::ErrorKind::LauncherError(
"Process failed to stay open.".to_string(),
))
})?;
Ok(pid)
) -> Result<Uuid> {
let minecraft_child = profile::run_credentials(path, &credentials).await?;
let uuid = minecraft_child.read().await.uuid;
Ok(uuid)
}
// Run Minecraft using a profile using the chosen credentials, and wait for the result

View File

@@ -2,14 +2,17 @@ use crate::api::Result;
use serde::{Deserialize, Serialize};
use theseus::prelude::*;
use super::TheseusSerializableError;
// Identical to theseus::settings::Settings except for the custom_java_args field
// This allows us to split the custom_java_args string into a Vec<String> here and join it back into a string in the backend
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FrontendSettings {
pub theme: Theme,
pub memory: MemorySettings,
pub game_resolution: WindowSize,
pub custom_java_args: String,
pub custom_env_args: Vec<(String, String)>,
pub custom_env_args: String,
pub java_globals: JavaGlobals,
pub default_user: Option<uuid::Uuid>,
pub hooks: Hooks,
@@ -23,10 +26,16 @@ pub struct FrontendSettings {
pub async fn settings_get() -> Result<FrontendSettings> {
let backend_settings = settings::get().await?;
let frontend_settings = FrontendSettings {
theme: backend_settings.theme,
memory: backend_settings.memory,
game_resolution: backend_settings.game_resolution,
custom_java_args: backend_settings.custom_java_args.join(" "),
custom_env_args: backend_settings.custom_env_args,
custom_env_args: backend_settings
.custom_env_args
.into_iter()
.map(|(s1, s2)| format!("{s1}={s2}"))
.collect::<Vec<String>>()
.join(" "),
java_globals: backend_settings.java_globals,
default_user: backend_settings.default_user,
hooks: backend_settings.hooks,
@@ -40,7 +49,25 @@ pub async fn settings_get() -> Result<FrontendSettings> {
// invoke('settings_set', settings)
#[tauri::command]
pub async fn settings_set(settings: FrontendSettings) -> Result<()> {
let custom_env_args: Vec<(String, String)> = settings
.custom_env_args
.split_whitespace()
.map(|s| s.to_string())
.map(|f| {
let mut split = f.split('=');
if let (Some(name), Some(value)) = (split.next(), split.next()) {
Ok((name.to_string(), value.to_string()))
} else {
Err(TheseusSerializableError::BadEnvVars(
"Invalid environment variable: {}".to_string(),
)
.into())
}
})
.collect::<Result<Vec<(String, String)>>>()?;
let backend_settings = Settings {
theme: settings.theme,
memory: settings.memory,
game_resolution: settings.game_resolution,
custom_java_args: settings
@@ -48,7 +75,7 @@ pub async fn settings_set(settings: FrontendSettings) -> Result<()> {
.split_whitespace()
.map(|s| s.to_string())
.collect(),
custom_env_args: settings.custom_env_args,
custom_env_args,
java_globals: settings.java_globals,
default_user: settings.default_user,
hooks: settings.hooks,

View File

@@ -58,17 +58,18 @@ fn main() {
api::jre::jre_validate_globals,
api::jre::jre_get_optimal_jre_key,
api::jre::jre_get_optimal_jre_key_by_path,
api::process::process_get_all_pids,
api::process::process_get_all_running_pids,
api::process::process_get_pids_by_profile_path,
api::jre::jre_get_jre,
api::process::process_get_all_uuids,
api::process::process_get_all_running_uuids,
api::process::process_get_uuids_by_profile_path,
api::process::process_get_all_running_profile_paths,
api::process::process_get_all_running_profiles,
api::process::process_get_exit_status_by_pid,
api::process::process_has_finished_by_pid,
api::process::process_get_stderr_by_pid,
api::process::process_get_stdout_by_pid,
api::process::process_kill_by_pid,
api::process::process_wait_for_by_pid,
api::process::process_get_exit_status_by_uuid,
api::process::process_has_finished_by_uuid,
api::process::process_get_stderr_by_uuid,
api::process::process_get_stdout_by_uuid,
api::process::process_kill_by_uuid,
api::process::process_wait_for_by_uuid,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@@ -44,6 +44,12 @@ export async function validate_globals() {
return await invoke('jre_validate_globals')
}
// Gets java version from a specific path by trying to run 'java -version' on it.
// This also validates it, as it returns null if no valid java version is found at the path
export async function get_jre(path) {
return await invoke('jre_get_jre', { path })
}
// Gets key for the optimal JRE to use, for a given profile path
// The key can be used in the hashmap contained by JavaGlobals in Settings (if it exists)
export async function get_optimal_jre_key_by_path(path) {
@@ -52,7 +58,7 @@ export async function get_optimal_jre_key_by_path(path) {
// Gets key for the optimal JRE to use, for a given profile
// The key can be used in the hashmap contained by JavaGlobals in Settings (if it exists)
export async function get_optimal_jre_ke(path) {
export async function get_optimal_jre_key(path) {
return await invoke('jre_get_optimal_jre_key', { path })
}

View File

@@ -5,34 +5,34 @@
*/
import { invoke } from '@tauri-apps/api/tauri'
/// Gets if a process has finished by PID
/// Gets if a process has finished by UUID
/// Returns bool
export async function has_finished_by_pid(pid) {
return await invoke('process_has_finished_by_pid', { pid })
export async function has_finished_by_uuid(uuid) {
return await invoke('process_has_finished_by_uuid', { uuid })
}
/// Gets process exit status by PID
/// Gets process exit status by UUID
/// Returns u32
export async function get_exit_status_by_pid(pid) {
return await invoke('process_get_exit_status_by_pid', { pid })
export async function get_exit_status_by_uuid(uuid) {
return await invoke('process_get_exit_status_by_uuid', { uuid })
}
/// Gets all process IDs
/// Returns [u32]
export async function get_all_pids() {
return await invoke('process_get_all_pids')
export async function get_all_uuids() {
return await invoke('process_get_all_uuids')
}
/// Gets all running process IDs
/// Returns [u32]
export async function get_all_running_pids() {
return await invoke('process_get_all_running_pids')
export async function get_all_running_uuids() {
return await invoke('process_get_all_running_uuids')
}
/// Gets all running process IDs with a given profile path
/// Returns [u32]
export async function get_pids_by_profile_path(profile_path) {
return await invoke('process_get_pids_by_profile_path', { profile_path })
export async function get_uuids_by_profile_path(profile_path) {
return await invoke('process_get_uuids_by_profile_path', { profile_path })
}
/// Gets all running process IDs with a given profile path
@@ -47,19 +47,19 @@ export async function get_all_running_profiles(profile_path) {
return await invoke('process_get_all_running_profiles', { profile_path })
}
/// Gets process stderr by PID
/// Gets process stderr by UUID
/// Returns String
export async function get_stderr_by_pid(pid) {
return await invoke('process_get_stderr_by_pid', { pid })
export async function get_stderr_by_uuid(uuid) {
return await invoke('process_get_stderr_by_uuid', { uuid })
}
/// Gets process stdout by PID
/// Gets process stdout by UUID
/// Returns String
export async function get_stdout_by_pid(pid) {
return await invoke('process_get_stdout_by_pid', { pid })
export async function get_stdout_by_uuid(uuid) {
return await invoke('process_get_stdout_by_uuid', { uuid })
}
/// Kills a process by PID
export async function kill_by_pid(pid) {
return await invoke('process_kill_by_pid', { pid })
/// Kills a process by UUID
export async function kill_by_uuid(uuid) {
return await invoke('process_kill_by_uuid', { uuid })
}

View File

@@ -6,12 +6,21 @@
import { invoke } from '@tauri-apps/api/tauri'
// Add empty default instance
export async function addDefaultInstance() {
export async function create_empty() {
return await invoke('profile_create_empty')
}
/// Creates instance
/// Returns a path to the profile created
/// Add instance
/*
name: String, // the name of the profile, and relative path to create
game_version: String, // the game version of the profile
modloader: ModLoader, // the modloader to use
- ModLoader is an enum, with the following variants: Vanilla, Forge, Fabric, Quilt
loader_version: String, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
icon: Path, // the icon for the profile
- icon is a path to an image file, which will be copied into the profile directory
*/
export async function create(name, game_version, modloader, loader_version, icon) {
return await invoke('profile_create', { name, game_version, modloader, loader_version, icon })
}

View File

@@ -33,6 +33,8 @@ async fn main() -> theseus::Result<()> {
// Initialize state
let st = State::get().await?;
st.settings.write().await.max_concurrent_downloads = 5;
st.settings.write().await.hooks.post_exit =
Some("echo This is after Minecraft runs- global setting!".to_string());
// Changed the settings, so need to reset the semaphore
st.reset_semaphore().await;
@@ -88,11 +90,11 @@ async fn main() -> theseus::Result<()> {
// (ie: changing the java runtime of an added profile)
println!("Editing.");
profile::edit(&profile_path, |_profile| {
// Eg: Java- this would let you change the java runtime of the profile instead of using the default
// use theseus::prelude::jre::JAVA__KEY;
// profile.java = Some(JavaSettings {
// jre_key: Some(JAVA_17_KEY.to_string()),
// extra_arguments: None,
// Add some hooks, for instance!
// profile.hooks = Some(Hooks {
// pre_launch: Some("echo This is before Minecraft runs!".to_string()),
// wrapper: None,
// post_exit: None,
// });
async { Ok(()) }
})
@@ -108,22 +110,22 @@ async fn main() -> theseus::Result<()> {
println!("running");
// Run a profile, running minecraft and store the RwLock to the process
let proc_lock = profile::run(&canonicalize(&profile_path)?).await?;
let uuid = proc_lock.read().await.uuid;
let pid = proc_lock.read().await.current_child.read().await.id();
let pid = proc_lock
.read()
.await
.child
.id()
.expect("Could not get PID from process.");
println!("Minecraft PID: {}", pid);
println!("Minecraft UUID: {}", uuid);
println!("Minecraft PID: {:?}", pid);
// Wait 5 seconds
println!("Waiting 5 seconds to gather logs...");
sleep(Duration::from_secs(5)).await;
let _stdout = process::get_stdout_by_pid(pid).await?;
let _stderr = process::get_stderr_by_pid(pid).await?;
// println!("Logs after 5sec <<< {stdout} >>> end stdout");
println!("Waiting 20 seconds to gather logs...");
sleep(Duration::from_secs(20)).await;
let stdout = process::get_stdout_by_uuid(&uuid).await?;
println!("Logs after 5sec <<< {stdout} >>> end stdout");
println!(
"All running process UUID {:?}",
process::get_all_running_uuids().await?
);
println!(
"All running process paths {:?}",
process::get_all_running_profile_paths().await?