fix(stores): 配置文件形状不符先备份再回退;日志尾部同步落盘

- commands/connections/settings 三个 store 原先只对 JSON.parse 抛错做
  .bak 备份,合法 JSON 但形状不符(如 [1,2,3])会静默回退空/默认,下次
  写盘把命令库/书签凭据/自定义主题整体销毁。现统一为形状不符也先备份
  (与 knownHosts 的 fail-closed 策略对齐),三个 store 测试补对应用例
- sanitizeRules 回退分支返回预设副本而非模块级共享数组引用,并在回退时
  记录 warning(原先连警告都没有)
- 会话日志尾部:logStop 触发的最后一轮 flush 改同步落盘(在途异步写未
  落地时由 drain 循环做保序的最终同步轮),before-quit 不再丢日志尾巴
This commit is contained in:
Bill committed 2026-10-09 11:31:06 +08:00
1 parent e23010f950
commit 172cce1962
6 files changed
+253 -19

No files matched your search

+47 -10
View File
@@ -202,11 +202,23 @@ function refreshBuiltinRules(rules: HighlightRule[], warnings: Warnings): Highli
return [...refreshed, ...missing.map((rule) => ({ ...rule }))].sort((a, b) => a.priority - b.priority)
}
/**
* Fresh copies of the preset rules for a caller that may mutate what it gets
* back. Handing out the module-level array (or its objects) would let one
* store's edit leak into every later load in the process.
*/
function copyDefaultRules(): HighlightRule[] {
return DEFAULT_HIGHLIGHT_RULES.map((rule) => ({ ...rule }))
}
function sanitizeRules(value: unknown, warnings: Warnings): HighlightRule[] {
// An explicit empty array is a valid choice ("no highlighting"); only
// malformed data falls back to the built-in rules. Returning the defaults for
// [] made deleting the last rule look like it silently failed.
if (!Array.isArray(value)) return DEFAULT_HIGHLIGHT_RULES
if (!Array.isArray(value)) {
warnings.push('highlightRules: not an array — built-in rules restored')
return copyDefaultRules()
}
const rules: HighlightRule[] = []
value.forEach((entry, index) => {
const rule = coerceRule(entry, index, warnings)
@@ -457,22 +469,47 @@ function backupUnparseableSettings(err: unknown): void {
}
}
/**
* The defaults loadSettings falls back to. Fresh objects every call so a caller
* mutating what it got back cannot poison the module-level presets.
*/
function defaultSettings(): AppSettings {
return {
terminal: { ...DEFAULT_SETTINGS.terminal },
customThemes: [...DEFAULT_SETTINGS.customThemes],
highlightRules: copyDefaultRules(),
highlightProfiles: [],
system: { ...DEFAULT_SYSTEM },
lock: { ...DEFAULT_SETTINGS.lock }
}
}
/**
* The single exit for an unusable settings.json — parse failure and wrong shape
* both end here, so neither can quietly skip the backup.
*/
function discardUnparseableSettings(err: unknown): AppSettings {
backupUnparseableSettings(err)
return defaultSettings()
}
export function loadSettings(): AppSettings {
try {
const raw: unknown = JSON.parse(readFileSync(settingsPath(), 'utf8'))
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
// Valid JSON of the wrong shape (`[1,2,3]`, `"text"`, `null`) is not "no
// settings": it is a file we cannot read, and the next mutation would
// rewrite it from the defaults — taking the custom themes and highlight
// rules with it. Per-field repair below only applies to a real object.
return discardUnparseableSettings(
new Error('settings.json shape mismatch: expected an object')
)
}
const { settings, errors } = deepMerge(raw)
reportWarnings(errors)
return settings
} catch (err) {
backupUnparseableSettings(err)
return {
terminal: { ...DEFAULT_SETTINGS.terminal },
customThemes: [...DEFAULT_SETTINGS.customThemes],
highlightRules: DEFAULT_HIGHLIGHT_RULES.map((rule) => ({ ...rule })),
highlightProfiles: [],
system: { ...DEFAULT_SYSTEM },
lock: { ...DEFAULT_SETTINGS.lock }
}
return discardUnparseableSettings(err)
}
}