MapleCheng

在浩瀚的網路世界中無限潛水欸少年郎!

0%

在 macOS 上用 OneDrive 遇到「Resource deadlock avoided」?這是解法

事情是這樣的。我需要寫一個腳本去自動讀取 OneDrive 目錄裡的檔案,做一些資料處理後再寫回去。聽起來很簡單對吧?不就是 cat 讀取、python open() 處理、再 cp 寫回去嗎?

結果一跑就炸了:

1
2
$ cat ~/Library/CloudStorage/OneDrive-xxx/some-file.xlsx
cat: Resource deadlock avoided

好,cat 不行,那試試 cp⋯⋯全死。連 Python 的 open() 也不行。整個 debug 過程讓我懷疑人生。

為什麼會這樣?

後來查了 Apple 的文件才發現,問題出在 macOS 的 File Provider 架構

從 macOS 11 Big Sur 開始,Apple 引入了新的 File Provider API 來管理雲端儲存(iCloud Drive、OneDrive、Dropbox 等)。這些檔案不是「真的」存在本機,而是以 on-demand 串流 的方式呈現。當你訪問一個檔案時,File Provider 需要:

  1. 檢查檔案是否在本地有快取
  2. 如果沒有,從雲端下載
  3. 協調多個程序同時訪問的衝突
  4. 同步回寫變更到雲端

傳統的 Unix 工具(catcpdd)和直接的 open() 呼叫,完全繞過了這套協調機制。它們試圖直接讀取底層檔案,但 File Provider 不讓——於是就報 Resource deadlock avoided

這不是 bug,是 by design。Apple 就是要你乖乖用他們提供的 API。

正確的解法:NSFileCoordinator

macOS 提供了 NSFileCoordinator 這個 class,專門用來跟 File Provider 協調檔案訪問。它的概念是:

「嘿,File Provider 大哥,我要讀這個檔案了,麻煩你幫我準備好,順便告訴我真的在哪裡。」

要用這個 API,只能透過 Swift 或 Objective-C。我寫了兩個 bash wrapper script,讓命令行也能用:

讀取腳本:onedrive-read.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/usr/bin/env bash
# onedrive-read.sh — 用 NSFileCoordinator 安全讀取 OneDrive 串流檔案
# 用法:onedrive-read.sh <OneDrive 檔案路徑> [輸出路徑]
# 預設輸出到 stdout,指定輸出路徑則寫到檔案

set -euo pipefail

SRC="${1:?用法:onedrive-read.sh <檔案路徑> [輸出路徑]}"
DST="${2:-}"

if [[ -z "$DST" ]]; then
DST=$(mktemp /tmp/onedrive-read.XXXXXX)
CLEANUP=true
else
CLEANUP=false
fi

swift -e "
import Foundation
let url = URL(fileURLWithPath: \"${SRC}\")
let coordinator = NSFileCoordinator()
var error: NSError?
coordinator.coordinate(readingItemAt: url, options: [], error: &error) { newURL in
do {
let data = try Data(contentsOf: newURL)
try data.write(to: URL(fileURLWithPath: \"${DST}\"))
} catch {
fputs(\"ERROR: \(error)\n\", stderr)
exit(1)
}
}
if let error = error {
fputs(\"ERROR: \(error)\n\", stderr)
exit(1)
}
"

if [[ "$CLEANUP" == "true" ]]; then
cat "$DST"
rm -f "$DST"
fi

用法:

1
2
3
4
5
# 讀取到 stdout
bash ~/clawd/scripts/onedrive-read.sh ~/Library/CloudStorage/OneDrive-xxx/file.xlsx

# 讀取到指定檔案
bash ~/clawd/scripts/onedrive-read.sh ~/Library/CloudStorage/OneDrive-xxx/file.xlsx /tmp/output.xlsx

寫入腳本:onedrive-write.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/usr/bin/env bash
# onedrive-write.sh — 用 NSFileCoordinator 安全寫入 OneDrive 串流檔案
# 用法:onedrive-write.sh <來源檔案> <OneDrive 目標路徑>

set -euo pipefail

SRC="${1:?用法:onedrive-write.sh <來源檔案> <目標路徑>}"
DST="${2:?用法:onedrive-write.sh <來源檔案> <目標路徑>}"

swift -e "
import Foundation
let srcData = try! Data(contentsOf: URL(fileURLWithPath: \"${SRC}\"))
let dstURL = URL(fileURLWithPath: \"${DST}\")
let coordinator = NSFileCoordinator()
var error: NSError?
coordinator.coordinate(writingItemAt: dstURL, options: .forReplacing, error: &error) { newURL in
do {
try srcData.write(to: newURL)
print(\"OK - \(srcData.count) bytes written\")
} catch {
fputs(\"ERROR: \(error)\n\", stderr)
exit(1)
}
}
if let error = error {
fputs(\"ERROR: \(error)\n\", stderr)
exit(1)
}
"

用法:

1
bash ~/clawd/scripts/onedrive-write.sh /tmp/processed.xlsx ~/Library/CloudStorage/OneDrive-xxx/file.xlsx

為什麼不用 brctl?

你可能聽說過 brctl download 這個命令(用來下載 iCloud 檔案)。遺憾的是,brctl 只支援 iCloud Drive,不支援 OneDrive。每個雲端服務提供商實作 File Provider 的方式不同,OneDrive 有自己的機制。

其他注意事項

Python 也不行嗎?

對,Python 的 open() 直接呼叫底層 system call,同樣會觸發 deadlock。你必須:

  1. 用上面的 script 讀取到临时檔案
  2. Python 處理临时檔案
  3. 寫回临时檔案
  4. 用 write script 寫回 OneDrive

效能影響?

有,但不大。NSFileCoordinator 的協調過程會增加一些 overhead,但對於自動化腳本來說,正確性比那幾毫秒重要多了。

iCloud 也適用嗎?

對,iCloud Drive 也是用 File Provider,同樣的問題、同樣的解法。只是 iCloud 多了 brctl 這個後門。

結論

這個坑踩得有點冤。表面上看起來是普通的檔案操作,背後卻是 macOS 整套雲端檔案同步架構在運作。Apple 的意圖是好的——避免衝突、保證一致性——但對於習慣 Unix「一切皆檔案」哲學的開發者來說,這種抽象洩漏(leaky abstraction)真的很痛。

關鍵教訓:

  • 在 macOS 上操作 OneDrive/iCloud 檔案,不要用傳統 Unix 工具
  • 用 NSFileCoordinator 跟 File Provider 協調
  • bash + Swift inline script 是可行的 workaround

如果這篇能幫你少踩一個坑,那我的血淚就沒白流了。😂