Bug fixes round 3 (#298)

* fixed bugs

* title case

* bugs; ioerror

* reset breadcrumbs

* more fixes

* more fixes

* scrolling bug

* more fixes

* more fixes

* clippy

* canonicalize fix

* fixed requested changes

* removed debouncer update
This commit is contained in:
Wyatt Verchere
2023-07-19 14:13:25 -07:00
committed by GitHub
parent 6650cc9ce4
commit 1f478ec9fc
34 changed files with 932 additions and 602 deletions

882
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,7 @@ thiserror = "1.0"
tracing = "0.1.37"
tracing-subscriber = {version = "0.2", features = ["chrono"]}
tracing-error = "0.1"
tracing-error = "0.1.0"
tracing-appender = "0.1"
paste = { version = "1.0"}

View File

@@ -1,8 +1,11 @@
use std::path::PathBuf;
use crate::event::{
emit::{emit_command, emit_warning},
CommandPayload,
use crate::{
event::{
emit::{emit_command, emit_warning},
CommandPayload,
},
util::io,
};
/// Handles external functions (such as through URL deep linkage)
@@ -46,7 +49,7 @@ pub async fn parse_command(
} else {
// We assume anything else is a filepath to an .mrpack file
let path = PathBuf::from(command_string);
let path = path.canonicalize()?;
let path = io::canonicalize(path)?;
if let Some(ext) = path.extension() {
if ext == "mrpack" {
return Ok(CommandPayload::RunMRPack { path });

View File

@@ -5,6 +5,7 @@ use std::path::PathBuf;
use crate::event::emit::{emit_loading, init_loading};
use crate::util::fetch::{fetch_advanced, fetch_json};
use crate::util::io;
use crate::util::jre::extract_java_majorminor_version;
use crate::{
state::JavaGlobals,
@@ -114,7 +115,7 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
let path = state.directories.java_versions_dir();
if path.exists() {
tokio::fs::remove_dir_all(&path).await?;
io::remove_dir_all(&path).await?;
}
let mut archive = zip::ZipArchive::new(std::io::Cursor::new(file))

View File

@@ -1,6 +1,8 @@
use crate::State;
use crate::{
util::io::{self, IOError},
State,
};
use serde::{Deserialize, Serialize};
use tokio::fs::read_to_string;
#[derive(Serialize, Deserialize, Debug)]
pub struct Logs {
@@ -36,8 +38,11 @@ pub async fn get_logs(
let logs_folder = state.directories.profile_logs_dir(profile_uuid);
let mut logs = Vec::new();
if logs_folder.exists() {
for entry in std::fs::read_dir(logs_folder)? {
let entry = entry?;
for entry in std::fs::read_dir(&logs_folder)
.map_err(|e| IOError::with_path(e, &logs_folder))?
{
let entry =
entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
let path = entry.path();
if path.is_dir() {
if let Some(datetime_string) = path.file_name() {
@@ -79,21 +84,21 @@ pub async fn get_output_by_datetime(
) -> crate::Result<String> {
let state = State::get().await?;
let logs_folder = state.directories.profile_logs_dir(profile_uuid);
Ok(
read_to_string(logs_folder.join(datetime_string).join("stdout.log"))
.await?,
)
let path = logs_folder.join(datetime_string).join("stdout.log");
Ok(io::read_to_string(&path).await?)
}
#[tracing::instrument]
pub async fn delete_logs(profile_uuid: uuid::Uuid) -> crate::Result<()> {
let state = State::get().await?;
let logs_folder = state.directories.profile_logs_dir(profile_uuid);
for entry in std::fs::read_dir(logs_folder)? {
let entry = entry?;
for entry in std::fs::read_dir(&logs_folder)
.map_err(|e| IOError::with_path(e, &logs_folder))?
{
let entry = entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
let path = entry.path();
if path.is_dir() {
std::fs::remove_dir_all(path)?;
io::remove_dir_all(&path).await?;
}
}
Ok(())
@@ -106,6 +111,7 @@ pub async fn delete_logs_by_datetime(
) -> crate::Result<()> {
let state = State::get().await?;
let logs_folder = state.directories.profile_logs_dir(profile_uuid);
std::fs::remove_dir_all(logs_folder.join(datetime_string))?;
let path = logs_folder.join(datetime_string);
io::remove_dir_all(&path).await?;
Ok(())
}

View File

@@ -29,7 +29,10 @@ pub mod prelude {
profile::{self, Profile},
profile_create, settings,
state::JavaGlobals,
util::jre::JavaVersion,
util::{
io::{canonicalize, IOError},
jre::JavaVersion,
},
State,
};
}

View File

@@ -6,6 +6,7 @@ use crate::state::{LinkedData, ModrinthProject, ModrinthVersion, SideType};
use crate::util::fetch::{
fetch, fetch_advanced, fetch_json, write_cached_icon,
};
use crate::util::io;
use crate::State;
use reqwest::Method;
@@ -13,7 +14,6 @@ use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
use tokio::fs;
#[derive(Serialize, Deserialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")]
@@ -263,7 +263,7 @@ pub async fn generate_pack_from_file(
path: PathBuf,
profile: PathBuf,
) -> crate::Result<CreatePackDescription> {
let file = fs::read(&path).await?;
let file = io::read(&path).await?;
Ok(CreatePackDescription {
file: bytes::Bytes::from(file),
icon: None,

View File

@@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
use uuid::Uuid;
use crate::state::MinecraftChild;
use crate::{state::MinecraftChild, util::io::IOError};
pub use crate::{
state::{
Hooks, JavaSettings, MemorySettings, Profile, Settings, WindowSize,
@@ -121,7 +121,13 @@ pub async fn wait_for_by_uuid(uuid: &Uuid) -> crate::Result<()> {
// Kill a running child process directly, and wait for it to be killed
#[tracing::instrument(skip(running))]
pub async fn kill(running: &mut MinecraftChild) -> crate::Result<()> {
running.current_child.write().await.kill().await?;
running
.current_child
.write()
.await
.kill()
.await
.map_err(IOError::from)?;
wait_for(running).await
}

View File

@@ -9,6 +9,7 @@ use crate::pack::install_from::{
use crate::prelude::JavaVersion;
use crate::state::ProjectMetadata;
use crate::util::io::{self, IOError};
use crate::{
auth::{self, refresh},
event::{emit::emit_profile, ProfilePayloadType},
@@ -27,11 +28,7 @@ use std::{
sync::Arc,
};
use tokio::io::AsyncReadExt;
use tokio::{
fs::{self, File},
process::Command,
sync::RwLock,
};
use tokio::{fs::File, process::Command, sync::RwLock};
/// Remove a profile
#[tracing::instrument]
@@ -110,8 +107,8 @@ pub async fn edit_icon(
) -> crate::Result<()> {
let state = State::get().await?;
if let Some(icon) = icon_path {
let bytes = tokio::fs::read(icon).await?;
let res = if let Some(icon) = icon_path {
let bytes = io::read(icon).await?;
let mut profiles = state.profiles.write().await;
@@ -133,8 +130,6 @@ pub async fn edit_icon(
ProfilePayloadType::Edited,
)
.await?;
State::sync().await?;
Ok(())
}
None => Err(crate::ErrorKind::UnmanagedProfileError(
@@ -151,7 +146,9 @@ pub async fn edit_icon(
State::sync().await?;
Ok(())
}
};
State::sync().await?;
res
}
// Gets the optimal JRE key for the given profile, using Daedalus
@@ -416,7 +413,7 @@ pub async fn add_project_from_path(
project_type: Option<String>,
) -> crate::Result<PathBuf> {
if let Some(profile) = get(profile_path, None).await? {
let file = fs::read(path).await?;
let file = io::read(path).await?;
let file_name = path
.file_name()
.unwrap_or_default()
@@ -525,7 +522,9 @@ pub async fn export_mrpack(
let profile_base_path = &profile.path;
let mut file = File::create(export_path).await?;
let mut file = File::create(&export_path)
.await
.map_err(|e| IOError::with_path(e, &export_path))?;
let mut writer = ZipFileWriter::new(&mut file);
// Create mrpack json configuration file
@@ -592,9 +591,13 @@ pub async fn export_mrpack(
// File is not in the config file, add it to the .mrpack zip
if path.is_file() {
let mut file = File::open(&path).await?;
let mut file = File::open(&path)
.await
.map_err(|e| IOError::with_path(e, &path))?;
let mut data = Vec::new();
file.read_to_end(&mut data).await?;
file.read_to_end(&mut data)
.await
.map_err(|e| IOError::with_path(e, &path))?;
let builder = ZipEntryBuilder::new(
format!("overrides/{relative_path}"),
Compression::Deflate,
@@ -639,13 +642,21 @@ pub async fn get_potential_override_folders(
let mrpack_files = get_modrinth_pack_list(&mrpack);
let mut path_list: Vec<PathBuf> = Vec::new();
let mut read_dir = fs::read_dir(&profile_path).await?;
while let Some(entry) = read_dir.next_entry().await? {
let mut read_dir = io::read_dir(&profile_path).await?;
while let Some(entry) = read_dir
.next_entry()
.await
.map_err(|e| IOError::with_path(e, &profile_path))?
{
let path: PathBuf = entry.path();
if path.is_dir() {
// Two layers of files/folders if its a folder
let mut read_dir = fs::read_dir(&path).await?;
while let Some(entry) = read_dir.next_entry().await? {
let mut read_dir = io::read_dir(&path).await?;
while let Some(entry) = read_dir
.next_entry()
.await
.map_err(|e| IOError::with_path(e, &profile_path))?
{
let path: PathBuf = entry.path();
let name = path.strip_prefix(&profile_path)?.to_path_buf();
if !mrpack_files.contains(&name.to_string_lossy().to_string()) {
@@ -712,9 +723,11 @@ pub async fn run_credentials(
let result = Command::new(command)
.args(&cmd.collect::<Vec<&str>>())
.current_dir(path)
.spawn()?
.spawn()
.map_err(|e| IOError::with_path(e, path))?
.wait()
.await?;
.await
.map_err(IOError::from)?;
if !result.success() {
return Err(crate::ErrorKind::LauncherError(format!(
@@ -925,8 +938,12 @@ pub async fn build_folder(
path: &Path,
path_list: &mut Vec<PathBuf>,
) -> crate::Result<()> {
let mut read_dir = fs::read_dir(path).await?;
while let Some(entry) = read_dir.next_entry().await? {
let mut read_dir = io::read_dir(path).await?;
while let Some(entry) = read_dir
.next_entry()
.await
.map_err(|e| IOError::with_path(e, path))?
{
let path = entry.path();
if path.is_dir() {
build_folder(&path, path_list).await?;

View File

@@ -1,5 +1,6 @@
//! Theseus profile management interface
use crate::state::LinkedData;
use crate::util::io::{self, canonicalize};
use crate::{
event::{emit::emit_profile, ProfilePayloadType},
prelude::ModLoader,
@@ -9,11 +10,9 @@ pub use crate::{
State,
};
use daedalus::modded::LoaderVersion;
use dunce::canonicalize;
use futures::prelude::*;
use std::path::PathBuf;
use tokio::fs;
use tokio_stream::wrappers::ReadDirStream;
use tracing::{info, trace};
use uuid::Uuid;
@@ -48,7 +47,7 @@ pub async fn profile_create(
.into());
}
if ReadDirStream::new(fs::read_dir(&path).await?)
if ReadDirStream::new(io::read_dir(&path).await?)
.next()
.await
.is_some()
@@ -56,7 +55,7 @@ pub async fn profile_create(
return Err(ProfileCreationError::NotEmptyFolder.into());
}
} else {
fs::create_dir_all(&path).await?;
io::create_dir_all(&path).await?;
}
info!(
@@ -80,7 +79,7 @@ pub async fn profile_create(
Profile::new(uuid, name, game_version, path.clone()).await?;
let result = async {
if let Some(ref icon) = icon {
let bytes = tokio::fs::read(icon).await?;
let bytes = io::read(icon).await?;
profile
.set_icon(
&state.directories.caches_dir(),

View File

@@ -1,5 +1,5 @@
//! Theseus error type
use crate::profile_create;
use crate::{profile_create, util};
use tracing_error::InstrumentError;
#[derive(thiserror::Error, Debug)]
@@ -29,7 +29,7 @@ pub enum ErrorKind {
AuthTaskError(#[from] crate::state::AuthTaskError),
#[error("I/O error: {0}")]
IOError(#[from] std::io::Error),
IOError(#[from] util::io::IOError),
#[error("Error launching Minecraft: {0}")]
LauncherError(String),

View File

@@ -3,7 +3,7 @@
use super::{auth::Credentials, parse_rule};
use crate::{
state::{MemorySettings, WindowSize},
util::platform::classpath_separator,
util::{io::IOError, platform::classpath_separator},
};
use daedalus::{
get_path_from_artifact,
@@ -393,7 +393,8 @@ pub async fn get_processor_main_class(
path: String,
) -> crate::Result<Option<String>> {
let main_class = tokio::task::spawn_blocking(move || {
let zipfile = std::fs::File::open(&path)?;
let zipfile = std::fs::File::open(&path)
.map_err(|e| IOError::with_path(e, &path))?;
let mut archive = zip::ZipArchive::new(zipfile).map_err(|_| {
crate::ErrorKind::LauncherError(format!(
"Cannot read processor at {}",
@@ -413,7 +414,7 @@ pub async fn get_processor_main_class(
let reader = BufReader::new(file);
for line in reader.lines() {
let mut line = line?;
let mut line = line.map_err(IOError::from)?;
line.retain(|c| !c.is_whitespace());
if line.starts_with("Main-Class:") {

View File

@@ -6,7 +6,7 @@ use crate::{
LoadingBarId,
},
state::State,
util::{fetch::*, platform::OsExt},
util::{fetch::*, io, platform::OsExt},
};
use daedalus::{
self as d,
@@ -17,7 +17,7 @@ use daedalus::{
modded::LoaderVersion,
};
use futures::prelude::*;
use tokio::{fs, sync::OnceCell};
use tokio::sync::OnceCell;
#[tracing::instrument(skip(st, version))]
pub async fn download_minecraft(
@@ -71,7 +71,7 @@ pub async fn download_version_info(
.join(format!("{version_id}.json"));
let res = if path.exists() && !force.unwrap_or(false) {
fs::read(path)
io::read(path)
.err_into::<crate::Error>()
.await
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
@@ -152,7 +152,7 @@ pub async fn download_assets_index(
.join(format!("{}.json", &version.asset_index.id));
let res = if path.exists() {
fs::read(path)
io::read(path)
.err_into::<crate::Error>()
.await
.and_then(|ref it| Ok(serde_json::from_slice(it)?))
@@ -245,8 +245,8 @@ pub async fn download_libraries(
tracing::debug!("Loading libraries");
tokio::try_join! {
fs::create_dir_all(st.directories.libraries_dir()),
fs::create_dir_all(st.directories.version_natives_dir(version))
io::create_dir_all(st.directories.libraries_dir()),
io::create_dir_all(st.directories.version_natives_dir(version))
}?;
let num_files = libraries.len();
loading_try_for_each_concurrent(

View File

@@ -1,9 +1,10 @@
//! Logic for launching Minecraft
use crate::event::emit::{emit_loading, init_or_edit_loading};
use crate::event::{LoadingBarId, LoadingBarType};
use crate::jre::{JAVA_17_KEY, JAVA_18PLUS_KEY, JAVA_8_KEY};
use crate::jre::{self, JAVA_17_KEY, JAVA_18PLUS_KEY, JAVA_8_KEY};
use crate::prelude::JavaVersion;
use crate::state::ProfileInstallStage;
use crate::util::io;
use crate::EventState;
use crate::{
process,
@@ -13,10 +14,8 @@ use crate::{
use chrono::Utc;
use daedalus as d;
use daedalus::minecraft::VersionInfo;
use dunce::canonicalize;
use st::Profile;
use std::collections::HashMap;
use std::fs;
use std::{process::Stdio, sync::Arc};
use tokio::process::Command;
use uuid::Uuid;
@@ -125,7 +124,7 @@ pub async fn install_minecraft(
State::sync().await?;
let state = State::get().await?;
let instance_path = &canonicalize(&profile.path)?;
let instance_path = &io::canonicalize(&profile.path)?;
let metadata = state.metadata.read().await;
let version = metadata
@@ -160,7 +159,7 @@ pub async fn install_minecraft(
.await?
.ok_or_else(|| {
crate::ErrorKind::OtherError(
"No available java installation".to_string(),
"Missing correct java installation".to_string(),
)
})?;
@@ -282,7 +281,7 @@ pub async fn install_minecraft(
Ok(())
}
#[tracing::instrument]
#[tracing::instrument(skip_all)]
#[theseus_macros::debug_pin]
#[allow(clippy::too_many_arguments)]
pub async fn launch_minecraft(
@@ -310,7 +309,7 @@ pub async fn launch_minecraft(
let state = State::get().await?;
let metadata = state.metadata.read().await;
let instance_path = &canonicalize(&profile.path)?;
let instance_path = &io::canonicalize(&profile.path)?;
let version = metadata
.minecraft
@@ -343,10 +342,20 @@ pub async fn launch_minecraft(
.await?
.ok_or_else(|| {
crate::ErrorKind::LauncherError(
"No available java installation".to_string(),
"Missing correct java installation".to_string(),
)
})?;
// Test jre version
let java_version = jre::check_jre(java_version.path.clone().into())
.await?
.ok_or_else(|| {
crate::ErrorKind::LauncherError(format!(
"Java path invalid or non-functional: {}",
java_version.path
))
})?;
let client_path = state
.directories
.version_dir(&version_jar)
@@ -433,7 +442,7 @@ pub async fn launch_minecraft(
.profile_logs_dir(profile.uuid)
.join(&datetime_string)
};
fs::create_dir_all(&logs_dir)?;
io::create_dir_all(&logs_dir).await?;
let stdout_log_path = logs_dir.join("stdout.log");

View File

@@ -12,6 +12,7 @@ use tracing::error;
use crate::event::emit::emit_process;
use crate::event::ProcessPayloadType;
use crate::util::io::IOError;
use crate::EventState;
use tokio::task::JoinHandle;
use uuid::Uuid;
@@ -39,7 +40,15 @@ impl Children {
// The threads for stdout and stderr are spawned here
// Unlike a Hashmap's 'insert', this directly returns the reference to the MinecraftChild rather than any previously stored MinecraftChild that may exist
#[tracing::instrument(skip(self))]
#[tracing::instrument(skip(
self,
uuid,
log_path,
mc_command,
post_command,
censor_strings
))]
#[tracing::instrument(level = "trace", skip(self))]
#[theseus_macros::debug_pin]
pub async fn insert_process(
&mut self,
@@ -51,7 +60,7 @@ impl Children {
censor_strings: HashMap<String, String>,
) -> crate::Result<Arc<RwLock<MinecraftChild>>> {
// Takes the first element of the commands vector and spawns it
let mut child = mc_command.spawn()?;
let mut child = mc_command.spawn().map_err(IOError::from)?;
// Create std watcher threads for stdout and stderr
let shared_output =
@@ -125,7 +134,12 @@ impl Children {
// Wait on current Minecraft Child
let mut mc_exit_status;
loop {
if let Some(t) = current_child.write().await.try_wait()? {
if let Some(t) = current_child
.write()
.await
.try_wait()
.map_err(IOError::from)?
{
mc_exit_status = t;
break;
}
@@ -156,7 +170,7 @@ impl Children {
if let Some(mut m_command) = post_command {
{
let mut current_child = current_child.write().await;
let new_child = m_command.spawn()?;
let new_child = m_command.spawn().map_err(IOError::from)?;
current_pid = new_child.id().ok_or_else(|| {
crate::ErrorKind::LauncherError(
"Process immediately failed, could not get PID"
@@ -174,7 +188,12 @@ impl Children {
.await?;
loop {
if let Some(t) = current_child.write().await.try_wait()? {
if let Some(t) = current_child
.write()
.await
.try_wait()
.map_err(IOError::from)?
{
mc_exit_status = t;
break;
}
@@ -210,7 +229,12 @@ impl Children {
) -> crate::Result<Option<std::process::ExitStatus>> {
if let Some(child) = self.get(uuid) {
let child = child.write().await;
let status = child.current_child.write().await.try_wait()?;
let status = child
.current_child
.write()
.await
.try_wait()
.map_err(IOError::from)?;
Ok(status)
} else {
Ok(None)
@@ -224,7 +248,14 @@ impl Children {
if let Some(child) = self.get(&key) {
let child = child.clone();
let child = child.write().await;
if child.current_child.write().await.try_wait()?.is_none() {
if child
.current_child
.write()
.await
.try_wait()
.map_err(IOError::from)?
.is_none()
{
keys.push(key);
}
}
@@ -258,7 +289,14 @@ impl Children {
if let Some(child) = self.get(&key) {
let child = child.clone();
let child = child.write().await;
if child.current_child.write().await.try_wait()?.is_none() {
if child
.current_child
.write()
.await
.try_wait()
.map_err(IOError::from)?
.is_none()
{
profiles.push(child.profile_path.clone());
}
}
@@ -274,7 +312,14 @@ impl Children {
if let Some(child) = self.get(&key) {
let child = child.clone();
let child = child.write().await;
if child.current_child.write().await.try_wait()?.is_none() {
if child
.current_child
.write()
.await
.try_wait()
.map_err(IOError::from)?
.is_none()
{
if let Some(prof) = crate::api::profile::get(
&child.profile_path.clone(),
None,
@@ -312,7 +357,11 @@ impl SharedOutput {
) -> crate::Result<Self> {
Ok(SharedOutput {
output: Arc::new(RwLock::new(String::new())),
log_file: Arc::new(RwLock::new(File::create(log_file_path).await?)),
log_file: Arc::new(RwLock::new(
File::create(log_file_path)
.await
.map_err(|e| IOError::with_path(e, log_file_path))?,
)),
censor_strings,
})
}
@@ -330,7 +379,12 @@ impl SharedOutput {
let mut buf_reader = BufReader::new(child_stdout);
let mut line = String::new();
while buf_reader.read_line(&mut line).await? > 0 {
while buf_reader
.read_line(&mut line)
.await
.map_err(IOError::from)?
> 0
{
let val_line = self.censor_log(line.clone());
{
@@ -339,7 +393,10 @@ impl SharedOutput {
}
{
let mut log_file = self.log_file.write().await;
log_file.write_all(val_line.as_bytes()).await?;
log_file
.write_all(val_line.as_bytes())
.await
.map_err(IOError::from)?;
}
line.clear();
@@ -354,7 +411,12 @@ impl SharedOutput {
let mut buf_reader = BufReader::new(child_stderr);
let mut line = String::new();
while buf_reader.read_line(&mut line).await? > 0 {
while buf_reader
.read_line(&mut line)
.await
.map_err(IOError::from)?
> 0
{
let val_line = self.censor_log(line.clone());
{
@@ -363,7 +425,10 @@ impl SharedOutput {
}
{
let mut log_file = self.log_file.write().await;
log_file.write_all(val_line.as_bytes()).await?;
log_file
.write_all(val_line.as_bytes())
.await
.map_err(IOError::from)?;
}
line.clear();

View File

@@ -9,11 +9,11 @@ use crate::state::{ModrinthVersion, ProjectMetadata, ProjectType};
use crate::util::fetch::{
fetch, fetch_json, write, write_cached_icon, IoSemaphore,
};
use crate::util::io::{self, IOError};
use crate::State;
use chrono::{DateTime, Utc};
use daedalus::get_hash;
use daedalus::modded::LoaderVersion;
use dunce::canonicalize;
use futures::prelude::*;
use notify::{RecommendedWatcher, RecursiveMode};
use notify_debouncer_mini::Debouncer;
@@ -24,7 +24,6 @@ use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use tokio::fs;
use uuid::Uuid;
const PROFILE_JSON_PATH: &str = "profile.json";
@@ -161,7 +160,7 @@ impl Profile {
Ok(Self {
uuid,
install_stage: ProfileInstallStage::NotInstalled,
path: canonicalize(path)?,
path: io::canonicalize(path)?,
metadata: ProfileMetadata {
name,
icon: None,
@@ -274,8 +273,11 @@ impl Profile {
let mut read_paths = |path: &str| {
let new_path = self.path.join(path);
if new_path.exists() {
for path in std::fs::read_dir(self.path.join(path))? {
let path = path?.path();
let path = self.path.join(path);
for path in std::fs::read_dir(&path)
.map_err(|e| IOError::with_path(e, &path))?
{
let path = path.map_err(IOError::from)?.path();
if path.is_file() {
files.push(path);
}
@@ -305,7 +307,7 @@ impl Profile {
) -> crate::Result<()> {
let path = profile_path.join(path);
fs::create_dir_all(&path).await?;
io::create_dir_all(&path).await?;
watcher
.watcher()
@@ -476,7 +478,7 @@ impl Profile {
project.disabled = true;
}
fs::rename(path, &new_path).await?;
io::rename(&path, &new_path).await?;
let mut profiles = state.profiles.write().await;
if let Some(profile) = profiles.0.get_mut(&self.path) {
@@ -501,7 +503,7 @@ impl Profile {
) -> crate::Result<()> {
let state = State::get().await?;
if self.projects.contains_key(path) {
fs::remove_file(path).await?;
io::remove_file(path).await?;
if !dont_remove_arr.unwrap_or(false) {
let mut profiles = state.profiles.write().await;
@@ -530,9 +532,11 @@ impl Profiles {
file_watcher: &mut Debouncer<RecommendedWatcher>,
) -> crate::Result<Self> {
let mut profiles = HashMap::new();
fs::create_dir_all(dirs.profiles_dir()).await?;
let mut entries = fs::read_dir(dirs.profiles_dir()).await?;
while let Some(entry) = entries.next_entry().await? {
io::create_dir_all(&dirs.profiles_dir()).await?;
let mut entries = io::read_dir(&dirs.profiles_dir()).await?;
while let Some(entry) =
entries.next_entry().await.map_err(IOError::from)?
{
let path = entry.path();
if path.is_dir() {
let prof = match Self::read_profile_from_dir(&path).await {
@@ -545,7 +549,7 @@ impl Profiles {
}
};
if let Some(profile) = prof {
let path = canonicalize(path)?;
let path = io::canonicalize(path)?;
Profile::watch_fs(&path, file_watcher).await?;
profiles.insert(path, profile);
}
@@ -629,7 +633,7 @@ impl Profiles {
Profile::watch_fs(&profile.path, &mut file_watcher).await?;
self.0.insert(
canonicalize(&profile.path)?
io::canonicalize(&profile.path)?
.to_str()
.ok_or(
crate::ErrorKind::UTFError(profile.path.clone()).as_error(),
@@ -645,12 +649,13 @@ impl Profiles {
&mut self,
path: &Path,
) -> crate::Result<Option<Profile>> {
let path =
PathBuf::from(&canonicalize(path)?.to_string_lossy().to_string());
let path = PathBuf::from(
&io::canonicalize(path)?.to_string_lossy().to_string(),
);
let profile = self.0.remove(&path);
if path.exists() {
fs::remove_dir_all(path).await?;
io::remove_dir_all(&path).await?;
}
Ok(profile)
@@ -666,7 +671,7 @@ impl Profiles {
let json_path = Path::new(&path.to_string_lossy().to_string())
.join(PROFILE_JSON_PATH);
fs::write(json_path, json).await?;
io::write(&json_path, &json).await?;
Ok::<_, crate::Error>(())
})
.await?;
@@ -675,7 +680,7 @@ impl Profiles {
}
async fn read_profile_from_dir(path: &Path) -> crate::Result<Profile> {
let json = fs::read(path.join(PROFILE_JSON_PATH)).await?;
let json = io::read(&path.join(PROFILE_JSON_PATH)).await?;
let mut profile = serde_json::from_slice::<Profile>(&json)?;
profile.path = PathBuf::from(path);
Ok(profile)

View File

@@ -5,6 +5,7 @@ use crate::state::Profile;
use crate::util::fetch::{
fetch_json, write_cached_icon, FetchSemaphore, IoSemaphore,
};
use crate::util::io::IOError;
use async_zip::tokio::read::fs::ZipFileReader;
use chrono::{DateTime, Utc};
use reqwest::Method;
@@ -252,7 +253,7 @@ async fn read_icon_from_file(
Ok(None)
}
#[tracing::instrument(skip(profile, io_semaphore, fetch_semaphore))]
#[tracing::instrument(skip(paths, profile, io_semaphore, fetch_semaphore))]
#[theseus_macros::debug_pin]
pub async fn infer_data_from_files(
profile: Profile,
@@ -265,10 +266,12 @@ pub async fn infer_data_from_files(
// TODO: Make this concurrent and use progressive hashing to avoid loading each JAR in memory
for path in paths {
let mut file = tokio::fs::File::open(path.clone()).await?;
let mut file = tokio::fs::File::open(path.clone())
.await
.map_err(|e| IOError::with_path(e, &path))?;
let mut buffer = Vec::new();
file.read_to_end(&mut buffer).await?;
file.read_to_end(&mut buffer).await.map_err(IOError::from)?;
let hash = format!("{:x}", sha2::Sha512::digest(&buffer));
file_path_hashes.insert(hash, path.clone());

View File

@@ -9,10 +9,9 @@ use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::time;
use tokio::sync::{RwLock, Semaphore};
use tokio::{
fs::{self, File},
io::AsyncWriteExt,
};
use tokio::{fs::File, io::AsyncWriteExt};
use super::io::{self, IOError};
#[derive(Debug)]
pub struct IoSemaphore(pub RwLock<Semaphore>);
@@ -193,7 +192,7 @@ where
let io_semaphore = semaphore.0.read().await;
let _permit = io_semaphore.acquire().await?;
let json = fs::read(path).await?;
let json = io::read(path).await?;
let json = serde_json::from_slice::<T>(&json)?;
Ok(json)
@@ -209,11 +208,15 @@ pub async fn write<'a>(
let _permit = io_semaphore.acquire().await?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
io::create_dir_all(parent).await?;
}
let mut file = File::create(path).await?;
file.write_all(bytes).await?;
let mut file = File::create(path)
.await
.map_err(|e| IOError::with_path(e, path))?;
file.write_all(bytes)
.await
.map_err(|e| IOError::with_path(e, path))?;
tracing::trace!("Done writing file {}", path.display());
Ok(())
}
@@ -235,7 +238,7 @@ pub async fn write_cached_icon(
write(&path, &bytes, semaphore).await?;
let path = dunce::canonicalize(path)?;
let path = io::canonicalize(path)?;
Ok(path)
}

149
theseus/src/util/io.rs Normal file
View File

@@ -0,0 +1,149 @@
// IO error
// A wrapper around the tokio IO functions that adds the path to the error message, instead of the uninformative std::io::Error.
#[derive(Debug, thiserror::Error)]
pub enum IOError {
#[error("{source}, path: {path}")]
IOPathError {
#[source]
source: std::io::Error,
path: String,
},
#[error(transparent)]
IOError(#[from] std::io::Error),
}
impl IOError {
pub fn from(source: std::io::Error) -> Self {
Self::IOError(source)
}
pub fn with_path(
source: std::io::Error,
path: impl AsRef<std::path::Path>,
) -> Self {
let path = path.as_ref();
Self::IOPathError {
source,
path: path.to_string_lossy().to_string(),
}
}
}
// dunce canonicalize
pub fn canonicalize(
path: impl AsRef<std::path::Path>,
) -> Result<std::path::PathBuf, IOError> {
let path = path.as_ref();
dunce::canonicalize(path).map_err(|e| IOError::IOPathError {
source: e,
path: path.to_string_lossy().to_string(),
})
}
// read_dir
pub async fn read_dir(
path: impl AsRef<std::path::Path>,
) -> Result<tokio::fs::ReadDir, IOError> {
let path = path.as_ref();
tokio::fs::read_dir(path)
.await
.map_err(|e| IOError::IOPathError {
source: e,
path: path.to_string_lossy().to_string(),
})
}
// create_dir_all
pub async fn create_dir_all(
path: impl AsRef<std::path::Path>,
) -> Result<(), IOError> {
let path = path.as_ref();
tokio::fs::create_dir_all(path)
.await
.map_err(|e| IOError::IOPathError {
source: e,
path: path.to_string_lossy().to_string(),
})
}
// remove_dir_all
pub async fn remove_dir_all(
path: impl AsRef<std::path::Path>,
) -> Result<(), IOError> {
let path = path.as_ref();
tokio::fs::remove_dir_all(path)
.await
.map_err(|e| IOError::IOPathError {
source: e,
path: path.to_string_lossy().to_string(),
})
}
// read_to_string
pub async fn read_to_string(
path: impl AsRef<std::path::Path>,
) -> Result<String, IOError> {
let path = path.as_ref();
tokio::fs::read_to_string(path)
.await
.map_err(|e| IOError::IOPathError {
source: e,
path: path.to_string_lossy().to_string(),
})
}
// read
pub async fn read(
path: impl AsRef<std::path::Path>,
) -> Result<Vec<u8>, IOError> {
let path = path.as_ref();
tokio::fs::read(path)
.await
.map_err(|e| IOError::IOPathError {
source: e,
path: path.to_string_lossy().to_string(),
})
}
// write
pub async fn write(
path: impl AsRef<std::path::Path>,
data: impl AsRef<[u8]>,
) -> Result<(), IOError> {
let path = path.as_ref();
tokio::fs::write(path, data)
.await
.map_err(|e| IOError::IOPathError {
source: e,
path: path.to_string_lossy().to_string(),
})
}
// rename
pub async fn rename(
from: impl AsRef<std::path::Path>,
to: impl AsRef<std::path::Path>,
) -> Result<(), IOError> {
let from = from.as_ref();
let to = to.as_ref();
tokio::fs::rename(from, to)
.await
.map_err(|e| IOError::IOPathError {
source: e,
path: from.to_string_lossy().to_string(),
})
}
// remove file
pub async fn remove_file(
path: impl AsRef<std::path::Path>,
) -> Result<(), IOError> {
let path = path.as_ref();
tokio::fs::remove_file(path)
.await
.map_err(|e| IOError::IOPathError {
source: e,
path: path.to_string_lossy().to_string(),
})
}

View File

@@ -1,4 +1,4 @@
use dunce::canonicalize;
use super::io;
use futures::prelude::*;
use serde::{Deserialize, Serialize};
use std::env;
@@ -270,7 +270,7 @@ pub async fn check_java_at_filepaths(
pub async fn check_java_at_filepath(path: &Path) -> Option<JavaVersion> {
// Attempt to canonicalize the potential java filepath
// If it fails, this path does not exist and None is returned (no Java here)
let Ok(path) = canonicalize(path) else { return None };
let Ok(path) = io::canonicalize(path) else { return None };
// Checks for existence of Java at this filepath
// Adds JAVA_BIN to the end of the path if it is not already there

View File

@@ -1,5 +1,6 @@
//! Theseus utility functions
pub mod fetch;
pub mod io;
pub mod jre;
pub mod platform;

View File

@@ -1,6 +1,6 @@
<script setup>
import { ref, watch } from 'vue'
import { RouterView, RouterLink, useRouter } from 'vue-router'
import { computed, ref, watch } from 'vue'
import { RouterView, RouterLink, useRouter, useRoute } from 'vue-router'
import {
HomeIcon,
SearchIcon,
@@ -106,6 +106,8 @@ router.afterEach((to, from, failure) => {
mixpanel.track('PageView', { path: to.path, fromPath: from.path, failed: failure })
}
})
const route = useRoute()
const isOnBrowse = computed(() => route.path.startsWith('/browse'))
const loading = useLoading()
@@ -179,6 +181,7 @@ const accounts = ref(null)
'icon-only': themeStore.collapsedNavigation,
'collapsed-button': themeStore.collapsedNavigation,
'expanded-button': !themeStore.collapsedNavigation,
'router-link-active': isOnBrowse,
}"
>
<SearchIcon />
@@ -340,7 +343,7 @@ const accounts = ref(null)
display: flex;
align-items: center;
background: var(--color-raised-bg);
box-shadow: var(--shadow-inset-sm), var(--shadow-floating);
box-shadow: inset 0px -3px 0px black;
text-align: center;
padding: var(--gap-md);
height: 3.25rem;

View File

@@ -122,9 +122,9 @@
</div>
<Chips
v-model="javaSelectionType"
:items="['automatically install', 'use existing installation']"
:items="['Automatically install', 'Use existing installation']"
/>
<div v-if="javaSelectionType === 'use existing installation'" class="settings-group">
<div v-if="javaSelectionType === 'Use existing installation'" class="settings-group">
<h3>Java location</h3>
<JavaSelector v-model="settings.java_globals.JAVA_17" compact />
</div>
@@ -230,7 +230,7 @@ async function pageTurn() {
}
}
const javaSelectionType = ref('automatically install')
const javaSelectionType = ref('Automatically install')
async function autoInstallJava() {
const path = await auto_install_java(17).catch(handleError)
@@ -239,6 +239,7 @@ async function autoInstallJava() {
// weird vue bug, ignore
settings.value.java_globals.JAVA_17 = version
settings.value.java_globals.JAVA_17 = version
set(settings.value)
mixpanel.track('OnboardingAutoInstallJava')
}
@@ -258,6 +259,10 @@ onBeforeUnmount(() => {
</script>
<style scoped lang="scss">
:deep(.chips .btn) {
text-transform: none !important;
}
.modal-body {
display: flex;
flex-direction: column;

View File

@@ -1,5 +1,6 @@
<template>
<div class="breadcrumbs">
{{ breadcrumbData.resetToNames(breadcrumbs) }}
<div v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name" class="breadcrumbs__item">
<router-link
v-if="breadcrumb.link"

View File

@@ -40,7 +40,7 @@
</div>
<div v-else class="status">
<span class="circle stopped" />
<span class="running-text"> No running instances </span>
<span class="running-text"> No instances running </span>
</div>
</div>
<transition name="download">

View File

@@ -14,6 +14,7 @@ defineProps({
<style scoped lang="scss">
.page-loading {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;

View File

@@ -272,6 +272,11 @@ async function onSearchChangeToTop(newPageNumber) {
searchWrapper.value.scrollTo({ top: 0, behavior: 'smooth' })
}
async function clearSearch() {
query.value = ''
await onSearchChange(1)
}
function getSearchUrl(offset, useObj) {
const queryItems = []
const obj = {}
@@ -355,6 +360,33 @@ const sortedCategories = computed(() => {
return values
})
// Sorts alphabetically, but correctly identifies 8x, 128x, 256x, etc
// identifier[0], then if it ties, identifier[1], etc
async function sortByNameOrNumber(sortable, identifiers) {
console.log(sortable)
sortable.sort((a, b) => {
for (let identifier of identifiers) {
let aNum = parseFloat(a[identifier])
let bNum = parseFloat(b[identifier])
if (isNaN(aNum) && isNaN(bNum)) {
// Both are strings, sort alphabetically
let stringComp = a[identifier].localeCompare(b[identifier])
if (stringComp != 0) return stringComp
} else if (!isNaN(aNum) && !isNaN(bNum)) {
// Both are numbers, sort numerically
let numComp = aNum - bNum
if (numComp != 0) return numComp
} else {
// One is a number and one is a string, numbers go first
let numStringComp = isNaN(aNum) ? 1 : -1
if (numStringComp != 0) return numStringComp
}
}
return 0
})
return sortable
}
async function clearFilters() {
for (const facet of [...facets.value]) {
await toggleFacet(facet, true)
@@ -426,7 +458,10 @@ watch(
)
const [categories, loaders, availableGameVersions] = await Promise.all([
get_categories().catch(handleError).then(ref),
get_categories()
.catch(handleError)
.then((s) => sortByNameOrNumber(s, ['header', 'name']))
.then(ref),
get_loaders().catch(handleError).then(ref),
get_game_versions().catch(handleError).then(ref),
refreshSearch(),
@@ -473,7 +508,7 @@ const showLoaders = computed(
</script>
<template>
<div class="search-container">
<div ref="searchWrapper" class="search-container">
<aside class="filter-panel">
<Card v-if="instanceContext" class="small-instance">
<router-link :to="`/instance/${encodeURIComponent(instanceContext.path)}`" class="instance">
@@ -525,7 +560,7 @@ const showLoaders = computed(
"
@click="clearFilters"
>
<ClearIcon /> Clear Filters
<ClearIcon /> Clear filters
</Button>
<div v-if="showLoaders" class="loaders">
<h2>Loaders</h2>
@@ -618,7 +653,7 @@ const showLoaders = computed(
</div>
</Card>
</aside>
<div ref="searchWrapper" class="search">
<div class="search">
<Promotion class="promotion" />
<Card class="project-type-container">
<NavRow :links="selectableProjectTypes" />
@@ -633,7 +668,7 @@ const showLoaders = computed(
:placeholder="`Search ${projectType}s...`"
@input="onSearchChange(1)"
/>
<Button @click="() => (searchStore.searchInput = '')">
<Button @click="() => clearSearch()">
<XIcon />
</Button>
</div>
@@ -698,6 +733,7 @@ const showLoaders = computed(
class="pagination-after"
@switch-page="onSearchChangeToTop"
/>
<br />
</div>
</div>
<InstallConfirmModal ref="confirmModal" />
@@ -818,6 +854,9 @@ const showLoaders = computed(
.search-container {
display: flex;
height: 100%; /* takes up only the necessary height */
overflow-y: auto;
scroll-behavior: smooth;
.filter-panel {
position: fixed;
@@ -846,7 +885,6 @@ const showLoaders = computed(
}
.search {
scroll-behavior: smooth;
margin: 0 1rem 0.5rem 20.5rem;
width: calc(100% - 20.5rem);

View File

@@ -65,7 +65,7 @@
</template>
<template #filter_update>
<UpdatedIcon />
Select Updatable
Select updatable
</template>
</DropdownButton>
<Button v-if="selected.length > 0" class="no-wrap" @click="deleteWarning.show()">
@@ -155,21 +155,16 @@
<TrashIcon />
</Button>
<AnimatedLogo v-if="mod.updating" class="btn icon-only updating-indicator"></AnimatedLogo>
<Button
v-else
v-tooltip="'Update project'"
:disabled="!mod.outdated"
icon-only
@click="updateProject(mod)"
>
<UpdatedIcon v-if="mod.outdated" />
<CheckIcon v-else />
<Button v-else :disabled="!mod.outdated" icon-only @click="updateProject(mod)">
<UpdatedIcon v-if="mod.outdated" v-tooltip="'Update project'" />
<CheckIcon v-else v-tooltip="'Updated'" />
</Button>
<input
id="switch-1"
autocomplete="off"
type="checkbox"
class="switch stylized-toggle"
:disabled="mod.toggleInProgress"
:checked="!mod.disabled"
@change="toggleDisableMod(mod)"
/>
@@ -454,7 +449,6 @@ async function updateProject(mod) {
async function toggleDisableMod(mod) {
mod.path = await toggle_disable_project(props.instance.path, mod.path).catch(handleError)
mod.disabled = !mod.disabled
mixpanel.track('InstanceProjectDisable', {
loader: props.instance.metadata.loader,
game_version: props.instance.metadata.game_version,

View File

@@ -610,6 +610,10 @@ async function saveGvLoaderEdits() {
padding: 1rem;
gap: 1rem;
:deep(.animated-dropdown .options) {
max-height: 13.375rem;
}
.input-label {
font-size: 1rem;
font-weight: bolder;

View File

@@ -207,6 +207,7 @@
:dependencies="dependencies"
:install="install"
:installed="installed"
:installing="installing"
:installed-version="installedVersion"
/>
</div>
@@ -310,7 +311,7 @@ async function fetchProjectData() {
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}/members`, 'project'),
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}/dependencies`, 'project'),
get_categories().catch(handleError),
route.query.i ? getInstance(route.query.i, true).catch(handleError) : Promise.resolve(),
route.query.i ? getInstance(route.query.i, false).catch(handleError) : Promise.resolve(),
])
installed.value =
@@ -344,6 +345,7 @@ const markInstalled = () => {
async function install(version) {
installing.value = true
let queuedVersionData
instance.value = await getInstance(instance.value.path, false).catch(handleError)
if (installed.value) {
await remove_project(

View File

@@ -14,10 +14,21 @@
<h2>{{ version.name }}</h2>
</div>
<div class="button-group">
<Button color="primary" :action="() => install(version.id)" :disabled="installed">
<Button
color="primary"
:action="() => install(version.id)"
:disabled="installing || (installed && installedVersion === version.id)"
>
<DownloadIcon v-if="!installed" />
<SwapIcon v-else-if="installedVersion !== version.id" />
<CheckIcon v-else />
{{ installed ? 'Installed' : 'Install' }}
{{
installing
? 'Installing...'
: installed && installedVersion === version.id
? 'Installed'
: 'Install'
}}
</Button>
<Button>
<ReportIcon />
@@ -29,7 +40,7 @@
class="btn"
>
<ExternalIcon />
Modrinth Website
Modrinth website
</a>
</div>
</Card>
@@ -195,6 +206,7 @@ import { releaseColor } from '@/helpers/utils'
import { ref, watch, computed } from 'vue'
import { useRoute } from 'vue-router'
import { useBreadcrumbs } from '@/store/breadcrumbs'
import { SwapIcon } from '@/assets/icons'
const breadcrumbs = useBreadcrumbs()
@@ -225,6 +237,14 @@ const props = defineProps({
type: Boolean,
required: true,
},
installing: {
type: Boolean,
required: true,
},
installedVersion: {
type: String,
required: true,
},
})
const version = ref(props.versions.find((version) => version.id === route.params.version))

View File

@@ -89,7 +89,7 @@
<Button
:color="installed && version.id === installedVersion ? '' : 'primary'"
icon-only
:disabled="installed && version.id === installedVersion"
:disabled="installing || (installed && version.id === installedVersion)"
@click.stop="() => install(version.id)"
>
<DownloadIcon v-if="!installed" />
@@ -191,6 +191,10 @@ const props = defineProps({
type: Boolean,
default: null,
},
installing: {
type: Boolean,
default: false,
},
instance: {
type: Object,
default: null,

View File

@@ -8,11 +8,24 @@ export const useBreadcrumbs = defineStore('breadcrumbsStore', {
}),
actions: {
getName(route) {
return this.names.get(route) ?? route
return this.names.get(route) ?? ''
},
setName(route, title) {
this.names.set(route, title)
},
// resets breadcrumbs to only included ones as to not have stale breadcrumbs
resetToNames(breadcrumbs) {
// names is an array of every breadcrumb.name that starts with a ?
const names = breadcrumbs
.filter((breadcrumb) => breadcrumb.name.charAt(0) === '?')
.map((breadcrumb) => breadcrumb.name.slice(1))
// remove all names that are not in the names array
for (const [route] of this.names) {
if (!names.includes(route)) {
this.names.delete(route)
}
}
},
setContext(context) {
this.context = context
},

View File

@@ -3,7 +3,6 @@
windows_subsystem = "windows"
)]
use dunce::canonicalize;
use theseus::jre::autodetect_java_globals;
use theseus::prelude::*;
@@ -19,7 +18,8 @@ pub async fn authenticate_run() -> theseus::Result<Credentials> {
let url = auth::authenticate_begin_flow().await?;
println!("URL {}", url.as_str());
webbrowser::open(url.as_str())?;
webbrowser::open(url.as_str())
.map_err(|e| IOError::with_path(e, url.as_str()))?;
let credentials = auth::authenticate_await_complete_flow().await?;
State::sync().await?;