Skip to content

Conversation

@BegoniaHe
Copy link
Contributor

@BegoniaHe BegoniaHe commented Jan 25, 2026

Summary by Sourcery

Refactor Java runtime handling into an asynchronous, modular subsystem with support for detection, compatibility selection, downloading, and per-instance overrides.

New Features:

  • Add a per-instance Java path override field to instance configuration for fine-grained JVM selection.
  • Introduce a new tauri command to detect all Java installations, keeping the existing detect_java command as a backward-compatible alias.
  • Add support for downloading and installing Java runtimes from the Adoptium API, including catalog fetching, version listing, and resumable downloads.

Enhancements:

  • Replace the previous monolithic Java module with a structured java namespace that separates detection, validation, provider integration, persistence, and priority resolution logic.
  • Make Java compatibility checks and detection functions asynchronous and integrate them into game start and Forge installation flows.
  • Centralize Java selection logic via a priority resolver that considers instance override, global config path, user-preferred path, and auto-detected installations in order.

- Split monolithic java.rs (1089 lines) into focused modules
  - detection: Java installation discovery
  - validation: Version validation and requirements checking
  - priority: Installation selection priority logic
  - provider: Java download provider trait
  - providers: Provider implementations (Adoptium)
  - persistence: Cache and catalog management
- Add java_path_override field to Instance struct for per-instance Java configuration
- Export JavaInstallation at core module level

This refactoring improves maintainability and prepares for multi-vendor Java provider support.

Reviewed-by: Claude Sonnet 4.5
Copilot AI review requested due to automatic review settings January 25, 2026 09:16
@vercel
Copy link

vercel bot commented Jan 25, 2026

@BegoniaHe is attempting to deploy a commit to the retrofor Team on Vercel.

A member of the Team first needs to authorize it.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Jan 25, 2026

Reviewer's Guide

Refactors Java handling into a new async, provider-based core::java module, adds richer Java detection/installation capabilities (via Adoptium), introduces per-instance Java overrides, and updates Tauri commands and game-start/install paths to use the new async APIs.

Sequence diagram for Java resolution on game start

sequenceDiagram
    actor User
    participant UI as TauriFrontend
    participant Cmd as start_game_command
    participant JP as JavaPriority
    participant JV as JavaValidation
    participant JPers as JavaPersistence
    participant JCore as CoreJava

    User->>UI: Click Play
    UI->>Cmd: Invoke start_game(instance_id)
    Cmd->>JP: resolve_java_for_launch(app_handle, instance_java_override, global_java_path, required_major_version, max_major_version)

    alt instance_java_override set and nonempty
        JP->>JV: check_java_installation(instance_java_path)
        JV-->>JP: Option_JavaInstallation
        alt valid and version compatible
            JP-->>Cmd: Some(JavaInstallation)
        else invalid or incompatible
            note over JP: Fallback to next priority
        end
    end

    alt no valid instance override
        alt global_java_path set and nonempty
            JP->>JV: check_java_installation(global_java_path)
            JV-->>JP: Option_JavaInstallation
            alt valid and version compatible
                JP-->>Cmd: Some(JavaInstallation)
            else invalid or incompatible
                note over JP: Fallback to preferred path
            end
        end
    end

    alt no valid global path
        JP->>JPers: get_preferred_java_path(app_handle)
        JPers-->>JP: Option_String
        alt preferred path exists
            JP->>JV: check_java_installation(preferred_path)
            JV-->>JP: Option_JavaInstallation
            alt valid and version compatible
                JP-->>Cmd: Some(JavaInstallation)
            else invalid or incompatible
                note over JP: Fallback to detection
            end
        end
    end

    alt no valid preferred path
        JP->>JCore: detect_all_java_installations(app_handle)
        JCore-->>JP: Vec_JavaInstallation
        JP-->>Cmd: Option_JavaInstallation (first compatible)
    end

    Cmd-->>UI: Success or error starting game
Loading

Class diagram for new core_java module and related types

classDiagram
    class JavaInstallation {
        +String path
        +String version
        +String arch
        +String vendor
        +String source
        +bool is_64bit
    }

    class ImageType {
        <<enum>>
        +Jre
        +Jdk
        +toString() String
    }

    class JavaReleaseInfo {
        +u32 major_version
        +String image_type
        +String version
        +String release_name
        +Option_String release_date
        +u64 file_size
        +Option_String checksum
        +String download_url
        +bool is_lts
        +bool is_available
        +String architecture
    }

    class JavaCatalog {
        +Vec_JavaReleaseInfo releases
        +Vec_u32 available_major_versions
        +Vec_u32 lts_versions
        +u64 cached_at
    }

    class JavaDownloadInfo {
        +String version
        +String release_name
        +String download_url
        +String file_name
        +u64 file_size
        +Option_String checksum
        +String image_type
    }

    class JavaConfig {
        +Vec_String user_defined_paths
        +Option_String preferred_java_path
        +u64 last_detection_time
    }

    class JavaProvider {
        <<interface>>
        +fetch_catalog(app_handle, force_refresh) Result_JavaCatalog_String
        +fetch_release(major_version, image_type) Result_JavaDownloadInfo_String
        +available_versions() Result_Vec_u32_String
        +provider_name() str
        +os_name() str
        +arch_name() str
        +install_prefix() str
    }

    class AdoptiumProvider {
        +new() AdoptiumProvider
        +fetch_catalog(app_handle, force_refresh) Result_JavaCatalog_String
        +fetch_release(major_version, image_type) Result_JavaDownloadInfo_String
        +available_versions() Result_Vec_u32_String
        +provider_name() str
        +os_name() str
        +arch_name() str
        +install_prefix() str
    }

    class DownloadQueue {
        +Vec_PendingJavaDownload pending_downloads
        +load(app_handle) DownloadQueue
        +save(app_handle) Result_void_String
        +add(pending) void
        +remove(major_version, image_type) void
    }

    class PendingJavaDownload {
        +u32 major_version
        +String image_type
        +String download_url
        +String file_name
        +u64 file_size
        +Option_String checksum
        +String install_path
        +u64 created_at
    }

    class JavaDetection {
        <<module>>
        +get_java_candidates() Vec_PathBuf
    }

    class JavaValidation {
        <<module>>
        +check_java_installation(path) Option_JavaInstallation
        +parse_version_string(output) Option_String
        +parse_java_version(version) u32
        +extract_architecture(output) String
        +extract_vendor(output) String
        +strip_unc_prefix(path) PathBuf
    }

    class JavaPersistence {
        <<module>>
        +load_java_config(app_handle) JavaConfig
        +save_java_config(app_handle, config) Result_void_String
        +add_user_defined_path(app_handle, path) Result_void_String
        +remove_user_defined_path(app_handle, path) Result_void_String
        +set_preferred_java_path(app_handle, path) Result_void_String
        +get_preferred_java_path(app_handle) Option_String
        +update_last_detection_time(app_handle) Result_void_String
    }

    class JavaPriority {
        <<module>>
        +resolve_java_for_launch(app_handle, instance_java_override, global_java_path, required_major_version, max_major_version) Option_JavaInstallation
    }

    class CoreJavaModule {
        <<module>>
        +get_java_install_dir(app_handle) PathBuf
        +load_cached_catalog(app_handle) Option_JavaCatalog
        +save_catalog_cache(app_handle, catalog) Result_void_String
        +clear_catalog_cache(app_handle) Result_void_String
        +fetch_java_catalog(app_handle, force_refresh) Result_JavaCatalog_String
        +fetch_java_release(major_version, image_type) Result_JavaDownloadInfo_String
        +fetch_available_versions() Result_Vec_u32_String
        +download_and_install_java(app_handle, major_version, image_type, custom_path) Result_JavaInstallation_String
        +detect_java_installations() Vec_JavaInstallation
        +get_recommended_java(required_major_version) Option_JavaInstallation
        +get_compatible_java(app_handle, required_major_version, max_major_version) Option_JavaInstallation
        +is_java_compatible(java_path, required_major_version, max_major_version) bool
        +detect_all_java_installations(app_handle) Vec_JavaInstallation
        +resume_pending_downloads(app_handle) Result_Vec_JavaInstallation_String
        +cancel_current_download() void
        +get_pending_downloads(app_handle) Vec_PendingJavaDownload
        +clear_pending_download(app_handle, major_version, image_type) Result_void_String
    }

    class Instance {
        +String id
        +String name
        +Option_String notes
        +Option_String mod_loader
        +Option_String mod_loader_version
        +Option_String java_path_override
    }

    JavaProvider <|.. AdoptiumProvider
    CoreJavaModule "1" o-- "many" JavaInstallation
    CoreJavaModule ..> JavaProvider
    CoreJavaModule ..> DownloadQueue
    CoreJavaModule ..> PendingJavaDownload
    CoreJavaModule ..> JavaDetection
    CoreJavaModule ..> JavaValidation
    CoreJavaModule ..> JavaPersistence
    CoreJavaModule ..> JavaPriority
    JavaPriority ..> JavaValidation
    JavaPriority ..> JavaPersistence
    JavaPriority ..> CoreJavaModule
    JavaDetection ..> JavaValidation
    DownloadQueue o-- PendingJavaDownload
    JavaPersistence o-- JavaConfig
    Instance --> JavaPriority
Loading

Flow diagram for download_and_install_java process

flowchart TD
    A[start download_and_install_java] --> B[Create AdoptiumProvider]
    B --> C["fetch_release(major_version, image_type)"]
    C --> D[Determine install_base and version_dir]
    D --> E[Create install_base directory]
    E --> F[Load DownloadQueue]
    F --> G[Add PendingJavaDownload and save queue]
    G --> H[Compute archive_path]
    H --> I{archive_path exists?}
    I -->|No| K[need_download = true]
    I -->|Yes| J[Verify checksum if available]
    J -->|Invalid or missing| K[need_download = true]
    J -->|Valid| L[need_download = false]

    K --> M[download_with_resume]
    L --> N[Skip download]
    M --> O[Emit java-download-progress status Extracting]
    N --> O[Emit java-download-progress status Extracting]

    O --> P[Remove existing version_dir if present]
    P --> Q[Create version_dir]
    Q --> R{Archive extension}
    R -->|.tar.gz or .tgz| S[extract_tar_gz to version_dir]
    R -->|.zip| T[extract_zip to version_dir and find_top_level_dir]
    R -->|other| U[Return error Unsupported archive format]

    S --> V[Remove archive file]
    T --> V[Remove archive file]

    V --> W[Compute java_home and java_bin path]
    W --> X{java_bin exists?}
    X -->|No| Y[Return error Java executable not found]
    X -->|Yes| Z[canonicalize java_bin and strip_unc_prefix]

    Z --> AA[validation::check_java_installation]
    AA --> AB{installation verified?}
    AB -->|No| AC[Return error Failed to verify Java installation]
    AB -->|Yes| AD[installation: JavaInstallation]

    AD --> AE[Remove entry from DownloadQueue]
    AE --> AF[Save DownloadQueue]
    AF --> AG[Emit java-download-progress status Completed]
    AG --> AH[Return JavaInstallation]

    U --> AI[end]
    Y --> AI[end]
    AC --> AI[end]
    AH --> AI[end]
Loading

File-Level Changes

