Compare commits

..
15 changed files with 37 additions and 308 deletions

View file

@ -1,7 +1,7 @@
{ {
"schema": "hololake.engineering-build-nodes/v0.1", "schema": "hololake.engineering-build-nodes/v0.1",
"id": "HOLOLAKE-BUILD-NODE-REGISTRY-0001", "id": "HOLOLAKE-BUILD-NODE-REGISTRY-0001",
"updated_at": "2026-08-17T14:01:16+08:00", "updated_at": "2026-07-30T12:52:24+08:00",
"authority": "HOLOLAKE_PRODUCT_ENGINEERING", "authority": "HOLOLAKE_PRODUCT_ENGINEERING",
"nodes": [ "nodes": [
{ {
@ -19,9 +19,9 @@
"product_scope": "HoloLake Era", "product_scope": "HoloLake Era",
"fifth_domain_node": false, "fifth_domain_node": false,
"pufferfish_node": false, "pufferfish_node": false,
"binding_state": "WINDOWS_X64_RELEASE_BUILD_VERIFIED", "binding_state": "LOCAL_BUILD_RUNTIME_VERIFIED",
"production_signing": "UPDATER_SIGNED_AUTHENTICODE_NOT_CONFIGURED", "production_signing": "NOT_CONFIGURED",
"record": "operations/2026-08-17-hololake-0.4.1-windows-x64-release.md" "record": "operations/2026-07-30-hololake-windows-build-node-044.md"
} }
] ]
} }

View file

@ -1,29 +0,0 @@
# HoloLake 0.4.1 · Windows x64 构建与验收回执
- 状态:`WINDOWS_X64_RELEASE_BUILD_VERIFIED`
- 构建节点:`HL-BUILD-WIN-GZ-001`
- 源码提交:`39fc36f`
- 构建产物:`HoloLake-0.4.1-Windows-x64-setup.exe`
- 安装包 SHA-256`5b84c082ea7adcf4999af11a6080a2c8a7d704ccfaef9c65cb92e84286ac32a2`
- 更新签名 SHA-256`62d14c273f04fa7abb05610d50acd6d4c129cc54a714fe32dffad761fb784b6a`
## 验收事实
- NSIS 安装器生成成功;安装器外壳为标准 32 位 NSIS 自解压程序,安装后的 HoloLake 主程序 PE Machine 为 `0x8664`x86_64
- Tauri updater 的 Minisign 签名使用产品内置公钥独立验证通过。
- Windows 更新安装保留广播复核、包大小、SHA-256 与 updater 签名验证;不再调用 macOS 的 `.app``ditto``codesign` 路径。
- Windows 当前不声明 macOS 式本地回滚;健康确认与失败回执显式记录 `NO_LOCAL_BACKUP`
- 静默安装返回 `0`,程序启动后保持响应,静默卸载完成且安装目录消失。
- Windows 代码仓库与 PNCC Git 命令使用 Windows 可执行路径和受限环境SSH 投影使用平台可执行路径。
- Windows 本机 AI 直连 Agent 尚未实现;客户端保持 `CLOSED_NO_TCP_FALLBACK`,不以不安全 TCP 监听冒充完成。
## 签名边界
- updater 包签名:`VERIFIED`
- Windows Authenticode`NOT_CONFIGURED`
- 因此首次下载或安装时 Windows SmartScreen 仍可能显示未知发布者提示;不得把 updater 签名表述为 Microsoft 代码签名。
## 节点与清理
- 本机持久入口使用私有 SSH 导航登记;公开工程仓库不保存公网地址、私钥或口令。
- 构建完成后已删除节点上的临时 updater 私钥、密码文件、Debug 缓存与临时 NSIS 解压副本;保留 Release 构建缓存和官方 NSIS 工具缓存用于下次构建。

View file

