fix(daemon): 去重失败版本并优化状态输出

This commit is contained in:
2026-07-15 22:32:27 +08:00
parent 60b8339ecb
commit 7890b667d7
4 changed files with 163 additions and 35 deletions
+45 -23
View File
@@ -1896,7 +1896,7 @@ fn print_human_json_value(value: &serde_json::Value) -> anyhow::Result<()> {
print_json_field(value, "current_message", "当前消息");
print_json_field(value, "download_progress", "下载进度");
print_json_field(value, "version_state_path", "版本状态路径");
print_json_field(value, "version_state", "版本状态");
print_daemon_version_state_json_summary(value.get("version_state"));
print_json_field(value, "resource_output_root", "资源目录");
print_json_field(value, "state_dir", "状态目录");
print_json_field(value, "socket_path", "socket");
@@ -1935,6 +1935,49 @@ fn print_human_json_value(value: &serde_json::Value) -> anyhow::Result<()> {
Ok(())
}
fn print_daemon_version_state_json_summary(value: Option<&serde_json::Value>) {
let Some(value) = value else {
return;
};
if value.is_null() {
return;
}
match serde_json::from_value::<OfficialVersionState>(value.clone()) {
Ok(version_state) => print_daemon_version_state_summary(&version_state),
Err(error) => print_field("版本状态", format!("无法解析:{error}")),
}
}
fn print_daemon_version_state_summary(version_state: &OfficialVersionState) {
print_optional_field(
"当前完成版本",
version_state
.current_completed_version
.as_ref()
.map(|version| version.id.as_str()),
);
print_optional_field(
"正在拉取版本",
version_state
.in_progress_version
.as_ref()
.map(|version| version.id.as_str()),
);
print_optional_field(
"上一个可用版本",
version_state
.previous_available_version
.as_ref()
.map(|version| version.id.as_str()),
);
print_field("失败版本数", version_state.failed_versions.len());
if let Some(failed) = version_state.failed_versions.last() {
print_field("最近失败版本", &failed.version.id);
print_field("最近失败时间", failed.failed_unix_seconds);
print_field("最近失败原因", &failed.error);
}
}
fn print_json_field(value: &serde_json::Value, key: &str, label: &str) {
let Some(value) = value.get(key) else {
return;
@@ -2177,28 +2220,7 @@ impl HumanReport for DaemonStatusReport {
print_field("下载进度", format_daemon_download_progress(progress));
}
if let Some(version_state) = self.version_state.as_ref() {
print_optional_field(
"当前完成版本",
version_state
.current_completed_version
.as_ref()
.map(|version| version.id.as_str()),
);
print_optional_field(
"正在拉取版本",
version_state
.in_progress_version
.as_ref()
.map(|version| version.id.as_str()),
);
print_optional_field(
"上一个可用版本",
version_state
.previous_available_version
.as_ref()
.map(|version| version.id.as_str()),
);
print_field("失败版本数", version_state.failed_versions.len());
print_daemon_version_state_summary(version_state);
}
print_optional_path_field("资源目录", self.resource_output_root.as_ref());
print_optional_path_field("版本状态", self.version_state_path.as_ref());
+115 -9
View File
@@ -1967,11 +1967,12 @@ fn recover_interrupted_version_state(path: &Path) -> anyhow::Result<()> {
return Ok(());
};
state.failed_versions.push(OfficialFailedVersionRecord {
upsert_failed_version(
&mut state.failed_versions,
version,
error: "上一次官方资源同步在完成发布前中断".to_string(),
failed_unix_seconds: unix_seconds_now(),
});
"上一次官方资源同步在完成发布前中断",
unix_seconds_now(),
);
trim_failed_versions(&mut state.failed_versions);
state.updated_unix_seconds = unix_seconds_now();
write_version_state(path, &state)
@@ -2047,6 +2048,9 @@ fn complete_version_state(
completed_unix_seconds: Some(unix_seconds_now()),
..record
});
if let Some(current) = state.current_completed_version.as_ref() {
remove_completed_version_failures(&mut state.failed_versions, current);
}
state.in_progress_version = None;
state.updated_unix_seconds = unix_seconds_now();
write_version_state(path, &state)
@@ -2059,11 +2063,12 @@ fn fail_version_state(
) -> anyhow::Result<()> {
let mut state = read_version_state(path)?.unwrap_or_default();
state.in_progress_version = None;
state.failed_versions.push(OfficialFailedVersionRecord {
version: record,
error: error.to_string(),
failed_unix_seconds: unix_seconds_now(),
});
upsert_failed_version(
&mut state.failed_versions,
record,
error,
unix_seconds_now(),
);
trim_failed_versions(&mut state.failed_versions);
state.updated_unix_seconds = unix_seconds_now();
write_version_state(path, &state)
@@ -2121,6 +2126,37 @@ fn trim_failed_versions(failed_versions: &mut Vec<OfficialFailedVersionRecord>)
}
}
fn upsert_failed_version(
failed_versions: &mut Vec<OfficialFailedVersionRecord>,
record: OfficialVersionRecord,
error: &str,
failed_unix_seconds: u64,
) {
if let Some(index) = failed_versions.iter().position(|failed| {
failed_version_matches(&failed.version, &record) && failed.error == error
}) {
failed_versions.remove(index);
}
failed_versions.push(OfficialFailedVersionRecord {
version: record,
error: error.to_string(),
failed_unix_seconds,
});
}
fn remove_completed_version_failures(
failed_versions: &mut Vec<OfficialFailedVersionRecord>,
completed: &OfficialVersionRecord,
) {
failed_versions.retain(|failed| !failed_version_matches(&failed.version, completed));
}
fn failed_version_matches(left: &OfficialVersionRecord, right: &OfficialVersionRecord) -> bool {
left.app_version == right.app_version
&& left.bundle_version == right.bundle_version
&& left.addressables_root == right.addressables_root
}
fn snapshot_path_for_state_path(
_snapshot: &OfficialUpdateSnapshot,
resource_root: &Path,
@@ -2764,6 +2800,76 @@ mod tests {
assert!(state.failed_versions[0].error.contains("403"));
}
#[test]
fn version_state_deduplicates_repeated_failures_and_clears_after_success() {
let temp = tempfile::TempDir::new().unwrap();
let path = temp.path().join("official-version-state.json");
let snapshot = OfficialUpdateSnapshot::new(
fixture_base_snapshot(),
vec![fixture_marker("current")],
Some(&fixture_bootstrap()),
);
let active = temp.path().join("versions/active");
for id in ["broken-1", "broken-2"] {
let staging = temp.path().join(".staging").join(id);
let failed = prepare_in_progress_version_state(
&path,
&snapshot,
id,
&active,
None,
&staging,
&staging.join("official-sync-snapshot.json"),
)
.unwrap();
fail_version_state(&path, failed, "download 403").unwrap();
}
let state = read_version_state(&path).unwrap().unwrap();
assert_eq!(state.failed_versions.len(), 1);
assert_eq!(state.failed_versions[0].version.id, "broken-2");
assert_eq!(state.failed_versions[0].error, "download 403");
let staging = temp.path().join(".staging/broken-3");
let failed = prepare_in_progress_version_state(
&path,
&snapshot,
"broken-3",
&active,
None,
&staging,
&staging.join("official-sync-snapshot.json"),
)
.unwrap();
fail_version_state(&path, failed, "download 404").unwrap();
let state = read_version_state(&path).unwrap().unwrap();
assert_eq!(state.failed_versions.len(), 2);
let staging = temp.path().join(".staging/fixed");
let fixed = prepare_in_progress_version_state(
&path,
&snapshot,
"fixed",
&active,
None,
&staging,
&staging.join("official-sync-snapshot.json"),
)
.unwrap();
let published = temp.path().join("versions/fixed");
complete_version_state(
&path,
fixed,
&published,
&published.join("official-sync-snapshot.json"),
)
.unwrap();
let state = read_version_state(&path).unwrap().unwrap();
assert!(state.failed_versions.is_empty());
}
#[test]
fn update_rejects_dangerous_output_root_before_network_work() {
let config = OfficialUpdateConfig {