
我有一個正在運行“補丁”腳本的板。補丁腳本始終在後台運行,它是一個運行以下偽代碼的 shell 腳本:
while true; do
# checks if a patch tar file exists and if yes then do patching
sleep 10
done
腳本位於/opt/patch.sh,由SystemV init 腳本啟動。
問題是,當腳本找到 tar 時,它會提取它,並且裡面有一個名為的 shell 腳本補丁文件這是特定於 tar 的內容。
當腳本位於/opt/patch.sh找到 tar 它會執行以下操作:
tar -xf /opt/update.tar -C /mnt/update
mv /mnt/update/patch.sh /opt/patch.sh
exec /opt/patch.sh
它用另一個腳本替換自身並從同一位置執行它。這樣做會出現什麼問題嗎?
答案1
如果檔案被就地寫入取代(inode 保持不變),則任何開啟該檔案的進程會在從該檔案讀取時看到新資料。如果透過取消舊檔案的連結並建立一個同名的新檔案來取代它,則索引節點號會發生變化,並且任何保持該檔案開啟的程序仍將具有老的文件。
mv
可能會執行任一操作,具體取決於檔案系統之間是否發生移動...為了確保獲得全新的文件,請先取消連結或重新命名原始文件。像這樣的東西:
mv /opt/patch.sh /opt/patch.sh.old # or rm
mv /mnt/update/patch.sh /opt/patch.sh
這樣,即使在移動之後,正在運行的 shell 仍將擁有舊資料的檔案句柄。
也就是說,據我測試,Bash 在執行任何循環之前都會讀取整個循環,因此只要執行保持在循環內,對底層檔案的任何更改都不會改變正在運行的腳本。 (它必須在執行之前讀取整個循環,因為最後可能會影響整個循環的重定向。)
退出循環後,Bash 將讀取指標移回循環結束後的位置,然後從循環結束後的位置繼續讀取輸入檔。
腳本中定義的任何函數也會載入到記憶體中,因此將腳本的主要邏輯放入函數中,並且僅在最後調用它將使腳本非常安全,不會對檔案進行修改:
#!/bin/sh
main() {
do_stuff
exit
}
main
不管怎樣,測試腳本被覆蓋時會發生什麼並不難:
$ cat > old.sh <<'EOF'
#!/bin/bash
for i in 1 2 3 4 ; do
# rm old.sh
cat new.sh > old.sh
sleep 1
echo $i
done
echo will this be reached?
EOF
$ cat > new.sh <<'EOF'
#!/bin/bash
echo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
echo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
echo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
echo xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
EOF
$ bash old.sh
註解掉後rm old.sh
,腳本將就地更改。如果沒有註釋,將建立一個新文件。 (此範例部分依賴new.sh
大於old.sh
,就好像它更短一樣,shell 的讀取位置將超過循環後新腳本的末尾。)
答案2
我以前遇到過這個問題,並且可以確認這可能是一個問題。就我而言,回歸腳本首先執行 git pull,並可能在開始運行後進行更新,從而導致問題。
問題通常是 shell 會傳回並檢查是否還有更多行需要解釋。即使所需的程式碼位於循環內,這也可能會導致錯誤。為了避免這種情況,請使用以下結構這個帖子。
答案3
一個自動執行、自動修改的腳本?這不是一個好主意。
更好的解決方案是創建一個具有最少功能的存根守護程序(即負責安裝新版本的從屬腳本並定期調用它)。類似...(未測試)
while true; do
# check if a patch tar file exists and if yes then do patching
if [ -f "$PATCH" ]; then
( cd /usr/local/mydaemon \
&& tar -xzf "$PATCH" \
&& rm -f "$PATCH" ) \
|| exit -1
fi
$SCRIPT
sleep 10
done