fix: post-hook now runs when game binary does not exist

validate_launch() rejects missing executables in build_plan(), causing an
early return via `?` before the post-hook block was ever reached. Fix by
separating the dry-run path (still fails fast with `?`) from the real-launch
path, where build_plan() result is captured without `?` and chained into
execute_wait() via and_then(). The post-hook fires for both plan-validation
failures and execute_wait() spawn failures.

Also skips the pre-launch hook when the plan is invalid (no binary to set up
for), and skips state recording in the same case.

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-06-14 09:51:57 -04:00
parent f984acf0e3
commit e7c3b2eee7
+23 -16
View File
@@ -1421,16 +1421,10 @@ fn launch_command(
let resolved = profile::resolve(config, &executable)?;
let verbose = resolved.settings.verbose;
let settings = resolved.settings.clone();
let plan = launch::build_plan(command, settings.clone())?;
let log_path = settings.log_file.then(|| {
settings
.log_path
.as_deref()
.map(PathBuf::from)
.unwrap_or_else(|| crate::log::default_log_path(&paths.state_dir))
});
// Dry-run: build the plan (fail-fast) and render it without launching.
if dry_run {
let plan = launch::build_plan(command, settings.clone())?;
println!(
"{}",
launch::render_plan(&plan, &resolved.profile_name, verbose)
@@ -1459,6 +1453,16 @@ fn launch_command(
return Ok(());
}
// Real launch: capture plan errors so the post-hook can always run.
let plan_result = launch::build_plan(command, settings.clone());
let log_path = settings.log_file.then(|| {
settings
.log_path
.as_deref()
.map(PathBuf::from)
.unwrap_or_else(|| crate::log::default_log_path(&paths.state_dir))
});
// Log the launch header.
if let Some(log_path) = &log_path {
crate::log::append(
@@ -1467,13 +1471,15 @@ fn launch_command(
"--- launch ---".to_string(),
format!("executable: {}", executable.basename),
format!("profile: {}", resolved.profile_name),
format!("command: {}", format_command(&plan.command)),
format!("command: {}", format_command(command)),
],
);
}
// Run pre-launch hook.
if let Some(pre_cmd) = settings.pre_launch.as_deref() {
// Run pre-launch hook (only when the plan is valid; skip if binary not found).
if plan_result.is_ok()
&& let Some(pre_cmd) = settings.pre_launch.as_deref()
{
let hook_result = run_hook(pre_cmd);
let log_label = match &hook_result {
Ok(status) => exit_status_label(*status),
@@ -1507,16 +1513,16 @@ fn launch_command(
}
}
// Record the launch in state (Steam context only).
if env::is_steam_context() {
// Record the launch in state (Steam context only, when plan is valid).
if plan_result.is_ok() && env::is_steam_context() {
detect::record_launch(state, &executable, &resolved.profile_name);
config::save_state(paths, state)?;
}
if let Some(post_cmd) = settings.post_launch.clone() {
// Spawn the game and wait. Capture both success and failure so the
// post-hook can always run once we've passed the pre-hook check.
let game_result = launch::execute_wait(plan);
// Chain plan building with game execution so post-hook runs even when
// the binary doesn't exist or otherwise fails to spawn.
let game_result = plan_result.and_then(launch::execute_wait);
let (elapsed_secs, game_exit_label) = match &game_result {
Ok((exit_status, elapsed)) => {
@@ -1579,6 +1585,7 @@ fn launch_command(
}
}
} else {
let plan = plan_result?;
launch::execute(plan)
}
}