Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,6 @@ rand = "=0.8.5" # Locked on 0.8 until argon2 and p256 update to 0.9
rand_chacha = "=0.3.1" # Locked on 0.3 until we can update rand to 0.9
rdkafka = { version = "0.36.2", features = ["cmake-build"] }
redis = "1.4.1"
reflink-copy = "0.1.30"
regex = "1.12.2"
reqwest = { version = "0.12.24", default-features = false }
rgb = "0.8.52"
Expand Down
2 changes: 1 addition & 1 deletion apps/app-frontend/src/locales/en-US/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -3714,7 +3714,7 @@
"message": "Adding files ({completed}/{total})"
},
"instance.files.managed-content-read-only": {
"message": "Managed content is read-only here. Add, disable, update, or remove it from the Content tab."
"message": "Manage your installed content via the Content tab"
},
"instance.files.save-as": {
"message": "Save as..."
Expand Down
6 changes: 1 addition & 5 deletions apps/app-frontend/src/pages/instance/files/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ const debug = useDebugLogger('Files')
const messages = defineMessages({
readOnly: {
id: 'instance.files.managed-content-read-only',
defaultMessage:
'Managed content is read-only here. Add, disable, update, or remove it from the Content tab.',
defaultMessage: 'Manage your installed content via the Content tab',
},
saveAs: {
id: 'instance.files.save-as',
Expand Down Expand Up @@ -337,9 +336,6 @@ provideFileManager({
<template>
<ReadyTransition :pending="firstPaintPending">
<div>
<p v-if="isReadOnly(currentPath)" class="m-0 mb-4 text-sm text-secondary">
{{ formatMessage(messages.readOnly) }}
</p>
<FilePageLayout :show-refresh-button="true" />
</div>
</ReadyTransition>
Expand Down
1 change: 0 additions & 1 deletion packages/app-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ postcard-bindgen = { workspace = true, features = ["generating", "std"], optiona
quartz_nbt = { workspace = true, features = ["serde"] }
quick-xml = { workspace = true, features = ["async-tokio"] }
rand = { workspace = true }
reflink-copy = { workspace = true }
regex = { workspace = true }
reqwest = { workspace = true, features = [
"brotli",
Expand Down
44 changes: 44 additions & 0 deletions packages/app-lib/src/launcher/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,50 @@ fn should_download(path_exists: bool, force: bool) -> bool {
!path_exists || force
}

/// Installed instances can outlive files in the shared runtime cache. Include
/// processor-generated libraries so recovery also reruns the loader installer.
pub(super) fn missing_runtime_file(
st: &State,
version: &GameVersionInfo,
java_arch: &str,
minecraft_updated: bool,
) -> crate::Result<Option<std::path::PathBuf>> {
let client_path = st
.directories
.version_dir(&version.id)
.join(format!("{}.jar", version.id));
if !client_path.try_exists()? {
return Ok(Some(client_path));
}

for library in &version.libraries {
if let Some(rules) = &library.rules
&& !parse_rules(
rules,
java_arch,
&QuickPlayType::None,
minecraft_updated,
)
{
continue;
}
if !library.include_in_classpath
|| library.natives_os_key_and_classifiers(java_arch).is_some()
{
continue;
}
let path = st
.directories
.libraries_dir()
.join(d::get_path_from_artifact(&library.name)?);
if !path.try_exists()? {
return Ok(Some(path));
}
}

Ok(None)
}

fn missing_client_bytes(
st: &State,
version: &GameVersionInfo,
Expand Down
15 changes: 14 additions & 1 deletion packages/app-lib/src/launcher/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -846,7 +846,7 @@ pub async fn launch_minecraft(
}

let state = State::get().await?;
let _runtime_lease = state.content_store.runtime_cache_lock.read().await;
let mut runtime_lease = state.content_store.runtime_cache_lock.read().await;

let instance_path = get_instance_full_path(&instance.path).await?;

Expand Down Expand Up @@ -971,6 +971,19 @@ pub async fn launch_minecraft(
.as_error());
}

if let Some(path) = download::missing_runtime_file(
&state,
&version_info,
&java_version.architecture,
minecraft_updated,
)? {
tracing::info!(instance_id = %instance.id, path = %path.display(), "Restoring missing Minecraft runtime files before launch");
drop(runtime_lease);
install_minecraft_with_reporter(context, false, None).await?;
runtime_lease = state.content_store.runtime_cache_lock.read().await;
}
let _runtime_lease = runtime_lease;

let natives_dir = state.directories.version_natives_dir(&version_jar);
if !natives_dir.exists() {
io::create_dir_all(&natives_dir).await?;
Expand Down
21 changes: 14 additions & 7 deletions packages/app-lib/src/state/content_store/adapters/filesystem.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,16 @@ pub(crate) async fn writable_copy(
source: &Path,
destination: &Path,
) -> crate::Result<()> {
fs::copy(source, destination).await?;
let mut input = File::open(source).await?;
let permissions = input.metadata().await?.permissions();
let mut output = File::create(destination).await?;
tokio::io::copy(&mut input, &mut output).await?;
output.flush().await?;
fs::set_permissions(destination, permissions).await?;
make_writable(destination).await
}

/*
pub(crate) async fn try_reflink(
source: &Path,
destination: &Path,
Expand All @@ -51,8 +57,6 @@ pub(crate) async fn try_reflink(
Ok(true)
}
Err(error) => {
// Windows can wrap filesystem errors in an HRESULT. Unwrap it before
// deciding whether another storage method can work.
let error = match error.raw_os_error().map(|code| code as u32) {
Some(code)
if cfg!(windows) && code & 0xffff0000 == 0x80070000 =>
Expand Down Expand Up @@ -83,6 +87,7 @@ pub(crate) async fn try_reflink(
}
}
}
*/

async fn make_writable(destination: &Path) -> crate::Result<()> {
let mut permissions = fs::metadata(destination).await?.permissions();
Expand Down Expand Up @@ -121,13 +126,15 @@ pub(in crate::state::content_store) async fn move_instance_file(
Ok(())
}

#[cfg(windows)]
pub(crate) fn link_unavailable(error: &std::io::Error) -> bool {
matches!(
error.kind(),
std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::Unsupported
) || cfg!(windows) && matches!(error.raw_os_error(), Some(1 | 50 | 1314))
}

/*
pub(in crate::state::content_store) async fn try_hardlink(
source: &Path,
target: &Path,
Expand All @@ -145,6 +152,7 @@ pub(in crate::state::content_store) async fn try_hardlink(
Err(error) => Err(error.into()),
}
}
*/

pub(crate) async fn remove_instance_file(path: &Path) -> crate::Result<()> {
let metadata = fs::symlink_metadata(path).await?;
Expand Down Expand Up @@ -182,13 +190,12 @@ pub(crate) async fn remove_instance_file(path: &Path) -> crate::Result<()> {
Ok(())
}

/// Returns `None` when sharing is unavailable, without creating a full copy.
/// Background migration uses this to avoid duplicating existing files just to adopt them.
pub(crate) async fn try_shared_file(
source: &Path,
destination: &Path,
policy: FileStoragePolicy,
) -> crate::Result<Option<FileStorageKind>> {
/*
if try_reflink(source, destination).await? {
return Ok(Some(FileStorageKind::Reflink));
}
Expand All @@ -197,6 +204,8 @@ pub(crate) async fn try_shared_file(
{
return Ok(Some(FileStorageKind::Hardlink));
}
*/
let _ = (source, destination, policy);
Ok(None)
}

Expand All @@ -212,8 +221,6 @@ pub(crate) async fn create_content_file(
Ok(FileStorageKind::Copy)
}

/// Rejects linked parent directories because they can redirect an otherwise valid
/// relative path outside the instance or store.
pub(crate) async fn validate_parent_directories(
root: &Path,
path: &Path,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ use std::collections::HashSet;
use std::path::Path;
use tokio::fs::{self, ReadDir};

/// Visits objects in filesystem order so recovery can register each verified file
/// before inspecting the next one. A later I/O error does not discard that progress.
pub(in crate::state::content_store) struct UnregisteredFiles {
prefixes: ReadDir,
entries: Option<ReadDir>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ impl FileStamp {
}

impl VerifiedFiles {
/// Reuses a hash only while identity, size, write time and change time match.
/// Filesystems without the required metadata are always read in full.
pub(in crate::state::content_store) async fn hash_file(
&self,
path: &Path,
Expand Down
3 changes: 0 additions & 3 deletions packages/app-lib/src/state/content_store/commands/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,9 +154,6 @@ impl ContentStore {
.await
}

/// Removes unused downloads, starting with the oldest content, to meet the cache limit.
/// Installed content and files needed for rollback remain available. `purge_unused`
/// also removes recent unused downloads instead of keeping them for another install.
pub async fn cleanup(
&self,
state: &State,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,10 @@ pub(crate) struct PendingFileChange {
journal: FileChangeJournal,
pub(crate) stored_file: Option<StoredFileHandle>,
_previous_file_lease: Option<StoredFileHandle>,
/// The failed change needs no further recovery and has preserved or restored the original file.
/// Background migration can skip this file and continue with the remaining files.
pub(crate) safe_to_defer: bool,
}

impl ContentStore {
/// Saves the original file so a failed install, replacement, or removal can be undone.
///
/// Recover earlier changes first. Hold the instance and store locks until commit or
/// rollback so another content operation cannot invalidate the saved original.
pub(crate) async fn prepare_file_change(
&self,
instance: &Instance,
Expand Down Expand Up @@ -179,8 +173,6 @@ impl ContentStore {
})
}

/// Records an enable/disable rename so recovery can undo an interrupted toggle.
/// Uses the same recovery and locking requirements as `prepare_file_change`.
pub(crate) async fn prepare_file_move(
&self,
instance: &Instance,
Expand Down
Loading
Loading