Что произойдет, если вы отредактируете скрипт во время выполнения?

31

У меня есть общий вопрос, который может быть результатом неправильного понимания того, как процессы обрабатываются в Linux.

Для моих целей я собираюсь определить «скрипт» как фрагмент кода bash, сохраненного в текстовом файле с разрешениями на выполнение, активированными для текущего пользователя.

У меня есть серия сценариев, которые вызывают друг друга в тандеме. Для простоты я назову их сценариями A, B и C. Сценарий A выполняет серию операторов и затем делает паузу, затем выполняет сценарий B, затем делает паузу, затем выполняет сценарий C. Другими словами, серия шагов это что-то вроде этого:

Запустите сценарий A:

  1. Серия заявлений
  2. Пауза
  3. Запустить скрипт B
  4. Пауза
  5. Запустить скрипт C

По своему опыту я знаю, что если я буду запускать сценарий A до первой паузы, а затем вносить изменения в сценарий B, эти изменения отражаются в выполнении кода, когда я позволяю ему возобновить работу. Аналогичным образом, если я внесу изменения в сценарий C, пока сценарий A все еще приостановлен, а затем разрешу его продолжить после сохранения изменений, эти изменения будут отражены в выполнении кода.

Вот реальный вопрос, есть ли способ отредактировать скрипт A, пока он еще работает? Или редактирование невозможно после начала его выполнения?

CaffeineConnoisseur
источник
2
Я думаю, это зависит от оболочки. хотя вы утверждаете, что используете bash. Похоже, это будет зависеть от того, как оболочка загружает скрипты внутри.
Струджи
Поведение может также измениться, если вы используете исходный файл вместо его выполнения.
Струджи
1
Я думаю, что bash читает весь сценарий в память перед выполнением.
w4etwetewtwet
2
@ handuel, нет, это не так. Как будто он не ждет, пока вы не наберете «exit» в приглашении, чтобы начать интерпретацию введенных вами команд.
Стефан Шазелас
1
@StephaneChazelas Да, чтение из терминала не делает, но это отличается от запуска скрипта.
w4etwetewtwet

Ответы:

21

В Unix большинство редакторов работают, создавая новый временный файл, содержащий отредактированное содержимое. Когда отредактированный файл сохранен, исходный файл удаляется, а временный файл переименовывается в исходное имя. (Конечно, существуют различные меры предосторожности для предотвращения потери данных.) Это, например, стиль, используемый sedили perlкогда вызывается с -iфлагом («на месте»), который на самом деле вообще не «на месте». Надо было называть «новое место со старым именем».

Это хорошо работает, потому что Unix гарантирует (по крайней мере для локальных файловых систем), что открытый файл продолжает существовать до тех пор, пока он не будет закрыт, даже если он «удален» и создан новый файл с тем же именем. (Это не случайно, что системный вызов unix для «удаления» файла фактически называется «unlink».) Так что, вообще говоря, если интерпретатор оболочки имеет открытый исходный файл, и вы «редактируете» файл, как описано выше оболочка даже не увидит изменений, так как у нее по-прежнему открыт исходный файл.

[Примечание: как и во всех комментариях, основанных на стандартах, вышеизложенное допускает многократное толкование, и существуют различные примеры, такие как NFS. Педанты могут заполнить комментарии исключениями.]

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

Итак, если вы хотите редактировать файл во время его обработки оболочкой, у вас есть два варианта:

  1. Вы можете добавить в файл. Это всегда должно работать.

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

Я не знаю ни одной формулировки в стандарте Posix, которая фактически требует возможности добавления к файлу сценария во время его выполнения, так что он может работать не с каждой оболочкой, совместимой с Posix, тем более с текущим предложением почти и иногда послеродовые оболочки. Итак, YMMV. Но, насколько я знаю, он действительно надежно работает с Bash.

В качестве доказательства приведем реализацию «без петель» печально известной программы на 99 бутылок пива в bash, которая используется ddдля перезаписи и добавления (перезапись предположительно безопасна, поскольку она заменяет текущую исполняемую строку, которая всегда является последней строкой файл с комментарием точно такой же длины; я сделал это для того, чтобы конечный результат мог быть выполнен без изменения поведения.)

#!/bin/bash
if [[ $1 == reset ]]; then
  printf "%s\n%-16s#\n" '####' 'next ${1:-99}' |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^#### $0 | cut -f1 -d:) bs=1 2>/dev/null
  exit
fi

