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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 38 additions & 5 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -999,7 +999,7 @@ def download_file(url, dest_path):

# ═══════════════════════════════════════════════════════════════════════════════
# § 9 MSBUILD CANDIDATE DETECTION (unchanged)
# ══════════════════════════════════════════════════════════════════════════════
# ═══════════════════════���═══════════════════════════════════════════════════════


def _add_msbuild_candidate(candidates, path, priority):
Expand Down Expand Up @@ -2239,7 +2239,10 @@ def run_cli_launcher():
# ═══════════════════════════════════════════════════════════════════════════════


def run_inprocess_launcher(memory_resident: bool = False):
def run_inprocess_launcher(
memory_resident: bool = False,
portablemc_inmemory: bool = False,
):
"""
Memory-resident JVM launcher — the entire game runs inside this process.

Expand All @@ -2263,12 +2266,27 @@ def run_inprocess_launcher(memory_resident: bool = False):
Minecraft runs. This call blocks until the game exits.

No .exe files are spawned at any point in this path.

Parameters
----------
memory_resident : bool
If True, load jvm.dll into memory using pythonmemorymodule instead
of the normal OS loader path.
portablemc_inmemory : bool
If True, load the _portablemc.pyd extension in-memory using
pythonmemorymodule. This is useful for environments with strict
.exe/.dll execution policies.
"""
print("\n" + "=" * 62)
env_memory_resident = (
os.environ.get("LAUNCHER_MEMORY_JVM", "").strip().lower() in TRUTHY_ENV_VALUES
)
env_portablemc_inmemory = (
os.environ.get("LAUNCHER_PORTABLEMC_INMEMORY", "").strip().lower()
in TRUTHY_ENV_VALUES
)
memory_resident = bool(memory_resident or env_memory_resident)
portablemc_inmemory = bool(portablemc_inmemory or env_portablemc_inmemory)
mode_name = "inprocess_memory" if memory_resident else "inprocess_jpype"
effective_jdk_bin = resolve_effective_launcher_jdk_bin(memory_resident=memory_resident)

Expand Down Expand Up @@ -2357,7 +2375,8 @@ def _enable_inprocess_crash_log() -> None:
print(f" server : {DEFAULT_SERVER_IP}")
print(f" jdk_bin : {effective_jdk_bin}")
print(f" main_dir : {BASE_DIR}")
print(f" loader : {'memory-resident' if memory_resident else 'os-loader'}")
print(f" jvm_load : {'memory-resident' if memory_resident else 'os-loader'}")
print(f" pmc_load : {'in-memory' if portablemc_inmemory else 'standard'}")
print()

# Build extra JVM flags: DEFAULT_JVM_OPTS tokens + auto-join server flag.
Expand Down Expand Up @@ -2388,6 +2407,7 @@ def _enable_inprocess_crash_log() -> None:
in TRUTHY_ENV_VALUES
),
memory_resident=memory_resident,
portablemc_inmemory=portablemc_inmemory,
)
print("\n✅ Minecraft exited normally.")
return True
Expand All @@ -2409,6 +2429,7 @@ def _enable_inprocess_crash_log() -> None:
"version": INPROCESS_VERSION,
"username": DEFAULT_USERNAME,
"memory_resident": memory_resident,
"portablemc_inmemory": portablemc_inmemory,
},
)
if dump:
Expand Down Expand Up @@ -2528,12 +2549,24 @@ def main():
elif choice == "4":
update_launcher_state(last_run_mode="inprocess_jpype")
os.environ.pop("LAUNCHER_MEMORY_JVM", None)
success = run_inprocess_launcher(memory_resident=False)
# Enable portablemc in-memory loading for environments with strict
# .exe/.dll execution policies (can be toggled via env var)
pmc_inmem = (
os.environ.get("LAUNCHER_PORTABLEMC_INMEMORY", "").strip().lower()
in TRUTHY_ENV_VALUES
)
success = run_inprocess_launcher(
memory_resident=False, portablemc_inmemory=pmc_inmem
)

elif choice == "5":
update_launcher_state(last_run_mode="inprocess_memory")
os.environ["LAUNCHER_MEMORY_JVM"] = "1"
success = run_inprocess_launcher(memory_resident=True)
# In memory-resident mode, always enable portablemc in-memory loading
# as this mode is specifically for environments with strict exec policies
success = run_inprocess_launcher(
memory_resident=True, portablemc_inmemory=True
)

