这是bash脚本的有效自我更新方法吗?

时间:2011-12-21 20:14:19

标签: bash

我正在编写一个已经变得如此复杂的脚本我希望包含一个简单的选项来将其更新到最新版本。这是我的方法:

set -o errexit

SELF=$(basename $0)
UPDATE_BASE=http://something

runSelfUpdate() {
  echo "Performing self-update..."
  # Download new version
  wget --quiet --output-document=$0.tmp $UPDATE_BASE/$SELF
  # Copy over modes from old version
  OCTAL_MODE=$(stat -c '%a' $0)
  chmod $OCTAL_MODE $0.tmp
  # Overwrite old file with new
  mv $0.tmp $0
  exit 0
}

该脚本似乎按预期工作,但我想知道是否可能有这种方法的警告。我很难相信脚本可以在不受任何影响的情况下覆盖自己。

为了更清楚,我想知道,如果,也许,bash会逐行阅读并执行脚本,而在mv之后,exit 0可能是其他内容。新脚本。我想我记得Windows的行为与.bat个文件类似。

更新:我的原始代码段未包含set -o errexit。根据我的理解,这应该使我免受wget引起的问题的影响 此外,在这种情况下,UPDATE_BASE指向版本控制下的位置(以缓解问题)。

结果:根据这些答案的输入,我构建了这个修订后的方法:

runSelfUpdate() {
  echo "Performing self-update..."

  # Download new version
  echo -n "Downloading latest version..."
  if ! wget --quiet --output-document="$0.tmp" $UPDATE_BASE/$SELF ; then
    echo "Failed: Error while trying to wget new version!"
    echo "File requested: $UPDATE_BASE/$SELF"
    exit 1
  fi
  echo "Done."

  # Copy over modes from old version
  OCTAL_MODE=$(stat -c '%a' $SELF)
  if ! chmod $OCTAL_MODE "$0.tmp" ; then
    echo "Failed: Error while trying to set mode on $0.tmp."
    exit 1
  fi

  # Spawn update script
  cat > updateScript.sh << EOF
#!/bin/bash
# Overwrite old file with new
if mv "$0.tmp" "$0"; then
  echo "Done. Update complete."
  rm \$0
else
  echo "Failed!"
fi
EOF

  echo -n "Inserting update process..."
  exec /bin/bash updateScript.sh
}

3 个答案:

答案 0 :(得分:5)

(至少它在尝试更新后不会继续运行!)

让我对你的方法感到紧张的是你正在运行时覆盖当前脚本(mv $0.tmp $0。这可能可能的原因有多种原因,但我绝不会打赌它可以保证在所有情况下都能正常工作。我不知道POSIX或任何其他标准中的任何内容,它指定shell如何处理它作为脚本执行的文件。

以下是可能会发生的事情:

您执行脚本。内核看到#!/bin/sh行(你没有显示它,但我认为它就在那里)并调用/bin/sh并将脚本的名称作为参数。然后,shell使用fopen()open()来打开脚本,从中读取,并开始将其内容解释为shell命令。

对于一个足够小的脚本,shell可能只是显式地或者作为普通文件I / O完成的缓冲的一部分将整个内容读入内存。对于更大的脚本,它可能会在执行时以块的形式读取它。但不管怎样,它可能只打开文件一次,并且只要它正在执行就保持打开状态。

如果删除或重命名文件,则不一定会立即从磁盘中删除实际文件。如果有另一个硬链接,或者某个进程打开它,则该文件仍然存在,即使另一个进程可能无法再使用相同的名称打开它,或者根本不可能。在删除引用它的最后一个链接(目录条目)之前,文件不会被物理删除,没有进程打开它。 (即便如此,其内容也不会立即被删除,但这超出了相关内容。)

此外,破坏脚本文件的mv命令紧跟exit 0

但是至少可以想象shell可以关闭文件,然后按名称重新打开它。我想不出有什么好的理由这样做,但我知道不会绝对保证它不会。

有些系统倾向于对大多数Unix系统进行更严格的文件锁定。例如,在Windows上,我怀疑mv命令会失败,因为进程(shell)打开了文件。您的脚本可能在Cygwin上失败。 (我没试过。)

所以让我感到紧张的不是它可能失败的可能性小,而是长期而微弱的推理线,这似乎表明它可能会成功,而且真正的可能性是我还有其他东西'想到了。

我的建议:编写第二个脚本,其唯一的工作是更新第一个脚本。将runSelfUpdate()函数或等效代码放入该脚本中。在原始脚本中,使用exec调用更新脚本,以便在更新原始脚本时不再运行该脚本。如果您想避免维护,分发和安装两个单独脚本的麻烦。您可以在/tmp中使用唯一的原始脚本创建更新脚本;这也将解决更新更新脚本的问题。 (我不担心在/tmp中清理自动生成的更新脚本;这只会重新打开同样的蠕虫病毒。)

答案 1 :(得分:4)

是的,但是......我建议您保留脚本历史记录的更多分层版本,除非远程主机也可以使用历史记录执行版本控制。话虽如此,要直接回复您发布的代码,请参阅以下注释; - )

