更新修正

master
cysamurai 1 month ago
parent c8242e98e0
commit ae44211368

@ -395,6 +395,7 @@ name = "call-client"
version = "0.1.5" version = "0.1.5"
dependencies = [ dependencies = [
"chrono", "chrono",
"flate2",
"fs2", "fs2",
"reqwest 0.12.28", "reqwest 0.12.28",
"serde", "serde",

@ -28,4 +28,5 @@ serde_json = "1"
fs2 = "0.4" fs2 = "0.4"
reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] } reqwest = { version = "0.12", default-features = false, features = ["blocking", "json", "rustls-tls"] }
chrono = { version = "0.4", default-features = false, features = ["clock"] } chrono = { version = "0.4", default-features = false, features = ["clock"] }
flate2 = "1"

@ -180,6 +180,239 @@ pub struct AptUpdateCheckResult {
update_command: String, update_command: String,
} }
#[cfg(target_os = "linux")]
const ZYYUN_APT_LIST_FILE: &str = "/etc/apt/sources.list.d/zyyun.list";
#[cfg(target_os = "linux")]
const DEFAULT_ZYYUN_REPO_BASE: &str = "http://80.12.140.29:80/apt";
#[cfg(target_os = "linux")]
const DEFAULT_ZYYUN_SUITE: &str = "v10";
#[cfg(target_os = "linux")]
const DEFAULT_ZYYUN_COMPONENT: &str = "main";
/// 读取紫云 apt 源配置;缺失时使用与 deb postinst 一致的默认值。
#[cfg(target_os = "linux")]
fn read_zyyun_apt_repo_config() -> (String, String, String) {
let fallback = || {
(
DEFAULT_ZYYUN_REPO_BASE.to_string(),
DEFAULT_ZYYUN_SUITE.to_string(),
DEFAULT_ZYYUN_COMPONENT.to_string(),
)
};
let Ok(raw) = std::fs::read_to_string(ZYYUN_APT_LIST_FILE) else {
return fallback();
};
for line in raw.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((url, suite, component)) = parse_deb_source_line(line) {
return (url, suite, component);
}
}
fallback()
}
#[cfg(target_os = "linux")]
fn parse_deb_source_line(line: &str) -> Option<(String, String, String)> {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.first().copied() != Some("deb") {
return None;
}
let mut idx = 1usize;
if idx < parts.len() && parts[idx].starts_with('[') {
idx += 1;
}
if idx + 2 >= parts.len() {
return None;
}
Some((
parts[idx].to_string(),
parts[idx + 1].to_string(),
parts[idx + 2].to_string(),
))
}
#[cfg(target_os = "linux")]
fn detect_dpkg_architecture() -> String {
std::process::Command::new("dpkg")
.args(["--print-architecture"])
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "amd64".to_string())
}
#[cfg(target_os = "linux")]
fn dpkg_version_gt(a: &str, b: &str) -> bool {
std::process::Command::new("dpkg")
.args(["--compare-versions", a, "gt", b])
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(target_os = "linux")]
fn max_dpkg_version(versions: Vec<String>) -> Option<String> {
versions.into_iter().reduce(|acc, v| {
if dpkg_version_gt(&v, &acc) {
v
} else {
acc
}
})
}
#[cfg(target_os = "linux")]
fn parse_package_versions_from_packages(text: &str, package_name: &str) -> Vec<String> {
let mut versions = Vec::new();
for block in text.split("\n\n") {
let mut name: Option<&str> = None;
let mut ver: Option<&str> = None;
for line in block.lines() {
if let Some(rest) = line.strip_prefix("Package: ") {
name = Some(rest.trim());
} else if let Some(rest) = line.strip_prefix("Version: ") {
ver = Some(rest.trim());
}
}
if name == Some(package_name) {
if let Some(v) = ver {
if !v.is_empty() {
versions.push(v.to_string());
}
}
}
}
versions
}
/// 直接从仓库 HTTP 拉取 Packages.gz避免本机未 `apt update` 时候选版本滞后。
#[cfg(target_os = "linux")]
fn fetch_remote_highest_package_version(package_name: &str) -> Result<Option<String>, String> {
use flate2::read::GzDecoder;
use std::io::Read;
use std::time::Duration;
let (base_url, suite, component) = read_zyyun_apt_repo_config();
let arch = detect_dpkg_architecture();
let base = base_url.trim_end_matches('/');
let url = format!(
"{base}/dists/{suite}/{component}/binary-{arch}/Packages.gz"
);
let _ = app_log(
"info".to_string(),
format!(
"检查更新: 在线拉取 Packages.gz package={} arch={} url={}",
package_name, arch, url
),
);
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(20))
.build()
.map_err(|e| format!("创建 HTTP 客户端失败: {e}"))?;
let response = client
.get(&url)
.send()
.map_err(|e| format!("请求 Packages.gz 失败: {e}"))?;
if !response.status().is_success() {
return Err(format!(
"Packages.gz HTTP {}: {}",
response.status().as_u16(),
url
));
}
let bytes = response
.bytes()
.map_err(|e| format!("读取 Packages.gz 响应失败: {e}"))?;
let mut decoder = GzDecoder::new(bytes.as_ref());
let mut text = String::new();
decoder
.read_to_string(&mut text)
.map_err(|e| format!("解压 Packages.gz 失败: {e}"))?;
let versions = parse_package_versions_from_packages(&text, package_name);
if versions.is_empty() {
let _ = app_log(
"warn".to_string(),
format!(
"检查更新: Packages.gz 中未找到 package={}arch={}",
package_name, arch
),
);
return Ok(None);
}
let highest = max_dpkg_version(versions);
let _ = app_log(
"info".to_string(),
format!(
"检查更新: 在线解析最高版本 package={} remoteCandidate={}",
package_name,
highest.as_deref().unwrap_or("(none)")
),
);
Ok(highest)
}
#[cfg(target_os = "linux")]
fn merge_candidate_with_remote(
package_name: &str,
policy_candidate: &str,
) -> (String, bool) {
let policy_ok =
policy_candidate != "(none)" && policy_candidate != "(unknown)" && !policy_candidate.is_empty();
match fetch_remote_highest_package_version(package_name) {
Ok(Some(remote)) => {
if !policy_ok {
let _ = app_log(
"info".to_string(),
format!(
"检查更新: apt 本地候选不可用,使用在线候选 {remote}(提示:可执行 apt update 同步本地索引)"
),
);
return (remote, true);
}
if dpkg_version_gt(&remote, policy_candidate) {
let _ = app_log(
"info".to_string(),
format!(
"检查更新: 在线候选 {remote} 高于 apt-cache 本地候选 {policy_candidate}(仓库已发布新版本但本机可能未 apt update"
),
);
return (remote, true);
}
(policy_candidate.to_string(), true)
}
Ok(None) => (
if policy_ok {
policy_candidate.to_string()
} else {
"(unknown)".to_string()
},
policy_ok,
),
Err(err) => {
let _ = app_log(
"warn".to_string(),
format!("检查更新: 在线 Packages.gz 拉取失败,仍使用 apt-cache 本地结果: {err}"),
);
(
if policy_ok {
policy_candidate.to_string()
} else {
"(unknown)".to_string()
},
policy_ok,
)
}
}
}
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
fn extract_policy_value(output: &str, key: &str) -> String { fn extract_policy_value(output: &str, key: &str) -> String {
let label = key.trim_end_matches(':').trim(); let label = key.trim_end_matches(':').trim();
@ -376,12 +609,23 @@ pub(crate) fn check_apt_update_internal(
); );
} }
let policy_candidate = candidate_version.clone();
let (merged_candidate, remote_source_ok) =
merge_candidate_with_remote(&package_name, &policy_candidate);
candidate_version = merged_candidate;
let baseline_version = if installed_version == "(none)" || installed_version == "(unknown)" { let baseline_version = if installed_version == "(none)" || installed_version == "(unknown)" {
current_version.clone() current_version.clone()
} else { } else {
installed_version.clone() installed_version.clone()
}; };
let source_available = candidate_version != "(none)" && candidate_version != "(unknown)"; let policy_source_ok =
policy_candidate != "(none)" && policy_candidate != "(unknown)" && !policy_candidate.is_empty();
let source_available = policy_source_ok
|| remote_source_ok
|| (candidate_version != "(none)"
&& candidate_version != "(unknown)"
&& !candidate_version.trim().is_empty());
let has_update = source_available let has_update = source_available
&& candidate_version != baseline_version && candidate_version != baseline_version
&& !candidate_version.trim().is_empty(); && !candidate_version.trim().is_empty();

Loading…
Cancel
Save