最近在處理一個自動化腳本的時候,踩到一個超級噁心的坑:OneDrive 在 macOS 上的串流模式(Files On-Demand),讓我所有的 CLI 工具全部陣亡。花了好幾天才搞清楚根本原因,順便也對 macOS 的 File Provider Extension 機制有了更深的理解。這篇把整個除錯過程和最終解法記錄下來,希望能幫到遇到同樣問題的人。
問題:Resource deadlock avoided
事情是這樣的。我需要寫一個腳本去自動讀取 OneDrive 目錄裡的檔案,做一些資料處理後再寫回去。聽起來很簡單對吧?
結果一跑就炸了:
1 | $ cat ~/Library/CloudStorage/OneDrive-xxx/some-file.xlsx |
好,cat 不行,那試試 cp:
1 | $ cp ~/Library/CloudStorage/OneDrive-xxx/some-file.xlsx /tmp/ |
dd 呢?
1 | $ dd if=~/Library/CloudStorage/OneDrive-xxx/some-file.xlsx of=/tmp/output |
全死。連 Python 也不行:
1 | f = open("/Users/xxx/Library/CloudStorage/OneDrive-xxx/some-file.xlsx", "rb") |
這個 Resource deadlock avoided 錯誤(errno 35, EDEADLK)在 Unix 世界裡通常跟 file lock 有關,但這裡的情況完全不是傳統意義上的 deadlock。
為什麼會這樣?
OneDrive 在 macOS 上使用的是 File Provider Extension 來實現串流模式。這是 Apple 從 macOS 11 開始大力推廣的機制,讓雲端儲存服務可以把檔案「虛擬化」——你在 Finder 裡看得到檔案,但實際上本機可能只有一個 placeholder,真正的內容要等你存取時才從雲端下載。
問題就在這裡:File Provider Extension 有自己的一套檔案存取協議。當你透過 Finder 雙擊開啟檔案,macOS 會自動走 File Provider 的協調機制,觸發下載、取得讀取權限,一切正常。但是!當你從 Terminal 用 cat、cp 這些 POSIX IO 工具去讀,它們是直接對 kernel 發 open() syscall,完全繞過了 File Provider 的協調機制。
kernel 一看:「這個檔案歸 File Provider Extension 管,你沒有經過正規管道就想讀,不行。」於是直接回一個 EDEADLK,意思是「你這個存取方式會造成協調上的死結」。
這也解釋了為什麼所有 POSIX-based 的工具都會失敗——它們本來就不知道 File Provider Extension 的存在。
試過的各種 workaround(和它們失敗的原因)
在找到正解之前,我試了不少歪路:
1. open -g 指令
1 | open -g ~/Library/CloudStorage/OneDrive-xxx/some-file.xlsx |
open 指令因為走的是 macOS 的 LaunchServices,所以它確實會觸發 File Provider 去下載檔案。加 -g 是在背景開啟,不會跳到前景。
問題是:它會用預設應用程式打開檔案(比如 Excel),而且你無法程式化地知道「下載完了沒」、「可以讀了沒」。就算加上 sleep 去等,也不穩定。在自動化腳本裡,這種做法完全不可靠。
2. brctl download
1 | brctl download ~/Library/CloudStorage/OneDrive-xxx/some-file.xlsx |
brctl 是 macOS 內建的工具,可以觸發雲端檔案的下載。看起來很完美對吧?
然而坑就是:**brctl 只支援 iCloud Drive**。對,它是 Apple 專門給 iCloud 用的工具,OneDrive 的 File Provider Extension 完全不吃這一套。執行後不報錯,但也不會觸發任何下載。
3. open -a "Microsoft Excel"
1 | open -a "Microsoft Excel" ~/Library/CloudStorage/OneDrive-xxx/some-file.xlsx |
指定用 Excel 開啟,確實可以觸發下載。但問題一堆:需要安裝 Excel、Excel 開啟後會鎖定檔案、多個檔案處理時 Excel 的行為不可預測、而且在 headless 環境(比如 SSH 遠端連線)根本無法使用。
4. 透過 Finder AppleScript
也試過用 AppleScript 去操作 Finder 打開檔案,想藉此觸發下載。結果一樣不穩定,而且 Finder 的 AppleScript 支援本來就很有限,錯誤處理幾乎不存在。
真正的解法:NSFileCoordinator
翻了大量 Apple 文件和開發者論壇之後,我終於找到了正確答案:NSFileCoordinator。
NSFileCoordinator 是 Apple 的 Foundation framework 裡提供的類別,專門用來協調多個 process 之間的檔案存取。關鍵是:File Provider Extension 就是透過 FileCoordination protocol 來管理檔案存取的。當你用 NSFileCoordinator 去讀寫檔案,它會自動跟 File Provider Extension 協調,觸發下載、取得存取權限,一切都走正規管道。
讀取檔案
1 | import Foundation |
重點解說:
coordinate(readingItemAt:options:error:)會跟 File Provider Extension 協調,告訴它「我要讀這個檔案」- File Provider Extension 收到請求後,會觸發雲端下載(如果還沒下載的話)
- 下載完成後,closure 裡的
newURL就是你可以安全讀取的路徑 - 在 closure 內部,用標準的
Data(contentsOf:)讀取,完全不會遇到 deadlock
寫入檔案
1 | import Foundation |
寫入也是同樣的模式,只是把 readingItemAt 換成 writingItemAt,加上 .forReplacing option 表示要覆寫檔案。File Provider Extension 會處理好上傳同步的事情。
為什麼這個方法有效?
讓我把整個機制串起來:
File Provider Extension 的設計哲學:macOS 的 File Provider 是一個 kernel-level 的檔案系統擴展,它要求所有對其管理的檔案的存取都必須透過 FileCoordination protocol 來協調。這不是 bug,是 by design。
POSIX IO 的局限:
cat、cp、Python 的open()都是直接發open()syscall,kernel 看到這是 File Provider 管理的檔案,但呼叫方沒有走 FileCoordination,所以直接拒絕。NSFileCoordinator 的角色:它是 Apple 提供的「正規管道」,會自動處理跟 File Provider Extension 的溝通、觸發下載、取得存取權限。用了它,kernel 就知道「這個存取是經過協調的」,自然放行。
Finder 為什麼可以:Finder 內部就是用 NSFileCoordinator(或等效機制)來存取檔案的,所以雙擊開啟檔案時完全正常。
注意事項與進階技巧
實際用了一段時間之後,還有幾個值得注意的地方:
超時處理:NSFileCoordinator 的 coordinate 方法是同步阻塞的——它會一直等到 File Provider Extension 完成協調才返回。如果檔案很大(幾百 MB 的影片之類)或網路很慢,等待時間可能長到令人崩潰。在自動化腳本裡,建議搭配 DispatchSemaphore 設定超時機制:
1 | let semaphore = DispatchSemaphore(value: 0) |
批次處理的眉角:如果需要一次讀取大量檔案,同一個 NSFileCoordinator 實例可以重複使用,但每個檔案仍需獨立呼叫 coordinate。要注意的是 File Provider Extension 對並發請求數量有限制,太激進的並行處理會被 throttle,甚至直接回錯誤。我的經驗是保守一點比較穩——用 serial queue 依序處理,一次不超過五到十個檔案,跑起來最穩定。
不只是 OneDrive 的問題:這個坑不只存在於 OneDrive。任何使用 macOS File Provider Extension 的雲端服務都可能遇到同樣的狀況,包括 Dropbox 和 Google Drive(取決於它們的實作方式)。所以 NSFileCoordinator 的解法是通用的,學一次就能應付所有類似的情境。
判斷是否需要走 File Provider:有時候你的腳本需要同時處理本地檔案和雲端檔案。可以透過路徑前綴來判斷——File Provider 管理的檔案通常在 ~/Library/CloudStorage/ 底下。如果路徑包含這個前綴就走 NSFileCoordinator,否則用一般的 POSIX IO 就好,不需要每個檔案都繞遠路。
包裝成實用工具
知道原理後,把它包裝成 CLI 工具就很簡單了。你可以寫一個 Swift 小程式:
1 | // fpread.swift - 讀取 File Provider 管理的檔案 |
編譯後就能在任何 shell script 裡使用:
1 | swiftc fpread.swift -o fpread |
要批次處理的話,套個 find + xargs 就搞定了。終於可以在自動化腳本裡正常讀寫 OneDrive 的檔案了。
結語
這個問題讓我深刻體會到 macOS 在檔案系統層面的變化。隨著雲端儲存越來越普遍,Apple 透過 File Provider Extension 把「本地檔案」和「雲端檔案」的界線模糊化了——在 Finder 裡看起來一模一樣,但底層的存取機制完全不同。
這種設計對一般使用者來說是透明的,但對開發者和 power user 來說,就需要理解這些底層機制。特別是當你要寫自動化腳本、CI/CD pipeline、或任何需要程式化存取雲端檔案的場景,傳統的 POSIX IO 可能已經不夠用了。
NSFileCoordinator 不是什麼新東西,它在 macOS 上已經存在很久了,原本是用來協調不同 app 之間的檔案存取。但在 File Provider Extension 的時代,它有了全新的重要性。如果你的工作會碰到 macOS 上的雲端儲存整合,強烈建議花點時間研究一下 Apple 的 File Provider 和 NSFileCoordinator 文件,會省掉很多除錯的時間。