elif choice == "d":
run_debug_menu()
Expand Down
138 changes: 128 additions & 10 deletions scripts/jvm_boot_glue.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""
"""
jvm_boot_glue.py
════════════════
Bridges the memory-resident JVM into JPype and launches Minecraft in-process.
Expand Down Expand Up @@ -58,6 +58,73 @@
_SCRIPTS_DIR = Path(__file__).resolve().parent
_PMC_INTERCEPT_PY = _SCRIPTS_DIR / "pmc_intercept.py"

# ── portablemc 5.x environment bootstrap ──────────────────────────────────
# This MUST be called before any portablemc import to ensure the vendored
# copy is used and (optionally) the native extension is loaded in-memory.
_PMC_BOOTSTRAP_COMPLETE = False
_PMC_INMEMORY_MODE = False


def _ensure_portablemc_environment(*, inmemory: bool = False) -> bool:
"""
Ensure the portablemc 5.x environment is properly initialized.

This function:
1. Adds the vendored portablemc tree to sys.path
2. Optionally loads the _portablemc.pyd in-memory (for strict exec policies)
3. Scrubs any pip-installed portablemc to avoid version conflicts

Parameters
----------
inmemory : bool
If True, attempt to load _portablemc.pyd via pythonmemorymodule
instead of the normal LoadLibrary path.

Returns True if the environment is ready for portablemc imports.
"""
global _PMC_BOOTSTRAP_COMPLETE, _PMC_INMEMORY_MODE

if _PMC_BOOTSTRAP_COMPLETE:
return True

# Ensure scripts dir is on path for local_portablemc import
scripts_str = str(_SCRIPTS_DIR)
if scripts_str not in sys.path:
sys.path.insert(0, scripts_str)

try:
from local_portablemc import bootstrap, bootstrap_inmemory

if inmemory:
log.info("Bootstrapping portablemc 5.x in in-memory mode")
result = bootstrap_inmemory(scripts_dir=_SCRIPTS_DIR)
_PMC_INMEMORY_MODE = result
_PMC_BOOTSTRAP_COMPLETE = result
if result:
log.info("portablemc 5.x in-memory bootstrap successful")
else:
log.warning(
"In-memory bootstrap failed; falling back to standard mode"
)
# Fall back to standard bootstrap
_PMC_BOOTSTRAP_COMPLETE = bootstrap(scripts_dir=_SCRIPTS_DIR)
else:
_PMC_BOOTSTRAP_COMPLETE = bootstrap(scripts_dir=_SCRIPTS_DIR)

return _PMC_BOOTSTRAP_COMPLETE

except ImportError as exc:
log.warning("Could not import local_portablemc: %s", exc)
return False
except Exception as exc:
log.error("portablemc environment bootstrap failed: %s", exc)
return False


def is_portablemc_inmemory() -> bool:
"""Check if portablemc is running in in-memory mode."""
return _PMC_INMEMORY_MODE


# ══════════════════════════════════════════════════════════════════════════════
# § 1 Process-level utilities
Expand Down Expand Up @@ -202,7 +269,7 @@ def _wide_path_arg_to_str(path) -> str:
return ""


# ═════════════════════════════════════════════════════════════════════════════
# ═══════════════════════════════════════��══════════════════════════════════════
# § 3 ctypes WINFUNCTYPE prototypes
# ══════════════════════════════════════════════════════════════════════════════

Expand Down Expand Up @@ -2158,17 +2225,56 @@ def __init__(

# ── public ────────────────────────────────────────────────────────────

def resolve(self) -> tuple[list[str], list[str], str, list[str]]:
"""Run the adapter and return (jvm_flags, classpath, main_class, game_args)."""
def resolve(
self, *, inmemory: bool = False
) -> tuple[list[str], list[str], str, list[str]]:
"""
Run the adapter and return (jvm_flags, classpath, main_class, game_args).

