Paskelbtas šaltinis
Atsisiųsti šaltinį ZIP
CH-J Tools
Naršyti aplankus ir failus konkrečiai programai išleisti.
CH-J / Server Manager
1704 failai
0.1.2-beta
Pridėta: · Pastatytas:
1
//! Update state machine (port of `updates/updateService.js`). The UI never supplies a path,2
//! key or "verified" flag; installation re-verifies the internally tracked artifact.4
use crate::core::config::ConfigStore;5
use crate::core::error::{CoreError, CoreResult};6
use crate::core::updates::contract::Release;7
use crate::core::updates::provider::{ReleaseProvider, VerifiedDownload};8
use crate::core::version::compare_versions;9
use parking_lot::Mutex;10
use serde_json::{json, Value};11
use std::cmp::Ordering;12
use std::path::PathBuf;13
use std::sync::Arc;15
#[derive(Default)]16
struct State {17
checked: bool,18
release: Option<Release>,19
releases: Vec<Release>,20
selected: Option<String>,21
download: Option<VerifiedDownload>,22
launch_authorized: bool,23
operation: Option<&'static str>,24
}26
pub struct UpdateService {27
pub app_version: String,28
pub build_id: String,29
pub platform: String,30
pub arch: String,31
config: Arc<ConfigStore>,32
provider: Box<dyn Fn() -> CoreResult<ReleaseProvider> + Send + Sync>,33
logger: Arc<crate::core::logging::Logger>,34
state: Mutex<State>,35
}37
struct Operation<'a>(&'a Mutex<State>);38
impl Drop for Operation<'_> {39
fn drop(&mut self) {40
self.0.lock().operation = None;41
}42
}44
impl UpdateService {45
pub fn new(app_version: &str, build_id: &str, platform: &str, arch: &str, config: Arc<ConfigStore>, provider: Box<dyn Fn() -> CoreResult<ReleaseProvider> + Send + Sync>, logger: Arc<crate::core::logging::Logger>) -> Self {46
Self { app_version: app_version.into(), build_id: build_id.into(), platform: platform.into(), arch: arch.into(), config, provider, logger, state: Mutex::new(State::default()) }47
}49
fn begin(&self, name: &'static str) -> CoreResult<Operation<'_>> {50
let mut state = self.state.lock();51
if state.operation.is_some() {52
return Err(CoreError::plain("Another update operation is already running."));53
}54
state.operation = Some(name);55
Ok(Operation(&self.state))56
}58
fn relation(&self, release: &Release, releases: &[Release]) -> &'static str {59
match compare_versions(&release.version, &self.app_version).unwrap_or(Ordering::Equal) {60
Ordering::Greater => return "newerVersion",61
Ordering::Less => return "olderVersion",62
Ordering::Equal => {}63
}64
if !self.build_id.is_empty() && release.build_id.as_deref() == Some(self.build_id.as_str()) {65
return "sameVersionCurrentRelease";66
}67
let last = self.config.get()["updates"]["lastLaunchedRelease"].clone();68
if !last.is_null() && compare_versions(last["version"].as_str().unwrap_or("0.0"), &release.version).ok() == Some(Ordering::Equal) {69
if last["id"].as_str() == Some(release.id.as_str()) || last["sha512"].as_str() == Some(release.sha512.as_str()) {70
return "sameVersionCurrentRelease";71
}72
let parse = |v: &str| crate::core::updates::contract::parse_date(v);73
if let (Some(a), Some(b)) = (release.published_at.as_deref().and_then(parse), last["publishedAt"].as_str().and_then(parse)) {74
return if a > b { "sameVersionNewerRelease" } else { "sameVersionOlderRelease" };75
}76
}77
let newest_same = releases.iter().find(|r| compare_versions(&r.version, &self.app_version).ok() == Some(Ordering::Equal));78
if newest_same.map(|r| r.id == release.id).unwrap_or(false) { "sameVersionNewerRelease" } else { "sameVersionOlderRelease" }79
}81
pub fn get_state(&self) -> Value {82
let state = self.state.lock();83
let relation = state.release.as_ref().map(|r| self.relation(r, &state.releases));84
json!({85
"status": state.operation.unwrap_or("idle"),86
"checked": state.checked,87
"currentVersion": self.app_version,88
"currentBuildId": self.build_id,89
"updateAvailable": state.release.is_some() && relation != Some("sameVersionCurrentRelease"),90
"release": state.release,91
"releases": state.releases,92
"selectedReleaseId": state.selected,93
"releaseRelation": relation,94
"downloaded": state.download.as_ref().map(|d| json!({95
"releaseId": d.release_id, "filename": d.filename, "size": d.size, "sha512": d.sha512,96
"signatureVerified": d.signature_verified, "primaryFingerprint": d.primary_fingerprint, "signingFingerprints": d.signing_fingerprints97
}))98
})99
}101
pub async fn check(&self) -> CoreResult<Value> {102
let operation = self.begin("checking")?;103
let channel = self.config.get()["updates"]["channel"].as_str().unwrap_or("beta").to_string();104
let provider = (self.provider)()?;105
let (releases, source) = provider.list_releases(&self.app_version, &self.platform, &self.arch, &channel).await?;106
let preferred = releases.iter().find(|r| compare_versions(&r.version, &self.app_version).ok() == Some(Ordering::Greater)).or(releases.first()).cloned();107
{108
let mut state = self.state.lock();109
state.selected = preferred.as_ref().map(|r| r.id.clone());110
state.release = preferred.clone();111
state.releases = releases;112
state.checked = true;113
state.download = None;114
state.launch_authorized = false;115
}116
self.logger.info("Update check completed.", json!({ "channel": channel, "updateAvailable": preferred.is_some(), "version": preferred.as_ref().map(|r| r.version.clone()), "sourceBaseUrl": source }));117
drop(operation);118
Ok(self.get_state())119
}121
pub fn select(&self, id: &str) -> CoreResult<Value> {122
{123
let mut state = self.state.lock();124
if state.operation.is_some() {125
return Err(CoreError::plain("Another update operation is already running."));126
}127
let release = state.releases.iter().find(|r| r.id == id).cloned().ok_or_else(|| CoreError::plain("Selected release is not available in the current catalog."))?;128
state.selected = Some(release.id.clone());129
state.release = Some(release);130
state.download = None;131
state.launch_authorized = false;132
}133
Ok(self.get_state())134
}136
pub async fn download(&self) -> CoreResult<Value> {137
let release = self.state.lock().release.clone().ok_or_else(|| CoreError::plain("No update release is selected."))?;138
let operation = self.begin("downloading")?;139
{140
let mut state = self.state.lock();141
state.download = None;142
state.launch_authorized = false;143
}144
let provider = (self.provider)()?;145
let mut download = provider.download_and_verify(&release).await?;146
if !download.signature_verified {147
return Err(CoreError::plain("The update did not pass mandatory OpenPGP verification."));148
}149
download.release_id = Some(release.id.clone());150
self.logger.info(151
"Update downloaded and verified with SHA-512 and OpenPGP.",152
json!({ "filename": download.filename, "size": download.size, "reused": download.reused, "primaryFingerprint": download.primary_fingerprint, "signingFingerprints": download.signing_fingerprints }),153
);154
self.state.lock().download = Some(download);155
drop(operation);156
Ok(self.get_state())157
}159
pub fn verified_download_path(&self) -> Option<PathBuf> {160
self.state.lock().download.as_ref().filter(|d| d.signature_verified).map(|d| d.path.clone())161
}163
pub async fn prepare_installer_launch(&self) -> CoreResult<PathBuf> {164
let (release, download) = {165
let state = self.state.lock();166
match (&state.release, &state.download) {167
(Some(r), Some(d)) if d.release_id.as_deref() == Some(r.id.as_str()) => (r.clone(), d.clone()),168
_ => return Err(CoreError::plain("No verified update release is ready to install.")),169
}170
};171
let operation = self.begin("verifying-signature")?;172
self.state.lock().launch_authorized = false;173
let provider = (self.provider)()?;174
match provider.reverify_for_install(&download, &release).await {175
Ok(path) => {176
self.state.lock().launch_authorized = true;177
drop(operation);178
Ok(path)179
}180
Err(error) => {181
self.state.lock().download = None;182
Err(error)183
}184
}185
}187
pub fn mark_installer_launched(&self) -> CoreResult<Value> {188
let release = {189
let mut state = self.state.lock();190
let ok = matches!((&state.release, &state.download), (Some(r), Some(d)) if d.release_id.as_deref() == Some(r.id.as_str()) && d.signature_verified) && state.launch_authorized;191
if !ok {192
return Err(CoreError::plain("No verified update release is ready to install."));193
}194
state.launch_authorized = false;195
state.release.clone().expect("release")196
};197
self.config.record_launched_release(&json!({ "id": release.id, "version": release.version, "publishedAt": release.published_at, "sha512": release.sha512 }))?;198
Ok(self.get_state())199
}201
pub fn cleanup_downloads(&self) -> CoreResult<bool> {202
let result = (self.provider)()?.cleanup_downloads();203
let mut state = self.state.lock();204
state.download = None;205
state.launch_authorized = false;206
result207
}208
}210
#[cfg(test)]211
mod safety_tests {212
use super::*;213
use crate::core::updates::contract::{validate_release, Expected};215
fn release(id: &str, version: &str, date: &str) -> Release {216
validate_release(&json!({"id":id,"version":version,"platform":"mac","arch":"arm64","channel":"beta","sha512":"a".repeat(128),"size":10,"filename":"test.dmg","download_url":"/files/test.dmg","signature_url":"/files/test.dmg.asc","published_at":date}),217
&Expected {platform:"mac",arch:"arm64",channel:"beta",current_version:"0.1.1"}).unwrap()218
}219
fn service(root: &std::path::Path) -> UpdateService {220
UpdateService::new("0.1.1","fixture-current-build","mac","arm64",Arc::new(ConfigStore::new(root,vec!["en".into()])),221
Box::new(|| Err(CoreError::new("FIXTURE_PROVIDER_FAILURE","offline fixture"))),Arc::new(crate::core::logging::Logger::new(root)))222
}223
fn downloaded(release: &Release) -> VerifiedDownload {224
VerifiedDownload {path:PathBuf::from("unused.dmg"),signature_path:PathBuf::from("unused.dmg.asc"),filename:release.filename.clone(),size:release.size,sha512:release.sha512.clone(),reused:false,signature_verified:true,primary_fingerprint:"fixture".into(),signing_fingerprints:vec![],signature_created_at:None,release_id:Some(release.id.clone())}225
}226
#[test]227
fn relation_version_build_and_catalog_fallback_matrix() {228
let dir=tempfile::tempdir().unwrap();let service=service(dir.path());229
let newest=release("newest","0.1.1","2026-02-02");let older=release("older","0.1.1","2026-01-01");let list=vec![newest.clone(),older.clone()];230
for (candidate,expected) in [(release("higher","0.1.2","2026-01-01"),"newerVersion"),(release("lower","0.1.0","2026-01-01"),"olderVersion"),(newest.clone(),"sameVersionNewerRelease"),(older,"sameVersionOlderRelease")] {231
assert_eq!(service.relation(&candidate,&list),expected);232
}233
let mut current=newest.clone();current.build_id=Some("fixture-current-build".into());234
assert_eq!(service.relation(¤t,&list),"sameVersionCurrentRelease");235
}236
#[test]237
fn launched_release_identity_hash_and_publication_matrix() {238
let dir=tempfile::tempdir().unwrap();let service=service(dir.path());239
service.config.record_launched_release(&json!({"id":"launched","version":"0.1.1","sha512":"b".repeat(128),"publishedAt":"2026-02-01"})).unwrap();240
for (id,date,hash,expected) in [("launched","2026-03-01",'a',"sameVersionCurrentRelease"),("other","2026-03-01",'b',"sameVersionCurrentRelease"),("other","2026-03-01",'a',"sameVersionNewerRelease"),("other","2026-01-01",'a',"sameVersionOlderRelease")] {241
let mut r=release(id,"0.1.1",date);r.sha512=hash.to_string().repeat(128);242
assert_eq!(service.relation(&r,&[r.clone()]),expected);243
}244
}245
#[test]246
fn selection_clears_download_and_launch_authorization_and_rejects_unknown_id() {247
let dir=tempfile::tempdir().unwrap();let service=service(dir.path());let first=release("first","0.1.2","2026-02-02");let second=release("second","0.1.1","2026-01-01");248
{let mut state=service.state.lock();state.releases=vec![first.clone(),second.clone()];state.release=Some(first.clone());state.selected=Some(first.id.clone());state.download=Some(downloaded(&first));state.launch_authorized=true;}249
let before=service.get_state();assert!(service.select("unknown").is_err());assert_eq!(service.get_state(),before);250
let state=service.select("second").unwrap();assert_eq!(state["selectedReleaseId"],"second");assert_eq!(state["release"]["id"],"second");assert!(state["downloaded"].is_null());assert!(!service.state.lock().launch_authorized);assert!(service.verified_download_path().is_none());assert!(service.mark_installer_launched().is_err());251
}252
#[test]253
fn operation_guard_serializes_selection_and_releases_on_drop() {254
let dir=tempfile::tempdir().unwrap();let service=service(dir.path());let r=release("r","0.1.2","2026-01-01");service.state.lock().releases.push(r);255
let guard=service.begin("checking").unwrap();assert_eq!(service.get_state()["status"],"checking");assert!(service.begin("downloading").is_err());assert!(service.select("r").is_err());256
drop(guard);assert_eq!(service.get_state()["status"],"idle");assert!(service.select("r").is_ok());257
}258
#[tokio::test]259
async fn failed_check_and_download_release_operation_guard() {260
let dir=tempfile::tempdir().unwrap();let service=service(dir.path());261
assert_eq!(service.check().await.unwrap_err().code,"FIXTURE_PROVIDER_FAILURE");assert_eq!(service.get_state()["status"],"idle");assert_eq!(service.get_state()["checked"],false);262
let r=release("r","0.1.2","2026-01-01");{let mut s=service.state.lock();s.release=Some(r.clone());s.download=Some(downloaded(&r));s.launch_authorized=true;}263
assert_eq!(service.download().await.unwrap_err().code,"FIXTURE_PROVIDER_FAILURE");assert_eq!(service.get_state()["status"],"idle");assert!(service.verified_download_path().is_none());assert!(!service.state.lock().launch_authorized);264
}265
#[test]266
fn installer_launch_requires_matching_release_and_one_use_authorization() {267
let dir=tempfile::tempdir().unwrap();let service=service(dir.path());let r=release("r","0.1.2","2026-01-01");268
assert!(service.mark_installer_launched().is_err());269
{let mut s=service.state.lock();s.release=Some(r.clone());s.download=Some(downloaded(&r));}270
assert!(service.mark_installer_launched().is_err());271
service.state.lock().launch_authorized=true;272
service.mark_installer_launched().unwrap();assert_eq!(service.config.get()["updates"]["lastLaunchedRelease"]["id"],"r");assert!(service.mark_installer_launched().is_err());273
{let mut s=service.state.lock();s.launch_authorized=true;s.download.as_mut().unwrap().release_id=Some("other".into());}274
assert!(service.mark_installer_launched().is_err());275
}276
}SHA-256
685667fc3a0d53ff9cff371cc4b285e4d356062b8a63b90fdeed033b61e4f341CH-J Proprietary Software License 1.14 · Šaltinis pateiktas pagal CH-J Proprietary Software License 1.14. Jo prieinamumas nekeičia licencijos sąlygų ir nesuteikia papildomų teisių.
Archyvas SHA-256
ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f