Skip to content

Commit 87543e5

Browse files
committed
Reset broken hosts backups during cleanup
1 parent 4d259a0 commit 87543e5

4 files changed

Lines changed: 167 additions & 41 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ macOS 不再走本地交叉编译脚本,而是通过 GitHub Actions 原生构
310310
- 首次写入系统 `hosts` 前,会在 `runtime` 目录自动创建完整备份
311311
- `clean-hosts` 只删除 `linuxdo-accelerator` 自己维护的 marker block
312312
- `restore-hosts` 会用完整备份覆盖当前 `hosts`,适合停止使用后的显式恢复
313-
- `cleanup` 会优先尝试恢复首次接管前的完整备份;如果完整备份缺失、状态异常或恢复失败,会退化为仅清理 marker block,并明确提示无法完整恢复原始 `hosts`
313+
- `cleanup` 会优先尝试恢复首次接管前的完整备份;如果完整备份缺失、损坏、状态异常或恢复失败,会退化为仅清理 marker block;遇到损坏 / 异常备份时还会顺带清掉失效备份,避免后续继续卡在错误状态
314314
- Windows 下会在写入前自动处理 `hosts` 的只读属性,并在写入后恢复原始文件属性
315315
- Windows 下对 `hosts` 原子替换增加了共享冲突重试,降低杀软 / 系统进程短暂占用导致的恢复失败概率
316316
- 当前恢复范围以 `hosts` 内容与常见文件属性为主,不额外恢复自定义 ACL / owner

src/hosts.rs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use std::path::PathBuf;
44
use anyhow::{Context, Result};
55

66
use crate::config::AppConfig;
7-
use crate::hosts_store::{ensure_hosts_backup, restore_hosts_from_backup, write_hosts_content};
7+
use crate::hosts_store::{
8+
ensure_hosts_backup, restore_hosts_from_backup, validate_hosts_backup, write_hosts_content,
9+
};
810
use crate::paths::AppPaths;
911

1012
const START_MARKER: &str = "# >>> linuxdo-accelerator >>>";
@@ -88,6 +90,19 @@ pub fn restore_hosts_file(paths: &AppPaths) -> Result<()> {
8890
}
8991
}
9092

