feat: complete Steam execution
This commit is contained in:
@@ -347,27 +347,91 @@ pub struct LaunchOutcome {
|
||||
pub success: bool,
|
||||
}
|
||||
|
||||
/// Executes a validated native launch plan and captures stdout/stderr in a private log.
|
||||
/// Result of submitting work to a provider-owned client.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub struct SubmissionOutcome {
|
||||
/// Process ID of the submitted client command.
|
||||
pub process_id: u32,
|
||||
}
|
||||
|
||||
/// Submits a validated launch plan without equating the client process with the game.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a structured error when the log cannot be secured or the command cannot start.
|
||||
pub fn submit(plan: &LaunchPlanV1, log_path: &Path) -> Result<SubmissionOutcome, ContractErrorV1> {
|
||||
if !plan.wrappers().is_empty() {
|
||||
return Err(ContractErrorV1::unsupported_capability(
|
||||
text("wrapper execution is not implemented"),
|
||||
name("wrappers"),
|
||||
));
|
||||
}
|
||||
let mut log = open_log(plan, log_path)?;
|
||||
let executable = executable_path(&plan.command().executable);
|
||||
let child = Command::new(executable)
|
||||
.args(plan.command().arguments.iter().map(CommandArgument::as_str))
|
||||
.env_clear()
|
||||
.envs(
|
||||
plan.environment()
|
||||
.iter()
|
||||
.map(|(key, value)| (key.as_str(), value.as_str())),
|
||||
)
|
||||
.stdin(Stdio::null())
|
||||
.current_dir(plan.working_directory().as_path())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.map_err(|error| launch_failed(format!("submit process: {error}")))?;
|
||||
let outcome = SubmissionOutcome {
|
||||
process_id: child.id(),
|
||||
};
|
||||
writeln!(log, "submitted {}", outcome.process_id)
|
||||
.map_err(|error| unavailable(format!("write launch log: {error}")))?;
|
||||
Ok(outcome)
|
||||
}
|
||||
|
||||
/// Executes a validated launch plan and captures stdout/stderr in a private log.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a structured error when the log cannot be secured, the command cannot start,
|
||||
/// or the child exits unsuccessfully.
|
||||
pub fn execute(plan: &LaunchPlanV1, log_path: &Path) -> Result<LaunchOutcome, ContractErrorV1> {
|
||||
let mut log = open_log(plan, log_path)?;
|
||||
let (status, stdout, stderr) = spawn(plan)?;
|
||||
write_redacted_output(&mut log, plan, "stdout", &stdout)?;
|
||||
write_redacted_output(&mut log, plan, "stderr", &stderr)?;
|
||||
writeln!(log, "exit {:?}", status.code())
|
||||
.map_err(|error| unavailable(format!("write native launch log: {error}")))?;
|
||||
let outcome = LaunchOutcome {
|
||||
exit_code: status.code(),
|
||||
success: status.success(),
|
||||
};
|
||||
if outcome.success {
|
||||
Ok(outcome)
|
||||
} else {
|
||||
Err(ContractErrorV1::new(
|
||||
ContractErrorCode::LaunchFailed,
|
||||
text("process exited unsuccessfully; see launch log"),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn open_log(plan: &LaunchPlanV1, log_path: &Path) -> Result<File, ContractErrorV1> {
|
||||
let parent = log_path
|
||||
.parent()
|
||||
.ok_or_else(|| invalid("native log path has no parent"))?;
|
||||
.ok_or_else(|| invalid("launch log path has no parent"))?;
|
||||
if !log_path.is_absolute()
|
||||
|| fs::symlink_metadata(log_path).is_ok_and(|m| m.file_type().is_symlink())
|
||||
{
|
||||
return Err(invalid("native log path is unsafe"));
|
||||
return Err(invalid("launch log path is unsafe"));
|
||||
}
|
||||
reject_symlink_components(parent)?;
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|error| unavailable(format!("create native log directory: {error}")))?;
|
||||
.map_err(|error| unavailable(format!("create launch log directory: {error}")))?;
|
||||
reject_symlink_components(parent)?;
|
||||
fs::set_permissions(parent, fs::Permissions::from_mode(DIRECTORY_MODE))
|
||||
.map_err(|error| unavailable(format!("secure native log directory: {error}")))?;
|
||||
.map_err(|error| unavailable(format!("secure launch log directory: {error}")))?;
|
||||
let mut log = OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
@@ -388,23 +452,7 @@ pub fn execute(plan: &LaunchPlanV1, log_path: &Path) -> Result<LaunchOutcome, Co
|
||||
}
|
||||
writeln!(log, "launch {}", plan.game_id())
|
||||
.map_err(|error| unavailable(format!("write native launch log: {error}")))?;
|
||||
let (status, stdout, stderr) = spawn(plan)?;
|
||||
write_redacted_output(&mut log, plan, "stdout", &stdout)?;
|
||||
write_redacted_output(&mut log, plan, "stderr", &stderr)?;
|
||||
writeln!(log, "exit {:?}", status.code())
|
||||
.map_err(|error| unavailable(format!("write native launch log: {error}")))?;
|
||||
let outcome = LaunchOutcome {
|
||||
exit_code: status.code(),
|
||||
success: status.success(),
|
||||
};
|
||||
if outcome.success {
|
||||
Ok(outcome)
|
||||
} else {
|
||||
Err(ContractErrorV1::new(
|
||||
ContractErrorCode::LaunchFailed,
|
||||
text("native process exited unsuccessfully; see launch log"),
|
||||
))
|
||||
}
|
||||
Ok(log)
|
||||
}
|
||||
|
||||
fn spawn(
|
||||
@@ -412,7 +460,7 @@ fn spawn(
|
||||
) -> Result<(ExitStatus, CapturedOutput, CapturedOutput), ContractErrorV1> {
|
||||
if !plan.wrappers().is_empty() {
|
||||
return Err(ContractErrorV1::unsupported_capability(
|
||||
text("native wrapper execution is not implemented"),
|
||||
text("wrapper execution is not implemented"),
|
||||
name("wrappers"),
|
||||
));
|
||||
}
|
||||
@@ -432,28 +480,28 @@ fn spawn(
|
||||
.stderr(Stdio::piped());
|
||||
let mut child = command
|
||||
.spawn()
|
||||
.map_err(|error| launch_failed(format!("start native process: {error}")))?;
|
||||
.map_err(|error| launch_failed(format!("start process: {error}")))?;
|
||||
let stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| launch_failed("capture native stdout"))?;
|
||||
.ok_or_else(|| launch_failed("capture process stdout"))?;
|
||||
let stderr = child
|
||||
.stderr
|
||||
.take()
|
||||
.ok_or_else(|| launch_failed("capture native stderr"))?;
|
||||
.ok_or_else(|| launch_failed("capture process stderr"))?;
|
||||
let stdout_reader = thread::spawn(move || capture_bounded(stdout));
|
||||
let stderr_reader = thread::spawn(move || capture_bounded(stderr));
|
||||
let status = child
|
||||
.wait()
|
||||
.map_err(|error| launch_failed(format!("wait for native process: {error}")))?;
|
||||
.map_err(|error| launch_failed(format!("wait for process: {error}")))?;
|
||||
let stdout = stdout_reader
|
||||
.join()
|
||||
.map_err(|_| launch_failed("native stdout capture failed"))?
|
||||
.map_err(|error| launch_failed(format!("read native stdout: {error}")))?;
|
||||
.map_err(|_| launch_failed("stdout capture failed"))?
|
||||
.map_err(|error| launch_failed(format!("read stdout: {error}")))?;
|
||||
let stderr = stderr_reader
|
||||
.join()
|
||||
.map_err(|_| launch_failed("native stderr capture failed"))?
|
||||
.map_err(|error| launch_failed(format!("read native stderr: {error}")))?;
|
||||
.map_err(|_| launch_failed("stderr capture failed"))?
|
||||
.map_err(|error| launch_failed(format!("read stderr: {error}")))?;
|
||||
Ok((status, stdout, stderr))
|
||||
}
|
||||
|
||||
@@ -527,11 +575,11 @@ fn reject_symlink_components(path: &Path) -> Result<(), ContractErrorV1> {
|
||||
current.push(component);
|
||||
match fs::symlink_metadata(¤t) {
|
||||
Ok(metadata) if metadata.file_type().is_symlink() => {
|
||||
return Err(invalid("native log path contains a symlink"));
|
||||
return Err(invalid("launch log path contains a symlink"));
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(error) => return Err(unavailable(format!("inspect native log path: {error}"))),
|
||||
Err(error) => return Err(unavailable(format!("inspect launch log path: {error}"))),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -815,6 +863,15 @@ fn config_home(xdg: Option<&OsStr>, home: Option<&OsStr>) -> Result<PathBuf, Con
|
||||
///
|
||||
/// Returns a structured error when no absolute XDG state or home path is available.
|
||||
pub fn current_log_path() -> Result<PathBuf, ContractErrorV1> {
|
||||
current_log_path_for(&name("native"))
|
||||
}
|
||||
|
||||
/// Resolves a provider-named private per-user launch log.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns a structured error when no absolute XDG state or home path is available.
|
||||
pub fn current_log_path_for(provider: &ContractName) -> Result<PathBuf, ContractErrorV1> {
|
||||
let xdg = env::var_os("XDG_STATE_HOME");
|
||||
let home = env::var_os("HOME");
|
||||
xdg.as_deref()
|
||||
@@ -827,7 +884,7 @@ pub fn current_log_path() -> Result<PathBuf, ContractErrorV1> {
|
||||
.filter(|path| path.is_absolute())
|
||||
.map(|path| path.join(".local/state"))
|
||||
})
|
||||
.map(|path| path.join("kiln/logs/native.log"))
|
||||
.map(|path| path.join(format!("kiln/logs/{}.log", provider.as_str())))
|
||||
.ok_or_else(|| unavailable("no absolute XDG state or home directory is available"))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user