You've already forked AstralRinth
forked from didirus/AstralRinth
Authentication (#37)
* Initial authentication implementation * Store user info in the database, improve encapsulation in profiles * Add user list, remove unused dependencies, add spantraces * Implement user remove, update UUID crate * Add user set-default * Revert submodule macro usage * Make tracing significantly less verbose
This commit is contained in:
1190
Cargo.lock
generated
1190
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
20
flake.nix
20
flake.nix
@@ -38,8 +38,7 @@
|
|||||||
gtk4 gdk-pixbuf atk webkitgtk dbus
|
gtk4 gdk-pixbuf atk webkitgtk dbus
|
||||||
];
|
];
|
||||||
shell = [
|
shell = [
|
||||||
toolchain
|
(with fenix; combine [toolchain default.clippy complete.rust-src rust-analyzer])
|
||||||
(with fenix; combine [toolchain default.clippy rust-analyzer])
|
|
||||||
git
|
git
|
||||||
jdk17 jdk8
|
jdk17 jdk8
|
||||||
];
|
];
|
||||||
@@ -58,19 +57,10 @@
|
|||||||
cli = utils.mkApp {
|
cli = utils.mkApp {
|
||||||
drv = self.packages.${system}.theseus-cli;
|
drv = self.packages.${system}.theseus-cli;
|
||||||
};
|
};
|
||||||
cli-test = utils.mkApp {
|
cli-dev = utils.mkApp {
|
||||||
drv = pkgs.writeShellApplication {
|
drv = self.packages.${system}.theseus-cli.overrideAttrs (old: old // {
|
||||||
name = "theseus-test-cli";
|
release = false;
|
||||||
runtimeInputs = [
|
});
|
||||||
(self.packages.${system}.theseus-cli.overrideAttrs (old: old // {
|
|
||||||
release = false;
|
|
||||||
}))
|
|
||||||
];
|
|
||||||
text = ''
|
|
||||||
DUMMY_ID="$(printf '%0.sa' {1..32})"
|
|
||||||
theseus_cli profile run -t "" -n "Test" -i "$DUMMY_ID" "$@"
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
{ pkgs ? import <nixpkgs> {} }:
|
|
||||||
|
|
||||||
pkgs.mkShell {
|
|
||||||
buildInputs = with pkgs; [
|
|
||||||
rustc cargo clippy openssl pkg-config
|
|
||||||
gtk4 gdk-pixbuf atk webkitgtk
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -7,38 +7,32 @@ edition = "2018"
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
thiserror = "1.0"
|
bytes = "1"
|
||||||
async-trait = "0.1.51"
|
|
||||||
|
|
||||||
daedalus = { version = "0.1.16", features = ["bincode"] }
|
|
||||||
|
|
||||||
bincode = { version = "2.0.0-rc.1", features = ["serde"] }
|
bincode = { version = "2.0.0-rc.1", features = ["serde"] }
|
||||||
sled = { version = "0.34.7", features = ["compression"] }
|
|
||||||
|
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
json5 = "0.4.1"
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
|
||||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
|
||||||
bytes = "1"
|
|
||||||
zip = "0.5"
|
|
||||||
zip-extensions = "0.6"
|
|
||||||
sha1 = { version = "0.6.0", features = ["std"]}
|
sha1 = { version = "0.6.0", features = ["std"]}
|
||||||
path-clean = "0.1.0"
|
sled = { version = "0.34.7", features = ["compression"] }
|
||||||
fs_extra = "1.2.0"
|
url = "2.2"
|
||||||
|
uuid = { version = "1.1", features = ["serde", "v4"] }
|
||||||
|
zip = "0.5"
|
||||||
|
|
||||||
|
chrono = { version = "0.4.19", features = ["serde"] }
|
||||||
|
daedalus = { version = "0.1.16", features = ["bincode"] }
|
||||||
dirs = "4.0"
|
dirs = "4.0"
|
||||||
|
# TODO: possibly replace with tracing to have structured logging
|
||||||
regex = "1.5"
|
|
||||||
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
futures = "0.3"
|
|
||||||
|
|
||||||
sys-info = "0.9.0"
|
|
||||||
|
|
||||||
log = "0.4.14"
|
log = "0.4.14"
|
||||||
const_format = "0.2.22"
|
regex = "1.5"
|
||||||
|
sys-info = "0.9.0"
|
||||||
|
thiserror = "1.0"
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-error = "0.2"
|
||||||
|
|
||||||
|
async-tungstenite = { version = "0.17", features = ["tokio-runtime", "tokio-native-tls"] }
|
||||||
|
futures = "0.3"
|
||||||
once_cell = "1.9.0"
|
once_cell = "1.9.0"
|
||||||
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
argh = "0.1.6"
|
argh = "0.1.6"
|
||||||
|
|||||||
100
theseus/src/api/auth.rs
Normal file
100
theseus/src/api/auth.rs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
//! Authentication flow interface
|
||||||
|
use crate::{launcher::auth as inner, State};
|
||||||
|
use futures::prelude::*;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
pub use inner::Credentials;
|
||||||
|
|
||||||
|
/// Authenticate a user with Hydra
|
||||||
|
/// To run this, you need to first spawn this function as a task, then
|
||||||
|
/// open a browser to the given URL and finally wait on the spawned future
|
||||||
|
/// with the ability to cancel in case the browser is closed before finishing
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn authenticate(
|
||||||
|
browser_url: oneshot::Sender<url::Url>,
|
||||||
|
) -> crate::Result<Credentials> {
|
||||||
|
let mut flow = inner::HydraAuthFlow::new().await?;
|
||||||
|
let state = State::get().await?;
|
||||||
|
let mut users = state.users.write().await;
|
||||||
|
|
||||||
|
let url = flow.prepare_login_url().await?;
|
||||||
|
browser_url.send(url).map_err(|url| {
|
||||||
|
crate::ErrorKind::OtherError(format!(
|
||||||
|
"Error sending browser url to parent: {url}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let credentials = flow.extract_credentials().await?;
|
||||||
|
users.insert(&credentials)?;
|
||||||
|
|
||||||
|
if state.settings.read().await.default_user.is_none() {
|
||||||
|
let mut settings = state.settings.write().await;
|
||||||
|
settings.default_user = Some(credentials.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Refresh some credentials using Hydra, if needed
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn refresh(
|
||||||
|
user: uuid::Uuid,
|
||||||
|
update_name: bool,
|
||||||
|
) -> crate::Result<Credentials> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let mut users = state.users.write().await;
|
||||||
|
|
||||||
|
futures::future::ready(users.get(user)?.ok_or_else(|| {
|
||||||
|
crate::ErrorKind::OtherError(format!(
|
||||||
|
"Tried to refresh nonexistent user with ID {user}"
|
||||||
|
))
|
||||||
|
.as_error()
|
||||||
|
}))
|
||||||
|
.and_then(|mut credentials| async move {
|
||||||
|
if chrono::offset::Utc::now() > credentials.expires {
|
||||||
|
inner::refresh_credentials(&mut credentials).await?;
|
||||||
|
if update_name {
|
||||||
|
inner::refresh_username(&mut credentials).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
users.insert(&credentials)?;
|
||||||
|
Ok(credentials)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a user account from the database
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn remove_user(user: uuid::Uuid) -> crate::Result<()> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let mut users = state.users.write().await;
|
||||||
|
|
||||||
|
if state.settings.read().await.default_user == Some(user) {
|
||||||
|
let mut settings = state.settings.write().await;
|
||||||
|
settings.default_user = users
|
||||||
|
.0
|
||||||
|
.first()?
|
||||||
|
.map(|it| uuid::Uuid::from_slice(&it.0))
|
||||||
|
.transpose()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
users.remove(user)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a user exists in Theseus
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn has_user(user: uuid::Uuid) -> crate::Result<bool> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let users = state.users.read().await;
|
||||||
|
|
||||||
|
Ok(users.contains(user)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a copy of the list of all user credentials
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn users() -> crate::Result<Box<[Credentials]>> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let users = state.users.read().await;
|
||||||
|
users.iter().collect()
|
||||||
|
}
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
//! API for interacting with Theseus
|
//! API for interacting with Theseus
|
||||||
|
pub mod auth;
|
||||||
pub mod profile;
|
pub mod profile;
|
||||||
|
|
||||||
pub mod data {
|
pub mod data {
|
||||||
pub use crate::{
|
pub use crate::state::{
|
||||||
launcher::Credentials,
|
DirectoryInfo, Hooks, JavaSettings, MemorySettings, ModLoader,
|
||||||
state::{
|
ProfileMetadata, Settings, WindowSize,
|
||||||
DirectoryInfo, Hooks, JavaSettings, MemorySettings, ModLoader,
|
|
||||||
ProfileMetadata, Settings, WindowSize,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
|
auth::{self, Credentials},
|
||||||
data::*,
|
data::*,
|
||||||
profile::{self, Profile},
|
profile::{self, Profile},
|
||||||
State,
|
State,
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
//! Theseus profile management interface
|
//! Theseus profile management interface
|
||||||
|
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
state::{JavaSettings, Profile},
|
state::{JavaSettings, Profile},
|
||||||
State,
|
State,
|
||||||
};
|
};
|
||||||
use daedalus as d;
|
use daedalus as d;
|
||||||
use std::{future::Future, path::Path};
|
use std::{
|
||||||
|
future::Future,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
use tokio::process::{Child, Command};
|
use tokio::process::{Child, Command};
|
||||||
|
|
||||||
/// Add a profile to the in-memory state
|
/// Add a profile to the in-memory state
|
||||||
|
#[tracing::instrument]
|
||||||
pub async fn add(profile: Profile) -> crate::Result<()> {
|
pub async fn add(profile: Profile) -> crate::Result<()> {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
let mut profiles = state.profiles.write().await;
|
let mut profiles = state.profiles.write().await;
|
||||||
@@ -18,6 +21,7 @@ pub async fn add(profile: Profile) -> crate::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Add a path as a profile in-memory
|
/// Add a path as a profile in-memory
|
||||||
|
#[tracing::instrument]
|
||||||
pub async fn add_path(path: &Path) -> crate::Result<()> {
|
pub async fn add_path(path: &Path) -> crate::Result<()> {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
let mut profiles = state.profiles.write().await;
|
let mut profiles = state.profiles.write().await;
|
||||||
@@ -27,6 +31,7 @@ pub async fn add_path(path: &Path) -> crate::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a profile
|
/// Remove a profile
|
||||||
|
#[tracing::instrument]
|
||||||
pub async fn remove(path: &Path) -> crate::Result<()> {
|
pub async fn remove(path: &Path) -> crate::Result<()> {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
let mut profiles = state.profiles.write().await;
|
let mut profiles = state.profiles.write().await;
|
||||||
@@ -36,19 +41,22 @@ pub async fn remove(path: &Path) -> crate::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get a profile by path,
|
/// Get a profile by path,
|
||||||
|
#[tracing::instrument]
|
||||||
pub async fn get(path: &Path) -> crate::Result<Option<Profile>> {
|
pub async fn get(path: &Path) -> crate::Result<Option<Profile>> {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
let profiles = state.profiles.read().await;
|
let profiles = state.profiles.read().await;
|
||||||
|
|
||||||
profiles.0.get(path).map_or(Ok(None), |prof| match prof {
|
profiles.0.get(path).map_or(Ok(None), |prof| match prof {
|
||||||
Some(prof) => Ok(Some(prof.clone())),
|
Some(prof) => Ok(Some(prof.clone())),
|
||||||
None => Err(crate::Error::UnloadedProfileError(
|
None => Err(crate::ErrorKind::UnloadedProfileError(
|
||||||
path.display().to_string(),
|
path.display().to_string(),
|
||||||
)),
|
)
|
||||||
|
.as_error()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a profile is already managed by Theseus
|
/// Check if a profile is already managed by Theseus
|
||||||
|
#[tracing::instrument]
|
||||||
pub async fn is_managed(profile: &Path) -> crate::Result<bool> {
|
pub async fn is_managed(profile: &Path) -> crate::Result<bool> {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
let profiles = state.profiles.read().await;
|
let profiles = state.profiles.read().await;
|
||||||
@@ -56,6 +64,7 @@ pub async fn is_managed(profile: &Path) -> crate::Result<bool> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a profile is loaded
|
/// Check if a profile is loaded
|
||||||
|
#[tracing::instrument]
|
||||||
pub async fn is_loaded(profile: &Path) -> crate::Result<bool> {
|
pub async fn is_loaded(profile: &Path) -> crate::Result<bool> {
|
||||||
let state = State::get().await?;
|
let state = State::get().await?;
|
||||||
let profiles = state.profiles.read().await;
|
let profiles = state.profiles.read().await;
|
||||||
@@ -75,29 +84,41 @@ pub async fn edit<Fut>(
|
|||||||
where
|
where
|
||||||
Fut: Future<Output = crate::Result<()>>,
|
Fut: Future<Output = crate::Result<()>>,
|
||||||
{
|
{
|
||||||
let state = State::get().await.unwrap();
|
let state = State::get().await?;
|
||||||
let mut profiles = state.profiles.write().await;
|
let mut profiles = state.profiles.write().await;
|
||||||
|
|
||||||
match profiles.0.get_mut(path) {
|
match profiles.0.get_mut(path) {
|
||||||
Some(&mut Some(ref mut profile)) => action(profile).await,
|
Some(&mut Some(ref mut profile)) => action(profile).await,
|
||||||
Some(&mut None) => Err(crate::Error::UnloadedProfileError(
|
Some(&mut None) => Err(crate::ErrorKind::UnloadedProfileError(
|
||||||
path.display().to_string(),
|
path.display().to_string(),
|
||||||
)),
|
)
|
||||||
None => Err(crate::Error::UnmanagedProfileError(
|
.as_error()),
|
||||||
|
None => Err(crate::ErrorKind::UnmanagedProfileError(
|
||||||
path.display().to_string(),
|
path.display().to_string(),
|
||||||
)),
|
)
|
||||||
|
.as_error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a copy of the profile set
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn list(
|
||||||
|
) -> crate::Result<std::collections::HashMap<PathBuf, Option<Profile>>> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let profiles = state.profiles.read().await;
|
||||||
|
Ok(profiles.0.clone())
|
||||||
|
}
|
||||||
|
|
||||||
/// Run Minecraft using a profile
|
/// Run Minecraft using a profile
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
credentials: &crate::launcher::Credentials,
|
credentials: &crate::auth::Credentials,
|
||||||
) -> crate::Result<Child> {
|
) -> crate::Result<Child> {
|
||||||
let state = State::get().await.unwrap();
|
let state = State::get().await.unwrap();
|
||||||
let settings = state.settings.read().await;
|
let settings = state.settings.read().await;
|
||||||
let profile = get(path).await?.ok_or_else(|| {
|
let profile = get(path).await?.ok_or_else(|| {
|
||||||
crate::Error::OtherError(format!(
|
crate::ErrorKind::OtherError(format!(
|
||||||
"Tried to run a nonexistent or unloaded profile at path {}!",
|
"Tried to run a nonexistent or unloaded profile at path {}!",
|
||||||
path.display()
|
path.display()
|
||||||
))
|
))
|
||||||
@@ -110,7 +131,7 @@ pub async fn run(
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|it| it.id == profile.metadata.game_version.as_ref())
|
.find(|it| it.id == profile.metadata.game_version.as_ref())
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
crate::Error::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Invalid or unknown Minecraft version: {}",
|
"Invalid or unknown Minecraft version: {}",
|
||||||
profile.metadata.game_version
|
profile.metadata.game_version
|
||||||
))
|
))
|
||||||
@@ -130,10 +151,11 @@ pub async fn run(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if !result.success() {
|
if !result.success() {
|
||||||
return Err(crate::Error::LauncherError(format!(
|
return Err(crate::ErrorKind::LauncherError(format!(
|
||||||
"Non-zero exit code for pre-launch hook: {}",
|
"Non-zero exit code for pre-launch hook: {}",
|
||||||
result.code().unwrap_or(-1)
|
result.code().unwrap_or(-1)
|
||||||
)));
|
))
|
||||||
|
.as_error());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +175,7 @@ pub async fn run(
|
|||||||
settings.java_8_path.as_ref()
|
settings.java_8_path.as_ref()
|
||||||
}
|
}
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
crate::Error::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"No Java installed for version {}",
|
"No Java installed for version {}",
|
||||||
version_info.java_version.map_or(8, |it| it.major_version),
|
version_info.java_version.map_or(8, |it| it.major_version),
|
||||||
))
|
))
|
||||||
@@ -161,10 +183,11 @@ pub async fn run(
|
|||||||
};
|
};
|
||||||
|
|
||||||
if !java_install.exists() {
|
if !java_install.exists() {
|
||||||
return Err(crate::Error::LauncherError(format!(
|
return Err(crate::ErrorKind::LauncherError(format!(
|
||||||
"Could not find Java install: {}",
|
"Could not find Java install: {}",
|
||||||
java_install.display()
|
java_install.display()
|
||||||
)));
|
))
|
||||||
|
.as_error());
|
||||||
}
|
}
|
||||||
|
|
||||||
let ref java_args = profile
|
let ref java_args = profile
|
||||||
@@ -195,21 +218,26 @@ pub async fn run(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
pub async fn kill(running: &mut Child) -> crate::Result<()> {
|
pub async fn kill(running: &mut Child) -> crate::Result<()> {
|
||||||
running.kill().await?;
|
running.kill().await?;
|
||||||
wait_for(running).await
|
wait_for(running).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
pub async fn wait_for(running: &mut Child) -> crate::Result<()> {
|
pub async fn wait_for(running: &mut Child) -> crate::Result<()> {
|
||||||
let result = running.wait().await.map_err(|err| {
|
let result = running.wait().await.map_err(|err| {
|
||||||
crate::Error::LauncherError(format!("Error running minecraft: {err}"))
|
crate::ErrorKind::LauncherError(format!(
|
||||||
|
"Error running minecraft: {err}"
|
||||||
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
match result.success() {
|
match result.success() {
|
||||||
false => Err(crate::Error::LauncherError(format!(
|
false => Err(crate::ErrorKind::LauncherError(format!(
|
||||||
"Minecraft exited with non-zero code {}",
|
"Minecraft exited with non-zero code {}",
|
||||||
result.code().unwrap_or(-1)
|
result.code().unwrap_or(-1)
|
||||||
))),
|
))
|
||||||
|
.as_error()),
|
||||||
true => Ok(()),
|
true => Ok(()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
//! Theseus error type
|
//! Theseus error type
|
||||||
|
use tracing_error::InstrumentError;
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum Error {
|
pub enum ErrorKind {
|
||||||
#[error("Filesystem error: {0}")]
|
#[error("Filesystem error: {0}")]
|
||||||
FSError(String),
|
FSError(String),
|
||||||
|
|
||||||
#[error("Serialization error (JSON): {0}")]
|
#[error("Serialization error (JSON): {0}")]
|
||||||
JSONError(#[from] serde_json::Error),
|
JSONError(#[from] serde_json::Error),
|
||||||
|
|
||||||
|
#[error("Error parsing UUID: {0}")]
|
||||||
|
UUIDError(#[from] uuid::Error),
|
||||||
|
|
||||||
#[error("Serialization error (Bincode): {0}")]
|
#[error("Serialization error (Bincode): {0}")]
|
||||||
EncodeError(#[from] bincode::error::DecodeError),
|
EncodeError(#[from] bincode::error::EncodeError),
|
||||||
|
|
||||||
#[error("Deserialization error (Bincode): {0}")]
|
#[error("Deserialization error (Bincode): {0}")]
|
||||||
DecodeError(#[from] bincode::error::EncodeError),
|
DecodeError(#[from] bincode::error::DecodeError),
|
||||||
|
|
||||||
|
#[error("Error parsing URL: {0}")]
|
||||||
|
URLError(#[from] url::ParseError),
|
||||||
|
|
||||||
#[error("Database error: {0}")]
|
#[error("Database error: {0}")]
|
||||||
DBError(#[from] sled::Error),
|
DBError(#[from] sled::Error),
|
||||||
@@ -22,6 +30,9 @@ pub enum Error {
|
|||||||
#[error("Metadata error: {0}")]
|
#[error("Metadata error: {0}")]
|
||||||
MetadataError(#[from] daedalus::Error),
|
MetadataError(#[from] daedalus::Error),
|
||||||
|
|
||||||
|
#[error("Minecraft authentication error: {0}")]
|
||||||
|
HydraError(String),
|
||||||
|
|
||||||
#[error("I/O error: {0}")]
|
#[error("I/O error: {0}")]
|
||||||
IOError(#[from] std::io::Error),
|
IOError(#[from] std::io::Error),
|
||||||
|
|
||||||
@@ -31,6 +42,12 @@ pub enum Error {
|
|||||||
#[error("Error fetching URL: {0}")]
|
#[error("Error fetching URL: {0}")]
|
||||||
FetchError(#[from] reqwest::Error),
|
FetchError(#[from] reqwest::Error),
|
||||||
|
|
||||||
|
#[error("Websocket error: {0}")]
|
||||||
|
WSError(#[from] async_tungstenite::tungstenite::Error),
|
||||||
|
|
||||||
|
#[error("Websocket closed before {0} could be received!")]
|
||||||
|
WSClosedError(String),
|
||||||
|
|
||||||
#[error("Incorrect Sha1 hash for download: {0} != {1}")]
|
#[error("Incorrect Sha1 hash for download: {0} != {1}")]
|
||||||
HashError(String, String),
|
HashError(String, String),
|
||||||
|
|
||||||
@@ -52,4 +69,35 @@ pub enum Error {
|
|||||||
OtherError(String),
|
OtherError(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Error {
|
||||||
|
source: tracing_error::TracedError<ErrorKind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {
|
||||||
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||||
|
self.source.source()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Error {
|
||||||
|
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(fmt, "{}", self.source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: Into<ErrorKind>> From<E> for Error {
|
||||||
|
fn from(source: E) -> Self {
|
||||||
|
Self {
|
||||||
|
source: Into::<ErrorKind>::into(source).in_current_span(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorKind {
|
||||||
|
pub fn as_error(self) -> Error {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub type Result<T> = core::result::Result<T, Error>;
|
pub type Result<T> = core::result::Result<T, Error>;
|
||||||
|
|||||||
@@ -40,10 +40,11 @@ pub fn get_class_paths(
|
|||||||
client_path
|
client_path
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
crate::Error::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Specified class path {} does not exist",
|
"Specified class path {} does not exist",
|
||||||
client_path.to_string_lossy()
|
client_path.to_string_lossy()
|
||||||
))
|
))
|
||||||
|
.as_error()
|
||||||
})?
|
})?
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -70,10 +71,11 @@ pub fn get_lib_path(libraries_path: &Path, lib: &str) -> crate::Result<String> {
|
|||||||
path.push(get_path_from_artifact(lib.as_ref())?);
|
path.push(get_path_from_artifact(lib.as_ref())?);
|
||||||
|
|
||||||
let path = &path.canonicalize().map_err(|_| {
|
let path = &path.canonicalize().map_err(|_| {
|
||||||
crate::Error::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Library file at path {} does not exist",
|
"Library file at path {} does not exist",
|
||||||
path.to_string_lossy()
|
path.to_string_lossy()
|
||||||
))
|
))
|
||||||
|
.as_error()
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(path.to_string_lossy().to_string())
|
Ok(path.to_string_lossy().to_string())
|
||||||
@@ -104,10 +106,11 @@ pub fn get_jvm_arguments(
|
|||||||
"-Djava.library.path={}",
|
"-Djava.library.path={}",
|
||||||
&natives_path
|
&natives_path
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
.map_err(|_| crate::Error::LauncherError(format!(
|
.map_err(|_| crate::ErrorKind::LauncherError(format!(
|
||||||
"Specified natives path {} does not exist",
|
"Specified natives path {} does not exist",
|
||||||
natives_path.to_string_lossy()
|
natives_path.to_string_lossy()
|
||||||
)))?
|
))
|
||||||
|
.as_error())?
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string()
|
.to_string()
|
||||||
));
|
));
|
||||||
@@ -142,10 +145,11 @@ fn parse_jvm_argument(
|
|||||||
&natives_path
|
&natives_path
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
crate::Error::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Specified natives path {} does not exist",
|
"Specified natives path {} does not exist",
|
||||||
natives_path.to_string_lossy()
|
natives_path.to_string_lossy()
|
||||||
))
|
))
|
||||||
|
.as_error()
|
||||||
})?
|
})?
|
||||||
.to_string_lossy(),
|
.to_string_lossy(),
|
||||||
)
|
)
|
||||||
@@ -154,10 +158,11 @@ fn parse_jvm_argument(
|
|||||||
&libraries_path
|
&libraries_path
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
crate::Error::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Specified libraries path {} does not exist",
|
"Specified libraries path {} does not exist",
|
||||||
libraries_path.to_string_lossy()
|
libraries_path.to_string_lossy()
|
||||||
))
|
))
|
||||||
|
.as_error()
|
||||||
})?
|
})?
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
@@ -239,7 +244,7 @@ fn parse_minecraft_argument(
|
|||||||
.replace("${auth_access_token}", access_token)
|
.replace("${auth_access_token}", access_token)
|
||||||
.replace("${auth_session}", access_token)
|
.replace("${auth_session}", access_token)
|
||||||
.replace("${auth_player_name}", username)
|
.replace("${auth_player_name}", username)
|
||||||
.replace("${auth_uuid}", &uuid.to_hyphenated().to_string())
|
.replace("${auth_uuid}", &uuid.hyphenated().to_string())
|
||||||
.replace("${user_properties}", "{}")
|
.replace("${user_properties}", "{}")
|
||||||
.replace("${user_type}", "mojang")
|
.replace("${user_type}", "mojang")
|
||||||
.replace("${version_name}", version)
|
.replace("${version_name}", version)
|
||||||
@@ -249,10 +254,11 @@ fn parse_minecraft_argument(
|
|||||||
&game_directory
|
&game_directory
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
crate::Error::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Specified game directory {} does not exist",
|
"Specified game directory {} does not exist",
|
||||||
game_directory.to_string_lossy()
|
game_directory.to_string_lossy()
|
||||||
))
|
))
|
||||||
|
.as_error()
|
||||||
})?
|
})?
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
@@ -262,10 +268,11 @@ fn parse_minecraft_argument(
|
|||||||
&assets_directory
|
&assets_directory
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
crate::Error::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Specified assets directory {} does not exist",
|
"Specified assets directory {} does not exist",
|
||||||
assets_directory.to_string_lossy()
|
assets_directory.to_string_lossy()
|
||||||
))
|
))
|
||||||
|
.as_error()
|
||||||
})?
|
})?
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
@@ -275,10 +282,11 @@ fn parse_minecraft_argument(
|
|||||||
&assets_directory
|
&assets_directory
|
||||||
.canonicalize()
|
.canonicalize()
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
crate::Error::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Specified assets directory {} does not exist",
|
"Specified assets directory {} does not exist",
|
||||||
assets_directory.to_string_lossy()
|
assets_directory.to_string_lossy()
|
||||||
))
|
))
|
||||||
|
.as_error()
|
||||||
})?
|
})?
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
@@ -361,17 +369,19 @@ pub async fn get_processor_main_class(
|
|||||||
Ok(tokio::task::spawn_blocking(move || {
|
Ok(tokio::task::spawn_blocking(move || {
|
||||||
let zipfile = std::fs::File::open(&path)?;
|
let zipfile = std::fs::File::open(&path)?;
|
||||||
let mut archive = zip::ZipArchive::new(zipfile).map_err(|_| {
|
let mut archive = zip::ZipArchive::new(zipfile).map_err(|_| {
|
||||||
crate::Error::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Cannot read processor at {}",
|
"Cannot read processor at {}",
|
||||||
path
|
path
|
||||||
))
|
))
|
||||||
|
.as_error()
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let file = archive.by_name("META-INF/MANIFEST.MF").map_err(|_| {
|
let file = archive.by_name("META-INF/MANIFEST.MF").map_err(|_| {
|
||||||
crate::Error::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Cannot read processor manifest at {}",
|
"Cannot read processor manifest at {}",
|
||||||
path
|
path
|
||||||
))
|
))
|
||||||
|
.as_error()
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
|
|||||||
@@ -1,8 +1,168 @@
|
|||||||
//! Authentication flow
|
//! Authentication flow based on Hydra
|
||||||
// TODO: Implement authentication
|
use async_tungstenite as ws;
|
||||||
#[derive(Debug)]
|
use bincode::{Decode, Encode};
|
||||||
|
use chrono::{prelude::*, Duration};
|
||||||
|
use futures::prelude::*;
|
||||||
|
use once_cell::sync::*;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub const HYDRA_URL: Lazy<Url> =
|
||||||
|
Lazy::new(|| Url::parse("https://hydra.modrinth.com").unwrap());
|
||||||
|
|
||||||
|
// Socket messages
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ErrorJSON {
|
||||||
|
error: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorJSON {
|
||||||
|
pub fn unwrap<'a, T: Deserialize<'a>>(data: &'a [u8]) -> crate::Result<T> {
|
||||||
|
if let Ok(err) = serde_json::from_slice::<Self>(data) {
|
||||||
|
Err(crate::ErrorKind::HydraError(err.error).as_error())
|
||||||
|
} else {
|
||||||
|
Ok(serde_json::from_slice::<T>(data)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct LoginCodeJSON {
|
||||||
|
login_code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct TokenJSON {
|
||||||
|
token: String,
|
||||||
|
refresh_token: String,
|
||||||
|
expires_after: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ProfileInfoJSON {
|
||||||
|
id: uuid::Uuid,
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login information
|
||||||
|
#[derive(Encode, Decode)]
|
||||||
pub struct Credentials {
|
pub struct Credentials {
|
||||||
|
#[bincode(with_serde)]
|
||||||
pub id: uuid::Uuid,
|
pub id: uuid::Uuid,
|
||||||
pub username: String,
|
pub username: String,
|
||||||
pub access_token: String,
|
pub access_token: String,
|
||||||
|
pub refresh_token: String,
|
||||||
|
#[bincode(with_serde)]
|
||||||
|
pub expires: DateTime<Utc>,
|
||||||
|
_ctor_scope: std::marker::PhantomData<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementation
|
||||||
|
pub struct HydraAuthFlow<S: AsyncRead + AsyncWrite + Unpin> {
|
||||||
|
socket: ws::WebSocketStream<S>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HydraAuthFlow<ws::tokio::ConnectStream> {
|
||||||
|
pub async fn new() -> crate::Result<Self> {
|
||||||
|
let sock_url = wrap_ref_builder!(
|
||||||
|
it = HYDRA_URL =>
|
||||||
|
{ it.set_scheme("wss").ok() }
|
||||||
|
);
|
||||||
|
let (socket, _) = ws::tokio::connect_async(sock_url.clone()).await?;
|
||||||
|
Ok(Self { socket })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn prepare_login_url(&mut self) -> crate::Result<Url> {
|
||||||
|
let code_resp = self
|
||||||
|
.socket
|
||||||
|
.try_next()
|
||||||
|
.await?
|
||||||
|
.ok_or(
|
||||||
|
crate::ErrorKind::WSClosedError(String::from(
|
||||||
|
"login socket ID",
|
||||||
|
))
|
||||||
|
.as_error(),
|
||||||
|
)?
|
||||||
|
.into_data();
|
||||||
|
let code = ErrorJSON::unwrap::<LoginCodeJSON>(&code_resp)?;
|
||||||
|
Ok(wrap_ref_builder!(
|
||||||
|
it = HYDRA_URL.join("login")? =>
|
||||||
|
{ it.query_pairs_mut().append_pair("id", &code.login_code); }
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extract_credentials(&mut self) -> crate::Result<Credentials> {
|
||||||
|
// Minecraft bearer token
|
||||||
|
let token_resp = self
|
||||||
|
.socket
|
||||||
|
.try_next()
|
||||||
|
.await?
|
||||||
|
.ok_or(
|
||||||
|
crate::ErrorKind::WSClosedError(String::from(
|
||||||
|
"login socket ID",
|
||||||
|
))
|
||||||
|
.as_error(),
|
||||||
|
)?
|
||||||
|
.into_data();
|
||||||
|
let token = ErrorJSON::unwrap::<TokenJSON>(&token_resp)?;
|
||||||
|
let expires =
|
||||||
|
Utc::now() + Duration::seconds(token.expires_after.into());
|
||||||
|
|
||||||
|
// Get account credentials
|
||||||
|
let info = fetch_info(&token.token).await?;
|
||||||
|
|
||||||
|
// Return structure from response
|
||||||
|
Ok(Credentials {
|
||||||
|
username: info.name,
|
||||||
|
id: info.id,
|
||||||
|
refresh_token: token.refresh_token,
|
||||||
|
access_token: token.token,
|
||||||
|
expires,
|
||||||
|
_ctor_scope: std::marker::PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh_credentials(
|
||||||
|
credentials: &mut Credentials,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
let resp = crate::config::REQWEST_CLIENT
|
||||||
|
.post(HYDRA_URL.join("/refresh")?)
|
||||||
|
.json(
|
||||||
|
&serde_json::json!({ "refresh_token": credentials.refresh_token }),
|
||||||
|
)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json::<TokenJSON>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
credentials.access_token = resp.token;
|
||||||
|
credentials.refresh_token = resp.refresh_token;
|
||||||
|
credentials.expires =
|
||||||
|
Utc::now() + Duration::seconds(resp.expires_after.into());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn refresh_username(
|
||||||
|
credentials: &mut Credentials,
|
||||||
|
) -> crate::Result<()> {
|
||||||
|
let info = fetch_info(&credentials.access_token).await?;
|
||||||
|
credentials.username = info.name;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
async fn fetch_info(token: &str) -> crate::Result<ProfileInfoJSON> {
|
||||||
|
let url =
|
||||||
|
Url::parse("https://api.minecraftservices.com/minecraft/profile")?;
|
||||||
|
Ok(crate::config::REQWEST_CLIENT
|
||||||
|
.get(url)
|
||||||
|
.header(reqwest::header::AUTHORIZATION, format!("Bearer {token}"))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?
|
||||||
|
.json::<ProfileInfoJSON>()
|
||||||
|
.await?)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ use daedalus::{
|
|||||||
modded::LoaderVersion,
|
modded::LoaderVersion,
|
||||||
};
|
};
|
||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::{fs, sync::OnceCell};
|
use tokio::{fs, sync::OnceCell};
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
pub async fn download_minecraft(
|
pub async fn download_minecraft(
|
||||||
st: &State,
|
st: &State,
|
||||||
version: &GameVersionInfo,
|
version: &GameVersionInfo,
|
||||||
@@ -33,6 +33,7 @@ pub async fn download_minecraft(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, fields(version = version.id.as_str(), loader = ?loader))]
|
||||||
pub async fn download_version_info(
|
pub async fn download_version_info(
|
||||||
st: &State,
|
st: &State,
|
||||||
version: &GameVersion,
|
version: &GameVersion,
|
||||||
@@ -69,6 +70,7 @@ pub async fn download_version_info(
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
pub async fn download_client(
|
pub async fn download_client(
|
||||||
st: &State,
|
st: &State,
|
||||||
version_info: &GameVersionInfo,
|
version_info: &GameVersionInfo,
|
||||||
@@ -78,9 +80,12 @@ pub async fn download_client(
|
|||||||
let client_download = version_info
|
let client_download = version_info
|
||||||
.downloads
|
.downloads
|
||||||
.get(&d::minecraft::DownloadType::Client)
|
.get(&d::minecraft::DownloadType::Client)
|
||||||
.ok_or(crate::Error::LauncherError(format!(
|
.ok_or(
|
||||||
"No client downloads exist for version {version}"
|
crate::ErrorKind::LauncherError(format!(
|
||||||
)))?;
|
"No client downloads exist for version {version}"
|
||||||
|
))
|
||||||
|
.as_error(),
|
||||||
|
)?;
|
||||||
let path = st
|
let path = st
|
||||||
.directories
|
.directories
|
||||||
.version_dir(version)
|
.version_dir(version)
|
||||||
@@ -99,6 +104,7 @@ pub async fn download_client(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
pub async fn download_assets_index(
|
pub async fn download_assets_index(
|
||||||
st: &State,
|
st: &State,
|
||||||
version: &GameVersionInfo,
|
version: &GameVersionInfo,
|
||||||
@@ -126,6 +132,7 @@ pub async fn download_assets_index(
|
|||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(st, index))]
|
||||||
pub async fn download_assets(
|
pub async fn download_assets(
|
||||||
st: &State,
|
st: &State,
|
||||||
with_legacy: bool,
|
with_legacy: bool,
|
||||||
@@ -180,16 +187,13 @@ pub async fn download_assets(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(st, libraries))]
|
||||||
pub async fn download_libraries(
|
pub async fn download_libraries(
|
||||||
st: &State,
|
st: &State,
|
||||||
libraries: &[Library],
|
libraries: &[Library],
|
||||||
version: &str,
|
version: &str,
|
||||||
) -> crate::Result<()> {
|
) -> crate::Result<()> {
|
||||||
log::debug!("Loading libraries");
|
log::debug!("Loading libraries");
|
||||||
let (libraries_dir, natives_dir) = (
|
|
||||||
Arc::new(st.directories.libraries_dir()),
|
|
||||||
Arc::new(st.directories.version_natives_dir(version)),
|
|
||||||
);
|
|
||||||
|
|
||||||
tokio::try_join! {
|
tokio::try_join! {
|
||||||
fs::create_dir_all(st.directories.libraries_dir()),
|
fs::create_dir_all(st.directories.libraries_dir()),
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ use tokio::process::{Child, Command};
|
|||||||
|
|
||||||
mod args;
|
mod args;
|
||||||
|
|
||||||
mod auth;
|
pub mod auth;
|
||||||
pub use auth::Credentials;
|
|
||||||
|
|
||||||
mod download;
|
mod download;
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
pub fn parse_rule(rule: &d::minecraft::Rule) -> bool {
|
pub fn parse_rule(rule: &d::minecraft::Rule) -> bool {
|
||||||
use d::minecraft::{Rule, RuleAction};
|
use d::minecraft::{Rule, RuleAction};
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@ macro_rules! processor_rules {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all, fields(path = ?instance_path))]
|
||||||
pub async fn launch_minecraft(
|
pub async fn launch_minecraft(
|
||||||
game_version: &str,
|
game_version: &str,
|
||||||
loader_version: &Option<d::modded::LoaderVersion>,
|
loader_version: &Option<d::modded::LoaderVersion>,
|
||||||
@@ -64,7 +65,7 @@ pub async fn launch_minecraft(
|
|||||||
.versions
|
.versions
|
||||||
.iter()
|
.iter()
|
||||||
.find(|it| it.id == game_version)
|
.find(|it| it.id == game_version)
|
||||||
.ok_or(crate::Error::LauncherError(format!(
|
.ok_or(crate::ErrorKind::LauncherError(format!(
|
||||||
"Invalid game version: {game_version}"
|
"Invalid game version: {game_version}"
|
||||||
)))?;
|
)))?;
|
||||||
|
|
||||||
@@ -115,8 +116,9 @@ pub async fn launch_minecraft(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut cp = processor.classpath.clone();
|
let cp = wrap_ref_builder!(cp = processor.classpath.clone() => {
|
||||||
cp.push(processor.jar.clone());
|
cp.push(processor.jar.clone())
|
||||||
|
});
|
||||||
|
|
||||||
let child = Command::new("java")
|
let child = Command::new("java")
|
||||||
.arg("-cp")
|
.arg("-cp")
|
||||||
@@ -131,7 +133,7 @@ pub async fn launch_minecraft(
|
|||||||
)?)
|
)?)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
crate::Error::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Could not find processor main class for {}",
|
"Could not find processor main class for {}",
|
||||||
processor.jar
|
processor.jar
|
||||||
))
|
))
|
||||||
@@ -145,16 +147,17 @@ pub async fn launch_minecraft(
|
|||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
crate::Error::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Error running processor: {err}",
|
"Error running processor: {err}",
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if !child.status.success() {
|
if !child.status.success() {
|
||||||
return Err(crate::Error::LauncherError(format!(
|
return Err(crate::ErrorKind::LauncherError(format!(
|
||||||
"Processor error: {}",
|
"Processor error: {}",
|
||||||
String::from_utf8_lossy(&child.stderr)
|
String::from_utf8_lossy(&child.stderr)
|
||||||
)));
|
))
|
||||||
|
.as_error());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -163,9 +166,7 @@ pub async fn launch_minecraft(
|
|||||||
let args = version_info.arguments.clone().unwrap_or_default();
|
let args = version_info.arguments.clone().unwrap_or_default();
|
||||||
let mut command = match wrapper {
|
let mut command = match wrapper {
|
||||||
Some(hook) => {
|
Some(hook) => {
|
||||||
let mut cmd = Command::new(hook);
|
wrap_ref_builder!(it = Command::new(hook) => {it.arg(java_install)})
|
||||||
cmd.arg(java_install);
|
|
||||||
cmd
|
|
||||||
}
|
}
|
||||||
None => Command::new(String::from(java_install.to_string_lossy())),
|
None => Command::new(String::from(java_install.to_string_lossy())),
|
||||||
};
|
};
|
||||||
@@ -203,10 +204,11 @@ pub async fn launch_minecraft(
|
|||||||
.stderr(Stdio::inherit());
|
.stderr(Stdio::inherit());
|
||||||
|
|
||||||
command.spawn().map_err(|err| {
|
command.spawn().map_err(|err| {
|
||||||
crate::Error::LauncherError(format!(
|
crate::ErrorKind::LauncherError(format!(
|
||||||
"Error running Minecraft (minecraft-{} @ {}): {err}",
|
"Error running Minecraft (minecraft-{} @ {}): {err}",
|
||||||
&version.id,
|
&version.id,
|
||||||
instance_path.display()
|
instance_path.display()
|
||||||
))
|
))
|
||||||
|
.as_error()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,17 @@
|
|||||||
Theseus is a library which provides utilities for launching minecraft, creating Modrinth mod packs,
|
Theseus is a library which provides utilities for launching minecraft, creating Modrinth mod packs,
|
||||||
and launching Modrinth mod packs
|
and launching Modrinth mod packs
|
||||||
*/
|
*/
|
||||||
#![warn(unused_import_braces, missing_debug_implementations)]
|
#![warn(unused_import_braces)]
|
||||||
#![deny(unused_must_use)]
|
#![deny(unused_must_use)]
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
mod util;
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
mod config;
|
mod config;
|
||||||
mod error;
|
mod error;
|
||||||
mod launcher;
|
mod launcher;
|
||||||
mod state;
|
mod state;
|
||||||
mod util;
|
|
||||||
|
|
||||||
pub use api::*;
|
pub use api::*;
|
||||||
pub use error::*;
|
pub use error::*;
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ pub struct DirectoryInfo {
|
|||||||
|
|
||||||
impl DirectoryInfo {
|
impl DirectoryInfo {
|
||||||
/// Get all paths needed for Theseus to operate properly
|
/// Get all paths needed for Theseus to operate properly
|
||||||
|
#[tracing::instrument]
|
||||||
pub async fn init() -> crate::Result<Self> {
|
pub async fn init() -> crate::Result<Self> {
|
||||||
// Working directory
|
// Working directory
|
||||||
let working_dir = std::env::current_dir().map_err(|err| {
|
let working_dir = std::env::current_dir().map_err(|err| {
|
||||||
crate::Error::FSError(format!(
|
crate::ErrorKind::FSError(format!(
|
||||||
"Could not open working directory: {err}"
|
"Could not open working directory: {err}"
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
@@ -21,12 +22,12 @@ impl DirectoryInfo {
|
|||||||
// Config directory
|
// Config directory
|
||||||
let config_dir = Self::env_path("THESEUS_CONFIG_DIR")
|
let config_dir = Self::env_path("THESEUS_CONFIG_DIR")
|
||||||
.or_else(|| Some(dirs::config_dir()?.join("theseus")))
|
.or_else(|| Some(dirs::config_dir()?.join("theseus")))
|
||||||
.ok_or(crate::Error::FSError(
|
.ok_or(crate::ErrorKind::FSError(
|
||||||
"Could not find valid config dir".to_string(),
|
"Could not find valid config dir".to_string(),
|
||||||
))?;
|
))?;
|
||||||
|
|
||||||
fs::create_dir_all(&config_dir).await.map_err(|err| {
|
fs::create_dir_all(&config_dir).await.map_err(|err| {
|
||||||
crate::Error::FSError(format!(
|
crate::ErrorKind::FSError(format!(
|
||||||
"Error creating Theseus config directory: {err}"
|
"Error creating Theseus config directory: {err}"
|
||||||
))
|
))
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use std::collections::LinkedList;
|
|||||||
const METADATA_URL: &str = "https://meta.modrinth.com/gamedata";
|
const METADATA_URL: &str = "https://meta.modrinth.com/gamedata";
|
||||||
const METADATA_DB_FIELD: &[u8] = b"metadata";
|
const METADATA_DB_FIELD: &[u8] = b"metadata";
|
||||||
|
|
||||||
|
// TODO: store as subtree in database
|
||||||
#[derive(Encode, Decode, Debug)]
|
#[derive(Encode, Decode, Debug)]
|
||||||
pub struct Metadata {
|
pub struct Metadata {
|
||||||
pub minecraft: MinecraftManifest,
|
pub minecraft: MinecraftManifest,
|
||||||
@@ -48,6 +49,7 @@ impl Metadata {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
pub async fn init(db: &sled::Db) -> crate::Result<Self> {
|
pub async fn init(db: &sled::Db) -> crate::Result<Self> {
|
||||||
let mut metadata = None;
|
let mut metadata = None;
|
||||||
|
|
||||||
@@ -84,7 +86,10 @@ impl Metadata {
|
|||||||
db.flush_async().await?;
|
db.flush_async().await?;
|
||||||
Ok(meta)
|
Ok(meta)
|
||||||
} else {
|
} else {
|
||||||
Err(crate::Error::NoValueFor(String::from("launcher metadata")))
|
Err(
|
||||||
|
crate::ErrorKind::NoValueFor(String::from("launcher metadata"))
|
||||||
|
.as_error(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,17 +8,19 @@ mod dirs;
|
|||||||
pub use self::dirs::*;
|
pub use self::dirs::*;
|
||||||
|
|
||||||
mod metadata;
|
mod metadata;
|
||||||
pub use metadata::*;
|
pub use self::metadata::*;
|
||||||
|
|
||||||
mod settings;
|
|
||||||
pub use settings::*;
|
|
||||||
|
|
||||||
mod profiles;
|
mod profiles;
|
||||||
pub use profiles::*;
|
pub use self::profiles::*;
|
||||||
|
|
||||||
|
mod settings;
|
||||||
|
pub use self::settings::*;
|
||||||
|
|
||||||
|
mod users;
|
||||||
|
pub use self::users::*;
|
||||||
|
|
||||||
// Global state
|
// Global state
|
||||||
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
|
static LAUNCHER_STATE: OnceCell<Arc<State>> = OnceCell::const_new();
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct State {
|
pub struct State {
|
||||||
/// Database, used to store some information
|
/// Database, used to store some information
|
||||||
pub(self) database: sled::Db,
|
pub(self) database: sled::Db,
|
||||||
@@ -28,52 +30,62 @@ pub struct State {
|
|||||||
pub io_semaphore: Semaphore,
|
pub io_semaphore: Semaphore,
|
||||||
/// Launcher metadata
|
/// Launcher metadata
|
||||||
pub metadata: Metadata,
|
pub metadata: Metadata,
|
||||||
|
// TODO: settings API
|
||||||
/// Launcher configuration
|
/// Launcher configuration
|
||||||
pub settings: RwLock<Settings>,
|
pub settings: RwLock<Settings>,
|
||||||
/// Launcher profile metadata
|
/// Launcher profile metadata
|
||||||
pub profiles: RwLock<Profiles>,
|
pub(crate) profiles: RwLock<Profiles>,
|
||||||
|
/// Launcher user account info
|
||||||
|
pub(crate) users: RwLock<Users>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl State {
|
impl State {
|
||||||
|
#[tracing::instrument]
|
||||||
/// Get the current launcher state, initializing it if needed
|
/// Get the current launcher state, initializing it if needed
|
||||||
pub async fn get() -> crate::Result<Arc<Self>> {
|
pub async fn get() -> crate::Result<Arc<Self>> {
|
||||||
LAUNCHER_STATE
|
LAUNCHER_STATE
|
||||||
.get_or_try_init(|| async {
|
.get_or_try_init(|| {
|
||||||
// Directories
|
async {
|
||||||
let directories = DirectoryInfo::init().await?;
|
// Directories
|
||||||
|
let directories = DirectoryInfo::init().await?;
|
||||||
|
|
||||||
// Database
|
// Database
|
||||||
// TODO: make database versioned
|
// TODO: make database versioned
|
||||||
let database =
|
let database = sled_config()
|
||||||
sled_config().path(directories.database_file()).open()?;
|
.path(directories.database_file())
|
||||||
|
.open()?;
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
let settings =
|
let settings =
|
||||||
Settings::init(&directories.settings_file()).await?;
|
Settings::init(&directories.settings_file()).await?;
|
||||||
|
|
||||||
// Metadata
|
// Launcher data
|
||||||
let metadata = Metadata::init(&database).await?;
|
let (metadata, profiles) = tokio::try_join! {
|
||||||
|
Metadata::init(&database),
|
||||||
|
Profiles::init(&database),
|
||||||
|
}?;
|
||||||
|
let users = Users::init(&database)?;
|
||||||
|
|
||||||
// Profiles
|
// Loose initializations
|
||||||
let profiles = Profiles::init(&database).await?;
|
let io_semaphore =
|
||||||
|
Semaphore::new(settings.max_concurrent_downloads);
|
||||||
|
|
||||||
// Loose initializations
|
Ok(Arc::new(Self {
|
||||||
let io_semaphore =
|
database,
|
||||||
Semaphore::new(settings.max_concurrent_downloads);
|
directories,
|
||||||
|
io_semaphore,
|
||||||
Ok(Arc::new(Self {
|
metadata,
|
||||||
database,
|
settings: RwLock::new(settings),
|
||||||
directories,
|
profiles: RwLock::new(profiles),
|
||||||
io_semaphore,
|
users: RwLock::new(users),
|
||||||
metadata,
|
}))
|
||||||
settings: RwLock::new(settings),
|
}
|
||||||
profiles: RwLock::new(profiles),
|
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map(Arc::clone)
|
.map(Arc::clone)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
/// Synchronize in-memory state with persistent state
|
/// Synchronize in-memory state with persistent state
|
||||||
pub async fn sync() -> crate::Result<()> {
|
pub async fn sync() -> crate::Result<()> {
|
||||||
let state = Self::get().await?;
|
let state = Self::get().await?;
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ use tokio::fs;
|
|||||||
const PROFILE_JSON_PATH: &str = "profile.json";
|
const PROFILE_JSON_PATH: &str = "profile.json";
|
||||||
const PROFILE_SUBTREE: &[u8] = b"profiles";
|
const PROFILE_SUBTREE: &[u8] = b"profiles";
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub(crate) struct Profiles(pub HashMap<PathBuf, Option<Profile>>);
|
||||||
pub struct Profiles(pub HashMap<PathBuf, Option<Profile>>);
|
|
||||||
|
|
||||||
// TODO: possibly add defaults to some of these values
|
// TODO: possibly add defaults to some of these values
|
||||||
pub const CURRENT_FORMAT_VERSION: u32 = 1;
|
pub const CURRENT_FORMAT_VERSION: u32 = 1;
|
||||||
@@ -84,15 +83,17 @@ pub struct JavaSettings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Profile {
|
impl Profile {
|
||||||
|
#[tracing::instrument]
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
name: String,
|
name: String,
|
||||||
version: String,
|
version: String,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
) -> crate::Result<Self> {
|
) -> crate::Result<Self> {
|
||||||
if name.trim().is_empty() {
|
if name.trim().is_empty() {
|
||||||
return Err(crate::Error::InputError(String::from(
|
return Err(crate::ErrorKind::InputError(String::from(
|
||||||
"Empty name for instance!",
|
"Empty name for instance!",
|
||||||
)));
|
))
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -114,11 +115,13 @@ impl Profile {
|
|||||||
|
|
||||||
// TODO: deduplicate these builder methods
|
// TODO: deduplicate these builder methods
|
||||||
// They are flat like this in order to allow builder-style usage
|
// They are flat like this in order to allow builder-style usage
|
||||||
|
#[tracing::instrument]
|
||||||
pub fn with_name(&mut self, name: String) -> &mut Self {
|
pub fn with_name(&mut self, name: String) -> &mut Self {
|
||||||
self.metadata.name = name;
|
self.metadata.name = name;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
pub async fn with_icon<'a>(
|
pub async fn with_icon<'a>(
|
||||||
&'a mut self,
|
&'a mut self,
|
||||||
icon: &'a Path,
|
icon: &'a Path,
|
||||||
@@ -136,17 +139,20 @@ impl Profile {
|
|||||||
|
|
||||||
Ok(self)
|
Ok(self)
|
||||||
} else {
|
} else {
|
||||||
Err(crate::Error::InputError(format!(
|
Err(crate::ErrorKind::InputError(format!(
|
||||||
"Unsupported image type: {ext}"
|
"Unsupported image type: {ext}"
|
||||||
)))
|
))
|
||||||
|
.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
pub fn with_game_version(&mut self, version: String) -> &mut Self {
|
pub fn with_game_version(&mut self, version: String) -> &mut Self {
|
||||||
self.metadata.game_version = version;
|
self.metadata.game_version = version;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
pub fn with_loader(
|
pub fn with_loader(
|
||||||
&mut self,
|
&mut self,
|
||||||
loader: ModLoader,
|
loader: ModLoader,
|
||||||
@@ -157,6 +163,7 @@ impl Profile {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
pub fn with_java_settings(
|
pub fn with_java_settings(
|
||||||
&mut self,
|
&mut self,
|
||||||
settings: Option<JavaSettings>,
|
settings: Option<JavaSettings>,
|
||||||
@@ -165,6 +172,7 @@ impl Profile {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
pub fn with_memory(
|
pub fn with_memory(
|
||||||
&mut self,
|
&mut self,
|
||||||
settings: Option<MemorySettings>,
|
settings: Option<MemorySettings>,
|
||||||
@@ -173,6 +181,7 @@ impl Profile {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
pub fn with_resolution(
|
pub fn with_resolution(
|
||||||
&mut self,
|
&mut self,
|
||||||
resolution: Option<WindowSize>,
|
resolution: Option<WindowSize>,
|
||||||
@@ -181,6 +190,7 @@ impl Profile {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
pub fn with_hooks(&mut self, hooks: Option<Hooks>) -> &mut Self {
|
pub fn with_hooks(&mut self, hooks: Option<Hooks>) -> &mut Self {
|
||||||
self.hooks = hooks;
|
self.hooks = hooks;
|
||||||
self
|
self
|
||||||
@@ -188,6 +198,7 @@ impl Profile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Profiles {
|
impl Profiles {
|
||||||
|
#[tracing::instrument(skip(db))]
|
||||||
pub async fn init(db: &sled::Db) -> crate::Result<Self> {
|
pub async fn init(db: &sled::Db) -> crate::Result<Self> {
|
||||||
let profile_db = db.get(PROFILE_SUBTREE)?.map_or(
|
let profile_db = db.get(PROFILE_SUBTREE)?.map_or(
|
||||||
Ok(Default::default()),
|
Ok(Default::default()),
|
||||||
@@ -218,19 +229,23 @@ impl Profiles {
|
|||||||
Ok(Self(profiles))
|
Ok(Self(profiles))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
pub fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
|
pub fn insert(&mut self, profile: Profile) -> crate::Result<&Self> {
|
||||||
self.0.insert(
|
self.0.insert(
|
||||||
profile
|
profile
|
||||||
.path
|
.path
|
||||||
.canonicalize()?
|
.canonicalize()?
|
||||||
.to_str()
|
.to_str()
|
||||||
.ok_or(crate::Error::UTFError(profile.path.clone()))?
|
.ok_or(
|
||||||
|
crate::ErrorKind::UTFError(profile.path.clone()).as_error(),
|
||||||
|
)?
|
||||||
.into(),
|
.into(),
|
||||||
Some(profile),
|
Some(profile),
|
||||||
);
|
);
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn insert_from<'a>(
|
pub async fn insert_from<'a>(
|
||||||
&'a mut self,
|
&'a mut self,
|
||||||
path: &'a Path,
|
path: &'a Path,
|
||||||
@@ -238,12 +253,14 @@ impl Profiles {
|
|||||||
self.insert(Self::read_profile_from_dir(&path.canonicalize()?).await?)
|
self.insert(Self::read_profile_from_dir(&path.canonicalize()?).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
pub fn remove(&mut self, path: &Path) -> crate::Result<&Self> {
|
pub fn remove(&mut self, path: &Path) -> crate::Result<&Self> {
|
||||||
let path = PathBuf::from(path.canonicalize()?.to_str().unwrap());
|
let path = PathBuf::from(path.canonicalize()?.to_str().unwrap());
|
||||||
self.0.remove(&path);
|
self.0.remove(&path);
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
pub async fn sync<'a>(
|
pub async fn sync<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
batch: &'a mut sled::Batch,
|
batch: &'a mut sled::Batch,
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ pub struct Settings {
|
|||||||
pub custom_java_args: Vec<String>,
|
pub custom_java_args: Vec<String>,
|
||||||
pub java_8_path: Option<PathBuf>,
|
pub java_8_path: Option<PathBuf>,
|
||||||
pub java_17_path: Option<PathBuf>,
|
pub java_17_path: Option<PathBuf>,
|
||||||
|
pub default_user: Option<uuid::Uuid>,
|
||||||
pub hooks: Hooks,
|
pub hooks: Hooks,
|
||||||
pub max_concurrent_downloads: usize,
|
pub max_concurrent_downloads: usize,
|
||||||
pub version: u32,
|
pub version: u32,
|
||||||
@@ -32,6 +33,7 @@ impl Default for Settings {
|
|||||||
custom_java_args: Vec::new(),
|
custom_java_args: Vec::new(),
|
||||||
java_8_path: None,
|
java_8_path: None,
|
||||||
java_17_path: None,
|
java_17_path: None,
|
||||||
|
default_user: None,
|
||||||
hooks: Hooks::default(),
|
hooks: Hooks::default(),
|
||||||
max_concurrent_downloads: 64,
|
max_concurrent_downloads: 64,
|
||||||
version: CURRENT_FORMAT_VERSION,
|
version: CURRENT_FORMAT_VERSION,
|
||||||
@@ -40,14 +42,16 @@ impl Default for Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Settings {
|
impl Settings {
|
||||||
|
#[tracing::instrument]
|
||||||
pub async fn init(file: &Path) -> crate::Result<Self> {
|
pub async fn init(file: &Path) -> crate::Result<Self> {
|
||||||
if file.exists() {
|
if file.exists() {
|
||||||
fs::read(&file)
|
fs::read(&file)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
crate::Error::FSError(format!(
|
crate::ErrorKind::FSError(format!(
|
||||||
"Error reading settings file: {err}"
|
"Error reading settings file: {err}"
|
||||||
))
|
))
|
||||||
|
.as_error()
|
||||||
})
|
})
|
||||||
.and_then(|it| {
|
.and_then(|it| {
|
||||||
serde_json::from_slice::<Settings>(&it)
|
serde_json::from_slice::<Settings>(&it)
|
||||||
@@ -58,13 +62,15 @@ impl Settings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
pub async fn sync(&self, to: &Path) -> crate::Result<()> {
|
pub async fn sync(&self, to: &Path) -> crate::Result<()> {
|
||||||
fs::write(to, serde_json::to_vec_pretty(self)?)
|
fs::write(to, serde_json::to_vec_pretty(self)?)
|
||||||
.await
|
.await
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
crate::Error::FSError(format!(
|
crate::ErrorKind::FSError(format!(
|
||||||
"Error saving settings to file: {err}"
|
"Error saving settings to file: {err}"
|
||||||
))
|
))
|
||||||
|
.as_error()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
79
theseus/src/state/users.rs
Normal file
79
theseus/src/state/users.rs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
//! User login info
|
||||||
|
use crate::{auth::Credentials, config::BINCODE_CONFIG};
|
||||||
|
|
||||||
|
const USER_DB_TREE: &[u8] = b"users";
|
||||||
|
|
||||||
|
/// The set of users stored in the launcher
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct Users(pub(crate) sled::Tree);
|
||||||
|
|
||||||
|
impl Users {
|
||||||
|
#[tracing::instrument(skip(db))]
|
||||||
|
pub fn init(db: &sled::Db) -> crate::Result<Self> {
|
||||||
|
Ok(Self(db.open_tree(USER_DB_TREE)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
pub fn insert(
|
||||||
|
&mut self,
|
||||||
|
credentials: &Credentials,
|
||||||
|
) -> crate::Result<&Self> {
|
||||||
|
let id = credentials.id.as_bytes();
|
||||||
|
self.0.insert(
|
||||||
|
id,
|
||||||
|
bincode::encode_to_vec(credentials, *BINCODE_CONFIG)?,
|
||||||
|
)?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub fn contains(&self, id: uuid::Uuid) -> crate::Result<bool> {
|
||||||
|
Ok(self.0.contains_key(id.as_bytes())?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub fn get(&self, id: uuid::Uuid) -> crate::Result<Option<Credentials>> {
|
||||||
|
self.0.get(id.as_bytes())?.map_or(Ok(None), |prof| {
|
||||||
|
bincode::decode_from_slice(&prof, *BINCODE_CONFIG)
|
||||||
|
.map_err(crate::Error::from)
|
||||||
|
.map(|it| Some(it.0))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
pub fn remove(&mut self, id: uuid::Uuid) -> crate::Result<&Self> {
|
||||||
|
self.0.remove(id.as_bytes())?;
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter(&self) -> UserIter<impl UserInnerIter> {
|
||||||
|
UserIter(self.0.iter().values(), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
alias_trait! {
|
||||||
|
pub UserInnerIter: Iterator<Item = sled::Result<sled::IVec>>, Send, Sync
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An iterator over the set of users
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct UserIter<I: UserInnerIter>(I, bool);
|
||||||
|
|
||||||
|
impl<I: UserInnerIter> Iterator for UserIter<I> {
|
||||||
|
type Item = crate::Result<Credentials>;
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self))]
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let it = self.0.next()?;
|
||||||
|
let res = it.map_err(crate::Error::from).and_then(|it| {
|
||||||
|
Ok(bincode::decode_from_slice(&it, *BINCODE_CONFIG)?.0)
|
||||||
|
});
|
||||||
|
|
||||||
|
self.1 = res.is_err();
|
||||||
|
Some(res)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ use tokio::{
|
|||||||
|
|
||||||
const FETCH_ATTEMPTS: usize = 3;
|
const FETCH_ATTEMPTS: usize = 3;
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(_permit))]
|
||||||
pub async fn fetch<'a>(
|
pub async fn fetch<'a>(
|
||||||
url: &str,
|
url: &str,
|
||||||
sha1: Option<&str>,
|
sha1: Option<&str>,
|
||||||
@@ -25,10 +26,11 @@ pub async fn fetch<'a>(
|
|||||||
if let Some(hash) = sha1 {
|
if let Some(hash) = sha1 {
|
||||||
let actual_hash = sha1_async(bytes.clone()).await;
|
let actual_hash = sha1_async(bytes.clone()).await;
|
||||||
if actual_hash != hash {
|
if actual_hash != hash {
|
||||||
return Err(crate::Error::HashError(
|
return Err(crate::ErrorKind::HashError(
|
||||||
actual_hash,
|
actual_hash,
|
||||||
String::from(hash),
|
String::from(hash),
|
||||||
));
|
)
|
||||||
|
.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +47,7 @@ pub async fn fetch<'a>(
|
|||||||
// This is implemented, as it will be useful in porting modpacks
|
// This is implemented, as it will be useful in porting modpacks
|
||||||
// For now, allow it to be dead code
|
// For now, allow it to be dead code
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
#[tracing::instrument(skip(sem))]
|
||||||
pub async fn fetch_mirrors(
|
pub async fn fetch_mirrors(
|
||||||
urls: &[&str],
|
urls: &[&str],
|
||||||
sha1: Option<&str>,
|
sha1: Option<&str>,
|
||||||
@@ -70,6 +73,7 @@ pub async fn fetch_mirrors(
|
|||||||
.map(|it| it.0)
|
.map(|it| it.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(bytes, _permit))]
|
||||||
pub async fn write<'a>(
|
pub async fn write<'a>(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
|
|||||||
@@ -1,3 +1,23 @@
|
|||||||
//! Theseus utility functions
|
//! Theseus utility functions
|
||||||
pub mod fetch;
|
pub mod fetch;
|
||||||
pub mod platform;
|
pub mod platform;
|
||||||
|
|
||||||
|
/// Wrap a builder which uses a mut reference into one which outputs an owned value
|
||||||
|
macro_rules! wrap_ref_builder {
|
||||||
|
($id:ident = $init:expr => $transform:block) => {{
|
||||||
|
let mut it = $init;
|
||||||
|
{
|
||||||
|
let $id = &mut it;
|
||||||
|
$transform;
|
||||||
|
}
|
||||||
|
it
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Alias a trait, used to avoid needing nightly features
|
||||||
|
macro_rules! alias_trait {
|
||||||
|
($scope:vis $name:ident : $bound:path $(, $bounds:path)*) => {
|
||||||
|
$scope trait $name: $bound $(+ $bounds)* {}
|
||||||
|
impl<T: $bound $(+ $bounds)*> $name for T {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,10 +15,16 @@ futures = "0.3"
|
|||||||
argh = "0.1"
|
argh = "0.1"
|
||||||
paris = { version = "1.5", features = ["macros", "no_logger"] }
|
paris = { version = "1.5", features = ["macros", "no_logger"] }
|
||||||
dialoguer = "0.10"
|
dialoguer = "0.10"
|
||||||
eyre = "0.6"
|
|
||||||
tabled = "0.5"
|
tabled = "0.5"
|
||||||
dirs = "4.0"
|
dirs = "4.0"
|
||||||
uuid = {version = "0.8", features = ["v4", "serde"]}
|
uuid = {version = "1.1", features = ["v4", "serde"]}
|
||||||
# TODO: merge logging with paris logging
|
url = "2.2"
|
||||||
pretty_env_logger = "0.4"
|
|
||||||
log = "0.4.14"
|
color-eyre = "0.6"
|
||||||
|
eyre = "0.6"
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-error = "0.2"
|
||||||
|
tracing-futures = "0.2"
|
||||||
|
tracing-subscriber = {version = "0.3", features = ["env-filter"]}
|
||||||
|
|
||||||
|
webbrowser = "0.7"
|
||||||
@@ -1,26 +1,52 @@
|
|||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
use futures::TryFutureExt;
|
use futures::TryFutureExt;
|
||||||
use paris::*;
|
use paris::*;
|
||||||
|
use tracing_error::ErrorLayer;
|
||||||
|
use tracing_futures::WithSubscriber;
|
||||||
|
use tracing_subscriber::prelude::*;
|
||||||
|
use tracing_subscriber::{fmt, EnvFilter};
|
||||||
|
|
||||||
mod subcommands;
|
#[macro_use]
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
#[derive(argh::FromArgs)]
|
mod subcommands;
|
||||||
|
|
||||||
|
#[derive(argh::FromArgs, Debug)]
|
||||||
/// The official Modrinth CLI
|
/// The official Modrinth CLI
|
||||||
pub struct Args {
|
pub struct Args {
|
||||||
#[argh(subcommand)]
|
#[argh(subcommand)]
|
||||||
pub subcommand: subcommands::SubCommand,
|
pub subcommand: subcommands::Subcommand,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tracing::instrument]
|
||||||
async fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let args = argh::from_env::<Args>();
|
let args = argh::from_env::<Args>();
|
||||||
pretty_env_logger::formatted_builder()
|
|
||||||
.filter_module("theseus", log::LevelFilter::Info)
|
color_eyre::install()?;
|
||||||
.target(pretty_env_logger::env_logger::Target::Stderr)
|
let filter = EnvFilter::try_from_default_env()
|
||||||
|
.or_else(|_| EnvFilter::try_new("info"))?;
|
||||||
|
|
||||||
|
let format = fmt::layer()
|
||||||
|
.without_time()
|
||||||
|
.with_writer(std::io::stderr)
|
||||||
|
.with_target(false)
|
||||||
|
.compact();
|
||||||
|
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(format)
|
||||||
|
.with(filter)
|
||||||
|
.with(ErrorLayer::default())
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
args.dispatch()
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
.inspect_err(|_| error!("An error has occurred!\n"))
|
.enable_all()
|
||||||
.await
|
.build()?
|
||||||
|
.block_on(
|
||||||
|
async move {
|
||||||
|
args.dispatch()
|
||||||
|
.inspect_err(|_| error!("An error has occurred!\n"))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
.with_current_subscriber(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
|
|
||||||
mod profile;
|
mod profile;
|
||||||
|
mod user;
|
||||||
|
|
||||||
#[derive(argh::FromArgs)]
|
#[derive(argh::FromArgs, Debug)]
|
||||||
#[argh(subcommand)]
|
#[argh(subcommand)]
|
||||||
pub enum SubCommand {
|
pub enum Subcommand {
|
||||||
Profile(profile::ProfileCommand),
|
Profile(profile::ProfileCommand),
|
||||||
|
User(user::UserCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::Args {
|
impl crate::Args {
|
||||||
pub async fn dispatch(&self) -> Result<()> {
|
pub async fn dispatch(&self) -> Result<()> {
|
||||||
match self.subcommand {
|
dispatch!(self.subcommand, (self) => {
|
||||||
SubCommand::Profile(ref cmd) => cmd.dispatch(self).await,
|
Subcommand::Profile,
|
||||||
}
|
Subcommand::User
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,26 @@
|
|||||||
//! Profile management subcommand
|
//! Profile management subcommand
|
||||||
use crate::util::{
|
use crate::util::{
|
||||||
confirm_async, prompt_async, select_async, table_path_display,
|
confirm_async, prompt_async, select_async, table, table_path_display,
|
||||||
};
|
};
|
||||||
use daedalus::modded::LoaderVersion;
|
use daedalus::modded::LoaderVersion;
|
||||||
use eyre::{ensure, Result};
|
use eyre::{ensure, Result};
|
||||||
use futures::prelude::*;
|
use futures::prelude::*;
|
||||||
use paris::*;
|
use paris::*;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use tabled::{Table, Tabled};
|
use tabled::Tabled;
|
||||||
use theseus::prelude::*;
|
use theseus::prelude::*;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tokio_stream::wrappers::ReadDirStream;
|
use tokio_stream::wrappers::ReadDirStream;
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(argh::FromArgs)]
|
#[derive(argh::FromArgs, Debug)]
|
||||||
#[argh(subcommand, name = "profile")]
|
#[argh(subcommand, name = "profile")]
|
||||||
/// profile management
|
/// manage Minecraft instances
|
||||||
pub struct ProfileCommand {
|
pub struct ProfileCommand {
|
||||||
#[argh(subcommand)]
|
#[argh(subcommand)]
|
||||||
action: ProfileSubcommand,
|
action: ProfileSubcommand,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(argh::FromArgs)]
|
#[derive(argh::FromArgs, Debug)]
|
||||||
#[argh(subcommand)]
|
#[argh(subcommand)]
|
||||||
pub enum ProfileSubcommand {
|
pub enum ProfileSubcommand {
|
||||||
Add(ProfileAdd),
|
Add(ProfileAdd),
|
||||||
@@ -31,7 +30,7 @@ pub enum ProfileSubcommand {
|
|||||||
Run(ProfileRun),
|
Run(ProfileRun),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(argh::FromArgs)]
|
#[derive(argh::FromArgs, Debug)]
|
||||||
#[argh(subcommand, name = "add")]
|
#[argh(subcommand, name = "add")]
|
||||||
/// add a new profile to Theseus
|
/// add a new profile to Theseus
|
||||||
pub struct ProfileAdd {
|
pub struct ProfileAdd {
|
||||||
@@ -71,7 +70,7 @@ impl ProfileAdd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(argh::FromArgs)]
|
#[derive(argh::FromArgs, Debug)]
|
||||||
#[argh(subcommand, name = "init")]
|
#[argh(subcommand, name = "init")]
|
||||||
/// create a new profile and manage it with Theseus
|
/// create a new profile and manage it with Theseus
|
||||||
pub struct ProfileInit {
|
pub struct ProfileInit {
|
||||||
@@ -260,7 +259,7 @@ impl ProfileInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(argh::FromArgs)]
|
#[derive(argh::FromArgs, Debug)]
|
||||||
/// list all managed profiles
|
/// list all managed profiles
|
||||||
#[argh(subcommand, name = "list")]
|
#[argh(subcommand, name = "list")]
|
||||||
pub struct ProfileList {}
|
pub struct ProfileList {}
|
||||||
@@ -311,16 +310,15 @@ impl ProfileList {
|
|||||||
_args: &crate::Args,
|
_args: &crate::Args,
|
||||||
_largs: &ProfileCommand,
|
_largs: &ProfileCommand,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let state = State::get().await?;
|
let profiles = profile::list().await?;
|
||||||
let profiles = state.profiles.read().await;
|
let rows = profiles.iter().map(|(path, prof)| {
|
||||||
let profiles = profiles.0.iter().map(|(path, prof)| {
|
|
||||||
prof.as_ref().map_or_else(
|
prof.as_ref().map_or_else(
|
||||||
|| ProfileRow::from(path.as_path()),
|
|| ProfileRow::from(path.as_path()),
|
||||||
ProfileRow::from,
|
ProfileRow::from,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
let table = Table::new(profiles).with(tabled::Style::psql()).with(
|
let table = table(rows).with(
|
||||||
tabled::Modify::new(tabled::Column(1..=1))
|
tabled::Modify::new(tabled::Column(1..=1))
|
||||||
.with(tabled::MaxWidth::wrapping(40)),
|
.with(tabled::MaxWidth::wrapping(40)),
|
||||||
);
|
);
|
||||||
@@ -330,7 +328,7 @@ impl ProfileList {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(argh::FromArgs)]
|
#[derive(argh::FromArgs, Debug)]
|
||||||
/// unmanage a profile
|
/// unmanage a profile
|
||||||
#[argh(subcommand, name = "remove")]
|
#[argh(subcommand, name = "remove")]
|
||||||
pub struct ProfileRemove {
|
pub struct ProfileRemove {
|
||||||
@@ -364,7 +362,7 @@ impl ProfileRemove {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(argh::FromArgs)]
|
#[derive(argh::FromArgs, Debug)]
|
||||||
/// run a profile
|
/// run a profile
|
||||||
#[argh(subcommand, name = "run")]
|
#[argh(subcommand, name = "run")]
|
||||||
pub struct ProfileRun {
|
pub struct ProfileRun {
|
||||||
@@ -372,18 +370,9 @@ pub struct ProfileRun {
|
|||||||
/// the profile to run
|
/// the profile to run
|
||||||
profile: PathBuf,
|
profile: PathBuf,
|
||||||
|
|
||||||
// TODO: auth
|
#[argh(option)]
|
||||||
#[argh(option, short = 't')]
|
/// the user to authenticate with
|
||||||
/// the Minecraft token to use for player login. Should be replaced by auth when that is a thing.
|
user: Option<uuid::Uuid>,
|
||||||
token: String,
|
|
||||||
|
|
||||||
#[argh(option, short = 'n')]
|
|
||||||
/// the uername to use for running the game
|
|
||||||
name: String,
|
|
||||||
|
|
||||||
#[argh(option, short = 'i')]
|
|
||||||
/// the account id to use for running the game
|
|
||||||
id: Uuid,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProfileRun {
|
impl ProfileRun {
|
||||||
@@ -400,11 +389,18 @@ impl ProfileRun {
|
|||||||
"Profile not managed by Theseus (if it exists, try using `profile add` first!)",
|
"Profile not managed by Theseus (if it exists, try using `profile add` first!)",
|
||||||
);
|
);
|
||||||
|
|
||||||
let credentials = Credentials {
|
let id = future::ready(self.user.ok_or(()))
|
||||||
id: self.id.clone(),
|
.or_else(|_| async move {
|
||||||
username: self.name.clone(),
|
let state = State::get().await?;
|
||||||
access_token: self.token.clone(),
|
let settings = state.settings.read().await;
|
||||||
};
|
|
||||||
|
settings.default_user
|
||||||
|
.ok_or(eyre::eyre!(
|
||||||
|
"Could not find any users, please add one using the `user add` command."
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let credentials = auth::refresh(id, false).await?;
|
||||||
|
|
||||||
let mut proc = profile::run(&path, &credentials).await?;
|
let mut proc = profile::run(&path, &credentials).await?;
|
||||||
profile::wait_for(&mut proc).await?;
|
profile::wait_for(&mut proc).await?;
|
||||||
@@ -415,14 +411,14 @@ impl ProfileRun {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ProfileCommand {
|
impl ProfileCommand {
|
||||||
pub async fn dispatch(&self, args: &crate::Args) -> Result<()> {
|
pub async fn run(&self, args: &crate::Args) -> Result<()> {
|
||||||
match &self.action {
|
dispatch!(&self.action, (args, self) => {
|
||||||
ProfileSubcommand::Add(ref cmd) => cmd.run(args, self).await,
|
ProfileSubcommand::Add,
|
||||||
ProfileSubcommand::Init(ref cmd) => cmd.run(args, self).await,
|
ProfileSubcommand::Init,
|
||||||
ProfileSubcommand::List(ref cmd) => cmd.run(args, self).await,
|
ProfileSubcommand::List,
|
||||||
ProfileSubcommand::Remove(ref cmd) => cmd.run(args, self).await,
|
ProfileSubcommand::Remove,
|
||||||
ProfileSubcommand::Run(ref cmd) => cmd.run(args, self).await,
|
ProfileSubcommand::Run
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
178
theseus_cli/src/subcommands/user.rs
Normal file
178
theseus_cli/src/subcommands/user.rs
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
//! User management subcommand
|
||||||
|
use crate::util::{confirm_async, table};
|
||||||
|
use eyre::Result;
|
||||||
|
use paris::*;
|
||||||
|
use tabled::Tabled;
|
||||||
|
use theseus::prelude::*;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
|
#[derive(argh::FromArgs, Debug)]
|
||||||
|
#[argh(subcommand, name = "user")]
|
||||||
|
/// manage Minecraft accounts
|
||||||
|
pub struct UserCommand {
|
||||||
|
#[argh(subcommand)]
|
||||||
|
action: UserSubcommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(argh::FromArgs, Debug)]
|
||||||
|
#[argh(subcommand)]
|
||||||
|
pub enum UserSubcommand {
|
||||||
|
Add(UserAdd),
|
||||||
|
List(UserList),
|
||||||
|
Remove(UserRemove),
|
||||||
|
SetDefault(UserDefault),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(argh::FromArgs, Debug)]
|
||||||
|
/// add a new user to Theseus
|
||||||
|
#[argh(subcommand, name = "add")]
|
||||||
|
pub struct UserAdd {
|
||||||
|
#[argh(option)]
|
||||||
|
/// the browser to authenticate using
|
||||||
|
browser: Option<webbrowser::Browser>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserAdd {
|
||||||
|
pub async fn run(
|
||||||
|
&self,
|
||||||
|
_args: &crate::Args,
|
||||||
|
_largs: &UserCommand,
|
||||||
|
) -> Result<()> {
|
||||||
|
info!("Adding new user account to Theseus");
|
||||||
|
info!("A browser window will now open, follow the login flow there.");
|
||||||
|
|
||||||
|
let (tx, rx) = oneshot::channel::<url::Url>();
|
||||||
|
let flow = tokio::spawn(auth::authenticate(tx));
|
||||||
|
|
||||||
|
let url = rx.await?;
|
||||||
|
match self.browser {
|
||||||
|
Some(browser) => webbrowser::open_browser(browser, url.as_str()),
|
||||||
|
None => webbrowser::open(url.as_str()),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let credentials = flow.await??;
|
||||||
|
State::sync().await?;
|
||||||
|
success!("Logged in user {}.", credentials.username);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(argh::FromArgs, Debug)]
|
||||||
|
/// list all known users
|
||||||
|
#[argh(subcommand, name = "list")]
|
||||||
|
pub struct UserList {}
|
||||||
|
|
||||||
|
#[derive(Tabled)]
|
||||||
|
struct UserRow<'a> {
|
||||||
|
username: &'a str,
|
||||||
|
id: uuid::Uuid,
|
||||||
|
default: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> UserRow<'a> {
|
||||||
|
pub fn from(
|
||||||
|
credentials: &'a Credentials,
|
||||||
|
default: Option<uuid::Uuid>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
username: &credentials.username,
|
||||||
|
id: credentials.id,
|
||||||
|
default: Some(credentials.id) == default,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserList {
|
||||||
|
pub async fn run(
|
||||||
|
&self,
|
||||||
|
_args: &crate::Args,
|
||||||
|
_largs: &UserCommand,
|
||||||
|
) -> Result<()> {
|
||||||
|
let state = State::get().await?;
|
||||||
|
let default = state.settings.read().await.default_user;
|
||||||
|
|
||||||
|
let users = auth::users().await?;
|
||||||
|
let rows = users.iter().map(|user| UserRow::from(user, default));
|
||||||
|
|
||||||
|
let table = table(rows);
|
||||||
|
println!("{table}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(argh::FromArgs, Debug)]
|
||||||
|
/// remove a user
|
||||||
|
#[argh(subcommand, name = "remove")]
|
||||||
|
pub struct UserRemove {
|
||||||
|
/// the user to remove
|
||||||
|
#[argh(positional)]
|
||||||
|
user: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserRemove {
|
||||||
|
pub async fn run(
|
||||||
|
&self,
|
||||||
|
_args: &crate::Args,
|
||||||
|
_largs: &UserCommand,
|
||||||
|
) -> Result<()> {
|
||||||
|
info!("Removing user {}", self.user.as_hyphenated());
|
||||||
|
|
||||||
|
if confirm_async(String::from("Do you wish to continue"), true).await? {
|
||||||
|
if !auth::has_user(self.user).await? {
|
||||||
|
warn!("Profile was not managed by Theseus!");
|
||||||
|
} else {
|
||||||
|
auth::remove_user(self.user).await?;
|
||||||
|
State::sync().await?;
|
||||||
|
success!("User removed!");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error!("Aborted!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(argh::FromArgs, Debug)]
|
||||||
|
/// set the default user
|
||||||
|
#[argh(subcommand, name = "set-default")]
|
||||||
|
pub struct UserDefault {
|
||||||
|
/// the user to set as default
|
||||||
|
#[argh(positional)]
|
||||||
|
user: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserDefault {
|
||||||
|
pub async fn run(
|
||||||
|
&self,
|
||||||
|
_args: &crate::Args,
|
||||||
|
_largs: &UserCommand,
|
||||||
|
) -> Result<()> {
|
||||||
|
info!("Setting user {} as default", self.user.as_hyphenated());
|
||||||
|
|
||||||
|
// TODO: settings API
|
||||||
|
let state: std::sync::Arc<State> = State::get().await?;
|
||||||
|
let mut settings = state.settings.write().await;
|
||||||
|
|
||||||
|
if settings.default_user == Some(self.user) {
|
||||||
|
warn!("User is already the default!");
|
||||||
|
} else {
|
||||||
|
settings.default_user = Some(self.user);
|
||||||
|
success!("User set as default!");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UserCommand {
|
||||||
|
pub async fn run(&self, args: &crate::Args) -> Result<()> {
|
||||||
|
dispatch!(&self.action, (args, self) => {
|
||||||
|
UserSubcommand::Add,
|
||||||
|
UserSubcommand::List,
|
||||||
|
UserSubcommand::Remove,
|
||||||
|
UserSubcommand::SetDefault
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
use dialoguer::{Confirm, Input, Select};
|
use dialoguer::{Confirm, Input, Select};
|
||||||
use eyre::Result;
|
use eyre::Result;
|
||||||
use std::{borrow::Cow, path::Path};
|
use std::{borrow::Cow, path::Path};
|
||||||
|
use tabled::{Table, Tabled};
|
||||||
|
|
||||||
// TODO: make primarily async to avoid copies
|
// TODO: make primarily async to avoid copies
|
||||||
|
|
||||||
@@ -56,7 +57,11 @@ pub async fn confirm_async(prompt: String, default: bool) -> Result<bool> {
|
|||||||
tokio::task::spawn_blocking(move || confirm(&prompt, default)).await?
|
tokio::task::spawn_blocking(move || confirm(&prompt, default)).await?
|
||||||
}
|
}
|
||||||
|
|
||||||
// Table display helpers
|
// Table helpers
|
||||||
|
pub fn table<T: Tabled>(rows: impl IntoIterator<Item = T>) -> Table {
|
||||||
|
Table::new(rows).with(tabled::Style::psql())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn table_path_display(path: &Path) -> String {
|
pub fn table_path_display(path: &Path) -> String {
|
||||||
let mut res = path.display().to_string();
|
let mut res = path.display().to_string();
|
||||||
|
|
||||||
@@ -67,6 +72,20 @@ pub fn table_path_display(path: &Path) -> String {
|
|||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dispatch macros
|
||||||
|
macro_rules! dispatch {
|
||||||
|
($on:expr, $args:tt => {$($option:path),+}) => {
|
||||||
|
match $on {
|
||||||
|
$($option (ref cmd) => dispatch!(@apply cmd => $args)),+
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(@apply $cmd:expr => ($($args:expr),*)) => {{
|
||||||
|
use tracing_futures::WithSubscriber;
|
||||||
|
$cmd.run($($args),*).with_current_subscriber().await
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
// Internal helpers
|
// Internal helpers
|
||||||
fn print_prompt(prompt: &str) {
|
fn print_prompt(prompt: &str) {
|
||||||
println!(
|
println!(
|
||||||
|
|||||||
Reference in New Issue
Block a user