Compare commits
15 changed files with 37 additions and 308 deletions
|
|
@ -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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 工具缓存用于下次构建。
|
|
||||||
|
|
@ -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 |
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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};
|
||||||
|
|
|
||||||
|
|
@ -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())
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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"]);
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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, ¤t_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), ¤t_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, ¤t_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), ¤t_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,
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue