feat: update base to official upstream 2.5.1 with all cracked features intact (2.5.1-c1)
Some checks are pending
Release Multiplatform Binaries / build (deb,appimage, linux-x86_64, ubuntu-22.04, x86_64-unknown-linux-gnu) (push) Waiting to run
Release Multiplatform Binaries / build (dmg,app, macos-arm64, macos-14, aarch64-apple-darwin) (push) Waiting to run
Release Multiplatform Binaries / build (dmg,app, macos-x86_64, macos-14, x86_64-apple-darwin) (push) Waiting to run
Release Multiplatform Binaries / build (nsis, windows-x86_64, windows-latest, x86_64-pc-windows-msvc) (push) Waiting to run
Release Multiplatform Binaries / create-release (push) Blocked by required conditions

This commit is contained in:
katR 2026-09-18 07:16:25 +05:30
parent 8093cafd53
commit 991a85c589
25 changed files with 232 additions and 196 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View file

@ -7,9 +7,9 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
tag: tag:
description: 'Release Tag (e.g. v2.5.0)' description: 'Release Tag (e.g. v2.5.1)'
required: false required: false
default: 'v2.5.0-c1' default: 'v2.5.1-c1'
type: string type: string
permissions: permissions:
@ -138,7 +138,7 @@ jobs:
elif [ -n "${{ inputs.tag }}" ]; then elif [ -n "${{ inputs.tag }}" ]; then
echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT
else else
echo "tag=v2.5.0-c1" >> $GITHUB_OUTPUT echo "tag=v2.5.1-c1" >> $GITHUB_OUTPUT
fi fi
- name: Clean up previous release if exists - name: Clean up previous release if exists
@ -155,7 +155,7 @@ jobs:
name: "OneLauncher-Cracked ${{ steps.release_tag.outputs.tag }}" name: "OneLauncher-Cracked ${{ steps.release_tag.outputs.tag }}"
body: | body: |
### What's New in ${{ steps.release_tag.outputs.tag }} ### What's New in ${{ steps.release_tag.outputs.tag }}
- **Rebased onto Official Upstream 2.5.0**: Includes all upstream 2.5.0 updates (New Control Center quick settings & system info, refined navbar styling, bundle version-tag matching and compatibility filtering, version metadata self-healing). - **Rebased onto Official Upstream 2.5.1**: Includes all upstream 2.5.1 updates (Process hook error handling and execution reporting, optimized bundled packages sync after launch, auto-refresh of stale package metadata, asset optimizations).
- **Guaranteed Offline Game Launch**: Completely eliminated the upstream check in `account_for_launch` that blocked offline accounts from launching without Microsoft accounts. Offline accounts can launch directly with 0 Microsoft accounts required. - **Guaranteed Offline Game Launch**: Completely eliminated the upstream check in `account_for_launch` that blocked offline accounts from launching without Microsoft accounts. Offline accounts can launch directly with 0 Microsoft accounts required.
- **Custom Offline Skins & Targeted Multiplayer Injection**: In-launcher 3D skin changer, slim/classic model toggle, per-account Java UUID hash targeting preventing multiplayer skin cloning, and automatic CustomSkinLoader config generation. - **Custom Offline Skins & Targeted Multiplayer Injection**: In-launcher 3D skin changer, slim/classic model toggle, per-account Java UUID hash targeting preventing multiplayer skin cloning, and automatic CustomSkinLoader config generation.
- **Offline / Cracked Accounts**: Full offline account support in onboarding and settings with inline username editing. - **Offline / Cracked Accounts**: Full offline account support in onboarding and settings with inline username editing.

30
Cargo.lock generated
View file

