我有一个正在运行“补丁”脚本的板。补丁脚本始终在后台运行,它是一个运行以下伪代码的 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