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
+2234
-491
No files matched your search
@@ -1,10 +1,11 @@
|
||||
//! PrestigeInjector: lists the running Minecraft (javaw/java) processes and loads prestige_native.dll into the selected one
|
||||
//! with the classic remote LoadLibraryW thread. The DLL does everything else once it is inside the JVM; the injector tails
|
||||
//! the DLL's log so every stage (transforming, injecting, initialising) is visible.
|
||||
//! PrestigeInjector: lists the running Minecraft (javaw/java) processes and loads the native library
|
||||
//! (prestige_native.dll / libprestige_native.so / libprestige_native.dylib) into the selected one: a remote LoadLibraryW
|
||||
//! 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.exe <pid> [dll path] non-interactive
|
||||
mod win;
|
||||
//! PrestigeInjector interactive process picker (Up/Down, Enter, Esc)
|
||||
//! PrestigeInjector <pid> [lib path] non-interactive
|
||||
mod platform;
|
||||
|
||||
use std::fs;
|
||||
use std::io::{self, Read, Seek, SeekFrom, Write};
|
||||
@@ -12,9 +13,9 @@ use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const DLL_NAME: &str = "prestige_native.dll";
|
||||
const WATERMARK: &str = "Cracked by Skid Solutions";
|
||||
const INVITE_URL: &str = "https://discord.gg/M56rd2wwC5";
|
||||
/// The one credit line, shown once at the top of the screen.
|
||||
const CREDIT: &str = "Cracked by Skid Solutions - https://discord.gg/x2FJyGrDNg";
|
||||
const INVITE_URL: &str = "https://discord.gg/x2FJyGrDNg";
|
||||
/// How long to follow the DLL's progress after it has been loaded.
|
||||
const FOLLOW_TIMEOUT: Duration = Duration::from_secs(180);
|
||||
|
||||
@@ -28,16 +29,18 @@ const RED: &str = "\x1b[91m";
|
||||
fn default_dll() -> PathBuf {
|
||||
std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|exe| exe.parent().map(|dir| dir.join(DLL_NAME)))
|
||||
.unwrap_or_else(|| PathBuf::from(DLL_NAME))
|
||||
.and_then(|exe| exe.parent().map(|dir| dir.join(platform::LIB_NAME)))
|
||||
.unwrap_or_else(|| PathBuf::from(platform::LIB_NAME))
|
||||
}
|
||||
|
||||
fn banner() {
|
||||
println!("{CYAN} ___ ___ ___ ___ _____ ___ ___ ___ ");
|
||||
println!(" | _ \\ _ \\ __/ __|_ _|_ _/ __| __|");
|
||||
println!(" | _/ / _|\\__ \\ | | | | (_ | _| ");
|
||||
println!(" |_| |_|_\\___|___/ |_| |___\\___|___|{RESET} injector");
|
||||
println!("{YELLOW} {WATERMARK}{RESET}\n");
|
||||
fn banner() -> String {
|
||||
let mut out = String::new();
|
||||
out.push_str(&format!("{CYAN} ___ ___ ___ ___ _____ ___ ___ ___ \n"));
|
||||
out.push_str(" | _ \\ _ \\ __/ __|_ _|_ _/ __| __|\n");
|
||||
out.push_str(" | _/ / _|\\__ \\ | | | | (_ | _| \n");
|
||||
out.push_str(&format!(" |_| |_|_\\___|___/ |_| |___\\___|___|{RESET} injector\n"));
|
||||
out.push_str(&format!("{YELLOW} {CREDIT}{RESET}\n\n"));
|
||||
out
|
||||
}
|
||||
|
||||
fn step(label: &str, message: &str) {
|
||||
@@ -58,21 +61,26 @@ fn clock() -> String {
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
win::enable_ansi();
|
||||
let code = real_main();
|
||||
platform::restore_terminal();
|
||||
code
|
||||
}
|
||||
|
||||
fn real_main() -> ExitCode {
|
||||
platform::enable_ansi();
|
||||
let _ = clock();
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let interactive = args.is_empty();
|
||||
|
||||
let (pid, dll): (Option<u32>, PathBuf) = match args.as_slice() {
|
||||
[] => {
|
||||
banner();
|
||||
invite_prompt();
|
||||
open_invite_once();
|
||||
(pick_process(), default_dll())
|
||||
}
|
||||
[pid] => (parse_pid(pid), default_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);
|
||||
}
|
||||
};
|
||||
@@ -81,7 +89,7 @@ fn main() -> ExitCode {
|
||||
return if interactive { ExitCode::SUCCESS } else { ExitCode::from(2) };
|
||||
};
|
||||
if !interactive {
|
||||
banner();
|
||||
print!("{}", banner());
|
||||
}
|
||||
|
||||
let code = run(pid, &dll);
|
||||
@@ -109,16 +117,18 @@ fn run(pid: u32, dll: &Path) -> ExitCode {
|
||||
}
|
||||
};
|
||||
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_start = fs::metadata(&log_path).map(|m| m.len()).unwrap_or(0);
|
||||
|
||||
step("checking", "64-bit JVM, process access, DLL not already loaded");
|
||||
step("injecting", "remote thread LoadLibraryW");
|
||||
match win::inject(pid, &dll) {
|
||||
Ok(()) => ok("injected", "prestige_native.dll is loaded in the game process"),
|
||||
step("checking", "64-bit JVM, process access, library not already loaded");
|
||||
step("injecting", "loading the native library into the JVM");
|
||||
match platform::inject(pid, &dll) {
|
||||
Ok(()) => ok("injected", &format!("{} is loaded in the game process", platform::LIB_NAME)),
|
||||
Err(e) => {
|
||||
fail("failed", &e);
|
||||
return ExitCode::from(1);
|
||||
@@ -130,7 +140,6 @@ fn run(pid: u32, dll: &Path) -> ExitCode {
|
||||
Outcome::Done => {
|
||||
println!();
|
||||
ok("success", "successfully injected - Prestige is running in the game");
|
||||
println!("{YELLOW} {WATERMARK}{RESET} {DIM}{INVITE_URL}{RESET}");
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Outcome::Failed(line) => {
|
||||
@@ -173,7 +182,9 @@ fn follow_log(path: &Path, start: u64) -> Outcome {
|
||||
// lines look like "[unix seconds] message"
|
||||
let message = line.split_once("] ").map(|(_, m)| m).unwrap_or(line);
|
||||
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);
|
||||
return Outcome::Failed(message.to_string());
|
||||
}
|
||||
@@ -189,51 +200,26 @@ fn follow_log(path: &Path, start: u64) -> Outcome {
|
||||
}
|
||||
|
||||
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.
|
||||
fn invite_prompt() {
|
||||
/// Opens the Discord invite on the very first launch only (no prompt); afterwards only the credit line shows it.
|
||||
fn open_invite_once() {
|
||||
let Some(path) = config_path() else { return };
|
||||
let saved = fs::read_to_string(&path).ok();
|
||||
let show = match saved.as_deref() {
|
||||
None => true, // first launch
|
||||
Some(text) => !text.lines().any(|l| l.trim() == "show_invite=false"),
|
||||
};
|
||||
if !show {
|
||||
if path.exists() {
|
||||
return;
|
||||
}
|
||||
let first = saved.is_none();
|
||||
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] "
|
||||
});
|
||||
let _ = platform::open_url(INVITE_URL);
|
||||
if let Some(dir) = path.parent() {
|
||||
let _ = fs::create_dir_all(dir);
|
||||
}
|
||||
let _ = fs::write(&path, format!("show_invite={again}\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")
|
||||
let _ = fs::write(&path, "invite_opened=true\n");
|
||||
}
|
||||
|
||||
/// Interactive list of Minecraft JVM windows, refreshed every 750 ms. Returns the chosen pid.
|
||||
fn pick_process() -> Option<u32> {
|
||||
let mut selected = 0usize;
|
||||
let mut processes = win::java_windows();
|
||||
let mut processes = platform::java_processes();
|
||||
let mut last_refresh = Instant::now();
|
||||
loop {
|
||||
if selected >= processes.len() {
|
||||
@@ -242,34 +228,34 @@ fn pick_process() -> Option<u32> {
|
||||
draw(&processes, selected);
|
||||
let deadline = Instant::now() + Duration::from_millis(50);
|
||||
while Instant::now() < deadline {
|
||||
if let Some(key) = win::poll_key() {
|
||||
if let Some(key) = platform::poll_key() {
|
||||
match key {
|
||||
win::Key::Up => selected = selected.saturating_sub(1),
|
||||
win::Key::Down => selected = (selected + 1).min(processes.len().saturating_sub(1)),
|
||||
win::Key::Enter => {
|
||||
platform::Key::Up => selected = selected.saturating_sub(1),
|
||||
platform::Key::Down => selected = (selected + 1).min(processes.len().saturating_sub(1)),
|
||||
platform::Key::Enter => {
|
||||
if let Some(p) = processes.get(selected) {
|
||||
return Some(p.pid);
|
||||
}
|
||||
}
|
||||
win::Key::Escape => return None,
|
||||
platform::Key::Escape => return None,
|
||||
}
|
||||
draw(&processes, selected);
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(10));
|
||||
}
|
||||
if last_refresh.elapsed() >= Duration::from_millis(750) {
|
||||
processes = win::java_windows();
|
||||
processes = platform::java_processes();
|
||||
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");
|
||||
out.push_str(&format!("{CYAN}Prestige injector{RESET} - Minecraft 1.21.11 (Fabric, 64-bit) {YELLOW}{WATERMARK}{RESET}\n"));
|
||||
out.push_str("Up/Down select, Enter inject, Esc quit\n\n");
|
||||
out.push_str(&banner());
|
||||
out.push_str("Minecraft 1.21.11 (Fabric, 64-bit) - Up/Down select, Enter inject, Esc quit\n\n");
|
||||
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() {
|
||||
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)
|
||||
}
|
||||
+270
-278
@@ -1,278 +1,270 @@
|
||||
//! Win32 plumbing of the injector (raw bindings, no external crates).
|
||||
use std::ffi::c_void;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
|
||||
type Handle = *mut c_void;
|
||||
|
||||
const PROCESS_CREATE_THREAD: u32 = 0x0002;
|
||||
const PROCESS_VM_OPERATION: u32 = 0x0008;
|
||||
const PROCESS_VM_WRITE: u32 = 0x0020;
|
||||
const PROCESS_VM_READ: u32 = 0x0010;
|
||||
const PROCESS_QUERY_INFORMATION: u32 = 0x0400;
|
||||
const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000;
|
||||
const MEM_COMMIT: u32 = 0x1000;
|
||||
const MEM_RESERVE: u32 = 0x2000;
|
||||
const MEM_RELEASE: u32 = 0x8000;
|
||||
const PAGE_READWRITE: u32 = 0x04;
|
||||
const TH32CS_SNAPMODULE: u32 = 0x8;
|
||||
const TH32CS_SNAPMODULE32: u32 = 0x10;
|
||||
const WAIT_OBJECT_0: u32 = 0;
|
||||
|
||||
#[repr(C)]
|
||||
struct ModuleEntry32W {
|
||||
size: u32,
|
||||
module_id: u32,
|
||||
process_id: u32,
|
||||
glblcnt_usage: u32,
|
||||
proccnt_usage: u32,
|
||||
base_addr: *mut u8,
|
||||
base_size: u32,
|
||||
module: Handle,
|
||||
sz_module: [u16; 256],
|
||||
sz_exe_path: [u16; 260],
|
||||
}
|
||||
|
||||
extern "system" {
|
||||
fn OpenProcess(access: u32, inherit: i32, pid: u32) -> Handle;
|
||||
fn CloseHandle(h: Handle) -> i32;
|
||||
fn VirtualAllocEx(process: Handle, address: *mut c_void, size: usize, alloc_type: u32, protect: u32) -> *mut c_void;
|
||||
fn VirtualFreeEx(process: Handle, address: *mut c_void, size: usize, free_type: u32) -> i32;
|
||||
fn WriteProcessMemory(process: Handle, address: *mut c_void, buf: *const c_void, size: usize, written: *mut usize) -> i32;
|
||||
fn CreateRemoteThread(process: Handle, attrs: *mut c_void, stack: usize, start: *mut c_void, param: *mut c_void, flags: u32, id: *mut u32) -> Handle;
|
||||
fn WaitForSingleObject(h: Handle, ms: u32) -> u32;
|
||||
fn GetExitCodeThread(h: Handle, code: *mut u32) -> i32;
|
||||
fn GetModuleHandleW(name: *const u16) -> Handle;
|
||||
fn GetProcAddress(module: Handle, name: *const u8) -> *mut c_void;
|
||||
fn IsWow64Process(process: Handle, wow64: *mut i32) -> i32;
|
||||
fn QueryFullProcessImageNameW(process: Handle, flags: u32, buf: *mut u16, size: *mut u32) -> i32;
|
||||
fn GetLastError() -> u32;
|
||||
fn CreateToolhelp32Snapshot(flags: u32, pid: u32) -> Handle;
|
||||
fn Module32FirstW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32;
|
||||
fn Module32NextW(snapshot: Handle, entry: *mut ModuleEntry32W) -> i32;
|
||||
fn GetStdHandle(id: u32) -> Handle;
|
||||
fn GetConsoleMode(h: Handle, mode: *mut u32) -> i32;
|
||||
fn SetConsoleMode(h: Handle, mode: u32) -> i32;
|
||||
}
|
||||
|
||||
#[link(name = "user32")]
|
||||
extern "system" {
|
||||
fn EnumWindows(callback: unsafe extern "system" fn(Handle, isize) -> i32, param: isize) -> i32;
|
||||
fn IsWindowVisible(hwnd: Handle) -> i32;
|
||||
fn GetWindowTextW(hwnd: Handle, buf: *mut u16, max: i32) -> i32;
|
||||
fn GetWindowThreadProcessId(hwnd: Handle, pid: *mut u32) -> u32;
|
||||
}
|
||||
|
||||
#[link(name = "shell32")]
|
||||
extern "system" {
|
||||
fn ShellExecuteW(hwnd: Handle, verb: *const u16, file: *const u16, params: *const u16, dir: *const u16, show: i32) -> isize;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
fn _kbhit() -> i32;
|
||||
fn _getch() -> i32;
|
||||
}
|
||||
|
||||
pub struct JavaWindow {
|
||||
pub pid: u32,
|
||||
pub title: String,
|
||||
pub exe: String,
|
||||
}
|
||||
|
||||
pub enum Key {
|
||||
Up,
|
||||
Down,
|
||||
Enter,
|
||||
Escape,
|
||||
}
|
||||
|
||||
pub fn enable_ansi() {
|
||||
unsafe {
|
||||
let out = GetStdHandle(-11i32 as u32);
|
||||
let mut mode = 0;
|
||||
if GetConsoleMode(out, &mut mode) != 0 {
|
||||
SetConsoleMode(out, mode | 0x0004);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-blocking key read: arrows arrive as 0xE0/0x00 followed by 72 (up) / 80 (down).
|
||||
pub fn poll_key() -> Option<Key> {
|
||||
unsafe {
|
||||
if _kbhit() == 0 {
|
||||
return None;
|
||||
}
|
||||
match _getch() {
|
||||
13 => Some(Key::Enter),
|
||||
27 => Some(Key::Escape),
|
||||
0 | 0xE0 => match _getch() {
|
||||
72 => Some(Key::Up),
|
||||
80 => Some(Key::Down),
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wide(path: &Path) -> Vec<u16> {
|
||||
path.as_os_str().encode_wide().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
|
||||
fn image_name(pid: u32) -> Option<String> {
|
||||
unsafe {
|
||||
let process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
|
||||
if process.is_null() {
|
||||
return None;
|
||||
}
|
||||
let mut buf = [0u16; 1024];
|
||||
let mut len = buf.len() as u32;
|
||||
let ok = QueryFullProcessImageNameW(process, 0, buf.as_mut_ptr(), &mut len);
|
||||
CloseHandle(process);
|
||||
if ok == 0 {
|
||||
return None;
|
||||
}
|
||||
Some(String::from_utf16_lossy(&buf[..len as usize]))
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "system" fn collect(hwnd: Handle, param: isize) -> i32 {
|
||||
let out = &mut *(param as *mut Vec<(u32, String)>);
|
||||
if IsWindowVisible(hwnd) == 0 {
|
||||
return 1;
|
||||
}
|
||||
let mut buf = [0u16; 256];
|
||||
let len = GetWindowTextW(hwnd, buf.as_mut_ptr(), buf.len() as i32);
|
||||
if len <= 0 {
|
||||
return 1;
|
||||
}
|
||||
let mut pid = 0;
|
||||
GetWindowThreadProcessId(hwnd, &mut pid);
|
||||
out.push((pid, String::from_utf16_lossy(&buf[..len as usize])));
|
||||
1
|
||||
}
|
||||
|
||||
/// Visible top-level windows that belong to java.exe / javaw.exe, one entry per process.
|
||||
pub fn java_windows() -> Vec<JavaWindow> {
|
||||
let mut windows: Vec<(u32, String)> = Vec::new();
|
||||
unsafe {
|
||||
EnumWindows(collect, &mut windows as *mut _ as isize);
|
||||
}
|
||||
let mut out: Vec<JavaWindow> = Vec::new();
|
||||
for (pid, title) in windows {
|
||||
if out.iter().any(|w| w.pid == pid) {
|
||||
continue;
|
||||
}
|
||||
let Some(path) = image_name(pid) else { continue };
|
||||
let exe = path.rsplit(['\\', '/']).next().unwrap_or("").to_string();
|
||||
if exe.eq_ignore_ascii_case("javaw.exe") || exe.eq_ignore_ascii_case("java.exe") {
|
||||
out.push(JavaWindow { pid, title, exe });
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn last_error(what: &str) -> String {
|
||||
format!("{what} failed (error {})", unsafe { GetLastError() })
|
||||
}
|
||||
|
||||
/// True when the module file name is loaded in the process.
|
||||
fn module_loaded(pid: u32, file_name: &str) -> bool {
|
||||
unsafe {
|
||||
let snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
|
||||
if snapshot.is_null() || snapshot as isize == -1 {
|
||||
return false;
|
||||
}
|
||||
let mut entry: ModuleEntry32W = std::mem::zeroed();
|
||||
entry.size = std::mem::size_of::<ModuleEntry32W>() as u32;
|
||||
let mut found = false;
|
||||
let mut ok = Module32FirstW(snapshot, &mut entry);
|
||||
while ok != 0 {
|
||||
let len = entry.sz_module.iter().position(|&c| c == 0).unwrap_or(entry.sz_module.len());
|
||||
if String::from_utf16_lossy(&entry.sz_module[..len]).eq_ignore_ascii_case(file_name) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
ok = Module32NextW(snapshot, &mut entry);
|
||||
}
|
||||
CloseHandle(snapshot);
|
||||
found
|
||||
}
|
||||
}
|
||||
|
||||
/// LoadLibraryW(dll) in the target process through CreateRemoteThread.
|
||||
pub fn inject(pid: u32, dll: &Path) -> Result<(), String> {
|
||||
unsafe {
|
||||
let access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ;
|
||||
let process = OpenProcess(access, 0, pid);
|
||||
if process.is_null() {
|
||||
return Err(last_error("OpenProcess (run as the same user, or as administrator)"));
|
||||
}
|
||||
let result = inject_into(process, pid, dll);
|
||||
CloseHandle(process);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn inject_into(process: Handle, pid: u32, dll: &Path) -> Result<(), String> {
|
||||
let mut wow64 = 0;
|
||||
if IsWow64Process(process, &mut wow64) != 0 && wow64 != 0 {
|
||||
return Err("the target is a 32-bit JVM; only 64-bit JVMs are supported".into());
|
||||
}
|
||||
let file_name = dll.file_name().map(|n| n.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
if module_loaded(pid, &file_name) {
|
||||
return Err(format!("{file_name} is already loaded in process {pid}"));
|
||||
}
|
||||
|
||||
let path = wide(dll);
|
||||
let bytes = path.len() * 2;
|
||||
let remote = VirtualAllocEx(process, std::ptr::null_mut(), bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
if remote.is_null() {
|
||||
return Err(last_error("VirtualAllocEx"));
|
||||
}
|
||||
let cleanup = |err: String| {
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
Err(err)
|
||||
};
|
||||
if WriteProcessMemory(process, remote, path.as_ptr() as *const c_void, bytes, std::ptr::null_mut()) == 0 {
|
||||
return cleanup(last_error("WriteProcessMemory"));
|
||||
}
|
||||
|
||||
// kernel32 is mapped at the same base in every process of a boot session, so the local address is valid remotely.
|
||||
let kernel32 = GetModuleHandleW(wide(Path::new("kernel32.dll")).as_ptr());
|
||||
let load_library = GetProcAddress(kernel32, c"LoadLibraryW".as_ptr() as *const u8);
|
||||
if load_library.is_null() {
|
||||
return cleanup(last_error("resolve LoadLibraryW"));
|
||||
}
|
||||
let thread = CreateRemoteThread(process, std::ptr::null_mut(), 0, load_library, remote, 0, std::ptr::null_mut());
|
||||
if thread.is_null() {
|
||||
return cleanup(last_error("CreateRemoteThread"));
|
||||
}
|
||||
let wait = WaitForSingleObject(thread, 30_000);
|
||||
let mut exit = 0;
|
||||
GetExitCodeThread(thread, &mut exit);
|
||||
CloseHandle(thread);
|
||||
if wait != WAIT_OBJECT_0 {
|
||||
// The thread may still be running inside the target; leave the buffer alone.
|
||||
return Err("remote LoadLibraryW did not finish within 30 seconds".into());
|
||||
}
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
|
||||
// LoadLibraryW's return value is truncated to 32 bits by GetExitCodeThread, so confirm through the module list.
|
||||
for _ in 0..20 {
|
||||
if module_loaded(pid, &file_name) {
|
||||
return Ok(());
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
Err(format!("LoadLibraryW returned {exit:#x} but {file_name} is not mapped (missing dependency or blocked by security software?)"))
|
||||
}
|
||||
|
||||
/// Opens a URL in the default web browser.
|
||||
pub fn open_url(url: &str) -> bool {
|
||||
let verb: Vec<u16> = "open | ||||