@ -4480,7 +4480,7 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]] [[package]]
name = "oneclient_app" name = "oneclient_app"
version = "2.5.0-c1" version = "2.5.1-c1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arboard", "arboard",
@ -4530,7 +4530,7 @@ dependencies = [
[[package]] [[package]]
name = "oneclient_auth" name = "oneclient_auth"
version = "2.5.0-c1" version = "2.5.1-c1"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",
@ -4554,7 +4554,7 @@ dependencies = [
[[package]] [[package]]
name = "oneclient_cluster" name = "oneclient_cluster"
version = "2.5.0-c1" version = "2.5.1-c1"
dependencies = [ dependencies = [
"bytes 1.11.1", "bytes 1.11.1",
"chrono", "chrono",
@ -4577,7 +4577,7 @@ dependencies = [
[[package]] [[package]]
name = "oneclient_common" name = "oneclient_common"
version = "2.5.0-c1" version = "2.5.1-c1"
dependencies = [ dependencies = [
"directories", "directories",
"interfrost", "interfrost",
@ -4590,7 +4590,7 @@ dependencies = [
[[package]] [[package]]
name = "oneclient_content" name = "oneclient_content"
version = "2.5.0-c1" version = "2.5.1-c1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"async_zip", "async_zip",
@ -4618,7 +4618,7 @@ dependencies = [
[[package]] [[package]]
name = "oneclient_core" name = "oneclient_core"
version = "2.5.0-c1" version = "2.5.1-c1"
dependencies = [ dependencies = [
"async-compression", "async-compression",
"async-stream", "async-stream",
@ -4683,7 +4683,7 @@ dependencies = [
[[package]] [[package]]
name = "oneclient_db" name = "oneclient_db"
version = "2.5.0-c1" version = "2.5.1-c1"
dependencies = [ dependencies = [
"async-tempfile", "async-tempfile",
"chrono", "chrono",
@ -4698,7 +4698,7 @@ dependencies = [
[[package]] [[package]]
name = "oneclient_discord" name = "oneclient_discord"
version = "2.5.0-c1" version = "2.5.1-c1"
dependencies = [ dependencies = [
"chrono", "chrono",
"discord-rich-presence", "discord-rich-presence",
@ -4710,7 +4710,7 @@ dependencies = [
[[package]] [[package]]
name = "oneclient_events" name = "oneclient_events"
version = "2.5.0-c1" version = "2.5.1-c1"
dependencies = [ dependencies = [
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
@ -4720,7 +4720,7 @@ dependencies = [
[[package]] [[package]]
name = "oneclient_java" name = "oneclient_java"
version = "2.5.0-c1" version = "2.5.1-c1"
dependencies = [ dependencies = [
"astral-tokio-tar", "astral-tokio-tar",
"async-tempfile", "async-tempfile",
@ -4743,7 +4743,7 @@ dependencies = [
[[package]] [[package]]
name = "oneclient_macro" name = "oneclient_macro"
version = "2.5.0-c1" version = "2.5.1-c1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -4752,7 +4752,7 @@ dependencies = [
[[package]] [[package]]
name = "oneclient_mc" name = "oneclient_mc"
version = "2.5.0-c1" version = "2.5.1-c1"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"chrono", "chrono",
@ -4774,7 +4774,7 @@ dependencies = [
[[package]] [[package]]
name = "oneclient_net" name = "oneclient_net"
version = "2.5.0-c1" version = "2.5.1-c1"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"async-trait", "async-trait",
@ -4796,7 +4796,7 @@ dependencies = [
[[package]] [[package]]
name = "oneclient_polyplus" name = "oneclient_polyplus"
version = "2.5.0-c1" version = "2.5.1-c1"
dependencies = [ dependencies = [
"async-tungstenite", "async-tungstenite",
"bytes 1.11.1", "bytes 1.11.1",
@ -5094,7 +5094,7 @@ checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
[[package]] [[package]]
name = "polyio" name = "polyio"
version = "2.5.0-c1" version = "2.5.1-c1"
dependencies = [ dependencies = [
"astral-tokio-tar", "astral-tokio-tar",
"async-compression", "async-compression",

View file

@ -16,7 +16,7 @@ license = "GPL-3.0-only"
readme = "README.md" readme = "README.md"
repository = "https://github.com/oiupoyt/OneLauncher-Cracked" repository = "https://github.com/oiupoyt/OneLauncher-Cracked"
rust-version = "1.97" rust-version = "1.97"
version = "2.5.0-c1" version = "2.5.1-c1"
[workspace.dependencies] [workspace.dependencies]
# core # core

View file

@ -28,7 +28,10 @@ makedepends=(
) )
provides=('oneclient') provides=('oneclient')
conflicts=('oneclient-bin') conflicts=('oneclient-bin')
options=('!lto', '!debug') options=(
'!lto'
'!debug'
)
source=("$pkgname-$pkgver.tar.gz::${url}/archive/refs/tags/oneclient-${pkgver}.tar.gz") source=("$pkgname-$pkgver.tar.gz::${url}/archive/refs/tags/oneclient-${pkgver}.tar.gz")
sha256sums=('SKIP') sha256sums=('SKIP')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 958 B

After

Width:  |  Height:  |  Size: 920 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 848 B

After

Width:  |  Height:  |  Size: 847 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1373,13 +1373,9 @@ impl Actions {
spawn_forever(async move { spawn_forever(async move {
let synced = match state.bundles.sync(&state.services.content()).await { if let Err(err) = state.bundles.sync(&state.services.content()).await {
Ok(_) => true,
Err(err) => {
tracing::error!("bundle catalog sync failed: {err:#}"); tracing::error!("bundle catalog sync failed: {err:#}");
false
} }
};
if let Err(err) = oneclient_core::clusters::apply_remote_migrations(&state).await { if let Err(err) = oneclient_core::clusters::apply_remote_migrations(&state).await {
tracing::error!("cluster migrations failed: {err:#}"); tracing::error!("cluster migrations failed: {err:#}");
} }
@ -1387,37 +1383,6 @@ impl Actions {
tracing::error!("bundle cluster provisioning failed: {err:#}"); tracing::error!("bundle cluster provisioning failed: {err:#}");
} }
// One grouped session for the batch so all downloads surface as a
// single notification `detach` lets it be converted in place
let session = oneclient_events::GroupedProgressSession::start(
&state.services.events,
"Updating mods",
);
let changed = oneclient_content::bundles::sync_all_cluster_bundles(
state.bundles.as_ref(),
&state.services.content(),
Some(&session),
)
.await;
let session_id = session.detach();
if synced
&& let Ok(clusters) = state.clusters.list().await
{
let unsettled: std::collections::HashSet<i64> = changed
.iter()
.filter(|(_, result)| !result.settled())
.map(|(cluster_id, _)| *cluster_id)
.collect();
actions.record_bundle_checks(
clusters
.iter()
.map(|cluster| cluster.id)
.filter(|cluster_id| !unsettled.contains(cluster_id)),
);
}
actions actions
.station .station
.clone() .clone()
@ -1425,12 +1390,6 @@ impl Actions {
.launcher .launcher
.syncing_bundles = false; .syncing_bundles = false;
super::invalidate_cluster_queries().await; super::invalidate_cluster_queries().await;
let spec = crate::install::combined_cluster_update_spec(&changed, &state.services).await;
actions.with_engine(|app| {
app.notifications
.finish_grouped_as_actions(&mut app.inbox, session_id, spec);
});
}); });
} }
@ -1451,8 +1410,6 @@ impl Actions {
return; return;
} }
let mode = self.cluster_update_mode(state, cluster_id).await;
self.station self.station
.clone() .clone()
.write_channel(AppChannel::Launcher) .write_channel(AppChannel::Launcher)
@ -1486,15 +1443,8 @@ impl Actions {
.launcher .launcher
.syncing_bundles = false; .syncing_bundles = false;
// No prompt for bundles the package manager's markers carry them instead // Bundle content is forced, so the update mode never gates it and the
if !matches!(mode, PackageUpdateMode::Automatic) { // modal never lists it
tracing::debug!(cluster_id, "bundle updates left for the user to apply");
if synced {
self.record_bundle_checks([cluster_id]);
}
return;
}
let result = match oneclient_core::apply_bundle_updates( let result = match oneclient_core::apply_bundle_updates(
cluster_id, cluster_id,
state.bundles.as_ref(), state.bundles.as_ref(),
@ -1533,12 +1483,6 @@ impl Actions {
} }
super::invalidate_cluster_queries().await; super::invalidate_cluster_queries().await;
if let Some(spec) =
crate::install::cluster_update_notification(cluster_id, &result, &state.services).await
{
self.push_notification(spec);
}
} }
/// The check runs and the cache is written whatever the mode says keeping /// The check runs and the cache is written whatever the mode says keeping

View file

@ -168,60 +168,6 @@ pub async fn cluster_update_notification(
}) })
} }
/// All clusters share one "View changes" action so the notification keeps exactly two buttons
pub async fn combined_cluster_update_spec(
changed: &[(i64, oneclient_core::ApplyBundleUpdatesResult)],
services: &oneclient_core::LauncherServices,
) -> Option<NotificationSpec> {
let mut summaries = Vec::new();
let mut total_changes = 0usize;
for (cluster_id, result) in changed {
if let Some(summary) = cluster_update_summary(*cluster_id, result, services).await {
total_changes += summary.total();
summaries.push(summary);
}
}
if summaries.is_empty() {
return None;
}
let cluster_count = summaries.len();
let (title, body) = if total_changes == 0 {
let offers: usize = summaries.iter().map(|s| s.optional.len()).sum();
(
"Optional mods available",
format!(
"{offers} optional mod{} across {cluster_count} cluster{}",
if offers == 1 { "" } else { "s" },
if cluster_count == 1 { "" } else { "s" }
),
)
} else {
(
"Mods updated",
format!(
"{total_changes} package{} updated across {cluster_count} cluster{}",
if total_changes == 1 { "" } else { "s" },
if cluster_count == 1 { "" } else { "s" }
),
)
};
Some(NotificationSpec {
title: title.to_string(),
body,
level: Level::Info,
icon: Some(IconType::DownloadCloud02),
progress: None,
actions: vec![NotificationAction {
label: "View changes".to_string(),
kind: NotificationActionKind::OpenClusterUpdate(summaries),
}],
})
}
pub async fn pending_optional_group( pub async fn pending_optional_group(
cluster_id: i64, cluster_id: i64,
pending: &[oneclient_core::PendingOptionalMod], pending: &[oneclient_core::PendingOptionalMod],

View file

@ -5,7 +5,6 @@ mod manifest;
mod optional; mod optional;
mod overrides; mod overrides;
mod polymrpack; mod polymrpack;
mod runtime;
mod types; mod types;
mod updates; mod updates;
@ -25,7 +24,6 @@ pub use optional::{
PendingOptionalMod, pending_optional_mods, resolve_optional_mods, skip_optional_mods, PendingOptionalMod, pending_optional_mods, resolve_optional_mods, skip_optional_mods,
}; };
pub use manifest::BundleManifest as RemoteBundleManifest; pub use manifest::BundleManifest as RemoteBundleManifest;
pub use runtime::{is_bundle_syncing, sync_all_cluster_bundles};
pub use types::{ pub use types::{
ApplyBundleUpdatesResult, BundleArchive, BundleFile, BundleFileKind, BundleOptionalPackage, ApplyBundleUpdatesResult, BundleArchive, BundleFile, BundleFileKind, BundleOptionalPackage,
BundleManifest, BundlePackageAddition, BundlePackageRemoval, BundlePackageUpdate, BundleManifest, BundlePackageAddition, BundlePackageRemoval, BundlePackageUpdate,

View file

@ -902,34 +902,6 @@ pub async fn get_bundles_with_update_status(
Ok(results) Ok(results)
} }
#[tracing::instrument(level = "debug", skip_all)]
pub async fn apply_bundle_updates_for_all_clusters(
bundles: &BundlesManager,
ctx: &ContentCtx,
session: Option<&oneclient_events::GroupedProgressSession>,
) -> ContentResult<Vec<(i64, ApplyBundleUpdatesResult)>> {
let mut changed = Vec::new();
for cluster in cluster_dao::list_all(&ctx.db).await? {
match apply_bundle_updates_with(cluster.id, bundles, ctx, session, None).await {
Ok(result) => {
if !result.updates_applied.is_empty()
|| !result.additions_applied.is_empty()
|| !result.removals_applied.is_empty()
|| !result.optional_available.is_empty()
{
changed.push((cluster.id, result));
}
}
Err(err) => tracing::warn!(
cluster_id = cluster.id,
error = %err,
"bundle update apply failed for cluster"
),
}
}
Ok(changed)
}
fn bundle_package_key( fn bundle_package_key(
bundle_pkg: &BundleTrackedArtifactRow, bundle_pkg: &BundleTrackedArtifactRow,
linked: &HashMap<String, &LinkedArtifactInfo>, linked: &HashMap<String, &LinkedArtifactInfo>,

View file

@ -13,7 +13,8 @@ use oneclient_db::dao::cluster_bundle as bundle_dao;
use crate::ctx::ContentCtx; use crate::ctx::ContentCtx;
use crate::error::ContentResult; use crate::error::ContentResult;
use crate::packages::dependencies::base_game_version; use crate::packages::dependencies::base_game_version;
use crate::packages::store::PackageStore; use crate::packages::FileIdentity;
use crate::packages::store::{PackageStore, record_release};
use crate::packages::types::LinkedArtifactInfo; use crate::packages::types::LinkedArtifactInfo;
struct Copy { struct Copy {
@ -126,11 +127,18 @@ pub async fn disable_foreign_game_versions(
.map(|row| row.hash) .map(|row| row.hash)
.collect(); .collect();
let candidates: Vec<&LinkedArtifactInfo> = linked
.iter()
.filter(|info| {
info.enabled && info.content_type == ContentType::Mod && !from_bundle.contains(&info.hash)
})
.collect();
refresh_stated_game_versions(cluster_id, &candidates, mc_version, ctx).await;
let mut switched_off = Vec::new(); let mut switched_off = Vec::new();
for info in linked.iter().filter(|info| { for info in candidates {
info.enabled && info.content_type == ContentType::Mod && !from_bundle.contains(&info.hash)
}) {
match switch_off_if_foreign(cluster_id, info, mc_version, ctx).await { match switch_off_if_foreign(cluster_id, info, mc_version, ctx).await {
Ok(true) => switched_off.push( Ok(true) => switched_off.push(
info.display_name info.display_name
@ -152,13 +160,64 @@ pub async fn disable_foreign_game_versions(
const FOREIGN_VERSION_REPAIR: &str = "disable-foreign-game-version"; const FOREIGN_VERSION_REPAIR: &str = "disable-foreign-game-version";
fn foreign_repair_id(cluster_id: i64, mc_version: &str, hash: &str) -> String {
format!("{FOREIGN_VERSION_REPAIR}:{cluster_id}:{mc_version}:{hash}")
}
async fn refresh_stated_game_versions(
cluster_id: i64,
candidates: &[&LinkedArtifactInfo],
mc_version: &str,
ctx: &ContentCtx,
) {
let mut stale = Vec::new();
for info in candidates {
match migration_dao::is_applied(&ctx.db, &foreign_repair_id(cluster_id, mc_version, &info.hash)).await {
Ok(true) => continue,
Ok(false) => {}
Err(err) => {
tracing::debug!(hash = %info.hash, %err, "could not read the repair marker");
continue;
}
}
match artifact_dao::list_release_game_versions(&ctx.db, &info.hash).await {
Ok(stated) if built_for_another_game_version(&stated, mc_version) => {
stale.push(FileIdentity::from_sha1(&info.hash));
}
Ok(_) => {}
Err(err) => tracing::debug!(hash = %info.hash, %err, "could not read the stated game versions"),
}
}
if stale.is_empty() {
return;
}
let found = match ctx.providers.lookup_versions(&stale, ctx).await {
Ok(found) => found,
// Offline or the provider is down, so the stored rows are all there is
Err(err) => {
tracing::debug!(%err, "could not refresh the stated game versions");
return;
}
};
for (sha1, (provider, version)) in found {
if let Err(err) = record_release(provider, &version, &sha1, ctx).await {
tracing::debug!(hash = %sha1, %err, "could not store the refreshed game versions");
}
}
}
async fn switch_off_if_foreign( async fn switch_off_if_foreign(
cluster_id: i64, cluster_id: i64,
info: &LinkedArtifactInfo, info: &LinkedArtifactInfo,
mc_version: &str, mc_version: &str,
ctx: &ContentCtx, ctx: &ContentCtx,
) -> ContentResult<bool> { ) -> ContentResult<bool> {
let repair_id = format!("{FOREIGN_VERSION_REPAIR}:{cluster_id}:{mc_version}:{}", info.hash); let repair_id = foreign_repair_id(cluster_id, mc_version, &info.hash);
if migration_dao::is_applied(&ctx.db, &repair_id).await? { if migration_dao::is_applied(&ctx.db, &repair_id).await? {
return Ok(false); return Ok(false);
} }

View file

@ -571,7 +571,7 @@ async fn already_described(row: &ArtifactRow, ctx: &ContentCtx) -> bool {
/// Written exactly as a download would have so the row joins the update flow /// Written exactly as a download would have so the row joins the update flow
/// rather than sitting outside it /// rather than sitting outside it
async fn record_release( pub(crate) async fn record_release(
provider: ProviderId, provider: ProviderId,
version: &VersionDetail, version: &VersionDetail,
hash: &str, hash: &str,

View file

@ -37,4 +37,28 @@ pub enum GameError {
#[error("failed to spawn the game process: {0}")] #[error("failed to spawn the game process: {0}")]
Spawn(String), Spawn(String),
#[error(
"the wrapper command '{program}' could not be started: {reason}. Check the Wrapper Command setting."
)]
WrapperSpawn { program: String, reason: String },
}
impl GameError {
pub(crate) fn wrapper_spawn(program: &str, err: &std::io::Error) -> Self {
let reason = match err.kind() {
std::io::ErrorKind::NotFound => {
"command not found".to_string()
}
std::io::ErrorKind::PermissionDenied => {
"insufficient permissions or file is not an executable".to_string()
}
_ => err.to_string(),
};
Self::WrapperSpawn {
program: program.to_string(),
reason,
}
}
} }

View file

@ -5,6 +5,7 @@ use std::time::Duration;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use interfrost::api::minecraft::ArgumentType; use interfrost::api::minecraft::ArgumentType;
use tokio::io::AsyncReadExt;
use tokio::process::Command; use tokio::process::Command;
use crate::ClusterStage; use crate::ClusterStage;
@ -332,7 +333,13 @@ async fn start(
)?; )?;
arguments::append_profile_game_arguments(&mut mc_args, profile.force_fullscreen, None); arguments::append_profile_game_arguments(&mut mc_args, profile.force_fullscreen, None);
run_hook(profile.hook_pre.as_deref(), &cwd).await; if let Some(reason) = run_hook(profile.hook_pre.as_deref(), &cwd).await {
events
.notify("Pre-launch command failed")
.body(reason)
.error()
.send();
}
tracing::info!( tracing::info!(
cluster_id, cluster_id,
@ -344,7 +351,7 @@ async fn start(
); );
tracing::debug!(cluster_id, ?jvm_args, main_class = %version_info.main_class, "jvm arguments"); tracing::debug!(cluster_id, ?jvm_args, main_class = %version_info.main_class, "jvm arguments");
let mut command = base_command(&profile, &java.absolute_path); let (mut command, wrapper) = base_command(&profile, &java.absolute_path);
if profile.use_discrete_gpu() { if profile.use_discrete_gpu() {
crate::game::gpu::prefer_discrete(&mut command, &java.absolute_path).await; crate::game::gpu::prefer_discrete(&mut command, &java.absolute_path).await;
@ -387,9 +394,10 @@ async fn start(
command.stdin(Stdio::null()); command.stdin(Stdio::null());
detach(&mut command); detach(&mut command);
let mut child = command let mut child = command.spawn().map_err(|err| match &wrapper {
.spawn() Some(program) => GameError::wrapper_spawn(program, &err),
.map_err(|err| GameError::Spawn(err.to_string()))?; None => GameError::Spawn(err.to_string()),
})?;
let pid = child.id(); let pid = child.id();
stage(LaunchStage::Running); stage(LaunchStage::Running);
@ -566,7 +574,15 @@ pub(crate) async fn finalize_session(
recorder.finish_at(&end.ended_at.to_rfc3339(), code).await; recorder.finish_at(&end.ended_at.to_rfc3339(), code).await;
} }
run_hook(post_hook, cwd).await; if let Some(reason) = run_hook(post_hook, cwd).await {
state
.services
.events
.notify("Post-exit command failed")
.body(reason)
.error()
.send();
}
if dedicated { if dedicated {
// The folder stays materialized so it remains a real Minecraft directory // The folder stays materialized so it remains a real Minecraft directory
@ -667,21 +683,25 @@ pub async fn offer_repair(
} }
} }
fn base_command(profile: &GameSettingsProfile, java_path: &str) -> Command { fn base_command(profile: &GameSettingsProfile, java_path: &str) -> (Command, Option<String>) {
if let Some(wrapper) = profile let Some(wrapper) = profile
.hook_wrapper .hook_wrapper
.as_deref() .as_deref()
.map(str::trim) .map(str::trim)
.filter(|hook| !hook.is_empty()) .filter(|hook| !hook.is_empty())
{ else {
return (Command::new(java_path), None);
};
let mut split = wrapper.split_whitespace(); let mut split = wrapper.split_whitespace();
let mut command = Command::new(split.next().unwrap_or("sh")); let Some(program) = split.next() else {
return (Command::new(java_path), None);
};
let mut command = Command::new(program);
command.args(split); command.args(split);
command.arg(java_path); command.arg(java_path);
command (command, Some(program.to_string()))
} else {
Command::new(java_path)
}
} }
fn apply_env(command: &mut Command, profile: &GameSettingsProfile) { fn apply_env(command: &mut Command, profile: &GameSettingsProfile) {
@ -697,10 +717,8 @@ fn apply_env(command: &mut Command, profile: &GameSettingsProfile) {
} }
#[tracing::instrument(skip(cwd), fields(hook), level = "debug")] #[tracing::instrument(skip(cwd), fields(hook), level = "debug")]
async fn run_hook(hook: Option<&str>, cwd: &Path) { async fn run_hook(hook: Option<&str>, cwd: &Path) -> Option<String> {
let Some(hook) = hook.map(str::trim).filter(|h| !h.is_empty()) else { let hook = hook.map(str::trim).filter(|h| !h.is_empty())?;
return;
};
#[cfg(windows)] #[cfg(windows)]
let mut command = { let mut command = {
@ -716,12 +734,84 @@ async fn run_hook(hook: Option<&str>, cwd: &Path) {
}; };
command.current_dir(cwd); command.current_dir(cwd);
command.stderr(Stdio::piped());
command.stdin(Stdio::null());
oneclient_common::process::no_window(command.as_std_mut()); oneclient_common::process::no_window(command.as_std_mut());
if let Err(err) = command.status().await {
tracing::warn!("hook '{hook}' failed: {err}"); let mut child = match command.spawn() {
Ok(child) => child,
Err(err) => {
tracing::warn!("hook '{hook}' could not start: {err}");
return Some(format!("The command shell could not be started: {err}"));
} }
};
let collector = child.stderr.take().map(|mut pipe| {
tokio::spawn(async move {
let mut buf = Vec::new();
let _ = pipe.read_to_end(&mut buf).await;
buf
})
});
let status = match child.wait().await {
Ok(status) => status,
Err(err) => {
if let Some(collector) = collector {
collector.abort();
}
tracing::warn!("hook '{hook}' could not be waited on: {err}");
return Some(format!("The command could not be waited on: {err}"));
}
};
if status.success() {
if let Some(collector) = collector {
collector.abort();
}
return None;
}
let detail = match collector {
Some(mut collector) => {
match tokio::time::timeout(Duration::from_millis(200), &mut collector).await {
Ok(Ok(buf)) => stderr_detail(&buf),
_ => {
collector.abort();
None
}
}
}
None => None,
};
let message = match (detail, status.code()) {
(Some(detail), _) => detail,
(None, Some(code)) => format!("The command exited with code {code}"),
(None, None) => "The command was stopped before it finished".to_string(),
};
tracing::warn!("hook '{hook}' failed: {message}");
Some(message)
} }
fn stderr_detail(bytes: &[u8]) -> Option<String> {
let text = String::from_utf8_lossy(bytes);
let line = text
.lines()
.rev()
.map(str::trim)
.find(|line| !line.is_empty())?;
const MAX: usize = 300;
if line.chars().count() <= MAX {
return Some(line.to_string());
}
let mut clipped: String = line.chars().take(MAX).collect();
clipped.push('…');
Some(clipped)
}
/// Computes Java's exact 32-bit signed integer `uuid.hashCode()`. /// Computes Java's exact 32-bit signed integer `uuid.hashCode()`.
fn java_uuid_hash(uuid: &uuid::Uuid) -> i32 { fn java_uuid_hash(uuid: &uuid::Uuid) -> i32 {
let (most_sig, least_sig) = uuid.as_u64_pair(); let (most_sig, least_sig) = uuid.as_u64_pair();

View file

@ -27,7 +27,7 @@ pub use oneclient_content::bundles::{
cluster_has_bundle_content, effective_enabled, cluster_has_bundle_content, effective_enabled,
install_bundle, install_bundle,
install_cluster_bundles, install_cluster_bundles,
install_package_from_bundle, is_bundle_syncing, list_cluster_bundle_overrides, install_package_from_bundle, list_cluster_bundle_overrides,
pending_optional_mods, resolve_optional_mods, skip_optional_mods, PendingOptionalMod, pending_optional_mods, resolve_optional_mods, skip_optional_mods, PendingOptionalMod,
set_bundle_package_enabled, set_bundle_package_opt_in, set_bundle_package_override, set_bundle_package_enabled, set_bundle_package_opt_in, set_bundle_package_override,
set_bundle_package_overrides, set_bundle_package_overrides,