Skip to content

Latest commit

 

History

History
141 lines (111 loc) · 5.23 KB

File metadata and controls

141 lines (111 loc) · 5.23 KB

Instance lifecycle

This page documents what happens behind InstanceControl: the InstanceManager singleton, the launch-to-exit state machine, the console streaming task. For the public API (get_pid / close_instance / delete_instance / size_of_instance), see instance-control.md.

State machine

1. Launch
   ├─ JVM spawned (Command::spawn)
   ├─ PID captured (child.id())
   ├─ GameInstance registered in INSTANCE_MANAGER
   └─ LaunchEvent::Launched { pid } emitted

2. Running
   ├─ stdout / stderr streamed → LaunchEvent::ProcessOutput { pid, stream, line }
   └─ window-appearance watcher (events feature) emits a one-shot event
      once the OS detects a window for the PID

3. Exit (one of):
   a. Process terminates on its own
      ├─ child.wait().await resolves with exit code
      ├─ LaunchEvent::ProcessExited { pid, exit_code } emitted
      └─ INSTANCE_MANAGER.unregister_instance(pid)
   b. User calls InstanceControl::close_instance(pid)
      ├─ INSTANCE_MANAGER removes the entry up-front
      ├─ SIGTERM (Unix) / taskkill /F (Windows) sent
      └─ Console task observes the child exiting and also
         emits ProcessExited (idempotent unregister)

4. Optional cleanup
   └─ InstanceControl::delete_instance() — only legal once no PID is
      tracked (otherwise InstanceError::StillRunning)

Instance manager

Global singleton:

pub(crate) static INSTANCE_MANAGER: Lazy<InstanceManager> = Lazy::new(InstanceManager::new);

pub(crate) struct InstanceManager {
    instances: RwLock<HashMap<u32, GameInstance>>,
}

pub(crate) struct GameInstance {
    pub pid:           u32,
    pub instance_name: String,
    pub version:       String,    // "{mc}-{loader}"
    pub username:      String,
    pub game_dir:      PathBuf,
    pub started_at:    SystemTime,
}

Keyed by PID — that's how the console task and close_instance find each other. Lookups by instance name (get_pid, get_pids) iterate the map and filter by instance_name.

Operations

Operation Trigger
register_instance(GameInstance) The launch runner after spawn(). Returns InstanceError::DuplicatePid if the PID is already tracked (race or OS PID reuse).
unregister_instance(pid) Console task after child.wait() resolves, and close_instance up-front (idempotent).
get_pid(name) / get_pids(name) Public API used by InstanceControl.
is_alive(pid) Internal — tells callers whether a PID is still tracked.
close_instance(pid) Public API; removes the entry, then sends the platform kill signal.

A poisoned RwLock is recovered with PoisonError::into_inner — the manager never panics on lock contention.

Console streaming

When the JVM is spawned, the runner hands the Child to a dedicated tokio task — handle_console_streams(pid, name, child, bus). The task:

  1. Takes ownership of child.stdout and child.stderr.
  2. Spawns one inner task per stream that reads line-by-line and emits LaunchEvent::ProcessOutput { pid, stream, line } (with stream set to "stdout" or "stderr").
  3. awaits child.wait().
  4. Emits LaunchEvent::ProcessExited { pid, exit_code }.
  5. Calls INSTANCE_MANAGER.unregister_instance(pid).

Without the events feature, console lines are logged via the tracing macros instead of being emitted as events. The lifecycle itself is identical.

Window-appearance watcher (events feature)

Spawned in parallel with the console task. Polls the OS until a window is attached to the spawned PID, then emits a one-shot event so UIs can flip from "Launching…" to "Game running". Implementation in crates/launch/src/launch/window.rs.

Why SIGTERM on Unix

Sending SIGTERM rather than SIGKILL lets the JVM run its registered shutdown hooks before exit — which means Minecraft saves the world, flushes the chunk cache, and exits cleanly. taskkill /F on Windows is the closest equivalent that actually terminates the process tree reliably; the trade-off is that no shutdown hooks fire there, which matches the historical behaviour of every other launcher on the platform.

What delete_instance actually wipes

{game_dirs()}/                      ← removed recursively
├── libraries/
├── natives/
├── assets/
├── versions/
├── mods/, resourcepacks/, …
└── saves/, screenshots/, options.txt

java_dirs() is not touched (Java distributions are shared across instances). Cache directories under {cache_dir}/modpacks/ are preserved as well — they're keyed by URL SHA1 and benefit other instances.

Re-launching after close

get_pid() becomes None immediately when close_instance returns (the manager entry is removed before the kill signal is sent). The console task may still be wrapping up its emit of ProcessExited for a few hundred milliseconds — for accurate "is it really gone" checks in UIs, listen for ProcessExited rather than polling get_pid.

Related