Change Details Files
Update game start, Forge install, and Java-related Tauri commands to use new async Java APIs.
  • Await java::is_java_compatible and java::get_compatible_java in start_game to avoid blocking and align with async implementations.
  • Adjust Java auto-detection for Forge install to await detect_all_java_installations.
  • Expose a new detect_all_java_installations Tauri command and keep detect_java as a backward-compatible alias that now also awaits detection.
  • Make get_recommended_java Tauri command async over the new async core::java function.
src-tauri/src/main.rs
Introduce per-instance Java path override support in the instance model.
  • Extend Instance struct with an optional java_path_override field for instance-level Java configuration.
  • Initialize java_path_override to None in the default instance creation logic.
src-tauri/src/core/instance.rs
Refactor Java logic into a dedicated core::java module with detection, validation, installation, provider abstraction, and priority resolution.
  • Create core::java module (with submodules) that replaces the previous flat core/java.rs implementation, and re-export JavaInstallation from core::mod.
  • Implement JavaInstallation, ImageType, JavaCatalog, JavaReleaseInfo, JavaDownloadInfo and cache handling for Java release metadata.
  • Add async detection of Java installations across system locations and app-managed Java install directory, including version parsing and compatibility checks.
  • Implement AdoptiumProvider and JavaProvider trait for fetching catalogs, releases, and available versions from the Adoptium API.
  • Add download_and_install_java, resume_pending_downloads, and related helpers that integrate with the existing downloader and emit progress events.
  • Introduce persistence helpers for Java-specific config (user-defined paths, preferred path, last detection time).
  • Add priority::resolve_java_for_launch to resolve the effective Java executable based on instance override, global config, user preference, and auto-detected installations.
src-tauri/src/core/mod.rs
src-tauri/src/core/java/mod.rs
src-tauri/src/core/java/detection.rs
src-tauri/src/core/java/validation.rs
src-tauri/src/core/java/persistence.rs
src-tauri/src/core/java/priority.rs
src-tauri/src/core/java/provider.rs
src-tauri/src/core/java/providers/mod.rs
src-tauri/src/core/java/providers/adoptium.rs
src-tauri/src/core/java.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 4 issues, and left some high level feedback:

  • There are multiple implementations of strip_unc_prefix (in core/java/detection.rs, core/java/validation.rs, and implicitly in core/java/mod.rs usage) with overlapping behavior; consider consolidating this into a single shared helper to avoid duplication and potential divergence.
  • The JAVA_CHECK_TIMEOUT constant in core/java/validation.rs is defined but never used; either wire it into the process invocation (e.g., by enforcing a timeout on java -version) or remove it to keep the module minimal.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- There are multiple implementations of `strip_unc_prefix` (in `core/java/detection.rs`, `core/java/validation.rs`, and implicitly in `core/java/mod.rs` usage) with overlapping behavior; consider consolidating this into a single shared helper to avoid duplication and potential divergence.
- The `JAVA_CHECK_TIMEOUT` constant in `core/java/validation.rs` is defined but never used; either wire it into the process invocation (e.g., by enforcing a timeout on `java -version`) or remove it to keep the module minimal.

## Individual Comments

### Comment 1
<location> `src-tauri/src/core/java/validation.rs:10` </location>
<code_context>
+
+use super::JavaInstallation;
+
+const JAVA_CHECK_TIMEOUT: Duration = Duration::from_secs(5);
+
+pub fn strip_unc_prefix(path: PathBuf) -> PathBuf {
</code_context>

<issue_to_address>
**issue (bug_risk):** Timeout constant is defined but not used, and `check_java_installation_blocking` may hang on broken Java binaries.

`JAVA_CHECK_TIMEOUT` implies the check should be time-bounded, but `Command::output` runs indefinitely, so a hanging `java -version` could block detection. Either apply this timeout to the command execution (e.g., via `wait_timeout`/a blocking task with `tokio::time::timeout`) or remove the constant to avoid confusion.
</issue_to_address>

### Comment 2
<location> `src-tauri/src/core/java/detection.rs:29` </location>
<code_context>
+    }
+}
+
+fn run_which_command_with_timeout() -> Option<String> {
+    let mut cmd = Command::new(if cfg!(windows) { "where" } else { "which" });
+    cmd.arg("java");
</code_context>

<issue_to_address>
**suggestion:** Function name implies a timeout but the implementation does not enforce one.

Given that it only calls `Command::output`, the current behavior doesn’t actually apply a timeout, which can be confusing alongside the timeout constant in `validation.rs`. Either implement an actual timeout here or rename the function to better match what it does.

Suggested implementation:

```rust
fn run_which_command_with_timeout() -> Option<String> {
    use std::io::Read;
    use std::process::Stdio;
    use std::thread::sleep;
    use std::time::{Duration, Instant};

    let mut cmd = Command::new(if cfg!(windows) { "where" } else { "which" });
    cmd.arg("java");
    #[cfg(target_os = "windows")]
    // Hide the console window on Windows
    cmd.creation_flags(0x08000000);

    // We only care about stdout from the command
    cmd.stdout(Stdio::piped());

    // TODO: consider sharing a timeout constant with validation.rs if appropriate
    let timeout = Duration::from_secs(2);
    let start = Instant::now();

    let mut child = match cmd.spawn() {
        Ok(child) => child,
        Err(_) => return None,
    };

    loop {
        match child.try_wait() {
            Ok(Some(status)) => {
                if status.success() {
                    let mut output = String::new();
                    if let Some(mut stdout) = child.stdout.take() {
                        let _ = stdout.read_to_string(&mut output);
                    }
                    return Some(output);
                } else {
                    let _ = child.wait(); // Ensure we reap the child
                    return None;
                }
            }
            Ok(None) => {
                if start.elapsed() >= timeout {
                    let _ = child.kill();
                    let _ = child.wait(); // Reap the killed child
                    return None;
                }
                sleep(Duration::from_millis(50));
            }
            Err(_) => {
                let _ = child.kill();
                let _ = child.wait();
                return None;
            }
        }
    }
}

```

1. If you prefer to reuse an existing timeout constant from `validation.rs`, replace the hard-coded `Duration::from_secs(2)` with that constant (or a derived `Duration`) and move the `use std::time::{Duration, Instant};` import accordingly.
2. If your module already has `use` statements for `std::io::Read`, `std::process::Stdio`, `std::thread::sleep`, or `std::time::{Duration, Instant}`, you can remove the inner `use` lines from the function and keep them at the module level to match your existing style.
</issue_to_address>

### Comment 3
<location> `src-tauri/src/core/java/detection.rs:7` </location>
<code_context>
+#[cfg(target_os = "windows")]
+use std::os::windows::process::CommandExt;
+
+pub fn strip_unc_prefix(path: PathBuf) -> PathBuf {
+    #[cfg(target_os = "windows")]
+    {
</code_context>

<issue_to_address>
**suggestion:** There are two separate `strip_unc_prefix` implementations; centralizing them would reduce duplication.

`strip_unc_prefix` exists in both `detection.rs` and `validation.rs` with the same behavior. Prefer a single shared implementation (for example in `validation` or the top-level `java` module) and call it from both to avoid divergence and ease maintenance.

Suggested implementation:

```rust
use std::path::PathBuf;
use std::process::Command;

#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;

// Reuse shared implementation from the parent `java` module.
use super::strip_unc_prefix;

pub fn find_sdkman_java() -> Option<PathBuf> {

```

To complete the refactor and actually centralize `strip_unc_prefix`:

1. Create a single shared implementation, for example in `src-tauri/src/core/java/mod.rs`:
   - Add:
     ```rust
     use std::path::PathBuf;

     pub fn strip_unc_prefix(path: PathBuf) -> PathBuf {
         #[cfg(target_os = "windows")]
         {
             // Remove the UNC prefix (\\?\) from Windows paths
             let s = path.to_string_lossy().to_string();
             if s.starts_with(r"\\?\") {
                 return PathBuf::from(&s[4..]);
             }
         }
         path
     }
     ```
   - Ensure you dont duplicate the function body elsewhere; this becomes the single source of truth.

2. In `src-tauri/src/core/java/validation.rs`, remove its local `strip_unc_prefix` definition and import the shared one instead:
   - Add `use super::strip_unc_prefix;` (or an appropriate path like `use crate::core::java::strip_unc_prefix;` depending on how the module is declared).
   - Update any calls to `strip_unc_prefix` to use this imported function (no signature changes needed if behavior remains the same).

3. If `strip_unc_prefix` needs to be available outside the `java` module, keep it `pub` in `mod.rs`; otherwise you can make it `pub(crate)` or `pub(super)` and adjust imports accordingly.

These additional changes ensure both `detection.rs` and `validation.rs` use the same implementation, eliminating duplication and the risk of divergence.
</issue_to_address>

### Comment 4
<location> `src-tauri/src/core/java/providers/adoptium.rs:96-13` </location>
<code_context>
+        for major_version in &available.available_releases {
</code_context>

<issue_to_address>
**suggestion (performance):** Catalog fetching performs many HTTP requests sequentially; this could be slow when many versions are available.

Each major-version/image-type pair triggers its own `GET` in a nested loop, all awaited one by one. On slower networks or with many `available_releases`, this will noticeably delay catalog loading. Consider using `futures::stream` / `join_all` with a bounded concurrency level so multiple requests can be fetched in parallel.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Resolved conflicts:
- Merged Instance struct fields: kept java_path_override from refactor and added jvm_args_override & memory_override from upstream
- Removed upstream's java.rs as it has been refactored into java/ module directory
- Updated duplicate_instance to include all new fields

Reviewed-by: Claude Sonnet 4.5
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the Java detection system from a monolithic 1087-line java.rs file into a modular architecture with separate concerns for detection, validation, persistence, and provider abstraction. The refactoring converts synchronous Java detection functions to async operations and introduces a provider pattern to support multiple Java distribution sources.

Changes:

  • Splits core/java.rs into modular structure: detection.rs, validation.rs, persistence.rs, priority.rs, provider.rs, and providers/adoptium.rs
  • Converts all Java-related functions to async with proper .await usage throughout main.rs
  • Adds new JavaInstallation fields (arch, vendor, source) for richer Java information
  • Introduces instance-level Java path override field in Instance struct
  • Creates provider trait pattern for future support of multiple Java download sources

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
src-tauri/src/main.rs Adds .await to all Java function calls, adds new detect_all_java_installations command and backward-compatible detect_java alias
src-tauri/src/core/mod.rs Re-exports JavaInstallation at core module level
src-tauri/src/core/java/validation.rs New module for Java validation logic with async check_java_installation, version/arch/vendor parsing
src-tauri/src/core/java/providers/mod.rs Module declaration for provider implementations
src-tauri/src/core/java/providers/adoptium.rs Adoptium provider implementation with trait methods for catalog/release fetching
src-tauri/src/core/java/provider.rs Trait definition for Java download providers following SOLID principles
src-tauri/src/core/java/priority.rs Java resolution priority logic (instance → global → preferred → auto-detect)
src-tauri/src/core/java/persistence.rs Java configuration persistence with user-defined paths and preferred Java tracking
src-tauri/src/core/java/mod.rs Main Java module consolidating detection, catalog, download, and installation logic
src-tauri/src/core/java/detection.rs System Java detection across Linux/macOS/Windows paths and environment variables
src-tauri/src/core/java.rs Complete removal of monolithic 1087-line file (replaced by modular structure)
src-tauri/src/core/instance.rs Adds java_path_override field for per-instance Java configuration

Comment on lines 3 to 11
use std::time::Duration;

#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;

use super::JavaInstallation;

const JAVA_CHECK_TIMEOUT: Duration = Duration::from_secs(5);

Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The constant JAVA_CHECK_TIMEOUT is defined but never used in this file. If this was intended for implementing timeout functionality in check_java_installation, it should be utilized. Otherwise, it should be removed to avoid confusion.

Suggested change
use std::time::Duration;
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
use super::JavaInstallation;
const JAVA_CHECK_TIMEOUT: Duration = Duration::from_secs(5);
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
use super::JavaInstallation;

Copilot uses AI. Check for mistakes.
Comment on lines 51 to 52
// Only attempt 'which' or 'where' if is not Windows
// CAUTION: linux 'which' may return symlinks, so we need to canonicalize later
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "Only attempt 'which' or 'where' if is not Windows" is inaccurate. The code actually runs the command on all platforms, using "where" on Windows and "which" on other systems (line 30). The comment should be updated or removed to avoid confusion.

Suggested change
// Only attempt 'which' or 'where' if is not Windows
// CAUTION: linux 'which' may return symlinks, so we need to canonicalize later
// Attempt to locate 'java' using 'where' on Windows or 'which' on Unix-like systems
// CAUTION: on Linux and other Unix-like systems, 'which' may return symlinks, so we canonicalize later

Copilot uses AI. Check for mistakes.
}
}

let home = std::env::var("HOME").unwrap_or_default();
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable home is declared but never used. The find_sdkman_java() function already retrieves and uses the HOME environment variable internally, so this declaration is redundant and should be removed.

Suggested change
let home = std::env::var("HOME").unwrap_or_default();

Copilot uses AI. Check for mistakes.
Comment on lines 373 to 395
}
}

