Подтвердить что ты не робот

Является ли это действительным методом самообновления для bash script?

Я работаю над script, который стал настолько сложным. Я хочу включить простой вариант для обновления его до последней версии. Это мой подход:

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
}

script, похоже, работает так, как предполагалось, но мне интересно, могут ли быть оговорки с таким подходом. Мне просто трудно полагать, что script может перезаписать себя без каких-либо последствий.

Чтобы быть более ясным, мне интересно, может быть, bash будет читать и выполнять script по очереди, а после mv, exit 0 может быть чем-то еще из новый script. Я думаю, что я помню, как 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
}
4b9b3361

Ответ 1

(По крайней мере, он не пытается продолжать работу после самого обновления!)

То, что заставляет меня нервничать по поводу вашего подхода, заключается в том, что вы перезаписываете текущий script (mv $0.tmp $0) по мере его запуска. Существует ряд причин, по которым это, вероятно, будет работать, но я бы не стал делать большие ставки, которые гарантировали бы работу при любых обстоятельствах. Я ничего не знаю в POSIX или любом другом стандарте, который указывает, как оболочка обрабатывает файл, который он выполняет как script.

Вот что, вероятно, произойдет:

Вы выполняете script. Ядро видит строку #!/bin/sh (вы ее не показывали, но я предполагаю это там) и вызывает /bin/sh с именем вашего script в качестве аргумента. Затем оболочка использует fopen() или, возможно, open(), чтобы открыть ваш script, читает его и начинает интерпретировать его содержимое в виде команд оболочки.

При достаточно малом script оболочка, вероятно, просто считывает все это в память, явно или как часть буферизации, выполняемой обычным файловым вводом-выводом. Для более крупного script он может читать его в кусках по мере его выполнения. Но в любом случае он, вероятно, только открывает файл один раз и сохраняет его открытым до тех пор, пока он выполняется.

Если вы удаляете или переименовываете файл, фактический файл не обязательно сразу же удаляется с диска. Если есть еще одна жесткая ссылка на него или если какой-то процесс открыт, файл продолжает существовать, хотя может быть невозможно, чтобы другой процесс открыл его под тем же именем или вообще. Файл физически не удаляется до тех пор, пока последняя ссылка (запись в каталоге), ссылающаяся на нее, не будет удалена, и никакие процессы не будут открыты. (Даже тогда его содержимое не будет немедленно удалено, но это выходит за рамки того, что здесь важно.)

Кроме того, команда mv, которая захватывает файл script, сразу же следует exit 0.

НО, по крайней мере, возможно, что оболочка может закрыть файл, а затем снова открыть его по имени. Я не могу придумать никаких оснований для этого, но я не знаю абсолютной гарантии, что это не будет.

И некоторые системы имеют тенденцию выполнять более строгую блокировку файлов, что и большинство систем Unix. В Windows, например, я подозреваю, что команда mv завершится неудачно, потому что процесс (оболочка) открывает файл. Ваш script может выйти из строя на Cygwin. (Я не пробовал.)

Итак, что заставляет меня нервничать, - это не столько малая вероятность, что это может потерпеть неудачу, но и длинная и тонкая линия рассуждений, которая, кажется, демонстрирует, что она, вероятно, преуспеет, и очень реальная возможность того, что есть что-то еще, думал.

Мое предложение: напишите второй script, чья единственная задача - обновить первую. Поместите функцию runSelfUpdate() или эквивалентный код в этот script. В исходном script используйте exec для вызова обновления script, чтобы исходный script больше не запускался при его обновлении. Если вы хотите избежать проблем с обслуживанием, распространением и установкой двух отдельных скриптов. вы можете создать оригинальное script создать обновление script с уникальным в /tmp; что также решит проблему обновления обновления script. (Я бы не стал беспокоиться о том, как очистить обновленное обновление script в /tmp, что просто откроет ту же самую червь червей.)

Ответ 2

Да, но... Я бы рекомендовал вам сохранить более многоуровневую версию вашей истории script, если удаленный хост также не может выполнять контроль версий с историями. При этом, чтобы ответить непосредственно на код, который вы опубликовали, см. Следующие комментарии: -)

Что происходит с вашей системой, когда wget имеет икоту, спокойно перезаписывает часть вашего рабочего script только с частичной или иной коррумпированной копией? Следующий шаг делает 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

Надеюсь, это поможет.

Ответ 3

Очень поздний ответ здесь, но поскольку я просто решил это тоже, я подумал, что это может помочь кому-то опубликовать такой подход:

#!/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}" "[email protected]"
}
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 "[email protected]"
else
  # update myself and invoke updated version
  trap clean_up EXIT
  update_self_and_invoke "[email protected]"
fi