Compare commits
10 Commits
v0.7.0
...
pre-monore
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8140db32dd | ||
|
|
13db5f4423 | ||
|
|
7394fdc162 | ||
|
|
a4f133eb46 | ||
|
|
53007465cd | ||
|
|
e1a748016a | ||
|
|
89c7adfbcd | ||
|
|
4de64d9a43 | ||
|
|
deedf4fc8b | ||
|
|
e9e99956ad |
2
.github/workflows/tauri-build.yml
vendored
2
.github/workflows/tauri-build.yml
vendored
@@ -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
34
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
|
||||
members = [
|
||||
"theseus",
|
||||
|
||||
50
README.md
50
README.md
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "theseus_gui",
|
||||
"private": true,
|
||||
"version": "0.7.0",
|
||||
"version": "0.7.2",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "theseus_gui"
|
||||
version = "0.7.0"
|
||||
version = "0.7.2"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?)
|
||||
}
|
||||
|
||||
@@ -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": []
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user