包包baobaolin.com
date
entry
045
topic
tooling
rev
—

沒給 --user-data-dir 的 headless Chrome,被砍掉之後設定檔目錄還在

不給 --user-data-dir,headless Chrome 會自己開一個暫時的設定檔,結束時刪掉。前提是它自己結束。我實測被 timeout 砍掉的每一次,設定檔都留在 ~/Library/Application Support;用 SIGTERM 中止的那次,Chrome 回的結束碼是 0,截圖卻不存在。

我產封面圖的腳本用 headless Chrome 截圖,沒有指定 --user-data-dir(使用者資料目錄,也就是設定檔放的地方)。不指定的話,Chrome 會開一個暫時的設定檔,用完自己刪掉,所以那支腳本只清自己的暫存檔,不管 Chrome 的。

但 10 月 15 日磁碟只剩 2 GB 時,我在 ~/Library/Application Support/Google/Chrome-headless/ 找到 4 個沒人清的 scoped_dir* 目錄,日期是 8 月 20 日、10 月 8 日、10 月 13 日(兩個),最大的一個 128 MiB(本篇的大小都是 du -k 量的,以 1,024 為單位)。這篇要談的是暫時設定檔在什麼情況下會留下來,以及順帶量到的兩件事:被 SIGTERM 中止時 Chrome 的結束碼,還有哪個旗標真的能讓卡住的截圖結束。依序是:暫時設定檔放在哪、什麼時候會留下、結束碼 0 但沒有圖、卡住的原因與 --timeout,最後是我現在用的包裝腳本。

暫時設定檔放在哪

測試環境是 macOS 上的 Google Chrome 155.0.8059.41,指令照我產封面圖的腳本寫:

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --headless --disable-gpu --hide-scrollbars \
  --window-size=1200,630 --screenshot=out.png \
  --virtual-time-budget=3000 file:///…/page.html

我原本以為暫時設定檔在 $TMPDIR。執行期間每 0.2 秒列一次 $TMPDIR,只看到一個新增的 com.google.Chrome.XXXXXX,結束後就不見了。讓 Chrome 卡住、再用 ps 看它的子行程,參數裡寫的是 --user-data-dir=/Users/…/Library/Application Support/Google/Chrome-headless/scoped_dirXXXXXX,lsof 也看到它開著那個目錄底下 Default/ 裡的快取檔。所以暫時設定檔在使用者的 Application Support 裡,不在暫存目錄。

什麼時候會留下

正常結束的三次,Chrome-headless 底下的項目數都是 7 → 7,沒有增加。接著用一個會卡住的頁面(一段 while(true){} 的 JavaScript)讓它停在那裡,3 秒後用不同方式中止,每次之後等 3 秒再數:

kill -TERM   → 結束碼 0     設定檔目錄 +1
kill -KILL   → 結束碼 137   設定檔目錄 +1
kill -INT    → 結束碼 133   設定檔目錄 +1   (133 = 128 + 5,對應 SIGTRAP,原因我沒追)
timeout 3 …  → 結束碼 124   設定檔目錄 +1

四種各留下一個。跑 3 秒就被中止的那幾個,每個 5.5 到 6.7 MiB;後面有三次是跑了 20 到 25 秒才被中止,每個約 47 MiB,其中約 40 MiB 是 optimization_guide_model_store。這 40 MiB 是哪裡來的我沒查清楚。我試了 --disable-component-update,以及 --disable-features=OptimizationGuideModelDownloading,OptimizationHintsFetching,OptimizationTargetPrediction,各自指定 --user-data-dir 跑 20 秒之後,這個目錄都還是約 40 MiB。

$TMPDIR 也有殘留,但小得多。每次被中止都留下一個 com.google.Chrome.XXXXXX 目錄,du 算出來 0 KiB,裡面只有 SingletonCookie 和 SingletonSocket;第一次用 SIGTERM 中止時,另外多了一個 788 KiB、含 CRX_INSTALL 的 scoped_dir。這台機器的 $TMPDIR 裡有 72 個 .com.google.Chrome.* 檔案、32 個同樣格式的 com.google.Chrome.XXXXXX 目錄,但一般的 Chrome 也會在那裡建檔,我沒辦法分出哪些是 headless 留的。

開頭那 4 個舊目錄是怎麼留下的,我不知道;只能說它們的位置和名稱,和這次實測留下的目錄一樣。

結束碼 0,但沒有截圖

上面那張表裡最容易誤會的是第一行。這個無窮迴圈的測試裡,kill -TERM 之後 wait 拿到的結束碼是 0,指定的截圖檔不存在。我另外又跑一次確認:一樣是 0,一樣沒有檔案。如果腳本只用結束碼判斷成功,這次會被當成成功。

