mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-23 07:05:14 +08:00
feat: track official versions and import resources
Persist official version-state, extend CAS/ResourceRepository import classification, and add offline catalog and failure regression fixtures. Fixes #13 Fixes #12 Fixes #8
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
use bat_adapters::official::yostar_jp::PatchPlatform;
|
||||
use bat_infrastructure::{
|
||||
lexical_absolute, open_append_file, read_file_no_symlink, validate_output_root,
|
||||
validate_runtime_state_dir, write_file_atomic, OfficialServerInfoSource, OfficialUpdateConfig,
|
||||
OfficialUpdateProgress, OfficialUpdateReport, OfficialUpdateService, OfficialUpdateStatus,
|
||||
OfficialVerificationSummary, PRIVATE_FILE_MODE,
|
||||
lexical_absolute, open_append_file, read_file_no_symlink, read_version_state,
|
||||
validate_output_root, validate_runtime_state_dir, write_file_atomic, OfficialServerInfoSource,
|
||||
OfficialUpdateConfig, OfficialUpdateProgress, OfficialUpdateReport, OfficialUpdateService,
|
||||
OfficialUpdateStatus, OfficialVerificationSummary, OfficialVersionState, PRIVATE_FILE_MODE,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
@@ -608,17 +608,7 @@ fn record_daemon_status(
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(error) = update_daemon_status(
|
||||
state_dir,
|
||||
update.state,
|
||||
update.last_update_status,
|
||||
update.last_error,
|
||||
update.next_retry_seconds,
|
||||
update.last_success_unix_seconds,
|
||||
update.next_check_unix_seconds,
|
||||
update.pending_scheduled_force,
|
||||
update.next_forced_refresh_at,
|
||||
) {
|
||||
if let Err(error) = update_daemon_status(state_dir, update) {
|
||||
logger.log_text("daemon", format!("更新后台状态失败:{error}"));
|
||||
}
|
||||
}
|
||||
@@ -731,10 +721,7 @@ fn classify_pid_lock_file(path: &Path) -> anyhow::Result<PidLockState> {
|
||||
if metadata.file_type().is_symlink() || !metadata.is_file() {
|
||||
return Ok(PidLockState::Corrupt);
|
||||
}
|
||||
let contents = match fs::read_to_string(path) {
|
||||
Ok(contents) => contents,
|
||||
Err(error) => return Err(error.into()),
|
||||
};
|
||||
let contents = fs::read_to_string(path)?;
|
||||
let Some(pid) = parse_pid_value(&contents) else {
|
||||
return Ok(PidLockState::Corrupt);
|
||||
};
|
||||
@@ -1322,6 +1309,8 @@ struct DaemonStatusReport {
|
||||
current_stage: Option<String>,
|
||||
current_message: Option<String>,
|
||||
download_progress: Option<DaemonDownloadProgress>,
|
||||
version_state_path: Option<PathBuf>,
|
||||
version_state: Option<OfficialVersionState>,
|
||||
pending_scheduled_force: Option<bool>,
|
||||
next_forced_refresh_unix_seconds: Option<u64>,
|
||||
command: Option<Vec<String>>,
|
||||
@@ -1600,6 +1589,14 @@ fn build_daemon_status_report(state_dir: &Path) -> anyhow::Result<DaemonStatusRe
|
||||
.unwrap_or_else(|| daemon_structured_log_path(state_dir));
|
||||
let structured_log_exists = path_exists_no_follow(&structured_log_path)?;
|
||||
let rotated_structured_log_paths = rotated_structured_log_paths(&structured_log_path);
|
||||
let version_state_path = status_file.as_ref().map(|status| {
|
||||
status
|
||||
.resource_output_root
|
||||
.join("official-version-state.json")
|
||||
});
|
||||
let version_state = version_state_path
|
||||
.as_ref()
|
||||
.and_then(|path| read_version_state(path).ok().flatten());
|
||||
|
||||
Ok(DaemonStatusReport {
|
||||
status: if running { "running" } else { "stopped" },
|
||||
@@ -1659,6 +1656,8 @@ fn build_daemon_status_report(state_dir: &Path) -> anyhow::Result<DaemonStatusRe
|
||||
download_progress: status_file
|
||||
.as_ref()
|
||||
.and_then(|status| status.download_progress.clone()),
|
||||
version_state_path,
|
||||
version_state,
|
||||
pending_scheduled_force: status_file
|
||||
.as_ref()
|
||||
.map(|status| status.pending_scheduled_force),
|
||||
@@ -1896,6 +1895,8 @@ fn print_human_json_value(value: &serde_json::Value) -> anyhow::Result<()> {
|
||||
print_json_field(value, "current_stage", "当前阶段");
|
||||
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_json_field(value, "resource_output_root", "资源目录");
|
||||
print_json_field(value, "state_dir", "状态目录");
|
||||
print_json_field(value, "socket_path", "socket");
|
||||
@@ -2102,6 +2103,7 @@ impl HumanReport for OfficialUpdateReport {
|
||||
print_path_field("输出根目录", &self.output_root);
|
||||
print_path_field("active release", &self.active_resource_root);
|
||||
print_path_field("current", &self.current_path);
|
||||
print_path_field("version state", &self.version_state_path);
|
||||
print_optional_path_field("staging", self.staging_path.as_ref());
|
||||
print_optional_path_field("published", self.published_version_path.as_ref());
|
||||
print_path_field("snapshot", &self.snapshot_path);
|
||||
@@ -2174,7 +2176,32 @@ impl HumanReport for DaemonStatusReport {
|
||||
if let Some(progress) = self.download_progress.as_ref() {
|
||||
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_optional_path_field("资源目录", self.resource_output_root.as_ref());
|
||||
print_optional_path_field("版本状态", self.version_state_path.as_ref());
|
||||
print_path_field("状态目录", &self.state_dir);
|
||||
print_path_field("socket", &self.socket_path);
|
||||
print_optional_path_field("日志", self.log_path.as_ref());
|
||||
@@ -2499,29 +2526,26 @@ struct DoctorReport {
|
||||
}
|
||||
|
||||
fn run_doctor_command(options: &CliOptions) -> anyhow::Result<bool> {
|
||||
let mut checks = Vec::new();
|
||||
checks.push(path_check(
|
||||
"state_dir",
|
||||
&options.state_dir,
|
||||
"后台状态目录可用",
|
||||
));
|
||||
checks.push(path_check(
|
||||
"output_root",
|
||||
&options.config.output_root,
|
||||
"资源输出目录可用",
|
||||
));
|
||||
checks.push(safety_check(
|
||||
"output_root_safety",
|
||||
validate_output_root(&options.config.output_root),
|
||||
"资源输出目录安全边界通过",
|
||||
));
|
||||
checks.push(safety_check(
|
||||
"state_dir_safety",
|
||||
validate_runtime_state_dir(&options.state_dir),
|
||||
"后台状态目录安全边界通过",
|
||||
));
|
||||
checks.push(command_check("curl", &options.config.curl_command));
|
||||
checks.push(command_check("unzip", &options.config.unzip_command));
|
||||
let mut checks = vec![
|
||||
path_check("state_dir", &options.state_dir, "后台状态目录可用"),
|
||||
path_check(
|
||||
"output_root",
|
||||
&options.config.output_root,
|
||||
"资源输出目录可用",
|
||||
),
|
||||
safety_check(
|
||||
"output_root_safety",
|
||||
validate_output_root(&options.config.output_root),
|
||||
"资源输出目录安全边界通过",
|
||||
),
|
||||
safety_check(
|
||||
"state_dir_safety",
|
||||
validate_runtime_state_dir(&options.state_dir),
|
||||
"后台状态目录安全边界通过",
|
||||
),
|
||||
command_check("curl", &options.config.curl_command),
|
||||
command_check("unzip", &options.config.unzip_command),
|
||||
];
|
||||
|
||||
let pid_path = daemon_pid_path(&options.state_dir);
|
||||
let daemon_running = read_pid_file(&pid_path)
|
||||
@@ -2864,17 +2888,7 @@ fn write_daemon_status_file(path: &Path, status: &DaemonStatusFile) -> anyhow::R
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_daemon_status(
|
||||
state_dir: &Path,
|
||||
state: &str,
|
||||
last_update_status: Option<String>,
|
||||
last_error: Option<String>,
|
||||
next_retry_seconds: Option<u64>,
|
||||
last_success_unix_seconds: Option<u64>,
|
||||
next_check_unix_seconds: Option<u64>,
|
||||
pending_scheduled_force: bool,
|
||||
next_forced_refresh_at: SystemTime,
|
||||
) -> anyhow::Result<()> {
|
||||
fn update_daemon_status(state_dir: &Path, update: DaemonStatusUpdate<'_>) -> anyhow::Result<()> {
|
||||
let status_path = daemon_status_path(state_dir);
|
||||
let mut status = read_daemon_status_file(&status_path)?.unwrap_or_else(|| DaemonStatusFile {
|
||||
version: DAEMON_STATUS_VERSION,
|
||||
@@ -2899,22 +2913,23 @@ fn update_daemon_status(
|
||||
command: env::args().collect(),
|
||||
});
|
||||
status.pid = std::process::id();
|
||||
status.state = state.to_string();
|
||||
status.state = update.state.to_string();
|
||||
status.updated_unix_seconds = unix_seconds_now();
|
||||
status.last_update_status = last_update_status;
|
||||
status.last_error = last_error;
|
||||
status.next_retry_seconds = next_retry_seconds;
|
||||
if last_success_unix_seconds.is_some() {
|
||||
status.last_success_unix_seconds = last_success_unix_seconds;
|
||||
status.last_update_status = update.last_update_status;
|
||||
status.last_error = update.last_error;
|
||||
status.next_retry_seconds = update.next_retry_seconds;
|
||||
if update.last_success_unix_seconds.is_some() {
|
||||
status.last_success_unix_seconds = update.last_success_unix_seconds;
|
||||
}
|
||||
status.next_check_unix_seconds = next_check_unix_seconds;
|
||||
if state != "running" {
|
||||
status.next_check_unix_seconds = update.next_check_unix_seconds;
|
||||
if update.state != "running" {
|
||||
status.current_stage = None;
|
||||
status.current_message = None;
|
||||
status.download_progress = None;
|
||||
}
|
||||
status.pending_scheduled_force = pending_scheduled_force;
|
||||
status.next_forced_refresh_unix_seconds = system_time_to_unix_seconds(next_forced_refresh_at);
|
||||
status.pending_scheduled_force = update.pending_scheduled_force;
|
||||
status.next_forced_refresh_unix_seconds =
|
||||
system_time_to_unix_seconds(update.next_forced_refresh_at);
|
||||
write_daemon_status_file(&status_path, &status)
|
||||
}
|
||||
|
||||
@@ -3130,11 +3145,11 @@ fn daemon_child_args(options: &CliOptions) -> Vec<String> {
|
||||
|
||||
fn format_duration_arg(duration: Duration) -> String {
|
||||
let millis = duration.as_millis();
|
||||
if millis % 3_600_000 == 0 {
|
||||
if millis.is_multiple_of(3_600_000) {
|
||||
format!("{}h", millis / 3_600_000)
|
||||
} else if millis % 60_000 == 0 {
|
||||
} else if millis.is_multiple_of(60_000) {
|
||||
format!("{}m", millis / 60_000)
|
||||
} else if millis % 1_000 == 0 {
|
||||
} else if millis.is_multiple_of(1_000) {
|
||||
format!("{}s", millis / 1_000)
|
||||
} else {
|
||||
format!("{millis}ms")
|
||||
@@ -3825,7 +3840,7 @@ fn parse_args_from(raw_args: impl IntoIterator<Item = String>) -> anyhow::Result
|
||||
"verify 只验证本地资源,不能和 --force 同时使用"
|
||||
));
|
||||
}
|
||||
if options.config.repair == false && options.config.audit_local == false {
|
||||
if !options.config.repair && !options.config.audit_local {
|
||||
return Err(anyhow::anyhow!(
|
||||
"verify 必须启用本地审计;不要同时传 --no-repair --no-audit-local"
|
||||
));
|
||||
@@ -3854,7 +3869,7 @@ fn ensure_command_not_set(command: CliCommand, next: &str) -> anyhow::Result<()>
|
||||
}
|
||||
|
||||
fn tools_are_non_default(config: &OfficialUpdateConfig) -> bool {
|
||||
config.curl_command != PathBuf::from("curl") || config.unzip_command != PathBuf::from("unzip")
|
||||
config.curl_command != Path::new("curl") || config.unzip_command != Path::new("unzip")
|
||||
}
|
||||
|
||||
fn next_option_value(
|
||||
@@ -4503,14 +4518,16 @@ mod tests {
|
||||
let next_check = unix_seconds_after(Duration::from_secs(30));
|
||||
update_daemon_status(
|
||||
&state_dir,
|
||||
"sleeping",
|
||||
Some("downloaded".to_string()),
|
||||
None,
|
||||
Some(30),
|
||||
Some(123456),
|
||||
Some(next_check),
|
||||
false,
|
||||
SystemTime::now() + Duration::from_secs(3600),
|
||||
DaemonStatusUpdate {
|
||||
state: "sleeping",
|
||||
last_update_status: Some("downloaded".to_string()),
|
||||
last_error: None,
|
||||
next_retry_seconds: Some(30),
|
||||
last_success_unix_seconds: Some(123456),
|
||||
next_check_unix_seconds: Some(next_check),
|
||||
pending_scheduled_force: false,
|
||||
next_forced_refresh_at: SystemTime::now() + Duration::from_secs(3600),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let report = build_daemon_status_report(&state_dir).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user