93+
pub fn validate_hosts_backup_file(paths: &AppPaths) -> Result<()> {
94+
#[cfg(target_os = "android")]
95+
{
96+
let _ = paths;
97+
return Ok(());
98+
}
99+
100+
#[cfg(not(target_os = "android"))]
101+
{
102+
validate_hosts_backup(paths, &hosts_path())
103+
}
104+
}
105+
91106
#[cfg(not(target_os = "android"))]
92107
fn render_managed_hosts(original: &str, config: &AppConfig) -> String {
93108
let newline = detect_newline(original);

src/hosts_store.rs

Lines changed: 108 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@ pub(crate) enum BackupState {
4545

4646
pub(crate) fn ensure_hosts_backup(paths: &AppPaths, source_path: &Path) -> Result<()> {
4747
match backup_state(paths) {
48-
BackupState::Ready => return Ok(()),
48+
BackupState::Ready => {
49+
validate_hosts_backup(paths, source_path)?;
50+
return Ok(());
51+
}
4952
BackupState::Inconsistent => {
5053
bail!(
5154
"hosts backup state is inconsistent; expected both {} and {}",
@@ -118,23 +121,7 @@ pub(crate) fn restore_hosts_from_backup(
118121
source_path: &Path,
119122
current_content: &str,
120123
) -> Result<()> {
121-
match backup_state(paths) {
122-
BackupState::Ready => {}
123-
BackupState::Missing => {
124-
bail!(
125-
"hosts backup is unavailable; expected both {} and {}",
126-
paths.hosts_backup_path.display(),
127-
paths.hosts_backup_meta_path.display()
128-
);
129-
}
130-
BackupState::Inconsistent => {
131-
bail!(
132-
"hosts backup state is inconsistent; expected both {} and {}",
133-
paths.hosts_backup_path.display(),
134-
paths.hosts_backup_meta_path.display()
135-
);
136-
}
137-
}
124+
validate_hosts_backup(paths, source_path)?;
138125

139126
let backup = fs::read_to_string(&paths.hosts_backup_path)
140127
.with_context(|| format!("failed to read {}", paths.hosts_backup_path.display()))?;
@@ -179,6 +166,61 @@ pub(crate) fn backup_state(paths: &AppPaths) -> BackupState {
179166
}
180167
}
181168

169+
pub(crate) fn validate_hosts_backup(paths: &AppPaths, source_path: &Path) -> Result<()> {
170+
match backup_state(paths) {
171+
BackupState::Ready => {}
172+
BackupState::Missing => {
173+
bail!(
174+
"hosts backup is unavailable; expected both {} and {}",
175+
paths.hosts_backup_path.display(),
176+
paths.hosts_backup_meta_path.display()
177+
);
178+
}
179+
BackupState::Inconsistent => {
180+
bail!(
181+
"hosts backup state is inconsistent; expected both {} and {}",
182+
paths.hosts_backup_path.display(),
183+
paths.hosts_backup_meta_path.display()
184+
);
185+
}
186+
}
187+
188+
let _ = fs::read_to_string(&paths.hosts_backup_path)
189+
.with_context(|| format!("failed to read {}", paths.hosts_backup_path.display()))?;
190+
let meta = load_backup_meta(paths)?;
191+
if !source_path_matches(source_path, &meta.source_path) {
192+
bail!(
193+
"hosts backup source path mismatch; expected {}, got {}",
194+
source_path.display(),
195+
meta.source_path
196+
);
197+
}
198+
Ok(())
199+
}
200+
201+
pub(crate) fn clear_hosts_backup(paths: &AppPaths) -> Result<()> {
202+
let backup_temp_path = temp_backup_path(&paths.hosts_backup_path);
203+
let meta_temp_path = temp_backup_path(&paths.hosts_backup_meta_path);
204+
let mut errors = Vec::new();
205+
206+
for path in [
207+
&paths.hosts_backup_path,
208+
&paths.hosts_backup_meta_path,
209+
&backup_temp_path,
210+
&meta_temp_path,
211+
] {
212+
if let Err(error) = cleanup_temp_file(path) {
213+
errors.push(format!("failed to remove {}: {error:#}", path.display()));
214+
}
215+
}
216+
217+
if errors.is_empty() {
218+
Ok(())
219+
} else {
220+
bail!(errors.join("; "));
221+
}
222+
}
223+
182224
fn load_backup_meta(paths: &AppPaths) -> Result<HostsBackupMeta> {
183225
let raw = fs::read(&paths.hosts_backup_meta_path)
184226
.with_context(|| format!("failed to read {}", paths.hosts_backup_meta_path.display()))?;
@@ -451,7 +493,8 @@ mod tests {
451493
use std::sync::atomic::{AtomicU64, Ordering};
452494

453495
use super::{
454-
ensure_hosts_backup, load_backup_meta, restore_hosts_from_backup, write_hosts_content,
496+
BackupState, backup_state, clear_hosts_backup, ensure_hosts_backup, load_backup_meta,
497+
restore_hosts_from_backup, validate_hosts_backup, write_hosts_content,
455498
};
456499
use crate::paths::AppPaths;
457500

@@ -548,6 +591,52 @@ mod tests {
548591
cleanup_test_dir(&test_dir);
549592
}
550593

594+
#[test]
595+
fn validate_hosts_backup_rejects_broken_metadata() {
596+
let test_dir = create_test_dir("validate_hosts_backup_rejects_broken_metadata");
597+
let paths = test_paths(&test_dir);
598+
std::fs::create_dir_all(&paths.runtime_dir).unwrap();
599+
let source_path = test_dir.join("hosts");
600+
601+
std::fs::write(&source_path, "original").unwrap();
602+
std::fs::write(&paths.hosts_backup_path, "backup").unwrap();
603+
std::fs::write(&paths.hosts_backup_meta_path, "{not-json").unwrap();
604+
605+
let error = validate_hosts_backup(&paths, &source_path).unwrap_err();
606+
assert!(
607+
error
608+
.to_string()
609+
.contains("failed to parse hosts backup metadata")
610+
);
611+
612+
cleanup_test_dir(&test_dir);
613+
}
614+
615+
#[test]
616+
fn clear_hosts_backup_removes_partial_and_temp_files() {
617+
let test_dir = create_test_dir("clear_hosts_backup_removes_partial_and_temp_files");
618+
let paths = test_paths(&test_dir);
619+
std::fs::create_dir_all(&paths.runtime_dir).unwrap();
620+
621+
std::fs::write(&paths.hosts_backup_path, "backup").unwrap();
622+
let backup_temp_path = paths
623+
.hosts_backup_path
624+
.with_file_name("hosts.backup.linuxdo-accelerator.tmp");
625+
let meta_temp_path = paths
626+
.hosts_backup_meta_path
627+
.with_file_name("hosts.backup.json.linuxdo-accelerator.tmp");
628+
std::fs::write(&backup_temp_path, "temp").unwrap();
629+
std::fs::write(&meta_temp_path, "temp").unwrap();
630+
631+
clear_hosts_backup(&paths).unwrap();
632+
633+
assert_eq!(backup_state(&paths), BackupState::Missing);
634+
assert!(!backup_temp_path.exists());
635+
assert!(!meta_temp_path.exists());
636+
637+
cleanup_test_dir(&test_dir);
638+
}
639+
551640
#[cfg(windows)]
552641
#[test]
553642
fn windows_retryable_replace_errors_cover_common_lock_cases() {

src/service.rs

Lines changed: 42 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ use anyhow::{Result, bail};
66

77
use crate::certs::{ensure_bundle, load_or_create_bundle};
88
use crate::config::AppConfig;
9-
use crate::hosts::{apply_hosts, backup_hosts_file, remove_hosts, restore_hosts_file};
10-
use crate::hosts_store::{BackupState, backup_state};
9+
use crate::hosts::{
10+
apply_hosts, backup_hosts_file, remove_hosts, restore_hosts_file, validate_hosts_backup_file,
11+
};
12+
use crate::hosts_store::{BackupState, backup_state, clear_hosts_backup};
1113
use crate::paths::AppPaths;
1214
use crate::platform::{
1315
ensure_elevated, ensure_loopback_alias, install_ca, is_process_running, remove_loopback_alias,
@@ -376,19 +378,38 @@ fn terminate_running_service(paths: &AppPaths) -> Result<()> {
376378

377379
fn cleanup_hosts_state(paths: &AppPaths) -> (String, Option<String>) {
378380
match backup_state(paths) {
379-
BackupState::Ready => match restore_hosts_file(paths) {
380-
Ok(()) => ("恢复原始 hosts".to_string(), None),
381-
Err(error) => match remove_hosts(paths) {
382-
Ok(()) => (
383-
"清理托管 hosts 规则(完整恢复失败)".to_string(),
384-
Some(format!(
385-
"failed to fully restore original hosts backup: {error:#}; cleanup fell back to removing the managed hosts block"
386-
)),
387-
),
381+
BackupState::Ready => match validate_hosts_backup_file(paths) {
382+
Ok(()) => match restore_hosts_file(paths) {
383+
Ok(()) => ("恢复原始 hosts".to_string(), None),
384+
Err(error) => match remove_hosts(paths) {
385+
Ok(()) => (
386+
"清理托管 hosts 规则(完整恢复失败)".to_string(),
387+
Some(format!(
388+
"failed to fully restore original hosts backup: {error:#}; cleanup fell back to removing the managed hosts block"
389+
)),
390+
),
391+
Err(remove_error) => (
392+
"hosts 未清理".to_string(),
393+
Some(format!(
394+
"failed to fully restore original hosts backup: {error:#}; fallback removal also failed: {remove_error:#}"
395+
)),
396+
),
397+
},
398+
},
399+
Err(validation_error) => match remove_hosts(paths) {
400+
Ok(()) => match clear_hosts_backup(paths) {
401+
Ok(()) => ("清理托管 hosts 规则并重置损坏的备份状态".to_string(), None),
402+
Err(clear_error) => (
403+
"清理托管 hosts 规则(备份损坏)".to_string(),
404+
Some(format!(
405+
"hosts backup is invalid: {validation_error:#}; managed hosts block was removed but clearing stale backup artifacts failed: {clear_error:#}"
406+
)),
407+
),
408+
},
388409
Err(remove_error) => (
389410
"hosts 未清理".to_string(),
390411
Some(format!(
391-
"failed to fully restore original hosts backup: {error:#}; fallback removal also failed: {remove_error:#}"
412+
"hosts backup is invalid: {validation_error:#}; fallback removal failed: {remove_error:#}"
392413
)),
393414
),
394415
},
@@ -401,14 +422,15 @@ fn cleanup_hosts_state(paths: &AppPaths) -> (String, Option<String>) {
401422
),
402423
},
403424
BackupState::Inconsistent => match remove_hosts(paths) {
404-
Ok(()) => (
405-
"清理托管 hosts 规则(备份状态异常)".to_string(),
406-
Some(format!(
407-
"hosts backup state is inconsistent; original hosts could not be fully restored (expected both {} and {})",
408-
paths.hosts_backup_path.display(),
409-
paths.hosts_backup_meta_path.display()
410-
)),
411-
),
425+
Ok(()) => match clear_hosts_backup(paths) {
426+
Ok(()) => ("清理托管 hosts 规则并重置异常备份状态".to_string(), None),
427+
Err(clear_error) => (
428+
"清理托管 hosts 规则(备份状态异常)".to_string(),
429+
Some(format!(
430+
"hosts backup state is inconsistent; managed hosts block was removed but clearing stale backup artifacts failed: {clear_error:#}"
431+
)),
432+
),
433+
},
412434
Err(error) => (
413435
"hosts 未清理".to_string(),
414436
Some(format!(

0 commit comments

Comments
 (0)