pub async fn is_java_compatible(
java_path: &str,
required_major_version: Option<u64>,
max_major_version: Option<u32>,
) -> bool {
let java_path_buf = PathBuf::from(java_path);
if let Some(java) = validation::check_java_installation(&java_path_buf).await {
let major = validation::parse_java_version(&java.version);
let meets_min = if let Some(required) = required_major_version {
major >= required as u32
} else {
true
};
let meets_max = if let Some(max_version) = max_major_version {
major <= max_version
} else {
true
};
meets_min && meets_max
} else {
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The is_version_compatible function duplicates the same logic found in priority.rs:52-72. Both implementations perform identical version compatibility checks. Consider consolidating this logic into a single location, possibly by making priority::is_version_compatible public and reusing it here, or by extracting it to the validation module.

Copilot uses AI. Check for mistakes.
Comment on lines 13 to 20
#[cfg(target_os = "windows")]
{
let s = path.to_string_lossy().to_string();
if s.starts_with(r"\\?\") {
return PathBuf::from(&s[4..]);
}
}
path
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The strip_unc_prefix function is duplicated in both validation.rs (lines 12-21) and detection.rs (lines 7-17). This violates the DRY (Don't Repeat Yourself) principle and creates maintenance overhead. Consider extracting this function to a shared utility module or making one module depend on the other's implementation.

Suggested change
#[cfg(target_os = "windows")]
{
let s = path.to_string_lossy().to_string();
if s.starts_with(r"\\?\") {
return PathBuf::from(&s[4..]);
}
}
path
super::detection::strip_unc_prefix(path)

Copilot uses AI. Check for mistakes.
Comment on lines 29 to 46
fn run_which_command_with_timeout() -> Option<String> {
let mut cmd = Command::new(if cfg!(windows) { "where" } else { "which" });
cmd.arg("java");
#[cfg(target_os = "windows")]
// Hide the console window on Windows
cmd.creation_flags(0x08000000);

match cmd.output() {
Ok(output) => {
if output.status.success() {
Some(String::from_utf8_lossy(&output.stdout).to_string())
} else {
None
}
}
Err(_) => None,
}
}
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function name run_which_command_with_timeout suggests timeout handling, but the implementation doesn't actually implement any timeout mechanism. The Command::output() call blocks indefinitely if the command hangs. Consider either:

  1. Renaming the function to remove the misleading "_with_timeout" suffix, or
  2. Implementing actual timeout functionality using tokio::time::timeout or a similar mechanism

This could be important for robustness if a Java installation is corrupted or the system is under heavy load.

Copilot uses AI. Check for mistakes.
cached_at: now,
};

let _ = super::super::save_catalog_cache(app_handle, &catalog);
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The result of save_catalog_cache is intentionally ignored with let _ =. While this might be acceptable if cache saving failure shouldn't block the operation, consider at least logging the error for debugging purposes. A failed cache write could indicate disk space issues or permission problems that administrators should be aware of.

Suggested change
let _ = super::super::save_catalog_cache(app_handle, &catalog);
if let Err(e) = super::super::save_catalog_cache(app_handle, &catalog) {
eprintln!("Failed to save Java catalog cache: {}", e);
}

Copilot uses AI. Check for mistakes.
Comment on lines 13 to 54
#[cfg(target_os = "windows")]
{
let s = path.to_string_lossy().to_string();
if s.starts_with(r"\\?\") {
return PathBuf::from(&s[4..]);
}
}
path
}

pub async fn check_java_installation(path: &PathBuf) -> Option<JavaInstallation> {
let path = path.clone();
tokio::task::spawn_blocking(move || check_java_installation_blocking(&path))
.await
.ok()?
}

fn check_java_installation_blocking(path: &PathBuf) -> Option<JavaInstallation> {
let mut cmd = Command::new(path);
cmd.arg("-version");
#[cfg(target_os = "windows")]
cmd.creation_flags(0x08000000);

let output = cmd.output().ok()?;

let version_output = String::from_utf8_lossy(&output.stderr);

let version = parse_version_string(&version_output)?;
let arch = extract_architecture(&version_output);
let vendor = extract_vendor(&version_output);
let is_64bit = version_output.contains("64-Bit");

Some(JavaInstallation {
path: path.to_string_lossy().to_string(),
version,
arch,
vendor,
source: "system".to_string(),
is_64bit,
})
}

Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file uses tabs for indentation, which is inconsistent with the rest of the Rust codebase that uses 4 spaces (see files like src-tauri/src/core/fabric.rs, src-tauri/src/core/auth.rs, src-tauri/src/main.rs). The project's pre-commit configuration includes cargo fmt which should enforce consistent formatting. Run cargo fmt to automatically fix the indentation to match project conventions.

Suggested change
#[cfg(target_os = "windows")]
{
let s = path.to_string_lossy().to_string();
if s.starts_with(r"\\?\") {
return PathBuf::from(&s[4..]);
}
}
path
}
pub async fn check_java_installation(path: &PathBuf) -> Option<JavaInstallation> {
let path = path.clone();
tokio::task::spawn_blocking(move || check_java_installation_blocking(&path))
.await
.ok()?
}
fn check_java_installation_blocking(path: &PathBuf) -> Option<JavaInstallation> {
let mut cmd = Command::new(path);
cmd.arg("-version");
#[cfg(target_os = "windows")]
cmd.creation_flags(0x08000000);
let output = cmd.output().ok()?;
let version_output = String::from_utf8_lossy(&output.stderr);
let version = parse_version_string(&version_output)?;
let arch = extract_architecture(&version_output);
let vendor = extract_vendor(&version_output);
let is_64bit = version_output.contains("64-Bit");
Some(JavaInstallation {
path: path.to_string_lossy().to_string(),
version,
arch,
vendor,
source: "system".to_string(),
is_64bit,
})
}
#[cfg(target_os = "windows")]
{
let s = path.to_string_lossy().to_string();
if s.starts_with(r"\\?\") {
return PathBuf::from(&s[4..]);
}
}
path
}
pub async fn check_java_installation(path: &PathBuf) -> Option<JavaInstallation> {
let path = path.clone();
tokio::task::spawn_blocking(move || {
check_java_installation_blocking(&path)
})
.await
.ok()?
}
fn check_java_installation_blocking(path: &PathBuf) -> Option<JavaInstallation> {
let mut cmd = Command::new(path);
cmd.arg("-version");
#[cfg(target_os = "windows")]
cmd.creation_flags(0x08000000);
let output = cmd.output().ok()?;
let version_output = String::from_utf8_lossy(&output.stderr);
let version = parse_version_string(&version_output)?;
let arch = extract_architecture(&version_output);
let vendor = extract_vendor(&version_output);
let is_64bit = version_output.contains("64-Bit");
Some(JavaInstallation {
path: path.to_string_lossy().to_string(),
version,
arch,
vendor,
source: "system".to_string(),
is_64bit,
})

Copilot uses AI. Check for mistakes.
Comment on lines 309 to 331
}
}

