O que acontece se você editar um script durante a execução?

28

Eu tenho uma pergunta geral, que pode ser resultado de uma má compreensão de como os processos são tratados no Linux.

Para os meus objetivos, vou definir um 'script' como um fragmento do código bash salvo em um arquivo de texto com permissões de execução ativadas para o usuário atual.

Eu tenho uma série de scripts que chamam uns aos outros em conjunto. Para simplificar, eu os chamarei de scripts A, B e C. O Script A executa uma série de instruções e, em seguida, faz uma pausa; em seguida, executa o script B, pausa e executa o script C. Em outras palavras, a série de passos é algo assim:

Executar script A:

  1. Série de declarações
  2. Pausa
  3. Executar script B
  4. Pausa
  5. Executar script C

Eu sei por experiência que, se eu executar o script A até a primeira pausa e, em seguida, fizer as edições no script B, essas edições serão refletidas na execução do código quando eu permitir que ele seja retomado. Da mesma forma, se eu fizer edições no script C enquanto o script A ainda estiver em pausa e permitir que continue depois de salvar as alterações, essas alterações serão refletidas na execução do código.

Aqui está a verdadeira questão, existe alguma maneira de editar o Script A enquanto ele ainda está rodando? Ou é impossível editar uma vez que sua execução começa?

    
por CaffeineConnoisseur 28.08.2013 / 05:37

4 respostas

20

No Unix, a maioria dos editores trabalha criando um novo arquivo temporário contendo o conteúdo editado. Quando o arquivo editado é salvo, o arquivo original é excluído e o arquivo temporário é renomeado para o nome original. (Existem, é claro, várias salvaguardas para evitar perda de dados.) Esse é, por exemplo, o estilo usado por sed ou perl quando invocado com o sinalizador -i ("in-place"), que não é realmente "no local" em tudo. Deveria ter sido chamado "novo lugar com nome antigo".

Isso funciona bem porque o unix assegura (pelo menos para sistemas de arquivos locais) que um arquivo aberto continua a existir até ser fechado, mesmo que seja "excluído" e um novo arquivo com o mesmo nome seja criado. (Não é coincidência que a chamada do sistema unix para "excluir" um arquivo seja na verdade chamada de "unlink".) Assim, em geral, se um interpretador de shell tiver algum arquivo de origem aberto, você "editará" o arquivo da maneira descrita acima , o shell nem verá as alterações, pois ainda tem o arquivo original aberto.

[Nota: como com todos os comentários baseados em padrões, o acima está sujeito a múltiplas interpretações e há vários casos-chave, como o NFS. Pedantes são bem-vindos para preencher os comentários com exceções.]

É claro que é possível modificar arquivos diretamente; simplesmente não é muito conveniente para fins de edição, porque enquanto você pode sobregravar dados em um arquivo, você não pode excluir ou inserir sem alterar todos os dados a seguir, o que implicaria bastante reescrita. Além disso, enquanto você estava fazendo essa mudança, o conteúdo do arquivo seria imprevisível e os processos que tinham o arquivo aberto sofreriam. Para fugir disso (como acontece com os sistemas de banco de dados, por exemplo), você precisa de um conjunto sofisticado de protocolos de modificação e bloqueios distribuídos; coisas que estão além do escopo de um utilitário típico de edição de arquivos.

Então, se você quiser editar um arquivo enquanto ele está sendo processado por um shell, você tem duas opções:

  1. Você pode acrescentar ao arquivo. Isso deve sempre funcionar.

  2. Você pode sobrescrever o arquivo com novos conteúdos de exatamente o mesmo tamanho . Isso pode ou não funcionar, dependendo se o shell já leu essa parte do arquivo ou não. Como a maioria das I / O de arquivo envolve buffers de leitura, e como todos os shells que conheço lêem um comando composto inteiro antes de executá-lo, é bem improvável que você consiga fugir disso. Certamente não seria confiável.

