mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 04:15:14 +08:00
feat: support cancellable official sync
This commit is contained in:
Generated
+1
@@ -205,6 +205,7 @@ dependencies = [
|
|||||||
"bat-core",
|
"bat-core",
|
||||||
"blake3",
|
"blake3",
|
||||||
"hex",
|
"hex",
|
||||||
|
"libc",
|
||||||
"md-5",
|
"md-5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ async-trait.workspace = true
|
|||||||
md-5 = "0.10"
|
md-5 = "0.10"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
tempfile = "3.14"
|
tempfile = "3.14"
|
||||||
|
libc = "0.2"
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -332,6 +332,17 @@ impl OfficialResourcePullService {
|
|||||||
&self,
|
&self,
|
||||||
plan: &OfficialResourcePullPlan,
|
plan: &OfficialResourcePullPlan,
|
||||||
mut progress: impl FnMut(OfficialResourcePullProgress),
|
mut progress: impl FnMut(OfficialResourcePullProgress),
|
||||||
|
) -> Result<OfficialResourcePullReport, String> {
|
||||||
|
self.pull_with_progress_and_cancellation(plan, &mut progress, || false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes the plan, emits URL progress, and aborts between URLs when
|
||||||
|
/// `should_cancel` returns true.
|
||||||
|
pub fn pull_with_progress_and_cancellation(
|
||||||
|
&self,
|
||||||
|
plan: &OfficialResourcePullPlan,
|
||||||
|
mut progress: impl FnMut(OfficialResourcePullProgress),
|
||||||
|
mut should_cancel: impl FnMut() -> bool,
|
||||||
) -> Result<OfficialResourcePullReport, String> {
|
) -> Result<OfficialResourcePullReport, String> {
|
||||||
fs::create_dir_all(&self.output_root).map_err(|error| {
|
fs::create_dir_all(&self.output_root).map_err(|error| {
|
||||||
format!(
|
format!(
|
||||||
@@ -347,6 +358,10 @@ impl OfficialResourcePullService {
|
|||||||
let total = urls.len();
|
let total = urls.len();
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
for (offset, url) in urls.into_iter().enumerate() {
|
for (offset, url) in urls.into_iter().enumerate() {
|
||||||
|
if should_cancel() {
|
||||||
|
return Err("official resource pull interrupted by shutdown request".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
let index = offset + 1;
|
let index = offset + 1;
|
||||||
progress(OfficialResourcePullProgress::started(
|
progress(OfficialResourcePullProgress::started(
|
||||||
index,
|
index,
|
||||||
@@ -400,6 +415,10 @@ impl OfficialResourcePullService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if should_cancel() {
|
||||||
|
return Err("official resource pull interrupted by shutdown request".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
let verified_hashes = self.verify_downloaded_official_hashes(&official_hash_pairs)?;
|
let verified_hashes = self.verify_downloaded_official_hashes(&official_hash_pairs)?;
|
||||||
|
|
||||||
Ok(OfficialResourcePullReport {
|
Ok(OfficialResourcePullReport {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ use bat_adapters::official::yostar_jp::{
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::fs::{self, OpenOptions};
|
use std::fs::{self, OpenOptions};
|
||||||
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
/// Current official update snapshot schema version.
|
/// Current official update snapshot schema version.
|
||||||
@@ -426,6 +427,17 @@ impl OfficialUpdateService {
|
|||||||
&self,
|
&self,
|
||||||
config: &OfficialUpdateConfig,
|
config: &OfficialUpdateConfig,
|
||||||
mut progress: impl FnMut(OfficialUpdateProgress),
|
mut progress: impl FnMut(OfficialUpdateProgress),
|
||||||
|
) -> anyhow::Result<OfficialUpdateReport> {
|
||||||
|
self.run_with_progress_and_cancellation(config, &mut progress, || false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes one official update run, emits progress events, and aborts
|
||||||
|
/// between safe steps when `should_cancel` returns true.
|
||||||
|
pub fn run_with_progress_and_cancellation(
|
||||||
|
&self,
|
||||||
|
config: &OfficialUpdateConfig,
|
||||||
|
mut progress: impl FnMut(OfficialUpdateProgress),
|
||||||
|
mut should_cancel: impl FnMut() -> bool,
|
||||||
) -> anyhow::Result<OfficialUpdateReport> {
|
) -> anyhow::Result<OfficialUpdateReport> {
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"start",
|
"start",
|
||||||
@@ -436,6 +448,7 @@ impl OfficialUpdateService {
|
|||||||
config.auto_discover
|
config.auto_discover
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
check_shutdown_requested(&mut should_cancel)?;
|
||||||
|
|
||||||
let _lock = if config.dry_run {
|
let _lock = if config.dry_run {
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
@@ -452,6 +465,7 @@ impl OfficialUpdateService {
|
|||||||
progress(OfficialUpdateProgress::new("lock", "state lock acquired"));
|
progress(OfficialUpdateProgress::new("lock", "state lock acquired"));
|
||||||
Some(lock)
|
Some(lock)
|
||||||
};
|
};
|
||||||
|
check_shutdown_requested(&mut should_cancel)?;
|
||||||
|
|
||||||
let default_platforms = default_official_platforms();
|
let default_platforms = default_official_platforms();
|
||||||
let platforms = config
|
let platforms = config
|
||||||
@@ -481,6 +495,7 @@ impl OfficialUpdateService {
|
|||||||
&bootstrap_cache_path,
|
&bootstrap_cache_path,
|
||||||
!config.dry_run,
|
!config.dry_run,
|
||||||
&mut progress,
|
&mut progress,
|
||||||
|
&mut should_cancel,
|
||||||
)?)
|
)?)
|
||||||
} else {
|
} else {
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
@@ -489,6 +504,7 @@ impl OfficialUpdateService {
|
|||||||
));
|
));
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
check_shutdown_requested(&mut should_cancel)?;
|
||||||
|
|
||||||
let app_version = config
|
let app_version = config
|
||||||
.app_version
|
.app_version
|
||||||
@@ -557,6 +573,7 @@ impl OfficialUpdateService {
|
|||||||
"server-info",
|
"server-info",
|
||||||
format!("parsing server-info bytes={}", server_info_bytes.len()),
|
format!("parsing server-info bytes={}", server_info_bytes.len()),
|
||||||
));
|
));
|
||||||
|
check_shutdown_requested(&mut should_cancel)?;
|
||||||
let server_info = YostarJpServerInfo::from_slice(&server_info_bytes)?;
|
let server_info = YostarJpServerInfo::from_slice(&server_info_bytes)?;
|
||||||
let current_snapshot = server_info
|
let current_snapshot = server_info
|
||||||
.sync_snapshot(&connection_group, &app_version, platforms)
|
.sync_snapshot(&connection_group, &app_version, platforms)
|
||||||
@@ -576,13 +593,18 @@ impl OfficialUpdateService {
|
|||||||
marker_endpoint_count(¤t_snapshot)
|
marker_endpoint_count(¤t_snapshot)
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
let endpoint_markers =
|
let endpoint_markers = collect_endpoint_markers(
|
||||||
collect_endpoint_markers(&fetcher, ¤t_snapshot, &mut progress)?;
|
&fetcher,
|
||||||
|
¤t_snapshot,
|
||||||
|
&mut progress,
|
||||||
|
&mut should_cancel,
|
||||||
|
)?;
|
||||||
let current_update_snapshot = OfficialUpdateSnapshot::new(
|
let current_update_snapshot = OfficialUpdateSnapshot::new(
|
||||||
current_snapshot.clone(),
|
current_snapshot.clone(),
|
||||||
endpoint_markers,
|
endpoint_markers,
|
||||||
bootstrap.as_ref(),
|
bootstrap.as_ref(),
|
||||||
);
|
);
|
||||||
|
check_shutdown_requested(&mut should_cancel)?;
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"snapshot",
|
"snapshot",
|
||||||
format!("reading previous snapshot {}", snapshot_path.display()),
|
format!("reading previous snapshot {}", snapshot_path.display()),
|
||||||
@@ -617,6 +639,7 @@ impl OfficialUpdateService {
|
|||||||
platforms,
|
platforms,
|
||||||
&fetcher,
|
&fetcher,
|
||||||
&mut progress,
|
&mut progress,
|
||||||
|
&mut should_cancel,
|
||||||
)?)
|
)?)
|
||||||
} else {
|
} else {
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
@@ -684,6 +707,7 @@ impl OfficialUpdateService {
|
|||||||
should_download, repair_needed
|
should_download, repair_needed
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
check_shutdown_requested(&mut should_cancel)?;
|
||||||
let mut report = OfficialUpdateReport {
|
let mut report = OfficialUpdateReport {
|
||||||
update_status: if should_download {
|
update_status: if should_download {
|
||||||
OfficialUpdateStatus::WouldDownload
|
OfficialUpdateStatus::WouldDownload
|
||||||
@@ -765,6 +789,7 @@ impl OfficialUpdateService {
|
|||||||
));
|
));
|
||||||
return Ok(report);
|
return Ok(report);
|
||||||
}
|
}
|
||||||
|
check_shutdown_requested(&mut should_cancel)?;
|
||||||
|
|
||||||
let pull_plan = pull_plan
|
let pull_plan = pull_plan
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -775,9 +800,13 @@ impl OfficialUpdateService {
|
|||||||
format!("downloading or reusing {download_url_count} official URLs"),
|
format!("downloading or reusing {download_url_count} official URLs"),
|
||||||
));
|
));
|
||||||
let pull_report = fetcher
|
let pull_report = fetcher
|
||||||
.pull_with_progress(pull_plan, |event| {
|
.pull_with_progress_and_cancellation(
|
||||||
|
pull_plan,
|
||||||
|
|event| {
|
||||||
progress(progress_from_pull_event(event));
|
progress(progress_from_pull_event(event));
|
||||||
})
|
},
|
||||||
|
&mut should_cancel,
|
||||||
|
)
|
||||||
.map_err(anyhow::Error::msg)?;
|
.map_err(anyhow::Error::msg)?;
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"download",
|
"download",
|
||||||
@@ -793,6 +822,7 @@ impl OfficialUpdateService {
|
|||||||
"audit",
|
"audit",
|
||||||
"running final local manifest audit",
|
"running final local manifest audit",
|
||||||
));
|
));
|
||||||
|
check_shutdown_requested(&mut should_cancel)?;
|
||||||
let final_audit = fetcher
|
let final_audit = fetcher
|
||||||
.audit_local_manifest(pull_plan)
|
.audit_local_manifest(pull_plan)
|
||||||
.map_err(anyhow::Error::msg)?;
|
.map_err(anyhow::Error::msg)?;
|
||||||
@@ -833,7 +863,9 @@ fn build_pull_plan(
|
|||||||
platforms: &[PatchPlatform],
|
platforms: &[PatchPlatform],
|
||||||
fetcher: &OfficialResourcePullService,
|
fetcher: &OfficialResourcePullService,
|
||||||
progress: &mut dyn FnMut(OfficialUpdateProgress),
|
progress: &mut dyn FnMut(OfficialUpdateProgress),
|
||||||
|
should_cancel: &mut dyn FnMut() -> bool,
|
||||||
) -> anyhow::Result<OfficialResourcePullPlan> {
|
) -> anyhow::Result<OfficialResourcePullPlan> {
|
||||||
|
check_shutdown_requested(should_cancel)?;
|
||||||
let discovery = server_info
|
let discovery = server_info
|
||||||
.discovery_plan(connection_group, app_version, platforms)
|
.discovery_plan(connection_group, app_version, platforms)
|
||||||
.map_err(anyhow::Error::msg)?;
|
.map_err(anyhow::Error::msg)?;
|
||||||
@@ -846,7 +878,9 @@ fn build_pull_plan(
|
|||||||
discovery.endpoints.len()
|
discovery.endpoints.len()
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
let seed_catalogs = fetch_seed_catalogs(fetcher, &discovery, progress)?;
|
check_shutdown_requested(should_cancel)?;
|
||||||
|
let seed_catalogs = fetch_seed_catalogs(fetcher, &discovery, progress, should_cancel)?;
|
||||||
|
check_shutdown_requested(should_cancel)?;
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"inventory",
|
"inventory",
|
||||||
"parsing seed catalogs into download inventory",
|
"parsing seed catalogs into download inventory",
|
||||||
@@ -877,6 +911,7 @@ fn collect_endpoint_markers(
|
|||||||
fetcher: &OfficialResourcePullService,
|
fetcher: &OfficialResourcePullService,
|
||||||
snapshot: &YostarJpSyncSnapshot,
|
snapshot: &YostarJpSyncSnapshot,
|
||||||
progress: &mut dyn FnMut(OfficialUpdateProgress),
|
progress: &mut dyn FnMut(OfficialUpdateProgress),
|
||||||
|
should_cancel: &mut dyn FnMut() -> bool,
|
||||||
) -> anyhow::Result<Vec<OfficialEndpointMarkerSnapshot>> {
|
) -> anyhow::Result<Vec<OfficialEndpointMarkerSnapshot>> {
|
||||||
let mut markers = Vec::new();
|
let mut markers = Vec::new();
|
||||||
|
|
||||||
@@ -884,6 +919,7 @@ fn collect_endpoint_markers(
|
|||||||
let Some(role) = marker_role(endpoint.kind) else {
|
let Some(role) = marker_role(endpoint.kind) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
check_shutdown_requested(should_cancel)?;
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"marker",
|
"marker",
|
||||||
format!(
|
format!(
|
||||||
@@ -906,9 +942,20 @@ fn collect_endpoint_markers(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_shutdown_requested(should_cancel)?;
|
||||||
Ok(markers)
|
Ok(markers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check_shutdown_requested(should_cancel: &mut dyn FnMut() -> bool) -> anyhow::Result<()> {
|
||||||
|
if should_cancel() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"official update interrupted by shutdown request"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn marker_role(kind: YostarJpResourceEndpointKind) -> Option<OfficialEndpointMarkerRole> {
|
fn marker_role(kind: YostarJpResourceEndpointKind) -> Option<OfficialEndpointMarkerRole> {
|
||||||
match kind {
|
match kind {
|
||||||
YostarJpResourceEndpointKind::TableCatalogHash
|
YostarJpResourceEndpointKind::TableCatalogHash
|
||||||
@@ -1124,7 +1171,9 @@ fn resolve_bootstrap(
|
|||||||
cache_path: &Path,
|
cache_path: &Path,
|
||||||
write_cache: bool,
|
write_cache: bool,
|
||||||
progress: &mut dyn FnMut(OfficialUpdateProgress),
|
progress: &mut dyn FnMut(OfficialUpdateProgress),
|
||||||
|
should_cancel: &mut dyn FnMut() -> bool,
|
||||||
) -> anyhow::Result<ResolvedBootstrap> {
|
) -> anyhow::Result<ResolvedBootstrap> {
|
||||||
|
check_shutdown_requested(should_cancel)?;
|
||||||
let launcher = OfficialLauncherBootstrapService::with_curl_command(
|
let launcher = OfficialLauncherBootstrapService::with_curl_command(
|
||||||
launcher_version,
|
launcher_version,
|
||||||
curl_command.to_string_lossy().to_string(),
|
curl_command.to_string_lossy().to_string(),
|
||||||
@@ -1136,6 +1185,7 @@ fn resolve_bootstrap(
|
|||||||
let (game_config, manifest_url, manifest) = launcher
|
let (game_config, manifest_url, manifest) = launcher
|
||||||
.fetch_latest_remote_manifest()
|
.fetch_latest_remote_manifest()
|
||||||
.map_err(anyhow::Error::msg)?;
|
.map_err(anyhow::Error::msg)?;
|
||||||
|
check_shutdown_requested(should_cancel)?;
|
||||||
let launcher_metadata =
|
let launcher_metadata =
|
||||||
launcher_metadata_from_parts(launcher_version, &game_config, &manifest_url, &manifest);
|
launcher_metadata_from_parts(launcher_version, &game_config, &manifest_url, &manifest);
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
@@ -1170,12 +1220,14 @@ fn resolve_bootstrap(
|
|||||||
"game-main-config",
|
"game-main-config",
|
||||||
"cache miss; fetching resources.assets and parsing GameMainConfig",
|
"cache miss; fetching resources.assets and parsing GameMainConfig",
|
||||||
));
|
));
|
||||||
|
check_shutdown_requested(should_cancel)?;
|
||||||
let bootstrapper = OfficialGameMainConfigBootstrapService::with_commands(
|
let bootstrapper = OfficialGameMainConfigBootstrapService::with_commands(
|
||||||
launcher_version,
|
launcher_version,
|
||||||
curl_command.to_path_buf(),
|
curl_command.to_path_buf(),
|
||||||
unzip_command.to_path_buf(),
|
unzip_command.to_path_buf(),
|
||||||
);
|
);
|
||||||
let bootstrap = bootstrapper.fetch_bootstrap().map_err(anyhow::Error::msg)?;
|
let bootstrap = bootstrapper.fetch_bootstrap().map_err(anyhow::Error::msg)?;
|
||||||
|
check_shutdown_requested(should_cancel)?;
|
||||||
let launcher_metadata = LauncherMetadataSnapshot {
|
let launcher_metadata = LauncherMetadataSnapshot {
|
||||||
launcher_version: launcher_version.to_string(),
|
launcher_version: launcher_version.to_string(),
|
||||||
game_latest_version: bootstrap.game_config.game_latest_version.clone(),
|
game_latest_version: bootstrap.game_config.game_latest_version.clone(),
|
||||||
@@ -1219,6 +1271,7 @@ fn fetch_seed_catalogs(
|
|||||||
fetcher: &OfficialResourcePullService,
|
fetcher: &OfficialResourcePullService,
|
||||||
discovery: &YostarJpResourceDiscoveryPlan,
|
discovery: &YostarJpResourceDiscoveryPlan,
|
||||||
progress: &mut dyn FnMut(OfficialUpdateProgress),
|
progress: &mut dyn FnMut(OfficialUpdateProgress),
|
||||||
|
should_cancel: &mut dyn FnMut() -> bool,
|
||||||
) -> anyhow::Result<SeedCatalogs> {
|
) -> anyhow::Result<SeedCatalogs> {
|
||||||
let mut catalogs = SeedCatalogs::default();
|
let mut catalogs = SeedCatalogs::default();
|
||||||
|
|
||||||
@@ -1232,6 +1285,7 @@ fn fetch_seed_catalogs(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_shutdown_requested(should_cancel)?;
|
||||||
progress(OfficialUpdateProgress::new(
|
progress(OfficialUpdateProgress::new(
|
||||||
"catalog",
|
"catalog",
|
||||||
format!(
|
format!(
|
||||||
@@ -1247,6 +1301,7 @@ fn fetch_seed_catalogs(
|
|||||||
catalogs.insert(endpoint, bytes)?;
|
catalogs.insert(endpoint, bytes)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_shutdown_requested(should_cancel)?;
|
||||||
Ok(catalogs)
|
Ok(catalogs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1338,31 +1393,82 @@ impl OfficialUpdateLock {
|
|||||||
fn acquire(config: &OfficialUpdateConfig) -> anyhow::Result<Self> {
|
fn acquire(config: &OfficialUpdateConfig) -> anyhow::Result<Self> {
|
||||||
fs::create_dir_all(&config.output_root)?;
|
fs::create_dir_all(&config.output_root)?;
|
||||||
let path = config.lock_path();
|
let path = config.lock_path();
|
||||||
|
for attempt in 0..=1 {
|
||||||
match OpenOptions::new().write(true).create_new(true).open(&path) {
|
match OpenOptions::new().write(true).create_new(true).open(&path) {
|
||||||
Ok(file) => {
|
Ok(mut file) => {
|
||||||
let pid = std::process::id().to_string();
|
let pid = std::process::id().to_string();
|
||||||
use std::io::Write;
|
|
||||||
let mut file = file;
|
|
||||||
file.write_all(pid.as_bytes())?;
|
file.write_all(pid.as_bytes())?;
|
||||||
Ok(Self { path })
|
return Ok(Self { path });
|
||||||
}
|
}
|
||||||
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => Err(
|
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||||
anyhow::anyhow!("official update output is locked: {}", path.display()),
|
if attempt == 0 && remove_stale_lock(&path)? {
|
||||||
),
|
continue;
|
||||||
Err(error) => Err(anyhow::anyhow!(
|
}
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"official update output is locked: {}",
|
||||||
|
path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
"failed to acquire official update lock {}: {error}",
|
"failed to acquire official update lock {}: {error}",
|
||||||
path.display()
|
path.display()
|
||||||
)),
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Err(anyhow::anyhow!(
|
||||||
|
"failed to acquire official update lock {}",
|
||||||
|
path.display()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Drop for OfficialUpdateLock {
|
impl Drop for OfficialUpdateLock {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
let _ = fs::remove_file(&self.path);
|
let _ = fs::remove_file(&self.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn remove_stale_lock(path: &Path) -> anyhow::Result<bool> {
|
||||||
|
let contents = fs::read_to_string(path).unwrap_or_default();
|
||||||
|
let Some(pid) = parse_lock_pid(&contents) else {
|
||||||
|
return Ok(false);
|
||||||
|
};
|
||||||
|
if process_exists(pid) {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::remove_file(path)?;
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_lock_pid(contents: &str) -> Option<u32> {
|
||||||
|
contents.trim().parse::<u32>().ok().filter(|pid| *pid > 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn process_exists(pid: u32) -> bool {
|
||||||
|
let Ok(pid) = libc::pid_t::try_from(pid) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if pid <= 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
libc::kill(pid, 0) == 0
|
||||||
|
|| std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
fn process_exists(_pid: u32) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -1539,4 +1645,45 @@ mod tests {
|
|||||||
drop(first);
|
drop(first);
|
||||||
assert!(OfficialUpdateLock::acquire(&config).is_ok());
|
assert!(OfficialUpdateLock::acquire(&config).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_dry_run_lock_removes_stale_pid_file() {
|
||||||
|
let temp = tempfile::TempDir::new().unwrap();
|
||||||
|
let config = OfficialUpdateConfig {
|
||||||
|
output_root: temp.path().to_path_buf(),
|
||||||
|
..OfficialUpdateConfig::default()
|
||||||
|
};
|
||||||
|
fs::create_dir_all(&config.output_root).unwrap();
|
||||||
|
fs::write(config.lock_path(), "999999999").unwrap();
|
||||||
|
|
||||||
|
let lock = OfficialUpdateLock::acquire(&config).unwrap();
|
||||||
|
assert!(config.lock_path().exists());
|
||||||
|
drop(lock);
|
||||||
|
assert!(!config.lock_path().exists());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_run_can_be_cancelled_before_network_work() {
|
||||||
|
let temp = tempfile::TempDir::new().unwrap();
|
||||||
|
let config = OfficialUpdateConfig {
|
||||||
|
output_root: temp.path().to_path_buf(),
|
||||||
|
dry_run: true,
|
||||||
|
..OfficialUpdateConfig::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut checks = 0;
|
||||||
|
let error = OfficialUpdateService::new()
|
||||||
|
.run_with_progress_and_cancellation(
|
||||||
|
&config,
|
||||||
|
|_| {},
|
||||||
|
|| {
|
||||||
|
checks += 1;
|
||||||
|
true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
assert!(error.to_string().contains("shutdown request"));
|
||||||
|
assert_eq!(checks, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user