step() {
  s=s
  one=one
  case $beer in
    2) beer=1; unset s;;
    1) beer="No more"; one=it;;
    "No more") beer=99; return 1;;
    *) ((--beer));;
  esac
}
next() {
  step ${beer:=$(($1+1))}
  refrain |
  dd if=/dev/stdin of=$0 seek=$(grep -bom1 ^next\  $0 | cut -f1 -d:) bs=1 conv=notrunc 2>/dev/null
}
refrain() {
  printf "%-17s\n" "# $beer bottles"
  echo echo ${beer:-No more} bottle$s of beer on the wall, ${beer:-No more} bottle$s of beer.
  if step; then
    echo echo Take $one down, pass it around, $beer bottle$s of beer on the wall.
    echo echo
    echo next abcdefghijkl
  else
    echo echo Go to the store, buy some more, $beer bottle$s of beer on the wall.
  fi
}
####
next ${1:-99}   #
RICi
источник
Когда я запускаю это, оно начинается с «No more», затем продолжается до -1 и бесконечно отрицательные числа.
Даниэль Гершкович
Если я делаю export beer=100до запуска сценария, он работает как ожидалось.
Даниэль Гершкович
@DanielHershcovich: совершенно верно; неаккуратное тестирование с моей стороны. Я думаю, что я это исправил; теперь он принимает необязательный параметр count. Лучшим и более интересным решением будет автоматический сброс настроек, если параметр не соответствует кэшированной копии.
Ричи
18

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

Например, в:

cmd1
cmd2

Оболочка будет читать сценарий по блокам, поэтому, скорее всего, прочитает обе команды, интерпретирует первую, а затем обратится к концу cmd1сценария и снова прочитает сценарий, чтобы прочитать cmd2и выполнить его.

Вы можете легко проверить это:

$ cat a
echo foo | dd 2> /dev/null bs=1 seek=50 of=a
echo bar
$ bash a
foo

(хотя, глядя на straceрезультаты этого, кажется, что он делает некоторые более причудливые вещи (например, читает данные несколько раз, выполняет поиск назад ...), чем когда я пытался сделать то же самое несколько лет назад, так что моё утверждение выше об обратном больше не распространяется на новые версии).

Однако, если вы напишите свой скрипт как:

{
  cmd1
  cmd2
  exit
}

Оболочка должна будет прочитать до закрытия }, сохранить это в памяти и выполнить его. Из-за этого exitоболочка не будет снова читать сценарий, поэтому вы можете безопасно редактировать его, пока оболочка интерпретирует его.

В качестве альтернативы, при редактировании сценария убедитесь, что вы написали новую копию сценария. Оболочка будет продолжать читать исходную (даже если она удалена или переименована).

Чтобы сделать это, переименовать the-scriptв the-script.oldи копировать the-script.oldв the-scriptи редактировать его.

Стефан Шазелас
источник
4

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

Часто, когда сценарий изменяется во время выполнения, оболочка заканчивает тем, что сообщает о синтаксических ошибках. Это связано с тем, что, когда оболочка закрывает и снова открывает файл сценария, она использует смещение байтов в файле, чтобы изменить положение при возврате.

ясень
источник
4

Вы можете обойти это, установив ловушку на вашем скрипте, а затем используя, execчтобы подобрать новое содержимое скрипта. Обратите внимание, однако, что execвызов запускает скрипт с нуля, а не с того места, где он был достигнут в процессе выполнения, и поэтому будет вызван скрипт B (и так далее).

#! /bin/bash

CMD="$0"
ARGS=("$@")

trap reexec 1

reexec() {
    exec "$CMD" "${ARGS[@]}"
}

while : ; do sleep 1 ; clear ; date ; done

Это будет продолжать отображать дату на экране. Затем я мог бы отредактировать свой сценарий и изменить dateна echo "Date: $(date)". При записи этого работающего скрипта все равно просто отображается дата. Однако, если я отправлю сигнал, который я установил trapдля захвата, сценарий будет exec(заменяет текущий запущенный процесс указанной командой), который является командой $CMDи аргументами $@. Вы можете сделать это, введя kill -1 PID- где PID - это PID запущенного скрипта - и выходные данные изменятся, чтобы показать Date:перед dateвыводом команды.

Вы можете сохранить «состояние» вашего скрипта во внешнем файле (например, в / tmp) и прочитать его содержимое, чтобы узнать, где «возобновить» выполнение программы при повторном выполнении. Затем можно добавить дополнительное завершение прерываний (SIGINT / SIGQUIT / SIGKILL / SIGTERM), чтобы очистить этот файл tmp, чтобы при перезапуске после прерывания «Сценария A» он начинался с самого начала. Версия с состоянием будет что-то вроде:

#! /bin/bash

trap reexec 1
trap cleanup 2 3 9 15

CMD="$0"
ARGS=("$@")
statefile='/tmp/scriptA.state'
EXIT=1

reexec() { echo "Restarting..." ; exec "$CMD" "${ARGS[@]}"; }
cleanup() { rm -f $statefile; exit $EXIT; }
run_scriptB() { /path/to/scriptB; echo "scriptC" > $statefile; }
run_scriptC() { /path/to/scriptC; echo "stop" > $statefile;  }

while [ "$state" != "stop" ] ; do

    if [ -f "$statefile" ] ; then
        state="$(cat "$statefile")"
    else
        state='starting'
    fi

    case "$state" in
        starting)         
            run_scriptB
        ;;
        scriptC)
            run_scriptC
        ;;
    esac
done

EXIT=0
cleanup
Драв Слоан
источник
Я исправил эту проблему, захватив $0и $@в начале скрипта и используя execвместо этого эти переменные .
Драв Слоан