CH-J ToolsSSH کے ذریعے سرور کا انتظام
مینو
شائع شدہ سورس

CH-J Tools

ایپلیکیشن کی کسی مخصوص ریلیز کی ڈائریکٹریز اور فائلیں دیکھیں۔

سورس ZIP ڈاؤن لوڈ کریں
CH-J / Server Manager 1704 فائلیں
0.1.2-beta شامل کیا گیا: · بلڈ کیا گیا:
service.rs شامل کیا گیا: تاریخ درج نہیں 15.3 KB · 276 لائنیں
کوڈ فائل ڈاؤن لوڈ کریں
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.
4use crate::core::config::ConfigStore;
5use crate::core::error::{CoreError, CoreResult};
6use crate::core::updates::contract::Release;
7use crate::core::updates::provider::{ReleaseProvider, VerifiedDownload};
8use crate::core::version::compare_versions;
9use parking_lot::Mutex;
10use serde_json::{json, Value};
11use std::cmp::Ordering;
12use std::path::PathBuf;
13use std::sync::Arc;
15#[derive(Default)]
16struct 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>,
26pub 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>,
37struct Operation<'a>(&'a Mutex<State>);
38impl Drop for Operation<'_> {
39 fn drop(&mut self) {
40 self.0.lock().operation = None;
41 }
44impl 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_fingerprints
97 }))
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 result
207 }
210#[cfg(test)]
211mod 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(&current,&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 }
SHA-256685667fc3a0d53ff9cff371cc4b285e4d356062b8a63b90fdeed033b61e4f341

CH-J Proprietary Software License 1.14 · سورس CH-J Proprietary Software License 1.14 کے تحت فراہم کیا جاتا ہے۔ اس کی دستیابی لائسنس کی شرائط نہیں بدلتی اور نہ ہی اضافی حقوق دیتی ہے۔

آرکائیو کا SHA-256ac8fe790f4ba60fc098f8c985fc2656cfcc6cdc21fb34b30e308a2b54e62258f
فائلیں