Parameters
----------
inmemory : bool
If True, attempt to load portablemc's native extension in-memory
using pythonmemorymodule. This is useful for environments with
strict .exe/.dll execution policies.
"""
# Step 1: Ensure portablemc environment is initialized
if not _ensure_portablemc_environment(inmemory=inmemory):
raise RuntimeError(
"portablemc 5.x environment initialization failed.\n"
"Ensure the vendored portablemc tree exists at:\n"
f" {_SCRIPTS_DIR / 'portablemc' / 'portablemc-py' / 'python'}\n"
"Or ensure the wheel is available at:\n"
f" {_SCRIPTS_DIR / 'portablemc' / 'target' / 'wheels'}"
)

# Step 2: Try the Python API
try:
result = self._via_python_api()
log.info("portablemc resolved via: Python API")
mode = "in-memory" if is_portablemc_inmemory() else "standard"
log.info("portablemc resolved via: Python API (%s mode)", mode)
return result
except ImportError as exc:
# More specific error for import failures
raise RuntimeError(
f"portablemc 5.x module import failed: {exc}\n"
"The native extension (_portablemc.pyd) may not be compatible "
"with this Python version.\n"
"Expected location: "
f"{_SCRIPTS_DIR / 'portablemc' / 'portablemc-py' / 'python' / 'portablemc'}"
) from exc
except AttributeError as exc:
# API mismatch - wrong portablemc version
raise RuntimeError(
f"portablemc API mismatch: {exc}\n"
"This launcher requires portablemc 5.x with PyO3 bindings.\n"
"Ensure you have the correct version of the native extension."
) from exc
except Exception as exc:
raise RuntimeError(
"portablemc 5.x API resolution failed.\n"
"Ensure portablemc is importable in this interpreter "
"(Windows: py -m pip install portablemc)."
f"portablemc 5.x API resolution failed: {type(exc).__name__}: {exc}\n"
"Check that the vendored portablemc installation is complete "
"and the native extension is compatible with this Python version."
) from exc

# ── path 1: portablemc 5.x PyO3 API ──────────────────────────────────
Expand Down Expand Up @@ -2280,7 +2386,7 @@ def _via_python_api(self) -> tuple:
log.warning("Could not auto-detect JDK from Python API launch args")
return _ArgSplitter.split(full_args[1:])

# ── static helpers ────────────────────────────────────────────────────
# ── static helpers ──────────────────────────────────────��──────────────

@staticmethod
def _extract_args(jvm_args, game_args, main_class):
Expand Down Expand Up @@ -2474,6 +2580,7 @@ def launch_minecraft(
extra_game_args: list[str] | None = None,
debug: bool = False,
memory_resident: bool = False,
portablemc_inmemory: bool = False,
) -> None:
"""
Full in-process Minecraft launcher. No java.exe is spawned.
Expand All @@ -2484,6 +2591,14 @@ def launch_minecraft(
memory_resident=True (option 5)
jvm.dll and all JDK deps mapped into RAM via JvmMemoryLoader.
jpype.startJVM intercepted via IAT hooks.

portablemc_inmemory=True
Load the _portablemc.pyd extension in-memory using pythonmemorymodule
instead of the normal LoadLibrary path. This is useful for environments
with strict .exe/.dll execution policies.

Note: subprocess/os.system/external process calls are NOT used.
Everything runs within the current Python process.
"""
logging.basicConfig(
level=logging.DEBUG if debug else logging.INFO,
Expand All @@ -2497,6 +2612,7 @@ def launch_minecraft(
# causes HotSpot's CRT bootstrap to abort with
# "Failed setting boot class path" or a segfault inside JNI_CreateJavaVM.
log.info("Resolving Minecraft version: %r", version)
log.info("portablemc mode: %s", "in-memory" if portablemc_inmemory else "standard")
adapter = PortableMCGameAdapter(
main_dir=main_dir,
version=version,
Expand All @@ -2505,7 +2621,9 @@ def launch_minecraft(
access_token=access_token,
extra_jvm_flags=extra_jvm_flags,
)
jvm_flags, classpath, main_class, game_args = adapter.resolve()
jvm_flags, classpath, main_class, game_args = adapter.resolve(
inmemory=portablemc_inmemory
)

# Always prefer portablemc's auto-detected JDK (e.g. java-runtime-epsilon)
# over the caller's explicit jdk_bin. For option 5 the memory loader MUST
Expand Down
Loading
Loading