最近在嘗試用 RPA 來取代例行的行政工作。
我用的架構是 Vercel + next.js,自動化網頁腳本 puppeteer-core 無頭瀏覽器 @sparticuz/chromium-min 。
但實務上遇到一個問題是,如果一個請求內併發多個實例。就會發生spawn ETXTBSY (Text File Busy)的錯誤。
單一個實例工作流程會是
- 下載:下載瀏覽器的壓縮檔案
- 解壓縮:解壓縮寫入到暫存目錄 /tmp/chromium
- 執行:puppeteer 啟動瀏覽器
而 @sparticuz/chromium 套件的 executablePath() 會檢查目錄底下是否已經有瀏覽器,若有的話就會直接進入到 3 執行的階段。
當有多個實例並行時,第一個實例只執行到 2 的階段,正在寫入檔案中。由於一個 chromium-min 也有數百 MB,而寫入的過程是漸進式的,檔案會被先創建再持續寫入個幾秒鐘瀏覽器才算真的完成。
同時第二實例發現當前目錄已經有瀏覽器了(但其實第一個實例的寫入尚未完成)就拿去執行,這時就會發生錯誤,因為檔案正處於被寫入的忙碌狀態。
解決方案:
不管有多少個實例只要其中一個開始下載,其他實例也就共用相同的下載動作(只有一次)。
當下載動作完成時,所有的實例都會拿到相同的路徑。就不會發生其他實例去執行到還沒壓縮完的檔案。
let cachedExecutablePath: string | null = null;
let downloadPromise: Promise<string> | null = null;
async function getChromiumPath(): Promise<string> {
// Return cached path if available
if (cachedExecutablePath) return cachedExecutablePath;
// Prevent concurrent downloads by reusing the same promise
if (!downloadPromise) {
const chromium = (await import("@sparticuz/chromium-min")).default;
downloadPromise = chromium
.executablePath(
"https://github.com/Sparticuz/chromium/releases/download/v129.0.0/chromium-v129.0.0-pack.tar"
)
.then((path) => {
cachedExecutablePath = path;
console.log("Chromium path resolved:", path);
return path;
})
.catch((error) => {
console.error("Failed to get Chromium path:", error);
downloadPromise = null; // Reset on error to allow retry
throw error;
});
}
return downloadPromise;
}




