Skip to content

fix: 使用原子写入防止重启时配置丢失#233

Open
trustedinster wants to merge 11 commits into
SECTL:masterfrom
trustedinster:fix/issue-231-atomic-write-data-loss
Open

fix: 使用原子写入防止重启时配置丢失#233
trustedinster wants to merge 11 commits into
SECTL:masterfrom
trustedinster:fix/issue-231-atomic-write-data-loss

Conversation

@trustedinster
Copy link
Copy Markdown
Contributor

问题

Closes #231

重启(正常或异常)后再次启动可能弹出程序 OOBE,原有配置丢失。

根因分析

settings.json 等配置文件使用非原子写入(open("w") + json.dump()),当系统在写入过程中崩溃或断电时,文件会被截断或损坏。应用启动时检测到损坏的配置文件,将其视为空配置,重新创建默认设置并触发 OOBE。

修复内容

1. 新增原子写入工具函数(path_utils.py

  • atomic_write_json() — 原子写入 JSON 文件
  • atomic_write_bytes() — 原子写入二进制文件

采用 tempfile.mkstemp() + os.replace() 模式:

  • 先写入临时文件,再通过 os.replace() 原子替换目标文件
  • 写入后调用 f.flush() + os.fsync() 确保数据落盘
  • POSIX 上 os.replace() 是原子操作;Windows NTFS 上也是近似原子的
  • 异常时自动清理临时文件

2. 替换所有关键配置文件的非原子写入

文件 函数 变更
settings_access.py update_settings() open+json.dumpatomic_write_json
settings_default.py manage_settings_file() 3处写入均替换
settings_default.py ensure_device_uuid() 写入替换
config.py import_settings() 写入替换
config.py _save_drawn_records() 写入替换
secure_store.py write_secrets() 主路径改为 atomic_write_bytes,保留降级逻辑
secure_store.py write_behind_scenes_settings() 同上
file_utils.py save_history_data() 写入替换

3. 新增备份恢复机制(settings_default.py

新增 _try_recover_settings_from_backup() 函数:当 settings.json 损坏时,优先从最近的备份 zip 中恢复配置,仅在无可用备份时才回退到默认值。

测试

  • ruff check 全部通过
  • 原子写入在 Linux 和 Windows 上均可正常工作

trustedinster and others added 11 commits April 4, 2026 17:05
确保抽奖结果中的学生ID能够正确从候选数据中提取并传递到奖品信息中
同时修正抽奖结果显示时学生ID的获取逻辑
当无法附加到共享内存时,增加服务器状态检测和资源清理逻辑
修复本地服务器启动失败时未清理残留socket的问题
在POSIX系统上,QSharedMemory在进程崩溃后不会自动清理残留的共享内存段
添加_cleanup_stale_shared_memory函数来检测并清理这些残留资源
修复服务器无响应检查的描述不准确问题,改为更明确的"服务器不可连接"
改进奖品ID处理逻辑,增加异常捕获和多种ID字段尝试
通过ruff移除未使用的time导入
统一文档中的换行格式,移除多余的空格
问题:重启(正常或异常)后再次启动可能弹出程序 OOBE,原有配置丢失。
原因:settings.json 等配置文件使用非原子写入(open + json.dump),
当系统在写入过程中崩溃或断电时,文件会被截断或损坏,导致应用
启动时将损坏的配置文件视为空配置,重新创建默认设置并触发 OOBE。

修复:
1. 在 path_utils.py 中新增 atomic_write_json() 和 atomic_write_bytes()
   工具函数,采用 tempfile + os.replace() 原子写入模式,确保写入
   过程中崩溃不会损坏目标文件
2. 将 settings_access.py、settings_default.py、config.py、
   secure_store.py、file_utils.py 中所有关键配置文件的写入操作
   替换为原子写入
3. 在 settings_default.py 的 manage_settings_file() 中增加备份恢复
   机制:当 settings.json 损坏时,优先从最近的备份 zip 中恢复配置,
   仅在无可用备份时才回退到默认值

Closes SECTL#231
Comment thread app/tools/settings_default.py
Comment on lines +149 to +167
except Exception as e:
logger.warning(f"写入安全配置失败:{p}, 错误:{e}")
try:
import tempfile

with tempfile.NamedTemporaryFile(
mode="wb", delete=False, dir=os.path.dirname(p)
) as tmp_file:
tmp_file.write(b"SRV1" + payload)
tmp_path = tmp_file.name

# 替换原文件
os.replace(tmp_path, p)
raw = json.dumps(d, ensure_ascii=False, indent=4).encode("utf-8")
comp = zlib.compress(raw, level=6)
key = _platform_key()
payload = _encrypt_payload(comp, key)
with open(p, "wb") as f:
f.write(b"SRV1" + payload)
_set_hidden(str(p))
logger.debug(f"使用临时文件写入安全配置成功:{p}")
except Exception as temp_e:
logger.warning(f"使用临时文件写入安全配置也失败:{temp_e}")
# 降级到明文JSON写入
logger.debug(f"降级直接写入安全配置成功:{p}")
except Exception as e2:
logger.warning(f"降级写入安全配置也失败:{e2}")
try:
with open(p, "w", encoding="utf-8") as f:
json.dump(d, f, ensure_ascii=False, indent=4)
logger.warning(f"写入安全配置降级为明文JSON:{p}")
except Exception as e2:
logger.warning(f"降级写入明文JSON也失败:{e2}")
except Exception as e3:
logger.warning(f"降级写入明文JSON也失败:{e3}")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里其实有个跟这个patch不太相关的点,就是说,你的当前实现都是三次对同一个文件写入……
问题在于,当加密配置文件被占用,按照Python的性能估计,这三个backup全炸,全都是epermdenied,根本就没有回旋余地,其他的settings.json同理,所以考虑是否提示用户解决权限问题?

@chenjintang-shrimp
Copy link
Copy Markdown
Contributor

@sourcery-ai review

@SourceryAI
Copy link
Copy Markdown

Hi @chenjintang-shrimp! 👋

Only authors and team members can run @sourcery-ai commands on public repos.

If you are a team member, install the @sourcery-ai bot to get access ✨

@chenjintang-shrimp
Copy link
Copy Markdown
Contributor

@trustedinster 请先解决合并冲突,解决完毕后我会合并

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

重启偶现数据丢失

3 participants