Compare commits

..

10 Commits

Author SHA1 Message Date
IlGrandeAnonimo
8140db32dd Changed app category to “Game” to enable GameMode on macOS Sonoma 14+ (#1242)
Some references from Apple's Website:
https://support.apple.com/en-en/105118
https://forums.developer.apple.com/forums/thread/739387
2024-06-28 18:13:05 -07:00
Prospector
13db5f4423 New fancy readme (#1234)
* New fancy readme

* Add ATLauncher, clarify OneSix format support, and finish sentence.

* remove below

* missing comma

* use percentage instead of pixels for image size

* copy paste error
2024-06-28 18:12:48 -07:00
Geometrically
7394fdc162 Fix auth timestamps (#1184)
* Fix auth timestamps

* Update error message, get valid timestamp on token refresh

* fix lint
2024-05-10 10:31:34 -07:00
Corey Shupe
a4f133eb46 Include crash reports and attempt to order by age. (#1178)
* Include crash reports and attempt to order by age.

* Do all sorting within rust.

* Remove excess debug.

* Remove new once_cell dep.

* Use EPOCH as fallback instead of now()

* Fix prettier lint warnings.
2024-05-09 10:29:19 -07:00
Corey Shupe
53007465cd UUID implements copy so borrows are unnecessary. (#1154) 2024-05-09 10:25:53 -07:00
Sasha Sorokin
e1a748016a Prompt users to provide debug info to support (#1172)
Many people contacting support forget to provide the debug information,
which significantly delays the resolution time because we're forced to
ask for this information anyway, as without it we cannot tell with
certainty the issue the person is facing.

Ideally in the future it would probably make sense to give a link to the
article describing the common issues for self-help, but there's no such
article yet. Perseus however is able to give suggestion for a few issues
given that it has the debug information.
2024-05-09 10:25:21 -07:00
Geometrically
89c7adfbcd Fix auth device token (#1152) 2024-04-25 11:45:52 -07:00
Sasha Sorokin
4de64d9a43 Further auth requests consistency fixes (#1149)
* Further auth requests consistency fixes

- Generated device UUIDs are lowercase, whereas they're uppercase in MCL
- TitleId in SISU authenticate is supposed to be a string (it is in MCL)
- UseModernGamertag in SISU authorize, on the other hand, is a boolean
- Clarified charset of our requests like MCL does
- Specified rng gen call to generate u8 to fix compile error (???)

* Enable deflate, gzip and brotli compression support
2024-04-25 11:45:32 -07:00
Geometrically
deedf4fc8b Switch to PKSE OAuth impl (#1146)
* Auth pkse

* add additional fields

* fix actions

* fix lint

* Purge broken auth + bump version
2024-04-24 21:27:25 -07:00
Geometrically
e9e99956ad Remove unsafe unwraps causing crashes (#1135)
* Remove unsafe unwraps causing crashes

* run fmt

* bump version
2024-04-23 15:03:03 -07:00
24 changed files with 635 additions and 285 deletions

View File

@@ -23,7 +23,7 @@ jobs:
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
targets: aarch64-apple-darwin
targets: aarch64-apple-darwin, x86_64-apple-darwin
- name: Rust setup
if: "!startsWith(matrix.platform, 'macos')"

34
Cargo.lock generated
View File

@@ -102,6 +102,7 @@ version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60"
dependencies = [
"brotli 4.0.0",
"bzip2",
"deflate64",
"flate2",
@@ -109,6 +110,7 @@ dependencies = [
"futures-io",
"memchr",
"pin-project-lite",
"tokio",
"xz2",
"zstd 0.13.1",
"zstd-safe 7.1.0",
@@ -453,7 +455,18 @@ checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
"brotli-decompressor 2.5.1",
]
[[package]]
name = "brotli"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "125740193d7fee5cc63ab9e16c2fdc4e07c74ba755cc53b327d6ea029e9fc569"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor 3.0.0",
]
[[package]]
@@ -466,6 +479,16 @@ dependencies = [
"alloc-stdlib",
]
[[package]]
name = "brotli-decompressor"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65622a320492e09b5e0ac436b14c54ff68199bac392d0e89a6832c4518eea525"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
]
[[package]]
name = "bstr"
version = "1.9.1"
@@ -3869,6 +3892,7 @@ version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
dependencies = [
"async-compression",
"base64 0.22.0",
"bytes",
"encoding_rs",
@@ -4891,7 +4915,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1554c5857f65dbc377cefb6b97c8ac77b1cb2a90d30d3448114d5d6b48a77fc"
dependencies = [
"base64 0.21.7",
"brotli",
"brotli 3.5.0",
"ico",
"json-patch",
"plist",
@@ -5015,7 +5039,7 @@ version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75ad0bbb31fccd1f4c56275d0a5c3abdf1f59999f72cb4ef8b79b4ed42082a21"
dependencies = [
"brotli",
"brotli 3.5.0",
"ctor",
"dunce",
"glob",
@@ -5074,7 +5098,7 @@ dependencies = [
[[package]]
name = "theseus"
version = "0.7.0"
version = "0.7.2"
dependencies = [
"async-recursion",
"async-tungstenite",
@@ -5126,7 +5150,7 @@ dependencies = [
[[package]]
name = "theseus_gui"
version = "0.7.0"
version = "0.7.2"
dependencies = [
"chrono",
"cocoa 0.25.0",

View File

@@ -1,4 +1,5 @@
[workspace]
resolver = "2"
members = [
"theseus",

View File

@@ -1,9 +1,45 @@
# theseus
A game launcher which can be used as a CLI, GUI, and a library for creating and playing modrinth projects
<img src="https://github.com/modrinth/theseus/assets/6166773/51d1ca87-05c0-445a-bd18-ddd1117f7f12" alt="modrinth app: theseus (desktop app)">
Theseus aims to provide three components:
- A library (theseus)
- A CLI (theseus-cli)
- A GUI (theseus-gui)
# Modrinth App
Feel free to contribute!
<img src="https://cdn-raw.modrinth.com/app-landing/app-screenshot.webp" alt="Screenshot of the Modrinth App's home page" align="right" width="50%">
The Modrinth App, codenamed theseus, is a modern launcher for Minecraft: Java Edition with a clean look, easy-to-use interface, and deep integration into Modrinth services.
### Features
- One-click installation of modpacks
- Automatic management of Java versions
- Windows, Mac, and Linux[^1] support
- Import your instances from CurseForge, Prism[^2], ATLauncher, MultiMC[^2], or GDLauncher
- Supports offline play once you've authenticated with your Minecraft account at least once
- Fully open source under GPLv3[^3]!
[^1]: While Linux is supported, due to the wide range of distributions out there, your mileage may vary with how well the Modrinth App works on your system. We officially distribute `.deb` and `.AppImage` packages, but third party packages have been created for a number of other package platforms. Additionally, some have reported lag issues running on Linux, we believe this to be due to an upstream Tauri issue, which we hope improves with further development.
[^2]: Certain features of the OneSix format used by Prism and MultiMC are not yet supported, so some instances may not import correctly, primarily on older Minecraft versions or unsupported mod loaders.
[^3]: Modrinth's logos, branding, and other trademarks are not free for use, see the [licensing section](#license) for more information.
## Contributing
You're welcome to help contribute to the Modrinth App if you'd like! Please review our [contribution guide](https://support.modrinth.com/en/articles/8802215-contributing-to-modrinth) before attempting to contribute or make a pull request though.
## Development
To get started, install [pnpm](https://pnpm.io/), [Rust](https://www.rust-lang.org/tools/install), and the [Tauri prerequisites](https://tauri.app/v1/guides/getting-started/prerequisites/#installing) for your system. Then, run the following commands:
```
cd theseus_gui
pnpm install
pnpm run tauri dev
```
Once the commands finish, you'll be viewing a Tauri window with Nuxt.js hot reloading.
You can use `pnpm run lint` to find any eslint problems, and `pnpm run fix` to try automatically fixing those problems.
## License
The source code of the theseus repository is licensed under the GNU General Public License, Version 3 only, which is provided in the file [LICENSE](https://github.com/modrinth/theseus/blob/master/LICENSE). However, some files are licensed under a different license.
Any files depicting the Modrinth branding, including the wrench-in-labyrinth logo, the landing image, and variations thereof, are licensed as follows:
> All rights reserved. © 2020-2024 Rinth, Inc.
Forking is permitted under the GPLv3, however do be aware that you must remove all Modrinth branding, including logos, brand colors, background images, or anything else that is related to trademarks or copyrights held by Rinth, Inc.

View File

@@ -1,6 +1,6 @@
[package]
name = "theseus"
version = "0.7.0"
version = "0.7.2"
authors = ["Jai A <jaiagr+gpg@pm.me>"]
edition = "2018"
@@ -46,7 +46,7 @@ indicatif = { version = "0.17.3", optional = true }
async-tungstenite = { version = "0.25.1", features = ["tokio-runtime", "tokio-native-tls"] }
futures = "0.3"
reqwest = { version = "0.12.3", features = ["json", "stream"] }
reqwest = { version = "0.12.3", features = ["json", "stream", "deflate", "gzip", "brotli"] }
tokio = { version = "1", features = ["full"] }
tokio-stream = { version = "0.1", features = ["fs"] }
async-recursion = "1.0.4"

View File

@@ -1,23 +1,33 @@
use std::io::{Read, SeekFrom};
use std::time::SystemTime;
use futures::TryFutureExt;
use serde::{Deserialize, Serialize};
use tokio::{
fs::File,
io::{AsyncReadExt, AsyncSeekExt},
};
use crate::{
prelude::{Credentials, DirectoryInfo},
util::io::{self, IOError},
{state::ProfilePathId, State},
};
use futures::TryFutureExt;
use serde::Serialize;
use tokio::{
fs::File,
io::{AsyncReadExt, AsyncSeekExt},
};
#[derive(Serialize, Debug)]
pub struct Logs {
pub log_type: LogType,
pub filename: String,
pub age: u64,
pub output: Option<CensoredString>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
pub enum LogType {
InfoLog,
CrashReport,
}
#[derive(Serialize, Debug)]
pub struct LatestLogCursor {
pub cursor: u64,
@@ -54,15 +64,29 @@ impl CensoredString {
impl Logs {
async fn build(
log_type: LogType,
age: SystemTime,
profile_subpath: &ProfilePathId,
filename: String,
clear_contents: Option<bool>,
) -> crate::Result<Self> {
Ok(Self {
log_type,
age: age
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_else(|_| std::time::Duration::from_secs(0))
.as_secs(),
output: if clear_contents.unwrap_or(false) {
None
} else {
Some(get_output_by_filename(profile_subpath, &filename).await?)
Some(
get_output_by_filename(
profile_subpath,
log_type,
&filename,
)
.await?,
)
},
filename,
})
@@ -70,74 +94,118 @@ impl Logs {
}
#[tracing::instrument]
pub async fn get_logs(
profile_path: ProfilePathId,
pub async fn get_logs_from_type(
profile_path: &ProfilePathId,
log_type: LogType,
clear_contents: Option<bool>,
) -> crate::Result<Vec<Logs>> {
let profile_path =
if let Some(p) = crate::profile::get(&profile_path, None).await? {
p.profile_id()
} else {
return Err(crate::ErrorKind::UnmanagedProfileError(
profile_path.to_string(),
)
.into());
};
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
let mut logs = Vec::new();
logs: &mut Vec<crate::Result<Logs>>,
) -> crate::Result<()> {
let logs_folder = match log_type {
LogType::InfoLog => {
DirectoryInfo::profile_logs_dir(profile_path).await?
}
LogType::CrashReport => {
DirectoryInfo::crash_reports_dir(profile_path).await?
}
};
if logs_folder.exists() {
for entry in std::fs::read_dir(&logs_folder)
.map_err(|e| IOError::with_path(e, &logs_folder))?
{
let entry: std::fs::DirEntry =
entry.map_err(|e| IOError::with_path(e, &logs_folder))?;
let age = entry.metadata()?.created().unwrap_or_else(|_| SystemTime::UNIX_EPOCH);
let path = entry.path();
if !path.is_file() {
continue;
}
if let Some(file_name) = path.file_name() {
let file_name = file_name.to_string_lossy().to_string();
logs.push(
Logs::build(&profile_path, file_name, clear_contents).await,
Logs::build(
log_type,
age,
&profile_path,
file_name,
clear_contents,
)
.await,
);
}
}
}
Ok(())
}
#[tracing::instrument]
pub async fn get_logs(
profile_path_id: ProfilePathId,
clear_contents: Option<bool>,
) -> crate::Result<Vec<Logs>> {
let profile_path = profile_path_id.profile_path().await?;
let mut logs = Vec::new();
get_logs_from_type(
&profile_path,
LogType::InfoLog,
clear_contents,
&mut logs,
)
.await?;
get_logs_from_type(
&profile_path,
LogType::CrashReport,
clear_contents,
&mut logs,
)
.await?;
let mut logs = logs.into_iter().collect::<crate::Result<Vec<Logs>>>()?;
logs.sort_by_key(|x| x.filename.clone());
logs.sort_by(|a, b| b.age.cmp(&a.age).then(b.filename.cmp(&a.filename)));
Ok(logs)
}
#[tracing::instrument]
pub async fn get_logs_by_filename(
profile_path: ProfilePathId,
profile_path_id: ProfilePathId,
log_type: LogType,
filename: String,
) -> crate::Result<Logs> {
let profile_path =
if let Some(p) = crate::profile::get(&profile_path, None).await? {
p.profile_id()
} else {
return Err(crate::ErrorKind::UnmanagedProfileError(
profile_path.to_string(),
)
.into());
};
Ok(Logs {
output: Some(get_output_by_filename(&profile_path, &filename).await?),
filename,
})
let profile_path = profile_path_id.profile_path().await?;
let path = match log_type {
LogType::InfoLog => {
DirectoryInfo::profile_logs_dir(&profile_path).await
}
LogType::CrashReport => {
DirectoryInfo::crash_reports_dir(&profile_path).await
}
}?
.join(&filename);
let metadata = std::fs::metadata(&path)?;
let age = metadata.created().unwrap_or_else(|_| SystemTime::UNIX_EPOCH);
Logs::build(log_type, age, &profile_path, filename, Some(true)).await
}
#[tracing::instrument]
pub async fn get_output_by_filename(
profile_subpath: &ProfilePathId,
log_type: LogType,
file_name: &str,
) -> crate::Result<CensoredString> {
let state = State::get().await?;
let logs_folder = DirectoryInfo::profile_logs_dir(profile_subpath).await?;
let logs_folder = match log_type {
LogType::InfoLog => {
DirectoryInfo::profile_logs_dir(profile_subpath).await?
}
LogType::CrashReport => {
DirectoryInfo::crash_reports_dir(profile_subpath).await?
}
};
let path = logs_folder.join(file_name);
let credentials: Vec<Credentials> = state
@@ -168,7 +236,7 @@ pub async fn get_output_by_filename(
contents = [0; 1024];
}
return Ok(CensoredString::censor(result, &credentials));
} else if ext == "log" {
} else if ext == "log" || ext == "txt" {
let mut result = String::new();
let mut contents = [0; 1024];
let mut file = std::fs::File::open(&path)
@@ -194,16 +262,8 @@ pub async fn get_output_by_filename(
}
#[tracing::instrument]
pub async fn delete_logs(profile_path: ProfilePathId) -> crate::Result<()> {
let profile_path =
if let Some(p) = crate::profile::get(&profile_path, None).await? {
p.profile_id()
} else {
return Err(crate::ErrorKind::UnmanagedProfileError(
profile_path.to_string(),
)
.into());
};
pub async fn delete_logs(profile_path_id: ProfilePathId) -> crate::Result<()> {
let profile_path = profile_path_id.profile_path().await?;
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
for entry in std::fs::read_dir(&logs_folder)
@@ -220,20 +280,21 @@ pub async fn delete_logs(profile_path: ProfilePathId) -> crate::Result<()> {
#[tracing::instrument]
pub async fn delete_logs_by_filename(
profile_path: ProfilePathId,
profile_path_id: ProfilePathId,
log_type: LogType,
filename: &str,
) -> crate::Result<()> {
let profile_path =
if let Some(p) = crate::profile::get(&profile_path, None).await? {
p.profile_id()
} else {
return Err(crate::ErrorKind::UnmanagedProfileError(
profile_path.to_string(),
)
.into());
};
let profile_path = profile_path_id.profile_path().await?;
let logs_folder = match log_type {
LogType::InfoLog => {
DirectoryInfo::profile_logs_dir(&profile_path).await
}
LogType::CrashReport => {
DirectoryInfo::crash_reports_dir(&profile_path).await
}
}?;
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;
let path = logs_folder.join(filename);
io::remove_dir_all(&path).await?;
Ok(())
@@ -249,19 +310,11 @@ pub async fn get_latest_log_cursor(
#[tracing::instrument]
pub async fn get_generic_live_log_cursor(
profile_path: ProfilePathId,
profile_path_id: ProfilePathId,
log_file_name: &str,
mut cursor: u64, // 0 to start at beginning of file
) -> crate::Result<LatestLogCursor> {
let profile_path =
if let Some(p) = crate::profile::get(&profile_path, None).await? {
p.profile_id()
} else {
return Err(crate::ErrorKind::UnmanagedProfileError(
profile_path.to_string(),
)
.into());
};
let profile_path = profile_path_id.profile_path().await?;
let state = State::get().await?;
let logs_folder = DirectoryInfo::profile_logs_dir(&profile_path).await?;

View File

@@ -12,14 +12,14 @@ pub use crate::{
// Gets whether a child process stored in the state by UUID has finished
#[tracing::instrument]
pub async fn has_finished_by_uuid(uuid: &Uuid) -> crate::Result<bool> {
pub async fn has_finished_by_uuid(uuid: Uuid) -> crate::Result<bool> {
Ok(get_exit_status_by_uuid(uuid).await?.is_some())
}
// Gets the exit status of a child process stored in the state by UUID
#[tracing::instrument]
pub async fn get_exit_status_by_uuid(
uuid: &Uuid,
uuid: Uuid,
) -> crate::Result<Option<i32>> {
let state = State::get().await?;
let children = state.children.read().await;
@@ -71,7 +71,7 @@ pub async fn get_uuids_by_profile_path(
// Kill a child process stored in the state by UUID, as a string
#[tracing::instrument]
pub async fn kill_by_uuid(uuid: &Uuid) -> crate::Result<()> {
pub async fn kill_by_uuid(uuid: Uuid) -> crate::Result<()> {
let state = State::get().await?;
let children = state.children.read().await;
if let Some(mchild) = children.get(uuid) {
@@ -85,7 +85,7 @@ pub async fn kill_by_uuid(uuid: &Uuid) -> crate::Result<()> {
// Wait for a child process stored in the state by UUID
#[tracing::instrument]
pub async fn wait_for_by_uuid(uuid: &Uuid) -> crate::Result<()> {
pub async fn wait_for_by_uuid(uuid: Uuid) -> crate::Result<()> {
let state = State::get().await?;
let children = state.children.read().await;
// No error returned for already killed process

View File

@@ -106,7 +106,8 @@ pub enum ErrorKind {
#[derive(Debug)]
pub struct Error {
source: tracing_error::TracedError<ErrorKind>,
pub raw: std::sync::Arc<ErrorKind>,
pub source: tracing_error::TracedError<std::sync::Arc<ErrorKind>>,
}
impl std::error::Error for Error {
@@ -123,8 +124,12 @@ impl std::fmt::Display for Error {
impl<E: Into<ErrorKind>> From<E> for Error {
fn from(source: E) -> Self {
let error = Into::<ErrorKind>::into(source);
let boxed_error = std::sync::Arc::new(error);
Self {
source: Into::<ErrorKind>::into(source).in_current_span(),
raw: boxed_error.clone(),
source: boxed_error.in_current_span(),
}
}
}

View File

@@ -217,7 +217,7 @@ pub fn get_minecraft_arguments(
arg,
&credentials.access_token,
&credentials.username,
&credentials.id,
credentials.id,
version,
asset_index_name,
game_directory,
@@ -237,7 +237,7 @@ pub fn get_minecraft_arguments(
&x.replace(' ', TEMPORARY_REPLACE_CHAR),
&credentials.access_token,
&credentials.username,
&credentials.id,
credentials.id,
version,
asset_index_name,
game_directory,
@@ -257,7 +257,7 @@ fn parse_minecraft_argument(
argument: &str,
access_token: &str,
username: &str,
uuid: &Uuid,
uuid: Uuid,
version: &str,
asset_index_name: &str,
game_directory: &Path,

View File

@@ -604,8 +604,8 @@ impl Children {
}
// Returns a ref to the child
pub fn get(&self, uuid: &Uuid) -> Option<Arc<RwLock<MinecraftChild>>> {
self.0.get(uuid).cloned()
pub fn get(&self, uuid: Uuid) -> Option<Arc<RwLock<MinecraftChild>>> {
self.0.get(&uuid).cloned()
}
// Gets all PID keys
@@ -615,7 +615,7 @@ impl Children {
// Get exit status of a child by PID
// Returns None if the child is still running
pub async fn exit_status(&self, uuid: &Uuid) -> crate::Result<Option<i32>> {
pub async fn exit_status(&self, uuid: Uuid) -> crate::Result<Option<i32>> {
if let Some(child) = self.get(uuid) {
let child = child.write().await;
let status = child.current_child.write().await.try_wait().await?;
@@ -629,7 +629,7 @@ impl Children {
pub async fn running_keys(&self) -> crate::Result<Vec<Uuid>> {
let mut keys = Vec::new();
for key in self.keys() {
if let Some(child) = self.get(&key) {
if let Some(child) = self.get(key) {
let child = child.clone();
let child = child.write().await;
if child
@@ -655,7 +655,7 @@ impl Children {
let running_keys = self.running_keys().await?;
let mut keys = Vec::new();
for key in running_keys {
if let Some(child) = self.get(&key) {
if let Some(child) = self.get(key) {
let child = child.clone();
let child = child.read().await;
if child.profile_relative_path == profile_path {
@@ -672,7 +672,7 @@ impl Children {
) -> crate::Result<Vec<ProfilePathId>> {
let mut profiles = Vec::new();
for key in self.keys() {
if let Some(child) = self.get(&key) {
if let Some(child) = self.get(key) {
let child = child.clone();
let child = child.write().await;
if child
@@ -695,7 +695,7 @@ impl Children {
pub async fn running_profiles(&self) -> crate::Result<Vec<Profile>> {
let mut profiles = Vec::new();
for key in self.keys() {
if let Some(child) = self.get(&key) {
if let Some(child) = self.get(key) {
let child = child.clone();
let child = child.write().await;
if child

View File

@@ -165,6 +165,14 @@ impl DirectoryInfo {
) -> crate::Result<PathBuf> {
Ok(profile_id.get_full_path().await?.join("logs"))
}
/// Gets the crash reports dir for a given profile
#[inline]
pub async fn crash_reports_dir(
profile_id: &ProfilePathId,
) -> crate::Result<PathBuf> {
Ok(profile_id.get_full_path().await?.join("crash-reports"))
}
#[inline]
pub fn launcher_logs_dir() -> Option<PathBuf> {

View File

@@ -146,7 +146,7 @@ impl Metadata {
.join("metadata.json.bak");
if metadata_path.exists() {
std::fs::copy(&metadata_path, &metadata_backup_path).unwrap();
std::fs::copy(&metadata_path, &metadata_backup_path)?;
}
write(
@@ -154,8 +154,7 @@ impl Metadata {
&serde_json::to_vec(&metadata_fetch)?,
&state.io_semaphore,
)
.await
.unwrap();
.await?;
let mut old_metadata = state.metadata.write().await;
*old_metadata = metadata_fetch;

View File

@@ -1,6 +1,6 @@
use crate::data::DirectoryInfo;
use crate::util::fetch::{read_json, write, IoSemaphore, REQWEST_CLIENT};
use crate::State;
use crate::{ErrorKind, State};
use base64::prelude::{BASE64_STANDARD, BASE64_URL_SAFE_NO_PAD};
use base64::Engine;
use byteorder::BigEndian;
@@ -15,6 +15,7 @@ use reqwest::Response;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha2::Digest;
use std::collections::HashMap;
use std::future::Future;
use uuid::Uuid;
@@ -34,6 +35,8 @@ pub enum MinecraftAuthStep {
#[derive(thiserror::Error, Debug)]
pub enum MinecraftAuthenticationError {
#[error("Error reading public key during generation")]
ReadingPublicKey,
#[error("Failed to serialize private key to PEM: {0}")]
PEMSerialize(#[from] p256::pkcs8::Error),
#[error("Failed to serialize body to JSON during step {step:?}: {source}")]
@@ -43,13 +46,14 @@ pub enum MinecraftAuthenticationError {
source: serde_json::Error,
},
#[error(
"Failed to deserialize response to JSON during step {step:?}: {source}"
"Failed to deserialize response to JSON during step {step:?}: {source}. Status Code: {status_code} Body: {raw}"
)]
DeserializeResponse {
step: MinecraftAuthStep,
raw: String,
#[source]
source: serde_json::Error,
status_code: reqwest::StatusCode,
},
#[error("Request failed during step {step:?}: {source}")]
Request {
@@ -63,6 +67,8 @@ pub enum MinecraftAuthenticationError {
#[source]
source: std::io::Error,
},
#[error("Error reading XBOX Session ID header")]
NoSessionId,
#[error("Error reading user hash")]
NoUserHash,
}
@@ -80,6 +86,7 @@ pub struct SaveDeviceToken {
#[derive(Serialize, Deserialize, Debug)]
pub struct MinecraftLoginFlow {
pub verifier: String,
pub challenge: String,
pub session_id: String,
pub redirect_uri: String,
@@ -127,11 +134,13 @@ impl MinecraftAuthStore {
#[tracing::instrument(skip(self))]
async fn refresh_and_get_device_token(
&mut self,
) -> crate::Result<(DeviceTokenKey, DeviceToken)> {
current_date: DateTime<Utc>,
force_generate: bool,
) -> crate::Result<(DeviceTokenKey, DeviceToken, DateTime<Utc>, bool)> {
macro_rules! generate_key {
($self:ident, $generate_key:expr, $device_token:expr, $SaveDeviceToken:path) => {{
let key = generate_key()?;
let token = device_token(&key).await?;
let res = device_token(&key, current_date).await?;
self.token = Some(SaveDeviceToken {
id: key.id.clone(),
@@ -144,19 +153,20 @@ impl MinecraftAuthStore {
.to_string(),
x: key.x.clone(),
y: key.y.clone(),
token: token.clone(),
token: res.value.clone(),
});
self.save().await?;
(key, token)
(key, res.value, res.date, true)
}};
}
let (key, token) = if let Some(ref token) = self.token {
if token.token.not_after > Utc::now() {
if let Ok(private_key) =
SigningKey::from_pkcs8_pem(&token.private_key)
{
let (key, token, date, valid_date) = if let Some(ref token) = self.token
{
if let Ok(private_key) =
SigningKey::from_pkcs8_pem(&token.private_key)
{
if token.token.not_after > Utc::now() && !force_generate {
(
DeviceTokenKey {
id: token.id.clone(),
@@ -165,14 +175,20 @@ impl MinecraftAuthStore {
y: token.y.clone(),
},
token.token.clone(),
current_date,
false,
)
} else {
generate_key!(
self,
generate_key,
device_token,
SaveDeviceToken
)
let key = DeviceTokenKey {
id: token.id.clone(),
key: private_key,
x: token.x.clone(),
y: token.y.clone(),
};
let res = device_token(&key, current_date).await?;
(key, res.value, res.date, true)
}
} else {
generate_key!(self, generate_key, device_token, SaveDeviceToken)
@@ -181,22 +197,60 @@ impl MinecraftAuthStore {
generate_key!(self, generate_key, device_token, SaveDeviceToken)
};
Ok((key, token))
Ok((key, token, date, valid_date))
}
#[tracing::instrument(skip(self))]
pub async fn login_begin(&mut self) -> crate::Result<MinecraftLoginFlow> {
let (key, token) = self.refresh_and_get_device_token().await?;
let (key, token, current_date, valid_date) =
self.refresh_and_get_device_token(Utc::now(), false).await?;
let challenge = generate_oauth_challenge();
let (session_id, redirect_uri) =
sisu_authenticate(&token.token, &challenge, &key).await?;
let verifier = generate_oauth_challenge();
let mut hasher = sha2::Sha256::new();
hasher.update(&verifier);
let result = hasher.finalize();
let challenge = BASE64_URL_SAFE_NO_PAD.encode(result);
Ok(MinecraftLoginFlow {
challenge,
session_id,
redirect_uri: redirect_uri.msa_oauth_redirect,
})
match sisu_authenticate(&token.token, &challenge, &key, current_date)
.await
{
Ok((session_id, redirect_uri)) => Ok(MinecraftLoginFlow {
verifier,
challenge,
session_id,
redirect_uri: redirect_uri.value.msa_oauth_redirect,
}),
Err(err) => {
if !valid_date {
let (key, token, current_date, _) = self
.refresh_and_get_device_token(Utc::now(), false)
.await?;
let verifier = generate_oauth_challenge();
let mut hasher = sha2::Sha256::new();
hasher.update(&verifier);
let result = hasher.finalize();
let challenge = BASE64_URL_SAFE_NO_PAD.encode(result);
let (session_id, redirect_uri) = sisu_authenticate(
&token.token,
&challenge,
&key,
current_date,
)
.await?;
Ok(MinecraftLoginFlow {
verifier,
challenge,
session_id,
redirect_uri: redirect_uri.value.msa_oauth_redirect,
})
} else {
Err(crate::ErrorKind::from(err).into())
}
}
}
}
#[tracing::instrument(skip(self))]
@@ -205,20 +259,27 @@ impl MinecraftAuthStore {
code: &str,
flow: MinecraftLoginFlow,
) -> crate::Result<Credentials> {
let (key, token) = self.refresh_and_get_device_token().await?;
let (key, token, _, _) =
self.refresh_and_get_device_token(Utc::now(), false).await?;
let oauth_token = oauth_token(code, &flow.challenge).await?;
let oauth_token = oauth_token(code, &flow.verifier).await?;
let sisu_authorize = sisu_authorize(
Some(&flow.session_id),
&oauth_token.access_token,
&oauth_token.value.access_token,
&token.token,
&key,
oauth_token.date,
)
.await?;
let xbox_token =
xsts_authorize(sisu_authorize, &token.token, &key).await?;
let minecraft_token = minecraft_token(xbox_token).await?;
let xbox_token = xsts_authorize(
sisu_authorize.value,
&token.token,
&key,
sisu_authorize.date,
)
.await?;
let minecraft_token = minecraft_token(xbox_token.value).await?;
minecraft_entitlements(&minecraft_token.access_token).await?;
@@ -230,9 +291,9 @@ impl MinecraftAuthStore {
id: profile_id,
username: profile.name,
access_token: minecraft_token.access_token,
refresh_token: oauth_token.refresh_token,
expires: Utc::now()
+ Duration::seconds(oauth_token.expires_in as i64),
refresh_token: oauth_token.value.refresh_token,
expires: oauth_token.date
+ Duration::seconds(oauth_token.value.expires_in as i64),
};
self.users.insert(profile_id, credentials.clone());
@@ -246,6 +307,52 @@ impl MinecraftAuthStore {
Ok(credentials)
}
async fn refresh_token(
&mut self,
creds: &Credentials,
) -> crate::Result<Option<Credentials>> {
let cred_id = creds.id;
let profile_name = creds.username.clone();
let oauth_token = oauth_refresh(&creds.refresh_token).await?;
let (key, token, current_date, _) = self
.refresh_and_get_device_token(oauth_token.date, false)
.await?;
let sisu_authorize = sisu_authorize(
None,
&oauth_token.value.access_token,
&token.token,
&key,
current_date,
)
.await?;
let xbox_token = xsts_authorize(
sisu_authorize.value,
&token.token,
&key,
sisu_authorize.date,
)
.await?;
let minecraft_token = minecraft_token(xbox_token.value).await?;
let val = Credentials {
id: cred_id,
username: profile_name,
access_token: minecraft_token.access_token,
refresh_token: oauth_token.value.refresh_token,
expires: oauth_token.date
+ Duration::seconds(oauth_token.value.expires_in as i64),
};
self.users.insert(val.id, val.clone());
self.save().await?;
Ok(Some(val))
}
#[tracing::instrument(skip(self))]
pub async fn get_default_credential(
&mut self,
@@ -267,38 +374,28 @@ impl MinecraftAuthStore {
}
if creds.expires < Utc::now() {
let cred_id = creds.id;
let profile_name = creds.username.clone();
let old_credentials = creds.clone();
let oauth_token = oauth_refresh(&creds.refresh_token).await?;
let (key, token) = self.refresh_and_get_device_token().await?;
let res = self.refresh_token(&old_credentials).await;
let sisu_authorize = sisu_authorize(
None,
&oauth_token.access_token,
&token.token,
&key,
)
.await?;
match res {
Ok(val) => Ok(val),
Err(err) => {
if let ErrorKind::MinecraftAuthenticationError(
MinecraftAuthenticationError::Request {
ref source,
..
},
) = *err.raw
{
if source.is_connect() || source.is_timeout() {
return Ok(Some(old_credentials));
}
}
let xbox_token =
xsts_authorize(sisu_authorize, &token.token, &key).await?;
let minecraft_token = minecraft_token(xbox_token).await?;
let val = Credentials {
id: cred_id,
username: profile_name,
access_token: minecraft_token.access_token,
refresh_token: oauth_token.refresh_token,
expires: Utc::now()
+ Duration::seconds(oauth_token.expires_in as i64),
};
self.users.insert(val.id, val.clone());
self.save().await?;
Ok(Some(val))
Err(err)
}
}
} else {
Ok(Some(creds.clone()))
}
@@ -331,6 +428,11 @@ const MICROSOFT_CLIENT_ID: &str = "00000000402b5328";
const REDIRECT_URL: &str = "https://login.live.com/oauth20_desktop.srf";
const REQUESTED_SCOPES: &str = "service::user.auth.xboxlive.com::MBI_SSL";
struct RequestWithDate<T> {
pub date: DateTime<Utc>,
pub value: T,
}
// flow steps
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "PascalCase")]
@@ -344,8 +446,9 @@ pub struct DeviceToken {
#[tracing::instrument(skip(key))]
pub async fn device_token(
key: &DeviceTokenKey,
) -> Result<DeviceToken, MinecraftAuthenticationError> {
Ok(send_signed_request(
current_date: DateTime<Utc>,
) -> Result<RequestWithDate<DeviceToken>, MinecraftAuthenticationError> {
let res = send_signed_request(
None,
"https://device.auth.xboxlive.com/device/authenticate",
"/device/authenticate",
@@ -370,9 +473,14 @@ pub async fn device_token(
}),
key,
MinecraftAuthStep::GetDeviceToken,
current_date,
)
.await?
.1)
.await?;
Ok(RequestWithDate {
date: res.current_date,
value: res.body,
})
}
#[derive(Deserialize)]
@@ -386,8 +494,10 @@ async fn sisu_authenticate(
token: &str,
challenge: &str,
key: &DeviceTokenKey,
) -> Result<(String, RedirectUri), MinecraftAuthenticationError> {
let (headers, res) = send_signed_request(
current_date: DateTime<Utc>,
) -> Result<(String, RequestWithDate<RedirectUri>), MinecraftAuthenticationError>
{
let res = send_signed_request::<RedirectUri>(
None,
"https://sisu.xboxlive.com/authenticate",
"/authenticate",
@@ -399,26 +509,35 @@ async fn sisu_authenticate(
],
"Query": {
"code_challenge": challenge,
"code_challenge_method": "plain",
"state": "",
"code_challenge_method": "S256",
"state": generate_oauth_challenge(),
"prompt": "select_account"
},
"RedirectUri": REDIRECT_URL,
"Sandbox": "RETAIL",
"TokenType": "code",
"TitleId": "1794566092",
}),
key,
MinecraftAuthStep::SisuAuthenicate,
current_date,
)
.await?;
let session_id = headers
let session_id = res
.headers
.get("X-SessionId")
.and_then(|x| x.to_str().ok())
.unwrap()
.ok_or_else(|| MinecraftAuthenticationError::NoSessionId)?
.to_string();
Ok((session_id, res))
Ok((
session_id,
RequestWithDate {
date: res.current_date,
value: res.body,
},
))
}
#[derive(Deserialize)]
@@ -435,12 +554,12 @@ struct OAuthToken {
#[tracing::instrument]
async fn oauth_token(
code: &str,
challenge: &str,
) -> Result<OAuthToken, MinecraftAuthenticationError> {
verifier: &str,
) -> Result<RequestWithDate<OAuthToken>, MinecraftAuthenticationError> {
let mut query = HashMap::new();
query.insert("client_id", "00000000402b5328");
query.insert("code", code);
query.insert("code_verifier", challenge);
query.insert("code_verifier", verifier);
query.insert("grant_type", "authorization_code");
query.insert("redirect_uri", "https://login.live.com/oauth20_desktop.srf");
query.insert("scope", "service::user.auth.xboxlive.com::MBI_SSL");
@@ -458,6 +577,8 @@ async fn oauth_token(
step: MinecraftAuthStep::GetOAuthToken,
})?;
let status = res.status();
let current_date = get_date_header(res.headers());
let text = res.text().await.map_err(|source| {
MinecraftAuthenticationError::Request {
source,
@@ -465,19 +586,25 @@ async fn oauth_token(
}
})?;
serde_json::from_str(&text).map_err(|source| {
let body = serde_json::from_str(&text).map_err(|source| {
MinecraftAuthenticationError::DeserializeResponse {
source,
raw: text,
step: MinecraftAuthStep::GetOAuthToken,
status_code: status,
}
})?;
Ok(RequestWithDate {
date: current_date,
value: body,
})
}
#[tracing::instrument]
async fn oauth_refresh(
refresh_token: &str,
) -> Result<OAuthToken, MinecraftAuthenticationError> {
) -> Result<RequestWithDate<OAuthToken>, MinecraftAuthenticationError> {
let mut query = HashMap::new();
query.insert("client_id", "00000000402b5328");
query.insert("refresh_token", refresh_token);
@@ -498,6 +625,8 @@ async fn oauth_refresh(
step: MinecraftAuthStep::RefreshOAuthToken,
})?;
let status = res.status();
let current_date = get_date_header(res.headers());
let text = res.text().await.map_err(|source| {
MinecraftAuthenticationError::Request {
source,
@@ -505,12 +634,18 @@ async fn oauth_refresh(
}
})?;
serde_json::from_str(&text).map_err(|source| {
let body = serde_json::from_str(&text).map_err(|source| {
MinecraftAuthenticationError::DeserializeResponse {
source,
raw: text,
step: MinecraftAuthStep::RefreshOAuthToken,
status_code: status,
}
})?;
Ok(RequestWithDate {
date: current_date,
value: body,
})
}
@@ -531,8 +666,9 @@ async fn sisu_authorize(
access_token: &str,
device_token: &str,
key: &DeviceTokenKey,
) -> Result<SisuAuthorize, MinecraftAuthenticationError> {
Ok(send_signed_request(
current_date: DateTime<Utc>,
) -> Result<RequestWithDate<SisuAuthorize>, MinecraftAuthenticationError> {
let res = send_signed_request(
None,
"https://sisu.xboxlive.com/authorize",
"/authorize",
@@ -551,12 +687,19 @@ async fn sisu_authorize(
"Sandbox": "RETAIL",
"SessionId": session_id,
"SiteName": "user.auth.xboxlive.com",
"RelyingParty": "http://xboxlive.com",
"UseModernGamertag": true
}),
key,
MinecraftAuthStep::SisuAuthorize,
current_date,
)
.await?
.1)
.await?;
Ok(RequestWithDate {
date: res.current_date,
value: res.body,
})
}
#[tracing::instrument(skip(key))]
@@ -564,8 +707,9 @@ async fn xsts_authorize(
authorize: SisuAuthorize,
device_token: &str,
key: &DeviceTokenKey,
) -> Result<DeviceToken, MinecraftAuthenticationError> {
Ok(send_signed_request(
current_date: DateTime<Utc>,
) -> Result<RequestWithDate<DeviceToken>, MinecraftAuthenticationError> {
let res = send_signed_request(
None,
"https://xsts.auth.xboxlive.com/xsts/authorize",
"/xsts/authorize",
@@ -581,9 +725,14 @@ async fn xsts_authorize(
}),
key,
MinecraftAuthStep::XstsAuthorize,
current_date,
)
.await?
.1)
.await?;
Ok(RequestWithDate {
date: res.current_date,
value: res.body,
})
}
#[derive(Deserialize)]
@@ -624,6 +773,7 @@ async fn minecraft_token(
step: MinecraftAuthStep::MinecraftToken,
})?;
let status = res.status();
let text = res.text().await.map_err(|source| {
MinecraftAuthenticationError::Request {
source,
@@ -636,6 +786,7 @@ async fn minecraft_token(
source,
raw: text,
step: MinecraftAuthStep::MinecraftToken,
status_code: status,
}
})
}
@@ -663,6 +814,7 @@ async fn minecraft_profile(
step: MinecraftAuthStep::MinecraftProfile,
})?;
let status = res.status();
let text = res.text().await.map_err(|source| {
MinecraftAuthenticationError::Request {
source,
@@ -675,6 +827,7 @@ async fn minecraft_profile(
source,
raw: text,
step: MinecraftAuthStep::MinecraftProfile,
status_code: status,
}
})
}
@@ -696,6 +849,7 @@ async fn minecraft_entitlements(
})
.await.map_err(|source| MinecraftAuthenticationError::Request { source, step: MinecraftAuthStep::MinecraftEntitlements })?;
let status = res.status();
let text = res.text().await.map_err(|source| {
MinecraftAuthenticationError::Request {
source,
@@ -708,6 +862,7 @@ async fn minecraft_entitlements(
source,
raw: text,
step: MinecraftAuthStep::MinecraftEntitlements,
status_code: status,
}
})
}
@@ -720,25 +875,33 @@ async fn auth_retry<F>(
where
F: Future<Output = Result<Response, reqwest::Error>>,
{
const RETRY_COUNT: usize = 9; // Does command 9 times
const RETRY_COUNT: usize = 5; // Does command 9 times
const RETRY_WAIT: std::time::Duration =
std::time::Duration::from_millis(250);
let mut resp = reqwest_request().await?;
let mut resp = reqwest_request().await;
for i in 0..RETRY_COUNT {
if resp.status().is_success() {
break;
match &resp {
Ok(_) => {
break;
}
Err(err) => {
if err.is_connect() || err.is_timeout() {
if i < RETRY_COUNT - 1 {
tracing::debug!(
"Request failed with connect error, retrying...",
);
tokio::time::sleep(RETRY_WAIT).await;
resp = reqwest_request().await;
} else {
break;
}
}
}
}
tracing::debug!(
"Request failed with status code {}, retrying...",
resp.status()
);
if i < RETRY_COUNT - 1 {
tokio::time::sleep(RETRY_WAIT).await;
}
resp = reqwest_request().await?;
}
Ok(resp)
resp
}
pub struct DeviceTokenKey {
@@ -750,7 +913,7 @@ pub struct DeviceTokenKey {
#[tracing::instrument]
fn generate_key() -> Result<DeviceTokenKey, MinecraftAuthenticationError> {
let id = Uuid::new_v4().to_string();
let id = Uuid::new_v4().to_string().to_uppercase();
let signing_key = SigningKey::random(&mut OsRng);
let public_key = VerifyingKey::from(&signing_key);
@@ -760,11 +923,25 @@ fn generate_key() -> Result<DeviceTokenKey, MinecraftAuthenticationError> {
Ok(DeviceTokenKey {
id,
key: signing_key,
x: BASE64_URL_SAFE_NO_PAD.encode(encoded_point.x().unwrap()),
y: BASE64_URL_SAFE_NO_PAD.encode(encoded_point.y().unwrap()),
x: BASE64_URL_SAFE_NO_PAD.encode(
encoded_point.x().ok_or_else(|| {
MinecraftAuthenticationError::ReadingPublicKey
})?,
),
y: BASE64_URL_SAFE_NO_PAD.encode(
encoded_point.y().ok_or_else(|| {
MinecraftAuthenticationError::ReadingPublicKey
})?,
),
})
}
struct SignedRequestResponse<T> {
pub headers: HeaderMap,
pub current_date: DateTime<Utc>,
pub body: T,
}
#[tracing::instrument(skip(key))]
async fn send_signed_request<T: DeserializeOwned>(
authorization: Option<&str>,
@@ -773,14 +950,15 @@ async fn send_signed_request<T: DeserializeOwned>(
raw_body: serde_json::Value,
key: &DeviceTokenKey,
step: MinecraftAuthStep,
) -> Result<(HeaderMap, T), MinecraftAuthenticationError> {
current_date: DateTime<Utc>,
) -> Result<SignedRequestResponse<T>, MinecraftAuthenticationError> {
let auth = authorization.map_or(Vec::new(), |v| v.as_bytes().to_vec());
let body = serde_json::to_vec(&raw_body).map_err(|source| {
MinecraftAuthenticationError::SerializeBody { source, step }
})?;
let time: u128 =
{ ((Utc::now().timestamp() as u128) + 11644473600) * 10000000 };
{ ((current_date.timestamp() as u128) + 11644473600) * 10000000 };
use byteorder::WriteBytesExt;
let mut buffer = Vec::new();
@@ -840,10 +1018,13 @@ async fn send_signed_request<T: DeserializeOwned>(
let res = auth_retry(|| {
let mut request = REQWEST_CLIENT
.post(url)
.header("Content-Type", "application/json")
.header("Content-Type", "application/json; charset=utf-8")
.header("Accept", "application/json")
.header("x-xbl-contract-version", "1")
.header("signature", &signature);
.header("Signature", &signature);
if url != "https://sisu.xboxlive.com/authorize" {
request = request.header("x-xbl-contract-version", "1");
}
if let Some(auth) = authorization {
request = request.header("Authorization", auth);
@@ -854,25 +1035,44 @@ async fn send_signed_request<T: DeserializeOwned>(
.await
.map_err(|source| MinecraftAuthenticationError::Request { source, step })?;
let status = res.status();
let headers = res.headers().clone();
let res = res.text().await.map_err(|source| {
let current_date = get_date_header(&headers);
let body = res.text().await.map_err(|source| {
MinecraftAuthenticationError::Request { source, step }
})?;
let body = serde_json::from_str(&res).map_err(|source| {
let body = serde_json::from_str(&body).map_err(|source| {
MinecraftAuthenticationError::DeserializeResponse {
source,
raw: res,
raw: body,
step,
status_code: status,
}
})?;
Ok((headers, body))
Ok(SignedRequestResponse {
headers,
current_date,
body,
})
}
#[tracing::instrument]
fn get_date_header(headers: &HeaderMap) -> DateTime<Utc> {
headers
.get(reqwest::header::DATE)
.and_then(|x| x.to_str().ok())
.and_then(|x| DateTime::parse_from_rfc2822(x).ok())
.map(|x| x.with_timezone(&Utc))
.unwrap_or(Utc::now())
}
#[tracing::instrument]
fn generate_oauth_challenge() -> String {
let mut rng = rand::thread_rng();
let bytes: Vec<u8> = (0..32).map(|_| rng.gen()).collect();
let bytes: Vec<u8> = (0..64).map(|_| rng.gen::<u8>()).collect();
bytes.iter().map(|byte| format!("{:02x}", byte)).collect()
}

View File

@@ -88,6 +88,15 @@ impl ProfilePathId {
.ok_or(crate::ErrorKind::UTFError(self.0.clone()).as_error())?;
Ok(self)
}
pub async fn profile_path(&self) -> crate::Result<ProfilePathId> {
if let Some(p) = crate::profile::get(&self, None).await? {
Ok(p.profile_id())
} else {
Err(crate::ErrorKind::UnmanagedProfileError(self.to_string())
.into())
}
}
}
impl std::fmt::Display for ProfilePathId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {

View File

@@ -232,24 +232,26 @@ async fn read_icon_from_file(
zip_file_reader.file().entries().iter().position(|f| {
f.filename().as_str().unwrap_or_default() == icon_path
});
let mut bytes = Vec::new();
if zip_file_reader
.reader_with_entry(zip_index_option.unwrap())
.await?
.read_to_end_checked(&mut bytes)
.await
.is_ok()
{
let bytes = bytes::Bytes::from(bytes);
let path = write_cached_icon(
&icon_path,
cache_dir,
bytes,
io_semaphore,
)
.await?;
if let Some(zip_index) = zip_index_option {
let mut bytes = Vec::new();
if zip_file_reader
.reader_with_entry(zip_index)
.await?
.read_to_end_checked(&mut bytes)
.await
.is_ok()
{
let bytes = bytes::Bytes::from(bytes);
let path = write_cached_icon(
&icon_path,
cache_dir,
bytes,
io_semaphore,
)
.await?;
return Ok(Some(path));
return Ok(Some(path));
}
}
}
}

View File

@@ -101,8 +101,7 @@ impl Tags {
&serde_json::to_vec(&tags_fetch)?,
&state.io_semaphore,
)
.await
.unwrap();
.await?;
let mut old_tags = state.tags.write().await;
*old_tags = tags_fetch;

View File

@@ -1,7 +1,7 @@
{
"name": "theseus_gui",
"private": true,
"version": "0.7.0",
"version": "0.7.2",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -1,6 +1,6 @@
[package]
name = "theseus_gui"
version = "0.7.0"
version = "0.7.2"
description = "A Tauri App"
authors = ["you"]
license = ""

View File

@@ -3,6 +3,7 @@ use theseus::{
logs::{self, CensoredString, LatestLogCursor, Logs},
prelude::ProfilePathId,
};
use theseus::logs::LogType;
/*
A log is a struct containing the filename string, stdout, and stderr, as follows:
@@ -42,15 +43,17 @@ pub async fn logs_get_logs(
#[tauri::command]
pub async fn logs_get_logs_by_filename(
profile_path: ProfilePathId,
log_type: LogType,
filename: String,
) -> Result<Logs> {
Ok(logs::get_logs_by_filename(profile_path, filename).await?)
Ok(logs::get_logs_by_filename(profile_path, log_type, filename).await?)
}
/// Get the stdout for a profile by profile id and filename string
#[tauri::command]
pub async fn logs_get_output_by_filename(
profile_path: ProfilePathId,
log_type: LogType,
filename: String,
) -> Result<CensoredString> {
let profile_path = if let Some(p) =
@@ -64,7 +67,7 @@ pub async fn logs_get_output_by_filename(
.into());
};
Ok(logs::get_output_by_filename(&profile_path, &filename).await?)
Ok(logs::get_output_by_filename(&profile_path, log_type, &filename).await?)
}
/// Delete all logs for a profile by profile id
@@ -77,9 +80,10 @@ pub async fn logs_delete_logs(profile_path: ProfilePathId) -> Result<()> {
#[tauri::command]
pub async fn logs_delete_logs_by_filename(
profile_path: ProfilePathId,
log_type: LogType,
filename: String,
) -> Result<()> {
Ok(logs::delete_logs_by_filename(profile_path, &filename).await?)
Ok(logs::delete_logs_by_filename(profile_path, log_type, &filename).await?)
}
/// Get live log from a cursor

View File

@@ -21,7 +21,7 @@ pub fn init<R: tauri::Runtime>() -> tauri::plugin::TauriPlugin<R> {
// Checks if a process has finished by process UUID
#[tauri::command]
pub async fn process_has_finished_by_uuid(uuid: Uuid) -> Result<bool> {
Ok(process::has_finished_by_uuid(&uuid).await?)
Ok(process::has_finished_by_uuid(uuid).await?)
}
// Gets process exit status by process UUID
@@ -29,7 +29,7 @@ pub async fn process_has_finished_by_uuid(uuid: Uuid) -> Result<bool> {
pub async fn process_get_exit_status_by_uuid(
uuid: Uuid,
) -> Result<Option<i32>> {
Ok(process::get_exit_status_by_uuid(&uuid).await?)
Ok(process::get_exit_status_by_uuid(uuid).await?)
}
// Gets all process UUIDs
@@ -68,11 +68,11 @@ pub async fn process_get_all_running_profiles() -> Result<Vec<Profile>> {
// Kill a process by process UUID
#[tauri::command]
pub async fn process_kill_by_uuid(uuid: Uuid) -> Result<()> {
Ok(process::kill_by_uuid(&uuid).await?)
Ok(process::kill_by_uuid(uuid).await?)
}
// Wait for a process to finish by process UUID
#[tauri::command]
pub async fn process_wait_for_by_uuid(uuid: Uuid) -> Result<()> {
Ok(process::wait_for_by_uuid(&uuid).await?)
Ok(process::wait_for_by_uuid(uuid).await?)
}

View File

@@ -8,7 +8,7 @@
},
"package": {
"productName": "Modrinth App",
"version": "0.7.0"
"version": "0.7.2"
},
"tauri": {
"allowlist": {
@@ -49,7 +49,7 @@
"macOSPrivateApi": true,
"bundle": {
"active": true,
"category": "Entertainment",
"category": "Game",
"copyright": "",
"deb": {
"depends": []

View File

@@ -23,7 +23,10 @@ defineExpose({
supportLink.value =
'https://support.modrinth.com/en/articles/9038231-minecraft-sign-in-issues'
if (errorVal.message.includes('existing connection was forcibly closed')) {
if (
errorVal.message.includes('existing connection was forcibly closed') ||
errorVal.message.includes('error sending request for url')
) {
metadata.value.network = true
}
if (errorVal.message.includes('because the target machine actively refused it')) {
@@ -70,10 +73,6 @@ async function loginMinecraft() {
<div class="modal-body">
<div class="markdown-body">
<template v-if="errorType === 'minecraft_auth'">
<p>
Signing into Microsoft account is a complex task for the launchers, and there are a lot
of things can go wrong.
</p>
<template v-if="metadata.network">
<h3>Network issues</h3>
<p>
@@ -103,23 +102,26 @@ async function loginMinecraft() {
</p>
</template>
<template v-else>
<h3>Make sure you are signing into the right Microsoft account</h3>
<h3>Try another Microsoft account</h3>
<p>
More often than not, this error is caused by you signing into an incorrect Microsoft
account which isn't linked to Minecraft. Double check and try again!
Double check you've signed in with the right account. You may own Minecraft on a
different Microsoft account.
</p>
<h3>Try signing in and launching through the official launcher first</h3>
<div class="cta-button">
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
<LogInIcon /> Try another account
</button>
</div>
<h3>Using PC Game Pass, coming from Bedrock, or just bought the game?</h3>
<p>
If you just bought Minecraft, are coming from the Bedrock Edition world and have never
played Java before, or just subscribed to PC Game Pass, you would need to start the
game at least once using the
<a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>.
Once you're done, come back here and sign in!
Try signing in with the
<a href="https://www.minecraft.net/en-us/download">official Minecraft Launcher</a>
first. Once you're done, come back here and sign in!
</p>
</template>
<div class="cta-button">
<button class="btn btn-primary" :disabled="loadingMinecraft" @click="loginMinecraft">
<LogInIcon /> Sign in to Minecraft
<LogInIcon /> Try signing in again
</button>
</div>
<hr />
@@ -127,10 +129,10 @@ async function loginMinecraft() {
If nothing is working and you need help, visit
<a :href="supportLink">our support page</a>
and start a chat using the widget in the bottom right and we will be more than happy to
assist!
assist! Make sure to provide the following debug information to the agent:
</p>
<details>
<summary>Debug info</summary>
<summary>Debug information</summary>
{{ error.message ?? error }}
</details>
</template>
@@ -165,7 +167,7 @@ async function loginMinecraft() {
</div>
<div class="input-group push-right">
<a :href="supportLink" class="btn" @click="errorModal.hide()"><ChatIcon /> Get support</a>
<button class="btn" @click="errorModal.hide()"><XIcon /> Close</button>
<button class="btn" @clicdck="errorModal.hide()"><XIcon /> Close</button>
</div>
</div>
</Modal>

View File

@@ -22,18 +22,22 @@ export async function get_logs(profilePath, clearContents) {
}
/// Get a profile's log by filename
export async function get_logs_by_filename(profilePath, filename) {
return await invoke('plugin:logs|logs_get_logs_by_filename', { profilePath, filename })
export async function get_logs_by_filename(profilePath, logType, filename) {
return await invoke('plugin:logs|logs_get_logs_by_filename', { profilePath, logType, filename })
}
/// Get a profile's log text only by filename
export async function get_output_by_filename(profilePath, filename) {
return await invoke('plugin:logs|logs_get_output_by_filename', { profilePath, filename })
export async function get_output_by_filename(profilePath, logType, filename) {
return await invoke('plugin:logs|logs_get_output_by_filename', { profilePath, logType, filename })
}
/// Delete a profile's log by filename
export async function delete_logs_by_filename(profilePath, filename) {
return await invoke('plugin:logs|logs_delete_logs_by_filename', { profilePath, filename })
export async function delete_logs_by_filename(profilePath, logType, filename) {
return await invoke('plugin:logs|logs_delete_logs_by_filename', {
profilePath,
logType,
filename,
})
}
/// Delete all logs for a given profile
@@ -50,6 +54,7 @@ export async function delete_logs(profilePath) {
new_file: bool <- the cursor was too far, meaning that the file was likely rotated/reset. This signals to the frontend to clear the log and start over with this struct.
}
*/
// From latest.log directly
export async function get_latest_log_cursor(profilePath, cursor) {
return await invoke('plugin:logs|logs_get_latest_log_cursor', { profilePath, cursor })

View File

@@ -54,8 +54,8 @@
v-model="levelFilters[level.toLowerCase()]"
class="filter-checkbox"
>
{{ level }}</Checkbox
>
{{ level }}
</Checkbox>
</div>
</div>
<div class="log-text">
@@ -225,7 +225,7 @@ async function getLiveStdLog() {
} else {
const logCursor = await get_latest_log_cursor(
props.instance.path,
currentLiveLogCursor.value,
currentLiveLogCursor.value
).catch(handleError)
if (logCursor.new_file) {
currentLiveLog.value = ''
@@ -241,14 +241,13 @@ async function getLiveStdLog() {
async function getLogs() {
return (await get_logs(props.instance.path, true).catch(handleError))
.reverse()
.filter(
// filter out latest_stdout.log or anything without .log in it
(log) =>
log.filename !== 'latest_stdout.log' &&
log.filename !== 'latest_stdout' &&
log.stdout !== '' &&
log.filename.includes('.log'),
(log.filename.includes('.log') || log.filename.endsWith('.txt'))
)
.map((log) => {
log.name = log.filename || 'Unknown'
@@ -291,7 +290,8 @@ watch(selectedLogIndex, async (newIndex) => {
logs.value[newIndex].stdout = 'Loading...'
logs.value[newIndex].stdout = await get_output_by_filename(
props.instance.path,
logs.value[newIndex].filename,
logs.value[newIndex].log_type,
logs.value[newIndex].filename
).catch(handleError)
}
})
@@ -306,9 +306,11 @@ const deleteLog = async () => {
if (logs.value[selectedLogIndex.value] && selectedLogIndex.value !== 0) {
let deleteIndex = selectedLogIndex.value
selectedLogIndex.value = deleteIndex - 1
await delete_logs_by_filename(props.instance.path, logs.value[deleteIndex].filename).catch(
handleError,
)
await delete_logs_by_filename(
props.instance.path,
logs.value[deleteIndex].log_type,
logs.value[deleteIndex].filename
).catch(handleError)
await setLogs()
}
}
@@ -512,6 +514,7 @@ onUnmounted(() => {
justify-self: center;
}
}
.filter-group {
display: flex;
padding: 0.6rem;