feat(hololake): add persistent signed update rollback

This commit is contained in:
冰朔 2026-08-13 14:45:17 +08:00
commit 73eb8bc1ca
9 changed files with 727 additions and 15 deletions

View file

@ -9,6 +9,7 @@ use std::fs::{self, OpenOptions};
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::{AppHandle, Manager};
use tauri_plugin_updater::{Update, UpdaterExt};
@ -16,7 +17,9 @@ use uuid::Uuid;
const CANDIDATE_SCHEMA: &str = "hololake.release-candidate/v1";
const RECEIPT_SCHEMA: &str = "hololake.release-install-receipt/v1";
const RECOVERY_SCHEMA: &str = "hololake.release-recovery/v1";
const CANDIDATE_TTL_MS: u128 = 30 * 60 * 1000;
const EXPECTED_BUNDLE_IDENTIFIER: &str = "world.guanghu.hololake";
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
@ -36,6 +39,30 @@ struct ReleaseCandidateRecord {
expires_at_unix_ms: u128,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
struct ReleaseRecoveryRecord {
schema: String,
state: String,
release_id: String,
previous_version: String,
installed_version: String,
backup_bundle_path: String,
backup_identifier: String,
backup_team_identifier: String,
backup_cdhash: String,
failed_bundle_path: Option<String>,
observed_at_unix_ms: u128,
receipt_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct CodeSignatureEvidence {
identifier: String,
team_identifier: String,
cdhash: String,
}
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct ConfirmReleaseInstallInput {
@ -143,10 +170,35 @@ pub struct ReleaseInstallReceipt {
receipt_id: String,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReleaseRecoveryStatus {
pub schema: &'static str,
pub state: String,
pub previous_version: Option<String>,
pub installed_version: Option<String>,
pub backup_ready: bool,
pub human_action_required: bool,
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ReleaseRecoveryReceipt {
pub schema: &'static str,
pub state: String,
pub previous_version: String,
pub installed_version: String,
pub automatic_restart: bool,
pub receipt_id: String,
}
#[tauri::command]
pub async fn check_hololake_update(app: AppHandle) -> Result<ReleaseCheckReceipt, String> {
let trust = provisioned_release_trust()?
.ok_or_else(|| "HOLOLAKE_RELEASE_TRUST_UNPROVISIONED_NO_NETWORK_REQUEST".to_string())?;
let root = release_update_root(&app)?;
let _install_lease = install_lease(&root)?;
ensure_no_unresolved_recovery_at(&root)?;
let update = app
.updater_builder()
.endpoints(trust.endpoints.clone())
@ -164,7 +216,6 @@ pub async fn check_hololake_update(app: AppHandle) -> Result<ReleaseCheckReceipt
});
};
let snapshot = validate_update(&update, &trust.allowed_release_host)?;
let root = release_update_root(&app)?;
let candidate = create_candidate_at(&root, &snapshot)?;
Ok(ReleaseCheckReceipt {
schema: CANDIDATE_SCHEMA,
@ -181,6 +232,8 @@ pub async fn confirm_hololake_update_install(
let trust = provisioned_release_trust()?
.ok_or_else(|| "HOLOLAKE_RELEASE_TRUST_UNPROVISIONED_NO_NETWORK_REQUEST".to_string())?;
let root = release_update_root(&app)?;
let _install_lease = install_lease(&root)?;
ensure_no_unresolved_recovery_at(&root)?;
let record = authorize_candidate_at(&root, &input)?;
let update = app
.updater_builder()
@ -203,6 +256,32 @@ pub async fn confirm_hololake_update_install(
{
return Err("HOLOLAKE_RELEASE_PACKAGE_DIGEST_OR_SIZE_MISMATCH".into());
}
let current_bundle = current_app_bundle_path()?;
let backup = prepare_last_known_good_at(&root, &current_bundle)?;
let mut recovery = ReleaseRecoveryRecord {
schema: RECOVERY_SCHEMA.into(),
state: "LAST_KNOWN_GOOD_VERIFIED_INSTALLING".into(),
release_id: snapshot.envelope.release_id.clone(),
previous_version: snapshot.current_version.clone(),
installed_version: snapshot.envelope.version.clone(),
backup_bundle_path: backup.0.to_string_lossy().into_owned(),
backup_identifier: backup.1.identifier,
backup_team_identifier: backup.1.team_identifier,
backup_cdhash: backup.1.cdhash,
failed_bundle_path: None,
observed_at_unix_ms: now_unix_ms()?,
receipt_id: sha256_hex(
format!(
"{}\n{}\n{}\n{}",
snapshot.envelope.release_id,
snapshot.current_version,
snapshot.envelope.version,
snapshot.package_sha256
)
.as_bytes(),
),
};
write_json_atomic(&recovery_path(&root), &recovery, "RECOVERY")?;
let receipt = ReleaseInstallReceipt {
schema: RECEIPT_SCHEMA,
state: "SIGNED_PACKAGE_VERIFIED_INSTALL_STARTED_RESTART_REQUIRED",
@ -230,13 +309,142 @@ pub async fn confirm_hololake_update_install(
&receipt,
"RECEIPT",
)?;
mark_candidate_installing_at(&root, &record.candidate_id)?;
update
.install(&bytes)
.map_err(|error| format!("HOLOLAKE_RELEASE_INSTALL_FAILED: {error}"))?;
if let Err(error) = mark_candidate_installing_at(&root, &record.candidate_id) {
recovery.state = "INSTALL_FAILED_BACKUP_RETAINED".into();
recovery.observed_at_unix_ms = now_unix_ms()?;
write_json_atomic(&recovery_path(&root), &recovery, "RECOVERY")?;
return Err(error);
}
if let Err(error) = update.install(&bytes) {
recovery.state = "INSTALL_FAILED_BACKUP_RETAINED".into();
recovery.observed_at_unix_ms = now_unix_ms()?;
write_json_atomic(&recovery_path(&root), &recovery, "RECOVERY")?;
return Err(format!("HOLOLAKE_RELEASE_INSTALL_FAILED: {error}"));
}
recovery.state = "INSTALL_REPLACED_AWAITING_MANUAL_RESTART".into();
recovery.observed_at_unix_ms = now_unix_ms()?;
write_json_atomic(&recovery_path(&root), &recovery, "RECOVERY")?;
Ok(receipt)
}
#[tauri::command]
pub fn get_hololake_release_recovery_status(
app: AppHandle,
) -> Result<ReleaseRecoveryStatus, String> {
release_recovery_status_at(&release_update_root(&app)?)
}
#[tauri::command]
pub fn confirm_hololake_release_health(app: AppHandle) -> Result<ReleaseRecoveryReceipt, String> {
let root = release_update_root(&app)?;
let _lock = lock(&root)?;
let path = recovery_path(&root);
let mut record: ReleaseRecoveryRecord = read_json(&path)?;
if !matches!(
record.state.as_str(),
"AWAITING_HUMAN_HEALTH_CONFIRMATION"
| "ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT"
| "INSTALL_FAILED_BACKUP_RETAINED"
| "INSTALL_INTERRUPTED_BACKUP_RETAINED"
) {
return Err("HOLOLAKE_RELEASE_HEALTH_CONFIRMATION_NOT_AVAILABLE".into());
}
let running_version = app.package_info().version.to_string();
let expected = if record.state == "AWAITING_HUMAN_HEALTH_CONFIRMATION" {
&record.installed_version
} else {
&record.previous_version
};
if &running_version != expected {
return Err("HOLOLAKE_RELEASE_RUNNING_VERSION_MISMATCH".into());
}
remove_owned_bundle_path(&root, Path::new(&record.backup_bundle_path))?;
if let Some(failed) = record.failed_bundle_path.as_deref() {
remove_owned_failed_bundle(Path::new(failed), &current_app_parent()?)?;
}
record.state = match record.state.as_str() {
"AWAITING_HUMAN_HEALTH_CONFIRMATION" => "HEALTH_CONFIRMED_BACKUP_REMOVED",
"ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT" => "ROLLBACK_CONFIRMED_BACKUP_REMOVED",
_ => "INSTALL_FAILURE_ACKNOWLEDGED_BACKUP_REMOVED",
}
.into();
record.observed_at_unix_ms = now_unix_ms()?;
write_json_atomic(&path, &record, "RECOVERY")?;
Ok(recovery_receipt(&record))
}
#[tauri::command]
pub fn rollback_hololake_update(app: AppHandle) -> Result<ReleaseRecoveryReceipt, String> {
let root = release_update_root(&app)?;
let _lock = lock(&root)?;
let path = recovery_path(&root);
let mut record: ReleaseRecoveryRecord = read_json(&path)?;
if record.state != "AWAITING_HUMAN_HEALTH_CONFIRMATION" {
return Err("HOLOLAKE_RELEASE_ROLLBACK_NOT_AVAILABLE".into());
}
if app.package_info().version.to_string() != record.installed_version {
return Err("HOLOLAKE_RELEASE_RUNNING_VERSION_MISMATCH".into());
}
let backup = validated_backup_path(&root, &record)?;
let current = current_app_bundle_path()?;
let failed = replace_bundle_with_backup(&current, &backup, &record)?;
record.failed_bundle_path = Some(failed.to_string_lossy().into_owned());
record.state = "ROLLBACK_REPLACED_AWAITING_MANUAL_RESTART".into();
record.observed_at_unix_ms = now_unix_ms()?;
write_json_atomic(&path, &record, "RECOVERY")?;
Ok(recovery_receipt(&record))
}
pub(crate) fn observe_release_startup(app: &AppHandle) -> Result<(), String> {
let root = release_update_root(app)?;
let path = recovery_path(&root);
if !path.exists() {
return Ok(());
}
let _lock = lock(&root)?;
let mut record: ReleaseRecoveryRecord = read_json(&path)?;
let version = app.package_info().version.to_string();
let next = match record.state.as_str() {
"INSTALL_REPLACED_AWAITING_MANUAL_RESTART" if version == record.installed_version => {
Some("AWAITING_HUMAN_HEALTH_CONFIRMATION")
}
"ROLLBACK_REPLACED_AWAITING_MANUAL_RESTART" if version == record.previous_version => {
if let Some(failed) = record.failed_bundle_path.as_deref() {
remove_owned_failed_bundle(Path::new(failed), &current_app_parent()?)?;
record.failed_bundle_path = None;
}
Some("ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT")
}
"LAST_KNOWN_GOOD_VERIFIED_INSTALLING" if version == record.previous_version => {
Some("INSTALL_INTERRUPTED_BACKUP_RETAINED")
}
"LAST_KNOWN_GOOD_VERIFIED_INSTALLING" if version == record.installed_version => {
Some("AWAITING_HUMAN_HEALTH_CONFIRMATION")
}
"AWAITING_HUMAN_HEALTH_CONFIRMATION" if version == record.previous_version => {
Some("ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT")
}
_ => None,
};
if let Some(next) = next {
record.state = next.into();
record.observed_at_unix_ms = now_unix_ms()?;
write_json_atomic(&path, &record, "RECOVERY")?;
}
Ok(())
}
pub(crate) fn release_recovery_state(app: &AppHandle) -> Result<String, String> {
let root = release_update_root(app)?;
Ok(release_recovery_state_at(&root))
}
fn release_recovery_state_at(root: &Path) -> String {
release_recovery_status_at(root)
.map(|status| status.state)
.unwrap_or_else(|_| "RECOVERY_STATE_UNREADABLE_ACTION_REQUIRED".into())
}
struct UpdateSnapshot {
current_version: String,
envelope: ReleaseEnvelope,
@ -392,6 +600,323 @@ fn mark_candidate_installing_at(root: &Path, candidate_id: &str) -> Result<(), S
write_json_atomic(&path, &record, "CANDIDATE")
}
fn ensure_no_unresolved_recovery_at(root: &Path) -> Result<(), String> {
let path = recovery_path(root);
if !path.exists() {
return Ok(());
}
let record: ReleaseRecoveryRecord = read_json(&path)?;
if matches!(
record.state.as_str(),
"HEALTH_CONFIRMED_BACKUP_REMOVED"
| "ROLLBACK_CONFIRMED_BACKUP_REMOVED"
| "INSTALL_FAILURE_ACKNOWLEDGED_BACKUP_REMOVED"
) {
Ok(())
} else {
Err("HOLOLAKE_RELEASE_RECOVERY_ACTION_REQUIRED_BEFORE_NEXT_UPDATE".into())
}
}
fn release_recovery_status_at(root: &Path) -> Result<ReleaseRecoveryStatus, String> {
let path = recovery_path(root);
if !path.exists() {
return Ok(ReleaseRecoveryStatus {
schema: RECOVERY_SCHEMA,
state: "NONE".into(),
previous_version: None,
installed_version: None,
backup_ready: false,
human_action_required: false,
});
}
let record: ReleaseRecoveryRecord = read_json(&path)?;
let human_action_required = matches!(
record.state.as_str(),
"AWAITING_HUMAN_HEALTH_CONFIRMATION"
| "ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT"
| "INSTALL_FAILED_BACKUP_RETAINED"
| "INSTALL_INTERRUPTED_BACKUP_RETAINED"
);
let backup_ready = Path::new(&record.backup_bundle_path).exists();
Ok(ReleaseRecoveryStatus {
schema: RECOVERY_SCHEMA,
state: record.state,
previous_version: Some(record.previous_version),
installed_version: Some(record.installed_version),
backup_ready,
human_action_required,
})
}
fn recovery_receipt(record: &ReleaseRecoveryRecord) -> ReleaseRecoveryReceipt {
ReleaseRecoveryReceipt {
schema: RECOVERY_SCHEMA,
state: record.state.clone(),
previous_version: record.previous_version.clone(),
installed_version: record.installed_version.clone(),
automatic_restart: false,
receipt_id: sha256_hex(
format!(
"{}\n{}\n{}\n{}",
record.receipt_id, record.state, record.previous_version, record.installed_version
)
.as_bytes(),
),
}
}
fn prepare_last_known_good_at(
root: &Path,
current_bundle: &Path,
) -> Result<(PathBuf, CodeSignatureEvidence), String> {
let current = current_bundle
.canonicalize()
.map_err(|error| format!("HOLOLAKE_RELEASE_CURRENT_BUNDLE_UNAVAILABLE: {error}"))?;
if current.extension().and_then(|value| value.to_str()) != Some("app") {
return Err("HOLOLAKE_RELEASE_CURRENT_BUNDLE_INVALID".into());
}
let current_evidence = verify_bundle_signature(&current)?;
if current_evidence.identifier != EXPECTED_BUNDLE_IDENTIFIER
|| current_evidence.team_identifier.is_empty()
{
return Err("HOLOLAKE_RELEASE_CURRENT_BUNDLE_IDENTITY_INVALID".into());
}
let next_root = root.join(format!(".last-known-good-next-{}", Uuid::new_v4()));
fs::create_dir(&next_root)
.map_err(|error| format!("HOLOLAKE_RELEASE_BACKUP_CREATE_FAILED: {error}"))?;
let next_bundle = next_root.join("HoloLake.app");
let copy = trusted_command("/usr/bin/ditto")
.arg(&current)
.arg(&next_bundle)
.output();
let copy = match copy {
Ok(output) => output,
Err(error) => {
let _ = fs::remove_dir_all(&next_root);
return Err(format!("HOLOLAKE_RELEASE_BACKUP_COPY_FAILED: {error}"));
}
};
if let Err(error) = require_command_success(copy, "HOLOLAKE_RELEASE_BACKUP_COPY") {
let _ = fs::remove_dir_all(&next_root);
return Err(error);
}
let copied_evidence = match verify_bundle_signature(&next_bundle) {
Ok(evidence) => evidence,
Err(error) => {
let _ = fs::remove_dir_all(&next_root);
return Err(error);
}
};
if copied_evidence != current_evidence {
let _ = fs::remove_dir_all(&next_root);
return Err("HOLOLAKE_RELEASE_BACKUP_SIGNATURE_DRIFT".into());
}
let final_root = root.join("last-known-good");
let previous_root = root.join(format!(".last-known-good-previous-{}", Uuid::new_v4()));
if final_root.exists() {
fs::rename(&final_root, &previous_root)
.map_err(|error| format!("HOLOLAKE_RELEASE_BACKUP_ROTATE_FAILED: {error}"))?;
}
if let Err(error) = fs::rename(&next_root, &final_root) {
if previous_root.exists() {
let _ = fs::rename(&previous_root, &final_root);
}
let _ = fs::remove_dir_all(&next_root);
return Err(format!("HOLOLAKE_RELEASE_BACKUP_ACTIVATE_FAILED: {error}"));
}
if previous_root.exists() {
fs::remove_dir_all(&previous_root)
.map_err(|error| format!("HOLOLAKE_RELEASE_BACKUP_ROTATE_FAILED: {error}"))?;
}
Ok((final_root.join("HoloLake.app"), copied_evidence))
}
fn validated_backup_path(root: &Path, record: &ReleaseRecoveryRecord) -> Result<PathBuf, String> {
let backup = PathBuf::from(&record.backup_bundle_path)
.canonicalize()
.map_err(|error| format!("HOLOLAKE_RELEASE_BACKUP_UNAVAILABLE: {error}"))?;
let owned_root = root
.join("last-known-good")
.canonicalize()
.map_err(|error| format!("HOLOLAKE_RELEASE_BACKUP_UNAVAILABLE: {error}"))?;
if backup != owned_root.join("HoloLake.app") {
return Err("HOLOLAKE_RELEASE_BACKUP_BOUNDARY_INVALID".into());
}
let evidence = verify_bundle_signature(&backup)?;
if evidence.identifier != record.backup_identifier
|| evidence.team_identifier != record.backup_team_identifier
|| evidence.cdhash != record.backup_cdhash
{
return Err("HOLOLAKE_RELEASE_BACKUP_SIGNATURE_MISMATCH".into());
}
Ok(backup)
}
fn replace_bundle_with_backup(
current: &Path,
backup: &Path,
record: &ReleaseRecoveryRecord,
) -> Result<PathBuf, String> {
let current = current
.canonicalize()
.map_err(|error| format!("HOLOLAKE_RELEASE_CURRENT_BUNDLE_UNAVAILABLE: {error}"))?;
let current_evidence = verify_bundle_signature(&current)?;
if current_evidence.identifier != EXPECTED_BUNDLE_IDENTIFIER
|| current_evidence.team_identifier != record.backup_team_identifier
{
return Err("HOLOLAKE_RELEASE_CURRENT_BUNDLE_IDENTITY_INVALID".into());
}
let parent = current
.parent()
.ok_or("HOLOLAKE_RELEASE_CURRENT_BUNDLE_PATH_INVALID")?;
let failed = parent.join(format!(".HoloLake.failed-{}.app", Uuid::new_v4()));
fs::rename(&current, &failed)
.map_err(|error| format!("HOLOLAKE_RELEASE_ROLLBACK_STAGE_FAILED: {error}"))?;
let copied = trusted_command("/usr/bin/ditto")
.arg(backup)
.arg(&current)
.output();
let copied = match copied {
Ok(output) => output,
Err(error) => {
restore_staged_current(&failed, &current)?;
return Err(format!("HOLOLAKE_RELEASE_ROLLBACK_COPY_FAILED: {error}"));
}
};
if let Err(error) = require_command_success(copied, "HOLOLAKE_RELEASE_ROLLBACK_COPY") {
restore_staged_current(&failed, &current)?;
return Err(error);
}
let restored = verify_bundle_signature(&current);
if restored.as_ref()
!= Ok(&CodeSignatureEvidence {
identifier: record.backup_identifier.clone(),
team_identifier: record.backup_team_identifier.clone(),
cdhash: record.backup_cdhash.clone(),
})
{
restore_staged_current(&failed, &current)?;
return Err("HOLOLAKE_RELEASE_ROLLBACK_SIGNATURE_MISMATCH".into());
}
Ok(failed)
}
fn restore_staged_current(staged: &Path, current: &Path) -> Result<(), String> {
if current.exists() {
fs::remove_dir_all(current)
.map_err(|error| format!("HOLOLAKE_RELEASE_ROLLBACK_RECOVERY_FAILED: {error}"))?;
}
fs::rename(staged, current)
.map_err(|error| format!("HOLOLAKE_RELEASE_ROLLBACK_RECOVERY_FAILED: {error}"))
}
fn remove_owned_bundle_path(root: &Path, bundle: &Path) -> Result<(), String> {
let expected = root.join("last-known-good").join("HoloLake.app");
if bundle != expected {
return Err("HOLOLAKE_RELEASE_BACKUP_BOUNDARY_INVALID".into());
}
let owner = root.join("last-known-good");
if owner.exists() {
fs::remove_dir_all(&owner)
.map_err(|error| format!("HOLOLAKE_RELEASE_BACKUP_REMOVE_FAILED: {error}"))?;
}
Ok(())
}
fn remove_owned_failed_bundle(bundle: &Path, expected_parent: &Path) -> Result<(), String> {
let name = bundle
.file_name()
.and_then(|value| value.to_str())
.ok_or("HOLOLAKE_RELEASE_FAILED_BUNDLE_BOUNDARY_INVALID")?;
if !name.starts_with(".HoloLake.failed-") || !name.ends_with(".app") {
return Err("HOLOLAKE_RELEASE_FAILED_BUNDLE_BOUNDARY_INVALID".into());
}
if bundle.parent() != Some(expected_parent) {
return Err("HOLOLAKE_RELEASE_FAILED_BUNDLE_BOUNDARY_INVALID".into());
}
if bundle.exists() {
fs::remove_dir_all(bundle)
.map_err(|error| format!("HOLOLAKE_RELEASE_FAILED_BUNDLE_REMOVE_FAILED: {error}"))?;
}
Ok(())
}
fn verify_bundle_signature(bundle: &Path) -> Result<CodeSignatureEvidence, String> {
let verify = trusted_command("/usr/bin/codesign")
.args(["--verify", "--deep", "--strict", "--verbose=4"])
.arg(bundle)
.output()
.map_err(|error| format!("HOLOLAKE_RELEASE_CODESIGN_VERIFY_FAILED: {error}"))?;
require_command_success(verify, "HOLOLAKE_RELEASE_CODESIGN_VERIFY")?;
let display = trusted_command("/usr/bin/codesign")
.args(["-dv", "--verbose=4"])
.arg(bundle)
.output()
.map_err(|error| format!("HOLOLAKE_RELEASE_CODESIGN_READ_FAILED: {error}"))?;
let text = String::from_utf8_lossy(&display.stderr);
let value = |name: &str| {
text.lines()
.find_map(|line| line.strip_prefix(&format!("{name}=")))
.map(str::to_owned)
.ok_or_else(|| format!("HOLOLAKE_RELEASE_CODESIGN_{name}_MISSING"))
};
Ok(CodeSignatureEvidence {
identifier: value("Identifier")?,
team_identifier: value("TeamIdentifier")?,
cdhash: value("CDHash")?,
})
}
fn current_app_bundle_path() -> Result<PathBuf, String> {
let executable = std::env::current_exe()
.map_err(|error| format!("HOLOLAKE_RELEASE_CURRENT_EXECUTABLE_UNAVAILABLE: {error}"))?
.canonicalize()
.map_err(|error| format!("HOLOLAKE_RELEASE_CURRENT_EXECUTABLE_UNAVAILABLE: {error}"))?;
let contents = executable
.parent()
.and_then(Path::parent)
.ok_or("HOLOLAKE_RELEASE_CURRENT_BUNDLE_PATH_INVALID")?;
let bundle = contents
.parent()
.ok_or("HOLOLAKE_RELEASE_CURRENT_BUNDLE_PATH_INVALID")?;
if contents.file_name().and_then(|value| value.to_str()) != Some("Contents")
|| bundle.extension().and_then(|value| value.to_str()) != Some("app")
{
return Err("HOLOLAKE_RELEASE_CURRENT_BUNDLE_PATH_INVALID".into());
}
Ok(bundle.to_path_buf())
}
fn current_app_parent() -> Result<PathBuf, String> {
let bundle = current_app_bundle_path()?;
let parent = bundle
.parent()
.ok_or("HOLOLAKE_RELEASE_CURRENT_BUNDLE_PATH_INVALID")?
.to_path_buf();
Ok(parent)
}
fn trusted_command(program: &str) -> Command {
let mut command = Command::new(program);
command
.env_clear()
.env("PATH", "/usr/bin:/bin")
.env("LANG", "C");
command
}
fn require_command_success(output: Output, label: &str) -> Result<(), String> {
if output.status.success() {
Ok(())
} else {
Err(format!(
"{label}_FAILED: {}",
String::from_utf8_lossy(&output.stderr).trim()
))
}
}
fn release_update_root(app: &AppHandle) -> Result<PathBuf, String> {
let root = app
.path()
@ -408,6 +933,10 @@ fn candidate_path(root: &Path, candidate_id: &str) -> PathBuf {
root.join(format!("{candidate_id}.json"))
}
fn recovery_path(root: &Path) -> PathBuf {
root.join("recovery.json")
}
fn lock(root: &Path) -> Result<std::fs::File, String> {
let lock = OpenOptions::new()
.read(true)
@ -422,6 +951,21 @@ fn lock(root: &Path) -> Result<std::fs::File, String> {
Ok(lock)
}
fn install_lease(root: &Path) -> Result<std::fs::File, String> {
let lease = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.mode(0o600)
.open(root.join("install.lock"))
.map_err(|error| format!("HOLOLAKE_RELEASE_INSTALL_LEASE_FAILED: {error}"))?;
lease
.try_lock_exclusive()
.map_err(|_| "HOLOLAKE_RELEASE_INSTALL_ALREADY_IN_PROGRESS".to_string())?;
Ok(lease)
}
fn read_json<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T, String> {
let metadata = fs::symlink_metadata(path)
.map_err(|error| format!("HOLOLAKE_RELEASE_CANDIDATE_READ_FAILED: {error}"))?;
@ -578,4 +1122,88 @@ mod tests {
"HOLOLAKE_RELEASE_BROADCAST_CHANGED_RECONFIRM_REQUIRED"
);
}
#[test]
fn unresolved_recovery_blocks_the_next_update_until_human_resolution() {
let root = TempDir::new().unwrap();
let backup = root.path().join("last-known-good/HoloLake.app");
fs::create_dir_all(&backup).unwrap();
let recovery = ReleaseRecoveryRecord {
schema: RECOVERY_SCHEMA.into(),
state: "AWAITING_HUMAN_HEALTH_CONFIRMATION".into(),
release_id: "release-1".into(),
previous_version: "0.1.0".into(),
installed_version: "0.2.0".into(),
backup_bundle_path: backup.to_string_lossy().into_owned(),
backup_identifier: EXPECTED_BUNDLE_IDENTIFIER.into(),
backup_team_identifier: "TEAM".into(),
backup_cdhash: "cdhash".into(),
failed_bundle_path: None,
observed_at_unix_ms: now_unix_ms().unwrap(),
receipt_id: "receipt".into(),
};
write_json_atomic(&recovery_path(root.path()), &recovery, "RECOVERY").unwrap();
assert_eq!(
ensure_no_unresolved_recovery_at(root.path()).unwrap_err(),
"HOLOLAKE_RELEASE_RECOVERY_ACTION_REQUIRED_BEFORE_NEXT_UPDATE"
);
let status = release_recovery_status_at(root.path()).unwrap();
assert_eq!(status.state, "AWAITING_HUMAN_HEALTH_CONFIRMATION");
assert!(status.backup_ready);
assert!(status.human_action_required);
}
#[test]
fn failed_bundle_cleanup_is_confined_to_the_exact_current_parent() {
let root = TempDir::new().unwrap();
let parent = root.path().join("Applications");
let elsewhere = root.path().join("elsewhere");
fs::create_dir_all(&parent).unwrap();
fs::create_dir_all(&elsewhere).unwrap();
let misleading = elsewhere.join(".HoloLake.failed-test.app");
fs::create_dir(&misleading).unwrap();
assert_eq!(
remove_owned_failed_bundle(&misleading, &parent).unwrap_err(),
"HOLOLAKE_RELEASE_FAILED_BUNDLE_BOUNDARY_INVALID"
);
assert!(misleading.exists());
}
#[test]
fn unreadable_recovery_record_keeps_the_app_available_but_locks_updates() {
let root = TempDir::new().unwrap();
fs::write(recovery_path(root.path()), b"not-json").unwrap();
assert_eq!(
release_recovery_state_at(root.path()),
"RECOVERY_STATE_UNREADABLE_ACTION_REQUIRED"
);
assert!(ensure_no_unresolved_recovery_at(root.path()).is_err());
}
#[test]
fn only_one_install_operation_can_hold_the_release_lease() {
let root = TempDir::new().unwrap();
let first = install_lease(root.path()).unwrap();
assert_eq!(
install_lease(root.path()).unwrap_err(),
"HOLOLAKE_RELEASE_INSTALL_ALREADY_IN_PROGRESS"
);
drop(first);
assert!(install_lease(root.path()).is_ok());
}
#[test]
fn partial_rollback_copy_is_removed_before_the_running_bundle_is_restored() {
let root = TempDir::new().unwrap();
let current = root.path().join("HoloLake.app");
let staged = root.path().join(".HoloLake.failed-test.app");
fs::create_dir(&current).unwrap();
fs::write(current.join("partial"), b"partial").unwrap();
fs::create_dir(&staged).unwrap();
fs::write(staged.join("original"), b"original").unwrap();
restore_staged_current(&staged, &current).unwrap();
assert!(current.join("original").exists());
assert!(!current.join("partial").exists());
assert!(!staged.exists());
}
}