native/src/bootstrap.rs
Rama main · réplica de lectura del repositorio original.
//! Bootstrap primitives shared by the console manager's setup/update flow.
//!
//! The release architecture uses a console `origin` manager and a separate
//! silent `origin-runtime`. This module avoids shell scripts at runtime: file
//! replacement and process launch use Rust/native process APIs directly.
use std::fs;
use std::io::{self, Read, Write};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicU64, Ordering};
const COPY_BUFFER: usize = 1024 * 1024;
static SWAP_SEQUENCE: AtomicU64 = AtomicU64::new(1);
#[derive(Debug)]
pub struct TransactionalSwap {
destination: PathBuf,
backup: Option<PathBuf>,
committed: bool,
}
impl TransactionalSwap {
pub fn rollback(&mut self) -> Result<(), String> {
if !self.committed {
return Ok(());
}
if self.destination.exists() {
fs::remove_file(&self.destination).map_err(|error| {
format!(
"remove replacement {} during rollback: {error}",
self.destination.display()
)
})?;
}
if let Some(backup) = self.backup.as_ref() {
fs::rename(backup, &self.destination).map_err(|error| {
format!(
"restore backup {} to {}: {error}",
backup.display(),
self.destination.display()
)
})?;
}
self.committed = false;
Ok(())
}
pub fn finalize(&mut self) -> Result<(), String> {
if let Some(backup) = self.backup.take() {
match fs::remove_file(&backup) {
Ok(()) => {}
Err(error) if error.kind() == io::ErrorKind::NotFound => {}
Err(error) => {
self.backup = Some(backup.clone());
return Err(format!(
"remove update backup {}: {error}",
backup.display()
));
}
}
}
self.committed = false;
Ok(())
}
#[cfg(test)]
fn backup_path(&self) -> Option<&Path> {
self.backup.as_deref()
}
}
pub fn runtime_path_for_manager(manager: &Path) -> Result<PathBuf, String> {
let parent = manager
.parent()
.ok_or_else(|| "manager executable has no parent directory".to_string())?;
#[cfg(target_os = "windows")]
let name = "origin-runtime.exe";
#[cfg(not(target_os = "windows"))]
let name = "origin-runtime";
Ok(parent.join(name))
}
pub fn find_sibling_runtime(manager: &Path, version: &str) -> Result<Option<PathBuf>, String> {
let parent = manager
.parent()
.ok_or_else(|| "manager executable has no parent directory".to_string())?;
let mut candidates = Vec::new();
#[cfg(not(target_os = "macos"))]
candidates.push(runtime_path_for_manager(manager)?);
#[cfg(target_os = "windows")]
candidates.push(parent.join(format!("origin-speak-runtime-{version}-windows-x86_64.exe")));
#[cfg(target_os = "macos")]
{
candidates.push(parent.join(format!(
"origin-speak-runtime-{version}-macos-universal.zip"
)));
candidates.push(parent.join(format!(
"origin-speak-runtime-{version}-macos-{}.zip",
std::env::consts::ARCH
)));
}
#[cfg(all(unix, not(target_os = "macos")))]
candidates.push(parent.join(format!(
"origin-speak-runtime-{version}-linux-{}",
std::env::consts::ARCH
)));
Ok(candidates.into_iter().find(|path| path.is_file()))
}
#[cfg(target_os = "macos")]
pub fn transactional_install_macos_runtime_bundle(
archive: &Path,
bundle_destination: &Path,
) -> Result<crate::macos_bundle::BundleSwap, String> {
crate::macos_bundle::transactional_install_bundle_zip(archive, bundle_destination)
}
/// Replace one installed payload while keeping the previous live file as a
/// sibling backup. Callers may roll back after a later multi-file or health
/// check failure, or finalize only after the new installation is known-good.
pub fn transactional_replace(
source: &Path,
destination: &Path,
) -> Result<TransactionalSwap, String> {
validate_regular_source(source)?;
validate_regular_destination_or_missing(destination)?;
let parent = destination
.parent()
.ok_or_else(|| "runtime destination has no parent directory".to_string())?;
fs::create_dir_all(parent).map_err(|error| format!("create {}: {error}", parent.display()))?;
validate_destination_parent(parent)?;
#[cfg(target_os = "windows")]
crate::management::validate_known_windows_owned_root(parent)?;
let file_name = destination
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| "runtime destination filename is not valid UTF-8".to_string())?;
let sequence = SWAP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
let token = format!("{}-{sequence}", std::process::id());
let staged = parent.join(format!(".{file_name}.update-{token}.new"));
let backup = parent.join(format!(".{file_name}.update-{token}.bak"));
copy_and_flush(source, &staged)?;
if !same_file_contents(source, &staged).unwrap_or(false) {
let _ = fs::remove_file(&staged);
return Err(format!(
"staged replacement for {} did not match its source payload",
destination.display()
));
}
let had_original = match fs::symlink_metadata(destination) {
Ok(metadata) => {
if metadata.is_dir() || metadata.file_type().is_symlink() {
let _ = fs::remove_file(&staged);
return Err(format!(
"refusing to replace non-regular installed payload: {}",
destination.display()
));
}
fs::rename(destination, &backup).map_err(|error| {
let _ = fs::remove_file(&staged);
format!(
"move existing {} to update backup: {error}",
destination.display()
)
})?;
true
}
Err(error) if error.kind() == io::ErrorKind::NotFound => false,
Err(error) => {
let _ = fs::remove_file(&staged);
return Err(format!("inspect {}: {error}", destination.display()));
}
};
if let Err(error) = fs::rename(&staged, destination) {
if had_original {
let restore = fs::rename(&backup, destination);
let _ = fs::remove_file(&staged);
return match restore {
Ok(()) => Err(format!(
"install {} failed; previous payload restored: {error}",
destination.display()
)),
Err(restore_error) => Err(format!(
"install {} failed ({error}) and restoring its backup also failed ({restore_error})",
destination.display()
)),
};
}
let _ = fs::remove_file(&staged);
return Err(format!("install {}: {error}", destination.display()));
}
Ok(TransactionalSwap {
destination: destination.to_path_buf(),
backup: had_original.then_some(backup),
committed: true,
})
}
pub fn spawn_runtime_silent(runtime: &Path, args: &[String]) -> Result<Child, String> {
let mut command = Command::new(runtime);
command
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
// Prevent a console window even when a development/runtime binary was
// accidentally built with the console subsystem.
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
command.creation_flags(CREATE_NO_WINDOW);
}
command
.spawn()
.map_err(|error| format!("start {}: {error}", runtime.display()))
}
fn copy_and_flush(source: &Path, destination: &Path) -> Result<(), String> {
validate_regular_source(source)?;
let mut input =
fs::File::open(source).map_err(|error| format!("open {}: {error}", source.display()))?;
let mut options = fs::OpenOptions::new();
options.write(true).create_new(true);
let mut output = options.open(destination).map_err(|error| {
format!(
"create exclusive staged file {}: {error}",
destination.display()
)
})?;
io::copy(&mut input, &mut output).map_err(|error| format!("copy runtime payload: {error}"))?;
output
.flush()
.map_err(|error| format!("flush runtime payload: {error}"))?;
output
.sync_all()
.map_err(|error| format!("sync runtime payload: {error}"))?;
let permissions = fs::metadata(source)
.map_err(|error| format!("read runtime payload permissions: {error}"))?
.permissions();
fs::set_permissions(destination, permissions)
.map_err(|error| format!("preserve runtime payload permissions: {error}"))?;
Ok(())
}
fn validate_regular_source(path: &Path) -> Result<(), String> {
let metadata = fs::symlink_metadata(path)
.map_err(|error| format!("inspect source payload {}: {error}", path.display()))?;
if !metadata.is_file() || is_link_or_reparse_point(&metadata) {
return Err(format!(
"refusing source payload that is not a regular file: {}",
path.display()
));
}
Ok(())
}
fn validate_regular_destination_or_missing(path: &Path) -> Result<(), String> {
match fs::symlink_metadata(path) {
Ok(metadata) if metadata.is_file() && !is_link_or_reparse_point(&metadata) => Ok(()),
Ok(_) => Err(format!(
"refusing installed payload path that is not a regular file: {}",
path.display()
)),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(format!(
"inspect installed payload {}: {error}",
path.display()
)),
}
}
fn validate_destination_parent(path: &Path) -> Result<(), String> {
let metadata = fs::symlink_metadata(path)
.map_err(|error| format!("inspect destination parent {}: {error}", path.display()))?;
if !metadata.is_dir() || is_link_or_reparse_point(&metadata) {
return Err(format!(
"refusing destination parent that is not a real directory or is a reparse point: {}",
path.display()
));
}
Ok(())
}
#[cfg(target_os = "windows")]
fn is_link_or_reparse_point(metadata: &fs::Metadata) -> bool {
use std::os::windows::fs::MetadataExt;
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0400;
metadata.file_type().is_symlink()
|| metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
}
#[cfg(not(target_os = "windows"))]
fn is_link_or_reparse_point(metadata: &fs::Metadata) -> bool {
metadata.file_type().is_symlink()
}
fn same_file_contents(left: &Path, right: &Path) -> io::Result<bool> {
let left_meta = fs::metadata(left)?;
let right_meta = match fs::metadata(right) {
Ok(metadata) => metadata,
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(false),
Err(error) => return Err(error),
};
if left_meta.len() != right_meta.len() {
return Ok(false);
}
let mut left = fs::File::open(left)?;
let mut right = fs::File::open(right)?;
let mut left_buf = vec![0_u8; COPY_BUFFER];
let mut right_buf = vec![0_u8; COPY_BUFFER];
loop {
let left_read = left.read(&mut left_buf)?;
let right_read = right.read(&mut right_buf)?;
if left_read != right_read || left_buf[..left_read] != right_buf[..right_read] {
return Ok(false);
}
if left_read == 0 {
return Ok(true);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transactional_swap_rollback_restores_original_after_later_failure() {
let root = std::env::temp_dir().join(format!(
"origin-speak-bootstrap-rollback-test-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
let runtime_source = root.join("runtime-new.bin");
let runtime_live = root.join("runtime.bin");
let manager_source = root.join("manager-new.bin");
let manager_live = root.join("manager.bin");
fs::write(&runtime_source, b"new runtime").unwrap();
fs::write(&runtime_live, b"old runtime").unwrap();
fs::write(&manager_source, b"new manager").unwrap();
fs::write(&manager_live, b"old manager").unwrap();
let mut runtime = transactional_replace(&runtime_source, &runtime_live).unwrap();
let mut manager = transactional_replace(&manager_source, &manager_live).unwrap();
assert!(runtime.backup_path().is_some_and(Path::is_file));
assert!(manager.backup_path().is_some_and(Path::is_file));
// Simulate a health-gate failure after both files were replaced.
manager.rollback().unwrap();
runtime.rollback().unwrap();
assert_eq!(fs::read(&runtime_live).unwrap(), b"old runtime");
assert_eq!(fs::read(&manager_live).unwrap(), b"old manager");
fs::remove_dir_all(&root).unwrap();
}
#[test]
fn transactional_swap_finalize_removes_backup_only_after_success() {
let root = std::env::temp_dir().join(format!(
"origin-speak-bootstrap-finalize-test-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
let source = root.join("new.bin");
let live = root.join("live.bin");
fs::write(&source, b"new").unwrap();
fs::write(&live, b"old").unwrap();
let mut swap = transactional_replace(&source, &live).unwrap();
let backup = swap.backup_path().unwrap().to_path_buf();
assert!(backup.is_file());
swap.finalize().unwrap();
assert!(!backup.exists());
assert_eq!(fs::read(&live).unwrap(), b"new");
fs::remove_dir_all(&root).unwrap();
}
#[test]
fn install_refuses_non_regular_source_and_destination() {
let root = std::env::temp_dir().join(format!(
"origin-speak-bootstrap-regular-file-test-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
let source_dir = root.join("source-dir");
let source_file = root.join("source.bin");
let destination_dir = root.join("destination-dir");
fs::create_dir_all(&source_dir).unwrap();
fs::write(&source_file, b"payload").unwrap();
fs::create_dir_all(&destination_dir).unwrap();
assert!(transactional_replace(&source_dir, &root.join("runtime.bin")).is_err());
assert!(transactional_replace(&source_file, &destination_dir).is_err());
fs::remove_dir_all(&root).unwrap();
}
#[test]
fn destination_parent_must_be_real_directory() {
let root = std::env::temp_dir().join(format!(
"origin-speak-bootstrap-parent-test-{}",
std::process::id()
));
let _ = fs::remove_dir_all(&root);
fs::create_dir_all(&root).unwrap();
let directory = root.join("directory");
let file = root.join("file");
fs::create_dir(&directory).unwrap();
fs::write(&file, b"not a directory").unwrap();
assert!(validate_destination_parent(&directory).is_ok());
assert!(validate_destination_parent(&file).is_err());
fs::remove_dir_all(&root).unwrap();
}
}