|
|
|
@ -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();
|
|
|
|
|