Eu não conheço nenhum texto no padrão Posix que realmente exija a possibilidade de anexar a um arquivo de script enquanto o arquivo está sendo executado, então ele pode não funcionar com todos os shell compatíveis com Posix, muito menos com o atual de conchas quase e às vezes posix-compliant. Então YMMV. Mas, tanto quanto sei, funciona de forma confiável com o bash.

Como evidência, aqui está uma implementação "loop-free" do infame programa 99 bottles of beer no bash, que usa dd para sobrescrever e anexar (a sobrescrita é presumivelmente segura porque substitui a linha atualmente em execução, que é sempre a última linha do arquivo, com um comentário exatamente do mesmo tamanho, eu fiz isso para que o resultado final pudesse ser executado sem o comportamento de auto-modificação.)

#!/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}   #
    
por 28.08.2013 / 19:08
17

bash ajuda muito a ler os comandos antes de executá-los.

Por exemplo, em:

cmd1
cmd2

O shell lerá o script por blocos, provavelmente lerá os dois comandos, interpretará o primeiro e, em seguida, voltará para o final de cmd1 no script e lerá o script novamente para ler cmd2 e executá-lo.

Você pode verificar isso facilmente:

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

(embora olhando para a saída strace disso, parece que faz algumas coisas mais extravagantes (como ler os dados várias vezes, procurar de volta ...) do que quando eu tentei o mesmo alguns anos atrás, então meu declaração acima sobre lseeking back pode não se aplicar mais em versões mais recentes).

Se, no entanto, você escrever seu script como:

{
  cmd1
  cmd2
  exit
}

O shell terá que ler até o fechamento } , armazenar isso na memória e executá-lo. Por causa do exit , o shell não lerá o script novamente para que você possa editá-lo com segurança enquanto o shell estiver interpretando-o.

Como alternativa, ao editar o script, certifique-se de escrever uma nova cópia do script. O shell continuará lendo o original (mesmo que seja excluído ou renomeado).

Para isso, renomeie the-script para the-script.old e copie the-script.old para the-script e edite-o.

    
por 28.08.2013 / 08:06
4

Não há realmente uma maneira segura de modificar o script enquanto ele está em execução, pois o shell pode usar o armazenamento em buffer para ler o arquivo. Além disso, se o script for modificado, substituindo-o por um novo arquivo, os shells normalmente só lerão o novo arquivo depois de executar determinadas operações.

Muitas vezes, quando um script é alterado durante a execução, o shell acaba relatando erros de sintaxe. Isso se deve ao fato de que, quando o shell fecha e reabre o arquivo de script, ele usa o deslocamento de byte no arquivo para se reposicionar no retorno.

    
por 28.08.2013 / 15:31
4

Você poderia contornar isso definindo uma armadilha no seu script e, em seguida, usando exec para selecionar o novo conteúdo do script. No entanto, observe que a chamada exec inicia o script a partir do zero e não a partir de onde ele chegou no processo em execução e, portanto, o script B será chamado (assim por diante).

#! /bin/bash

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

trap reexec 1

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

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

Isso continuará exibindo a data na tela. Eu poderia editar meu script e altere date para echo "Date: $(date)" . Ao escrever isso, o script em execução ainda exibe a data. Como sempre, se eu enviar o sinal que eu defino o trap para capturar, o script irá exec (substitui o processo atual em execução pelo comando especificado), que é o comando $CMD e os argumentos $@ . Você pode fazer isso emitindo kill -1 PID - onde PID é o PID do script em execução - e a saída muda para mostrar Date: antes da saída do comando date .

Você pode armazenar o "estado" do seu script em um arquivo externo (digamos / tmp) e ler o conteúdo para saber onde "resumir" no quando o programa for reexecutado. Você poderia então adicionar uma terminação adicional de traps (SIGINT / SIGQUIT / SIGKILL / SIGTERM) para limpar o arquivo tmp, então quando você reiniciar após a interrupção do "Script A", ele será iniciado desde o início. Uma versão com informações de estado seria algo como:

#! /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
    
por 28.08.2013 / 06:47