JavaRuntime is a thin wrapper around tokio::process::Command. It
spawns the binary, returns the Child, and offers handle_io to
stream stdout / stderr until the process exits (or a one-shot
terminator fires).
pub struct JavaRuntime(pub PathBuf);
impl JavaRuntime {
pub fn new(path: PathBuf) -> Self;
pub async fn execute(
&self,
arguments: Vec<String>,
game_dir: &Path,
) -> JavaRuntimeResult<tokio::process::Child>;
pub async fn handle_io<D: Send + Sync>(
&self,
process: &mut tokio::process::Child,
on_stdout: fn(&D, &[u8]) -> JavaRuntimeResult<()>,
on_stderr: fn(&D, &[u8]) -> JavaRuntimeResult<()>,
terminator: tokio::sync::oneshot::Receiver<()>,
data: &D,
) -> JavaRuntimeResult<()>;
}game_dir becomes the process's working directory (this is what gets
passed to Minecraft via ${game_directory}).
handle_io takes function pointers (not closures) so they can
cross the Tokio select! cleanly. The data argument is an opaque
context bag — pass whatever the callbacks need to share (a logger
handle, an EventBus, …).
Windows-only detail: execute sets CREATE_NO_WINDOW so spawned
processes don't pop up a console window.
use lighty_java::runtime::JavaRuntime;
use tokio::sync::oneshot;
use std::path::Path;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = JavaRuntime::new("/usr/bin/java".into());
let mut child = rt.execute(vec!["-version".into()], Path::new(".")).await?;
let (_tx, rx) = oneshot::channel();
rt.handle_io::<()>(
&mut child,
|_, b| { print!("{}", String::from_utf8_lossy(b)); Ok(()) },
|_, b| { eprint!("{}", String::from_utf8_lossy(b)); Ok(()) },
rx,
&(),
).await?;
Ok(())
}java -version writes to stderr — both callbacks get hit.
use lighty_java::runtime::JavaRuntime;
use tokio::sync::oneshot;
use std::path::Path;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let rt = JavaRuntime::new("/path/to/java".into());
let args = vec![
"-Xmx4G".into(),
"-Xms1G".into(),
"-XX:+UseG1GC".into(),
"-jar".into(),
"minecraft.jar".into(),
];
let mut child = rt.execute(args, Path::new("/games/minecraft")).await?;
let (term_tx, term_rx) = oneshot::channel();
let io = rt.handle_io::<()>(
&mut child,
|_, b| { print!("{}", String::from_utf8_lossy(b)); Ok(()) },
|_, b| { eprint!("{}", String::from_utf8_lossy(b)); Ok(()) },
term_rx,
&(),
);
// Fire `term_tx` from elsewhere to bail out early
let _ = term_tx;
io.await?;
Ok(())
}handle_io returns once the process exits or the terminator fires.
The exit code is available via child.wait().await?:
# use lighty_java::runtime::JavaRuntime;
# use tokio::sync::oneshot;
# use std::path::Path;
# async fn run() -> anyhow::Result<()> {
let rt = JavaRuntime::new("/usr/bin/java".into());
let mut child = rt.execute(vec!["-version".into()], Path::new(".")).await?;
let (_tx, rx) = oneshot::channel();
rt.handle_io::<()>(&mut child,
|_, _| Ok(()), |_, _| Ok(()), rx, &()).await?;
let status = child.wait().await?;
println!("exit code: {:?}", status.code());
# Ok(()) }pub enum JavaRuntimeError {
NotFound { path: PathBuf },
NonZeroExit { code: i32 },
IoCaptureFailure, // stdout/stderr couldn't be captured
Spawn(std::io::Error),
SignalTerminated,
}The Windows forceful-termination code -1073740791 (0xC0000409) is
treated as a normal exit, not an error.
lighty-launch::launcher::Launcher calls execute with the full
launch argv (built from Arguments) and pipes handle_io into the
event bus — every line becomes a ConsoleOutputEvent and the final
exit triggers InstanceExited. The same oneshot terminator is
hooked to the cancel button in the host UI.
overview.md— crate scopeinstallation.md— get a binary path first../../launch/docs/launch.md— howLauncherbuilds the argv../../launch/docs/arguments.md— JVM arg construction