当wget出现打嗝时,系统会发生什么情况,只用部分或其他损坏的副本悄悄地覆盖部分工作脚本?您的下一步是mv $0.tmp $0,因此您丢失了工作版本。 (我希望你能在遥控器上进行版本控制!)

您可以检查wget是否返回任何错误消息

 if ! wget --quiet --output-document=$0.tmp $UPDATE_BASE/$SELF ; then
    echo "error on wget on $UPDATE_BASE/$SELF" 
    exit 1
 fi

此外,经验法则测试将有所帮助,即

if (( $(wc -c < $0.tmp) >= $(wc -c < $0) )); then 
    mv $0.tmp $0
fi

但几乎不是万无一失。

如果您的$ 0可以在其中包含空格,最好包围所有引用,例如"$0"

要成为超级子弹证明,请考虑检查所有命令返回并且Octal_Mode具有合理的值

  OCTAL_MODE=$(stat -c '%a' $0)
  case ${OCTAL_MODE:--1} in
      -[1] ) 
        printf "Error : OCTAL_MODE was empty\n"
        exit 1
     ;;       
     777|775|755 ) : nothing ;;
     * ) 
        printf "Error in OCTAL_MODEs, found value=${OCTAL_MODE}\n"
        exit 1
     ;;         
  esac

  if  ! chmod $OCTAL_MODE $0.tmp ; then
    echo "error on chmod $OCTAL_MODE %0.tmp from $UPDATE_BASE/$SELF, can't continue" 
    exit 1
 fi

我希望这会有所帮助。

答案 2 :(得分:1)

这里的答案非常晚,但正如我刚刚解决了这个问题,我认为可能会有人发布这种方法:

#!/usr/bin/env bash
#
set -fb

readonly THISDIR=$(cd "$(dirname "$0")" ; pwd)
readonly MY_NAME=$(basename "$0")
readonly FILE_TO_FETCH_URL="https://your_url_to_downloadable_file_here"
readonly EXISTING_SHELL_SCRIPT="${THISDIR}/somescript.sh"
readonly EXECUTABLE_SHELL_SCRIPT="${THISDIR}/.somescript.sh"

function get_remote_file() {
  readonly REQUEST_URL=$1
  readonly OUTPUT_FILENAME=$2
  readonly TEMP_FILE="${THISDIR}/tmp.file"
  if [ -n "$(which wget)" ]; then
    $(wget -O "${TEMP_FILE}"  "$REQUEST_URL" 2>&1)
    if [[ $? -eq 0 ]]; then
      mv "${TEMP_FILE}" "${OUTPUT_FILENAME}"
      chmod 755 "${OUTPUT_FILENAME}"
    else
      return 1
    fi
  fi
}
function clean_up() {
  # clean up code (if required) that has to execute every time here
}
function self_clean_up() {
  rm -f "${EXECUTABLE_SHELL_SCRIPT}"
}

function update_self_and_invoke() {
  get_remote_file "${FILE_TO_FETCH_URL}" "${EXECUTABLE_SHELL_SCRIPT}"
  if [ $? -ne 0 ]; then
    cp "${EXISTING_SHELL_SCRIPT}" "${EXECUTABLE_SHELL_SCRIPT}"
  fi
  exec "${EXECUTABLE_SHELL_SCRIPT}" "$@"
}
function main() {
  cp "${EXECUTABLE_SHELL_SCRIPT}" "${EXISTING_SHELL_SCRIPT}"
  # your code here
} 

if [[ $MY_NAME = \.* ]]; then
  # invoke real main program
  trap "clean_up; self_clean_up" EXIT
  main "$@"
else
  # update myself and invoke updated version
  trap clean_up EXIT
  update_self_and_invoke "$@"
fi