UPDATE
Add Linux and macOS support, batch class transformations with a disk cache, and add cross-platform release packaging. Also improve runtime namespace handling, keybind editing, and startup behavior.
This commit is contained in:
1 parent
910a126f04
commit
38ff6d387c
29 files changed
+1971
-228
No files matched your search
@@ -0,0 +1,86 @@
|
|||||||
|
name: Injector
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "injector/**"
|
||||||
|
- "src/**"
|
||||||
|
- ".github/workflows/injector.yml"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
payload:
|
||||||
|
name: Payload jar
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: temurin
|
||||||
|
java-version: "21"
|
||||||
|
- name: Build remapped jar
|
||||||
|
run: |
|
||||||
|
chmod +x ./gradlew
|
||||||
|
./gradlew remapJar --console=plain
|
||||||
|
- name: Upload jar
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: payload-jar # also the Fabric mod: drop prestige-*.jar into mods/
|
||||||
|
path: |
|
||||||
|
build/libs/prestige-*.jar
|
||||||
|
!build/libs/*-dev.jar
|
||||||
|
!build/libs/*-sources.jar
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
injector:
|
||||||
|
name: Injector (${{ matrix.target }})
|
||||||
|
needs: payload
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: windows-latest
|
||||||
|
target: x86_64-pc-windows-msvc
|
||||||
|
name: windows-x86_64
|
||||||
|
files: |
|
||||||
|
injector/target/release/PrestigeInjector.exe
|
||||||
|
injector/target/release/prestige_native.dll
|
||||||
|
- os: ubuntu-latest
|
||||||
|
target: x86_64-unknown-linux-gnu
|
||||||
|
name: linux-x86_64
|
||||||
|
files: |
|
||||||
|
injector/target/release/PrestigeInjector
|
||||||
|
injector/target/release/libprestige_native.so
|
||||||
|
- os: macos-13
|
||||||
|
target: x86_64-apple-darwin
|
||||||
|
name: macos-x86_64
|
||||||
|
files: |
|
||||||
|
injector/target/release/PrestigeInjector
|
||||||
|
injector/target/release/libprestige_native.dylib
|
||||||
|
- os: macos-14
|
||||||
|
target: aarch64-apple-darwin
|
||||||
|
name: macos-aarch64
|
||||||
|
files: |
|
||||||
|
injector/target/release/PrestigeInjector
|
||||||
|
injector/target/release/libprestige_native.dylib
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: ${{ matrix.target }}
|
||||||
|
- name: Download payload jar
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: payload-jar
|
||||||
|
path: build/libs
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --release --manifest-path injector/Cargo.toml
|
||||||
|
- name: Test
|
||||||
|
run: cargo test --manifest-path injector/Cargo.toml
|
||||||
|
- name: Upload injector
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: injector-${{ matrix.name }}
|
||||||
|
path: ${{ matrix.files }}
|
||||||
|
if-no-files-found: error
|
||||||
@@ -18,3 +18,4 @@ openvape.zip
|
|||||||
|
|
||||||
/dist/
|
/dist/
|
||||||
/injector/target/
|
/injector/target/
|
||||||
|
/tools
|
||||||
@@ -18,7 +18,7 @@ A semi deobfuscated, IntelliJ-ready, runnable open sourced version of the Presti
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
Cracked by Skid Solutions.
|
Cracked by Skid Solutions.
|
||||||
Join the Discord: https://discord.gg/skidsolutions
|
Join the Discord: https://discord.gg/x2FJyGrDNg
|
||||||
|
|
||||||
## Why?
|
## Why?
|
||||||
zPrestige couldnt give a fuck it seems about his client. AI slop all over, no time nor thought it seems goes into testing if things actually work. banned in 2 games with the most popular CAS config on mcpvp with countless modules flagging grim on actest.
|
zPrestige couldnt give a fuck it seems about his client. AI slop all over, no time nor thought it seems goes into testing if things actually work. banned in 2 games with the most popular CAS config on mcpvp with countless modules flagging grim on actest.
|
||||||
|
|||||||
+2
-1
@@ -10,4 +10,5 @@ cargo build --release --manifest-path injector\Cargo.toml
|
|||||||
if ($LASTEXITCODE -ne 0) { throw 'cargo build failed' }
|
if ($LASTEXITCODE -ne 0) { throw 'cargo build failed' }
|
||||||
New-Item -ItemType Directory -Force dist | Out-Null
|
New-Item -ItemType Directory -Force dist | Out-Null
|
||||||
Copy-Item injector\target\release\PrestigeInjector.exe, injector\target\release\prestige_native.dll dist\ -Force
|
Copy-Item injector\target\release\PrestigeInjector.exe, injector\target\release\prestige_native.dll dist\ -Force
|
||||||
Write-Host 'Done: dist\PrestigeInjector.exe + dist\prestige_native.dll'
|
Get-ChildItem build\libs\prestige-*.jar | Where-Object { $_.Name -notmatch '-(dev|sources)\.jar$' } | Copy-Item -Destination dist\ -Force
|
||||||
|
Write-Host 'Done: dist\PrestigeInjector.exe + dist\prestige_native.dll + dist\prestige-*.jar (Fabric mod)'
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Builds the Prestige payload jar (remapped for the production Fabric runtime), then the Rust injector and loader
|
||||||
|
# library, and collects PrestigeInjector + libprestige_native.{so,dylib} in dist/.
|
||||||
|
# Optional: set JAVA_HOME to pick the JDK used by Gradle.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux) LIB=libprestige_native.so ;;
|
||||||
|
Darwin) LIB=libprestige_native.dylib ;;
|
||||||
|
*) echo "unsupported OS: $(uname -s) (use build-injector.ps1 on Windows)" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -n "${JAVA_HOME:-}" ]; then
|
||||||
|
export JAVA_HOME
|
||||||
|
PATH="$JAVA_HOME/bin:$PATH"
|
||||||
|
export PATH
|
||||||
|
fi
|
||||||
|
|
||||||
|
./gradlew remapJar --console=plain
|
||||||
|
cargo build --release --manifest-path injector/Cargo.toml
|
||||||
|
|
||||||
|
mkdir -p dist
|
||||||
|
cp -f "injector/target/release/PrestigeInjector" "injector/target/release/$LIB" dist/
|
||||||
|
chmod +x dist/PrestigeInjector
|
||||||
|
|
||||||
|
# the same remapped jar is the Fabric mod (drop it into mods/)
|
||||||
|
for jar in build/libs/prestige-*.jar; do
|
||||||
|
case "$jar" in *-dev.jar|*-sources.jar) continue ;; esac
|
||||||
|
cp -f "$jar" dist/
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "Done: dist/PrestigeInjector + dist/$LIB + dist/prestige-*.jar (Fabric mod)"
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
@echo off
|
||||||
|
rem Builds release zips for every platform into dist\release\ (run on Windows):
|
||||||
|
rem prestige-<ver>-windows-x86_64.zip PrestigeInjector.exe + prestige_native.dll + mod jar
|
||||||
|
rem prestige-<ver>-linux-x86_64.zip PrestigeInjector + libprestige_native.so + mod jar (glibc 2.17+, any distro)
|
||||||
|
rem prestige-<ver>-macos-universal.zip PrestigeInjector + libprestige_native.dylib + mod jar (Intel + Apple Silicon)
|
||||||
|
rem prestige-<ver>.jar the Fabric mod on its own
|
||||||
|
rem SHA256SUMS.txt
|
||||||
|
rem Linux and macOS are cross-compiled with cargo-zigbuild + zig; both are installed on first run if missing.
|
||||||
|
rem Requires: Rust (rustup), Python 3, JDK 21.
|
||||||
|
setlocal EnableExtensions
|
||||||
|
cd /d "%~dp0"
|
||||||
|
set "SELF=%~f0"
|
||||||
|
|
||||||
|
set "OUT=%CD%\dist\release"
|
||||||
|
if exist "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot" set "JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
|
||||||
|
|
||||||
|
for /f "tokens=1,* delims==" %%a in ('findstr /b /c:"mod_version=" gradle.properties') do set "VERSION=%%b"
|
||||||
|
if not defined VERSION set "VERSION=dev"
|
||||||
|
echo === Prestige %VERSION% release build
|
||||||
|
|
||||||
|
rem ---- tools -------------------------------------------------------------
|
||||||
|
where cargo >nul 2>&1 || (echo cargo not found: install Rust from https://rustup.rs & goto :fail)
|
||||||
|
where python >nul 2>&1 || (echo python not found: install Python 3 from https://python.org & goto :fail)
|
||||||
|
python -m ziglang version >nul 2>&1 || (
|
||||||
|
echo === Installing zig ^(pip ziglang^)
|
||||||
|
python -m pip install --user ziglang || goto :fail
|
||||||
|
)
|
||||||
|
where cargo-zigbuild >nul 2>&1 || (
|
||||||
|
echo === Installing cargo-zigbuild
|
||||||
|
cargo install --locked cargo-zigbuild || goto :fail
|
||||||
|
)
|
||||||
|
rustup target add x86_64-pc-windows-msvc x86_64-unknown-linux-gnu x86_64-apple-darwin aarch64-apple-darwin || goto :fail
|
||||||
|
|
||||||
|
rem zigbuild's linker wrappers break on paths with spaces, so keep its cache on the 8.3 short path
|
||||||
|
if not exist "%LOCALAPPDATA%\cargo-zigbuild" mkdir "%LOCALAPPDATA%\cargo-zigbuild"
|
||||||
|
for %%I in ("%LOCALAPPDATA%\cargo-zigbuild") do set "CARGO_ZIGBUILD_CACHE_DIR=%%~sI"
|
||||||
|
|
||||||
|
rem ---- payload jar -------------------------------------------------------
|
||||||
|
echo === Building payload jar
|
||||||
|
call "%CD%\gradlew.bat" remapJar --console=plain || goto :fail
|
||||||
|
set "JAR="
|
||||||
|
for /f "delims=" %%j in ('dir /b /o-d "build\libs\prestige-*.jar" ^| findstr /v /i /c:"-dev.jar" /c:"-sources.jar"') do if not defined JAR set "JAR=%CD%\build\libs\%%j"
|
||||||
|
if not defined JAR (echo no remapped jar in build\libs & goto :fail)
|
||||||
|
rem every target embeds this exact jar
|
||||||
|
set "PRESTIGE_PAYLOAD=%JAR%"
|
||||||
|
|
||||||
|
if exist "%OUT%" rmdir /s /q "%OUT%"
|
||||||
|
mkdir "%OUT%" || goto :fail
|
||||||
|
|
||||||
|
rem ---- native builds -----------------------------------------------------
|
||||||
|
echo === Windows x86_64
|
||||||
|
cargo build --release --manifest-path injector\Cargo.toml --target x86_64-pc-windows-msvc || goto :fail
|
||||||
|
call :package windows-x86_64 x86_64-pc-windows-msvc PrestigeInjector.exe prestige_native.dll || goto :fail
|
||||||
|
|
||||||
|
echo === Linux x86_64 (glibc 2.17+)
|
||||||
|
cargo zigbuild --release --manifest-path injector\Cargo.toml --target x86_64-unknown-linux-gnu.2.17 || goto :fail
|
||||||
|
call :package linux-x86_64 x86_64-unknown-linux-gnu PrestigeInjector libprestige_native.so || goto :fail
|
||||||
|
|
||||||
|
echo === macOS universal (x86_64 + arm64)
|
||||||
|
cargo zigbuild --release --manifest-path injector\Cargo.toml --target universal2-apple-darwin || goto :fail
|
||||||
|
call :package macos-universal universal2-apple-darwin PrestigeInjector libprestige_native.dylib || goto :fail
|
||||||
|
|
||||||
|
copy /y "%JAR%" "%OUT%\" >nul || goto :fail
|
||||||
|
python scripts\package_release.py checksums "%OUT%" || goto :fail
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo === Done: %OUT%
|
||||||
|
dir /b "%OUT%"
|
||||||
|
call :maybe_pause
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
rem %1 platform name, %2 cargo target dir, %3 injector file, %4 library file
|
||||||
|
:package
|
||||||
|
python scripts\package_release.py zip "%OUT%" "%VERSION%" %1 "injector\target\%~2\release\%~3" "injector\target\%~2\release\%~4" "%JAR%"
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
|
|
||||||
|
:fail
|
||||||
|
echo.
|
||||||
|
echo *** Release build FAILED
|
||||||
|
call :maybe_pause
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
rem keep the window open when started by double-click
|
||||||
|
:maybe_pause
|
||||||
|
echo %cmdcmdline% | find /i "%SELF%" >nul && pause
|
||||||
|
exit /b 0
|
||||||
+4
-1
@@ -52,11 +52,14 @@ dependencies {
|
|||||||
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
|
modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}"
|
||||||
|
|
||||||
// Dear ImGui. The binding contains the Java classes, lwjgl3 the GL3/GLFW backend and
|
// Dear ImGui. The binding contains the Java classes, lwjgl3 the GL3/GLFW backend and
|
||||||
// natives-windows the imgui-java64.dll (extracted from the jar at runtime by imgui-java).
|
// natives-* the imgui-java64 libraries (extracted from the jar at runtime by imgui-java).
|
||||||
|
// Linux natives are x86_64 only; the macOS dylib is universal (x86_64 + arm64).
|
||||||
shade "io.github.spair:imgui-java-binding:${project.imgui_version}"
|
shade "io.github.spair:imgui-java-binding:${project.imgui_version}"
|
||||||
// imgui-java-lwjgl3 pulls LWJGL 3.4.1 transitively, which replaces Minecraft's 3.3.3 jars and breaks world saving
|
// imgui-java-lwjgl3 pulls LWJGL 3.4.1 transitively, which replaces Minecraft's 3.3.3 jars and breaks world saving
|
||||||
shade("io.github.spair:imgui-java-lwjgl3:${project.imgui_version}") { exclude group: 'org.lwjgl' }
|
shade("io.github.spair:imgui-java-lwjgl3:${project.imgui_version}") { exclude group: 'org.lwjgl' }
|
||||||
shade "io.github.spair:imgui-java-natives-windows:${project.imgui_version}"
|
shade "io.github.spair:imgui-java-natives-windows:${project.imgui_version}"
|
||||||
|
shade "io.github.spair:imgui-java-natives-linux:${project.imgui_version}"
|
||||||
|
shade "io.github.spair:imgui-java-natives-macos:${project.imgui_version}"
|
||||||
|
|
||||||
// ClassTransform (net.lenni0451, customised build); engine/EnginePlugin uses it to apply the client's @CTransformer hooks.
|
// ClassTransform (net.lenni0451, customised build); engine/EnginePlugin uses it to apply the client's @CTransformer hooks.
|
||||||
shade files('libs/classtransform-bundled.jar')
|
shade files('libs/classtransform-bundled.jar')
|
||||||
|
|||||||
+75
-11
@@ -1,15 +1,34 @@
|
|||||||
# Prestige injector (Rust)
|
# Prestige injector (Rust)
|
||||||
|
|
||||||
`PrestigeInjector.exe` loads `prestige_native.dll` into a running Minecraft **1.21.11 Fabric** JVM (64-bit, Fabric Loader 0.19.x,
|
`PrestigeInjector` loads the native loader library (`prestige_native.dll` / `libprestige_native.so` / `libprestige_native.dylib`)
|
||||||
Fabric API installed). It uses the same overall method as the openvape bundle, written from scratch in Rust with no external crates.
|
into a running Minecraft **1.21.11 Fabric** JVM (64-bit, Fabric Loader 0.19.x, Fabric API installed). It uses the same overall
|
||||||
|
method as the openvape bundle, written from scratch in Rust with no external crates.
|
||||||
|
|
||||||
|
## Supported platforms
|
||||||
|
|
||||||
|
| OS | Architectures | Injector binary | Loader library |
|
||||||
|
|---------|--------------------|-----------------------|-----------------------------|
|
||||||
|
| Windows | x86_64 | `PrestigeInjector.exe`| `prestige_native.dll` |
|
||||||
|
| Linux | x86_64 | `PrestigeInjector` | `libprestige_native.so` |
|
||||||
|
| macOS | x86_64, arm64 | `PrestigeInjector` | `libprestige_native.dylib` |
|
||||||
|
|
||||||
|
The injector and library must be built for the same architecture as the target JVM.
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
1. **Injector** lists visible `java.exe`/`javaw.exe` windows (refreshed every 750 ms), then runs `LoadLibraryW` in the chosen
|
1. **Injector** lists the running Fabric client JVMs, then loads the library into the chosen process.
|
||||||
process through `CreateRemoteThread` and confirms the module is mapped.
|
- **Windows:** lists visible `java.exe`/`javaw.exe` windows (refreshed every 750 ms), then runs `LoadLibraryW` in the chosen
|
||||||
2. **`prestige_native.dll`** (`DllMain` only spawns a worker thread):
|
process through `CreateRemoteThread` and confirms the module is mapped.
|
||||||
- waits for `jvm.dll`, `JNI_GetCreatedJavaVMs`, attaches, acquires the JVMTI redefine/retransform capabilities;
|
- **Linux / macOS:** uses the HotSpot attach protocol (the same mechanism as `jcmd <pid> JVMTI.agent_load`). The injector
|
||||||
- writes the embedded payload jar (the remapped `prestige-*.jar`) to `%TEMP%\PrestigeLoader`;
|
creates `.attach_pid<pid>` and sends `SIGQUIT` to the JVM, connects to its attach socket (`/tmp/.java_pid<pid>`, under
|
||||||
|
`$TMPDIR` on macOS) and asks it to load the library. The JVM `dlopen`s `libprestige_native` and calls its
|
||||||
|
`Agent_OnAttach` entry point. No ptrace, no root, no per-architecture shellcode.
|
||||||
|
Processes are found via `/proc` (Linux) or `ps` (macOS); there are no window titles, so the picker shows the pid and
|
||||||
|
instance name.
|
||||||
|
2. **The native library** only spawns a worker thread from its entry point (`DllMain` on Windows, `Agent_OnAttach` on
|
||||||
|
unix, which also hands over the `JavaVM*`). The worker:
|
||||||
|
- waits for the JVM (Windows: `jvm.dll`, `JNI_GetCreatedJavaVMs`), attaches, acquires the JVMTI redefine/retransform capabilities;
|
||||||
|
- writes the embedded payload jar (the remapped `prestige-*.jar`) to the temp directory under `PrestigeLoader`;
|
||||||
- `FabricLauncherBase.getLauncher().addToClassPath(jar)` so payload and game classes share the Knot class loader;
|
- `FabricLauncherBase.getLauncher().addToClassPath(jar)` so payload and game classes share the Knot class loader;
|
||||||
- registers `NativeBridge.classBytes` / `NativeBridge.redefine` and calls `Bootstrap.start()`.
|
- registers `NativeBridge.classBytes` / `NativeBridge.redefine` and calls `Bootstrap.start()`.
|
||||||
3. **`Bootstrap`** (Java) waits for the client window, then for every ClassTransform hook target gets the current class bytes
|
3. **`Bootstrap`** (Java) waits for the client window, then for every ClassTransform hook target gets the current class bytes
|
||||||
@@ -19,14 +38,59 @@ Fabric API installed). It uses the same overall method as the openvape bundle, w
|
|||||||
helper classes, call sites become `invokestatic`, and private member access goes through `Access` MethodHandles.
|
helper classes, call sites become `invokestatic`, and private member access goes through `Access` MethodHandles.
|
||||||
Finally the Prestige and ClickGUI entrypoints run on the client thread.
|
Finally the Prestige and ClickGUI entrypoints run on the client thread.
|
||||||
|
|
||||||
Log: `prestige-native.log` beside the DLL, plus `[Prestige-Inject]` lines in the game log.
|
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
|
The payload jar is embedded with `include_bytes!`, so each target needs its own build. Both scripts below build for the host OS only and run
|
||||||
|
`gradlew remapJar`, `cargo build --release`, and copy the results to `dist/`.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.\build-injector.ps1 # gradlew remapJar, cargo build --release, copies to dist\
|
.\build-injector.ps1 # Windows: dist\PrestigeInjector.exe + dist\prestige_native.dll
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./build-injector.sh # Linux/macOS: dist/PrestigeInjector + dist/libprestige_native.{so,dylib}
|
||||||
|
```
|
||||||
|
|
||||||
|
To build release zips for every platform from Windows, run `build-release.bat`. It cross-compiles Linux (x86_64,
|
||||||
|
glibc 2.17+, so one build covers every mainstream distro) and macOS (universal x86_64 + arm64) with `cargo-zigbuild`, installing
|
||||||
|
zig and cargo-zigbuild on first run, and writes `dist\release\prestige-<ver>-{windows-x86_64,linux-x86_64,macos-universal}.zip`,
|
||||||
|
the bare mod jar and `SHA256SUMS.txt`.
|
||||||
|
|
||||||
|
Set `JAVA_HOME` beforehand to choose the JDK for Gradle (`build-injector.sh` honours it). The GitHub Actions workflow
|
||||||
|
`.github/workflows/injector.yml` builds and tests all four targets (Windows x86_64, Linux x86_64, macOS x86_64, macOS arm64)
|
||||||
|
and uploads them as artifacts.
|
||||||
|
|
||||||
## Use
|
## Use
|
||||||
|
|
||||||
Start a Fabric 1.21.11 instance, then run `dist\PrestigeInjector.exe` (or `PrestigeInjector.exe <pid> [dll]`).
|
Start a Fabric 1.21.11 instance, then run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
PrestigeInjector # interactive: pick the game from the list
|
||||||
|
PrestigeInjector <pid> [lib path] # inject into a specific pid, optionally with an explicit library path
|
||||||
|
```
|
||||||
|
|
||||||
|
On Windows this is `PrestigeInjector.exe`. By default the library is looked up beside the injector.
|
||||||
|
|
||||||
|
## Install as a mod instead
|
||||||
|
|
||||||
|
The payload jar is also a normal Fabric mod. Put `prestige-*.jar` (from `dist/` or the CI `payload-jar` artifact) in the
|
||||||
|
`mods/` folder of a Fabric Loader 0.19+ / Fabric API / Minecraft 1.21.11 instance; no injector is needed, on any OS.
|
||||||
|
Do not use both: if the mod is already loaded, the injector's bootstrap logs "already loaded as a Fabric mod" and skips.
|
||||||
|
|
||||||
|
## Requirements (Linux / macOS)
|
||||||
|
|
||||||
|
- Run the injector as the **same user** as the game.
|
||||||
|
- The attach mechanism must be enabled: the game must not be started with `-XX:+DisableAttachMechanism`.
|
||||||
|
- The JVM architecture must match the injector build. On Apple Silicon use an arm64 JVM, or use the x86_64 build for JVMs
|
||||||
|
running under Rosetta.
|
||||||
|
- Sandboxed launchers (Flatpak, Snap) hide the attach socket and are not supported.
|
||||||
|
|
||||||
|
## Known limits
|
||||||
|
|
||||||
|
- On Linux/macOS the library stays loaded after self-destruct: hooks are removed and natives unregistered, but the library is
|
||||||
|
not `dlclose`d (unmapping code the thread is still returning into would crash the JVM). On Windows the library is freed.
|
||||||
|
- Linux and macOS support is compile-checked in CI; test on a real host before relying on it.
|
||||||
|
|
||||||
|
## Log
|
||||||
|
|
||||||
|
`prestige-native.log` is written beside the loader library, plus `[Prestige-Inject]` lines in the game log.
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
//! PrestigeInjector: lists the running Minecraft (javaw/java) processes and loads prestige_native.dll into the selected one
|
//! PrestigeInjector: lists the running Minecraft (javaw/java) processes and loads the native library
|
||||||
//! with the classic remote LoadLibraryW thread. The DLL does everything else once it is inside the JVM; the injector tails
|
//! (prestige_native.dll / libprestige_native.so / libprestige_native.dylib) into the selected one: a remote LoadLibraryW
|
||||||
//! the DLL's log so every stage (transforming, injecting, initialising) is visible.
|
//! thread on Windows, the HotSpot attach protocol on Linux and macOS. The library does everything else once it is inside
|
||||||
|
//! the JVM; the injector tails its log so every stage (transforming, injecting, initialising) is visible.
|
||||||
//!
|
//!
|
||||||
//! PrestigeInjector.exe interactive process picker (Up/Down, Enter, Esc)
|
//! PrestigeInjector interactive process picker (Up/Down, Enter, Esc)
|
||||||
//! PrestigeInjector.exe <pid> [dll path] non-interactive
|
//! PrestigeInjector <pid> [lib path] non-interactive
|
||||||
mod win;
|
mod platform;
|
||||||
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||||
@@ -12,9 +13,9 @@ use std::path::{Path, PathBuf};
|
|||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
const DLL_NAME: &str = "prestige_native.dll";
|
/// The one credit line, shown once at the top of the screen.
|
||||||
const WATERMARK: &str = "Cracked by Skid Solutions";
|
const CREDIT: &str = "Cracked by Skid Solutions - https://discord.gg/x2FJyGrDNg";
|
||||||
const INVITE_URL: &str = "https://discord.gg/M56rd2wwC5";
|
const INVITE_URL: &str = "https://discord.gg/x2FJyGrDNg";
|
||||||
/// How long to follow the DLL's progress after it has been loaded.
|
/// How long to follow the DLL's progress after it has been loaded.
|
||||||
const FOLLOW_TIMEOUT: Duration = Duration::from_secs(180);
|
const FOLLOW_TIMEOUT: Duration = Duration::from_secs(180);
|
||||||
|
|
||||||
@@ -28,16 +29,18 @@ const RED: &str = "\x1b[91m";
|
|||||||
fn default_dll() -> PathBuf {
|
fn default_dll() -> PathBuf {
|
||||||
std::env::current_exe()
|
std::env::current_exe()
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|exe| exe.parent().map(|dir| dir.join(DLL_NAME)))
|
.and_then(|exe| exe.parent().map(|dir| dir.join(platform::LIB_NAME)))
|
||||||
.unwrap_or_else(|| PathBuf::from(DLL_NAME))
|
.unwrap_or_else(|| PathBuf::from(platform::LIB_NAME))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn banner() {
|
fn banner() -> String {
|
||||||
println!("{CYAN} ___ ___ ___ ___ _____ ___ ___ ___ ");
|
let mut out = String::new();
|
||||||
println!(" | _ \\ _ \\ __/ __|_ _|_ _/ __| __|");
|
out.push_str(&format!("{CYAN} ___ ___ ___ ___ _____ ___ ___ ___ \n"));
|
||||||
println!(" | _/ / _|\\__ \\ | | | | (_ | _| ");
|
out.push_str(" | _ \\ _ \\ __/ __|_ _|_ _/ __| __|\n");
|
||||||
println!(" |_| |_|_\\___|___/ |_| |___\\___|___|{RESET} injector");
|
out.push_str(" | _/ / _|\\__ \\ | | | | (_ | _| \n");
|
||||||
println!("{YELLOW} {WATERMARK}{RESET}\n");
|
out.push_str(&format!(" |_| |_|_\\___|___/ |_| |___\\___|___|{RESET} injector\n"));
|
||||||
|
out.push_str(&format!("{YELLOW} {CREDIT}{RESET}\n\n"));
|
||||||
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
fn step(label: &str, message: &str) {
|
fn step(label: &str, message: &str) {
|
||||||
@@ -58,21 +61,26 @@ fn clock() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
win::enable_ansi();
|
let code = real_main();
|
||||||
|
platform::restore_terminal();
|
||||||
|
code
|
||||||
|
}
|
||||||
|
|
||||||
|
fn real_main() -> ExitCode {
|
||||||
|
platform::enable_ansi();
|
||||||
let _ = clock();
|
let _ = clock();
|
||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
let interactive = args.is_empty();
|
let interactive = args.is_empty();
|
||||||
|
|
||||||
let (pid, dll): (Option<u32>, PathBuf) = match args.as_slice() {
|
let (pid, dll): (Option<u32>, PathBuf) = match args.as_slice() {
|
||||||
[] => {
|
[] => {
|
||||||
banner();
|
open_invite_once();
|
||||||
invite_prompt();
|
|
||||||
(pick_process(), default_dll())
|
(pick_process(), default_dll())
|
||||||
}
|
}
|
||||||
[pid] => (parse_pid(pid), default_dll()),
|
[pid] => (parse_pid(pid), default_dll()),
|
||||||
[pid, dll] => (parse_pid(pid), PathBuf::from(dll)),
|
[pid, dll] => (parse_pid(pid), PathBuf::from(dll)),
|
||||||
_ => {
|
_ => {
|
||||||
eprintln!("usage: PrestigeInjector.exe [<pid> [dll path]]");
|
eprintln!("usage: PrestigeInjector [<pid> [lib path]]");
|
||||||
return ExitCode::from(2);
|
return ExitCode::from(2);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -81,7 +89,7 @@ fn main() -> ExitCode {
|
|||||||
return if interactive { ExitCode::SUCCESS } else { ExitCode::from(2) };
|
return if interactive { ExitCode::SUCCESS } else { ExitCode::from(2) };
|
||||||
};
|
};
|
||||||
if !interactive {
|
if !interactive {
|
||||||
banner();
|
print!("{}", banner());
|
||||||
}
|
}
|
||||||
|
|
||||||
let code = run(pid, &dll);
|
let code = run(pid, &dll);
|
||||||
@@ -109,16 +117,18 @@ fn run(pid: u32, dll: &Path) -> ExitCode {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let dll_display = dll.display().to_string();
|
let dll_display = dll.display().to_string();
|
||||||
step("loader", &dll_display.trim_start_matches(r"\\?\").to_string());
|
#[cfg(windows)]
|
||||||
|
let dll_display = dll_display.trim_start_matches(r"\\?\").to_string();
|
||||||
|
step("loader", &dll_display);
|
||||||
|
|
||||||
// The DLL appends to prestige-native.log beside itself; remember where it ends so only this run's lines are shown.
|
// The library appends to prestige-native.log beside itself; remember where it ends so only this run's lines are shown.
|
||||||
let log_path = dll.with_file_name("prestige-native.log");
|
let log_path = dll.with_file_name("prestige-native.log");
|
||||||
let log_start = fs::metadata(&log_path).map(|m| m.len()).unwrap_or(0);
|
let log_start = fs::metadata(&log_path).map(|m| m.len()).unwrap_or(0);
|
||||||
|
|
||||||
step("checking", "64-bit JVM, process access, DLL not already loaded");
|
step("checking", "64-bit JVM, process access, library not already loaded");
|
||||||
step("injecting", "remote thread LoadLibraryW");
|
step("injecting", "loading the native library into the JVM");
|
||||||
match win::inject(pid, &dll) {
|
match platform::inject(pid, &dll) {
|
||||||
Ok(()) => ok("injected", "prestige_native.dll is loaded in the game process"),
|
Ok(()) => ok("injected", &format!("{} is loaded in the game process", platform::LIB_NAME)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
fail("failed", &e);
|
fail("failed", &e);
|
||||||
return ExitCode::from(1);
|
return ExitCode::from(1);
|
||||||
@@ -130,7 +140,6 @@ fn run(pid: u32, dll: &Path) -> ExitCode {
|
|||||||
Outcome::Done => {
|
Outcome::Done => {
|
||||||
println!();
|
println!();
|
||||||
ok("success", "successfully injected - Prestige is running in the game");
|
ok("success", "successfully injected - Prestige is running in the game");
|
||||||
println!("{YELLOW} {WATERMARK}{RESET} {DIM}{INVITE_URL}{RESET}");
|
|
||||||
ExitCode::SUCCESS
|
ExitCode::SUCCESS
|
||||||
}
|
}
|
||||||
Outcome::Failed(line) => {
|
Outcome::Failed(line) => {
|
||||||
@@ -173,7 +182,9 @@ fn follow_log(path: &Path, start: u64) -> Outcome {
|
|||||||
// lines look like "[unix seconds] message"
|
// lines look like "[unix seconds] message"
|
||||||
let message = line.split_once("] ").map(|(_, m)| m).unwrap_or(line);
|
let message = line.split_once("] ").map(|(_, m)| m).unwrap_or(line);
|
||||||
let lower = message.to_ascii_lowercase();
|
let lower = message.to_ascii_lowercase();
|
||||||
if lower.contains("failed") || lower.contains("not found") {
|
// Per-hook failures ("[java] hook failed for ...") are not fatal: Bootstrap keeps the hooks that applied.
|
||||||
|
let fatal = message.starts_with("FAILED:") || lower.contains("start failed") || lower.contains("client init failed") || lower.contains("could not hook");
|
||||||
|
if fatal {
|
||||||
fail("loader", message);
|
fail("loader", message);
|
||||||
return Outcome::Failed(message.to_string());
|
return Outcome::Failed(message.to_string());
|
||||||
}
|
}
|
||||||
@@ -189,51 +200,26 @@ fn follow_log(path: &Path, start: u64) -> Outcome {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn config_path() -> Option<PathBuf> {
|
fn config_path() -> Option<PathBuf> {
|
||||||
std::env::var_os("APPDATA").map(|dir| PathBuf::from(dir).join("PrestigeInjector").join("settings.txt"))
|
platform::config_dir().map(|dir| dir.join("PrestigeInjector").join("settings.txt"))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// First launch: open the Discord invite, then ask whether to open it again on later launches.
|
/// Opens the Discord invite on the very first launch only (no prompt); afterwards only the credit line shows it.
|
||||||
fn invite_prompt() {
|
fn open_invite_once() {
|
||||||
let Some(path) = config_path() else { return };
|
let Some(path) = config_path() else { return };
|
||||||
let saved = fs::read_to_string(&path).ok();
|
if path.exists() {
|
||||||
let show = match saved.as_deref() {
|
|
||||||
None => true, // first launch
|
|
||||||
Some(text) => !text.lines().any(|l| l.trim() == "show_invite=false"),
|
|
||||||
};
|
|
||||||
if !show {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let first = saved.is_none();
|
let _ = platform::open_url(INVITE_URL);
|
||||||
step("discord", &format!("opening {INVITE_URL}"));
|
|
||||||
if !win::open_url(INVITE_URL) {
|
|
||||||
fail("discord", &format!("could not open a browser, visit {INVITE_URL} manually"));
|
|
||||||
}
|
|
||||||
let again = ask_yes_no(if first {
|
|
||||||
"Show the Discord invite again next time you start the injector? [Y/n] "
|
|
||||||
} else {
|
|
||||||
"Keep showing the Discord invite on launch? [Y/n] "
|
|
||||||
});
|
|
||||||
if let Some(dir) = path.parent() {
|
if let Some(dir) = path.parent() {
|
||||||
let _ = fs::create_dir_all(dir);
|
let _ = fs::create_dir_all(dir);
|
||||||
}
|
}
|
||||||
let _ = fs::write(&path, format!("show_invite={again}\n"));
|
let _ = fs::write(&path, "invite_opened=true\n");
|
||||||
println!();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ask_yes_no(question: &str) -> bool {
|
|
||||||
print!("{question}");
|
|
||||||
let _ = io::stdout().flush();
|
|
||||||
let mut line = String::new();
|
|
||||||
if io::stdin().read_line(&mut line).is_err() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
!matches!(line.trim().to_ascii_lowercase().as_str(), "n" | "no")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Interactive list of Minecraft JVM windows, refreshed every 750 ms. Returns the chosen pid.
|
/// Interactive list of Minecraft JVM windows, refreshed every 750 ms. Returns the chosen pid.
|
||||||
fn pick_process() -> Option<u32> {
|
fn pick_process() -> Option<u32> {
|
||||||
let mut selected = 0usize;
|
let mut selected = 0usize;
|
||||||
let mut processes = win::java_windows();
|
let mut processes = platform::java_processes();
|
||||||
let mut last_refresh = Instant::now();
|
let mut last_refresh = Instant::now();
|
||||||
loop {
|
loop {
|
||||||
if selected >= processes.len() {
|
if selected >= processes.len() {
|
||||||
@@ -242,34 +228,34 @@ fn pick_process() -> Option<u32> {
|
|||||||
draw(&processes, selected);
|
draw(&processes, selected);
|
||||||
let deadline = Instant::now() + Duration::from_millis(50);
|
let deadline = Instant::now() + Duration::from_millis(50);
|
||||||
while Instant::now() < deadline {
|
while Instant::now() < deadline {
|
||||||
if let Some(key) = win::poll_key() {
|
if let Some(key) = platform::poll_key() {
|
||||||
match key {
|
match key {
|
||||||
win::Key::Up => selected = selected.saturating_sub(1),
|
platform::Key::Up => selected = selected.saturating_sub(1),
|
||||||
win::Key::Down => selected = (selected + 1).min(processes.len().saturating_sub(1)),
|
platform::Key::Down => selected = (selected + 1).min(processes.len().saturating_sub(1)),
|
||||||
win::Key::Enter => {
|
platform::Key::Enter => {
|
||||||
if let Some(p) = processes.get(selected) {
|
if let Some(p) = processes.get(selected) {
|
||||||
return Some(p.pid);
|
return Some(p.pid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
win::Key::Escape => return None,
|
platform::Key::Escape => return None,
|
||||||
}
|
}
|
||||||
draw(&processes, selected);
|
draw(&processes, selected);
|
||||||
}
|
}
|
||||||
std::thread::sleep(Duration::from_millis(10));
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
}
|
}
|
||||||
if last_refresh.elapsed() >= Duration::from_millis(750) {
|
if last_refresh.elapsed() >= Duration::from_millis(750) {
|
||||||
processes = win::java_windows();
|
processes = platform::java_processes();
|
||||||
last_refresh = Instant::now();
|
last_refresh = Instant::now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw(processes: &[win::JavaWindow], selected: usize) {
|
fn draw(processes: &[platform::JavaProcess], selected: usize) {
|
||||||
let mut out = String::from("\x1b[2J\x1b[H");
|
let mut out = String::from("\x1b[2J\x1b[H");
|
||||||
out.push_str(&format!("{CYAN}Prestige injector{RESET} - Minecraft 1.21.11 (Fabric, 64-bit) {YELLOW}{WATERMARK}{RESET}\n"));
|
out.push_str(&banner());
|
||||||
out.push_str("Up/Down select, Enter inject, Esc quit\n\n");
|
out.push_str("Minecraft 1.21.11 (Fabric, 64-bit) - Up/Down select, Enter inject, Esc quit\n\n");
|
||||||
if processes.is_empty() {
|
if processes.is_empty() {
|
||||||
out.push_str(" waiting for a javaw.exe / java.exe window...\n");
|
out.push_str(" waiting for a Minecraft JVM (java/javaw)...\n");
|
||||||
}
|
}
|
||||||
for (i, p) in processes.iter().enumerate() {
|
for (i, p) in processes.iter().enumerate() {
|
||||||
let marker = if i == selected { ">" } else { " " };
|
let marker = if i == selected { ">" } else { " " };
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
//! Platform backends of the injector. Each backend exposes the same functions:
|
||||||
|
//! enable_ansi, poll_key, java_processes, inject, open_url, restore_terminal.
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// A running Minecraft JVM shown in the picker.
|
||||||
|
pub struct JavaProcess {
|
||||||
|
pub pid: u32,
|
||||||
|
pub title: String,
|
||||||
|
pub exe: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum Key {
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Enter,
|
||||||
|
Escape,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod win;
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub use win::*;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
mod unix_attach;
|
||||||
|
#[cfg(unix)]
|
||||||
|
mod unix_procs;
|
||||||
|
#[cfg(unix)]
|
||||||
|
mod unix_term;
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub use unix_attach::inject;
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub use unix_procs::java_processes;
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub use unix_term::{enable_ansi, open_url, poll_key, restore_terminal};
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub const LIB_NAME: &str = "prestige_native.dll";
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub const LIB_NAME: &str = "libprestige_native.dylib";
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
pub const LIB_NAME: &str = "libprestige_native.so";
|
||||||
|
|
||||||
|
/// Per-user configuration directory: APPDATA, $XDG_CONFIG_HOME or ~/.config, ~/Library/Application Support.
|
||||||
|
pub fn config_dir() -> Option<PathBuf> {
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
std::env::var_os("APPDATA").map(PathBuf::from)
|
||||||
|
}
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
std::env::var_os("HOME").map(|h| PathBuf::from(h).join("Library").join("Application Support"))
|
||||||
|
}
|
||||||
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
|
{
|
||||||
|
match std::env::var_os("XDG_CONFIG_HOME") {
|
||||||
|
Some(dir) if !dir.is_empty() => Some(PathBuf::from(dir)),
|
||||||
|
_ => std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,405 @@
|
|||||||
|
//! HotSpot attach-protocol injection for Linux and macOS (what `jcmd <pid> JVMTI.agent_load` does).
|
||||||
|
//!
|
||||||
|
//! 1. Create `.attach_pid<pid>` where the target JVM looks for it and send SIGQUIT.
|
||||||
|
//! 2. Wait for the UNIX socket `.java_pid<pid>`.
|
||||||
|
//! 3. Send `1\0load\0<abs lib>\0true\0\0` and read the status reply.
|
||||||
|
//!
|
||||||
|
//! The JVM then dlopens the library and calls `Agent_OnAttach`.
|
||||||
|
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
fn kill(pid: i32, sig: i32) -> i32;
|
||||||
|
fn geteuid() -> u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SIGQUIT: i32 = 3;
|
||||||
|
const SOCKET_WAIT: Duration = Duration::from_secs(10);
|
||||||
|
const POLL_INTERVAL: Duration = Duration::from_millis(50);
|
||||||
|
const REPLY_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
|
const IS_MACOS: bool = cfg!(target_os = "macos");
|
||||||
|
|
||||||
|
pub fn inject(pid: u32, lib: &Path) -> Result<(), String> {
|
||||||
|
let lib = fs::canonicalize(lib)
|
||||||
|
.map_err(|e| format!("library '{}' not found: {e}", lib.display()))?;
|
||||||
|
let lib_str = lib
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| "library path is not valid UTF-8".to_string())?;
|
||||||
|
|
||||||
|
preflight_owner(pid)?;
|
||||||
|
if let Some(msg) = arch_mismatch(pid) {
|
||||||
|
return Err(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sockets = socket_candidates(pid);
|
||||||
|
let existing = sockets.iter().find(|p| is_socket(p)).cloned();
|
||||||
|
|
||||||
|
let sock_path = match existing {
|
||||||
|
Some(p) => p,
|
||||||
|
None => trigger_attach(pid, &sockets)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
send_load(&sock_path, lib_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- preflight
|
||||||
|
|
||||||
|
fn current_uid() -> u32 {
|
||||||
|
unsafe { geteuid() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preflight_owner(pid: u32) -> Result<(), String> {
|
||||||
|
let uid = if IS_MACOS {
|
||||||
|
let out = Command::new("ps")
|
||||||
|
.args(["-o", "uid=", "-p", &pid.to_string()])
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("failed to run ps: {e}"))?;
|
||||||
|
let text = String::from_utf8_lossy(&out.stdout);
|
||||||
|
let text = text.trim();
|
||||||
|
if text.is_empty() {
|
||||||
|
return Err(format!("process {pid} does not exist"));
|
||||||
|
}
|
||||||
|
text.parse::<u32>()
|
||||||
|
.map_err(|_| format!("could not parse owner of process {pid}: '{text}'"))?
|
||||||
|
} else {
|
||||||
|
match fs::metadata(format!("/proc/{pid}")) {
|
||||||
|
Ok(m) => m.uid(),
|
||||||
|
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
|
||||||
|
return Err(format!("process {pid} does not exist"))
|
||||||
|
}
|
||||||
|
Err(e) => return Err(format!("cannot inspect process {pid}: {e}")),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let me = current_uid();
|
||||||
|
if uid != me {
|
||||||
|
return Err(format!(
|
||||||
|
"process {pid} belongs to uid {uid} but the injector runs as uid {me}; \
|
||||||
|
the attach protocol only works for the same user (run the injector as the user that started the game)"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_arch() -> &'static str {
|
||||||
|
match std::env::consts::ARCH {
|
||||||
|
"aarch64" | "arm64" => "arm64",
|
||||||
|
other => other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Some(message) only on a definite mismatch; None when matching or undeterminable.
|
||||||
|
fn arch_mismatch(pid: u32) -> Option<String> {
|
||||||
|
let target = target_arch(pid)?;
|
||||||
|
if target == host_arch() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let hint = if IS_MACOS && target == "x86_64" && host_arch() == "arm64" {
|
||||||
|
" (Rosetta?)"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
Some(format!(
|
||||||
|
"target JVM is {target}{hint} but this injector is {}; use a matching JVM or a matching injector build",
|
||||||
|
host_arch()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn target_arch(pid: u32) -> Option<String> {
|
||||||
|
if IS_MACOS {
|
||||||
|
let out = Command::new("ps")
|
||||||
|
.args(["-o", "comm=", "-p", &pid.to_string()])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
let comm = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||||
|
if comm.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let head = read_head(Path::new(&comm), 64)?;
|
||||||
|
macho_arch(&head).map(str::to_string)
|
||||||
|
} else {
|
||||||
|
// Prefer the libjvm mapped into the process, fall back to the executable.
|
||||||
|
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||||
|
if let Ok(maps) = fs::read_to_string(format!("/proc/{pid}/maps")) {
|
||||||
|
for line in maps.lines() {
|
||||||
|
if line.contains("libjvm.so") {
|
||||||
|
if let Some(path) = line.split_whitespace().nth(5) {
|
||||||
|
candidates.push(PathBuf::from(path));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
candidates.push(PathBuf::from(format!("/proc/{pid}/exe")));
|
||||||
|
for c in candidates {
|
||||||
|
if let Some(head) = read_head(&c, 64) {
|
||||||
|
if let Some(a) = elf_arch(&head) {
|
||||||
|
return Some(a.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_head(path: &Path, n: usize) -> Option<Vec<u8>> {
|
||||||
|
let mut f = fs::File::open(path).ok()?;
|
||||||
|
let mut buf = vec![0u8; n];
|
||||||
|
let mut got = 0;
|
||||||
|
while got < n {
|
||||||
|
match f.read(&mut buf[got..]) {
|
||||||
|
Ok(0) => break,
|
||||||
|
Ok(k) => got += k,
|
||||||
|
Err(_) => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf.truncate(got);
|
||||||
|
Some(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Architecture name ("x86_64", "arm64", ...) from an ELF header, None if not ELF/unknown.
|
||||||
|
fn elf_arch(h: &[u8]) -> Option<&'static str> {
|
||||||
|
if h.len() < 20 || &h[0..4] != b"\x7fELF" {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let machine = match h[5] {
|
||||||
|
1 => u16::from_le_bytes([h[18], h[19]]),
|
||||||
|
2 => u16::from_be_bytes([h[18], h[19]]),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
Some(match machine {
|
||||||
|
3 => "x86",
|
||||||
|
40 => "arm",
|
||||||
|
62 => "x86_64",
|
||||||
|
183 => "arm64",
|
||||||
|
243 => "riscv",
|
||||||
|
21 => "ppc64",
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Architecture from a thin 64-bit Mach-O header. Fat/universal binaries return None
|
||||||
|
/// (the running slice cannot be determined from the file alone).
|
||||||
|
fn macho_arch(h: &[u8]) -> Option<&'static str> {
|
||||||
|
if h.len() < 8 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let magic = u32::from_le_bytes([h[0], h[1], h[2], h[3]]);
|
||||||
|
let cputype = match magic {
|
||||||
|
0xfeed_facf => u32::from_le_bytes([h[4], h[5], h[6], h[7]]),
|
||||||
|
0xcffa_edfe => u32::from_be_bytes([h[4], h[5], h[6], h[7]]),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
match cputype {
|
||||||
|
0x0100_0007 => Some("x86_64"),
|
||||||
|
0x0100_000c => Some("arm64"),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------ attach trigger
|
||||||
|
|
||||||
|
fn socket_candidates(pid: u32) -> Vec<PathBuf> {
|
||||||
|
let name = format!(".java_pid{pid}");
|
||||||
|
let mut v = Vec::new();
|
||||||
|
if IS_MACOS {
|
||||||
|
if let Some(t) = std::env::var_os("TMPDIR") {
|
||||||
|
v.push(Path::new(&t).join(&name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let tmp = Path::new("/tmp").join(&name);
|
||||||
|
if !v.contains(&tmp) {
|
||||||
|
v.push(tmp);
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_socket(p: &Path) -> bool {
|
||||||
|
use std::os::unix::fs::FileTypeExt;
|
||||||
|
fs::symlink_metadata(p)
|
||||||
|
.map(|m| m.file_type().is_socket())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the trigger files when dropped, on every path.
|
||||||
|
struct TriggerFiles(Vec<PathBuf>);
|
||||||
|
|
||||||
|
impl Drop for TriggerFiles {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
for p in &self.0 {
|
||||||
|
let _ = fs::remove_file(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trigger_candidates(pid: u32) -> Vec<PathBuf> {
|
||||||
|
let name = format!(".attach_pid{pid}");
|
||||||
|
let mut v = Vec::new();
|
||||||
|
if !IS_MACOS {
|
||||||
|
if let Ok(cwd) = fs::read_link(format!("/proc/{pid}/cwd")) {
|
||||||
|
v.push(cwd.join(&name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.push(Path::new("/tmp").join(&name));
|
||||||
|
if IS_MACOS {
|
||||||
|
if let Some(t) = std::env::var_os("TMPDIR") {
|
||||||
|
let p = Path::new(&t).join(&name);
|
||||||
|
if !v.contains(&p) {
|
||||||
|
v.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trigger_attach(pid: u32, sockets: &[PathBuf]) -> Result<PathBuf, String> {
|
||||||
|
let mut created = Vec::new();
|
||||||
|
for p in trigger_candidates(pid) {
|
||||||
|
if fs::File::create(&p).is_ok() {
|
||||||
|
created.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if created.is_empty() {
|
||||||
|
return Err(format!(
|
||||||
|
"could not create .attach_pid{pid} in the target's working directory or /tmp"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let _guard = TriggerFiles(created);
|
||||||
|
|
||||||
|
if unsafe { kill(pid as i32, SIGQUIT) } != 0 {
|
||||||
|
return Err(format!(
|
||||||
|
"failed to send SIGQUIT to {pid}: {}",
|
||||||
|
std::io::Error::last_os_error()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
while start.elapsed() < SOCKET_WAIT {
|
||||||
|
if let Some(p) = sockets.iter().find(|p| is_socket(p)) {
|
||||||
|
return Ok(p.clone());
|
||||||
|
}
|
||||||
|
std::thread::sleep(POLL_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(msg) = arch_mismatch(pid) {
|
||||||
|
return Err(format!("attach socket never appeared; {msg}"));
|
||||||
|
}
|
||||||
|
Err(format!(
|
||||||
|
"the JVM did not create its attach socket within {}s. Possible causes: the JVM was started with \
|
||||||
|
-XX:+DisableAttachMechanism or -Xrs, the game runs in a sandbox (Flatpak/Snap) with a private /tmp, \
|
||||||
|
or it belongs to a different user",
|
||||||
|
SOCKET_WAIT.as_secs()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------------------------------------------ protocol
|
||||||
|
|
||||||
|
/// `1\0load\0<lib>\0true\0\0`: version, command, then three NUL-terminated args
|
||||||
|
/// (library path, "true" = absolute path, empty options).
|
||||||
|
fn build_request(lib: &str) -> Vec<u8> {
|
||||||
|
let mut v = Vec::with_capacity(lib.len() + 16);
|
||||||
|
v.extend_from_slice(b"1\0load\0");
|
||||||
|
v.extend_from_slice(lib.as_bytes());
|
||||||
|
v.extend_from_slice(b"\0true\0\0");
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
/// First line is the integer status (0 = success); the rest is message text.
|
||||||
|
fn parse_response(data: &[u8]) -> Result<(), String> {
|
||||||
|
let text = String::from_utf8_lossy(data);
|
||||||
|
let (first, rest) = match text.split_once('\n') {
|
||||||
|
Some((a, b)) => (a.trim(), b.trim()),
|
||||||
|
None => (text.trim(), ""),
|
||||||
|
};
|
||||||
|
if first.is_empty() {
|
||||||
|
return Err("JVM closed the attach connection without a reply".into());
|
||||||
|
}
|
||||||
|
match first.parse::<i32>() {
|
||||||
|
Ok(0) => Ok(()),
|
||||||
|
Ok(code) => {
|
||||||
|
if rest.is_empty() {
|
||||||
|
Err(format!("JVM rejected the load request (status {code})"))
|
||||||
|
} else {
|
||||||
|
Err(format!("JVM rejected the load request (status {code}): {rest}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => Err(format!("unexpected reply from JVM: {}", text.trim())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_load(sock: &Path, lib: &str) -> Result<(), String> {
|
||||||
|
let mut s = UnixStream::connect(sock).map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"could not connect to attach socket {}: {e} (stale socket, or the JVM runs as another user)",
|
||||||
|
sock.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let _ = s.set_read_timeout(Some(REPLY_TIMEOUT));
|
||||||
|
let _ = s.set_write_timeout(Some(REPLY_TIMEOUT));
|
||||||
|
s.write_all(&build_request(lib))
|
||||||
|
.map_err(|e| format!("failed to send attach request: {e}"))?;
|
||||||
|
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
match s.read_to_end(&mut buf) {
|
||||||
|
Ok(_) => {}
|
||||||
|
// A partial reply is still parseable; a bare timeout with no data is not.
|
||||||
|
Err(e) if buf.is_empty() => return Err(format!("no reply from JVM: {e}")),
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
parse_response(&buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn request_format() {
|
||||||
|
assert_eq!(
|
||||||
|
build_request("/opt/libprestige_native.so"),
|
||||||
|
b"1\0load\0/opt/libprestige_native.so\0true\0\0".to_vec()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn response_ok_and_errors() {
|
||||||
|
assert!(parse_response(b"0\n").is_ok());
|
||||||
|
assert!(parse_response(b"0").is_ok());
|
||||||
|
let e = parse_response(b"-1\nreturn code: 1\n").unwrap_err();
|
||||||
|
assert!(e.contains("-1") && e.contains("return code: 1"));
|
||||||
|
assert!(parse_response(b"").is_err());
|
||||||
|
assert!(parse_response(b"garbage").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn elf_headers() {
|
||||||
|
let mut h = vec![0u8; 64];
|
||||||
|
h[..4].copy_from_slice(b"\x7fELF");
|
||||||
|
h[5] = 1;
|
||||||
|
h[18..20].copy_from_slice(&62u16.to_le_bytes());
|
||||||
|
assert_eq!(elf_arch(&h), Some("x86_64"));
|
||||||
|
h[18..20].copy_from_slice(&183u16.to_le_bytes());
|
||||||
|
assert_eq!(elf_arch(&h), Some("arm64"));
|
||||||
|
assert_eq!(elf_arch(b"nope"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn macho_headers() {
|
||||||
|
let mut h = vec![0xcf, 0xfa, 0xed, 0xfe];
|
||||||
|
h.extend_from_slice(&0x0100_000cu32.to_le_bytes());
|
||||||
|
assert_eq!(macho_arch(&h), Some("arm64"));
|
||||||
|
let mut h = vec![0xcf, 0xfa, 0xed, 0xfe];
|
||||||
|
h.extend_from_slice(&0x0100_0007u32.to_le_bytes());
|
||||||
|
assert_eq!(macho_arch(&h), Some("x86_64"));
|
||||||
|
// fat binary -> undeterminable
|
||||||
|
assert_eq!(macho_arch(&[0xca, 0xfe, 0xba, 0xbe, 0, 0, 0, 2]), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
//! Lists running Minecraft (Fabric) JVMs on Linux and macOS. std only.
|
||||||
|
use super::JavaProcess;
|
||||||
|
|
||||||
|
/// Last path component, accepting both separators.
|
||||||
|
fn file_name(path: &str) -> &str {
|
||||||
|
path.trim_end_matches(['/', '\\']).rsplit(['/', '\\']).next().unwrap_or(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when `argv0` names a java launcher and the arguments look like a Minecraft/Fabric JVM.
|
||||||
|
pub(crate) fn is_minecraft_jvm(args: &[String]) -> bool {
|
||||||
|
let Some(argv0) = args.first() else { return false };
|
||||||
|
if !file_name(argv0).to_ascii_lowercase().contains("java") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
args.iter().any(|a| {
|
||||||
|
let a = a.to_ascii_lowercase();
|
||||||
|
a.contains("net.fabricmc.loader") || a.contains("knotclient") || a.contains("minecraft")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Friendly instance label: value of --version, else the last component of --gameDir, else "Minecraft".
|
||||||
|
pub(crate) fn instance_label(args: &[String]) -> String {
|
||||||
|
fn value<'a>(args: &'a [String], flag: &str) -> Option<&'a str> {
|
||||||
|
let prefix = format!("{flag}=");
|
||||||
|
let mut it = args.iter();
|
||||||
|
while let Some(a) = it.next() {
|
||||||
|
if a == flag {
|
||||||
|
return it.next().map(String::as_str).filter(|v| !v.is_empty());
|
||||||
|
}
|
||||||
|
if let Some(v) = a.strip_prefix(&prefix) {
|
||||||
|
if !v.is_empty() {
|
||||||
|
return Some(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
if let Some(v) = value(args, "--version") {
|
||||||
|
return v.to_string();
|
||||||
|
}
|
||||||
|
if let Some(dir) = value(args, "--gameDir") {
|
||||||
|
let name = file_name(dir);
|
||||||
|
if !name.is_empty() {
|
||||||
|
return name.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"Minecraft".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(pid: u32, args: &[String]) -> Option<JavaProcess> {
|
||||||
|
if !is_minecraft_jvm(args) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(JavaProcess { pid, title: instance_label(args), exe: file_name(&args[0]).to_string() })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses one `ps -axo pid=,uid=,command=` line into (pid, uid, argv).
|
||||||
|
/// ps joins argv with spaces, so argv[0] is taken as everything before the first " -" option.
|
||||||
|
#[cfg_attr(target_os = "linux", allow(dead_code))]
|
||||||
|
pub(crate) fn parse_ps_line(line: &str) -> Option<(u32, u32, Vec<String>)> {
|
||||||
|
let line = line.trim_start();
|
||||||
|
let (pid, rest) = line.split_once(char::is_whitespace)?;
|
||||||
|
let rest = rest.trim_start();
|
||||||
|
let (uid, cmd) = rest.split_once(char::is_whitespace)?;
|
||||||
|
let cmd = cmd.trim();
|
||||||
|
let (argv0, tail) = match cmd.find(" -") {
|
||||||
|
Some(i) => (&cmd[..i], &cmd[i..]),
|
||||||
|
None => (cmd, ""),
|
||||||
|
};
|
||||||
|
let mut args = vec![argv0.to_string()];
|
||||||
|
args.extend(tail.split_whitespace().map(str::to_string));
|
||||||
|
Some((pid.parse().ok()?, uid.parse().ok()?, args))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
pub fn java_processes() -> Vec<JavaProcess> {
|
||||||
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
let own_pid = std::process::id();
|
||||||
|
let own_uid = std::fs::metadata("/proc/self").map(|m| m.uid()).ok();
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let Ok(dir) = std::fs::read_dir("/proc") else { return out };
|
||||||
|
for entry in dir.flatten() {
|
||||||
|
let Some(pid) = entry.file_name().to_str().and_then(|n| n.parse::<u32>().ok()) else { continue };
|
||||||
|
if pid == own_pid {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let (Some(me), Ok(meta)) = (own_uid, entry.metadata()) {
|
||||||
|
if meta.uid() != me {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Ok(raw) = std::fs::read(entry.path().join("cmdline")) else { continue };
|
||||||
|
let args: Vec<String> = raw
|
||||||
|
.split(|&b| b == 0)
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| String::from_utf8_lossy(s).into_owned())
|
||||||
|
.collect();
|
||||||
|
if let Some(p) = build(pid, &args) {
|
||||||
|
out.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.sort_by_key(|p| p.pid);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
pub fn java_processes() -> Vec<JavaProcess> {
|
||||||
|
let own_pid = std::process::id();
|
||||||
|
let Ok(output) = std::process::Command::new("ps").args(["-axo", "pid=,uid=,command="]).output() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let text = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let rows: Vec<_> = text.lines().filter_map(parse_ps_line).collect();
|
||||||
|
let own_uid = rows.iter().find(|r| r.0 == own_pid).map(|r| r.1);
|
||||||
|
let mut out: Vec<JavaProcess> = rows
|
||||||
|
.iter()
|
||||||
|
.filter(|(pid, uid, _)| *pid != own_pid && own_uid.map_or(true, |u| u == *uid))
|
||||||
|
.filter_map(|(pid, _, args)| build(*pid, args))
|
||||||
|
.collect();
|
||||||
|
out.sort_by_key(|p| p.pid);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn v(s: &[&str]) -> Vec<String> {
|
||||||
|
s.iter().map(|x| x.to_string()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn filters_minecraft_jvms() {
|
||||||
|
assert!(is_minecraft_jvm(&v(&["/usr/lib/jvm/java-21/bin/java", "-cp", "x", "net.fabricmc.loader.impl.launch.knot.KnotClient"])));
|
||||||
|
assert!(is_minecraft_jvm(&v(&["java", "-Dminecraft.launcher.brand=x", "Main"])));
|
||||||
|
assert!(!is_minecraft_jvm(&v(&["/usr/bin/java", "-jar", "gradle.jar"])));
|
||||||
|
assert!(!is_minecraft_jvm(&v(&["/usr/bin/bash", "-c", "minecraft"])));
|
||||||
|
assert!(!is_minecraft_jvm(&[]));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn labels() {
|
||||||
|
assert_eq!(instance_label(&v(&["java", "--version", "fabric-1.21.11"])), "fabric-1.21.11");
|
||||||
|
assert_eq!(instance_label(&v(&["java", "--version=1.21"])), "1.21");
|
||||||
|
assert_eq!(instance_label(&v(&["java", "--gameDir", "/home/u/.minecraft/instances/Pack/"])), "Pack");
|
||||||
|
assert_eq!(instance_label(&v(&["java", "KnotClient"])), "Minecraft");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ps_lines() {
|
||||||
|
let (pid, uid, args) = parse_ps_line(" 123 501 /Users/a/Library/Application Support/x/bin/java -Xmx2G --version 1.21 net.fabricmc.loader").unwrap();
|
||||||
|
assert_eq!((pid, uid), (123, 501));
|
||||||
|
assert_eq!(args[0], "/Users/a/Library/Application Support/x/bin/java");
|
||||||
|
assert!(is_minecraft_jvm(&args));
|
||||||
|
assert_eq!(instance_label(&args), "1.21");
|
||||||
|
assert!(parse_ps_line("garbage").is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
//! Terminal input and URL opening on Linux and macOS. std only (no termios structs).
|
||||||
|
//!
|
||||||
|
//! Raw mode is entered lazily on the first `poll_key` call via `stty -icanon -echo min 1 time 0`
|
||||||
|
//! on /dev/tty (ISIG stays on, so Ctrl-C still raises SIGINT). A reader thread blocks on the tty
|
||||||
|
//! and forwards bytes over a channel; `poll_key` parses them. The original settings (`stty -g`)
|
||||||
|
//! are restored by `restore_terminal()` and, for Ctrl-C, by a SIGINT handler that only writes to
|
||||||
|
//! a self-pipe (async-signal-safe); a helper thread then restores the terminal and exits 130.
|
||||||
|
use super::Key;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::Read;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::sync::mpsc::{channel, Receiver, RecvTimeoutError};
|
||||||
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
fn signal(signum: i32, handler: usize) -> usize;
|
||||||
|
fn pipe(fds: *mut i32) -> i32;
|
||||||
|
fn write(fd: i32, buf: *const u8, n: usize) -> isize;
|
||||||
|
fn read(fd: i32, buf: *mut u8, n: usize) -> isize;
|
||||||
|
}
|
||||||
|
const SIGINT: i32 = 2;
|
||||||
|
|
||||||
|
static SAVED: Mutex<Option<String>> = Mutex::new(None);
|
||||||
|
static INPUT: OnceLock<Mutex<Option<Receiver<u8>>>> = OnceLock::new();
|
||||||
|
static SIG_WRITE_FD: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1);
|
||||||
|
|
||||||
|
fn input() -> &'static Mutex<Option<Receiver<u8>>> {
|
||||||
|
INPUT.get_or_init(|| Mutex::new(None))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tty_stdin() -> Stdio {
|
||||||
|
match File::open("/dev/tty") {
|
||||||
|
Ok(f) => Stdio::from(f),
|
||||||
|
Err(_) => Stdio::inherit(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stty(args: &[&str]) -> Option<String> {
|
||||||
|
let out = Command::new("stty").args(args).stdin(tty_stdin()).stderr(Stdio::null()).output().ok()?;
|
||||||
|
out.status.success().then(|| String::from_utf8_lossy(&out.stdout).trim().to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nothing to do on unix (terminals understand ANSI); raw mode is entered lazily by `poll_key`.
|
||||||
|
pub fn enable_ansi() {}
|
||||||
|
|
||||||
|
extern "C" fn on_sigint(_sig: i32) {
|
||||||
|
let fd = SIG_WRITE_FD.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
if fd >= 0 {
|
||||||
|
let b = 1u8;
|
||||||
|
unsafe { write(fd, &b, 1) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_sigint() {
|
||||||
|
let mut fds = [0i32; 2];
|
||||||
|
if unsafe { pipe(fds.as_mut_ptr()) } != 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SIG_WRITE_FD.store(fds[1], std::sync::atomic::Ordering::Relaxed);
|
||||||
|
let rfd = fds[0];
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let mut b = 0u8;
|
||||||
|
loop {
|
||||||
|
let n = unsafe { read(rfd, &mut b, 1) };
|
||||||
|
if n == 1 {
|
||||||
|
restore_terminal();
|
||||||
|
println!();
|
||||||
|
std::process::exit(130);
|
||||||
|
}
|
||||||
|
if n == 0 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
unsafe { signal(SIGINT, on_sigint as extern "C" fn(i32) as usize) };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enter_raw() -> Option<Receiver<u8>> {
|
||||||
|
let saved = stty(&["-g"])?;
|
||||||
|
*SAVED.lock().ok()? = Some(saved);
|
||||||
|
stty(&["-icanon", "-echo", "min", "1", "time", "0"])?;
|
||||||
|
let mut tty: Box<dyn Read + Send> = match File::open("/dev/tty") {
|
||||||
|
Ok(f) => Box::new(f),
|
||||||
|
Err(_) => Box::new(std::io::stdin()),
|
||||||
|
};
|
||||||
|
let (tx, rx) = channel();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let mut b = [0u8; 1];
|
||||||
|
while let Ok(1) = tty.read(&mut b) {
|
||||||
|
if tx.send(b[0]).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
install_sigint();
|
||||||
|
Some(rx)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a pending key press, if any, without blocking (except ~30ms to disambiguate a lone Esc).
|
||||||
|
pub fn poll_key() -> Option<Key> {
|
||||||
|
let mut guard = input().lock().ok()?;
|
||||||
|
if guard.is_none() {
|
||||||
|
*guard = Some(enter_raw()?);
|
||||||
|
}
|
||||||
|
let rx = guard.as_ref()?;
|
||||||
|
loop {
|
||||||
|
let b = rx.try_recv().ok()?;
|
||||||
|
match b {
|
||||||
|
b'\n' | b'\r' => return Some(Key::Enter),
|
||||||
|
b'k' => return Some(Key::Up),
|
||||||
|
b'j' => return Some(Key::Down),
|
||||||
|
27 => {
|
||||||
|
let ms30 = Duration::from_millis(30);
|
||||||
|
match rx.recv_timeout(ms30) {
|
||||||
|
Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => return Some(Key::Escape),
|
||||||
|
Ok(b'[') | Ok(b'O') => match rx.recv_timeout(ms30) {
|
||||||
|
Ok(b'A') => return Some(Key::Up),
|
||||||
|
Ok(b'B') => return Some(Key::Down),
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Ok(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restores the terminal settings saved when raw mode was entered. Safe to call repeatedly.
|
||||||
|
pub fn restore_terminal() {
|
||||||
|
let saved = SAVED.lock().ok().and_then(|mut s| s.take());
|
||||||
|
if let Some(s) = saved {
|
||||||
|
stty(&[&s]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Opens a URL in the default browser (xdg-open / open). Returns whether the spawn succeeded.
|
||||||
|
pub fn open_url(url: &str) -> bool {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
let opener = "open";
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
let opener = "xdg-open";
|
||||||
|
Command::new(opener)
|
||||||
|
.arg(url)
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
.map(|_| true)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ use std::os::windows::ffi::OsStrExt;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use super::{JavaProcess, Key};
|
||||||
|
|
||||||
type Handle = *mut c_void;
|
type Handle = *mut c_void;
|
||||||
|
|
||||||
const PROCESS_CREATE_THREAD: u32 = 0x0002;
|
const PROCESS_CREATE_THREAD: u32 = 0x0002;
|
||||||
@@ -74,18 +76,8 @@ extern "C" {
|
|||||||
fn _getch() -> i32;
|
fn _getch() -> i32;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct JavaWindow {
|
/// Nothing to restore on Windows (keys are read with _getch).
|
||||||
pub pid: u32,
|
pub fn restore_terminal() {}
|
||||||
pub title: String,
|
|
||||||
pub exe: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Key {
|
|
||||||
Up,
|
|
||||||
Down,
|
|
||||||
Enter,
|
|
||||||
Escape,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enable_ansi() {
|
pub fn enable_ansi() {
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -154,12 +146,12 @@ unsafe extern "system" fn collect(hwnd: Handle, param: isize) -> i32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Visible top-level windows that belong to java.exe / javaw.exe, one entry per process.
|
/// Visible top-level windows that belong to java.exe / javaw.exe, one entry per process.
|
||||||
pub fn java_windows() -> Vec<JavaWindow> {
|
pub fn java_processes() -> Vec<JavaProcess> {
|
||||||
let mut windows: Vec<(u32, String)> = Vec::new();
|
let mut windows: Vec<(u32, String)> = Vec::new();
|
||||||
unsafe {
|
unsafe {
|
||||||
EnumWindows(collect, &mut windows as *mut _ as isize);
|
EnumWindows(collect, &mut windows as *mut _ as isize);
|
||||||
}
|
}
|
||||||
let mut out: Vec<JavaWindow> = Vec::new();
|
let mut out: Vec<JavaProcess> = Vec::new();
|
||||||
for (pid, title) in windows {
|
for (pid, title) in windows {
|
||||||
if out.iter().any(|w| w.pid == pid) {
|
if out.iter().any(|w| w.pid == pid) {
|
||||||
continue;
|
continue;
|
||||||
@@ -167,7 +159,7 @@ pub fn java_windows() -> Vec<JavaWindow> {
|
|||||||
let Some(path) = image_name(pid) else { continue };
|
let Some(path) = image_name(pid) else { continue };
|
||||||
let exe = path.rsplit(['\\', '/']).next().unwrap_or("").to_string();
|
let exe = path.rsplit(['\\', '/']).next().unwrap_or("").to_string();
|
||||||
if exe.eq_ignore_ascii_case("javaw.exe") || exe.eq_ignore_ascii_case("java.exe") {
|
if exe.eq_ignore_ascii_case("javaw.exe") || exe.eq_ignore_ascii_case("java.exe") {
|
||||||
out.push(JavaWindow { pid, title, exe });
|
out.push(JavaProcess { pid, title, exe });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
@@ -1,14 +1,16 @@
|
|||||||
//! JVMTI state and the two natives of dev.zprestige.prestige.inject.NativeBridge.
|
//! JVMTI state and the two natives of dev.zprestige.prestige.inject.NativeBridge.
|
||||||
use crate::jni::*;
|
use crate::jni::*;
|
||||||
use std::ffi::{c_char, c_void};
|
use std::ffi::{c_char, c_void};
|
||||||
use std::sync::atomic::{AtomicPtr, AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicPtr, Ordering};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
static JVMTI: AtomicPtr<*const *const c_void> = AtomicPtr::new(std::ptr::null_mut());
|
static JVMTI: AtomicPtr<*const *const c_void> = AtomicPtr::new(std::ptr::null_mut());
|
||||||
/// jclass whose bytes the ClassFileLoadHook should capture during the current RetransformClasses call (0 = none).
|
/// jclass references (as addresses) whose bytes the ClassFileLoadHook captures during the current RetransformClasses call.
|
||||||
static CAPTURE_TARGET: AtomicUsize = AtomicUsize::new(0);
|
/// Never held across the JVMTI call: the hook runs on the calling thread and locks it again.
|
||||||
static CAPTURED: Mutex<Option<Vec<u8>>> = Mutex::new(None);
|
static CAPTURE_TARGETS: Mutex<Vec<usize>> = Mutex::new(Vec::new());
|
||||||
/// Serialises classBytes calls: CAPTURE_TARGET holds a thread-local jclass reference, so only one thread may capture at a time.
|
/// Captured bytes, index-aligned with CAPTURE_TARGETS.
|
||||||
|
static CAPTURED: Mutex<Vec<Option<Vec<u8>>>> = Mutex::new(Vec::new());
|
||||||
|
/// Serialises classBytes calls: CAPTURE_TARGETS holds thread-local jclass references, so only one thread may capture at a time.
|
||||||
static CAPTURE_LOCK: Mutex<()> = Mutex::new(());
|
static CAPTURE_LOCK: Mutex<()> = Mutex::new(());
|
||||||
|
|
||||||
/// Acquires the JVMTI capabilities and enables the class file load hook. Called once from the worker thread.
|
/// Acquires the JVMTI capabilities and enables the class file load hook. Called once from the worker thread.
|
||||||
@@ -22,7 +24,7 @@ pub unsafe fn init_jvmti(vm: VmPtr) -> Result<(), String> {
|
|||||||
if rc != 0 {
|
if rc != 0 {
|
||||||
return Err(format!("AddCapabilities failed ({rc})"));
|
return Err(format!("AddCapabilities failed ({rc})"));
|
||||||
}
|
}
|
||||||
enable_class_file_load_hook(jvmti, class_file_load_hook)?;
|
install_class_file_load_hook(jvmti, class_file_load_hook)?;
|
||||||
JVMTI.store(jvmti, Ordering::SeqCst);
|
JVMTI.store(jvmti, Ordering::SeqCst);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -40,14 +42,22 @@ unsafe extern "system" fn class_file_load_hook(
|
|||||||
_new_len: *mut JInt,
|
_new_len: *mut JInt,
|
||||||
_new_data: *mut *mut u8,
|
_new_data: *mut *mut u8,
|
||||||
) {
|
) {
|
||||||
let target = CAPTURE_TARGET.load(Ordering::SeqCst);
|
if class_being_redefined.is_null() || data.is_null() {
|
||||||
if target == 0 || class_being_redefined.is_null() || data.is_null() {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let targets: Vec<usize> = match CAPTURE_TARGETS.lock() {
|
||||||
|
Ok(t) if !t.is_empty() => t.clone(),
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
let jni = Jni::new(env);
|
let jni = Jni::new(env);
|
||||||
if jni.is_same_object(class_being_redefined, target as JObject) {
|
for (i, &target) in targets.iter().enumerate() {
|
||||||
if let Ok(mut slot) = CAPTURED.lock() {
|
if jni.is_same_object(class_being_redefined, target as JObject) {
|
||||||
*slot = Some(std::slice::from_raw_parts(data, data_len as usize).to_vec());
|
if let Ok(mut slots) = CAPTURED.lock() {
|
||||||
|
if let Some(slot) = slots.get_mut(i) {
|
||||||
|
*slot = Some(std::slice::from_raw_parts(data, data_len as usize).to_vec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,22 +68,39 @@ pub unsafe extern "system" fn native_class_bytes(env: EnvPtr, _cls: JClass, targ
|
|||||||
if jvmti.is_null() {
|
if jvmti.is_null() {
|
||||||
return std::ptr::null_mut();
|
return std::ptr::null_mut();
|
||||||
}
|
}
|
||||||
let _serial = CAPTURE_LOCK.lock();
|
match capture(jvmti, &[target]).and_then(|mut v| v.pop().flatten()) {
|
||||||
if let Ok(mut slot) = CAPTURED.lock() {
|
Some(bytes) => to_byte_array(&Jni::new(env), &bytes),
|
||||||
*slot = None;
|
None => std::ptr::null_mut(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retransforms `classes` in one JVMTI call and returns the captured bytes, index-aligned (None = not captured).
|
||||||
|
unsafe fn capture(jvmti: JvmtiPtr, classes: &[JClass]) -> Option<Vec<Option<Vec<u8>>>> {
|
||||||
|
let _serial = CAPTURE_LOCK.lock();
|
||||||
|
if let Ok(mut c) = CAPTURED.lock() {
|
||||||
|
*c = vec![None; classes.len()];
|
||||||
|
}
|
||||||
|
if let Ok(mut t) = CAPTURE_TARGETS.lock() {
|
||||||
|
*t = classes.iter().map(|&c| c as usize).collect();
|
||||||
|
}
|
||||||
|
let enabled = set_class_file_load_hook_enabled(jvmti, true);
|
||||||
|
let rc = if enabled.is_ok() { retransform_classes(jvmti, classes) } else { -1 };
|
||||||
|
let _ = set_class_file_load_hook_enabled(jvmti, false);
|
||||||
|
if let Ok(mut t) = CAPTURE_TARGETS.lock() {
|
||||||
|
t.clear();
|
||||||
|
}
|
||||||
|
if let Err(e) = enabled {
|
||||||
|
crate::log!("classBytes: {e}");
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
CAPTURE_TARGET.store(target as usize, Ordering::SeqCst);
|
|
||||||
let rc = retransform_class(jvmti, target);
|
|
||||||
CAPTURE_TARGET.store(0, Ordering::SeqCst);
|
|
||||||
if rc != 0 {
|
if rc != 0 {
|
||||||
crate::log!("RetransformClasses failed ({rc})");
|
crate::log!("RetransformClasses failed ({rc})");
|
||||||
return std::ptr::null_mut();
|
return None;
|
||||||
}
|
}
|
||||||
let bytes = CAPTURED.lock().ok().and_then(|mut slot| slot.take());
|
CAPTURED.lock().ok().map(|mut c| std::mem::take(&mut *c))
|
||||||
let Some(bytes) = bytes else {
|
}
|
||||||
return std::ptr::null_mut();
|
|
||||||
};
|
unsafe fn to_byte_array(jni: &Jni, bytes: &[u8]) -> JObject {
|
||||||
let jni = Jni::new(env);
|
|
||||||
let array = jni.new_byte_array(bytes.len() as JInt);
|
let array = jni.new_byte_array(bytes.len() as JInt);
|
||||||
if !array.is_null() {
|
if !array.is_null() {
|
||||||
jni.set_byte_array_region(array, 0, bytes.len() as JInt, bytes.as_ptr());
|
jni.set_byte_array_region(array, 0, bytes.len() as JInt, bytes.as_ptr());
|
||||||
@@ -81,6 +108,80 @@ pub unsafe extern "system" fn native_class_bytes(env: EnvPtr, _cls: JClass, targ
|
|||||||
array
|
array
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// byte[][] classBytesBatch(Class[]): classBytes for many classes with a single RetransformClasses call (one safepoint, one pass
|
||||||
|
/// over the other agents' transformers). Entries are null for classes that were not captured; the result is null on failure.
|
||||||
|
pub unsafe extern "system" fn native_class_bytes_batch(env: EnvPtr, _cls: JClass, targets: JObject) -> JObject {
|
||||||
|
let jvmti = JVMTI.load(Ordering::SeqCst);
|
||||||
|
if jvmti.is_null() || targets.is_null() {
|
||||||
|
return std::ptr::null_mut();
|
||||||
|
}
|
||||||
|
let jni = Jni::new(env);
|
||||||
|
let n = jni.get_array_length(targets);
|
||||||
|
if n <= 0 {
|
||||||
|
return std::ptr::null_mut();
|
||||||
|
}
|
||||||
|
jni.ensure_local_capacity(2 * n + 16);
|
||||||
|
let classes: Vec<JClass> = (0..n).map(|i| jni.get_object_array_element(targets, i)).collect();
|
||||||
|
let Some(captured) = capture(jvmti, &classes) else {
|
||||||
|
return std::ptr::null_mut();
|
||||||
|
};
|
||||||
|
let byte_array_class = jni.find_class("[B");
|
||||||
|
let result = jni.new_object_array(n, byte_array_class, std::ptr::null_mut());
|
||||||
|
if result.is_null() {
|
||||||
|
return std::ptr::null_mut();
|
||||||
|
}
|
||||||
|
for (i, bytes) in captured.iter().enumerate() {
|
||||||
|
if let Some(bytes) = bytes {
|
||||||
|
let array = to_byte_array(&jni, bytes);
|
||||||
|
if !array.is_null() {
|
||||||
|
jni.set_object_array_element(result, i as JInt, array);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// int redefineBatch(Class[], byte[][]): JVMTI RedefineClasses of many classes in one call (one safepoint). It is all or nothing:
|
||||||
|
/// the JVMTI error code is returned (0 = success) and no class is changed on failure.
|
||||||
|
pub unsafe extern "system" fn native_redefine_batch(env: EnvPtr, _cls: JClass, targets: JObject, bytes: JObject) -> JInt {
|
||||||
|
let jvmti = JVMTI.load(Ordering::SeqCst);
|
||||||
|
if jvmti.is_null() || targets.is_null() || bytes.is_null() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
let jni = Jni::new(env);
|
||||||
|
let n = jni.get_array_length(targets);
|
||||||
|
if n <= 0 || jni.get_array_length(bytes) != n {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
jni.ensure_local_capacity(2 * n + 16);
|
||||||
|
let mut buffers: Vec<Vec<u8>> = Vec::with_capacity(n as usize);
|
||||||
|
let mut classes: Vec<JClass> = Vec::with_capacity(n as usize);
|
||||||
|
for i in 0..n {
|
||||||
|
let array = jni.get_object_array_element(bytes, i);
|
||||||
|
if array.is_null() {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
let len = jni.get_array_length(array);
|
||||||
|
if len < 0 {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
let mut buf = vec![0u8; len as usize];
|
||||||
|
jni.get_byte_array_region(array, 0, len, buf.as_mut_ptr());
|
||||||
|
buffers.push(buf);
|
||||||
|
classes.push(jni.get_object_array_element(targets, i));
|
||||||
|
}
|
||||||
|
let defs: Vec<ClassDefinition> = classes
|
||||||
|
.iter()
|
||||||
|
.zip(&buffers)
|
||||||
|
.map(|(&klass, buf)| ClassDefinition { klass, class_byte_count: buf.len() as JInt, class_bytes: buf.as_ptr() })
|
||||||
|
.collect();
|
||||||
|
let rc = redefine_classes(jvmti, &defs);
|
||||||
|
if rc != 0 {
|
||||||
|
crate::log!("RedefineClasses (batch of {n}) failed ({rc})");
|
||||||
|
}
|
||||||
|
rc
|
||||||
|
}
|
||||||
|
|
||||||
/// int redefine(Class, byte[]): JVMTI RedefineClasses of a single class; returns the JVMTI error code (0 = success).
|
/// int redefine(Class, byte[]): JVMTI RedefineClasses of a single class; returns the JVMTI error code (0 = success).
|
||||||
pub unsafe extern "system" fn native_redefine(env: EnvPtr, _cls: JClass, target: JClass, bytes: JObject) -> JInt {
|
pub unsafe extern "system" fn native_redefine(env: EnvPtr, _cls: JClass, target: JClass, bytes: JObject) -> JInt {
|
||||||
let jvmti = JVMTI.load(Ordering::SeqCst);
|
let jvmti = JVMTI.load(Ordering::SeqCst);
|
||||||
@@ -111,6 +212,7 @@ pub unsafe extern "system" fn native_log(env: EnvPtr, _cls: JClass, message: JOb
|
|||||||
crate::log!("[java] {text}");
|
crate::log!("[java] {text}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
extern "system" {
|
extern "system" {
|
||||||
fn FreeLibraryAndExitThread(module: *mut c_void, exit_code: u32) -> !;
|
fn FreeLibraryAndExitThread(module: *mut c_void, exit_code: u32) -> !;
|
||||||
}
|
}
|
||||||
@@ -129,11 +231,18 @@ pub unsafe extern "system" fn native_release(env: EnvPtr, cls: JClass) {
|
|||||||
if rc != JNI_OK {
|
if rc != JNI_OK {
|
||||||
crate::log!("release: UnregisterNatives failed ({rc})");
|
crate::log!("release: UnregisterNatives failed ({rc})");
|
||||||
}
|
}
|
||||||
crate::log!("[java] released, unloading the loader");
|
#[cfg(windows)]
|
||||||
let module = crate::MODULE as usize;
|
{
|
||||||
// The thread below runs code from this DLL until FreeLibraryAndExitThread; the delay lets in-flight native calls return.
|
crate::log!("[java] released, unloading the loader");
|
||||||
let _ = std::thread::Builder::new().name("prestige-unload".into()).spawn(move || {
|
let module = crate::MODULE as usize;
|
||||||
std::thread::sleep(std::time::Duration::from_millis(2000));
|
// The thread below runs code from this DLL until FreeLibraryAndExitThread; the delay lets in-flight native calls return.
|
||||||
FreeLibraryAndExitThread(module as *mut c_void, 0);
|
let _ = std::thread::Builder::new().name("prestige-unload".into()).spawn(move || {
|
||||||
});
|
std::thread::sleep(std::time::Duration::from_millis(2000));
|
||||||
|
FreeLibraryAndExitThread(module as *mut c_void, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// On unix the library is deliberately NOT dlclose'd: a thread of ours would return into unmapped code (and the
|
||||||
|
// JVM may still hold the agent handle). The hook and natives are gone; the code just stays resident.
|
||||||
|
#[cfg(unix)]
|
||||||
|
crate::log!("[java] released (library stays resident on this platform)");
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,6 @@ pub type JBool = u8;
|
|||||||
pub type EnvPtr = *mut *const *const c_void;
|
pub type EnvPtr = *mut *const *const c_void;
|
||||||
|
|
||||||
pub const JNI_OK: JInt = 0;
|
pub const JNI_OK: JInt = 0;
|
||||||
pub const JNI_VERSION_1_8: JInt = 0x0001_0008;
|
|
||||||
pub const JVMTI_VERSION_1_2: JInt = 0x3001_0200;
|
pub const JVMTI_VERSION_1_2: JInt = 0x3001_0200;
|
||||||
|
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
@@ -60,9 +59,6 @@ impl Jni {
|
|||||||
pub unsafe fn new_global_ref(&self, o: JObject) -> JObject {
|
pub unsafe fn new_global_ref(&self, o: JObject) -> JObject {
|
||||||
f!(self, 21, unsafe extern "system" fn(EnvPtr, JObject) -> JObject)(self.env, o)
|
f!(self, 21, unsafe extern "system" fn(EnvPtr, JObject) -> JObject)(self.env, o)
|
||||||
}
|
}
|
||||||
pub unsafe fn delete_local_ref(&self, o: JObject) {
|
|
||||||
f!(self, 23, unsafe extern "system" fn(EnvPtr, JObject))(self.env, o)
|
|
||||||
}
|
|
||||||
pub unsafe fn is_same_object(&self, a: JObject, b: JObject) -> bool {
|
pub unsafe fn is_same_object(&self, a: JObject, b: JObject) -> bool {
|
||||||
f!(self, 24, unsafe extern "system" fn(EnvPtr, JObject, JObject) -> JBool)(self.env, a, b) != 0
|
f!(self, 24, unsafe extern "system" fn(EnvPtr, JObject, JObject) -> JBool)(self.env, a, b) != 0
|
||||||
}
|
}
|
||||||
@@ -108,6 +104,15 @@ impl Jni {
|
|||||||
pub unsafe fn get_array_length(&self, a: JObject) -> JInt {
|
pub unsafe fn get_array_length(&self, a: JObject) -> JInt {
|
||||||
f!(self, 171, unsafe extern "system" fn(EnvPtr, JObject) -> JInt)(self.env, a)
|
f!(self, 171, unsafe extern "system" fn(EnvPtr, JObject) -> JInt)(self.env, a)
|
||||||
}
|
}
|
||||||
|
pub unsafe fn get_object_array_element(&self, a: JObject, i: JInt) -> JObject {
|
||||||
|
f!(self, 173, unsafe extern "system" fn(EnvPtr, JObject, JInt) -> JObject)(self.env, a, i)
|
||||||
|
}
|
||||||
|
pub unsafe fn set_object_array_element(&self, a: JObject, i: JInt, v: JObject) {
|
||||||
|
f!(self, 174, unsafe extern "system" fn(EnvPtr, JObject, JInt, JObject))(self.env, a, i, v)
|
||||||
|
}
|
||||||
|
pub unsafe fn ensure_local_capacity(&self, capacity: JInt) -> JInt {
|
||||||
|
f!(self, 26, unsafe extern "system" fn(EnvPtr, JInt) -> JInt)(self.env, capacity)
|
||||||
|
}
|
||||||
pub unsafe fn new_object_array(&self, len: JInt, c: JClass, init: JObject) -> JObject {
|
pub unsafe fn new_object_array(&self, len: JInt, c: JClass, init: JObject) -> JObject {
|
||||||
f!(self, 172, unsafe extern "system" fn(EnvPtr, JInt, JClass, JObject) -> JObject)(self.env, len, c, init)
|
f!(self, 172, unsafe extern "system" fn(EnvPtr, JInt, JClass, JObject) -> JObject)(self.env, len, c, init)
|
||||||
}
|
}
|
||||||
@@ -126,9 +131,6 @@ impl Jni {
|
|||||||
pub unsafe fn unregister_natives(&self, c: JClass) -> JInt {
|
pub unsafe fn unregister_natives(&self, c: JClass) -> JInt {
|
||||||
f!(self, 216, unsafe extern "system" fn(EnvPtr, JClass) -> JInt)(self.env, c)
|
f!(self, 216, unsafe extern "system" fn(EnvPtr, JClass) -> JInt)(self.env, c)
|
||||||
}
|
}
|
||||||
pub unsafe fn exception_check(&self) -> bool {
|
|
||||||
f!(self, 228, unsafe extern "system" fn(EnvPtr) -> JBool)(self.env) != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns "ExceptionClass: message" of the pending exception and clears it.
|
/// Returns "ExceptionClass: message" of the pending exception and clears it.
|
||||||
pub unsafe fn take_exception(&self) -> Option<String> {
|
pub unsafe fn take_exception(&self) -> Option<String> {
|
||||||
@@ -236,8 +238,8 @@ pub unsafe fn add_capabilities(j: JvmtiPtr, caps: &Capabilities) -> JInt {
|
|||||||
f(j, caps)
|
f(j, caps)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Installs the ClassFileLoadHook callback (field 5 of jvmtiEventCallbacks) and enables the event.
|
/// Installs the ClassFileLoadHook callback (field 5 of jvmtiEventCallbacks); the event itself stays off until enabled.
|
||||||
pub unsafe fn enable_class_file_load_hook(j: JvmtiPtr, hook: ClassFileLoadHook) -> Result<(), String> {
|
pub unsafe fn install_class_file_load_hook(j: JvmtiPtr, hook: ClassFileLoadHook) -> Result<(), String> {
|
||||||
let mut callbacks = [0usize; 40];
|
let mut callbacks = [0usize; 40];
|
||||||
callbacks[4] = hook as usize;
|
callbacks[4] = hook as usize;
|
||||||
let set_callbacks = std::mem::transmute::<*const c_void, unsafe extern "system" fn(JvmtiPtr, *const usize, JInt) -> JInt>(*(*j).add(121));
|
let set_callbacks = std::mem::transmute::<*const c_void, unsafe extern "system" fn(JvmtiPtr, *const usize, JInt) -> JInt>(*(*j).add(121));
|
||||||
@@ -245,17 +247,28 @@ pub unsafe fn enable_class_file_load_hook(j: JvmtiPtr, hook: ClassFileLoadHook)
|
|||||||
if rc != 0 {
|
if rc != 0 {
|
||||||
return Err(format!("SetEventCallbacks failed ({rc})"));
|
return Err(format!("SetEventCallbacks failed ({rc})"));
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Turns the ClassFileLoadHook event on or off. It is only on while class bytes are being captured: with the event enabled the
|
||||||
|
/// JVM hands the bytes of every class it loads to the agent, which slows class loading down for the whole game.
|
||||||
|
pub unsafe fn set_class_file_load_hook_enabled(j: JvmtiPtr, enabled: bool) -> Result<(), String> {
|
||||||
let set_mode = std::mem::transmute::<*const c_void, unsafe extern "C" fn(JvmtiPtr, JInt, JInt, JObject, ...) -> JInt>(*(*j).add(1));
|
let set_mode = std::mem::transmute::<*const c_void, unsafe extern "C" fn(JvmtiPtr, JInt, JInt, JObject, ...) -> JInt>(*(*j).add(1));
|
||||||
let rc = set_mode(j, 1, EVENT_CLASS_FILE_LOAD_HOOK, std::ptr::null_mut());
|
let rc = set_mode(j, enabled as JInt, EVENT_CLASS_FILE_LOAD_HOOK, std::ptr::null_mut());
|
||||||
if rc != 0 {
|
if rc != 0 {
|
||||||
return Err(format!("SetEventNotificationMode failed ({rc})"));
|
return Err(format!("SetEventNotificationMode({enabled}) failed ({rc})"));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub unsafe fn retransform_class(j: JvmtiPtr, class: JClass) -> JInt {
|
pub unsafe fn retransform_classes(j: JvmtiPtr, classes: &[JClass]) -> JInt {
|
||||||
let f = std::mem::transmute::<*const c_void, unsafe extern "system" fn(JvmtiPtr, JInt, *const JClass) -> JInt>(*(*j).add(151));
|
let f = std::mem::transmute::<*const c_void, unsafe extern "system" fn(JvmtiPtr, JInt, *const JClass) -> JInt>(*(*j).add(151));
|
||||||
f(j, 1, &class)
|
f(j, classes.len() as JInt, classes.as_ptr())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn redefine_classes(j: JvmtiPtr, defs: &[ClassDefinition]) -> JInt {
|
||||||
|
let f = std::mem::transmute::<*const c_void, unsafe extern "system" fn(JvmtiPtr, JInt, *const ClassDefinition) -> JInt>(*(*j).add(86));
|
||||||
|
f(j, defs.len() as JInt, defs.as_ptr())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub unsafe fn redefine_class(j: JvmtiPtr, def: &ClassDefinition) -> JInt {
|
pub unsafe fn redefine_class(j: JvmtiPtr, def: &ClassDefinition) -> JInt {
|
||||||
|
|||||||
@@ -10,14 +10,18 @@ mod worker;
|
|||||||
|
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
const DLL_PROCESS_ATTACH: u32 = 1;
|
const DLL_PROCESS_ATTACH: u32 = 1;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
extern "system" {
|
extern "system" {
|
||||||
fn DisableThreadLibraryCalls(module: *mut c_void) -> i32;
|
fn DisableThreadLibraryCalls(module: *mut c_void) -> i32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
pub(crate) static mut MODULE: *mut c_void = std::ptr::null_mut();
|
pub(crate) static mut MODULE: *mut c_void = std::ptr::null_mut();
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "system" fn DllMain(module: *mut c_void, reason: u32, _reserved: *mut c_void) -> i32 {
|
pub extern "system" fn DllMain(module: *mut c_void, reason: u32, _reserved: *mut c_void) -> i32 {
|
||||||
if reason == DLL_PROCESS_ATTACH {
|
if reason == DLL_PROCESS_ATTACH {
|
||||||
@@ -30,3 +34,30 @@ pub extern "system" fn DllMain(module: *mut c_void, reason: u32, _reserved: *mut
|
|||||||
}
|
}
|
||||||
1
|
1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The JavaVM handed to Agent_OnAttach / Agent_OnLoad by the JVM (unix); read by the worker.
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub(crate) static VM: std::sync::atomic::AtomicPtr<c_void> = std::sync::atomic::AtomicPtr::new(std::ptr::null_mut());
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn start_worker(vm: *mut c_void) -> i32 {
|
||||||
|
VM.store(vm, std::sync::atomic::Ordering::SeqCst);
|
||||||
|
match std::thread::Builder::new().name("prestige-loader".into()).spawn(worker::run) {
|
||||||
|
Ok(_) => 0,
|
||||||
|
Err(_) => -1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called by HotSpot when the library is loaded through the attach protocol (JVMTI.agent_load).
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn Agent_OnAttach(vm: *mut c_void, _options: *mut std::ffi::c_char, _reserved: *mut c_void) -> i32 {
|
||||||
|
start_worker(vm)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Called by the JVM when the library is given as -agentpath at start-up.
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn Agent_OnLoad(vm: *mut c_void, _options: *mut std::ffi::c_char, _reserved: *mut c_void) -> i32 {
|
||||||
|
start_worker(vm)
|
||||||
|
}
|
||||||
@@ -1,21 +1,55 @@
|
|||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
extern "system" {
|
extern "system" {
|
||||||
fn GetModuleFileNameW(module: *mut c_void, buf: *mut u16, size: u32) -> u32;
|
fn GetModuleFileNameW(module: *mut c_void, buf: *mut u16, size: u32) -> u32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[repr(C)]
|
||||||
|
struct DlInfo {
|
||||||
|
dli_fname: *const std::ffi::c_char,
|
||||||
|
dli_fbase: *mut c_void,
|
||||||
|
dli_sname: *const std::ffi::c_char,
|
||||||
|
dli_saddr: *mut c_void,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
extern "C" {
|
||||||
|
fn dladdr(addr: *const c_void, info: *mut DlInfo) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn module_path() -> PathBuf {
|
||||||
|
fn anchor() {}
|
||||||
|
let mut info = DlInfo { dli_fname: std::ptr::null(), dli_fbase: std::ptr::null_mut(), dli_sname: std::ptr::null(), dli_saddr: std::ptr::null_mut() };
|
||||||
|
let ok = unsafe { dladdr(anchor as fn() as usize as *const c_void, &mut info) };
|
||||||
|
if ok != 0 && !info.dli_fname.is_null() {
|
||||||
|
let name = unsafe { std::ffi::CStr::from_ptr(info.dli_fname) };
|
||||||
|
PathBuf::from(std::ffi::OsStr::from_bytes(name.to_bytes()))
|
||||||
|
} else {
|
||||||
|
std::env::temp_dir().join("libprestige_native")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn module_path() -> PathBuf {
|
||||||
|
let mut buf = [0u16; 1024];
|
||||||
|
let len = unsafe { GetModuleFileNameW(crate::MODULE, buf.as_mut_ptr(), buf.len() as u32) } as usize;
|
||||||
|
PathBuf::from(String::from_utf16_lossy(&buf[..len]))
|
||||||
|
}
|
||||||
|
|
||||||
fn log_path() -> &'static PathBuf {
|
fn log_path() -> &'static PathBuf {
|
||||||
static PATH: OnceLock<PathBuf> = OnceLock::new();
|
static PATH: OnceLock<PathBuf> = OnceLock::new();
|
||||||
PATH.get_or_init(|| {
|
PATH.get_or_init(|| {
|
||||||
let mut buf = [0u16; 1024];
|
module_path().with_file_name("prestige-native.log")
|
||||||
let len = unsafe { GetModuleFileNameW(crate::MODULE, buf.as_mut_ptr(), buf.len() as u32) } as usize;
|
|
||||||
let dll = PathBuf::from(String::from_utf16_lossy(&buf[..len]));
|
|
||||||
dll.with_file_name("prestige-native.log")
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
//! The loader worker: JVM discovery, payload installation into Fabric's Knot class loader, native registration, bootstrap.
|
//! The loader worker: JVM discovery, payload installation into Fabric's Knot class loader, native registration, bootstrap.
|
||||||
use crate::bridge::{init_jvmti, native_class_bytes, native_log, native_redefine, native_release};
|
use crate::bridge::{init_jvmti, native_class_bytes, native_class_bytes_batch, native_log, native_redefine, native_redefine_batch, native_release};
|
||||||
use crate::jni::*;
|
use crate::jni::*;
|
||||||
use crate::log;
|
use crate::log;
|
||||||
use std::ffi::{c_char, c_void};
|
#[cfg(windows)]
|
||||||
|
use std::ffi::c_char;
|
||||||
|
use std::ffi::c_void;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
#[cfg(windows)]
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
extern "system" {
|
extern "system" {
|
||||||
fn GetModuleHandleA(name: *const c_char) -> *mut c_void;
|
fn GetModuleHandleA(name: *const c_char) -> *mut c_void;
|
||||||
fn GetProcAddress(module: *mut c_void, name: *const c_char) -> *mut c_void;
|
fn GetProcAddress(module: *mut c_void, name: *const c_char) -> *mut c_void;
|
||||||
fn GetCurrentProcessId() -> u32;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static PAYLOAD: &[u8] = include_bytes!(env!("PRESTIGE_PAYLOAD_JAR"));
|
static PAYLOAD: &[u8] = include_bytes!(env!("PRESTIGE_PAYLOAD_JAR"));
|
||||||
@@ -18,7 +21,7 @@ const NATIVE_BRIDGE: &str = "dev.zprestige.prestige.inject.NativeBridge";
|
|||||||
const BOOTSTRAP: &str = "dev.zprestige.prestige.inject.Bootstrap";
|
const BOOTSTRAP: &str = "dev.zprestige.prestige.inject.Bootstrap";
|
||||||
|
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
log!("worker started, pid {}", unsafe { GetCurrentProcessId() });
|
log!("worker started, pid {}", std::process::id());
|
||||||
match unsafe { install() } {
|
match unsafe { install() } {
|
||||||
Ok(()) => log!("bootstrap returned"),
|
Ok(()) => log!("bootstrap returned"),
|
||||||
Err(e) => log!("FAILED: {e}"),
|
Err(e) => log!("FAILED: {e}"),
|
||||||
@@ -50,7 +53,7 @@ unsafe fn install() -> Result<(), String> {
|
|||||||
register_natives(&jni, loader)?;
|
register_natives(&jni, loader)?;
|
||||||
log!("natives registered");
|
log!("natives registered");
|
||||||
|
|
||||||
let bootstrap = class_for_name(&jni, BOOTSTRAP, loader)?;
|
let bootstrap = class_for_name(&jni, BOOTSTRAP, loader, true)?;
|
||||||
let start = jni.get_static_method_id(bootstrap, "start", "()V");
|
let start = jni.get_static_method_id(bootstrap, "start", "()V");
|
||||||
jni.check("resolve Bootstrap.start")?;
|
jni.check("resolve Bootstrap.start")?;
|
||||||
// Bootstrap.start waits for the client, installs the hooks and schedules the client initialisation.
|
// Bootstrap.start waits for the client, installs the hooks and schedules the client initialisation.
|
||||||
@@ -59,7 +62,18 @@ unsafe fn install() -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The JavaVM passed to Agent_OnAttach / Agent_OnLoad.
|
||||||
|
#[cfg(unix)]
|
||||||
|
unsafe fn find_vm() -> Result<VmPtr, String> {
|
||||||
|
let vm = crate::VM.load(std::sync::atomic::Ordering::SeqCst) as VmPtr;
|
||||||
|
if vm.is_null() {
|
||||||
|
return Err("no JavaVM was passed to the agent".into());
|
||||||
|
}
|
||||||
|
Ok(vm)
|
||||||
|
}
|
||||||
|
|
||||||
/// Waits for jvm.dll and a created JavaVM (the injector may run before Minecraft has started its JVM).
|
/// Waits for jvm.dll and a created JavaVM (the injector may run before Minecraft has started its JVM).
|
||||||
|
#[cfg(windows)]
|
||||||
unsafe fn find_vm() -> Result<VmPtr, String> {
|
unsafe fn find_vm() -> Result<VmPtr, String> {
|
||||||
let mut module = std::ptr::null_mut();
|
let mut module = std::ptr::null_mut();
|
||||||
for _ in 0..600 {
|
for _ in 0..600 {
|
||||||
@@ -91,7 +105,7 @@ unsafe fn find_vm() -> Result<VmPtr, String> {
|
|||||||
fn write_payload() -> Result<PathBuf, String> {
|
fn write_payload() -> Result<PathBuf, String> {
|
||||||
let dir = std::env::temp_dir().join("PrestigeLoader");
|
let dir = std::env::temp_dir().join("PrestigeLoader");
|
||||||
std::fs::create_dir_all(&dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
|
std::fs::create_dir_all(&dir).map_err(|e| format!("create {}: {e}", dir.display()))?;
|
||||||
let path = dir.join(format!("prestige-payload-{}.jar", unsafe { GetCurrentProcessId() }));
|
let path = dir.join(format!("prestige-payload-{}.jar", std::process::id()));
|
||||||
std::fs::write(&path, PAYLOAD).map_err(|e| format!("write {}: {e}", path.display()))?;
|
std::fs::write(&path, PAYLOAD).map_err(|e| format!("write {}: {e}", path.display()))?;
|
||||||
Ok(path)
|
Ok(path)
|
||||||
}
|
}
|
||||||
@@ -99,9 +113,8 @@ fn write_payload() -> Result<PathBuf, String> {
|
|||||||
/// FabricLauncherBase.getLauncher().addToClassPath(path) so the payload and the game classes share the Knot class loader.
|
/// FabricLauncherBase.getLauncher().addToClassPath(path) so the payload and the game classes share the Knot class loader.
|
||||||
/// Returns a global reference to the Knot target class loader.
|
/// Returns a global reference to the Knot target class loader.
|
||||||
unsafe fn add_to_knot(jni: &Jni, jar: &std::path::Path) -> Result<JObject, String> {
|
unsafe fn add_to_knot(jni: &Jni, jar: &std::path::Path) -> Result<JObject, String> {
|
||||||
// A thread attached from native code resolves classes through the system class loader, which holds fabric-loader.
|
jni.ensure_local_capacity(4096);
|
||||||
let base = jni.find_class("net/fabricmc/loader/impl/launch/FabricLauncherBase");
|
let (base, fabric_loader) = locate_fabric(jni)?;
|
||||||
jni.check("FabricLauncherBase (is this a Fabric instance?)")?;
|
|
||||||
let get_launcher = jni.get_static_method_id(base, "getLauncher", "()Lnet/fabricmc/loader/impl/launch/FabricLauncher;");
|
let get_launcher = jni.get_static_method_id(base, "getLauncher", "()Lnet/fabricmc/loader/impl/launch/FabricLauncher;");
|
||||||
jni.check("FabricLauncherBase.getLauncher")?;
|
jni.check("FabricLauncherBase.getLauncher")?;
|
||||||
let launcher = jni.call_static_object(base, get_launcher, &[]);
|
let launcher = jni.call_static_object(base, get_launcher, &[]);
|
||||||
@@ -110,8 +123,7 @@ unsafe fn add_to_knot(jni: &Jni, jar: &std::path::Path) -> Result<JObject, Strin
|
|||||||
return Err("Fabric launcher is not available yet".into());
|
return Err("Fabric launcher is not available yet".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let launcher_class = jni.find_class("net/fabricmc/loader/impl/launch/FabricLauncher");
|
let launcher_class = load_fabric_class(jni, "net.fabricmc.loader.impl.launch.FabricLauncher", fabric_loader)?;
|
||||||
jni.check("FabricLauncher")?;
|
|
||||||
let add = jni.get_method_id(launcher_class, "addToClassPath", "(Ljava/nio/file/Path;[Ljava/lang/String;)V");
|
let add = jni.get_method_id(launcher_class, "addToClassPath", "(Ljava/nio/file/Path;[Ljava/lang/String;)V");
|
||||||
jni.check("FabricLauncher.addToClassPath")?;
|
jni.check("FabricLauncher.addToClassPath")?;
|
||||||
let target = jni.get_method_id(launcher_class, "getTargetClassLoader", "()Ljava/lang/ClassLoader;");
|
let target = jni.get_method_id(launcher_class, "getTargetClassLoader", "()Ljava/lang/ClassLoader;");
|
||||||
@@ -140,13 +152,98 @@ unsafe fn add_to_knot(jni: &Jni, jar: &std::path::Path) -> Result<JObject, Strin
|
|||||||
Ok(jni.new_global_ref(loader))
|
Ok(jni.new_global_ref(loader))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Class.forName(name, true, loader)
|
const FABRIC_BASE: &str = "net.fabricmc.loader.impl.launch.FabricLauncherBase";
|
||||||
unsafe fn class_for_name(jni: &Jni, name: &str, loader: JObject) -> Result<JClass, String> {
|
|
||||||
|
/// Finds the class loader that can see fabric-loader. The system class loader is tried first (official launcher, most
|
||||||
|
/// others); launchers that run the game from their own URLClassLoader (Prism/MultiMC, many modpack launchers) keep
|
||||||
|
/// fabric-loader out of it, so every thread's context class loader is probed too. Returns the FabricLauncherBase class
|
||||||
|
/// and the loader it was found through (null = FindClass / system loader).
|
||||||
|
unsafe fn locate_fabric(jni: &Jni) -> Result<(JClass, JObject), String> {
|
||||||
|
let base = jni.find_class(&FABRIC_BASE.replace('.', "/"));
|
||||||
|
if jni.take_exception().is_none() && !base.is_null() {
|
||||||
|
return Ok((base, std::ptr::null_mut()));
|
||||||
|
}
|
||||||
|
log!("FabricLauncherBase is not visible to the system class loader, probing thread context class loaders");
|
||||||
|
let loaders = thread_loaders(jni)?;
|
||||||
|
for &loader in &loaders {
|
||||||
|
if let Ok(class) = class_for_name(jni, FABRIC_BASE, loader, false) {
|
||||||
|
log!("fabric-loader found through {}", describe(jni, loader));
|
||||||
|
return Ok((class, loader));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let seen: Vec<String> = loaders.iter().map(|&l| describe(jni, l)).collect();
|
||||||
|
Err(format!(
|
||||||
|
"net.fabricmc.loader is not visible to any class loader ({} probed: {}); is this a Fabric instance?",
|
||||||
|
seen.len(),
|
||||||
|
seen.join(" | ")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Distinct context class loaders of all live threads (the game threads inherit the launcher's loader).
|
||||||
|
unsafe fn thread_loaders(jni: &Jni) -> Result<Vec<JObject>, String> {
|
||||||
|
let thread_class = jni.find_class("java/lang/Thread");
|
||||||
|
let get_all = jni.get_static_method_id(thread_class, "getAllStackTraces", "()Ljava/util/Map;");
|
||||||
|
let get_context = jni.get_method_id(thread_class, "getContextClassLoader", "()Ljava/lang/ClassLoader;");
|
||||||
|
let map_class = jni.find_class("java/util/Map");
|
||||||
|
let key_set = jni.get_method_id(map_class, "keySet", "()Ljava/util/Set;");
|
||||||
|
let set_class = jni.find_class("java/util/Set");
|
||||||
|
let to_array = jni.get_method_id(set_class, "toArray", "()[Ljava/lang/Object;");
|
||||||
|
jni.check("Thread/Map/Set lookup")?;
|
||||||
|
let map = jni.call_static_object(thread_class, get_all, &[]);
|
||||||
|
jni.check("Thread.getAllStackTraces")?;
|
||||||
|
let set = jni.call_object(jni_non_null(map)?, key_set, &[]);
|
||||||
|
let threads = jni.call_object(jni_non_null(set)?, to_array, &[]);
|
||||||
|
jni.check("thread list")?;
|
||||||
|
let mut loaders: Vec<JObject> = Vec::new();
|
||||||
|
for i in 0..jni.get_array_length(jni_non_null(threads)?) {
|
||||||
|
let thread = jni.get_object_array_element(threads, i);
|
||||||
|
if thread.is_null() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let loader = jni.call_object(thread, get_context, &[]);
|
||||||
|
if jni.take_exception().is_some() || loader.is_null() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !loaders.iter().any(|&known| jni.is_same_object(known, loader)) {
|
||||||
|
loaders.push(loader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(loaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn jni_non_null(o: JObject) -> Result<JObject, String> {
|
||||||
|
if o.is_null() { Err("unexpected null from the JVM".into()) } else { Ok(o) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Object.toString() of a class loader for the log.
|
||||||
|
unsafe fn describe(jni: &Jni, o: JObject) -> String {
|
||||||
|
let class = jni.get_object_class(o);
|
||||||
|
let to_string = jni.get_method_id(class, "toString", "()Ljava/lang/String;");
|
||||||
|
if to_string.is_null() {
|
||||||
|
jni.exception_clear();
|
||||||
|
return "<loader>".into();
|
||||||
|
}
|
||||||
|
let s = jni.call_object(o, to_string, &[]);
|
||||||
|
if jni.take_exception().is_some() || s.is_null() { "<loader>".into() } else { jni.get_string_utf(s) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads a fabric-loader class through the loader found by `locate_fabric` (null = system class loader).
|
||||||
|
unsafe fn load_fabric_class(jni: &Jni, name: &str, loader: JObject) -> Result<JClass, String> {
|
||||||
|
if loader.is_null() {
|
||||||
|
let class = jni.find_class(&name.replace('.', "/"));
|
||||||
|
jni.check(name)?;
|
||||||
|
return Ok(class);
|
||||||
|
}
|
||||||
|
class_for_name(jni, name, loader, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Class.forName(name, initialize, loader)
|
||||||
|
unsafe fn class_for_name(jni: &Jni, name: &str, loader: JObject, initialize: bool) -> Result<JClass, String> {
|
||||||
let class_class = jni.find_class("java/lang/Class");
|
let class_class = jni.find_class("java/lang/Class");
|
||||||
let for_name = jni.get_static_method_id(class_class, "forName", "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;");
|
let for_name = jni.get_static_method_id(class_class, "forName", "(Ljava/lang/String;ZLjava/lang/ClassLoader;)Ljava/lang/Class;");
|
||||||
jni.check("Class.forName")?;
|
jni.check("Class.forName")?;
|
||||||
let name_string = jni.new_string(name);
|
let name_string = jni.new_string(name);
|
||||||
let class = jni.call_static_object(class_class, for_name, &[obj(name_string), boolean(true), obj(loader)]);
|
let class = jni.call_static_object(class_class, for_name, &[obj(name_string), boolean(initialize), obj(loader)]);
|
||||||
jni.check(&format!("load {name}"))?;
|
jni.check(&format!("load {name}"))?;
|
||||||
if class.is_null() {
|
if class.is_null() {
|
||||||
return Err(format!("{name} not found"));
|
return Err(format!("{name} not found"));
|
||||||
@@ -155,7 +252,7 @@ unsafe fn class_for_name(jni: &Jni, name: &str, loader: JObject) -> Result<JClas
|
|||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn register_natives(jni: &Jni, loader: JObject) -> Result<(), String> {
|
unsafe fn register_natives(jni: &Jni, loader: JObject) -> Result<(), String> {
|
||||||
let class = class_for_name(jni, NATIVE_BRIDGE, loader)?;
|
let class = class_for_name(jni, NATIVE_BRIDGE, loader, true)?;
|
||||||
let methods = [
|
let methods = [
|
||||||
NativeMethod {
|
NativeMethod {
|
||||||
name: c"classBytes".as_ptr(),
|
name: c"classBytes".as_ptr(),
|
||||||
@@ -167,6 +264,16 @@ unsafe fn register_natives(jni: &Jni, loader: JObject) -> Result<(), String> {
|
|||||||
signature: c"(Ljava/lang/Class;[B)I".as_ptr(),
|
signature: c"(Ljava/lang/Class;[B)I".as_ptr(),
|
||||||
fn_ptr: native_redefine as *const () as usize as *mut c_void,
|
fn_ptr: native_redefine as *const () as usize as *mut c_void,
|
||||||
},
|
},
|
||||||
|
NativeMethod {
|
||||||
|
name: c"classBytesBatch".as_ptr(),
|
||||||
|
signature: c"([Ljava/lang/Class;)[[B".as_ptr(),
|
||||||
|
fn_ptr: native_class_bytes_batch as *const () as usize as *mut c_void,
|
||||||
|
},
|
||||||
|
NativeMethod {
|
||||||
|
name: c"redefineBatch".as_ptr(),
|
||||||
|
signature: c"([Ljava/lang/Class;[[B)I".as_ptr(),
|
||||||
|
fn_ptr: native_redefine_batch as *const () as usize as *mut c_void,
|
||||||
|
},
|
||||||
NativeMethod {
|
NativeMethod {
|
||||||
name: c"release".as_ptr(),
|
name: c"release".as_ptr(),
|
||||||
signature: c"()V".as_ptr(),
|
signature: c"()V".as_ptr(),
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
"""Packs one platform's release zip, or writes SHA256SUMS.txt for dist/release. Called by release.bat.
|
||||||
|
|
||||||
|
package_release.py zip <out_dir> <version> <platform> <injector> <native_lib> <payload_jar>
|
||||||
|
package_release.py checksums <out_dir>
|
||||||
|
|
||||||
|
Zips are written with unix permissions so PrestigeInjector stays executable after unzip on Linux/macOS.
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
README = {
|
||||||
|
"windows": """Prestige {version} - Windows (x86_64)
|
||||||
|
|
||||||
|
Contents
|
||||||
|
PrestigeInjector.exe injector
|
||||||
|
prestige_native.dll loader library (keep it next to the injector)
|
||||||
|
{jar} the same client as a Fabric mod (optional, see below)
|
||||||
|
|
||||||
|
Inject
|
||||||
|
1. Start Minecraft 1.21.11 with Fabric Loader 0.19+ and Fabric API.
|
||||||
|
2. Run PrestigeInjector.exe and pick the game from the list.
|
||||||
|
|
||||||
|
If SmartScreen blocks the exe: More info -> Run anyway.
|
||||||
|
""",
|
||||||
|
"linux": """Prestige {version} - Linux (x86_64)
|
||||||
|
|
||||||
|
Works on any glibc distro from 2012 on (glibc 2.17+): Ubuntu, Debian, Fedora, Arch, Mint, openSUSE, ...
|
||||||
|
Not for musl distros (Alpine) or ARM machines.
|
||||||
|
|
||||||
|
Contents
|
||||||
|
PrestigeInjector injector
|
||||||
|
libprestige_native.so loader library (keep it next to the injector)
|
||||||
|
{jar} the same client as a Fabric mod (optional, see below)
|
||||||
|
|
||||||
|
Inject
|
||||||
|
1. Start Minecraft 1.21.11 with Fabric Loader 0.19+ and Fabric API.
|
||||||
|
2. In a terminal in this folder: ./PrestigeInjector
|
||||||
|
Run it as the same user as the game. The game must not use -XX:+DisableAttachMechanism.
|
||||||
|
""",
|
||||||
|
"macos": """Prestige {version} - macOS (universal: Intel + Apple Silicon)
|
||||||
|
|
||||||
|
Contents
|
||||||
|
PrestigeInjector injector
|
||||||
|
libprestige_native.dylib loader library (keep it next to the injector)
|
||||||
|
{jar} the same client as a Fabric mod (optional, see below)
|
||||||
|
|
||||||
|
Inject
|
||||||
|
1. Start Minecraft 1.21.11 with Fabric Loader 0.19+ and Fabric API.
|
||||||
|
2. In Terminal, in this folder, clear the download quarantine once:
|
||||||
|
xattr -dr com.apple.quarantine .
|
||||||
|
3. Run: ./PrestigeInjector
|
||||||
|
Run it as the same user as the game. The game must not use -XX:+DisableAttachMechanism.
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
|
||||||
|
MOD_NOTE = """
|
||||||
|
Install as a mod instead
|
||||||
|
Put {jar} into the mods/ folder of a Fabric 1.21.11 instance; no injector needed.
|
||||||
|
Do not use both at once.
|
||||||
|
"""
|
||||||
|
|
||||||
|
TESTED = "Tested on Prism Launcher with Minecraft 1.21.11 + Fabric.\n"
|
||||||
|
|
||||||
|
FOOTER = """
|
||||||
|
Credits
|
||||||
|
Cracked by Skid Solutions
|
||||||
|
technix sourcing / Frida scripts
|
||||||
|
stutheguyy fixing
|
||||||
|
bynbang / nitrogenoliquido ZKM transformers
|
||||||
|
dario AI models
|
||||||
|
|
||||||
|
Discord: https://discord.gg/x2FJyGrDNg
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def add(zf, arcname, data, mode):
|
||||||
|
info = zipfile.ZipInfo(arcname, time.localtime()[:6])
|
||||||
|
info.create_system = 3 # unix, so external_attr carries the permission bits
|
||||||
|
info.external_attr = (0o100000 | mode) << 16
|
||||||
|
info.compress_type = zipfile.ZIP_DEFLATED
|
||||||
|
zf.writestr(info, data)
|
||||||
|
|
||||||
|
|
||||||
|
def make_zip(out_dir, version, platform, injector, lib, jar):
|
||||||
|
family = platform.split("-")[0]
|
||||||
|
name = f"prestige-{version}-{platform}"
|
||||||
|
jar_name = os.path.basename(jar)
|
||||||
|
title, body = README[family].format(version=version, jar=jar_name).split("\n", 1)
|
||||||
|
readme = title + "\n" + TESTED + body + MOD_NOTE.format(jar=jar_name) + FOOTER
|
||||||
|
if family == "windows":
|
||||||
|
readme = readme.replace("\n", "\r\n")
|
||||||
|
out = os.path.join(out_dir, name + ".zip")
|
||||||
|
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
|
||||||
|
for path, mode in ((injector, 0o755), (lib, 0o644), (jar, 0o644)):
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
add(zf, f"{name}/{os.path.basename(path)}", f.read(), mode)
|
||||||
|
add(zf, f"{name}/README.txt", readme.encode(), 0o644)
|
||||||
|
print(f" wrote {out} ({os.path.getsize(out) // 1024} KiB)")
|
||||||
|
|
||||||
|
|
||||||
|
def checksums(out_dir):
|
||||||
|
lines = []
|
||||||
|
for entry in sorted(os.listdir(out_dir)):
|
||||||
|
path = os.path.join(out_dir, entry)
|
||||||
|
if not os.path.isfile(path) or entry == "SHA256SUMS.txt":
|
||||||
|
continue
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
lines.append(f"{hashlib.sha256(f.read()).hexdigest()} {entry}\n")
|
||||||
|
with open(os.path.join(out_dir, "SHA256SUMS.txt"), "w", newline="\n") as f:
|
||||||
|
f.writelines(lines)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) == 8 and sys.argv[1] == "zip":
|
||||||
|
make_zip(*sys.argv[2:])
|
||||||
|
elif len(sys.argv) == 3 and sys.argv[1] == "checksums":
|
||||||
|
checksums(sys.argv[2])
|
||||||
|
else:
|
||||||
|
sys.exit(__doc__)
|
||||||
@@ -5,6 +5,7 @@ import java.io.IOException;
|
|||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -47,11 +48,29 @@ public final class EnginePlugin implements IMixinConfigPlugin {
|
|||||||
private String namespace = "named";
|
private String namespace = "named";
|
||||||
private int expectedTargets;
|
private int expectedTargets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The namespace the running classes are named in, which selects the target list and the mapping used by the transformers.
|
||||||
|
* Fabric reports it directly. Lunar's Genesis loader reports another namespace, but its classes carry intermediary names
|
||||||
|
* (its own stack traces show net.minecraft.class_310.method_55608), so intermediary is used there. Without this, the mapper
|
||||||
|
* resolved some names through the official namespace and others through the named one, which broke the hooks.
|
||||||
|
*/
|
||||||
|
public static String runtimeNamespace() {
|
||||||
|
String forced = System.getProperty("prestige.engine.namespace"); // test override ("named" / "intermediary")
|
||||||
|
if (forced != null) return forced;
|
||||||
|
String reported;
|
||||||
|
try {
|
||||||
|
reported = FabricLoader.getInstance().getMappingResolver().getCurrentRuntimeNamespace();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
return "named";
|
||||||
|
}
|
||||||
|
if (reported.equals("named") || reported.equals("intermediary")) return reported;
|
||||||
|
System.out.println("[Engine] runtime reports namespace '" + reported + "'; the classes use intermediary names, so using intermediary");
|
||||||
|
return "intermediary";
|
||||||
|
}
|
||||||
|
|
||||||
@Override public void onLoad(String mixinPackage) {
|
@Override public void onLoad(String mixinPackage) {
|
||||||
try {
|
try {
|
||||||
String forced = System.getProperty("prestige.engine.namespace"); // test override ("named" / "intermediary")
|
namespace = runtimeNamespace();
|
||||||
if (forced != null) namespace = forced;
|
|
||||||
else try { namespace = FabricLoader.getInstance().getMappingResolver().getCurrentRuntimeNamespace(); } catch (Throwable t) { namespace = "named"; }
|
|
||||||
List<String> transformers = lines("prestige-engine/transformers.list");
|
List<String> transformers = lines("prestige-engine/transformers.list");
|
||||||
expectedTargets = lines(namespace.equals("named") ? "prestige-engine/targets.named.list" : "prestige-engine/targets.intermediary.list").size();
|
expectedTargets = lines(namespace.equals("named") ? "prestige-engine/targets.named.list" : "prestige-engine/targets.intermediary.list").size();
|
||||||
manager = createManager(namespace, transformers);
|
manager = createManager(namespace, transformers);
|
||||||
@@ -68,8 +87,10 @@ public final class EnginePlugin implements IMixinConfigPlugin {
|
|||||||
public static TransformerManager createManager(String namespace, List<String> transformers) throws IOException {
|
public static TransformerManager createManager(String namespace, List<String> transformers) throws IOException {
|
||||||
ClassLoader loader = EnginePlugin.class.getClassLoader();
|
ClassLoader loader = EnginePlugin.class.getClassLoader();
|
||||||
byte[] tiny = namespace.equals("named") ? null : resource("prestige-engine/mappings.tiny");
|
byte[] tiny = namespace.equals("named") ? null : resource("prestige-engine/mappings.tiny");
|
||||||
TransformerManager manager = new TransformerManager(new ResourceClassProvider(loader, new HashSet<>(transformers)), tiny == null ? null : new ByteArrayInputStream(tiny));
|
TransformerManager manager = new TransformerManager(new ResourceClassProvider(loader, new HashSet<>(transformers), tiny), tiny == null ? null : new ByteArrayInputStream(tiny));
|
||||||
manager.setFailStrategy(FailStrategy.CANCEL);
|
// A transformer whose target method does not exist in this runtime (Lunar's Genesis loader names some methods differently
|
||||||
|
// from the bundled mappings) is skipped on its own. CANCEL would drop every hook of the whole class and abort the injection.
|
||||||
|
manager.setFailStrategy(FailStrategy.CONTINUE);
|
||||||
for (String name : transformers) manager.addTransformer(name);
|
for (String name : transformers) manager.addTransformer(name);
|
||||||
return manager;
|
return manager;
|
||||||
}
|
}
|
||||||
@@ -78,14 +99,40 @@ public final class EnginePlugin implements IMixinConfigPlugin {
|
|||||||
private static final class ResourceClassProvider implements IClassProvider {
|
private static final class ResourceClassProvider implements IClassProvider {
|
||||||
private final ClassLoader loader;
|
private final ClassLoader loader;
|
||||||
private final Set<String> transformers;
|
private final Set<String> transformers;
|
||||||
ResourceClassProvider(ClassLoader loader, Set<String> transformers) { this.loader = loader; this.transformers = transformers; }
|
/** Named (Yarn) class name -> intermediary name, from the bundled mapping. Lunar's jar only has the intermediary files. */
|
||||||
|
private final Map<String, String> namedToIntermediary;
|
||||||
|
ResourceClassProvider(ClassLoader loader, Set<String> transformers, byte[] tiny) {
|
||||||
|
this.loader = loader;
|
||||||
|
this.transformers = transformers;
|
||||||
|
this.namedToIntermediary = classNames(tiny);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Map<String, String> classNames(byte[] tiny) {
|
||||||
|
Map<String, String> out = new HashMap<>();
|
||||||
|
if (tiny == null) return out;
|
||||||
|
for (String line : new String(tiny, StandardCharsets.UTF_8).split("\n")) {
|
||||||
|
String[] f = line.split("\t");
|
||||||
|
// header "tiny 2 0 intermediary named"; class lines "c <intermediary> <named>"
|
||||||
|
if (f.length >= 3 && f[0].equals("c")) out.put(f[2].trim(), f[1].trim());
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
@Override public byte[] getClass(String name) throws ClassNotFoundException {
|
@Override public byte[] getClass(String name) throws ClassNotFoundException {
|
||||||
String path = name.replace('.', '/') + ".class";
|
String slashed = name.replace('.', '/');
|
||||||
try (InputStream in = loader.getResourceAsStream(path)) {
|
InputStream in = loader.getResourceAsStream(slashed + ".class");
|
||||||
if (in == null) throw new ClassNotFoundException(name + " (missing resource " + path + ")");
|
if (in == null) {
|
||||||
byte[] bytes = in.readAllBytes();
|
// a named name the runtime does not have: load the intermediary class it maps to
|
||||||
return transformers.contains(name.replace('/', '.')) ? gate(bytes) : bytes;
|
String intermediary = namedToIntermediary.get(slashed);
|
||||||
|
if (intermediary != null) {
|
||||||
|
slashed = intermediary;
|
||||||
|
in = loader.getResourceAsStream(slashed + ".class");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (in == null) throw new ClassNotFoundException(name + " (missing resource " + slashed + ".class)");
|
||||||
|
try (InputStream stream = in) {
|
||||||
|
byte[] bytes = stream.readAllBytes();
|
||||||
|
return transformers.contains(slashed.replace('/', '.')) ? gate(bytes) : bytes;
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new ClassNotFoundException(name, e);
|
throw new ClassNotFoundException(name, e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,12 +32,22 @@ public final class Bootstrap {
|
|||||||
public static synchronized void start() {
|
public static synchronized void start() {
|
||||||
if (started) return;
|
if (started) return;
|
||||||
started = true;
|
started = true;
|
||||||
|
if (net.fabricmc.loader.api.FabricLoader.getInstance().isModLoaded("prestige")) {
|
||||||
|
// the jar is also installed in mods/: Mixin already applied the hooks and Fabric ran the entrypoints
|
||||||
|
log("Prestige is already loaded as a Fabric mod, skipping injection");
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
log("waiting for the Minecraft client");
|
log("waiting for the Minecraft client");
|
||||||
waitForClient();
|
waitForClient();
|
||||||
Report report = install(NativeBridge::classBytes, NativeBridge::redefine);
|
Report report = install(NativeBridge::classBytes, NativeBridge::redefine, nativeBatch());
|
||||||
log("hooks installed: " + report.redefined + " classes, " + report.moved + " handlers moved" + (report.failed.isEmpty() ? "" : ", FAILED " + report.failed));
|
log("hooks installed: " + report.redefined + " classes, " + report.moved + " handlers moved" + (report.failed.isEmpty() ? "" : ", FAILED " + report.failed));
|
||||||
if (!report.failed.isEmpty()) throw new IllegalStateException("could not hook " + report.failed);
|
// Some clients (Lunar) name a few classes and methods differently. Keep going with the hooks that did apply: a missing
|
||||||
|
// hook only disables the features behind it, while stopping here would leave the client without Prestige at all.
|
||||||
|
if (!report.failed.isEmpty()) {
|
||||||
|
if (report.redefined == 0) throw new IllegalStateException("could not hook " + report.failed);
|
||||||
|
log("continuing with " + report.redefined + " hooked classes; skipped " + report.failed);
|
||||||
|
}
|
||||||
log("initialising Prestige on the client thread");
|
log("initialising Prestige on the client thread");
|
||||||
MinecraftClient.getInstance().execute(Bootstrap::initClient);
|
MinecraftClient.getInstance().execute(Bootstrap::initClient);
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
@@ -66,7 +76,39 @@ public final class Bootstrap {
|
|||||||
* without the native library.
|
* without the native library.
|
||||||
*/
|
*/
|
||||||
public static Report install(Function<Class<?>, byte[]> classBytes, ToIntBiFunction<Class<?>, byte[]> redefine) throws Exception {
|
public static Report install(Function<Class<?>, byte[]> classBytes, ToIntBiFunction<Class<?>, byte[]> redefine) throws Exception {
|
||||||
String namespace = FabricLoader.getInstance().getMappingResolver().getCurrentRuntimeNamespace();
|
return install(classBytes, redefine, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Many classes per native call: one RetransformClasses / RedefineClasses (one safepoint each) instead of one per class. */
|
||||||
|
public interface Batch {
|
||||||
|
byte[][] classBytes(Class<?>[] types);
|
||||||
|
|
||||||
|
int redefine(Class<?>[] types, byte[][] bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The batch natives of {@link NativeBridge}, or null when the loaded prestige_native.dll predates them. */
|
||||||
|
private static Batch nativeBatch() {
|
||||||
|
try {
|
||||||
|
NativeBridge.classBytesBatch(new Class<?>[0]); // probes the registration; a missing native throws UnsatisfiedLinkError
|
||||||
|
} catch (UnsatisfiedLinkError e) {
|
||||||
|
return null;
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// anything else means the native exists
|
||||||
|
}
|
||||||
|
return new Batch() {
|
||||||
|
@Override public byte[][] classBytes(Class<?>[] types) { return NativeBridge.classBytesBatch(types); }
|
||||||
|
@Override public int redefine(Class<?>[] types, byte[][] bytes) { return NativeBridge.redefineBatch(types, bytes); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Same as {@link #install(Function, ToIntBiFunction)} with an optional batch path: all original bytes are captured with one
|
||||||
|
* call, the ClassTransform step reuses cached results where it can, and all hooked classes are redefined with one call. A
|
||||||
|
* failing batch step falls back to per-class calls, so one bad class only fails itself.
|
||||||
|
*/
|
||||||
|
public static Report install(Function<Class<?>, byte[]> classBytes, ToIntBiFunction<Class<?>, byte[]> redefine, Batch batch) throws Exception {
|
||||||
|
String namespace = EnginePlugin.runtimeNamespace();
|
||||||
|
log("runtime namespace " + namespace + " (the loader reports " + FabricLoader.getInstance().getMappingResolver().getCurrentRuntimeNamespace() + ")");
|
||||||
List<String> transformers = new ArrayList<>(EnginePlugin.lines("prestige-engine/transformers.list"));
|
List<String> transformers = new ArrayList<>(EnginePlugin.lines("prestige-engine/transformers.list"));
|
||||||
transformers.addAll(EnginePlugin.lines("prestige-engine/inject-transformers.list"));
|
transformers.addAll(EnginePlugin.lines("prestige-engine/inject-transformers.list"));
|
||||||
TransformerManager manager = EnginePlugin.createManager(namespace, transformers);
|
TransformerManager manager = EnginePlugin.createManager(namespace, transformers);
|
||||||
@@ -75,31 +117,106 @@ public final class Bootstrap {
|
|||||||
EngineState.installed = true;
|
EngineState.installed = true;
|
||||||
|
|
||||||
Report report = new Report();
|
Report report = new Report();
|
||||||
int total = manager.getTransformedClasses().size(), done = 0;
|
List<String> targets = new ArrayList<>(manager.getTransformedClasses());
|
||||||
|
int total = targets.size();
|
||||||
log("transforming " + total + " hook targets");
|
log("transforming " + total + " hook targets");
|
||||||
for (String target : manager.getTransformedClasses()) {
|
long start = System.nanoTime();
|
||||||
if (done > 0 && done % 10 == 0) log("transformed " + done + "/" + total);
|
|
||||||
done++;
|
// 1. resolve the target classes
|
||||||
|
Class<?>[] types = new Class<?>[total];
|
||||||
|
for (int i = 0; i < total; i++) {
|
||||||
try {
|
try {
|
||||||
Class<?> type = Class.forName(target, false, loader);
|
types[i] = Class.forName(targets.get(i), false, loader);
|
||||||
byte[] current = classBytes.apply(type);
|
|
||||||
if (current == null) throw new IllegalStateException("no class bytes");
|
|
||||||
InPlaceTransformer.Result result = transformer.transform(target, current);
|
|
||||||
for (byte[] helper : result.helpers().values()) Definer.define(helper);
|
|
||||||
int code = redefine.applyAsInt(type, result.target());
|
|
||||||
if (code != 0) throw new IllegalStateException("redefine returned JVMTI error " + code);
|
|
||||||
ORIGINALS.put(type, current);
|
|
||||||
report.redefined++;
|
|
||||||
report.moved += result.movedMethods();
|
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
report.failed.add(target);
|
fail(report, targets.get(i), t);
|
||||||
log("hook failed for " + target + ": " + t);
|
|
||||||
t.printStackTrace(System.out);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. the bytes each class currently has: one native call for all of them, per-class for any that did not come back
|
||||||
|
byte[][] originals = new byte[total][];
|
||||||
|
if (batch != null) {
|
||||||
|
Class<?>[] resolved = java.util.Arrays.stream(types).filter(java.util.Objects::nonNull).toArray(Class<?>[]::new);
|
||||||
|
byte[][] captured = resolved.length == 0 ? null : batch.classBytes(resolved);
|
||||||
|
if (captured != null && captured.length == resolved.length) {
|
||||||
|
int next = 0;
|
||||||
|
for (int i = 0; i < total; i++) if (types[i] != null) originals[i] = captured[next++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (int i = 0; i < total; i++) {
|
||||||
|
if (types[i] != null && originals[i] == null) originals[i] = classBytes.apply(types[i]);
|
||||||
|
}
|
||||||
|
long captured = System.nanoTime();
|
||||||
|
|
||||||
|
// 3. ClassTransform, one class at a time (its mapper is not thread-safe: running it on several threads corrupts its state).
|
||||||
|
// A result that is already in the disk cache skips the transformation, which is where nearly all of the time goes.
|
||||||
|
InPlaceTransformer.Result[] results = new InPlaceTransformer.Result[total];
|
||||||
|
TransformCache cache = TransformCache.open();
|
||||||
|
int cacheHits = 0;
|
||||||
|
for (int i = 0; i < total; i++) {
|
||||||
|
if (types[i] == null) continue;
|
||||||
|
String target = targets.get(i);
|
||||||
|
if (originals[i] == null) {
|
||||||
|
fail(report, target, new IllegalStateException("no class bytes"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
byte[] hit = cache == null ? null : cache.get(target, originals[i]);
|
||||||
|
if (hit != null) {
|
||||||
|
results[i] = new InPlaceTransformer.Result(hit, Map.of(), 0);
|
||||||
|
cacheHits++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
results[i] = transformer.transform(target, originals[i]);
|
||||||
|
if (cache != null && results[i].helpers().isEmpty()) cache.put(target, originals[i], results[i].target());
|
||||||
|
} catch (Throwable t) {
|
||||||
|
fail(report, target, t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long transformed = System.nanoTime();
|
||||||
|
|
||||||
|
// 4. helper classes first, then redefine everything in one call (all or nothing); on an error redefine one by one
|
||||||
|
List<Integer> ready = new ArrayList<>();
|
||||||
|
for (int i = 0; i < total; i++) {
|
||||||
|
if (results[i] == null) continue;
|
||||||
|
try {
|
||||||
|
for (byte[] helper : results[i].helpers().values()) Definer.define(helper);
|
||||||
|
ready.add(i);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
fail(report, targets.get(i), t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
boolean redefinedAll = false;
|
||||||
|
if (batch != null && !ready.isEmpty()) {
|
||||||
|
Class<?>[] batchTypes = ready.stream().map(i -> types[i]).toArray(Class<?>[]::new);
|
||||||
|
byte[][] batchBytes = ready.stream().map(i -> results[i].target()).toArray(byte[][]::new);
|
||||||
|
redefinedAll = batch.redefine(batchTypes, batchBytes) == 0;
|
||||||
|
}
|
||||||
|
for (int i : ready) {
|
||||||
|
try {
|
||||||
|
if (!redefinedAll) {
|
||||||
|
int code = redefine.applyAsInt(types[i], results[i].target());
|
||||||
|
if (code != 0) throw new IllegalStateException("redefine returned JVMTI error " + code);
|
||||||
|
}
|
||||||
|
ORIGINALS.put(types[i], originals[i]);
|
||||||
|
report.redefined++;
|
||||||
|
report.moved += results[i].movedMethods();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
fail(report, targets.get(i), t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
long end = System.nanoTime();
|
||||||
|
log(String.format("timing: bytes %d ms, transform %d ms (%d of %d from cache), redefine %d ms%s",
|
||||||
|
(captured - start) / 1_000_000, (transformed - captured) / 1_000_000, cacheHits, total, (end - transformed) / 1_000_000,
|
||||||
|
batch == null ? " [per-class natives]" : redefinedAll ? "" : " [batch redefine refused, per-class]"));
|
||||||
return report;
|
return report;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void fail(Report report, String target, Throwable t) {
|
||||||
|
report.failed.add(target);
|
||||||
|
log("hook failed for " + target + ": " + t);
|
||||||
|
t.printStackTrace(System.out);
|
||||||
|
}
|
||||||
|
|
||||||
/** True when this client was injected, i.e. hooks were applied through JVMTI and can be taken out again. */
|
/** True when this client was injected, i.e. hooks were applied through JVMTI and can be taken out again. */
|
||||||
public static boolean isInjected() {
|
public static boolean isInjected() {
|
||||||
return !ORIGINALS.isEmpty();
|
return !ORIGINALS.isEmpty();
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ public final class InPlaceTransformer {
|
|||||||
|
|
||||||
private final TransformerManager manager;
|
private final TransformerManager manager;
|
||||||
private final ClassLoader loader;
|
private final ClassLoader loader;
|
||||||
private int sequence;
|
private final java.util.concurrent.atomic.AtomicInteger sequence = new java.util.concurrent.atomic.AtomicInteger();
|
||||||
|
|
||||||
public InPlaceTransformer(TransformerManager manager, ClassLoader loader) {
|
public InPlaceTransformer(TransformerManager manager, ClassLoader loader) {
|
||||||
this.manager = manager;
|
this.manager = manager;
|
||||||
@@ -67,7 +67,7 @@ public final class InPlaceTransformer {
|
|||||||
if (after.interfaces.size() != before.interfaces.size())
|
if (after.interfaces.size() != before.interfaces.size())
|
||||||
throw new IllegalStateException(dottedName + ": transformer changed the interface list");
|
throw new IllegalStateException(dottedName + ": transformer changed the interface list");
|
||||||
|
|
||||||
String helperName = HOOK_PACKAGE + "H" + (sequence++) + "_" + after.name.substring(after.name.lastIndexOf('/') + 1).replace('$', '_');
|
String helperName = HOOK_PACKAGE + "H" + (sequence.getAndIncrement()) + "_" + after.name.substring(after.name.lastIndexOf('/') + 1).replace('$', '_');
|
||||||
ClassNode helper = new ClassNode();
|
ClassNode helper = new ClassNode();
|
||||||
helper.version = after.version;
|
helper.version = after.version;
|
||||||
helper.access = Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_SUPER;
|
helper.access = Opcodes.ACC_PUBLIC | Opcodes.ACC_FINAL | Opcodes.ACC_SUPER;
|
||||||
|
|||||||
@@ -7,6 +7,12 @@ public final class NativeBridge {
|
|||||||
/** Bytes of the loaded class as the JVM currently has it (including changes made by earlier transformers). */
|
/** Bytes of the loaded class as the JVM currently has it (including changes made by earlier transformers). */
|
||||||
public static native byte[] classBytes(Class<?> type);
|
public static native byte[] classBytes(Class<?> type);
|
||||||
|
|
||||||
|
/** classBytes for many classes with one RetransformClasses call; entries are null where nothing was captured, null on failure. */
|
||||||
|
public static native byte[][] classBytesBatch(Class<?>[] types);
|
||||||
|
|
||||||
|
/** RedefineClasses for many classes in one call, all or nothing; returns the JVMTI error code, 0 on success. */
|
||||||
|
public static native int redefineBatch(Class<?>[] types, byte[][] bytes);
|
||||||
|
|
||||||
/** Appends a line to prestige-native.log, which the injector tails to show progress. */
|
/** Appends a line to prestige-native.log, which the injector tails to show progress. */
|
||||||
public static native void log(String message);
|
public static native void log(String message);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package dev.zprestige.prestige.inject;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.nio.file.attribute.FileTime;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disk cache of finished ClassTransform results. The ClassTransform step is by far the slowest part of an injection, and its
|
||||||
|
* output only depends on the payload jar and on the class bytes it is given, so a repeat injection into the same game setup can
|
||||||
|
* skip it. Entries are keyed by SHA-256(payload jar, target name, original class bytes); a rebuilt payload or a changed modpack
|
||||||
|
* simply misses. Only results that need no helper classes are stored.
|
||||||
|
*/
|
||||||
|
final class TransformCache {
|
||||||
|
private static final long MAX_AGE_MILLIS = 14L * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
private final Path dir;
|
||||||
|
private final byte[] payloadId;
|
||||||
|
|
||||||
|
private TransformCache(Path dir, byte[] payloadId) {
|
||||||
|
this.dir = dir;
|
||||||
|
this.payloadId = payloadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The cache for the payload jar this class was loaded from, or null when it cannot be used. */
|
||||||
|
static TransformCache open() {
|
||||||
|
try {
|
||||||
|
Path jar = Path.of(Bootstrap.class.getProtectionDomain().getCodeSource().getLocation().toURI());
|
||||||
|
if (!Files.isRegularFile(jar)) return null;
|
||||||
|
Path dir = Path.of(System.getProperty("java.io.tmpdir"), "PrestigeLoader", "cache");
|
||||||
|
Files.createDirectories(dir);
|
||||||
|
prune(dir);
|
||||||
|
return new TransformCache(dir, MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(jar)));
|
||||||
|
} catch (Throwable t) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Cached transformed bytes of {@code target}, or null. */
|
||||||
|
byte[] get(String target, byte[] original) {
|
||||||
|
try {
|
||||||
|
Path file = file(target, original);
|
||||||
|
if (!Files.isRegularFile(file)) return null;
|
||||||
|
byte[] bytes = Files.readAllBytes(file);
|
||||||
|
if (bytes.length < 4 || (bytes[0] & 0xFF) != 0xCA || (bytes[1] & 0xFF) != 0xFE || (bytes[2] & 0xFF) != 0xBA || (bytes[3] & 0xFF) != 0xBE) return null;
|
||||||
|
Files.setLastModifiedTime(file, FileTime.from(Instant.now()));
|
||||||
|
return bytes;
|
||||||
|
} catch (Throwable t) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void put(String target, byte[] original, byte[] transformed) {
|
||||||
|
try {
|
||||||
|
Path file = file(target, original);
|
||||||
|
Path temp = file.resolveSibling(file.getFileName() + ".tmp");
|
||||||
|
Files.write(temp, transformed);
|
||||||
|
Files.move(temp, file, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// the cache is only an optimisation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path file(String target, byte[] original) throws Exception {
|
||||||
|
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||||
|
digest.update(payloadId);
|
||||||
|
digest.update(target.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||||
|
digest.update((byte) 0);
|
||||||
|
digest.update(original);
|
||||||
|
StringBuilder name = new StringBuilder();
|
||||||
|
for (byte b : digest.digest()) name.append(String.format("%02x", b));
|
||||||
|
return dir.resolve(name + ".class");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Drops entries that have not been used for two weeks (every rebuild of the payload starts a fresh set). */
|
||||||
|
private static void prune(Path dir) {
|
||||||
|
try (Stream<Path> files = Files.list(dir)) {
|
||||||
|
long limit = System.currentTimeMillis() - MAX_AGE_MILLIS;
|
||||||
|
files.forEach(file -> {
|
||||||
|
try {
|
||||||
|
if (Files.getLastModifiedTime(file).toMillis() < limit) Files.deleteIfExists(file);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// leave it
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
// leave it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,7 +53,7 @@ public class SelfDestruct extends Module {
|
|||||||
error.printStackTrace();
|
error.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Prestige.moduleManager.setModules(new ArrayList());
|
Prestige.moduleManager.setModules(new ArrayList<>());
|
||||||
|
|
||||||
if (Bootstrap.isInjected()) {
|
if (Bootstrap.isInjected()) {
|
||||||
// one more tick so no hook of the current tick is still being entered when the original classes come back
|
// one more tick so no hook of the current tick is still being entered when the original classes come back
|
||||||
|
|||||||
@@ -797,6 +797,7 @@ public final class Input {
|
|||||||
if (!active) return;
|
if (!active) return;
|
||||||
syncMouse();
|
syncMouse();
|
||||||
if (WidgetsPage.click(button)) return;
|
if (WidgetsPage.click(button)) return;
|
||||||
|
if (Ui.S.page == 10 && KeybindsPage.capturing && button >= 2 && button <= 4) { KeybindsPage.click(button); return; }
|
||||||
switch (button) {
|
switch (button) {
|
||||||
case 0: lDown(); break;
|
case 0: lDown(); break;
|
||||||
case 1: rDown(); break;
|
case 1: rDown(); break;
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ package dev.zprestige.prestige.ui;
|
|||||||
|
|
||||||
import static dev.zprestige.prestige.ui.Lib.*;
|
import static dev.zprestige.prestige.ui.Lib.*;
|
||||||
|
|
||||||
|
import dev.zprestige.prestige.ui.bridge.Bridge;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keybinds page (page 10). Port of clean.c ~1273-1490 (draw), 180145320.c (click / key capture / wheel) and the key chip
|
* Keybinds page (page 10). Port of clean.c ~1273-1490 (draw), 180145320.c (click / key capture / wheel) and the key chip
|
||||||
* sub_18011F140. Key codes are Windows virtual-key codes like in the original (+216 of the "Bind" setting); 0 = None.
|
* sub_18011F140. Rows are modules whose Bind is set, read from the live client; edits are written back to that Bind.
|
||||||
|
* The page works in Windows virtual-key codes like the original (+216 of the module's bind); 0 = None. The client stores GLFW
|
||||||
|
* key codes (-1 = None) and 9995..9999 for mouse buttons (9999 - button), so values are converted on the way in and out.
|
||||||
*/
|
*/
|
||||||
public final class KeybindsPage {
|
public final class KeybindsPage {
|
||||||
private KeybindsPage() {}
|
private KeybindsPage() {}
|
||||||
@@ -17,12 +20,49 @@ public final class KeybindsPage {
|
|||||||
public String name, desc;
|
public String name, desc;
|
||||||
public int vk; // Windows VK code (or 9995..9999 for mouse buttons); 0 = none (hidden from the list)
|
public int vk; // Windows VK code (or 9995..9999 for mouse buttons); 0 = none (hidden from the list)
|
||||||
public boolean toggle; // Hold / Toggle mode (not shown on this page, kept for the lead)
|
public boolean toggle; // Hold / Toggle mode (not shown on this page, kept for the lead)
|
||||||
public Bound(String name, String desc, int vk, boolean toggle) { this.name = name; this.desc = desc; this.vk = vk; this.toggle = toggle; }
|
public Bridge.LiveSetting live; // the module's live Bind, written on every edit
|
||||||
|
public Bound(String name, String desc, int vk, boolean toggle, Bridge.LiveSetting live) {
|
||||||
|
this.name = name; this.desc = desc; this.vk = vk; this.toggle = toggle; this.live = live;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The lead fills this from the real modules (only entries with vk != 0 are listed). */
|
/** The rows, rebuilt from the live modules every frame the page is shown (only modules with a key set). */
|
||||||
public static final List<Bound> BOUND = new ArrayList<>();
|
public static final List<Bound> BOUND = new ArrayList<>();
|
||||||
|
|
||||||
|
/** Rebuilds {@link #BOUND} from the bridge. The original walks its module list and lists each module whose key is not None. */
|
||||||
|
static void refresh() {
|
||||||
|
BOUND.clear();
|
||||||
|
for (Bridge.LiveModule m : Bridge.modules()) {
|
||||||
|
Bridge.LiveSetting bind = m.keybind();
|
||||||
|
if (bind == null) continue;
|
||||||
|
int vk = liveToVk(bind.value());
|
||||||
|
if (vk == 0) continue;
|
||||||
|
BOUND.add(new Bound(m.name, m.desc, vk, bind.toggleMode(), bind));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Client key value (GLFW code, -1 = None, 9995..9999 = mouse) -> VK code used by this page (0 = None). */
|
||||||
|
static int liveToVk(Object value) {
|
||||||
|
int g = value instanceof Number n ? n.intValue() : -1;
|
||||||
|
if (g < 0) return 0;
|
||||||
|
if (g >= 9995) return g;
|
||||||
|
return Input.toVk(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** VK code of this page -> client key value (-1 = None). */
|
||||||
|
static int vkToLive(int vk) {
|
||||||
|
if (vk == 0) return -1;
|
||||||
|
if (vk >= 9995) return vk;
|
||||||
|
for (int g = 32; g <= 348; g++) if (Input.toVk(g) == vk) return g;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets a row's key on the page and on the module's live Bind. */
|
||||||
|
private static void setKey(Bound b, int vk) {
|
||||||
|
b.vk = vk;
|
||||||
|
if (b.live != null) b.live.set(vkToLive(vk));
|
||||||
|
}
|
||||||
|
|
||||||
/** dword_1804CCE5C: hovered row (-1 none). dword_1804CCE90 / byte_18070893F: row being captured / capture active. */
|
/** dword_1804CCE5C: hovered row (-1 none). dword_1804CCE90 / byte_18070893F: row being captured / capture active. */
|
||||||
public static int hoverRow = -1, captureRow = -1;
|
public static int hoverRow = -1, captureRow = -1;
|
||||||
public static boolean capturing;
|
public static boolean capturing;
|
||||||
@@ -118,6 +158,7 @@ public final class KeybindsPage {
|
|||||||
public static void update(float dt) {
|
public static void update(float dt) {
|
||||||
hoverRow = -1;
|
hoverRow = -1;
|
||||||
if (Ui.S.page != 10) return;
|
if (Ui.S.page != 10) return;
|
||||||
|
refresh();
|
||||||
int n = visible().size();
|
int n = visible().size();
|
||||||
float v108 = v108(), w = width();
|
float v108 = v108(), w = width();
|
||||||
float y = 110f - scroll;
|
float y = 110f - scroll;
|
||||||
@@ -129,10 +170,16 @@ public final class KeybindsPage {
|
|||||||
|
|
||||||
private static float maxScroll(int n) { return Math.max(0f, (n * 70f - 10f) - 575f); }
|
private static float maxScroll(int n) { return Math.max(0f, (n * 70f - 10f) - 575f); }
|
||||||
|
|
||||||
/** Left click: Edit starts capture, Delete clears the key (sets it to None). */
|
/** Click: while capturing, any mouse button becomes the bind (9999 - button, as the client encodes it). Otherwise Edit
|
||||||
|
* starts capture and Delete clears the key (sets it to None). */
|
||||||
public static boolean click(int button) {
|
public static boolean click(int button) {
|
||||||
if (button != 0) return false;
|
|
||||||
List<Bound> list = visible();
|
List<Bound> list = visible();
|
||||||
|
if (capturing) {
|
||||||
|
if (captureRow >= 0 && captureRow < list.size() && button >= 0 && button <= 4) setKey(list.get(captureRow), 9999 - button);
|
||||||
|
captureRow = -1; capturing = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (button != 0) return false;
|
||||||
if (hoverRow < 0 || hoverRow >= list.size()) return false;
|
if (hoverRow < 0 || hoverRow >= list.size()) return false;
|
||||||
float v108 = v108(), w = width();
|
float v108 = v108(), w = width();
|
||||||
float rowY = hoverRow * 70f + (110f - scroll);
|
float rowY = hoverRow * 70f + (110f - scroll);
|
||||||
@@ -142,7 +189,7 @@ public final class KeybindsPage {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (Input.mx >= del && Input.mx <= del + 80f && Input.my >= rowY + 15f && Input.my <= rowY + 45f) {
|
if (Input.mx >= del && Input.mx <= del + 80f && Input.my >= rowY + 15f && Input.my <= rowY + 45f) {
|
||||||
list.get(hoverRow).vk = 0;
|
setKey(list.get(hoverRow), 0);
|
||||||
captureRow = -1; capturing = false;
|
captureRow = -1; capturing = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -162,8 +209,8 @@ public final class KeybindsPage {
|
|||||||
List<Bound> list = visible();
|
List<Bound> list = visible();
|
||||||
if (captureRow < list.size()) {
|
if (captureRow < list.size()) {
|
||||||
Bound b = list.get(captureRow);
|
Bound b = list.get(captureRow);
|
||||||
if (glfwKey == 256 || glfwKey == 261) b.vk = 0; // VK 27 / 46
|
if (glfwKey == 256 || glfwKey == 261) setKey(b, 0); // Esc / Delete clear the bind (VK 27 / 46)
|
||||||
else { int vk = glfwToVk(glfwKey); if (vk != 0) b.vk = vk; else return true; }
|
else { int vk = Input.toVk(glfwKey); if (vk != 0) setKey(b, vk); else return true; }
|
||||||
}
|
}
|
||||||
capturing = false; captureRow = -1;
|
capturing = false; captureRow = -1;
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package dev.zprestige.prestige.ui.mc;
|
|||||||
|
|
||||||
import javax.swing.JWindow;
|
import javax.swing.JWindow;
|
||||||
import javax.swing.Timer;
|
import javax.swing.Timer;
|
||||||
import javax.swing.JOptionPane;
|
|
||||||
import java.awt.Graphics;
|
import java.awt.Graphics;
|
||||||
import java.awt.Graphics2D;
|
import java.awt.Graphics2D;
|
||||||
import java.awt.RenderingHints;
|
import java.awt.RenderingHints;
|
||||||
@@ -24,13 +23,12 @@ import java.io.FileWriter;
|
|||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.concurrent.CountDownLatch;
|
import java.util.concurrent.CountDownLatch;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
public class Skid extends JWindow implements ActionListener, MouseListener, MouseMotionListener {
|
public class Skid extends JWindow implements ActionListener, MouseListener, MouseMotionListener {
|
||||||
private static final int WIDTH = 440;
|
private static final int WIDTH = 440;
|
||||||
private static final int HEIGHT = 270;
|
private static final int HEIGHT = 270;
|
||||||
private static final int DURATION = 4000;
|
private static final int DURATION = 4000;
|
||||||
private static final String DISCORD_URL = "https://discord.gg/5FB5hqJacr";
|
private static final String DISCORD_URL = "https://discord.gg/x2FJyGrDNg";
|
||||||
|
|
||||||
private final CountDownLatch latch;
|
private final CountDownLatch latch;
|
||||||
private final Timer timer;
|
private final Timer timer;
|
||||||
@@ -161,27 +159,15 @@ public class Skid extends JWindow implements ActionListener, MouseListener, Mous
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final CountDownLatch latch = new CountDownLatch(1);
|
// Never block the caller: run() is called on the game's render thread, and a modal Swing dialog there freezes
|
||||||
Skid ui = new Skid(latch);
|
// Minecraft (the dialog opens behind the game window). The splash runs on the AWT event thread on its own.
|
||||||
ui.startSplash();
|
|
||||||
|
|
||||||
try {
|
|
||||||
latch.await(DURATION + 3000, TimeUnit.MILLISECONDS);
|
|
||||||
} catch (InterruptedException ignored) {}
|
|
||||||
|
|
||||||
if ("unset".equals(pref)) {
|
if ("unset".equals(pref)) {
|
||||||
int res = JOptionPane.showConfirmDialog(
|
savePref("yes");
|
||||||
null,
|
|
||||||
"Do you want to see this splash screen again?",
|
|
||||||
"Skid Solutions",
|
|
||||||
JOptionPane.YES_NO_OPTION,
|
|
||||||
JOptionPane.QUESTION_MESSAGE
|
|
||||||
);
|
|
||||||
if (res == JOptionPane.YES_OPTION) {
|
|
||||||
savePref("yes");
|
|
||||||
} else {
|
|
||||||
savePref("no");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
java.awt.EventQueue.invokeLater(() -> {
|
||||||
|
try {
|
||||||
|
new Skid(new CountDownLatch(1)).startSplash();
|
||||||
|
} catch (Throwable ignored) {}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user