@ -1,5 +1,4 @@
import assert from 'node:assert/strict' import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test' import test from 'node:test'
import { import {
@ -75,11 +74,3 @@ test('release pipeline requires updater signing, Developer ID and Apple notariza
APPLE_TEAM_ID: 'TEAM', APPLE_TEAM_ID: 'TEAM',
})) }))
}) })
test('Windows updater keeps signed installation but never claims the macOS rollback boundary', () => {
const source = readFileSync(new URL('../src-tauri/src/release_update.rs', import.meta.url), 'utf8')
assert.match(source, /SIGNED_PACKAGE_VERIFIED_INSTALLING_NO_LOCAL_ROLLBACK/)
assert.match(source, /HOLOLAKE_RELEASE_WINDOWS_LOCAL_ROLLBACK_NOT_AVAILABLE/)
assert.match(source, /snapshot\.envelope\.hololake\.rollback\.supported\s*&&\s*platform_recovery\.local_rollback_available/)
assert.match(source, /update\.install\(&bytes\)/)
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

View file

@ -4,7 +4,6 @@ use serde::{Deserialize, Serialize};
use std::cmp::Reverse; use std::cmp::Reverse;
use std::fs::{self, OpenOptions}; use std::fs::{self, OpenOptions};
use std::io::Write; use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
use std::process::Command; use std::process::Command;
@ -14,18 +13,6 @@ use tauri_plugin_dialog::DialogExt;
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
#[cfg(windows)]
trait OpenOptionsModeExt {
fn mode(&mut self, mode: u32) -> &mut Self;
}
#[cfg(windows)]
impl OpenOptionsModeExt for OpenOptions {
fn mode(&mut self, _mode: u32) -> &mut Self {
self
}
}
const SNAPSHOT_SCHEMA: &str = "hololake.code-channel/v1"; const SNAPSHOT_SCHEMA: &str = "hololake.code-channel/v1";
const REGISTRY_SCHEMA: &str = "hololake.code-channel-registry/v1"; const REGISTRY_SCHEMA: &str = "hololake.code-channel-registry/v1";
const ALLOWED_HOSTS: &[&str] = &["guanghulab.com", "guanghubingshuo.com", "guanghu.chat"]; const ALLOWED_HOSTS: &[&str] = &["guanghulab.com", "guanghubingshuo.com", "guanghu.chat"];
@ -220,7 +207,6 @@ pub(crate) fn register_managed_repository(
fn ensure_root(root: &Path) -> Result<(), String> { fn ensure_root(root: &Path) -> Result<(), String> {
fs::create_dir_all(root.join("repositories")) fs::create_dir_all(root.join("repositories"))
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_STORAGE_UNAVAILABLE: {error}"))?; .map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_STORAGE_UNAVAILABLE: {error}"))?;
#[cfg(unix)]
fs::set_permissions(root, fs::Permissions::from_mode(0o700)) fs::set_permissions(root, fs::Permissions::from_mode(0o700))
.map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_PERMISSION_FAILED: {error}"))?; .map_err(|error| format!("HOLOLAKE_CODE_CHANNEL_PERMISSION_FAILED: {error}"))?;
Ok(()) Ok(())
@ -540,15 +526,11 @@ fn clone_at(root: &Path, input: CloneCodeChannelInput) -> Result<CodeChannelSnap
if destination.exists() { if destination.exists() {
return Err("HOLOLAKE_CODE_CHANNEL_DESTINATION_EXISTS".into()); return Err("HOLOLAKE_CODE_CHANNEL_DESTINATION_EXISTS".into());
} }
let mut command = Command::new(git_executable()); let output = Command::new("/usr/bin/git")
command
.env("GIT_TERMINAL_PROMPT", "0") .env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_CONFIG_NOSYSTEM", "1");
#[cfg(unix)]
command
.env("GIT_ASKPASS", "/usr/bin/false") .env("GIT_ASKPASS", "/usr/bin/false")
.env("SSH_ASKPASS", "/usr/bin/false"); .env("SSH_ASKPASS", "/usr/bin/false")
let output = command .env("GIT_CONFIG_NOSYSTEM", "1")
.args([ .args([
"-c", "-c",
"credential.helper=", "credential.helper=",
@ -779,7 +761,7 @@ fn write_registry(root: &Path, registry: &CodeChannelRegistry) -> Result<(), Str
} }
fn git(root: &Path, args: &[&str], operation: &str) -> Result<String, String> { fn git(root: &Path, args: &[&str], operation: &str) -> Result<String, String> {
let output = Command::new(git_executable()) let output = Command::new("/usr/bin/git")
.current_dir(root) .current_dir(root)
.env("GIT_TERMINAL_PROMPT", "0") .env("GIT_TERMINAL_PROMPT", "0")
.args(args) .args(args)
@ -796,7 +778,7 @@ fn git(root: &Path, args: &[&str], operation: &str) -> Result<String, String> {
} }
fn git_optional(root: &Path, args: &[&str]) -> Option<String> { fn git_optional(root: &Path, args: &[&str]) -> Option<String> {
let output = Command::new(git_executable()) let output = Command::new("/usr/bin/git")
.current_dir(root) .current_dir(root)
.env("GIT_TERMINAL_PROMPT", "0") .env("GIT_TERMINAL_PROMPT", "0")
.args(args) .args(args)
@ -808,14 +790,6 @@ fn git_optional(root: &Path, args: &[&str]) -> Option<String> {
.then(|| String::from_utf8_lossy(&output.stdout).into_owned()) .then(|| String::from_utf8_lossy(&output.stdout).into_owned())
} }
fn git_executable() -> &'static str {
if cfg!(windows) {
"git"
} else {
"/usr/bin/git"
}
}
fn now_unix_ms() -> Result<u128, String> { fn now_unix_ms() -> Result<u128, String> {
SystemTime::now() SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)

View file

@ -14,7 +14,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
#[cfg(target_os = "macos")]
use std::process::Command; use std::process::Command;
use std::time::{Duration, SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tauri::{AppHandle, Manager, State}; use tauri::{AppHandle, Manager, State};

View file

@ -1,57 +0,0 @@
use serde::Serialize;
use tauri::AppHandle;
const DISCOVERY_SCHEMA: &str = "hololake.nearby-ai-discovery/v1";
/// Windows currently has no Unix-domain socket transport. Keep the public app
/// launchable while reporting the connector boundary as closed, rather than
/// silently opening a TCP listener with weaker local-user isolation.
#[derive(Default)]
pub struct DirectLocalBrokerState;
impl DirectLocalBrokerState {
pub fn ensure_started(&self, _app: &AppHandle) -> Result<bool, String> {
Ok(false)
}
pub fn active_connection_count(&self) -> usize {
0
}
}
#[derive(Clone, Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NearbyAiDiscoverySnapshot {
pub schema: &'static str,
pub state: &'static str,
pub service_name: &'static str,
pub transport: &'static str,
pub language_protocol: &'static str,
pub automatic_same_device_discovery: bool,
pub large_invitation_copy_required: bool,
pub generic_ai_visitor: &'static str,
pub guanghu_persona: &'static str,
pub local_network_discovery: &'static str,
pub authority: &'static str,
}
#[tauri::command]
pub fn get_nearby_ai_discovery() -> NearbyAiDiscoverySnapshot {
NearbyAiDiscoverySnapshot {
schema: DISCOVERY_SCHEMA,
state: "WINDOWS_TRANSPORT_NOT_YET_IMPLEMENTED",
service_name: "HoloLake",
transport: "CLOSED_NO_TCP_FALLBACK",
language_protocol: "GLP/1.0",
automatic_same_device_discovery: false,
large_invitation_copy_required: false,
generic_ai_visitor: "DEFERRED",
guanghu_persona: "BINDING_EVIDENCE_REQUIRED",
local_network_discovery: "DEFERRED_UNTIL_ENCRYPTED_TRANSPORT_AND_APPROVAL",
authority: "UNAVAILABLE_IS_NOT_AUTHORIZATION",
}
}
pub fn run_connector() -> Result<(), String> {
Err("HOLOLAKE_WINDOWS_DIRECT_LOCAL_CONNECTOR_NOT_IMPLEMENTED".into())
}

View file

@ -1195,8 +1195,7 @@ fn modified_unix_ms(metadata: &fs::Metadata) -> u128 {
} }
fn git(root: &Path, args: &[&str], operation: &str) -> Result<String, String> { fn git(root: &Path, args: &[&str], operation: &str) -> Result<String, String> {
let executable = if cfg!(windows) { "git" } else { "/usr/bin/git" }; let output = Command::new("/usr/bin/git")
let output = Command::new(executable)
.current_dir(root) .current_dir(root)
.env("GIT_TERMINAL_PROMPT", "0") .env("GIT_TERMINAL_PROMPT", "0")
.args(args) .args(args)

View file

@ -1,11 +1,7 @@
mod authenticated_storage;
mod circular_lake_membrane; mod circular_lake_membrane;
mod authenticated_storage;
mod code_channel; mod code_channel;
mod code_repo_login; mod code_repo_login;
#[cfg(unix)]
mod direct_local_broker;
#[cfg(windows)]
#[path = "direct_local_broker_windows.rs"]
mod direct_local_broker; mod direct_local_broker;
mod direct_local_session; mod direct_local_session;
mod dynamic_capability_routing; mod dynamic_capability_routing;

View file

@ -688,23 +688,12 @@ fn remove_exact_cache(root: &Path, cache: &Path) -> Result<(), String> {
} }
fn trusted_git() -> Command { fn trusted_git() -> Command {
let executable = if cfg!(windows) { "git" } else { "/usr/bin/git" }; let mut command = Command::new("/usr/bin/git");
let mut command = Command::new(executable);
command.env_clear();
#[cfg(unix)]
command.env("PATH", "/usr/bin:/bin");
#[cfg(windows)]
for key in ["PATH", "SystemRoot", "WINDIR", "TEMP", "TMP"] {
if let Some(value) = std::env::var_os(key) {
command.env(key, value);
}
}
command command
.env_clear()
.env("PATH", "/usr/bin:/bin")
.env("GIT_CONFIG_NOSYSTEM", "1") .env("GIT_CONFIG_NOSYSTEM", "1")
.env( .env("GIT_CONFIG_GLOBAL", "/dev/null")
"GIT_CONFIG_GLOBAL",
if cfg!(windows) { "NUL" } else { "/dev/null" },
)
.env("GIT_TERMINAL_PROMPT", "0") .env("GIT_TERMINAL_PROMPT", "0")
.env("GCM_INTERACTIVE", "Never") .env("GCM_INTERACTIVE", "Never")
.args(["-c", "protocol.file.allow=never"]); .args(["-c", "protocol.file.allow=never"]);

View file

@ -8,7 +8,6 @@ use serde::{Deserialize, Serialize};
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::fs::{self, OpenOptions}; use std::fs::{self, OpenOptions};
use std::io::Write; use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::OpenOptionsExt;
use std::path::{Component, Path, PathBuf}; use std::path::{Component, Path, PathBuf};
use std::process::{Command, Output}; use std::process::{Command, Output};
@ -17,18 +16,6 @@ use tauri::AppHandle;
use tauri_plugin_dialog::DialogExt; use tauri_plugin_dialog::DialogExt;
use uuid::Uuid; use uuid::Uuid;
#[cfg(windows)]
trait OpenOptionsModeExt {
fn mode(&mut self, mode: u32) -> &mut Self;
}
#[cfg(windows)]
impl OpenOptionsModeExt for OpenOptions {
fn mode(&mut self, _mode: u32) -> &mut Self {
self
}
}
const MANIFEST_PATH: &str = ".hololake/persona/manifest.json"; const MANIFEST_PATH: &str = ".hololake/persona/manifest.json";
const MANIFEST_SCHEMA: &str = "hololake.persona/v1"; const MANIFEST_SCHEMA: &str = "hololake.persona/v1";
const MAX_MANIFEST_BYTES: usize = 512 * 1024; const MAX_MANIFEST_BYTES: usize = 512 * 1024;
@ -778,23 +765,12 @@ fn now_unix_ms() -> Result<u128, String> {
} }
fn trusted_git() -> Command { fn trusted_git() -> Command {
let executable = if cfg!(windows) { "git" } else { "/usr/bin/git" }; let mut command = Command::new("/usr/bin/git");
let mut command = Command::new(executable);
command.env_clear();
#[cfg(unix)]
command.env("PATH", "/usr/bin:/bin");
#[cfg(windows)]
for key in ["PATH", "SystemRoot", "WINDIR", "TEMP", "TMP"] {
if let Some(value) = std::env::var_os(key) {
command.env(key, value);
}
}
command command
.env_clear()
.env("PATH", "/usr/bin:/bin")
.env("GIT_CONFIG_NOSYSTEM", "1") .env("GIT_CONFIG_NOSYSTEM", "1")
.env( .env("GIT_CONFIG_GLOBAL", "/dev/null")
"GIT_CONFIG_GLOBAL",
if cfg!(windows) { "NUL" } else { "/dev/null" },
)
.env("GIT_TERMINAL_PROMPT", "0"); .env("GIT_TERMINAL_PROMPT", "0");
command command
} }

View file

@ -120,8 +120,7 @@ fn parse_projection(bytes: &[u8]) -> Result<ServerPnccProjection, String> {
#[tauri::command] #[tauri::command]
pub async fn query_jd_pncc_server_projection() -> Result<ServerPnccProjection, String> { pub async fn query_jd_pncc_server_projection() -> Result<ServerPnccProjection, String> {
tauri::async_runtime::spawn_blocking(|| { tauri::async_runtime::spawn_blocking(|| {
let executable = if cfg!(windows) { "ssh" } else { "/usr/bin/ssh" }; let output = Command::new("/usr/bin/ssh")
let output = Command::new(executable)
.args([ .args([
"-o", "-o",
"BatchMode=yes", "BatchMode=yes",

View file

@ -7,7 +7,6 @@ use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::fs::{self, OpenOptions}; use std::fs::{self, OpenOptions};
use std::io::Write; use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::{Command, Output}; use std::process::{Command, Output};
@ -16,18 +15,6 @@ use tauri::{AppHandle, Manager};
use tauri_plugin_updater::{Update, UpdaterExt}; use tauri_plugin_updater::{Update, UpdaterExt};
use uuid::Uuid; use uuid::Uuid;
#[cfg(windows)]
trait OpenOptionsModeExt {
fn mode(&mut self, mode: u32) -> &mut Self;
}
#[cfg(windows)]
impl OpenOptionsModeExt for OpenOptions {
fn mode(&mut self, _mode: u32) -> &mut Self {
self
}
}
const CANDIDATE_SCHEMA: &str = "hololake.release-candidate/v1"; const CANDIDATE_SCHEMA: &str = "hololake.release-candidate/v1";
const RECEIPT_SCHEMA: &str = "hololake.release-install-receipt/v1"; const RECEIPT_SCHEMA: &str = "hololake.release-install-receipt/v1";
const RECOVERY_SCHEMA: &str = "hololake.release-recovery/v1"; const RECOVERY_SCHEMA: &str = "hololake.release-recovery/v1";
@ -270,17 +257,18 @@ pub async fn confirm_hololake_update_install(
{ {
return Err("HOLOLAKE_RELEASE_PACKAGE_DIGEST_OR_SIZE_MISMATCH".into()); return Err("HOLOLAKE_RELEASE_PACKAGE_DIGEST_OR_SIZE_MISMATCH".into());
} }
let platform_recovery = prepare_platform_recovery_at(&root)?; let current_bundle = current_app_bundle_path()?;
let backup = prepare_last_known_good_at(&root, &current_bundle)?;
let mut recovery = ReleaseRecoveryRecord { let mut recovery = ReleaseRecoveryRecord {
schema: RECOVERY_SCHEMA.into(), schema: RECOVERY_SCHEMA.into(),
state: platform_recovery.installing_state.into(), state: "LAST_KNOWN_GOOD_VERIFIED_INSTALLING".into(),
release_id: snapshot.envelope.release_id.clone(), release_id: snapshot.envelope.release_id.clone(),
previous_version: snapshot.current_version.clone(), previous_version: snapshot.current_version.clone(),
installed_version: snapshot.envelope.version.clone(), installed_version: snapshot.envelope.version.clone(),
backup_bundle_path: platform_recovery.backup_bundle_path, backup_bundle_path: backup.0.to_string_lossy().into_owned(),
backup_identifier: platform_recovery.backup_identifier, backup_identifier: backup.1.identifier,
backup_team_identifier: platform_recovery.backup_team_identifier, backup_team_identifier: backup.1.team_identifier,
backup_cdhash: platform_recovery.backup_cdhash, backup_cdhash: backup.1.cdhash,
failed_bundle_path: None, failed_bundle_path: None,
observed_at_unix_ms: now_unix_ms()?, observed_at_unix_ms: now_unix_ms()?,
receipt_id: sha256_hex( receipt_id: sha256_hex(
@ -305,8 +293,7 @@ pub async fn confirm_hololake_update_install(
package_size: snapshot.package_size, package_size: snapshot.package_size,
automatic_restart: false, automatic_restart: false,
health_receipt_required: snapshot.envelope.hololake.rollback.health_receipt_required, health_receipt_required: snapshot.envelope.hololake.rollback.health_receipt_required,
rollback_declared_by_broadcast: snapshot.envelope.hololake.rollback.supported rollback_declared_by_broadcast: snapshot.envelope.hololake.rollback.supported,
&& platform_recovery.local_rollback_available,
receipt_id: sha256_hex( receipt_id: sha256_hex(
format!( format!(
"{}\n{}\n{}\n{}", "{}\n{}\n{}\n{}",
@ -324,13 +311,13 @@ pub async fn confirm_hololake_update_install(
"RECEIPT", "RECEIPT",
)?; )?;
if let Err(error) = mark_candidate_installing_at(&root, &record.candidate_id) { if let Err(error) = mark_candidate_installing_at(&root, &record.candidate_id) {
recovery.state = platform_install_failed_state().into(); recovery.state = "INSTALL_FAILED_BACKUP_RETAINED".into();
recovery.observed_at_unix_ms = now_unix_ms()?; recovery.observed_at_unix_ms = now_unix_ms()?;
write_json_atomic(&recovery_path(&root), &recovery, "RECOVERY")?; write_json_atomic(&recovery_path(&root), &recovery, "RECOVERY")?;
return Err(error); return Err(error);
} }
if let Err(error) = update.install(&bytes) { if let Err(error) = update.install(&bytes) {
recovery.state = platform_install_failed_state().into(); recovery.state = "INSTALL_FAILED_BACKUP_RETAINED".into();
recovery.observed_at_unix_ms = now_unix_ms()?; recovery.observed_at_unix_ms = now_unix_ms()?;
write_json_atomic(&recovery_path(&root), &recovery, "RECOVERY")?; write_json_atomic(&recovery_path(&root), &recovery, "RECOVERY")?;
return Err(format!("HOLOLAKE_RELEASE_INSTALL_FAILED: {error}")); return Err(format!("HOLOLAKE_RELEASE_INSTALL_FAILED: {error}"));
@ -360,8 +347,6 @@ pub fn confirm_hololake_release_health(app: AppHandle) -> Result<ReleaseRecovery
| "ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT" | "ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT"
| "INSTALL_FAILED_BACKUP_RETAINED" | "INSTALL_FAILED_BACKUP_RETAINED"
| "INSTALL_INTERRUPTED_BACKUP_RETAINED" | "INSTALL_INTERRUPTED_BACKUP_RETAINED"
| "INSTALL_FAILED_NO_LOCAL_BACKUP"
| "INSTALL_INTERRUPTED_NO_LOCAL_BACKUP"
) { ) {
return Err("HOLOLAKE_RELEASE_HEALTH_CONFIRMATION_NOT_AVAILABLE".into()); return Err("HOLOLAKE_RELEASE_HEALTH_CONFIRMATION_NOT_AVAILABLE".into());
} }
@ -374,14 +359,13 @@ pub fn confirm_hololake_release_health(app: AppHandle) -> Result<ReleaseRecovery
if &running_version != expected { if &running_version != expected {
return Err("HOLOLAKE_RELEASE_RUNNING_VERSION_MISMATCH".into()); return Err("HOLOLAKE_RELEASE_RUNNING_VERSION_MISMATCH".into());
} }
finalize_platform_recovery_artifacts(&root, &record)?; 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() { record.state = match record.state.as_str() {
"AWAITING_HUMAN_HEALTH_CONFIRMATION" if record.backup_bundle_path.is_empty() => {
"HEALTH_CONFIRMED_NO_LOCAL_BACKUP"
}
"AWAITING_HUMAN_HEALTH_CONFIRMATION" => "HEALTH_CONFIRMED_BACKUP_REMOVED", "AWAITING_HUMAN_HEALTH_CONFIRMATION" => "HEALTH_CONFIRMED_BACKUP_REMOVED",
"ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT" => "ROLLBACK_CONFIRMED_BACKUP_REMOVED", "ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT" => "ROLLBACK_CONFIRMED_BACKUP_REMOVED",
_ if record.backup_bundle_path.is_empty() => "INSTALL_FAILURE_ACKNOWLEDGED_NO_LOCAL_BACKUP",
_ => "INSTALL_FAILURE_ACKNOWLEDGED_BACKUP_REMOVED", _ => "INSTALL_FAILURE_ACKNOWLEDGED_BACKUP_REMOVED",
} }
.into(); .into();
@ -390,7 +374,6 @@ pub fn confirm_hololake_release_health(app: AppHandle) -> Result<ReleaseRecovery
Ok(recovery_receipt(&record)) Ok(recovery_receipt(&record))
} }
#[cfg(not(windows))]
#[tauri::command] #[tauri::command]
pub fn rollback_hololake_update(app: AppHandle) -> Result<ReleaseRecoveryReceipt, String> { pub fn rollback_hololake_update(app: AppHandle) -> Result<ReleaseRecoveryReceipt, String> {
let root = release_update_root(&app)?; let root = release_update_root(&app)?;
@ -413,12 +396,6 @@ pub fn rollback_hololake_update(app: AppHandle) -> Result<ReleaseRecoveryReceipt
Ok(recovery_receipt(&record)) Ok(recovery_receipt(&record))
} }
#[cfg(windows)]
#[tauri::command]
pub fn rollback_hololake_update(_app: AppHandle) -> Result<ReleaseRecoveryReceipt, String> {
Err("HOLOLAKE_RELEASE_WINDOWS_LOCAL_ROLLBACK_NOT_AVAILABLE".into())
}
pub(crate) fn observe_release_startup(app: &AppHandle) -> Result<(), String> { pub(crate) fn observe_release_startup(app: &AppHandle) -> Result<(), String> {
let root = release_update_root(app)?; let root = release_update_root(app)?;
let path = recovery_path(&root); let path = recovery_path(&root);
@ -445,16 +422,6 @@ pub(crate) fn observe_release_startup(app: &AppHandle) -> Result<(), String> {
"LAST_KNOWN_GOOD_VERIFIED_INSTALLING" if version == record.installed_version => { "LAST_KNOWN_GOOD_VERIFIED_INSTALLING" if version == record.installed_version => {
Some("AWAITING_HUMAN_HEALTH_CONFIRMATION") Some("AWAITING_HUMAN_HEALTH_CONFIRMATION")
} }
"SIGNED_PACKAGE_VERIFIED_INSTALLING_NO_LOCAL_ROLLBACK"
if version == record.previous_version =>
{
Some("INSTALL_INTERRUPTED_NO_LOCAL_BACKUP")
}
"SIGNED_PACKAGE_VERIFIED_INSTALLING_NO_LOCAL_ROLLBACK"
if version == record.installed_version =>
{
Some("AWAITING_HUMAN_HEALTH_CONFIRMATION")
}
"AWAITING_HUMAN_HEALTH_CONFIRMATION" if version == record.previous_version => { "AWAITING_HUMAN_HEALTH_CONFIRMATION" if version == record.previous_version => {
Some("ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT") Some("ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT")
} }
@ -648,10 +615,8 @@ fn ensure_no_unresolved_recovery_at(root: &Path) -> Result<(), String> {
if matches!( if matches!(
record.state.as_str(), record.state.as_str(),
"HEALTH_CONFIRMED_BACKUP_REMOVED" "HEALTH_CONFIRMED_BACKUP_REMOVED"
| "HEALTH_CONFIRMED_NO_LOCAL_BACKUP"
| "ROLLBACK_CONFIRMED_BACKUP_REMOVED" | "ROLLBACK_CONFIRMED_BACKUP_REMOVED"
| "INSTALL_FAILURE_ACKNOWLEDGED_BACKUP_REMOVED" | "INSTALL_FAILURE_ACKNOWLEDGED_BACKUP_REMOVED"
| "INSTALL_FAILURE_ACKNOWLEDGED_NO_LOCAL_BACKUP"
) { ) {
Ok(()) Ok(())
} else { } else {
@ -678,11 +643,8 @@ fn release_recovery_status_at(root: &Path) -> Result<ReleaseRecoveryStatus, Stri
| "ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT" | "ROLLED_BACK_AWAITING_HUMAN_ACKNOWLEDGEMENT"
| "INSTALL_FAILED_BACKUP_RETAINED" | "INSTALL_FAILED_BACKUP_RETAINED"
| "INSTALL_INTERRUPTED_BACKUP_RETAINED" | "INSTALL_INTERRUPTED_BACKUP_RETAINED"
| "INSTALL_FAILED_NO_LOCAL_BACKUP"
| "INSTALL_INTERRUPTED_NO_LOCAL_BACKUP"
); );
let backup_ready = let backup_ready = Path::new(&record.backup_bundle_path).exists();
!record.backup_bundle_path.is_empty() && Path::new(&record.backup_bundle_path).exists();
Ok(ReleaseRecoveryStatus { Ok(ReleaseRecoveryStatus {
schema: RECOVERY_SCHEMA, schema: RECOVERY_SCHEMA,
state: record.state, state: record.state,
@ -710,74 +672,6 @@ fn recovery_receipt(record: &ReleaseRecoveryRecord) -> ReleaseRecoveryReceipt {
} }
} }
struct PlatformRecoveryPreparation {
installing_state: &'static str,
backup_bundle_path: String,
backup_identifier: String,
backup_team_identifier: String,
backup_cdhash: String,
local_rollback_available: bool,
}
#[cfg(not(windows))]
fn prepare_platform_recovery_at(root: &Path) -> Result<PlatformRecoveryPreparation, String> {
let current_bundle = current_app_bundle_path()?;
let (backup, evidence) = prepare_last_known_good_at(root, &current_bundle)?;
Ok(PlatformRecoveryPreparation {
installing_state: "LAST_KNOWN_GOOD_VERIFIED_INSTALLING",
backup_bundle_path: backup.to_string_lossy().into_owned(),
backup_identifier: evidence.identifier,
backup_team_identifier: evidence.team_identifier,
backup_cdhash: evidence.cdhash,
local_rollback_available: true,
})
}
#[cfg(windows)]
fn prepare_platform_recovery_at(_root: &Path) -> Result<PlatformRecoveryPreparation, String> {
Ok(PlatformRecoveryPreparation {
installing_state: "SIGNED_PACKAGE_VERIFIED_INSTALLING_NO_LOCAL_ROLLBACK",
backup_bundle_path: String::new(),
backup_identifier: String::new(),
backup_team_identifier: String::new(),
backup_cdhash: String::new(),
local_rollback_available: false,
})
}
#[cfg(not(windows))]
fn platform_install_failed_state() -> &'static str {
"INSTALL_FAILED_BACKUP_RETAINED"
}
#[cfg(windows)]
fn platform_install_failed_state() -> &'static str {
"INSTALL_FAILED_NO_LOCAL_BACKUP"
}
#[cfg(not(windows))]
fn finalize_platform_recovery_artifacts(
root: &Path,
record: &ReleaseRecoveryRecord,
) -> Result<(), String> {
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()?)?;
}
Ok(())
}
#[cfg(windows)]
fn finalize_platform_recovery_artifacts(
_root: &Path,
record: &ReleaseRecoveryRecord,
) -> Result<(), String> {
if !record.backup_bundle_path.is_empty() || record.failed_bundle_path.is_some() {
return Err("HOLOLAKE_RELEASE_WINDOWS_RECOVERY_BOUNDARY_INVALID".into());
}
Ok(())
}
fn prepare_last_known_good_at( fn prepare_last_known_good_at(
root: &Path, root: &Path,
current_bundle: &Path, current_bundle: &Path,

View file

@ -402,8 +402,7 @@ fn write_json_atomic(path: &Path, value: &UserPnccBindingRecord) -> Result<(), S
} }
fn run_git(root: &Path, args: &[&str], operation: &str) -> Result<String, String> { fn run_git(root: &Path, args: &[&str], operation: &str) -> Result<String, String> {
let executable = if cfg!(windows) { "git" } else { "/usr/bin/git" }; let output = Command::new("/usr/bin/git")
let output = Command::new(executable)
.current_dir(root) .current_dir(root)
.env("GIT_TERMINAL_PROMPT", "0") .env("GIT_TERMINAL_PROMPT", "0")
.env("GIT_CONFIG_NOSYSTEM", "1") .env("GIT_CONFIG_NOSYSTEM", "1")

View file

@ -37,8 +37,7 @@
"targets": "all", "targets": "all",
"icon": [ "icon": [
"icons/icon.icns", "icons/icon.icns",
"icons/icon.png", "icons/icon.png"
"icons/icon.ico"
], ],
"createUpdaterArtifacts": true, "createUpdaterArtifacts": true,
"category": "Productivity", "category": "Productivity",