refactor: 移除遥测并修复启动器流程

This commit is contained in:
2026-09-13 20:31:55 +08:00
parent 1fa56add19
commit 1593ac7a7c
175 changed files with 547 additions and 12210 deletions

View File

@ -69,7 +69,6 @@ pub use self::projects::{
pub use self::run::{
GcLaunchIntent, GcLaunchReport, QuickPlayType, kill, run,
run_with_extra_launch_args, run_with_extra_launch_args_with_gc,
try_update_playtime_by_instance_id,
};
pub use self::upgrade::{
dismiss_instance_post_upgrade_notice, execute_instance_upgrade,

View File

@ -6,7 +6,6 @@ use crate::state::instances::{
PackMemberOverrideKind,
};
use crate::state::{ContentProvider, ContentSourceKind, ProjectType, State};
use crate::util::fetch;
use modrinth_content_management::{
ContentType, ResolutionPreferences, ResolveContentPlan,
};
@ -85,16 +84,12 @@ pub async fn update_project(
pub async fn add_project_from_version(
instance_id: &str,
version_id: &str,
reason: fetch::DownloadReason,
dependent_on_version_id: Option<String>,
) -> crate::Result<String> {
let state = State::get().await?;
let project_path =
crate::state::instances::commands::add_project_from_version(
instance_id,
version_id,
reason,
dependent_on_version_id,
crate::state::ContentSourceKind::Local,
crate::state::instances::ContentOwnershipKind::UserAdded,
&state,
@ -674,8 +669,6 @@ pub async fn restore_pack_member_default(
crate::state::instances::commands::add_project_from_version(
instance_id,
release_id,
fetch::DownloadReason::Update,
None,
ContentSourceKind::ModrinthModpack,
ContentOwnershipKind::PackManaged,
&state,

View File

@ -1,17 +1,10 @@
use super::content::get_projects;
use crate::server_address::ServerAddress;
use crate::state::{
Credentials, InstanceInstallStage, InstanceLink, ProcessMetadata, Settings,
State,
Credentials, InstanceInstallStage, ProcessMetadata, Settings, State,
};
use crate::util::fetch;
use crate::util::io::IOError;
use crate::util::mojang::mojang_service_url;
use serde_json::json;
use std::collections::HashMap;
use std::time::Duration;
use tokio::process::Command;
use tracing::{info, warn};
pub use crate::launcher::jvm_args::{GcLaunchIntent, GcLaunchReport};
@ -253,60 +246,6 @@ async fn run_credentials(
mc_set_options.push(("fullscreen".to_string(), "true".to_string()));
}
if credentials.is_microsoft()
&& let Some(project_id) = server_play_project_id(&context.link)
&& !project_id.trim().is_empty()
{
let server_id = uuid::Uuid::new_v4().to_string();
let join_url = mojang_service_url(
"https://sessionserver.mojang.com/session/minecraft/join",
state.mojang_auth_use_mirror(),
);
let join_result = fetch::INSECURE_REQWEST_CLIENT
.post(join_url.as_ref())
.json(&json!({
"accessToken": &credentials.access_token,
"selectedProfile": credentials.offline_profile.id.simple().to_string(),
"serverId": &server_id,
}))
.timeout(Duration::from_secs(5))
.send()
.await;
match join_result {
Ok(resp) if resp.status().is_success() => {
let result = fetch::post_json(
concat!(
env!("MODRINTH_API_BASE_URL"),
"analytics/minecraft-server-play"
),
json!({
"project_id": project_id,
"username": &credentials.offline_profile.name,
"server_id": &server_id,
}),
&state.api_semaphore,
&state.pool,
)
.await;
match result {
Ok(()) => {
info!(
"Tracked server play for '{project_id}' in analytics"
)
}
Err(err) => warn!("Failed to report server play: {err:?}"),
}
}
Ok(resp) => warn!(
"Failed to join Mojang session server: HTTP {}",
resp.status()
),
Err(err) => warn!("Failed to join Mojang session server: {err:?}"),
}
}
if offline_mode {
crate::minecraft_skins::flush_pending_skin_change_for_profile(
credentials.offline_profile.id,
@ -368,40 +307,6 @@ async fn run_credentials(
Ok((process, gc_report))
}
fn server_play_project_id(link: &InstanceLink) -> Option<&String> {
match link {
InstanceLink::ServerProject { project_id }
| InstanceLink::ServerProjectModpack {
server_project_id: project_id,
..
} => Some(project_id),
InstanceLink::Unmanaged
| InstanceLink::ModrinthModpack { .. }
| InstanceLink::CurseForgeModpack { .. }
| InstanceLink::ImportedModpack { .. }
| InstanceLink::SharedInstance { .. } => None,
}
}
fn modrinth_pack_version_id(link: &InstanceLink) -> Option<&str> {
match link {
InstanceLink::ModrinthModpack { version_id, .. }
| InstanceLink::ServerProjectModpack {
content_version_id: version_id,
..
} => Some(version_id),
InstanceLink::Unmanaged
| InstanceLink::ServerProject { .. }
| InstanceLink::CurseForgeModpack { .. }
| InstanceLink::ImportedModpack { .. }
| InstanceLink::SharedInstance { .. } => None,
}
}
fn playtime_api_url(base_url: &str) -> String {
format!("{}/analytics/playtime", base_url.trim_end_matches('/'))
}
pub async fn kill(instance_id: &str) -> crate::Result<()> {
let state = State::get().await?;
let processes =
@ -413,104 +318,3 @@ pub async fn kill(instance_id: &str) -> crate::Result<()> {
Ok(())
}
#[tracing::instrument]
pub async fn try_update_playtime_by_instance_id(
instance_id: &str,
) -> crate::Result<()> {
let state = State::get().await?;
let context =
crate::state::instances::commands::get_instance_launch_context(
instance_id,
&state.pool,
)
.await?
.ok_or_else(|| {
crate::ErrorKind::OtherError(format!(
"Tried to update playtime for nonexistent instance {instance_id}!"
))
})?;
let updated_recent_playtime = context.instance.recent_time_played;
let res = if updated_recent_playtime > 0 {
let modrinth_pack_version_id = modrinth_pack_version_id(&context.link);
let playtime_update_json = json!({
"seconds": updated_recent_playtime,
"loader": context.applied_content_set.loader.as_str(),
"game_version": &context.applied_content_set.game_version,
"parent": modrinth_pack_version_id,
});
let mut hashmap: HashMap<String, serde_json::Value> = HashMap::new();
for (_, project) in get_projects(instance_id, None).await? {
if let Some(metadata) = project.modrinth {
hashmap.insert(
metadata.version_id.to_string(),
playtime_update_json.clone(),
);
}
}
let playtime_url = playtime_api_url(env!("MODRINTH_API_BASE_URL"));
fetch::post_json(
&playtime_url,
serde_json::to_value(hashmap)?,
&state.api_semaphore,
&state.pool,
)
.await
} else {
Ok(())
};
if res.is_ok() {
crate::state::instances::commands::mark_instance_playtime_submitted(
&context.instance.id,
updated_recent_playtime,
&state.pool,
)
.await?;
}
res
}
#[cfg(test)]
mod tests {
use super::{modrinth_pack_version_id, playtime_api_url};
use crate::state::InstanceLink;
#[test]
fn playtime_parent_requires_an_explicit_modrinth_link() {
let modrinth = InstanceLink::ModrinthModpack {
project_id: "project".to_string(),
version_id: "version".to_string(),
};
let curseforge = InstanceLink::CurseForgeModpack {
project_id: "123".to_string(),
version_id: "456".to_string(),
};
let imported = InstanceLink::ImportedModpack {
project_id: Some("legacy-project".to_string()),
version_id: Some("legacy-version".to_string()),
name: None,
version_number: None,
filename: None,
};
assert_eq!(modrinth_pack_version_id(&modrinth), Some("version"));
assert_eq!(modrinth_pack_version_id(&curseforge), None);
assert_eq!(modrinth_pack_version_id(&imported), None);
}
#[test]
fn playtime_url_has_a_single_path_separator() {
assert_eq!(
playtime_api_url("https://api.modrinth.com"),
"https://api.modrinth.com/analytics/playtime"
);
assert_eq!(
playtime_api_url("https://api.modrinth.com/"),
"https://api.modrinth.com/analytics/playtime"
);
}
}

View File

@ -1096,7 +1096,7 @@ async fn fetch_maven_metadata(
fetch_semaphore: &FetchSemaphore,
pool: &SqlitePool,
) -> crate::Result<MavenMetadata> {
let bytes = fetch(url, None, None, None, fetch_semaphore, pool).await?;
let bytes = fetch(url, None, None, fetch_semaphore, pool).await?;
let xml = std::str::from_utf8(&bytes).map_err(|error| {
crate::ErrorKind::OtherError(format!(
"Maven metadata at {url} was not UTF-8: {error}"
@ -1116,8 +1116,7 @@ async fn fetch_forge_maven_metadata(
fetch_semaphore: &FetchSemaphore,
pool: &SqlitePool,
) -> crate::Result<MavenMetadata> {
let bytes =
fetch_official(url, None, None, None, fetch_semaphore, pool).await?;
let bytes = fetch_official(url, None, None, fetch_semaphore, pool).await?;
let xml = std::str::from_utf8(&bytes).map_err(|error| {
crate::ErrorKind::OtherError(format!(
"Forge Maven metadata at {url} was not UTF-8: {error}"
@ -1651,15 +1650,9 @@ async fn resolve_installer_profile(
state: &State,
installer_url: &str,
) -> crate::Result<PartialVersionInfo> {
let bytes = fetch(
installer_url,
None,
None,
None,
&state.api_semaphore,
&state.pool,
)
.await?;
let bytes =
fetch(installer_url, None, None, &state.api_semaphore, &state.pool)
.await?;
let installer_url = installer_url.to_string();
let parsed = tokio::task::spawn_blocking(move || {
parse_installer(bytes.to_vec(), &installer_url)
@ -1702,7 +1695,6 @@ pub(crate) async fn ensure_installer_artifacts(
&installer_url.client,
None,
None,
None,
&state.api_semaphore,
&state.pool,
)

View File

@ -65,9 +65,9 @@ pub mod data {
ManualDownloadOperationKind, ManualDownloadState, MemorySettings,
ModLoader, ModrinthCredentials, Organization, OwnerType,
PackMemberMaterializationState, PackMemberOverrideKind,
PendingManualDownload, PrivacySettings, ProcessMetadata, Project,
ProjectType, ProjectV3, SearchResult, SearchResults, SearchResultsV3,
Settings, ShaderRuntime, TeamMember, Theme, User, UserFriend, Version,
PendingManualDownload, ProcessMetadata, Project, ProjectType,
ProjectV3, SearchResult, SearchResults, SearchResultsV3, Settings,
ShaderRuntime, TeamMember, Theme, User, UserFriend, Version,
WindowSize,
};
pub use ariadne::users::UserStatus;

View File

@ -117,7 +117,6 @@ pub async fn import_curseforge(
&thumbnail_url,
None,
None,
None,
&state.fetch_semaphore,
&state.pool,
)

View File

@ -12,9 +12,8 @@ use crate::state::{
ModrinthVersionId, SideType,
};
use crate::util::fetch::{
ContentValidation, DownloadMeta, DownloadReason, DownloadRequest,
FetchProgressFn, Integrity, ResourceClass, download_to_path, fetch,
sha1_file_async, write_cached_icon,
ContentValidation, DownloadRequest, FetchProgressFn, Integrity,
ResourceClass, download_to_path, fetch, sha1_file_async, write_cached_icon,
};
use path_util::SafeRelativeUtf8UnixPathBuf;
use serde::{Deserialize, Serialize};
@ -267,7 +266,6 @@ pub(crate) async fn generate_pack_from_version_id_with_reporter(
title: String,
icon_url: Option<String>,
instance_id: String,
reason: DownloadReason,
reporter: InstallProgressReporter,
) -> crate::Result<CreatePack> {
let state = State::get().await?;
@ -340,22 +338,6 @@ pub(crate) async fn generate_pack_from_version_id_with_reporter(
.join(&version_id)
.join(file_name);
let metadata =
crate::api::instance::get(&instance_id)
.await?
.ok_or_else(|| {
crate::ErrorKind::InputError(format!(
"Unknown instance {instance_id}"
))
})?;
let download_meta = DownloadMeta {
reason,
game_version: metadata.applied_content_set.game_version.clone(),
loader: metadata.applied_content_set.loader.as_str().to_string(),
dependent_on: Some(version_id.clone()),
};
let details = InstallPhaseDetails::Modpack {
project_id: Some(project_id.clone()),
version_id: Some(version_id.clone()),
@ -430,7 +412,6 @@ pub(crate) async fn generate_pack_from_version_id_with_reporter(
..Integrity::default()
})
.with_h2_range_concurrency(16)
.with_download_meta(download_meta)
.with_install_tracking(
reporter.clone(),
pack_path.display().to_string(),
@ -487,7 +468,6 @@ pub(crate) async fn generate_pack_from_version_id_with_reporter(
&icon_url,
None,
None,
None,
&state.fetch_semaphore,
&state.pool,
)

View File

@ -23,8 +23,8 @@ use crate::state::{
Settings, SideType,
};
use crate::util::fetch::{
ContentValidation, DownloadMeta, DownloadReason, DownloadRequest,
Integrity, ResourceClass, download_to_path, sha1_file_async,
ContentValidation, DownloadRequest, Integrity, ResourceClass,
download_to_path, sha1_file_async,
};
use crate::util::io;
use async_zip::base::read::seek::ZipFileReader as SeekZipFileReader;
@ -219,7 +219,6 @@ fn missing_required_content_pause(
struct ModpackContentInstallContext {
instance_id: String,
instance_full_path: PathBuf,
download_meta: DownloadMeta,
pack_version_id: Option<String>,
pack_project_id: Option<String>,
reporter: InstallProgressReporter,
@ -689,7 +688,6 @@ where
pub(crate) async fn install_zipped_mrpack_files_with_reporter(
create_pack: CreatePack,
ignore_lock: bool,
reason: DownloadReason,
reporter: InstallProgressReporter,
) -> crate::Result<MrpackInstallOutcome> {
let state = &State::get().await?;
@ -806,21 +804,6 @@ pub(crate) async fn install_zipped_mrpack_files_with_reporter(
)
.await?;
let metadata =
crate::api::instance::get(&instance_id)
.await?
.ok_or_else(|| {
crate::ErrorKind::InputError(format!(
"Unknown instance {instance_id}"
))
})?;
let download_meta = DownloadMeta {
reason,
game_version: metadata.applied_content_set.game_version.clone(),
loader: metadata.applied_content_set.loader.as_str().to_string(),
dependent_on: version_id.clone(),
};
let num_files = pack.files.len();
let content_total_bytes = pack
.files
@ -892,7 +875,6 @@ pub(crate) async fn install_zipped_mrpack_files_with_reporter(
let content_context = ModpackContentInstallContext {
instance_id: instance_id.clone(),
instance_full_path: instance_full_path.clone(),
download_meta,
pack_version_id: version_id.clone(),
pack_project_id: project_id.clone(),
reporter: reporter.clone(),
@ -1191,9 +1173,6 @@ pub(crate) async fn install_zipped_mrpack_files_with_reporter(
project.downloads.iter().skip(1).cloned(),
)
.with_integrity(integrity)
.with_download_meta(
content_context.download_meta.clone(),
)
.with_segmented_download(true)
.with_http1_segmented_download(false)
.with_install_tracking(

View File

@ -3,10 +3,7 @@
pub use crate::util::download::DownloadEngine;
pub use crate::{
State,
state::{
DownloadSourceMode, Hooks, MemorySettings, PrivacySettings, Settings,
WindowSize,
},
state::{DownloadSourceMode, Hooks, MemorySettings, Settings, WindowSize},
};
/// Gets entire settings
@ -21,10 +18,6 @@ pub async fn get() -> crate::Result<Settings> {
#[tracing::instrument]
pub async fn set(mut settings: Settings) -> crate::Result<()> {
let state = State::get().await?;
let current = Settings::get(&state.pool).await?;
settings.telemetry = current.telemetry;
settings.telemetry_consent_version = current.telemetry_consent_version;
settings.discord_rpc = current.discord_rpc;
super::terracotta::validate_public_nodes(
&settings.terracotta_public_nodes,
)?;
@ -46,78 +39,6 @@ pub async fn set_download_engine(engine: DownloadEngine) -> crate::Result<()> {
Ok(())
}
#[tracing::instrument]
pub async fn get_privacy() -> crate::Result<PrivacySettings> {
let state = State::get().await?;
let settings = Settings::get(&state.pool).await?;
Ok(PrivacySettings {
telemetry: settings.telemetry,
discord_rpc: settings.discord_rpc,
consent_version: settings.telemetry_consent_version,
})
}
#[tracing::instrument]
pub async fn set_privacy(
privacy: PrivacySettings,
) -> crate::Result<PrivacySettings> {
let state = State::get().await?;
let mut transaction = state.pool.begin().await?;
sqlx::query(
"UPDATE settings SET telemetry = ?, discord_rpc = ?, telemetry_consent_version = ? WHERE id = 0",
)
.bind(privacy.telemetry)
.bind(privacy.discord_rpc)
.bind(privacy.consent_version)
.execute(&mut *transaction)
.await?;
sqlx::query("DELETE FROM telemetry_outbox")
.execute(&mut *transaction)
.await?;
transaction.commit().await?;
if let Err(error) =
crate::telemetry::set_enabled(&state, privacy.telemetry).await
{
tracing::debug!(target: "theseus::telemetry", %error, "Failed to apply telemetry state");
}
if let Err(error) = state.discord_rpc.clear_to_default(true).await {
tracing::debug!(target: "theseus::telemetry", %error, "Failed to apply Discord RPC state");
}
get_privacy().await
}
#[tracing::instrument]
pub async fn set_telemetry(enabled: bool) -> crate::Result<PrivacySettings> {
let state = State::get().await?;
let mut transaction = state.pool.begin().await?;
sqlx::query("UPDATE settings SET telemetry = ? WHERE id = 0")
.bind(enabled)
.execute(&mut *transaction)
.await?;
sqlx::query("DELETE FROM telemetry_outbox")
.execute(&mut *transaction)
.await?;
transaction.commit().await?;
if let Err(error) = crate::telemetry::set_enabled(&state, enabled).await {
tracing::debug!(target: "theseus::telemetry", %error, "Failed to apply telemetry state");
}
get_privacy().await
}
#[tracing::instrument]
pub async fn set_discord_rpc(enabled: bool) -> crate::Result<PrivacySettings> {
let state = State::get().await?;
sqlx::query("UPDATE settings SET discord_rpc = ? WHERE id = 0")
.bind(enabled)
.execute(&state.pool)
.await?;
if let Err(error) = state.discord_rpc.clear_to_default(true).await {
tracing::debug!(target: "theseus::telemetry", %error, "Failed to apply Discord RPC state");
}
get_privacy().await
}
#[tracing::instrument]
pub async fn cancel_directory_change(
app_identifier: &str,

View File

@ -222,9 +222,6 @@ pub enum ErrorKind {
#[error("Deserialization error: {0}")]
DeserializationError(#[from] serde::de::value::Error),
#[error("Discord IPC error: {0}")]
DiscordRichPresenceError(#[from] discord_rich_presence::error::Error),
}
#[derive(Debug)]

View File

@ -28,7 +28,6 @@ use crate::state::{
LoaderComponent, LoaderComponentKind, LoaderComponentRole, ModLoader,
State,
};
use crate::util::fetch::DownloadReason;
use futures::stream::{FuturesUnordered, StreamExt};
use std::collections::{HashMap, HashSet};
use std::future::Future;
@ -1513,14 +1512,8 @@ async fn run_request(
)
.await?;
if let InstallExecutionOutcome::WaitingForUser(reason) =
install_pack(
job_id,
job_state,
location,
instance_id.clone(),
DownloadReason::Modpack,
)
.await?
install_pack(job_id, job_state, location, instance_id.clone())
.await?
{
return Ok(InstallExecutionOutcome::WaitingForUser(reason));
}
@ -1714,14 +1707,8 @@ async fn run_request(
}
};
if let InstallExecutionOutcome::WaitingForUser(reason) =
install_pack(
job_id,
job_state,
location,
instance_id.clone(),
DownloadReason::Modpack,
)
.await?
install_pack(job_id, job_state, location, instance_id.clone())
.await?
{
return Ok(InstallExecutionOutcome::WaitingForUser(reason));
}
@ -2973,13 +2960,8 @@ async fn stage_upgrade_content(
let reporter = reporter.clone();
async move {
let index = context.index;
let mutation = stage_one_upgrade_request(
instance_id,
context,
reporter,
state,
)
.await?;
let mutation =
stage_one_upgrade_request(context, reporter, state).await?;
Ok::<_, crate::Error>((index, mutation))
}
})
@ -3002,7 +2984,6 @@ where
}
async fn stage_one_upgrade_request(
instance_id: &str,
context: UpgradeStagingRequest,
reporter: Option<InstallProgressReporter>,
state: &State,
@ -3011,27 +2992,13 @@ async fn stage_one_upgrade_request(
ContentProvider::Modrinth => StagedUpgradeDownload::Modrinth(
match reporter.as_ref() {
Some(reporter) => crate::state::instances::commands::download_project_version_with_reporter(
instance_id,
&context.release_id,
if context.auto_dependency {
DownloadReason::Dependency
} else {
DownloadReason::Update
},
None,
reporter.clone(),
state,
)
.await?,
None => crate::state::instances::commands::download_project_version(
instance_id,
&context.release_id,
if context.auto_dependency {
DownloadReason::Dependency
} else {
DownloadReason::Update
},
None,
state,
)
.await?,
@ -3739,7 +3706,6 @@ async fn remove_existing_pack_content(
metadata.instance.name.clone(),
None,
instance_id.to_string(),
DownloadReason::Update,
reporter,
)
.await?;
@ -3799,7 +3765,6 @@ async fn install_pack(
job_state: &mut InstallJobState,
location: CreatePackLocation,
instance_id: String,
reason: DownloadReason,
) -> crate::Result<InstallExecutionOutcome<()>> {
let reporter = InstallProgressReporter::new(job_id, job_state.clone());
reporter
@ -3831,7 +3796,6 @@ async fn install_pack(
title,
icon_url,
instance_id.clone(),
reason,
reporter.clone(),
)
.await?
@ -3882,13 +3846,9 @@ async fn install_pack(
}
};
let outcome = install_zipped_mrpack_files_with_reporter(
create_pack,
false,
reason,
reporter,
)
.await?;
let outcome =
install_zipped_mrpack_files_with_reporter(create_pack, false, reporter)
.await?;
Ok(match outcome {
MrpackInstallOutcome::Completed(_) => {
InstallExecutionOutcome::Completed(())
@ -4056,7 +4016,6 @@ async fn install_local_pack_file(
match install_zipped_mrpack_files_with_reporter(
create_pack,
false,
DownloadReason::Modpack,
reporter,
)
.await?

View File

@ -2389,11 +2389,6 @@ pub async fn launch_minecraft(
)
.await?;
let _ = state
.discord_rpc
.set_activity(&format!("Playing {}", instance.name), true)
.await;
// The launcher log must land where the game and log browser read it. The
// resolved directory is PCL's PathIndie equivalent for direct links.
let logs_folder = state.directories.game_logs_dir(&instance_path);

View File

@ -21,7 +21,6 @@ mod logger;
pub mod mod_metadata;
mod state;
pub mod storage;
pub mod telemetry;
pub use api::*;
pub use error::*;
@ -37,7 +36,7 @@ pub use state::db::{
};
pub use state::{DirectoryInfo, State};
pub use storage::*;
pub use util::fetch::{DownloadReason, build_proxied_client};
pub use util::fetch::build_proxied_client;
pub use util::file_lock::{LockingProcess, get_locking_processes};
pub use util::platform::is_process_elevated;
pub use util::proxy::{ProxyConfig, ProxyMode, ProxyTestResult};

View File

@ -1,238 +0,0 @@
use std::sync::{
Arc, Mutex, TryLockError, atomic::AtomicBool, atomic::Ordering,
};
use std::time::Duration;
use discord_rich_presence::{
DiscordIpc, DiscordIpcClient,
activity::{Activity, Assets},
};
use tokio::sync::RwLock;
use tokio::task::JoinHandle;
use crate::State;
pub struct DiscordGuard {
client: Arc<Mutex<DiscordIpcClient>>,
connected: Arc<AtomicBool>,
launcher_activity: Arc<RwLock<String>>,
}
const DISCORD_IPC_TIMEOUT: Duration = Duration::from_secs(2);
async fn await_ipc_task<T>(
operation: &'static str,
timeout: Duration,
task: JoinHandle<T>,
) -> Option<T> {
match tokio::time::timeout(timeout, task).await {
Ok(Ok(result)) => Some(result),
Ok(Err(error)) => {
tracing::warn!(%error, operation, "Discord IPC worker failed");
None
}
Err(_) => {
tracing::warn!(operation, "Discord IPC operation timed out");
None
}
}
}
impl DiscordGuard {
/// Initialize discord IPC client, and attempt to connect to it
/// If it fails, it will still return a DiscordGuard, but the client will be unconnected
pub fn init() -> crate::Result<DiscordGuard> {
let dipc = DiscordIpcClient::new("1533353147349864458");
Ok(DiscordGuard {
client: Arc::new(Mutex::new(dipc)),
connected: Arc::new(AtomicBool::new(false)),
launcher_activity: Arc::new(RwLock::new("Idling...".to_string())),
})
}
async fn run_ipc<F>(
&self,
operation: &'static str,
connect_if_needed: bool,
action: F,
) where
F: FnOnce(&mut DiscordIpcClient) -> crate::Result<()> + Send + 'static,
{
let client = self.client.clone();
let connected = self.connected.clone();
let task = tokio::task::spawn_blocking(move || {
let mut client = match client.try_lock() {
Ok(client) => client,
Err(TryLockError::WouldBlock) => {
tracing::warn!(
operation,
"Discord IPC client is busy; skipping activity update"
);
return;
}
Err(TryLockError::Poisoned(error)) => {
tracing::warn!(
operation,
"Discord IPC client lock was poisoned; recovering"
);
error.into_inner()
}
};
if !connected.load(Ordering::Relaxed) {
if !connect_if_needed {
return;
}
if client.connect().is_err() {
return;
}
connected.store(true, Ordering::Relaxed);
}
if let Err(error) = action(&mut client) {
connected.store(false, Ordering::Relaxed);
tracing::warn!(%error, operation, "Discord IPC operation failed");
}
});
let _ = await_ipc_task(operation, DISCORD_IPC_TIMEOUT, task).await;
}
/// Set the activity to the given message
/// First checks if discord is disabled, and if so, clear the activity instead
pub async fn set_activity(
&self,
msg: &str,
reconnect_if_fail: bool,
) -> crate::Result<()> {
// Check if discord is disabled, and if so, clear the activity instead
let state = State::get().await?;
let settings = crate::state::Settings::get(&state.pool).await?;
if !settings.discord_rpc {
Ok(self.clear_activity(true).await?)
} else {
Ok(self.force_set_activity(msg, reconnect_if_fail).await?)
}
}
pub async fn set_launcher_activity(
&self,
msg: &str,
reconnect_if_fail: bool,
) -> crate::Result<()> {
*self.launcher_activity.write().await = msg.to_string();
let state = State::get().await?;
if state.process_manager.get_all().is_empty() {
self.set_activity(msg, reconnect_if_fail).await?;
}
Ok(())
}
/// Sets the activity to the given message, regardless of if discord is disabled or offline
/// Should not be used except for in the above method, or if it is already known that discord is enabled (specifically for state initialization) and we are connected to the internet
pub async fn force_set_activity(
&self,
msg: &str,
reconnect_if_fail: bool,
) -> crate::Result<()> {
let msg = msg.to_string();
self.run_ipc("set activity", true, move |client| {
let activity = Activity::new().state(&msg).assets(
Assets::new()
.large_image("modrinth_simple")
.large_text("Modrinth Logo"),
);
let result = client.set_activity(activity.clone());
if reconnect_if_fail && result.is_err() {
client.reconnect()?;
client.set_activity(activity)?;
} else {
result?;
}
Ok(())
})
.await;
Ok(())
}
/// Clear the activity entirely ('disabling' the RPC until the next set_activity)
pub async fn clear_activity(
&self,
reconnect_if_fail: bool,
) -> crate::Result<()> {
self.run_ipc("clear activity", false, move |client| {
let result = client.clear_activity();
if reconnect_if_fail && result.is_err() {
client.reconnect()?;
client.clear_activity()?;
} else {
result?;
}
Ok(())
})
.await;
Ok(())
}
/// Clear the activity, but if there is a running profile, set the activity to that instead
pub async fn clear_to_default(
&self,
reconnect_if_fail: bool,
) -> crate::Result<()> {
let state = State::get().await?;
let settings = crate::state::Settings::get(&state.pool).await?;
if !settings.discord_rpc {
println!("Discord is disabled, clearing activity");
return self.clear_activity(true).await;
}
let running_instances = state.process_manager.get_all();
if let Some(existing_child) = running_instances.first() {
self.set_activity(
&format!("Playing {}", existing_child.instance_name),
reconnect_if_fail,
)
.await?;
} else {
let launcher_activity = self.launcher_activity.read().await.clone();
self.set_activity(&launcher_activity, reconnect_if_fail)
.await?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::await_ipc_task;
use std::time::Duration;
#[tokio::test]
async fn ipc_task_returns_completed_result() {
let task = tokio::task::spawn_blocking(|| 42);
assert_eq!(
await_ipc_task("test", Duration::from_secs(1), task).await,
Some(42)
);
}
#[tokio::test]
async fn ipc_task_stops_waiting_after_timeout() {
let task = tokio::task::spawn_blocking(|| {
std::thread::sleep(Duration::from_millis(100));
42
});
assert_eq!(
await_ipc_task("test", Duration::from_millis(10), task).await,
None
);
}
}

View File

@ -357,7 +357,6 @@ impl FriendsSocket {
None,
None,
None,
None,
Some("/v3/friend/:user_id"),
semaphore,
exec,
@ -391,7 +390,6 @@ impl FriendsSocket {
None,
None,
None,
None,
Some("/v3/friend/:user_id"),
semaphore,
exec,

View File

@ -18,8 +18,8 @@ use crate::state::{
Version, cache_file_hash, cache_file_hash_metadata,
};
use crate::util::fetch::{
self, ContentValidation, DownloadMeta, DownloadReason, DownloadRequest,
Integrity, ResourceClass, download_to_path,
self, ContentValidation, DownloadRequest, Integrity, ResourceClass,
download_to_path,
};
use crate::util::io;
use crate::util::io::io_error_with_lock_info;
@ -307,7 +307,6 @@ pub(crate) async fn install_resolved_content_plan_with_reporter(
add_resolved_content_with_progress(
instance_id,
&plan.primary,
DownloadReason::Standalone,
false,
primary_progress,
state,
@ -332,7 +331,6 @@ pub(crate) async fn install_resolved_content_plan_with_reporter(
add_resolved_content_with_progress(
instance_id,
dependency,
DownloadReason::Dependency,
true,
dependency_progress,
state,
@ -424,8 +422,6 @@ pub(crate) async fn switch_project_version_with_dependencies(
let mut new_path = add_project_from_version(
instance_id,
&plan.primary.version_id,
DownloadReason::Update,
None,
ContentSourceKind::Local,
ownership_kind,
state,
@ -442,14 +438,7 @@ pub(crate) async fn switch_project_version_with_dependencies(
installed_paths.push(new_path.clone());
for dependency in &plan.dependencies {
installed_paths.push(
add_resolved_content(
instance_id,
dependency,
DownloadReason::Dependency,
true,
state,
)
.await?,
add_resolved_content(instance_id, dependency, true, state).await?,
);
}
persist_resolved_plan_dependency_edges(
@ -474,14 +463,12 @@ pub(crate) async fn switch_project_version_with_dependencies(
pub(crate) async fn add_resolved_content(
instance_id: &str,
content: &ResolvedContent,
reason: DownloadReason,
auto_dependency: bool,
state: &State,
) -> crate::Result<String> {
add_resolved_content_with_progress(
instance_id,
content,
reason,
auto_dependency,
None,
state,
@ -492,7 +479,6 @@ pub(crate) async fn add_resolved_content(
async fn add_resolved_content_with_progress(
instance_id: &str,
content: &ResolvedContent,
reason: DownloadReason,
auto_dependency: bool,
progress: Option<ResolvedContentDownloadProgress>,
state: &State,
@ -500,8 +486,6 @@ async fn add_resolved_content_with_progress(
let path = add_project_from_version_with_progress(
instance_id,
&content.version_id,
reason,
content.dependent_on_version_id.clone(),
ContentSourceKind::Local,
ContentOwnershipKind::UserAdded,
progress,
@ -536,15 +520,8 @@ pub(crate) async fn install_resolved_dependency(
content: &ResolvedContent,
state: &State,
) -> crate::Result<String> {
add_resolved_content_with_progress(
instance_id,
content,
DownloadReason::Dependency,
true,
None,
state,
)
.await
add_resolved_content_with_progress(instance_id, content, true, None, state)
.await
}
pub(crate) async fn persist_resolved_plan_dependency_edges(
@ -728,8 +705,6 @@ pub(crate) async fn resolve_content_scope(
pub(crate) async fn add_project_from_version(
instance_id: &str,
version_id: &str,
reason: DownloadReason,
dependent_on_version_id: Option<String>,
source_kind: ContentSourceKind,
ownership_kind: ContentOwnershipKind,
state: &State,
@ -737,8 +712,6 @@ pub(crate) async fn add_project_from_version(
add_project_from_version_with_progress(
instance_id,
version_id,
reason,
dependent_on_version_id,
source_kind,
ownership_kind,
None,
@ -751,22 +724,14 @@ pub(crate) async fn add_project_from_version(
pub(crate) async fn add_project_from_version_with_progress(
instance_id: &str,
version_id: &str,
reason: DownloadReason,
dependent_on_version_id: Option<String>,
source_kind: ContentSourceKind,
ownership_kind: ContentOwnershipKind,
progress: Option<ResolvedContentDownloadProgress>,
state: &State,
) -> crate::Result<String> {
let downloaded = download_project_version_with_progress(
instance_id,
version_id,
reason,
dependent_on_version_id,
progress,
state,
)
.await?;
let downloaded =
download_project_version_with_progress(version_id, progress, state)
.await?;
add_downloaded_project_version(
instance_id,
@ -779,21 +744,10 @@ pub(crate) async fn add_project_from_version_with_progress(
}
pub(crate) async fn download_project_version(
instance_id: &str,
version_id: &str,
reason: DownloadReason,
dependent_on_version_id: Option<String>,
state: &State,
) -> crate::Result<DownloadedProjectVersion> {
download_project_version_with_progress(
instance_id,
version_id,
reason,
dependent_on_version_id,
None,
state,
)
.await
download_project_version_with_progress(version_id, None, state).await
}
/// Progress context for one file inside a multi-file content install.
@ -807,38 +761,21 @@ pub(crate) struct ResolvedContentDownloadProgress {
}
pub(crate) async fn download_project_version_with_progress(
instance_id: &str,
version_id: &str,
reason: DownloadReason,
dependent_on_version_id: Option<String>,
progress: Option<ResolvedContentDownloadProgress>,
state: &State,
) -> crate::Result<DownloadedProjectVersion> {
download_project_version_with_reporting(
instance_id,
version_id,
reason,
dependent_on_version_id,
progress,
None,
state,
)
.await
download_project_version_with_reporting(version_id, progress, None, state)
.await
}
pub(crate) async fn download_project_version_with_reporter(
instance_id: &str,
version_id: &str,
reason: DownloadReason,
dependent_on_version_id: Option<String>,
reporter: crate::install::InstallProgressReporter,
state: &State,
) -> crate::Result<DownloadedProjectVersion> {
download_project_version_with_reporting(
instance_id,
version_id,
reason,
dependent_on_version_id,
None,
Some(reporter),
state,
@ -847,26 +784,15 @@ pub(crate) async fn download_project_version_with_reporter(
}
async fn download_project_version_with_reporting(
instance_id: &str,
version_id: &str,
reason: DownloadReason,
dependent_on_version_id: Option<String>,
progress: Option<ResolvedContentDownloadProgress>,
reporter: Option<crate::install::InstallProgressReporter>,
state: &State,
) -> crate::Result<DownloadedProjectVersion> {
let prepared = prepare_version_download(
instance_id,
version_id,
reason,
dependent_on_version_id,
state,
)
.await?;
let prepared = prepare_version_download(version_id, state).await?;
let mut request =
DownloadRequest::new(&prepared.url, ResourceClass::Modrinth)
.with_integrity(prepared.integrity)
.with_download_meta(prepared.download_meta);
.with_integrity(prepared.integrity);
let tracking_reporter = progress
.as_ref()
.map(|progress| progress.reporter.clone())
@ -961,7 +887,6 @@ async fn download_project_version_with_reporting(
struct PreparedVersionDownload {
url: String,
path: PathBuf,
download_meta: DownloadMeta,
integrity: Integrity,
file_name: String,
sha1: Option<String>,
@ -973,22 +898,9 @@ struct PreparedVersionDownload {
/// Resolves the content scope and version metadata for a download and
/// validates the target path, without touching the network.
async fn prepare_version_download(
instance_id: &str,
version_id: &str,
reason: DownloadReason,
dependent_on_version_id: Option<String>,
state: &State,
) -> crate::Result<PreparedVersionDownload> {
let scope = resolve_content_scope(instance_id, None, state).await?;
let content_set =
content_rows::get_content_set(&scope.content_set_id, &state.pool)
.await?
.ok_or_else(|| {
crate::ErrorKind::InputError(format!(
"Unknown content set {}",
scope.content_set_id
))
})?;
let version = CachedEntry::get_version(
&ModrinthVersionId::new(version_id.to_string())?,
None,
@ -1011,12 +923,6 @@ async fn prepare_version_download(
"No files for input version present!".to_string(),
)
})?;
let download_meta = DownloadMeta {
reason,
game_version: content_set.game_version,
loader: content_set.loader.as_str().to_string(),
dependent_on: dependent_on_version_id,
};
let file_name_path = Path::new(&file.filename);
if file_name_path.as_os_str().is_empty()
|| file_name_path.is_absolute()
@ -1057,7 +963,6 @@ async fn prepare_version_download(
Ok(PreparedVersionDownload {
url: file.url.clone(),
path,
download_meta,
integrity,
file_name: file.filename.clone(),
sha1: file.hashes.get("sha1").cloned(),

View File

@ -6,7 +6,6 @@ use crate::state::{
CacheBehaviour, CachedEntry, ContentProviderRef, Dependency,
DependencyType, ModrinthVersionId, ProjectType, State, Version,
};
use crate::util::fetch::DownloadReason;
use futures::stream::{FuturesUnordered, StreamExt};
use std::cmp::Reverse;
use std::collections::{HashMap, HashSet};
@ -31,7 +30,6 @@ struct BulkUpdatePlan {
struct PlannedProjectUpdate {
relative_path: String,
project_id: String,
current_version_id: String,
update_version_id: String,
}
@ -107,7 +105,6 @@ async fn apply_content_update(
let mut new_path = match update {
ContentUpdate::Modrinth {
project_id,
current_version_id,
update_version_id,
..
} => {
@ -149,8 +146,6 @@ async fn apply_content_update(
add_project_from_version(
instance_id,
&plan.primary.version_id,
DownloadReason::Update,
Some(current_version_id.to_string()),
ContentSourceKind::Local,
ownership_kind,
state,
@ -159,14 +154,8 @@ async fn apply_content_update(
);
for dependency in &plan.dependencies {
paths.push(
add_resolved_content(
instance_id,
dependency,
DownloadReason::Dependency,
true,
state,
)
.await?,
add_resolved_content(instance_id, dependency, true, state)
.await?,
);
}
persist_resolved_plan_dependency_edges(
@ -449,10 +438,7 @@ async fn download_planned_projects(
match download {
PlannedDownload::ProjectUpdate(update) => {
let downloaded = download_project_version(
instance_id,
&update.update_version_id,
DownloadReason::Update,
Some(update.current_version_id.clone()),
state,
)
.await?;
@ -462,14 +448,9 @@ async fn download_planned_projects(
))
}
PlannedDownload::DependencyAddition(dependency) => {
let downloaded = download_project_version(
instance_id,
&dependency.version_id,
DownloadReason::Dependency,
Some(dependency.parent_version_id.clone()),
state,
)
.await?;
let downloaded =
download_project_version(&dependency.version_id, state)
.await?;
Ok::<_, crate::Error>(
DownloadedBulkProject::DependencyAddition(
@ -636,11 +617,10 @@ async fn plan_bulk_update(
let project_updates = updates
.into_iter()
.filter_map(|update| {
let (project_id, current, target) = update.modrinth_ids()?;
let (project_id, _, target) = update.modrinth_ids()?;
Some(PlannedProjectUpdate {
relative_path: update.relative_path().to_string(),
project_id: project_id.to_string(),
current_version_id: current.to_string(),
update_version_id: target.to_string(),
})
})

View File

@ -294,15 +294,8 @@ async fn fetch_icon_bytes(
return Ok(response.bytes().await?);
}
fetch::fetch(
icon_url,
None,
None,
None,
&state.fetch_semaphore,
&state.pool,
)
.await
fetch::fetch(icon_url, None, None, &state.fetch_semaphore, &state.pool)
.await
}
fn is_direct_cdn_icon_url(url: &str) -> bool {

View File

@ -507,47 +507,6 @@ pub(crate) async fn add_instance_recent_playtime(
Ok(())
}
pub(crate) async fn mark_instance_playtime_submitted(
instance_id: &str,
recent_time_played: u64,
pool: &SqlitePool,
) -> crate::Result<()> {
if recent_time_played == 0 {
return Ok(());
}
let recent_time_played =
playtime_to_storage(recent_time_played, "recent_time_played")?;
let max_playtime = i64::MAX;
let max_playtime_before_increment = max_playtime - recent_time_played;
let modified = Utc::now().timestamp();
sqlx::query!(
"
UPDATE instances
SET
submitted_time_played = CASE
WHEN submitted_time_played < 0 THEN ?
WHEN submitted_time_played > ? THEN ?
ELSE submitted_time_played + ?
END,
recent_time_played = 0,
modified = ?
WHERE id = ?
",
recent_time_played,
max_playtime_before_increment,
max_playtime,
recent_time_played,
modified,
instance_id,
)
.execute(pool)
.await?;
Ok(())
}
#[cfg(test)]
mod tests {
use chrono::{TimeZone, Utc};

View File

@ -20,8 +20,8 @@ use crate::state::{
ReleaseChannel, TeamMember, Version, VersionV3,
};
use crate::util::fetch::{
ContentValidation, DownloadMeta, DownloadReason, DownloadRequest,
FetchSemaphore, Integrity, ResourceClass, download_to_path, sha1_async,
ContentValidation, DownloadRequest, FetchSemaphore, Integrity,
ResourceClass, download_to_path, sha1_async,
};
use async_zip::tokio::read::fs::ZipFileReader;
use dashmap::DashMap;
@ -507,7 +507,6 @@ pub(crate) async fn list_linked_modpack_content(
};
let ids = match get_modpack_identifiers(
version_id,
&resolved.content_set,
&state.pool,
&state.api_semaphore,
)
@ -2520,7 +2519,6 @@ async fn get_cached_modpack_identifiers(
async fn get_modpack_identifiers(
version_id: &str,
content_set: &ContentSet,
pool: &SqlitePool,
fetch_semaphore: &FetchSemaphore,
) -> crate::Result<ModpackIdentifiers> {
@ -2585,12 +2583,6 @@ async fn get_modpack_identifiers(
"No files found for modpack version {version_id}"
))
})?;
let download_meta = DownloadMeta {
reason: DownloadReason::Modpack,
game_version: content_set.game_version.clone(),
loader: content_set.loader.as_str().to_string(),
dependent_on: Some(version_id.to_string()),
};
let state = State::get().await?;
let file_name = Path::new(&primary_file.filename);
if file_name.components().count() != 1
@ -2619,8 +2611,7 @@ async fn get_modpack_identifiers(
sha512: primary_file.hashes.get("sha512").cloned(),
content: ContentValidation::Jar,
..Integrity::default()
})
.with_download_meta(download_meta),
}),
&pack_path,
&state.download_semaphore,
pool,

View File

@ -61,8 +61,6 @@ where
settings.collapsed_navigation = legacy_settings.collapsed_navigation;
settings.advanced_rendering = legacy_settings.advanced_rendering;
settings.native_decorations = legacy_settings.native_decorations;
settings.telemetry = !legacy_settings.opt_out_analytics;
settings.discord_rpc = !legacy_settings.disable_discord_rpc;
settings.developer_mode = legacy_settings.developer_mode;
settings.onboarded = legacy_settings.fully_onboarded;
settings.extra_launch_args = legacy_settings.custom_java_args;
@ -660,8 +658,6 @@ struct LegacySettings {
pub max_concurrent_writes: usize,
pub collapsed_navigation: bool,
#[serde(default)]
pub disable_discord_rpc: bool,
#[serde(default)]
pub hide_on_process: bool,
#[serde(default)]
pub native_decorations: bool,
@ -670,8 +666,6 @@ struct LegacySettings {
#[serde(default)]
pub developer_mode: bool,
#[serde(default)]
pub opt_out_analytics: bool,
#[serde(default)]
pub advanced_rendering: bool,
#[serde(default)]
pub fully_onboarded: bool,

View File

@ -42,9 +42,6 @@ pub use self::java_globals::*;
mod discovered_javas;
pub use self::discovered_javas::*;
mod discord;
pub use self::discord::*;
mod minecraft_auth;
pub use self::minecraft_auth::*;
@ -124,9 +121,6 @@ pub struct State {
pub(crate) install_job_operation_locks:
DashMap<Uuid, Arc<AsyncMutex<InstallJobOperationState>>>,
/// Discord RPC
pub discord_rpc: DiscordGuard,
/// Process manager
pub process_manager: ProcessManager,
@ -501,8 +495,6 @@ impl State {
concurrency_state.run_auto_concurrency_controller().await;
});
crate::telemetry::start(Arc::clone(state));
tokio::task::spawn(async move {
crate::google_ip::preload().await;
});
@ -521,14 +513,13 @@ impl State {
.await;
let res = tokio::try_join!(
state.discord_rpc.clear_to_default(true),
instances::refresh_all_instances(),
Settings::migrate(&state.pool),
ModrinthCredentials::refresh_all(),
);
if let Err(e) = res {
tracing::error!("Error running discord RPC: {e}");
tracing::error!("Error refreshing startup state: {e}");
}
// Axolotl does not connect to Modrinth's private friends socket.
@ -861,8 +852,6 @@ impl State {
let directories =
DirectoryInfo::init(settings.custom_dir, &app_identifier).await?;
let discord_rpc = DiscordGuard::init()?;
tracing::info!("Initializing file watcher");
let file_watcher = instances::watcher::init_watcher().await?;
@ -915,7 +904,6 @@ impl State {
install_db_semaphore: Semaphore::new(1),
install_job_cancellations: DashMap::new(),
install_job_operation_locks: DashMap::new(),
discord_rpc,
process_manager,
friends_socket,
restart_after_pending_update: AtomicBool::new(false),
@ -983,7 +971,6 @@ pub(crate) async fn test_state(
install_db_semaphore: Semaphore::new(1),
install_job_cancellations: DashMap::new(),
install_job_operation_locks: DashMap::new(),
discord_rpc: DiscordGuard::init()?,
process_manager: ProcessManager::new(),
friends_socket: FriendsSocket::new(),
restart_after_pending_update: AtomicBool::new(false),

View File

@ -35,7 +35,6 @@ impl ModrinthCredentials {
None,
Some(("Authorization", &*creds.session)),
None,
None,
Some("/v2/session/refresh"),
semaphore,
exec,
@ -228,7 +227,6 @@ async fn fetch_info(
None,
Some(("Authorization", token)),
None,
None,
Some("/v2/user"),
semaphore,
exec,

View File

@ -1194,25 +1194,6 @@ impl Process {
record_post_upgrade_launch_best_effort(&instance_id, clean_launch)
.await;
// Publish play time update
// Allow failure, it will be stored locally and sent next time
// Sent in another thread as first call may take a couple seconds and hold up process ending
let playtime_instance_id = instance_id.clone();
tokio::spawn(async move {
if let Err(e) =
crate::api::instance::try_update_playtime_by_instance_id(
&playtime_instance_id,
)
.await
{
tracing::warn!(
"Failed to update playtime for instance {}: {}",
playtime_instance_id,
e
);
}
});
let log_path = logs_folder.join(LAUNCHER_LOG_PATH);
if log_path.exists()
@ -1248,8 +1229,6 @@ impl Process {
)
.await?;
let _ = state.discord_rpc.clear_to_default(true).await;
if mc_exit_status.success() {
// We do not wait on the post exist command to finish running! We let it spawn + run on its own.
// This behaviour may be changed in the future

View File

@ -154,10 +154,6 @@ pub struct Settings {
#[serde(default = "default_terracotta_public_nodes")]
pub terracotta_public_nodes: Vec<String>,
pub telemetry: bool,
#[serde(default)]
pub telemetry_consent_version: u32,
pub discord_rpc: bool,
#[serde(skip, default)]
pub personalized_ads: bool,
@ -190,13 +186,6 @@ pub struct Settings {
pub version: usize,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub struct PrivacySettings {
pub telemetry: bool,
pub discord_rpc: bool,
pub consent_version: u32,
}
fn default_true() -> bool {
true
}
@ -330,9 +319,6 @@ impl Settings {
.as_ref()
.and_then(|value| serde_json::from_str(value).ok())
.unwrap_or_else(default_terracotta_public_nodes),
telemetry: res.telemetry == 1,
telemetry_consent_version: res.telemetry_consent_version as u32,
discord_rpc: res.discord_rpc == 1,
developer_mode: res.developer_mode == 1,
personalized_ads: res.personalized_ads == 1,
onboarded: res.onboarded == 1,
@ -521,9 +507,9 @@ impl Settings {
self.collapsed_navigation,
self.advanced_rendering,
self.native_decorations,
self.discord_rpc,
false,
self.developer_mode,
self.telemetry,
false,
self.personalized_ads,
self.onboarded,
extra_launch_args,
@ -573,7 +559,7 @@ impl Settings {
home_widgets,
mojang_auth_source,
terracotta_public_nodes,
self.telemetry_consent_version,
0_i64,
)
.execute(exec)
.await?;
@ -1355,16 +1341,20 @@ mod tests {
}
#[tokio::test]
async fn telemetry_schema_migrates_fresh_and_existing_settings_databases() {
async fn removed_data_collection_schema_is_cleaned_up() {
let fresh = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.unwrap();
sqlx::migrate!().run(&fresh).await.unwrap();
let settings = Settings::get(&fresh).await.unwrap();
assert!(!settings.telemetry);
assert_eq!(settings.telemetry_consent_version, 0);
let telemetry_tables = sqlx::query_scalar::<_, String>(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name LIKE 'telemetry_%'",
)
.fetch_all(&fresh)
.await
.unwrap();
assert!(telemetry_tables.is_empty());
assert!(
sqlx::query("PRAGMA foreign_key_check")
.fetch_all(&fresh)
@ -1372,44 +1362,5 @@ mod tests {
.unwrap()
.is_empty()
);
let upgrade = sqlx::sqlite::SqlitePoolOptions::new()
.max_connections(1)
.connect("sqlite::memory:")
.await
.unwrap();
sqlx::query(
"CREATE TABLE settings (id INTEGER PRIMARY KEY CHECK (id = 0), telemetry INTEGER NOT NULL DEFAULT 0, discord_rpc INTEGER NOT NULL DEFAULT 1)",
)
.execute(&upgrade)
.await
.unwrap();
sqlx::query(
"INSERT INTO settings (id, telemetry, discord_rpc) VALUES (0, 0, 1)",
)
.execute(&upgrade)
.await
.unwrap();
sqlx::raw_sql(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/migrations/20260814120000_telemetry.sql"
)))
.execute(&upgrade)
.await
.unwrap();
let consent_version = sqlx::query_scalar::<_, i64>(
"SELECT telemetry_consent_version FROM settings WHERE id = 0",
)
.fetch_one(&upgrade)
.await
.unwrap();
assert_eq!(consent_version, 0);
assert!(
sqlx::query("PRAGMA foreign_key_check")
.fetch_all(&upgrade)
.await
.unwrap()
.is_empty()
);
}
}

View File

@ -1,391 +0,0 @@
use std::fmt::Write as _;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use chrono::{SecondsFormat, Utc};
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use sqlx::Row;
use uuid::Uuid;
use crate::State;
const ENDPOINT: &str = "https://telemetry.axlmc.org/v1/batch";
const MAX_OUTBOX_EVENTS: i64 = 100;
const MAX_OUTBOX_BYTES: i64 = 2 * 1024 * 1024;
const MAX_EVENT_AGE_SECONDS: i64 = 7 * 24 * 60 * 60;
const MAX_BATCH_EVENTS: i64 = 10;
const MAX_BATCH_BYTES: usize = 60 * 1024;
static STARTED: AtomicBool = AtomicBool::new(false);
static WAKE_TX: OnceLock<tokio::sync::mpsc::Sender<()>> = OnceLock::new();
pub(crate) fn start(state: Arc<State>) {
if STARTED.swap(true, Ordering::AcqRel) {
return;
}
let (wake_tx, mut wake_rx) = tokio::sync::mpsc::channel(1);
let _ = WAKE_TX.set(wake_tx);
tokio::spawn(async move {
loop {
let client = match crate::util::fetch::configured_client().await {
Ok(client) => client,
Err(error) => {
tracing::debug!(target: "theseus::telemetry", %error, "Telemetry client configuration failed");
tokio::time::sleep(Duration::from_secs(60)).await;
continue;
}
};
if let Err(error) = run_cycle(&state, &client).await {
tracing::debug!(target: "theseus::telemetry", %error, "Telemetry cycle failed");
}
tokio::select! {
_ = tokio::time::sleep(Duration::from_secs(60)) => {},
_ = wake_rx.recv() => {},
}
}
});
}
pub async fn set_enabled(state: &State, enabled: bool) -> crate::Result<()> {
sqlx::query("DELETE FROM telemetry_outbox")
.execute(&state.pool)
.await?;
if enabled {
ensure_identity(&state.pool).await?;
enqueue_heartbeat(state).await?;
wake();
}
Ok(())
}
pub fn notify_online() {
wake();
}
async fn run_cycle(
state: &State,
client: &reqwest::Client,
) -> crate::Result<()> {
if !is_enabled(state).await? {
sqlx::query("DELETE FROM telemetry_outbox")
.execute(&state.pool)
.await?;
return Ok(());
}
ensure_identity(&state.pool).await?;
// Error events were supported by older clients. Drop any that remain in
// the local queue before selecting uploadable events.
sqlx::query("DELETE FROM telemetry_outbox WHERE event_type <> 'heartbeat'")
.execute(&state.pool)
.await?;
enqueue_heartbeat(state).await?;
cleanup_outbox(state).await?;
upload_next_batch(state, client).await?;
Ok(())
}
async fn is_enabled(state: &State) -> crate::Result<bool> {
let row = sqlx::query(
"SELECT telemetry, telemetry_consent_version FROM settings WHERE id = 0",
)
.fetch_one(&state.pool)
.await?;
Ok(row.get::<i64, _>("telemetry") == 1
&& row.get::<i64, _>("telemetry_consent_version") > 0)
}
async fn ensure_identity(pool: &sqlx::SqlitePool) -> crate::Result<String> {
if let Some(row) = sqlx::query(
"SELECT installation_id FROM telemetry_identity WHERE id = 0",
)
.fetch_optional(pool)
.await?
{
return Ok(row.get("installation_id"));
}
let installation_id = Uuid::new_v4().to_string();
sqlx::query(
"INSERT OR IGNORE INTO telemetry_identity (id, installation_id) VALUES (0, ?)",
)
.bind(&installation_id)
.execute(pool)
.await?;
let row = sqlx::query(
"SELECT installation_id FROM telemetry_identity WHERE id = 0",
)
.fetch_one(pool)
.await?;
Ok(row.get("installation_id"))
}
async fn enqueue_heartbeat(state: &State) -> crate::Result<()> {
let day = Utc::now().format("%Y-%m-%d").to_string();
let row = sqlx::query(
"SELECT last_heartbeat_day FROM telemetry_identity WHERE id = 0",
)
.fetch_one(&state.pool)
.await?;
if row
.get::<Option<String>, _>("last_heartbeat_day")
.as_deref()
== Some(&day)
{
return Ok(());
}
let event_id = Uuid::new_v4().to_string();
let occurred_at = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
let payload = heartbeat_payload(&event_id, &occurred_at, &day);
insert_outbox_event(
state,
&event_id,
&format!("heartbeat:{day}"),
&payload,
)
.await?;
sqlx::query(
"UPDATE telemetry_identity SET last_heartbeat_day = ? WHERE id = 0",
)
.bind(day)
.execute(&state.pool)
.await?;
Ok(())
}
fn heartbeat_payload(event_id: &str, occurred_at: &str, day: &str) -> Value {
json!({
"type": "heartbeat",
"event_id": event_id,
"occurred_at": occurred_at,
"day": day,
})
}
async fn insert_outbox_event(
state: &State,
event_id: &str,
dedupe_key: &str,
payload: &Value,
) -> crate::Result<()> {
let payload = serde_json::to_string(payload)?;
let now = Utc::now().timestamp();
sqlx::query(
r#"
INSERT INTO telemetry_outbox (
event_id, event_type, payload, created_at, next_attempt_at,
size_bytes, dedupe_key
) VALUES (?, 'heartbeat', jsonb(?), ?, ?, ?, ?)
ON CONFLICT(dedupe_key) DO NOTHING
"#,
)
.bind(event_id)
.bind(&payload)
.bind(now)
.bind(now)
.bind(payload.len() as i64)
.bind(dedupe_key)
.execute(&state.pool)
.await?;
cleanup_outbox(state).await
}
async fn cleanup_outbox(state: &State) -> crate::Result<()> {
let oldest = Utc::now().timestamp() - MAX_EVENT_AGE_SECONDS;
sqlx::query("DELETE FROM telemetry_outbox WHERE created_at < ?")
.bind(oldest)
.execute(&state.pool)
.await?;
sqlx::query(
"DELETE FROM telemetry_outbox WHERE event_id IN (SELECT event_id FROM telemetry_outbox ORDER BY created_at DESC LIMIT -1 OFFSET ?)",
)
.bind(MAX_OUTBOX_EVENTS)
.execute(&state.pool)
.await?;
sqlx::query(
r#"
DELETE FROM telemetry_outbox
WHERE event_id IN (
SELECT event_id FROM (
SELECT event_id,
SUM(size_bytes) OVER (ORDER BY created_at DESC, event_id DESC) AS running_bytes
FROM telemetry_outbox
)
WHERE running_bytes > ?
)
"#,
)
.bind(MAX_OUTBOX_BYTES)
.execute(&state.pool)
.await?;
Ok(())
}
async fn upload_next_batch(
state: &State,
client: &reqwest::Client,
) -> crate::Result<()> {
let now = Utc::now().timestamp();
let rows = sqlx::query(
"SELECT event_id, json(payload) AS payload FROM telemetry_outbox WHERE event_type = 'heartbeat' AND next_attempt_at <= ? ORDER BY created_at LIMIT ?",
)
.bind(now)
.bind(MAX_BATCH_EVENTS)
.fetch_all(&state.pool)
.await?;
if rows.is_empty() {
return Ok(());
}
let mut events = Vec::new();
let mut event_ids = Vec::new();
let mut approximate_size = 0;
for row in rows {
let payload: String = row.get("payload");
if approximate_size + payload.len() > MAX_BATCH_BYTES
&& !events.is_empty()
{
break;
}
let event: Value = serde_json::from_str(&payload)?;
approximate_size += payload.len();
events.push(event);
event_ids.push(row.get::<String, _>("event_id"));
}
let installation_id = ensure_identity(&state.pool).await?;
let batch_id = stable_batch_id(&event_ids);
let body = json!({
"schema_version": 1,
"batch_id": batch_id,
"installation_id": installation_id,
"app": {
"version": env!("CARGO_PKG_VERSION"),
"environment": if cfg!(debug_assertions) { "development" } else { "production" },
"platform": std::env::consts::OS,
"arch": std::env::consts::ARCH,
},
"events": events,
});
let endpoint = std::env::var("THESEUS_TELEMETRY_ENDPOINT")
.unwrap_or_else(|_| ENDPOINT.to_string());
let response = client.post(endpoint).json(&body).send().await;
match response {
Ok(response) if response.status().is_success() => {
delete_events(state, &event_ids).await?;
}
Ok(response)
if response.status().is_client_error()
&& response.status().as_u16() != 429 =>
{
delete_events(state, &event_ids).await?;
}
_ => schedule_retry(state, &event_ids).await?,
}
Ok(())
}
async fn delete_events(
state: &State,
event_ids: &[String],
) -> crate::Result<()> {
for event_id in event_ids {
sqlx::query("DELETE FROM telemetry_outbox WHERE event_id = ?")
.bind(event_id)
.execute(&state.pool)
.await?;
}
Ok(())
}
async fn schedule_retry(
state: &State,
event_ids: &[String],
) -> crate::Result<()> {
for event_id in event_ids {
let row = sqlx::query(
"SELECT attempts FROM telemetry_outbox WHERE event_id = ?",
)
.bind(event_id)
.fetch_optional(&state.pool)
.await?;
let Some(row) = row else { continue };
let attempts = row.get::<i64, _>("attempts") + 1;
let delay = match attempts {
1 => 60,
2 => 5 * 60,
3 => 30 * 60,
_ => 6 * 60 * 60,
};
sqlx::query(
"UPDATE telemetry_outbox SET attempts = ?, next_attempt_at = ? WHERE event_id = ?",
)
.bind(attempts)
.bind(Utc::now().timestamp() + delay)
.bind(event_id)
.execute(&state.pool)
.await?;
}
Ok(())
}
fn stable_batch_id(event_ids: &[String]) -> String {
let mut hasher = Sha256::new();
for event_id in event_ids {
hasher.update(event_id.as_bytes());
hasher.update([0]);
}
let digest = hex_digest(hasher.finalize().as_slice());
format!(
"{}-{}-{}-{}-{}",
&digest[0..8],
&digest[8..12],
&digest[12..16],
&digest[16..20],
&digest[20..32]
)
}
fn hex_digest(bytes: &[u8]) -> String {
let mut output = String::with_capacity(bytes.len() * 2);
for byte in bytes {
let _ = write!(output, "{byte:02x}");
}
output
}
fn wake() {
if let Some(sender) = WAKE_TX.get() {
let _ = sender.try_send(());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn batch_ids_are_stable() {
let ids = vec!["one".to_string(), "two".to_string()];
assert_eq!(stable_batch_id(&ids), stable_batch_id(&ids));
assert_ne!(stable_batch_id(&ids), stable_batch_id(&ids[..1]));
}
#[test]
fn heartbeat_payload_matches_the_strict_worker_shape() {
let payload = heartbeat_payload(
"11111111-1111-4111-8111-111111111111",
"2026-08-17T00:00:00.000Z",
"2026-08-17",
);
assert_eq!(payload.as_object().unwrap().len(), 4);
assert!(payload.get("download_stats").is_none());
assert_eq!(payload["type"], "heartbeat");
}
}

View File

@ -7,8 +7,7 @@
use super::h2_pool::{H2ConnectFailureKind, SharedH2Connection};
use crate::util::fetch;
use crate::util::fetch::{
DownloadRequest, DownloadResult, DownloadRoute, DownloadRouteSource,
Integrity,
DownloadRequest, DownloadResult, DownloadRoute, Integrity,
};
use futures::StreamExt;
use http::header::{ACCEPT_ENCODING, RANGE, USER_AGENT};
@ -395,16 +394,6 @@ pub(crate) fn request_headers(
}
}
}
if route.source == DownloadRouteSource::Official
&& fetch::is_official_modrinth_download_url(&request.url)
&& let Some(download_meta) = &request.download_meta
{
if let Ok(value) =
HeaderValue::from_str(&download_meta.to_header_value())
{
headers.insert("modrinth-download-meta", value);
}
}
headers
}

View File

@ -45,8 +45,6 @@ fn is_safe_redirect_location(location: &str) -> bool {
}
use uuid::Uuid;
pub const DOWNLOAD_META_HEADER: &str = "modrinth-download-meta";
const BMCLAPI_BASE_URL: &str = "https://bmclapi2.bangbang93.com";
const MCIM_BASE_URL: &str = "https://mod.mcimirror.top";
pub(crate) const TIANPAO_HOST: &str = "mod.tianpao.top";
@ -253,7 +251,6 @@ pub struct DownloadRequest {
pub url: String,
pub resource: ResourceClass,
pub integrity: Integrity,
pub download_meta: Option<DownloadMeta>,
pub header: Option<(String, String)>,
pub candidate_urls: Vec<String>,
/// Whether range-segmented (multi-connection) downloading is allowed.
@ -283,7 +280,6 @@ impl DownloadRequest {
url: url.into(),
resource,
integrity: Integrity::default(),
download_meta: None,
header: None,
candidate_urls: Vec::new(),
allow_segmented_download: true,
@ -318,11 +314,6 @@ impl DownloadRequest {
self
}
pub fn with_download_meta(mut self, download_meta: DownloadMeta) -> Self {
self.download_meta = Some(download_meta);
self
}
pub fn with_header(
mut self,
name: impl Into<String>,
@ -1077,19 +1068,6 @@ fn route_host(route: &DownloadRoute) -> Option<String> {
.and_then(|url| url.host_str().map(str::to_string))
}
pub(crate) fn is_official_modrinth_download_url(url: &str) -> bool {
Url::parse(url).is_ok_and(|url| {
matches!(
url.host_str(),
Some(
"api.modrinth.com"
| "cdn.modrinth.com"
| "cdn-alt.modrinth.com"
)
)
})
}
fn is_official_version_manifest_url(url: &str) -> bool {
Url::parse(url).is_ok_and(|url| {
matches!(
@ -1300,30 +1278,6 @@ fn infer_resource_class(url: &str) -> ResourceClass {
}
}
#[derive(Debug, derive_more::Display, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[display(rename_all = "snake_case")]
pub enum DownloadReason {
Standalone,
Dependency,
Modpack,
Update,
}
#[derive(Debug, Clone, Serialize)]
pub struct DownloadMeta {
pub reason: DownloadReason,
pub game_version: String,
pub loader: String,
pub dependent_on: Option<String>,
}
impl DownloadMeta {
pub fn to_header_value(&self) -> String {
serde_json::to_string(self).unwrap_or_default()
}
}
#[derive(Debug)]
pub struct IoSemaphore(pub Semaphore);
#[derive(Debug)]
@ -1956,7 +1910,6 @@ async fn fetch_hedged_metadata(
pub async fn fetch(
url: &str,
sha1: Option<&str>,
download_meta: Option<&DownloadMeta>,
uri_path: Option<&'static str>,
semaphore: &FetchSemaphore,
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
@ -1967,7 +1920,6 @@ pub async fn fetch(
sha1,
None,
None,
download_meta,
None,
uri_path,
semaphore,
@ -1981,7 +1933,6 @@ pub async fn fetch(
pub async fn fetch_official(
url: &str,
sha1: Option<&str>,
download_meta: Option<&DownloadMeta>,
uri_path: Option<&'static str>,
semaphore: &FetchSemaphore,
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
@ -1993,7 +1944,6 @@ pub async fn fetch_official(
sha1,
None,
None,
download_meta,
None,
uri_path,
semaphore,
@ -2033,7 +1983,6 @@ where
json_body,
None,
None,
None,
uri_path,
semaphore,
exec,
@ -2086,7 +2035,6 @@ where
json_body,
None,
None,
None,
uri_path,
semaphore,
exec,
@ -2110,7 +2058,6 @@ pub async fn fetch_advanced(
sha1: Option<&str>,
json_body: Option<serde_json::Value>,
header: Option<(&str, &str)>,
download_meta: Option<&DownloadMeta>,
loading_bar: Option<(&LoadingBarId, f64)>,
uri_path: Option<&'static str>,
semaphore: &FetchSemaphore,
@ -2122,7 +2069,6 @@ pub async fn fetch_advanced(
sha1,
json_body,
header,
download_meta,
loading_bar,
uri_path,
semaphore,
@ -2141,7 +2087,6 @@ pub async fn fetch_advanced_with_client(
sha1: Option<&str>,
json_body: Option<serde_json::Value>,
header: Option<(&str, &str)>,
download_meta: Option<&DownloadMeta>,
loading_bar: Option<(&LoadingBarId, f64)>,
uri_path: Option<&'static str>,
semaphore: &FetchSemaphore,
@ -2154,7 +2099,6 @@ pub async fn fetch_advanced_with_client(
sha1,
json_body,
header,
download_meta,
loading_bar,
uri_path,
semaphore,
@ -2176,7 +2120,6 @@ async fn fetch_advanced_with_client_and_progress(
sha1: Option<&str>,
json_body: Option<serde_json::Value>,
header: Option<(&str, &str)>,
download_meta: Option<&DownloadMeta>,
loading_bar: Option<(&LoadingBarId, f64)>,
uri_path: Option<&'static str>,
semaphore: &FetchSemaphore,
@ -2224,7 +2167,6 @@ async fn fetch_advanced_with_client_and_progress(
let mut attempt_history = VecDeque::new();
let hedge_is_safe = method == Method::GET
&& json_body.is_none()
&& download_meta.is_none()
&& progress.is_none()
&& creds.is_none()
&& header.is_none_or(|(name, _)| !is_sensitive_header(name))
@ -2273,15 +2215,6 @@ async fn fetch_advanced_with_client_and_progress(
let route_source = route.source;
let request_target = if is_mirror { "mirror" } else { "official" };
let has_next_route = route_index + 1 < request_routes.len();
let download_meta_header = (!is_mirror
&& is_official_modrinth_download_url(request_url))
.then(|| {
download_meta.map(|m| {
(DOWNLOAD_META_HEADER.to_string(), m.to_header_value())
})
})
.flatten();
let max_attempts = if modrinth_request_kind == Some("CDN") {
if is_mirror { 1 } else { MODRINTH_CDN_ATTEMPTS }
} else {
@ -2309,7 +2242,6 @@ async fn fetch_advanced_with_client_and_progress(
);
let protected_headers = creds.is_some()
|| download_meta_header.is_some()
|| header.is_some_and(|header| is_sensitive_header(header.0));
let route_client = match (route.proxy, protected_headers) {
(ProxyPolicy::System, false)
@ -2344,11 +2276,6 @@ async fn fetch_advanced_with_client_and_progress(
req = req.header("Authorization", &creds.session);
}
if let Some((name, value)) = &download_meta_header {
tracing::debug!("Sending download analytics: {value}");
req = req.header(name.as_str(), value.as_str());
}
let permit = semaphore.0.acquire().await?;
let request_started = Instant::now();
let result = req.send().await;
@ -3418,7 +3345,6 @@ async fn send_path_request_with_clients(
route: &DownloadRoute,
custom_header: Option<&(String, String)>,
credentials: Option<&crate::state::ModrinthCredentials>,
download_meta: Option<&DownloadMeta>,
range_start: Option<u64>,
range_end: Option<u64>,
system_client: &reqwest::Client,
@ -3468,14 +3394,6 @@ async fn send_path_request_with_clients(
if allow_sensitive && let Some(credentials) = credentials {
request = request.header("Authorization", &credentials.session);
}
if !route.is_mirror
&& same_as_original
&& is_official_modrinth_download_url(original.as_str())
&& let Some(download_meta) = download_meta
{
request = request
.header(DOWNLOAD_META_HEADER, download_meta.to_header_value());
}
if let Some(range) = byte_range_header_value(range_start, range_end) {
request = request
.header(header::RANGE, range)
@ -3581,7 +3499,6 @@ async fn send_path_request(
route: &DownloadRoute,
custom_header: Option<&(String, String)>,
credentials: Option<&crate::state::ModrinthCredentials>,
download_meta: Option<&DownloadMeta>,
range_start: Option<u64>,
range_end: Option<u64>,
) -> crate::Result<(reqwest::Response, String)> {
@ -3589,7 +3506,6 @@ async fn send_path_request(
route,
custom_header,
credentials,
download_meta,
range_start,
range_end,
&NO_REDIRECT_REQWEST_CLIENT,
@ -4034,7 +3950,6 @@ async fn probe_route_throughput(
total_size: u64,
custom_header: Option<&(String, String)>,
credentials: Option<&crate::state::ModrinthCredentials>,
download_meta: Option<&DownloadMeta>,
semaphore: &FetchSemaphore,
system_client: &reqwest::Client,
direct_client: &reqwest::Client,
@ -4054,7 +3969,6 @@ async fn probe_route_throughput(
route,
custom_header,
credentials,
download_meta,
Some(0),
Some(probe_end),
system_client,
@ -4134,7 +4048,6 @@ async fn probe_faster_route(
total_size: u64,
custom_header: Option<&(String, String)>,
credentials: Option<&crate::state::ModrinthCredentials>,
download_meta: Option<&DownloadMeta>,
semaphore: &FetchSemaphore,
system_client: &reqwest::Client,
direct_client: &reqwest::Client,
@ -4162,7 +4075,6 @@ async fn probe_faster_route(
total_size,
custom_header,
credentials,
download_meta,
semaphore,
system_client,
direct_client,
@ -4339,7 +4251,6 @@ async fn ensure_task_routes_probed(
size,
request.header.as_ref(),
None,
request.download_meta.as_ref(),
semaphore,
system_client,
direct_client,
@ -4473,7 +4384,6 @@ async fn download_tail_candidate(
requested_end: u64,
custom_header: Option<&(String, String)>,
credentials: Option<&crate::state::ModrinthCredentials>,
download_meta: Option<&DownloadMeta>,
part_path: &Path,
candidate_index: usize,
system_client: &reqwest::Client,
@ -4493,7 +4403,6 @@ async fn download_tail_candidate(
route,
custom_header,
credentials,
download_meta,
Some(requested_start),
Some(requested_end),
system_client,
@ -4588,7 +4497,6 @@ async fn race_tail_candidates(
requested_end: u64,
custom_header: Option<&(String, String)>,
credentials: Option<&crate::state::ModrinthCredentials>,
download_meta: Option<&DownloadMeta>,
part_path: &Path,
system_client: &reqwest::Client,
direct_client: &reqwest::Client,
@ -4603,7 +4511,6 @@ async fn race_tail_candidates(
requested_end,
custom_header,
credentials,
download_meta,
part_path,
0,
system_client,
@ -4619,7 +4526,6 @@ async fn race_tail_candidates(
requested_end,
custom_header,
credentials,
download_meta,
part_path,
1,
system_client,
@ -4647,7 +4553,6 @@ async fn download_segment(
total_size: u64,
custom_header: Option<&(String, String)>,
credentials: Option<&crate::state::ModrinthCredentials>,
download_meta: Option<&DownloadMeta>,
part_path: &Path,
output: &Arc<crate::util::download::range_output::RangeOutput>,
_permit: NativeConnectionPermit<'_>,
@ -4691,7 +4596,6 @@ async fn download_segment(
route,
custom_header,
credentials,
download_meta,
Some(requested_start),
Some(requested_end),
system_client,
@ -4819,7 +4723,6 @@ async fn download_segment(
hedge_end,
custom_header,
credentials,
download_meta,
part_path,
system_client,
direct_client,
@ -5085,7 +4988,6 @@ async fn try_segmented_download(
size,
request.header.as_ref(),
credentials,
request.download_meta.as_ref(),
part_path,
&output,
permit,
@ -5208,7 +5110,6 @@ async fn try_segmented_download(
size,
request.header.as_ref(),
credentials,
request.download_meta.as_ref(),
semaphore,
system_client,
direct_client,
@ -5305,7 +5206,6 @@ async fn try_segmented_download(
size,
request.header.as_ref(),
credentials,
None,
part_path,
&output,
permit,
@ -6216,7 +6116,6 @@ async fn download_to_path_inner(
&attempt_route,
request.header.as_ref(),
credentials.as_ref(),
request.download_meta.as_ref(),
(resume_offset > 0).then_some(resume_offset),
None,
),
@ -6600,7 +6499,6 @@ async fn download_to_path_inner(
total_size,
request.header.as_ref(),
credentials.as_ref(),
request.download_meta.as_ref(),
semaphore,
&NO_REDIRECT_REQWEST_CLIENT,
&DIRECT_REQWEST_CLIENT,
@ -6975,28 +6873,6 @@ async fn download_to_path_inner(
))
}
/// Posts a JSON to a URL
#[tracing::instrument(skip_all)]
pub async fn post_json(
url: &str,
json_body: serde_json::Value,
semaphore: &FetchSemaphore,
exec: impl sqlx::Executor<'_, Database = sqlx::Sqlite>,
) -> crate::Result<()> {
let _permit = semaphore.0.acquire().await?;
let mut req = INSECURE_REQWEST_CLIENT.post(url).json(&json_body);
if let Some(creds) =
crate::state::ModrinthCredentials::get_active(exec).await?
{
req = req.header("Authorization", &creds.session);
}
req.send().await?.error_for_status()?;
Ok(())
}
pub async fn read_json<T>(
path: &Path,
semaphore: &IoSemaphore,
@ -7526,22 +7402,6 @@ mod tests {
}
}
#[test]
fn modrinth_download_url_recognition_includes_cdn_alt() {
assert!(is_official_modrinth_download_url(
"https://cdn-alt.modrinth.com/data/project/version/file.jar"
));
assert!(is_official_modrinth_download_url(
"https://cdn.modrinth.com/data/project/version/file.jar"
));
assert!(is_official_modrinth_download_url(
"https://api.modrinth.com/v2/project/abc"
));
assert!(!is_official_modrinth_download_url(
"https://example.com/file.jar"
));
}
#[test]
fn cache_busted_urls_preserve_existing_query() {
assert_eq!(
@ -8678,7 +8538,6 @@ mod tests {
data.len() as u64,
None,
None,
None,
&semaphore,
&client,
&client,
@ -8707,7 +8566,6 @@ mod tests {
data.len() as u64,
None,
None,
None,
&semaphore,
&client,
&client,
@ -9044,7 +8902,6 @@ mod tests {
data.len() as u64,
None,
None,
None,
&part_path,
&output,
permit,