pub async fn detect_java_installations() -> Vec<JavaInstallation> {
let mut installations = Vec::new();
let candidates = detection::get_java_candidates();

for candidate in candidates {
if let Some(java) = validation::check_java_installation(&candidate).await {
if !installations
.iter()
.any(|j: &JavaInstallation| j.path == java.path)
{
installations.push(java);
}
}
}

installations.sort_by(|a, b| {
let v_a = validation::parse_java_version(&a.version);
let v_b = validation::parse_java_version(&b.version);
v_b.cmp(&v_a)
});
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user_defined_paths field in JavaConfig is not being utilized during Java detection. The detect_java_installations function only checks system-default paths through detection::get_java_candidates(), but doesn't include user-defined paths that may have been added via persistence::add_user_defined_path. This means users cannot add custom Java installation paths, making the persistence functionality incomplete.

Consider adding code to check user_defined_paths from the JavaConfig and validate those paths before adding them to the installations list.

Copilot uses AI. Check for mistakes.
Comment on lines 74 to 76
if let Some(cached) = super::super::load_cached_catalog(app_handle) {
return Ok(cached);
}
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cache access path super::super::load_cached_catalog(app_handle) uses a fragile double-parent reference. If the module structure changes (e.g., moving providers to a different location), this will break. Consider using an absolute path like crate::core::java::load_cached_catalog(app_handle) or re-exporting the cache functions at a stable location for better maintainability.

Copilot uses AI. Check for mistakes.
- Centralize strip_unc_prefix into java/mod.rs to eliminate duplication across detection.rs and validation.rs
- Remove unused JAVA_CHECK_TIMEOUT constant from validation.rs
- Implement actual timeout mechanism in run_which_command_with_timeout() using try_wait() loop
- Parallelize Adoptium API requests for better catalog fetch performance

Fixes:
- Multiple strip_unc_prefix implementations consolidated
- Timeout constant now properly enforced in which/where command execution
- Catalog fetching now uses concurrent tokio::spawn tasks instead of sequential await

Reviewed-by: Claude Sonnet 4.5
- Add #[allow(dead_code)] attributes to utility functions
- Improve 64-bit detection with case-insensitive check
- Support aarch64 architecture in bitness detection
- Add TODO for future vendor expansion

Reviewed-by: Claude Sonnet 4.5
Replace potentially panicking unwrap() call with expect() that includes a descriptive error message to aid debugging if the edge case occurs.

Reviewed-by: Claude Sonnet 4.5
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 14 comments.

Comment on lines +54 to +92

#[allow(dead_code)]
pub fn add_user_defined_path(app_handle: &AppHandle, path: String) -> Result<(), String> {
let mut config = load_java_config(app_handle);
if !config.user_defined_paths.contains(&path) {
config.user_defined_paths.push(path);
}
save_java_config(app_handle, &config)
}

#[allow(dead_code)]
pub fn remove_user_defined_path(app_handle: &AppHandle, path: &str) -> Result<(), String> {
let mut config = load_java_config(app_handle);
config.user_defined_paths.retain(|p| p != path);
save_java_config(app_handle, &config)
}

#[allow(dead_code)]
pub fn set_preferred_java_path(app_handle: &AppHandle, path: Option<String>) -> Result<(), String> {
let mut config = load_java_config(app_handle);
config.preferred_java_path = path;
save_java_config(app_handle, &config)
}

#[allow(dead_code)]
pub fn get_preferred_java_path(app_handle: &AppHandle) -> Option<String> {
let config = load_java_config(app_handle);
config.preferred_java_path
}

#[allow(dead_code)]
pub fn update_last_detection_time(app_handle: &AppHandle) -> Result<(), String> {
let mut config = load_java_config(app_handle);
config.last_detection_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
save_java_config(app_handle, &config)
}
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple persistence functions (add_user_defined_path, remove_user_defined_path, set_preferred_java_path, update_last_detection_time) are marked with #[allow(dead_code)] and are not used anywhere in the codebase. These appear to be part of a planned feature for user-managed Java paths that isn't fully implemented yet. The JavaConfig struct fields (user_defined_paths, preferred_java_path, last_detection_time) are defined but never populated. Consider either implementing the full feature or removing this unused infrastructure to reduce maintenance burden.

Suggested change
#[allow(dead_code)]
pub fn add_user_defined_path(app_handle: &AppHandle, path: String) -> Result<(), String> {
let mut config = load_java_config(app_handle);
if !config.user_defined_paths.contains(&path) {
config.user_defined_paths.push(path);
}
save_java_config(app_handle, &config)
}
#[allow(dead_code)]
pub fn remove_user_defined_path(app_handle: &AppHandle, path: &str) -> Result<(), String> {
let mut config = load_java_config(app_handle);
config.user_defined_paths.retain(|p| p != path);
save_java_config(app_handle, &config)
}
#[allow(dead_code)]
pub fn set_preferred_java_path(app_handle: &AppHandle, path: Option<String>) -> Result<(), String> {
let mut config = load_java_config(app_handle);
config.preferred_java_path = path;
save_java_config(app_handle, &config)
}
#[allow(dead_code)]
pub fn get_preferred_java_path(app_handle: &AppHandle) -> Option<String> {
let config = load_java_config(app_handle);
config.preferred_java_path
}
#[allow(dead_code)]
pub fn update_last_detection_time(app_handle: &AppHandle) -> Result<(), String> {
let mut config = load_java_config(app_handle);
config.last_detection_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
save_java_config(app_handle, &config)
}

Copilot uses AI. Check for mistakes.
Comment on lines 24 to 63
fn run_which_command_with_timeout() -> Option<String> {
let mut cmd = Command::new(if cfg!(windows) { "where" } else { "which" });
cmd.arg("java");
#[cfg(target_os = "windows")]
cmd.creation_flags(0x08000000);
cmd.stdout(Stdio::piped());

let start = Instant::now();
let mut child = cmd.spawn().ok()?;

loop {
match child.try_wait() {
Ok(Some(status)) => {
if status.success() {
let mut output = String::new();
if let Some(mut stdout) = child.stdout.take() {
let _ = stdout.read_to_string(&mut output);
}
return Some(output);
} else {
let _ = child.wait();
return None;
}
}
Ok(None) => {
if start.elapsed() >= WHICH_TIMEOUT {
let _ = child.kill();
let _ = child.wait();
return None;
}
sleep(Duration::from_millis(50));
}
Err(_) => {
let _ = child.kill();
let _ = child.wait();
return None;
}
}
}
}
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The run_which_command_with_timeout function uses std::thread::sleep in a polling loop, blocking the thread for 50ms intervals. Since this is called from the synchronous get_java_candidates function which is then called from async detect_java_installations, this blocking sleep could impact performance during Java detection. Consider using tokio::time::sleep and making this function async, or restructure the timeout mechanism to avoid blocking waits in an async context.

Copilot uses AI. Check for mistakes.
Comment on lines 9 to 15
pub async fn check_java_installation(path: &PathBuf) -> Option<JavaInstallation> {
let path = path.clone();
tokio::task::spawn_blocking(move || check_java_installation_blocking(&path))
.await
.ok()?
}

Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path parameter is cloned before passing to spawn_blocking. Since PathBuf implements Clone and the parameter is already a reference, consider taking ownership of the PathBuf in the function signature instead of cloning a reference. This would avoid an unnecessary clone operation and make the API more ergonomic.

Suggested change
pub async fn check_java_installation(path: &PathBuf) -> Option<JavaInstallation> {
let path = path.clone();
tokio::task::spawn_blocking(move || check_java_installation_blocking(&path))
.await
.ok()?
}
pub async fn check_java_installation(path: PathBuf) -> Option<JavaInstallation> {
tokio::task::spawn_blocking(move || check_java_installation_blocking(&path))
.await
.ok()?
}

Copilot uses AI. Check for mistakes.
pub notes: Option<String>, // 备注(可选)
pub mod_loader: Option<String>, // 模组加载器类型:"fabric", "forge", "vanilla"
pub mod_loader_version: Option<String>, // 模组加载器版本
pub java_path_override: Option<String>, // 实例级Java路径覆盖(可选)
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The java_path_override field is added to the Instance struct but is not being used in the start_game function. The game launch logic (lines 203-288 in main.rs) only checks config.java_path and doesn't consider instance-level Java overrides. The priority resolution logic should integrate priority::resolve_java_for_launch which was designed to handle instance overrides, or alternatively, the start_game function needs to be updated to read and use the instance.java_path_override field before falling back to config.java_path.

Copilot uses AI. Check for mistakes.
}
}

let home = std::env::var("HOME").unwrap_or_default();
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable 'home' is assigned but never used in this scope. It appears to be a leftover from the refactoring where SDKMAN detection was moved to a separate function (find_sdkman_java). This unused variable should be removed.

Suggested change
let home = std::env::var("HOME").unwrap_or_default();