用 timeout 中止時看到的 124 是 timeout 自己的結束碼,表示它把指令中止了,不是 Chrome 回的。所以判斷截圖有沒有成功,至少要直接看輸出檔在不在、大小是不是 0;結束碼留著當診斷資訊。我產封面的腳本本來就有 existsSync 檢查檔案存在,這次才知道那一步不能拿掉,而且最好再加上非空的檢查。

卡住的原因:一個沒回應的請求

卡住不一定是無窮迴圈,另一種是某個資源一直沒回應。我在本機開一個只接受連線、不回任何資料的 socket,頁面放一張指向它的 <img>,外面包 25 秒的 timeout:

不加旗標                    → 25 秒被 timeout 中止,沒有截圖,設定檔目錄 +1
--virtual-time-budget=3000  → 25 秒被 timeout 中止,沒有截圖,設定檔目錄 +1
--timeout=3000              → 約 4 秒產出截圖,結束碼 0,設定檔目錄 +0

--virtual-time-budget 看起來像是「最多等 3 秒」,其實不是。Chromium 原始碼 components/headless/command_handler/headless_command_switches.cc(GitHub 鏡像的 main 分支上,這個檔案最近一次修改是 2023 年 11 月 17 日)對它的註解是:為了結果可重現,有網路請求還沒完成時,虛擬時間不會前進。請求一直不回,預算就一直沒用掉。同一個檔案對 --timeout 的註解是:在指定的毫秒數後發出停止,取消所有導覽並觸發 DOMContentLoaded。實測看到的是:它截下了當時的畫面、正常結束,暫時設定檔也刪了;DOMContentLoaded 有沒有觸發,我沒有監測。

--timeout 救不了這個無窮迴圈。用 while(true){} 那頁加 --timeout=3000,還是要靠外層的 timeout 中止,設定檔目錄一樣多一個。所以下面的包裝腳本兩層都放:--timeout 處理等不到的資源,外層的 timeout 處理 Chrome 自己停不下來的情況。

包裝腳本

既然這幾次外層中止都留下了設定檔,就自己指定一個暫時目錄,由腳本負責刪:

#!/bin/bash
# usage: shot.sh <url> <out.png> [寬,高]
set -u
CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
url=$1; out=$2; size=${3:-1200,630}
prof=$(mktemp -d "${TMPDIR:-/tmp}/shot-profile.XXXXXX") || exit 1
trap 'rm -rf "$prof"' EXIT
rm -f "$out" || exit 1
timeout -k 5 30 "$CHROME" --headless --disable-gpu --hide-scrollbars \
  --user-data-dir="$prof" --timeout=5000 \
  --window-size="$size" --screenshot="$out" "$url" >/dev/null 2>&1
rc=$?
[ -s "$out" ] || { echo "沒有截圖(結束碼 ${rc})" >&2; exit 1; }

timeout 在 macOS 上不是內建的,這台是 Homebrew 的 coreutils 9.5。它到期先送 SIGTERM,-k 5 表示 5 秒後還沒結束就改送 SIGKILL。rc 是 timeout 的結束碼,不是 Chrome 的。實測兩種情況:一般頁面產出 1200×630 的 PNG、結束碼 0;無窮迴圈的頁面 30 秒後印出「沒有截圖(結束碼 124)」、結束碼 1。兩次之後 Chrome-headless 底下都沒有多出目錄,$TMPDIR 裡也沒有 shot-profile.*。被 timeout 中止時,$TMPDIR 裡還是會多出 0 KiB 的 com.google.Chrome.*(Singleton 那個),不在 trap 清理的範圍內,我先不管它。

這支腳本還有三個限制。trap … EXIT 在腳本本身被 SIGKILL 時不會執行,那時暫時目錄一樣會留下;-s 只確認檔案非空,不保證是完整的 PNG,要更嚴可以再用 sips -g pixelWidth 讀一次尺寸;而且只要輸出檔非空,rc 不是 0 也算成功,這是我刻意的選擇(--timeout 截到的畫面我也要),不要的話就把 rc 一起檢查。另外,${rc} 的大括號不是習慣問題。第一版的訊息寫成 (chrome 結束碼 $rc),macOS 的 /bin/bash 3.2 回了 rc�: unbound variable。原因另外寫在 046。

測試留下的 11 個設定檔目錄(合計約 192 MiB)和 $TMPDIR 裡的殘留,我還沒刪;開頭那 4 個舊目錄也還在。

如果只記得一件事

在這幾次實測裡,headless Chrome 只在自己結束時刪掉暫時設定檔;被中止的每一次,~/Library/Application Support/Google/Chrome-headless/ 都多一個目錄,而 SIGTERM 那次的結束碼是 0。截圖腳本最好自己給 --user-data-dir 並用 trap 刪掉,加 --timeout 處理等不到的請求,最後檢查輸出檔,不能只看結束碼。

修訂紀錄

  1. 首次發布