MarkdowOO 開發日誌 - 檔案被覆蓋慘案
前言:慘案經過
那天我在 MarkdowOO 裡用 WordPress 模式寫了一篇文章。花時間把內容整理好,存檔時 app 從正文第一行標題自動設定為 WordPress 文章標題,一切看起來都很順利。
存完之後,我順手在另一個 tab 開了一個本地的 .md 檔案查資料。看完之後切回 WordPress 的 tab,繼續補充內容。
就在這時候,畫面頂端突然出現了一條橫幅:
「檔案已在外部變更,是否重新載入?」
我心想奇怪,明明只有我自己在用這篇文章,怎麼會有外部變更?但 MarkdowOO 這樣提示,我就反射性地點了「重新載入」。
然後——編輯器的內容整個換掉了,換掉的內容竟是我開本地端的檔案內容,心想!這Bug來的未免太.....令我想噴....一句話!
新文章才剛寫好就全都沒了!全都沒了!好吧!自已的坑自已救!

架構背景
fileWatchers 是 Map<wcId, FSWatcher>,整個 app window 只有一個 watcher,監視最後一個被 journal:readFile 讀取的本地檔案。
設計假設(單 tab 情境)
graph LR
A["📄 本地檔 A.md"] -->|readFile| W["👁 Watcher"]
W -->|externalChange| T["📝 active tab"]
實際情況(多 tab 情境)
graph LR
WP["🌐 WP Tab(active)"]
W["👁 Watcher"]
B["📄 本地檔 B.md"]
W -->|"watches"| B
W -.->|"❌ externalChange\n(錯誤觸發)"| WP
Bug 觸發流程
sequenceDiagram
actor User
participant WP as WP Tab(filePath=null)
participant Local as Local File Tab
participant MP as Main Process
participant FS as File System
Note over WP: 正在編輯 WP 文章
User->>Local: 開啟 B.md(新 tab)
Local->>MP: journal:readFile("B.md")
MP->>FS: startWatching(webContents, "B.md")
Note over MP: ⚠️ 唯一 watcher 改為監視 B.md
User->>WP: 切回 WP tab 繼續寫作
Note over MP: Watcher 仍在監視 B.md,沒有停止
FS->>MP: B.md 檔案變更事件
MP->>WP: ipc: file:externalChange("B.md")
Note over WP: ❌ 不檢查 active tab 是誰
User->>WP: 看到 banner,點「重新載入」
WP->>MP: journal:readFile("B.md")
MP-->>WP: B.md 的內容
WP->>WP: updateActiveTabById(wpTabId,{ markdown: B.md 的內容 })
Note over WP: 💀 WP 精心寫好的內容被覆蓋!
根本原因
| 問題點 | 說明 |
|---|---|
| ① Watcher 不跟著 tab 切換 | 切換 active tab 時 main process 的 watcher 不更新,繼續監視舊 tab 的檔案 |
② onFileExternalChange 不驗 tab |
收到 IPC 後直接 setExternalChangedFile(path),沒檢查 active tab 的 filePath 是否吻合 |
③ reloadFromDisk 不驗 tab |
直接讀 changedPath 內容寫入 active tab,沒有任何防護 — 這是造成資料損失的直接原因 |
修法
策略:兩層防護
不修改 main process 的 watcher 架構(複雜度高),在 renderer 兩個關鍵點加防護:
flowchart TD
E["📩 file:externalChange 事件"] --> C1{"active tab.filePath\n=== changedPath?"}
C1 -->|否 — WP/SSH tab| R1["resumeFileWatch()\n靜默恢復,不彈 banner"]
C1 -->|是 — 本地檔 tab| S["setExternalChangedFile()\n顯示 banner"]
S --> U["使用者點「重新載入」"]
U --> C2{"active tab.filePath\n=== externalChangedFile?\n(race condition 防護)"}
C2 -->|否| D["dismiss\n不更動任何 tab"]
C2 -->|是| RL["readFile → 更新 tab ✅"]
修法後的行為對照
| 情境 | 修法前 | 修法後 |
|---|---|---|
| active tab = 本地檔,該檔案變更 | ✅ 正確顯示 banner | ✅ 正確顯示 banner |
| active tab = WP tab,本地檔在後台變更 | ❌ Banner 出現,reload 後 WP 內容被覆蓋 | ✅ 靜默 resume,WP tab 完全不受影響 |
| active tab = WP tab,點「重新載入」(race condition) | ❌ WP 內容被覆蓋 | ✅ Layer 2 攔截,dismiss banner 不動 tab |
| active tab = SSH tab,本地檔在後台變更 | ❌ SSH 內容可能被覆蓋 | ✅ 同樣保護(filePath 也是 null) |
已知限制: 若使用者切換到本地檔 tab 後又切走,在 WP tab 期間發生的本地檔變更通知會被靜默忽略。切回本地檔 tab 時不會再補通知。這是可接受的 trade-off — 不損失資料比多一次通知重要。
結論
現階段這篇文章就是用 修正版的 MarkdowOO 寫的文章,看來功能是改善了!