Copilot uses AI. Check for mistakes.
Comment on lines 174 to 175
if let Ok(Some(release)) = task.await {
releases.push(release);
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Task join errors are silently ignored with the pattern 'if let Ok(Some(release))'. If a task panics or fails to join, the error is discarded without logging. This could make debugging difficult if parallel fetches fail unexpectedly. Consider logging task join errors (Err case from task.await) to help diagnose issues during catalog fetching, especially since this involves multiple concurrent network requests.

Suggested change
if let Ok(Some(release)) = task.await {
releases.push(release);
match task.await {
Ok(Some(release)) => {
releases.push(release);
}
Ok(None) => {
// No release for this task; nothing to collect.
}
Err(e) => {
eprintln!("AdoptiumProvider::fetch_catalog task join error: {:?}", e);
}

Copilot uses AI. Check for mistakes.
Comment on lines 388 to 410
pub async fn is_java_compatible(
java_path: &str,
required_major_version: Option<u64>,
max_major_version: Option<u32>,
) -> bool {
let java_path_buf = PathBuf::from(java_path);
if let Some(java) = validation::check_java_installation(&java_path_buf).await {
let major = validation::parse_java_version(&java.version);
let meets_min = if let Some(required) = required_major_version {
major >= required as u32
} else {
true
};
let meets_max = if let Some(max_version) = max_major_version {
major <= max_version
} else {
true
};
meets_min && meets_max
} else {
false
}
}
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version compatibility checking logic is duplicated between priority::is_version_compatible (lines 54-74 in priority.rs) and this function. This creates maintenance burden as any changes to compatibility logic need to be applied in both places. Consider extracting this logic into a shared helper function in the validation module, or having both functions delegate to priority::is_version_compatible.

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +39
pub struct JavaInstallation {
pub path: String,
pub version: String,
pub arch: String,
pub vendor: String,
pub source: String,
pub is_64bit: bool,
}
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JavaInstallation struct has been updated to include new fields (arch, vendor, source), but the TypeScript interface at packages/ui/src/types/index.ts (lines 86-90) only defines the old fields (path, version, is_64bit). This will cause runtime errors when the frontend tries to deserialize JavaInstallation objects that include the new fields. The TypeScript interface should be updated to match the new Rust struct definition.

Copilot uses AI. Check for mistakes.
Comment on lines 361 to 386
pub async fn get_compatible_java(
app_handle: &AppHandle,
required_major_version: Option<u64>,
max_major_version: Option<u32>,
) -> Option<JavaInstallation> {
let installations = detect_all_java_installations(app_handle).await;

if let Some(max_version) = max_major_version {
installations.into_iter().find(|java| {
let major = validation::parse_java_version(&java.version);
let meets_min = if let Some(required) = required_major_version {
major >= required as u32
} else {
true
};
meets_min && major <= max_version
})
} else if let Some(required) = required_major_version {
installations.into_iter().find(|java| {
let major = validation::parse_java_version(&java.version);
major >= required as u32
})
} else {
installations.into_iter().next()
}
}
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function duplicates the version compatibility checking logic that also exists in is_java_compatible (lines 388-410) and priority::is_version_compatible. The nested if-else branches for checking version bounds appear in multiple places. Consider consolidating this into a single shared compatibility check function to reduce code duplication and improve maintainability.

Copilot uses AI. Check for mistakes.
Comment on lines 7 to 51
#[allow(dead_code)]
pub async fn resolve_java_for_launch(
app_handle: &AppHandle,
instance_java_override: Option<&str>,
global_java_path: Option<&str>,
required_major_version: Option<u64>,
max_major_version: Option<u32>,
) -> Option<JavaInstallation> {
if let Some(override_path) = instance_java_override {
if !override_path.is_empty() {
let path_buf = std::path::PathBuf::from(override_path);
if let Some(java) = validation::check_java_installation(&path_buf).await {
if is_version_compatible(&java, required_major_version, max_major_version) {
return Some(java);
}
}
}
}

if let Some(global_path) = global_java_path {
if !global_path.is_empty() {
let path_buf = std::path::PathBuf::from(global_path);
if let Some(java) = validation::check_java_installation(&path_buf).await {
if is_version_compatible(&java, required_major_version, max_major_version) {
return Some(java);
}
}
}
}

let preferred = persistence::get_preferred_java_path(app_handle);
if let Some(pref_path) = preferred {
let path_buf = std::path::PathBuf::from(&pref_path);
if let Some(java) = validation::check_java_installation(&path_buf).await {
if is_version_compatible(&java, required_major_version, max_major_version) {
return Some(java);
}
}
}

let installations = super::detect_all_java_installations(app_handle).await;
installations
.into_iter()
.find(|java| is_version_compatible(java, required_major_version, max_major_version))
}
Copy link

Copilot AI Jan 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolve_java_for_launch function is marked with #[allow(dead_code)] and is never called. This suggests the priority resolution system described in the PR (instance override → global config → user-preferred → auto-detected) is not actually integrated into the game launch flow. If this is work-in-progress functionality, it should be noted in code comments or the PR description. Otherwise, this function should either be integrated or removed.

Copilot uses AI. Check for mistakes.
@HsiangNianian
Copy link
Member

I will review this PR within the next week Thank you very much for your contribution

@HsiangNianian
Copy link
Member

@SourceryAI title

@sourcery-ai sourcery-ai bot changed the title Refactor/java detect Refactor Java runtime to async modular system with Adoptium support Jan 26, 2026
@fu050409
Copy link
Contributor

lgtm, @HsiangNianian take a look?

@HsiangNianian
Copy link
Member

HsiangNianian commented Jan 26, 2026

lgtm, @HsiangNianian take a look?

should test first

function

  • find java in correctly path sets
    platform

    • macos
    • linux
    • windows
  • download java in correctly path
    platform

    • macos
    • linux
    • windows
  • select java correctly
    platform

    • macos
    • linux
    • windows

@BegoniaHe
Copy link
Contributor Author

To complete the select java correctly (user customized java path) intergration test, we need an updated frontend in java download page @fu050409

@BegoniaHe
Copy link
Contributor Author

test functionality

  • find java in correctly path sets
    • macos
    • linux
    • windows
image
  • download java in correctly path

    • macos
    • linux
    • windows

@fu050409 @HsiangNianian Need update listeners and event name to display the download progress correctly

Though the path is correct and works properly when launching

Path
----
C:\Users\begonia\AppData\Roaming\com.dropout.launcher\java

PSPath            : Microsoft.PowerShell.Core\FileSystem::C:\Users\begonia\AppData\Roaming\com.dropout.launcher\java\te
                    murin-21-jdk
PSParentPath      : Microsoft.PowerShell.Core\FileSystem::C:\Users\begonia\AppData\Roaming\com.dropout.launcher\java
PSChildName       : temurin-21-jdk
PSDrive           : C
PSProvider        : Microsoft.PowerShell.Core\FileSystem
PSIsContainer     : True
Name              : temurin-21-jdk
FullName          : C:\Users\begonia\AppData\Roaming\com.dropout.launcher\java\temurin-21-jdk
Parent            : java
Exists            : True
Root              : C:\
Extension         :
CreationTime      : 2026/1/26 18:32:09
CreationTimeUtc   : 2026/1/26 17:32:09
LastAccessTime    : 2026/1/26 18:32:12
LastAccessTimeUtc : 2026/1/26 17:32:12
LastWriteTime     : 2026/1/26 18:32:09
LastWriteTimeUtc  : 2026/1/26 17:32:09
Attributes        : Directory
Mode              : d-----
BaseName          : temurin-21-jdk
Target            : {}
LinkType          :


PSPath            : Microsoft.PowerShell.Core\FileSystem::C:\Users\begonia\AppData\Roaming\com.dropout.launcher\java\te
                    murin-8-jdk
PSParentPath      : Microsoft.PowerShell.Core\FileSystem::C:\Users\begonia\AppData\Roaming\com.dropout.launcher\java
PSChildName       : temurin-8-jdk
PSDrive           : C
PSProvider        : Microsoft.PowerShell.Core\FileSystem
PSIsContainer     : True
Name              : temurin-8-jdk
FullName          : C:\Users\begonia\AppData\Roaming\com.dropout.launcher\java\temurin-8-jdk
Parent            : java
Exists            : True
Root              : C:\
Extension         :
CreationTime      : 2026/1/16 20:16:51
CreationTimeUtc   : 2026/1/16 19:16:51
LastAccessTime    : 2026/1/26 18:26:58
LastAccessTimeUtc : 2026/1/26 17:26:58
LastWriteTime     : 2026/1/16 20:16:51
LastWriteTimeUtc  : 2026/1/16 19:16:51
Attributes        : Directory
Mode              : d-----
BaseName          : temurin-8-jdk
Target            : {}
LinkType          :
sr2026-01-26.183250.mp4

…r handling

- Extract version compatibility check into shared validation function
- Remove duplicated version checking code across multiple modules
- Simplify Java detection timeout logic in detection.rs
- Expand vendor detection to support more JDK distributions (Dragonwell, Kona, Semeru, BiSheng, etc.)
- Refactor start_game to use priority-based Java resolution
- Improve error handling in Adoptium provider task collection

Reviewed-by: Claude Sonnet 4.5
@BegoniaHe
Copy link
Contributor Author

test functionality

  • find java in correctly path sets
    • macos
    • linux
    • windows
image
  • download java in correctly path

    • macos
    • linux
    • windows

@HsiangNianian
Copy link
Member

To complete the select java correctly (user customized java path) intergration test, we need an updated frontend in java download page @fu050409

write test in specific rust file and run cargo test

@BegoniaHe
Copy link
Contributor Author

Java 组件设计规范检查报告

概述

本报告对 src-tauri/src/core/java 组件进行了全面的设计规范检查,对标参考目录中的 24 个设计原则。


✅ 符合的设计原则

1. 单一职责原则 (Single Responsibility Principle)

状态: ✅ 符合

分析:

  • detection.rs: 专注于 Java 候选项检测
  • validation.rs: 专注于 Java 安装验证
  • persistence.rs: 专注于配置持久化
  • priority.rs: 专注于 Java 优先级解析
  • provider.rs: 定义提供者接口
  • providers/adoptium.rs: 专注于 Adoptium API 集成

每个模块职责清晰,变更原因单一。


2. 显式依赖原则 (Explicit Dependencies Principle)

状态: ✅ 符合

分析:

// 好的例子 - 显式参数
pub async fn check_java_installation(path: &PathBuf) -> Option<JavaInstallation>
pub fn parse_java_version(version: &str) -> u32
pub async fn fetch_catalog(
    &self,
    app_handle: &AppHandle,
    force_refresh: bool,
) -> Result<JavaCatalog, String>

所有依赖都通过参数显式传入,没有隐式的全局状态或静态方法调用。


3. 关注点分离 (Separation of Concerns)

状态: ✅ 符合

分析:

  • 检测逻辑 (detection.rs): 仅处理系统 Java 查找
  • 验证逻辑 (validation.rs): 仅处理版本验证
  • 下载逻辑 (mod.rs): 调用 downloader 模块
  • 缓存逻辑 (mod.rs): 独立的缓存管理

各层职责分离清晰,易于维护和测试。


4. 开闭原则 (Open-Closed Principle)

状态: ✅ 符合

分析:

// 通过 trait 实现扩展性
pub trait JavaProvider: Send + Sync {
    async fn fetch_catalog(...) -> Result<JavaCatalog, String>;
    async fn fetch_release(...) -> Result<JavaDownloadInfo, String>;
    async fn available_versions(&self) -> Result<Vec<u32>, String>;
    fn provider_name(&self) -> &'static str;
    fn os_name(&self) -> &'static str;
    fn arch_name(&self) -> &'static str;
    fn install_prefix(&self) -> &'static str;
}

通过 JavaProvider trait,可以轻松添加新的 Java 提供者(如 Temurin、Corretto)而无需修改现有代码。


5. 接口隔离原则 (Interface Segregation Principle)

状态: ✅ 符合

分析:

  • JavaProvider trait 定义了清晰的接口
  • 每个方法都有明确的职责
  • 实现者只需实现必要的方法

6. 依赖倒置原则 (Dependency Inversion Principle)

状态: ✅ 符合

分析:

// 高层模块依赖抽象
pub async fn fetch_java_catalog(
    app_handle: &AppHandle,
    force_refresh: bool,
) -> Result<JavaCatalog, String> {
    let provider = AdoptiumProvider::new();
    provider.fetch_catalog(app_handle, force_refresh).await
}

通过 JavaProvider trait 抽象,高层模块不依赖具体实现。


7. 快速失败原则 (Fail Fast)

状态: ✅ 符合

分析:

// 立即验证和返回错误
pub async fn check_java_installation(path: &PathBuf) -> Option<JavaInstallation> {
    let mut cmd = Command::new(path);
    cmd.arg("-version");
    
    let output = cmd.output().ok()?;  // 快速失败
    
    let version_output = String::from_utf8_lossy(&output.stderr);
    let version = parse_version_string(&version_output)?;  // 快速失败
    
    // ...
}

使用 ? 操作符和 Option 类型,错误立即返回。


8. 不重复自己 (Don't Repeat Yourself)

状态: ✅ 符合

分析:

  • 版本解析逻辑集中在 parse_java_version()
  • 架构检测逻辑集中在 extract_architecture()
  • 供应商检测逻辑集中在 extract_vendor()

避免了重复的字符串匹配和版本解析代码。


9. 持久化无关性 (Persistence Ignorance)

状态: ✅ 符合

分析:

  • JavaInstallation 结构体不包含任何持久化逻辑
  • 持久化由 persistence.rs 模块独立处理
  • 核心业务逻辑与存储机制解耦

10. 明确告诉,不要询问 (Tell, Don't Ask)

状态: ✅ 符合

分析:

// 好的例子 - 告诉对象做什么
pub async fn resolve_java_for_launch(
    app_handle: &AppHandle,
    instance_java_override: Option<&str>,
    global_java_path: Option<&str>,
    required_major_version: Option<u64>,
    max_major_version: Option<u32>,
) -> Option<JavaInstallation>

调用者告诉系统需要什么版本的 Java,而不是查询所有 Java 然后自己过滤。


11. 保持简单 (Keep It Simple)

状态: ✅ 符合

分析:

  • 代码结构清晰,没有过度设计
  • 函数职责单一,易于理解
  • 避免了复杂的嵌套逻辑

12. 架构敏捷性 (Architectural Agility)

状态: ✅ 符合

分析:

  • 模块化设计允许轻松添加新的 Java 提供者
  • 缓存机制可独立调整
  • 检测策略可灵活扩展

13. 稳定依赖 (Stable Dependencies)

状态: ✅ 符合

分析:

  • 依赖于稳定的 Rust 标准库
  • 使用成熟的 crate(reqwest, serde, tokio
  • 不依赖不稳定的内部模块

14. 一次且仅一次 (Once and Only Once)

状态: ✅ 符合

分析:

  • 版本比较逻辑只在一个地方定义
  • 路径规范化逻辑集中在 strip_unc_prefix()
  • 避免了重复的条件检查

15. 最少惊讶原则 (Principle of Least Astonishment)

状态: ✅ 符合

分析:

  • 函数名清晰表达意图(detect_java, check_java_installation
  • 返回类型符合预期(Option, Result
  • 行为符合函数名的承诺

16. Boy Scout 规则

状态: ✅ 符合

分析:

  • 代码结构清晰,易于维护
  • 注释清楚地解释了复杂逻辑
  • 错误处理完善


⚠️ 需要改进的地方

1. 无效状态不可表示 (Make Invalid States Unrepresentable)

状态: ⚠️ 部分改进空间

问题:

pub struct JavaInstallation {
    pub path: String,
    pub version: String,
    pub arch: String,
    pub vendor: String,
    pub source: String,
    pub is_64bit: bool,
}

问题分析:

  • path 可能是空字符串(无效状态)
  • version 可能是无效的版本格式
  • arch 可能是未知的架构
  • 这些字段的组合可能产生无效状态

建议:

// 使用类型系统确保有效性
pub struct JavaInstallation {
    pub path: NonEmptyPath,  // 自定义类型确保非空
    pub version: JavaVersion,  // 自定义类型确保有效版本
    pub arch: Architecture,  // 枚举而非字符串
    pub vendor: JavaVendor,  // 枚举而非字符串
    pub source: InstallationSource,  // 枚举而非字符串
    pub is_64bit: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Architecture {
    X64,
    X86,
    Aarch64,
    Arm,
    Unknown,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum JavaVendor {
    EclipseAdoptium,
    OpenJDK,
    Oracle,
    Microsoft,
    AzulZulu,
    AmazonCorretto,
    BellSoftLiberica,
    GraalVM,
    Unknown,
}

优势:

  • 编译时验证,不可能创建无效状态
  • 模式匹配更安全
  • 减少运行时错误

2. 缓存策略改进

状态: ⚠️ 可以更健壮

问题:

pub fn load_cached_catalog(app_handle: &AppHandle) -> Option<JavaCatalog> {
    let cache_path = get_catalog_cache_path(app_handle);
    if !cache_path.exists() {
        return None;
    }

    let content = std::fs::read_to_string(&cache_path).ok()?;
    let catalog: JavaCatalog = serde_json::from_str(&content).ok()?;

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs();

    if now - catalog.cached_at < CACHE_DURATION_SECS {
        Some(catalog)
    } else {
        None
    }
}

问题分析:

  • 缓存过期后没有自动清理
  • 没有缓存版本控制(API 变化时可能读取过时格式)
  • 没有缓存大小限制

建议:

pub fn load_cached_catalog(app_handle: &AppHandle) -> Option<JavaCatalog> {
    let cache_path = get_catalog_cache_path(app_handle);
    if !cache_path.exists() {
        return None;
    }

    let content = std::fs::read_to_string(&cache_path).ok()?;
    let catalog: JavaCatalog = serde_json::from_str(&content).ok()?;

    // 验证缓存版本
    if catalog.cache_version != CACHE_VERSION {
        // 缓存格式已过期,删除并返回 None
        let _ = std::fs::remove_file(&cache_path);
        return None;
    }

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap()
        .as_secs();

    if now - catalog.cached_at < CACHE_DURATION_SECS {
        Some(catalog)
    } else {
        // 缓存已过期,清理
        let _ = std::fs::remove_file(&cache_path);
        None
    }
}

3. 错误处理的一致性

状态: ⚠️ 可以更一致

问题:

// 有些地方返回 Option
pub fn find_sdkman_java() -> Option<PathBuf>

// 有些地方返回 Result
pub async fn fetch_java_catalog(
    app_handle: &AppHandle,
    force_refresh: bool,
) -> Result<JavaCatalog, String>

// 有些地方返回 bool
pub async fn is_java_compatible(
    java_path: &str,
    required_major_version: Option<u64>,
    max_major_version: Option<u32>,
) -> bool

问题分析:

  • 混合使用 Option, Result, bool 使 API 不一致
  • 调用者需要记住每个函数的返回类型
  • 难以统一处理错误

建议:

// 统一使用 Result<T, JavaError>
#[derive(Debug, Clone)]
pub enum JavaError {
    NotFound,
    InvalidVersion(String),
    VerificationFailed,
    NetworkError(String),
    IoError(String),
}

pub fn find_sdkman_java() -> Result<PathBuf, JavaError>
pub async fn fetch_java_catalog(...) -> Result<JavaCatalog, JavaError>
pub async fn is_java_compatible(...) -> Result<bool, JavaError>

4. 平台特定代码的组织

状态: ⚠️ 可以更清晰

问题:

// detection.rs 中混合了多个平台的代码
#[cfg(target_os = "linux")]
{
    // Linux 特定代码
}

#[cfg(target_os = "macos")]
{
    // macOS 特定代码
}

#[cfg(target_os = "windows")]
{
    // Windows 特定代码
}

问题分析:

  • 单个文件中混合了三个平台的逻辑
  • 难以维护和测试特定平台的代码
  • 代码行数过长

建议:

src-tauri/src/core/java/
├── mod.rs
├── detection/
│   ├── mod.rs
│   ├── linux.rs
│   ├── macos.rs
│   └── windows.rs
├── validation.rs
├── persistence.rs
├── priority.rs
├── provider.rs
└── providers/
    ├── mod.rs
    └── adoptium.rs

5. 测试覆盖

状态: ⚠️ 缺少单元测试

问题:

  • detection.rs 没有单元测试
  • validation.rs 没有单元测试
  • persistence.rs 没有单元测试
  • 只有 maven.rs 有测试

建议:

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_java_version_legacy() {
        assert_eq!(parse_java_version("1.8.0_292"), 8);
    }

    #[test]
    fn test_parse_java_version_modern() {
        assert_eq!(parse_java_version("17.0.2"), 17);
    }

    #[test]
    fn test_extract_architecture_64bit() {
        let output = "Java HotSpot(TM) 64-Bit Server VM";
        assert_eq!(extract_architecture(output), "x64");
    }

    #[test]
    fn test_extract_vendor_adoptium() {
        let output = "Eclipse Adoptium";
        assert_eq!(extract_vendor(output), "Eclipse Adoptium");
    }
}

6. 文档注释

状态: ⚠️ 部分缺少

问题:

  • 许多公共函数缺少 doc 注释
  • 复杂的逻辑没有解释

建议:

/// 检测系统中所有可用的 Java 安装
///
/// 此函数扫描多个位置以查找 Java 安装:
/// - Linux: `/usr/lib/jvm`, `/usr/java`, `/opt/java` 等
/// - macOS: `/Library/Java/JavaVirtualMachines` 等
/// - Windows: `Program Files`, `Program Files (x86)` 等
/// - 所有平台: `JAVA_HOME` 环境变量, `PATH` 中的 `java` 命令
///
/// # Returns
/// 返回找到的所有 Java 安装的向量,按版本号降序排序
///
/// # Examples
/// ```
/// let installations = get_java_candidates();
/// for candidate in installations {
///     println!("Found Java at: {}", candidate.display());
/// }
/// ```
pub fn get_java_candidates() -> Vec<PathBuf>

7. 超时处理

状态: ⚠️ 可以更健壮

问题:

const WHICH_TIMEOUT: Duration = Duration::from_secs(2);

fn run_which_command_with_timeout() -> Option<String> {
    // 手动实现超时逻辑
    loop {
        match child.try_wait() {
            Ok(Some(status)) => { /* ... */ }
            Ok(None) => {
                if start.elapsed() >= WHICH_TIMEOUT {
                    let _ = child.kill();
                    return None;
                }
                sleep(POLL_INTERVAL);
            }
            Err(_) => { /* ... */ }
        }
    }
}

问题分析:

  • 手动实现超时容易出错
  • 轮询方式效率低

建议:

use tokio::time::timeout;

async fn run_which_command_with_timeout() -> Option<String> {
    let mut cmd = Command::new(if cfg!(windows) { "where" } else { "which" });
    cmd.arg("java");
    cmd.stdout(Stdio::piped());

    let mut child = cmd.spawn().ok()?;
    
    // 使用 tokio 的超时机制
    match timeout(WHICH_TIMEOUT, child.wait()).await {
        Ok(Ok(status)) if status.success() => {
            let mut output = String::new();
            if let Some(mut stdout) = child.stdout.take() {
                let _ = stdout.read_to_string(&mut output);
            }
            Some(output)
        }
        Ok(_) => None,
        Err(_) => {
            let _ = child.kill();
            None
        }
    }
}

8. 配置管理

状态: ⚠️ 硬编码常量

问题:

const WHICH_TIMEOUT: Duration = Duration::from_secs(2);
const POLL_INTERVAL: Duration = Duration::from_millis(50);
const CACHE_DURATION_SECS: u64 = 24 * 60 * 60;

问题分析:

  • 这些值硬编码在代码中
  • 用户无法自定义
  • 难以调整性能参数

建议:

pub struct JavaConfig {
    pub which_timeout_secs: u64,
    pub poll_interval_ms: u64,
    pub cache_duration_secs: u64,
}

impl Default for JavaConfig {
    fn default() -> Self {
        Self {
            which_timeout_secs: 2,
            poll_interval_ms: 50,
            cache_duration_secs: 24 * 60 * 60,
        }
    }
}

📊 总体评分

原则 状态 分数
单一职责原则 10/10
显式依赖原则 10/10
关注点分离 10/10
开闭原则 9/10
接口隔离原则 9/10
依赖倒置原则 9/10
快速失败原则 9/10
不重复自己 9/10
持久化无关性 10/10
明确告诉 9/10
保持简单 9/10
架构敏捷性 9/10
稳定依赖 10/10
一次且仅一次 9/10
最少惊讶原则 9/10
Boy Scout 规则 9/10
无效状态不可表示 ⚠️ 6/10
缓存策略 ⚠️ 7/10
错误处理一致性 ⚠️ 7/10
平台代码组织 ⚠️ 7/10
测试覆盖 ⚠️ 5/10
文档注释 ⚠️ 6/10
超时处理 ⚠️ 7/10
配置管理 ⚠️ 6/10

总体评分: 8.2/10 ✅ 优秀


🎯 优先级改进建议

高优先级 (应立即处理)

  1. 添加单元测试 - 提高代码可靠性
  2. 统一错误处理 - 使用 Result<T, JavaError> 替代混合的返回类型
  3. 无效状态不可表示 - 使用类型系统确保有效性

中优先级 (下一个迭代)

  1. 改进缓存策略 - 添加版本控制和自动清理
  2. 平台代码分离 - 将平台特定代码移到独立模块
  3. 添加文档注释 - 为所有公共 API 添加 doc 注释

低优先级 (长期改进)

  1. 超时处理现代化 - 使用 tokio::time::timeout
  2. 配置管理 - 允许用户自定义参数

结论

src-tauri/src/core/java 组件整体设计良好,遵循了大多数 SOLID 原则和最佳实践。代码结构清晰,职责分离明确,易于维护和扩展。

主要改进空间在于:

  • 类型系统的更好利用(无效状态不可表示)
  • 错误处理的一致性
  • 测试覆盖的完善
  • 文档的补充

这些改进将进一步提升代码质量和可维护性。


Drafted by Sisyphus from OMO
Using model Claude 3.5 Sonnet

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 10 comments.

Comment on lines +187 to +190
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another .unwrap() call on duration_since that could theoretically panic. This should be handled more gracefully, perhaps with unwrap_or(0) or by propagating an error.

Suggested change
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let now = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
Ok(duration) => duration.as_secs(),
Err(_) => 0,
};

Copilot uses AI. Check for mistakes.
}
}

let home = std::env::var("HOME").unwrap_or_default();
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable 'home' is declared but never used in the Linux-specific section. This line appears to be left over from refactoring since the SDKMAN check doesn't need it. Consider removing this line.

Suggested change
let home = std::env::var("HOME").unwrap_or_default();

Copilot uses AI. Check for mistakes.
Comment on lines 88 to 125
let vendor_name: HashMap<&str, &str> = [
// Eclipse/Adoptium
("temurin", "Temurin (Eclipse)"),
("adoptium", "Eclipse Adoptium"),
// Amazon
("corretto", "Corretto (Amazon)"),
("amzn", "Corretto (Amazon)"),
// Alibaba
("dragonwell", "Dragonwell (Alibaba)"),
("albba", "Dragonwell (Alibaba)"),
// GraalVM
("graalvm", "GraalVM"),
// Oracle
("oracle", "Java SE Development Kit (Oracle)"),
// Tencent
("kona", "Kona (Tencent)"),
// BellSoft
("liberica", "Liberica (Bellsoft)"),
("mandrel", "Mandrel (Red Hat)"),
// Microsoft
("microsoft", "OpenJDK (Microsoft)"),
// SAP
("sapmachine", "SapMachine (SAP)"),
// IBM
("semeru", "Semeru (IBM)"),
("sem", "Semeru (IBM)"),
// Azul
("zulu", "Zulu (Azul Systems)"),
// Trava
("trava", "Trava (Trava)"),
// Huawei
("bisheng", "BiSheng (Huawei)"),
// Generic OpenJDK
("openjdk", "OpenJDK"),
]
.iter()
.cloned()
.collect();
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The vendor name HashMap is recreated on every call to extract_vendor. This is inefficient since the HashMap contains static data. Consider using once_cell::sync::Lazy or defining it as a static constant to avoid repeated allocations. This function is called during Java detection which can happen frequently.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +27
fn get_java_config_path(app_handle: &AppHandle) -> PathBuf {
app_handle
.path()
.app_data_dir()
.unwrap()
.join("java_config.json")
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .unwrap() call on app_data_dir() could panic if the app data directory cannot be determined. While this is unlikely in practice, it would be more robust to handle this error gracefully by returning a Result or using a fallback path.

Copilot uses AI. Check for mistakes.
Comment on lines +97 to +106
pub fn get_java_install_dir(app_handle: &AppHandle) -> PathBuf {
app_handle.path().app_data_dir().unwrap().join("java")
}

fn get_catalog_cache_path(app_handle: &AppHandle) -> PathBuf {
app_handle
.path()
.app_data_dir()
.unwrap()
.join("java_catalog_cache.json")
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple .unwrap() calls on app_data_dir() throughout this module could panic if the app data directory cannot be determined. Consider creating a helper function that returns a Result or handles the error more gracefully. This pattern appears in get_java_install_dir (line 98), get_catalog_cache_path (lines 104-105), and potentially other places.

Suggested change
pub fn get_java_install_dir(app_handle: &AppHandle) -> PathBuf {
app_handle.path().app_data_dir().unwrap().join("java")
}
fn get_catalog_cache_path(app_handle: &AppHandle) -> PathBuf {
app_handle
.path()
.app_data_dir()
.unwrap()
.join("java_catalog_cache.json")
/// Get the base application data directory, falling back to the OS temp dir
/// if Tauri cannot determine an app-specific data directory.
fn get_app_data_dir(app_handle: &AppHandle) -> PathBuf {
app_handle
.path()
.app_data_dir()
.unwrap_or_else(|| std::env::temp_dir())
}
pub fn get_java_install_dir(app_handle: &AppHandle) -> PathBuf {
get_app_data_dir(app_handle).join("java")
}
fn get_catalog_cache_path(app_handle: &AppHandle) -> PathBuf {
get_app_data_dir(app_handle).join("java_catalog_cache.json")

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +123
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .unwrap() call on duration_since could theoretically panic if system time is before UNIX_EPOCH or if there's a clock adjustment. While rare, it would be more robust to handle this with unwrap_or(0) or return an error.

Suggested change
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let now = match std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH) {
Ok(duration) => duration.as_secs(),
// If system time is before UNIX_EPOCH or there is a clock issue,
// treat the cache as invalid instead of panicking.
Err(_) => return None,
};

Copilot uses AI. Check for mistakes.
Comment on lines 11 to 56
const WHICH_TIMEOUT: Duration = Duration::from_secs(2);

pub fn find_sdkman_java() -> Option<PathBuf> {
let home = std::env::var("HOME").ok()?;
let sdkman_path = PathBuf::from(&home).join(".sdkman/candidates/java/current/bin/java");
if sdkman_path.exists() {
Some(sdkman_path)
} else {
None
}
}

fn run_which_command_with_timeout() -> Option<String> {
let mut cmd = Command::new(if cfg!(windows) { "where" } else { "which" });
cmd.arg("java");
// Hide console window
#[cfg(target_os = "windows")]
cmd.creation_flags(0x08000000);
cmd.stdout(Stdio::piped());

let mut child = cmd.spawn().ok()?;

loop {
match child.try_wait() {
Ok(Some(status)) => {
if status.success() {
let mut output = String::new();
if let Some(mut stdout) = child.stdout.take() {
let _ = stdout.read_to_string(&mut output);
}
return Some(output);
} else {
let _ = child.wait();
return None;
}
}
Ok(None) => {
std::thread::sleep(Duration::from_millis(50));
}
Err(_) => {
let _ = child.kill();
let _ = child.wait();
return None;
}
}
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The WHICH_TIMEOUT constant is defined but never used. The run_which_command_with_timeout function implements a timeout mechanism with busy-waiting (50ms sleep), but it never checks against the defined timeout duration. This could cause the function to hang indefinitely if the which/where command doesn't complete. Consider adding actual timeout enforcement using the constant or removing it if not needed.

Copilot uses AI. Check for mistakes.
// Check if configured Java is compatible
// Resolve Java using priority-based resolution
// Priority: instance override > global config > user preference > auto-detect
// TODO: refactor into a separate function
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TODO comment indicates that this logic should be refactored into a separate function, but it hasn't been done. Since a resolve_java_for_launch function already exists in the priority module and is being called here, this TODO comment should either be removed (as the refactoring is complete) or clarified if additional refactoring is still needed.

Suggested change
// TODO: refactor into a separate function

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +50
std::fs::create_dir_all(
config_path
.parent()
.expect("Java config path should have a parent directory"),
)
.map_err(|e| e.to_string())?;
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using .expect() with a descriptive message here could panic. Since this function returns a Result, it would be better to handle this error by returning an error instead of panicking. Consider using .ok_or_else(|| "...".to_string())? to propagate the error.

Suggested change
std::fs::create_dir_all(
config_path
.parent()
.expect("Java config path should have a parent directory"),
)
.map_err(|e| e.to_string())?;
let parent_dir = config_path
.parent()
.ok_or_else(|| "Java config path should have a parent directory".to_string())?;
std::fs::create_dir_all(parent_dir).map_err(|e| e.to_string())?;

Copilot uses AI. Check for mistakes.
Comment on lines +200 to +203
created_at: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another .unwrap() call on duration_since that could theoretically panic. This should be handled more gracefully, perhaps with unwrap_or(0) or by propagating an error.

Copilot uses AI. Check for mistakes.
- Added support for detecting Java installations from SDKMAN! in `find_sdkman_java`.
- Improved `run_which_command_with_timeout` to handle command timeouts gracefully.
- Introduced a unified `JavaError` enum for consistent error handling across Java operations.
- Updated functions to return `Result` types instead of `Option` for better error reporting.
- Enhanced `load_cached_catalog` and `save_catalog_cache` to use `JavaError`.
- Refactored `fetch_java_catalog`, `fetch_java_release`, and `fetch_available_versions` to return `JavaError`.
- Improved validation functions to return detailed errors when checking Java installations.
- Added tests for version parsing and compatibility checks.
- Updated `resolve_java_for_launch` to handle instance-specific and global Java paths.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 12 comments.

// Task completed but returned None, should not happen in current implementation
}
Err(e) => {
eprintln!("AdoptiumProvider::fetch_catalog task join error: {:?}", e);
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The eprintln! on line 186 writes to stderr but uses a debug format. In production builds, this won't be visible to users and might be lost. Additionally, task join errors are usually programming errors (like panics in spawned tasks), not normal operational errors.

Consider using a proper logging framework or emitting an event to notify the user/frontend of the error. Alternatively, if task join failures are not expected and indicate bugs, consider propagating them as errors rather than silently continuing.

Suggested change
eprintln!("AdoptiumProvider::fetch_catalog task join error: {:?}", e);
return Err(JavaError::NetworkError(format!(
"Failed to join Adoptium catalog fetch task: {}",
e
)));

Copilot uses AI. Check for mistakes.
Comment on lines +21 to +30
pub async fn check_java_installation(path: &PathBuf) -> Result<JavaInstallation, JavaError> {
if !path.exists() {
return Err(JavaError::NotFound);
}

let path = path.clone();
tokio::task::spawn_blocking(move || check_java_installation_blocking(&path))
.await
.map_err(|e| JavaError::VerificationFailed(format!("Task join error: {}", e)))?
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spawned task in check_java_installation checks if the path exists before spawning, but this creates a TOCTOU (Time-of-Check to Time-of-Use) race condition. The path could be deleted or become inaccessible between the check on line 22 and the actual use in the spawned task on line 27.

Consider moving the existence check inside the spawned blocking task or handling the error case more robustly when the path doesn't exist during execution.

Copilot uses AI. Check for mistakes.
Comment on lines +37 to +38
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
Err(_) => JavaConfig::default(),
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The load_java_config function silently returns JavaConfig::default() on any deserialization error. This could hide corruption in the config file or schema changes that break deserialization. Users might lose their Java preferences without any notification.

Consider logging deserialization errors to help with debugging, or at minimum distinguishing between "file doesn't exist" (expected on first run) and "file exists but can't be parsed" (indicates a problem).

Suggested change
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
Err(_) => JavaConfig::default(),
Ok(content) => match serde_json::from_str(&content) {
Ok(config) => config,
Err(err) => {
eprintln!(
"Failed to parse Java config at {}: {}. Falling back to default configuration.",
config_path.display(),
err
);
JavaConfig::default()
}
},
Err(err) => {
eprintln!(
"Failed to read Java config at {}: {}. Falling back to default configuration.",
config_path.display(),
err
);
JavaConfig::default()
}

Copilot uses AI. Check for mistakes.
Comment on lines +129 to +131
pub version: String,
pub arch: String,
pub vendor: String,
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JavaInstallation struct now includes arch, vendor, and source fields that were not present in the old implementation. This is a breaking change in the API contract, as any frontend code expecting the old structure with only path, version, and is_64bit will fail to deserialize the new structure.

Consider documenting this breaking change in the PR description and migration guide, or handle backward compatibility by making the new fields optional with #[serde(default)].

Suggested change
pub version: String,
pub arch: String,
pub vendor: String,
pub version: String,
#[serde(default)]
pub arch: String,
#[serde(default)]
pub vendor: String,
#[serde(default)]

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +42
// Check instance-specific override first
if let Some(override_path) = instance_java_override {
if !override_path.is_empty() {
let path_buf = std::path::PathBuf::from(override_path);
if let Ok(java) = validation::check_java_installation(&path_buf).await {
if is_version_compatible(&java, required_major_version, max_major_version) {
return Some(java);
}
}
}
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the priority resolver, if the instance override path is provided but invalid or incompatible, the function silently falls through to check the global path. There's no logging or notification to the user that their instance-specific override was ignored.

Consider adding logging (via emit_log or similar) to inform users when their instance Java override is invalid or incompatible, helping with debugging configuration issues.

Copilot uses AI. Check for mistakes.
Comment on lines +25 to +72
pub async fn resolve_java_for_launch(
app_handle: &AppHandle,
instance_java_override: Option<&str>,
global_java_path: Option<&str>,
required_major_version: Option<u64>,
max_major_version: Option<u32>,
) -> Option<JavaInstallation> {
// Check instance-specific override first
if let Some(override_path) = instance_java_override {
if !override_path.is_empty() {
let path_buf = std::path::PathBuf::from(override_path);
if let Ok(java) = validation::check_java_installation(&path_buf).await {
if is_version_compatible(&java, required_major_version, max_major_version) {
return Some(java);
}
}
}
}

// Check global Java path setting
if let Some(global_path) = global_java_path {
if !global_path.is_empty() {
let path_buf = std::path::PathBuf::from(global_path);
if let Ok(java) = validation::check_java_installation(&path_buf).await {
if is_version_compatible(&java, required_major_version, max_major_version) {
return Some(java);
}
}
}
}

// Check preferred Java path from config
let preferred = persistence::get_preferred_java_path(app_handle);
if let Some(pref_path) = preferred {
let path_buf = std::path::PathBuf::from(&pref_path);
if let Ok(java) = validation::check_java_installation(&path_buf).await {
if is_version_compatible(&java, required_major_version, max_major_version) {
return Some(java);
}
}
}

// Fall back to first compatible installation from system scan
let installations = super::detect_all_java_installations(app_handle).await;
installations
.into_iter()
.find(|java| is_version_compatible(java, required_major_version, max_major_version))
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolve_java_for_launch function implements critical priority-based resolution logic that determines which Java installation to use for game launching, but it has no test coverage. This is a core function that affects game startup reliability.

Since the validation module has tests and this function follows a clear priority order, consider adding unit tests for this function to verify: 1) priority order is respected, 2) version compatibility checks work correctly, 3) fallback to system scan works, 4) empty string paths are handled correctly.

Copilot uses AI. Check for mistakes.
Comment on lines +112 to +247
pub fn get_java_candidates() -> Vec<PathBuf> {
let mut candidates = Vec::new();

// Try to find Java in PATH using 'which' or 'where' command with timeout
// CAUTION: linux 'which' may return symlinks, so we need to canonicalize later
if let Some(paths_str) = run_which_command_with_timeout() {
for line in paths_str.lines() {
let path = PathBuf::from(line.trim());
if path.exists() {
let resolved = std::fs::canonicalize(&path).unwrap_or(path);
let final_path = strip_unc_prefix(resolved);
candidates.push(final_path);
}
}
}

#[cfg(target_os = "linux")]
{
let linux_paths = [
"/usr/lib/jvm",
"/usr/java",
"/opt/java",
"/opt/jdk",
"/opt/openjdk",
];

for base in &linux_paths {
if let Ok(entries) = std::fs::read_dir(base) {
for entry in entries.flatten() {
let java_path = entry.path().join("bin/java");
if java_path.exists() {
candidates.push(java_path);
}
}
}
}

// Check common SDKMAN! java candidates
if let Some(sdkman_java) = find_sdkman_java() {
candidates.push(sdkman_java);
}
}

#[cfg(target_os = "macos")]
{
let mac_paths = [
"/Library/Java/JavaVirtualMachines",
"/System/Library/Java/JavaVirtualMachines",
"/usr/local/opt/openjdk/bin/java",
"/opt/homebrew/opt/openjdk/bin/java",
];

for path in &mac_paths {
let p = PathBuf::from(path);
if p.is_dir() {
if let Ok(entries) = std::fs::read_dir(&p) {
for entry in entries.flatten() {
let java_path = entry.path().join("Contents/Home/bin/java");
if java_path.exists() {
candidates.push(java_path);
}
}
}
} else if p.exists() {
candidates.push(p);
}
}

// Check common Homebrew java candidates for aarch64 macs
let homebrew_arm = PathBuf::from("/opt/homebrew/Cellar/openjdk");
if homebrew_arm.exists() {
if let Ok(entries) = std::fs::read_dir(&homebrew_arm) {
for entry in entries.flatten() {
let java_path = entry
.path()
.join("libexec/openjdk.jdk/Contents/Home/bin/java");
if java_path.exists() {
candidates.push(java_path);
}
}
}
}

// Check common SDKMAN! java candidates
if let Some(sdkman_java) = find_sdkman_java() {
candidates.push(sdkman_java);
}
}

#[cfg(target_os = "windows")]
{
let program_files =
std::env::var("ProgramFiles").unwrap_or_else(|_| "C:\\Program Files".to_string());
let program_files_x86 = std::env::var("ProgramFiles(x86)")
.unwrap_or_else(|_| "C:\\Program Files (x86)".to_string());
let local_app_data = std::env::var("LOCALAPPDATA").unwrap_or_default();

// Common installation paths for various JDK distributions
let mut win_paths = vec![];
for base in &[&program_files, &program_files_x86, &local_app_data] {
win_paths.push(format!("{}\\Java", base));
win_paths.push(format!("{}\\Eclipse Adoptium", base));
win_paths.push(format!("{}\\AdoptOpenJDK", base));
win_paths.push(format!("{}\\Microsoft\\jdk", base));
win_paths.push(format!("{}\\Zulu", base));
win_paths.push(format!("{}\\Amazon Corretto", base));
win_paths.push(format!("{}\\BellSoft\\LibericaJDK", base));
win_paths.push(format!("{}\\Programs\\Eclipse Adoptium", base));
}

for base in &win_paths {
let base_path = PathBuf::from(base);
if base_path.exists() {
if let Ok(entries) = std::fs::read_dir(&base_path) {
for entry in entries.flatten() {
let java_path = entry.path().join("bin\\java.exe");
if java_path.exists() {
candidates.push(java_path);
}
}
}
}
}
}

// Check JAVA_HOME environment variable
if let Ok(java_home) = std::env::var("JAVA_HOME") {
let bin_name = if cfg!(windows) { "java.exe" } else { "java" };
let java_path = PathBuf::from(&java_home).join("bin").join(bin_name);
if java_path.exists() {
candidates.push(java_path);
}
}

candidates
}
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The detection module contains complex platform-specific logic for finding Java installations across different operating systems, but has no test coverage. Given that it's a new module with timeout handling and path resolution logic that varies by OS, it would benefit from tests.

Consider adding tests for: 1) SDKMAN detection, 2) timeout behavior of which/where commands, 3) platform-specific path candidate generation, 4) path deduplication and canonicalization.

Copilot uses AI. Check for mistakes.
Comment on lines +354 to +357
let need_download = if archive_path.exists() {
if let Some(expected_checksum) = &info.checksum {
let data = std::fs::read(&archive_path)?;
!crate::core::downloader::verify_checksum(&data, Some(expected_checksum), None)
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the entire archive file into memory on line 356 could cause memory issues for large Java distributions (which can be hundreds of megabytes). This happens when verifying if a re-download is needed.

Consider using a streaming checksum verification approach or reading the file in chunks to reduce memory pressure, especially on systems with limited RAM.

Suggested change
let need_download = if archive_path.exists() {
if let Some(expected_checksum) = &info.checksum {
let data = std::fs::read(&archive_path)?;
!crate::core::downloader::verify_checksum(&data, Some(expected_checksum), None)
fn compute_sha1_file(path: &std::path::Path) -> Result<String, JavaError> {
use std::fs::File;
use std::io::{BufReader, Read};
use sha1::{Digest, Sha1};
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let mut hasher = Sha1::new();
let mut buffer = [0u8; 8 * 1024];
loop {
let read_bytes = reader.read(&mut buffer)?;
if read_bytes == 0 {
break;
}
hasher.update(&buffer[..read_bytes]);
}
Ok(format!("{:x}", hasher.finalize()))
}
fn compute_sha256_file(path: &std::path::Path) -> Result<String, JavaError> {
use std::fs::File;
use std::io::{BufReader, Read};
use sha2::{Digest, Sha256};
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let mut hasher = Sha256::new();
let mut buffer = [0u8; 8 * 1024];
loop {
let read_bytes = reader.read(&mut buffer)?;
if read_bytes == 0 {
break;
}
hasher.update(&buffer[..read_bytes]);
}
Ok(format!("{:x}", hasher.finalize()))
}
let need_download = if archive_path.exists() {
if let Some(expected_checksum) = &info.checksum {
let expected = expected_checksum.to_ascii_lowercase();
let computed = if expected.len() == 40 {
compute_sha1_file(&archive_path)?
} else {
compute_sha256_file(&archive_path)?
};
computed != expected

Copilot uses AI. Check for mistakes.
Comment on lines +156 to +170
Err(_) => Some(JavaReleaseInfo {
major_version,
image_type,
version: format!("{}.x", major_version),
release_name: format!("jdk-{}", major_version),
release_date: None,
file_size: 0,
checksum: None,
download_url: String::new(),
is_lts,
is_available: false,
architecture: arch,
}),
}
});
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handling in the parallel fetch tasks silently swallows network errors and creates placeholder entries with is_available: false. While this prevents the entire catalog fetch from failing, it means users won't know if there was a transient network issue vs. the version genuinely not being available for their platform.

Consider distinguishing between network errors (which might be retried) and genuine "not available" responses (HTTP 404), and either logging the network errors or providing that information in the returned catalog structure.

Copilot uses AI. Check for mistakes.
let asset = assets
.into_iter()
.next()
.ok_or_else(|| JavaError::NotFound)?;
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fetch_release method returns JavaError::NotFound when no assets are found, but this error variant has no message parameter. This makes it difficult to distinguish between different "not found" scenarios (version doesn't exist vs. not available for platform vs. API error).

Consider using a more descriptive error like JavaError::Other(format!("Java {} {} not found for current platform", major_version, image_type)) to provide better context to users.

Suggested change
.ok_or_else(|| JavaError::NotFound)?;
.ok_or_else(|| {
JavaError::Other(format!(
"Java {} {} not found for current platform (os={}, arch={})",
major_version, image_type, os, arch
))
})?;

Copilot uses AI. Check for mistakes.
…eanup

- Add CACHE_VERSION constant for cache format compatibility tracking
- Add MAX_CACHE_SIZE_BYTES limit (10 MB) to prevent unbounded cache growth
- Add cache_version field to JavaCatalog struct with default value
- Implement cache version validation in load_cached_catalog()
- Implement cache size enforcement in save_catalog_cache()
- Add cleanup_expired_caches() for background cache cleanup
- Add enforce_cache_size_limit() to validate cache file sizes
- Add is_cache_version_compatible() helper function
- Automatically clean up expired caches on load and clear operations
- Validate cache version before using cached data

Fixes:
- Cache expiration without automatic cleanup (now cleaned on load)
- Missing cache version control (now validates format compatibility)
- Unbounded cache size growth (now limited to 10 MB)

Reviewed-by: Claude 3.5 Sonnet
@vercel
Copy link

vercel bot commented Jan 27, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
drop-out-docs Ready Ready Preview, Comment Jan 27, 2026 8:50am

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deps: java enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants