Now launcher has a nice launch ui and it can launch MiNeCRaFt.
This commit is contained in:
@@ -306,11 +306,16 @@ def fetch_specific_version_manifest(version_manifest: dict, spec_version: str) -
|
|||||||
# mapping
|
# mapping
|
||||||
get_specific_version_manifest = fetch_specific_version_manifest
|
get_specific_version_manifest = fetch_specific_version_manifest
|
||||||
|
|
||||||
def find_useful_part_in_specific_version_manifest(spec_version_manifest: dict)\
|
def default_java_version():
|
||||||
|
return {"component": "jre-legacy", "majorVersion": 8}
|
||||||
|
|
||||||
|
def find_useful_part_in_specific_version_manifest(spec_version_manifest: dict,
|
||||||
|
failback_java_version: dict | None=None) \
|
||||||
-> tuple[dict | None, dict | None, dict | None, dict | None, list | None, str | None, str | None]:
|
-> tuple[dict | None, dict | None, dict | None, dict | None, list | None, str | None, str | None]:
|
||||||
"""
|
"""
|
||||||
Return useful part from version manifest.
|
Return useful part from version manifest.
|
||||||
:param spec_version_manifest: Specific version's data
|
:param spec_version_manifest: Specific version's data
|
||||||
|
:param failback_java_version: Failback java version data (default is Java 8)
|
||||||
:return:
|
:return:
|
||||||
arguments: dict (contains jvm and game args, for newer version of Minecraft)
|
arguments: dict (contains jvm and game args, for newer version of Minecraft)
|
||||||
asset_index: dict
|
asset_index: dict
|
||||||
@@ -323,11 +328,15 @@ def find_useful_part_in_specific_version_manifest(spec_version_manifest: dict)\
|
|||||||
arguments = spec_version_manifest.get("arguments", {})
|
arguments = spec_version_manifest.get("arguments", {})
|
||||||
asset_index = spec_version_manifest.get("assetIndex", {})
|
asset_index = spec_version_manifest.get("assetIndex", {})
|
||||||
downloads = spec_version_manifest.get("downloads", {})
|
downloads = spec_version_manifest.get("downloads", {})
|
||||||
java_version = spec_version_manifest.get("javaVersion", {})
|
|
||||||
libraries = spec_version_manifest.get("libraries", [])
|
libraries = spec_version_manifest.get("libraries", [])
|
||||||
main_class = spec_version_manifest.get("mainClass", None)
|
main_class = spec_version_manifest.get("mainClass", None)
|
||||||
legacy_game_args = spec_version_manifest.get("minecraftArguments", None)
|
legacy_game_args = spec_version_manifest.get("minecraftArguments", None)
|
||||||
|
|
||||||
|
if failback_java_version is None:
|
||||||
|
failback_java_version = default_java_version()
|
||||||
|
|
||||||
|
java_version = spec_version_manifest.get("javaVersion", failback_java_version)
|
||||||
|
|
||||||
if not arguments and not legacy_game_args:
|
if not arguments and not legacy_game_args:
|
||||||
raise VersionDataKeyNotFoundException(
|
raise VersionDataKeyNotFoundException(
|
||||||
"No version manifest available in version manifest. (\"arguments\" or \"minecraftArguments\")"
|
"No version manifest available in version manifest. (\"arguments\" or \"minecraftArguments\")"
|
||||||
@@ -500,4 +509,10 @@ def download_core_file(manifest: dict, destination: Path, is_server=False, raise
|
|||||||
"Core file url is None. Unable to download it."
|
"Core file url is None. Unable to download it."
|
||||||
)
|
)
|
||||||
|
|
||||||
return file
|
return file
|
||||||
|
|
||||||
|
|
||||||
|
LATEST_VERSION_ALIASES = {
|
||||||
|
"latest-release": {"displayName": "Latest Release"},
|
||||||
|
"latest-snapshot": {"displayName": "Latest Snapshot"},
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,6 +46,38 @@ def warning(parent: QWidget, title: str, message: str,
|
|||||||
).exec()
|
).exec()
|
||||||
|
|
||||||
|
|
||||||
|
def error_with_plain_text(
|
||||||
|
parent: QWidget,
|
||||||
|
title: str,
|
||||||
|
message: str,
|
||||||
|
content: str,
|
||||||
|
) -> None:
|
||||||
|
dialog = QDialog(parent)
|
||||||
|
dialog.setWindowTitle(title)
|
||||||
|
dialog.resize(720, 420)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(dialog)
|
||||||
|
|
||||||
|
label = QLabel(message)
|
||||||
|
label.setWordWrap(True)
|
||||||
|
|
||||||
|
details_box = QPlainTextEdit()
|
||||||
|
details_box.setReadOnly(True)
|
||||||
|
details_box.setPlainText(content)
|
||||||
|
details_box.setSizePolicy(
|
||||||
|
QSizePolicy.Policy.Expanding,
|
||||||
|
QSizePolicy.Policy.Expanding,
|
||||||
|
)
|
||||||
|
|
||||||
|
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok)
|
||||||
|
buttons.accepted.connect(dialog.accept)
|
||||||
|
|
||||||
|
layout.addWidget(label)
|
||||||
|
layout.addWidget(details_box, 1)
|
||||||
|
layout.addWidget(buttons)
|
||||||
|
dialog.exec()
|
||||||
|
|
||||||
|
|
||||||
def ask_yes_no_with_plain_text(parent: QWidget, title: str, message: str, content: str):
|
def ask_yes_no_with_plain_text(parent: QWidget, title: str, message: str, content: str):
|
||||||
dialog = QDialog(parent)
|
dialog = QDialog(parent)
|
||||||
dialog.setWindowTitle(title)
|
dialog.setWindowTitle(title)
|
||||||
|
|||||||
+385
-2
@@ -1,7 +1,10 @@
|
|||||||
import json
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from PySide6.QtCore import QObject
|
from PySide6.QtCore import QObject
|
||||||
@@ -9,8 +12,16 @@ from PySide6.QtCore import QObject
|
|||||||
from core_lib.common.common import FileObject
|
from core_lib.common.common import FileObject
|
||||||
from core_lib.common.exception import VersionManifestFetchException, VersionManifestSaveException, \
|
from core_lib.common.exception import VersionManifestFetchException, VersionManifestSaveException, \
|
||||||
NoSpecifiedVersionKeyException
|
NoSpecifiedVersionKeyException
|
||||||
|
from core_lib.game.argument import ArgumentMappings, generate_launch_command
|
||||||
|
from core_lib.game.asset import check_assets_exist, download_assets, find_correct_assets_dir
|
||||||
|
from core_lib.game.library_core import check_full_libraries_exists, download_full_libraries, generate_classpath
|
||||||
|
from core_lib.game.mod.mod_core import merge_manifest
|
||||||
from core_lib.game.version_info import fetch_and_save_version_manifest, get_specific_version_manifest_url_and_hash, \
|
from core_lib.game.version_info import fetch_and_save_version_manifest, get_specific_version_manifest_url_and_hash, \
|
||||||
save_version_manifest
|
save_version_manifest, find_useful_part_in_specific_version_manifest, download_core_file, get_latest_version, \
|
||||||
|
LATEST_VERSION_ALIASES
|
||||||
|
from core_lib.runtime.java import find_java_installations
|
||||||
|
from manager.profile import LaunchProfile
|
||||||
|
from manager.widgets import SelectPathRequest, AskForRequest
|
||||||
|
|
||||||
|
|
||||||
class ManifestManager:
|
class ManifestManager:
|
||||||
@@ -206,10 +217,382 @@ class ManifestManager:
|
|||||||
|
|
||||||
return callback, task_id
|
return callback, task_id
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ResolvedVersionManifest:
|
||||||
|
manifest: dict
|
||||||
|
version_id: str
|
||||||
|
client_version_id: str
|
||||||
|
|
||||||
|
def resolve_launch_manifest(self, version_id: str, visited: set[str] | None = None) -> ResolvedVersionManifest | None:
|
||||||
|
visited = set() if visited is None else visited
|
||||||
|
|
||||||
|
if version_id in visited:
|
||||||
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
|
f"Circular version inheritance detected: {version_id}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
visited.add(version_id)
|
||||||
|
|
||||||
|
manifest_path = self.get_specific_version_manifest_path(version_id)
|
||||||
|
|
||||||
|
# Read custom (or mod loader) manifest first. If not exist, get manifest from official data
|
||||||
|
if manifest_path.is_file():
|
||||||
|
manifest = self._read_cached_version_manifest(version_id)
|
||||||
|
else:
|
||||||
|
manifest = self.get_cached_version_manifest(version_id)
|
||||||
|
|
||||||
|
if not manifest:
|
||||||
|
return None
|
||||||
|
|
||||||
|
parent_id = manifest.get("inheritsFrom")
|
||||||
|
|
||||||
|
# Return if the version don't need to merge manifest
|
||||||
|
if not parent_id:
|
||||||
|
return self.ResolvedVersionManifest(
|
||||||
|
manifest=manifest,
|
||||||
|
version_id=version_id,
|
||||||
|
client_version_id=version_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get inherits if needed
|
||||||
|
parent = self.resolve_launch_manifest(parent_id, visited)
|
||||||
|
|
||||||
|
if parent is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Merge official and loader manifest
|
||||||
|
merged = merge_manifest(
|
||||||
|
inherits_version_manifest=parent.manifest,
|
||||||
|
mod_version_manifest=manifest,
|
||||||
|
)
|
||||||
|
except Exception as exc:
|
||||||
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
|
f"Unable to merge version {version_id} with {parent_id}: {exc}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return self.ResolvedVersionManifest(
|
||||||
|
manifest=merged,
|
||||||
|
version_id=version_id,
|
||||||
|
client_version_id=parent.client_version_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def resolve_version_alias(self, version_id: str) -> str | None:
|
||||||
|
"""
|
||||||
|
Resolve version alias to related version_id (Such as latest-release)
|
||||||
|
:param version_id:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if version_id not in LATEST_VERSION_ALIASES:
|
||||||
|
return version_id
|
||||||
|
|
||||||
|
root_manifest = self.get_cached_version_manifest()
|
||||||
|
|
||||||
|
if not root_manifest:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return get_latest_version(
|
||||||
|
root_manifest,
|
||||||
|
is_snapshot=version_id == "latest-snapshot",
|
||||||
|
)
|
||||||
|
|
||||||
|
class JavaManager:
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
self.widget_mgr = app.widget_manager
|
||||||
|
|
||||||
|
#
|
||||||
|
# JVM Management
|
||||||
|
#
|
||||||
|
@property
|
||||||
|
def jvm_settings_path(self) -> Path:
|
||||||
|
return Path(self.app.work_dir) / "jvms.json"
|
||||||
|
|
||||||
|
def _find_and_save_jvm_search_result(self):
|
||||||
|
installations, errors = find_java_installations()
|
||||||
|
if not installations:
|
||||||
|
self.widget_mgr.signal.show_warning_message.emit(
|
||||||
|
"There are no Java installations available in you system.\n"
|
||||||
|
)
|
||||||
|
return []
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
request = AskForRequest("Java Virtual Machine Finder", message = (
|
||||||
|
"Unable to detect below java installation version:\n"
|
||||||
|
))
|
||||||
|
request.details = "\n".join(path.as_posix() for path in errors)
|
||||||
|
request.use_plain_text = True
|
||||||
|
|
||||||
|
self.widget_mgr.signal.askyesno.emit(request)
|
||||||
|
|
||||||
|
self.jvm_settings_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with self.jvm_settings_path.open("w") as file:
|
||||||
|
json.dump(installations, file, indent=4)
|
||||||
|
|
||||||
|
return installations
|
||||||
|
except Exception as e:
|
||||||
|
self.widget_mgr.signal.show_and_print_error_message.emit(
|
||||||
|
{
|
||||||
|
"error": traceback.format_exception(e),
|
||||||
|
"title": "Settings Error",
|
||||||
|
"message": "An error occurred while attempting to save jvm settings. Please try again.",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _read_exist_jvm_settings(self):
|
||||||
|
try:
|
||||||
|
return json.loads(self.jvm_settings_path.read_text())
|
||||||
|
except Exception as e:
|
||||||
|
self.widget_mgr.signal.show_and_print_error_message.emit(
|
||||||
|
{
|
||||||
|
"error": traceback.format_exception(e),
|
||||||
|
"title": "Settings Error",
|
||||||
|
"message": "An error occurred while reading jvm settings.",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_or_cache_jvm_settings(self) -> list:
|
||||||
|
if self.jvm_settings_path.exists():
|
||||||
|
exists = self._read_exist_jvm_settings()
|
||||||
|
|
||||||
|
if exists:
|
||||||
|
return exists
|
||||||
|
|
||||||
|
return self._find_and_save_jvm_search_result()
|
||||||
|
|
||||||
|
def get_support_runtime_jvm_executable(self, major_version: int | str, no_error: bool=False) -> str | None:
|
||||||
|
result = self.get_or_cache_jvm_settings()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for item in result:
|
||||||
|
version = item.get("major", None)
|
||||||
|
executable = item.get("executable", None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
version = str(version)
|
||||||
|
major_version = str(major_version)
|
||||||
|
except Exception as e:
|
||||||
|
self.app.logger.warning(
|
||||||
|
f"Unable to convert version {version} to string: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if major_version == version and executable:
|
||||||
|
return executable
|
||||||
|
|
||||||
|
if not no_error:
|
||||||
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
|
"No Java executable found for major version {}.".format(major_version)
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LaunchResult:
|
||||||
|
process: subprocess.Popen | None = None
|
||||||
|
error_message: str | None = None
|
||||||
|
warnings: list[str] = field(default_factory=list)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def success(self) -> bool:
|
||||||
|
return self.process is not None and self.error_message is None
|
||||||
|
|
||||||
|
def fail(self, message: str) -> "LaunchResult":
|
||||||
|
self.error_message = message
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_warning(self, message: str) -> None:
|
||||||
|
self.warnings.append(message)
|
||||||
|
|
||||||
|
class LaunchManager:
|
||||||
|
def __init__(self, app, manifest_manager: ManifestManager, java_manager: JavaManager):
|
||||||
|
self.app = app
|
||||||
|
self.manifest_mgr = manifest_manager
|
||||||
|
self.widget_mgr = app.widget_manager
|
||||||
|
self.java_mgr = java_manager
|
||||||
|
self.launched_profiles: list[str] = []
|
||||||
|
|
||||||
|
def launch(self, profile: LaunchProfile) -> LaunchResult:
|
||||||
|
result = LaunchResult()
|
||||||
|
try:
|
||||||
|
version_id = profile.version_id
|
||||||
|
version_id = self.manifest_mgr.resolve_version_alias(version_id)
|
||||||
|
|
||||||
|
# Version manifest
|
||||||
|
resolved = self.manifest_mgr.resolve_launch_manifest(version_id)
|
||||||
|
|
||||||
|
if resolved is None:
|
||||||
|
return result.fail(
|
||||||
|
f"Unable to resolve version manifest: {version_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
version_manifest = resolved.manifest
|
||||||
|
client_version_id = resolved.client_version_id
|
||||||
|
|
||||||
|
arguments, asset_index, downloads, java_version, libraries, main_class, old_args = (
|
||||||
|
find_useful_part_in_specific_version_manifest(version_manifest)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Vars
|
||||||
|
asset_id = asset_index.get("id")
|
||||||
|
java_major_version = java_version.get("majorVersion")
|
||||||
|
|
||||||
|
# Game paths
|
||||||
|
core_file_path = Path(self.app.work_dir, "versions", client_version_id, f"{client_version_id}.jar")
|
||||||
|
natives_dir = Path(self.app.work_dir, "versions", version_id, "natives")
|
||||||
|
libraries_dir = Path(self.app.work_dir, "libraries")
|
||||||
|
|
||||||
|
game_dir = profile.game_dir
|
||||||
|
|
||||||
|
if not game_dir:
|
||||||
|
game_dir = Path(self.app.temp_dir, "minecraft")
|
||||||
|
elif not game_dir.exists():
|
||||||
|
game_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Check client
|
||||||
|
if not core_file_path.exists():
|
||||||
|
download_core_file(version_manifest, core_file_path, raise_for_null_hash=True)
|
||||||
|
|
||||||
|
# Libraries
|
||||||
|
if not check_full_libraries_exists(version_manifest, libraries_dir, natives_dir):
|
||||||
|
download_full_libraries(version_manifest, libraries_dir, natives_dir)
|
||||||
|
|
||||||
|
# Asset
|
||||||
|
default_assets_dir = Path(self.app.work_dir, "assets")
|
||||||
|
if not check_assets_exist(version_manifest, default_assets_dir, launcher_root=self.app.work_dir):
|
||||||
|
callback, task_id = self.manifest_mgr.create_download_progress_callback(
|
||||||
|
"Downloading missing assets"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
download_assets(
|
||||||
|
version_manifest,
|
||||||
|
default_assets_dir,
|
||||||
|
no_hash_check=True,
|
||||||
|
progress_callback=callback,
|
||||||
|
launcher_root=self.app.work_dir,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
self.widget_mgr.download_signal.failed.emit(task_id, str(e))
|
||||||
|
result.add_warning(
|
||||||
|
"Some assets were not downloaded. Although is not necessary for launching game."
|
||||||
|
" But it may cause some texture issue when you playing. (Such as missing textures)"
|
||||||
|
" Try relaunching to automatic redownload assets."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.widget_mgr.download_signal.finished.emit(task_id)
|
||||||
|
|
||||||
|
assets_dir = find_correct_assets_dir(asset_id, default_assets_dir, self.app.work_dir)
|
||||||
|
|
||||||
|
classpath = generate_classpath(
|
||||||
|
version_manifest,
|
||||||
|
libraries_dir,
|
||||||
|
core_file_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Mappings
|
||||||
|
arg_maps = ArgumentMappings(
|
||||||
|
player_name="Player",
|
||||||
|
version_name=version_id,
|
||||||
|
game_directory=game_dir.as_posix(),
|
||||||
|
assets_root=assets_dir.as_posix(),
|
||||||
|
legacy_assets_root=assets_dir,
|
||||||
|
assets_index_name=asset_id,
|
||||||
|
auth_uuid="00000000000000000000000000000000",
|
||||||
|
auth_access_token="0",
|
||||||
|
user_type="legacy",
|
||||||
|
version_type=profile.version_type,
|
||||||
|
natives_directory=natives_dir.as_posix(),
|
||||||
|
launcher_name="TestLauncher",
|
||||||
|
launcher_version="0.1",
|
||||||
|
classpath=classpath,
|
||||||
|
classpath_separator=os.pathsep,
|
||||||
|
library_directory=libraries_dir.as_posix(),
|
||||||
|
)
|
||||||
|
|
||||||
|
target_args = arguments if arguments else old_args
|
||||||
|
|
||||||
|
if not target_args:
|
||||||
|
return result.fail(
|
||||||
|
"Unable to parse arguments because data are invalid."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use found java runtime (if available)
|
||||||
|
java_path = self.java_mgr.get_support_runtime_jvm_executable(
|
||||||
|
java_major_version,
|
||||||
|
no_error=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if not java_path:
|
||||||
|
# Ask for a support java version. If not, use system environment java
|
||||||
|
request = SelectPathRequest(
|
||||||
|
title="Require a java executable path to launch game. (Major Version: {})".format(
|
||||||
|
java_major_version),
|
||||||
|
message=None,
|
||||||
|
filter_raw="Java executable (java.exe javaw.exe);;All files (*)"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.widget_mgr.signal.ask_for_path.emit(request)
|
||||||
|
|
||||||
|
if not request.wait(120):
|
||||||
|
return result.fail(
|
||||||
|
"Timed out while waiting for Java executable selection."
|
||||||
|
)
|
||||||
|
|
||||||
|
java_path = request.result()
|
||||||
|
|
||||||
|
if java_path:
|
||||||
|
self.app.logger.debug(f"Use specified java executable path: {java_path}")
|
||||||
|
else:
|
||||||
|
self.app.logger.debug("Use system environment java.")
|
||||||
|
java_path = "java"
|
||||||
|
|
||||||
|
# Finally, generate launch command
|
||||||
|
cmd = generate_launch_command(
|
||||||
|
arg_maps=arg_maps,
|
||||||
|
main_class=main_class,
|
||||||
|
java_path=java_path,
|
||||||
|
full_args=target_args,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.app.logger.debug(f"Launch command: {cmd}")
|
||||||
|
|
||||||
|
# run it
|
||||||
|
process = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
cwd=game_dir,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
errors="replace",
|
||||||
|
)
|
||||||
|
self.launched_profiles.append(profile.profile_id)
|
||||||
|
result.process = process
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
result.fail(
|
||||||
|
"Unable to launch. An unknown error occurred:\n{}".format(
|
||||||
|
"".join(traceback.format_exception(e))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def is_profile_running(self, profile_id):
|
||||||
|
return profile_id in self.launched_profiles
|
||||||
|
|
||||||
class GameManager(QObject):
|
class GameManager(QObject):
|
||||||
def __init__(self, app, parent=None):
|
def __init__(self, app, parent=None):
|
||||||
super(GameManager, self).__init__(parent)
|
super(GameManager, self).__init__(parent)
|
||||||
self.app = app
|
self.app = app
|
||||||
self.widget_mgr = app.widget_manager
|
self.widget_mgr = app.widget_manager
|
||||||
self.manifest = ManifestManager(self.app)
|
self.manifest = ManifestManager(self.app)
|
||||||
|
self.java = JavaManager(self.app)
|
||||||
|
self.launch = LaunchManager(self.app, self.manifest, self.java)
|
||||||
|
|||||||
+32
-3
@@ -1,5 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
IMPORTANT: Still under construction!
|
Profile Manager
|
||||||
|
|
||||||
|
A Minecraft profile manager that completely support official Minecraft Launcher.
|
||||||
"""
|
"""
|
||||||
import copy
|
import copy
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -11,14 +13,16 @@ from datetime import datetime
|
|||||||
from core_lib.common.exception import ProfileException, ProfileLoadException, ProfileSaveException
|
from core_lib.common.exception import ProfileException, ProfileLoadException, ProfileSaveException
|
||||||
from core_lib.game.profile import read_profile_file, is_profiles_valid, \
|
from core_lib.game.profile import read_profile_file, is_profiles_valid, \
|
||||||
get_profile_sample, create_or_save_profile_file, list_profiles as list_profiles_raw, convert_profile_data_to_object, \
|
get_profile_sample, create_or_save_profile_file, list_profiles as list_profiles_raw, convert_profile_data_to_object, \
|
||||||
is_profile_exists, create_profile as create_profile_raw, update_profile as update_profile_raw, is_profile_valid, \
|
is_profile_exists, create_profile as create_profile_raw, update_profile as update_profile_raw, \
|
||||||
get_current_profile_id, set_current_profile_id, remove_profile, duplicate_profile
|
get_current_profile_id, set_current_profile_id, remove_profile, duplicate_profile
|
||||||
|
from core_lib.game.version_info import LATEST_VERSION_ALIASES
|
||||||
from manager.widgets import AskForRequest
|
from manager.widgets import AskForRequest
|
||||||
|
|
||||||
DEFAuLT_PROFILE_RELATIVE_PATH = Path("launcher_profiles.json")
|
DEFAuLT_PROFILE_RELATIVE_PATH = Path("launcher_profiles.json")
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ProfileSummary:
|
class ProfileSummary:
|
||||||
|
display_name: str
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
version_id: str
|
version_id: str
|
||||||
@@ -62,6 +66,8 @@ class UpdateProfileRequest:
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class LaunchProfile:
|
class LaunchProfile:
|
||||||
|
display_name: str
|
||||||
|
name: str
|
||||||
profile_id: str
|
profile_id: str
|
||||||
version_id: str
|
version_id: str
|
||||||
version_type: str
|
version_type: str
|
||||||
@@ -235,6 +241,7 @@ class ProfileManager(QObject):
|
|||||||
for profile_id in profiles:
|
for profile_id in profiles:
|
||||||
profile = convert_profile_data_to_object(self.profiles, profile_id)
|
profile = convert_profile_data_to_object(self.profiles, profile_id)
|
||||||
profile_summary = ProfileSummary(
|
profile_summary = ProfileSummary(
|
||||||
|
display_name=self._get_profile_display_name(profile.name, profile.version_id),
|
||||||
icon=str(profile.icon),
|
icon=str(profile.icon),
|
||||||
id=profile_id,
|
id=profile_id,
|
||||||
last_used=profile.last_used,
|
last_used=profile.last_used,
|
||||||
@@ -265,6 +272,7 @@ class ProfileManager(QObject):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return ProfileDetail(
|
return ProfileDetail(
|
||||||
|
display_name=self._get_profile_display_name(profile.name, profile.version_id),
|
||||||
id=profile_id,
|
id=profile_id,
|
||||||
name=profile.name,
|
name=profile.name,
|
||||||
version_id=profile.version_id,
|
version_id=profile.version_id,
|
||||||
@@ -357,6 +365,22 @@ class ProfileManager(QObject):
|
|||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_profile_display_name(self, existing_name: str, version_id: str) -> str:
|
||||||
|
if existing_name.strip():
|
||||||
|
return existing_name.strip()
|
||||||
|
|
||||||
|
return self._get_profile_alias_display_name(version_id)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_profile_alias_display_name(version_id: str) -> str:
|
||||||
|
alias = LATEST_VERSION_ALIASES.get(version_id)
|
||||||
|
|
||||||
|
if alias:
|
||||||
|
return alias.get("displayName", version_id)
|
||||||
|
|
||||||
|
return "Unnamed Profile"
|
||||||
|
|
||||||
def update_profile(self, profile_id: str, changes: UpdateProfileRequest) -> bool:
|
def update_profile(self, profile_id: str, changes: UpdateProfileRequest) -> bool:
|
||||||
# Check profile exists
|
# Check profile exists
|
||||||
if not self.contains(profile_id, show_error=True):
|
if not self.contains(profile_id, show_error=True):
|
||||||
@@ -566,6 +590,11 @@ class ProfileManager(QObject):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return LaunchProfile(
|
return LaunchProfile(
|
||||||
|
display_name=self._get_profile_display_name(
|
||||||
|
profile.name,
|
||||||
|
profile.version_id,
|
||||||
|
),
|
||||||
|
name=profile.name,
|
||||||
profile_id=profile.id,
|
profile_id=profile.id,
|
||||||
version_id=profile.version_id,
|
version_id=profile.version_id,
|
||||||
version_type=profile.version_type,
|
version_type=profile.version_type,
|
||||||
@@ -671,4 +700,4 @@ class ProfileManager(QObject):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_modified(self) -> bool:
|
def is_modified(self) -> bool:
|
||||||
return self._is_modified
|
return self._is_modified
|
||||||
|
|||||||
+62
-7
@@ -1,11 +1,15 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, Signal, QPointF
|
from PySide6.QtCore import Qt, Signal
|
||||||
from PySide6.QtGui import QIcon, QMouseEvent, QHideEvent, QPixmap, QPainter, QColor
|
from PySide6.QtGui import QIcon, QMouseEvent, QHideEvent, QPixmap, QPainter
|
||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QScrollArea, QGridLayout, \
|
from PySide6.QtWidgets import (QWidget, QVBoxLayout, QLabel, QHBoxLayout, QPushButton, QScrollArea, QFrame,
|
||||||
QComboBox, QFrame, QSizePolicy, QApplication, QGraphicsDropShadowEffect
|
QSizePolicy, QApplication)
|
||||||
|
|
||||||
|
from manager.game import LaunchManager
|
||||||
|
from core_lib.game.version_info import LATEST_VERSION_ALIASES
|
||||||
from manager.profile import ProfileSummary
|
from manager.profile import ProfileSummary
|
||||||
|
from manager.widgets import AskForRequest, WidgetManager
|
||||||
|
from pages.process_log import ProcessLogWindow
|
||||||
|
|
||||||
|
|
||||||
class ProfileButton(QFrame):
|
class ProfileButton(QFrame):
|
||||||
@@ -61,7 +65,7 @@ class ProfileButton(QFrame):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
self.setProperty("profile_id", profile.id)
|
self.setProperty("profile_id", profile.id)
|
||||||
self.name_label.setText(profile.name)
|
self.name_label.setText(profile.display_name)
|
||||||
self.version_label.setText(profile.version_id)
|
self.version_label.setText(profile.version_id)
|
||||||
|
|
||||||
icon_path = profile.icon
|
icon_path = profile.icon
|
||||||
@@ -103,7 +107,9 @@ class ProfileListItem(QPushButton):
|
|||||||
|
|
||||||
text_layout = QVBoxLayout()
|
text_layout = QVBoxLayout()
|
||||||
text_layout.setSpacing(1)
|
text_layout.setSpacing(1)
|
||||||
name_label = QLabel(profile.name)
|
|
||||||
|
name_label = QLabel(profile.display_name)
|
||||||
|
|
||||||
name_label.setObjectName("itemName")
|
name_label.setObjectName("itemName")
|
||||||
version_label = QLabel(profile.version_id)
|
version_label = QLabel(profile.version_id)
|
||||||
version_label.setObjectName("itemVersion")
|
version_label.setObjectName("itemVersion")
|
||||||
@@ -180,7 +186,11 @@ class LaunchProfilePage(QWidget):
|
|||||||
def __init__(self, app, parent=None):
|
def __init__(self, app, parent=None):
|
||||||
super(LaunchProfilePage, self).__init__(parent)
|
super(LaunchProfilePage, self).__init__(parent)
|
||||||
self.app = app
|
self.app = app
|
||||||
|
self.widget_mgr: WidgetManager = app.widget_manager
|
||||||
self.profile_manager = app.profile_manager
|
self.profile_manager = app.profile_manager
|
||||||
|
self.game_manager = app.game_manager
|
||||||
|
self.launch_manager: LaunchManager = self.game_manager.launch
|
||||||
|
self.process_log_windows: list[ProcessLogWindow] = []
|
||||||
|
|
||||||
# Layout
|
# Layout
|
||||||
self.layout = QVBoxLayout(self)
|
self.layout = QVBoxLayout(self)
|
||||||
@@ -354,4 +364,49 @@ class LaunchProfilePage(QWidget):
|
|||||||
|
|
||||||
def launch_profile(self) -> None:
|
def launch_profile(self) -> None:
|
||||||
profile_id = self.profile_button.property("profile_id")
|
profile_id = self.profile_button.property("profile_id")
|
||||||
self.status_label.setText(f"Preparing to launch...")
|
self.status_label.setText("Preparing to launch...")
|
||||||
|
launch_profile = self.app.profile_manager.get_launch_profile(profile_id)
|
||||||
|
|
||||||
|
if launch_profile is None:
|
||||||
|
self.status_label.setText("Unable to load the selected profile.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.launch_manager.is_profile_running(launch_profile.profile_id):
|
||||||
|
request = AskForRequest(
|
||||||
|
"Launch Request",
|
||||||
|
"Target profile {} is running. Are you sure you want to launch it again?".format(
|
||||||
|
launch_profile.display_name
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.widget_mgr.signal.askyesno.emit(request)
|
||||||
|
|
||||||
|
if not request.wait(60):
|
||||||
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
|
"Time out waiting for request for launch."
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not request.result() is True:
|
||||||
|
return
|
||||||
|
|
||||||
|
result = self.launch_manager.launch(launch_profile)
|
||||||
|
|
||||||
|
if result.success:
|
||||||
|
self.status_label.setText("Launched!")
|
||||||
|
log_window = ProcessLogWindow(
|
||||||
|
result.process,
|
||||||
|
launch_profile.profile_id,
|
||||||
|
title=f"Minecraft Log - {launch_profile.display_name} ({launch_profile.version_id})",
|
||||||
|
parent=None,
|
||||||
|
)
|
||||||
|
for warning in result.warnings:
|
||||||
|
log_window.append_message(f"[launcher warning] {warning}")
|
||||||
|
self.process_log_windows.append(log_window)
|
||||||
|
log_window.show()
|
||||||
|
else:
|
||||||
|
self.widget_mgr.signal.show_error_message.emit(
|
||||||
|
"Unable to launch profile {}: {}".format(
|
||||||
|
launch_profile.display_name,
|
||||||
|
result.error_message,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
from collections import deque
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
from PySide6.QtCore import QObject, Signal
|
||||||
|
from PySide6.QtGui import QCloseEvent
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QMainWindow,
|
||||||
|
QPlainTextEdit,
|
||||||
|
QPushButton,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
)
|
||||||
|
|
||||||
|
from core_lib.qt import messagebox
|
||||||
|
|
||||||
|
|
||||||
|
class _ProcessSignals(QObject):
|
||||||
|
output = Signal(str)
|
||||||
|
# Windows may return an unsigned 32-bit exit code (for example
|
||||||
|
# 0xFFFFFFFF), which does not fit in Qt's signed Signal(int).
|
||||||
|
finished = Signal(object)
|
||||||
|
read_failed = Signal(str)
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessLogWindow(QMainWindow):
|
||||||
|
"""
|
||||||
|
A Simple client log view
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, process: subprocess.Popen, profile_id: str, title: str = "Game Log",
|
||||||
|
parent: QWidget | None = None, finished_callback: Callable[[str, int], None]=None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self.process = process
|
||||||
|
self.profile_id = profile_id
|
||||||
|
self._signals = _ProcessSignals(self)
|
||||||
|
self._last_error_message: str | None = None
|
||||||
|
self._stderr_lines: deque[str] = deque(maxlen=500)
|
||||||
|
|
||||||
|
self.setWindowTitle(title)
|
||||||
|
self.resize(700, 720)
|
||||||
|
|
||||||
|
container = QWidget(self)
|
||||||
|
layout = QVBoxLayout(container)
|
||||||
|
controls = QHBoxLayout()
|
||||||
|
|
||||||
|
self.status_label = QLabel(f"Running (PID {process.pid})")
|
||||||
|
self.kill_button = QPushButton("Kill Process")
|
||||||
|
self.kill_button.clicked.connect(self.kill_process)
|
||||||
|
|
||||||
|
self.log_output = QPlainTextEdit()
|
||||||
|
self.log_output.setReadOnly(True)
|
||||||
|
self.log_output.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
|
||||||
|
self.log_output.setMaximumBlockCount(10000)
|
||||||
|
|
||||||
|
controls.addWidget(self.status_label)
|
||||||
|
controls.addStretch()
|
||||||
|
controls.addWidget(self.kill_button)
|
||||||
|
layout.addLayout(controls)
|
||||||
|
layout.addWidget(self.log_output)
|
||||||
|
self.setCentralWidget(container)
|
||||||
|
|
||||||
|
self._signals.output.connect(self.log_output.appendPlainText)
|
||||||
|
self._signals.finished.connect(self._process_finished)
|
||||||
|
self._signals.read_failed.connect(self._read_failed)
|
||||||
|
|
||||||
|
self.finished_callback = finished_callback
|
||||||
|
|
||||||
|
self._stdout_reader = threading.Thread(
|
||||||
|
target=self._read_stdout,
|
||||||
|
name=f"process-stdout-{process.pid}",
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
self._stderr_reader = threading.Thread(
|
||||||
|
target=self._read_stderr,
|
||||||
|
name=f"process-stderr-{process.pid}",
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
self._waiter = threading.Thread(
|
||||||
|
target=self._wait_for_process,
|
||||||
|
name=f"process-wait-{process.pid}",
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
self._stdout_reader.start()
|
||||||
|
self._stderr_reader.start()
|
||||||
|
self._waiter.start()
|
||||||
|
|
||||||
|
def append_message(self, message: str) -> None:
|
||||||
|
self.log_output.appendPlainText(message)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_error_message(self) -> str | None:
|
||||||
|
return self._last_error_message
|
||||||
|
|
||||||
|
def _read_stdout(self) -> None:
|
||||||
|
try:
|
||||||
|
if self.process.stdout is not None:
|
||||||
|
for line in self.process.stdout:
|
||||||
|
self._signals.output.emit(line.rstrip("\r\n"))
|
||||||
|
except Exception as exc:
|
||||||
|
self._signals.read_failed.emit(str(exc))
|
||||||
|
|
||||||
|
def _read_stderr(self) -> None:
|
||||||
|
try:
|
||||||
|
if self.process.stderr is not None:
|
||||||
|
for line in self.process.stderr:
|
||||||
|
message = line.rstrip("\r\n").strip()
|
||||||
|
if message:
|
||||||
|
self._last_error_message = message
|
||||||
|
self._stderr_lines.append(message)
|
||||||
|
except Exception as exc:
|
||||||
|
self._signals.read_failed.emit(str(exc))
|
||||||
|
|
||||||
|
def _wait_for_process(self) -> None:
|
||||||
|
exit_code = self._normalize_exit_code(self.process.wait())
|
||||||
|
self._stdout_reader.join()
|
||||||
|
self._stderr_reader.join()
|
||||||
|
self._signals.finished.emit(exit_code)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_exit_code(exit_code: int) -> int:
|
||||||
|
# Avoid some system (WINDOWS) give exit code that overflow in 32bit integer
|
||||||
|
if exit_code > 0x7FFFFFFF:
|
||||||
|
return exit_code - 0x100000000
|
||||||
|
return exit_code
|
||||||
|
|
||||||
|
def kill_process(self) -> None:
|
||||||
|
if self.process.poll() is not None:
|
||||||
|
self._process_finished(
|
||||||
|
self._normalize_exit_code(self.process.returncode or 0)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.kill_button.setEnabled(False)
|
||||||
|
self.status_label.setText("Killing process...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.process.kill()
|
||||||
|
except Exception as exc:
|
||||||
|
self.kill_button.setEnabled(True)
|
||||||
|
self.status_label.setText("Unable to kill process")
|
||||||
|
self.append_message(f"[launcher] Unable to kill process: {exc}")
|
||||||
|
|
||||||
|
def _process_finished(self, exit_code: int) -> None:
|
||||||
|
self.kill_button.setEnabled(False)
|
||||||
|
self.status_label.setText(f"Process exited with code {exit_code}")
|
||||||
|
self.append_message(f"[launcher] Process exited with code {exit_code}.")
|
||||||
|
|
||||||
|
if exit_code != 0 and self._last_error_message:
|
||||||
|
self.append_message(
|
||||||
|
f"[launcher] Last error: {self._last_error_message}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if exit_code != 0:
|
||||||
|
error_content = "\n".join(self._stderr_lines)
|
||||||
|
if not error_content:
|
||||||
|
error_content = (
|
||||||
|
"The client did not write an error message to stderr.\n"
|
||||||
|
f"Process exit code: {exit_code}"
|
||||||
|
)
|
||||||
|
|
||||||
|
messagebox.error_with_plain_text(
|
||||||
|
self,
|
||||||
|
title="Minecraft Client Error",
|
||||||
|
message=f"Minecraft exited with code {exit_code}.",
|
||||||
|
content=error_content,
|
||||||
|
)
|
||||||
|
|
||||||
|
if callable(self.finished_callback):
|
||||||
|
self.finished_callback(self.profile_id, exit_code)
|
||||||
|
|
||||||
|
def _read_failed(self, message: str) -> None:
|
||||||
|
self.append_message(f"[launcher] Unable to read process output: {message}")
|
||||||
|
if self.process.poll() is not None:
|
||||||
|
self._process_finished(
|
||||||
|
self._normalize_exit_code(self.process.returncode or 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
def closeEvent(self, event: QCloseEvent) -> None:
|
||||||
|
# Closing the log window must not stop the game. The process remains
|
||||||
|
# available through this window instance and can be shown again.
|
||||||
|
event.accept()
|
||||||
@@ -906,93 +906,3 @@ class TestPage(QWidget):
|
|||||||
|
|
||||||
return False, pending
|
return False, pending
|
||||||
|
|
||||||
#
|
|
||||||
# JVM Management
|
|
||||||
#
|
|
||||||
@property
|
|
||||||
def jvm_settings_path(self) -> Path:
|
|
||||||
return Path(self.app.work_dir) / "jvms.json"
|
|
||||||
|
|
||||||
def _find_and_save_jvm_search_result(self):
|
|
||||||
installations, errors = find_java_installations()
|
|
||||||
if not installations:
|
|
||||||
self.widget_mgr.signal.show_warning_message.emit(
|
|
||||||
"There are no Java installations available in you system.\n"
|
|
||||||
)
|
|
||||||
return []
|
|
||||||
|
|
||||||
if errors:
|
|
||||||
request = AskForRequest("Java Virtual Machine Finder", message = (
|
|
||||||
"Unable to detect below java installation version:\n"
|
|
||||||
))
|
|
||||||
request.details = "\n".join(path.as_posix() for path in errors)
|
|
||||||
request.use_plain_text = True
|
|
||||||
|
|
||||||
self.widget_mgr.signal.askyesno.emit(request)
|
|
||||||
|
|
||||||
self.jvm_settings_path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
try:
|
|
||||||
with self.jvm_settings_path.open("w") as file:
|
|
||||||
json.dump(installations, file, indent=4)
|
|
||||||
|
|
||||||
return installations
|
|
||||||
except Exception as e:
|
|
||||||
self.widget_mgr.signal.show_and_print_error_message.emit(
|
|
||||||
{
|
|
||||||
"error": traceback.format_exception(e),
|
|
||||||
"title": "Settings Error",
|
|
||||||
"message": "An error occurred while attempting to save jvm settings. Please try again.",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
def _read_exist_jvm_settings(self):
|
|
||||||
try:
|
|
||||||
return json.loads(self.jvm_settings_path.read_text())
|
|
||||||
except Exception as e:
|
|
||||||
self.widget_mgr.signal.show_and_print_error_message.emit(
|
|
||||||
{
|
|
||||||
"error": traceback.format_exception(e),
|
|
||||||
"title": "Settings Error",
|
|
||||||
"message": "An error occurred while reading jvm settings.",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_or_cache_jvm_settings(self) -> list:
|
|
||||||
if self.jvm_settings_path.exists():
|
|
||||||
exists = self._read_exist_jvm_settings()
|
|
||||||
|
|
||||||
if exists:
|
|
||||||
return exists
|
|
||||||
|
|
||||||
return self._find_and_save_jvm_search_result()
|
|
||||||
|
|
||||||
def get_support_runtime_jvm_executable(self, major_version: int | str, no_error: bool=False) -> str | None:
|
|
||||||
result = self.get_or_cache_jvm_settings()
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
return None
|
|
||||||
|
|
||||||
for item in result:
|
|
||||||
version = item.get("major", None)
|
|
||||||
executable = item.get("executable", None)
|
|
||||||
|
|
||||||
try:
|
|
||||||
version = str(version)
|
|
||||||
major_version = str(major_version)
|
|
||||||
except Exception as e:
|
|
||||||
self.app.logger.warning(
|
|
||||||
f"Unable to convert version {version} to string: {e}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if major_version == version and executable:
|
|
||||||
return executable
|
|
||||||
|
|
||||||
if not no_error:
|
|
||||||
self.widget_mgr.signal.show_error_message.emit(
|
|
||||||
"No Java executable found for major version {}.".format(major_version)
|
|
||||||
)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user