You've already forked AstralRinth
forked from didirus/AstralRinth
Profile bindings (#55)
* basic framework. still has errors * added functionality for main endpoints + some structuring * formatting * unused code * mimicked CLI function with wait_for process * made PR changes, added playground * cargo fmt * removed missed println * misc tests fixes * cargo fmt * added windows support * cargo fmt * all OS use dunce * restructured profile slightly; fixed mac bug * profile changes, new main.rs * fixed requested pr + canonicaliation bug * fixed regressed bug in ui * fixed regressed bugs * fixed git error * typo * ran prettier * clippy * playground clippy * ported profile loading fix * profile change for real, url println and clippy * PR changes --------- Co-authored-by: Wyatt <wyatt@modrinth.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,6 +8,9 @@ WixTools
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.pnpm-debug.log
|
.pnpm-debug.log
|
||||||
|
|
||||||
|
minecraft
|
||||||
|
config
|
||||||
|
|
||||||
[#]*[#]
|
[#]*[#]
|
||||||
|
|
||||||
# TEMPORARY: ignore my test instance and metadata
|
# TEMPORARY: ignore my test instance and metadata
|
||||||
|
|||||||
27
Cargo.lock
generated
27
Cargo.lock
generated
@@ -3531,6 +3531,7 @@ dependencies = [
|
|||||||
"sys-info",
|
"sys-info",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
"toml 0.7.3",
|
"toml 0.7.3",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-error",
|
"tracing-error",
|
||||||
@@ -3549,6 +3550,7 @@ dependencies = [
|
|||||||
"daedalus",
|
"daedalus",
|
||||||
"dialoguer",
|
"dialoguer",
|
||||||
"dirs",
|
"dirs",
|
||||||
|
"dunce",
|
||||||
"eyre",
|
"eyre",
|
||||||
"futures",
|
"futures",
|
||||||
"paris",
|
"paris",
|
||||||
@@ -3563,16 +3565,41 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
"uuid 1.3.0",
|
"uuid 1.3.0",
|
||||||
"webbrowser",
|
"webbrowser",
|
||||||
|
"winreg 0.11.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "theseus_gui"
|
name = "theseus_gui"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"daedalus",
|
||||||
|
"futures",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"theseus",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "theseus_playground"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"daedalus",
|
||||||
|
"dunce",
|
||||||
|
"futures",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"theseus",
|
||||||
|
"thiserror",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"url",
|
||||||
|
"webbrowser",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
members = [
|
members = [
|
||||||
"theseus",
|
"theseus",
|
||||||
"theseus_cli",
|
"theseus_cli",
|
||||||
|
"theseus_playground",
|
||||||
"theseus_gui/src-tauri"
|
"theseus_gui/src-tauri"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -36,11 +36,13 @@ futures = "0.3"
|
|||||||
once_cell = "1.9.0"
|
once_cell = "1.9.0"
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||||
|
|
||||||
lazy_static = "1.4.0"
|
lazy_static = "1.4.0"
|
||||||
|
dunce = "1.0.3"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
winreg = "0.11.0"
|
winreg = "0.11.0"
|
||||||
dunce = "1.0.3"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
argh = "0.1.6"
|
argh = "0.1.6"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! API for interacting with Theseus
|
//! API for interacting with Theseus
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
|
pub mod profile_create;
|
||||||
|
|
||||||
pub mod data {
|
pub mod data {
|
||||||
pub use crate::state::{
|
pub use crate::state::{
|
||||||
@@ -14,6 +15,6 @@ pub mod prelude {
|
|||||||
auth::{self, Credentials},
|
auth::{self, Credentials},
|
||||||
data::*,
|
data::*,
|
||||||
profile::{self, Profile},
|
profile::{self, Profile},
|
||||||
State,
|
profile_create, State,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,12 @@ use daedalus as d;
|
|||||||
use std::{
|
use std::{
|
||||||
future::Future,
|
future::Future,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use tokio::{
|
||||||
|
process::{Child, Command},
|
||||||
|
sync::RwLock,
|
||||||
};
|
};
|
||||||
use tokio::process::{Child, Command};
|
|
||||||
|
|
||||||
/// Add a profile to the in-memory state
|
/// Add a profile to the in-memory state
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
@@ -105,11 +109,12 @@ pub async fn list(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Run Minecraft using a profile
|
/// Run Minecraft using a profile
|
||||||
|
/// Returns Arc pointer to RwLock to Child
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
credentials: &crate::auth::Credentials,
|
credentials: &crate::auth::Credentials,
|
||||||
) -> crate::Result<Child> {
|
) -> crate::Result<Arc<RwLock<Child>>> {
|
||||||
let state = State::get().await.unwrap();
|
let state = State::get().await.unwrap();
|
||||||
let settings = state.settings.read().await;
|
let settings = state.settings.read().await;
|
||||||
let profile = get(path).await?.ok_or_else(|| {
|
let profile = get(path).await?.ok_or_else(|| {
|
||||||
@@ -199,7 +204,7 @@ pub async fn run(
|
|||||||
let memory = profile.memory.unwrap_or(settings.memory);
|
let memory = profile.memory.unwrap_or(settings.memory);
|
||||||
let resolution = profile.resolution.unwrap_or(settings.game_resolution);
|
let resolution = profile.resolution.unwrap_or(settings.game_resolution);
|
||||||
|
|
||||||
crate::launcher::launch_minecraft(
|
let mc_process = crate::launcher::launch_minecraft(
|
||||||
&profile.metadata.game_version,
|
&profile.metadata.game_version,
|
||||||
&profile.metadata.loader_version,
|
&profile.metadata.loader_version,
|
||||||
&profile.path,
|
&profile.path,
|
||||||
@@ -210,7 +215,18 @@ pub async fn run(
|
|||||||
&resolution,
|
&resolution,
|
||||||
credentials,
|
credentials,
|
||||||
)
|
)
|
||||||
.await
|
.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 child_arc = state_children.insert(pid, mc_process);
|
||||||
|
|
||||||
|
Ok(child_arc)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
|
|||||||
165
theseus/src/api/profile_create.rs
Normal file
165
theseus/src/api/profile_create.rs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
//! Theseus profile management interface
|
||||||
|
use crate::{prelude::ModLoader, profile};
|
||||||
|
pub use crate::{
|
||||||
|
state::{JavaSettings, Profile},
|
||||||
|
State,
|
||||||
|
};
|
||||||
|
use daedalus::modded::LoaderVersion;
|
||||||
|
use dunce::canonicalize;
|
||||||
|
use futures::prelude::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tokio::fs;
|
||||||
|
use tokio_stream::wrappers::ReadDirStream;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
const DEFAULT_NAME: &str = "Untitled Instance";
|
||||||
|
|
||||||
|
// Generic basic profile creation tool.
|
||||||
|
// Creates an essentially empty dummy profile with profile_create
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn profile_create_empty() -> crate::Result<PathBuf> {
|
||||||
|
profile_create(
|
||||||
|
String::from(DEFAULT_NAME), // the name/path of the profile
|
||||||
|
String::from("1.19.2"), // the game version of the profile
|
||||||
|
ModLoader::Vanilla, // the modloader to use
|
||||||
|
String::from("stable"), // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
|
||||||
|
None, // the icon for the profile
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a profile at the given filepath and adds it to the in-memory state
|
||||||
|
// Returns filepath at which it can be accessed in the State
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn profile_create(
|
||||||
|
name: String, // the name of the profile, and relative path
|
||||||
|
game_version: String, // the game version of the profile
|
||||||
|
modloader: ModLoader, // the modloader to use
|
||||||
|
loader_version: String, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
|
||||||
|
icon: Option<PathBuf>, // the icon for the profile
|
||||||
|
) -> crate::Result<PathBuf> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
|
||||||
|
let uuid = Uuid::new_v4();
|
||||||
|
let path = state.directories.profiles_dir().join(uuid.to_string());
|
||||||
|
|
||||||
|
if path.exists() {
|
||||||
|
if !path.is_dir() {
|
||||||
|
return Err(ProfileCreationError::NotFolder.into());
|
||||||
|
}
|
||||||
|
if path.join("profile.json").exists() {
|
||||||
|
return Err(ProfileCreationError::ProfileExistsError(
|
||||||
|
path.join("profile.json"),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ReadDirStream::new(fs::read_dir(&path).await?)
|
||||||
|
.next()
|
||||||
|
.await
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Err(ProfileCreationError::NotEmptyFolder.into());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs::create_dir_all(&path).await?;
|
||||||
|
}
|
||||||
|
println!(
|
||||||
|
"Creating profile at path {}",
|
||||||
|
&canonicalize(&path)?.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
let loader = modloader;
|
||||||
|
let loader = if loader != ModLoader::Vanilla {
|
||||||
|
let version = loader_version;
|
||||||
|
|
||||||
|
let filter = |it: &LoaderVersion| match version.as_str() {
|
||||||
|
"latest" => true,
|
||||||
|
"stable" => it.stable,
|
||||||
|
id => it.id == *id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let loader_data = match loader {
|
||||||
|
ModLoader::Forge => &state.metadata.forge,
|
||||||
|
ModLoader::Fabric => &state.metadata.fabric,
|
||||||
|
_ => {
|
||||||
|
return Err(ProfileCreationError::NoManifest(
|
||||||
|
loader.to_string(),
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let loaders = &loader_data
|
||||||
|
.game_versions
|
||||||
|
.iter()
|
||||||
|
.find(|it| it.id == game_version)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ProfileCreationError::ModloaderUnsupported(
|
||||||
|
loader.to_string(),
|
||||||
|
game_version.clone(),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.loaders;
|
||||||
|
|
||||||
|
let loader_version = loaders
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.find(filter)
|
||||||
|
.or(
|
||||||
|
// If stable was searched for but not found, return latest by default
|
||||||
|
if version == "stable" {
|
||||||
|
loaders.iter().next().cloned()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ProfileCreationError::InvalidVersionModloader(
|
||||||
|
version,
|
||||||
|
loader.to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Some((loader_version, loader))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fully canonicalize now that its created for storing purposes
|
||||||
|
let path = canonicalize(&path)?;
|
||||||
|
let mut profile = Profile::new(name, game_version, path.clone()).await?;
|
||||||
|
if let Some(ref icon) = icon {
|
||||||
|
profile.with_icon(icon).await?;
|
||||||
|
}
|
||||||
|
if let Some((loader_version, loader)) = loader {
|
||||||
|
profile.with_loader(loader, Some(loader_version));
|
||||||
|
}
|
||||||
|
|
||||||
|
profile::add(profile).await?;
|
||||||
|
State::sync().await?;
|
||||||
|
|
||||||
|
Ok(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum ProfileCreationError {
|
||||||
|
#[error("Profile .json exists: {0}")]
|
||||||
|
ProfileExistsError(PathBuf),
|
||||||
|
#[error("Modloader {0} unsupported for Minecraft version {1}")]
|
||||||
|
ModloaderUnsupported(String, String),
|
||||||
|
#[error("Invalid version {0} for modloader {1}")]
|
||||||
|
InvalidVersionModloader(String, String),
|
||||||
|
#[error("Could not get manifest for loader {0}. This is a bug in the GUI")]
|
||||||
|
NoManifest(String),
|
||||||
|
#[error("Could not get State.")]
|
||||||
|
NoState,
|
||||||
|
|
||||||
|
#[error("Attempted to create project in something other than a folder.")]
|
||||||
|
NotFolder,
|
||||||
|
#[error("You are trying to create a profile in a non-empty directory")]
|
||||||
|
NotEmptyFolder,
|
||||||
|
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
IOError(#[from] std::io::Error),
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
//! Theseus error type
|
//! Theseus error type
|
||||||
use tracing_error::InstrumentError;
|
use tracing_error::InstrumentError;
|
||||||
|
|
||||||
|
use crate::profile_create;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum ErrorKind {
|
pub enum ErrorKind {
|
||||||
#[error("Filesystem error: {0}")]
|
#[error("Filesystem error: {0}")]
|
||||||
@@ -57,6 +59,9 @@ pub enum ErrorKind {
|
|||||||
#[error("Invalid input: {0}")]
|
#[error("Invalid input: {0}")]
|
||||||
InputError(String),
|
InputError(String),
|
||||||
|
|
||||||
|
#[error("Recv error: {0}")]
|
||||||
|
RecvError(#[from] tokio::sync::oneshot::error::RecvError),
|
||||||
|
|
||||||
#[error(
|
#[error(
|
||||||
"Tried to access unloaded profile {0}, loading it probably failed"
|
"Tried to access unloaded profile {0}, loading it probably failed"
|
||||||
)]
|
)]
|
||||||
@@ -65,6 +70,9 @@ pub enum ErrorKind {
|
|||||||
#[error("Profile {0} is not managed by Theseus!")]
|
#[error("Profile {0} is not managed by Theseus!")]
|
||||||
UnmanagedProfileError(String),
|
UnmanagedProfileError(String),
|
||||||
|
|
||||||
|
#[error("Could not create profile: {0}")]
|
||||||
|
ProfileCreationError(#[from] profile_create::ProfileCreationError),
|
||||||
|
|
||||||
#[error("Error: {0}")]
|
#[error("Error: {0}")]
|
||||||
OtherError(String),
|
OtherError(String),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,14 @@ use daedalus::{
|
|||||||
minecraft::{Argument, ArgumentValue, Library, VersionType},
|
minecraft::{Argument, ArgumentValue, Library, VersionType},
|
||||||
modded::SidedDataEntry,
|
modded::SidedDataEntry,
|
||||||
};
|
};
|
||||||
|
use dunce::canonicalize;
|
||||||
use std::io::{BufRead, BufReader};
|
use std::io::{BufRead, BufReader};
|
||||||
use std::{collections::HashMap, path::Path};
|
use std::{collections::HashMap, path::Path};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
// Replaces the space separator with a newline character, as to not split the arguments
|
||||||
|
const TEMPORARY_REPLACE_CHAR: &str = "\n";
|
||||||
|
|
||||||
pub fn get_class_paths(
|
pub fn get_class_paths(
|
||||||
libraries_path: &Path,
|
libraries_path: &Path,
|
||||||
libraries: &[Library],
|
libraries: &[Library],
|
||||||
@@ -37,8 +41,7 @@ pub fn get_class_paths(
|
|||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
cps.push(
|
cps.push(
|
||||||
client_path
|
canonicalize(client_path)
|
||||||
.canonicalize()
|
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
crate::ErrorKind::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Specified class path {} does not exist",
|
"Specified class path {} does not exist",
|
||||||
@@ -70,7 +73,7 @@ pub fn get_lib_path(libraries_path: &Path, lib: &str) -> crate::Result<String> {
|
|||||||
|
|
||||||
path.push(get_path_from_artifact(lib)?);
|
path.push(get_path_from_artifact(lib)?);
|
||||||
|
|
||||||
let path = &path.canonicalize().map_err(|_| {
|
let path = &canonicalize(&path).map_err(|_| {
|
||||||
crate::ErrorKind::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Library file at path {} does not exist",
|
"Library file at path {} does not exist",
|
||||||
path.to_string_lossy()
|
path.to_string_lossy()
|
||||||
@@ -104,15 +107,13 @@ pub fn get_jvm_arguments(
|
|||||||
} else {
|
} else {
|
||||||
parsed_arguments.push(format!(
|
parsed_arguments.push(format!(
|
||||||
"-Djava.library.path={}",
|
"-Djava.library.path={}",
|
||||||
&natives_path
|
canonicalize(natives_path)
|
||||||
.canonicalize()
|
|
||||||
.map_err(|_| crate::ErrorKind::LauncherError(format!(
|
.map_err(|_| crate::ErrorKind::LauncherError(format!(
|
||||||
"Specified natives path {} does not exist",
|
"Specified natives path {} does not exist",
|
||||||
natives_path.to_string_lossy()
|
natives_path.to_string_lossy()
|
||||||
))
|
))
|
||||||
.as_error())?
|
.as_error())?
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string()
|
|
||||||
));
|
));
|
||||||
parsed_arguments.push("-cp".to_string());
|
parsed_arguments.push("-cp".to_string());
|
||||||
parsed_arguments.push(class_paths.to_string());
|
parsed_arguments.push(class_paths.to_string());
|
||||||
@@ -142,8 +143,7 @@ fn parse_jvm_argument(
|
|||||||
Ok(argument
|
Ok(argument
|
||||||
.replace(
|
.replace(
|
||||||
"${natives_directory}",
|
"${natives_directory}",
|
||||||
&natives_path
|
&canonicalize(natives_path)
|
||||||
.canonicalize()
|
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
crate::ErrorKind::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Specified natives path {} does not exist",
|
"Specified natives path {} does not exist",
|
||||||
@@ -155,8 +155,7 @@ fn parse_jvm_argument(
|
|||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"${library_directory}",
|
"${library_directory}",
|
||||||
&libraries_path
|
&canonicalize(libraries_path)
|
||||||
.canonicalize()
|
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
crate::ErrorKind::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Specified libraries path {} does not exist",
|
"Specified libraries path {} does not exist",
|
||||||
@@ -206,7 +205,7 @@ pub fn get_minecraft_arguments(
|
|||||||
Ok(parsed_arguments)
|
Ok(parsed_arguments)
|
||||||
} else if let Some(legacy_arguments) = legacy_arguments {
|
} else if let Some(legacy_arguments) = legacy_arguments {
|
||||||
Ok(parse_minecraft_argument(
|
Ok(parse_minecraft_argument(
|
||||||
legacy_arguments,
|
&legacy_arguments.replace(' ', TEMPORARY_REPLACE_CHAR),
|
||||||
&credentials.access_token,
|
&credentials.access_token,
|
||||||
&credentials.username,
|
&credentials.username,
|
||||||
&credentials.id,
|
&credentials.id,
|
||||||
@@ -249,8 +248,7 @@ fn parse_minecraft_argument(
|
|||||||
.replace("${assets_index_name}", asset_index_name)
|
.replace("${assets_index_name}", asset_index_name)
|
||||||
.replace(
|
.replace(
|
||||||
"${game_directory}",
|
"${game_directory}",
|
||||||
&game_directory
|
&canonicalize(game_directory)
|
||||||
.canonicalize()
|
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
crate::ErrorKind::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Specified game directory {} does not exist",
|
"Specified game directory {} does not exist",
|
||||||
@@ -262,8 +260,7 @@ fn parse_minecraft_argument(
|
|||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"${assets_root}",
|
"${assets_root}",
|
||||||
&assets_directory
|
&canonicalize(assets_directory)
|
||||||
.canonicalize()
|
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
crate::ErrorKind::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Specified assets directory {} does not exist",
|
"Specified assets directory {} does not exist",
|
||||||
@@ -275,8 +272,7 @@ fn parse_minecraft_argument(
|
|||||||
)
|
)
|
||||||
.replace(
|
.replace(
|
||||||
"${game_assets}",
|
"${game_assets}",
|
||||||
&assets_directory
|
&canonicalize(assets_directory)
|
||||||
.canonicalize()
|
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
crate::ErrorKind::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Specified assets directory {} does not exist",
|
"Specified assets directory {} does not exist",
|
||||||
@@ -302,9 +298,9 @@ where
|
|||||||
for argument in arguments {
|
for argument in arguments {
|
||||||
match argument {
|
match argument {
|
||||||
Argument::Normal(arg) => {
|
Argument::Normal(arg) => {
|
||||||
let parsed = parse_function(arg)?;
|
let parsed =
|
||||||
|
parse_function(&arg.replace(' ', TEMPORARY_REPLACE_CHAR))?;
|
||||||
for arg in parsed.split(' ') {
|
for arg in parsed.split(TEMPORARY_REPLACE_CHAR) {
|
||||||
parsed_arguments.push(arg.to_string());
|
parsed_arguments.push(arg.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,11 +308,15 @@ where
|
|||||||
if rules.iter().all(parse_rule) {
|
if rules.iter().all(parse_rule) {
|
||||||
match value {
|
match value {
|
||||||
ArgumentValue::Single(arg) => {
|
ArgumentValue::Single(arg) => {
|
||||||
parsed_arguments.push(parse_function(arg)?);
|
parsed_arguments.push(parse_function(
|
||||||
|
&arg.replace(' ', TEMPORARY_REPLACE_CHAR),
|
||||||
|
)?);
|
||||||
}
|
}
|
||||||
ArgumentValue::Many(args) => {
|
ArgumentValue::Many(args) => {
|
||||||
for arg in args {
|
for arg in args {
|
||||||
parsed_arguments.push(parse_function(arg)?);
|
parsed_arguments.push(parse_function(
|
||||||
|
&arg.replace(' ', TEMPORARY_REPLACE_CHAR),
|
||||||
|
)?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use bincode::{Decode, Encode};
|
|||||||
use chrono::{prelude::*, Duration};
|
use chrono::{prelude::*, Duration};
|
||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
@@ -47,7 +47,7 @@ struct ProfileInfoJSON {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Login information
|
// Login information
|
||||||
#[derive(Encode, Decode)]
|
#[derive(Encode, Decode, Serialize, Deserialize)]
|
||||||
pub struct Credentials {
|
pub struct Credentials {
|
||||||
#[bincode(with_serde)]
|
#[bincode(with_serde)]
|
||||||
pub id: uuid::Uuid,
|
pub id: uuid::Uuid,
|
||||||
|
|||||||
@@ -139,10 +139,9 @@ pub async fn download_assets(
|
|||||||
index: &AssetsIndex,
|
index: &AssetsIndex,
|
||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
log::debug!("Loading assets");
|
log::debug!("Loading assets");
|
||||||
|
|
||||||
stream::iter(index.objects.iter())
|
stream::iter(index.objects.iter())
|
||||||
.map(Ok::<(&String, &Asset), crate::Error>)
|
.map(Ok::<(&String, &Asset), crate::Error>)
|
||||||
.try_for_each_concurrent(None, |(name, asset)| async move {
|
.try_for_each_concurrent(Some(st.settings.read().await.max_concurrent_downloads), |(name, asset)| async move {
|
||||||
let hash = &asset.hash;
|
let hash = &asset.hash;
|
||||||
let resource_path = st.directories.object_dir(hash);
|
let resource_path = st.directories.object_dir(hash);
|
||||||
let url = format!(
|
let url = format!(
|
||||||
@@ -202,7 +201,7 @@ pub async fn download_libraries(
|
|||||||
|
|
||||||
stream::iter(libraries.iter())
|
stream::iter(libraries.iter())
|
||||||
.map(Ok::<&Library, crate::Error>)
|
.map(Ok::<&Library, crate::Error>)
|
||||||
.try_for_each_concurrent(None, |library| async move {
|
.try_for_each_concurrent(Some(st.settings.read().await.max_concurrent_downloads), |library| async move {
|
||||||
if let Some(rules) = &library.rules {
|
if let Some(rules) = &library.rules {
|
||||||
if !rules.iter().all(super::parse_rule) {
|
if !rules.iter().all(super::parse_rule) {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
//! Logic for launching Minecraft
|
//! Logic for launching Minecraft
|
||||||
use crate::state as st;
|
use crate::state as st;
|
||||||
use daedalus as d;
|
use daedalus as d;
|
||||||
|
use dunce::canonicalize;
|
||||||
use std::{path::Path, process::Stdio};
|
use std::{path::Path, process::Stdio};
|
||||||
use tokio::process::{Child, Command};
|
use tokio::process::{Child, Command};
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ pub async fn launch_minecraft(
|
|||||||
credentials: &auth::Credentials,
|
credentials: &auth::Credentials,
|
||||||
) -> crate::Result<Child> {
|
) -> crate::Result<Child> {
|
||||||
let state = st::State::get().await?;
|
let state = st::State::get().await?;
|
||||||
let instance_path = instance_path.canonicalize()?;
|
let instance_path = &canonicalize(instance_path)?;
|
||||||
|
|
||||||
let version = state
|
let version = state
|
||||||
.metadata
|
.metadata
|
||||||
@@ -173,34 +174,45 @@ pub async fn launch_minecraft(
|
|||||||
};
|
};
|
||||||
|
|
||||||
command
|
command
|
||||||
.args(args::get_jvm_arguments(
|
.args(
|
||||||
args.get(&d::minecraft::ArgumentType::Jvm)
|
args::get_jvm_arguments(
|
||||||
.map(|x| x.as_slice()),
|
args.get(&d::minecraft::ArgumentType::Jvm)
|
||||||
&state.directories.version_natives_dir(&version.id),
|
.map(|x| x.as_slice()),
|
||||||
&state.directories.libraries_dir(),
|
&state.directories.version_natives_dir(&version.id),
|
||||||
&args::get_class_paths(
|
|
||||||
&state.directories.libraries_dir(),
|
&state.directories.libraries_dir(),
|
||||||
version_info.libraries.as_slice(),
|
&args::get_class_paths(
|
||||||
&client_path,
|
&state.directories.libraries_dir(),
|
||||||
)?,
|
version_info.libraries.as_slice(),
|
||||||
&version_jar,
|
&client_path,
|
||||||
*memory,
|
)?,
|
||||||
Vec::from(java_args),
|
&version_jar,
|
||||||
)?)
|
*memory,
|
||||||
|
Vec::from(java_args),
|
||||||
|
)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| r.replace(' ', r"\ "))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
.arg(version_info.main_class.clone())
|
.arg(version_info.main_class.clone())
|
||||||
.args(args::get_minecraft_arguments(
|
.args(
|
||||||
args.get(&d::minecraft::ArgumentType::Game)
|
args::get_minecraft_arguments(
|
||||||
.map(|x| x.as_slice()),
|
args.get(&d::minecraft::ArgumentType::Game)
|
||||||
version_info.minecraft_arguments.as_deref(),
|
.map(|x| x.as_slice()),
|
||||||
credentials,
|
version_info.minecraft_arguments.as_deref(),
|
||||||
&version.id,
|
credentials,
|
||||||
&version_info.asset_index.id,
|
&version.id,
|
||||||
&instance_path,
|
&version_info.asset_index.id,
|
||||||
&state.directories.assets_dir(),
|
instance_path,
|
||||||
&version.type_,
|
&state.directories.assets_dir(),
|
||||||
*resolution,
|
&version.type_,
|
||||||
)?)
|
*resolution,
|
||||||
|
)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| r.replace(' ', r"\ "))
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
)
|
||||||
.current_dir(instance_path.clone())
|
.current_dir(instance_path.clone())
|
||||||
|
.env_clear()
|
||||||
.stdout(Stdio::inherit())
|
.stdout(Stdio::inherit())
|
||||||
.stderr(Stdio::inherit());
|
.stderr(Stdio::inherit());
|
||||||
|
|
||||||
|
|||||||
37
theseus/src/state/children.rs
Normal file
37
theseus/src/state/children.rs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
use tokio::process::Child;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
// Child processes (instances of Minecraft)
|
||||||
|
// A wrapper over a Hashmap connecting PID -> Child
|
||||||
|
// Left open for future functionality re: polling children
|
||||||
|
pub struct Children(HashMap<u32, Arc<RwLock<Child>>>);
|
||||||
|
|
||||||
|
impl Children {
|
||||||
|
pub fn new() -> Children {
|
||||||
|
Children(HashMap::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inserts and returns a ref to the child
|
||||||
|
// Unlike a Hashmap, this directly returns the reference to the Child rather than any previously stored Child that may exist
|
||||||
|
pub fn insert(
|
||||||
|
&mut self,
|
||||||
|
pid: u32,
|
||||||
|
child: tokio::process::Child,
|
||||||
|
) -> Arc<RwLock<Child>> {
|
||||||
|
let child = Arc::new(RwLock::new(child));
|
||||||
|
self.0.insert(pid, child.clone());
|
||||||
|
child
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a ref to the child
|
||||||
|
pub fn get(&self, pid: &u32) -> Option<Arc<RwLock<Child>>> {
|
||||||
|
self.0.get(pid).cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Children {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -110,6 +110,12 @@ impl DirectoryInfo {
|
|||||||
self.config_dir.join("icons")
|
self.config_dir.join("icons")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the profiles directory for created profiles
|
||||||
|
#[inline]
|
||||||
|
pub fn profiles_dir(&self) -> PathBuf {
|
||||||
|
self.config_dir.join("profiles")
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the file containing the global database
|
/// Get the file containing the global database
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn database_file(&self) -> PathBuf {
|
pub fn database_file(&self) -> PathBuf {
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ pub use self::projects::*;
|
|||||||
mod users;
|
mod users;
|
||||||
pub use self::users::*;
|
pub use self::users::*;
|
||||||
|
|
||||||
|
mod children;
|
||||||
|
pub use self::children::*;
|
||||||
|
|
||||||
// Global state
|
// Global state
|
||||||
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
|
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
|
||||||
pub struct State {
|
pub struct State {
|
||||||
@@ -36,6 +39,8 @@ pub struct State {
|
|||||||
// TODO: settings API
|
// TODO: settings API
|
||||||
/// Launcher configuration
|
/// Launcher configuration
|
||||||
pub settings: RwLock<Settings>,
|
pub settings: RwLock<Settings>,
|
||||||
|
/// Reference to process children
|
||||||
|
pub children: RwLock<Children>,
|
||||||
/// Launcher profile metadata
|
/// Launcher profile metadata
|
||||||
pub(crate) profiles: RwLock<Profiles>,
|
pub(crate) profiles: RwLock<Profiles>,
|
||||||
/// Launcher user account info
|
/// Launcher user account info
|
||||||
@@ -73,6 +78,8 @@ impl State {
|
|||||||
let io_semaphore =
|
let io_semaphore =
|
||||||
Semaphore::new(settings.max_concurrent_downloads);
|
Semaphore::new(settings.max_concurrent_downloads);
|
||||||
|
|
||||||
|
let children = Children::new();
|
||||||
|
|
||||||
Ok(Arc::new(Self {
|
Ok(Arc::new(Self {
|
||||||
database,
|
database,
|
||||||
directories,
|
directories,
|
||||||
@@ -81,6 +88,7 @@ impl State {
|
|||||||
settings: RwLock::new(settings),
|
settings: RwLock::new(settings),
|
||||||
profiles: RwLock::new(profiles),
|
profiles: RwLock::new(profiles),
|
||||||
users: RwLock::new(users),
|
users: RwLock::new(users),
|
||||||
|
children: RwLock::new(children),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::config::BINCODE_CONFIG;
|
|||||||
use crate::data::DirectoryInfo;
|
use crate::data::DirectoryInfo;
|
||||||
use crate::state::projects::Project;
|
use crate::state::projects::Project;
|
||||||
use daedalus::modded::LoaderVersion;
|
use daedalus::modded::LoaderVersion;
|
||||||
|
use dunce::canonicalize;
|
||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
@@ -98,7 +99,7 @@ impl Profile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
path: path.canonicalize()?,
|
path: canonicalize(path)?,
|
||||||
metadata: ProfileMetadata {
|
metadata: ProfileMetadata {
|
||||||
name,
|
name,
|
||||||
icon: None,
|
icon: None,
|
||||||
@@ -236,10 +237,13 @@ impl Profiles {
|
|||||||
{
|
{
|
||||||
for (profile_path, _profile_opt) in profiles.iter() {
|
for (profile_path, _profile_opt) in profiles.iter() {
|
||||||
let mut read_paths = |path: &str| {
|
let mut read_paths = |path: &str| {
|
||||||
for path in std::fs::read_dir(profile_path.join(path))? {
|
let new_path = profile_path.join(path);
|
||||||
files.insert(path?.path(), profile_path.clone());
|
if new_path.exists() {
|
||||||
|
for path in std::fs::read_dir(profile_path.join(path))?
|
||||||
|
{
|
||||||
|
files.insert(path?.path(), profile_path.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok::<(), crate::Error>(())
|
Ok::<(), crate::Error>(())
|
||||||
};
|
};
|
||||||
read_paths("mods")?;
|
read_paths("mods")?;
|
||||||
@@ -268,9 +272,7 @@ impl Profiles {
|
|||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
|
pub fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
|
||||||
self.0.insert(
|
self.0.insert(
|
||||||
profile
|
canonicalize(&profile.path)?
|
||||||
.path
|
|
||||||
.canonicalize()?
|
|
||||||
.to_str()
|
.to_str()
|
||||||
.ok_or(
|
.ok_or(
|
||||||
crate::ErrorKind::UTFError(profile.path.clone()).as_error(),
|
crate::ErrorKind::UTFError(profile.path.clone()).as_error(),
|
||||||
@@ -286,12 +288,12 @@ impl Profiles {
|
|||||||
&'a mut self,
|
&'a mut self,
|
||||||
path: &'a Path,
|
path: &'a Path,
|
||||||
) -> crate::Result<&Self> {
|
) -> crate::Result<&Self> {
|
||||||
self.insert(Self::read_profile_from_dir(&path.canonicalize()?).await?)
|
self.insert(Self::read_profile_from_dir(&canonicalize(path)?).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument(skip(self))]
|
||||||
pub fn remove(&mut self, path: &Path) -> crate::Result<&Self> {
|
pub fn remove(&mut self, path: &Path) -> crate::Result<&Self> {
|
||||||
let path = PathBuf::from(path.canonicalize()?.to_str().unwrap());
|
let path = PathBuf::from(&canonicalize(path)?.to_str().unwrap());
|
||||||
self.0.remove(&path);
|
self.0.remove(&path);
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use dunce::canonicalize;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
@@ -11,12 +12,6 @@ use winreg::{
|
|||||||
RegKey,
|
RegKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Uses dunce canonicalization to resolve symlinks without UNC prefixes
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
use dunce::canonicalize;
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
|
||||||
use std::fs::canonicalize;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct JavaVersion {
|
pub struct JavaVersion {
|
||||||
pub path: String,
|
pub path: String,
|
||||||
|
|||||||
@@ -26,5 +26,9 @@ tracing = "0.1"
|
|||||||
tracing-error = "0.2"
|
tracing-error = "0.2"
|
||||||
tracing-futures = "0.2"
|
tracing-futures = "0.2"
|
||||||
tracing-subscriber = {version = "0.3", features = ["env-filter"]}
|
tracing-subscriber = {version = "0.3", features = ["env-filter"]}
|
||||||
|
dunce = "1.0.3"
|
||||||
|
|
||||||
webbrowser = "0.7"
|
webbrowser = "0.7"
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
winreg = "0.11.0"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::util::{
|
|||||||
confirm_async, prompt_async, select_async, table, table_path_display,
|
confirm_async, prompt_async, select_async, table, table_path_display,
|
||||||
};
|
};
|
||||||
use daedalus::modded::LoaderVersion;
|
use daedalus::modded::LoaderVersion;
|
||||||
|
use dunce::canonicalize;
|
||||||
use eyre::{ensure, Result};
|
use eyre::{ensure, Result};
|
||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
use paris::*;
|
use paris::*;
|
||||||
@@ -50,7 +51,7 @@ impl ProfileAdd {
|
|||||||
self.profile.display()
|
self.profile.display()
|
||||||
);
|
);
|
||||||
|
|
||||||
let profile = self.profile.canonicalize()?;
|
let profile = canonicalize(&self.profile)?;
|
||||||
let json_path = profile.join("profile.json");
|
let json_path = profile.join("profile.json");
|
||||||
|
|
||||||
ensure!(
|
ensure!(
|
||||||
@@ -137,7 +138,7 @@ impl ProfileInit {
|
|||||||
}
|
}
|
||||||
info!(
|
info!(
|
||||||
"Creating profile at path {}",
|
"Creating profile at path {}",
|
||||||
&self.path.canonicalize()?.display()
|
&canonicalize(&self.path)?.display()
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO: abstract default prompting
|
// TODO: abstract default prompting
|
||||||
@@ -343,7 +344,7 @@ impl ProfileRemove {
|
|||||||
_args: &crate::Args,
|
_args: &crate::Args,
|
||||||
_largs: &ProfileCommand,
|
_largs: &ProfileCommand,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let profile = self.profile.canonicalize()?;
|
let profile = canonicalize(&self.profile)?;
|
||||||
info!("Removing profile {} from Theseus", self.profile.display());
|
info!("Removing profile {} from Theseus", self.profile.display());
|
||||||
|
|
||||||
if confirm_async(String::from("Do you wish to continue"), true).await? {
|
if confirm_async(String::from("Do you wish to continue"), true).await? {
|
||||||
@@ -382,7 +383,7 @@ impl ProfileRun {
|
|||||||
_largs: &ProfileCommand,
|
_largs: &ProfileCommand,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
info!("Starting profile at path {}...", self.profile.display());
|
info!("Starting profile at path {}...", self.profile.display());
|
||||||
let path = self.profile.canonicalize()?;
|
let path = canonicalize(&self.profile)?;
|
||||||
|
|
||||||
ensure!(
|
ensure!(
|
||||||
profile::is_managed(&path).await?,
|
profile::is_managed(&path).await?,
|
||||||
@@ -402,7 +403,8 @@ impl ProfileRun {
|
|||||||
.await?;
|
.await?;
|
||||||
let credentials = auth::refresh(id, false).await?;
|
let credentials = auth::refresh(id, false).await?;
|
||||||
|
|
||||||
let mut proc = profile::run(&path, &credentials).await?;
|
let proc_lock = profile::run(&path, &credentials).await?;
|
||||||
|
let mut proc = proc_lock.write().await;
|
||||||
profile::wait_for(&mut proc).await?;
|
profile::wait_for(&mut proc).await?;
|
||||||
|
|
||||||
success!("Process exited successfully!");
|
success!("Process exited successfully!");
|
||||||
|
|||||||
2
theseus_gui/.gitignore
vendored
2
theseus_gui/.gitignore
vendored
@@ -1,3 +1,5 @@
|
|||||||
|
.minecraft
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -13,9 +13,17 @@ edition = "2021"
|
|||||||
tauri-build = { version = "1.2", features = [] }
|
tauri-build = { version = "1.2", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
theseus = { path = "../../theseus" }
|
||||||
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tauri = { version = "1.2", features = ["shell-open"] }
|
tauri = { version = "1.2", features = ["shell-open"] }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
thiserror = "1.0"
|
||||||
|
|
||||||
|
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||||
|
futures = "0.3"
|
||||||
|
daedalus = {version = "0.1.15", features = ["bincode"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
# by default Tauri runs in production mode
|
# by default Tauri runs in production mode
|
||||||
|
|||||||
66
theseus_gui/src-tauri/src/api/mod.rs
Normal file
66
theseus_gui/src-tauri/src/api/mod.rs
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
use serde::ser::SerializeStruct;
|
||||||
|
use serde::{Serialize, Serializer};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub mod profile;
|
||||||
|
pub mod profile_create;
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, TheseusGuiError>;
|
||||||
|
|
||||||
|
// Main returnable Theseus GUI error
|
||||||
|
// Needs to be Serializable to be returned to the JavaScript side
|
||||||
|
#[derive(Error, Debug, Serialize)]
|
||||||
|
pub enum TheseusGuiError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Serializable(TheseusSerializableError),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serializable error intermediary, so TheseusGuiError can be Serializable (eg: so that we can return theseus::Errors in Tauri directly)
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum TheseusSerializableError {
|
||||||
|
#[error("Theseus API error: {0}")]
|
||||||
|
Theseus(#[from] theseus::Error),
|
||||||
|
|
||||||
|
#[error("IO error: {0}")]
|
||||||
|
IO(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic implementation of From<T> for ErrorTypeA
|
||||||
|
impl<T> From<T> for TheseusGuiError
|
||||||
|
where
|
||||||
|
TheseusSerializableError: From<T>,
|
||||||
|
{
|
||||||
|
fn from(error: T) -> Self {
|
||||||
|
TheseusGuiError::Serializable(TheseusSerializableError::from(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a very simple macro that implements a very basic Serializable for each variant of TheseusSerializableError,
|
||||||
|
// where the field is the string. (This allows easy extension to errors without many match arms)
|
||||||
|
macro_rules! impl_serialize {
|
||||||
|
($($variant:ident),* $(,)?) => {
|
||||||
|
impl Serialize for TheseusSerializableError {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
match self {
|
||||||
|
$(
|
||||||
|
TheseusSerializableError::$variant(message) => {
|
||||||
|
let mut state = serializer.serialize_struct(stringify!($variant), 2)?;
|
||||||
|
state.serialize_field("field_name", stringify!($variant))?;
|
||||||
|
state.serialize_field("message", &message.to_string())?;
|
||||||
|
state.end()
|
||||||
|
},
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the macro to implement Serialize for TheseusSerializableError
|
||||||
|
impl_serialize! {
|
||||||
|
Theseus,
|
||||||
|
IO,
|
||||||
|
}
|
||||||
124
theseus_gui/src-tauri/src/api/profile.rs
Normal file
124
theseus_gui/src-tauri/src/api/profile.rs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
use crate::api::Result;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use theseus::prelude::*;
|
||||||
|
|
||||||
|
// Add a profile to the in-memory state
|
||||||
|
// invoke('profile_add',profile)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn profile_add(profile: Profile) -> Result<()> {
|
||||||
|
let res = profile::add(profile).await?;
|
||||||
|
State::sync().await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a path as a profile in-memory
|
||||||
|
// invoke('profile_add_path',path)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn profile_add_path(path: &Path) -> Result<()> {
|
||||||
|
let res = profile::add_path(path).await?;
|
||||||
|
State::sync().await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a profile
|
||||||
|
// invoke('profile_add_path',path)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn profile_remove(path: &Path) -> Result<()> {
|
||||||
|
let res = profile::remove(path).await?;
|
||||||
|
State::sync().await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a profile by path
|
||||||
|
// invoke('profile_add_path',path)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn profile_get(path: &Path) -> Result<Option<Profile>> {
|
||||||
|
let res = profile::get(path).await?;
|
||||||
|
State::sync().await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a profile is already managed by Theseus
|
||||||
|
// invoke('profile_is_managed',profile)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn profile_is_managed(profile: &Path) -> Result<bool> {
|
||||||
|
let res = profile::is_managed(profile).await?;
|
||||||
|
State::sync().await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a profile is loaded
|
||||||
|
// invoke('profile_is_loaded',profile)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn profile_is_loaded(profile: &Path) -> Result<bool> {
|
||||||
|
let res = profile::is_loaded(profile).await?;
|
||||||
|
State::sync().await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a copy of the profile set
|
||||||
|
// invoke('profile_list')
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn profile_list(
|
||||||
|
) -> Result<std::collections::HashMap<PathBuf, Option<Profile>>> {
|
||||||
|
let res = profile::list().await?;
|
||||||
|
State::sync().await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run Minecraft using a profile
|
||||||
|
// Returns a u32 representing the PID, which can be used to poll
|
||||||
|
// for the actual Child in the state.
|
||||||
|
// invoke('profile_run')
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn profile_run(
|
||||||
|
path: &Path,
|
||||||
|
credentials: theseus::auth::Credentials,
|
||||||
|
) -> Result<u32> {
|
||||||
|
let proc_lock = profile::run(path, &credentials).await?;
|
||||||
|
let pid = proc_lock.read().await.id().ok_or_else(|| {
|
||||||
|
theseus::Error::from(theseus::ErrorKind::LauncherError(format!(
|
||||||
|
"Process failed to stay open."
|
||||||
|
)))
|
||||||
|
})?;
|
||||||
|
Ok(pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run Minecraft using a profile, and wait for the result
|
||||||
|
// invoke('profile_wait_for', path, credentials)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn profile_run_wait(
|
||||||
|
path: &Path,
|
||||||
|
credentials: theseus::auth::Credentials,
|
||||||
|
) -> Result<()> {
|
||||||
|
let proc_lock = profile::run(path, &credentials).await?;
|
||||||
|
let mut proc = proc_lock.write().await;
|
||||||
|
Ok(profile::wait_for(&mut proc).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a running minecraft process (a Child)
|
||||||
|
// invoke('profile_wait_for', pid)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn profile_wait_for(pid: u32) -> Result<()> {
|
||||||
|
let st = State::get().await?;
|
||||||
|
if let Some(proc_lock) = st.children.blocking_read().get(&pid) {
|
||||||
|
let mut proc = proc_lock.write().await;
|
||||||
|
return Ok(profile::wait_for(&mut proc).await?);
|
||||||
|
}
|
||||||
|
// If child is gone from state, it's not tracked or already finished
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tries to kill a running minecraft process (if PID is still stored)
|
||||||
|
// invoke('profile_kill', pid)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn profile_kill(pid: u32) -> Result<()> {
|
||||||
|
let st = State::get().await?;
|
||||||
|
let st = State::get().await?;
|
||||||
|
if let Some(proc_lock) = st.children.blocking_read().get(&pid) {
|
||||||
|
let mut proc = proc_lock.write().await;
|
||||||
|
return Ok(profile::kill(&mut proc).await?);
|
||||||
|
}
|
||||||
|
// If child is gone from state, it's not tracked or already finished
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
34
theseus_gui/src-tauri/src/api/profile_create.rs
Normal file
34
theseus_gui/src-tauri/src/api/profile_create.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use crate::api::Result;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use theseus::prelude::*;
|
||||||
|
|
||||||
|
// Generic basic profile creation tool.
|
||||||
|
// Creates an essentially empty dummy profile with profile_create
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn profile_create_empty() -> Result<PathBuf> {
|
||||||
|
let res = profile_create::profile_create_empty().await?;
|
||||||
|
State::sync().await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a profile at the given filepath and adds it to the in-memory state
|
||||||
|
// invoke('profile_add',profile)
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn profile_create(
|
||||||
|
name: String, // the name of the profile, and relative path
|
||||||
|
game_version: String, // the game version of the profile
|
||||||
|
modloader: ModLoader, // the modloader to use
|
||||||
|
loader_version: String, // the modloader version to use, set to "latest", "stable", or the ID of your chosen loader
|
||||||
|
icon: Option<PathBuf>, // the icon for the profile
|
||||||
|
) -> Result<PathBuf> {
|
||||||
|
let res = profile_create::profile_create(
|
||||||
|
name,
|
||||||
|
game_version,
|
||||||
|
modloader,
|
||||||
|
loader_version,
|
||||||
|
icon,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
State::sync().await?;
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
@@ -3,8 +3,33 @@
|
|||||||
windows_subsystem = "windows"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
|
use theseus::prelude::*;
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
|
||||||
|
// Should be called in launcher initialization
|
||||||
|
#[tauri::command]
|
||||||
|
async fn initialize_state() -> api::Result<()> {
|
||||||
|
State::get().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
initialize_state,
|
||||||
|
api::profile_create::profile_create_empty,
|
||||||
|
api::profile_create::profile_create,
|
||||||
|
api::profile::profile_add,
|
||||||
|
api::profile::profile_add_path,
|
||||||
|
api::profile::profile_remove,
|
||||||
|
api::profile::profile_get,
|
||||||
|
api::profile::profile_is_managed,
|
||||||
|
api::profile::profile_is_loaded,
|
||||||
|
api::profile::profile_list,
|
||||||
|
api::profile::profile_run,
|
||||||
|
api::profile::profile_run_wait,
|
||||||
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|||||||
73
theseus_gui/src/helpers/profile.js
Normal file
73
theseus_gui/src/helpers/profile.js
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* All theseus API calls return serialized values (both return values and errors);
|
||||||
|
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||||
|
* and deserialized into a usable JS object.
|
||||||
|
*/
|
||||||
|
import { invoke } from '@tauri-apps/api/tauri'
|
||||||
|
|
||||||
|
// Add empty default instance
|
||||||
|
export async function addDefaultInstance() {
|
||||||
|
return await invoke('profile_create_empty')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add empty default instance
|
||||||
|
export async function create() {
|
||||||
|
return await invoke('profile_create')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a profile to the in-memory state
|
||||||
|
export async function add(profile) {
|
||||||
|
return await invoke('profile_add', profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a path as a profile in-memory
|
||||||
|
export async function add_path(path) {
|
||||||
|
return await invoke('profile_add_path', path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a profile
|
||||||
|
export async function remove(path) {
|
||||||
|
return await invoke('profile_remove', path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a profile by path
|
||||||
|
export async function get(path) {
|
||||||
|
return await invoke('profile_get', path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a pathed profile is already managed by Theseus
|
||||||
|
export async function is_managed(path) {
|
||||||
|
return await invoke('profile_is_managed', path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a pathed profile is loaded
|
||||||
|
export async function is_loaded(path) {
|
||||||
|
return await invoke('profile_is_loaded', path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a copy of the profile set
|
||||||
|
export async function list() {
|
||||||
|
return await invoke('profile_list')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run Minecraft using a pathed profile
|
||||||
|
// Returns PID of child
|
||||||
|
export async function run(path, credentials) {
|
||||||
|
return await invoke('profile_run', path, credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run Minecraft using a pathed profile
|
||||||
|
// Waits for end
|
||||||
|
export async function run_wait(path, credentials) {
|
||||||
|
return await invoke('run_wait', path, credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tries to kill a running minecraft process (if PID is still stored)
|
||||||
|
export async function kill(child_pid) {
|
||||||
|
return await invoke('profile_kill', child_pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for a running minecraft process (a Child)
|
||||||
|
export async function wait_for(child_pid) {
|
||||||
|
return await invoke('profile_wait_for', child_pid)
|
||||||
|
}
|
||||||
12
theseus_gui/src/helpers/state.js
Normal file
12
theseus_gui/src/helpers/state.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* All theseus API calls return serialized values (both return values and errors);
|
||||||
|
* So, for example, addDefaultInstance creates a blank Profile object, where the Rust struct is serialized,
|
||||||
|
* and deserialized into a usable JS object.
|
||||||
|
*/
|
||||||
|
import { invoke } from '@tauri-apps/api/tauri'
|
||||||
|
|
||||||
|
// Initialize the theseus API state
|
||||||
|
// This should be called during the initializion/opening of the launcher
|
||||||
|
export async function initialize_state() {
|
||||||
|
return await invoke('initialize_state')
|
||||||
|
}
|
||||||
22
theseus_playground/Cargo.toml
Normal file
22
theseus_playground/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "theseus_playground"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
theseus = { path = "../theseus" }
|
||||||
|
|
||||||
|
serde_json = "1.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
tauri = { version = "1.2", features = ["shell-open"] }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
thiserror = "1.0"
|
||||||
|
url = "2.2"
|
||||||
|
webbrowser = "0.7"
|
||||||
|
dunce = "1.0.3"
|
||||||
|
|
||||||
|
tokio-stream = { version = "0.1", features = ["fs"] }
|
||||||
|
futures = "0.3"
|
||||||
|
daedalus = {version = "0.1.15", features = ["bincode"] }
|
||||||
119
theseus_playground/src/main.rs
Normal file
119
theseus_playground/src/main.rs
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
#![cfg_attr(
|
||||||
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
|
windows_subsystem = "windows"
|
||||||
|
)]
|
||||||
|
|
||||||
|
use dunce::canonicalize;
|
||||||
|
use std::path::Path;
|
||||||
|
use theseus::{prelude::*, profile_create::profile_create};
|
||||||
|
use tokio::process::Child;
|
||||||
|
use tokio::sync::{oneshot, RwLockWriteGuard};
|
||||||
|
|
||||||
|
// We use this function directly to call authentication procedure
|
||||||
|
// Note: "let url = match url" logic is handled differently, so that if there is a rate limit in the other set causing that one to end early,
|
||||||
|
// we can see the error message in this thread rather than a Recv error on 'rx' when the receiver is mysteriously droppped
|
||||||
|
pub async fn authenticate_run() -> theseus::Result<Credentials> {
|
||||||
|
println!("Adding new user account to Theseus");
|
||||||
|
println!("A browser window will now open, follow the login flow there.");
|
||||||
|
|
||||||
|
let (tx, rx) = oneshot::channel::<url::Url>();
|
||||||
|
let flow = tokio::spawn(auth::authenticate(tx));
|
||||||
|
|
||||||
|
let url = rx.await;
|
||||||
|
let url = match url {
|
||||||
|
Ok(url) => url,
|
||||||
|
Err(e) => {
|
||||||
|
flow.await.unwrap()?;
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
println!("URL {}", url.as_str());
|
||||||
|
webbrowser::open(url.as_str())?;
|
||||||
|
let credentials = flow.await.unwrap()?;
|
||||||
|
State::sync().await?;
|
||||||
|
println!("Logged in user {}.", credentials.username);
|
||||||
|
Ok(credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> theseus::Result<()> {
|
||||||
|
println!("Starting.");
|
||||||
|
|
||||||
|
// Initialize state
|
||||||
|
let st = State::get().await?;
|
||||||
|
|
||||||
|
// Set max concurrent downloads to 10
|
||||||
|
st.settings.write().await.max_concurrent_downloads = 10;
|
||||||
|
|
||||||
|
// Example variables for simple project case
|
||||||
|
let name = "Example".to_string();
|
||||||
|
let game_version = "1.19.2".to_string();
|
||||||
|
let modloader = ModLoader::Vanilla;
|
||||||
|
let loader_version = "stable".to_string();
|
||||||
|
|
||||||
|
// let icon = Some(
|
||||||
|
// Path::new("../icon_test.png")
|
||||||
|
// .canonicalize()
|
||||||
|
// .expect("Icon could be not be found. If not using, set to None"),
|
||||||
|
// );
|
||||||
|
let icon = None;
|
||||||
|
|
||||||
|
// Clear profiles
|
||||||
|
println!("Clearing profiles.");
|
||||||
|
let h = profile::list().await?;
|
||||||
|
for (path, _) in h.into_iter() {
|
||||||
|
profile::remove(&path).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Creating/adding profile.");
|
||||||
|
// Attempt to create a profile. If that fails, try adding one from the same path.
|
||||||
|
// TODO: actually do a nested error check for the correct profile error.
|
||||||
|
let profile_path = profile_create(
|
||||||
|
name.clone(),
|
||||||
|
game_version,
|
||||||
|
modloader,
|
||||||
|
loader_version,
|
||||||
|
icon,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
State::sync().await?;
|
||||||
|
|
||||||
|
// async closure for testing any desired edits
|
||||||
|
// (ie: changing the java runtime of an added profile)
|
||||||
|
println!("Editing.");
|
||||||
|
profile::edit(&profile_path, |profile| {
|
||||||
|
// Eg: Java. TODO: hook up to jre.rs class to pick optimal java
|
||||||
|
profile.java = Some(JavaSettings {
|
||||||
|
install: Some(Path::new("/usr/bin/java").to_path_buf()),
|
||||||
|
extra_arguments: None,
|
||||||
|
});
|
||||||
|
async { Ok(()) }
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
State::sync().await?;
|
||||||
|
|
||||||
|
println!("Authenticating.");
|
||||||
|
// Attempt to create credentials and run.
|
||||||
|
let proc_lock = match authenticate_run().await {
|
||||||
|
Ok(credentials) => {
|
||||||
|
println!("Running.");
|
||||||
|
profile::run(&canonicalize(&profile_path)?, &credentials).await
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("Could not authenticate: {}.\nAttempting stored authentication.",e);
|
||||||
|
// Attempt to load credentials if Hydra is down/rate limit hit
|
||||||
|
let users = auth::users().await.unwrap();
|
||||||
|
let credentials = users.first().unwrap();
|
||||||
|
|
||||||
|
println!("Running.");
|
||||||
|
profile::run(&canonicalize(&profile_path)?, credentials).await
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
|
||||||
|
println!("Started. Waiting...");
|
||||||
|
let mut proc: RwLockWriteGuard<Child> = proc_lock.write().await;
|
||||||
|
profile::wait_for(&mut proc).await?;
|
||||||
|
|
||||||
|
// Run MC
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user